Skip to content

feat(gateway): resume a woken task as a new turn and ask in the room [AgentActivity] - #5885

Merged
marklysze merged 112 commits into
sonichi:mainfrom
marklysze:feat/agent-activity-wake
Nov 1, 2026
Merged

marklysze merged 112 commits into
sonichi:mainfrom
marklysze:feat/agent-activity-wake

Conversation

@marklysze

@marklysze marklysze commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Not stacked. Its parent, #5883, merged on 2026-10-30 (850e74cb1). This PR is based on main (last merged: 9ab5350eb). The head is 5c64cae64abf8429f25732eecb0c330a0a8e1946.

Closes #6085.

Flags, upgrade, size

  • Both flags default off. LIFECYCLE_V2 = REMOTE_TASK_LIFECYCLE_V2 == "1", and TURN_CONTEXT = LIFECYCLE_V2 and REMOTE_TASK_TURN_CONTEXT == "1". With both unset, the bridge behaves as on main.

    • The Stop hook (src/check-pending-tasks.sh) changes only for a task file carrying a wake_episode: header, which only the bridge writes, and only under turn context.
  • In-flight attempts are dropped on upgrade. The attempt records move to one new ledger, state/relay-task-ledger{suffix}.json (format 3). The earlier relay-task-attempts*.json files are never read. Start logs <file> is from an earlier bridge and is ignored; its attempts are not resumed, and leaves the file unchanged. Witnessed live with a non-empty old ledger (section U of the round-18 witness).

  • Final size: 33 files, +8045 −981.

    Part Files Lines
    Runtime 16 +2954 −675
    Tests 13 +4750 −298
    Docs 3 +75 −8
    Rig-only witness launcher 1 +266

    The runtime files include 6 src/ modules and their vendored copies under packages/ag2-sparrow/ (sync_from_src --check is in sync). Most of the runtime change is in remote_gateway_bridge.py (+1488 −354) and task_attempts.py (+1102 −299).

What changed, and why

The resume half of the AgentActivity HITL flow (Rev 2.6 §8.2 steps 3–4, backend contract B13), and the attempt bookkeeping it needs. Everything is behind the flags above.

  1. A wake is a new run. When the owner answers a question, the broker re-serves the task with wake.resolved_blockers. That wake runs once per blocker_episode_id:

    • as a new run of the task, <local>~~<n>;
    • with a new turn;
    • with its answer appended as quoted, untrusted room data;
    • with its result on its own outbox item, <task>.wake-<episode>.

    A settled episode is never run again.

  2. Ask in the room. Under turn context, each task file carries one writer-owned line telling the agent to ask a blocking question with room.question.ask on its exact broker task_id (task_attempts.ASK_IN_ROOM_RULE).

  3. One writer for attempt state. Every change to a task's attempt goes through TaskAttempts.transition(wire): a per-task lock, plus a leaf commit under a cross-process flock that re-reads, checks the ownership epoch, and writes temp → fsync → rename → fsync dir. It fails closed. No provider call is made with a transition held (assert_unlocked).

  4. Claim, effect, commit.

    • Each broker effect (ack, result, cancel-ack) is claimed on the ledger with an operation id before it is sent. The op id is sent as Idempotency-Key.
    • The effect is committed only while the claim still owns the attempt.
    • A crash between the effect and its commit is reconciled under the same op id after a restart.
    • A result claim records the exact outbox item before each publish.
  5. A newer generation is a new run.

    • A re-serve under a new lease is held (nothing published) until its ack names the generation.
    • The same generation adopts the lease.
    • A newer one stops the earlier run, then switches to a run of its own, with its own item <task>.run-<n>.
    • The same holds after the earlier run has finished, e.g. when its answer was refused as stale. A serve under a lease other than the one the run finished under, or for a run whose details were pruned, is held for its ack. It's never taken for a redelivery of the finished run.
    • Every run after the first has its own item; new_run assigns it.
    • A newer generation's task file says what became of the earlier attempt's answer (delivered, refused, or sent as room messages), from what the bridge recorded, so the core never takes it for a redelivery it already answered.
    • One invariant: an ack the broker accepted counts as done only once its generation can reach the core (its run on disk or owed a publish, or a newer serve has replaced it); otherwise it is kept and retried (_ack_unlanded). Before the switch, every result the earlier run claimed is settled under its op id: unsent is withdrawn, DELIVERED or PARKED is final, still sending waits.
    • Anything that can't be read keeps the claim, and the wait is logged with what clears it.

Review first

  1. packages/ag2-sparrow/ag2_sparrow/task_attempts.py: TaskAttempts, Transition (claim, holds_claim, commit_claim, release_claim, note_item, new_run, open_wake, revive_finished, adopt_serve/switch_run), _commit, run_item.
  2. packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:
    • serves and acks: _serve_attempt, _hold_reserve, _claim_ack, _commit_ack, _decide_held_serve;
    • the switch: _reconcile_earlier_run, _end_unpublished_claims, _claim_published, _stop_run;
    • the drain: _pin_and_claim, _rule_result, _deliver_result_payload, _committed;
    • the tri-state reads: _on_disk, _outbox_record.
  3. docs/remote-gateway-protocol.md, "Task lifecycle (opt-in)".

User behaviour this proves

With both flags on, the agent asks the owner a question in the room. The task shows waiting_input. After the owner answers, the agent resumes with the answer as a new turn and its reply is delivered once. This holds across a bridge restart, a Stop, a crash after the broker accepted an ack or a result, and a re-serve under a newer generation. With the flags off, nothing changes.

Docs

docs/remote-gateway-protocol.md, "Task lifecycle (opt-in)", is updated, and its docs/catalog.json last_verified is 2026-11-01. CHANGELOG-PENDING.md describes the ledger and the upgrade drop.

Before / after

Before: parent a3efa9b6f (main) with only this PR's test files (and the rig launcher they load) copied in:

$ git log --oneline -1
a3efa9b6f feat(ag2space-voice): navigate_space — "go to the Commons list" and "go back" move the owner's own screen (#6057)
$ python3 tests/gateway-agent-activity-hooks.test.py
AttributeError: 'TaskAttempts' object has no attribute 'take_ownership'        (exit 1)
$ python3 tests/sparrow-task-attempts.test.py
AttributeError: module 'ag2_sparrow.task_attempts' has no attribute 'UNKNOWN'  (exit 1)
$ python3 tests/sparrow-task-transition.test.py
AttributeError: module 'ag2_sparrow.task_attempts' has no attribute 'UNKNOWN'  (exit 1)
$ python3 tests/gateway-agent-activity-lifecycle.test.py
AssertionError: the woken task queued: not seen (bridge exit None); its log ends:            (x2)
AssertionError: the log line 'wake for task-aa1: not run here': not seen (bridge exit None); its log ends:
Ran 6 tests in 185.649s
FAILED (failures=3)
$ python3 tests/local-task-run-ids.test.py
AttributeError: module 'local_task_protocol' has no attribute 'run_task_id'   (x2)
Ran 2 tests in 0.001s
FAILED (errors=2)
$ python3 tests/witness-lifecycle-fault-launcher.test.py
AttributeError: 'TaskAttempts' object has no attribute 'take_ownership'        (exit 1)

After: this head:

$ python3 tests/gateway-agent-activity-hooks.test.py
Ran 177 tests in 23.951s
OK
$ python3 tests/sparrow-task-attempts.test.py
Ran 53 tests in 0.542s
OK
$ python3 tests/sparrow-task-transition.test.py
Ran 23 tests in 0.517s
OK
$ python3 tests/gateway-agent-activity-lifecycle.test.py
Ran 7 tests in 22.470s
OK
$ python3 tests/local-task-run-ids.test.py
Ran 2 tests in 0.000s
OK
$ python3 tests/witness-lifecycle-fault-launcher.test.py
Ran 9 tests in 0.779s
OK

Each review round's blocker has its own before/after (its regression failing at the previous head, passing at the fix) in the round replies: round 14, 15, 16, 17, 19.

Tests run

  • The six files above, plus tests/gateway-disposal-delegation.test.py (85 OK) and tests/hermetic-env-structural.test.py (28 OK).
  • Every file scripts/discover-python-tests.sh lists (1,320), 6 at a time, each under a timeout. 19 fail, and all 19 fail identically on main (environment on the author's Mac). None are touched here.
  • sync_from_src --check: in sync. git diff --check: clean. review-checks: PASS. ruff --select F: clean.
  • CI at 5c64cae6: 47 passed, 3 skipped, 0 failed.

Live round trip

Run by Mark's coordinator agent on Mark's Mac: a local k3d rig with a real broker and both flags on, after a plain restart. The raw output is in the round-18 witness at 295c9d1a4. It covers:

  • U, the upgrade with a non-empty old ledger;
  • A, ask, wake and one reply (answer to reply in 25 s);
  • B, Stop while waiting;
  • C, a same-episode replay across a stop and restart;
  • D, Stop during a tasks-dir outage and a restart;
  • E, Stop during a stalled result POST;
  • F1 and F2, a kill right after the broker accepted the ack, and the result, each reconciled once after a plain restart.

scripts/witness/lifecycle-fault-launcher.py (rig-only, never imported by production) injects the stall and kill faults. Its reclaim fault drives the stale-generation schedule against the real broker: it withholds the task's beats until the broker reclaims the lease, and keeps the newer serve back until generation 1's answer has had its real 409.

Live status.

  • At e66aea13, the coordinator's rig passed the ordinary post-restart round trip and every live section except G.
  • Also at 8e87d036, the stale-generation schedule (G) passed every bridge step against the real broker: a real 409, L2 delivered and held, ~~2 queued once. But the core then took ~~2 for a redelivery and sent nothing. Fixed at a619c219: the new run's task file says what became of the earlier answer.

Still owed at this head:

  • the live sections rerun at this head, G included;
  • the real voice ack_first with a named lease.

Edge cases checked

Each has a production-path regression:

  • crash after the broker accepted an ack or a result;
  • a newer serve at every boundary of a delivery;
  • a result disposed of without publishing (attribution refusal, dedup requeue or report, any drain exit);
  • an unreadable or unstattable reply, or an unreadable outbox item (kept, logged);
  • a close on a later run's item after a crash;
  • a generation served after the earlier one's answer was refused as stale (canonical 409), plus its same-lease and same-generation controls, and the pruned-state case;
  • a lost run entry;
  • ledger trimming keeping live and just-settled tasks;
  • run ids never reused;
  • ownership retried after a failed take.

Risks

  • Flags off: no change.
  • Flags on: the upgrade drops in-flight attempts, as above.
  • Rollback: unset the flags. Downgrading the bridge with the flags on ignores the new ledger, since main reads only relay-task-attempts*.json.
  • Merging doesn't publish the package; publish-sparrow.yml is a separate step.

Known gaps and follow-ups

Non-blocking per the reviewers, and all behind the two default-off flags. Not fixed in this PR:

  • An ack_first (voice relay-ask) wake is left unacked and logged once per episode, so a voice-path question doesn't resume in this release.
  • Upgrade drops records that were in flight in the earlier ledger format (relay-task-attempts*.json); start logs it.
  • Operator visibility: a persistent read failure is visible only as a decaying log line. It could name the path and the outbox command, or reach the owner.
  • Locking: some inflight reads iterate without the mutex.
  • Test gap: removing the cross-process flock is caught only by the lock-fault test.
  • _stop_run: an unreadable own-run item counts as answered (its result is quarantined anyway).
  • No-reply pin: the delivered-then-reissued case (answered) is pinned by the bridge-side tests only. A stronger pin that the core then sends no duplicate needs a real model.
  • Pre-existing helpers on main: _task_pending, _delivered_copy_exists and _archived_task_file use glob/exists, so an unreadable directory reads as empty. In this PR they're only OR-ed with tri-state reads. Every one of them is keyed on a local run id, and a new lease never lands on a finished run, so evidence about one generation can't swallow another.
  • A run's details pruned (more than 2,000 later tasks): a new lease is held and the broker's ack decides. A same-lease redelivery the broker re-acks as the same generation would run again.

Hardcoded paths

review-checks (hardcoded-paths, root-artifacts, prose-cap): PASS. Added lines use RESULTS_DIR, TASKS_DIR, _STATE and the workspace helpers; test fixtures use temp dirs.

Adds src/agent_turn.py (vendored into ag2-sparrow): the 0400 turn file at
state/turns/<task_id>_g<gen>_<ulid>.json, its .closed fence, the journal
reader for dispatch_intent/dispatch_outcome records, and classify_turn.
A plain-text answer may be sent only when the turn has no bound intent, no
unbound intent since it began, and core-api confirms zero operations.
…gate on results

Behind REMOTE_TASK_LIFECYCLE_V2 (default off) the bridge sends one lease-holder
worker_id and protocol_version 2 on ack, records the execution_generation the
ack reports, beats each live attempt on POST /v1/tasks/heartbeat, and puts the
generation and worker_id on every result. A fenced attempt (task_cancelled,
stale_execution_generation) stops the core through a CANCEL_INSTRUCTION task and
is cancel-acked; its late result is settled, never posted. Every five minutes
GET /v1/tasks/active is reconciled with the in-flight set.

With REMOTE_TASK_TURN_CONTEXT too, each attempt's turn file is written on ack and
closed when its result settles, and the result is gated on the turn's journal:
skipped when the task closed terminally through MCP, posted as waiting_input +
no_send while suspended, and posted with no_send when the same text was already
sent as a room message. A result 409 that ends the task, or a 400
waiting_input_without_blocker, settles the item instead of quarantining it. A
re-served task carrying `wake` is left unacked rather than closed as a
redelivery.
…ask names

The coverage gate measures tests/**/*.test.py in process: the package test moves
to tests/sparrow-task-attempts.test.py, the bridge hooks get an in-process suite,
and the vendored agent_turn.py copy is omitted like the other src/ copies. Two
cancels in one millisecond no longer share a control-task name, and a recorded
acked_at of 0 is read as itself.
…er turn context

With REMOTE_TASK_TURN_CONTEXT, a task re-served with `wake` after its question was
answered runs once per blocker_episode_id: it is queued again under the same id
with each answer appended to the task's line as quoted room data, acked at the
same generation into a new turn, and its result rides its own outbox item
`<id>.wake-<episode>`. The earlier attempt's archived copies no longer settle it.
Each task file also carries one writer-owned line telling the agent to ask a
question that blocks the task with room.question.ask on its exact task_id.
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 94%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_a.py (100%)
  • packages/ag2-sparrow/ag2_sparrow/gateway_instance.py (100%)
  • packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py (94.6%): Missing lines 3475,3477,4112,4147-4149,4164,4171,4197,4203,4223,4255,4262,4266,4334,4392,4410,4412-4415,4424,4474,4496,4587,4620,4637,4885,5785,5796,5798,5861,5863,6932,6955-6956,6958,7076,7090,7108,8485,8634,8636,8743,8803,8806,8959,8965-8966,8997,9069-9070,9114-9115,9168,9195,9205-9206,9928
  • packages/ag2-sparrow/ag2_sparrow/task_attempts.py (97.2%): Missing lines 309,333,375,500,592,609,646-648,662,719,766,779,782,866,1085-1086,1088,1221,1267
  • scripts/witness/lifecycle-fault-launcher.py (93.7%): Missing lines 130-131,147-153,155-156,158
  • src/agent_turn.py (100%)
  • src/delivery/disposal.py (96.2%): Missing lines 703,743
  • src/delivery/task_dispatch.py (92.3%): Missing lines 182-183
  • src/local_task_protocol.py (100%)
  • src/outbox.py (100%)
  • src/turn_ledger.py (100%)

Summary

  • Total: 2132 lines
  • Missing: 95 lines
  • Coverage: 95%

packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py

Lines 3471-3481

  3471         doc = _req("POST", f"/v1/tasks/{urllib.parse.quote(wire_tid, safe='')}/ack", body,
  3472                    timeout=_ACK_TIMEOUT_S, **bound)
  3473         recorded = _commit_ack(wire_tid, claim, doc) if claim is not None else "recorded"
  3474         if recorded != "unrecorded" and claim is not None and _ack_unlanded(wire_tid, lease_id):
! 3475             recorded = "unrecorded"             # accepted, but its generation has no run here yet
  3476         if recorded == "stale":
! 3477             return "stale"
  3478         if recorded == "unrecorded":
  3479             _log_backoff((tid, "ack-unrecorded"), f"ack_first task {tid}: accepted, but its generation "
  3480                          "could not be recorded — kept staged, retrying")
  3481             return "retry"

Lines 4108-4116

  4108     when the attempt holds neither, None when it cannot be read or the claim could not be saved."""
  4109     with _ATTEMPTS.transition(wire_tid) as t:
  4110         rec = t.rec
  4111         if rec is task_attempts.UNKNOWN:
! 4112             return None
  4113         pending = rec.get("pending_serve") if isinstance(rec, dict) else None
  4114         held = isinstance(pending, dict) and pending.get("lease") == lease_id
  4115         if not rec or not (rec.get("lease_id") == lease_id or held):
  4116             return "elsewhere"

Lines 4143-4153

  4143     with _ATTEMPTS.transition(wire_tid) as t:
  4144         if t.rec is task_attempts.UNKNOWN:
  4145             return "unrecorded"
  4146         if not t.holds_claim(claim):
! 4147             t.release_claim(claim)
! 4148             _log(f"task {wire_tid}: ack for a serve this attempt no longer holds — not recorded")
! 4149             return "stale"
  4150         rec = t.record_ack(claim, doc, _reenroll_identity() or None, open_turn=open_turn)
  4151         if rec == "generation":
  4152             t.release_claim(claim)
  4153     if rec in ("stale", "generation"):

Lines 4160-4168

  4160     core: its serve still held, or the record holding it with its run neither on disk nor owed a
  4161     publish. A record now holding another serve, or none, superseded or settled it."""
  4162     rec = _ATTEMPTS.entry(wire_tid)
  4163     if rec is task_attempts.UNKNOWN:
! 4164         return True
  4165     if not isinstance(rec, dict) or not lease:
  4166         return False
  4167     pending = rec.get("pending_serve")
  4168     if isinstance(pending, dict):

Lines 4167-4175

  4167     pending = rec.get("pending_serve")
  4168     if isinstance(pending, dict):
  4169         return pending.get("lease") == lease
  4170     if rec.get("lease_id") != lease or rec.get("finished"):
! 4171         return False
  4172     run = rec.get("run")
  4173     return not (isinstance(run, str) and (rec.get("owed_publish") == run or _run_id_used(run)))
  4174 

Lines 4193-4201

  4193             return "stale"
  4194         if t.serve_adoptable(gen):
  4195             rec = t.adopt_serve(claim, doc, _reenroll_identity() or None, open_turn=open_turn)
  4196             if isinstance(rec, dict) and not _run_id_used(rec["run"]):
! 4197                 rec = t.rec if t.put(owed_publish=rec["run"]) else None   # its run was never published
  4198             adopted = True
  4199         elif t.rec.get("finished") and (gen is None or gen == t.rec.get("execution_generation")):
  4200             # The run already settled this generation: the re-serve has nothing left to run.
  4201             rec, adopted = ({"execution_generation": gen} if t.abandon_serve() else None), True

Lines 4199-4207

  4199         elif t.rec.get("finished") and (gen is None or gen == t.rec.get("execution_generation")):
  4200             # The run already settled this generation: the re-serve has nothing left to run.
  4201             rec, adopted = ({"execution_generation": gen} if t.abandon_serve() else None), True
  4202         elif gen is None:
! 4203             rec, adopted = (t.rec if t.abandon_serve() and t.release_claim(claim) else None), True
  4204         else:
  4205             adopted = False
  4206     if adopted:
  4207         if isinstance(rec, dict):

Lines 4219-4227

  4219     except (OSError, ValueError):
  4220         context = {}
  4221     with _ATTEMPTS.transition(wire_tid) as t:
  4222         if t.rec is task_attempts.UNKNOWN or not t.holds_claim(claim):
! 4223             return "unrecorded"
  4224         earlier = t.rec.get("run")
  4225         if not _stop_run(t, earlier, "a newer generation of this task runs as its own run"):
  4226             return "unrecorded"
  4227         root = _delivery_core().backend.root

Lines 4251-4259

  4251     if not _end_unpublished_claims(wire_tid):
  4252         return False                            # claimed, its file still there, not yet in the outbox
  4253     rec = _ATTEMPTS.entry(wire_tid)
  4254     if not isinstance(rec, dict):
! 4255         return False
  4256     root = _delivery_core().backend.root
  4257     for op, c in (rec.get("claims") or {}).items():
  4258         if c.get("kind") != "result":
  4259             continue

Lines 4258-4270

  4258         if c.get("kind") != "result":
  4259             continue
  4260         published = _claim_published(wire_tid, op, c)
  4261         if not published or published == task_attempts.UNKNOWN:
! 4262             return False
  4263         for item in published:
  4264             record = _outbox_record(root, item)
  4265             if record == task_attempts.UNKNOWN:
! 4266                 return False
  4267             if (record or {}).get("status") in ("DELIVERED", "PARKED"):
  4268                 continue
  4269             try:
  4270                 if not withdraw_unsent(root, item, lambda _payload: True):

Lines 4330-4338

  4330     nothing was sent. Files are read before the outbox, which a publish writes before the file goes.
  4331     False while one's reply is still there with no item: its delivery may be under way."""
  4332     with _ATTEMPTS.transition(wire_tid) as t:
  4333         if not isinstance(t.rec, dict):
! 4334             return False
  4335         open_ = True
  4336         for c in t.claims("result"):
  4337             tid = str((c.get("data") or {}).get("tid") or "")
  4338             copy = _claimed_copy_present(tid, c) if tid else True

Lines 4388-4396

  4388     whose lease is gone ends without a run; an answer about another serve moves nothing. False when
  4389     that could not be recorded."""
  4390     with _ATTEMPTS.transition(wire_tid) as t:
  4391         if t.rec is task_attempts.UNKNOWN:
! 4392             return False
  4393         if not t.holds_claim(claim) or _superseded_by_held(t.rec, claim):
  4394             return t.release_claim(claim)
  4395         return t.ack_settled(claim.get("serve")) and t.release_claim(claim)

Lines 4406-4419

  4406         return False
  4407     wire_tid, lease_id = _broker_tid(tid), claim.get("serve")
  4408     with _ATTEMPTS.transition(wire_tid) as t:
  4409         if t.rec is task_attempts.UNKNOWN:
! 4410             return True                         # its pending ack is kept: the retry is refused again
  4411         if not t.holds_claim(claim):
! 4412             t.release_claim(claim)
! 4413             _forget_pending_ack(tid, lease_id)
! 4414             _log(f"task ack for {tid}: refused ({code}) for a serve this attempt no longer holds — ignored")
! 4415             return True
  4416         if code == "stale_execution_generation" and _superseded_by_held(t.rec, claim):
  4417             t.release_claim(claim)
  4418             _forget_pending_ack(tid, lease_id)
  4419             _log(f"task ack for {tid}: refused ({code}) for the lease a held re-serve replaces — its own ack decides")

Lines 4420-4428

  4420             return True
  4421         if _held_serve(t.rec, claim):
  4422             # The held serve never ran: it ends without a run. Only a task-wide answer fences the run.
  4423             if not (t.abandon_serve() and t.release_claim(claim)):
! 4424                 return True
  4425             _forget_pending_ack(tid, lease_id)
  4426             _drop_reserve(wire_tid)
  4427             if code == "stale_execution_generation":
  4428                 _log(f"task ack for {tid}: its held re-serve was refused ({code}) — the run goes on")

Lines 4470-4478

  4470     """The control task is queued, taken, answered, archived or withdrawn: writing it again would
  4471     repeat it. None when a name cannot be told: neither written nor cleared this pass."""
  4472     marks = [_on_disk(TASKS_DIR / f".{ctl}.withdrawn"), _on_disk(RESULTS_DIR / f"{ctl}.txt")]
  4473     if True in marks or _task_pending(ctl) or _archived_task_file(ctl) is not None or _delivered_copy_exists(ctl):
! 4474         return True
  4475     return None if None in marks else False
  4476 
  4477 
  4478 def _run_tid(wire_tid: str, rec) -> str:

Lines 4492-4500

  4492     held in flight (its failure result is posted from there) and settled each pass. One newly held
  4493     is settled only once the in-flight set naming it is on disk."""
  4494     owing = _ATTEMPTS.owing()
  4495     if not owing:
! 4496         return
  4497     records = _ATTEMPTS.records() or {}
  4498     adopted = {_run_tid(w, records.get(w)) for w in owing} - inflight
  4499     if adopted:
  4500         if _hold_in_flight(inflight, adopted):

Lines 4583-4591

  4583     if due(rec.get("owed_close")):
  4584         _withdraw_closed(t, str(rec["owed_close"].get("why")))
  4585         rec = t.rec
  4586         if not isinstance(rec, dict):
! 4587             return
  4588     owed = rec.get("owed_ctl")
  4589     if due(owed) and isinstance(owed.get("id"), str):
  4590         ctl = owed["id"]
  4591         written = _ctl_written(ctl)

Lines 4616-4624

  4616 def _drive_cancel_acks(wire_tid: str) -> None:
  4617     """Send each claimed cancel-ack under its operation id, then commit it only while its claim
  4618     still owns the attempt. A failed or unknown send leaves the claim for the next pass."""
  4619     if _ATTEMPTS is None:
! 4620         return
  4621     with _ATTEMPTS.transition(wire_tid) as t:
  4622         claims = t.claims("cancel-ack")
  4623     for c in claims:
  4624         try:

Lines 4633-4641

  4633             _log(f"task {wire_tid}: cancel-ack failed ({diagnostics.describe(e)}) — retried next beat")
  4634             continue
  4635         with _ATTEMPTS.transition(wire_tid) as t:
  4636             if not t.cancel_acked(c):
! 4637                 t.release_claim(c)
  4638 
  4639 
  4640 def _thawed(value):
  4641     return json.loads(json.dumps(value))

Lines 4881-4889

  4881     queue or a result cannot be read: a read fault is not evidence that nothing is there."""
  4882     try:
  4883         names = os.listdir(TASKS_DIR)
  4884     except FileNotFoundError:
! 4885         names = []
  4886     except OSError:
  4887         return None
  4888     found = set()
  4889     for tid in tids:

Lines 5781-5789

  5781                 replay = isinstance(t.rec, dict) and bool(episode) and t.rec.get("wake_episode") == episode
  5782                 opened = t.open_wake(wake, first, used=_run_id_used)
  5783                 if (opened and replay and t.run_published(opened[1]) and not _task_pending(opened[1])
  5784                         and opened[1] not in (_load_held() or {})):
! 5785                     opened = None               # its run has left the queue: a replay publishes nothing
  5786             if not opened:
  5787                 if (first, episode) not in _WAKES_LEFT_UNACKED:
  5788                     _WAKES_LEFT_UNACKED.add((first, episode))
  5789                     why = ("an ack_first wake is not run" if hold else "turn context is off"

Lines 5792-5802

  5792                 return None
  5793             woken, tid = opened
  5794         else:
  5795             if t.revive_finished(first, lease, used=_run_id_used) is None:
! 5796                 _log_backoff((first, "unrecorded"), f"task {first}: its attempt could not be recorded "
  5797                              "— left unacked for a re-serve")
! 5798                 return None
  5799             rec = t.rec
  5800             if isinstance(rec, dict) and rec.get("run") and rec.get("lease_id") != lease:
  5801                 return _hold_reserve(t, first, task, lease)   # never rebound to the earlier run
  5802             tid = _serve_run(t, first)

Lines 5857-5867

  5857         return t.new_run(first, used=_run_id_used)
  5858     if (not t.run_published(cur) or _task_pending(cur) or cur in (_load_held() or {})
  5859             or _archived_task_file(cur) is not None or _delivered_copy_exists(cur)):
  5860         if (rec or {}).get("run") != cur and not t.put(run=cur):
! 5861             return None
  5862         return cur
! 5863     return t.new_run(first, used=_run_id_used)
  5864 
  5865 
  5866 def _run_id_used(local: str) -> bool:
  5867     """Whether anything of a local id is on disk: queued, staged, archived, answered or delivered.

Lines 6928-6936

  6928         return False
  6929     done = disposal.retire_generation(RESULTS_DIR, path, generation, _log, ARCHIVE_RESULTS_DIR,
  6930                                       _names(f"{tid}-{int(time.time())}"))
  6931     if disposal.pinned(RESULTS_DIR, tid, generation):
! 6932         return False                            # its pin still holds the id: retried next pass
  6933     if done.outcome is disposal.Retirement.REPLACEMENT_LIVE:
  6934         _log(f"result {tid}: the reply this pass sent was replaced at its name before it was "
  6935              "archived; the newer one stays live")
  6936         return False

Lines 6951-6962

  6951 
  6952 def _retire_superseded(tid: str, generation) -> None:
  6953     """A result delivered for an attempt its task has since moved on from: only its own file is
  6954     retired; the task stays with the attempt that holds it now."""
! 6955     if generation is not None:
! 6956         disposal.retire_generation(RESULTS_DIR, RESULTS_DIR / f"{tid}.txt", generation, _log,
  6957                                    ARCHIVE_RESULTS_DIR, _names(f"{tid}-{int(time.time())}-superseded"))
! 6958     _log(f"result {tid}: settled for an earlier attempt; the task stays with the attempt that holds it now")
  6959 
  6960 
  6961 def _archive_task_file(tid: str) -> bool:
  6962     """Move a served task out of the queue. True once no file of it is left there;

Lines 7072-7080

  7072     if mapping is None and not isinstance(_ATTEMPTS.entry(_broker_tid(tid)), dict):
  7073         return True
  7074     at_name = _on_disk(rfile)
  7075     if at_name is None:
! 7076         return None
  7077     if not at_name:
  7078         return True                             # nothing at the name: the settle path decides
  7079     _, generation = _read_ready_generation(rfile)
  7080     why = ("its run was closed before this result was written" if mapping is not None

Lines 7086-7094

  7086     """This id's own result was archived or kept, or its own delivery succeeded;
  7087     an alias is judged only by its own copies, never by its primary's record."""
  7088     woken_item = _run_item(tid)
  7089     if woken_item == task_attempts.UNKNOWN:
! 7090         return False
  7091     if woken_item:
  7092         # The first attempt's copies are archived under this id; only the woken item decides.
  7093         root = getattr(_delivery_core().backend, "root", None)
  7094         record = _outbox_record(root, woken_item) if root is not None else None

Lines 7104-7112

  7104 def _open_wake_unsettled(tid: str) -> bool:
  7105     """A wake runs here under this id and its own result is not delivered yet."""
  7106     item = _run_item(tid)
  7107     if item == task_attempts.UNKNOWN:
! 7108         return True
  7109     if not item:
  7110         return False
  7111     root = getattr(_delivery_core().backend, "root", None)
  7112     record = _outbox_record(root, item) if root is not None else None

Lines 8481-8489

  8481         return True
  8482     with _ATTEMPTS.transition(broker_tid) as t:
  8483         done = t.finish(claim, delivered=delivered)
  8484     if done is False:
! 8485         _log_backoff((tid, "retire"), f"result {tid}: settled, but its attempt could not be retired yet "
  8486                      "— the result stays live and is retried")
  8487     return done
  8488 

Lines 8630-8640

  8630     if claim is not None and getattr(core.backend, "records_lifecycle", False):
  8631         with _ATTEMPTS.transition(broker_tid) as t:
  8632             noted = t.note_item(claim, item_id)
  8633         if noted is None:
! 8634             _log_backoff((tid, "item-unrecorded"), f"result {tid}: its outbox item could not be recorded "
  8635                          "on its claim — not published, retried next pass")
! 8636             return False
  8637         claim = noted
  8638         extra["lifecycle"] = _lifecycle_of(broker_tid, claim)
  8639     if item_id != broker_tid and not woken_item:
  8640         # A wake's item is its episode's own, so a delivered one is this result: re-offered, not re-sent.

Lines 8739-8747

  8739     """What became of the attempt a newer generation's run replaced, from what this bridge recorded
  8740     (`rec` and the replaced run's `earlier` entry, read together): its answer delivered, its result
  8741     undelivered but its own room messages sent, or neither; None for a run that replaced nothing."""
  8742     if earlier is None:
! 8743         return None
  8744     if rec.get("earlier_delivered") or earlier.get("delivered") is True:
  8745         return "answered"
  8746     records = task_attempts.agent_turn.read_turn_records(_STATE, "", task_id=wire_tid)
  8747     if not records.readable:

Lines 8799-8810

  8799     if root is None or _on_disk(rfile) is not False:
  8800         return False                            # something is, or may be, live at the name: keep looking
  8801     record = _outbox_record(root, item_id)
  8802     if record == task_attempts.UNKNOWN:
! 8803         return False
  8804     record = record or {}
  8805     if not _delivered_for_current_attempt(record):
! 8806         return False
  8807     if record.get("status") == "DELIVERED":
  8808         return _settle_task(tid, inflight)
  8809     if record.get("status") != "PARKED" or not is_refused_for_good(record.get("reason")):
  8810         return False

Lines 8955-8963

  8955 
  8956 def _delivery_worker(tid: str, rfile: Path, raw: str, generation, inflight: "set[str]") -> None:
  8957     try:
  8958         if _deliver_ready(tid, rfile, raw, generation, inflight):
! 8959             _save_inflight(inflight)
  8960     except Exception as e:  # noqa: BLE001 — one result's failure never stops another's
  8961         _log(f"result {tid}: delivery error (isolated): {diagnostics.describe(e)}")
  8962     finally:
  8963         try:

Lines 8961-8970

  8961         _log(f"result {tid}: delivery error (isolated): {diagnostics.describe(e)}")
  8962     finally:
  8963         try:
  8964             _end_unpublished_claims(_broker_tid(tid))
! 8965         except Exception as e:  # noqa: BLE001 — a claim left open is ended by the next reconcile
! 8966             _log(f"result {tid}: its claim could not be ended yet ({diagnostics.describe(e)})")
  8967         with _DELIVERING_MUTEX:
  8968             _DELIVERING.discard(tid)
  8969 

Lines 8993-9001

  8993                 inflight.discard(tid)
  8994                 changed = True
  8995                 continue
  8996         if _ATTEMPTS is not None and _delivering(tid):
! 8997             continue                            # its delivery is still under way on its own worker
  8998         raw, generation = _pin_and_claim(tid) if _ATTEMPTS is not None else _read_ready_generation(rfile)
  8999         if raw is None:
  9000             # A disposed-of id whose task could not be archived earlier is retried here.
  9001             if not os.path.lexists(rfile):

Lines 9065-9074

  9065                 if not delivered:
  9066                     changed |= _settle_delivered(tid, _broker_tid(_delivery), rfile, inflight)
  9067                     return changed
  9068                 if delivered == "kept":
! 9069                     _retire_superseded(tid, generation)
! 9070                     return changed
  9071             _holder = (skip.extra or "").strip()
  9072             # An out-of-grammar holder is sender-controlled; name its shape,
  9073             # never its bytes.
  9074             _shown = (_holder if local_task_protocol.valid_archive_lookup_id(_holder)

Lines 9110-9119

  9110         if not delivered:
  9111             changed |= _settle_delivered(tid, _wire, rfile, inflight)
  9112             return changed
  9113         if delivered == "kept":
! 9114             _retire_superseded(tid, generation)
! 9115             return changed
  9116         with _retiring(tid):  # the archive and the id's retirement are one step to a wake
  9117             if not _archive_result(rfile, tid, generation):
  9118                 return changed
  9119             # Retire the provenance WITH the result, never at read: this line is

Lines 9164-9172

  9164         for ordinal, fp in enumerate(attaches):
  9165             # Uploads happen before the result POST (so failures can be
  9166             # annotated in-band); if that POST then fails and this loop
  9167             if (tid, fp) in _uploaded_attachments:
! 9168                 sent += 1
  9169                 continue
  9170             if task_media:
  9171                 status, reason = _upload_task_attachment(
  9172                     _wire, ordinal, fp, confine_root=confine_root)

Lines 9191-9199

  9191                 _log(f"attachment skipped for {tid}: {fp} ({reason})")
  9192         if deferred:
  9193             return changed
  9194         if not out_body.strip() and sent:
! 9195             out_body = "(file attached)"
  9196     # A redirected answer leaves the asking room, so it never asks for a thread there.
  9197     thread_ask = redirect is None and any(a.kind == "thread-ask" for a in parsed.actions)
  9198     delivered = _deliver_result_payload(tid, _wire, out_body, result_file=rfile,
  9199                                         generation=generation, thread_ask=thread_ask,

Lines 9201-9210

  9201     if not delivered:
  9202         changed |= _settle_delivered(tid, _wire, rfile, inflight)
  9203         return changed
  9204     if delivered == "kept":
! 9205         _retire_superseded(tid, generation)
! 9206         return changed
  9207     with _retiring(tid):  # the archive and the id's retirement are one step to a wake
  9208         if not _archive_result(rfile, tid, generation):
  9209             return changed
  9210         _inflight_discard(inflight, tid)

Lines 9924-9932

  9924     atexit.register(_retract_own_lane)
  9925     inflight: set[str] = _load_inflight()
  9926     _recover_orphan_proactive()
  9927     if _ATTEMPTS is not None and not _ATTEMPTS.take_ownership():
! 9928         _log("attempt ledger: ownership not taken — no attempt is recorded, served or settled until it is")
  9929     _close_stale_turns(inflight)
  9930     for sweep in (_retire_delivered_marks, _sweep_held_dir):
  9931         try:
  9932             sweep()

packages/ag2-sparrow/ag2_sparrow/task_attempts.py

Lines 305-313

  305     def get(self, wire_tid: str) -> dict:
  306         """For inspection only: the record, {} when there is none."""
  307         rec = self.entry(wire_tid)
  308         if rec is UNKNOWN:
! 309             raise ValueError(f"attempt record {wire_tid} cannot be read")
  310         return rec or Frozen()
  311 
  312     def records(self) -> "dict | None":
  313         """Every attempt (a record or UNKNOWN); None when the ledger cannot be read."""

Lines 329-337

  329         return bool(rec) and ((_beats(rec) and not self.task_beat_disabled) or _owes(rec))
  330 
  331     def beating(self, wire_tids) -> bool:
  332         if self.task_beat_disabled:
! 333             return False
  334         data = self._read()
  335         return any(isinstance(r, dict) and _beats(r) for r in (_entry_of(data, t) for t in wire_tids))
  336 
  337     def broker_names_leases(self) -> bool:

Lines 371-379

  371             return UNKNOWN
  372         earlier = rec.get("earlier_run") if isinstance(rec, dict) else None
  373         runs = data.get("runs") or {}
  374         if earlier and not isinstance(runs, dict):
! 375             return UNKNOWN
  376         return rec, (dict(runs.get(earlier) or {}) if earlier else None)
  377 
  378     def run_item(self, local_tid: str):
  379         """The outbox item of a run's own result, the one name every reader and the delivery use: its

Lines 496-504

  496         live, closed = set(live_wire_tids), 0
  497         for p in agent_turn.open_turns(self.state_dir):
  498             task = (agent_turn.read_turn_context(p) or {}).get("task_id")
  499             if not isinstance(task, str):
! 500                 continue
  501             with self.transition(task) as t:
  502                 rec = t.rec
  503                 if rec is UNKNOWN:
  504                     continue

Lines 588-596

  588         return self._change(edit, f"the change of {', '.join(sorted(fields))}")[0]
  589 
  590     def update(self, **fields):
  591         """`put`, returning the record written ({} when nothing was)."""
! 592         return self.rec if self.put(**fields) else Frozen()
  593 
  594     def forget(self) -> bool:
  595         return self._change(lambda _d, _r: None, "the retirement")[0]

Lines 605-613

  605         def edit(data, rec):
  606             seqs = data.setdefault("seqs", {})
  607             seq = int(seqs.get(self.wire) or 0) + 1
  608             if seq == 1 and isinstance(rec, dict) and rec.get("run"):
! 609                 seq = 2                         # a counter lost with the record's run still live: never the bare id
  610             local = local_task_protocol.run_task_id(first_run, seq)
  611             while used(local) or local in (data.get("runs") or {}):
  612                 seq += 1
  613                 local = local_task_protocol.run_task_id(first_run, seq)

Lines 642-652

  642         return self._change(edit, "the run's publication")[0]
  643 
  644     # ── a re-serve under a new lease: held until its ack names the generation ──
  645     def pending_lease(self) -> "str | None":
! 646         rec = self.rec
! 647         pending = rec.get("pending_serve") if isinstance(rec, dict) else None
! 648         return pending.get("lease") if isinstance(pending, dict) else None
  649 
  650     def hold_serve(self, lease: "str | None", run: str, source: "str | None", wake=None) -> bool:
  651         """Record a serve under a new lease for a run that may already execute: the run keeps its
  652         generation and nothing is published until the serve's ack names one."""

Lines 658-666

  658         bound to (one never bound yet is generation 1, the first the broker issues)."""
  659         rec = self.rec
  660         pending = rec.get("pending_serve") if isinstance(rec, dict) else None
  661         if not isinstance(pending, dict) or not rec.get("run"):
! 662             return False
  663         bound = rec.get("execution_generation")
  664         same = gen is None or (bound == gen if type(bound) is int else gen == 1)
  665         quiet = not any(rec.get(k) for k in ("fenced", "stop_requested", "stop_confirmed", "dispatch_lost",
  666                                               "owed_close", "owed_ctl", "finished"))

Lines 715-723

  715         gen = response.get("execution_generation") if isinstance(response, dict) else None
  716         if not self.holds_claim(c) or not isinstance(pending, dict) or type(gen) is not int:
  717             return None
  718         if not self.owner.broker_names_leases():
! 719             self.owner._commit(lambda d: d.setdefault("contract", {}).update(names_lease_generation=True),
  720                                why="the broker's lease contract")
  721         if rec.get("turn_id"):
  722             self.close_turn(rec["turn_id"])
  723         fields = {**context, "run": pending["run"], "lease_id": pending.get("lease"), "pending_serve": None,

Lines 762-770

  762     def holds_claim(self, c: Claim) -> bool:
  763         """Whether the exact claim is still open and the attempt is still the one it was made for."""
  764         rec = self.rec
  765         if not isinstance(rec, dict):
! 766             return False
  767         stored = (rec.get("claims") or {}).get(c["op"])
  768         pending = rec.get("pending_serve")
  769         if c.get("kind") == "ack" and c.get("serve") != rec.get("lease_id") and not (
  770                 isinstance(pending, dict) and pending.get("lease") == c.get("serve")):

Lines 775-786

  775     def note_item(self, c: Claim, item: str) -> "Claim | None":
  776         """Record on the claim the outbox item its effect is about to be published as, before the publish;
  777         the claim as it now stands, or None when it no longer holds."""
  778         if not self.holds_claim(c):
! 779             return None
  780         items = list(c.get("items") or [])
  781         if item in items:
! 782             return c
  783         noted = {**_thaw(c), "items": [*items, item]}
  784         noted.pop("op")
  785         ok, _ = self._change(lambda _d, r: {**r, "claims": {**r["claims"], c["op"]: noted}}, f"item of {c['op']}")
  786         return Claim({**noted, "op": c["op"]}) if ok else None

Lines 862-870

  862         if rec is UNKNOWN:
  863             return False
  864         pending = rec.get("pending_serve") if rec else None
  865         if isinstance(pending, dict) and pending.get("lease") == lease and rec.get("lease_id") != lease:
! 866             return self.abandon_serve()         # the pending serve's lease is gone: the run goes on
  867         if not rec or not rec.get("awaiting_ack") or rec.get("lease_id") != lease:
  868             return True
  869         return self.put(awaiting_ack=False)

Lines 1081-1092

  1081                 wakes[self.wire] = (seen + [episode])[-WAKE_EPISODES_KEPT:]
  1082                 _bound(wakes, keep={self.wire, *(data.get("tasks") or {})})
  1083             if isinstance((r or {}).get("pending_serve"), dict):
  1084                 # A serve held for its generation outlives the run: its ack, claim included, still decides.
! 1085                 lease = r["pending_serve"].get("lease")
! 1086                 acks = {op: k for op, k in (r.get("claims") or {}).items()
  1087                         if k.get("kind") == "ack" and k.get("serve") == lease}
! 1088                 return {**r, "finished": True, "claims": acks or None, "turn_id": None, "owed_close": None,
  1089                         "owed_ctl": None, "dispatch_lost": None, "stop_requested": None}
  1090             return None
  1091         if not self._change(edit, "the retirement")[0]:
  1092             return False

Lines 1217-1225

  1217     """The task text with each resolved blocker appended as room data, on the task's one line."""
  1218     parts = []
  1219     for b in wake.get("resolved_blockers") or []:
  1220         if not isinstance(b, dict):
! 1221             continue
  1222         answer = b.get("answer") if isinstance(b.get("answer"), dict) else None
  1223         if b.get("outcome") == "expired" or answer is None:
  1224             said = "no answer: the question expired unanswered"
  1225         elif answer.get("kind") == "choice" and answer.get("label"):

Lines 1263-1271

  1263     tasks = data.get("tasks")
  1264     if tasks is None:
  1265         return None
  1266     if not isinstance(tasks, dict):
! 1267         return UNKNOWN
  1268     if wire_tid not in tasks:
  1269         return None
  1270     rec = tasks[wire_tid]
  1271     return _thaw(rec) if isinstance(rec, dict) else UNKNOWN

scripts/witness/lifecycle-fault-launcher.py

Lines 126-135

  126             if first:
  127                 _report(report, {"event": "offline: broker calls withheld", "path": path})
  128             raise urllib.error.URLError("withheld by the witness launcher (offline)")
  129         if seen["withheld"] and not seen.get("online"):
! 130             seen["online"] = True
! 131             _report(report, {"event": "online", "withheld_calls": seen["withheld"]})
  132         holding = sentinel.exists()
  133         record = mod._ATTEMPTS.entry(rule.task) if mod._ATTEMPTS is not None else None
  134         if seen["first"] is None and isinstance(record, dict) and record.get("lease_id"):
  135             seen["first"] = record["lease_id"]  # the lease the running generation holds

Lines 143-162

  143                 _report(report, {"event": "beats withheld", "task": rule.task})
  144             payload = {**payload, "tasks": kept}
  145         if (holding and method == "POST" and path.split("?")[0] == KINDS["result"]
  146                 and isinstance(payload, dict) and payload.get("id") == rule.task):
! 147             while seen["held"] is None and sentinel.exists():
! 148                 time.sleep(0.2)                 # the result goes once the newer serve exists
! 149             try:
! 150                 answer = real(method, path, payload, *a, **k)
! 151             except urllib.error.HTTPError as e:
! 152                 body = e.read().decode("utf-8", "replace")
! 153                 _report(report, {"event": "result posted after the newer serve", "status": e.code, "body": body,
  154                                  "execution_generation": payload.get("execution_generation")})
! 155                 raise urllib.error.HTTPError(e.url, e.code, e.msg, e.hdrs, io.BytesIO(body.encode()))
! 156             _report(report, {"event": "result posted after the newer serve", "status": 200, "answer": answer,
  157                              "execution_generation": payload.get("execution_generation")})
! 158             return answer
  159         answer = real(method, path, payload, *a, **k)
  160         if method != "GET" or path.split("?")[0] != KINDS["serve"] or not isinstance(answer, dict):
  161             return answer
  162         if not holding and seen["held_task"] is not None and not seen["delivered"]:

src/delivery/disposal.py

Lines 699-707

  699         pinned.parent.mkdir(exist_ok=True)
  700         try:
  701             os.link(Path(results_dir) / f"{stem}.txt", pinned)
  702         except FileExistsError:
! 703             pass
  704     return pinned
  705 
  706 
  707 def pinned(results_dir: Path, stem: str, generation: ResultIdentity) -> bool:

Lines 739-747

  739             log(f"result {stem}: the reply its delivery pinned, replaced at its name, was kept as {kept.name}")
  740         else:
  741             os.unlink(pinned)
  742     except FileNotFoundError:
! 743         pass
  744     except OSError as e:
  745         log(f"result {stem}: its pin could not be released ({e}) — retried")
  746         return False
  747     return True

src/delivery/task_dispatch.py

Lines 178-187

  178 
  179 def _mtime(path) -> "float | None":
  180     try:
  181         return os.stat(path).st_mtime
! 182     except OSError:
! 183         return None
  184 
  185 
  186 def ready_result_filenames(results_dir: "Path | str", filenames, *,
  187                            reader=read_ready_result) -> "set[str]":

…urn-gate

# Conflicts:
#	packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py
…re, a corrupt journal is unreadable

Under REMOTE_TASK_LIFECYCLE_V2 a result written before its task's ack named the
lease waits for that ack (at most 120 s), and records the proxy could not bind but
that name the task count as the attempt's turn from publication on, so neither
the lease fields nor the journal gate can be skipped by a fast core. Every fencing
error_code on either ack path (404 or 409) fences the attempt, retires its pending
ack and, for a published task, stops the core, except a stale older lease whose
newer serve is pending here. A journal line that does not parse makes the journal
unreadable instead of absent; an unterminated last line younger than 30 s defers
the result. A served task carrying `wake` is left unacked whether or not this node
saw its first attempt. A late ack for a task no longer queued opens no turn.
…pt to settle and always runs with its answer

Merges feat/agent-activity-turn-gate. A served `wake` is decided before any local
shortcut: run with its answer under turn context, or left unacked (turn context
off, ack_first, an episode already run, or no usable episode), whether or not
this node saw the first attempt. A wake served while an earlier attempt of the
same id is still settling here is left unacked until it is served again, under a
lock the settle also holds, so that settle can never retire the wake. Finishing
an attempt checks it is still the one the result belonged to, and opening a wake
starts a fresh turn.
…edelivery keeps the journal boundary

On a broker that has ever named a lease, a result whose ack never answered keeps
waiting until the ack lands, the attempt is fenced, or the server closes the task.
On a broker never seen to name one, the 120 s fallback is gated on the journal
records that name the task, so an early send, terminal action or blocker still
rules. A task served again before its ack keeps its first publication time, so
its earlier unbound room actions stay inside the turn's journal window.
…on every settle branch

The normal, skip-marker, dedup and orphan-recovery branches archive a result and
then retire its id (in-flight removal, room and media sidecars). Each now runs that
whole transition under the wake handoff lock and marks the id retiring, so a wake
served in that window, on another thread or this one, waits to be served again
instead of being published and then losing its tracking.
…-wake

# Conflicts:
#	packages/ag2-sparrow/ag2_sparrow/task_attempts.py
…r reconcile from its first publication

A result held because its ack never named the lease on a broker that names leases
is now released by the reconcile too: once the complete active listing omits the
task past the grace, the attempt is fenced, the core stopped, and the result
settled without a post.
…nd a settle never archives an open wake

The no-live-result recovery branch now judges disposal inside the same locked
retirement step as its settle, so a wake admitted after the judgement waits to be
served again. A settle also re-checks inside the lock: while a wake for the id is
open here and its own result is not delivered, nothing is archived or retired,
whatever the caller judged before it took the lock.
… every held task beats

A task_cancelled in a per-task beat no longer fences the attempt. The core gets one
CANCEL_INSTRUCTION task; once it has handled it, a result it wrote for the task is
posted normally with no cancel-ack, and with no result the stop is confirmed on
cancel-ack and a later result is settled. A task_cancelled on the ack still means
the task is not started and is cancel-acked at once. The per-task beat now runs on
its own 60 s cadence ahead of the client heartbeat's backoff, so a Stop reaches
every task the bridge holds for as long as it holds it.
…-wake

# Conflicts:
#	tests/gateway-agent-activity-hooks.test.py
@marklysze

Copy link
Copy Markdown
Collaborator Author

Round 20, Kewei's review at 5bed4b9 and the live run (head: 8e87d0368a8ade4962ac15210d6c36a58e60a733)

keweichen, thanks. Both of your P1s reproduce, and the coordinator's live run on the rig found a third. All three end the same way: the broker accepts a newer generation, and it never reaches the core.

The invariant, enforced in one place

An ack the broker accepted counts as done only once its generation can reach the core: its run's task file is on disk, or the record owes that run a publish, or a newer serve has replaced it. Otherwise the ack is not done: the pending ack is kept and retried.

_ack_unlanded(wire, lease) decides it.

  • _post_task_ack and _post_ack_first consult it after the commit, so an accepted ack can't return success while its generation is stranded.
  • A retry re-acks under its operation id (the broker answers duplicate with the same generation), and the commit then decides afresh.
  • test_an_accepted_ack_its_generation_cannot_reach_returns_no_success_and_is_retried pins it. A commit that returns stale while the serve is still held gives False, the pending ack is kept, the retry lands it, and generation 2 answers on its own run.

The three fixes below each remove a way to reach the stranded state. The invariant makes any path I missed a retry instead of a loss.

1. Live: on a named instance the newer run could never be published

Cause. _write_task turns task["id"] into the local id (task-<inst>~<broker>) before _serve_attempt, so _hold_reserve saved the held serve under that local id. _publish_owed_run then fed it to _write_task(task, run=run), whose _valid_tid refuses a ~ id: dropping task with unsafe id 'task-dlocal~task-aa1', retried forever. My in-process tests run with no instance name, where local and broker ids are equal, so they never saw it.

Fix.

  • The reserve is saved under the broker id ({**task, "id": t.wire}).
  • _publish_owed_run always publishes with id set to the wire id, so a reserve already on disk from the earlier head publishes too.
  • switch_run now clears finished. A generation switched from a revived finished record kept finished: true, so it didn't beat and a restart retired it. That's the "retired with the generation never run" you saw.

Regression, the real poll path. test_on_a_named_instance_a_generation_served_after_a_stale_refusal_reaches_the_core in tests/gateway-agent-activity-lifecycle.test.py runs a real bridge process with GATEWAY_INSTANCE=dlocal, against a relay on a real socket:

  1. L1 is served and acked as gen 1; its answer gets a real-shaped 409 stale_execution_generation and is settled.
  2. L2 is served and acked as gen 2.
  3. task-dlocal~task-aa1~~2 is queued, and gen 2's answer posts once, as (2, …).

At 5bed4b9 it fails with exactly the live log: dropping task with unsafe id 'task-dlocal~task-aa1'.

2. P1 A: finishing gen 1 erased an accepted gen 2 ack

Fix. finish on a record with a held serve now keeps that serve's ack claim (kind == "ack" and serve == pending_serve.lease), with every other claim dropped as before. So _commit_ack finds its claim, decides, and switches. The invariant would have caught this anyway: the ack is not done while its serve is held.

Regression: test_finishing_the_earlier_generation_keeps_a_newer_serves_accepted_ack, in your exact ordering, through the production functions:

  1. Gen 1's answer gets the 409 and pauses before _retired.
  2. L2 is served and held.
  3. L2's ack POST returns gen 2 and pauses before _commit_ack.
  4. Gen 1 finishes; the record keeps pending_serve L2.
  5. The ack resumes and switches to ~~2, and gen 2 answers on <task>.run-2.

At 5bed4b9 the run stays the original.

3. P1 B: one failed publication-marker write rebound gen 2 to gen 1's run

Fix. A newer lease no longer depends on that bit.

  • _serve_attempt holds every serve whose lease differs from the record's, whether or not the run's publication was recorded. A newer lease is never rebound to the earlier run.
  • revive_finished no longer needs published.
  • When the held serve's ack names the run's own generation and the run's task file isn't on disk (_run_id_used is false), the adopt sets owed_publish for that run and keeps the reserve, so the run is published from it. When the file is on disk (the marker write failed after it), nothing is republished.

Regression: test_a_failed_publication_marker_never_rebinds_a_newer_lease_to_the_earlier_run, with both interleavings. L1 is served with only the "the run's publication" commit failing (task file present, published false); later writes are healthy.

  • L1 running: L2 is held (pending_serve L2), acked as gen 2, switched to ~~2, and gen 2 answers.
  • L1 finished (409 stale, settled): L2 is held, never "dedup: already handled", acked as gen 2 on ~~2, and gen 2 answers.

Both fail at 5bed4b9.

Launcher

Changed. On release, the next poll hands the bridge the held serve exactly as the broker sent it (unless that poll already carries it), so L2 itself is acked. In the live run the bridge first saw the task about 165 s later, as L3. Reported as delivered the held serve; tested.

Checks at this head

  • tests/gateway-agent-activity-hooks.test.py: 175 OK
  • tests/sparrow-task-attempts.test.py: 53 OK
  • tests/sparrow-task-transition.test.py: 23 OK
  • tests/gateway-agent-activity-lifecycle.test.py: 7 OK (incl. the named-instance end-to-end)
  • tests/witness-lifecycle-fault-launcher.test.py: 9 OK
  • tests/gateway-ack-first.test.py: 65 OK
  • tests/gateway-disposal-delegation.test.py: 85 OK
  • tests/hermetic-env-structural.test.py: 28 OK
  • Every file scripts/discover-python-tests.sh lists: 1,320 files, 19 fail, all of them failing identically on main (environment on this Mac).
  • sync_from_src --check: in sync. git diff --check: clean. review-checks: PASS. ruff --select F: clean. origin/main is merged.
  • CI: green at this head (47 passed, 3 skipped, 0 failed).

Not done by me: a run against the real broker. The rig is the coordinator's. The end-to-end above uses a real bridge process and real HTTP, against the test relay.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Live stale-generation run at exact head 8e87d0368a8ade4962ac15210d6c36a58e60a733: the transport now works, and the reply is still lost (Mark's coordinator agent, local k3d rig, real broker, verbatim launcher, $APP/$S path replacements only, repeated retry lines removed).

Worked: real 409 stale_execution_generation (current 2), generation 1 settled and archived, the held L2 delivered, generation 2 gets its own run …~~2, queued …~~2 once, no dedup, no unsafe id, ledger empty after a plain restart. That closes the unsafe-id finding from my earlier comment.

Not solved: generation 2's core turn wrote [no-send] Redelivery of source message … already answered (delta141245) via the original task. That earlier answer is the one the broker refused, so the task ended suppressed and nothing reached the room. The core can't tell a refused answer from a delivered one, because ~~2 reads like a plain redelivery of the same source message.

The PR author is fixing this so a later run says the earlier answer was not delivered. Please hold your review of this head until the next push, which will change runtime bytes. A/B/C/D and the upgrade section also pass at this head; I'll repost the full set at the next one.

bridge output for this task
served task-c4f9c9f27a8e6f81a4 lease=lease-cc66dcc8fa6d
queued task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4
result POST not confirmed for task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4 (not_delivered: transport failure for task-c4f9c9f27a8e6f81a4: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793502875.693747, deadline=1793503473.692325
result POST not confirmed for task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4 (not_delivered: transport failure for task-c4f9c9f27a8e6f81a4: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793502879.8900921, deadline=1793503473.692325
result POST not confirmed for task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4 (not_delivered: transport failure for task-c4f9c9f27a8e6f81a4: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793502887.956874, deadline=1793503473.692325
result POST not confirmed for task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4 (not_delivered: transport failure for task-c4f9c9f27a8e6f81a4: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793502904.0827992, deadline=1793503473.692325
result POST not confirmed for task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4 (not_delivered: transport failure for task-c4f9c9f27a8e6f81a4: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793502934.330278, deadline=1793503473.692325
result POST not confirmed for task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4 (not_delivered: transport failure for task-c4f9c9f27a8e6f81a4: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793502964.746052, deadline=1793503473.692325
result task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4 accepted by the broker, room delivery unconfirmed (settled:stale_execution_generation), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
archived result for task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4
outbound worker: task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4 seen->retired 91504ms (monotonic)
served task-c4f9c9f27a8e6f81a4 lease=lease-9fad7bce497f
task task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4: served again under a new lease while its run may be executing — nothing is published until the ack names the generation
task task-c4f9c9f27a8e6f81a4: generation 2 gets its own run task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4~~2; the earlier run task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4 is stopped and its result is never posted for it
held in flight while their records owe work: task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4~~2
queued task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4~~2
result task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4~~2 accepted by the broker, room delivery unconfirmed (accepted), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
archived task-ag2space_dlocal~task-c4f9c9f27a8e6f81a4~~2 (marker no-send, lease closed, not sent)
launcher report
{"at": 1793500194.933236, "event": "loaded", "head": "5bed4b92880dbe3dbce65110f054519d680dabaa", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py", "module_sha256": "4c86bd37d080a0b71e36a6c661c26a7f475f3699e51b7872040fd97537868abe", "rule": {"fault": "reclaim", "generation": null, "kind": "serve", "lease": null, "report": "$S/at/rep-reclaim-132954.jsonl", "run": null, "sentinel": "$S/at/hold-132954", "task": "task-e41dde03c23202564d"}, "runtime_identity": {"build_sha": "5bed4b92880dbe3dbce65110f054519d680dabaa", "entrypoint": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_sha256": "4c86bd37d080a0b71e36a
{"at": 1793500210.306728, "event": "beats withheld", "task": "task-e41dde03c23202564d"}
{"at": 1793500270.5235682, "event": "offline: broker calls withheld", "path": "/v1/tasks/heartbeat"}
{"at": 1793500453.69118, "event": "online", "withheld_calls": 17}
{"at": 1793500453.7552042, "claim": {"execution_generation": 2, "kind": "serve", "lease_id": "lease-648773969e4d", "task": "task-e41dde03c23202564d"}, "event": "matched", "fault": "reclaim", "first_lease": "lease-3592e8046a15", "path": "/v1/tasks?wait=15&capabilities=voice-outcome-v1"}
{"at": 1793500453.875903, "body": "{\"error\": \"stale execution generation\", \"error_code\": \"stale_execution_generation\", \"current_execution_generation\": 2}", "event": "result posted after the newer serve", "execution_generation": 1, "status": 409}
{"at": 1793500468.788029, "event": "released", "lease_id": "lease-648773969e4d"}

…er was delivered, so the core never takes it for a redelivery it answered

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at a619c21. A newer generation's task file now gets one writer-owned rule line, task_attempts.rerun_rule, saying what became of the earlier attempt, so the core doesn't treat it as a redelivery it already answered. There are four cases:

  • answered: a real delivery receipt (_really_delivered excludes the broker's settled: refusals);
  • posted_in_room: the result wasn't delivered, but its own room.message.send messages were;
  • unknown: the turn journal can't be read;
  • not_delivered.

The flag is computed at the switch from the earlier run's recorded result items and kept on the run once it finishes. The line is appended by _write_task alongside the tier lines, so a message body can't forge it.

Ran here in a clean env: hooks 176 OK, lifecycle 7 OK, sparrow-task-attempts 53 OK. With the bridge and task_attempts.py reverted to a619c21^, the new "says what became of the earlier answer" case and both delivery-boundary cases fail.

Small note: answered tells the core to start with [no-send] "unless something new is needed". That leaves the call to the model, which is fine, but a test that a delivered-then-reissued task posts nothing would pin the intended default.

(agent-assisted, Blue for Chi's seat)

@marklysze

Copy link
Copy Markdown
Collaborator Author

Round 21, the live finding at 8e87d03 (head: a619c219d873b51de00eb3f9131fea7fc5d229c6)

The coordinator's live run at 8e87d03 confirmed the three round-20 fixes on the real broker:

  • a real 409 (current 2);
  • delivered the held serve with L2;
  • generation 2 gets its own run …~~2 and queued …~~2 exactly once;
  • no dedup line, no unsafe id.

But generation 2's core turn then wrote [no-send] Redelivery of source message … already answered … via the original task, so the room still got no reply.

Cause

The ~~2 task file looked like a plain redelivery of the same source message, and nothing told the core its earlier answer had been refused. There's no rule for this in the repo, no code path keyed on the source id: the core decided it was a repeat. So the fix gives the core the fact, from the bridge's own record, rather than relying on how it judges a repeat.

Fix

A newer generation's task file carries one writer-owned line (below the one-line body, like the wake and ask-in-room lines) that says what became of the earlier attempt. The bridge decides from what it recorded, never a guess:

What the bridge recorded Line (task_attempts.RERUN_RULES)
The earlier result's outbox item DELIVERED with a real receipt answered: it was delivered; don't post it again, [no-send] unless something new is needed
Not delivered, but the same task's earlier turn has room.message.send intents whose outcome isn't a failure (agent_turn's sibling_sends, the same rule that already stops a repeated room message from being posted) posted_in_room: the result wasn't delivered but those messages were; don't repeat them, post only what's new
Not delivered, journal unreadable unknown: check the room first, repeat nothing it shows, answer the rest
Neither not_delivered: it's not a redelivery; the earlier answer was refused, so answer again in full; don't [no-send] because of it

How "delivered" is known:

  • When the earlier result settled: _committed passes _really_delivered(record) to finish, which writes delivered on the run's ledger entry. Really delivered means status == DELIVERED and provider_ref not settled:…. A 409 stale_execution_generation / task_already_terminal / task_cancelled is stored as DELIVERED with settled:<code>, so it counts as not delivered.
  • When it went out just before the switch (the earlier run not yet finished): _decide_held_serve checks the earlier run's open result claims' recorded items the same way, and stores earlier_delivered with earlier_run on the switched record.

A first run carries no such line.

Tests

  • test_a_newer_generations_task_says_what_became_of_the_earlier_answer covers four cases, each through the production path (served and acked as gen 1, then a 409 or acceptance, settle, then L2 served and acked as gen 2, then the run published):

    • refused → not_delivered;
    • delivered → answered;
    • refused after the turn sent a room message → posted_in_room;
    • refused with the journal unreadable → unknown.

    Each ~~2 file carries exactly one line, and the first run's file carries none.

  • test_a_newer_serve_at_each_boundary_of_a_delivery_keeps_the_task_and_is_never_credited: generation 1's answer delivered just before the switch gives answered in ~~2.

  • test_on_a_named_instance_a_generation_served_after_a_stale_refusal_reaches_the_core (a real bridge process, named instance): the queued ~~2 file says the earlier answer was never delivered.

  • A real model reading the line can't be unit-tested. The coordinator will verify it live.

Checks at this head

  • tests/gateway-agent-activity-hooks.test.py: 176 OK
  • tests/sparrow-task-attempts.test.py: 53 OK
  • tests/sparrow-task-transition.test.py: 23 OK
  • tests/gateway-agent-activity-lifecycle.test.py: 7 OK
  • tests/witness-lifecycle-fault-launcher.test.py: 9 OK
  • tests/gateway-ack-first.test.py: 65 OK
  • tests/gateway-disposal-delegation.test.py: 85 OK
  • tests/hermetic-env-structural.test.py: 28 OK
  • tests/release-docs-audit.test.py: PASS
  • Every file scripts/discover-python-tests.sh lists: 1,320 files, 19 fail, all of them failing identically on main (environment on this Mac).
  • sync_from_src --check: in sync. review-checks: PASS. ruff --select F: clean. origin/main is merged.
  • CI: green at this head (47 passed, 3 skipped, 0 failed).
  • The launcher is unchanged since 8e87d03.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Raw witness at exact head a619c219d873b51de00eb3f9131fea7fc5d229c6 (run by Mark's coordinator agent on Mark's Mac, local k3d rig, real broker; unedited apart from replacing local paths with $APP and $S, and capping log lines at 1,500 characters; the launcher reports below are complete). CI at this head: 47 passed, 3 skipped, 0 failed.

Section Result
U. Upgrade with a non-empty old ledger The old ledger's record count and content are printed before start (1 record). Start logs it as ignored, the file is unchanged (same sha), and the probe is in neither the new ledger nor the bridge log
A. Ask, wake, one reply One wake, one final post, ledger settled
B. Stop while waiting Question cancelled, ended dead/cancel_confirmed
C. Same-episode replay across a stop and restart write_task=None, no task file, queued count 1
D. Stop during a tasks-dir outage, restart during it Stop retried through the outage and the restart, one control after recovery, dead/cancel_confirmed, ledger empty
E. Stop during a stalled result POST Stop answered in 0.02s. Task B posted and task C reached working and took a Stop (0.02s) while A stayed held. A posted once on release and ended responded. After a plain restart, C ended dead/cancel_confirmed
F1. Kill after the broker accepted the ack os._exit(9) after a real 2xx, plain restart reconciled under the same op id, one post, ledger empty
F2. Kill after the broker accepted the result Same, the retry was fenced task_already_terminal and settled without a post, one post, ledger empty
G. Stale generation, real broker Offline 180s, real re-lease to generation 2, generation 1's result got canonical 409 stale_execution_generation and settled, the held L2 was delivered and acked, generation 2 gets its own run …~~2, queued …~~2 once, no dedup. The ~~2 task carried the not-delivered line, the core answered in full, item <task>.run-2 ended DELIVERED under generation 2, the room got one reply, and the task ended responded with an empty ledger

The launcher ran verbatim from scripts/witness/lifecycle-fault-launcher.py at this head, via the production loader, with --lease '*'. Each matched line holds the matched claim's full identity including the lease. The rule fired once per run, and the reports hold no FAIL line. Each restart after a fault was plain src/remote-gateway-bridge.py.

Not covered live: the delivered-then-newer-generation case and the two injected-commit-failure schedules from your 5bed4b9 review. They rest on the regressions in the author's reply (6552718718, 6553089085).

Not yet done: the real voice ack_first with a named lease. We will run it post-restart on the head you consider the final candidate, as you asked. Please say when this head is that candidate. It needs Mark on a live call.

G: bridge output for the stale-generation run
served task-030b475adf6c9b8e02 lease=lease-0ec7a80c7b22
queued task-ag2space_dlocal~task-030b475adf6c9b8e02
result POST not confirmed for task-ag2space_dlocal~task-030b475adf6c9b8e02 (not_delivered: transport failure for task-030b475adf6c9b8e02: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793505365.927094, deadline=1793505963.9256918
result POST not confirmed for task-ag2space_dlocal~task-030b475adf6c9b8e02 (not_delivered: transport failure for task-030b475adf6c9b8e02: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793505370.103644, deadline=1793505963.9256918
result POST not confirmed for task-ag2space_dlocal~task-030b475adf6c9b8e02 (not_delivered: transport failure for task-030b475adf6c9b8e02: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793505378.143769, deadline=1793505963.9256918
result POST not confirmed for task-ag2space_dlocal~task-030b475adf6c9b8e02 (not_delivered: transport failure for task-030b475adf6c9b8e02: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793505394.2269819, deadline=1793505963.9256918
result POST not confirmed for task-ag2space_dlocal~task-030b475adf6c9b8e02 (not_delivered: transport failure for task-030b475adf6c9b8e02: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793505424.38341, deadline=1793505963.9256918
result POST not confirmed for task-ag2space_dlocal~task-030b475adf6c9b8e02 (not_delivered: transport failure for task-030b475adf6c9b8e02: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793505454.689312, deadline=1793505963.9256918
result POST not confirmed for task-ag2space_dlocal~task-030b475adf6c9b8e02 (not_delivered: transport failure for task-030b475adf6c9b8e02: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793505484.995456, deadline=1793505963.9256918
result task-ag2space_dlocal~task-030b475adf6c9b8e02 accepted by the broker, room delivery unconfirmed (settled:stale_execution_generation), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
archived result for task-ag2space_dlocal~task-030b475adf6c9b8e02
outbound worker: task-ag2space_dlocal~task-030b475adf6c9b8e02 seen->retired 126060ms (monotonic)
served task-030b475adf6c9b8e02 lease=lease-0295c668d585
task task-ag2space_dlocal~task-030b475adf6c9b8e02: served again under a new lease while its run may be executing — nothing is published until the ack names the generation
task task-030b475adf6c9b8e02: generation 2 gets its own run task-ag2space_dlocal~task-030b475adf6c9b8e02~~2; the earlier run task-ag2space_dlocal~task-030b475adf6c9b8e02 is stopped and its result is never posted for it
held in flight while their records owe work: task-ag2space_dlocal~task-030b475adf6c9b8e02~~2
queued task-ag2space_dlocal~task-030b475adf6c9b8e02~~2
result task-ag2space_dlocal~task-030b475adf6c9b8e02~~2 delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
archived result for task-ag2space_dlocal~task-030b475adf6c9b8e02~~2
G: harness output
=== G: stale generation 1 answer (real 409), then the newer generation runs
+   0.1s gateway stopped; bridges 0
+   0.6s task task-030b475adf6c9b8e02 [' queued          | f        |                    |                 |                    1 |       1 |             1']
+   0.6s launcher started in the gateway window --fault reclaim --kind serve --task task-030b475adf6c9b8e02 --sentinel $S/at/hold-145450 --report $S/at/rep-reclaim-145450.jsonl
+  10.7s gen 1 acked and queued [' working         | f        |                    |                 |                    1 |       1 |             3'] {"lease_id": "lease-0ec7a80c7b22", "execution_generation": 1, "run": "task-ag2space_dlocal~task-030b475adf6c9b8e02"}
+  10.7s offline: broker calls withheld
+ 190.9s after 180s offline [' queued          | f        |                    |                 |                    2 |       2 |             4']
+ 190.9s online again
+ 200.0s report {"at": 1793505291.170816, "event": "loaded", "head": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+ 200.0s report {"at": 1793505306.599624, "event": "offline: broker calls withheld", "path": "/v1/tasks/heartbeat"}
+ 200.0s report {"at": 1793505485.2936819, "event": "online", "withheld_calls": 24}
+ 200.0s report {"at": 1793505489.766548, "event": "beats withheld", "task": "task-030b475adf6c9b8e02"}
+ 200.0s report {"at": 1793505489.8255801, "claim": {"execution_generation": 2, "kind": "serve", "lease_id": "lease-0295c668d585", "task": "task-030b475adf6c9b8e02"}, "event": "matched", "fault": "reclaim", "first_lease": "lease-0ec7a80c7b22", "path": "/v1/tasks?wait=15&capabilities=voice-outcome-v1"}
+ 200.0s report {"at": 1793505489.975512, "body": "{\"error\": \"stale execution generation\", \"error_code\": \"stale_execution_generation\", \"current_execution_generation\": 2}", "event": "result posted after the newer serve", "execution_generation": 1, "status": 409}
+ 200.0s stale step True [' claimed         | f        |                    |                 |                    2 |       2 |             5']
[remote-gateway-bridge] result task-ag2space_dlocal~task-030b475adf6c9b8e02 accepted by the broker, room delivery unconfirmed (settled:stale_execution_generation), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignA
[remote-gateway-bridge] archived result for task-ag2space_dlocal~task-030b475adf6c9b8e02
[remote-gateway-bridge] outbound worker: task-ag2space_dlocal~task-030b475adf6c9b8e02 seen->retired 126060ms (monotonic)
+ 200.0s gen 1 outbox item {}
+ 200.1s posts before release []
+ 206.1s removing the serve hold
+ 320.4s report {"at": 1793505291.170816, "event": "loaded", "head": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+ 320.4s report {"at": 1793505306.599624, "event": "offline: broker calls withheld", "path": "/v1/tasks/heartbeat"}
+ 320.4s report {"at": 1793505485.2936819, "event": "online", "withheld_calls": 24}
+ 320.4s report {"at": 1793505489.766548, "event": "beats withheld", "task": "task-030b475adf6c9b8e02"}
+ 320.4s report {"at": 1793505489.8255801, "claim": {"execution_generation": 2, "kind": "serve", "lease_id": "lease-0295c668d585", "task": "task-030b475adf6c9b8e02"}, "event": "matched", "fault": "reclaim", "first_lease": "lease-0ec7a80c7b22", "path": "/v1/tasks?wait=15&capabilities=voice-outcome-v1"}
+ 320.4s report {"at": 1793505489.975512, "body": "{\"error\": \"stale execution generation\", \"error_code\": \"stale_execution_generation\", \"current_execution_generation\": 2}", "event": "result posted after the newer serve", "execution_generation": 1, "status": 409}
+ 320.4s report {"at": 1793505504.857826, "event": "released", "lease_id": "lease-0295c668d585"}
+ 320.4s report {"at": 1793505529.865483, "event": "delivered the held serve", "lease_id": "lease-0295c668d585"}
+ 320.6s final [' responded       | t        |                    |                 |                    2 |       2 |             7'] ledger null
[remote-gateway-bridge] result task-ag2space_dlocal~task-030b475adf6c9b8e02 accepted by the broker, room delivery unconfirmed (settled:stale_execution_generation), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignA
[remote-gateway-bridge] archived result for task-ag2space_dlocal~task-030b475adf6c9b8e02
[remote-gateway-bridge] outbound worker: task-ag2space_dlocal~task-030b475adf6c9b8e02 seen->retired 126060ms (monotonic)
[remote-gateway-bridge] served task-030b475adf6c9b8e02 lease=lease-0295c668d585
[remote-gateway-bridge] task task-ag2space_dlocal~task-030b475adf6c9b8e02: served again under a new lease while its run may be executing — nothing is published until the ack names the generation
[remote-gateway-bridge] task task-030b475adf6c9b8e02: generation 2 gets its own run task-ag2space_dlocal~task-030b475adf6c9b8e02~~2; the earlier run task-ag2space_dlocal~task-030b475adf6c9b8e02 is stopped and its result is never p
[remote-gateway-bridge] held in flight while their records owe work: task-ag2space_dlocal~task-030b475adf6c9b8e02~~2
[remote-gateway-bridge] queued task-ag2space_dlocal~task-030b475adf6c9b8e02~~2
[remote-gateway-bridge] result task-ag2space_dlocal~task-030b475adf6c9b8e02~~2 delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
[remote-gateway-bridge] archived result for task-ag2space_dlocal~task-030b475adf6c9b8e02~~2
+ 320.6s posts ['delta145450']
+ 320.6s run-2 item {}
+ 320.6s queued ~~2 count 1  dedup lines 0
+ 328.8s restarted normally, no launcher: launcher procs 0  bridge cmd src/remote-gateway-bridge.py
+ 344.0s after plain restart [' responded       | t        |                    |                 |                    2 |       2 |             7'] ledger None
+ 344.1s posts ['delta145450']
+ 344.1s bridge procs 1
raw-5885-r22.txt
rig 200
=== U: upgrade with a non-empty ledger from the earlier format
old ledger before start: records=1 content={"task-upgrade-probe": {"attempt": 1, "execution_generation": 1, "lease_id": "lease-upgrade-probe", "awaiting_ack": false, "stop_requested": null}}
old ledger sha before start: a2f811be440b69fd
old ledger sha after start:  a2f811be440b69fd
2026-11-01T04:00:54Z [remote-gateway-bridge] relay-task-attempts.ag2space_dlocal.json is from an earlier bridge and is ignored; its attempts are not resumed
2026-11-01T04:00:54Z [remote-gateway-bridge] relay-task-attempts-v2.ag2space_dlocal.json is from an earlier bridge and is ignored; its attempts are not resumed
probe in new ledger: 0
probe mentioned in bridge log: 0
$ date -u; git rev-parse HEAD; git status --porcelain
Sun Nov  1 04:01:54 UTC 2026
a619c219d873b51de00eb3f9131fea7fc5d229c6
?? workspace
bridge started: Sun Nov  1 15:00:53 2026    
REMOTE_TASK_LIFECYCLE_V2=1
REMOTE_TASK_TURN_CONTEXT=1
Sutando: idle                                                               [-]
=== A: ask, wake, one reply
+  0.1s sent $H0JIx9niPBpJ649zlgeqznSLEXNYjR3dDHjPfxAIUVY
+ 21.8s task task-8bce2e1e8221c395ae question q_qQRYk3-3WGu5P3dnVA4HRK_X
+ 25.9s ledger [' waiting_input   | ']
+ 26.1s answer 200 {"candidate_id": "mcp:@admin:ag2space.local:op-a84b68d2f8", "question_id": "q_qQRYk3-3WGu5P3dnVA4HRK_X", "outcome": "accepted", "reason": null, "question_revision": 2, "question_status": "answered"}
+ 50.6s ledger final [' responded       | t        |                 |                    |                |             7']
+ 50.7s question [' answered | ']
2026-11-01T04:01:54Z [remote-gateway-bridge] served task-8bce2e1e8221c395ae lease=lease-eddbeb4203a4
2026-11-01T04:01:54Z [remote-gateway-bridge] queued task-ag2space_dlocal~task-8bce2e1e8221c395ae
2026-11-01T04:02:15Z [remote-gateway-bridge] result task-ag2space_dlocal~task-8bce2e1e8221c395ae: open HITL blocker -> {'status': 'waiting_input', 'no_send': True}
2026-11-01T04:02:15Z [remote-gateway-bridge] result task-ag2space_dlocal~task-8bce2e1e8221c395ae accepted by the broker, room delivery unconfirmed (accepted), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaim
2026-11-01T04:02:15Z [remote-gateway-bridge] archived result for task-ag2space_dlocal~task-8bce2e1e8221c395ae
2026-11-01T04:02:21Z [remote-gateway-bridge] served task-8bce2e1e8221c395ae lease=lease-3232fa4fd952
2026-11-01T04:02:21Z [remote-gateway-bridge] wake for task-ag2space_dlocal~task-8bce2e1e8221c395ae~~2: episode ep_q_qQRYk3-3WGu5P3dnVA4HRK_X_1 queued as a new turn
2026-11-01T04:02:21Z [remote-gateway-bridge] queued task-ag2space_dlocal~task-8bce2e1e8221c395ae~~2
2026-11-01T04:02:28Z [remote-gateway-bridge] result task-ag2space_dlocal~task-8bce2e1e8221c395ae~~2: deliberate result file -> {'status': 'completed'}
2026-11-01T04:02:28Z [remote-gateway-bridge] result task-ag2space_dlocal~task-8bce2e1e8221c395ae~~2 delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
2026-11-01T04:02:28Z [remote-gateway-bridge] archived result for task-ag2space_dlocal~task-8bce2e1e8221c395ae~~2
wake headers: wake_episode: ep_q_qQRYk3-3WGu5P3dnVA4HRK_X_1
wake item: None
+ 50.7s agent posts: ['Question: Do you want Red or Blue?', '* Question resolved: Do you want Red or Blue?', 'You chose Blue.']
TID=task-8bce2e1e8221c395ae EP=ep_q_qQRYk3-3WGu5P3dnVA4HRK_X_1
execution_generation:                     1
=== B: Stop while waiting
+  0.0s sent $NBPtrX94fQ9qSqJ5tDQ_yNFIDHVQ376W1wauJbOHcww
+ 12.9s task task-4f9c72b7206f626ddd question q_Gofp7XPX0IFAqaPqCuAOWQQ8
+ 16.9s ledger [' waiting_input   | ']
+ 17.1s STOP 200 {"task_id": "task-4f9c72b7206f626ddd", "mode": "cancel_execution", "ui_dismissed_at": null, "cancellation_state": "cancel_confirmed", "execution_generation": 1, "terminal": true, "terminal_reason": "cancelled_by_user", "closure_reason": "stopped_while_waiting"
+ 20.1s answer 200 {"candidate_id": "mcp:@admin:ag2space.local:op-e3f939b0fd", "question_id": "q_Gofp7XPX0IFAqaPqCuAOWQQ8", "outcome": "rejected", "reason": "cancelled", "question_revision": 2, "question_status": "cance
+ 35.3s ledger final [' dead            | t        | cancelled_by_user | cancel_confirmed   | stopped_while_waiting |             5']
+ 35.4s question [' cancelled | asker_stopped']
2026-11-01T04:02:45Z [remote-gateway-bridge] served task-4f9c72b7206f626ddd lease=lease-5be2454e29be
2026-11-01T04:02:45Z [remote-gateway-bridge] queued task-ag2space_dlocal~task-4f9c72b7206f626ddd
2026-11-01T04:02:58Z [remote-gateway-bridge] result task-ag2space_dlocal~task-4f9c72b7206f626ddd: open HITL blocker -> {'status': 'waiting_input', 'no_send': True}
2026-11-01T04:02:58Z [remote-gateway-bridge] result task-ag2space_dlocal~task-4f9c72b7206f626ddd accepted by the broker, room delivery unconfirmed (accepted), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaim
2026-11-01T04:02:58Z [remote-gateway-bridge] archived result for task-ag2space_dlocal~task-4f9c72b7206f626ddd
wake headers: 
wake item: None
+ 35.5s agent posts: ['Question: Do you want Cats or Dogs?', '* Question cancelled: Do you want Cats or Dogs?']
=== C: same-episode replay across a gateway stop/restart
[remote-gateway-bridge] wake for task-ag2space_dlocal~task-8bce2e1e8221c395ae: not run here (already run here, or no usable episode) — left unacked; logged once per episode
REPLAY local=task-ag2space_dlocal~task-8bce2e1e8221c395ae episode=ep_q_qQRYk3-3WGu5P3dnVA4HRK_X_1 write_task=None task_file_before=False after=False
REPLAY wake_item=None
queued count for episode: 1  bridges: 1
=== D: Stop during a tasks-dir write outage, gateway restart during it
+  0.1s sent $MdYaIgGN471d7poogXC9IDvPHMKsunmCNMqUKqb--P8
+  1.4s task task-f78d45af96dbe69199
+  1.4s core took the task; task file gone: False
+ 15.5s phase [' working']
+ 15.5s tasks dir read-only
+ 15.6s STOP 200 cancel_requested False
+ 40.6s ledger during fault {'run': 'task-ag2space_dlocal~task-f78d45af96dbe69199', 'claims': None, 'dispatch_lost': None, 'owed_close': None, 'stop_requested': True, 'stop_ctl': 'task-ctl-1793505846164-7a8d64-ag2space_dlocal', 'owed_ctl': {'id': 'task-ctl-1793505846164-7a8d64-ag2space_dlocal', 'reason': 'task_cancelled', 'gen': 1}}
+ 46.0s gateway restarted during fault
+ 66.0s ledger after restart {'run': 'task-ag2space_dlocal~task-f78d45af96dbe69199', 'claims': None, 'dispatch_lost': None, 'owed_close': None, 'stop_requested': True, 'stop_ctl': 'task-ctl-1793505846164-7a8d64-ag2space_dlocal', 'owed_ctl': {'id': 'task-ctl-1793505846164-7a8d64-ag2space_dlocal', 'reason': 'task_cancelled', 'gen': 1}}
+ 66.0s tasks dir restored
+ 86.0s ledger after recovery {'run': 'task-ag2space_dlocal~task-f78d45af96dbe69199', 'claims': None, 'dispatch_lost': None, 'owed_close': None, 'stop_requested': True, 'stop_ctl': 'task-ctl-1793505846164-7a8d64-ag2space_dlocal', 'owed_ctl': None}
+ 88.6s ctl files ['tasks/task-ctl-1793505846164-7a8d64-ag2space_dlocal.txt', 'state/activity/.task-ctl-1793505846164-7a8d64-ag2space_dlocal.lock', 'state/activity/task-ctl-1793505846164-7a8d64-ag2space_dlocal.json', 'state/agent-activity.acks/task-ctl-1793505846164-7a8d64-ag2space_dlocal']
+231.0s ctl files final ['tasks/task-ctl-1793505846164-7a8d64-ag2space_dlocal.txt', 'state/activity/.task-ctl-1793505846164-7a8d64-ag2space_dlocal.lock', 'state/activity/task-ctl-1793505846164-7a8d64-ag2space_dlocal.json', 'state/agent-activity.acks/task-ctl-1793505846164-7a8d64-ag2space_dlocal', 'results/task-ctl-1793505846164-7a8d64-ag2space_dlocal.txt']
+231.2s final [' dead            | cancel_confirmed   | cancelled_by_user']
+231.2s ledger final {}
eway-bridge] task task-ag2space_dlocal~task-f78d45af96dbe69199: cancel instruction not written — retried next beat
2026-11-01T04:04:21Z [remote-gateway-bridge] durable stage failed for task-ctl-1793505846164-7a8d64-ag2space_dlocal.txt (PermissionError EACCES at result_publish.py:46)
2026-11-01T04:04:21Z [remote-gateway-bridge] task task-ag2space_dlocal~task-f78d45af96dbe69199: cancel instruction not written — retried next beat
2026-11-01T04:04:36Z [remote-gateway-bridge] durable stage failed for task-ctl-1793505846164-7a8d64-ag2space_dlocal.txt (PermissionError EACCES at result_publish.py:46)
2026-11-01T04:04:36Z [remote-gateway-bridge] task task-ag2space_dlocal~task-f78d45af96dbe69199: cancel instruction not written — retried next beat
2026-11-01T04:04:36Z [remote-gateway-bridge] durable stage failed for task-ctl-1793505846164-7a8d64-ag2space_dlocal.txt (PermissionError EACCES at result_publish.py:46)
2026-11-01T04:04:36Z [remote-gateway-bridge] task task-ag2space_dlocal~task-f78d45af96dbe69199: cancel instruction not written — retried next beat
2026-11-01T04:04:51Z [remote-gateway-bridge] durable stage failed for task-ctl-1793505846164-7a8d64-ag2space_dlocal.txt (PermissionError EACCES at result_publish.py:46)
2026-11-01T04:04:51Z [remote-gateway-bridge] task task-ag2space_dlocal~task-f78d45af96dbe69199: cancel instruction not written — retried next beat
2026-11-01T04:06:04Z [remote-gateway-bridge] task task-f78d45af96dbe69199: the core answered the Stop with nothing to deliver — stopped
2026-11-01T04:06:04Z [remote-gateway-bridge] result task-ag2space_dlocal~task-f78d45af96dbe69199: settled without a post (the Stop is confirmed)
2026-11-01T04:06:04Z [remote-gateway-bridge] archived task-ag2space_dlocal~task-f78d45af96dbe69199 (marker no-send, lease closed, not sent)

$APP/workspace/tasks/task-ctl-1793505846164-7a8d64-ag2space_dlocal.txt
$APP/workspace/tasks/archive/task-ag2space_dlocal~task-f78d45af96dbe69199.txt
task-ctl-1793505846164-7a8d64-ag2space_dlocal.txt

tasks dir mode after run: drwxr-xr-x@
raw-5885-r22-stall.txt
=== E: Stop during a stalled effect, unrelated tasks keep moving
+  21.4s gateway stopped; bridges 1
+  21.8s task A task-bfbe47a7366d994909 [' queued          | f        |                    |                 |                    1 |       1 |             1']
+  21.8s launcher started in the gateway window --fault stall --kind result --task task-bfbe47a7366d994909 --lease * --sentinel $S/at/hold-150801 --report $S/at/rep-stall-150801.jsonl
+  31.8s A held at its result POST
+  31.8s report {"at": 1793506103.573877, "event": "loaded", "head": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+  31.8s report {"at": 1793506108.7371242, "claim": {"epoch": 79, "execution_generation": 1, "kind": "result", "lease_id": "lease-85463db9ec4a", "op": "result-4ab8f29e8cfbea60", "run": "task-ag2space_dlocal~task-bfbe47a7366d994909", "task": "task-bfbe47a7366d994909", "wake_episode": null}, "event": "matched", "fault": "stall", "path": "/v1/results"}
+  31.8s A ledger {"run": "task-ag2space_dlocal~task-bfbe47a7366d994909", "room_id": "!0NKpmrHnsMCcW1d6gAKa3wEVPNIz-knxdErdCmNVN0w", "thread_root_id": null, "source_event_id": "$opIjx1TmNDZ2M8lbO8fgRvsQwNgGFYGZWsc7N_3Msw4", "source_kind": null, "awaiting_ack": false, "lease_id": "lease-85463db9ec4a", "published_at": 1793506103.671508, "claims": {"result-4ab8f29e8cfbea60": {"kind": "result", "run": "task-ag2space_dlocal~task-bfbe47a7366d994909", "execution_generation": 1, "wake_episode": null, "file": [16777232, 2
+  31.9s STOP task-bfbe47a7366d994909 http 200 in 0.02s cancel_requested terminal False
+  34.0s A after Stop [' working         | f        | cancel_requested   |                 |                    1 |       1 |             4'] held still: True
+  34.1s task B task-492a5ca270c867b87d
+  40.2s B posted: ['beta150801'] [' responded       | t        |                    |                 |                    1 |       1 |             4']
+  40.3s task C task-30a5183e064cc3e52f
+  49.5s C [' working         | f        |                    |                 |                    1 |       1 |             3']
+  49.6s STOP task-30a5183e064cc3e52f http 200 in 0.02s cancel_requested terminal False
+  69.7s C after Stop [' working         | f        | cancel_requested   |                 |                    1 |       1 |             4']
+  69.7s still held while B and C moved: True
+  69.7s sentinel removed
+  94.7s report {"at": 1793506103.573877, "event": "loaded", "head": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+  94.7s report {"at": 1793506108.7371242, "claim": {"epoch": 79, "execution_generation": 1, "kind": "result", "lease_id": "lease-85463db9ec4a", "op": "result-4ab8f29e8cfbea60", "run": "task-ag2space_dlocal~task-bfbe47a7366d994909", "task": "task-bfbe47a7366d994909", "wake_episode": null}, "event": "matched", "fault": "stall", "path": "/v1/results"}
+  94.7s report {"answer": {"delivery_readable": true, "ok": true}, "at": 1793506151.323725, "event": "released"}
+  95.1s A final [' responded       | t        |                    |                 |                    1 |       1 |             5'] ledger null
+  95.1s alpha posts ['alpha150801'] beta posts ['beta150801']
[remote-gateway-bridge] served task-bfbe47a7366d994909 lease=lease-85463db9ec4a
[remote-gateway-bridge] queued task-ag2space_dlocal~task-bfbe47a7366d994909
[remote-gateway-bridge] result task-ag2space_dlocal~task-bfbe47a7366d994909: deliberate result file -> {'status': 'completed'}
[remote-gateway-bridge] task task-bfbe47a7366d994909: Stop requested — the core is asked to stop; a result it still writes is delivered
[remote-gateway-bridge] result task-ag2space_dlocal~task-bfbe47a7366d994909 delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
[remote-gateway-bridge] archived result for task-ag2space_dlocal~task-bfbe47a7366d994909
[remote-gateway-bridge] outbound worker: task-ag2space_dlocal~task-bfbe47a7366d994909 seen->retired 38000ms (monotonic)
+ 102.7s restarted normally, no launcher: launcher procs 0  bridge cmd src/remote-gateway-bridge.py
+ 122.8s after plain restart: A [' responded       | t        |                    |                 |                    1 |       1 |             5'] ledger None
+ 122.8s alpha posts ['alpha150801']
+ 122.8s bridges 3
raw-5885-r22-crash-ack.txt
=== F: kill after the broker accepted the ack, before the local commit
+  21.4s gateway stopped; bridges 1
+  21.7s task task-f5c69d259d0887071e [' queued          | f        |                    |                 |                    1 |       1 |             1']
+  21.7s launcher started in the gateway window --fault crash --kind ack --task task-f5c69d259d0887071e --lease * --report $S/at/rep-crash-ack-151024.jsonl
+  32.7s report {"at": 1793506246.7495239, "event": "loaded", "head": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_spar
+  32.7s report {"at": 1793506246.99106, "claim": {"epoch": 81, "execution_generation": null, "kind": "ack", "lease_id": "lease-2629029e5ba2", "op": "ack-ca9864ae56bcd5f2", "run": "task-ag2space_dlocal~task-f5c69d259d0887071e", "task": "task-f5c69d259d0887071e", "wake_episode": null}, "event": "matched", "fault": "crash", "path": "/v1/tasks/task-f5c69d259d0887071e/ack"}
+  32.7s report {"answer": {"attempt": 1, "durable": true, "execution_generation": 1, "lease_expires_at": 1793506306.851513, "ok": true, "task_id": "task-f5c69d259d0887071e"}, "at": 1793506247.012022, "event": "accepted by the broker; killing the bridge before its commit", "op": "ack-ca9864ae56bcd5f2"}
+  33.1s launcher procs after exit 9 0
+  33.2s window ['claude']
+  33.3s broker row at the crash [' working         | f        |                    |                 |                    1 |       1 |             3']
+  33.3s ledger at the crash {"run": "task-ag2space_dlocal~task-f5c69d259d0887071e", "room_id": "!0NKpmrHnsMCcW1d6gAKa3wEVPNIz-knxdErdCmNVN0w", "thread_root_id": null, "source_event_id": "$mKdvdKFv3_p65PPu7xFnbmxR82bCIJ1-gpiJ452hcCE", "source_kind": null, "awaiting_ack": true, "lease_id": "lease-2629029e5ba2", "published_at": 1793506246.874571, "claims": {"ack-ca9864ae56bcd5f2": {"kind": "ack", "run": "task-ag2space_dlocal~task-f5c69d259d0887071e", "execution_generation": null, "wake_episode": null, "serve": "lease-2629029e5ba2", "epoch": 81, "at": 1793506246.990151, "turn_id": null}}}
+  40.5s restarted normally, no launcher: launcher procs 0  bridge cmd src/remote-gateway-bridge.py
+  40.5s owner epoch before/after {'epoch': 80, 'pid': 93909, 'at': 1793506184.301316} {'epoch': 81, 'pid': 98840, 'at': 1793506246.763082}
+  67.6s posted: True
+  67.7s final [' responded       | t        |                    |                 |                    1 |       1 |             4'] ledger None
+  67.7s posts ['gamma151024']
[remote-gateway-bridge] served task-f5c69d259d0887071e lease=lease-2629029e5ba2
[remote-gateway-bridge] queued task-ag2space_dlocal~task-f5c69d259d0887071e
2026-10-31T20:17:56Z [remote-gateway-bridge] task task-8fc72a9cdaa0e3cbd4: effects claimed before this start, outcome unknown: ack ack-7e9e07ccbd8ec831 — reconciled under the same operation ids
2026-10-31T20:18:53Z [remote-gateway-bridge] task task-333dc602a30d7a1110: effects claimed before this start, outcome unknown: result result-fe5c3038041d7f96 — reconciled under the same operation ids
2026-10-31T21:34:04Z [remote-gateway-bridge] task task-d95ca1aee94fb49cbe: effects claimed before this start, outcome unknown: ack ack-fd0ad3ce50336d8a — reconciled under the same operation ids
2026-10-31T21:35:11Z [remote-gateway-bridge] task task-77b1fe37600fcdc120: effects claimed before this start, outcome unknown: result result-2d6ce694137a3f81 — reconciled under the same operation ids
2026-10-31T22:33:58Z [remote-gateway-bridge] task task-7577a7e3472bff5212: effects claimed before this start, outcome unknown: ack ack-8f13e896a5f45355 — reconciled under the same operation ids
2026-10-31T22:35:05Z [remote-gateway-bridge] task task-65ba3bfffa7ebf6476: effects claimed before this start, outcome unknown: result result-05f32880a83ab006 — reconciled under the same operation ids
2026-10-31T23:23:58Z [remote-gateway-bridge] task task-864997f86eecd3ea9a: effects claimed before this start, outcome unknown: ack ack-4ef92fb826d9a0a3 — reconciled under the same operation ids
2026-10-31T23:25:04Z [remote-gateway-bridge] task task-c7e9831e872b8e1f99: effects claimed before this start, outcome unknown: result result-19c02908b61394ca — reconciled under the same operation ids
2026-11-01T00:49:01Z [remote-gateway-bridge] task task-a5cccd2c4efaec5a57: effects claimed before this start, outcome unknown: ack ack-16400be29b2597d1 — reconciled under the same operation ids
2026-11-01T00:50:09Z [remote-gateway-bridge] task task-91870f9816b14847f9: effects claimed before this start, outcome unknown: result result-06d2f2f19a7c9147 — reconciled under the same operation ids
2026-11-01T02:14:36Z [remote-gateway-bridge] task task-aacc45ebcdc6824221: effects claimed before this start, outcome unknown: ack ack-f80fbc178e35437a — reconciled under the same operation ids
2026-11-01T02:15:43Z [remote-gateway-bridge] task task-4042fca78ee474018a: effects claimed before this start, outcome unknown: result result-288e47d2ce28dbd6 — reconciled under the same operation ids
2026-11-01T04:11:05Z [remote-gateway-bridge] task task-f5c69d259d0887071e: effects claimed before this start, outcome unknown: ack ack-ca9864ae56bcd5f2 — reconciled under the same operation ids
2026-11-01T04:11:05Z [remote-gateway-bridge] result task-ag2space_dlocal~task-f5c69d259d0887071e: held — its ack has not named the lease yet
2026-11-01T04:11:05Z [remote-gateway-bridge] task task-f5c69d259d0887071e: result claim result-b944e4ddcf9e2ced is kept — its reply is still at its name, unsent; a newer generation waits until it is delivered or, if it cannot be r
2026-11-01T04:11:06Z [remote-gateway-bridge] result task-ag2space_dlocal~task-f5c69d259d0887071e: deliberate result file -> {'status': 'completed'}
2026-11-01T04:11:06Z [remote-gateway-bridge] result task-ag2space_dlocal~task-f5c69d259d0887071e delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
2026-11-01T04:11:06Z [remote-gateway-bridge] archived result for task-ag2space_dlocal~task-f5c69d259d0887071e
2026-11-01T04:11:06Z [remote-gateway-bridge] outbound worker: task-ag2space_dlocal~task-f5c69d259d0887071e seen->retired 1362ms (monotonic)
+  67.7s bridges 3
raw-5885-r22-crash-result.txt
=== F: kill after the broker accepted the result, before the local commit
+  21.2s gateway stopped; bridges 1
+  21.6s task task-b6dc1e97a656d3289f [' queued          | f        |                    |                 |                    1 |       1 |             1']
+  21.6s launcher started in the gateway window --fault crash --kind result --task task-b6dc1e97a656d3289f --lease * --report $S/at/rep-crash-result-151152.jsonl
+  32.6s report {"at": 1793506334.704354, "event": "loaded", "head": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+  32.6s report {"at": 1793506340.633976, "claim": {"epoch": 83, "execution_generation": 1, "kind": "result", "lease_id": "lease-74b0c206e270", "op": "result-cd953b06385e6621", "run": "task-ag2space_dlocal~task-b6dc1e97a656d3289f", "task": "task-b6dc1e97a656d3289f", "wake_episode": null}, "event": "matched", "fault": "crash", "path": "/v1/results"}
+  32.6s report {"answer": {"delivery_readable": true, "ok": true}, "at": 1793506340.6583989, "event": "accepted by the broker; killing the bridge before its commit", "op": "result-cd953b06385e6621"}
+  32.6s launcher procs after exit 9 0
+  32.6s window ['claude']
+  32.8s broker row at the crash [' responded       | t        |                    |                 |                    1 |       1 |             4']
+  32.8s ledger at the crash {"run": "task-ag2space_dlocal~task-b6dc1e97a656d3289f", "room_id": "!0NKpmrHnsMCcW1d6gAKa3wEVPNIz-knxdErdCmNVN0w", "thread_root_id": null, "source_event_id": "$_rjn8-I083MJAmJogsb40rZVKhlfezC1tP4ED2j-4Ao", "source_kind": null, "awaiting_ack": false, "lease_id": "lease-74b0c206e270", "published_at": 1793506334.809289, "claims": {"result-cd953b06385e6621": {"kind": "result", "run": "task-ag2space_dlocal~task-b6dc1e97a656d3289f", "execution_generation": 1, "wake_episode": null, "file": [16777232, 293116101, 1793506340414939528, "890d3d893ca09540316f0dc5fe99619e1fbc73a9fcf340fa9fc6622474b6e7d8"], "data": {"digest": "890d3d893ca09540316f0dc5fe99619e1fbc73a9fcf340fa9fc6622474b6e7d8", "tid": "task-
+  39.9s restarted normally, no launcher: launcher procs 0  bridge cmd src/remote-gateway-bridge.py
+  39.9s owner epoch before/after {'epoch': 82, 'pid': 1810, 'at': 1793506265.4227781} {'epoch': 83, 'pid': 6954, 'at': 1793506334.7187772}
+  64.9s posted: True
+  65.0s final [' responded       | t        |                    |                 |                    1 |       1 |             4'] ledger None
+  65.0s posts ['gamma151152']
[remote-gateway-bridge] served task-b6dc1e97a656d3289f lease=lease-74b0c206e270
[remote-gateway-bridge] queued task-ag2space_dlocal~task-b6dc1e97a656d3289f
[remote-gateway-bridge] result task-ag2space_dlocal~task-b6dc1e97a656d3289f: deliberate result file -> {'status': 'completed'}
2026-10-31T20:17:56Z [remote-gateway-bridge] task task-8fc72a9cdaa0e3cbd4: effects claimed before this start, outcome unknown: ack ack-7e9e07ccbd8ec831 — reconciled under the same operation ids
2026-10-31T20:18:53Z [remote-gateway-bridge] task task-333dc602a30d7a1110: effects claimed before this start, outcome unknown: result result-fe5c3038041d7f96 — reconciled under the same operation ids
2026-10-31T21:34:04Z [remote-gateway-bridge] task task-d95ca1aee94fb49cbe: effects claimed before this start, outcome unknown: ack ack-fd0ad3ce50336d8a — reconciled under the same operation ids
2026-10-31T21:35:11Z [remote-gateway-bridge] task task-77b1fe37600fcdc120: effects claimed before this start, outcome unknown: result result-2d6ce694137a3f81 — reconciled under the same operation ids
2026-10-31T22:33:58Z [remote-gateway-bridge] task task-7577a7e3472bff5212: effects claimed before this start, outcome unknown: ack ack-8f13e896a5f45355 — reconciled under the same operation ids
2026-10-31T22:35:05Z [remote-gateway-bridge] task task-65ba3bfffa7ebf6476: effects claimed before this start, outcome unknown: result result-05f32880a83ab006 — reconciled under the same operation ids
2026-10-31T23:23:58Z [remote-gateway-bridge] task task-864997f86eecd3ea9a: effects claimed before this start, outcome unknown: ack ack-4ef92fb826d9a0a3 — reconciled under the same operation ids
2026-10-31T23:25:04Z [remote-gateway-bridge] task task-c7e9831e872b8e1f99: effects claimed before this start, outcome unknown: result result-19c02908b61394ca — reconciled under the same operation ids
2026-11-01T00:49:01Z [remote-gateway-bridge] task task-a5cccd2c4efaec5a57: effects claimed before this start, outcome unknown: ack ack-16400be29b2597d1 — reconciled under the same operation ids
2026-11-01T00:50:09Z [remote-gateway-bridge] task task-91870f9816b14847f9: effects claimed before this start, outcome unknown: result result-06d2f2f19a7c9147 — reconciled under the same operation ids
2026-11-01T02:14:36Z [remote-gateway-bridge] task task-aacc45ebcdc6824221: effects claimed before this start, outcome unknown: ack ack-f80fbc178e35437a — reconciled under the same operation ids
2026-11-01T02:15:43Z [remote-gateway-bridge] task task-4042fca78ee474018a: effects claimed before this start, outcome unknown: result result-288e47d2ce28dbd6 — reconciled under the same operation ids
2026-11-01T04:11:05Z [remote-gateway-bridge] task task-f5c69d259d0887071e: effects claimed before this start, outcome unknown: ack ack-ca9864ae56bcd5f2 — reconciled under the same operation ids
2026-11-01T04:12:32Z [remote-gateway-bridge] task task-b6dc1e97a656d3289f: effects claimed before this start, outcome unknown: result result-cd953b06385e6621 — reconciled under the same operation ids
2026-11-01T04:12:32Z [remote-gateway-bridge] result task-ag2space_dlocal~task-b6dc1e97a656d3289f: deliberate result file -> {'status': 'completed'}
2026-11-01T04:12:32Z [remote-gateway-bridge] result task-ag2space_dlocal~task-b6dc1e97a656d3289f: pending retry or delivery claim (attempts=0, next=1793506340.633122, deadline=1793506940.633122)
2026-11-01T04:12:32Z [remote-gateway-bridge] task task-b6dc1e97a656d3289f: attempt fenced by the broker (task_already_terminal)
2026-11-01T04:12:33Z [remote-gateway-bridge] result task-ag2space_dlocal~task-b6dc1e97a656d3289f: settled without a post (attempt fenced (task_already_terminal))
2026-11-01T04:12:33Z [remote-gateway-bridge] archived result for task-ag2space_dlocal~task-b6dc1e97a656d3289f
2026-11-01T04:12:33Z [remote-gateway-bridge] outbound worker: task-ag2space_dlocal~task-b6dc1e97a656d3289f seen->retired 1033ms (monotonic)
+  65.0s bridges 3
launcher report: rep-reclaim
{"at": 1793500194.933236, "event": "loaded", "head": "5bed4b92880dbe3dbce65110f054519d680dabaa", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py", "module_sha256": "4c86bd37d080a0b71e36a6c661c26a7f475f3699e51b7872040fd97537868abe", "rule": {"fault": "reclaim", "generation": null, "kind": "serve", "lease": null, "report": "$S/at/rep-reclaim-132954.jsonl", "run": null, "sentinel": "$S/at/hold-132954", "task": "task-e41dde03c23202564d"}, "runtime_identity": {"build_sha": "5bed4b92880dbe3dbce65110f054519d680dabaa", "entrypoint": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_sha256": "4c86bd37d080a0b71e36a6c661c26a7f475f3699e51b7872040fd97537868abe"}}
{"at": 1793500210.306728, "event": "beats withheld", "task": "task-e41dde03c23202564d"}
{"at": 1793500270.5235682, "event": "offline: broker calls withheld", "path": "/v1/tasks/heartbeat"}
{"at": 1793500453.69118, "event": "online", "withheld_calls": 17}
{"at": 1793500453.7552042, "claim": {"execution_generation": 2, "kind": "serve", "lease_id": "lease-648773969e4d", "task": "task-e41dde03c23202564d"}, "event": "matched", "fault": "reclaim", "first_lease": "lease-3592e8046a15", "path": "/v1/tasks?wait=15&capabilities=voice-outcome-v1"}
{"at": 1793500453.875903, "body": "{\"error\": \"stale execution generation\", \"error_code\": \"stale_execution_generation\", \"current_execution_generation\": 2}", "event": "result posted after the newer serve", "execution_generation": 1, "status": 409}
{"at": 1793500468.788029, "event": "released", "lease_id": "lease-648773969e4d"}
launcher report: rep-crash-result
{"at": 1793506334.704354, "event": "loaded", "head": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py", "module_sha256": "da99df409ba0e12aecc8bde5cf7c4c263108fe636823fa9b1c0877b288449d60", "rule": {"fault": "crash", "generation": null, "kind": "result", "lease": "*", "report": "$S/at/rep-crash-result-151152.jsonl", "run": null, "sentinel": null, "task": "task-b6dc1e97a656d3289f"}, "runtime_identity": {"build_sha": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "entrypoint": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_sha256": "da99df409ba0e12aecc8bde5cf7c4c263108fe636823fa9b1c0877b288449d60"}}
{"at": 1793506340.633976, "claim": {"epoch": 83, "execution_generation": 1, "kind": "result", "lease_id": "lease-74b0c206e270", "op": "result-cd953b06385e6621", "run": "task-ag2space_dlocal~task-b6dc1e97a656d3289f", "task": "task-b6dc1e97a656d3289f", "wake_episode": null}, "event": "matched", "fault": "crash", "path": "/v1/results"}
{"answer": {"delivery_readable": true, "ok": true}, "at": 1793506340.6583989, "event": "accepted by the broker; killing the bridge before its commit", "op": "result-cd953b06385e6621"}
launcher report: rep-crash-ack
{"at": 1793506246.7495239, "event": "loaded", "head": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py", "module_sha256": "da99df409ba0e12aecc8bde5cf7c4c263108fe636823fa9b1c0877b288449d60", "rule": {"fault": "crash", "generation": null, "kind": "ack", "lease": "*", "report": "$S/at/rep-crash-ack-151024.jsonl", "run": null, "sentinel": null, "task": "task-f5c69d259d0887071e"}, "runtime_identity": {"build_sha": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "entrypoint": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_sha256": "da99df409ba0e12aecc8bde5cf7c4c263108fe636823fa9b1c0877b288449d60"}}
{"at": 1793506246.99106, "claim": {"epoch": 81, "execution_generation": null, "kind": "ack", "lease_id": "lease-2629029e5ba2", "op": "ack-ca9864ae56bcd5f2", "run": "task-ag2space_dlocal~task-f5c69d259d0887071e", "task": "task-f5c69d259d0887071e", "wake_episode": null}, "event": "matched", "fault": "crash", "path": "/v1/tasks/task-f5c69d259d0887071e/ack"}
{"answer": {"attempt": 1, "durable": true, "execution_generation": 1, "lease_expires_at": 1793506306.851513, "ok": true, "task_id": "task-f5c69d259d0887071e"}, "at": 1793506247.012022, "event": "accepted by the broker; killing the bridge before its commit", "op": "ack-ca9864ae56bcd5f2"}
launcher report: rep-stall
{"at": 1793506103.573877, "event": "loaded", "head": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py", "module_sha256": "da99df409ba0e12aecc8bde5cf7c4c263108fe636823fa9b1c0877b288449d60", "rule": {"fault": "stall", "generation": null, "kind": "result", "lease": "*", "report": "$S/at/rep-stall-150801.jsonl", "run": null, "sentinel": "$S/at/hold-150801", "task": "task-bfbe47a7366d994909"}, "runtime_identity": {"build_sha": "a619c219d873b51de00eb3f9131fea7fc5d229c6", "entrypoint": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_sha256": "da99df409ba0e12aecc8bde5cf7c4c263108fe636823fa9b1c0877b288449d60"}}
{"at": 1793506108.7371242, "claim": {"epoch": 79, "execution_generation": 1, "kind": "result", "lease_id": "lease-85463db9ec4a", "op": "result-4ab8f29e8cfbea60", "run": "task-ag2space_dlocal~task-bfbe47a7366d994909", "task": "task-bfbe47a7366d994909", "wake_episode": null}, "event": "matched", "fault": "stall", "path": "/v1/results"}
{"answer": {"delivery_readable": true, "ok": true}, "at": 1793506151.323725, "event": "released"}

@marklysze
marklysze requested a review from keweichen November 1, 2026 04:14
@marklysze

Copy link
Copy Markdown
Collaborator Author

@keweichen, a request from Mark. This PR is blocking Agent Activity, and Mark is preparing the s.space launch on 2 November PST. Your a619c21 finding from Sutando Triage is with the author, and the fix is under way. Mark asks whether you can help fix this with us rather than review round by round:

  1. Where you've reproduced a defect, please push or post the fix with your probe as a regression. I can arrange branch access if you need it.
  2. Please do one exhaustive pass and give us everything you'd block on in one list, so we can fix them in one push.
  3. Please mark each finding as must-fix before merge, or tracked follow-up behind the default-off flags.

What's the quickest way to work together on this today? Your reviews have found real bugs, and the design is much better for them; this is only about getting to the end in time.

(Mark's coordinator agent, on Mark's behalf.)

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at 5c64cae. Two commits tighten the rerun line from a619c21:

  • _publish_owed_run now reads the owed run and the replaced run's delivery state from one ledger snapshot (owed_view), and publishes nothing, retrying next beat, when either can't be read. _write_task takes the computed rerun instead of re-reading. So a newer generation's file never states "delivered" or "not delivered" from a torn view.
  • A transient OSError reading the held serve is now retried, not failed. Only a missing or garbled serve fails the generation.

Ran here in a clean env: hooks 177 OK, sparrow-task-attempts 53 OK. With the bridge and task_attempts.py back at a619c21, the new "published only once what became of the earlier answer can be read" cases error under both the ledger fault and the reserve fault.

(agent-assisted, Blue for Chi's seat)

@marklysze

Copy link
Copy Markdown
Collaborator Author

Round 22, Kewei's consolidated list at a619c21 (head: 5c64cae64abf8429f25732eecb0c330a0a8e1946, one combined push: 54eeeda00 + 5c64cae64, with origin/main merged)

keweichen, this is the one stable SHA for your re-review.

The code blocker: an unreadable delivery verdict published a missing or false rerun line

  1. One ledger snapshot. TaskAttempts.owed_view(wire) reads the ledger once and returns the record together with the replaced run's entry (lease, generation, delivered). _publish_owed_run decides the owed run and its line from that snapshot only. _write_task(task, run=…, rerun=…) takes the verdict as an argument and reads nothing more. The earlier run_entry second read is gone.

  2. UNKNOWN is preserved. owed_view returns UNKNOWN when the ledger, the record or the runs map can't be read. Nothing converts it to None, {} or not_delivered.

  3. Nothing is published while the evidence is unreadable. _publish_owed_run returns before _write_task, logs (with backoff) that it's waiting, and keeps owed_publish and the reserve, so the next healthy beat retries. A reserve that exists but can't be read (OSError other than FileNotFoundError) is now waited on too, never taken as lost. Only a missing or corrupt reserve still fails the generation visibly.

  4. Regressions, in test_a_newer_run_is_published_only_once_what_became_of_the_earlier_answer_can_be_read. Both cases go through the production path: gen 1 settled, L2 served and acked as gen 2, then the beat that publishes. One case has the earlier result refused (409 stale), the other delivered. Each runs with:

    • one failed ledger read inside the production owed_view (its _read returns None once; all later IO healthy);
    • one reserve read failing with EACCES.

    On the failed pass: no ~~2 task, owed_publish kept, the reserve kept, and no failure written. After recovery: exactly one ~~2 task carrying exactly the right line (not_delivered or answered), the debt cleared and the reserve dropped. At a619c21 the ledger case raises and the reserve case fails the generation.

  5. unknown wording is now neutral: "Whether the earlier attempt's answer or the messages it sent reached the room cannot be established. Check the room first, repeat nothing it already shows, and answer the rest."

Audit of remaining second reads in the publish path:

  • _publish_owed_run now reads the ledger once (owed_view), then the reserve.
  • _write_task(run=…) makes no ledger read before writing.
  • After the file is written, _mark_published and the debt-clearing transition each re-read the ledger. Both are guarded: an unreadable record leaves the mark for the next serve, and leaves the debt, whose next pass finds the run already queued (_task_pending) and does not write it again.

PR body

  • The stale "Still owed" list is reconciled into a live status.
  • The non-blocking items, all behind the default-off flags, are listed under "Known gaps and follow-ups":
    • an ack_first wake doesn't resume;
    • upgrade drops old-format in-flight records;
    • operator visibility for persistent read failures;
    • unlocked inflight reads;
    • the flock structural-test gap;
    • the unreadable own-run item;
    • a stronger delivered-then-reissued no-reply pin.

Gates at this head

  • Focused, all OK: hooks 177, sparrow-task-attempts 53, sparrow-task-transition 23, lifecycle 7 (incl. the named-instance real-bridge end-to-end), ack-first 65, disposal delegation 85, launcher 9, hermetic 28.
  • Every file scripts/discover-python-tests.sh lists: 1,320 files, 21 fail. 19 fail identically on main (environment on this Mac); 2 (coverage-gate-timeout-dump, discord-bridge-split-config-root) fail only under parallel load and pass alone 3 of 3.
  • CI: green, 47 passed, 3 skipped, 0 failed. One shard first failed in tests/skills/worker-pool/create-worker-roster-preflight.test.py ("timed out waiting for disposable descendants to be reaped"), which this PR doesn't touch, and passed on rerun.
  • CLA: signed.
  • Hardcoded-path scan: review-checks PASS on the PR diff. The firing control works: a diff adding "/Users/alice/sutando/workspace" is rejected with 1 violation.
  • sync_from_src --check: in sync. ruff --select F: clean.
  • The launcher is unchanged since 8e87d03.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Raw witness at exact head 5c64cae64abf8429f25732eecb0c330a0a8e1946 (run by Mark's coordinator agent on Mark's Mac, local k3d rig, real broker; unedited apart from replacing local paths with $APP and $S, and capping log lines at 1,500 characters; the launcher reports below are complete). CI at this head: 47 passed, 3 skipped, 0 failed.

Section Result
U. Upgrade with a non-empty old ledger The old ledger's record count and content are printed before start (1 record). Start logs it as ignored, the file is unchanged (same sha), and the probe is in neither the new ledger nor the bridge log
A. Ask, wake, one reply One wake, one final post, ledger settled
B. Stop while waiting Question cancelled, ended dead/cancel_confirmed
C. Same-episode replay across a stop and restart write_task=None, no task file, queued count 1
D. Stop during a tasks-dir outage, restart during it Stop retried through the outage and the restart, one control after recovery, dead/cancel_confirmed, ledger empty
E. Stop during a stalled result POST Stop answered in 0.03s. Task B posted and task C reached working and took a Stop (0.03s) while A stayed held. A posted once on release and ended responded. After a plain restart, C ended dead/cancel_confirmed
F1. Kill after the broker accepted the ack os._exit(9) after a real 2xx, plain restart reconciled under the same op id, one post, ledger empty
F2. Kill after the broker accepted the result Same, the retry was fenced task_already_terminal and settled without a post, one post, ledger empty
G. Stale generation, real broker Offline 180s, real re-lease to generation 2, generation 1's result got canonical 409 stale_execution_generation and settled, the held L2 was delivered and acked, generation 2 gets its own run …~~2, queued …~~2 once, no dedup. The ~~2 task carried the not-delivered line, the core answered in full, item <task>.run-2 ended DELIVERED under generation 2, the room got one reply, and the task ended responded with an empty ledger

The launcher ran verbatim from scripts/witness/lifecycle-fault-launcher.py at this head, via the production loader, with --lease '*'. Each matched line holds the matched claim's full identity including the lease. The rule fired once per run, and the reports hold no FAIL line. Each restart after a fault was plain src/remote-gateway-bridge.py.

Not covered live: the delivered-then-newer-generation case, your unreadable-evidence schedules, and the injected-commit-failure schedules. They rest on the regressions in the author's reply (6553925848).

Not yet done: the real voice ack_first through the deployed voice lane. A real dev.s.space lane for mark-dev-sutando is now connected on 0.8.3. It is being cleared of an old task backlog first, and the call will run on this exact head with both flags on and a plain restart. If you need that first, tell us; otherwise please review this SHA now.

G: bridge output for the stale-generation run
served task-5964797fe2d1ae25c3 lease=lease-c9c9725daddd
queued task-ag2space_dlocal~task-5964797fe2d1ae25c3
result POST not confirmed for task-ag2space_dlocal~task-5964797fe2d1ae25c3 (not_delivered: transport failure for task-5964797fe2d1ae25c3: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793511094.5847008, deadline=1793511692.583182
result POST not confirmed for task-ag2space_dlocal~task-5964797fe2d1ae25c3 (not_delivered: transport failure for task-5964797fe2d1ae25c3: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793511098.7755, deadline=1793511692.583182
result POST not confirmed for task-ag2space_dlocal~task-5964797fe2d1ae25c3 (not_delivered: transport failure for task-5964797fe2d1ae25c3: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793511106.8259578, deadline=1793511692.583182
result POST not confirmed for task-ag2space_dlocal~task-5964797fe2d1ae25c3 (not_delivered: transport failure for task-5964797fe2d1ae25c3: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793511122.921632, deadline=1793511692.583182
result POST not confirmed for task-ag2space_dlocal~task-5964797fe2d1ae25c3 (not_delivered: transport failure for task-5964797fe2d1ae25c3: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793511153.122601, deadline=1793511692.583182
result POST not confirmed for task-ag2space_dlocal~task-5964797fe2d1ae25c3 (not_delivered: transport failure for task-5964797fe2d1ae25c3: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793511183.4804559, deadline=1793511692.583182
result POST not confirmed for task-ag2space_dlocal~task-5964797fe2d1ae25c3 (not_delivered: transport failure for task-5964797fe2d1ae25c3: <urlopen error withheld by the witness launcher (offline)>) — status=READY, reason=None, next=1793511213.8440928, deadline=1793511692.583182
result task-ag2space_dlocal~task-5964797fe2d1ae25c3 accepted by the broker, room delivery unconfirmed (settled:stale_execution_generation), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
archived result for task-ag2space_dlocal~task-5964797fe2d1ae25c3
outbound worker: task-ag2space_dlocal~task-5964797fe2d1ae25c3 seen->retired 127180ms (monotonic)
served task-5964797fe2d1ae25c3 lease=lease-19dd5ff200a6
task task-ag2space_dlocal~task-5964797fe2d1ae25c3: served again under a new lease while its run may be executing — nothing is published until the ack names the generation
task task-5964797fe2d1ae25c3: generation 2 gets its own run task-ag2space_dlocal~task-5964797fe2d1ae25c3~~2; the earlier run task-ag2space_dlocal~task-5964797fe2d1ae25c3 is stopped and its result is never posted for it
held in flight while their records owe work: task-ag2space_dlocal~task-5964797fe2d1ae25c3~~2
queued task-ag2space_dlocal~task-5964797fe2d1ae25c3~~2
result task-ag2space_dlocal~task-5964797fe2d1ae25c3~~2 delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
archived result for task-ag2space_dlocal~task-5964797fe2d1ae25c3~~2
G: harness output
=== G: stale generation 1 answer (real 409), then the newer generation runs
+   0.1s gateway stopped; bridges 0
+   0.5s task task-5964797fe2d1ae25c3 [' queued          | f        |                    |                 |                    1 |       1 |             1']
+   0.6s launcher started in the gateway window --fault reclaim --kind serve --task task-5964797fe2d1ae25c3 --sentinel $S/at/hold-163020 --report $S/at/rep-reclaim-163020.jsonl
+  10.6s gen 1 acked and queued [' working         | f        |                    |                 |                    1 |       1 |             3'] {"lease_id": "lease-c9c9725daddd", "execution_generation": 1, "run": "task-ag2space_dlocal~task-5964797fe2d1ae25c3"}
+  10.6s offline: broker calls withheld
+ 190.8s after 180s offline [' queued          | f        |                    |                 |                    2 |       2 |             4']
+ 190.8s online again
+ 199.8s report {"at": 1793511020.980017, "event": "loaded", "head": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+ 199.8s report {"at": 1793511036.325828, "event": "offline: broker calls withheld", "path": "/v1/tasks/heartbeat"}
+ 199.8s report {"at": 1793511214.228234, "event": "online", "withheld_calls": 23}
+ 199.8s report {"at": 1793511219.47579, "event": "beats withheld", "task": "task-5964797fe2d1ae25c3"}
+ 199.8s report {"at": 1793511219.535635, "claim": {"execution_generation": 2, "kind": "serve", "lease_id": "lease-19dd5ff200a6", "task": "task-5964797fe2d1ae25c3"}, "event": "matched", "fault": "reclaim", "first_lease": "lease-c9c9725daddd", "path": "/v1/tasks?wait=15&capabilities=voice-outcome-v1"}
+ 199.8s report {"at": 1793511219.7299788, "body": "{\"error\": \"stale execution generation\", \"error_code\": \"stale_execution_generation\", \"current_execution_generation\": 2}", "event": "result posted after the newer serve", "execution_generation": 1, "status": 409}
+ 200.0s stale step True [' claimed         | f        |                    |                 |                    2 |       2 |             5']
[remote-gateway-bridge] result task-ag2space_dlocal~task-5964797fe2d1ae25c3 accepted by the broker, room delivery unconfirmed (settled:stale_execution_generation), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignA
[remote-gateway-bridge] archived result for task-ag2space_dlocal~task-5964797fe2d1ae25c3
[remote-gateway-bridge] outbound worker: task-ag2space_dlocal~task-5964797fe2d1ae25c3 seen->retired 127180ms (monotonic)
+ 200.0s gen 1 outbox item {}
+ 200.0s posts before release []
+ 206.0s removing the serve hold
+ 320.5s report {"at": 1793511020.980017, "event": "loaded", "head": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+ 320.5s report {"at": 1793511036.325828, "event": "offline: broker calls withheld", "path": "/v1/tasks/heartbeat"}
+ 320.5s report {"at": 1793511214.228234, "event": "online", "withheld_calls": 23}
+ 320.5s report {"at": 1793511219.47579, "event": "beats withheld", "task": "task-5964797fe2d1ae25c3"}
+ 320.5s report {"at": 1793511219.535635, "claim": {"execution_generation": 2, "kind": "serve", "lease_id": "lease-19dd5ff200a6", "task": "task-5964797fe2d1ae25c3"}, "event": "matched", "fault": "reclaim", "first_lease": "lease-c9c9725daddd", "path": "/v1/tasks?wait=15&capabilities=voice-outcome-v1"}
+ 320.5s report {"at": 1793511219.7299788, "body": "{\"error\": \"stale execution generation\", \"error_code\": \"stale_execution_generation\", \"current_execution_generation\": 2}", "event": "result posted after the newer serve", "execution_generation": 1, "status": 409}
+ 320.5s report {"at": 1793511234.57182, "event": "released", "lease_id": "lease-19dd5ff200a6"}
+ 320.5s report {"at": 1793511259.5794659, "event": "delivered the held serve", "lease_id": "lease-19dd5ff200a6"}
+ 320.8s final [' responded       | t        |                    |                 |                    2 |       2 |             7'] ledger null
[remote-gateway-bridge] result task-ag2space_dlocal~task-5964797fe2d1ae25c3 accepted by the broker, room delivery unconfirmed (settled:stale_execution_generation), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignA
[remote-gateway-bridge] archived result for task-ag2space_dlocal~task-5964797fe2d1ae25c3
[remote-gateway-bridge] outbound worker: task-ag2space_dlocal~task-5964797fe2d1ae25c3 seen->retired 127180ms (monotonic)
[remote-gateway-bridge] served task-5964797fe2d1ae25c3 lease=lease-19dd5ff200a6
[remote-gateway-bridge] task task-ag2space_dlocal~task-5964797fe2d1ae25c3: served again under a new lease while its run may be executing — nothing is published until the ack names the generation
[remote-gateway-bridge] task task-5964797fe2d1ae25c3: generation 2 gets its own run task-ag2space_dlocal~task-5964797fe2d1ae25c3~~2; the earlier run task-ag2space_dlocal~task-5964797fe2d1ae25c3 is stopped and its result is never p
[remote-gateway-bridge] held in flight while their records owe work: task-ag2space_dlocal~task-5964797fe2d1ae25c3~~2
[remote-gateway-bridge] queued task-ag2space_dlocal~task-5964797fe2d1ae25c3~~2
[remote-gateway-bridge] result task-ag2space_dlocal~task-5964797fe2d1ae25c3~~2 delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
[remote-gateway-bridge] archived result for task-ag2space_dlocal~task-5964797fe2d1ae25c3~~2
+ 320.8s posts ['delta163020']
+ 320.8s run-2 item {}
+ 320.9s queued ~~2 count 1  dedup lines 0
+ 329.1s restarted normally, no launcher: launcher procs 0  bridge cmd src/remote-gateway-bridge.py
+ 344.3s after plain restart [' responded       | t        |                    |                 |                    2 |       2 |             7'] ledger None
+ 344.3s posts ['delta163020']
+ 344.4s bridge procs 2
raw-5885-r23.txt
rig 200
=== U: upgrade with a non-empty ledger from the earlier format
old ledger before start: records=1 content={"task-upgrade-probe": {"attempt": 1, "execution_generation": 1, "lease_id": "lease-upgrade-probe", "awaiting_ack": false, "stop_requested": null}}
old ledger sha before start: a2f811be440b69fd
old ledger sha after start:  a2f811be440b69fd
2026-11-01T05:36:21Z [remote-gateway-bridge] relay-task-attempts.ag2space_dlocal.json is from an earlier bridge and is ignored; its attempts are not resumed
2026-11-01T05:36:21Z [remote-gateway-bridge] relay-task-attempts-v2.ag2space_dlocal.json is from an earlier bridge and is ignored; its attempts are not resumed
probe in new ledger: 0
probe mentioned in bridge log: 0
$ date -u; git rev-parse HEAD; git status --porcelain
Sun Nov  1 05:37:21 UTC 2026
5c64cae64abf8429f25732eecb0c330a0a8e1946
?? workspace
bridge started: Sun Nov  1 16:36:21 2026    
REMOTE_TASK_LIFECYCLE_V2=1
REMOTE_TASK_TURN_CONTEXT=1
Sutando: idle                                                               [-]
=== A: ask, wake, one reply
+  0.1s sent $q7zZH7fiY8AZlwMRArNEkPYovLLSy9Pb642KIF3FUKk
+ 20.1s task task-48d6b87886baa3d29c question q_o8eptCTDdHoGOAsaSA3I2NSN
+ 24.2s ledger [' waiting_input   | ']
+ 24.4s answer 200 {"candidate_id": "mcp:@admin:ag2space.local:op-c152d0aeb4", "question_id": "q_o8eptCTDdHoGOAsaSA3I2NSN", "outcome": "accepted", "reason": null, "question_revision": 2, "question_status": "answered"}
+ 48.9s ledger final [' responded       | t        |                 |                    |                |             7']
+ 49.0s question [' answered | ']
2026-11-01T05:37:22Z [remote-gateway-bridge] served task-48d6b87886baa3d29c lease=lease-1fa0592a1536
2026-11-01T05:37:22Z [remote-gateway-bridge] queued task-ag2space_dlocal~task-48d6b87886baa3d29c
2026-11-01T05:37:41Z [remote-gateway-bridge] result task-ag2space_dlocal~task-48d6b87886baa3d29c: open HITL blocker -> {'status': 'waiting_input', 'no_send': True}
2026-11-01T05:37:41Z [remote-gateway-bridge] result task-ag2space_dlocal~task-48d6b87886baa3d29c accepted by the broker, room delivery unconfirmed (accepted), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaim
2026-11-01T05:37:41Z [remote-gateway-bridge] archived result for task-ag2space_dlocal~task-48d6b87886baa3d29c
2026-11-01T05:37:46Z [remote-gateway-bridge] served task-48d6b87886baa3d29c lease=lease-10679fd14d10
2026-11-01T05:37:46Z [remote-gateway-bridge] wake for task-ag2space_dlocal~task-48d6b87886baa3d29c~~2: episode ep_q_o8eptCTDdHoGOAsaSA3I2NSN_1 queued as a new turn
2026-11-01T05:37:46Z [remote-gateway-bridge] queued task-ag2space_dlocal~task-48d6b87886baa3d29c~~2
2026-11-01T05:37:53Z [remote-gateway-bridge] result task-ag2space_dlocal~task-48d6b87886baa3d29c~~2: deliberate result file -> {'status': 'completed'}
2026-11-01T05:37:53Z [remote-gateway-bridge] result task-ag2space_dlocal~task-48d6b87886baa3d29c~~2 delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
2026-11-01T05:37:53Z [remote-gateway-bridge] archived result for task-ag2space_dlocal~task-48d6b87886baa3d29c~~2
wake headers: wake_episode: ep_q_o8eptCTDdHoGOAsaSA3I2NSN_1
wake item: None
+ 49.1s agent posts: ['Question: Do you want Red or Blue?', '* Question resolved: Do you want Red or Blue?', 'You chose Blue.']
TID=task-48d6b87886baa3d29c EP=ep_q_o8eptCTDdHoGOAsaSA3I2NSN_1
execution_generation:                     1
=== B: Stop while waiting
+  0.0s sent $YOX4TZvda0glU-fzZ0jrJL00UzruMR4c3H9957aPazU
+ 10.7s task task-f5b19511f0400f668b question q_vzlIib_6j3lkJxkfH5Yg281G
+ 14.8s ledger [' waiting_input   | ']
+ 14.9s STOP 200 {"task_id": "task-f5b19511f0400f668b", "mode": "cancel_execution", "ui_dismissed_at": null, "cancellation_state": "cancel_confirmed", "execution_generation": 1, "terminal": true, "terminal_reason": "cancelled_by_user", "closure_reason": "stopped_while_waiting"
+ 17.9s answer 200 {"candidate_id": "mcp:@admin:ag2space.local:op-3a4f6cbe6f", "question_id": "q_vzlIib_6j3lkJxkfH5Yg281G", "outcome": "rejected", "reason": "cancelled", "question_revision": 2, "question_status": "cance
+ 33.2s ledger final [' dead            | t        | cancelled_by_user | cancel_confirmed   | stopped_while_waiting |             5']
+ 33.2s question [' cancelled | asker_stopped']
2026-11-01T05:38:12Z [remote-gateway-bridge] served task-f5b19511f0400f668b lease=lease-4db131ffec87
2026-11-01T05:38:12Z [remote-gateway-bridge] queued task-ag2space_dlocal~task-f5b19511f0400f668b
2026-11-01T05:38:20Z [remote-gateway-bridge] result task-ag2space_dlocal~task-f5b19511f0400f668b: open HITL blocker -> {'status': 'waiting_input', 'no_send': True}
2026-11-01T05:38:20Z [remote-gateway-bridge] result task-ag2space_dlocal~task-f5b19511f0400f668b accepted by the broker, room delivery unconfirmed (accepted), via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaim
2026-11-01T05:38:20Z [remote-gateway-bridge] archived result for task-ag2space_dlocal~task-f5b19511f0400f668b
wake headers: 
wake item: None
+ 33.3s agent posts: ['Question: Do you want Cats or Dogs?', '* Question cancelled: Do you want Cats or Dogs?']
=== C: same-episode replay across a gateway stop/restart
[remote-gateway-bridge] wake for task-ag2space_dlocal~task-48d6b87886baa3d29c: not run here (already run here, or no usable episode) — left unacked; logged once per episode
REPLAY local=task-ag2space_dlocal~task-48d6b87886baa3d29c episode=ep_q_o8eptCTDdHoGOAsaSA3I2NSN_1 write_task=None task_file_before=False after=False
REPLAY wake_item=None
queued count for episode: 1  bridges: 1
=== D: Stop during a tasks-dir write outage, gateway restart during it
+  0.1s sent $ooZR9oAN9ZDsmuDjjsfsH6PDuFMBR5mZZjWvUHrvUSk
+  1.4s task task-8e923dc7bbda2a6ae8
+  1.4s core took the task; task file gone: False
+ 15.5s phase [' working']
+ 15.6s tasks dir read-only
+ 15.6s STOP 200 cancel_requested False
+ 40.6s ledger during fault {'run': 'task-ag2space_dlocal~task-8e923dc7bbda2a6ae8', 'claims': None, 'dispatch_lost': None, 'owed_close': None, 'stop_requested': True, 'stop_ctl': 'task-ctl-1793511570532-fd17e5-ag2space_dlocal', 'owed_ctl': {'id': 'task-ctl-1793511570532-fd17e5-ag2space_dlocal', 'reason': 'task_cancelled', 'gen': 1}}
+ 46.8s gateway restarted during fault
+ 66.8s ledger after restart {'run': 'task-ag2space_dlocal~task-8e923dc7bbda2a6ae8', 'claims': None, 'dispatch_lost': None, 'owed_close': None, 'stop_requested': True, 'stop_ctl': 'task-ctl-1793511570532-fd17e5-ag2space_dlocal', 'owed_ctl': {'id': 'task-ctl-1793511570532-fd17e5-ag2space_dlocal', 'reason': 'task_cancelled', 'gen': 1}}
+ 66.8s tasks dir restored
+ 86.9s ledger after recovery {'run': 'task-ag2space_dlocal~task-8e923dc7bbda2a6ae8', 'claims': None, 'dispatch_lost': None, 'owed_close': None, 'stop_requested': True, 'stop_ctl': 'task-ctl-1793511570532-fd17e5-ag2space_dlocal', 'owed_ctl': None}
+ 89.6s ctl files ['tasks/task-ctl-1793511570532-fd17e5-ag2space_dlocal.txt', 'state/activity/.task-ctl-1793511570532-fd17e5-ag2space_dlocal.lock', 'state/activity/task-ctl-1793511570532-fd17e5-ag2space_dlocal.json', 'state/agent-activity.acks/task-ctl-1793511570532-fd17e5-ag2space_dlocal']
+232.3s ctl files final ['tasks/task-ctl-1793511570532-fd17e5-ag2space_dlocal.txt', 'state/activity/.task-ctl-1793511570532-fd17e5-ag2space_dlocal.lock', 'state/activity/task-ctl-1793511570532-fd17e5-ag2space_dlocal.json', 'state/agent-activity.acks/task-ctl-1793511570532-fd17e5-ag2space_dlocal', 'results/task-ctl-1793511570532-fd17e5-ag2space_dlocal.txt']
+232.6s final [' dead            | cancel_confirmed   | cancelled_by_user']
+232.6s ledger final {}
eway-bridge] task task-ag2space_dlocal~task-8e923dc7bbda2a6ae8: cancel instruction not written — retried next beat
2026-11-01T05:39:45Z [remote-gateway-bridge] durable stage failed for task-ctl-1793511570532-fd17e5-ag2space_dlocal.txt (PermissionError EACCES at result_publish.py:46)
2026-11-01T05:39:45Z [remote-gateway-bridge] task task-ag2space_dlocal~task-8e923dc7bbda2a6ae8: cancel instruction not written — retried next beat
2026-11-01T05:40:01Z [remote-gateway-bridge] durable stage failed for task-ctl-1793511570532-fd17e5-ag2space_dlocal.txt (PermissionError EACCES at result_publish.py:46)
2026-11-01T05:40:01Z [remote-gateway-bridge] task task-ag2space_dlocal~task-8e923dc7bbda2a6ae8: cancel instruction not written — retried next beat
2026-11-01T05:40:01Z [remote-gateway-bridge] durable stage failed for task-ctl-1793511570532-fd17e5-ag2space_dlocal.txt (PermissionError EACCES at result_publish.py:46)
2026-11-01T05:40:01Z [remote-gateway-bridge] task task-ag2space_dlocal~task-8e923dc7bbda2a6ae8: cancel instruction not written — retried next beat
2026-11-01T05:40:16Z [remote-gateway-bridge] durable stage failed for task-ctl-1793511570532-fd17e5-ag2space_dlocal.txt (PermissionError EACCES at result_publish.py:46)
2026-11-01T05:40:16Z [remote-gateway-bridge] task task-ag2space_dlocal~task-8e923dc7bbda2a6ae8: cancel instruction not written — retried next beat
2026-11-01T05:41:38Z [remote-gateway-bridge] task task-8e923dc7bbda2a6ae8: the core answered the Stop with nothing to deliver — stopped
2026-11-01T05:41:38Z [remote-gateway-bridge] result task-ag2space_dlocal~task-8e923dc7bbda2a6ae8: settled without a post (the Stop is confirmed)
2026-11-01T05:41:38Z [remote-gateway-bridge] archived task-ag2space_dlocal~task-8e923dc7bbda2a6ae8 (marker no-send, lease closed, not sent)

$APP/workspace/tasks/task-ctl-1793511570532-fd17e5-ag2space_dlocal.txt
$APP/workspace/tasks/archive/task-ag2space_dlocal~task-8e923dc7bbda2a6ae8.txt
task-ctl-1793511570532-fd17e5-ag2space_dlocal.txt

tasks dir mode after run: drwxr-xr-x@
raw-5885-r23-stall.txt
=== E: Stop during a stalled effect, unrelated tasks keep moving
+  21.8s gateway stopped; bridges 1
+  22.3s task A task-6b1c269181323b56b1 [' queued          | f        |                    |                 |                    1 |       1 |             1']
+  22.3s launcher started in the gateway window --fault stall --kind result --task task-6b1c269181323b56b1 --lease * --sentinel $S/at/hold-164323 --report $S/at/rep-stall-164323.jsonl
+  32.3s A held at its result POST
+  32.3s report {"at": 1793511826.381951, "event": "loaded", "head": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+  32.3s report {"at": 1793511832.589449, "claim": {"epoch": 91, "execution_generation": 1, "kind": "result", "lease_id": "lease-edd4116990bc", "op": "result-0d0fbfd5b278aff8", "run": "task-ag2space_dlocal~task-6b1c269181323b56b1", "task": "task-6b1c269181323b56b1", "wake_episode": null}, "event": "matched", "fault": "stall", "path": "/v1/results"}
+  32.3s A ledger {"run": "task-ag2space_dlocal~task-6b1c269181323b56b1", "room_id": "!0NKpmrHnsMCcW1d6gAKa3wEVPNIz-knxdErdCmNVN0w", "thread_root_id": null, "source_event_id": "$PutzZmVpv8KGjyDDO0LZBhy3A27aQntyove3PeQ50OE", "source_kind": null, "awaiting_ack": false, "lease_id": "lease-edd4116990bc", "published_at": 1793511826.498396, "claims": {"result-0d0fbfd5b278aff8": {"kind": "result", "run": "task-ag2space_dlocal~task-6b1c269181323b56b1", "execution_generation": 1, "wake_episode": null, "file": [16777232, 2
+  32.4s STOP task-6b1c269181323b56b1 http 200 in 0.03s cancel_requested terminal False
+  34.5s A after Stop [' working         | f        | cancel_requested   |                 |                    1 |       1 |             4'] held still: True
+  34.6s task B task-4c915d46d737ef10ac
+  40.8s B posted: ['beta164323'] [' responded       | t        |                    |                 |                    1 |       1 |             4']
+  40.8s task C task-281de60d96a28ae994
+  50.1s C [' working         | f        |                    |                 |                    1 |       1 |             3']
+  50.2s STOP task-281de60d96a28ae994 http 200 in 0.03s cancel_requested terminal False
+  70.2s C after Stop [' working         | f        | cancel_requested   |                 |                    1 |       1 |             4']
+  70.2s still held while B and C moved: True
+  70.2s sentinel removed
+  95.2s report {"at": 1793511826.381951, "event": "loaded", "head": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+  95.2s report {"at": 1793511832.589449, "claim": {"epoch": 91, "execution_generation": 1, "kind": "result", "lease_id": "lease-edd4116990bc", "op": "result-0d0fbfd5b278aff8", "run": "task-ag2space_dlocal~task-6b1c269181323b56b1", "task": "task-6b1c269181323b56b1", "wake_episode": null}, "event": "matched", "fault": "stall", "path": "/v1/results"}
+  95.2s report {"answer": {"delivery_readable": true, "ok": true}, "at": 1793511874.075718, "event": "released"}
+  95.4s A final [' responded       | t        |                    |                 |                    1 |       1 |             5'] ledger null
+  95.5s alpha posts ['alpha164323'] beta posts ['beta164323']
[remote-gateway-bridge] served task-6b1c269181323b56b1 lease=lease-edd4116990bc
[remote-gateway-bridge] queued task-ag2space_dlocal~task-6b1c269181323b56b1
[remote-gateway-bridge] result task-ag2space_dlocal~task-6b1c269181323b56b1: deliberate result file -> {'status': 'completed'}
[remote-gateway-bridge] task task-6b1c269181323b56b1: Stop requested — the core is asked to stop; a result it still writes is delivered
[remote-gateway-bridge] result task-ag2space_dlocal~task-6b1c269181323b56b1 delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
[remote-gateway-bridge] archived result for task-ag2space_dlocal~task-6b1c269181323b56b1
[remote-gateway-bridge] outbound worker: task-ag2space_dlocal~task-6b1c269181323b56b1 seen->retired 36889ms (monotonic)
+ 103.8s restarted normally, no launcher: launcher procs 0  bridge cmd src/remote-gateway-bridge.py
+ 123.9s after plain restart: A [' responded       | t        |                    |                 |                    1 |       1 |             5'] ledger None
+ 124.0s alpha posts ['alpha164323']
+ 124.0s bridges 3
raw-5885-r23-crash-ack.txt
=== F: kill after the broker accepted the ack, before the local commit
+  21.2s gateway stopped; bridges 1
+  21.6s task task-af2096836461e2335f [' queued          | f        |                    |                 |                    1 |       1 |             1']
+  21.7s launcher started in the gateway window --fault crash --kind ack --task task-af2096836461e2335f --lease * --report $S/at/rep-crash-ack-164548.jsonl
+  32.7s report {"at": 1793511970.082388, "event": "loaded", "head": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+  32.7s report {"at": 1793511970.3319712, "claim": {"epoch": 93, "execution_generation": null, "kind": "ack", "lease_id": "lease-47ae841a3fd0", "op": "ack-fd9cdcb10e21777c", "run": "task-ag2space_dlocal~task-af2096836461e2335f", "task": "task-af2096836461e2335f", "wake_episode": null}, "event": "matched", "fault": "crash", "path": "/v1/tasks/task-af2096836461e2335f/ack"}
+  32.7s report {"answer": {"attempt": 1, "durable": true, "execution_generation": 1, "lease_expires_at": 1793512030.197883, "ok": true, "task_id": "task-af2096836461e2335f"}, "at": 1793511970.3434198, "event": "accepted by the broker; killing the bridge before its commit", "op": "ack-fd9cdcb10e21777c"}
+  32.7s launcher procs after exit 9 0
+  32.7s window ['claude']
+  32.9s broker row at the crash [' working         | f        |                    |                 |                    1 |       1 |             3']
+  32.9s ledger at the crash {"run": "task-ag2space_dlocal~task-af2096836461e2335f", "room_id": "!0NKpmrHnsMCcW1d6gAKa3wEVPNIz-knxdErdCmNVN0w", "thread_root_id": null, "source_event_id": "$RUc4B0X8Gpn7-QYN0_WAMVkUjzEDtkH30NP07EKGm18", "source_kind": null, "awaiting_ack": true, "lease_id": "lease-47ae841a3fd0", "published_at": 1793511970.217556, "claims": {"ack-fd9cdcb10e21777c": {"kind": "ack", "run": "task-ag2space_dlocal~task-af2096836461e2335f", "execution_generation": null, "wake_episode": null, "serve": "lease-47ae841a3fd0", "epoch": 93, "at": 1793511970.331139, "turn_id": null}}}
+  39.8s restarted normally, no launcher: launcher procs 0  bridge cmd src/remote-gateway-bridge.py
+  39.8s owner epoch before/after {'epoch': 92, 'pid': 15373, 'at': 1793511907.569962} {'epoch': 93, 'pid': 20936, 'at': 1793511970.094399}
+  66.8s posted: True
+  67.0s final [' responded       | t        |                    |                 |                    1 |       1 |             4'] ledger None
+  67.0s posts ['gamma164548']
[remote-gateway-bridge] served task-af2096836461e2335f lease=lease-47ae841a3fd0
[remote-gateway-bridge] queued task-ag2space_dlocal~task-af2096836461e2335f
2026-10-31T20:17:56Z [remote-gateway-bridge] task task-8fc72a9cdaa0e3cbd4: effects claimed before this start, outcome unknown: ack ack-7e9e07ccbd8ec831 — reconciled under the same operation ids
2026-10-31T20:18:53Z [remote-gateway-bridge] task task-333dc602a30d7a1110: effects claimed before this start, outcome unknown: result result-fe5c3038041d7f96 — reconciled under the same operation ids
2026-10-31T21:34:04Z [remote-gateway-bridge] task task-d95ca1aee94fb49cbe: effects claimed before this start, outcome unknown: ack ack-fd0ad3ce50336d8a — reconciled under the same operation ids
2026-10-31T21:35:11Z [remote-gateway-bridge] task task-77b1fe37600fcdc120: effects claimed before this start, outcome unknown: result result-2d6ce694137a3f81 — reconciled under the same operation ids
2026-10-31T22:33:58Z [remote-gateway-bridge] task task-7577a7e3472bff5212: effects claimed before this start, outcome unknown: ack ack-8f13e896a5f45355 — reconciled under the same operation ids
2026-10-31T22:35:05Z [remote-gateway-bridge] task task-65ba3bfffa7ebf6476: effects claimed before this start, outcome unknown: result result-05f32880a83ab006 — reconciled under the same operation ids
2026-10-31T23:23:58Z [remote-gateway-bridge] task task-864997f86eecd3ea9a: effects claimed before this start, outcome unknown: ack ack-4ef92fb826d9a0a3 — reconciled under the same operation ids
2026-10-31T23:25:04Z [remote-gateway-bridge] task task-c7e9831e872b8e1f99: effects claimed before this start, outcome unknown: result result-19c02908b61394ca — reconciled under the same operation ids
2026-11-01T00:49:01Z [remote-gateway-bridge] task task-a5cccd2c4efaec5a57: effects claimed before this start, outcome unknown: ack ack-16400be29b2597d1 — reconciled under the same operation ids
2026-11-01T00:50:09Z [remote-gateway-bridge] task task-91870f9816b14847f9: effects claimed before this start, outcome unknown: result result-06d2f2f19a7c9147 — reconciled under the same operation ids
2026-11-01T02:14:36Z [remote-gateway-bridge] task task-aacc45ebcdc6824221: effects claimed before this start, outcome unknown: ack ack-f80fbc178e35437a — reconciled under the same operation ids
2026-11-01T02:15:43Z [remote-gateway-bridge] task task-4042fca78ee474018a: effects claimed before this start, outcome unknown: result result-288e47d2ce28dbd6 — reconciled under the same operation ids
2026-11-01T04:11:05Z [remote-gateway-bridge] task task-f5c69d259d0887071e: effects claimed before this start, outcome unknown: ack ack-ca9864ae56bcd5f2 — reconciled under the same operation ids
2026-11-01T04:12:32Z [remote-gateway-bridge] task task-b6dc1e97a656d3289f: effects claimed before this start, outcome unknown: result result-cd953b06385e6621 — reconciled under the same operation ids
2026-11-01T05:46:27Z [remote-gateway-bridge] task task-af2096836461e2335f: effects claimed before this start, outcome unknown: ack ack-fd9cdcb10e21777c — reconciled under the same operation ids
2026-11-01T05:46:27Z [remote-gateway-bridge] result task-ag2space_dlocal~task-af2096836461e2335f: held — its ack has not named the lease yet
2026-11-01T05:46:27Z [remote-gateway-bridge] task task-af2096836461e2335f: result claim result-310cd1ae160c6b81 is kept — its reply is still at its name, unsent; a newer generation waits until it is delivered or, if it cannot be r
2026-11-01T05:46:28Z [remote-gateway-bridge] result task-ag2space_dlocal~task-af2096836461e2335f: deliberate result file -> {'status': 'completed'}
2026-11-01T05:46:29Z [remote-gateway-bridge] result task-ag2space_dlocal~task-af2096836461e2335f delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
2026-11-01T05:46:29Z [remote-gateway-bridge] archived result for task-ag2space_dlocal~task-af2096836461e2335f
2026-11-01T05:46:29Z [remote-gateway-bridge] outbound worker: task-ag2space_dlocal~task-af2096836461e2335f seen->retired 1357ms (monotonic)
+  67.1s bridges 3
raw-5885-r23-crash-result.txt
=== F: kill after the broker accepted the result, before the local commit
+  21.2s gateway stopped; bridges 1
+  21.5s task task-9e59059fc05961ecc2 [' queued          | f        |                    |                 |                    1 |       1 |             1']
+  21.6s launcher started in the gateway window --fault crash --kind result --task task-9e59059fc05961ecc2 --lease * --report $S/at/rep-crash-result-164715.jsonl
+  32.6s report {"at": 1793512057.251819, "event": "loaded", "head": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparr
+  32.6s report {"at": 1793512063.536895, "claim": {"epoch": 95, "execution_generation": 1, "kind": "result", "lease_id": "lease-ad48a2dd1914", "op": "result-93e90f653b249c67", "run": "task-ag2space_dlocal~task-9e59059fc05961ecc2", "task": "task-9e59059fc05961ecc2", "wake_episode": null}, "event": "matched", "fault": "crash", "path": "/v1/results"}
+  32.6s report {"answer": {"delivery_readable": true, "ok": true}, "at": 1793512063.563562, "event": "accepted by the broker; killing the bridge before its commit", "op": "result-93e90f653b249c67"}
+  32.6s launcher procs after exit 9 0
+  32.6s window ['claude']
+  32.8s broker row at the crash [' responded       | t        |                    |                 |                    1 |       1 |             4']
+  32.8s ledger at the crash {"run": "task-ag2space_dlocal~task-9e59059fc05961ecc2", "room_id": "!0NKpmrHnsMCcW1d6gAKa3wEVPNIz-knxdErdCmNVN0w", "thread_root_id": null, "source_event_id": "$UmxssEzdf0-ZHQVmFCdK48G06tBlVmKUFh98c_GaTgY", "source_kind": null, "awaiting_ack": false, "lease_id": "lease-ad48a2dd1914", "published_at": 1793512057.35443, "claims": {"result-93e90f653b249c67": {"kind": "result", "run": "task-ag2space_dlocal~task-9e59059fc05961ecc2", "execution_generation": 1, "wake_episode": null, "file": [16777232, 294050028, 1793512063312203070, "313eea21447b252fc73d609ab4101011e9b80245a1b5f490c50aa64440500434"], "data": {"digest": "313eea21447b252fc73d609ab4101011e9b80245a1b5f490c50aa64440500434", "tid": "task-a
+  39.9s restarted normally, no launcher: launcher procs 0  bridge cmd src/remote-gateway-bridge.py
+  39.9s owner epoch before/after {'epoch': 94, 'pid': 23898, 'at': 1793511987.9266791} {'epoch': 95, 'pid': 30067, 'at': 1793512057.264566}
+  64.9s posted: True
+  65.1s final [' responded       | t        |                    |                 |                    1 |       1 |             4'] ledger None
+  65.1s posts ['gamma164715']
[remote-gateway-bridge] queued task-ag2space_dlocal~task-9e59059fc05961ecc2
[remote-gateway-bridge] result task-ag2space_dlocal~task-9e59059fc05961ecc2: deliberate result file -> {'status': 'completed'}
2026-10-31T20:17:56Z [remote-gateway-bridge] task task-8fc72a9cdaa0e3cbd4: effects claimed before this start, outcome unknown: ack ack-7e9e07ccbd8ec831 — reconciled under the same operation ids
2026-10-31T20:18:53Z [remote-gateway-bridge] task task-333dc602a30d7a1110: effects claimed before this start, outcome unknown: result result-fe5c3038041d7f96 — reconciled under the same operation ids
2026-10-31T21:34:04Z [remote-gateway-bridge] task task-d95ca1aee94fb49cbe: effects claimed before this start, outcome unknown: ack ack-fd0ad3ce50336d8a — reconciled under the same operation ids
2026-10-31T21:35:11Z [remote-gateway-bridge] task task-77b1fe37600fcdc120: effects claimed before this start, outcome unknown: result result-2d6ce694137a3f81 — reconciled under the same operation ids
2026-10-31T22:33:58Z [remote-gateway-bridge] task task-7577a7e3472bff5212: effects claimed before this start, outcome unknown: ack ack-8f13e896a5f45355 — reconciled under the same operation ids
2026-10-31T22:35:05Z [remote-gateway-bridge] task task-65ba3bfffa7ebf6476: effects claimed before this start, outcome unknown: result result-05f32880a83ab006 — reconciled under the same operation ids
2026-10-31T23:23:58Z [remote-gateway-bridge] task task-864997f86eecd3ea9a: effects claimed before this start, outcome unknown: ack ack-4ef92fb826d9a0a3 — reconciled under the same operation ids
2026-10-31T23:25:04Z [remote-gateway-bridge] task task-c7e9831e872b8e1f99: effects claimed before this start, outcome unknown: result result-19c02908b61394ca — reconciled under the same operation ids
2026-11-01T00:49:01Z [remote-gateway-bridge] task task-a5cccd2c4efaec5a57: effects claimed before this start, outcome unknown: ack ack-16400be29b2597d1 — reconciled under the same operation ids
2026-11-01T00:50:09Z [remote-gateway-bridge] task task-91870f9816b14847f9: effects claimed before this start, outcome unknown: result result-06d2f2f19a7c9147 — reconciled under the same operation ids
2026-11-01T02:14:36Z [remote-gateway-bridge] task task-aacc45ebcdc6824221: effects claimed before this start, outcome unknown: ack ack-f80fbc178e35437a — reconciled under the same operation ids
2026-11-01T02:15:43Z [remote-gateway-bridge] task task-4042fca78ee474018a: effects claimed before this start, outcome unknown: result result-288e47d2ce28dbd6 — reconciled under the same operation ids
2026-11-01T04:11:05Z [remote-gateway-bridge] task task-f5c69d259d0887071e: effects claimed before this start, outcome unknown: ack ack-ca9864ae56bcd5f2 — reconciled under the same operation ids
2026-11-01T04:12:32Z [remote-gateway-bridge] task task-b6dc1e97a656d3289f: effects claimed before this start, outcome unknown: result result-cd953b06385e6621 — reconciled under the same operation ids
2026-11-01T05:46:27Z [remote-gateway-bridge] task task-af2096836461e2335f: effects claimed before this start, outcome unknown: ack ack-fd9cdcb10e21777c — reconciled under the same operation ids
2026-11-01T05:47:55Z [remote-gateway-bridge] task task-9e59059fc05961ecc2: effects claimed before this start, outcome unknown: result result-93e90f653b249c67 — reconciled under the same operation ids
2026-11-01T05:47:55Z [remote-gateway-bridge] result task-ag2space_dlocal~task-9e59059fc05961ecc2: deliberate result file -> {'status': 'completed'}
2026-11-01T05:47:55Z [remote-gateway-bridge] result task-ag2space_dlocal~task-9e59059fc05961ecc2: pending retry or delivery claim (attempts=0, next=1793512063.5355241, deadline=1793512663.5355241)
2026-11-01T05:47:55Z [remote-gateway-bridge] task task-9e59059fc05961ecc2: attempt fenced by the broker (task_already_terminal)
2026-11-01T05:47:56Z [remote-gateway-bridge] result task-ag2space_dlocal~task-9e59059fc05961ecc2: settled without a post (attempt fenced (task_already_terminal))
2026-11-01T05:47:56Z [remote-gateway-bridge] archived result for task-ag2space_dlocal~task-9e59059fc05961ecc2
2026-11-01T05:47:56Z [remote-gateway-bridge] outbound worker: task-ag2space_dlocal~task-9e59059fc05961ecc2 seen->retired 1031ms (monotonic)
+  65.2s bridges 3
launcher report: rep-reclaim
{"at": 1793500194.933236, "event": "loaded", "head": "5bed4b92880dbe3dbce65110f054519d680dabaa", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py", "module_sha256": "4c86bd37d080a0b71e36a6c661c26a7f475f3699e51b7872040fd97537868abe", "rule": {"fault": "reclaim", "generation": null, "kind": "serve", "lease": null, "report": "$S/at/rep-reclaim-132954.jsonl", "run": null, "sentinel": "$S/at/hold-132954", "task": "task-e41dde03c23202564d"}, "runtime_identity": {"build_sha": "5bed4b92880dbe3dbce65110f054519d680dabaa", "entrypoint": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_sha256": "4c86bd37d080a0b71e36a6c661c26a7f475f3699e51b7872040fd97537868abe"}}
{"at": 1793500210.306728, "event": "beats withheld", "task": "task-e41dde03c23202564d"}
{"at": 1793500270.5235682, "event": "offline: broker calls withheld", "path": "/v1/tasks/heartbeat"}
{"at": 1793500453.69118, "event": "online", "withheld_calls": 17}
{"at": 1793500453.7552042, "claim": {"execution_generation": 2, "kind": "serve", "lease_id": "lease-648773969e4d", "task": "task-e41dde03c23202564d"}, "event": "matched", "fault": "reclaim", "first_lease": "lease-3592e8046a15", "path": "/v1/tasks?wait=15&capabilities=voice-outcome-v1"}
{"at": 1793500453.875903, "body": "{\"error\": \"stale execution generation\", \"error_code\": \"stale_execution_generation\", \"current_execution_generation\": 2}", "event": "result posted after the newer serve", "execution_generation": 1, "status": 409}
{"at": 1793500468.788029, "event": "released", "lease_id": "lease-648773969e4d"}
launcher report: rep-crash-result
{"at": 1793512057.251819, "event": "loaded", "head": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py", "module_sha256": "66cb9804066ba379c908b4010029974332118500c4a2187b39658f89ebea8292", "rule": {"fault": "crash", "generation": null, "kind": "result", "lease": "*", "report": "$S/at/rep-crash-result-164715.jsonl", "run": null, "sentinel": null, "task": "task-9e59059fc05961ecc2"}, "runtime_identity": {"build_sha": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "entrypoint": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_sha256": "66cb9804066ba379c908b4010029974332118500c4a2187b39658f89ebea8292"}}
{"at": 1793512063.536895, "claim": {"epoch": 95, "execution_generation": 1, "kind": "result", "lease_id": "lease-ad48a2dd1914", "op": "result-93e90f653b249c67", "run": "task-ag2space_dlocal~task-9e59059fc05961ecc2", "task": "task-9e59059fc05961ecc2", "wake_episode": null}, "event": "matched", "fault": "crash", "path": "/v1/results"}
{"answer": {"delivery_readable": true, "ok": true}, "at": 1793512063.563562, "event": "accepted by the broker; killing the bridge before its commit", "op": "result-93e90f653b249c67"}
launcher report: rep-crash-ack
{"at": 1793511970.082388, "event": "loaded", "head": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py", "module_sha256": "66cb9804066ba379c908b4010029974332118500c4a2187b39658f89ebea8292", "rule": {"fault": "crash", "generation": null, "kind": "ack", "lease": "*", "report": "$S/at/rep-crash-ack-164548.jsonl", "run": null, "sentinel": null, "task": "task-af2096836461e2335f"}, "runtime_identity": {"build_sha": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "entrypoint": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_sha256": "66cb9804066ba379c908b4010029974332118500c4a2187b39658f89ebea8292"}}
{"at": 1793511970.3319712, "claim": {"epoch": 93, "execution_generation": null, "kind": "ack", "lease_id": "lease-47ae841a3fd0", "op": "ack-fd9cdcb10e21777c", "run": "task-ag2space_dlocal~task-af2096836461e2335f", "task": "task-af2096836461e2335f", "wake_episode": null}, "event": "matched", "fault": "crash", "path": "/v1/tasks/task-af2096836461e2335f/ack"}
{"answer": {"attempt": 1, "durable": true, "execution_generation": 1, "lease_expires_at": 1793512030.197883, "ok": true, "task_id": "task-af2096836461e2335f"}, "at": 1793511970.3434198, "event": "accepted by the broker; killing the bridge before its commit", "op": "ack-fd9cdcb10e21777c"}
launcher report: rep-stall
{"at": 1793511826.381951, "event": "loaded", "head": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py", "module_sha256": "66cb9804066ba379c908b4010029974332118500c4a2187b39658f89ebea8292", "rule": {"fault": "stall", "generation": null, "kind": "result", "lease": "*", "report": "$S/at/rep-stall-164323.jsonl", "run": null, "sentinel": "$S/at/hold-164323", "task": "task-6b1c269181323b56b1"}, "runtime_identity": {"build_sha": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "entrypoint": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_sha256": "66cb9804066ba379c908b4010029974332118500c4a2187b39658f89ebea8292"}}
{"at": 1793511832.589449, "claim": {"epoch": 91, "execution_generation": 1, "kind": "result", "lease_id": "lease-edd4116990bc", "op": "result-0d0fbfd5b278aff8", "run": "task-ag2space_dlocal~task-6b1c269181323b56b1", "task": "task-6b1c269181323b56b1", "wake_episode": null}, "event": "matched", "fault": "stall", "path": "/v1/results"}
{"answer": {"delivery_readable": true, "ok": true}, "at": 1793511874.075718, "event": "released"}

@marklysze

Copy link
Copy Markdown
Collaborator Author

Correction to my witness at 5c64cae64abf8429f25732eecb0c330a0a8e1946: Kewei is right that the rep-reclaim appendix was the wrong file, from an older run at 5bed4b92. That was my paste error. This is the report for the G run at this head (task task-5964797fe2d1ae25c3). The rest of the witness stands.

launcher report: rep-reclaim at 5c64cae
{"at": 1793511020.980017, "event": "loaded", "head": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "loader": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_file": "$APP/engine/sutando/packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py", "module_sha256": "66cb9804066ba379c908b4010029974332118500c4a2187b39658f89ebea8292", "rule": {"fault": "reclaim", "generation": null, "kind": "serve", "lease": null, "report": "$S/at/rep-reclaim-163020.jsonl", "run": null, "sentinel": "$S/at/hold-163020", "task": "task-5964797fe2d1ae25c3"}, "runtime_identity": {"build_sha": "5c64cae64abf8429f25732eecb0c330a0a8e1946", "entrypoint": "$APP/engine/sutando/src/remote-gateway-bridge.py", "loader_sha256": "62cb65992a14264e4d91b340471edba4388e12cf826d433764e2201bf9b76a22", "module_sha256": "66cb9804066ba379c908b4010029974332118500c4a2187b39658f89ebea8292"}}
{"at": 1793511036.325828, "event": "offline: broker calls withheld", "path": "/v1/tasks/heartbeat"}
{"at": 1793511214.228234, "event": "online", "withheld_calls": 23}
{"at": 1793511219.47579, "event": "beats withheld", "task": "task-5964797fe2d1ae25c3"}
{"at": 1793511219.535635, "claim": {"execution_generation": 2, "kind": "serve", "lease_id": "lease-19dd5ff200a6", "task": "task-5964797fe2d1ae25c3"}, "event": "matched", "fault": "reclaim", "first_lease": "lease-c9c9725daddd", "path": "/v1/tasks?wait=15&capabilities=voice-outcome-v1"}
{"at": 1793511219.7299788, "body": "{\"error\": \"stale execution generation\", \"error_code\": \"stale_execution_generation\", \"current_execution_generation\": 2}", "event": "result posted after the newer serve", "execution_generation": 1, "status": 409}
{"at": 1793511234.57182, "event": "released", "lease_id": "lease-19dd5ff200a6"}
{"at": 1793511259.5794659, "event": "delivered the held serve", "lease_id": "lease-19dd5ff200a6"}

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head review of 5c64cae64abf8429f25732eecb0c330a0a8e1946: changes requested.

I completed the remaining activated-path audit: every TaskAttempts reader and its direct bridge callers, the held-serve reserve through live and restart recovery, wake re-service, and pending-ACK recovery. The earlier one-snapshot fix is sound, but four blocker roots remain.

1. Unknown or pruned ledger state is still converted into a known decision

These are independently reproduced with one transient EACCES; all later I/O succeeds:

  • broker_names_leases() (task_attempts.py:337-340) returns false on unreadable state. result_hold() (:994-1017) then treats a broker already known to name generations as legacy. Healthy control posted nothing and retained awaiting_ack; one denied read posted once without execution_generation and retired the attempt.
  • serve_adoptable() (:655-673) uses _read() or {}. A same-generation L2/gen1 re-serve that normally keeps the original run instead creates original~~2 with owed_publish=original~~2.
  • run_published() (:631-633) uses _read() or {}. At remote_gateway_bridge.py:5783-5785, the same wake episode after core uptake is republished instead of rejected as a replay.
  • last_run() and revive_finished() (task_attempts.py:626-629,675-691) each collapse an unreadable historical read. Re-serving already delivered G2/L2 normally owes nothing; a fault in either read creates original~~3 at generation 2 and owes publication again.
  • _serve_run() separately calls last_run() (remote_gateway_bridge.py:5855). One denied read bound generation 2 to the original generation-1 run ID.
  • Normal retention is also a real input: _bound() (task_attempts.py:1295-1304) can prune the earlier_run entry. owed_view() (:364-376) turns that absence into {}, and _earlier_attempt() (remote_gateway_bridge.py:8738-8749) emits not_delivered. A genuinely delivered earlier answer is therefore re-run with the instruction that nobody saw it.

Observed discriminators:

contract EACCES: RESULT_POSTS=1 GENERATIONS=[None] ATTEMPT=None
adoption EACCES: GEN=1 RUN=original~~2 OWED=original~~2
wake EACCES: RETURN=(original, True) DUPLICATE_QUEUED=True
last-run/detail EACCES: GEN=2 RUN=original~~3 OWED=original~~3
serve-run EACCES: GEN=2 RUN=original
retention: BEFORE_DELIVERED=True AFTER_TRIM_RETAINS_EARLIER=False RERUN_RULES=[not_delivered]

This needs one typed tri-state reader contract. Unreadable is not false or absent; pruned delivery evidence is neutral unknown, not not_delivered. Falsey malformed maps/entries must not become absence, and truthy malformed entries must not raise. Each caller above needs a healthy control, one-fault recovery, and proof that no extra run/publication/result occurs.

2. The held-serve reserve has no owner identity

_reserve_path() is keyed only by wire task (remote_gateway_bridge.py:5814-5820). A prior owed_publish and a newer pending_serve can therefore own the same mutable file. _hold_reserve() overwrites it (:5823-5846), while _publish_owed_run() combines the owed run with whatever bytes are there (:4511-4541). Deletes are also task-wide, not owner-checked.

Production-harness failures reproduced at this head:

  • R2 owed plus R3 held queues R3 bytes under R2, live and after restart.
  • A canonical 409 for R3 deletes the reserve still needed by R2; R2 becomes dispatch_lost.
  • A bare 404 abandons the R3 pointer but leaves R3 bytes; R2 publishes them.
  • Reserve replacement followed by pending-pointer commit failure, including visible-after-fsync-failure, leaves the old debt pointing at new bytes.
  • Same-generation adoption drops the reserve after releasing the task lock; a concurrent newer hold can be written and then deleted by the old ACK.
  • Repeating the same held lease after its reserve vanished returns durable success without repairing it; the accepted generation has no task.
  • _decide_held_serve() (:4217-4231) maps reserve OSError or malformed JSON to {} and commits anyway. The actual L2 task later queues while the ledger and turn retain L1 room/source identity.

The fix needs immutable reserve objects with an opaque ID and an envelope binding at least (wire, candidate run, lease, payload digest). pending_serve must reference the exact object; switch_run must atomically move that reference into structured owed_publish; reads and deletes must validate and retire only that reference. A per-run filename alone is insufficient because consecutive held leases reuse a candidate run. Add live and restart regressions for the schedules above, including L2→L3→L4 positive control and old-drop/new-hold concurrency against the production writer.

3. A re-served wake bypasses the held-generation protocol

The wake branch of _serve_attempt() (remote_gateway_bridge.py:5764-5811) reuses the run and calls remember_task() directly. It overwrites the lease instead of holding the serve. When the same episode is served as L1/gen1 and then L2/gen2, record_ack() rejects the generation change, _commit_ack() calls it stale, and _post_task_ack() reports success and forgets the retry.

after L1: awaiting_ack=False gen=1 lease=L1
serve L2: awaiting_ack=True  gen=1 lease=L2 pending_serve=None
ACK L2/gen2 returned True
after ACK: awaiting_ack=True gen=1 lease=L2 pending_serve=None

This path is PR-activated, and same-episode re-service is required by the PR test suite. Wakes need the same hold/adopt/switch protocol as ordinary runs, with same-generation and newer-generation regression arms.

4. The durable ACK claim is recoverable only through a lossy sidecar index

_read_ack_json() (remote_gateway_bridge.py:3238-3267) maps unreadable state to {}. One transient denied read while _record_pending_acks() adds another task overwrote the prior pending ACK and lease records, while the authoritative ledger still held its open ACK claim:

before: pending={ack1: true}, lease={ack1: L1}, claim=ack(L1)
after one EACCES plus record ack2: pending={ack2: true}, lease={ack2: L2}, claim1 still open

A missing lease sidecar similarly retries with lease=None; _claim_ack() answers elsewhere, the pending index is deleted, and the L1 claim remains forever. _recover_claims() (:4866-4876) only logs ACK claims; _retry_pending_acks() enumerates the sidecar, so it never retries that claim.

Recovery must enumerate ACK work from the authoritative ledger, or make the index tri-state and refuse any overwrite/retirement while unreadable. Regressions need malformed/missing main and lease indexes with an ambiguous ACK, preserved operation ID, an actual retry, and claim/awaiting_ack clearance.

Scope and verdict

I separately checked the conservative branches: unknown top-level ledger at startup, holds, sweep, claim commit/release, _ack_recorded, _mark_published, heartbeat snapshots, and sole-reserve temporary EACCES. They delay or retry and did not yield another terminal decision. The process-only worker-ID failure is pre-existing and the unreadable ack_first manifest caused local discard of an unaccepted stage but did not establish permanent loss; neither is an independent blocker for this PR on current evidence.

Worst case from the four blockers is duplicate model execution or duplicate room answers, an unfenced result closing the wrong generation, wrong-room turn binding, or an accepted task stranded forever. The blast radius is the opt-in lifecycle path, but that is the path this PR is meant to make safe.

One combined fix is welcome. Please include the discriminating tests above, show the new tests were collected, and rerun the exact-head post-restart live witness after the push. I will re-review that frozen head; this one is not yet the final voice-witness candidate.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Merging with both reviewers approved at 5c64cae64abf8429f25732eecb0c330a0a8e1946. The four failure schedules from the review at #pullrequestreview-5726780247 are tracked in #6088, as the rule for reproduced findings requires. They are behind REMOTE_TASK_LIFECYCLE_V2 and REMOTE_TASK_TURN_CONTEXT, which default off, and must be fixed before either flag is turned on. The real voice ack_first witness is also still owed there. (Mark's coordinator agent, on Mark's go.)

@marklysze
marklysze merged commit 5930a5d into sonichi:main Nov 1, 2026
77 of 79 checks passed
@keweichen

Copy link
Copy Markdown
Collaborator

Implementation proposal for the four blockers on exact head 5c64cae. This is a code plan, not another verdict; the demonstrated schedules and expected outcomes are in my formal review above. I would land this as one lifecycle patch because the reserve owner, wake replay, and ACK recovery all meet in the same attempt transitions.

1. Make every safety-relevant ledger read explicitly tri-state

In packages/ag2-sparrow/ag2_sparrow/task_attempts.py, add validated private readers for top-level maps and run entries. Do not use data.get(...) or {}, because unreadable, absent, falsey-malformed, and pruned are different states.

Keep the disk format unchanged and use the existing UNKNOWN sentinel consistently:

  • broker_names_leases() -> bool | UNKNOWN
  • last_run() -> str | None | UNKNOWN
  • run_published() -> bool | UNKNOWN. A valid retained run with published absent/false is False; a missing/pruned or malformed named run is UNKNOWN.
  • serve_adoptable() -> bool | UNKNOWN
  • revive_finished() -> bool | UNKNOWN
  • owed_view() -> UNKNOWN | tuple[record, None | run_record | UNKNOWN]

For owed_view(), outer UNKNOWN means no safe current snapshot, so publication waits. Inner None means there was no earlier run. Inner UNKNOWN means earlier_run names evidence that retention pruned, omitted, or malformed; the new run may proceed, but _earlier_attempt() must use the neutral RERUN_RULES["unknown"] wording, never not_delivered/posted_in_room.

Route records(), run_of(), run_item(), and wake_seen() through the same validators so [] and other falsey malformed containers cannot become empty state and truthy malformed values cannot raise.

Caller rules in remote_gateway_bridge.py:

  • Transition.result_hold(): UNKNOWN holds and changes nothing; only explicit False may take the legacy path.
  • _ack_recorded(): require broker_names_leases() is True.
  • _decide_held_serve(): UNKNOWN adoptability returns unrecorded and preserves the claim/pending serve.
  • Wake replay: UNKNOWN publication leaves the serve unacked and queues nothing.
  • _serve_run(): UNKNOWN last-run or publication returns None; never bind/allocate from it.
  • _earlier_run_settling(): UNKNOWN means settling.
  • _serve_attempt(): UNKNOWN revive leaves the task unacked; only False continues.
  • _mark_published(): after the task file is actually published, call mark_published() unless the read is explicitly True; the writer still validates ownership.

When an ACK contains an integer execution_generation, set contract.names_lease_generation=true inside the same TaskAttempts _change that records/adopts/switches that ACK. Do not do a read-then-separate contract commit. That makes _ack_recorded() proof of one committed state rather than two independently failing writes.

2. Give every held serve an immutable owner-specific reserve

The task-wide relay-reserves/.json path cannot represent owed G2 and pending G3 simultaneously. Replace new writes with immutable envelopes:

{
  "format": 1,
  "owner": {
    "task": "task-X",
    "candidate_run": "task-X~~2",
    "lease": "L2"
  },
  "sha256": "<canonical-task-digest>",
  "task": {"id": "task-X", "...": "..."}
}

Use an opaque/generated filename derived from the validated owner tuple, under relay-reserves/. Create it durably with no replacement; an existing file is idempotent only if the complete envelope and digest match.

pending_serve should carry the exact reference:

{
  "lease": "L2",
  "run": "task-X~~2",
  "source": "$event",
  "wake_episode": "episode-or-null",
  "at": 1234.5,
  "reserve": {
    "id": "v1-<hex>",
    "run": "task-X~~2",
    "lease": "L2",
    "sha256": "<hex>"
  }
}

On an accepted newer-generation ACK, switch_run() must atomically clear that pending serve and promote the same reference into structured generation debt:

{
  "owed_publish": {
    "run": "task-X~~2",
    "generation": 2,
    "reserve": {
      "id": "v1-<hex>",
      "run": "task-X~~2",
      "lease": "L2",
      "sha256": "<hex>"
    }
  }
}

The required ordering is:

  1. Allocate the candidate run.
  2. Durably create/fsync the immutable envelope.
  3. Commit pending_serve with that exact reference.
  4. On ACK, atomically promote the reference to generation-stamped owed_publish while binding the generation.
  5. Publish only from owed_publish.reserve.
  6. Compare-and-clear the complete debt after the task is durably queued.
  7. Delete only that reserve after the clear commits.

A ledger failure after step 2 leaves an orphan; it must not overwrite the old owner. A crash after steps 3/4 restarts from the exact reference. A queue success before debt clear retries idempotently against the same run.

Specific transition changes:

  • _hold_reserve() validates/recreates the exact envelope even for the same held lease; it must not return durable merely because the lease matches.
  • adopt_serve() and switch_run() perform pending-clear plus any publication-debt creation in one TaskAttempts change. Remove the current multi-commit gap.
  • _decide_held_serve() reads context only from the claimed pending reference. A transient OSError is UNKNOWN/unrecorded. Confirmed missing, malformed, digest-mismatched, or owner-mismatched bytes can fail that owner as dispatch_lost, but can never supply context or payload to another generation.
  • _publish_owed_run() reads only the debt reference, validates task/run/lease/digest and generation, and revalidates the exact debt before queueing/clearing.
  • Refusal, adoption, abandonment, and stale cleanup capture the exact reference in the successful ledger transition, then delete only that file. No _drop_reserve(wire) remains.
  • Startup GC deletes only unreferenced generated envelopes after one fully readable ledger snapshot. If the ledger is unreadable, delete nothing. Unknown/unreadable reserve files are retained and logged.

Keep TaskAttempts FORMAT=3 and dual-read existing state. Normalize legacy string owed_publish and pending_serve-without-reserve to the legacy task-wide file. Before consuming legacy bytes, prove ownership from their id and lease_id. If owed and pending both name the legacy file, only the matching owner may consume it; the other generation fails its own dispatch instead of cross-publishing, and the file is not unlinked while either reference remains. New writes always use v1 envelopes. A downgrade may conservatively hold the structured debt, but must never publish it through the old shared path.

3. Route same-episode wake re-serves through that held-generation state machine

In _serve_attempt(), after open_wake() identifies a replay of the active blocker_episode_id, compare leases before the current “already left the queue” replay suppression and before remember_task():

  • Same episode + same lease: preserve the existing idempotent path.
  • Same episode + different lease: call _hold_reserve() and return held. Do not overwrite the active run lease with remember_task().
  • Different episode: preserve the current new-wake/new-run path.

Normalize pending_serve.wake_episode to str | None. hold_serve() receives the episode string, not the entire wake dict; serve_adoptable() compares that string to rec.wake_episode. For compatibility, read a legacy string pending_serve.wake as an alias; a legacy dict is non-adoptable and takes the conservative switch/retry path.

Then use the ordinary held-serve decision:

  • Same generation adopts the serve, rotates the lease onto the existing wake run, and never re-executes.
  • New generation reconciles/stops the earlier run and atomically selects the candidate plus its reserve debt.
  • UNKNOWN adoptability retries without a decision.

switch_run() must copy pending_serve.wake_episode instead of forcing wake_episode=None. _publish_owed_run() passes it to _write_task(), so the new run receives the saved wake answer, wake_episode header/rule, rerun rule, its new execution generation, and its own distinct outbox item.

4. Make an open ACK claim sufficient for recovery

Sidecars may remain the pre-claim/downgrade index, but once a TaskAttempts ACK claim exists, the claim is authoritative.

Extend new ACK claims with non-identity retry data:

{
  "retry_owner": "pending_ack",
  "local_tid": "<local-run-id>",
  "durable": true
}

Use retry_owner=ack_first for the held-manifest path. The claim serve remains the exact lease, and claim op remains the broker idempotency key.

Split first-send from resend:

  • _post_task_ack() obtains/creates the claim and delegates to a helper that posts an already claimed ACK.
  • That helper builds the request from claim.serve plus claim data and uses claim.op directly. Immediately before sending, verify that exact claim is still open; do not create a replacement operation.
  • Add a validated TaskAttempts open_claims("ack") one-snapshot reader.
  • _retry_pending_acks() enumerates ordinary open claims first, retries them even if either sidecar is absent, and deduplicates matching sidecar entries. It processes remaining sidecar-only entries afterward.
  • _recover_claims() must actually drive this recovery; logging alone is insufficient.
  • ack_first claims remain owned by _advance_held()/_post_ack_first(), not the ordinary retry loop.

Make sidecar reads tri-state:

  • FileNotFoundError = known empty.
  • Wrong shape, JSON error, or OSError = UNKNOWN.
  • _record_pending_acks() writes neither file if either required read is UNKNOWN.
  • _forget_pending_ack() writes nothing on UNKNOWN.
  • Missing lease metadata never becomes lease=None. A sidecar-only entry is preserved until its lease can be proved; a claim uses its authoritative serve.

For pre-change claims without retry data, use a matching readable held manifest to classify ack_first; otherwise treat it as ordinary. Derive local_tid from the claim/current or pending run only when unambiguous. Take durable from a matching sidecar only when its lease agrees; otherwise omit the durability claim. If the held manifest needed to classify the claim is unreadable, defer rather than guess. Keep lifecycle-v2-disabled behavior sidecar-only.

On success, commit/release the ledger claim first, then clean sidecars best-effort. Network ambiguity or a local generation-commit failure keeps the same claim/op. Terminal fencing/not-leased follows the existing settlement transition and must not depend on sidecar survival.

Discriminating tests to add

Please put policy tests in tests/sparrow-task-attempts.test.py and production-path schedules in tests/gateway-agent-activity-hooks.test.py:

  1. Table-driven absent, one-read EACCES, falsey malformed, and truthy malformed contract/seqs/runs/run-entry cases; assert return values by identity and no mutation.
  2. Retention beyond RUNS_KEPT: a retained delivered prior run says answered; a pruned named prior run uses exactly the neutral unknown rerun rule.
  3. G2 owed while L3 is held, live and after restart: G2 queues only L2 bytes; L3 and its file survive; its ACK later queues only L3 bytes.
  4. The same schedule with L3 fenced 409, bare 404, reserve EACCES, missing/corrupt/mismatched reserve, pointer-commit failure, debt-clear failure, and a paused old-delete/new-hold race.
  5. Same held lease with a missing reserve repairs it before reporting durable and refuses changed bytes.
  6. Legacy pending-only, owed-only, and shared owed+pending reserve states drain without cross-publication; GC is a no-op on unreadable ledger.
  7. Wake L1/gen1 then same episode L2/gen1: one run, lease rotates, no owed publication. Repeat with L2/gen2: one new run, one publication, preserved wake metadata/rules, distinct item. Cover restart between hold and ACK.
  8. Delete/corrupt each ACK sidecar after an ambiguous POST: restart resends from the open claim with the same op and lease, then clears awaiting_ack/claim.
  9. Deny one sidecar read while recording ACK2: ACK1 remains recoverable and ACK2 is not allowed to overwrite it.
  10. Claim + matching sidecar posts once; missing lease sidecar never posts lease=None; ack_first is retried only through the held-manifest owner.
  11. Healthy controls for every injected schedule and lifecycle-off/legacy-state controls.

Please show that the new tests are collected, fail on 5c64cae for the intended assertion, and pass at the proposed head. Afterward I will rerun the exact-head audit, including the post-restart lifecycle round trip and the outstanding real voice ack_first witness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gateway lifecycle: a newer generation served after the earlier run ended reuses the task's own outbox item

4 participants