@vellumai/assistant 0.3.14 → 0.3.16
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 +142 -0
- package/Dockerfile +2 -2
- package/README.md +5 -5
- package/docs/architecture/http-token-refresh.md +252 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +331 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +16 -14
- package/src/__tests__/checker.test.ts +24 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +358 -0
- package/src/__tests__/conversation-pairing.test.ts +24 -24
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-control-plane-policy.test.ts +1 -3
- package/src/__tests__/guardian-outbound-http.test.ts +202 -10
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +2 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +282 -0
- package/src/__tests__/notification-decision-strategy.test.ts +136 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -2
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent-fallback.test.ts +0 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -3
- package/src/__tests__/recording-intent.test.ts +3 -2
- package/src/__tests__/recording-state-machine.test.ts +337 -26
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +17 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +260 -0
- package/src/__tests__/update-template-contract.test.ts +29 -0
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +115 -34
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +4 -4
- package/src/cli/core-commands.ts +3 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +16 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +52 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin.ts +82 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +1 -0
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +164 -7
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +8 -8
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/index.ts +1 -1
- package/src/daemon/handlers/misc.ts +84 -6
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +107 -24
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract-inventory.json +5 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +306 -266
- package/src/daemon/recording-executor.ts +1 -1
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +6 -6
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +256 -23
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +6 -7
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +55 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +37 -567
- package/src/memory/channel-guardian-store.ts +66 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +4 -7
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +45 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +25 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +121 -0
- package/src/memory/guardian-action-store.ts +366 -3
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +520 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +8 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +3 -0
- package/src/memory/schema.ts +25 -7
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +1 -1
- package/src/notifications/broadcaster.ts +20 -2
- package/src/notifications/conversation-pairing.ts +3 -3
- package/src/notifications/decision-engine.ts +173 -8
- package/src/notifications/deliveries-store.ts +27 -8
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +234 -0
- package/src/notifications/types.ts +18 -0
- package/src/permissions/defaults.ts +11 -1
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +35 -15
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +140 -51
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +5 -4
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +82 -20
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +2 -2
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +272 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
// --- In-memory checkpoint store ---
|
|
7
|
+
const store = new Map<string, string>();
|
|
8
|
+
|
|
9
|
+
mock.module('../memory/checkpoints.js', () => ({
|
|
10
|
+
getMemoryCheckpoint: mock((key: string) => store.get(key) ?? null),
|
|
11
|
+
setMemoryCheckpoint: mock((key: string, value: string) => store.set(key, value)),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// --- Temp directory for workspace paths ---
|
|
15
|
+
let tempDir: string;
|
|
16
|
+
|
|
17
|
+
// Mock platform to avoid env-registry transitive imports.
|
|
18
|
+
// All needed exports are stubbed; getWorkspacePromptPath is the only one
|
|
19
|
+
// exercised by update-bulletin.ts.
|
|
20
|
+
mock.module('../util/platform.js', () => ({
|
|
21
|
+
getWorkspacePromptPath: mock((file: string) => join(tempDir, file)),
|
|
22
|
+
getWorkspaceDir: () => tempDir,
|
|
23
|
+
getRootDir: () => tempDir,
|
|
24
|
+
getDataDir: () => join(tempDir, 'data'),
|
|
25
|
+
getPlatformName: () => 'darwin',
|
|
26
|
+
isMacOS: () => false,
|
|
27
|
+
isLinux: () => false,
|
|
28
|
+
isWindows: () => false,
|
|
29
|
+
ensureDataDir: () => {},
|
|
30
|
+
getDbPath: () => '',
|
|
31
|
+
getLogPath: () => '',
|
|
32
|
+
getHistoryPath: () => '',
|
|
33
|
+
getHooksDir: () => '',
|
|
34
|
+
getSocketPath: () => '',
|
|
35
|
+
getSessionTokenPath: () => '',
|
|
36
|
+
getHttpTokenPath: () => '',
|
|
37
|
+
getPlatformTokenPath: () => '',
|
|
38
|
+
getPidPath: () => '',
|
|
39
|
+
getWorkspaceConfigPath: () => '',
|
|
40
|
+
getWorkspaceSkillsDir: () => '',
|
|
41
|
+
getWorkspaceHooksDir: () => '',
|
|
42
|
+
getIpcBlobDir: () => '',
|
|
43
|
+
getSandboxRootDir: () => '',
|
|
44
|
+
getSandboxWorkingDir: () => '',
|
|
45
|
+
getInterfacesDir: () => '',
|
|
46
|
+
getClipboardCommand: () => null,
|
|
47
|
+
readLockfile: () => null,
|
|
48
|
+
normalizeAssistantId: (id: string) => id,
|
|
49
|
+
writeLockfile: () => {},
|
|
50
|
+
readPlatformToken: () => null,
|
|
51
|
+
readSessionToken: () => null,
|
|
52
|
+
readHttpToken: () => null,
|
|
53
|
+
removeSocketFile: () => {},
|
|
54
|
+
getTCPPort: () => 8765,
|
|
55
|
+
isTCPEnabled: () => false,
|
|
56
|
+
getTCPHost: () => '127.0.0.1',
|
|
57
|
+
isIOSPairingEnabled: () => false,
|
|
58
|
+
migrateToDataLayout: () => {},
|
|
59
|
+
migratePath: () => {},
|
|
60
|
+
migrateToWorkspaceLayout: () => {},
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// Mock system-prompt to provide only stripCommentLines without pulling in
|
|
64
|
+
// the rest of the system-prompt transitive dependency tree.
|
|
65
|
+
mock.module('../config/system-prompt.js', () => {
|
|
66
|
+
// Inline a minimal implementation of stripCommentLines matching production behavior.
|
|
67
|
+
function stripCommentLines(content: string): string {
|
|
68
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
69
|
+
let openFenceChar: string | null = null;
|
|
70
|
+
const filtered = normalized.split('\n').filter((line) => {
|
|
71
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
72
|
+
if (fenceMatch) {
|
|
73
|
+
const char = fenceMatch[1][0];
|
|
74
|
+
if (!openFenceChar) {
|
|
75
|
+
openFenceChar = char;
|
|
76
|
+
} else if (char === openFenceChar) {
|
|
77
|
+
openFenceChar = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (openFenceChar) return true;
|
|
81
|
+
return !line.trimStart().startsWith('_');
|
|
82
|
+
});
|
|
83
|
+
return filtered
|
|
84
|
+
.join('\n')
|
|
85
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
86
|
+
.trim();
|
|
87
|
+
}
|
|
88
|
+
return { stripCommentLines };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
mock.module('../version.js', () => ({
|
|
92
|
+
APP_VERSION: '1.0.0',
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
const { syncUpdateBulletinOnStartup } = await import('../config/update-bulletin.js');
|
|
96
|
+
|
|
97
|
+
describe('syncUpdateBulletinOnStartup', () => {
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
store.clear();
|
|
100
|
+
tempDir = join(tmpdir(), `update-bulletin-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
101
|
+
mkdirSync(tempDir, { recursive: true });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('creates workspace file on first eligible run', () => {
|
|
109
|
+
const workspacePath = join(tempDir, 'UPDATES.md');
|
|
110
|
+
expect(existsSync(workspacePath)).toBe(false);
|
|
111
|
+
|
|
112
|
+
syncUpdateBulletinOnStartup();
|
|
113
|
+
|
|
114
|
+
expect(existsSync(workspacePath)).toBe(true);
|
|
115
|
+
const content = readFileSync(workspacePath, 'utf-8');
|
|
116
|
+
expect(content).toContain('<!-- vellum-update-release:1.0.0 -->');
|
|
117
|
+
expect(content).toContain("What's New");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('appends release block when workspace file exists without current marker', () => {
|
|
121
|
+
const workspacePath = join(tempDir, 'UPDATES.md');
|
|
122
|
+
const preExisting = '<!-- vellum-update-release:0.9.0 -->\nOld release notes.\n';
|
|
123
|
+
writeFileSync(workspacePath, preExisting, 'utf-8');
|
|
124
|
+
|
|
125
|
+
syncUpdateBulletinOnStartup();
|
|
126
|
+
|
|
127
|
+
const content = readFileSync(workspacePath, 'utf-8');
|
|
128
|
+
expect(content).toContain('<!-- vellum-update-release:0.9.0 -->');
|
|
129
|
+
expect(content).toContain('<!-- vellum-update-release:1.0.0 -->');
|
|
130
|
+
expect(content).toContain('Old release notes.');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('does not duplicate same marker on repeated runs', () => {
|
|
134
|
+
syncUpdateBulletinOnStartup();
|
|
135
|
+
const workspacePath = join(tempDir, 'UPDATES.md');
|
|
136
|
+
const afterFirst = readFileSync(workspacePath, 'utf-8');
|
|
137
|
+
|
|
138
|
+
syncUpdateBulletinOnStartup();
|
|
139
|
+
const afterSecond = readFileSync(workspacePath, 'utf-8');
|
|
140
|
+
|
|
141
|
+
expect(afterSecond).toBe(afterFirst);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('skips completed release', () => {
|
|
145
|
+
store.set('updates:completed_releases', JSON.stringify(['1.0.0']));
|
|
146
|
+
const workspacePath = join(tempDir, 'UPDATES.md');
|
|
147
|
+
|
|
148
|
+
syncUpdateBulletinOnStartup();
|
|
149
|
+
|
|
150
|
+
expect(existsSync(workspacePath)).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('adds current release to active set', () => {
|
|
154
|
+
syncUpdateBulletinOnStartup();
|
|
155
|
+
|
|
156
|
+
const raw = store.get('updates:active_releases');
|
|
157
|
+
expect(raw).toBeDefined();
|
|
158
|
+
const active: string[] = JSON.parse(raw!);
|
|
159
|
+
expect(active).toContain('1.0.0');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('marks active releases as completed when UPDATES.md is deleted', () => {
|
|
163
|
+
// Pre-populate active releases in the store
|
|
164
|
+
store.set('updates:active_releases', JSON.stringify(['0.8.0', '0.9.0']));
|
|
165
|
+
|
|
166
|
+
// Workspace file does not exist — simulates the assistant having deleted it
|
|
167
|
+
const workspacePath = join(tempDir, 'UPDATES.md');
|
|
168
|
+
expect(existsSync(workspacePath)).toBe(false);
|
|
169
|
+
|
|
170
|
+
syncUpdateBulletinOnStartup();
|
|
171
|
+
|
|
172
|
+
// Active set should be cleared (except for the newly-added current release)
|
|
173
|
+
const activeRaw = store.get('updates:active_releases');
|
|
174
|
+
expect(activeRaw).toBeDefined();
|
|
175
|
+
const active: string[] = JSON.parse(activeRaw!);
|
|
176
|
+
// The old releases should not be in the active set
|
|
177
|
+
expect(active).not.toContain('0.8.0');
|
|
178
|
+
expect(active).not.toContain('0.9.0');
|
|
179
|
+
|
|
180
|
+
// The old releases should now be completed
|
|
181
|
+
const completedRaw = store.get('updates:completed_releases');
|
|
182
|
+
expect(completedRaw).toBeDefined();
|
|
183
|
+
const completed: string[] = JSON.parse(completedRaw!);
|
|
184
|
+
expect(completed).toContain('0.8.0');
|
|
185
|
+
expect(completed).toContain('0.9.0');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('does not recreate completed release after deletion', () => {
|
|
189
|
+
// First run — creates the workspace file and marks 1.0.0 active
|
|
190
|
+
syncUpdateBulletinOnStartup();
|
|
191
|
+
const workspacePath = join(tempDir, 'UPDATES.md');
|
|
192
|
+
expect(existsSync(workspacePath)).toBe(true);
|
|
193
|
+
|
|
194
|
+
// Simulate assistant deleting the file to signal completion
|
|
195
|
+
rmSync(workspacePath);
|
|
196
|
+
expect(existsSync(workspacePath)).toBe(false);
|
|
197
|
+
|
|
198
|
+
// Second run — deletion-completion should mark 1.0.0 completed
|
|
199
|
+
syncUpdateBulletinOnStartup();
|
|
200
|
+
|
|
201
|
+
// The file should NOT be recreated since the release is now completed
|
|
202
|
+
expect(existsSync(workspacePath)).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('merges pending old block with new release block', () => {
|
|
206
|
+
const workspacePath = join(tempDir, 'UPDATES.md');
|
|
207
|
+
// Pre-create workspace file with an old release block
|
|
208
|
+
const oldContent =
|
|
209
|
+
'<!-- vellum-update-release:0.9.0 -->\nOld release notes for 0.9.0.\n<!-- /vellum-update-release:0.9.0 -->\n';
|
|
210
|
+
writeFileSync(workspacePath, oldContent, 'utf-8');
|
|
211
|
+
|
|
212
|
+
syncUpdateBulletinOnStartup();
|
|
213
|
+
|
|
214
|
+
const content = readFileSync(workspacePath, 'utf-8');
|
|
215
|
+
// Both old and new release blocks should be present
|
|
216
|
+
expect(content).toContain('<!-- vellum-update-release:0.9.0 -->');
|
|
217
|
+
expect(content).toContain('Old release notes for 0.9.0.');
|
|
218
|
+
expect(content).toContain('<!-- vellum-update-release:1.0.0 -->');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('idempotent on repeated sync calls', () => {
|
|
222
|
+
// First call
|
|
223
|
+
syncUpdateBulletinOnStartup();
|
|
224
|
+
const workspacePath = join(tempDir, 'UPDATES.md');
|
|
225
|
+
const afterFirst = readFileSync(workspacePath, 'utf-8');
|
|
226
|
+
|
|
227
|
+
// Second call
|
|
228
|
+
syncUpdateBulletinOnStartup();
|
|
229
|
+
const afterSecond = readFileSync(workspacePath, 'utf-8');
|
|
230
|
+
|
|
231
|
+
expect(afterSecond).toBe(afterFirst);
|
|
232
|
+
|
|
233
|
+
// Third call for good measure
|
|
234
|
+
syncUpdateBulletinOnStartup();
|
|
235
|
+
const afterThird = readFileSync(workspacePath, 'utf-8');
|
|
236
|
+
|
|
237
|
+
expect(afterThird).toBe(afterFirst);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('write path produces valid UTF-8 with trailing newline', () => {
|
|
241
|
+
syncUpdateBulletinOnStartup();
|
|
242
|
+
const workspacePath = join(tempDir, 'UPDATES.md');
|
|
243
|
+
const content = readFileSync(workspacePath, 'utf-8');
|
|
244
|
+
|
|
245
|
+
expect(content.length).toBeGreaterThan(0);
|
|
246
|
+
expect(content.endsWith('\n')).toBe(true);
|
|
247
|
+
|
|
248
|
+
// Verify round-trip through Buffer produces identical content (valid UTF-8)
|
|
249
|
+
const roundTripped = Buffer.from(content, 'utf-8').toString('utf-8');
|
|
250
|
+
expect(roundTripped).toBe(content);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('no temp file leftovers after successful write', () => {
|
|
254
|
+
syncUpdateBulletinOnStartup();
|
|
255
|
+
|
|
256
|
+
const entries = readdirSync(tempDir);
|
|
257
|
+
const tmpFiles = entries.filter((e) => e.includes('.tmp.'));
|
|
258
|
+
expect(tmpFiles).toHaveLength(0);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test: ensures the bundled UPDATES.md template exists and meets
|
|
3
|
+
* the format expectations that the bulletin system depends on at runtime.
|
|
4
|
+
*
|
|
5
|
+
* The "## What's New" heading is a structural contract — bulletin rendering
|
|
6
|
+
* logic expects this section to be present in the template.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { describe, expect, test } from 'bun:test';
|
|
12
|
+
|
|
13
|
+
const TEMPLATE_PATH = join(import.meta.dirname, '..', 'config', 'templates', 'UPDATES.md');
|
|
14
|
+
|
|
15
|
+
describe('UPDATES.md template contract', () => {
|
|
16
|
+
test('template file exists', () => {
|
|
17
|
+
expect(existsSync(TEMPLATE_PATH)).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('template contains non-whitespace content', () => {
|
|
21
|
+
const content = readFileSync(TEMPLATE_PATH, 'utf-8');
|
|
22
|
+
expect(content.trim().length).toBeGreaterThan(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('template contains the "## What\'s New" heading', () => {
|
|
26
|
+
const content = readFileSync(TEMPLATE_PATH, 'utf-8');
|
|
27
|
+
expect(content).toContain("## What's New");
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -85,7 +85,7 @@ export class AgentLoop {
|
|
|
85
85
|
|
|
86
86
|
async run(
|
|
87
87
|
messages: Message[],
|
|
88
|
-
onEvent: (event: AgentEvent) => void
|
|
88
|
+
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
89
89
|
signal?: AbortSignal,
|
|
90
90
|
requestId?: string,
|
|
91
91
|
onCheckpoint?: (checkpoint: CheckpointInfo) => CheckpointDecision,
|
|
@@ -244,7 +244,7 @@ export class AgentLoop {
|
|
|
244
244
|
};
|
|
245
245
|
history.push(assistantMessage);
|
|
246
246
|
|
|
247
|
-
onEvent({ type: 'message_complete', message: assistantMessage });
|
|
247
|
+
await onEvent({ type: 'message_complete', message: assistantMessage });
|
|
248
248
|
|
|
249
249
|
// Check for tool use
|
|
250
250
|
toolUseBlocks = response.content.filter(
|
package/src/amazon/client.ts
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
import type { ExtensionCommand, ExtensionResponse } from '../browser-extension-relay/protocol.js';
|
|
52
52
|
import { extensionRelayServer } from '../browser-extension-relay/server.js';
|
|
53
|
-
import {
|
|
53
|
+
import { getGatewayInternalBaseUrl } from '../config/env.js';
|
|
54
54
|
import type { ExtractedCredential } from '../tools/browser/network-recording-types.js';
|
|
55
55
|
import { readHttpToken } from '../util/platform.js';
|
|
56
56
|
import {
|
|
@@ -81,8 +81,7 @@ export async function sendRelayCommand(command: Record<string, unknown>): Promis
|
|
|
81
81
|
throw new Error('Browser extension relay is not connected and no HTTP token found. Is the daemon running?');
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const
|
|
85
|
-
const resp = await fetch(`http://127.0.0.1:${port}/v1/browser-relay/command`, {
|
|
84
|
+
const resp = await fetch(`${getGatewayInternalBaseUrl()}/v1/browser-relay/command`, {
|
|
86
85
|
method: 'POST',
|
|
87
86
|
headers: {
|
|
88
87
|
'Content-Type': 'application/json',
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ServerMessage } from '../daemon/ipc-contract.js';
|
|
12
12
|
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
13
|
+
import {
|
|
14
|
+
getDeliveriesByRequestId,
|
|
15
|
+
getPendingRequestByCallSessionId,
|
|
16
|
+
markTimedOutWithReason,
|
|
17
|
+
} from '../memory/guardian-action-store.js';
|
|
13
18
|
import { getLogger } from '../util/logger.js';
|
|
14
19
|
import { getMaxCallDurationMs, getUserConsultationTimeoutMs, SILENCE_TIMEOUT_MS } from './call-constants.js';
|
|
15
20
|
import { persistCallCompletionMessage } from './call-conversation-messages.js';
|
|
@@ -22,7 +27,10 @@ import {
|
|
|
22
27
|
recordCallEvent,
|
|
23
28
|
updateCallSession,
|
|
24
29
|
} from './call-store.js';
|
|
30
|
+
import { getGatewayInternalBaseUrl } from '../config/env.js';
|
|
31
|
+
import { readHttpToken } from '../util/platform.js';
|
|
25
32
|
import { dispatchGuardianQuestion } from './guardian-dispatch.js';
|
|
33
|
+
import { sendGuardianExpiryNotices } from './guardian-action-sweep.js';
|
|
26
34
|
import type { RelayConnection } from './relay-server.js';
|
|
27
35
|
import type { PromptSpeakerContext } from './speaker-identification.js';
|
|
28
36
|
import { startVoiceTurn, type VoiceTurnHandle } from './voice-session-bridge.js';
|
|
@@ -38,6 +46,8 @@ const USER_INSTRUCTION_MARKER_REGEX = /\[USER_INSTRUCTION:\s*.+?\]/g;
|
|
|
38
46
|
const CALL_OPENING_MARKER_REGEX = /\[CALL_OPENING\]/g;
|
|
39
47
|
const CALL_OPENING_ACK_MARKER_REGEX = /\[CALL_OPENING_ACK\]/g;
|
|
40
48
|
const END_CALL_MARKER_REGEX = /\[END_CALL\]/g;
|
|
49
|
+
const GUARDIAN_TIMEOUT_MARKER_REGEX = /\[GUARDIAN_TIMEOUT\]/g;
|
|
50
|
+
const GUARDIAN_UNAVAILABLE_MARKER_REGEX = /\[GUARDIAN_UNAVAILABLE\]/g;
|
|
41
51
|
const CALL_OPENING_MARKER = '[CALL_OPENING]';
|
|
42
52
|
const CALL_OPENING_ACK_MARKER = '[CALL_OPENING_ACK]';
|
|
43
53
|
const END_CALL_MARKER = '[END_CALL]';
|
|
@@ -49,7 +59,9 @@ function stripInternalSpeechMarkers(text: string): string {
|
|
|
49
59
|
.replace(USER_INSTRUCTION_MARKER_REGEX, '')
|
|
50
60
|
.replace(CALL_OPENING_MARKER_REGEX, '')
|
|
51
61
|
.replace(CALL_OPENING_ACK_MARKER_REGEX, '')
|
|
52
|
-
.replace(END_CALL_MARKER_REGEX, '')
|
|
62
|
+
.replace(END_CALL_MARKER_REGEX, '')
|
|
63
|
+
.replace(GUARDIAN_TIMEOUT_MARKER_REGEX, '')
|
|
64
|
+
.replace(GUARDIAN_UNAVAILABLE_MARKER_REGEX, '');
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
export class CallController {
|
|
@@ -92,6 +104,13 @@ export class CallController {
|
|
|
92
104
|
* alternation in the underlying session pipeline.
|
|
93
105
|
*/
|
|
94
106
|
private lastSentWasOpener = false;
|
|
107
|
+
/**
|
|
108
|
+
* Set to true after a guardian consultation timeout occurs in this call.
|
|
109
|
+
* Subsequent ASK_GUARDIAN attempts skip the full wait and immediately
|
|
110
|
+
* inject a guardian-unavailable instruction so the model can adapt
|
|
111
|
+
* without blocking the caller.
|
|
112
|
+
*/
|
|
113
|
+
private guardianUnavailableForCall = false;
|
|
95
114
|
|
|
96
115
|
constructor(
|
|
97
116
|
callSessionId: string,
|
|
@@ -401,6 +420,8 @@ export class CallController {
|
|
|
401
420
|
'[CALL_OPENING]'.startsWith(afterBracket) ||
|
|
402
421
|
'[CALL_OPENING_ACK]'.startsWith(afterBracket) ||
|
|
403
422
|
'[END_CALL]'.startsWith(afterBracket) ||
|
|
423
|
+
'[GUARDIAN_TIMEOUT]'.startsWith(afterBracket) ||
|
|
424
|
+
'[GUARDIAN_UNAVAILABLE]'.startsWith(afterBracket) ||
|
|
404
425
|
afterBracket.startsWith('[ASK_GUARDIAN:') ||
|
|
405
426
|
afterBracket.startsWith('[USER_ANSWERED:') ||
|
|
406
427
|
afterBracket.startsWith('[USER_INSTRUCTION:') ||
|
|
@@ -409,7 +430,11 @@ export class CallController {
|
|
|
409
430
|
afterBracket === '[CALL_OPENING_ACK' ||
|
|
410
431
|
afterBracket.startsWith('[CALL_OPENING_ACK]') ||
|
|
411
432
|
afterBracket === '[END_CALL' ||
|
|
412
|
-
afterBracket.startsWith('[END_CALL]')
|
|
433
|
+
afterBracket.startsWith('[END_CALL]') ||
|
|
434
|
+
afterBracket === '[GUARDIAN_TIMEOUT' ||
|
|
435
|
+
afterBracket.startsWith('[GUARDIAN_TIMEOUT]') ||
|
|
436
|
+
afterBracket === '[GUARDIAN_UNAVAILABLE' ||
|
|
437
|
+
afterBracket.startsWith('[GUARDIAN_UNAVAILABLE]');
|
|
413
438
|
|
|
414
439
|
if (!couldBeControl) {
|
|
415
440
|
// Not a control marker prefix — flush up to the next '[' (if any)
|
|
@@ -514,6 +539,19 @@ export class CallController {
|
|
|
514
539
|
log.info({ callSessionId: this.callSessionId }, 'Caller is guardian — skipping ASK_GUARDIAN dispatch, asking directly');
|
|
515
540
|
this.pendingInstructions.push(`You just tried to use [ASK_GUARDIAN] but the person on the phone IS your guardian. Ask them directly: "${questionText}"`);
|
|
516
541
|
// Fall through to normal turn completion (idle + flushPendingInstructions)
|
|
542
|
+
} else if (this.guardianUnavailableForCall) {
|
|
543
|
+
// Guardian already timed out earlier in this call — skip the full
|
|
544
|
+
// consultation wait and immediately tell the model to proceed
|
|
545
|
+
// without guardian input.
|
|
546
|
+
log.info({ callSessionId: this.callSessionId }, 'Guardian unavailable for call — skipping ASK_GUARDIAN wait');
|
|
547
|
+
recordCallEvent(this.callSessionId, 'guardian_unavailable_skipped', { question: questionText });
|
|
548
|
+
this.pendingInstructions.push(
|
|
549
|
+
`[GUARDIAN_UNAVAILABLE] You tried to consult your guardian again, but they were already unreachable earlier in this call. `
|
|
550
|
+
+ `Do NOT use [ASK_GUARDIAN] again. Instead, let the caller know you cannot reach the guardian right now, `
|
|
551
|
+
+ `and continue the conversation by asking if there is anything else you can help with or if they would like a callback. `
|
|
552
|
+
+ `The unanswered question was: "${questionText}"`,
|
|
553
|
+
);
|
|
554
|
+
// Fall through to normal turn completion (idle + flushPendingInstructions)
|
|
517
555
|
} else {
|
|
518
556
|
const pendingQuestion = createPendingQuestion(this.callSessionId, questionText);
|
|
519
557
|
this.state = 'waiting_on_user';
|
|
@@ -536,39 +574,74 @@ export class CallController {
|
|
|
536
574
|
|
|
537
575
|
// Set a consultation timeout
|
|
538
576
|
this.consultationTimer = setTimeout(() => {
|
|
539
|
-
if (this.state
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
577
|
+
if (this.state !== 'waiting_on_user') return;
|
|
578
|
+
|
|
579
|
+
log.info({ callSessionId: this.callSessionId }, 'User consultation timed out');
|
|
580
|
+
|
|
581
|
+
// Mark the linked guardian action request as timed out and
|
|
582
|
+
// send expiry notices to guardian destinations. Deliveries
|
|
583
|
+
// must be captured before markTimedOutWithReason changes
|
|
584
|
+
// their status.
|
|
585
|
+
const pendingActionRequest = getPendingRequestByCallSessionId(this.callSessionId);
|
|
586
|
+
if (pendingActionRequest) {
|
|
587
|
+
const deliveries = getDeliveriesByRequestId(pendingActionRequest.id);
|
|
588
|
+
markTimedOutWithReason(pendingActionRequest.id, 'call_timeout');
|
|
589
|
+
log.info(
|
|
590
|
+
{ callSessionId: this.callSessionId, requestId: pendingActionRequest.id },
|
|
591
|
+
'Marked guardian action request as timed out',
|
|
544
592
|
);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
593
|
+
void sendGuardianExpiryNotices(
|
|
594
|
+
deliveries,
|
|
595
|
+
pendingActionRequest.assistantId,
|
|
596
|
+
getGatewayInternalBaseUrl(),
|
|
597
|
+
readHttpToken() ?? undefined,
|
|
598
|
+
).catch((err) => {
|
|
599
|
+
log.error(
|
|
600
|
+
{ err, callSessionId: this.callSessionId, requestId: pendingActionRequest.id },
|
|
601
|
+
'Failed to send guardian action expiry notices after call timeout',
|
|
602
|
+
);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Expire pending questions and update call state
|
|
607
|
+
expirePendingQuestions(this.callSessionId);
|
|
608
|
+
this.state = 'idle';
|
|
609
|
+
updateCallSession(this.callSessionId, { status: 'in_progress' });
|
|
610
|
+
this.guardianUnavailableForCall = true;
|
|
611
|
+
recordCallEvent(this.callSessionId, 'guardian_consultation_timed_out', { question: questionText });
|
|
612
|
+
|
|
613
|
+
// Restart silence detection before firing the generated turn
|
|
614
|
+
this.resetSilenceTimer();
|
|
615
|
+
|
|
616
|
+
// Build a generated turn instruction instead of hardcoded text.
|
|
617
|
+
// Merge any queued instructions and caller utterances into the
|
|
618
|
+
// timeout turn to avoid concurrent-turn races.
|
|
619
|
+
const timeoutInstruction =
|
|
620
|
+
`[GUARDIAN_TIMEOUT] Your guardian did not respond in time to your question: "${questionText}". `
|
|
621
|
+
+ `Apologize to the caller for the delay, let them know you were unable to reach your guardian, `
|
|
622
|
+
+ `ask if they would like to leave a message or receive a callback, `
|
|
623
|
+
+ `and ask if there are any other questions you can help with right now.`;
|
|
624
|
+
|
|
625
|
+
const parts: string[] = [];
|
|
626
|
+
for (const instr of this.pendingInstructions) {
|
|
627
|
+
parts.push(`[USER_INSTRUCTION: ${instr}]`);
|
|
628
|
+
}
|
|
629
|
+
this.pendingInstructions = [];
|
|
630
|
+
parts.push(`[USER_INSTRUCTION: ${timeoutInstruction}]`);
|
|
631
|
+
|
|
632
|
+
if (this.pendingCallerUtterances.length > 0) {
|
|
633
|
+
const latest = this.pendingCallerUtterances[this.pendingCallerUtterances.length - 1];
|
|
634
|
+
this.pendingCallerUtterances = [];
|
|
635
|
+
const callerContent = this.formatCallerUtterance(latest.transcript, latest.speaker);
|
|
636
|
+
if (callerContent.length > 0) {
|
|
637
|
+
parts.push(callerContent);
|
|
570
638
|
}
|
|
571
639
|
}
|
|
640
|
+
|
|
641
|
+
const content = parts.join('\n');
|
|
642
|
+
this.runTurn(content).catch((err) =>
|
|
643
|
+
log.error({ err, callSessionId: this.callSessionId }, 'runTurn failed after guardian consultation timeout'),
|
|
644
|
+
);
|
|
572
645
|
}, getUserConsultationTimeoutMs());
|
|
573
646
|
return;
|
|
574
647
|
}
|
|
@@ -587,7 +660,9 @@ export class CallController {
|
|
|
587
660
|
|
|
588
661
|
// Notify the voice conversation
|
|
589
662
|
if (shouldNotifyCompletion && currentSession) {
|
|
590
|
-
persistCallCompletionMessage(currentSession.conversationId, this.callSessionId)
|
|
663
|
+
persistCallCompletionMessage(currentSession.conversationId, this.callSessionId).catch((err) => {
|
|
664
|
+
log.error({ err, conversationId: currentSession.conversationId, callSessionId: this.callSessionId }, 'Failed to persist call completion message');
|
|
665
|
+
});
|
|
591
666
|
fireCallCompletionNotifier(currentSession.conversationId, this.callSessionId);
|
|
592
667
|
}
|
|
593
668
|
|
|
@@ -596,6 +671,8 @@ export class CallController {
|
|
|
596
671
|
const durationMs = currentSession.startedAt ? Date.now() - currentSession.startedAt : 0;
|
|
597
672
|
addPointerMessage(currentSession.initiatedFromConversationId, 'completed', currentSession.toNumber, {
|
|
598
673
|
duration: durationMs > 0 ? formatDuration(durationMs) : undefined,
|
|
674
|
+
}).catch((err) => {
|
|
675
|
+
log.warn({ conversationId: currentSession.initiatedFromConversationId, err }, 'Skipping pointer write — origin conversation may no longer exist');
|
|
599
676
|
});
|
|
600
677
|
}
|
|
601
678
|
this.state = 'idle';
|
|
@@ -761,7 +838,9 @@ export class CallController {
|
|
|
761
838
|
updateCallSession(this.callSessionId, { status: 'completed', endedAt: Date.now() });
|
|
762
839
|
recordCallEvent(this.callSessionId, 'call_ended', { reason: 'max_duration' });
|
|
763
840
|
if (shouldNotifyCompletion && currentSession) {
|
|
764
|
-
persistCallCompletionMessage(currentSession.conversationId, this.callSessionId)
|
|
841
|
+
persistCallCompletionMessage(currentSession.conversationId, this.callSessionId).catch((err) => {
|
|
842
|
+
log.error({ err, conversationId: currentSession.conversationId, callSessionId: this.callSessionId }, 'Failed to persist call completion message');
|
|
843
|
+
});
|
|
765
844
|
fireCallCompletionNotifier(currentSession.conversationId, this.callSessionId);
|
|
766
845
|
}
|
|
767
846
|
|
|
@@ -770,6 +849,8 @@ export class CallController {
|
|
|
770
849
|
const durationMs = currentSession.startedAt ? Date.now() - currentSession.startedAt : 0;
|
|
771
850
|
addPointerMessage(currentSession.initiatedFromConversationId, 'completed', currentSession.toNumber, {
|
|
772
851
|
duration: durationMs > 0 ? formatDuration(durationMs) : undefined,
|
|
852
|
+
}).catch((err) => {
|
|
853
|
+
log.warn({ conversationId: currentSession.initiatedFromConversationId, err }, 'Skipping pointer write — origin conversation may no longer exist');
|
|
773
854
|
});
|
|
774
855
|
}
|
|
775
856
|
}, 3000);
|
|
@@ -21,9 +21,9 @@ export function buildCallCompletionMessage(callSessionId: string): string {
|
|
|
21
21
|
return `**${statusLabel}**${durationStr}. ${events.length} event(s) recorded.`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function persistCallCompletionMessage(conversationId: string, callSessionId: string): string {
|
|
24
|
+
export async function persistCallCompletionMessage(conversationId: string, callSessionId: string): Promise<string> {
|
|
25
25
|
const summaryText = buildCallCompletionMessage(callSessionId);
|
|
26
|
-
conversationStore.addMessage(
|
|
26
|
+
await conversationStore.addMessage(
|
|
27
27
|
conversationId,
|
|
28
28
|
'assistant',
|
|
29
29
|
JSON.stringify([{ type: 'text', text: summaryText }]),
|
package/src/calls/call-domain.ts
CHANGED
|
@@ -366,7 +366,9 @@ export async function startCall(input: StartCallInput): Promise<StartCallResult
|
|
|
366
366
|
log.info({ callSessionId: session.id, callSid }, 'Call initiated successfully');
|
|
367
367
|
|
|
368
368
|
// Post a concise pointer message in the initiating conversation
|
|
369
|
-
addPointerMessage(conversationId, 'started', phoneNumber)
|
|
369
|
+
addPointerMessage(conversationId, 'started', phoneNumber).catch((err) => {
|
|
370
|
+
log.warn({ conversationId, err }, 'Failed to post call-started pointer message');
|
|
371
|
+
});
|
|
370
372
|
|
|
371
373
|
return {
|
|
372
374
|
ok: true,
|
|
@@ -392,7 +394,9 @@ export async function startCall(input: StartCallInput): Promise<StartCallResult
|
|
|
392
394
|
}
|
|
393
395
|
|
|
394
396
|
// Post a failure pointer message in the initiating conversation
|
|
395
|
-
addPointerMessage(conversationId, 'failed', phoneNumber, { reason: msg })
|
|
397
|
+
addPointerMessage(conversationId, 'failed', phoneNumber, { reason: msg }).catch((pointerErr) => {
|
|
398
|
+
log.warn({ conversationId, err: pointerErr }, 'Failed to post call-failed pointer message');
|
|
399
|
+
});
|
|
396
400
|
|
|
397
401
|
return { ok: false, error: `Error initiating call: ${msg}`, status: 500 };
|
|
398
402
|
}
|
|
@@ -572,6 +576,8 @@ export type StartGuardianVerificationCallInput = {
|
|
|
572
576
|
phoneNumber: string;
|
|
573
577
|
guardianVerificationSessionId: string;
|
|
574
578
|
assistantId?: string;
|
|
579
|
+
/** Origin conversation ID so completion/failure pointers can route back. */
|
|
580
|
+
originConversationId?: string;
|
|
575
581
|
};
|
|
576
582
|
|
|
577
583
|
export type StartGuardianVerificationCallResult =
|
|
@@ -588,7 +594,7 @@ export type StartGuardianVerificationCallResult =
|
|
|
588
594
|
export async function startGuardianVerificationCall(
|
|
589
595
|
input: StartGuardianVerificationCallInput,
|
|
590
596
|
): Promise<StartGuardianVerificationCallResult> {
|
|
591
|
-
const { phoneNumber, guardianVerificationSessionId, assistantId = 'self' } = input;
|
|
597
|
+
const { phoneNumber, guardianVerificationSessionId, assistantId = 'self', originConversationId } = input;
|
|
592
598
|
|
|
593
599
|
if (!phoneNumber || !E164_REGEX.test(phoneNumber)) {
|
|
594
600
|
return { ok: false, error: 'phone_number must be in E.164 format', status: 400 };
|
|
@@ -626,6 +632,7 @@ export async function startGuardianVerificationCall(
|
|
|
626
632
|
callMode: 'guardian_verification',
|
|
627
633
|
guardianVerificationSessionId,
|
|
628
634
|
assistantId,
|
|
635
|
+
initiatedFromConversationId: originConversationId,
|
|
629
636
|
});
|
|
630
637
|
sessionId = session.id;
|
|
631
638
|
|