Commit Graph
292 Commits
Author SHA1 Message Date
362b1fe47e feat(teams,dsh): overview dashboard, lead task auth, and DSH launch tokens
- teams: cross-team overview read model + route/query/page; enforce member
  target visibility and restrict member task creation to the Lead
- dsh: accept 0.2.x launch tokens from the printed URL, HAPI_DSH_TOKEN, or
  HAPI_DSH_TOKEN_FILE; bundled wrapper captures the printed token securely;
  per-session agent availability and remote event/auth handling
- usage: support the 1d range and per-session totals in summaries
- cli/web: team prompt/client plumbing, availability gating, locale and
  route wiring for the overview page

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: HAPI <noreply@hapi.run>
2026-11-05 02:22:50 +08:00
bc6b1577fa merge: hapi upstream v0.34.1
Merge release tag v0.34.1 into the local deployment fork. Preserve local
features (Agent Team, Studio, session shares, skins, DSH Web runtime,
attachment cache, deploy tooling) while adopting upstream session-list,
composer, steering, model-catalog, native-client, and migration changes.

Regenerate the local chat_attachments migration as v31→v32 behind the
upstream v28-v31 ladder; keep DSH on the Web runtime instead of the ACP
transport added upstream.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: HAPI <noreply@hapi.run>
2026-11-05 00:51:11 +08:00
weishu 13541a1928 Release version 0.34.1 2026-11-04 16:00:13 +08:00
Junmo KimandGitHub c4847b241e feat(opencode): support mid-turn steering (#1981)
* feat(opencode): support mid-turn steering

Register the per-message Queue/Steer RPC on the OpenCode remote launcher so
a waiting-queue row can be delivered into the active turn as a concurrent
ACP session/prompt (soft inject, no cancel, no mode/model/effort mutation),
reusing the existing reservation/delivery-state protocol shared with Codex
and Cursor ACP.

The steer gate is closed synchronously by every cancelling or terminal path
(normal-prompt finally, abort, switch/exit, teardown, stderr stall cancel),
each steer operation is tracked from the moment its reservation is taken
until completion plus all restore/consumed bookkeeping settles, and the
foreground prompt drains those operations — abort-aware through the original
turn signal — before ready, thinking=false or the next batch. steerability is
published through the existing agentState.steeringActive flag so the web's
queued Steer button is reachable.

Gate the feature on the shared steering flavors (now including opencode) and
update the hub's supported-flavor wording.

* fix(opencode): acknowledge native steering acceptance

* fix(opencode): preserve steering delivery outcomes

* docs(opencode): remove obsolete abort-race comment

* fix(opencode): preserve steering resolution ownership

* fix(opencode): isolate ambiguous steering attempts

* fix(opencode): compare resolved steering settings

* fix(opencode): reconcile steering receipts after dispatch

* fix(opencode): distinguish pending steering delivery outcomes

* fix(opencode): restore receipts after successful native responses

Track native input requests separately from detached steer operations. Resume receipt correlation after inputs quarantined by cancellation return their own successful responses, while keeping uncertain writes on completion-based confirmation.

* fix(opencode): bound receipt proof to unambiguous native inputs

* feat(opencode): steer v2 inputs through native inbox

* fix(opencode): preserve v2 restoration and cancellation ownership

* docs(opencode): describe v2 inbox steering confirmation

* fix(steer): expire Claude confirmation with its owning worker
2026-11-03 18:37:53 +08:00
AnanovoandGitHub 82c5c00a38 feat(web,hub): add opt-in search for session message content (#1598)
Adds an opt-in content search over session messages, alongside the
existing metadata search. An FTS5 index covers visible user and assistant
prose; the sidebar gains a scope toggle, and selecting a result opens the
matching message rather than the session tail.

Search scope is a two-state toggle rather than a dropdown: a 48x48 target
matching the clear endcap, distinct glyphs for session details vs message
text, tooltips naming the real scopes, and the active mode kept on the
collapsed chip (tiann/hapi#2081).

Dogfooded on a live 1178-session fleet. The scope toggle and reading
forward from a search result were both click-tested on that install —
the latter walking six bounded pages through a 1,507-message session and
stopping correctly at the session head.

The per-commit history does not survive this squash; the review trail —
including which commit closed which finding — is on the PR threads.

Fixes #1554
Fixes #2081
2026-11-02 15:20:45 +00:00
weishu 1f4518df20 Release version 0.34.0 2026-11-02 19:07:01 +08:00
AnanovoandGitHub 3fc15e92cf fix(dsh): resume DSH ACP sessions after HAPI restart (#1926)
* fix(dsh): resume DSH ACP sessions after HAPI restart

* fix(dsh): register active session handoff

* fix(dsh): preserve native id in summaries

* fix(dsh): keep native resume on recorded machine

* test: refresh CI checks
2026-11-02 18:24:08 +08:00
AnanovoandGitHub 3208199f99 fix(files): optimize exact path and directory search (#1778)
* fix(files): optimize exact path and directory search

* fix(web): preserve incremental file mention search

* fix(cli): contain recursive file search within workspace

* perf(cli): reuse canonical workspace path

* fix(cli): classify archived workspace file stats
2026-11-02 18:08:32 +08:00
7f5d6b17f3 feat(peer): spawn-peer remit + rename-proof Parent UUID stamps (#1922)
* feat(cli): spawn-peer with required remit and MCP spawn_peer

Machine spawn creates an empty composer; stuffing message into that
JSON is stripped. Add hapi spawn-peer and MCP spawn_peer that spawn,
rename, deliver via the ping-peer path, and fail closed unless the
remit is on the new session. Reject message/prompt/text on machine
spawn with 400. Do not auto-approve spawn_peer in default/read-only.

Fixes #1509

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

* fix(cli): reject overlong spawn-peer names before spawn

Hub rename max is 255; continuing past PATCH 400 left a live session under a different title. MCP/steer copy now says the hub 400s a spawn-body remit instead of stripping it.

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

* fix(cli): wait for fresh spawn-peer child instead of resuming it

Machine spawn can return while the hub row is still inactive. ping-peer then POSTed /resume, which can launch a second agent because the original spawn has no existingSessionId for runner dedupe.

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

* fix(cli): reject flavor-illegal permissionMode before spawn-peer

Codex + bypassPermissions (and Pi + any mode) used to reach the runner and fail after a child existed, or silently drop the mode. Check isPermissionModeAllowedForFlavor when both agent and mode are explicit.

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

* fix(cli): treat omitted spawn-peer agent as Claude for permissionMode

Hub and runner default agent to claude when spawn omits it. Validate permissionMode against that effective flavor so yolo/read-only fail closed before a child starts.

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

* fix(cli): resolve spawn-peer dir locally and archive failed children

Relative --dir values were forwarded unchanged, so the runner resolved them against daemon cwd. Remit delivery failures also left the new session running, so a retry spawned a duplicate. Resolve in the caller and best-effort archive the child before rethrowing.

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

* fix(cli): verify remit before archiving a failed spawn-peer child

A timed-out ping-peer POST can still land the remit. Check the transcript before archive, and only say the child was archived when the hub returns ok.

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

* fix(cli): swallow spawn-peer transcript GET errors and skip unapplied names

A thrown messages GET after spawn skipped archive and leaked a live child. Treat transport errors as a miss so the verify loop can retry, then clean up. Also do not return a requested name the hub never applied.

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

* feat(spawn-peer): hub peerSpawnDefaults + model/effort spawn surface

Add hub settings peerSpawnDefaults (agent, permissionMode, per-flavor models)
with stock claude/yolo/sonnet fallbacks. spawnPeer resolves explicit args →
hub settings → stock, maps yolo to flavor equivalents, and forwards model/effort
into machine spawn. Settings UI under General → Agents; MCP/CLI optional overrides.

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

* test(hub): use cursor-valid permission mode in peer spawn defaults test

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

* fix(cli): teach spawn_peer that omit permissionMode means yolo

MCP copy said "pass permissionMode only when the operator asked", so agents
omitted it and got hub/stock whatever was stored — often approval-gated
default. Clarify omit = yolo via peerSpawnDefaults/stock; pass only to tighten.

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

* fix(hub,web): apply peerSpawnDefaults to machine spawn and New Session

POST /api/machines/:id/spawn now resolves omitted agent/permissionMode/model
from hub Settings, and New Session seeds from the same peerSpawnDefaults so
UI create and peer spawn share one authoritative default (stock yolo).

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

* fix(web): NewSession hub seed respects sticky prefs + QueryClient in tests

Sticky localStorage last-used agent wins over hub seed; machine spawn still
applies peerSpawnDefaults server-side. Tests provide QueryClientProvider.

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

* test(web): mock CREATABLE_AGENT_FLAVORS for settings general peerSpawn UI

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

* test(web): stabilize NewSession tests after hub peerSpawnDefaults seed

Wrap remaining mounts in QueryClientProvider, pre-seed hub settings so a
late useQuery resolve cannot wipe in-test model picks, and use neutral
stock permission defaults in the fixture.

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

* fix(spawn-peer): address tip Codex Majors for peerSpawnDefaults

Preserve explicit yolo booleans on machine spawn, fail closed when hub
defaults cannot load, validate permission modes against the resolved
agent, remap auto-approval across flavors, and stop late hub settings
from wiping New Session edits. Reject retired agents in spawn defaults
and restore the CLI executable build docs.

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

* fix(spawn-peer): address follow-up tip Codex Majors

Stop absent Yolo localStorage from beating hub defaults, track New Session
edits before late hub seed, reject incompatible machine-spawn permissions,
resolve MCP spawn_peer paths against the session working directory, and
hide unsupported Codex safe-yolo from Settings spawn defaults.

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

* fix(spawn-peer): clear tip-2 CI and follow-up Codex Majors

Omit permissionMode for Pi/DSH, preserve explicit Yolo-off across delayed
hub seed, fix settings test mocks for launch permission helpers, and
restore unrelated CLI README lifecycle/ACP docs.

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

* fix(spawn-peer): tip-3 Majors for seed edits and launchable modes

Skip hub agent seed after permission edits, resolve inherited modes via
the launch catalog (AGY request-review fallback), preserve hub Cursor
models through preference restore, and restore Codex auth README nuance.

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

* fix(web): tip Minors for Cursor/Grok hub permission seeding

Give Cursor the native permission select so Plan/Ask survive machine
spawn, and re-apply Grok hub permissions after agent-init resets.

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

* fix(spawn-peer): tip Minors for Cursor select and hub seed remap

Map hub permission through resolvePermissionModeForFlavor, keep native
Default when agent is explicit without a model, and assert Cursor Plan/Ask Mode labels.

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

* fix(web): tip Minors for delayed hub seed and Default model

Apply hub permission/model to unset fields even with a sticky agent,
preserve explicitly saved auto models, and seed OpenCode/AGY selectors
from hub defaults.

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

* fix(spawn-peer): tip Minors for model edit sticky and clear override

Mark OpenCode/AGY model picks as edited so delayed hub seed cannot
overwrite them, and treat empty hub model values as clear sentinels.

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

* fix(spawn-peer): tip Majors for models patch typing and remit page scan

Type the models patch as NonNullable and paginate transcript verification
oldest-ward so a busy child is not archived when the remit leaves page 1.

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

* fix(web): tip Minor — keep agent after explicit model pick

Do not apply a late hub default agent when the operator already chose a
model; otherwise preference restore drops that selection.

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

* fix(spawn-peer): tip Cursor YOLO migrate and Codex effort map

Bridge Cursor's saved YOLO toggle (incl. false) into native Default over
hub yolo, and send Codex/OpenCode --effort as modelReasoningEffort.

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

* fix(web): tip Majors — scoped YOLO bridge and catalog fallback

Keep unsupported saved permission modes at Default instead of hub yolo,
and apply delayed YOLO migration only when the sticky bridge agent matches.

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

* fix(cli): drop rebase conflict markers in startHappyServer

Both sessionDisplayRename and spawnPeer imports are required after
rebasing onto upstream rename work; markers had slipped into tip.

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

* fix(web): wrap Kimi NewSession tests in QueryClientProvider

Hub settings useQuery requires a provider; upstream Kimi catalog tests
rendered bare and broke after the spawn-peer rebase onto main.

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

* feat(peer): rename-proof Parent UUID stamp on spawn remits

Orchestrated spawn_peer / spawn-peer remits now auto-stamp a durable
[title](/sessions/<uuid>) Parent chip (fail-closed in-session). MCP copy
forbids inventing titles; UUID stays identity after parent rename.

Refs heavygee/hapi#175

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

* style(peer): plain hyphen in Parent-stamp warning and test comment

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

* fix(peer): reject blank remit before Parent stamp

Codex review on #1922: stamping first turned empty remits non-empty and
bypassed the idle-session guard. Also only treat ## Parent blocks as
already-present so bare /sessions citations still get the chip.

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

* fix(peer): gate New Session Create on hub defaults; require Parent chip

Codex tip review on #1922: block Create while getHubSettings is pending so
stock mount defaults cannot bypass restrictive hub peerSpawnDefaults; skip
Parent stamp only when ## Parent already has a markdown session chip; clarify
codex resume takes a native thread id.

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

* fix(peer): resolve MCP spawn cwd; Agents settings section

Codex Major on #1922: spawn_peer relative directories now prefer the
launcher cwd (Codex --cd), then skillLookup, then session metadata.path.
Move peerSpawnDefaults UI into the unused Agents settings section.

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

* fix(peer): pass session cwd to MCP spawn_peer; Agents settings section

Codex tip review on #1922: resolve relative spawn_peer directories from the
launcher working directory (Codex --cd), skillLookup, then metadata.path;
move peerSpawnDefaults UI into the Agents settings section.

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

* fix(peer): skip archive on unread transcript; validate startingMode after hub agent

Codex tip review: do not archive a child when remit verification cannot read
the transcript; run AGY/DSH remote-only startingMode checks against the
resolved hub agent; update MCP bridge test expectations for workingDirectory.

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

* fix(peer): treat exhausted remit pagination as unverifiable

Hitting the messages page cap with hasMore still true must not count as an
observed-empty transcript — that would archive a child whose remit may still
exist on older pages.

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

* docs(cli): list HAPI_API_URL as optional with localhost default

Codex tip review: the README had moved HAPI_API_URL under Required even
though configuration defaults it to http://localhost:3006 (and settings can
supply a URL). Also drop trailing whitespace flagged by git diff --check.

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

* fix(peer): re-verify empty transcript before archiving failed spawn

An earlier observed-absent page plus later unread GETs must not archive a
child whose remit may have landed after the first empty read.

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

* fix(peer): never auto-archive on spawnPeer failure path

Timed-out remit POSTs and empty-read/archive TOCTOU races can still land
work on the child. Leave the session running with its id for inspection
instead of terminating an in-flight peer.

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

* fix(peer): classify empty_session from final remit verify

An earlier empty transcript read must not stick as empty_session once
later message GETs become unavailable — report verify_failed instead.

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

* fix(peer): recognize escaped brackets in Parent chip labels

Titles with ] were escaped in the chip label but the idempotency regex
stopped at the first raw bracket and re-stamped a second Parent block.

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

* fix(peer): avoid Parent chip regex backtracking on backslashes

Exclude \\ from the single-char label alternative so escaped pairs have
one parse path — long nonmatching backslash runs no longer explode.

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

* feat(cli): MCP/CLI spawn_peer --machine for cross-host remits

Wire optional machine (UUID or hostname via GET /api/machines) through
MCP spawn_peer and hapi spawn-peer into existing spawnPeer({ machineId }).
Skip local directory existence checks for cross-host targets to match the
estate wrapper; keep fail-closed remit verify.

Fixes tiann/hapi#1931

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

* fix(peer): reject ambiguous spawn --machine label matches

Tip bot Major on #1922: hostname/displayName selectors that match more
than one online runner now fail closed with ambiguous + UUID guidance.

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

* fix(peer): preserve cross-OS absolute dirs for spawn --machine

path.resolve on the caller OS was rewriting foreign absolute roots
(C:\… on Linux, /srv/… on Windows) before the hub spawn POST. Detect
POSIX or Windows absolute paths and only resolve relative directories.

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

* fix(peer): resolve spawn dirs after local vs remote is known

Defer directory resolution until the target machine is selected. Remote
spawns still preserve foreign absolute roots; local spawns use native
path.resolve so Windows drive-less paths like \repo bind to the caller
drive.

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

* fix(web): apply delayed DSH model defaults without replacing edits

* fix(web): preserve hub permission defaults across form remounts

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-11-02 13:17:22 +08:00
weishuandGitHub 7bb2eb914f feat(claude): mid-turn Steer for remote Claude sessions (#2259)
* feat(claude): mid-turn Steer for remote Claude sessions

Queued messages can now be steered into a running Claude turn, the same as
Ctrl+X Ctrl+S in the Claude TUI. The row is written to the live stream-json
stdin and Claude folds it in at its next tool boundary.

- Spawn Claude with --replay-user-messages and tag every stdin message with
  a uuid; the echo acks pickup and is never relayed to chat
- A result that arrives while a steer is still unacked does not end the
  turn: Claude runs a follow-up turn for it, so ready waits for that result
- Launcher registers steer-queued-message: reserve the row, reject control
  commands and mode mismatches, ack the hub after the write, mark consumed
  (steered) on the echo; abort before pickup holds the row indeterminate,
  an unprompted exit requeues it
- Open the shared steer gate and peer/hub wording to Claude (remote only)

* fix(claude): retire completed input before a steer follow-up turn

- Generalize onCompactResultAccepted to onResultAccepted and fire it when a
  result arrives with a steer still unacked, so a stream failure during the
  follow-up turn recovers only the steer instead of replaying the finished
  prompt
- Close the steer target and settle pending steers when query() setup
  throws, so a retry backoff cannot accept a steer nobody will read
2026-11-02 10:12:14 +08:00
weishu 7ba52b9a5a Release version 0.33.6 2026-11-01 20:30:00 +08:00
AnanovoandGitHub ecf40d8851 feat(web): fork active and archived sessions from the session list (#2258)
* feat(web): fork active and archived sessions from the session list

* fix(codex): preserve fork titles during history replay

* fix(web): limit list forks to supported Codex sessions
2026-11-01 14:52:42 +08:00
weishu ba2b5c5651 Release version 0.33.5 2026-11-01 13:19:38 +08:00
weishu b0d01c5307 Release version 0.33.4 2026-11-01 12:26:43 +08:00
AnanovoandGitHub 46bae9b0f1 fix(codex): fork archived sessions without reopening the source (#2231)
* fix(codex): fork archived sessions without reopening the source

* fix(codex): reconcile interrupted archived fork recovery

* fix(codex): release rejected forks and serialize source startup

* fix(codex): fence unconfirmed resume startup across retries

* test(codex): validate durable resume recovery and serialize archive

* fix(codex): block local resume of pending archived fork sources

* fix(hub): coalesce concurrent resume requests to preserve idempotent startup

* fix(hub): bypass startup coalescing for PTY sessions
2026-11-01 09:55:01 +08:00
Junmo KimandGitHub be41e854e0 feat(codex,opencode): show compaction summaries and token counts (#2249)
* fix(chat): render available compaction facts

* feat(opencode): unify native compaction summary cards

* fix(opencode): report actual compaction application lifecycle

* feat(codex): render native compaction checkpoints and metrics

* fix(chat): render compaction summaries and token facts in native clients

* fix(opencode): preserve transcript priming across read failures

* fix(opencode): recover snapshot-only replies after attachment
2026-11-01 09:54:10 +08:00
9c6639c3c3 feat(hub): title untitled sessions with their first prompt (#2252)
New sessions showed the working directory name until the first turn
finished and a generated or native title arrived. The Hub now records
the first non-command user prompt as metadata.promptTitle, with
whitespace collapsed and capped at 80 characters, and the web shows it
after name and summary but before the path fallback.

The prompt title is Hub-owned and kept outside summary, so the existing
"only while untitled" title sources (native ai-title, change_title, the
remote prompt fallback and automatic suggestions) still apply and
replace it. It is published as a versioned metadata patch, so it does
not race the thinking patch sent while the prompt is accepted.

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

Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:53:35 +08:00
weishu 5ef4304a4a fix: synchronize read state across web and native clients
Track readable content revisions and durable read cursors in the hub. Acknowledge only rendered foreground tails, persist offline read operations, and preserve newer manual unread choices across devices.

Migrate existing history to a read baseline and cover pagination receipts with shared native conformance fixtures.
2026-11-01 09:52:51 +08:00
SSU-WEI HUANGandGitHub 5fcd4a067a fix(agy): separate model and effort selection (#2244)
* fix(agy): separate model families and reasoning effort

* fix(agy): report startup before model discovery
2026-10-30 07:36:13 +08:00
weishu d9acc0c31e Release version 0.33.3 2026-10-29 22:29:49 +08:00
3f8686e623 feat(claude): offer the four current Claude models, drop the 1M presets (#2153)
* fix(claude): drop the obsolete 1M model presets

The opus, sonnet and fable aliases now resolve to models with a native 1M
window (Opus 5.5, Sonnet 5.5, Fable 5.1), so Claude Code documents the
"[1m]" suffix as having no effect on them and its own /model picker no
longer lists 1M rows. Remove Sonnet 1M / Opus 1M / Fable 1M from the
shared, Android and iOS catalogs.

Sessions stored with a "[1m]" value keep working: the composer keeps
the current model selectable, and the context-budget fallback treats any
"[1m]" model and the remaining aliases as 1M, so local-mode sessions on
plain opus/sonnet no longer show a 200k budget.

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

* fix(cli): seed a 1M turn-1 context window for bare Claude aliases

With the 1M presets gone, a session on plain fable/opus/sonnet must not
seed 200k before its first result: those aliases now resolve to models
with a native 1M window. Seed 1M when the selected model is a preset
alias; an explicit full id still seeds conservatively until its result
arrives.

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

* feat(claude): offer the four current Claude models as presets

The Claude presets now mirror the aliases Claude Code's /model lists, in
its order: Opus, Fable, Sonnet, Haiku. Each alias resolves to the newest
model of its family, and all four have a native 1M window, so neither
1M variants nor pinned older versions are offered. Haiku was missing.

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

* feat(claude): read the effective context window from get_context_usage

The 1M seed for the aliases is wrong for users who disable 1M
(CLAUDE_CODE_DISABLE_1M_CONTEXT) or cap it with autoCompactWindow. In
remote mode, ask Claude via the get_context_usage control request after
each system/init and use its maxTokens, which is what /context shows.
A result's raw contextWindow no longer overwrites that value. Older CLIs
that reject the request keep the existing seed.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-10-29 22:28:59 +08:00
AnanovoandGitHub 0f76485ad9 feat(web): support local Markdown images in conversation messages (#2215)
* fix(web): resolve relative Markdown assets in file previews

* fix(web): harden standalone Markdown asset resolution

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

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

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

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

* fix(cli): bound session file reads

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

* test(cli): run FIFO regression with Bun

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

* test(web): update markdown preview fixture context

* fix(cli): reject symlinked session files outside workspace
2026-10-29 22:12:57 +08:00
weishu 9384ce031b feat(cli): add Codex-aware auto peer delivery
Default peer CLI and MCP messages to auto, steering eligible Codex turns and queueing otherwise. Preserve explicit delivery modes and uncertain-send safety, and clarify steer guidance for important time-sensitive findings.
2026-10-29 22:02:43 +08:00
weishu 730dfcb22e Release version 0.33.2 2026-10-29 13:24:52 +08:00
weishu 01dabad204 fix(sessions): stabilize ordering by latest user input
Persist and backfill the last user-message timestamp, and use it for deterministic session ordering across web and native clients while preserving manual pins.

Remove the fixed-order setting and guard ordering against transcript echoes, replayed messages, and stale REST/SSE updates. Add regression coverage and regenerate the SSE fixture.
2026-10-29 12:47:52 +08:00
weishu a6cf8fd078 Release version 0.33.1 2026-10-29 10:13:33 +08:00
weishu ee273464f4 Release version 0.33.0 2026-10-29 08:51:11 +08:00
weishuandClaude Opus 5.5 23122ccbc0 feat(hub): persist agent turns and report them through hapi usage
Whether a session's agent is thinking lived only in the session cache,
so once a turn ended nothing could say how long the agent had worked,
and a hub restart forgot even the turn in progress. Tools that track
time (Weft, for one) had no way to see when agents on other machines
were busy.

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:48:06 +08:00
AnanovoandGitHub 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
2026-10-28 12:14:59 +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 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
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
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>
2026-10-28 12:12:41 +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
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
weishu 85ccab1ac6 Release version 0.31.3 2026-10-27 14:23:48 +08:00
weishu 7bf0e5bc35 Release version 0.31.2 2026-10-27 08:32:28 +08:00
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>
2026-10-26 10:27:52 +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
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
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
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
AnanovoandGitHub cb2ff05c2a fix(codex): distinguish compaction waits from unknown delivery (#2115) 2026-10-25 15:34:52 +08:00
ee8802784d merge: hapi upstream main 2026-10-19 (34 commits past v0.30.7)
Bring in the post-0.30.7 upstream work while keeping local team, macaron,
requirement, DSH and deployment features:

- runner/hub: cross-platform orphan reap on archive and tracking loss,
  machine-spawn preallocated stubs, keepalive-only sessions reconcile to idle
- hub: prepared-statement cache, stale/canceled permission request handling
- cli: Codex compaction announcements, dynamic Kimi models, MCP bridge reap
- web: session toolbar icon shedding, mark-all-read, unread/active sections,
  Russian locale, external-link preference, 48px search chrome
- ios/android: locale and font-size updates

Conflict resolution keeps local-only behavior (agent team spawns, macaron
skins, collapsible sidebar, workspace markdown images) layered on the new
upstream toolbar and session lifecycle plumbing.

Test infrastructure: skip the Docker-only pwsh integration test quickly when
Docker is unavailable, and exclude the Bun-only usageScan test from vitest.

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

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 12:44:05 +08:00
wushenghua ee4422dbc5 feat(codex,web): show live context compaction status
The composer status bar surfaces an in-flight Codex context compaction
with the native start time and an elapsed timer; completion,
interruption, reconnect, and shutdown all clear it. The CLI follows the
contextCompaction item on the app-server stream and publishes
contextCompactionStartedAt through the shared agent state.
2026-10-16 20:32:57 +08:00
wushenghua 2d57763177 fix(hub,cli): run OpenCode usage scans off the process event loop
The 5-minute reconciliation read the multi-GB OpenCode store on the hub's
event loop with bun:sqlite; a cold scan froze every request for ~88s and
stalled each machine's runner the same way.

- scan in a spawned `hapi usage-scan` child process (stdin/stdout JSON)
- refresh on demand from the usage page (debounced) + a daily fallback
- a runner whose hub is localhost skips the store the hub already owns
- machine scans run every 30min and read the message table sequentially
  in one pass instead of per-session indexed lookups
2026-10-16 19:38:07 +08:00
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>
2026-10-15 15:42:01 +01:00
wushenghua baeeb11356 fix: preserve deployed session fixes
Persist paged Codex history checkpoints and bounded, acknowledged delivery.
Keep usage sources and accounting across deleted or merged sessions.
Include native Goal controls, faster peer/session queries, and release guards.
Track the complete deployed source so isolated builds cannot omit local fixes.

Validation: all typechecks pass. The full suite reports 7,997 passing tests
and 46 existing failures, each reproduced against the prior commit.
2026-10-08 19:51:45 +08:00