@cjhyy/code-shell-core 0.6.0-rc.14 → 0.6.0-rc.16
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/manager.d.ts +14 -2
- package/dist/context/manager.js +83 -8
- 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 +115 -25
- package/dist/engine/turn-loop.d.ts +3 -1
- package/dist/engine/turn-loop.js +11 -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 +90 -5
- package/dist/protocol/types.d.ts +6 -0
- package/dist/protocol/types.js +2 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- 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 +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +1 -1
- package/dist/session/session-manager.js +6 -6
- package/dist/settings/manager.js +43 -12
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/background-jobs.d.ts +2 -0
- package/dist/tool-system/builtin/background-jobs.js +4 -0
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/index.js +18 -8
- package/dist/tool-system/builtin/memory.js +40 -8
- 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 +20 -0
- package/package.json +1 -1
package/dist/session/memory.js
CHANGED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
* auto-extracted entries (extract-memories output). LLM may
|
|
13
13
|
* only modify these through permission-gated tool calls.
|
|
14
14
|
* - dream/ The dream pipeline's workspace. The LLM is free to add /
|
|
15
|
-
* merge / delete entries here
|
|
15
|
+
* merge / delete entries here. The dream loop may ALSO maintain
|
|
16
|
+
* user/ entries whose origin is `auto` or `dream` (dedup/merge/
|
|
17
|
+
* improve), but origin:`manual` user entries — things the user
|
|
18
|
+
* explicitly asked to remember — are never touched by dream.
|
|
19
|
+
* See dream-consolidation.ts (the origin guard at ~:240).
|
|
16
20
|
*
|
|
17
21
|
* MEMORY.md is the index file (one per scope).
|
|
18
22
|
*
|
|
@@ -22,6 +26,18 @@
|
|
|
22
26
|
import { mkdirSync, existsSync, readFileSync, writeFileSync, readdirSync, renameSync, statSync, } from "node:fs";
|
|
23
27
|
import { join } from "node:path";
|
|
24
28
|
import { homedir } from "node:os";
|
|
29
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
30
|
+
function frontmatterLine(key, value) {
|
|
31
|
+
return `${key}: ${JSON.stringify(value)}\n`;
|
|
32
|
+
}
|
|
33
|
+
function parseFrontmatterValue(raw) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(raw);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return raw;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
25
41
|
/**
|
|
26
42
|
* Drop memories older than `maxAgeDays` (by file mtime) for context injection
|
|
27
43
|
* (TODO 8.1). A non-positive/undefined maxAge means no filtering. Entries with
|
|
@@ -33,7 +49,10 @@ export function filterByAge(entries, maxAgeDays, now = Date.now()) {
|
|
|
33
49
|
return entries;
|
|
34
50
|
const cutoff = now - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
35
51
|
// Pinned memories never age out of injection — that's the point of the pin.
|
|
36
|
-
return entries.filter((e) =>
|
|
52
|
+
return entries.filter((e) => {
|
|
53
|
+
const updatedMs = e.mtimeMs ?? (e.updatedAt ? new Date(e.updatedAt).getTime() : Number.NaN);
|
|
54
|
+
return e.pinned || !Number.isFinite(updatedMs) || updatedMs <= 0 || updatedMs >= cutoff;
|
|
55
|
+
});
|
|
37
56
|
}
|
|
38
57
|
export function resolveMemoryBaseDir(override) {
|
|
39
58
|
if (override)
|
|
@@ -83,37 +102,75 @@ export class MemoryManager {
|
|
|
83
102
|
// no flat .md files and this becomes a no-op.
|
|
84
103
|
this.migrateFlatLayout();
|
|
85
104
|
}
|
|
86
|
-
getMemoryDir() {
|
|
87
|
-
|
|
105
|
+
getMemoryDir() {
|
|
106
|
+
return this.memoryDir;
|
|
107
|
+
}
|
|
108
|
+
getScope() {
|
|
109
|
+
return this.scope;
|
|
110
|
+
}
|
|
88
111
|
/**
|
|
89
|
-
* Save a memory entry.
|
|
112
|
+
* Save a memory entry. New entries get a stable id and `<id>.md`; entries
|
|
113
|
+
* carrying an existing id update that file even when name changes.
|
|
90
114
|
*/
|
|
91
|
-
save(entry) {
|
|
92
|
-
const fileName = this.slugify(entry.name) + ".md";
|
|
93
|
-
const filePath = join(this.memoryDir, fileName);
|
|
115
|
+
save(entry, opts = {}) {
|
|
94
116
|
const nowIso = new Date().toISOString();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
117
|
+
const inputId = entry.id;
|
|
118
|
+
const syntheticLegacy = inputId ? this.parseSyntheticLegacyId(inputId) : null;
|
|
119
|
+
const existing = inputId && !syntheticLegacy
|
|
120
|
+
? (this.findById(inputId) ?? null)
|
|
121
|
+
: syntheticLegacy && syntheticLegacy.scope === this.scope
|
|
122
|
+
? this.loadFile(syntheticLegacy.fileName)
|
|
123
|
+
: null;
|
|
124
|
+
const id = inputId && !syntheticLegacy ? inputId : this.generateId();
|
|
125
|
+
const fileName = existing?.fileName ?? this.availableFileNameForId(id);
|
|
126
|
+
const filePath = join(this.memoryDir, fileName);
|
|
127
|
+
const origin = opts.forceOrigin ?? entry.origin ?? existing?.origin ?? "manual";
|
|
128
|
+
const pinned = entry.pinned ?? existing?.pinned;
|
|
129
|
+
const createdAt = entry.createdAt ?? entry.created ?? existing?.createdAt ?? nowIso;
|
|
130
|
+
const useCount = entry.useCount ?? entry.usageCount ?? existing?.useCount ?? 0;
|
|
131
|
+
const lastUsedAt = entry.lastUsedAt ?? entry.lastUsed ?? existing?.lastUsedAt ?? createdAt;
|
|
103
132
|
const originProject = entry.originProject ?? existing?.originProject;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
const promotionKey = entry.promotionKey ?? existing?.promotionKey;
|
|
134
|
+
const originProjects = entry.originProjects ?? existing?.originProjects;
|
|
135
|
+
const evidenceCount = entry.evidenceCount ?? existing?.evidenceCount;
|
|
136
|
+
const firstSeenAt = entry.firstSeenAt ?? existing?.firstSeenAt;
|
|
137
|
+
const lastSeenAt = entry.lastSeenAt ?? existing?.lastSeenAt;
|
|
138
|
+
const promotionReason = entry.promotionReason ?? existing?.promotionReason;
|
|
139
|
+
const promotionStatus = entry.promotionStatus ?? existing?.promotionStatus;
|
|
140
|
+
const promotionSourceId = entry.promotionSourceId ?? existing?.promotionSourceId;
|
|
141
|
+
const contentChanged = existing != null &&
|
|
142
|
+
(existing.name !== entry.name ||
|
|
143
|
+
existing.description !== entry.description ||
|
|
144
|
+
existing.type !== entry.type ||
|
|
145
|
+
existing.content !== entry.content);
|
|
146
|
+
const incrementUpdateCount = opts.incrementUpdateCount !== false && contentChanged;
|
|
147
|
+
const updateCount = (entry.updateCount ?? existing?.updateCount ?? 0) + (incrementUpdateCount ? 1 : 0);
|
|
148
|
+
const updatedAt = incrementUpdateCount
|
|
149
|
+
? nowIso
|
|
150
|
+
: (entry.updatedAt ?? existing?.updatedAt ?? nowIso);
|
|
107
151
|
const content = `---\n` +
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
(entry.
|
|
112
|
-
(
|
|
113
|
-
(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
152
|
+
frontmatterLine("id", id) +
|
|
153
|
+
frontmatterLine("name", entry.name) +
|
|
154
|
+
frontmatterLine("description", entry.description) +
|
|
155
|
+
frontmatterLine("type", entry.type) +
|
|
156
|
+
frontmatterLine("origin", origin) +
|
|
157
|
+
(pinned ? frontmatterLine("pinned", true) : "") +
|
|
158
|
+
(originProject ? frontmatterLine("originProject", originProject) : "") +
|
|
159
|
+
(promotionKey ? frontmatterLine("promotionKey", promotionKey) : "") +
|
|
160
|
+
(originProjects && originProjects.length > 0
|
|
161
|
+
? frontmatterLine("originProjects", originProjects)
|
|
162
|
+
: "") +
|
|
163
|
+
(typeof evidenceCount === "number" ? frontmatterLine("evidenceCount", evidenceCount) : "") +
|
|
164
|
+
(firstSeenAt ? frontmatterLine("firstSeenAt", firstSeenAt) : "") +
|
|
165
|
+
(lastSeenAt ? frontmatterLine("lastSeenAt", lastSeenAt) : "") +
|
|
166
|
+
(promotionReason ? frontmatterLine("promotionReason", promotionReason) : "") +
|
|
167
|
+
(promotionStatus ? frontmatterLine("promotionStatus", promotionStatus) : "") +
|
|
168
|
+
(promotionSourceId ? frontmatterLine("promotionSourceId", promotionSourceId) : "") +
|
|
169
|
+
frontmatterLine("createdAt", createdAt) +
|
|
170
|
+
frontmatterLine("updatedAt", updatedAt) +
|
|
171
|
+
frontmatterLine("lastUsedAt", lastUsedAt) +
|
|
172
|
+
frontmatterLine("useCount", useCount) +
|
|
173
|
+
frontmatterLine("updateCount", updateCount) +
|
|
117
174
|
`---\n\n` +
|
|
118
175
|
`${entry.content}\n`;
|
|
119
176
|
writeFileSync(filePath, content, "utf-8");
|
|
@@ -140,6 +197,25 @@ export class MemoryManager {
|
|
|
140
197
|
}
|
|
141
198
|
return entries;
|
|
142
199
|
}
|
|
200
|
+
findById(id) {
|
|
201
|
+
return this.loadAll().find((e) => e.id === id);
|
|
202
|
+
}
|
|
203
|
+
find(nameOrFileOrId) {
|
|
204
|
+
return this.loadAll().find((e) => e.id === nameOrFileOrId || e.name === nameOrFileOrId || e.fileName === nameOrFileOrId);
|
|
205
|
+
}
|
|
206
|
+
isOwnedBy(entry, allowedOrigins) {
|
|
207
|
+
if (!entry)
|
|
208
|
+
return false;
|
|
209
|
+
return allowedOrigins.includes(entry.origin ?? "manual");
|
|
210
|
+
}
|
|
211
|
+
deleteIfOwned(nameOrFileOrId, allowedOrigins) {
|
|
212
|
+
const entry = this.find(nameOrFileOrId);
|
|
213
|
+
if (!entry)
|
|
214
|
+
return "not_found";
|
|
215
|
+
if (!this.isOwnedBy(entry, allowedOrigins))
|
|
216
|
+
return "protected";
|
|
217
|
+
return this.delete(entry.id ?? entry.fileName) ? "deleted" : "not_found";
|
|
218
|
+
}
|
|
143
219
|
/**
|
|
144
220
|
* Load a single memory file.
|
|
145
221
|
*/
|
|
@@ -165,32 +241,128 @@ export class MemoryManager {
|
|
|
165
241
|
return null;
|
|
166
242
|
const frontmatter = frontmatterMatch[1];
|
|
167
243
|
const content = frontmatterMatch[2].trim();
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
244
|
+
const readRaw = (key) => frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"))?.[1]?.trim();
|
|
245
|
+
const readValue = (key) => {
|
|
246
|
+
const raw = readRaw(key);
|
|
247
|
+
return raw === undefined ? undefined : parseFrontmatterValue(raw);
|
|
248
|
+
};
|
|
249
|
+
const read = (key) => {
|
|
250
|
+
const value = readValue(key);
|
|
251
|
+
if (value === undefined || value === null)
|
|
252
|
+
return undefined;
|
|
253
|
+
if (Array.isArray(value))
|
|
254
|
+
return value.map(String).join(",");
|
|
255
|
+
return String(value);
|
|
256
|
+
};
|
|
257
|
+
const readList = (key) => {
|
|
258
|
+
const raw = readRaw(key);
|
|
259
|
+
if (raw !== undefined && raw.length > 0) {
|
|
260
|
+
const value = parseFrontmatterValue(raw);
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
const values = value.map((item) => String(item)).filter(Boolean);
|
|
263
|
+
return values.length > 0 ? values : undefined;
|
|
264
|
+
}
|
|
265
|
+
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
|
|
266
|
+
const values = value
|
|
267
|
+
.slice(1, -1)
|
|
268
|
+
.split(",")
|
|
269
|
+
.map((item) => item.trim().replace(/^["']|["']$/g, ""))
|
|
270
|
+
.filter(Boolean);
|
|
271
|
+
return values.length > 0 ? values : undefined;
|
|
272
|
+
}
|
|
273
|
+
return [String(value)];
|
|
274
|
+
}
|
|
275
|
+
const block = frontmatter.match(new RegExp(`^${key}:\\s*\\n((?:\\s+-\\s+.*\\n?)*)`, "m"));
|
|
276
|
+
if (block?.[1]) {
|
|
277
|
+
const values = block[1]
|
|
278
|
+
.split(/\n/)
|
|
279
|
+
.map((line) => line.match(/^\s+-\s+(.+)\s*$/)?.[1]?.trim())
|
|
280
|
+
.filter((value) => Boolean(value));
|
|
281
|
+
return values.length > 0 ? values : undefined;
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
};
|
|
285
|
+
const readNumber = (key) => {
|
|
286
|
+
const value = readValue(key);
|
|
287
|
+
if (value === undefined || value === null || value === "")
|
|
288
|
+
return undefined;
|
|
289
|
+
if (typeof value === "number")
|
|
290
|
+
return Number.isFinite(value) ? value : undefined;
|
|
291
|
+
const parsed = parseInt(String(value), 10);
|
|
292
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
293
|
+
};
|
|
294
|
+
const readBoolean = (key) => {
|
|
295
|
+
const value = readValue(key);
|
|
296
|
+
return value === true || value === "true";
|
|
297
|
+
};
|
|
298
|
+
const id = read("id") ?? `legacy:${scope}:${fileName}`;
|
|
299
|
+
const name = read("name") ?? fileName;
|
|
300
|
+
const description = read("description") ?? "";
|
|
301
|
+
const type = (read("type") ?? "project");
|
|
302
|
+
const pinned = readBoolean("pinned");
|
|
172
303
|
// Anchor with ^…/m so `origin:` doesn't accidentally match the
|
|
173
304
|
// `originProject:` line (and vice-versa).
|
|
174
|
-
const originRaw =
|
|
175
|
-
const origin = originRaw === "auto" || originRaw === "manual"
|
|
176
|
-
|
|
177
|
-
|
|
305
|
+
const originRaw = read("origin");
|
|
306
|
+
const origin = originRaw === "auto" || originRaw === "manual" || originRaw === "dream"
|
|
307
|
+
? originRaw
|
|
308
|
+
: "manual";
|
|
309
|
+
const originProject = read("originProject") || undefined;
|
|
310
|
+
let mtimeMs = 0;
|
|
178
311
|
try {
|
|
179
|
-
|
|
312
|
+
mtimeMs = statSync(filePath).mtimeMs;
|
|
180
313
|
}
|
|
181
314
|
catch {
|
|
182
315
|
// mtime best-effort; 0 = unknown (never filtered out by maxAge).
|
|
183
316
|
}
|
|
184
317
|
// Lifecycle fields. Legacy files lack them → fall back to mtime so a TTL
|
|
185
318
|
// sweep treats them as "last used = file age" rather than "never used".
|
|
186
|
-
const mtimeIso =
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
319
|
+
const mtimeIso = mtimeMs > 0 ? new Date(mtimeMs).toISOString() : undefined;
|
|
320
|
+
const createdAt = read("createdAt") ?? read("created") ?? mtimeIso;
|
|
321
|
+
const updatedAt = read("updatedAt") ?? mtimeIso ?? createdAt;
|
|
322
|
+
const lastUsedAt = read("lastUsedAt") ?? read("lastUsed") ?? createdAt;
|
|
323
|
+
const useRaw = read("useCount") ?? read("usageCount");
|
|
324
|
+
const updateRaw = read("updateCount");
|
|
325
|
+
const useCount = useRaw ? parseInt(useRaw, 10) : 0;
|
|
326
|
+
const updateCount = updateRaw ? parseInt(updateRaw, 10) : 0;
|
|
327
|
+
const promotionKey = read("promotionKey");
|
|
328
|
+
const originProjects = readList("originProjects");
|
|
329
|
+
const evidenceCount = readNumber("evidenceCount");
|
|
330
|
+
const firstSeenAt = read("firstSeenAt");
|
|
331
|
+
const lastSeenAt = read("lastSeenAt");
|
|
332
|
+
const promotionReason = read("promotionReason");
|
|
333
|
+
const promotionStatusRaw = read("promotionStatus");
|
|
334
|
+
const promotionStatus = promotionStatusRaw === "pending" || promotionStatusRaw === "rejected"
|
|
335
|
+
? promotionStatusRaw
|
|
336
|
+
: undefined;
|
|
337
|
+
const promotionSourceId = read("promotionSourceId");
|
|
191
338
|
return {
|
|
192
|
-
|
|
193
|
-
|
|
339
|
+
id,
|
|
340
|
+
name,
|
|
341
|
+
description,
|
|
342
|
+
type,
|
|
343
|
+
content,
|
|
344
|
+
fileName,
|
|
345
|
+
scope,
|
|
346
|
+
updatedAt,
|
|
347
|
+
mtimeMs,
|
|
348
|
+
pinned,
|
|
349
|
+
origin,
|
|
350
|
+
useCount,
|
|
351
|
+
updateCount,
|
|
352
|
+
lastUsedAt,
|
|
353
|
+
createdAt,
|
|
354
|
+
usageCount: useCount,
|
|
355
|
+
lastUsed: lastUsedAt,
|
|
356
|
+
created: createdAt,
|
|
357
|
+
originProject,
|
|
358
|
+
promotionKey,
|
|
359
|
+
originProjects,
|
|
360
|
+
evidenceCount,
|
|
361
|
+
firstSeenAt,
|
|
362
|
+
lastSeenAt,
|
|
363
|
+
promotionReason,
|
|
364
|
+
promotionStatus,
|
|
365
|
+
promotionSourceId,
|
|
194
366
|
};
|
|
195
367
|
}
|
|
196
368
|
catch {
|
|
@@ -204,7 +376,7 @@ export class MemoryManager {
|
|
|
204
376
|
*/
|
|
205
377
|
delete(nameOrFile) {
|
|
206
378
|
const entries = this.loadAll();
|
|
207
|
-
const entry = entries.find((e) => e.name === nameOrFile || e.fileName === nameOrFile);
|
|
379
|
+
const entry = entries.find((e) => e.id === nameOrFile || e.name === nameOrFile || e.fileName === nameOrFile);
|
|
208
380
|
if (!entry)
|
|
209
381
|
return false;
|
|
210
382
|
try {
|
|
@@ -228,21 +400,25 @@ export class MemoryManager {
|
|
|
228
400
|
* never ages out. Idempotent-safe: missing entry → false, no throw.
|
|
229
401
|
*/
|
|
230
402
|
recordRecall(nameOrFile, now = new Date()) {
|
|
231
|
-
const entry = this.loadAll().find((e) => e.name === nameOrFile || e.fileName === nameOrFile);
|
|
403
|
+
const entry = this.loadAll().find((e) => e.id === nameOrFile || e.name === nameOrFile || e.fileName === nameOrFile);
|
|
232
404
|
if (!entry)
|
|
233
405
|
return false;
|
|
234
406
|
try {
|
|
235
407
|
this.save({
|
|
408
|
+
id: entry.id,
|
|
236
409
|
name: entry.name,
|
|
237
410
|
description: entry.description,
|
|
238
411
|
type: entry.type,
|
|
239
412
|
content: entry.content,
|
|
240
413
|
pinned: entry.pinned,
|
|
241
414
|
origin: entry.origin,
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
415
|
+
createdAt: entry.createdAt,
|
|
416
|
+
updatedAt: entry.updatedAt,
|
|
417
|
+
useCount: (entry.useCount ?? 0) + 1,
|
|
418
|
+
updateCount: entry.updateCount,
|
|
419
|
+
lastUsedAt: now.toISOString(),
|
|
420
|
+
originProject: entry.originProject,
|
|
421
|
+
}, { incrementUpdateCount: false });
|
|
246
422
|
return true;
|
|
247
423
|
}
|
|
248
424
|
catch {
|
|
@@ -265,7 +441,7 @@ export class MemoryManager {
|
|
|
265
441
|
continue; // stable types are exempt
|
|
266
442
|
if (e.pinned)
|
|
267
443
|
continue; // pinned exempt
|
|
268
|
-
const lastUsedMs = e.
|
|
444
|
+
const lastUsedMs = e.lastUsedAt ? new Date(e.lastUsedAt).getTime() : NaN;
|
|
269
445
|
// Unknown lastUsed (unparseable) → keep, never prune on missing data.
|
|
270
446
|
if (!Number.isFinite(lastUsedMs))
|
|
271
447
|
continue;
|
|
@@ -277,13 +453,14 @@ export class MemoryManager {
|
|
|
277
453
|
return pruned;
|
|
278
454
|
}
|
|
279
455
|
/**
|
|
280
|
-
* Approve a pending memory: move it from the pending scope into the
|
|
281
|
-
* of the SAME memory root
|
|
282
|
-
* approval
|
|
283
|
-
*
|
|
456
|
+
* Approve a pending memory: move it from the pending scope into the global
|
|
457
|
+
* dream scope of the SAME memory root. Pending only ever exists at the global
|
|
458
|
+
* root; approval gates whether a suggested global dream becomes injected.
|
|
459
|
+
* Must be called on a pending-scope manager. Returns the new global dream
|
|
460
|
+
* filename, or null if not found / wrong scope.
|
|
284
461
|
*
|
|
285
|
-
* 审批门 (用户拍板): the only path that moves
|
|
286
|
-
*
|
|
462
|
+
* 审批门 (用户拍板): the only path that moves a suggested global dream into the
|
|
463
|
+
* injected global dream store.
|
|
287
464
|
*/
|
|
288
465
|
approvePending(nameOrFile) {
|
|
289
466
|
return this.movePending(nameOrFile, "global");
|
|
@@ -295,27 +472,86 @@ export class MemoryManager {
|
|
|
295
472
|
demotePending(nameOrFile) {
|
|
296
473
|
return this.movePending(nameOrFile, "project");
|
|
297
474
|
}
|
|
298
|
-
/**
|
|
475
|
+
/**
|
|
476
|
+
* Reject a pending global-dream suggestion. The project dream entry stays in
|
|
477
|
+
* place, but is marked so the same source is not suggested again.
|
|
478
|
+
*/
|
|
479
|
+
rejectPending(nameOrFile) {
|
|
480
|
+
if (this.scope !== "pending")
|
|
481
|
+
return false;
|
|
482
|
+
const entry = this.find(nameOrFile);
|
|
483
|
+
if (!entry)
|
|
484
|
+
return false;
|
|
485
|
+
if (entry.originProject) {
|
|
486
|
+
const projectDream = new MemoryManager({
|
|
487
|
+
baseDir: this.baseDir,
|
|
488
|
+
projectDir: entry.originProject,
|
|
489
|
+
scope: "dream",
|
|
490
|
+
});
|
|
491
|
+
const source = (entry.promotionSourceId ? projectDream.findById(entry.promotionSourceId) : undefined) ??
|
|
492
|
+
projectDream.loadAll().find((candidate) => candidate.name === entry.name);
|
|
493
|
+
if (source && source.origin !== "manual") {
|
|
494
|
+
projectDream.save({
|
|
495
|
+
id: source.id,
|
|
496
|
+
name: source.name,
|
|
497
|
+
description: source.description,
|
|
498
|
+
type: source.type,
|
|
499
|
+
content: source.content,
|
|
500
|
+
origin: source.origin ?? "dream",
|
|
501
|
+
pinned: source.pinned,
|
|
502
|
+
createdAt: source.createdAt,
|
|
503
|
+
updatedAt: source.updatedAt,
|
|
504
|
+
lastUsedAt: source.lastUsedAt,
|
|
505
|
+
useCount: source.useCount,
|
|
506
|
+
updateCount: source.updateCount,
|
|
507
|
+
originProject: source.originProject,
|
|
508
|
+
originProjects: source.originProjects,
|
|
509
|
+
evidenceCount: source.evidenceCount,
|
|
510
|
+
firstSeenAt: source.firstSeenAt,
|
|
511
|
+
lastSeenAt: source.lastSeenAt,
|
|
512
|
+
promotionReason: entry.promotionReason ?? source.promotionReason,
|
|
513
|
+
promotionStatus: "rejected",
|
|
514
|
+
}, { incrementUpdateCount: false, forceOrigin: source.origin ?? "dream" });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return this.delete(entry.id ?? entry.name);
|
|
518
|
+
}
|
|
519
|
+
/** Shared move for approve(→global dream)/demote(→origin-project user). */
|
|
299
520
|
movePending(nameOrFile, dest) {
|
|
300
521
|
if (this.scope !== "pending")
|
|
301
522
|
return null;
|
|
302
|
-
const entry = this.
|
|
523
|
+
const entry = this.find(nameOrFile);
|
|
303
524
|
if (!entry)
|
|
304
525
|
return null;
|
|
305
|
-
const targetMgr = dest === "
|
|
306
|
-
? new MemoryManager({ baseDir: this.baseDir,
|
|
307
|
-
:
|
|
526
|
+
const targetMgr = dest === "global"
|
|
527
|
+
? new MemoryManager({ baseDir: this.baseDir, scope: "dream" })
|
|
528
|
+
: dest === "project" && entry.originProject
|
|
529
|
+
? new MemoryManager({
|
|
530
|
+
baseDir: this.baseDir,
|
|
531
|
+
projectDir: entry.originProject,
|
|
532
|
+
scope: "user",
|
|
533
|
+
})
|
|
534
|
+
: new MemoryManager({ baseDir: this.baseDir, scope: "user" }); // demote fallback
|
|
535
|
+
const destinationOrigin = dest === "global" ? "dream" : (entry.origin ?? "auto");
|
|
308
536
|
const fileName = targetMgr.save({
|
|
309
537
|
name: entry.name,
|
|
310
538
|
description: entry.description,
|
|
311
539
|
type: entry.type,
|
|
312
540
|
content: entry.content,
|
|
313
|
-
origin:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
541
|
+
origin: destinationOrigin,
|
|
542
|
+
createdAt: entry.createdAt,
|
|
543
|
+
updatedAt: entry.updatedAt,
|
|
544
|
+
lastUsedAt: entry.lastUsedAt,
|
|
545
|
+
useCount: entry.useCount,
|
|
546
|
+
updateCount: entry.updateCount,
|
|
547
|
+
originProjects: entry.originProjects ?? (entry.originProject ? [entry.originProject] : []),
|
|
548
|
+
evidenceCount: entry.evidenceCount,
|
|
549
|
+
firstSeenAt: entry.firstSeenAt,
|
|
550
|
+
lastSeenAt: entry.lastSeenAt,
|
|
551
|
+
promotionReason: entry.promotionReason,
|
|
552
|
+
// originProject/promotionSourceId are no longer needed once it has landed
|
|
553
|
+
// in a real store.
|
|
554
|
+
}, { forceOrigin: destinationOrigin });
|
|
319
555
|
this.delete(entry.name); // soft-delete the pending copy
|
|
320
556
|
return fileName;
|
|
321
557
|
}
|
|
@@ -337,9 +573,11 @@ export class MemoryManager {
|
|
|
337
573
|
content: entry.content,
|
|
338
574
|
origin: entry.origin,
|
|
339
575
|
pinned: entry.pinned,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
576
|
+
createdAt: entry.createdAt,
|
|
577
|
+
updatedAt: entry.updatedAt,
|
|
578
|
+
lastUsedAt: entry.lastUsedAt,
|
|
579
|
+
useCount: entry.useCount,
|
|
580
|
+
updateCount: entry.updateCount,
|
|
343
581
|
});
|
|
344
582
|
this.delete(entry.name); // remove from the project store
|
|
345
583
|
return fileName;
|
|
@@ -517,16 +755,41 @@ export class MemoryManager {
|
|
|
517
755
|
for (const [fileName, meta] of cache) {
|
|
518
756
|
lines.push(`- [${meta.name}](${fileName}) — ${meta.description}`);
|
|
519
757
|
}
|
|
520
|
-
const content = lines.length > 0
|
|
521
|
-
? lines.join("\n") + "\n"
|
|
522
|
-
: "(no memories stored)\n";
|
|
758
|
+
const content = lines.length > 0 ? lines.join("\n") + "\n" : "(no memories stored)\n";
|
|
523
759
|
writeFileSync(this.indexPath, content, "utf-8");
|
|
524
760
|
}
|
|
525
|
-
|
|
526
|
-
return
|
|
761
|
+
generateId() {
|
|
762
|
+
return `mem_${randomUUID().replace(/-/g, "")}`;
|
|
763
|
+
}
|
|
764
|
+
availableFileNameForId(id) {
|
|
765
|
+
let attempt = 0;
|
|
766
|
+
while (attempt < 100) {
|
|
767
|
+
const fileName = this.fileNameForId(id, attempt);
|
|
768
|
+
const existing = this.loadFile(fileName);
|
|
769
|
+
if (!existing || existing.id === id)
|
|
770
|
+
return fileName;
|
|
771
|
+
attempt++;
|
|
772
|
+
}
|
|
773
|
+
throw new Error(`Unable to allocate a memory filename for id "${id}"`);
|
|
774
|
+
}
|
|
775
|
+
fileNameForId(id, collisionAttempt = 0) {
|
|
776
|
+
if (collisionAttempt === 0 && /^[a-z0-9][a-z0-9_.-]*$/.test(id) && !id.includes("..")) {
|
|
777
|
+
return `${id}.md`;
|
|
778
|
+
}
|
|
779
|
+
const safeStem = id
|
|
527
780
|
.toLowerCase()
|
|
528
|
-
.replace(/[^a-z0-
|
|
529
|
-
.replace(
|
|
530
|
-
.
|
|
781
|
+
.replace(/[^a-z0-9_.-]/g, "_")
|
|
782
|
+
.replace(/_+/g, "_")
|
|
783
|
+
.replace(/^[._-]+|[._-]+$/g, "")
|
|
784
|
+
.slice(0, 80) || "memory";
|
|
785
|
+
const hashInput = collisionAttempt === 0 ? id : `${id}:${collisionAttempt}`;
|
|
786
|
+
const hash = createHash("sha256").update(hashInput).digest("hex").slice(0, 12);
|
|
787
|
+
return `${safeStem}-${hash}.md`;
|
|
788
|
+
}
|
|
789
|
+
parseSyntheticLegacyId(id) {
|
|
790
|
+
const match = id.match(/^legacy:(user|dream|pending):(.+)$/);
|
|
791
|
+
if (!match)
|
|
792
|
+
return null;
|
|
793
|
+
return { scope: match[1], fileName: match[2] };
|
|
531
794
|
}
|
|
532
795
|
}
|
|
@@ -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) {
|