pantheon-opencode 1.4.2 → 1.5.0-beta.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/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +11 -0
- package/.pantheon/code-mode/README.md +93 -0
- package/.pantheon/code-mode/checkpoint-session.py +7 -0
- package/.pantheon/code-mode/checkpoint-session.sh +11 -0
- package/.pantheon/code-mode/checkpoint_session.py +481 -0
- package/.pantheon/code-mode/compress-inline.py +450 -0
- package/.pantheon/code-mode/eval-llm-judge.py +255 -0
- package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
- package/.pantheon/code-mode/eval-run.py +197 -0
- package/.pantheon/code-mode/eval-static.py +321 -0
- package/.pantheon/code-mode/example-sync.sh +5 -0
- package/.pantheon/code-mode/session-end-save.py +186 -0
- package/.pantheon/code-mode/session-end-save.sh +18 -0
- package/.pantheon/code-mode/test-allowed.sh +5 -0
- package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
- package/.pantheon/code-mode/test-env.sh +7 -0
- package/.pantheon/code-mode/test-prlimit.sh +9 -0
- package/.pantheon/code-mode/test-timeout.sh +6 -0
- package/README.md +158 -1189
- package/README.pt-BR.md +222 -0
- package/bin/pantheon-init.mjs +30 -11
- package/opencode.json +1 -0
- package/package.json +14 -3
- package/plugin.json +15 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +33 -2
- package/scripts/doctor.mjs +135 -127
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +8 -1
- package/scripts/install/config-migration.mjs +401 -0
- package/scripts/install/health-check.mjs +3 -3
- package/scripts/install/model-picker.mjs +72 -23
- package/scripts/install/opencode-version.mjs +47 -0
- package/scripts/install/opencode.mjs +261 -88
- package/scripts/install/plugin.mjs +112 -42
- package/scripts/install/shared.mjs +13 -158
- package/scripts/install-mcp.mjs +44 -232
- package/scripts/mcp_persistence_server.py +344 -15
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/sync-tui.mjs +32 -7
- package/scripts/test-opencode-v1-v2-sandbox.sh +591 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +33 -2
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +344 -15
- package/src/mcp/mcp_resources_server.py +144 -40
- package/src/mcp/pantheon_vision_server.py +1 -1
- package/src/mcp/toon_codec.py +491 -0
- package/src/pantheon/background-job-board.ts +67 -1
- package/src/pantheon/cost-command.ts +6 -2
- package/src/pantheon/delegate-manager.ts +384 -0
- package/src/pantheon/delegate-task-adapter.ts +444 -0
- package/src/pantheon/delegation-notify.ts +35 -20
- package/src/pantheon/model-command.ts +86 -32
- package/src/pantheon/token-opt.ts +555 -0
- package/src/pantheon/toon-codec.ts +481 -0
- package/src/pantheon/tui-version-check.ts +1 -3
- package/src/pantheon/v2-bridge.ts +129 -0
- package/src/pantheon/v2-events.ts +131 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +836 -0
- package/src/plugin.ts +57 -10
- 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 +10 -7
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +11 -8
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +22 -16
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +22 -16
- package/src/skills/README.md +0 -2
|
@@ -11,11 +11,13 @@ Usage:
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
14
16
|
import re
|
|
15
17
|
import sqlite3
|
|
16
18
|
import sys
|
|
17
19
|
import uuid
|
|
18
|
-
from datetime import datetime, timedelta
|
|
20
|
+
from datetime import UTC, datetime, timedelta
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
|
|
21
23
|
from _pantheon_paths import pantheon_home, pantheon_project
|
|
@@ -159,7 +161,7 @@ def _write_deletelog(db_path: Path, count: int, keys: list[str]) -> None:
|
|
|
159
161
|
log_path = db_path.with_name(db_path.name + ".deletelog")
|
|
160
162
|
_rotate_deletelog(log_path)
|
|
161
163
|
|
|
162
|
-
timestamp = datetime.now(
|
|
164
|
+
timestamp = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
163
165
|
keys_str = ",".join(keys)
|
|
164
166
|
with open(log_path, "a", encoding="utf-8") as f:
|
|
165
167
|
f.write(f"[{timestamp}] PURGED={count} KEYS=[{keys_str}]\n")
|
|
@@ -220,7 +222,7 @@ async def kv_store(
|
|
|
220
222
|
conn = _db(scope)
|
|
221
223
|
expires_at: str | None = None
|
|
222
224
|
if ttl is not None:
|
|
223
|
-
expires_at = (datetime.now(
|
|
225
|
+
expires_at = (datetime.now(UTC) + timedelta(seconds=ttl)).isoformat()
|
|
224
226
|
|
|
225
227
|
conn.execute(
|
|
226
228
|
"INSERT INTO kv_store (namespace, key, value, expires_at, "
|
|
@@ -287,7 +289,9 @@ async def kv_stats(
|
|
|
287
289
|
"""
|
|
288
290
|
conn = _db(scope)
|
|
289
291
|
|
|
290
|
-
total = conn.execute(
|
|
292
|
+
total = conn.execute(
|
|
293
|
+
"SELECT COUNT(*) FROM kv_store WHERE deleted_at IS NULL"
|
|
294
|
+
).fetchone()[0]
|
|
291
295
|
expired = conn.execute(
|
|
292
296
|
"SELECT COUNT(*) FROM kv_store "
|
|
293
297
|
"WHERE expires_at IS NOT NULL AND datetime(expires_at) < datetime('now') "
|
|
@@ -300,10 +304,7 @@ async def kv_stats(
|
|
|
300
304
|
"FROM kv_store WHERE deleted_at IS NULL GROUP BY namespace ORDER BY cnt DESC"
|
|
301
305
|
).fetchall()
|
|
302
306
|
|
|
303
|
-
namespaces = {
|
|
304
|
-
r[0]: {"count": r[1], "expired": r[2]}
|
|
305
|
-
for r in ns_rows
|
|
306
|
-
}
|
|
307
|
+
namespaces = {r[0]: {"count": r[1], "expired": r[2]} for r in ns_rows}
|
|
307
308
|
|
|
308
309
|
db_path = _resolve_db_path(scope)
|
|
309
310
|
db_size = db_path.stat().st_size if db_path and db_path.exists() else 0
|
|
@@ -316,6 +317,7 @@ async def kv_stats(
|
|
|
316
317
|
"db_size_bytes": db_size,
|
|
317
318
|
}
|
|
318
319
|
|
|
320
|
+
|
|
319
321
|
@mcp.tool(
|
|
320
322
|
name="kv_delete",
|
|
321
323
|
description="Delete a key-value pair by namespace and key. "
|
|
@@ -504,8 +506,9 @@ async def purge_expired(
|
|
|
504
506
|
return {"purged": count, "dry_run": False}
|
|
505
507
|
|
|
506
508
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
+
def _opportunistic_auto_purge(
|
|
510
|
+
conn: sqlite3.Connection, namespace: str, threshold: int = 500
|
|
511
|
+
) -> None:
|
|
509
512
|
"""Lightweight auto-purge: if namespace exceeds threshold, soft-delete expired entries."""
|
|
510
513
|
count = conn.execute(
|
|
511
514
|
"SELECT COUNT(*) FROM kv_store WHERE namespace = ? AND deleted_at IS NULL",
|
|
@@ -603,7 +606,7 @@ async def context_save(
|
|
|
603
606
|
actual_ttl = ttl if ttl is not None else DEFAULT_CONTEXT_TTL
|
|
604
607
|
|
|
605
608
|
conn = _db(scope)
|
|
606
|
-
expires_at = (datetime.now(
|
|
609
|
+
expires_at = (datetime.now(UTC) + timedelta(seconds=actual_ttl)).isoformat()
|
|
607
610
|
|
|
608
611
|
conn.execute(
|
|
609
612
|
"INSERT INTO kv_store (namespace, key, value, expires_at, "
|
|
@@ -630,7 +633,13 @@ async def context_save(
|
|
|
630
633
|
)
|
|
631
634
|
conn.commit()
|
|
632
635
|
|
|
633
|
-
return {
|
|
636
|
+
return {
|
|
637
|
+
"status": "stored",
|
|
638
|
+
"namespace": ns,
|
|
639
|
+
"key": key,
|
|
640
|
+
"session_id": actual_session,
|
|
641
|
+
"ttl": actual_ttl,
|
|
642
|
+
}
|
|
634
643
|
|
|
635
644
|
|
|
636
645
|
@mcp.tool(
|
|
@@ -731,8 +740,7 @@ async def context_stats(
|
|
|
731
740
|
ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
|
|
732
741
|
|
|
733
742
|
count = conn.execute(
|
|
734
|
-
"SELECT COUNT(*) FROM kv_store "
|
|
735
|
-
"WHERE namespace = ? AND deleted_at IS NULL",
|
|
743
|
+
"SELECT COUNT(*) FROM kv_store WHERE namespace = ? AND deleted_at IS NULL",
|
|
736
744
|
(ns,),
|
|
737
745
|
).fetchone()[0]
|
|
738
746
|
|
|
@@ -759,7 +767,7 @@ async def context_stats(
|
|
|
759
767
|
if row and row[0]:
|
|
760
768
|
try:
|
|
761
769
|
exp = datetime.fromisoformat(row[0])
|
|
762
|
-
now = datetime.now(
|
|
770
|
+
now = datetime.now(UTC)
|
|
763
771
|
remaining = max(0, int((exp - now).total_seconds()))
|
|
764
772
|
except (ValueError, TypeError):
|
|
765
773
|
pass
|
|
@@ -773,6 +781,327 @@ async def context_stats(
|
|
|
773
781
|
"ttl_remaining_seconds": remaining,
|
|
774
782
|
}
|
|
775
783
|
|
|
784
|
+
|
|
785
|
+
# ── Post-Compaction Injector (WS2 PR #94) ─────────────────────────────────────
|
|
786
|
+
# Deterministic rehydration after a native compaction event: a long session
|
|
787
|
+
# reads ``latest`` + serialized ``tail`` from ``checkpoint:<slug>`` and
|
|
788
|
+
# rebuilds its critical context (active goal, in-flight delegations,
|
|
789
|
+
# current phase) WITHOUT any LLM call or generative embedding.
|
|
790
|
+
#
|
|
791
|
+
# Checkpoint JSON convention (written by Zeus via context_save):
|
|
792
|
+
# {"version": 1,
|
|
793
|
+
# "goal": {"id": ..., "objective": ..., "status": ...},
|
|
794
|
+
# "phase": {"current": ..., "total": ..., "name": ...},
|
|
795
|
+
# "delegations": {"in_flight": [{"alias": ..., "agent": ..., "task_id": ...}]},
|
|
796
|
+
# "tail": ["phase:1 ...", ...], # serialized last-N phase digests
|
|
797
|
+
# "heartbeat": {...}}
|
|
798
|
+
# ``tail`` may also live under a separate ``tail`` key (JSON array).
|
|
799
|
+
#
|
|
800
|
+
# Guarantees: kill-switch PANTHEON_COMPACTION=off, TTL-respecting reads,
|
|
801
|
+
# heartbeat TTL refresh (extended, never shortened), idempotent (pure reads
|
|
802
|
+
# + TTL touch — no checkpoint rows are added or duplicated).
|
|
803
|
+
|
|
804
|
+
COMPACTION_KILL_SWITCH_ENV: str = "PANTHEON_COMPACTION"
|
|
805
|
+
SESSION_END_SUMMARY_ENV: str = "PANTHEON_SESSION_END_SUMMARY"
|
|
806
|
+
REHYDRATE_TAIL_CAP: int = 10
|
|
807
|
+
SUMMARY_TAIL_CAP: int = 5
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _compaction_disabled() -> bool:
|
|
811
|
+
"""True when the PANTHEON_COMPACTION kill-switch is off."""
|
|
812
|
+
return os.environ.get(COMPACTION_KILL_SWITCH_ENV, "").lower() == "off"
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _read_checkpoint_value(
|
|
816
|
+
conn: sqlite3.Connection, namespace: str, key: str
|
|
817
|
+
) -> str | None:
|
|
818
|
+
"""Read one unexpired, non-deleted checkpoint value (None when absent)."""
|
|
819
|
+
row = conn.execute(
|
|
820
|
+
"SELECT value FROM kv_store "
|
|
821
|
+
"WHERE namespace = ? AND key = ? "
|
|
822
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
823
|
+
"AND deleted_at IS NULL",
|
|
824
|
+
(namespace, key),
|
|
825
|
+
).fetchone()
|
|
826
|
+
return row[0] if row else None
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _parse_checkpoint(content: str) -> dict:
|
|
830
|
+
"""Parse checkpoint JSON; unparsable content degrades to {} (fail-open)."""
|
|
831
|
+
try:
|
|
832
|
+
data = json.loads(content)
|
|
833
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
834
|
+
return {}
|
|
835
|
+
return data if isinstance(data, dict) else {}
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _checkpoint_tail(checkpoint: dict, tail_raw: str | None) -> list[str]:
|
|
839
|
+
"""Tail lines from embedded ``tail`` with fallback to the ``tail`` key."""
|
|
840
|
+
tail = checkpoint.get("tail")
|
|
841
|
+
if isinstance(tail, list):
|
|
842
|
+
lines = [str(line) for line in tail]
|
|
843
|
+
elif isinstance(tail_raw, str):
|
|
844
|
+
try:
|
|
845
|
+
parsed = json.loads(tail_raw)
|
|
846
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
847
|
+
parsed = None
|
|
848
|
+
lines = [str(line) for line in parsed] if isinstance(parsed, list) else []
|
|
849
|
+
else:
|
|
850
|
+
lines = []
|
|
851
|
+
return [line for line in lines if line.strip()][:REHYDRATE_TAIL_CAP]
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _mission_block(checkpoint: dict) -> str | None:
|
|
855
|
+
"""<mission_context> for a non-done goal (done goals stay buried)."""
|
|
856
|
+
goal = checkpoint.get("goal")
|
|
857
|
+
if not isinstance(goal, dict):
|
|
858
|
+
return None
|
|
859
|
+
status = str(goal.get("status", ""))
|
|
860
|
+
objective = str(goal.get("objective", "")).strip()
|
|
861
|
+
if status == "done" or not objective:
|
|
862
|
+
return None
|
|
863
|
+
gid = str(goal.get("id", "goal")).strip() or "goal"
|
|
864
|
+
return f"<mission_context>\n [{gid}] {objective} — {status}"
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _phase_block(checkpoint: dict) -> str | None:
|
|
868
|
+
"""<phase_context> for the current phase (omitted when unknown)."""
|
|
869
|
+
phase = checkpoint.get("phase")
|
|
870
|
+
if not isinstance(phase, dict) or phase.get("current") is None:
|
|
871
|
+
return None
|
|
872
|
+
total = phase.get("total")
|
|
873
|
+
current = phase.get("current")
|
|
874
|
+
span = f"{current}/{total}" if total is not None else str(current)
|
|
875
|
+
name = str(phase.get("name", "")).strip()
|
|
876
|
+
line = f" phase {span}" + (f" — {name}" if name else "")
|
|
877
|
+
return f"<phase_context>\n{line}"
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _delegation_block(checkpoint: dict) -> str | None:
|
|
881
|
+
"""<delegation_context> for in-flight delegations (never capped)."""
|
|
882
|
+
delegations = checkpoint.get("delegations")
|
|
883
|
+
if not isinstance(delegations, dict):
|
|
884
|
+
return None
|
|
885
|
+
in_flight = delegations.get("in_flight")
|
|
886
|
+
if not isinstance(in_flight, list):
|
|
887
|
+
return None
|
|
888
|
+
lines = []
|
|
889
|
+
for job in in_flight:
|
|
890
|
+
if not isinstance(job, dict):
|
|
891
|
+
continue
|
|
892
|
+
alias = str(job.get("alias") or job.get("task_id") or "?").strip()
|
|
893
|
+
agent = str(job.get("agent", "?")).strip()
|
|
894
|
+
task_id = str(job.get("task_id", "")).strip()
|
|
895
|
+
detail = f" — {task_id}" if task_id and task_id != alias else ""
|
|
896
|
+
lines.append(f" [{alias}] {agent}{detail} [in-flight]")
|
|
897
|
+
if not lines:
|
|
898
|
+
return None
|
|
899
|
+
return "<delegation_context>\n" + "\n".join(lines)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _tail_block(checkpoint: dict) -> str | None:
|
|
903
|
+
"""<tail_context> for the serialized tail (already capped on load)."""
|
|
904
|
+
tail_lines = checkpoint.get("_tail_raw")
|
|
905
|
+
if not isinstance(tail_lines, list) or not tail_lines:
|
|
906
|
+
return None
|
|
907
|
+
quoted = "\n".join(f" - {line}" for line in tail_lines)
|
|
908
|
+
return f"<tail_context>\n{quoted}"
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def build_rehydration_blocks(checkpoint: dict) -> list[str] | None:
|
|
912
|
+
"""Build deterministic rehydration blocks from a checkpoint dict.
|
|
913
|
+
|
|
914
|
+
Pure string templating — no LLM, no embeddings. Mirrors the
|
|
915
|
+
``<mission_context>`` convention of delegation-compaction.ts: only a
|
|
916
|
+
non-done goal is re-emitted. Returns None when nothing critical remains.
|
|
917
|
+
"""
|
|
918
|
+
if not isinstance(checkpoint, dict) or not checkpoint:
|
|
919
|
+
return None
|
|
920
|
+
blocks = [
|
|
921
|
+
block
|
|
922
|
+
for block in (
|
|
923
|
+
_mission_block(checkpoint),
|
|
924
|
+
_phase_block(checkpoint),
|
|
925
|
+
_delegation_block(checkpoint),
|
|
926
|
+
_tail_block(checkpoint),
|
|
927
|
+
)
|
|
928
|
+
if block is not None
|
|
929
|
+
]
|
|
930
|
+
return blocks if blocks else None
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def build_session_end_summary(checkpoint: dict) -> str | None:
|
|
934
|
+
"""Build the Tier 2 session-end summary deterministically (no LLM).
|
|
935
|
+
|
|
936
|
+
Fires automatically at session end — no Themis approval gate. Returns
|
|
937
|
+
None when the checkpoint carries nothing worth compressing.
|
|
938
|
+
"""
|
|
939
|
+
if not isinstance(checkpoint, dict) or not checkpoint:
|
|
940
|
+
return None
|
|
941
|
+
parts: list[str] = []
|
|
942
|
+
|
|
943
|
+
goal = checkpoint.get("goal")
|
|
944
|
+
if isinstance(goal, dict):
|
|
945
|
+
objective = str(goal.get("objective", "")).strip()
|
|
946
|
+
if objective:
|
|
947
|
+
parts.append(f"Goal: {objective} ({goal.get('status', '?')})")
|
|
948
|
+
|
|
949
|
+
phase = checkpoint.get("phase")
|
|
950
|
+
if isinstance(phase, dict) and phase.get("current") is not None:
|
|
951
|
+
total = phase.get("total")
|
|
952
|
+
span = (
|
|
953
|
+
f"{phase.get('current')}/{total}"
|
|
954
|
+
if total is not None
|
|
955
|
+
else phase.get("current")
|
|
956
|
+
)
|
|
957
|
+
name = str(phase.get("name", "")).strip()
|
|
958
|
+
parts.append(f"Phase: {span}" + (f" {name}" if name else ""))
|
|
959
|
+
|
|
960
|
+
delegations = checkpoint.get("delegations")
|
|
961
|
+
if isinstance(delegations, dict):
|
|
962
|
+
in_flight = delegations.get("in_flight")
|
|
963
|
+
if isinstance(in_flight, list):
|
|
964
|
+
aliases = [
|
|
965
|
+
str(job.get("alias") or job.get("task_id") or "?")
|
|
966
|
+
for job in in_flight
|
|
967
|
+
if isinstance(job, dict)
|
|
968
|
+
]
|
|
969
|
+
if aliases:
|
|
970
|
+
parts.append(f"In-flight: {', '.join(aliases)}")
|
|
971
|
+
|
|
972
|
+
tail_raw = checkpoint.get("_tail_raw")
|
|
973
|
+
if isinstance(tail_raw, list) and tail_raw:
|
|
974
|
+
kept = [str(line) for line in tail_raw[:SUMMARY_TAIL_CAP]]
|
|
975
|
+
parts.append("Tail: " + " | ".join(kept))
|
|
976
|
+
|
|
977
|
+
if not parts:
|
|
978
|
+
return None
|
|
979
|
+
return "# Session summary (Tier 2 auto — no approval gate)\n" + "\n".join(
|
|
980
|
+
f"- {part}" for part in parts
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _load_checkpoint_for_rehydrate(
|
|
985
|
+
conn: sqlite3.Connection, namespace: str
|
|
986
|
+
) -> dict | None:
|
|
987
|
+
"""Load ``latest`` (+ ``tail`` fallback) or None when expired/absent.
|
|
988
|
+
|
|
989
|
+
Resilient to pointer clobbering: ``context_save`` refreshes ``latest``
|
|
990
|
+
on every write, so a later heartbeat can overwrite the checkpoint
|
|
991
|
+
pointer. When ``latest`` is not a checkpoint, scan sibling keys
|
|
992
|
+
newest-first and use the first value that rebuilds critical blocks.
|
|
993
|
+
Deterministic (created_at DESC, bounded scan).
|
|
994
|
+
"""
|
|
995
|
+
tail_raw = _read_checkpoint_value(conn, namespace, "tail")
|
|
996
|
+
|
|
997
|
+
def _with_tail(raw: str | None) -> dict | None:
|
|
998
|
+
if raw is None:
|
|
999
|
+
return None
|
|
1000
|
+
checkpoint = _parse_checkpoint(raw)
|
|
1001
|
+
if not checkpoint:
|
|
1002
|
+
return None
|
|
1003
|
+
checkpoint["_tail_raw"] = _checkpoint_tail(checkpoint, tail_raw)
|
|
1004
|
+
if build_rehydration_blocks(checkpoint) is None:
|
|
1005
|
+
return None
|
|
1006
|
+
return checkpoint
|
|
1007
|
+
|
|
1008
|
+
found = _with_tail(_read_checkpoint_value(conn, namespace, "latest"))
|
|
1009
|
+
if found is not None:
|
|
1010
|
+
return found
|
|
1011
|
+
rows = conn.execute(
|
|
1012
|
+
"SELECT value FROM kv_store "
|
|
1013
|
+
"WHERE namespace = ? AND key != 'latest' "
|
|
1014
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
1015
|
+
"AND deleted_at IS NULL "
|
|
1016
|
+
"ORDER BY created_at DESC, key DESC LIMIT 20",
|
|
1017
|
+
(namespace,),
|
|
1018
|
+
).fetchall()
|
|
1019
|
+
for row in rows:
|
|
1020
|
+
found = _with_tail(row[0] if row else None)
|
|
1021
|
+
if found is not None:
|
|
1022
|
+
return found
|
|
1023
|
+
return None
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _refresh_heartbeat_ttl(conn: sqlite3.Connection, namespace: str) -> None:
|
|
1027
|
+
"""Extend an unexpired heartbeat TTL; never shorten, never resurrect."""
|
|
1028
|
+
row = conn.execute(
|
|
1029
|
+
"SELECT expires_at FROM kv_store "
|
|
1030
|
+
"WHERE namespace = ? AND key = 'heartbeat' "
|
|
1031
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
1032
|
+
"AND deleted_at IS NULL",
|
|
1033
|
+
(namespace,),
|
|
1034
|
+
).fetchone()
|
|
1035
|
+
if not row or not row[0]:
|
|
1036
|
+
return
|
|
1037
|
+
try:
|
|
1038
|
+
current_exp = datetime.fromisoformat(row[0])
|
|
1039
|
+
except (ValueError, TypeError):
|
|
1040
|
+
return
|
|
1041
|
+
now = datetime.now(UTC)
|
|
1042
|
+
if current_exp.tzinfo is None:
|
|
1043
|
+
current_exp = current_exp.replace(tzinfo=UTC)
|
|
1044
|
+
fresh_exp = now + timedelta(seconds=DEFAULT_CONTEXT_TTL)
|
|
1045
|
+
if fresh_exp <= current_exp:
|
|
1046
|
+
return
|
|
1047
|
+
conn.execute(
|
|
1048
|
+
"UPDATE kv_store SET expires_at = ?, updated_at = datetime('now') "
|
|
1049
|
+
"WHERE namespace = ? AND key = 'heartbeat'",
|
|
1050
|
+
(fresh_exp.isoformat(), namespace),
|
|
1051
|
+
)
|
|
1052
|
+
conn.commit()
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
@mcp.tool(
|
|
1056
|
+
name="context_rehydrate",
|
|
1057
|
+
description="Rehydrate critical context after native compaction. "
|
|
1058
|
+
"Reads 'latest' + serialized 'tail' from checkpoint:<slug> and rebuilds "
|
|
1059
|
+
"goal/phase/delegation blocks deterministically (no LLM). Refreshes the "
|
|
1060
|
+
"heartbeat TTL. Idempotent. Honors PANTHEON_COMPACTION=off.",
|
|
1061
|
+
)
|
|
1062
|
+
async def context_rehydrate(
|
|
1063
|
+
slug: str,
|
|
1064
|
+
session_id: str | None = None,
|
|
1065
|
+
scope: str = "project",
|
|
1066
|
+
) -> list[str] | None:
|
|
1067
|
+
"""Post-compaction injector: deterministic rehydration, fail-open."""
|
|
1068
|
+
if _compaction_disabled():
|
|
1069
|
+
return None
|
|
1070
|
+
conn = _db(scope)
|
|
1071
|
+
ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
|
|
1072
|
+
checkpoint = _load_checkpoint_for_rehydrate(conn, ns)
|
|
1073
|
+
if checkpoint is None:
|
|
1074
|
+
return None
|
|
1075
|
+
blocks = build_rehydration_blocks(checkpoint)
|
|
1076
|
+
if blocks is None:
|
|
1077
|
+
return None
|
|
1078
|
+
_refresh_heartbeat_ttl(conn, ns)
|
|
1079
|
+
return blocks
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
@mcp.tool(
|
|
1083
|
+
name="context_session_summary",
|
|
1084
|
+
description="Tier 2 session-end summary without Themis approval. "
|
|
1085
|
+
"Compresses the checkpoint (goal, phase, in-flight, tail) into a "
|
|
1086
|
+
"memory-bank-ready summary deterministically (no LLM). "
|
|
1087
|
+
"Opt-out via PANTHEON_SESSION_END_SUMMARY=off.",
|
|
1088
|
+
)
|
|
1089
|
+
async def context_session_summary(
|
|
1090
|
+
slug: str,
|
|
1091
|
+
session_id: str | None = None,
|
|
1092
|
+
scope: str = "project",
|
|
1093
|
+
) -> str | None:
|
|
1094
|
+
"""Session-end Tier 2 trigger: automatic, opt-out instead of opt-in."""
|
|
1095
|
+
if os.environ.get(SESSION_END_SUMMARY_ENV, "").lower() == "off":
|
|
1096
|
+
return None
|
|
1097
|
+
conn = _db(scope)
|
|
1098
|
+
ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
|
|
1099
|
+
checkpoint = _load_checkpoint_for_rehydrate(conn, ns)
|
|
1100
|
+
if checkpoint is None:
|
|
1101
|
+
return None
|
|
1102
|
+
return build_session_end_summary(checkpoint)
|
|
1103
|
+
|
|
1104
|
+
|
|
776
1105
|
def _resolve_db_path(scope: str) -> Path | None:
|
|
777
1106
|
"""Resolve the database file path for a given scope.
|
|
778
1107
|
|