@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
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Tests for workspace migration `069-seed-onboarding-threads`.
3
+ *
4
+ * The migration writes onboarding bullet content to `memory/threads.md` only
5
+ * when the file exists and is empty (or whitespace-only). It must preserve
6
+ * any existing content and must be idempotent.
7
+ */
8
+
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ mkdtempSync,
13
+ readFileSync,
14
+ rmSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
20
+
21
+ import { memoryV2InitMigration } from "../workspace/migrations/060-memory-v2-init.js";
22
+ import { seedOnboardingThreadsMigration } from "../workspace/migrations/069-seed-onboarding-threads.js";
23
+
24
+ let workspaceDir: string;
25
+
26
+ beforeEach(() => {
27
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-migration-069-test-"));
28
+ });
29
+
30
+ afterEach(() => {
31
+ if (existsSync(workspaceDir)) {
32
+ rmSync(workspaceDir, { recursive: true, force: true });
33
+ }
34
+ });
35
+
36
+ function readThreads(): string {
37
+ return readFileSync(join(workspaceDir, "memory", "threads.md"), "utf-8");
38
+ }
39
+
40
+ describe("069-seed-onboarding-threads migration", () => {
41
+ test("has correct id and description", () => {
42
+ expect(seedOnboardingThreadsMigration.id).toBe(
43
+ "069-seed-onboarding-threads",
44
+ );
45
+ expect(seedOnboardingThreadsMigration.description).toContain("threads.md");
46
+ });
47
+
48
+ test.each([
49
+ ["empty file", ""],
50
+ ["whitespace-only file", " \n\n"],
51
+ ])("seeds onboarding bullets when memory/threads.md is %s", (_, initial) => {
52
+ mkdirSync(join(workspaceDir, "memory"), { recursive: true });
53
+ writeFileSync(join(workspaceDir, "memory", "threads.md"), initial, "utf-8");
54
+
55
+ seedOnboardingThreadsMigration.run(workspaceDir);
56
+
57
+ const content = readThreads();
58
+ expect(content).toContain("Figure out what kind of personality");
59
+ expect(content).toContain("data/avatar/avatar-image.png");
60
+ expect(content).toContain("ChatGPT, Claude");
61
+ expect(content).toContain("Slack or Telegram");
62
+ });
63
+
64
+ test("preserves existing content when memory/threads.md is non-empty", () => {
65
+ mkdirSync(join(workspaceDir, "memory"), { recursive: true });
66
+ const existing = "Follow up with Bob about the design review.\n";
67
+ writeFileSync(
68
+ join(workspaceDir, "memory", "threads.md"),
69
+ existing,
70
+ "utf-8",
71
+ );
72
+
73
+ seedOnboardingThreadsMigration.run(workspaceDir);
74
+
75
+ expect(readThreads()).toBe(existing);
76
+ });
77
+
78
+ test("no-op when memory/threads.md does not exist", () => {
79
+ seedOnboardingThreadsMigration.run(workspaceDir);
80
+ expect(existsSync(join(workspaceDir, "memory", "threads.md"))).toBe(false);
81
+ });
82
+
83
+ test("idempotent — second run does not duplicate or rewrite content", () => {
84
+ mkdirSync(join(workspaceDir, "memory"), { recursive: true });
85
+ writeFileSync(join(workspaceDir, "memory", "threads.md"), "", "utf-8");
86
+
87
+ seedOnboardingThreadsMigration.run(workspaceDir);
88
+ const afterFirst = readThreads();
89
+
90
+ seedOnboardingThreadsMigration.run(workspaceDir);
91
+ const afterSecond = readThreads();
92
+
93
+ expect(afterSecond).toBe(afterFirst);
94
+ });
95
+
96
+ test("composes with 060: fresh workspace -> 060 -> 069 produces seeded threads.md", () => {
97
+ memoryV2InitMigration.run(workspaceDir);
98
+ seedOnboardingThreadsMigration.run(workspaceDir);
99
+
100
+ expect(readThreads()).toContain("Figure out what kind of personality");
101
+ // The other v2 prose files are untouched (still empty).
102
+ for (const filename of ["essentials.md", "recent.md", "buffer.md"]) {
103
+ expect(
104
+ readFileSync(join(workspaceDir, "memory", filename), "utf-8"),
105
+ ).toBe("");
106
+ }
107
+ });
108
+
109
+ test("down() is a no-op — seeded content remains", () => {
110
+ mkdirSync(join(workspaceDir, "memory"), { recursive: true });
111
+ writeFileSync(join(workspaceDir, "memory", "threads.md"), "", "utf-8");
112
+
113
+ seedOnboardingThreadsMigration.run(workspaceDir);
114
+ const seeded = readThreads();
115
+
116
+ seedOnboardingThreadsMigration.down(workspaceDir);
117
+
118
+ expect(readThreads()).toBe(seeded);
119
+ });
120
+ });
@@ -0,0 +1,206 @@
1
+ import {
2
+ existsSync,
3
+ mkdtempSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import {
11
+ afterAll,
12
+ afterEach,
13
+ beforeAll,
14
+ beforeEach,
15
+ describe,
16
+ expect,
17
+ test,
18
+ } from "bun:test";
19
+
20
+ import { removeSafeStorageReleaseNoteMigration } from "../workspace/migrations/071-remove-safe-storage-release-note.js";
21
+
22
+ const MIGRATION_ID = "071-remove-safe-storage-release-note";
23
+ const SAFE_STORAGE_MARKER =
24
+ "<!-- release-note-id:067-release-notes-safe-storage-limits -->";
25
+ const LATER_MARKER =
26
+ "<!-- release-note-id:068-release-notes-local-timezone -->";
27
+
28
+ const SAFE_STORAGE_RELEASE_NOTE = `${SAFE_STORAGE_MARKER}
29
+ ## Safe storage limits
30
+
31
+ A new storage protection mode is available behind the safe-storage-limits
32
+ rollout flag. When enabled, the assistant watches workspace disk usage and
33
+ enters cleanup mode if the volume reaches the critical 95% threshold.
34
+
35
+ In cleanup mode, background processes pause and remote messages, including
36
+ trusted-contact messages, are blocked until the guardian frees enough space or
37
+ explicitly overrides the lock. The macOS app now shows a storage cleanup banner
38
+ that must be acknowledged before cleanup chat continues, then keeps a status
39
+ banner visible while cleanup mode is active.
40
+ `;
41
+
42
+ let testRoot: string;
43
+ let workspaceDir: string;
44
+
45
+ beforeAll(() => {
46
+ testRoot = mkdtempSync(join(tmpdir(), "migration-071-remove-safe-storage-"));
47
+ });
48
+
49
+ afterAll(() => {
50
+ rmSync(testRoot, { recursive: true, force: true });
51
+ });
52
+
53
+ beforeEach(() => {
54
+ workspaceDir = mkdtempSync(join(testRoot, "ws-"));
55
+ });
56
+
57
+ afterEach(() => {
58
+ rmSync(workspaceDir, { recursive: true, force: true });
59
+ });
60
+
61
+ function updatesPath(): string {
62
+ return join(workspaceDir, "UPDATES.md");
63
+ }
64
+
65
+ describe("workspace migration 071-remove-safe-storage-release-note", () => {
66
+ test("has the correct id and description", () => {
67
+ expect(removeSafeStorageReleaseNoteMigration.id).toBe(MIGRATION_ID);
68
+ expect(removeSafeStorageReleaseNoteMigration.description).toContain(
69
+ "safe storage release note",
70
+ );
71
+ });
72
+
73
+ test("missing UPDATES.md is a no-op", () => {
74
+ expect(existsSync(updatesPath())).toBe(false);
75
+
76
+ removeSafeStorageReleaseNoteMigration.run(workspaceDir);
77
+
78
+ expect(existsSync(updatesPath())).toBe(false);
79
+ });
80
+
81
+ test("removes UPDATES.md when it only contains the safe-storage bulletin", () => {
82
+ writeFileSync(updatesPath(), SAFE_STORAGE_RELEASE_NOTE, "utf-8");
83
+
84
+ removeSafeStorageReleaseNoteMigration.run(workspaceDir);
85
+
86
+ expect(existsSync(updatesPath())).toBe(false);
87
+ });
88
+
89
+ test("preserves prior unrelated release notes when removing the safe-storage block", () => {
90
+ const prior = `<!-- release-note-id:066-earlier-note -->
91
+ ## Earlier note
92
+
93
+ This note should stay.
94
+ `;
95
+ writeFileSync(
96
+ updatesPath(),
97
+ `${prior}\n${SAFE_STORAGE_RELEASE_NOTE}`,
98
+ "utf-8",
99
+ );
100
+
101
+ removeSafeStorageReleaseNoteMigration.run(workspaceDir);
102
+
103
+ const content = readFileSync(updatesPath(), "utf-8");
104
+ expect(content).toContain("## Earlier note");
105
+ expect(content).toContain("This note should stay.");
106
+ expect(content).not.toContain(SAFE_STORAGE_MARKER);
107
+ expect(content).not.toContain("Safe storage limits");
108
+ });
109
+
110
+ test("preserves a later release-note block after safe storage", () => {
111
+ const later = `${LATER_MARKER}
112
+ ## Local timezone grounding
113
+
114
+ This later note should stay.
115
+ `;
116
+ writeFileSync(
117
+ updatesPath(),
118
+ `${SAFE_STORAGE_RELEASE_NOTE}\n${later}`,
119
+ "utf-8",
120
+ );
121
+
122
+ removeSafeStorageReleaseNoteMigration.run(workspaceDir);
123
+
124
+ const content = readFileSync(updatesPath(), "utf-8");
125
+ expect(content.startsWith(LATER_MARKER)).toBe(true);
126
+ expect(content).toContain("## Local timezone grounding");
127
+ expect(content).toContain("This later note should stay.");
128
+ expect(content).not.toContain(SAFE_STORAGE_MARKER);
129
+ expect(content).not.toContain("Safe storage limits");
130
+ });
131
+
132
+ test("fallback preserves a later release-note block after a partial safe-storage block", () => {
133
+ const partialSafeStorage = `${SAFE_STORAGE_MARKER}
134
+ ## Safe storage limits
135
+
136
+ Partially written note.
137
+ `;
138
+ const later = `${LATER_MARKER}
139
+ ## Local timezone grounding
140
+
141
+ This later note should stay.
142
+ `;
143
+ writeFileSync(updatesPath(), `${partialSafeStorage}\n${later}`, "utf-8");
144
+
145
+ removeSafeStorageReleaseNoteMigration.run(workspaceDir);
146
+
147
+ const content = readFileSync(updatesPath(), "utf-8");
148
+ expect(content.startsWith(LATER_MARKER)).toBe(true);
149
+ expect(content).toContain("## Local timezone grounding");
150
+ expect(content).not.toContain(SAFE_STORAGE_MARKER);
151
+ expect(content).not.toContain("Partially written note.");
152
+ });
153
+
154
+ test("content without the safe-storage marker is byte-identical", () => {
155
+ const original =
156
+ "## Existing note\r\n\r\nNo safe storage marker appears here.\r\n";
157
+ writeFileSync(updatesPath(), original, "utf-8");
158
+
159
+ removeSafeStorageReleaseNoteMigration.run(workspaceDir);
160
+
161
+ expect(readFileSync(updatesPath(), "utf-8")).toBe(original);
162
+ });
163
+
164
+ test("preserves CRLF in unrelated content when removing the safe-storage block", () => {
165
+ const prior =
166
+ "<!-- release-note-id:066-earlier-note -->\r\n## Earlier note\r\n\r\nThis note should keep CRLF.\r\n";
167
+ writeFileSync(
168
+ updatesPath(),
169
+ `${prior}\r\n${SAFE_STORAGE_RELEASE_NOTE}`,
170
+ "utf-8",
171
+ );
172
+
173
+ removeSafeStorageReleaseNoteMigration.run(workspaceDir);
174
+
175
+ expect(readFileSync(updatesPath(), "utf-8")).toBe(prior);
176
+ });
177
+
178
+ test("running twice is idempotent", () => {
179
+ const prior = `<!-- release-note-id:066-earlier-note -->
180
+ ## Earlier note
181
+
182
+ This note should stay.
183
+ `;
184
+ const later = `${LATER_MARKER}
185
+ ## Local timezone grounding
186
+
187
+ This later note should stay.
188
+ `;
189
+ writeFileSync(
190
+ updatesPath(),
191
+ `${prior}\n${SAFE_STORAGE_RELEASE_NOTE}\n${later}`,
192
+ "utf-8",
193
+ );
194
+
195
+ removeSafeStorageReleaseNoteMigration.run(workspaceDir);
196
+ const afterFirst = readFileSync(updatesPath(), "utf-8");
197
+
198
+ removeSafeStorageReleaseNoteMigration.run(workspaceDir);
199
+ const afterSecond = readFileSync(updatesPath(), "utf-8");
200
+
201
+ expect(afterSecond).toBe(afterFirst);
202
+ expect(afterSecond).toContain("## Earlier note");
203
+ expect(afterSecond).toContain("## Local timezone grounding");
204
+ expect(afterSecond).not.toContain(SAFE_STORAGE_MARKER);
205
+ });
206
+ });
@@ -12,7 +12,6 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
12
  import { releaseNotesSafeStorageLimitsMigration } from "../workspace/migrations/067-release-notes-safe-storage-limits.js";
13
13
 
14
14
  const MIGRATION_ID = "067-release-notes-safe-storage-limits";
15
- const MARKER = `<!-- release-note-id:${MIGRATION_ID} -->`;
16
15
 
17
16
  let workspaceDir: string;
18
17
 
@@ -28,10 +27,6 @@ function updatesPath(): string {
28
27
  return join(workspaceDir, "UPDATES.md");
29
28
  }
30
29
 
31
- function markerCount(content: string): number {
32
- return content.split(MARKER).length - 1;
33
- }
34
-
35
30
  beforeEach(() => {
36
31
  freshWorkspace();
37
32
  });
@@ -47,44 +42,37 @@ describe("workspace migration 067-release-notes-safe-storage-limits", () => {
47
42
  expect(releaseNotesSafeStorageLimitsMigration.id).toBe(MIGRATION_ID);
48
43
  });
49
44
 
50
- test("creates UPDATES.md with marker and key copy when file is absent", () => {
45
+ test("does not create UPDATES.md when file is absent", () => {
51
46
  expect(existsSync(updatesPath())).toBe(false);
52
47
 
53
48
  releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
54
49
 
55
- const content = readFileSync(updatesPath(), "utf-8");
56
- expect(content).toContain(MARKER);
57
- expect(content).toContain("safe-storage-limits");
58
- expect(content).toContain("critical 95% threshold");
59
- expect(content).toContain("trusted-contact messages");
50
+ expect(existsSync(updatesPath())).toBe(false);
60
51
  });
61
52
 
62
- test("is idempotent when run twice", () => {
63
- releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
53
+ test("leaves existing UPDATES.md byte-identical", () => {
54
+ const existing = "## Prior\n\nExisting release note.\n";
55
+ writeFileSync(updatesPath(), existing, "utf-8");
56
+
64
57
  releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
65
58
 
66
- const content = readFileSync(updatesPath(), "utf-8");
67
- expect(markerCount(content)).toBe(1);
68
- expect(content.match(/Safe storage limits/g)?.length).toBe(1);
59
+ expect(readFileSync(updatesPath(), "utf-8")).toBe(existing);
69
60
  });
70
61
 
71
- test("appends to existing UPDATES.md when marker is absent", () => {
72
- const prior = "## Prior\n\nExisting release note.\n";
73
- writeFileSync(updatesPath(), prior, "utf-8");
74
-
62
+ test("is idempotent when run twice in an empty workspace", () => {
63
+ releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
75
64
  releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
76
65
 
77
- const content = readFileSync(updatesPath(), "utf-8");
78
- expect(content.startsWith(prior)).toBe(true);
79
- expect(content).toContain(MARKER);
66
+ expect(existsSync(updatesPath())).toBe(false);
80
67
  });
81
68
 
82
- test("is a no-op when marker is already present", () => {
83
- const seeded = `## Prior\n\n${MARKER}\nAlready announced.\n`;
84
- writeFileSync(updatesPath(), seeded, "utf-8");
69
+ test("is idempotent when run twice with existing UPDATES.md", () => {
70
+ const existing = "## Prior\n\nExisting release note.\n";
71
+ writeFileSync(updatesPath(), existing, "utf-8");
85
72
 
73
+ releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
86
74
  releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
87
75
 
88
- expect(readFileSync(updatesPath(), "utf-8")).toBe(seeded);
76
+ expect(readFileSync(updatesPath(), "utf-8")).toBe(existing);
89
77
  });
90
78
  });
package/src/agent/loop.ts CHANGED
@@ -96,6 +96,11 @@ export type AgentEvent =
96
96
  matchedTrustRuleId?: string;
97
97
  isContainerized?: boolean;
98
98
  riskScopeOptions?: Array<{ pattern: string; label: string }>;
99
+ riskAllowlistOptions?: Array<{
100
+ label: string;
101
+ description: string;
102
+ pattern: string;
103
+ }>;
99
104
  riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
100
105
  approvalMode?: string;
101
106
  approvalReason?: string;
@@ -282,6 +287,11 @@ export type LoopToolExecutor = (
282
287
  matchedTrustRuleId?: string;
283
288
  isContainerized?: boolean;
284
289
  riskScopeOptions?: Array<{ pattern: string; label: string }>;
290
+ riskAllowlistOptions?: Array<{
291
+ label: string;
292
+ description: string;
293
+ pattern: string;
294
+ }>;
285
295
  riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
286
296
  approvalMode?: string;
287
297
  approvalReason?: string;
@@ -1001,6 +1011,7 @@ export class AgentLoop {
1001
1011
  matchedTrustRuleId: result.matchedTrustRuleId,
1002
1012
  isContainerized: result.isContainerized,
1003
1013
  riskScopeOptions: result.riskScopeOptions,
1014
+ riskAllowlistOptions: result.riskAllowlistOptions,
1004
1015
  riskDirectoryScopeOptions: result.riskDirectoryScopeOptions,
1005
1016
  approvalMode: result.approvalMode,
1006
1017
  approvalReason: result.approvalReason,
@@ -355,12 +355,6 @@ export type CanonicalDecisionResult =
355
355
  resolverFailed?: boolean;
356
356
  resolverFailureReason?: string;
357
357
  resolverReplyText?: string;
358
- activatedContact?: {
359
- sourceChannel: string;
360
- externalUserId: string;
361
- externalChatId?: string;
362
- displayName?: string;
363
- };
364
358
  }
365
359
  | {
366
360
  applied: false;
@@ -527,9 +521,6 @@ export async function applyCanonicalGuardianDecision(
527
521
  let resolverFailed = false;
528
522
  let resolverFailureReason: string | undefined;
529
523
  let resolverReplyText: string | undefined;
530
- let activatedContact:
531
- | { sourceChannel: string; externalUserId: string; externalChatId?: string; displayName?: string }
532
- | undefined;
533
524
  const resolver = getResolver(request.kind);
534
525
  if (resolver) {
535
526
  const resolverResult = await resolver.resolve({
@@ -558,9 +549,6 @@ export async function applyCanonicalGuardianDecision(
558
549
  resolverFailureReason = resolverResult.reason;
559
550
  } else {
560
551
  resolverReplyText = resolverResult.guardianReplyText;
561
- if (resolverResult.activatedContact) {
562
- activatedContact = resolverResult.activatedContact;
563
- }
564
552
  }
565
553
  } else {
566
554
  log.info(
@@ -612,6 +600,5 @@ export async function applyCanonicalGuardianDecision(
612
600
  grantMinted,
613
601
  ...(resolverFailed ? { resolverFailed, resolverFailureReason } : {}),
614
602
  ...(resolverReplyText ? { resolverReplyText } : {}),
615
- ...(activatedContact ? { activatedContact } : {}),
616
603
  };
617
604
  }
@@ -119,12 +119,6 @@ export type ResolverResult =
119
119
  applied: true;
120
120
  grantMinted?: boolean;
121
121
  guardianReplyText?: string;
122
- activatedContact?: {
123
- sourceChannel: string;
124
- externalUserId: string;
125
- externalChatId?: string;
126
- displayName?: string;
127
- };
128
122
  }
129
123
  | { ok: false; reason: string };
130
124
 
@@ -192,28 +186,15 @@ const pendingInteractionResolver: GuardianRequestResolver = {
192
186
  return { ok: false, reason: "pending_interaction_not_found" };
193
187
  }
194
188
 
195
- // Resolve the interaction: remove from tracker and get the session.
196
- const resolved = pendingInteractions.resolve(request.id);
197
- if (!resolved) {
198
- // Race condition: interaction was consumed between get() and resolve().
199
- log.warn(
200
- {
201
- event: "resolver_tool_approval_resolve_race",
202
- requestId: request.id,
203
- },
204
- "Tool approval resolver: pending interaction consumed between lookup and resolve",
205
- );
206
- return { ok: false, reason: "pending_interaction_race" };
207
- }
208
-
209
189
  // Map action to the permission system's UserDecision type and notify session.
190
+ // resolveConfirmation() owns pendingInteractions deregistration.
210
191
  const userDecision: UserDecision =
211
192
  decision.action === "reject" ? "deny" : "allow";
212
- const conversation = findConversation(resolved.conversationId);
193
+ const conversation = findConversation(interaction.conversationId);
213
194
  if (!conversation) {
214
195
  return {
215
196
  ok: false,
216
- reason: `conversation_not_found: ${resolved.conversationId}`,
197
+ reason: `conversation_not_found: ${interaction.conversationId}`,
217
198
  };
218
199
  }
219
200
  conversation.handleConfirmationResponse(
@@ -539,16 +520,7 @@ const accessRequestResolver: GuardianRequestResolver = {
539
520
  );
540
521
  });
541
522
 
542
- return {
543
- ok: true,
544
- applied: true,
545
- activatedContact: {
546
- sourceChannel: "phone",
547
- externalUserId: requesterExternalUserId,
548
- ...(requesterChatId ? { externalChatId: requesterChatId } : {}),
549
- ...(requesterDisplayName ? { displayName: requesterDisplayName } : {}),
550
- },
551
- };
523
+ return { ok: true, applied: true };
552
524
  }
553
525
 
554
526
  // Non-voice approvals: mint an identity-bound verification session so the
@@ -1,14 +1,7 @@
1
1
  import { getConfig } from "../config/loader.js";
2
2
 
3
3
  // Emergency/high-risk numbers that should never be called
4
- const DENIED_NUMBERS = new Set([
5
- "911",
6
- "112",
7
- "999",
8
- "000",
9
- "110",
10
- "119",
11
- ]);
4
+ const DENIED_NUMBERS = new Set(["911", "112", "999", "000", "110", "119"]);
12
5
 
13
6
  /**
14
7
  * Check whether a phone number is a denied emergency number.
@@ -75,3 +68,7 @@ export function getGuardianWaitUpdateSteadyMaxIntervalMs(): number {
75
68
  export function getSilenceTimeoutMs(): number {
76
69
  return 30 * 1000; // 30 seconds
77
70
  }
71
+
72
+ export function getEndCallListenWindowMs(): number {
73
+ return 15 * 1000;
74
+ }