@vellumai/assistant 0.8.11 → 0.8.12-staging.1

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 (219) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +126 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-usage-store.test.ts +11 -0
  37. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  38. package/src/__tests__/model-intents.test.ts +1 -1
  39. package/src/__tests__/oauth-cli.test.ts +19 -8
  40. package/src/__tests__/openai-provider.test.ts +34 -0
  41. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  42. package/src/__tests__/recurrence-engine.test.ts +45 -0
  43. package/src/__tests__/schedule-routes.test.ts +34 -0
  44. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  45. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  46. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  47. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  48. package/src/__tests__/subagent-tools.test.ts +1 -0
  49. package/src/__tests__/system-prompt.test.ts +109 -11
  50. package/src/__tests__/tool-error-hook.test.ts +1 -0
  51. package/src/__tests__/tool-result-spool.test.ts +337 -0
  52. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  53. package/src/__tests__/validate-input.test.ts +95 -1
  54. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  55. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  56. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  57. package/src/agent/loop.ts +81 -24
  58. package/src/api/events/usage-progress.ts +28 -0
  59. package/src/api/index.ts +6 -0
  60. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  61. package/src/bundler/app-bundler.ts +25 -42
  62. package/src/calls/call-controller.ts +1 -1
  63. package/src/cli/commands/plugins.ts +248 -15
  64. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  65. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  66. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  67. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  68. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  69. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
  70. package/src/cli/lib/inspect-plugin.ts +252 -0
  71. package/src/cli/lib/install-from-github.ts +214 -21
  72. package/src/cli/lib/list-installed-plugins.ts +17 -6
  73. package/src/cli/lib/plugin-artifact.ts +103 -0
  74. package/src/cli/lib/plugin-details.ts +18 -1
  75. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  76. package/src/cli/lib/upgrade-plugin.ts +219 -0
  77. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  78. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  79. package/src/config/call-site-defaults.ts +13 -2
  80. package/src/config/feature-flag-registry.json +8 -16
  81. package/src/config/loader.ts +52 -59
  82. package/src/config/schema.ts +0 -2
  83. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  84. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  85. package/src/config/schemas/llm.ts +10 -0
  86. package/src/config/schemas/memory-v2.ts +13 -0
  87. package/src/config/schemas/memory-v3.ts +92 -0
  88. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  89. package/src/context/tool-result-spool.ts +104 -0
  90. package/src/credential-execution/feature-gates.ts +0 -1
  91. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  92. package/src/daemon/conversation-error.ts +6 -15
  93. package/src/daemon/conversation.ts +9 -0
  94. package/src/daemon/disk-pressure-policy.ts +0 -1
  95. package/src/daemon/lifecycle.ts +1 -20
  96. package/src/daemon/message-types/conversations.ts +2 -15
  97. package/src/daemon/trust-context.ts +1 -1
  98. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  99. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  100. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  101. package/src/home/home-content-refresh.ts +43 -31
  102. package/src/home/home-greeting-cache.ts +8 -1
  103. package/src/home/home-greeting.ts +13 -9
  104. package/src/home/suggested-prompts.ts +77 -24
  105. package/src/ipc/routes/trust-rules.test.ts +66 -72
  106. package/src/media/image-credentials.ts +2 -2
  107. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  108. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  109. package/src/memory/conversation-attention-store.ts +1 -0
  110. package/src/memory/conversation-bootstrap.ts +18 -9
  111. package/src/memory/conversation-crud.ts +12 -2
  112. package/src/memory/conversation-title-service.ts +53 -9
  113. package/src/memory/delivery-channels.ts +0 -69
  114. package/src/memory/graph/extraction-job.ts +0 -15
  115. package/src/memory/guardian-action-store.ts +1 -376
  116. package/src/memory/llm-usage-store.ts +5 -1
  117. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  119. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  120. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  121. package/src/memory/v2/consolidation-job.ts +93 -9
  122. package/src/memory/v2/injection.ts +53 -0
  123. package/src/memory/v2/prompts/consolidation.ts +1 -0
  124. package/src/memory/v2/static-context.ts +13 -1
  125. package/src/memory/v2/sweep-job.ts +1 -1
  126. package/src/memory/v2/types.ts +5 -0
  127. package/src/plugin-api/types.ts +7 -0
  128. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  129. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  130. package/src/plugins/defaults/index.ts +25 -0
  131. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  132. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  133. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  134. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  135. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  136. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  137. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  138. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  139. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  140. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  141. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  142. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  143. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  144. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  145. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  146. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  147. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  148. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  149. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  150. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  151. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  152. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  153. package/src/prompts/cache-boundary.ts +17 -0
  154. package/src/prompts/sections.ts +50 -17
  155. package/src/prompts/system-prompt.ts +12 -4
  156. package/src/prompts/templates/system-sections.ts +22 -0
  157. package/src/providers/anthropic/client.ts +74 -28
  158. package/src/providers/gemini/client.ts +5 -1
  159. package/src/providers/minimax/client.ts +9 -0
  160. package/src/providers/model-intents.ts +2 -2
  161. package/src/providers/openai/chat-completions-provider.ts +2 -1
  162. package/src/providers/openai/responses-provider.ts +5 -1
  163. package/src/providers/retry.ts +8 -0
  164. package/src/providers/types.ts +11 -0
  165. package/src/runtime/AGENTS.md +6 -0
  166. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  167. package/src/runtime/agent-wake.ts +5 -5
  168. package/src/runtime/background-job-runner.ts +2 -2
  169. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  170. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  171. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  172. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  173. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  174. package/src/runtime/pre-first-message-gate.ts +1 -1
  175. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  176. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  177. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  178. package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
  179. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  180. package/src/runtime/routes/btw-routes.ts +0 -14
  181. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  182. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  183. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  184. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  185. package/src/runtime/routes/home-feed-routes.ts +10 -0
  186. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  187. package/src/runtime/routes/identity-routes.ts +76 -20
  188. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  189. package/src/runtime/routes/plugins-routes.ts +21 -0
  190. package/src/runtime/routes/schedule-routes.ts +19 -2
  191. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  192. package/src/schedule/recurrence-engine.ts +34 -0
  193. package/src/schedule/scheduler.ts +1 -0
  194. package/src/skills/validate-input.ts +41 -1
  195. package/src/subagent/types.ts +26 -1
  196. package/src/telemetry/types.ts +15 -1
  197. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  198. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  199. package/src/tools/apps/executors.ts +1 -1
  200. package/src/tools/skills/skill-tool-factory.ts +19 -8
  201. package/src/usage/types.ts +8 -1
  202. package/src/util/platform.ts +16 -0
  203. package/src/watcher/engine.ts +1 -0
  204. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  205. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  206. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  207. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  210. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  211. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  212. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  213. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  214. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  215. package/src/config/schemas/updates.ts +0 -14
  216. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  217. package/src/memory/conversation-starters-cadence.ts +0 -78
  218. package/src/prompts/update-bulletin-job.ts +0 -180
  219. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -1,322 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- mock.module("../util/logger.js", () => ({
4
- getLogger: () =>
5
- new Proxy({} as Record<string, unknown>, {
6
- get: () => () => {},
7
- }),
8
- }));
9
-
10
- // Track call starts for assertions
11
- const startedCalls: Array<{
12
- phoneNumber: string;
13
- task: string;
14
- conversationId: string;
15
- }> = [];
16
- let mockStartCallResult:
17
- | {
18
- ok: true;
19
- session: { id: string };
20
- callSid: string;
21
- callerIdentityMode: string;
22
- }
23
- | { ok: false; error: string } = {
24
- ok: true,
25
- session: { id: "mock-call-session" },
26
- callSid: "CA-mock",
27
- callerIdentityMode: "assistant_number",
28
- };
29
-
30
- mock.module("../calls/call-domain.js", () => ({
31
- startCall: async (input: {
32
- phoneNumber: string;
33
- task: string;
34
- conversationId: string;
35
- }) => {
36
- startedCalls.push(input);
37
- return mockStartCallResult;
38
- },
39
- }));
40
-
41
- mock.module("../config/env.js", () => ({
42
- isHttpAuthDisabled: () => true,
43
- getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
44
- }));
45
-
46
- // Mock conversation-key-store for call_back conversation creation
47
- let conversationCounter = 0;
48
- mock.module("../memory/conversation-key-store.js", () => ({
49
- getOrCreateConversation: () => ({
50
- conversationId: `mock-conv-${++conversationCounter}`,
51
- created: true,
52
- }),
53
- }));
54
-
55
- import {
56
- createCallSession,
57
- createPendingQuestion,
58
- } from "../calls/call-store.js";
59
- import { getDb } from "../memory/db-connection.js";
60
- import { initializeDb } from "../memory/db-init.js";
61
- import {
62
- createGuardianActionDelivery,
63
- createGuardianActionRequest,
64
- getGuardianActionRequest,
65
- markTimedOutWithReason,
66
- progressFollowupState,
67
- startFollowupFromExpiredRequest,
68
- updateDeliveryStatus,
69
- } from "../memory/guardian-action-store.js";
70
- import { conversations } from "../memory/schema.js";
71
- import { executeFollowupAction } from "../runtime/guardian-action-followup-executor.js";
72
- import { resolveCounterparty } from "../runtime/guardian-action-followup-executor.js";
73
-
74
- initializeDb();
75
-
76
- function ensureConversation(id: string): void {
77
- const db = getDb();
78
- const now = Date.now();
79
- db.insert(conversations)
80
- .values({
81
- id,
82
- title: `Conversation ${id}`,
83
- createdAt: now,
84
- updatedAt: now,
85
- })
86
- .run();
87
- }
88
-
89
- function resetTables(): void {
90
- const db = getDb();
91
- db.run("DELETE FROM guardian_action_deliveries");
92
- db.run("DELETE FROM guardian_action_requests");
93
- db.run("DELETE FROM call_pending_questions");
94
- db.run("DELETE FROM call_events");
95
- db.run("DELETE FROM call_sessions");
96
- db.run("DELETE FROM messages");
97
- db.run("DELETE FROM conversations");
98
- startedCalls.length = 0;
99
- conversationCounter = 0;
100
- mockStartCallResult = {
101
- ok: true,
102
- session: { id: "mock-call-session" },
103
- callSid: "CA-mock",
104
- callerIdentityMode: "assistant_number",
105
- };
106
- }
107
-
108
- /**
109
- * Create a request in `dispatching` state ready for the executor.
110
- * The call session has fromNumber='+15550001111' (the counterparty).
111
- */
112
- function createDispatchingRequest(convId: string, action: "call_back") {
113
- ensureConversation(convId);
114
- const session = createCallSession({
115
- conversationId: convId,
116
- provider: "twilio",
117
- fromNumber: "+15550001111",
118
- toNumber: "+15550002222",
119
- });
120
- const pq = createPendingQuestion(session.id, "What is the gate code?");
121
- const request = createGuardianActionRequest({
122
- kind: "ask_guardian",
123
- sourceChannel: "phone",
124
- sourceConversationId: convId,
125
- callSessionId: session.id,
126
- pendingQuestionId: pq.id,
127
- questionText: pq.questionText,
128
- expiresAt: Date.now() - 10_000,
129
- });
130
-
131
- const deliveryConvId = `delivery-conv-${request.id}`;
132
- ensureConversation(deliveryConvId);
133
- const delivery = createGuardianActionDelivery({
134
- requestId: request.id,
135
- destinationChannel: "telegram",
136
- destinationChatId: "chat-123",
137
- destinationExternalUserId: "user-456",
138
- destinationConversationId: deliveryConvId,
139
- });
140
- updateDeliveryStatus(delivery.id, "sent");
141
-
142
- // Expire the request
143
- markTimedOutWithReason(request.id, "call_timeout");
144
-
145
- // Start follow-up
146
- startFollowupFromExpiredRequest(request.id, "The gate code is 1234");
147
-
148
- // Progress to dispatching with the given action
149
- progressFollowupState(request.id, "dispatching", action);
150
-
151
- return {
152
- request: getGuardianActionRequest(request.id)!,
153
- delivery,
154
- callSession: session,
155
- };
156
- }
157
-
158
- describe("guardian-action-followup-executor", () => {
159
- beforeEach(() => {
160
- resetTables();
161
- });
162
-
163
- // ── Counterparty resolution ─────────────────────────────────────────
164
-
165
- describe("resolveCounterparty", () => {
166
- test("resolves fromNumber as counterparty for inbound call", () => {
167
- ensureConversation("cp-test-1");
168
- const session = createCallSession({
169
- conversationId: "cp-test-1",
170
- provider: "twilio",
171
- fromNumber: "+15550001111",
172
- toNumber: "+15550002222",
173
- });
174
-
175
- const result = resolveCounterparty(session.id);
176
- expect(result).not.toBeNull();
177
- expect(result!.phoneNumber).toBe("+15550001111");
178
- expect(result!.displayIdentifier).toBe("+15550001111");
179
- });
180
-
181
- test("resolves toNumber as counterparty for outbound call", () => {
182
- ensureConversation("cp-test-outbound");
183
- const session = createCallSession({
184
- conversationId: "cp-test-outbound",
185
- provider: "twilio",
186
- fromNumber: "+15550002222", // assistant's number
187
- toNumber: "+15550001111", // callee (the counterparty)
188
- initiatedFromConversationId: "cp-test-outbound", // signals outbound
189
- });
190
-
191
- const result = resolveCounterparty(session.id);
192
- expect(result).not.toBeNull();
193
- expect(result!.phoneNumber).toBe("+15550001111");
194
- expect(result!.displayIdentifier).toBe("+15550001111");
195
- });
196
-
197
- test("returns null for nonexistent call session", () => {
198
- const result = resolveCounterparty("nonexistent-session-id");
199
- expect(result).toBeNull();
200
- });
201
- });
202
-
203
- // ── call_back execution ─────────────────────────────────────────────
204
-
205
- describe("call_back", () => {
206
- test("starts outbound call to counterparty and finalizes as completed", async () => {
207
- const { request } = createDispatchingRequest("exec-call-1", "call_back");
208
-
209
- const result = await executeFollowupAction(request.id, "call_back");
210
-
211
- expect(result.ok).toBe(true);
212
- expect(result.action).toBe("call_back");
213
- expect(result.guardianReplyText.length).toBeGreaterThan(0);
214
-
215
- // Verify call was started to the counterparty
216
- expect(startedCalls.length).toBe(1);
217
- expect(startedCalls[0].phoneNumber).toBe("+15550001111");
218
- expect(startedCalls[0].task).toContain("gate code");
219
-
220
- // Verify follow-up state is completed
221
- const updated = getGuardianActionRequest(request.id);
222
- expect(updated!.followupState).toBe("completed");
223
- expect(updated!.followupCompletedAt).toBeGreaterThan(0);
224
- });
225
-
226
- test("confirmation text mentions calling back", async () => {
227
- const { request } = createDispatchingRequest("exec-call-2", "call_back");
228
-
229
- const result = await executeFollowupAction(request.id, "call_back");
230
-
231
- expect(result.ok).toBe(true);
232
- expect(result.guardianReplyText).toContain("calling");
233
- });
234
-
235
- test("failed call start finalizes as failed with error message", async () => {
236
- mockStartCallResult = { ok: false, error: "Twilio account suspended" };
237
- const { request } = createDispatchingRequest(
238
- "exec-call-fail-1",
239
- "call_back",
240
- );
241
-
242
- const result = await executeFollowupAction(request.id, "call_back");
243
-
244
- expect(result.ok).toBe(false);
245
- if (!result.ok) {
246
- expect(result.error).toContain("Twilio account suspended");
247
- }
248
- expect(result.guardianReplyText.length).toBeGreaterThan(0);
249
-
250
- // Verify follow-up state is failed
251
- const updated = getGuardianActionRequest(request.id);
252
- expect(updated!.followupState).toBe("failed");
253
- expect(updated!.followupCompletedAt).toBeGreaterThan(0);
254
- });
255
- });
256
-
257
- // ── Error handling ──────────────────────────────────────────────────
258
-
259
- describe("error handling", () => {
260
- test("nonexistent request returns failure with error message", async () => {
261
- const result = await executeFollowupAction("nonexistent-id", "call_back");
262
-
263
- expect(result.ok).toBe(false);
264
- if (!result.ok) {
265
- expect(result.error).toContain("not found");
266
- }
267
- expect(result.guardianReplyText.length).toBeGreaterThan(0);
268
- });
269
-
270
- test("request not in dispatching state returns failure", async () => {
271
- // Create a request in awaiting_guardian_choice (not dispatching)
272
- ensureConversation("exec-wrong-state");
273
- const session = createCallSession({
274
- conversationId: "exec-wrong-state",
275
- provider: "twilio",
276
- fromNumber: "+15550001111",
277
- toNumber: "+15550002222",
278
- });
279
- const pq = createPendingQuestion(session.id, "Question?");
280
- const request = createGuardianActionRequest({
281
- kind: "ask_guardian",
282
- sourceChannel: "phone",
283
- sourceConversationId: "exec-wrong-state",
284
- callSessionId: session.id,
285
- pendingQuestionId: pq.id,
286
- questionText: pq.questionText,
287
- expiresAt: Date.now() - 10_000,
288
- });
289
- markTimedOutWithReason(request.id, "call_timeout");
290
- startFollowupFromExpiredRequest(request.id, "Answer");
291
- // Still in awaiting_guardian_choice — do NOT progress to dispatching
292
-
293
- const result = await executeFollowupAction(request.id, "call_back");
294
-
295
- expect(result.ok).toBe(false);
296
- if (!result.ok) {
297
- expect(result.error).toContain("Invalid followup state");
298
- }
299
- });
300
-
301
- test("follow-up states terminate correctly on success", async () => {
302
- const { request } = createDispatchingRequest("exec-state-1", "call_back");
303
-
304
- await executeFollowupAction(request.id, "call_back");
305
-
306
- const updated = getGuardianActionRequest(request.id);
307
- expect(updated!.followupState).toBe("completed");
308
- expect(updated!.followupCompletedAt).not.toBeNull();
309
- });
310
-
311
- test("follow-up states terminate correctly on failure", async () => {
312
- mockStartCallResult = { ok: false, error: "Provider error" };
313
- const { request } = createDispatchingRequest("exec-state-2", "call_back");
314
-
315
- await executeFollowupAction(request.id, "call_back");
316
-
317
- const updated = getGuardianActionRequest(request.id);
318
- expect(updated!.followupState).toBe("failed");
319
- expect(updated!.followupCompletedAt).not.toBeNull();
320
- });
321
- });
322
- });
@@ -1,373 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- mock.module("../util/logger.js", () => ({
4
- getLogger: () =>
5
- new Proxy({} as Record<string, unknown>, {
6
- get: () => () => {},
7
- }),
8
- }));
9
-
10
- import {
11
- createCallSession,
12
- createPendingQuestion,
13
- } from "../calls/call-store.js";
14
- import { getDb } from "../memory/db-connection.js";
15
- import { initializeDb } from "../memory/db-init.js";
16
- import {
17
- createGuardianActionRequest,
18
- expireGuardianActionRequest,
19
- finalizeFollowup,
20
- getGuardianActionRequest,
21
- markTimedOutWithReason,
22
- progressFollowupState,
23
- resolveGuardianActionRequest,
24
- startFollowupFromExpiredRequest,
25
- } from "../memory/guardian-action-store.js";
26
- import { conversations } from "../memory/schema.js";
27
-
28
- initializeDb();
29
-
30
- function ensureConversation(id: string): void {
31
- const db = getDb();
32
- const now = Date.now();
33
- db.insert(conversations)
34
- .values({
35
- id,
36
- title: `Conversation ${id}`,
37
- createdAt: now,
38
- updatedAt: now,
39
- })
40
- .run();
41
- }
42
-
43
- function resetTables(): void {
44
- const db = getDb();
45
- db.run("DELETE FROM guardian_action_deliveries");
46
- db.run("DELETE FROM guardian_action_requests");
47
- db.run("DELETE FROM call_pending_questions");
48
- db.run("DELETE FROM call_events");
49
- db.run("DELETE FROM call_sessions");
50
- db.run("DELETE FROM messages");
51
- db.run("DELETE FROM conversations");
52
- }
53
-
54
- function createTestRequest(convId: string) {
55
- ensureConversation(convId);
56
- const session = createCallSession({
57
- conversationId: convId,
58
- provider: "twilio",
59
- fromNumber: "+15550001111",
60
- toNumber: "+15550002222",
61
- });
62
- const pq = createPendingQuestion(session.id, "What is the gate code?");
63
- return createGuardianActionRequest({
64
- kind: "ask_guardian",
65
- sourceChannel: "phone",
66
- sourceConversationId: convId,
67
- callSessionId: session.id,
68
- pendingQuestionId: pq.id,
69
- questionText: pq.questionText,
70
- expiresAt: Date.now() + 60_000,
71
- });
72
- }
73
-
74
- describe("guardian-action-followup-store", () => {
75
- beforeEach(() => {
76
- resetTables();
77
- });
78
-
79
- // ── markTimedOutWithReason ──────────────────────────────────────────
80
-
81
- test("markTimedOutWithReason sets expired_reason correctly for call_timeout", () => {
82
- const request = createTestRequest("conv-followup-1");
83
- const result = markTimedOutWithReason(request.id, "call_timeout");
84
-
85
- expect(result).not.toBeNull();
86
- expect(result!.status).toBe("expired");
87
- expect(result!.expiredReason).toBe("call_timeout");
88
- });
89
-
90
- test("markTimedOutWithReason sets expired_reason correctly for sweep_timeout", () => {
91
- const request = createTestRequest("conv-followup-2");
92
- const result = markTimedOutWithReason(request.id, "sweep_timeout");
93
-
94
- expect(result).not.toBeNull();
95
- expect(result!.status).toBe("expired");
96
- expect(result!.expiredReason).toBe("sweep_timeout");
97
- });
98
-
99
- test("markTimedOutWithReason returns null for already-expired request", () => {
100
- const request = createTestRequest("conv-followup-3");
101
-
102
- // First call succeeds
103
- const first = markTimedOutWithReason(request.id, "call_timeout");
104
- expect(first).not.toBeNull();
105
-
106
- // Second call returns null (already expired)
107
- const second = markTimedOutWithReason(request.id, "sweep_timeout");
108
- expect(second).toBeNull();
109
-
110
- // Verify the original reason is preserved
111
- const reloaded = getGuardianActionRequest(request.id);
112
- expect(reloaded!.expiredReason).toBe("call_timeout");
113
- });
114
-
115
- test("markTimedOutWithReason returns null for answered request", () => {
116
- const request = createTestRequest("conv-followup-4");
117
- resolveGuardianActionRequest(request.id, "The code is 1234", "telegram");
118
-
119
- const result = markTimedOutWithReason(request.id, "call_timeout");
120
- expect(result).toBeNull();
121
- });
122
-
123
- // ── startFollowupFromExpiredRequest ─────────────────────────────────
124
-
125
- test("startFollowupFromExpiredRequest transitions correctly", () => {
126
- const request = createTestRequest("conv-followup-5");
127
- markTimedOutWithReason(request.id, "call_timeout");
128
-
129
- const result = startFollowupFromExpiredRequest(
130
- request.id,
131
- "The code is 5678",
132
- );
133
- expect(result).not.toBeNull();
134
- expect(result!.followupState).toBe("awaiting_guardian_choice");
135
- expect(result!.lateAnswerText).toBe("The code is 5678");
136
- expect(result!.lateAnsweredAt).toBeGreaterThan(0);
137
- });
138
-
139
- test("startFollowupFromExpiredRequest rejects pending request", () => {
140
- const request = createTestRequest("conv-followup-6");
141
-
142
- const result = startFollowupFromExpiredRequest(request.id, "Late answer");
143
- expect(result).toBeNull();
144
-
145
- // Verify followup_state unchanged
146
- const reloaded = getGuardianActionRequest(request.id);
147
- expect(reloaded!.followupState).toBe("none");
148
- });
149
-
150
- test("startFollowupFromExpiredRequest rejects answered request", () => {
151
- const request = createTestRequest("conv-followup-7");
152
- resolveGuardianActionRequest(request.id, "Original answer", "telegram");
153
-
154
- const result = startFollowupFromExpiredRequest(request.id, "Late answer");
155
- expect(result).toBeNull();
156
- });
157
-
158
- test("startFollowupFromExpiredRequest rejects already-in-followup request", () => {
159
- const request = createTestRequest("conv-followup-8");
160
- markTimedOutWithReason(request.id, "call_timeout");
161
- startFollowupFromExpiredRequest(request.id, "First late answer");
162
-
163
- // Second attempt should fail
164
- const result = startFollowupFromExpiredRequest(
165
- request.id,
166
- "Another late answer",
167
- );
168
- expect(result).toBeNull();
169
-
170
- // Verify original late answer preserved
171
- const reloaded = getGuardianActionRequest(request.id);
172
- expect(reloaded!.lateAnswerText).toBe("First late answer");
173
- });
174
-
175
- // ── progressFollowupState ───────────────────────────────────────────
176
-
177
- test("progressFollowupState valid transition: awaiting_guardian_choice -> dispatching", () => {
178
- const request = createTestRequest("conv-followup-9");
179
- markTimedOutWithReason(request.id, "call_timeout");
180
- startFollowupFromExpiredRequest(request.id, "Late answer");
181
-
182
- const result = progressFollowupState(
183
- request.id,
184
- "dispatching",
185
- "call_back",
186
- );
187
- expect(result).not.toBeNull();
188
- expect(result!.followupState).toBe("dispatching");
189
- expect(result!.followupAction).toBe("call_back");
190
- });
191
-
192
- test("progressFollowupState rejects terminal transition: awaiting_guardian_choice -> declined", () => {
193
- const request = createTestRequest("conv-followup-10");
194
- markTimedOutWithReason(request.id, "call_timeout");
195
- startFollowupFromExpiredRequest(request.id, "Late answer");
196
-
197
- // Terminal transitions must go through finalizeFollowup, not progressFollowupState
198
- const result = progressFollowupState(request.id, "declined", "decline");
199
- expect(result).toBeNull();
200
-
201
- // Verify state unchanged
202
- const reloaded = getGuardianActionRequest(request.id);
203
- expect(reloaded!.followupState).toBe("awaiting_guardian_choice");
204
- });
205
-
206
- test("progressFollowupState rejects invalid transition: none -> dispatching", () => {
207
- const request = createTestRequest("conv-followup-11");
208
- markTimedOutWithReason(request.id, "call_timeout");
209
-
210
- // followup_state is 'none', cannot jump to 'dispatching'
211
- const result = progressFollowupState(request.id, "dispatching");
212
- expect(result).toBeNull();
213
- });
214
-
215
- test("progressFollowupState rejects invalid transition: dispatching -> awaiting_guardian_choice", () => {
216
- const request = createTestRequest("conv-followup-12");
217
- markTimedOutWithReason(request.id, "call_timeout");
218
- startFollowupFromExpiredRequest(request.id, "Late answer");
219
- progressFollowupState(request.id, "dispatching", "call_back");
220
-
221
- // Cannot go back to awaiting_guardian_choice
222
- const result = progressFollowupState(
223
- request.id,
224
- "awaiting_guardian_choice",
225
- );
226
- expect(result).toBeNull();
227
- });
228
-
229
- test("progressFollowupState rejects transition from terminal state", () => {
230
- const request = createTestRequest("conv-followup-13");
231
- markTimedOutWithReason(request.id, "call_timeout");
232
- startFollowupFromExpiredRequest(request.id, "Late answer");
233
- progressFollowupState(request.id, "dispatching", "call_back");
234
- finalizeFollowup(request.id, "completed");
235
-
236
- // completed is terminal — progressFollowupState cannot leave it
237
- const result = progressFollowupState(request.id, "dispatching");
238
- expect(result).toBeNull();
239
- });
240
-
241
- test("progressFollowupState rejects none -> awaiting_guardian_choice even on expired request", () => {
242
- const request = createTestRequest("conv-followup-13b");
243
- markTimedOutWithReason(request.id, "call_timeout");
244
-
245
- // none -> awaiting_guardian_choice must only go through startFollowupFromExpiredRequest
246
- // (which atomically sets lateAnswerText and lateAnsweredAt)
247
- const result = progressFollowupState(
248
- request.id,
249
- "awaiting_guardian_choice",
250
- );
251
- expect(result).toBeNull();
252
-
253
- // Verify followup_state unchanged
254
- const reloaded = getGuardianActionRequest(request.id);
255
- expect(reloaded!.followupState).toBe("none");
256
- expect(reloaded!.status).toBe("expired");
257
- });
258
-
259
- test("progressFollowupState rejects non-expired request", () => {
260
- const request = createTestRequest("conv-followup-13c");
261
-
262
- // Request is still 'pending', not 'expired' — follow-up transitions must not apply
263
- const result = progressFollowupState(
264
- request.id,
265
- "awaiting_guardian_choice",
266
- );
267
- expect(result).toBeNull();
268
-
269
- // Verify followup_state unchanged
270
- const reloaded = getGuardianActionRequest(request.id);
271
- expect(reloaded!.followupState).toBe("none");
272
- expect(reloaded!.status).toBe("pending");
273
- });
274
-
275
- // ── finalizeFollowup ────────────────────────────────────────────────
276
-
277
- test("finalizeFollowup sets followup_completed_at for completed", () => {
278
- const request = createTestRequest("conv-followup-14");
279
- markTimedOutWithReason(request.id, "call_timeout");
280
- startFollowupFromExpiredRequest(request.id, "Late answer");
281
- progressFollowupState(request.id, "dispatching", "call_back");
282
-
283
- const result = finalizeFollowup(request.id, "completed");
284
- expect(result).not.toBeNull();
285
- expect(result!.followupState).toBe("completed");
286
- expect(result!.followupCompletedAt).toBeGreaterThan(0);
287
- });
288
-
289
- test("finalizeFollowup sets followup_completed_at for failed", () => {
290
- const request = createTestRequest("conv-followup-15");
291
- markTimedOutWithReason(request.id, "call_timeout");
292
- startFollowupFromExpiredRequest(request.id, "Late answer");
293
- progressFollowupState(request.id, "dispatching", "call_back");
294
-
295
- const result = finalizeFollowup(request.id, "failed");
296
- expect(result).not.toBeNull();
297
- expect(result!.followupState).toBe("failed");
298
- expect(result!.followupCompletedAt).toBeGreaterThan(0);
299
- });
300
-
301
- test("finalizeFollowup with declined from awaiting_guardian_choice", () => {
302
- const request = createTestRequest("conv-followup-16");
303
- markTimedOutWithReason(request.id, "call_timeout");
304
- startFollowupFromExpiredRequest(request.id, "Late answer");
305
-
306
- const result = finalizeFollowup(request.id, "declined");
307
- expect(result).not.toBeNull();
308
- expect(result!.followupState).toBe("declined");
309
- expect(result!.followupCompletedAt).toBeGreaterThan(0);
310
- });
311
-
312
- test("finalizeFollowup rejects invalid transition from none", () => {
313
- const request = createTestRequest("conv-followup-17");
314
- markTimedOutWithReason(request.id, "call_timeout");
315
-
316
- // followup_state is 'none', cannot finalize
317
- const result = finalizeFollowup(request.id, "completed");
318
- expect(result).toBeNull();
319
- });
320
-
321
- test("finalizeFollowup rejects non-expired request", () => {
322
- const request = createTestRequest("conv-followup-17b");
323
-
324
- // Request is still 'pending', not 'expired' — finalize must not apply
325
- const result = finalizeFollowup(request.id, "completed");
326
- expect(result).toBeNull();
327
-
328
- // Verify followup_state unchanged
329
- const reloaded = getGuardianActionRequest(request.id);
330
- expect(reloaded!.followupState).toBe("none");
331
- expect(reloaded!.status).toBe("pending");
332
- });
333
-
334
- // ── Existing behavior preserved ─────────────────────────────────────
335
-
336
- test("resolve/expire behavior unchanged: resolveGuardianActionRequest still works", () => {
337
- const request = createTestRequest("conv-followup-18");
338
-
339
- const resolved = resolveGuardianActionRequest(
340
- request.id,
341
- "Answer here",
342
- "telegram",
343
- "user-1",
344
- );
345
- expect(resolved).not.toBeNull();
346
- expect(resolved!.status).toBe("answered");
347
- expect(resolved!.answerText).toBe("Answer here");
348
- // Follow-up fields remain at defaults
349
- expect(resolved!.followupState).toBe("none");
350
- expect(resolved!.expiredReason).toBeNull();
351
- });
352
-
353
- test("expireGuardianActionRequest sets explicit reason", () => {
354
- const request = createTestRequest("conv-followup-19");
355
-
356
- expireGuardianActionRequest(request.id, "call_timeout");
357
-
358
- const reloaded = getGuardianActionRequest(request.id);
359
- expect(reloaded!.status).toBe("expired");
360
- expect(reloaded!.expiredReason).toBe("call_timeout");
361
- });
362
-
363
- test("new fields default correctly on freshly created request", () => {
364
- const request = createTestRequest("conv-followup-21");
365
-
366
- expect(request.expiredReason).toBeNull();
367
- expect(request.followupState).toBe("none");
368
- expect(request.lateAnswerText).toBeNull();
369
- expect(request.lateAnsweredAt).toBeNull();
370
- expect(request.followupAction).toBeNull();
371
- expect(request.followupCompletedAt).toBeNull();
372
- });
373
- });