mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-27 22:51:43 +00:00
efc9004fda5c8fd30f876efdebd8f47aaa1b64ff
222
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 | ||
|
|
b41b9293e9 |
fix(web): render cold sessions from a small initial page (#1809)
* fix(web): speed up cold session first paint * test(web): update pagination fixtures for cold page * test(web): wait for cold page before loading history * feat(native): match cold session pagination * test(native): cover cached re-entry page size |
||
|
|
8cefb0f04c | Release version 0.30.4 | ||
|
|
0a9f15a9a7 | fix: align request_user_input other answers across clients | ||
|
|
95b06bd502 | Release version 0.30.3 | ||
|
|
917baf1201 | fix(codex): sync shared plan proposals and web actions | ||
|
|
dd9cfdcee5 | Release version 0.30.2 | ||
|
|
26fddff038 |
fix(chat): restore grouping for shared Codex commands
Group ordinary Codex commands with default tools across web, iOS and Android while preserving exploration and user-shell boundaries. Add regression tests, generated protocol fixtures and shared-command coverage in the iOS transcript UI suite. |
||
|
|
2c738b6730 | Release version 0.30.1 | ||
|
|
d29713aeda |
fix(native): render plan proposals inline
Render ExitPlanMode and exit_plan_mode Markdown from input.plan in iOS and Android conversations. Preserve approvals, raw source and diagnostics while hiding empty output placeholders and prewarming plan documents. Add generated protocol fixtures and native regression coverage for long plans, live updates, recycling, themes and typography. |
||
|
|
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 |
||
|
|
3873e58496 |
fix(web): keep streamed reasoning/text block ids stable across snapshot rows (#1741)
* fix(web): keep streamed reasoning/text block ids stable across snapshot rows
Streaming snapshots of one stream (pi/codex reasoning and text) arrive as
separate message rows, and the window store retires older rows as newer
snapshots land. The timeline derived the block id from whichever row was
first seen, so the id (and the threadMessageId built from it) churned on
every snapshot, remounting the rendered reasoning panel mid-stream and
replaying its open animation — the panel visibly flashed/re-rendered on
every snapshot tick.
Derive the block id from the stream id when present (unique per stream,
stable across snapshot rows) so the block is updated in place and the
smooth streaming keeps appending to the previous text. Row-derived ids
remain the fallback for content without a stream id.
Also rerun gen:fixtures to refresh the two golden fixtures affected by
the new id shape.
* fix(ios,android): mirror stream-stable block ids in native chat ports
The native HapiKit (Swift) and protocol (Kotlin) chat pipelines are ports
of the web reducerTimeline and are pinned by the same golden fixtures in
shared/fixtures/chat. After the web-side change to derive streamed
reasoning/text block ids from the stream id, the ports still produced
row-derived ids, so the iOS/Android fixture conformance suites went red
on the two refreshed fixtures.
Apply the same streamId-first id derivation (row-derived fallback kept)
to both ports so all three pipelines project identical block ids.
* fix(web,ios,android): reject blank stream ids as block identity
Blank ('' or whitespace-only) stream ids are not streams per the wire
semantics in shared/src/messages.ts (readReasoningStreamId trims before
accepting). The previous nullish fallback let accepted payloads carrying
blank ids through, so every such row shared one empty block id: the
merge maps collided and assistant-ui occurrence suffixes churned with
list position, reintroducing remounts.
Normalize with a trim guard in all three pipelines (web, HapiKit,
protocol) and add a web regression test covering both empty and
whitespace-only ids.
* fix(ios): use normalized stream id for block construction identity
The blank-id guard was applied to lookup and map insertion but block
construction still read the raw optional, so accepted payloads carrying
blank/whitespace ids produced blocks sharing one blank SwiftUI identity
instead of falling back to row-derived ids (web/Android already used the
normalized local). Hoist the nonBlankStreamId result and reuse it for
lookup, block identity, and insertion in both the text and reasoning
branches.
Also add native coverage for stream identity: stream-id derivation for
text/reasoning plus blank ('' and whitespace-only) fallbacks, which the
golden fixtures do not exercise.
* fix(web): pin blank stream-id identity contract in golden fixtures
Update the two stale fixture descriptions (stream-keyed blocks are now
keyed by the stream id, not the first message) and add a generated
conformance fixture covering empty and whitespace-only codex data.id
values for both reasoning and text: blank ids are not stream identities,
so each payload keeps its own row-derived block id instead of collapsing
onto a shared blank identity. Web, iOS, and Android all run this same
golden fixture.
* feat(hub): make title provider max_tokens and timeout env-tunable
Reasoning models used as title providers (e.g. GLM thinking models) need
more than 64 completion tokens and more than the hardcoded 10s timeout to
emit a title, and the only workaround was patching the compiled binary
after every install.
Expose both knobs via HAPI_TITLE_PROVIDER_MAX_TOKENS and
HAPI_TITLE_PROVIDER_TIMEOUT_MS, following the existing
HAPI_TITLE_SUGGESTION_RATE_LIMIT pattern; defaults are unchanged.
* docs(hub): document title provider max_tokens/timeout env knobs
Add the two new HAPI_TITLE_PROVIDER_* variables to the title-provider
configuration table in the installation guide, and extend the provider
test to cover the timeout abort path (the signal fires and rejects the
in-flight request).
---------
Co-authored-by: HongChenGG <HongChenGG@users.noreply.github.com>
|
||
|
|
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 |
||
|
|
ec08959f07 |
fix(agy): show Gemini 3.7 Flash in the agy model list (#1585)
* refactor(agy): extract the agy models probe from the fetch flow
One invocation and the decision of what to do with its output were
tangled in a single promise. Split them so the probe can be given
different arguments, and run more than once, without duplicating the
stream and timeout handling.
No behaviour change: the same argument vector, timeout, stream
handling and fallbacks remain, and the existing tests are untouched.
* fix(agy): read the model list from agy's structured output
The picker recovered ids and labels from `agy models`, whose table is
meant for people to read: it has emitted display names only, then
bare ids, then tab-separated id/label pairs over the 1.1.x line, and
each shape change silently sent the probe back to the hardcoded
mirror. Since agy started offering Gemini 3.7 Flash, that mirror is
what users see, so the three new entries never appear.
agy publishes the same listing as a structured payload, so read that
instead: `agy --output-format=json models` returns
`command.data.models[] = {id, label}`. The flag is global, so it goes
before the subcommand and takes its `=` form.
Releases that predate the flag ignore it and print the table, so the
text parser stays as the fallback, and it now also understands the
tab-separated shape. Every release checked, 1.0.16 through 1.1.13,
ignores the flag rather than rejecting it; a build that rejected it
would emit no models at all, so ask once more without the flag when
the first invocation yields nothing either parser can read.
* fix(agy): follow the Gemini 3.7 Flash row in the model picker
agy now lists Gemini 3.7 Flash as the top row of the `/model` TUI
picker, but the hardcoded row table still starts at 3.6 Flash. A
session whose current model is that row can never change its model:
the picker cannot identify the current row and the change is
rejected.
Add the 3.7 row to MODEL_ROWS/TARGETS. Every existing row shifts
down by one, which leaves their relative deltas, and therefore the
navigation keys emitted between them, unchanged. Mirror the three
new ids into AGY_MODEL_LABELS so the session pickers offer them too.
|
||
|
|
e5a8212f4a | feat(session): validate agents and browse workspace directories | ||
|
|
be1ef2a2e4 |
feat(dsh): integrate DeepSeek Harness through ACP (#1632)
* feat(dsh): add DeepSeek Harness ACP flavor * fix(dsh): update mobile flavor catalogs * fix(dsh): keep mobile spawn policy managed * fix(dsh): keep managed policy and prompt retry * fix(dsh): suppress unsupported runner policy flags * fix(dsh): align native managed-policy UX |
||
|
|
fdf4fdaa38 |
feat(web,hub): make large conversation export warning-only (#1648)
* feat(export): warn before large session downloads * fix(export): surface server size errors |
||
|
|
0aebf39c78 |
fix(opencode): keep one stored message per reasoning stream (#1643)
* fix(acp): carry the live reasoning marker on the wire payload ACP agents stream thoughts a token at a time, so the handler coalesces them into a buffer and re-sends the whole buffer under a stable stream id every 250ms. The converter dropped the marker that says a payload is one of those throttled snapshots, leaving the hub unable to tell a replaceable snapshot from the settled message that closes the stream. Mirrors how the text variant already forwards streamSnapshot. * fix(hub): keep one stored message per reasoning stream OpenCode reasoning arrives as a series of growing snapshots sharing one stream id, and every snapshot was persisted as its own message. A 26h session reached 48,844 rows and 63MB, and because the web budgets a fixed number of messages, its 400-message window covered barely three minutes of conversation — scrolling up walked through duplicate snapshots instead of history. Retire a stream's earlier live snapshots once their replacement is stored. Sweeping only after the insert matters: the two statements are separate transactions, so clearing first would leave a window where a crash takes the whole stream. Only rows marked live are eligible and the replacement is spared, so a stream always keeps at least one row and the settled message that closes it is never removed. Live rendering is unchanged: the web still receives every snapshot and already folds them by stream id. * fix(web): spend the message window on conversation, not repeated snapshots The window budgets raw messages, but a reasoning stream renders as a single folded block no matter how many snapshots it arrived in. On sessions recorded before the hub started retiring them, those snapshots fill the window on their own: in one 26h session the newest 400 messages covered 202 seconds, so scrolling up paged through duplicates instead of history. Collapse each stream to its newest snapshot before trimming. Rendering is unchanged — the timeline already folds them by stream id — and rows without a stream id are never touched. * fix(ios,android): port reasoning-snapshot compaction to the native windows The window logic in HapiProtocol and :core:protocol is a one-to-one port of the web store, so collapsing superseded reasoning snapshots only on the web left the native windows budgeting raw snapshot rows. The hub stores one row per stream now, but a client that already holds the older snapshots still spends its window on them. Add the same stream-id reader and compaction to both ports, in the shape each already uses for agent-run rows, and pin the behaviour with a pagination fixture. Both fixture suites enumerate shared/fixtures/pagination from disk, so the ports cannot drift from the web again without CI saying so. |
||
|
|
0f7a3da68b |
feat(cursor): mid-turn Steer via concurrent ACP session/prompt (#888) (#1609)
* feat(shared): steer capability gates and live steered signal schemas - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession gate which agents can deliver queued messages into the active turn (pi, codex, cursor ACP; legacy stream-json cursor excluded) - AgentState.steeringActive, DecryptedMessage.steered and messages-consumed live signal (never persisted by the hub) * feat(cli): queue reservations and steered messages-consumed option - MessageQueue2 gains takeByLocalId/restoreReservation/ beginReservationDispatch/commitReservation so an async steer can reserve a queued row without racing the main loop's turn/start drain - emitMessagesConsumed accepts steered: true to mark mid-turn delivery * feat(codex): mid-turn steer via app-server turn/steer (#888) - CodexAppServerClient.steerTurn + TurnSteerParams/Response types - CodexRemoteLauncher registers the steer-queued-message RPC handler: reserves the queued row, validates it against the active turn (no control commands, matching mode hash), injects via turn/steer with an epoch guard that invalidates in-flight steers on abort/cleanup - steeringActive agent state tracks the active-turn window - hub syncEngine gate opens to codex; messages-consumed relays steered * feat(web): Steered badge and steer gating for codex sessions - HappyUserMessage shows a ↳ Steered badge fed by the live messages-consumed steered signal, preserved across server echoes and refetches (mergeMessages carries the optimistic marker) - SessionChat gates canSteer via isSteeringSupportedForSession instead of the pi-only check - clearStaleQueuedStatus normalizes a queued status on an invoked message - fix(web): drop duplicate showSessionSummaryInChat in markdown test (upstream typecheck breakage) * feat(acp): split request dispatch from completion and add soft steer - AcpStdioTransport.sendRequestWithDispatch separates stdin-accepted dispatch from the JSON-RPC response, keeping sendRequest behavior unchanged - AcpSdkBackend tracks concurrent session/prompt requests with an activePromptRequests counter (main prompt + soft steers); response completion stays pending until every concurrent prompt settles - beginSoftSteerPrompt kicks off a concurrent session/prompt (Cursor GUI Send semantics — no cancel, no handler swap) returning {dispatched, completed}; softSteerPrompt awaits the full response for direct callers * feat(cursor): mid-turn soft steer via concurrent session/prompt (#888) - CursorAcpRemoteLauncher registers the steer-queued-message RPC handler: reserves the queued row, rejects control commands and mode mismatches, then soft-injects via beginSoftSteerPrompt without canceling the in-flight turn - Acks the hub once stdin accepts the inject (not on turn completion) to stay inside the 30s RPC window; the launcher stays busy until the concurrent prompt settles so handlers are not swapped mid-inject - steeringActive agent state mirrors the active-turn window; abort and cleanup reset it and invalidate pending steers - Legacy stream-json Cursor sessions register a steer handler that reports unsupported * fix(codex,shared): address bot findings on steer gate and ambiguous turn/steer - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession advertise codex and pi only; cursor joins when its soft-steer handler lands (#1609) - turn/steer now splits dispatch (stdin accepted) from completion (turn finished): the hub RPC acks once dispatch succeeds — never on the concurrent turn's completion, which can exceed the 30s RPC window - queue row commits only after the turn settles; a rejected/aborted steer restores the row so the message still delivers via turn/start, and a dispatched steer is never restored (no duplicate delivery) - steer carries clientUserMessageId (echoed as userMessage.clientId) so ambiguous transport failures can reconcile the thread later - client tests cover dispatch/complete split and stdin-write failure * feat(shared): advertise cursor in the steer gate now that its handler lands Cursor ACP sessions pass the web and hub steer gates; legacy stream-json cursor sessions stay excluded. * fix(codex): reconcile dispatched steers before restoring; align error copy - A dispatched turn/steer whose completion fails (disconnect / protocol error) is now reconciled via thread/read by clientUserMessageId before the queued row is restored — the instruction is only re-delivered by turn/start when the thread never received it - Reconcile targets the pinned steer thread, not whichever turn is current when completion fails - syncEngine unsupported-flavor error now matches the capability gate (Pi and Codex only until the cursor handler lands) - launcher tests cover steer success (ack on dispatch), reconcile-accepted and reconcile-rejected outcomes * fix(codex): consume the row at dispatch; drop background reconcile - The hub RPC acks and the queue row is consumed as soon as stdin accepts turn/steer; completion is background-only logging. A dispatched steer is never restored, so the same localId cannot be re-delivered via turn/start after the caller was told the steer succeeded - Dispatch failure (stdin write error) still restores the row and reports failure - steer.completed rejection is always handled (no unhandled rejection on the dispatch-failure path) - tests updated: completion failure after dispatch keeps the row consumed; dispatch failure restores it * fix(cursor): consume the row at dispatch; keep waiters for prompt gating - The hub RPC acks and the queue row is consumed as soon as stdin accepts the concurrent session/prompt; completion is background-only. A dispatched steer is never restored (no duplicate via the next prompt) - softSteerWaiters are registered before awaiting dispatch so the main loop's finally cannot start the next prompt mid-inject; they still gate prompt handover on completion - tests updated: post-dispatch ACP rejection keeps the row consumed * fix(cursor,hub): never hang teardown on unresolved soft steer; align diagnostics - Prompt-finally waits for soft-steer completion only when not exiting; the outer finally no longer waits at all — cleanup() disconnects the ACP transport, which rejects pending requests and settles the waiters - syncEngine gate diagnostics and JSDoc name all supported flavors (Pi, Codex, Cursor ACP) - regression test: Switch with an unresolved soft-steer completion still reaches teardown * fix(codex): distinguish definite rejection from indeterminate completion - Transport-level failures (timeout, abort, disconnect, spawn, protocol) carry an indeterminate marker; explicit JSON-RPC error responses do not - After a dispatched steer, turn completion resolves → commit + consumed; a definite app-server rejection restores the row (instruction was never accepted, so turn/start cannot duplicate it); an indeterminate outcome leaves the row reserved so it can never be delivered twice - Completion handling registers before awaiting dispatch so the dispatch-failure path cannot leak an unhandled rejection - client/launcher tests cover explicit rejection (restore), indeterminate outcome (row stays reserved) and dispatch failure * fix(codex): reconcile indeterminate steers instead of a permanent reservation - After an indeterminate completion (disconnect/protocol), reconcile the thread by clientUserMessageId immediately: accepted → commit + consumed, provably rejected → restore, still unreadable → keep the reservation and retry from the main-loop top on later passes (post-reconnect) - A row never sits in dispatching forever: the hub cannot stamp it invoked while the instruction may never have been accepted - tests: indeterminate keeps reserved while thread unreadable; accepted reconciliation consumes; rejected path restores * fix(cursor): abort drops soft-steer waiters so the next prompt never blocks - Ordinary Abort (shouldExit false) now clears softSteerWaiters: the prompt finally cannot wait forever on a soft steer whose completion is unbounded; the ACP cancel rejects in-flight requests, and cleanup() settles leftovers on session end - regression test: unresolved soft-steer completion after Abort no longer blocks the next prompt * fix(codex): accept all thread item shapes; retry reconcile; ack through abort - Reconcile matcher accepts userMessage/user_message with clientId/ client_id, matching the shapes the thread parser supports — an accepted steer can no longer be misclassified as rejected - A pending reconciliation schedules a wakeLoop retry, so a temporary app-server outage cannot strand the reservation behind waitForTurnOrRecovery - The success-path ACK no longer checks the steer epoch: the hub already reported steered on dispatch, so commit + messages-consumed must reach it even when an abort resets the queue in between * fix(cursor,acp): abort force-settles soft-steer bookkeeping - AcpSdkBackend.abortSoftSteers() drops the concurrent-prompt counter and notifies response-complete so the next turn's waitForResponseComplete() cannot block on a soft steer that will never settle after abort - handleAbort calls it before clearing the waiters; the main prompt's own finishPromptRequest stays guarded by Math.max(0, ...) - unit tests cover counter release and no-op when idle * fix(codex): reinit reconnected app-server; keep reconcile retries alive - thread/read after a disconnect auto-connects a fresh app-server, which must be initialized before any request — reconcile now ensures connect + initialize (isConnected getter added to the client) - every still-unknown loop-top reconciliation schedules the next retry, so recovery without external traffic is eventually observed - launcher mock gains isConnected * test(acp): match finishPromptRequest epoch signature in whitebox test * fix(codex): timer-driven reconciliation; init tracking; abort-safe ACK - Reconciliation runs on a self-rescheduling 1s timer independent of the main loop (wakes it too), so idle loops and waitForTurnOrRecovery still observe app-server recovery; abort clears nothing implicitly — the ACK path commits and consumes even when the reservation was cancelled - Absence of a durable client id is ambiguous: unmatched reads stay 'unknown' and keep retrying instead of restoring the row - CodexAppServerClient tracks initialized state (reset on disconnect/exit) so ensureAppServerInitialized re-initializes a fresh process before thread/read; initialize failures leave the flag false for the next retry - tests: accepted reconciliation via scheduled timer, indeterminate keeps reserved, explicit rejection restores * fix(codex): bind reconciliation to the launcher lifecycle - runSteerReconciliation clears any armed retry timer on entry and never installs a second one, so loop-top and timer-driven passes cannot multiply - shuttingDown is set when the main loop ends: timers are cleared and the pending map is dropped, so an unresolved steer can never respawn an app-server after cleanup (remote-to-local switch included) * fix(cursor): abort releases an in-progress soft-steer wait - The prompt-finally wait races Promise.allSettled against the abort signal: an Abort that clears the waiters now also releases a wait that already started, so the launcher always reaches the next queued prompt * fix(codex): report steered only after app-server acceptance - The handler now awaits steer.completed (the inject-acceptance response): an explicit JSON-RPC rejection surfaces as failed and restores the row for the normal turn/start path instead of a false steered - Transport failure after dispatch reports 'Steer outcome is being reconciled' and keeps the row reserved while the timer-driven thread reconciliation runs - dispatch-failure path also swallows the paired completion rejection * fix(cursor,acp): commit on ACP acceptance; distinguish transport failures - AcpStdioTransport marks transport-level failures (timeout, closed, stdin write) as indeterminate; explicit JSON-RPC error responses are not - The steer handler commits + consumes on completion (ACP acceptance) and restores the row on an explicit rejection; an indeterminate transport failure keeps the row reserved so a delivered instruction is never re-sent, and the ACK reaches the hub even when abort reset the queue - launcher/transport tests updated for the three outcomes * fix(steer): tri-state cancel, clear-safe reservations, bounded acceptance wait - MessageQueue2.cancelByLocalId returns 'in-flight' for a dispatching steer reservation: the hub neither deletes the row nor stamps invoked_at (new CancelMessageResponse 'busy' status; web restores the optimistic row); pushIsolateAndClear and reset/close share cancelReservations so /clear-style commands cannot have a rejected steer resurrect a discarded prompt - turn/steer acceptance wait bounded at 25s (< hub 30s RPC timeout): a lost response is indeterminate and funnels into thread reconciliation instead of stranding the reservation - tests updated for the tri-state cancel contract * test(cursor): match tri-state cancel contract for dispatching steers * fix(cursor): drop duplicate promptInFlight declaration after upstream merge * fix(codex,web): busy-aware edit flow; bound reconciliation reads - QueuedMessagesBar edit flow treats a 'busy' cancel as unsuccessful: it never prefills the composer when the row is inside an async steer, so a second client cannot send a duplicate - reconcileSteerByClientId bounds thread/read with a 5s timeout so a connected-but-silent app-server cannot hold the reservation in-flight indefinitely * fix(steer): inFlight-dominated cancel acks; bounded reconciliation - hub cancel-queued-message acks check inFlight before removed: a stale duplicate socket reporting removed can no longer delete the durable row while another socket is dispatching the steer - reconciliation entries expire after 60s and mark delivered: after the rejection window, a dispatched steer that the app-server never proved (client ids dropped on restart) is committed instead of polling thread/read forever - pre-dispatch failures (abort before write included) never enter reconciliation — they restore the row and report failure * fix(cursor,acp,web): indeterminate close marks, steer gating precision - AcpStdioTransport.rejectAllPending marks close/protocol failures indeterminate, so an accepted-but-close-interrupted soft steer restores nothing (no duplicate delivery) - the abort race in the soft-steer wait removes its listener in finally (no accumulation across repeated waits) - SessionChat gates the Steer button on agentState.steeringActive for codex/cursor instead of the queued-grace thinking flag, so Steer is not exposed before the launcher can accept it - codex pre-dispatch abort never enters reconciliation (merged from #1606) * fix(steer): persist indeterminate outcomes without replay * fix(cursor): hold ambiguous steers for explicit resolution * fix(steer): make ambiguous delivery restart-safe * fix(cursor): make ambiguous delivery restart-safe * fix(steer): recover crash-held rows and preserve retry dedup * fix(steer): ack retries and bound stdin dispatch * fix(cursor): reject steers when prompt generation changes * fix(steer): reconcile indeterminate dispatches and serialize retries * fix(cursor): preserve soft-steer reservations across abort * fix(codex): classify stdin callback failures as indeterminate * fix(steer): recheck indeterminate cancels after ACK * fix(steer): close retry and abort races * fix(cursor): hold ambiguous dispatch failures * fix(steer): serialize live retries and abort admission * fix(steer): distinguish live dispatching from unknown * fix(cursor): bound ACP dispatch acknowledgements * fix(steer): keep ACK failures held and reconcile busy cancel * fix(cursor): preserve state when dispatch ACK is uncertain * fix(steer): distinguish held cancel from removal * fix(store): combine schema v24 migrations * fix(cursor): distinguish held cancel from removal * fix(store): reserve schema v25 for steer delivery state * fix(cursor): suppress late ACP updates after abort * fix(steer): keep held cancel state and notify requeue * fix(cursor): isolate late updates after abort * fix(steer): release explicitly cancelled unknown reservations * test(cursor): cover explicit held cancellation * fix(codex): reject cancelled reservations before native steer * fix(cursor): reject cancelled reservations before ACP steer * fix(codex): make reservation restore atomic with state * fix(cursor): make reservation restore atomic with state * fix(codex): terminate abandoned transport writes * fix(cursor): hard-stop abandoned writes and update native queue state * fix(steer): own abandoned app-server lifecycle and consume races * fix(cursor): isolate aborts and add native retry resolution * fix(codex): confirm dispatch and recover abandoned turns * test(codex): mock abandoned transport callback * fix(native): reconcile retry responses * fix(codex): clear visible turn state on transport loss * fix(steer): claim retries and cover native delivery state * fix(native): resync busy cancel outcomes * fix(native): preserve indeterminate state on Android hydration * fix(steer): make retry claims single-winner * fix(cursor): hold restore failures for explicit resolution * fix(steer): serialize concurrent retry claims * fix(socket): tolerate missing steer-state ACK callbacks * fix(native): serialize retry operations * docs(web): document unknown steer delivery and retry controls * fix(steer): handle retry failures and abort-before-connect * fix(cursor): drain foreground prompt after soft-steer abort * fix(steer): reinitialize after transport loss and finish iOS retry errors * fix(steer): preserve indeterminate rows across reconnect gaps * test(web): mock indeterminate queued recovery state * fix(steer): recover consumed ACK tombstones * fix(steer): expose consumed cancel tombstones * fix(cursor): drain soft steers before handler replacement * fix(cursor): preserve buffered output on abort |
||
|
|
1f5602ad6a | Release version 0.29.0 | ||
|
|
f0e5ba9c0f |
feat(codex): mid-turn Steer via app-server turn/steer (#888) (#1606)
* feat(shared): steer capability gates and live steered signal schemas - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession gate which agents can deliver queued messages into the active turn (pi, codex, cursor ACP; legacy stream-json cursor excluded) - AgentState.steeringActive, DecryptedMessage.steered and messages-consumed live signal (never persisted by the hub) * feat(cli): queue reservations and steered messages-consumed option - MessageQueue2 gains takeByLocalId/restoreReservation/ beginReservationDispatch/commitReservation so an async steer can reserve a queued row without racing the main loop's turn/start drain - emitMessagesConsumed accepts steered: true to mark mid-turn delivery * feat(codex): mid-turn steer via app-server turn/steer (#888) - CodexAppServerClient.steerTurn + TurnSteerParams/Response types - CodexRemoteLauncher registers the steer-queued-message RPC handler: reserves the queued row, validates it against the active turn (no control commands, matching mode hash), injects via turn/steer with an epoch guard that invalidates in-flight steers on abort/cleanup - steeringActive agent state tracks the active-turn window - hub syncEngine gate opens to codex; messages-consumed relays steered * feat(web): Steered badge and steer gating for codex sessions - HappyUserMessage shows a ↳ Steered badge fed by the live messages-consumed steered signal, preserved across server echoes and refetches (mergeMessages carries the optimistic marker) - SessionChat gates canSteer via isSteeringSupportedForSession instead of the pi-only check - clearStaleQueuedStatus normalizes a queued status on an invoked message - fix(web): drop duplicate showSessionSummaryInChat in markdown test (upstream typecheck breakage) * fix(codex,shared): address bot findings on steer gate and ambiguous turn/steer - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession advertise codex and pi only; cursor joins when its soft-steer handler lands (#1609) - turn/steer now splits dispatch (stdin accepted) from completion (turn finished): the hub RPC acks once dispatch succeeds — never on the concurrent turn's completion, which can exceed the 30s RPC window - queue row commits only after the turn settles; a rejected/aborted steer restores the row so the message still delivers via turn/start, and a dispatched steer is never restored (no duplicate delivery) - steer carries clientUserMessageId (echoed as userMessage.clientId) so ambiguous transport failures can reconcile the thread later - client tests cover dispatch/complete split and stdin-write failure * fix(codex): reconcile dispatched steers before restoring; align error copy - A dispatched turn/steer whose completion fails (disconnect / protocol error) is now reconciled via thread/read by clientUserMessageId before the queued row is restored — the instruction is only re-delivered by turn/start when the thread never received it - Reconcile targets the pinned steer thread, not whichever turn is current when completion fails - syncEngine unsupported-flavor error now matches the capability gate (Pi and Codex only until the cursor handler lands) - launcher tests cover steer success (ack on dispatch), reconcile-accepted and reconcile-rejected outcomes * fix(codex): consume the row at dispatch; drop background reconcile - The hub RPC acks and the queue row is consumed as soon as stdin accepts turn/steer; completion is background-only logging. A dispatched steer is never restored, so the same localId cannot be re-delivered via turn/start after the caller was told the steer succeeded - Dispatch failure (stdin write error) still restores the row and reports failure - steer.completed rejection is always handled (no unhandled rejection on the dispatch-failure path) - tests updated: completion failure after dispatch keeps the row consumed; dispatch failure restores it * fix(codex): distinguish definite rejection from indeterminate completion - Transport-level failures (timeout, abort, disconnect, spawn, protocol) carry an indeterminate marker; explicit JSON-RPC error responses do not - After a dispatched steer, turn completion resolves → commit + consumed; a definite app-server rejection restores the row (instruction was never accepted, so turn/start cannot duplicate it); an indeterminate outcome leaves the row reserved so it can never be delivered twice - Completion handling registers before awaiting dispatch so the dispatch-failure path cannot leak an unhandled rejection - client/launcher tests cover explicit rejection (restore), indeterminate outcome (row stays reserved) and dispatch failure * fix(codex): reconcile indeterminate steers instead of a permanent reservation - After an indeterminate completion (disconnect/protocol), reconcile the thread by clientUserMessageId immediately: accepted → commit + consumed, provably rejected → restore, still unreadable → keep the reservation and retry from the main-loop top on later passes (post-reconnect) - A row never sits in dispatching forever: the hub cannot stamp it invoked while the instruction may never have been accepted - tests: indeterminate keeps reserved while thread unreadable; accepted reconciliation consumes; rejected path restores * fix(codex): accept all thread item shapes; retry reconcile; ack through abort - Reconcile matcher accepts userMessage/user_message with clientId/ client_id, matching the shapes the thread parser supports — an accepted steer can no longer be misclassified as rejected - A pending reconciliation schedules a wakeLoop retry, so a temporary app-server outage cannot strand the reservation behind waitForTurnOrRecovery - The success-path ACK no longer checks the steer epoch: the hub already reported steered on dispatch, so commit + messages-consumed must reach it even when an abort resets the queue in between * fix(codex): reinit reconnected app-server; keep reconcile retries alive - thread/read after a disconnect auto-connects a fresh app-server, which must be initialized before any request — reconcile now ensures connect + initialize (isConnected getter added to the client) - every still-unknown loop-top reconciliation schedules the next retry, so recovery without external traffic is eventually observed - launcher mock gains isConnected * fix(codex): timer-driven reconciliation; init tracking; abort-safe ACK - Reconciliation runs on a self-rescheduling 1s timer independent of the main loop (wakes it too), so idle loops and waitForTurnOrRecovery still observe app-server recovery; abort clears nothing implicitly — the ACK path commits and consumes even when the reservation was cancelled - Absence of a durable client id is ambiguous: unmatched reads stay 'unknown' and keep retrying instead of restoring the row - CodexAppServerClient tracks initialized state (reset on disconnect/exit) so ensureAppServerInitialized re-initializes a fresh process before thread/read; initialize failures leave the flag false for the next retry - tests: accepted reconciliation via scheduled timer, indeterminate keeps reserved, explicit rejection restores * fix(codex): bind reconciliation to the launcher lifecycle - runSteerReconciliation clears any armed retry timer on entry and never installs a second one, so loop-top and timer-driven passes cannot multiply - shuttingDown is set when the main loop ends: timers are cleared and the pending map is dropped, so an unresolved steer can never respawn an app-server after cleanup (remote-to-local switch included) * fix(codex): report steered only after app-server acceptance - The handler now awaits steer.completed (the inject-acceptance response): an explicit JSON-RPC rejection surfaces as failed and restores the row for the normal turn/start path instead of a false steered - Transport failure after dispatch reports 'Steer outcome is being reconciled' and keeps the row reserved while the timer-driven thread reconciliation runs - dispatch-failure path also swallows the paired completion rejection * fix(steer): tri-state cancel, clear-safe reservations, bounded acceptance wait - MessageQueue2.cancelByLocalId returns 'in-flight' for a dispatching steer reservation: the hub neither deletes the row nor stamps invoked_at (new CancelMessageResponse 'busy' status; web restores the optimistic row); pushIsolateAndClear and reset/close share cancelReservations so /clear-style commands cannot have a rejected steer resurrect a discarded prompt - turn/steer acceptance wait bounded at 25s (< hub 30s RPC timeout): a lost response is indeterminate and funnels into thread reconciliation instead of stranding the reservation - tests updated for the tri-state cancel contract * fix(codex,web): busy-aware edit flow; bound reconciliation reads - QueuedMessagesBar edit flow treats a 'busy' cancel as unsuccessful: it never prefills the composer when the row is inside an async steer, so a second client cannot send a duplicate - reconcileSteerByClientId bounds thread/read with a 5s timeout so a connected-but-silent app-server cannot hold the reservation in-flight indefinitely * fix(steer): inFlight-dominated cancel acks; bounded reconciliation - hub cancel-queued-message acks check inFlight before removed: a stale duplicate socket reporting removed can no longer delete the durable row while another socket is dispatching the steer - reconciliation entries expire after 60s and mark delivered: after the rejection window, a dispatched steer that the app-server never proved (client ids dropped on restart) is committed instead of polling thread/read forever - pre-dispatch failures (abort before write included) never enter reconciliation — they restore the row and report failure * fix(steer): persist indeterminate outcomes without replay * fix(steer): make ambiguous delivery restart-safe * fix(steer): recover crash-held rows and preserve retry dedup * fix(steer): ack retries and bound stdin dispatch * fix(steer): reconcile indeterminate dispatches and serialize retries * fix(codex): classify stdin callback failures as indeterminate * fix(steer): recheck indeterminate cancels after ACK * fix(steer): close retry and abort races * fix(steer): serialize live retries and abort admission * fix(steer): distinguish live dispatching from unknown * fix(steer): keep ACK failures held and reconcile busy cancel * fix(steer): distinguish held cancel from removal * fix(store): combine schema v24 migrations * fix(store): reserve schema v25 for steer delivery state * fix(steer): keep held cancel state and notify requeue * fix(steer): release explicitly cancelled unknown reservations * fix(codex): reject cancelled reservations before native steer * fix(codex): make reservation restore atomic with state * fix(codex): terminate abandoned transport writes * fix(steer): own abandoned app-server lifecycle and consume races * fix(codex): confirm dispatch and recover abandoned turns * test(codex): mock abandoned transport callback * fix(codex): clear visible turn state on transport loss * fix(steer): claim retries and cover native delivery state * fix(native): preserve indeterminate state on Android hydration * fix(steer): make retry claims single-winner * fix(steer): serialize concurrent retry claims * fix(socket): tolerate missing steer-state ACK callbacks * fix(native): serialize retry operations * docs(web): document unknown steer delivery and retry controls * fix(steer): handle retry failures and abort-before-connect * fix(steer): reinitialize after transport loss and finish iOS retry errors * fix(steer): preserve indeterminate rows across reconnect gaps * test(web): mock indeterminate queued recovery state * fix(steer): recover consumed ACK tombstones * fix(steer): expose consumed cancel tombstones |
||
|
|
1ad78f409a | feat(hub): iOS push channel — direct APNs + relay with E2E envelope (P1) | ||
|
|
f110845be6 |
merge: K6 fixtures batch 2 (events, permissions, sidechains, flavors, catalogs)
# Conflicts: # shared/fixtures/README.md # web/scripts/fixtures/generate.ts |
||
|
|
e1bfdd6265 |
feat(fixtures): SSE patch + pagination scenario fixtures (K7)
Two new golden-fixture suites, expectations machine-generated from the web
implementation (same source-of-truth principle as the chat suite):
- shared/fixtures/sse/ (12 cases): session-updated versioned-patch
application via applySessionDetailPatch — strict version gates for
metadata/agentState/todos/teamState, out-of-order arrival, teamState:null
clear, max-monotonic updatedAt, flat last-write-wins fields, sub-minute
activeAt keep-alive drop, scratchlistUpdatedAt trigger, and the pinned
actual behavior that activeTurnStartedAt is NOT applied by the patch path.
Inputs are stored schema-normalized and validated against SessionSchema /
strict SessionPatchSchema at generation time; per-patch applied/unchanged
verdicts are part of the contract.
- shared/fixtures/pagination/ (11 cases): op scripts driving the real
message-window store with a scripted ApiClient — latest page + SSE ingest,
before-cursor older pages, epoch-mismatch reset (window discard + recorded
internal latest request), reset:true replace preserving optimistic rows,
localId echo reconciliation, messages-consumed invokedAt stamping (no
cursor advance), message-cancelled removal, cancel-too-late invoked-row
ingest, hidden-row cursor advance, 400-row trim preserving queued rows,
and queued-state gap recovery. Documents pin the exact requests the store
issued, older-load outcomes, reconcile candidates, and a minimal window
projection (ids/order, queued/optimistic flags, hasMore, epoch, viewMode,
compound cursors).
Generator: web/scripts/fixtures/{sse,pagination}/ extend the K5 framework
(canonical serialization reused; suites pruned of stale files). Web
self-conformance tests replay every fixture against the real implementation
in bun run test:web. README documents schemas, projections, replay
contracts, and native consumption for both suites.
No time injection needed: the store's only Date.now() reads gate
notification throttling, which never reaches persisted or projected state.
|
||
|
|
c1ecb4798e |
feat(fixtures): batch 2 — events, permissions, sidechains, flavors, catalogs (K6)
34 new golden chat fixtures generated from the web pipeline, plus a machine-generated modes catalog: - Claude output: away_summary/microcompact_boundary/compact_boundary system subtypes, <task-notification> summary extraction, multi-block assistant message with an array-of-parts (text+image) tool_result - AgentEvent union completed: switch/message/error/title-changed(+dedupe)/ limit-warning(pipe text)/api-error folding/turn-duration/compact-summary/ abort-restore/unknown-type passthrough; thread-goal updated+cleared (silent) - Permission lifecycle via agentState.completedRequests: approved_for_session (mode+allowTools), denied with reason, canceled, AskUserQuestion flat answers, request_user_input nested answers (all with explicit createdAt) - Sidechain: Task tool_use with parentToolUseId-grouped subagent children - codex family: error, context_compacted, generated-image, review JSON, thread-goal messages, CodexBash call/result pair, exploration tool group - cli-output: cli-origin command block + stdout merge - agy flavor (output envelope): agy_message, agy_tool_action run_command mapping; cursor ACP plan snapshot (copilot skipped: identical codex shapes) - generator now also emits shared/fixtures/catalogs/modes.json from shared/src/modes.ts (per-flavor permission modes with labels/tones); self-conformance test covers it; README documents the catalogs contract |
||
|
|
9479f667b5 | feat(fixtures): golden chat fixtures generated from web pipeline (K4+K5) | ||
|
|
12847ddd0f | Release version 0.28.0 | ||
|
|
a6feb6e8ba |
feat: unified agent configuration descriptors (session config consolidation) (#1469)
* feat(config): add agent config descriptor protocol and advertise via runner capability Introduce shared agent configuration descriptors covering model, effort, permission, and secondary settings per agent flavor, plus the canonical HAPI YOLO -> native permission mode mapping. Runners advertise the builtin descriptors through the runner-state capability so hubs and web can render configuration without hardcoded flavor branches. Migrate the OpenCode create-session model picker from a bespoke radio list to the shared SelectControl combobox. * feat(web): render create-session permission from agent config descriptor Replace the flavor-branched Grok/Codex-family/YOLO permission block with a descriptor-driven PermissionField. Pi now reports permission as managed instead of silently ignoring the YOLO toggle, and YOLO-only flavors show the native permission mode the preference maps to. Removes the superseded GrokPermissionModeSelector and CodexFamilyPermissionModeSelector components. * ci: retry flaky claudeRemote 5s-timeout failure * fix(web): persist explicit OpenCode Default selection instead of restoring a concrete model The parent initialization effect treated every null selected model as 'uninitialized' and auto-picked a concrete advertised model, clobbering the user's explicit Default choice (and a restored Default preference). null now means explicit Default and is preserved; only undefined (no choice made yet) triggers probe-based initialization. Add parent-level regression tests for Default persistence and remembered-model restore. * fix(web): accept undefined selected model in OpencodeModelSelector props * feat(web+cli+hub): unify create-session model/effort fields and add Pi model/effort support Pi's agent config descriptor now advertises model (machine) and effort (static thinking levels) for create AND session availability: - cli: ListPiModelsForMachine RPC runs 'pi --list-models' (cached, inflight deduped) and parses the provider/model table; startup model match accepts provider-qualified ids - hub: GET /api/machines/:id/pi-models route + rpcGateway/syncEngine passthrough - web: NewSession renders Pi models grouped by provider through the generic ModelSelector and a new descriptor-driven EffortField (replaces the per-flavor LaunchEffortSelector/ReasoningEffortSelector pair); launch payload forwards Pi model + thinking-level effort (runner already supported --model/ --effort for pi) * fix(web): render Pi provider groups in ModelSelector and scope Grok availability warning - ModelSelector now renders grouped options as <optgroup> (Pi models are provider-grouped; identical modelIds from different providers stay distinct) - PermissionField only receives autoPermissionModeSupported for Grok — a cached Grok probe result no longer leaks the Grok warning onto other agents Addresses HAPI Bot Minor findings on #1469. * fix(web): drop Object.groupBy from ModelSelector; revalidate restored Pi models against the catalog - ModelSelector buckets options with a reduce instead of Object.groupBy (Safari < 17.4 has no polyfill — New Session would throw on those clients) - Pi restored model/effort are cleared when the value is absent from the live machine catalog, and Create waits for the catalog while a non-default Pi choice is being validated (mirrors Codex/Grok/Copilot handling) Addresses HAPI Bot findings on #1469. * fix(pi+web): serialize startup model before thinking level; hide Pi launch controls during history import - PiSession gains startupModelSettled; the startup set_thinking_level waits for the requested model's set_model attempt to settle first, so a level the default model rejects is not lost before the requested model is confirmed (set_model and set_thinking_level were already serialized by the runtime mutation lock; this pins the model-first ordering) - Create Session hides Pi model/effort controls while a Pi history import is selected — the import reopens the native session as-is and would silently ignore launch-only model/effort values Addresses HAPI Bot findings on #1469. * fix(pi): settle startup-model gate when model discovery fails or returns no models A failed or empty get_available_models response would leave the startupModelSettled gate unresolved, stranding a requested startup effort indefinitely. Resolve the gate on the error path and the empty-models path; adds regression tests for both. |
||
|
|
7909c46fff |
feat(search): support wildcard patterns across search fields (#1571)
* feat(search): add wildcard matching to search fields * fix(search): harden wildcard matching and file globs * fix(search): align file matching with shared wildcard semantics * fix(search): bound file wildcard search in runner * fix(search): normalize outline queries through shared matcher * fix(web): remove duplicate markdown test context field |
||
|
|
5c81ed69e2 |
fix(runner) preserve native instructions (#1557)
* fix(codex): preserve native instructions Keep HAPI guidance in the developer layer so app-server retains Codex persistence rules. Align long-running goal states with the Codex 0.147 protocol. * fix(codex): filter new goal statuses |
||
|
|
901f17d0ca |
feat(pi): support Pi slash commands from HAPI web (compact/session/model/help) (#1570)
* feat(pi): support Pi slash commands from HAPI web (compact/session/model/help)
Pi runs as 'pi --mode rpc' over piped stdio, so TUI slash commands typed in
web chat previously fell through to the LLM as plain text and silently did
nothing (notably /compact).
- shared: add Pi builtin slash command list (help/compact/session/model) so
the web / menu exposes them; web test updated to match
- cli: intercept Pi builtin commands in runPi's user-message path
* /compact [instructions] -> Pi compact RPC (120s timeout, works while
streaming; summary + token delta reported back as chat messages)
* /session -> get_session_stats formatted stats
* /model [modelId] -> list/switch via set_model
* /help -> supported-commands list
* other Pi TUI builtins (/tree, /export, /reload, ...) -> explicit
terminal-only notice instead of silent LLM pass-through
* unknown slash text still passes through (extension commands, skills,
templates keep working)
- gate the prompt pump with piCompactInFlight so queued prompts are not
rejected by Pi mid-compaction; buffer commands until ready like prompts
- ListSlashCommands RPC merges HAPI builtins with Pi extension commands
- tests: parser unit tests + runPi integration tests (compact execution,
streaming steer interception, failure reporting, FIFO blocking, model
switch, unsupported commands, slash list merge)
- docs: document Pi slash command support in docs/guide/agents.md
* fix(pi): address review findings on slash command lifecycle
- compact timeout: fail the session (indeterminate outcome, runtime lease
poisoned) instead of reopening the prompt FIFO into a possibly-compacting
Pi; pump only when cleanup has not been initiated
- special commands: release the cancellation reservation before executing so
a cancel landing mid-command is not acknowledged (hub would delete the
queued row while the command still runs)
- tests: drop the duplicated slash-command describe block; add focused tests
for compaction timeout with a queued prompt and cancellation during an
in-flight special command
* fix(pi): route slash commands through the prompt FIFO and reject ambiguous models
- slash commands now share the prompt FIFO with ordinary messages: a
/compact or /model typed after a queued prompt dispatches only after it
(and after the active turn settles), instead of jumping the queue from
the preparation chain
- the pump dispatches special entries out-of-band while piSpecialCommandInFlight
keeps the FIFO blocked; steer promotion refuses slash commands
- /model <id> prefers an exact provider/modelId match and reports bare IDs
shared by multiple providers as ambiguous instead of picking the first
- tests: FIFO ordering (queued prompt before /compact), steer-delivered
/compact queued until settle, ambiguous/qualified model selection
* fix(pi): keep /compact interruptible, honor extension precedence, require token boundary
- head-of-line /compact dispatches even while Pi is streaming (Pi's
compact() aborts the active generation itself); every other queued item
still waits for the stream to settle, preserving FIFO order
- discovered extension commands / prompt templates override same-name
builtins at message time, matching the slash-list merge precedence
- parsePiSpecialCommand requires a command-token boundary, so path-like
text such as /compact.md or /model/config stays an ordinary prompt
- tests: interrupt rule, extension collision, reserved-name path prefixes,
non-compact commands waiting for stream settle
* fix(pi): honor cancellation acknowledged during slash-command discovery
A cancel arriving while the chain awaits get_commands (cold cache) was
acknowledged via the preparing reservation but never re-checked, so a
canceled /compact could still execute. Re-check the cancellation marker
after discovery and drop the message before dispatch.
* fix(pi): qualify /model selectors and report failed slash RPCs once
- /model lists provider-qualified selectors (openai/gpt-5.2) so duplicate
bare IDs remain usable and copy-pasteable; current model is qualified too
- compact/set_model failures are owned by the awaited slash/config handlers:
the common response handler no longer emits the raw Pi error a second time
- tests: qualified listing with duplicate providers, single-message failure
reporting for rejected /compact and /model
* fix(pi): consume slash-command queue row at dispatch
Special commands (/compact, /session, /model, /help) are executed by HAPI
itself and never delivered to Pi as prompts. Consumption was deferred until
the command finished, so a /compact run — an LLM summarization pass that can
take minutes — left the row stuck in the web queued bar for its whole
duration, then surfaced as a sent message. Consume the row the moment
dispatch starts; failures still surface via the explicit event message.
* fix(pi): guard special-command dispatch against unexpected rejections
* ci: retry Codex PR Review after infra failure (proxy 503)
* fix(pi): keep session queued-thinking grace during /compact dispatch
The queued-thinking grace is session-scoped, so clearing it while
acknowledging a dispatch-time /compact row also drops the grace for any
prompt queued behind it. /compact keeps running for minutes without
toggling Pi thinking state, which would leave the web session looking idle
while compaction and the following prompt are still pending. Only the
fast, synchronous commands (/session, /model, /help) clear the grace.
* fix(pi): render compaction summary as a dedicated chat block
The manual /compact RPC result was reported as two plain message
events ("📦 Compaction completed (tokens: …)" + "📦 Compaction
summary: …"), which the web chat renders as tiny centered status
lines — unusable for a real summary payload. Emit a structured
compact-summary event instead (summary + token delta) and render
it as an independent block: header with the delta and the summary
markdown in a scrollable panel.
Also emit the same structured event when importing Pi session
files (compaction entries), and queue the event lossless like
other user-visible messages so a disconnect cannot drop it.
Verified: bun typecheck clean; bun run test exit 0 (cli 2481
passed, web 2451 passed, hub/shared clean); runPi/loop/apiSession/
piSessions/presentation suites green.
* fix(pi): address HAPI Bot findings on compact dispatch and import
- Track compaction as thinking for its whole duration: /compact runs for
minutes without a Pi streaming event, so the 15s queued-thinking grace
alone left the web session looking idle while compaction and any queued
prompts were still pending (updateThinkingState around the compact RPC).
- Imported Pi compaction summaries must use the event envelope
(content.type: 'event') like the live wrapper's compact RPC result; the
codex payload envelope is dropped by the web normalizer. Extend
CodexImportedMessageSchema with the event variant.
* fix(pi): /model retries discovery when the model cache is empty
Startup model discovery can be late or fail once; using only the cached
catalog made /model report valid models as unknown. getPiModels() falls
back to the get_available_models RPC on an empty cache, used for both
listing and switching.
* fix(pi): interrupt in-flight /compact on Abort; surface startup model rejection
- The Abort action no longer waits on the runtime-mutation lease when a
manual /compact is in flight (compaction can hold it for up to 120s,
blowing the 25s abort deadline and failing closed). It sends the abort
RPC directly so Pi cancels its compaction AbortController; the compact
RPC's 'Compaction cancelled' error is not double-reported as a failure
since Pi already emits the compaction_end(aborted) lifecycle event.
- A rejected detached startup set_model now emits a visible ⚠️ event into
chat instead of only a debug log, restoring the pre-existing behavior.
* fix(pi): close the Abort race when /compact is queued on the mutation lock
Abort previously assumed an in-flight /compact always had its RPC issued;
the command is marked active at queue dispatch, but the compact RPC is sent
only after the runtime-mutation lock is acquired. An Abort landing in that
gap acknowledged success while the compact RPC still ran afterwards.
Track the compact's rpcStarted/cancelled state: Abort cancels a not-yet-
started compact in place (the queued callback skips it), and interrupts a
started one via the abort RPC as before.
* fix(pi): persist provider-qualified selection after /model switch
The success path updated currentModel/currentProvider and keepalive with a
bare model ID, leaving metadata.piSelectedModel on the previous provider.
The web picker prefers that metadata for selection, context-window
resolution, and effort options, so a switch like openai/gpt-5.2 ->
azure/gpt-5.2 was invisible. Persist piSelectedModel with the full
provider/modelId pair on every confirmed switch.
* fix(pi): retire pending extension UI requests when /compact interrupts a turn
The streaming-interrupt path sent the compact RPC without cancelling
pending extension UI requests first, unlike the Abort path. Editor
requests have no timeout, so the web could stay stuck on a stale
input/permission card and a later answer could be routed to the aborted
turn. Cancel all pending requests (with a response) before compacting.
* fix(pi): fail closed when the direct compact-abort RPC times out
The in-flight /compact abort branch awaited the abort RPC without the
ordinary Abort path's timeout handling: an unanswered abort left the
compaction outcome indeterminate (the compact RPC keeps the mutation
lease for up to 120s) while the wrapper still looked live. Fail the
session on PiRpcTimeoutError, mirroring the standard abort fail-closed
path.
---------
Co-authored-by: swear01 <swear01@users.noreply.github.com>
|
||
|
|
ad72229923 |
feat(sessions): add on-demand AI title suggestions (#1577)
* feat(sessions): add on-demand AI title suggestions * fix(sessions): address title suggestion review feedback * fix(web): ignore stale title generation results |
||
|
|
df1a56e1db |
fix(cursor): exclusive agent spawn lease for list-models vs ACP (#1529)
* fix(cursor): exclusive agent spawn lease for list-models vs ACP (#1520) Add a proper-lockfile spawn lease beside agent-acp-active so model probes and ACP transport acquire mutual exclusion atomically before spawning agent children, closing the post-#1518 check-then-act overlap window. Fixes #1520 Co-authored-by: Cursor <cursoragent@cursor.com> * chore(web): fix markdown-a test HappyChatContext mock for typecheck Adds showSessionSummaryInChat to chatContext() so CI typecheck passes on the PR branch (pre-existing main breakage unrelated to #1520). Co-authored-by: Cursor <cursoragent@cursor.com> * Revert "chore(web): fix markdown-a test HappyChatContext mock for typecheck" This reverts commit 4973f321a31fda772e8990ea6cda20517ca906aa. * fix(cursor): scope spawn lease to agent spawn window only (#1520) Hold agent-cli.spawn only around spawn('agent') in AcpStdioTransport, not for the full ACP session. Restores N concurrent cursor sessions per host; list-models probe lease unchanged. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): tighten spawn lease lifecycle for babysit (#1520) Acquire spawn lease before ACP marker publish; unregister on spawn failure. Hold list-models probe lease until child exit on timeout. Add missing showSessionSummaryInChat to markdown-a test mock (unblocks CI typecheck). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): re-check ACP marker after spawn lease acquire (#1520) Close check-then-act window where ACP could publish its marker between the inactive guard read and list-models spawn. Add regression test. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cursor): fix ACP-after-acquire mock call order (#1520) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): async spawn lease + force-kill probe timeout (#1520) Add acquireAgentCliSpawnLease (setTimeout yields) for ACP create path; AcpStdioTransport.create() async factory. Probe timeout uses killProcessByChildProcess(force) while holding lease until child exit. Addresses Bugbot Majors: session-lifetime mutex (fe07b708d), probe lease release, post-acquire re-check (137baa779), sync loop starvation, timeout escalation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(acp): coalesce concurrent initialize() transport spawns (#1520) Await shared bootstrapTransport promise so overlapping initialize calls do not spawn duplicate ACP children while create() is in flight. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
fea42212ea | Release version 0.27.3 | ||
|
|
51ae260a3f |
feat(web): settings for AGENT_NOTIFY_SUMMARY chat display (default hide) (#1477)
* feat(web): render AGENT_NOTIFY_SUMMARY as compact metadata * fix(web): defer summary rendering until completion * fix(shared): preserve indentation when splitting summaries * fix(web): guard unknown summary statuses * feat(web): settings for AGENT_NOTIFY_SUMMARY chat display (default hide) Add hub setting sibling to emit (#1376): show compact NotifySummaryText when on; strip footer from chat/copy when off. Store/parse/FCM unchanged. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): address #1477 Majors for display setting Allow any namespace to GET hub-settings (PUT stays owner-only) so sessionSummaryInChat applies hub-wide. Reject whitespace-delimited AGENT_NOTIFY_SUMMARY examples so default-hide does not eat prose. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(shared): allow indented AGENT_NOTIFY_SUMMARY footers Keep rejecting whitespace-delimited prose examples, but accept a standalone footer whose only prefix is leading indentation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): poll hub display setting; hide empty notify footers Refetch sessionSummaryInChat so open clients pick up owner toggles. When display is on, recognized footers without status/summary/action still strip raw JSON instead of falling back to MarkdownText. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): poll notify display once in chat shell Move hub-settings refetchInterval off per-message hooks into HappyThread context. Strip well-formed footers while streaming when display is off. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web,hub): QueryClient for HappyThread tests; atomic hub-settings Wrap HappyThread mobile-scroll tests in QueryClientProvider after the chat-shell hub-settings poll. Read/write both hub setting flags in one settings.json snapshot under the shared lock. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Ananovo <78636812+techotaku39@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> |