feat(overseer): read-only Overseer entity + 7 query tools + voice route#56
feat(overseer): read-only Overseer entity + 7 query tools + voice route#56heavygee wants to merge 31 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e21a3c584c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| getSession: (sessionId) => this.getSession(sessionId), | ||
| getSessions: () => this.getSessions() |
There was a problem hiding this comment.
Scope Overseer reads to the caller namespace
When an authenticated client calls any /api/overseer tool, the entity resolves sessions through the global getSession()/getSessions() accessors instead of the JWT namespace. In a multi-namespace hub, list_active_workers enumerates every tenant's sessions, and the returned IDs can then be passed to get_session_recent_output or get_worker_health to read another namespace's transcript and state. Pass c.get('namespace') into tool dispatch and use namespace-scoped accessors or reject cross-namespace session IDs.
Useful? React with 👍 / 👎.
| const event = engine.getOverseer().recordConvoTurn({ | ||
| operatorText: parsed.data.operatorText, | ||
| overseerText: parsed.data.overseerText, | ||
| relatedSessionId: parsed.data.relatedSessionId ?? null, |
There was a problem hiding this comment.
Validate convo-turn session ownership
When a caller includes relatedSessionId, this write path records it without resolving that session against c.get('namespace'), unlike the existing session routes that use requireSessionFromParam. If a namespace A user knows a namespace B session ID, the FK accepts it and the convo_turn is stored under B's session history, polluting another tenant's audit context. Validate ownership before recording the relation or drop it.
Useful? React with 👍 / 👎.
| return { | ||
| items, | ||
| candidates: items.filter((item) => item.status === 'new'), | ||
| surfaced: items.filter((item) => item.status === 'surfaced'), |
There was a problem hiding this comment.
Exclude Overseer turns from worker activity
When an Overseer conversation turn is saved with relatedSessionId, it creates a latest event for that session with sourceKind: 'overseer'. These health/state calculations fetch the latest event without filtering to worker or hub-observed activity, so asking the Overseer about a stale session makes lastActivityAt become the conversation timestamp and silenceMs near zero, masking stale workers in get_session_state, get_worker_health, and list_active_workers. Exclude convo_turn or overseer-sourced events from activity calculations.
Useful? React with 👍 / 👎.
| const lastToolCall = this.events.query({ sessionId, eventType: 'tool_call', limit: 1 })[0] | ||
| ?? this.events.query({ sessionId, eventType: 'tool_result', limit: 1 })[0] |
There was a problem hiding this comment.
Compare tool call and result timestamps
When a session has any tool_call event, this expression never looks at tool_result, even if the result is newer than the call it completed. lastToolCallAgeMs can therefore report an old start time immediately after fresh tool activity, misleading the Overseer about recency during long-running tools or after a just-finished command. Query both event types together or compare the two timestamps before selecting one.
Useful? React with 👍 / 👎.
|
|
||
| getSessionRecentOutput(sessionId: string, n = 10): OverseerRecentOutputChunk[] { | ||
| const limit = Math.min(Math.max(n, 1), 50) | ||
| const messages = this.messages.getMessages(sessionId, limit) |
There was a problem hiding this comment.
Fetch enough rows before filtering recent output
When the last n stored messages are non-text tool-call/tool-result records, getMessages(sessionId, limit) returns only those rows and the subsequent plain-text filter drops them, so get_session_recent_output returns fewer chunks or an empty list even though earlier recent transcript text exists. Fetch a larger window and then slice after filtering so the tool fulfills its “last N transcript chunks” contract.
Useful? React with 👍 / 👎.
| if (record?.role === 'user' && typeof record.content === 'string') { | ||
| return record.content |
There was a problem hiding this comment.
Parse structured user messages as transcript text
When operator messages use the normal web shape written by MessageService.sendMessage ({ role: 'user', content: { type: 'text', text } }), this branch only accepts string content and returns null, so get_session_recent_output drops the operator prompts from context. That leaves the Overseer seeing worker output without the instruction that produced it; extract content.text for text records before skipping the row.
Useful? React with 👍 / 👎.
| if (reported === 'blocked' || reported === 'failed' || reported === 'complete') { | ||
| return { state: reported, confidence: 0.9, note: `worker self-reported ${reported}` } |
There was a problem hiding this comment.
Let live pending requests override stale completion reports
When a session has a current permission request, deriveObservedWorkerState returns waiting_on_operator, but an older worker completed event still makes this branch return complete with 0.9 confidence. In that scenario get_worker_health tells the Overseer the worker is complete while there is an active operator decision pending; treat observed === 'waiting_on_operator' as a conflicting current signal before terminal self-reports.
Useful? React with 👍 / 👎.
| let body: unknown | ||
| try { | ||
| body = await c.req.json() | ||
| } catch { | ||
| body = {} |
There was a problem hiding this comment.
Reject malformed tool request bodies
When JSON parsing fails here, optional-argument tools run with {} instead of rejecting the request. A client that sends a malformed filtered query_events or query_inbox body receives the default unfiltered result set (up to 50 rows), which is surprising and can expose more operational context than intended. Return 400 on invalid JSON as the convo-turn route does.
Useful? React with 👍 / 👎.
… chat (tiann#974) agentState keeps an answered permission request in completedRequests. When its tool_use message is not in the loaded window, the permission-only synthesis appended a card to the end of the timeline — and there is no chronological re-sort, so the card stays pinned above the composer as a stale "answered" card that never moves to its place in history. With several answered asks this piles up at the bottom of the chat. Synthesize a card only for a *pending* request (the case that needs an answerable card when its message hasn't loaded). A resolved request is history and renders only via its own message when that message is in the window.
…#973) asStrOrDef/asOpt* helpers now treat missing keys as undefined so get_available_models/get_commands parsing works again. safeParse success checked explicitly. Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web,hub,cli): show machine load in session sidebar Runners attach OS health snapshots to machine-alive heartbeats; the hub caches them and the web session list renders load or CPU between the machine label and session count. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web,cli): show CPU and RAM pressure in machine health badge Sidebar label now combines CPU and RAM percentages for overload signaling; load stays in the tooltip on Unix. Prime CPU sampling so the first heartbeat includes usage, not just memory. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): visual machine health meters with tooltip Replace bare CPU/RAM text with labeled mini bar gauges, chip border tint by severity, and a HoverTooltip explaining capacity and overload guidance. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): widen machine health tooltip with horizontal layout Allow a generous popover width and lay CPU/RAM/load out side by side so the capacity tooltip reads wider and less tall than the chip. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): anchor machine health tooltip to row left edge Wide tooltip was align=end on the chip, so it grew left off-screen. Use row-span positioning on the machine tile button instead. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): machine host card with OS label and inline health Turn the session sidebar machine row into a bordered host panel with OS metadata and side-by-side CPU/RAM meters embedded in the tile instead of a flat label line matching project rows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep machine host tile single-row height Collapse the machine header back to one py-1.5 row with OS and compact inline health beside the name, and restore the original project indent without the extra nested rail or second header line. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): show CPU core count in machine health tooltip When the runner reports cpuCount, the tooltip reads "CPU across all 6 cores" instead of the generic all-cores label. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: add machine health sidebar screenshots Dogfood captures for the session sidebar machine tile and capacity tooltip, for upstream PR review. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): clear machine-alive priming timeout on disconnect Track the 50ms CPU priming setTimeout and clear it in stopKeepAlive so disconnect/shutdown during the delay cannot leave a stray interval alive. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: drop dogfood screenshots from upstream PR diff Review evidence lives in the PR discussion only; no need to ship PNGs in the repo long-term. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): truncate long machine OS/host metadata in sidebar row Bound the metadata span so a long hostname cannot push the health chip or session count off-screen in narrow sidebars. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): reveal machine health tooltip on keyboard row focus Wire MACHINE_ROW_TOOLTIP_FOCUS_CLASS and aria-describedby on the machine header button so keyboard users can read the health tooltip like session rows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): use MemAvailable for Linux RAM pressure on Bun Bun's os.freemem() reflects MemFree (~1% on cache-heavy hosts), which made sidebar RAM read ~99% while btop showed ~40% used. Parse /proc/meminfo MemAvailable instead so used percent matches operator tools. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web,cli): show machine uptime in sidebar tiles and tooltip Collect os.uptime() as uptimeSeconds on keepalive and render compact up 1h 54m in the machine meta row plus an Uptime line in the health tooltip. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): anchor machine health tooltip to chip not row align=row positioned the tooltip below the full machine header button, so the collapsible project panel painted over it on hover. Use align=end with a min-width panel so mouse and keyboard tooltips stay visible. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…eadable (tiann#953) * feat(gemini): remove launchable Gemini CLI agent, keep sessions readable Google sunset the consumer Gemini CLI (Pro/Ultra/free tiers stopped serving requests 2026-06-18). This removes the ability to launch/create Gemini CLI sessions while keeping existing stored Gemini sessions fully readable in the web UI. Removed (no longer launchable): - cli/src/gemini/ runtime (runGemini, loop, local/remote launchers, session, ACP backend, config, scanner) + GeminiDisplay ink view - `hapi gemini` command + registry entry + usage line - runner spawn branch & buildCliArgs mapping now reject gemini with a clear error; resume dispatch throws a clear "no longer supported" error - gemini dropped from the new-session agent selector via new CREATABLE_AGENT_FLAVORS, and from preferred-agent defaults Kept (read path — existing sessions still validate, load, render): - `gemini` in AGENT_FLAVORS / AgentFlavorSchema, FLAVOR_CAPS / FLAVOR_LABELS - AgentFlavorIcon badge, model-option labels, ACP message normalization, metadata.geminiSessionId, hub session dedup/resume-id Note: the Gemini *Live voice* backend is a separate feature and is untouched. Adds read-guarantee tests (stored gemini validates; excluded from creatable). typecheck + full suite green. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): reject gemini resume before handoff (tiann#953 review) HAPI Bot [Major]: `hapi resume <active-gemini-session>` called handoffSessionToLocal() — which tells the running remote agent to exit — before reaching the gemini-unsupported throw in dispatchLocalResume, so it could stop the live/readable session and then fail locally. Move the gemini guard into resumeCommand.run before the handoff, so an active Gemini session is left running/readable instead of being stopped. Keep the dispatch-layer guard as defense-in-depth. Adds a regression test asserting handoffSessionToLocal is not called for an active gemini target. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): harden against stale gemini input (tiann#953 review) Two [Minor] follow-ups from HAPI Bot: - newSessionFormDraft: coerce a restored browse draft's agent to a creatable flavor, so a pre-removal 'gemini' draft cannot submit agent:'gemini' even though the selector no longer offers it. - buildCliArgs: reject 'gemini' explicitly instead of silently falling through to the 'claude' command if the exported helper is reused outside the guarded spawnSession path. Updated the buildCliArgs precedence test to a creatable agent and added a test asserting buildCliArgs('gemini') throws. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): reset dependent draft fields when coercing stale agent (tiann#953 review) Follow-up [Minor]: coercing a stale gemini draft's agent to claude left model/base/effort untouched, so a { agent:'gemini', model:'gemini-2.5-pro' } draft restored as claude *with* a Gemini model, which handleCreate() then sent to the runner. Now reset model / cursorSelectedBase / effort / modelReasoningEffort to defaults whenever the agent is coerced. Adds a regression test. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): tombstone `hapi gemini` so it errors clearly (tiann#953 review) HAPI Bot [Major]: after removing geminiCommand from the registry, resolveCommand() treats `gemini` as an unknown subcommand and falls through to the default Claude command (forwarding "gemini" as an arg), so `hapi gemini` silently started Claude instead of reporting the sunset. Add an explicit tombstone `gemini` command that prints the sunset error and exits 1. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(web): assert AgentSelector hides the sunset Gemini agent (tiann#953) Render regression test confirming the new-session AgentSelector offers exactly CREATABLE_AGENT_FLAVORS and never shows a Gemini radio. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: HAPI <noreply@hapi.run> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…iann#992) * refactor(claude): thread session's selected model into SDKToLogConverter Adds an optional selectedModel field to the converter's context, wired from session.getModel() in the launcher, so a later commit can seed the turn-1 contextWindow estimate for presets whose system/init model arrives without the "[1m]" suffix. No behavior change yet. * fix(claude): key contextWindow cache by model to stop 1M/200k flicker The remote launcher re-emits system/init on every turn for the same converter instance. Its init-time estimate only checked whether the model string ended in "[1m]", but current claude CLI versions strip that suffix from system/init for some 1M presets (fable[1m] arrives as "claude-fable-5"), so the estimate guessed 200k for them. The one authoritative value is result.modelUsage[<model>].contextWindow, which arrives after the heuristic has already injected 200k into that turn's assistant message and then gets clobbered back to 200k by the very next turn's init - producing the observed 200k<->1M oscillation in the web status bar. Cache the authoritative contextWindow per model id instead of a single session-wide number, and only let system/init seed a heuristic guess for a model that has no cached value yet, so a same-model re-init no longer downgrades an already-learned value. Two observed facts about the CLI's model ids drive the design: system/init.model and the result.modelUsage keys always agree with each other within a session (both bare for plain/fable[1m], both suffixed for opus[1m]/sonnet[1m]), while each per-turn assistant message reports its model bare and thus can't distinguish a 200k plain preset from its 1M "[1m]" variant on tiers where they share a base id. So the cache is keyed on the raw id (init/result agree, no normalization) and assistant injection looks the value up via resolvedModel (the last init id) rather than the lossy message.model. Keying raw keeps a plain preset and its [1m] variant on distinct entries; looking up via resolvedModel also means sidechain (Task subagent) messages carry the main session window rather than the subagent's own, since the web status bar picks the most recent usage message without filtering sidechains and would otherwise flicker to the subagent's smaller window while it runs. For presets whose init model arrives bare even though they are 1M (fable[1m]), the originally-selected preset - which preserves the "[1m]" suffix - seeds the turn-1 estimate, kept live across mid-session model switches via updateSelectedModel() (called from the launcher on every turn) so it never goes stale. * fix(web): recognize [1m] suffix on full Claude model ids in budget fallback getContextBudgetTokens already special-cased "[1m]" for short preset values (e.g. "opus[1m]") but fell through to the default 200k budget for full model ids (e.g. "claude-opus-4-8[1m]"), which is what the CLI now reports once context_window isn't available and this fallback is consulted. Check the suffix on that branch too so it stays a correct last-resort even without a session-provided context_window. * refactor(web): merge duplicate Claude context-budget branches isClaudeModelPreset(trimmedModel) and the startsWith('claude-') branch below it had become byte-for-byte identical bodies after the [1m] suffix check was added to both. Merge them into one condition; no behavior change. * fix(claude): distinguish fable from fable[1m] when the CLI reports both bare The per-model contextWindow cache keyed on the raw system/init model id, on the assumption that a 1M preset and its plain form always land on distinct ids. That holds for opus[1m]/sonnet[1m] (the CLI reports the "[1m]" suffix on their init and result ids) but not for fable: the CLI reports both "fable" and "fable[1m]" with the same bare id "claude-fable-5". So the "seed only if not already cached" guard would skip re-seeding when switching fable[1m] -> fable, leaving the stale 1M in place until fable's own result arrived - the same switch flicker this change set out to remove, just for fable specifically. Fold the selected preset's "[1m]" back into the cache key (computeContextWindowKey): when the init model arrives bare but the session selected an "[1m]" preset, key the entry as "<id>[1m]" so the 1M and plain variants stay distinct; ids the CLI already suffixed are left as-is. Seeding, lookups, and the current-model result entry all use this resolved key. Subagent result entries (e.g. haiku) keep their own raw id so the session's "[1m]" is never folded onto a model that isn't the selected one.
* fix(cli): add darwin vm_stat memory percent parser Add readDarwinMemoryUsedPercent, a pure parser that computes macOS used memory as App Memory + Wired + Compressed (anonymous + wired-down + occupied-by-compressor pages), matching Activity Monitor's "Memory Used" figure. Page size is parsed from the vm_stat header rather than hardcoded, since it differs between Apple Silicon (16KB) and Intel (4KB) Macs. Not wired up yet; covered by unit tests, including a verbatim vm_stat capture from a 16GB Mac mini where the pre-fix total - freemem() path reported 99% (counting reclaimable cache as used) while App+Wired+ Compressed is 79% — the number a user sees in Activity Monitor. * fix(cli): wire darwin memory percent into computeMemoryPercent Add a platform() === 'darwin' branch that shells out to vm_stat (1s timeout, guarded by try/catch) and feeds its output to readDarwinMemoryUsedPercent. On any failure or undefined result it falls through to the existing total - freemem() fallback, matching the Linux branch's structure. This fixes the Machine capacity tooltip showing a stuck ~99% "High pressure" warning on macOS runners: os.freemem() there counts reclaimable file cache as used, so it reports near-total usage. Summing only App Memory + Wired + Compressed reports the same figure Activity Monitor shows.
* fix: improve Qwen realtime compatibility * fix: preserve Qwen endpoint query parameters
…nn#957) * feat(web): markdown Source | Preview toggle in session file pane Add Source | Preview toggle for .md/.mdx files in the session file route, defaulting to preview with localStorage persistence. Reuse chat markdown pipeline via MarkdownRenderer standalone mode (no assistant-ui thread). Includes unit tests, Playwright smoke, and e2e fixture. Closes tiann#954 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): cast standalone MarkdownRenderer components for react-markdown Soup verify gate: defaultComponents merge type is wider than react-markdown Components; standalone file-pane path needs explicit cast. * fix(web): route file-pane markdown fences through SyntaxHighlighter Standalone file preview now mirrors chat code-block rendering: fenced blocks use SyntaxHighlighter and MARKDOWN_COMPONENTS_BY_LANGUAGE (mermaid included) without requiring ThreadPrimitive context. Addresses HAPI Bot Major on tiann#957. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): detect fenced vs inline code in standalone markdown preview Move block detection to the pre override (react-markdown v10 does not pass inline to custom code components). Add inline-code regression test. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): mermaid diagram lightbox on click Click rendered mermaid blocks in chat to open a zoomable full-screen viewer. Re-renders from source in the modal with the current theme. Closes tiann#737. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fit mermaid lightbox to viewport on open Auto-scale diagrams to fill the viewer instead of opening at intrinsic mermaid size. Reset returns to fit; zoom label is relative to fit (100%). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fit mermaid lightbox to device screen not inner panel Use visualViewport for fit scale, full-screen pan layer, and a floating toolbar so the diagram can use the whole display. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): show mermaid lightbox by reusing inline SVG Second mermaid.render on open often left a 0×0 SVG while fit scale was computed from the loading placeholder. Reuse the inline SVG in the modal and measure viewBox with retried fit-to-screen. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): uniquify mermaid SVG ids in lightbox clone Inlining the same mermaid markup twice duplicates element ids and breaks url(#ref) resolution in the modal copy. Prefix ids and hrefs for lightbox only. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): give mermaid lightbox SVG explicit dimensions Mermaid emits width="100%" with max-width in px; that collapses to 0×0 inside the centered lightbox layer. Derive width/height from viewBox for the uniquified lightbox clone. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): render mermaid lightbox via isolated SVG data URL String id rewrites broke mermaid's embedded CSS so only labels appeared zoomed. Rasterize the inline SVG to a data-URL img instead of duplicating markup in the DOM. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): lightbox re-renders SVG for sequence diagrams Data-URL images drop or blank some mermaid diagram types (sequence). Re-render with a modal-specific id into inline SVG on a code-bg panel, and add sequence theme variables for dark/light. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): mermaid lightbox uses inline SVG in shadow DOM Reuse the inline render in an isolated shadow root so sequence CSS stays intact, and fit the viewport from viewBox dimensions instead of the loading placeholder or width="100%" layout. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright lightbox coverage per mermaid diagram type Add e2e harness and a script that opens the lightbox for each diagram kind (flowchart through kanban). Fit uses inline getBBox() so compact charts like gitGraph fill the viewport. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): bounded Playwright via webServer, fix gantt fit sizing Playwright owns Vite lifecycle (no agent-spawned dev server). Fit uses viewBox unless viewBox padding is excessive (gitGraph); wide charts use width-based coverage in e2e. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(web): gitignore Playwright test-results Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): address PR 741 bot feedback (typecheck, fit floor, gitignore) Guard lightbox open when svg is null; allow fit scale down to 0.01 while keeping 0.25 minimum for manual zoom; ignore Playwright test-results/ correctly. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright asserts click expands diagram vs inline Measure inline vs lightbox bounding box after click; require visible growth (area ratio or max dimension) plus dialog + shadow SVG content. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright against live HAPI session for mermaid lightbox Add seed script for a dedicated chat session, live hub Playwright suite (HAPI_LIVE=1), and dogfood doc. Live tests fail until driver serves shadow-DOM lightbox (catches gray-box regression on stale bundles). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): undo wrapper transform in lightbox fit; carry fit floor in zoom Resolves PR tiann#741 review threads (HAPI Bot Major): 1. measureSvgIntrinsicSize / measureContentSize prefer intrinsic dimensions (viewBox -> width/height attrs -> img.naturalSize) before getBoundingClientRect. When the rect is the only signal, divide by scaleRef.current so the 50/200ms refit retries stop compounding with the wrapper's scale(...) transform. Large diagrams no longer jump tiny or oversize after async render completes. 2. Interactive zoom (wheel/keys/buttons/pinch) now clamps with Math.min(MIN_SCALE, baseScaleRef.current). A diagram fitted below the normal 25% floor stays reachable instead of snapping back to 25% and clipping. Zoom-out button disabled threshold uses the same min. 3. Add Vitest coverage for both helpers (intrinsic precedence, scale-aware rect fallback, divide-by-zero guard) so regressions surface without needing the full Playwright stack. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): mermaid seed refuses to wipe non-fixture sessions HAPI Bot Major (PR tiann#741): SESSION_ID is documented as overridable, and the script unconditionally deletes every message for the target session before seeding fixtures. If pointed at a real session id, that's silent data loss. Refuse to proceed when an existing session id has a tag other than 'mermaid-lightbox-e2e'. New ids and the canonical fixture session still seed normally; real sessions throw before any DELETE runs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): normalize mermaid svg for lightbox shadow root Mermaid emits width="100%" on every diagram. Inside a shadow root whose host has no explicit size, that collapses to zero in Chromium for most diagram types - only ones that ship pixel attrs (e.g. journey) happen to render. Operator confirmed on the live driver: every diagram except journey opened to a grey rounded square. MermaidLightboxSvg now runs normalizeMermaidSvgForStandaloneDisplay before injecting (strips width/height="100%", bakes viewBox dims as pixels) and sets :host{display:inline-block} so the host sizes to the SVG. Inline svg in chat is unchanged - only the lightbox copy is normalized. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep mermaid lightbox content below the toolbar Operator screenshot showed the diagram top (e.g. pie 'Pets' title) clipped behind the toolbar bar. Two causes: 1. getScreenFitSize used the full viewport height, so the fit scale sized the diagram to fill an area the toolbar overlapped. 2. The viewport (drag/zoom area) was inset-0; content centered on the full viewport center, not the visible region's center, pushing the top behind the toolbar. Measure the toolbar with a ResizeObserver, subtract its height from the fit calculation (clamped at zero), and start the viewport region below the toolbar (top: toolbarHeight). Fit scale recomputes whenever toolbar height changes. Adds Vitest coverage for getScreenFitSize reserved-top math. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): guard ResizeObserver before constructing it HAPI Bot Major (PR tiann#741): Vitest jsdom does not polyfill ResizeObserver, so the toolbar measure effect throws ReferenceError when the existing mermaid-diagram React tests open the lightbox. Same code path is also brittle in any browser/webview without the API. Fall back to plain window 'resize' listener when ResizeObserver is absent. Toolbar height won't auto-update on element resize without it, but the lightbox still renders and the resize listener catches the common viewport-rotation case. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): live mermaid playwright wrapper runs from repo root HAPI Bot Minor (PR tiann#741): the wrapper sets cwd to scripts/, but the test:mermaid-lightbox:live npm script lives in the repo-root package.json, so spawning npm there exited before Playwright started. Switch cwd to the repo root and drop the unused WEB_DIR constant. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): accept signed viewBox values in mermaid lightbox normalize HAPI Bot Minor (PR tiann#741): the viewBox regex only matched digits, dots, and spaces, so a valid viewBox with negative origin (e.g. '-8 -8 640 480') returned null. normalizeMermaidSvgForStandaloneDisplay then became a no-op and left width='100%', re-introducing the zero-sized lightbox render this PR is meant to fix for the affected diagrams. Switch to the bot's suggested regex (signed numbers, single or double quotes, comma or space separators) and reject NaN parts. Adds Vitest coverage for signed origins, single quotes, comma separators, the malformed/no-viewBox null paths, and an end-to-end normalize test that fails against the old regex. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align @playwright/test on 1.60.0 across workspaces HAPI Bot Major (PR tiann#741): web/package.json pinned @playwright/test at 1.49.1 while the root workspace and bun.lock were on 1.60.0. The mismatch surfaced after rebasing onto upstream/main, where the root had already moved to 1.60.0 while my web devDependency lagged from an older commit. A frozen install would reject the lockfile and the new web e2e script could resolve a different Playwright than root scripts. Bump the web devDependency to 1.60.0 and regenerate bun.lock so all workspaces share one Playwright version. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): move mermaid playwright fixtures out of public HAPI Bot Minor (PR tiann#741): the e2e and smoke fixtures lived under web/public, so Vite copied them verbatim into web/dist and the hub asset generator embedded them in production bundles. Both pages import Vite dev-only paths (/@react-refresh and /src/dev/...), so the production /mermaid-lightbox-{e2e,smoke}.html routes would 404 on those imports. Move both fixtures to web/e2e-fixtures/ to match the existing scratchlist-fixture pattern (relative ../src/dev import, served by Vite at /e2e-fixtures/...) and update the Playwright spec to hit the new path. Build now ships 112 PWA precache entries instead of 114 (both fixtures excluded from dist). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Use semantic events as the visible text source and preserve completed plans as ordered plan proposal cards.
* fix(codex): support model-reported reasoning efforts * fix(web): prevent service worker edge caching * ci: retrigger stuck Actions run * fix(codex): accept dynamic reasoning effort values * fix(web): restore reasoning effort on model switch failure
* test: reproduce issue tiann#1020 * fix: preserve Codex safety behavior (closes tiann#1020) * test: cover Codex safety buffering dismissal * test: cover Codex safety retry edge cases * fix: keep dismissed safety buffering prompts hidden
Surface the same reasoning label already shown in the composer StatusBar in the top SessionHeader for codex/opencode sessions. Also show an explicit Fast badge only when serviceTier is fast (tiann#1004-aligned). Closes tiann#1015 (header display portion). Co-authored-by: Cursor <cursoragent@cursor.com>
…ir (tiann#1014) * feat(cursor): multitask slash, autoReview mode, native worktree/add-dir Close the highest-value Cursor Agent gaps for remote HAPI: expand ACP-safe slash pass-through (/multitask, worktree, add-dir, …), add autoReview permission mode (--auto-review spawn + mid-session slash), and route Cursor New Session worktrees through agent --worktree instead of HAPI sibling trees. Fixes tiann#1013 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): accept --mode autoReview for hapi cursor Align --mode parsing with CURSOR_PERMISSION_MODES so documented `hapi cursor --mode autoReview` enables Smart Auto instead of silently falling back to default. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(codex): bridge MCP elicitation through user input * fix(codex): allow MCP elicitation in yolo mode * fix(codex): preserve MCP form semantics * fix(codex): accept implicit MCP form mode * fix(codex): harden MCP elicitation prompts * fix(codex): require valid MCP choice answers * fix(codex): round-trip MCP array elicitation * fix(web): require explicit MCP URL confirmation * fix(codex): preserve MCP array item types * fix(codex): support multi-select MCP elicitation * fix(codex): allow MCP elicitation in read-only mode * fix(codex): route MCP tool approvals through permissions
Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2bd89e1f5c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const limit = Math.min(Math.max(args.limit ?? 100, 1), 200) | ||
| const roster: OverseerActiveWorker[] = [] | ||
|
|
||
| for (const session of this.getSessions()) { |
There was a problem hiding this comment.
Exclude inactive sessions from active roster
When a session has expired or been archived but is still cached, this loop still adds it to list_active_workers; deriveObservedWorkerState maps !active sessions to idle, so both the default roster and state: 'idle' queries report old inactive sessions as active workers and skew the Overseer's age/minAge filters. Skip !session.active here (or provide active sessions from the dependency) before deriving the roster entry.
Useful? React with 👍 / 👎.
| const host = parsed.hostname.toLowerCase() | ||
| const path = parsed.pathname.replace(/\/+$/, '') || '' | ||
| const search = parsed.search | ||
| return `${parsed.protocol}//${host}${path}${search}` |
There was a problem hiding this comment.
Preserve URL ports in link dedupe keys
When one message contains links that differ only by port, such as http://localhost:3000/foo and http://localhost:4000/foo, hostname drops the port before building the idempotency key. The second link_seen insert reuses the first key and returns the existing event, so the session log loses one of the URLs; include parsed.host or parsed.port in the normalized key.
Useful? React with 👍 / 👎.
| // Always scoop URLs from any ingestible message text (agent or user). | ||
| this.scoopLinksFromContent(session, messageId, content, ts) |
There was a problem hiding this comment.
Wire live operator messages into link scooping
This call only runs when OverseerEventRecorder.onAgentMessage is invoked from SyncEngine.handleRealtimeEvent for CLI/imported message events. Normal operator sends from the web app go through MessageService.sendMessage, which emits SSE directly and never calls this recorder, so URLs pasted by the operator are not recorded as link_seen despite the new extractor supporting user-shaped content; add the same scoop to the live message-send path.
Useful? React with 👍 / 👎.
| if (input.thinking) { | ||
| return 'working' |
There was a problem hiding this comment.
Let stale silence override stuck thinking
When an active session remains thinking but has produced no output beyond OVERSEER_STALE_SILENCE_MS, this branch returns working before checking the silence threshold. That hides exactly the wedged-worker case from get_session_state, get_worker_health, and list_active_workers (no self-report is needed for the fallback to stay working), so move the stale check ahead of the thinking check after pending requests are handled.
Useful? React with 👍 / 👎.
* test: define Grok Build integration behavior * feat: add Grok Build agent integration * test: cover Grok permissions and resume paths * docs: add Grok Build setup guide * fix: scope Grok ACP discovery to session cwd * fix: align Grok permission UI semantics * docs: clarify Grok runner setup * test: require Grok create model and effort options * feat: add Grok create model and effort pickers * test: define Grok runtime parity behavior * feat: add Grok runtime ACP controls and discovery * fix: tighten Grok runtime controls * fix: suppress nonfatal Grok title quota errors * feat: support Grok Auto permission mode * feat: forward ACP native session titles for Grok * fix: guard Grok Windows shell arguments
…n#1026) Legacy rows and inserts left sessions.active_at NULL while SessionSchema required a number, so CLI GET /cli/sessions/:id failed Zod and resume surfaced HTTP 500. Persist active_at on insert, harden hub read coerce, and nullish-transform activeAt in SessionSchema (output stays number). Fixes tiann#1025 Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse> Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: zj1123581321 <zj1123581321@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2486764af8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (loadingOlder || reachedEnd || events.length === 0) return | ||
| const beforeId = events[events.length - 1]?.id |
There was a problem hiding this comment.
Use an unfiltered cursor when loading older log rows
When the All tab filters out the latest link_seen/stale rows, events is the post-filtered list, so pagination either disappears (events.length === 0) even though older progress rows exist, or repeatedly uses the same visible beforeId after a fetch returns only excluded rows. This makes sessions with many scooped links or stale rows look empty/stuck in the Session Log; keep a cursor from the unfiltered merged page or filter these event types server-side before paginating.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8045059e05
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| operatorText: parsed.data.operatorText, | ||
| overseerText: parsed.data.overseerText, | ||
| relatedSessionId: parsed.data.relatedSessionId ?? null, | ||
| relatedEventId: parsed.data.relatedEventId ?? null, |
There was a problem hiding this comment.
Validate relatedEventId before inserting convo turns
When a client sends a positive but nonexistent relatedEventId, this value is forwarded directly into the events insert, but events.related_event_id is a foreign key to events(id) with FK enforcement enabled. That makes a bad thread id surface as an uncaught SQLite constraint failure/500 instead of a 400, so one malformed Overseer turn can fail the request path; resolve the event first or reject/drop invalid ids before calling recordConvoTurn.
Useful? React with 👍 / 👎.
| payloadJson: JSON.stringify({ | ||
| operatorText, | ||
| overseerText, | ||
| toolCalls: input.toolCalls ?? [] | ||
| }), |
There was a problem hiding this comment.
Include session identity in convo-turn payloads
When an Overseer turn is recorded with relatedSessionId, the event is meant to be memory for that worker/project, but query_events({ project }) only matches payload_json.session.project and this payload contains no session object. As a result, project-scoped Overseer queries omit the very conversation turns that were threaded to sessions in that project; enrich the convo-turn payload with the related session identity or make the project filter fall back through relatedSessionId.
Useful? React with 👍 / 👎.
| } | ||
| if (options.project) { | ||
| // Denormalized session.project lives in payload_json (#22). | ||
| clauses.push("json_extract(payload_json, '$.session.project') = ?") |
There was a problem hiding this comment.
Guard project filters against malformed payload JSON
When any existing event row has a non-JSON payload_json value, a query_events call with project reaches this json_extract expression and SQLite raises malformed JSON, turning the read-only Overseer tool into a 500 for the whole result set. Since insertSystemEvent accepts payloadJson as an arbitrary string and the column is plain TEXT, add a json_valid(payload_json) guard or otherwise ignore malformed payload rows before extracting the project.
Useful? React with 👍 / 👎.
Recreate feat/overseer-readonly-entity on upstream/main without soup merge history or FCM-owned paths, for rematerialize-safe layer + clean PR. Co-authored-by: Cursor <cursoragent@cursor.com>
8045059 to
4cb90c2
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4cb90c23f9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const events = engine.getSystemEvents({ | ||
| limit: parsed.data.limit ?? 50, | ||
| beforeId: parsed.data.beforeId ?? null, | ||
| sessionId: parsed.data.sessionId ?? null, | ||
| attentionCandidate, |
There was a problem hiding this comment.
Scope system event reads to the JWT namespace
When an authenticated client supplies a sessionId from another namespace, or omits sessionId entirely, this route forwards the query to the global event store without using c.get('namespace'). In a multi-namespace hub this exposes other tenants' event summaries/artifact URLs and the global event count via /api/system-events; validate session ownership and scope both rows and totals to the caller namespace before returning them.
Useful? React with 👍 / 👎.
| const item = engine.recordInboxOperatorAction( | ||
| inboxItemId, | ||
| parsed.data.action, | ||
| parsed.data.feedback ?? null, | ||
| parsed.data.snoozedUntil ?? null |
There was a problem hiding this comment.
Scope inbox item actions to the JWT namespace
When a caller knows or guesses another namespace's numeric inbox item id, this action path updates it by id only and never checks the item's related session against c.get('namespace'). That lets one tenant resolve/snooze/feedback another tenant's operator inbox item and receive the updated item in the response; resolve the inbox item/source session under the caller namespace before recording the action.
Useful? React with 👍 / 👎.
| const items = engine.getInboxItems({ | ||
| limit: parsed.data.limit ?? 50, | ||
| activeOnly, | ||
| sessionId: parsed.data.sessionId ?? null | ||
| }) |
There was a problem hiding this comment.
Scope inbox item reads to the JWT namespace
When an authenticated client omits sessionId or supplies another namespace's session id, this list path calls the global inbox store without using c.get('namespace'). In a multi-namespace hub /api/inbox-items exposes other tenants' attention items, summaries, source ids, and the global total; validate the session or scope the query/count by namespace before returning rows.
Useful? React with 👍 / 👎.
Step 3 of the Overseer build sequence: read-only entity + tools + voice route
Stacked on
fix/overseer-inbox-stale-noise(events #22 + inbox #23 + stale-noise fix). PR diff is exactly the 12 files of this step.What this adds
query_events- events stream by session/project/type/severity/time/status/attention_candidatequery_inbox- candidates + surfaced + heldget_session_state- hub-observed state + last activity + tool-call recency + worker_reported_stateget_session_recent_output- last N transcript chunksget_worker_health- combined reported/observed/inferred (contracts §2)explain_priority- provenance trail, reuses the storedreason_for_priority(no reverse-engineering)list_active_workers- roster by project/state/ageconvo_turnwriteback: voice/text conversation turns recorded to the events stream for provenance.GET /api/overseer/voice): dedicated Overseer surface that consumes the existing stt/tts voice substrate (feat/overseer-stt-tts-endpoints,feat/overseer-voice-persistence) - does not reimplement it. Chrome-button relocation is Step 5 (out of scope).Store changes (additive only)
queryEventsadds project/sourceKind/severity/time filters; existing query shapes unchanged.listgainsstatuses[]+categoryfilters.Protocol layer
shared/src/overseerEntity.ts: identity, tool catalog with zod arg schemas, system prompt, worker-state derivation (mapNotifyStatusToWorkerState/mapEventTypeToWorkerState/deriveObservedWorkerState/inferWorkerState),convo_turnbuilder.Tests
24 new tests pass (protocol unit + OverseerEntity tools + routes). No regressions in existing substrate tests (
systemEvents,inboxItems,store).Known: pre-existing base breakage (not from this PR)
The souped base carries unrelated breakage from other peers' incomplete merges -
hub/src/fcm/fcmNotificationChannel.tsimports a missing../notifications/modelErrorCopy, plus aweb/src/hooks/useSSE.tstype error. These are the onlybun typecheck/bun run testfailures and are outside this PR's surface. My 12 files are type-clean and fully tested.Gating note
Persona + voice-answer-quality tuning is intentionally not in this PR - it gates on the replay harness (now landed on
feat/overseer-replay-harness). This PR is the independent plumbing.Made with Cursor