pantheon-opencode 1.3.5 → 1.3.7
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.
- package/README.md +33 -1
- package/package.json +3 -3
- package/scripts/_pantheon_paths.py +0 -1
- package/scripts/build-agents-md.mjs +4 -2
- package/scripts/code_mode_server.py +307 -21
- package/scripts/doctor.mjs +26 -13
- package/scripts/hooks/format-multi-language.sh +6 -3
- package/scripts/hooks/log-session-start.sh +6 -0
- package/scripts/hooks/on-subagent-delegation-start.sh +22 -6
- package/scripts/hooks/validate-post-conditions.sh +1 -1
- package/scripts/hooks/validate-tool-safety.sh +5 -2
- package/scripts/install/agents-md.mjs +3 -1
- package/scripts/install/cli-ui.mjs +24 -4
- package/scripts/install/interactive.mjs +18 -7
- package/scripts/install/migrate-config.mjs +13 -19
- package/scripts/install/migrate.mjs +8 -5
- package/scripts/install/model-picker.mjs +4 -1
- package/scripts/install/opencode.mjs +2 -0
- package/scripts/install/shared.mjs +6 -4
- package/scripts/install/state.mjs +9 -6
- package/scripts/mcp_persistence_server.py +23 -22
- package/scripts/mcp_resources_server.py +60 -1
- package/scripts/memory_mcp_server.py +80 -4
- package/scripts/postinstall.mjs +27 -25
- package/scripts/prune.mjs +1 -1
- package/scripts/redaction-gate.mjs +3 -1
- package/scripts/sync-tui.mjs +148 -0
- package/scripts/uninstall.mjs +0 -2
- package/scripts/validation-policy.mjs +6 -1
- package/scripts/version-check.mjs +9 -4
- package/src/agents/aphrodite.md +1 -1
- package/src/agents/demeter.md +1 -1
- package/src/agents/hephaestus.md +1 -1
- package/src/agents/hermes.md +1 -1
- package/src/agents/themis.md +4 -0
- package/src/mcp/code_mode_server.py +304 -18
- package/src/mcp/eval_store.py +233 -0
- package/src/mcp/mcp_persistence_server.py +23 -22
- package/src/mcp/mcp_resources_server.py +58 -0
- package/src/mcp/memory_mcp_server.py +80 -3
- package/src/pantheon/auto-wake.ts +2 -4
- package/src/pantheon/background-job-board.ts +17 -4
- package/src/pantheon/comment-checker.ts +48 -0
- package/src/pantheon/delegation-classifier.ts +307 -0
- package/src/pantheon/delegation-enforce.ts +21 -21
- package/src/pantheon/delegation-finalize.ts +44 -4
- package/src/pantheon/delegation-notify.ts +97 -0
- package/src/pantheon/delegation.ts +413 -23
- package/src/pantheon/dispatch-guard.ts +2 -0
- package/src/pantheon/file-persistence.ts +7 -3
- package/src/pantheon/permission-eval.ts +284 -0
- package/src/pantheon/permission-globs.ts +86 -0
- package/src/pantheon/presets.d.mts +41 -0
- package/src/pantheon/presets.mjs +134 -0
- package/src/pantheon/retry-policy.ts +212 -0
- package/src/pantheon/session-guard.ts +1 -3
- package/src/pantheon/step-cap.ts +110 -0
- package/src/pantheon/tui-version-check.ts +72 -0
- package/src/pantheon/vision.ts +7 -5
- package/src/pantheon/zeus-delegate-with-retry.ts +81 -12
- package/src/plugin.ts +57 -3
- package/src/plugins/hook-runner.ts +1 -1
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +19 -14
- package/src/plugins/tui/dist/tui.tsx +52 -18
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +106 -24
- package/src/plugins/tui/tsdown.config.ts +1 -1
- package/src/routing.yml +42 -0
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* is returned as a structured HookResult so the opencode session can never be
|
|
23
23
|
* taken down by the security layer.
|
|
24
24
|
*/
|
|
25
|
-
import {
|
|
25
|
+
import { type ChildProcessWithoutNullStreams, spawn } from 'node:child_process'
|
|
26
26
|
import path from 'node:path'
|
|
27
27
|
import type { Readable } from 'node:stream'
|
|
28
28
|
import { fileURLToPath } from 'node:url'
|
|
@@ -192,8 +192,8 @@ import {
|
|
|
192
192
|
enqueueChatReminder,
|
|
193
193
|
pendingChatReminders,
|
|
194
194
|
} from '../pantheon/chat-reminders.ts'
|
|
195
|
-
import { type HookPayload, type HookResult, runHook } from './hook-runner.ts'
|
|
196
195
|
import { pantheonPluginOnce } from '../pantheon/plugin-once.ts'
|
|
196
|
+
import { type HookPayload, type HookResult, runHook } from './hook-runner.ts'
|
|
197
197
|
|
|
198
198
|
/** Tools that represent a subagent delegation (opencode `task` tool etc.). */
|
|
199
199
|
const DELEGATION_TOOL_RE = /^(task|.*delegate.*|.*subagent.*)$/i
|
|
@@ -11,7 +11,7 @@ type DelegationEntry = {
|
|
|
11
11
|
taskID?: string;
|
|
12
12
|
/** Agent name, e.g. "apollo". */
|
|
13
13
|
agent: string;
|
|
14
|
-
state: 'running' | 'completed' | 'error' | 'cancelled';
|
|
14
|
+
state: 'running' | 'completed' | 'error' | 'startup_failed' | 'startup_unknown' | 'cancelled';
|
|
15
15
|
/** Epoch ms of the `Started` header. */
|
|
16
16
|
startedAt: number;
|
|
17
17
|
/** Epoch ms of the `Finalized` header — null while still running. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;KAq6BY;;EAEV;;EAEA;;;;EAIA;;EAEA;EACA;;EAEA;;EAEA;EACA;EACA;;EAEA;;;;EAIA;;;;;;;;;;;iBAsBc,wBACd,aACA,oBACA,qBACC;;;;;;
|
|
1
|
+
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;KAq6BY;;EAEV;;EAEA;;;;EAIA;;EAEA;EACA;;EAEA;;EAEA;EACA;EACA;;EAEA;;;;EAIA;;;;;;;;;;;iBAsBc,wBACd,aACA,oBACA,qBACC;;;;;;iBAgHmB,sBAAsB,cAAc,QAAQ;;;;;;;;iBAwC5C,yBAAyB,eAAe,QAAQ;;;;;;;;iBAWtD,sBACd;EAAS;EAAoB;eAC7B;;;;;;iBAYc,YAAY;;;iBAMZ,WAAW;;;;iBAOX,yBAAyB,GAAG,iBAAiB,GAAG;;;;;iBAWhD,sBACd,cAAc,mBACd,uBACC;;;iBAQa,WAAW;;;iBAcX,kBAAkB,OAAO,iBAAiB;;;KAO9C;iBAQI,mBAAmB,OAAO,kBAAkB;iBAc5C,wBAAwB,OAAO;;iBAoB/B,uBAAuB;;;;;;;iBAWvB,4BACd,mBAAmB,mBACnB,eAAe,wBACd;;KAmFS;EACV;EACA;EACA;EACA;EACA;EACA;IACE;IACA,QAAQ;IACR;IACA;IACA;MAAS;MAAgB;;;;;KAKjB;;EAEV;;EAEA;;EAEA;EACA;;EAEA;EACA;;EAEA;;EAEA;EACA;EACA;EACA;;EAEA;;;KAIU;EACV;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;iBAac,wBACd,MAAM,oBACN,eACC;;;iBA+Ea,yBACd,KAAK,YAAY,sBACjB,MAAM,oBACN;;;iBA+Fc,sBACd,KAAK,YAAY,sBACjB;;;;;;iBAiBc,2BACd;EAAqB;EAAa;iBAClC,YAAY,uDACX;;;;;iBA2Ba,sBACd,KAAK,YAAY,sBACjB,gBAAgB,sBAChB;;;iBAWc,kBAAkB,MAAM,sBAAsB;;;;;;iBAqB9C,uBACd,eAAe,uBACf,aAAa,oBACZ;;;;;;;;;iBA8Da,iBAAiB,cAAc;;;;KAOnC;;EAEV;EACA;;IAEE;MAAU;;;IAEV;MAAU;QAAY;QAAe,SAAS;;;;;;;;;;iBASlC,wBAAwB,SAAS;;;;;;;;iBAmBjC,gBAAgB;EAAgB;IAAQ;;;;;;;iBASxC,kBAAkB;EAAkC;IAAQ;;;;KAKhE;;EAEV;;EAEA;;;EAGA;;;EAGA;EACA;IACE;IACA;;;;;;;iBAQY,mBAAmB;;;;;iBASnB,cAAc,OAAO;;;;;;;;;;;;;;iBAiBrB,4BACd,mBAAmB,mCACnB,aAAa,mBACb,eACC;;;;;;iBAwCa,4BACd;EAAS,YAAY,cAAc,SAAS;eAC5C;;;;;KA8KU;EACV,KAAK,YAAY;;EAEjB;;EAEA;;cAucI,QAAQ;EAAoB"}
|
|
@@ -455,10 +455,10 @@ function parseConfig(raw) {
|
|
|
455
455
|
if (!toml?.parse) return defaultConfig();
|
|
456
456
|
const root = asTable(toml.parse(raw));
|
|
457
457
|
const cfg = defaultConfig();
|
|
458
|
-
const ui = asTable(root
|
|
459
|
-
cfg.showBars = bool(ui
|
|
460
|
-
cfg.showStatus = bool(ui
|
|
461
|
-
const rawWidth = ui
|
|
458
|
+
const ui = asTable(root.ui);
|
|
459
|
+
cfg.showBars = bool(ui.show_bars, cfg.showBars);
|
|
460
|
+
cfg.showStatus = bool(ui.show_status, cfg.showStatus);
|
|
461
|
+
const rawWidth = ui.bar_width;
|
|
462
462
|
if (typeof rawWidth === "number" && Number.isFinite(rawWidth) && rawWidth >= 1) cfg.barWidth = Math.min(40, Math.floor(rawWidth));
|
|
463
463
|
for (const id of [
|
|
464
464
|
"anthropic",
|
|
@@ -467,14 +467,14 @@ function parseConfig(raw) {
|
|
|
467
467
|
]) {
|
|
468
468
|
const t = asTable(root[id]);
|
|
469
469
|
const p = cfg.providers[id];
|
|
470
|
-
p.enabled = bool(t
|
|
471
|
-
p.show["5h"] = bool(t
|
|
472
|
-
p.show["7d"] = bool(t
|
|
473
|
-
p.show["1m"] = bool(t
|
|
474
|
-
p.show.model = bool(t
|
|
475
|
-
const credentialsPath = str(t
|
|
470
|
+
p.enabled = bool(t.enabled, p.enabled);
|
|
471
|
+
p.show["5h"] = bool(t.show_5h, p.show["5h"]);
|
|
472
|
+
p.show["7d"] = bool(t.show_7d, p.show["7d"]);
|
|
473
|
+
p.show["1m"] = bool(t.show_1m, p.show["1m"]);
|
|
474
|
+
p.show.model = bool(t.show_model, p.show.model);
|
|
475
|
+
const credentialsPath = str(t.credentials_path);
|
|
476
476
|
if (credentialsPath !== void 0) p.credentialsPath = credentialsPath;
|
|
477
|
-
const codexAuthPath = str(t
|
|
477
|
+
const codexAuthPath = str(t.codex_auth_path);
|
|
478
478
|
if (codexAuthPath !== void 0) p.codexAuthPath = codexAuthPath;
|
|
479
479
|
}
|
|
480
480
|
return cfg;
|
|
@@ -765,7 +765,7 @@ function parseDelegationMarkdown(raw, fileAlias, sessionID = "") {
|
|
|
765
765
|
const startedAt = started !== void 0 ? Date.parse(started) : NaN;
|
|
766
766
|
if (agent === void 0 || state === void 0 || Number.isNaN(startedAt)) return null;
|
|
767
767
|
const normalized = state.toLowerCase();
|
|
768
|
-
if (normalized !== "running" && normalized !== "completed" && normalized !== "error" && normalized !== "cancelled") return null;
|
|
768
|
+
if (normalized !== "running" && normalized !== "completed" && normalized !== "error" && normalized !== "startup_failed" && normalized !== "startup_unknown" && normalized !== "cancelled") return null;
|
|
769
769
|
const finalizedAt = finalized !== void 0 ? Date.parse(finalized) : NaN;
|
|
770
770
|
return {
|
|
771
771
|
alias: title ?? (fileAlias !== void 0 ? stripMdSuffix(fileAlias) : "unknown"),
|
|
@@ -898,7 +898,7 @@ function delegationElapsed(entry, now) {
|
|
|
898
898
|
* state machine testable without booting OpenCode's renderer. */
|
|
899
899
|
function delegationActivity(entry) {
|
|
900
900
|
if (entry.state === "completed") return "completed";
|
|
901
|
-
if (entry.state === "error") return "error";
|
|
901
|
+
if (entry.state === "error" || entry.state === "startup_failed" || entry.state === "startup_unknown") return "error";
|
|
902
902
|
if (entry.state === "cancelled") return "cancelled";
|
|
903
903
|
if (entry.read) return "reading";
|
|
904
904
|
if (entry.alias.startsWith("live-") && entry.taskID === void 0) return "delegating";
|
|
@@ -959,7 +959,12 @@ function mergeChildDelegationSources(children, live) {
|
|
|
959
959
|
}
|
|
960
960
|
const existing = result[index];
|
|
961
961
|
if (existing === void 0) continue;
|
|
962
|
-
if (existing.
|
|
962
|
+
if (existing.state !== "running") {
|
|
963
|
+
if (liveEntry.alias !== null && existing.alias !== incoming.alias) existing.alias = incoming.alias;
|
|
964
|
+
if (incoming.agent !== "agent" && existing.agent !== incoming.agent) existing.agent = incoming.agent;
|
|
965
|
+
if (incoming.read && !existing.read) existing.read = true;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
963
968
|
result[index] = {
|
|
964
969
|
...existing,
|
|
965
970
|
alias: liveEntry.alias !== null ? incoming.alias : existing.alias,
|
|
@@ -677,25 +677,25 @@ function parseConfig(raw: string): UsageBarConfig {
|
|
|
677
677
|
const root = asTable(toml.parse(raw))
|
|
678
678
|
const cfg = defaultConfig()
|
|
679
679
|
|
|
680
|
-
const ui = asTable(root
|
|
681
|
-
cfg.showBars = bool(ui
|
|
682
|
-
cfg.showStatus = bool(ui
|
|
683
|
-
const rawWidth = ui
|
|
680
|
+
const ui = asTable(root.ui)
|
|
681
|
+
cfg.showBars = bool(ui.show_bars, cfg.showBars)
|
|
682
|
+
cfg.showStatus = bool(ui.show_status, cfg.showStatus)
|
|
683
|
+
const rawWidth = ui.bar_width
|
|
684
684
|
if (typeof rawWidth === 'number' && Number.isFinite(rawWidth) && rawWidth >= 1)
|
|
685
685
|
cfg.barWidth = Math.min(40, Math.floor(rawWidth))
|
|
686
686
|
|
|
687
687
|
for (const id of ['anthropic', 'openai', 'opencodego'] as ProviderId[]) {
|
|
688
688
|
const t = asTable(root[id])
|
|
689
689
|
const p = cfg.providers[id]
|
|
690
|
-
p.enabled = bool(t
|
|
691
|
-
p.show['5h'] = bool(t
|
|
692
|
-
p.show['7d'] = bool(t
|
|
693
|
-
p.show['1m'] = bool(t
|
|
694
|
-
p.show.model = bool(t
|
|
690
|
+
p.enabled = bool(t.enabled, p.enabled)
|
|
691
|
+
p.show['5h'] = bool(t.show_5h, p.show['5h'])
|
|
692
|
+
p.show['7d'] = bool(t.show_7d, p.show['7d'])
|
|
693
|
+
p.show['1m'] = bool(t.show_1m, p.show['1m'])
|
|
694
|
+
p.show.model = bool(t.show_model, p.show.model)
|
|
695
695
|
// Assign only when present — keeps `exactOptionalPropertyTypes` happy.
|
|
696
|
-
const credentialsPath = str(t
|
|
696
|
+
const credentialsPath = str(t.credentials_path)
|
|
697
697
|
if (credentialsPath !== undefined) p.credentialsPath = credentialsPath
|
|
698
|
-
const codexAuthPath = str(t
|
|
698
|
+
const codexAuthPath = str(t.codex_auth_path)
|
|
699
699
|
if (codexAuthPath !== undefined) p.codexAuthPath = codexAuthPath
|
|
700
700
|
}
|
|
701
701
|
return cfg
|
|
@@ -942,7 +942,7 @@ export type DelegationEntry = {
|
|
|
942
942
|
taskID?: string
|
|
943
943
|
/** Agent name, e.g. "apollo". */
|
|
944
944
|
agent: string
|
|
945
|
-
state: 'running' | 'completed' | 'error' | 'cancelled'
|
|
945
|
+
state: 'running' | 'completed' | 'error' | 'startup_failed' | 'startup_unknown' | 'cancelled'
|
|
946
946
|
/** Epoch ms of the `Started` header. */
|
|
947
947
|
startedAt: number
|
|
948
948
|
/** Epoch ms of the `Finalized` header — null while still running. */
|
|
@@ -1052,6 +1052,8 @@ export function parseDelegationMarkdown(
|
|
|
1052
1052
|
normalized !== 'running' &&
|
|
1053
1053
|
normalized !== 'completed' &&
|
|
1054
1054
|
normalized !== 'error' &&
|
|
1055
|
+
normalized !== 'startup_failed' &&
|
|
1056
|
+
normalized !== 'startup_unknown' &&
|
|
1055
1057
|
normalized !== 'cancelled'
|
|
1056
1058
|
) {
|
|
1057
1059
|
return null
|
|
@@ -1221,7 +1223,12 @@ export type DelegationActivity =
|
|
|
1221
1223
|
|
|
1222
1224
|
export function delegationActivity(entry: DelegationEntry): DelegationActivity {
|
|
1223
1225
|
if (entry.state === 'completed') return 'completed'
|
|
1224
|
-
if (
|
|
1226
|
+
if (
|
|
1227
|
+
entry.state === 'error' ||
|
|
1228
|
+
entry.state === 'startup_failed' ||
|
|
1229
|
+
entry.state === 'startup_unknown'
|
|
1230
|
+
)
|
|
1231
|
+
return 'error'
|
|
1225
1232
|
if (entry.state === 'cancelled') return 'cancelled'
|
|
1226
1233
|
if (entry.read) return 'reading'
|
|
1227
1234
|
if (entry.alias.startsWith('live-') && entry.taskID === undefined) return 'delegating'
|
|
@@ -1274,7 +1281,12 @@ export function mergeChildDelegationSources(
|
|
|
1274
1281
|
})
|
|
1275
1282
|
|
|
1276
1283
|
for (const liveEntry of live) {
|
|
1277
|
-
if (
|
|
1284
|
+
if (
|
|
1285
|
+
liveEntry.alias === null &&
|
|
1286
|
+
liveEntry.taskID === null &&
|
|
1287
|
+
Date.now() - liveEntry.startedAt > 30_000
|
|
1288
|
+
)
|
|
1289
|
+
continue
|
|
1278
1290
|
const incoming = toDelegationEntry(liveEntry)
|
|
1279
1291
|
const index =
|
|
1280
1292
|
(incoming.taskID !== undefined ? byTask.get(incoming.taskID) : undefined) ??
|
|
@@ -1292,8 +1304,26 @@ export function mergeChildDelegationSources(
|
|
|
1292
1304
|
|
|
1293
1305
|
const existing = result[index]
|
|
1294
1306
|
if (existing === undefined) continue
|
|
1295
|
-
|
|
1296
|
-
|
|
1307
|
+
// Any terminal state is authoritative — a child session that went idle
|
|
1308
|
+
// correctly derives 'completed' via childStatusToState before the MD
|
|
1309
|
+
// report is written. The live tool-part channel stays 'running' until
|
|
1310
|
+
// pantheon_delegation_read fires, so allowing it to overwrite a
|
|
1311
|
+
// terminal state would flip completed → running (the bug this fixes).
|
|
1312
|
+
//
|
|
1313
|
+
// When the existing entry IS terminal, we still absorb live metadata
|
|
1314
|
+
// (alias, agent, read) but preserve the terminal state.
|
|
1315
|
+
if (existing.state !== 'running') {
|
|
1316
|
+
if (liveEntry.alias !== null && existing.alias !== incoming.alias) {
|
|
1317
|
+
existing.alias = incoming.alias
|
|
1318
|
+
}
|
|
1319
|
+
if (incoming.agent !== 'agent' && existing.agent !== incoming.agent) {
|
|
1320
|
+
existing.agent = incoming.agent
|
|
1321
|
+
}
|
|
1322
|
+
if (incoming.read && !existing.read) {
|
|
1323
|
+
existing.read = true
|
|
1324
|
+
}
|
|
1325
|
+
continue
|
|
1326
|
+
}
|
|
1297
1327
|
result[index] = {
|
|
1298
1328
|
...existing,
|
|
1299
1329
|
alias: liveEntry.alias !== null ? incoming.alias : existing.alias,
|
|
@@ -2220,7 +2250,9 @@ function View(props: {
|
|
|
2220
2250
|
}
|
|
2221
2251
|
const withStatus = children.map((c) => ({ ...c, status: resolveStatus(c.id) }))
|
|
2222
2252
|
const childEntries = childrenToDelegationEntries(withStatus, md, now())
|
|
2223
|
-
for (const [k, v] of props.liveStore.map)
|
|
2253
|
+
for (const [k, v] of props.liveStore.map)
|
|
2254
|
+
if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 30_000)
|
|
2255
|
+
props.liveStore.map.delete(k)
|
|
2224
2256
|
// The event channel is optimistic: it renders a job even while the
|
|
2225
2257
|
// child session/report is still being created. Filter by parent so a
|
|
2226
2258
|
// different focused session never leaks rows into this sidebar.
|
|
@@ -2488,7 +2520,9 @@ const tui: TuiPlugin = (api, _options, _meta) => {
|
|
|
2488
2520
|
order: 900,
|
|
2489
2521
|
slots: {
|
|
2490
2522
|
sidebar_content(_ctx, props) {
|
|
2491
|
-
return
|
|
2523
|
+
return (
|
|
2524
|
+
<View api={api} sessionID={props.session_id} version={version()} liveStore={liveStore} />
|
|
2525
|
+
)
|
|
2492
2526
|
},
|
|
2493
2527
|
},
|
|
2494
2528
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.7",
|
|
5
5
|
"description": "Pantheon TUI plugin — sidebar (version, preset, sessions, real-time delegations) + AI subscription usage gauges incl. OpenCode Go/Zen dollar limits (vendored, MIT)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -677,25 +677,25 @@ function parseConfig(raw: string): UsageBarConfig {
|
|
|
677
677
|
const root = asTable(toml.parse(raw))
|
|
678
678
|
const cfg = defaultConfig()
|
|
679
679
|
|
|
680
|
-
const ui = asTable(root
|
|
681
|
-
cfg.showBars = bool(ui
|
|
682
|
-
cfg.showStatus = bool(ui
|
|
683
|
-
const rawWidth = ui
|
|
680
|
+
const ui = asTable(root.ui)
|
|
681
|
+
cfg.showBars = bool(ui.show_bars, cfg.showBars)
|
|
682
|
+
cfg.showStatus = bool(ui.show_status, cfg.showStatus)
|
|
683
|
+
const rawWidth = ui.bar_width
|
|
684
684
|
if (typeof rawWidth === 'number' && Number.isFinite(rawWidth) && rawWidth >= 1)
|
|
685
685
|
cfg.barWidth = Math.min(40, Math.floor(rawWidth))
|
|
686
686
|
|
|
687
687
|
for (const id of ['anthropic', 'openai', 'opencodego'] as ProviderId[]) {
|
|
688
688
|
const t = asTable(root[id])
|
|
689
689
|
const p = cfg.providers[id]
|
|
690
|
-
p.enabled = bool(t
|
|
691
|
-
p.show['5h'] = bool(t
|
|
692
|
-
p.show['7d'] = bool(t
|
|
693
|
-
p.show['1m'] = bool(t
|
|
694
|
-
p.show.model = bool(t
|
|
690
|
+
p.enabled = bool(t.enabled, p.enabled)
|
|
691
|
+
p.show['5h'] = bool(t.show_5h, p.show['5h'])
|
|
692
|
+
p.show['7d'] = bool(t.show_7d, p.show['7d'])
|
|
693
|
+
p.show['1m'] = bool(t.show_1m, p.show['1m'])
|
|
694
|
+
p.show.model = bool(t.show_model, p.show.model)
|
|
695
695
|
// Assign only when present — keeps `exactOptionalPropertyTypes` happy.
|
|
696
|
-
const credentialsPath = str(t
|
|
696
|
+
const credentialsPath = str(t.credentials_path)
|
|
697
697
|
if (credentialsPath !== undefined) p.credentialsPath = credentialsPath
|
|
698
|
-
const codexAuthPath = str(t
|
|
698
|
+
const codexAuthPath = str(t.codex_auth_path)
|
|
699
699
|
if (codexAuthPath !== undefined) p.codexAuthPath = codexAuthPath
|
|
700
700
|
}
|
|
701
701
|
return cfg
|
|
@@ -942,7 +942,14 @@ export type DelegationEntry = {
|
|
|
942
942
|
taskID?: string
|
|
943
943
|
/** Agent name, e.g. "apollo". */
|
|
944
944
|
agent: string
|
|
945
|
-
state:
|
|
945
|
+
state:
|
|
946
|
+
| 'running'
|
|
947
|
+
| 'completed'
|
|
948
|
+
| 'error'
|
|
949
|
+
| 'startup_failed'
|
|
950
|
+
| 'startup_unknown'
|
|
951
|
+
| 'cancelled'
|
|
952
|
+
| 'stale-running'
|
|
946
953
|
/** Epoch ms of the `Started` header. */
|
|
947
954
|
startedAt: number
|
|
948
955
|
/** Epoch ms of the `Finalized` header — null while still running. */
|
|
@@ -1048,10 +1055,14 @@ export function parseDelegationMarkdown(
|
|
|
1048
1055
|
const startedAt = started !== undefined ? Date.parse(started) : NaN
|
|
1049
1056
|
if (agent === undefined || state === undefined || Number.isNaN(startedAt)) return null
|
|
1050
1057
|
const normalized = state.toLowerCase()
|
|
1058
|
+
// Running state should only come from live channels, never from persisted
|
|
1059
|
+
// reports. A stale "running" MD artifact contaminates the panel — skip it.
|
|
1060
|
+
if (normalized === 'running') return null
|
|
1051
1061
|
if (
|
|
1052
|
-
normalized !== 'running' &&
|
|
1053
1062
|
normalized !== 'completed' &&
|
|
1054
1063
|
normalized !== 'error' &&
|
|
1064
|
+
normalized !== 'startup_failed' &&
|
|
1065
|
+
normalized !== 'startup_unknown' &&
|
|
1055
1066
|
normalized !== 'cancelled'
|
|
1056
1067
|
) {
|
|
1057
1068
|
return null
|
|
@@ -1169,8 +1180,8 @@ export function tuiLogPath(projectRoot: string): string {
|
|
|
1169
1180
|
* falling back to startedAt, descending). Shared by the md reader and
|
|
1170
1181
|
* mergeDelegationSources. */
|
|
1171
1182
|
export function compareDelegationEntries(a: DelegationEntry, b: DelegationEntry): number {
|
|
1172
|
-
const aRun = a.state === 'running' ? 1 : 0
|
|
1173
|
-
const bRun = b.state === 'running' ? 1 : 0
|
|
1183
|
+
const aRun = a.state === 'running' || a.state === 'stale-running' ? 1 : 0
|
|
1184
|
+
const bRun = b.state === 'running' || b.state === 'stale-running' ? 1 : 0
|
|
1174
1185
|
if (aRun !== bRun) return bRun - aRun
|
|
1175
1186
|
return (b.updatedAt ?? b.startedAt) - (a.updatedAt ?? a.startedAt)
|
|
1176
1187
|
}
|
|
@@ -1182,12 +1193,45 @@ export function compareDelegationEntries(a: DelegationEntry, b: DelegationEntry)
|
|
|
1182
1193
|
export function visibleDelegationList(
|
|
1183
1194
|
all: readonly DelegationEntry[],
|
|
1184
1195
|
maxTerminal = 8,
|
|
1196
|
+
now = Date.now(),
|
|
1197
|
+
staleThresholdMs = 30 * 60 * 1000, // 30 minutes
|
|
1185
1198
|
): DelegationEntry[] {
|
|
1186
|
-
const running = all
|
|
1199
|
+
const running = all
|
|
1200
|
+
.filter((d) => d.state === 'running')
|
|
1201
|
+
.map((d) => markStaleIfRunning(d, now, staleThresholdMs))
|
|
1187
1202
|
const terminal = all.filter((d) => d.state !== 'running').slice(0, maxTerminal)
|
|
1188
1203
|
return [...running, ...terminal]
|
|
1189
1204
|
}
|
|
1190
1205
|
|
|
1206
|
+
/** Default stale-running threshold: 30 minutes. */
|
|
1207
|
+
export const STALE_RUNNING_THRESHOLD_MS = 30 * 60 * 1000
|
|
1208
|
+
|
|
1209
|
+
/** Idle silence window: if no updatedAt change in this window, the entry is
|
|
1210
|
+
* considered stale. Combined with the stale-running threshold to produce the
|
|
1211
|
+
* display-only `stale-running` state. */
|
|
1212
|
+
export const IDLE_SILENCE_MS = 60 * 1000
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Mark a running entry as `stale-running` if it has been running longer than
|
|
1216
|
+
* the threshold AND has no recent activity (no `updatedAt` change in the last
|
|
1217
|
+
* `IDLE_SILENCE_MS`). This is DISPLAY-ONLY — the board state is unchanged.
|
|
1218
|
+
*
|
|
1219
|
+
* A `stale-running` entry renders with a warning indicator but the underlying
|
|
1220
|
+
* delegation is still treated as running by the backend.
|
|
1221
|
+
*/
|
|
1222
|
+
export function markStaleIfRunning(
|
|
1223
|
+
entry: DelegationEntry,
|
|
1224
|
+
now: number,
|
|
1225
|
+
thresholdMs = STALE_RUNNING_THRESHOLD_MS,
|
|
1226
|
+
): DelegationEntry {
|
|
1227
|
+
if (entry.state !== 'running') return entry
|
|
1228
|
+
const elapsed = now - entry.startedAt
|
|
1229
|
+
if (elapsed < thresholdMs) return entry
|
|
1230
|
+
// If updatedAt exists and is recent, entry is still active — not stale.
|
|
1231
|
+
if (entry.updatedAt !== null && now - entry.updatedAt < IDLE_SILENCE_MS) return entry
|
|
1232
|
+
return { ...entry, state: 'stale-running' as const }
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1191
1235
|
/** Compact elapsed-time label: "5m 12s", "1h 30m", "2d 4h" — ticks every
|
|
1192
1236
|
* second for running jobs. */
|
|
1193
1237
|
export function fmtElapsed(ms: number): string {
|
|
@@ -1205,7 +1249,8 @@ export function fmtElapsed(ms: number): string {
|
|
|
1205
1249
|
/** Elapsed label for one entry: running → ticks `now - startedAt`, terminal
|
|
1206
1250
|
* → fixed `updatedAt - startedAt` (em dash when no finalized timestamp). */
|
|
1207
1251
|
export function delegationElapsed(entry: DelegationEntry, now: number): string {
|
|
1208
|
-
if (entry.state === 'running'
|
|
1252
|
+
if (entry.state === 'running' || entry.state === 'stale-running')
|
|
1253
|
+
return fmtElapsed(now - entry.startedAt)
|
|
1209
1254
|
return entry.updatedAt !== null ? fmtElapsed(entry.updatedAt - entry.startedAt) : '\u2014'
|
|
1210
1255
|
}
|
|
1211
1256
|
|
|
@@ -1221,7 +1266,12 @@ export type DelegationActivity =
|
|
|
1221
1266
|
|
|
1222
1267
|
export function delegationActivity(entry: DelegationEntry): DelegationActivity {
|
|
1223
1268
|
if (entry.state === 'completed') return 'completed'
|
|
1224
|
-
if (
|
|
1269
|
+
if (
|
|
1270
|
+
entry.state === 'error' ||
|
|
1271
|
+
entry.state === 'startup_failed' ||
|
|
1272
|
+
entry.state === 'startup_unknown'
|
|
1273
|
+
)
|
|
1274
|
+
return 'error'
|
|
1225
1275
|
if (entry.state === 'cancelled') return 'cancelled'
|
|
1226
1276
|
if (entry.read) return 'reading'
|
|
1227
1277
|
if (entry.alias.startsWith('live-') && entry.taskID === undefined) return 'delegating'
|
|
@@ -1274,7 +1324,12 @@ export function mergeChildDelegationSources(
|
|
|
1274
1324
|
})
|
|
1275
1325
|
|
|
1276
1326
|
for (const liveEntry of live) {
|
|
1277
|
-
if (
|
|
1327
|
+
if (
|
|
1328
|
+
liveEntry.alias === null &&
|
|
1329
|
+
liveEntry.taskID === null &&
|
|
1330
|
+
Date.now() - liveEntry.startedAt > 30_000
|
|
1331
|
+
)
|
|
1332
|
+
continue
|
|
1278
1333
|
const incoming = toDelegationEntry(liveEntry)
|
|
1279
1334
|
const index =
|
|
1280
1335
|
(incoming.taskID !== undefined ? byTask.get(incoming.taskID) : undefined) ??
|
|
@@ -1292,8 +1347,26 @@ export function mergeChildDelegationSources(
|
|
|
1292
1347
|
|
|
1293
1348
|
const existing = result[index]
|
|
1294
1349
|
if (existing === undefined) continue
|
|
1295
|
-
|
|
1296
|
-
|
|
1350
|
+
// Any terminal state is authoritative — a child session that went idle
|
|
1351
|
+
// correctly derives 'completed' via childStatusToState before the MD
|
|
1352
|
+
// report is written. The live tool-part channel stays 'running' until
|
|
1353
|
+
// pantheon_delegation_read fires, so allowing it to overwrite a
|
|
1354
|
+
// terminal state would flip completed → running (the bug this fixes).
|
|
1355
|
+
//
|
|
1356
|
+
// When the existing entry IS terminal, we still absorb live metadata
|
|
1357
|
+
// (alias, agent, read) but preserve the terminal state.
|
|
1358
|
+
if (existing.state !== 'running') {
|
|
1359
|
+
if (liveEntry.alias !== null && existing.alias !== incoming.alias) {
|
|
1360
|
+
existing.alias = incoming.alias
|
|
1361
|
+
}
|
|
1362
|
+
if (incoming.agent !== 'agent' && existing.agent !== incoming.agent) {
|
|
1363
|
+
existing.agent = incoming.agent
|
|
1364
|
+
}
|
|
1365
|
+
if (incoming.read && !existing.read) {
|
|
1366
|
+
existing.read = true
|
|
1367
|
+
}
|
|
1368
|
+
continue
|
|
1369
|
+
}
|
|
1297
1370
|
result[index] = {
|
|
1298
1371
|
...existing,
|
|
1299
1372
|
alias: liveEntry.alias !== null ? incoming.alias : existing.alias,
|
|
@@ -1844,7 +1917,9 @@ export function childrenToDelegationEntries(
|
|
|
1844
1917
|
agent: mdEntry?.agent ?? child.agent ?? 'agent',
|
|
1845
1918
|
state,
|
|
1846
1919
|
startedAt: mdEntry?.startedAt ?? child.time?.created ?? now,
|
|
1847
|
-
updatedAt:
|
|
1920
|
+
updatedAt:
|
|
1921
|
+
mdEntry?.updatedAt ??
|
|
1922
|
+
(state === 'running' || state === 'stale-running' ? null : (child.time?.updated ?? null)),
|
|
1848
1923
|
timedOut: mdEntry?.timedOut ?? false,
|
|
1849
1924
|
description:
|
|
1850
1925
|
mdEntry !== undefined && mdEntry.description !== ''
|
|
@@ -1978,6 +2053,8 @@ function DelegationRow(props: {
|
|
|
1978
2053
|
switch (props.job.state) {
|
|
1979
2054
|
case 'running':
|
|
1980
2055
|
return t.warning
|
|
2056
|
+
case 'stale-running':
|
|
2057
|
+
return t.error // warning color for stale delegations
|
|
1981
2058
|
case 'completed':
|
|
1982
2059
|
return t.success
|
|
1983
2060
|
case 'error':
|
|
@@ -1989,6 +2066,7 @@ function DelegationRow(props: {
|
|
|
1989
2066
|
|
|
1990
2067
|
const marker = createMemo(() => {
|
|
1991
2068
|
if (props.job.state === 'running') return `${delegationSpinnerFrame(props.animationNow)} `
|
|
2069
|
+
if (props.job.state === 'stale-running') return '\u26a0 ' // warning triangle
|
|
1992
2070
|
switch (props.job.state) {
|
|
1993
2071
|
case 'completed':
|
|
1994
2072
|
return '\u2713 '
|
|
@@ -2220,7 +2298,9 @@ function View(props: {
|
|
|
2220
2298
|
}
|
|
2221
2299
|
const withStatus = children.map((c) => ({ ...c, status: resolveStatus(c.id) }))
|
|
2222
2300
|
const childEntries = childrenToDelegationEntries(withStatus, md, now())
|
|
2223
|
-
for (const [k, v] of props.liveStore.map)
|
|
2301
|
+
for (const [k, v] of props.liveStore.map)
|
|
2302
|
+
if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 30_000)
|
|
2303
|
+
props.liveStore.map.delete(k)
|
|
2224
2304
|
// The event channel is optimistic: it renders a job even while the
|
|
2225
2305
|
// child session/report is still being created. Filter by parent so a
|
|
2226
2306
|
// different focused session never leaks rows into this sidebar.
|
|
@@ -2488,7 +2568,9 @@ const tui: TuiPlugin = (api, _options, _meta) => {
|
|
|
2488
2568
|
order: 900,
|
|
2489
2569
|
slots: {
|
|
2490
2570
|
sidebar_content(_ctx, props) {
|
|
2491
|
-
return
|
|
2571
|
+
return (
|
|
2572
|
+
<View api={api} sessionID={props.session_id} version={version()} liveStore={liveStore} />
|
|
2573
|
+
)
|
|
2492
2574
|
},
|
|
2493
2575
|
},
|
|
2494
2576
|
})
|
package/src/routing.yml
CHANGED
|
@@ -151,6 +151,7 @@ presets:
|
|
|
151
151
|
agents:
|
|
152
152
|
zeus:
|
|
153
153
|
role: Central orchestrator
|
|
154
|
+
max_steps: 40 # R4: step budget — at max_steps the agent is forced to summarize-and-stop. Tracker is permanent-per-process (no reset wiring).
|
|
154
155
|
reasoning_effort: medium
|
|
155
156
|
user_invocable: true
|
|
156
157
|
can_delegate: true
|
|
@@ -189,6 +190,7 @@ agents:
|
|
|
189
190
|
- incremental-implementation
|
|
190
191
|
athena:
|
|
191
192
|
role: Strategic planner
|
|
193
|
+
max_steps: 15 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
192
194
|
reasoning_effort: high
|
|
193
195
|
user_invocable: true
|
|
194
196
|
can_delegate: true
|
|
@@ -208,6 +210,7 @@ agents:
|
|
|
208
210
|
- memory-bank
|
|
209
211
|
apollo:
|
|
210
212
|
role: Investigation scout
|
|
213
|
+
max_steps: 25 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
211
214
|
reasoning_effort: low
|
|
212
215
|
user_invocable: false
|
|
213
216
|
can_delegate: false
|
|
@@ -225,6 +228,7 @@ agents:
|
|
|
225
228
|
- auto-continue
|
|
226
229
|
hermes:
|
|
227
230
|
role: Backend specialist
|
|
231
|
+
max_steps: 60 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
228
232
|
reasoning_effort: medium
|
|
229
233
|
user_invocable: true
|
|
230
234
|
can_delegate: true
|
|
@@ -247,6 +251,7 @@ agents:
|
|
|
247
251
|
- incremental-implementation
|
|
248
252
|
aphrodite:
|
|
249
253
|
role: Frontend specialist
|
|
254
|
+
max_steps: 60 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
250
255
|
reasoning_effort: medium
|
|
251
256
|
user_invocable: true
|
|
252
257
|
can_delegate: false
|
|
@@ -267,6 +272,7 @@ agents:
|
|
|
267
272
|
- file-prompts
|
|
268
273
|
demeter:
|
|
269
274
|
role: Database specialist
|
|
275
|
+
max_steps: 50 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
270
276
|
reasoning_effort: medium
|
|
271
277
|
user_invocable: true
|
|
272
278
|
can_delegate: false
|
|
@@ -286,6 +292,7 @@ agents:
|
|
|
286
292
|
- incremental-implementation
|
|
287
293
|
themis:
|
|
288
294
|
role: Quality gate
|
|
295
|
+
max_steps: 25 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
289
296
|
reasoning_effort: high
|
|
290
297
|
user_invocable: true
|
|
291
298
|
can_delegate: false
|
|
@@ -307,6 +314,7 @@ agents:
|
|
|
307
314
|
- context-compression
|
|
308
315
|
prometheus:
|
|
309
316
|
role: Infrastructure specialist
|
|
317
|
+
max_steps: 30 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
310
318
|
reasoning_effort: medium
|
|
311
319
|
user_invocable: true
|
|
312
320
|
can_delegate: false
|
|
@@ -325,6 +333,7 @@ agents:
|
|
|
325
333
|
- incremental-implementation
|
|
326
334
|
hephaestus:
|
|
327
335
|
role: AI pipelines
|
|
336
|
+
max_steps: 50 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
328
337
|
reasoning_effort: medium
|
|
329
338
|
user_invocable: true
|
|
330
339
|
can_delegate: false
|
|
@@ -343,6 +352,7 @@ agents:
|
|
|
343
352
|
- auto-continue
|
|
344
353
|
nyx:
|
|
345
354
|
role: Observability
|
|
355
|
+
max_steps: 25 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
346
356
|
reasoning_effort: low
|
|
347
357
|
user_invocable: true
|
|
348
358
|
can_delegate: false
|
|
@@ -361,6 +371,7 @@ agents:
|
|
|
361
371
|
- auto-continue
|
|
362
372
|
gaia:
|
|
363
373
|
role: Remote sensing domain specialist
|
|
374
|
+
max_steps: 25 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
364
375
|
reasoning_effort: low
|
|
365
376
|
user_invocable: false
|
|
366
377
|
can_delegate: false
|
|
@@ -375,6 +386,7 @@ agents:
|
|
|
375
386
|
- auto-continue
|
|
376
387
|
iris:
|
|
377
388
|
role: GitHub operations
|
|
389
|
+
max_steps: 20 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
378
390
|
reasoning_effort: low
|
|
379
391
|
user_invocable: true
|
|
380
392
|
can_delegate: false
|
|
@@ -392,6 +404,7 @@ agents:
|
|
|
392
404
|
- artifact-management
|
|
393
405
|
mnemosyne:
|
|
394
406
|
role: Memory bank
|
|
407
|
+
max_steps: 20 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
395
408
|
reasoning_effort: low
|
|
396
409
|
user_invocable: true
|
|
397
410
|
can_delegate: false
|
|
@@ -415,6 +428,7 @@ agents:
|
|
|
415
428
|
- session-goal
|
|
416
429
|
talos:
|
|
417
430
|
role: Hotfix agent
|
|
431
|
+
max_steps: 15 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
|
|
418
432
|
reasoning_effort: low
|
|
419
433
|
user_invocable: true
|
|
420
434
|
can_delegate: false
|
|
@@ -573,6 +587,34 @@ background_delegation:
|
|
|
573
587
|
read_only_agents: [apollo, gaia]
|
|
574
588
|
session_max: 3
|
|
575
589
|
retry_count: 1
|
|
590
|
+
# ── R1: Per-error-type retry + provider cooldown (LiteLLM pattern) ──────
|
|
591
|
+
# retry_policy: max retries per error class before escalating. Exponential
|
|
592
|
+
# backoff (base 1s, doubling, capped 30s) applies between retries. auth
|
|
593
|
+
# errors are NEVER retried (0) — a credential problem will not fix itself.
|
|
594
|
+
# Enforcement: zeus-delegate-with-retry (manual waves) + RetryPolicyEngine.
|
|
595
|
+
retry_policy:
|
|
596
|
+
auth: 0
|
|
597
|
+
rate_limit: 3
|
|
598
|
+
timeout: 2
|
|
599
|
+
other: 1
|
|
600
|
+
# cooldown: after `allowed_fails` consecutive failures for a provider, skip
|
|
601
|
+
# that provider for `cooldown_time_seconds`. A success resets the counter.
|
|
602
|
+
# In-memory tracker (no Redis) — process-wide, shared across retry helpers.
|
|
603
|
+
cooldown:
|
|
604
|
+
allowed_fails: 3
|
|
605
|
+
cooldown_time_seconds: 60
|
|
606
|
+
# ── O5: permission.task — glob rules for which subagents an agent may
|
|
607
|
+
# invoke (last matching rule wins; deny removes the agent from the delegate
|
|
608
|
+
# tool description entirely — prevents circular delegation at the
|
|
609
|
+
# permission layer). Default "*": allow keeps the existing runtime matrix
|
|
610
|
+
# (zeus → anyone; athena/hermes → apollo only). Example deny-first config:
|
|
611
|
+
# permission:
|
|
612
|
+
# task:
|
|
613
|
+
# "*": deny
|
|
614
|
+
# "orchestrator-*": allow
|
|
615
|
+
permission:
|
|
616
|
+
task:
|
|
617
|
+
"*": allow
|
|
576
618
|
todo_enforcer:
|
|
577
619
|
enabled: true
|
|
578
620
|
cooldown_base_ms: 5000 # base backoff between injections
|