@cydm/happy-elves 0.1.0-beta.73 → 0.1.0-beta.74

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 (93) hide show
  1. package/apps/daemon/dist/gateway/runtime.js +1 -0
  2. package/apps/daemon/dist/lifecycle/detached-helper-bundle.mjs +53 -4
  3. package/apps/daemon/dist/loop/runner.js +1 -0
  4. package/apps/daemon/dist/memory/commands.js +44 -42
  5. package/apps/daemon/dist/memory/flush.js +1 -0
  6. package/apps/daemon/dist/memory/inject.d.ts +1 -0
  7. package/apps/daemon/dist/memory/inject.js +6 -4
  8. package/apps/daemon/dist/memory/managers.d.ts +5 -3
  9. package/apps/daemon/dist/memory/managers.js +110 -34
  10. package/apps/daemon/dist/memory/project-commands.d.ts +3 -0
  11. package/apps/daemon/dist/memory/project-commands.js +394 -0
  12. package/apps/daemon/dist/relay/connection.js +5 -0
  13. package/apps/daemon/dist/relay/register.js +1 -0
  14. package/apps/daemon/dist/runtime/external-provider.js +40 -3
  15. package/apps/daemon/dist/session/prompt.js +1 -0
  16. package/apps/daemon/package.json +1 -1
  17. package/apps/relay/dist/controller-handlers.js +132 -0
  18. package/apps/relay/dist/db.js +67 -1
  19. package/apps/relay/dist/http-routes.js +1 -1
  20. package/apps/relay/dist/http-schemas.d.ts +1 -0
  21. package/apps/relay/dist/http-schemas.js +1 -1
  22. package/apps/relay/dist/machine-command-result-handlers.js +55 -1
  23. package/apps/relay/dist/projections.js +2 -0
  24. package/apps/relay/dist/relay-context.js +5 -0
  25. package/apps/relay/dist/scope.js +5 -0
  26. package/apps/relay/dist/types.d.ts +2 -1
  27. package/apps/relay/dist/websocket.js +2 -0
  28. package/build-identity.json +2 -2
  29. package/npm-shrinkwrap.json +2 -2
  30. package/package.json +1 -1
  31. package/packages/client/dist/client.d.ts +19 -2
  32. package/packages/client/dist/client.js +289 -3
  33. package/packages/client/dist/index.d.ts +2 -2
  34. package/packages/client/dist/live-types.d.ts +1 -1
  35. package/packages/client/dist/transport.d.ts +5 -0
  36. package/packages/client/dist/transport.js +19 -1
  37. package/packages/client/dist/types.d.ts +20 -0
  38. package/packages/daemon-service/dist/index.js +14 -2
  39. package/packages/memory/dist/index.d.ts +2 -1
  40. package/packages/memory/dist/index.js +2 -1
  41. package/packages/memory/dist/manager.d.ts +18 -0
  42. package/packages/memory/dist/manager.js +108 -8
  43. package/packages/memory/dist/projects.d.ts +13 -0
  44. package/packages/memory/dist/projects.js +211 -0
  45. package/packages/runtime/dist/index.d.ts +5 -0
  46. package/packages/runtime-acpx/dist/index.js +6 -0
  47. package/packages/runtime-cli/dist/claude-turn.d.ts +1 -0
  48. package/packages/runtime-cli/dist/claude-turn.js +1 -0
  49. package/packages/runtime-cli/dist/codex-app-server.d.ts +1 -0
  50. package/packages/runtime-cli/dist/codex-app-server.js +3 -3
  51. package/packages/runtime-cli/dist/codex-json-rpc.js +32 -8
  52. package/packages/runtime-cli/dist/codex-protocol.d.ts +1 -1
  53. package/packages/runtime-cli/dist/codex-protocol.js +4 -1
  54. package/packages/runtime-cli/dist/index.js +2 -0
  55. package/packages/shared/dist/crypto.d.ts +28 -1
  56. package/packages/shared/dist/crypto.js +89 -1
  57. package/packages/shared/dist/protocol-schemas.d.ts +2 -0
  58. package/packages/shared/dist/protocol-schemas.js +1 -0
  59. package/packages/shared/dist/protocol-types.d.ts +74 -1
  60. package/packages/shared/dist/protocol.d.ts +38 -2
  61. package/packages/shared/dist/protocol.js +41 -1
  62. package/apps/cli/dist/commands/interaction.d.ts +0 -2
  63. package/apps/cli/dist/commands/interaction.js +0 -416
  64. package/apps/cli/dist/commands/lib/loop-store.d.ts +0 -9
  65. package/apps/cli/dist/commands/lib/loop-store.js +0 -88
  66. package/apps/daemon/dist/agent-interaction-ipc.d.ts +0 -45
  67. package/apps/daemon/dist/agent-interaction-ipc.js +0 -578
  68. package/apps/daemon/dist/agent-interaction-mcp.d.ts +0 -6
  69. package/apps/daemon/dist/agent-interaction-mcp.js +0 -259
  70. package/apps/daemon/dist/interaction-claim-history.d.ts +0 -4
  71. package/apps/daemon/dist/interaction-claim-history.js +0 -51
  72. package/apps/daemon/dist/interaction-claim-journal.d.ts +0 -20
  73. package/apps/daemon/dist/interaction-claim-journal.js +0 -139
  74. package/apps/daemon/dist/interaction-local-state.d.ts +0 -3
  75. package/apps/daemon/dist/interaction-local-state.js +0 -12
  76. package/apps/daemon/dist/interaction-privacy.d.ts +0 -10
  77. package/apps/daemon/dist/interaction-privacy.js +0 -23
  78. package/apps/daemon/dist/interaction-prompt-redaction-cache.d.ts +0 -7
  79. package/apps/daemon/dist/interaction-prompt-redaction-cache.js +0 -254
  80. package/apps/daemon/dist/interaction-runtime.d.ts +0 -58
  81. package/apps/daemon/dist/interaction-runtime.js +0 -674
  82. package/apps/daemon/dist/lifecycle/index.d.ts +0 -7
  83. package/apps/daemon/dist/lifecycle/index.js +0 -6
  84. package/apps/relay/dist/session-record-handlers.d.ts +0 -15
  85. package/apps/relay/dist/session-record-handlers.js +0 -148
  86. package/apps/relay/dist/session-record-repository.d.ts +0 -38
  87. package/apps/relay/dist/session-record-repository.js +0 -312
  88. package/packages/runtime-cli/dist/session-environment.d.ts +0 -7
  89. package/packages/runtime-cli/dist/session-environment.js +0 -24
  90. package/packages/shared/dist/interaction.d.ts +0 -95
  91. package/packages/shared/dist/interaction.js +0 -169
  92. package/packages/shared/dist/session-record.d.ts +0 -2
  93. package/packages/shared/dist/session-record.js +0 -4
@@ -923,6 +923,7 @@ async function runGatewayPrompt(ws, config, gateway, binding, input, session, le
923
923
  }
924
924
  const started = await startGatewayProviderTurnWithFence(lease, input, binding, () => withCancelSettlement(getRuntime(session.cwd).startTurn({
925
925
  handle: session.handle,
926
+ applicationContext: prepared.applicationContext,
926
927
  text: prepared.text,
927
928
  mode: "prompt",
928
929
  permissionMode,
@@ -17831,6 +17831,7 @@ var capabilitiesSchema = external_exports.object({
17831
17831
  memory: external_exports.boolean().optional(),
17832
17832
  memoryTurnOptionsV1: external_exports.boolean().optional(),
17833
17833
  memoryCommandsV1: external_exports.boolean().optional(),
17834
+ memoryProjectsV1: external_exports.boolean().optional(),
17834
17835
  gatewayCommandsV1: external_exports.boolean().optional(),
17835
17836
  filePreview: external_exports.boolean().optional(),
17836
17837
  scheduleAuthContextV1: external_exports.boolean().optional(),
@@ -18169,6 +18170,14 @@ var controllerMessageSchema = external_exports.discriminatedUnion("type", [
18169
18170
  encryptedPayload: encryptedEnvelopeSchema,
18170
18171
  semanticDigest: external_exports.string().min(1)
18171
18172
  }),
18173
+ external_exports.object({
18174
+ type: external_exports.literal("controller:memoryProjectCommand"),
18175
+ requestId: external_exports.string().min(1),
18176
+ machineId: external_exports.string().min(1),
18177
+ action: external_exports.enum(["list", "overview", "read", "search", "reindex"]),
18178
+ encryptedPayload: encryptedEnvelopeSchema,
18179
+ semanticDigest: external_exports.string().min(1)
18180
+ }),
18172
18181
  external_exports.object({
18173
18182
  type: external_exports.literal("controller:gatewayCommand"),
18174
18183
  requestId: external_exports.string().min(1),
@@ -18532,6 +18541,13 @@ var machineMessageSchema = external_exports.discriminatedUnion("type", [
18532
18541
  encryptedPayload: encryptedEnvelopeSchema,
18533
18542
  messageId: external_exports.string().min(1).optional()
18534
18543
  }),
18544
+ external_exports.object({
18545
+ type: external_exports.literal("machine:memoryProjectResult"),
18546
+ requestId: external_exports.string().min(1),
18547
+ action: external_exports.enum(["list", "overview", "read", "search", "reindex"]),
18548
+ encryptedPayload: encryptedEnvelopeSchema,
18549
+ messageId: external_exports.string().min(1).optional()
18550
+ }),
18535
18551
  external_exports.object({
18536
18552
  type: external_exports.literal("machine:gatewayResult"),
18537
18553
  requestId: external_exports.string().min(1),
@@ -18568,7 +18584,8 @@ var clientMessageSchema = external_exports.union([
18568
18584
  capabilities: external_exports.object({
18569
18585
  daemonLifecycleEventsV1: external_exports.boolean().optional(),
18570
18586
  daemonAutostartV1: external_exports.boolean().optional(),
18571
- resourcePreferenceInvalidationV1: external_exports.boolean().optional()
18587
+ resourcePreferenceInvalidationV1: external_exports.boolean().optional(),
18588
+ daemonProjectsPreferenceV1: external_exports.boolean().optional()
18572
18589
  }).optional()
18573
18590
  }),
18574
18591
  controllerMessageSchema,
@@ -18610,6 +18627,21 @@ var machineCommandSchema = external_exports.discriminatedUnion("type", [
18610
18627
  semanticDigest: external_exports.string().min(1),
18611
18628
  authorization: scheduleAuthorizationContextV1Schema
18612
18629
  }),
18630
+ external_exports.object({
18631
+ type: external_exports.literal("machine:memoryProjectCommand"),
18632
+ requestId: external_exports.string().min(1),
18633
+ controllerRequestId: external_exports.string().min(1),
18634
+ machineId: external_exports.string().min(1),
18635
+ action: external_exports.enum(["list", "overview", "read", "search", "reindex"]),
18636
+ encryptedPayload: encryptedEnvelopeSchema,
18637
+ semanticDigest: external_exports.string().min(1),
18638
+ projectCandidates: external_exports.array(external_exports.object({
18639
+ sessionId: external_exports.string().min(1),
18640
+ cwd: external_exports.string().min(1).max(4096).refine((value) => !value.includes("\0"), "cwd must not contain NUL"),
18641
+ updatedAt: external_exports.number().int().nonnegative()
18642
+ })).max(500),
18643
+ authorization: scheduleAuthorizationContextV1Schema
18644
+ }),
18613
18645
  external_exports.object({
18614
18646
  type: external_exports.literal("machine:gatewayCommand"),
18615
18647
  requestId: external_exports.string().min(1),
@@ -18771,6 +18803,7 @@ var _machineClientMessageTypeSchema = external_exports.enum([
18771
18803
  "machine:cancelResult",
18772
18804
  "machine:scheduleResult",
18773
18805
  "machine:memoryResult",
18806
+ "machine:memoryProjectResult",
18774
18807
  "machine:gatewayResult",
18775
18808
  "machine:daemonLifecycleResult",
18776
18809
  "machine:error"
@@ -18783,6 +18816,7 @@ var serverMessageSchema = external_exports.union([
18783
18816
  machineMessages: external_exports.array(external_exports.string().min(1)).optional(),
18784
18817
  scheduleCreateSemanticDigestV1: external_exports.boolean().optional(),
18785
18818
  memoryCommandsV1: external_exports.boolean().optional(),
18819
+ memoryProjectsV1: external_exports.boolean().optional(),
18786
18820
  memoryTurnOptionsV1: external_exports.boolean().optional(),
18787
18821
  gatewayCommandsV1: external_exports.boolean().optional(),
18788
18822
  enrollmentInvitesV1: external_exports.boolean().optional(),
@@ -18869,6 +18903,13 @@ var serverMessageSchema = external_exports.union([
18869
18903
  action: external_exports.string().min(1),
18870
18904
  encryptedPayload: encryptedEnvelopeSchema
18871
18905
  }),
18906
+ external_exports.object({
18907
+ type: external_exports.literal("server:memoryProjectResult"),
18908
+ requestId: external_exports.string().min(1),
18909
+ machineId: external_exports.string().min(1),
18910
+ action: external_exports.enum(["list", "overview", "read", "search", "reindex"]),
18911
+ encryptedPayload: encryptedEnvelopeSchema
18912
+ }),
18872
18913
  external_exports.object({
18873
18914
  type: external_exports.literal("server:gatewayResult"),
18874
18915
  requestId: external_exports.string().min(1),
@@ -18888,7 +18929,7 @@ var serverMessageSchema = external_exports.union([
18888
18929
  type: external_exports.literal("server:resourcePreferenceInvalidated"),
18889
18930
  resourceType: external_exports.enum(["session", "daemon"]),
18890
18931
  resourceId: external_exports.string().min(1),
18891
- preferenceKey: external_exports.enum(["session.star.v1", "daemon.alias.v1"]),
18932
+ preferenceKey: external_exports.enum(["session.star.v1", "daemon.alias.v1", "daemon.projects.v1"]),
18892
18933
  revision: external_exports.number().int().positive()
18893
18934
  }),
18894
18935
  external_exports.object({
@@ -20539,10 +20580,9 @@ function windowsTaskBackend(run) {
20539
20580
  return {
20540
20581
  async inspect(spec) {
20541
20582
  const [result, identity] = await Promise.all([
20542
- run(command, ["/Query", "/TN", spec.serviceId, "/XML"]),
20583
+ inspectWindowsTask(run, command, spec.serviceId),
20543
20584
  run(identityCommand, ["/user", "/fo", "csv", "/nh"])
20544
20585
  ]);
20545
- requireManagerInspectionQuery(result, "schtasks /Query");
20546
20586
  const registered = result.exitCode === 0;
20547
20587
  if (registered)
20548
20588
  requireManagerInspectionSuccess(identity, "whoami /user");
@@ -20576,6 +20616,15 @@ function windowsTaskBackend(run) {
20576
20616
  }
20577
20617
  };
20578
20618
  }
20619
+ async function inspectWindowsTask(run, command, serviceId) {
20620
+ const result = await run(command, ["/Query", "/TN", serviceId, "/XML"]);
20621
+ requireManagerInspectionSettled(result, "schtasks /Query");
20622
+ if (result.exitCode === 0 || managerObjectMissing(result))
20623
+ return result;
20624
+ const managerProbe = await run(command, ["/Query", "/FO", "CSV", "/NH"]);
20625
+ requireManagerInspectionSuccess(managerProbe, "schtasks /Query");
20626
+ return result;
20627
+ }
20579
20628
  function windowsTaskDefinitionMatches(spec, xmlSource, userSid) {
20580
20629
  const triggers = singleXmlElementBody(xmlSource, "Triggers");
20581
20630
  const principals = singleXmlElementBody(xmlSource, "Principals");
@@ -242,6 +242,7 @@ async function runLoopOnce(ws, config, loop, reason) {
242
242
  };
243
243
  const turn = withCancelSettlement(runtime.startTurn({
244
244
  handle: turnHandle,
245
+ applicationContext: prepared.applicationContext,
245
246
  text: prepared.text,
246
247
  mode: "prompt",
247
248
  permissionMode,
@@ -3,7 +3,7 @@ import { canonicalizeMemoryCommandSemanticsV1, decryptJson, encryptJson, memoryC
3
3
  import { listMemoryFiles, MemoryError, scopeKeyFor } from "../../../../packages/memory/dist/index.js";
4
4
  import { appendAudit } from "../audit.js";
5
5
  import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
6
- import { getMemoryManager } from "./managers.js";
6
+ import { withMemoryManager } from "./managers.js";
7
7
  const MAX_READ_BYTES = 256 * 1024;
8
8
  export async function handleMemoryCommand(ws, config, command) {
9
9
  if (command.type !== "machine:memoryCommand")
@@ -36,8 +36,48 @@ export async function handleMemoryCommand(ws, config, command) {
36
36
  if (semantics.action !== command.action || !digestEqual(memoryCommandSemanticDigestV1(config.accountSecret, semantics), command.semanticDigest)) {
37
37
  throw memoryError("Memory command semantic digest is invalid", "MEMORY_COMMAND_INVALID");
38
38
  }
39
- const manager = await getMemoryManager(config, trustedSession.cwd);
40
- if (!manager) {
39
+ const handled = await withMemoryManager(config, trustedSession.cwd, async (manager) => {
40
+ switch (command.action) {
41
+ case "overview": {
42
+ const [status, files] = await Promise.all([manager.status(), listMemoryFiles(manager.rootDir)]);
43
+ await respond(ws, config, command, { enabled: true, status: publicStatus(status), files });
44
+ return true;
45
+ }
46
+ case "read": {
47
+ if (!semantics.path)
48
+ throw memoryError("Memory path is missing", "MEMORY_COMMAND_INVALID");
49
+ const from = semantics.from ?? 1;
50
+ const requestedLines = semantics.lines ?? 500;
51
+ const read = await manager.readMemory(semantics.path, { from, lines: requestedLines });
52
+ const limited = truncateUtf8(read.text, MAX_READ_BYTES);
53
+ const lines = limited.text.length === 0 ? 0 : limited.text.split("\n").length;
54
+ await respond(ws, config, command, {
55
+ path: read.path,
56
+ text: limited.text,
57
+ from,
58
+ lines,
59
+ truncated: limited.truncated || read.endLine < read.totalLines,
60
+ });
61
+ return true;
62
+ }
63
+ case "search": {
64
+ if (!semantics.query)
65
+ throw memoryError("Memory query is missing", "MEMORY_COMMAND_INVALID");
66
+ const results = await manager.search(semantics.query, {
67
+ maxResults: semantics.maxResults,
68
+ ...(semantics.minScore === undefined ? {} : { minScore: semantics.minScore }),
69
+ });
70
+ await respond(ws, config, command, { results });
71
+ return true;
72
+ }
73
+ case "reindex": {
74
+ const sync = await manager.sync({ force: true });
75
+ await respond(ws, config, command, { sync, status: publicStatus(await manager.status()) });
76
+ return true;
77
+ }
78
+ }
79
+ });
80
+ if (handled === null) {
41
81
  if (command.action === "overview") {
42
82
  await respond(ws, config, command, {
43
83
  enabled: false,
@@ -55,45 +95,7 @@ export async function handleMemoryCommand(ws, config, command) {
55
95
  }
56
96
  throw memoryError("Memory is disabled on this machine", "MEMORY_DISABLED");
57
97
  }
58
- switch (command.action) {
59
- case "overview": {
60
- const [status, files] = await Promise.all([manager.status(), listMemoryFiles(manager.rootDir)]);
61
- await respond(ws, config, command, { enabled: true, status: publicStatus(status), files });
62
- return;
63
- }
64
- case "read": {
65
- if (!semantics.path)
66
- throw memoryError("Memory path is missing", "MEMORY_COMMAND_INVALID");
67
- const from = semantics.from ?? 1;
68
- const requestedLines = semantics.lines ?? 500;
69
- const read = await manager.readMemory(semantics.path, { from, lines: requestedLines });
70
- const limited = truncateUtf8(read.text, MAX_READ_BYTES);
71
- const lines = limited.text.length === 0 ? 0 : limited.text.split("\n").length;
72
- await respond(ws, config, command, {
73
- path: read.path,
74
- text: limited.text,
75
- from,
76
- lines,
77
- truncated: limited.truncated || read.endLine < read.totalLines,
78
- });
79
- return;
80
- }
81
- case "search": {
82
- if (!semantics.query)
83
- throw memoryError("Memory query is missing", "MEMORY_COMMAND_INVALID");
84
- const results = await manager.search(semantics.query, {
85
- maxResults: semantics.maxResults,
86
- ...(semantics.minScore === undefined ? {} : { minScore: semantics.minScore }),
87
- });
88
- await respond(ws, config, command, { results });
89
- return;
90
- }
91
- case "reindex": {
92
- const sync = await manager.sync({ force: true });
93
- await respond(ws, config, command, { sync, status: publicStatus(await manager.status()) });
94
- return;
95
- }
96
- }
98
+ return;
97
99
  }
98
100
  catch (error) {
99
101
  await appendAudit({
@@ -61,6 +61,7 @@ export async function maybeRunMemoryFlush(ws, config, session, metadata) {
61
61
  memoryAccessLease = prepared.lease;
62
62
  const turn = getRuntime(session.cwd).startTurn({
63
63
  handle: session.handle,
64
+ applicationContext: prepared.applicationContext,
64
65
  text: prepared.text,
65
66
  mode: "prompt",
66
67
  requestId,
@@ -15,6 +15,7 @@ export declare function prepareMemoryPrompt(input: {
15
15
  text: string;
16
16
  }): {
17
17
  text: string;
18
+ applicationContext?: string;
18
19
  memoryMode: MemoryMode;
19
20
  accessToken?: string;
20
21
  lease: MemoryAccessLease;
@@ -2,7 +2,7 @@ import { createMemoryAccessLease, revokeCurrentProcessMemoryAccess, } from "../.
2
2
  import { BoundedTtlCache } from "../turn-coordinator.js";
3
3
  import { memoryEnabled, resolveMemoryMode } from "./config.js";
4
4
  import { MEMORY_CLI_LAUNCHER, memoryInstructions } from "./instructions.js";
5
- import { getMemoryManager } from "./managers.js";
5
+ import { withMemoryManager } from "./managers.js";
6
6
  import { assertMemoryAccessHealthy } from "./access-lifecycle.js";
7
7
  const injectedSessionLimit = 1000;
8
8
  const injectedSessionTtlMs = 24 * 60 * 60 * 1000;
@@ -26,14 +26,15 @@ export function prepareMemoryPrompt(input) {
26
26
  return { text: input.text, memoryMode, lease: granted.lease };
27
27
  // Warm the workspace index/watcher so agent writes via the memory CLI are
28
28
  // re-indexed promptly. Fire-and-forget; prompt latency is unaffected.
29
- void getMemoryManager(input.config, input.cwd).catch(() => undefined);
29
+ void withMemoryManager(input.config, input.cwd, async () => undefined).catch(() => undefined);
30
30
  const injectionKey = input.injectionKey ?? input.sessionId;
31
31
  const capabilityPrefix = granted.accessToken
32
32
  ? `Memory write capability for this turn: add \`--access-token ${granted.accessToken}\` to every \`${MEMORY_CLI_LAUNCHER} memory write\` command.`
33
33
  : "";
34
34
  if (injectedSessions.get(injectionKey) === memoryMode) {
35
35
  return {
36
- text: capabilityPrefix ? `${capabilityPrefix}\n\n${input.text}` : input.text,
36
+ text: input.text,
37
+ ...(capabilityPrefix ? { applicationContext: capabilityPrefix } : {}),
37
38
  memoryMode,
38
39
  ...(granted.accessToken ? { accessToken: granted.accessToken } : {}),
39
40
  lease: granted.lease,
@@ -44,7 +45,8 @@ export function prepareMemoryPrompt(input) {
44
45
  return { text: input.text, memoryMode, lease: granted.lease };
45
46
  injectedSessions.set(injectionKey, memoryMode);
46
47
  return {
47
- text: `${instructions}${capabilityPrefix ? `\n\n${capabilityPrefix}` : ""}\n\n---\n\n${input.text}`,
48
+ text: input.text,
49
+ applicationContext: `${instructions}${capabilityPrefix ? `\n\n${capabilityPrefix}` : ""}`,
48
50
  memoryMode,
49
51
  ...(granted.accessToken ? { accessToken: granted.accessToken } : {}),
50
52
  lease: granted.lease,
@@ -1,8 +1,10 @@
1
1
  import { MemoryManager } from "../../../../packages/memory/dist/index.js";
2
2
  import type { DaemonConfig } from "../types.js";
3
3
  /**
4
- * Returns the workspace memory manager for a session cwd, or null when the
5
- * memory subsystem is disabled in daemon.json.
4
+ * Borrows one workspace manager for the complete duration of an operation.
5
+ * Cache eviction may close only idle managers; a bulk all-project search can
6
+ * therefore cross the cache limit without closing a database underneath an
7
+ * in-flight search or sync.
6
8
  */
7
- export declare function getMemoryManager(config: DaemonConfig, cwd: string): Promise<MemoryManager | null>;
9
+ export declare function withMemoryManager<T>(config: DaemonConfig, cwd: string, action: (manager: MemoryManager) => Promise<T>): Promise<T | null>;
8
10
  export declare function closeMemoryManagers(): Promise<void>;
@@ -1,53 +1,56 @@
1
1
  import path from "node:path";
2
- import { MemoryManager, revokeCurrentProcessMemoryAccess } from "../../../../packages/memory/dist/index.js";
2
+ import { listRegisteredMemoryProjects, MemoryManager, registerMemoryProject, revokeCurrentProcessMemoryAccess, } from "../../../../packages/memory/dist/index.js";
3
3
  import { appendAudit } from "../audit.js";
4
4
  import { memoryEnabled } from "./config.js";
5
- const managers = new Map();
6
5
  const managerLimit = 32;
6
+ const managers = new Map();
7
+ const closingManagers = new Map();
7
8
  async function createManager(config, cwd) {
8
9
  const manager = new MemoryManager({
9
10
  scope: { kind: "workspace", cwd },
10
11
  embedding: config.memory?.embedding === "none" ? "none" : "auto",
11
12
  openai: config.memory?.openai,
12
13
  });
13
- await manager.init();
14
- await manager.sync();
15
- manager.startWatcher();
16
- return manager;
14
+ try {
15
+ await manager.init();
16
+ await manager.sync();
17
+ await registerMemoryProject(cwd);
18
+ manager.startWatcher();
19
+ return manager;
20
+ }
21
+ catch (error) {
22
+ await manager.close().catch(() => undefined);
23
+ throw error;
24
+ }
17
25
  }
18
26
  /**
19
- * Returns the workspace memory manager for a session cwd, or null when the
20
- * memory subsystem is disabled in daemon.json.
27
+ * Borrows one workspace manager for the complete duration of an operation.
28
+ * Cache eviction may close only idle managers; a bulk all-project search can
29
+ * therefore cross the cache limit without closing a database underneath an
30
+ * in-flight search or sync.
21
31
  */
22
- export async function getMemoryManager(config, cwd) {
32
+ export async function withMemoryManager(config, cwd, action) {
23
33
  if (!memoryEnabled(config))
24
34
  return null;
25
- // Normalize so different spellings of the same directory share one manager.
35
+ // Validate on every access, including cache hits. A future or malformed
36
+ // project registry must fail closed before an existing manager can continue
37
+ // reading or mutating adjacent Memory state.
38
+ await listRegisteredMemoryProjects();
26
39
  const key = `workspace:${path.resolve(cwd)}`;
27
- let pending = managers.get(key);
28
- if (!pending) {
29
- while (managers.size >= managerLimit) {
30
- const oldestKey = managers.keys().next().value;
31
- if (oldestKey === undefined)
32
- break;
33
- const oldest = managers.get(oldestKey);
34
- managers.delete(oldestKey);
35
- void oldest?.then((manager) => manager.close()).catch(() => undefined);
36
- }
37
- pending = createManager(config, cwd).catch(async (error) => {
38
- managers.delete(key);
39
- await appendAudit({
40
- machineId: config.machineId,
41
- actor: "system",
42
- action: "memory.manager.failed",
43
- summary: `Failed to initialize memory manager for ${cwd}`,
44
- evidence: { error: error instanceof Error ? error.message : String(error) },
45
- });
46
- throw error;
47
- });
48
- managers.set(key, pending);
40
+ let entry = managers.get(key);
41
+ if (!entry) {
42
+ entry = createManagerEntry(config, cwd, key);
43
+ managers.set(key, entry);
44
+ }
45
+ entry.borrowers += 1;
46
+ trimIdleManagers();
47
+ try {
48
+ return await action(await entry.pending);
49
+ }
50
+ finally {
51
+ entry.borrowers -= 1;
52
+ trimIdleManagers();
49
53
  }
50
- return pending;
51
54
  }
52
55
  export async function closeMemoryManagers() {
53
56
  let revokeError;
@@ -57,12 +60,85 @@ export async function closeMemoryManagers() {
57
60
  catch (error) {
58
61
  revokeError = error;
59
62
  }
60
- const pending = [...managers.values()];
63
+ const pending = [...managers.values()].map((entry) => entry.pending);
61
64
  managers.clear();
62
65
  for (const managerPromise of pending) {
63
66
  const manager = await managerPromise.catch(() => null);
64
67
  await manager?.close().catch(() => undefined);
65
68
  }
69
+ await Promise.allSettled([...closingManagers.values()].map((entry) => entry.pending));
70
+ closingManagers.clear();
66
71
  if (revokeError)
67
72
  throw revokeError;
68
73
  }
74
+ function createManagerEntry(config, cwd, key) {
75
+ const entry = {
76
+ borrowers: 0,
77
+ pending: Promise.resolve(null),
78
+ };
79
+ entry.pending = (async () => {
80
+ // An evicted manager may still be completing a watcher sync. Do not open a
81
+ // replacement database for the same workspace until that exact close has
82
+ // settled.
83
+ const closing = closingManagers.get(key);
84
+ if (closing) {
85
+ await closing.pending;
86
+ if (closing.error) {
87
+ throw memoryManagerCloseError();
88
+ }
89
+ }
90
+ return await createManager(config, cwd);
91
+ })().catch(async (error) => {
92
+ if (managers.get(key) === entry)
93
+ managers.delete(key);
94
+ await appendAudit({
95
+ machineId: config.machineId,
96
+ actor: "system",
97
+ action: "memory.manager.failed",
98
+ summary: `Failed to initialize memory manager for ${cwd}`,
99
+ evidence: { error: error instanceof Error ? error.message : String(error) },
100
+ });
101
+ throw error;
102
+ });
103
+ return entry;
104
+ }
105
+ function trimIdleManagers() {
106
+ while (managers.size > managerLimit) {
107
+ const idle = [...managers.entries()].find(([, entry]) => entry.borrowers === 0);
108
+ if (!idle)
109
+ return;
110
+ const [key, entry] = idle;
111
+ managers.delete(key);
112
+ closeManagerEntry(key, entry);
113
+ }
114
+ }
115
+ function closeManagerEntry(key, entry) {
116
+ if (closingManagers.has(key))
117
+ return;
118
+ const closing = {
119
+ error: null,
120
+ pending: Promise.resolve(),
121
+ };
122
+ closing.pending = entry.pending
123
+ .then(async (manager) => {
124
+ try {
125
+ await manager.close();
126
+ }
127
+ catch (error) {
128
+ closing.error = error;
129
+ }
130
+ }, () => undefined)
131
+ .finally(() => {
132
+ // A failed close is an ownership fence, not a best-effort cleanup. Keep
133
+ // it until daemon shutdown so another manager cannot open the same DB
134
+ // while the old watcher/SQLite handle may still be alive.
135
+ if (closing.error === null && closingManagers.get(key) === closing)
136
+ closingManagers.delete(key);
137
+ });
138
+ closingManagers.set(key, closing);
139
+ }
140
+ function memoryManagerCloseError() {
141
+ const error = new Error("Previous Memory manager did not close safely");
142
+ error.code = "MEMORY_MANAGER_CLOSE_FAILED";
143
+ return error;
144
+ }
@@ -0,0 +1,3 @@
1
+ import { type MachineCommand } from "../../../../packages/shared/dist/index.js";
2
+ import type { DaemonConfig } from "../types.js";
3
+ export declare function handleMemoryProjectCommand(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;