mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
f8e55fb930cde45dd4ef5cc5dd1fac0a047f38b8
239
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5fcd4a067a |
fix(agy): separate model and effort selection (#2244)
* fix(agy): separate model families and reasoning effort * fix(agy): report startup before model discovery |
||
|
|
d9acc0c31e | Release version 0.33.3 | ||
|
|
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> |
||
|
|
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 |
||
|
|
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. |
||
|
|
730dfcb22e | Release version 0.33.2 | ||
|
|
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. |
||
|
|
a6cf8fd078 | Release version 0.33.1 | ||
|
|
ee273464f4 | Release version 0.33.0 | ||
|
|
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> |
||
|
|
575e995480 |
feat(hub): generate fallback titles after the first completed turn (#2075)
* feat(hub): generate fallback titles after the first completed turn * fix(hub): recognize batched first-turn title prompts * fix(hub): track heartbeat-only first turns for title fallback * fix(hub): preserve title turn identity across restarts |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
fe2fbb881d |
fix: list Claude background tasks from the snapshot instead of guessing (#2177)
* fix: list Claude background tasks from the snapshot instead of guessing
The status panel only had a count from `background_tasks_changed` and
reconstructed commands by regex-matching Bash results in loaded chat
history, so tasks whose start message was unloaded, auto-backgrounded
("moved to the background (ID: …)"), or non-Bash showed
"N running · exact command unavailable".
The hub now keeps the snapshot's tasks (id, type, description) on the
session alongside the count. The web panel lists them directly, using
the exact task ID to look up the Bash command when loaded and the
snapshot description otherwise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): only attribute a Bash command from its background launch result
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(native): accept backgroundTasks in strict session patch decoders
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
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. |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
85ccab1ac6 | Release version 0.31.3 | ||
|
|
7bf0e5bc35 | Release version 0.31.2 | ||
|
|
8121d0df6e |
feat(web): fixed session order display setting (#2033)
* feat(web): fixed session order display setting Add a per-device "Fixed session order" toggle (default off, Settings → Display → Sessions). When on, the session list freezes: sessions sort by creation time only (newest first) instead of re-sorting on activity, workspace groups stop jumping (ordered by the newest session created inside them), and the pin-in-progress running buckets are paused. Manual pins still float to the top; search-relevance ordering is unchanged. - shared: SessionSummary gains an optional createdAt (toSessionSummary copies it). Older hubs simply omit it and the web falls back to updatedAt, degrading to the legacy activity order. - web: sessionOrder lib (stable key + comparator), useFixedSessionOrder localStorage hook, useSSE cache sort via ref (no EventSource teardown on toggle), SessionList in-group/group/machine sorts plus bucket gating, settings switch, en/zh-CN/ru strings. - tests: sessionOrder unit, useFixedSessionOrder hook, useSSE fixed-order cache cases, SessionList grouping cases, settings page switch, shared createdAt passthrough. Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): tie-break equal order keys on session id Review follow-up: sessions created in the same instant compared as equal, so their relative order came from the incoming array order — which the hub may derive from activity, making tied sessions visibly swap after every refetch. Tie-break on session id to make the comparator a total order. Two tests cover the comparator and the refetch-stability view. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(web): tie-break equal group creation instants on the group key Review follow-up: the directory- and machine-level sorts compared only the groups' latest creation instants, so groups created in the same instant followed the incoming array order and could shuffle after every refetch. Route both fixed-order group sorts through a shared compareDescWithKeyTiebreak helper that falls back to the group's stable key (directory groups: machine::path key; machine groups: machineId). Test covers both incoming orders of two tied groups. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
cb2ff05c2a | fix(codex): distinguish compaction waits from unknown delivery (#2115) | ||
|
|
7b00df458c |
fix(voice): configurable ElevenLabs max conversation duration (#2017)
* feat(voice): configurable ElevenLabs max conversation duration Auto-created ConvAI agents never set conversation.max_duration_seconds, so ElevenLabs applied its 600s default and cut voice conversations off. Add ELEVENLABS_MAX_DURATION_SECONDS (default 1800, validated 60-7200). The cap is applied when the hub creates an agent and reconciled onto existing hub-managed agents (found by name) once per process. Operator-supplied agents (ELEVENLABS_AGENT_ID / voice map / customAgentId) keep their own cap. Invalid values fail the token request with a clear message instead of an API 422. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * feat(voice): Settings UI for ElevenLabs max conversation duration Extend the hub-owner credentials panel (Settings -> Voice -> manage providers) with a numeric field for ELEVENLABS_MAX_DURATION_SECONDS, reusing the same env-wins-over-Settings overlay that already backs the ElevenLabs API key field. A Settings save takes effect on the very next token request in the same process; the env var still requires a restart. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(voice): reconcile max duration on cached agents, not just once per process Codex review on heavygee/hapi#230 caught this: getOrCreateAgentIdForVoice's agentIdCache short-circuited before ever reaching ensureAgentOverrides, and ensureAgentOverrides's overridesEnsuredAgents gate was a one-time-forever marker that didn't account for the configured duration changing after the first reconciliation. Together, any voice already used once in the hub process would keep its old cap until a restart, contradicting the Settings UI's 'takes effect immediately' claim. Track the last-applied max duration per agent and compare it against the live setting on every call, including agentIdCache hits, so a Settings change reaches an already-cached agent on its very next token request. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(voice): address tiann/hapi#2017 review findings - Fix TS2769 typecheck failure: test assertions casting the credentials response narrowed the type to omit 'editable', which upstream CI's bun typecheck catches and this fork's (currently broken) CI did not. - An invalid ELEVENLABS_MAX_DURATION_SECONDS env value threw out of buildMaxDurationStatus(), which is shared by every provider's GET/PUT credentials status — breaking visibility into OpenAI/Deepgram/ Groq settings too, not just ElevenLabs. The token-issuance hot path (getConfiguredElevenLabsMaxDurationSeconds) still throws there by design; only the status/update endpoints now degrade gracefully. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(voice): interpolate duration range in the Settings hint Nit from tiann/hapi#2017 follow-up review: the hint under the max-duration field rendered the literal '{min}-{max}' placeholders instead of '60-7200'. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
86c88df93b |
fix(runner,hub): cross-platform orphan reap on archive / tracking loss (#1911)
* fix(runner,hub): reap detached driver CLI on archive and tracking loss Archive always verifies via stopRunnerSession; runner stopSession scans argv orphans and awaits webhook-timeout tree-kill so PPID=1 children cannot accumulate after KillSession miss or map drop. Fixes #1910 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): keep --hapi-session-id reap-only; test orphan kill path Claude/cursor swallow the stamp without bootstrap reuse, and reapRunnerSpawnedOrphans is unit-tested as the stopSession map-miss path. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,hub): address #1911 CI and bot Majors Use Vitest in orphanReap tests, add Pi session.on mock, refuse archive when machine StopSession is unreachable, trust full tree-kill results, and argv-sweep orphans after every mapped stop. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): gate late-webhook kill on timeout orphans; fail closed on scan error Track webhook-timeout PIDs separately so recovered shared roots are not killed, and treat ps-list failures as still_alive rather than empty. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): keep shared Codex wrapper alive when archiving one root StopSession must detach a single shared root without tree-killing the wrapper PID while sibling roots remain, including after KillSession has already marked the runtime inactive. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): keep shared Codex siblings after restart tracking loss Before persisted-PID or argv orphan kills, consult the durable runtime registry for active sibling roots on the same wrapper PID so archiving one root after a runner restart cannot tree-kill live shared sessions. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,cli): deliver hub-authored archive metadata to reconnecting CLIs When KillSession misses and archive is written via markSessionArchivedFromHub, emit Socket.IO update-session to the session room. On reconnect, ApiSessionClient reconciles GET /cli/sessions/:id metadata so hub-archived still fires. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): argv-scan orphans even when shared siblings remain Remove session-wide early returns that skipped orphan reaping whenever any shared wrapper had active siblings. Keep only the PID-specific filter so older untracked CLIs with the same HAPI id are still reaped. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): protect tracked shared wrappers when runtime registry is empty Argv orphan sweeps must exclude in-memory shared wrapper PIDs that still host sibling roots, not only durable registry siblings — otherwise a missing runtime record lets the sweep tree-kill the wrapper while archiving one root. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): adopt untracked nonshared CLIs after runner restart When a runner-spawned Claude webhook arrives with no TrackedSession (typical after restart mid-bootstrap), durably adopt the PID instead of ignoring it so StopSession can reap. Keep killing only nonshared timeout orphans; never kill shared Codex wrappers on late webhooks. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): keep recovered shared wrappers for registry siblings After restart adoption tracks only a newly reported Codex root, archiving it must consult the durable registry before tree-killing the wrapper PID so older active sibling roots survive. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): do not archive on KillSession ack when StopSession is unknown KillSession returns before cleanupAndExit finishes. When the runner cannot find the PID (unknown), wait for the session KillSession handler to drop before archiving; refuse if the CLI socket remains reachable. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,cli): confirm KillSession pid via StopSession before archive KillSession now returns the CLI pid. When session-id StopSession is unknown, archive confirms that OS pid (without treating socket loss as exit) and still refuses when confirmation stays unknown or still_alive. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner,hub): tree-kill adopted CLIs and require PID start markers Adopted post-restart runner sessions now use killProcessTreeByPid when no ChildProcess handle exists. KillSession carries a process-start marker so raw PID confirmation cannot tree-kill a reused OS pid. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner,hub): verify adopted PID markers and hub-archive after stop Adopted sessions compare the persisted start marker before tree-kill and drop stale tracking on mismatch. After confirmed StopSession, hub-authors archive metadata when the CLI did not finish its flush. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): refuse unconfirmed shared/adopted stops Keep shared wrappers when siblings remain, but return unknown unless runtimeControl already ended the root. Adopted PIDs without a start marker no longer tree-kill by tracked PID alone. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): require inactive binding before sibling-kept archive Siblings alone must not flip StopSession to stopped on retry, while an acknowledged inactive Codex registry binding remains stop proof. Handle PID- markers before any tracked kill so reuse cannot nuke strangers. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): distinguish orphan reap from unknown keep-wrapper base Pass unknown into finishWithOrphanSweep so a successful argv reap stays stopped instead of being overwritten by an absent registry binding. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): honor orphan reap when adopted PID lacks a start marker finishWithOrphanSweep with an unknown base lets a successful argv kill report stopped instead of forcing unknown after the marker probe fails. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): confirm archive via metadata.hostPid tombstone When StopSession(sessionId) is unknown and KillSession supplies no confirmable pid+marker, probe PID-<metadata.hostPid>. already_gone allows archive; still_alive/unknown still refuse (#1911 Peer #1820). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): escalate Windows taskkill to /F like SIGTERM→SIGKILL Soft taskkill /T is refused on win32 console trees; orphanReap left orphans still_alive. waitForProcessToDie now force-escalates on both platforms so cross-platform reaping matches. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): widen process.kill mock signal to string | number Match @types/node process.kill(pid, signal?: string | number) so the Windows taskkill escalation mocks typecheck on tip 54f141368. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): tasklist liveness + immediate /F after soft taskkill refuse Windows orphan reap still failed in 5ms on Teemo: soft taskkill refused but escalate waited on an unreliable process.kill(0) probe. Prefer tasklist for win32 liveness and escalate to /F as soon as the PID is still live after soft kill. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): coerce win32 ps-list PIDs and escalate soft taskkill to /F String PIDs from ps-list made Number.isFinite fail so orphan kill returned still_alive without calling taskkill. Coerce PIDs, prefer tasklist liveness, and escalate soft taskkill to /F like SIGTERM→SIGKILL. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): Windows orphan scan via CIM CommandLine + taskkill /F escalate ps-list/fastlist is missing CommandLine and often missing from single-exe bundles, so Teemo orphan reap returned scan_failed (logged as live PIDs). List via Win32_Process CIM, coerce PIDs, escalate soft taskkill to /F. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): grace-wait after successful soft taskkill before /F Escalate immediately only when soft taskkill is refused. When soft succeeds but the PID is still draining, honor the existing grace period before force (Codex Major on #1911). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(runner): re-verify start marker before orphan argv tree-kill Capture getProcessStartMarker after the argv match list, re-check immediately before killTree, and skip on mismatch/null-while-alive so PID reuse cannot tree-kill an unrelated process on the orphan fallback. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): preallocate HAPI id before fresh machine spawn Fresh POST /machines/:id/spawn omitted existingSessionId, so argv never got a reap stamp. Preallocate the hub row (fork/clear pattern) and pass that id so a runner restart before the first webhook remains reapeable. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,hub): close three post-prealloc unreapable paths Pass --hapi-session-id into Claude/cursor/kimi/copilot bootstrap so the preallocated hub row is used. Treat null marker re-check while alive as still_alive (not stopped). Keep the preallocated stub when spawn-fail StopSession cannot confirm the child is gone. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): adopt-stub via reservedSessionId, not existingSessionId `--hapi-session-id` binds a fresh create bootstrap to a hub-preallocated UUID (getOrCreateSession id) without taking the reopen path. Non-UUID stamps stay reap-only to match hub create schema. Restores integration spawn invariants broken by mapping hapi→existingSessionId. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,hub): close Opus cold-read B1–B3 for #1911 B1: capture start markers from the same process snapshot as argv match (POSIX ps lstart / Win32 CreationDate); re-check before kill. Continue other orphans when one target is unconfirmed. B2: Windows killProcessTreeByPid verifies every collected descendant, not root-only — taskkill /T exit 0 is signalled, not gone. B3: runner sets childStarted:false on pre-exec rejection; hub deletes the preallocated stub without StopSession. Post-exec ambiguity still keeps the row until stop confirms. Terminal archives no longer refuse when no runner is connected. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,hub): explicit adopt for hub-preallocated session stubs Fresh machine spawn preallocates with tag machine-spawn:<uuid>; CLI create with a different tag and the same id 409'd on identity conflict. Add adoptPreallocatedSession (hub-internal stub check) and POST /cli/sessions { adopt: true, id } so create binds the reserved row and overwrites tag/metadata without guessing the hub tag. Tests fail-first on the real prealloc→CLI create path. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): preserve snapshot markers + unify Windows CIM start markers Runner stopSession was still on findOrphans (PID-only), discarding same-snapshot markers and reopening the PID-reuse kill window. Switch both call sites to findTargets. Force UTC ISO 'o' CreationDate for list and single-PID probe so ConvertTo-Json cannot disagree with getProcessStartMarker on WinPS 5.1. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): exercise production orphan sweep wiring in tests Extract findStopSessionOrphanTargets as the stopSession discovery path both run.ts sweeps call. Add tests that drive that helper end-to-end (not an injected findOrphans seam), assert run.ts source wiring, and cover CIM-list ISO vs probe ISO agreement plus the broken WinPS shape mismatch. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,cli): never delete live spawn on RPC timeout; release stub tags Ambiguous machine-spawn failures (timeout / unset childStarted) no longer call stopRunnerSession or deleteSession — that path killed healthy late boots and CASCADE-wiped transcripts. Pre-exec delete still requires a real machine-spawn stub (store tag check). Metadata updates release the stub tag so reopen flavors are not permanently adoptable; adopt refuses archived stubs. POSIX pgrep ENOENT fails closed instead of root-only verify. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): parseable Windows tree scan + fail closed on CIM failure Space-joined PowerShell for collectWindowsProcessTree was a WinPS parse error ($seen=@{} $bfs=@()), so every scan fell open to [root] and B2 false-stopped again. Join with newlines; return scan_failed on error/empty; killProcessTreeByPid refuses root-only verify. Assert on generated command text (Teemo-confirmed) so mocks cannot hide the next malformed script. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,hub): reopen stamps existing-session-id; scan-fail signals root Opus Critical on #1911: runner always stamps --existing-session-id for known hub ids so Claude/Kimi/Copilot reopen does not hit adopt-stub 409. On pgrep/CIM tree-scan failure still signal the verified root but never claim stopped. Archive when both KillSession and machine RPC are missing. Empty Win process-list stdout and strict Codex registry reads fail closed. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,hub): OK: sentinel for Windows tree scan; adopt CAS CIM failure under SilentlyContinue printed root-only with exit 0 — byte-identical to a healthy childless tree, so scan_failed never fired and killProcessTreeByPid false-stopped (#1911 Overseer B1). Require ErrorActionPreference=Stop, trap exit 1, and an OK: success sentinel; parse anything else as scan_failed. Same Stop/trap on the machine-wide list command. Execute the generated scripts under Docker pwsh in CI when the image is present. Treat pgrep status=null as scan_failed; coarsen Windows tasklist poll; wrap adoptPreallocatedSession in a transaction with stub-tag CAS. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,hub): reservedSessionId for fresh prealloc; signal Win survivors Fresh machine-spawn stubs must not stamp --existing-session-id — that sends Codex down reopen without a thread binding (#1911 bot Major) and re-broke local HTTP non-UUID hints as reopen (round-4). Hub passes prealloc as reservedSessionId; buildCliArgs stamps --hapi-session-id for reserved/sessionId and --existing-session-id only for live reopen. Codex parses --hapi-session-id and adopts via bootstrapSession. Windows tree kill now taskkills surviving pre-scan PIDs after /T misses broken intermediate links. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,hub): fail-loud Docker pwsh CI; scope Codex registry; stub leak CI skipped all four windowsProcessTree.pwsh tests on 48bb (image inspect without pull). Pull the image in test.yml and throw under CI when missing. Scope strict runtime-registry failure to Codex stop contexts so one bad JSON cannot block archive machine-wide. Stamp childStarted:false on machine RPC outside_workspace_roots. Add Codex reservedSessionId prealloc and runSharedCodex regression coverage for the #1911 Critical. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,hub): single-use reservedSessionId; skip UUID HTTP hints Consume the machine-spawn reservation on first prepare() so a second Codex root/fork cannot re-adopt a consumed stub (409). Stop stamping UUID local-HTTP sessionId as --hapi-session-id (adopt hard-fail). Update stale hub spawn mocks for the reservedSessionId arity. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): archive never-started machine-spawn stubs on unknown Ambiguous keep-stubs (startedBy=runner, no hostPid, still machine-spawn tagged) made StopSession return unknown forever while the runner was online, so archive threw permanently. Treat unknown + prealloc stub + no hostPid as already_gone. Also reject spawn success when the runner reports a different id than the preallocated stub. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,cli): refuse dual-RPC-miss archive; generation-check Win survivors Both KillSession and StopSession missing is not proof a detached CLI exited — keep the row unconfirmed until the runner reconnects. Windows tree kill now re-checks process-start markers before individually signalling survivors so PID reuse is not killed. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): refuse archived reopen in bootstrapExistingSession Hub-archived rows could be resurrected when a late-booting child hit --existing-session-id: buildSessionMetadata stamped lifecycleState=running and updateMetadata overwrote the archive. Adopt already rejected archived stubs; reopen now matches. Intentional revive still goes through hub reopenSession first. Correct stub-hatch comment (no post-archive argv sweep). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,cli): reject un-archive in updateSessionMetadata CAS Hub-side transactional guard (Discovery #1911 M1): refuse metadata writes that clear archived lifecycle unless allowUnarchive (reopen clear). Client stops infinite updateMetadata backoff when mismatch applies archived. Clear archive immediately before spawn so Pi/PTY attempt snapshots stay valid. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,cli): success+preserve hub archive; M3 reservedSessionId split Replace version-mismatch refuse (livelock in CAS backoff) with success + merge-preserve of hub-archived fields unless allowUnarchive. Ack path routes through noteHubArchived so the CLI exits (criterion 6). Clear- before-spawn remains the authorized revive. Thread --hapi-session-id to reservedSessionId for shared-parser flavors (adopt≠reopen). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail-closed orphan ps signal; PID- registry; spawn timeouts Overseer B1: signalled ps (status null) is scan_failed, not empty table. B2: unreadable registry on PID-* stops returns null (no soft []). B3: 10s timeout on process/orphanReap spawn.sync. Narrow bootstrap archived belt to archivedBy=hub to match hub merge-preserve. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,hub): hubArchived latch; PTY archiveSnapshot durability Criterion 6: noteHubArchived latches hubArchived so late registerKillSessionHandler registrations still EXIT (EventEmitter does not replay). Mirror Pi: persist archiveSnapshot on ptyResumeAttempt across clear-before-spawn and quarantine, restore on failed-resume cleanup (#1911 Discovery Majors). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): Codex hub-archived exit; AC6 latch test production order C2: SharedCodexRoot registers hub-archived (and latch check) beside its private KillSession — Codex never called registerKillSessionHandler. C1: keep latch+subscribe; test write-then-register order Discovery proved. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): type Codex hub-archived end mock for tsc vi.fn(async () => {}) inferred zero-arg calls; RootHost['end'] signature unblocks end.mock.calls[0][0] (TS2493). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): catch hub-archived Codex host.end rejection void this.host.end() discarded archive-RPC failures as unhandled rejections; Codex has no unhandledRejection handler, so Node can exit the shared wrapper and kill sibling roots mid-turn. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
b6a27bdc68 |
fix(kimi): discover configured models dynamically (#1873)
* fix(kimi): discover configured models dynamically * fix(kimi): address dynamic model review findings Probe settlement (Major): parse `kimi provider list --json` on the child's 'close' event instead of 'exit', so stdio is fully drained first, and run the parse inside a guarded settle() helper. A parser throw previously escaped the promise from an async event callback and reached the runner's uncaughtException shutdown handler instead of listKimiModelsForCwd's catch. The helper also settles the promise exactly once across timeout, spawn error and close, names the terminating signal when there is no exit code, and tolerates a kill() that throws. Running-session discovery (Minor): add a session-scoped Kimi catalog. The shared RPC method `listKimiModels`, a CLI handler registered on the session connection with the probe cwd taken from the session metadata (never from RPC arguments), hub rpcGateway/syncEngine pass-throughs, and GET /api/sessions/:id/kimi-models gated on an active session of flavor `kimi`. SessionChat now reads the catalog from that endpoint, so an active session advertises its models even when no background runner holds the machine socket. New Session keeps the machine-scoped discovery unchanged, and model switching still goes through the existing ACP set_model path. * fix(kimi): register session model discovery before the local/remote loop The session catalog handler was registered inside the remote launcher, after ACP initialization. A session started with `hapi kimi` in local mode therefore answered the web picker's request with an unregistered-handler error, and the failed query was not retried or invalidated when remote control was taken over. The catalog is backend independent, so it is now registered in runKimi session setup alongside the other session-level handlers, using the session's working directory, and the late registration in the remote launcher is removed. This covers local, remote and resumed sessions on the same session connection. Adds cli/src/kimi/runKimi.test.ts asserting the handler is registered after bootstrap and before the loop, and that the probe cwd is the session's working directory. The launcher wiring assertion moves out of kimiRemoteLauncher.test.ts with the registration. |
||
|
|
ca1fd80afd |
fix(cursor): pin Auto to CLI --model auto instead of account default (#1819)
* fix(cursor): pin Auto to CLI --model auto instead of account default Selecting Auto was treated as unset/ACP default[] after the Default→Auto rename. Persist and spawn `auto` so the picker matches `agent --list-models`. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): keep live ACP model until Auto actually applies Pin session.model to auto for the next `--model auto` spawn, but do not claim the live backend switched when ACP only advertises default[]. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): reject live Auto unless ACP auto or spawn confirmed it Do not persist Auto when the running catalog has no auto option. Clear the spawn-auto flag after a concrete model apply so Auto → concrete → Auto cannot fake a live switch. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): keep ACP wire ids spawn-safe and apply effort Cursor's parameterized picker (`clientCapabilities._meta.parameterizedModelPicker`) advertises bare model bases plus separate `fast` / `thought_level` config options. `buildCursorModelsSnapshotFromAcp` synthesized every base as `${base}[fast=${fast}]`, and Cursor rejects any bracket id whose parameter set is not that model's complete set, so 74 of the 76 published picker ids failed at ACP `session/new` with `Cannot use this model` (#1818). - Stop synthesizing wire ids: keep the advertised values verbatim (bare bases are accepted by `--model`, defaults applied). Parameter sets differ per model, so a synthesized id cannot be validated from the options alone. - Flag such catalogs as `parameterized` so variant CLI skus still attach to their base rows — the ACP apply path expresses them through config options. - Apply effort in `applyParameterizedCursorModel` (ids `effort`, `reasoning`, `reasoning_effort`), applied against the config options Cursor re-advertises for the newly selected model, and skipped on models without that axis. - Match CLI sku bases across Cursor's legacy `cursor-` family prefix (`cursor-grok-4.6-high` → ACP base `grok-4.6`) so those variant rows are not dropped. - Version the shared on-disk catalog cache (v2) so runners that already cached the rejected synthesized ids re-probe instead of serving them. Verified with live `cursor-agent 2026.09.10-fd3934a` ACP runs: the catalog no longer contains bracket ids, every advertised base and sampled variant sku spawns, and in-session `claude-opus-4-8-low` / `cursor-grok-4.6-low` set the matching `effort`/`reasoning` option. * test(cursor): cover aliased effort ids and thinking coexistence Covers the branches the HAPI bot review called out for #1818: the `xhigh` → `extra-high` alias on `gpt-5.5` (`reasoning`), the `reasoning_effort` id used by `gemini-3.8-flash`, and switching effort on a model that also advertises Claude's separate `thought_level` `thinking` toggle. * test(cursor): cover legacy sku base and post-switch effort option Adds the two apply-path cases the HAPI bot review called out for #1818: resolving `cursor-grok-4.6-high` onto an advertised `grok-4.6` base, and reading the effort option Cursor advertises for the newly selected model (`reasoning` for gpt-5.5 → `effort` for claude-opus-4-8) instead of the previous model's options. * test(cursor): create the cache dir in the migration test The version-envelope test wrote $HAPI_HOME/cache/cursor-models.json without creating the directory, so running it alone (or filtering with -t) failed with ENOENT before the assertion; it only passed because sibling tests in the same file had already created the path. Reported in the HAPI bot review of #1822. Verified: `vitest run ... -t 'ignores pre-versioned'` → 1 passed, whole file → 4 passed, and every other changed test file passes when run standalone. * Clarify Cursor Auto live-switch limitation in session pickers * Reconcile partial Cursor model changes before acknowledging Auto * Serialize Cursor model changes across clients with the existing async lock --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
8f9d6bd7f8 |
fix(hub): keepalive-only sessions reconcile to an idle lifecycle (#1820) (#1821)
* fix(hub): reconcile keepalive-only sessions to an idle lifecycle `sessionCache.expireInactive` only expires a session when `activeAt` goes stale, and `session-alive` refreshes `activeAt` every ~2s for as long as the CLI socket lives. A session whose agent has done nothing for days therefore stays `active: true` / `lifecycleState: running` forever — socket pinging was being read as agent health. Add the missing second signal. `active` keeps its meaning (the CLI is reachable); `metadata.lifecycleState` moves `running` -> `idle` once the hub has seen nothing but keepalives for `HAPI_SESSION_IDLE_TIMEOUT_MS` (default 12h, `0` disables), and back to `running` on the next real progress. `active` is deliberately not flipped: the socket really is up, and a false `active: false` would let `resumeSession` spawn a second agent against a live process and would unlock dedup-merge / delete on it — the #1705 class of bug. - `sessionCache.recordAgentProgress` tracks a progress clock that keepalives can never touch: messages in either direction, queued prompts, background tasks. Seeded from `updatedAt` so a hub restart re-derives it from disk. - `reconcileKeepaliveIdle` runs on the existing 5s inactivity tick. Sessions that are thinking, running background tasks or holding a pending permission request are never marked; `metadata.idleReconcileExempt` opts out entirely. - Only real progress clears `idle`, never `thinking` — Cursor ACP emits ambient idle/running churn (#1553) that would otherwise flap the lifecycle every tick. - `isLiveLifecycleState` replaces the `=== 'running'` literals so an `idle` row does not read as dead to the archive route or the Cursor migrator. - Web splits keepalive-only sessions into their own session-list bucket. Refs #1820. Distinct from #842 (`active=false` with a stale `running` badge); this is the inverse. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(hub): seed the idle clock from stored messages across hub restart PR review on #1821: a cold `SessionCache` had no observed progress and fell back to `updatedAt` alone. Assistant messages deliberately do not move `updatedAt`, so a session that was streaming output a minute before a hub restart read as hours idle and got marked on the very next tick — a false `idle` that persisted until the next progress event. Seed the clock once from `getNewestMessagePosition`, which is the durable record of that output. `updatedAt` stays a floor on top of the seed: todos / teamState / agentState writes bump it without routing through `recordAgentProgress`. Regression test recreates a SessionCache over the same store after recent assistant output and confirms keepalives no longer cause premature idle classification — and that the same session is still marked once the output itself goes stale. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * test: cover the socket wake-up path and the idle session-list bucket Follow-up review on #1821 reported no findings but flagged two uncovered paths. Both have bad failure modes, so close them. - Hub: an assistant message arriving through `registerSessionHandlers` must wake an idle session. Assistant messages never bump `updatedAt`, so the `onAgentProgress` hook is the only thing that can clear the mark — a refactor dropping that wiring would silently false-idle every actively working session, and nothing would have caught it. - Web: extract the inline bucketing from the `runningSessions` useMemo into a pure `bucketRunningSessions`, matching how the rest of SessionList's logic is exported and tested. Covers idle vs quiet-but-ready separation, no double-bucketing, the transition back out of idle on thinking/background task/pending request, and the disconnected/pinned skips. No behaviour change. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * test: cover the idle lifecycle at both isLiveLifecycleState call sites Third review round reported no findings but named the two guards this PR changed as uncovered. Both protect against reading a keepalive-idle row as a dead one: - Cursor migrator: an idle-marked row still has a live CLI behind it, so preflight must refuse it without `forceArchiveRunning`. A regression here would let the migrator transplant the ACP session directory out from under a running Cursor runner. - Archive route: once a keepalive-idle session finally loses its socket it must stay archivable, rather than being stranded behind a 409. Both tests were confirmed non-vacuous by reverting each guard to the `=== 'running'` literal; each then fails. No behaviour change. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * test: stop the restart regression leaking its temp DB on failure The file-backed store's `rmSync` was a trailing call, so any failing assertion above it skipped cleanup and left a temp DB behind — one per failed run. Noticed while verifying the test was non-vacuous, which deliberately fails it. Wrap in try/finally. Verified: a forced failure now leaves zero `/tmp/hapi-1820-*` directories. Test-only. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
facb66ee03 | Release version 0.30.7 | ||
|
|
6c69e2f315 |
fix(cli,hub): finalize canceled permission requests and surface stale answers (#1854)
* fix(cli,hub): finalize canceled permission requests and surface stale answers
When Claude Code sends control_cancel_request for a pending AskUserQuestion,
the abort handler only removed the local pendingRequests entry and rejected
the promise — it never finalized the request in agentState the way the
session-level cancelPendingRequests path already does. A late answer to the
now-stale widget would still pass the hub's requests[id] check, forward via
RPC, land on a CLI that had already forgotten the request, and get silently
absorbed with no error anywhere.
Two changes, needed together:
- permissionHandler.ts's abort handler now calls finalizeRequest with
status: 'canceled', so cancellation is reflected in agentState the same
way a session-level cancel already is. Once synced to the hub, the
existing requests[requestId] check in permissions.ts naturally rejects a
late answer with 404 before it ever reaches the CLI.
- For the narrower race where an answer's RPC reaches the CLI before that
sync lands, handleMissingPendingResponse now throws instead of only
logging. RpcHandlerManager.handleRequest already turns a thrown error
into a `{ error }` RPC response; RpcGateway.approvePermission/
denyPermission now check for that and throw a typed
PermissionRequestNotFoundError, which the hub route turns into a 409
instead of silently returning { ok: true }.
Fixes #1735
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(cli,hub): match the specific not-found message instead of any RPC error
approvePermission/denyPermission treated any error on the Permission RPC
method as "request not found", reasoning only in a code comment that
nothing else on that path currently throws. Share the exact message via a
new PERMISSION_REQUEST_NOT_FOUND_MESSAGE constant in shared/rpcMethods.ts,
imported by both sides, so a future unrelated throw in handlePermissionResponse
surfaces as itself instead of being relabeled as a stale request.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Ian Stevenson <is@lockhouse.systems>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
6d44996a59 | Release version 0.30.6 | ||
|
|
8cefb0f04c | Release version 0.30.4 | ||
|
|
95b06bd502 | Release version 0.30.3 | ||
|
|
917baf1201 | fix(codex): sync shared plan proposals and web actions | ||
|
|
dd9cfdcee5 | Release version 0.30.2 | ||
|
|
2c738b6730 | Release version 0.30.1 | ||
|
|
d18c01b4b6 |
revert(codex): remove Luna Reserve fallback (#1780)
Revert
|
||
|
|
c75cf8154f | Release version 0.30.0 | ||
|
|
0c4abcb3d1 |
feat(codex): share sessions across terminal and web
Use one native app-server for terminal, Web and phone clients while retaining the existing CLI and Runner lifecycle. Synchronize native queues, permissions, question history and steering state; preserve explicit permission precedence and per-turn usage models. Resume inactive clear commands through Runner and reject independent child cold resumes. Add shared-runtime regression tests, generated protocol fixtures and lifecycle documentation. |
||
|
|
a729456682 |
fix(claude): answer local permission prompts from web
Bridge main-session PermissionRequest hooks without suppressing the native terminal dialog. Reconcile replies against native results and clean up on timeout, cancellation, mode switches, and session changes. Keep reply IDs distinct from native tool IDs across web and native clients; add protocol fixtures and regression tests. Refs #1796 |
||
|
|
68422ed6d3 |
fix(web): use conversation content for session reference eligibility (#1800)
* fix(web): base session reference eligibility on conversation content * fix(hub): refresh replacement content after clear abort * fix(hub): refresh content on idempotent clear aborts |
||
|
|
fd2822bab5 |
fix(agy): allow switching existing sessions to newly available models (#1814)
* fix(agy): say what the model picker is actually waiting on The spinner in the New Session AGY picker read "Checking Antigravity authentication…", but nothing at that point checks authentication — the machine is running `agy models`, and the sign-in prompt is a separate branch below it, shown only when agy reports the failure. Name the wait after the work: "Fetching available models…", the same words agy prints while it fetches. * refactor(agy): describe a probe by its outcome, not by its response The probe function returned a finished `AgyModelsResponse`, so "agy could not be reached" and "agy listed no models" both arrived as a successful response carrying the hardcoded mirror, and the caller could no longer tell which had happened. Every policy decision about that answer has to live inside the probe as a result. Hand back what the probe observed — a live catalog, an auth failure, or nothing usable — and let the caller turn it into a response. Same behaviour: the mirror still stands in for both failure modes, and the 60s cache still holds whatever came out. * fix(agy): serve the model catalog stale-while-revalidate The `agy models` probe is a whole agy invocation — around 3s on a good day, 15s when it times out — and the 60s window meant the New Session picker paid that again a minute after the last look. Keep the last listing agy actually returned and answer from it: fresh for ten minutes, then still answered while a probe refreshes behind it, until the entry is a day old and stops standing in for the machine at all. A probe that times out or loses auth leaves that entry alone, so a blip no longer empties a working picker, and the hardcoded mirror is no longer recorded as if the machine had reported it. Three things fall out of that and are handled here. A machine whose sign-in has actually gone bad would otherwise look healthy for a day, so an auth failure rides along with the catalog it can still serve — and, because nothing else would re-probe a catalog that is still fresh, a warning riding on the answer is itself a reason to look again. A failed probe is not repeated on the very next request either, or a machine where agy hangs would spawn it once per poll. An explicit refresh always costs a probe, and never rides one that was already running when it was asked for. * fix(agy): let Retry force a fresh model catalog probe With the catalog held for ten minutes, Retry would otherwise hand back the answer it was pressed to replace, so the intent travels to the machine: `?refresh=true` on the machine route, an optional RPC param, and a one-shot flag on the query so ordinary mount and focus refetches stay cheap. Every hop is optional, so a hub and a runner on different versions still talk — the older side ignores it and answers from its cache. Retry also has to be reachable, and honest, in the state that needs it. The machine now answers with both a usable catalog and the sign-in failure behind it, so the picker keeps the list and says why it may be out of date, with the button right there rather than only once there is nothing left to show. Pressing it runs agy, which can take tens of seconds, so the button says so while it does. The client contract covers both: `agy-models` is the one catalog route that can carry an `error` on a successful response, and the one that takes a refresh parameter. * fix(agy): use the machine catalog in the in-session model picker New Session already asks the machine what `agy models` lists, but a session that is already open offered the built-in list in `shared/src/models.ts`. That list is a hand-maintained mirror, so a model agy started offering after the last release could be picked for a new session and not for the one already running. Point the composer at the same machine catalog. The mirror stays as the fallback for the moment before the machine answers, and a model the session is already on is kept selectable — and readable, when it is one of the known presets — even after the catalog moves on without it. * fix(agy): announce a model catalog re-check that changed the answer Serving the last known catalog answers the picker instantly, but a picker that was already open kept showing that answer until the user closed and reopened it — the machine had no way to say it had found something newer. Say it on the stream that already carries machine changes. The machine daemon — the only process that answers `<machineId>:listAgyModels` — emits it, and the hub forwards it as `machine-agy-models-updated` with nothing but the machineId. Namespace resolution, per-machine delivery and reconnect replay all come from the existing path. What counts as a change is what the route would answer, not what sits in the cache. That distinction carries the cases: a sign-in that lapsed or came back changes no models yet changes what the user is told; a machine whose agy was signed out has been answering from the hardcoded mirror, and its first real listing is the largest change there is, for every client except the one awaiting it. * fix(agy): re-read the announced machine's model catalog On `machine-agy-models-updated`, cancel and refetch that one machine's catalog query — the app's global connection is always subscribed, so an open picker redraws wherever it is. Cancelling first is what makes it correct rather than merely likely. query-core cancels an in-flight fetch only when the query already holds data, so a picker opening for the first time would otherwise join the request already on its way and settle on the listing the announcement replaced. The refetch is answered from the machine's cache, so it starts no probe and cannot bounce another announcement back. A reconnect the hub could not replay takes the resync path, which clears the agy catalogs the same way — that path has no announcement to fall back on, so it is the one that can least afford to join a stale request. * fix(agy): keep a model the user picked when the catalog moves under them The catalog can now change while the New Session form is open, and the form dropped any selection the machine no longer advertised — including one the user had just made. Keep that one, and list it as no longer listed so the form does not imply agy is still offering it. A model restored from a draft or a saved preference is still dropped: it may never have been runnable here. * fix(agy): announce uncached authentication changes |
||
|
|
de93d7b977 | Release version 0.29.1 | ||
|
|
7031d60eb4 |
fix(opencode): expose model-specific reasoning effort options (#1716)
* fix(cli): discover opencode thought_level via set_config_option on model switch * fix(cli): apply and refresh opencode model switches so thought_level stays discoverable * fix(web): track opencode effort options across model switches * feat(cli,hub,web): dynamic opencode effort options in new-session form * test(cli): avoid platform-specific process event narrowing * fix(opencode): address variant discovery review findings * fix(opencode): synchronize effort options with model targets * fix(opencode): roll back rejected model targets * fix(cli): guard opencode variant probe workspace paths * fix(web): clear stale opencode effort on model switch * fix(cli): clear stale opencode effort metadata * fix(web): reset stale effort options on model switch * fix(web): reset opencode effort poll budget * test(web): enforce opencode effort poll budget |
||
|
|
8357da0a9d |
feat(codex): support backend-authorized Luna Reserve fallback (#1780)
* feat(codex): reconcile Luna Reserve fallback and conditional usage * fix(codex): preserve queued settings and reconcile Reserve sessions |
||
|
|
17ee052d9a |
fix(web): preserve the visible chat window during rewind (#1766)
* fix(web): preserve chat window during rewind * fix(web): scope rewind invalidation preservation * fix(web): clear unknown rewind boundaries * fix(web): deduplicate rewind invalidations * fix(web): retain rewind dedupe history |
||
|
|
aa0c1dc808 |
fix(codex): fail closed on ambiguous Web Rewind boundaries (#1707)
* fix(codex): fail closed on ambiguous Web Rewind boundaries * fix(codex): reject malformed native rewind history * fix(codex): reject duplicate rewind identifiers * feat(codex): offer Fork fallback for ambiguous rewind * fix(codex): gate rewind Fork fallback on exact native boundary * fix(codex): reject unresolved rewind boundaries safely * chore: trigger PR checks * fix(codex): gate safe rewind fallback on fork support * fix(codex): require complete user ids for rewind fallback |