@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.
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/external-agent-bindings.js +1 -1
- package/dist/cc-orchestrator/external-agent-session-store.js +3 -1
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +7 -2
- package/dist/context/compaction.d.ts +8 -1
- package/dist/context/compaction.js +49 -4
- package/dist/context/manager.d.ts +11 -2
- package/dist/context/manager.js +64 -3
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +29 -10
- package/dist/credentials/use-credential-tool.d.ts +1 -0
- package/dist/credentials/use-credential-tool.js +3 -0
- package/dist/engine/engine.d.ts +9 -0
- package/dist/engine/engine.js +96 -17
- package/dist/engine/turn-loop.d.ts +3 -1
- package/dist/engine/turn-loop.js +4 -2
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -127
- package/dist/git/worktree.js +5 -464
- package/dist/index.d.ts +28 -27
- package/dist/index.js +24 -24
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +6 -0
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.js +7 -0
- package/dist/protocol/server.d.ts +10 -0
- package/dist/protocol/server.js +88 -4
- package/dist/protocol/types.d.ts +6 -0
- package/dist/protocol/types.js +2 -0
- package/dist/runtime/background-shell.js +2 -3
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +56 -17
- package/dist/session/memory.js +337 -78
- package/dist/session/session-manager.d.ts +1 -1
- package/dist/session/session-manager.js +6 -6
- package/dist/session/transcript.d.ts +1 -1
- package/dist/session/transcript.js +17 -5
- package/dist/settings/manager.js +43 -12
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/tool-system/builtin/index.js +18 -8
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +100 -19
- package/dist/tool-system/builtin/worktree.d.ts +2 -0
- package/dist/tool-system/builtin/worktree.js +59 -11
- package/dist/tool-system/context.d.ts +11 -1
- package/dist/tool-system/mcp-manager.d.ts +8 -5
- package/dist/tool-system/mcp-manager.js +85 -55
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +28 -12
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
package/dist/session/memory.js
CHANGED
|
@@ -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) =>
|
|
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() {
|
|
87
|
-
|
|
101
|
+
getMemoryDir() {
|
|
102
|
+
return this.memoryDir;
|
|
103
|
+
}
|
|
104
|
+
getScope() {
|
|
105
|
+
return this.scope;
|
|
106
|
+
}
|
|
88
107
|
/**
|
|
89
|
-
* Save a memory entry.
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
(entry.
|
|
112
|
-
(
|
|
113
|
-
(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
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 =
|
|
175
|
-
const origin = originRaw === "auto" || originRaw === "manual"
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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 =
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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.
|
|
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
|
|
281
|
-
* of the SAME memory root
|
|
282
|
-
* approval
|
|
283
|
-
*
|
|
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
|
|
286
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
519
|
+
const entry = this.find(nameOrFile);
|
|
303
520
|
if (!entry)
|
|
304
521
|
return null;
|
|
305
|
-
const targetMgr = dest === "
|
|
306
|
-
? new MemoryManager({ baseDir: this.baseDir,
|
|
307
|
-
:
|
|
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:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
526
|
-
return
|
|
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-
|
|
529
|
-
.replace(
|
|
530
|
-
.
|
|
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
|
|
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() {
|
|
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", {
|
|
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
|
|
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);
|