pantheon-opencode 1.5.0-beta.8 → 1.5.0
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/AGENTS.md +14 -63
- package/CHANGELOG.md +320 -0
- package/README.md +14 -27
- package/README.pt-BR.md +5 -6
- package/commands/pantheon.md +1 -35
- package/package.json +1 -2
- package/plugin.json +1 -1
- package/pyproject.toml +2 -5
- package/scripts/doctor.mjs +32 -3
- package/scripts/install/plugin.mjs +9 -12
- package/scripts/mcp_persistence_server.py +1 -1
- package/scripts/sync-tui.mjs +8 -11
- package/src/agents/apollo.md +1 -0
- package/src/agents/gaia.md +1 -0
- package/src/agents/zeus.md +9 -52
- package/src/instructions/memory-protocol.instructions.md +6 -3
- package/src/instructions/zeus-anti-stall.instructions.md +2 -4
- package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
- package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
- package/src/mcp/mcp_persistence_server.py +1 -1
- package/src/mcp/toon_codec.py +2 -4
- package/src/pantheon/background-job-board.ts +4 -4
- package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
- package/src/pantheon/cost-command.ts +1 -2
- package/src/pantheon/delegation-enforce.ts +59 -14
- package/src/pantheon/goal-loop.ts +1 -1
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/idle-continuation.ts +4 -6
- package/src/pantheon/model-command.ts +1 -2
- package/src/pantheon/native-task-status.ts +2 -2
- package/src/pantheon/presets.d.mts +0 -20
- package/src/pantheon/presets.mjs +0 -64
- package/src/pantheon/session-guard.ts +2 -2
- package/src/pantheon/session-id.ts +30 -0
- package/src/pantheon/task-result-guard.ts +2 -3
- package/src/pantheon/todo-enforcer.ts +3 -3
- package/src/pantheon/tool-context.ts +23 -0
- package/src/pantheon/v2-bridge.ts +1 -4
- package/src/plugin-v2.ts +8 -21
- package/src/plugin.ts +61 -245
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +229 -77
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +509 -259
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +2 -2
- package/src/plugins/tui/src/index.tsx +753 -279
- package/src/routing.yml +0 -16
- package/src/skills/context-compression/SKILL.md +1 -1
- package/scripts/generate-preset-docs.mjs +0 -300
- package/scripts/generate-prompts.sh +0 -222
- package/scripts/hash_verify.py +0 -192
- package/scripts/install/component.mjs +0 -24
- package/scripts/install/migrate-config.mjs +0 -302
- package/scripts/test-context-compression.sh +0 -169
- package/src/pantheon/auto-wake.ts +0 -85
- package/src/pantheon/chat-reminders.ts +0 -9
- package/src/pantheon/comment-checker.ts +0 -48
- package/src/pantheon/compaction-assert.ts +0 -160
- package/src/pantheon/compaction-handoff.ts +0 -304
- package/src/pantheon/cost-tracker.ts +0 -108
- package/src/pantheon/delegate-manager.ts +0 -766
- package/src/pantheon/delegation-classifier.ts +0 -307
- package/src/pantheon/delegation-finalize.ts +0 -406
- package/src/pantheon/delegation-notify.ts +0 -212
- package/src/pantheon/delegation-toast.ts +0 -73
- package/src/pantheon/delegation.ts +0 -1337
- package/src/pantheon/dispatch-guard.ts +0 -109
- package/src/pantheon/native-probe.ts +0 -234
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/preemptive-compact.ts +0 -76
- package/src/pantheon/provider-capabilities.ts +0 -221
- package/src/pantheon/provider-stream-tracker.ts +0 -182
- package/src/pantheon/retry-policy.ts +0 -212
- package/src/pantheon/step-cap.ts +0 -110
- package/src/pantheon/token-opt.ts +0 -689
- package/src/pantheon/toon-codec.ts +0 -481
- package/src/pantheon/v2-events.ts +0 -148
- package/src/pantheon/v2-hooks.ts +0 -287
- package/src/pantheon/v2-tool-definitions.ts +0 -331
- package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
- package/src/plugins/tui/dist/tui.tsx +0 -2777
package/src/plugin.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
1
|
import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
4
2
|
import type { PluginConfig } from 'opencode'
|
|
5
3
|
import { createCommandNormalizer } from './pantheon/command-normalizer.ts'
|
|
4
|
+
import { buildCompactionContext } from './pantheon/compaction-context.ts'
|
|
6
5
|
import {
|
|
7
6
|
type ContextSandboxConfig,
|
|
8
7
|
createContextSandbox,
|
|
@@ -11,24 +10,13 @@ import {
|
|
|
11
10
|
DEFAULT_LIMITS as SANDBOX_DEFAULT_LIMITS,
|
|
12
11
|
} from './pantheon/context-sandbox.ts'
|
|
13
12
|
import { createCostCommand } from './pantheon/cost-command.ts'
|
|
14
|
-
import { buildPluginNativeDelegation, resolveDelegateMode } from './pantheon/delegate-manager.ts'
|
|
15
|
-
import {
|
|
16
|
-
collectRootSessionIDs,
|
|
17
|
-
createDelegationTools,
|
|
18
|
-
type DelegationClient,
|
|
19
|
-
resolveDelegationBudgets,
|
|
20
|
-
resolveDelegationTimeoutMs,
|
|
21
|
-
startIdleChildScan,
|
|
22
|
-
} from './pantheon/delegation.ts'
|
|
23
|
-
import { buildCompactionContext } from './pantheon/delegation-compaction.ts'
|
|
24
13
|
import {
|
|
25
14
|
createEnforcementGuard,
|
|
26
15
|
readOnlyRegistry,
|
|
27
16
|
SessionHierarchyRegistry,
|
|
17
|
+
syncReadOnlySession,
|
|
28
18
|
zeusReadGuard,
|
|
29
19
|
} from './pantheon/delegation-enforce.ts'
|
|
30
|
-
import { handleDelegationEvent } from './pantheon/delegation-notify.ts'
|
|
31
|
-
import { showDelegationTerminalToast, type ToastClient } from './pantheon/delegation-toast.ts'
|
|
32
20
|
import { GOAL_LOOP_DEFAULTS, GoalLoop, GoalStore } from './pantheon/goal-loop.ts'
|
|
33
21
|
import { createReadEnhancer } from './pantheon/hashline/read-enhancer.ts'
|
|
34
22
|
import { createHashlineEditTool } from './pantheon/hashline/tool.ts'
|
|
@@ -36,15 +24,9 @@ import { createIdleDispatcher } from './pantheon/idle-continuation.ts'
|
|
|
36
24
|
import { createPantheonLogger } from './pantheon/logger.ts'
|
|
37
25
|
import { createModelCommand } from './pantheon/model-command.ts'
|
|
38
26
|
import { pantheonPluginOnce } from './pantheon/plugin-once.ts'
|
|
39
|
-
import {
|
|
40
|
-
applyActivePresetToConfig,
|
|
41
|
-
loadRoutingAgentModels,
|
|
42
|
-
loadRoutingMaxSteps,
|
|
43
|
-
loadRoutingPermissionTask,
|
|
44
|
-
} from './pantheon/presets.mjs'
|
|
27
|
+
import { applyActivePresetToConfig } from './pantheon/presets.mjs'
|
|
45
28
|
import { safeSessionPath } from './pantheon/session-guard.ts'
|
|
46
29
|
import { getSharedBoard } from './pantheon/shared-board.ts'
|
|
47
|
-
import { StepCapTracker } from './pantheon/step-cap.ts'
|
|
48
30
|
import { createTaskResultGuard } from './pantheon/task-result-guard.ts'
|
|
49
31
|
import {
|
|
50
32
|
TODO_ENFORCER_DEFAULTS,
|
|
@@ -65,23 +47,10 @@ import { activePresetCandidates, createVisionHandler } from './pantheon/vision.t
|
|
|
65
47
|
// .pantheon/logs/hooks.log; the console echo is opt-in via PANTHEON_HOOKS_LOG=1.
|
|
66
48
|
const log = createPantheonLogger({ module: 'pantheon-plugin' })
|
|
67
49
|
|
|
68
|
-
// R4: per-agent step caps from routing.yml `agents.<name>.max_steps`
|
|
69
|
-
// (fail-open → {} = no agent capped). R1 retry_policy/cooldown are NOT
|
|
70
|
-
// wired here — the plugin has no retry path (opencode cannot intercept task
|
|
71
|
-
// completion); zeus waves load them via presets.mjs loaders and pass them
|
|
72
|
-
// to zeusDelegateWithRetry explicitly.
|
|
73
|
-
const stepCapTracker = new StepCapTracker(loadRoutingMaxSteps({ logger: log }))
|
|
74
|
-
|
|
75
|
-
// O5: permission.task glob rules from routing.yml `permission.task`
|
|
76
|
-
// (fail-open → undefined = existing runtime matrix only).
|
|
77
|
-
const routingPermissionTask = loadRoutingPermissionTask({ logger: log })
|
|
78
|
-
|
|
79
50
|
// beta.5: the board is a PROCESS-wide singleton (getSharedBoard) shared with
|
|
80
51
|
// src/plugins/pantheon-hooks.ts, whose native-task mirror registers children
|
|
81
|
-
// here
|
|
82
|
-
// SAME instance. globalThis anchoring dedupes the npm+repo double load.
|
|
52
|
+
// here. globalThis anchoring dedupes the npm+repo double load.
|
|
83
53
|
const board = getSharedBoard()
|
|
84
|
-
let terminalToastClient: ToastClient = {}
|
|
85
54
|
board
|
|
86
55
|
.recoverRunningJobs()
|
|
87
56
|
.catch((err) => log.error('[Pantheon Plugin] Failed to recover running jobs:', err))
|
|
@@ -91,17 +60,14 @@ board
|
|
|
91
60
|
// file-only log line (console echo opt-in via PANTHEON_HOOKS_LOG). There is
|
|
92
61
|
// deliberately NO chat delivery — the user policy is zero delegation
|
|
93
62
|
// notifications in the transcript. Completion visibility lives in the board
|
|
94
|
-
// `[unread]` marker
|
|
95
|
-
//
|
|
96
|
-
// carry-forward.
|
|
63
|
+
// `[unread]` marker and the TUI toasts owned by pantheon-hooks
|
|
64
|
+
// (PANTHEON_TOASTS gate).
|
|
97
65
|
board.onTerminal((taskID: string) => {
|
|
98
66
|
const job = board.get(taskID)
|
|
99
67
|
if (!job) return
|
|
100
68
|
log.info(
|
|
101
69
|
`[Pantheon Plugin] Board terminal: [${job.alias}] ${job.description} → ${job.state}${job.resultSummary ? ` — ${job.resultSummary}` : ''}`,
|
|
102
70
|
)
|
|
103
|
-
// Deliberately direct-to-TUI: onTerminal must not inject chat/transcript text.
|
|
104
|
-
void showDelegationTerminalToast(terminalToastClient, job)
|
|
105
71
|
})
|
|
106
72
|
|
|
107
73
|
// Periodically prune terminal/reconciled jobs (24h TTL, every 30 min).
|
|
@@ -127,10 +93,11 @@ function isRootSession(sessionID: string): boolean {
|
|
|
127
93
|
return rootSessions.has(sessionID) || sessionHierarchy.isRoot(sessionID)
|
|
128
94
|
}
|
|
129
95
|
|
|
130
|
-
// The read-only registry is populated by
|
|
131
|
-
//
|
|
132
|
-
// The guard denies edit/write/bash/task
|
|
133
|
-
// from tool.execute.before; unknown sessions
|
|
96
|
+
// The read-only registry is populated by the chat.params hook below, when a
|
|
97
|
+
// session's active agent is read-only (apollo/gaia — READ_ONLY_AGENTS /
|
|
98
|
+
// syncReadOnlySession). The guard denies edit/write/bash/task/hashline_edit
|
|
99
|
+
// inside those sessions by throwing from tool.execute.before; unknown sessions
|
|
100
|
+
// are allowed (safe default).
|
|
134
101
|
const enforcementGuard = createEnforcementGuard({
|
|
135
102
|
getReadOnlySessions: () => readOnlyRegistry.sessionIDs(),
|
|
136
103
|
options: {
|
|
@@ -148,10 +115,7 @@ const enforcementGuard = createEnforcementGuard({
|
|
|
148
115
|
// to the shared hooks.log for @nyx frequency measurement.
|
|
149
116
|
const commandNormalizer = createCommandNormalizer({ logger: log })
|
|
150
117
|
|
|
151
|
-
//
|
|
152
|
-
const COMPACTION_MAX_ITEMS = 10
|
|
153
|
-
|
|
154
|
-
// Root-session registry for the delegation depth guard: sessions created
|
|
118
|
+
// Root-session registry for the read-only enforcement guard: sessions created
|
|
155
119
|
// without a parentID are roots. Populated from TWO sources:
|
|
156
120
|
// 1. the `session.created` event hook (live roots — fires for sessions
|
|
157
121
|
// created while the plugin runs);
|
|
@@ -159,20 +123,30 @@ const COMPACTION_MAX_ITEMS = 10
|
|
|
159
123
|
// hook (compaction-134) — opencode does NOT replay session.created
|
|
160
124
|
// events for sessions that exist before plugin load, so without the
|
|
161
125
|
// seed a resumed root would never enter this set after a restart.
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// works even when this set is incomplete.
|
|
126
|
+
// The guard treats unknown sessions as roots, so this set is an enrichment
|
|
127
|
+
// that improves accuracy but is never required for correctness.
|
|
165
128
|
const rootSessions = new Set<string>()
|
|
166
129
|
const sessionHierarchy = new SessionHierarchyRegistry()
|
|
167
130
|
const sessionAgents = new Map<string, string>()
|
|
168
131
|
const sessionToolCeilings = new Map<string, ToolCeilingResult>()
|
|
169
132
|
|
|
133
|
+
/** Pure helper: collect the IDs of root sessions (parentID === undefined). */
|
|
134
|
+
function collectRootSessionIDs(
|
|
135
|
+
sessions: ReadonlyArray<{ id: string; parentID?: string }>,
|
|
136
|
+
): Set<string> {
|
|
137
|
+
const roots = new Set<string>()
|
|
138
|
+
for (const session of sessions) {
|
|
139
|
+
if (session.parentID === undefined) roots.add(session.id)
|
|
140
|
+
}
|
|
141
|
+
return roots
|
|
142
|
+
}
|
|
143
|
+
|
|
170
144
|
/**
|
|
171
145
|
* Seed resumed root sessions without making OpenCode startup depend on the
|
|
172
146
|
* session API responding. Some OpenCode environments do not have the API
|
|
173
147
|
* ready while plugin config hooks run; awaiting session.list() there leaves
|
|
174
|
-
* the TUI stuck before it can open. The
|
|
175
|
-
*
|
|
148
|
+
* the TUI stuck before it can open. The enforcement guard treats an unknown
|
|
149
|
+
* session as a root, so this enrichment is deliberately best-effort.
|
|
176
150
|
*/
|
|
177
151
|
function seedRootSessionsInBackground(input: PluginInput): void {
|
|
178
152
|
// Fail-open: environments without the session API (client.session absent or
|
|
@@ -209,62 +183,6 @@ function sdkErrorMessage(error: { data?: { message?: string } } | null | undefin
|
|
|
209
183
|
return typeof message === 'string' && message.trim() !== '' ? message : 'request failed'
|
|
210
184
|
}
|
|
211
185
|
|
|
212
|
-
/**
|
|
213
|
-
* Adapt the opencode SDK client to the structural DelegationClient the
|
|
214
|
-
* delegation toolset expects: the SDK wraps every call in a
|
|
215
|
-
* `{data, error, request, response}` result while delegation.ts uses direct
|
|
216
|
-
* returns. Errors surface as throws so the toolset's own error handling
|
|
217
|
-
* (tool errors returned as TEXT) keeps working.
|
|
218
|
-
*/
|
|
219
|
-
function adaptDelegationClient(client: PluginInput['client']): DelegationClient {
|
|
220
|
-
return {
|
|
221
|
-
session: {
|
|
222
|
-
create: async (input) => {
|
|
223
|
-
const body: {
|
|
224
|
-
parentID?: string
|
|
225
|
-
title?: string
|
|
226
|
-
model?: { id: string; providerID: string }
|
|
227
|
-
} = { parentID: input.body.parentID }
|
|
228
|
-
if (input.body.title !== undefined) body.title = input.body.title
|
|
229
|
-
// Forward a model only when the active routing profile supplied one.
|
|
230
|
-
if (input.body.model !== undefined) body.model = input.body.model
|
|
231
|
-
const result = await client.session.create({ body })
|
|
232
|
-
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
233
|
-
return { id: result.data.id }
|
|
234
|
-
},
|
|
235
|
-
promptAsync: async (input) => {
|
|
236
|
-
const path = safeSessionPath(input.path.id)
|
|
237
|
-
if (!path) {
|
|
238
|
-
log.warn(`[Pantheon Plugin] promptAsync skipped: invalid sessionID "${input.path.id}"`)
|
|
239
|
-
throw new Error(`invalid sessionID: ${input.path.id}`)
|
|
240
|
-
}
|
|
241
|
-
const result = await client.session.promptAsync({
|
|
242
|
-
path: path.path,
|
|
243
|
-
body: {
|
|
244
|
-
agent: input.body.agent,
|
|
245
|
-
...(input.body.model !== undefined
|
|
246
|
-
? { model: { providerID: input.body.model.providerID, modelID: input.body.model.id } }
|
|
247
|
-
: {}),
|
|
248
|
-
parts: input.body.parts,
|
|
249
|
-
},
|
|
250
|
-
})
|
|
251
|
-
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
252
|
-
return result.data
|
|
253
|
-
},
|
|
254
|
-
messages: async (input) => {
|
|
255
|
-
const path = safeSessionPath(input.path.id)
|
|
256
|
-
if (!path) {
|
|
257
|
-
log.warn(`[Pantheon Plugin] messages skipped: invalid sessionID "${input.path.id}"`)
|
|
258
|
-
return []
|
|
259
|
-
}
|
|
260
|
-
const result = await client.session.messages({ path: path.path })
|
|
261
|
-
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
262
|
-
return result.data
|
|
263
|
-
},
|
|
264
|
-
},
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
186
|
/**
|
|
269
187
|
* Adapt the opencode SDK client to the structural TodoEnforcerClient the
|
|
270
188
|
* todo enforcer expects: unwraps the SDK `{data, error, request, response}`
|
|
@@ -345,7 +263,6 @@ function adaptTodoEnforcerClient(client: PluginInput['client']): TodoEnforcerCli
|
|
|
345
263
|
* Helpers live in src/pantheon/vision.ts and are imported from there directly.
|
|
346
264
|
*/
|
|
347
265
|
const plugin: Plugin = async (input: PluginInput) => {
|
|
348
|
-
terminalToastClient = input.client as unknown as ToastClient
|
|
349
266
|
// release-134: runtime guard against DOUBLE registration (duplicate toasts +
|
|
350
267
|
// duplicate task_ids in one process — 2026-08-12 live logs). opencode
|
|
351
268
|
// merges the global config (npm-package plugin paths) with the project
|
|
@@ -360,100 +277,11 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
360
277
|
// is older than the installed package. Runs once, fail-open, no auto-sync.
|
|
361
278
|
checkTuiVersionStaleness((msg) => log.warn(msg))
|
|
362
279
|
const vision = createVisionHandler(input)
|
|
363
|
-
//
|
|
364
|
-
// hook.
|
|
365
|
-
// (
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
// plugin/frontmatter; these are enforced by delegation.ts itself.
|
|
369
|
-
const delegationBudgets = resolveDelegationBudgets()
|
|
370
|
-
const wallClockTimeoutMs = resolveDelegationTimeoutMs()
|
|
371
|
-
|
|
372
|
-
// WS1 (PR #94): delegation backend gate. Default = legacy (the V1
|
|
373
|
-
// fire-and-forget toolset, intact); PANTHEON_DELEGATE_MODE=native selects
|
|
374
|
-
// the thin manager over native task() semantics. Kill-switch
|
|
375
|
-
// PANTHEON_DELEGATION=off covers the native manager; finalizeDelegation stays on
|
|
376
|
-
// the legacy observer: it works on the shared board in both modes and must
|
|
377
|
-
// never be kill-switched, so children never orphan. The idle hook below is
|
|
378
|
-
// untouched.
|
|
379
|
-
const delegationClient = adaptDelegationClient(input.client)
|
|
380
|
-
// Only an explicitly active routing profile may provide child models.
|
|
381
|
-
// An empty map deliberately lets OpenCode inherit the parent model.
|
|
382
|
-
const routingAgentModels = loadRoutingAgentModels({
|
|
383
|
-
candidates: activePresetCandidates(),
|
|
384
|
-
logger: log,
|
|
385
|
-
})
|
|
386
|
-
const readOnlyAgents = new Set(['apollo', 'gaia'])
|
|
387
|
-
const legacyDelegation = createDelegationTools({
|
|
388
|
-
board,
|
|
389
|
-
client: delegationClient,
|
|
390
|
-
options: {
|
|
391
|
-
rootSessions,
|
|
392
|
-
isChildSession: (sessionID) => sessionHierarchy.isChild(sessionID),
|
|
393
|
-
registerChildSession: (sessionID, parentID) => {
|
|
394
|
-
sessionHierarchy.register({ id: sessionID, parentID })
|
|
395
|
-
},
|
|
396
|
-
enforceRuntimeMatrix: true,
|
|
397
|
-
readOnlyAgents,
|
|
398
|
-
agentModels: routingAgentModels,
|
|
399
|
-
delegationBudgets,
|
|
400
|
-
stepCapTracker,
|
|
401
|
-
...(routingPermissionTask != null ? { permissionTask: routingPermissionTask } : {}),
|
|
402
|
-
...(wallClockTimeoutMs !== undefined ? { wallClockTimeoutMs } : {}),
|
|
403
|
-
},
|
|
404
|
-
})
|
|
405
|
-
const delegateMode = resolveDelegateMode()
|
|
406
|
-
if (delegateMode === 'native') {
|
|
407
|
-
log.info('[Pantheon Plugin] Delegation mode: native (PANTHEON_DELEGATE_MODE=native)')
|
|
408
|
-
}
|
|
409
|
-
const delegation =
|
|
410
|
-
delegateMode === 'native'
|
|
411
|
-
? buildPluginNativeDelegation({
|
|
412
|
-
board,
|
|
413
|
-
client: delegationClient,
|
|
414
|
-
isRootSession,
|
|
415
|
-
registerChildSession: (sessionID, parentID) => {
|
|
416
|
-
sessionHierarchy.register({ id: sessionID, parentID })
|
|
417
|
-
},
|
|
418
|
-
registerReadOnlySession: (sessionID, info) => {
|
|
419
|
-
readOnlyRegistry.register(sessionID, info)
|
|
420
|
-
},
|
|
421
|
-
readOnlyAgents,
|
|
422
|
-
agentModels: routingAgentModels,
|
|
423
|
-
stepCap: stepCapTracker,
|
|
424
|
-
...(wallClockTimeoutMs !== undefined ? { wallClockTimeoutMs } : {}),
|
|
425
|
-
finalizeDelegation: legacyDelegation.finalizeDelegation,
|
|
426
|
-
})
|
|
427
|
-
: legacyDelegation
|
|
428
|
-
|
|
429
|
-
// Proactive idle-child scan (Fix 2): finalize children that are idle on the
|
|
430
|
-
// board but have no MD report on disk — eliminates the phantom "running"
|
|
431
|
-
// window. Runs every 30s (configurable via PANTHEON_IDLE_SCAN_INTERVAL_MS),
|
|
432
|
-
// unref'd so it never holds the process open. Fail-open by design.
|
|
433
|
-
const idleScanIntervalMs = Number(process.env.PANTHEON_IDLE_SCAN_INTERVAL_MS) || undefined
|
|
434
|
-
startIdleChildScan(
|
|
435
|
-
{
|
|
436
|
-
board,
|
|
437
|
-
finalize: (childSessionID, opts) => delegation.finalizeDelegation(childSessionID, opts),
|
|
438
|
-
hasReport: (job) => {
|
|
439
|
-
try {
|
|
440
|
-
const filePath = join('.pantheon/delegations', job.parentSessionID, `${job.alias}.md`)
|
|
441
|
-
return existsSync(filePath)
|
|
442
|
-
} catch {
|
|
443
|
-
return false
|
|
444
|
-
}
|
|
445
|
-
},
|
|
446
|
-
logger: log,
|
|
447
|
-
},
|
|
448
|
-
idleScanIntervalMs,
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
// Wave 1 (PR #46): TODO continuation enforcer for root/non-board sessions.
|
|
452
|
-
// Mirrors routing.yml `todo_enforcer` (plugin scope has no routing.yml
|
|
453
|
-
// access — COMPACTION_MAX_ITEMS pattern). The event hook routes non-board
|
|
454
|
-
// session.idle events here; board-child idles go to finalizeDelegation.
|
|
455
|
-
// Runtime kill-switch: PANTHEON_TODO_ENFORCER=off (routing.yml is a doc
|
|
456
|
-
// mirror only — the env var is the real switch, PANTHEON_TOASTS pattern).
|
|
280
|
+
// Wave 1 (PR #46): TODO continuation enforcer for root sessions. The event
|
|
281
|
+
// hook routes `session.idle` events here; an active goal owns the idle
|
|
282
|
+
// (goal loop), otherwise the todo enforcer re-injects. Runtime kill-switch:
|
|
283
|
+
// PANTHEON_TODO_ENFORCER=off (routing.yml is a doc mirror only — the env var
|
|
284
|
+
// is the real switch, PANTHEON_TOASTS pattern).
|
|
457
285
|
const todoEnforcer = new TodoEnforcer({
|
|
458
286
|
client: adaptTodoEnforcerClient(input.client),
|
|
459
287
|
board,
|
|
@@ -516,9 +344,7 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
516
344
|
}
|
|
517
345
|
|
|
518
346
|
// Wave 4 (PR #46): /cost — delegation cost + token visibility. Reads
|
|
519
|
-
// opencode.db read-only via the single node:sqlite backend.
|
|
520
|
-
// Fully wired (unlike dispatch-guard, which is manual-orchestration-only
|
|
521
|
-
// because opencode 1.18.x cannot intercept task completion via hooks).
|
|
347
|
+
// opencode.db read-only via the single node:sqlite backend. Fully wired.
|
|
522
348
|
const costCommand = createCostCommand()
|
|
523
349
|
const modelCommand = createModelCommand()
|
|
524
350
|
|
|
@@ -527,11 +353,11 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
527
353
|
// compaction-134 seed: opencode does NOT replay `session.created`
|
|
528
354
|
// events for sessions that exist before plugin load, so after a
|
|
529
355
|
// restart the resumed ROOT session never enters rootSessions and the
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
356
|
+
// read-only enforcement guard would misclassify it. Seed the registry
|
|
357
|
+
// from session.list() in the background — every session WITHOUT a
|
|
358
|
+
// parentID is a root. Fail-open: a failed/throttled list leaves the set
|
|
359
|
+
// untouched (the guard treats unknown sessions as roots), and config
|
|
360
|
+
// setup must never wait for the session API.
|
|
535
361
|
// Context sandbox config — opencode.json → context_sandbox: {enabled, limits}
|
|
536
362
|
try {
|
|
537
363
|
const raw = (config as unknown as Record<string, unknown>).context_sandbox
|
|
@@ -576,12 +402,9 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
576
402
|
log.warn('[plugin] preset application failed (see logs for details)')
|
|
577
403
|
}
|
|
578
404
|
},
|
|
579
|
-
//
|
|
580
|
-
//
|
|
405
|
+
// Plugin tool surface: tag-anchored edits (hashline_edit), the full-auto
|
|
406
|
+
// goal loop, /cost and /pantheon-model management.
|
|
581
407
|
tool: {
|
|
582
|
-
pantheon_delegate: delegation.pantheon_delegate,
|
|
583
|
-
pantheon_delegation_read: delegation.pantheon_delegation_read,
|
|
584
|
-
pantheon_delegation_list: delegation.pantheon_delegation_list,
|
|
585
408
|
hashline_edit: hashlineEdit,
|
|
586
409
|
// Wave 3 (PR #46): full-auto goal tools (single active goal/session).
|
|
587
410
|
pantheon_goal_create: goalTools.pantheon_goal_create,
|
|
@@ -604,6 +427,10 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
604
427
|
// The SDK exposes the active agent here; tool.execute.before has no
|
|
605
428
|
// agent field. Never infer identity from a prompt or tool name.
|
|
606
429
|
sessionAgents.set(hookInput.sessionID, hookInput.agent)
|
|
430
|
+
// Phase 4: chat.params is the ONLY hook with the active agent, so this
|
|
431
|
+
// is where the read-only registry is populated — apollo/gaia sessions
|
|
432
|
+
// must be denied edit/write/bash/task/hashline_edit by the guard.
|
|
433
|
+
syncReadOnlySession(readOnlyRegistry, hookInput.sessionID, hookInput.agent)
|
|
607
434
|
},
|
|
608
435
|
'experimental.chat.messages.transform': vision.messagesTransform,
|
|
609
436
|
event: async ({ event: ev }) => {
|
|
@@ -615,6 +442,12 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
615
442
|
if (info.parentID === undefined) rootSessions.add(info.id)
|
|
616
443
|
}
|
|
617
444
|
}
|
|
445
|
+
// Phase 4: a deleted session must not leave a read-only registration
|
|
446
|
+
// (or cached agent identity) behind.
|
|
447
|
+
if (ev.type === 'session.deleted') {
|
|
448
|
+
readOnlyRegistry.unregister(ev.properties.info.id)
|
|
449
|
+
sessionAgents.delete(ev.properties.info.id)
|
|
450
|
+
}
|
|
618
451
|
// release-134 Phase 3: activate the todo snapshot captured at
|
|
619
452
|
// compacting time — the first todowrite within the restore window is
|
|
620
453
|
// rewritten with the exact list (see todo-preserve.ts). Fail-open.
|
|
@@ -624,27 +457,12 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
624
457
|
if (ev.type === 'message.updated') {
|
|
625
458
|
await handleToolCeilingEvent(input.client, ev, sessionToolCeilings)
|
|
626
459
|
}
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
board,
|
|
634
|
-
finalize: (childSessionID, opts) => delegation.finalizeDelegation(childSessionID, opts),
|
|
635
|
-
})
|
|
636
|
-
// Wave 1/3: non-board idle → idle dispatcher. Board children are
|
|
637
|
-
// handled by handleDelegationEvent above; everything else (roots,
|
|
638
|
-
// non-board sessions) is routed by priority: an active goal owns the
|
|
639
|
-
// idle (goal loop), otherwise the todo enforcer re-injects. Neither
|
|
640
|
-
// onIdle ever throws (internal failures are logged + swallowed), so
|
|
641
|
-
// the session is safe.
|
|
642
|
-
if (!delegated && ev.type === 'session.idle') {
|
|
643
|
-
await idleDispatcher.onIdle(ev.properties.sessionID)
|
|
644
|
-
}
|
|
645
|
-
} catch (err) {
|
|
646
|
-
// The event hook must never break the session.
|
|
647
|
-
log.error('[Pantheon Plugin] Delegation event handling failed:', err)
|
|
460
|
+
// Wave 1/3: `session.idle` → idle dispatcher. An active goal owns the
|
|
461
|
+
// idle (goal loop); otherwise the todo enforcer re-injects. onIdle never
|
|
462
|
+
// throws (internal failures are logged + swallowed), so the session is
|
|
463
|
+
// safe.
|
|
464
|
+
if (ev.type === 'session.idle') {
|
|
465
|
+
await idleDispatcher.onIdle(ev.properties.sessionID)
|
|
648
466
|
}
|
|
649
467
|
await vision.event({ event: ev })
|
|
650
468
|
},
|
|
@@ -670,16 +488,14 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
670
488
|
// through untouched.
|
|
671
489
|
'tool.execute.after': combinedAfter,
|
|
672
490
|
// Phase 4 + release-134 Phase 2: keep the session's working state across
|
|
673
|
-
// compaction — preservation directive, active goals (<mission_context>)
|
|
674
|
-
// pending todos (<todo_context>)
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
// break the experimental compaction hook.
|
|
491
|
+
// compaction — preservation directive, active goals (<mission_context>)
|
|
492
|
+
// and pending todos (<todo_context>). Guarded: only pushes when there is
|
|
493
|
+
// something to preserve; a build failure must never break the experimental
|
|
494
|
+
// compaction hook.
|
|
678
495
|
'experimental.session.compacting': async (_input, output) => {
|
|
679
496
|
try {
|
|
680
|
-
const blocks = await buildCompactionContext(
|
|
497
|
+
const blocks = await buildCompactionContext({
|
|
681
498
|
sessionID: _input.sessionID,
|
|
682
|
-
maxItems: COMPACTION_MAX_ITEMS,
|
|
683
499
|
goals: {
|
|
684
500
|
enabled: GOAL_LOOP_DEFAULTS.enabled,
|
|
685
501
|
list: (sessionID: string) => goalStore.list(sessionID),
|
|
@@ -783,7 +783,7 @@ function delegationAgent(tool: string, args?: unknown): string {
|
|
|
783
783
|
* panel tracks the child end-to-end. Fire-and-forget and silent: a mirror
|
|
784
784
|
* failure must never surface into the tool call or the transcript.
|
|
785
785
|
* registerLaunchIfAbsent keeps it idempotent against any other dispatcher
|
|
786
|
-
* (
|
|
786
|
+
* (plugin double-load, repeated after-hook firings).
|
|
787
787
|
*/
|
|
788
788
|
function mirrorNativeBackgroundDispatch(
|
|
789
789
|
input: { tool: string; sessionID: string },
|