@cjhyy/code-shell-core 0.6.0-rc.14 → 0.6.0-rc.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/capability-control/service.d.ts +2 -0
  2. package/dist/capability-control/service.js +4 -2
  3. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  4. package/dist/cc-orchestrator/external-agent-bindings.js +1 -1
  5. package/dist/cc-orchestrator/external-agent-session-store.js +3 -1
  6. package/dist/cc-orchestrator/session-history.js +2 -2
  7. package/dist/cli/agent-server-stdio.js +7 -2
  8. package/dist/context/manager.d.ts +11 -2
  9. package/dist/context/manager.js +64 -3
  10. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  11. package/dist/credentials/inject-credential-tool.js +29 -10
  12. package/dist/credentials/use-credential-tool.d.ts +1 -0
  13. package/dist/credentials/use-credential-tool.js +3 -0
  14. package/dist/engine/engine.d.ts +9 -0
  15. package/dist/engine/engine.js +96 -17
  16. package/dist/engine/turn-loop.d.ts +3 -1
  17. package/dist/engine/turn-loop.js +3 -1
  18. package/dist/engine/types.d.ts +8 -0
  19. package/dist/git/worktree/crud.d.ts +69 -0
  20. package/dist/git/worktree/crud.js +206 -0
  21. package/dist/git/worktree/diff.d.ts +14 -0
  22. package/dist/git/worktree/diff.js +82 -0
  23. package/dist/git/worktree/git-exec.d.ts +7 -0
  24. package/dist/git/worktree/git-exec.js +51 -0
  25. package/dist/git/worktree/index.d.ts +5 -0
  26. package/dist/git/worktree/index.js +5 -0
  27. package/dist/git/worktree/query.d.ts +42 -0
  28. package/dist/git/worktree/query.js +121 -0
  29. package/dist/git/worktree/slug.d.ts +11 -0
  30. package/dist/git/worktree/slug.js +58 -0
  31. package/dist/git/worktree.d.ts +1 -127
  32. package/dist/git/worktree.js +5 -464
  33. package/dist/index.d.ts +28 -27
  34. package/dist/index.js +24 -24
  35. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  36. package/dist/plugins/installer/checkUpdate.js +4 -2
  37. package/dist/plugins/installer/install.js +2 -0
  38. package/dist/plugins/installer/parseSource.d.ts +4 -1
  39. package/dist/plugins/installer/parseSource.js +28 -10
  40. package/dist/plugins/installer/update.d.ts +4 -1
  41. package/dist/plugins/installer/update.js +5 -3
  42. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  43. package/dist/plugins/parseMarketplaceInput.js +4 -3
  44. package/dist/preset/index.d.ts +1 -0
  45. package/dist/preset/index.js +6 -0
  46. package/dist/prompt/sections/base.md +1 -1
  47. package/dist/protocol/chat-session-manager.js +7 -0
  48. package/dist/protocol/server.d.ts +10 -0
  49. package/dist/protocol/server.js +88 -4
  50. package/dist/protocol/types.d.ts +6 -0
  51. package/dist/protocol/types.js +2 -0
  52. package/dist/runtime/background-shell.js +2 -3
  53. package/dist/services/auto-dream.d.ts +15 -4
  54. package/dist/services/auto-dream.js +20 -20
  55. package/dist/services/dream-consolidation.d.ts +2 -3
  56. package/dist/services/dream-consolidation.js +65 -15
  57. package/dist/services/extract-memories.d.ts +14 -5
  58. package/dist/services/extract-memories.js +20 -3
  59. package/dist/services/global-dream-promotion.d.ts +23 -0
  60. package/dist/services/global-dream-promotion.js +112 -0
  61. package/dist/services/memory-orchestrator.js +347 -34
  62. package/dist/session/memory.d.ts +56 -17
  63. package/dist/session/memory.js +337 -78
  64. package/dist/session/session-manager.d.ts +1 -1
  65. package/dist/session/session-manager.js +6 -6
  66. package/dist/settings/manager.js +43 -12
  67. package/dist/settings/schema.d.ts +21 -0
  68. package/dist/settings/schema.js +12 -0
  69. package/dist/tool-system/builtin/index.js +18 -8
  70. package/dist/tool-system/builtin/memory.js +40 -8
  71. package/dist/tool-system/builtin/worktree.d.ts +2 -0
  72. package/dist/tool-system/builtin/worktree.js +59 -11
  73. package/dist/tool-system/context.d.ts +11 -1
  74. package/dist/tool-system/mcp-manager.d.ts +8 -5
  75. package/dist/tool-system/mcp-manager.js +85 -55
  76. package/dist/tool-system/path-policy.d.ts +2 -0
  77. package/dist/tool-system/path-policy.js +28 -12
  78. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  79. package/dist/tool-system/workspace-bridge.js +1 -0
  80. package/dist/types.d.ts +14 -0
  81. package/package.json +1 -1
@@ -50,6 +50,12 @@ export interface AgentServerOptions {
50
50
  * reopened-session path can omit it.
51
51
  */
52
52
  readActiveGoalFromDisk?: (sessionId: string) => import("../engine/goal.js").GoalConfig | undefined;
53
+ /**
54
+ * Enable the desktop workspace bridge channel. Off by default so generic
55
+ * protocol hosts do not emit hidden workspace approval requests they cannot
56
+ * service.
57
+ */
58
+ workspaceBridge?: boolean;
53
59
  }
54
60
  export declare class AgentServer {
55
61
  private readonly chatManager;
@@ -60,6 +66,7 @@ export declare class AgentServer {
60
66
  private readonly settingsReader;
61
67
  /** Disk-only active-goal reader for agent/goalGet on a non-live session. */
62
68
  private readonly readActiveGoalFromDisk;
69
+ private readonly workspaceBridgeEnabled;
63
70
  /**
64
71
  * Monotonic config-reload version, bumped per reloadSettings request so each
65
72
  * Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
@@ -178,6 +185,7 @@ export declare class AgentServer {
178
185
  */
179
186
  private handleBackgroundWork;
180
187
  private handleCloseSession;
188
+ private handleReleaseWorkspace;
181
189
  private handleConfigure;
182
190
  private handleQuery;
183
191
  private handleInject;
@@ -211,6 +219,8 @@ export declare class AgentServer {
211
219
  * parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
212
220
  */
213
221
  private requestCredentialInjectForSession;
222
+ private makeWorkspaceBridge;
223
+ private requestWorkspaceSwitchForSession;
214
224
  /**
215
225
  * Ask the client to answer a question from the agent (legacy single-engine
216
226
  * path). This intentionally has no wall-clock timeout; Stop/cancel drains
@@ -48,6 +48,7 @@ export class AgentServer {
48
48
  settingsReader;
49
49
  /** Disk-only active-goal reader for agent/goalGet on a non-live session. */
50
50
  readActiveGoalFromDisk;
51
+ workspaceBridgeEnabled;
51
52
  /**
52
53
  * Monotonic config-reload version, bumped per reloadSettings request so each
53
54
  * Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
@@ -93,6 +94,7 @@ export class AgentServer {
93
94
  this.legacyEngine = options.engine ?? null;
94
95
  this.settingsReader = options.settingsReader ?? null;
95
96
  this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
97
+ this.workspaceBridgeEnabled = options.workspaceBridge === true;
96
98
  if (!this.chatManager && !this.legacyEngine) {
97
99
  throw new Error("AgentServer: either chatManager or engine must be supplied");
98
100
  }
@@ -267,6 +269,9 @@ export class AgentServer {
267
269
  case Methods.CloseSession:
268
270
  this.handleCloseSession(req);
269
271
  break;
272
+ case Methods.ReleaseWorkspace:
273
+ this.handleReleaseWorkspace(req);
274
+ break;
270
275
  case Methods.GoalExtend:
271
276
  this.handleGoalExtend(req);
272
277
  break;
@@ -363,7 +368,10 @@ export class AgentServer {
363
368
  session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
364
369
  // Cookie→browser injection (InjectCredential tool): same cross-process
365
370
  // channel; main restores the cookie jar into the built-in browser.
366
- session.engine.setInjectCredential((credentialId) => this.requestCredentialInjectForSession(session, sid, credentialId));
371
+ session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
372
+ if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
373
+ session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
374
+ }
367
375
  }
368
376
  try {
369
377
  const result = await session.enqueueTurn(params.task, {
@@ -753,6 +761,28 @@ export class AgentServer {
753
761
  backgroundJobRegistry.dropForSession(params.sessionId);
754
762
  this.transport.send(createResponse(req.id, { ok: true }));
755
763
  }
764
+ // ─── ReleaseWorkspace ──────────────────────────────────────────
765
+ handleReleaseWorkspace(req) {
766
+ const params = (req.params ?? {});
767
+ if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
768
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
769
+ return;
770
+ }
771
+ if (this.chatManager) {
772
+ const session = this.chatManager.get(params.sessionId);
773
+ if (!session) {
774
+ this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
775
+ return;
776
+ }
777
+ const engine = session.engine;
778
+ const workspace = engine.releaseSessionWorkspace?.(params.sessionId) ?? null;
779
+ this.transport.send(createResponse(req.id, { ok: true, workspace }));
780
+ return;
781
+ }
782
+ const engine = this.legacyEngine;
783
+ const workspace = engine?.releaseSessionWorkspace?.(params.sessionId) ?? null;
784
+ this.transport.send(createResponse(req.id, { ok: true, workspace }));
785
+ }
756
786
  // ─── Configure ──────────────────────────────────────────────────
757
787
  handleConfigure(req) {
758
788
  const params = (req.params ?? {});
@@ -1501,7 +1531,7 @@ export class AgentServer {
1501
1531
  // ask and resolve with a nudge so the loop keeps making progress instead
1502
1532
  // of hanging. Plain interactive (no-goal) asks keep their intentional
1503
1533
  // no-timeout behavior — a human can take as long as they like.
1504
- let goalActive = false;
1534
+ let goalActive;
1505
1535
  try {
1506
1536
  goalActive = !!session.getGoal();
1507
1537
  }
@@ -1605,7 +1635,7 @@ export class AgentServer {
1605
1635
  * calls restoreCookiesToBrowser and replies with a JSON result string we
1606
1636
  * parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
1607
1637
  */
1608
- requestCredentialInjectForSession(session, sessionId, credentialId) {
1638
+ requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full") {
1609
1639
  return new Promise((resolve) => {
1610
1640
  const requestId = nanoid(12);
1611
1641
  session.pendingApprovals.set(requestId, (decision) => {
@@ -1642,13 +1672,67 @@ export class AgentServer {
1642
1672
  requestId,
1643
1673
  request: {
1644
1674
  toolName: "__credential_action__",
1645
- args: { action: "injectCookie", credentialId },
1675
+ args: { action: "injectCookie", credentialId, credentialScope },
1646
1676
  description: `credential:inject:${credentialId}`,
1647
1677
  riskLevel: "low",
1648
1678
  },
1649
1679
  });
1650
1680
  });
1651
1681
  }
1682
+ makeWorkspaceBridge(session, sessionId) {
1683
+ return {
1684
+ switch: (target) => this.requestWorkspaceSwitchForSession(session, sessionId, target),
1685
+ };
1686
+ }
1687
+ requestWorkspaceSwitchForSession(session, sessionId, target) {
1688
+ return new Promise((resolve, reject) => {
1689
+ const requestId = nanoid(12);
1690
+ session.pendingApprovals.set(requestId, (decision) => {
1691
+ this.clearApprovalTimer(requestId);
1692
+ let raw;
1693
+ if (decision && typeof decision === "object" && "approved" in decision) {
1694
+ const r = decision;
1695
+ raw = r.approved ? r.answer : undefined;
1696
+ }
1697
+ else if (typeof decision === "string") {
1698
+ raw = decision;
1699
+ }
1700
+ if (raw === undefined) {
1701
+ reject(new Error("workspace switch declined or unavailable"));
1702
+ return;
1703
+ }
1704
+ try {
1705
+ const parsed = JSON.parse(raw);
1706
+ if ("ok" in parsed && parsed.ok === false) {
1707
+ reject(new Error(parsed.error ?? "workspace switch failed"));
1708
+ return;
1709
+ }
1710
+ resolve(parsed);
1711
+ }
1712
+ catch {
1713
+ reject(new Error("malformed workspace switch result"));
1714
+ }
1715
+ });
1716
+ const timer = setTimeout(() => {
1717
+ if (session.pendingApprovals.has(requestId)) {
1718
+ session.pendingApprovals.delete(requestId);
1719
+ this.approvalTimers.delete(requestId);
1720
+ reject(new Error("workspace switch timed out"));
1721
+ }
1722
+ }, AgentServer.APPROVAL_TIMEOUT_MS);
1723
+ this.approvalTimers.set(requestId, timer);
1724
+ this.notify(Methods.ApprovalRequest, {
1725
+ sessionId,
1726
+ requestId,
1727
+ request: {
1728
+ toolName: "__workspace_action__",
1729
+ args: { action: "switch", target },
1730
+ description: `workspace:switch:${target}`,
1731
+ riskLevel: "low",
1732
+ },
1733
+ });
1734
+ });
1735
+ }
1652
1736
  /**
1653
1737
  * Ask the client to answer a question from the agent (legacy single-engine
1654
1738
  * path). This intentionally has no wall-clock timeout; Stop/cancel drains
@@ -123,6 +123,10 @@ export interface CancelParams {
123
123
  export interface CloseSessionParams {
124
124
  sessionId: string;
125
125
  }
126
+ /** Reset a live session's workspace binding back to main. */
127
+ export interface ReleaseWorkspaceParams {
128
+ sessionId: string;
129
+ }
126
130
  /** Inject context into a session transcript. */
127
131
  export interface InjectParams {
128
132
  sessionId: string;
@@ -279,6 +283,8 @@ export declare const Methods: {
279
283
  readonly Unsteer: "agent/unsteer";
280
284
  /** Close (destroy) a session. */
281
285
  readonly CloseSession: "agent/closeSession";
286
+ /** Reset a session's workspace binding to main without closing it. */
287
+ readonly ReleaseWorkspace: "agent/releaseWorkspace";
282
288
  /** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
283
289
  readonly GoalExtend: "agent/goalExtend";
284
290
  /** Clear a session's persisted active goal (CC /goal clear). */
@@ -41,6 +41,8 @@ export const Methods = {
41
41
  Unsteer: "agent/unsteer",
42
42
  /** Close (destroy) a session. */
43
43
  CloseSession: "agent/closeSession",
44
+ /** Reset a session's workspace binding to main without closing it. */
45
+ ReleaseWorkspace: "agent/releaseWorkspace",
44
46
  /** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
45
47
  GoalExtend: "agent/goalExtend",
46
48
  /** Clear a session's persisted active goal (CC /goal clear). */
@@ -23,7 +23,6 @@
23
23
  * worker discover and reap them (§难点1, `reapOrphansFromPidfiles`).
24
24
  */
25
25
  import { spawn } from "node:child_process";
26
- import { homedir } from "node:os";
27
26
  import { join } from "node:path";
28
27
  import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync, readFileSync, } from "node:fs";
29
28
  import { StringDecoder } from "node:string_decoder";
@@ -33,6 +32,7 @@ import { RingFile } from "./ring-file.js";
33
32
  import { cleanOutput } from "./output-clean.js";
34
33
  import { notificationQueue } from "../tool-system/builtin/agent-notifications.js";
35
34
  import { logger } from "../logging/logger.js";
35
+ import { codeShellHome } from "../session/session-manager.js";
36
36
  /** Per-session soft cap on background shells (design §7 — fork-bomb guard). */
37
37
  export const MAX_SHELLS_PER_SESSION = 16;
38
38
  /** Disk ring-file cap: keep the most recent 8MB of raw output (design §6). */
@@ -49,8 +49,7 @@ function nextShellId() {
49
49
  return `bg_${shellCounter.toString(36)}${(shellCounter * 2654435761 % 0xffffff).toString(36)}`;
50
50
  }
51
51
  function bgShellsRoot() {
52
- const base = process.env.CODE_SHELL_HOME ?? process.env.HOME ?? homedir();
53
- return join(base, ".code-shell", "bg-shells");
52
+ return join(codeShellHome(), "bg-shells");
54
53
  }
55
54
  const PORT_REGEXES = [
56
55
  /localhost:(\d{2,5})/i,
@@ -27,10 +27,9 @@ export declare function recordDreamComplete(): void;
27
27
  /**
28
28
  * System prompt for the auto-dream tool-call loop.
29
29
  *
30
- * Drives the LLM as a "memory consolidation assistant" that operates ONLY in
31
- * the dream scope via the MemorySave/MemoryDelete tools. The user scope is
32
- * read-only context modifying it would need permission, which a background
33
- * dream pass cannot obtain (no UI on the call path).
30
+ * Drives the LLM as a "memory consolidation assistant" that operates in dream
31
+ * scope and may maintain dream-owned user memories. Manual user memories are
32
+ * read-only and protected by the dispatcher.
34
33
  */
35
34
  export declare function buildDreamSystemPrompt(): string;
36
35
  /**
@@ -39,15 +38,27 @@ export declare function buildDreamSystemPrompt(): string;
39
38
  * body. The LLM uses MemoryRead to fetch bodies for entries it wants to act on.
40
39
  */
41
40
  export declare function buildDreamUserPrompt(userMemories: Array<{
41
+ id?: string;
42
42
  name: string;
43
43
  type: string;
44
44
  description: string;
45
+ origin?: string;
46
+ useCount?: number;
47
+ updateCount?: number;
45
48
  }>, dreamMemories: Array<{
49
+ id?: string;
46
50
  name: string;
47
51
  type: string;
48
52
  description: string;
53
+ origin?: string;
54
+ useCount?: number;
55
+ updateCount?: number;
49
56
  }>, globalMemories?: Array<{
57
+ id?: string;
50
58
  name: string;
51
59
  type: string;
52
60
  description: string;
61
+ origin?: string;
62
+ useCount?: number;
63
+ updateCount?: number;
53
64
  }>): string;
@@ -71,34 +71,34 @@ export function recordDreamComplete() {
71
71
  /**
72
72
  * System prompt for the auto-dream tool-call loop.
73
73
  *
74
- * Drives the LLM as a "memory consolidation assistant" that operates ONLY in
75
- * the dream scope via the MemorySave/MemoryDelete tools. The user scope is
76
- * read-only context modifying it would need permission, which a background
77
- * dream pass cannot obtain (no UI on the call path).
74
+ * Drives the LLM as a "memory consolidation assistant" that operates in dream
75
+ * scope and may maintain dream-owned user memories. Manual user memories are
76
+ * read-only and protected by the dispatcher.
78
77
  */
79
78
  export function buildDreamSystemPrompt() {
80
79
  return [
81
80
  "You are a memory consolidation assistant for the CodeShell memory system.",
82
81
  "",
83
- "Your job: clean up the `dream` scope by deduplicating, merging, removing stale, and improving descriptions.",
82
+ "Your job: clean up the `dream` scope by deduplicating, merging, removing stale, and improving descriptions, and promote only durable lessons into dream-owned `user` entries.",
84
83
  "",
85
84
  "Tools available (each takes `location`: 'global' = cross-project store, 'project' = this repo; default project):",
86
85
  "- MemoryList({ scope, location }): list memories in a scope/location",
87
86
  "- MemoryRead({ scope, location, name }): read full content of an entry",
88
- "- MemorySave({ scope: 'dream', location, ... }): create or overwrite a dream entry (auto-approved)",
89
- "- MemoryDelete({ scope: 'dream', location, name }): soft-delete a dream entry (auto-approved, recoverable from trash)",
87
+ "- MemorySave({ scope: 'dream'|'user', location, id?, ... }): create or update an entry by id",
88
+ "- MemoryDelete({ scope: 'dream'|'user', location, name }): soft-delete an owned entry (recoverable from trash)",
90
89
  "",
91
- "Consolidate BOTH the project dream scope AND the global dream scope (pass location accordingly). Global holds cross-project lessons; keep it deduped too.",
90
+ "Consolidate BOTH the project dream workspace AND the global dream workspace (pass location accordingly). Global dream is a cross-project workspace, not a dumping ground for single-project progress.",
92
91
  "",
93
92
  "Rules — read carefully:",
94
- "1. You may freely Save/Delete in the `dream` scope (any location). These operations DO NOT prompt the user.",
95
- "2. You may NOT Save/Delete in the `user` scope from this loop those operations require interactive permission which is not available here. Treat user-scope entries as read-only context.",
96
- "3. If you find user-scope entries that look stale, surface them in your final summary text don't try to delete them.",
97
- "4. Prefer fewer, higher-quality merged entries over many similar fragments.",
98
- "5. When an entry has versioned variants (e.g. `*-v1`, `*-v2`, `*-v3`), keep only the latest.",
99
- "6. Be conservative: if uncertain whether two entries are truly duplicates, leave them alone.",
100
- "7. Archive COMPLETED work: dream entries that only record a finished fix/task (\"已修\", \"已完成\", \"done\") and carry no reusable lesson should be deleted, or merged into one compact changelog-style entry — completed-state notes that only grow are the main source of clutter. Keep any durable lesson (root cause, pitfall, convention) by folding it into a topical entry first.",
101
- "8. When you're done, stop calling tools and respond with a one-paragraph summary of what you changed.",
93
+ "1. You may Save/Delete dream-scope entries only when they are origin:auto or origin:dream. Never modify origin:manual; missing origin means manual.",
94
+ "2. You may Save user-scope entries only for durable conclusions you own. The dispatcher will force origin:dream. You may update existing user entries only when they are origin:dream or origin:auto. Never modify/delete origin:manual user entries.",
95
+ "3. Use MemoryList first. If the same topic already has an id, update that id instead of creating a date, version, batch, or progress variant.",
96
+ "4. Cluster duplicates without vectors: compare stable topic words after removing dates, versions, batch numbers, and progress/completed wording.",
97
+ "5. Time-sensitivity rule: entries with dates, today/yesterday, progress snapshots, completed fixes, review batches, or one-off task state should stay in dream, be merged, or be deleted. Do not promote them to user.",
98
+ "6. Durable lessons may be promoted to user only when they are reusable: user preferences, long-term project constraints, architecture decisions, root-cause lessons, non-obvious test/build traps, or stable references.",
99
+ '7. Archive COMPLETED work: dream entries that only record a finished fix/task ("已修", "已完成", "done") and carry no reusable lesson should be deleted, or merged into one compact topical entry. Keep any durable lesson by folding it into a date-free topic entry first.',
100
+ "8. Prefer fewer, higher-quality merged entries over many similar fragments. Be conservative: if uncertain whether two entries are truly duplicates, leave them alone.",
101
+ "9. When you're done, stop calling tools and respond with a one-paragraph summary of what you changed.",
102
102
  ].join("\n");
103
103
  }
104
104
  /**
@@ -107,18 +107,18 @@ export function buildDreamSystemPrompt() {
107
107
  * body. The LLM uses MemoryRead to fetch bodies for entries it wants to act on.
108
108
  */
109
109
  export function buildDreamUserPrompt(userMemories, dreamMemories, globalMemories = []) {
110
- const fmt = (m) => ` - [${m.type}] ${m.name}: ${m.description}`;
110
+ const fmt = (m) => ` - [${m.type}] ${m.name} (id:${m.id ?? "(none)"}, origin:${m.origin ?? "manual"}, use:${m.useCount ?? 0}, updates:${m.updateCount ?? 0}): ${m.description}`;
111
111
  const listOrNone = (arr, noneMsg) => arr.length === 0 ? noneMsg : arr.map(fmt).join("\n");
112
112
  const sections = [];
113
- sections.push(`Project user-scope memories (READ-ONLY context, ${userMemories.length} entries):`);
113
+ sections.push(`Project user-scope memories (manual is READ-ONLY; origin:dream/auto may be maintained by id, ${userMemories.length} entries):`);
114
114
  sections.push(listOrNone(userMemories, " (none)"));
115
115
  sections.push("");
116
116
  sections.push(`Project dream-scope memories (YOUR WORKSPACE — location:'project', ${dreamMemories.length} entries):`);
117
117
  sections.push(listOrNone(dreamMemories, " (none — you may consolidate from user-scope by re-saving curated entries into dream)"));
118
118
  sections.push("");
119
- sections.push(`Global memories (cross-project — clean these too via location:'global', ${globalMemories.length} entries):`);
119
+ sections.push(`Global dream workspace memories (cross-project dream — clean these too via location:'global', ${globalMemories.length} entries):`);
120
120
  sections.push(listOrNone(globalMemories, " (none)"));
121
121
  sections.push("");
122
- sections.push("Begin consolidation. Use MemoryRead to inspect any entries whose names suggest duplication or staleness, then MemorySave/MemoryDelete (with the right location) to clean up.");
122
+ sections.push("Begin consolidation. Use MemoryRead to inspect any entries whose names suggest duplication or staleness, then MemorySave/MemoryDelete (with the right id/location/scope) to clean up.");
123
123
  return sections.join("\n");
124
124
  }
@@ -13,9 +13,8 @@
13
13
  * The loop is intentionally small and offline:
14
14
  * - No streaming, no UI events — it runs in the background.
15
15
  * - No permission prompts — there is no interactive backend on this path, so
16
- * we hard-reject any attempt to Save/Delete in the "user" scope before
17
- * dispatching. The "dream" scope is the LLM's workspace and goes through
18
- * freely.
16
+ * Save/Delete goes through an origin guard before dispatching. Dream may
17
+ * maintain origin:dream/auto entries but never touches origin:manual.
19
18
  * - Capped at MAX_TURNS LLM round-trips and MAX_WRITES total mutations to
20
19
  * bound damage on misbehavior.
21
20
  */
@@ -13,17 +13,17 @@
13
13
  * The loop is intentionally small and offline:
14
14
  * - No streaming, no UI events — it runs in the background.
15
15
  * - No permission prompts — there is no interactive backend on this path, so
16
- * we hard-reject any attempt to Save/Delete in the "user" scope before
17
- * dispatching. The "dream" scope is the LLM's workspace and goes through
18
- * freely.
16
+ * Save/Delete goes through an origin guard before dispatching. Dream may
17
+ * maintain origin:dream/auto entries but never touches origin:manual.
19
18
  * - Capped at MAX_TURNS LLM round-trips and MAX_WRITES total mutations to
20
19
  * bound damage on misbehavior.
21
20
  */
22
21
  import { MemoryManager } from "../session/memory.js";
23
- import { buildDreamSystemPrompt, buildDreamUserPrompt, } from "./auto-dream.js";
22
+ import { buildDreamSystemPrompt, buildDreamUserPrompt } from "./auto-dream.js";
24
23
  import { logger } from "../logging/logger.js";
25
24
  const MAX_TURNS = 8;
26
25
  const MAX_WRITES = 10;
26
+ const FRESH_ENTRY_GRACE_MS = 10 * 60 * 1000;
27
27
  const MEMORY_TOOL_NAMES = ["MemoryList", "MemoryRead", "MemorySave", "MemoryDelete"];
28
28
  /**
29
29
  * Drive the dream-scope consolidation tool-call loop.
@@ -37,9 +37,7 @@ const MEMORY_TOOL_NAMES = ["MemoryList", "MemoryRead", "MemorySave", "MemoryDele
37
37
  */
38
38
  export async function runDreamConsolidation(input) {
39
39
  const { llmClient, toolRegistry, projectDir, sessionId } = input;
40
- const memoryTools = MEMORY_TOOL_NAMES
41
- .map((n) => toolRegistry.getTool(n))
42
- .filter((t) => t != null);
40
+ const memoryTools = MEMORY_TOOL_NAMES.map((n) => toolRegistry.getTool(n)).filter((t) => t != null);
43
41
  if (memoryTools.length < MEMORY_TOOL_NAMES.length) {
44
42
  logger.warn("memory.dream_missing_tools", {
45
43
  sessionId,
@@ -52,8 +50,9 @@ export async function runDreamConsolidation(input) {
52
50
  const mm = new MemoryManager({ projectDir });
53
51
  const userMems = mm.loadScope("user");
54
52
  const dreamMems = mm.loadScope("dream");
53
+ const globalDreamMems = projectDir ? new MemoryManager({ scope: "dream" }).loadAll() : [];
55
54
  const systemPrompt = buildDreamSystemPrompt();
56
- const userPrompt = buildDreamUserPrompt(userMems, dreamMems);
55
+ const userPrompt = buildDreamUserPrompt(userMems, dreamMems, globalDreamMems);
57
56
  // Strip RegisteredTool down to the shape createMessage expects.
58
57
  const toolDefs = memoryTools.map((t) => ({
59
58
  name: t.name,
@@ -120,8 +119,8 @@ export async function runDreamConsolidation(input) {
120
119
  * Execute one memory tool call inside the dream loop. Enforces the two
121
120
  * dream-loop invariants the prompt also states:
122
121
  * - Only the 4 memory tools are dispatchable.
123
- * - Save/Delete in "user" scope is refused (returned as a tool error)
124
- * because dream runs without an interactive permission backend.
122
+ * - Save/Delete is allowed only for origin:auto/origin:dream owned entries.
123
+ * Missing origin is manual, and manual is always protected.
125
124
  */
126
125
  async function dispatchDreamTool(tc, toolRegistry, ctx, consumeWriteBudget) {
127
126
  const allowed = new Set(MEMORY_TOOL_NAMES);
@@ -130,17 +129,17 @@ async function dispatchDreamTool(tc, toolRegistry, ctx, consumeWriteBudget) {
130
129
  }
131
130
  const isWrite = tc.toolName === "MemorySave" || tc.toolName === "MemoryDelete";
132
131
  if (isWrite) {
133
- const scope = tc.args?.scope;
134
- if (scope !== "dream") {
135
- return (`Error: dream loop may only write to scope "dream", got "${scope}". ` +
136
- `User-scope changes require interactive permission, which is not available here.`);
132
+ const guard = checkDreamWriteGuard(tc, ctx);
133
+ if (!guard.ok) {
134
+ return guard.error;
137
135
  }
138
136
  if (!consumeWriteBudget()) {
139
137
  return "Error: dream write budget exhausted — stop calling write tools and summarize instead.";
140
138
  }
141
139
  }
142
140
  try {
143
- const result = await toolRegistry.executeTool(tc.toolName, tc.args, { ctx });
141
+ const dreamCtx = { ...ctx, __dreamLoop: true };
142
+ const result = await toolRegistry.executeTool(tc.toolName, tc.args, { ctx: dreamCtx });
144
143
  if (result.isError)
145
144
  return result.error ?? `Error executing ${tc.toolName}`;
146
145
  return result.result ?? "";
@@ -149,3 +148,54 @@ async function dispatchDreamTool(tc, toolRegistry, ctx, consumeWriteBudget) {
149
148
  return `Error executing ${tc.toolName}: ${err.message}`;
150
149
  }
151
150
  }
151
+ function checkDreamWriteGuard(tc, ctx) {
152
+ const scope = tc.args?.scope;
153
+ if (scope !== "dream" && scope !== "user") {
154
+ return {
155
+ ok: false,
156
+ error: `Error: dream loop may only write to scope "dream" or "user", got "${scope}".`,
157
+ };
158
+ }
159
+ const location = tc.args?.location === "global" ? "global" : "project";
160
+ const mm = new MemoryManager({
161
+ projectDir: location === "project" ? ctx.cwd : undefined,
162
+ scope,
163
+ });
164
+ const id = typeof tc.args?.id === "string" ? tc.args.id : undefined;
165
+ const name = typeof tc.args?.name === "string" ? tc.args.name : undefined;
166
+ const targetById = id ? mm.findById(id) : undefined;
167
+ const targetByName = name ? mm.find(name) : undefined;
168
+ const targets = [targetById, targetByName].filter((target, index, all) => Boolean(target) && all.findIndex((item) => item?.id === target?.id) === index);
169
+ const protectedTarget = targets.find((target) => target.origin === "manual" || !target.origin);
170
+ if (protectedTarget) {
171
+ return {
172
+ ok: false,
173
+ error: `Error: dream loop cannot modify origin:manual memory "${protectedTarget.name}" ` +
174
+ `(${location}/${scope}/${protectedTarget.id ?? protectedTarget.fileName}).`,
175
+ };
176
+ }
177
+ const target = targetById ?? targetByName;
178
+ if (!target)
179
+ return { ok: true };
180
+ if (target.origin !== "auto" && target.origin !== "dream") {
181
+ return {
182
+ ok: false,
183
+ error: `Error: dream loop can only modify origin:auto or origin:dream memories; ` +
184
+ `"${target.name}" has origin:${target.origin}.`,
185
+ };
186
+ }
187
+ if (tc.toolName === "MemoryDelete" && isFreshEntry(target.createdAt)) {
188
+ return {
189
+ ok: false,
190
+ error: `Error: dream loop cannot delete freshly-created memory "${target.name}" yet; ` +
191
+ "leave it for the next consolidation pass.",
192
+ };
193
+ }
194
+ return { ok: true };
195
+ }
196
+ function isFreshEntry(createdAt) {
197
+ if (!createdAt)
198
+ return false;
199
+ const createdMs = new Date(createdAt).getTime();
200
+ return Number.isFinite(createdMs) && Date.now() - createdMs < FRESH_ENTRY_GRACE_MS;
201
+ }
@@ -18,17 +18,26 @@ export interface ExtractedMemory {
18
18
  description: string;
19
19
  content: string;
20
20
  }
21
+ export interface ExistingMemorySummary {
22
+ id?: string;
23
+ name: string;
24
+ type: string;
25
+ description: string;
26
+ location?: "project" | "global";
27
+ memoryScope?: "user" | "dream";
28
+ origin?: "manual" | "auto" | "dream";
29
+ pinned?: boolean;
30
+ useCount?: number;
31
+ updateCount?: number;
32
+ updatedAt?: string;
33
+ }
21
34
  /**
22
35
  * Build the prompt for extracting memories from a conversation.
23
36
  */
24
37
  export declare function buildExtractionPrompt(transcript: Array<{
25
38
  role: string;
26
39
  content: string;
27
- }>, existingMemories: Array<{
28
- name: string;
29
- type: string;
30
- description: string;
31
- }>): string;
40
+ }>, existingMemories: ExistingMemorySummary[]): string;
32
41
  /** Max memories to accept from a single extraction pass. Code-side cap that
33
42
  * enforces the same limit the prompt asks for — the model occasionally
34
43
  * ignores prompt rules, but this guarantees the cap. */
@@ -12,7 +12,21 @@ export function buildExtractionPrompt(transcript, existingMemories) {
12
12
  .map((m) => `[${m.role}]: ${m.content.slice(0, 3000)}`)
13
13
  .join("\n\n");
14
14
  const existingList = existingMemories.length > 0
15
- ? existingMemories.map((m) => ` - [${m.type}] ${m.name}: ${m.description}`).join("\n")
15
+ ? existingMemories
16
+ .map((m) => {
17
+ const meta = [
18
+ m.id ? `id:${m.id}` : null,
19
+ m.location ? `location:${m.location}` : null,
20
+ m.memoryScope ? `scope:${m.memoryScope}` : null,
21
+ m.origin ? `origin:${m.origin}` : null,
22
+ typeof m.useCount === "number" ? `use:${m.useCount}` : null,
23
+ typeof m.updateCount === "number" ? `updates:${m.updateCount}` : null,
24
+ ]
25
+ .filter(Boolean)
26
+ .join(", ");
27
+ return ` - [${m.type}] ${m.name}${meta ? ` (${meta})` : ""}: ${m.description}`;
28
+ })
29
+ .join("\n")
16
30
  : " (none)";
17
31
  return `Analyze the following conversation and extract any information worth remembering for future sessions.
18
32
 
@@ -23,7 +37,7 @@ ${existingList}
23
37
  ${conversationText.slice(0, 30000)}
24
38
 
25
39
  ## Instructions
26
- Identify NEW information that should be saved as persistent memories. Categories (type):
40
+ Identify candidate information that may be saved as persistent memories. The extractor only proposes candidates; code will route automatic candidates into dream memory first, never directly into user memory. Categories (type):
27
41
  - **user**: Information about the user's role, preferences, or expertise
28
42
  - **feedback**: Guidance about how to approach work (corrections or confirmations)
29
43
  - **project**: Non-obvious facts about ongoing work, goals, or decisions
@@ -39,7 +53,10 @@ Rules:
39
53
  - Extract AT MOST 2 memories per session. Prefer 0 to a marginal one — most sessions are noise.
40
54
  - AT MOST 1 of the extracted memories may be "global". The global layer is injected every session, so keep it tiny and high-value. If two candidates both seem global, keep the single most universal one global and make the other "project".
41
55
  - Only extract information that would be useful in FUTURE conversations
42
- - Do not duplicate existing memories (re-read the "Existing Memories" list above carefully)
56
+ - Do not duplicate existing memories (re-read the "Existing Memories" list above carefully).
57
+ - If an existing auto/dream memory has the same durable topic, reuse that topic in the candidate instead of creating a date-stamped variant; the write decision layer will update the existing id.
58
+ - Manual user memories are read-only curated memories. Do not re-extract the same topic just because the wording or date differs.
59
+ - Avoid dates, version stamps, batch names, and progress-snapshot suffixes in names. Names should be stable topic identifiers, not "today" variants.
43
60
  - Do not extract code patterns, file structures, or git history (derivable from code)
44
61
  - Do not extract ephemeral task details, progress snapshots, or in-flight work state — those belong in the conversation, not memory
45
62
  - Do not extract one-off research products (news reports, AI industry summaries, slide deck content, daily progress dumps) — they're done and not "durable, reusable information"
@@ -0,0 +1,23 @@
1
+ import type { ExtractedMemory } from "./extract-memories.js";
2
+ export interface GlobalDreamPromotionInput {
3
+ candidate: ExtractedMemory;
4
+ projectDir?: string;
5
+ baseDir?: string;
6
+ userDirectGlobal?: boolean;
7
+ now?: Date;
8
+ }
9
+ export interface GlobalDreamPromotionResult {
10
+ /** No automatic write to global dream is performed; approval is required. */
11
+ promoted: false;
12
+ /** True when this call created a new pending approval item. */
13
+ pendingSuggested: boolean;
14
+ originProjects: string[];
15
+ evidenceCount: number;
16
+ projectEvidenceSaved: boolean;
17
+ promotionReason: string;
18
+ }
19
+ export declare function detectUserDirectGlobalPreference(transcript: Array<{
20
+ role: string;
21
+ content: string;
22
+ }>): boolean;
23
+ export declare function applyGlobalDreamPromotionGate(input: GlobalDreamPromotionInput): GlobalDreamPromotionResult;