Skip to content

feat(native-chat): keep the proof that an agent is gone after the next owner starts - #26667

Closed
brennanb2025 wants to merge 14 commits into
brennanb2025/chat-settle-at-owner-changefrom
brennanb2025/chat-closed-owner-history
Closed

brennanb2025 wants to merge 14 commits into
brennanb2025/chat-settle-at-owner-changefrom
brennanb2025/chat-closed-owner-history

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 16 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1784 $\color{#cf222e}{\Huge{\mathbf{−}}}$​86 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1698
Prod 32 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​723 $\color{#cf222e}{\Huge{\mathbf{−}}}$​181 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​542

ELI5

After an agent dies, its chat can stay stuck on “Working” or an old approval. If saving the result fails, starting the next owner clears the only saved proof of which agent generation died. A restart can then lose the information needed to finish that old work.

This PR changes nothing users see yet. It keeps the proof after the next owner starts. The next PR in this stack will use it to finish old work when ownership changes, fail a start if that result cannot be saved, and retry at the next ownership attempt. Opening history will stay read-only.

What Changed

The execution host now stores one immutable closed-owner fact per session and owner generation in its existing chat database. Each positive exit, proven eviction, failed acquisition with proof, or proven replacement commits the fact with its ownership transition, before mutable evidence is cleared. Facts contain the proof, the old process identity without its spawn token, and the known journal cutoff. They survive replacement and restart.

The store exposes facts oldest first and passes the committed fact to death listeners while preserving current callers' notification behavior. A journal receipt can delete a fact in the same transaction that saves the result making it unnecessary; deleting the session also deletes its facts. This PR does not invoke that receipt from production. Host-only process snapshots and existing fence comparisons reject stale probes without closing a successor; public wire shapes are unchanged.

The host schema advances to version 5 so an older host uses the existing read-only refusal instead of changing owners without maintaining this history. A one-time migration preserves safely attributable old proof; missing identities and historical cutoffs stay unknown.

Why

Keeping the single mutable field loses old proof when the next owner starts. An in-memory copy cannot survive restart and misses replacement's fresh probe. A “cleanup owed” flag would duplicate state without recording which owner actually died. A past ownership fact preserves the cause and generation, and its delete receipt provides a definite end when the journal result commits.

Compared with the common pattern:

  • INTENDED: closure requires positive execution proof because agents can outlive the app and run over SSH. A disconnect or unverified owner remains unverifiable.
  • TEMPORARY: settlement stays at its current placement. The next PR in this stack moves it to ownership changes, uses these facts, and retires them with the resulting journal transaction.

Linked Issue

Maintainer work in the approved lane C stack. No separate issue is linked. This is preparation for the stuck-chat fix, not a claim that this PR alone fixes it.

Visual Proof

N/A — this PR changes host-only ownership persistence and preserves existing UI and ordinary interaction behavior.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated

On macOS, the final explicit-file run passed 42 files and 434 tests with two workers and background launch enabled. New real SQLite tests cover positive ownership transitions and rollback, fresh reservation proof, retention after replacement/restart, no proof on expiry/uncertainty/unproven release, stale fence and same-fence identity protection, receipt commit/rollback, record deletion, migration and newer-schema write refusal. Existing ownership, restart, attachment, journal and stored-record tests passed unchanged apart from the state fixture's new history map.

Oxfmt, changed-file oxlint, and the changed-code quality gate pass. React Doctor reports four existing warnings in dependency-stack files and no new findings. Node, web and cli typechecks pass. No build, live SSH/WSL run or rendered UI check is claimed here; CI covers the full suite. Remote and folder scopes are covered by fixtures.

AI Disclosure

Review

Second PR of a stacked series. The branch also contains #26555 and #26577 (merged in, unchanged) because the follow-up builds on them; their diffs are reviewed in those PRs.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Merge #26555 and #26577 first, then #26664, then this PR. Host schema fencing preserves older-host safety; clients exchange the existing wire shapes. The execution host owns proof and history, including remote and WSL scopes. Folder workspaces require no Git operation. Facts contain no account home, claim key, or spawn token. No retry flag or timer is introduced.

Author: Brennan Benson (@BrennanKB5).

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

brennanb2025 and others added 12 commits October 8, 2026 03:48
…ry other chat

Starting, resuming or switching a chat's agent first ran restart
reconciliation, which probed every unreconciled session and committed them
together, so one slow or failing probe for another chat delayed or failed
this chat's action. Acquisition now reconciles only the addressed session;
the explicit startup pass still covers all of them, and an owner that isn't
proven still can't be granted a writer.

Co-Authored-By: OpenAI Codex gpt-6.1-sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sqlite tests

- Startup restore no longer lets one chat's failed lease check skip recovery for the others.
- Run the two new SQLite-backed tests in the Node runtime project.
- Startup reconcile runs the whole-host pass without a placeholder session id.
- Test names describe an injected failing/stalling owner check, not an SSH host.
The restore opens with the whole-host pass again and stops per-chat bookkeeping after the
first failure, as on main: on a failing store it makes one lease-check write, not one per
chat. User actions stay targeted to their own chat.
…ocks starting a chat

After a restart, starting or resuming a chat first settled the status of
sends from the previous run, and a failed settlement write failed the start.
Settlement is now best effort: unconfirmed sends keep showing as
unconfirmed and keep their replay protection, the failure is logged and
retried later, and the user's start proceeds.

Co-Authored-By: OpenAI Codex gpt-6.1-sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rovably starts before it

A send whose verdict failed to save stays unconfirmed; a later turn can move the
resume point the next restart reads from past it, and that history then reads the
delivered send as never sent. The reconciler now rejects an absent send only when
it was handed over at or after the fence of the owner that last moved that resume
point (derived from the record's chain head and the journal fold); otherwise it
stays unconfirmed. Presence still settles it.

Also: settlement failures are injected at the SQLite transaction in tests and
checked on disk; re-attaching to a running agent keeps it when a settlement fails;
docs on what a failed settlement leaves now say what is true.
…last moved

Round-1 dated the start from the chain head, but every later acquisition re-proves an
unmoved resume point as a new link, so one undecided attach left a truly undelivered send
unconfirmed forever. The start is now the first of the trailing links that carry the head's
handle, leaf included.

The same owner can also move the point past a send it handed over (a turn end after an
unrecorded accept), so at an equal fence the reconciler now rejects only when the point
moved strictly before the handover on that owner's clock; a tie stays unconfirmed.

attachJournal no longer samples history itself (only tests did, from the post-acquisition
record); callers pass the sampled window.
… and time together

A send handed over twice (legacy rows) paired its first handover's fence with its last
handover's time. Both now come from one derived firstHandover. Also drop a writer comment
about a pending retry path that no longer exists.
…-its-own' into brennanb2025/chat-closed-owner-history
…tlement-best-effort' into brennanb2025/chat-closed-owner-history
…t owner starts

The proof that an agent generation died lived in one mutable lease field
that the next reservation cleared, so after a failed cleanup or a restart
nothing durable said which generation was gone. Record each proven ending
as an immutable closed-owner fact in the owner store, in the same
transaction as the transition that proved it, keyed by execution scope,
session and dead owner fence. Unproven endings write nothing. A staged
receipt deletes a fact inside the journal write that settles it, and
deleting a session removes its facts. Nothing consumes the facts yet.
…r-change' into brennanb2025/chat-closed-owner-history

# Conflicts:
#	src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts
#	src/main/runtime/agent-session-record-store.ts
Bumping the host schema made every older build open this database
read-only, refusing all chat writes and launches. The table is now
created at every writable open, like the queued-message table, and its
one-time migration of old proofs runs in the same transaction that first
creates it, so a retired fact never comes back.

Drop the stored journal boundary: taken at reservation it came before
rows the dead generation still writes, and every row already carries
its fence. Register the two new SQLite test files for the Node runtime.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #26867.

This PR kept the proof that an agent is gone after the next owner starts, so that leftover work could be cleaned up later. #26867 removes the need for that proof. Leftover work is now settled when ownership changes:

  • each saved row records which agent run wrote it;
  • the app works out "what is still running" from that;
  • one background retry finishes any cleanup that couldn't be saved straight away.

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.

1 participant