mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
d3473f9ed42e8a1d64fc3d71433b0507d98306f9
1562
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d3473f9ed4 |
fix(cli): forward Claude API error records with null service_tier (#1857) (#2087)
Claude Code writes failed API calls ("Prompt is too long", usage limits,
logged out) as synthetic assistant records whose usage carries
`service_tier: null`. The schema only accepted undefined, so the local
session scanner dropped every one and the web session just looked stalled.
- Accept null for the usage fields the Messages API types as nullable.
- Keep the top-level `error`/`isApiErrorMessage` on assistant records in
local mode, and `error` in the SDK converter for remote mode.
- Render assistant records carrying `error` as the existing error event,
so the all-zero synthetic usage no longer resets the context gauge.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
6bf0a633bb |
fix(claude): seed local-mode context window from the configured model (#1858) (#2088)
* fix(claude): seed local-mode context window from the configured model (#1858) Local transcripts carry bare model ids and no usage.context_window, so local Opus/Sonnet [1m] sessions fell back to the web's 200k heuristic (190k denominator). At local launch, resolve the model Claude Code starts with (--model, ANTHROPIC_MODEL, project local/project/user settings) with its [1m] suffix intact, and stamp a 1M context_window on assistant messages from that model. Subagent models are left alone. Register PostModelSwitch in the local hook settings so /model switches inside the TUI update the seed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(claude): honor --model and --setting-sources when seeding the local context window (#1858) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6b3912dc56 |
fix(hub): judge liveness by hub receive time, not runner clock (#1986) (#2089)
machine-alive and session-alive set activeAt from the CLI-supplied timestamp, while expireInactive compares activeAt against the hub clock. A runner whose clock lags the hub by more than the timeout minus the heartbeat interval was expired seconds after every heartbeat, producing the periodic active:false/true flapping. Use the hub receive time for activeAt; client time still drives thinkingAt and runtime-config ordering. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
878ca2058c |
feat(codex): expose Approve for me as a permission mode (#2016) (#2090)
* feat(codex): expose Approve for me as a permission mode (#2016) Add autoReview to the Codex permission catalog. It keeps the workspace-write sandbox with on-request approvals and routes escalations to Codex's native reviewer (approvals_reviewer = "auto_review"), the same config as `hapi codex --approve-for-me`. It works at launch and mid-session via thread/settings/update; explicit modes now reset the reviewer to user, and sessions report autoReview from the native reviewer setting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(native): expect Auto-review in the Codex permission lists (#2016) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(modes): label autoReview as Auto, matching Claude's auto mode (#2016) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
60a4971087 |
fix(cli): forward Claude Code 2.x subagent transcripts in local mode (#1904) (#2091)
Claude Code 2.x writes each subagent to <session>/subagents/agent-*.jsonl instead of the main session log. The local session scanner only read the main log, so subagent activity of local Claude sessions never reached the hub and Agent/Task cards stayed empty. Scan the subagents directory of every tracked session and stamp parentToolUseId from the sibling agent-*.meta.json (toolUseId), so the web tracer groups the transcript under its Agent card. Subagent files already present at startup are marked seen, like the main log. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1488be153a |
feat(cli): add hapi set-mode to change a session's permission mode (#2094) (#2095)
* feat(cli): add hapi set-mode to change a session's permission mode (#2094) The CLI could only pick a permission mode at launch; changing it on an existing session required the web UI or a hand-rolled JWT + curl call. `hapi set-mode <session-id-or-prefix> <mode>` resolves the session like ping-peer / inspect-peer, validates the mode against the session flavor's catalog (printing the allowed list on mismatch), and calls the existing POST /api/sessions/:id/permission-mode route. Active sessions switch live; inactive ones keep the mode for the next resume. No MCP tool is added: raising a peer's mode stays behind the agent's normal shell-approval path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: reject Safe Yolo for shared sessions on the permission-mode route Shared (concurrentClients) Codex sessions cannot launch with safe-yolo. The web picker hides it, but the route only checked the flavor catalog, so `hapi set-mode` (or any API client) could store it on an inactive shared session and break its next resume. Guard the route and apply the same filter to the CLI's allowed-mode list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4233bf8759 |
fix(web): give the rich composer its own undo history (#2097)
* fix(web): give the rich composer its own undo history (#2085) Plain-text paste, selected-text deletion, cut, Enter, and mention or suggestion insertion rebuild the contenteditable DOM, which the browser's native undo stack cannot follow, so Ctrl+Z silently did nothing for them. The composer now records a snapshot (serialized value + selection) before every edit, groups consecutive native typing into one step, and handles Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, Ctrl+Y and beforeinput historyUndo/Redo itself. Undo restores the prior selection, including ranges. An external value change (send, draft restore) starts a fresh history. Fixes #2085 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): keep composer undo snapshots as segments Reparsing a serialized snapshot turned literal `[title](/sessions/<id>)` text into a mention chip (and normalized its title) on undo. Store the segments themselves so undo restores the exact draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8657c11754 |
fix(web): load one older page when the cold tail is tool-only (#2098)
* fix(web): load one older page when the cold tail is tool-only A cold session open fetches the latest 20 raw messages. When they reduce to only ordinary tool activity, the thread showed a single collapsed tool group until viewport-coverage loading ran after the 1.8s initial scroll settling window. Before committing the cold page, check its reduced/grouped blocks; when they are tool-only and older history exists, fetch one 200-message older page (bounded by a 500ms wait) and commit both together, keeping the existing skeleton meanwhile. Timeout, failure, or an epoch change falls back to the tail page. Cached re-entry and reset paths are unchanged. Fixes #2045 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): limit tool-only context fetch to cold windows and ignore queued prompts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e040d23371 |
fix(web): ignore pending requests on archived sessions in the sidebar (#2101)
Archived sessions keep their stale pending requests in agentState. The sidebar still treated them as live: the collapsed folder preview forced them visible and trimmed active sessions to make room, so a folder could look like it had no active sessions. They also kept the permission/input attention dot and the "Pending N" badge. Only count pending requests on active sessions for preview pinning, attention, and the badge. The state itself is unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4254942aeb |
fix: show Claude sessions waiting on background tasks (#2100) (#2102)
* fix: show Claude sessions waiting on background tasks (#2100) backgroundTaskCount was always 0 for Claude: the hub parsed the "Command running in background" tool result only from tool_result / assistant records (Claude sends it as a user record), and waited for a <task-notification> user record that current Claude Code never emits. Set the count from Claude's system/background_tasks_changed snapshot, which the CLI already forwards and which lists every running background task (empty once the last one finishes). In the session list, waiting on background work now shows a blue spinner instead of a static blue dot (too close to the "needs input" dot) or the green "running" pulse used for selected and pinned rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): forward background_tasks_changed snapshots to the hub OutgoingMessageQueue only sends chat-visible records, so Claude's background task snapshots never left the CLI. Forward that one hidden subtype, and have the hub apply it as session state without storing a hidden message row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): draw the background spinner with the thinking spinner code Both row spinners now render through one SessionSpinner; background work only switches the color to blue and no longer adds a separate Running label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cb2ff05c2a | fix(codex): distinguish compaction waits from unknown delivery (#2115) | ||
|
|
3eadf65f21 |
perf(hub): cached settings reads (SWR + single-flight) for hot paths (#2122)
Per-request settings reads hit the disk on every call: each flag helper re-reads the whole settings.json, so a 15-session reconnect fan-out on POST/GET /cli/sessions queued ~45 file reads (existsSync + readFile + JSON.parse) on the single hub event loop — during engine long turns those trivial probes measured 1-9s, and page-load bursts stretched the settings GET / voice-credential status endpoints to 2-4s as well. Add a settings read cache (createSettingsReadCache) with stale-while-revalidate semantics and swap the hot readers onto it: - fresh (within TTL, default 5s) -> memory hit, zero disk I/O - stale -> stale snapshot returned immediately plus ONE single-flight background revalidation (a probe storm costs at most one disk read) - cold -> await the shared in-flight read - writes from this process -> write-through refresh from the writeSettingsUnlocked funnel (PUT /api/hub-settings and every writeXxxEnabled helper), so readers see mutations instantly; a generation counter keeps a late-landing revalidation that raced a write (stale inode) from stomping the newer value - corrupt file (parse null) -> evict, next read surfaces the error loudly, matching pre-cache semantics updateSettings / writeSettings still read authoritatively under the lock; only read paths use the cache. External hand-edits of settings.json converge within the TTL. Swapped readers: readSessionSummaryContractEnabled, readSessionSummaryInChatEnabled, GET /api/hub-settings, getTranscriptionCredentialStatus. Tests: 6 unit cases on the factory (cold single-flight, fresh hit, stale serve + single revalidation, revalidation failure keeps snapshot, parse-null eviction, write-through beats late revalidation) plus a real-file integration test pinning write-through visibility and the memory-hit proof. Full hub suite green. via [HAPI](https://hapi.run) Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
431c170f4a |
feat(android): persistent permission-mode chip + inactive gating (#2125)
* feat(android): settled permission verdict details Port the web PermissionFooter verdict classification so a settled permission row explains HOW a request was decided, not just that it was. Behavior vs web (web/src/components/ToolCard/PermissionFooter.tsx:11-63): - codex family sessions (flavor family + Codex/Gemini/OpenCode/Copilot/ Cursor tool prefixes): approved + decision approved_for_session shows "Approved for session"; denied + decision abort shows "Aborted" with the wire reason appended when present. - claude sessions: mode acceptEdits shows "Approved: allow all edits" (previously the raw mode id was appended); decision approved_for_session or an allowedTools hit — plain tool name or the `Bash(command)` qualifier built from the tool input command/cmd — shows "Approved for session" (previously indistinguishable from a plain approval). - canceled now appends the wire reason (previously dropped). - resolved/pending/denied unchanged in shape; denied keeps the error tone and aborted shares it. Deliberate divergence: web's own settled footer renders only a minimal red reason box (PermissionFooter.tsx:207-220) — its formatPermissionSummary classification is unreachable there. Android renders the classified verdict line in the existing subdued row style instead, as intended. Implementation: new pure JVM classifier feature/chat/permissions/PermissionVerdict.kt (exhaustive status -> PermissionVerdictLabel mapping, unit-tested in PermissionVerdictTest); ToolCallBlockView.PermissionStateRow becomes a thin renderer over it. Strings added in en/zh-rCN/ru (ru mirrors en key order; zh values from web locales). Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * feat(android): persistent permission-mode chip + inactive gating Item 2 — persistent permission-mode chip above the composer, mirroring the web StatusBar mode span (StatusBar.tsx:253-265, 389-393): - New PermissionModeChip composable + PermissionModeChipRule pure object: resolve the raw wire mode, keep it only when the flavor offers it, and hide `default` everywhere except copilot (model=auto sessions still surface the chip). Unknown wire ids hide (no raw fallback). - Tone mapping follows the web PERMISSION_TONE_CLASSES (StatusBar.tsx: 43-48): neutral=onSurfaceVariant, info=primary, warning=tertiary, danger=error. Labels stay en catalog strings — web mode labels are hardcoded English, not i18n. - The chip consults the unfiltered flavor catalog: the concurrent-clients Safe-Yolo carve-out only narrows the switcher, never the chip. - Deliberate divergence: the web span is not clickable (its switcher is the composer settings sheet / Shift+Tab). Android has no Shift+Tab, so tapping the chip opens the session config sheet that hosts the switcher — same surface as the top-bar gear, no new top-bar icon (ChatScreen keeps its two-icons-max rule). Item 4 — permission-mode gating aligned with web (SessionChat.tsx: 2079, 2138-2163; HappyComposer.tsx:536-537, 1544-1549, 1960-1995): - Inactive sessions stay switchable, matching web: SessionChat passes allowSendWhenInactive unconditionally, so configurationControlsDisabled never trips for inactive threads and the offline notice stays informational only (the send path auto-resumes). - The permission section now disappears for copilot sessions under local terminal control — web withholds onPermissionModeChange there and the whole section then hides (HappyComposer.tsx:1595). Other flavors keep switching while terminal-controlled. - ChatViewModel.setPermissionMode gains the matching belt-and-suspenders guard; the controlledByUser derivation is extracted into terminalControlled() (controlledByUser && !concurrentClients, same as SessionChat.tsx:996) and shared with buildConfigUi. Tests: PermissionModeChipTest (6 cases — default/codex/copilot/pi/unknown/ safe-yolo), ChatViewModelInteractionTest +3 (withheld for terminal copilot; survives terminal claude + shared-clients copilot; inactive stays switchable). No new user-visible strings (chip label is catalog en, contentDescription reuses chat_config_permission_mode). Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * test(android): derive the dsh chip expectation from the flavor catalog The hardcoded "dsh offers no modes" assumption breaks on trees where dsh gained the codex-style mode list — derive the expectation from the catalog so the test pins the actual contract (the chip mirrors the flavor's offered list). Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
4c87a0f1a2 |
feat(android): settled permission verdict details (#2123)
Port the web PermissionFooter verdict classification so a settled permission row explains HOW a request was decided, not just that it was. Behavior vs web (web/src/components/ToolCard/PermissionFooter.tsx:11-63): - codex family sessions (flavor family + Codex/Gemini/OpenCode/Copilot/ Cursor tool prefixes): approved + decision approved_for_session shows "Approved for session"; denied + decision abort shows "Aborted" with the wire reason appended when present. - claude sessions: mode acceptEdits shows "Approved: allow all edits" (previously the raw mode id was appended); decision approved_for_session or an allowedTools hit — plain tool name or the `Bash(command)` qualifier built from the tool input command/cmd — shows "Approved for session" (previously indistinguishable from a plain approval). - canceled now appends the wire reason (previously dropped). - resolved/pending/denied unchanged in shape; denied keeps the error tone and aborted shares it. Deliberate divergence: web's own settled footer renders only a minimal red reason box (PermissionFooter.tsx:207-220) — its formatPermissionSummary classification is unreachable there. Android renders the classified verdict line in the existing subdued row style instead, as intended. Implementation: new pure JVM classifier feature/chat/permissions/PermissionVerdict.kt (exhaustive status -> PermissionVerdictLabel mapping, unit-tested in PermissionVerdictTest); ToolCallBlockView.PermissionStateRow becomes a thin renderer over it. Strings added in en/zh-rCN/ru (ru mirrors en key order; zh values from web locales). via [HAPI](https://hapi.run) Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
2c1361ba5d | fix(web): stop composer image previews from dragging after release (#2124) | ||
|
|
097a693348 |
fix(web): remove composer attachments without waiting for cleanup (#2128)
* fix(web): remove composer attachments without waiting for cleanup * test(web): run attachment removal regression in CI |
||
|
|
33a9a740e8 | feat(opencode): support OpenCode v2 alongside v1 sessions (#2042) | ||
|
|
5d8deb2011 |
fix(web): allow pasting non-image files into the composer (#2026)
Paste only accepted image/* files, silently dropping PDFs, archives, etc. copied from the file manager, while drag-and-drop and the attach button accept any file. Remove the image-only filter from both paste handlers so paste matches drag-and-drop. The scheduled-send guard is unchanged. via [HAPI](https://hapi.run) Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
6f155313ff | fix(codex): keep status summaries out of intermediate messages (#2041) | ||
|
|
f13a1aaeab |
fix(web): shed session toolbar icons by keep rank instead of overlapping (#2040)
* fix(web): stop session toolbar search/clear overlapping neighbors Collapsed search used min-w-0 and crushed under shrink-0 icons, so clear painted over filter/BlockedJumpPill on narrow widths. Keep search chrome shrink-0 and wrap the header actions group instead (#2039). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): size session toolbar actions to content so they wrap flex-1 min-w-0 kept the action group beside a long search chip in a 280px sidebar, so Browse/Settings overflowed onto the calendar. Use w-max shrink-0 and keep the router header-action cluster from shrinking (#2040). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep session toolbar icons in one slab when the row wraps max-w-full plus inner flex-wrap let the action group split, so some icons stayed beside search and the rest dropped underneath. The group is nowrap and content-sized, so a tight row moves every icon together. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): shed secondary session toolbar icons instead of wrapping Quest was burning a full toolbar line when the icon slab did not fit beside collapsed search. Keep one nowrap row; hide browse/settings (and similar headerActions) below a container-query width, with a higher bar when the query chip is present (#2039). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): satisfy typecheck for session header-row test CI failed because optional chaining left headerRow possibly null when reading className. Use getByTestId instead. * fix(web): put shed session toolbar actions in a More menu Hiding headerActions on narrow headers removed Settings with no alternative. Keep one toolbar row, but surface secondary actions in an accessible ⋯ popover below the container-query threshold (#2039). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fold primary toolbar into More when query chip is active At ~320px the collapsed chip plus unread/filter/New still overflowed past More/Settings. When searchQuery is non-empty, put primary controls in the same overflow menu so the row is [chip][⋯] (#2039). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): pack session toolbar actions beside search ml-auto shoved the action slab to the right edge and left a Quest-width gap between the search icon and BlockedJumpPill (#2039). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): widen machine menu clamp and width-gate toolbar More MachineFilterMenu near the left edge (inside More) got a tiny right-only maxWidth. Clamp to the wider horizontal pocket and left-align when that side wins. Fold primaries into More only when a query chip is present and the header is under 32rem (#2039 / #2040). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fold toolbar into More on narrow headers without a query Empty query still forced toolbarCrowded=false, so at 320px / 130% font with machines+unread the More control went offscreen. Crowd from header width in both query states (#2039 / #2040). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): stop Quest-width empty query collapsing to search+More A flat 32rem crowd gate folded every primary into More on typical Quest sidebars (~400px), leaving [search][⋯] and a desert of empty space. Empty query now crowds only below ~22rem (still covers 320px/130% font); query chip keeps the higher 32rem bar (#2039 / #2040). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): pin toolbar More to the trailing edge on Quest widths Left-packing every action left a dead zone after New while Settings hid in More. Primaries stay beside search; secondary/More get ml-auto on a flex-1 actions row so the chrome reads [search][primaries] … [⋯] (#2039 / #2040). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): shed session toolbar icons by keep rank A single width gate either hid the whole row or left a hole before More. Drop the lowest-priority controls first (browse, mark-all, settings) and keep search, new, and the unread lens on the row while they fit (#2039). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): size toolbar shed from actions box, not a search guess Active query reserved 200px for a max-w-[9rem] chip and collapsed the row to search+More. Measure the actions flex box and each control; keep daily icons until width is known (#2039). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): remeasure toolbar shed after search collapse The actions box unmounts while search is expanded, so a one-shot observer left the next layout on a stale width. Fit math now reattaches on the new node, subtracts a visible machine menu, and uses the measured More width and column gap. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): let query chip yield so toolbar More stays on-screen At 320px and 130% type a long chip plus calendar left too little room for the machine menu and More. An active query now uses min-w-0 on search and shrink-0 on actions so only the chip text truncates. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): stop desktop toolbar using an infinite shed budget Empty-query first paint treated the actions gutter as infinite, so every shrink-0 icon stayed inline and painted past the split. Use a finite fallback, cap the budget by the real row gutter, and give the sidebar min-w-0 so --sidebar-w actually constrains the flex item. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): use one-gap slack instead of leaving an icon-sized hole The shed refused the next icon when the gutter was only a column-gap short, so the row kept an empty slot the size of that icon. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fill the toolbar gutter before opening More The shed was budgeting a guessed remainder, so More stayed open while a wide empty stretch sat before the split. The budget is now the pixels from the actions' left edge to the row's right edge. If every icon fits, overflow is empty. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): size the toolbar budget from the flex leftover, not the icons shrink-0 made the actions box as wide as whatever was already showing, so search lost its clear and calendar and a collapsed query could hide every action. The actions box is flex-1 again, and a query chip cannot shrink below its icon, clear, and calendar. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): reserve machine menu and More before search width A long 9rem query chip at 320px / 130% left the actions box under 50px while machine menu + More need ~94px, so More slid off-screen. Cap the collapsed search group to the row minus those trailing controls. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
317e39a0bb |
fix(web): 48px session search clear with calendar left of it (#2034)
* fix(web): 48px session search clear with calendar left of it (#2019) Quest/touch: expanded search uses a min 48x48 clear end-cap as the furthest-right control, with the calendar embedded immediately to its left inside the field. Collapsed search stays shrink-0 with a larger clear chip so it does not steal the toolbar row. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): anchor date picker to search group; let collapsed chip shrink (#2019) HAPI Bot: mount the calendar popup on the search control (not the 48px button) so left/right align stays on-screen at narrow widths. Drop shrink-0 on the collapsed group so the query chip can yield space while calendar/clear stay fixed hit targets. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
7b00df458c |
fix(voice): configurable ElevenLabs max conversation duration (#2017)
* feat(voice): configurable ElevenLabs max conversation duration Auto-created ConvAI agents never set conversation.max_duration_seconds, so ElevenLabs applied its 600s default and cut voice conversations off. Add ELEVENLABS_MAX_DURATION_SECONDS (default 1800, validated 60-7200). The cap is applied when the hub creates an agent and reconciled onto existing hub-managed agents (found by name) once per process. Operator-supplied agents (ELEVENLABS_AGENT_ID / voice map / customAgentId) keep their own cap. Invalid values fail the token request with a clear message instead of an API 422. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * feat(voice): Settings UI for ElevenLabs max conversation duration Extend the hub-owner credentials panel (Settings -> Voice -> manage providers) with a numeric field for ELEVENLABS_MAX_DURATION_SECONDS, reusing the same env-wins-over-Settings overlay that already backs the ElevenLabs API key field. A Settings save takes effect on the very next token request in the same process; the env var still requires a restart. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(voice): reconcile max duration on cached agents, not just once per process Codex review on heavygee/hapi#230 caught this: getOrCreateAgentIdForVoice's agentIdCache short-circuited before ever reaching ensureAgentOverrides, and ensureAgentOverrides's overridesEnsuredAgents gate was a one-time-forever marker that didn't account for the configured duration changing after the first reconciliation. Together, any voice already used once in the hub process would keep its old cap until a restart, contradicting the Settings UI's 'takes effect immediately' claim. Track the last-applied max duration per agent and compare it against the live setting on every call, including agentIdCache hits, so a Settings change reaches an already-cached agent on its very next token request. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(voice): address tiann/hapi#2017 review findings - Fix TS2769 typecheck failure: test assertions casting the credentials response narrowed the type to omit 'editable', which upstream CI's bun typecheck catches and this fork's (currently broken) CI did not. - An invalid ELEVENLABS_MAX_DURATION_SECONDS env value threw out of buildMaxDurationStatus(), which is shared by every provider's GET/PUT credentials status — breaking visibility into OpenAI/Deepgram/ Groq settings too, not just ElevenLabs. The token-issuance hot path (getConfiguredElevenLabsMaxDurationSeconds) still throws there by design; only the status/update endpoints now degrade gracefully. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(voice): interpolate duration range in the Settings hint Nit from tiann/hapi#2017 follow-up review: the hint under the max-duration field rendered the literal '{min}-{max}' placeholders instead of '60-7200'. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
5847b034e5 | fix(codex): announce automatic context compaction when it starts (#1999) | ||
|
|
b3a9c0cddf |
fix(cli): strip agent-context env from web Terminal PTYs (#2014)
Runner injects HAPI_AGENT_CONTEXT into session CLIs; TerminalManager was forwarding it into human-facing web Terminal shells, so operator tools (lane B auth, stack-switch guards) refused a real person. Strip agent markers, keep HAPI_SESSION_ID, set HAPI_USER_TERMINAL=1. Fixes #2013 Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
c3e33835c3 |
fix(cli): stamp and reap orphan HAPI MCP bridge helpers (#2004)
* fix(cli): stamp and reap orphan HAPI MCP bridge helpers MCP stdio bridges survive agent-root death (OOM / partial kills) and were invisible to #1911 argv orphan reap. Stamp --hapi-session-id on bridge spawn and match mcp --url helpers in orphanReap. Fixes #2003 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): use ApiSessionClient.sessionId for MCP bridge stamp bootstrapSession returns ApiSessionClient directly; session.client was a typecheck failure that broke CI typecheck:cli on #2004. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger Codex PR review after 503 infra failure Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
eca9e99331 |
feat(web): Enlarge button for dictation and text submission (#2006) (#2007)
* feat(web): larger empty-composer voice/dictate hit target When the primary UnifiedButton is idle voice/dictate (empty draft), use h-12 w-12 so Quest and touch clients get a usable target. Compact h-8 w-8 remains for Send, stop, and disabled states. Closes #2006 Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): full-height composer primary (voice ↔ send) Park UnifiedButton beside the composer card so it stretches the full input+toolbar height. Empty shows giant voice/dictate; draft swaps the same giant control to Send (icon only). Refs #2006 Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): fix fill-height primary goldens + UnifiedButton mocks (#2006) Fill-height tip regressed no-arg onSend assertions and empty-voice helper expect; HappyComposer omitPrimaryButton needs UnifiedButton in vi.mocks. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): assert onSend('default') + fill-height helpers (#2006) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): flush fill-height composer primary to card edge (#2006) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep draft voice companion beside fill-height Send (#2006) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep draft voice companion beside fill-height Send (#2006) Revert accidental soup dump of expandEnter + fat ComposerButtons. Empty stays giant voice; draft shows companion waveform left of giant Send. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): draft voice as toolbar circle, not beside giant Send (#2006) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): hide draft voice when scratchlist owns Send (#2006) Codex P2: toolbar DictationButton and Ctrl/Cmd+Shift+D must not start voice while routesToScratchlist — same suppress as UnifiedButton. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): compact primary when composer is expanded (#2006) Fill-height Send/voice is for the collapsed Quest card. Expanded mode stays a compact control so we do not paint a full-shell action strip. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: re-trigger upstream pr-review after Sol distributor 503 Empty commit — test/integration already green on tip; pr-review failed twice with "No available channel for model gpt-6.1-sol". Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
99cd3d4172 |
feat(android): add Russian locale (#1963)
* feat(android): add Russian locale
Translate all 518 app strings into values-ru/strings.xml, register ru in
locales_config.xml, and add AppLanguage.RUSSIAN to the in-app language
picker.
* fix(android): use count-agnostic Russian forms for dynamic counts
The *_many strings used a genitive-plural noun after the number, which is
wrong for 2-4 (e.g. "2 инструментов"). The house style avoids <plurals>,
so phrase these labels with the noun first ("Инструменты: %1") instead:
tool groups, review findings, queued/new messages, agent steps, questions,
lines, chars, edits, usage requests, scratchlist count and attachment cap.
* fix(android): make the process-steps label count-safe
* fix(android): use a count-safe token label in the usage tooltip
---------
Co-authored-by: mrudy <mrudy@local>
|
||
|
|
efc9004fda |
feat(web): add Russian locale and detect initial language (#1958)
* feat(web): add Russian locale - Add web/src/lib/locales/ru.ts - Register 'ru' in the locale registry, i18n context, language switcher and settings - Format outline timestamps with the day.month convention for 'ru' * feat(web): detect initial locale from Telegram and browser The Telegram Mini App runs in its own web view, so a language chosen in the browser does not carry over and the app always opened in English. Pick the first session locale from an explicit stored choice, then the Telegram user's language_code, then navigator.language, then English. * feat(web): translate the newly localized strings Russian values for the keys added by the hardcoded-string fix (see the separate web i18n PR). Keeping them on this branch so the locale PR is complete when both land. * feat(web): add Russian for the newly localized strings * fix(web): recheck the Telegram language after a slow SDK load * fix(web): keep a manual language choice over late Telegram detection --------- Co-authored-by: mrudy <mrudy@local> |
||
|
|
0ef5f976c0 | fix(web): render Codex goal responses as cards (#1954) | ||
|
|
d3d8bf2620 |
feat(ios): add Russian locale (#1964)
* feat(ios): add Russian locale
Add Russian translations to Localizable.xcstrings for all 591 catalog
entries (reusing matching Android translations and translating the rest);
zh-Hans and format placeholders are preserved.
* fix(ios): make Russian selectable and use correct plural forms
- add AppLanguage.russian ("ru"), its display name, the AppleLanguages
override, and register ru in the Xcode project's knownRegions so the
language picker can actually select Russian
- add Russian one/few/many/other plural variations for %lld sessions,
%lld attachments and %@, %lld sessions
- use count-agnostic phrasing for the other dynamic counts (tools, edits,
lines, chars, findings, notes, queued messages, questions, ...)
- correct Input/Output to Ввод/Вывод
- cover the new language case in ThemePrefsTests
* fix(ios): use Russian plural forms for the remaining count labels
* fix(ios): pass the step count to the plural localization
* fix(ios): pass tool counts to the plural localization
* fix(ios): keep the singular English tool-group labels
---------
Co-authored-by: mrudy <mrudy@local>
|
||
|
|
c2d2cfe6f8 | Add 130% app font size (#1975) | ||
|
|
5153ff23c1 | test(ios): make SessionSplit width assertion responsive (#1907) | ||
|
|
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> |
||
|
|
97cf69bb59 |
feat(web): dictate keyboard shortcut (Ctrl/Cmd+Shift+D) (#1902)
* feat(web): add Ctrl/Cmd+Shift+D dictate keyboard shortcut Toggle composer dictation or voice assistant via the same path as the mic buttons, with dialog/input guards and tooltip documentation. Fixes tiann/hapi#1900 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): address dictate hotkey bot findings on PR 1902 Ignore key repeat on the session listener and align assistant-mode invoke guards with UnifiedButton send mode (canSend). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
b6849c1d35 |
fix(codex): reject history sync for active sessions (#1895)
* fix(codex): guard transcript sync for active sessions * test(codex): exercise divergent transcript prefix |
||
|
|
a259ad3144 |
fix(cli): change_title updates metadata.name for visible renames (#1896)
* fix(cli): change_title updates metadata.name for visible renames Agent change_title was writing metadata.summary only, so spawn --name sessions kept showing the old title. Route explicit renames through metadata.name (web rename semantics) across MCP, Codex, and Pi. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): voice context prefers metadata.name for session title Voice bootstrap/full formatters only read summary.text, so after change_title sets name the spoken context kept a stale title. Use the same name→summary precedence as getSessionTitle. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: polish title rename tests after cold review Rename the Codex root title test to match last-writer-wins behavior, and skip the voice full-context summary header when no title signal exists (avoid literal undefined). 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. |
||
|
|
c258a2279e | fix(web): synchronize assistant runtime and history loading (#1893) | ||
|
|
2f72a8012e |
fix(web): save composer draft on visibilitychange, not only on unmount (#1883)
Links in a session open via window.open(_blank). For an installed standalone-display PWA, navigating to a URL outside the manifest's scope is a platform behavior (Chromium/Edge show it as a minimal overlay with a close affordance), not custom code here — but closing it can mean the underlying page had a real top-level teardown, not just an in-app remount. useComposerDraft only ever persisted the draft in its effect's cleanup — reliable for a React-managed unmount (switching sessions), but browsers don't guarantee that cleanup runs to completion before a genuine top-level navigation completes, so the save could simply never happen. Extracts the existing save logic into persistNow() and calls it both from the unmount cleanup (unchanged) and a new visibilitychange listener (when document.visibilityState becomes 'hidden'), mirroring the pattern already used elsewhere in this codebase (useSSE.ts, useAuth.ts, useSyncingState.ts, usePwaUpdate.ts, useVisibilityReporter.ts) for the same class of problem. Fixes #1882 Co-authored-by: Ian Stevenson <is@lockhouse.systems> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
773af2cf65 |
feat(web): setting to open external links in a new tab (#1884)
* feat(web): setting to open external links in a new tab Left-clicking an external http(s) link renders it with no explicit target, defaulting to _self. In an installed/standalone PWA this gets intercepted by the browser and shown as a captive in-app overlay instead of leaving the app; in a normal tab it navigates away from HAPI entirely. Right-click already opens in a new tab, but that's not a fair ask for every link. Add an opt-in Settings > Display > Links toggle that forces target="_blank" rel="noopener noreferrer" on genuine external http(s) links only (relative/SPA hrefs and other schemes like mailto are left untouched). Defaults to off, preserving today's behaviour, since the overlay may be intentional/liked by some users. Vibe Coding disclosure: implemented with Claude Sonnet 5 (Claude Code). * fix(web): simplify link-toggle setting description per review The description mentioned the target attribute (an implementation detail, not user-facing) and asserted the in-app overlay is what every installed PWA shows, which was only actually confirmed on Chromium/ Edge. Describe the user-visible behaviour only, without promising a specific rendering across all installed-app environments. * fix(web): stop promising specific behaviour when installed as an app Per follow-up review: describing the setting as "always" opening links in a new tab "even when installed as an app" still guarantees a specific outcome across every browser's PWA handling, which isn't something this change can verify or promise. State only what the setting does (open in a separate tab), not how every installed-app environment will render that. --------- Co-authored-by: Ian Stevenson <is@lockhouse.systems> |
||
|
|
a7ad631691 |
feat(web): auto-hide primary session scrollbars (#1730)
* feat(web): auto-hide primary session scrollbars * test(web): run scrollbar auto-hide E2E in CI |
||
|
|
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> |
||
|
|
7b6c575514 | fix(web): refresh cached session tails with a small page (#1847) | ||
|
|
d14683a4b2 | perf(hub): cache prepared statements and memoize per-socket session access (#1851) | ||
|
|
b11358f333 | test(ios): assert Scratchlist preview visibility after focus (#1861) | ||
|
|
88b037df10 | docs(guide): document HAPI Markdown math delimiters (#1862) | ||
|
|
c70d2282db | test(ios): stabilize QuestionCard viewport assertions (#1865) | ||
|
|
08dfce67c3 |
fix(grok): preserve ACP text deltas (#1860)
Grok ACP emits true agent_message_chunk deltas. Default overlap dedupe treats a later "0" after "300" as an already-buffered suffix and drops it, so the hub stores 300 instead of 3000. Set textChunkMode: 'delta' on createGrokBackend, matching OpenCode. Co-authored-by: Aqi77su <304702613+Aqi77su@users.noreply.github.com> |
||
|
|
facb66ee03 | Release version 0.30.7 |