pantheon-opencode 1.4.0 → 1.4.1
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 +36 -46
- package/opencode.json +0 -2
- package/package.json +1 -1
- package/src/pantheon/chat-reminders.ts +4 -74
- package/src/pantheon/compaction-assert.ts +28 -28
- package/src/pantheon/delegation-finalize.ts +44 -11
- package/src/pantheon/delegation-toast.ts +73 -0
- package/src/pantheon/delegation.ts +20 -106
- package/src/pantheon/presets.d.mts +4 -3
- package/src/pantheon/presets.mjs +17 -21
- package/src/plugin.ts +13 -29
- package/src/plugins/pantheon-hooks.ts +15 -158
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<h1 align="center">Pantheon</h1>
|
|
3
3
|
|
|
4
4
|
<p align="center">
|
|
5
|
-
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-v1.
|
|
5
|
+
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-v1.4.1--candidate-blue" alt="Version"></a>
|
|
6
6
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
|
|
7
7
|
<a href="docs/agents/README.md"><img src="https://img.shields.io/badge/agents-14-purple" alt="Agents"></a>
|
|
8
8
|
<a href="src/skills/README.md"><img src="https://img.shields.io/badge/skills-21-orange" alt="Skills"></a>
|
|
@@ -239,17 +239,16 @@ Fail-open: if the child session messages are unavailable (or empty), the read
|
|
|
239
239
|
returns the report exactly as before — the activity sampling never breaks the
|
|
240
240
|
delegation read.
|
|
241
241
|
|
|
242
|
-
**Completion visibility —
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
(
|
|
251
|
-
|
|
252
|
-
transition.
|
|
242
|
+
**Completion visibility — zero chat noise.** Delegation notifications are never
|
|
243
|
+
injected into the chat transcript: neither `chat.message` nor any other chat
|
|
244
|
+
hook or transcript marker is used as a delivery channel. When a job reaches a
|
|
245
|
+
terminal state (completion observed via `session.idle`/`session.error` on the
|
|
246
|
+
child, or the timeout finalize path), the plugin writes a file-only audit log
|
|
247
|
+
line (echo opt-in via `PANTHEON_HOOKS_LOG`). Completion visibility lives in the
|
|
248
|
+
legitimate channels: the board `[unread]` marker
|
|
249
|
+
(`pantheon_delegation_list`), `pantheon_delegation_read`, TUI toasts
|
|
250
|
+
(`PANTHEON_TOASTS` gate), and compaction carry-forward. Reconcile is an
|
|
251
|
+
acknowledgment, not a completion — it never re-fires the terminal transition.
|
|
253
252
|
|
|
254
253
|
**Timeout:** `background_delegation.timeout_ms` (default `900000` = 15 min). A job
|
|
255
254
|
that has not reached a terminal state is finalized as `error`/`timedOut`.
|
|
@@ -338,10 +337,9 @@ logged warn and pass-through.
|
|
|
338
337
|
|
|
339
338
|
**Post-compaction state re-assertion (1.3.4):** on `session.compacted`, fresh
|
|
340
339
|
state — running/unread delegations from the board + active goals, capped at 10
|
|
341
|
-
lines —
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
assert is a silent skip.
|
|
340
|
+
lines — remains available through compaction carry-forward and the board
|
|
341
|
+
(`pantheon_delegation_list` / `pantheon_delegation_read`). A session with
|
|
342
|
+
nothing to assert is a silent skip; no chat transcript injection is performed.
|
|
345
343
|
|
|
346
344
|
**Preemptive compaction check (1.3.4):** a pure threshold core
|
|
347
345
|
(`preemptive-compact.ts`) warns the model before the context fills: at 78%
|
|
@@ -350,36 +348,22 @@ dormant / ready-to-wire — opencode 1.18.x exposes no runtime context-usage
|
|
|
350
348
|
percentage, so nothing observes it yet; when a source appears, the caller
|
|
351
349
|
wires it in (the enqueue callback is injected).
|
|
352
350
|
|
|
353
|
-
**Model
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
`
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
Setting `PANTHEON_MODEL_PRESET` to a preset whose providers have keys, or
|
|
364
|
-
filling the required `PANTHEON_*_API_KEY` env var for the preset's provider,
|
|
365
|
-
resolves the fallback path. If nothing resolves at all (no model, no preset),
|
|
366
|
-
the child keeps using opencode's default model (warned).
|
|
367
|
-
|
|
368
|
-
**agentModels wiring (1.3.4):** the delegate now also resolves the child model
|
|
369
|
-
via `routing.yml` — `loadRoutingAgentModels` extracts the per-agent models of
|
|
370
|
-
the FIRST-listed preset (the static default, `go-deepseek` today) and passes
|
|
371
|
-
them as `options.agentModels`, branch (b) of the resolve precedence: explicit
|
|
372
|
-
`model` > `options.agentModels` > active preset > opencode default. Delegation
|
|
373
|
-
no longer depends exclusively on the active preset — a delegated child gets a
|
|
374
|
-
sane per-agent model even when no preset is active. Fail-open: a missing or
|
|
375
|
-
unparseable routing.yml yields `{}` (previous behavior, warned).
|
|
351
|
+
**Model selection (1.4.1):** the child session inherits the parent session's
|
|
352
|
+
model through OpenCode. A model override is supplied only when the active
|
|
353
|
+
routing profile defines one; no model API-key validation is performed here.
|
|
354
|
+
|
|
355
|
+
**agentModels wiring (1.3.4):** `loadRoutingAgentModels` extracts only the
|
|
356
|
+
per-agent models from the explicitly active routing profile and passes them as
|
|
357
|
+
`options.agentModels`. No active profile, missing profile entry, or profile
|
|
358
|
+
without `model` produces an empty map; there is no implicit first-preset or
|
|
359
|
+
DeepSeek fallback. A missing or unparseable `routing.yml` is fail-open and
|
|
360
|
+
omits the child model.
|
|
376
361
|
|
|
377
362
|
**Delegation log hygiene (1.3.4):** `delegations.log` now records the real
|
|
378
363
|
`task_id` (omitted when empty, never `""`) and a **numeric** `duration_ms`
|
|
379
|
-
(null when unset) so downstream aggregation works.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
duplicate lines for the same notification.
|
|
364
|
+
(null when unset) so downstream aggregation works. Terminal events are
|
|
365
|
+
file-only audit entries; completion is surfaced by TUI toast (when enabled),
|
|
366
|
+
the board, and the list/read tools — never by chat delivery.
|
|
383
367
|
|
|
384
368
|
**Env vars & config:**
|
|
385
369
|
|
|
@@ -1040,11 +1024,17 @@ Use the isolated sandbox (`~/pantheon-sandbox/`) — **never** the dev environme
|
|
|
1040
1024
|
|
|
1041
1025
|
**Happy path (clean edit):** any normal tool call (exit 0) is **silent** — no console output by design, even though the audit hooks append their log files (`sessions.log`, `delegations.log`). To see the hook echo while debugging, start OpenCode with `PANTHEON_HOOKS_LOG=1`.
|
|
1042
1026
|
|
|
1043
|
-
**Delegation path (chat
|
|
1027
|
+
**Delegation path (zero chat noise):** delegation signals are surfaced through
|
|
1028
|
+
TUI toasts (when enabled), the board's `[unread]` marker, and the
|
|
1029
|
+
`pantheon_delegation_list` / `pantheon_delegation_read` tools. Ask something
|
|
1030
|
+
that dispatches subagents (e.g. *"dispare 2 subagentes apollo em paralelo para
|
|
1031
|
+
listar arquivos e comparar resultados"*) and expect:
|
|
1044
1032
|
|
|
1045
|
-
- A
|
|
1033
|
+
- A TUI toast with `🚀 apollo em execução` / `✅ apollo concluiu` when
|
|
1034
|
+
`PANTHEON_TOASTS` permits it
|
|
1035
|
+
- `[unread]` in `pantheon_delegation_list` until the terminal result is read
|
|
1046
1036
|
- An honest append to `logs/agent-sessions/delegations.log`: the **real agent name** (extracted from `tool_input.subagent_type`, never `unknown` when present) and a **non-fabricated status** — `success` only on explicit completion evidence, `failure` for refusals/errors, `unknown` otherwise
|
|
1047
|
-
- With `export PANTHEON_TOASTS=off` before starting OpenCode: no toasts/
|
|
1037
|
+
- With `export PANTHEON_TOASTS=off` before starting OpenCode: no toasts; board/list/read and audit-log visibility remain available
|
|
1048
1038
|
|
|
1049
1039
|
### Pre-commit hooks (local secret gate)
|
|
1050
1040
|
|
package/opencode.json
CHANGED
package/package.json
CHANGED
|
@@ -1,79 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Retired transcript-notification module.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `Object.values(mod)` and treats EVERY export as a plugin factory —
|
|
8
|
-
* a non-function export throws `TypeError("Plugin export is not a
|
|
9
|
-
* function")` and a function export is invoked with a PluginInput.
|
|
10
|
-
* pantheon-hooks.ts therefore cannot EXPORT the enqueue API.
|
|
11
|
-
* 2. src/plugin.ts (a separate plugin file) must ENQUEUE into the SAME
|
|
12
|
-
* buffer the pantheon-hooks `chat.message` hook drains — a local copy
|
|
13
|
-
* would enqueue into a buffer that is never delivered.
|
|
14
|
-
* 3. opencode loads plugins via plain `await import(entry)` (PluginLoader),
|
|
15
|
-
* so both plugin files importing this module resolve to ONE module
|
|
16
|
-
* instance (Bun/Node module cache) → one shared buffer.
|
|
17
|
-
*
|
|
18
|
-
* release-134 Phase 4 uses this buffer for the post-compaction state
|
|
19
|
-
* re-assertion: `reassertAfterCompaction` (compaction-assert.ts) enqueues a
|
|
20
|
-
* fresh-state block here, and the existing chat.message delivery path
|
|
21
|
-
* (pantheon-hooks.ts) injects it as a <system-reminder> into the session's
|
|
22
|
-
* next message. Zero new delivery code — the P0 messageID guard already
|
|
23
|
-
* protects subagent promptAsync fires.
|
|
24
|
-
*
|
|
25
|
-
* Bounded: at most CHAT_REMINDER_MAX entries, each expiring
|
|
26
|
-
* CHAT_REMINDER_TTL_MS after enqueue. Full-buffer enqueues are SKIPPED,
|
|
27
|
-
* never unbounded. No background timers — expiry is only checked on enqueue
|
|
28
|
-
* and on delivery (drainFreshChatReminders).
|
|
4
|
+
* Delegation lifecycle state must not be queued for delivery into user
|
|
5
|
+
* messages. Visibility is provided by the board/list/read tools and TUI
|
|
6
|
+
* toasts; this intentionally exports no delivery API.
|
|
29
7
|
*
|
|
30
8
|
* @module chat-reminders
|
|
31
9
|
*/
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Hard cap on queued reminder entries (anti-spam philosophy, matching the
|
|
35
|
-
* "throttled signals are skipped, never backlogged" rule).
|
|
36
|
-
*/
|
|
37
|
-
export const CHAT_REMINDER_MAX = 10
|
|
38
|
-
|
|
39
|
-
/** How long a queued reminder stays deliverable (pruned after ~60s). */
|
|
40
|
-
export const CHAT_REMINDER_TTL_MS = 60_000
|
|
41
|
-
|
|
42
|
-
/** One queued reminder: the text plus its enqueue timestamp. */
|
|
43
|
-
export interface ChatReminderEntry {
|
|
44
|
-
text: string
|
|
45
|
-
at: number
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** The shared buffer — module-level state, consumed by the chat.message hook. */
|
|
49
|
-
export const pendingChatReminders: ChatReminderEntry[] = []
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Queue one chat-reminder entry, pruning expired entries first. Never throws.
|
|
53
|
-
* A full buffer skips the push (bounded, never backlogged).
|
|
54
|
-
*/
|
|
55
|
-
export function enqueueChatReminder(text: string): void {
|
|
56
|
-
const now = Date.now()
|
|
57
|
-
for (let i = pendingChatReminders.length - 1; i >= 0; i--) {
|
|
58
|
-
const r = pendingChatReminders[i]
|
|
59
|
-
if (r !== undefined && now - r.at > CHAT_REMINDER_TTL_MS) pendingChatReminders.splice(i, 1)
|
|
60
|
-
}
|
|
61
|
-
if (pendingChatReminders.length >= CHAT_REMINDER_MAX) return
|
|
62
|
-
pendingChatReminders.push({ text, at: now })
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Deliver (drain) every FRESH reminder as one joined block: expired entries
|
|
67
|
-
* are dropped, the buffer is cleared in all cases (no leak). Returns
|
|
68
|
-
* undefined when nothing fresh remains — the caller skips injection.
|
|
69
|
-
*
|
|
70
|
-
* This is the delivery helper the pantheon-hooks `chat.message` hook calls
|
|
71
|
-
* AFTER its P0 messageID guard (empty messageID → early return, buffer kept).
|
|
72
|
-
*/
|
|
73
|
-
export function drainFreshChatReminders(): string | undefined {
|
|
74
|
-
const now = Date.now()
|
|
75
|
-
const fresh = pendingChatReminders.filter((r) => now - r.at <= CHAT_REMINDER_TTL_MS)
|
|
76
|
-
pendingChatReminders.length = 0
|
|
77
|
-
if (fresh.length === 0) return undefined
|
|
78
|
-
return fresh.map((r) => r.text).join('\n')
|
|
79
|
-
}
|
|
@@ -1,37 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Compaction Re-Assert (release-134 Phase 4) —
|
|
3
|
-
* compaction.
|
|
2
|
+
* Compaction Re-Assert (release-134 Phase 4) — detect fresh state after
|
|
3
|
+
* compaction without writing to the chat transcript.
|
|
4
4
|
*
|
|
5
5
|
* WHY: after a session is compacted, the system state may have changed since
|
|
6
6
|
* the summary was built (new background delegations launched, results
|
|
7
7
|
* delivered, goals updated). The compacted session's context is frozen at
|
|
8
8
|
* summary time — the model would operate on stale state.
|
|
9
9
|
*
|
|
10
|
-
* HOW: the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* enqueues it into the SHARED chat-reminder buffer (chat-reminders.ts). The
|
|
14
|
-
* existing pantheon-hooks `chat.message` hook delivers it as a
|
|
15
|
-
* <system-reminder> into the session's next message — zero new delivery
|
|
16
|
-
* code, and the P0 messageID guard already protects subagent fires.
|
|
10
|
+
* HOW: the compaction lifecycle keeps the board and goal stores authoritative;
|
|
11
|
+
* the `experimental.session.compacting` hook builds the context that is
|
|
12
|
+
* consumed by the compaction itself. Chat is never used as a delivery channel.
|
|
17
13
|
*
|
|
18
14
|
* Deliberately NOT `session.promptAsync({noReply:true})`: the sprint 4+7
|
|
19
15
|
* spike refuted noReply as a push mechanism on this opencode build (nothing
|
|
20
16
|
* is delivered to the parent).
|
|
21
17
|
*
|
|
22
|
-
* FAIL-OPEN: every source (board list
|
|
18
|
+
* FAIL-OPEN: every source (board list and goals list) is wrapped —
|
|
23
19
|
* a failure logs a warn via the silence-by-default logger and the hook never
|
|
24
20
|
* throws. A session with NO fresh state (nothing running/unread/goals) is a
|
|
25
|
-
* SILENT skip (no warn
|
|
21
|
+
* SILENT skip (no warn). The block is capped at
|
|
26
22
|
* REASSERT_MAX_LINES item lines (1 header) so it stays compact.
|
|
27
23
|
*
|
|
28
|
-
* Pure TypeScript — zero runtime deps beyond the board/goal types
|
|
29
|
-
* shared reminder buffer.
|
|
24
|
+
* Pure TypeScript — zero runtime deps beyond the board/goal types.
|
|
30
25
|
*
|
|
31
26
|
* @module compaction-assert
|
|
32
27
|
*/
|
|
33
28
|
import type { BackgroundJobBoard } from './background-job-board.ts'
|
|
34
|
-
import { enqueueChatReminder } from './chat-reminders.ts'
|
|
35
29
|
import type { CompactionGoalSource } from './delegation-compaction.ts'
|
|
36
30
|
import { createPantheonLogger } from './logger.ts'
|
|
37
31
|
|
|
@@ -51,16 +45,16 @@ export const REASSERT_HEADER = 'State re-assertion after compaction — the summ
|
|
|
51
45
|
|
|
52
46
|
/** Dependencies threaded to reassertAfterCompaction (wired from plugin.ts). */
|
|
53
47
|
export interface CompactionAssertDeps {
|
|
54
|
-
/** The
|
|
48
|
+
/** The session whose compaction context is being prepared. */
|
|
55
49
|
sessionID: string
|
|
56
50
|
/** Board query surface — `list` is all the re-assertion needs. */
|
|
57
51
|
board: Pick<BackgroundJobBoard, 'list'>
|
|
58
52
|
/** Goal source for the goals block (optional — same gating as Phase 2). */
|
|
59
53
|
goals?: CompactionGoalSource
|
|
60
|
-
/** Injectable enqueue (testable). Defaults to the shared chat buffer. */
|
|
61
|
-
enqueue?: (text: string) => void
|
|
62
54
|
/** Injectable warn logger (testable). Defaults to silence-by-default. */
|
|
63
55
|
logger?: { warn: (message: string) => void }
|
|
56
|
+
/** Non-chat delivery sink (for example a pending `output.context` block). */
|
|
57
|
+
deliverContext?: (context: string[]) => void | Promise<void>
|
|
64
58
|
}
|
|
65
59
|
|
|
66
60
|
// ─── Formatting ────────────────────────────────────────────────────────
|
|
@@ -135,26 +129,32 @@ async function buildStateLines(
|
|
|
135
129
|
}
|
|
136
130
|
|
|
137
131
|
/**
|
|
138
|
-
* Re-assert fresh state for a compacted session
|
|
139
|
-
*
|
|
132
|
+
* Re-assert fresh state for a compacted session and return the context block
|
|
133
|
+
* to the non-chat caller. The caller can hand this array to OpenCode's
|
|
134
|
+
* `output.context` or persist it through the board; constructing it here and
|
|
135
|
+
* throwing it away would make the hook a no-op.
|
|
140
136
|
*
|
|
141
|
-
* Returns
|
|
142
|
-
* assert (silent skip) or a source failed (warned). NEVER throws —
|
|
143
|
-
* hook must never break the session.
|
|
137
|
+
* Returns the fresh-state context when found, or undefined when there was
|
|
138
|
+
* nothing to assert (silent skip) or a source failed (warned). NEVER throws —
|
|
139
|
+
* the event hook must never break the session.
|
|
144
140
|
*/
|
|
145
|
-
export async function reassertAfterCompaction(
|
|
141
|
+
export async function reassertAfterCompaction(
|
|
142
|
+
deps: CompactionAssertDeps,
|
|
143
|
+
): Promise<string[] | undefined> {
|
|
146
144
|
// Silence-by-default logger: file-only warn, console echo opt-in via
|
|
147
145
|
// PANTHEON_HOOKS_LOG=1 (TUI pollution policy, pantheon-hooks L42-58).
|
|
148
146
|
const warn = deps.logger?.warn ?? ((message: string) => log.warn(message))
|
|
149
147
|
try {
|
|
150
148
|
const lines = await buildStateLines(deps, warn)
|
|
151
|
-
if (lines === undefined) return
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
if (lines === undefined) return undefined
|
|
150
|
+
// This is a context return value, not a chat message. The caller owns the
|
|
151
|
+
// supported delivery path (output.context or persisted board state).
|
|
152
|
+
const context = [REASSERT_HEADER, ...lines]
|
|
153
|
+
await deps.deliverContext?.(context)
|
|
154
|
+
return context
|
|
155
155
|
} catch (err: unknown) {
|
|
156
156
|
const reason = err instanceof Error ? err.message : String(err)
|
|
157
157
|
warn(`compaction re-assert failed for session ${deps.sessionID}: ${reason}`)
|
|
158
|
-
return
|
|
158
|
+
return undefined
|
|
159
159
|
}
|
|
160
160
|
}
|
|
@@ -172,24 +172,48 @@ export const DELEGATION_DEFAULTS = {
|
|
|
172
172
|
|
|
173
173
|
// ─── Output pulling ────────────────────────────────────────────────────
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
interface PulledOutput {
|
|
176
|
+
text: string
|
|
177
|
+
/** True only when the child produced an assistant message or tool activity. */
|
|
178
|
+
hasWork: boolean
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Pull visible text and structurally identify real child work. */
|
|
182
|
+
async function pullOutput(client: DelegationClient, childSessionID: string): Promise<PulledOutput> {
|
|
177
183
|
if (typeof client.session.messages !== 'function')
|
|
178
|
-
return '(child session messages API unavailable)'
|
|
184
|
+
return { text: '(child session messages API unavailable)', hasWork: false }
|
|
179
185
|
try {
|
|
180
186
|
const bundles = await client.session.messages({ path: { id: childSessionID } })
|
|
181
187
|
const lines: string[] = []
|
|
188
|
+
let hasWork = false
|
|
182
189
|
for (const bundle of bundles) {
|
|
190
|
+
const hasToolPart = (bundle.parts ?? []).some((part) => part.type === 'tool')
|
|
191
|
+
const hasTextPart = (bundle.parts ?? []).some(
|
|
192
|
+
(part) => part.type === 'text' && typeof part.text === 'string' && part.text.trim() !== '',
|
|
193
|
+
)
|
|
194
|
+
if (
|
|
195
|
+
bundle.info?.role === 'assistant' &&
|
|
196
|
+
bundle.info.error === undefined &&
|
|
197
|
+
(hasToolPart || hasTextPart)
|
|
198
|
+
) {
|
|
199
|
+
hasWork = true
|
|
200
|
+
}
|
|
183
201
|
for (const part of bundle.parts ?? []) {
|
|
202
|
+
if (
|
|
203
|
+
part.type === 'tool' &&
|
|
204
|
+
bundle.info?.role === 'assistant' &&
|
|
205
|
+
bundle.info.error === undefined
|
|
206
|
+
)
|
|
207
|
+
hasWork = true
|
|
184
208
|
if (part.type === 'text' && typeof part.text === 'string' && part.text.trim() !== '') {
|
|
185
209
|
lines.push(part.text)
|
|
186
210
|
}
|
|
187
211
|
}
|
|
188
212
|
}
|
|
189
|
-
return lines.join('\n\n')
|
|
213
|
+
return { text: lines.join('\n\n'), hasWork }
|
|
190
214
|
} catch (err: unknown) {
|
|
191
215
|
const reason = err instanceof Error ? err.message : String(err)
|
|
192
|
-
return `(failed to pull child session output: ${reason})
|
|
216
|
+
return { text: `(failed to pull child session output: ${reason})`, hasWork: false }
|
|
193
217
|
}
|
|
194
218
|
}
|
|
195
219
|
|
|
@@ -311,9 +335,18 @@ export async function finalizeDelegation(
|
|
|
311
335
|
const job = deps.board.get(childSessionID)
|
|
312
336
|
if (!job) return undefined
|
|
313
337
|
|
|
314
|
-
const
|
|
338
|
+
const pulled = await pullOutput(deps.client, childSessionID)
|
|
339
|
+
const output = pulled.text
|
|
340
|
+
const effectiveOpts: FinalizeInput =
|
|
341
|
+
opts.state === 'completed' && !pulled.hasWork
|
|
342
|
+
? {
|
|
343
|
+
state: 'error',
|
|
344
|
+
error: opts.error ?? 'Child session produced no assistant or tool output',
|
|
345
|
+
...(opts.timedOut !== undefined ? { timedOut: opts.timedOut } : {}),
|
|
346
|
+
}
|
|
347
|
+
: opts
|
|
315
348
|
const outputDir = deps.options.outputDir ?? DELEGATION_DEFAULTS.outputDir
|
|
316
|
-
await writeDelegationReport(outputDir, job, renderDelegationMarkdown(job, output,
|
|
349
|
+
await writeDelegationReport(outputDir, job, renderDelegationMarkdown(job, output, effectiveOpts))
|
|
317
350
|
|
|
318
351
|
const status: {
|
|
319
352
|
taskID: string
|
|
@@ -323,15 +356,15 @@ export async function finalizeDelegation(
|
|
|
323
356
|
resultSummary?: string
|
|
324
357
|
} = {
|
|
325
358
|
taskID: childSessionID,
|
|
326
|
-
state:
|
|
359
|
+
state: effectiveOpts.state,
|
|
327
360
|
resultSummary: summarizeOutput(output),
|
|
328
361
|
}
|
|
329
|
-
if (
|
|
330
|
-
if (
|
|
362
|
+
if (effectiveOpts.error !== undefined) status.error = effectiveOpts.error
|
|
363
|
+
if (effectiveOpts.timedOut !== undefined) status.timedOut = effectiveOpts.timedOut
|
|
331
364
|
|
|
332
365
|
const current = deps.board.get(childSessionID)
|
|
333
366
|
if (current !== undefined) {
|
|
334
|
-
if (current.state === 'running' || current.state ===
|
|
367
|
+
if (current.state === 'running' || current.state === effectiveOpts.state) {
|
|
335
368
|
await deps.board.updateStatus(status)
|
|
336
369
|
}
|
|
337
370
|
// Terminal in a DIFFERENT state (e.g. timeout already recorded) — the
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { BackgroundJobRecord } from './background-job-board.ts'
|
|
2
|
+
|
|
3
|
+
type ToastCategory = 'errors' | 'delegations' | 'council'
|
|
4
|
+
|
|
5
|
+
const toastMode = (process.env.PANTHEON_TOASTS ?? '').trim().toLowerCase()
|
|
6
|
+
const enabledToastCategories: ReadonlySet<ToastCategory> = (() => {
|
|
7
|
+
switch (toastMode) {
|
|
8
|
+
case 'off':
|
|
9
|
+
return new Set()
|
|
10
|
+
case 'errors':
|
|
11
|
+
return new Set(['errors'])
|
|
12
|
+
case 'council':
|
|
13
|
+
return new Set(['errors', 'council'])
|
|
14
|
+
case 'all':
|
|
15
|
+
return new Set(['errors', 'delegations', 'council'])
|
|
16
|
+
default:
|
|
17
|
+
return new Set(['errors', 'delegations', 'council'])
|
|
18
|
+
}
|
|
19
|
+
})()
|
|
20
|
+
const terminalToastsShown = new Set<string>()
|
|
21
|
+
const terminalStates = new Set(['completed', 'error', 'cancelled'])
|
|
22
|
+
|
|
23
|
+
/** Match the lifecycle toast gate used by pantheon-hooks. */
|
|
24
|
+
export function delegationToastEnabled(): boolean {
|
|
25
|
+
return enabledToastCategories.has('delegations')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ToastClient = {
|
|
29
|
+
tui?: {
|
|
30
|
+
showToast?: (input: {
|
|
31
|
+
body: {
|
|
32
|
+
title?: string
|
|
33
|
+
message: string
|
|
34
|
+
variant: 'info' | 'success' | 'warning' | 'error'
|
|
35
|
+
duration: number
|
|
36
|
+
}
|
|
37
|
+
}) => Promise<unknown>
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Best-effort visual completion signal; never affects board finalization. */
|
|
42
|
+
export async function showDelegationTerminalToast(
|
|
43
|
+
client: ToastClient,
|
|
44
|
+
job: Pick<BackgroundJobRecord, 'taskID' | 'alias' | 'agent' | 'state' | 'timedOut'>,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
try {
|
|
47
|
+
if (!delegationToastEnabled()) return
|
|
48
|
+
const isTerminal = job.timedOut || terminalStates.has(job.state)
|
|
49
|
+
if (isTerminal) {
|
|
50
|
+
if (terminalToastsShown.has(job.taskID)) return
|
|
51
|
+
terminalToastsShown.add(job.taskID)
|
|
52
|
+
}
|
|
53
|
+
const showToast = client.tui?.showToast
|
|
54
|
+
if (typeof showToast !== 'function') return
|
|
55
|
+
const state = job.timedOut ? 'timeout' : job.state
|
|
56
|
+
const variant =
|
|
57
|
+
state === 'completed'
|
|
58
|
+
? 'success'
|
|
59
|
+
: state === 'error' || state === 'timeout'
|
|
60
|
+
? 'error'
|
|
61
|
+
: 'warning'
|
|
62
|
+
await showToast({
|
|
63
|
+
body: {
|
|
64
|
+
title: 'Delegação concluída',
|
|
65
|
+
message: `${job.alias} (${job.agent}) — ${state}`,
|
|
66
|
+
variant,
|
|
67
|
+
duration: 4000,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
} catch {
|
|
71
|
+
// TUI is optional (and may disappear during shutdown); completion is durable.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -58,14 +58,9 @@ import {
|
|
|
58
58
|
finalizeDelegation as finalizeDelegationReport,
|
|
59
59
|
readDelegationReport,
|
|
60
60
|
} from './delegation-finalize.ts'
|
|
61
|
-
import {
|
|
62
|
-
type IdleChildScanDeps,
|
|
63
|
-
finalizeIdleChildrenWithoutMd,
|
|
64
|
-
startIdleChildScan,
|
|
65
|
-
} from './delegation-notify.ts'
|
|
61
|
+
import { finalizeIdleChildrenWithoutMd } from './delegation-notify.ts'
|
|
66
62
|
import { createPantheonLogger } from './logger.ts'
|
|
67
63
|
import { buildAgentListDescription } from './permission-globs.ts'
|
|
68
|
-
import { missingProviderKeyEnv, resolveActivePreset } from './presets.mjs'
|
|
69
64
|
import { buildStopInstruction, cappedSummary, DEFAULT_MAX_STEPS } from './step-cap.ts'
|
|
70
65
|
|
|
71
66
|
export type {
|
|
@@ -217,8 +212,10 @@ async function bootstrapChild(
|
|
|
217
212
|
const remainingMs = timeoutMs - (now() - startedAt)
|
|
218
213
|
if (hasMessagesAPI) {
|
|
219
214
|
try {
|
|
215
|
+
const messagesAPI = client.session.messages
|
|
216
|
+
if (messagesAPI === undefined) continue
|
|
220
217
|
const messages = await boundedProbe(
|
|
221
|
-
|
|
218
|
+
messagesAPI({ path: { id: childSessionID } }),
|
|
222
219
|
remainingMs,
|
|
223
220
|
)
|
|
224
221
|
if (messages !== undefined) {
|
|
@@ -231,8 +228,10 @@ async function bootstrapChild(
|
|
|
231
228
|
}
|
|
232
229
|
if (hasStatusAPI) {
|
|
233
230
|
try {
|
|
231
|
+
const statusAPI = client.session.get
|
|
232
|
+
if (statusAPI === undefined) continue
|
|
234
233
|
const status = await boundedProbe(
|
|
235
|
-
|
|
234
|
+
statusAPI({ path: { id: childSessionID } }),
|
|
236
235
|
timeoutMs - (now() - startedAt),
|
|
237
236
|
)
|
|
238
237
|
if (status !== undefined) {
|
|
@@ -591,20 +590,6 @@ export interface ChildSessionModelRef {
|
|
|
591
590
|
providerID: string
|
|
592
591
|
}
|
|
593
592
|
|
|
594
|
-
/**
|
|
595
|
-
* Known-good fallback model (P1, 2026-08-11 — release 1.3.4, updated
|
|
596
|
-
* 2026-08-21 for sandbox without PANTHEON_OPENCODE_API_KEY): when an
|
|
597
|
-
* AUTO-RESOLVED model (routing.yml agent entry / active preset) points at a
|
|
598
|
-
* provider whose API key is not configured, the child is dispatched on the
|
|
599
|
-
* `opencode-go` provider instead — `opencode-go/deepseek-v4-flash`
|
|
600
|
-
* via https://opencode.ai/zen/go/v1 works without an external API key
|
|
601
|
-
* (Go subscription / opencode auth, sandbox default). Validated with the
|
|
602
|
-
* same providerKeyConfigured gate as the resolved model, with an
|
|
603
|
-
* opencode-go exception for the sandbox; an explicit caller-supplied
|
|
604
|
-
* `model` always wins over this fallback.
|
|
605
|
-
*/
|
|
606
|
-
const FALLBACK_MODEL = 'opencode-go/deepseek-v4-flash'
|
|
607
|
-
|
|
608
593
|
/**
|
|
609
594
|
* Split a `provider/model` model ID (e.g. `opencode/deepseek-v4-flash-free`)
|
|
610
595
|
* into the `{ id, providerID }` ref the session.create body expects.
|
|
@@ -617,12 +602,10 @@ function splitModelRef(model: string): ChildSessionModelRef | undefined {
|
|
|
617
602
|
}
|
|
618
603
|
|
|
619
604
|
/**
|
|
620
|
-
* Resolve
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
* 4. fallback: undefined — the child is created without a model (the
|
|
625
|
-
* caller warns) so opencode's default applies.
|
|
605
|
+
* Resolve only an explicitly supplied model. A routing profile's `model` is
|
|
606
|
+
* passed through as `options.agentModels`; profiles without one, and agents
|
|
607
|
+
* without a profile, intentionally resolve to undefined so the OpenCode
|
|
608
|
+
* parent session can provide inheritance.
|
|
626
609
|
*/
|
|
627
610
|
function resolveChildModel(
|
|
628
611
|
options: DelegationOptions,
|
|
@@ -641,43 +624,14 @@ function resolveChildModel(
|
|
|
641
624
|
const ref = splitModelRef(mapped)
|
|
642
625
|
if (ref !== undefined) return ref
|
|
643
626
|
}
|
|
644
|
-
// (c) active preset agent entry (resolveActivePreset reads routing.yml
|
|
645
|
-
// presets + .pantheon/active-preset.json). Guarded — a broken routing.yml
|
|
646
|
-
// must never kill the delegate.
|
|
647
|
-
try {
|
|
648
|
-
const presetModel = resolveActivePreset({
|
|
649
|
-
...(options.presetEnv !== undefined ? { env: options.presetEnv } : {}),
|
|
650
|
-
...(options.logger !== undefined ? { logger: options.logger } : {}),
|
|
651
|
-
})?.agents?.[key]?.model
|
|
652
|
-
if (presetModel !== undefined && presetModel !== '') {
|
|
653
|
-
const ref = splitModelRef(presetModel)
|
|
654
|
-
if (ref !== undefined) return ref
|
|
655
|
-
}
|
|
656
|
-
} catch (err: unknown) {
|
|
657
|
-
const reason = err instanceof Error ? err.message : String(err)
|
|
658
|
-
options.logger?.warn?.(`[pantheon-delegate] active preset resolution failed: ${reason}`)
|
|
659
|
-
}
|
|
660
627
|
return undefined
|
|
661
628
|
}
|
|
662
629
|
|
|
663
630
|
/**
|
|
664
|
-
* Resolve a
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
* options.agentModels > active preset) — see resolveChildModel.
|
|
669
|
-
* 2. provider key gate: the resolved provider requires its apiKeyEnv env
|
|
670
|
-
* var (routing.yml preset defs — same source applyPreset enforces at
|
|
671
|
-
* startup). No gate (native providers) or key present → usable as-is.
|
|
672
|
-
* 3. key MISSING + EXPLICIT caller model → respected anyway (user intent),
|
|
673
|
-
* warned.
|
|
674
|
-
* 4. key MISSING + AUTO-resolved model → fall back to FALLBACK_MODEL
|
|
675
|
-
* (validated the same way). Fallback also unusable → returns an `error`
|
|
676
|
-
* TEXT (the caller returns it from the tool — never throws) and the
|
|
677
|
-
* caller registers NO job on the board.
|
|
678
|
-
*
|
|
679
|
-
* The no-model case (nothing resolved) keeps the pre-existing behavior:
|
|
680
|
-
* warn and let the child use opencode's default.
|
|
631
|
+
* Resolve a child model without selecting a provider or model on the caller's
|
|
632
|
+
* behalf. OpenCode owns model inheritance when this returns no model; if the
|
|
633
|
+
* host requires one, its session.create/promptAsync error is returned with
|
|
634
|
+
* the normal diagnostic rather than silently choosing a fallback.
|
|
681
635
|
*
|
|
682
636
|
* @returns `{ model, error }` — at most one of `model` / `error` is set;
|
|
683
637
|
* both may be unset (no model resolved, warn emitted).
|
|
@@ -687,50 +641,15 @@ function resolveUsableChildModel(
|
|
|
687
641
|
agent: string,
|
|
688
642
|
explicitModel: string | undefined,
|
|
689
643
|
): { model: ChildSessionModelRef | undefined; error: string | undefined } {
|
|
690
|
-
const env = options.presetEnv ?? process.env
|
|
691
|
-
const explicit = explicitModel !== undefined && explicitModel !== ''
|
|
692
|
-
const warn = (msg: string) => options.logger?.warn?.(msg)
|
|
693
|
-
|
|
694
644
|
const model = resolveChildModel(options, agent, explicitModel)
|
|
695
645
|
if (model === undefined) {
|
|
696
|
-
warn(
|
|
646
|
+
options.logger?.warn?.(
|
|
697
647
|
`[pantheon-delegate] no model resolved for agent "${agent}" — ` +
|
|
698
|
-
|
|
648
|
+
'omitting model so OpenCode can inherit it from the parent session',
|
|
699
649
|
)
|
|
700
650
|
return { model: undefined, error: undefined }
|
|
701
651
|
}
|
|
702
|
-
|
|
703
|
-
const missingVar = missingProviderKeyEnv(model.providerID, { env })
|
|
704
|
-
if (missingVar === undefined) return { model, error: undefined }
|
|
705
|
-
|
|
706
|
-
if (explicit) {
|
|
707
|
-
// Rule of precedence: the caller's explicit model is respected — warn only.
|
|
708
|
-
warn(
|
|
709
|
-
`[pantheon-delegate] model "${explicitModel}" provider "${model.providerID}" requires ` +
|
|
710
|
-
`API key ${missingVar} (unset) — respecting the explicit model anyway`,
|
|
711
|
-
)
|
|
712
|
-
return { model, error: undefined }
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Auto-resolved (agentModels / preset) → try the known-good fallback.
|
|
716
|
-
const fallback = splitModelRef(FALLBACK_MODEL)
|
|
717
|
-
if (
|
|
718
|
-
fallback !== undefined &&
|
|
719
|
-
(fallback.providerID === 'opencode-go' ||
|
|
720
|
-
missingProviderKeyEnv(fallback.providerID, { env }) === undefined)
|
|
721
|
-
) {
|
|
722
|
-
warn(
|
|
723
|
-
`[pantheon-delegate] provider "${model.providerID}" requires API key ${missingVar} ` +
|
|
724
|
-
`(unset) — falling back to ${FALLBACK_MODEL}`,
|
|
725
|
-
)
|
|
726
|
-
return { model: fallback, error: undefined }
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const message =
|
|
730
|
-
`pantheon_delegate: no usable model for agent "${agent}" — provider "${model.providerID}" ` +
|
|
731
|
-
`requires API key (set ${missingVar} or PANTHEON_MODEL_PRESET)`
|
|
732
|
-
warn(`[pantheon-delegate] ${message}`)
|
|
733
|
-
return { model: undefined, error: message }
|
|
652
|
+
return { model, error: undefined }
|
|
734
653
|
}
|
|
735
654
|
|
|
736
655
|
// ─── Factory ───────────────────────────────────────────────────────────
|
|
@@ -900,13 +819,8 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
|
|
|
900
819
|
// session.create failure → return a clear error as TEXT (tools return
|
|
901
820
|
// errors as text, not thrown) and register NO job on the board — an
|
|
902
821
|
// unhandled rejection here would otherwise lose the failure entirely.
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
// preset) is used when its provider key is configured; an explicit
|
|
906
|
-
// caller model is always respected (warned); an auto-resolved model
|
|
907
|
-
// whose provider key is missing falls back to the known-good
|
|
908
|
-
// opencode-go/deepseek-v4-flash; if nothing usable remains the
|
|
909
|
-
// failure is returned as TEXT below — no session, no board job.
|
|
822
|
+
// Only an explicit delegate model or routing profile model is sent.
|
|
823
|
+
// Otherwise omit it so OpenCode inherits the parent model.
|
|
910
824
|
const resolved = resolveUsableChildModel(options, args.agent, args.model)
|
|
911
825
|
if (resolved.error !== undefined) {
|
|
912
826
|
if (budgetReserved && budgetKey !== undefined)
|
|
@@ -86,13 +86,14 @@ export declare function loadPresetDefs(routingPath?: string): Record<string, Pre
|
|
|
86
86
|
|
|
87
87
|
export interface RoutingAgentModelsOptions {
|
|
88
88
|
routingPath?: string
|
|
89
|
+
candidates?: string[]
|
|
90
|
+
env?: Record<string, string | undefined>
|
|
89
91
|
logger?: { warn?: (msg: string) => void }
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
/**
|
|
93
|
-
* Lowercase agent → "provider/model" mapping from
|
|
94
|
-
*
|
|
95
|
-
* options.agentModels. Fail-open: missing/corrupt routing.yml → {}.
|
|
95
|
+
* Lowercase agent → "provider/model" mapping from the explicitly active
|
|
96
|
+
* routing profile. No active profile (or a profile without model) → {}.
|
|
96
97
|
*/
|
|
97
98
|
export declare function loadRoutingAgentModels(
|
|
98
99
|
options?: RoutingAgentModelsOptions,
|
package/src/pantheon/presets.mjs
CHANGED
|
@@ -345,46 +345,42 @@ export function loadRoutingPermissionTask({ routingPath, logger = console } = {}
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
|
-
* Load the
|
|
348
|
+
* Load the active routing profile's agent → model mapping for the delegation
|
|
349
349
|
* toolset (Fase 6 — wiring agentModels).
|
|
350
350
|
*
|
|
351
351
|
* routing.yml keeps per-agent models ONLY inside presets
|
|
352
352
|
* (`presets.<name>.agents.<agent>.model`) — the top-level `agents:` section
|
|
353
|
-
* carries no model field.
|
|
354
|
-
*
|
|
355
|
-
* delegated child always gets a sane per-agent model even when no preset is
|
|
356
|
-
* active — delegation no longer depends 100% on the active preset (branch (b)
|
|
357
|
-
* of resolveChildModel, which outranks the active-preset branch (c)).
|
|
353
|
+
* carries no model field. No profile means an empty mapping; there is no
|
|
354
|
+
* implicit first/default preset or fallback model.
|
|
358
355
|
*
|
|
359
|
-
* Fail-open: a missing or unparseable routing.yml yields {}
|
|
360
|
-
* the previous behavior — active preset / opencode default) and warns via the
|
|
356
|
+
* Fail-open: a missing or unparseable routing.yml yields {} and warns via the
|
|
361
357
|
* optional logger; it NEVER throws at startup.
|
|
362
358
|
*
|
|
363
359
|
* @param {object} [opts]
|
|
364
360
|
* @param {string} [opts.routingPath]
|
|
361
|
+
* @param {string[]} [opts.candidates]
|
|
362
|
+
* @param {Record<string, string|undefined>} [opts.env]
|
|
365
363
|
* @param {{warn?: Function}} [opts.logger]
|
|
366
364
|
* @returns {Record<string, string>} lowercase agent → "provider/model"
|
|
367
365
|
*/
|
|
368
|
-
export function loadRoutingAgentModels({ routingPath, logger = console } = {}) {
|
|
369
|
-
let defs
|
|
366
|
+
export function loadRoutingAgentModels({ routingPath, candidates, env, logger = console } = {}) {
|
|
370
367
|
try {
|
|
371
|
-
|
|
368
|
+
const active = resolveActivePreset({ routingPath, candidates, env, logger })
|
|
369
|
+
if (active === null) return {}
|
|
370
|
+
const models = {}
|
|
371
|
+
for (const [agent, spec] of Object.entries(active.agents ?? {})) {
|
|
372
|
+
if (spec && typeof spec === 'object' && typeof spec.model === 'string' && spec.model !== '') {
|
|
373
|
+
models[agent.toLowerCase()] = spec.model
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return models
|
|
372
377
|
} catch (err) {
|
|
373
378
|
const reason = err instanceof Error ? err.message : String(err)
|
|
374
379
|
logger.warn?.(
|
|
375
|
-
`presets: routing.yml
|
|
380
|
+
`presets: routing.yml active profile models unavailable (${reason}) — child model omitted`,
|
|
376
381
|
)
|
|
377
382
|
return {}
|
|
378
383
|
}
|
|
379
|
-
const names = Object.keys(defs)
|
|
380
|
-
if (names.length === 0) return {}
|
|
381
|
-
const models = {}
|
|
382
|
-
for (const [agent, spec] of Object.entries(defs[names[0]]?.agents ?? {})) {
|
|
383
|
-
if (spec && typeof spec === 'object' && typeof spec.model === 'string' && spec.model !== '') {
|
|
384
|
-
models[agent.toLowerCase()] = spec.model
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return models
|
|
388
384
|
}
|
|
389
385
|
|
|
390
386
|
/**
|
package/src/plugin.ts
CHANGED
|
@@ -4,7 +4,6 @@ import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
|
4
4
|
import type { PluginConfig } from 'opencode'
|
|
5
5
|
import { BackgroundJobBoard } from './pantheon/background-job-board.ts'
|
|
6
6
|
import { createCommandNormalizer } from './pantheon/command-normalizer.ts'
|
|
7
|
-
import { reassertAfterCompaction } from './pantheon/compaction-assert.ts'
|
|
8
7
|
import {
|
|
9
8
|
type ContextSandboxConfig,
|
|
10
9
|
createContextSandbox,
|
|
@@ -29,6 +28,7 @@ import {
|
|
|
29
28
|
zeusReadGuard,
|
|
30
29
|
} from './pantheon/delegation-enforce.ts'
|
|
31
30
|
import { handleDelegationEvent } from './pantheon/delegation-notify.ts'
|
|
31
|
+
import { showDelegationTerminalToast, type ToastClient } from './pantheon/delegation-toast.ts'
|
|
32
32
|
import { FilePersistenceAdapter } from './pantheon/file-persistence.ts'
|
|
33
33
|
import { GOAL_LOOP_DEFAULTS, GoalLoop, GoalStore } from './pantheon/goal-loop.ts'
|
|
34
34
|
import { createReadEnhancer } from './pantheon/hashline/read-enhancer.ts'
|
|
@@ -62,17 +62,6 @@ import { activePresetCandidates, createVisionHandler } from './pantheon/vision.t
|
|
|
62
62
|
// .pantheon/logs/hooks.log; the console echo is opt-in via PANTHEON_HOOKS_LOG=1.
|
|
63
63
|
const log = createPantheonLogger({ module: 'pantheon-plugin' })
|
|
64
64
|
|
|
65
|
-
// Fase 6 (release-134): the delegation toolset's options.agentModels (branch
|
|
66
|
-
// (b) of resolveChildModel) is wired from routing.yml's default (first)
|
|
67
|
-
// preset — a STATIC per-agent model mapping independent of the active preset,
|
|
68
|
-
// so delegated children get a sane model even without an active preset (the
|
|
69
|
-
// previous production wiring left branch (b) dead: delegation depended 100%
|
|
70
|
-
// on the active preset). Built ONCE at module load, never per dispatch.
|
|
71
|
-
// Fail-open: a missing/corrupt routing.yml yields {} (warned by the helper)
|
|
72
|
-
// and delegation falls back to the active preset / opencode default — the
|
|
73
|
-
// plugin never throws at startup.
|
|
74
|
-
const routingAgentModels = loadRoutingAgentModels({ logger: log })
|
|
75
|
-
|
|
76
65
|
// R4: per-agent step caps from routing.yml `agents.<name>.max_steps`
|
|
77
66
|
// (fail-open → {} = no agent capped). R1 retry_policy/cooldown are NOT
|
|
78
67
|
// wired here — the plugin has no retry path (opencode cannot intercept task
|
|
@@ -90,6 +79,7 @@ const board = new BackgroundJobBoard({
|
|
|
90
79
|
})
|
|
91
80
|
const persistence = new FilePersistenceAdapter('.pantheon/board/state.json')
|
|
92
81
|
board.setPersistence(persistence)
|
|
82
|
+
let terminalToastClient: ToastClient = {}
|
|
93
83
|
board
|
|
94
84
|
.recoverRunningJobs()
|
|
95
85
|
.catch((err) => log.error('[Pantheon Plugin] Failed to recover running jobs:', err))
|
|
@@ -108,6 +98,8 @@ board.onTerminal((taskID: string) => {
|
|
|
108
98
|
log.info(
|
|
109
99
|
`[Pantheon Plugin] Board terminal: [${job.alias}] ${job.description} → ${job.state}${job.resultSummary ? ` — ${job.resultSummary}` : ''}`,
|
|
110
100
|
)
|
|
101
|
+
// Deliberately direct-to-TUI: onTerminal must not inject chat/transcript text.
|
|
102
|
+
void showDelegationTerminalToast(terminalToastClient, job)
|
|
111
103
|
})
|
|
112
104
|
|
|
113
105
|
// Periodically prune terminal/reconciled jobs (24h TTL, every 30 min).
|
|
@@ -223,8 +215,7 @@ function adaptDelegationClient(client: PluginInput['client']): DelegationClient
|
|
|
223
215
|
model?: { id: string; providerID: string }
|
|
224
216
|
} = { parentID: input.body.parentID }
|
|
225
217
|
if (input.body.title !== undefined) body.title = input.body.title
|
|
226
|
-
// Forward
|
|
227
|
-
// falls back to the key-gated default model and dies at startup.
|
|
218
|
+
// Forward a model only when the active routing profile supplied one.
|
|
228
219
|
if (input.body.model !== undefined) body.model = input.body.model
|
|
229
220
|
const result = await client.session.create({ body })
|
|
230
221
|
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
@@ -343,6 +334,7 @@ function adaptTodoEnforcerClient(client: PluginInput['client']): TodoEnforcerCli
|
|
|
343
334
|
* Helpers live in src/pantheon/vision.ts and are imported from there directly.
|
|
344
335
|
*/
|
|
345
336
|
const plugin: Plugin = async (input: PluginInput) => {
|
|
337
|
+
terminalToastClient = input.client as unknown as ToastClient
|
|
346
338
|
// release-134: runtime guard against DOUBLE registration (duplicate toasts +
|
|
347
339
|
// duplicate task_ids in one process — 2026-08-12 live logs). opencode
|
|
348
340
|
// merges the global config (npm-package plugin paths) with the project
|
|
@@ -377,7 +369,12 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
377
369
|
},
|
|
378
370
|
enforceRuntimeMatrix: true,
|
|
379
371
|
readOnlyAgents: new Set(['apollo', 'gaia']),
|
|
380
|
-
|
|
372
|
+
// Only an explicitly active routing profile may provide child models.
|
|
373
|
+
// An empty map deliberately lets OpenCode inherit the parent model.
|
|
374
|
+
agentModels: loadRoutingAgentModels({
|
|
375
|
+
candidates: activePresetCandidates(),
|
|
376
|
+
logger: log,
|
|
377
|
+
}),
|
|
381
378
|
delegationBudgets,
|
|
382
379
|
stepCapTracker,
|
|
383
380
|
...(routingPermissionTask != null ? { permissionTask: routingPermissionTask } : {}),
|
|
@@ -546,9 +543,7 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
546
543
|
'chat.message': async (hookInput, output) => {
|
|
547
544
|
await vision.chatMessage(hookInput, output)
|
|
548
545
|
// No delegation notification delivery here — the user policy is ZERO
|
|
549
|
-
// notification text injected into the chat transcript
|
|
550
|
-
// notifier.flushQueue prepend). pantheon-hooks' chat-reminders.ts keeps
|
|
551
|
-
// its own <system-reminder> channel, untouched.
|
|
546
|
+
// notification text injected into the chat transcript.
|
|
552
547
|
// Wave 1: a user message is activity — the todo enforcer's
|
|
553
548
|
// user-activity gate skips injection for userActivityQuietMs afterwards.
|
|
554
549
|
todoEnforcer.noteUserActivity(hookInput.sessionID)
|
|
@@ -573,17 +568,6 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
573
568
|
// rewritten with the exact list (see todo-preserve.ts). Fail-open.
|
|
574
569
|
if (ev.type === 'session.compacted') {
|
|
575
570
|
await todoPreserver.onCompacted(ev.properties.sessionID)
|
|
576
|
-
// release-134 Phase 4: re-assert post-compaction state — enqueue a
|
|
577
|
-
// fresh-state reminder (running/unread board jobs + active goals) for
|
|
578
|
-
// the session's next chat.message delivery. Fail-open (never throws).
|
|
579
|
-
await reassertAfterCompaction({
|
|
580
|
-
sessionID: ev.properties.sessionID,
|
|
581
|
-
board,
|
|
582
|
-
goals: {
|
|
583
|
-
enabled: GOAL_LOOP_DEFAULTS.enabled,
|
|
584
|
-
list: (s: string) => goalStore.list(s),
|
|
585
|
-
},
|
|
586
|
-
})
|
|
587
571
|
}
|
|
588
572
|
// Phase 3: observe completion on child sessions → finalizeDelegation.
|
|
589
573
|
// The board transition fires onTerminal → the file-only audit log (no
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
* PANTHEON_TOASTS=council → {errors, council}
|
|
80
80
|
* PANTHEON_TOASTS=all → {errors, delegations, council}
|
|
81
81
|
* Unknown values fall back to the default. The gate controls the TUI
|
|
82
|
-
* display
|
|
82
|
+
* display only — every fired toast is also
|
|
83
83
|
* recorded to the structured log + hooks.log (script 'toast', level
|
|
84
84
|
* 'info') so the toast trail stays auditable at any setting.
|
|
85
85
|
* - Severity priority (2026-08-06): errors/blockers — hook failures
|
|
@@ -96,15 +96,9 @@
|
|
|
96
96
|
* One group buffer at a time (groups rarely overlap); a task call more
|
|
97
97
|
* than OLYMPIANS_DETECT_WINDOW_MS after the previous one starts a fresh
|
|
98
98
|
* group.
|
|
99
|
-
* - session.idle
|
|
100
|
-
* `session.idle
|
|
101
|
-
*
|
|
102
|
-
* "✅ <agent> concluiu" lines collapse into "✅ N agentes concluídos (...)")
|
|
103
|
-
* and re-armed with a fresh timestamp — so the completion summary
|
|
104
|
-
* survives until the next user message (delivered via chat.message) instead
|
|
105
|
-
* of expiring after CHAT_REMINDER_TTL_MS while the user reads the reply.
|
|
106
|
-
* Partial groups (some members completed, some never reported) and
|
|
107
|
-
* interrupted council counters are also flushed/reset here.
|
|
99
|
+
* - session.idle handling (2026-08-06): the `event` hook additionally handles
|
|
100
|
+
* `session.idle` to reset partial aggregation state and interrupted council
|
|
101
|
+
* counters. Completion visibility remains in the board and TUI only.
|
|
108
102
|
* - Council notifications (new category 2026-08-06): a `task` whose prompt/
|
|
109
103
|
* description matches "council"/"conselho"/"synthesi" is treated as council
|
|
110
104
|
* activity — "🏛️ Council: especialistas consultados" on start and
|
|
@@ -122,11 +116,8 @@
|
|
|
122
116
|
* with wrk"). The upstream fix (server-side workspace tagging) only exists
|
|
123
117
|
* in NEWER opencode versions. THEREFORE on 1.18.13 the toast path below is
|
|
124
118
|
* a documented TUI no-op and agent-lifecycle feedback is surfaced via the
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* <system-reminder> text part into the next user message. Toasts stay in
|
|
128
|
-
* place for future opencode versions that render them — upgrade opencode
|
|
129
|
-
* for real TUI toasts.
|
|
119
|
+
* no transcript fallback is used. Toasts stay in place for future opencode
|
|
120
|
+
* versions that render them — upgrade opencode for real TUI toasts.
|
|
130
121
|
* - tool.execute.after payload fix (P1-1 follow-up, 2026-08-06): the after
|
|
131
122
|
* hook receives `(input, output)` where `output` = {title, output,
|
|
132
123
|
* metadata} — the REAL tool result. It is now merged into the payload
|
|
@@ -185,13 +176,6 @@
|
|
|
185
176
|
import { appendFile, mkdir } from 'node:fs/promises'
|
|
186
177
|
import { join } from 'node:path'
|
|
187
178
|
import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
188
|
-
import type { Part } from '@opencode-ai/sdk'
|
|
189
|
-
import {
|
|
190
|
-
CHAT_REMINDER_TTL_MS,
|
|
191
|
-
drainFreshChatReminders,
|
|
192
|
-
enqueueChatReminder,
|
|
193
|
-
pendingChatReminders,
|
|
194
|
-
} from '../pantheon/chat-reminders.ts'
|
|
195
179
|
import { pantheonPluginOnce } from '../pantheon/plugin-once.ts'
|
|
196
180
|
import { type HookPayload, type HookResult, runHook } from './hook-runner.ts'
|
|
197
181
|
|
|
@@ -231,7 +215,7 @@ type ToastCategory = 'errors' | 'delegations' | 'council'
|
|
|
231
215
|
* of enabled categories; every notification declares its category and is
|
|
232
216
|
* skipped unless enabled. "off" = empty set, "all" = every category. Unknown
|
|
233
217
|
* values fall back to the default. The gate controls the TUI display +
|
|
234
|
-
*
|
|
218
|
+
* display only — the structured log + hooks.log channels
|
|
235
219
|
* always write (every fired toast is recorded via reportFailure with script
|
|
236
220
|
* 'toast').
|
|
237
221
|
*/
|
|
@@ -427,26 +411,8 @@ const BACKGROUND_DISPATCH_TEXT_RE =
|
|
|
427
411
|
const TASK_ID_TEXT_RE = /\btask\s+id="(ses_[^"]+)"/i
|
|
428
412
|
|
|
429
413
|
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* that would fire a toast is ALSO queued here and injected into the next user
|
|
433
|
-
* message by the 'chat.message' hook as a single <system-reminder> text part.
|
|
434
|
-
*
|
|
435
|
-
* The buffer itself lives in the SHARED chat-reminders.ts module: this plugin
|
|
436
|
-
* file must export EXACTLY ONE function-valued export (legacy loader —
|
|
437
|
-
* see the IMPORTANT note at the top), so the enqueue API cannot be exported
|
|
438
|
-
* from here. Importing the shared module instead gives src/plugin.ts (a
|
|
439
|
-
* separate plugin file, loaded via plain dynamic import) the SAME buffer
|
|
440
|
-
* instance — release-134 Phase 4 (compaction-assert.ts) enqueues
|
|
441
|
-
* post-compaction state re-assertions here and this hook delivers them.
|
|
442
|
-
*
|
|
443
|
-
* Bounded: at most CHAT_REMINDER_MAX entries, each expiring
|
|
444
|
-
* CHAT_REMINDER_TTL_MS after its tool event. Consumed (and cleared) by the
|
|
445
|
-
* chat.message hook. Full-buffer enqueues are SKIPPED, never unbounded — the
|
|
446
|
-
* olympians/3-in-6s aggregates still cover delegation groups, matching the
|
|
447
|
-
* "throttled toasts are skipped, never backlogged" anti-spam philosophy.
|
|
448
|
-
* session.idle flushes the buffer into ONE fresh aggregated entry (see
|
|
449
|
-
* flushIdleReminders) so completions survive until the next user message.
|
|
414
|
+
* Delegation lifecycle state is intentionally not mirrored into the transcript.
|
|
415
|
+
* The board/list/read tools and TUI toasts are the supported visibility channels.
|
|
450
416
|
*/
|
|
451
417
|
|
|
452
418
|
/** Variants accepted by the opencode TUI showToast (TuiShowToastData.body.variant). */
|
|
@@ -462,11 +428,6 @@ type ToastOptions = {
|
|
|
462
428
|
dedupeKey?: string
|
|
463
429
|
/** Optional TUI toast headline (opencode showToast body.title). */
|
|
464
430
|
title?: string
|
|
465
|
-
/**
|
|
466
|
-
* Optional text for the 1.18.13 chat.message <system-reminder> fallback.
|
|
467
|
-
* Defaults to `message` when omitted — the reminder mirrors the toast.
|
|
468
|
-
*/
|
|
469
|
-
reminder?: string
|
|
470
431
|
}
|
|
471
432
|
|
|
472
433
|
/** Logging context threaded through safeRun — provided by the plugin input. */
|
|
@@ -757,10 +718,6 @@ async function notifyToast(ctx: HookContext, opts: ToastOptions): Promise<void>
|
|
|
757
718
|
toastShown.add(opts.dedupeKey)
|
|
758
719
|
}
|
|
759
720
|
if (!toastCategoryEnabled(opts.category)) return
|
|
760
|
-
// 1.18.13 fallback: queue the same signal for the chat.message hook (the
|
|
761
|
-
// TUI drops tui.toast.show on 1.18.13 — see header). Enqueued even if
|
|
762
|
-
// showToast fails, so headless/no-TUI servers still surface feedback.
|
|
763
|
-
enqueueChatReminder(opts.reminder ?? opts.message)
|
|
764
721
|
await ctx.client.tui.showToast({
|
|
765
722
|
body: {
|
|
766
723
|
...(opts.title !== undefined ? { title: opts.title } : {}),
|
|
@@ -907,7 +864,7 @@ async function agentFailedToast(
|
|
|
907
864
|
): Promise<void> {
|
|
908
865
|
const raw = result?.output ?? result?.title ?? ''
|
|
909
866
|
// Fix 1: the failure reason echoes tool RESULT content — mask any token so
|
|
910
|
-
// the toast/
|
|
867
|
+
// the toast/hooks.log trail never exposes a raw secret.
|
|
911
868
|
const reason = maskSecret(
|
|
912
869
|
trim(typeof raw === 'string' && raw.trim() !== '' ? raw : 'tool failed', 120),
|
|
913
870
|
)
|
|
@@ -1057,40 +1014,7 @@ function onCouncilDone(ctx: HookContext): void {
|
|
|
1057
1014
|
}
|
|
1058
1015
|
}
|
|
1059
1016
|
|
|
1060
|
-
/**
|
|
1061
|
-
* session.idle flush (see header): aggregate any pending chat reminders into
|
|
1062
|
-
* ONE fresh <system-reminder> entry (individual ✅ completions collapse into
|
|
1063
|
-
* "✅ N agentes concluídos (...)"), report partial groups whose members never
|
|
1064
|
-
* all completed, and reset interrupted council counters. The chat.message
|
|
1065
|
-
* hook remains the delivery path — the flushed entry has a fresh timestamp so
|
|
1066
|
-
* it survives until the next user message instead of expiring while the user
|
|
1067
|
-
* reads the reply.
|
|
1068
|
-
*/
|
|
1069
1017
|
function flushIdleReminders(ctx: HookContext): void {
|
|
1070
|
-
if (ENABLED_TOAST_CATEGORIES.size === 0) {
|
|
1071
|
-
pendingChatReminders.length = 0
|
|
1072
|
-
activeOlympians = null
|
|
1073
|
-
return
|
|
1074
|
-
}
|
|
1075
|
-
const now = Date.now()
|
|
1076
|
-
const fresh = pendingChatReminders.filter((r) => now - r.at <= CHAT_REMINDER_TTL_MS)
|
|
1077
|
-
pendingChatReminders.length = 0
|
|
1078
|
-
|
|
1079
|
-
// Partial group: some members completed but the group never finished (their
|
|
1080
|
-
// completion was suppressed waiting for the aggregate). Report the subset.
|
|
1081
|
-
const partialGroup = activeOlympians
|
|
1082
|
-
if (partialGroup !== null) {
|
|
1083
|
-
const done = [...partialGroup.agents].filter((a) => !partialGroup.pendingCompletions.has(a))
|
|
1084
|
-
if (partialGroup.detected && partialGroup.doneCount > 0 && done.length > 0) {
|
|
1085
|
-
fresh.push({
|
|
1086
|
-
text:
|
|
1087
|
-
done.length === 1
|
|
1088
|
-
? `✅ ${done[0]} concluiu`
|
|
1089
|
-
: `✅ ${done.length} agentes concluídos (${done.sort().join(', ')})`,
|
|
1090
|
-
at: now,
|
|
1091
|
-
})
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
1018
|
activeOlympians = null
|
|
1095
1019
|
|
|
1096
1020
|
// Interrupted council counters must not block a later council's verdict —
|
|
@@ -1105,34 +1029,12 @@ function flushIdleReminders(ctx: HookContext): void {
|
|
|
1105
1029
|
councilStartNotified = false
|
|
1106
1030
|
}
|
|
1107
1031
|
|
|
1108
|
-
// Collapse individual ✅ completions into one aggregate line.
|
|
1109
|
-
const doneAgents: string[] = []
|
|
1110
|
-
const others: string[] = []
|
|
1111
|
-
for (const r of fresh) {
|
|
1112
|
-
const m = /^✅ (.+?) concluiu$/.exec(r.text)
|
|
1113
|
-
if (m?.[1] !== undefined) doneAgents.push(m[1])
|
|
1114
|
-
else others.push(r.text)
|
|
1115
|
-
}
|
|
1116
|
-
const lines: string[] = []
|
|
1117
|
-
if (doneAgents.length > 0) {
|
|
1118
|
-
lines.push(
|
|
1119
|
-
doneAgents.length === 1
|
|
1120
|
-
? `✅ ${doneAgents[0]} concluiu`
|
|
1121
|
-
: `✅ ${doneAgents.length} agentes concluídos (${doneAgents.sort().join(', ')})`,
|
|
1122
|
-
)
|
|
1123
|
-
}
|
|
1124
|
-
lines.push(...others)
|
|
1125
|
-
if (lines.length === 0) return
|
|
1126
|
-
|
|
1127
|
-
// ONE aggregated entry, fresh timestamp — chat.message delivers it next.
|
|
1128
|
-
enqueueChatReminder(lines.join('\n'))
|
|
1129
|
-
// 1.3.4 dedup: log a SUMMARY here (count only), never the joined body — the
|
|
1130
|
-
// SAME lines used to be echoed twice (idle-flush with " | ", chat-reminder
|
|
1131
|
-
// delivery with "\n"). The content is logged exactly once, at delivery.
|
|
1132
1032
|
void reportFailure(
|
|
1133
1033
|
ctx,
|
|
1134
|
-
|
|
1135
|
-
{
|
|
1034
|
+
'[pantheon-hooks:idle-flush] delegation state reset',
|
|
1035
|
+
{
|
|
1036
|
+
script: 'idle-flush',
|
|
1037
|
+
},
|
|
1136
1038
|
'info',
|
|
1137
1039
|
)
|
|
1138
1040
|
}
|
|
@@ -1213,7 +1115,7 @@ const plugin: Plugin = async ({ client, directory }) => {
|
|
|
1213
1115
|
// merges the global config (npm-package plugin paths) with the project
|
|
1214
1116
|
// config (repo source paths), so this module loads from TWO filesystem
|
|
1215
1117
|
// paths in the same process and the factory would otherwise run twice —
|
|
1216
|
-
// every hook (tool.execute.before/after
|
|
1118
|
+
// every lifecycle hook (tool.execute.before/after and event) firing
|
|
1217
1119
|
// TWICE. The second invocation becomes a no-op (empty hooks object) so
|
|
1218
1120
|
// hooks register exactly once.
|
|
1219
1121
|
if (pantheonPluginOnce('pantheon:hooks')) {
|
|
@@ -1439,51 +1341,6 @@ const plugin: Plugin = async ({ client, directory }) => {
|
|
|
1439
1341
|
// Never let a hook take down the opencode session.
|
|
1440
1342
|
}
|
|
1441
1343
|
},
|
|
1442
|
-
// 1.18.13 fallback (Path C, oh-my-openagent pattern): the TUI drops
|
|
1443
|
-
// tui.toast.show events on opencode 1.18.13 (untagged — see header), so
|
|
1444
|
-
// queued agent-lifecycle signals (delegation start/done, hook failures,
|
|
1445
|
-
// olympians/council summaries) are injected here as ONE <system-reminder> text
|
|
1446
|
-
// part into the next user message, then cleared.
|
|
1447
|
-
// P0 (2026-08-11, live E2E — release blocker): this hook ALSO fires for the
|
|
1448
|
-
// child session's client.session.promptAsync (the old "subagent prompts use
|
|
1449
|
-
// a different path" claim is FALSE on 1.18.13), where input.messageID is
|
|
1450
|
-
// EMPTY/undefined. Injecting there with `?? ''` produced a part opencode's
|
|
1451
|
-
// schema rejects (SchemaError: Expected a string starting with "msg", got
|
|
1452
|
-
// "") → prompt_async failed → every delegation died in ~20ms. The guard
|
|
1453
|
-
// below skips injection ENTIRELY on that path WITHOUT draining the buffer,
|
|
1454
|
-
// so the reminder still lands on the parent's next real message.
|
|
1455
|
-
'chat.message': async (input, output) => {
|
|
1456
|
-
try {
|
|
1457
|
-
if (ENABLED_TOAST_CATEGORIES.size === 0) return
|
|
1458
|
-
if (pendingChatReminders.length === 0) return
|
|
1459
|
-
// Subagent promptAsync fires have no messageID — injecting any part
|
|
1460
|
-
// with an empty messageID crashes the child session (schema reject).
|
|
1461
|
-
// Early return BEFORE the buffer drain keeps the reminder queued for
|
|
1462
|
-
// the parent's next real (msg_-ID'd) message.
|
|
1463
|
-
if (!input.messageID) return
|
|
1464
|
-
const body = drainFreshChatReminders()
|
|
1465
|
-
if (body === undefined) return
|
|
1466
|
-
// Minimal TextPart — the core backfills messageID/sessionID for
|
|
1467
|
-
// hook-injected parts (see opencode chat.message trigger pipeline).
|
|
1468
|
-
// The guard above guarantees input.messageID is a non-empty string,
|
|
1469
|
-
// so no `?? ''` fallback is needed (an explicit "" is schema-rejected).
|
|
1470
|
-
output.parts.push({
|
|
1471
|
-
id: `prt_${crypto.randomUUID()}`,
|
|
1472
|
-
sessionID: input.sessionID,
|
|
1473
|
-
messageID: input.messageID,
|
|
1474
|
-
type: 'text',
|
|
1475
|
-
text: `<system-reminder>\n${body}\n</system-reminder>`,
|
|
1476
|
-
} satisfies Part)
|
|
1477
|
-
await reportFailure(
|
|
1478
|
-
ctx,
|
|
1479
|
-
`[pantheon-hooks:chat-reminder] ${body}`,
|
|
1480
|
-
{ script: 'chat-reminder' },
|
|
1481
|
-
'info',
|
|
1482
|
-
)
|
|
1483
|
-
} catch {
|
|
1484
|
-
// Never let a hook take down the opencode session.
|
|
1485
|
-
}
|
|
1486
|
-
},
|
|
1487
1344
|
event: async ({ event: ev }) => {
|
|
1488
1345
|
try {
|
|
1489
1346
|
if (ev.type === 'session.created') {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pantheon-tui",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.4.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@opencode-ai/plugin": ">=1.14.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.1",
|
|
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": {
|