| D-M2-09-0 |
2026-10-22 |
Built on the old integration line and ported to main after M2-08 landed: only the ticket's own commits were brought over, with the integration fixes INT-W54/4 (lint findings, no behaviour change) and INT-W54/5 (the canary scenario calls the six enabled operations). The spec commit that ported run.token.renew and renewInvocationToken into the old base is dropped, because main already carries both; the additions to the German chapter files of the old base go to their English files on main. INT-W54/3 is not needed, because the terminal sweep of M2-46 is not on main; AccessChanged and AgentDisabled -> runs follow MessagePosted -> runs directly. |
The rules of the landing replace the integration branches; main is the reference. |
| D-M2-09-1 |
2026-10-21 |
OwnerID of a run descriptor: runs.agent_id for run.start, run.token.renew, run.finish; runs.triggered_by for run.cancel. An unknown run answers the 404 of an invisible one. |
The principal each action reserves the run to (05). |
| D-M2-09-2 |
2026-10-21 |
runs computes the token verdict (authorize.TokenFacts); authz decides run.start as All(AgentSelf, Token(valid)), run.token.renew as All(AgentSelf, Token(valid, expired)); the guard of run.start gives billing_mode_mismatch, then tool_class_mismatch. The authz copy is InvocationFacts (TokenFacts serves token.create). |
runs owns keys and jti columns; the reasons are decisions of authorize (05 1.1, 07 4.1). |
| D-M2-09-3 |
2026-10-21 |
Decision.Detail carries the problem reason (expired, superseded, invalid) of invocation_token_invalid; the audit keeps that code with detail token_verdict. |
One audit code, three adapter reactions. |
| D-M2-09-4 |
2026-10-21 |
Server expiry tolerance at start is auth.clock_leeway; exactly at exp plus it the token is valid. |
Upper bound of the adapter's tolerance (09 4.5). |
| D-M2-09-5 |
2026-10-21 |
startRun on waiting_input and resumeRun on queued answer 409 invalid_state_transition. |
Each endpoint performs only its own transition (06 16). |
| D-M2-09-6 |
2026-10-21 |
RunStart.external_session_ref is refused with 400 validation_failed (errors[].code not_enabled) until M2-12. |
The session row it binds is M2-12's (06 5 point 3). |
| D-M2-09-7 |
2026-10-21 |
A token rejected as invalid changes nothing; the adapter fails the run with invalid_invocation_token. |
04 15.3 point 4; a forged token must not fail a foreign run. |
| D-M2-09-8 |
2026-10-21 |
A renewal refused at step 4 fails the run with the reason of the delivery check, audits it (stage renewal) and answers 403 agent_invocation_denied with that reason. |
The adapter sees the same cause as in run.state_changed. |
| D-M2-09-9 |
2026-10-21 |
finishRun: failed and cancelled need reason; failed keeps adapter reasons of 09 5.2, others become adapter_error; cancelled takes only cancel-request reasons; completed ignores it. |
An adapter confirms the server's cause and cannot invent one. |
| D-M2-09-10 |
2026-10-21 |
An adapter-reported I3 mismatch writes one audit entry run.finish deny with billing_mode_expected and tool_class codes and counts bothy.agent.billing_mode_mismatch. |
10 14 row "I3 mismatch". |
| D-M2-09-11 |
2026-10-21 |
RunCancel.reason is checked (textcheck.DisplayText, audit.reason_max_chars) and stored nowhere. |
03 allows free text only for mandatory admin reasons (spec gap). |
| D-M2-09-12 |
2026-10-21 |
cancelRun on queued ends the run user_cancelled at once and still writes run.cancel_requested; no job. |
No delivery marker in 03; a later start gets 409 already_started. |
| D-M2-09-13 |
2026-10-21 |
run.cancel_requested is personal to the run's agent (agent.events.consume), resource without version; requested_by only for user_cancelled. |
Only a person's stop has a requester (05 6). |
| D-M2-09-14 |
2026-10-21 |
RunFinish.costs is refused with 400 validation_failed (errors[].code not_enabled) until M2-22. |
Dropping reported costs silently would lose money data. |
| D-M2-09-15 |
2026-10-21 |
Reason after a loss of trigger: user.disable user_disabled (all runs of the person), channel.archive/space.archive channel_read_only, else access_revoked. |
08 11 point 7, 09 5.2. |
| D-M2-09-16 |
2026-10-21 |
After a loss of trigger the states stay (also queued): cancel request plus runs.cancel_timeout. |
Deliveries and renewals fail on their own; a token cannot outlive agent.cancel_grace. |
| D-M2-09-17 |
2026-10-21 |
runs.cancel_timeout is scheduled per request at now plus agent.cancel_grace (args the run); it ends an active run cancel_timeout, an ended one stays. |
03 keeps no time of a request; repeats are harmless. |
| D-M2-09-18 |
2026-10-21 |
A disabled agent's runs end agent_disabled at once with run.cancel_requested; no job. |
Its tokens are revoked in the same transaction (09 2.1). |
| D-M2-09-19 |
2026-10-21 |
runs.stall_check runs every agent.presence_timeout (queue maintenance, once at worker start). |
The stall warning of run.stall_after is not built (no time of the last run.update). |
| D-M2-09-20 |
2026-10-21 |
interrupted uses the last persisted heartbeat (run missing from active_runs, heartbeat more than agent.presence_timeout after started_at); without active_runs no interrupted. |
The worker sees only the stored heartbeat (03 agents). |
| D-M2-09-21 |
2026-10-21 |
Start check: agent.heartbeat_persist_interval below run.orphan_timeout. |
The stored heartbeat lags by the persist interval. |
| D-M2-09-22 |
2026-10-21 |
Names follow the code: port methods read the principal from the context; subscribers are methods of runs/app.Acceptor; domain LiveRun; port agents.Heartbeats with agents/app.HeartbeatReader. |
Brief names predate the merge (Boundaries). |
| D-M2-09-23 |
2026-10-21 |
Tokens of trigger_kind interaction are not renewed (internal error) until M2-15. Since M2-16 is on main, a token of a command run is renewed as the agent.invocation of that run with the command of the token (renewalInput, the resolver of command runs); a test holds every run trigger kind the delivery check resolves renewable (TestRenewalInput_CommandRun, 2026-10-22, red team finding of the rebase). |
The delivery check has no resolver for interaction yet; I2 holds. A command run must not hang in queued until run.queued_max_age because its token expired before the start. |
| D-M2-09-24 |
2026-10-21 |
runs.stall_check reads all started active runs without a batch limit. |
At most one per thread (R-DM-14); no number without a key. |
| D-M2-09-26 |
2026-10-22 |
runs.stall_check reads the persisted heartbeat of the run's agent again inside the unit of work that locked the run, and decides the verdict on that reading; the first read only selects the candidates. The examples of the 200 responses of startRun and resumeRun show a run in running. |
Review finding on the pull request: a heartbeat stored between the first read and the lock must win, as the comment and 09 5.5 say (TestStallCheck_HeartbeatAfterReadWins); a start answers the started run. |