@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.
- package/ARCHITECTURE.md +29 -28
- package/Dockerfile +1 -0
- package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
- package/bun.lock +3 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/openapi.yaml +22 -4
- package/package.json +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +291 -0
- package/src/__tests__/approval-cascade.test.ts +8 -16
- package/src/__tests__/approval-routes-http.test.ts +6 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
- package/src/__tests__/call-constants.test.ts +10 -1
- package/src/__tests__/call-controller.test.ts +127 -0
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
- package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -26
- package/src/__tests__/context-search-pkb-source.test.ts +12 -6
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +3 -3
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -6
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
- package/src/__tests__/filing-service.test.ts +2 -19
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
- package/src/__tests__/injector-chain.test.ts +24 -16
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
- package/src/__tests__/notification-decision-fallback.test.ts +91 -0
- package/src/__tests__/notification-decision-strategy.test.ts +22 -0
- package/src/__tests__/oauth-cli.test.ts +121 -0
- package/src/__tests__/relay-server.test.ts +46 -2
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
- package/src/__tests__/secret-response-routing.test.ts +7 -5
- package/src/__tests__/server-history-render.test.ts +82 -0
- package/src/__tests__/skill-include-graph.test.ts +31 -0
- package/src/__tests__/skill-load-tool.test.ts +44 -16
- package/src/__tests__/skills.test.ts +39 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
- package/src/__tests__/tool-executor.test.ts +155 -0
- package/src/__tests__/voice-session-bridge.test.ts +3 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
- package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
- package/src/agent/loop.ts +11 -0
- package/src/approvals/guardian-decision-primitive.ts +0 -13
- package/src/approvals/guardian-request-resolvers.ts +4 -32
- package/src/calls/call-constants.ts +5 -8
- package/src/calls/call-controller.ts +130 -67
- package/src/calls/relay-server.ts +7 -1
- package/src/calls/voice-session-bridge.ts +1 -1
- package/src/cli/commands/memory-v2.ts +7 -7
- package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
- package/src/cli/commands/oauth/connect.ts +10 -52
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/loader.ts +72 -19
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
- package/src/daemon/conversation-agent-loop.ts +13 -10
- package/src/daemon/conversation-lifecycle.ts +22 -8
- package/src/daemon/conversation-surfaces.ts +16 -14
- package/src/daemon/conversation-tool-setup.ts +9 -5
- package/src/daemon/conversation.ts +1 -1
- package/src/daemon/handlers/shared.ts +26 -0
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-browser-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +88 -73
- package/src/daemon/memory-v2-startup.ts +55 -14
- package/src/daemon/message-types/messages.ts +19 -1
- package/src/documents/document-store.ts +35 -1
- package/src/filing/filing-service.ts +2 -3
- package/src/heartbeat/heartbeat-service.ts +1 -1
- package/src/ipc/assistant-server.ts +93 -36
- package/src/ipc/skill-server.ts +99 -42
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
- package/src/memory/context-search/sources/memory-v2.ts +1 -17
- package/src/memory/context-search/sources/memory.ts +2 -2
- package/src/memory/context-search/sources/pkb.ts +2 -3
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
- package/src/memory/graph/conversation-graph-memory.ts +32 -9
- package/src/memory/graph/graph-search.test.ts +6 -5
- package/src/memory/graph/graph-search.ts +3 -4
- package/src/memory/graph/retriever.test.ts +12 -7
- package/src/memory/graph/retriever.ts +4 -5
- package/src/memory/graph/tool-handlers.ts +3 -4
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/indexer.ts +1 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
- package/src/memory/jobs/embed-concept-page.ts +223 -87
- package/src/memory/jobs-worker.ts +8 -4
- package/src/memory/pkb/pkb-search.test.ts +6 -5
- package/src/memory/pkb/pkb-search.ts +4 -5
- package/src/memory/qdrant-client.ts +3 -0
- package/src/memory/search/semantic.ts +4 -5
- package/src/memory/v2/__tests__/activation.test.ts +35 -5
- package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
- package/src/memory/v2/__tests__/injection.test.ts +140 -23
- package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
- package/src/memory/v2/__tests__/sim.test.ts +118 -7
- package/src/memory/v2/__tests__/static-context.test.ts +1 -13
- package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
- package/src/memory/v2/consolidation-job.ts +7 -8
- package/src/memory/v2/injection.ts +32 -12
- package/src/memory/v2/page-store.ts +39 -0
- package/src/memory/v2/prompts/consolidation.ts +5 -0
- package/src/memory/v2/qdrant.ts +209 -48
- package/src/memory/v2/sim.ts +67 -26
- package/src/memory/v2/static-context.ts +4 -8
- package/src/memory/v2/sweep-job.ts +5 -6
- package/src/memory/v2/types.ts +7 -0
- package/src/notifications/copy-composer.ts +46 -12
- package/src/notifications/decision-engine.ts +46 -0
- package/src/permissions/gateway-threshold-reader.ts +116 -8
- package/src/permissions/prompter.ts +86 -96
- package/src/permissions/secret-prompter.ts +31 -31
- package/src/plugins/defaults/injectors.ts +1 -2
- package/src/proactive-artifact/job.test.ts +51 -4
- package/src/proactive-artifact/job.ts +16 -2
- package/src/proactive-artifact/message-copy.ts +18 -1
- package/src/prompts/templates/SOUL.md +13 -28
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-approvals.ts +3 -2
- package/src/runtime/guardian-reply-router.ts +0 -10
- package/src/runtime/pending-interactions.ts +19 -15
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
- package/src/runtime/routes/approval-routes.ts +7 -3
- package/src/runtime/routes/consolidation-routes.ts +8 -9
- package/src/runtime/routes/conversation-query-routes.ts +44 -1
- package/src/runtime/routes/debug-bash-routes.ts +2 -0
- package/src/runtime/routes/filing-routes.ts +2 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
- package/src/runtime/routes/memory-item-routes.test.ts +3 -9
- package/src/runtime/routes/memory-item-routes.ts +5 -6
- package/src/runtime/routes/memory-v2-routes.ts +103 -17
- package/src/skills/include-graph.ts +35 -13
- package/src/tools/document/document-tool.ts +20 -0
- package/src/tools/executor.ts +18 -2
- package/src/tools/memory/register.test.ts +7 -5
- package/src/tools/permission-checker.ts +15 -0
- package/src/tools/skills/load.ts +24 -20
- package/src/tools/tool-name-aliases.ts +19 -0
- package/src/tools/types.ts +19 -1
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
- package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
29
|
-
* pipeline. While
|
|
30
|
-
*
|
|
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
|
|
175
|
+
retrieval. v2 is gated behind the memory.v2.enabled config field —
|
|
176
176
|
these subcommands remain useful operator tools regardless of whether
|
|
177
|
-
|
|
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
|
|
277
|
-
|
|
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
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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);
|