@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
@@ -26,6 +26,7 @@ import type { TtsProvider, TtsProviderId } from "../tts/types.js";
26
26
  import { getLogger } from "../util/logger.js";
27
27
  import { createStreamingEntry } from "./audio-store.js";
28
28
  import {
29
+ getEndCallListenWindowMs,
29
30
  getMaxCallDurationMs,
30
31
  getSilenceTimeoutMs,
31
32
  getUserConsultationTimeoutMs,
@@ -37,6 +38,7 @@ import {
37
38
  registerCallController,
38
39
  unregisterCallController,
39
40
  } from "./call-state.js";
41
+ import { isTerminalState } from "./call-state-machine.js";
40
42
  import {
41
43
  createPendingQuestion,
42
44
  expirePendingQuestions,
@@ -93,6 +95,7 @@ export class CallController {
93
95
  private currentTurnPromise: Promise<void> | null = null;
94
96
  private destroyed = false;
95
97
  private silenceTimer: ReturnType<typeof setTimeout> | null = null;
98
+ private endCallListenTimer: ReturnType<typeof setTimeout> | null = null;
96
99
  private durationTimer: ReturnType<typeof setTimeout> | null = null;
97
100
  private durationWarningTimer: ReturnType<typeof setTimeout> | null = null;
98
101
  /**
@@ -244,6 +247,8 @@ export class CallController {
244
247
  transcript: string,
245
248
  speaker?: PromptSpeakerContext,
246
249
  ): Promise<void> {
250
+ this.cancelPendingEndCall();
251
+
247
252
  const interruptedInFlight =
248
253
  this.state === "processing" || this.state === "speaking";
249
254
  // If we're already processing or speaking, abort the in-flight generation
@@ -295,6 +300,8 @@ export class CallController {
295
300
  return false;
296
301
  }
297
302
 
303
+ this.cancelPendingEndCall();
304
+
298
305
  // Clear the consultation timeout and record
299
306
  clearTimeout(this.pendingGuardianInput.timer);
300
307
  this.pendingGuardianInput = null;
@@ -326,6 +333,8 @@ export class CallController {
326
333
  * position once the current turn completes.
327
334
  */
328
335
  async handleUserInstruction(instructionText: string): Promise<void> {
336
+ this.cancelPendingEndCall();
337
+
329
338
  recordCallEvent(this.callSessionId, "user_instruction_relayed", {
330
339
  instruction: instructionText,
331
340
  });
@@ -408,6 +417,7 @@ export class CallController {
408
417
  destroy(): void {
409
418
  this.destroyed = true;
410
419
  if (this.silenceTimer) clearTimeout(this.silenceTimer);
420
+ if (this.endCallListenTimer) clearTimeout(this.endCallListenTimer);
411
421
  if (this.durationTimer) clearTimeout(this.durationTimer);
412
422
  if (this.durationWarningTimer) clearTimeout(this.durationWarningTimer);
413
423
  if (this.pendingGuardianInput) {
@@ -419,6 +429,7 @@ export class CallController {
419
429
  this.durationEndTimer = null;
420
430
  }
421
431
  this.pendingInstructions = [];
432
+ this.endCallListenTimer = null;
422
433
  this.llmRunVersion++;
423
434
  this.abortCurrentTurn();
424
435
  if (this.activeSynthesisAbort) {
@@ -1075,73 +1086,7 @@ export class CallController {
1075
1086
 
1076
1087
  // Check for END_CALL marker
1077
1088
  if (responseText.includes(END_CALL_MARKER)) {
1078
- // Clear any pending consultation before completing the call.
1079
- // Without this, the consultation timeout can fire on an already-ended
1080
- // call, overwriting 'completed' status back to 'in_progress' and
1081
- // starting a new LLM turn on a dead conversation. Similarly, a late
1082
- // handleUserAnswer could be accepted since pendingGuardianInput is
1083
- // still non-null.
1084
- if (this.pendingGuardianInput) {
1085
- clearTimeout(this.pendingGuardianInput.timer);
1086
-
1087
- // Expire store-side consultation records so clients don't observe
1088
- // a completed call with a dangling pendingQuestion, and guardian
1089
- // replies are cleanly rejected instead of hitting answerCall failures.
1090
- expirePendingQuestions(this.callSessionId);
1091
- const previousRequest = getPendingCanonicalRequestByCallSessionId(
1092
- this.callSessionId,
1093
- );
1094
- if (previousRequest) {
1095
- expireCanonicalGuardianRequest(previousRequest.id);
1096
- }
1097
-
1098
- this.pendingGuardianInput = null;
1099
- }
1100
-
1101
- const currentSession = getCallSession(this.callSessionId);
1102
- const shouldNotifyCompletion = currentSession
1103
- ? currentSession.status !== "completed" &&
1104
- currentSession.status !== "failed" &&
1105
- currentSession.status !== "cancelled"
1106
- : false;
1107
-
1108
- this.transport.endSession("Call completed");
1109
- updateCallSession(this.callSessionId, {
1110
- status: "completed",
1111
- endedAt: Date.now(),
1112
- });
1113
- recordCallEvent(this.callSessionId, "call_ended", {
1114
- reason: "completed",
1115
- });
1116
-
1117
- // Notify the voice conversation
1118
- if (shouldNotifyCompletion && currentSession) {
1119
- finalizeCall(this.callSessionId, currentSession.conversationId);
1120
- }
1121
-
1122
- // Post a pointer message in the initiating conversation
1123
- if (currentSession?.initiatedFromConversationId) {
1124
- const durationMs = currentSession.startedAt
1125
- ? Date.now() - currentSession.startedAt
1126
- : 0;
1127
- addPointerMessage(
1128
- currentSession.initiatedFromConversationId,
1129
- "completed",
1130
- currentSession.toNumber,
1131
- {
1132
- duration: durationMs > 0 ? formatDuration(durationMs) : undefined,
1133
- },
1134
- ).catch((err) => {
1135
- log.warn(
1136
- {
1137
- conversationId: currentSession.initiatedFromConversationId,
1138
- err,
1139
- },
1140
- "Skipping pointer write — origin conversation may no longer exist",
1141
- );
1142
- });
1143
- }
1144
- this.state = "idle";
1089
+ this.scheduleEndCallAfterListenWindow();
1145
1090
  return;
1146
1091
  }
1147
1092
 
@@ -1153,6 +1098,124 @@ export class CallController {
1153
1098
  this.flushPendingInstructions();
1154
1099
  }
1155
1100
 
1101
+ private scheduleEndCallAfterListenWindow(): void {
1102
+ const currentSession = getCallSession(this.callSessionId);
1103
+ if (currentSession && isTerminalState(currentSession.status)) {
1104
+ this.state = "idle";
1105
+ this.currentTurnHandle = null;
1106
+ return;
1107
+ }
1108
+
1109
+ const clearedPendingGuardianInput =
1110
+ this.clearPendingGuardianInputForCallEnd();
1111
+ this.state = "idle";
1112
+ this.currentTurnHandle = null;
1113
+
1114
+ if (this.endCallListenTimer) {
1115
+ clearTimeout(this.endCallListenTimer);
1116
+ this.endCallListenTimer = null;
1117
+ }
1118
+
1119
+ const listenWindowMs = getEndCallListenWindowMs();
1120
+ const callContinues =
1121
+ this.pendingInstructions.length > 0 || listenWindowMs > 0;
1122
+ if (clearedPendingGuardianInput && callContinues) {
1123
+ updateCallSession(this.callSessionId, { status: "in_progress" });
1124
+ }
1125
+
1126
+ if (this.pendingInstructions.length > 0) {
1127
+ this.flushPendingInstructions();
1128
+ return;
1129
+ }
1130
+
1131
+ if (listenWindowMs <= 0) {
1132
+ this.completeCallFromEndMarker();
1133
+ return;
1134
+ }
1135
+
1136
+ this.resetSilenceTimer();
1137
+ this.endCallListenTimer = setTimeout(() => {
1138
+ this.endCallListenTimer = null;
1139
+ this.completeCallFromEndMarker();
1140
+ }, listenWindowMs);
1141
+ }
1142
+
1143
+ private cancelPendingEndCall(): void {
1144
+ if (!this.endCallListenTimer) return;
1145
+ clearTimeout(this.endCallListenTimer);
1146
+ this.endCallListenTimer = null;
1147
+ }
1148
+
1149
+ private clearPendingGuardianInputForCallEnd(): boolean {
1150
+ if (!this.pendingGuardianInput) return false;
1151
+
1152
+ clearTimeout(this.pendingGuardianInput.timer);
1153
+
1154
+ // Expire store-side consultation records so clients don't observe
1155
+ // a completed call with a dangling pendingQuestion, and guardian
1156
+ // replies are cleanly rejected instead of hitting answerCall failures.
1157
+ expirePendingQuestions(this.callSessionId);
1158
+ const previousRequest = getPendingCanonicalRequestByCallSessionId(
1159
+ this.callSessionId,
1160
+ );
1161
+ if (previousRequest) {
1162
+ expireCanonicalGuardianRequest(previousRequest.id);
1163
+ }
1164
+
1165
+ this.pendingGuardianInput = null;
1166
+ return true;
1167
+ }
1168
+
1169
+ private completeCallFromEndMarker(): void {
1170
+ if (this.destroyed) return;
1171
+
1172
+ const currentSession = getCallSession(this.callSessionId);
1173
+ if (currentSession && isTerminalState(currentSession.status)) {
1174
+ this.state = "idle";
1175
+ return;
1176
+ }
1177
+
1178
+ const shouldNotifyCompletion = !!currentSession;
1179
+
1180
+ this.transport.endSession("Call completed");
1181
+ updateCallSession(this.callSessionId, {
1182
+ status: "completed",
1183
+ endedAt: Date.now(),
1184
+ });
1185
+ recordCallEvent(this.callSessionId, "call_ended", {
1186
+ reason: "completed",
1187
+ });
1188
+
1189
+ // Notify the voice conversation
1190
+ if (shouldNotifyCompletion && currentSession) {
1191
+ finalizeCall(this.callSessionId, currentSession.conversationId);
1192
+ }
1193
+
1194
+ // Post a pointer message in the initiating conversation
1195
+ if (currentSession?.initiatedFromConversationId) {
1196
+ const durationMs = currentSession.startedAt
1197
+ ? Date.now() - currentSession.startedAt
1198
+ : 0;
1199
+ addPointerMessage(
1200
+ currentSession.initiatedFromConversationId,
1201
+ "completed",
1202
+ currentSession.toNumber,
1203
+ {
1204
+ duration: durationMs > 0 ? formatDuration(durationMs) : undefined,
1205
+ },
1206
+ ).catch((err) => {
1207
+ log.warn(
1208
+ {
1209
+ conversationId: currentSession.initiatedFromConversationId,
1210
+ err,
1211
+ },
1212
+ "Skipping pointer write — origin conversation may no longer exist",
1213
+ );
1214
+ });
1215
+ }
1216
+ this.state = "idle";
1217
+ }
1218
+
1156
1219
  private isExpectedAbortError(err: unknown): boolean {
1157
1220
  if (!(err instanceof Error)) return false;
1158
1221
  return err.name === "AbortError" || err.name === "APIUserAbortError";
@@ -66,6 +66,8 @@ import {
66
66
  } from "./speaker-identification.js";
67
67
 
68
68
  const log = getLogger("relay-server");
69
+ const UUID_SHAPED_NAME =
70
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
69
71
 
70
72
  // ── ConversationRelay message types ──────────────────────────────────
71
73
 
@@ -1615,7 +1617,11 @@ export class RelayConnection {
1615
1617
  private resolveAssistantLabel(): string | null {
1616
1618
  try {
1617
1619
  const name = getAssistantName();
1618
- return name?.trim() || null;
1620
+ const trimmedName = name?.trim();
1621
+ if (!trimmedName || UUID_SHAPED_NAME.test(trimmedName)) {
1622
+ return null;
1623
+ }
1624
+ return trimmedName;
1619
1625
  } catch {
1620
1626
  return null;
1621
1627
  }
@@ -233,7 +233,7 @@ function buildVoiceCallControlPrompt(opts: {
233
233
  );
234
234
  } else {
235
235
  lines.push(
236
- '7. If the latest user turn is "(call connected — deliver opening greeting)", this is an inbound call you are answering (not a call you initiated). Greet the caller warmly and ask how you can help. Introduce yourself once at the start using your assistant name if you know it (for example: "Hey there, this is Ava, Sam\'s assistant. How can I help?"). If your assistant name is not known, skip the name and just identify yourself as the guardian\'s assistant. Do NOT say "I\'m calling" or "I\'m calling on behalf of". Vary the wording; do not use a fixed template.',
236
+ '7. If the latest user turn is "(call connected — deliver opening greeting)", this is an inbound call you are answering (not a call you initiated). Greet the caller warmly and ask how you can help. Introduce yourself once at the start using your assistant name if you know it (for example: "Hey there, this is Ava, Sam\'s assistant. How can I help?"). If your assistant name is not known, skip the name and just identify yourself as the guardian\'s assistant. Never use a UUID-shaped internal assistant ID as your spoken name. Do NOT say "I\'m calling" or "I\'m calling on behalf of". Vary the wording; do not use a fixed template.',
237
237
  );
238
238
  }
239
239
  lines.push(
@@ -25,9 +25,9 @@
25
25
  * violation lists). Does not mutate the workspace.
26
26
  *
27
27
  * Lives alongside the existing v1 `memory` command rather than replacing it
28
- * so flipping `memory-v2-enabled` back to off fully re-engages the v1
29
- * pipeline. While the flag is on, v1 graph extraction/maintenance and PKB
30
- * filing are suppressed; v1 data stays in place but stops being updated.
28
+ * so flipping `memory.v2.enabled` back to false fully re-engages the v1
29
+ * pipeline. While v2 is on, v1 graph extraction/maintenance and PKB filing
30
+ * are suppressed; v1 data stays in place but stops being updated.
31
31
  */
32
32
 
33
33
  import type { Command } from "commander";
@@ -172,9 +172,9 @@ export function registerMemoryV2Command(program: Command): void {
172
172
  `
173
173
  The v2 subsystem replaces the v1 graph + PKB with prose concept pages,
174
174
  directed edges stored in each page's frontmatter, and activation-based
175
- retrieval. v2 stays gated behind the memory-v2-enabled feature flag
175
+ retrieval. v2 is gated behind the memory.v2.enabled config field
176
176
  these subcommands remain useful operator tools regardless of whether
177
- the flag is on.
177
+ v2 is currently active.
178
178
 
179
179
  Subcommands fall into three groups:
180
180
 
@@ -273,8 +273,8 @@ prefix). Useful after editing a skill's SKILL.md, after a feature-flag flip
273
273
  changes the enabled-skill set, or to recover corrupted skill embeddings.
274
274
 
275
275
  Unlike 'reembed' (concept pages), this runs synchronously inside the
276
- daemon — the command returns only once the seed completes. Requires both
277
- the memory-v2-enabled feature flag and memory.v2.enabled to be on.
276
+ daemon — the command returns only once the seed completes. Requires
277
+ memory.v2.enabled to be true.
278
278
 
279
279
  Examples:
280
280
  $ assistant memory v2 reembed-skills`,
@@ -395,106 +395,6 @@ describe("assistant oauth connect", () => {
395
395
  );
396
396
  });
397
397
 
398
- // -------------------------------------------------------------------------
399
- // BYO mode with --no-browser: prints auth URL (deferred)
400
- // -------------------------------------------------------------------------
401
-
402
- test("BYO mode with --no-browser: prints auth URL", async () => {
403
- mockGetProvider = () => ({
404
- provider: "google",
405
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
406
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
407
- tokenExchangeBodyFormat: "form",
408
- managedServiceConfigKey: null,
409
- });
410
- mockIsManagedMode = () => false;
411
-
412
- mockGetMostRecentAppByProvider = () => ({
413
- id: "app-1",
414
- clientId: "byo-client-id",
415
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
416
- provider: "google",
417
- createdAt: 0,
418
- updatedAt: 0,
419
- });
420
-
421
- mockOrchestrateOAuthConnect = async () => ({
422
- success: true,
423
- deferred: true,
424
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth?state=abc",
425
- state: "abc",
426
- service: "google",
427
- });
428
-
429
- const { exitCode, stdout } = await runCommand([
430
- "connect",
431
- "google",
432
- "--no-browser",
433
- "--json",
434
- ]);
435
- expect(exitCode).toBe(0);
436
- const parsed = JSON.parse(stdout);
437
- expect(parsed.ok).toBe(true);
438
- expect(parsed.deferred).toBe(true);
439
- expect(parsed.authUrl).toBe(
440
- "https://accounts.google.com/o/oauth2/v2/auth?state=abc",
441
- );
442
- expect(parsed.service).toBe("google");
443
- });
444
-
445
- // -------------------------------------------------------------------------
446
- // BYO mode default: orchestrator called with isInteractive true
447
- // -------------------------------------------------------------------------
448
-
449
- test("BYO mode default calls orchestrator with isInteractive: true", async () => {
450
- mockGetProvider = () => ({
451
- provider: "google",
452
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
453
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
454
- tokenExchangeBodyFormat: "form",
455
- managedServiceConfigKey: null,
456
- });
457
- mockIsManagedMode = () => false;
458
-
459
- mockGetAppByProviderAndClientId = () => ({
460
- id: "app-1",
461
- clientId: "test-id",
462
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
463
- provider: "google",
464
- createdAt: 0,
465
- updatedAt: 0,
466
- });
467
-
468
- let capturedOpts: Record<string, unknown> | undefined;
469
- mockOrchestrateOAuthConnect = async (opts) => {
470
- capturedOpts = opts;
471
- return {
472
- success: true,
473
- deferred: false,
474
- grantedScopes: ["email"],
475
- accountInfo: "user@example.com",
476
- };
477
- };
478
-
479
- const { exitCode, stdout } = await runCommand([
480
- "connect",
481
- "google",
482
- "--client-id",
483
- "test-id",
484
- "--json",
485
- ]);
486
- expect(exitCode).toBe(0);
487
- expect(capturedOpts).toBeDefined();
488
- expect(capturedOpts!.isInteractive).toBe(true);
489
- // openUrl should be provided by default (browser opens automatically)
490
- expect(typeof capturedOpts!.openUrl).toBe("function");
491
-
492
- const parsed = JSON.parse(stdout);
493
- expect(parsed.ok).toBe(true);
494
- expect(parsed.grantedScopes).toEqual(["email"]);
495
- expect(parsed.accountInfo).toBe("user@example.com");
496
- });
497
-
498
398
  // -------------------------------------------------------------------------
499
399
  // BYO missing app: error with hint
500
400
  // -------------------------------------------------------------------------
@@ -590,96 +490,6 @@ describe("assistant oauth connect", () => {
590
490
  );
591
491
  });
592
492
 
593
- // -------------------------------------------------------------------------
594
- // JSON output format for deferred case (BYO)
595
- // -------------------------------------------------------------------------
596
-
597
- test("JSON output for deferred case includes ok, deferred, authUrl, service", async () => {
598
- mockGetProvider = () => ({
599
- provider: "slack",
600
- authorizeUrl: "https://slack.com/oauth/v2/authorize",
601
- tokenExchangeUrl: "https://slack.com/api/oauth.v2.access",
602
- tokenExchangeBodyFormat: "form",
603
- managedServiceConfigKey: null,
604
- });
605
- mockIsManagedMode = () => false;
606
-
607
- mockGetMostRecentAppByProvider = () => ({
608
- id: "app-slack",
609
- clientId: "slack-client-id",
610
- clientSecretCredentialPath: "oauth_app/app-slack/client_secret",
611
- provider: "slack",
612
- createdAt: 0,
613
- updatedAt: 0,
614
- });
615
-
616
- mockOrchestrateOAuthConnect = async () => ({
617
- success: true,
618
- deferred: true,
619
- authorizeUrl: "https://slack.com/oauth/v2/authorize?state=xyz",
620
- state: "xyz",
621
- service: "slack",
622
- });
623
-
624
- const { exitCode, stdout } = await runCommand([
625
- "connect",
626
- "slack",
627
- "--no-browser",
628
- "--json",
629
- ]);
630
- expect(exitCode).toBe(0);
631
- const parsed = JSON.parse(stdout);
632
- expect(parsed).toHaveProperty("ok", true);
633
- expect(parsed).toHaveProperty("deferred", true);
634
- expect(parsed).toHaveProperty("authUrl");
635
- expect(parsed).toHaveProperty("service", "slack");
636
- });
637
-
638
- // -------------------------------------------------------------------------
639
- // JSON output format for completed case (BYO)
640
- // -------------------------------------------------------------------------
641
-
642
- test("JSON output for completed case includes ok, grantedScopes, accountInfo", async () => {
643
- mockGetProvider = () => ({
644
- provider: "google",
645
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
646
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
647
- tokenExchangeBodyFormat: "form",
648
- managedServiceConfigKey: null,
649
- });
650
- mockIsManagedMode = () => false;
651
-
652
- mockGetMostRecentAppByProvider = () => ({
653
- id: "app-1",
654
- clientId: "completed-client-id",
655
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
656
- provider: "google",
657
- createdAt: 0,
658
- updatedAt: 0,
659
- });
660
-
661
- mockOrchestrateOAuthConnect = async () => ({
662
- success: true,
663
- deferred: false,
664
- grantedScopes: ["email", "profile"],
665
- accountInfo: "test@gmail.com",
666
- });
667
-
668
- const { exitCode, stdout } = await runCommand([
669
- "connect",
670
- "google",
671
- "--json",
672
- ]);
673
- expect(exitCode).toBe(0);
674
- const parsed = JSON.parse(stdout);
675
- expect(parsed).toHaveProperty("ok", true);
676
- expect(parsed).toHaveProperty("grantedScopes");
677
- expect(parsed.grantedScopes).toEqual(["email", "profile"]);
678
- expect(parsed).toHaveProperty("accountInfo", "test@gmail.com");
679
- // Should NOT have deferred
680
- expect(parsed.deferred).toBeUndefined();
681
- });
682
-
683
493
  // -------------------------------------------------------------------------
684
494
  // BYO mode: client_secret required but missing
685
495
  // -------------------------------------------------------------------------
@@ -911,31 +721,6 @@ describe("assistant oauth connect", () => {
911
721
  expect(mockOpenInBrowserCalls.length).toBe(0);
912
722
  });
913
723
 
914
- test("IPC returns ok:false → falls back to in-process orchestrateOAuthConnect", async () => {
915
- // Default mockCliIpcCallFn already returns ok: false
916
- let orchestratorCalled = false;
917
- mockOrchestrateOAuthConnect = async () => {
918
- orchestratorCalled = true;
919
- return {
920
- success: true,
921
- deferred: false,
922
- grantedScopes: ["email"],
923
- accountInfo: "fallback@example.com",
924
- };
925
- };
926
-
927
- const { exitCode, stdout } = await runCommand([
928
- "connect",
929
- "google",
930
- "--json",
931
- ]);
932
- expect(exitCode).toBe(0);
933
- expect(orchestratorCalled).toBe(true);
934
- const parsed = JSON.parse(stdout);
935
- expect(parsed.ok).toBe(true);
936
- expect(parsed.accountInfo).toBe("fallback@example.com");
937
- });
938
-
939
724
  test("IPC returns ok:false with statusCode → surfaces daemon error, does NOT fall back", async () => {
940
725
  // Daemon was reachable but returned an error (e.g. 500)
941
726
  mockCliIpcCallFn = async (method) => {
@@ -1159,43 +944,4 @@ describe("assistant oauth connect", () => {
1159
944
  expect(parsed.accountInfo).toBe("gw-user@example.com");
1160
945
  });
1161
946
  });
1162
-
1163
- // -------------------------------------------------------------------------
1164
- // Orchestrator error propagation
1165
- // -------------------------------------------------------------------------
1166
-
1167
- test("BYO mode: orchestrator error propagates correctly", async () => {
1168
- mockGetProvider = () => ({
1169
- provider: "google",
1170
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1171
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1172
- tokenExchangeBodyFormat: "form",
1173
- managedServiceConfigKey: null,
1174
- });
1175
- mockIsManagedMode = () => false;
1176
-
1177
- mockGetMostRecentAppByProvider = () => ({
1178
- id: "app-1",
1179
- clientId: "client-id",
1180
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
1181
- provider: "google",
1182
- createdAt: 0,
1183
- updatedAt: 0,
1184
- });
1185
-
1186
- mockOrchestrateOAuthConnect = async () => ({
1187
- success: false,
1188
- error: "Token exchange failed: invalid_grant",
1189
- });
1190
-
1191
- const { exitCode, stdout } = await runCommand([
1192
- "connect",
1193
- "google",
1194
- "--json",
1195
- ]);
1196
- expect(exitCode).toBe(1);
1197
- const parsed = JSON.parse(stdout);
1198
- expect(parsed.ok).toBe(false);
1199
- expect(parsed.error).toBe("Token exchange failed: invalid_grant");
1200
- });
1201
947
  });
@@ -4,7 +4,6 @@ import type { Command } from "commander";
4
4
 
5
5
  import { getIsContainerized } from "../../../config/env-registry.js";
6
6
  import { cliIpcCall } from "../../../ipc/cli-client.js";
7
- import { orchestrateOAuthConnect } from "../../../oauth/connect-orchestrator.js";
8
7
  import {
9
8
  getAppByProviderAndClientId,
10
9
  getMostRecentAppByProvider,
@@ -514,57 +513,16 @@ Examples:
514
513
  return;
515
514
  }
516
515
 
517
- // IPC unavailable (daemon unreachable, older daemon without this route, socket missing).
518
- // Fall through to the existing in-process flow. This still carries the heap-split bug
519
- // for gateway transport, but if the daemon is unreachable we have a worse problem;
520
- // the fallback preserves existing behavior as a regression guard.
521
- // e. Call the orchestrator (in-process fallback)
522
- const result = await orchestrateOAuthConnect({
523
- service: provider,
524
- clientId,
525
- clientSecret,
526
- callbackTransport: opts.callbackTransport,
527
- isInteractive: opts.browser !== false,
528
- openUrl: opts.browser !== false ? openInHostBrowser : undefined,
529
- ...(opts.scopes ? { requestedScopes: opts.scopes } : {}),
530
- });
531
-
532
- // f. Handle results
533
- if (!result.success) {
534
- writeError(result.error ?? "OAuth connect failed");
535
- return;
536
- }
537
-
538
- if (result.deferred) {
539
- if (jsonMode) {
540
- writeOutput(cmd, {
541
- ok: true,
542
- deferred: true,
543
- // Wire key stays `authUrl` for backward compatibility with
544
- // existing CLI script consumers; the internal field on
545
- // `result` is `authorizeUrl`.
546
- authUrl: result.authorizeUrl,
547
- service: result.service,
548
- });
549
- } else {
550
- process.stdout.write(
551
- `\nAuthorize with ${provider}:\n\n${result.authorizeUrl}\n\nThe connection will complete automatically once you authorize.\n`,
552
- );
553
- }
554
- return;
555
- }
556
-
557
- // Interactive mode completed
558
- if (jsonMode) {
559
- writeOutput(cmd, {
560
- ok: true,
561
- grantedScopes: result.grantedScopes,
562
- accountInfo: result.accountInfo,
563
- });
564
- } else {
565
- const msg = `Connected to ${provider}${result.accountInfo ? ` as ${result.accountInfo}` : ""}`;
566
- process.stdout.write(msg + "\n");
567
- }
516
+ // IPC unavailable: the assistant must be running for OAuth connect. The
517
+ // gateway-routed callback lands in the assistant's process, and any tokens
518
+ // acquired need the assistant to store and use them so an unreachable
519
+ // assistant is a fatal precondition. Surface a clear error and exit 1.
520
+ writeError(
521
+ startResult.error
522
+ ? `Could not reach the assistant: ${startResult.error}. Is the assistant running?`
523
+ : "Could not reach the assistant. Is the assistant running?",
524
+ );
525
+ return;
568
526
  }
569
527
  } catch (err) {
570
528
  const message = err instanceof Error ? err.message : String(err);