@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
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
* The caller supplies `runDream`, which owns the LLM client and tool registry
|
|
13
13
|
* — orchestrator just decides WHEN to trigger.
|
|
14
14
|
*/
|
|
15
|
-
import { MemoryManager } from "../session/memory.js";
|
|
16
|
-
import { buildExtractionPrompt, parseExtractionResponse } from "./extract-memories.js";
|
|
15
|
+
import { MemoryManager, } from "../session/memory.js";
|
|
16
|
+
import { buildExtractionPrompt, parseExtractionResponse, } from "./extract-memories.js";
|
|
17
17
|
import { saveSessionMemory, buildSessionMemoryPrompt } from "./session-memory.js";
|
|
18
18
|
import { extractJSON } from "../utils/json.js";
|
|
19
19
|
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
20
|
-
import { shouldAutoDream, recordSession, recordDreamComplete, buildDreamSystemPrompt, buildDreamUserPrompt } from "./auto-dream.js";
|
|
20
|
+
import { shouldAutoDream, recordSession, recordDreamComplete, buildDreamSystemPrompt, buildDreamUserPrompt, } from "./auto-dream.js";
|
|
21
|
+
import { applyGlobalDreamPromotionGate, detectUserDirectGlobalPreference, } from "./global-dream-promotion.js";
|
|
21
22
|
import { logger } from "../logging/logger.js";
|
|
22
23
|
export class MemoryOrchestrator {
|
|
23
24
|
options;
|
|
@@ -45,7 +46,7 @@ export class MemoryOrchestrator {
|
|
|
45
46
|
else
|
|
46
47
|
try {
|
|
47
48
|
let t = Date.now();
|
|
48
|
-
const existing = mm.
|
|
49
|
+
const existing = collectExistingMemorySummaries(mm, this.options.projectDir);
|
|
49
50
|
const loadMs = Date.now() - t;
|
|
50
51
|
t = Date.now();
|
|
51
52
|
const extractionPrompt = buildExtractionPrompt(transcript, existing);
|
|
@@ -57,45 +58,130 @@ export class MemoryOrchestrator {
|
|
|
57
58
|
const entries = parseExtractionResponse(response, this.options.maxCount);
|
|
58
59
|
const parseMs = Date.now() - t;
|
|
59
60
|
t = Date.now();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
let
|
|
65
|
-
let
|
|
61
|
+
let globalDreamCount = 0;
|
|
62
|
+
let pendingGlobalCount = 0;
|
|
63
|
+
let projectDreamCount = 0;
|
|
64
|
+
let addCount = 0;
|
|
65
|
+
let updateCount = 0;
|
|
66
|
+
let noopCount = 0;
|
|
67
|
+
let deleteCount = 0;
|
|
68
|
+
let guardedManualCount = 0;
|
|
69
|
+
const userDirectGlobal = detectUserDirectGlobalPreference(transcript);
|
|
66
70
|
for (const entry of entries) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
? (pendingMm ??= new MemoryManager({ scope: "pending" }))
|
|
70
|
-
: mm;
|
|
71
|
-
target.save({
|
|
72
|
-
type: entry.type,
|
|
73
|
-
name: entry.name,
|
|
74
|
-
// Defense-in-depth: the extraction prompt forbids secrets, but a prompt
|
|
75
|
-
// is not a guarantee. An auto-extracted memory is persisted with no user
|
|
76
|
-
// review, so run the model-authored strings through the same
|
|
77
|
-
// redactSecrets the logging path uses — a leaked key never lands on disk.
|
|
71
|
+
const redactedEntry = {
|
|
72
|
+
...entry,
|
|
78
73
|
description: redactSecrets(entry.description),
|
|
79
74
|
content: redactSecrets(entry.content),
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
75
|
+
};
|
|
76
|
+
const decision = await decideWriteAction(redactedEntry, existing, this.options.callLLM);
|
|
77
|
+
switch (decision.action) {
|
|
78
|
+
case "ADD": {
|
|
79
|
+
const isGlobal = redactedEntry.scope === "global";
|
|
80
|
+
if (isGlobal) {
|
|
81
|
+
const promotion = applyGlobalDreamPromotionGate({
|
|
82
|
+
projectDir: this.options.projectDir,
|
|
83
|
+
candidate: redactedEntry,
|
|
84
|
+
userDirectGlobal,
|
|
85
|
+
});
|
|
86
|
+
if (promotion.projectEvidenceSaved)
|
|
87
|
+
projectDreamCount++;
|
|
88
|
+
if (promotion.pendingSuggested)
|
|
89
|
+
pendingGlobalCount++;
|
|
90
|
+
if (promotion.promoted)
|
|
91
|
+
globalDreamCount++;
|
|
92
|
+
addCount++;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const target = memoryManagerForDecision("project", "dream", this.options.projectDir);
|
|
96
|
+
target.save({
|
|
97
|
+
type: redactedEntry.type,
|
|
98
|
+
name: redactedEntry.name,
|
|
99
|
+
description: redactedEntry.description,
|
|
100
|
+
content: redactedEntry.content,
|
|
101
|
+
origin: "auto",
|
|
102
|
+
...(isGlobal && this.options.projectDir
|
|
103
|
+
? { originProject: this.options.projectDir }
|
|
104
|
+
: {}),
|
|
105
|
+
}, { forceOrigin: "auto" });
|
|
106
|
+
if (isGlobal)
|
|
107
|
+
globalDreamCount++;
|
|
108
|
+
else
|
|
109
|
+
projectDreamCount++;
|
|
110
|
+
addCount++;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "UPDATE": {
|
|
114
|
+
const targetId = decision.target?.id;
|
|
115
|
+
if (!targetId) {
|
|
116
|
+
noopCount++;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const location = decision.target?.location ?? redactedEntry.scope;
|
|
120
|
+
const target = memoryManagerForDecision(location, "dream", this.options.projectDir);
|
|
121
|
+
const existingTarget = target.findById(targetId);
|
|
122
|
+
if (!existingTarget || !target.isOwnedBy(existingTarget, ["auto", "dream"])) {
|
|
123
|
+
noopCount++;
|
|
124
|
+
guardedManualCount += existingTarget?.origin === "manual" ? 1 : 0;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
target.save({
|
|
128
|
+
id: targetId,
|
|
129
|
+
type: redactedEntry.type,
|
|
130
|
+
name: redactedEntry.name,
|
|
131
|
+
description: redactedEntry.description,
|
|
132
|
+
content: redactedEntry.content,
|
|
133
|
+
origin: existingTarget.origin ?? "auto",
|
|
134
|
+
pinned: existingTarget.pinned,
|
|
135
|
+
createdAt: existingTarget.createdAt,
|
|
136
|
+
useCount: existingTarget.useCount,
|
|
137
|
+
lastUsedAt: existingTarget.lastUsedAt,
|
|
138
|
+
originProject: existingTarget.originProject,
|
|
139
|
+
}, { forceOrigin: existingTarget.origin ?? "auto" });
|
|
140
|
+
if (location === "global")
|
|
141
|
+
globalDreamCount++;
|
|
142
|
+
else
|
|
143
|
+
projectDreamCount++;
|
|
144
|
+
updateCount++;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "DELETE": {
|
|
148
|
+
const targetId = decision.target?.id;
|
|
149
|
+
const location = decision.target?.location ?? redactedEntry.scope;
|
|
150
|
+
const target = memoryManagerForDecision(location, "dream", this.options.projectDir);
|
|
151
|
+
if (!targetId) {
|
|
152
|
+
noopCount++;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
const deleteResult = target.deleteIfOwned(targetId, ["auto", "dream"]);
|
|
156
|
+
if (deleteResult === "deleted")
|
|
157
|
+
deleteCount++;
|
|
158
|
+
else {
|
|
159
|
+
noopCount++;
|
|
160
|
+
if (deleteResult === "protected")
|
|
161
|
+
guardedManualCount++;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case "NOOP":
|
|
166
|
+
noopCount++;
|
|
167
|
+
if (decision.guardedManual)
|
|
168
|
+
guardedManualCount++;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
91
171
|
}
|
|
92
172
|
const saveMs = Date.now() - t;
|
|
93
173
|
extracted = entries.length;
|
|
94
174
|
logger.info("memory.extraction_done", {
|
|
95
175
|
sessionId,
|
|
96
176
|
extracted,
|
|
97
|
-
|
|
98
|
-
|
|
177
|
+
globalDreamCount,
|
|
178
|
+
pendingGlobalCount,
|
|
179
|
+
projectDreamCount,
|
|
180
|
+
addCount,
|
|
181
|
+
updateCount,
|
|
182
|
+
noopCount,
|
|
183
|
+
deleteCount,
|
|
184
|
+
guardedManualCount,
|
|
99
185
|
elapsedMs: Date.now() - startTime,
|
|
100
186
|
loadMs,
|
|
101
187
|
promptMs,
|
|
@@ -224,3 +310,230 @@ export class MemoryOrchestrator {
|
|
|
224
310
|
return { extracted, dreamTriggered, pruned };
|
|
225
311
|
}
|
|
226
312
|
}
|
|
313
|
+
function memoryManagerForDecision(location, scope, projectDir) {
|
|
314
|
+
return new MemoryManager({
|
|
315
|
+
projectDir: location === "project" ? projectDir : undefined,
|
|
316
|
+
scope,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function collectExistingMemorySummaries(projectUserManager, projectDir) {
|
|
320
|
+
const seen = new Set();
|
|
321
|
+
const out = [];
|
|
322
|
+
const add = (entries, location, memoryScope) => {
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
const key = `${location}:${memoryScope}:${entry.fileName}:${entry.id ?? ""}`;
|
|
325
|
+
if (seen.has(key))
|
|
326
|
+
continue;
|
|
327
|
+
seen.add(key);
|
|
328
|
+
out.push({
|
|
329
|
+
id: entry.id,
|
|
330
|
+
name: entry.name,
|
|
331
|
+
type: entry.type,
|
|
332
|
+
description: entry.description,
|
|
333
|
+
location,
|
|
334
|
+
memoryScope,
|
|
335
|
+
origin: entry.origin ?? "manual",
|
|
336
|
+
pinned: entry.pinned,
|
|
337
|
+
useCount: entry.useCount,
|
|
338
|
+
updateCount: entry.updateCount,
|
|
339
|
+
updatedAt: entry.updatedAt,
|
|
340
|
+
entry,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
add(projectUserManager.loadScope("user"), "project", "user");
|
|
345
|
+
add(memoryManagerForDecision("project", "dream", projectDir).loadAll(), "project", "dream");
|
|
346
|
+
add(memoryManagerForDecision("global", "dream", projectDir).loadAll(), "global", "dream");
|
|
347
|
+
add(memoryManagerForDecision("global", "user", projectDir).loadAll(), "global", "user");
|
|
348
|
+
return out
|
|
349
|
+
.sort((a, b) => {
|
|
350
|
+
const aTime = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
351
|
+
const bTime = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
352
|
+
return bTime - aTime;
|
|
353
|
+
})
|
|
354
|
+
.slice(0, 120);
|
|
355
|
+
}
|
|
356
|
+
async function decideWriteAction(candidate, existing, callLLM) {
|
|
357
|
+
const fallback = fallbackWriteDecision(candidate, existing);
|
|
358
|
+
const related = rankRelated(candidate, existing).slice(0, 40);
|
|
359
|
+
const llmDecision = await askLLMForWriteDecision(candidate, related.map((r) => r.summary), callLLM);
|
|
360
|
+
if (!llmDecision)
|
|
361
|
+
return fallback;
|
|
362
|
+
const target = llmDecision.target?.id
|
|
363
|
+
? existing.find((m) => m.id === llmDecision.target?.id)
|
|
364
|
+
: undefined;
|
|
365
|
+
if ((llmDecision.action === "UPDATE" || llmDecision.action === "DELETE") && target) {
|
|
366
|
+
if (target.origin === "manual") {
|
|
367
|
+
return {
|
|
368
|
+
action: "NOOP",
|
|
369
|
+
memory: candidate,
|
|
370
|
+
reason: "manual memory is protected",
|
|
371
|
+
guardedManual: true,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (target.memoryScope === "dream" && (target.origin === "auto" || target.origin === "dream")) {
|
|
375
|
+
return {
|
|
376
|
+
...llmDecision,
|
|
377
|
+
memory: candidate,
|
|
378
|
+
target: {
|
|
379
|
+
id: target.id,
|
|
380
|
+
location: target.location,
|
|
381
|
+
scope: "dream",
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Downgrade unsafe or duplicate ADDs using the deterministic guard.
|
|
387
|
+
if (llmDecision.action === "ADD" && fallback.action !== "ADD")
|
|
388
|
+
return fallback;
|
|
389
|
+
if (llmDecision.action === "NOOP")
|
|
390
|
+
return { ...llmDecision, memory: candidate };
|
|
391
|
+
if (llmDecision.action === "ADD")
|
|
392
|
+
return { ...llmDecision, memory: candidate };
|
|
393
|
+
return fallback;
|
|
394
|
+
}
|
|
395
|
+
async function askLLMForWriteDecision(candidate, related, callLLM) {
|
|
396
|
+
if (related.length === 0)
|
|
397
|
+
return null;
|
|
398
|
+
try {
|
|
399
|
+
const relatedText = related
|
|
400
|
+
.map((m) => `- id:${m.id ?? "(none)"} location:${m.location} scope:${m.memoryScope} origin:${m.origin} ` +
|
|
401
|
+
`[${m.type}] ${m.name}: ${m.description}`)
|
|
402
|
+
.join("\n");
|
|
403
|
+
const response = await callLLM("You are a memory write decision assistant. Output only one JSON object.", [
|
|
404
|
+
"Decide whether this automatic memory candidate should ADD, UPDATE, NOOP, or DELETE.",
|
|
405
|
+
"Manual memories are protected: if the target is origin:manual, choose NOOP.",
|
|
406
|
+
"Automatic extraction writes new entries only to dream. Reuse an existing auto/dream dream id for same-topic updates.",
|
|
407
|
+
"",
|
|
408
|
+
"Candidate:",
|
|
409
|
+
JSON.stringify(candidate),
|
|
410
|
+
"",
|
|
411
|
+
"Related existing memories:",
|
|
412
|
+
relatedText,
|
|
413
|
+
"",
|
|
414
|
+
'Respond with JSON: {"action":"ADD|UPDATE|DELETE|NOOP","target":{"id":"...","location":"project|global","scope":"dream|user"},"reason":"short","confidence":"high|medium|low"}',
|
|
415
|
+
].join("\n"));
|
|
416
|
+
const parsed = JSON.parse(extractJSON(response));
|
|
417
|
+
const action = typeof parsed.action === "string" ? parsed.action.toUpperCase() : "";
|
|
418
|
+
if (!["ADD", "UPDATE", "DELETE", "NOOP"].includes(action))
|
|
419
|
+
return null;
|
|
420
|
+
const location = parsed.target?.location === "global" || parsed.target?.location === "project"
|
|
421
|
+
? parsed.target.location
|
|
422
|
+
: candidate.scope;
|
|
423
|
+
const scope = parsed.target?.scope === "user" ? "user" : "dream";
|
|
424
|
+
return {
|
|
425
|
+
action: action,
|
|
426
|
+
target: parsed.target?.id ? { id: String(parsed.target.id), location, scope } : undefined,
|
|
427
|
+
memory: candidate,
|
|
428
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : "llm decision",
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function fallbackWriteDecision(candidate, existing) {
|
|
436
|
+
const ranked = rankRelated(candidate, existing);
|
|
437
|
+
const strong = ranked.filter((m) => m.score >= 0.55 || m.canonicalMatch);
|
|
438
|
+
const manual = strong.find((m) => m.summary.origin === "manual");
|
|
439
|
+
if (manual) {
|
|
440
|
+
return {
|
|
441
|
+
action: "NOOP",
|
|
442
|
+
memory: candidate,
|
|
443
|
+
reason: `same topic as manual memory ${manual.summary.id ?? manual.summary.name}`,
|
|
444
|
+
guardedManual: true,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const ownedDream = strong.find((m) => m.summary.location === candidate.scope &&
|
|
448
|
+
m.summary.memoryScope === "dream" &&
|
|
449
|
+
(m.summary.origin === "auto" || m.summary.origin === "dream") &&
|
|
450
|
+
m.summary.id &&
|
|
451
|
+
!m.summary.id.startsWith("legacy:"));
|
|
452
|
+
if (ownedDream?.summary.id) {
|
|
453
|
+
return {
|
|
454
|
+
action: "UPDATE",
|
|
455
|
+
target: {
|
|
456
|
+
id: ownedDream.summary.id,
|
|
457
|
+
location: ownedDream.summary.location,
|
|
458
|
+
scope: "dream",
|
|
459
|
+
},
|
|
460
|
+
memory: candidate,
|
|
461
|
+
reason: `same topic as auto/dream memory ${ownedDream.summary.id}`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return { action: "ADD", memory: candidate, reason: "new automatic candidate" };
|
|
465
|
+
}
|
|
466
|
+
function rankRelated(candidate, existing) {
|
|
467
|
+
const candidateText = `${candidate.name} ${candidate.description}`;
|
|
468
|
+
const candidateKey = canonicalKey(candidateText);
|
|
469
|
+
const candidateTokens = tokenSet(candidateText);
|
|
470
|
+
return existing
|
|
471
|
+
.map((summary) => {
|
|
472
|
+
const existingText = `${summary.name} ${summary.description}`;
|
|
473
|
+
const existingKey = canonicalKey(existingText);
|
|
474
|
+
const existingTokens = tokenSet(existingText);
|
|
475
|
+
return {
|
|
476
|
+
summary,
|
|
477
|
+
score: tokenOverlap(candidateTokens, existingTokens),
|
|
478
|
+
canonicalMatch: candidateKey.length > 0 && candidateKey === existingKey,
|
|
479
|
+
};
|
|
480
|
+
})
|
|
481
|
+
.sort((a, b) => {
|
|
482
|
+
if (a.canonicalMatch !== b.canonicalMatch)
|
|
483
|
+
return a.canonicalMatch ? -1 : 1;
|
|
484
|
+
if (a.score !== b.score)
|
|
485
|
+
return b.score - a.score;
|
|
486
|
+
const aOwned = a.summary.origin === "auto" || a.summary.origin === "dream" ? 1 : 0;
|
|
487
|
+
const bOwned = b.summary.origin === "auto" || b.summary.origin === "dream" ? 1 : 0;
|
|
488
|
+
if (aOwned !== bOwned)
|
|
489
|
+
return bOwned - aOwned;
|
|
490
|
+
const aTime = a.summary.updatedAt ? new Date(a.summary.updatedAt).getTime() : 0;
|
|
491
|
+
const bTime = b.summary.updatedAt ? new Date(b.summary.updatedAt).getTime() : 0;
|
|
492
|
+
return bTime - aTime;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function canonicalKey(text) {
|
|
496
|
+
return [...tokenSet(text)].sort().join(" ");
|
|
497
|
+
}
|
|
498
|
+
function tokenSet(text) {
|
|
499
|
+
const normalized = text
|
|
500
|
+
.toLowerCase()
|
|
501
|
+
.replace(/\b\d{4}-\d{2}-\d{2}\b/g, " ")
|
|
502
|
+
.replace(/\b\d{8}\b/g, " ")
|
|
503
|
+
.replace(/\b(today|yesterday|tomorrow|本轮|今天|昨天|明天)\b/g, " ")
|
|
504
|
+
.replace(/\bv\d+\b/g, " ")
|
|
505
|
+
.replace(/\b(batch|fix-batch)-\d+\b/g, " ")
|
|
506
|
+
.replace(/\b[a-f0-9]{7,}\b/g, " ")
|
|
507
|
+
.replace(/[\d]+/g, " ")
|
|
508
|
+
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, " ");
|
|
509
|
+
const stop = new Set([
|
|
510
|
+
"the",
|
|
511
|
+
"and",
|
|
512
|
+
"for",
|
|
513
|
+
"with",
|
|
514
|
+
"that",
|
|
515
|
+
"this",
|
|
516
|
+
"from",
|
|
517
|
+
"memory",
|
|
518
|
+
"memories",
|
|
519
|
+
"should",
|
|
520
|
+
"must",
|
|
521
|
+
"不要",
|
|
522
|
+
"需要",
|
|
523
|
+
"已经",
|
|
524
|
+
]);
|
|
525
|
+
return new Set(normalized
|
|
526
|
+
.split(/\s+/)
|
|
527
|
+
.map((t) => t.trim())
|
|
528
|
+
.filter((t) => t.length > 2 && !stop.has(t)));
|
|
529
|
+
}
|
|
530
|
+
function tokenOverlap(a, b) {
|
|
531
|
+
if (a.size === 0 || b.size === 0)
|
|
532
|
+
return 0;
|
|
533
|
+
let shared = 0;
|
|
534
|
+
for (const token of a) {
|
|
535
|
+
if (b.has(token))
|
|
536
|
+
shared++;
|
|
537
|
+
}
|
|
538
|
+
return shared / Math.min(a.size, b.size);
|
|
539
|
+
}
|
package/dist/session/memory.d.ts
CHANGED
|
@@ -29,15 +29,20 @@
|
|
|
29
29
|
* panel. Keeps the global layer curated — nothing auto-lands global.
|
|
30
30
|
*/
|
|
31
31
|
export type MemoryScope = "user" | "dream" | "pending";
|
|
32
|
+
export type MemoryOrigin = "auto" | "manual" | "dream";
|
|
32
33
|
export interface MemoryEntry {
|
|
34
|
+
/** Stable identity. Legacy files without frontmatter id read as legacy:<scope>:<fileName>. */
|
|
35
|
+
id?: string;
|
|
33
36
|
name: string;
|
|
34
37
|
description: string;
|
|
35
38
|
type: "user" | "feedback" | "project" | "reference";
|
|
36
39
|
content: string;
|
|
37
40
|
fileName: string;
|
|
38
41
|
scope: MemoryScope;
|
|
42
|
+
/** Semantic last content update time from frontmatter. */
|
|
43
|
+
updatedAt?: string;
|
|
39
44
|
/** File mtime in epoch ms — drives maxAge filtering (TODO 8.1). 0 if unknown. */
|
|
40
|
-
|
|
45
|
+
mtimeMs?: number;
|
|
41
46
|
/**
|
|
42
47
|
* 固定/置顶 (feedback#18 方案 A): a pinned memory is exempt from maxAge
|
|
43
48
|
* injection filtering and sorts first in the injected context. UI surfaces
|
|
@@ -49,15 +54,20 @@ export interface MemoryEntry {
|
|
|
49
54
|
* "manual" (or absent — legacy files) = written by the user/UI. Lets the
|
|
50
55
|
* UI distinguish curated memories from extractor noise.
|
|
51
56
|
*/
|
|
52
|
-
origin?:
|
|
57
|
+
origin?: MemoryOrigin;
|
|
53
58
|
/**
|
|
54
|
-
* Recall lifecycle (召回 TTL). `
|
|
55
|
-
* hits this entry; `
|
|
56
|
-
* is set on first save and preserved across UPDATE. Drives recall-based TTL:
|
|
59
|
+
* Recall lifecycle (召回 TTL). `useCount` increments each time MemoryRead
|
|
60
|
+
* hits this entry; `lastUsedAt` is the ISO timestamp of that last hit;
|
|
61
|
+
* `createdAt` is set on first save and preserved across UPDATE. Drives recall-based TTL:
|
|
57
62
|
* a `project`-type memory not read for N days is pruned. Stored in frontmatter
|
|
58
|
-
* — no SQLite. Legacy
|
|
59
|
-
*
|
|
63
|
+
* — no SQLite. Legacy `usageCount/lastUsed/created` fields are still read and
|
|
64
|
+
* exposed through their old aliases for compatibility.
|
|
60
65
|
*/
|
|
66
|
+
useCount?: number;
|
|
67
|
+
updateCount?: number;
|
|
68
|
+
lastUsedAt?: string;
|
|
69
|
+
createdAt?: string;
|
|
70
|
+
/** Legacy compatibility aliases. New writes use useCount/lastUsedAt/createdAt. */
|
|
61
71
|
usageCount?: number;
|
|
62
72
|
lastUsed?: string;
|
|
63
73
|
created?: string;
|
|
@@ -67,6 +77,15 @@ export interface MemoryEntry {
|
|
|
67
77
|
* user store (not the current one). Absent → fall back to no-repo / global.
|
|
68
78
|
*/
|
|
69
79
|
originProject?: string;
|
|
80
|
+
/** Global dream promotion metadata (P1). */
|
|
81
|
+
promotionKey?: string;
|
|
82
|
+
originProjects?: string[];
|
|
83
|
+
evidenceCount?: number;
|
|
84
|
+
firstSeenAt?: string;
|
|
85
|
+
lastSeenAt?: string;
|
|
86
|
+
promotionReason?: string;
|
|
87
|
+
promotionStatus?: "pending" | "rejected";
|
|
88
|
+
promotionSourceId?: string;
|
|
70
89
|
}
|
|
71
90
|
/**
|
|
72
91
|
* Drop memories older than `maxAgeDays` (by file mtime) for context injection
|
|
@@ -75,6 +94,12 @@ export interface MemoryEntry {
|
|
|
75
94
|
* just because we couldn't read its timestamp. Pure + testable.
|
|
76
95
|
*/
|
|
77
96
|
export declare function filterByAge(entries: MemoryEntry[], maxAgeDays?: number, now?: number): MemoryEntry[];
|
|
97
|
+
export interface SaveMemoryOptions {
|
|
98
|
+
/** False for lifecycle-only writes such as recordRecall and pin/unpin. */
|
|
99
|
+
incrementUpdateCount?: boolean;
|
|
100
|
+
/** Force provenance regardless of model/tool args. Used by origin guards. */
|
|
101
|
+
forceOrigin?: MemoryOrigin;
|
|
102
|
+
}
|
|
78
103
|
export interface MemoryManagerOptions {
|
|
79
104
|
/** Project root path; when set, memories are scoped under projects/<hash>/memory. */
|
|
80
105
|
projectDir?: string;
|
|
@@ -97,13 +122,18 @@ export declare class MemoryManager {
|
|
|
97
122
|
getMemoryDir(): string;
|
|
98
123
|
getScope(): MemoryScope;
|
|
99
124
|
/**
|
|
100
|
-
* Save a memory entry.
|
|
125
|
+
* Save a memory entry. New entries get a stable id and `<id>.md`; entries
|
|
126
|
+
* carrying an existing id update that file even when name changes.
|
|
101
127
|
*/
|
|
102
|
-
save(entry: Omit<MemoryEntry, "fileName" | "scope"
|
|
128
|
+
save(entry: Omit<MemoryEntry, "fileName" | "scope">, opts?: SaveMemoryOptions): string;
|
|
103
129
|
/**
|
|
104
130
|
* Load all memory entries.
|
|
105
131
|
*/
|
|
106
132
|
loadAll(): MemoryEntry[];
|
|
133
|
+
findById(id: string): MemoryEntry | undefined;
|
|
134
|
+
find(nameOrFileOrId: string): MemoryEntry | undefined;
|
|
135
|
+
isOwnedBy(entry: MemoryEntry | undefined, allowedOrigins: readonly MemoryOrigin[]): boolean;
|
|
136
|
+
deleteIfOwned(nameOrFileOrId: string, allowedOrigins: readonly MemoryOrigin[]): "deleted" | "not_found" | "protected";
|
|
107
137
|
/**
|
|
108
138
|
* Load a single memory file.
|
|
109
139
|
*/
|
|
@@ -136,13 +166,14 @@ export declare class MemoryManager {
|
|
|
136
166
|
*/
|
|
137
167
|
pruneByRecall(ttlDays: number, now?: Date): string[];
|
|
138
168
|
/**
|
|
139
|
-
* Approve a pending memory: move it from the pending scope into the
|
|
140
|
-
* of the SAME memory root
|
|
141
|
-
* approval
|
|
142
|
-
*
|
|
169
|
+
* Approve a pending memory: move it from the pending scope into the global
|
|
170
|
+
* dream scope of the SAME memory root. Pending only ever exists at the global
|
|
171
|
+
* root; approval gates whether a suggested global dream becomes injected.
|
|
172
|
+
* Must be called on a pending-scope manager. Returns the new global dream
|
|
173
|
+
* filename, or null if not found / wrong scope.
|
|
143
174
|
*
|
|
144
|
-
* 审批门 (用户拍板): the only path that moves
|
|
145
|
-
*
|
|
175
|
+
* 审批门 (用户拍板): the only path that moves a suggested global dream into the
|
|
176
|
+
* injected global dream store.
|
|
146
177
|
*/
|
|
147
178
|
approvePending(nameOrFile: string): string | null;
|
|
148
179
|
/**
|
|
@@ -150,7 +181,12 @@ export declare class MemoryManager {
|
|
|
150
181
|
* user store (originProject)。无 originProject 时落全局 user(兜底,不丢)。
|
|
151
182
|
*/
|
|
152
183
|
demotePending(nameOrFile: string): string | null;
|
|
153
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Reject a pending global-dream suggestion. The project dream entry stays in
|
|
186
|
+
* place, but is marked so the same source is not suggested again.
|
|
187
|
+
*/
|
|
188
|
+
rejectPending(nameOrFile: string): boolean;
|
|
189
|
+
/** Shared move for approve(→global dream)/demote(→origin-project user). */
|
|
154
190
|
private movePending;
|
|
155
191
|
/**
|
|
156
192
|
* Promote a project-level user memory to the GLOBAL user store (用户手动点
|
|
@@ -214,5 +250,8 @@ export declare class MemoryManager {
|
|
|
214
250
|
*/
|
|
215
251
|
private ensureIndexCache;
|
|
216
252
|
private writeIndex;
|
|
217
|
-
private
|
|
253
|
+
private generateId;
|
|
254
|
+
private availableFileNameForId;
|
|
255
|
+
private fileNameForId;
|
|
256
|
+
private parseSyntheticLegacyId;
|
|
218
257
|
}
|