Commit Graph
682 Commits
Author SHA1 Message Date
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
weishu e5a845cf9f test(cli): include Codex Auto Review in peer permission coverage 2026-10-26 14:45:02 +08:00
weishu 7f7d871866 fix(runner): share effective webhook startup timeout 2026-10-26 10:59:22 +08:00
SSU-WEI HUANGandGitHub c64051eba6 fix(codex): persist Round duration across session reloads (#2025)
* fix(codex): persist Round duration across session reloads

* fix(codex): preserve completed plan summaries between turns

* test(ios): make plan transcript recycling deterministic

* fix(ios): preserve reading anchors with stale sizing attributes
2026-10-26 10:26:55 +08:00
713b2d1a7f fix(cli): replay the latest runtime heartbeat on connect (#2054)
* fix(cli): reliably publish initial session runtime

* fix(cli): replay the latest runtime heartbeat on connect

A volatile session-alive sent before the socket connects is dropped, and
the connect handler sent only thinking=false, so the hub could see an
active session without its permission mode until the next heartbeat.

Keep the latest keep-alive payload and include it in the connect
heartbeat. Drop the separate first non-volatile emit: once connected,
volatile heartbeats are delivered.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:26:16 +08:00
9a9630280f fix(claude): pass the session permission mode to the local TUI (#2159)
Since #460 the claude command consumes --permission-mode into session
state, but claudeLocal never passed it on. A terminal `hapi claude
--permission-mode X` or `hapi resume` of a non-default session opened the
TUI in Claude's own default mode, and the next hook payload then
overwrote HAPI's mode with it.

claudeLocal now passes the current session mode as --permission-mode
(default is omitted so Claude's settings.json defaultMode still applies).
--yolo / --dangerously-skip-permissions now only set the session mode:
Claude lets --dangerously-skip-permissions override --permission-mode, so
forwarding it kept every later local relaunch in bypass even after the
session was switched back to Default.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:25:59 +08:00
fc7f53fb43 fix(claude): keep remote permission downgrades effective (#2158)
acceptEdits and bypassPermissions are emulated in canCallTool so they can
change mid-session without restarting Claude. But the remote process was
launched with the first turn's mode natively, so a session started in Yolo
ran Claude in bypassPermissions, which never calls canCallTool. Switching
the session back to Default then changed only HAPI's state: Bash and edits
kept running without approval until something else restarted the process.

Launch Claude in default for these emulated modes so every tool call goes
through canCallTool and the current session mode applies. plan and auto are
still passed natively since they are enforced inside Claude and already
restart the process when toggled.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:25:46 +08:00
927b4971b9 feat(dsh): model selection for DeepSeek Harness (ACP config options) (#2030)
* feat(acp): flatten Zed-style grouped configOptions choices

DeepSeek Harness 0.2.x (and Zed-style agents generally) nest select
choices one level under group entries that carry no `value` of their
own:

    options: [{ group, name, options: [{ value, name }] }]

Both the per-session config-option descriptor and the model-metadata
merge previously read only flat `{ value, name }` entries, so grouped
pickers surfaced zero choices. Flatten recursively in a shared helper;
flat options are unaffected.

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

* feat(dsh): runtime model switching through ACP config options

dsh advertises its model catalog as a Zed-style grouped config option
(provider groups with JSON-encoded provider+model tuple values) instead
of session/set_model, so wire it through the generic config-option rail:

- shared: ListDshModels/ListDshModelsForMachine RPC methods, response
  types, and grant dsh the ModelChange flavor capability
- cli: DshMode becomes {model} and rides the queued-batch mode like
  other flavors; the launcher applies model switches per-turn via
  session/set_config_option, discovers the live current model from the
  config option after newSession, rolls back (and warns) when a switch
  is rejected, and serves ListDshModels from session metadata. A
  machine-scoped probe (60s single-flight cache) backs the new-session
  catalog; --model is now accepted by the dsh command
- hub: dsh-models passthrough routes (machine + session scope)

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

* feat(web): dsh model selector for new sessions and in-chat switching

New-session form gets a Default-plus-catalog picker fed by the machine
probe (opaque tuple values verbatim, friendly names as labels, stale
draft picks dropped when the catalog rotates). Running sessions get the
same catalog over the session-scoped hook; switching rides the existing
generic model-change path now that dsh advertises ModelChange.

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

* fix(dsh): honor explicit Default reset and cover pending catalog discovery

The launcher now captures the server default before applying any spawn
override, and a queued `model: null` (the picker's "Default" after an
explicit switch) switches back to it instead of silently keeping the
last applied model; DshMode keeps null distinct from undefined all the
way from the config RPC through the queue (runDsh no longer collapses
it). The web model-option helpers get explicit dsh branches mirroring
opencode: pending, failed, or empty ACP discovery renders no Claude
presets and the Ctrl/Cmd+M cycler keeps the current model. NewSession
restores dshSelectedModel from preferred launch settings alongside
agy/opencode (the existing catalog-validation effect drops stale tuples)
with remount, forward-on-create, and fallback tests.

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

* fix(dsh): keep unset model preferences distinct from Default resets

Two review follow-ups:

- currentModel collapses "no preference yet" into null, so a session
  started without --model on a server that reports no current model
  queued model:null on every ordinary turn — the launcher read each one
  as an explicit "Default" reset and emitted 'Cannot reset the DSH
  model' every turn. The preference is now three-state (undefined =
  nothing requested, null = picker's explicit Default, string = model),
  encoded through dshModeForPreference() so preference-less turns carry
  no model field at all.

- ListDshModels was registered only after initialize/session/new and
  discovery finished; a picker opened during startup hit an
  unregistered-method rejection with no retry path and stayed empty
  until remounted. The handler is now registered before initialization
  and parks on a readiness promise resolved once the session exists and
  discovery has settled (rejected with the startup error otherwise).

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

* docs: DSH advertises model selection over ACP

The capability blurbs still said HAPI exposes no model pickers for DSH and
that the ACP server has no model switching. Update them to the wire reality:
models are listed via ListDshModels and applied per turn with
session/set_config_option. Resume remains out of scope on this branch.

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

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-26 10:02:51 +08:00
cc1a09be0c fix(dsh): default ACP entrypoint to the official dsh CLI (#2031)
* fix(dsh): default ACP entrypoint to the official dsh CLI

The default spawn command was `dsh-acp-demo` — the standalone legacy
demo package. Since `@deepseek-ai/dsh` 0.2.0 the ACP profile ships with
the main CLI, so `dsh --profile acp` is the maintained entrypoint and
the one a plain `npm i -g @deepseek-ai/dsh` install provides. The old
default required every self-hoster to also discover and pin the demo
package (whose npm `latest` tag is stale) before `hapi dsh` worked at
all.

- resolveDshAcpCommand defaults to `dsh --profile acp` when no DSH env
  is set; any explicit `HAPI_DSH_ACP_COMMAND` keeps bare `[]` args so
  custom wrappers never silently receive the official CLI's flags
- DEFAULT_COMMANDS/availability follow, so the PATH probe now checks
  `dsh` itself
- the demo package remains fully supported via explicit
  `HAPI_DSH_ACP_COMMAND=dsh-acp-demo`; docs updated accordingly

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

* Apply config-only DSH overrides through the official CLI's patch overlay

HAPI_DSH_ACP_CONFIG without HAPI_DSH_ACP_COMMAND fell through to the
default official CLI but passed `--config <path>`, a flag the official
dsh CLI does not have — and dropped the required `--profile acp`, so the
boot would run the wrong entrypoint with an unknown flag. A config-only
override now composes the path as a `--patch` overlay on top of the ACP
profile (verified against dsh 0.2.0-rc.2 --dump-config). The `--config`
form remains available for legacy community servers pinned via
HAPI_DSH_ACP_COMMAND, and both READMEs now document the split.

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-26 10:02:36 +08:00
62872d80fa feat(cli): HAPI_DISABLE_PEER_TOOLS — hide peer-session tools from wrapped agents (#2052)
* feat(cli): add HAPI_DISABLE_PEER_TOOLS to hide peer-session tools from agents

Wrapped agents with peer tools can browse the session list and message
other sessions, polluting peer contexts on busy runners. Setting
HAPI_DISABLE_PEER_TOOLS=1 (true/yes also accepted) in the runner
environment now removes the peer surface entirely instead of
advertising it and refusing:

- the session MCP server does not register list_peers / inspect_peer /
  ping_peer, and they are dropped from the toolNames consumed by
  Claude --allowedTools and the codex/generic bridge --tools lists
- the stdio bridge filters peer names even when a stale --tools value
  lists them explicitly
- flavor system prompts (claude/codex/opencode) swap the peer steer
  block for a short notice that keeps the hub-path-is-not-a-file guard
  while pointing citations at the user
- the peer core (pingPeer/inspectPeer/listPeerSessions) refuses with a
  new 'disabled' error code before any network call, so shell
  `hapi ping-peer` / `inspect-peer` from inside a wrapped agent fails
  loudly as defense in depth

Operators scope the variable to the runner (systemd/launchd/guard env);
an interactive terminal without it keeps full CLI access.

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

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Code <noreply@anthropic.com>

* test(cli): cover peer-tool prompt steer and default bridge tool list

Adds regression tests for the two surfaces the kill switch mutates beyond
the explicit --tools path:

- happyMcpStdioBridge: default tool list (no --tools) keeps the peer tools
  as a positive control, then HAPI_DISABLE_PEER_TOOLS=1 filters them out
  of the same default seed.
- New cross-flavor prompt test: the citation steer baked into the
  claude/codex/opencode prompt consts swaps to the disabled notice under
  the env flag and keeps the flavor-specific "Call \"<tool>\"" instruction
  by default (module consts evaluate env at import, so each mode is
  imported fresh via vi.resetModules + dynamic import).

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

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Code <noreply@anthropic.com>

* test(cli): keep peer-tools env set through prompt assertions

Keeps HAPI_DISABLE_PEER_TOOLS applied until after the assertions and
restores it at test end. Some upstream shapes resolve the citation steer
at call time (getSystemPrompt(env)) rather than baking it into the
import-time const, so restoring the env inside the import helper made
those shapes observe the enabled steer and fail.

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

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-26 10:02:06 +08:00
d3f7c107d5 feat(pi): display inline tool-result images (#2060)
Build on tiann/hapi#1831, retain bounded image reads, and support current
Pi nested and failed tool results through the existing generated-image path.
Keep conversion synchronous to preserve transport ordering without a new
publication queue.

Co-authored-by: Debin <luodeb@outlook.com>
2026-10-26 10:01:55 +08:00
Yincan HuangandGitHub 5afdb54109 fix(runner): add webhook timeout configuration hint (#2066)
Report the effective wait when session startup exceeds the webhook
deadline and explain how to configure it on the runner host.

Refs #2062
2026-10-26 10:01:31 +08:00
AnanovoandGitHub 61c7bcc6f8 fix(runner): preserve slow Codex sessions across startup deadlines (#2078)
* fix(runner): preserve slow Codex sessions across startup deadlines

* fix(runner): clean failed startup worktrees after child exit
2026-10-26 10:00:38 +08:00
d3473f9ed4 fix(cli): forward Claude API error records with null service_tier (#1857) (#2087)
Claude Code writes failed API calls ("Prompt is too long", usage limits,
logged out) as synthetic assistant records whose usage carries
`service_tier: null`. The schema only accepted undefined, so the local
session scanner dropped every one and the web session just looked stalled.

- Accept null for the usage fields the Messages API types as nullable.
- Keep the top-level `error`/`isApiErrorMessage` on assistant records in
  local mode, and `error` in the SDK converter for remote mode.
- Render assistant records carrying `error` as the existing error event,
  so the all-zero synthetic usage no longer resets the context gauge.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:00:23 +08:00
6bf0a633bb fix(claude): seed local-mode context window from the configured model (#1858) (#2088)
* fix(claude): seed local-mode context window from the configured model (#1858)

Local transcripts carry bare model ids and no usage.context_window, so
local Opus/Sonnet [1m] sessions fell back to the web's 200k heuristic
(190k denominator). At local launch, resolve the model Claude Code
starts with (--model, ANTHROPIC_MODEL, project local/project/user
settings) with its [1m] suffix intact, and stamp a 1M context_window on
assistant messages from that model. Subagent models are left alone.
Register PostModelSwitch in the local hook settings so /model switches
inside the TUI update the seed.

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

* fix(claude): honor --model and --setting-sources when seeding the local context window (#1858)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:00:12 +08:00
878ca2058c feat(codex): expose Approve for me as a permission mode (#2016) (#2090)
* feat(codex): expose Approve for me as a permission mode (#2016)

Add autoReview to the Codex permission catalog. It keeps the
workspace-write sandbox with on-request approvals and routes escalations
to Codex's native reviewer (approvals_reviewer = "auto_review"), the same
config as `hapi codex --approve-for-me`. It works at launch and mid-session
via thread/settings/update; explicit modes now reset the reviewer to user,
and sessions report autoReview from the native reviewer setting.

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

* test(native): expect Auto-review in the Codex permission lists (#2016)

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

* feat(modes): label autoReview as Auto, matching Claude's auto mode (#2016)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:59:47 +08:00
60a4971087 fix(cli): forward Claude Code 2.x subagent transcripts in local mode (#1904) (#2091)
Claude Code 2.x writes each subagent to <session>/subagents/agent-*.jsonl
instead of the main session log. The local session scanner only read the
main log, so subagent activity of local Claude sessions never reached the
hub and Agent/Task cards stayed empty.

Scan the subagents directory of every tracked session and stamp
parentToolUseId from the sibling agent-*.meta.json (toolUseId), so the
web tracer groups the transcript under its Agent card. Subagent files
already present at startup are marked seen, like the main log.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:59:36 +08:00
1488be153a feat(cli): add hapi set-mode to change a session's permission mode (#2094) (#2095)
* feat(cli): add hapi set-mode to change a session's permission mode (#2094)

The CLI could only pick a permission mode at launch; changing it on an
existing session required the web UI or a hand-rolled JWT + curl call.

`hapi set-mode <session-id-or-prefix> <mode>` resolves the session like
ping-peer / inspect-peer, validates the mode against the session flavor's
catalog (printing the allowed list on mismatch), and calls the existing
POST /api/sessions/:id/permission-mode route. Active sessions switch live;
inactive ones keep the mode for the next resume.

No MCP tool is added: raising a peer's mode stays behind the agent's
normal shell-approval path.

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

* fix: reject Safe Yolo for shared sessions on the permission-mode route

Shared (concurrentClients) Codex sessions cannot launch with safe-yolo.
The web picker hides it, but the route only checked the flavor catalog,
so `hapi set-mode` (or any API client) could store it on an inactive
shared session and break its next resume. Guard the route and apply the
same filter to the CLI's allowed-mode list.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:59:25 +08:00
4254942aeb fix: show Claude sessions waiting on background tasks (#2100) (#2102)
* fix: show Claude sessions waiting on background tasks (#2100)

backgroundTaskCount was always 0 for Claude: the hub parsed the
"Command running in background" tool result only from tool_result /
assistant records (Claude sends it as a user record), and waited for a
<task-notification> user record that current Claude Code never emits.

Set the count from Claude's system/background_tasks_changed snapshot,
which the CLI already forwards and which lists every running background
task (empty once the last one finishes).

In the session list, waiting on background work now shows a blue
spinner instead of a static blue dot (too close to the "needs input"
dot) or the green "running" pulse used for selected and pinned rows.

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

* fix(cli): forward background_tasks_changed snapshots to the hub

OutgoingMessageQueue only sends chat-visible records, so Claude's
background task snapshots never left the CLI. Forward that one hidden
subtype, and have the hub apply it as session state without storing a
hidden message row.

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

* fix(web): draw the background spinner with the thinking spinner code

Both row spinners now render through one SessionSpinner; background work
only switches the color to blue and no longer adds a separate Running label.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:35:02 +08:00
AnanovoandGitHub cb2ff05c2a fix(codex): distinguish compaction waits from unknown delivery (#2115) 2026-10-25 15:34:52 +08:00