@cjhyy/code-shell-core 0.6.0-rc.9 → 0.7.0

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 (193) 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 +4 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +11 -1
  8. package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
  9. package/dist/cc-orchestrator/codex-session-history.js +65 -5
  10. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  11. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  13. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  14. package/dist/cc-orchestrator/external-agent-changes.js +22 -5
  15. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  16. package/dist/cc-orchestrator/external-agent-driver.js +265 -50
  17. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  18. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  19. package/dist/cc-orchestrator/session-history.d.ts +35 -0
  20. package/dist/cc-orchestrator/session-history.js +98 -15
  21. package/dist/cli/agent-server-stdio.js +9 -2
  22. package/dist/context/compaction.d.ts +8 -1
  23. package/dist/context/compaction.js +49 -4
  24. package/dist/context/manager.d.ts +16 -2
  25. package/dist/context/manager.js +103 -19
  26. package/dist/credentials/access.d.ts +57 -0
  27. package/dist/credentials/access.js +185 -0
  28. package/dist/credentials/index.d.ts +3 -1
  29. package/dist/credentials/index.js +2 -0
  30. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  31. package/dist/credentials/inject-credential-tool.js +30 -11
  32. package/dist/credentials/oauth.d.ts +20 -0
  33. package/dist/credentials/oauth.js +114 -0
  34. package/dist/credentials/store.d.ts +1 -0
  35. package/dist/credentials/store.js +3 -1
  36. package/dist/credentials/types.d.ts +49 -3
  37. package/dist/credentials/use-credential-tool.d.ts +9 -1
  38. package/dist/credentials/use-credential-tool.js +58 -45
  39. package/dist/engine/engine.d.ts +23 -3
  40. package/dist/engine/engine.js +380 -255
  41. package/dist/engine/goal.d.ts +17 -0
  42. package/dist/engine/goal.js +16 -6
  43. package/dist/engine/image-policy.d.ts +6 -0
  44. package/dist/engine/image-policy.js +17 -6
  45. package/dist/engine/input-attachments.d.ts +13 -0
  46. package/dist/engine/input-attachments.js +398 -0
  47. package/dist/engine/model-facade.d.ts +5 -2
  48. package/dist/engine/model-facade.js +4 -4
  49. package/dist/engine/parse-task.d.ts +10 -0
  50. package/dist/engine/parse-task.js +5 -0
  51. package/dist/engine/run-image-input.d.ts +22 -0
  52. package/dist/engine/run-image-input.js +195 -0
  53. package/dist/engine/steer-queue.d.ts +3 -1
  54. package/dist/engine/steer-queue.js +10 -2
  55. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  56. package/dist/engine/streaming-tool-queue.js +11 -7
  57. package/dist/engine/turn-loop.d.ts +37 -2
  58. package/dist/engine/turn-loop.js +228 -43
  59. package/dist/engine/types.d.ts +8 -0
  60. package/dist/git/worktree/crud.d.ts +69 -0
  61. package/dist/git/worktree/crud.js +206 -0
  62. package/dist/git/worktree/diff.d.ts +14 -0
  63. package/dist/git/worktree/diff.js +82 -0
  64. package/dist/git/worktree/git-exec.d.ts +7 -0
  65. package/dist/git/worktree/git-exec.js +51 -0
  66. package/dist/git/worktree/index.d.ts +5 -0
  67. package/dist/git/worktree/index.js +5 -0
  68. package/dist/git/worktree/query.d.ts +42 -0
  69. package/dist/git/worktree/query.js +121 -0
  70. package/dist/git/worktree/slug.d.ts +11 -0
  71. package/dist/git/worktree/slug.js +58 -0
  72. package/dist/git/worktree.d.ts +1 -84
  73. package/dist/git/worktree.js +5 -230
  74. package/dist/hooks/goal-stop-hook.d.ts +33 -1
  75. package/dist/hooks/goal-stop-hook.js +202 -34
  76. package/dist/index.d.ts +31 -28
  77. package/dist/index.js +27 -25
  78. package/dist/logging/logger.js +6 -6
  79. package/dist/logging/sanitize-messages.d.ts +10 -2
  80. package/dist/logging/sanitize-messages.js +21 -6
  81. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  82. package/dist/plugins/installer/checkUpdate.js +4 -2
  83. package/dist/plugins/installer/install.js +2 -0
  84. package/dist/plugins/installer/installFromSource.js +9 -1
  85. package/dist/plugins/installer/parseSource.d.ts +4 -1
  86. package/dist/plugins/installer/parseSource.js +28 -10
  87. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  88. package/dist/plugins/installer/sourcePath.js +50 -0
  89. package/dist/plugins/installer/update.d.ts +4 -1
  90. package/dist/plugins/installer/update.js +5 -3
  91. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  92. package/dist/plugins/parseMarketplaceInput.js +4 -3
  93. package/dist/plugins/pluginInstaller.js +24 -22
  94. package/dist/preset/index.d.ts +1 -0
  95. package/dist/preset/index.js +30 -13
  96. package/dist/prompt/sections/base.md +1 -1
  97. package/dist/protocol/chat-session-manager.d.ts +2 -0
  98. package/dist/protocol/chat-session-manager.js +38 -2
  99. package/dist/protocol/chat-session.d.ts +3 -0
  100. package/dist/protocol/chat-session.js +1 -0
  101. package/dist/protocol/client.d.ts +9 -4
  102. package/dist/protocol/client.js +18 -1
  103. package/dist/protocol/server.d.ts +30 -0
  104. package/dist/protocol/server.js +247 -47
  105. package/dist/protocol/types.d.ts +49 -0
  106. package/dist/protocol/types.js +6 -0
  107. package/dist/run/FileRunStore.js +10 -1
  108. package/dist/run/Heartbeat.js +12 -0
  109. package/dist/run/RunApprovalBackend.d.ts +3 -0
  110. package/dist/run/RunApprovalBackend.js +41 -6
  111. package/dist/run/RunLock.js +2 -0
  112. package/dist/run/RunManager.d.ts +2 -0
  113. package/dist/run/RunManager.js +64 -24
  114. package/dist/run/ids.d.ts +2 -0
  115. package/dist/run/ids.js +23 -0
  116. package/dist/runtime/background-shell.d.ts +1 -0
  117. package/dist/runtime/background-shell.js +23 -13
  118. package/dist/runtime/spawn-common.js +10 -0
  119. package/dist/services/auto-dream.d.ts +15 -4
  120. package/dist/services/auto-dream.js +20 -20
  121. package/dist/services/dream-consolidation.d.ts +2 -3
  122. package/dist/services/dream-consolidation.js +65 -15
  123. package/dist/services/extract-memories.d.ts +14 -5
  124. package/dist/services/extract-memories.js +20 -3
  125. package/dist/services/global-dream-promotion.d.ts +23 -0
  126. package/dist/services/global-dream-promotion.js +112 -0
  127. package/dist/services/memory-orchestrator.js +347 -34
  128. package/dist/session/memory.d.ts +61 -18
  129. package/dist/session/memory.js +342 -79
  130. package/dist/session/session-manager.d.ts +35 -1
  131. package/dist/session/session-manager.js +224 -4
  132. package/dist/session/transcript.d.ts +1 -1
  133. package/dist/session/transcript.js +17 -5
  134. package/dist/settings/manager.d.ts +1 -0
  135. package/dist/settings/manager.js +87 -37
  136. package/dist/settings/schema-export.d.ts +2 -3
  137. package/dist/settings/schema-export.js +2 -3
  138. package/dist/settings/schema.d.ts +21 -0
  139. package/dist/settings/schema.js +12 -0
  140. package/dist/skills/scanner.d.ts +3 -2
  141. package/dist/skills/scanner.js +12 -9
  142. package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
  143. package/dist/tool-system/builtin/agent-notifications.js +19 -7
  144. package/dist/tool-system/builtin/background-jobs.d.ts +35 -6
  145. package/dist/tool-system/builtin/background-jobs.js +116 -7
  146. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  147. package/dist/tool-system/builtin/background-work.js +51 -5
  148. package/dist/tool-system/builtin/config.d.ts +2 -1
  149. package/dist/tool-system/builtin/config.js +16 -11
  150. package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
  151. package/dist/tool-system/builtin/cron-list.definition.js +6 -0
  152. package/dist/tool-system/builtin/cron.d.ts +1 -2
  153. package/dist/tool-system/builtin/cron.js +9 -7
  154. package/dist/tool-system/builtin/drive-claude-code.d.ts +23 -2
  155. package/dist/tool-system/builtin/drive-claude-code.js +589 -48
  156. package/dist/tool-system/builtin/edit.js +5 -2
  157. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  158. package/dist/tool-system/builtin/generate-video.js +13 -4
  159. package/dist/tool-system/builtin/index.d.ts +8 -3
  160. package/dist/tool-system/builtin/index.js +50 -22
  161. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  162. package/dist/tool-system/builtin/lsp.js +6 -3
  163. package/dist/tool-system/builtin/memory.js +40 -8
  164. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  165. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  166. package/dist/tool-system/builtin/powershell.js +11 -7
  167. package/dist/tool-system/builtin/read.js +118 -6
  168. package/dist/tool-system/builtin/sleep.d.ts +1 -2
  169. package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
  170. package/dist/tool-system/builtin/sleep.definition.js +28 -0
  171. package/dist/tool-system/builtin/sleep.js +1 -22
  172. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  173. package/dist/tool-system/builtin/view-image.js +109 -19
  174. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  175. package/dist/tool-system/builtin/worktree.js +297 -74
  176. package/dist/tool-system/builtin/write.js +5 -3
  177. package/dist/tool-system/context.d.ts +41 -1
  178. package/dist/tool-system/executor.d.ts +1 -5
  179. package/dist/tool-system/executor.js +94 -115
  180. package/dist/tool-system/mcp-manager.d.ts +31 -6
  181. package/dist/tool-system/mcp-manager.js +193 -78
  182. package/dist/tool-system/path-policy.d.ts +2 -0
  183. package/dist/tool-system/path-policy.js +41 -12
  184. package/dist/tool-system/permission.d.ts +28 -7
  185. package/dist/tool-system/permission.js +130 -49
  186. package/dist/tool-system/registry.js +11 -4
  187. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  188. package/dist/tool-system/tool-result-redaction.js +48 -0
  189. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  190. package/dist/tool-system/workspace-bridge.js +1 -0
  191. package/dist/types.d.ts +78 -11
  192. package/dist/utils/toolDisplay.js +1 -1
  193. package/package.json +4 -3
@@ -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; user/ is read-only to dream.
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) => e.pinned || !e.updatedAt || e.updatedAt >= cutoff);
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() { return this.memoryDir; }
87
- getScope() { return this.scope; }
105
+ getMemoryDir() {
106
+ return this.memoryDir;
107
+ }
108
+ getScope() {
109
+ return this.scope;
110
+ }
88
111
  /**
89
- * Save a memory entry. Creates the file and updates the index.
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
- // Overwrite = UPDATE: preserve the original `created` and accumulated
96
- // `usageCount` of the file being replaced (only `created` is load-bearing
97
- // for TTL; carrying usageCount avoids resetting a popular memory's score).
98
- // explicit entry fields win over the existing file (lets callers force a value).
99
- const existing = existsSync(filePath) ? this.loadFile(fileName) : null;
100
- const created = entry.created ?? existing?.created ?? nowIso;
101
- const usageCount = entry.usageCount ?? existing?.usageCount ?? 0;
102
- const lastUsed = entry.lastUsed ?? existing?.lastUsed ?? created;
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
- // pinned/origin are written only when meaningful so legacy-shaped files
105
- // stay byte-identical for unpinned manual saves. Lifecycle fields are
106
- // always written now (created/lastUsed/usageCount) — they're how TTL works.
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
- `name: ${entry.name}\n` +
109
- `description: ${entry.description}\n` +
110
- `type: ${entry.type}\n` +
111
- (entry.pinned ? `pinned: true\n` : "") +
112
- (entry.origin ? `origin: ${entry.origin}\n` : "") +
113
- (originProject ? `originProject: ${originProject}\n` : "") +
114
- `created: ${created}\n` +
115
- `lastUsed: ${lastUsed}\n` +
116
- `usageCount: ${usageCount}\n` +
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 name = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim() ?? fileName;
169
- const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim() ?? "";
170
- const type = (frontmatter.match(/type:\s*(.+)/)?.[1]?.trim() ?? "project");
171
- const pinned = frontmatter.match(/pinned:\s*(.+)/)?.[1]?.trim() === "true";
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 = frontmatter.match(/^origin:\s*(.+)/m)?.[1]?.trim();
175
- const origin = originRaw === "auto" || originRaw === "manual" ? originRaw : undefined;
176
- const originProject = frontmatter.match(/^originProject:\s*(.+)/m)?.[1]?.trim() || undefined;
177
- let updatedAt = 0;
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
- updatedAt = statSync(filePath).mtimeMs;
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 = updatedAt > 0 ? new Date(updatedAt).toISOString() : undefined;
187
- const created = frontmatter.match(/created:\s*(.+)/)?.[1]?.trim() || mtimeIso;
188
- const lastUsed = frontmatter.match(/lastUsed:\s*(.+)/)?.[1]?.trim() || created;
189
- const usageRaw = frontmatter.match(/usageCount:\s*(\d+)/)?.[1];
190
- const usageCount = usageRaw ? parseInt(usageRaw, 10) : 0;
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
- name, description, type, content, fileName, scope,
193
- updatedAt, pinned, origin, usageCount, lastUsed, created, originProject,
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
- created: entry.created,
243
- usageCount: (entry.usageCount ?? 0) + 1,
244
- lastUsed: now.toISOString(),
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.lastUsed ? new Date(e.lastUsed).getTime() : NaN;
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 user scope
281
- * of the SAME memory root (pending only ever exists at the global root — the
282
- * approval only gates global writes). Must be called on a pending-scope
283
- * manager. Returns the new user filename, or null if not found / wrong scope.
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 an auto-extracted memory into the
286
- * curated, injected global user store.
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
- /** Shared move for approve(→global user)/demote(→origin-project user). */
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.loadAll().find((e) => e.name === nameOrFile || e.fileName === nameOrFile);
523
+ const entry = this.find(nameOrFile);
303
524
  if (!entry)
304
525
  return null;
305
- const targetMgr = dest === "project" && entry.originProject
306
- ? new MemoryManager({ baseDir: this.baseDir, projectDir: entry.originProject, scope: "user" })
307
- : new MemoryManager({ baseDir: this.baseDir, scope: "user" }); // global, or fallback
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: entry.origin ?? "auto",
314
- created: entry.created,
315
- lastUsed: entry.lastUsed,
316
- usageCount: entry.usageCount,
317
- // originProject is no longer needed once it has landed in a real store.
318
- });
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
- created: entry.created,
341
- lastUsed: entry.lastUsed,
342
- usageCount: entry.usageCount,
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
- slugify(text) {
526
- return text
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-9]+/g, "-")
529
- .replace(/^-|-$/g, "")
530
- .slice(0, 60);
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
  }
@@ -1,12 +1,25 @@
1
1
  /**
2
2
  * Session lifecycle manager.
3
3
  */
4
- import type { SessionState } from "../types.js";
4
+ import type { SessionState, SessionWorkspace } from "../types.js";
5
5
  import { Transcript } from "./transcript.js";
6
6
  export interface SessionBundle {
7
7
  state: SessionState;
8
8
  transcript: Transcript;
9
9
  }
10
+ export type SessionWorkspaceResumeResolution = {
11
+ ok: true;
12
+ cwd: string;
13
+ workspace: SessionWorkspace;
14
+ message?: string;
15
+ reason?: "main" | "worktree" | "legacy" | "worktree_missing_branch_gone";
16
+ } | {
17
+ ok: false;
18
+ cwd: string;
19
+ workspace: SessionWorkspace;
20
+ message: string;
21
+ reason: "worktree_missing_branch_exists";
22
+ };
10
23
  /**
11
24
  * Validate a session ID before it is joined into a filesystem path.
12
25
  *
@@ -57,6 +70,27 @@ export declare class SessionManager {
57
70
  * caller treats "no recoverable cwd" the same as "not given".
58
71
  */
59
72
  readCwd(sessionId: string): string | undefined;
73
+ /**
74
+ * Disk-only workspace pointer reader. Legacy sessions written before
75
+ * `workspace` existed are treated as main-workspace sessions rooted at
76
+ * `state.cwd`; the read is intentionally non-mutating.
77
+ */
78
+ getSessionWorkspace(sessionId: string): SessionWorkspace | undefined;
79
+ /**
80
+ * Persist the current session workspace pointer without changing legacy
81
+ * `cwd`. P1 will teach ToolContext to resolve cwd from this field; for P0 it
82
+ * is a safety pointer and resume breadcrumb.
83
+ */
84
+ setSessionWorkspace(sessionId: string, workspace: SessionWorkspace): void;
85
+ recordWorkspaceHandoff(sessionId: string, from: SessionWorkspace | undefined, to: SessionWorkspace): void;
86
+ /**
87
+ * Resolve the cwd a resumed session must run in from its persisted workspace
88
+ * pointer. A missing worktree directory is never silently treated as the main
89
+ * repo: if the branch still exists callers get a blocking recreate message;
90
+ * if the branch is gone the workspace pointer is reset to main and a warning
91
+ * message is returned for the host to surface before continuing.
92
+ */
93
+ resolveSessionWorkspaceForResume(sessionId: string): Promise<SessionWorkspaceResumeResolution>;
60
94
  /**
61
95
  * Cheap "does this session have a persisted goal?" probe — reads only
62
96
  * state.json, NOT the transcript (like readCwd). A persistent goal lives ONLY