Skip to content

Add workspace file mentions to the composer - #11

Merged
wingleeio merged 6 commits into
zeronsh:mainfrom
sabari245:agent/composer-file-mentions
Aug 2, 2026
Merged

wingleeio merged 6 commits into
zeronsh:mainfrom
sabari245:agent/composer-file-mentions

Conversation

@sabari245

@sabari245 sabari245 commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Add workspace file mentions to the composer

This PR adds @ file and directory mentions to the chat composer.

Typing @ or @query opens a workspace-relative picker. Selecting a result inserts a Markdown link such as:

[composer.rs](comet-file:crates/ui/src/composer.rs)
[components](comet-file:crates/ui/src/components/)

The editor renders these links as atomic file or folder chips while keeping the raw Markdown prompt as the single source of truth.

Highlights

  • Searches files and directories within the active repository or a validated linked worktree.
  • Supports case-insensitive, whitespace-separated subsequence matching.
  • Prioritizes filename matches, recent files, changed files, and shallow paths.
  • Respects Git and repository ignore rules while allowing hidden files.
  • Returns up to eight results after an 80 ms debounce.
  • Uses shortest unique path suffixes for duplicate filenames.
  • Supports keyboard navigation, dismissal, tooltips, wrapped chips, deletion, undo, and selection as a single atomic unit.

Safety and lifecycle

  • The client sends only a chat or space identity; the engine resolves and validates the checkout.
  • Searches outside known repository roots and linked worktrees are rejected.
  • Filesystem walks are cancellable, serialized per checkout, and bounded by a six-second deadline.
  • Blocking filesystem work runs on a disposable OS thread rather than Tokio’s shared blocking pool.
  • Request generations prevent stale results from replacing newer UI state.
  • Mention behavior is enabled only for the chat composer and is cleared when the token, draft, chat, submission state, or wizard mode changes.

GPUI structure

  • Composer handles token detection, search requests, selection, dismissal, and popover rendering.
  • ComposerInput handles Markdown projection, atomic chip editing, layout, painting, hit testing, and tooltips.
  • The menu reuses existing popover and menu helpers and is anchored to the visible token position.

Validation

  • cargo fmt --all -- --check
  • git diff --check
  • cargo test --workspace --all-targets

All passed. Six environment-dependent tests remain ignored, and only pre-existing warnings were emitted.

Focused tests cover root validation, ignore rules, hidden files, directories, ranking, cancellation, concurrent callers, stale responses, Markdown validation, duplicate labels, atomic editing, wrapped geometry, tooltips, and menu navigation.

Reviewed locally with no blocking findings. Safe to merge.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@sabari245

sabari245 commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Screenshots

File mention search and result picker:

image1

Inserted directory mention with its full-path tooltip:

image2

@sabari245
sabari245 marked this pull request as ready for review August 2, 2026 17:50
wing-anara and others added 2 commits August 2, 2026 20:06
Sent prompts previously showed the raw [name](comet-file:path) Markdown in
the message bubble; chips only existed while drafting. User rows now carry
a display projection (chip labels + spans), computed once per entry change
in rows_for_entry — the fingerprinted row cache means never per frame — and
rendered with the markdown renderer's underlay recipe: StyledText for
wrapped glyph geometry, a canvas painting rounded washes beneath and
file/folder icons into their em-space slots. Ordinary prompts keep the
zero-allocation plain-text path via a substring probe, and shaping hits
gpui's line-layout cache during streaming frames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@wingleeio wingleeio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the full diff with a security lens (external-contributor PR). Engine surface is well-defended: SearchFiles resolves roots from the engine's own synced rows, validates them by canonicalized equality against the repo root or registered worktrees (stricter than the existing ListFolders), caps query length and results, bounds walks with deadlines on disposable threads, and only relative paths ever cross the RPC boundary. The ignore/globset/bstr additions are the standard ripgrep-family crates. Composer chip parsing rejects absolute paths, dot-segments, backslashes, control chars, and non-canonical encodings, with tests for the hostile variants. No process/network/env/unsafe usage, no hidden Unicode.

Follow-ups pushed to the branch: the merge with main (one conflict in composer.rs, resolved in favor of the themed selection/caret tokens plus the mention wash) and transcript-side chip rendering for sent messages (projection cached per entry change, gpui line-layout cache keeps streaming frames cheap; ordinary prompts stay on the zero-allocation path).

Verified at head: cargo fmt --check clean, 723/723 workspace tests pass. Known follow-up: transcript chips have no hover tooltip yet — InteractiveText::tooltip makes that a small addition.

@wingleeio
wingleeio merged commit 07ba53d into zeronsh:main Aug 2, 2026
1 check passed
@sabari245
sabari245 deleted the agent/composer-file-mentions branch August 3, 2026 17:03
guilhermexp referenced this pull request in guilhermexp/comet Aug 4, 2026
Twenty commits: light mode (#10), workspace file mentions (#11), native
word-editing (#8), memory bounding (doc LRU, delta transcript watch,
image cache, mimalloc, GPU eviction), a shared 30fps loader clock,
supervised cross-device sync, and the removal of the standalone TUI.

Five conflicts, resolved toward the fork's own work:

- `crates/tui` + `apps/tui` deleted as upstream did. That is the
  ratatui viewport reached with `comet tui`, not the terminal panel
  inside the app — different code, and the panel is untouched. The move
  of `resolve_comet_bin` into `crates/harness` had already cut the only
  dependency that would have made the deletion awkward.
- `main.rs`: kept `McpServer`, dropped `Tui`, and rebuilt the tracing
  setup on upstream's log-file registry while keeping the one rule the
  MCP server cannot lose — its stdout is the transport, so its console
  layer writes to stderr.
- The tab strip keeps this fork's scroll and stays without the bottom
  divider `f62d179` deliberately removed; upstream was re-adding it as
  part of theming it.
- `close_tab` keeps this fork's signature. Upstream added a `window` to
  dispatch `ToggleTerminal` when the last tab closes; this fork already
  solves that through `reconcile_terminal_presence` and the panel event
  the shell subscribes to, which also covers background chats and the
  fallback to Changes. Taking both would fire twice.
- The strip keeps the fork's ending: upstream's in-strip `+` and
  collapse chevron now live in the shell's unified utility strip.

Light mode reaches this fork's chrome too: `white_alpha` is gone from
theme.rs upstream, so the four uses left in the utility column moved to
`ink`/`hairline` with the same mapping upstream applied to the twin code
in the terminal panel. Without that the column would have kept a white
wash on a light background.
davidhs26 added a commit to davidhs26/comet that referenced this pull request Aug 21, 2026
* linux: draw caption controls under client-side decorations

On Linux the window had no minimize/maximize/close at all wherever the
compositor ends up in client-side-decoration mode (GNOME/Mutter has no
SSD support; gpui's WindowControlArea hit-testing is Windows-only, so
the Windows caption cluster never applied). Meanwhile leaving
window_decorations unset requested SERVER decorations, which stacked a
compositor titlebar on top of zeron's own chrome under sway/labwc.

- Request client decorations on Linux (WindowOptions.window_decorations)
  to match the unified-titlebar design; the compositor can still
  override via xdg-decoration and the shell re-resolves every frame.
- Draw zeron-styled caption buttons (24px cluster rhythm, hand-drawn
  linear glyphs) with explicit handlers - minimize_window / zoom_window /
  remove_window - since the native control-area hit-test is inert on
  Linux. Mounted at the root like the Windows cluster so they survive
  gates and the splash.
- Follow the desktop's button layout (GNOME button-layout via
  cx.button_layout, observe_button_layout_changed): right side by
  default; "close:..." layouts put buttons on the left and the app
  cluster shifts past them at the shared 2px rhythm. Controls the
  compositor can't do (per wm_capabilities) drop out; close always
  stays. Server decorations draw nothing - no double controls.
- Reserve titlebar right padding for the caption strip (tabs strip,
  changes-pane header, settings bar) via titlebar_right_pad.
- Give gate/splash pages a real drag strip on Linux/Windows
  (titlebar_drag_region - the WindowControlArea::Drag surface alone is
  dead on Linux).

Rig-verified under labwc (CSD: all three buttons work - minimize
unmaps, zoom maximizes and flips to the restore glyph, close exits;
close hover shows the red wash) and sway (SSD: no double controls);
left-side layout verified end-to-end through xdg-desktop-portal-gtk
with button-layout "close:".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(proto): model checkout change request status

* feat(engine): resolve GitHub pull requests with gh

* feat(engine): stream cached checkout change request status

* feat(ui): track pull requests for local and remote sessions

* feat(ui): show pull request badges in sidebar and composer

* feat(ios): support streaming device RPC

* feat(ios): show host pull requests in session surfaces

* test: cover pull request status across device boundaries

* fix(ui): frost pull request tooltip

* fix(engine): resolve gh from login shell path

* fix(engine): preserve links on unsupported streams

* fix(ui): retry pull request watch after host upgrade

* fix(engine): use origin for untracked branches

* fix(engine): resolve uncached remote default branch

* fix(engine): evict inactive change request cache entries

* test(engine): isolate login shell gh path fixture

* github: add FUNDING.yml so the repo shows a Sponsor button

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: adopt external pi sessions into workspace (ID01-420) (#1)

* feat: adopt external pi sessions into workspace (ID01-420)

Add pi_adopt module that discovers pi harness sessions created outside
the Zeron engine (CLI runs, pi-worker) and surfaces them as regular
workspace chats so every client (iOS included) can list and open them.

Discovery reads two sources:
- pi-acp session map (~/.pi/pi-acp/session-map.json)
- pi agent session store (~/.pi/agent/sessions/*/*.jsonl) as fallback

Each session becomes a Chat row with deterministic UUIDv5 id, resolved
space (created if needed), and idle Session row. Rows are idempotent —
re-scans never duplicate or rewrite existing rows.

Feature flag: ZERON_ADOPT_PI_SESSIONS=1 (off by default). With the flag
absent, the binary behaves identically to upstream.

Includes integration tests (2 passing) and proper CancellationToken-based
shutdown.

* docs: notas de implementación y verificación de pi_adopt

* fix(pi-adopt): post-review fixes — skip adoption without space, downgrade map warn to debug

- If resolve_space returns None (create_space transient error), skip
  adoption for this scan instead of writing an invisible chat row
- Downgrade recurring session-map warning to debug (45s spam)
- Update NOTAS with adversarial review findings

* fix(pi-adopt): don't guess a dangling space_id after create_space

Review (kimi-coding/k3): if create_space no-ops on an existing
(device,path) and the re-read fails, the derived UUIDv5 would
dangle and SpacesSync's orphan sweep would delete the chat.

Also: run the scan in spawn_blocking, log skip reasons at debug,
and document the spec-5 warning→debug deviation.

---------

Co-authored-by: David <davidhsutton@gmail.com>

* ci: fork gate (check + engine tests); upstream deploy/release manual-only

Los workflows heredados apuntan a la infra de zeron.sh (R2/releases) y no
aplican al fork; quedan en workflow_dispatch. El gate del fork es ci.yml:
cargo check --workspace + tests del engine. (ID01-424)

* landing: add nine new tweets to the marquee

Five to the top row, four to the reverse row, avatars mirrored into
assets/tweets. Marquee durations scaled (72s->117s, 80s->126s) so the
longer tracks keep the same scroll speed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* harness: native Claude driver over stream-json (A1)

Resurrect the pre-ACP claude driver (ca05336^) and modernize it against
CLI 2.1.228, replacing the claude-agent-acp adapter for HarnessId::ClaudeCode:

- Permissions ride the stdio control channel (--permission-prompt-tool
  stdio, validated live: can_use_tool requests arrive and allow/deny
  responses are honored). Tools auto-allow (unattended parity);
  AskUserQuestion round-trips through the engine input bridge.
- Done is the CLI's own result frame, eagerly — background subagents
  never hold the turn. The CLI's native wake turn (second init on the
  same session id + second result, live-verified) flows through the same
  stream; the engine's existing parked-session resume path turns it into
  the done→Working→done wake with no engine changes.
- Subagent frames (top-level parent_tool_use_id) are wrapped in the new
  AgentEvent::Subagent variant instead of dropped — never folded into
  the parent feed (they'd split contiguous text around phantom tool
  calls, and their message boundaries must not rotate the parent's
  assistant message id). The parent doc fold ignores them; routing to
  per-subagent docs lands with the subagent-visualization commit.
- --thinking-display summarized (newer models emit no readable thinking
  otherwise) alongside --verbose/--include-partial-messages.

Tests: fake-CLI scenarios (init dedupe, tagging, eager-done+wake, ask-
user round-trip, steering, interrupt escalation, error mapping), a
verbatim live-captured 2.1.228 background-subagent frame replay, and an
ignored live-CLI smoke.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* net: cap mobile warm dials, overlap checkpoint with backfill, flush sends at state

Three cold-open wire cuts for thin links (450kbps target):

- iOS caps background room dials at 8 (attention order). All 46 sessions
  still hydrate from disk instantly; sidebar status rides the registry
  room; an undialed chat connects the moment it's opened. Foreground and
  path-recovery kick sweeps skip dial-held stores — no more 46-socket
  stampede on every flap.
- Both clients send the rows request BEFORE the checkpoint HTTP fetch and
  stream the backfill in parallel with the download, buffering rows until
  the import lands (order pinned by test). The download used to push the
  whole join back by its own duration.
- Both clients flush pending pushes right after the state answer instead
  of after full convergence — a message typed offline now lands ~2 RTTs
  after the socket does, not after checkpoint + backfill (server batchId
  dedupe makes replays no-ops; deadlock-shaped regression test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ios: checkpoint downloads survive redials, registry single-dial, registry-first dial gating

On-device NLC (Edge profile) round 2 fixes:

- ChatRoomClient: one checkpoint download per chat that OUTLIVES socket
  generations (a saturated link trips the silence lease mid-download —
  pongs queue behind the blob — and the redial used to discard the bytes
  and start over, looping a fresh-chat open forever). Partial downloads
  Range-resume across attempts; download progress defers the silence
  lease (120s backfill deadline stays as the backstop); a completed blob
  applies even if its socket died (CRDT import + max-cursor are socket-
  independent), so the next session joins RowsOnly.
- RegistryClient.kick: don't redial while a handshake is in flight —
  foregrounded() fires on scene activation at launch and killed the dial
  mid-TLS every cold open, doubling the spinner (6.9s → measured single
  dial 4.5s on Edge). Mirrors ChatRoomClient's existing guard.
- AppModel.preloadSessions: warm chat dials wait for the registry to
  connect (10s cap) — the sidebar's own handshake+state gets the thin
  pipe to itself; open views still dial instantly via releaseDial.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* sync: pull-first HTTPS transport — 1-RTT bootstrap, WS demoted to enhancement

The airplane-wifi fix. The WS needs 4+ serial round trips (DNS, TCP, TLS,
upgrade, hello) before byte one, and hostile middleboxes strip the upgrade
entirely — the user-reported 'never connects on the plane'. A plain HTTPS
GET is 1 RTT on a warm connection and indistinguishable from a webpage to
any proxy. So: push/pull/poke.

Edge (worker + DOs):
- GET /registry/{org}/rows?since= — the WS hello's exact delta answer
  (full/gcFloor rules shared with handleHello); ?beat=1 doubles as a
  presence beat. POST /registry/{org}/push — the WS push twin via a shared
  applyPushBatch core (validate, atomic apply, rows broadcast to live
  sockets); LWW clocks make replays no-ops.
- GET /chat2/{id}/rows?after= — one request collapsing connect→hello→
  state→rowsReq→backfill: u32-LE length-prefixed frames (state with
  frontier payload, rows, rowsDone), byte-identical encoding to the WS.
  POST /chat2/{id}/rows?batchId= — push twin, batchId-deduped.

Desktop (Rust): RegistryTransport/ChatTransport seams; with a transport,
construction resolves immediately (local-first), an HTTP pull bootstraps
in ~1 RTT while the socket dials, and every backoff cycle pushes pending +
pulls over HTTPS — a WS-hostile network degrades to ≤30s polling instead
of silence. Engine wires both via the existing bearer/reqwest plumbing
(registry: URL derived from the WS provider; chat: EdgeChatTransport).

iOS (Swift): WorkspaceStore pull-first bootstrap + 20s poll-while-down +
HTTP flush of pending writes; the header spinner now means 'server state
reached this session' (flips on the ~1-RTT pull, not the 4-RTT socket).
ChatRoomClient.pullSync: HTTP push of pending sends (permanent rejections
retire, mirroring the error-frame path), framed rows pull applied through
the exact socket path, checkpoint fetch shared via fetchInFlight.

Validation: new edge/scripts/pullpush-check.mjs green against wrangler dev
(push/ack/no-op replay, delta-at-cursor, presence beat, framed pull,
excludeOwn, batchId dedupe); existing smoke + chat2-check (65) + reset
suites green; zeron-sync 34, engine+rpc 180; iOS sim build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* sync: audit fixes — heal waits for known server state, iOS retires pushes only on edge verdicts

Pre-merge audit findings:
- With immediate-ready construction, the host's bootstrap heal could read
  all-zero placeholder stats 3s after construction (server state not yet
  known on a slow link) and upload a spurious full-doc checkpoint. New
  ChatStatsSnapshot.server_known gates it; the heal waits (bounded 2min)
  for a real state answer.
- iOS pullSync retired pending batches on ANY http 400/413 — a captive
  portal answering a POST with junk would silently drop a real message on
  exactly the networks this path serves. Retirement now requires a parsed
  edge error code (bad_push/too_large/empty), mirroring the WS error-frame
  discipline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* harness: native Codex driver over app-server JSON-RPC (A2)

Resurrect the pre-ACP codex driver and modernize it against codex-cli
0.146.1 (the app-server API is experimental — the pin is noted in the
module header), replacing the codex-acp adapter for HarnessId::Codex:

- Done stays turn/completed (turn.status == "failed" now maps to an
  errored Done even without a turn.error; the error notification's
  nested params.error.message shape is accepted alongside the old flat
  one — both live-verified).
- Approvals ride the app-server request RPCs as before (wire policy
  "never" for unattended parity, stray requests round-trip as yes/no
  questions); the experimental item/tool/requestUserInput content
  questions now reach the input bridge and answer keyed by wire id.
- Subagents are full child threads. A registered child's notifications
  route through an EXPLICIT table (route_child_notification), pure and
  tested: item lifecycles AND message/reasoning deltas become tagged
  AgentEvent::Subagent events (live capture shows child threads DO
  stream deltas on this wire — token-level live transcripts without
  touching the rollout files), child turn/thread bookkeeping is
  consumed so it can never settle the parent turn, and unknown methods
  fall through to the parent path — fail open, never silent loss (the
  reference implementation shipped two bugs from a catch-all swallow:
  a child error left the agent running forever, and a swallowed
  serverRequest/resolved stuck approvals). Registration comes from
  subAgentActivity items on the parent thread; the root's own activity
  marker never registers (registering the root as its own child
  intercepted its turn/completed and hung the thread Working).

Live-validated: basic turn end-to-end (marker file written, clean
turn/completed) and a two-child fan-out capture archived under
tests/fixtures/codex/. Ignored live smoke included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.2.4

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pi-adopt): filter noise sessions (ephemeral cwds + utility prompts) (#2)

Skip adoption of sessions in /tmp-style ephemeral workdirs (E2E test
workspaces) and of machine-to-machine utility prompts (title generator).
Observed in production: first rollout adopted title-gen sessions and
pw2-e2e workspaces, flooding the mobile sessions list. (ID01-420)

Co-authored-by: David <davidhsutton@gmail.com>

* harness: Cursor driver via the pinned @cursor/sdk shim (A3)

Move HarnessId::Cursor off ACP (that surface strips subagent transcripts
— live-verified: task rawOutput is {durationMs, isBackground} only) onto
Cursor's SDK, per Cursor's own guidance to integrators.

What wire the SDK wraps, investigated first: @cursor/sdk@1.0.28 does NOT
spawn the cursor-agent binary — it is Cursor's agent runtime bundled
in-process, speaking proprietary protobuf/ConnectRPC to api2.cursor.sh
with client-side tool execution (sandbox helper binaries, MCP spawning).
There is no speakable stdio wire to drive from Rust, so the driver is a
thin zeron-owned Node shim (shim.mjs, JSONL frames over stdio) around the
PINNED SDK, materialized into a managed npm install
(adapter_install::ensure_installed_shim) and spawned as `node <shim>`.

- Done comes from the SDK run's terminal result — a crisp turn end by
  construction; steers queue and deliver at turn boundaries (parity with
  the prior ACP behavior).
- Subagents: tool-call-delta carries the child's FULL nested transcript
  (NestedTaskUpdate) tagged by the spawning task's callId; the shim
  forwards it tagged and the driver wraps it as AgentEvent::Subagent —
  token-level live, no file tailing needed for cursor.
- The permission-free print surface is never used. askQuestion is
  disallowed at agent creation: the SDK exposes no public answer channel
  (SDKRequestMessage carries only a request id), so a question would
  block the run forever. Known gap vs the ACP ask_question bridge.
- AUTH: the SDK's credentials are SEPARATE from `cursor-agent login`
  (verified live — Agent.create succeeds unauthed, the turn then fails
  with "Invalid User API Key"). Turn/startup errors carry the exact fix
  (set CURSOR_API_KEY) as the error chip.
- Beta churn: unknown SDK update kinds are ignored, never fatal — a
  drifted SDK degrades the stream, not the chat.

Shim validated live against the real SDK (ready frame + clean actionable
auth error end-to-end); driver covered by fake-shim scenarios (mapping +
tagging, steer-as-next-turn, interrupt, fatal auth, crash tail).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* landing: point downloads at v0.2.4

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* harness/engine: retire adapter machinery for claude/codex/cursor (A4)

AcpHarness now serves ONLY agents built ground-up on ACP — Grok and
Hermes — plus pi via pi-acp until a native driver exists.

Removed with the claude/codex/cursor ACP paths:
- The claude-agent-acp / codex-acp specs, managed-install prewarm
  entries, and constructors (the pi pin keeps the npm install machinery;
  the cursor shim reuses it for @cursor/sdk).
- The `_session/turn_ended` extension handler — the upstream adapter
  dependency (adapter PR #997) is gone; the native claude driver forwards
  the CLI's own wake-turn result instead.
- The Claude cost-frame settle machinery (COST_HINT_GRACE, turn-end cost
  arming, the quiet-settle exemption): all of it compensated for the
  adapter dropping prompt replies, which the native driver cannot do.
  The blanket quiet settle stays for the remaining ACP agents.
- Cursor's ACP extension surface (cursor/ask_question, create_plan,
  update_todos, task, generate_image handlers + the parameterized model
  picker opt-in and variant-id plumbing).

Engine: new Harness::deterministic_turn_end capability — native drivers
whose every turn shape (user-prompted AND agent-initiated wakes) ends
with a wire Done return true, and drive_run stops arming the 120s/20s
quiesce watchdogs for them (ZERON_TURN_QUIESCE_MS still forces a window
on for diagnostics). ACP agents keep the watchdog backstop.

Tests: claude/codex/cursor ACP suites and the cost-frame/turn-ended/
quiet-exemption tests removed with the machinery they pinned; managed-
install tests repointed at the pi pin; the login-shell resolution test
now proves the native claude driver consults the same PATH snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* harness: Grok ACP hardening (A5)

Grok stays on ACP — its only surface with supervised permissions
(headless is --always-approve only) — hardened against the field
reports of silent non-response and multi-second stalls:

- Spawn args are now `grok --no-auto-update agent --no-leader stdio`.
  Flag placement verified against 1.0.4 (--no-auto-update is top-level;
  --no-leader lives on the agent subcommand — NOT the spellings the
  investigation guessed). --no-leader starts a fresh agent even when
  `[cli] use_leader` makes `agent stdio` ATTACH to a shared leader via
  ~/.grok/leader.sock; --no-auto-update kills the launch-time update
  check. Field-hang REPRO (leader on a private socket, SIGSTOP, attach):
  no initialize response for 10s then a client-timeout death on 1.0.4
  (older builds hung forever — the reported signature); the same wedged
  machine completes a turn in ~1s with --no-leader. npm fallback pin
  bumped to @xai-official/grok@1.0.4 to match the verified flags.
- `_x.ai/session/prompt_complete` is now the AUTHORITATIVE turn end
  (live-confirmed on 1.0.4, including the echo of a zeron-minted
  `_meta.promptId`): the notification swaps the in-flight prompt future
  for a synthesized response, so the ENTIRE existing response-settle arm
  (steering settlement, drain ordering, park/queued-steer) runs
  unchanged and the possibly-hung RPC drains discarded in the
  background. Stale completions (already-settled prompt ids, ring of
  32), foreign sessions, and out-of-order ids can never settle a newer
  turn. The prompt response remains the fallback — whichever lands
  first wins, exactly once.
- Prompt-stall watchdog (spec-gated, grok 30s;
  ZERON_ACP_PROMPT_STALL_MS overrides): TOTAL wire silence after a
  prompt — healthy grok acknowledges within milliseconds — surfaces a
  visible error chip naming the likely wedge and closes the run with an
  errored Done, instead of indefinite Working. Any wire traffic clears
  it; stderr already feeds the harness log via the rolling tail.
- Usage: grok settles token counts on the response `_meta`
  (inputTokens/outputTokens — verified live); usage_from_response reads
  it alongside the adapters' first-class `usage` object.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* subagent visualization: per-subagent docs, spawn chips, right-pane tab (B)

Main doc stays light: subagent content NEVER enters the parent chat doc.
The spawn tool call becomes the index chip — subagent_ref (the subagent
doc id), subagent_status (running/done/failed; distinct from `resolved`,
which the eager-done policy fires while the subagent still runs), and a
one-line live tail — all additive doc fields old readers ignore.

Engine (drive_run): tagged AgentEvent::Subagent traffic routes to a
per-subagent doc, id `{chatId}--sub--{toolUseId}` (hashed when unclean/
over the edge ID_RE budget — same id names the ChatRoom and the frozen
blob). Sinks fold events into one streaming entry, flushed on the same
coalesced STREAM_COMMIT_MS tick as the parent; the held doc Arc pins the
doc warm while running. The chip refreshes through the live fold while
its segment streams, else in place via the new
SessionDoc::update_subagent_chip (newest-entry-first scan — the parked/
wake norm), with tail stamps throttled to ~1/s. On the tagged terminal
event the entry finalizes and the transcript FREEZES: entries JSON
uploads as a static R2 blob (blob/{chatId}/{subDocId}) so viewers of
finished subagents never wake the room; dropping the sink unpins the
doc and the room idles. Every failure path degrades to chip-only.
Routing runs BEFORE the parked gate, so a parked session's background
traffic reaches the subagent doc without un-parking the chat.

UI: RightSurface::Subagent tab beside Diff/Terminal, opened from an
"Open subagent" affordance in the chip's existing blob-affordance slot
(analytic heights untouched). Strictly read-only Transcript::for_doc
instances — no composer, no steering, no permission UI, no own-turn
machinery, no global attachment-protection writes. Running subagents
watch their doc over the unchanged WatchDocMessages RPC (any doc id);
finished ones fetch the frozen blob first, doc watch as fallback.
Closing the tab drops the watch task, which cancels the server-side
watch and unpins the doc from the WARM_DOC_CAP LRU.

zeron-doc: SegmentWriter::resume/into_state (cross-flush sink seam) and
the in-place chip update API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Also: the cursor shim's crash message now waits briefly for the reaped
exit status (try_wait raced the child's death — the error chip read
"still running" with no stderr tail).

* ios: pull's checkpoint fetch must drain the socket's frame buffer

The HTTPS pull and the socket handshake race on a frontier-missing chat:
the pull's inline fetch set fetchInFlight, the socket's handleState saw it,
skipped its own fetch, and armed checkpointBuffer expecting the completion
path to drain it — which the inline fetch never did. Every socket row
(backfill AND live broadcasts) buffered until the 120s backfill deadline
redialed: a frozen transcript on exactly the fresh-chat open the pull was
meant to speed up (user report, 2026-08-18). Route the pull's fetch
through completeCheckpointFetch, whose tail applies the blob and drains
whatever buffer a racing socket armed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* sync: pull-path failures are loud on both platforms

The 2026-08-17 cellular incident was undiagnosable from device logs: the
phone's sockets fleet-wedged on 2-bar 5G while HTTPS demonstrably worked
(sends + uploads flowed), the pull path failed to fill the transcript —
and logged NOTHING, because every failure exit was a silent try?/guard.
That violates this codebase's own rule (roomLog header: sync must never
fail silently). Every pull/push exit now names its failure (no-token,
transport error, http status, malformed body, frontier-still-missing),
and the Rust offline-sync failures log at warn instead of debug so real
users' daemon.log shows them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* sync: empty checkpoint frontier is NOT contained — fetch, never skip history

Root cause of the 'Add Tweets' invisible-transcript incident (2026-08-18),
diagnosed from room stats + phone log archives + the absent on-device doc:
the room's checkpoint (seq 5, rows 1-5 trimmed) carries an empty frontier
label, and both clients treated empty-frontier as 'already contained' — so
every FRESH reader skipped the checkpoint, imported rows 6..75 raw, and
Loro parked everything depending on the chat's founding ops: transcript
invisible past the reader's own-lineage rows, no import errors (parking
is not an error), and snapshot export fails with pending ops so the doc
never persists. Converged devices (synced from row 1 pre-trim) unaffected,
which is why the bug only shows on new devices/fresh installs.

- Both clients: drop the empty-frontier shortcut; empty fails the decode
  path → not contained → fetch. Fetching is always safe (full-state merge;
  an empty-doc seed applies as a no-op) — the shortcut contradicted the
  adjacent comment's own principle ('never silently skips history').
  Applying the poisoned room's real checkpoint bytes heals it client-side
  with no server repair.
- Edge: reject checkpoint POSTs with an empty frontier when seqCovered>0
  (empty stays legal for the M1 empty-doc seed) so no new rooms are
  poisoned; chat2-check codifies the guard (66 checks green).
- Regression tests: EngineChatSink::contains_frontier(&[]) must be false;
  plan-level decision-table entry for the fresh-reader fetch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* sync: an encoded-empty frontier is a vacuous claim — fetch, never skip

The first frontier fix (zero-length bytes) didn't heal the poisoned room:
the phone's next cold open STILL skipped the fetch cleanly. Mechanism
proven by failing test: the room's frontier is a VALID ENCODING of an
EMPTY version vector — every doc vacuously 'includes' empty, so the
containment check passed for readers holding nothing and they skipped the
chat's founding ops anyway. One representation deeper than the last fix.

Both platforms now treat a decoded-but-empty version vector as NOT
contained (a size>0 checkpoint claiming empty state is a contradiction),
with an info log at the decision so any future skip names itself.
Regression tests: encoded-empty frontier → fetch; real contained frontier
(doc with actual ops) still short-circuits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* sync: cursor amnesty — a cursor above the checkpoint is re-verified, once

The 'Add Tweets' wedge, closed with artifact evidence: the phone's
persisted doc was EXACTLY the 2728-byte checkpoint (state at seq 5, which
includes the user's own message) under cursor=75. Rows 6-75 had imported
as PENDING during the frontier-bug era (deps missing → Loro parks
silently, import reports success, no logs), the cursor advanced anyway,
and parked ops vanish on export — so the persisted doc lied under a
cursor that guaranteed nothing below it would ever be re-read. Every
transport then truthfully reported 'converged' forever.

Fix (both platforms): once per client, on the first state answer, clamp
the cursor to the room's checkpointSeq. Rows since the checkpoint refetch
(re-imports are no-ops; cost bounded by the trim policy to the last
checkpoint window), converting any lying cursor into a true one — heals
every wedged replica with no server change and no data-dir surgery, and
keeps healing any future parked-history divergence from any cause.

iOS grows a clampCursor delegate (persists doc+cursor in the same write,
C2 rule); Rust clamps shared.cursor at the state site both transports
share. Amnesty logs at info so a clamp names itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.2.5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* landing: point downloads at v0.2.5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pi-adopt): repair tests + k3 findings (ID01-426) (#3)

Tests: non-ephemeral cwds (the filter itself broke tempdir-based tests);
/tmp adoption asserted as filtered. Findings: /private/var/tmp coverage,
shared TITLE_PROMPT_PREFIX const with titles.rs, info-level utility skip,
extra ephemeral tests, module doc on operator-side purge.

Implementer: glm-5.3 (timed out at 40m but left complete work).
Local gate: targeted tests (unit+integration pi_adopt) + workspace check;
full engine suite runs in CI only (hangs locally against the live engine).

Co-authored-by: David <davidhsutton@gmail.com>

* subagent viz: three live-rig findings fixed

The PR's media rig session (real claude + codex through the built app)
surfaced three gaps the fixture suites missed; all three are fixed and
pinned by regression tests:

- update_part_fields dropped the chip fields: a spawn chip mutating
  while its segment still streamed (the codex fan-out shape) lost
  subagentRef/subagentStatus/subagentTail at SegmentWriter::sync — only
  the parked in-place path persisted them. Live-segment chips now carry
  the fields.
- claude 2.1.228 emits NO tagged terminal frame for a finished
  background subagent: completion arrives as an untagged system
  task_notification carrying the spawning tool's id (shape pinned from
  the live capture). The normalizer now surfaces it as the subagent's
  tagged Done, so chips flip done/failed and transcripts freeze instead
  of reading running forever.
- claude streams no tagged partial deltas: a subagent's text arrives
  only as full text blocks on its tagged assistant frames, which were
  dropped — subagent transcripts were tool-chips-only. Tagged text
  blocks now emit as attributed text (message granularity; the wire
  offers nothing finer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* subagent tabs: contextual titles + top-aligned streaming runway

User feedback on the PR build:

- Tab names are contextual. The root cause was the harness: Claude's
  spawn decode named the chip a bare "Agent", so the tab inherited it.
  The Agent/Task decode now names the chip after the task's description
  ("Agent: scan repo") — codex (agent-path leaf) and cursor already did.
  The tab-title fallback also digs description/prompt out of the stored
  tool input for older docs whose chips predate the decode, and every
  derived title caps at 40 chars for the fixed-width strip.
- Subagent transcripts start from the TOP and stream downward. Override
  instances build their list Top-aligned — the notes-page shape
  structurally, with the empty space below as the runway — instead of
  Bottom-aligned, which pinned short/starting transcripts to the foot
  of the pane. The whole glue-trap surface (end-sentinel re-glue,
  distance-0 restick, pad/hold machinery) is a Bottom-alignment
  property, so none of it is needed or armed here: the pin starts off,
  the wheel restick is gated off for overrides, and row 0 keeps only
  the turn gap (the pane already pads for the titlebar — the full
  window-chrome pad double-counted and started content ~66px down).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): round project picker section corners

* feat(pi): step-boundary steering — descriptor truth-up for the patched pi-acp adapter

The local pi-acp adapter now implements the _session/steering ACP
extension, bridging to pi native RPC steer (delivered before the next
LLM call). Runtime detection stays dynamic via
initialize._meta.steering.supported, so an unpatched adapter degrades
to boundary delivery instead of breaking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScYmkStp1WkbMwchzaXjcb

* subagent UX: real terminal statuses, bare tab titles, designed chip

User feedback round — statuses were wrong and the chip read as bolted-on;
all fixed now, none deferred:

- Codex and Cursor subagents get REAL terminal signals. Codex children
  never see thread/closed in real fan-outs (it needs the collab
  close_agent tool), so chips stayed "running" after the child settled —
  a child's turn/completed / turn/failed / turn/aborted now routes as
  the subagent's tagged Done (completed/errored/interrupted). Cursor's
  SDK has no separate terminal frame for the nested transcript, so the
  task tool's end doubles as the tagged Done. Claude already settles via
  task_notification. The engine skips minting a subagent doc for a
  Done-only event (a terminal that beats registration is chip-only, not
  an empty frozen doc).
- Tab titles are the BARE task description — the "Agent: " prefix ate
  most of a fixed-width tab. Genus stripping is word-boundary-safe
  ("Taskmaster" survives), first-line-only, 40-char capped; "Subagent"
  is the last resort.
- The chip is designed, not annotated: state is visual — the app's
  mini gradient spinner (the sessions-sidebar dot loader) pulses in the
  trailing slot while running, done is the ordinary quiet chip, failed
  takes the danger tint; the "· running/· done" text suffixes are gone.
  The affordance row is "Show subagent transcript", rendered from the
  same base as "Show full output". The Subagent tab's strip icon swaps
  to the mini spinner while its doc streams (the history fetch-all
  pattern), settling back on completion.
- Clipped running-tail bug root-caused: the tail bypassed the
  single_line whitespace fold every other chip detail passes through,
  so raw text wrapped inside the fixed 30px header and clipped. It now
  folds like every detail line; height accounting is unchanged by
  construction (no new visible rows).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* subagent polish: Agent chip label + codex message paragraphing

Two cosmetic findings from the live verification pass:

- Spawn chips read "Tool · Agent: scan repo" — two labels fighting. The
  Unknown decode convention every native driver uses ("Agent[: <desc>]")
  now labels the chip "Agent" with the description as the detail.
- Codex emits several assistant messages per turn (commentary, final
  answer) whose deltas carry no separator, so consecutive messages
  rendered concatenated ("…waiting.Beta's 90-second…"). Each completed
  agentMessage now closes a paragraph, in the parent transcript and in
  child subagent docs alike.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* subagent tab: scroll follow + jump pill; chip quiets down; top fade

Feedback batch on the subagent surfaces:

- The tab now has the main transcript's follow UX. Top alignment stays
  (the start-from-top runway is untouched) — verified against the pinned
  gpui source that the list glue trap is Bottom-alignment-only: under
  Top, past-end offsets always materialize concrete, so a parked spring
  cannot re-glue and hard-track growth. The pin/spring/restick machinery
  is distance-based and runs unchanged on top: streaming overflow
  follows the end with the same glide, wheel-up releases to a held
  offset, and the jump pill — extracted into a shared element, not
  copied — renders pane-locally for the active tab and re-engages
  follow on click. Frozen transcripts open unpinned at the top.
- The chip's live tail is REMOVED end to end. Rewriting the header per
  delta batch grew the parent doc's oplog for the whole subagent run and
  rendered as mid-stream markdown fragments. The fold and the engine's
  in-place stamps are lifecycle-only now; the running chip is the static
  task description plus the working spinner. subagent_tail stays in the
  schema (and the row-cache fingerprints) for docs that carry it.
- The affordance row reads "Open subagent".
- The pane transcript gets the top overflow fade (edge_faded, top edge
  only — nothing sits at the pane's bottom), gated on real overflow so a
  short top-anchored transcript shows no fade band.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* shell: jump pills go glass

Both scroll-to-bottom pills (one shared element since the subagent-pane
round) rendered as an opaque raised surface — one of the few non-glass
floating elements left. On glass appearances the pill now frosts at the
composer tier (radius-clipped backdrop blur 16) under the standard
floating-card tint, hover fading the on-glass wash in OVER the tint —
compositing the wash rather than mixing toward it, which would have
thinned the pill see-through on hover. Opaque appearances keep the
raised-surface blend. Frost opens its own scene layer at the pill's
paint position, so blur + pill composite over the scrolling transcript
in both hosts; size, label, motion, and click behavior unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* spaces: reach other drives from the add-project palette

The folder browser only ever started at home, so a project on an
external SSD or second disk meant walking up to / and hunting for the
mount by hand (if you even knew where your platform puts them). The
palette's rail now has a Locations section under Devices: Home plus the
device's mounted drives, served by a new relay-forwardable ListDrives
RPC (macOS /Volumes with the boot-volume symlink resolved; Linux
/proc/mounts under /media, /run/media, /mnt plus a System row for /;
Windows drive letters). Clicking one rebrowses the palette at that
root, the row owning the browsed path carries the selection wash, and
the drive's mount folds into a breadcrumb named after the drive the
same way home folds into the device crumb. Hard-drive + home glyphs
hand-drawn in the Solar Linear style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* subagent tab: every open lands at the end

Opening a tab now lands on the latest content universally, not only for
live tabs. Frozen transcripts arm a one-shot end-landing that fires in
sync on the FIRST non-empty row set — whichever feed delivers it (blob
snapshot or watch replay) and whenever it resolves — via the item-
anchored scroll_to_end (a past-the-end offset the next layout
materializes by walking back from the last item; pixel offsets would
land short on freshly-spliced unmeasured rows). The flag clears on
fire, so later growth never re-asserts and the viewport belongs to the
user immediately. `pinned` stays untouched: a frozen doc that grows
after the landing appends below with no auto-follow — landing at the
end and following it remain distinct states. Short content still rests
top-anchored (the past-end offset clamps to the top under Top
alignment), so the runway behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* subagent iconography: bot glyph for tabs and spawn chips

Hand-drawn bot head (antenna + eyes + ears) in the Solar Linear style —
the embedded set had no robot glyph. The Subagent tab drops the chat
bubble for it, and the spawn chip's icon tile matches (the "Agent"
Unknown convention drew the generic widget glyph, inconsistent with its
own tab). Chat-bubble usages that ARE chat (question chip, comments,
composer) stay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ui: real frosted floats on Linux (wgpu backdrop blur)

Theme::is_frost() separates scene-level frost (floating surfaces
blurring in-app content) from is_glass() window vibrancy: the wgpu
renderer now rasterizes BackdropBlur like Metal does, so Linux gets
frosted popovers/modals and the composer pill without compositor
vibrancy. The window chrome itself stays opaque off macOS (no
blur-behind guarantee outside KWin).

- composer pill + question panel: paint their backdrop blur and
  translucent tint on Linux; drop the opaque shadow slab
- popovers/modals/settings cards: frosted tint over the blur
- macOS unchanged (is_frost == is_glass there)

Requires the gpui rev bump to wingleeio/zed comet/edge-fade-horizontal
with the wgpu BackdropBlur port (PR to that fork); rev points at the
port branch until it merges.

* spaces: enumerate custom mounts and accept typed paths (PR #144 feedback)

Linux drive partitions live wherever the user mounted them — /disk2 is
as much a drive as /media/user/T7. The Linux walk now also takes any
block-device mount at a depth-one non-FHS point (squashfs/erofs images
excluded, so snaps stay out; the system's own split /boot//home//var
partitions are plumbing, not drives). And the browse root was never
meant to be a cage: a query starting with / or ~ now navigates —
typing / jumps to the root and each segment/ descends shell-style,
while a pasted path (or ⏎ on one) jumps in one hop, ~ expanding to the
device's home. A typed path that doesn't exist shows the engine's real
folder error instead of blaming the device's connectivity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.2.6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* landing: point downloads at v0.2.6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chat2: claim rooms on HTTP first-contact, not just WS join

The /rows GET/POST twins predate the pull-first HTTPS transport and sat
behind an owner gate that only /ws and checkpoint POST could satisfy. A
brand-new chat on a WS-hostile network deadlocked: the sender's HTTP
push 404s (room unclaimed), the host's HTTP pull 404s, and the only
claimants — WS join and the checkpoint heal (gated on a server state
that the 404'd pull can never learn) — never run. Every remote new-chat
send hung on "Sending…" until some WS dial happened to succeed.

First authed toucher now owns the room on any HTTP contact, the same
client-minted-id discipline /ws already applies; foreign users still
get 403.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* grok: subagent visualization over ACP — lifecycle wire events + disk-tailed transcripts

Grok subagents rendered as plain tool calls because the ACP path never
emitted AgentEvent::Subagent: grok's frames carry no parent_tool_use_id
(tagging documented but unimplemented as of 1.0.4), the subagent's interior
transcript never rides the parent's wire, and the lifecycle updates
(subagent_spawned/progress/finished) arrive on grok's extension channel
_x.ai/session_notification, which the harness dropped entirely.

The new SubagentTracker correlates the spawn_subagent tool call (via
_meta["x.ai/tool"]) with its subagent_spawned update — binding through the
subagent_id echoed in the spawn's completion output, falling back to
description-FIFO for sync spawns — then tails the child session's
chat_history.jsonl (located by session id under ~/.grok/sessions, one glob
level to dodge the cwd encoding) into tagged transcript events: reasoning
summaries, assistant text, typed tool calls, tool results.
subagent_finished settles the chip with a tagged Done after a bounded
drain; a torn-down session settles running tails as Interrupted. Every
disk parse fails soft to chip + final-output-only — the format is
vendor-private. Spawn chips are named "Agent: {description}", claude-driver
parity.

Live-validated against grok 1.0.4 (examples/grok_subagent_probe.rs): full
tagged stream — reasoning, text, Exec("echo viz-probe-ok && sleep 3"),
result, Done — attributed to the spawn chip on a real background subagent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(engine): per-harness model allowlist for ListModels (#5)

Trim the picker to ~/.zeron/model-allowlist.json at the RPC seam.
Missing/corrupt/empty config or zero matches pass through so the
catalog never goes empty. Deepseek ids with an extra slash match
deepseek-payg/*.

Co-authored-by: David <davidhsutton@gmail.com>

* feat(ios): create a space from the new-session menu (#6)

The folder browser already existed. This wires New space into the +
menu, adds a typed path (engine-validated via ListFolders), and
clears a stale gitDetected when jumping paths.

Co-authored-by: David <davidhsutton@gmail.com>

* fix(engine): deaf-socket tripwire retries with backoff + sweeps chat rooms (#7)

* fix(engine): deaf-socket tripwire retries with backoff + sweeps chat rooms

The presence tripwire fired one registry redial and disarmed ("one probe +
one redial, ever"), so a redial that landed on another dead socket left the
engine dark forever. Chat2 rooms were never touched at all, so a completed
turn's final message sat undelivered on the phone (2026-08-18 incident,
11:37-11:51 UTC: redial requested, never recovered, summary never arrived).

- keep the tripwire armed while presence stays dark: repeat redials with a
  doubling interval (60s base, 15 min cap), log each attempt and recovery
- new deaf-escalation hook wired to DocHost::probe_open_chats — a probe is
  free on a healthy room; a deaf room misses its probe deadline and redials
  itself (chat rooms ride the same relay as the registry room)

Gate: cargo check --workspace --all-targets OK; zeron-engine --lib 76/76.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtyrsrkwhZsVP7QvPKUGLr

* fix(engine): invoke presence hooks outside reg/room guards + test the ladder

Review finding (K3, CRITICAL): the deaf-escalation hook ran under the
registry `reg` guard (match-scrutinee temporary in publish()) plus the
room/presence_seen guards, while DocHost's open/eviction paths take
`handles` before `reg` — an AB-BA deadlock armed exactly during the long
dark-presence spells the watchdog exists for.

- publish() binds read_all() outside the match so `reg` drops before
  overlay_presence (also moves the pre-existing peer-alive hook out from
  under `reg`)
- check_presence_deafness returns the decision; overlay_presence invokes
  the hook after releasing room/presence_seen, mirroring peer_alive
- the tripwire state machine is now PresenceWatch::tick (pure) with unit
  tests for the probe->redial ladder, doubling backoff + cap, and re-arm

Gate: cargo check --workspace --all-targets OK; zeron-engine --lib 79/79.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtyrsrkwhZsVP7QvPKUGLr

---------

Co-authored-by: David <davidhsutton@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(engine): keep task timer across park/self-continued (ID01-423) (#8)

* fix(engine): keep task timer across park/self-continued resume (ID01-423)

started_at now tracks the user task, not each internal turn. A parked
persistent session keeps the base; self-continued output does not
restamp; user dispatch/steer does. Only a true settle (run handle gone)
clears it.

* fix(engine): don't double-restamp timer on Steered; skip settle if a new run owns the chat

* fix(engine): hold runs lock across settle_status so a new dispatch cannot be clobbered to Idle

---------

Co-authored-by: David <davidhsutton@gmail.com>

* feat(pi): foreground subagents tool (ID01-432) (#9)

* feat(pi): foreground subagents tool (ID01-432)

Versioned copy of ~/.pi/agent/extensions/subagents.ts plus unit tests.
Foreground only; background is ID01-433.

* fix(pi): keep SIGKILL timer referenced so timeout tests settle

* fix(pi): clear SIGKILL timers on settle; cap stdout incrementally

---------

Co-authored-by: David <davidhsutton@gmail.com>

* feat(pi): background subagents + status (ID01-433) (#11)

* feat(pi): background subagents + status (ID01-433)

* fix(pi): per-entry kill + batch token so background reuse stays clean (ID01-433)

killAll no longer sticks a runtime-wide flag that would mark later batches
killed. Sweep only touches entries owned by the same dispatch token.

* fix(pi): close k3 findings on background subagents (ID01-433)

Foreground workers stop spawning after killAll via killEpoch. Pre-aborted
signals reject instead of returning a silent dispatched list. Status
distinguishes queued vs running. Duplicate batch ids fail in foreground.
installSubagents covers steer/nextTurn/shutdown delivery.

* fix(pi): drop dead abort/dup branches after resolveTasks (ID01-433)

---------

Co-authored-by: David <davidhsutton@gmail.com>

* cursor: in-app Connect via the SDK's browser login + accounts integration

The @cursor/sdk conversion (#136) left cursor unusable without a manually
minted CURSOR_API_KEY — the SDK's credentials are separate from
`cursor-agent login`, and the shim's only guidance was an error pointing at
cursor.com/settings. But the SDK ships its own PKCE browser login
(Cursor.auth.login, verified live on 1.0.28): it mints a named, revocable,
90-day user API key and persists it to ~/.cursor/sdk/auth.json.

This drives that flow from the Accounts settings page:

- shim login mode (`node <shim> login <store-path>`): runs Cursor.auth.login
  with openBrowser:false, streaming {"ev":"auth-url"} then "logged-in" /
  "fatal" JSONL; the minted key lands in an engine-chosen store file, never
  the live login. Keys are dashboard-named "zeron — <hostname>".
- agent_accounts grows a Cursor provider: detection reads the SDK's
  StoredSdkCredentials (email + key expiry; expired keys warn and show
  "Key expired" instead of failing runs mysteriously), slots snapshot the
  store file, switching rewrites it — the codex slot machinery, generalized
  (LoginFlow::Codex → LoginFlow::Spawned, shared child wiring). First
  connect on a device with no usable live login auto-activates the minted
  key, so runs work immediately after the browser round-trip.
- Accounts page renders the Cursor card (Connect Cursor, browser-poll
  dialog with cursor-specific copy); run-time auth errors now point at
  Settings → Accounts instead of only the env var. CURSOR_API_KEY still
  wins as the headless override.

Live-validated: the real shim in login mode emits the loginDeepControl URL
immediately and polls Cursor's backend (browser completion covered by the
fake-shim integration test; full round-trip needs a real account login).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.2.7

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* landing: point downloads at v0.2.7

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cursor: live model discovery + typed model options actually applied

The picker showed only Auto + Composer 2.5 — a static catalog from #136,
built on the assumption that Cursor.models.list() required auth. It
doesn't (verified live, logged out, 1.0.28): the endpoint returns the full
catalog — 37 models with typed parameter definitions (thinking, context,
effort, reasoning, fast, Auto's optimize_for), display names, aliases, and
default variants.

- shim models mode (`node <shim> models`): prints the live catalog as one
  JSONL frame.
- CursorHarness::models(): spawns it (15s bound), maps ModelListItem →
  picker Model — parameters become ModelOption choice sets, isDefault
  variants set the default choice, the bare `default` alias twin of the
  parameterized Auto entry is skipped. Successful non-empty catalogs cache
  (OnceCell); failures fall back to a minimal static pair, uncached so the
  next picker open retries.
- Model options now REACH the run: the run frame carries modelOptions and
  the shim folds them into the SDK's ModelSelection params — the old
  static "optimizeFor" choice was picker decoration that never left the
  app. Legacy stored optimizeFor/speed picks translate to
  optimize_for/cost instead of sending ids the backend never knew.

Live-validated end-to-end through the real harness path: 36 models with
options and catalog defaults (examples/cursor_models_probe.rs, kept for
pin-bump revalidation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.2.8

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* landing: point downloads at v0.2.8

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cursor: isolate each SDK run's agent store — concurrent runs no longer lock

"cursor agent failed to start: database is locked" (user report): the SDK's
default local store is SQLite keyed by WORKSPACE (getDefaultSdkStateRoot(cwd)),
designed for one process per workspace. Zeron runs concurrent shims on the
same cwd as a matter of course — a chat turn and its title generation start
together, and two chats can share a worktree — so the second Agent.create
finds index.db locked and dies. Reproduced live: 3 concurrent shims, one
ready, two fatal with exactly the reported error.

A SHARED JSONL store is not the fix — JsonlLocalAgentStore serializes writes
only process-wide and rewrites whole files on update. Instead every run gets
its own JsonlLocalAgentStore directory under ~/.zeron/cursor-state/agents/
(one agent per store, so the backend's small-catalog caveat never applies,
and the native sqlite3 dependency drops out), and a one-writer marker file
(by-agent/<agentId> → dir, atomic tmp+rename) lets Agent.resume from a later
process find the agent's store. Pre-isolation agents have no marker and
resume from the SDK default store exactly as before. ZERON_CURSOR_STATE_DIR
overrides the base for tests.

Live-validated (real SDK, 1.0.28): 4 concurrent creates on one cwd all reach
ready; resume round-trips through the marker in a fresh process; markerless
resume routes to the default store.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.2.9

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* landing: point downloads at v0.2.9

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(pi): subagents usage/cost + budget (ID01-434) (#12)

* feat(pi): subagents usage/cost + budget (ID01-434)

* feat(pi): subagents usage/cost + budget (ID01-434)

Cada hijo reporta usage (session-dir descartable) y el batch una línea
summary (wall vs cpu, costo o n/d, PAYG, skipped). Tope opcional
PI_SUBAGENTS_MAX_COST_USD. Review k3: path traversal en task.id,
leak de dirs, pid en batchKey, budget re-leído por batch.

---------

Co-authored-by: David <davidhsutton@gmail.com>

* fix(pi): subagents heartbeat + role timeout floors + honest timeout message (#13)

Los hijos corren pi -p (output solo al final): con timeouts bajos elegidos por
el orquestador LLM morian a los 5 min con output vacio, y el silencio >30s
hacia que el engine parkee el turno (chat mudo en la app). Heartbeat cada 20s
via onUpdate (tasks vivas + elapsed), pisos de timeout por rol que clampean el
timeoutMs pedido (implement/hard 900s, review 600s, research/grunt 300s;
deps.timeoutFloorsMs={} para tests) y mensaje de timeout que explica que la
task probablemente seguia trabajando. Hallazgo k3 aplicado: emitUpdate con
try/catch (teardown del host no tumba el batch).

Diagnostico y spec: Linear ID01-434 (comentario 2026-08-18). Review: k3 APROBADO.


Claude-Session: https://claude.ai/code/session_01LWydxxHbUSos6jJr2XfqjL

Co-authored-by: David <davidhsutton@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* opencode: ACP harness with subagent viz off the sidecar event bus

Adds OpenCode (SST's opencode, `opencode acp`) as an ACP harness — the
new-agent recipe end to end: HarnessId variant, AcpAgentSpec (no npm shim:
the opencode-ai package ships a native binary, so the CLI itself is the
ACP server), registry_lazy slot, harness_slug, pixel-"o" brand mark, and
the settings/picker arms. Model discovery rides the existing config-option
probe (verified: the model select lists every configured provider with no
auth); no thought_level option exists, so the effort ladder is empty
(hermes parity). Steering is turn-boundary; loadSession resume works.

Subagent virtualization: opencode's `task` tool call rides the parent
session/update stream, but the child transcript never touches the ACP
wire, and 1.18 moved session storage to SQLite — so no grok-style JSONL
tail. Instead the harness exploits the fact that `opencode acp` doubles as
opencode's HTTP server: runs pass `--port <free>` (the shared default 4096
loses silently under concurrency) and OpencodeTracker tails the `/event`
SSE bus. session.created (parentID + "{description} (@agent subagent)"
title) binds children to task chips ahead of the completion's
authoritative rawOutput.metadata.sessionId; message.part.updated/delta map
to tagged AgentEvent::Subagent traffic (token-level text, typed tool
chips with results); the ACP task completion settles the chip with a
tagged Done after a short drain. Everything fails soft to chip + final
<task_result> output. A normalize arm keeps the "Agent: {description}"
chip naming across the completion frame, which drops rawInput but keeps
the spawn signature in rawOutput.metadata.

Live-validated against opencode 1.18.18 through the real harness path
(examples/opencode_subagent_probe.rs, kept for bump revalidation) with a
mock OpenAI-compatible provider forcing deterministic spawns: single task
and two concurrent tasks both stream fully tagged interior transcripts
attributed to the right chips. Engine needed zero changes (generic
Subagent routing from the native-drivers work).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(pi): subagents streaming real via --mode rpc (ID01-461) (#14)

Los hijos pasan de 'pi -p' (output opaco hasta el final) a 'pi --mode rpc':
prompt por stdin JSONL y parseo de los eventos del stdout (text_delta,
toolcall_end, tool_execution_start, message_end, agent_settled). El heartbeat
del padre ahora muestra QUE esta haciendo el hijo, no solo cuanto lleva vivo.
Modo print queda como fallback (deps.childMode / PI_SUBAGENTS_CHILD_MODE=print).

Correcciones de 3 rondas de review adversarial (modelo distinto al implementer):
- stdin sin handler 'error': un EPIPE async tumbaba el pi PADRE.
- cierre: stdin.end() primero + reaper TERM/KILL y resolucion en 'exit' (no mas
  hijos huerfanos fuera de 'live'; usage leido con el jsonl flusheado).
- gracia post-settle solo si el cwd tiene .pi-verify.json (verify-gate reinyecta
  turnos de reparacion): cancela con cualquier evento, 15s, cero costo si no aplica.
- timeout manda sobre el cierre ordenado salvo que el cierre haya empezado antes
  (no reportar exit 0 un task que reventó el tiempo, ni 124 uno que ya termino).
- cap de linea JSONL 1M chars con descarte reportado; buffers de salida acotados
  de forma continua; rpcError transitorio limpiado solo en retry del assistant;
  timers de señales en un Set por task (no mas aliasing en pendingKills).
- si el pi instalado no soporta --mode rpc: hint accionable + stdout crudo.

67/67 tests (incluye reaper sin huerfano, EPIPE del runtime, settle post-abort,
linea gigante, error transitorio, multi-mensaje). E2E real en la Beelink.
Linear: ID01-461. Review: APROBADO.


Claude-Session: https://claude.ai/code/session_01LWydxxHbUSos6jJr2XfqjL

Co-authored-by: David <davidhsutton@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* send: worktree creation rides the durable command plane; relay calls get deadlines

New-worktree sends wedged on "Sending…" forever while the session ran
remotely anyway: the composer's blocking CreateWorktree relay RPC had no
timeout, so a silently lost relay reply (the DO's auto-pong keeps a dead
host socket looking healthy) hung the send task after the remote had
already created the worktree and — via a retry — run the session.

Two fixes, defense in depth:

- RunRequest grows an additive `worktree: Option<WorktreeSpec>`: the HOST
  materializes the worktree at command-drain time, repoints the chat row's
  cwd, and stamps the actual zeron/<name> branch. The composer no longer
  makes ANY relay call on the send path — createChat and QueueCommand are
  local, so a new-worktree send is exactly as durable as a plain send. The
  resolved request (spec taken, cwd = worktree path) is what gets journaled,
  so resumes and steer→new-turn fallbacks reuse the checkout; a duplicate
  spec-carrying Run reuses the chat's existing worktree instead of minting
  another. Old hosts ignore the unknown field and degrade to the main
  checkout instead of failing.

- Relay-forwarded unary calls get a reply deadline (30s interactive, 120s
  CreateWorktree, 15m network git/update). On expiry the peer link is
  invalidated — the zombie socket re-dials — and the caller sees a
  retryable error instead of hanging forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* harness: native drivers rediscover slash commands

The ACP→native cutover (#136) silently dropped slash-command discovery
for claude/codex/cursor: the adapters used to advertise
availableCommands, and the native drivers inherited the trait's empty
default — only ACP-path agents (grok/hermes/pi, now opencode) kept a
populated composer popover.

- claude: short-lived probe sends the `initialize` control request on
  the stream-json channel (the same handshake the Agent SDK's query()
  opens) and reads commands + argument hints out of the
  control_response. No user message is written, so no model turn and no
  API cost (verified live on 2.1.228: 48 commands in ~1s). The probe
  needs `--verbose` — mandatory with --print + stream-json output.
- codex: short-lived `codex app-server` handshake + `skills/list`, the
  only invocable-listing method on the 0.146.x wire (custom
  ~/.codex/prompts are not exposed; verified against the full method
  list). Skills are deduped across cwd groups; the interface's
  shortDescription wins over the model-facing paragraph.
- cursor: documented no-op — @cursor/sdk 1.0.28 has no slash-command
  surface, so the empty default is the honest answer.

Both discoveries cache on success only (ACP parity), time-bound at 10s,
and tear the child down immediately. Fake-CLI tests cover parsing,
dedupe, hint mapping, and caching; live `#[ignore]`d smokes cover the
real binaries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.2.10

* landing: point downloads at v0.2.10

* accounts: keep the list order stable — always

Two ways the accounts page reshuffled, both gone:

- Same-millisecond logins (issue #161): the sort key was created_at
  alone, ms-resolution; a tie fell back to read_dir enumeration order,
  which is filesystem-arbitrary for UUID-named slot files — the flaky
  cursor_slot_swap_round_trip flip. New slots now get stamped strictly
  after every sibling (max+1 bump in write_slot), and the sort carries
  the slot id as a deterministic tie-break for pre-existing collided
  files.

- Switching accounts moved the switched-to card to the top: the engine
  deliberately lists in creation order ("never active-first"), but the
  UI's provider_accounts re-sorted active-first, defeating it. The
  re-sort is removed; the Active badge already marks the live account,
  and a list that reshuffles under the click reads as broken.

Fixes #161.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* harness: opencode probe example builds again post-worktree field

PR #159 added RunRequest.worktree and updated every initializer except
the opencode probe example, which landed in parallel via #157 — the
cross-PR race broke `cargo test --workspace` (examples compile there;
no CI job builds them).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* transcript: spawn chips link to the subagent tab, not an accordion

The spawn chip's expandable body only repeated the subagent's own
transcript (and its footer 'Open subagent' row was easy to miss). The
chip is now a plain link: clicking anywhere on it opens the subagent
tab, and the trailing tile shows a top-right open-arrow (new
arrow-up-right icon, arrow-up rotated 45°) instead of the accordion
chevron. ChipAffordance collapses to the blob-fetch struct now that
the subagent slot is gone.

ZERON_MOCK_SUBAGENT=1 makes the mock harness fan out two scripted
subagents (tagged AgentEvent::Subagent traffic) — the data-side way to
put spawn chips and their openable docs on screen in the demo rig.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* attachments: 10x chunks, parallel window, overall deadline, live progress

An image send could crawl indefinitely on a flapping link while reading
only "Sending…": per-chunk timeouts retry 3x, so chunks that succeed on
attempt 2-of-3 never trip the consecutive-failure abort, and a ~3MB
screenshot was ~70 sequential 45KB relay round trips — hours of lawful
crawling with no feedback and no overall bound (2026-08-18 user report;
text-only sends are all-local and instant, which is why only image sends
looked hung).

- UPLOAD_CHUNK_B64_CHARS 60_000 → 680_000 (~510KB binary per chunk,
  ~35% headroom under Cloudflare's 1MB WebSocket message cap). Chunk
  slot files engine-side are size-agnostic; no engine change.
- Three chunks in flight (seq slots are…
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