| D-M3-07-0 |
2026-10-23 |
Built first on the old integration line (stacked on M2-10 and M2-11) and ported to main after its dependencies landed: only the ticket's own commits were brought over as one change. The migrations are renumbered to 00066 to 00068 behind the migrations main gained meanwhile; the changes to the German chapter files of the old base go to their English files on main (02, 09, 12, the component diagram), the rulings and the changelog entry to fragments, the wiring to the module files module_files.go, module_messaging.go, module_runs.go and module_audit.go; generated files were regenerated. The answer check of M2-19 (run_id of agents) and the attachment rules share the body check checkBody; the switch supports_files_out runs before the answer check. |
The rules of the landing replace the integration branches; main is the reference. |
| D-M3-07-1 |
2026-10-21 |
runs may use files (02 4.4, architecture/modules.yaml, the edge runs --> files of docs/architecture/c4-3-server-components.mmd, the runs README): it writes the held invocation on files.FileFinalized and reads the attachment state through files.Attachments. The source port AttachmentLocator of 02 4.3 is not built and is removed from the rows files and messaging. |
The edge points down (files is below messaging, runs above it), so the graph stays acyclic; the copies of R-DM-43 give files the path of every attached file in its own table, so files needs no source in messaging. |
| D-M3-07-2 |
2026-10-21 |
Every attach failure answers one 409 conflict with reason file_not_attachable and detail = the JSON pointer of the failing entry (<pointer>/<index>; the pointer of the list for a unique violation of a concurrent send); the list is checked in request order after the file rows are locked, and an ID listed twice fails as already attached (files/app Attach, 12 10). |
Unknown, foreign, deleted, other-channel and unfinished files must look the same, so the answer tells nothing about files of other people (SEC-UP-9, AZ-P4); locking before the check makes a concurrent attach and the scan outcome wait. |
| D-M3-07-3 |
2026-10-21 |
Attach locks the file rows with FOR UPDATE OF files in ascending id, joined with the state of the upload session, without locking the session row; the attachment rows of these files are read in a later statement (files/adapters/postgres queries of Attach). |
completed is final for a session (M3-03), so its row needs no lock; the lock order of 03 6.3 stays thread, message, files, message_attachments. |
| D-M3-07-4 |
2026-10-21 |
With at least one attachment content_md may be empty or white space only; without attachments the M1-56 rule (errors[].code empty) stays (messaging/domain body rules, 12 10, MessageCreate.content_md). |
A picture without caption is a normal message; a message without content and without attachments says nothing. |
| D-M3-07-5 |
2026-10-21 |
scope_channel_id is absent from the File of an attached file in every representation (Message.attachments, getFile, file.updated); sha256 appears only when upload_state = 'complete' (12 10, File.scope_channel_id). |
After a move the scope channel names a channel the reader may not see; the channel of the message counts (05 2.2). |
| D-M3-07-6 |
2026-10-21 |
files.FileFinalized is published by the one write that appends file.updated (recordFileChange in files/app) exactly when the file becomes final, no longer by the outcome write of M3-04; every finalizing writer goes through it: the scan outcome, the abort of an upload and the image transactions of M3-06 (12 2). |
Every finalization publishes exactly once and in the transaction that stores the final state; the abort of an open upload now publishes too, because failed is final. |
| D-M3-07-7 |
2026-10-21 |
Deleting a message removes its attachment rows and sets files.deleted_at of its files in the same transaction without file.updated; one function of files/app writes deleted_at (named in the files README), and the objects stay until files.purge_objects (M3-08) removes them. |
12 2 announces only changes of upload_state, scan_status, thumbnails and transcript, and message.deleted already removes the message with its attachments from every client; M3-08 and M3-15 add their deletions to the one function. |
| D-M3-07-8 |
2026-10-21 |
The release time of a held invocation is fixed at acceptance as one kernel/clock reading plus file.invocation_hold_max (runs/domain.HoldUntil); the same value is runs.invocation_held_until and the scheduled time of the job runs.invocation_hold_timeout. |
A later change of file.invocation_hold_max does not move held runs, and column and job never disagree. |
| D-M3-07-9 |
2026-10-21 |
The follow-on run of M2-11 asks files.Attachments.Pending for its triggering message unconditionally and is held like an acceptance (runs/app follow-on path through invokeOrHold). |
That path has no MessagePosted with HasAttachments; one query per follow-on run is cheap and rare. |
| D-M3-07-10 |
2026-10-21 |
A follow-up (agent.followup) whose attachments are not final is not held (12 10). |
The core contract holds only agent.invocation (04 15.3 point 1, R-SY-20); the adapter reads attachments only through file.read, which serves no bytes before scan_status IN ('clean','skipped') (12 9.3); a hold would need a marker on run_followups that 03 does not have and would reorder the follow-ups of a run. |
| D-M3-07-11 |
2026-10-21 |
A held run that ended meanwhile (runs.queued_expiry, a cancel) keeps invocation_held_until and is never released (runs/domain.ReleaseVerdict). |
An ended run must not receive an invocation; the column documents that it was held. |
| D-M3-07-12 |
2026-10-21 |
A released agent.invocation has no actor and no correlation_id; the trigger stays in triggered_by. The release sets invocation_held_until to NULL and raises runs.version by one without run.state_changed (runs/app.ReleaseInvocation). |
In every path of this ticket the writer is a job without principal (04 2.2 row actor, 03 event_log.actor_kind); every stored change of a run raises its version (03 runs.version), and the state does not change. |
| D-M3-07-13 |
2026-10-21 |
The handler of runs.invocation_hold_timeout inserts itself again at the stored release time when it runs before it, and returns nil (runs/adapters/jobs). |
Clock skew between nodes must not release early; repeated execution has no second effect (02 12.4 rule 1). |
| D-M3-07-14 |
2026-10-21 |
No new index for the held-run lookup of runs.OnFileFinalized; it filters the partial index of active runs (state, agent_id). |
A held run is always queued, and active runs are few (03 runs). |
| D-M3-07-15 |
2026-10-21 |
The owner's switch supports_files_out is checked in the runs subscriber of messaging.MessagePosted, before every other branch, for a message of an agent with attachments; false vetoes the unit of work with feature_disabled, which the handlers answer as 403 feature_disabled, and a missing key allows (09 2.2). |
messaging and files may not use agents (02 4.4); the same place M2-15 chose for supports_components, and the veto leaves neither message nor attachment rows. |
| D-M3-07-16 |
2026-10-21 |
SQL visibility lint exceptions: after_authorize for the file lock read of Attach, ForMessages, Pending and the lock read of DeleteForMessage; authorize_resolution for the attachment read by file_id that getFile, MessageOf and recordFileChange use; system_job for the run lock of ReleaseInvocation, the held-run lookup and the read of the hold job (tools/lint/sql_visibility.yaml). |
Each read follows a decided action, builds the resource of a check, or runs in a job without principal (07 7.4, AZ-P12); writes are not linted. |
| D-M3-07-17 |
2026-10-21 |
No migration for runs.invocation_held_until. |
The column already exists since migration 00051 (M2-05) with the type and nullability of 03 runs (R-DM-44). |
| D-M3-07-18 |
2026-10-21 |
The switch supports_files_out is read as the fact FilesOutPermitted of agents.RunFacts (nil when the key is absent) instead of a new Agents.OperatorConfig read. |
The merged port already reads the agent record per acceptance and carries FollowupsPermitted the same way (D-M2-11-12). |
| D-M3-07-19 |
2026-10-21 |
files.Attachments.ForMessages and MessageOf also read on the pool outside a unit of work; the writers and Pending require one. |
The history and single-message reads of messaging run outside a unit of work. |
| D-M3-07-20 |
2026-10-21 |
The space_id of the scope of file.updated for an attached file comes from the channel copy (ChannelLocator.SpaceOf of message_attachments.channel_id) instead of chat.Threads.Info. |
The files module already resolves the space of a channel through this port; the copy holds the channel of the message, whose space is the space of the thread. |
| D-M3-07-22 |
2026-10-21 |
The files use cases (files/app.Service) implement files.Attachments; internal/wire hands messaging and runs a LazyAttachments that the files step binds. Place: internal/wire/attachments.go, module_files.go, module_messaging.go, module_runs.go. |
The chat step builds messaging and runs before the files step, which needs chat; a message without attachments never calls the port, and the server serves only after every step ran. |
| D-M3-07-23 |
2026-10-21 |
A released agent.invocation drops the correlation through runs/app.Deps.Uncorrelated, which wire binds to sync/adapters/telemetry.Uncorrelated (the span context the correlator reads), and through an empty kernel/reqmeta correlation id. Place: internal/runs/app/hold.go, internal/sync/adapters/telemetry/correlation.go. |
runs/app may not import otel and wire has no vendors; the correlator of sync is the one place that knows what the appender reads. |
| D-M3-07-24 |
2026-10-21 |
The attachments row of TestCreateMessage_NotEnabledMembers400 keeps the not_enabled answer: its world (test/messagingworld) builds messaging without the files port, which keeps the M1-56 behaviour; the new rule is proven over the real server in test/attachments. Place: internal/messaging/adapters/httpapi/messages_test.go. |
Wiring files into the shared messaging world would change every test of that world for one row. |
| D-M3-07-25 |
2026-10-21 |
Test seam wire.Deps.TxnTestSubscribers (and harness.Options.TxnTestSubscribers): in-transaction subscribers of a test registered after every production subscriber, for the veto tests of the move, the delete and the release. Place: internal/wire/txn.go, audit.go, module_audit.go, serve.go (Deps), test/harness. |
The brief's veto tests need a subscriber after files.OnThreadMoved, runs.OnFileFinalized and the delete path in the real server; nil in production. |
| D-M3-07-26 |
2026-10-21 |
ReleaseInvocation and the job runs.invocation_hold_timeout lock the run with the existing runs.LockRun, as the lifecycle paths of M2-09 to M2-11 do; its SQL lint exception stays unchanged and only runs.LockHeldRunsOfMessage gets a new system_job entry. Place: internal/runs/app/hold.go, tools/lint/sql_visibility.yaml. |
The same statement on the same key; the brief forbids widening an existing exception. |
| D-M3-07-27 |
2026-10-21 |
The detail of a refused attachment names the entry as a JSON pointer with a zero-based index (/attachment_file_ids/0 for the first file); a unique violation at insert names the list. Place: internal/files/app/attach.go. |
JSON pointers index arrays from zero (RFC 6901); position stays index plus one. |
| D-M3-07-28 |
2026-10-21 |
The integration packages test/attachments and test/invocationhold run at most three harness servers at a time. Place: their helpers_test.go. |
Every server opens its own pools on the one pgtest container; more parallel servers exhausted its connection slots (SQLSTATE 53300). |