Skip to content

Buddy: the Platform derives his persona byte for byte as the installer and brings a change to running conversations when he is idle (DEV-6676 PR 2, F64) - #636

Merged
immakermatty merged 1 commit into
mainfrom
agent/DEV-6676-buddy-persona
Oct 25, 2026
Merged

immakermatty merged 1 commit into
mainfrom
agent/DEV-6676-buddy-persona

Conversation

@immakermatty

@immakermatty immakermatty commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Why

Buddy's persona (his name, ty or vy, the user's name in three cases, an optional tone) lives in one file, ~/.config/lazurio/buddy/persona.json. Until now only the interim installer wrote it, once, and derived everything from it: SOUL.md, the acting sender of Buddy's gateway, Delegace's drop-in. The Platform only read the user's name for Buddy's T3 settings (F61). So a name changed after the install reached nothing, and Začínáme and Nastavení → Buddy (root decision 0210, addenda of 2026-10-20) had nothing to call.

This is PR 2 of Buddy's onboarding (plan DEV-6676, the Buddy lead's shaping of 2026-10-25, §1.4, §2, §3.6 and §5). It makes the Platform the one deriver of the persona, with no UI yet. PR 3 adds the route POST /api/buddy/persona and the screens.

What changes

  • src/buddy/persona.ts: reads and validates persona.json exactly as installer 2026-10-25.1's persona_check does, in its order. It renders SOUL.md (cs and en, ty and vy, a tone or Lazurio's line) and the gateway's acting drop-in byte for byte as the installer. Every write (writePersona, confirmPersona) holds one kernel flock from its read of the persona to its replace; it writes only over the revision the writer read, otherwise it answers persona-changed (PR 3's 409). Of two writers of one revision, exactly one writes. Each derived file is replaced atomically on its own with its exact mode (0600, 0644) whatever the umask, never through a link. New optional member confirmed_at, which every current reader ignores.
  • src/buddy/gateway-refresh.ts: how a change reaches running conversations. Hermes does not notice a changed SOUL.md.
    • hermes sessions repair-prompts <id> --apply --json runs for every live conversation that holds a stored prompt. The store decides whether it worked, by the prompt hash Hermes keeps, not the command's exit.
    • One systemctl --user restart hermes-gateway.service follows, only once Hermes's own active-work verb says Buddy is idle, and never on units systemd failed to reread.
    • A restart that waits for files is derived (ExecMainStartTimestamp against the files' times, or NeedDaemonReload). An owed refresh of stored prompts cannot be derived, so it is recorded in the Platform's own file until a complete refresh and its restart.
  • src/buddy/t3-runtime.ts: buddyT3Quiet, T3 Code's own count of unfinished runs. Buddy's own t3 runs thread list --status … --subagents --json against ~/.t3-buddy.
  • src/buddy/persona-apply.ts and lazurio buddy persona [--apply] [--confirm] [--folder] [--json]: read and report, or apply, one apply at a time. An apply reads the persona only once it holds its turn, and answers only for the persona still current: it checks the revision again under the writers' lock before answering, and goes again if the persona changed. What waits answers waiting; what failed answers failed with its reason. Both stay visible and the next apply retries. Buddy's T3 settings follow; his T3 Code is stopped and started around them only when it has no run going (F61's rule).
  • src/buddy/chat.ts: Buddy's T3 display name follows his name (amends F61 point 1). The persona is checked whole, by the same reader.
  • docs/decisions.md: F64, with the alternatives, the hand-over (I-b) and the honest limits.

Review of cea70f4 (Pablo), addressed at this head

  1. Writers serialized. A kernel flock on .persona.lock covers the read, the comparison and the replace, for writes and confirmations alike. A dead holder's lock is released by the kernel. Tested: two writers in one process, a writer in another process, and a confirmation racing a write.

  2. A failed reload fails closed. No restart happens on old units, and NeedDaemonReload keeps it pending even when systemd starts the gateway by itself. Pending is judged on the unit as it stood before the reload, so a gateway that ran on older units restarts after a reload that succeeds.

  3. Prompts stay owed until the store shows them cleared. Any of these makes the refresh incomplete and the apply failed, never applied:

    • a command that fails;
    • a command that answers cleared and did nothing;
    • an id never passed to a command;
    • more than 500 conversations at once.

    The owed record survives anyone's restart. Where a store cannot say which prompt a conversation holds, the commands' exits decide.

  4. T3 Code's own runs gate its restart. The gateway's active-work no longer decides it. T3 Code's answer, by Buddy's own runtime and home, does: any thread in preparing, queued, starting, running or waiting, subagents included, at v0.1.1-lazurio.2 and v0.1.2-lazurio.1 alike. A T3 Code that cannot say counts as busy.

Also from the review:

  • F64 now says each file is replaced on its own, with no transaction, and that partial failure stays visibly retryable.
  • The claim about repair exits is narrowed, and failure tests were added.

Review of 28d9ce5 (Pablo), addressed at this head

P2, an apply answering for a persona already replaced. buddyPersona read the persona and rendered its files before it took .persona-apply.lock, while writers take .persona.lock. An apply that waited while a new name was written answered applied for the old name, and could write the old files over newer ones. The writer's own apply could meanwhile have given up as apply-running. Now:

  • The apply takes its turn first, then reads, renders and applies under it.
  • Before answering, it reads the revision again under the writers' lock. The order is always the turn, then the writers' lock; writers never take the turn, so the two cannot deadlock. It gives up its turn before that lock.
  • A write that completes meanwhile is either seen at that check (and applied in another round), or completes only after the turn is free, so its writer's next apply reads it. apply-running is therefore safe again.
  • After three rounds in which the persona changed each time, the apply answers waiting (persona-changed), never applied.

Tests:

  • the exact overlap reproduced in the review (the new name is applied once, with one restart);
  • a name written while an apply applies;
  • a persona that changes in every round;
  • a second apply answering apply-running while the first applies the name written meanwhile.

What deliberately does not change

Byte parity and provenance

tests/fixtures/buddy-persona/goldens.json is the installer's own output.

  • The installer lives outside this repository. It is an interim draft that its maintainer owns until Machines take the install over (Machines#701). The provenance is its recorded sha256 (2026-10-25.1: f2413529…7d3c3e) plus the committed goldens. The committed generator (generate-goldens.py) checks that sha256, extracts the installer's helper, and runs its own persona-check, soul-render, acting-dropin-render and persona-build into real files. Whoever holds the installer can produce the goldens again; nobody needs it to run the tests.
  • Render parity: 28 SOUL.md renders, 19 drop-ins, 8 quoting cases and 4 persona files are identical byte for byte, and identical from 2026-10-24.2 and 2026-10-25.1. Three files the installer's own container tests compare with cmp are among them.
  • Validation parity: 84 cases, each accepted, refused for the same member, or (where the installer crashed) refused. Lone surrogates in every member are now parity (2026-10-25.1 refuses them), and the order of the checks is pinned.
  • One knowing difference remains: NaN, which Python's json reads.

Verified

  • bun run lint, bun run typecheck, bun run check:public: pass.
  • tests/buddy-persona.test.ts (43 tests, three more runs in a row without a failure) and tests/buddy-chat.test.ts (11) pass. With tests/buddy-t3-runtime.test.ts, tests/folder-buddy.test.ts, tests/shell-buddy.test.ts and tests/launchpad-chat.test.ts, 274 tests pass.
  • Whole bun test on 1c37ff5d (macOS): 4801 pass, 10 skip, 2 fail across 315 files (1072 s). The run overlapped another agent's full run and mutation run on the same machine. Both failures are known flakes under load, outside Buddy's code, and both files pass alone, twice: Flaky under load on macOS: two recover tests about the Recovery-mode issue body and launchpad-hosted's hostile HTTP_PROXY test #580 (launchpad-hosted, the hostile HTTP_PROXY test, at its 5 s limit) and Flaky under a loaded full run: application-environment "session runner …" refuses its fixture port as port-occupied #222 (application-environment, the session runner's fixture port). The previous head's clean run passed 4799 of 4799. CI on Linux at this head: static checks, both test shards and ubuntu-24.04 pass.
  • Flake, reported separately: on the first revision, a local run that overlapped another full run on the same machine failed launchpad-new-tab "reconciliation: a tab closed for everyone…" at its 3 s limit. That is the known flake Flaky after #385: the new-tab holder's reconciliation test sometimes never sees the hub's second list within 3 s #386, unrelated; every clean run since passed it.
  • Mutation checks: 70 of 70 caught. Each guard removed or loosened fails a test. That is the first revision's 48 still in the code, one more lone-surrogate case, the first review's 18 and the second review's 3; F64 lists them.
  • systemctl show --timestamp=us+utc was checked on systemd 255.4 (Ubuntu 24.04, as Machines' guests run) in a disposable container: ExecMainStartTimestamp to the microsecond in UTC with an English weekday under an empty environment, and NeedDaemonReload=yes after a drop-in write.
  • Not run: a real Hermes gateway, T3 Code, Buddy or Osobní Prostředí (fakes with recorded shapes of Hermes v0.22.4-lazurio.1 and T3 Code v0.1.1-lazurio.2). The first acceptance is Ring 0 with PR 3's UI.

Risks and follow-ups

  • Hand-over I-b (installer, then Machines#701): stop rendering SOUL.md and the acting drop-in, call lazurio buddy persona --apply, read restart and skip its own restart when it is done, and move its drop-in verification with it.
  • Delegace: Buddy's briefs say 'Buddy za uživatele', because the names live only in delegace.service; read them from Buddy's persona #616 should read the persona first, or retire the drop-in together.
  • The active-work question has its own small control-socket client. F63's gateway client (PR 1, Buddy: his ChatGPT sign-in and state through his own Hermes gateway, in the Launchpad and lazurio buddy (DEV-6676 PR 1, F63) #635) is the place for one client once both are on main; accepted in review as a follow-up. docs/decisions.md will conflict with PR 1 (F63 there, F64 here).
  • The installer's own write of the persona takes no lock. The comparison right before the replace narrows that window and does not close it.
  • T3 Code's count and the stop of its unit are two steps: a run that starts between them is stopped with the unit.
  • That an apply gives up its turn before the writers' lock is argued, not tested (the window lies between two releases). A persona that changes in every round leaves its last change to the next apply.
  • The shaping's P3 rejection said Hermes's PUT /api/profiles/<name>/soul writes 0644. It keeps an existing file's mode and uses 0644 only for a new file. P3 stays rejected for its other reasons (F64's table).

Plan DEV-6676 · worktree DEV-6676-buddy-persona · branch agent/DEV-6676-buddy-persona · based on main 9781aed

🤖 Generated with Claude Code

@immakermatty
immakermatty marked this pull request as ready for review October 25, 2026 01:39

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

@immakermatty I reviewed exact head cea70f4 against 9781aed. The focused local tests pass (40/40), but these failure and concurrency paths block approval:

  1. src/buddy/persona.ts:654-663,761-763: the revision is checked before rename, with no serialization/conditional swap. Two different writers can both observe the same old hash, both return written, and the later rename silently overwrites the first. The promised persona-changed guarantee is not atomic. Serialize writers across the comparison and replacement (including confirmation), then test two concurrent updates.
  2. src/buddy/persona-apply.ts:309-316,346-365: when NeedDaemonReload=yes and daemon-reload fails, execution still restarts the gateway and reports success. systemd can start with its previously cached drop-in; the new process timestamp then clears the derived pending condition, so a later apply need not repair the acting sender. Fail closed on reload failure and retain a retryable pending state.
  3. src/buddy/gateway-refresh.ts:334,372-388 and src/buddy/persona-apply.ts:346-365: failed repair-prompts commands are counted but still return state: refreshed; apply restarts and can report applied. I reproduced {"state":"refreshed","refreshed":0,"live":1} when the sole repair exits 1. After a successful restart, the timestamp pending marker disappears, so that conversation is never retried. The 200-row cap likewise omits older live conversations while claiming every live prompt was cleared. Preserve retryable failure/coverage and do not call the refresh complete in these cases.
  4. src/buddy/persona-apply.ts:322-331,352-395: the gateway active-work=idle does not establish that the separate lazurio-t3-buddy.service has no active ACP turn. A running T3 unit can be stopped during such a turn. Gate the T3 stop on T3-specific quiescence/drain, not just gateway idleness.

The committed generator checks the installer SHA and invokes its extracted helper, and the committed golden comparisons pass, but the private installer itself is not in this public checkout, so I could not independently regenerate provenance. F64 correctly labels the socket owner-UID path and real gateway/SIGTERM behavior as not exercised; repair exit-code handling needs explicit failure tests and a narrower claim. File replacements are individually atomic with tested modes and final-component link handling, not a transaction across SOUL.md, the drop-in and T3 settings; partial failure must stay visibly retryable. The small active-work client overlapping #635 is acceptable as a bounded follow-up to consolidate after both land. No merge while main is frozen for v0.1.11.

immakermatty added a commit that referenced this pull request Oct 25, 2026
…g, stored prompts stay owed until the store shows them cleared, and Buddy's T3 Code restarts only with no run going (DEV-6676)

Pablo's review of #636 at cea70f4 found four failure paths; each is fixed
and tested:

- Writers serialized: writePersona and confirmPersona hold a kernel flock
  on ~/.config/lazurio/buddy/.persona.lock (acquireFileLock, which the
  kernel releases when its holder dies) from the read to the replace, so of
  two writers of one revision exactly one writes and the other answers
  persona-changed. Tested with two writers in one process, a writer holding
  the lock in another process, and a confirmation racing a write.
- A failed daemon-reload fails closed: no restart on the units systemd still
  holds; NeedDaemonReload keeps the restart pending even when the gateway
  starts again by itself, and pending is judged on the unit as it stood
  before the reload, so a gateway that ran on older units is restarted
  after a reload that succeeds.
- Stored prompts: only live conversations that hold a stored prompt are
  cleared, and the store decides, by the prompt hash Hermes keeps
  (sessions.system_prompt_hash), never by the command's exit alone; a
  failure, an answer that did nothing, an id never passed or more than 500
  conversations leave the refresh incomplete and the apply failed. Because
  stored prompts have no time of their own and anyone's restart erases the
  files' evidence, an owed refresh is recorded in the Platform's own file
  (~/.local/state/lazurio/buddy/prompt-refresh-owed.json), written before
  SOUL.md changes and removed only after a complete refresh and the restart
  that follows it.
- Buddy's T3 Code is stopped around its settings only when T3 Code itself
  has no run going: his own t3 (the unit's runtime) asks thread list for
  every thread in an unfinished state, subagents included, at
  v0.1.1-lazurio.2 and v0.1.2-lazurio.1 alike; the gateway's active-work no
  longer decides it.

Also: one apply at a time (a flock in Buddy's state directory); checks in
the order of installer 2026-10-25.1, which refuses a lone surrogate in every
member, so that case is parity now; goldens produced from 2026-10-25.1,
whose renders are byte-identical to 2026-10-24.2's. 67 guards, each removed
or loosened, fail a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
immakermatty added a commit that referenced this pull request Oct 25, 2026
…en left visible, writers serialized, a failed reload restarting nothing, an owed prompt refresh recorded, T3 Code's own runs gating its restart (DEV-6676)

F64 now says what the code does after Pablo's review of #636: each derived
file is replaced atomically on its own, with no transaction across SOUL.md,
the drop-in and the T3 settings, and whatever did not happen stays visible
and is retried; the Platform's writers of the persona hold one kernel lock;
a reload systemd refuses restarts nothing and stays pending; a refresh of
stored prompts is verified against Hermes's store and, since it cannot be
derived, recorded while owed (with why); Buddy's T3 Code restarts only when
T3 Code counts no unfinished run. It names installer 2026-10-25.1 as the
byte authority (2026-10-24.2 renders the same bytes), says that the
installer lives outside this repository so the provenance is its recorded
sha256 with the committed goldens and generator, narrows the claim about
repair exits, and lists the new alternatives, checks and limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty

Copy link
Copy Markdown
Contributor Author

Thank you for the review of cea70f4. All four findings are fixed on the same branch, at exact head 28d9ce5 (two commits on top: f282a09f with the code and tests, 28d9ce5c with F64). No review requested and nothing merged.

1. Writers serialized (src/buddy/persona.ts, underPersonaLock)

  • writePersona and confirmPersona both hold one kernel flock from their read of the persona to the rename: acquireFileLock on ~/.config/lazurio/buddy/.persona.lock.
  • I chose flock over a lock file taken with an exclusive create, because the kernel releases it when its holder dies; a crashed writer's exclusive-create file would block every later write.
  • Under the lock a writer reads, compares the revision, then replaces. Of two writers of one revision, exactly one writes and the other gets persona-changed with the persona the first wrote. A writer that waits longer than 10 s gets busy. Where no lock can be taken, nothing is written.
  • The comparison right before the rename stays, as a narrower guard against the installer's own write, which takes no lock.
  • Tests:
    • two writers of one revision in one process, each dwelling between read and replace;
    • a writer in another process holding the lock while one here waits and then answers persona-changed;
    • a confirmation racing a write with nothing lost.
  • Mutations: removing the lock from the write, from the confirmation, or from the apply each fails a test.

2. A failed reload fails closed (src/buddy/persona-apply.ts, src/buddy/gateway-refresh.ts)

  • When daemon-reload fails, no restart happens: the answer is failed with units-reload-failed and restart: "failed".
  • restartPending now counts NeedDaemonReload=yes for a running gateway. So the restart stays pending even after systemd starts the gateway by itself on the units it holds.
  • Pending is judged on the unit as it stood before the reload. A gateway that ran on older units is therefore restarted after a later reload that succeeds. The test caught exactly this once I stopped re-reading the unit after the reload.
  • A stopped gateway on units systemd did not reread reads outdated too.
  • Tests: reload fails → no restart → systemd restarts the gateway itself → still pending (with the owed record removed, to isolate the signal) → reload succeeds → reload, then restart. The stopped-gateway case is covered as well.
  • Mutations: a restart despite the failed reload fails a test, and so does dropping NeedDaemonReload from the pending check.

3. Repair failures and coverage stay retryable

  • Only live conversations that hold a stored prompt are cleared.
  • Hermes v0.22.4-lazurio.1 keeps a stored prompt by content hash (sessions.system_prompt_hash). A conversation counts as refreshed only when the store no longer holds the hash it held before: cleared, rebuilt by a turn, or gone. Only the hash is read, never a prompt.
  • These make the refresh incomplete and the apply failed with prompts-incomplete; it is never called complete:
    • a command that fails;
    • a command that answers cleared and did nothing;
    • an id never passed to a command;
    • more than 500 conversations.
  • Where a store has neither prompt column, the commands' exits decide, and that is the narrower claim now in F64.
  • Retry signal. It cannot be derived: stored prompts have no time of their own, staleness could only be read from content, and a restart by anyone erases the files' timestamp evidence. So it is recorded explicitly:
    • the file is ~/.local/state/lazurio/buddy/prompt-refresh-owed.json, 0600 in 0700, the Platform's own and never the installer's managed.json;
    • it is written before SOUL.md changes;
    • it is removed only after a complete refresh and the restart that follows it, or with no gateway running.
  • While the record is there, a complete refresh is followed by a restart, so agents built from stale prompts in between are dropped too. An incomplete refresh never makes the record go.
  • F64 justifies the record in its own paragraph and in the alternatives table.
  • Tests:
    • fail and lie, then the retry clears the rest and restarts once more;
    • a conversation gone counts as refreshed;
    • an id that reads as an option is never passed and keeps the refresh owed;
    • the cap, then the next refresh going on;
    • the record outliving a restart by systemd;
    • a store that can only be judged by exits.
  • Mutations: not consulting the store, exits alone deciding, a cap that claims completeness, an unpassed id counted as cleared, no record before SOUL.md, settling the record before its restart or after an incomplete refresh, and no restart after the owed refresh each fail a test.

4. T3 Code's own runs gate its restart (src/buddy/t3-runtime.ts, buddyT3Quiet)

  • Buddy's own t3 asks T3 Code itself. That is the runtime his unit names (F61 addendum point 3), never the one on PATH. It runs thread list --base-dir ~/.t3-buddy --status preparing --status queued --status starting --status running --status waiting --subagents --limit 1 --json.
  • T3 Code counts every matching thread in total (OrchestratorMcpService.listThreads: total: filtered.length). The flags and run states are the same at v0.1.1-lazurio.2 and v0.1.2-lazurio.1.
  • Only zero lets the unit stop, the settings apply and the unit start again. A run going, T3 Code not answering (exit 3) or an answer that is none means chat: waiting, visibly. The gateway's active-work no longer decides it.
  • Tests:
    • a T3 run going while the gateway is idle: no stop;
    • not running, and no answer: waiting;
    • no run going: is-active, stop, start, whatever the gateway does;
    • the exact argv.
  • Mutations: a stop while T3 Code runs, "cannot say" counted as quiet, --subagents left out, and waiting counted as finished each fail a test.

Also

  • F64 now says each file is replaced atomically on its own, with no transaction across SOUL.md, the drop-in and the T3 settings, and that whatever did not happen stays visible and is retried. A test covers a drop-in that cannot be written after SOUL.md was.
  • Provenance: F64 and the PR body say the installer lives outside the repository (an interim draft its maintainer owns, until Machines take over). The provenance is its recorded sha256 plus the committed goldens and generator. The generator still checks the sha.
  • The goldens are now produced from installer 2026-10-25.1 (f2413529…7d3c3e). Its renders are byte-identical to 2026-10-24.2's. It refuses a lone surrogate in every member, so that case moved from exception to parity, with cases for every member and one that pins the check order. NaN stays the one knowing difference.
  • One apply runs at a time; a second answers waiting with apply-running.
  • The small active-work client stays as the accepted follow-up for Buddy: his ChatGPT sign-in and state through his own Hermes gateway, in the Launchpad and lazurio buddy (DEV-6676 PR 1, F63) #635.
  • Checks: lint, typecheck and check:public pass. tests/buddy-persona.test.ts (39) and tests/buddy-chat.test.ts (11) pass, five runs in a row without a failure. With the other Buddy test files, 270 tests pass. 67 of 67 mutations are caught. The whole bun test on 28d9ce5 passes: 4799 pass, 10 skip, 0 fail across 315 files. CI is green at this head.

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

@immakermatty Re: #636 (comment) — reviewed exact head 28d9ce5 against the four findings on cea70f4.

  1. Concurrent writes: resolved. writePersona and confirmPersona share the kernel .persona.lock across read, revision comparison and replace; in-process, cross-process and confirmation races are covered.
  2. Failed daemon-reload: resolved. Pending is captured before reload; failure prevents restart, and NeedDaemonReload=yes remains pending even after an external gateway restart. The failed-then-successful reload path is tested.
  3. Repair failures/cap: resolved for the reviewed paths. The session store's prompt hash decides clearance; failed/unconfirmed/omitted work and the cap remain incomplete. The owed marker is written before SOUL.md changes and survives a partial refresh or external restart; it is settled only after complete refresh and the ensuing restart (or with no running gateway).
  4. T3 stop: resolved. Buddy's unit-pinned T3 runtime queries all five unfinished statuses, including subagents; only total: 0 permits stop, while no answer waits. The count-to-stop race remains the documented limit.

Blocking new concurrency finding (P2), src/buddy/persona-apply.ts:238-279,462: buddyPersona reads the persona and renders files before acquiring .persona-apply.lock, while writePersona uses a different lock. A write during that wait lets the apply return state: applied for the old persona after the new persona is already current; a subsequent read reports outdated. I reproduced this in the isolated checkout: hold the apply lock, start apply, write a new buddy name, release the lock; the temporary probe passed 1/1 and observed applied/old name followed by outdated/new name. The second apply can answer apply-running, so leaving completion to the first is not safe in this race. Re-read/build after acquiring the apply lock and guard against a persona revision change during application before returning applied; retry or expose pending work. Please add a regression for this overlap.

Local on this exact head: focused Buddy suites 54 pass, 0 fail; lint, typecheck and check:public pass. CI checks green. I did not run the full suite or a real gateway/T3 service. No merge while main is frozen for v0.1.11.

immakermatty added a commit that referenced this pull request Oct 25, 2026
…wers only for the persona still current (DEV-6676)

Pablo's review of #636 at 28d9ce5 (P2): buddyPersona read the persona and
rendered its files before it took .persona-apply.lock, while writers take
.persona.lock. An apply that waited for its turn while a new name was
written answered `applied` for the old name (and could write the old files
over newer ones), the next read said `outdated`, and the writer's own apply
could already have given up as `apply-running`.

Now the apply takes its turn first and reads, renders and applies under it.
Before answering it reads the persona's revision again under the writers'
lock, always the turn first and the writers' lock second (writers never take
the turn, so the order cannot deadlock), and gives up its turn before that
lock. A write that completes meanwhile is either seen at that check and
applied in another round, or completes only after the turn is free, so its
writer's next apply takes the turn and reads it: `apply-running` is safe
again. After three rounds in which the persona changed each time it answers
`waiting` (`persona-changed`), never `applied`.

Tests: the overlap Pablo reproduced (an apply waiting for its turn while a
new name is written, which now applies the new name once, with one restart);
a name written while an apply applies; a persona that changes in every
round; and a second apply answering `apply-running` while the first applies
the name written meanwhile. The three new guards each fail a test when
removed; 70 of 70 guards in all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty

Copy link
Copy Markdown
Contributor Author

Thank you for the review of 28d9ce5. P2 is fixed on the same branch, at exact head 1c37ff5 (ef4bc252 with the code and tests, 1c37ff5d with F64). No review requested and nothing merged.

P2, an apply answering for a persona already replaced (src/buddy/persona-apply.ts, buddyPersona)

  • Read under the turn. The apply now takes .persona-apply.lock (its turn) before it reads anything. It reads, renders and applies under it. An apply that waited for its turn applies the persona that is current when it gets it.

  • Revision check under the writers' lock. Before it answers, the apply reads the persona's revision again under .persona.lock.

    • The order is always the turn first, then the writers' lock. Writers never take the turn, so the two cannot deadlock.
    • The apply releases its turn before the writers' lock.
  • Why apply-running is safe again. A write that completes while an apply runs is either seen at that check, and the apply goes again with it, or it completes only after the turn is free, so its writer's next apply takes the turn and reads it.

  • Bounded retry. After three rounds in which the persona changed each time, the apply answers waiting with persona-changed and never claims applied; the next apply takes it on.

  • Why not the alternatives.

    • I did not hold the writers' lock across the whole apply. That takes minutes (drain, restart, T3 Code's stop and start), and every rename in Nastavení → Buddy would answer busy meanwhile.
    • I also kept a check before the answer, not only a re-read after acquiring the turn. A write during the apply still needs a check before answering.
    • F64 has a new paragraph, "The apply's turn", plus three new rows in its alternatives table.
  • Tests:

    • your overlap exactly: hold the apply's turn, start an apply, write a new Buddy name, release. The apply now answers applied for the new name, applies only it (one restart, not one for each name), and the next read is in-place with the new name;
    • a name written while an apply applies, picked up in another round;
    • a persona that changes in every round: waiting (persona-changed) after three rounds, and the next apply finishes it;
    • a second apply answering apply-running while the first, held mid-way, then applies the name written meanwhile.
  • Mutations: these each fail a test:

    • reading the persona before the turn;
    • no revision check before the answer;
    • applied claimed after the rounds.

    70 of 70 guards in all.

  • Limit (in F64): that the turn is released before the writers' lock is argued, not tested. The window lies between two releases, and no test can stop in it.

Checks:

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

@immakermatty APPROVED on exact head dff4c0c. The P2 overlap from my review of 28d9ce5 is resolved: the apply acquires its turn before reading/rendering the persona, and after each applied round compares the revision while holding the writers’ lock. A changed revision triggers another round; after three changed rounds it returns waiting/persona-changed rather than applied. The turn is released while the writers’ lock is still held, so a later writer cannot finish before the turn is free; writers take only the writers’ lock, preventing a lock-order cycle. The second release is idempotent in acquireFileLock.

I reproduced the overlap regression and the mid-apply, three-round and second-apply cases in the isolated exact-head checkout: 54 Buddy persona/chat tests passed, plus 220 adjacent Buddy tests; lint, typecheck and check:public passed. git diff --check is clean. Current-head CI scope, static checks, both test shards and ubuntu-24.04 are green; zero unresolved review threads. I did not run the entire local suite or a real gateway/T3 service. F64 records the untested release-order window and the bounded-retry limit. This is a review only: no merge, push or release while main is frozen for v0.1.11.

…er, and brings a change to his running conversations when he is idle (DEV-6676, F64)

One source (persona.json) and one deriver (the Platform): src/buddy/persona.ts
reads, checks and writes the persona under its writers' lock and renders
SOUL.md and the gateway's acting drop-in byte for byte as installer
2026-10-24.2/2026-10-25.1 (golden tests from the installer's own renderer);
src/buddy/persona-apply.ts and gateway-refresh.ts apply it in the apply's turn,
re-check the revision before answering, clear the stored prompts of live
conversations when Buddy is idle (an owed-refresh marker until complete) and
restart his gateway once; T3 Code's settings follow the name, stopped only
when T3 Code has no run. lazurio buddy persona [--apply] [--confirm]. Decision
F64. Squashed from the reviewed #636 commits for the rebase onto main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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

@immakermatty APPROVED on exact head 1e2ba63 against main 4a28b43. The cli.ts blob is byte-identical to the previously reviewed #635+#636 integration base 19623db. chat.ts code outside its provider comment is byte-identical to dff4c0c; its comment combines F64 naming with main’s #630 gateway-channel correction. decisions.md is current main followed by the byte-identical F64 section from dff4c0c. All seven other reviewed implementation/golden blobs are byte-identical; buddy-chat.test.ts differs only in the invalid-command cases already on main for #635. The 11-file PR scope is unchanged. Local Buddy tests: 104/104 across persona, chat, CLI, gateway, sign-in and Launchpad suites (plus 4/4 T3 runtime); git diff --check clean. Exact-head CI 40187974141: scope, static checks, both test shards and ubuntu-24.04 pass; macOS check intentionally skipped; zero unresolved review threads. No real gateway/T3 service tested here. Review only: no merge, push or release.

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