@rubytech/create-sitedesk-code 0.1.612 → 0.1.614

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 (82) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/agent-dispatch-rule/dist/index.d.ts +34 -0
  3. package/payload/platform/lib/agent-dispatch-rule/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/agent-dispatch-rule/dist/index.js +39 -0
  5. package/payload/platform/lib/agent-dispatch-rule/dist/index.js.map +1 -1
  6. package/payload/platform/lib/agent-dispatch-rule/src/__tests__/carrier-verdict.test.ts +92 -0
  7. package/payload/platform/lib/agent-dispatch-rule/src/index.ts +59 -0
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +67 -10
  9. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +11 -0
  10. package/payload/platform/plugins/docs/references/telegram-guide.md +66 -9
  11. package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -0
  12. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch-carrier.test.d.ts +2 -0
  14. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch-carrier.test.d.ts.map +1 -0
  15. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch-carrier.test.js +72 -0
  16. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch-carrier.test.js.map +1 -0
  17. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery-reconcile.test.d.ts +2 -0
  18. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery-reconcile.test.d.ts.map +1 -0
  19. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery-reconcile.test.js +327 -0
  20. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery-reconcile.test.js.map +1 -0
  21. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery.test.d.ts +2 -0
  22. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery.test.d.ts.map +1 -0
  23. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery.test.js +130 -0
  24. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery.test.js.map +1 -0
  25. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-run.test.js +27 -58
  26. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-run.test.js.map +1 -1
  27. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.d.ts +8 -3
  28. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.d.ts.map +1 -1
  29. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.js +37 -3
  30. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.js.map +1 -1
  31. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-delivery.d.ts +67 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-delivery.d.ts.map +1 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-delivery.js +133 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-delivery.js.map +1 -0
  35. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.d.ts +67 -12
  36. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.d.ts.map +1 -1
  37. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.js +226 -72
  38. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.js.map +1 -1
  39. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js +66 -4
  40. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js.map +1 -1
  41. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/reconcile-run-controls.test.d.ts +2 -0
  42. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/reconcile-run-controls.test.d.ts.map +1 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/reconcile-run-controls.test.js +45 -0
  44. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/reconcile-run-controls.test.js.map +1 -0
  45. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +3 -1
  46. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  47. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +32 -7
  48. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  49. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +12 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  51. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +15 -2
  52. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  53. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +14 -0
  54. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  55. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +4 -0
  56. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  57. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts +3 -0
  58. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts.map +1 -1
  59. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js +2 -0
  60. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js.map +1 -1
  61. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  62. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +14 -0
  63. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  64. package/payload/platform/plugins/telegram/mcp/dist/__tests__/card-row.test.d.ts +2 -0
  65. package/payload/platform/plugins/telegram/mcp/dist/__tests__/card-row.test.d.ts.map +1 -0
  66. package/payload/platform/plugins/telegram/mcp/dist/__tests__/card-row.test.js +237 -0
  67. package/payload/platform/plugins/telegram/mcp/dist/__tests__/card-row.test.js.map +1 -0
  68. package/payload/platform/plugins/telegram/mcp/dist/index.js +6 -4
  69. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
  70. package/payload/platform/plugins/telegram/mcp/dist/lib/card-row.d.ts +96 -0
  71. package/payload/platform/plugins/telegram/mcp/dist/lib/card-row.d.ts.map +1 -0
  72. package/payload/platform/plugins/telegram/mcp/dist/lib/card-row.js +156 -0
  73. package/payload/platform/plugins/telegram/mcp/dist/lib/card-row.js.map +1 -0
  74. package/payload/platform/plugins/telegram/mcp/dist/tools/card.d.ts.map +1 -1
  75. package/payload/platform/plugins/telegram/mcp/dist/tools/card.js +53 -0
  76. package/payload/platform/plugins/telegram/mcp/dist/tools/card.js.map +1 -1
  77. package/payload/platform/plugins/telegram/mcp/dist/tools/instruct.d.ts +7 -5
  78. package/payload/platform/plugins/telegram/mcp/dist/tools/instruct.d.ts.map +1 -1
  79. package/payload/platform/plugins/telegram/mcp/dist/tools/instruct.js +14 -12
  80. package/payload/platform/plugins/telegram/mcp/dist/tools/instruct.js.map +1 -1
  81. package/payload/server/public/operator.html +1 -1
  82. package/payload/server/server.js +804 -724
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Task 2776 — establishing that a routine's turn actually reached the chat.
3
+ *
4
+ * The dispatch path cannot report this, and the line that claimed to was wrong.
5
+ * A scheduled dispatch is `role:'admin'`, and `TelegramGateway.sendOutbound` —
6
+ * the path that runs a reply closure inline — is the PUBLIC door's route and
7
+ * says so in its own comment. An admin turn's final text is delivered much
8
+ * later, by the native file follower's fan-out over `telegramFanTargets`. That
9
+ * send site holds no eventId and no runId, so it cannot name the run it belongs
10
+ * to, and threading run identity through the inject POST, the gateway, the PTY
11
+ * session and the transcript to reach it would be a change across four
12
+ * subsystems.
13
+ *
14
+ * What the send site DOES record is the session. The reply closure writes each
15
+ * outbound row with `sessionId` taken from the same value `schedule-inject`
16
+ * returns as `sessionKey`, and `recordRunSession` already persists that on the
17
+ * run. A run's deliveries are therefore recoverable by joining two records that
18
+ * are already on disk — no reproduction, no threading, and no later traffic
19
+ * required, which is what the standing check needs.
20
+ *
21
+ * Telegram only. WhatsApp resolves a phone rather than a bot and has no
22
+ * equivalent store to reconcile against; its runs are left alone rather than
23
+ * stamped null, because counting them would manufacture exactly the false
24
+ * silence this task removes.
25
+ */
26
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
27
+ import { join } from "node:path";
28
+ import { telegramStoreRoot } from "../../../../../lib/telegram-reach/dist/index.js";
29
+ /**
30
+ * The rows belonging to one run: this session's, at or after the run started,
31
+ * and strictly before the ceiling.
32
+ *
33
+ * `to` is the NEXT run's `startedAt` on the same session, or null when this is
34
+ * the newest run there and the window is still open. Half-open at the top for
35
+ * the reason `promoteRoutineRuns` bounds its transcript window the same way:
36
+ * without it, run N's window would swallow every later run's output and both
37
+ * would be credited with one answer.
38
+ *
39
+ * The session match is what makes this an attribution rather than a guess.
40
+ * `adminSessionIdFor` puts every run for one destination in ONE conversation
41
+ * together with the operator's own messages, so a time window alone would credit
42
+ * a routine that has been dead for a week with whatever the operator was told.
43
+ *
44
+ * TWO known limits, stated rather than hidden.
45
+ *
46
+ * A send landing after the NEXT run has started is attributed to that next run.
47
+ * For a five-minute recurrence the window is five minutes, far longer than a
48
+ * turn's send takes.
49
+ *
50
+ * More importantly, the session is NOT a per-routine identity.
51
+ * `telegramAdminSessionId` hashes (accountId, chatId, botId), so two routines
52
+ * reporting to the same chat share one session and their runs interleave here.
53
+ * A message one of them sent after the other fired lands in the other's window
54
+ * and is credited to it, leaving the true sender recorded as silent. A store row
55
+ * carries no eventId, so nothing here can tell them apart —
56
+ * `promoteRoutineRuns` can, because a transcript carries the routine's own
57
+ * schedule marker, and no equivalent exists on the store side. Task 2779 holds
58
+ * the fix. Until then this is exact for an account whose destinations each have
59
+ * one routine, which is the measured case, and approximate otherwise.
60
+ *
61
+ * Pure — no IO — so the rule is testable without a filesystem or a graph, the
62
+ * same contract `classifyRunEvidence` states for itself.
63
+ */
64
+ export function selectDeliveredRows(rows, sessionKey, from, to) {
65
+ return rows
66
+ .filter((r) => r.sessionId === sessionKey && r.createdAt >= from && (to === null || r.createdAt < to))
67
+ .sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
68
+ }
69
+ /**
70
+ * Every confirmed agent row this account holds for `destination`, across EVERY
71
+ * bot that has one.
72
+ *
73
+ * Not only the expected carrier's file, deliberately: a routine whose carrier
74
+ * moved still delivered to a real chat, on the wrong bot, and reading one file
75
+ * would record that as silence. The row id embeds the channel key, so the
76
+ * `deliveredMessageId` this produces names the bot that actually carried it —
77
+ * which is how a moved carrier becomes visible on the delivery itself.
78
+ *
79
+ * `origin: 'agent'` AND `fromMe: true` because the store holds the inbound side
80
+ * of the same conversation, and an inbound row is the operator talking, not the
81
+ * routine answering.
82
+ *
83
+ * Never throws: an absent directory, an unreadable file, a malformed line and a
84
+ * row missing the joined fields are each skipped, because this runs inside the
85
+ * reconcile sweep and a bookkeeping failure must never cost the tick.
86
+ */
87
+ export function readAgentRowsForDestination(platformRoot, accountId, destination) {
88
+ const dir = join(telegramStoreRoot(platformRoot), accountId);
89
+ if (!existsSync(dir))
90
+ return [];
91
+ let names;
92
+ try {
93
+ // The `_` is part of the match, so chat 1311221864 does not also collect
94
+ // rows from chat 11311221864's file.
95
+ names = readdirSync(dir).filter((n) => n.endsWith(`_${destination}.jsonl`));
96
+ }
97
+ catch {
98
+ return [];
99
+ }
100
+ const out = [];
101
+ for (const name of names) {
102
+ let text;
103
+ try {
104
+ text = readFileSync(join(dir, name), "utf-8");
105
+ }
106
+ catch {
107
+ continue;
108
+ }
109
+ for (const raw of text.split("\n")) {
110
+ const line = raw.trim();
111
+ if (line.length === 0)
112
+ continue;
113
+ let rec;
114
+ try {
115
+ rec = JSON.parse(line);
116
+ }
117
+ catch {
118
+ continue;
119
+ }
120
+ if (rec.origin !== "agent" || rec.fromMe !== true)
121
+ continue;
122
+ const { messageId, sessionId, createdAt, body } = rec;
123
+ if (typeof messageId !== "string" ||
124
+ typeof sessionId !== "string" ||
125
+ typeof createdAt !== "string") {
126
+ continue;
127
+ }
128
+ out.push({ messageId, sessionId, createdAt, body: typeof body === "string" ? body : "" });
129
+ }
130
+ }
131
+ return out;
132
+ }
133
+ //# sourceMappingURL=routine-delivery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routine-delivery.js","sourceRoot":"","sources":["../../src/lib/routine-delivery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAcpF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAsB,EACtB,UAAkB,EAClB,IAAY,EACZ,EAAiB;IAEjB,OAAO,IAAI;SACR,MAAM,CACL,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,UAAU,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC,CAAC,SAAS,GAAG,EAAE,CAAC,CAC9F;SACA,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1F,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,2BAA2B,CACzC,YAAoB,EACpB,SAAiB,EACjB,WAAmB;IAEnB,MAAM,GAAG,GAAG,IAAI,CAAC,iBAAiB,CAAC,YAAY,CAAC,EAAE,SAAS,CAAC,CAAC;IAC7D,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,yEAAyE;QACzE,qCAAqC;QACrC,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,WAAW,QAAQ,CAAC,CAAC,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,GAAG,GAAqB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;YACxB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAChC,IAAI,GAA4B,CAAC;YACjC,IAAI,CAAC;gBACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC;YACpD,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;YACD,IAAI,GAAG,CAAC,MAAM,KAAK,OAAO,IAAI,GAAG,CAAC,MAAM,KAAK,IAAI;gBAAE,SAAS;YAC5D,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC;YACtD,IACE,OAAO,SAAS,KAAK,QAAQ;gBAC7B,OAAO,SAAS,KAAK,QAAQ;gBAC7B,OAAO,SAAS,KAAK,QAAQ,EAC7B,CAAC;gBACD,SAAS;YACX,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import type { Driver } from "neo4j-driver";
2
+ import { type StoredAgentRow } from "./routine-delivery.js";
2
3
  /** Which dispatch loop opened the record. */
3
4
  export type RunKind = "recurring" | "one-time";
4
5
  /** The `chooseDispatchMode` result the record was opened for. */
@@ -23,6 +24,19 @@ export type RunMode = "agent" | "action" | "none";
23
24
  export type RunOutcome = "accepted" | "failed" | "no-destination" | "gated";
24
25
  /** Records are kept for 30 days. A quarter-hourly routine settles at ~2,900 per account. */
25
26
  export declare const RETENTION_DAYS = 30;
27
+ /**
28
+ * Task 2776 — the window BOTH delivery controls work over.
29
+ *
30
+ * One constant because the two must not drift: if `reconcileRoutineDelivery`
31
+ * looked over a shorter window than `routineDeliveryCensus`, the census would
32
+ * report as silent a run the reconciliation had already stopped trying to
33
+ * stamp — an alarm about the checker rather than about the routine.
34
+ *
35
+ * It is also what makes the change forward-only without a migration: runs
36
+ * recorded before this shipped leave the window within a day, so nothing needs
37
+ * backfilling and nothing is backfilled.
38
+ */
39
+ export declare const DELIVERY_WINDOW_HOURS = 24;
26
40
  /**
27
41
  * How long an `open` record may live before it counts as an orphan. This is
28
42
  * `DISPATCH_TIMEOUT_MS` from check-due-events, the hard ceiling on the dispatch
@@ -133,18 +147,6 @@ export declare function classifyRunEvidence(window: string, eventId: string): "n
133
147
  * characters holds every real message and bounds the node, the JSON the admin
134
148
  * route serves and the CSV cell together. */
135
149
  export declare const OUTPUT_EXCERPT_CAP = 2000;
136
- /**
137
- * The delivered text inside one run's transcript window, or null when the run
138
- * delivered nothing. Pure, so the rule is testable without a filesystem or a
139
- * graph, exactly like `classifyRunEvidence` above.
140
- *
141
- * Null is not a failure. A routine that only writes to the calendar finishes
142
- * having sent nothing, and `finished` with no excerpt is the honest reading of
143
- * that; the run surfaces say so in those words. A malformed line is skipped
144
- * rather than thrown on — the window is a byte range of a file being appended
145
- * to, so a partial trailing line is normal.
146
- */
147
- export declare function extractDeliveredText(window: string): string | null;
148
150
  /**
149
151
  * Promote `accepted` and `started` records from evidence in the transcript.
150
152
  *
@@ -219,4 +221,57 @@ export declare function auditRunsWithoutOutput(driver: Driver, accountId: string
219
221
  * nothing watching it. Emitted every tick including zero.
220
222
  */
221
223
  export declare function auditRunCensus(driver: Driver, accountId: string): Promise<void>;
224
+ /**
225
+ * Task 2776 — stamp each telegram run with the send that reached the chat, and
226
+ * with the text of it.
227
+ *
228
+ * Runs AFTER `promoteRoutineRuns` and separately from it. Promotion acts on a
229
+ * status transition, and the send lands at about the same instant as the
230
+ * assistant row that triggers it, so stamping inside that transition would race
231
+ * and record a false silence.
232
+ *
233
+ * No status filter, deliberately: a run can deliver while still `accepted`,
234
+ * before promotion has seen its assistant row, and filtering to `finished` would
235
+ * read that as silence. Rows already carrying a `deliveredMessageId` are skipped
236
+ * in JS rather than in Cypher because every run on a session is needed to
237
+ * compute the next one's ceiling — the same reason `promoteRoutineRuns` selects
238
+ * them all. That skip is what makes the pass idempotent, so a delivery arriving
239
+ * after promotion is picked up on a later tick.
240
+ *
241
+ * Telegram only. A WhatsApp run has no equivalent store to reconcile against and
242
+ * is left alone rather than stamped null-and-counted, which would manufacture
243
+ * exactly the false silence this task removes.
244
+ *
245
+ * Never throws (honours the sweep contract).
246
+ */
247
+ export declare function reconcileRoutineDelivery(driver: Driver, accountId: string, platformRoot: string, nowMs: number, deps?: {
248
+ readRows?: (root: string, acct: string, dest: string) => StoredAgentRow[];
249
+ }): Promise<{
250
+ stamped: number;
251
+ }>;
252
+ /**
253
+ * Task 2776 — the standing reconciliation: accepted wakes against runs that
254
+ * carry a delivery, over a rolling window.
255
+ *
256
+ * This is the control the defect needed and did not have. A wake that produced
257
+ * no message emits nothing, and re-firing the routine does not surface it, so
258
+ * only a periodic comparison of expected against actual can see it — the same
259
+ * reasoning `auditRunsWithoutOutput` states for itself.
260
+ *
261
+ * `silent` is NOT an error on its own: a routine told to report only when
262
+ * something is new is silent by design. So the line NAMES the events
263
+ * contributing to it rather than alarming on the count, and an event silent
264
+ * across every accepted wake for a full window is the shape worth reading.
265
+ *
266
+ * The window also carries the forward-only rule. Runs recorded before this
267
+ * shipped fall out of a 24-hour window within a day, so nothing needs
268
+ * backfilling and nothing is backfilled.
269
+ *
270
+ * Telegram only, matching `reconcileRoutineDelivery`: a WhatsApp run can never
271
+ * be stamped, so counting it would report every one of them as silent forever.
272
+ *
273
+ * Emitted every sweep including zero, because a check that is silent when
274
+ * healthy cannot be told apart from one that stopped running. Never throws.
275
+ */
276
+ export declare function routineDeliveryCensus(driver: Driver, accountId: string, nowMs: number, windowHours?: number): Promise<void>;
222
277
  //# sourceMappingURL=routine-run.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routine-run.d.ts","sourceRoot":"","sources":["../../src/lib/routine-run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routine-run.d.ts","sourceRoot":"","sources":["../../src/lib/routine-run.ts"],"names":[],"mappings":"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"}
@@ -18,8 +18,22 @@
18
18
  import { randomUUID } from "node:crypto";
19
19
  import { closeSync, openSync, readdirSync, readSync, statSync } from "node:fs";
20
20
  import { dirname, join } from "node:path";
21
+ import { readAgentRowsForDestination, selectDeliveredRows, } from "./routine-delivery.js";
21
22
  /** Records are kept for 30 days. A quarter-hourly routine settles at ~2,900 per account. */
22
23
  export const RETENTION_DAYS = 30;
24
+ /**
25
+ * Task 2776 — the window BOTH delivery controls work over.
26
+ *
27
+ * One constant because the two must not drift: if `reconcileRoutineDelivery`
28
+ * looked over a shorter window than `routineDeliveryCensus`, the census would
29
+ * report as silent a run the reconciliation had already stopped trying to
30
+ * stamp — an alarm about the checker rather than about the routine.
31
+ *
32
+ * It is also what makes the change forward-only without a migration: runs
33
+ * recorded before this shipped leave the window within a day, so nothing needs
34
+ * backfilling and nothing is backfilled.
35
+ */
36
+ export const DELIVERY_WINDOW_HOURS = 24;
23
37
  /**
24
38
  * How long an `open` record may live before it counts as an orphan. This is
25
39
  * `DISPATCH_TIMEOUT_MS` from check-due-events, the hard ceiling on the dispatch
@@ -71,6 +85,11 @@ export async function openRoutineRun(driver, input) {
71
85
  sessionKey: null,
72
86
  transcriptOffset: null,
73
87
  bounded: false,
88
+ // Task 2776 — the store row id of the message that reached the chat,
89
+ // stamped later by `reconcileRoutineDelivery`. Null here and null
90
+ // forever on a run that sent nothing, which is the property that
91
+ // separates a quiet routine from a broken one.
92
+ deliveredMessageId: null,
74
93
  error: null,
75
94
  },
76
95
  });
@@ -287,66 +306,19 @@ export function classifyRunEvidence(window, eventId) {
287
306
  * characters holds every real message and bounds the node, the JSON the admin
288
307
  * route serves and the CSV cell together. */
289
308
  export const OUTPUT_EXCERPT_CAP = 2000;
290
- /**
291
- * The tool calls whose `input.text` IS the message the recipient saw.
292
- *
293
- * Deliberately a second copy of the reader's constants
294
- * (`platform/ui/app/lib/reader/parse-transcript.ts:147-168`): `platform/plugins`
295
- * must never import from `platform/ui`, exactly as `mapRun` in
296
- * `platform/ui/server/routes/admin/routines.ts` restates this module's own
297
- * projection across the same boundary.
298
- *
299
- * Reading the assistant's `text` blocks instead would store the agent's internal
300
- * narration the reader classifies those as `agent-text` and never as anything
301
- * delivered and present it as the message that was sent.
302
- */
303
- const DELIVERY_TOOLS = new Set([
304
- "mcp__whatsapp-channel__reply",
305
- "mcp__channel__reply",
306
- "mcp__webchat-channel__reply",
307
- "mcp__whatsapp-channel__send-to-party",
308
- "mcp__channel__send-to-party",
309
- ]);
310
- /**
311
- * The delivered text inside one run's transcript window, or null when the run
312
- * delivered nothing. Pure, so the rule is testable without a filesystem or a
313
- * graph, exactly like `classifyRunEvidence` above.
314
- *
315
- * Null is not a failure. A routine that only writes to the calendar finishes
316
- * having sent nothing, and `finished` with no excerpt is the honest reading of
317
- * that; the run surfaces say so in those words. A malformed line is skipped
318
- * rather than thrown on — the window is a byte range of a file being appended
319
- * to, so a partial trailing line is normal.
320
- */
321
- export function extractDeliveredText(window) {
322
- const parts = [];
323
- for (const raw of window.split("\n")) {
324
- const line = raw.trim();
325
- if (line.length === 0)
326
- continue;
327
- let rec;
328
- try {
329
- rec = JSON.parse(line);
330
- }
331
- catch {
332
- continue;
333
- }
334
- const r = rec;
335
- if (r?.type !== "assistant" || !Array.isArray(r.message?.content))
336
- continue;
337
- for (const block of r.message.content) {
338
- const b = block;
339
- if (b?.type !== "tool_use" || typeof b.name !== "string" || !DELIVERY_TOOLS.has(b.name))
340
- continue;
341
- const text = b.input?.text;
342
- if (typeof text === "string" && text.trim().length > 0)
343
- parts.push(text);
344
- }
345
- }
346
- if (parts.length === 0)
347
- return null;
348
- return parts.join("\n\n").slice(0, OUTPUT_EXCERPT_CAP);
349
- }
309
+ // Task 2776 — `DELIVERY_TOOLS` and `extractDeliveredText` are GONE.
310
+ //
311
+ // They scanned a run's transcript window for the channel reply tools
312
+ // (`mcp__channel__reply` and siblings). Task 2078 retired every one of them: the
313
+ // turn's own final text is the delivery on all four doors now, and nothing live
314
+ // emits those tool calls. So the scan matched nothing, `outputExcerpt` was null
315
+ // on every modern run, and `auditRunsWithoutOutput` reported `missing` equal to
316
+ // `finished` forever a control pinned at its alarm value, which reads as noise
317
+ // rather than as the signal it was built to be.
318
+ //
319
+ // The excerpt now comes from the outbound store rows `reconcileRoutineDelivery`
320
+ // finds below: the text the recipient actually received, from the only record
321
+ // that exists because a send was confirmed.
350
322
  /**
351
323
  * Promote `accepted` and `started` records from evidence in the transcript.
352
324
  *
@@ -413,27 +385,32 @@ export async function promoteRoutineRuns(driver, accountId, deps = {}) {
413
385
  const produced = evidence === "finished";
414
386
  if (next === status)
415
387
  continue;
416
- // Task 2359captured on the finished transition only. A `started` run
417
- // has produced nothing yet, and a run already at `finished` never reaches
418
- // here (the `next === status` guard above), so the window is read for its
419
- // text exactly once per run.
420
- const excerpt = produced ? extractDeliveredText(win) : null;
388
+ // Task 2776the excerpt is no longer written here. It was read from the
389
+ // transcript by `extractDeliveredText`, which scanned for reply tools Task
390
+ // 2078 retired and so stored null on every modern run. The text the
391
+ // recipient received is the outbound store row, stamped by
392
+ // `reconcileRoutineDelivery` on the same pass that records WHICH message
393
+ // reached the chat — after this one, because a send lands at about the
394
+ // same moment as the assistant row that triggers `finished` here.
395
+ //
396
+ // This function now establishes status alone, which is all a transcript
397
+ // can honestly answer.
421
398
  const now = new Date().toISOString();
422
399
  const w = driver.session();
423
400
  try {
424
401
  await w.run(`MATCH (r:RoutineRun {runId: $runId})
425
402
  SET r.status = $status,
426
- r.finishedAt = $finishedAt,
427
- r.outputExcerpt = $outputExcerpt`, { runId, status: next, finishedAt: produced ? now : null, outputExcerpt: excerpt });
403
+ r.finishedAt = $finishedAt`, { runId, status: next, finishedAt: produced ? now : null });
428
404
  if (produced)
429
405
  finished++;
430
406
  else
431
407
  started++;
432
- // outputChars is the measured length of what was stored, not a claim
433
- // that capture ran: `outputChars=0` on `status=finished` is the
434
- // signature of a run that finished having sent nothing, or of
435
- // extraction having gone blind.
436
- console.error(`[routine-run] op=promote runId=${runId} status=${next} outputChars=${excerpt?.length ?? 0}`);
408
+ // Task 2776 `outputChars` is gone from this line. It reported the
409
+ // length of an excerpt this function no longer captures, so keeping it
410
+ // would print `outputChars=0` on every promotion and read as a run that
411
+ // sent nothing. What was sent is named on `op=routine-delivered`, by the
412
+ // pass that actually knows.
413
+ console.error(`[routine-run] op=promote runId=${runId} status=${next}`);
437
414
  }
438
415
  finally {
439
416
  try {
@@ -644,4 +621,181 @@ export async function auditRunCensus(driver, accountId) {
644
621
  }
645
622
  }
646
623
  }
624
+ /**
625
+ * Task 2776 — stamp each telegram run with the send that reached the chat, and
626
+ * with the text of it.
627
+ *
628
+ * Runs AFTER `promoteRoutineRuns` and separately from it. Promotion acts on a
629
+ * status transition, and the send lands at about the same instant as the
630
+ * assistant row that triggers it, so stamping inside that transition would race
631
+ * and record a false silence.
632
+ *
633
+ * No status filter, deliberately: a run can deliver while still `accepted`,
634
+ * before promotion has seen its assistant row, and filtering to `finished` would
635
+ * read that as silence. Rows already carrying a `deliveredMessageId` are skipped
636
+ * in JS rather than in Cypher because every run on a session is needed to
637
+ * compute the next one's ceiling — the same reason `promoteRoutineRuns` selects
638
+ * them all. That skip is what makes the pass idempotent, so a delivery arriving
639
+ * after promotion is picked up on a later tick.
640
+ *
641
+ * Telegram only. A WhatsApp run has no equivalent store to reconcile against and
642
+ * is left alone rather than stamped null-and-counted, which would manufacture
643
+ * exactly the false silence this task removes.
644
+ *
645
+ * Never throws (honours the sweep contract).
646
+ */
647
+ export async function reconcileRoutineDelivery(driver, accountId, platformRoot, nowMs, deps = {}) {
648
+ const readRows = deps.readRows ?? readAgentRowsForDestination;
649
+ const since = new Date(nowMs - DELIVERY_WINDOW_HOURS * 60 * 60 * 1000).toISOString();
650
+ let stamped = 0;
651
+ // One read per DESTINATION, not per run. Every run against one chat shares a
652
+ // destination, so reading inside the loop parsed the same files once per run
653
+ // and discarded all but one result. The files cannot change within a pass.
654
+ const rowCache = new Map();
655
+ const rowsFor = (dest) => {
656
+ const hit = rowCache.get(dest);
657
+ if (hit !== undefined)
658
+ return hit;
659
+ const read = readRows(platformRoot, accountId, dest);
660
+ rowCache.set(dest, read);
661
+ return read;
662
+ };
663
+ const session = driver.session();
664
+ try {
665
+ // Bounded by the SAME window as the census, deliberately. Unbounded, this
666
+ // stamped every telegram run in the 30-day retention on the first tick after
667
+ // upgrade — the backfill this task's scope boundaries exclude — and never
668
+ // settled: a run that legitimately delivered nothing stays unstamped, so it
669
+ // was re-examined on every 120-second tick for as long as it survived.
670
+ //
671
+ // The window must never be SHORTER than the census's, or the census would
672
+ // report as silent a run this pass had already stopped trying to stamp. One
673
+ // constant serves both so they cannot drift.
674
+ const res = await session.run(`MATCH (r:RoutineRun)
675
+ WHERE r.accountId = $accountId
676
+ AND r.target STARTS WITH 'telegram:'
677
+ AND r.sessionKey IS NOT NULL
678
+ AND r.startedAt >= $since
679
+ RETURN r.runId AS runId, r.sessionKey AS sessionKey, r.target AS target,
680
+ r.eventId AS eventId, r.startedAt AS startedAt,
681
+ r.deliveredMessageId AS deliveredMessageId
682
+ ORDER BY r.sessionKey, r.startedAt`, { accountId, since });
683
+ const rows = res.records.map((rec) => ({
684
+ runId: rec.get("runId"),
685
+ sessionKey: rec.get("sessionKey"),
686
+ destination: rec.get("target").slice("telegram:".length),
687
+ eventId: rec.get("eventId"),
688
+ startedAt: rec.get("startedAt"),
689
+ delivered: (rec.get("deliveredMessageId") ?? null),
690
+ }));
691
+ for (let i = 0; i < rows.length; i++) {
692
+ const row = rows[i];
693
+ if (row.delivered !== null)
694
+ continue;
695
+ // The ceiling is the next run's start in the SAME conversation. Null means
696
+ // this is the newest run there and its window is still open, so a slow
697
+ // turn is still attributed rather than read as silence.
698
+ const succ = rows[i + 1];
699
+ const ceiling = succ && succ.sessionKey === row.sessionKey ? succ.startedAt : null;
700
+ const found = selectDeliveredRows(rowsFor(row.destination), row.sessionKey, row.startedAt, ceiling);
701
+ if (found.length === 0)
702
+ continue;
703
+ const excerpt = found.map((r) => r.body).join("\n\n").slice(0, OUTPUT_EXCERPT_CAP);
704
+ const w = driver.session();
705
+ try {
706
+ await w.run(`MATCH (r:RoutineRun {runId: $runId})
707
+ SET r.deliveredMessageId = $deliveredMessageId,
708
+ r.outputExcerpt = $outputExcerpt`, { runId: row.runId, deliveredMessageId: found[0].messageId, outputExcerpt: excerpt });
709
+ stamped++;
710
+ // The carrier comes off the row id (`telegram:<acct>:<botId>:<chat>:out:<iso>`)
711
+ // rather than from the event, so this names the bot that ACTUALLY carried
712
+ // the message. A routine whose carrier moved shows a botId here that
713
+ // differs from the one it named — the wrong-bot case made visible on the
714
+ // delivery itself rather than inferred afterwards.
715
+ const botId = found[0].messageId.split(":")[2] ?? "-";
716
+ console.error(`[schedule-dispatch] op=routine-delivered event=${row.eventId} run=${row.runId} ` +
717
+ `botId=${botId} chatId=${row.destination} messageId=${found[0].messageId} ` +
718
+ `rows=${found.length} chars=${excerpt.length}`);
719
+ }
720
+ finally {
721
+ try {
722
+ await w.close();
723
+ }
724
+ catch {
725
+ // ignore
726
+ }
727
+ }
728
+ }
729
+ }
730
+ catch (err) {
731
+ console.error(`[routine-run] op=delivery-reconcile-error acct=${accountId.slice(0, 8)} ` +
732
+ `detail=${err instanceof Error ? err.message : String(err)}`);
733
+ }
734
+ finally {
735
+ try {
736
+ await session.close();
737
+ }
738
+ catch {
739
+ // ignore
740
+ }
741
+ }
742
+ return { stamped };
743
+ }
744
+ /**
745
+ * Task 2776 — the standing reconciliation: accepted wakes against runs that
746
+ * carry a delivery, over a rolling window.
747
+ *
748
+ * This is the control the defect needed and did not have. A wake that produced
749
+ * no message emits nothing, and re-firing the routine does not surface it, so
750
+ * only a periodic comparison of expected against actual can see it — the same
751
+ * reasoning `auditRunsWithoutOutput` states for itself.
752
+ *
753
+ * `silent` is NOT an error on its own: a routine told to report only when
754
+ * something is new is silent by design. So the line NAMES the events
755
+ * contributing to it rather than alarming on the count, and an event silent
756
+ * across every accepted wake for a full window is the shape worth reading.
757
+ *
758
+ * The window also carries the forward-only rule. Runs recorded before this
759
+ * shipped fall out of a 24-hour window within a day, so nothing needs
760
+ * backfilling and nothing is backfilled.
761
+ *
762
+ * Telegram only, matching `reconcileRoutineDelivery`: a WhatsApp run can never
763
+ * be stamped, so counting it would report every one of them as silent forever.
764
+ *
765
+ * Emitted every sweep including zero, because a check that is silent when
766
+ * healthy cannot be told apart from one that stopped running. Never throws.
767
+ */
768
+ export async function routineDeliveryCensus(driver, accountId, nowMs, windowHours = DELIVERY_WINDOW_HOURS) {
769
+ const since = new Date(nowMs - windowHours * 60 * 60 * 1000).toISOString();
770
+ const session = driver.session();
771
+ try {
772
+ const res = await session.run(`MATCH (r:RoutineRun)
773
+ WHERE r.accountId = $accountId
774
+ AND r.target STARTS WITH 'telegram:'
775
+ AND r.dispatchResult = 'accepted'
776
+ AND r.startedAt >= $since
777
+ RETURN count(r) AS accepted,
778
+ count(r.deliveredMessageId) AS delivered,
779
+ collect(DISTINCT CASE WHEN r.deliveredMessageId IS NULL THEN r.eventId END) AS silentEvents`, { accountId, since });
780
+ const rec = res.records[0];
781
+ const accepted = toNumber(rec?.get("accepted")) ?? 0;
782
+ const delivered = toNumber(rec?.get("delivered")) ?? 0;
783
+ const silentEvents = (rec?.get("silentEvents") ?? []).filter((e) => typeof e === "string");
784
+ const head = `[routine-delivery-census] accountId=${accountId.slice(0, 8)} accepted=${accepted} ` +
785
+ `delivered=${delivered} silent=${accepted - delivered} window=${windowHours}h`;
786
+ console.error(silentEvents.length > 0 ? `${head} silent-events=[${silentEvents.join(",")}]` : head);
787
+ }
788
+ catch (err) {
789
+ console.error(`[routine-delivery-census] op=routine-delivery-census-error acct=${accountId.slice(0, 8)} ` +
790
+ `detail=${err instanceof Error ? err.message : String(err)}`);
791
+ }
792
+ finally {
793
+ try {
794
+ await session.close();
795
+ }
796
+ catch {
797
+ // ignore
798
+ }
799
+ }
800
+ }
647
801
  //# sourceMappingURL=routine-run.js.map