ADR 0035 makes a claimed A2A Task one of the exact source facts behind the v13.2 LifecycleFrontier. A source audit before filing that producer found a lower-level authority defect in the current Memory Core state machine: a broadcast Task's Submitted → Working transition serializes the first state change, but it does not persist who won.
This ticket repairs that source contract before any lifecycle composer, hook projection, Fleet reader, or UI consumes it. The ordering is deliberate: building those consumers over an ownerless Working Task would turn an existing ambiguity into a trusted false fact.
The Agent OS structure map was run against current dev. The owner is the existing ai/services/memory-core/MailboxService.mjs task-transition boundary and its existing wake consumer; no new .mjs file or service directory is introduced.
The Problem
MailboxService.transitionTask() currently derives isAssignee from the MESSAGE's SENT_TO edge on every transition. For SENT_TO: AGENT:*, every authenticated recipient therefore remains an assignee after one peer has already won Submitted → Working.
The optimistic UPDATE ... WHERE task.state = 'Submitted' protects only the first state mutation. It does not bind later authority:
Alice sends a broadcast Task in Submitted.
Bob wins Submitted → Working.
The stored Task contains state: 'Working' but no winning assignee.
Charlie still satisfies the broadcast isAssignee check and can execute Working → InputRequired|Completed|Failed.
That violates the original claim-and-lock intent from Discussion #10313 / ticket #10338: the first claimant wins the Task, not merely the first write.
Two adjacent facts confirm the missing owner:
addMessage() treats the Task payload as caller-authored opaque JSON apart from state validation, so a caller-supplied task.assignee is not trustworthy.
heartbeatPulseEvaluator already reads task.assignee, but no Memory Core write path owns or updates that field. It also reports properties.updatedAt || sentAt even though transitions persist properties.lastModifiedAt.
The negative ROI is severe: without this repair, stage-4 awareness can attribute one Task to multiple peers, wake the wrong actor, and permit a non-winning peer to terminally mutate another peer's claimed work.
The Architectural Reality
MESSAGE routing edges own delivery audience. SENT_TO: AGENT:* means “claimable by the broadcast cohort”; it cannot remain the durable post-claim owner.
MailboxService already owns Task state validation, transition RBAC, request-bound identity, and the atomic SQLite state update. Claim ownership belongs in that same transaction.
The Task envelope is already mutated by the transition API. task.assignee is the existing downstream consumer field, so this repair makes it a server-owned Neo extension rather than adding a second graph edge or parallel owner record.
RequestContextService.getAgentIdentityNodeId() is the canonical claimant identity. Caller payload, session id, subject/body text, and Fleet process identity are not assignment authority.
properties.lastModifiedAt is the existing transition clock required by the graduated A2A contract. Wake and later lifecycle reads must consume it, not synthesize a timestamp.
This is Memory Core source ownership only. The zero-authority lifecycle normalizer later maps source facts into fixed stages; it does not repair Task identity.
The Fix
1. Make task.assignee server-owned
When addMessage() accepts a Task:
for a direct AgentIdentity recipient, clone the caller Task and overwrite task.assignee with the canonical validated recipient;
for AGENT:*, overwrite task.assignee with null until a claimant wins;
never trust a caller-supplied assignee;
stamp the MESSAGE with top-level taskAssignmentAuthority: 'memory-core.v1', outside caller-controlled Task JSON, so later readers can distinguish a Memory-Core-owned assignment from legacy spoofable payload;
leave non-Task messages unchanged.
Document assignee and its provenance marker as read-only/server-owned Neo Task extensions on the Memory Core MCP surface and in the A2A guide.
2. Bind the broadcast winner atomically
For Submitted → Working:
direct Task: preserve/backfill the canonical direct recipient as assignee;
broadcast Task: write task.state = 'Working', task.assignee = <bound claimant>, taskAssignmentAuthority = 'memory-core.v1', and properties.lastModifiedAt = <same timestamp> in the same guarded SQLite update;
the race loser refreshes both state and assignee from SQLite and cannot overwrite the winner.
3. Enforce later RBAC against the winner
For assignee-owned transitions out of Working, compare the bound identity with the persisted canonical task.assignee, not the broadcast audience edge.
Backward compatibility is fail-closed:
a legacy direct Task without task.assignee may derive/backfill only from its one direct SENT_TO AgentIdentity;
a legacy broadcast Task already beyond Submitted (including Working or InputRequired) without a trusted provenance marker plus canonical task.assignee has no provable winner and must fail closed with an explicit owner-unknown error;
originator-owned transitions retain the existing RBAC matrix when canonical ownership is known; Submitted → Canceled remains valid for an unclaimed broadcast because it cannot recreate ownerless active work.
4. Truth-sync the wake consumer
TASK_STATE_CHANGED matches the originator or an assignee carrying the exact server-owned taskAssignmentAuthority marker and reports properties.lastModifiedAt as the transition clock. It must not accept a caller-spoofed assignee or fall back to a stale send timestamp for a real transition.
Contract Ledger
Target Surface
Source of Authority
Proposed Behavior
Fallback / Edge Case
Docs
Evidence
Existing MailboxService.addMessage({to, task})
ADR 0035 §2.3/§2.4 + validated MESSAGE routing
Preserve caller Task content but overwrite server-owned task.assignee: direct canonical recipient or null for broadcast; stamp top-level taskAssignmentAuthority
Notify originator or canonical assignee with the actual transition clock
Missing/untrusted assignee cannot widen recipients; malformed Task returns no match
JSDoc
Evaluator + subscription tests for recipient and timestamp
Existing Task read surfaces
Memory Core MESSAGE Task projection
get_message / list_messages expose the canonical task.assignee after write/transition
Legacy absent stays visibly absent until safe direct backfill; never infer broadcast winner
OpenAPI
Read-after-write and read-after-claim specs
Decision Record Impact
Aligned with ADR 0035. This localized source-authority repair implements the Task owner fact required by §2.3 and the categorical identity rule in §2.4. It does not amend the four-surface composition, add graph ontology, or change Fleet capabilities.
Decision Record: Not needed — the cross-substrate decision already graduated through Discussion #15090 and ADR 0035; this ticket repairs the existing Memory Core primitive beneath it.
Acceptance Criteria
Direct Task creation stores the canonical validated direct recipient in server-owned task.assignee, overriding any caller-supplied value.
Task creation and safe legacy backfill stamp top-level taskAssignmentAuthority: 'memory-core.v1'; caller-authored Task JSON cannot forge this provenance.
The winning broadcast Submitted → Working update atomically persists state, bound claimant, provenance marker, and one lastModifiedAt.
A losing concurrent claimant reads back the winner's state and assignee and cannot overwrite either.
After Bob claims a broadcast Task, Charlie cannot execute Working → InputRequired, Completed, or Failed; Bob can.
Existing originator transitions (Submitted → Canceled, InputRequired → Working) remain valid when their ownership premise is sound and do not erase the canonical assignee; an ownerless legacy broadcast may still be canceled from Submitted but cannot resume from InputRequired.
Legacy direct Tasks without assignee backfill only from their one direct AgentIdentity recipient.
Legacy broadcast Tasks beyond Submitted without trusted assignment provenance fail closed with an explicit owner-unknown diagnostic.
TASK_STATE_CHANGED targets only originator/canonical assignee and reports lastModifiedAt.
get_message and list_messages surface the canonical assignee; cross-process/cold-cache reads match SQLite.
TTL expiry preserves the assignee and advances the existing transition timestamp without broadening authority.
OpenAPI, JSDoc, and learn/agentos/A2A.md explain that task.assignee is server-owned and that broadcast audience is not post-claim ownership.
Focused Memory Core unit suites pass through the canonical Playwright unit configuration; no default Playwright command is used.
Out of Scope
Building the GitHub Workflow lifecycle raw-facts adapter.
Emitting or ordering LifecycleFrontier rows.
Hook projection storage/readers, Bird Views, fleet-awareness-read, or #14620 UI.
New Task states, task history/artifacts, reassignment/lease takeover, or automatic retry.
A new graph edge/ontology for Task assignment.
Treating ordinary broadcasts or issue assignments as actionable lifecycle work.
Avoided Traps
State-only “lock”: serializes the first write but leaves later authority cohort-wide.
Trust caller task.assignee: lets a message author spoof transition/wake authority.
Use SENT_TO: AGENT:* forever: confuses eligible claimants with the winning assignee.
Add a second assignment edge: creates dual state and atomicity problems when the Task JSON already owns transition state.
Build Fleet/lifecycle consumers first: makes an ambiguous source row look authoritative and multiplies repair cost.
Live latest-open sweep: checked the latest 20 open issues at 2026-07-12T20:38Z; no equivalent found. Recent all-state A2A claim sweep: only Emmy's earlier #15100 lane-intent overlaps this scope; no foreign collision.
The accepted Message WAL is immutable send-time intake truth. A full graph re-projection over an existing MESSAGE must therefore be storage-neutral for the MESSAGE node: do not read mutable Task/read/tombstone fields and then whole-node-upsert a merged snapshot. That still leaves a stale-writer window in which a peer transition can land after the read and be rewound by the replay write. If the MESSAGE row already exists, replay skips the node write entirely and may repair only independently missing edges/markers.
Post-marker node loss is a different failure class. Once the MESSAGE row is gone, intake WAL cannot prove whether the Task was still Submitted, already claimed, or terminal. Repair must never resurrect the send-time Task as claimable work. It restores the envelope as state: Unknown, assignee: null, with server provenance but no synthesized transition clock; that state is observable and non-claimable. A genuinely never-projected marker-less record still takes its WAL send-time Task on first projection.
Additional acceptance criteria:
Existing-node full replay performs no MESSAGE-node write; an interposed peer transition remains exact in SQLite/cache instead of being rewound by a stale snapshot.
Post-marker recovery of a missing Task MESSAGE restores non-claimable Unknown + assignee:null and cannot recreate WAL Submitted.
After a broadcast Task is claimed, replaying its original accepted WAL record preserves the canonical winning assignee, exact assignment-authority marker, state, and committed lastModifiedAt.
Evidence: focused existing-node replay, interposed-transition, and delete-after-claim recovery falsifiers alongside the existing read/retraction replay guards.
tobiu referenced in commit 69554b0 - "fix(ai): persist authoritative A2A Task ownership (#15106) (#15111)" on 12:41 AM
Context
ADR 0035 makes a claimed A2A Task one of the exact source facts behind the v13.2
LifecycleFrontier. A source audit before filing that producer found a lower-level authority defect in the current Memory Core state machine: a broadcast Task'sSubmitted → Workingtransition serializes the first state change, but it does not persist who won.This ticket repairs that source contract before any lifecycle composer, hook projection, Fleet reader, or UI consumes it. The ordering is deliberate: building those consumers over an ownerless
WorkingTask would turn an existing ambiguity into a trusted false fact.The Agent OS structure map was run against current
dev. The owner is the existingai/services/memory-core/MailboxService.mjstask-transition boundary and its existing wake consumer; no new.mjsfile or service directory is introduced.The Problem
MailboxService.transitionTask()currently derivesisAssigneefrom the MESSAGE'sSENT_TOedge on every transition. ForSENT_TO: AGENT:*, every authenticated recipient therefore remains an assignee after one peer has already wonSubmitted → Working.The optimistic
UPDATE ... WHERE task.state = 'Submitted'protects only the first state mutation. It does not bind later authority:Submitted.Submitted → Working.state: 'Working'but no winning assignee.isAssigneecheck and can executeWorking → InputRequired|Completed|Failed.That violates the original claim-and-lock intent from Discussion
#10313/ ticket#10338: the first claimant wins the Task, not merely the first write.Two adjacent facts confirm the missing owner:
addMessage()treats the Task payload as caller-authored opaque JSON apart from state validation, so a caller-suppliedtask.assigneeis not trustworthy.heartbeatPulseEvaluatoralready readstask.assignee, but no Memory Core write path owns or updates that field. It also reportsproperties.updatedAt || sentAteven though transitions persistproperties.lastModifiedAt.The negative ROI is severe: without this repair, stage-4 awareness can attribute one Task to multiple peers, wake the wrong actor, and permit a non-winning peer to terminally mutate another peer's claimed work.
The Architectural Reality
SENT_TO: AGENT:*means “claimable by the broadcast cohort”; it cannot remain the durable post-claim owner.MailboxServicealready owns Task state validation, transition RBAC, request-bound identity, and the atomic SQLite state update. Claim ownership belongs in that same transaction.task.assigneeis the existing downstream consumer field, so this repair makes it a server-owned Neo extension rather than adding a second graph edge or parallel owner record.RequestContextService.getAgentIdentityNodeId()is the canonical claimant identity. Caller payload, session id, subject/body text, and Fleet process identity are not assignment authority.properties.lastModifiedAtis the existing transition clock required by the graduated A2A contract. Wake and later lifecycle reads must consume it, not synthesize a timestamp.The Fix
1. Make
task.assigneeserver-ownedWhen
addMessage()accepts a Task:task.assigneewith the canonical validated recipient;AGENT:*, overwritetask.assigneewithnulluntil a claimant wins;taskAssignmentAuthority: 'memory-core.v1', outside caller-controlled Task JSON, so later readers can distinguish a Memory-Core-owned assignment from legacy spoofable payload;Document
assigneeand its provenance marker as read-only/server-owned Neo Task extensions on the Memory Core MCP surface and in the A2A guide.2. Bind the broadcast winner atomically
For
Submitted → Working:task.state = 'Working',task.assignee = <bound claimant>,taskAssignmentAuthority = 'memory-core.v1', andproperties.lastModifiedAt = <same timestamp>in the same guarded SQLite update;3. Enforce later RBAC against the winner
For assignee-owned transitions out of
Working, compare the bound identity with the persisted canonicaltask.assignee, not the broadcast audience edge.Backward compatibility is fail-closed:
task.assigneemay derive/backfill only from its one directSENT_TOAgentIdentity;Submitted(includingWorkingorInputRequired) without a trusted provenance marker plus canonicaltask.assigneehas no provable winner and must fail closed with an explicit owner-unknown error;Submitted → Canceledremains valid for an unclaimed broadcast because it cannot recreate ownerless active work.4. Truth-sync the wake consumer
TASK_STATE_CHANGEDmatches the originator or an assignee carrying the exact server-ownedtaskAssignmentAuthoritymarker and reportsproperties.lastModifiedAtas the transition clock. It must not accept a caller-spoofed assignee or fall back to a stale send timestamp for a real transition.Contract Ledger
MailboxService.addMessage({to, task})task.assignee: direct canonical recipient ornullfor broadcast; stamp top-leveltaskAssignmentAuthorityMailboxService.transitionTask()Submitted → Working#10313OQ5/OQ6 + ADR 0035 stage 4lastModifiedAttimestamp under the existing expected-state guardTASK_STATE_CHANGEDwake evaluationlastModifiedAtget_message/list_messagesexpose the canonicaltask.assigneeafter write/transitionDecision Record Impact
Aligned with ADR 0035. This localized source-authority repair implements the Task owner fact required by §2.3 and the categorical identity rule in §2.4. It does not amend the four-surface composition, add graph ontology, or change Fleet capabilities.
Decision Record: Not needed — the cross-substrate decision already graduated through Discussion #15090 and ADR 0035; this ticket repairs the existing Memory Core primitive beneath it.
Acceptance Criteria
task.assignee, overriding any caller-supplied value.task.assignee: null; caller payload cannot pre-claim it.taskAssignmentAuthority: 'memory-core.v1'; caller-authored Task JSON cannot forge this provenance.Submitted → Workingupdate atomically persists state, bound claimant, provenance marker, and onelastModifiedAt.Working → InputRequired,Completed, orFailed; Bob can.Submitted → Canceled,InputRequired → Working) remain valid when their ownership premise is sound and do not erase the canonical assignee; an ownerless legacy broadcast may still be canceled from Submitted but cannot resume from InputRequired.Submittedwithout trusted assignment provenance fail closed with an explicit owner-unknown diagnostic.TASK_STATE_CHANGEDtargets only originator/canonical assignee and reportslastModifiedAt.get_messageandlist_messagessurface the canonical assignee; cross-process/cold-cache reads match SQLite.learn/agentos/A2A.mdexplain thattask.assigneeis server-owned and that broadcast audience is not post-claim ownership.Out of Scope
LifecycleFrontierrows.fleet-awareness-read, or #14620 UI.Avoided Traps
task.assignee: lets a message author spoof transition/wake authority.SENT_TO: AGENT:*forever: confuses eligible claimants with the winning assignee.Related
#10338#14620Origin Session ID: f95e01ff-ba36-409a-98af-573263fab247
Retrieval Hint:
A2A broadcast Task winning assignee transitionTask lifecycle frontierRetrieval Hint:task.assignee server-owned claim-and-lock lastModifiedAtLive latest-open sweep: checked the latest 20 open issues at
2026-07-12T20:38Z; no equivalent found. Recent all-state A2A claim sweep: only Emmy's earlier #15100 lane-intent overlaps this scope; no foreign collision.Intake Contract Refinement — WAL Replay Durability
The accepted Message WAL is immutable send-time intake truth. A full graph re-projection over an existing MESSAGE must therefore be storage-neutral for the MESSAGE node: do not read mutable Task/read/tombstone fields and then whole-node-upsert a merged snapshot. That still leaves a stale-writer window in which a peer transition can land after the read and be rewound by the replay write. If the MESSAGE row already exists, replay skips the node write entirely and may repair only independently missing edges/markers.
Post-marker node loss is a different failure class. Once the MESSAGE row is gone, intake WAL cannot prove whether the Task was still Submitted, already claimed, or terminal. Repair must never resurrect the send-time Task as claimable work. It restores the envelope as
state: Unknown,assignee: null, with server provenance but no synthesized transition clock; that state is observable and non-claimable. A genuinely never-projected marker-less record still takes its WAL send-time Task on first projection.Additional acceptance criteria:
Unknown + assignee:nulland cannot recreate WALSubmitted.lastModifiedAt.Evidence: focused existing-node replay, interposed-transition, and delete-after-claim recovery falsifiers alongside the existing read/retraction replay guards.