pantheon-opencode 1.5.0-beta.13 → 1.5.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/package.json +1 -1
  3. package/plugin.json +1 -1
  4. package/pyproject.toml +2 -5
  5. package/src/agents/zeus.md +4 -26
  6. package/src/pantheon/background-job-board.ts +4 -4
  7. package/src/pantheon/delegate-manager.ts +95 -10
  8. package/src/pantheon/delegation-classifier.ts +3 -4
  9. package/src/pantheon/delegation-finalize.ts +69 -7
  10. package/src/pantheon/delegation.ts +16 -4
  11. package/src/pantheon/presets.d.mts +0 -20
  12. package/src/pantheon/presets.mjs +0 -64
  13. package/src/pantheon/toon-codec.ts +1 -1
  14. package/src/plugin-v2.ts +4 -6
  15. package/src/plugin.ts +6 -6
  16. package/src/plugins/pantheon-hooks.ts +1 -1
  17. package/src/plugins/tui/dist/tui.d.ts +57 -40
  18. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  19. package/src/plugins/tui/dist/tui.js +145 -54
  20. package/src/plugins/tui/package-lock.json +2 -2
  21. package/src/plugins/tui/package.json +1 -1
  22. package/src/plugins/tui/src/index.tsx +245 -83
  23. package/src/routing.yml +0 -16
  24. package/scripts/hash_verify.py +0 -192
  25. package/scripts/test-context-compression.sh +0 -169
  26. package/src/pantheon/auto-wake.ts +0 -85
  27. package/src/pantheon/chat-reminders.ts +0 -9
  28. package/src/pantheon/comment-checker.ts +0 -48
  29. package/src/pantheon/compaction-assert.ts +0 -160
  30. package/src/pantheon/compaction-handoff.ts +0 -304
  31. package/src/pantheon/cost-tracker.ts +0 -108
  32. package/src/pantheon/dispatch-guard.ts +0 -109
  33. package/src/pantheon/permission-eval.ts +0 -284
  34. package/src/pantheon/preemptive-compact.ts +0 -76
  35. package/src/pantheon/provider-capabilities.ts +0 -221
  36. package/src/pantheon/provider-stream-tracker.ts +0 -182
  37. package/src/pantheon/retry-policy.ts +0 -212
  38. package/src/pantheon/token-opt.ts +0 -689
  39. package/src/pantheon/v2-events.ts +0 -148
  40. package/src/pantheon/v2-hooks.ts +0 -287
  41. package/src/pantheon/v2-tool-definitions.ts +0 -331
  42. package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
@@ -906,16 +906,17 @@ async function setupUsageBar(api: TuiPluginApi) {
906
906
  * with a board row for the same child (a child WITH a report keeps the
907
907
  * md entry, source 'md').
908
908
  *
909
- * 2. ENRICHMENT + HISTORY — markdown reports the job board persists under
909
+ * 2. ENRICHMENT — markdown reports the job board persists under
910
910
  * `.pantheon/delegations/<sessionID>/<alias>.md` (written by
911
911
  * src/pantheon/delegation-finalize.ts at terminal state). Each child
912
912
  * is matched to its report by the `Task ID` header (== child session
913
913
  * id == board task id), which supplies the alias (apo-N), agent,
914
914
  * description and terminal duration/timedOut. No report → the child
915
915
  * itself still renders (title as description, derived state). The md
916
- * channel is ALSO read from EVERY session (readAllDelegationEntries) —
917
- * the panel shows the full delegation history even without a focused
918
- * session.
916
+ * channel is read from EVERY session (readAllDelegationEntries) as an
917
+ * enrichment source, but the ACTIVE-SESSION FILTER
918
+ * (filterDelegationsToSession) drops every non-focused report — the panel
919
+ * lists only the focused session.
919
920
  *
920
921
  * 3. REFRESH — session.created/updated/deleted/status events and
921
922
  * message.part.updated/removed (via liveStore.version bumps) re-fetch
@@ -930,8 +931,8 @@ async function setupUsageBar(api: TuiPluginApi) {
930
931
  * A children fetch
931
932
  * failure logs "panel: error <msg>" (previously silenced), and a missing
932
933
  * current session (null/placeholder sessionID) logs "panel: children=0
933
- * md=N ... (no sessionID — history shown)" — the md HISTORY renders
934
- * instead of erroring.
934
+ * md=N ... (no sessionID — inactive panel)" — the panel stays EMPTY
935
+ * instead of erroring (no cross-session history).
935
936
  *
936
937
  * 5. SESSIONID GUARD (placeholder regression) — the runtime may hand the
937
938
  * sidebar the UNSUBSTITUTED template literal "{sessionID}" as its
@@ -941,8 +942,8 @@ async function setupUsageBar(api: TuiPluginApi) {
941
942
  * panel into "(0)". resolveCurrentSessionID (slot prop → api.state →
942
943
  * route.current) never yields a placeholder (isValidSessionId rejects
943
944
  * "{sessionID}" and "%7BsessionID%7D" via the startsWith("ses")
944
- * contract); a null resolution SKIPS the children fetch entirely and
945
- * renders the md history — zero errors.
945
+ * contract); a null resolution SKIPS the children fetch entirely and the
946
+ * panel renders EMPTY — zero rows, zero errors.
946
947
  *
947
948
  * The live tool-part lifecycle helpers below remain (tested, exported) but
948
949
  * are no longer the source of truth: they feed a version signal that merely
@@ -1161,11 +1162,12 @@ export async function readDelegationEntries(dir: string): Promise<DelegationEntr
1161
1162
 
1162
1163
  /** Read delegation reports from EVERY session under
1163
1164
  * `<root>/.pantheon/delegations/<sessionID>/<alias>.md` — the panel's
1164
- * HISTORY channel. Unlike the children/live channels it does NOT depend on a
1165
- * resolved sessionID: with no focused session (null/placeholder), the panel
1166
- * still shows the reports from all past sessions (running first, Finalized
1167
- * desc — the sort applied by readDelegationEntries). Fail-open: a
1168
- * missing/unreadable directory yields []. */
1165
+ * ENRICHMENT source. The read is deliberately unfiltered (each entry carries
1166
+ * its directory's sessionID, so a taskID match can enrich any channel);
1167
+ * callers MUST scope the merged result with {@link filterDelegationsToSession}
1168
+ * before rendering — only the active session's reports are shown. Entries are
1169
+ * running first, Finalized desc (the sort applied by readDelegationEntries).
1170
+ * Fail-open: a missing/unreadable directory yields []. */
1169
1171
  export async function readAllDelegationEntries(root: string): Promise<DelegationEntry[]> {
1170
1172
  return readDelegationEntries(join(root, '.pantheon', 'delegations'))
1171
1173
  }
@@ -1320,7 +1322,22 @@ export function splitDelegationList(
1320
1322
  const active = all
1321
1323
  .filter((d) => isActiveState(d.state))
1322
1324
  .map((d) => markStaleIfRunning(d, now, staleThresholdMs))
1323
- const terminal = all.filter((d) => !isActiveState(d.state))
1325
+ .sort(compareDelegationEntries)
1326
+ const terminal = all
1327
+ .filter((d) => !isActiveState(d.state))
1328
+ .filter((d) => {
1329
+ // A missing/invalid finalized timestamp is not evidence that a report is
1330
+ // old. Keep it visible rather than accidentally hiding a terminal job.
1331
+ if (d.updatedAt === null || !Number.isFinite(d.updatedAt)) return true
1332
+ const age = now - d.updatedAt
1333
+ if (age < 0) return true
1334
+ const retention =
1335
+ delegationRowStatus(d.state) === 'failed'
1336
+ ? DELEGATION_FAILED_RETENTION_MS
1337
+ : DELEGATION_DONE_RETENTION_MS
1338
+ return age < retention
1339
+ })
1340
+ .sort(compareDelegationEntries)
1324
1341
  return {
1325
1342
  active,
1326
1343
  recent: terminal.slice(0, maxRecent),
@@ -1348,6 +1365,17 @@ export const STALE_RUNNING_THRESHOLD_MS = 30 * 60 * 1000
1348
1365
  * display-only `stale-running` state. */
1349
1366
  export const IDLE_SILENCE_MS = 60 * 1000
1350
1367
 
1368
+ /** Visual-only terminal retention windows. Reports remain on disk and in the
1369
+ * board; these constants only control which rows enter the TUI window. */
1370
+ export const DELEGATION_DONE_RETENTION_MS = 2 * 60 * 1000
1371
+ export const DELEGATION_FAILED_RETENTION_MS = 10 * 60 * 1000
1372
+
1373
+ /** Alias-less NATIVE task() live entries never receive a board alias (the
1374
+ * task tool output carries none), so the 30s alias-less prune in
1375
+ * mergeChildDelegationSources must not apply to them — 5 minutes covers a
1376
+ * slow child listing while still bounding the live map. */
1377
+ export const NATIVE_LIVE_ALIASLESS_TTL_MS = 5 * 60 * 1000
1378
+
1351
1379
  /**
1352
1380
  * Mark a running entry as `stale-running` if it has been running longer than
1353
1381
  * the threshold AND has no recent activity (no `updatedAt` change in the last
@@ -1452,26 +1480,26 @@ export type DelegationDisplayState = DelegationEntry['state']
1452
1480
 
1453
1481
  /** Semantic tone mapped to the TUI theme at the row ({@link DelegationRow}).
1454
1482
  * Kept separate + pure so the color channel is testable without booting the
1455
- * renderer. */
1456
- export type DelegationStateTone = 'warning' | 'error' | 'success' | 'muted'
1483
+ * renderer. Every display state resolves to one of the three status colors;
1484
+ * the row paints its whole content with it (see {@link DelegationRow}). */
1485
+ export type DelegationStateTone = 'warning' | 'error' | 'success'
1457
1486
 
1458
- /** Color is the ONLY channel separating `stale-running` from a live `running`
1459
- * row — the 4-glyph row design has no dedicated stale shape. Display-only;
1460
- * no behavior change. */
1487
+ /** Status → color mapping for the whole-row tone. Red = failure, green =
1488
+ * terminal, yellow = in flight. The glyph remains a redundant channel so the
1489
+ * state stays legible in monochrome. Display-only; no behavior change. */
1461
1490
  export function delegationStateTone(state: DelegationDisplayState): DelegationStateTone {
1462
1491
  switch (state) {
1463
1492
  case 'running':
1464
1493
  case 'retry':
1465
1494
  case 'startup_unknown':
1466
- return 'warning'
1467
1495
  case 'stale-running':
1496
+ return 'warning'
1468
1497
  case 'error':
1469
1498
  case 'startup_failed':
1470
1499
  return 'error'
1471
1500
  case 'completed':
1472
- return 'success'
1473
1501
  case 'cancelled':
1474
- return 'muted'
1502
+ return 'success'
1475
1503
  }
1476
1504
  }
1477
1505
 
@@ -1575,10 +1603,13 @@ export function mergeChildDelegationSources(
1575
1603
  })
1576
1604
 
1577
1605
  for (const liveEntry of live) {
1606
+ // A native task() live part never carries a board alias or taskID, so it
1607
+ // is exempt from the 30s alias-less prune and lives on its own longer TTL.
1608
+ const aliaslessTTL = liveEntry.tool === 'task' ? NATIVE_LIVE_ALIASLESS_TTL_MS : 30_000
1578
1609
  if (
1579
1610
  liveEntry.alias === null &&
1580
1611
  liveEntry.taskID === null &&
1581
- Date.now() - liveEntry.startedAt > 30_000
1612
+ Date.now() - liveEntry.startedAt > aliaslessTTL
1582
1613
  )
1583
1614
  continue
1584
1615
  const incoming = toDelegationEntry(liveEntry)
@@ -1588,10 +1619,12 @@ export function mergeChildDelegationSources(
1588
1619
  // children-only one. pantheon_delegate pending parts keep source 'live'.
1589
1620
  const isNativeLive = liveEntry.tool === 'task' && liveEntry.alias === null
1590
1621
  if (isNativeLive) {
1591
- // Native task() rows carry no board alias — reuse the synthetic
1592
- // 'native-task' marker so the single identity falls back to the agent.
1622
+ // Native task() rows carry no board alias — keep the per-call
1623
+ // `live-<callID>` identity from toDelegationEntry (one row per native
1624
+ // call) with the children-only provenance; the row identity still falls
1625
+ // back to the agent via delegationRowIdentity. A taskID-known native
1626
+ // still merges into its child row through the byTask lookup below.
1593
1627
  incoming.source = 'children-only'
1594
- incoming.alias = 'native-task'
1595
1628
  }
1596
1629
  const index =
1597
1630
  (incoming.taskID !== undefined ? byTask.get(incoming.taskID) : undefined) ??
@@ -2092,10 +2125,35 @@ export function boardRecordsToDelegationEntries(
2092
2125
  return out
2093
2126
  }
2094
2127
 
2128
+ /** Active (non-terminal) display states — a job in one of these is still live. */
2129
+ function isActiveDelegationState(state: DelegationEntry['state']): boolean {
2130
+ return state === 'running' || state === 'retry' || state === 'stale-running'
2131
+ }
2132
+
2133
+ /** Temporal guard for the board merge (recycled-alias regression): board
2134
+ * aliases recycle across jobs (apo-1 of a previous job), so a board
2135
+ * terminal record only closes a running entry when its Finalized timestamp
2136
+ * is STRICTLY NEWER than the entry it would close. Older/equal/missing
2137
+ * keeps the live entry — when in doubt the live job wins. Pure. */
2138
+ function shouldKeepRunningOverBoardTerminal(
2139
+ existing: DelegationEntry | undefined,
2140
+ incoming: DelegationEntry,
2141
+ ): boolean {
2142
+ if (existing === undefined) return false
2143
+ if (!isActiveDelegationState(existing.state)) return false
2144
+ if (isActiveDelegationState(incoming.state)) return false
2145
+ const incomingTs = incoming.updatedAt
2146
+ if (incomingTs === null) return true
2147
+ const existingTs = existing.updatedAt ?? existing.startedAt
2148
+ return incomingTs <= existingTs
2149
+ }
2150
+
2095
2151
  /** Merge the board over any other channel. The board is AUTHORITATIVE for
2096
- * state/alias/agent (it is the persisted FSM) and it is the only channel
2097
- * that carries jobs from OTHER sessions. Dedup by taskID first, then by
2098
- * (sessionID, alias) — aliases are per parent session. Pure. */
2152
+ * state/alias/agent (it is the persisted FSM). This merge is intentionally
2153
+ * UNSCOPED so the board can enrich an active-session row (dedup by taskID);
2154
+ * the caller scopes the merged list with {@link filterDelegationsToSession},
2155
+ * which is what keeps other-session board jobs out of the panel. Dedup by
2156
+ * taskID first, then by (sessionID, alias) — aliases are per parent session. Pure. */
2099
2157
  export function mergeBoardDelegationSources(
2100
2158
  base: readonly DelegationEntry[],
2101
2159
  board: readonly DelegationEntry[],
@@ -2120,6 +2178,10 @@ export function mergeBoardDelegationSources(
2120
2178
  bySessionAlias.set(key(incoming.sessionID, incoming.alias), next)
2121
2179
  continue
2122
2180
  }
2181
+ // Temporal guard (recycled-alias regression): a STALE board terminal must
2182
+ // never downgrade a live running entry — skip the board record entirely
2183
+ // and keep the live row (same job alias, previous incarnation).
2184
+ if (shouldKeepRunningOverBoardTerminal(out[index], incoming)) continue
2123
2185
  out[index] = {
2124
2186
  ...incoming,
2125
2187
  // Session preservation: the board is authoritative for state/alias/agent,
@@ -2134,6 +2196,30 @@ export function mergeBoardDelegationSources(
2134
2196
  return out
2135
2197
  }
2136
2198
 
2199
+ /** Scope a fully-merged display list to the ACTIVE session only.
2200
+ *
2201
+ * The panel is session-scoped: rows from other sessions (board snapshot, md
2202
+ * history under `.pantheon/delegations/<other-session>/`) and rows with no
2203
+ * attributable session (empty sessionID) are DROPPED. The previous
2204
+ * cross-session behavior rendered those rows and clicking them led to
2205
+ * "Session not found" — there is no cross-session channel anymore.
2206
+ *
2207
+ * The board/md channels still feed the merge UNFILTERED so the board can
2208
+ * ENRICH an active-session row with state/alias/agent (dedup by taskID), but
2209
+ * they can never introduce a row for another session: this filter is applied
2210
+ * ONCE, after every merge (children + live + md + board).
2211
+ *
2212
+ * Returns [] when no active session resolves (null/placeholder) — there is no
2213
+ * scope to show. Native children always carry the active session id (stamped
2214
+ * by `childrenToDelegationEntries`), so they survive the filter. Pure. */
2215
+ export function filterDelegationsToSession(
2216
+ entries: readonly DelegationEntry[],
2217
+ activeSessionID: string | null | undefined,
2218
+ ): DelegationEntry[] {
2219
+ if (!isValidSessionId(activeSessionID)) return []
2220
+ return entries.filter((entry) => entry.sessionID === activeSessionID)
2221
+ }
2222
+
2137
2223
  /* ─── Children channel (primary source, delegations-sidebar pattern) ────
2138
2224
  * `api.client.session.children` returns the child sessions of the current
2139
2225
  * session — every pantheon_delegate spawns one (parentID = caller). The
@@ -2305,10 +2391,13 @@ export function delegationRowMarker(state: DelegationDisplayState, now = Date.no
2305
2391
  }
2306
2392
 
2307
2393
  /** ONE identity per row: the short board alias (`apo-1`); a row without a
2308
- * board alias (native task() child, alias `native-task`) shows the agent
2309
- * instead — never alias + agent stacked. Pure. */
2394
+ * board alias (native task() child `native-task` / `native-<id>`, or an
2395
+ * alias-less native live row `live-<callID>` kept as children-only) shows
2396
+ * the agent instead — never alias + agent stacked. Pure. */
2310
2397
  export function delegationRowIdentity(entry: DelegationEntry): string {
2311
- if (entry.alias === 'native-task' && entry.agent !== '') return entry.agent
2398
+ if (entry.agent === '') return entry.alias
2399
+ if (entry.alias === 'native-task' || entry.alias.startsWith('native-')) return entry.agent
2400
+ if (entry.alias.startsWith('live-') && entry.source === 'children-only') return entry.agent
2312
2401
  return entry.alias
2313
2402
  }
2314
2403
 
@@ -2410,16 +2499,23 @@ export function formatDelegationHeader(entries: readonly DelegationEntry[]): str
2410
2499
  return `(${active} active · ${done} done${tail})`
2411
2500
  }
2412
2501
 
2413
- /** Cap the panel: live rows first, then most-recent terminal rows, at most
2414
- * `maxVisible` total. `hidden` is how many were dropped (rendered as
2415
- * "… +N more"). Pure. */
2502
+ /** Cap the panel: live rows first, then most-recent retained terminal rows, at
2503
+ * most `maxVisible` total. Hidden counts describe the retained render list,
2504
+ * not expired history. */
2416
2505
  export function ceilingDelegationList(
2417
2506
  all: readonly DelegationEntry[],
2418
2507
  maxVisible = DELEGATION_VISIBLE_CEILING,
2419
2508
  now = Date.now(),
2420
- ): { visible: DelegationEntry[]; hidden: number } {
2421
- const visible = visibleDelegationList(all, maxVisible, now).slice(0, maxVisible)
2422
- return { visible, hidden: Math.max(0, all.length - visible.length) }
2509
+ ): { visible: DelegationEntry[]; hidden: number; hiddenActive: number; hiddenTerminal: number } {
2510
+ const { active, recent } = splitDelegationList(all, all.length, now)
2511
+ const visibleActive = active.slice(0, maxVisible)
2512
+ const visible = [
2513
+ ...visibleActive,
2514
+ ...recent.slice(0, Math.max(0, maxVisible - visibleActive.length)),
2515
+ ]
2516
+ const hiddenActive = Math.max(0, active.length - visibleActive.length)
2517
+ const hiddenTerminal = Math.max(0, recent.length - (visible.length - visibleActive.length))
2518
+ return { visible, hidden: hiddenActive + hiddenTerminal, hiddenActive, hiddenTerminal }
2423
2519
  }
2424
2520
 
2425
2521
  /** Split a display list into native task() rows vs pantheon_delegate rows.
@@ -2456,11 +2552,13 @@ export function formatPanelLogLine(
2456
2552
  * itself (fallback 'agent'), state derived from its status, startedAt from
2457
2553
  * time.created. A report-less child is a NATIVE task() child (every
2458
2554
  * child of the current session — pantheon_delegate OR the native `task()`
2459
- * tool — carries parentID = caller), so it gets source 'children-only',
2460
- * the internal alias 'native-task' instead of a
2461
- * board alias. The 'task nativa' description fallback keeps the row
2462
- * non-empty when the child carries no title.
2463
- * Terminal md state wins over the derived state; a running md defers to
2555
+ * tool — carries parentID = caller), so it gets source 'children-only', a
2556
+ * per-child alias 'native-<last4 of the child id>' (one identity per native
2557
+ * row) instead of a board alias. The 'task nativa' description fallback
2558
+ * keeps the row non-empty when the child carries no title.
2559
+ * A FRESH terminal md state wins over the derived state (a stale md report
2560
+ * from a previous incarnation never flips a live child — see
2561
+ * mdTerminalCoversChild); a running md defers to
2464
2562
  * the child's live status. A running child is NEVER archived — it always
2465
2563
  * lands in the active split (splitDelegationList active = running/retry).
2466
2564
  * `parentSessionID` (the focused session id) is stamped on report-less
@@ -2468,6 +2566,22 @@ export function formatPanelLogLine(
2468
2566
  * unscoped ('').
2469
2567
  * Sorted running-first (compareDelegationEntries).
2470
2568
  * Pure — no I/O. */
2569
+ /** Temporal guard for the children channel (same recycled-alias class as the
2570
+ * board merge): an md terminal report matched by taskID may still belong to
2571
+ * a PREVIOUS job incarnation. Accept it only when its Finalized timestamp
2572
+ * covers the child's latest activity, when either side has no timestamp to
2573
+ * compare, or when both startedAt agree on a single job incarnation.
2574
+ * Pure — no I/O. */
2575
+ function mdTerminalCoversChild(md: DelegationEntry, child: ChildDelegationLike): boolean {
2576
+ const childUpdated = child.time?.updated
2577
+ if (md.updatedAt !== null && childUpdated !== undefined) {
2578
+ if (md.updatedAt >= childUpdated) return true
2579
+ // Stale Finalized — same incarnation only if both startedAt agree.
2580
+ return md.startedAt === child.time?.created
2581
+ }
2582
+ return true
2583
+ }
2584
+
2471
2585
  export function childrenToDelegationEntries(
2472
2586
  children: readonly ChildDelegationLike[] | undefined,
2473
2587
  md: readonly DelegationEntry[],
@@ -2485,11 +2599,11 @@ export function childrenToDelegationEntries(
2485
2599
  seen.add(child.id)
2486
2600
  const mdEntry = byTaskID.get(child.id)
2487
2601
  const state =
2488
- mdEntry !== undefined && mdEntry.state !== 'running'
2602
+ mdEntry !== undefined && mdEntry.state !== 'running' && mdTerminalCoversChild(mdEntry, child)
2489
2603
  ? mdEntry.state
2490
2604
  : childStatusToState(child.status)
2491
2605
  out.push({
2492
- alias: mdEntry?.alias ?? 'native-task',
2606
+ alias: mdEntry?.alias ?? `native-${child.id.slice(-4)}`,
2493
2607
  // A report-less child IS a child of the focused session
2494
2608
  // (session.children is session-scoped), so the caller-passed
2495
2609
  // parentSessionID is stamped on it as its origin session; an md
@@ -2522,12 +2636,41 @@ export function childrenToDelegationEntries(
2522
2636
  * server-valid session id ("ses...") ever reaches the router, so an
2523
2637
  * unsubstituted "{sessionID}" placeholder can never be routed. */
2524
2638
  export function navigateToDelegationSession(
2525
- route: { navigate?: (name: string, params?: Record<string, unknown>) => void } | undefined,
2639
+ route:
2640
+ | {
2641
+ navigate?: (name: string, params?: Record<string, unknown>) => void | PromiseLike<void>
2642
+ }
2643
+ | undefined,
2526
2644
  taskID: string | undefined,
2527
2645
  ): boolean {
2528
2646
  if (typeof route?.navigate !== 'function' || !isValidSessionId(taskID)) return false
2529
- route.navigate('session', { sessionID: taskID })
2530
- return true
2647
+ try {
2648
+ const navigation = route.navigate('session', { sessionID: taskID })
2649
+ if (navigation !== undefined) {
2650
+ // A removed session can reject route.navigate for historical rows. That
2651
+ // rejection is expected and intentionally absorbed so it cannot break
2652
+ // the TUI; the no-op avoids logging sensitive session details.
2653
+ void Promise.resolve(navigation).catch(() => undefined)
2654
+ }
2655
+ return true
2656
+ } catch {
2657
+ // Synchronous router failures are also fail-open for an inert historical row.
2658
+ return false
2659
+ }
2660
+ }
2661
+
2662
+ /** Build the mouse handler used by each delegation row. */
2663
+ export function createDelegationRowOpenHandler(
2664
+ route:
2665
+ | {
2666
+ navigate?: (name: string, params?: Record<string, unknown>) => void | PromiseLike<void>
2667
+ }
2668
+ | undefined,
2669
+ taskID: string | undefined,
2670
+ ): () => void {
2671
+ return () => {
2672
+ navigateToDelegationSession(route, taskID)
2673
+ }
2531
2674
  }
2532
2675
 
2533
2676
  /* ─── Silence-by-default panel logger ──────────────────────
@@ -2643,9 +2786,10 @@ function DelegationRow(props: {
2643
2786
  }) {
2644
2787
  const theme = () => props.api.theme.current
2645
2788
 
2646
- // Color is a SEPARATE channel from the glyph: the lead (glyph + identity)
2647
- // carries the state tone, while elapsed + description stay muted so the
2648
- // state reads at a glance without shouting.
2789
+ // Color is a status-wide channel: the WHOLE row (lead, elapsed, description,
2790
+ // live activity) carries the state tone, so red=failed / green=done /
2791
+ // yellow=in-flight reads at a glance. The glyph stays as a redundant,
2792
+ // monochrome-legible signal — never the only one.
2649
2793
  const tone = createMemo(() => {
2650
2794
  const t = theme()
2651
2795
  switch (delegationStateTone(props.job.state)) {
@@ -2656,7 +2800,7 @@ function DelegationRow(props: {
2656
2800
  case 'success':
2657
2801
  return t.success
2658
2802
  default:
2659
- return t.textMuted // muted (cancelled)
2803
+ return t.textMuted // defensive: unknown runtime state
2660
2804
  }
2661
2805
  })
2662
2806
 
@@ -2674,27 +2818,25 @@ function DelegationRow(props: {
2674
2818
  // Click a row → navigate to the child session (route API guarded in the
2675
2819
  // helper). Enter-key binding is left out (per-row keymap wiring is not
2676
2820
  // worth the complexity in this sidebar); mouse is enabled in tui.json.
2677
- const open = () => {
2678
- navigateToDelegationSession(props.api.route, props.job.taskID)
2679
- }
2821
+ const open = createDelegationRowOpenHandler(props.api.route, props.job.taskID)
2680
2822
 
2681
2823
  return (
2682
2824
  <box onMouseDown={open}>
2683
- {/* Line 1: colored lead (glyph + identity) + muted elapsed tail. */}
2825
+ {/* Line 1: toned lead (glyph + identity) + toned elapsed tail. */}
2684
2826
  <box flexDirection="row">
2685
2827
  <text>
2686
2828
  <span fg={tone()}>{lead()}</span>
2687
- <span fg={theme().textMuted}>{elapsed()}</span>
2829
+ <span fg={tone()}>{elapsed()}</span>
2688
2830
  </text>
2689
2831
  </box>
2690
- {/* Line 2: the description, indented under the identity and muted. */}
2832
+ {/* Line 2: the description, indented under the identity and toned. */}
2691
2833
  <Show when={description() !== ''}>
2692
- <text fg={theme().textMuted}>{` ${description()}`}</text>
2834
+ <text fg={tone()}>{` ${description()}`}</text>
2693
2835
  </Show>
2694
2836
  <Show when={activity()}>
2695
2837
  {(a) => (
2696
2838
  <text
2697
- fg={theme().textMuted}
2839
+ fg={tone()}
2698
2840
  >{` ↳ ${truncateDelegationDescription(`${a().tool}${a().summary !== '' ? ` ${a().summary}` : ''}`)}`}</text>
2699
2841
  )}
2700
2842
  </Show>
@@ -2810,9 +2952,10 @@ function View(props: {
2810
2952
  const projectRoot = createMemo(() => resolvePantheonRoot((props.api.state as any).path))
2811
2953
  // Silence-by-default panel logger → the REAL <root>/.pantheon/logs/hooks.log.
2812
2954
  const panelLog = createTuiLogger(projectRoot())
2813
- // Every job the board knows about is shown — no scope filter ("showing is
2814
- // what matters"). Live-first ordering keeps running rows on top; the
2815
- // ceiling collapses the rest into "… +N more".
2955
+ // ONLY the active session renders: the final list is filtered by
2956
+ // filterDelegationsToSession after every merge (children + live + md +
2957
+ // board). Live-first ordering keeps running rows on top; the ceiling
2958
+ // collapses the rest into "… +N more".
2816
2959
  // The rendered list is childDelegations (children enriched with md);
2817
2960
  // there is no separate md-only signal — the md channel only feeds
2818
2961
  // childrenToDelegationEntries below.
@@ -2839,19 +2982,20 @@ function View(props: {
2839
2982
  try {
2840
2983
  const state = props.api.state as any
2841
2984
  // 0. md reports from EVERY session under .pantheon/delegations — the
2842
- // HISTORY channel. Read BEFORE the sessionID guard so the panel shows
2843
- // past reports (running first, Finalized desc) even when no session
2844
- // is focused (null/placeholder sessionID): the "(0)" symptom becomes
2845
- // the full historical list. Fail-open: a missing dir yields [].
2985
+ // ENRICHMENT channel. Read unfiltered (taskID match supplies alias/
2986
+ // agent/description/terminal state); the active-session filter after
2987
+ // the merge keeps only the focused session's reports. Fail-open: a
2988
+ // missing dir yields [].
2846
2989
  let md: DelegationEntry[] = []
2847
2990
  try {
2848
2991
  md = await readAllDelegationEntries(projectRoot())
2849
2992
  } catch {
2850
2993
  md = [] // fail-open: never crash the sidebar on a read error
2851
2994
  }
2852
- // 0b. board snapshot — the 4th channel. Cross-session and AUTHORITATIVE
2853
- // for state/alias/agent (the persisted FSM). This is what makes a job
2854
- // running in ANOTHER session visible. Fail-open: absent/corrupt → [].
2995
+ // 0b. board snapshot — the 4th channel. It is cross-session on disk but
2996
+ // used here ONLY to ENRICH an active-session row (state/alias/agent,
2997
+ // dedup by taskID) — the active-session filter applied after the merge
2998
+ // drops its other-session records. Fail-open: absent/corrupt → [].
2855
2999
  let board: DelegationEntry[] = []
2856
3000
  try {
2857
3001
  board = boardRecordsToDelegationEntries(await readBoardState(projectRoot()))
@@ -2863,13 +3007,18 @@ function View(props: {
2863
3007
  // slot prop; forwarding it to session.children made the server reject
2864
3008
  // every poll (~1 err/s: "Expected a string starting with \"ses\", got
2865
3009
  // \"%7BsessionID%7D\"") and fail-open the panel into "(0)". A null
2866
- // resolution SKIPS the fetch entirely: the md history is shown alone
2867
- // (zero children, zero errors).
3010
+ // resolution SKIPS the fetch entirely: the active-session filter yields
3011
+ // an EMPTY panel (zero children, zero rows, zero errors).
2868
3012
  const sessionID = resolveCurrentSessionID({ sessionID: props.sessionID, api: props.api })
2869
3013
  if (sessionID === null) {
2870
- setChildDelegations(mergeBoardDelegationSources(md, board))
3014
+ // No focused session → no scope → nothing renders. The md/board reads
3015
+ // still ran (enrichment sources) but the active-session filter drops
3016
+ // every row: there is no cross-session panel.
3017
+ setChildDelegations(
3018
+ filterDelegationsToSession(mergeBoardDelegationSources(md, board), sessionID),
3019
+ )
2871
3020
  panelLog.info(
2872
- `${formatPanelLogLine(0, 0, 0, md.length, eventRefreshCount)} (no sessionID — history+board shown)`,
3021
+ `${formatPanelLogLine(0, 0, 0, md.length, eventRefreshCount)} (no sessionID — inactive panel)`,
2873
3022
  )
2874
3023
  return
2875
3024
  }
@@ -2914,11 +3063,17 @@ function View(props: {
2914
3063
  (entry) => entry.sessionID === sessionID,
2915
3064
  )
2916
3065
  // Board LAST so it is authoritative over children/md/live for the same
2917
- // job, and adds the rows from other sessions the children API cannot see.
3066
+ // job and can ENRICH an active-session row (state/alias/agent). Then the
3067
+ // active-session filter — applied ONCE after every merge — keeps ONLY
3068
+ // rows whose sessionID === the focused session, so board/md entries from
3069
+ // other sessions (and orphans with no sessionID) can never render.
2918
3070
  setChildDelegations(
2919
- mergeBoardDelegationSources(
2920
- mergeChildDelegationSources(childEntries, liveEntries),
2921
- board,
3071
+ filterDelegationsToSession(
3072
+ mergeBoardDelegationSources(
3073
+ mergeChildDelegationSources(childEntries, liveEntries),
3074
+ board,
3075
+ ),
3076
+ sessionID,
2922
3077
  ),
2923
3078
  )
2924
3079
  // 4. diagnostic line — every re-fetch (silence-by-default, hooks.log).
@@ -2939,12 +3094,15 @@ function View(props: {
2939
3094
  return delegationsInflight
2940
3095
  }
2941
3096
  // Live-first ceiling: at most DELEGATION_VISIBLE_CEILING rows render; the
2942
- // remainder collapses into a single "… +N more" line. The header counts the
2943
- // FULL board list — every session, no scope filter.
3097
+ // remainder collapses into a single "… +N more" line. Both the ceiling and
3098
+ // the header operate on the ALREADY-FILTERED active-session list
3099
+ // (childDelegations), so other-session / hidden rows never inflate counts.
2944
3100
  const delegationCeiling = createMemo(() =>
2945
3101
  ceilingDelegationList(childDelegations(), DELEGATION_VISIBLE_CEILING, now()),
2946
3102
  )
2947
- const delegationHeader = createMemo(() => formatDelegationHeader(childDelegations()))
3103
+ // Header counts the same retained rows shown below, so hidden/expired board
3104
+ // history cannot inflate the summary.
3105
+ const delegationHeader = createMemo(() => formatDelegationHeader(delegationCeiling().visible))
2948
3106
 
2949
3107
  // ── Event subscriptions for live session/delegation updates ──
2950
3108
  onMount(() => {
@@ -3090,7 +3248,7 @@ function View(props: {
3090
3248
  {`${showDelegations() ? '▼' : '▶'} Delegations`}
3091
3249
  </text>
3092
3250
  <text fg={theme().textMuted}>
3093
- {childDelegations().length > 0 ? ` ${delegationHeader()}` : ' — idle'}
3251
+ {delegationCeiling().visible.length > 0 ? ` ${delegationHeader()}` : ' — idle'}
3094
3252
  </text>
3095
3253
  </box>
3096
3254
  <Show when={showDelegations()}>
@@ -3114,7 +3272,11 @@ function View(props: {
3114
3272
  )}
3115
3273
  </For>
3116
3274
  <Show when={delegationCeiling().hidden > 0}>
3117
- <text fg={theme().textMuted}>{`… +${delegationCeiling().hidden} more`}</text>
3275
+ <text fg={theme().textMuted}>
3276
+ {delegationCeiling().hiddenActive > 0
3277
+ ? `… +${delegationCeiling().hiddenActive} active`
3278
+ : `… +${delegationCeiling().hiddenTerminal} more`}
3279
+ </text>
3118
3280
  </Show>
3119
3281
  </box>
3120
3282
  </Show>
package/src/routing.yml CHANGED
@@ -535,22 +535,6 @@ background_delegation:
535
535
  read_only_agents: [apollo, gaia]
536
536
  session_max: 3
537
537
  retry_count: 1
538
- # ── R1: Per-error-type retry + provider cooldown (LiteLLM pattern) ──────
539
- # retry_policy: max retries per error class before escalating. Exponential
540
- # backoff (base 1s, doubling, capped 30s) applies between retries. auth
541
- # errors are NEVER retried (0) — a credential problem will not fix itself.
542
- # Enforcement: zeus-delegate-with-retry (manual waves) + RetryPolicyEngine.
543
- retry_policy:
544
- auth: 0
545
- rate_limit: 3
546
- timeout: 2
547
- other: 1
548
- # cooldown: after `allowed_fails` consecutive failures for a provider, skip
549
- # that provider for `cooldown_time_seconds`. A success resets the counter.
550
- # In-memory tracker (no Redis) — process-wide, shared across retry helpers.
551
- cooldown:
552
- allowed_fails: 3
553
- cooldown_time_seconds: 60
554
538
  # ── O5: permission.task — glob rules for which subagents an agent may
555
539
  # invoke (last matching rule wins; deny removes the agent from the delegate
556
540
  # tool description entirely — prevents circular delegation at the