| D-M3-09-1 |
2026-10-24 |
files reads the switch voice.enabled through its outgoing port VoiceSwitch, which internal/wire binds to instance.Flags.Enabled with instance.FlagVoiceEnabled; there is no module edge files -> instance. Place: internal/files/app/ports.go, internal/wire. |
Only the composition root needs to know where flags live; the module graph stays as it is (02 4.4). |
| D-M3-09-2 |
2026-10-24 |
The effective mode (files.Transcripts.Mode) is voice.transcription_mode, except that service counts as off while VoiceSwitch is off for the subject; the subject is the author of the voice message (routing and the automatic start), the caller (requestTranscription) and the uploader (the job files.transcribe). agent and off do not depend on the switch (12 8.2). |
The flag supports a rollout to people and groups (M1-47), and audio leaves the server only while the switch is on for the person whose voice it is (02 2 row "Transcription service"). |
| D-M3-09-3 |
2026-10-24 |
Voice formats are the detected MP4 audio types that files/domain.HasContainerMetadata treats as audio (audio/mp4, audio/x-m4a, pinned in TestIsVoiceFormat) for every author, and audio/mpeg in addition for an agent; people and apps send only MP4 audio (files/domain.IsVoiceFormat, 12 8.1). |
12 8.1 accepts MP3 "also for agents"; the apps record only AAC-LC in MP4, and the names are the ones the pinned type detector reports. |
| D-M3-09-4 |
2026-10-24 |
The voice check (files/domain.VoiceCheck) runs after the attachment checks of M3-07 on the locked file row and answers the first failure in this order: not final 409 file_not_ready; not deliverable 409 file_blocked; not a voice format, an MP4 with width or height, or an MP4 without a duration 400 validation_failed with errors[].code voice_format_unsupported; a duration above voice.max_duration voice_too_long (equal passes). The file must be ready when the message is sent; the pointer is <list pointer>/0 (12 8.1). |
The detected type and the duration exist only after the worker's read pass (12 3.4, 7.3), and the plan demands a synchronous 400; a client keeps the voice message in its outbox until file.updated reports the file ready (15 13). |
| D-M3-09-5 |
2026-10-24 |
The codec inside the MP4 (AAC-LC, the esds object type) is not verified; the server checks the type, the absence of a video track and the duration only. |
The API process must not parse audio (SEC-UP-5) and files stores no codec fact; a wrong codec only fails the playback on a device. |
| D-M3-09-6 |
2026-10-24 |
MP4 audio becomes complete only after the container phase: for the types of D-M3-09-3 (files/domain.DeferContainerComplete) the final transaction of files.scan (clean, skipped) leaves upload_state = 'pending' and inserts files.thumbnail; its transaction T3 sets duration_ms (when the box reader found it) and upload_state = 'complete' together through recordFileChange. A box error leaves duration_ms absent and still completes the file. Video and QuickTime keep the order of M3-06 (12 2). |
Otherwise a voice file would be deliverable for a moment without its duration, and a send in that moment would be refused as unsupported. |
| D-M3-09-7 |
2026-10-24 |
In the effective mode service the send of a voice message sets transcript_status = 'pending' through recordFileChange (one file.updated in the scope of the message) and inserts files.transcribe with InsertTx in the transaction of the send; in agent and off the file keeps none (files/app Attach, 12 8.2). |
The transcript starts without a second request, and the job exists exactly when the message is stored. |
| D-M3-09-8 |
2026-10-24 |
A voice message needs exactly one attachment: none gives 400 validation_failed with errors[].code required, more than one limit_reached, both with the pointer /attachment_file_ids (/first_message/attachment_file_ids in createThread); content_md may be empty, a caption's mentions trigger, components stay card-only (messaging/domain body rules, 12 8.1). |
12 8.1 defines the voice message as one audio attachment; the codes are the schema codes for a missing member and a quantity limit (06 5 point 3). |
| D-M3-09-9 |
2026-10-24 |
The transcript ETag (files/domain.TranscriptETag) is the strong value "<first 22 characters of the base64url SHA-256>" over transcript_status, transcript_source, transcript_language and transcript_text, each followed by one zero byte; getFile, getTranscript and submitTranscript send it in ETag, and submitTranscript compares the raw If-Match with it (IfMatchRequired: 428 without, 412 version_conflict with current and ETag on a mismatch). |
files has no version column and 03 R-DM-48 derives the ETag; the zero separator keeps different field splits apart, the form is the one of getInstance (M1-46), and ParseIfMatch accepts only decimal versions. |
| D-M3-09-10 |
2026-10-24 |
submitTranscript outside mode agent and requestTranscription outside mode service, and both for a file that is not a voice format (IsVoiceFormat with the uploader's kind), answer 409 conflict with reason transcription_unavailable; a file that is not ready answers 409 file_not_ready or 409 file_blocked (12 8.2). |
One reason for "no transcription for this file in this mode", as 12 8.2 names it for file.transcribe; an agent transcript would overwrite the service or contradict mode off. |
| D-M3-09-11 |
2026-10-24 |
requestTranscription sets a transcript in state none or failed to pending and inserts the job; pending and done change nothing; the answer is always 202 with the current Transcript view, and the switch off for the caller gives 403 feature_disabled before the mode check (12 8.2). |
A repeated tap must not start a second transcription or overwrite a finished one; the idempotency stage of M1-20 replays a repeated Idempotency-Key. |
| D-M3-09-12 |
2026-10-24 |
ErrTranscriptionPermanent from the adapter writes failed at once; any other error returns the error so the job retries, and the attempt that reaches voice.transcription_max_attempts writes failed instead; a database error never writes failed. A run without effect returns nil when the row is gone, deleted or not pending, or when the row changed between read and write (files/app/transcribe.go, 02 14.4). |
02 14.4 says "job retries, then transcript_status = failed"; a lost database never turns a transcript into a failure, and a repeated run has no second effect (02 12.4 rule 1). |
| D-M3-09-13 |
2026-10-24 |
The language hint is the primary subtag of the uploader's users.locale in lower case (files/domain.LanguageHint), empty for an invalid locale or a principal without locale; the stored transcript_language is the language of the service's answer when ValidLanguageTag accepts it, else the hint, else absent (12 8.2). |
12 8.2 names users.locale as the hint; transcription services expect a two- or three-letter code, and an invalid answer never reaches the column. |
| D-M3-09-14 |
2026-10-24 |
One function decides "final for the invocation": files/domain.InvocationFinal(f) = Final(f) && f.TranscriptStatus != "pending"; BecameFinal compares it, so recordFileChange publishes files.FileFinalized on the scan result, the container phase or the end of a pending transcript, and setting pending on a final file publishes nothing. Final and Deliverable keep their meaning for delivery (12 2). |
The invocation waits for the transcript in mode service (12 10), while a deliverable file with a pending transcript is still served. |
| D-M3-09-15 |
2026-10-24 |
The adapter openai_compatible sends one POST to voice.transcription_url through an egress client limited to the scheme, host and port of that URL, without redirects, with voice.transcription_timeout and egress.max_response_bytes; the body is streamed multipart/form-data with file (audio.m4a or audio.mp3, the stored mime_type), model only with voice.transcription_model, language only with a hint, response_format = json, and Authorization: Bearer only with voice.transcription_api_key_ref. Transient: timeouts, connection errors, 408, 429, 5xx; permanent: a blocked address, an answer above the limit, every 3xx, every other 4xx, a malformed or invalid body (12 8.2). |
The request form is the OpenAI-compatible transcription endpoint; audio never reaches another address (SEC-NET-1 to SEC-NET-3, T-NW-01), the original name never leaves the server, and only errors a retry can fix are retried. |
| D-M3-09-16 |
2026-10-24 |
files.Attachments.Pending returns a PendingReason (PendingNone, PendingFiles, PendingTranscript); the hold window is runs/domain.HoldWindow(reason == PendingTranscript, file.invocation_hold_max, voice.transcript_hold_max), the release on FileFinalized happens only at PendingNone, the release by the timeout job also with the transcript pending, and the follow-on run uses the same rule (12 10). The transcript is a reason to hold only through transcript_status = 'pending', which only mode service sets. |
A voice message waits for its transcript at most voice.transcript_hold_max and one waiting for a scan at most file.invocation_hold_max (03 R-DM-44, R-DM-47); the domain stays free of other modules. |
| D-M3-09-17 |
2026-10-24 |
The "foreign agent" of the plan row is an agent that cannot read the file; it gets the same 404 as an unknown id. file.transcript.submit requires All(ThreadOperator, Level(read)) on the file resource of M3-07; a visible caller who is not the operator gets 403 forbidden with the reason of the ThreadOperator atom, and an unattached file is visible only to its uploader (internal/authz, 05 4.8). |
Existence of a file is not revealed to an agent without read access (AZ-P4, ADR 0017); the requirement replaces the deferred entry of M1-28. |
| D-M3-09-18 |
2026-10-24 |
Two new configuration keys: voice.transcription_model (string, default empty: the request has no model field) and voice.transcription_max_attempts (integer, at least 1, default 5); mode service requires voice.transcription_url to be an absolute https or http URL with a host, without user info and fragment (cross-check voice_service_url, the error names keys only) (12 18). |
An OpenAI-compatible endpoint usually needs a model, and 02 14.4 retries without a number, while limits come only from named keys (brief rule 9). |
| D-M3-09-19 |
2026-10-24 |
The deferred parts AG-20 attachment, AG-20 transcript and AG-25 run as cases of test/agents/scenarios/ag-20.yaml and ag-25.yaml; the conductor gains the run_jobs kinds files.scan, files.thumbnail, files.transcribe, an upload body from a generated fixture and a file check through AdminDB (never the text); the Fake-Agent gains the ops read_file (byte length only) and submit_transcript (If-Match from getFile), and context reports the attachments and whether their texts occur only in a marked data block (09 16.2, ADR 0054 point 4). |
The three lines leave testdata/deferred.golden in the change that builds them (D-W-108); the Fake-Agent never echoes file bytes or transcript text in a reply. |
| D-M3-09-20 |
2026-10-24 |
The header ETag of getFile, getTranscript and submitTranscript reuses the shared header component ETag; its meaning (the derived transcript version) stands in the operation descriptions. The examples of getTranscript and submitTranscript show status: done with source, and the example of requestTranscription status: pending without text (docs/api/openapi.yaml). |
Clients treat every ETag as an opaque string (06 8 point 1); text, language and source are present only with done, so the former examples contradicted the schema description. |
| D-M3-09-21 |
2026-10-24 |
TranscriptSubmit.language carries the schema pattern ^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$, the same rule as files/domain.ValidLanguageTag, so a wrong tag answers pattern_mismatch in the schema stage. |
BCP 47 tags are case-insensitive (en-GB); the schema stage and the domain must accept the same set. |
| D-M3-09-22 |
2026-10-24 |
The merged code has no agents.EffectiveCapabilities: agents.RunFacts carries VoicePermitted (the owner's supports_voice, absent allows) and VoiceReported (the heartbeat's capabilities.voice_input), and runs/domain.EffectiveVoiceInput combines them; runs takes the transcription mode through the narrow port runsapp.VoiceModes (only Mode). Place: internal/agents/agents.go, internal/runs/app/ports.go. |
The brief follows the code names of the merged dependencies (D-W-100); the facts of an agent are read once per acceptance anyway, so a text message costs no extra statement. |
| D-M3-09-23 |
2026-10-24 |
A voice message whose candidate has no transcription path is refused with voice_unsupported only after the filter check of agent.invoke: a person without the trigger right who does not mention the agent gets no answer (R-AG-05); the refusal itself writes no audit entry. Place: internal/runs/app/accept.go. |
09 3.5 makes voice_unsupported a decision-function rejection, which never audits; the silence rule of R-AG-05 comes first for every rejection. |
| D-M3-09-24 |
2026-10-24 |
The transcript of a voice message stands in the run context on its attachment inside the element of the message and carries that element's trust (trigger for the triggering message), not inside a separate data block; the text of a file attachment that the agent reads stays inside a marked data block. The conformance case AG-20 transcript checks exactly this. Place: internal/runs/app/runcontext.go, test/agents/scenarios/ag-20.yaml. |
09 4.6 and 16.2 and ADR 0054 point 4 say the transcript carries the trust marker of its message; the brief's design note 20 d sentence on a marked block is read against them (spec first). |
| D-M3-09-25 |
2026-10-24 |
requestTranscription and submitTranscript lock the file row with their own query files.LockVoiceFile (category after_authorize), separate from the job's files.LockTranscriptFile (category system_job), so every exception of the SQL visibility lint names one reason. Place: internal/files/adapters/postgres/queries/transcripts.sql, tools/lint/sql_visibility.yaml. |
AZ-P12 needs one category per query (M1-70). |
| D-M3-09-26 |
2026-10-24 |
The canary run of SEC-T10 runs in mode service: the fake service returns a canary transcript that the job stores, and submitTranscript with a second canary is refused with 409 conflict with reason transcription_unavailable, so both texts pass the server without reaching a log, span, metric, audit row, event payload or problem body. The stored agent path is covered by test/voice (TestVoice_ModeAgent_SubmitTranscript checks the event payloads). Place: services/server/test/canary/scenario_voice.go. |
One canary run has one configuration; mode service exercises both transcript sources' inputs. |
| D-M3-09-27 |
2026-10-24 |
A key reference voice.transcription_api_key_ref that cannot be read during a call is a transient error; at the start of a worker in mode service it is read once, so a missing key stops the start. Place: internal/files/adapters/transcription/openai_compatible.go, internal/wire/transcription.go. |
Design note 11 d lists the permanent failures exhaustively; a secret provider outage heals, a wrong reference is caught at the start (fail fast, as for APNs). |
| D-M3-09-28 |
2026-10-24 |
An anonymous caller of requestTranscription or submitTranscript gets the 401 of authorize before any file read, like getFile and deleteFile (D-M3-08-16); TestAnonymousFileByID_Unauthenticated now walks every operation on a file by its id for a live, a deleted, a purged and an unknown id. Place: internal/files/app/transcripts.go (authorizeFile), services/server/test/filecleanup/cleanup_test.go. |
The second occurrence of the class, so the check sits in the one helper both use cases call first and the test covers all file routes (red team M3-09). |
| D-M3-09-29 |
2026-10-24 |
The run context lists transcript only on an attachment of a message whose kind is voice; a transcript of an audio file attached to a text message (possible through requestTranscription or submitTranscript) never stands inline, because it is file content (T-AI-03). Place: internal/runs/app/runcontext.go (contextAttachment). |
09 4.6 and ADR 0054 point 4 give the trust of its message only to the transcript of a voice message (red team M3-09). |