@cjhyy/code-shell-core 0.6.0-rc.13 → 0.6.0-rc.15

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 (87) hide show
  1. package/dist/capability-control/service.d.ts +2 -0
  2. package/dist/capability-control/service.js +4 -2
  3. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  4. package/dist/cc-orchestrator/external-agent-bindings.js +1 -1
  5. package/dist/cc-orchestrator/external-agent-session-store.js +3 -1
  6. package/dist/cc-orchestrator/session-history.js +2 -2
  7. package/dist/cli/agent-server-stdio.js +7 -2
  8. package/dist/context/compaction.d.ts +8 -1
  9. package/dist/context/compaction.js +49 -4
  10. package/dist/context/manager.d.ts +11 -2
  11. package/dist/context/manager.js +64 -3
  12. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  13. package/dist/credentials/inject-credential-tool.js +29 -10
  14. package/dist/credentials/use-credential-tool.d.ts +1 -0
  15. package/dist/credentials/use-credential-tool.js +3 -0
  16. package/dist/engine/engine.d.ts +9 -0
  17. package/dist/engine/engine.js +96 -17
  18. package/dist/engine/turn-loop.d.ts +3 -1
  19. package/dist/engine/turn-loop.js +4 -2
  20. package/dist/engine/types.d.ts +8 -0
  21. package/dist/git/worktree/crud.d.ts +69 -0
  22. package/dist/git/worktree/crud.js +206 -0
  23. package/dist/git/worktree/diff.d.ts +14 -0
  24. package/dist/git/worktree/diff.js +82 -0
  25. package/dist/git/worktree/git-exec.d.ts +7 -0
  26. package/dist/git/worktree/git-exec.js +51 -0
  27. package/dist/git/worktree/index.d.ts +5 -0
  28. package/dist/git/worktree/index.js +5 -0
  29. package/dist/git/worktree/query.d.ts +42 -0
  30. package/dist/git/worktree/query.js +121 -0
  31. package/dist/git/worktree/slug.d.ts +11 -0
  32. package/dist/git/worktree/slug.js +58 -0
  33. package/dist/git/worktree.d.ts +1 -127
  34. package/dist/git/worktree.js +5 -464
  35. package/dist/index.d.ts +28 -27
  36. package/dist/index.js +24 -24
  37. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  38. package/dist/plugins/installer/checkUpdate.js +4 -2
  39. package/dist/plugins/installer/install.js +2 -0
  40. package/dist/plugins/installer/parseSource.d.ts +4 -1
  41. package/dist/plugins/installer/parseSource.js +28 -10
  42. package/dist/plugins/installer/update.d.ts +4 -1
  43. package/dist/plugins/installer/update.js +5 -3
  44. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  45. package/dist/plugins/parseMarketplaceInput.js +4 -3
  46. package/dist/preset/index.d.ts +1 -0
  47. package/dist/preset/index.js +6 -0
  48. package/dist/prompt/sections/base.md +1 -1
  49. package/dist/protocol/chat-session-manager.js +7 -0
  50. package/dist/protocol/server.d.ts +10 -0
  51. package/dist/protocol/server.js +88 -4
  52. package/dist/protocol/types.d.ts +6 -0
  53. package/dist/protocol/types.js +2 -0
  54. package/dist/runtime/background-shell.js +2 -3
  55. package/dist/services/auto-dream.d.ts +15 -4
  56. package/dist/services/auto-dream.js +20 -20
  57. package/dist/services/dream-consolidation.d.ts +2 -3
  58. package/dist/services/dream-consolidation.js +65 -15
  59. package/dist/services/extract-memories.d.ts +14 -5
  60. package/dist/services/extract-memories.js +20 -3
  61. package/dist/services/global-dream-promotion.d.ts +23 -0
  62. package/dist/services/global-dream-promotion.js +112 -0
  63. package/dist/services/memory-orchestrator.js +347 -34
  64. package/dist/session/memory.d.ts +56 -17
  65. package/dist/session/memory.js +337 -78
  66. package/dist/session/session-manager.d.ts +1 -1
  67. package/dist/session/session-manager.js +6 -6
  68. package/dist/session/transcript.d.ts +1 -1
  69. package/dist/session/transcript.js +17 -5
  70. package/dist/settings/manager.js +43 -12
  71. package/dist/settings/schema.d.ts +21 -0
  72. package/dist/settings/schema.js +12 -0
  73. package/dist/tool-system/builtin/index.js +18 -8
  74. package/dist/tool-system/builtin/memory.js +40 -8
  75. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  76. package/dist/tool-system/builtin/view-image.js +100 -19
  77. package/dist/tool-system/builtin/worktree.d.ts +2 -0
  78. package/dist/tool-system/builtin/worktree.js +59 -11
  79. package/dist/tool-system/context.d.ts +11 -1
  80. package/dist/tool-system/mcp-manager.d.ts +8 -5
  81. package/dist/tool-system/mcp-manager.js +85 -55
  82. package/dist/tool-system/path-policy.d.ts +2 -0
  83. package/dist/tool-system/path-policy.js +28 -12
  84. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  85. package/dist/tool-system/workspace-bridge.js +1 -0
  86. package/dist/types.d.ts +14 -0
  87. package/package.json +1 -1
@@ -22,6 +22,18 @@
22
22
  import { mkdirSync, existsSync, readFileSync, writeFileSync, readdirSync, renameSync, statSync, } from "node:fs";
23
23
  import { join } from "node:path";
24
24
  import { homedir } from "node:os";
25
+ import { createHash, randomUUID } from "node:crypto";
26
+ function frontmatterLine(key, value) {
27
+ return `${key}: ${JSON.stringify(value)}\n`;
28
+ }
29
+ function parseFrontmatterValue(raw) {
30
+ try {
31
+ return JSON.parse(raw);
32
+ }
33
+ catch {
34
+ return raw;
35
+ }
36
+ }
25
37
  /**
26
38
  * Drop memories older than `maxAgeDays` (by file mtime) for context injection
27
39
  * (TODO 8.1). A non-positive/undefined maxAge means no filtering. Entries with
@@ -33,7 +45,10 @@ export function filterByAge(entries, maxAgeDays, now = Date.now()) {
33
45
  return entries;
34
46
  const cutoff = now - maxAgeDays * 24 * 60 * 60 * 1000;
35
47
  // Pinned memories never age out of injection — that's the point of the pin.
36
- return entries.filter((e) => e.pinned || !e.updatedAt || e.updatedAt >= cutoff);
48
+ return entries.filter((e) => {
49
+ const updatedMs = e.mtimeMs ?? (e.updatedAt ? new Date(e.updatedAt).getTime() : Number.NaN);
50
+ return e.pinned || !Number.isFinite(updatedMs) || updatedMs <= 0 || updatedMs >= cutoff;
51
+ });
37
52
  }
38
53
  export function resolveMemoryBaseDir(override) {
39
54
  if (override)
@@ -83,37 +98,75 @@ export class MemoryManager {
83
98
  // no flat .md files and this becomes a no-op.
84
99
  this.migrateFlatLayout();
85
100
  }
86
- getMemoryDir() { return this.memoryDir; }
87
- getScope() { return this.scope; }
101
+ getMemoryDir() {
102
+ return this.memoryDir;
103
+ }
104
+ getScope() {
105
+ return this.scope;
106
+ }
88
107
  /**
89
- * Save a memory entry. Creates the file and updates the index.
108
+ * Save a memory entry. New entries get a stable id and `<id>.md`; entries
109
+ * carrying an existing id update that file even when name changes.
90
110
  */
91
- save(entry) {
92
- const fileName = this.slugify(entry.name) + ".md";
93
- const filePath = join(this.memoryDir, fileName);
111
+ save(entry, opts = {}) {
94
112
  const nowIso = new Date().toISOString();
95
- // Overwrite = UPDATE: preserve the original `created` and accumulated
96
- // `usageCount` of the file being replaced (only `created` is load-bearing
97
- // for TTL; carrying usageCount avoids resetting a popular memory's score).
98
- // explicit entry fields win over the existing file (lets callers force a value).
99
- const existing = existsSync(filePath) ? this.loadFile(fileName) : null;
100
- const created = entry.created ?? existing?.created ?? nowIso;
101
- const usageCount = entry.usageCount ?? existing?.usageCount ?? 0;
102
- const lastUsed = entry.lastUsed ?? existing?.lastUsed ?? created;
113
+ const inputId = entry.id;
114
+ const syntheticLegacy = inputId ? this.parseSyntheticLegacyId(inputId) : null;
115
+ const existing = inputId && !syntheticLegacy
116
+ ? (this.findById(inputId) ?? null)
117
+ : syntheticLegacy && syntheticLegacy.scope === this.scope
118
+ ? this.loadFile(syntheticLegacy.fileName)
119
+ : null;
120
+ const id = inputId && !syntheticLegacy ? inputId : this.generateId();
121
+ const fileName = existing?.fileName ?? this.availableFileNameForId(id);
122
+ const filePath = join(this.memoryDir, fileName);
123
+ const origin = opts.forceOrigin ?? entry.origin ?? existing?.origin ?? "manual";
124
+ const pinned = entry.pinned ?? existing?.pinned;
125
+ const createdAt = entry.createdAt ?? entry.created ?? existing?.createdAt ?? nowIso;
126
+ const useCount = entry.useCount ?? entry.usageCount ?? existing?.useCount ?? 0;
127
+ const lastUsedAt = entry.lastUsedAt ?? entry.lastUsed ?? existing?.lastUsedAt ?? createdAt;
103
128
  const originProject = entry.originProject ?? existing?.originProject;
104
- // pinned/origin are written only when meaningful so legacy-shaped files
105
- // stay byte-identical for unpinned manual saves. Lifecycle fields are
106
- // always written now (created/lastUsed/usageCount) — they're how TTL works.
129
+ const promotionKey = entry.promotionKey ?? existing?.promotionKey;
130
+ const originProjects = entry.originProjects ?? existing?.originProjects;
131
+ const evidenceCount = entry.evidenceCount ?? existing?.evidenceCount;
132
+ const firstSeenAt = entry.firstSeenAt ?? existing?.firstSeenAt;
133
+ const lastSeenAt = entry.lastSeenAt ?? existing?.lastSeenAt;
134
+ const promotionReason = entry.promotionReason ?? existing?.promotionReason;
135
+ const promotionStatus = entry.promotionStatus ?? existing?.promotionStatus;
136
+ const promotionSourceId = entry.promotionSourceId ?? existing?.promotionSourceId;
137
+ const contentChanged = existing != null &&
138
+ (existing.name !== entry.name ||
139
+ existing.description !== entry.description ||
140
+ existing.type !== entry.type ||
141
+ existing.content !== entry.content);
142
+ const incrementUpdateCount = opts.incrementUpdateCount !== false && contentChanged;
143
+ const updateCount = (entry.updateCount ?? existing?.updateCount ?? 0) + (incrementUpdateCount ? 1 : 0);
144
+ const updatedAt = incrementUpdateCount
145
+ ? nowIso
146
+ : (entry.updatedAt ?? existing?.updatedAt ?? nowIso);
107
147
  const content = `---\n` +
108
- `name: ${entry.name}\n` +
109
- `description: ${entry.description}\n` +
110
- `type: ${entry.type}\n` +
111
- (entry.pinned ? `pinned: true\n` : "") +
112
- (entry.origin ? `origin: ${entry.origin}\n` : "") +
113
- (originProject ? `originProject: ${originProject}\n` : "") +
114
- `created: ${created}\n` +
115
- `lastUsed: ${lastUsed}\n` +
116
- `usageCount: ${usageCount}\n` +
148
+ frontmatterLine("id", id) +
149
+ frontmatterLine("name", entry.name) +
150
+ frontmatterLine("description", entry.description) +
151
+ frontmatterLine("type", entry.type) +
152
+ frontmatterLine("origin", origin) +
153
+ (pinned ? frontmatterLine("pinned", true) : "") +
154
+ (originProject ? frontmatterLine("originProject", originProject) : "") +
155
+ (promotionKey ? frontmatterLine("promotionKey", promotionKey) : "") +
156
+ (originProjects && originProjects.length > 0
157
+ ? frontmatterLine("originProjects", originProjects)
158
+ : "") +
159
+ (typeof evidenceCount === "number" ? frontmatterLine("evidenceCount", evidenceCount) : "") +
160
+ (firstSeenAt ? frontmatterLine("firstSeenAt", firstSeenAt) : "") +
161
+ (lastSeenAt ? frontmatterLine("lastSeenAt", lastSeenAt) : "") +
162
+ (promotionReason ? frontmatterLine("promotionReason", promotionReason) : "") +
163
+ (promotionStatus ? frontmatterLine("promotionStatus", promotionStatus) : "") +
164
+ (promotionSourceId ? frontmatterLine("promotionSourceId", promotionSourceId) : "") +
165
+ frontmatterLine("createdAt", createdAt) +
166
+ frontmatterLine("updatedAt", updatedAt) +
167
+ frontmatterLine("lastUsedAt", lastUsedAt) +
168
+ frontmatterLine("useCount", useCount) +
169
+ frontmatterLine("updateCount", updateCount) +
117
170
  `---\n\n` +
118
171
  `${entry.content}\n`;
119
172
  writeFileSync(filePath, content, "utf-8");
@@ -140,6 +193,25 @@ export class MemoryManager {
140
193
  }
141
194
  return entries;
142
195
  }
196
+ findById(id) {
197
+ return this.loadAll().find((e) => e.id === id);
198
+ }
199
+ find(nameOrFileOrId) {
200
+ return this.loadAll().find((e) => e.id === nameOrFileOrId || e.name === nameOrFileOrId || e.fileName === nameOrFileOrId);
201
+ }
202
+ isOwnedBy(entry, allowedOrigins) {
203
+ if (!entry)
204
+ return false;
205
+ return allowedOrigins.includes(entry.origin ?? "manual");
206
+ }
207
+ deleteIfOwned(nameOrFileOrId, allowedOrigins) {
208
+ const entry = this.find(nameOrFileOrId);
209
+ if (!entry)
210
+ return "not_found";
211
+ if (!this.isOwnedBy(entry, allowedOrigins))
212
+ return "protected";
213
+ return this.delete(entry.id ?? entry.fileName) ? "deleted" : "not_found";
214
+ }
143
215
  /**
144
216
  * Load a single memory file.
145
217
  */
@@ -165,32 +237,128 @@ export class MemoryManager {
165
237
  return null;
166
238
  const frontmatter = frontmatterMatch[1];
167
239
  const content = frontmatterMatch[2].trim();
168
- const name = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim() ?? fileName;
169
- const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim() ?? "";
170
- const type = (frontmatter.match(/type:\s*(.+)/)?.[1]?.trim() ?? "project");
171
- const pinned = frontmatter.match(/pinned:\s*(.+)/)?.[1]?.trim() === "true";
240
+ const readRaw = (key) => frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"))?.[1]?.trim();
241
+ const readValue = (key) => {
242
+ const raw = readRaw(key);
243
+ return raw === undefined ? undefined : parseFrontmatterValue(raw);
244
+ };
245
+ const read = (key) => {
246
+ const value = readValue(key);
247
+ if (value === undefined || value === null)
248
+ return undefined;
249
+ if (Array.isArray(value))
250
+ return value.map(String).join(",");
251
+ return String(value);
252
+ };
253
+ const readList = (key) => {
254
+ const raw = readRaw(key);
255
+ if (raw !== undefined && raw.length > 0) {
256
+ const value = parseFrontmatterValue(raw);
257
+ if (Array.isArray(value)) {
258
+ const values = value.map((item) => String(item)).filter(Boolean);
259
+ return values.length > 0 ? values : undefined;
260
+ }
261
+ if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
262
+ const values = value
263
+ .slice(1, -1)
264
+ .split(",")
265
+ .map((item) => item.trim().replace(/^["']|["']$/g, ""))
266
+ .filter(Boolean);
267
+ return values.length > 0 ? values : undefined;
268
+ }
269
+ return [String(value)];
270
+ }
271
+ const block = frontmatter.match(new RegExp(`^${key}:\\s*\\n((?:\\s+-\\s+.*\\n?)*)`, "m"));
272
+ if (block?.[1]) {
273
+ const values = block[1]
274
+ .split(/\n/)
275
+ .map((line) => line.match(/^\s+-\s+(.+)\s*$/)?.[1]?.trim())
276
+ .filter((value) => Boolean(value));
277
+ return values.length > 0 ? values : undefined;
278
+ }
279
+ return undefined;
280
+ };
281
+ const readNumber = (key) => {
282
+ const value = readValue(key);
283
+ if (value === undefined || value === null || value === "")
284
+ return undefined;
285
+ if (typeof value === "number")
286
+ return Number.isFinite(value) ? value : undefined;
287
+ const parsed = parseInt(String(value), 10);
288
+ return Number.isFinite(parsed) ? parsed : undefined;
289
+ };
290
+ const readBoolean = (key) => {
291
+ const value = readValue(key);
292
+ return value === true || value === "true";
293
+ };
294
+ const id = read("id") ?? `legacy:${scope}:${fileName}`;
295
+ const name = read("name") ?? fileName;
296
+ const description = read("description") ?? "";
297
+ const type = (read("type") ?? "project");
298
+ const pinned = readBoolean("pinned");
172
299
  // Anchor with ^…/m so `origin:` doesn't accidentally match the
173
300
  // `originProject:` line (and vice-versa).
174
- const originRaw = frontmatter.match(/^origin:\s*(.+)/m)?.[1]?.trim();
175
- const origin = originRaw === "auto" || originRaw === "manual" ? originRaw : undefined;
176
- const originProject = frontmatter.match(/^originProject:\s*(.+)/m)?.[1]?.trim() || undefined;
177
- let updatedAt = 0;
301
+ const originRaw = read("origin");
302
+ const origin = originRaw === "auto" || originRaw === "manual" || originRaw === "dream"
303
+ ? originRaw
304
+ : "manual";
305
+ const originProject = read("originProject") || undefined;
306
+ let mtimeMs = 0;
178
307
  try {
179
- updatedAt = statSync(filePath).mtimeMs;
308
+ mtimeMs = statSync(filePath).mtimeMs;
180
309
  }
181
310
  catch {
182
311
  // mtime best-effort; 0 = unknown (never filtered out by maxAge).
183
312
  }
184
313
  // Lifecycle fields. Legacy files lack them → fall back to mtime so a TTL
185
314
  // sweep treats them as "last used = file age" rather than "never used".
186
- const mtimeIso = updatedAt > 0 ? new Date(updatedAt).toISOString() : undefined;
187
- const created = frontmatter.match(/created:\s*(.+)/)?.[1]?.trim() || mtimeIso;
188
- const lastUsed = frontmatter.match(/lastUsed:\s*(.+)/)?.[1]?.trim() || created;
189
- const usageRaw = frontmatter.match(/usageCount:\s*(\d+)/)?.[1];
190
- const usageCount = usageRaw ? parseInt(usageRaw, 10) : 0;
315
+ const mtimeIso = mtimeMs > 0 ? new Date(mtimeMs).toISOString() : undefined;
316
+ const createdAt = read("createdAt") ?? read("created") ?? mtimeIso;
317
+ const updatedAt = read("updatedAt") ?? mtimeIso ?? createdAt;
318
+ const lastUsedAt = read("lastUsedAt") ?? read("lastUsed") ?? createdAt;
319
+ const useRaw = read("useCount") ?? read("usageCount");
320
+ const updateRaw = read("updateCount");
321
+ const useCount = useRaw ? parseInt(useRaw, 10) : 0;
322
+ const updateCount = updateRaw ? parseInt(updateRaw, 10) : 0;
323
+ const promotionKey = read("promotionKey");
324
+ const originProjects = readList("originProjects");
325
+ const evidenceCount = readNumber("evidenceCount");
326
+ const firstSeenAt = read("firstSeenAt");
327
+ const lastSeenAt = read("lastSeenAt");
328
+ const promotionReason = read("promotionReason");
329
+ const promotionStatusRaw = read("promotionStatus");
330
+ const promotionStatus = promotionStatusRaw === "pending" || promotionStatusRaw === "rejected"
331
+ ? promotionStatusRaw
332
+ : undefined;
333
+ const promotionSourceId = read("promotionSourceId");
191
334
  return {
192
- name, description, type, content, fileName, scope,
193
- updatedAt, pinned, origin, usageCount, lastUsed, created, originProject,
335
+ id,
336
+ name,
337
+ description,
338
+ type,
339
+ content,
340
+ fileName,
341
+ scope,
342
+ updatedAt,
343
+ mtimeMs,
344
+ pinned,
345
+ origin,
346
+ useCount,
347
+ updateCount,
348
+ lastUsedAt,
349
+ createdAt,
350
+ usageCount: useCount,
351
+ lastUsed: lastUsedAt,
352
+ created: createdAt,
353
+ originProject,
354
+ promotionKey,
355
+ originProjects,
356
+ evidenceCount,
357
+ firstSeenAt,
358
+ lastSeenAt,
359
+ promotionReason,
360
+ promotionStatus,
361
+ promotionSourceId,
194
362
  };
195
363
  }
196
364
  catch {
@@ -204,7 +372,7 @@ export class MemoryManager {
204
372
  */
205
373
  delete(nameOrFile) {
206
374
  const entries = this.loadAll();
207
- const entry = entries.find((e) => e.name === nameOrFile || e.fileName === nameOrFile);
375
+ const entry = entries.find((e) => e.id === nameOrFile || e.name === nameOrFile || e.fileName === nameOrFile);
208
376
  if (!entry)
209
377
  return false;
210
378
  try {
@@ -228,21 +396,25 @@ export class MemoryManager {
228
396
  * never ages out. Idempotent-safe: missing entry → false, no throw.
229
397
  */
230
398
  recordRecall(nameOrFile, now = new Date()) {
231
- const entry = this.loadAll().find((e) => e.name === nameOrFile || e.fileName === nameOrFile);
399
+ const entry = this.loadAll().find((e) => e.id === nameOrFile || e.name === nameOrFile || e.fileName === nameOrFile);
232
400
  if (!entry)
233
401
  return false;
234
402
  try {
235
403
  this.save({
404
+ id: entry.id,
236
405
  name: entry.name,
237
406
  description: entry.description,
238
407
  type: entry.type,
239
408
  content: entry.content,
240
409
  pinned: entry.pinned,
241
410
  origin: entry.origin,
242
- created: entry.created,
243
- usageCount: (entry.usageCount ?? 0) + 1,
244
- lastUsed: now.toISOString(),
245
- });
411
+ createdAt: entry.createdAt,
412
+ updatedAt: entry.updatedAt,
413
+ useCount: (entry.useCount ?? 0) + 1,
414
+ updateCount: entry.updateCount,
415
+ lastUsedAt: now.toISOString(),
416
+ originProject: entry.originProject,
417
+ }, { incrementUpdateCount: false });
246
418
  return true;
247
419
  }
248
420
  catch {
@@ -265,7 +437,7 @@ export class MemoryManager {
265
437
  continue; // stable types are exempt
266
438
  if (e.pinned)
267
439
  continue; // pinned exempt
268
- const lastUsedMs = e.lastUsed ? new Date(e.lastUsed).getTime() : NaN;
440
+ const lastUsedMs = e.lastUsedAt ? new Date(e.lastUsedAt).getTime() : NaN;
269
441
  // Unknown lastUsed (unparseable) → keep, never prune on missing data.
270
442
  if (!Number.isFinite(lastUsedMs))
271
443
  continue;
@@ -277,13 +449,14 @@ export class MemoryManager {
277
449
  return pruned;
278
450
  }
279
451
  /**
280
- * Approve a pending memory: move it from the pending scope into the user scope
281
- * of the SAME memory root (pending only ever exists at the global root — the
282
- * approval only gates global writes). Must be called on a pending-scope
283
- * manager. Returns the new user filename, or null if not found / wrong scope.
452
+ * Approve a pending memory: move it from the pending scope into the global
453
+ * dream scope of the SAME memory root. Pending only ever exists at the global
454
+ * root; approval gates whether a suggested global dream becomes injected.
455
+ * Must be called on a pending-scope manager. Returns the new global dream
456
+ * filename, or null if not found / wrong scope.
284
457
  *
285
- * 审批门 (用户拍板): the only path that moves an auto-extracted memory into the
286
- * curated, injected global user store.
458
+ * 审批门 (用户拍板): the only path that moves a suggested global dream into the
459
+ * injected global dream store.
287
460
  */
288
461
  approvePending(nameOrFile) {
289
462
  return this.movePending(nameOrFile, "global");
@@ -295,27 +468,86 @@ export class MemoryManager {
295
468
  demotePending(nameOrFile) {
296
469
  return this.movePending(nameOrFile, "project");
297
470
  }
298
- /** Shared move for approve(→global user)/demote(→origin-project user). */
471
+ /**
472
+ * Reject a pending global-dream suggestion. The project dream entry stays in
473
+ * place, but is marked so the same source is not suggested again.
474
+ */
475
+ rejectPending(nameOrFile) {
476
+ if (this.scope !== "pending")
477
+ return false;
478
+ const entry = this.find(nameOrFile);
479
+ if (!entry)
480
+ return false;
481
+ if (entry.originProject) {
482
+ const projectDream = new MemoryManager({
483
+ baseDir: this.baseDir,
484
+ projectDir: entry.originProject,
485
+ scope: "dream",
486
+ });
487
+ const source = (entry.promotionSourceId ? projectDream.findById(entry.promotionSourceId) : undefined) ??
488
+ projectDream.loadAll().find((candidate) => candidate.name === entry.name);
489
+ if (source && source.origin !== "manual") {
490
+ projectDream.save({
491
+ id: source.id,
492
+ name: source.name,
493
+ description: source.description,
494
+ type: source.type,
495
+ content: source.content,
496
+ origin: source.origin ?? "dream",
497
+ pinned: source.pinned,
498
+ createdAt: source.createdAt,
499
+ updatedAt: source.updatedAt,
500
+ lastUsedAt: source.lastUsedAt,
501
+ useCount: source.useCount,
502
+ updateCount: source.updateCount,
503
+ originProject: source.originProject,
504
+ originProjects: source.originProjects,
505
+ evidenceCount: source.evidenceCount,
506
+ firstSeenAt: source.firstSeenAt,
507
+ lastSeenAt: source.lastSeenAt,
508
+ promotionReason: entry.promotionReason ?? source.promotionReason,
509
+ promotionStatus: "rejected",
510
+ }, { incrementUpdateCount: false, forceOrigin: source.origin ?? "dream" });
511
+ }
512
+ }
513
+ return this.delete(entry.id ?? entry.name);
514
+ }
515
+ /** Shared move for approve(→global dream)/demote(→origin-project user). */
299
516
  movePending(nameOrFile, dest) {
300
517
  if (this.scope !== "pending")
301
518
  return null;
302
- const entry = this.loadAll().find((e) => e.name === nameOrFile || e.fileName === nameOrFile);
519
+ const entry = this.find(nameOrFile);
303
520
  if (!entry)
304
521
  return null;
305
- const targetMgr = dest === "project" && entry.originProject
306
- ? new MemoryManager({ baseDir: this.baseDir, projectDir: entry.originProject, scope: "user" })
307
- : new MemoryManager({ baseDir: this.baseDir, scope: "user" }); // global, or fallback
522
+ const targetMgr = dest === "global"
523
+ ? new MemoryManager({ baseDir: this.baseDir, scope: "dream" })
524
+ : dest === "project" && entry.originProject
525
+ ? new MemoryManager({
526
+ baseDir: this.baseDir,
527
+ projectDir: entry.originProject,
528
+ scope: "user",
529
+ })
530
+ : new MemoryManager({ baseDir: this.baseDir, scope: "user" }); // demote fallback
531
+ const destinationOrigin = dest === "global" ? "dream" : (entry.origin ?? "auto");
308
532
  const fileName = targetMgr.save({
309
533
  name: entry.name,
310
534
  description: entry.description,
311
535
  type: entry.type,
312
536
  content: entry.content,
313
- origin: entry.origin ?? "auto",
314
- created: entry.created,
315
- lastUsed: entry.lastUsed,
316
- usageCount: entry.usageCount,
317
- // originProject is no longer needed once it has landed in a real store.
318
- });
537
+ origin: destinationOrigin,
538
+ createdAt: entry.createdAt,
539
+ updatedAt: entry.updatedAt,
540
+ lastUsedAt: entry.lastUsedAt,
541
+ useCount: entry.useCount,
542
+ updateCount: entry.updateCount,
543
+ originProjects: entry.originProjects ?? (entry.originProject ? [entry.originProject] : []),
544
+ evidenceCount: entry.evidenceCount,
545
+ firstSeenAt: entry.firstSeenAt,
546
+ lastSeenAt: entry.lastSeenAt,
547
+ promotionReason: entry.promotionReason,
548
+ // originProject/promotionSourceId are no longer needed once it has landed
549
+ // in a real store.
550
+ }, { forceOrigin: destinationOrigin });
319
551
  this.delete(entry.name); // soft-delete the pending copy
320
552
  return fileName;
321
553
  }
@@ -337,9 +569,11 @@ export class MemoryManager {
337
569
  content: entry.content,
338
570
  origin: entry.origin,
339
571
  pinned: entry.pinned,
340
- created: entry.created,
341
- lastUsed: entry.lastUsed,
342
- usageCount: entry.usageCount,
572
+ createdAt: entry.createdAt,
573
+ updatedAt: entry.updatedAt,
574
+ lastUsedAt: entry.lastUsedAt,
575
+ useCount: entry.useCount,
576
+ updateCount: entry.updateCount,
343
577
  });
344
578
  this.delete(entry.name); // remove from the project store
345
579
  return fileName;
@@ -517,16 +751,41 @@ export class MemoryManager {
517
751
  for (const [fileName, meta] of cache) {
518
752
  lines.push(`- [${meta.name}](${fileName}) — ${meta.description}`);
519
753
  }
520
- const content = lines.length > 0
521
- ? lines.join("\n") + "\n"
522
- : "(no memories stored)\n";
754
+ const content = lines.length > 0 ? lines.join("\n") + "\n" : "(no memories stored)\n";
523
755
  writeFileSync(this.indexPath, content, "utf-8");
524
756
  }
525
- slugify(text) {
526
- return text
757
+ generateId() {
758
+ return `mem_${randomUUID().replace(/-/g, "")}`;
759
+ }
760
+ availableFileNameForId(id) {
761
+ let attempt = 0;
762
+ while (attempt < 100) {
763
+ const fileName = this.fileNameForId(id, attempt);
764
+ const existing = this.loadFile(fileName);
765
+ if (!existing || existing.id === id)
766
+ return fileName;
767
+ attempt++;
768
+ }
769
+ throw new Error(`Unable to allocate a memory filename for id "${id}"`);
770
+ }
771
+ fileNameForId(id, collisionAttempt = 0) {
772
+ if (collisionAttempt === 0 && /^[a-z0-9][a-z0-9_.-]*$/.test(id) && !id.includes("..")) {
773
+ return `${id}.md`;
774
+ }
775
+ const safeStem = id
527
776
  .toLowerCase()
528
- .replace(/[^a-z0-9]+/g, "-")
529
- .replace(/^-|-$/g, "")
530
- .slice(0, 60);
777
+ .replace(/[^a-z0-9_.-]/g, "_")
778
+ .replace(/_+/g, "_")
779
+ .replace(/^[._-]+|[._-]+$/g, "")
780
+ .slice(0, 80) || "memory";
781
+ const hashInput = collisionAttempt === 0 ? id : `${id}:${collisionAttempt}`;
782
+ const hash = createHash("sha256").update(hashInput).digest("hex").slice(0, 12);
783
+ return `${safeStem}-${hash}.md`;
784
+ }
785
+ parseSyntheticLegacyId(id) {
786
+ const match = id.match(/^legacy:(user|dream|pending):(.+)$/);
787
+ if (!match)
788
+ return null;
789
+ return { scope: match[1], fileName: match[2] };
531
790
  }
532
791
  }
@@ -90,7 +90,7 @@ export declare class SessionManager {
90
90
  * if the branch is gone the workspace pointer is reset to main and a warning
91
91
  * message is returned for the host to surface before continuing.
92
92
  */
93
- resolveSessionWorkspaceForResume(sessionId: string): SessionWorkspaceResumeResolution;
93
+ resolveSessionWorkspaceForResume(sessionId: string): Promise<SessionWorkspaceResumeResolution>;
94
94
  /**
95
95
  * Cheap "does this session have a persisted goal?" probe — reads only
96
96
  * state.json, NOT the transcript (like readCwd). A persistent goal lives ONLY
@@ -76,7 +76,7 @@ function isSessionWorkspace(value) {
76
76
  wt.baseRef.length > 0 &&
77
77
  wt.createdBy === "codeshell");
78
78
  }
79
- function validateResumeWorktreeRoot(root) {
79
+ async function validateResumeWorktreeRoot(root) {
80
80
  if (!existsSync(root))
81
81
  return "no longer exists";
82
82
  let stat;
@@ -90,7 +90,7 @@ function validateResumeWorktreeRoot(root) {
90
90
  return "is not a valid git worktree (symbolic link)";
91
91
  if (!stat.isDirectory())
92
92
  return "is not a valid git worktree (not a directory)";
93
- if (!isGitWorktreeRoot(root))
93
+ if (!(await isGitWorktreeRoot(root)))
94
94
  return "is not a valid git worktree";
95
95
  return null;
96
96
  }
@@ -284,7 +284,7 @@ export class SessionManager {
284
284
  * if the branch is gone the workspace pointer is reset to main and a warning
285
285
  * message is returned for the host to surface before continuing.
286
286
  */
287
- resolveSessionWorkspaceForResume(sessionId) {
287
+ async resolveSessionWorkspaceForResume(sessionId) {
288
288
  assertSafeSessionId(sessionId);
289
289
  const stateFile = join(this.sessionsDir, sessionId, "state.json");
290
290
  if (!existsSync(stateFile)) {
@@ -312,13 +312,13 @@ export class SessionManager {
312
312
  reason: isSessionWorkspace(state.workspace) ? "main" : "legacy",
313
313
  };
314
314
  }
315
- const invalidWorktreeReason = validateResumeWorktreeRoot(workspace.root);
315
+ const invalidWorktreeReason = await validateResumeWorktreeRoot(workspace.root);
316
316
  if (!invalidWorktreeReason) {
317
317
  return { ok: true, cwd: workspace.root, workspace, reason: "worktree" };
318
318
  }
319
319
  const branch = workspace.worktree?.branch;
320
320
  const mainRoot = typeof state.cwd === "string" && state.cwd.length > 0 ? state.cwd : workspace.root;
321
- if (branch && existsSync(mainRoot) && branchExists(mainRoot, branch)) {
321
+ if (branch && existsSync(mainRoot) && (await branchExists(mainRoot, branch))) {
322
322
  return {
323
323
  ok: false,
324
324
  cwd: mainRoot,
@@ -487,7 +487,7 @@ export class SessionManager {
487
487
  continue;
488
488
  const transcriptFile = join(this.sessionsDir, dir, "transcript.jsonl");
489
489
  let lastActiveAt;
490
- let transcriptExists = false;
490
+ let transcriptExists;
491
491
  try {
492
492
  transcriptExists = existsSync(transcriptFile);
493
493
  if (transcriptExists) {
@@ -27,7 +27,7 @@ export declare class Transcript {
27
27
  }): TranscriptEvent;
28
28
  hasClientMessageId(clientMessageId: string): boolean;
29
29
  appendToolUse(toolName: string, toolCallId: string, args: Record<string, unknown>): TranscriptEvent;
30
- appendToolResult(toolCallId: string, toolName: string, result?: string, error?: string): TranscriptEvent;
30
+ appendToolResult(toolCallId: string, toolName: string, result?: string, error?: string, contentBlocks?: ContentBlock[]): TranscriptEvent;
31
31
  /** Anchor for a spawned sub-agent (see TranscriptEventType "subagent").
32
32
  * Written at spawn time so replay can rebuild the sub-agent's card from
33
33
  * sessions/<agentId>/ — agentId === the sub-agent's session id. */
@@ -10,7 +10,9 @@ export class Transcript {
10
10
  events = [];
11
11
  filePath;
12
12
  currentTurn = 0;
13
- getFilePath() { return this.filePath; }
13
+ getFilePath() {
14
+ return this.filePath;
15
+ }
14
16
  constructor(filePath) {
15
17
  this.filePath = filePath;
16
18
  mkdirSync(dirname(filePath), { recursive: true });
@@ -66,8 +68,14 @@ export class Transcript {
66
68
  appendToolUse(toolName, toolCallId, args) {
67
69
  return this.append("tool_use", { toolName, toolCallId, args });
68
70
  }
69
- appendToolResult(toolCallId, toolName, result, error) {
70
- return this.append("tool_result", { toolCallId, toolName, result, error });
71
+ appendToolResult(toolCallId, toolName, result, error, contentBlocks) {
72
+ return this.append("tool_result", {
73
+ toolCallId,
74
+ toolName,
75
+ result,
76
+ error,
77
+ ...(contentBlocks && contentBlocks.length > 0 ? { contentBlocks } : {}),
78
+ });
71
79
  }
72
80
  /** Anchor for a spawned sub-agent (see TranscriptEventType "subagent").
73
81
  * Written at spawn time so replay can rebuild the sub-agent's card from
@@ -125,13 +133,17 @@ export class Transcript {
125
133
  break;
126
134
  }
127
135
  case "tool_result": {
128
- const { toolCallId, result, error } = event.data;
136
+ const { toolCallId, result, error, contentBlocks } = event.data;
129
137
  // Find if there's already a user message with tool_results to append to
130
138
  const lastMsg = messages[messages.length - 1];
131
139
  const block = {
132
140
  type: "tool_result",
133
141
  tool_use_id: toolCallId,
134
- content: error ? `Error: ${error}` : result ?? "(no output)",
142
+ content: error
143
+ ? `Error: ${error}`
144
+ : Array.isArray(contentBlocks) && contentBlocks.length > 0
145
+ ? contentBlocks
146
+ : (result ?? "(no output)"),
135
147
  };
136
148
  if (lastMsg?.role === "user" && Array.isArray(lastMsg.content)) {
137
149
  lastMsg.content.push(block);