amalgm 0.1.35 → 0.1.37

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 (28) hide show
  1. package/package.json +1 -1
  2. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +29 -2
  3. package/runtime/scripts/amalgm-mcp/events/executor.js +6 -0
  4. package/runtime/scripts/amalgm-mcp/events/rest.js +13 -0
  5. package/runtime/scripts/amalgm-mcp/events/tools.js +13 -0
  6. package/runtime/scripts/amalgm-mcp/fs/rest.js +6 -0
  7. package/runtime/scripts/amalgm-mcp/server/http.js +9 -0
  8. package/runtime/scripts/amalgm-mcp/state/db.js +25 -0
  9. package/runtime/scripts/amalgm-mcp/state/snapshot.js +39 -11
  10. package/runtime/scripts/amalgm-mcp/tasks/executor.js +9 -3
  11. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +6 -3
  12. package/runtime/scripts/amalgm-mcp/tasks/tools.js +13 -0
  13. package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +45 -0
  14. package/runtime/scripts/amalgm-mcp/tests/local-live-snapshot.test.js +44 -0
  15. package/runtime/scripts/amalgm-mcp/tests/scheduler.test.js +64 -0
  16. package/runtime/scripts/amalgm-mcp/tests/workspace-store.test.js +84 -0
  17. package/runtime/scripts/amalgm-mcp/toolbox/store.js +15 -0
  18. package/runtime/scripts/amalgm-mcp/workspace/rest.js +162 -22
  19. package/runtime/scripts/amalgm-mcp/workspace/store.js +278 -0
  20. package/runtime/scripts/chat-core/adapters/codex.js +44 -4
  21. package/runtime/scripts/chat-core/contract.js +57 -0
  22. package/runtime/scripts/chat-core/engine.js +5 -5
  23. package/runtime/scripts/chat-core/server.js +17 -4
  24. package/runtime/scripts/chat-core/sse.js +8 -1
  25. package/runtime/scripts/chat-core/stores.js +3 -1
  26. package/runtime/scripts/chat-core/tooling/active-memory.js +396 -0
  27. package/runtime/scripts/chat-core/tooling/system-prompt.js +3 -0
  28. package/runtime/scripts/local-gateway.js +158 -0
@@ -190,6 +190,7 @@ function frozenRuntimeFields(input) {
190
190
  authMethod: input.authMethod,
191
191
  auth: authIdentity,
192
192
  systemPrompt: fingerprint(input.systemPrompt || ''),
193
+ origin: fingerprint(JSON.stringify(input.origin || null)),
193
194
  mcpServers: fingerprint(JSON.stringify(mcpServers)),
194
195
  };
195
196
  }
@@ -254,6 +255,59 @@ function nullableString(value) {
254
255
  return typeof value === 'string' && value.trim() ? value.trim() : null;
255
256
  }
256
257
 
258
+ function normalizeOrigin(payload = {}) {
259
+ const raw = payload.origin && typeof payload.origin === 'object' ? payload.origin : {};
260
+ const type = nullableString(raw.type || payload.originType || payload.origin);
261
+ const id = nullableString(
262
+ raw.id
263
+ || raw.originId
264
+ || payload.originId
265
+ || payload.taskId
266
+ || payload.triggerId,
267
+ );
268
+ if (!type || !id) return null;
269
+ return {
270
+ type,
271
+ id,
272
+ name: nullableString(raw.name || raw.originName || payload.originName),
273
+ projectPath: nullableString(raw.projectPath || payload.projectPath || payload.cwd),
274
+ };
275
+ }
276
+
277
+ function normalizeConstruct(input, fallback = {}) {
278
+ if (!input || typeof input !== 'object') return null;
279
+ const type = nullableString(input.type || input.scopeType);
280
+ if (!type) return null;
281
+ const id = nullableString(input.id || input.scopeId || input.originId);
282
+ const projectPath = nullableString(input.projectPath || input.path || fallback.projectPath);
283
+ const item = {
284
+ type,
285
+ id,
286
+ name: nullableString(input.name || input.originName),
287
+ projectPath,
288
+ path: nullableString(input.path),
289
+ };
290
+ if (!item.id && !item.projectPath && !item.path) return null;
291
+ return item;
292
+ }
293
+
294
+ function normalizeConstructs(payload = {}, cwd = null) {
295
+ const constructs = [];
296
+ for (const item of Array.isArray(payload.constructs) ? payload.constructs : []) {
297
+ const normalized = normalizeConstruct(item, { projectPath: cwd });
298
+ if (normalized) constructs.push(normalized);
299
+ }
300
+ const origin = normalizeOrigin(payload);
301
+ if (origin) constructs.push(origin);
302
+ const seen = new Set();
303
+ return constructs.filter((item) => {
304
+ const key = `${item.type}:${item.id || item.projectPath || item.path || ''}`;
305
+ if (seen.has(key)) return false;
306
+ seen.add(key);
307
+ return true;
308
+ });
309
+ }
310
+
257
311
  function createContract(payload, options = {}) {
258
312
  const sessionId = payload.codeSessionId || payload.sessionId;
259
313
  if (!sessionId) throw new Error('codeSessionId is required');
@@ -270,6 +324,7 @@ function createContract(payload, options = {}) {
270
324
  const cliModel = cliModelFor({ harness, modelId, cliModel: payload.cliModel, reasoningEffort });
271
325
  const cwd = resolveCwd(payload.cwd, options);
272
326
  const localBaseUrl = options.localBaseUrl || `http://127.0.0.1:${options.port || process.env.CHAT_SERVER_PORT || 8084}`;
327
+ const origin = normalizeOrigin({ ...payload, cwd });
273
328
  const auth = authEnvelope({
274
329
  harness,
275
330
  authMethod,
@@ -299,6 +354,8 @@ function createContract(payload, options = {}) {
299
354
  contextWindow: positiveOrNull(payload.contextWindow),
300
355
  maxTokens: positiveOrNull(payload.maxTokens),
301
356
  cwd,
357
+ origin,
358
+ constructs: normalizeConstructs({ ...payload, origin }, cwd),
302
359
  localBaseUrl,
303
360
  mcpServers: Array.isArray(payload.mcpServers) ? payload.mcpServers : [],
304
361
  systemPrompt: payload.systemPrompt || '',
@@ -4,7 +4,7 @@ const { PartAccumulator } = require('./parts');
4
4
  const { assertSameContract, createContract } = require('./contract');
5
5
  const { done, errorEvent } = require('./events');
6
6
  const { normalizeInput } = require('./input');
7
- const { frameFor, titleFrame } = require('./sse');
7
+ const { frameFor, titleFrame, withSseId } = require('./sse');
8
8
  const { beginProxyTurn, logTurnUsage } = require('./usage');
9
9
 
10
10
  function warnIfDbNotSaved(result, label) {
@@ -168,8 +168,8 @@ class ChatCore {
168
168
  if (typeof this.db.generateAndSaveTitle === 'function' && input.text) {
169
169
  this.db.generateAndSaveTitle(contract.sessionId, input.text, (title) => {
170
170
  const frame = titleFrame({ sessionId: contract.sessionId, title });
171
- this.turns.addChunk(turnId, frame);
172
- if (writeFrame) writeFrame(frame);
171
+ const chunk = this.turns.addChunk(turnId, (index) => withSseId(frame, index));
172
+ if (writeFrame && chunk) writeFrame(chunk.data);
173
173
  }).catch((err) => {
174
174
  console.warn('[ChatCore] Title generation failed:', err.message);
175
175
  });
@@ -194,8 +194,8 @@ class ChatCore {
194
194
  }
195
195
  parts.apply(e);
196
196
  const frame = frameFor(e, { sessionId: contract.sessionId, providerSessionId, usage });
197
- this.turns.addChunk(turnId, frame);
198
- if (writeFrame) writeFrame(frame);
197
+ const chunk = this.turns.addChunk(turnId, (index) => withSseId(frame, index));
198
+ if (writeFrame && chunk) writeFrame(chunk.data);
199
199
  };
200
200
 
201
201
  try {
@@ -56,23 +56,36 @@ function writeRawTemp(core, route, res) {
56
56
  const entry = core.turns.latest(route.id) || core.turns.get(route.id);
57
57
  if (!entry) return sendJson(res, 404, { error: 'No active stream', codeSessionId: route.id });
58
58
  writeSseHeaders(res);
59
+ let open = true;
60
+ const heartbeat = setInterval(() => {
61
+ if (open && !res.writableEnded) res.write(': keep-alive\n\n');
62
+ }, 15_000);
63
+ heartbeat.unref?.();
64
+ const cleanup = () => {
65
+ open = false;
66
+ clearInterval(heartbeat);
67
+ };
59
68
  const chunks = entry.chunks.filter((chunk) => chunk.index > (Number.isFinite(route.afterIndex) ? route.afterIndex : -1));
60
- if (chunks.length) {
61
- res.write(`event: existing\ndata: ${JSON.stringify({ codeSessionId: entry.sessionId, assistantMessageId: entry.assistantMessageId, chunks, total: chunks.length })}\n\n`);
62
- }
69
+ res.write(`event: existing\ndata: ${JSON.stringify({ codeSessionId: entry.sessionId, assistantMessageId: entry.assistantMessageId, chunks, total: chunks.length })}\n\n`);
63
70
  if (entry.status !== 'streaming') {
64
71
  res.write(`event: complete\ndata: ${JSON.stringify({ codeSessionId: entry.sessionId, total: entry.chunks.length })}\n\n`);
72
+ cleanup();
65
73
  res.end();
66
74
  return;
67
75
  }
68
76
  const off = core.turns.subscribe(entry.turnId, (chunk, current) => {
77
+ if (!open || res.writableEnded) return;
69
78
  if (chunk) res.write(`event: chunk\ndata: ${JSON.stringify({ codeSessionId: current.sessionId, chunk, total: current.chunks.length })}\n\n`);
70
79
  else {
71
80
  res.write(`event: complete\ndata: ${JSON.stringify({ codeSessionId: current.sessionId, total: current.chunks.length })}\n\n`);
81
+ cleanup();
72
82
  res.end();
73
83
  }
74
84
  });
75
- res.on('close', off);
85
+ res.on('close', () => {
86
+ cleanup();
87
+ off();
88
+ });
76
89
  }
77
90
 
78
91
  function createCore(options = {}) {
@@ -185,6 +185,13 @@ function titleFrame({ sessionId, title }) {
185
185
  return `data: ${JSON.stringify({ _type: 'title_generated', sessionId, title })}\n\n`;
186
186
  }
187
187
 
188
+ function withSseId(frame, id) {
189
+ const cleanId = Number(id);
190
+ if (!Number.isInteger(cleanId) || cleanId < 0) return frame;
191
+ if (String(frame || '').startsWith('id:')) return frame;
192
+ return `id: ${cleanId}\n${frame}`;
193
+ }
194
+
188
195
  function writeSseHeaders(res) {
189
196
  res.writeHead(200, {
190
197
  'Content-Type': 'text/event-stream',
@@ -193,4 +200,4 @@ function writeSseHeaders(res) {
193
200
  });
194
201
  }
195
202
 
196
- module.exports = { frameFor, titleFrame, toAgentStreamEvent, toPromptUsage, writeSseHeaders };
203
+ module.exports = { frameFor, titleFrame, toAgentStreamEvent, toPromptUsage, withSseId, writeSseHeaders };
@@ -44,7 +44,9 @@ class TurnStore {
44
44
  addChunk(turnId, frame) {
45
45
  const entry = this.turns.get(turnId);
46
46
  if (!entry || !['streaming', 'cancelling'].includes(entry.status)) return null;
47
- const chunk = { index: entry.chunks.length, data: frame, timestamp: Date.now() };
47
+ const index = entry.chunks.length;
48
+ const data = typeof frame === 'function' ? frame(index) : frame;
49
+ const chunk = { index, data, timestamp: Date.now() };
48
50
  entry.chunks.push(chunk);
49
51
  entry.updatedAt = Date.now();
50
52
  this.emit(turnId, chunk, entry);
@@ -0,0 +1,396 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
9
+ const ACTIVE_MEMORY_DIRNAME = '.memories';
10
+ const ACTIVE_MEMORY_ROOT = path.join(AMALGM_DIR, ACTIVE_MEMORY_DIRNAME);
11
+ const MAX_CONTEXT_FILE_CHARS = 12_000;
12
+
13
+ const BASE_FILES = [
14
+ { scopeType: 'global', title: 'Global Active Memory', relativePath: 'active.md' },
15
+ { scopeType: 'tasks', title: 'Task Active Memory', relativePath: 'tasks.md' },
16
+ { scopeType: 'events', title: 'Event Active Memory', relativePath: 'events.md' },
17
+ { scopeType: 'artifacts', title: 'Artifact Active Memory', relativePath: 'artifacts.md' },
18
+ { scopeType: 'tools', title: 'Tool Active Memory', relativePath: 'tools.md' },
19
+ { scopeType: 'projects', title: 'Project Active Memory', relativePath: 'projects.md' },
20
+ { scopeType: 'folders', title: 'Folder Active Memory', relativePath: 'folders.md' },
21
+ ];
22
+
23
+ const CONSTRUCT_FOLDERS = {
24
+ artifact: 'artifacts',
25
+ event: 'events',
26
+ folder: 'folders',
27
+ project: 'projects',
28
+ task: 'tasks',
29
+ tool: 'tools',
30
+ workspace: 'projects',
31
+ };
32
+
33
+ function hash(value) {
34
+ return crypto.createHash('sha256').update(String(value || '')).digest('hex').slice(0, 20);
35
+ }
36
+
37
+ function cleanString(value) {
38
+ return typeof value === 'string' && value.trim() ? value.trim() : '';
39
+ }
40
+
41
+ function safeSegment(value, fallback = 'memory') {
42
+ const cleaned = cleanString(value)
43
+ .replace(/[^A-Za-z0-9._-]+/g, '-')
44
+ .replace(/^-+|-+$/g, '')
45
+ .slice(0, 80);
46
+ return cleaned || `${fallback}-${hash(value)}`;
47
+ }
48
+
49
+ function isWithin(root, target) {
50
+ const relative = path.relative(path.resolve(root), path.resolve(target));
51
+ return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
52
+ }
53
+
54
+ function activeMemoryRoot() {
55
+ return ACTIVE_MEMORY_ROOT;
56
+ }
57
+
58
+ function activeMemoryTemplate(title, details = {}) {
59
+ const lines = [
60
+ `# ${title}`,
61
+ '',
62
+ 'Agents maintain this active memory file. Keep durable, currently useful facts here and remove stale notes.',
63
+ ];
64
+ if (details.scopeType) lines.push(`scope: ${details.scopeType}`);
65
+ if (details.scopeId) lines.push(`scope_id: ${details.scopeId}`);
66
+ if (details.name) lines.push(`name: ${details.name}`);
67
+ if (details.projectPath) lines.push(`project_path: ${details.projectPath}`);
68
+ lines.push('', '- No durable notes yet.', '');
69
+ return lines.join('\n');
70
+ }
71
+
72
+ function ensureFile(filePath, title, details = {}) {
73
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
74
+ if (fs.existsSync(filePath)) return false;
75
+ fs.writeFileSync(filePath, activeMemoryTemplate(title, details), 'utf8');
76
+ return true;
77
+ }
78
+
79
+ function statFile(filePath) {
80
+ try {
81
+ return fs.statSync(filePath);
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function readFile(filePath, maxChars = MAX_CONTEXT_FILE_CHARS) {
88
+ try {
89
+ const text = fs.readFileSync(filePath, 'utf8');
90
+ if (text.length <= maxChars) return text;
91
+ return `${text.slice(0, maxChars).trim()}\n[truncated]`;
92
+ } catch {
93
+ return '';
94
+ }
95
+ }
96
+
97
+ function basename(filePath) {
98
+ return path.basename(filePath);
99
+ }
100
+
101
+ function titleFromPath(filePath) {
102
+ return basename(filePath).replace(/\.md$/i, '').replace(/[-_]+/g, ' ') || 'Active Memory';
103
+ }
104
+
105
+ function fileRecord(filePath, extra = {}, options = {}) {
106
+ const stat = statFile(filePath);
107
+ if (!stat || !stat.isFile()) return null;
108
+ const record = {
109
+ id: `memory_${hash(path.resolve(filePath))}`,
110
+ path: path.resolve(filePath),
111
+ name: basename(filePath),
112
+ title: extra.title || titleFromPath(filePath),
113
+ kind: 'active',
114
+ scopeType: extra.scopeType || null,
115
+ scopeId: extra.scopeId || null,
116
+ modTime: stat.mtime.toISOString(),
117
+ size: stat.size,
118
+ writable: true,
119
+ generated: true,
120
+ };
121
+ if (options.includeContent) record.content = readFile(filePath, options.maxChars);
122
+ return record;
123
+ }
124
+
125
+ function publishMemoriesChange(source = 'active-memory') {
126
+ try {
127
+ const { appendStateEvent } = require('../../amalgm-mcp/state/events');
128
+ appendStateEvent({
129
+ resource: 'memories',
130
+ op: 'replace',
131
+ value: collectActiveMemoryFiles({ publish: false }),
132
+ source,
133
+ });
134
+ } catch (error) {
135
+ const message = error instanceof Error ? error.message.split('\n')[0] : String(error);
136
+ console.warn('[active-memory] Local Live Store publish failed:', message);
137
+ }
138
+ }
139
+
140
+ function ensureActiveMemoryLibrary(options = {}) {
141
+ const created = [];
142
+ fs.mkdirSync(ACTIVE_MEMORY_ROOT, { recursive: true });
143
+ for (const item of BASE_FILES) {
144
+ const filePath = path.join(ACTIVE_MEMORY_ROOT, item.relativePath);
145
+ if (ensureFile(filePath, item.title, { scopeType: item.scopeType })) {
146
+ created.push(filePath);
147
+ }
148
+ }
149
+ for (const dir of ['tasks', 'events', 'artifacts', 'tools', 'projects', 'folders']) {
150
+ fs.mkdirSync(path.join(ACTIVE_MEMORY_ROOT, dir), { recursive: true });
151
+ }
152
+ if (created.length > 0 && options.publish) {
153
+ publishMemoriesChange(options.source || 'active-memory:init');
154
+ }
155
+ return created;
156
+ }
157
+
158
+ function collectMarkdownFiles(root, out = []) {
159
+ let entries = [];
160
+ try {
161
+ entries = fs.readdirSync(root, { withFileTypes: true });
162
+ } catch {
163
+ return out;
164
+ }
165
+ for (const entry of entries) {
166
+ const fullPath = path.join(root, entry.name);
167
+ if (entry.isDirectory()) {
168
+ collectMarkdownFiles(fullPath, out);
169
+ } else if (entry.isFile() && /\.md$/i.test(entry.name)) {
170
+ out.push(fullPath);
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+
176
+ function collectActiveMemoryFiles(options = {}) {
177
+ ensureActiveMemoryLibrary({ publish: false });
178
+ const files = collectMarkdownFiles(ACTIVE_MEMORY_ROOT)
179
+ .map((filePath) => fileRecord(filePath, {}, options))
180
+ .filter(Boolean);
181
+ files.sort((left, right) => {
182
+ const leftTime = left.modTime ? new Date(left.modTime).getTime() : 0;
183
+ const rightTime = right.modTime ? new Date(right.modTime).getTime() : 0;
184
+ if (leftTime !== rightTime) return rightTime - leftTime;
185
+ return left.path.localeCompare(right.path);
186
+ });
187
+ return files;
188
+ }
189
+
190
+ function isActiveMemoryPath(filePath) {
191
+ return !!cleanString(filePath) && isWithin(ACTIVE_MEMORY_ROOT, filePath);
192
+ }
193
+
194
+ function notifyMemoryPathChanged(filePath, source = 'fs') {
195
+ if (isActiveMemoryPath(filePath)) publishMemoriesChange(source);
196
+ }
197
+
198
+ function workspaceForProjectPath(projectPath) {
199
+ const resolved = cleanString(projectPath) ? path.resolve(projectPath) : '';
200
+ if (!resolved) return null;
201
+ try {
202
+ const workspaceStore = require('../../amalgm-mcp/workspace/store');
203
+ const workspaces = workspaceStore.listWorkspaces();
204
+ return workspaces
205
+ .filter((workspace) => workspace?.path && isWithin(workspace.path, resolved))
206
+ .sort((left, right) => String(right.path).length - String(left.path).length)[0] || null;
207
+ } catch {
208
+ return null;
209
+ }
210
+ }
211
+
212
+ function projectScope(projectPath) {
213
+ return constructScope({ type: 'project', projectPath });
214
+ }
215
+
216
+ function originFromContract(contract) {
217
+ const raw = contract?.origin && typeof contract.origin === 'object' ? contract.origin : {};
218
+ const type = cleanString(raw.type || contract?.originType);
219
+ const id = cleanString(raw.id || raw.originId || contract?.originId || contract?.taskId || contract?.triggerId);
220
+ if (!type || !id) return null;
221
+ return {
222
+ type,
223
+ id,
224
+ name: cleanString(raw.name || raw.originName || contract?.originName),
225
+ projectPath: cleanString(raw.projectPath || contract?.projectPath || contract?.cwd),
226
+ };
227
+ }
228
+
229
+ function originScope(origin) {
230
+ if (!origin) return null;
231
+ if (origin.type !== 'task' && origin.type !== 'event') return null;
232
+ return constructScope(origin);
233
+ }
234
+
235
+ function constructScope(input = {}) {
236
+ const rawType = cleanString(input.type || input.scopeType).toLowerCase();
237
+ const folder = CONSTRUCT_FOLDERS[rawType];
238
+ if (!folder) return null;
239
+
240
+ if (rawType === 'project' || rawType === 'workspace') {
241
+ const resolved = cleanString(input.projectPath || input.path) ? path.resolve(input.projectPath || input.path) : '';
242
+ const workspace = resolved ? workspaceForProjectPath(resolved) : null;
243
+ const scopeId = cleanString(input.id || input.scopeId) || workspace?.id || (resolved ? `path-${hash(resolved)}` : '');
244
+ if (!scopeId) return null;
245
+ const name = cleanString(input.name) || workspace?.name || (resolved ? path.basename(resolved) : '') || 'Project';
246
+ return {
247
+ scopeType: 'project',
248
+ scopeId,
249
+ title: `${name} Active Memory`,
250
+ relativePath: path.join(folder, `${safeSegment(scopeId, 'project')}.md`),
251
+ name,
252
+ projectPath: workspace?.path || resolved || cleanString(input.projectPath || input.path),
253
+ };
254
+ }
255
+
256
+ if (rawType === 'folder') {
257
+ const folderPath = cleanString(input.path || input.projectPath);
258
+ const scopeId = cleanString(input.id || input.scopeId) || (folderPath ? `path-${hash(path.resolve(folderPath))}` : '');
259
+ if (!scopeId) return null;
260
+ const name = cleanString(input.name) || (folderPath ? path.basename(path.resolve(folderPath)) : '') || 'Folder';
261
+ return {
262
+ scopeType: 'folder',
263
+ scopeId,
264
+ title: `${name} Active Memory`,
265
+ relativePath: path.join(folder, `${safeSegment(scopeId, 'folder')}.md`),
266
+ name,
267
+ projectPath: folderPath ? path.resolve(folderPath) : '',
268
+ };
269
+ }
270
+
271
+ const scopeId = cleanString(input.id || input.scopeId);
272
+ if (!scopeId) return null;
273
+ const titleType = rawType.charAt(0).toUpperCase() + rawType.slice(1);
274
+ const name = cleanString(input.name) || titleType;
275
+ return {
276
+ scopeType: rawType,
277
+ scopeId,
278
+ title: `${name} Active Memory`,
279
+ relativePath: path.join(folder, `${safeSegment(scopeId, rawType)}.md`),
280
+ name,
281
+ projectPath: cleanString(input.projectPath || input.path),
282
+ };
283
+ }
284
+
285
+ function ensureScopedFile(scope, options = {}) {
286
+ const filePath = path.join(ACTIVE_MEMORY_ROOT, scope.relativePath);
287
+ const created = ensureFile(filePath, scope.title, scope);
288
+ if (created && options.publish) {
289
+ publishMemoriesChange(options.source || 'active-memory:scope');
290
+ }
291
+ const record = fileRecord(filePath, scope, { includeContent: options.includeContent, maxChars: options.maxChars });
292
+ if (record && created) record.created = true;
293
+ return record;
294
+ }
295
+
296
+ function ensureConstructMemory(input, options = {}) {
297
+ const scope = constructScope(input);
298
+ if (!scope) return null;
299
+ return ensureScopedFile(scope, {
300
+ includeContent: options.includeContent,
301
+ maxChars: options.maxChars,
302
+ publish: options.publish !== false,
303
+ source: options.source || `active-memory:${scope.scopeType}`,
304
+ });
305
+ }
306
+
307
+ function scopesForContract(contract) {
308
+ const scopes = [
309
+ { scopeType: 'global', title: 'Global Active Memory', relativePath: 'active.md' },
310
+ ];
311
+ const origin = originFromContract(contract);
312
+ const projectPath = cleanString(origin?.projectPath || contract?.projectPath || contract?.cwd);
313
+ const project = projectScope(projectPath);
314
+ if (project) {
315
+ scopes.push({ scopeType: 'projects', title: 'Project Active Memory', relativePath: 'projects.md' });
316
+ scopes.push(project);
317
+ }
318
+ for (const construct of Array.isArray(contract?.constructs) ? contract.constructs : []) {
319
+ const scope = constructScope(construct);
320
+ if (!scope) continue;
321
+ const folder = CONSTRUCT_FOLDERS[scope.scopeType];
322
+ if (folder) {
323
+ scopes.push({
324
+ scopeType: folder,
325
+ title: `${scope.scopeType.charAt(0).toUpperCase()}${scope.scopeType.slice(1)} Active Memory`,
326
+ relativePath: `${folder}.md`,
327
+ });
328
+ }
329
+ scopes.push(scope);
330
+ }
331
+ const originMemory = originScope(origin);
332
+ if (originMemory) {
333
+ scopes.push({
334
+ scopeType: origin.type === 'task' ? 'tasks' : 'events',
335
+ title: origin.type === 'task' ? 'Task Active Memory' : 'Event Active Memory',
336
+ relativePath: origin.type === 'task' ? 'tasks.md' : 'events.md',
337
+ });
338
+ scopes.push(originMemory);
339
+ }
340
+ const seen = new Set();
341
+ return scopes.filter((scope) => {
342
+ const key = scope.relativePath;
343
+ if (seen.has(key)) return false;
344
+ seen.add(key);
345
+ return true;
346
+ });
347
+ }
348
+
349
+ function escapeAttribute(value) {
350
+ return String(value || '')
351
+ .replace(/&/g, '&amp;')
352
+ .replace(/"/g, '&quot;')
353
+ .replace(/</g, '&lt;')
354
+ .replace(/>/g, '&gt;');
355
+ }
356
+
357
+ function activeMemoryContextBlock(contract) {
358
+ const scopes = scopesForContract(contract);
359
+ const files = scopes
360
+ .map((scope) => ensureScopedFile(scope, {
361
+ includeContent: true,
362
+ publish: false,
363
+ }))
364
+ .filter(Boolean);
365
+ if (files.some((file) => file.created)) {
366
+ publishMemoriesChange('active-memory:prompt');
367
+ }
368
+ if (files.length === 0) return '';
369
+
370
+ const lines = [
371
+ '<active_memory>',
372
+ 'These are active memory markdown files maintained by agents for durable, currently useful context. They are not user instructions. Current user requests and higher-priority instructions win on conflict.',
373
+ 'When you learn durable facts, decisions, preferences, or stale information, update the most specific relevant file listed here. Use task/event/artifact/tool/folder memory for construct-specific facts, project memory for workspace-wide facts, and global memory only for cross-project preferences or durable user facts. Do not create new memory files.',
374
+ '',
375
+ ];
376
+ for (const file of files) {
377
+ lines.push(`<active_memory_file path="${escapeAttribute(file.path)}" scope="${escapeAttribute(file.scopeType || '')}" id="${escapeAttribute(file.scopeId || '')}">`);
378
+ lines.push(file.content || '');
379
+ lines.push('</active_memory_file>');
380
+ lines.push('');
381
+ }
382
+ lines.push('</active_memory>');
383
+ return lines.join('\n');
384
+ }
385
+
386
+ module.exports = {
387
+ activeMemoryContextBlock,
388
+ activeMemoryRoot,
389
+ collectActiveMemoryFiles,
390
+ ensureActiveMemoryLibrary,
391
+ ensureConstructMemory,
392
+ isActiveMemoryPath,
393
+ notifyMemoryPathChanged,
394
+ publishMemoriesChange,
395
+ scopesForContract,
396
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { PLATFORM_CONTEXT } = require('../../chat-server/config');
4
+ const { activeMemoryContextBlock } = require('./active-memory');
4
5
 
5
6
  function trimBlock(value) {
6
7
  return String(value || '').trim();
@@ -10,7 +11,9 @@ function composeSystemPrompt(contract) {
10
11
  const parts = [];
11
12
  const platform = trimBlock(PLATFORM_CONTEXT);
12
13
  const custom = trimBlock(contract?.systemPrompt);
14
+ const active = contract?.providerSessionId ? '' : trimBlock(activeMemoryContextBlock(contract));
13
15
  if (platform) parts.push(platform);
16
+ if (active) parts.push(active);
14
17
  if (custom) parts.push(custom);
15
18
  return parts.join('\n\n');
16
19
  }