@vellumai/assistant 0.4.30 → 0.4.32

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 (194) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +14 -8
  3. package/README.md +2 -2
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +1 -4
  6. package/package.json +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  8. package/src/__tests__/anthropic-provider.test.ts +86 -1
  9. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  10. package/src/__tests__/checker.test.ts +37 -98
  11. package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
  12. package/src/__tests__/config-schema.test.ts +6 -14
  13. package/src/__tests__/conflict-policy.test.ts +76 -0
  14. package/src/__tests__/conflict-store.test.ts +14 -20
  15. package/src/__tests__/contacts-tools.test.ts +8 -61
  16. package/src/__tests__/contradiction-checker.test.ts +5 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  19. package/src/__tests__/followup-tools.test.ts +0 -30
  20. package/src/__tests__/gemini-provider.test.ts +79 -1
  21. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  22. package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
  23. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  24. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  25. package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
  26. package/src/__tests__/memory-regressions.test.ts +6 -6
  27. package/src/__tests__/openai-provider.test.ts +82 -0
  28. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  29. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  30. package/src/__tests__/recurrence-types.test.ts +0 -15
  31. package/src/__tests__/registry.test.ts +0 -10
  32. package/src/__tests__/schedule-tools.test.ts +28 -44
  33. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  34. package/src/__tests__/session-agent-loop.test.ts +0 -2
  35. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  36. package/src/__tests__/session-profile-injection.test.ts +0 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  38. package/src/__tests__/session-skill-tools.test.ts +0 -49
  39. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  40. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  41. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  42. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  43. package/src/__tests__/task-management-tools.test.ts +111 -0
  44. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  45. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  46. package/src/__tests__/twilio-config.test.ts +0 -3
  47. package/src/amazon/session.ts +30 -91
  48. package/src/approvals/guardian-decision-primitive.ts +11 -7
  49. package/src/approvals/guardian-request-resolvers.ts +5 -3
  50. package/src/calls/call-controller.ts +423 -571
  51. package/src/calls/finalize-call.ts +20 -0
  52. package/src/calls/relay-access-wait.ts +340 -0
  53. package/src/calls/relay-server.ts +269 -899
  54. package/src/calls/relay-setup-router.ts +307 -0
  55. package/src/calls/relay-verification.ts +280 -0
  56. package/src/calls/twilio-config.ts +1 -8
  57. package/src/calls/voice-control-protocol.ts +184 -0
  58. package/src/calls/voice-session-bridge.ts +1 -8
  59. package/src/config/agent-schema.ts +1 -1
  60. package/src/config/bundled-skills/contacts/SKILL.md +7 -18
  61. package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
  62. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
  63. package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
  64. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
  65. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  66. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  67. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  68. package/src/config/bundled-tool-registry.ts +0 -5
  69. package/src/config/core-schema.ts +1 -1
  70. package/src/config/env.ts +0 -10
  71. package/src/config/feature-flag-registry.json +1 -1
  72. package/src/config/loader.ts +19 -0
  73. package/src/config/memory-schema.ts +0 -10
  74. package/src/config/schema.ts +2 -2
  75. package/src/config/system-prompt.ts +6 -0
  76. package/src/contacts/contact-store.ts +36 -62
  77. package/src/contacts/contacts-write.ts +14 -3
  78. package/src/contacts/types.ts +9 -4
  79. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  80. package/src/daemon/handlers/contacts.ts +2 -2
  81. package/src/daemon/handlers/guardian-actions.ts +1 -1
  82. package/src/daemon/handlers/session-history.ts +398 -0
  83. package/src/daemon/handlers/session-user-message.ts +982 -0
  84. package/src/daemon/handlers/sessions.ts +9 -1337
  85. package/src/daemon/ipc-contract/contacts.ts +2 -2
  86. package/src/daemon/ipc-contract/sessions.ts +0 -6
  87. package/src/daemon/ipc-contract-inventory.json +0 -1
  88. package/src/daemon/lifecycle.ts +0 -29
  89. package/src/daemon/session-agent-loop.ts +1 -45
  90. package/src/daemon/session-conflict-gate.ts +21 -82
  91. package/src/daemon/session-memory.ts +7 -52
  92. package/src/daemon/session-process.ts +3 -1
  93. package/src/daemon/session-runtime-assembly.ts +18 -35
  94. package/src/heartbeat/heartbeat-service.ts +5 -1
  95. package/src/home-base/app-link-store.ts +0 -7
  96. package/src/memory/conflict-intent.ts +3 -6
  97. package/src/memory/conflict-policy.ts +34 -0
  98. package/src/memory/conflict-store.ts +10 -18
  99. package/src/memory/contradiction-checker.ts +2 -2
  100. package/src/memory/conversation-attention-store.ts +1 -1
  101. package/src/memory/conversation-store.ts +0 -51
  102. package/src/memory/db-init.ts +8 -0
  103. package/src/memory/job-handlers/conflict.ts +24 -7
  104. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  105. package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
  106. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  107. package/src/memory/migrations/index.ts +2 -0
  108. package/src/memory/migrations/registry.ts +6 -0
  109. package/src/memory/recall-cache.ts +0 -5
  110. package/src/memory/schema/calls.ts +274 -0
  111. package/src/memory/schema/contacts.ts +125 -0
  112. package/src/memory/schema/conversations.ts +129 -0
  113. package/src/memory/schema/guardian.ts +172 -0
  114. package/src/memory/schema/index.ts +8 -0
  115. package/src/memory/schema/infrastructure.ts +205 -0
  116. package/src/memory/schema/memory-core.ts +196 -0
  117. package/src/memory/schema/notifications.ts +191 -0
  118. package/src/memory/schema/tasks.ts +78 -0
  119. package/src/memory/schema.ts +1 -1402
  120. package/src/memory/slack-thread-store.ts +0 -69
  121. package/src/messaging/index.ts +0 -1
  122. package/src/messaging/types.ts +0 -38
  123. package/src/notifications/decisions-store.ts +2 -105
  124. package/src/notifications/deliveries-store.ts +0 -11
  125. package/src/notifications/preferences-store.ts +1 -58
  126. package/src/permissions/checker.ts +6 -17
  127. package/src/providers/anthropic/client.ts +6 -2
  128. package/src/providers/gemini/client.ts +13 -2
  129. package/src/providers/managed-proxy/constants.ts +55 -0
  130. package/src/providers/managed-proxy/context.ts +77 -0
  131. package/src/providers/registry.ts +112 -0
  132. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
  133. package/src/runtime/guardian-action-service.ts +3 -2
  134. package/src/runtime/guardian-outbound-actions.ts +3 -3
  135. package/src/runtime/guardian-reply-router.ts +4 -4
  136. package/src/runtime/http-server.ts +83 -710
  137. package/src/runtime/http-types.ts +0 -16
  138. package/src/runtime/middleware/auth.ts +0 -12
  139. package/src/runtime/routes/app-routes.ts +33 -0
  140. package/src/runtime/routes/approval-routes.ts +32 -0
  141. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  142. package/src/runtime/routes/attachment-routes.ts +32 -0
  143. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  144. package/src/runtime/routes/call-routes.ts +41 -0
  145. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  146. package/src/runtime/routes/channel-routes.ts +70 -0
  147. package/src/runtime/routes/contact-routes.ts +371 -29
  148. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  149. package/src/runtime/routes/conversation-routes.ts +192 -194
  150. package/src/runtime/routes/debug-routes.ts +15 -0
  151. package/src/runtime/routes/events-routes.ts +16 -0
  152. package/src/runtime/routes/global-search-routes.ts +17 -2
  153. package/src/runtime/routes/guardian-action-routes.ts +23 -1
  154. package/src/runtime/routes/guardian-approval-interception.ts +2 -1
  155. package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
  156. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  157. package/src/runtime/routes/identity-routes.ts +20 -0
  158. package/src/runtime/routes/inbound-message-handler.ts +8 -0
  159. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
  160. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  161. package/src/runtime/routes/integration-routes.ts +83 -0
  162. package/src/runtime/routes/invite-routes.ts +31 -0
  163. package/src/runtime/routes/migration-routes.ts +47 -17
  164. package/src/runtime/routes/pairing-routes.ts +18 -0
  165. package/src/runtime/routes/secret-routes.ts +20 -0
  166. package/src/runtime/routes/surface-action-routes.ts +26 -0
  167. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  168. package/src/runtime/routes/twilio-routes.ts +79 -0
  169. package/src/schedule/recurrence-types.ts +1 -11
  170. package/src/tools/followups/followup_create.ts +9 -3
  171. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  172. package/src/tools/memory/definitions.ts +0 -6
  173. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  174. package/src/tools/schedule/create.ts +1 -3
  175. package/src/tools/schedule/update.ts +9 -6
  176. package/src/twitter/session.ts +29 -77
  177. package/src/util/cookie-session.ts +114 -0
  178. package/src/workspace/git-service.ts +6 -4
  179. package/src/__tests__/conversation-routes.test.ts +0 -99
  180. package/src/__tests__/get-weather.test.ts +0 -393
  181. package/src/__tests__/task-tools.test.ts +0 -685
  182. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  183. package/src/autonomy/autonomy-resolver.ts +0 -62
  184. package/src/autonomy/autonomy-store.ts +0 -138
  185. package/src/autonomy/disposition-mapper.ts +0 -31
  186. package/src/autonomy/index.ts +0 -11
  187. package/src/autonomy/types.ts +0 -43
  188. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  189. package/src/config/bundled-skills/weather/TOOLS.json +0 -36
  190. package/src/config/bundled-skills/weather/icon.svg +0 -24
  191. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  192. package/src/contacts/startup-migration.ts +0 -21
  193. package/src/messaging/triage-engine.ts +0 -344
  194. package/src/tools/weather/service.ts +0 -712
@@ -42,10 +42,6 @@ describe("CommitEnrichmentService", () => {
42
42
  beforeEach(() => {
43
43
  _resetGitServiceRegistry();
44
44
  _resetEnrichmentService();
45
- // Previous tests' enrichment jobs may leave a stale index.lock if
46
- // the git process exits but the lock file isn't flushed before the
47
- // next test runs git operations in the shared testDir.
48
- rmSync(join(testDir, ".git", "index.lock"), { force: true });
49
45
  });
50
46
 
51
47
  afterEach(async () => {
@@ -55,6 +51,14 @@ describe("CommitEnrichmentService", () => {
55
51
  /* ignore */
56
52
  }
57
53
  _resetEnrichmentService();
54
+
55
+ // Remove stale index.lock left by async enrichment jobs that ran git
56
+ // commands concurrently. Without this, the next test's createCommit()
57
+ // can fail with "Unable to create index.lock: File exists".
58
+ const lockFile = join(testDir, ".git", "index.lock");
59
+ if (existsSync(lockFile)) {
60
+ rmSync(lockFile, { force: true });
61
+ }
58
62
  });
59
63
 
60
64
  afterAll(async () => {
@@ -82,6 +86,13 @@ describe("CommitEnrichmentService", () => {
82
86
  }
83
87
 
84
88
  async function createCommit(): Promise<string> {
89
+ // Remove stale index.lock left by async enrichment jobs that ran git
90
+ // commands concurrently in a previous test. Without this, git add -A
91
+ // can fail with "Unable to create index.lock: File exists".
92
+ const lockFile = join(testDir, ".git", "index.lock");
93
+ if (existsSync(lockFile)) {
94
+ rmSync(lockFile, { force: true });
95
+ }
85
96
  writeFileSync(join(testDir, `file-${Date.now()}.txt`), "content");
86
97
  await gitService.commitChanges("test commit");
87
98
  return await gitService.getHeadHash();
@@ -168,10 +168,8 @@ describe("AssistantConfigSchema", () => {
168
168
  expect(result.memory.conflicts).toEqual({
169
169
  enabled: true,
170
170
  gateMode: "soft",
171
- reaskCooldownTurns: 3,
172
171
  resolverLlmTimeoutMs: 12000,
173
172
  relevanceThreshold: 0.3,
174
- askOnIrrelevantTurns: false,
175
173
  conflictableKinds: [
176
174
  "preference",
177
175
  "profile",
@@ -189,13 +187,6 @@ describe("AssistantConfigSchema", () => {
189
187
  expect(result.success).toBe(false);
190
188
  });
191
189
 
192
- test("rejects invalid memory.conflicts.askOnIrrelevantTurns", () => {
193
- const result = AssistantConfigSchema.safeParse({
194
- memory: { conflicts: { askOnIrrelevantTurns: 123 } },
195
- });
196
- expect(result.success).toBe(false);
197
- });
198
-
199
190
  test("rejects invalid memory.conflicts.conflictableKinds entry", () => {
200
191
  const result = AssistantConfigSchema.safeParse({
201
192
  memory: { conflicts: { conflictableKinds: ["invalid_kind"] } },
@@ -536,11 +527,12 @@ describe("AssistantConfigSchema", () => {
536
527
  expect(result.permissions.mode).toBe("strict");
537
528
  });
538
529
 
539
- test("accepts explicit permissions.mode legacy", () => {
540
- const result = AssistantConfigSchema.parse({
541
- permissions: { mode: "legacy" },
542
- });
543
- expect(result.permissions.mode).toBe("legacy");
530
+ test("rejects permissions.mode legacy", () => {
531
+ expect(() =>
532
+ AssistantConfigSchema.parse({
533
+ permissions: { mode: "legacy" },
534
+ }),
535
+ ).toThrow();
544
536
  });
545
537
 
546
538
  test("accepts explicit permissions.mode workspace", () => {
@@ -3,9 +3,11 @@ import { describe, expect, test } from "bun:test";
3
3
  import {
4
4
  isConflictKindEligible,
5
5
  isConflictKindPairEligible,
6
+ isConflictUserEvidenced,
6
7
  isDurableInstructionStatement,
7
8
  isStatementConflictEligible,
8
9
  isTransientTrackingStatement,
10
+ isUserEvidencedVerificationState,
9
11
  } from "../memory/conflict-policy.js";
10
12
 
11
13
  describe("conflict-policy", () => {
@@ -190,4 +192,78 @@ describe("conflict-policy", () => {
190
192
  ).toBe(true);
191
193
  });
192
194
  });
195
+
196
+ describe("isUserEvidencedVerificationState", () => {
197
+ test("accepts user_reported", () => {
198
+ expect(isUserEvidencedVerificationState("user_reported")).toBe(true);
199
+ });
200
+
201
+ test("accepts user_confirmed", () => {
202
+ expect(isUserEvidencedVerificationState("user_confirmed")).toBe(true);
203
+ });
204
+
205
+ test("accepts legacy_import", () => {
206
+ expect(isUserEvidencedVerificationState("legacy_import")).toBe(true);
207
+ });
208
+
209
+ test("rejects assistant_inferred", () => {
210
+ expect(isUserEvidencedVerificationState("assistant_inferred")).toBe(
211
+ false,
212
+ );
213
+ });
214
+
215
+ test("rejects unknown states", () => {
216
+ expect(isUserEvidencedVerificationState("")).toBe(false);
217
+ expect(isUserEvidencedVerificationState("auto_detected")).toBe(false);
218
+ expect(isUserEvidencedVerificationState("pending")).toBe(false);
219
+ });
220
+ });
221
+
222
+ describe("isConflictUserEvidenced", () => {
223
+ test("returns true when existing side is user-evidenced", () => {
224
+ expect(
225
+ isConflictUserEvidenced("user_reported", "assistant_inferred"),
226
+ ).toBe(true);
227
+ expect(
228
+ isConflictUserEvidenced("user_confirmed", "assistant_inferred"),
229
+ ).toBe(true);
230
+ expect(
231
+ isConflictUserEvidenced("legacy_import", "assistant_inferred"),
232
+ ).toBe(true);
233
+ });
234
+
235
+ test("returns true when candidate side is user-evidenced", () => {
236
+ expect(
237
+ isConflictUserEvidenced("assistant_inferred", "user_reported"),
238
+ ).toBe(true);
239
+ expect(
240
+ isConflictUserEvidenced("assistant_inferred", "user_confirmed"),
241
+ ).toBe(true);
242
+ expect(
243
+ isConflictUserEvidenced("assistant_inferred", "legacy_import"),
244
+ ).toBe(true);
245
+ });
246
+
247
+ test("returns true when both sides are user-evidenced", () => {
248
+ expect(isConflictUserEvidenced("user_reported", "user_confirmed")).toBe(
249
+ true,
250
+ );
251
+ expect(isConflictUserEvidenced("legacy_import", "user_reported")).toBe(
252
+ true,
253
+ );
254
+ });
255
+
256
+ test("returns false when neither side is user-evidenced", () => {
257
+ expect(
258
+ isConflictUserEvidenced("assistant_inferred", "assistant_inferred"),
259
+ ).toBe(false);
260
+ });
261
+
262
+ test("returns false for unknown states on both sides", () => {
263
+ expect(isConflictUserEvidenced("auto_detected", "pending")).toBe(false);
264
+ expect(
265
+ isConflictUserEvidenced("assistant_inferred", "auto_detected"),
266
+ ).toBe(false);
267
+ });
268
+ });
193
269
  });
@@ -33,7 +33,6 @@ import {
33
33
  getPendingConflictByPair,
34
34
  listPendingConflictDetails,
35
35
  listPendingConflicts,
36
- markConflictAsked,
37
36
  resolveConflict,
38
37
  } from "../memory/conflict-store.js";
39
38
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
@@ -60,6 +59,10 @@ function resetTables() {
60
59
  function insertItemPair(
61
60
  suffix: string,
62
61
  scopeId = "default",
62
+ opts?: {
63
+ existingVerificationState?: string;
64
+ candidateVerificationState?: string;
65
+ },
63
66
  ): { existingItemId: string; candidateItemId: string } {
64
67
  const db = getDb();
65
68
  const now = Date.now();
@@ -76,7 +79,8 @@ function insertItemPair(
76
79
  confidence: 0.8,
77
80
  importance: 0.5,
78
81
  fingerprint: `fp-existing-${suffix}`,
79
- verificationState: "assistant_inferred",
82
+ verificationState:
83
+ opts?.existingVerificationState ?? "assistant_inferred",
80
84
  scopeId,
81
85
  firstSeenAt: now,
82
86
  lastSeenAt: now,
@@ -90,7 +94,8 @@ function insertItemPair(
90
94
  confidence: 0.8,
91
95
  importance: 0.5,
92
96
  fingerprint: `fp-candidate-${suffix}`,
93
- verificationState: "assistant_inferred",
97
+ verificationState:
98
+ opts?.candidateVerificationState ?? "assistant_inferred",
94
99
  scopeId,
95
100
  firstSeenAt: now,
96
101
  lastSeenAt: now,
@@ -218,24 +223,11 @@ describe("conflict-store", () => {
218
223
  expect(pendingDefault[0].status).toBe("pending_clarification");
219
224
  });
220
225
 
221
- test("markConflictAsked updates lastAskedAt", () => {
222
- const pair = insertItemPair("asked");
223
- const conflict = createOrUpdatePendingConflict({
224
- scopeId: "default",
225
- existingItemId: pair.existingItemId,
226
- candidateItemId: pair.candidateItemId,
227
- relationship: "ambiguous_contradiction",
226
+ test("listPendingConflictDetails joins current statements and verification states", () => {
227
+ const pair = insertItemPair("details", "workspace-a", {
228
+ existingVerificationState: "user_confirmed",
229
+ candidateVerificationState: "assistant_inferred",
228
230
  });
229
-
230
- const askedAt = 1_734_000_000_000;
231
- expect(markConflictAsked(conflict.id, askedAt)).toBe(true);
232
- const updated = getConflictById(conflict.id);
233
- expect(updated?.lastAskedAt).toBe(askedAt);
234
- expect(updated?.updatedAt).toBe(askedAt);
235
- });
236
-
237
- test("listPendingConflictDetails joins current statements", () => {
238
- const pair = insertItemPair("details", "workspace-a");
239
231
  createOrUpdatePendingConflict({
240
232
  scopeId: "workspace-a",
241
233
  existingItemId: pair.existingItemId,
@@ -250,6 +242,8 @@ describe("conflict-store", () => {
250
242
  expect(details[0].candidateStatement).toBe("Candidate statement details");
251
243
  expect(details[0].existingKind).toBe("fact");
252
244
  expect(details[0].candidateKind).toBe("fact");
245
+ expect(details[0].existingVerificationState).toBe("user_confirmed");
246
+ expect(details[0].candidateVerificationState).toBe("assistant_inferred");
253
247
  });
254
248
 
255
249
  test("applyConflictResolution keeps candidate and resolves conflict row", () => {
@@ -140,17 +140,14 @@ describe("contact_upsert tool", () => {
140
140
  expect(result.isError).toBe(false);
141
141
  expect(result.content).toContain("Created contact");
142
142
  expect(result.content).toContain("Alice");
143
- expect(result.content).toContain("Importance: 0.50");
144
143
  });
145
144
 
146
145
  test("creates a contact with all fields", async () => {
147
146
  const result = await executeContactUpsert(
148
147
  {
149
148
  display_name: "Bob",
150
- relationship: "colleague",
151
- importance: 0.8,
152
- response_expectation: "within_hours",
153
- preferred_tone: "professional",
149
+ notes:
150
+ "Colleague at Acme Corp, prefers professional tone, responds within hours",
154
151
  channels: [
155
152
  { type: "email", address: "bob@example.com", is_primary: true },
156
153
  { type: "slack", address: "@bob" },
@@ -161,10 +158,7 @@ describe("contact_upsert tool", () => {
161
158
 
162
159
  expect(result.isError).toBe(false);
163
160
  expect(result.content).toContain("Bob");
164
- expect(result.content).toContain("colleague");
165
- expect(result.content).toContain("0.80");
166
- expect(result.content).toContain("within_hours");
167
- expect(result.content).toContain("professional");
161
+ expect(result.content).toContain("Notes: Colleague at Acme Corp");
168
162
  expect(result.content).toContain("email: bob@example.com");
169
163
  expect(result.content).toContain("slack: @bob");
170
164
  });
@@ -185,7 +179,7 @@ describe("contact_upsert tool", () => {
185
179
  {
186
180
  id: contactId,
187
181
  display_name: "Charlie Updated",
188
- importance: 0.9,
182
+ notes: "Updated notes for Charlie",
189
183
  },
190
184
  ctx,
191
185
  );
@@ -193,7 +187,7 @@ describe("contact_upsert tool", () => {
193
187
  expect(updateResult.isError).toBe(false);
194
188
  expect(updateResult.content).toContain("Updated contact");
195
189
  expect(updateResult.content).toContain("Charlie Updated");
196
- expect(updateResult.content).toContain("0.90");
190
+ expect(updateResult.content).toContain("Notes: Updated notes for Charlie");
197
191
  });
198
192
 
199
193
  test("auto-matches by channel address on create", async () => {
@@ -239,36 +233,6 @@ describe("contact_upsert tool", () => {
239
233
  expect(result.isError).toBe(true);
240
234
  expect(result.content).toContain("display_name is required");
241
235
  });
242
-
243
- test("rejects importance out of range", async () => {
244
- const result = await executeContactUpsert(
245
- {
246
- display_name: "Test",
247
- importance: 1.5,
248
- },
249
- ctx,
250
- );
251
-
252
- expect(result.isError).toBe(true);
253
- expect(result.content).toContain(
254
- "importance must be a number between 0 and 1",
255
- );
256
- });
257
-
258
- test("rejects negative importance", async () => {
259
- const result = await executeContactUpsert(
260
- {
261
- display_name: "Test",
262
- importance: -0.1,
263
- },
264
- ctx,
265
- );
266
-
267
- expect(result.isError).toBe(true);
268
- expect(result.content).toContain(
269
- "importance must be a number between 0 and 1",
270
- );
271
- });
272
236
  });
273
237
 
274
238
  // ── contact_search ──────────────────────────────────────────────────
@@ -305,23 +269,6 @@ describe("contact_search tool", () => {
305
269
  expect(result.content).toContain("Charlie");
306
270
  });
307
271
 
308
- test("searches by relationship", async () => {
309
- await executeContactUpsert(
310
- { display_name: "Diana", relationship: "friend" },
311
- ctx,
312
- );
313
- await executeContactUpsert(
314
- { display_name: "Eve", relationship: "colleague" },
315
- ctx,
316
- );
317
-
318
- const result = await executeContactSearch({ relationship: "friend" }, ctx);
319
-
320
- expect(result.isError).toBe(false);
321
- expect(result.content).toContain("Diana");
322
- expect(result.content).not.toContain("Eve");
323
- });
324
-
325
272
  test("returns no results message when nothing matches", async () => {
326
273
  await executeContactUpsert({ display_name: "Existing" }, ctx);
327
274
 
@@ -380,7 +327,7 @@ describe("contact_merge tool", () => {
380
327
  const r1 = await executeContactUpsert(
381
328
  {
382
329
  display_name: "Alice (Email)",
383
- importance: 0.7,
330
+ notes: "Prefers email",
384
331
  channels: [{ type: "email", address: "alice@example.com" }],
385
332
  },
386
333
  ctx,
@@ -388,7 +335,7 @@ describe("contact_merge tool", () => {
388
335
  const r2 = await executeContactUpsert(
389
336
  {
390
337
  display_name: "Alice (Slack)",
391
- importance: 0.9,
338
+ notes: "Active on Slack",
392
339
  channels: [{ type: "slack", address: "@alice" }],
393
340
  },
394
341
  ctx,
@@ -407,7 +354,7 @@ describe("contact_merge tool", () => {
407
354
 
408
355
  expect(result.isError).toBe(false);
409
356
  expect(result.content).toContain("Merged");
410
- expect(result.content).toContain("Importance: 0.90"); // takes higher importance
357
+ expect(result.content).toContain("Notes: Prefers email\nActive on Slack"); // concatenated notes
411
358
  expect(result.content).toContain("email: alice@example.com");
412
359
  expect(result.content).toContain("slack: @alice");
413
360
 
@@ -201,7 +201,11 @@ describe("checkContradictions", () => {
201
201
  expect(conflicts[0].existingItemId).toBe("item-existing-ambiguous");
202
202
  expect(conflicts[0].candidateItemId).toBe("item-candidate-ambiguous");
203
203
  expect(conflicts[0].relationship).toBe("ambiguous_contradiction");
204
- expect(conflicts[0].clarificationQuestion).toContain(
204
+ expect(conflicts[0].clarificationQuestion).toContain("Pending conflict:");
205
+ expect(conflicts[0].clarificationQuestion).not.toContain(
206
+ "I have conflicting notes",
207
+ );
208
+ expect(conflicts[0].clarificationQuestion).not.toContain(
205
209
  "Which one is correct?",
206
210
  );
207
211
  });
@@ -241,6 +241,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
241
241
  "schedule/integration-status.ts", // integration status checks for scheduled reports
242
242
  "daemon/handlers/oauth-connect.ts", // OAuth connect handler for integration setup
243
243
  "daemon/handlers/config-slack-channel.ts", // Slack channel config credential management
244
+ "providers/managed-proxy/context.ts", // managed proxy API key lookup for provider initialization
244
245
  ]);
245
246
 
246
247
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -52,7 +52,6 @@ class MockSession {
52
52
  | { skipPreMessageRollback?: boolean; isInteractive?: boolean }
53
53
  | undefined;
54
54
  public updateClientHistory: Array<{ hasNoClient: boolean }> = [];
55
- public setSandboxOverrideCalls = 0;
56
55
  private stale = false;
57
56
  private processing = false;
58
57
  public trustContext: Record<string, unknown> | null = null;
@@ -95,9 +94,7 @@ class MockSession {
95
94
  return this._currentSender;
96
95
  }
97
96
 
98
- setSandboxOverride(): void {
99
- this.setSandboxOverrideCalls += 1;
100
- }
97
+ setSandboxOverride(): void {}
101
98
 
102
99
  isProcessing(): boolean {
103
100
  return this.processing;
@@ -433,21 +430,6 @@ describe("DaemonServer initial session hydration", () => {
433
430
  expect(lastCreatedWorkingDir).toBe("/tmp/workspace");
434
431
  });
435
432
 
436
- test("ignores deprecated sandbox_set runtime override messages", async () => {
437
- const server = new DaemonServer();
438
- const internal = asDaemonServerTestAccess(server);
439
- const { socket } = createFakeSocket();
440
-
441
- await internal.sendInitialSession(socket);
442
- const session = internal.sessions.get(conversation.id);
443
- expect(session).toBeDefined();
444
- expect(session!.setSandboxOverrideCalls).toBe(0);
445
-
446
- internal.dispatchMessage({ type: "sandbox_set", enabled: false }, socket);
447
-
448
- expect(session!.setSandboxOverrideCalls).toBe(0);
449
- });
450
-
451
433
  test("sendInitialSession includes threadType in session_info", async () => {
452
434
  conversation.threadType = "private";
453
435
  const server = new DaemonServer();
@@ -122,36 +122,6 @@ describe("followup_create tool", () => {
122
122
  expect(result.content).toContain("Reminder schedule: sched-abc");
123
123
  });
124
124
 
125
- test("creates a follow-up with deprecated reminder_cron_id alias", async () => {
126
- const result = await executeFollowupCreate(
127
- {
128
- channel: "email",
129
- thread_id: "thread-789",
130
- reminder_cron_id: "cron-abc",
131
- },
132
- ctx,
133
- );
134
-
135
- expect(result.isError).toBe(false);
136
- expect(result.content).toContain("Reminder schedule: cron-abc");
137
- });
138
-
139
- test("reminder_schedule_id takes precedence over reminder_cron_id", async () => {
140
- const result = await executeFollowupCreate(
141
- {
142
- channel: "email",
143
- thread_id: "thread-prio",
144
- reminder_schedule_id: "sched-wins",
145
- reminder_cron_id: "cron-loses",
146
- },
147
- ctx,
148
- );
149
-
150
- expect(result.isError).toBe(false);
151
- expect(result.content).toContain("Reminder schedule: sched-wins");
152
- expect(result.content).not.toContain("cron-loses");
153
- });
154
-
155
125
  test("rejects missing channel", async () => {
156
126
  const result = await executeFollowupCreate(
157
127
  {
@@ -30,6 +30,7 @@ interface FakeChunk {
30
30
 
31
31
  let fakeChunks: FakeChunk[] = [];
32
32
  let lastStreamParams: Record<string, unknown> | null = null;
33
+ let lastConstructorOpts: Record<string, unknown> | null = null;
33
34
  let shouldThrow: Error | null = null;
34
35
 
35
36
  class FakeApiError extends Error {
@@ -43,7 +44,9 @@ class FakeApiError extends Error {
43
44
 
44
45
  mock.module("@google/genai", () => ({
45
46
  GoogleGenAI: class MockGoogleGenAI {
46
- constructor(_opts: Record<string, unknown>) {}
47
+ constructor(opts: Record<string, unknown>) {
48
+ lastConstructorOpts = opts;
49
+ }
47
50
  models = {
48
51
  generateContentStream: async (params: Record<string, unknown>) => {
49
52
  lastStreamParams = params;
@@ -108,6 +111,7 @@ describe("GeminiProvider", () => {
108
111
  provider = new GeminiProvider("test-api-key", "gemini-3-flash");
109
112
  fakeChunks = [];
110
113
  lastStreamParams = null;
114
+ lastConstructorOpts = null;
111
115
  shouldThrow = null;
112
116
  });
113
117
 
@@ -726,4 +730,78 @@ describe("GeminiProvider", () => {
726
730
 
727
731
  expect(result.usage).toEqual({ inputTokens: 0, outputTokens: 0 });
728
732
  });
733
+
734
+ // -----------------------------------------------------------------------
735
+ // Managed transport — constructor configuration
736
+ // -----------------------------------------------------------------------
737
+ test("does not set httpOptions when managedBaseUrl is not provided", () => {
738
+ new GeminiProvider("test-key", "gemini-3-flash");
739
+ expect(lastConstructorOpts).toEqual({ apiKey: "test-key" });
740
+ });
741
+
742
+ test("sets httpOptions.baseUrl when managedBaseUrl is provided", () => {
743
+ new GeminiProvider("managed-key", "gemini-3-flash", {
744
+ managedBaseUrl: "https://platform.example.com/v1/runtime-proxy/gemini",
745
+ });
746
+ expect(lastConstructorOpts).toEqual({
747
+ apiKey: "managed-key",
748
+ httpOptions: {
749
+ baseUrl: "https://platform.example.com/v1/runtime-proxy/gemini",
750
+ },
751
+ });
752
+ });
753
+
754
+ test("managed transport produces same ProviderResponse shape", async () => {
755
+ const managedProvider = new GeminiProvider(
756
+ "managed-key",
757
+ "gemini-3-flash",
758
+ {
759
+ managedBaseUrl: "https://platform.example.com/v1/runtime-proxy/gemini",
760
+ },
761
+ );
762
+
763
+ fakeChunks = [textChunk("Hello from managed"), finishChunk("STOP", 15, 8)];
764
+
765
+ const result = await managedProvider.sendMessage([
766
+ { role: "user", content: [{ type: "text", text: "Hi" }] },
767
+ ]);
768
+
769
+ expect(result.content).toHaveLength(1);
770
+ expect(result.content[0]).toEqual({
771
+ type: "text",
772
+ text: "Hello from managed",
773
+ });
774
+ expect(result.model).toBe("gemini-3-flash-001");
775
+ expect(result.usage).toEqual({ inputTokens: 15, outputTokens: 8 });
776
+ expect(result.stopReason).toBe("STOP");
777
+ });
778
+
779
+ test("managed transport handles tool calls correctly", async () => {
780
+ const managedProvider = new GeminiProvider(
781
+ "managed-key",
782
+ "gemini-3-flash",
783
+ {
784
+ managedBaseUrl: "https://platform.example.com/v1/runtime-proxy/gemini",
785
+ },
786
+ );
787
+
788
+ fakeChunks = [
789
+ functionCallChunk([
790
+ { id: "call_managed", name: "file_read", args: { path: "/tmp/test" } },
791
+ ]),
792
+ finishChunk("STOP", 10, 15),
793
+ ];
794
+
795
+ const result = await managedProvider.sendMessage([
796
+ { role: "user", content: [{ type: "text", text: "Read /tmp/test" }] },
797
+ ]);
798
+
799
+ expect(result.content).toHaveLength(1);
800
+ expect(result.content[0]).toEqual({
801
+ type: "tool_use",
802
+ id: "call_managed",
803
+ name: "file_read",
804
+ input: { path: "/tmp/test" },
805
+ });
806
+ });
729
807
  });
@@ -71,7 +71,8 @@ const TEST_PRINCIPAL_ID = "test-principal-id";
71
71
 
72
72
  function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
73
73
  return {
74
- externalUserId: "guardian-1",
74
+ actorPrincipalId: TEST_PRINCIPAL_ID,
75
+ actorExternalUserId: "guardian-1",
75
76
  channel: "telegram",
76
77
  guardianPrincipalId: TEST_PRINCIPAL_ID,
77
78
  ...overrides,
@@ -80,7 +81,8 @@ function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
80
81
 
81
82
  function trustedActor(overrides: Partial<ActorContext> = {}): ActorContext {
82
83
  return {
83
- externalUserId: undefined,
84
+ actorPrincipalId: TEST_PRINCIPAL_ID,
85
+ actorExternalUserId: undefined,
84
86
  channel: "desktop",
85
87
  guardianPrincipalId: TEST_PRINCIPAL_ID,
86
88
  ...overrides,
@@ -254,7 +256,7 @@ describe("applyCanonicalGuardianDecision", () => {
254
256
 
255
257
  expect(result.applied).toBe(true);
256
258
  if (!result.applied) return;
257
- // No grant minted because trusted actor has no externalUserId
259
+ // No grant minted because trusted actor has no actorExternalUserId
258
260
  expect(result.grantMinted).toBe(false);
259
261
  });
260
262
 
@@ -94,7 +94,8 @@ const TEST_PRINCIPAL_ID = "test-principal-id";
94
94
 
95
95
  function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
96
96
  return {
97
- externalUserId: "guardian-1",
97
+ actorPrincipalId: TEST_PRINCIPAL_ID,
98
+ actorExternalUserId: "guardian-1",
98
99
  channel: "telegram",
99
100
  guardianPrincipalId: TEST_PRINCIPAL_ID,
100
101
  ...overrides,
@@ -103,7 +104,8 @@ function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
103
104
 
104
105
  function trustedActor(overrides: Partial<ActorContext> = {}): ActorContext {
105
106
  return {
106
- externalUserId: undefined,
107
+ actorPrincipalId: TEST_PRINCIPAL_ID,
108
+ actorExternalUserId: undefined,
107
109
  channel: "desktop",
108
110
  guardianPrincipalId: TEST_PRINCIPAL_ID,
109
111
  ...overrides,
@@ -1212,13 +1214,13 @@ describe("routing invariant: destination hints do not bypass tool_approval princ
1212
1214
  });
1213
1215
 
1214
1216
  // No pendingRequestIds passed — identity-based fallback uses
1215
- // actor.externalUserId which does not match any request's
1217
+ // actor.actorExternalUserId which does not match any request's
1216
1218
  // guardianExternalUserId (since it's null).
1217
1219
  const result = await routeGuardianReply(
1218
1220
  replyCtx({
1219
1221
  messageText: "approve",
1220
1222
  channel: "telegram",
1221
- actor: guardianActor({ externalUserId: "guardian-tg-user" }),
1223
+ actor: guardianActor({ actorExternalUserId: "guardian-tg-user" }),
1222
1224
  conversationId: "conv-guardian-chat",
1223
1225
  // pendingRequestIds: undefined — no delivery hints
1224
1226
  approvalConversationGenerator: undefined,
@@ -101,10 +101,6 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
101
101
  type: "usage_request",
102
102
  sessionId: "sess-001",
103
103
  },
104
- sandbox_set: {
105
- type: "sandbox_set",
106
- enabled: true,
107
- },
108
104
  cu_session_create: {
109
105
  type: "cu_session_create",
110
106
  sessionId: "cu-sess-001",