@cjhyy/code-shell-core 0.6.0-rc.8 → 0.7.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/THIRD_PARTY_NOTICES.md +206 -0
  2. package/dist/automation/scheduler.d.ts +13 -7
  3. package/dist/automation/scheduler.js +116 -37
  4. package/dist/capability-control/service.d.ts +2 -0
  5. package/dist/capability-control/service.js +4 -2
  6. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +7 -1
  8. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  9. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  10. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  11. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  13. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  14. package/dist/cc-orchestrator/external-agent-driver.js +102 -51
  15. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  16. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  17. package/dist/cc-orchestrator/session-history.js +2 -2
  18. package/dist/cli/agent-server-stdio.js +9 -2
  19. package/dist/context/compaction.d.ts +38 -1
  20. package/dist/context/compaction.js +138 -0
  21. package/dist/context/manager.d.ts +34 -2
  22. package/dist/context/manager.js +254 -58
  23. package/dist/context/token-counter.js +13 -0
  24. package/dist/credentials/access.d.ts +56 -0
  25. package/dist/credentials/access.js +183 -0
  26. package/dist/credentials/index.d.ts +1 -0
  27. package/dist/credentials/index.js +1 -0
  28. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  29. package/dist/credentials/inject-credential-tool.js +30 -11
  30. package/dist/credentials/types.d.ts +2 -2
  31. package/dist/credentials/use-credential-tool.d.ts +9 -1
  32. package/dist/credentials/use-credential-tool.js +58 -45
  33. package/dist/engine/engine.d.ts +38 -12
  34. package/dist/engine/engine.js +477 -163
  35. package/dist/engine/image-policy.d.ts +6 -0
  36. package/dist/engine/image-policy.js +17 -6
  37. package/dist/engine/input-attachments.d.ts +13 -0
  38. package/dist/engine/input-attachments.js +255 -0
  39. package/dist/engine/model-facade.d.ts +5 -2
  40. package/dist/engine/model-facade.js +6 -16
  41. package/dist/engine/parse-task.d.ts +10 -0
  42. package/dist/engine/parse-task.js +5 -0
  43. package/dist/engine/query.js +2 -0
  44. package/dist/engine/session-usage.d.ts +12 -0
  45. package/dist/engine/session-usage.js +56 -0
  46. package/dist/engine/steer-queue.d.ts +2 -1
  47. package/dist/engine/steer-queue.js +2 -2
  48. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  49. package/dist/engine/streaming-tool-queue.js +11 -7
  50. package/dist/engine/turn-loop.d.ts +34 -2
  51. package/dist/engine/turn-loop.js +269 -52
  52. package/dist/engine/types.d.ts +8 -0
  53. package/dist/git/worktree/crud.d.ts +69 -0
  54. package/dist/git/worktree/crud.js +206 -0
  55. package/dist/git/worktree/diff.d.ts +14 -0
  56. package/dist/git/worktree/diff.js +82 -0
  57. package/dist/git/worktree/git-exec.d.ts +7 -0
  58. package/dist/git/worktree/git-exec.js +51 -0
  59. package/dist/git/worktree/index.d.ts +5 -0
  60. package/dist/git/worktree/index.js +5 -0
  61. package/dist/git/worktree/query.d.ts +42 -0
  62. package/dist/git/worktree/query.js +121 -0
  63. package/dist/git/worktree/slug.d.ts +11 -0
  64. package/dist/git/worktree/slug.js +58 -0
  65. package/dist/git/worktree.d.ts +1 -84
  66. package/dist/git/worktree.js +5 -230
  67. package/dist/index.d.ts +31 -28
  68. package/dist/index.js +27 -25
  69. package/dist/logging/logger.js +6 -6
  70. package/dist/logging/sanitize-messages.d.ts +10 -2
  71. package/dist/logging/sanitize-messages.js +21 -6
  72. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  73. package/dist/plugins/installer/checkUpdate.js +4 -2
  74. package/dist/plugins/installer/install.js +2 -0
  75. package/dist/plugins/installer/installFromSource.js +9 -1
  76. package/dist/plugins/installer/parseSource.d.ts +4 -1
  77. package/dist/plugins/installer/parseSource.js +28 -10
  78. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  79. package/dist/plugins/installer/sourcePath.js +50 -0
  80. package/dist/plugins/installer/update.d.ts +4 -1
  81. package/dist/plugins/installer/update.js +5 -3
  82. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  83. package/dist/plugins/parseMarketplaceInput.js +4 -3
  84. package/dist/plugins/pluginInstaller.js +24 -22
  85. package/dist/preset/index.d.ts +1 -0
  86. package/dist/preset/index.js +16 -9
  87. package/dist/prompt/sections/base.md +1 -1
  88. package/dist/protocol/chat-session-manager.d.ts +2 -0
  89. package/dist/protocol/chat-session-manager.js +38 -2
  90. package/dist/protocol/chat-session.d.ts +5 -0
  91. package/dist/protocol/chat-session.js +2 -0
  92. package/dist/protocol/client.d.ts +13 -5
  93. package/dist/protocol/client.js +26 -3
  94. package/dist/protocol/server.d.ts +43 -12
  95. package/dist/protocol/server.js +326 -104
  96. package/dist/protocol/types.d.ts +51 -0
  97. package/dist/protocol/types.js +6 -0
  98. package/dist/run/FileRunStore.js +10 -1
  99. package/dist/run/Heartbeat.js +12 -0
  100. package/dist/run/RunApprovalBackend.d.ts +3 -0
  101. package/dist/run/RunApprovalBackend.js +41 -6
  102. package/dist/run/RunLock.js +2 -0
  103. package/dist/run/RunManager.d.ts +2 -0
  104. package/dist/run/RunManager.js +64 -24
  105. package/dist/run/ids.d.ts +2 -0
  106. package/dist/run/ids.js +23 -0
  107. package/dist/runtime/background-shell.d.ts +1 -0
  108. package/dist/runtime/background-shell.js +23 -13
  109. package/dist/runtime/safe-spawn.js +74 -11
  110. package/dist/runtime/spawn-common.js +10 -0
  111. package/dist/services/auto-dream.d.ts +15 -4
  112. package/dist/services/auto-dream.js +20 -20
  113. package/dist/services/dream-consolidation.d.ts +2 -3
  114. package/dist/services/dream-consolidation.js +65 -15
  115. package/dist/services/extract-memories.d.ts +14 -5
  116. package/dist/services/extract-memories.js +20 -3
  117. package/dist/services/global-dream-promotion.d.ts +23 -0
  118. package/dist/services/global-dream-promotion.js +112 -0
  119. package/dist/services/memory-orchestrator.js +347 -34
  120. package/dist/session/memory.d.ts +61 -18
  121. package/dist/session/memory.js +342 -79
  122. package/dist/session/session-manager.d.ts +35 -1
  123. package/dist/session/session-manager.js +197 -4
  124. package/dist/session/transcript.d.ts +5 -1
  125. package/dist/session/transcript.js +38 -5
  126. package/dist/settings/manager.d.ts +1 -0
  127. package/dist/settings/manager.js +87 -37
  128. package/dist/settings/schema-export.d.ts +2 -3
  129. package/dist/settings/schema-export.js +2 -3
  130. package/dist/settings/schema.d.ts +21 -0
  131. package/dist/settings/schema.js +12 -0
  132. package/dist/skills/scanner.d.ts +3 -2
  133. package/dist/skills/scanner.js +12 -9
  134. package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
  135. package/dist/tool-system/builtin/background-jobs.js +12 -1
  136. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  137. package/dist/tool-system/builtin/background-work.js +51 -5
  138. package/dist/tool-system/builtin/config.d.ts +2 -1
  139. package/dist/tool-system/builtin/config.js +16 -11
  140. package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
  141. package/dist/tool-system/builtin/drive-claude-code.js +301 -47
  142. package/dist/tool-system/builtin/edit.js +5 -2
  143. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  144. package/dist/tool-system/builtin/generate-video.js +13 -4
  145. package/dist/tool-system/builtin/index.d.ts +8 -3
  146. package/dist/tool-system/builtin/index.js +37 -21
  147. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  148. package/dist/tool-system/builtin/lsp.js +6 -3
  149. package/dist/tool-system/builtin/memory.js +40 -8
  150. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  151. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  152. package/dist/tool-system/builtin/powershell.js +11 -7
  153. package/dist/tool-system/builtin/read.js +118 -6
  154. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  155. package/dist/tool-system/builtin/view-image.js +109 -19
  156. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  157. package/dist/tool-system/builtin/worktree.js +297 -74
  158. package/dist/tool-system/builtin/write.js +5 -3
  159. package/dist/tool-system/context.d.ts +23 -1
  160. package/dist/tool-system/executor.d.ts +1 -5
  161. package/dist/tool-system/executor.js +94 -115
  162. package/dist/tool-system/mcp-manager.d.ts +18 -5
  163. package/dist/tool-system/mcp-manager.js +157 -74
  164. package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
  165. package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
  166. package/dist/tool-system/path-policy.d.ts +2 -0
  167. package/dist/tool-system/path-policy.js +41 -12
  168. package/dist/tool-system/permission.d.ts +28 -7
  169. package/dist/tool-system/permission.js +130 -49
  170. package/dist/tool-system/registry.js +11 -4
  171. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  172. package/dist/tool-system/tool-result-redaction.js +48 -0
  173. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  174. package/dist/tool-system/workspace-bridge.js +1 -0
  175. package/dist/types.d.ts +86 -5
  176. package/dist/utils/toolDisplay.js +1 -1
  177. package/package.json +4 -3
@@ -0,0 +1,112 @@
1
+ import { MemoryManager, resolveMemoryBaseDir } from "../session/memory.js";
2
+ export function detectUserDirectGlobalPreference(transcript) {
3
+ const userText = transcript
4
+ .filter((m) => m.role === "user")
5
+ .map((m) => m.content)
6
+ .join("\n")
7
+ .toLowerCase();
8
+ return (/(?:全局记住|全局保存|所有项目|全部项目|以后所有项目|今后所有项目|以后都|以后默认|我偏好|我的偏好)/u.test(userText) ||
9
+ /(?:remember globally|global memory|all projects|every project|my preference|i prefer|always remember)/i.test(userText));
10
+ }
11
+ export function applyGlobalDreamPromotionGate(input) {
12
+ const baseDir = resolveMemoryBaseDir(input.baseDir);
13
+ const now = input.now ?? new Date();
14
+ const nowIso = now.toISOString();
15
+ const promotionReason = input.userDirectGlobal
16
+ ? "user-direct global preference suggested global dream approval"
17
+ : "candidate suggested global dream approval";
18
+ const projectEvidence = input.projectDir
19
+ ? saveProjectDreamCandidate({
20
+ baseDir,
21
+ projectDir: input.projectDir,
22
+ candidate: input.candidate,
23
+ nowIso,
24
+ promotionReason,
25
+ })
26
+ : undefined;
27
+ const originProjects = projectEvidence?.originProjects ?? (input.projectDir ? [input.projectDir] : []);
28
+ const rejected = projectEvidence?.promotionStatus === "rejected";
29
+ const pendingSuggested = !rejected &&
30
+ savePendingGlobalSuggestion({
31
+ baseDir,
32
+ projectDir: input.projectDir,
33
+ candidate: input.candidate,
34
+ source: projectEvidence,
35
+ originProjects,
36
+ nowIso,
37
+ promotionReason,
38
+ });
39
+ return {
40
+ promoted: false,
41
+ pendingSuggested,
42
+ originProjects,
43
+ evidenceCount: originProjects.length,
44
+ projectEvidenceSaved: Boolean(projectEvidence),
45
+ promotionReason,
46
+ };
47
+ }
48
+ function saveProjectDreamCandidate(input) {
49
+ const mm = new MemoryManager({
50
+ baseDir: input.baseDir,
51
+ projectDir: input.projectDir,
52
+ scope: "dream",
53
+ });
54
+ const existing = mm
55
+ .loadAll()
56
+ .find((entry) => entry.name === input.candidate.name &&
57
+ (entry.origin === "auto" || entry.origin === "dream"));
58
+ const originProjects = unique([...(existing?.originProjects ?? []), input.projectDir]);
59
+ const promotionStatus = existing?.promotionStatus === "rejected" ? "rejected" : "pending";
60
+ const fileName = mm.save({
61
+ id: existing?.id,
62
+ name: input.candidate.name,
63
+ description: input.candidate.description,
64
+ type: input.candidate.type,
65
+ content: input.candidate.content,
66
+ origin: existing?.origin ?? "auto",
67
+ pinned: existing?.pinned,
68
+ createdAt: existing?.createdAt,
69
+ useCount: existing?.useCount,
70
+ updateCount: existing?.updateCount,
71
+ lastUsedAt: existing?.lastUsedAt,
72
+ originProject: input.projectDir,
73
+ originProjects,
74
+ evidenceCount: originProjects.length,
75
+ firstSeenAt: existing?.firstSeenAt ?? existing?.createdAt ?? input.nowIso,
76
+ lastSeenAt: input.nowIso,
77
+ promotionReason: input.promotionReason,
78
+ promotionStatus,
79
+ }, { forceOrigin: existing?.origin ?? "auto" });
80
+ return mm.find(fileName);
81
+ }
82
+ function savePendingGlobalSuggestion(input) {
83
+ const globalDream = new MemoryManager({ baseDir: input.baseDir, scope: "dream" });
84
+ if (globalDream.loadAll().some((entry) => entry.name === input.candidate.name))
85
+ return false;
86
+ const pending = new MemoryManager({ baseDir: input.baseDir, scope: "pending" });
87
+ const duplicate = pending
88
+ .loadAll()
89
+ .some((entry) => entry.name === input.candidate.name &&
90
+ (entry.originProject ?? "") === (input.projectDir ?? ""));
91
+ if (duplicate)
92
+ return false;
93
+ pending.save({
94
+ name: input.candidate.name,
95
+ description: input.candidate.description,
96
+ type: input.candidate.type,
97
+ content: input.candidate.content,
98
+ origin: "dream",
99
+ originProject: input.projectDir,
100
+ originProjects: input.originProjects,
101
+ evidenceCount: input.originProjects.length,
102
+ firstSeenAt: input.source?.firstSeenAt ?? input.source?.createdAt ?? input.nowIso,
103
+ lastSeenAt: input.nowIso,
104
+ promotionReason: input.promotionReason,
105
+ promotionStatus: "pending",
106
+ promotionSourceId: input.source?.id,
107
+ }, { forceOrigin: "dream" });
108
+ return true;
109
+ }
110
+ function unique(values) {
111
+ return [...new Set(values.filter((value) => Boolean(value)))];
112
+ }
@@ -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.loadAll();
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
- // Route by scope (用户拍板 审批门): "project" memories auto-land in the
61
- // per-project store (`mm`). "global" memories do NOT auto-land global —
62
- // they go to the GLOBAL pending scope and wait for the user to approve in
63
- // the settings panel. Nothing auto-writes the injected global store.
64
- let pendingMm = null;
65
- let globalCount = 0;
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 isGlobal = entry.scope === "global";
68
- const target = isGlobal
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
- // Provenance mark (feedback#18 方案 C): extractor writes are "auto"
81
- // so the UI can tell curated memories from extractor noise.
82
- origin: "auto",
83
- // 审批门: stamp the source project on pending entries so "不批准/降级"
84
- // can fall them back to the project they were extracted in.
85
- ...(isGlobal && this.options.projectDir
86
- ? { originProject: this.options.projectDir }
87
- : {}),
88
- });
89
- if (isGlobal)
90
- globalCount++;
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
- pendingGlobalCount: globalCount, // global candidates queued for approval
98
- projectCount: extracted - globalCount,
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
+ }