Skip to content

feat(overseer): read-only Overseer entity + 7 query tools + voice route#56

Open
heavygee wants to merge 31 commits into
fix/overseer-inbox-stale-noisefrom
feat/overseer-readonly-entity
Open

feat(overseer): read-only Overseer entity + 7 query tools + voice route#56
heavygee wants to merge 31 commits into
fix/overseer-inbox-stale-noisefrom
feat/overseer-readonly-entity

Conversation

@heavygee

Copy link
Copy Markdown
Owner

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

  • Overseer as a real conversational entity in the hub: stable identity, system prompt, dedicated voice surface. Inform-only - no dispatch, no confirm, no state mutation (dispatch-with-confirm is Step 4).
  • 7 read-only tools, all unit-tested against the live substrate:
    • query_events - events stream by session/project/type/severity/time/status/attention_candidate
    • query_inbox - candidates + surfaced + held
    • get_session_state - hub-observed state + last activity + tool-call recency + worker_reported_state
    • get_session_recent_output - last N transcript chunks
    • get_worker_health - combined reported/observed/inferred (contracts §2)
    • explain_priority - provenance trail, reuses the stored reason_for_priority (no reverse-engineering)
    • list_active_workers - roster by project/state/age
  • convo_turn writeback: voice/text conversation turns recorded to the events stream for provenance.
  • Voice route (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)

  • queryEvents adds project/sourceKind/severity/time filters; existing query shapes unchanged.
  • Inbox list gains statuses[] + category filters.

Protocol layer

shared/src/overseerEntity.ts: identity, tool catalog with zod arg schemas, system prompt, worker-state derivation (mapNotifyStatusToWorkerState / mapEventTypeToWorkerState / deriveObservedWorkerState / inferWorkerState), convo_turn builder.

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.ts imports a missing ../notifications/modelErrorCopy, plus a web/src/hooks/useSSE.ts type error. These are the only bun typecheck/bun run test failures 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +171 to +172
getSession: (sessionId) => this.getSession(sessionId),
getSessions: () => this.getSessions()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +162 to +163
const lastToolCall = this.events.query({ sessionId, eventType: 'tool_call', limit: 1 })[0]
?? this.events.query({ sessionId, eventType: 'tool_result', limit: 1 })[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +364 to +365
if (record?.role === 'user' && typeof record.content === 'string') {
return record.content

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +230 to +231
if (reported === 'blocked' || reported === 'failed' || reported === 'complete') {
return { state: reported, confidence: 0.9, note: `worker self-reported ${reported}` }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +100 to +104
let body: unknown
try {
body = await c.req.json()
} catch {
body = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

junmo-kim and others added 25 commits June 29, 2026 11:40
… 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>
* test: reproduce issue tiann#968

* fix: support Cursor parameterized model picker (closes tiann#968)
* 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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +106 to +109
const host = parsed.hostname.toLowerCase()
const path = parsed.pathname.replace(/\/+$/, '') || ''
const search = parsed.search
return `${parsed.protocol}//${host}${path}${search}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +228 to +229
// Always scoop URLs from any ingestible message text (agent or user).
this.scoopLinksFromContent(session, messageId, content, ts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +192 to +193
if (input.thinking) {
return 'working'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

swear01 and others added 3 commits July 13, 2026 08:41
* 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>
tiann and others added 2 commits July 13, 2026 09:03
Co-authored-by: zj1123581321 <zj1123581321@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +115 to +116
if (loadingOlder || reachedEnd || events.length === 0) return
const beforeId = events[events.length - 1]?.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +501 to +505
payloadJson: JSON.stringify({
operatorText,
overseerText,
toolCalls: input.toolCalls ?? []
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread hub/src/store/events.ts
}
if (options.project) {
// Denormalized session.project lives in payload_json (#22).
clauses.push("json_extract(payload_json, '$.session.project') = ?")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@heavygee heavygee force-pushed the feat/overseer-readonly-entity branch from 8045059 to 4cb90c2 Compare July 14, 2026 13:21

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +33 to +37
const events = engine.getSystemEvents({
limit: parsed.data.limit ?? 50,
beforeId: parsed.data.beforeId ?? null,
sessionId: parsed.data.sessionId ?? null,
attentionCandidate,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +70 to +74
const item = engine.recordInboxOperatorAction(
inboxItemId,
parsed.data.action,
parsed.data.feedback ?? null,
parsed.data.snoozedUntil ?? null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +35 to +39
const items = engine.getInboxItems({
limit: parsed.data.limit ?? 50,
activeOnly,
sessionId: parsed.data.sessionId ?? null
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants