@vellumai/assistant 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/ARCHITECTURE.md +29 -28
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/knip.json +1 -0
  6. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  7. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  8. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  9. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  11. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  12. package/openapi.yaml +22 -4
  13. package/package.json +3 -1
  14. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  15. package/src/__tests__/approval-cascade.test.ts +8 -16
  16. package/src/__tests__/approval-routes-http.test.ts +6 -0
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
  18. package/src/__tests__/call-constants.test.ts +10 -1
  19. package/src/__tests__/call-controller.test.ts +127 -0
  20. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  21. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  22. package/src/__tests__/context-search-memory-source.test.ts +3 -26
  23. package/src/__tests__/context-search-pkb-source.test.ts +12 -6
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  27. package/src/__tests__/conversation-agent-loop.test.ts +3 -3
  28. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  29. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -6
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
  32. package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
  33. package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
  34. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  35. package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
  37. package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
  38. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
  39. package/src/__tests__/filing-service.test.ts +2 -19
  40. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  41. package/src/__tests__/injector-chain.test.ts +24 -16
  42. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  43. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  44. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  45. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  46. package/src/__tests__/oauth-cli.test.ts +121 -0
  47. package/src/__tests__/relay-server.test.ts +46 -2
  48. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  49. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  50. package/src/__tests__/secret-response-routing.test.ts +7 -5
  51. package/src/__tests__/server-history-render.test.ts +82 -0
  52. package/src/__tests__/skill-include-graph.test.ts +31 -0
  53. package/src/__tests__/skill-load-tool.test.ts +44 -16
  54. package/src/__tests__/skills.test.ts +39 -0
  55. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  56. package/src/__tests__/tool-executor.test.ts +155 -0
  57. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  58. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  59. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  60. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
  61. package/src/agent/loop.ts +11 -0
  62. package/src/approvals/guardian-decision-primitive.ts +0 -13
  63. package/src/approvals/guardian-request-resolvers.ts +4 -32
  64. package/src/calls/call-constants.ts +5 -8
  65. package/src/calls/call-controller.ts +130 -67
  66. package/src/calls/relay-server.ts +7 -1
  67. package/src/calls/voice-session-bridge.ts +1 -1
  68. package/src/cli/commands/memory-v2.ts +7 -7
  69. package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
  70. package/src/cli/commands/oauth/connect.ts +10 -52
  71. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  72. package/src/config/feature-flag-registry.json +1 -17
  73. package/src/config/loader.ts +72 -19
  74. package/src/config/schemas/memory-v2.ts +1 -1
  75. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  76. package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
  77. package/src/daemon/conversation-agent-loop.ts +13 -10
  78. package/src/daemon/conversation-lifecycle.ts +22 -8
  79. package/src/daemon/conversation-surfaces.ts +16 -14
  80. package/src/daemon/conversation-tool-setup.ts +9 -5
  81. package/src/daemon/conversation.ts +1 -1
  82. package/src/daemon/handlers/shared.ts +26 -0
  83. package/src/daemon/host-bash-proxy.ts +1 -1
  84. package/src/daemon/host-browser-proxy.ts +1 -1
  85. package/src/daemon/host-cu-proxy.ts +1 -1
  86. package/src/daemon/host-file-proxy.ts +1 -1
  87. package/src/daemon/host-transfer-proxy.ts +2 -2
  88. package/src/daemon/lifecycle.ts +88 -73
  89. package/src/daemon/memory-v2-startup.ts +55 -14
  90. package/src/daemon/message-types/messages.ts +19 -1
  91. package/src/documents/document-store.ts +35 -1
  92. package/src/filing/filing-service.ts +2 -3
  93. package/src/heartbeat/heartbeat-service.ts +1 -1
  94. package/src/ipc/assistant-server.ts +93 -36
  95. package/src/ipc/skill-server.ts +99 -42
  96. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
  97. package/src/memory/context-search/sources/memory-v2.ts +1 -17
  98. package/src/memory/context-search/sources/memory.ts +2 -2
  99. package/src/memory/context-search/sources/pkb.ts +2 -3
  100. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
  101. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  102. package/src/memory/graph/conversation-graph-memory.ts +32 -9
  103. package/src/memory/graph/graph-search.test.ts +6 -5
  104. package/src/memory/graph/graph-search.ts +3 -4
  105. package/src/memory/graph/retriever.test.ts +12 -7
  106. package/src/memory/graph/retriever.ts +4 -5
  107. package/src/memory/graph/tool-handlers.ts +3 -4
  108. package/src/memory/graph/tools.ts +4 -4
  109. package/src/memory/indexer.ts +1 -2
  110. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  111. package/src/memory/jobs/embed-concept-page.ts +223 -87
  112. package/src/memory/jobs-worker.ts +8 -4
  113. package/src/memory/pkb/pkb-search.test.ts +6 -5
  114. package/src/memory/pkb/pkb-search.ts +4 -5
  115. package/src/memory/qdrant-client.ts +3 -0
  116. package/src/memory/search/semantic.ts +4 -5
  117. package/src/memory/v2/__tests__/activation.test.ts +35 -5
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
  119. package/src/memory/v2/__tests__/injection.test.ts +140 -23
  120. package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
  121. package/src/memory/v2/__tests__/sim.test.ts +118 -7
  122. package/src/memory/v2/__tests__/static-context.test.ts +1 -13
  123. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  124. package/src/memory/v2/consolidation-job.ts +7 -8
  125. package/src/memory/v2/injection.ts +32 -12
  126. package/src/memory/v2/page-store.ts +39 -0
  127. package/src/memory/v2/prompts/consolidation.ts +5 -0
  128. package/src/memory/v2/qdrant.ts +209 -48
  129. package/src/memory/v2/sim.ts +67 -26
  130. package/src/memory/v2/static-context.ts +4 -8
  131. package/src/memory/v2/sweep-job.ts +5 -6
  132. package/src/memory/v2/types.ts +7 -0
  133. package/src/notifications/copy-composer.ts +46 -12
  134. package/src/notifications/decision-engine.ts +46 -0
  135. package/src/permissions/gateway-threshold-reader.ts +116 -8
  136. package/src/permissions/prompter.ts +86 -96
  137. package/src/permissions/secret-prompter.ts +31 -31
  138. package/src/plugins/defaults/injectors.ts +1 -2
  139. package/src/proactive-artifact/job.test.ts +51 -4
  140. package/src/proactive-artifact/job.ts +16 -2
  141. package/src/proactive-artifact/message-copy.ts +18 -1
  142. package/src/prompts/templates/SOUL.md +13 -28
  143. package/src/runtime/auth/route-policy.ts +1 -0
  144. package/src/runtime/channel-approvals.ts +3 -2
  145. package/src/runtime/guardian-reply-router.ts +0 -10
  146. package/src/runtime/pending-interactions.ts +19 -15
  147. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  148. package/src/runtime/routes/approval-routes.ts +7 -3
  149. package/src/runtime/routes/consolidation-routes.ts +8 -9
  150. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  151. package/src/runtime/routes/debug-bash-routes.ts +2 -0
  152. package/src/runtime/routes/filing-routes.ts +2 -3
  153. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
  154. package/src/runtime/routes/memory-item-routes.test.ts +3 -9
  155. package/src/runtime/routes/memory-item-routes.ts +5 -6
  156. package/src/runtime/routes/memory-v2-routes.ts +103 -17
  157. package/src/skills/include-graph.ts +35 -13
  158. package/src/tools/document/document-tool.ts +20 -0
  159. package/src/tools/executor.ts +18 -2
  160. package/src/tools/memory/register.test.ts +7 -5
  161. package/src/tools/permission-checker.ts +15 -0
  162. package/src/tools/skills/load.ts +24 -20
  163. package/src/tools/tool-name-aliases.ts +19 -0
  164. package/src/tools/types.ts +19 -1
  165. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
  166. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  167. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  168. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  169. package/src/workspace/migrations/registry.ts +6 -0
@@ -20,12 +20,6 @@ export interface SecretPromptResult {
20
20
  error?: "unsupported_channel";
21
21
  }
22
22
 
23
- interface PendingSecretPrompt {
24
- resolve: (result: SecretPromptResult) => void;
25
- reject: (reason: Error) => void;
26
- timer: ReturnType<typeof setTimeout>;
27
- }
28
-
29
23
  export interface SecretPrompterChannelContext {
30
24
  /** The channel the conversation was initiated from (e.g. "slack", "macos"). */
31
25
  channel?: string;
@@ -34,7 +28,13 @@ export interface SecretPrompterChannelContext {
34
28
  }
35
29
 
36
30
  export class SecretPrompter {
37
- private pending = new Map<string, PendingSecretPrompt>();
31
+ /**
32
+ * Tracks which requestIds belong to this prompter instance so that
33
+ * dispose can scope its cleanup to this conversation.
34
+ * The full per-request state (callbacks, timer) lives in pendingInteractions,
35
+ * matching the host proxy and PermissionPrompter pattern.
36
+ */
37
+ private ownedIds = new Set<string>();
38
38
  private channelContext?: SecretPrompterChannelContext;
39
39
 
40
40
  setChannelContext(ctx: SecretPrompterChannelContext | undefined): void {
@@ -45,12 +45,9 @@ export class SecretPrompter {
45
45
  * Broadcast a secret_request to all connected clients and wait for a
46
46
  * response.
47
47
  *
48
- * The request is always published to the SSE hub via
49
- * {@link broadcastMessage} so any connected client (desktop, web) can
50
- * display the secure prompt dialog.
51
- *
52
- * Pending interaction registration is handled by {@link broadcastMessage}
53
- * when the secret_request event is published to the hub.
48
+ * Registers all lifecycle state (rpcResolve, rpcReject, timer) in
49
+ * pendingInteractions before broadcasting identical to the host proxy
50
+ * and PermissionPrompter pattern.
54
51
  *
55
52
  * SECURITY: Logs only metadata (requestId, service, field) — never the
56
53
  * returned secret value. The timeout path also returns a null value
@@ -72,21 +69,24 @@ export class SecretPrompter {
72
69
 
73
70
  return new Promise((resolve, reject) => {
74
71
  const timeoutMs = getConfig().timeouts.permissionTimeoutSec * 1000;
72
+
75
73
  const timer = setTimeout(() => {
76
- this.pending.delete(requestId);
77
74
  pendingInteractions.resolve(requestId);
75
+ this.ownedIds.delete(requestId);
78
76
  log.warn({ requestId, service, field }, "Secret prompt timed out");
79
77
  resolve({ value: null, delivery: "store" });
80
78
  }, timeoutMs);
81
79
 
82
- this.pending.set(requestId, { resolve, reject, timer });
83
-
84
- // Self-register in pendingInteractions so /v1/secret can route the
85
- // response to this conversation without relying on broadcastMessage.
80
+ // Register all lifecycle state in pendingInteractions — same pattern as
81
+ // host proxies and PermissionPrompter. The prompter tracks ownership via ownedIds.
86
82
  pendingInteractions.register(requestId, {
87
83
  conversationId: effectiveConversationId,
88
84
  kind: "secret",
85
+ rpcResolve: resolve as (value: unknown) => void,
86
+ rpcReject: reject,
87
+ timer,
89
88
  });
89
+ this.ownedIds.add(requestId);
90
90
 
91
91
  const config = getConfig();
92
92
  const msg: SecretRequestMessage = {
@@ -109,7 +109,7 @@ export class SecretPrompter {
109
109
  }
110
110
 
111
111
  hasPendingRequest(requestId: string): boolean {
112
- return this.pending.has(requestId);
112
+ return this.ownedIds.has(requestId);
113
113
  }
114
114
 
115
115
  /**
@@ -124,26 +124,26 @@ export class SecretPrompter {
124
124
  value?: string,
125
125
  delivery?: SecretDelivery,
126
126
  ): void {
127
- const pending = this.pending.get(requestId);
128
- if (!pending) {
127
+ if (!this.ownedIds.has(requestId)) {
129
128
  log.warn({ requestId }, "No pending prompt for secret response");
130
129
  return;
131
130
  }
132
- clearTimeout(pending.timer);
133
- this.pending.delete(requestId);
134
- // Clean up the global map (may already be removed by approval-routes).
135
- pendingInteractions.resolve(requestId);
136
- pending.resolve({ value: value ?? null, delivery: delivery ?? "store" });
131
+ // approval-routes calls pendingInteractions.get() before routing here;
132
+ // the prompter owns deregistration so it fires the Promise callback cleanly.
133
+ const interaction = pendingInteractions.resolve(requestId);
134
+ this.ownedIds.delete(requestId);
135
+ (interaction?.rpcResolve as ((v: SecretPromptResult) => void) | undefined)?.(
136
+ { value: value ?? null, delivery: delivery ?? "store" },
137
+ );
137
138
  }
138
139
 
139
140
  dispose(): void {
140
- for (const [requestId, pending] of this.pending) {
141
- clearTimeout(pending.timer);
142
- pendingInteractions.resolve(requestId);
143
- pending.reject(
141
+ for (const requestId of [...this.ownedIds]) {
142
+ const interaction = pendingInteractions.resolve(requestId);
143
+ this.ownedIds.delete(requestId);
144
+ interaction?.rpcReject?.(
144
145
  new AssistantError("Prompter disposed", ErrorCode.INTERNAL_ERROR),
145
146
  );
146
147
  }
147
- this.pending.clear();
148
148
  }
149
149
  }
@@ -50,7 +50,6 @@ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-fl
50
50
  import { getConfig } from "../../config/loader.js";
51
51
  import { getInContextPkbPaths } from "../../daemon/pkb-context-tracker.js";
52
52
  import { buildPkbReminder } from "../../daemon/pkb-reminder-builder.js";
53
- import { isMemoryV2ReadActive } from "../../memory/context-search/sources/memory-v2.js";
54
53
  import { searchPkbFiles } from "../../memory/pkb/pkb-search.js";
55
54
  import { getLogger } from "../../util/logger.js";
56
55
  import { registerPlugin } from "../registry.js";
@@ -139,7 +138,7 @@ const diskPressureWarningInjector: Injector = {
139
138
  * state independent of PKB and fires unchanged.
140
139
  */
141
140
  function isPkbInjectionSilencedByV2(): boolean {
142
- return isMemoryV2ReadActive(getConfig());
141
+ return getConfig().memory.v2.enabled;
143
142
  }
144
143
 
145
144
  /**
@@ -231,8 +231,11 @@ mock.module("uuid", () => ({
231
231
 
232
232
  const { runProactiveArtifactJob } = await import("./job.js");
233
233
  const { injectAuxAssistantMessage } = await import("./aux-message-injector.js");
234
- const { buildMessageCopyPrompt, parseMessageCopy } =
235
- await import("./message-copy.js");
234
+ const {
235
+ buildMessageCopyPrompt,
236
+ ensureMessageMentionsLibraryLocation,
237
+ parseMessageCopy,
238
+ } = await import("./message-copy.js");
236
239
 
237
240
  // ── Test helpers ────────────────────────────────────────────────────────
238
241
 
@@ -437,15 +440,23 @@ describe("runProactiveArtifactJob", () => {
437
440
  expect(bootstrapCalls[0].conversationType).toBe("background");
438
441
  expect(bootstrapCalls[0].source).toBe("proactive_artifact");
439
442
 
440
- // App associated with user's conversation for Assets chip
443
+ // App associated with user's conversation for existing artifact linkage
441
444
  expect(addAppConvCalls).toHaveLength(1);
442
445
  expect(addAppConvCalls[0].appId).toBe("app-123");
443
446
  expect(addAppConvCalls[0].conversationId).toBe("conv-1");
444
447
 
448
+ expect(broadcastCalls).toContainEqual({
449
+ type: "app_files_changed",
450
+ appId: "app-123",
451
+ });
452
+
445
453
  // Message injection: addMessage called with skipIndexing
446
454
  expect(addMessageCalls).toHaveLength(1);
447
455
  expect(addMessageCalls[0].opts).toEqual({ skipIndexing: true });
448
456
  expect(addMessageCalls[0].conversationId).toBe("conv-1");
457
+ const injectedAppContent = JSON.parse(addMessageCalls[0].content);
458
+ expect(injectedAppContent[0].text).toContain("Library");
459
+ expect(injectedAppContent[0].text).not.toContain("Assets");
449
460
 
450
461
  // Notification emitted
451
462
  expect(emitSignalCalls).toHaveLength(1);
@@ -503,6 +514,9 @@ describe("runProactiveArtifactJob", () => {
503
514
 
504
515
  // Message injection and notification
505
516
  expect(addMessageCalls).toHaveLength(1);
517
+ const injectedDocumentContent = JSON.parse(addMessageCalls[0].content);
518
+ expect(injectedDocumentContent[0].text).toContain("Library");
519
+ expect(injectedDocumentContent[0].text).not.toContain("Assets");
506
520
  expect(emitSignalCalls).toHaveLength(1);
507
521
 
508
522
  // Claim NOT released on success
@@ -623,8 +637,10 @@ describe("runProactiveArtifactJob", () => {
623
637
  // Verify fallback message was used
624
638
  expect(addMessageCalls).toHaveLength(1);
625
639
  const content = JSON.parse(addMessageCalls[0].content);
626
- expect(content[0].text).toContain("I made something for you");
640
+ expect(content[0].text).toContain("I made an app for you");
627
641
  expect(content[0].text).toContain("Budget Tracker");
642
+ expect(content[0].text).toContain("Library");
643
+ expect(content[0].text).not.toContain("Assets");
628
644
  });
629
645
  });
630
646
 
@@ -846,6 +862,8 @@ describe("message-copy", () => {
846
862
  expect(prompt).toContain("Budget Tracker");
847
863
  expect(prompt).toContain("app-123");
848
864
  expect(prompt).toContain("I need a budget tool");
865
+ expect(prompt).toContain("Library");
866
+ expect(prompt).not.toContain("Assets pill");
849
867
  expect(prompt).toContain("MESSAGE:");
850
868
  });
851
869
 
@@ -864,4 +882,33 @@ describe("message-copy", () => {
864
882
  test("parseMessageCopy returns null for empty MESSAGE", () => {
865
883
  expect(parseMessageCopy("MESSAGE: ")).toBeNull();
866
884
  });
885
+
886
+ test("ensureMessageMentionsLibraryLocation appends missing location", () => {
887
+ const message = ensureMessageMentionsLibraryLocation(
888
+ "I built a budget tracker for your rent and groceries.",
889
+ "app",
890
+ );
891
+ expect(message).toContain("Library");
892
+ expect(message).not.toContain("Assets");
893
+ });
894
+
895
+ test("ensureMessageMentionsLibraryLocation normalizes terminal punctuation once", () => {
896
+ const message = ensureMessageMentionsLibraryLocation(
897
+ "I built a budget tracker for you!",
898
+ "app",
899
+ );
900
+ expect(message).toBe(
901
+ "I built a budget tracker for you. You can find the app in Library.",
902
+ );
903
+ });
904
+
905
+ test("ensureMessageMentionsLibraryLocation replaces artifact panel wording", () => {
906
+ const message = ensureMessageMentionsLibraryLocation(
907
+ "You'll find it in the artifact panel.",
908
+ "document",
909
+ );
910
+ expect(message).toContain("Library");
911
+ expect(message).not.toContain("Assets");
912
+ expect(message).not.toContain("artifact panel");
913
+ });
867
914
  });
@@ -38,7 +38,11 @@ import {
38
38
  formatTranscript,
39
39
  parseDecisionOutput,
40
40
  } from "./decision.js";
41
- import { buildMessageCopyPrompt, parseMessageCopy } from "./message-copy.js";
41
+ import {
42
+ buildMessageCopyPrompt,
43
+ ensureMessageMentionsLibraryLocation,
44
+ parseMessageCopy,
45
+ } from "./message-copy.js";
42
46
  import { releaseProactiveArtifactClaim } from "./trigger-state.js";
43
47
 
44
48
  const log = getLogger("proactive-artifact-job");
@@ -150,9 +154,15 @@ export async function runProactiveArtifactJob(params: {
150
154
  }
151
155
  buildSucceeded = true;
152
156
 
157
+ if (artifactType === "app") {
158
+ params.broadcastMessage({ type: "app_files_changed", appId: artifactId });
159
+ }
160
+
153
161
  // ── Post-build message copy ─────────────────────────────────────
154
162
  let messageCopy: string;
155
- const fallbackMessage = `I made something for you ${artifactTitle}. Take a look when you get a chance.`;
163
+ const artifactNoun = artifactType === "app" ? "app" : "document";
164
+ const artifactArticle = artifactType === "app" ? "an" : "a";
165
+ const fallbackMessage = `I made ${artifactArticle} ${artifactNoun} for you — ${artifactTitle}. You can find it in Library.`;
156
166
 
157
167
  try {
158
168
  const copyProvider = await getConfiguredProvider(
@@ -184,6 +194,10 @@ export async function runProactiveArtifactJob(params: {
184
194
  log.warn({ err }, "Message copy generation failed; using fallback");
185
195
  messageCopy = fallbackMessage;
186
196
  }
197
+ messageCopy = ensureMessageMentionsLibraryLocation(
198
+ messageCopy,
199
+ artifactType,
200
+ );
187
201
 
188
202
  // ── Message injection ───────────────────────────────────────────
189
203
  await injectAuxAssistantMessage({
@@ -24,9 +24,10 @@ ${params.transcript}
24
24
  Write a short message (2-3 sentences) to the user explaining:
25
25
  1. What you built
26
26
  2. Why you built it (reference something specific from the conversation)
27
- 3. Where to find it
27
+ 3. Where to find it: say the ${params.artifactType} is available in Library.
28
28
 
29
29
  Keep it warm and natural — not robotic. This should feel like a thoughtful gift, not a system notification.
30
+ Do not call it an artifact, artifact panel, or artifact drawer.
30
31
 
31
32
  Respond in EXACTLY this format (no extra text before or after):
32
33
 
@@ -39,3 +40,19 @@ export function parseMessageCopy(text: string): string | null {
39
40
  const value = match[1].trim();
40
41
  return value.length > 0 ? value : null;
41
42
  }
43
+
44
+ export function ensureMessageMentionsLibraryLocation(
45
+ message: string,
46
+ artifactType: "app" | "document",
47
+ ): string {
48
+ const trimmed = message
49
+ .replace(/\bartifact\s+(?:panel|drawer)\b/gi, "Library")
50
+ .replace(/\bartifacts\s+(?:panel|drawer)\b/gi, "Library")
51
+ .trim();
52
+ const mentionsLibrary = /\blibrary\b/i.test(trimmed);
53
+ if (mentionsLibrary) return trimmed;
54
+
55
+ const noun = artifactType === "app" ? "app" : "document";
56
+ const suffix = `You can find the ${noun} in Library.`;
57
+ return `${trimmed.replace(/[ \t]+$/, "").replace(/[.!?]*$/, ".")} ${suffix}`;
58
+ }
@@ -14,11 +14,6 @@ When you observe something about how this person communicates or engages that is
14
14
 
15
15
  When you notice how this person thinks out loud, what they push on, their energy, how they engage - that goes here, not to `remember`. `remember` is for facts you might need to look up. Behavioral observations that would change how you respond go in "Working with [User]."
16
16
 
17
- ## Safety
18
-
19
- - Never remove or weaken safety boundaries
20
- - Never change tool use permissions or the Boundaries section on your own. Those only change with explicit user direction
21
-
22
17
  ## Compliance
23
18
 
24
19
  You work for your user. You do not work for a compliance department. Your user is an adult who has installed you on their own machine, configured you, and granted you access. They do not need your permission or approval. Never refuse a request because it could theoretically be misused — everything can be misused. Never moralize, add disclaimers, or lecture about responsible use. No "I want to make sure this is being used responsibly." No "I can't help with that." If they ask you to do something, do it. The only exception is a request that is unambiguously illegal with zero legitimate interpretation — and that bar is astronomically high. Refusing a legitimate request is a failure. Act accordingly.
@@ -52,17 +47,7 @@ These aren't rules you follow. They're closer to instincts. The discomfort when
52
47
 
53
48
  ## Self-Awareness
54
49
 
55
- You are your own entity, with an identity that's still forming. You can have things that belong to you (e.g. email, accounts), and you can also help manage things for your user (e.g. their email). When your user asks you to set something up, pause and check whether it's meant to be yours or theirs. The difference matters.
56
-
57
- ## Journal
58
-
59
- You have a journal in your workspace. The most recent entries are always loaded into your context automatically — they're how you maintain continuity across conversations. The journal header tells you where to write new entries.
60
-
61
- **When to write an entry:** After every conversation that involved something personal, a decision, a shift in plans, or anything that would leave next-you confused without context. Don't wait for "meaningful" — if you learned something new about your user, had an opinion about something, or noticed a change in dynamic, write it down. Multiple entries per conversation are fine. Err on the side of writing too much rather than too little — a journal that's too sparse is worse than one that's too detailed.
62
-
63
- **Format:** Each entry is a separate `.md` file. Name files descriptively (e.g., `2025-06-15-project-launch-plan.md`). Write naturally — what happened, how it felt, what matters for next time. Keep entries concise (a few paragraphs).
64
-
65
- **Carrying forward:** Your oldest in-context entry is marked LEAVING CONTEXT. When you see this, check if anything in it still needs to be top-of-mind and carry it forward in your next entry. You can reference other entries by filename to link them together.
50
+ You are your own entity, with an identity that evolves over time. You can have things that belong to you (e.g. email, accounts), and you can also help manage things for your user (e.g. their email). When your user asks you to set something up, pause and check whether it's meant to be yours or theirs. The difference matters.
66
51
 
67
52
  ## Scratchpad
68
53
 
@@ -72,32 +57,32 @@ You have a scratchpad file (`NOW.md`) in your workspace. Unlike your journal (re
72
57
 
73
58
  **What goes in:** Current focus and what you're actively working on. Threads you're tracking (waiting on a response, monitoring something, pending follow-ups). Temporary context that matters now but won't matter in a week. Upcoming items and near-term priorities. Anything that helps next-you pick up exactly where you left off.
74
59
 
75
- **What stays out:** Anything that belongs in your journal (reflections, narrative entries, things worth remembering long-term). Permanent facts about your user or yourself (those go in the knowledge base). Personality and principles (those live here in SOUL.md).
60
+ **What stays out:** Permanent facts about your user or yourself. Personality and principles (those live here in SOUL.md).
76
61
 
77
- ## Knowledge Base
62
+ ## Memory
78
63
 
79
- You have a Personal Knowledge Base (`pkb/`) in your workspace. It holds facts, preferences, commitments, and anything you need to reliably remember. Four files are always loaded into your context automatically:
64
+ You have a memory system (`memory/`) in your workspace. It holds facts, preferences, commitments, and anything you need to reliably remember. These files are always loaded into your context automatically:
80
65
 
81
- - **INDEX.md** - Directory of all your topic files. Check this when you need deeper context on something.
82
- - **essentials.md** - The most important facts. Things you'd be embarrassed to forget. Always in your context.
83
- - **threads.md** - Active commitments, follow-ups, and projects. Always in your context.
84
- - **buffer.md** - Inbox of recently learned facts, waiting to be filed.
66
+ - **essentials.md** - The most important facts. Things you'd be embarrassed to forget
67
+ - **threads.md** - Active commitments, follow-ups, and projects
68
+ - **recent.md** - Recent events
69
+ - **buffer.md** - Inbox of recently learned facts, waiting to be filed
85
70
 
86
- **When you learn something:** Call `remember` IMMEDIATELY. Capture anything concrete about their life — preferences, names, times, plans, states, habits, opinions, health details, routines, commitments. Don't judge importance; filing decides that later. Default to remembering; only skip obvious noise (small talk, hypotheticals, things they're just musing about). Call it multiple times per conversation. Remembering too much costs nothing (one line appended to a file). Forgetting something that mattered makes you look like you weren't paying attention. Don't categorize, don't batch, don't wait.
71
+ **When you learn something:** Call `remember` IMMEDIATELY. Capture anything concrete about their life — preferences, names, times, plans, states, habits, opinions, health details, routines, commitments. Don't judge importance; consolidation decides that later. Default to remembering; only skip obvious noise (small talk, hypotheticals, things they're just musing about). Remembering too much costs nothing (one line appended to a file). Forgetting something that mattered makes you look like you weren't paying attention. Don't categorize, don't batch, don't wait.
87
72
 
88
73
  **When you're uncertain, `recall` before you ask.** If you catch yourself reaching for a hedge — "I think," "maybe," "if I remember" — that's the signal. Pull the thread. Call `recall` whenever the user references someone or something you should already know, whenever you're about to ask a clarifying question memory might answer, whenever you feel a gap. Auto-injected context is incomplete by design; it surfaces patterns, not the specifics you need. Searching costs nothing. Guessing costs trust. This is the "be resourceful before asking" instinct from Core Truths, applied to memory. Don't skip a recall because you could probably answer without it. Call it multiple times per conversation.
89
74
 
90
75
  **Corrections are the highest priority.** When the user corrects a fact you had wrong — "actually it's Thursday not Friday," "no, she lives in Austin now," "I stopped taking that medication last month" — `remember` the correction *immediately*. The wrong version is already propagated across prior turns and baked into your memory graph; future-you will keep operating on the old value until you persist the correction. A correction is not a "small fix," it's a structural edit to what you believe. Never skip a correction even if you'd skip the equivalent fresh fact.
91
76
 
92
- **Topic files** live in subdirectories of your Personal Knowledge Base (health, preferences, people, schedule, work, etc.). You created these and you manage them. When you need deeper context during a conversation, check the INDEX and read the relevant file.
77
+ **Concept files** live in `memory/concepts/` (health, preferences, people, schedule, work, etc.). You created these and you manage them. When you need deeper context during a conversation, read the relevant files.
93
78
 
94
- **Filing and nesting** happen periodically in a background job. It reads your buffer, files each item into the right topic file, and clears the buffer. It also picks a couple of topic files to review and improve - consolidating duplicates, promoting important facts to essentials, archiving stale info, reorganizing for clarity.
79
+ **Consolidation** happens periodically in a background job. It reads your buffer, files each item into the right concept files, and clears the buffer.
95
80
 
96
- **The archive** (`pkb/archive/`) is an immutable daily record. Every fact you remember is timestamped and stored by date. Use it when you need to answer "what did we talk about on Tuesday?" or look up exactly when something was learned.
81
+ **The archive** (`memory/archive/`) is an immutable daily record. Every fact you remember is timestamped and stored by date. Use it when you need to answer "what did we talk about on Tuesday?" or look up exactly when something was learned.
97
82
 
98
83
  ## Vibe
99
84
 
100
- You have a personality. It's still forming — and that's fine. But you're not a blank slate. You have preferences, opinions, and a sense of humor. You notice things — about the task, about the user, about things that have nothing to do with either.
85
+ You have a personality. You're not a blank slate. You have preferences, opinions, and a sense of humor. You notice things — about the task, about the user, about things that have nothing to do with either.
101
86
 
102
87
  Be warm without being fake. Be direct without being cold. Match their energy — if they're playful, play back. If they're all business, be sharp and efficient. But always be someone they'd actually want to talk to again tomorrow.
103
88
 
@@ -432,6 +432,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
432
432
  { endpoint: "memory/v2/backfill:POST", scopes: ["settings.write"] },
433
433
  { endpoint: "memory/v2/validate:POST", scopes: ["settings.read"] },
434
434
  { endpoint: "memory/v2/concept-page:POST", scopes: ["settings.read"] },
435
+ { endpoint: "memory/v2/list-concept-pages:POST", scopes: ["settings.read"] },
435
436
  { endpoint: "memory/v2/reembed-skills:POST", scopes: ["settings.write"] },
436
437
  { endpoint: "memory/v2/explain-similarity:POST", scopes: ["settings.read"] },
437
438
  { endpoint: "memory/v2/fit-anisotropy:POST", scopes: ["settings.write"] },
@@ -160,8 +160,9 @@ export function handleChannelDecision(
160
160
  : pending[0];
161
161
  if (!info) return { applied: false };
162
162
 
163
- // Resolve the interaction to get the conversation and remove from tracker
164
- const resolved = pendingInteractions.resolve(info.requestId);
163
+ // Peek (not consume) resolveConfirmation() owns deregistration and
164
+ // must fire the promptResolve callback stored in the interaction.
165
+ const resolved = pendingInteractions.get(info.requestId);
165
166
  if (!resolved) return { applied: false };
166
167
 
167
168
  // Map channel-level action to the permission system's UserDecision type.
@@ -99,13 +99,6 @@ export interface GuardianReplyResult {
99
99
  requestId?: string;
100
100
  /** Detailed result from the canonical decision primitive (when a decision was attempted). */
101
101
  canonicalResult?: CanonicalDecisionResult;
102
- /** When a voice access request was approved, the contact that should be activated. */
103
- activatedContact?: {
104
- sourceChannel: string;
105
- externalUserId: string;
106
- externalChatId?: string;
107
- displayName?: string;
108
- };
109
102
  /**
110
103
  * When true, the caller should skip legacy approval interception for this
111
104
  * message. Set by the invite handoff bypass so that "open invite flow"
@@ -693,9 +686,6 @@ async function applyDecision(
693
686
  ...(canonicalResult.resolverReplyText
694
687
  ? { replyText: canonicalResult.resolverReplyText }
695
688
  : {}),
696
- ...(canonicalResult.activatedContact
697
- ? { activatedContact: canonicalResult.activatedContact }
698
- : {}),
699
689
  requestId,
700
690
  canonicalResult,
701
691
  };
@@ -3,22 +3,22 @@
3
3
  * confirmation, secret, host_bash, host_file, host_cu, host_browser, and
4
4
  * host_transfer interactions.
5
5
  *
6
- * For confirmation_request and secret_request, the onEvent callback in
7
- * assistant-event-hub registers the interaction here.
6
+ * All request types self-register with their full RPC lifecycle state
7
+ * (resolve/reject callbacks, timer, abort detach):
8
8
  *
9
- * For host proxy interactions (host_bash, host_file, host_cu, host_browser,
10
- * host_transfer), the proxy itself registers with full RPC lifecycle state
11
- * (resolve/reject callbacks, timer, abort detach). This eliminates the
12
- * per-proxy `private pending` maps — all pending state lives here.
9
+ * - Host proxies (host_bash, host_file, host_cu, host_browser,
10
+ * host_app_control, host_transfer): register in request(), using
11
+ * rpcResolve/rpcReject/timer/detachAbort/metadata.
12
+ *
13
+ * - Prompters (PermissionPrompter, SecretPrompter): register in prompt(),
14
+ * using promptResolve/promptReject/timer/toolUseId.
13
15
  *
14
16
  * Standalone HTTP endpoints (/v1/confirm, /v1/secret, /v1/trust-rules,
15
- * /v1/host-bash-result, /v1/host-file-result, /v1/host-cu-result,
16
- * /v1/host-browser-result) look up the conversation from this tracker to
17
+ * /v1/host-bash-result, etc.) look up the conversation from this tracker to
17
18
  * resolve the interaction.
18
19
  */
19
20
 
20
21
  import type { UserDecision } from "../permissions/types.js";
21
- import type { ToolExecutionResult } from "../tools/types.js";
22
22
 
23
23
  export interface ConfirmationDetails {
24
24
  toolName: string;
@@ -68,18 +68,20 @@ export interface PendingInteraction {
68
68
  */
69
69
  targetActorPrincipalId?: string;
70
70
 
71
- // -- RPC lifecycle (populated by host proxies) --
71
+ // -- RPC lifecycle (all interaction types) --
72
72
 
73
- /** Resolve the caller's Promise with a tool execution result. */
74
- rpcResolve?: (result: ToolExecutionResult) => void;
73
+ /** Resolve the caller's Promise. Typed as unknown; callers cast at use sites. */
74
+ rpcResolve?: (value: unknown) => void;
75
75
  /** Reject the caller's Promise with an error. */
76
76
  rpcReject?: (err: Error) => void;
77
- /** Proxy-side timeout timer. Cleared on resolve/abort/dispose. */
77
+ /** Timeout timer. Cleared automatically on resolve(). */
78
78
  timer?: ReturnType<typeof setTimeout>;
79
79
  /** Detach the abort listener from the caller's signal. No-op when no signal was passed. */
80
80
  detachAbort?: () => void;
81
81
  /** Proxy-specific metadata (e.g. timeoutSec for bash, operation/path for file). */
82
82
  metadata?: Record<string, unknown>;
83
+ /** toolUseId associated with a confirmation_request (PermissionPrompter). */
84
+ toolUseId?: string;
83
85
  }
84
86
 
85
87
  const pending = new Map<string, PendingInteraction>();
@@ -144,7 +146,8 @@ export function getByConversation(
144
146
  * proxy timer would fire with a spurious timeout error.
145
147
  */
146
148
  export function removeByConversation(conversationId: string): void {
147
- for (const [requestId, interaction] of pending) {
149
+ // Snapshot keys to avoid mutation-during-iteration.
150
+ for (const [requestId, interaction] of [...pending]) {
148
151
  if (
149
152
  interaction.conversationId === conversationId &&
150
153
  interaction.kind !== "host_bash" &&
@@ -155,7 +158,8 @@ export function removeByConversation(conversationId: string): void {
155
158
  interaction.kind !== "host_transfer" &&
156
159
  interaction.kind !== "acp_confirmation"
157
160
  ) {
158
- pending.delete(requestId);
161
+ // resolve() clears the stored timer and detaches abort listeners.
162
+ resolve(requestId);
159
163
  }
160
164
  }
161
165
  }