Repository navigation
Releases: distronode-corporation/district-core-swift
Release list
8.1.0
The shared core for District AI for iPhone 3.2.0 and its call Live Activity. Additive only; nothing is removed or renamed.
CallsRepository.registerLiveActivity(workspaceId:callId:token:)(POST /api/district/calls/{callId}/live-activity): registers a call's Live Activity push token, so the server can update the Lock Screen, the Dynamic Island and the Apple Watch Smart Stack.CallsRepository.registerLiveActivityStartToken(workspaceId:token:)(POST /api/district/live-activity/start-token): registers the device's push-to-start token, so a ring can start the Live Activity on a locked phone.- Two contract fixtures, byte-identical to the service's.
- The api-error codes from #41 (
engine_unavailable,workflow_member_push_roles_invalid) with reviewed French.
Full notes: CHANGELOG.md [8.1.0].
8.0.0
The shared core for the Apple Watch round 2 of District AI for iPhone, iPad and Mac: a
person's decline reaches the server, a supervisor can whisper to the agent, and a
notification extension can name a message's sender. Nothing is removed or renamed, and it is a
major version because it is source-breaking (the pull request template's rule):
public enums gain a case (IncomingCallEvent.withdrawnLocally,
IncomingCallCommand.requestDecline, RingCancelReason.declined), so an exhaustive switch
over the inbound command list (both apps' IncomingCallModel) or over a
DesktopRingEnd.cancelled(_:) reason (District AI for Mac's ringStopped) needs the new arm
when the pin moves.
Added
DistrictEndpoints.declineCall(callId:workspaceId:)(POST /api/district/calls/{id}/decline
with{workspaceId}) andInboundCallRepository.decline(callId:workspaceId:), decoding
SuccessResponse. Never retried.DistrictEndpoints.whisper(workspaceId:callId:text:)(POST /api/district/calls/whisper
with{workspaceId, callId, whisper}) andCallsRepository.whisper(workspaceId:callId:text:),
decodingSuccessResponse. Never retried; a refusal keeps the server'scode.DistrictEndpoints.notifyToken()(POST /api/auth/native/notify-tokenwith{}),
NotifyTokenResponse(its token redacted from every description and the mirror) and
NotifyTokenRepository.mint().DistrictEndpoints.messageSender(workspaceId:id:)(GET /api/district/messages/{id}/sender),
MessageSenderResponseandInboxRepository.messageSender(workspaceId:messageId:), which
answers the trimmed name, or nil for a thread with no named contact.EndpointIDcasesdeclineCall,whisper,notifyTokenandmessageSender, typed from
their first commit (TypedEndpoints.liveCall,TypedEndpoints.notifyRead). The endpoint
total is 124 and the typed count 122.contracts/mobile/district-call-decline.json,district-message-sender.jsonand
district-notify-token.json, from the service's contract, gated inContractFixtureTests
(ContractManifest.expectedFixtureCountis 190).RingCancelReason.declined, the ring cancel the server sends once every member it rang has
declined. Any other unknown value still reads aswithdrawn.
Changed
- ⛔ A person's decline is now reported (Sean's decision, 2026-10-24, GitLab #2 note
4083623386).IncomingCallControllerappends.requestDecline(workspace:call:)to the
shared ending when.declinePressedarrives while ringing, so the server can end the ring
and the agent takes a message at once. A timeout, a withdrawal and a missing session still
send nothing: the apps'withdrawandendForNoSessionpaths feed the new
.withdrawnLocally, which ends the call exactly as.declinePresseddid and emits no
request.testDecliningAndTimingOutEmitByteIdenticalCommandListsis replaced by tests that
pin both halves. - Comments that said an expired ring "falls back to PSTN" now say the agent takes a message.
7.1.0
The shared core for District AI for iPhone, iPad and Mac after 3.0.3. One public type is
added; nothing is removed or renamed.
Added
SchedulingWallClock, a working-hours time field's two directions:fieldText(_:)shows a
storedHH:MMas « 9 h », « 9 h 30 » or « 17 h » in French (unchanged09:00in English),
andwireValue(_:)turns a typed9 h,9 h 30,9h30,9:30or09:30into the
zero-paddedHH:MMthe scheduler requires, or nil for garbage or a clock outside
00:00...23:59. The apps' Edit working hours sheet showed the wire value « 09:00 » on a
French screen. Tested inSchedulingWallClockTests.
7.0.3
The shared core of District AI for iPhone, iPad and Mac 3.0.3. Nothing public is added,
removed or renamed.
Changed
contracts/mobile/district-voice-studio.json,contracts/i18n/api-errors.jsonand
contracts/i18n/wire-vocabulary.jsonfrom monorepo ac4071d14, byte for byte: the Claude
Haiku 5.5 brain entry, 44 new error codes (knowledge_url_*,member_phone_*,
transfer_*,workflow_*), the portal's disposition labels, and new workflow triggers, an
action and reason codes. Five of those reason codes (no-devices,no-message,
push-lookup-failed,push-not-delivered,signing-unavailable) now have a label of their
own in English and French, where they read as the code itself before ("No devices"). Their
catalog entries and accessors are generated (scripts/gen-l10n.py), and the 61 new French
entries are reviewed, each as the contract words it. The contract's French for the
dispositionsbookedandvoicemailis now « Rendez-vous pris » and « Boîte vocale », the
words this catalog already had.api_error.transfer_managed_onlysays « est accessible aux espaces de travail », not
« est offert »: frcheck's STORE-APPLE rule reads an affirmative « offert » as an offer the
English lacks. The service's French changed to the same words, and the entry'sfr_variant
records why.
Release guard (French sign-off): https://github.com/distronode-corporation/district-core-swift/actions/runs/39868325437
7.0.2
The shared core of District AI for iPhone, iPad and Mac 3.0.2.
Changed
contracts/i18n/api-errors.jsonfrom monorepo b48ce94d4, byte for byte: the new code
sending_domain_provider_changed(English and French), which the monorepo's
drift:app_contractswas red on. Its catalog entryapi_error.sending_domain_provider_changed
and accessor are generated (scripts/gen-l10n.py).⚠️ Its French is NOT the contract's
verbatim: the contract says « les nouveaux enregistrements DNS », which frcheck's ban on
« enregistrement » outside a denial refuses (an error, which cannot be waived), so the
catalog says « les nouvelles entrées DNS », the words the service's French is moving to.contracts/i18n/wire-vocabulary.jsonandformat-vectors.jsonfrom monorepo ecf098046,
byte for byte. New labels, generated intol10n/wire.tomlwith the fixture's French: the
workflow skip reasonsemail-misconfigured(« Courriel mal configuré ») andses-missing
(« SES non configuré »; the English "SES missing" keeps the initialism in capitals, as
District AI for Android writes it), the number statusin-use("Active", « Actif »), and the portal's
headings for three research keys,estimatedValue"DGI Evaluation" (« Évaluation DGI »),
person"Contact Profile" (« Profil du contact ») andcallerIntel"Caller intelligence"
(« Renseignements sur l'appelant »), which read as the key itself before.- A length in French no longer breaks between the minutes and the seconds:
L10nFormat.minutesSecondswrites « 1 min 5 s », a no-break space (U+00A0)
between each number and its unit and between the two parts, as the format vectors now say.
It was « 1 min 5 s ».
7.0.1
Fixes found in the District AI for Mac 3.0.0 ring test (2026-10-15), and the ring cancel every
3.0.1 app reads. Nothing is removed or renamed.
(TelemetryEventType.callRingCancelled, DesktopRingEnd.cancelled), so an exhaustive switch
over either needs the new arm; the only consumer of both is District AI for Mac, updated in
its 3.0.1, and district-ios switches over neither.
Changed
- ⛔ The scheduling hand-off never mints UNBOUND (S33; Sean, GitLab #2 note 4022232351).
SchedulingHandoffFlowused to fall back to a mint with nononcewhen leg 1 did not
call back within the clock, failed visibly, or the state was unusable; while any build
could do that, the server could not switchHANDOFF_REQUIRE_NONCEon. Those paths now
end as the newSchedulingHandoffOutcome.noCallbackand send nothing; the app shows a
notice and the person presses again.SchedulingHandoffClient.mint(workspaceId:next:nonce:)
takes a requirednonce: String, so no caller can mint without one. The callback clock
is 30 s (was 10 s), since running out now costs a retry rather than an unbound page.
⚠️ Source-breaking for an app: an exhaustiveswitchoverSchedulingHandoffOutcome
needs the.noCallbackarm (district-macos and district-ios both switch over it), and
a call tomintwithout a nonce no longer compiles (neither app calls it).
Added
-
The ring cancel (
docs/ring_cancel_contract.mdin the service repository): when a ring the
server started ends (answered on some device, timed out, the caller hung up, or withdrawn),
the telemetry socket carriescall_ring_cancelled{callId, reason}and the Apple alert
rows get a background push{type: "call_ring_cancelled", workspaceId, callId, reason}.TelemetryEventType.callRingCancelledandTelemetryEnvelope.ringCancelReason.RingCancelReason(answered,timeout,call_ended,withdrawn; any other value,
or none, iswithdrawn).RingCancel.parse(_:)andparse(userInfo:)for the push.PushPayload.parsestill
drops the type: it is never tapped.DesktopRingGatestops a ring still ringing for that call with
DesktopRingEnd.cancelled(reason). A call this device answered was cleared from the gate
at the press, so the cancel it also receives changes nothing.- The contract fixture
contracts/desktop/telemetry-event-call-ring-cancelled.json
(14 desktop fixtures).
-
CallJoinGate(DistrictCall): whether a call's join is still on its way, for the
microphone command that must not race it. The SDK reports a room connected before it
publishes the microphone the join asked for, and both reducers answer.connectedwith
an unmute; an unmute reaching the SDK in that gap found no microphone publication and
published a second track (two microphone tracks on every answered Mac call, and the
join's own publish then timed out waiting for its first audio frame). The engine asks
admitsMute(_:)before every mute command: an unmute while the join is out is dropped
(the join publishes the microphone live), a mute waits for the join and goes through only
if it succeeded. -
WireLabel.knownLabel(_:_:)(DistrictL10n): the vocabulary's label of a value in the
active language, or nil when the vocabulary does not list it. For an English screen that
shows the portal's word (Completed, notcompleted) and keeps an unlisted value as
sent; a French screen keepslabel(_:_:). -
TokenRefreshCoordinator.finishRefreshInFlight(): waits for a refresh already on the
wire, and the write of its successor, without starting one. For an app about to quit: a
quit between the server's rotation and the store's write left the spent token on disk
with the marker naming it, and the next launch could only report an interrupted refresh
(the person signed out by pressing Quit). A successor the store refused earlier is
written one last time too.
7.0.0
The core of District AI 3.0 for iPhone, iPad and Mac: every sentence the core writes in
English and reviewed Canadian French. It is the three 7.0.0 betas below, and these
changes since 7.0.0-beta.3.
What 7.0.0 brings over 6.0.0:
- Languages.
DistrictL10n: every sentence the core writes comes from a catalog, in
English and reviewed Canadian French, and the app chooses which
(L10nRuntime.locale). The service's error codes (ApiErrorText), stored values
(WireLabel, 39 categories) and formats (L10nFormat: money, dates, times,
durations, numbers) read as the service writes them, held tocontracts/i18n/. English
is byte-identical to 6.0.0. - Requests send
Accept-Language, andApiError.httpcarries the service's error
code (http(status:message:code:));localizedMessagereads it in the reader's
language. - Account settings:
AccountSettingsRepositoryreads the account's stored language
and writes it (saveLocale). - Billing:
price(cents:)on subscriptions and invoices names the currency in French
(249,00 $ CA); English stays$249.00. - Webhooks: an editor offers the four booking events, as the web portal does, and a
save keeps the events a webhook already has. - Contracts from monorepo 43eed5fd6: 187 mobile fixtures.
- Breaking, for the apps:
ApiError.httphas a third value,WireCategorynew cases,
and several stored lists are computed; 7.0.0-beta.1 lists what the apps absorb.
Changed
- The catalogs carry the v3.0 reviewed French.
- README: 7.0.0 is the current release and the examples pin it; the GitLab mirror and
project page links; the contract gate's 187 fixtures. scripts/check-public-hygiene.pyallows this repository's own GitLab mirror URL, and no
other GitLab URL.- CHANGELOG: the date of 1.0.0.
- CI checks that every catalog has its reviewed French (
l10n.py check --require fr-CA),
one catalog per call.
6.0.0
The second factor after native Sign in with Apple. A major release (see Changed). Every
request 5.0.0 could make is byte-identical; what is new is one answer to the Apple exchange
and one route.
Added
POST /api/auth/native/applenow answers401 mfa_requiredwith a single-use ticket when
the account has an authenticator enrolled (the service's S152, live since 2026-10-06).
NativeAuthClient.exchangeAppleIdentityToken(_:)reads it as the new
CodeExchangeResult.mfaRequired(NativeMfaChallenge)(the ticket, and its expiry parsed by
WireInstant); any other 401 staystransportFailure, and the PKCE exchange never
produces the case.NativeAuthClient.submitMfaCode(_:)postsNativeMfaRequest(mfaTicket,code,
deviceId,deviceNameomitted when nil,platform) toPOST /api/auth/native/mfaand
answersNativeMfaResult:successwith the unchanged five-key grant,invalidCode
(401: wrong code or locked account, the ticket stands),ticketRejected(400: expired,
spent, another install or the account changed; start again from the Apple sheet),
rateLimited(429) ortransportFailure.NativeMfaChallenge.isExpired(at:), false when the expiry could not be read.- In
DistrictModel:NativeMfaRequiredResponse, the 401's five keys. - In
DistrictAuthCore:NativeMfaCode, the shape check the apps run before sending a code
(six ASCII digits for an authenticator code; a recovery code of 8 to 64 characters that is
not six digits, sent upper-cased), mirroring the service's routing by shape, and
authenticatorInput(_:)for filtering a field as it is typed. - Contracts:
contracts/mobile/gains the service's two new fixtures (166 files, was 164),
district-native-apple-mfa-required.jsonanddistrict-native-mfa.json, both gated
(NativeMfaRequiredResponse, andNativeTokenResponse, which is the first recorded native
grant).ContractFixtureTestsnow depends onDistrictAuthCore, and
DistrictNetworkTestsonContractGateSupportso the client tests read the recorded bytes.
Changed
⚠️ Source-breaking:CodeExchangeResultgains a case, so an exhaustiveswitchover it
needs.mfaRequired(both apps'AppleSignInController.exchangehave one). That makes
this release a major version.
5.0.0
A major release (see Changed). Every request 4.0.0 could make is byte-identical; what is
new is the socket's outbound side.
Added
- The live call transcript (
transcriptv1 on the telemetry socket). InDistrictModel:
TelemetryEventTypegainstranscriptSnapshot,transcriptSegment,transcriptEnded,
transcriptRetractedandtranscriptError(andisTranscript);
TelemetryEnvelope.transcriptEventreads theirdataintoTranscriptEvent
(TranscriptSnapshotData,TranscriptSegmentData,TranscriptEndedData,
TranscriptRetractedData,TranscriptErrorData, withTranscriptSegment), refusing a
vother than 1 and data whosecallIddisagrees with the envelope's. The speaker, end
reason, retract reason and error code are open vocabularies (.otherkeeps an unknown
name), unknowndatakeys are ignored, and a segment's text is left out of every
printed form.TranscriptClientOpwrites the three client ops
(transcript.subscribe,transcript.unsubscribe,socket.mode), refusing a call id
outside[A-Za-z0-9_-]{1,64}. - In
DistrictLive:TelemetryConnection(andTelemetryConnectionRunner) keep a set of
transcript subscriptions and send them again on every open, renewals included, after a
socket.modewhen built withbroadcast: false; new events
subscribeTranscript/unsubscribeTranscript/resubscribeTranscript, a new command
send, and the runner's three matching methods.TranscriptReduceris the contract's
client algorithm for one call as a pure state machine, following the contract's §4.6
and its §4.12 clarifications: a high-water mark per epoch plus the set of missing seqs
below it (a late frame fills its gap, anything else at or below the mark is a duplicate;
the missing set is kept as ranges, so a jump of any size is tracked exactly), revisions,
(epoch, index)order, a re-subscribe after a gap lasts 2 s with one heal in flight per
call, snapshots whose parts arrive back to back with the same header (a broken one is
asked for again once its last part is in) and which close every older epoch, retraction
with tombstones (counted only when it carriesepochwithseq), and the end:
call_ended,handed_offor an unknown reason is final and fetches the full transcript
with backoff;agent_erroris the newLiveTranscriptPhase.reconnectinguntil a fresh
assistant's epoch or the call's end; a snapshot'sendedReasonsays the same (§4.12 Q9),
so a fresh subscribe afteragent_erroris reconnecting too. A snapshot with no epoch
follows anall: truepurge (Q12): state and missing set are cleared, and the next frame
of each epoch sets its baseline.not_liveis final for its subscribe:
callShownInProgress(atMilliseconds:)(a call-status signal that shows the call in
progress, at most once per 30 s per call, Q10) is the only way to subscribe again, as the
newTranscriptCommand.subscribe; after the 120 s eviction it is the path after the call
(Q11). Only an error whoseopis exactly
transcript.subscribechanges anything. TranscriptSnapshotDatacarriesendedReason(null while live, absent from an older
server and then nil), written back as an explicit null.TranscriptRetractedDatacarriesepochbesideseq(both null from the website),
written back as explicit nulls; its initialiser takes the contract's key order.- Tests: the service's own fixtures for the five events (six frames, an interim segment
among them) inTests/TranscriptFrames/, copied byte for byte from its
contracts/mobile/set. They follow the contract revision with §4.12 Q1 to Q17 (the
retraction'sepoch, no empty opening snapshot, the snapshot'sendedReason), and
TranscriptFrameTestsputs each through the strict gate and checks §4.12's invariants
on them. - Contracts:
contracts/mobile/gains the service's seven new fixtures (164 files, was
157): the sixtelemetry-event-transcript-*events anddistrict-telemetry-token.json
(the desktop file, byte for byte).ContractFixtureTestsgates all seven, the events as
the envelope and again as their typeddata, with eight allowed-null paths (201, was
193), andTranscriptFrameTestsasserts its frames match the vendored copies byte for
byte.
Changed
⚠️ Source-breaking:TelemetryEventTypegains five cases, so an exhaustiveswitch
over it needs them (district-macos has none;DesktopRingGate's own is updated).
TelemetrySocketgains a requirement,send(_:), which district-macos's
URLSessionTelemetrySocketmust implement;TelemetryConnectionEventand
TelemetryConnectionCommandgain cases. That makes this release a major version.- CI's floor on the number of tests run rises from 1850 to 1980.
4.0.0
A major release for one reason: DistrictSection gains a case (below). Every request
3.1.0 could make is byte-identical.
Added
- District Studio links.
AppLinkResolvermaps/dashboard/district/studio(the web's
section for the AI receptionist's settings since 2026-10-04) to the new
DistrictSection.studio, and/dashboard/district/studio/<area>to it with the area
carried:StudioAreanames the pages the apps draw (persona,voice,
call-handling,skills,knowledge), read back through
AppLinkDestination.studioArea. A Studio page outside that list (integrations,
video, anything else) resolves toopenInBrowser. A Studio destination waits for a
workspace.
Changed
- Contract fixtures only, no change to the package:
district-enrich-disabled.jsonand
district-voice-studio.jsonmirror the service's new copies. The enrichment refusal now
names District Studio → Integrations, and the Voice Studio read'sheadingis "Voice",
the name of the web's District Studio page. ⚠️ Source-breaking:DistrictSectiongains a case, so an exhaustiveswitchover it
(district-ios'sAppLinkRouting) needs a.studioarm when it adopts this release;
district-macos has no such switch and needs only the pin. That
makes the next release a major version.- Repository only, no change to the package: the README carries the OpenSSF Scorecard
badge, links district-macos and drops the private-era CI note;.github/SUPPORT.mdsays
where questions and bugs go; the issue chooser links the README, Discussions and private
vulnerability reporting; CodeQL runs gain a workflow-levelconcurrencygroup; and a
newdependency-review.ymlchecks the dependencies a pull request adds.