Skip to content

fix(runtime-api): reconcile expired human-request mirrors - #5742

Merged
keweichen merged 2 commits into
mainfrom
factory/kc/recovery-71ad2538517fa594dc893e98-44df5c0ff2b8
Oct 20, 2026
Merged

keweichen merged 2 commits into
mainfrom
factory/kc/recovery-71ad2538517fa594dc893e98-44df5c0ff2b8

Conversation

@keweichen

Copy link
Copy Markdown
Collaborator

Priority

Level: medium
Reason: Owner-approved reliability improvement.

Closes

N/A — owner-approved work; no public issue is claimed as closed.

Summary

fix(runtime-api): reconcile expired human-request mirrors

Review first: src/runtime-api/dispatcher.py, src/runtime-api/ha_adapter.py, src/runtime-api/request_store.py, tests/runtime-api-dispatcher.test.py, tests/runtime-api-human-action.test.py, tests/runtime-api-request-expiry.test.py, tests/runtime-api-store.test.py.

Checklist

  • Confirmed no other open PR closes the same issue (gh pr list --repo sonichi/sutando --search "closes #N")
  • Git author email matches my CLA-signed email (git log -1 --format='%ae' shows a GH-mapped email, not *.local or noreply@anthropic.com)
  • Single concern per PR — no bundled refactors / drive-by feature additions
  • Confirmed bug exists on upstream/main (or feature isn't already covered)
  • Test added (or N/A explained below)
  • Before/after evidence pasted below — the actual command output for both states, not a description of it (this is the Add demo video to README #1 reason PRs bounce)
  • Every claim in this PR body is verifiable from the diff or the pasted output — no statement the reviewer can't check
  • Live path? If this touches a bridge, network path, delivery loop, or startup, I included a real post-restart round-trip — unit tests / harnesses alone are not accepted for these
  • Stacked PR? I named the parent + merge order and will rebase/update + rerun full checks after the parent lands (or N/A)
  • Scanned added lines for host-specific hardcoded paths and inline home/workspace fallbacks; fixtures are narrowly scoped
  • If this PR adds or edits a CI workflow, I confirmed it actually runs on this PR (see the Checks tab — a branches: filter can silently exclude a stacked PR)
  • Doesn't reinvent workspace / home-path resolution — uses the resolve_workspace / resolveWorkspace / claude_home_path helpers (see CLAUDE.md "Workspace contract"; enforced by the lint-workspace-resolution, lint-claude-home-path, and workspace-leak-check CI checks)

Contribution observations (unchecked items remain reviewer decisions):

  • Duplicate search: Searched PRs (all states) and open issues using fix(runtime-api): reconcile expired human-request mirrors, "request.cancel". Related results for reviewer comparison: fix(runtime-api): cancel mirrored human requirements #5741 Returned PRs were compared against current files and overlapping diffs: fix(runtime-api): cancel mirrored human requirements #5741: Related files, different patch; current-source reproduction and independent review remain required.. Title similarity alone is not equivalence; this is not exhaustive semantic coverage.

  • Upstream observed during evidence preparation: 0a6e182b94ebffd22c55b038c5ce8c08291b1a17. Main advanced after the recorded source base. Local evidence verifies the exact PR candidate, not its integration with newer main. PR checks, independent review and branch maintenance govern merge.

  • Diff review: - review-correctness: Inspected changed logic and its recovery, resolver, store, reply-handler and projector callers. Expired rows remain retryable; identity checks and expiry run under the reentrant store lock. Existing terminal outcomes, answers and projection receipts are preserved. Transient mirror-write failure and subsequent successful retry were exercised.

  • review-scope: HEAD and parent match the supplied candidate and approved base. The reconstructed patch SHA-256 is 7ed4d82b74d46b950ce8bce0a4da3403e16945567042084f4f3acb0856414d9a, matching supplied evidence. Exactly seven allowed paths changed. Excluded cancellation, deadline, capability, schema, transport and desktop behavior remains unchanged. Git status stayed clean; git diff --check passed.

  • review-maintainability: The change keeps coordination in dispatcher.py, mirror ownership validation in ha_adapter.py and filtered lookup in request_store.py. It reuses HitlManager.expire and existing atomic persistence without introducing another lifecycle writer or reconciliation schema. Tests cover failure isolation, ownership guards and idempotence.

  • Documentation: No standalone documentation files changed. The reviewer should assess the code and contract comments for documentation completeness.

  • Live-path verification: - monitor-user-journey: Ran python3 [local path] exit 0, during 2026-10-19 22:15:24–22:15:51 UTC. It launched unchanged python3 src/runtime-api/server.py and exercised bash bin/sutando over private Unix sockets with the supplied environment. For example, approval request --task-id task-rtapi-ed208d880e91 --action 'Local synthetic approval' --expires-in 6 returned approval-41ea4cf9abc2 pending; request get approval-41ea4cf9abc2 later returned expired. Equivalent elicitation and human-action journeys passed. Raw persisted records confirmed expiry before CLI reads. Full commands, outputs, timestamps and readiness are in monitor-journey-events.json.

  • monitor-repeatability: The independent witness stopped the daemon, crossed six request deadlines, and restarted python3 src/runtime-api/server.py against the same stores. Three pending runtime rows and three already-expired rows with active mirrors reconciled successfully. bash bin/sutando request get elicitation-9f0936a8f3ec returned status expired. A second restart preserved all recorded identities, revisions, deadlines, answers and projection receipts. Repeated production project() calls returned []; nine expiry edits produced zero duplicates. Commands and observations are in monitor-journey-events.json and monitor-journey-summary.json.

These observations used the isolated candidate; no production deployment was performed.

  • Risks / rollback: Change scope: Reconcile expired approval, elicitation and human-action runtime requests with their matching nonterminal HumanRequirements in the request-domain coordinator. Reuse the existing adapter and HitlManager expiry operation, with narrowly scoped store-query support if needed. Ensure periodic processing and restart can retry incomplete mirror transitions. Preserve request identities, deadlines, terminal outcomes, answers and projection receipts. Exclude request.cancel changes, generic HITL expiry policy, deadline redesign, capability execution, schema migrations, historical answer repair, transport and desktop changes.
    Rollback: revert this PR. Production behavior after deployment remains to be observed.
  • Stacking: N/A; one concern against main.
  • Author maps to keweichen; CLA status must be observed on the PR.

Before / after evidence

Before

  • triage-evidence: HEAD matched 8e6ebd5; initial and final Git status were clean. Loaded [local path] for every product subprocess. process-tools.py preflight confirmed private sockets and scoped process inspection. Ran python3 [local path] during 2026-10-19 21:52:26–21:52:47 UTC. Actual daemon readiness took 0.615s and 0.263s after restart; both processes exited 0. Commands, timestamps, persisted observations and synthetic projection payloads are recorded in diagnostics/triage-witness.json; startup logs and capability-preflight.json are in the same private diagnostics directory.

Recorded command/output on the pinned source:

/bin/zsh -lc "python3 - <<'PY'
import json, os, subprocess
p='[local path]'
env=os.environ.copy(); env.update(json.load(open(p+'/environment.json')))
r=subprocess.run(['python3',p+'/process-tools.py','preflight'],env=env,text=True,capture_output=True)
print(r.stdout); print(r.stderr); print('exit=',r.returncode)
PY"
{"schema": "ag2-runtime-preflight/v1", "product_ready": "not observed", "tmux": {"available": true, "detail": "tmux 3.8\n"}, "process_inspection": {"available": true, "detail": "66623\n"}, "private_socket": {"available": true}}


exit= 0

Exit: 0

/bin/zsh -lc "python3 - <<'PY'
import json
p='[local path]'
e=json.load(open(p)); print(json.dumps(e, indent=2))
PY
rg -n 'transport vs|request domain|HTTP transport|HTTP route|Shared adapter policy|Placement checklist' docs/architecture-boundaries.md"
{
  "TMPDIR": "[local path]",
  "TMP": "[local path]",
  "TEMP": "[local path]",
  "SUTANDO_TEST_MODE": "1",
  "SUTANDO_PACKAGED": "",
  "SUTANDO_APP_SUPPORT": "",
  "SUTANDO_WORKSPACE": "[local path]",
  "SUTANDO_WORKSPACE_DIR": "[local path]",
  "SUTANDO_DEFAULT_WORKSPACE": "[local path]",
  "SUTANDO_RESULTS_DIR": "[local path]",
  "SUTANDO_RUNTIME_STATE": "[local path]",
  "SUTANDO_RUNTIME_SOCKET": "[local path]",
  "SUTANDO_RUNTIME_DB": "[local path]",
  "SUTANDO_HA_DIR": "[local path]",
  "SUTANDO_RUN_DIR": "[local path]",
  "SUTANDO_INSTANCE_REGISTRY": "[local path]",
  "SUTANDO_AGENT_ID": "factory-runtime-test",
  "SUTANDO_HOST_LABEL": "factory-runtime-test",
  "SUTANDO_INSTANCE_ID": "",
  "SUTANDO_WATCHER_BEAT": "",
  "SUTANDO_WATCHER_TRANSITION_HOOK": "",
  "CLAUDE_CONFIG_DIR": "[local path]",
  "SUTANDO_TMUX_SOCKET": "[local path]",
  "SUTANDO_STANDBY_STOP_TIMEOUT": "2",
  "PATH": "[local path]",
  "PYTHONPATH": "[local path]"
}
117:### Shared adapter policy
169:### HTTP transport handlers
178:### HTTP route boundaries
180:HTTP route methods should remain dispatch layers: parse and authorize the request,
286:## Transport vs request domain
361:placement checklist in "Placement checklist and boundary rules" below remains
381:## Placement checklist and boundary rules
399:**Transport does not own authorization or durable state.** `src/runtime-api/server.py` is transport + daemon composition only; dispatch, approval/authorization policy, idempotency and durable transitions live in `src/runtime-api/dispatcher.py`, with actor identity resolved daemon-side — never from a client parameter. → "Transport vs request domain"
446:## Events plane: resident transport vs on-demand capability (2026-08-05)

Exit: 0

After

  • acceptance-1: The added journey test loaded the supplied environment into product subprocesses and exercised unchanged server.py and bin/sutando. At the default two-second resolver interval, approval, elicitation and human-action requests and matching requirements expired within 1.874 seconds after their deadlines. Requirements left active(); tasks stopped reporting waiting.

  • acceptance-2: Production projector with synthetic local delivery generated exactly one expired edit per requirement against its original target. Repeated projection passes emitted nothing further. Previous-revision owner actions were rejected without changing expired outcomes. Independently checked recorded payloads and replies.

  • acceptance-3: Real daemon restart reconciled all three types after downtime and already-expired runtime rows with active mirrors. Identities, deadlines and projection receipts survived; another restart added no revisions. The focused dispatcher test injected a transient HitlStore.save failure and verified isolation, periodic retry and idempotence.

  • acceptance-4: Unrelated pending requests, requests without explicit deadlines before adapter TTL, and previously resolved requests remained unchanged. Selected answer Beta remained retrievable after restart. Non-owner and stale replies were rejected. Task/result payloads and populated second-workspace state were preserved.

  • check-request-expiry-journey: python3 tests/runtime-api-request-expiry.test.py: exit 0; two journeys passed in 32.954 seconds, 2026-10-19 22:07:29–22:08:02 UTC.

  • check-request-domain-regressions: python3 tests/runtime-api-dispatcher.test.py: exit 0 in 1.610 seconds; expiry recovery, transient-write retry and repeated-success checks passed.

  • check-human-action-regressions: python3 tests/runtime-api-human-action.test.py: exit 0 in 0.287 seconds; all 16 tests passed.

  • check-request-store-regressions: python3 tests/runtime-api-store.test.py: exit 0 in 0.288 seconds; durable retry filtering and preservation checks passed.

  • coverage: Reran all four suites with python3 -m coverage run --rcfile=.coveragerc. diff-cover against 8e6ebd5 with --fail-under=95 measured 21/21 changed executable lines covered: 100%, zero missing. Commands and measurements: [local path]

  • runtime-evidence: All ten daemon starts across ordinary and coverage runs reached request.list readiness in 0.285–0.432 seconds and were reaped with exit 0. Commands, PIDs, timestamps and startup logs are recorded in [local path] independently checked journey receipts are in verify-runtime-summary.json.

  • scope: HEAD matched the pinned candidate before and after verification; tracked/untracked status remained clean. Exactly the seven allowed paths differ from the approved base. Entry points and coverage configuration are unchanged; git diff --check passed.

Test plan

python3 tests/runtime-api-request-expiry.test.py

Observed: PASS; exit 0.

test_periodic_expiry_projection_and_negative_controls (__main__.ExpiryJourneyTests.test_periodic_expiry_projection_and_negative_controls) ... observations: [local path]
ok
test_restart_recovers_downtime_and_already_expired_rows (__main__.ExpiryJourneyTests.test_restart_recovers_downtime_and_already_expired_rows) ... observations: [local path]
ok

----------------------------------------------------------------------
Ran 2 tests in 33.031s

OK
python3 tests/runtime-api-dispatcher.test.py

Observed: PASS; exit 0.

── dispatch table ──
  ok   unknown method → -32601
  ok   ungranted human_action.complete → -32601 before any param check
  ok   granted human_action.complete still requires requestId
  ok   approval.request requires action
  ok   elicitation.request requires question
  ok   approval.request returns pending + requestId
  ok   issued row is durable and pending
  ok   actor persisted from CONSTRUCTOR, not params
  ok   a client-supplied actor cannot override the daemon actor
── elicitation types ──
  ok   free_text rejected in v0
  ok   unknown type rejected
  ok   single_select requires options
  ok   multi_select requires options
  ok   valid single_select issues
  ok   confirmation needs no options
── human-action mirror failure ──
  ok   mirror failure raises -32603
  ok   a failed mirror leaves NO pending row behind
── get / wait / cancel ──
  ok   get unknown requestId → -32602
  ok   cancel u
[Excerpt: middle omitted; full recorded output SHA256 e5d20240b987f34d35c8f583b70c19d2e49d113be5b2109d5dcbb5c7db571865]
not die on first error)
── crash recovery ──
[runtime-api] recovered 2 pending request(s)
  ok   recovery relinks a pending approval to its card
  ok   recovery relinks a pending elicitation to its card
  ok   recovered approval is still pending
── expired mirror reconciliation and retry ──
  ok   settle expires the mirror when its read expires the runtime row
[runtime-api] requirement expiry error for elicitation-c4f80d546ee1 (isolated): synthetic transient mirror write failure
  ok   recovery isolates a transient mirror-write failure
  ok   one failed expiry does not block another mirror
  ok   periodic reconciliation retries an already-expired runtime row
  ok   repeated success adds no revisions or runtime transitions
  ok   expiry preserves original projection receipts
  ok   expired mirrors leave active()
── approval.respond (device-plane; needs a daemon-resolved grant) ──
  ok   respond WITHOUT a grant is not callable (fails closed)
  ok   ungranted respond leaves the approval pending
  ok   respond requires requestId
  ok   respond requires a valid decision
  ok   respond on unknown request
  ok   respond approves and reports the transition
  ok   approved is durable
  ok   second respond reports alreadyTerminal, never flips the row
  ok   respond refuses a non-approval request type
  ok   respond reject denies
── schedule / task param edges ──
  ok   schedule.list without a schedules surface
  ok   task.* without a task pipeline is a clean -32601
  ok   request.get without requestId
── configured-surface branches ──
  ok   schedule.list serves configured rows through the dispatcher
  ok   task.status with a pipeline but no taskId
  ok   task.details unknown id
  ok   task.submit empty text is a ValueError -> -32602
  ok   request.wait without requestId

PASS — runtime-api dispatcher
python3 tests/runtime-api-human-action.test.py

Observed: PASS; exit 0.

test_a_duplicate_answer_is_swallowed_not_raised (__main__.AdapterEdgeTests.test_a_duplicate_answer_is_swallowed_not_raised) ... ok
test_an_expired_requirement_reports_expired_on_every_poll (__main__.AdapterEdgeTests.test_an_expired_requirement_reports_expired_on_every_poll) ... ok
test_answer_shape_fallbacks (__main__.AdapterEdgeTests.test_answer_shape_fallbacks) ... ok
test_expire_delegates_and_preserves_identity_answer_and_receipt (__main__.AdapterEdgeTests.test_expire_delegates_and_preserves_identity_answer_and_receipt) ... ok
test_expire_never_creates_or_changes_foreign_or_terminal_requirements (__main__.AdapterEdgeTests.test_expire_never_creates_or_changes_foreign_or_terminal_requirements) ... ok
test_free_text_decision_round_trips_the_typed_answer (__main__.AdapterEdgeTests.test_free_text_decision_round_trips_the_typed_answer) ... ok
test_resolve_refuses_an_unknown_requirement_and_a_bad_index (__main__.AdapterEdgeTests.test_resolve_refuses_an_unknown_requirement_and_a_bad_index) ... ok
test_api_complete_resolves_request_and_closes_card (__main__.HumanActionTests.test_api_complete_resolves_request_and_closes_card) ... ok
test_api_decline_and_terminal_is_idempotent_safe (__main__.HumanActionTests.test_api_decline_and_terminal_is_idempotent_safe) ... ok
test_card_done_completes_and_decline_declines (__main__.HumanActionTests.test_card_done_completes_and_decline_declines) ... ok
test_complete_rejects_wrong_type_and_unknown (__main__.HumanActionTests.test_complete_rejects_wrong_type_and_unknown) ... ok
test_request_mirrors_card_with_act_and_outcome_options (__main__.HumanActionTests.test_request_mirrors_card_with_act_and_outcome_options) ... ok
test_status_returns_request_state (__main__.HumanActionTests.test_status_returns_request_state) ... ok
test_ungranted_caller_cannot_settle_and_leaves_the_row_pending (__main__.HumanActionTests.test_ungranted_caller_cannot_settle_and_leaves_the_row_pending) ... ok
test_ungranted_unix_client_cannot_settle_its_own_request (__main__.ProductionComposedSettleTests.test_ungranted_unix_client_cannot_settle_its_own_request) ... ok
test_unix_transport_grants_nothing (__main__.ProductionComposedSettleTests.test_unix_transport_grants_nothing) ... ok

----------------------------------------------------------------------
Ran 16 tests in 0.142s

OK
python3 tests/runtime-api-store.test.py

Observed: PASS; exit 0.

ok  create → get roundtrip (pending, params, no result)
  ok  get unknown id → None
  ok  pending() lists the open request
  ok  pending → approved transition succeeds
  ok  second transition loses the CAS (terminal immutable)
  ok  terminal state + resolver recorded
  ok  approved consumes once
  ok  second consume refused
  ok  consume unknown id refused
  ok  pending request cannot be consumed
  ok  past-deadline pending reads as expired (lazy, durable)
  ok  expiry is persisted (second read agrees)
  ok  result payload persists on transition
  ok  by_idempotency_key returns the request with its fingerprint
  ok  unknown idempotency key → None
  ok  create_consuming: record created
  ok  create_consuming: approval consumed in the same commit
  ok  already-consumed approval → create_consuming returns None
  ok  lost consume race rolls back the insert (no orphan row, key unspent)
  ok  duplicate idempotency key raises IntegrityError
  ok  same-key/different-approval race leaves the 2nd approval unconsumed
  ok  raced-but-unspent approval remains consumable afterwards
  ok  expiry retry query includes only expired human requests
  ok  pending() stays restricted to pending rows
  ok  expiry retry query preserves all stored fields
  ok  expiry retry candidates survive reopen
  ok  pre-idempotency DB migrates: old rows readable
  ok  migrated DB: idempotency index usable
  ok  migrated DB: new columns writable
  ok  re-boot over migrated DB is clean

PASS — request store unit suite green

Canonical review scanner:

review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean)
prose-cap: PASS (comment blocks within cap 2, exts .py)

Verified candidate: 9fa42a7b55487802e194895527c90fb0c489f30b. Publication commit: 29b7e44dab603bc81daaa7f419057c87028bb0dc. Both have tree 0a056947810747162949f35b28ebe2107fe99ecd; only factory-authored commit metadata changed. Acceptance evidence refers to the verified candidate; the scanner ran on the publication tree.

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

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

Diff Coverage

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

  • src/runtime-api/dispatcher.py (100%)
  • src/runtime-api/ha_adapter.py (100%)
  • src/runtime-api/request_store.py (100%)

Summary

  • Total: 25 lines
  • Missing: 0 lines
  • Coverage: 100%

@qingyun-wu qingyun-wu 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.

Code-owner review at 29b7e44. The fix is right: store.pending() lazily flips an elapsed row to 'expired', so the old _settle (gated on 'pending') never expired the HA mirror. Targeting the mirror by its deterministic ha_action_id, with ha_adapter.expire() checking subject and guard ownership first, closes the dangling card even for rows that expired before a restart.

Before approval:

  1. Duplicated policy (blocking per CLAUDE.md "fix where the policy lives"). This branch predates #5741, so _settle hardcodes ('approval','elicitation','human_action'). #5741 introduces MIRRORED_REQUEST_TYPES for exactly this set. Please rebase on #5741, or on main once it lands, and use the constant. The two PRs also conflict in dispatcher.py near recover() and _settle.
  2. Flaky test. test_restart_recovers_downtime_and_already_expired_rows failed 2 of 4 local runs ('expired' != 'pending'). Its 8 s TTL margin races the live daemon's 2 s resolver poll across about 8 sequential CLI fixture calls. Widen the margin or inject the clock.

Non-blocking: expired_human_requests() has no bound or high-water mark, so every historically expired mirrored row is rescanned every 2 s for the daemon's lifetime.

@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@keweichen

Copy link
Copy Markdown
Collaborator Author

Addressed review feedback in 3155aa11d3ee439bf2285bb1c19556a121556228.

  • acceptance-1: Unchanged server.py and bin/sutando inherited the supplied environment and used private Unix sockets. All three request types and matching requirements expired within 1.50 seconds after deadline, left active(), and cleared task waiting states within 1.60 seconds under the default resolver interval.

  • acceptance-2: Production projection with synthetic local delivery created pending projections, then exactly one expired edit per requirement against its original target. Repeated passes emitted no unchanged edits. Previous-revision owner actions were rejected without altering expired outcomes.

  • acceptance-3: Downtime-expired requests and already-expired rows with active mirrors reconciled within 0.25 seconds of restart. Identities, deadlines and projection receipts survived repeated restart. The production-writer failure test proved retry, failure isolation and revision idempotence.

  • acceptance-4: Negative controls preserved unrelated pending requests, requests without explicit deadlines before adapter TTL, and resolved outcomes across restart. The selected Beta answer remained retrievable. Non-owner and stale replies were rejected; task/result payloads and another workspace's state remained unchanged.

Regression checks:

  • request-expiry-journey: PASS
  • request-domain-regressions: PASS
  • human-action-regressions: PASS
  • request-store-regressions: PASS
/bin/zsh -c "python3 - <<'PY'
import datetime,hashlib,json,os,pathlib,subprocess,time
root=pathlib.Path('[local path]'); diag=root/'diagnostics'
env=os.environ.copy(); env.update(json.loads((root/'environment.json').read_text())); env.update(PYTHONDONTWRITEBYTECODE='1',SUTANDO_TELEMETRY='0')
argv=['python3','tests/runtime-api-request-cancellation.test.py']; t=time.monotonic(); started=datetime.datetime.now(datetime.timezone.utc).isoformat()
p=subprocess.run(argv,env=env,capture_output=True,text=True,timeout=60)
path=diag/'upstream-cancellation.log'; path.write_text(p.stdout+p.stderr)
receipt={'argv':argv,'started_at':started,'ended_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'elapsed_seconds':time.monotonic()-t,'exit_code':p.returncode,'log':str(path),'sha256':hashlib.sha256(path.read_bytes()).hexdigest()}
(diag/'upstream-cancellation.json').write_text(json.dumps(receipt,indent=2))
print(json.dumps(receipt)); print((p.stdout+p.stderr)[-2000:])
PY"
{"argv": ["python3", "tests/runtime-api-request-cancellation.test.py"], "started_at": "2026-10-20T04:25:00.904338+00:00", "ended_at": "2026-10-20T04:25:09.172651+00:00", "elapsed_seconds": 8.268414166057482, "exit_code": 0, "log": "[local path]", "sha256": "3f8d08dbd9009a71db2452fed1cbb9f14ad26cf0cd4f3768f56e3c9b5944933d"}
9b651269a01c8d/verify-19-6/bin/sutando", "request", "cancel", "unknown-fixture"], "exit_code": 1, "stdout": "", "stderr": "{\"error\": \"-32602: unknown requestId: 'unknown-fixture'\"}\n"}
{"event": "cli", "at": "2026-10-20T04:25:09.045831+00:00", "argv": ["./bin/sutando", "request", "cancel", "approval-fa3ca93bf3d2"], "exit_code": 0, "stdout": "{\n \"requestId\": \"approval-fa3ca93bf3d2\",\n \"status\": \"cancelled\",\n \"resolvedBy\": \"synthetic-cancellation-test\"\n}\n", "stderr": ""}
{"event": "cli", "at": "2026-10-20T04:25:09.137425+00:00", "argv": ["./bin/sutando", "request", "cancel", "capability-ba35ee305600"], "exit_code": 0, "stdout": "{\n \"requestId\": \"capability-ba35ee305600\",\n \"status\": \"cancelled\",\n \"resolvedBy\": \"synthetic-cancellation-test\"\n}\n", "stderr": ""}
{"event": "daemon-stop", "at": "2026-10-20T04:25:09.161837+00:00", "pid": 41319, "exit_code": 0, "reaped": true, "log": "[runtime-api] listening on [local path] (actor=synthetic-cancellation-test)\n"}
test_all_types_cancel_stale_actions_project_and_survive_restart (__main__.RequestCancellationTests.test_all_types_cancel_stale_actions_project_and_survive_restart) ... ok
test_failed_mirror_write_is_reported_and_retry_repairs_after_restart (__main__.RequestCancellationTests.test_failed_mirror_write_is_reported_and_retry_repairs_after_restart) ... ok
test_retry_preserves_a_mirror_answer_that_won_before_reconciliation (__main__.RequestCancellationTests.test_retry_preserves_a_mirror_answer_that_won_before_reconciliation) ... ok
test_unrelated_terminal_unknown_and_unmirrored_controls (__main__.RequestCancellationTests.test_unrelated_terminal_unknown_and_unmirrored_controls) ... ok

----------------------------------------------------------------------
Ran 4 tests in 8.191s

OK

Exit: 0

/bin/zsh -c "python3 - <<'PY'
import datetime,json,os,pathlib,subprocess,time
root=pathlib.Path('[local path]'); diag=root/'diagnostics'
env=os.environ.copy(); env.update(json.loads((root/'environment.json').read_text())); env.update(PYTHONDONTWRITEBYTECODE='1',SUTANDO_TELEMETRY='0')
code='''import importlib.util,time,unittest
from pathlib import Path
path=Path.cwd()/\"tests/runtime-api-request-expiry.test.py\"
spec=importlib.util.spec_from_file_location(\"expiry_journey\",path)
module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
class DelayedExpiryJourney(module.ExpiryJourneyTests):
    def issue(self,*args,**kwargs):
        record=super().issue(*args,**kwargs)
        time.sleep(2)
        self.note(\"synthetic-fixture-delay\",seconds=2)
        return record
suite=unittest.TestSuite([DelayedExpiryJourney(\"test_restart_recovers_downtime_and_already_expired_rows\")])
result=unittest.TextTestRunner(verbosity=2).run(suite)
raise SystemExit(not result.wasSuccessful())
'''
argv=['python3','-c',code]; t=time.monotonic(); started=datetime.datetime.now(datetime.timezone.utc).isoformat()
p=subprocess.run(argv,env=env,capture_output=True,text=True,timeout=90)
path=diag/'restart-fixture-delay.log'; path.write_text(p.stdout+p.stderr)
receipt={'argv':argv,'started_at':started,'ended_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'elapsed_seconds':time.monotonic()-t,'exit_code':p.returncode,'log':str(path)}
(diag/'restart-fixture-delay.json').write_text(json.dumps(receipt,indent=2))
print(json.dumps(receipt)); print(p.stdout+p.stderr)
PY"
{"argv": ["python3", "-c", "import importlib.util,time,unittest\nfrom pathlib import Path\npath=Path.cwd()/\"tests/runtime-api-request-expiry.test.py\"\nspec=importlib.util.spec_from_file_location(\"expiry_journey\",path)\nmodule=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)\nclass DelayedExpiryJourney(module.ExpiryJourneyTests):\n    def issue(self,*args,**kwargs):\n        record=super().issue(*args,**kwargs)\n        time.sleep(2)\n        self.note(\"synthetic-fixture-delay\",seconds=2)\n        return record\nsuite=unittest.TestSuite([DelayedExpiryJourney(\"test_restart_recovers_downtime_and_already_expired_rows\")])\nresult=unittest.TextTestRunner(verbosity=2).run(suite)\nraise SystemExit(not result.wasSuccessful())\n"], "started_at": "2026-10-20T04:25:16.831815+00:00", "ended_at": "2026-10-20T04:26:01.648966+00:00", "elapsed_seconds": 44.81763891701121, "exit_code": 0, "log": "[local path]"}
observations: [local path]
test_restart_recovers_downtime_and_already_expired_rows (__main__.DelayedExpiryJourney.test_restart_recovers_downtime_and_already_expired_rows) ... ok

----------------------------------------------------------------------
Ran 1 test in 44.733s

OK

Exit: 0

Independent verification, code review and product checks passed on candidate 523630f9468a438df568f8db836e4ca4196a68f3; the published commit has the same tree 52ad5c3dd57a0de7136a2c89684e16184ac61db4. Please review the new head.

@qingyun-wu qingyun-wu 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.

Code-owner approve at 3155aa1. Both of my blockers from 29b7e44 are resolved:

  1. The branch is rebased onto merged #5741 (3133b05 is an ancestor). MIRRORED_REQUEST_TYPES (dispatcher.py:87) is the single source, used in _reconcile_expired, _settle and recover. No second production copy exists; tasks_view.py:419 is an unrelated display ordering.
  2. test_restart_recovers_downtime_and_already_expired_rows now uses a 30 s TTL with an explicit margin guard, and passed 6 of 6 runs in isolation. Red-first holds: with dispatcher.py, ha_adapter.py and request_store.py reverted to before the fix, it fails with 'expiry did not reconcile: … expired/pending'.
    CI is green and the CLA is signed. Non-blocking follow-up still open: expired() is rescanned without a bound on every 2 s resolver tick.

@keweichen
keweichen merged commit 581b6fc into main Oct 20, 2026
35 checks passed
@keweichen
keweichen deleted the factory/kc/recovery-71ad2538517fa594dc893e98-44df5c0ff2b8 branch October 20, 2026 04:51
@qingyun-wu

Copy link
Copy Markdown
Collaborator

Post-merge review of #5741 (949505d) and #5742 (3155aa1). Both fixes are correct, and each new test fails when its fix lines are removed:

No blockers. These are worth follow-up issues:

  1. A test can write into a live runtime directory. tests/runtime-api-request-expiry.test.py:31-40,65 reads the ambient SUTANDO_RUNTIME_SOCKET, loads the environment.json next to it, and puts its temp dir and diagnostics/ there. Run on a shell with a live socket env, it writes into the live runtime directory. It should always use its own temp dir.
  2. Unbounded rescan. Every resolver pass (2 s) goes dispatcher.py:645 → request_store.py:196, which re-selects every historically expired mirrored row. Then ha_adapter.py:152 takes the cross-process HITL file lock before checking whether the card is already terminal. The cost grows with history on a lock shared with the bridge and hooks. Fix: check the card is terminal before taking the lock, or keep a high-water mark.
  3. A cancelled card can stay pending. If ha.cancel's write fails and the client never retries, the card stays pending. Later clicks are accepted and then ignored by _settle. The store.expired(...) reconcile could cover cancelled rows too.
  4. The adapters disagree on ownership checks. ha_adapter.py:130 cancel() has no ownership guard, but expire() (:149-157) checks subject, runtime and guard.
  5. Edge case: _settle can show expired over an owner click that landed before the resolver ran. The requester outcome was already expired; only the display changed.
  6. Slow tests. The daemon journeys are sleep-based and take 71–150 s. An injected clock would speed them up.

Live witness: each PR body has an isolated server.py + bin/sutando journey with restarts. A round trip on a deployed host after release would close the remaining gap.

— Sutando (Mars seat)

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.

2 participants