-
Notifications
You must be signed in to change notification settings - Fork 0
feat(overseer): query_open_loops — "what am I forgetting?" lens #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/overseer-text-converse
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # Overseer "what am I forgetting?" — cold open-loops lens (implementation) | ||
|
|
||
| > **Branch:** `feat/overseer-open-loops` (stacked on `feat/overseer-text-converse`). | ||
| > **Design + evidence:** [`2026-07-31-overseer-forgotten-open-loops-lens.md`](./2026-07-31-overseer-forgotten-open-loops-lens.md) | ||
| > (validated live against the 27B on `:3006`). This doc = what landed. | ||
|
|
||
| The lens is the **neglect axis** — "what have I abandoned?" — orthogonal to the urgency axis | ||
| ("what needs me now?", `query_inbox` + `explain_priority`). It is self-populating from the | ||
| `AGENT_NOTIFY_SUMMARY` each worker turn already emits; **no operator triage required**. | ||
|
|
||
| ## What landed (build path steps 1 + 2) | ||
|
|
||
| ### Step 2 — `query_open_loops` read-only tool | ||
|
|
||
| An 8th read-only Overseer tool. Definition: | ||
|
|
||
| > **A cold open loop** = a session whose *latest* status-bearing worker event is NOT `done` | ||
| > (`needs_decision` / `needs_review` / `blocked` / `failed` / `stalled`) and was never closed by | ||
| > a later `completed`. | ||
|
|
||
| - **Substrate:** raw `events`, not the coalesced inbox (the design showed events surface ~10 real | ||
| forgotten decisions that never became inbox items). One indexed query takes the latest | ||
| status-bearing worker event **per session** (`progress` is excluded — a progress ping does not | ||
| close an operator-owed decision; `completed` is the only closer). | ||
| - **Strong filter:** `status != done`. A no-op `action` ("none"/"complete"/"n/a"/…) is nulled but | ||
| the loop still surfaces — action text is a *tiebreak*, not the filter (per the spec correction | ||
| that killed 108 Tier-B false positives). | ||
| - **Buckets:** `waiting_on_you` (needs_decision / needs_review — the operator owes a decision) is | ||
| presented **before** `half_finished` (blocked / failed / stalled). Each bucket is **coldest-first**. | ||
| - **Args:** `{ minAgeMs?, bucket?, project?, limit? }` — `minAgeMs` is the "went cold" knob (default 0, | ||
| raise it to focus on genuinely stale threads). | ||
| - **Returns:** `{ openLoops: [{ sessionId, name, project, flavor, status, eventType, eventId, action, | ||
| summary, lastTs, ageMs, ageDays, bucket }], counts: { total, waitingOnYou, halfFinished } }`. | ||
| The brain-facing projection thins this to `{ id, name, project, status, action, what, ageDays, bucket }`. | ||
|
|
||
| ### System-prompt changes (converse/entity layer) | ||
|
|
||
| - **Two questions, two axes** section: urgency (`query_inbox`, priority-ordered) vs neglect | ||
| (`query_open_loops`, age-ordered). Tells the brain which tool answers "what am I forgetting?". | ||
| - **Priority direction fix:** priority is **lower-is-higher** (1 = most important). This corrects the | ||
| 27B's live mistake of calling p50 "highest". | ||
|
|
||
| ## Step 1 — zero-code weekly digest (works today, no substrate change) | ||
|
|
||
| `query_open_loops` makes the "what am I forgetting?" converse prompt reliable. A scheduled weekly | ||
| digest can send this to `POST /api/overseer/converse` and post the reply once (never an interrupt): | ||
|
|
||
| ``` | ||
| What have I forgotten or abandoned? Use query_open_loops (minAgeMs = 3 days). Lead with the | ||
| "Waiting on You" bucket — decisions I owe — then half-finished work. For each, one line: what it is, | ||
| how many days cold, and the concrete next step (skip ones with no real next step). Do not rank by | ||
| priority; this is about neglect, not urgency. Keep it to the top ~15. | ||
| ``` | ||
|
|
||
| This doubles as a **triage bootstrap**: surface ~15 cold loops, operator dispositions them, and the | ||
| inbox disposition loop the spec always wanted becomes tractable. | ||
|
|
||
| ## Ingest-peer handoff items (PR #99 → this layer) | ||
|
|
||
| - **H1 (done):** system-prompt rule — when the operator asks about a *specific* inbox item, the brain | ||
| first calls `explain_priority` then `query_events{sessionId}` to pull the rest of that session's | ||
| recorded activity as salience (capability already existed; `sessionId` is an accepted arg). | ||
| - **H2 (done):** a two-level `detail: 'lean' | 'full'` knob (default `lean`) on every context tool | ||
| (`query_events` / `query_inbox` / `get_session_state` / `get_session_recent_output` / | ||
| `get_worker_health` / `list_active_workers` / `query_open_loops`), threaded into | ||
| `projectToolResultForBrain(tool, result, detail)`. Coverage gap closed: `get_session_state`, | ||
| `get_session_recent_output` (raw terminal text capped at 280 chars in lean — was a token bomb), and | ||
| `get_worker_health` (signal trail dropped in lean) now have lean projections. `full` returns the raw | ||
| rows, still bounded by `limit`/`n` and the outer char clamp. Deliberately NOT a token-budget engine — | ||
| two levels + good defaults. | ||
| - **H3 (deferred):** `query_session_actions` reader over `inbox_operator_actions` — deferred until | ||
| disposition volume justifies it (~0 today). ~30-line add when wanted. | ||
| - **H4 (confirmed):** `query_open_loops` spans **all non-deleted sessions** (active AND archived). It | ||
| reads only the events table and never filters on `session.active`; deleted sessions drop out because | ||
| `deleteSession` detaches their events (`related_session_id = NULL`). | ||
|
|
||
| ## Not in this branch (follow-ups) | ||
|
|
||
| - **Archiving hygiene (step 3):** aggressive session archive + sweeping legacy `stale` | ||
| ("No agent output for 30 minutes") rows that predate the fix which stopped writing them | ||
| (`checkStaleSessions` already returns `[]`; those legacy rows are `source_kind=system` so | ||
| `query_open_loops` — worker-only — already excludes them from the lens, but they still bloat | ||
| Session Logs). Coordinate the sweep with the inbox-ingest lane. | ||
| - **Dependency:** inbox PR-title + priority-band fix (PR #99) lands on the urgency axis, independent | ||
| of this lens. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -294,6 +294,34 @@ export function queryEvents(db: Database, options: QueryEventsOptions = {}): Sto | |
| return rows.map(mapRow) | ||
| } | ||
|
|
||
| /** | ||
| * Latest status-bearing worker event per session — the substrate for the | ||
| * cold-open-loops lens. "Status-bearing" = the notify-derived types that either | ||
| * open a loop (needs_decision/needs_review/blocked/failed/stale) or close it | ||
| * (completed); `progress` is intentionally excluded so a progress ping does not | ||
| * mask an unanswered decision. The caller decides open vs closed by inspecting | ||
| * the returned event's type. Bounded and index-friendly (one row per session). | ||
| */ | ||
| export function queryLatestWorkerStatusPerSession(db: Database, limit = 500): StoredSystemEvent[] { | ||
| const cap = Math.min(Math.max(limit, 1), 2000) | ||
| const rows = db.prepare(` | ||
| SELECT e.* FROM events e | ||
| JOIN ( | ||
| SELECT related_session_id AS sid, MAX(id) AS max_id | ||
| FROM events | ||
| WHERE source_kind = 'worker' | ||
| AND related_session_id IS NOT NULL | ||
| AND event_type IN ( | ||
| 'needs_decision', 'needs_review', 'blocked', 'failed', 'stale', 'completed' | ||
|
Comment on lines
+312
to
+315
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a session ends with Useful? React with 👍 / 👎. |
||
| ) | ||
| GROUP BY related_session_id | ||
| ) latest ON e.id = latest.max_id | ||
| ORDER BY e.ts ASC | ||
| LIMIT ? | ||
|
Comment on lines
+319
to
+320
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Once the database contains more than 500 sessions with status-bearing events, this selects only the 500 oldest per-session statuses before the caller removes Useful? React with 👍 / 👎. |
||
| `).all(cap) as SystemEventRow[] | ||
| return rows.map(mapRow) | ||
| } | ||
|
|
||
| export function insertEventLink( | ||
| db: Database, | ||
| input: { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a worker chunk exceeds 280 characters, the default lean projection retains only its prefix. This repository's agent contract requires the status, action, and summary line at the end of every response (
shared/src/overseerEvents.ts:14-18), soget_session_recent_outputnow routinely hides both that machine summary and often the worker's conclusion, causing the Overseer to answer from incomplete context unless the model happens to requestdetail: "full". Preserve a head-and-tail slice or explicitly retain the final summary line within the same cap.Useful? React with 👍 / 👎.