pantheon-opencode 1.5.0-beta.1 → 1.5.0-beta.11

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.
Files changed (83) hide show
  1. package/.pantheon/code-mode/manifest.json +21 -0
  2. package/CHANGELOG.md +1054 -0
  3. package/README.md +171 -16
  4. package/README.pt-BR.md +99 -13
  5. package/bin/pantheon-init.mjs +178 -21
  6. package/package.json +23 -15
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +65 -0
  9. package/scripts/code_mode_server.py +431 -55
  10. package/scripts/cost.mjs +30 -14
  11. package/scripts/dev-tui.sh +48 -0
  12. package/scripts/doctor.mjs +207 -9
  13. package/scripts/install/cli-ui.mjs +17 -11
  14. package/scripts/install/config-migration.mjs +4 -6
  15. package/scripts/install/opencode.mjs +137 -35
  16. package/scripts/install/plugin.mjs +3 -33
  17. package/scripts/install/shared.mjs +43 -2
  18. package/scripts/install/strings.mjs +129 -0
  19. package/scripts/install-mcp.mjs +1 -0
  20. package/scripts/manifest-inventory.mjs +410 -0
  21. package/scripts/mcp_persistence_server.py +537 -128
  22. package/scripts/package-evidence.mjs +242 -0
  23. package/scripts/probe-context-rehydrate.mjs +240 -0
  24. package/scripts/probe-pantheon-cost.mjs +313 -0
  25. package/scripts/release-beta-version.mjs +56 -60
  26. package/scripts/sync-artifacts.mjs +169 -0
  27. package/scripts/sync-tui.mjs +121 -53
  28. package/scripts/test-opencode-v1-v2-sandbox.sh +367 -96
  29. package/scripts/validation-policy.mjs +7 -7
  30. package/scripts/version-check.mjs +113 -108
  31. package/scripts/versioning.mjs +230 -86
  32. package/src/agents/mnemosyne.md +1 -1
  33. package/src/agents/zeus.md +2 -2
  34. package/src/mcp/code_mode_server.py +431 -55
  35. package/src/mcp/mcp_persistence_server.py +537 -128
  36. package/src/pantheon/auto-wake.ts +1 -5
  37. package/src/pantheon/background-job-board.ts +43 -1
  38. package/src/pantheon/compaction-handoff.ts +304 -0
  39. package/src/pantheon/cost-command.ts +345 -62
  40. package/src/pantheon/delegate-manager.ts +390 -8
  41. package/src/pantheon/delegation-finalize.ts +32 -0
  42. package/src/pantheon/delegation.ts +235 -18
  43. package/src/pantheon/file-persistence-salvage.ts +64 -0
  44. package/src/pantheon/file-persistence.ts +194 -45
  45. package/src/pantheon/native-probe.ts +234 -0
  46. package/src/pantheon/native-task-status.ts +139 -0
  47. package/src/pantheon/provider-capabilities.ts +221 -0
  48. package/src/pantheon/provider-stream-tracker.ts +182 -0
  49. package/src/pantheon/shared-board.ts +37 -0
  50. package/src/pantheon/task-result-guard.ts +107 -0
  51. package/src/pantheon/token-opt.ts +146 -12
  52. package/src/pantheon/tool-ceiling.ts +159 -0
  53. package/src/pantheon/v2-events.ts +18 -1
  54. package/src/plugin-v2.ts +2 -14
  55. package/src/plugin.ts +28 -17
  56. package/src/plugins/pantheon-hooks.ts +51 -0
  57. package/src/plugins/tui/dist/server.d.ts +1 -1
  58. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  59. package/src/plugins/tui/dist/server.js +1 -1
  60. package/src/plugins/tui/dist/server.js.map +1 -1
  61. package/src/plugins/tui/dist/tui.d.ts +137 -27
  62. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  63. package/src/plugins/tui/dist/tui.js +459 -163
  64. package/src/plugins/tui/dist/tui.js.map +1 -1
  65. package/src/plugins/tui/dist/tui.tsx +506 -103
  66. package/src/plugins/tui/package-lock.json +2 -2
  67. package/src/plugins/tui/package.json +1 -1
  68. package/src/plugins/tui/src/index.tsx +506 -103
  69. package/.pantheon/code-mode/.gitkeep +0 -0
  70. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +0 -8
  71. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +0 -8
  72. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +0 -8
  73. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +0 -8
  74. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +0 -8
  75. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +0 -8
  76. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +0 -8
  77. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +0 -8
  78. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +0 -8
  79. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +0 -8
  80. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +0 -8
  81. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +0 -8
  82. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +0 -11
  83. 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()