zelari-code 2.6.3 → 2.8.0

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 (55) hide show
  1. package/README.md +24 -2
  2. package/dist/cli/hooks/useChatTurn.js +109 -1
  3. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  4. package/dist/cli/hooks/useSlashDispatch.js +7 -0
  5. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  6. package/dist/cli/kraken/executor.js +73 -0
  7. package/dist/cli/kraken/executor.js.map +1 -1
  8. package/dist/cli/main.bundled.js +5323 -1502
  9. package/dist/cli/main.bundled.js.map +4 -4
  10. package/dist/cli/main.js +46 -0
  11. package/dist/cli/main.js.map +1 -1
  12. package/dist/cli/memory/fileBackend.js +8 -0
  13. package/dist/cli/memory/fileBackend.js.map +1 -1
  14. package/dist/cli/memory/jsonApi.js +83 -0
  15. package/dist/cli/memory/jsonApi.js.map +1 -0
  16. package/dist/cli/memory/legacyImport.js +91 -0
  17. package/dist/cli/memory/legacyImport.js.map +1 -0
  18. package/dist/cli/memory/mcpAdapter.js +255 -0
  19. package/dist/cli/memory/mcpAdapter.js.map +1 -0
  20. package/dist/cli/memory/mcpCli.js +19 -0
  21. package/dist/cli/memory/mcpCli.js.map +1 -0
  22. package/dist/cli/memory/mcpServer.js +126 -0
  23. package/dist/cli/memory/mcpServer.js.map +1 -0
  24. package/dist/cli/memory/promotion.js +68 -0
  25. package/dist/cli/memory/promotion.js.map +1 -0
  26. package/dist/cli/memory/serviceFactory.js +94 -0
  27. package/dist/cli/memory/serviceFactory.js.map +1 -0
  28. package/dist/cli/memory/sqliteBackend.js +636 -0
  29. package/dist/cli/memory/sqliteBackend.js.map +1 -0
  30. package/dist/cli/memory/sqliteCodec.js +114 -0
  31. package/dist/cli/memory/sqliteCodec.js.map +1 -0
  32. package/dist/cli/memory/sqliteRpc.js +120 -0
  33. package/dist/cli/memory/sqliteRpc.js.map +1 -0
  34. package/dist/cli/memory/sqliteSchema.js +181 -0
  35. package/dist/cli/memory/sqliteSchema.js.map +1 -0
  36. package/dist/cli/memory/sqliteWorker.mjs +264 -0
  37. package/dist/cli/runHeadless.js +97 -3
  38. package/dist/cli/runHeadless.js.map +1 -1
  39. package/dist/cli/semantic/embeddings.js +13 -1
  40. package/dist/cli/semantic/embeddings.js.map +1 -1
  41. package/dist/cli/semantic/provider.js +11 -2
  42. package/dist/cli/semantic/provider.js.map +1 -1
  43. package/dist/cli/slashCommands.js +10 -0
  44. package/dist/cli/slashCommands.js.map +1 -1
  45. package/dist/cli/slashHandlers/krakenGraph.js +11 -0
  46. package/dist/cli/slashHandlers/krakenGraph.js.map +1 -1
  47. package/dist/cli/slashHandlers/memory.js +239 -0
  48. package/dist/cli/slashHandlers/memory.js.map +1 -0
  49. package/dist/cli/toolRegistry.js +4 -0
  50. package/dist/cli/toolRegistry.js.map +1 -1
  51. package/dist/cli/tools/taskTool.js +41 -0
  52. package/dist/cli/tools/taskTool.js.map +1 -1
  53. package/dist/cli/zelariMission.js +6 -0
  54. package/dist/cli/zelariMission.js.map +1 -1
  55. package/package.json +4 -3
@@ -0,0 +1,264 @@
1
+ import { parentPort } from 'node:worker_threads';
2
+ import { backup, DatabaseSync } from 'node:sqlite';
3
+ import { createHash } from 'node:crypto';
4
+ import { closeSync, existsSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
5
+
6
+ if (!parentPort) throw new Error('sqliteWorker must run inside a worker thread');
7
+
8
+ let database = null;
9
+
10
+ function waitSync(ms) {
11
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
12
+ }
13
+
14
+ function acquireMigrationLock(dbPath, timeoutMs = 10_000) {
15
+ if (!dbPath || dbPath === ':memory:') return { path: null, fd: null };
16
+ const lockPath = `${dbPath}.migration.lock`;
17
+ const started = Date.now();
18
+ while (true) {
19
+ try {
20
+ const fd = openSync(lockPath, 'wx');
21
+ writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
22
+ return { path: lockPath, fd };
23
+ } catch (error) {
24
+ if (error?.code !== 'EEXIST') throw error;
25
+ try {
26
+ if (Date.now() - statSync(lockPath).mtimeMs > 300_000) {
27
+ let ownerAlive = false;
28
+ try {
29
+ const owner = JSON.parse(readFileSync(lockPath, 'utf8'));
30
+ if (Number.isInteger(owner?.pid) && owner.pid > 0) {
31
+ try { process.kill(owner.pid, 0); ownerAlive = true; } catch {}
32
+ }
33
+ } catch {}
34
+ if (!ownerAlive) unlinkSync(lockPath);
35
+ }
36
+ } catch (statError) {
37
+ if (statError?.code !== 'ENOENT') throw statError;
38
+ }
39
+ if (Date.now() - started >= timeoutMs) {
40
+ const locked = new Error(`Timed out waiting for memory migration lock: ${lockPath}`);
41
+ locked.code = 'ZELARI_MEMORY_MIGRATION_LOCKED';
42
+ throw locked;
43
+ }
44
+ waitSync(50);
45
+ }
46
+ }
47
+ }
48
+
49
+ function releaseMigrationLock(lock) {
50
+ if (lock.fd !== null) {
51
+ try { closeSync(lock.fd); } catch {}
52
+ }
53
+ if (lock.path) {
54
+ try { unlinkSync(lock.path); } catch {}
55
+ }
56
+ }
57
+
58
+ function invoke(statement, mode, params = []) {
59
+ const fn = statement[mode];
60
+ if (typeof fn !== 'function') throw new Error(`Unsupported SQLite statement mode: ${mode}`);
61
+ return Array.isArray(params) ? fn.call(statement, ...params) : fn.call(statement, params);
62
+ }
63
+
64
+ function executeStep(step) {
65
+ const statement = database.prepare(step.sql);
66
+ const result = invoke(statement, step.mode ?? 'run', step.params ?? []);
67
+ if (step.mode === 'run' || !step.mode) {
68
+ return {
69
+ changes: Number(result.changes ?? 0),
70
+ lastInsertRowid: String(result.lastInsertRowid ?? ''),
71
+ };
72
+ }
73
+ return result;
74
+ }
75
+
76
+ function hashContent(content) {
77
+ return createHash('sha256').update(String(content ?? '')).digest('hex');
78
+ }
79
+
80
+ function decodeVector(value, dimensions) {
81
+ if (typeof value !== 'string') return null;
82
+ try {
83
+ const vector = JSON.parse(value);
84
+ const expected = Number(dimensions);
85
+ return Array.isArray(vector) && vector.length === expected && vector.length > 0 &&
86
+ vector.every((item) => typeof item === 'number' && Number.isFinite(item))
87
+ ? vector
88
+ : null;
89
+ } catch { return null; }
90
+ }
91
+
92
+ function cosine(left, right) {
93
+ if (!left.length || left.length !== right.length) return 0;
94
+ let dot = 0;
95
+ let leftNorm = 0;
96
+ let rightNorm = 0;
97
+ for (let index = 0; index < left.length; index += 1) {
98
+ dot += left[index] * right[index];
99
+ leftNorm += left[index] * left[index];
100
+ rightNorm += right[index] * right[index];
101
+ }
102
+ return leftNorm && rightNorm ? dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm)) : 0;
103
+ }
104
+
105
+ const handlers = {
106
+ async open(args) {
107
+ if (database) return { fts: true };
108
+ const lock = acquireMigrationLock(args.dbPath, args.migrationLockTimeoutMs ?? 30_000);
109
+ try {
110
+ database = new DatabaseSync(args.dbPath, {
111
+ timeout: args.timeoutMs ?? 5_000,
112
+ enableForeignKeyConstraints: true,
113
+ });
114
+ const row = database.prepare('PRAGMA user_version').get();
115
+ const currentVersion = Number(row?.user_version ?? 0);
116
+ if (currentVersion > args.schemaVersion) {
117
+ const error = new Error(
118
+ `Memory database schema v${currentVersion} is newer than runtime v${args.schemaVersion}; refusing to downgrade.`,
119
+ );
120
+ error.code = 'ZELARI_MEMORY_SCHEMA_NEWER';
121
+ throw error;
122
+ }
123
+ let migratedFrom;
124
+ let backupPath;
125
+ if (currentVersion === 0) {
126
+ database.exec(args.schemaSql);
127
+ database.exec(`PRAGMA user_version = ${Number(args.schemaVersion)}`);
128
+ } else if (currentVersion < args.schemaVersion) {
129
+ migratedFrom = currentVersion;
130
+ const baseBackupPath = `${args.dbPath}.v${currentVersion}.bak`;
131
+ backupPath = existsSync(baseBackupPath)
132
+ ? `${baseBackupPath}.${Date.now()}`
133
+ : baseBackupPath;
134
+ await backup(database, backupPath);
135
+ const migrations = [...(args.migrations ?? [])]
136
+ .filter((migration) => migration.version > currentVersion && migration.version <= args.schemaVersion)
137
+ .sort((a, b) => a.version - b.version);
138
+ let expected = currentVersion + 1;
139
+ for (const migration of migrations) {
140
+ if (migration.version !== expected) {
141
+ const error = new Error(`Missing memory migration v${expected} -> v${migration.version}.`);
142
+ error.code = 'ZELARI_MEMORY_MIGRATION_GAP';
143
+ throw error;
144
+ }
145
+ database.exec('BEGIN EXCLUSIVE');
146
+ try {
147
+ database.exec(migration.sql);
148
+ database.exec(`PRAGMA user_version = ${Number(migration.version)}`);
149
+ database.exec('COMMIT');
150
+ } catch (error) {
151
+ try { database.exec('ROLLBACK'); } catch {}
152
+ throw error;
153
+ }
154
+ expected += 1;
155
+ }
156
+ if (expected - 1 !== args.schemaVersion) {
157
+ const error = new Error(`No complete migration path from v${currentVersion} to v${args.schemaVersion}.`);
158
+ error.code = 'ZELARI_MEMORY_MIGRATION_GAP';
159
+ throw error;
160
+ }
161
+ database.exec(args.schemaSql);
162
+ } else {
163
+ database.exec(args.schemaSql);
164
+ }
165
+ let fts = true;
166
+ try {
167
+ database.exec(args.ftsSql);
168
+ } catch {
169
+ fts = false;
170
+ }
171
+ return { fts, migratedFrom, backupPath };
172
+ } catch (error) {
173
+ try { database?.close(); } catch {}
174
+ database = null;
175
+ throw error;
176
+ } finally {
177
+ releaseMigrationLock(lock);
178
+ }
179
+ },
180
+ exec(args) {
181
+ database.exec(args.sql);
182
+ return null;
183
+ },
184
+ statement(args) {
185
+ return executeStep(args);
186
+ },
187
+ vectorSearch(args) {
188
+ const rows = invoke(database.prepare(args.sql), 'all', args.params ?? []);
189
+ const scored = [];
190
+ for (const row of rows) {
191
+ const vector = decodeVector(row.vector_json, row.dimensions);
192
+ if (!vector || vector.length !== args.vector.length) continue;
193
+ if (row.content_hash !== hashContent(row.content)) continue;
194
+ scored.push({
195
+ ...row,
196
+ semantic_relevance: Math.max(0, Math.min(1, cosine(args.vector, vector))),
197
+ });
198
+ }
199
+ scored.sort((left, right) =>
200
+ right.semantic_relevance - left.semantic_relevance ||
201
+ Number(right.importance ?? 0) - Number(left.importance ?? 0));
202
+ return scored.slice(0, Math.max(1, Number(args.limit) || 1));
203
+ },
204
+ semanticStatus(args) {
205
+ const rows = database.prepare(`SELECT n.content, e.content_hash, e.dimensions,
206
+ e.vector_json, e.indexed_at FROM memory_nodes n LEFT JOIN memory_embeddings e
207
+ ON e.memory_id=n.id AND e.model=? WHERE n.project_id=? AND n.status='active'`)
208
+ .all(args.model, args.projectId);
209
+ let indexed = 0;
210
+ let stale = 0;
211
+ let corrupt = 0;
212
+ let lastIndexedAt;
213
+ for (const row of rows) {
214
+ const vector = decodeVector(row.vector_json, row.dimensions);
215
+ const fresh = vector && row.content_hash === hashContent(row.content);
216
+ if (fresh) {
217
+ indexed += 1;
218
+ if (typeof row.indexed_at === 'string' && (!lastIndexedAt || row.indexed_at > lastIndexedAt)) {
219
+ lastIndexedAt = row.indexed_at;
220
+ }
221
+ } else {
222
+ stale += 1;
223
+ if (row.vector_json !== null && row.vector_json !== undefined) corrupt += 1;
224
+ }
225
+ }
226
+ return { indexed, stale, corrupt, lastIndexedAt };
227
+ },
228
+ batch(args) {
229
+ database.exec(args.immediate === false ? 'BEGIN' : 'BEGIN IMMEDIATE');
230
+ try {
231
+ const results = args.steps.map(executeStep);
232
+ database.exec('COMMIT');
233
+ return results;
234
+ } catch (error) {
235
+ try { database.exec('ROLLBACK'); } catch {}
236
+ throw error;
237
+ }
238
+ },
239
+ close() {
240
+ database?.close();
241
+ database = null;
242
+ return null;
243
+ },
244
+ };
245
+
246
+ parentPort.on('message', async ({ id, operation, args }) => {
247
+ try {
248
+ const handler = handlers[operation];
249
+ if (!handler) throw new Error(`Unknown SQLite worker operation: ${operation}`);
250
+ parentPort.postMessage({ id, ok: true, value: await handler(args ?? {}) });
251
+ } catch (error) {
252
+ parentPort.postMessage({
253
+ id,
254
+ ok: false,
255
+ error: {
256
+ name: error?.name ?? 'Error',
257
+ message: error?.message ?? String(error),
258
+ stack: error?.stack,
259
+ code: error?.code,
260
+ errcode: error?.errcode,
261
+ },
262
+ });
263
+ }
264
+ });
@@ -224,6 +224,10 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
224
224
  const { createKrakenSubAgentContextFactory } = await import('./toolRegistry.js');
225
225
  const cwd = process.cwd();
226
226
  const sessionId = crypto.randomUUID();
227
+ const { getMemoryService, isMemoryAutoWriteEnabled, isMemoryV2Enabled } = await import('./memory/serviceFactory.js');
228
+ const graphMemory = isMemoryV2Enabled()
229
+ ? await getMemoryService(cwd, process.env)
230
+ : undefined;
227
231
  const log = (message) => {
228
232
  if (opts.output === 'json') {
229
233
  emitEvent({ type: 'log', message });
@@ -323,6 +327,8 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
323
327
  provider,
324
328
  model,
325
329
  }),
330
+ ...(graphMemory ? { memoryService: graphMemory } : {}),
331
+ memoryAutoWrite: isMemoryAutoWriteEnabled(),
326
332
  },
327
333
  parentCwd: cwd,
328
334
  sessionId,
@@ -371,6 +377,7 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
371
377
  }
372
378
  finally {
373
379
  process.off('SIGINT', onSigint);
380
+ await graphMemory?.close().catch(() => undefined);
374
381
  }
375
382
  }
376
383
  function planModeFromOpts(opts) {
@@ -420,6 +427,11 @@ async function registerHeadlessMcp(toolRegistry, opts) {
420
427
  }
421
428
  async function runHeadlessSingle(opts, provider, model, providerStream) {
422
429
  const sessionId = crypto.randomUUID();
430
+ const memoryFactory = await import('./memory/serviceFactory.js');
431
+ const nativeMemory = memoryFactory.isMemoryV2Enabled()
432
+ ? await memoryFactory.getMemoryService(process.cwd(), process.env)
433
+ : undefined;
434
+ const memoryAutoWrite = memoryFactory.isMemoryAutoWriteEnabled();
423
435
  // Headless / Desktop: no interactive permission UI — auto-allow "ask" rules
424
436
  // unless the user set an explicit deny. Override with ZELARI_AUTO=0 and
425
437
  // ZELARI_PERMISSION_*=deny for hard lockdown.
@@ -457,6 +469,8 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
457
469
  ui: 'allow',
458
470
  auto: true,
459
471
  },
472
+ ...(nativeMemory ? { memoryService: nativeMemory } : {}),
473
+ memoryAutoWrite,
460
474
  });
461
475
  // Parity with TUI: project MCP tools must be available from Desktop/headless.
462
476
  await registerHeadlessMcp(toolRegistry, opts);
@@ -672,6 +686,13 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
672
686
  // command is a test/typecheck/build/git-diff line.
673
687
  toolCallGate: (name, args) => spine.gateResourceToolCall(name, args) ?? { allowed: true },
674
688
  maxToolLoopIterations: maxToolLoop,
689
+ ...(nativeMemory
690
+ ? {
691
+ memoryService: nativeMemory,
692
+ memoryQuery: opts.task,
693
+ memoryContextChars: 2_000,
694
+ }
695
+ : {}),
675
696
  });
676
697
  let finalReason = 'completed';
677
698
  let exitCode = 0;
@@ -973,6 +994,35 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
973
994
  }
974
995
  catch { /* export is best-effort */ }
975
996
  }
997
+ if (nativeMemory && memoryAutoWrite && pass.finalReason !== 'error') {
998
+ try {
999
+ const finalContent = [...pass.messages]
1000
+ .reverse()
1001
+ .find((message) => message.role === 'assistant' && message.content.trim())
1002
+ ?.content.trim();
1003
+ if (finalContent) {
1004
+ await nativeMemory.remember({
1005
+ kind: planModeFromOpts(opts) ? 'finding' : 'outcome',
1006
+ content: finalContent.slice(0, 8_000),
1007
+ importance: planModeFromOpts(opts) ? 0.55 : 0.7,
1008
+ confidence: strictExit === 0 ? 0.75 : 0.45,
1009
+ source: { agent: 'zelari-headless', sessionId: spine.sessionId },
1010
+ tags: ['headless', `phase:${opts.phase ?? 'build'}`],
1011
+ metadata: {
1012
+ objective: opts.task.slice(0, 2_000),
1013
+ successfulWrites: pass.successfulWrites,
1014
+ strictExit,
1015
+ writeClass: planModeFromOpts(opts) ? 'candidate' : 'auto',
1016
+ },
1017
+ writeClass: planModeFromOpts(opts) ? 'candidate' : 'auto',
1018
+ });
1019
+ }
1020
+ }
1021
+ catch {
1022
+ // Headless exit status is never governed by memory persistence.
1023
+ }
1024
+ }
1025
+ await nativeMemory?.close().catch(() => undefined);
976
1026
  if (pass.finalReason === 'error')
977
1027
  return 3;
978
1028
  // E2.2: strict done gate — a blocked verdict overrides a clean pass exit.
@@ -980,7 +1030,7 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
980
1030
  return strictExit;
981
1031
  return pass.exitCode;
982
1032
  }
983
- async function buildCouncilToolRegistry(planMode, opts) {
1033
+ async function buildCouncilToolRegistry(planMode, opts, memoryService, memoryAutoWrite = false) {
984
1034
  const { registry: toolRegistry } = createBuiltinToolRegistry({
985
1035
  planMode,
986
1036
  permissionPolicy: {
@@ -991,6 +1041,8 @@ async function buildCouncilToolRegistry(planMode, opts) {
991
1041
  ui: 'allow',
992
1042
  auto: true,
993
1043
  },
1044
+ ...(memoryService ? { memoryService } : {}),
1045
+ memoryAutoWrite,
994
1046
  });
995
1047
  const { createWorkspaceContext, createWorkspaceStubs } = await import('./workspace/stubs.js');
996
1048
  const { createWorkspaceToolRegistry } = await import('./workspace/toolRegistry.js');
@@ -1011,6 +1063,11 @@ async function buildCouncilToolRegistry(planMode, opts) {
1011
1063
  async function runHeadlessCouncil(opts, provider, model, providerStream) {
1012
1064
  const { dispatchCouncil } = await import('./councilDispatcher.js');
1013
1065
  const sessionId = crypto.randomUUID();
1066
+ const memoryFactory = await import('./memory/serviceFactory.js');
1067
+ const nativeMemory = memoryFactory.isMemoryV2Enabled()
1068
+ ? await memoryFactory.getMemoryService(process.cwd(), process.env)
1069
+ : undefined;
1070
+ const memoryAutoWrite = memoryFactory.isMemoryAutoWriteEnabled();
1014
1071
  const spine = await openHeadlessSpine({
1015
1072
  sessionId: opts.resumeSessionId ?? sessionId,
1016
1073
  mode: opts.mode,
@@ -1038,7 +1095,7 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
1038
1095
  process.stderr.write('[zelari-code --headless] council build soft-gate: forced design-phase ' +
1039
1096
  '(set ZELARI_COUNCIL_CAN_BUILD=1 to allow Lucifero implement)\n');
1040
1097
  }
1041
- const { toolRegistry } = await buildCouncilToolRegistry(planModeFromOpts(opts) || softGated, opts);
1098
+ const { toolRegistry } = await buildCouncilToolRegistry(planModeFromOpts(opts) || softGated, opts, nativeMemory, memoryAutoWrite);
1042
1099
  const { FeedbackStore } = await import('./councilFeedback.js');
1043
1100
  const feedbackStore = new FeedbackStore();
1044
1101
  // Multi-turn: Desktop passes --history, but council used to ignore it →
@@ -1091,11 +1148,20 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
1091
1148
  const { loadDurableContext } = await import('./state/loadDurableContext.js');
1092
1149
  const cwd = process.cwd();
1093
1150
  const durableState = await loadDurableContext(cwd);
1151
+ const memoryContext = nativeMemory
1152
+ ? (await nativeMemory.buildContext({
1153
+ text: effectiveTask,
1154
+ useGraph: true,
1155
+ maxChars: 2_000,
1156
+ maxMemories: 8,
1157
+ })).text
1158
+ : '';
1094
1159
  const composed = composeProjectContext({
1095
1160
  mode: 'council',
1096
1161
  cwd,
1097
1162
  userMessage: opts.task,
1098
1163
  includeLessons: true,
1164
+ memoryHits: memoryContext || undefined,
1099
1165
  durableState: durableState || undefined,
1100
1166
  includeDurableState: false,
1101
1167
  });
@@ -1164,6 +1230,7 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
1164
1230
  }
1165
1231
  catch (err) {
1166
1232
  process.stderr.write(`[zelari-code --headless] council error: ${err instanceof Error ? err.message : String(err)}\n`);
1233
+ await nativeMemory?.close().catch(() => undefined);
1167
1234
  return 2;
1168
1235
  }
1169
1236
  // Desktop multi-turn: append this turn so the next "procedi" has context.
@@ -1185,6 +1252,32 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
1185
1252
  }
1186
1253
  catch { /* export is best-effort */ }
1187
1254
  }
1255
+ if (nativeMemory && memoryAutoWrite && lastAssistantText) {
1256
+ try {
1257
+ await nativeMemory.remember({
1258
+ kind: councilRunMode === 'design-phase' ? 'decision' : 'outcome',
1259
+ content: lastAssistantText.slice(0, 12_000),
1260
+ importance: councilRunMode === 'design-phase' ? 0.8 : 0.75,
1261
+ confidence: exitCode === 0 ? 0.78 : 0.45,
1262
+ source: { agent: 'council-headless', sessionId: spine.sessionId },
1263
+ tags: ['council', 'headless', `run-mode:${councilRunMode}`],
1264
+ metadata: {
1265
+ objective: opts.task.slice(0, 2_000),
1266
+ exitCode,
1267
+ writeClass: exitCode === 0 ? 'auto' : 'candidate',
1268
+ },
1269
+ writeClass: exitCode === 0 ? 'auto' : 'candidate',
1270
+ });
1271
+ await nativeMemory.consolidate({
1272
+ source: { agent: 'council-headless', sessionId: spine.sessionId },
1273
+ minOccurrences: 2,
1274
+ });
1275
+ }
1276
+ catch {
1277
+ // Memory is not part of the council completion gate.
1278
+ }
1279
+ }
1280
+ await nativeMemory?.close().catch(() => undefined);
1188
1281
  return exitCode;
1189
1282
  }
1190
1283
  /**
@@ -1220,7 +1313,8 @@ async function runHeadlessZelari(opts, provider, model, providerStream) {
1220
1313
  hasPlan: hasWorkspacePlan(projectRoot),
1221
1314
  });
1222
1315
  const memory = await getMemoryBackend(projectRoot);
1223
- const { toolRegistry, workspaceCtx } = await buildCouncilToolRegistry(planModeFromOpts(opts), opts);
1316
+ const nativeMissionMemory = memory.service;
1317
+ const { toolRegistry, workspaceCtx } = await buildCouncilToolRegistry(planModeFromOpts(opts), opts, nativeMissionMemory, Boolean(nativeMissionMemory) && process.env.ZELARI_MEMORY_AUTO_WRITE !== '0');
1224
1318
  const feedbackStore = new FeedbackStore();
1225
1319
  const chairmanBudget = envNumber(process.env.ZELARI_MODE_MAX_TOOLS_LUCIFER, {
1226
1320
  default: 30,