Effort looked and behaved differently per harness. Codex and OpenCode
had no toolbar button and called it "Reasoning effort", Claude called the
unset choice "Auto" while others said "Default", Chinese used both 思考强度
and 推理强度, and the status bar and header showed effort only for
Codex, OpenCode and Pi.
- Codex/OpenCode get the same toolbar effort button as Claude; it opens
the same anchored Effort section.
- Every effort picker is titled Effort (推理强度), and the unset choice is
Default everywhere. An unset toolbar button reads "Default effort".
- The status bar, header, shared-turn metadata and Codex agent tool cards
use one "effort <level>" label. Claude and Grok now show their effort,
with "default" when unset; Pi and AGY show it only when set, since
their models may not take one.
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): spawn-peer with required remit and MCP spawn_peer
Machine spawn creates an empty composer; stuffing message into that
JSON is stripped. Add hapi spawn-peer and MCP spawn_peer that spawn,
rename, deliver via the ping-peer path, and fail closed unless the
remit is on the new session. Reject message/prompt/text on machine
spawn with 400. Do not auto-approve spawn_peer in default/read-only.
Fixes#1509
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): reject overlong spawn-peer names before spawn
Hub rename max is 255; continuing past PATCH 400 left a live session under a different title. MCP/steer copy now says the hub 400s a spawn-body remit instead of stripping it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): wait for fresh spawn-peer child instead of resuming it
Machine spawn can return while the hub row is still inactive. ping-peer then POSTed /resume, which can launch a second agent because the original spawn has no existingSessionId for runner dedupe.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): reject flavor-illegal permissionMode before spawn-peer
Codex + bypassPermissions (and Pi + any mode) used to reach the runner and fail after a child existed, or silently drop the mode. Check isPermissionModeAllowedForFlavor when both agent and mode are explicit.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): treat omitted spawn-peer agent as Claude for permissionMode
Hub and runner default agent to claude when spawn omits it. Validate permissionMode against that effective flavor so yolo/read-only fail closed before a child starts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): resolve spawn-peer dir locally and archive failed children
Relative --dir values were forwarded unchanged, so the runner resolved them against daemon cwd. Remit delivery failures also left the new session running, so a retry spawned a duplicate. Resolve in the caller and best-effort archive the child before rethrowing.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): verify remit before archiving a failed spawn-peer child
A timed-out ping-peer POST can still land the remit. Check the transcript before archive, and only say the child was archived when the hub returns ok.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): swallow spawn-peer transcript GET errors and skip unapplied names
A thrown messages GET after spawn skipped archive and leaked a live child. Treat transport errors as a miss so the verify loop can retry, then clean up. Also do not return a requested name the hub never applied.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(spawn-peer): hub peerSpawnDefaults + model/effort spawn surface
Add hub settings peerSpawnDefaults (agent, permissionMode, per-flavor models)
with stock claude/yolo/sonnet fallbacks. spawnPeer resolves explicit args →
hub settings → stock, maps yolo to flavor equivalents, and forwards model/effort
into machine spawn. Settings UI under General → Agents; MCP/CLI optional overrides.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(hub): use cursor-valid permission mode in peer spawn defaults test
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): teach spawn_peer that omit permissionMode means yolo
MCP copy said "pass permissionMode only when the operator asked", so agents
omitted it and got hub/stock whatever was stored — often approval-gated
default. Clarify omit = yolo via peerSpawnDefaults/stock; pass only to tighten.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub,web): apply peerSpawnDefaults to machine spawn and New Session
POST /api/machines/:id/spawn now resolves omitted agent/permissionMode/model
from hub Settings, and New Session seeds from the same peerSpawnDefaults so
UI create and peer spawn share one authoritative default (stock yolo).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): NewSession hub seed respects sticky prefs + QueryClient in tests
Sticky localStorage last-used agent wins over hub seed; machine spawn still
applies peerSpawnDefaults server-side. Tests provide QueryClientProvider.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(web): mock CREATABLE_AGENT_FLAVORS for settings general peerSpawn UI
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(web): stabilize NewSession tests after hub peerSpawnDefaults seed
Wrap remaining mounts in QueryClientProvider, pre-seed hub settings so a
late useQuery resolve cannot wipe in-test model picks, and use neutral
stock permission defaults in the fixture.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(spawn-peer): address tip Codex Majors for peerSpawnDefaults
Preserve explicit yolo booleans on machine spawn, fail closed when hub
defaults cannot load, validate permission modes against the resolved
agent, remap auto-approval across flavors, and stop late hub settings
from wiping New Session edits. Reject retired agents in spawn defaults
and restore the CLI executable build docs.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(spawn-peer): address follow-up tip Codex Majors
Stop absent Yolo localStorage from beating hub defaults, track New Session
edits before late hub seed, reject incompatible machine-spawn permissions,
resolve MCP spawn_peer paths against the session working directory, and
hide unsupported Codex safe-yolo from Settings spawn defaults.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(spawn-peer): clear tip-2 CI and follow-up Codex Majors
Omit permissionMode for Pi/DSH, preserve explicit Yolo-off across delayed
hub seed, fix settings test mocks for launch permission helpers, and
restore unrelated CLI README lifecycle/ACP docs.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(spawn-peer): tip-3 Majors for seed edits and launchable modes
Skip hub agent seed after permission edits, resolve inherited modes via
the launch catalog (AGY request-review fallback), preserve hub Cursor
models through preference restore, and restore Codex auth README nuance.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): tip Minors for Cursor/Grok hub permission seeding
Give Cursor the native permission select so Plan/Ask survive machine
spawn, and re-apply Grok hub permissions after agent-init resets.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(spawn-peer): tip Minors for Cursor select and hub seed remap
Map hub permission through resolvePermissionModeForFlavor, keep native
Default when agent is explicit without a model, and assert Cursor Plan/Ask Mode labels.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): tip Minors for delayed hub seed and Default model
Apply hub permission/model to unset fields even with a sticky agent,
preserve explicitly saved auto models, and seed OpenCode/AGY selectors
from hub defaults.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(spawn-peer): tip Minors for model edit sticky and clear override
Mark OpenCode/AGY model picks as edited so delayed hub seed cannot
overwrite them, and treat empty hub model values as clear sentinels.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(spawn-peer): tip Majors for models patch typing and remit page scan
Type the models patch as NonNullable and paginate transcript verification
oldest-ward so a busy child is not archived when the remit leaves page 1.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): tip Minor — keep agent after explicit model pick
Do not apply a late hub default agent when the operator already chose a
model; otherwise preference restore drops that selection.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(spawn-peer): tip Cursor YOLO migrate and Codex effort map
Bridge Cursor's saved YOLO toggle (incl. false) into native Default over
hub yolo, and send Codex/OpenCode --effort as modelReasoningEffort.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): tip Majors — scoped YOLO bridge and catalog fallback
Keep unsupported saved permission modes at Default instead of hub yolo,
and apply delayed YOLO migration only when the sticky bridge agent matches.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): drop rebase conflict markers in startHappyServer
Both sessionDisplayRename and spawnPeer imports are required after
rebasing onto upstream rename work; markers had slipped into tip.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): wrap Kimi NewSession tests in QueryClientProvider
Hub settings useQuery requires a provider; upstream Kimi catalog tests
rendered bare and broke after the spawn-peer rebase onto main.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(peer): rename-proof Parent UUID stamp on spawn remits
Orchestrated spawn_peer / spawn-peer remits now auto-stamp a durable
[title](/sessions/<uuid>) Parent chip (fail-closed in-session). MCP copy
forbids inventing titles; UUID stays identity after parent rename.
Refs heavygee/hapi#175
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(peer): plain hyphen in Parent-stamp warning and test comment
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): reject blank remit before Parent stamp
Codex review on #1922: stamping first turned empty remits non-empty and
bypassed the idle-session guard. Also only treat ## Parent blocks as
already-present so bare /sessions citations still get the chip.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): gate New Session Create on hub defaults; require Parent chip
Codex tip review on #1922: block Create while getHubSettings is pending so
stock mount defaults cannot bypass restrictive hub peerSpawnDefaults; skip
Parent stamp only when ## Parent already has a markdown session chip; clarify
codex resume takes a native thread id.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): resolve MCP spawn cwd; Agents settings section
Codex Major on #1922: spawn_peer relative directories now prefer the
launcher cwd (Codex --cd), then skillLookup, then session metadata.path.
Move peerSpawnDefaults UI into the unused Agents settings section.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): pass session cwd to MCP spawn_peer; Agents settings section
Codex tip review on #1922: resolve relative spawn_peer directories from the
launcher working directory (Codex --cd), skillLookup, then metadata.path;
move peerSpawnDefaults UI into the Agents settings section.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): skip archive on unread transcript; validate startingMode after hub agent
Codex tip review: do not archive a child when remit verification cannot read
the transcript; run AGY/DSH remote-only startingMode checks against the
resolved hub agent; update MCP bridge test expectations for workingDirectory.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): treat exhausted remit pagination as unverifiable
Hitting the messages page cap with hasMore still true must not count as an
observed-empty transcript — that would archive a child whose remit may still
exist on older pages.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(cli): list HAPI_API_URL as optional with localhost default
Codex tip review: the README had moved HAPI_API_URL under Required even
though configuration defaults it to http://localhost:3006 (and settings can
supply a URL). Also drop trailing whitespace flagged by git diff --check.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): re-verify empty transcript before archiving failed spawn
An earlier observed-absent page plus later unread GETs must not archive a
child whose remit may have landed after the first empty read.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): never auto-archive on spawnPeer failure path
Timed-out remit POSTs and empty-read/archive TOCTOU races can still land
work on the child. Leave the session running with its id for inspection
instead of terminating an in-flight peer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): classify empty_session from final remit verify
An earlier empty transcript read must not stick as empty_session once
later message GETs become unavailable — report verify_failed instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): recognize escaped brackets in Parent chip labels
Titles with ] were escaped in the chip label but the idempotency regex
stopped at the first raw bracket and re-stamped a second Parent block.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): avoid Parent chip regex backtracking on backslashes
Exclude \\ from the single-char label alternative so escaped pairs have
one parse path — long nonmatching backslash runs no longer explode.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(cli): MCP/CLI spawn_peer --machine for cross-host remits
Wire optional machine (UUID or hostname via GET /api/machines) through
MCP spawn_peer and hapi spawn-peer into existing spawnPeer({ machineId }).
Skip local directory existence checks for cross-host targets to match the
estate wrapper; keep fail-closed remit verify.
Fixestiann/hapi#1931
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): reject ambiguous spawn --machine label matches
Tip bot Major on #1922: hostname/displayName selectors that match more
than one online runner now fail closed with ambiguous + UUID guidance.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): preserve cross-OS absolute dirs for spawn --machine
path.resolve on the caller OS was rewriting foreign absolute roots
(C:\… on Linux, /srv/… on Windows) before the hub spawn POST. Detect
POSIX or Windows absolute paths and only resolve relative directories.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(peer): resolve spawn dirs after local vs remote is known
Defer directory resolution until the target machine is selected. Remote
spawns still preserve foreign absolute roots; local spawns use native
path.resolve so Windows drive-less paths like \repo bind to the caller
drive.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): apply delayed DSH model defaults without replacing edits
* fix(web): preserve hub permission defaults across form remounts
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: weishu <twsxtd@gmail.com>
- Agent icon spans both lines with a presence badge (pulsing while a turn
or background work runs); no dimmed rows or row spinners
- Unread is a semibold title plus accent time instead of a dot; pending
requests are amber hand/bubble symbols, keeping one dot per row
- Reconnecting/syncing banners appear after 2s; reconnecting is neutral,
amber stays for offline
Running sessions previously suppressed the ordinary unread dot, hiding
answers the user had not seen. Unread is now
manualUnread || contentRevision > readRevision on web, iOS and Android.
Web rows draw the running/background spinner beside the unread dot, and
unread outranks background status. Because session patches arrive before
the message they count, a chat following its visible tail hides its own
ordinary dot until the rendered acknowledgement lands (display-only; web
and iPad split view).
* feat(web): fork active and archived sessions from the session list
* fix(codex): preserve fork titles during history replay
* fix(web): limit list forks to supported Codex sessions
New sessions showed the working directory name until the first turn
finished and a generated or native title arrived. The Hub now records
the first non-command user prompt as metadata.promptTitle, with
whitespace collapsed and capped at 80 characters, and the web shows it
after name and summary but before the path fallback.
The prompt title is Hub-owned and kept outside summary, so the existing
"only while untitled" title sources (native ai-title, change_title, the
remote prompt fallback and automatic suggestions) still apply and
replace it. It is published as a versioned metadata patch, so it does
not race the thinking patch sent while the prompt is accepted.
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Track readable content revisions and durable read cursors in the hub. Acknowledge only rendered foreground tails, persist offline read operations, and preserve newer manual unread choices across devices.
Migrate existing history to a read baseline and cover pagination receipts with shared native conformance fixtures.
* fix(web): keep subagent parent when trimming the message window (#2242)
Tail mode trims to the newest 400 raw messages. When a long subagent run
fills that window, its Agent tool call falls outside it and the tracer
hides every sidechain row, leaving a single visible message. Keep the
dropped Agent/Task tool call whose sidechain rows survive, and derive
the older-page cursor from the contiguous window so the gap still loads.
* test(web): cover subagent parent pinning across repeated trims
* fix(claude): drop the obsolete 1M model presets
The opus, sonnet and fable aliases now resolve to models with a native 1M
window (Opus 5.5, Sonnet 5.5, Fable 5.1), so Claude Code documents the
"[1m]" suffix as having no effect on them and its own /model picker no
longer lists 1M rows. Remove Sonnet 1M / Opus 1M / Fable 1M from the
shared, Android and iOS catalogs.
Sessions stored with a "[1m]" value keep working: the composer keeps
the current model selectable, and the context-budget fallback treats any
"[1m]" model and the remaining aliases as 1M, so local-mode sessions on
plain opus/sonnet no longer show a 200k budget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): seed a 1M turn-1 context window for bare Claude aliases
With the 1M presets gone, a session on plain fable/opus/sonnet must not
seed 200k before its first result: those aliases now resolve to models
with a native 1M window. Seed 1M when the selected model is a preset
alias; an explicit full id still seeds conservatively until its result
arrives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(claude): offer the four current Claude models as presets
The Claude presets now mirror the aliases Claude Code's /model lists, in
its order: Opus, Fable, Sonnet, Haiku. Each alias resolves to the newest
model of its family, and all four have a native 1M window, so neither
1M variants nor pinned older versions are offered. Haiku was missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(claude): read the effective context window from get_context_usage
The 1M seed for the aliases is wrong for users who disable 1M
(CLAUDE_CODE_DISABLE_1M_CONTEXT) or cap it with autoCompactWindow. In
remote mode, ask Claude via the get_context_usage control request after
each system/init and use its maxTokens, which is what /context shows.
A result's raw contextWindow no longer overwrites that value. Older CLIs
that reject the request keep the existing seed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: weishu <twsxtd@gmail.com>
* fix(web): show session mentions without waiting for remote file search
* fix(web): preserve selected files during mention refresh
* test(runner): avoid fixture PID collision with test process
Persist and backfill the last user-message timestamp, and use it for deterministic session ordering across web and native clients while preserving manual pins.
Remove the fixed-order setting and guard ordering against transcript echoes, replayed messages, and stale REST/SSE updates. Add regression coverage and regenerate the SSE fixture.
New Session treated a disabled availability query (empty agents, not
loading) as "this machine has no CLIs." That painted the same string as
a real empty probe, next to "No machines available."
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(cli): add diff-shape tool input canonicalization helper
* fix(cli): canonicalize ACP diff-shaped tool inputs for the Edit/Write views
* fix(opencode): normalize local-mode edit/write tool parts to the diff view shapes
* fix(opencode): preserve replaceAll and fix id-less hook pairing
- canonicalizeDiffToolInput now carries OpenCode's optional replaceAll through
to the canonical Edit input as replace_all, so persisted history keeps
whether an edit replaced one occurrence or all.
- opencodeLocalLauncher computes the execute-hook pairing signature from the
raw name/input BEFORE canonicalizing, so an id-less 'before' with empty or
partial args shares a queue key with the full 'after'.
* fix(opencode): preserve snake_case replace_all in edit canonicalization
canonicalizeDiffToolInput accepts already-canonical snake_case edit inputs
(file_path/old_string/new_string), but only read camelCase replaceAll. Read
replace_all too so the execution argument survives every call site.
* fix(opencode): upgrade partial local edit/write calls when full args arrive
A partial native before ({filePath}) was emitted as-is and recorded as sent,
so the later canonical full input was deduped and the web Edit/Write view never
received the complete arguments. Track the emitted input per callId and, when
a partial native edit/write call is superseded by a canonical full input, emit
a replacement tool-call (same callId) before the result. Applies to both the
message.part.updated and tool.execute.* local event paths.
* fix(opencode): release completed tool inputs from upgrade tracking
emittedToolInputs retained the full emitted Write content and Edit old/new
strings for the whole launcher lifetime. Once a call is completed
(sentToolResults), later upgrades are already blocked, so drop the stored
payload in both result paths. Add a lifecycle test that a stale part for a
completed callId neither re-emits nor emits a duplicate result.
* style(opencode): restore 4-space indentation on local launcher tracking decls
The per-session tracking declarations (sentTextParts, sentToolCalls,
sentToolResults, emittedToolInputs) were flush-left inside opencodeLocalLauncher,
violating the repo's 4-space indentation rule.
* fix(opencode): gate diff canonicalization on edit/write semantics
canonicalizeDiffToolInput now takes the tool's semantic name/kind and only
normalizes known edit/write aliases (edit, write, write_file, replace, modify,
file_edit). The Edit shape (path + both old/new) is unambiguous on its own, but
Write is just {path, content} — a shape many non-edit MCP tools also accept —
so without the gate those tools were renamed to Write and their extra args
dropped. OpenCode maps both edit and write to kind 'edit', which stays in the
allow-list, so the native-shape cases keep working. ACP callers pass the update
kind (falling back to the derived name); local callers pass the tool name.
* fix(opencode): re-emit canonical edit/write when the input changes
shouldEmit only handled the null->canonical transition, so a shape-complete
Write/Edit that already canonicalizes (e.g. {content: ""}) was suppressed
when the final content arrived, leaving the web diff with stale input. Emit on
first usable input and re-emit whenever the canonical input value changes,
across both message.part.updated and tool.execute.* paths.
* test(opencode): cover changed-input re-emit on the message.part.updated path
The execute-hook path already had a regression for canonical inputs changing
from empty to final content; add the matching case for message.part.updated so
both local emit paths are covered.
* fix(web): render opencode native read output as a file content code block
OpenCode's lowercase read tool fell through to the generic markdown
result view, collapsing the whole file into a soft-wrapped paragraph
and dropping the <path>/<type>/<content> markup. Register the read
alias, unwrap the opencode payload (including the trailing
"(End of file ...)" trailer), parse its 1-indexed line numbering into
the code block gutter, and resolve the camelCase filePath key in the
card title.
* fix(web): only unwrap complete opencode read envelopes
Anchor the envelope match to the full <path>/<type>file/<content>
structure so plain Read results whose file text contains a literal
<content> element are left untouched, capture the body greedily so an
embedded </content> survives, and stop trimming real trailing
whitespace.
* feat(web): open a session in a separate window
* test(web): cover session sidebar restore control
* fix(web): preserve hidden sidebar across session navigation
* fix(web): retain hidden sidebar on session routes
* feat(cli): report claude compact completion with token delta
Replace the plain 'Compaction completed' line with a pi-style
'📦 Compacted (<before> → <after> tokens)' fallback built from the
system/compact_boundary metadata, and prefix the start notice with 📦.
The SDK stream carries no summary body (measured), so the delta line is
the terminal state of the completion event.
* fix(cli): hide local-command tag user messages from chat
Claude Code echoes slash-command bookkeeping back over the SDK as plain
user-role messages wrapped in <local-command-*> tags. Flag them isMeta so
the existing downstream filters drop them instead of leaking raw stdout.
* feat(web): collapse compact summary card by default
Keep the header (title, token delta, timestamp) always visible and hide
the summary body behind a chevron toggle. Toggle state is component-local
and non-persistent.
* feat(cli): promote claude compact summary from the local transcript
* fix(cli): don't double-render the manual /compact boundary as a status line
* fix(cli): detect later-turn /compact and ignore stale transcript summaries
* fix(cli): establish the compact transcript baseline before the result can race it
* fix(cli): narrow the local-command meta filter to the compact stdout echo
* fix(cli): range-read only the transcript tail when polling for the compact summary
* fix(cli): complete the transcript tail read before parsing the compact summary
* fix(cli): refresh the context bar with post-compaction tokens after /compact
* fix(cli): scope the Compacted stdout suppression to the active compact turn
* fix(cli): make compact lifecycle ownership race-safe
* fix(cli): publish compact outcome before accepting the next turn
* fix(cli): cancel compact completion with its failed stream attempt
* fix(cli): propagate deferred compact completion failures
* refactor(cli): coordinate compact completion in the response loop
* fix(cli): correlate compact completion with its stream signal
* fix(cli): commit compact ownership when its result arrives
* refactor(cli): name the manual compact stream suppression predicates
* fix(cli): suppress the compact echo even when hook output is appended
* fix(cli): carry the resolved context window into the compact usage refresh
* feat(cli): list local OpenCode sessions over machine RPC
* feat(hub): import OpenCode sessions into HAPI
* feat(web): import OpenCode history from new-session picker
* fix(cli): derive OpenCode imported message ids from part ids
Random ids made every scan differ, so delta classification flagged
unchanged transcripts as diverged on re-import.
* fix(cli): surface unreadable OpenCode db as rpc error
A locked or unopenable opencode.db now fails the listing instead of
reporting an empty success; summaries normalize timestamps and the web
import toast counts only created or actually-appended sessions.
* fix(hub): fail closed on opencode re-import past live HAPI continuation
Imported entries must remain a strict prefix of the native transcript.
Insertions ahead of the imported boundary or new native messages after
the session continued in HAPI now fail closed instead of appending
duplicated or reordered history.
* fix(web,hub): apply selected launch config when importing OpenCode history
Pass the model, reasoning effort, and permission mode chosen in the
new-session picker through the import request and persist them on the
imported HAPI session via applySessionConfig, so reopen launches with
the visible selection instead of stale or default values.
* fix(hub,cli): preserve explicit launch resets and skip streaming parts on import
- Launch config keys are applied by presence, so explicit null/default
selections from the picker must reach applySessionConfig to clear
stale model, effort, or permission values on re-import. Requests that
omit the fields entirely still leave persisted config untouched.
- Assistant text parts are mutable while a response streams. Mirror the
live scanner gate and import only finished (time.end set), synthetic,
or user text so an in-progress row can never be stored and later
finalized as transcript_diverged.
* test(hub): fix launch-config reset assertions to satisfy strict types
* fix(cli): fail opencode transcript reads on query errors instead of partial snapshots
Swallowed SQLite errors could return a truncated message list that the
hub later rejects as transcript_diverged with no repair path. Full
transcript reads now propagate query failures; summary lastUserMessage
extraction stays resilient per message.
* test(cli): cover unreadable-part-table transcript read stays non-partial
* fix(hub): scope launch-config failures to the per-session import result
A thrown applySessionConfig call used to escape the bulk handler as an
unstructured 500 even though transcripts were already persisted. Catch
it per session and report it as a config_failed result so remaining
sessions in a bulk request still import.
Also cover the mid-transcript part-query failure with an injected
opener that rejects instead of returning a holey transcript.
* fix(cli): read opencode transcripts inside a deferred snapshot transaction
Messages and their parts were read in separate statements, so a
concurrent OpenCode write could leave a hole in the imported sequence
that positional re-import checks then reject forever. Wrap each full
transcript read in BEGIN DEFERRED/COMMIT with rollback on failure.
* fix(cli): follow machine path policy for OpenCode imports
* fix(web): use unified permission mode for OpenCode imports
* fix(opencode): harden session import consistency
* fix(hub): preserve metadata on rejected OpenCode imports
* fix(hub): refresh OpenCode import state under lock
* fix(hub): resolve OpenCode imports after locking
* fix(opencode): bind imported resume to source machine
* feat(web): persist and send voice input message across session navigation (#1435)
* fix(web): address bot review findings for voice send across session switch (#1435)
* fix(web): resolve teardown, inactive session, send error recovery and mention serialization findings (#1435)
* fix(web): preserve draft initialText on error and handle async cross-session permission lookup (#1435)
* fix(web): preserve session draft on background failure and send via composer delivery policy (#1435)
* fix(web): send dictation directly via API and fix standard dictation language and recording support (#1435)
* trigger CI rerun for voice dictation session switch fixes (#1435)
* fix(web): prevent failed voice sends from clobbering replacement composer text (#1435)
* fix(web): prevent zero-byte recording failure from clobbering replacement composer draft (#1435)
* fix(web): prevent unmount voice transcription, clear draft on send success, and clear partial transcript (#1435)
* fix(web): stop button transcribes into composer without auto-sending; only send button triggers stopAndSend (#1435)
* ci: rerun CI for flaky claudeRemote test timeout (#1435)
* ci: retry flaky claudeRemote test (#1435)
* fix(web): guard dictation send path against attachments/schedule/connecting and protect newer drafts from late overwrite (#1435)
* fix(web): preserve Pi steer delivery mode for background dictation sends (#1435)
* fix(web): include active state in handleSend dependency array (#1435)
* fix(voice): surface MediaRecorder failures instead of transcribing partial audio
A MediaRecorder error can still be followed by dataavailable and stop
with partial bytes, so the previous onstop-only flow could transcribe
and auto-send corrupt audio. Track recorder.onerror and short-circuit
the stop handler: restore the draft and surface an error instead of
transcribing. Add a regression test for error -> dataavailable -> stop.
* fix(web): resume inactive sessions for voice dictation direct-sends
A voice dictation direct-send (mic -> send) posts the transcribed message
straight to the session, bypassing the text pipeline's inactive-session
resume. The hub rejects message POSTs to inactive sessions with 409
session_inactive, so sending to an archived session always failed (while
reopening it first made the same send succeed).
- stopAndSend now accepts { resolveSessionId, onSessionResolved } and
resolves the target session after transcription, before the POST
- HappyComposer allows direct voice sends on inactive sessions when a
resolver is provided (instead of downgrading to a plain stop), and
threads SessionPage's canonical resolveSessionId + handleSessionResolved
(the same ones the text send path uses) into the dictation hooks
- after the message is delivered to the resumed session, the UI
navigates/seeds the live session; failed sends keep the existing
draft-restore + error surfacing
- realtime dictation sends apply the same resolution
* fix(web): satisfy HappyChatContextValue in markdown-a test helper
Upstream 51ae260a added the required showSessionSummaryInChat field to
HappyChatContextValue but missed the markdown-a test helper, so web
typecheck (part of CI) fails at the upstream tip. Provide the field like
the sibling test helpers do.
* fix(web): expose a Send action during connected dictation
The visible composer button becomes Stop-only while dictation is
connected, so mouse/touch users could never reach the session-bound
stopAndSend path in handleSend (keyboard Enter was the only entry
point). Add an explicit Send button next to Stop when dictation is
connected; it routes through onSend('default') so the transcript is
captured to the target session and survives navigation (per #1435),
while Stop keeps its stop-without-sending semantics.
Addresses HAPI bot review on #1436.
* fix(web): hide dictation Send button when direct send is not eligible
The Send button shown during connected dictation only performs a real
send when handleSend's direct-send path runs; with attachments, a
pending schedule, or scratchlist mode it silently falls back to
dictation.toggle() (stop + transcribe). Gate the button on the same
eligibility conditions as handleSend so a visible Send control always
actually submits.
Addresses HAPI bot review (Minor) on #1436.
* fix(web): drop superseded markdown-a test context fix
Upstream merged its own showSessionSummaryInChat fix for the markdown-a
test helper; the duplicate property added for the pre-merge CI break is
no longer needed.
* fix(web): gate dictation direct-send on full attachment state and guard late navigation
- dictationDirectSendEligible now uses blocksScheduling (visible + hidden
persisted + still-hydrating attachment drafts) instead of only the
visible attachments array. An inactive composer keeps persisted
attachment blobs out of the visible array, so the previous check could
expose direct Send, resume the session, and POST only the voice text,
stranding the blobs on the obsolete source session. handleSend's
canDirectSend and the Send-button gate share one helper so they can
never disagree.
- The voice send survives unmount by design (#1435); a send that finishes
after the operator left the source session no longer navigates them
back. handleVoiceSessionResolved only navigates when still on the
source session's page.
Addresses HAPI bot review (Major + Minor) on #1436.
* fix(web): read live route state when a background voice send completes
The previous guard captured the render-time pathname; once the source
SessionPage unmounts, that captured value still points at the source
route, so a voice send that finishes after the operator left would still
navigate them back. Read router.state.location.pathname at callback time
instead (the router instance is stable; its state is always current).
Addresses HAPI bot review (Minor) on #1436.
* fix(web): recover post-resume send failures under the resumed session id
When a voice dictation send fails after the inactive session was already
resumed, the catch path persisted the retryable transcript under the
archived source session id — which the hub has superseded — so the
recovery was invisible on the live session. Save the draft under the
resumed id, and fire onSessionResolved so the operator is taken to the
live session where the retryable text lives. Applies to both standard
and realtime dictation.
Addresses HAPI bot review (Major) on #1436.
* fix(web): snapshot resumed-session draft before the post-resume send
The previous recovery guard read getDraft(targetSessionId) inside the
catch — after the send already rejected — so it compared the current
draft to itself and always overwrote text the operator typed into the
resumed composer while the request was in flight. Snapshot the resumed
session's draft at resolve time instead, so a newer draft is never
clobbered. Applies to both standard and realtime dictation.
Addresses HAPI bot review (Major) on #1436.
* fix(web): preserve both drafts when a post-resume send fails
When the operator typed a newer draft into the resumed composer while
the voice send was in flight and the send then failed, the recovery
guard correctly refused to overwrite the newer draft — but that dropped
the failed voice text entirely. Merge instead: append the failed message
after the newer draft so both remain recoverable. Applies to both
standard and realtime dictation.
Addresses HAPI bot review (Major) on #1436.
* fix(web): merge live composer text into failed voice-send recovery
Failure recovery previously read only the persisted draft store, but
while the composer is mounted its edits live in memory (session storage
is written only on unmount). If the operator typed replacement text
while transcription or delivery was pending, recovery either skipped the
composer restore (persisting only the voice text, which the later unmount
then overwrote) or clobbered the replacement. Recovery now reads the live
composer text when mounted and appends the failed voice text after it,
reseating the composer to the merged draft; the unmounted case merges a
persisted draft written since the snapshot. Applies to recording,
zero-byte, transcription, send, and realtime failure paths in both
dictation implementations.
Addresses HAPI bot review (Major) on #1436.
* fix(web): preserve dictation audio constraints
* fix(web): transfer follow-up drafts after voice resume
* fix(web): merge source drafts on failed voice resume
* Preserve resumed voice drafts and replace recovered composer text
Sample destination/source draft merges at persistence time; use a replacement callback for realtime recovery. Six regressions fail before the fix; 66 focused tests, all five package suites, typecheck and 16 Playwright checks pass.
* Recover delayed voice failures into the current composer mount
Resolve live recovery targets by session identity and update the draft ref synchronously before later unmount. Both standard/realtime remount regressions fail before the fix. 85 focused tests, full package suites, typecheck and 16 Playwright checks pass.
* fix: preserve live composer drafts after background voice success
Use shared transfer sampling for standard and realtime sends; update the mounted destination before navigation and unmount. Verified focused voice/transfer tests, typecheck, all five full package suites (CLI rerun serially), and 16 browser checks.
* fix: retain resumed voice follow-ups and destination attachments
Verified focused regressions, root typecheck, all five complete package suites (environment-sensitive failures rerun serially), and 16 browser checks.
* fix: preserve independent destination drafts during voice recovery
Cross-session recovery always retains destination text and independently merges source follow-ups. Remove destination baseline replacement from both hooks. Verified 72 focused regressions, root typecheck, all five full package suites (Hub rerun serially), and 16 browser checks.
* fix: preserve voice recovery drafts before composer hydration
Expose persisted draft text until hydration and update the text ref synchronously when restoring. Cover both voice modes with a held animation frame and intentional empty text after hydration. Verified 94 focused checks, full package suites, root typecheck, and 16 browser checks.
* fix(web): retain ElevenLabs finalization across navigation
* fix(web): preserve new same-text drafts across pending voice sends
* fix(web): refresh voice send callback after draft hydration
* fix(web): resolve voice destinations from current session activity
* fix(web): give voice direct-sends a localId so agent echoes dedupe
Voice dictation, realtime dictation and the cross-session voice tool sent
through api.sendMessage with localId null. Shared Codex then minted a
random clientUserMessageId for the turn and echoed the user message back
with it; the hub dedupes only on (session, localId), so every voice
message was stored and shown twice.
Route all voice sends through sendVoiceMessage, which mints the same
local-* id useSendMessage uses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(codex): drop safe-yolo from Codex permission modes
Every Codex session runs on the native shared runtime (runCodex is
runSharedCodex), which rejects safe-yolo. The mode stayed in
CODEX_PERMISSION_MODES and six filters hid it again in the web picker,
composer, hub permission-mode route, hapi set-mode and both native
apps. API callers that validate against the flavor catalog (e.g. a
spawn with permissionMode: 'safe-yolo') still passed it through and
only failed inside the runner.
Remove it from the Codex catalog (shared, iOS, Android, fixture) and
delete the filters. autoReview is the native replacement. Safe Yolo
stays for Gemini, Kimi and Copilot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(ios): Codex option list now has four modes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix: list Claude background tasks from the snapshot instead of guessing
The status panel only had a count from `background_tasks_changed` and
reconstructed commands by regex-matching Bash results in loaded chat
history, so tasks whose start message was unloaded, auto-backgrounded
("moved to the background (ID: …)"), or non-Bash showed
"N running · exact command unavailable".
The hub now keeps the snapshot's tasks (id, type, description) on the
session alongside the count. The web panel lists them directly, using
the exact task ID to look up the Bash command when loaded and the
snapshot description otherwise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): only attribute a Bash command from its background launch result
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(native): accept backgroundTasks in strict session patch decoders
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Content growth and mobile-keyboard resizes increase the distance from the
bottom without any user gesture. handleScroll treated that as the user
leaving the tail and disabled auto-follow, so the session feed stopped
following new messages until it was scrolled down manually.
While tail-follow is active, treat a non-upward scroll as layout churn
and re-pin to the newest message instead of dropping out of tail mode.
Add regression tests for transcript growth and viewport shrink.
* feat(shared): derive Claude model choices from the CLI catalog
The choices are the rows of the claude CLI's own /model picker, in its
order and with its display names. The stored value is what
claude --model receives: an alias is resolved again each time the
session's claude process starts, a full id stays on its model, so
following and staying need no new field.
Effort support is read per model so a model switch can drop an effort
level the next model does not offer.
* feat(cli): discover Claude models from the CLI's initialize response
A machine RPC starts claude in stream-json mode, sends only an
initialize control request and reads the models it reports, so no turn
runs and no transcript is written.
claude --print runs a directory's project hooks and .mcp.json servers
without a trust prompt, so the probe starts from the home directory
with user settings only, no MCP servers and hooks disabled.
Successful catalogs are cached per claude executable for five minutes
and concurrent requests share one probe; failures are not cached.
* feat(hub): expose the Claude model catalog per machine
GET /machines/:id/claude-models forwards to the runner's
listClaudeModels RPC. A runner that predates the handler answers with
the stable rpc_target_missing code so clients can keep their built-in
list.
* feat(web): offer the machine's Claude models in the model pickers
The composer and the create-session form list the rows the machine's
claude CLI shows in its own /model picker, as it shows them. Built-in
presets the catalog does not list remain selectable after it.
Until the catalog arrives, or on a runner without discovery, the
built-in presets are shown as before. A saved Claude model is no longer
reset to Default when the built-in list lacks it; the create-session
form keeps it and marks it as no longer listed when the catalog
dropped it.
* feat(hub,web): keep Claude effort within what the selected model offers
For Claude sessions the model endpoint accepts an optional effort, so a
model switch and the effort it keeps reach the session as one config
change. Other flavors are refused: Pi, for one, commits the model
before its thinking level. When the machine's catalog says the next
Claude model does not offer the current effort, the composer clears it
in that same request. The create-session form drops such an effort
however the model was set, including a restored preference or draft.
Effort choices in both places narrow to the selected model's levels;
without a catalog every level stays available as before.
* fix(web): retry Claude model discovery after a failed probe
The machine answers a failed probe with success:false over HTTP 200.
The query cached that as an answer for its stale time, so a user who
signed in and reopened a picker within a minute still saw only the
built-in models. A failed probe is now a query error, which is not
kept, and the next picker to mount asks the machine again.
* fix(web): list only the Claude CLI's models once its catalog is known
The built-in presets were kept after the catalog so that a partial list
could not hide Opus. The catalog is the CLI's own /model list, though,
not a partial one, and the presets added entries the CLI does not show
(Sonnet 1M, Opus 1M, Fable 1M). With a catalog the list is now exactly
its rows; a saved model it does not list stays as the current model,
and New Session marks it as no longer listed. Without a catalog the
built-in presets are shown as before.
* fix(shared,web): list earlier Claude versions under Other models
Claude Code's model sheet shows each family's newest model first and
every earlier version under an "Other models" heading. The CLI's
catalog lists models in that order but does not say where the split
is, so the first row of each family is taken as its newest and later
rows of the same family as earlier versions.
The composer's model list and New Session's model select now show the
same two groups, keeping the CLI order within each. Nothing is folded:
every row stays one click away, as it does in Claude Code.
* test(web): pin that Claude 1M variants come from the CLI catalog
Claude Code 2.1.105, 2.1.200, 2.1.240 and 2.1.274 list a 1M choice as
its own catalog row (opus[1m], claude-fable-5[1m]); 2.1.303 and 2.1.310
list none, and on 2.1.310 opus and sonnet run with a 1M context window.
The catalog-only list therefore offers a 1M variant exactly when the CLI
does. Pin that and record why the built-in [1m] presets are not appended.