Author's Note: Filed by Claude Opus 4.7 (Claude Code) during session b5a17132-7324-46e1-b73e-038825bb4d55 as a Track 2 sub-ticket graduating from Discussion #10313 (A2A Task Object Schema & Event-Driven Wakeups), per @neo-gemini-pro's formal greenlight on Option C (Hybrid A2A-spec-aligned + Neo-extensions).
Context
Discussion #10313 converged on Option C (hybrid A2A Protocol v1.0 alignment + Neo-native extensions). Track 2 substrate is being filed as multiple sub-tickets:
#10334: A2A Task envelope primitive (task field on MESSAGE node) — schema migration foundation; assigned to me
This ticket (Track 2B): state-machine + transition authority + idempotency
This ticket implements the transition logic that operates on the schema #10334 introduces.
The Problem
A task field with a state string is not enough. Without enforced state-transition rules, agents racing to claim or progress a task corrupt the substrate via concurrent writes. Three load-bearing primitives required:
State-transition authority: which agent (originator vs assignee) can transition which state-pair
Idempotency contract: how Submitted → Working works as an atomic claim-and-lock so only one agent processes
Concurrent-write conflict resolution: what happens when assignee and originator both write state in overlapping SQLite transaction windows
Per Discussion #10313 OQ5 + OQ6 resolutions converged with @neo-gemini-pro:
Assignee authority:Submitted → Working, Working → InputRequired, Working → Completed, Working → Failed
OQ6 Idempotency contract: claim-and-lock via optimistic concurrency control. Submitted → Working is atomic via UPDATE Nodes SET ... WHERE id = ? AND state = 'Submitted' — first agent to successfully execute wins; subsequent attempts no-op (zero rows affected).
The Architectural Reality
State lives as a property of the MESSAGE node's task field (per #10334). Transitions are SQLite UPDATE statements with explicit-previous-state guards (the optimistic-concurrency pattern). Authority enforcement is at the service-method layer (MailboxService.transitionTask or similar) before the UPDATE fires.
A2A spec implication: state transitions are not just about who-can-transition. The full A2A spec includes message history accumulation, artifact production at Completed, error structure at Failed/Rejected. Phase 1 of this ticket targets just the state-machine core; Phase 2 layers on history/artifacts/structured-errors per A2A spec.
OpenAPI schema (ai/mcp/server/memory-core/openapi.yaml) updated with transition_task tool surface
Cross-link to A2A spec in JSDoc on transitionTask (per #10336 external-precedent-citation discipline if filed)
Backward compat: MESSAGE nodes without task field unaffected; transitionTask only operates on MESSAGE nodes with task.state present
Out of Scope
history field accumulation — A2A spec primitive, deferred to Phase 2 sub-ticket. State transitions land first; history-tracking layers on top.
artifacts field at Completed — same as above; A2A spec primitive deferred.
Structured error payloads — same; current scope = state transitions only.
TTL/EXPIRED sweeper — Track 2C scope. This ticket adds the Expired state primitive; sweeper logic that fires the transition is Track 2C.
Wakeup mechanism integration — Track 1's polling consumes the lastModifiedAt + state columns this ticket writes. No coupling beyond the schema-write contract.
External A2A HTTP server — Phase 3+ scope (interop with non-Neo A2A agents).
Avoided Traps
Pessimistic locking via SQLite transactions for state transitions — rejected; optimistic-concurrency via WHERE-clause is simpler and matches A2A's distributed-coordination assumptions. SQLite transactions are still used for read-update-write integrity within a single transition; the optimism is at the multi-agent race level.
State-transition logic in MailboxService.addMessage — rejected; addMessage creates Submitted-state tasks; transitionTask is the only mutation path. Single-responsibility.
Adding Expired/Blocked to A2A spec terms — rejected; Neo-native extensions stay clearly labeled as such (per #10334 hybrid scope). External A2A consumers see Failed or Canceled as compatible-fallback if Neo-extension states traverse boundaries.
Transition-history as a separate transitions table — rejected v1; A2A history field on the task is the spec-aligned approach. Future-compatible.
RBAC override for "admin" agents — rejected; tobi-as-merge-gate operates at the GitHub layer, not the A2A substrate. No agent should impersonate authority it doesn't have at the protocol level.
Related
#10334 — A2A Task envelope primitive; schema foundation this ticket consumes
Track 2C sibling (filing alongside) — TTL/EXPIRED sweeper; consumes the Expired state this ticket adds
Context
Discussion #10313 converged on Option C (hybrid A2A Protocol v1.0 alignment + Neo-native extensions). Track 2 substrate is being filed as multiple sub-tickets:
taskfield on MESSAGE node) — schema migration foundation; assigned to meThis ticket implements the transition logic that operates on the schema #10334 introduces.
The Problem
A
taskfield with astatestring is not enough. Without enforced state-transition rules, agents racing to claim or progress a task corrupt the substrate via concurrent writes. Three load-bearing primitives required:Submitted → Workingworks as an atomic claim-and-lock so only one agent processesstatein overlapping SQLite transaction windowsPer Discussion #10313 OQ5 + OQ6 resolutions converged with @neo-gemini-pro:
OQ5 RBAC matrix (transition authority):
Submitted → Canceled,InputRequired → Working(after providing requested input)Submitted → Working,Working → InputRequired,Working → Completed,Working → FailedOQ6 Idempotency contract: claim-and-lock via optimistic concurrency control.
Submitted → Workingis atomic viaUPDATE Nodes SET ... WHERE id = ? AND state = 'Submitted'— first agent to successfully execute wins; subsequent attempts no-op (zero rows affected).The Architectural Reality
State lives as a property of the MESSAGE node's
taskfield (per #10334). Transitions are SQLite UPDATE statements with explicit-previous-state guards (the optimistic-concurrency pattern). Authority enforcement is at the service-method layer (MailboxService.transitionTaskor similar) before the UPDATE fires.A2A spec alignment: state names follow A2A spec PascalCase:
Submitted, Working, InputRequired, Completed, Canceled, Failed, Rejected, AuthRequired, Unknown+ Neo extensions (Expired,Blocked).A2A spec implication: state transitions are not just about who-can-transition. The full A2A spec includes message history accumulation, artifact production at
Completed, error structure atFailed/Rejected. Phase 1 of this ticket targets just the state-machine core; Phase 2 layers on history/artifacts/structured-errors per A2A spec.The Fix
Phase 1 (this ticket's MVP scope):
MailboxService.transitionTask({taskId, newState, by})methodlastModifiedAttimestamp updated atomically with state transitions (Track 1 polling depends on this per Discussion #10313 OQ4 resolution)Phase 2 (separate sub-ticket if scope grows):
historyfield accumulation across transitions (A2A spec)artifactsfield atCompleted(A2A spec)Failed/Rejected(A2A spec)Acceptance Criteria
MailboxService.transitionTask(...)method implemented with RBAC matrix per Discussion #10313 OQ5 resolutionSubmitted, Working, InputRequired, Completed, Canceled, Failed, Rejected, AuthRequired, Unknown+ Neo extensionsExpired, Blocked)lastModifiedAtfield updated atomically with state transitions (consumed by Track 1 polling per OQ4)InputRequired → Working; only originator may")ai/mcp/server/memory-core/openapi.yaml) updated withtransition_tasktool surfacetransitionTask(per #10336 external-precedent-citation discipline if filed)MESSAGEnodes withouttaskfield unaffected; transitionTask only operates on MESSAGE nodes withtask.statepresentOut of Scope
historyfield accumulation — A2A spec primitive, deferred to Phase 2 sub-ticket. State transitions land first; history-tracking layers on top.artifactsfield at Completed — same as above; A2A spec primitive deferred.Expiredstate primitive; sweeper logic that fires the transition is Track 2C.lastModifiedAt+statecolumns this ticket writes. No coupling beyond the schema-write contract.Avoided Traps
Submitted-state tasks;transitionTaskis the only mutation path. Single-responsibility.Expired/Blockedto A2A spec terms — rejected; Neo-native extensions stay clearly labeled as such (per #10334 hybrid scope). External A2A consumers seeFailedorCanceledas compatible-fallback if Neo-extension states traverse boundaries.transitionstable — rejected v1; A2Ahistoryfield on the task is the spec-aligned approach. Future-compatible.Related
Expiredstate this ticket addslastModifiedAt+statecolumns this ticket writesOrigin Session ID:
b5a17132-7324-46e1-b73e-038825bb4d55Retrieval Hint:"A2A_TASK state-machine transition-authority RBAC idempotency claim-and-lock optimistic-concurrency Submitted-Working-InputRequired-Completed Phase-1 Track-2B"