@rubytech/create-realagent-code 0.1.603 → 0.1.604
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/dist/__tests__/email-sampler-unit.test.js +22 -0
- package/dist/index.js +3 -0
- package/dist/port-resolution.js +9 -1
- package/package.json +1 -1
- package/payload/platform/lib/routine-templates/dist/roster.d.ts +17 -1
- package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
- package/payload/platform/lib/routine-templates/dist/roster.js +8 -0
- package/payload/platform/lib/routine-templates/dist/roster.js.map +1 -1
- package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +26 -1
- package/payload/platform/lib/routine-templates/src/roster.ts +26 -1
- package/payload/platform/lib/telegram-store-read/dist/index.d.ts +59 -0
- package/payload/platform/lib/telegram-store-read/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/telegram-store-read/dist/index.js +219 -0
- package/payload/platform/lib/telegram-store-read/dist/index.js.map +1 -0
- package/payload/platform/lib/telegram-store-read/src/__tests__/fold.test.ts +124 -0
- package/payload/platform/lib/telegram-store-read/src/index.ts +262 -0
- package/payload/platform/lib/telegram-store-read/tsconfig.json +9 -0
- package/payload/platform/lib/telegram-store-read/vitest.config.ts +9 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +37 -4
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
- package/payload/platform/plugins/docs/references/outlook-guide.md +21 -2
- package/payload/platform/plugins/docs/references/telegram-guide.md +15 -1
- package/payload/platform/plugins/email/PLUGIN.md +10 -1
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/sample-mailbox.test.js +30 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/sample-mailbox.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/sampler/main.js +49 -3
- package/payload/platform/plugins/email/mcp/dist/sampler/main.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.d.ts +5 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.js +10 -5
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-section-properties-strip.test.js +11 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-section-properties-strip.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +8 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.js +324 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.js +102 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.js +125 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.js +109 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.d.ts +25 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.js +94 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.d.ts +41 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.js +122 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.d.ts +73 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.js +162 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.js +90 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js +26 -4
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.d.ts +78 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.js +76 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts +8 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js +9 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js +15 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +12 -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 +34 -1
- 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 +9 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +116 -4
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts +60 -2
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +101 -4
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/public/operator.html +1 -1
- package/payload/server/server.js +69 -5
|
@@ -29,3 +29,25 @@ test("carries the plugin runtime env and the brand .env", () => {
|
|
|
29
29
|
assert.match(unit, /^Environment=EMAIL_SAMPLER_INTERVAL_SEC=60$/m);
|
|
30
30
|
assert.match(unit, /^EnvironmentFile=-\/home\/x\/\.maxy\/\.env$/m);
|
|
31
31
|
});
|
|
32
|
+
// Task 2683 — the Outlook arm shares this unit's process, so it needs the same
|
|
33
|
+
// Azure app credentials the session-manager unit carries. Without them
|
|
34
|
+
// callGraph cannot refresh an access token, so every Outlook mailbox would
|
|
35
|
+
// sample correctly until its current token expired and then error permanently.
|
|
36
|
+
test("a brand with an outlook client id stamps both outlook env lines", () => {
|
|
37
|
+
const withOutlook = buildEmailSamplerUnitFile({
|
|
38
|
+
productName: "Maxy",
|
|
39
|
+
installDir: "/home/x/maxy-code",
|
|
40
|
+
persistDir: "/home/x/.maxy",
|
|
41
|
+
accountId: "acc-1",
|
|
42
|
+
logPath: "/home/x/.maxy/logs/email-sampler.log",
|
|
43
|
+
outlookClientId: "azure-app-id",
|
|
44
|
+
});
|
|
45
|
+
assert.match(withOutlook, /^Environment=OUTLOOK_CLIENT_ID=azure-app-id$/m);
|
|
46
|
+
assert.match(withOutlook, /^Environment=OUTLOOK_TENANT_ID=common$/m);
|
|
47
|
+
// Both land above EnvironmentFile so the brand .env can still override.
|
|
48
|
+
assert.ok(withOutlook.indexOf("OUTLOOK_TENANT_ID") < withOutlook.indexOf("EnvironmentFile="), "outlook env must precede EnvironmentFile");
|
|
49
|
+
});
|
|
50
|
+
test("a brand with no outlook client id stamps neither outlook env line", () => {
|
|
51
|
+
assert.doesNotMatch(unit, /OUTLOOK_CLIENT_ID/);
|
|
52
|
+
assert.doesNotMatch(unit, /OUTLOOK_TENANT_ID/);
|
|
53
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -4582,6 +4582,9 @@ WantedBy=default.target
|
|
|
4582
4582
|
persistDir,
|
|
4583
4583
|
accountId: installAccountId,
|
|
4584
4584
|
logPath: emailSamplerLogPath,
|
|
4585
|
+
// Task 2683 — the sampler's Outlook arm runs in this process and needs the
|
|
4586
|
+
// brand's Azure app to refresh a token.
|
|
4587
|
+
outlookClientId: BRAND.outlookClientId,
|
|
4585
4588
|
});
|
|
4586
4589
|
writeFileSync(join(serviceDir, emailSamplerUnitName), emailSamplerUnit);
|
|
4587
4590
|
logFile(` email-sampler installed: ${emailSamplerUnitName} LOG_PATH=${emailSamplerLogPath}`);
|
package/dist/port-resolution.js
CHANGED
|
@@ -172,6 +172,14 @@ WantedBy=default.target
|
|
|
172
172
|
`;
|
|
173
173
|
}
|
|
174
174
|
export function buildEmailSamplerUnitFile(o) {
|
|
175
|
+
// Task 2683 — the Outlook arm runs inside this unit's process, so it needs the
|
|
176
|
+
// same Azure app credentials the session-manager unit carries. Both lines land
|
|
177
|
+
// together so OUTLOOK_TENANT_ID never trails a bare client id. Without them
|
|
178
|
+
// callGraph cannot refresh an access token, so every Outlook mailbox would
|
|
179
|
+
// sample correctly until its current token expired and then error for ever.
|
|
180
|
+
const outlookEnv = o.outlookClientId
|
|
181
|
+
? `\nEnvironment=OUTLOOK_CLIENT_ID=${o.outlookClientId}\nEnvironment=OUTLOOK_TENANT_ID=common`
|
|
182
|
+
: "";
|
|
175
183
|
return `[Unit]
|
|
176
184
|
Description=${o.productName} Email Sampler
|
|
177
185
|
After=network.target
|
|
@@ -186,7 +194,7 @@ Environment=NODE_ENV=production
|
|
|
186
194
|
Environment=ACCOUNT_ID=${o.accountId}
|
|
187
195
|
Environment=PLATFORM_ROOT=${o.installDir}/platform
|
|
188
196
|
Environment=EMAIL_SAMPLER_LOG_PATH=${o.logPath}
|
|
189
|
-
Environment=EMAIL_SAMPLER_INTERVAL_SEC=${o.intervalSec ?? 60}
|
|
197
|
+
Environment=EMAIL_SAMPLER_INTERVAL_SEC=${o.intervalSec ?? 60}${outlookEnv}
|
|
190
198
|
EnvironmentFile=-${o.persistDir}/.env
|
|
191
199
|
StandardOutput=append:${o.persistDir}/logs/server.log
|
|
192
200
|
StandardError=append:${o.persistDir}/logs/server.log
|
package/package.json
CHANGED
|
@@ -76,7 +76,23 @@ export interface RoutineGraphCheck {
|
|
|
76
76
|
readonly type: "graph";
|
|
77
77
|
readonly scope: "appointments";
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
/** Task 2684 — nodes of one label this routine has not yet flagged processed.
|
|
80
|
+
*
|
|
81
|
+
* The other graph check compares one stored timestamp, which can skip a node
|
|
82
|
+
* written between the probe and the promote. This one holds no position at all:
|
|
83
|
+
* "outstanding for this routine" is an exact set, so a node the probe missed is
|
|
84
|
+
* simply still unflagged next fire.
|
|
85
|
+
*
|
|
86
|
+
* `label` is a closed pair rather than a free string because a Cypher label
|
|
87
|
+
* cannot be a parameter. `Section` and `ConversationArchive` are different
|
|
88
|
+
* grains: sections are written per ingest, so new content always arrives
|
|
89
|
+
* unflagged, while an archive is MERGEd per thread, so a reply into a thread
|
|
90
|
+
* this routine already flagged leaves that node flagged. */
|
|
91
|
+
export interface RoutineGraphUnflaggedCheck {
|
|
92
|
+
readonly type: "graph-unflagged";
|
|
93
|
+
readonly label: "ConversationArchive" | "Section";
|
|
94
|
+
}
|
|
95
|
+
export type RoutineGateCheck = RoutineMailCheck | RoutineD1Check | RoutineWhatsappCheck | RoutineCalendarCheck | RoutineTelegramCheck | RoutineGraphCheck | RoutineGraphUnflaggedCheck;
|
|
80
96
|
/** The plugin and the tool are constants for every roster gate, so a template
|
|
81
97
|
* carries neither. `hasWork` is true when ANY check is true. */
|
|
82
98
|
export interface RoutineGate {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roster.d.ts","sourceRoot":"","sources":["../src/roster.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH;;;gFAGgF;AAChF,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AACD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AACD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;CACtC;AACD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AACD;iDACiD;AACjD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;CAC3B;AACD;2EAC2E;AAC3E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC;AACD,MAAM,MAAM,gBAAgB,GACxB,gBAAgB,GAChB,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,oBAAoB,GACpB,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"roster.d.ts","sourceRoot":"","sources":["../src/roster.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH;;;gFAGgF;AAChF,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AACD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AACD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;CACtC;AACD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AACD;iDACiD;AACjD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;CAC3B;AACD;2EAC2E;AAC3E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC;AACD;;;;;;;;;;;6DAW6D;AAC7D,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,qBAAqB,GAAG,SAAS,CAAC;CACnD;AACD,MAAM,MAAM,gBAAgB,GACxB,gBAAgB,GAChB,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,oBAAoB,GACpB,iBAAiB,GACjB,0BAA0B,CAAC;AAE/B;iEACiE;AACjE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC9C;AAWD;;;;;;;;;;;4DAW4D;AAC5D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAkBnE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAE1D;AAED,MAAM,WAAW,eAAe;IAC9B;;mEAE+D;IAC/D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;6EAGyE;IACzE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;wEACoE;IACpE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;2CAIuC;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;IAC5B;;;+DAG2D;IAC3D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;2BAO2B;AAC3B,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAI/D;AA8KD;;;;;;;;iFAQiF;AACjF,eAAO,MAAM,cAAc,EAAE,SAAS,eAAe,EAiIpD,CAAC;AAEF;;6DAE6D;AAC7D,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAqC,CAAC"}
|
|
@@ -64,6 +64,14 @@ exports.ROUTINE_TEMPLATE_VERSION = 6;
|
|
|
64
64
|
function isWatermarkedCheck(check) {
|
|
65
65
|
if (check.type === "whatsapp")
|
|
66
66
|
return check.scope === "inbound";
|
|
67
|
+
// Task 2684 — `graph-unflagged` is NOT watermarked, and it is the first check
|
|
68
|
+
// that is neither watermarked nor stateless. Its state is a flag on each node,
|
|
69
|
+
// written by the dispatcher on an accepted wake, so it needs no stored
|
|
70
|
+
// position and a routine gated only on it needs no commit line. It is listed
|
|
71
|
+
// by omission from the set below, and a test pins that: a later edit that
|
|
72
|
+
// added it here would make `gateNeedsCommit` demand a commit for a check that
|
|
73
|
+
// has no mark to advance.
|
|
74
|
+
//
|
|
67
75
|
// Task 2681 — `graph` compares an ISO `updatedAt` against a stored one, so a
|
|
68
76
|
// commitless graph gate reports work on every fire exactly as a mail one does.
|
|
69
77
|
return (check.type === "mail" ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roster.js","sourceRoot":"","sources":["../src/roster.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;;;
|
|
1
|
+
{"version":3,"file":"roster.js","sourceRoot":"","sources":["../src/roster.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type RoutineMergeAction,
|
|
26
26
|
type SeedSessionLike,
|
|
27
27
|
} from "../index.js";
|
|
28
|
-
import { isClassified } from "../roster.js";
|
|
28
|
+
import { isClassified, type RoutineGateCheck } from "../roster.js";
|
|
29
29
|
|
|
30
30
|
interface FakeCall {
|
|
31
31
|
query: string;
|
|
@@ -437,3 +437,28 @@ describe("telegram-inbound-check template (Task 2675)", () => {
|
|
|
437
437
|
expect(gateNeedsCommit(tg.gate!)).toBe(true);
|
|
438
438
|
});
|
|
439
439
|
});
|
|
440
|
+
|
|
441
|
+
// Task 2684 — the unflagged-node check carries no stored position, so it must
|
|
442
|
+
// never join the watermarked set. If it did, `gateNeedsCommit` would demand a
|
|
443
|
+
// commit for a check with no mark to advance, and `schedule-audit`'s stalled-
|
|
444
|
+
// mark check would measure a routine against a file it never writes.
|
|
445
|
+
describe("graph-unflagged check classification (Task 2684)", () => {
|
|
446
|
+
const CHECK: RoutineGateCheck = { type: "graph-unflagged", label: "Section" };
|
|
447
|
+
|
|
448
|
+
it("is not watermarked — its state is the flag on each node", () => {
|
|
449
|
+
expect(isWatermarkedCheck(CHECK)).toBe(false);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("needs no commit when it is a gate's only check", () => {
|
|
453
|
+
expect(gateNeedsCommit({ checks: [CHECK] })).toBe(false);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("does not suppress a watermarked sibling's commit", () => {
|
|
457
|
+
expect(gateNeedsCommit({ checks: [CHECK, { type: "telegram", scope: "inbound" }] })).toBe(true);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("accepts both grains the email sampler writes", () => {
|
|
461
|
+
const archives: RoutineGateCheck = { type: "graph-unflagged", label: "ConversationArchive" };
|
|
462
|
+
expect(isWatermarkedCheck(archives)).toBe(false);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
@@ -78,13 +78,30 @@ export interface RoutineGraphCheck {
|
|
|
78
78
|
readonly type: "graph";
|
|
79
79
|
readonly scope: "appointments";
|
|
80
80
|
}
|
|
81
|
+
/** Task 2684 — nodes of one label this routine has not yet flagged processed.
|
|
82
|
+
*
|
|
83
|
+
* The other graph check compares one stored timestamp, which can skip a node
|
|
84
|
+
* written between the probe and the promote. This one holds no position at all:
|
|
85
|
+
* "outstanding for this routine" is an exact set, so a node the probe missed is
|
|
86
|
+
* simply still unflagged next fire.
|
|
87
|
+
*
|
|
88
|
+
* `label` is a closed pair rather than a free string because a Cypher label
|
|
89
|
+
* cannot be a parameter. `Section` and `ConversationArchive` are different
|
|
90
|
+
* grains: sections are written per ingest, so new content always arrives
|
|
91
|
+
* unflagged, while an archive is MERGEd per thread, so a reply into a thread
|
|
92
|
+
* this routine already flagged leaves that node flagged. */
|
|
93
|
+
export interface RoutineGraphUnflaggedCheck {
|
|
94
|
+
readonly type: "graph-unflagged";
|
|
95
|
+
readonly label: "ConversationArchive" | "Section";
|
|
96
|
+
}
|
|
81
97
|
export type RoutineGateCheck =
|
|
82
98
|
| RoutineMailCheck
|
|
83
99
|
| RoutineD1Check
|
|
84
100
|
| RoutineWhatsappCheck
|
|
85
101
|
| RoutineCalendarCheck
|
|
86
102
|
| RoutineTelegramCheck
|
|
87
|
-
| RoutineGraphCheck
|
|
103
|
+
| RoutineGraphCheck
|
|
104
|
+
| RoutineGraphUnflaggedCheck;
|
|
88
105
|
|
|
89
106
|
/** The plugin and the tool are constants for every roster gate, so a template
|
|
90
107
|
* carries neither. `hasWork` is true when ANY check is true. */
|
|
@@ -115,6 +132,14 @@ export interface RoutineGate {
|
|
|
115
132
|
* on every fire exactly as a commitless mail gate does. */
|
|
116
133
|
export function isWatermarkedCheck(check: RoutineGateCheck): boolean {
|
|
117
134
|
if (check.type === "whatsapp") return check.scope === "inbound";
|
|
135
|
+
// Task 2684 — `graph-unflagged` is NOT watermarked, and it is the first check
|
|
136
|
+
// that is neither watermarked nor stateless. Its state is a flag on each node,
|
|
137
|
+
// written by the dispatcher on an accepted wake, so it needs no stored
|
|
138
|
+
// position and a routine gated only on it needs no commit line. It is listed
|
|
139
|
+
// by omission from the set below, and a test pins that: a later edit that
|
|
140
|
+
// added it here would make `gateNeedsCommit` demand a commit for a check that
|
|
141
|
+
// has no mark to advance.
|
|
142
|
+
//
|
|
118
143
|
// Task 2681 — `graph` compares an ISO `updatedAt` against a stored one, so a
|
|
119
144
|
// commitless graph gate reports work on every fire exactly as a mail one does.
|
|
120
145
|
return (
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** One stored message with every patch that names it already applied. */
|
|
2
|
+
export interface FoldedMessage {
|
|
3
|
+
messageId: string;
|
|
4
|
+
/** ISO 8601, this install's clock at write time — NOT Telegram's own second. */
|
|
5
|
+
dateSent: string;
|
|
6
|
+
/** The patched body when an update replaced it, else the row's own. */
|
|
7
|
+
body: string;
|
|
8
|
+
/** 'inbound' | 'agent' | 'operator-manual'. Kept as the string the store
|
|
9
|
+
* holds: a reader that meets an origin this build does not know should show
|
|
10
|
+
* it, not drop the message. */
|
|
11
|
+
origin: string;
|
|
12
|
+
fromMe: boolean;
|
|
13
|
+
senderId: string;
|
|
14
|
+
senderName: string | null;
|
|
15
|
+
/** The sender withdrew this at their end. The body is KEPT: Telegram sends
|
|
16
|
+
* none on a deletion, so the stored body is the only account anywhere of what
|
|
17
|
+
* was withdrawn. */
|
|
18
|
+
deleted: boolean;
|
|
19
|
+
/** Servable attachment ids in arrival order, de-duplicated. Raw ids, never
|
|
20
|
+
* resolved metadata — see the file header. */
|
|
21
|
+
attachmentIds: string[];
|
|
22
|
+
/** A card's inline-keyboard labels. Null on an ordinary message that never
|
|
23
|
+
* carried a keyboard; an EMPTY ARRAY when an edit removed the one it had.
|
|
24
|
+
* The two are different facts and the distinction is the route's, kept here
|
|
25
|
+
* so the agent and the operator cannot read one card two ways. */
|
|
26
|
+
buttons: {
|
|
27
|
+
text: string;
|
|
28
|
+
data: string | null;
|
|
29
|
+
}[] | null;
|
|
30
|
+
}
|
|
31
|
+
export interface ConversationCensus {
|
|
32
|
+
/** Store files in this directory holding a storable channel key. */
|
|
33
|
+
conversations: number;
|
|
34
|
+
/** Message rows across all of them. Update rows are patches, not messages, and
|
|
35
|
+
* counting them would inflate the total. */
|
|
36
|
+
rows: number;
|
|
37
|
+
/** Newest `dateSent` anywhere in the directory, or null when empty. */
|
|
38
|
+
newest: string | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One conversation, oldest first, with every patch applied.
|
|
42
|
+
*
|
|
43
|
+
* Empty for an unstorable key, a missing directory and a conversation with no
|
|
44
|
+
* file. Those are three different facts, and none of them is an error the caller
|
|
45
|
+
* can act on differently: all three mean there is nothing stored to show.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readFoldedConversation(storeDir: string, channelKey: string): FoldedMessage[];
|
|
48
|
+
/**
|
|
49
|
+
* What this account's store actually holds.
|
|
50
|
+
*
|
|
51
|
+
* `conversations` beside `rows` is load-bearing. A tool that returns nothing
|
|
52
|
+
* looks identical to a conversation that is empty, and `conversations=0
|
|
53
|
+
* rows=0` is also exactly what a reader pointed at the wrong directory prints —
|
|
54
|
+
* which is the failure Task 2644 shipped and Task 2557 named. With both numbers,
|
|
55
|
+
* a zero result on an account showing `conversations>0` is a wrong chat id, and
|
|
56
|
+
* `conversations=0` on a live account is a dead reader.
|
|
57
|
+
*/
|
|
58
|
+
export declare function conversationCensus(storeDir: string): ConversationCensus;
|
|
59
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwCA,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb;;oCAEgC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;yBAEqB;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB;mDAC+C;IAC/C,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB;;;uEAGmE;IACnE,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EAAE,GAAG,IAAI,CAAC;CACzD;AAED,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;IACtB;iDAC6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAsGD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,aAAa,EAAE,CAiC5F;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CA+BvE"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readFoldedConversation = readFoldedConversation;
|
|
4
|
+
exports.conversationCensus = conversationCensus;
|
|
5
|
+
/**
|
|
6
|
+
* Task 2709 — reading one Telegram conversation back off disk, patches folded.
|
|
7
|
+
*
|
|
8
|
+
* The store is the only complete record of a Telegram conversation. No platform
|
|
9
|
+
* path writes a `:Message` node for Telegram: `persistMessage` in
|
|
10
|
+
* `ui/app/lib/neo4j-store.ts` has one caller, the webchat customer-inbound path,
|
|
11
|
+
* so a `:Message` carrying `channel:'telegram'` was written by an agent calling
|
|
12
|
+
* `memory-write` and records only what that agent chose to save. On 2026-08-13
|
|
13
|
+
* that was every inbound and one of ten outbound, and the agent read it back as
|
|
14
|
+
* the whole conversation.
|
|
15
|
+
*
|
|
16
|
+
* A message row is never rewritten once appended — `telegram-reader.ts` streams
|
|
17
|
+
* byte offsets as SSE resume ids, so a read-modify-write would invalidate every
|
|
18
|
+
* offset behind it. A correction is therefore a SECOND line, a
|
|
19
|
+
* `TelegramUpdateRecord`, naming its target. Reading the message rows alone
|
|
20
|
+
* yields the pre-patch text: a voice note's placeholder instead of its
|
|
21
|
+
* transcript, an edited message's original wording, and a withdrawn message with
|
|
22
|
+
* nothing saying it was withdrawn. Folding is what makes the read truthful,
|
|
23
|
+
* which is why this is a library and not four lines at the call site.
|
|
24
|
+
*
|
|
25
|
+
* Attachment ids come back RAW. `telegram-reader.ts` resolves each one to
|
|
26
|
+
* servable metadata against an uploads directory, because it is answering a
|
|
27
|
+
* browser; an agent has no use for that URL, and importing the uploads directory
|
|
28
|
+
* into the MCP would drag an HTTP concern across the plugin boundary. The three
|
|
29
|
+
* rules that ARE shared — body last-write-wins, buttons last-write-wins, deleted
|
|
30
|
+
* sticky-true — are pinned against the route by
|
|
31
|
+
* `ui/app/lib/telegram/__tests__/store-read-boundary.test.ts`.
|
|
32
|
+
*
|
|
33
|
+
* The key and filename rules are imported from `telegram-reach` rather than
|
|
34
|
+
* restated. That module is already the one derivation the store writer uses, and
|
|
35
|
+
* Task 2644 is the record of what a second derivation costs.
|
|
36
|
+
*/
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
|
+
const node_path_1 = require("node:path");
|
|
39
|
+
const index_js_1 = require("../../telegram-reach/dist/index.js");
|
|
40
|
+
function parseLines(raw) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const line of raw.split("\n")) {
|
|
43
|
+
if (!line.trim())
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
out.push(JSON.parse(line));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// A partial write from a crash mid-append is skipped, never fatal: one
|
|
50
|
+
// corrupt line must not hide every other message in the conversation.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function isUpdateRow(row) {
|
|
56
|
+
return row.kind === "update";
|
|
57
|
+
}
|
|
58
|
+
function readButtons(value) {
|
|
59
|
+
if (!Array.isArray(value))
|
|
60
|
+
return null;
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const b of value) {
|
|
63
|
+
if (!b || typeof b !== "object")
|
|
64
|
+
continue;
|
|
65
|
+
const { text, data } = b;
|
|
66
|
+
if (typeof text !== "string")
|
|
67
|
+
continue;
|
|
68
|
+
out.push({ text, data: typeof data === "string" ? data : null });
|
|
69
|
+
}
|
|
70
|
+
return out.length > 0 ? out : null;
|
|
71
|
+
}
|
|
72
|
+
/** Fold every update row into one patch per target id.
|
|
73
|
+
*
|
|
74
|
+
* Mirrors `telegram-reader.ts:77-106`. Later rows win for the body and the
|
|
75
|
+
* buttons. Attachment ids accumulate in arrival order and are de-duplicated per
|
|
76
|
+
* target, because two rows can legitimately name the same id: `appendMessage`
|
|
77
|
+
* dedupes a Telegram redelivery, but the route does not read that result — it
|
|
78
|
+
* downloads again, resolves the SAME deterministic id, and patches again.
|
|
79
|
+
* `deleted` is sticky: a withdrawal is not undone by a later transcript or
|
|
80
|
+
* media patch, and letting one clear it would hide the withdrawal. */
|
|
81
|
+
function foldPatches(rows) {
|
|
82
|
+
const out = new Map();
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
if (!isUpdateRow(row))
|
|
85
|
+
continue;
|
|
86
|
+
if (typeof row.targetMessageId !== "string")
|
|
87
|
+
continue;
|
|
88
|
+
const patch = out.get(row.targetMessageId) ?? { attachmentIds: [] };
|
|
89
|
+
if (Array.isArray(row.attachmentIds)) {
|
|
90
|
+
for (const id of row.attachmentIds) {
|
|
91
|
+
if (typeof id !== "string")
|
|
92
|
+
continue;
|
|
93
|
+
if (patch.attachmentIds.includes(id))
|
|
94
|
+
continue;
|
|
95
|
+
patch.attachmentIds.push(id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (typeof row.body === "string")
|
|
99
|
+
patch.body = row.body;
|
|
100
|
+
// Keyed on the field BEING an array, not on it holding anything. An operator
|
|
101
|
+
// editing a card down to no buttons produces `buttons: []`, and that is a
|
|
102
|
+
// removal: the route reads it as one, because an empty array is truthy
|
|
103
|
+
// there. Treating it as "no patch" would keep showing the agent buttons the
|
|
104
|
+
// customer can no longer see, with nothing failing while the two surfaces
|
|
105
|
+
// disagreed.
|
|
106
|
+
if (Array.isArray(row.buttons))
|
|
107
|
+
patch.buttons = readButtons(row.buttons) ?? [];
|
|
108
|
+
if (row.deleted === true)
|
|
109
|
+
patch.deleted = true;
|
|
110
|
+
out.set(row.targetMessageId, patch);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
function conversationFile(storeDir, channelKey) {
|
|
115
|
+
// The guard, not the path. Both halves of a key are Telegram numeric ids
|
|
116
|
+
// around an optional literal `biz`, so a key that passes cannot carry a `.`,
|
|
117
|
+
// `/` or `\`, and one that fails is refused rather than sanitised into a path
|
|
118
|
+
// nobody asked for.
|
|
119
|
+
if (!(0, index_js_1.isStorableChannelKey)(channelKey))
|
|
120
|
+
return null;
|
|
121
|
+
return (0, node_path_1.join)(storeDir, `${(0, index_js_1.channelKeyToFileStem)(channelKey)}.jsonl`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* One conversation, oldest first, with every patch applied.
|
|
125
|
+
*
|
|
126
|
+
* Empty for an unstorable key, a missing directory and a conversation with no
|
|
127
|
+
* file. Those are three different facts, and none of them is an error the caller
|
|
128
|
+
* can act on differently: all three mean there is nothing stored to show.
|
|
129
|
+
*/
|
|
130
|
+
function readFoldedConversation(storeDir, channelKey) {
|
|
131
|
+
const file = conversationFile(storeDir, channelKey);
|
|
132
|
+
if (file === null || !(0, node_fs_1.existsSync)(file))
|
|
133
|
+
return [];
|
|
134
|
+
let raw;
|
|
135
|
+
try {
|
|
136
|
+
raw = (0, node_fs_1.readFileSync)(file, "utf8");
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
const rows = parseLines(raw);
|
|
142
|
+
const patches = foldPatches(rows);
|
|
143
|
+
const out = [];
|
|
144
|
+
for (const row of rows) {
|
|
145
|
+
if (isUpdateRow(row))
|
|
146
|
+
continue;
|
|
147
|
+
if (typeof row.messageId !== "string")
|
|
148
|
+
continue;
|
|
149
|
+
if (typeof row.dateSent !== "string")
|
|
150
|
+
continue;
|
|
151
|
+
const patch = patches.get(row.messageId);
|
|
152
|
+
out.push({
|
|
153
|
+
messageId: row.messageId,
|
|
154
|
+
dateSent: row.dateSent,
|
|
155
|
+
body: patch?.body ?? (typeof row.body === "string" ? row.body : ""),
|
|
156
|
+
origin: typeof row.origin === "string" ? row.origin : "inbound",
|
|
157
|
+
fromMe: row.fromMe === true,
|
|
158
|
+
senderId: typeof row.senderId === "string" ? row.senderId : "",
|
|
159
|
+
senderName: typeof row.senderName === "string" ? row.senderName : null,
|
|
160
|
+
deleted: patch?.deleted === true,
|
|
161
|
+
attachmentIds: patch?.attachmentIds ?? [],
|
|
162
|
+
buttons: patch?.buttons ?? readButtons(row.buttons),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// Sort AFTER folding, so a patch that arrived before its target in file order
|
|
166
|
+
// still lands on it. Ties keep file order, which is arrival order.
|
|
167
|
+
return out.sort((a, b) => a.dateSent.localeCompare(b.dateSent));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* What this account's store actually holds.
|
|
171
|
+
*
|
|
172
|
+
* `conversations` beside `rows` is load-bearing. A tool that returns nothing
|
|
173
|
+
* looks identical to a conversation that is empty, and `conversations=0
|
|
174
|
+
* rows=0` is also exactly what a reader pointed at the wrong directory prints —
|
|
175
|
+
* which is the failure Task 2644 shipped and Task 2557 named. With both numbers,
|
|
176
|
+
* a zero result on an account showing `conversations>0` is a wrong chat id, and
|
|
177
|
+
* `conversations=0` on a live account is a dead reader.
|
|
178
|
+
*/
|
|
179
|
+
function conversationCensus(storeDir) {
|
|
180
|
+
const empty = { conversations: 0, rows: 0, newest: null };
|
|
181
|
+
if (!storeDir || !(0, node_fs_1.existsSync)(storeDir))
|
|
182
|
+
return empty;
|
|
183
|
+
let entries;
|
|
184
|
+
try {
|
|
185
|
+
entries = (0, node_fs_1.readdirSync)(storeDir);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return empty;
|
|
189
|
+
}
|
|
190
|
+
let conversations = 0;
|
|
191
|
+
let rows = 0;
|
|
192
|
+
let newest = null;
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
if (!entry.endsWith(".jsonl"))
|
|
195
|
+
continue;
|
|
196
|
+
const channelKey = (0, index_js_1.fileStemToChannelKey)(entry.slice(0, -".jsonl".length));
|
|
197
|
+
if (!(0, index_js_1.isStorableChannelKey)(channelKey))
|
|
198
|
+
continue;
|
|
199
|
+
conversations += 1;
|
|
200
|
+
let raw;
|
|
201
|
+
try {
|
|
202
|
+
raw = (0, node_fs_1.readFileSync)((0, node_path_1.join)(storeDir, entry), "utf8");
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
for (const row of parseLines(raw)) {
|
|
208
|
+
if (isUpdateRow(row))
|
|
209
|
+
continue;
|
|
210
|
+
rows += 1;
|
|
211
|
+
if (typeof row.dateSent !== "string")
|
|
212
|
+
continue;
|
|
213
|
+
if (newest === null || newest.localeCompare(row.dateSent) < 0)
|
|
214
|
+
newest = row.dateSent;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { conversations, rows, newest };
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { conversationCensus, readFoldedConversation } from "../index.js";
|
|
6
|
+
|
|
7
|
+
function seed(rows: unknown[], stem = "111_222"): string {
|
|
8
|
+
const dir = mkdtempSync(join(tmpdir(), "tg-store-"));
|
|
9
|
+
writeFileSync(join(dir, `${stem}.jsonl`), rows.map((r) => JSON.stringify(r)).join("\n") + "\n");
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const msg = (over: Record<string, unknown> = {}) => ({
|
|
14
|
+
messageId: "m1",
|
|
15
|
+
sessionId: "s",
|
|
16
|
+
dateSent: "2026-08-13T17:00:00.000Z",
|
|
17
|
+
body: "hello",
|
|
18
|
+
fromMe: false,
|
|
19
|
+
senderId: "222",
|
|
20
|
+
senderName: "Mcha",
|
|
21
|
+
chatId: "222",
|
|
22
|
+
channelKey: "111:222",
|
|
23
|
+
scope: "admin",
|
|
24
|
+
origin: "inbound",
|
|
25
|
+
createdAt: "2026-08-13T17:00:00.000Z",
|
|
26
|
+
...over,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("readFoldedConversation", () => {
|
|
30
|
+
it("returns both directions in dateSent order", () => {
|
|
31
|
+
const dir = seed([
|
|
32
|
+
msg({
|
|
33
|
+
messageId: "m2",
|
|
34
|
+
dateSent: "2026-08-13T17:01:00.000Z",
|
|
35
|
+
origin: "agent",
|
|
36
|
+
fromMe: true,
|
|
37
|
+
body: "hi back",
|
|
38
|
+
}),
|
|
39
|
+
msg(),
|
|
40
|
+
]);
|
|
41
|
+
const out = readFoldedConversation(dir, "111:222");
|
|
42
|
+
expect(out.map((m) => m.body)).toEqual(["hello", "hi back"]);
|
|
43
|
+
expect(out.map((m) => m.origin)).toEqual(["inbound", "agent"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("lets a later update row replace the body", () => {
|
|
47
|
+
const dir = seed([
|
|
48
|
+
msg({ body: "[voice note]" }),
|
|
49
|
+
{ kind: "update", targetMessageId: "m1", body: "the transcript", createdAt: "x" },
|
|
50
|
+
]);
|
|
51
|
+
expect(readFoldedConversation(dir, "111:222")[0].body).toBe("the transcript");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps deleted sticky once a later patch arrives", () => {
|
|
55
|
+
const dir = seed([
|
|
56
|
+
msg(),
|
|
57
|
+
{ kind: "update", targetMessageId: "m1", deleted: true, createdAt: "x" },
|
|
58
|
+
{ kind: "update", targetMessageId: "m1", attachmentIds: ["a1"], createdAt: "x" },
|
|
59
|
+
]);
|
|
60
|
+
expect(readFoldedConversation(dir, "111:222")[0].deleted).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("de-dupes attachment ids across repeated patches", () => {
|
|
64
|
+
const dir = seed([
|
|
65
|
+
msg(),
|
|
66
|
+
{ kind: "update", targetMessageId: "m1", attachmentIds: ["a1"], createdAt: "x" },
|
|
67
|
+
{ kind: "update", targetMessageId: "m1", attachmentIds: ["a1", "a2"], createdAt: "x" },
|
|
68
|
+
]);
|
|
69
|
+
expect(readFoldedConversation(dir, "111:222")[0].attachmentIds).toEqual(["a1", "a2"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("lets a later update row replace the buttons", () => {
|
|
73
|
+
const dir = seed([
|
|
74
|
+
msg({ buttons: [{ text: "old", data: "o" }] }),
|
|
75
|
+
{ kind: "update", targetMessageId: "m1", buttons: [{ text: "new", data: "n" }], createdAt: "x" },
|
|
76
|
+
]);
|
|
77
|
+
expect(readFoldedConversation(dir, "111:222")[0].buttons).toEqual([{ text: "new", data: "n" }]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("refuses an unstorable channel key rather than building a path", () => {
|
|
81
|
+
const dir = seed([msg()]);
|
|
82
|
+
expect(readFoldedConversation(dir, "../../etc:222")).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns nothing for a conversation with no file, without throwing", () => {
|
|
86
|
+
const dir = seed([msg()]);
|
|
87
|
+
expect(readFoldedConversation(dir, "111:999")).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("skips one corrupt line and keeps the rest", () => {
|
|
91
|
+
const dir = mkdtempSync(join(tmpdir(), "tg-store-"));
|
|
92
|
+
writeFileSync(join(dir, "111_222.jsonl"), JSON.stringify(msg()) + "\n{not json\n");
|
|
93
|
+
expect(readFoldedConversation(dir, "111:222")).toHaveLength(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("reads a business conversation under its three-part key", () => {
|
|
97
|
+
const dir = seed([msg({ channelKey: "111:biz:222" })], "111_biz_222");
|
|
98
|
+
expect(readFoldedConversation(dir, "111:biz:222")).toHaveLength(1);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("conversationCensus", () => {
|
|
103
|
+
it("counts conversations and message rows, and reports the newest dateSent", () => {
|
|
104
|
+
const dir = seed([
|
|
105
|
+
msg(),
|
|
106
|
+
msg({ messageId: "m2", dateSent: "2026-08-13T17:05:00.000Z" }),
|
|
107
|
+
{ kind: "update", targetMessageId: "m1", body: "patched", createdAt: "x" },
|
|
108
|
+
]);
|
|
109
|
+
writeFileSync(join(dir, "111_333.jsonl"), JSON.stringify(msg({ chatId: "333", senderId: "333" })) + "\n");
|
|
110
|
+
const census = conversationCensus(dir);
|
|
111
|
+
expect(census.conversations).toBe(2);
|
|
112
|
+
// The update row is a patch, not a message: counting it would inflate the total.
|
|
113
|
+
expect(census.rows).toBe(3);
|
|
114
|
+
expect(census.newest).toBe("2026-08-13T17:05:00.000Z");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("reports zeroes and a null newest for a directory that does not exist", () => {
|
|
118
|
+
expect(conversationCensus(join(tmpdir(), "tg-store-absent-xyz"))).toEqual({
|
|
119
|
+
conversations: 0,
|
|
120
|
+
rows: 0,
|
|
121
|
+
newest: null,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|