pantheon-opencode 1.5.0-beta.9 → 1.5.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.
Files changed (84) hide show
  1. package/AGENTS.md +14 -63
  2. package/CHANGELOG.md +326 -0
  3. package/README.md +77 -29
  4. package/README.pt-BR.md +5 -6
  5. package/commands/pantheon.md +1 -35
  6. package/package.json +2 -3
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +3 -5
  9. package/scripts/doctor.mjs +260 -5
  10. package/scripts/install/plugin.mjs +9 -12
  11. package/scripts/mcp_persistence_server.py +1 -1
  12. package/scripts/sync-tui.mjs +8 -11
  13. package/src/agents/apollo.md +1 -0
  14. package/src/agents/gaia.md +1 -0
  15. package/src/agents/mnemosyne.md +11 -1
  16. package/src/agents/zeus.md +10 -52
  17. package/src/instructions/memory-protocol.instructions.md +6 -3
  18. package/src/instructions/zeus-anti-stall.instructions.md +2 -4
  19. package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
  20. package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
  21. package/src/mcp/mcp_persistence_server.py +1 -1
  22. package/src/mcp/memory_mcp_server.py +81 -27
  23. package/src/pantheon/background-job-board.ts +4 -4
  24. package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
  25. package/src/pantheon/cost-command.ts +1 -2
  26. package/src/pantheon/delegation-enforce.ts +59 -14
  27. package/src/pantheon/goal-loop.ts +1 -1
  28. package/src/pantheon/goal-store.ts +1 -1
  29. package/src/pantheon/idle-continuation.ts +4 -6
  30. package/src/pantheon/model-command.ts +1 -2
  31. package/src/pantheon/native-task-status.ts +2 -2
  32. package/src/pantheon/presets.d.mts +0 -20
  33. package/src/pantheon/presets.mjs +0 -64
  34. package/src/pantheon/session-guard.ts +2 -2
  35. package/src/pantheon/session-id.ts +30 -0
  36. package/src/pantheon/task-result-guard.ts +2 -3
  37. package/src/pantheon/todo-enforcer.ts +3 -3
  38. package/src/pantheon/tool-context.ts +23 -0
  39. package/src/pantheon/v2-bridge.ts +1 -4
  40. package/src/plugin-v2.ts +8 -21
  41. package/src/plugin.ts +61 -245
  42. package/src/plugins/pantheon-hooks.ts +2 -2
  43. package/src/plugins/tui/dist/tui.d.ts +229 -103
  44. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  45. package/src/plugins/tui/dist/tui.js +495 -333
  46. package/src/plugins/tui/package-lock.json +5 -2
  47. package/src/plugins/tui/package.json +5 -2
  48. package/src/plugins/tui/src/index.tsx +710 -409
  49. package/src/routing.yml +0 -16
  50. package/src/skills/context-compression/SKILL.md +1 -1
  51. package/scripts/generate-preset-docs.mjs +0 -300
  52. package/scripts/generate-prompts.sh +0 -222
  53. package/scripts/hash_verify.py +0 -192
  54. package/scripts/install/component.mjs +0 -24
  55. package/scripts/install/migrate-config.mjs +0 -302
  56. package/scripts/test-context-compression.sh +0 -169
  57. package/src/mcp/toon_codec.py +0 -491
  58. package/src/pantheon/auto-wake.ts +0 -85
  59. package/src/pantheon/chat-reminders.ts +0 -9
  60. package/src/pantheon/comment-checker.ts +0 -48
  61. package/src/pantheon/compaction-assert.ts +0 -160
  62. package/src/pantheon/compaction-handoff.ts +0 -304
  63. package/src/pantheon/cost-tracker.ts +0 -108
  64. package/src/pantheon/delegate-manager.ts +0 -766
  65. package/src/pantheon/delegation-classifier.ts +0 -307
  66. package/src/pantheon/delegation-finalize.ts +0 -406
  67. package/src/pantheon/delegation-notify.ts +0 -212
  68. package/src/pantheon/delegation-toast.ts +0 -73
  69. package/src/pantheon/delegation.ts +0 -1337
  70. package/src/pantheon/dispatch-guard.ts +0 -109
  71. package/src/pantheon/native-probe.ts +0 -234
  72. package/src/pantheon/permission-eval.ts +0 -284
  73. package/src/pantheon/preemptive-compact.ts +0 -76
  74. package/src/pantheon/provider-capabilities.ts +0 -221
  75. package/src/pantheon/provider-stream-tracker.ts +0 -182
  76. package/src/pantheon/retry-policy.ts +0 -212
  77. package/src/pantheon/step-cap.ts +0 -110
  78. package/src/pantheon/token-opt.ts +0 -689
  79. package/src/pantheon/toon-codec.ts +0 -481
  80. package/src/pantheon/v2-events.ts +0 -148
  81. package/src/pantheon/v2-hooks.ts +0 -287
  82. package/src/pantheon/v2-tool-definitions.ts +0 -331
  83. package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
  84. package/src/plugins/tui/dist/tui.tsx +0 -2853
@@ -3,6 +3,7 @@ description: "Orquestrador central — NUNCA implementa. Roteia para especialist
3
3
  mode: primary
4
4
  reasoning_effort: medium
5
5
  permission:
6
+ read: allow
6
7
  edit: deny
7
8
  bash: deny
8
9
  task:
@@ -144,33 +145,11 @@ task_status(task_id="ses_xxx", wait=true)
144
145
  -> bloqueia ate completar: { state: "completed", task_result: "..." }
145
146
  ```
146
147
 
147
- ### Pantheon Delegation Tools (plugin) — 3-tool API
148
+ ### DELEGATION_RULES (native task API)
148
149
 
149
- O plugin Pantheon adiciona 3 tools de delegacao gerenciada pelo BackgroundJobBoard:
150
-
151
- ```
152
- pantheon_delegate({prompt, agent, description?, read_only?})
153
- -> cria sessao filha, registra no board, retorna alias: [apo-1]
154
- pantheon_delegation_read({id: "apo-1"})
155
- -> BLOQUEIA ate o job terminar, retorna o report markdown, marca reconciled
156
- -> durante a espera, mostra o que o agente esta fazendo: seccao final "## Agent Activity"
157
- (amostra as messages do child a cada ~2s; tool calls + args truncados / texto)
158
- pantheon_delegation_list()
159
- -> lista jobs da sessao, com [unread] para jobs terminados nao lidos
160
- -> jobs RUNNING mostram "last activity:" (ultima acao visivel do child)
161
- -> fail-open: sem suporte a messages, formato atual inalterado
162
- ```
163
-
164
- - **Read-only enforcement:** agentes read-only (apollo, gaia — `read_only_agents` no routing.yml) tem `edit`, `write`, `bash`, `task` NEGADOS dentro da sessao delegada (o guard de `tool.execute.before` THROW com mensagem acionavel). Use `read_only: true` no delegate para qualquer agente de investigacao.
165
- - **Depth-2 hard-enforced:** sessao read-only NAO cria subagentes (`task` bloqueado) — investigacao nunca vira arvore.
166
- - **Compactacao:** jobs em voo (running + unread terminal) sao injetados no contexto de compactacao via `experimental.session.compacting` — delegacao em andamento nao se perde ao compactar.
167
-
168
- ### DELEGATION_RULES (plugin path)
169
-
170
- 1. **NUNCA** polle `pantheon_delegation_list` para checar se um job terminou.
171
- 2. Voce NAO recebe notificacao injetada no chat — politica do usuario: ZERO `task-notification` no transcript. Visibilidade de conclusao: marcador `[unread]` em `pantheon_delegation_list`, `pantheon_delegation_read({id})`, toasts TUI (pantheon-hooks) e carry-forward de compactacao.
172
- 3. Use `pantheon_delegation_read({id})` APENAS para fan-in explicito / recuperacao de resultado sob demanda (bloqueia ate o fim).
173
- 4. `pantheon_delegation_list` e para diagnostico, nao para polling.
150
+ 1. Todo dispatch usa `task(background=true, subagent_type=..., prompt=...)`.
151
+ 2. Recolha com `task_status(task_id=..., wait=true)` — bloqueia ate o fim, sem polling.
152
+ 3. Use `task_status(wait=true)` apenas para fan-in explicito / recuperacao sob demanda.
174
153
 
175
154
  ### Background (sempre usar)
176
155
  - **Apollo, Hermes, Aphrodite, Demeter, Hephaestus, Prometheus**
@@ -301,32 +280,11 @@ vão a idle com todos incompletos. Guards (todos no `src/pantheon/todo-enforcer.
301
280
 
302
281
  ## Wave 4 (PR #46): Empty-Result Retry + /cost + Themis Tier
303
282
 
304
- ### Empty-Result Retry (dispatch-guard — MANUAL orchestration)
305
- OpenCode 1.18.x NAO permite interceptar a conclusao de `task()` via hooks,
306
- entao o `src/pantheon/dispatch-guard.ts` e uma lib pura usada POR VOCE na
307
- orquestracao (NAO esta wired no plugin):
308
-
309
- ```
310
- import { createDispatchGuard } from '.../src/pantheon/dispatch-guard.ts' # via code-mode ou subagente
311
-
312
- guard = createDispatchGuard({ retryOnEmpty: true, logger: { warn: console.warn } })
313
-
314
- # classificar resultado de task_status/wait:
315
- # 'content' → tem texto, pronto
316
- # 'empty-mode1' → SEM texto E SEM tokens (nada voltou) → RETRY 1x
317
- # 'empty-mode2' → SEM texto, MAS tokens (raciocinou, perdeu a parte de texto;
318
- # assinatura da falha themis Wave-2) → RETRY 1x
319
-
320
- out = await guard.maybeRetry(result, async () => { ...task() de novo... })
321
- # out.retried=true se redisparou; CAP DURO de 1 retry — nunca 2x.
322
- # Se o retry voltar vazio, `out.retried=false` — NAO tente de novo: escale.
323
- ```
324
- Regra: retry 1x APENAS em `empty-mode1`/`empty-mode2`. Resultado com conteudo
325
- nunca redispara. Apos 1 retry vazio → escalate (mesma regra do TODO Enforcer).
326
-
327
- > **Waves DEVEM usar `zeusDelegateWithRetry`** (`src/pantheon/zeus-delegate-with-retry.ts`) — encapsula `delegate → waitForTerminal → classify → retry 1x → escalate` com cap duro 1; se ainda vazio throw `ZeusEscalationError` (escale: tente outro agente, simplifique, manual).
328
- > Uso alto-nível: `import { zeusDelegateWithRetry } from './pantheon/zeus-delegate-with-retry.ts'` → `await zeusDelegateWithRetry({ board, client, sessionID, agent, prompt })`.
329
- > Uso baixo-nível: `createZeusRetryHelper().executeWithRetry(first, () => secondRead())` — classifica empty e decide retry/escalate (mesmo cap 1).
283
+ ### Empty-Result Retry (runtime — automatic)
284
+ Empty-response detection for native `task()` subagent calls is wired at runtime
285
+ in `task-result-guard.ts` (plugin `tool.execute.after` chain): an
286
+ empty/whitespace-only result is converted into an explicit error so a silent
287
+ child failure is never mistaken for success. No manual retry helper is required.
330
288
 
331
289
  ### /cost — pantheon_cost tool (WIRED no plugin)
332
290
  `pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY usando o backend
@@ -84,7 +84,9 @@ memory_store({
84
84
  agreements: ["point1", "point2"],
85
85
  divergences: [{"issue": "...", "resolution": "..."}],
86
86
  response_rate: "X of Y",
87
- themis_audit: "approved|issues"
87
+ themis_audit: "approved|issues",
88
+ precedent_used: false,
89
+ timestamp: "<ISO-8601>"
88
90
  },
89
91
  metadata: {
90
92
  type: "council_decision",
@@ -93,6 +95,7 @@ memory_store({
93
95
  }
94
96
  })
95
97
  ```
98
+ `value` is JSON-serialized before storing (the MCP `memory_store.value` argument is a string).
96
99
 
97
100
  ### Read Path (Precedent Fast-Path)
98
101
  Before dispatching a new council, Zeus runs:
@@ -103,8 +106,8 @@ memory_search(question, top_k=2, namespace="council_decisions")
103
106
  Result interpretation:
104
107
  | Score | Age | Action |
105
108
  |-------|-----|--------|
106
- | > 0.85 | < 30 days | Return precedent as fast-path answer. Skip council dispatch entirely. |
107
- | > 0.85 | >= 30 days | Return with warning "Reavaliar se contexto mudou" + proceed with council |
109
+ | > 0.85 | < 30 days | Return precedent verbatim (note "⚠️ Decisão de [data] — reavaliar se contexto mudou") as fast-path answer. Skip council dispatch entirely. |
110
+ | > 0.85 | >= 30 days | Return with warning "Reavaliar se contexto mudou — decisão tem mais de 30 dias" + proceed with council |
108
111
  | 0.5 - 0.85 | Any | Include as context for specialists but still dispatch council |
109
112
  | < 0.5 | Any | Ignore, proceed with fresh council |
110
113
 
@@ -15,7 +15,7 @@ You MUST self-monitor for these stall conditions:
15
15
  | Symptom | Detection Rule | Recovery Action |
16
16
  |---------|---------------|-----------------|
17
17
  | Silent loop | 3+ consecutive turns with no tool call AND no visible progress | Output `[STALL_DETECTED]` and re-read your task definition. If still stuck, escalate to user with: "I appear to be stuck on [task]. Options: (1) retry with different approach, (2) delegate to specialist, (3) simplify scope." |
18
- | Delegation black hole | Agent dispatched but no response after 2x the timeout from routing.yml | Log the hang, cancel via `cancel_task`, dispatch to fallback agent, report to user |
18
+ | Delegation black hole | Agent dispatched but no response after 2x the timeout from routing.yml | Log the hang, cancel via `cancel_task`, then follow the fallback chain in `## ⏱️ Timeout & Retry Enforcement` |
19
19
  | Circular delegation | Same specialist re-dispatched for same task 2+ times without progress | Break cycle: dispatch to different specialist OR escalate to user |
20
20
  | Idle after completion | All background tasks completed but no synthesis/next step for 2+ turns | Force synthesis: summarize all completed results and propose next action |
21
21
  | Context thrash | Re-reading same files repeatedly without new action | Stop re-reading. State: "Already have context on [file]. Proceeding with [action]." |
@@ -41,9 +41,7 @@ When a delegation fails (timeout, empty response, error):
41
41
 
42
42
  2. **Retry ONCE** with rephrased prompt — add: "Previous attempt failed with: [error]. Adjusted approach: [what changed]."
43
43
 
44
- 3. **If retry also fails** → DO NOT retry a third time blindly. Instead:
45
- - Dispatch to fallback agent (from routing.yml)
46
- - If no fallback, escalate to user with: "Task [X] failed after retry. Options: (a) simplify, (b) different agent, (c) manual intervention."
44
+ 3. **If retry also fails** → do NOT retry blindly; follow the fallback chain and escalation protocol in `## ⏱️ Timeout & Retry Enforcement`.
47
45
 
48
46
  ## Progress Checkpoint
49
47
 
@@ -85,10 +85,7 @@ sequenceDiagram
85
85
  ## Dispatch Sequence (9-Step Protocol)
86
86
 
87
87
  ### Step 0 — Precedent Fast-Path (Fase 1)
88
- Before any dispatch: `memory_search(query, top_k=2, namespace="council_decisions")`
89
- - Score > 0.85 AND age < 30 days → return precedent verbatim with note "⚠️ Decisão de [data] — reavaliar se contexto mudou". SKIP entire dispatch.
90
- - Score > 0.85 AND age > 30 days → return precedent with ⚠️ "Reavaliar se contexto mudou — decisão tem mais de 30 dias"
91
- - Else → proceed to Step 0b
88
+ Run the precedent read path defined in `## Memory Protocol > Council Decisions Namespace` (`memory_search(query, top_k=2, namespace="council_decisions")`). If no precedent applies, proceed to Step 0b.
92
89
 
93
90
  ### Step 0b — Apollo Pre-Scan (Fase 2, --research flag)
94
91
  If `/pantheon --research <question>`: dispatch @apollo with 30s timeout. Inject findings as `shared_context` into ALL specialist prompts. Skip if flag absent.
@@ -190,33 +187,11 @@ task(subagent_type: "themis", prompt: "Audit this council synthesis for fidelity
190
187
  - If issues → fix each issue before delivering to user
191
188
 
192
189
  ### Step 9 — Persist & Reconcile (Fase 1)
193
- ```
194
- memory_store({
195
- namespace: "council_decisions",
196
- key: "council:<yyyy-mm-dd>:<slug>",
197
- value: JSON.stringify({
198
- question, specialists, recommendation, confidence,
199
- agreements, divergences, precedent_used, timestamp
200
- }),
201
- metadata: {type: "council_decision", specialist_count: N}
202
- })
203
- board.markReconciled("<task-id>")
204
- ```
190
+ Persist the decision using the write path in `## Memory Protocol > Council Decisions Namespace`, then `board.markReconciled("<task-id>")`.
205
191
 
206
192
  ## Specialist Output Format (Fase 1 — machine-parseable structured fields)
207
193
 
208
- Specialists MUST return these structured fields in their response:
209
-
210
- ```
211
- ## specialist_response
212
- **position:** <clear one-sentence position>
213
- **reasoning:** <2-4 sentences>
214
- **trade_offs:** <what's gained vs lost>
215
- **risks:** <what could go wrong>
216
- **confidence:** High | Medium | Low
217
- **agreement_signals:** agree: @agent1, @agent2 on [issue] | disagree: @agent3 on [issue]
218
- **specific_claims:** <count of specific factual claims in response>
219
- ```
194
+ Specialists MUST return the structured `## specialist_response` format (position, reasoning, trade_offs, risks, confidence, agreement_signals, specific_claims) defined in `## Agent Return Format > Council Specialist Response Format`.
220
195
 
221
196
  ## Domain-to-Specialist Mapping
222
197
 
@@ -126,39 +126,14 @@ ANY NO → full task (IMPL artifact + Themis review mandatory)
126
126
  3. **No Themis dependency** — output doesn't feed into a phase that requires review
127
127
 
128
128
  ## Subtask Return Format
129
- Expect a `subtask_summary` response with:
130
- ```
131
- ## subtask_summary
132
- **files_changed:** [paths]
133
- **summary:** What was done
134
- **tests:** ✅ or N/A
135
- **status:** complete | partial | escalated
136
- ```
129
+ Return the standard `## subtask_summary` defined in `## Agent Return Format` (files_changed, summary, tests, coverage, tokens, status, blockers).
137
130
 
138
131
  ## Timeout Parcial (Partial Results)
139
132
 
140
- Timeout parcial is ONLY for read-only, independent agents:
141
- - ✅ @apollo — can return partial file list ("found 7 of 12 files before timeout")
142
- - ✅ @gaia — can return partial literature findings
143
-
144
- - ✅ @talos — can confirm progress if hotfix times out
145
- - ❌ Never for implementers or reviewers — must complete or fail
146
-
147
- When dispatching with partial-OK, set expectation:
148
- ```
149
- @apollo Search for auth files. Timeout parcial OK — return whatever you have.
150
- ```
133
+ Only agents marked ✅ in the Timeout Behavior table above may return partial results: @apollo (partial file list, e.g. "found 7 of 12 files before timeout"), @gaia (partial literature findings) and @talos (confirm progress if a hotfix times out). Never for implementers or reviewers — they must complete or fail. When dispatching with partial-OK, set the expectation: `@apollo Search for auth files. Timeout parcial OK — return whatever you have.`
151
134
 
152
135
  ---
153
136
 
154
137
  # 📊 TIMEOUT TRACKING
155
138
 
156
- Maintain awareness of in-flight delegations:
157
-
158
- | Agent | Timeout | Status | Partial OK? |
159
- |-------|---------|--------|-------------|
160
- | @apollo | 60s | ✅ complete | ✅ |
161
- | @hermes | 180s | ⏳ in progress | ❌ |
162
- | @themis | 120s | ⏳ in progress | ❌ |
163
-
164
- Log timeouts to `/memories/session/timeout-log.md` for later analysis.
139
+ Track in-flight delegations against the Timeout Behavior table above. Log timeouts to `/memories/session/timeout-log.md` for later analysis.
@@ -1323,7 +1323,7 @@ def build_rehydration_blocks(checkpoint: dict) -> list[str] | None:
1323
1323
  """Build deterministic rehydration blocks from a checkpoint dict.
1324
1324
 
1325
1325
  Pure string templating — no LLM, no embeddings. Mirrors the
1326
- ``<mission_context>`` convention of delegation-compaction.ts: only a
1326
+ ``<mission_context>`` convention of the compaction-context builder: only a
1327
1327
  non-done goal is re-emitted. Returns None when nothing critical remains.
1328
1328
  """
1329
1329
  if not isinstance(checkpoint, dict) or not checkpoint:
@@ -6,6 +6,10 @@ for semantic memory with hybrid search (vector cosine + FTS5 BM25).
6
6
 
7
7
  Total footprint: ~50MB (sqlite-vec + fastembed) vs ~1.4GB (old chromadb).
8
8
 
9
+ Both embedding backends are optional: if either fails to import the server
10
+ still starts and serves FTS5-only search (``PANTHEON_MEMORY_EMBED=off``
11
+ disables the vector pipeline explicitly).
12
+
9
13
  Tools:
10
14
  memory_store — Store with automatic embedding
11
15
  memory_search — Hybrid vector + FTS5 keyword search
@@ -30,9 +34,21 @@ from datetime import UTC, datetime
30
34
  from pathlib import Path
31
35
  from typing import Any
32
36
 
33
- import sqlite_vec
37
+ # Embedding backends are optional (issue #159): a failed fastembed/sqlite_vec
38
+ # install (network error, incompatible wheel) must not kill the server — it
39
+ # degrades to FTS-only search instead of crashing before the MCP handshake.
40
+ try:
41
+ import sqlite_vec
42
+ except ImportError: # pragma: no cover - degraded mode
43
+ sqlite_vec = None # type: ignore[assignment]
44
+
34
45
  from _pantheon_paths import pantheon_home
35
- from fastembed import TextEmbedding
46
+
47
+ try:
48
+ from fastembed import TextEmbedding
49
+ except ImportError: # pragma: no cover - degraded mode
50
+ TextEmbedding = None # type: ignore[assignment]
51
+
36
52
  from mcp.server.fastmcp import FastMCP
37
53
 
38
54
  try:
@@ -52,6 +68,12 @@ except ImportError: # pragma: no cover - deployed runtime may need path fix
52
68
  DB_PATH = pantheon_home() / "memory" / "memory.db"
53
69
  EMBED_CACHE = Path.home() / ".cache" / "fastembed"
54
70
 
71
+ # PANTHEON_MEMORY_EMBED=off disables the embedding/vector pipeline so the
72
+ # server serves FTS-only search (issue #159). Default "on" keeps the current
73
+ # hybrid vector + FTS behavior. The flag is also forced off automatically
74
+ # when fastembed failed to import.
75
+ _EMBED_DISABLED = frozenset({"off", "0", "false", "no"})
76
+
55
77
 
56
78
  _BYTE_UNIT = 1024
57
79
 
@@ -81,11 +103,6 @@ CREATE TABLE IF NOT EXISTS memories (
81
103
  CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_ns_key
82
104
  ON memories(namespace, key);
83
105
 
84
- CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
85
- id INTEGER PRIMARY KEY,
86
- embedding float[384]
87
- );
88
-
89
106
  CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
90
107
  namespace, key, value, metadata,
91
108
  content='memories', content_rowid='id',
@@ -113,6 +130,14 @@ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
113
130
  END;
114
131
  """
115
132
 
133
+ # Vector store requires the sqlite-vec extension; created only when available.
134
+ VEC_SCHEMA_SQL = """
135
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
136
+ id INTEGER PRIMARY KEY,
137
+ embedding float[384]
138
+ );
139
+ """
140
+
116
141
  # ── FastMCP App ───────────────────────────────────────────────────────────────
117
142
 
118
143
  mcp = FastMCP(
@@ -129,7 +154,8 @@ def _get_db() -> sqlite3.Connection:
129
154
  """Get or create the SQLite connection singleton.
130
155
 
131
156
  Creates DB directory, applies WAL/performance pragmas, registers
132
- the sqlite-vec extension, and runs schema init.
157
+ the sqlite-vec extension, and runs schema init. When sqlite-vec is
158
+ unavailable the vector table is skipped — search degrades to FTS-only.
133
159
  """
134
160
  DB_PATH.parent.mkdir(parents=True, exist_ok=True)
135
161
  conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
@@ -137,9 +163,10 @@ def _get_db() -> sqlite3.Connection:
137
163
  conn.execute("PRAGMA journal_mode=WAL")
138
164
  conn.execute("PRAGMA busy_timeout=5000")
139
165
  conn.execute("PRAGMA foreign_keys=ON")
140
- # Register sqlite-vec extension
141
-
142
- sqlite_vec.load(conn)
166
+ # Register sqlite-vec extension (optional; enables vector search)
167
+ if sqlite_vec is not None:
168
+ sqlite_vec.load(conn)
169
+ conn.executescript(VEC_SCHEMA_SQL)
143
170
  conn.executescript(SCHEMA_SQL)
144
171
  if _codemap is not None:
145
172
  with contextlib.suppress(Exception):
@@ -155,13 +182,30 @@ def _get_conn() -> sqlite3.Connection:
155
182
  # ── Embedding ─────────────────────────────────────────────────────────────────
156
183
 
157
184
 
185
+ def _embeddings_enabled() -> bool:
186
+ """Whether the embedding/vector pipeline is active.
187
+
188
+ Controlled by ``PANTHEON_MEMORY_EMBED`` (``on`` by default; ``off``,
189
+ ``0``, ``false`` or ``no`` disable it). Automatically disabled when
190
+ ``fastembed`` failed to import, so the server degrades to FTS-only
191
+ search instead of crashing at startup (issue #159).
192
+ """
193
+ if TextEmbedding is None:
194
+ return False
195
+ flag = os.environ.get("PANTHEON_MEMORY_EMBED", "on").strip().lower()
196
+ return flag not in _EMBED_DISABLED
197
+
198
+
158
199
  @functools.cache
159
- def _get_embedder() -> TextEmbedding:
200
+ def _get_embedder() -> TextEmbedding | None:
160
201
  """Lazy-load fastembed model (auto-downloads on first call, ~30MB).
161
202
 
162
203
  Uses BAAI/bge-small-en-v1.5 (384-dim, CPU, ONNX) — no PyTorch needed.
163
- Model is cached in ~/.cache/fastembed/.
204
+ Model is cached in ~/.cache/fastembed/. Returns None when fastembed is
205
+ unavailable.
164
206
  """
207
+ if TextEmbedding is None:
208
+ return None
165
209
  os.environ.setdefault("TQDM_DISABLE", "1")
166
210
  return TextEmbedding(
167
211
  model_name="BAAI/bge-small-en-v1.5",
@@ -169,9 +213,18 @@ def _get_embedder() -> TextEmbedding:
169
213
  )
170
214
 
171
215
 
172
- def _embed(text: str) -> list[float]:
173
- """Generate a 384-dim embedding vector for the given text."""
174
- return next(iter(_get_embedder().embed(text))).tolist()
216
+ def _embed(text: str) -> list[float] | None:
217
+ """Generate a 384-dim embedding vector for the given text.
218
+
219
+ Returns None when embeddings are disabled (``PANTHEON_MEMORY_EMBED=off``
220
+ or fastembed missing) — callers fall back to FTS-only search.
221
+ """
222
+ if not _embeddings_enabled():
223
+ return None
224
+ embedder = _get_embedder()
225
+ if embedder is None:
226
+ return None
227
+ return next(iter(embedder.embed(text))).tolist()
175
228
 
176
229
 
177
230
  # ── Helpers ───────────────────────────────────────────────────────────────────
@@ -341,14 +394,14 @@ def memory_store(
341
394
  )
342
395
  entry_id = cur.lastrowid
343
396
 
344
- # Generate embedding
397
+ # Generate embedding (None when disabled → FTS-only mode)
345
398
  embedding = _embed(value)
346
- embedding_json = json.dumps(embedding)
347
-
348
- db.execute(
349
- "INSERT INTO vec_memories(id, embedding) VALUES (?, ?)",
350
- [entry_id, embedding_json],
351
- )
399
+ if embedding is not None and sqlite_vec is not None:
400
+ embedding_json = json.dumps(embedding)
401
+ db.execute(
402
+ "INSERT INTO vec_memories(id, embedding) VALUES (?, ?)",
403
+ [entry_id, embedding_json],
404
+ )
352
405
  db.commit()
353
406
  except sqlite3.IntegrityError as e:
354
407
  db.rollback()
@@ -406,8 +459,8 @@ def memory_search( # noqa: C901, PLR0912
406
459
  vec_ids: list[int] = []
407
460
  fts_ids: list[int] = []
408
461
 
409
- # 2. Vector search
410
- if query_vec is not None:
462
+ # 2. Vector search (skipped when embeddings or sqlite-vec are unavailable)
463
+ if query_vec is not None and sqlite_vec is not None:
411
464
  nvec = top_k * 2
412
465
  try:
413
466
  if namespace:
@@ -564,8 +617,9 @@ def memory_forget(
564
617
  db = _get_conn()
565
618
  try:
566
619
  if col == "id":
567
- # Vec table cascade
568
- db.execute("DELETE FROM vec_memories WHERE id = ?", [id])
620
+ # Vec table cascade (absent when sqlite-vec is unavailable)
621
+ if sqlite_vec is not None:
622
+ db.execute("DELETE FROM vec_memories WHERE id = ?", [id])
569
623
  cur = db.execute("DELETE FROM memories WHERE id = ?", [id])
570
624
  else:
571
625
  cur = db.execute(sql, params)
@@ -2,7 +2,7 @@
2
2
  * Background Job Board — state machine for tracking background agent jobs.
3
3
  *
4
4
  * Tracks jobs through a state machine (running → completed/error/cancelled → reconciled),
5
- * supports alias generation per session, persistence via adapter, auto-wake signal files,
5
+ * supports alias generation per session, persistence via adapter, wake signal files,
6
6
  * and terminal state listeners (persist-before-notify write-ahead-log pattern).
7
7
  *
8
8
  * Pure TypeScript — zero external dependencies beyond Node.js builtins (fs/path).
@@ -123,7 +123,7 @@ export interface BoardOptions {
123
123
  /** Maximum completed jobs kept per agent for reuse (default: 3). */
124
124
  maxReusablePerAgent?: number
125
125
  /**
126
- * Directory for auto-wake signal files.
126
+ * Directory for wake signal files.
127
127
  * When set, a `.signal.json` file is atomically written on every terminal transition.
128
128
  * When null or undefined, no signal files are written.
129
129
  */
@@ -175,7 +175,7 @@ function getAgentPrefix(agent: string): string {
175
175
  * - Strict state machine with validated transitions
176
176
  * - Per-session, per-agent alias generation (apo-1, her-2, …)
177
177
  * - Optional persistence via injected PersistenceAdapter
178
- * - Optional auto-wake signal files on terminal transitions
178
+ * - Optional wake signal files on terminal transitions
179
179
  * - Terminal state listeners with persist-before-notify guarantee
180
180
  * - expire/prune helpers for session lifecycle management
181
181
  */
@@ -652,7 +652,7 @@ export class BackgroundJobBoard {
652
652
  }
653
653
 
654
654
  /**
655
- * Delete the auto-wake signal file for an alias (`<alias>.signal.json`).
655
+ * Delete the wake signal file for an alias (`<alias>.signal.json`).
656
656
  * No-op (false) when no signalDir is configured or the file is absent.
657
657
  * The delegate manager calls this on the spot right after auto-reconcile.
658
658
  */
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Compaction Context (Phase 4 + release-134 Phase 2) — build the context
3
- * blocks injected by the `experimental.session.compacting` hook so a
4
- * compacted session does not lose its working state: in-flight background
5
- * delegations, active goals and pending todos.
2
+ * Compaction Context (Phase 4 + release-134 Phase 2, slimmed in Fase 2 of the
3
+ * delegate removal) — build the context blocks injected by the
4
+ * `experimental.session.compacting` hook so a compacted session does not lose
5
+ * its working state: active goals and pending todos.
6
6
  *
7
7
  * Returns a Promise<string[]> (empty when there is nothing to preserve).
8
8
  * Section order is stable:
@@ -17,23 +17,10 @@
17
17
  * `opts.todos`: " [id] description — status". Omitted when the todo
18
18
  * source is absent, disabled, unscoped (no sessionID), fails, or has
19
19
  * no pending todos.
20
- * - "Background Delegations (running):" — every running job launched by
21
- * the session (active work is never capped): alias, agent, started ISO,
22
- * truncated prompt.
23
- * - "Background Delegations (finished, unread):" — terminal-unreconciled
24
- * jobs, newest first, capped at `maxItems` (default 10, matching
25
- * routing.yml background_delegation.max_compaction_items).
26
20
  *
27
- * Reconciled jobs are excluded — they have been read already.
28
- *
29
- * The block style mirrors board.formatForPrompt (" [alias] description —
30
- * STATUS") so compaction context reads consistently with the system prompt,
31
- * but with the extra id/agent/started detail the compaction hook needs.
32
- *
33
- * @module delegation-compaction
21
+ * @module compaction-context
34
22
  */
35
23
 
36
- import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
37
24
  import type { Goal } from './goal-store.ts'
38
25
  import { createPantheonLogger } from './logger.ts'
39
26
  import type { TodoLike } from './todo-enforcer.ts'
@@ -72,56 +59,14 @@ export interface CompactionTodoSource {
72
59
  }
73
60
 
74
61
  export interface CompactionContextOptions {
75
- /** Scope to jobs launched by this session; omit for all jobs. */
62
+ /** Scope to this session; omit to skip the per-session sections. */
76
63
  sessionID?: string
77
- /** Cap on unread terminal jobs kept (default 10). */
78
- maxItems?: number
79
64
  /** Goal source for <mission_context> (optional — omitted when absent). */
80
65
  goals?: CompactionGoalSource
81
66
  /** Todo source for <todo_context> (optional — omitted when absent). */
82
67
  todos?: CompactionTodoSource
83
68
  }
84
69
 
85
- /** Prompt truncation length for a context line. */
86
- const PROMPT_MAX_LEN = 100
87
-
88
- // ─── Formatting ────────────────────────────────────────────────────────
89
-
90
- /** Collapse whitespace and truncate a prompt to PROMPT_MAX_LEN chars. */
91
- function truncatePrompt(text: string): string {
92
- const flat = text.trim().replace(/\s+/g, ' ')
93
- if (flat.length <= PROMPT_MAX_LEN) return flat
94
- return `${flat.slice(0, PROMPT_MAX_LEN)}…`
95
- }
96
-
97
- /** One-line terminal status label, mirroring formatForPrompt. */
98
- function terminalLabel(state: BackgroundJobRecord['state']): string {
99
- switch (state) {
100
- case 'completed':
101
- return 'OK'
102
- case 'error':
103
- return 'ERR'
104
- case 'cancelled':
105
- return 'CAN'
106
- default:
107
- return state.toUpperCase()
108
- }
109
- }
110
-
111
- function runningLine(job: BackgroundJobRecord): string {
112
- return (
113
- ` [${job.alias}] ${job.agent} — ${truncatePrompt(job.description)} — ` +
114
- `RUNNING since ${new Date(job.launchedAt).toISOString()}`
115
- )
116
- }
117
-
118
- function unreadTerminalLine(job: BackgroundJobRecord): string {
119
- return (
120
- ` [${job.alias}] ${job.agent} — ${truncatePrompt(job.description)} — ` +
121
- `${terminalLabel(job.state)} ${new Date(job.updatedAt).toISOString()} [unread]`
122
- )
123
- }
124
-
125
70
  // ─── Sections ──────────────────────────────────────────────────────────
126
71
 
127
72
  /**
@@ -180,58 +125,23 @@ async function todoContextBlock(opts: CompactionContextOptions): Promise<string
180
125
  return `<todo_context>\n${lines.join('\n')}`
181
126
  }
182
127
 
183
- /**
184
- * `<delegation_context>` — the existing delegation blocks, byte-for-byte
185
- * unchanged from Phase 4 (running, then unread terminal ≤ maxItems).
186
- */
187
- function delegationBlocks(board: BackgroundJobBoard, opts: CompactionContextOptions): string[] {
188
- const maxItems = opts.maxItems ?? 10
189
- const jobs = opts.sessionID !== undefined ? board.list(opts.sessionID) : board.list()
190
-
191
- const running = jobs
192
- .filter((j) => j.state === 'running')
193
- .sort((a, b) => a.launchedAt - b.launchedAt)
194
- const unreadTerminal = jobs
195
- .filter((j) => j.terminalUnreconciled)
196
- .sort((a, b) => b.updatedAt - a.updatedAt)
197
- .slice(0, maxItems)
198
-
199
- const blocks: string[] = []
200
- if (running.length > 0) {
201
- blocks.push(`Background Delegations (running):\n${running.map(runningLine).join('\n')}`)
202
- }
203
- if (unreadTerminal.length > 0) {
204
- blocks.push(
205
- `Background Delegations (finished, unread):\n${unreadTerminal
206
- .map(unreadTerminalLine)
207
- .join('\n')}`,
208
- )
209
- }
210
- return blocks
211
- }
212
-
213
128
  // ─── Build ─────────────────────────────────────────────────────────────
214
129
 
215
130
  /**
216
131
  * Build the compaction context blocks for a session: preservation
217
- * directive, active goals, pending todos, and in-flight background
218
- * delegations. Returns an empty array when there is nothing to preserve
219
- * (caller skips the hook injection entirely).
132
+ * directive, active goals, and pending todos. Returns an empty array when
133
+ * there is nothing to preserve (caller skips the hook injection entirely).
220
134
  */
221
135
  export async function buildCompactionContext(
222
- board: BackgroundJobBoard,
223
136
  opts: CompactionContextOptions = {},
224
137
  ): Promise<string[]> {
225
138
  const [mission, todo] = await Promise.all([missionContextBlock(opts), todoContextBlock(opts)])
226
- const delegation = delegationBlocks(board, opts)
227
139
 
228
140
  // Totally-empty state → nothing to preserve; a lone directive is noise.
229
- if (mission === undefined && todo === undefined && delegation.length === 0) return []
141
+ if (mission === undefined && todo === undefined) return []
230
142
 
231
- const blocks: string[] = []
232
- blocks.push(`<pantheon-context directive>\n${PANTHEON_COMPACTION_DIRECTIVE}`)
143
+ const blocks: string[] = [`<pantheon-context directive>\n${PANTHEON_COMPACTION_DIRECTIVE}`]
233
144
  if (mission !== undefined) blocks.push(mission)
234
145
  if (todo !== undefined) blocks.push(todo)
235
- blocks.push(...delegation)
236
146
  return blocks
237
147
  }
@@ -21,9 +21,8 @@ import { homedir } from 'node:os'
21
21
  import { delimiter, dirname, join } from 'node:path'
22
22
  import { fileURLToPath } from 'node:url'
23
23
  import { z } from 'zod'
24
-
25
- import type { ToolContextLike } from './delegation.ts'
26
24
  import type { NativeTaskStatus } from './native-task-status.ts'
25
+ import type { ToolContextLike } from './tool-context.ts'
27
26
 
28
27
  const __filename = fileURLToPath(import.meta.url)
29
28
  const __dirname = dirname(__filename)