Commit Graph
701 Commits
Author SHA1 Message Date
SSU-WEI HUANGandGitHub f8e55fb930 feat(agy): reuse a persistent headless stream process (#2248) 2026-10-30 19:11:26 +08:00
013ace8e88 fix(cli): show resolved versions in Claude model labels (#2245)
Co-authored-by: Kiyoshi Guo <kiyoshi.guo@bytedance.com>
2026-10-30 14:37:59 +08:00
SSU-WEI HUANGandGitHub 5fcd4a067a fix(agy): separate model and effort selection (#2244)
* fix(agy): separate model families and reasoning effort

* fix(agy): report startup before model discovery
2026-10-30 07:36:13 +08:00
weishu d9acc0c31e Release version 0.33.3 2026-10-29 22:29:49 +08:00
3f8686e623 feat(claude): offer the four current Claude models, drop the 1M presets (#2153)
* 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>
2026-10-29 22:28:59 +08:00
Junmo KimandGitHub 97224c9dd7 fix(runner): recover sessions after host reboot (#2169)
* fix(runner): recover sessions after host reboot

* fix(runner): settle startup when process generation changes

* test(runner): wait for Hub RPC readiness before resume
2026-10-29 22:13:56 +08:00
JeeandGitHub 13fec3e157 fix(web): show session mentions without waiting for remote file search (#2170)
* 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
2026-10-29 22:13:38 +08:00
557ed3c99f perf(codex): reduce history replay during thread recovery (#2214)
* perf(codex): reduce history replay during thread recovery

* fix(codex): restore live child subscriptions during bounded replay

---------

Co-authored-by: gaodq <gaodq@koal.com>
2026-10-29 22:13:11 +08:00
AnanovoandGitHub 0f76485ad9 feat(web): support local Markdown images in conversation messages (#2215)
* fix(web): resolve relative Markdown assets in file previews

* fix(web): harden standalone Markdown asset resolution

* fix(web): preserve Markdown URI and image policies

* fix(web): route Markdown file links through app history

* fix(web): preserve session reference navigation in previews

* feat(web): support local Markdown images in conversation messages

* fix(cli): bound session file reads

* fix(cli): avoid blocking bounded reads on FIFOs

* test(cli): run FIFO regression with Bun

* fix(hub): preserve legacy session file RPC calls

* test(web): update markdown preview fixture context

* fix(cli): reject symlinked session files outside workspace
2026-10-29 22:12:57 +08:00
AnanovoandGitHub f29be07bb9 fix(codex,web): preserve late command result identity and timing (#2239) 2026-10-29 22:12:27 +08:00
weishu 9384ce031b feat(cli): add Codex-aware auto peer delivery
Default peer CLI and MCP messages to auto, steering eligible Codex turns and queueing otherwise. Preserve explicit delivery modes and uncertain-send safety, and clarify steer guidance for important time-sensitive findings.
2026-10-29 22:02:43 +08:00
weishu 730dfcb22e Release version 0.33.2 2026-10-29 13:24:52 +08:00
weishu 01dabad204 fix(sessions): stabilize ordering by latest user input
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.
2026-10-29 12:47:52 +08:00
2a75984b75 feat(cli): add peer delivery modes for CLI and MCP (#2197)
* feat(cli): select queue or steer for peer messages

* fix(peer): respect per-agent steering availability

---------

Co-authored-by: NightWatcher <hitszzzf@gmail.com>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-10-29 10:55:58 +08:00
weishu a6cf8fd078 Release version 0.33.1 2026-10-29 10:13:33 +08:00
weishu 47aad06866 fix(release): upgrade Bun and reject mismatched platform binaries
Pin the build toolchain to Bun 1.4.4 so CI includes the darwin-arm64 signing fix required by macOS 27.

Reject stale optional platform packages before launching, document clean reinstall recovery, and add launcher regression coverage.
2026-10-29 09:45:46 +08:00
weishu ee273464f4 Release version 0.33.0 2026-10-29 08:51:11 +08:00
weishuandClaude Opus 5.5 23122ccbc0 feat(hub): persist agent turns and report them through hapi usage
Whether a session's agent is thinking lived only in the session cache,
so once a turn ended nothing could say how long the agent had worked,
and a hub restart forgot even the turn in progress. Tools that track
time (Weft, for one) had no way to see when agents on other machines
were busy.

Schema v28 adds session_turns: a row opens when a session starts
thinking (a prompt queued through the hub, the agent reporting it,
or a patch) and closes when it stops, the session ends, or its
keepalives expire, at the last keepalive. Rows are only written on
those transitions, never per keepalive. Turns move with a session
when it is merged away.

At startup the hub closes turns left open by the previous process at
the session's newest message. If the agent carries on thinking within
10 minutes of the restart, the same turn continues, so the work between
that message and the restart isn't lost. The deadline counts from the
restart, and a keepalive or patch reporting idle, the session ending,
or a new prompt ends the chance: those start a new turn.

GET /api/activity returns, for the caller's namespace, each session's
turns in a window (default 24 h, at most 31 days) with what identifies
it (flavor, agent session id, machine, path, title) and the permission
prompts and questions it waited on, taken from agent_state.

`hapi usage [--since] [--until] [--json]` prints a summary or a
hapi.activity.v1 report for other tools, with distinct exit codes for
bad arguments (2), missing or refused credentials (3) and a hub too
old to answer (4). The summary counts only the part of each turn
inside the window and subtracts overlapping waits once. It reuses the
peer commands' token exchange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:48:06 +08:00
b4be7c2d93 fix(cursor): stop acknowledging soft steers that an abort cancels (#2229)
Abort sends session/cancel, and ACP answers the pending soft-steer
session/prompt with stopReason "cancelled". The completion handler
ignored the stopReason and acknowledged the row as steered, so a
message Cursor may never have run was shown as delivered.

Surface the stopReason from beginSoftSteerPrompt. On "cancelled" the
row is held as indeterminate (the same path a transport failure takes),
because Cursor may or may not have folded it into the cancelled turn:
the user resolves it with Retry or Cancel instead of a silent claim or
an automatic re-run after Stop.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:07:28 +08:00
Junmo KimandGitHub a4a5779829 fix: render opencode edit/write/read tool calls through the shared views (#1644)
* 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.
2026-10-28 12:17:14 +08:00
Junmo KimandGitHub 83088aae73 fix(agy): report the failure reason instead of echoing the answer (#1642)
A failed `result` envelope still carries the turn's answer in `response` —
agy decodes that field from the complete buffer regardless of status. The
driver used it as the failure text, so a failed turn delivered the whole
answer a second time as an error event, right under the streamed copy.

Read the reason from `error` instead, which is what agy actually reports
there (e.g. "timeout waiting for response"), and keep the existing
`agy turn failed: <status>` fallback when the envelope carries none.
2026-10-28 12:17:00 +08:00
AnanovoandGitHub 009e6d7799 feat(codex): allow historical forks while a turn is running (#2185)
* feat(codex): allow historical forks while a turn is running

* fix(web): fork user-only completed turns during generation
2026-10-28 12:15:11 +08:00
AnanovoandGitHub 577ab7546b feat(sessions): add explicit per-session title locks (#2072)
* feat(sessions): add explicit per-session title locks

* fix(sessions): preserve title locks across rename entry points
2026-10-28 12:14:48 +08:00
Junmo KimandGitHub 328ddb0ee3 feat(cli): show Claude compaction as a summary card with token delta (#1691)
* 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
2026-10-28 12:14:34 +08:00
Junmo KimandGitHub ae147e85db feat(opencode): import local session history (#1677)
* 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
2026-10-28 12:14:18 +08:00
Junmo KimandGitHub 9e5d848796 fix(claude): announce automatic compaction when it starts (#2190) 2026-10-28 12:13:27 +08:00
ffd950a220 fix(codex): drop safe-yolo from Codex permission modes (#2183)
* 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>
2026-10-28 12:12:55 +08:00
40070a56db fix(claude): apply mid-turn permission mode switches and surface refused modes (#2173)
* fix(claude): apply mid-turn switches to auto/plan in the running process

Claude enforces auto and plan natively, so switching a remote session from
Manual to Auto during a turn only took effect after the next message
restarted Claude; the running turn kept asking for approval. Send
set_permission_mode to the live process whenever the native mode changes.

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

* fix(claude): surface refused or fallen-back permission modes

Claude rejects set_permission_mode when auto is unavailable (e.g. disabled
by settings) and silently starts in default when launched with an
unavailable auto/plan. Both left the UI showing a mode that was not in
effect. Revert the session to the mode Claude actually runs and post the
reason in the chat.

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

* fix(claude): ignore permission mode rejections superseded by later switches

A delayed rejection of auto could restore an older mode after the user had
switched away and back again, because staleness was checked by mode value.
Track each transition with a sequence number instead.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:21 +08:00
Junmo KimandGitHub 01f80e186a fix(peer-delivery): truthful accepted vs consumer-confirmed receipt (#2071)
ping-peer now sends a ping-peer- localId and reports hub-accepted instead
of delivered; the hub exempts those rows from the session-end synthetic
sweep so only a real runtime ACK confirms them; web shows pending vs
delivered badges only on peer rows, excluding failed, indeterminate and
scheduled states.
2026-10-28 12:12:04 +08:00
AnanovoandGitHub 116756e833 fix(web,cli): clarify archive cleanup progress and bound shutdown waits (#2061)
* fix(web,cli): clarify archive cleanup progress and bound shutdown waits

* fix(web): keep archive dismissal and delete waiting states consistent

* fix(web): scope archive subscriptions to active consumers
2026-10-28 12:11:52 +08:00
AnanovoandGitHub 2a612c5c72 perf(cli): reduce Windows process probes during session shutdown (#2046)
* fix(runner): prevent Windows process probes from blocking session lifecycle

* perf(cli): reduce Windows process probes during session shutdown

* test(cli): update PowerShell fixtures for complete process snapshots
2026-10-28 12:11:40 +08:00
Junmo KimandGitHub cd0b8e3589 fix(claude): stop locking Claude sessions to three hard-coded model aliases (#1948)
* 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.
2026-10-28 12:10:36 +08:00
2b63091421 fix(runner): adopt untracked runner webhooks only from the reporting process generation (#1943)
* feat(cli): report the CLI process start marker in session metadata

The session webhook only names hostPid. A PID alone does not tell the
runner which process generation reported it: once the CLI exits, the OS
can hand the same PID to any other process, including another
runner-spawned CLI.

Add metadata.hostStartMarker, the reporting process's own start marker
(getProcessStartMarker(process.pid), the same probe the runner uses for
its resume records and the stop-session generation check).
buildSessionMetadata is the only place that sets hostPid, so every
bootstrap path (fresh, existing, lazy, shared Codex) reports it. The
runner uses it to decide whether an untracked runner webhook may be
adopted (decideUntrackedRunnerWebhook).

* fix(runner): adopt untracked runner webhooks only from the reporting process generation

Since #1911 the runner adopts an untracked `startedBy: 'runner'` webhook
(runner restart before the webhook; Claude has no HAPI id on argv) and
persists getProcessStartMarker(pid) of whatever currently owns hostPid,
so that a later stopSession can tree-kill that generation.

If the webhook is late - the reporting CLI has exited and the OS has
already reused its PID - the persisted marker belongs to a foreign
process, possibly another runner-spawned CLI. The generation check in
stopSession then passes and the foreign process tree is killed.

The webhook now has to prove the generation itself: the CLI reports its
own marker as metadata.hostStartMarker and decideUntrackedRunnerWebhook
adopts only when it matches the live PID. A marker that names another
generation, or a failed probe, is ignored without killing anything,
also on the webhook-timeout path: a proven mismatch means the PID is not
the ghost. A webhook without a marker (older CLI) is left untracked
instead of adopted blindly; the timeout kill for those CLIs is unchanged.

The integration test drives a real runner over its control server: a
webhook carrying the live marker is adopted, listed and stoppable; a
webhook with a stale or missing marker leaves the PID untracked and
alive, and stop-session reports unknown.

Follow-up to #1911 (#1910).

* fix(cli): probe the process start marker only for runner-started sessions

buildSessionMetadata probed getProcessStartMarker(process.pid) on every
bootstrap, terminal starts included. The runner reads
metadata.hostStartMarker only in the untracked `startedBy: 'runner'`
path (decideUntrackedRunnerWebhook); a terminal webhook never consumes
it, and neither do hub or web.

The probe is a synchronous spawn: `ps` on POSIX, `tasklist` plus a
PowerShell Get-CimInstance call on Windows, each with a 10 s timeout.
A terminal `hapi` start on Windows therefore blocked on a PowerShell
cold start before the session row existed, for a value nobody reads.
With shared Codex the probe also ran per /new and /fork on the event
loop shared by all sibling roots.

Set hostStartMarker only when startedBy is 'runner'; that is the one
case the runner evaluates. This narrows the earlier commit: every
bootstrap path reports the marker for runner-started sessions, not for
terminal ones.

* fix(codex): keep hostStartMarker in the shared runtime's runner webhook

The shared Codex runtime bootstraps its roots with reportStarted: false
and reports them to the runner itself, from root.session.getMetadata().
That is the hub's view of the metadata: the hub parses stored metadata
through MetadataSchema, so a hub that predates hostStartMarker strips
the field and the webhook arrives without it. The hub's update-metadata
merge never returns it either, so the loss lasts for the root's
lifetime.

With the generation check, an untracked runner-started webhook without
a marker is ignored instead of adopted. A runner restart (for example
the version handoff of a single-host upgrade, where the hub process
keeps running the old code) followed by /new or /fork in a long-lived
shared root therefore left the root unlisted and untracked, the very
situation the adoption from #1911 addresses. The Claude and lazy
bootstrap paths are unaffected: they report the local metadata.

Take the marker from the local bootstrap metadata instead. The opt-in
runtime integration test models the stripped hub view in its session
mock and asserts the reported payload still carries the marker. The
mock also gains the `on` subscription that SharedCodexRoot requires
since #1911, without which the suite's bind test could not run.

* test(runner): run the late-webhook suite in the serial integration project

lateRunnerWebhook.integration.test.ts starts a real runner and a
detached child process per case, but the default vitest project picked
it up and `bun run test` ran it in a parallel worker. Tests that spawn
runner process trees belong to the serial integration project
(vitest.integration.config.ts, CI job `integration`), as the config
comments and .github/workflows/test.yml require.

Include the file in the integration project, exclude it from the
default one, and remove the temporary HAPI_HOME of each case, which
was left behind in the temp directory.

---------

Co-authored-by: Benedict Groppe <195190800+BGGBTAC@users.noreply.github.com>
2026-10-28 12:10:21 +08:00
SSU-WEI HUANGandGitHub 3e68e29110 fix(codex): reuse live model catalogs and extend discovery cache (#1938)
* fix(codex): reuse live model catalogs and extend discovery cache

Closes #1937

* fix(codex): track relative model catalog changes

* fix(hub): expose missing Codex session model RPC code
2026-10-28 12:10:08 +08:00
db4ee82c80 fix(cli): release stdin before handing terminal to local-mode child (#1932)
* fix(cli): release stdin before handing terminal to local-mode child

hapi ships as a Bun-compiled executable, and Bun's tty.ReadStream keeps
an in-flight read on fd 0 even after process.stdin.pause()
(https://github.com/oven-sh/bun/issues/29126).

Remote mode reads stdin (Ink UI in raw mode, for the double-space
switch-to-local gesture). When switching back to local mode,
spawnWithTerminalGuard only paused stdin before spawning the agent TUI
with stdio:'inherit'. The parent's stale pending read kept racing the
child for console input and won: after a remote -> local switch the
agent TUI rendered but received no keystrokes at all — the input box
was dead and even Ctrl-C never reached it. Instrumented at local spawn
time: dataListeners=0, isPaused=true, but _readableState.reading=true.

Fix: destroy stdin outright before spawning the local child
(releaseStdinForChild) — destroy reliably stops the pending read where
pause() does not — and re-create process.stdin via
`new tty.ReadStream(0)` when remote mode starts again
(ensureLiveStdin, called from RemoteLauncherBase.setupTerminal).
hasTTY is now computed lazily since stdin may legitimately be
destroyed at launcher construction time.

Verified end-to-end on Windows with `hapi kimi` by injecting console
input via WriteConsoleInput and checking the agent session wire log:
remote -> double-space -> local -> typed message reaches the agent;
switch to remote again (double-space detection works on the
re-created stdin) -> local -> input works again. Direct local launch
unaffected. `bun run typecheck:cli` and the cli vitest suite pass.

* fix(cli): preserve piped stdin when releasing terminal for local child

Addresses review feedback on #1932: releaseStdinForChild() destroyed
stdin unconditionally, but destroying a piped stdin closes fd 0 and
starves the child spawned with stdio:'inherit'. The Bun stale-read
workaround is only needed for a TTY, so skip destroy() when stdin is
not a terminal; pause() suffices for pipes. Adds regression coverage
for the piped case.

---------

Co-authored-by: Naitian LIU <Liunaitian@users.noreply.github.com>
2026-10-28 12:09:49 +08:00
SSU-WEI HUANGandGitHub ebca96e575 fix(runner): confirm archived Codex root after tracking loss (#1930) 2026-10-28 12:09:26 +08:00
b450aa8236 fix(cli): discover DSH skills from the DeepSeek Harness home (#1916)
For the `dsh` flavor HAPI's `$` skill menu only scanned `~/.agents/skills`
and `<dir>/.agents/skills`, so skills installed under the DeepSeek Harness
home (for example `~/.dsh/skills/<skill>`, where the clawhub CLI installs
them) never appeared in the completion list even though DSH's own
skill-filesystem provider loads them.

Add the roots that provider uses, plus the `$DSH_HOME` override the other
supported agents already have: `$DSH_HOME/skills` for user skills and
`<project>/.dsh/skills` for project skills. The DSH integration (#1632) did
not extend the per-flavor skill roots, unlike the earlier per-agent
additions (#288/#482/#487 Claude and Codex, #1030 Grok, #1245 Copilot).

Verified with a temporary-home Red/Green check, three new unit tests, a real
DSH session in a Full test environment (its skills endpoint now returns the
previously missing skill), typecheck, build, and the Root
terminal-wrap-fidelity Playwright suite.

via [HAPI](https://hapi.run)

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-28 12:09:15 +08:00
8ae7f0aa7b fix(cursor): in-place relaunch when switching live session to Auto (#1909)
* fix(cursor): in-place relaunch when switching live session to Auto

When ACP does not advertise literal auto, switch to Auto by respawning
the same hub session under --model auto + session/load instead of
rejecting. Keeps #1819 CLI Auto pin semantics. Fixes #1908.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): widen in-place relaunch spawn model typing

Allow null/undefined rollback spawn models from resolveCursorSpawnModel.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): re-pin ACP session id after in-place relaunch load

If session/load returns a different agent id, update hub metadata the
same way as a fresh session so resume stays recoverable.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): coordinate message loop with in-place Auto relaunch

Await relaunch completion before prompting, re-read the live backend
after model apply, and publish this.backend only after session/load.
Addresses tip bot Majors on #1909.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): harden Auto relaunch against prompt races and cleanup

Hold promptInFlight before mode apply, end the loop if backend restore
fails instead of requeue-spinning, and discard late replacement backends
when session teardown has started. Addresses tip bot Majors on #1909.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): avoid continue-in-try circular typing; finish relaunch harden

Refactor mismatch requeue off continue-inside-try so typecheck passes,
keeping promptInFlight hold and teardown discard for #1909 tip bot Majors.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): preserve queued turns and Auto-review across relaunch failure

Requeue dequeued batches before ending when restore fails, and still
record Auto-review (slash after publish) when mode flips while backend
is null mid-relaunch. Addresses tip bot Majors on #1909 @ 9e5f241f1.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): defer hub ack until Auto relaunch has a live backend

Avoid acknowledging queued turns before a usable ACP process exists, and
reset Auto-review slash bookkeeping on in-place process replacement.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): recompute Auto-review spawn flag for replacement ACP

Slash bookkeeping after in-place relaunch must use the replacement
process's --auto-review arg, not the prior process's sticky flag.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): drop queued Auto-review slash when replacement has --auto-review

Cursor treats /auto-review as a toggle; leave no leftover slash for a
spawn that already enabled Auto-review via the CLI flag.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): discard held Auto-review toggle after --auto-review relaunch

Mark internal toggles so queue reconcile and post-relaunch dispatch drop
only HAPI-generated /auto-review slashes, preserving user-sent commands.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 12:09:00 +08:00
MaizandGitHub b49c5d1dcb fix(cli): flush buffered ACP assistant text after an idle gap (#1898)
* fix(cli): flush buffered ACP assistant text after an idle gap

ACP gives no "text segment finished" signal for a turn's last text segment and AcpMessageHandler only emits buffered text at explicit boundaries or through the prompt-lifecycle drains. When neither arrives - a text-only turn whose prompt() never returns - the answer is held until the next user prompt.

Emit buffered text once the stream has been silent for TEXT_IDLE_FLUSH_INTERVAL_MS (1000ms). The idle flush defers while a reasoning block is open (keeps Reasoning above the answer) and while the buffer may be an unfinished control envelope (keeps the internal-event filter complete). A later boundary emits only the remainder, so no character is emitted twice.

* fix(cli): stream idle ACP text as snapshots under one stream id

An id-less idle emission could render a reordered or duplicated answer when a dedupe agent rewrites its buffer with a prefix, and a pause inside an open Markdown fence split the rendered answer into separate blocks.

Stream idle emissions as full-text snapshots under a stable stream id and finalize with the same id at the boundary, mirroring flushReasoning; the partial-watermark computation is removed. The control-envelope guard no longer assumes type is the first JSON key.

* fix(cli): skip interim idle text snapshots in local launcher displays
2026-10-28 12:08:26 +08:00
Sharad.andGitHub 7a29b60353 fix(test): use portable ps axeww in test-process audit (#1869)
* fix(test): use portable `ps axeww` in test-process audit

The audit ran `ps eww` which is terminal-scoped on macOS (and exits 1
without a controlling terminal, failing the whole CLI suite via the
global-teardown audit). The previously suggested `-eww` also breaks the
scan: macOS treats `-e` in SysV form as "all processes" and silently
omits the environment dump, so a marked test child is never detected.
Use the BSD full-dump form `ps axeww` (all users + tty-less + env +
unlimited width), which ports across darwin and procps-ng. Add a
regression test that spawns a real marked child and asserts the audit
finds it.

* test(audit): skip live-process detection test on Windows

The scan function short-circuits to an empty list on win32 where ps
axeww is unavailable, so the assertion that finds a live child can
never pass on that platform.  Skip it conditionally.
2026-10-28 12:08:09 +08:00
AnanovoandGitHub 5d7e13fc5d fix(hub,cli): keep inactive file previews available through runner (#1727)
* fix(hub,cli): keep inactive file previews available through runner

* fix(cli): bound runner workspace file reads

* fix(cli): constrain runner git fallback pathspecs

* fix(cli): allow dot-prefixed workspace paths
2026-10-28 12:07:18 +08:00
AnanovoandGitHub 0aec975434 fix(codex): gate history actions until native thread is ready (#1604)
* fix(codex): gate history actions until native thread is ready

* fix(codex): clear history capabilities during bootstrap

* style(codex): normalize CLI file line endings

* fix(codex): await history capability persistence

* fix(codex): restore goal status coverage

* fix(codex): await bootstrap metadata persistence

* chore: retrigger PR review

* fix(codex): scope metadata readiness flush

* fix(codex): gate metadata flush by stale history
2026-10-28 12:06:08 +08:00
xxfadandGitHub fe5950973f perf(codex): optimize session history import (#2206) 2026-10-28 11:02:06 +08:00
01da312458 fix(claude): use native ai-title for remote session titles (#2207)
* fix(claude): use native ai-title for remote session titles

Remote mode talks to Claude over the SDK stream, which never carries the
ai-title Claude Code writes to its transcript, so the first-result fallback
titled the session from the formatted prompt. With an attachment that prompt
starts with "@/tmp/hapi-blobs/..." and the 80-char cut left only the path.

On the first result, read the latest ai-title from the transcript; fall back
to the first prompt as typed, without attachment @paths.

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

* fix(claude): derive fallback title from the delivered prompt

Recording the first enqueued text could title the session from a prompt
that was cancelled or edited before delivery. Strip the upload @path
references from the prompt actually delivered instead; upload filenames
have whitespace replaced, so each reference is a single token.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:01:57 +08:00
f069eda79e fix(cli): follow Claude transcript relocated into a git worktree (#2208)
Claude Code moves the session transcript to the new cwd's project dir
when the session enters or leaves a git worktree. The local-mode scanner
pinned the launch project dir, so nothing reached the web until the
session left the worktree. Look the transcript up by session id across
project dirs when the known path is gone.

Fixes #1950

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:01:45 +08:00
00bbbfa780 fix(claude): report thinking for local-mode sessions (#2209)
* fix(claude): report thinking for local-mode sessions

Local Claude sessions have no SDK stream, so nothing ever set the
session's thinking flag and the web UI showed them idle mid-turn.
Derive it from the local hooks instead: UserPromptSubmit/PreToolUse
turn it on, Stop/StopFailure/SessionEnd turn it off. Esc interrupts
fire no Stop hook, so the transcript's interrupt marker also clears
it, and leaving local mode always resets it.

Fixes #1864

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

* fix(claude): ignore interrupt markers from an earlier local turn

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 10:31:44 +08:00
weishu 85ccab1ac6 Release version 0.31.3 2026-10-27 14:23:48 +08:00
weishu 2ef7d5855b ci(release): publish npm packages via Trusted Publishing
Move npm publishing from the local release script to the tag-triggered
Release workflow, authenticated with npm Trusted Publishing (OIDC) so no
npm token is needed. Local `release-all` now only bumps versions, updates
the lockfile, commits, tags, and pushes.

- Add cli/scripts/publish-npm.ts: publish platform packages, wait until
  they are live, then publish the main package; skip versions already on
  npm so a failed run can be re-run.
- Drop the platform optionalDependencies from cli/package.json. The
  published main package gets them from prepare-npm-packages; the
  workspace copy only made bun.lock depend on not-yet-published versions.
2026-10-27 14:22:30 +08:00
weishu 7bf0e5bc35 Release version 0.31.2 2026-10-27 08:32:28 +08:00
JeeandGitHub aed99e22cf fix(codex): allow larger histories over shared WebSockets (#2171) 2026-10-26 15:14:45 +08:00