pantheon-opencode 1.5.0-beta.1 → 1.5.0-beta.10
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/.pantheon/code-mode/manifest.json +21 -0
- package/CHANGELOG.md +1026 -0
- package/README.md +171 -16
- package/README.pt-BR.md +99 -13
- package/bin/pantheon-init.mjs +178 -21
- package/package.json +23 -14
- package/plugin.json +1 -1
- package/pyproject.toml +65 -0
- package/scripts/code_mode_server.py +431 -55
- package/scripts/cost.mjs +30 -14
- package/scripts/doctor.mjs +178 -9
- package/scripts/install/cli-ui.mjs +17 -11
- package/scripts/install/config-migration.mjs +4 -6
- package/scripts/install/opencode.mjs +137 -35
- package/scripts/install/plugin.mjs +3 -33
- package/scripts/install/shared.mjs +43 -2
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +1 -0
- package/scripts/manifest-inventory.mjs +410 -0
- package/scripts/mcp_persistence_server.py +537 -128
- package/scripts/package-evidence.mjs +242 -0
- package/scripts/probe-context-rehydrate.mjs +240 -0
- package/scripts/probe-pantheon-cost.mjs +313 -0
- package/scripts/release-beta-version.mjs +56 -60
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +121 -53
- package/scripts/test-opencode-v1-v2-sandbox.sh +367 -96
- package/scripts/validation-policy.mjs +7 -7
- package/scripts/version-check.mjs +113 -108
- package/scripts/versioning.mjs +230 -86
- package/src/agents/mnemosyne.md +1 -1
- package/src/agents/zeus.md +2 -2
- package/src/mcp/code_mode_server.py +431 -55
- package/src/mcp/mcp_persistence_server.py +537 -128
- package/src/pantheon/auto-wake.ts +1 -5
- package/src/pantheon/background-job-board.ts +43 -1
- package/src/pantheon/compaction-handoff.ts +304 -0
- package/src/pantheon/cost-command.ts +345 -62
- package/src/pantheon/delegate-manager.ts +390 -8
- package/src/pantheon/delegation-finalize.ts +32 -0
- package/src/pantheon/delegation.ts +235 -18
- package/src/pantheon/file-persistence-salvage.ts +64 -0
- package/src/pantheon/file-persistence.ts +194 -45
- package/src/pantheon/native-probe.ts +234 -0
- package/src/pantheon/native-task-status.ts +139 -0
- package/src/pantheon/provider-capabilities.ts +221 -0
- package/src/pantheon/provider-stream-tracker.ts +182 -0
- package/src/pantheon/shared-board.ts +37 -0
- package/src/pantheon/task-result-guard.ts +107 -0
- package/src/pantheon/token-opt.ts +146 -12
- package/src/pantheon/tool-ceiling.ts +159 -0
- package/src/pantheon/v2-events.ts +18 -1
- package/src/plugin-v2.ts +2 -14
- package/src/plugin.ts +28 -17
- package/src/plugins/pantheon-hooks.ts +51 -0
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +116 -27
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +414 -150
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +447 -84
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +447 -84
- package/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +0 -11
- package/src/pantheon/delegate-task-adapter.ts +0 -444
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Build and verify the single immutable npm package evidence artifact. */
|
|
3
|
+
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from 'node:fs'
|
|
17
|
+
import { tmpdir } from 'node:os'
|
|
18
|
+
import { isAbsolute, join } from 'node:path'
|
|
19
|
+
import { validateInventory } from './manifest-inventory.mjs'
|
|
20
|
+
|
|
21
|
+
const CI_ARGS = ['ci', '--ignore-scripts', '--omit=dev', '--no-audit', '--no-fund']
|
|
22
|
+
const REQUIRED_PAYLOAD = [
|
|
23
|
+
'package/package.json',
|
|
24
|
+
'package/plugin.json',
|
|
25
|
+
'package/pyproject.toml',
|
|
26
|
+
'package/src/plugins/tui/package.json',
|
|
27
|
+
'package/src/plugins/tui/package-lock.json',
|
|
28
|
+
'package/src/plugin.ts',
|
|
29
|
+
'package/src/plugin-v2.ts',
|
|
30
|
+
'package/src/pantheon/v2-bridge.ts',
|
|
31
|
+
'package/src/plugins/tui/dist/tui.js',
|
|
32
|
+
'package/src/plugins/tui/dist/server.js',
|
|
33
|
+
'package/bin/pantheon-init.mjs',
|
|
34
|
+
'package/scripts/doctor.mjs',
|
|
35
|
+
'package/.pantheon/code-mode/compress-inline.py',
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
function fail(message) {
|
|
39
|
+
throw new Error(message)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveTargetSha(root, supplied) {
|
|
43
|
+
const raw =
|
|
44
|
+
supplied ?? execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' })
|
|
45
|
+
const sha = String(raw).trim().toLowerCase()
|
|
46
|
+
if (!/^[0-9a-f]{40}$/.test(sha))
|
|
47
|
+
fail('target SHA must be a full lowercase 40-character hexadecimal SHA')
|
|
48
|
+
return sha
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createOutputDirectory(outputDir) {
|
|
52
|
+
const directory = outputDir ?? mkdtempSync(join(tmpdir(), 'pantheon-package-evidence-'))
|
|
53
|
+
mkdirSync(directory, { recursive: true })
|
|
54
|
+
if (readdirSync(directory).length !== 0) fail(`output directory must be empty: ${directory}`)
|
|
55
|
+
return { directory }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function packOnce(root, outputDir) {
|
|
59
|
+
const raw = execFileSync(
|
|
60
|
+
'npm',
|
|
61
|
+
['pack', '--ignore-scripts', '--json', '--pack-destination', outputDir],
|
|
62
|
+
{ cwd: root, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 },
|
|
63
|
+
)
|
|
64
|
+
let result
|
|
65
|
+
try {
|
|
66
|
+
result = JSON.parse(raw)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
fail(`npm pack returned malformed JSON: ${error.message}`)
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(result) || result.length !== 1 || typeof result[0]?.filename !== 'string') {
|
|
71
|
+
fail('npm pack must produce exactly one JSON result')
|
|
72
|
+
}
|
|
73
|
+
const tgzFiles = readdirSync(outputDir).filter((name) => name.endsWith('.tgz'))
|
|
74
|
+
if (tgzFiles.length !== 1)
|
|
75
|
+
fail(`package evidence requires exactly one .tgz, found ${tgzFiles.length}`)
|
|
76
|
+
if (tgzFiles[0] !== result[0].filename)
|
|
77
|
+
fail('npm pack result does not identify the sole .tgz artifact')
|
|
78
|
+
return join(outputDir, tgzFiles[0])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validateArchiveEntries(tarball) {
|
|
82
|
+
const listing = execFileSync('tar', ['-tzf', tarball], { encoding: 'utf8' })
|
|
83
|
+
const entries = listing.split('\n').filter(Boolean)
|
|
84
|
+
if (entries.length === 0) fail('package tarball is empty')
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const normalized = entry.replace(/\/$/, '')
|
|
87
|
+
const parts = normalized.split('/')
|
|
88
|
+
if (
|
|
89
|
+
isAbsolute(normalized) ||
|
|
90
|
+
normalized.includes('\\') ||
|
|
91
|
+
parts.includes('..') ||
|
|
92
|
+
(!normalized.startsWith('package/') && normalized !== 'package')
|
|
93
|
+
) {
|
|
94
|
+
fail(`unsafe package archive entry: ${entry}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const detailed = execFileSync('tar', ['-tvzf', tarball], { encoding: 'utf8' })
|
|
98
|
+
for (const line of detailed.split('\n').filter(Boolean)) {
|
|
99
|
+
if (/^[lh]/i.test(line)) fail(`package archive contains a link entry: ${line}`)
|
|
100
|
+
}
|
|
101
|
+
return new Set(entries)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extractSafely(tarball, workDir) {
|
|
105
|
+
const extractionRoot = join(workDir, 'extract')
|
|
106
|
+
mkdirSync(extractionRoot)
|
|
107
|
+
validateArchiveEntries(tarball)
|
|
108
|
+
execFileSync(
|
|
109
|
+
'tar',
|
|
110
|
+
['-xzf', tarball, '--directory', extractionRoot, '--no-same-owner', '--no-same-permissions'],
|
|
111
|
+
{ encoding: 'utf8' },
|
|
112
|
+
)
|
|
113
|
+
const realRoot = realpathSync(extractionRoot)
|
|
114
|
+
const walk = (current) => {
|
|
115
|
+
const stat = lstatSync(current)
|
|
116
|
+
if (stat.isSymbolicLink()) fail(`package archive extraction contains a symlink: ${current}`)
|
|
117
|
+
const resolved = realpathSync(current)
|
|
118
|
+
if (resolved !== realRoot && !resolved.startsWith(`${realRoot}/`)) {
|
|
119
|
+
fail(`package archive extraction escaped its temporary directory: ${current}`)
|
|
120
|
+
}
|
|
121
|
+
if (stat.isDirectory()) for (const name of readdirSync(current)) walk(join(current, name))
|
|
122
|
+
}
|
|
123
|
+
walk(extractionRoot)
|
|
124
|
+
return join(extractionRoot, 'package')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validatePayload(packageRoot, inventory) {
|
|
128
|
+
for (const relativePath of REQUIRED_PAYLOAD) {
|
|
129
|
+
const path = join(packageRoot, relativePath.replace(/^package\//, ''))
|
|
130
|
+
if (!existsSync(path) || !lstatSync(path).isFile())
|
|
131
|
+
fail(`required package payload is missing: ${relativePath}`)
|
|
132
|
+
}
|
|
133
|
+
for (const document of inventory.documents) {
|
|
134
|
+
const { entry, path: sourcePath } = document
|
|
135
|
+
const packagedPath = join(packageRoot, entry.file)
|
|
136
|
+
if (entry.role === 'lock' && entry.file === 'package-lock.json' && !existsSync(packagedPath)) {
|
|
137
|
+
// npm-packlist excludes the package root lockfile by policy. It remains
|
|
138
|
+
// validated below as source evidence, but is not fabricated in the tgz.
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
if (!existsSync(packagedPath) || !lstatSync(packagedPath).isFile())
|
|
142
|
+
fail(`versioned package payload is missing: package/${entry.file}`)
|
|
143
|
+
if (sha256(sourcePath) !== sha256(packagedPath))
|
|
144
|
+
fail(`published package payload differs from source: ${entry.file}`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function collectLockfileEvidence(inventory, entries, packageRoot) {
|
|
149
|
+
return inventory.documents
|
|
150
|
+
.filter(({ entry }) => entry.role === 'lock')
|
|
151
|
+
.map(({ entry, data, path: sourcePath }) => {
|
|
152
|
+
const archivePath = `package/${entry.file}`
|
|
153
|
+
const packagedPath = join(packageRoot, entry.file)
|
|
154
|
+
const publishedInTarball = entries.has(archivePath)
|
|
155
|
+
return {
|
|
156
|
+
file: entry.file,
|
|
157
|
+
sourceSha256: sha256(sourcePath),
|
|
158
|
+
publishedSha256: publishedInTarball ? sha256(packagedPath) : null,
|
|
159
|
+
sourceName: data.name,
|
|
160
|
+
sourceVersion: data.version,
|
|
161
|
+
lockfileVersion: data.lockfileVersion,
|
|
162
|
+
parity: 'validated-against-manifest',
|
|
163
|
+
publishedInTarball,
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function installProductionDependencies(root) {
|
|
169
|
+
// The root package-lock.json is source evidence only: npm-packlist omits it
|
|
170
|
+
// from the root of an npm package, so root ci must validate the source tree.
|
|
171
|
+
execFileSync('npm', CI_ARGS, { cwd: root, stdio: 'inherit' })
|
|
172
|
+
execFileSync('npm', CI_ARGS, {
|
|
173
|
+
cwd: join(root, 'src', 'plugins', 'tui'),
|
|
174
|
+
stdio: 'inherit',
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sha256(path) {
|
|
179
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function createPackageEvidence(root, { outputDir, targetSha } = {}) {
|
|
183
|
+
const inventory = validateInventory(root)
|
|
184
|
+
const sha = resolveTargetSha(root, targetSha)
|
|
185
|
+
const output = createOutputDirectory(outputDir)
|
|
186
|
+
let workDir
|
|
187
|
+
try {
|
|
188
|
+
const tarball = packOnce(root, output.directory)
|
|
189
|
+
workDir = mkdtempSync(join(tmpdir(), 'pantheon-package-evidence-check-'))
|
|
190
|
+
const entries = validateArchiveEntries(tarball)
|
|
191
|
+
const packageRoot = extractSafely(tarball, workDir)
|
|
192
|
+
validatePayload(packageRoot, inventory)
|
|
193
|
+
installProductionDependencies(root)
|
|
194
|
+
const digest = sha256(tarball)
|
|
195
|
+
if (!/^[0-9a-f]{64}$/.test(digest)) fail('tarball SHA-256 is not lowercase 64-hex')
|
|
196
|
+
const metadata = {
|
|
197
|
+
version: inventory.sourceVersion,
|
|
198
|
+
targetSha: sha,
|
|
199
|
+
tarballSha256: digest,
|
|
200
|
+
tarball: tarball.split('/').at(-1),
|
|
201
|
+
lockfilePolicy: {
|
|
202
|
+
rootPackageLockfile: entries.has('package/package-lock.json')
|
|
203
|
+
? 'published-by-npm-pack'
|
|
204
|
+
: 'omitted-by-npm-pack',
|
|
205
|
+
note: entries.has('package/package-lock.json')
|
|
206
|
+
? 'npm pack published the root package-lock.json; lockfile parity is validated and recorded as source evidence, while tarball validation covers only the payload npm pack publishes.'
|
|
207
|
+
: 'npm pack omits the package root package-lock.json by policy; lockfile parity is validated and recorded as source evidence, while tarball validation covers only the payload npm pack publishes.',
|
|
208
|
+
},
|
|
209
|
+
lockfileEvidence: collectLockfileEvidence(inventory, entries, packageRoot),
|
|
210
|
+
}
|
|
211
|
+
writeFileSync(join(output.directory, 'metadata.json'), `${JSON.stringify(metadata, null, 2)}\n`)
|
|
212
|
+
writeFileSync(join(output.directory, 'version'), `${metadata.version}\n`)
|
|
213
|
+
writeFileSync(join(output.directory, 'target-sha'), `${metadata.targetSha}\n`)
|
|
214
|
+
writeFileSync(join(output.directory, 'tarball-sha256'), `${metadata.tarballSha256}\n`)
|
|
215
|
+
return { ...metadata, outputDir: output.directory, tarball }
|
|
216
|
+
} catch (error) {
|
|
217
|
+
rmSync(output.directory, { recursive: true, force: true })
|
|
218
|
+
throw error
|
|
219
|
+
} finally {
|
|
220
|
+
if (workDir) rmSync(workDir, { recursive: true, force: true })
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseOptions(args) {
|
|
225
|
+
const options = {}
|
|
226
|
+
for (const arg of args) {
|
|
227
|
+
const match = /^--(output-dir|target-sha)=(.*)$/.exec(arg)
|
|
228
|
+
if (!match) fail(`unsupported package evidence option: ${arg}`)
|
|
229
|
+
options[match[1] === 'output-dir' ? 'outputDir' : 'targetSha'] = match[2]
|
|
230
|
+
}
|
|
231
|
+
return options
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (process.argv[1]?.endsWith('package-evidence.mjs')) {
|
|
235
|
+
try {
|
|
236
|
+
const evidence = createPackageEvidence(process.cwd(), parseOptions(process.argv.slice(2)))
|
|
237
|
+
console.log(JSON.stringify(evidence))
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error(`package-evidence: ${error.message}`)
|
|
240
|
+
process.exit(1)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
/**
|
|
4
|
+
* Offline/ambiental probe for context_rehydrate and context_session_summary.
|
|
5
|
+
*
|
|
6
|
+
* The Python child imports the installed persistence server and exercises the
|
|
7
|
+
* real FastMCP tools against a temporary SQLite database. No OpenCode command,
|
|
8
|
+
* network call, LLM, or user database is involved. Missing package/runtime is
|
|
9
|
+
* NOT_TESTED/AMBIENTAL rather than a false PASS.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs'
|
|
12
|
+
import { tmpdir } from 'node:os'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
const requestedVersion = valueFor('--version')
|
|
17
|
+
const jsonOutput = process.argv.includes('--json')
|
|
18
|
+
const runtime = fileURLToPath(new URL('./mcp_persistence_server.py', import.meta.url))
|
|
19
|
+
|
|
20
|
+
function valueFor(name) {
|
|
21
|
+
const index = process.argv.indexOf(name)
|
|
22
|
+
return index >= 0 ? process.argv[index + 1] : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function emit(status, detail, checks = []) {
|
|
26
|
+
const result = { status, version: requestedVersion, detail, checks }
|
|
27
|
+
if (jsonOutput) process.stdout.write(JSON.stringify(result))
|
|
28
|
+
else process.stdout.write(`${status}: ${detail}\n`)
|
|
29
|
+
if (status === 'FAIL') process.exitCode = 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function unavailable(detail) {
|
|
33
|
+
emit('NOT_TESTED', detail)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pythonCandidates() {
|
|
37
|
+
const venvPython = process.env.HOME
|
|
38
|
+
? join(process.env.HOME, '.config', 'opencode', '.venv', 'bin', 'python')
|
|
39
|
+
: undefined
|
|
40
|
+
return [process.env.PANTHEON_PYTHON, venvPython, 'python3'].filter(Boolean)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const PYTHON_HARNESS = `
|
|
44
|
+
import asyncio
|
|
45
|
+
import importlib.util
|
|
46
|
+
import json
|
|
47
|
+
import os
|
|
48
|
+
import sys
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
|
|
51
|
+
runtime_path = Path(sys.argv[1]).resolve()
|
|
52
|
+
db_root = Path(sys.argv[2]).resolve()
|
|
53
|
+
sys.path.insert(0, str(runtime_path.parent))
|
|
54
|
+
sys.argv = [str(runtime_path), "--global-db", str(db_root / "global.db"),
|
|
55
|
+
"--project-db", str(db_root / "project.db")]
|
|
56
|
+
|
|
57
|
+
spec = importlib.util.spec_from_file_location("probe_mcp_persistence", runtime_path)
|
|
58
|
+
if spec is None or spec.loader is None:
|
|
59
|
+
raise RuntimeError("could not load persistence runtime")
|
|
60
|
+
module = importlib.util.module_from_spec(spec)
|
|
61
|
+
spec.loader.exec_module(module)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def call(name, arguments):
|
|
65
|
+
result = await module.mcp.call_tool(name, arguments)
|
|
66
|
+
if isinstance(result, tuple):
|
|
67
|
+
structured = result[1]
|
|
68
|
+
if structured is not None:
|
|
69
|
+
if isinstance(structured, dict) and "result" in structured:
|
|
70
|
+
return structured["result"]
|
|
71
|
+
return structured
|
|
72
|
+
result = result[0]
|
|
73
|
+
if isinstance(result, dict):
|
|
74
|
+
return result
|
|
75
|
+
if not result:
|
|
76
|
+
return None
|
|
77
|
+
text = getattr(result[0], "text", str(result[0]))
|
|
78
|
+
try:
|
|
79
|
+
return json.loads(text)
|
|
80
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
81
|
+
return text
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def main():
|
|
85
|
+
checks = []
|
|
86
|
+
slug = "context-rehydrate-probe"
|
|
87
|
+
session_id = "synthetic-session"
|
|
88
|
+
checkpoint = {
|
|
89
|
+
"version": 1,
|
|
90
|
+
"goal": {"id": "probe-goal", "objective": "verify context recovery",
|
|
91
|
+
"status": "in_progress"},
|
|
92
|
+
"phase": {"current": 2, "total": 3, "name": "offline validation"},
|
|
93
|
+
"delegations": {"in_flight": [{"alias": "apo-probe", "agent": "apollo",
|
|
94
|
+
"task_id": "probe-task"}]},
|
|
95
|
+
}
|
|
96
|
+
tail = ["phase 1 complete", "phase 2 payload validated"]
|
|
97
|
+
|
|
98
|
+
# context_save creates the real session-qualified latest pointer. Keeping
|
|
99
|
+
# tail under its own key also tests the runtime's latest/tail fallback.
|
|
100
|
+
await call("context_save", {"slug": slug, "key": "tail",
|
|
101
|
+
"content": json.dumps(tail),
|
|
102
|
+
"session_id": session_id})
|
|
103
|
+
await call("context_save", {"slug": slug, "key": "phase:2",
|
|
104
|
+
"content": json.dumps(checkpoint),
|
|
105
|
+
"session_id": session_id})
|
|
106
|
+
latest = await call("context_get", {"slug": slug, "key": "latest",
|
|
107
|
+
"session_id": session_id})
|
|
108
|
+
stored_tail = await call("context_get", {"slug": slug, "key": "tail",
|
|
109
|
+
"session_id": session_id})
|
|
110
|
+
if latest is None or stored_tail is None:
|
|
111
|
+
raise AssertionError("synthetic latest/tail checkpoint payload is null")
|
|
112
|
+
checks.append("synthetic latest/tail/goal/phase/delegation checkpoint: PASS")
|
|
113
|
+
|
|
114
|
+
os.environ.pop("PANTHEON_COMPACTION", None)
|
|
115
|
+
os.environ.pop("PANTHEON_SESSION_END_SUMMARY", None)
|
|
116
|
+
blocks = await call("context_rehydrate", {"slug": slug})
|
|
117
|
+
summary = await call("context_session_summary", {"slug": slug})
|
|
118
|
+
if not isinstance(blocks, list) or not blocks:
|
|
119
|
+
raise AssertionError("context_rehydrate returned a null/empty payload")
|
|
120
|
+
if not isinstance(summary, str) or not summary:
|
|
121
|
+
raise AssertionError("context_session_summary returned a null/empty payload")
|
|
122
|
+
for fragment in ("verify context recovery", "phase 2/3", "apo-probe",
|
|
123
|
+
"phase 2 payload validated"):
|
|
124
|
+
if not any(fragment in block for block in blocks) and fragment not in summary:
|
|
125
|
+
raise AssertionError(f"payload is missing {fragment!r}")
|
|
126
|
+
checks.append("context_rehydrate non-null payload: PASS")
|
|
127
|
+
checks.append("context_session_summary non-null payload: PASS")
|
|
128
|
+
|
|
129
|
+
missing_blocks = await call("context_rehydrate", {"slug": "probe-absent"})
|
|
130
|
+
missing_summary = await call("context_session_summary", {"slug": "probe-absent"})
|
|
131
|
+
if missing_blocks is not None or missing_summary is not None:
|
|
132
|
+
raise AssertionError("absent checkpoint must return null payloads")
|
|
133
|
+
checks.append("absence returns null: PASS")
|
|
134
|
+
|
|
135
|
+
expired_slug = "context-expired-probe"
|
|
136
|
+
await call("context_save", {"slug": expired_slug, "key": "phase:1",
|
|
137
|
+
"content": json.dumps(checkpoint), "ttl": -1,
|
|
138
|
+
"session_id": session_id})
|
|
139
|
+
expired_blocks = await call("context_rehydrate", {"slug": expired_slug})
|
|
140
|
+
expired_summary = await call("context_session_summary", {"slug": expired_slug})
|
|
141
|
+
if expired_blocks is not None or expired_summary is not None:
|
|
142
|
+
raise AssertionError("expired checkpoint must return null payloads")
|
|
143
|
+
checks.append("expiration returns null: PASS")
|
|
144
|
+
|
|
145
|
+
invalid_slug = "context-invalid-probe"
|
|
146
|
+
await call("context_save", {"slug": invalid_slug, "key": "phase:1",
|
|
147
|
+
"content": "not-json", "session_id": session_id})
|
|
148
|
+
invalid_blocks = await call("context_rehydrate", {"slug": invalid_slug})
|
|
149
|
+
invalid_summary = await call("context_session_summary", {"slug": invalid_slug})
|
|
150
|
+
if invalid_blocks is not None or invalid_summary is not None:
|
|
151
|
+
raise AssertionError("invalid checkpoint must return null payloads")
|
|
152
|
+
checks.append("invalid payload returns null: PASS")
|
|
153
|
+
|
|
154
|
+
os.environ["PANTHEON_COMPACTION"] = "off"
|
|
155
|
+
switched_blocks = await call("context_rehydrate", {"slug": slug})
|
|
156
|
+
if switched_blocks is not None:
|
|
157
|
+
raise AssertionError("PANTHEON_COMPACTION=off must return null")
|
|
158
|
+
os.environ.pop("PANTHEON_COMPACTION", None)
|
|
159
|
+
os.environ["PANTHEON_SESSION_END_SUMMARY"] = "off"
|
|
160
|
+
switched_summary = await call("context_session_summary", {"slug": slug})
|
|
161
|
+
if switched_summary is not None:
|
|
162
|
+
raise AssertionError("PANTHEON_SESSION_END_SUMMARY=off must return null")
|
|
163
|
+
checks.append("kill-switches return null: PASS")
|
|
164
|
+
print(json.dumps({"checks": checks}))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
asyncio.run(main())
|
|
168
|
+
`
|
|
169
|
+
|
|
170
|
+
async function main() {
|
|
171
|
+
if (requestedVersion !== 'v1' && requestedVersion !== 'v2') {
|
|
172
|
+
emit('FAIL', 'usage: --version v1|v2 [--json]')
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (!existsSync(runtime)) {
|
|
176
|
+
unavailable('installed package does not include mcp_persistence_server.py')
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const root = mkdtempSync(join(tmpdir(), 'pantheon-context-probe-'))
|
|
181
|
+
const dbRoot = join(root, 'db')
|
|
182
|
+
const python = pythonCandidates().find((candidate) =>
|
|
183
|
+
candidate.includes('/') ? existsSync(candidate) : true,
|
|
184
|
+
)
|
|
185
|
+
if (!python) {
|
|
186
|
+
rmSync(root, { recursive: true, force: true })
|
|
187
|
+
unavailable('Python persistence runtime is not available')
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const child = spawnSync(python, ['-c', PYTHON_HARNESS, runtime, dbRoot], {
|
|
193
|
+
encoding: 'utf8',
|
|
194
|
+
timeout: 30000,
|
|
195
|
+
env: process.env,
|
|
196
|
+
})
|
|
197
|
+
if (child.error) {
|
|
198
|
+
const detail = child.error instanceof Error ? child.error.message : String(child.error)
|
|
199
|
+
if (/EACCES|permission denied|not found/i.test(detail)) {
|
|
200
|
+
emit('AMBIENTAL', `persistence runtime unavailable: ${detail}`)
|
|
201
|
+
} else {
|
|
202
|
+
unavailable(`persistence runtime unavailable: ${detail}`)
|
|
203
|
+
}
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if (child.signal === 'SIGTERM') {
|
|
207
|
+
emit('AMBIENTAL', 'persistence probe timed out after 30s')
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
if (child.status !== 0) {
|
|
211
|
+
const detail = `${child.stderr || child.stdout || 'persistence probe failed'}`.trim()
|
|
212
|
+
if (
|
|
213
|
+
/No module named|ModuleNotFoundError|can't open file|No such file|importerror/i.test(detail)
|
|
214
|
+
) {
|
|
215
|
+
unavailable(`persistence runtime unavailable: ${detail.slice(0, 320)}`)
|
|
216
|
+
} else {
|
|
217
|
+
emit('FAIL', `offline persistence checks failed: ${detail.slice(0, 320)}`)
|
|
218
|
+
}
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const lines = child.stdout.trim().split('\n').filter(Boolean)
|
|
223
|
+
const payload = JSON.parse(lines.at(-1) || '')
|
|
224
|
+
if (!Array.isArray(payload.checks) || payload.checks.length < 6) {
|
|
225
|
+
emit('FAIL', 'offline persistence probe returned incomplete checks')
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
emit(
|
|
229
|
+
'PASS',
|
|
230
|
+
'offline synthetic persistence checks passed; no LLM or user DB was used',
|
|
231
|
+
payload.checks,
|
|
232
|
+
)
|
|
233
|
+
} catch (error) {
|
|
234
|
+
emit('FAIL', error instanceof Error ? error.message : String(error))
|
|
235
|
+
} finally {
|
|
236
|
+
rmSync(root, { recursive: true, force: true })
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await main()
|