8000 Tags · devswha/ovis · GitHub
[go: up one dir, main page]

Skip to content

Tags

Tags

beta/v0.5.2-beta.15

Toggle beta/v0.5.2-beta.15's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(usage): read only the Hub account shown again, and keep hidden ac…

…counts unread after a sign-in (#3364)

Fixes #3361.

## What
These are follow-ups to #3355 (the usage report and Hub rows):
- **Showing a hidden Hub account again reads only that account.** Every
other row, and its schedule, is kept. Before, the pass after an unhide
re-read every Hub account at once, bypassing `FRESH_MS` and
`MIN_REFRESH_MS` (`a,b,a,b`; now `a,b,a`).
- The next scheduled or forced read skips an account read on its own
less than `MIN_REFRESH_MS` ago.
- A failed read after an unhide keeps the cached rows instead of
emptying them.
- **A same-provider sign-in keeps the provider's hidden accounts
unread.** `invalidate(provider)` keeps hidden accounts' entries, with
the hidden set taken from the current preferences, so an account hidden
since the last report counts too. The account check from #3355 still
re-reads a hidden place that now holds another account.

## Validation
- Failing-first tests:
  - `server/cliproxy.test.ts`:
- "reads only the Hub account shown again, keeping the other accounts'
fresh rows"
- "keeps every Hub row when the read after an account is shown again
fails"
- "does not read an account shown again a second time at the scheduled
read that follows"
  - `server/usage.test.ts`:
- "keeps a provider's hidden accounts unread when its own sign-in ends"
- "keeps an account hidden since the last report unread when its
provider's sign-in ends"
  - Each failed before its fix.
- `bun run check fast`: passes, with unit tests at 13237 pass, 0 fail.

## Review
A Codex adversarial review (gpt-6.1-terra, xhigh, route: axwith) ran
before this PR opened. Its 3 MEDIUMs are fixed in the second commit: the
hidden set at sign-in, the failed partial read, and the second read of
the shown account. It found no HIGH.

## Not covered (follow-ups)
- A fresh server still reads a hidden account once, so its row can be
listed; changing that is a product decision about what an unread row
shows.
- The Hub's own `invalidate()` uses the last report's hidden set.
- `gh auth token --user <hidden>` still runs every `FRESH_MS`, which
would need a `signIns` interface change.

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

beta/v0.5.2-beta.14

Toggle beta/v0.5.2-beta.14's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(status): keep a Claude rest when its last background task ends, a…

…nd refuse a linked subagents folder (#3360)

Fixes #3348.

## What
In `server/claude-subagents.ts`:
- **The last background task ending no longer brings back a stale
`working`.** When the last background task ended (subagent, Bash command
or Workflow) while herdr's last report was still `working`, the running
count dropped to 0. `restsOn()` turned false, and herdr's old `working`
came back even though the transcript said the turn was at rest. The pane
now keeps the transcript's rest. That holds across a same-session
re-lookup and clears on a reset. A newer herdr `working` event or a new
turn still makes the pane work again.
- **A linked folder is refused, and the listing is bounded.** The
`<session>/subagents` reader followed a linked folder, so another
session's records counted for this pane, and it read the whole directory
before keeping 5,000 names. The listing now opens the folder through the
same checked opening as the detail read (`openFolder`), refuses a linked
`subagents` or `<session>` folder, lists nothing if a folder on the way
is swapped, and stops at the cap. `subagentDetails()` (the chat cards'
titles) goes through the same checked folder.

## Validation
- Failing-first tests, each of which failed on main or on the first
commit:
- rest kept at the pane-status level, returned snapshot included, after
the last task ends
  - rest kept after a same-session re-lookup
- linked `subagents` and linked `<session>` folders, through both the
listing and `subagentDetails()`
  - a folder swapped before and after its open
- Guard tests: a herdr `working` event after the last task ends, with
and without a poll first, and a next turn written late. Each one makes
the pane work again.
- **One existing #2072 test changed:** it expected `working` in exactly
the case this issue calls a bug (the transcript at rest, herdr's only
`working` before the child ended). It now expects rest.
- `bun run check fast` passes on current main (e450d27).

## Review
A Codex (gpt-6.1-terra, xhigh) adversarial review ran before this PR
opened.
- **Fixed in the second commit** (three MEDIUMs):
  - the rest flag was lost on a same-session re-lookup
  - a linked `<session>` folder was still followed
  - `subagentDetails()` skipped the folder check
- **Left as is:**
- LOW: the 5,000-entry window is taken before names are filtered, so in
a huge folder an active agent past it can go uncounted. This predates
the change.
- After the listing, each agent's files are read by path, so a swap
between the listing and those reads is not anchored.

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

beta/v0.5.2-beta.13

Toggle beta/v0.5.2-beta.13's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(usage): stop reading the sign-ins of accounts hidden from Plan li…

…mits (#3355)

Fixes #3333.

## What
With Plan limits on, hiding or removing accounts in Settings → Agents
was display-only. Ovis still asked every provider for its sign-ins on
each refresh, which ran `gh auth token --user <login>` for every
github.com account, including accounts with no Copilot plan. Endpoint
security can flag that as credential access. Now:

- **Fully hidden providers aren't asked.** A provider whose every known
account is hidden or removed is no longer asked for its sign-ins. Its
last rows stay as they were, and no `gh auth token` runs for it.
- **Hidden Hub accounts aren't used.** The Hub (CLIProxy) is no longer
asked to use the credential of a hidden or removed account that already
has a row. An account it has never read is still read once, so it can be
listed.
- **A sign-in clears only its own provider.** A sign-in that ends
invalidates only that provider's cache. Before, any sign-in (for example
Claude's) cleared every provider, and the next report ran `gh auth
token` for a fully hidden Copilot again.
- **Showing an account again reads it at once.** Its provider is looked
up and read immediately, and rate-limit backoffs are kept.
- **A provider with any visible account** is still searched as before,
so new accounts are still discovered there.

No OAuth refresh is added: expired tokens are still reported and left to
the CLI.

## Not covered
Accounts that have no plan for a provider (for example github.com
accounts with no Copilot plan) are never listed, so they cannot be
hidden. A provider whose only accounts are such unlisted ones is still
searched on each refresh. Stopping that needs a per-provider on/off
switch (the issue's option 2), left for a follow-up.

## Validation
Failing-first tests, each of which failed before its fix:
- **`server/usage.test.ts`:**
  - A fully hidden provider is not asked for sign-ins.
  - A mixed hidden/visible provider still is.
- A hidden Copilot account stays unread when another provider's sign-in
ends: `gh auth token` ran twice before, once after.
  - Showing an account again looks its provider up at once.
- **`server/cliproxy.test.ts`:** the Hub is asked nothing for hidden
accounts and their last rows stay, until one is shown again. It counts
`/requests/api-call`.
- **`server/usage-account-list.test.ts`:** showing an account in
Settings starts the search.

`bun run check fast` passes on current main (e450d27).

## Review
A Codex (gpt-6.1-terra, xhigh) adversarial review ran before this PR
opened. Its three MEDIUMs are fixed in the second commit:
- the Hub path still used hidden accounts' credentials
- an unrelated sign-in cleared every provider
- showing an account didn't refresh it

**Left as LOW:**
- While a provider is skipped, a removed account's row can go stale; for
example, it is still listed as removed after being signed out of its
CLI.
- Each provider is still looked up once after a server restart, before
the hidden list applies to a cached inventory.

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

beta/v0.5.2-beta.12

Toggle beta/v0.5.2-beta.12's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(letta): claim on a peer record gone only for a moment or rewritte…

…n while read (#3357)

Fixes #3356.

## What
This closes two fail-opens left in `server/letta-store.ts`
`folderClaims` (the peer-record check from #3351):
- **A record gone only for a moment.** An ENOENT/ENOTDIR at the open
counted as "no record". Now the record's path is checked again with
`lstat`. Only a record still gone claims nothing; anything back at the
path is a claim.
- **A record rewritten in place during the read.** The opened handle is
stat'ed again after the read. A short read (`bytesRead !== size`), or a
size or `mtimeNs` different from the first stat, is a claim.

## Validation
- Failing-first tests in `server/letta.test.ts`, each run with and
without `/proc`:
- **".letta gone just as it is opened, or a file in its place, and back
at once, is still a claim":** an ENOENT and an ENOTDIR variant. A
`.letta` that stays gone still claims nothing.
- **"a peer's record rewritten in place while it is read is still a
claim":** short-read, size and mtime variants.
- All five variants failed on main, in both modes, and the file passes
65/0 with the fix.
- `bun run check fast`: green on the final tree, unit tests 13146 pass,
0 fail.
- The changelog line is added to #3351's unreleased fragment
`changelog.d/letta-record-scan.md`, which describes the same behaviour.
- **Not covered here (the issue's LOWs):** the `fsp.stat` spy that never
fires in Bun, and the one fs-pool slot held per stuck peer folder.

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

beta/v0.5.2-beta.11

Toggle beta/v0.5.2-beta.11's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(machines): update any PC from here, one or all, over relay, dire…

…ct or SSH (#3309)

## What

Every PC added here is now updated from the PC in use. This covers PCs
reached through the relay (direct or relayed) and over SSH. Nobody has
to go to that PC.

- **Badge on the roster.** `Machine.ovis_update` is the update a PC can
take from here, told unasked:
- `own`: the PC's own Ovis offers a newer release. A connected relay PC
or SSH-own-Ovis PC is asked through its connection (`GET /api/updates`)
when it connects and then every 5 minutes.
  - `bridge`: this app's bridge on that PC is of another version.
- When an update is offered but cannot run from here, `updatable: false`
comes with a plain-words `reason`.
- **One per-PC route.** `POST /api/machines/:id/update` (202 `{ job_id
}`) works whichever way the PC is reached.
- A connected PC's own Ovis is asked to install the release it offers,
by revision (no redirect followed). It is followed through its restart
and reconnect until it answers on that release (`server/pc-update.ts`).
  - Anything else runs the existing bridge update.
- When nothing can run, the answer is 409 `update_unavailable` with the
same words.
  - `DELETE` ends the wait.
- **One click for all.** `POST /api/machines/update-all` returns `{
started, queued, skipped }`. Own updates run side by side. Bridge
updates run one after another, sharing one bundle download, and show
`updating.queued` until their turn.
- **Failures in plain words.** `Machine.update_error` keeps why a PC's
last update failed. It is cleared when the next one starts.
- A relay PC no longer has to be updated on that PC. The relay device
this server paired as already drives, and the test shows its install
goes through.

Contract: `shared/machines.ts` (`PcUpdateOffer`, `PcUpdateStarted`,
`PcUpdateAll`, `MachineUpdate.queued`). Docs: docs/remote-pcs.md,
"Updating PCs from here". This is the server/contract half; the
devices-list buttons come in a separate UI change.

## Who may do this (contract change)

One guard covers every change to this PC's PCs, and this PC's own
updates: every `POST`, `PATCH` and `DELETE` under `/api/machines/`
except a PC's own proxied routes (its panes, workspaces and files), plus
`POST /api/updates/install` and `POST /api/herdr/update`. The rule is
`ownerAccess` (`level === "full" && role === "drive" && via` not `open`
or `portal`) and `updateRequestAllowed` (`X-Herdr-Update: 1`, same
origin, no query). Anything else gets 403 `forbidden`. The connection
server's bridge token still asks for its own install by revision.

| May | Who |
|---|---|
| Yes | a driving sign-in on this PC, its token, its Tailscale login |
| Yes | a driving device paired with this install, directly or over the
relay (the owner's phone, his other PCs) |
| No | a visitor of an open install, with nothing paired |
| No | a Portal visitor, whatever role Portal gives |
| No | any device that only watches |

Before this PR, the existing `/api/machines` changes (setup and approve,
settings, enable, remove, update-bridge, relay and Tailscale adds) took
any driving caller, the open LAN and Portal included, and
`/api/updates/install` and `/api/herdr/update` took any authenticated
caller with the header. `server/machine-owner-guard.test.ts` runs every
route against every kind of access, with and without the header. The app
sends the header on every PC request (`machineRequest`), and the
sidebar's update buttons send the release the card shows.

## Validation

- `bun run check fast`: passed (typecheck, build, unit 13009 pass / 0
fail).
- New: `server/pc-update.test.ts` (offer reading, following an install
through a restart, failures in its own words), and 3 relay contract
tests in `server/relay-pc.contract.test.ts` (two real servers through a
local relay, with a stand-in updater on the far PC). Extended:
`server/machine-own-update.test.ts` (the SSH badge, the one route,
refusals and `update_error`).
- Skipped: browser regressions (no UI change); integration and browser
CI run on GitHub.

beta/v0.5.2-beta.10

Toggle beta/v0.5.2-beta.10's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(letta): check another pane's Letta record off the event loop and …

…fail closed (#3351)

Refs #3326 (item 2). Item 1, the long-line tool-output scan, went back
to #3326 after the merge pass measured 57-108 ms of blocking at its 8
MiB line cap; see
#3326 (comment).
Item 3 needs herdr or hc to report a process start time.

## What
In `server/letta-store.ts`, the check that another Letta pane's record
doesn't claim the same conversation:
- **Runs off the event loop.** It uses `fs/promises`, including
`lettaRecordOwned`'s peer-folder resolution. Every peer wait has a
deadline, and a peer that misses it counts as a claim.
- **Can't be fooled by links or swaps.**
- The peer folder, its `.letta` and the record are opened through
descriptors held along a chain of plain folders, checked by device and
inode before and after the read. Where there's no `/proc` (macOS,
Windows), the record is opened with `O_NOFOLLOW` and the folder chain is
checked the same way.
- The opened record's `fstat` must match an `lstat` of its path taken
after the read. Any mismatch counts as a claim, and so does an error or
the file vanishing, so the check fails closed.
- A `realpath` error other than ENOENT/ENOTDIR counts as a claim; only a
folder known to be gone claims nothing.
- **Re-checks before binding.** After awaiting peer folders, it reads
the pane's own record again and asks herdr about the pane's Letta again:
the same pid, start identity and cwd. It binds nothing if either
changed.

## Validation
- Failing-first tests in `server/letta.test.ts`, run in both `/proc` and
no-`/proc` modes where it applies:
- link and decoy swaps, put back or not, of the peer folder, its
`.letta` and the record
- a decoy opened while the original record is removed (the fail-closed
check)
  - an unresolvable peer folder (EACCES/EIO)
- a peer that never answers, and a hanging `realpath` through
`resolveLettaTranscript`
  - the pane's own record rewritten during the wait
  - the pane's Letta exiting or moving during the wait
- `bun run check fast` passes on current main. The one failure,
`scripts/check.test.ts`, was load-flaky and passes 78/0 when run alone.

## Review
- A Codex (gpt-6.1-terra, xhigh) review ran before this PR opened, and
later rounds addressed:
  - CodeRabbit's five threads
- dori's findings: the non-`/proc` path and the deadline on
`lettaRecordOwned`
- the merge pass's HIGHs: the `fstat` identity check, and failing closed
when the record vanishes
- **Behaviour change:** a peer record that exists but can't be read or
proven now blocks binding a pane that has no report, where before it was
ignored.

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

beta/v0.5.2-beta.9

Toggle beta/v0.5.2-beta.9's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(relay): end a pairing host on revocation or key removal, and stop…

… per-second alarms in grace (#3346)

Fixes #3307.

## What
In `relay/pair-room.ts`, a typed-code pairing host:
- **Is re-checked against the relay's current settings.** Its attachment
now keeps the pass's key id and server id next to the expiry. `lapsed()`
closes the host with 4402 `relay pass invalid` once the PC is revoked or
its signing key is removed. Before, an admitted host outlived both until
its pass expired or its 11-minute deadline passed.
- **No longer fires an alarm every second during grace.** Before
enforcement, `passDue()` returns only the enforcement time, so an expiry
already in the past doesn't re-arm the alarm.
- **Hosts from before this deploy are kept.** A host admitted by the
previous version, whose attachment has only `pass_exp`, keeps its expiry
check but is not closed for missing the new fields. Hibernating sockets
survive a deploy, and those hosts end at their 11-minute deadline
anyway.
- **README:** `relay/README.md` now says that revocation applies at the
next frame, arrival or alarm that the new settings reach, and that a
Durable Object may keep running the previous version for a few minutes
after a deploy.

These cases matter only once the relay enforces passes; it does not yet.
Merging deploys the relay (`relay-deploy.yml`), which drops every socket
once.

## Validation
- Failing-first tests in `relay/worker.test.ts`. Each failed on main and
passes now:
  - revocation or key removal ends the host at the next guest's arrival
  - the grace alarm is no longer set one second ahead
  - a host restored from a main-shaped attachment stays open
- More tests:
- revocation with a guest already connected, ended by its next frame
and, in a second case, by an alarm
- a valid host crossing `enforceAt`, with the next alarm asserted before
and after
- Relay test files run one at a time: 160 pass, 0 fail.
- `bun run check fast` passes on current main (eb584a0), with the unit
suite fully green.

## Review
A Codex (gpt-6.1-terra, xhigh) adversarial review ran before this PR
opened. It found:
- MEDIUM, fixed: hosts from the previous version would be closed after a
deploy.
- MEDIUM, documented in the README: after a deploy, an object can keep
the old policy for a few minutes.
- LOW, fixed: the existing-guest and enforcement-crossing tests above.

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

beta/v0.5.2-beta.8

Toggle beta/v0.5.2-beta.8's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(letta): Letta status follow-ups from review (#3334)

## Summary
Part of #3332: follow-ups to #3331 (Letta status where no report gives
one) from its reviews.

- **Refresh key only for Letta panes:** `lettaRefreshKey` returned a key
for every pane, so on herdr-core a title change in a shell, an editor or
another agent forced a new Letta lookup and bypassed the 5 s interval.
Panes that aren't Letta's now give an empty key.
- **Blocked kept in a late read:** in a snapshot read that finished
after a newer one, a file-tracked pane waiting for approval kept the
file's `working`. It now keeps the `blocked` the newest read gave it.
- **Tracking kept through a replay:** after a subscription gap, the
collector replays the snapshot's status, which is Ovis's own Letta
status. `onStatus` took it for a report and ended tracking. Now only a
live event, or another agent, hands the pane back.
- **A denied approval told as a failed rest:** while the title asked for
approval, the file's changes were dropped, including the reason for a
rest. An aborted or errored answer written meanwhile is now kept and
passed on when the wait ends, so the end is told as a failed rest, not a
finish.
- **The checked file is the one opened:** the messages file's path is
checked, then opened with `O_NOFOLLOW`. The opened file's device and
inode must now match the checked one, so a folder swapped for a link in
between opens nothing.
- **A status event that says nothing:** for an unreported Letta pane on
herdr-core, an `unknown` event now takes the status its own title last
showed, instead of overwriting the work it showed.

Still open in #3332, each needing more than a small change: a false
`done` when an approval starts during a snapshot; turn continuity when a
brief report takes the pane; title-only turns shorter than the
collector's 500 ms snapshot delay; checking that a spinner title is
Letta's own.

## Validation
- `bun run check fast`: ok (typecheck, build, unit 13084 pass).
- Failing-first tests: `server/letta-status.test.ts` (a title change in
another pane, a late read) and `server/core/letta-status-hub.test.ts` (a
replay, a denied approval, an `unknown` event). The check-then-open race
can't be hit deterministically. The link and FIFO refusals stay tested.

beta/v0.5.2-beta.7

Toggle beta/v0.5.2-beta.7's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(letta): show Letta's status where no report gives one (#3331)

## Summary
On herdr-core, a Letta Code pane is named (#3308) and its chat opens
(#3324), but its status stayed `unknown`: Letta sends no OSC 7501
report. This gives Letta panes a status where no report gives one, from
facts Letta writes itself, never read off the screen:

- **Local conversations:** a new tracker (`server/letta-status.ts`)
reads the conversation's `messages.jsonl` in Letta's local backend,
which Letta appends to during the turn. Its transcript is written only
when the turn ends, so it can't show work in progress. The records have
the same shape as gjc's transcript, so gjc's reducer and the existing
session-file tracker are reused: a prompt, a tool call or a tool result
means working; `stop` or `length` means at rest; `error` or `aborted`
means a failed rest. It is wired into `server/core/status.ts` next to
gjc's, and applies only to Letta panes whose status is `unknown`.
- **Cloud conversations, or panes without a bound conversation:**
Letta's own terminal title (`terminal_title` only) carries a braille
spinner frame while a turn runs (`⠋ Tutor`) and the plain name at rest
(`titleStatus()` in `server/agent-identity.ts`).
- **Waiting for approval:** in Letta's `standard` permission mode, a
tool call waiting for the user's approval shows only as the pending call
in `messages.jsonl`. The titl
7C1B
e reads `[ ! ] Action Required | <name>`,
so the pane shows `blocked`, even when its messages file is tracked. It
applies only where no report and no tracked messages file gave a status.
The herdr-core adapter sends `pane.updated` when that spinner starts or
stops, and a title-derived turn goes through the normal status path, so
a finished turn can alert.
- **Handing a pane back:** a status the program reports, or another
agent starting in the pane, takes it back from Letta's files at once.
Rediscovery runs at gjc's 5 s, and earlier when the title's name or mark
changes.

## Verification
On a Linux PC with herdr-core (hc 0.1.17, an isolated HOME and
herdr-core session) and letta-code 0.34.44, while Letta ran a 6-second
tool call (last checked at this branch's final code):
- **Local conversation:** `idle` → `working` (t1 to t8, matching Letta's
spinner) → `done` (t9 onward).
- **Pane with no bound conversation:** a Letta pane started in its home
folder, where no conversation is bound, so only the title applies. It
showed `idle` → `working` (8 s) → `done`.

## Validation
- `bun run check fast`: ok (typecheck, build, unit 13075 pass).
- Tests: `server/letta-status.test.ts` covers the reducer on one real
turn of letta-code 0.34.44's `messages.jsonl` (fixture), discovery (only
unknown Letta panes; cloud conversations get no file), and the title
fallback (first look tells nobody; changes are told; a reported or gone
pane is forgotten). `server/agent-identity.test.ts` covers
`titleStatus`, and `server/herdr-core/adapter.test.ts` covers the
spinner title event.

## Known limits
- A title some other program left behind could be read as Letta's before
Letta writes its own. This is limited by the Letta-only and
still-`unknown` gates, but not ruled out.
- A title-only turn shorter than the collector's 500 ms snapshot delay
can pass unseen. Spinner changes reach the rows as `pane.updated`, not
as status events.
- A report or another agent that arrives while the first discovery of a
pane is still running can be overtaken by it once, until the next
refresh (every second) hands the pane back.

Review notes: Codex reviewed this branch twice before it opened. All
findings from the first pass are fixed. From the second pass, three
MEDIUMs are fixed: a title turn found running now counts as work; the
title goes on from the file's last status when the file stops being
tracked; a report with an unrecognised word no longer stops title
updates. The other two are the limits above.

beta/v0.5.2-beta.6

Toggle beta/v0.5.2-beta.6's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(letta): open Letta's chat from its own record when no report name…

…s the conversation (#3324)

## Summary
On herdr-core, Letta Code's pane was named after #3308, but its chat
still showed only the terminal. Letta's chat binding needs a session
report (`herdr:letta`), which only herdr's Letta integration sends.
herdr-core sends none, and neither does herdr without the integration.

With no report, `lettaTranscriptFor` now binds the one conversation
Letta's own project record (`.letta/settings.local.json`) names. Without
a report, the record's age is the only thing that backs it, so more must
hold:
- the letta was started plainly, or with only model, backend, toolset or
system-prompt options. Plain `letta` resumes the conversation the record
names; any other option (`--new`, `--resume`, `--agent`, `--name`, an
unknown flag) leaves the chat on the terminal;
- Letta saves its sessions: `LETTA_DISABLE_SESSION_PERSIST` is not set
to anything, and the process isn't a subagent
(`LETTA_CODE_AGENT_ROLE=subagent`);
- no other Letta pane goes without a report: it might have moved here
from a folder with the same record;
- the transcript was written after the process started, which shows this
process's own use of the conversation; before that, the chat stays on
the terminal;
- the record names exactly one conversation, and no other Letta pane's
folder record names the same one. This holds even when that pane's own
report names a different one, because a report can predate a `/resume`.

The existing checks still apply on top of these: the record is older
than the process, the process is still in the folder it started in, the
folder isn't its home, no other Letta pane runs in the same folder, the
conversation belongs to that agent, and the transcript is held.

This applies to herdr's TUI too: a Letta pane without the herdr
integration now gets its chat. The behaviour is the same on both
runtimes.

## Verification
On a Linux PC with herdr-core (hc 0.1.17, an isolated HOME and
herdr-core session) and letta-code 0.34.44 on its local backend, after
one real turn: `GET /api/pane/conversation` returned `source:
letta-transcript` (16 turns at the latest check) and metadata `model:
openai-compatible/claude-haiku-4-5-20251001`, which the model card
reads. Before this change it returned `source: scrollback`. With a
second, unreported Letta pane open, it stays `scrollback`, as intended.

## Validation
- `bun run check fast`: ok (typecheck, build, unit 13023 pass).
- Failing-first tests in `server/letta.test.ts`: no report binds the
record's one conversation (default and local), still refused when the
record is newer than the process, when it names two conversations, or
when the report has another form; another Letta pane's folder record
naming the same conversation refuses, reported or not. One older
assertion pinned the old behaviour, "no report → Terminal"; it now
expects the record's conversation.
0