feat(overseer): text conversation core + debug-settings surface (Stage 0) - #98
feat(overseer): text conversation core + debug-settings surface (Stage 0)#98heavygee wants to merge 9 commits into
Conversation
…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>
There was a problem hiding this comment.
💡 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".
| const { reply, toolTrace } = await runOverseerConverse({ | ||
| overseer: engine.getOverseer(), | ||
| config, | ||
| messages |
There was a problem hiding this comment.
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 👍 / 👎.
| query_inbox: obj({ | ||
| statuses: { type: 'array', items: { type: 'string' }, description: 'e.g. candidate, surfaced, held.' }, | ||
| sessionId: sessionIdProp, |
There was a problem hiding this comment.
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 👍 / 👎.
| setTurns((prev) => [...prev, { role: 'operator', content: trimmed }]) | ||
| setLoading(true) | ||
| try { |
There was a problem hiding this comment.
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 👍 / 👎.
| } finally { | ||
| clearTimeout(timeout) |
There was a problem hiding this comment.
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 👍 / 👎.
| const parsed: unknown = JSON.parse(raw) | ||
| return parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : {} | ||
| } catch { | ||
| return {} |
There was a problem hiding this comment.
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 👍 / 👎.
| role: z.enum(['operator', 'overseer']), | ||
| content: z.string().max(8000) | ||
| })).min(1).max(40), | ||
| relatedSessionId: z.string().min(1).optional() |
There was a problem hiding this comment.
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 👍 / 👎.
| if (!res.ok) { | ||
| const body = await res.text().catch(() => '') | ||
| throw new BrainUnavailableError(`Overseer brain returned ${res.status}: ${body.slice(0, 200)}`) |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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 } |
There was a problem hiding this comment.
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 👍 / 👎.
| const result = runOverseerTool(overseer, name, args) | ||
| toolTrace.push({ tool: name, args, ok: true }) | ||
| resultLines.push(`${name}(${argsRaw}) => ${JSON.stringify(result ?? null)}`) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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.` | ||
| }) |
There was a problem hiding this comment.
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 👍 / 👎.
| toolCalls: toolTrace | ||
| .filter((t) => t.ok) | ||
| .map((t) => ({ tool: t.tool, argsSummary: JSON.stringify(t.args).slice(0, 500) })) |
There was a problem hiding this comment.
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 👍 / 👎.
| const { reply, toolTrace } = await runOverseerConverse({ | ||
| overseer: engine.getOverseer(), | ||
| config, | ||
| messages | ||
| }) |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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('') |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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' }, |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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) { |
There was a problem hiding this comment.
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 👍 / 👎.
| export function projectToolResultForBrain(tool: OverseerToolName, result: unknown): unknown { | ||
| if (tool === 'query_inbox' && isObj(result) && Array.isArray(result.items)) { | ||
| return { | ||
| total: result.total, |
There was a problem hiding this comment.
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] }, |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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))}`) |
There was a problem hiding this comment.
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' }) |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| 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) } |
There was a problem hiding this comment.
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 👍 / 👎.
| return c.json({ | ||
| reply, | ||
| toolTrace: [], | ||
| model: config.model, |
There was a problem hiding this comment.
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 👍 / 👎.
| type: o.eventType, | ||
| source: o.sourceKind, | ||
| session: o.relatedSessionId ?? o.sourceRef, | ||
| attention: o.attentionCandidate, | ||
| what: o.summary |
There was a problem hiding this comment.
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 👍 / 👎.
| session: o.relatedSessionId ?? o.sourceRef, | ||
| attention: o.attentionCandidate, | ||
| what: o.summary |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| } finally { | ||
| clearTimeout(timeout) |
There was a problem hiding this comment.
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 👍 / 👎.
| messages: z.array(z.object({ | ||
| role: z.enum(['operator', 'overseer']), | ||
| content: z.string().max(8000) | ||
| })).min(1).max(40), |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 👍 / 👎.
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/converseendpoint 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
buildOverseerOpenAiTools()— the 7 read-only tools as OpenAI function schemas, hand-mapped from the existing zod arg schemas (no new dependency).brainClient— OpenAI/chat/completionsclient with a typedBrainUnavailableError; env configOVERSEER_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-bearingconvo_turnper exchange (attention 0, never an inbox item) and degrades gracefully tobrainOnline:falsewith a friendly message when the brain is unreachable (its GPU is shared with other workloads).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 andquery_inboxwas 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 typecheckclean across shared/hub/webquery_inboxafter the grounding fixquery_inboxtrace