pantheon-opencode 1.3.7 → 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 +2 -3
- package/package.json +1 -1
- package/scripts/install/opencode.mjs +2 -0
- package/src/mcp/mcp_codemap_module.py +185 -0
- package/src/mcp/memory_mcp_server.py +91 -3
- package/src/pantheon/chat-reminders.ts +4 -74
- package/src/pantheon/command-normalizer.ts +101 -0
- package/src/pantheon/compaction-assert.ts +28 -28
- package/src/pantheon/context-sandbox.ts +273 -0
- 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 +62 -33
- package/src/plugins/pantheon-hooks.ts +15 -158
- package/src/plugins/tui/dist/tui.d.ts +18 -3
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +42 -10
- package/src/plugins/tui/dist/tui.tsx +69 -17
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +14 -10
- package/src/skills/codemap/SKILL.md +7 -0
- package/src/skills/context-compression/SKILL.md +39 -0
|
@@ -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
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
2
|
-
import type { PluginConfig } from 'opencode'
|
|
3
1
|
import { existsSync } from 'node:fs'
|
|
4
2
|
import { join } from 'node:path'
|
|
3
|
+
import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
4
|
+
import type { PluginConfig } from 'opencode'
|
|
5
5
|
import { BackgroundJobBoard } from './pantheon/background-job-board.ts'
|
|
6
|
-
import {
|
|
6
|
+
import { createCommandNormalizer } from './pantheon/command-normalizer.ts'
|
|
7
|
+
import {
|
|
8
|
+
type ContextSandboxConfig,
|
|
9
|
+
createContextSandbox,
|
|
10
|
+
resolveSandboxConfig,
|
|
11
|
+
DEFAULT_CONFIG as SANDBOX_DEFAULT_CONFIG,
|
|
12
|
+
DEFAULT_LIMITS as SANDBOX_DEFAULT_LIMITS,
|
|
13
|
+
} from './pantheon/context-sandbox.ts'
|
|
7
14
|
import { createCostCommand } from './pantheon/cost-command.ts'
|
|
8
15
|
import {
|
|
9
16
|
collectRootSessionIDs,
|
|
@@ -21,6 +28,7 @@ import {
|
|
|
21
28
|
zeusReadGuard,
|
|
22
29
|
} from './pantheon/delegation-enforce.ts'
|
|
23
30
|
import { handleDelegationEvent } from './pantheon/delegation-notify.ts'
|
|
31
|
+
import { showDelegationTerminalToast, type ToastClient } from './pantheon/delegation-toast.ts'
|
|
24
32
|
import { FilePersistenceAdapter } from './pantheon/file-persistence.ts'
|
|
25
33
|
import { GOAL_LOOP_DEFAULTS, GoalLoop, GoalStore } from './pantheon/goal-loop.ts'
|
|
26
34
|
import { createReadEnhancer } from './pantheon/hashline/read-enhancer.ts'
|
|
@@ -54,17 +62,6 @@ import { activePresetCandidates, createVisionHandler } from './pantheon/vision.t
|
|
|
54
62
|
// .pantheon/logs/hooks.log; the console echo is opt-in via PANTHEON_HOOKS_LOG=1.
|
|
55
63
|
const log = createPantheonLogger({ module: 'pantheon-plugin' })
|
|
56
64
|
|
|
57
|
-
// Fase 6 (release-134): the delegation toolset's options.agentModels (branch
|
|
58
|
-
// (b) of resolveChildModel) is wired from routing.yml's default (first)
|
|
59
|
-
// preset — a STATIC per-agent model mapping independent of the active preset,
|
|
60
|
-
// so delegated children get a sane model even without an active preset (the
|
|
61
|
-
// previous production wiring left branch (b) dead: delegation depended 100%
|
|
62
|
-
// on the active preset). Built ONCE at module load, never per dispatch.
|
|
63
|
-
// Fail-open: a missing/corrupt routing.yml yields {} (warned by the helper)
|
|
64
|
-
// and delegation falls back to the active preset / opencode default — the
|
|
65
|
-
// plugin never throws at startup.
|
|
66
|
-
const routingAgentModels = loadRoutingAgentModels({ logger: log })
|
|
67
|
-
|
|
68
65
|
// R4: per-agent step caps from routing.yml `agents.<name>.max_steps`
|
|
69
66
|
// (fail-open → {} = no agent capped). R1 retry_policy/cooldown are NOT
|
|
70
67
|
// wired here — the plugin has no retry path (opencode cannot intercept task
|
|
@@ -82,6 +79,7 @@ const board = new BackgroundJobBoard({
|
|
|
82
79
|
})
|
|
83
80
|
const persistence = new FilePersistenceAdapter('.pantheon/board/state.json')
|
|
84
81
|
board.setPersistence(persistence)
|
|
82
|
+
let terminalToastClient: ToastClient = {}
|
|
85
83
|
board
|
|
86
84
|
.recoverRunningJobs()
|
|
87
85
|
.catch((err) => log.error('[Pantheon Plugin] Failed to recover running jobs:', err))
|
|
@@ -100,6 +98,8 @@ board.onTerminal((taskID: string) => {
|
|
|
100
98
|
log.info(
|
|
101
99
|
`[Pantheon Plugin] Board terminal: [${job.alias}] ${job.description} → ${job.state}${job.resultSummary ? ` — ${job.resultSummary}` : ''}`,
|
|
102
100
|
)
|
|
101
|
+
// Deliberately direct-to-TUI: onTerminal must not inject chat/transcript text.
|
|
102
|
+
void showDelegationTerminalToast(terminalToastClient, job)
|
|
103
103
|
})
|
|
104
104
|
|
|
105
105
|
// Periodically prune terminal/reconciled jobs (24h TTL, every 30 min).
|
|
@@ -131,6 +131,13 @@ const enforcementGuard = createEnforcementGuard({
|
|
|
131
131
|
},
|
|
132
132
|
})
|
|
133
133
|
|
|
134
|
+
// Fase 1 (command-normalizer): deterministic `python` → `python3` rewrite for
|
|
135
|
+
// `bash` commands (PEP 394 — Ubuntu 20.04+ has no bare `python`). Unlike the
|
|
136
|
+
// enforcement guard above (which THROWS to deny), this guard REWRITES the
|
|
137
|
+
// command in place and lets it run. Logs every rewrite + `python -c` warning
|
|
138
|
+
// to the shared hooks.log for @nyx frequency measurement.
|
|
139
|
+
const commandNormalizer = createCommandNormalizer({ logger: log })
|
|
140
|
+
|
|
134
141
|
// Mirrors routing.yml background_delegation.max_compaction_items.
|
|
135
142
|
const COMPACTION_MAX_ITEMS = 10
|
|
136
143
|
|
|
@@ -208,8 +215,7 @@ function adaptDelegationClient(client: PluginInput['client']): DelegationClient
|
|
|
208
215
|
model?: { id: string; providerID: string }
|
|
209
216
|
} = { parentID: input.body.parentID }
|
|
210
217
|
if (input.body.title !== undefined) body.title = input.body.title
|
|
211
|
-
// Forward
|
|
212
|
-
// falls back to the key-gated default model and dies at startup.
|
|
218
|
+
// Forward a model only when the active routing profile supplied one.
|
|
213
219
|
if (input.body.model !== undefined) body.model = input.body.model
|
|
214
220
|
const result = await client.session.create({ body })
|
|
215
221
|
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
@@ -328,6 +334,7 @@ function adaptTodoEnforcerClient(client: PluginInput['client']): TodoEnforcerCli
|
|
|
328
334
|
* Helpers live in src/pantheon/vision.ts and are imported from there directly.
|
|
329
335
|
*/
|
|
330
336
|
const plugin: Plugin = async (input: PluginInput) => {
|
|
337
|
+
terminalToastClient = input.client as unknown as ToastClient
|
|
331
338
|
// release-134: runtime guard against DOUBLE registration (duplicate toasts +
|
|
332
339
|
// duplicate task_ids in one process — 2026-08-12 live logs). opencode
|
|
333
340
|
// merges the global config (npm-package plugin paths) with the project
|
|
@@ -362,7 +369,12 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
362
369
|
},
|
|
363
370
|
enforceRuntimeMatrix: true,
|
|
364
371
|
readOnlyAgents: new Set(['apollo', 'gaia']),
|
|
365
|
-
|
|
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
|
+
}),
|
|
366
378
|
delegationBudgets,
|
|
367
379
|
stepCapTracker,
|
|
368
380
|
...(routingPermissionTask != null ? { permissionTask: routingPermissionTask } : {}),
|
|
@@ -436,6 +448,24 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
436
448
|
const hashlineEdit = createHashlineEditTool()
|
|
437
449
|
const readEnhancer = createReadEnhancer()
|
|
438
450
|
|
|
451
|
+
// Context Window Optimization (v2 P0) — tool output sandboxing
|
|
452
|
+
// Pure truncation hook: read/grep/glob/webfetch → head + [TRUNCATED] + tail
|
|
453
|
+
// Fail-open: never throws, disabled via opencode.json context_sandbox.enabled
|
|
454
|
+
const sandboxConfig: ContextSandboxConfig = {
|
|
455
|
+
enabled: SANDBOX_DEFAULT_CONFIG.enabled,
|
|
456
|
+
limits: {
|
|
457
|
+
read: { ...SANDBOX_DEFAULT_LIMITS.read },
|
|
458
|
+
grep: { ...SANDBOX_DEFAULT_LIMITS.grep },
|
|
459
|
+
glob: { ...SANDBOX_DEFAULT_LIMITS.glob },
|
|
460
|
+
webfetch: { ...SANDBOX_DEFAULT_LIMITS.webfetch },
|
|
461
|
+
},
|
|
462
|
+
}
|
|
463
|
+
const sandboxHandler = createContextSandbox(sandboxConfig)
|
|
464
|
+
const combinedAfter: typeof readEnhancer = async (input, output) => {
|
|
465
|
+
await sandboxHandler(input, output)
|
|
466
|
+
await readEnhancer(input, output)
|
|
467
|
+
}
|
|
468
|
+
|
|
439
469
|
// Wave 4 (PR #46): /cost — delegation cost + token visibility. Reads
|
|
440
470
|
// opencode.db read-only (node:sqlite, falls back to scripts/cost.mjs).
|
|
441
471
|
// Fully wired (unlike dispatch-guard, which is manual-orchestration-only
|
|
@@ -452,6 +482,15 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
452
482
|
// WITHOUT a parentID is a root. Fail-open: a failed/throttled list
|
|
453
483
|
// leaves the set untouched (delegation.ts treats unknown sessions as
|
|
454
484
|
// roots), and config setup must never wait for the session API.
|
|
485
|
+
// Context sandbox config — opencode.json → context_sandbox: {enabled, limits}
|
|
486
|
+
try {
|
|
487
|
+
const raw = (config as unknown as Record<string, unknown>).context_sandbox
|
|
488
|
+
const resolved = resolveSandboxConfig(raw)
|
|
489
|
+
sandboxConfig.enabled = resolved.enabled
|
|
490
|
+
sandboxConfig.limits = resolved.limits
|
|
491
|
+
} catch {
|
|
492
|
+
// fail-open: keep defaults
|
|
493
|
+
}
|
|
455
494
|
seedRootSessionsInBackground(input)
|
|
456
495
|
config.agentsPath = config.agentsPath ?? []
|
|
457
496
|
config.agentsPath.push(new URL('./agents', import.meta.url).pathname)
|
|
@@ -504,9 +543,7 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
504
543
|
'chat.message': async (hookInput, output) => {
|
|
505
544
|
await vision.chatMessage(hookInput, output)
|
|
506
545
|
// No delegation notification delivery here — the user policy is ZERO
|
|
507
|
-
// notification text injected into the chat transcript
|
|
508
|
-
// notifier.flushQueue prepend). pantheon-hooks' chat-reminders.ts keeps
|
|
509
|
-
// its own <system-reminder> channel, untouched.
|
|
546
|
+
// notification text injected into the chat transcript.
|
|
510
547
|
// Wave 1: a user message is activity — the todo enforcer's
|
|
511
548
|
// user-activity gate skips injection for userActivityQuietMs afterwards.
|
|
512
549
|
todoEnforcer.noteUserActivity(hookInput.sessionID)
|
|
@@ -531,17 +568,6 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
531
568
|
// rewritten with the exact list (see todo-preserve.ts). Fail-open.
|
|
532
569
|
if (ev.type === 'session.compacted') {
|
|
533
570
|
await todoPreserver.onCompacted(ev.properties.sessionID)
|
|
534
|
-
// release-134 Phase 4: re-assert post-compaction state — enqueue a
|
|
535
|
-
// fresh-state reminder (running/unread board jobs + active goals) for
|
|
536
|
-
// the session's next chat.message delivery. Fail-open (never throws).
|
|
537
|
-
await reassertAfterCompaction({
|
|
538
|
-
sessionID: ev.properties.sessionID,
|
|
539
|
-
board,
|
|
540
|
-
goals: {
|
|
541
|
-
enabled: GOAL_LOOP_DEFAULTS.enabled,
|
|
542
|
-
list: (s: string) => goalStore.list(s),
|
|
543
|
-
},
|
|
544
|
-
})
|
|
545
571
|
}
|
|
546
572
|
// Phase 3: observe completion on child sessions → finalizeDelegation.
|
|
547
573
|
// The board transition fires onTerminal → the file-only audit log (no
|
|
@@ -578,13 +604,16 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
578
604
|
// no-ops for non-matching sessions/tools.
|
|
579
605
|
zeusReadGuard(input.tool, output?.args, sessionAgents.get(input.sessionID))
|
|
580
606
|
await enforcementGuard(input, output)
|
|
607
|
+
await commandNormalizer(input, output)
|
|
581
608
|
await todoPreserver.beforeTodoWrite(input, output)
|
|
582
609
|
},
|
|
583
|
-
// Wave 2 (PR #46)
|
|
610
|
+
// Wave 2 (PR #46) + Context Sandbox (v2 P0): sandbox truncates oversized
|
|
611
|
+
// outputs BEFORE hashline tags (so tags remain stable on kept lines).
|
|
612
|
+
// Additive —
|
|
584
613
|
// pantheon-hooks.ts (a separate plugin instance) owns its own
|
|
585
614
|
// tool.execute.after, so there is no key collision. Non-read tools pass
|
|
586
615
|
// through untouched.
|
|
587
|
-
'tool.execute.after':
|
|
616
|
+
'tool.execute.after': combinedAfter,
|
|
588
617
|
// Phase 4 + release-134 Phase 2: keep the session's working state across
|
|
589
618
|
// compaction — preservation directive, active goals (<mission_context>),
|
|
590
619
|
// pending todos (<todo_context>), and in-flight background delegations
|