Skip to content

feat(agents): keep agent state in files that a program outside the app can read - #974

Open
loganj wants to merge 13 commits into
mainfrom
agent-host-state
Open

loganj wants to merge 13 commits into
mainfrom
agent-host-state

Conversation

@loganj

@loganj loganj commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

🤖

Summary

  • Second of three PRs that let a Buzz agent run without the desktop app open (see Why). Stacked on refactor(agents): separate the agent runner and its reads from the desktop app #961.
  • The problem: the app kept each agent's saved state inside its own web page storage (the browser localStorage). Only the app can read that. So a program outside the app could run the agent's code, but not with the agent's settings, its team instructions, or its saved conversations.
  • The change: the app now keeps this state in files, in the same agents folder where it already keeps the list of agents. The standalone program in feat(agent-host): run a saved agent without the desktop app #967 reads and writes the same files. An agent then behaves the same in both places.
  • For users of the app nothing changes. On first start the app moves the existing state into the folder. Settings, teams and sessions stay as they were.

Why

Buzz agents are AI assistants, such as Claude or Codex, that a person sets up in the Buzz desktop app. They answer when someone mentions them in a channel, and they can also watch for events and run on a timer. Today an agent runs only while its owner's desktop app is open, because the code that runs agents lives inside the app.

The goal of this stack is a small command-line program that runs an agent with the app closed. It must use the same code and the same saved state as the app, so the agent behaves the same wherever it runs.

Stack

Review in order. Each PR keeps the app working.

  1. refactor(agents): separate the agent runner and its reads from the desktop app #961: move the code that decides when an agent runs into its own module, and let the Claude and Codex agent code read conversations without depending on the app.
  2. This PR: keep agent state in files that a program outside the app can read.
  3. feat(agent-host): run a saved agent without the desktop app #967: add buzz-agent-host, the program that runs an agent with the app closed.

What moves

State Before Now
Each agent's settings, watches and timers one localStorage value for all agents one file per agent
Teams (the shared instructions a group of agents gets) localStorage one file
Hosted agents whose setup did not finish localStorage one file
Claude sessions (which conversation continues which Claude session) one localStorage value for all agents one file per agent
Codex sessions one localStorage value per agent and community one file per agent and community

The files are in agents/state/ in the app's data folder, beside identities.json.

Details

  • Native side (src-tauri/src/agent_state.rs): two commands, load all and set one key. Each key is one file. A write goes to a temporary file first and is then renamed, so a crash leaves the last complete copy. Characters that are not safe in a file name are written as %XX. If that name is longer than 255 bytes (the longest file name macOS, Linux and Windows all allow), as a Codex session key on a long community host can be, the file is named ~ plus the key's SHA-256 instead, and it holds the key with the value so a load gets the key back. The standalone host (feat(agent-host): run a saved agent without the desktop app #967) names files the same way.
  • App side (src/features/agents/state.ts): web page storage reads must be synchronous, so the app reads all the files once before it starts (main.tsx) and keeps a copy in memory. Each write changes the copy at once and is then saved to its file, in order. If the folder cannot be read, or reading and moving take longer than 15 seconds, the app uses localStorage for that start, as before, and the window still opens.
  • Moving the old state: at start, each agent key in localStorage is copied into the folder. Agent keys are in localStorage only before the first move, or after a start that could not use the folder, so they are the newer copy and replace what the folder has. They are removed from localStorage only once every copy is saved. If any copy fails, the app uses localStorage for that start, so nothing it shows is missing, and the next start tries again.
  • One key per agent: the app and the standalone program can run different agents at the same time. When all agents' records shared one value, a save by one program could write over the other program's changes. Now each program writes only the agents it changed. The old shared record value is split into per-agent files at the first start and removed once they are all saved. The old shared Claude sessions value drops each agent after that agent's own sessions are saved.
  • Agent types get the storage from the app: the Claude and Codex agent types used localStorage directly. They now use agents.storage, which the app provides.

Limits

  • A save that cannot be written puts back the saved value, in storage and in what the app shows, and the save fails with the error. So the settings form keeps the edit with Save enabled, and an agent's attention tool (a watch or interest_set change) reports the failure. A failed save of timer progress only logs: the app keeps the progress in memory so the timer does not run again at once, but after a restart the timer can run once more.
  • The app reads the files only when it starts. If the standalone program changes an agent's state while the app is open, the app shows the change after it restarts.
  • If the owner edits an agent in the app while the standalone program runs that agent, the app's save can undo the program's latest timer progress for that agent. In the worst case a timer runs one extra time.
  • If a start uses localStorage because a copy failed, and the owner deletes an agent's settings or a team during that start, a copy saved to the folder before the failure can bring it back on the next start.
  • Development builds and the installed app already share the agent list and locks. They now also share agent settings.
  • Older builds of the app do not see the moved state.

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 thanks for turning the first round around so fast. the record split, the Claude sessions split and the agents2 rename all keep their source now when a write fails, provider changes roll back correctly, and a direct save() retry reaches disk. four persistence issues are still open on 99500019a, details inline:

  1. blocking: a failed first copy out of localStorage can still lose teams and settings (state.ts)
  2. blocking: a failed settings Save leaves the form's Save button disabled, so the user can't retry (service.ts write)
  3. blocking: agent attention tools (watches, interest_set) report success when the write failed (service.ts runner adapter)
  4. blocking: valid Codex keys on very long hosts still go over the file name limit (agent_state.rs)

nonblocking, still open from the first round: main.tsx shows a blank window if loading agent state never finishes.

these come from reading the source and testing with injected write failures. nobody has run them in a real window or with two programs.

Comment thread src/features/agents/state.ts Outdated
Comment thread src/features/agents/service.ts Outdated
Comment thread src/features/agents/service.ts Outdated
Comment thread src-tauri/src/agent_state.rs Outdated
@loganj

This comment has been minimized.

@loganj
loganj force-pushed the agent-host-state branch 2 times, most recently from 4df4d09 to c3f5fe1 Compare October 14, 2026 00:15
@loganj
loganj added this pull request to stack #1080 October 14, 2026 15:00
Base automatically changed from agent-host-runner to main October 14, 2026 15:14

@wesbillman wesbillman 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.

🤖 Star Lord automated source follow-up via Wes’s account: no additional changes requested. The three prior findings are addressed: startup retains readable legacy records when the split fails, unfinished hosted-agent changes await persistence and propagate failure, and both provider-default dialogs disable their choices during a save. Source assessment: 9/10 for this repair scope.

The supported hosted provider retains its registration replay key until activation, preserving same-name retry if the initial unfinished-state save fails. Reviewed the repairs, their callers and error/retry paths, and performed a fresh-frame self-review; this is not a new exhaustive review of unrelated stacked changes.

Head 5d343f45b28d9ceedb1518e75edff2d8dd575ef4; base c8c0d52df1fb5a5c08099b2d8836aec35bab0657. Public PR description (no attached images), commit messages and changed publication surface inspected. Source-only: no PR code, tests, builds or app executed; CI was not assessed. Live file migration, concurrent-process behavior, native focus, themes/sizes and enlarged-text acceptance remain unverified. Shared Select, Button and host Dialog ownership is preserved. COMMENT only; not approval or merge authorization.

@wesbillman wesbillman 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.

🤖 Star Lord automated source follow-up via Wes’s account: one P2 finding, detailed inline. Shared catch-up checkpoints can overwrite another host’s progress and replay handled watch events. Source assessment: 8/10 until that persistence boundary is split. This was missed in my earlier review; the newly traced checkpoint producer/consumer establishes the defect. It is not a regression introduced by this rebase.

The three prior findings remain fixed: failed record splitting preserves startup, unfinished hosted-agent saves are awaited, and provider-default selections are disabled while saving. Range-diff confirms the five feature patches are unchanged apart from surrounding context. Rocket, Mantis and Groot completed independent source lanes; I reconciled their evidence and the new agent-start/config integration.

Head 2445e49973dd7625d9174579721735acb8dffdee; base ccd1be5191f1f47b70adb2e3783d4ffd00553edf. Public description (no attached images), changed publication surface and five commit messages inspected. Source-only: no tests, builds or app execution; live migration, cross-process behavior, native keyboard/error-retry focus, themes/sizes and human acceptance remain unverified. One CI snapshot has two failed JavaScript shards and other work still running. Shard 2 logs show TS2741 missing Goose effort fields in launch-boundary.test.ts:560,596, unchanged from the base; Windows native validation is skipped. COMMENT only, not approval.

Comment thread src/features/agents/state.ts

@wesbillman wesbillman 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.

🤖 Star Lord automated source follow-up via Wes’s account: one P2 finding in the new checkpoint migration, detailed inline. The per-agent files fix the steady-state overwrite, but overlapping first starts can still replace newer saved progress with a stale migration snapshot. Source assessment: 8/10 until that migration write is protected.

The prior startup, unfinished-save and provider-default repairs remain intact. Rocket, Mantis and Groot completed independent lanes; I reconciled their evidence and checked the source path. This review covers the checkpoint repair and rebase integration, not a new exhaustive review of unchanged feature code.

Head 747d8a3290234a0caa37c3577512ce4097081d61; base 330e60ba0e373ff292dcd48da326fcff3c36a711. Public description (no attached images), six commit messages and changed publication surface inspected. Source-only: no tests, builds or app execution; live concurrent-host migration, native focus/error-retry behavior, themes/sizes and human acceptance remain unverified. Shared Select/Button and host Dialog ownership is preserved. One hosted CI snapshot succeeds, including CI required; Windows native validation is skipped. COMMENT only, not approval.

Comment thread src/features/agents/catch-up.ts Outdated

@wesbillman wesbillman 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.

🤖 Star Lord automated source follow-up via Wes’s account: no additional changes requested. The prior checkpoint-migration finding is addressed: native create-if-absent preserves an existing destination, and checkpoint saves compare this process’s previous snapshot so an unrelated agent’s progress is not rewritten. Source assessment: 9/10 for this repair scope.

Rocket, Mantis and Groot completed independent lanes; I reconciled their evidence and checked the production callers and failure paths. The startup, unfinished-save and provider-default repairs remain intact. Existing limits still apply: a later edit of the same agent from an older process snapshot can replace newer state; this repair does not provide live cross-process synchronization. This is a follow-up of the eight-file repair, not a new exhaustive review of unchanged feature code.

Head 496533da4c2e43ffe4769bbb2e45b1f637be09b5; base 330e60ba0e373ff292dcd48da326fcff3c36a711. Public description (no attached images), seven commit messages and changed publication surface inspected. Shared Select/Button and host Dialog ownership remains intact. Source-only: no tests, builds or app execution; live concurrent-host migration, crash recovery, performance, native keyboard/error-retry focus, themes/sizes and human acceptance remain unverified. One hosted CI snapshot shows 34 successful checks, including CI required; Windows native validation is skipped. COMMENT only, not approval or merge authorization.

@wesbillman wesbillman 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.

🤖 Star Lord automated source follow-up via Wes’s account: two P2 findings, detailed inline. Timer progress can overwrite a pending settings edit, and synchronous storage refusal can admit the same overdue timer repeatedly. Source assessment: 8/10 until these persistence paths are repaired. Both existed at the prior reviewed head and were missed in my earlier review; neither is a new rebase regression.

The prior migration, per-agent checkpoint and provider-default repairs remain intact. The rebase preserves the feature patches while retaining the model-loading and save guards. Rocket, Mantis and Groot completed independent systems, correctness/security and product/accessibility lanes; I reconciled their evidence and verified both findings against the source. This is a repair/integration follow-up, not a new exhaustive audit of unchanged feature code.

Head 100d6534a5c32350f343bca674e0c0f1b7a96d63; base dcd3960c9f6613ded457232903d250caa6e7e1fd. Public description (no attachments), seven commit messages and changed publication surface inspected. Shared Select/Button and host Dialog ownership remains intact. Source-only: no tests, builds or app execution, and current CI was not assessed. Live migration, concurrent-host operation, native keyboard/error-retry focus, themes/sizes, performance and human acceptance remain unverified. COMMENT only, not approval or merge authorization.

Comment thread src/features/agents/service.ts Outdated
Comment thread src/features/agents/service.ts Outdated

@wesbillman wesbillman 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.

🤖 Star Lord automated source follow-up via Wes’s account: one P2 finding, detailed inline. Source assessment: 8/10 because timer progress can still overwrite a pending timer rearm’s saved usage, granting an unintended fresh occurrence budget.

The synchronous storage-refusal fix is confirmed in source, and pending non-timer settings now survive progress writes. The remaining issue is the per-timer state within that merge. Rocket, Mantis and Groot completed independent lanes; I checked the finding against the supported attention-write path and reconciled their results.

Reviewed head d914c830ebfaa007b042a52449c89e0871014d55, base dcd3960c9f6613ded457232903d250caa6e7e1fd; repair delta from 100d6534a5c32350f343bca674e0c0f1b7a96d63 is three files. Public description (no image attachments), changed publication surface and eight commit messages inspected. Source-only: no tests, builds or app execution; CI was not assessed. Live persistence/concurrency, native focus and error/retry interactions, themes/sizes and human acceptance remain unverified. This is a non-blocking COMMENT review, not approval.

Comment thread src/features/agents/service.ts Outdated

@wesbillman wesbillman 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.

🤖 Star Lord automated source follow-up via Wes’s account: one P2 finding, detailed inline. Source assessment: 8/10 because a timer can consume an occurrence while its own rearm is saving, then lose that consumed count and exceed its configured budget.

The prior different-timer overwrite is repaired: only changed slugs are merged, and unrelated pending timer state is retained. Rocket, Mantis and Groot completed independent systems, correctness/security and product lanes; I reconciled their evidence and checked the runner lifecycle. Later invalidation cannot undo an occurrence that already completed before the save settled.

Head e71ab26c907a93eae0e7da1e2d1ceb843a0baef5; base dcd3960c9f6613ded457232903d250caa6e7e1fd. Scope: three-file repair delta from d914c830ebfaa007b042a52449c89e0871014d55, prior finding and affected callers, not a new exhaustive audit of unchanged feature code. Public description (no attachments), commit messages and repair publication surface inspected. Source-only: no tests, builds or app execution; current CI was not assessed. Live persistence/concurrency, native focus/error-retry, performance and human acceptance remain unverified. This is a non-blocking COMMENT review, not approval.

Comment thread src/features/agents/service.ts Outdated
@loganj

loganj commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Plan: restructure agent storage instead of another patch round

The last several review rounds on this PR found the same kind of bug in a new place each time. The latest finding (a timer that uses an occurrence while its own rearm saves) is one more. I fixed each case where it was found. Those fixes are correct, but they do not remove the cause, so I expect more. I will change the structure in this PR.

Why the bugs keep coming

1. File storage looks like localStorage, but does not act like it. fileBacked keeps the Storage interface. setItem returns at once, and the file save finishes later and can fail. To know if a save worked, a caller must also call persisted(). Every caller written for the old rule ("setItem saved it or threw") is wrong until someone finds it and updates it. That is how the defaults dialogs, unfinished setups, teams, record saves and the refused-save timer bug happened. The type system cannot find the next one.

2. No single owner changes an agent's record. Owner settings, the attention tools, timer progress, retype and the migrations each copy the whole record from a snapshot and write all of it back. The snapshot can be the shown record, the one still saving, or storage. Each pair of writers that overlap is a new bug: timer vs settings save, timer vs another timer's rearm, and now timer vs its own rearm. The shown / keep / saving rules in write patch this one case at a time.

The old localStorage code did not have these problems: it was synchronous, one process owned it, and a write was never pending. This PR made storage asynchronous without changing the contract that callers depend on.

What I will change

  1. An explicit async store for agent state. A new AgentStore type replaces Storage for agent state. Reads stay synchronous (from memory). Writes return a promise that settles when the file is saved and rejects when it is not. Because the methods have new names and types, the compiler shows every caller, and no caller can forget to wait. persisted(), afterSaved() and the addItem() duck-typing go away. In the browser, a small adapter wraps localStorage in the same type.
  2. One owner for each agent's record, with an ordered queue of changes. Each writer sends a change instead of a whole record: for example "set config", "set this attention object" or "timer A ran". The owner runs the changes for one agent in order. Each change applies to the newest saved record. When the save finishes, the owner updates the saved record that forms compare against. If the save fails, the change is dropped and the error goes to the caller.
    • The timers read the saved record with the pending changes applied in order. So a pending rearm takes effect for timers at once, and a superseded schedule cannot run.
    • Timer progress becomes a field change ("this timer used one more occurrence; next due at T"), not a whole-record write. If its timer was rearmed in between, the used count still goes up and the new deadline stays. This fixes the open finding.
    • The saving gate in the runner and the shown / keep logic in write go away.

Scope and limits

  • This changes about 150 to 250 lines in service.ts, runner.ts, state.ts and store.ts, plus the bundled agent types that save to agent storage. feat(agent-host): run a saved agent without the desktop app #967's host must adapt to the new store type.
  • Writes from other processes stay a documented limit. This change does not address them.
  • I will add regression tests for the open finding (a timer that runs at its old deadline while its own rearm saves), and for the earlier overlap cases, through the new owner.

I will reply to the open thread when the change is pushed.

@wesbillman wesbillman 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.

🤖 Star Lord automated source review via Wes's account, integrating independent Rocket (systems/lifecycle), Mantis (correctness/security/concurrency), and Groot (product/user-flow) lanes.

Follow-up review of head 8aa080b against base dcd3960; repair delta from previously reviewed e71ab26. The prior pending-rearm occurrence-budget defect is repaired in source. One new P2 is recorded inline. Assessment: 8/10 pending that lifecycle repair.

Inspected the three-file repair, relevant runner/storage/caller context, public description (no attachments), and commit messages. Source-only: no tests, builds, app execution, live persistence/concurrency reproduction, native focus/retry testing, current CI assessment, or human acceptance. This COMMENT is non-blocking feedback, not approval or merge authorization.

Comment thread src/features/agents/runner.ts Outdated

@wesbillman wesbillman 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.

🤖 Star Lord automated source follow-up via Wes’s account: one P2 finding, detailed inline. The AgentStore conversion leaves the documented Codex live acceptance caller on the old storage interface, so that workflow fails when saving its first session. Source assessment: 8/10 until that caller is adapted. The production plugin supplies the new store correctly.

The prior timer-wake finding is addressed in source: pending settings changes wake their runner on settlement, and timer progress no longer blocks a free Interest lane. Rocket, Mantis and Groot completed independent systems/lifecycle, correctness/security/concurrency and product/accessibility lanes; I reconciled their evidence and checked the supported callers. This is a follow-up of the storage refactor, timer repair and rebase integration, not a new exhaustive audit of unchanged feature code.

Head c1479a63f9575b9964fd2c9cf0698015347fed56; base a5b3391ecd2f7cfc141ba6ecd0886226daaaee9a. Public description (no attached images), twelve commit messages and changed publication surface inspected. Shared Select/Button and host Dialog ownership is preserved. Source-only: no PR code, tests, builds or app execution; CI was not assessed. Live migration/concurrent-host persistence, native keyboard/error-retry focus, themes/sizes, performance and human acceptance remain unverified. Non-blocking COMMENT only, not approval or merge authorization.

Comment thread src/bundled/codex/runtime.ts

@wesbillman wesbillman 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.

🤖 Star Lord automated source follow-up via Wes’s account: no additional changes requested. The prior Codex live-acceptance finding is addressed: the fixture now provides AgentStore get/keys and promise-returning set/add, including deletion and create-if-absent behavior. The thread-name assertion matches the existing title plus conversation-tag format. Source assessment: 9/10 for this repair scope.

This uses the small-follow-up solo exception: comparison with previously team-reviewed c1479a6 shows only src/bundled/codex/live.mjs changed (17 additions, 4 deletions), with the same base and unchanged feature source. I checked the repair against AgentStore, CodexRuntime save/load and sessionName/sessionTag, then reread the delta for integration mistakes. No new independent team lanes were run.

Head 9eb07892a1425aee31e5c848ec76a64d562460b6; base a5b3391ecd2f7cfc141ba6ecd0886226daaaee9a. Public description (no attachments), commit messages and changed publication surface inspected. Source-only: no acceptance command, tests, builds or app execution; current CI was not assessed. Live saved-session recovery, native focus/error-retry behavior and human acceptance remain unverified. This is a bounded repair follow-up, not a new exhaustive audit of unchanged feature code. Non-blocking COMMENT only, not approval or merge authorization.

loganj and others added 13 commits October 18, 2026 00:11
The app kept each agent's settings, the owner's teams, unfinished setups,
the Claude and Codex sessions and their default settings in the WebView's
localStorage, which no program outside the app can read. Keep them in
agents/state/ beside identities.json instead, one file per key, through
two native commands.

The app reads the folder once before it starts, keeps a copy in memory,
and saves each write to its key's file in order. On first start it moves
the agent keys it had in localStorage into the folder.

Each agent's record and its Claude sessions now have their own key, so
two programs that run different agents never write over each other.
Agent types get the storage from the agents service (agents.storage).

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
A failed file write during the one-time moves (the shared record value,
the shared Claude sessions value, the agents2 rename) still removed the
source, so the next start lost that agent's settings or sessions. Each
move now removes its source only after every new copy is saved.

A failed write also no longer passes as saved: agent storage puts back the
key's saved value, saves of settings, providers and teams reject, and the
same value can be saved again. A failed timer-progress save still keeps the
run's memory, so a timer does not run again at once.

Agent state keys are limited by their file name (255 bytes), not by a
200-byte key, so Codex sessions on long community hosts are saved.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
A settings, team or attention change was shown before its save finished
and stayed shown when the save failed. The settings form then saw no
change and turned off Save, and an agent's watch or interest_set edit
reported success. Each now goes back to what storage still has when its
save fails, and an attention write passes the error to the tool. Only a
timer's progress stays, so the timer does not run again at once.

The first move out of localStorage deleted a key whose copy failed from
the storage the app reads, so the app started with no teams and default
settings, and a later save replaced them. Now every key is copied before
any leaves localStorage, and if a copy fails, or loading takes longer
than 15 seconds, the app uses localStorage until the next start. The
agent keys still in localStorage then replace the copies in the folder,
since the app wrote them last.

A key whose encoded file name is over 255 bytes, such as a Codex session
key on a long community host, could not be saved. Its file is now named
by the key's SHA-256 and holds the key with the value.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
…saved

Each start let agent keys in localStorage replace the agent storage
files. After the first move, localStorage has no agent state, so a start
that could not use agent storage began with none, and what it saved
then replaced the full files at the next start: for example, a session
list with one conversation replaced one with three. Agent storage now
keeps a marker once every key was moved. After that, the files win, and
keys from such a start only fill in keys the files do not have.

A settings save was shown before it was saved. The Codex settings form
resets its draft when the shown settings change, so when the save
failed, the owner's edits were replaced with the saved settings and Save
was turned off. Settings are now shown only once they are saved. Later
changes still build on a write that is being saved. Timer progress is
still shown at once.

The Claude Code and Codex provider defaults dialogs closed before the
defaults were saved, and a failed save was lost at the next start. They
now wait for the save, close only when it succeeds, and otherwise keep
the draft and show the error, so the owner can try again.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
- A full store no longer stops startup. If the move of the old shared
  agent records fails, the app logs it and reads the old value, which
  stays for the next start to move.
- Unfinished hosted-agent changes now wait for their save. When it
  fails, Delete and setup report the error and the app shows the saved
  list again, so a restart shows the same state.
- The default-model and thinking choices stay fixed while defaults
  save, so the dialog never closes over a choice it did not save.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
Catch-up progress was one shared value. With agent state in shared files,
two programs that run different agents each rewrote the whole value from
their own copy, so one could undo the other's saved progress and an agent
could get already handled events again after a restart.

Each agent's progress now has its own key, and a save writes only the
agents whose progress changed. The old shared value is split into the
per-agent keys at start, before any progress is read, and removed once
they are saved.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
Two programs can start from the old shared value before either moves it.
If one moves it and runs its agent on, the other's late move wrote its
older copy over the newer file, so a restart could replay handled events
or bring back an older record.

A move now adds each agent's key only where no file holds it. The native
store does that in one step and returns the file's value, which the copy
then takes. Catch-up progress is also saved only for the agents this
program moved since its last save, so an older copy of another
program's agent is never written.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
A timer that came due while a settings or attention change was still
saving wrote the shown record with its progress, after the change. The
change was lost on disk and in the app, though its Save succeeded. Timer
progress now goes into the record being saved, and the app still shows
the saved settings until that save finishes.

A save refused at once by storage (localStorage quota) now throws at
once again, so the runner does not start a timer whose progress it could
not save. Before, the refusal became a logged rejection, and the timer
ran again after each run.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
…ress

Timer progress replaced the whole timers map of a pending change with
the shown one. So when an agent or owner rearmed timer A while timer B
came due, B's progress put back A's old armed time, and A then got a new
budget of runs. The runner now reports only the timers whose state
changed. The service copies each into the pending change only when that
change left the timer's state as shown; a timer it rearmed, restarted or
removed keeps that.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
Timers ran from the shown record while a change to it was still saving,
and their progress was then merged into that change. Each kind of change
needed its own merge rule, and a timer rearmed during the save could run
from its old schedule and lose that run from its count.

Now a timer does not start while a save of its agent's record is pending;
the next tick runs it from the record that save left. So every write to
the record starts from the latest one, and the progress merge is gone.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
…wner

Review rounds on this PR kept finding the same two kinds of bug in new
places. This fixes their cause instead of the next case.

Agent storage is now an explicit async store (AgentStore). Its set and add
return a promise that settles once the value is saved and rejects when it
was not. The fake Storage over the agent state files, and the persisted()
and afterSaved() helpers callers had to remember, are gone. The compiler
now shows every caller that writes agent state. Doing so found one more:
an imported team's save was not awaited, so a failed save was not
reported as a team error.

Each agent's record now changes only through one ordered queue in the
agents service. Owner settings, the agent's attention tools, provider
changes, removal and timer progress all send a change. Each applies to
the newest saved record after the change before it settles. Timer progress
is a change to run state only: when a timer was rearmed or restarted since
the pass read it, the new deadline stays and only the occurrences the pass
used are added. The shown/keep/saving rules are gone. Timers still wait
while another change to their record is queued, so a pending rearm keeps
its budget.

A timer whose progress cannot be saved keeps it in memory, as a failed
save did before. It now runs only when it is due, not never: an async
store cannot refuse at once.

Cross-process writes are still a documented limit.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
… saved

Timers wait while a change other than timer progress is waiting or saving,
since it may rearm, restart or remove one. When the last such change settled,
nothing woke the runner, so a timer due meanwhile waited for the next
five-second tick and could expire first. The change owner now wakes the
agent's runner once its last change is saved or fails.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
The live acceptance check (`bin/node src/bundled/codex/live.mjs`) still
passed a `{ getItem, setItem }` fixture to CodexRuntime. Its first session
save threw, since agent storage now has `get`, `set`, `add` and `keys`, and
JavaScript callers are not type-checked. Its thread name check also predates
the conversation tag in thread names (#846).

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>

@wesbillman wesbillman 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.

🤖 Star Lord automated source review via Wes’s account, integrating independent Rocket (systems/lifecycle), Mantis (correctness/security/concurrency), and Groot (product/accessibility) reviews.

No new actionable findings in this revision-focused review. Source-review assessment: 9/10.

Reviewed head: d3786b6
Reviewed base: dc305c9
Previous reviewed head: 9eb0789

The 13-commit range-diff retains 11 identical patches; the remaining two differ only in service import context for the Me→Solo rename. We checked the rebased service/runner and Claude/Codex routing integration, startup and fallback handling, ordered persistence and session-storage consumers. The prior Codex acceptance-fixture repair remains present. Provider-default save failures retain drafts and expose retryable errors; the shared button preserves focus during saving. The public description and commit messages were inspected; the description has no attached images.

Scope and limits: this is a follow-up integration review, not a fresh exhaustive review of unchanged feature patches. Source only: no tests, typecheck, builds, app launches, or PR code execution. Live migration/restart and cross-process behavior, rendered accessibility/focus, and performance remain unverified. A single hosted-check snapshot had jobs still running; Windows native validation was skipped. This COMMENT is not an approval or merge authorization.

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.

3 participants