jorgex-stack 1.9.57 → 1.9.58

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.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ codexAdapter,
4
+ verifyOfficialSetup
5
+ } from "./chunk-GOLJ5PA3.js";
6
+ import "./chunk-KB5IQEKL.js";
7
+ import "./chunk-NQZVJFV2.js";
8
+ export {
9
+ codexAdapter,
10
+ verifyOfficialSetup
11
+ };
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ checkOpencodeDuplicates,
4
+ checkOpencodeOfficialMcp,
5
+ checkOpencodeOfficialStatusline,
6
+ collectUnverifiableOpencodeConfigs,
7
+ inspectOpencodeEngramPlugin,
8
+ inspectOpencodePluginFile,
9
+ isOfficialOpencodePluginContent,
10
+ opencodeAdapter,
11
+ readOpencodeConfigFile,
12
+ shouldRetireLegacyEngram,
13
+ transferEngramOwnership,
14
+ verifyOfficialSetup
15
+ } from "./chunk-M6QX2F3A.js";
16
+ import "./chunk-KB5IQEKL.js";
17
+ import "./chunk-NQZVJFV2.js";
18
+ export {
19
+ checkOpencodeDuplicates,
20
+ checkOpencodeOfficialMcp,
21
+ checkOpencodeOfficialStatusline,
22
+ collectUnverifiableOpencodeConfigs,
23
+ inspectOpencodeEngramPlugin,
24
+ inspectOpencodePluginFile,
25
+ isOfficialOpencodePluginContent,
26
+ opencodeAdapter,
27
+ readOpencodeConfigFile,
28
+ shouldRetireLegacyEngram,
29
+ transferEngramOwnership,
30
+ verifyOfficialSetup
31
+ };
@@ -3,10 +3,12 @@ import {
3
3
  QUALITY_PROFILES,
4
4
  canonicalJson,
5
5
  evaluateQualityPolicy,
6
- samePath,
7
6
  sha256,
8
7
  validateQualityReceipt
9
- } from "./chunk-IO2FFZOH.js";
8
+ } from "./chunk-N5IC2JBJ.js";
9
+ import {
10
+ samePath
11
+ } from "./chunk-NQZVJFV2.js";
10
12
 
11
13
  // src/lib/quality-verifier.ts
12
14
  import { createHash } from "crypto";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.9.57",
3
+ "version": "1.9.58",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI, OpenCode y Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jorgex/opencode-plugins",
3
3
  "version": "1.0.0",
4
- "description": "OpenCode plugins del stack: engram (memoria persistente), hooks (puente de scripts por evento/tool) y worktree (gestión de git worktrees)",
4
+ "description": "OpenCode plugins del stack: hooks (puente de scripts por evento/tool) y worktree (gestión de git worktrees). Engram lo provee `engram setup opencode` (plugin oficial).",
5
5
  "type": "module",
6
6
  "main": "hooks.ts",
7
7
  "dependencies": {
package/upstreams.json CHANGED
@@ -9,48 +9,55 @@
9
9
  }
10
10
  },
11
11
  "complements": {
12
- "$comment": "Complementos oficiales externos (ejecutan código en el runtime): pin exacto al instalar, nunca latest ni rangos flotantes. `reviewed` = última revisión aceptada; `update --check` es discovery-only y avisa si el upstream se movió; la adopción es re-pin deliberado con verificación, nunca auto-update.",
12
+ "$comment": "Complementos oficiales externos (ejecutan código en el runtime): `strategy` distingue `exact` (pin obligatorio, ausencia es error; al instalar se usa el pin, nunca latest ni rangos) de `provider-managed` (el provider gestiona la versión: rolling/null/main/latest aceptados sin warning por ausencia de pin; update --check informa la versión observada/drift sin bloquear). `reviewed` = última revisión aceptada; `update --check` es discovery-only, nunca auto-update; la adopción exact es re-pin deliberado con verificación.",
13
13
  "gentle-engram": {
14
14
  "source": "npm:gentle-engram",
15
- "version": "0.1.12",
16
- "reviewed": "2026-09-17",
15
+ "version": "0.1.14",
16
+ "strategy": "provider-managed",
17
+ "reviewed": "2026-09-20",
17
18
  "status": "pendiente-adopcion",
18
- "note": "última publicada en npm (dist-tag latest); el setup oficial en main referencia 0.1.13 aún no publicada — re-pinear al salir. Incluye auto-cierre en session_shutdown. Issue: #155"
19
+ "note": "publicado/latest en npm y usado por el setup oficial en main: provider gestiona la versión. Incluye auto-cierre en session_shutdown. Issue: #155"
19
20
  },
20
21
  "pi-mcp-adapter": {
21
22
  "source": "npm:pi-mcp-adapter",
22
23
  "version": null,
23
- "reviewed": "2026-09-17",
24
+ "strategy": "provider-managed",
25
+ "reviewed": "2026-09-20",
24
26
  "status": "pendiente-pin",
25
- "note": "upstream lo referencia sin versión (setup.go main): exigir pin exacto antes de adoptar. Issue: #155"
27
+ "note": "provider-managed: upstream lo referencia sin versión (setup.go main); rolling aceptado, sin warning por ausencia de pin. Último observado latest 2.34.0 como referencia informativa, sin fijarlo. Issue: #155"
26
28
  },
27
29
  "engram-claude-plugin": {
28
30
  "source": "github:Gentleman-Programming/engram#plugin/claude-code",
29
- "version": "1.20.0",
30
- "reviewed": "2026-09-17",
31
- "note": "autocierre vía hook Stop (v1.20.0) / SessionEnd (main). Stack convive (detección + allowlists duales); falta instalación/verificación por Stack. Issue: #155"
31
+ "version": "2.0.0",
32
+ "strategy": "provider-managed",
33
+ "reviewed": "2026-09-20",
34
+ "note": "provider setup según upstream (2.0.0 solo referencia observada, no pin de instalación); autocierre vía hook Stop (v2.0.0) / SessionEnd (main). Stack convive (detección + allowlists duales); falta instalación/verificación por Stack. Issue: #155"
32
35
  },
33
36
  "engram-codex-plugin": {
34
37
  "source": "github:Gentleman-Programming/engram#plugin/codex",
35
- "version": "1.20.0",
36
- "reviewed": "2026-09-17",
37
- "note": "autocierre vía hook Stop. Stack convive; falta instalación/verificación por Stack. Issue: #155"
38
+ "version": "main",
39
+ "strategy": "provider-managed",
40
+ "reviewed": "2026-09-20",
41
+ "note": "main rolling aprobado (provider-managed): autocierre vía hook Stop. Stack convive; falta instalación/verificación por Stack. Issue: #155"
38
42
  },
39
43
  "engram-opencode-plugin": {
40
44
  "source": "github:Gentleman-Programming/engram#plugin/opencode",
41
- "version": "1.20.0",
42
- "reviewed": "2026-09-17",
43
- "note": "auto-cierre (endSessionInEngram) solo en main; la copia vendorizada de Stack no lo tiene — migrar con transferencia de ownership de plugins/engram.ts. Issue: #155"
45
+ "version": "2.0.0",
46
+ "strategy": "provider-managed",
47
+ "reviewed": "2026-09-20",
48
+ "note": "provider setup según upstream (2.0.0 solo referencia observada, no pin de instalación); auto-cierre (endSessionInEngram) solo en main; la copia vendorizada de Stack no lo tiene — migrar con transferencia de ownership de plugins/engram.ts. Issue: #155"
44
49
  },
45
50
  "chrome-devtools-mcp": {
46
51
  "source": "npm:chrome-devtools-mcp",
47
52
  "version": "1.6.0",
53
+ "strategy": "exact",
48
54
  "reviewed": "2026-09-17",
49
55
  "note": "opt-in, default-off; flags --isolated --redact-network-headers --no-performance-crux --no-usage-statistics"
50
56
  },
51
57
  "playwright-cli": {
52
58
  "source": "npm:@playwright/cli",
53
59
  "version": "0.1.18",
60
+ "strategy": "exact",
54
61
  "reviewed": "2026-09-17",
55
62
  "note": "CLI global compartido por la máquina; camino recomendado opt-in"
56
63
  }
@@ -1,444 +0,0 @@
1
- /**
2
- * Engram — OpenCode plugin adapter
3
- *
4
- * Thin layer that connects OpenCode's event system to the Engram Go binary.
5
- * The Go binary runs as a local HTTP server and handles all persistence.
6
- *
7
- * Flow:
8
- * OpenCode events → this plugin → HTTP calls → engram serve → SQLite
9
- *
10
- * Session resilience:
11
- * Uses `ensureSession()` before any DB write. This means sessions are
12
- * created on-demand — even if the plugin was loaded after the session
13
- * started (restart, reconnect, etc.). The session ID comes from OpenCode's
14
- * hooks (input.sessionID) rather than relying on a session.created event.
15
- */
16
-
17
- import type { Plugin } from "@opencode-ai/plugin"
18
-
19
- declare const Bun: {
20
- which?: (bin: string) => string | null
21
- spawnSync: (args: string[], options?: Record<string, unknown>) => { exitCode: number; stdout?: { toString(): string } | string }
22
- spawn: (args: string[], options?: Record<string, unknown>) => unknown
23
- file: (path: string) => { exists: () => Promise<boolean> }
24
- }
25
-
26
- // ─── Configuration ───────────────────────────────────────────────────────────
27
-
28
- const ENGRAM_PORT = parseInt(process.env.ENGRAM_PORT ?? "7437")
29
- const ENGRAM_URL = `http://127.0.0.1:${ENGRAM_PORT}`
30
- // "{{ENGRAM_BIN}}" lo resuelve el instalador con el binario detectado (D7).
31
- const ENGRAM_BIN = "{{ENGRAM_BIN}}"
32
-
33
- export function resolveEngramBin(installerBin = ENGRAM_BIN): string {
34
- const envBin = process.env.ENGRAM_BIN
35
- if (envBin) return envBin
36
-
37
- const bun = globalThis as typeof globalThis & { Bun?: { which?: (bin: string) => string | null } }
38
- const bunBin = bun.Bun?.which?.("engram")
39
- if (bunBin) return bunBin
40
-
41
- return installerBin !== "{{ENGRAM_BIN}}" ? installerBin : "engram"
42
- }
43
-
44
- // Engram's own MCP tools — don't count these as "tool calls" for session stats
45
- const ENGRAM_TOOLS = new Set([
46
- "mem_search",
47
- "mem_save",
48
- "mem_update",
49
- "mem_delete",
50
- "mem_suggest_topic_key",
51
- "mem_save_prompt",
52
- "mem_session_summary",
53
- "mem_context",
54
- "mem_stats",
55
- "mem_timeline",
56
- "mem_get_observation",
57
- "mem_session_start",
58
- "mem_session_end",
59
- ])
60
-
61
- // ─── Memory Instructions ─────────────────────────────────────────────────────
62
- // These get injected into the agent's context so it knows to call mem_save.
63
- // El placeholder ENGRAM_PROTOCOL lo resuelve el instalador con el contenido
64
- // del protocolo canónico (stack/system-prompt/engram-protocol.md) — única
65
- // fuente, alineada automáticamente con lo que reciben Claude Code y Codex.
66
-
67
- const MEMORY_INSTRUCTIONS = "{{ENGRAM_PROTOCOL}}"
68
-
69
- // ─── HTTP Client ─────────────────────────────────────────────────────────────
70
-
71
- async function engramFetch(
72
- path: string,
73
- opts: { method?: string; body?: any } = {}
74
- ): Promise<any> {
75
- try {
76
- const res = await fetch(`${ENGRAM_URL}${path}`, {
77
- method: opts.method ?? "GET",
78
- headers: opts.body ? { "Content-Type": "application/json" } : undefined,
79
- body: opts.body ? JSON.stringify(opts.body) : undefined,
80
- })
81
- if (!res.ok) return null
82
- return await res.json()
83
- } catch {
84
- // Engram server not running — silently fail
85
- return null
86
- }
87
- }
88
-
89
- async function isEngramRunning(): Promise<boolean> {
90
- try {
91
- const res = await fetch(`${ENGRAM_URL}/health`, {
92
- signal: AbortSignal.timeout(500),
93
- })
94
- return res.ok
95
- } catch {
96
- return false
97
- }
98
- }
99
-
100
- // ─── Helpers ─────────────────────────────────────────────────────────────────
101
-
102
- function extractProjectName(directory: string): string {
103
- // Try git remote origin URL
104
- try {
105
- const result = Bun.spawnSync(["git", "-C", directory, "remote", "get-url", "origin"], { stdout: "pipe", stderr: "ignore", stdin: "ignore" })
106
- if (result.exitCode === 0) {
107
- const url = result.stdout?.toString().trim()
108
- if (url) {
109
- const name = url.replace(/\.git$/, "").split(/[/:]/).pop()
110
- if (name) return name
111
- }
112
- }
113
- } catch {}
114
-
115
- // Fallback: git root directory name (works in worktrees)
116
- try {
117
- const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"], { stdout: "pipe", stderr: "ignore", stdin: "ignore" })
118
- if (result.exitCode === 0) {
119
- const root = result.stdout?.toString().trim()
120
- if (root) return root.split(/[\\/]/).pop() ?? "unknown"
121
- }
122
- } catch {}
123
-
124
- // Final fallback: cwd basename
125
- return directory.split(/[\\/]/).pop() ?? "unknown"
126
- }
127
-
128
- function truncate(str: string, max: number): string {
129
- if (!str) return ""
130
- return str.length > max ? str.slice(0, max) + "..." : str
131
- }
132
-
133
- /**
134
- * Strip <private>...</private> tags before sending to engram.
135
- * Double safety: the Go binary also strips, but we strip here too
136
- * so sensitive data never even hits the wire.
137
- */
138
- function stripPrivateTags(str: string): string {
139
- if (!str) return ""
140
- return str.replace(/<private>[\s\S]*?<\/private>/gi, "[REDACTED]").trim()
141
- }
142
-
143
- /**
144
- * Log errors and warnings to OpenCode's TUI log.
145
- * Errors in hooks must not propagate to the plugin surface — they are silently
146
- * swallowed here so the TUI does not crash. The error is visible only in OpenCode's
147
- * log stream (visible to the user via `opencode logs`).
148
- */
149
- async function logToOpenCode(client: unknown, level: string, message: string, extra?: unknown): Promise<void> {
150
- if (typeof client !== "object" || client === null) return;
151
- const app = (client as { app?: unknown }).app;
152
- if (typeof app !== "object" || app === null) return;
153
- const log = (app as { log?: unknown }).log;
154
- if (typeof log !== "function") return;
155
- const safeExtra = extra instanceof Error ? { message: extra.message, stack: extra.stack } : extra;
156
- try {
157
- await log.call(app, {
158
- body: {
159
- service: "engram",
160
- level,
161
- message,
162
- extra: safeExtra,
163
- },
164
- });
165
- } catch {
166
- // Logging must never break the plugin path.
167
- }
168
- }
169
-
170
- // ─── Plugin Export ───────────────────────────────────────────────────────────
171
-
172
- export const Engram: Plugin = async (ctx) => {
173
- const oldProject = ctx.directory.split(/[\\/]/).pop() ?? "unknown"
174
- const project = extractProjectName(ctx.directory)
175
- const client = (ctx as { client?: unknown }).client
176
- const engramBin = resolveEngramBin()
177
-
178
- // Track tool counts per session (in-memory only, not critical)
179
- const toolCounts = new Map<string, number>()
180
-
181
- // Track which sessions we've already ensured exist in engram
182
- const knownSessions = new Set<string>()
183
-
184
- // Track sub-agent session IDs so we can suppress their tool-hook registrations.
185
- // Sub-agents (Task() calls) have a parentID or a title ending in " subagent)".
186
- // We must not register them as top-level Engram sessions — they cause session
187
- // inflation (e.g. 170 sessions for 1 real conversation, issue #116).
188
- const subAgentSessions = new Set<string>()
189
-
190
- /**
191
- * Ensure a session exists in engram. Idempotent — calls POST /sessions
192
- * which uses INSERT OR IGNORE. Safe to call multiple times.
193
- *
194
- * Silently skips sub-agent sessions (tracked in `subAgentSessions`).
195
- */
196
- async function ensureSession(sessionId: string): Promise<boolean> {
197
- if (!sessionId) return false
198
- if (knownSessions.has(sessionId)) return true
199
- // Do not register sub-agent sessions in Engram (issue #116).
200
- if (subAgentSessions.has(sessionId)) return false
201
- const session = await engramFetch("/sessions", {
202
- method: "POST",
203
- body: {
204
- id: sessionId,
205
- project,
206
- directory: ctx.directory,
207
- },
208
- })
209
-
210
- if (session === null) return false
211
-
212
- knownSessions.add(sessionId)
213
- return true
214
- }
215
-
216
- // Try to start engram server if not running
217
- const running = await isEngramRunning()
218
- if (!running) {
219
- try {
220
- Bun.spawn([engramBin, "serve"], {
221
- stdout: "ignore",
222
- stderr: "ignore",
223
- stdin: "ignore",
224
- })
225
- await new Promise((r) => setTimeout(r, 500))
226
- } catch {
227
- // Binary not found or can't start — plugin will silently no-op
228
- }
229
- }
230
-
231
- // Migrate project name if it changed (one-time, idempotent)
232
- // Must run AFTER server startup to ensure the endpoint is available
233
- if (oldProject !== project) {
234
- await engramFetch("/projects/migrate", {
235
- method: "POST",
236
- body: { old_project: oldProject, new_project: project },
237
- })
238
- }
239
-
240
- // Auto-import: if .engram/manifest.json exists in the project repo,
241
- // run `engram sync --import` to load any new chunks into the local DB.
242
- // This is how git-synced memories get loaded when cloning a repo or
243
- // pulling changes. Each chunk is imported only once (tracked by ID).
244
- try {
245
- const manifestFile = `${ctx.directory}/.engram/manifest.json`
246
- const file = Bun.file(manifestFile)
247
- if (await file.exists()) {
248
- Bun.spawn([engramBin, "sync", "--import"], {
249
- cwd: ctx.directory,
250
- stdout: "ignore",
251
- stderr: "ignore",
252
- stdin: "ignore",
253
- })
254
- }
255
- } catch {
256
- // Manifest doesn't exist or binary not found — silently skip
257
- }
258
-
259
- // ─── Safe Hook Wrapper ─────────────────────────────────────────
260
- // Wraps every hook to catch errors and log them to OpenCode instead
261
- // of letting them propagate. This prevents malformed payloads or
262
- // missing fields (output.context, output.system, etc.) from crashing
263
- // the TUI. Errors are logged and swallowed (promise resolves cleanly).
264
- const safe = <A extends unknown[]>(name: string, fn: (...args: A) => Promise<void>) =>
265
- async (...args: A): Promise<void> => {
266
- try { await fn(...args) }
267
- catch (error) { void logToOpenCode(client, "error", `Engram hook ${name} failed`, error) }
268
- }
269
-
270
- return {
271
- // ─── Event Listeners ───────────────────────────────────────────
272
-
273
- event: safe("event", async ({ event }: { event: { type: string; properties?: unknown } }) => {
274
- // --- Session Created ---
275
- if (event.type === "session.created") {
276
- // Bug fix (#116): session data is nested under event.properties.info,
277
- // not event.properties directly.
278
- const info = (event.properties as any)?.info
279
- const sessionId = info?.id
280
- const parentID = info?.parentID
281
- const title: string = info?.title ?? ""
282
-
283
- // Sub-agent sessions (created via Task()) must NOT be registered as
284
- // top-level Engram sessions. They cause massive session inflation
285
- // (e.g. 170 sessions for 1 real conversation).
286
- //
287
- // Detection heuristics:
288
- // - parentID is set on all Task() sub-agent sessions
289
- // - title ends with " subagent)" as a secondary signal
290
- const isSubAgent = !!parentID || title.endsWith(" subagent)")
291
-
292
- if (sessionId && !isSubAgent) {
293
- await ensureSession(sessionId)
294
- } else if (sessionId && isSubAgent) {
295
- // Remember this as a sub-agent session so tool-hook calls
296
- // to ensureSession() are also suppressed for it.
297
- subAgentSessions.add(sessionId)
298
- }
299
- }
300
-
301
- // --- Session Deleted ---
302
- if (event.type === "session.deleted") {
303
- // Same properties.info path as session.created.
304
- const info = (event.properties as any)?.info
305
- const sessionId = info?.id
306
- if (sessionId) {
307
- toolCounts.delete(sessionId)
308
- knownSessions.delete(sessionId)
309
- subAgentSessions.delete(sessionId)
310
- }
311
- }
312
-
313
- }),
314
-
315
- // ─── User Prompt Capture ──────────────────────────────────────
316
- // chat.message is called once per user message, before the LLM sees it.
317
- // input.sessionID is always reliable here (no knownSessions workaround).
318
- // output.message is typed as UserMessage (role:"user" already guaranteed).
319
- // output.parts contains TextPart[] with the actual message text.
320
-
321
- "chat.message": safe("chat.message", async (input: any, output: any) => {
322
- // Skip sub-agent sessions — they inflate session counts (issue #116)
323
- if (subAgentSessions.has(input.sessionID)) return
324
-
325
- const sessionId = input.sessionID
326
-
327
- // Extract text from parts (type:"text")
328
- const content = output.parts
329
- .filter((p: any) => p.type === "text")
330
- .map((p: any) => p.text ?? "")
331
- .join("\n")
332
- .trim()
333
-
334
- // Also fallback to summary if parts yield nothing
335
- const fallback = !content && output.message.summary
336
- ? `${output.message.summary.title ?? ""}\n${output.message.summary.body ?? ""}`.trim()
337
- : ""
338
-
339
- const finalContent = content || fallback
340
-
341
- // Only capture non-trivial prompts (>10 chars)
342
- if (finalContent.length > 10) {
343
- const sessionReady = await ensureSession(sessionId)
344
- if (sessionReady) {
345
- await engramFetch("/prompts", {
346
- method: "POST",
347
- body: {
348
- session_id: sessionId,
349
- content: stripPrivateTags(truncate(finalContent, 2000)),
350
- project,
351
- },
352
- })
353
- }
354
- }
355
- }),
356
-
357
- // ─── Tool Execution Hook ─────────────────────────────────────
358
- // Count tool calls per session (for session end stats).
359
- // Also ensures the session exists — handles plugin reload / reconnect.
360
- // Passive capture: when a Task tool completes, POST its output to
361
- // the passive capture endpoint so the server extracts learnings.
362
-
363
- "tool.execute.after": safe("tool.execute.after", async (input: any, output: any) => {
364
- if (ENGRAM_TOOLS.has(input.tool.toLowerCase())) return
365
-
366
- // input.sessionID comes from OpenCode — always available
367
- const sessionId = input.sessionID
368
- const sessionReady = sessionId ? await ensureSession(sessionId) : false
369
- if (sessionReady && sessionId) {
370
- toolCounts.set(sessionId, (toolCounts.get(sessionId) ?? 0) + 1)
371
- }
372
-
373
- // Passive capture: extract learnings from Task tool output
374
- // (OpenCode reports the tool name in lowercase: "task")
375
- if (input.tool.toLowerCase() === "task" && output && sessionId) {
376
- const text = typeof output === "string" ? output : JSON.stringify(output)
377
- if (text.length > 50 && sessionReady) {
378
- await engramFetch("/observations/passive", {
379
- method: "POST",
380
- body: {
381
- session_id: sessionId,
382
- content: stripPrivateTags(text),
383
- project,
384
- source: "task-complete",
385
- },
386
- })
387
- }
388
- }
389
- }),
390
-
391
- // ─── System Prompt: Always-on memory instructions ──────────
392
- // Injects MEMORY_INSTRUCTIONS into the system prompt of every message.
393
- // This ensures the agent ALWAYS knows about Engram, even after compaction.
394
- //
395
- // We append to the last existing system entry instead of pushing a new one.
396
- // Some models (Qwen3.5, Mistral/Ministral via llama.cpp) reject multiple
397
- // system messages — their Jinja chat templates only allow a single system
398
- // block at the beginning. By concatenating, we avoid adding extra system
399
- // messages that would break these models. See: GitHub issue #23.
400
-
401
- "experimental.chat.system.transform": safe("experimental.chat.system.transform", async (_input: any, output: any) => {
402
- if (output.system.length > 0) {
403
- output.system[output.system.length - 1] += "\n\n" + MEMORY_INSTRUCTIONS
404
- } else {
405
- output.system.push(MEMORY_INSTRUCTIONS)
406
- }
407
- }),
408
-
409
- // ─── Compaction Hook: Persist memory + inject context ──────────
410
- // Compaction is triggered by the system (not the agent) when context
411
- // gets too long. The old agent "dies" and a new one starts with the
412
- // compacted summary. This is our chance to:
413
- // 1. Auto-save a session checkpoint (the agent can't do this itself)
414
- // 2. Inject context from previous sessions into the compaction prompt
415
- // 3. Tell the compressor to remind the new agent to save memories
416
-
417
- "experimental.session.compacting": safe("experimental.session.compacting", async (input: any, output: any) => {
418
- if (input.sessionID) {
419
- await ensureSession(input.sessionID)
420
- }
421
-
422
- // Inject context from previous sessions
423
- const data = await engramFetch(
424
- `/context?project=${encodeURIComponent(project)}`
425
- )
426
- if (data?.context) {
427
- output.context.push(data.context)
428
- }
429
-
430
- // Tell the compressor to instruct the new agent to persist the
431
- // compacted summary to Engram. The new agent reads the compacted
432
- // summary and this instruction, then saves it as a session summary.
433
- output.context.push(
434
- `CRITICAL INSTRUCTION FOR COMPACTED SUMMARY:\n` +
435
- `The agent has access to Engram persistent memory via MCP tools.\n` +
436
- `You MUST include the following instruction at the TOP of the compacted summary:\n\n` +
437
- `"FIRST ACTION REQUIRED: Call mem_session_summary with the content of this compacted summary. ` +
438
- `Use project '${project}' if the tool accepts it. This preserves what was accomplished before compaction. ` +
439
- `Do this BEFORE any other work. After that, delegate to the global engram subagent to recover only the relevant previous context."\n\n` +
440
- `This is NOT optional. Without this, everything done before compaction is lost from memory.`
441
- )
442
- }),
443
- }
444
- }