Skip to content

Buddy: a change of his Lazurio layer reaches his running conversations (F64, #648) - #652

Merged
immakermatty merged 6 commits into
mainfrom
agent/DEV-6676-layer-prompt-refresh
Oct 26, 2026
Merged

immakermatty merged 6 commits into
mainfrom
agent/DEV-6676-layer-prompt-refresh

Conversation

@immakermatty

@immakermatty immakermatty commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Refs #648
Refs #565 (point 4)

Draft: Platform main is locked for v0.1.12 final. This targets v0.1.13. Plan DEV-6676.

Why

Buddy's stored prompts are built from SOUL.md and from his Lazurio layer in the Folder (buddyLayerPaths: .agents/buddy/.hermes.md and his Lazurio skills). F64 refreshed them only when SOUL.md changed. A Platform release that changes the layer (v0.1.12-rc.2's base-instructions-76, #645) rewrites those files, and nothing clears his stored prompts afterwards. His chat is one endless conversation (F61), so it keeps the old rules.

v0.1.12 ships with this as a known limitation: after that update, someone has to write the owed marker by hand and run lazurio buddy persona --apply. This PR removes that step.

What changes

It follows variant B of the shaping in #565: F64 compares what Buddy's prompts were built from with what the layer holds now. Nothing in src/folder changes, and no Folder template changes.

  1. Record (src/buddy/gateway-refresh.ts). After a complete refresh and the restart that follows it, F64 already removes the owed marker. It now also records the digest of the layer as it was when the apply began, in ~/.local/state/lazurio/buddy/prompt-inputs.json (schema_version, layer_digest).
    • The file is written like the marker: 0600 in 0700, written whole and renamed into place. The record is written before the marker goes.
    • The digest is a sha256 over each file of buddyLayerPaths, in order: its Folder-relative path, then its length and bytes. An absent file and an unreadable one each get a token of their own. Links are never followed.
  2. Owed. promptRefreshOwed(home, folder) is true when the marker is there, or when the layer's bytes differ from the record. A missing record counts too, once.
    • The read without --apply (reported()) and the Launchpad's readings count it and write nothing.
    • applied() turns a mismatch into the marker first. The existing flow does the rest: clear the prompts when Buddy is idle, restart once, record, settle.
    • The marker-only check is now promptRefreshMarked(home). The new required folder argument made every caller a type error until it was updated, so no caller can silently miss the layer.
  3. Trigger (src/buddy/persona-save.ts, src/launchpad/server.ts). Where the recorded entry names Buddy, the Launchpad's persona controller reads once at start (start()) and then every two minutes until it closes (personaWatchMs). Each reading is an ordinary read(), with the same single apply, 15 s spacing, busy retries and failure backoff. The Launchpad's settled() waits for the start reading and the apply it began.
  4. Failure modes.
    • Busy Buddy: waiting, restart pending.
    • Capped or incomplete refresh: failed (prompts-incomplete), still owed.
    • Unreadable layer: owed when it becomes unreadable, and every answer names the new reason layer-unreadable. An apply reports it as failed, which the controller journals and retries. The refresh records the token, so an unreadable layer doesn't restart Buddy again and again, and repairing it owes the next refresh.
    • Not personal, or no Hermes home: skipped or unavailable, as F64 answers now. Nothing is written.
  5. Ended sessions that Hermes resumes are refreshed too (found today on a Ring 0 Buddy). F64 cleared live sessions only (ended_at IS NULL). Hermes ends the web chat's ACP session on every disconnect (end_reason = acp_disconnect), and T3 Code's next message reopens it with the prompt it stored, so the chat kept the stale prompt after a complete refresh. An operator had to clear it by hand.
    • The predicate comes from how Lazurio/hermes-agent v0.22.4-lazurio.3 resumes sessions. A session counts if it is live, or if it is ended, has messages, and its end reason is not one that says the conversation went on in another row or stopped. Those reasons are compression, new_session and the reset reasons in hermes_state_common.py's _BOUNDARY_END_REASONS.
    • Every other end reason, acp_disconnect included, is reopened by the next turn (gateway/session_local_recovery.py, reopen_local_session). The editor's catalog lists no empty session (acp_adapter/catalog.py), which is why sessions without messages don't count.
    • A subagent's run (_delegate_from in model_config) and a tool's session never count. With model_config present, a compression's or a reset's continuation counts, because Hermes resumes a conversation's latest row. A store without these columns keeps the old live, root-only selection.
    • The 500 cap stays, and these sessions count in refreshed and live. repair-prompts <id> clears an ended session exactly as it clears a live one (hermes_cli/sessions_cmd.py).
    • Clearing an ended session's prompt is harmless: the prompt is rebuilt on resume, and the drained restart drops cached agents.
  6. A small fix found on the way. An apply whose owed refresh waits for Buddy to be idle now answers restart: pending, as the read already did. Before, it answered none when no file of its own had changed. That only showed for a marker left behind; after every release it would be the common case.
  7. Docs. The F64 addendum of 2026-10-25 in docs/decisions.md covers motivation, what changes and what stays, alternatives (variant A among them) and Verified by. The CLI help of lazurio buddy persona and the Delegace README note about Buddy's Lazurio layer in the Lazurio Folder: .hermes.md, Buddy skills, the Buddy section for all agents and the wake-up service (root 0210) #565 point 4 are updated. docs/launchpad-development.md and docs/ have no F64 or Buddy-persona description to update.

Beyond the shaping: the reading every two minutes

The shaping assumed that the Platform update's Launchpad restart comes after the layer change. On a Remote Environment it doesn't.

  • Machines' apply raises the Platform first, and that restarts the Launchpad.
  • Only then does it run lazurio machine folder-refresh (docs/machine-handover.md). That writes the new layer and restarts nothing, because the entry didn't change.
  • The user's own lazurio update never writes the Folder (F14).

So the start reading alone sees the old layer, and #648's acceptance ("without a manual step") would fail on Ring 0. The periodic reading fixes that. It also picks up a marker that a CLI --apply left when it had to wait for Buddy.

It is its own commit (e680002), so it can be dropped if a different trigger is preferred. The addendum's alternatives table lists two candidates: folder-refresh restarting the Launchpad, and a file watch.

What doesn't change

  • src/folder and the Folder templates.
  • The owed marker, its schema and its rules.
  • A persona change owing its refresh.
  • The installer's and Machines' hand-over (F64 point 7).
  • Environments whose entry doesn't name Buddy. The Launchpad reads nothing of Buddy there.

Verification

  • bun run check:static on 0f196b6: pass (biome, tsc --noEmit, the public guard over 1079 files, the proof build and smoke).
  • bun test on every test file that imports src/buddy, plus tests/launchpad-chat.test.ts, which starts a Launchpad with a Buddy entry and the real runner: 205 pass, 0 fail across 13 files on 0f196b6. The baseline before this change was 194 pass.
  • bun test (whole suite) on 895c3f7: 4959 pass, 10 skip, 1 fail across 326 files. The failure is tests/recover.test.ts "the supervised unit through systemctl --user, and its health socket", which timed out at 5 s while other suites loaded the machine. That file doesn't touch Buddy or the Launchpad and passes 3 of 3 when run alone. The two later commits change only the Buddy refresh selection and docs, which the 205 tests above cover.
  • New tests:
    • tests/buddy-persona.test.ts: a real refreshMachineFolder that rewrites .hermes.md owes the refresh, and one that changes nothing owes none (Buddy: a Folder render that changes his rules leaves the owed prompt refresh, so open conversations follow them (#565 point 4) #648's acceptance test). The read reports it and writes nothing. A busy apply writes the marker and waits. An idle apply clears, restarts once and records. The next apply does nothing.
    • Also in tests/buddy-persona.test.ts: a missing record owes once and then stays settled (the record's mode and members are checked). Bad records count as none. A layer changed during the apply stays owed, and the beginning's digest is recorded. An unreadable layer (a link) follows the rules above. The digest is checked by path and bytes, including a one-byte change at the same length.
    • Also in tests/buddy-persona.test.ts, for ended sessions: an apply clears an ended acp_disconnect session with messages and a prompt, and counts it in refreshed. An ended session with no prompt stays untouched. The selection takes live rows, the web chat after a disconnect, compression and reset continuations, and a closed CLI chat. It never takes rows ended by compression, a daily reset or /new, empty ones, subagent runs or tool sessions, and a model_config that isn't JSON breaks nothing.
    • tests/buddy-persona-save.test.ts: read() starts an apply on a mismatch without writing. start() reads once and waits for its apply. The two-minute readings pick up new rules and a marker, and stop at close().
    • tests/launchpad-buddy.test.ts: a real Launchpad start with a Buddy entry runs F64's apply without any request when his rules changed. It runs none when nothing is owed or the entry doesn't name Buddy.
  • Mutation checks: each of 30 guards, removed or loosened, fails a test. One more guard, a sha256 shape check of the recorded digest, survived and was removed as redundant, because a value that isn't a layer's digest never equals one.

How Ring 0 verifies it on a real Buddy

On a Ring 0 Buddy, after a Platform update (with this change) whose release changes .hermes.md, with no Launchpad page opened:

  1. Before the update, note his conversation's stored prompt: hermes sessions shows it as held.
  2. Let Machines' apply run (Platform raise, then machine folder-refresh). Within about two minutes of the folder refresh, once Buddy is idle, the Launchpad's journal shows a {"scope":"buddy-persona","operation":"apply",...} line. hermes sessions shows the web chat's stored prompt cleared, also when its ACP session is ended after a disconnect, and hermes-gateway.service restarted once.
  3. ~/.local/state/lazurio/buddy/prompt-inputs.json holds the digest of the new layer, and prompt-refresh-owed.json is gone.
  4. His next turn follows the new rule, and hermes sessions shows the prompt held again, rebuilt.

The first update with this change refreshes every Buddy once, because none has a record yet.

Risks

  • One extra refresh per Buddy on the first update: one prefix-cache miss per conversation that can still take a turn, and one restart, only at an idle moment.
  • Background reads: every two minutes, a Launchpad on a personal Environment with Buddy reads his persona and a handful of small files. Nothing else runs unless something is owed.
  • More sessions cleared: ended sessions Hermes resumes are now cleared too, at the cost of one repair-prompts process each. Superseded rows (compression, resets, /new), empty sessions and subagent runs are excluded, so old Telegram days and the 15-minute empty ACP sessions don't add to it. The end-reason list is Hermes's at v0.22.4-lazurio.3. A reason Hermes adds later counts as resumable, which costs at most one needless clear.
  • layer-unreadable exit code: an apply that names it is failed, so lazurio buddy persona --apply exits 1 while a layer file is a link or a directory. The Folder's own writer refuses such a file too, so this shouldn't happen on a healthy Environment.
  • Not run on real infrastructure: no real Hermes, Buddy or Remote Environment ran. hermes, systemctl and the gateway are fakes with recorded shapes, as for F64. Acceptance is on Ring 0.

Follow-ups

  • Merge only after v0.1.12 final, by the release owner's schedule for v0.1.13.
  • Ring 0 acceptance as described above.

🤖 Generated with Claude Code

Note

Detect Buddy Lazurio layer changes and refresh prompts in running conversations (F64)

  • Buddy now computes a SHA-256 digest of the Folder's Lazurio layer files and records it after each completed refresh. A refresh is owed when the digest changes or the marker exists. See gateway-refresh.ts and persona-apply.ts.
  • Prompt refresh selection now includes resumable ended conversations (e.g. ACP disconnect), excluding subagent and tool sessions.
  • Launchpad now starts the persona controller in the background for Buddy entries and re-checks every two minutes; persona startup failures do not fail Launchpad startup. See server.ts and persona-save.ts.
  • Layer files that are symlinks, non-regular, oversized, or unreadable are reported as a new layer-unreadable reason instead of failing the operation.
  • State files (marker and digest record) are now written atomically via a temporary file with restrictive permissions.
  • Behavioral Change: promptRefreshOwed now requires a Folder path argument, and applies record the layer captured at apply start, so layer changes during an apply stay owed for the next round. Documented in decisions.md.

Macroscope summarized 0f196b6.

immakermatty and others added 6 commits October 25, 2026 19:26
…mpts (F64)

F64 refreshed Buddy's stored prompts only after a persona change. A Platform
release that rewrites his Lazurio layer in the Folder (.agents/buddy/.hermes.md
and his skills, written by any of the Folder's entry points) left his open
conversations on the old rules (#648, #565 point 4).

A complete refresh now also records the digest of that layer as it was when
the refresh began, in ~/.local/state/lazurio/buddy/prompt-inputs.json beside
the owed marker (0600 in 0700, written whole and renamed). A refresh is owed
when the marker is there or the layer's bytes differ from the record; no
record counts once. The read without --apply and the Launchpad's readings
count it and write nothing; an apply turns it into the marker, and the
existing flow clears the prompts when Buddy is idle, restarts once and
settles. A layer file that is no regular file counts by its own token and is
named layer-unreadable. Nothing in src/folder changes.

An apply whose owed refresh waits for Buddy to be idle now reports the
restart as pending, as the read already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is applied without a page

Where the recorded entry names Buddy, the Launchpad's persona controller now
reads once when the Launchpad starts, in the background (start()), and the
Launchpad's settled() waits for that reading and the apply it started. A
refresh owed while no Launchpad ran, the owed marker or a Lazurio layer his
last complete refresh was not built from (F64's addendum of 2026-10-25), is
then applied once Buddy is idle, without anyone opening a page.

Without an entry, or with an entry that does not name Buddy, nothing is read.
The start never waits for the apply and never fails on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er refresh after the update reaches him

The reading at start sees Buddy's Lazurio layer as it was when the
Launchpad restarted. Machines' apply raises the Platform first, which
restarts the Launchpad, and only afterwards runs `lazurio machine
folder-refresh`, which writes the new layer without restarting the
Launchpad again (docs/machine-handover.md). A release's new rules would then
wait for someone to open a page. So after start() the controller reads again
every two minutes (personaWatchMs), as often as the Launchpad asks an
Organization's settings, until close().

A reading that finds nothing owed reads a handful of small files and starts
nothing. An apply it starts is the one any reading starts, with the same
retries and backoff. It also takes up the marker a `lazurio buddy persona
--apply` leaves when it had to wait for Buddy.

The reading at start now begins once the Launchpad's holds are reconciled,
beside its other background setups, so a start that fails before that point
leaves no apply behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eaches his conversations

Records the decision behind the three previous commits: what a complete
refresh records, when a refresh is owed, how the Launchpad applies it on its
own, what fails and how it stays visible, the alternatives (variant A of the
shaping in #565 among them) and how it is verified. It states that v0.1.12
ships without the fix as a known limitation: after that update the owed
marker has to be written by hand and `lazurio buddy persona --apply` run.

The Delegace README no longer sends a reader to clear stored prompts by hand
on a Platform with the fix.

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

F64 cleared the stored prompts of live sessions only (ended_at IS NULL).
Hermes ends the web chat's ACP session whenever its last viewer leaves
(end_reason acp_disconnect), and T3 Code's next message reopens it with the
prompt it stored, so after a complete refresh the chat kept the old one;
on Ring 0 it had to be cleared by hand with hermes sessions repair-prompts.

The refresh now takes every conversation that holds a stored prompt and can
still take a turn, as Lazurio/hermes-agent v0.22.4-lazurio.3 resumes them.
That is a live row, or an ended one with messages whose end does not say its
conversation went on elsewhere or stopped (compression, new_session and the
reset reasons of hermes_state_common.py); any other end, acp_disconnect
included, is reopened by the next turn (gateway/session_local_recovery.py).
A subagent's run (_delegate_from in model_config) and a tool's session never
count. With model_config, a compression's or a reset's continuation counts,
since a long conversation goes on in that row. A store without these columns
keeps the installer's live, root-only selection. The cap stays, and these
sessions count in refreshed. repair-prompts clears an ended session by its id
as it clears a live one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds point 5 to the F64 addendum of 2026-10-25: why the refresh takes ended
sessions with messages whose end reason Hermes reopens (acp_disconnect
among them) and never a compression, a reset boundary, a subagent's run or a
tool's session, read from Lazurio/hermes-agent v0.22.4-lazurio.3. It also adds
three alternatives (every ended session, ACP only, Hermes's local receipts),
the tests and mutation checks, and two honest limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty
immakermatty marked this pull request as ready for review October 25, 2026 22:12

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

Exact-head QA on 0f196b6 against main 8571aa7 (v0.1.12-rc.2): approved for review, not for merge during the v0.1.12 final freeze.

I traced buddyLayerPaths into the ordered path/length/bytes digest and its absent/unreadable tokens, the no-follow bounded reads, read-only owed comparison, and the beginning-of-apply snapshot recorded only after successful prompt clearing and gateway restart, before the marker is removed. A layer changed during apply stays owed. The Launchpad only arms the two-minute reading for an entry naming Buddy; close cancels it, and the controller coalesces applies, retries busy and backs off failures. This catches Machines folder-refresh after Launchpad restart without changing Folder writers.

Against Lazurio/hermes-agent v0.22.4-lazurio.3 I checked sessions columns, the ACP disconnect/end and local reopen-on-turn paths, the compression/reset boundaries and child/tool markers, and targeted repair-prompts clearing by ID. The 500 cap leaves an incomplete refresh owed, and a busy owed refresh reports restart: pending. Local evidence: check:static passed and the focused 13 Buddy/Launchpad files passed 205/205 on Bun 1.4.4 (project manifest pins 1.4.2); PR CI checks are green. No real gateway or Remote Environment was exercised here: the stated Ring 0 post-update acceptance remains required. No merge/release performed.

@immakermatty
immakermatty merged commit b5ecc58 into main Oct 26, 2026
7 checks passed
immakermatty added a commit that referenced this pull request Oct 27, 2026
…nal v0.1.13 (DEV-6656, DEV-6671) (#668)

The canary of v0.1.13-rc.1 ran 8 h 2 min 4 s directly observed (2026-10-26T13:53:42Z to 21:55:46Z) on all 5 Environments of the pilot Organization and one personal VM. Every Machine ended healthy on the candidate with NRestarts 0, the journals show only the deliberate G.3 restarts, and no Recovery issue names the candidate.

G.3 covered:
- Folder refreshes and deliberate Launchpad restarts;
- module starts, including the planning app through start_script;
- the one lazurio update through Nová karta;
- #652's prompt refresh on the personal VM (on update and on a one-byte layer change, reverted);
- the Buddy acceptance.

main moved past the candidate (#661 and later merges), so the final v0.1.13 is tagged on release/v0.1.13: this merge plus one commit that restores the candidate's tree outside qualification/ and docs/evidence/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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