pantheon-opencode 1.5.0-beta.1 → 1.5.0-beta.2
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 +3 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/cost.mjs +30 -14
- package/scripts/mcp_persistence_server.py +44 -7
- package/scripts/probe-context-rehydrate.mjs +240 -0
- package/scripts/probe-pantheon-cost.mjs +304 -0
- package/scripts/test-opencode-v1-v2-sandbox.sh +394 -41
- package/src/mcp/mcp_persistence_server.py +44 -7
- package/src/pantheon/cost-command.ts +49 -32
- package/src/pantheon/delegate-manager.ts +285 -0
- package/src/pantheon/token-opt.ts +237 -11
- package/src/plugin-v2.ts +2 -14
- package/src/plugin.ts +3 -8
- package/src/plugins/tui/package.json +1 -1
- 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
package/README.md
CHANGED
|
@@ -90,6 +90,9 @@ and [changelog](CHANGELOG.md) for the latest changes.
|
|
|
90
90
|
- Sandbox validator for global installs (`scripts/test-opencode-v1-v2-sandbox.sh`)
|
|
91
91
|
covering OpenCode V1/V2 side by side — see
|
|
92
92
|
[Sandbox validation](#sandbox-validation-v1v2).
|
|
93
|
+
- Beta2 agent-economy policy: direct native delegation, bounded compaction
|
|
94
|
+
carry-forward, compact context encoding, and quality floors — see
|
|
95
|
+
[Beta2 agent economy policy](docs/ws3-token-opt-measurements.md#beta2-agent-economy-policy).
|
|
93
96
|
- A `--prompts` installer flag is planned for a future release.
|
|
94
97
|
|
|
95
98
|
## OpenCode V1/V2 — Dual Version (1.5.0)
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon",
|
|
3
3
|
"publisher": "ils15",
|
|
4
|
-
"version": "1.5.0
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"displayName": "Pantheon",
|
|
6
6
|
"description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
|
|
7
7
|
"agents": "./src/agents",
|
package/scripts/cost.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* cost.mjs — read-only
|
|
3
|
+
* cost.mjs — read-only token aggregation from opencode.db.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* the plugin process
|
|
7
|
-
*
|
|
8
|
-
* { ok: true,
|
|
5
|
+
* CLI fallback for the `pantheon_cost` tool when node:sqlite is unavailable
|
|
6
|
+
* in the plugin process. Uses node:sqlite itself and prints a single JSON
|
|
7
|
+
* object to stdout:
|
|
8
|
+
* { ok: true, days, rows: [{agent, phase, tokensInput, tokensOutput, tokensTotal}] }
|
|
9
9
|
* { ok: false, error: "<message>" } (exit code 1)
|
|
10
10
|
*
|
|
11
11
|
* NEVER writes to the database — opened read-only. Malformed rows are
|
|
12
|
-
* skipped; assistant messages carry the authoritative
|
|
12
|
+
* skipped; assistant messages carry the authoritative token usage.
|
|
13
13
|
*
|
|
14
|
-
* Usage: node scripts/cost.mjs <dbPath> <days>
|
|
14
|
+
* Usage: node scripts/cost.mjs --db-path <dbPath> --days <days>
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync } from 'node:fs'
|
|
17
17
|
import { DatabaseSync } from 'node:sqlite'
|
|
@@ -43,8 +43,13 @@ function assertCompatibleSchema(db) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function main() {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
46
|
+
const argv = process.argv.slice(2)
|
|
47
|
+
const valueFor = (name) => {
|
|
48
|
+
const index = argv.indexOf(name)
|
|
49
|
+
return index >= 0 ? argv[index + 1] : undefined
|
|
50
|
+
}
|
|
51
|
+
const dbPath = valueFor('--db-path')
|
|
52
|
+
const days = Number(valueFor('--days') ?? '7')
|
|
48
53
|
|
|
49
54
|
if (!dbPath) return fail('missing <dbPath> argument')
|
|
50
55
|
if (!existsSync(dbPath)) return fail(`database not found: ${dbPath}`)
|
|
@@ -64,19 +69,30 @@ function main() {
|
|
|
64
69
|
if (info.role !== 'assistant') continue
|
|
65
70
|
const agent = typeof info.agent === 'string' && info.agent !== '' ? info.agent : null
|
|
66
71
|
if (!agent) continue
|
|
67
|
-
const
|
|
72
|
+
const metadata = info.metadata
|
|
73
|
+
const phase =
|
|
74
|
+
(typeof info.phase === 'string' && info.phase) ||
|
|
75
|
+
(typeof metadata?.phase === 'string' && metadata.phase) ||
|
|
76
|
+
'unknown'
|
|
68
77
|
const tokensInput = Number(info.tokens?.input) || 0
|
|
69
78
|
const tokensOutput = Number(info.tokens?.output) || 0
|
|
70
|
-
const
|
|
71
|
-
acc.
|
|
79
|
+
const key = `${agent}\u0000${phase}`
|
|
80
|
+
const acc = byAgent.get(key) ?? {
|
|
81
|
+
agent,
|
|
82
|
+
phase,
|
|
83
|
+
tokensInput: 0,
|
|
84
|
+
tokensOutput: 0,
|
|
85
|
+
tokensTotal: 0,
|
|
86
|
+
}
|
|
72
87
|
acc.tokensInput += tokensInput
|
|
73
88
|
acc.tokensOutput += tokensOutput
|
|
74
|
-
|
|
89
|
+
acc.tokensTotal += tokensInput + tokensOutput
|
|
90
|
+
byAgent.set(key, acc)
|
|
75
91
|
} catch {
|
|
76
92
|
// Skip malformed rows — a partial ledger never breaks the report.
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
|
-
const result = [...byAgent.values()].sort((a, b) => b.
|
|
95
|
+
const result = [...byAgent.values()].sort((a, b) => b.tokensTotal - a.tokensTotal)
|
|
80
96
|
process.stdout.write(JSON.stringify({ ok: true, days, rows: result }))
|
|
81
97
|
} catch (err) {
|
|
82
98
|
fail(err instanceof Error ? err.message : String(err))
|
|
@@ -566,7 +566,7 @@ async def kv_delete_namespace(
|
|
|
566
566
|
# ── Context Checkpoint Tools ────────────────────────────────────────────────────
|
|
567
567
|
# Session-scoped context storage for deepwork checkpoints, phase state,
|
|
568
568
|
# tool call history, and reasoning chains. All entries use namespace
|
|
569
|
-
# "checkpoint:{slug}" with auto-TTL of 4 hours (session duration).
|
|
569
|
+
# "checkpoint:{slug}:{session}" with auto-TTL of 4 hours (session duration).
|
|
570
570
|
|
|
571
571
|
DEFAULT_CONTEXT_TTL: int = 14400 # 4 hours
|
|
572
572
|
|
|
@@ -1023,6 +1023,31 @@ def _load_checkpoint_for_rehydrate(
|
|
|
1023
1023
|
return None
|
|
1024
1024
|
|
|
1025
1025
|
|
|
1026
|
+
def _checkpoint_namespaces(
|
|
1027
|
+
conn: sqlite3.Connection, slug: str, session_id: str | None
|
|
1028
|
+
) -> list[str]:
|
|
1029
|
+
"""Resolve checkpoint namespaces while preserving session isolation.
|
|
1030
|
+
|
|
1031
|
+
``context_save`` always creates a session-qualified namespace, including
|
|
1032
|
+
when the caller omits ``session_id`` (it generates one and returns it).
|
|
1033
|
+
Runtime callers may not retain that return value after compaction, so an
|
|
1034
|
+
unqualified rehydrate/summary must discover the newest matching session.
|
|
1035
|
+
The prefix comparison is parameterized and avoids LIKE wildcard leakage.
|
|
1036
|
+
"""
|
|
1037
|
+
if session_id:
|
|
1038
|
+
return [f"checkpoint:{slug}:{session_id}"]
|
|
1039
|
+
prefix = f"checkpoint:{slug}:"
|
|
1040
|
+
rows = conn.execute(
|
|
1041
|
+
"SELECT namespace FROM kv_store "
|
|
1042
|
+
"WHERE substr(namespace, 1, ?) = ? "
|
|
1043
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
1044
|
+
"AND deleted_at IS NULL "
|
|
1045
|
+
"GROUP BY namespace ORDER BY MAX(updated_at) DESC LIMIT 20",
|
|
1046
|
+
(len(prefix), prefix),
|
|
1047
|
+
).fetchall()
|
|
1048
|
+
return [str(row[0]) for row in rows if row and row[0]]
|
|
1049
|
+
|
|
1050
|
+
|
|
1026
1051
|
def _refresh_heartbeat_ttl(conn: sqlite3.Connection, namespace: str) -> None:
|
|
1027
1052
|
"""Extend an unexpired heartbeat TTL; never shorten, never resurrect."""
|
|
1028
1053
|
row = conn.execute(
|
|
@@ -1055,7 +1080,8 @@ def _refresh_heartbeat_ttl(conn: sqlite3.Connection, namespace: str) -> None:
|
|
|
1055
1080
|
@mcp.tool(
|
|
1056
1081
|
name="context_rehydrate",
|
|
1057
1082
|
description="Rehydrate critical context after native compaction. "
|
|
1058
|
-
"Reads 'latest' + serialized 'tail' from
|
|
1083
|
+
"Reads 'latest' + serialized 'tail' from a session-qualified checkpoint "
|
|
1084
|
+
"namespace and rebuilds "
|
|
1059
1085
|
"goal/phase/delegation blocks deterministically (no LLM). Refreshes the "
|
|
1060
1086
|
"heartbeat TTL. Idempotent. Honors PANTHEON_COMPACTION=off.",
|
|
1061
1087
|
)
|
|
@@ -1068,14 +1094,21 @@ async def context_rehydrate(
|
|
|
1068
1094
|
if _compaction_disabled():
|
|
1069
1095
|
return None
|
|
1070
1096
|
conn = _db(scope)
|
|
1071
|
-
|
|
1072
|
-
checkpoint =
|
|
1097
|
+
namespaces = _checkpoint_namespaces(conn, slug, session_id)
|
|
1098
|
+
checkpoint = None
|
|
1099
|
+
selected_namespace = None
|
|
1100
|
+
for namespace in namespaces:
|
|
1101
|
+
checkpoint = _load_checkpoint_for_rehydrate(conn, namespace)
|
|
1102
|
+
if checkpoint is not None:
|
|
1103
|
+
selected_namespace = namespace
|
|
1104
|
+
break
|
|
1073
1105
|
if checkpoint is None:
|
|
1074
1106
|
return None
|
|
1075
1107
|
blocks = build_rehydration_blocks(checkpoint)
|
|
1076
1108
|
if blocks is None:
|
|
1077
1109
|
return None
|
|
1078
|
-
|
|
1110
|
+
if selected_namespace is not None:
|
|
1111
|
+
_refresh_heartbeat_ttl(conn, selected_namespace)
|
|
1079
1112
|
return blocks
|
|
1080
1113
|
|
|
1081
1114
|
|
|
@@ -1095,8 +1128,12 @@ async def context_session_summary(
|
|
|
1095
1128
|
if os.environ.get(SESSION_END_SUMMARY_ENV, "").lower() == "off":
|
|
1096
1129
|
return None
|
|
1097
1130
|
conn = _db(scope)
|
|
1098
|
-
|
|
1099
|
-
checkpoint =
|
|
1131
|
+
namespaces = _checkpoint_namespaces(conn, slug, session_id)
|
|
1132
|
+
checkpoint = None
|
|
1133
|
+
for namespace in namespaces:
|
|
1134
|
+
checkpoint = _load_checkpoint_for_rehydrate(conn, namespace)
|
|
1135
|
+
if checkpoint is not None:
|
|
1136
|
+
break
|
|
1100
1137
|
if checkpoint is None:
|
|
1101
1138
|
return None
|
|
1102
1139
|
return build_session_end_summary(checkpoint)
|
|
@@ -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()
|