@teamscala/service-boot 0.23.411 → 0.23.412

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/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.411",
9
+ "version": "0.23.412",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -148,21 +148,16 @@ export function formatSpanMinutes(minutes: number): string {
148
148
  }
149
149
 
150
150
  /**
151
- * The WhatsApp message id from a GOWA /send/message result. The canonical
152
- * shape (from the spec the api-gowa adapter is generated from,
153
- * SendResponse = { code, message, results: { message_id, status } }) is
154
- * `results.message_id`; the legacy shape returned the bare id string as
155
- * `results`. Delivery is only VERIFIED on a real id — `"true"`, a status
156
- * string, or an empty body is not a message id and must not be recorded as
157
- * delivered.
151
+ * The WhatsApp message id from a messaging_whatsapp_send result. The
152
+ * intrinsic (scala-mcp / messaging-mcp-boot) returns
153
+ * `{ ok, message_id, error }` — the Rust scala-messaging-service owns the
154
+ * channel, picks the device, and records the send in messaging_logs itself.
155
+ * Delivery is only VERIFIED on a real id — a `"true"` ack, an error body, or
156
+ * an empty body is not a message id and must not be recorded as delivered.
158
157
  */
159
- export function extractGowaMessageId(sendResult: unknown): string | null {
160
- const results = (sendResult as { results?: unknown } | undefined)?.results;
161
- if (typeof results === "string") {
162
- return /^[A-Z0-9]{8,}$/i.test(results) ? results : null;
163
- }
164
- if (results && typeof results === "object") {
165
- const id = (results as { message_id?: unknown }).message_id;
158
+ export function extractWhatsAppMessageId(sendResult: unknown): string | null {
159
+ if (sendResult && typeof sendResult === "object") {
160
+ const id = (sendResult as { message_id?: unknown }).message_id;
166
161
  if (typeof id === "string" && id.length > 0) return id;
167
162
  }
168
163
  return null;
@@ -12,7 +12,7 @@ import { describe, expect, test } from "bun:test";
12
12
  import {
13
13
  composeEscalationMessage,
14
14
  decideSweep,
15
- extractGowaMessageId,
15
+ extractWhatsAppMessageId,
16
16
  formatEscalationSample,
17
17
  type EscalationEpisode,
18
18
  type FaultAggregate,
@@ -186,23 +186,20 @@ describe("decideSweep — a future-dated lastSeen is not recurrence evidence", (
186
186
  });
187
187
  });
188
188
 
189
- describe("extractGowaMessageId — delivery verification", () => {
190
- test("the canonical GOWA SendResponse shape yields results.message_id", () => {
191
- expect(extractGowaMessageId({ code: "SUCCESS", results: { message_id: "3EB089B9D6ADD58153C561", status: "sent" } })).toBe(
189
+ describe("extractWhatsAppMessageId — delivery verification", () => {
190
+ test("the messaging_whatsapp_send result shape yields message_id", () => {
191
+ expect(extractWhatsAppMessageId({ ok: true, message_id: "3EB089B9D6ADD58153C561" })).toBe(
192
192
  "3EB089B9D6ADD58153C561",
193
193
  );
194
194
  });
195
195
 
196
- test("the legacy bare-string results shape yields the id", () => {
197
- expect(extractGowaMessageId({ results: "3EB089B9D6ADD58153C561" })).toBe("3EB089B9D6ADD58153C561");
198
- });
199
-
200
- test("a 'true' ack is NOT a message id — delivery stays unverified", () => {
196
+ test("a 'true' ack or an error body is NOT a message id — delivery stays unverified", () => {
201
197
  // A boolean-ish ack string must never be recorded as a delivered id:
202
198
  // that would mark the episode delivered with an unfollowable handle.
203
- expect(extractGowaMessageId({ results: "true" })).toBeNull();
204
- expect(extractGowaMessageId({ code: "SUCCESS", message: "sent", results: { status: "sent" } })).toBeNull();
205
- expect(extractGowaMessageId({ code: "SUCCESS" })).toBeNull();
199
+ expect(extractWhatsAppMessageId({ ok: true })).toBeNull();
200
+ expect(extractWhatsAppMessageId({ ok: false, error: "no device" })).toBeNull();
201
+ expect(extractWhatsAppMessageId("true")).toBeNull();
202
+ expect(extractWhatsAppMessageId(null)).toBeNull();
206
203
  });
207
204
  });
208
205
 
@@ -19,7 +19,7 @@ import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
19
19
  import {
20
20
  composeEscalationMessage,
21
21
  decideSweep,
22
- extractGowaMessageId,
22
+ extractWhatsAppMessageId,
23
23
  formatEscalationSample,
24
24
  type EscalationEpisode,
25
25
  type FaultAggregate,
@@ -276,20 +276,6 @@ async function resolveOperatorRelay(): Promise<OperatorRelayBot> {
276
276
  return bot as OperatorRelayBot;
277
277
  }
278
278
 
279
- /** GOWA device whose JID matches the PA number, so the escalation is sent BY the PA. */
280
- async function resolvePaDeviceId(paPhoneDigits: string): Promise<string | undefined> {
281
- const { userId, organisationId } = await getSystemIdentity();
282
- const systemCtx: ExecutionContext = { userId, organisationId } as ExecutionContext;
283
- const { forwardToolToCentralMcp } = await import("@teamscala/chat-completion/central-tool-forwarder");
284
- const res = (await forwardToolToCentralMcp(
285
- "api_gowa_devices",
286
- { action: "listDevices" },
287
- systemCtx,
288
- )) as { results?: Array<{ id: string; jid?: string }> } | undefined;
289
- const match = (res?.results ?? []).find((device) => (device.jid ?? "").includes(paPhoneDigits));
290
- return match?.id;
291
- }
292
-
293
279
  async function deliverEscalation(
294
280
  kind: string,
295
281
  aggregate: FaultAggregate,
@@ -316,7 +302,12 @@ async function deliverEscalation(
316
302
  },
317
303
  });
318
304
 
319
- // 2. Resolve the PA path (bot → operator phone → PA device) and send.
305
+ // 2. Resolve the operator relay and send. The send surface is the
306
+ // `messaging_whatsapp_send` intrinsic (scala-mcp / messaging-mcp-boot),
307
+ // which delegates to the Rust scala-messaging-service — the channel owner
308
+ // that picks the device and records the send in messaging_logs. The retired
309
+ // api_gowa_message tool (deactivated 2026-08-26) is no longer callable;
310
+ // naming it here left every escalation send failing with "Unknown tool".
320
311
  const bot = await resolveOperatorRelay();
321
312
  const operatorPhone = (bot.flow_config as { operator_phone?: string }).operator_phone!;
322
313
  const copy = config.copy[kind] ?? config.default_copy;
@@ -333,23 +324,15 @@ async function deliverEscalation(
333
324
  const { userId, organisationId } = await getSystemIdentity();
334
325
  const systemCtx: ExecutionContext = { userId, organisationId } as ExecutionContext;
335
326
  const { forwardToolToCentralMcp } = await import("@teamscala/chat-completion/central-tool-forwarder");
336
- const paDigits = (bot.external_id ?? "").replace(/\D/g, "");
337
- const deviceId = paDigits ? await resolvePaDeviceId(paDigits) : undefined;
338
- if (paDigits && !deviceId) {
339
- log.warn("[platform-fault-escalation] no GOWA device matched the PA number — sending via default device");
340
- }
341
327
  const sendResult = await forwardToolToCentralMcp(
342
- "api_gowa_message",
343
- {
344
- action: "sendMessage",
345
- params: { phone: operatorPhone, message, ...(deviceId ? { device_id: deviceId } : {}) },
346
- },
328
+ "messaging_whatsapp_send",
329
+ { phone: operatorPhone, message },
347
330
  systemCtx,
348
331
  );
349
332
 
350
333
  // 3. VERIFY delivery before recording it: the send must return a WhatsApp
351
334
  // message id. No id => throw, the claim stays undelivered, next sweep retries.
352
- const messageId = extractGowaMessageId(sendResult);
335
+ const messageId = extractWhatsAppMessageId(sendResult);
353
336
  if (!messageId) {
354
337
  throw new Error(
355
338
  `platform-fault-escalation: WhatsApp send for '${kind}' returned no message id — not recording delivery. ` +