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.
- package/AGENTS.md +14 -63
- package/CHANGELOG.md +326 -0
- package/README.md +77 -29
- package/README.pt-BR.md +5 -6
- package/commands/pantheon.md +1 -35
- package/package.json +2 -3
- package/plugin.json +1 -1
- package/pyproject.toml +3 -5
- package/scripts/doctor.mjs +260 -5
- package/scripts/install/plugin.mjs +9 -12
- package/scripts/mcp_persistence_server.py +1 -1
- package/scripts/sync-tui.mjs +8 -11
- package/src/agents/apollo.md +1 -0
- package/src/agents/gaia.md +1 -0
- package/src/agents/mnemosyne.md +11 -1
- package/src/agents/zeus.md +10 -52
- package/src/instructions/memory-protocol.instructions.md +6 -3
- package/src/instructions/zeus-anti-stall.instructions.md +2 -4
- package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
- package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
- package/src/mcp/mcp_persistence_server.py +1 -1
- package/src/mcp/memory_mcp_server.py +81 -27
- package/src/pantheon/background-job-board.ts +4 -4
- package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
- package/src/pantheon/cost-command.ts +1 -2
- package/src/pantheon/delegation-enforce.ts +59 -14
- package/src/pantheon/goal-loop.ts +1 -1
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/idle-continuation.ts +4 -6
- package/src/pantheon/model-command.ts +1 -2
- package/src/pantheon/native-task-status.ts +2 -2
- package/src/pantheon/presets.d.mts +0 -20
- package/src/pantheon/presets.mjs +0 -64
- package/src/pantheon/session-guard.ts +2 -2
- package/src/pantheon/session-id.ts +30 -0
- package/src/pantheon/task-result-guard.ts +2 -3
- package/src/pantheon/todo-enforcer.ts +3 -3
- package/src/pantheon/tool-context.ts +23 -0
- package/src/pantheon/v2-bridge.ts +1 -4
- package/src/plugin-v2.ts +8 -21
- package/src/plugin.ts +61 -245
- package/src/plugins/pantheon-hooks.ts +2 -2
- package/src/plugins/tui/dist/tui.d.ts +229 -103
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +495 -333
- package/src/plugins/tui/package-lock.json +5 -2
- package/src/plugins/tui/package.json +5 -2
- package/src/plugins/tui/src/index.tsx +710 -409
- package/src/routing.yml +0 -16
- package/src/skills/context-compression/SKILL.md +1 -1
- package/scripts/generate-preset-docs.mjs +0 -300
- package/scripts/generate-prompts.sh +0 -222
- package/scripts/hash_verify.py +0 -192
- package/scripts/install/component.mjs +0 -24
- package/scripts/install/migrate-config.mjs +0 -302
- package/scripts/test-context-compression.sh +0 -169
- package/src/mcp/toon_codec.py +0 -491
- package/src/pantheon/auto-wake.ts +0 -85
- package/src/pantheon/chat-reminders.ts +0 -9
- package/src/pantheon/comment-checker.ts +0 -48
- package/src/pantheon/compaction-assert.ts +0 -160
- package/src/pantheon/compaction-handoff.ts +0 -304
- package/src/pantheon/cost-tracker.ts +0 -108
- package/src/pantheon/delegate-manager.ts +0 -766
- package/src/pantheon/delegation-classifier.ts +0 -307
- package/src/pantheon/delegation-finalize.ts +0 -406
- package/src/pantheon/delegation-notify.ts +0 -212
- package/src/pantheon/delegation-toast.ts +0 -73
- package/src/pantheon/delegation.ts +0 -1337
- package/src/pantheon/dispatch-guard.ts +0 -109
- package/src/pantheon/native-probe.ts +0 -234
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/preemptive-compact.ts +0 -76
- package/src/pantheon/provider-capabilities.ts +0 -221
- package/src/pantheon/provider-stream-tracker.ts +0 -182
- package/src/pantheon/retry-policy.ts +0 -212
- package/src/pantheon/step-cap.ts +0 -110
- package/src/pantheon/token-opt.ts +0 -689
- package/src/pantheon/toon-codec.ts +0 -481
- package/src/pantheon/v2-events.ts +0 -148
- package/src/pantheon/v2-hooks.ts +0 -287
- package/src/pantheon/v2-tool-definitions.ts +0 -331
- package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
- package/src/plugins/tui/dist/tui.tsx +0 -2853
package/src/agents/zeus.md
CHANGED
|
@@ -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
|
-
###
|
|
148
|
+
### DELEGATION_RULES (native task API)
|
|
148
149
|
|
|
149
|
-
|
|
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 (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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`,
|
|
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** →
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3
|
-
*
|
|
4
|
-
* compacted session does not lose
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
218
|
-
*
|
|
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
|
|
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)
|