@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.
Files changed (100) hide show
  1. package/dist/__tests__/email-sampler-unit.test.js +22 -0
  2. package/dist/index.js +3 -0
  3. package/dist/port-resolution.js +9 -1
  4. package/package.json +1 -1
  5. package/payload/platform/lib/routine-templates/dist/roster.d.ts +17 -1
  6. package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
  7. package/payload/platform/lib/routine-templates/dist/roster.js +8 -0
  8. package/payload/platform/lib/routine-templates/dist/roster.js.map +1 -1
  9. package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +26 -1
  10. package/payload/platform/lib/routine-templates/src/roster.ts +26 -1
  11. package/payload/platform/lib/telegram-store-read/dist/index.d.ts +59 -0
  12. package/payload/platform/lib/telegram-store-read/dist/index.d.ts.map +1 -0
  13. package/payload/platform/lib/telegram-store-read/dist/index.js +219 -0
  14. package/payload/platform/lib/telegram-store-read/dist/index.js.map +1 -0
  15. package/payload/platform/lib/telegram-store-read/src/__tests__/fold.test.ts +124 -0
  16. package/payload/platform/lib/telegram-store-read/src/index.ts +262 -0
  17. package/payload/platform/lib/telegram-store-read/tsconfig.json +9 -0
  18. package/payload/platform/lib/telegram-store-read/vitest.config.ts +9 -0
  19. package/payload/platform/package.json +2 -2
  20. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +37 -4
  21. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  22. package/payload/platform/plugins/docs/references/outlook-guide.md +21 -2
  23. package/payload/platform/plugins/docs/references/telegram-guide.md +15 -1
  24. package/payload/platform/plugins/email/PLUGIN.md +10 -1
  25. package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/sample-mailbox.test.js +30 -0
  26. package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/sample-mailbox.test.js.map +1 -1
  27. package/payload/platform/plugins/email/mcp/dist/sampler/main.js +49 -3
  28. package/payload/platform/plugins/email/mcp/dist/sampler/main.js.map +1 -1
  29. package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.d.ts +5 -0
  30. package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.d.ts.map +1 -1
  31. package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.js +10 -5
  32. package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.js.map +1 -1
  33. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-section-properties-strip.test.js +11 -0
  34. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-section-properties-strip.test.js.map +1 -1
  35. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +8 -0
  37. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  38. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.d.ts +2 -0
  39. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.d.ts.map +1 -0
  40. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.js +324 -0
  41. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.js.map +1 -0
  42. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.d.ts +2 -0
  43. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.d.ts.map +1 -0
  44. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.js +102 -0
  45. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.js.map +1 -0
  46. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.d.ts +2 -0
  47. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.d.ts.map +1 -0
  48. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.js +125 -0
  49. package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.js.map +1 -0
  50. package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.d.ts +2 -0
  51. package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.d.ts.map +1 -0
  52. package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.js +109 -0
  53. package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.js.map +1 -0
  54. package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.d.ts +25 -0
  55. package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.d.ts.map +1 -0
  56. package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.js +94 -0
  57. package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.js.map +1 -0
  58. package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.d.ts +41 -0
  59. package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.d.ts.map +1 -0
  60. package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.js +122 -0
  61. package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.js.map +1 -0
  62. package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.d.ts +73 -0
  63. package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.d.ts.map +1 -0
  64. package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.js +162 -0
  65. package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.js.map +1 -0
  66. package/payload/platform/plugins/scheduling/mcp/dist/index.js +1 -1
  67. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  68. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.d.ts +2 -0
  69. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.d.ts.map +1 -0
  70. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.js +90 -0
  71. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.js.map +1 -0
  72. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js +26 -4
  73. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js.map +1 -1
  74. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.d.ts +78 -0
  75. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.d.ts.map +1 -0
  76. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.js +76 -0
  77. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.js.map +1 -0
  78. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts +8 -0
  79. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts.map +1 -1
  80. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js +9 -1
  81. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js.map +1 -1
  82. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js +15 -0
  83. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js.map +1 -1
  84. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +12 -1
  85. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  86. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +34 -1
  87. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  88. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +9 -1
  89. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  90. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +116 -4
  91. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
  92. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts +60 -2
  93. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
  94. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +101 -4
  95. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
  96. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -1
  97. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  98. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  99. package/payload/server/public/operator.html +1 -1
  100. 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}`);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent-code",
3
- "version": "0.1.603",
3
+ "version": "0.1.604",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent-code": "./dist/index.js"
@@ -76,7 +76,23 @@ export interface RoutineGraphCheck {
76
76
  readonly type: "graph";
77
77
  readonly scope: "appointments";
78
78
  }
79
- export type RoutineGateCheck = RoutineMailCheck | RoutineD1Check | RoutineWhatsappCheck | RoutineCalendarCheck | RoutineTelegramCheck | RoutineGraphCheck;
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;AAEtB;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,CAUnE;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"}
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":"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"}
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
+ });