@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.
- package/package.json +1 -1
- package/payload/platform/lib/agent-dispatch-rule/dist/index.d.ts +34 -0
- package/payload/platform/lib/agent-dispatch-rule/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/agent-dispatch-rule/dist/index.js +39 -0
- package/payload/platform/lib/agent-dispatch-rule/dist/index.js.map +1 -1
- package/payload/platform/lib/agent-dispatch-rule/src/__tests__/carrier-verdict.test.ts +92 -0
- package/payload/platform/lib/agent-dispatch-rule/src/index.ts +59 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +67 -10
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +11 -0
- package/payload/platform/plugins/docs/references/telegram-guide.md +66 -9
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch-carrier.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch-carrier.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch-carrier.test.js +72 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch-carrier.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery-reconcile.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery-reconcile.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery-reconcile.test.js +327 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery-reconcile.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery.test.js +130 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-delivery.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-run.test.js +27 -58
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/routine-run.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.d.ts +8 -3
- package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.js +37 -3
- package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-delivery.d.ts +67 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-delivery.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-delivery.js +133 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-delivery.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.d.ts +67 -12
- package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.js +226 -72
- package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js +66 -4
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/reconcile-run-controls.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/reconcile-run-controls.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/reconcile-run-controls.test.js +45 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/reconcile-run-controls.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +3 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +32 -7
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +12 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +15 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +14 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +4 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts +3 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +14 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/card-row.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/card-row.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/card-row.test.js +237 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/card-row.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js +6 -4
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/card-row.d.ts +96 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/card-row.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/card-row.js +156 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/card-row.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/card.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/card.js +53 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/card.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/instruct.d.ts +7 -5
- package/payload/platform/plugins/telegram/mcp/dist/tools/instruct.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/instruct.js +14 -12
- package/payload/platform/plugins/telegram/mcp/dist/tools/instruct.js.map +1 -1
- package/payload/server/public/operator.html +1 -1
- 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":"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"}
|
|
@@ -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":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
|
|
388
|
+
// Task 2776 — the 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
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
|
|
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
|