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

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 (88) 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 +14 -2
  9. package/dist/context/manager.js +83 -8
  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 +115 -25
  16. package/dist/engine/turn-loop.d.ts +3 -1
  17. package/dist/engine/turn-loop.js +11 -2
  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 +90 -5
  50. package/dist/protocol/types.d.ts +6 -0
  51. package/dist/protocol/types.js +2 -0
  52. package/dist/runtime/background-shell.d.ts +1 -0
  53. package/dist/runtime/background-shell.js +23 -13
  54. package/dist/services/auto-dream.d.ts +15 -4
  55. package/dist/services/auto-dream.js +20 -20
  56. package/dist/services/dream-consolidation.d.ts +2 -3
  57. package/dist/services/dream-consolidation.js +65 -15
  58. package/dist/services/extract-memories.d.ts +14 -5
  59. package/dist/services/extract-memories.js +20 -3
  60. package/dist/services/global-dream-promotion.d.ts +23 -0
  61. package/dist/services/global-dream-promotion.js +112 -0
  62. package/dist/services/memory-orchestrator.js +347 -34
  63. package/dist/session/memory.d.ts +61 -18
  64. package/dist/session/memory.js +342 -79
  65. package/dist/session/session-manager.d.ts +1 -1
  66. package/dist/session/session-manager.js +6 -6
  67. package/dist/settings/manager.js +43 -12
  68. package/dist/settings/schema.d.ts +21 -0
  69. package/dist/settings/schema.js +12 -0
  70. package/dist/skills/scanner.d.ts +3 -2
  71. package/dist/skills/scanner.js +12 -9
  72. package/dist/tool-system/builtin/background-jobs.d.ts +2 -0
  73. package/dist/tool-system/builtin/background-jobs.js +4 -0
  74. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  75. package/dist/tool-system/builtin/background-work.js +51 -5
  76. package/dist/tool-system/builtin/index.js +18 -8
  77. package/dist/tool-system/builtin/memory.js +40 -8
  78. package/dist/tool-system/builtin/worktree.d.ts +2 -0
  79. package/dist/tool-system/builtin/worktree.js +59 -11
  80. package/dist/tool-system/context.d.ts +11 -1
  81. package/dist/tool-system/mcp-manager.d.ts +8 -5
  82. package/dist/tool-system/mcp-manager.js +85 -55
  83. package/dist/tool-system/path-policy.d.ts +2 -0
  84. package/dist/tool-system/path-policy.js +28 -12
  85. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  86. package/dist/tool-system/workspace-bridge.js +1 -0
  87. package/dist/types.d.ts +20 -0
  88. package/package.json +1 -1
@@ -51,14 +51,35 @@ export function isProtectedSettingKey(key) {
51
51
  return PROTECTED_SETTING_ROOTS.has(root);
52
52
  }
53
53
  const FORBIDDEN_SETTING_KEY_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
54
+ function isForbiddenSettingKeySegment(key) {
55
+ return FORBIDDEN_SETTING_KEY_SEGMENTS.has(key);
56
+ }
54
57
  function parseDottedSettingKey(key) {
55
58
  const parts = key.split(".");
56
59
  if (parts.length === 0 ||
57
- parts.some((seg) => seg.length === 0 || FORBIDDEN_SETTING_KEY_SEGMENTS.has(seg))) {
60
+ parts.some((seg) => seg.length === 0 || isForbiddenSettingKeySegment(seg))) {
58
61
  throw new Error(`invalid setting key: ${key}`);
59
62
  }
60
63
  return parts;
61
64
  }
65
+ function sanitizeSettingsValue(value) {
66
+ if (Array.isArray(value)) {
67
+ return value.map((entry) => sanitizeSettingsValue(entry));
68
+ }
69
+ if (!value || typeof value !== "object") {
70
+ return value;
71
+ }
72
+ const out = {};
73
+ for (const [key, child] of Object.entries(value)) {
74
+ if (isForbiddenSettingKeySegment(key))
75
+ continue;
76
+ out[key] = sanitizeSettingsValue(child);
77
+ }
78
+ return out;
79
+ }
80
+ function sanitizeSettingsObject(data) {
81
+ return sanitizeSettingsValue(data);
82
+ }
62
83
  function isOwnPlainObject(parent, key) {
63
84
  if (!Object.prototype.hasOwnProperty.call(parent, key))
64
85
  return false;
@@ -174,7 +195,7 @@ export class SettingsManager {
174
195
  }
175
196
  // 5. CLI flags (highest priority)
176
197
  if (flagOverrides && Object.keys(flagOverrides).length > 0) {
177
- this.sources.push({ name: "flag", priority: 4, data: flagOverrides });
198
+ this.sources.push({ name: "flag", priority: 4, data: sanitizeSettingsObject(flagOverrides) });
178
199
  }
179
200
  // Sort by priority ascending (merge in order, later wins)
180
201
  this.sources.sort((a, b) => a.priority - b.priority);
@@ -215,7 +236,7 @@ export class SettingsManager {
215
236
  const userPath = join(userHome(), ".code-shell", "settings.json");
216
237
  if (readUser && existsSync(userPath)) {
217
238
  try {
218
- const userRaw = JSON.parse(readFileSync(userPath, "utf-8"));
239
+ const userRaw = sanitizeSettingsObject(JSON.parse(readFileSync(userPath, "utf-8")));
219
240
  const result = migrateModels({
220
241
  providers: userRaw.providers ?? [],
221
242
  models: userRaw.models ?? [],
@@ -227,14 +248,15 @@ export class SettingsManager {
227
248
  providers: result.providers,
228
249
  models: result.models,
229
250
  };
251
+ const sanitized = sanitizeSettingsObject(migrated);
230
252
  // Atomic write (tmp+rename) — a concurrent load must not see a
231
253
  // half-written file. File exists here (existsSync guard above).
232
- this.atomicWriteJson(userPath, migrated);
254
+ this.atomicWriteJson(userPath, sanitized);
233
255
  // Re-deep-merge with the migrated user data so the validate
234
256
  // call sees the new shape rather than the legacy one.
235
257
  const userSource = this.sources.find((s) => s.name === "user");
236
258
  if (userSource)
237
- userSource.data = migrated;
259
+ userSource.data = sanitized;
238
260
  const remerged = this.deepMerge();
239
261
  this.merged = validateSettings(remerged);
240
262
  return this.merged;
@@ -263,7 +285,7 @@ export class SettingsManager {
263
285
  const parsed = JSON.parse(readFileSync(path, "utf-8"));
264
286
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
265
287
  return;
266
- const raw = parsed;
288
+ const raw = sanitizeSettingsObject(parsed);
267
289
  const result = migrateConfig(raw);
268
290
  if (!result.changed)
269
291
  return;
@@ -279,10 +301,11 @@ export class SettingsManager {
279
301
  // Atomic write (tmp+rename) so a concurrent load can't read a half-written
280
302
  // migrated file — matches the normal save path (atomicWriteJson). The file
281
303
  // exists here (existsSync guard above), so the recursive mkdir is a no-op.
282
- this.atomicWriteJson(path, result.config);
304
+ const sanitized = sanitizeSettingsObject(result.config);
305
+ this.atomicWriteJson(path, sanitized);
283
306
  const source = this.sources.find((s) => s.name === sourceName);
284
307
  if (source)
285
- source.data = result.config;
308
+ source.data = sanitized;
286
309
  }
287
310
  catch {
288
311
  // Best-effort — fall through to normal merge/validate.
@@ -318,7 +341,7 @@ export class SettingsManager {
318
341
  const raw = readFileSync(path, "utf-8");
319
342
  const parsed = JSON.parse(raw);
320
343
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
321
- current = parsed;
344
+ current = sanitizeSettingsObject(parsed);
322
345
  }
323
346
  }
324
347
  catch {
@@ -500,7 +523,7 @@ function parseConfigFile(path) {
500
523
  const ext = extname(path).toLowerCase();
501
524
  const parsed = ext === ".yaml" || ext === ".yml" ? parseYaml(content) : JSON.parse(content);
502
525
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
503
- return parsed;
526
+ return sanitizeSettingsObject(parsed);
504
527
  }
505
528
  }
506
529
  catch {
@@ -527,9 +550,17 @@ function resolveConfigPath(jsonPath) {
527
550
  return null;
528
551
  }
529
552
  function merge(base, override) {
530
- const result = { ...base };
553
+ const result = {};
554
+ for (const [key, value] of Object.entries(base)) {
555
+ if (isForbiddenSettingKeySegment(key))
556
+ continue;
557
+ result[key] = value;
558
+ }
531
559
  for (const [key, value] of Object.entries(override)) {
532
- if (value === null) {
560
+ if (isForbiddenSettingKeySegment(key)) {
561
+ continue;
562
+ }
563
+ else if (value === null) {
533
564
  delete result[key];
534
565
  }
535
566
  else if (typeof value === "object" &&
@@ -366,6 +366,13 @@ export declare const SettingsSchema: z.ZodObject<{
366
366
  storageDir?: string | undefined;
367
367
  maxHistory?: number | undefined;
368
368
  }>>;
369
+ worktree: z.ZodDefault<z.ZodObject<{
370
+ branchPrefix: z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
371
+ }, "strip", z.ZodTypeAny, {
372
+ branchPrefix: string;
373
+ }, {
374
+ branchPrefix?: string | undefined;
375
+ }>>;
369
376
  mcpServers: z.ZodDefault<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodObject<{
370
377
  name: z.ZodString;
371
378
  command: z.ZodOptional<z.ZodString>;
@@ -1240,6 +1247,13 @@ export declare const SettingsSchema: z.ZodObject<{
1240
1247
  storageDir?: string | undefined;
1241
1248
  maxHistory?: number | undefined;
1242
1249
  }>>;
1250
+ worktree: z.ZodDefault<z.ZodObject<{
1251
+ branchPrefix: z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
1252
+ }, "strip", z.ZodTypeAny, {
1253
+ branchPrefix: string;
1254
+ }, {
1255
+ branchPrefix?: string | undefined;
1256
+ }>>;
1243
1257
  mcpServers: z.ZodDefault<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodObject<{
1244
1258
  name: z.ZodString;
1245
1259
  command: z.ZodOptional<z.ZodString>;
@@ -2114,6 +2128,13 @@ export declare const SettingsSchema: z.ZodObject<{
2114
2128
  storageDir?: string | undefined;
2115
2129
  maxHistory?: number | undefined;
2116
2130
  }>>;
2131
+ worktree: z.ZodDefault<z.ZodObject<{
2132
+ branchPrefix: z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
2133
+ }, "strip", z.ZodTypeAny, {
2134
+ branchPrefix: string;
2135
+ }, {
2136
+ branchPrefix?: string | undefined;
2137
+ }>>;
2117
2138
  mcpServers: z.ZodDefault<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodObject<{
2118
2139
  name: z.ZodString;
2119
2140
  command: z.ZodOptional<z.ZodString>;
@@ -2,6 +2,7 @@
2
2
  * Settings schema with Zod validation.
3
3
  */
4
4
  import { z } from "zod";
5
+ import { DEFAULT_WORKTREE_BRANCH_PREFIX, isValidWorktreeBranchPrefix, normalizeWorktreeBranchPrefix, } from "../git/worktree/slug.js";
5
6
  /**
6
7
  * Tri-state project capability overlay. Lives in PROJECT settings only and
7
8
  * layers over the global baseline (disabledSkills / disabledPlugins /
@@ -30,6 +31,12 @@ export const CapabilityOverridesSchema = z
30
31
  pluginHooks: z.record(CapabilityOverrideSchema).optional(),
31
32
  })
32
33
  .optional();
34
+ const WorktreeBranchPrefixSchema = z
35
+ .string()
36
+ .trim()
37
+ .min(1)
38
+ .refine(isValidWorktreeBranchPrefix, "Invalid git branch prefix")
39
+ .transform((value) => normalizeWorktreeBranchPrefix(value));
33
40
  export const SettingsSchema = z
34
41
  .object({
35
42
  agent: z
@@ -214,6 +221,11 @@ export const SettingsSchema = z
214
221
  maxHistory: z.number().default(100),
215
222
  })
216
223
  .default({}),
224
+ worktree: z
225
+ .object({
226
+ branchPrefix: WorktreeBranchPrefixSchema.default(DEFAULT_WORKTREE_BRANCH_PREFIX),
227
+ })
228
+ .default({}),
217
229
  // The record key IS the server name at runtime (MCPManager.connectAll
218
230
  // uses Object.entries keys; desktop's persist strips the `name` field and
219
231
  // keys by it via stripNameFromServer). So `name` here is optional and
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Skill scanner — discovers <base>/<name>/SKILL.md files from project + user
3
- * directories AND from installed plugins. Mirrors Claude Code's
2
+ * Skill scanner — discovers <base>/<name>/SKILL.md files from project
3
+ * `.code-shell/skills`, project `.agents/skills`, user `.code-shell/skills`,
4
+ * and installed plugins. Mirrors Claude Code's
4
5
  * `loadSkillsFromSkillsDir` (skills/loadSkillsDir.ts:407) plus plugin
5
6
  * integration (utils/plugins/pluginLoader.ts).
6
7
  */
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Skill scanner — discovers <base>/<name>/SKILL.md files from project + user
3
- * directories AND from installed plugins. Mirrors Claude Code's
2
+ * Skill scanner — discovers <base>/<name>/SKILL.md files from project
3
+ * `.code-shell/skills`, project `.agents/skills`, user `.code-shell/skills`,
4
+ * and installed plugins. Mirrors Claude Code's
4
5
  * `loadSkillsFromSkillsDir` (skills/loadSkillsDir.ts:407) plus plugin
5
6
  * integration (utils/plugins/pluginLoader.ts).
6
7
  */
@@ -20,6 +21,7 @@ function userHome() {
20
21
  function bases(cwd) {
21
22
  return [
22
23
  { dir: join(cwd, ".code-shell", "skills"), source: "project" },
24
+ { dir: join(cwd, ".agents", "skills"), source: "project" },
23
25
  { dir: join(userHome(), ".code-shell", "skills"), source: "user" },
24
26
  ];
25
27
  }
@@ -158,13 +160,14 @@ function installedPluginsMtime() {
158
160
  }
159
161
  }
160
162
  /**
161
- * mtime of each local skills base dir (project + user). A directory's mtime
162
- * changes when a child entry is added/removed, so installing a new skill into
163
- * `<cwd>/.code-shell/skills` or `~/.code-shell/skills` busts the cache on the
164
- * next scan — the just-installed skill becomes visible to the running session
165
- * without a restart. (Editing an existing skill's *contents* does not bump the
166
- * dir mtime; install paths additionally call invalidateSkillCache() to cover
167
- * that, so this is the passive half of a two-part guard.)
163
+ * mtime of each local skills base dir (project .code-shell/.agents + user). A
164
+ * directory's mtime changes when a child entry is added/removed, so installing
165
+ * a new skill into `<cwd>/.code-shell/skills`, `<cwd>/.agents/skills`, or
166
+ * `~/.code-shell/skills` busts the cache on the next scan — the just-installed
167
+ * skill becomes visible to the running session without a restart. (Editing an
168
+ * existing skill's *contents* does not bump the dir mtime; install paths
169
+ * additionally call invalidateSkillCache() to cover that, so this is the
170
+ * passive half of a two-part guard.)
168
171
  */
169
172
  function skillsDirsMtime(cwd) {
170
173
  return bases(cwd)
@@ -69,6 +69,8 @@ declare class BackgroundJobRegistry {
69
69
  listRunningByCwd(cwd: string): BackgroundJobEntry[];
70
70
  /** All jobs (running + retained terminal) for `sessionId`. Feeds the panel. */
71
71
  listForSession(sessionId: string): BackgroundJobEntry[];
72
+ /** All jobs (running + retained terminal) across sessions. Feeds all-scope UI. */
73
+ list(): BackgroundJobEntry[];
72
74
  /** Drop every job of a session — called when the session is deleted/closed. */
73
75
  dropForSession(sessionId: string): void;
74
76
  subscribe: (cb: Listener) => (() => void);
@@ -82,6 +82,10 @@ class BackgroundJobRegistry {
82
82
  listForSession(sessionId) {
83
83
  return [...this.jobs.values()].filter((e) => e.sessionId === sessionId);
84
84
  }
85
+ /** All jobs (running + retained terminal) across sessions. Feeds all-scope UI. */
86
+ list() {
87
+ return [...this.jobs.values()];
88
+ }
85
89
  /** Drop every job of a session — called when the session is deleted/closed. */
86
90
  dropForSession(sessionId) {
87
91
  let removed = false;
@@ -1,16 +1,3 @@
1
- /**
2
- * Unified view over the THREE kinds of async background work — background
3
- * sub-agents (asyncAgentRegistry), background jobs / video polls
4
- * (backgroundJobRegistry), and background shells (backgroundShellManager).
5
- *
6
- * Used by the goal-stop-hook to show the judge LLM what's still running, so it
7
- * can tell "the goal is done except for a finite task that will wake me" (a
8
- * download / video render → allow stop, wait for the wakeup) from "a long-lived
9
- * service that the goal doesn't depend on" (a dev server → judge the goal
10
- * normally). Replacing the old mechanical `hasRunningForSession` short-circuit:
11
- * a boolean can't distinguish a finite download from a never-ending dev server,
12
- * but the judge — given each task's kind + command — can.
13
- */
14
1
  import { type AsyncAgentStatus } from "./agent-registry.js";
15
2
  import { type BackgroundJobStatus } from "./background-jobs.js";
16
3
  import { type BgShell } from "../../runtime/background-shell.js";
@@ -30,12 +17,21 @@ export interface BackgroundWorkItem {
30
17
  /** List every still-running background work item spawned by `sessionId`. */
31
18
  export declare function listRunningBackgroundWork(sessionId: string): BackgroundWorkItem[];
32
19
  /** One background-work row for the desktop panel, discriminated by `kind`. */
33
- export type BackgroundWorkEntry = {
20
+ export interface BackgroundWorkSourceSession {
21
+ sessionId: string;
22
+ shortId: string;
23
+ title?: string;
24
+ current: boolean;
25
+ }
26
+ type WithSource<T> = T & {
27
+ sourceSession: BackgroundWorkSourceSession;
28
+ };
29
+ export type BackgroundWorkEntry = WithSource<{
34
30
  kind: "shell";
35
31
  /** Full shell snapshot — the panel already renders this shape (output/kill
36
32
  * still go through the dedicated agent/backgroundShells RPC by shellId). */
37
33
  shell: BgShell;
38
- } | {
34
+ }> | WithSource<{
39
35
  kind: "subagent";
40
36
  agentId: string;
41
37
  name?: string;
@@ -44,7 +40,7 @@ export type BackgroundWorkEntry = {
44
40
  status: AsyncAgentStatus;
45
41
  startedAt: number;
46
42
  finishedAt?: number;
47
- } | {
43
+ }> | WithSource<{
48
44
  kind: "job";
49
45
  jobId: string;
50
46
  description: string;
@@ -55,7 +51,7 @@ export type BackgroundWorkEntry = {
55
51
  finalText?: string;
56
52
  /** Files an external agent (DriveAgent) changed, parsed from its transcript. */
57
53
  changedFiles?: string[];
58
- };
54
+ }>;
59
55
  /**
60
56
  * Every background-work item spawned by `sessionId`, with per-kind detail, for
61
57
  * the desktop panel. Includes finished sub-agents that are still within their
@@ -64,4 +60,7 @@ export type BackgroundWorkEntry = {
64
60
  * keep them. Jobs are only ever present while running (the registry drops them
65
61
  * on finish).
66
62
  */
67
- export declare function listBackgroundWorkForUI(sessionId: string): BackgroundWorkEntry[];
63
+ export declare function listBackgroundWorkForUI(sessionId: string, opts?: {
64
+ scope?: "session" | "all";
65
+ }): BackgroundWorkEntry[];
66
+ export {};
@@ -11,9 +11,12 @@
11
11
  * a boolean can't distinguish a finite download from a never-ending dev server,
12
12
  * but the judge — given each task's kind + command — can.
13
13
  */
14
+ import { readFileSync, statSync } from "node:fs";
15
+ import { join } from "node:path";
14
16
  import { asyncAgentRegistry } from "./agent-registry.js";
15
17
  import { backgroundJobRegistry } from "./background-jobs.js";
16
18
  import { backgroundShellManager } from "../../runtime/background-shell.js";
19
+ import { codeShellHome } from "../../session/session-manager.js";
17
20
  /** List every still-running background work item spawned by `sessionId`. */
18
21
  export function listRunningBackgroundWork(sessionId) {
19
22
  const items = [];
@@ -39,6 +42,38 @@ export function listRunningBackgroundWork(sessionId) {
39
42
  }
40
43
  return items;
41
44
  }
45
+ const sessionTitleCache = new Map();
46
+ function shortSessionId(sessionId) {
47
+ return sessionId.length <= 10 ? sessionId : sessionId.slice(0, 10);
48
+ }
49
+ function readSessionTitle(sessionId) {
50
+ try {
51
+ const statePath = join(codeShellHome(), "sessions", sessionId, "state.json");
52
+ const st = statSync(statePath);
53
+ const cached = sessionTitleCache.get(sessionId);
54
+ if (cached && cached.mtimeMs === st.mtimeMs)
55
+ return cached.title;
56
+ const raw = JSON.parse(readFileSync(statePath, "utf8"));
57
+ const title = typeof raw.title === "string" && raw.title.trim()
58
+ ? raw.title.trim()
59
+ : typeof raw.summary === "string" && raw.summary.trim()
60
+ ? raw.summary.trim()
61
+ : undefined;
62
+ sessionTitleCache.set(sessionId, { mtimeMs: st.mtimeMs, title });
63
+ return title;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ function sourceSession(currentSessionId, ownerSessionId) {
70
+ return {
71
+ sessionId: ownerSessionId,
72
+ shortId: shortSessionId(ownerSessionId),
73
+ title: readSessionTitle(ownerSessionId),
74
+ current: ownerSessionId === currentSessionId,
75
+ };
76
+ }
42
77
  /**
43
78
  * Every background-work item spawned by `sessionId`, with per-kind detail, for
44
79
  * the desktop panel. Includes finished sub-agents that are still within their
@@ -47,18 +82,24 @@ export function listRunningBackgroundWork(sessionId) {
47
82
  * keep them. Jobs are only ever present while running (the registry drops them
48
83
  * on finish).
49
84
  */
50
- export function listBackgroundWorkForUI(sessionId) {
85
+ export function listBackgroundWorkForUI(sessionId, opts = {}) {
51
86
  const entries = [];
52
- for (const s of backgroundShellManager.listForSession(sessionId)) {
53
- entries.push({ kind: "shell", shell: s });
87
+ const scope = opts.scope ?? "session";
88
+ const shells = scope === "all"
89
+ ? backgroundShellManager.list()
90
+ : backgroundShellManager.listForSession(sessionId);
91
+ for (const s of shells) {
92
+ entries.push({ kind: "shell", shell: s, sourceSession: sourceSession(sessionId, s.sessionId) });
54
93
  }
55
94
  const now = Date.now();
56
- for (const a of asyncAgentRegistry.listForSession(sessionId)) {
95
+ const agents = scope === "all" ? asyncAgentRegistry.list() : asyncAgentRegistry.listForSession(sessionId);
96
+ for (const a of agents) {
57
97
  // Keep running agents, plus finished ones still inside their fade window so
58
98
  // a completion is briefly visible. (finishedFadeAt = finishedAt + 30s.)
59
99
  const fresh = a.status === "running" || (a.finishedFadeAt != null && a.finishedFadeAt > now);
60
100
  if (!fresh)
61
101
  continue;
102
+ const ownerSessionId = a.sessionId ?? sessionId;
62
103
  entries.push({
63
104
  kind: "subagent",
64
105
  agentId: a.agentId,
@@ -68,9 +109,13 @@ export function listBackgroundWorkForUI(sessionId) {
68
109
  status: a.status,
69
110
  startedAt: a.startedAt,
70
111
  finishedAt: a.finishedAt,
112
+ sourceSession: sourceSession(sessionId, ownerSessionId),
71
113
  });
72
114
  }
73
- for (const j of backgroundJobRegistry.listForSession(sessionId)) {
115
+ const jobs = scope === "all"
116
+ ? backgroundJobRegistry.list()
117
+ : backgroundJobRegistry.listForSession(sessionId);
118
+ for (const j of jobs) {
74
119
  entries.push({
75
120
  kind: "job",
76
121
  jobId: j.jobId,
@@ -80,6 +125,7 @@ export function listBackgroundWorkForUI(sessionId) {
80
125
  ...(j.finishedAt != null ? { finishedAt: j.finishedAt } : {}),
81
126
  ...(j.finalText != null ? { finalText: j.finalText } : {}),
82
127
  ...(j.changedFiles && j.changedFiles.length ? { changedFiles: j.changedFiles } : {}),
128
+ sourceSession: sourceSession(sessionId, j.sessionId),
83
129
  });
84
130
  }
85
131
  return entries;
@@ -3,9 +3,9 @@
3
3
  */
4
4
  import { readToolDef, readTool } from "./read.js";
5
5
  import { writeToolDef, writeTool } from "./write.js";
6
- import { generateImageToolDef, generateImageTool, isGenerateImageAvailable } from "./generate-image.js";
6
+ import { generateImageToolDef, generateImageTool, isGenerateImageAvailable, } from "./generate-image.js";
7
7
  import { editModelCatalogToolDef, editModelCatalogTool } from "./edit-model-catalog.js";
8
- import { generateVideoToolDef, generateVideoTool, isGenerateVideoAvailable } from "./generate-video.js";
8
+ import { generateVideoToolDef, generateVideoTool, isGenerateVideoAvailable, } from "./generate-video.js";
9
9
  import { viewImageToolDef, viewImageTool } from "./view-image.js";
10
10
  import { editToolDef, editTool } from "./edit.js";
11
11
  import { applyPatchToolDef, applyPatchTool } from "./apply-patch/index.js";
@@ -16,19 +16,19 @@ import { webSearchToolDef, webSearchTool, isWebSearchAvailable } from "./web-sea
16
16
  import { webFetchToolDef, webFetchTool } from "./web-fetch.js";
17
17
  import { askUserToolDef, askUserTool } from "./ask-user.js";
18
18
  import { agentToolDef, agentTool, agentStatusToolDef, agentStatusTool, agentCancelToolDef, agentCancelTool, agentSendInputToolDef, agentSendInputTool, } from "./agent.js";
19
- import { enterPlanModeToolDef, enterPlanModeTool, exitPlanModeToolDef, exitPlanModeTool } from "./plan.js";
19
+ import { enterPlanModeToolDef, enterPlanModeTool, exitPlanModeToolDef, exitPlanModeTool, } from "./plan.js";
20
20
  import { toolSearchToolDef, toolSearchTool } from "./tool-search.js";
21
21
  import { todoWriteToolDef, todoWriteTool } from "./task.js";
22
- import { enterWorktreeToolDef, enterWorktreeTool, exitWorktreeToolDef, exitWorktreeTool } from "./worktree.js";
22
+ import { enterWorktreeToolDef, enterWorktreeTool, exitWorktreeToolDef, exitWorktreeTool, switchSessionWorkspaceToolDef, switchSessionWorkspaceTool, } from "./worktree.js";
23
23
  import { sleepToolDef, sleepTool } from "./sleep.js";
24
24
  import { configToolDef, configTool } from "./config.js";
25
25
  import { notebookEditToolDef, notebookEditTool } from "./notebook-edit.js";
26
26
  import { lspToolDef, lspTool } from "./lsp.js";
27
- import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool, cronListToolDef, cronListTool } from "./cron.js";
28
- import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool, DRIVE_AGENT_TOOL_TIMEOUT_MS } from "./drive-claude-code.js";
27
+ import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool, cronListToolDef, cronListTool, } from "./cron.js";
28
+ import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool, DRIVE_AGENT_TOOL_TIMEOUT_MS, } from "./drive-claude-code.js";
29
29
  import { checkQuotaToolDef, checkQuotaTool } from "./check-quota.js";
30
30
  import { skillToolDef, skillTool } from "./skill.js";
31
- import { mcpToolDef, mcpToolExecute, listMcpResourcesToolDef, listMcpResourcesTool, readMcpResourceToolDef, readMcpResourceTool } from "./mcp-tools.js";
31
+ import { mcpToolDef, mcpToolExecute, listMcpResourcesToolDef, listMcpResourcesTool, readMcpResourceToolDef, readMcpResourceTool, } from "./mcp-tools.js";
32
32
  import { replToolDef, replTool } from "./repl.js";
33
33
  import { briefToolDef, briefTool } from "./brief.js";
34
34
  import { powershellToolDef, powershellTool } from "./powershell.js";
@@ -344,6 +344,16 @@ export const BUILTIN_TOOLS = [
344
344
  },
345
345
  execute: exitWorktreeTool,
346
346
  },
347
+ {
348
+ definition: {
349
+ ...switchSessionWorkspaceToolDef,
350
+ source: "builtin",
351
+ permissionDefault: "ask",
352
+ isReadOnly: false,
353
+ isConcurrencySafe: false,
354
+ },
355
+ execute: switchSessionWorkspaceTool,
356
+ },
347
357
  // ─── Phase 5: Utility Tools ────────────────────────────────────
348
358
  {
349
359
  definition: {
@@ -723,7 +733,7 @@ export const BUILTIN_TOOL_GUARDS = new Map([
723
733
  [useCredentialToolDef.name, (ctx) => isUseCredentialAvailable(ctx.cwd)],
724
734
  // InjectCredential hidden until ≥1 cookie credential exists (browser injection
725
735
  // is cookie-only). Also degrades at call time if no browser bridge is wired.
726
- [injectCredentialToolDef.name, (ctx) => isInjectCredentialAvailable(ctx.cwd)],
736
+ [injectCredentialToolDef.name, (ctx) => isInjectCredentialAvailable(ctx.cwd, ctx.settingsScope)],
727
737
  [completeGoalToolDef.name, (ctx) => ctx.hasGoal === true],
728
738
  [cancelGoalToolDef.name, (ctx) => ctx.hasGoal === true],
729
739
  ]);
@@ -17,9 +17,10 @@
17
17
  import { MemoryManager } from "../../session/memory.js";
18
18
  const VALID_SCOPES = ["user", "dream"];
19
19
  const VALID_TYPES = ["user", "feedback", "project", "reference"];
20
+ const VALID_ORIGINS = ["auto", "manual", "dream"];
20
21
  function parseScope(raw) {
21
22
  if (typeof raw !== "string")
22
- return "Error: scope is required (\"user\" or \"dream\")";
23
+ return 'Error: scope is required ("user" or "dream")';
23
24
  if (!VALID_SCOPES.includes(raw)) {
24
25
  return `Error: scope must be "user" or "dream", got "${raw}"`;
25
26
  }
@@ -30,6 +31,11 @@ function parseScope(raw) {
30
31
  function parseLocation(raw) {
31
32
  return raw === "global" ? "global" : "project";
32
33
  }
34
+ function parseOrigin(raw) {
35
+ return typeof raw === "string" && VALID_ORIGINS.includes(raw)
36
+ ? raw
37
+ : undefined;
38
+ }
33
39
  function mmFor(ctx, scope, location = "project") {
34
40
  // location=global → omit projectDir so the manager points at the global store.
35
41
  return new MemoryManager({
@@ -46,7 +52,7 @@ const LOCATION_SCHEMA = {
46
52
  export const memoryListToolDef = {
47
53
  name: "MemoryList",
48
54
  description: "List persistent memory entries from one scope. " +
49
- "Returns each entry's name, type, and short description (not the full content — use MemoryRead for that). " +
55
+ "Returns each entry's id, origin, counts, name, type, and short description (not the full content — use MemoryRead for that). " +
50
56
  "Use this before MemorySave/MemoryDelete to find the exact name to target. " +
51
57
  'Scopes: "user" (entries the user owns; you need permission to modify) or "dream" (auto-consolidation workspace; you may freely modify).',
52
58
  inputSchema: {
@@ -73,7 +79,9 @@ export async function memoryListTool(args, ctx) {
73
79
  if (entries.length === 0)
74
80
  return `(no memories in scope "${scope}")`;
75
81
  return entries
76
- .map((e) => `- [${e.type}] ${e.name} ${e.description}`)
82
+ .map((e) => `- [${e.type}] ${e.name} ` +
83
+ `(id:${e.id ?? "(none)"}, origin:${e.origin ?? "manual"}, use:${e.useCount ?? 0}, updates:${e.updateCount ?? 0}) — ` +
84
+ e.description)
77
85
  .join("\n");
78
86
  }
79
87
  catch (err) {
@@ -127,15 +135,24 @@ export async function memoryReadTool(args, ctx) {
127
135
  if (cb) {
128
136
  // scope here is always "user"|"dream" (validated above); pending is
129
137
  // never tool-readable, so the narrower event type is correct.
130
- void cb({ type: "memory_recalled", name: entry.name, scope: scope, location });
138
+ void cb({
139
+ type: "memory_recalled",
140
+ name: entry.name,
141
+ scope: scope,
142
+ location,
143
+ });
131
144
  }
132
145
  }
133
146
  catch {
134
147
  // ignore — the read result below is what matters
135
148
  }
136
149
  return (`name: ${entry.name}\n` +
150
+ `id: ${entry.id ?? ""}\n` +
137
151
  `description: ${entry.description}\n` +
138
152
  `type: ${entry.type}\n` +
153
+ `origin: ${entry.origin ?? "manual"}\n` +
154
+ `useCount: ${entry.useCount ?? 0}\n` +
155
+ `updateCount: ${entry.updateCount ?? 0}\n` +
139
156
  `\n${entry.content}`);
140
157
  }
141
158
  catch (err) {
@@ -145,7 +162,7 @@ export async function memoryReadTool(args, ctx) {
145
162
  // ─── MemorySave ────────────────────────────────────────────────────────────
146
163
  export const memorySaveToolDef = {
147
164
  name: "MemorySave",
148
- description: "Create or overwrite a memory entry. Same name overwrite. " +
165
+ description: "Create or update a memory entry. Before saving, scan the injected memory index and/or call MemoryList; for the same durable topic, update the existing entry by id instead of creating date-stamped variants. " +
149
166
  'Saving to scope "user" requires user permission (you will see a confirmation prompt). ' +
150
167
  'Saving to scope "dream" is automatic — it is your auto-consolidation workspace. ' +
151
168
  "Pick `type` carefully: " +
@@ -154,7 +171,7 @@ export const memorySaveToolDef = {
154
171
  "project (non-obvious facts about ongoing work), " +
155
172
  "reference (pointers to external resources). " +
156
173
  "Pick `location`: global (a lesson/preference true in ANY project) vs project (this repo only). " +
157
- "If a memory in the injected index is now stale or wrong, overwrite it (same name) or MemoryDelete it — keep the store correct rather than letting contradictions pile up. " +
174
+ "If a memory in the injected index is now stale or wrong, update it by id or MemoryDelete it — keep the store correct rather than letting contradictions pile up. " +
158
175
  "The `description` is a one-line summary shown in the index; the `content` is the full body.",
159
176
  inputSchema: {
160
177
  type: "object",
@@ -165,9 +182,13 @@ export const memorySaveToolDef = {
165
182
  description: "Which scope to save to",
166
183
  },
167
184
  location: LOCATION_SCHEMA,
185
+ id: {
186
+ type: "string",
187
+ description: "Stable id from MemoryList/MemoryRead when updating an existing memory. Omit only for genuinely new topics.",
188
+ },
168
189
  name: {
169
190
  type: "string",
170
- description: "Short kebab-case identifier (also becomes the filename slug)",
191
+ description: "Short stable topic identifier; avoid dates, versions, or batch suffixes",
171
192
  },
172
193
  description: {
173
194
  type: "string",
@@ -192,11 +213,14 @@ export async function memorySaveTool(args, ctx) {
192
213
  return scope;
193
214
  }
194
215
  const name = args.name;
216
+ const id = args.id;
195
217
  const description = args.description;
196
218
  const type = args.type;
197
219
  const content = args.content;
198
220
  if (typeof name !== "string" || !name)
199
221
  return "Error: name is required";
222
+ if (id !== undefined && typeof id !== "string")
223
+ return "Error: id must be a string";
200
224
  if (typeof description !== "string")
201
225
  return "Error: description is required";
202
226
  if (typeof content !== "string")
@@ -207,12 +231,20 @@ export async function memorySaveTool(args, ctx) {
207
231
  try {
208
232
  const location = parseLocation(args.location);
209
233
  const mm = mmFor(ctx, scope, location);
234
+ const dreamLoop = ctx?.__dreamLoop === true;
235
+ const forcedOrigin = dreamLoop
236
+ ? "dream"
237
+ : scope === "user"
238
+ ? "manual"
239
+ : (parseOrigin(args.origin) ?? "manual");
210
240
  const fileName = mm.save({
241
+ id,
211
242
  name,
212
243
  description,
213
244
  type: type,
214
245
  content,
215
- });
246
+ origin: forcedOrigin,
247
+ }, { forceOrigin: forcedOrigin });
216
248
  return `Saved memory "${name}" → ${location}/${scope}/${fileName}`;
217
249
  }
218
250
  catch (err) {