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