@vellumai/assistant 0.11.8-dev.202609012313.7a1acce → 0.11.8-staging.2

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 (101) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/docs/guardian-request-flow.md +13 -13
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +18 -0
  4. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +21 -0
  5. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  6. package/openapi.yaml +0 -53
  7. package/package.json +1 -1
  8. package/src/__tests__/channel-delivery-store.test.ts +18 -129
  9. package/src/__tests__/channel-reply-delivery.test.ts +2 -126
  10. package/src/__tests__/channel-retry-sweep.test.ts +0 -168
  11. package/src/__tests__/conversation-error.test.ts +0 -15
  12. package/src/__tests__/conversation-load-history-repair.test.ts +0 -118
  13. package/src/__tests__/delete-propagation.test.ts +0 -300
  14. package/src/__tests__/guardian-card-withdrawal.test.ts +22 -0
  15. package/src/__tests__/guardian-gateway-sim.ts +23 -0
  16. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +2 -0
  17. package/src/__tests__/guardian-routing-invariants.test.ts +5 -0
  18. package/src/__tests__/list-messages-page-latest.test.ts +0 -85
  19. package/src/__tests__/outbound-slack-persistence.test.ts +0 -14
  20. package/src/__tests__/reaction-persistence.test.ts +198 -20
  21. package/src/__tests__/skill-load-tool.test.ts +0 -27
  22. package/src/__tests__/skills.test.ts +1 -34
  23. package/src/__tests__/slack-reaction-approvals.test.ts +97 -0
  24. package/src/__tests__/slack-reaction-guardian-approval.test.ts +307 -0
  25. package/src/approvals/AGENTS.md +1 -1
  26. package/src/channels/__tests__/gateway-guardian-requests.test.ts +23 -1
  27. package/src/channels/gateway-guardian-requests.ts +22 -0
  28. package/src/cli/commands/db/__tests__/status.test.ts +0 -22
  29. package/src/cli/commands/db/index.help.ts +1 -1
  30. package/src/cli/commands/db/status.ts +1 -172
  31. package/src/config/bundled-skills/acp/SKILL.md +3 -10
  32. package/src/config/bundled-skills/schedule/SKILL.md +11 -23
  33. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +1 -3
  34. package/src/daemon/conversation-error.ts +0 -2
  35. package/src/daemon/conversation-messaging.ts +0 -42
  36. package/src/daemon/conversation-process.ts +1 -0
  37. package/src/daemon/conversation.ts +2 -24
  38. package/src/daemon/handlers/config-channels.ts +3 -33
  39. package/src/daemon/handlers/shared.ts +0 -9
  40. package/src/daemon/process-message.ts +16 -87
  41. package/src/daemon/reaction-record.ts +9 -2
  42. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +1 -70
  43. package/src/live-voice/live-voice-session.ts +0 -3
  44. package/src/live-voice/protocol.ts +1 -20
  45. package/src/messaging/provider-message-metadata.ts +1 -13
  46. package/src/messaging/providers/slack/api.ts +1 -11
  47. package/src/messaging/reaction-envelopes.ts +0 -16
  48. package/src/monitoring/recovery/db.ts +0 -35
  49. package/src/monitoring/recovery/orphaned-channel-events.ts +22 -3
  50. package/src/monitoring/recovery/run-recovery.ts +2 -6
  51. package/src/monitoring/recovery/stale-processing.ts +20 -3
  52. package/src/notifications/AGENTS.md +1 -1
  53. package/src/notifications/adapters/discord.ts +1 -1
  54. package/src/notifications/guardian-delivery-recorder.ts +9 -5
  55. package/src/permissions/confirmation-guardian-request.ts +2 -2
  56. package/src/permissions/prompter.ts +1 -1
  57. package/src/persistence/delivery-crud.ts +28 -224
  58. package/src/persistence/delivery-status.ts +2 -21
  59. package/src/persistence/schema/conversations.ts +2 -71
  60. package/src/persistence/steps.ts +0 -4
  61. package/src/providers/__tests__/dispatch-connection-routing.test.ts +0 -50
  62. package/src/providers/__tests__/registry-native-web-search.test.ts +2 -49
  63. package/src/providers/call-site-routing.ts +4 -5
  64. package/src/providers/connection-resolution.ts +1 -29
  65. package/src/providers/inference/auth.ts +6 -6
  66. package/src/providers/registry.ts +7 -10
  67. package/src/providers/routing-identity.ts +1 -2
  68. package/src/providers/vellum-model-routing.ts +2 -3
  69. package/src/runtime/AGENTS.md +3 -14
  70. package/src/runtime/channel-approval-types.ts +5 -1
  71. package/src/runtime/channel-reply-delivery.ts +42 -99
  72. package/src/runtime/channel-retry-sweep.ts +0 -94
  73. package/src/runtime/guardian-reply-router.ts +75 -4
  74. package/src/runtime/question-request-guardian-bridge.ts +2 -1
  75. package/src/runtime/routes/acp-routes.ts +1 -1
  76. package/src/runtime/routes/channel-route-shared.ts +39 -0
  77. package/src/runtime/routes/channel-verification-routes.ts +1 -4
  78. package/src/runtime/routes/conversation-routes.ts +1 -0
  79. package/src/runtime/routes/guardian-approval-interception.ts +5 -0
  80. package/src/runtime/routes/inbound-message-handler.ts +32 -125
  81. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +4 -141
  82. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -77
  83. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +48 -23
  84. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +94 -452
  85. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +102 -307
  86. package/src/tools/client-os.ts +0 -9
  87. package/src/tools/host-filesystem/edit.ts +2 -2
  88. package/src/tools/host-filesystem/read.ts +2 -2
  89. package/src/tools/host-filesystem/transfer.ts +2 -2
  90. package/src/tools/host-filesystem/write.ts +2 -2
  91. package/src/tools/host-terminal/host-shell.ts +2 -2
  92. package/src/tools/skills/load.ts +1 -1
  93. package/src/tools/ui-surface/definitions.ts +3 -4
  94. package/src/__tests__/provider-meta-persistence.test.ts +0 -67
  95. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +0 -208
  96. package/src/monitoring/recovery/stranded-delivery-events.ts +0 -68
  97. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +0 -26
  98. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +0 -52
  99. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +0 -84
  100. package/src/persistence/schema-contract.test.ts +0 -78
  101. package/src/persistence/schema-contract.ts +0 -96
package/ARCHITECTURE.md CHANGED
@@ -120,7 +120,7 @@ Scoped approval grants allow a guardian's approval decision on one channel (e.g.
120
120
 
121
121
  ### Guardian Decision Primitive
122
122
 
123
- All guardian approval decisions, regardless of how they arrive, route through a single primitive in `src/approvals/guardian-decision-primitive.ts`, which centralizes decision logic for callback button handlers, the conversational approval engine, and channel text replies.
123
+ All guardian approval decisions — regardless of how they arrive — route through a single primitive in `src/approvals/guardian-decision-primitive.ts`, which centralizes decision logic for callback button handlers, the conversational approval engine, and channel reactions/text.
124
124
 
125
125
  **Core API:**
126
126
 
@@ -28,10 +28,10 @@ anti-pattern was retired in #35642 and again in the ask_question redesign).
28
28
  telegram: inline keyboard; slack: blocks; vellum: conversation card via approval-card-builder)
29
29
  card deliveries recorded per channel (guardian-delivery-recorder.ts → guardian_request_deliveries)
30
30
  │
31
- ▼ user responds: button tap / "CODE <reply>" / bare text
31
+ ▼ user responds: button tap / emoji reaction / "CODE <reply>" / bare text
32
32
  guardian reply router (runtime/guardian-reply-router.ts, invoked from
33
- callbacks → request codes → bare answer → routes/inbound-stages/guardian-reply-intercept.ts,
34
- explicit approve/reject → NL BEFORE background dispatch: replies to parked
33
+ reactions → callbacks → request codes → routes/inbound-stages/guardian-reply-intercept.ts,
34
+ bare answer → explicit approve/reject → NL BEFORE background dispatch — replies to parked
35
35
  │ prompts resolve inline, never deferred)
36
36
  ▼ one decision primitive
37
37
  applyGuardianDecision (approvals/guardian-decision-primitive.ts:
@@ -45,14 +45,14 @@ anti-pattern was retired in #35642 and again in the ask_question redesign).
45
45
 
46
46
  ## Who owns what
47
47
 
48
- | Concern | Owner | Never |
49
- | --------------------------------------- | ------------------------------------------------- | ----------------------------------- |
50
- | The "what": card text, actions, options | broadcaster context build (once per broadcast) | built per-adapter |
51
- | The "how": channel-native rendering | `notifications/adapters/<channel>` | domain logic, payload parsing |
52
- | Request state | gateway `guardian_requests` (+ deliveries) | daemon-side request tables |
53
- | Decisions | `applyGuardianDecision` (CAS, atomic ACL outcome) | inline decision logic at call sites |
54
- | Kind-specific follow-through | resolver registry (`kind` → resolver) | switch statements in the router |
55
- | Reply understanding | guardian reply router (codes, buttons, modes) | per-feature inbound intercepts |
48
+ | Concern | Owner | Never |
49
+ | --------------------------------------- | -------------------------------------------------------- | ----------------------------------- |
50
+ | The "what": card text, actions, options | broadcaster context build (once per broadcast) | built per-adapter |
51
+ | The "how": channel-native rendering | `notifications/adapters/<channel>` | domain logic, payload parsing |
52
+ | Request state | gateway `guardian_requests` (+ deliveries) | daemon-side request tables |
53
+ | Decisions | `applyGuardianDecision` (CAS, atomic ACL outcome) | inline decision logic at call sites |
54
+ | Kind-specific follow-through | resolver registry (`kind` → resolver) | switch statements in the router |
55
+ | Reply understanding | guardian reply router (codes, buttons, reactions, modes) | per-feature inbound intercepts |
56
56
 
57
57
  ## The canonical home-feed projection
58
58
 
@@ -186,8 +186,8 @@ chat the turn is running in. On Slack that chat can be a shared room, and the
186
186
  card carries the tool, a command preview and live buttons.
187
187
  `resolveGuardianPromptDelivery` addresses it to the guardian's bound DM
188
188
  instead, by chat id rather than user id because that address is written to the
189
- delivery row and read back to scope plain-text replies and edit the decided
190
- card. It returns the address and its route together, since
189
+ delivery row and read back to match reactions, scope plain-text replies and
190
+ edit the decided card. It returns the address and its route together, since
191
191
  the turn's own callback carries a `threadTs` naming a thread that does not
192
192
  exist in the DM. When no private address resolves it returns nothing and the
193
193
  prompt is left to the in-app confirmation, because the room is the disclosure
@@ -16,6 +16,7 @@ import {
16
16
  ExpireGuardianRequestIpcParamsSchema,
17
17
  ExpireInteractionBoundIpcResponseSchema,
18
18
  GetGuardianRequestByCallSessionIpcParamsSchema,
19
+ GetGuardianRequestByDestinationMessageIpcParamsSchema,
19
20
  GetGuardianRequestByPendingQuestionIpcParamsSchema,
20
21
  GUARDIAN_REQUESTS_IPC_METHODS,
21
22
  GuardianRequestAclOutcomeSchema,
@@ -481,6 +482,23 @@ describe("delivery + destination IPC schemas", () => {
481
482
  );
482
483
  });
483
484
 
485
+ test("get_by_destination_message requires all three keys", () => {
486
+ const params = {
487
+ channel: "telegram",
488
+ chatId: "tg-chat-guardian",
489
+ messageId: "msg-42",
490
+ };
491
+ expect(
492
+ GetGuardianRequestByDestinationMessageIpcParamsSchema.parse(params),
493
+ ).toEqual(params);
494
+ expect(() =>
495
+ GetGuardianRequestByDestinationMessageIpcParamsSchema.parse({
496
+ channel: "telegram",
497
+ chatId: "tg-chat-guardian",
498
+ }),
499
+ ).toThrow();
500
+ });
501
+
484
502
  test("list_pending_by_destination accepts either addressing form", () => {
485
503
  const byConversation = { conversationId: "conv-1", channel: "telegram" };
486
504
  expect(
@@ -15,6 +15,11 @@
15
15
  * daemon's client-facing HTTP surface owns the distinct `guardian_actions_*`
16
16
  * operationIds (`guardian_actions_pending` / `guardian_actions_decision`),
17
17
  * which do not change.
18
+ *
19
+ * Destination lookups are deliberately split into a single-message lookup
20
+ * (`get_by_destination_message`) and a pending-list read
21
+ * (`list_pending_by_destination`) so each response schema has exactly one
22
+ * shape instead of a params-dependent polymorphic result.
18
23
  */
19
24
 
20
25
  import { z } from "zod";
@@ -189,6 +194,7 @@ export const GUARDIAN_REQUESTS_IPC_METHODS = {
189
194
  updateDelivery: "guardian_requests_update_delivery",
190
195
  listDeliveries: "guardian_requests_list_deliveries",
191
196
  listDeliveriesByChat: "guardian_requests_list_deliveries_by_chat",
197
+ getByDestinationMessage: "guardian_requests_get_by_destination_message",
192
198
  listPendingByDestination: "guardian_requests_list_pending_by_destination",
193
199
  listPendingByScope: "guardian_requests_list_pending_by_scope",
194
200
  inScope: "guardian_requests_in_scope",
@@ -621,6 +627,21 @@ export type GuardianRequestDeliveryListIpcResponse = z.infer<
621
627
  // Destination + scope lookups
622
628
  // ---------------------------------------------------------------------------
623
629
 
630
+ /**
631
+ * Request for `guardian_requests_get_by_destination_message` — reaction
632
+ * routing: recover the pending request whose delivered card is the reacted-to
633
+ * message.
634
+ */
635
+ export const GetGuardianRequestByDestinationMessageIpcParamsSchema = z.object({
636
+ channel: z.string().min(1),
637
+ chatId: z.string().min(1),
638
+ messageId: z.string().min(1),
639
+ });
640
+
641
+ export type GetGuardianRequestByDestinationMessageIpcParams = z.infer<
642
+ typeof GetGuardianRequestByDestinationMessageIpcParamsSchema
643
+ >;
644
+
624
645
  /**
625
646
  * Request for `guardian_requests_list_pending_by_destination`. Two forms:
626
647
  * by destination conversation (`conversationId`, optionally narrowed by
@@ -245,6 +245,7 @@ export {
245
245
  ExpireInteractionBoundIpcResponseSchema,
246
246
  GetGuardianRequestByCallSessionIpcParamsSchema,
247
247
  GetGuardianRequestByCodeIpcParamsSchema,
248
+ GetGuardianRequestByDestinationMessageIpcParamsSchema,
248
249
  GetGuardianRequestByPendingQuestionIpcParamsSchema,
249
250
  GetGuardianRequestIpcParamsSchema,
250
251
  GUARDIAN_REQUESTS_IPC_METHODS,
@@ -285,6 +286,7 @@ export type {
285
286
  ExpireInteractionBoundIpcResponse,
286
287
  GetGuardianRequestByCallSessionIpcParams,
287
288
  GetGuardianRequestByCodeIpcParams,
289
+ GetGuardianRequestByDestinationMessageIpcParams,
288
290
  GetGuardianRequestByPendingQuestionIpcParams,
289
291
  GetGuardianRequestIpcParams,
290
292
  GuardianRequestAclOutcome,
package/openapi.yaml CHANGED
@@ -4076,59 +4076,6 @@ paths:
4076
4076
  responses:
4077
4077
  "200":
4078
4078
  description: Successful response
4079
- content:
4080
- application/json:
4081
- schema:
4082
- type: object
4083
- properties:
4084
- success:
4085
- type: boolean
4086
- bound:
4087
- type: boolean
4088
- channel:
4089
- type: string
4090
- enum:
4091
- - telegram
4092
- - phone
4093
- - vellum
4094
- - whatsapp
4095
- - slack
4096
- - email
4097
- - platform
4098
- - a2a
4099
- - discord
4100
- - plugin
4101
- assistantId:
4102
- type: string
4103
- hasPendingChallenge:
4104
- type: boolean
4105
- guardianExternalUserId:
4106
- type: string
4107
- guardianDeliveryChatId:
4108
- type: string
4109
- guardianUsername:
4110
- type: string
4111
- guardianDisplayName:
4112
- type: string
4113
- verificationSessionId:
4114
- type: string
4115
- expiresAt:
4116
- type: number
4117
- nextResendAt:
4118
- anyOf:
4119
- - type: number
4120
- - type: "null"
4121
- sendCount:
4122
- type: number
4123
- pendingBootstrap:
4124
- type: boolean
4125
- required:
4126
- - success
4127
- - bound
4128
- - channel
4129
- - assistantId
4130
- - hasPendingChallenge
4131
- additionalProperties: false
4132
4079
  /v1/channels/available:
4133
4080
  get:
4134
4081
  operationId: channels_available_get
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.8-dev.202609012313.7a1acce",
3
+ "version": "0.11.8-staging.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -10,11 +10,10 @@ import { getDb } from "../persistence/db-connection.js";
10
10
  import { initializeDb } from "../persistence/db-init.js";
11
11
  import {
12
12
  clearPayload,
13
- findMessageByProviderMessageId,
13
+ findConversationByProviderMessageId,
14
14
  findMessageBySourceId,
15
15
  linkMessage,
16
16
  recordInbound,
17
- recordOutboundPost,
18
17
  storePayload,
19
18
  } from "../persistence/delivery-crud.js";
20
19
  import {
@@ -53,7 +52,6 @@ await initializeDb();
53
52
  function resetTables() {
54
53
  const db = getDb();
55
54
  db.run("DELETE FROM channel_inbound_events");
56
- db.run("DELETE FROM channel_outbound_posts");
57
55
  db.run("DELETE FROM messages");
58
56
  db.run("DELETE FROM conversation_keys");
59
57
  db.run("DELETE FROM external_conversation_bindings");
@@ -149,144 +147,35 @@ describe("channel-delivery-store", () => {
149
147
  .run();
150
148
 
151
149
  expect(
152
- findMessageByProviderMessageId("discord", chatId, "1234567890123456789"),
153
- ).toEqual({
154
- messageId: "assistant-post-1",
155
- conversationId: minted.conversationId,
156
- });
150
+ findConversationByProviderMessageId(
151
+ "discord",
152
+ chatId,
153
+ "1234567890123456789",
154
+ ),
155
+ ).toBe(minted.conversationId);
157
156
  // A split reply posts several provider messages from one row; any of
158
157
  // their ids resolves it.
159
158
  expect(
160
- findMessageByProviderMessageId("discord", chatId, "2234567890123456789"),
161
- ).toEqual({
162
- messageId: "assistant-post-1",
163
- conversationId: minted.conversationId,
164
- });
159
+ findConversationByProviderMessageId(
160
+ "discord",
161
+ chatId,
162
+ "2234567890123456789",
163
+ ),
164
+ ).toBe(minted.conversationId);
165
165
  // The scan is scoped to the reaction's own channel address: the same id
166
166
  // is invisible from another channel's or another chat's key space.
167
167
  expect(
168
- findMessageByProviderMessageId("telegram", chatId, "1234567890123456789"),
169
- ).toBeNull();
170
- expect(
171
- findMessageByProviderMessageId(
172
- "discord",
173
- "other-chat",
168
+ findConversationByProviderMessageId(
169
+ "telegram",
170
+ chatId,
174
171
  "1234567890123456789",
175
172
  ),
176
173
  ).toBeNull();
177
- });
178
-
179
- test("linked inbound rows are excluded from the provider-id scan; crash-window rows resolve", () => {
180
- const chatId = "chat-scan-budget";
181
- const minted = recordInbound("telegram", chatId, "evt-linked", {
182
- sourceMessageId: "tg-linked-1",
183
- });
184
- const envelope = (messageId: string) =>
185
- JSON.stringify({
186
- providerMeta: JSON.stringify({
187
- source: "telegram",
188
- conversationExternalId: chatId,
189
- messageId,
190
- eventKind: "message",
191
- }),
192
- });
193
-
194
- // A normally-ingested inbound row: linked to its event, so the
195
- // inbound-event index owns its resolution and the fallback scan must
196
- // not spend budget on it.
197
- insertMessage("linked-user-row", minted.conversationId, {});
198
- getDb()
199
- .update(messages)
200
- .set({ metadata: envelope("tg-linked-1") })
201
- .where(eq(messages.id, "linked-user-row"))
202
- .run();
203
- linkMessage(minted.eventId, "linked-user-row");
204
- expect(
205
- findMessageByProviderMessageId("telegram", chatId, "tg-linked-1"),
206
- ).toBeNull();
207
-
208
- // A crash-window row: persisted with its envelope but its event link
209
- // never landed. The fallback is the only path that can resolve it.
210
- insertMessage("orphan-user-row", minted.conversationId, {});
211
- getDb()
212
- .update(messages)
213
- .set({ metadata: envelope("tg-orphan-1") })
214
- .where(eq(messages.id, "orphan-user-row"))
215
- .run();
216
- expect(
217
- findMessageByProviderMessageId("telegram", chatId, "tg-orphan-1"),
218
- ).toEqual({
219
- messageId: "orphan-user-row",
220
- conversationId: minted.conversationId,
221
- });
222
-
223
- // A legacy linked row whose event carries no source_message_id:
224
- // findMessageBySourceId matches only that column, so the fallback must
225
- // still admit the row or reactions to it become unresolvable.
226
- const legacy = recordInbound("telegram", chatId, "evt-legacy");
227
- insertMessage("legacy-user-row", legacy.conversationId, {});
228
- getDb()
229
- .update(messages)
230
- .set({ metadata: envelope("tg-legacy-1") })
231
- .where(eq(messages.id, "legacy-user-row"))
232
- .run();
233
- linkMessage(legacy.eventId, "legacy-user-row");
234
- expect(
235
- findMessageByProviderMessageId("telegram", chatId, "tg-legacy-1"),
236
- ).toEqual({
237
- messageId: "legacy-user-row",
238
- conversationId: legacy.conversationId,
239
- });
240
- });
241
-
242
- test("the outbound-posts index resolves a post exactly, at any age", () => {
243
- const chatId = "999000111222333444";
244
- const minted = recordInbound("discord", chatId, "evt-index-seed");
245
- const db = getDb();
246
- // The row's envelope names nothing: resolution comes from the index
247
- // alone, proving the table is the contract rather than a cache in
248
- // front of the scan.
249
- db.insert(messages)
250
- .values({
251
- id: "assistant-indexed-post",
252
- conversationId: minted.conversationId,
253
- role: "assistant",
254
- content: "indexed reply",
255
- createdAt: Date.now(),
256
- metadata: null,
257
- })
258
- .run();
259
- recordOutboundPost({
260
- sourceChannel: "discord",
261
- externalChatId: chatId,
262
- providerMessageId: "555666777888999000",
263
- messageId: "assistant-indexed-post",
264
- conversationId: minted.conversationId,
265
- });
266
- // Idempotent on redelivery.
267
- recordOutboundPost({
268
- sourceChannel: "discord",
269
- externalChatId: chatId,
270
- providerMessageId: "555666777888999000",
271
- messageId: "assistant-indexed-post",
272
- conversationId: minted.conversationId,
273
- });
274
-
275
- expect(
276
- findMessageByProviderMessageId("discord", chatId, "555666777888999000"),
277
- ).toEqual({
278
- messageId: "assistant-indexed-post",
279
- conversationId: minted.conversationId,
280
- });
281
- // Channel and chat scoping hold on the index path too.
282
- expect(
283
- findMessageByProviderMessageId("telegram", chatId, "555666777888999000"),
284
- ).toBeNull();
285
174
  expect(
286
- findMessageByProviderMessageId(
175
+ findConversationByProviderMessageId(
287
176
  "discord",
288
177
  "other-chat",
289
- "555666777888999000",
178
+ "1234567890123456789",
290
179
  ),
291
180
  ).toBeNull();
292
181
  });
@@ -32,13 +32,6 @@ type UpdateMessageMetadataCall = {
32
32
  };
33
33
  const updateMessageMetadataCalls: UpdateMessageMetadataCall[] = [];
34
34
 
35
- /**
36
- * Number of leading `updateMessageMetadata` calls that throw a transient
37
- * SQLite error before the mock starts writing. Lets a test assert that a lost
38
- * reconciliation write is recovered rather than forfeited.
39
- */
40
- let metadataWriteFailuresRemaining = 0;
41
-
42
35
  /** Per-test override for the synthetic Slack `ts` returned by deliverChannelReply. */
43
36
  let nextDeliveryTs: string | null = null;
44
37
 
@@ -152,14 +145,6 @@ mock.module("../persistence/conversation-crud.js", () => ({
152
145
  messageId: string,
153
146
  updates: Record<string, unknown>,
154
147
  ) => {
155
- if (metadataWriteFailuresRemaining > 0) {
156
- metadataWriteFailuresRemaining -= 1;
157
- // Shaped like bun:sqlite's error so `withSqliteRetry` classifies it as
158
- // transient contention rather than a fatal write.
159
- throw Object.assign(new Error("database is locked"), {
160
- code: "SQLITE_BUSY",
161
- });
162
- }
163
148
  updateMessageMetadataCalls.push({ messageId, updates });
164
149
  const row = conversationMessages.find((m) => m.id === messageId);
165
150
  if (!row) {
@@ -174,15 +159,6 @@ mock.module("../persistence/conversation-crud.js", () => ({
174
159
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
175
160
  }));
176
161
 
177
- // The reconciler writes the outbound-posts index through delivery-crud;
178
- // stub it so this suite stays DB-free while capturing the writes.
179
- const recordedOutboundPosts: Array<Record<string, string>> = [];
180
- mock.module("../persistence/delivery-crud.js", () => ({
181
- recordOutboundPost: (post: Record<string, string>) => {
182
- recordedOutboundPosts.push(post);
183
- },
184
- }));
185
-
186
162
  mock.module("../persistence/attachments-store.js", () => ({
187
163
  getAttachmentMetadataForMessage: (messageId: string) =>
188
164
  attachmentsByMessageId.get(messageId) ?? [],
@@ -207,7 +183,6 @@ describe("channel-reply-delivery", () => {
207
183
  conversationMessages.length = 0;
208
184
  attachmentsByMessageId.clear();
209
185
  updateMessageMetadataCalls.length = 0;
210
- metadataWriteFailuresRemaining = 0;
211
186
  nextDeliveryTs = null;
212
187
  renderedHistoryContentQueue.length = 0;
213
188
  renderedHistoryContent = {
@@ -812,9 +787,7 @@ describe("channel-reply-delivery", () => {
812
787
  attachments,
813
788
  startFromSegment: 1,
814
789
  messageTs: "1700000000.000055",
815
- onMessageTs: (ts) => {
816
- seenTs.push(ts);
817
- },
790
+ onMessageTs: (ts) => seenTs.push(ts),
818
791
  });
819
792
 
820
793
  expect(deliveryCalls).toHaveLength(1);
@@ -1281,9 +1254,7 @@ describe("channel-reply-delivery", () => {
1281
1254
  "http://gateway/deliver/slack",
1282
1255
  "assistant-compose",
1283
1256
  {
1284
- onMessageTs: (ts) => {
1285
- callerTsSeen.push(ts);
1286
- },
1257
+ onMessageTs: (ts) => callerTsSeen.push(ts),
1287
1258
  },
1288
1259
  );
1289
1260
 
@@ -1323,100 +1294,5 @@ describe("channel-reply-delivery", () => {
1323
1294
  expect(parsed?.source).toBe("slack");
1324
1295
  expect(parsed?.eventKind).toBe("message");
1325
1296
  });
1326
-
1327
- it("retries a reconciliation write that hits transient SQLite contention", async () => {
1328
- // The stamp is the only durable record of the post's ts: no later sweep
1329
- // heals a miss once the event is marked delivered, so a SQLITE_BUSY on
1330
- // the first attempt must not lose it.
1331
- pushPartialAssistantRow("conv-busy", "msg-busy", "C333");
1332
- nextDeliveryTs = "1700000800.000444";
1333
- metadataWriteFailuresRemaining = 1;
1334
-
1335
- await deliverReplyViaCallback(
1336
- "conv-busy",
1337
- "C333",
1338
- "http://gateway/deliver/slack",
1339
- "assistant-busy",
1340
- );
1341
-
1342
- expect(updateMessageMetadataCalls.length).toBe(1);
1343
- const merged = updateMessageMetadataCalls[0].updates.slackMeta as string;
1344
- const parsed = JSON.parse(merged) as Record<string, unknown>;
1345
- expect(parsed.channelTs).toBe("1700000800.000444");
1346
- });
1347
-
1348
- it("stamps from a later segment when the first segment's write is lost", async () => {
1349
- // The row records the first ts that reaches a DURABLE write, not the
1350
- // first ts observed. A first-invocation latch would forfeit channelTs
1351
- // forever when that write fails, which is the reconciliation gap this
1352
- // reproduces: the reply posts successfully and the row never gains its
1353
- // own id, so history projects no `slackMessage` for it.
1354
- pushPartialAssistantRow("conv-lost", "msg-lost", "C444");
1355
- renderedHistoryContent = {
1356
- text: "AlphaBeta",
1357
- textSegments: ["Alpha", "Beta"],
1358
- toolCalls: [],
1359
- toolCallsBeforeText: false,
1360
- contentOrder: ["text:0", "tool:0", "text:1"],
1361
- surfaces: [],
1362
- thinkingSegments: [],
1363
- };
1364
- nextDeliveryTs = "1700000900.000555";
1365
- // Exhaust the initial attempt plus every `withSqliteRetry` retry, so the
1366
- // first segment's reconciliation is genuinely swallowed.
1367
- metadataWriteFailuresRemaining = 4;
1368
-
1369
- await deliverReplyViaCallback(
1370
- "conv-lost",
1371
- "C444",
1372
- "http://gateway/deliver/slack",
1373
- "assistant-lost",
1374
- );
1375
-
1376
- // Both segments posted; the surviving write carries the second post's ts
1377
- // rather than leaving the row without any id of its own.
1378
- expect(deliveryCalls.length).toBe(2);
1379
- expect(updateMessageMetadataCalls.length).toBe(1);
1380
- const merged = updateMessageMetadataCalls[0].updates.slackMeta as string;
1381
- const { readSlackMetadata } =
1382
- await import("../messaging/providers/slack/message-metadata.js");
1383
- expect(readSlackMetadata(merged)).not.toBeNull();
1384
- });
1385
-
1386
- it("commits the reconciliation write before posting the next segment", async () => {
1387
- // `onMessageTs` is awaited, so the row is durable at each segment
1388
- // boundary. Otherwise a crash between two posts of a split reply loses
1389
- // the stamp for a message the reader can already see.
1390
- pushPartialAssistantRow("conv-order", "msg-order", "C555");
1391
- renderedHistoryContent = {
1392
- text: "AlphaBeta",
1393
- textSegments: ["Alpha", "Beta"],
1394
- toolCalls: [],
1395
- toolCallsBeforeText: false,
1396
- contentOrder: ["text:0", "tool:0", "text:1"],
1397
- surfaces: [],
1398
- thinkingSegments: [],
1399
- };
1400
- nextDeliveryTs = "1700001000.000666";
1401
-
1402
- await deliverReplyViaCallback(
1403
- "conv-order",
1404
- "C555",
1405
- "http://gateway/deliver/slack",
1406
- "assistant-order",
1407
- );
1408
-
1409
- // The row already parses through the strict reader by the time the
1410
- // second POST goes out, which is what makes the second segment a no-op.
1411
- const row = conversationMessages.find((m) => m.id === "msg-order");
1412
- const envelope = JSON.parse(row?.metadata ?? "{}") as Record<
1413
- string,
1414
- unknown
1415
- >;
1416
- const { readSlackMetadata } =
1417
- await import("../messaging/providers/slack/message-metadata.js");
1418
- expect(readSlackMetadata(envelope.slackMeta as string)).not.toBeNull();
1419
- expect(updateMessageMetadataCalls.length).toBe(1);
1420
- });
1421
1297
  });
1422
1298
  });