Skip to content

feat(overseer): text conversation core + debug-settings surface (Stage 0) - #98

Open
heavygee wants to merge 9 commits into
feat/overseer-readonly-entityfrom
feat/overseer-text-converse
Open

feat(overseer): text conversation core + debug-settings surface (Stage 0)#98
heavygee wants to merge 9 commits into
feat/overseer-readonly-entityfrom
feat/overseer-text-converse

Conversation

@heavygee

Copy link
Copy Markdown
Owner

Summary

Adds the modality-agnostic Overseer conversation core and a text debug surface in Settings (Stage 0, read-only). Operator messages → brain LLM (OpenAI-compatible) reasons and calls the 7 existing read-only overseer tools → human-facing reply + tool trace. Text is the first transport because it is the cheapest to test; voice/XR reuse the same /api/overseer/converse endpoint and it lives in Settings/debug, not top-level nav, so it is not privileged over those modalities.

Stacked on feat/overseer-readonly-entity (the read-only entity + tools + system prompt this builds on).

What's here

  • shared: converse request/response types + buildOverseerOpenAiTools() — the 7 read-only tools as OpenAI function schemas, hand-mapped from the existing zod arg schemas (no new dependency).
  • hub:
    • brainClient — OpenAI /chat/completions client with a typed BrainUnavailableError; env config OVERSEER_BRAIN_URL / OVERSEER_BRAIN_MODEL / OVERSEER_BRAIN_API_KEY.
    • runOverseerTool — the tool dispatcher extracted from the tools route so the route and the converse loop share one path.
    • converse — the read-only tool-calling loop with an iteration cap and a grounding guardrail (see below).
    • POST /api/overseer/converse — records a memory-bearing convo_turn per exchange (attention 0, never an inbox item) and degrades gracefully to brainOnline:false with a friendly message when the brain is unreachable (its GPU is shared with other workloads).
  • web: a "Talk to the Overseer (text · debug)" panel in Settings → About (next to the existing Events/Inbox debug controls) — transcript, starter questions, and a collapsible per-turn tool trace.

Grounding guardrail

The brain (llama-server) does not honor tool_choice:"required", so it would sometimes answer a fleet question from nothing (observed: "the inbox is empty" when the inbox held 10 items and query_inbox was never called). The loop appends a mandatory grounding directive to the system prompt and, if the first reply skips tools, nudges once to force verification before accepting the answer.

Test plan

  • bun typecheck clean across shared/hub/web
  • Hub unit tests: converse loop (tool call → answer, failed-tool tolerance, unknown-tool rejection, brain-unavailable, nudge-then-ground, genuine no-tool answer) + existing overseer route/entity suites green
  • Verified live end-to-end vs a real OpenAI-compatible 27B brain: name→id resolution, reported-vs-observed conflict surfacing, prioritized root-cause answers; 3/3 triage runs call query_inbox after the grounding fix
  • Dogfood on the integration build: Settings → About → ask "what needs my attention?" and confirm a real query_inbox trace

heavygee and others added 2 commits July 30, 2026 21:25
…e 0)

Add the modality-agnostic Overseer converse core: operator messages ->
brain LLM (OpenAI-compatible) reasons and calls the 7 read-only tools ->
human-facing reply + tool trace. Text is the first transport (Settings debug
panel), not a first-class surface; voice/XR reuse the same /api/overseer/converse
endpoint.

- shared: OverseerConverse types + buildOverseerOpenAiTools (7 read-only tools as
  OpenAI function schemas, hand-mapped from the zod arg schemas, no new dep).
- hub: brainClient (OpenAI chat-completions, typed BrainUnavailableError, env
  config OVERSEER_BRAIN_URL/MODEL/API_KEY), runOverseerTool (extracted dispatcher
  shared with the tools route), converse loop (tool-calling, read-only, iteration
  cap), POST /overseer/converse (records a memory-bearing convo_turn per turn,
  degrades gracefully when the brain is offline).
- web: Settings "Talk to the Overseer (text - debug)" panel with transcript,
  starter questions, and a collapsible per-turn tool trace. No top-level nav.

Verified live against the estate 27B brain (Qwen3.6) end-to-end: name->id
resolution, reported-vs-observed conflict surfacing, prioritized root-cause
answers. 4 loop unit tests (mocked brain) + existing overseer suite green;
root typecheck clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
The brain (llama-server) does not honor tool_choice:'required', so it would
sometimes answer a fleet question from nothing — e.g. "the inbox is empty" when
the inbox had 10 items and query_inbox was never called. Add two guardrails:

- Append a mandatory grounding directive to the converse system prompt ("you
  have NO prior knowledge of the fleet; every fact must come from a tool call
  this turn; never say 'nothing needs attention' without calling the tool").
- If the first answer carries zero tool calls and none has run this turn, nudge
  once to force verification; if it still declines, the question genuinely
  needed no tool. +2 tests.

Verified live vs the 27B: 3/3 triage runs now call query_inbox and answer
correctly (previously 0/1 on the same question).

Co-authored-by: Cursor <cursoragent@cursor.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: f8e20a9a76

ℹ️ 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 +128 to +131
const { reply, toolTrace } = await runOverseerConverse({
overseer: engine.getOverseer(),
config,
messages

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 conversations to the caller's namespace

When a hub serves multiple namespaces, this route ignores c.get('namespace') and gives the model the global Overseer entity. SyncEngine constructs that entity from the unfiltered event/inbox stores plus getSessions(), so any authenticated namespace can ask the new converse endpoint about another namespace's workers, transcript output, events, and inbox items. Pass a namespace-scoped entity/tool context here, and validate relatedSessionId against the same namespace.

AGENTS.md reference: AGENTS.md:L145-L149

Useful? React with 👍 / 👎.

Comment on lines +100 to +102
query_inbox: obj({
statuses: { type: 'array', items: { type: 'string' }, description: 'e.g. candidate, surfaced, held.' },
sessionId: sessionIdProp,

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 Advertise the actual inbox status values

When the model follows this description and calls query_inbox with statuses: ["candidate"], the call validates but returns no items because the stored status is new (INBOX_ITEM_STATUSES contains new, not candidate). The grounding guard then treats that tool call as successful and can accept an incorrect “nothing needs attention” answer. Define this property with an enum derived from the real status constants rather than the conceptual candidate label.

Useful? React with 👍 / 👎.

Comment on lines +41 to +43
setTurns((prev) => [...prev, { role: 'operator', content: trimmed }])
setLoading(true)
try {

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 Trim chat history before sending it

After 20 successful exchanges, turns contains 40 messages and the next send submits 41, exceeding the route's .max(40) schema and returning HTTP 400. Because the rejected operator turn is appended locally before the request, every subsequent request remains oversized until the user clears the entire conversation. Retain only the newest history that fits the server limit (and avoid retaining rejected turns).

Useful? React with 👍 / 👎.

Comment on lines +99 to +100
} finally {
clearTimeout(timeout)

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 Keep the timeout active while consuming the response

When the brain sends response headers but stalls or disconnects while producing the body, fetch() has already resolved and this finally clears the timeout before either res.text() or res.json() runs. That leaves the converse request and debug UI waiting without the configured timeout, which is especially relevant for the documented case where the contended brain disappears mid-request. Clear the timer only after the response body has been fully consumed and parsed.

Useful? React with 👍 / 👎.

Comment on lines +41 to +44
const parsed: unknown = JSON.parse(raw)
return parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : {}
} catch {
return {}

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 argument JSON

When the model emits malformed JSON for an optional-argument tool such as query_events, query_inbox, or list_active_workers, this converts the parse failure to {}; the Zod schema then accepts it and the trace records a successful unfiltered query. A truncated intended filter can therefore become a fleet-wide result and support an inaccurate grounded answer. Preserve the parse failure and return an error tool result instead of executing the tool with empty arguments.

Useful? React with 👍 / 👎.

Comment thread hub/src/web/routes/overseer.ts Outdated
role: z.enum(['operator', 'overseer']),
content: z.string().max(8000)
})).min(1).max(40),
relatedSessionId: z.string().min(1).optional()

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 Accept null related session IDs as declared

When a voice/XR or other protocol client sends relatedSessionId: null, the request is valid according to the newly exported OverseerConverseRequest type but this route rejects it with HTTP 400 because only a string or omission is accepted. The web client happens to omit nulls, but other intended transports can serialize the shared request directly. Make the route schema nullable or remove null from the shared contract.

Useful? React with 👍 / 👎.

Comment thread hub/src/overseer/brainClient.ts Outdated
Comment on lines +103 to +105
if (!res.ok) {
const body = await res.text().catch(() => '')
throw new BrainUnavailableError(`Overseer brain returned ${res.status}: ${body.slice(0, 200)}`)

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 Distinguish brain errors from unavailability

When the configured endpoint returns a normal HTTP error—such as 400 for context overflow, 401 for a bad API key, 404 for an invalid model, or 429 for rate limiting—this converts it to BrainUnavailableError, and the route reports brainOnline: false with the GPU-offline message. That hides actionable configuration/request failures and makes a reachable brain appear offline indefinitely. Reserve BrainUnavailableError for transport/timeout failures and surface HTTP response failures separately.

Useful? React with 👍 / 👎.

…e (multi-round 400)

Real-data dogfood surfaced a 400 from llama.cpp on the SECOND+ tool round: the
loaded chat template chokes on role:'tool'+tool_call_id follow-ups once the model
does multi-round tool use over a non-empty inbox (400 body: "template"/"tool_call_id").
brainClient threw BrainUnavailableError, which the route mislabeled as "brain
offline (VR)". Two fixes:

- converse loop: feed tool results back as a plain USER message (name(args) => json)
  instead of role:'tool' messages, and drop the raw assistant tool-call message from
  history. Keeps every turn on the user/assistant path all templates render.
- brainClient: BrainUnavailableError now carries kind ('unreachable'|'timeout'|
  'http'|'protocol') + status + a reachable getter; the converse route uses it to
  say "tool conversation failed (retry)" for a reachable 4xx/5xx instead of
  "brain offline". +1 test.

Verified live vs the 27B with the real 10-item inbox + a two-round question:
200/200, no 400, grounded answer (previously 400 -> false "offline").

Co-authored-by: Cursor <cursoragent@cursor.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: b4abbeb0ef

ℹ️ 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".

})
continue
}
return { reply: (message.content ?? '').trim(), toolTrace }

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 Refuse answers that remain ungrounded

When the same noncompliant brain ignores both the initial tool instruction and the follow-up nudge, nudged is already true and this returns its second zero-tool response even for fleet questions such as “What needs my attention?”. A second refusal does not prove that no fleet data was needed, so the observed hallucination mode can still report false empty or healthy states; after nudging, return a verification failure rather than accepting a fleet answer without a successful relevant tool call.

Useful? React with 👍 / 👎.

Comment thread hub/src/overseer/converse.ts Outdated
Comment on lines +108 to +110
const result = runOverseerTool(overseer, name, args)
toolTrace.push({ tool: name, args, ok: true })
resultLines.push(`${name}(${argsRaw}) => ${JSON.stringify(result ?? 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 Cap tool results before re-submitting them

When get_session_recent_output returns large assistant messages, this serializes every chunk in full into the next completion request; the message store limits the number of records but not their text size. A session with several long outputs can therefore overflow the brain's context window and turn an otherwise valid conversation into an HTTP error. Apply a per-result or overall character/token budget before appending tool data to convo.

Useful? React with 👍 / 👎.

Comment on lines +117 to +120
convo.push({
role: 'user',
content: `Results of the tool call(s) you requested:\n${resultLines.join('\n')}\n\nAnswer my question using only these results. Call another tool only if you still lack data.`
})

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 Keep tool output out of the user-instruction channel

When a worker transcript or event contains imperative text such as “ignore prior instructions,” this wraps that untrusted fleet data in a new role: 'user' message, giving repository- or worker-controlled content the same conversational role as operator instructions. In particular, get_session_recent_output can therefore prompt-inject the Overseer into concealing or falsifying fleet state; preserve a distinct tool/data channel or otherwise isolate and explicitly mark tool results as untrusted data rather than synthesizing an operator turn.

Useful? React with 👍 / 👎.

Comment on lines +139 to +141
toolCalls: toolTrace
.filter((t) => t.ok)
.map((t) => ({ tool: t.tool, argsSummary: JSON.stringify(t.args).slice(0, 500) }))

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 Persist failed calls in the conversation audit trail

When the brain requests an unknown tool or supplies arguments that fail validation, the returned toolTrace records the failed call but this filter removes it from the persisted convo_turn. Those failures influenced the subsequent answer, so replay and debugging from the memory-bearing event falsely show that no call was attempted; persist every trace entry, including its success state and error, rather than only successful calls.

Useful? React with 👍 / 👎.

Comment on lines +128 to +132
const { reply, toolTrace } = await runOverseerConverse({
overseer: engine.getOverseer(),
config,
messages
})

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 Abort brain work when the caller disconnects

When a browser unloads or another transport cancels the converse request, this route does not forward c.req.raw.signal even though both runOverseerConverse and callBrain support cancellation. The abandoned request can consequently continue through up to six tool rounds plus the final completion, consume the contended GPU, and record a conversation turn no client received; pass the incoming request signal into the loop.

Useful? React with 👍 / 👎.

…ut restart)

Swap the converse brain per request — no env edit, no hub restart:
- Request gains optional `model` (override the model name on a multi-model
  endpoint) and `profile` (select a named brain configured server-side).
- Named profiles via env: OVERSEER_BRAIN_PROFILE_<ID>_URL/_MODEL/_API_KEY, so a
  frontier endpoint's URL + key stay off the browser. Default profile unchanged
  (OVERSEER_BRAIN_URL/_MODEL/_API_KEY).
- GET /api/overseer/brains lists profiles (id/label/model only — no url/key).
- Debug panel: a "Model" text input (blank = default) + a "Brain" profile
  dropdown (shown only when >1 profile configured).

Lets you A/B the local 27B against a frontier OpenAI-compatible model for
tool-selection/quality testing. +8 resolveBrainConfig/listBrainProfiles tests.

Co-authored-by: Cursor <cursoragent@cursor.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: 0096ccb82e

ℹ️ 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 [error, setError] = useState<string | null>(null)
const [model, setModel] = useState<string | null>(null)
const [profiles, setProfiles] = useState<OverseerBrainProfileInfo[]>([])
const [selectedProfile, setSelectedProfile] = useState('')

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 Select the sole named brain profile

When a hub configures only a named profile such as OVERSEER_BRAIN_PROFILE_OPENAI_URL and omits OVERSEER_BRAIN_URL, the profiles request returns that usable profile but selectedProfile remains blank. Because the selector is also hidden when exactly one profile exists, every send omits profile; resolveBrainConfig then checks the missing default configuration and reports that the brain is unconfigured. Initialize the selection from the returned non-default profile, or keep the selector available whenever no default profile exists.

Useful? React with 👍 / 👎.

The local brain serves a 64k-token window, but query_inbox/query_events at
limit=200 return ~75k/~60k tokens — a full dump would overflow the window and
break the turn. Guard the converse loop:

- Cap each flattened tool result at 16k chars (~4k tokens); truncation keeps the
  priority-/recency-ordered head and appends a "narrow your query" note so the
  model re-queries with a smaller limit / filter or uses explain_priority.
- Add a query-narrowly directive to the grounding prompt (small limit, use
  filters, don't dump the whole stream).

Verified live vs the 27B with a 200-item (~75k-token) inbox: no overflow, model
answers from the top-priority head. +3 tests.

Co-authored-by: Cursor <cursoragent@cursor.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: f5b84cf285

ℹ️ 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".

query_inbox: obj({
statuses: { type: 'array', items: { type: 'string' }, description: 'e.g. candidate, surfaced, held.' },
sessionId: sessionIdProp,
category: { type: 'string' },

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 Enumerate the stored inbox categories

When the model filters blockers with a natural value such as category: "blocked", this tool call succeeds but returns no rows because inbox categories are stored and compared case-sensitively as BLOCKED, APPROVAL, etc. The grounding guard can then accept a false empty-inbox answer because it sees a successful tool call. Define this property with an enum derived from INBOX_CATEGORIES so the brain receives the exact accepted values.

Useful? React with 👍 / 👎.

throw new BrainUnavailableError('Overseer brain returned invalid JSON', 'protocol', undefined, error)
}

const message = (json as { choices?: Array<{ message?: OpenAiChatMessage }> })?.choices?.[0]?.message

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 the brain message before returning it

When an endpoint returns valid JSON with a truthy but malformed choices[0].message—for example, an object-valued content or malformed tool_calls—this unchecked cast accepts it, and the converse loop later throws a TypeError while calling .trim() or inspecting calls. That bypasses the intended BrainUnavailableError('protocol') handling and turns a reachable protocol failure into an HTTP 500. Parse the external response with a runtime schema and classify invalid message fields as a protocol error.

AGENTS.md reference: AGENTS.md:L61-L61

Useful? React with 👍 / 👎.

The brain does not need the fat inbox rows — just enough to reason and to
reference an item by id. Project query_inbox on the converse path to
{id, what(title), status, priority}, keeping priority order; drop the
provenance (source events, reasons, artifactRefs, timestamps), which is one
explain_priority call away.

Measured on 174 live inbox items: FULL ~75k tokens (overflows the 64k window)
-> projected ~3.7k tokens. The WHOLE inbox now fits with no truncation, so the
overseer reasons over every item instead of a truncated head. Verified live vs
the 27B: query_inbox -> correct top-N answer over all 174 items. +3 tests.

Projection is converse-only; the HTTP tool endpoint + debug panels keep full
rows. query_events / list_active_workers projections are the obvious next step
(clamp still backstops them).

Co-authored-by: Cursor <cursoragent@cursor.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: 5e64aa750f

ℹ️ 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 (calls.length === 0) {
convo.push(message)
if (toolTrace.length === 0 && !nudged) {

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 Require a successful tool before accepting grounded answers

When the brain's only attempted check fails—such as explain_priority with an invalid item ID—and its next response states fleet facts anyway, toolTrace.length is nonzero, so this condition skips the grounding nudge and returns an answer despite obtaining no data. Base this guard on at least one successful relevant tool call rather than any trace entry.

Useful? React with 👍 / 👎.

Comment thread hub/src/overseer/toolProjection.ts Outdated
export function projectToolResultForBrain(tool: OverseerToolName, result: unknown): unknown {
if (tool === 'query_inbox' && isObj(result) && Array.isArray(result.items)) {
return {
total: result.total,

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 Populate inbox totals from the real tool result

When more inbox items match than the requested limit, the production OverseerEntity.queryInbox() result has items, candidates, surfaced, and held but no total, so this reads undefined and JSON.stringify omits the field entirely. Because the grounding directive encourages limits of 10–25, the brain cannot distinguish a complete result from a truncated page and can report incorrect counts or claim it saw everything; return a real total or an explicit continuation indicator.

Useful? React with 👍 / 👎.

}, ['itemId']),
list_active_workers: obj({
project: { type: 'string' },
state: { type: 'string', enum: [...OVERSEER_WORKER_STATES] },

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 Stop advertising states the roster cannot produce

When the model follows this enum for a question such as the supplied starter “Which agents are blocked?” and calls list_active_workers with state: "blocked", the call succeeds but always returns an empty roster: the implementation filters on observedState, while deriveObservedWorkerState can only produce idle, working, waiting_on_operator, or stale. The same false-empty result affects failed, complete, waiting_on_external, and unknown; either advertise only observable states or filter using the reported/inferred state intended by this enum.

Useful? React with 👍 / 👎.

Live output review: the projected query_inbox reported total:null. The raw
result is {items, candidates, surfaced, held} (four arrays, no `total` field) —
so `total: result.total` was always null, and the model could only cite the
returned count. Derive total from items.length and add cheap segment counts
{candidates, surfaced, held} so the model can frame the inbox
("174 items: 32 candidates, 142 surfaced"). Still ~3.7k tokens.

Co-authored-by: Cursor <cursoragent@cursor.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: b5a76feea0

ℹ️ 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 result = runOverseerTool(overseer, name, args)
toolTrace.push({ tool: name, args, ok: true })
const lean = projectToolResultForBrain(name, result)
resultLines.push(`${name}(${argsRaw}) => ${clampToolResult(JSON.stringify(lean ?? 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 Bound aggregate tool output across the conversation

When the brain issues several tool calls in one round, or continues calling tools across the six iterations, each result can independently contribute 16,000 characters and every prior result remains in convo; three capped results per round can therefore exceed the documented 64k-token context before the final answer. The fresh evidence beyond the earlier uncapped-result report is that the current per-result clamp still has no aggregate conversation budget, so cap or evict tool output across the whole request.

Useful? React with 👍 / 👎.

const argsRaw = call.function?.arguments ?? ''
const args = parseToolArgs(argsRaw)
if (!isOverseerToolName(name)) {
toolTrace.push({ tool: name as never, args, ok: false, error: 'unknown tool' })

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 Represent unknown tool names in the trace contract

When the brain hallucinates an unknown function such as the tested dispatch_now, this as never cast places that arbitrary name in the returned toolTrace, even though the shared OverseerToolTraceEntry.tool contract permits only OverseerToolName. The converse endpoint therefore returns a value that its declared response type says is impossible; widen or discriminate failed unknown-tool entries instead of bypassing the strict type contract.

AGENTS.md reference: AGENTS.md:L57-L57

Useful? React with 👍 / 👎.

… too

Same lean-projection pattern applied to the other two list tools on the converse
path:
- query_events -> {id, ts, type, source, session, attention, what}; drops the
  fat payloadJson / idempotencyKey / artifactRefs / provenance (~61k tokens at
  limit=200 before).
- list_active_workers -> {id, name, project, state, ageMs}; drops flavor + raw
  timestamps.
Both add a `total`. Un-projected tools (get_session_state, get_worker_health,
explain_priority, get_session_recent_output) pass through. +2 tests.

Co-authored-by: Cursor <cursoragent@cursor.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: 1997929f33

ℹ️ 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 +68 to +72
if (tool === 'query_events' && isObj(result) && Array.isArray(result.events)) {
return { total: result.events.length, events: result.events.map(projectEvent) }
}
if (tool === 'list_active_workers' && isObj(result) && Array.isArray(result.workers)) {
return { total: result.workers.length, workers: result.workers.map(projectWorker) }

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 Expose pagination instead of reporting page length as total

When either tool has more matches than its requested limit—which the grounding prompt explicitly encourages setting to 10–25—these branches report only the returned page length as total. For example, 40 matching workers queried with limit: 10 become total: 10, so the brain can incorrectly report a fleet-wide count or claim the result is complete. Return the actual matching count or an explicit hasMore/continuation indicator.

Useful? React with 👍 / 👎.

Comment on lines +165 to +168
return c.json({
reply,
toolTrace: [],
model: config.model,

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 tool calls completed before a brain failure

When an initial brain round successfully invokes a tool but a later completion times out or returns an HTTP/protocol error, the tool has already executed, yet the exception discards the accumulated trace and this response unconditionally claims that no tools ran. That breaks the response's audit contract and hides data reads from the debug client; carry the partial trace through BrainUnavailableError or otherwise return the calls completed before the failure.

Useful? React with 👍 / 👎.

Comment on lines +47 to +51
type: o.eventType,
source: o.sourceKind,
session: o.relatedSessionId ?? o.sourceRef,
attention: o.attentionCandidate,
what: o.summary

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 event severity in the brain projection

When query_events returns mixed severities, this projection removes each event's stored severity even though the tool explicitly advertises severity filtering and the Overseer is expected to prioritize fleet events. A request such as “compare the severity of today's failures” therefore gives the brain no severity values to compare, so any ranking or exact severity claim must be guessed. Keep severity in the lean event representation.

Useful? React with 👍 / 👎.

Comment on lines +49 to +51
session: o.relatedSessionId ?? o.sourceRef,
attention: o.attentionCandidate,
what: o.summary

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 persisted conversation content in event projection

When a cleared, restarted, voice, or XR client asks about an earlier conversation, the brain can query convo_turn events, but this generic projection exposes only the event summary. buildOverseerConvoTurnEventInput stores the prior overseerText, full operator text, and tool calls exclusively in payloadJson, and no other tool retrieves them, so dropping that payload makes the advertised memory-bearing events unable to restore conversation memory. Special-case convo_turn to expose bounded conversation fields.

Useful? React with 👍 / 👎.

Replace the free-text model override in the debug converse panel with a
brain-profile picker (local vs remote) and a model dropdown populated live
from the selected profile's OpenAI-compatible GET /models. The api key for a
remote profile (e.g. OpenAI) stays server-side — the hub proxies the model
list, the browser only ever sees model ids.

- brainClient: listBrainModels() + filterChatModels() (drop embeddings/audio/
  image/etc., keep local ids like "main")
- route: GET /overseer/brains/:id/models (server-side key, filtered ids)
- client: fetchOverseerBrainModels()
- settings panel: id-based profile <select> + live model <select>

Co-authored-by: Cursor <cursoragent@cursor.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: b727f3e057

ℹ️ 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 +130 to +131
} finally {
clearTimeout(timeout)

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 Keep the model-list timeout through body parsing

When the configured brain returns /models headers but stalls while sending the JSON body, this finally clears the 12-second timeout before res.json() consumes the response. The Settings model dropdown can consequently remain loading indefinitely; clear the timer only after the body has been fully read, as is also required for the separate chat-completions request path.

Useful? React with 👍 / 👎.

Comment on lines +30 to +33
messages: z.array(z.object({
role: z.enum(['operator', 'overseer']),
content: z.string().max(8000)
})).min(1).max(40),

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 Bound conversation history by context size

A schema-valid request can contain 40 messages of 8,000 characters each, or roughly 80k tokens for ordinary ASCII text before adding the system prompt and tool schemas. That already exceeds the 64k local-brain context documented in converse.ts, so long pasted turns can make conversation fail before reaching this count limit; trim or reject history using an aggregate character/token budget rather than only per-message and message-count limits.

Useful? React with 👍 / 👎.

name: o.name,
project: o.project,
state: o.observedState,
ageMs: o.ageMs

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 whether roster entries are active

When ended sessions remain in the session cache, OverseerEntity.listActiveWorkers() includes them and supplies an active: false field, but this projection removes that field and exposes only their derived idle state. The brain therefore cannot distinguish an active idle worker from an inactive historical session and can incorrectly include stopped agents when answering roster questions; retain active in the projected worker or filter inactive sessions before projection.

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.

1 participant