@rubytech/create-maxy-code 0.1.547 → 0.1.549

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 (113) hide show
  1. package/dist/index.js +14 -0
  2. package/package.json +2 -2
  3. package/payload/platform/lib/routine-templates/dist/index.d.ts +22 -16
  4. package/payload/platform/lib/routine-templates/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/routine-templates/dist/index.js +15 -10
  6. package/payload/platform/lib/routine-templates/dist/index.js.map +1 -1
  7. package/payload/platform/lib/routine-templates/dist/roster.d.ts +3 -3
  8. package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
  9. package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +15 -11
  10. package/payload/platform/lib/routine-templates/src/index.ts +23 -17
  11. package/payload/platform/lib/routine-templates/src/roster.ts +3 -3
  12. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +48 -14
  13. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +11 -0
  14. package/payload/platform/plugins/docs/references/admin-ui.md +47 -13
  15. package/payload/platform/plugins/scheduling/PLUGIN.md +12 -0
  16. package/payload/platform/plugins/scheduling/mcp/dist/index.js +6 -2
  17. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/update-summary.test.d.ts +2 -0
  19. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/update-summary.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/update-summary.test.js +54 -0
  21. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/update-summary.test.js.map +1 -0
  22. package/payload/platform/plugins/scheduling/mcp/dist/lib/update-summary.d.ts +17 -0
  23. package/payload/platform/plugins/scheduling/mcp/dist/lib/update-summary.d.ts.map +1 -0
  24. package/payload/platform/plugins/scheduling/mcp/dist/lib/update-summary.js +43 -0
  25. package/payload/platform/plugins/scheduling/mcp/dist/lib/update-summary.js.map +1 -0
  26. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.d.ts +2 -0
  27. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.d.ts.map +1 -0
  28. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.js +87 -0
  29. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.js.map +1 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/description-coverage.test.d.ts +2 -0
  31. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/description-coverage.test.d.ts.map +1 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/description-coverage.test.js +123 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/description-coverage.test.js.map +1 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +36 -0
  35. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  36. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +89 -0
  37. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  38. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +13 -1
  39. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  40. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.d.ts +35 -0
  41. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.d.ts.map +1 -0
  42. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.js +56 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.js.map +1 -0
  44. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-summary.test.d.ts +2 -0
  45. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-summary.test.d.ts.map +1 -0
  46. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-summary.test.js +80 -0
  47. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-summary.test.js.map +1 -0
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts +2 -1
  49. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +19 -1
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  52. package/payload/platform/plugins/telegram/PLUGIN.md +2 -0
  53. package/payload/platform/plugins/telegram/mcp/dist/index.js +47 -47
  54. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
  55. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.d.ts +16 -0
  56. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.d.ts.map +1 -0
  57. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.js +50 -0
  58. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.js.map +1 -0
  59. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.d.ts +8 -0
  60. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.d.ts.map +1 -0
  61. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.js +12 -0
  62. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.js.map +1 -0
  63. package/payload/platform/plugins/telegram/references/setup-guide.md +2 -0
  64. package/payload/platform/plugins/telegram/skills/configure/SKILL.md +6 -1
  65. package/payload/platform/scripts/check-routine-roster-doc.mjs +127 -0
  66. package/payload/platform/services/telegram-channel/dist/notification.d.ts +8 -3
  67. package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
  68. package/payload/platform/services/telegram-channel/dist/notification.js +10 -3
  69. package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
  70. package/payload/platform/services/telegram-channel/dist/server.js +1 -1
  71. package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
  72. package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts +28 -0
  73. package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts.map +1 -1
  74. package/payload/platform/services/telegram-channel/dist/turn-follow.js +33 -1
  75. package/payload/platform/services/telegram-channel/dist/turn-follow.js.map +1 -1
  76. package/payload/platform/services/webchat-channel/dist/notification.d.ts +8 -3
  77. package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
  78. package/payload/platform/services/webchat-channel/dist/notification.js +10 -3
  79. package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
  80. package/payload/platform/services/webchat-channel/dist/server.js +1 -1
  81. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  82. package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts +28 -0
  83. package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -1
  84. package/payload/platform/services/webchat-channel/dist/turn-follow.js +33 -1
  85. package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -1
  86. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +28 -4
  87. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  88. package/payload/platform/services/whatsapp-channel/dist/notification.js +30 -7
  89. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  90. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  91. package/payload/platform/services/whatsapp-channel/dist/server.js +13 -5
  92. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  93. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +2 -0
  94. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
  95. package/payload/platform/services/whatsapp-channel/dist/targets.js +14 -1
  96. package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
  97. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +28 -0
  98. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -1
  99. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +34 -1
  100. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -1
  101. package/payload/server/{chunk-MHNHG3GL.js → chunk-G6ULACHG.js} +1 -1
  102. package/payload/server/{chunk-Q33KT7YJ.js → chunk-JOVREC6E.js} +117 -1
  103. package/payload/server/{chunk-WGMIUXXG.js → chunk-SKYZG6OT.js} +11 -0
  104. package/payload/server/{manager-W32LVPCT.js → manager-2VEJKVEO.js} +2 -2
  105. package/payload/server/maxy-edge.js +2 -2
  106. package/payload/server/public/assets/routines-OGFXwTFO.js +2 -0
  107. package/payload/server/public/assets/routines-OGFXwTFO.js.br +0 -0
  108. package/payload/server/public/assets/routines-OGFXwTFO.js.gz +0 -0
  109. package/payload/server/public/routines.html +1 -1
  110. package/payload/server/server.js +285 -170
  111. package/payload/server/public/assets/routines-Ds0yz7_C.js +0 -2
  112. package/payload/server/public/assets/routines-Ds0yz7_C.js.br +0 -0
  113. package/payload/server/public/assets/routines-Ds0yz7_C.js.gz +0 -0
@@ -4,10 +4,14 @@ import {
4
4
  HTTPException,
5
5
  Hono,
6
6
  HtmlEscapedCallbackPhase,
7
+ MANIFEST_CONTENT_TYPE,
7
8
  PUBLIC_ALLOWED_EXACT,
8
9
  PUBLIC_ALLOWED_PREFIXES,
10
+ PWA_SURFACES,
11
+ SW_CONTENT_TYPE,
9
12
  accessPasswordCollides,
10
13
  autoDeliverPremiumPlugins,
14
+ buildManifest,
11
15
  canAccessAdmin,
12
16
  checkRateLimit,
13
17
  classifyHost,
@@ -33,9 +37,13 @@ import {
33
37
  isOperatorHost,
34
38
  isPasswordValid,
35
39
  isPublicPathAllowed,
40
+ isRemoteAuthBypassPath,
36
41
  isRemoteAuthConfigured,
37
42
  migrateLegacyRemotePassword,
38
43
  parseScreencastEvent,
44
+ pwaCensus,
45
+ pwaHeadTags,
46
+ pwaSurfaceForShell,
39
47
  reconcileEnabledPluginsForAccounts,
40
48
  recordFailedAttempt,
41
49
  registerSession,
@@ -62,7 +70,7 @@ import {
62
70
  verifyRemotePassword,
63
71
  vncLog,
64
72
  walkPremiumBundles
65
- } from "./chunk-Q33KT7YJ.js";
73
+ } from "./chunk-JOVREC6E.js";
66
74
  import {
67
75
  ACCOUNTS_ROOT,
68
76
  DATA_ROOT,
@@ -203,7 +211,7 @@ import {
203
211
  waitForConnection,
204
212
  warnNonUuidAccount,
205
213
  writeChatMetadataMerged
206
- } from "./chunk-MHNHG3GL.js";
214
+ } from "./chunk-G6ULACHG.js";
207
215
  import {
208
216
  ACCOUNTS_DIR,
209
217
  AGENT_SLUG_PATTERN,
@@ -230,6 +238,7 @@ import {
230
238
  deleteAgentProjection,
231
239
  deleteConversation,
232
240
  embed,
241
+ embedForWrite,
233
242
  ensureConversation,
234
243
  ensureSessionConversation,
235
244
  fetchAccountName,
@@ -267,7 +276,7 @@ import {
267
276
  verifyAndGetConversationUpdatedAt,
268
277
  verifyConversationOwnership,
269
278
  writeAdminUserAndPerson
270
- } from "./chunk-WGMIUXXG.js";
279
+ } from "./chunk-SKYZG6OT.js";
271
280
  import {
272
281
  D1_MAX_SQL_STATEMENT_BYTES,
273
282
  D1_QUERY_MAX_BODY_BYTES,
@@ -1827,10 +1836,10 @@ var require_dist6 = __commonJS({
1827
1836
  });
1828
1837
 
1829
1838
  // server/index.ts
1830
- import { readFileSync as readFileSync47, existsSync as existsSync51, watchFile, readdirSync as readdirSync30, statSync as statSync26 } from "fs";
1839
+ import { readFileSync as readFileSync47, existsSync as existsSync51, watchFile, readdirSync as readdirSync30, statSync as statSync27 } from "fs";
1831
1840
  import { spawn as spawn3 } from "child_process";
1832
1841
  import { createHash as createHash7 } from "crypto";
1833
- import { resolve as resolve45, join as join54, basename as basename17 } from "path";
1842
+ import { resolve as resolve45, join as join55, basename as basename18 } from "path";
1834
1843
  import { homedir as homedir4 } from "os";
1835
1844
 
1836
1845
  // server/lib/route-cache-watchers.ts
@@ -2637,99 +2646,6 @@ async function resolveNotifyAccountName(accountId, fetch2) {
2637
2646
  return name;
2638
2647
  }
2639
2648
 
2640
- // app/lib/pwa.ts
2641
- var PWA_SURFACES = [
2642
- {
2643
- surface: "admin",
2644
- shellFile: "index.html",
2645
- manifestPath: "/manifest-admin.webmanifest",
2646
- nameSuffix: "",
2647
- startUrl: "/",
2648
- scope: "/",
2649
- id: "/?pwa=admin"
2650
- },
2651
- {
2652
- surface: "operator",
2653
- shellFile: "operator.html",
2654
- manifestPath: "/manifest-operator.webmanifest",
2655
- nameSuffix: " Operator",
2656
- startUrl: "/",
2657
- scope: "/",
2658
- id: "/?pwa=operator"
2659
- },
2660
- {
2661
- surface: "chat",
2662
- shellFile: "chat.html",
2663
- manifestPath: "/manifest-chat.webmanifest",
2664
- nameSuffix: " Chat",
2665
- startUrl: "/chat",
2666
- scope: "/chat",
2667
- id: "/chat?pwa=chat"
2668
- }
2669
- ];
2670
- var MANIFEST_CONTENT_TYPE = "application/manifest+json";
2671
- var SW_CONTENT_TYPE = "application/javascript";
2672
- function pwaSurfaceForShell(file) {
2673
- return PWA_SURFACES.find((s) => s.shellFile === file);
2674
- }
2675
- function buildManifest(s, b) {
2676
- const name = `${b.productName}${s.nameSuffix}`;
2677
- return {
2678
- id: s.id,
2679
- name,
2680
- short_name: name,
2681
- start_url: s.startUrl,
2682
- scope: s.scope,
2683
- display: "standalone",
2684
- theme_color: b.themeColor,
2685
- background_color: b.backgroundColor,
2686
- // Explicit 192/512 `any` icons (iOS/macOS composite transparency onto black,
2687
- // so these are opaque) plus a wider-safe-zone `maskable` entry for Android's
2688
- // circular crop. background_color sets only the splash; the tile background
2689
- // is baked into the PNGs.
2690
- icons: [
2691
- { src: b.appIcon192, sizes: "192x192", purpose: "any" },
2692
- { src: b.appIcon512, sizes: "512x512", purpose: "any" },
2693
- { src: b.maskableIcon, sizes: "512x512", purpose: "maskable" }
2694
- ]
2695
- };
2696
- }
2697
- function escAttr(s) {
2698
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2699
- }
2700
- function swRegisterScript(s) {
2701
- return [
2702
- "<script>",
2703
- "if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').catch(function(e){",
2704
- `try{fetch('/api/_client-error',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({kind:'event',source:'pwa',op:'sw-register-fail',surface:'${s.surface}',scope:'${s.scope}',err:String((e&&e.message)||e)}),keepalive:true}).catch(function(){});}catch(_){}`,
2705
- "});}",
2706
- "</script>"
2707
- ].join("\n");
2708
- }
2709
- function installHeadLines(manifestPath, b) {
2710
- return [
2711
- `<link rel="manifest" href="${escAttr(manifestPath)}">`,
2712
- `<link rel="apple-touch-icon" href="${escAttr(b.appleTouchIcon)}">`,
2713
- // Both the standards-based and the legacy Apple capable meta: Chromium
2714
- // deprecates `apple-mobile-web-app-capable` in favour of
2715
- // `mobile-web-app-capable`, while iOS Safari still reads the apple one.
2716
- '<meta name="mobile-web-app-capable" content="yes">',
2717
- '<meta name="apple-mobile-web-app-capable" content="yes">',
2718
- `<meta name="theme-color" content="${escAttr(b.themeColor)}">`
2719
- ];
2720
- }
2721
- function pwaHeadTags(s, b) {
2722
- return [...installHeadLines(s.manifestPath, b), swRegisterScript(s)].join("\n");
2723
- }
2724
- function pwaCensus(d) {
2725
- const manifestLinked = PWA_SURFACES.filter((s) => {
2726
- const html = d.shellHtml(s.shellFile);
2727
- return html != null && html.includes(`"${s.manifestPath}"`);
2728
- }).length;
2729
- const iconsPresent = d.iconExists() ? PWA_SURFACES.length : 0;
2730
- return { surfaces: PWA_SURFACES.length, manifestLinked, iconsPresent };
2731
- }
2732
-
2733
2649
  // server/webchat-descriptor.ts
2734
2650
  import { resolve as resolve2, join as join4 } from "path";
2735
2651
  function webchatGatewayUrl() {
@@ -4157,11 +4073,11 @@ function findExistingJsonlForSessionId(claudeConfigDir2, sessionId) {
4157
4073
  } catch {
4158
4074
  return null;
4159
4075
  }
4160
- const basename18 = `${sessionId}.jsonl`;
4076
+ const basename19 = `${sessionId}.jsonl`;
4161
4077
  for (const slug of slugs) {
4162
- const live = join7(projectsRoot, slug, basename18);
4078
+ const live = join7(projectsRoot, slug, basename19);
4163
4079
  if (existsSync3(live)) return live;
4164
- const archived = join7(projectsRoot, slug, "archive", basename18);
4080
+ const archived = join7(projectsRoot, slug, "archive", basename19);
4165
4081
  if (existsSync3(archived)) return archived;
4166
4082
  }
4167
4083
  return null;
@@ -4947,15 +4863,24 @@ function createScheduleInjectRoutes(deps) {
4947
4863
  console.error(`${TAG5} op=inject-spawn eventId=${eventId} sessionId=${sessionId2} result=ok status=-`);
4948
4864
  return c.json({ ok: true, sessionKey: sessionId2, ...transcriptBoundary(sessionId2) }, 200);
4949
4865
  }
4950
- const effectiveAccount = houseAccountId;
4866
+ const resolution = deps.telegramAccountForDestination(destination);
4867
+ if (resolution.kind === "none") {
4868
+ console.error(`${TAG5} op=schedule-telegram-reject eventId=${eventId} destination=${destination} reason=unresolved-effective-account`);
4869
+ return c.json({ ok: false, error: "unresolved-effective-account" }, 403);
4870
+ }
4871
+ if (resolution.kind === "ambiguous") {
4872
+ console.error(`${TAG5} op=telegram-destination-ambiguous eventId=${eventId} destination=${destination} accounts=${resolution.count} dispatched=no`);
4873
+ return c.json({ ok: false, error: "telegram-destination-ambiguous" }, 403);
4874
+ }
4875
+ const effectiveAccount = resolution.accountId;
4951
4876
  console.error(`${TAG5} op=effective-account eventId=${eventId} effectiveAccount=${effectiveAccount}`);
4952
4877
  if (eventAccountId === null || eventAccountId !== effectiveAccount) {
4953
4878
  console.error(`${TAG5} op=event-account-scope-mismatch eventId=${eventId} eventAccount=${eventAccountId ?? "-"} effectiveAccount=${effectiveAccount} destination=${destination} dispatched=no`);
4954
4879
  return c.json({ ok: false, error: "event-account-scope-mismatch" }, 403);
4955
4880
  }
4956
- const botToken = account.config.telegram?.adminBotToken;
4881
+ const botToken = resolution.botToken;
4957
4882
  if (!botToken) {
4958
- console.error(`${TAG5} reject reason=no-admin-bot-token eventId=${eventId}`);
4883
+ console.error(`${TAG5} reject reason=no-admin-bot-token eventId=${eventId} accountId=${effectiveAccount}`);
4959
4884
  return c.json({ ok: false, error: "no-admin-bot-token" }, 400);
4960
4885
  }
4961
4886
  const chatId = Number(destination);
@@ -4963,7 +4888,11 @@ function createScheduleInjectRoutes(deps) {
4963
4888
  const reply = (text) => deps.sendTelegramText(botToken, chatId, text);
4964
4889
  try {
4965
4890
  await deps.tgHandleInbound({
4966
- accountId: houseAccountId,
4891
+ // Task 2365 — the resolved account, not the house. After the
4892
+ // per-account webhook the bot IS that account's own, so transport and
4893
+ // session scope are one account and Telegram needs no
4894
+ // effectiveAccountId companion field the way WhatsApp does above.
4895
+ accountId: effectiveAccount,
4967
4896
  senderId: destination,
4968
4897
  chatId: destination,
4969
4898
  role: "admin",
@@ -5086,7 +5015,7 @@ async function defaultHttpGet(url) {
5086
5015
  }
5087
5016
  }
5088
5017
  async function defaultResolve(accountId) {
5089
- const { resolveSocket: resolveSocket2 } = await import("./manager-W32LVPCT.js");
5018
+ const { resolveSocket: resolveSocket2 } = await import("./manager-2VEJKVEO.js");
5090
5019
  return resolveSocket2(accountId);
5091
5020
  }
5092
5021
  async function fetchAvatar(accountId, jid, deps = {}) {
@@ -13044,9 +12973,9 @@ function configDirName() {
13044
12973
  }
13045
12974
  return ".maxy";
13046
12975
  }
13047
- function secretPath(botType) {
13048
- const filename = botType === "admin" ? ".telegram-admin-webhook-secret" : ".telegram-webhook-secret";
13049
- return resolve17(homedir2(), configDirName(), filename);
12976
+ function secretPath(botType, accountId) {
12977
+ const stem = botType === "admin" ? ".telegram-admin-webhook-secret" : ".telegram-webhook-secret";
12978
+ return resolve17(homedir2(), configDirName(), `${stem}.${accountId}`);
13050
12979
  }
13051
12980
  var app10 = new Hono();
13052
12981
  app10.post("/", async (c) => {
@@ -13056,15 +12985,25 @@ app10.post("/", async (c) => {
13056
12985
  console.error(`${TAG19} op=reject reason=missing-bot-param`);
13057
12986
  return c.json({ ok: false }, 400);
13058
12987
  }
13059
- const sp = secretPath(botType);
12988
+ const accountParam = c.req.query("account");
12989
+ if (!accountParam) {
12990
+ console.error(`${TAG19} op=reject reason=missing-account-param botType=${botType}`);
12991
+ return c.json({ ok: false }, 401);
12992
+ }
12993
+ const account = listValidAccounts().find((a) => a.accountId === accountParam) ?? null;
12994
+ if (!account) {
12995
+ console.error(`${TAG19} op=reject reason=bad-account botType=${botType}`);
12996
+ return c.json({ ok: false }, 401);
12997
+ }
12998
+ const sp = secretPath(botType, account.accountId);
13060
12999
  if (!existsSync21(sp)) {
13061
- console.error(`${TAG19} op=reject reason=no-secret-file botType=${botType}`);
13000
+ console.error(`${TAG19} op=reject reason=no-secret-file botType=${botType} accountId=${account.accountId}`);
13062
13001
  return c.json({ ok: false }, 401);
13063
13002
  }
13064
13003
  const expected = readFileSync23(sp, "utf-8").trim();
13065
13004
  const got = c.req.header("x-telegram-bot-api-secret-token") ?? "";
13066
13005
  if (got !== expected) {
13067
- console.error(`${TAG19} op=reject reason=bad-secret botType=${botType}`);
13006
+ console.error(`${TAG19} op=reject reason=bad-secret botType=${botType} accountId=${account.accountId}`);
13068
13007
  return c.json({ ok: false }, 401);
13069
13008
  }
13070
13009
  let update;
@@ -13073,11 +13012,6 @@ app10.post("/", async (c) => {
13073
13012
  } catch {
13074
13013
  return c.json({ ok: true }, 200);
13075
13014
  }
13076
- const account = resolveAccount();
13077
- if (!account) {
13078
- console.error(`${TAG19} op=reject reason=no-account`);
13079
- return c.json({ ok: true }, 200);
13080
- }
13081
13015
  const decision = routeTelegramUpdate({ update, botType, config: account.config.telegram ?? {} });
13082
13016
  if (decision.kind === "ignore") {
13083
13017
  console.error(`${TAG19} op=ignore reason=${decision.reason}`);
@@ -13123,6 +13057,12 @@ app10.post("/", async (c) => {
13123
13057
  });
13124
13058
  var telegram_default = app10;
13125
13059
 
13060
+ // server/telegram-mount.ts
13061
+ function mountTelegramRoutes(app77, telegramRoutes) {
13062
+ app77.route("/api/telegram", telegramRoutes);
13063
+ app77.route("/api/telegram/", telegramRoutes);
13064
+ }
13065
+
13126
13066
  // server/routes/quickbooks.ts
13127
13067
  import { join as join30 } from "path";
13128
13068
  import { existsSync as existsSync22, readFileSync as readFileSync24, writeFileSync as writeFileSync9, mkdirSync as mkdirSync4, rmSync as rmSync3, renameSync as renameSync5 } from "fs";
@@ -24070,7 +24010,7 @@ var ROUTINE_COLS = `e.eventId AS eventId, e.name AS name, e.eventStatus AS event
24070
24010
  e.timezone AS timezone, e.startDate AS startDate,
24071
24011
  e.agentChannel AS agentChannel, e.agentDestination AS agentDestination, e.agentPrompt AS agentPrompt,
24072
24012
  e.actionPlugin AS actionPlugin, e.actionTool AS actionTool, e.actionArgs AS actionArgs,
24073
- e.notes AS notes, e.routineDescription AS routineDescription,
24013
+ e.notes AS notes, e.description AS description,
24074
24014
  e.lastDispatchResult AS lastDispatchResult, e.lastDispatchAt AS lastDispatchAt,
24075
24015
  e.suspendedInBulk AS suspendedInBulk`;
24076
24016
  function mapRoutine(r) {
@@ -24091,11 +24031,11 @@ function mapRoutine(r) {
24091
24031
  actionTool: g("actionTool"),
24092
24032
  actionArgs: g("actionArgs"),
24093
24033
  notes: g("notes"),
24094
- // Task 2214read-only on the wire. Deliberately absent from the PATCH
24095
- // field set below: the roster is its only writer, so a PATCH that accepted
24096
- // it would be discarded by the next seed, and silently accepting an edit
24097
- // that the next upgrade throws away is worse than refusing it.
24098
- routineDescription: g("routineDescription"),
24034
+ // Task 2361operator-writable, and the only caption any routines surface
24035
+ // shows. Never dispatched: agentPrompt is the text a fire sends. PATCH
24036
+ // accepts it below and stamps operatorEdited, which is what stops the next
24037
+ // roster version bump overwriting the operator's wording.
24038
+ description: g("description"),
24099
24039
  lastDispatchResult: g("lastDispatchResult"),
24100
24040
  lastDispatchAt: g("lastDispatchAt"),
24101
24041
  // Task 2106 — on the wire so the page can gate Resume all on the set the
@@ -24391,23 +24331,29 @@ app51.patch("/:eventId", requireAdminSession, async (c) => {
24391
24331
  console.error(`[admin:routines] op=patch id=${id82} fields=agentChannel+agentDestination outcome=rejected reason=invalid-channel channel=${body.agentChannel}`);
24392
24332
  return c.json({ error: `Unknown dispatch channel "${body.agentChannel}".` }, 400);
24393
24333
  }
24334
+ const describing = "description" in body;
24394
24335
  let timezone = null;
24395
24336
  let storedPrompt = null;
24396
24337
  let storedRecurrence = null;
24397
24338
  let storedActionPlugin = null;
24398
- if (typeof body.recurrence === "string" || binding || clearing) {
24339
+ let storedName = null;
24340
+ let storedDescription = null;
24341
+ if (typeof body.recurrence === "string" || binding || clearing || describing) {
24399
24342
  const preSession = getSession();
24400
24343
  try {
24401
24344
  const pre = await preSession.run(
24402
24345
  `MATCH (e:Event {eventId: $eventId, accountId: $accountId})
24403
24346
  RETURN e.timezone AS timezone, e.agentPrompt AS agentPrompt,
24404
- e.recurrence AS recurrence, e.actionPlugin AS actionPlugin`,
24347
+ e.recurrence AS recurrence, e.actionPlugin AS actionPlugin,
24348
+ e.name AS name, e.description AS description`,
24405
24349
  { eventId, accountId }
24406
24350
  );
24407
24351
  timezone = pre.records[0]?.get("timezone") ?? null;
24408
24352
  storedPrompt = pre.records[0]?.get("agentPrompt") ?? null;
24409
24353
  storedRecurrence = pre.records[0]?.get("recurrence") ?? null;
24410
24354
  storedActionPlugin = pre.records[0]?.get("actionPlugin") ?? null;
24355
+ storedName = pre.records[0]?.get("name") ?? null;
24356
+ storedDescription = pre.records[0]?.get("description") ?? null;
24411
24357
  } catch (err) {
24412
24358
  const message = err instanceof Error ? err.message : String(err);
24413
24359
  console.error(`[admin:routines] op=patch id=${id82} outcome=rejected reason=tz-read-fail err="${message}"`);
@@ -24475,6 +24421,27 @@ app51.patch("/:eventId", requireAdminSession, async (c) => {
24475
24421
  setClauses.push("e.operatorEdited = $operatorEdited");
24476
24422
  params.operatorEdited = (/* @__PURE__ */ new Date()).toISOString();
24477
24423
  }
24424
+ if (describing) {
24425
+ const raw = body.description;
24426
+ if (typeof raw !== "string" && raw !== null) {
24427
+ console.error(`[admin:routines] op=patch id=${id82} fields=description outcome=rejected reason=not-a-string`);
24428
+ return c.json({ error: "A description must be text, or null to clear it." }, 400);
24429
+ }
24430
+ const next = typeof raw === "string" && raw.trim().length > 0 ? raw : null;
24431
+ setClauses.push("e.description = $description");
24432
+ params.description = next;
24433
+ fields.push("description");
24434
+ setClauses.push("e.operatorEdited = $operatorEdited");
24435
+ params.operatorEdited = (/* @__PURE__ */ new Date()).toISOString();
24436
+ if (next !== storedDescription) {
24437
+ const parts = [`Event: ${storedName ?? ""}`, next ?? ""].filter(Boolean);
24438
+ const embedding = await embedForWrite(parts.join(" \u2014 "));
24439
+ if (embedding.length > 0) {
24440
+ setClauses.push("e.embedding = $embedding");
24441
+ params.embedding = embedding;
24442
+ }
24443
+ }
24444
+ }
24478
24445
  if (typeof body.recurrence === "string") {
24479
24446
  try {
24480
24447
  const interval = parseExpression(body.recurrence, { currentDate: /* @__PURE__ */ new Date(), tz: timezone ?? void 0 });
@@ -28790,18 +28757,23 @@ function createWaChannelRoutes(deps) {
28790
28757
  if (typeof senderId !== "string" || typeof text !== "string") {
28791
28758
  return c.json({ error: "senderId and text required" }, 400);
28792
28759
  }
28760
+ const accountId = body?.accountId;
28761
+ if (typeof accountId !== "string" || accountId.length === 0) {
28762
+ console.error(`[whatsapp-native] op=reply-rejected senderId=${senderId} reason=no-account`);
28763
+ return c.json({ error: "accountId required" }, 400);
28764
+ }
28793
28765
  const bytes = Buffer.byteLength(text, "utf8");
28794
28766
  try {
28795
- await deps.sendOutbound(senderId, text);
28767
+ await deps.sendOutbound(accountId, senderId, text);
28796
28768
  } catch (err) {
28797
28769
  const message = err instanceof Error ? err.message : String(err);
28798
28770
  const terminal = err instanceof WaReplyError ? err.terminal : false;
28799
28771
  console.error(
28800
- `[whatsapp-native] op=reply-failed senderId=${senderId} bytes=${bytes} terminal=${terminal} error=${message}`
28772
+ `[whatsapp-native] op=reply-failed senderId=${senderId} account=${accountId} bytes=${bytes} terminal=${terminal} error=${message}`
28801
28773
  );
28802
28774
  return c.json({ error: message, terminal }, 500);
28803
28775
  }
28804
- console.error(`[whatsapp-native] op=reply-dispatch senderId=${senderId} bytes=${bytes}`);
28776
+ console.error(`[whatsapp-native] op=reply-dispatch senderId=${senderId} account=${accountId} bytes=${bytes}`);
28805
28777
  deps.onReplyDispatch?.(senderId, Date.now());
28806
28778
  return c.json({ ok: true });
28807
28779
  });
@@ -28813,13 +28785,18 @@ function createWaChannelRoutes(deps) {
28813
28785
  if (typeof senderId !== "string" || !Array.isArray(files) || files.length === 0 || !files.every((f) => typeof f === "string")) {
28814
28786
  return c.json({ error: "senderId and a non-empty files string array required" }, 400);
28815
28787
  }
28788
+ const accountId = body?.accountId;
28789
+ if (typeof accountId !== "string" || accountId.length === 0) {
28790
+ console.error(`[whatsapp-native] op=reply-document-rejected senderId=${senderId} reason=no-account`);
28791
+ return c.json({ error: "accountId required" }, 400);
28792
+ }
28816
28793
  const results = [];
28817
28794
  for (let i = 0; i < files.length; i++) {
28818
28795
  const file = files[i];
28819
28796
  console.error(`[whatsapp-native] op=reply-document-request senderId=${senderId} file=${file}`);
28820
28797
  let outcome;
28821
28798
  try {
28822
- outcome = await deps.sendDocument(senderId, file, i === 0 ? caption : void 0);
28799
+ outcome = await deps.sendDocument(accountId, senderId, file, i === 0 ? caption : void 0);
28823
28800
  } catch (err) {
28824
28801
  const message = err instanceof Error ? err.message : String(err);
28825
28802
  const terminal = err instanceof WaReplyError ? err.terminal : false;
@@ -28899,6 +28876,11 @@ function createWaChannelRoutes(deps) {
28899
28876
  if (typeof senderId !== "string" || typeof sessionId !== "string" || typeof replied !== "boolean") {
28900
28877
  return c.json({ error: "senderId, sessionId and replied required" }, 400);
28901
28878
  }
28879
+ const accountId = body?.accountId;
28880
+ if (typeof accountId !== "string" || accountId.length === 0) {
28881
+ console.error(`[whatsapp-native] op=turn-end-rejected senderId=${senderId} reason=no-account`);
28882
+ return c.json({ error: "accountId required" }, 400);
28883
+ }
28902
28884
  const interactive = body?.interactive === true;
28903
28885
  const finalText = typeof body?.finalText === "string" ? body.finalText : "";
28904
28886
  const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
@@ -28908,6 +28890,9 @@ function createWaChannelRoutes(deps) {
28908
28890
  const consumed2 = num(body?.consumed);
28909
28891
  const source = typeof body?.source === "string" ? body.source : null;
28910
28892
  const sid = sessionId.slice(0, 8);
28893
+ console.error(
28894
+ `[whatsapp-native] op=turn-context sessionId=${sid} cacheReadTokens=${num(body?.cacheReadTokens)} inputTokens=${num(body?.inputTokens)} outputTokens=${num(body?.outputTokens)} thinkingBlocks=${num(body?.thinkingBlocks)} thinkingChars=${num(body?.thinkingChars)}`
28895
+ );
28911
28896
  const deliver = interactive && !replied && finalText.trim().length > 0;
28912
28897
  const decision = `sessionId=${sid} replied=${replied ? "yes" : "no"} interactive=${interactive ? "yes" : "no"} source=${source ?? "unset"} finalBytes=${finalBytes} queueDepth=${queueDepth} pendingTurns=${pendingTurns} consumed=${consumed2}`;
28913
28898
  if (!replied && finalBytes > 0 && !deliver) {
@@ -28919,7 +28904,7 @@ function createWaChannelRoutes(deps) {
28919
28904
  if (deliver) {
28920
28905
  const bytes = Buffer.byteLength(finalText, "utf8");
28921
28906
  try {
28922
- await deps.sendOutbound(senderId, finalText);
28907
+ await deps.sendOutbound(accountId, senderId, finalText);
28923
28908
  } catch (err) {
28924
28909
  const message = err instanceof Error ? err.message : String(err);
28925
28910
  console.error(
@@ -29003,6 +28988,13 @@ var WaGateway = class {
29003
28988
  this.deps = deps;
29004
28989
  }
29005
28990
  hub = new InboundHub();
28991
+ // Task 2363 — keyed by (effective account, sender) through the same `hubKey`
28992
+ // the hub uses, never by the number alone. A phone bound to two accounts holds
28993
+ // one reply closure per account, and the last inbound used to overwrite the
28994
+ // other account's route, so an answer composed for one account went out on the
28995
+ // other's socket. The account is the EFFECTIVE session account, which is what
28996
+ // `hub.deliver` below keys on and what the channel server sends back: the
28997
+ // socket owner is not addressable by the requester.
29006
28998
  replies = /* @__PURE__ */ new Map();
29007
28999
  // Per-sender document-delivery context, set on every inbound. Its presence is
29008
29000
  // the reply-only guard: a sender with no inbound has no context, so a file
@@ -29034,6 +29026,16 @@ var WaGateway = class {
29034
29026
  mediaRaces = 0;
29035
29027
  mediaRaceSenders = /* @__PURE__ */ new Set();
29036
29028
  mediaStanzaExpired = 0;
29029
+ // Task 2363 — the outbound-account census, reset on every print. `sends` is
29030
+ // every sendOutbound call; `mismatched` is the subset that missed for
29031
+ // (account, sender) while ANOTHER account held a live route for that sender.
29032
+ // Before this task those sends succeeded on the other account's socket and
29033
+ // emitted no error, so nothing but a reconciliation count could see them; they
29034
+ // now throw, and this counts them at the point they are refused. A cold miss
29035
+ // (no route under any account) is deliberately excluded — it is an ordinary
29036
+ // reply to a sender with no conversation, not a crossed one.
29037
+ outboundSends = 0;
29038
+ outboundMismatched = 0;
29037
29039
  /** Task 2110 — the WhatsApp manager reports that a non-audio media stanza for
29038
29040
  * this sender has started downloading. The gateway cannot derive this itself:
29039
29041
  * it first learns of the media when the completed inbound arrives, which is
@@ -29090,8 +29092,8 @@ var WaGateway = class {
29090
29092
  routes() {
29091
29093
  return createWaChannelRoutes({
29092
29094
  hub: this.hub,
29093
- sendOutbound: (senderId, text) => this.sendOutbound(senderId, text),
29094
- sendDocument: (senderId, filePath, caption) => this.sendDocument(senderId, filePath, caption),
29095
+ sendOutbound: (accountId, senderId, text) => this.sendOutbound(accountId, senderId, text),
29096
+ sendDocument: (accountId, senderId, filePath, caption) => this.sendDocument(accountId, senderId, filePath, caption),
29095
29097
  onReady: (accountId, senderId) => {
29096
29098
  const count = this.hub.markReady(accountId, senderId, Date.now());
29097
29099
  if (count > 0) {
@@ -29225,7 +29227,7 @@ var WaGateway = class {
29225
29227
  const openable = input.media.filter((m) => m.type !== "audio");
29226
29228
  const mediaField = openable.length > 0 ? `media=${openable.map((m) => m.type).join(",")} mediaPaths=${openable.map((m) => m.path).join(",")}` : input.media.some((m) => m.type === "audio") ? "media=audio mediaPath=transcribed" : "media=none";
29227
29229
  const source = input.source ?? "user";
29228
- this.replies.set(senderId, input.reply);
29230
+ this.replies.set(hubKey(input.effectiveAccountId, senderId), input.reply);
29229
29231
  this.docContexts.set(senderId, { accountId: input.accountId, maxyAccountId: input.effectiveAccountId });
29230
29232
  console.error(
29231
29233
  `[whatsapp-native] op=doc-context senderId=${senderId} accountId=${input.accountId} maxyAccountId=${input.effectiveAccountId}`
@@ -29336,17 +29338,41 @@ var WaGateway = class {
29336
29338
  input.role === "public" && input.personId ? input.personId : void 0
29337
29339
  );
29338
29340
  }
29339
- async sendOutbound(senderId, text) {
29340
- const reply = this.replies.get(senderId);
29341
- if (!reply) throw new Error(`wa-gateway: no reply route for sender ${senderId}`);
29341
+ async sendOutbound(accountId, senderId, text) {
29342
+ this.outboundSends++;
29343
+ const reply = this.replies.get(hubKey(accountId, senderId));
29344
+ if (!reply) {
29345
+ const suffix = hubKey("", senderId);
29346
+ if ([...this.replies.keys()].some((k) => k.endsWith(suffix))) {
29347
+ this.outboundMismatched++;
29348
+ console.error(
29349
+ `[whatsapp-native] op=outbound-account-miss senderId=${senderId} requested=${accountId} held=other-account`
29350
+ );
29351
+ }
29352
+ throw new Error(`wa-gateway: no reply route for account ${accountId} sender ${senderId}`);
29353
+ }
29342
29354
  await reply(text);
29343
29355
  }
29356
+ /** Task 2363 — read and reset the outbound-account census. Printed on the
29357
+ * duplicate-sender audit tick; see server/index.ts. */
29358
+ takeOutboundAccountCensus() {
29359
+ const census = { sends: this.outboundSends, mismatched: this.outboundMismatched };
29360
+ this.outboundSends = 0;
29361
+ this.outboundMismatched = 0;
29362
+ return census;
29363
+ }
29344
29364
  /** Deliver a file to a sender as a WhatsApp document. Throws `WaReplyError`
29345
29365
  * (reply-only guard) when no inbound has registered a document context for
29346
29366
  * the sender — file delivery is only ever a reply on a live conversation. */
29347
- async sendDocument(senderId, filePath, caption) {
29367
+ async sendDocument(accountId, senderId, filePath, caption) {
29348
29368
  const ctx = this.docContexts.get(senderId);
29349
29369
  if (!ctx) throw new WaReplyError(`no conversation for sender ${senderId}`, false);
29370
+ if (ctx.maxyAccountId !== accountId) {
29371
+ console.error(
29372
+ `[whatsapp-native] op=document-account-mismatch senderId=${senderId} requested=${accountId} context=${ctx.maxyAccountId}`
29373
+ );
29374
+ throw new WaReplyError(`no conversation for sender ${senderId} on account ${accountId}`, false);
29375
+ }
29350
29376
  return this.deps.sendDocument({ senderId, accountId: ctx.accountId, maxyAccountId: ctx.maxyAccountId, filePath, caption });
29351
29377
  }
29352
29378
  };
@@ -29782,6 +29808,10 @@ function createWebchatChannelRoutes(deps) {
29782
29808
  const finalBytes = typeof body?.finalBytes === "number" && Number.isFinite(body.finalBytes) ? body.finalBytes : 0;
29783
29809
  const k = key.slice(0, 8);
29784
29810
  const sid = sessionId.slice(0, 8);
29811
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
29812
+ console.error(
29813
+ `[webchat-native] op=turn-context sessionId=${sid} cacheReadTokens=${num(body?.cacheReadTokens)} inputTokens=${num(body?.inputTokens)} outputTokens=${num(body?.outputTokens)} thinkingBlocks=${num(body?.thinkingBlocks)} thinkingChars=${num(body?.thinkingChars)}`
29814
+ );
29785
29815
  const repliedField = role === "public" ? ` replied=${replied ? "yes" : "no"}` : "";
29786
29816
  console.error(`[webchat-native] op=turn-end channel=webchat key=${k} role=${role} sessionId=${sid}${repliedField} trailing=${finalBytes}`);
29787
29817
  if (role === "admin" && finalBytes === 0) {
@@ -30964,6 +30994,10 @@ function createTelegramChannelRoutes(deps) {
30964
30994
  const interactive = body?.interactive === true;
30965
30995
  const finalText = typeof body?.finalText === "string" ? body.finalText : "";
30966
30996
  const sid = sessionId.slice(0, 8);
30997
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
30998
+ console.error(
30999
+ `[telegram-native] op=turn-context sessionId=${sid} cacheReadTokens=${num(body?.cacheReadTokens)} inputTokens=${num(body?.inputTokens)} outputTokens=${num(body?.outputTokens)} thinkingBlocks=${num(body?.thinkingBlocks)} thinkingChars=${num(body?.thinkingChars)}`
31000
+ );
30967
31001
  const deliver = interactive && !replied && finalText.trim().length > 0;
30968
31002
  if (deliver) {
30969
31003
  const bytes = Buffer.byteLength(finalText, "utf8");
@@ -31585,6 +31619,50 @@ function runPublicAddressCollisionAudit(deps) {
31585
31619
  );
31586
31620
  }
31587
31621
 
31622
+ // server/telegram-audit.ts
31623
+ import { statSync as statSync25 } from "fs";
31624
+ import { basename as basename17, dirname as dirname17, join as join54 } from "path";
31625
+ function buildTelegramAuditLines(input) {
31626
+ const lines = [];
31627
+ for (const account of input.accounts) {
31628
+ const tg = account.telegram;
31629
+ if (!tg) continue;
31630
+ const botTypes = [];
31631
+ if (tg.adminBotToken) botTypes.push("admin");
31632
+ if (tg.publicBotToken) botTypes.push("public");
31633
+ if (botTypes.length === 0) continue;
31634
+ const last = input.lastInboundAt(account.accountId);
31635
+ const age = last === null ? "never" : String(input.now - last);
31636
+ for (const botType of botTypes) {
31637
+ const secretFile = input.secretFileExists(botType, account.accountId) ? "present" : "absent";
31638
+ lines.push(
31639
+ `[telegram-audit] op=configured accountId=${account.accountId} botType=${botType} secretFile=${secretFile} lastInboundAgeMs=${age}`
31640
+ );
31641
+ }
31642
+ }
31643
+ return lines;
31644
+ }
31645
+ function newestTelegramSessionByAccount() {
31646
+ const out = /* @__PURE__ */ new Map();
31647
+ const cfg = claudeConfigDir();
31648
+ if (!cfg) return out;
31649
+ const projectsRoot = join54(cfg, "projects");
31650
+ for (const { path } of enumerateJsonls(projectsRoot)) {
31651
+ const sessionId = basename17(path).replace(/\.jsonl$/, "");
31652
+ const meta = readSidecarMeta(join54(dirname17(path), `${sessionId}.meta.json`));
31653
+ if (meta.channel !== "telegram" || !meta.accountId) continue;
31654
+ let mtime;
31655
+ try {
31656
+ mtime = statSync25(path).mtimeMs;
31657
+ } catch {
31658
+ continue;
31659
+ }
31660
+ const prev = out.get(meta.accountId);
31661
+ if (prev === void 0 || mtime > prev) out.set(meta.accountId, mtime);
31662
+ }
31663
+ return out;
31664
+ }
31665
+
31588
31666
  // app/lib/whatsapp/recall-store-backfill.ts
31589
31667
  function backfillMisfiledRecords(params) {
31590
31668
  const { houseAccountId, isTargetAccount } = params;
@@ -31666,7 +31744,7 @@ function broadcastAdminShutdown(reason) {
31666
31744
 
31667
31745
  // ../lib/entitlement/src/index.ts
31668
31746
  import { createPublicKey, createHash as createHash6, verify as cryptoVerify } from "crypto";
31669
- import { existsSync as existsSync50, readFileSync as readFileSync46, statSync as statSync25 } from "fs";
31747
+ import { existsSync as existsSync50, readFileSync as readFileSync46, statSync as statSync26 } from "fs";
31670
31748
  import { resolve as resolve44 } from "path";
31671
31749
 
31672
31750
  // ../lib/entitlement/src/canonicalize.ts
@@ -31718,7 +31796,7 @@ function resolveEntitlement(brand, account) {
31718
31796
  if (!existsSync50(entitlementPath)) {
31719
31797
  return logResolved(anonymousFallback("missing"), { reason: "missing" });
31720
31798
  }
31721
- const stat11 = statSync25(entitlementPath);
31799
+ const stat11 = statSync26(entitlementPath);
31722
31800
  const key = memoKey(stat11.mtimeMs, account);
31723
31801
  if (memo && memo.key === key) {
31724
31802
  return memo.result;
@@ -31906,7 +31984,7 @@ function clientFrom(c) {
31906
31984
  }
31907
31985
  installMediaDownloadGuard();
31908
31986
  var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT || "";
31909
- var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join54(PLATFORM_ROOT6, "config", "brand.json") : "";
31987
+ var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join55(PLATFORM_ROOT6, "config", "brand.json") : "";
31910
31988
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", marketingUrl: "https://getmaxy.com" };
31911
31989
  if (BRAND_JSON_PATH && !existsSync51(BRAND_JSON_PATH)) {
31912
31990
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
@@ -31945,7 +32023,7 @@ var brandLoginOpts = {
31945
32023
  appleTouchIconPath: brandAppIcon512Path,
31946
32024
  themeColor: BRAND.defaultColors?.primary
31947
32025
  };
31948
- var ALIAS_DOMAINS_PATH = join54(homedir4(), BRAND.configDir, "alias-domains.json");
32026
+ var ALIAS_DOMAINS_PATH = join55(homedir4(), BRAND.configDir, "alias-domains.json");
31949
32027
  function loadAliasDomains() {
31950
32028
  try {
31951
32029
  if (!existsSync51(ALIAS_DOMAINS_PATH)) return null;
@@ -32040,7 +32118,7 @@ var waGateway = new WaGateway({
32040
32118
  sendToParty: (accountId, phone, text) => sendWhatsAppTextForRelay(accountId, phone, text),
32041
32119
  resolveSessionOverride: (accountId, senderId) => resolveChannelOverride(accountId, "whatsapp", senderId),
32042
32120
  gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
32043
- serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve45(process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
32121
+ serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve45(process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
32044
32122
  // Task 751 / 1390 — file delivery on the native channel. `maxyAccountId` (the
32045
32123
  // path-validation scope) is the sender's effective SESSION account, resolved
32046
32124
  // once at the inbound gate and threaded here via the gateway's per-sender doc
@@ -32055,7 +32133,7 @@ var waGateway = new WaGateway({
32055
32133
  caption,
32056
32134
  accountId,
32057
32135
  maxyAccountId,
32058
- platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, ".."))
32136
+ platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, ".."))
32059
32137
  });
32060
32138
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
32061
32139
  },
@@ -32222,7 +32300,7 @@ function runDuplicateSenderAudit() {
32222
32300
  try {
32223
32301
  const configDir = process.env.CLAUDE_CONFIG_DIR;
32224
32302
  if (!configDir) return;
32225
- const projectsRoot = join54(configDir, "projects");
32303
+ const projectsRoot = join55(configDir, "projects");
32226
32304
  const records = [];
32227
32305
  for (const { path: jsonlPath, isSubagent, archived } of enumerateJsonls(projectsRoot)) {
32228
32306
  if (isSubagent || archived) continue;
@@ -32233,6 +32311,10 @@ function runDuplicateSenderAudit() {
32233
32311
  }
32234
32312
  logDuplicateSenderGroups(findDuplicateSenderGroups(records));
32235
32313
  logCrossAccountSenderGroups(findCrossAccountSenderGroups(records));
32314
+ const outbound = waGateway.takeOutboundAccountCensus();
32315
+ console.error(
32316
+ `[whatsapp-native] op=outbound-account-mismatch sends=${outbound.sends} mismatched=${outbound.mismatched}`
32317
+ );
32236
32318
  } catch (err) {
32237
32319
  console.error(`[whatsapp-native] op=duplicate-sender-audit-error error=${err instanceof Error ? err.message : String(err)}`);
32238
32320
  }
@@ -32435,7 +32517,26 @@ var scheduleInjectRoutes = createScheduleInjectRoutes({
32435
32517
  // than returning the house id this closure used to be handed. See
32436
32518
  // effective-account.ts for why `resolveAccount()` was the wrong side to move,
32437
32519
  // and why the real dependencies are bound there rather than assembled here.
32438
- effectiveAccountFor: resolveScheduledEffectiveAccountForInstall
32520
+ effectiveAccountFor: resolveScheduledEffectiveAccountForInstall,
32521
+ // Task 2365 — the account whose own telegram.adminUsers claims this chat id.
32522
+ // Reads the same list, through the same readChannelAdmins call, that
32523
+ // validateAgentDestination uses (agent-dispatch-rule), so the routine picker,
32524
+ // the save and this dispatch cannot disagree. Two claimants is reported to
32525
+ // the route and refused there, never picked between.
32526
+ telegramAccountForDestination: (destination) => {
32527
+ const asNumber = Number(destination);
32528
+ if (!Number.isInteger(asNumber)) return { kind: "none" };
32529
+ const claimants = listValidAccounts().filter(
32530
+ (a) => readChannelAdmins(a.accountDir, "telegram").adminUsers.includes(asNumber)
32531
+ );
32532
+ if (claimants.length === 0) return { kind: "none" };
32533
+ if (claimants.length > 1) return { kind: "ambiguous", count: claimants.length };
32534
+ return {
32535
+ kind: "one",
32536
+ accountId: claimants[0].accountId,
32537
+ botToken: claimants[0].config.telegram?.adminBotToken ?? null
32538
+ };
32539
+ }
32439
32540
  });
32440
32541
  app76.route("/api/channel/schedule-inject", scheduleInjectRoutes);
32441
32542
  app76.use("*", clientIpMiddleware);
@@ -32710,15 +32811,10 @@ app76.post("/api/remote-auth/set-password", async (c) => {
32710
32811
  });
32711
32812
  app76.route("/api/_client-error", client_error_default);
32712
32813
  console.log("[client-error-route] mounted");
32713
- var PWA_PUBLIC_PATHS = /* @__PURE__ */ new Set(["/sw.js", ...PWA_SURFACES.map((s) => s.manifestPath)]);
32714
32814
  app76.use("*", async (c, next) => {
32715
32815
  const host = (c.req.header("host") ?? "").split(":")[0];
32716
32816
  const path = c.req.path;
32717
- if (path === "/favicon.ico" || path.startsWith("/assets/") || path.startsWith("/brand/") || // Public free/busy is read by the booking page (a separate Cloudflare Pages
32718
- // origin) against the brand host with no admin cookie. It is public by
32719
- // design — open slots only, no :Meeting detail — so it bypasses the gate
32720
- // exactly like /brand/. The detail-leak guard lives in the route + its test.
32721
- path === "/api/calendar/free-busy" || PWA_PUBLIC_PATHS.has(path)) {
32817
+ if (isRemoteAuthBypassPath(path)) {
32722
32818
  await next();
32723
32819
  return;
32724
32820
  }
@@ -32771,7 +32867,7 @@ app76.route("/api/webchat", createWebchatRoutes({
32771
32867
  resolvePermissionVerdict: (key, requestId, behavior) => webchatGateway.resolvePermissionVerdict(key, requestId, behavior)
32772
32868
  }));
32773
32869
  app76.route("/api/webchat/greeting", webchat_greeting_default);
32774
- app76.route("/api/telegram", telegram_default);
32870
+ mountTelegramRoutes(app76, telegram_default);
32775
32871
  app76.route("/api/quickbooks", quickbooks_default);
32776
32872
  app76.route("/api/onboarding", onboarding_default);
32777
32873
  app76.route("/api/admin", admin_default);
@@ -32903,7 +32999,7 @@ var SW_SOURCE = (() => {
32903
32999
  function readInstalledVersion() {
32904
33000
  try {
32905
33001
  if (!PLATFORM_ROOT6) return "unknown";
32906
- const versionFile = join54(PLATFORM_ROOT6, "config", `.${BRAND.hostname}-version`);
33002
+ const versionFile = join55(PLATFORM_ROOT6, "config", `.${BRAND.hostname}-version`);
32907
33003
  if (!existsSync51(versionFile)) return "unknown";
32908
33004
  const content = readFileSync47(versionFile, "utf-8").trim();
32909
33005
  return content || "unknown";
@@ -32933,12 +33029,12 @@ ${clientErrorReporterScript}
32933
33029
  return html;
32934
33030
  }
32935
33031
  function loadBrandingCache(agentSlug) {
32936
- const configDir = join54(homedir4(), BRAND.configDir);
33032
+ const configDir = join55(homedir4(), BRAND.configDir);
32937
33033
  try {
32938
33034
  const verdict = resolvePublicAddressFromDisk(agentSlug);
32939
33035
  const accountId = verdict.kind === "served" ? verdict.owner.accountId : getDefaultAccountId();
32940
33036
  if (!accountId) return null;
32941
- const cachePath = join54(configDir, "branding-cache", accountId, `${agentSlug}.json`);
33037
+ const cachePath = join55(configDir, "branding-cache", accountId, `${agentSlug}.json`);
32942
33038
  if (!existsSync51(cachePath)) return null;
32943
33039
  return JSON.parse(readFileSync47(cachePath, "utf-8"));
32944
33040
  } catch {
@@ -33188,7 +33284,7 @@ var hostname = process.env.HOSTNAME ?? "127.0.0.1";
33188
33284
  var httpServer = serve({ fetch: app76.fetch, port, hostname });
33189
33285
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33190
33286
  {
33191
- const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33287
+ const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33192
33288
  const reconcileScript = resolve45(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
33193
33289
  const runReconcile = (ctx) => {
33194
33290
  if (!existsSync51(reconcileScript)) return;
@@ -33211,7 +33307,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33211
33307
  detached: true,
33212
33308
  run: runReconcile
33213
33309
  });
33214
- const reconcileStatePath = join54(MAXY_DIR, "booking-reconcile-state.json");
33310
+ const reconcileStatePath = join55(MAXY_DIR, "booking-reconcile-state.json");
33215
33311
  registerLoop({
33216
33312
  name: "calendar-reconcile-liveness",
33217
33313
  intervalMs: 6e5,
@@ -33229,7 +33325,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33229
33325
  }
33230
33326
  {
33231
33327
  const DREAM_CYCLE_SWEEP_INTERVAL_MS = 36e5;
33232
- const dreamPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33328
+ const dreamPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33233
33329
  const dreamSweepScript = resolve45(dreamPlatformRoot, "plugins/memory/mcp/dist/scripts/dream-cycle-sweep.js");
33234
33330
  registerLoop({
33235
33331
  name: "dream-cycle-sweep",
@@ -33253,7 +33349,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33253
33349
  });
33254
33350
  }
33255
33351
  {
33256
- const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33352
+ const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33257
33353
  const outlookScript = resolve45(outlookPlatformRoot, "plugins/outlook/mcp/dist/scripts/complete-registration.js");
33258
33354
  const OUTLOOK_COMPLETE_INTERVAL_MS = 3e4;
33259
33355
  const runOutlookComplete = (ctx) => {
@@ -33279,7 +33375,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33279
33375
  });
33280
33376
  }
33281
33377
  {
33282
- const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33378
+ const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33283
33379
  const strandedAccountsDir = resolve45(auditRoot, "..", "data/accounts");
33284
33380
  const STRANDED_AUDIT_INTERVAL_MS = 3e5;
33285
33381
  const STRANDED_AGE_MS = 16 * 6e4;
@@ -33292,7 +33388,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33292
33388
  if (!STRANDED_UUID_RE.test(name)) continue;
33293
33389
  const pendingPath2 = resolve45(strandedAccountsDir, name, "secrets/outlook/pending-devicecode.enc");
33294
33390
  if (!existsSync51(pendingPath2)) continue;
33295
- const ageMs2 = now - statSync26(pendingPath2).mtimeMs;
33391
+ const ageMs2 = now - statSync27(pendingPath2).mtimeMs;
33296
33392
  if (ageMs2 > STRANDED_AGE_MS) {
33297
33393
  console.error(`[outlook-mcp] devicecode-stranded account=${name} ageSec=${Math.floor(ageMs2 / 1e3)}`);
33298
33394
  }
@@ -33309,7 +33405,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33309
33405
  });
33310
33406
  }
33311
33407
  {
33312
- const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33408
+ const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33313
33409
  const googleAccountsDir = resolve45(googleRoot, "..", "data/accounts");
33314
33410
  const GOOGLE_PENDING_AUDIT_INTERVAL_MS = 3e5;
33315
33411
  const runGooglePendingAuditSafe = () => {
@@ -33353,7 +33449,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33353
33449
  });
33354
33450
  }
33355
33451
  {
33356
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33452
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33357
33453
  const STORAGE_AUDIT_INTERVAL_MS = 3e5;
33358
33454
  const runStorageAuditSafe = () => runStorageAudit(auditPlatformRoot).catch((err) => {
33359
33455
  console.error(`[storage-audit] error="${err.message}"`);
@@ -33385,6 +33481,25 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33385
33481
  firstRunDelayMs: 5e4,
33386
33482
  run: runDataPortalAuditSafe
33387
33483
  });
33484
+ const runTelegramAudit = async () => {
33485
+ const accounts = listValidAccounts().map((a) => ({ accountId: a.accountId, telegram: a.config.telegram }));
33486
+ const configured = accounts.some((a) => a.telegram?.adminBotToken || a.telegram?.publicBotToken);
33487
+ if (!configured) return;
33488
+ const lastByAccount = newestTelegramSessionByAccount();
33489
+ const lines = buildTelegramAuditLines({
33490
+ accounts,
33491
+ secretFileExists: (botType, accountId) => existsSync51(secretPath(botType, accountId)),
33492
+ lastInboundAt: (accountId) => lastByAccount.get(accountId) ?? null,
33493
+ now: Date.now()
33494
+ });
33495
+ for (const line of lines) console.error(line);
33496
+ };
33497
+ registerLoop({
33498
+ name: "telegram-audit",
33499
+ intervalMs: STORAGE_AUDIT_INTERVAL_MS,
33500
+ firstRunDelayMs: 65e3,
33501
+ run: runTelegramAudit
33502
+ });
33388
33503
  registerLoop({
33389
33504
  name: "portal-uploads-pull",
33390
33505
  intervalMs: PORTAL_UPLOADS_PULL_INTERVAL_MS,
@@ -33450,7 +33565,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33450
33565
  });
33451
33566
  }
33452
33567
  {
33453
- const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33568
+ const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33454
33569
  const publishScript = resolve45(publishPlatformRoot, "plugins/scheduling/mcp/dist/scripts/publish-availability.js");
33455
33570
  const PUBLISH_INTERVAL_MS = 3e5;
33456
33571
  const bookingAccounts = () => {
@@ -33527,7 +33642,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33527
33642
  startTimeEntryCensus(() => getSession());
33528
33643
  startLedgerCensus(() => getSession());
33529
33644
  {
33530
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33645
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33531
33646
  const auditScript = resolve45(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
33532
33647
  const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
33533
33648
  const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
@@ -33738,8 +33853,8 @@ var runIngestAuditTick = () => {
33738
33853
  },
33739
33854
  agentExists: (accountId, slug) => {
33740
33855
  if (!house) return false;
33741
- const dir = isHouseAccountKey(house.accountId, accountId) ? house.accountDir : join54(ACCOUNTS_DIR, accountId);
33742
- return existsSync51(join54(dir, "agents", slug, "config.json"));
33856
+ const dir = isHouseAccountKey(house.accountId, accountId) ? house.accountDir : join55(ACCOUNTS_DIR, accountId);
33857
+ return existsSync51(join55(dir, "agents", slug, "config.json"));
33743
33858
  }
33744
33859
  });
33745
33860
  } catch (err) {
@@ -33787,7 +33902,7 @@ registerLoop({
33787
33902
  runPublicAddressCollisionAudit({
33788
33903
  houseAccountId: () => resolveAccount()?.accountId ?? null,
33789
33904
  activeSlugs: () => listValidAccounts().map((a) => {
33790
- const agentsRoot = join54(a.accountDir, "agents");
33905
+ const agentsRoot = join55(a.accountDir, "agents");
33791
33906
  if (!existsSync51(agentsRoot)) return { accountId: a.accountId, slugs: [] };
33792
33907
  const slugs = readdirSync30(agentsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((slug) => isActiveAgentSlug(a.accountDir, slug));
33793
33908
  return { accountId: a.accountId, slugs };
@@ -33873,7 +33988,7 @@ try {
33873
33988
  } catch (err) {
33874
33989
  console.error(`[graph-health] account-enumeration unavailable reason=${err instanceof Error ? err.message : String(err)}`);
33875
33990
  }
33876
- var configDirForWhatsApp = basename17(MAXY_DIR) || ".maxy";
33991
+ var configDirForWhatsApp = basename18(MAXY_DIR) || ".maxy";
33877
33992
  var bootAccount = resolveAccount();
33878
33993
  if (bootAccount) {
33879
33994
  migrateRemovedConfigKeys(bootAccount.accountDir);
@@ -33939,7 +34054,7 @@ if (bootAccountConfig?.whatsapp) {
33939
34054
  }
33940
34055
  init({
33941
34056
  configDir: configDirForWhatsApp,
33942
- platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..")),
34057
+ platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..")),
33943
34058
  accountConfig: bootAccountConfig,
33944
34059
  // Task 2074 — enrich with the sub-account name and fan out to every open
33945
34060
  // admin SSE stream. Fire-and-forget: the manager's persist path must not