@rubytech/create-maxy-code 0.1.547 → 0.1.548

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 (112) hide show
  1. package/dist/index.js +14 -0
  2. package/package.json +1 -1
  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 +41 -9
  13. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  14. package/payload/platform/plugins/docs/references/admin-ui.md +40 -8
  15. package/payload/platform/plugins/scheduling/PLUGIN.md +6 -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/services/telegram-channel/dist/notification.d.ts +8 -3
  66. package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
  67. package/payload/platform/services/telegram-channel/dist/notification.js +10 -3
  68. package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
  69. package/payload/platform/services/telegram-channel/dist/server.js +1 -1
  70. package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
  71. package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts +28 -0
  72. package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts.map +1 -1
  73. package/payload/platform/services/telegram-channel/dist/turn-follow.js +33 -1
  74. package/payload/platform/services/telegram-channel/dist/turn-follow.js.map +1 -1
  75. package/payload/platform/services/webchat-channel/dist/notification.d.ts +8 -3
  76. package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
  77. package/payload/platform/services/webchat-channel/dist/notification.js +10 -3
  78. package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
  79. package/payload/platform/services/webchat-channel/dist/server.js +1 -1
  80. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  81. package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts +28 -0
  82. package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -1
  83. package/payload/platform/services/webchat-channel/dist/turn-follow.js +33 -1
  84. package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -1
  85. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +28 -4
  86. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  87. package/payload/platform/services/whatsapp-channel/dist/notification.js +30 -7
  88. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  89. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  90. package/payload/platform/services/whatsapp-channel/dist/server.js +13 -5
  91. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  92. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +2 -0
  93. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
  94. package/payload/platform/services/whatsapp-channel/dist/targets.js +14 -1
  95. package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
  96. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +28 -0
  97. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -1
  98. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +34 -1
  99. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -1
  100. package/payload/server/{chunk-MHNHG3GL.js → chunk-G6ULACHG.js} +1 -1
  101. package/payload/server/{chunk-Q33KT7YJ.js → chunk-JOVREC6E.js} +117 -1
  102. package/payload/server/{chunk-WGMIUXXG.js → chunk-SKYZG6OT.js} +11 -0
  103. package/payload/server/{manager-W32LVPCT.js → manager-2VEJKVEO.js} +2 -2
  104. package/payload/server/maxy-edge.js +2 -2
  105. package/payload/server/public/assets/routines-OGFXwTFO.js +2 -0
  106. package/payload/server/public/assets/routines-OGFXwTFO.js.br +0 -0
  107. package/payload/server/public/assets/routines-OGFXwTFO.js.gz +0 -0
  108. package/payload/server/public/routines.html +1 -1
  109. package/payload/server/server.js +278 -169
  110. package/payload/server/public/assets/routines-Ds0yz7_C.js +0 -2
  111. package/payload/server/public/assets/routines-Ds0yz7_C.js.br +0 -0
  112. 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}`);
@@ -24070,7 +24004,7 @@ var ROUTINE_COLS = `e.eventId AS eventId, e.name AS name, e.eventStatus AS event
24070
24004
  e.timezone AS timezone, e.startDate AS startDate,
24071
24005
  e.agentChannel AS agentChannel, e.agentDestination AS agentDestination, e.agentPrompt AS agentPrompt,
24072
24006
  e.actionPlugin AS actionPlugin, e.actionTool AS actionTool, e.actionArgs AS actionArgs,
24073
- e.notes AS notes, e.routineDescription AS routineDescription,
24007
+ e.notes AS notes, e.description AS description,
24074
24008
  e.lastDispatchResult AS lastDispatchResult, e.lastDispatchAt AS lastDispatchAt,
24075
24009
  e.suspendedInBulk AS suspendedInBulk`;
24076
24010
  function mapRoutine(r) {
@@ -24091,11 +24025,11 @@ function mapRoutine(r) {
24091
24025
  actionTool: g("actionTool"),
24092
24026
  actionArgs: g("actionArgs"),
24093
24027
  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"),
24028
+ // Task 2361operator-writable, and the only caption any routines surface
24029
+ // shows. Never dispatched: agentPrompt is the text a fire sends. PATCH
24030
+ // accepts it below and stamps operatorEdited, which is what stops the next
24031
+ // roster version bump overwriting the operator's wording.
24032
+ description: g("description"),
24099
24033
  lastDispatchResult: g("lastDispatchResult"),
24100
24034
  lastDispatchAt: g("lastDispatchAt"),
24101
24035
  // Task 2106 — on the wire so the page can gate Resume all on the set the
@@ -24391,23 +24325,29 @@ app51.patch("/:eventId", requireAdminSession, async (c) => {
24391
24325
  console.error(`[admin:routines] op=patch id=${id82} fields=agentChannel+agentDestination outcome=rejected reason=invalid-channel channel=${body.agentChannel}`);
24392
24326
  return c.json({ error: `Unknown dispatch channel "${body.agentChannel}".` }, 400);
24393
24327
  }
24328
+ const describing = "description" in body;
24394
24329
  let timezone = null;
24395
24330
  let storedPrompt = null;
24396
24331
  let storedRecurrence = null;
24397
24332
  let storedActionPlugin = null;
24398
- if (typeof body.recurrence === "string" || binding || clearing) {
24333
+ let storedName = null;
24334
+ let storedDescription = null;
24335
+ if (typeof body.recurrence === "string" || binding || clearing || describing) {
24399
24336
  const preSession = getSession();
24400
24337
  try {
24401
24338
  const pre = await preSession.run(
24402
24339
  `MATCH (e:Event {eventId: $eventId, accountId: $accountId})
24403
24340
  RETURN e.timezone AS timezone, e.agentPrompt AS agentPrompt,
24404
- e.recurrence AS recurrence, e.actionPlugin AS actionPlugin`,
24341
+ e.recurrence AS recurrence, e.actionPlugin AS actionPlugin,
24342
+ e.name AS name, e.description AS description`,
24405
24343
  { eventId, accountId }
24406
24344
  );
24407
24345
  timezone = pre.records[0]?.get("timezone") ?? null;
24408
24346
  storedPrompt = pre.records[0]?.get("agentPrompt") ?? null;
24409
24347
  storedRecurrence = pre.records[0]?.get("recurrence") ?? null;
24410
24348
  storedActionPlugin = pre.records[0]?.get("actionPlugin") ?? null;
24349
+ storedName = pre.records[0]?.get("name") ?? null;
24350
+ storedDescription = pre.records[0]?.get("description") ?? null;
24411
24351
  } catch (err) {
24412
24352
  const message = err instanceof Error ? err.message : String(err);
24413
24353
  console.error(`[admin:routines] op=patch id=${id82} outcome=rejected reason=tz-read-fail err="${message}"`);
@@ -24475,6 +24415,27 @@ app51.patch("/:eventId", requireAdminSession, async (c) => {
24475
24415
  setClauses.push("e.operatorEdited = $operatorEdited");
24476
24416
  params.operatorEdited = (/* @__PURE__ */ new Date()).toISOString();
24477
24417
  }
24418
+ if (describing) {
24419
+ const raw = body.description;
24420
+ if (typeof raw !== "string" && raw !== null) {
24421
+ console.error(`[admin:routines] op=patch id=${id82} fields=description outcome=rejected reason=not-a-string`);
24422
+ return c.json({ error: "A description must be text, or null to clear it." }, 400);
24423
+ }
24424
+ const next = typeof raw === "string" && raw.trim().length > 0 ? raw : null;
24425
+ setClauses.push("e.description = $description");
24426
+ params.description = next;
24427
+ fields.push("description");
24428
+ setClauses.push("e.operatorEdited = $operatorEdited");
24429
+ params.operatorEdited = (/* @__PURE__ */ new Date()).toISOString();
24430
+ if (next !== storedDescription) {
24431
+ const parts = [`Event: ${storedName ?? ""}`, next ?? ""].filter(Boolean);
24432
+ const embedding = await embedForWrite(parts.join(" \u2014 "));
24433
+ if (embedding.length > 0) {
24434
+ setClauses.push("e.embedding = $embedding");
24435
+ params.embedding = embedding;
24436
+ }
24437
+ }
24438
+ }
24478
24439
  if (typeof body.recurrence === "string") {
24479
24440
  try {
24480
24441
  const interval = parseExpression(body.recurrence, { currentDate: /* @__PURE__ */ new Date(), tz: timezone ?? void 0 });
@@ -28790,18 +28751,23 @@ function createWaChannelRoutes(deps) {
28790
28751
  if (typeof senderId !== "string" || typeof text !== "string") {
28791
28752
  return c.json({ error: "senderId and text required" }, 400);
28792
28753
  }
28754
+ const accountId = body?.accountId;
28755
+ if (typeof accountId !== "string" || accountId.length === 0) {
28756
+ console.error(`[whatsapp-native] op=reply-rejected senderId=${senderId} reason=no-account`);
28757
+ return c.json({ error: "accountId required" }, 400);
28758
+ }
28793
28759
  const bytes = Buffer.byteLength(text, "utf8");
28794
28760
  try {
28795
- await deps.sendOutbound(senderId, text);
28761
+ await deps.sendOutbound(accountId, senderId, text);
28796
28762
  } catch (err) {
28797
28763
  const message = err instanceof Error ? err.message : String(err);
28798
28764
  const terminal = err instanceof WaReplyError ? err.terminal : false;
28799
28765
  console.error(
28800
- `[whatsapp-native] op=reply-failed senderId=${senderId} bytes=${bytes} terminal=${terminal} error=${message}`
28766
+ `[whatsapp-native] op=reply-failed senderId=${senderId} account=${accountId} bytes=${bytes} terminal=${terminal} error=${message}`
28801
28767
  );
28802
28768
  return c.json({ error: message, terminal }, 500);
28803
28769
  }
28804
- console.error(`[whatsapp-native] op=reply-dispatch senderId=${senderId} bytes=${bytes}`);
28770
+ console.error(`[whatsapp-native] op=reply-dispatch senderId=${senderId} account=${accountId} bytes=${bytes}`);
28805
28771
  deps.onReplyDispatch?.(senderId, Date.now());
28806
28772
  return c.json({ ok: true });
28807
28773
  });
@@ -28813,13 +28779,18 @@ function createWaChannelRoutes(deps) {
28813
28779
  if (typeof senderId !== "string" || !Array.isArray(files) || files.length === 0 || !files.every((f) => typeof f === "string")) {
28814
28780
  return c.json({ error: "senderId and a non-empty files string array required" }, 400);
28815
28781
  }
28782
+ const accountId = body?.accountId;
28783
+ if (typeof accountId !== "string" || accountId.length === 0) {
28784
+ console.error(`[whatsapp-native] op=reply-document-rejected senderId=${senderId} reason=no-account`);
28785
+ return c.json({ error: "accountId required" }, 400);
28786
+ }
28816
28787
  const results = [];
28817
28788
  for (let i = 0; i < files.length; i++) {
28818
28789
  const file = files[i];
28819
28790
  console.error(`[whatsapp-native] op=reply-document-request senderId=${senderId} file=${file}`);
28820
28791
  let outcome;
28821
28792
  try {
28822
- outcome = await deps.sendDocument(senderId, file, i === 0 ? caption : void 0);
28793
+ outcome = await deps.sendDocument(accountId, senderId, file, i === 0 ? caption : void 0);
28823
28794
  } catch (err) {
28824
28795
  const message = err instanceof Error ? err.message : String(err);
28825
28796
  const terminal = err instanceof WaReplyError ? err.terminal : false;
@@ -28899,6 +28870,11 @@ function createWaChannelRoutes(deps) {
28899
28870
  if (typeof senderId !== "string" || typeof sessionId !== "string" || typeof replied !== "boolean") {
28900
28871
  return c.json({ error: "senderId, sessionId and replied required" }, 400);
28901
28872
  }
28873
+ const accountId = body?.accountId;
28874
+ if (typeof accountId !== "string" || accountId.length === 0) {
28875
+ console.error(`[whatsapp-native] op=turn-end-rejected senderId=${senderId} reason=no-account`);
28876
+ return c.json({ error: "accountId required" }, 400);
28877
+ }
28902
28878
  const interactive = body?.interactive === true;
28903
28879
  const finalText = typeof body?.finalText === "string" ? body.finalText : "";
28904
28880
  const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
@@ -28908,6 +28884,9 @@ function createWaChannelRoutes(deps) {
28908
28884
  const consumed2 = num(body?.consumed);
28909
28885
  const source = typeof body?.source === "string" ? body.source : null;
28910
28886
  const sid = sessionId.slice(0, 8);
28887
+ console.error(
28888
+ `[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)}`
28889
+ );
28911
28890
  const deliver = interactive && !replied && finalText.trim().length > 0;
28912
28891
  const decision = `sessionId=${sid} replied=${replied ? "yes" : "no"} interactive=${interactive ? "yes" : "no"} source=${source ?? "unset"} finalBytes=${finalBytes} queueDepth=${queueDepth} pendingTurns=${pendingTurns} consumed=${consumed2}`;
28913
28892
  if (!replied && finalBytes > 0 && !deliver) {
@@ -28919,7 +28898,7 @@ function createWaChannelRoutes(deps) {
28919
28898
  if (deliver) {
28920
28899
  const bytes = Buffer.byteLength(finalText, "utf8");
28921
28900
  try {
28922
- await deps.sendOutbound(senderId, finalText);
28901
+ await deps.sendOutbound(accountId, senderId, finalText);
28923
28902
  } catch (err) {
28924
28903
  const message = err instanceof Error ? err.message : String(err);
28925
28904
  console.error(
@@ -29003,6 +28982,13 @@ var WaGateway = class {
29003
28982
  this.deps = deps;
29004
28983
  }
29005
28984
  hub = new InboundHub();
28985
+ // Task 2363 — keyed by (effective account, sender) through the same `hubKey`
28986
+ // the hub uses, never by the number alone. A phone bound to two accounts holds
28987
+ // one reply closure per account, and the last inbound used to overwrite the
28988
+ // other account's route, so an answer composed for one account went out on the
28989
+ // other's socket. The account is the EFFECTIVE session account, which is what
28990
+ // `hub.deliver` below keys on and what the channel server sends back: the
28991
+ // socket owner is not addressable by the requester.
29006
28992
  replies = /* @__PURE__ */ new Map();
29007
28993
  // Per-sender document-delivery context, set on every inbound. Its presence is
29008
28994
  // the reply-only guard: a sender with no inbound has no context, so a file
@@ -29034,6 +29020,16 @@ var WaGateway = class {
29034
29020
  mediaRaces = 0;
29035
29021
  mediaRaceSenders = /* @__PURE__ */ new Set();
29036
29022
  mediaStanzaExpired = 0;
29023
+ // Task 2363 — the outbound-account census, reset on every print. `sends` is
29024
+ // every sendOutbound call; `mismatched` is the subset that missed for
29025
+ // (account, sender) while ANOTHER account held a live route for that sender.
29026
+ // Before this task those sends succeeded on the other account's socket and
29027
+ // emitted no error, so nothing but a reconciliation count could see them; they
29028
+ // now throw, and this counts them at the point they are refused. A cold miss
29029
+ // (no route under any account) is deliberately excluded — it is an ordinary
29030
+ // reply to a sender with no conversation, not a crossed one.
29031
+ outboundSends = 0;
29032
+ outboundMismatched = 0;
29037
29033
  /** Task 2110 — the WhatsApp manager reports that a non-audio media stanza for
29038
29034
  * this sender has started downloading. The gateway cannot derive this itself:
29039
29035
  * it first learns of the media when the completed inbound arrives, which is
@@ -29090,8 +29086,8 @@ var WaGateway = class {
29090
29086
  routes() {
29091
29087
  return createWaChannelRoutes({
29092
29088
  hub: this.hub,
29093
- sendOutbound: (senderId, text) => this.sendOutbound(senderId, text),
29094
- sendDocument: (senderId, filePath, caption) => this.sendDocument(senderId, filePath, caption),
29089
+ sendOutbound: (accountId, senderId, text) => this.sendOutbound(accountId, senderId, text),
29090
+ sendDocument: (accountId, senderId, filePath, caption) => this.sendDocument(accountId, senderId, filePath, caption),
29095
29091
  onReady: (accountId, senderId) => {
29096
29092
  const count = this.hub.markReady(accountId, senderId, Date.now());
29097
29093
  if (count > 0) {
@@ -29225,7 +29221,7 @@ var WaGateway = class {
29225
29221
  const openable = input.media.filter((m) => m.type !== "audio");
29226
29222
  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
29223
  const source = input.source ?? "user";
29228
- this.replies.set(senderId, input.reply);
29224
+ this.replies.set(hubKey(input.effectiveAccountId, senderId), input.reply);
29229
29225
  this.docContexts.set(senderId, { accountId: input.accountId, maxyAccountId: input.effectiveAccountId });
29230
29226
  console.error(
29231
29227
  `[whatsapp-native] op=doc-context senderId=${senderId} accountId=${input.accountId} maxyAccountId=${input.effectiveAccountId}`
@@ -29336,17 +29332,41 @@ var WaGateway = class {
29336
29332
  input.role === "public" && input.personId ? input.personId : void 0
29337
29333
  );
29338
29334
  }
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}`);
29335
+ async sendOutbound(accountId, senderId, text) {
29336
+ this.outboundSends++;
29337
+ const reply = this.replies.get(hubKey(accountId, senderId));
29338
+ if (!reply) {
29339
+ const suffix = hubKey("", senderId);
29340
+ if ([...this.replies.keys()].some((k) => k.endsWith(suffix))) {
29341
+ this.outboundMismatched++;
29342
+ console.error(
29343
+ `[whatsapp-native] op=outbound-account-miss senderId=${senderId} requested=${accountId} held=other-account`
29344
+ );
29345
+ }
29346
+ throw new Error(`wa-gateway: no reply route for account ${accountId} sender ${senderId}`);
29347
+ }
29342
29348
  await reply(text);
29343
29349
  }
29350
+ /** Task 2363 — read and reset the outbound-account census. Printed on the
29351
+ * duplicate-sender audit tick; see server/index.ts. */
29352
+ takeOutboundAccountCensus() {
29353
+ const census = { sends: this.outboundSends, mismatched: this.outboundMismatched };
29354
+ this.outboundSends = 0;
29355
+ this.outboundMismatched = 0;
29356
+ return census;
29357
+ }
29344
29358
  /** Deliver a file to a sender as a WhatsApp document. Throws `WaReplyError`
29345
29359
  * (reply-only guard) when no inbound has registered a document context for
29346
29360
  * the sender — file delivery is only ever a reply on a live conversation. */
29347
- async sendDocument(senderId, filePath, caption) {
29361
+ async sendDocument(accountId, senderId, filePath, caption) {
29348
29362
  const ctx = this.docContexts.get(senderId);
29349
29363
  if (!ctx) throw new WaReplyError(`no conversation for sender ${senderId}`, false);
29364
+ if (ctx.maxyAccountId !== accountId) {
29365
+ console.error(
29366
+ `[whatsapp-native] op=document-account-mismatch senderId=${senderId} requested=${accountId} context=${ctx.maxyAccountId}`
29367
+ );
29368
+ throw new WaReplyError(`no conversation for sender ${senderId} on account ${accountId}`, false);
29369
+ }
29350
29370
  return this.deps.sendDocument({ senderId, accountId: ctx.accountId, maxyAccountId: ctx.maxyAccountId, filePath, caption });
29351
29371
  }
29352
29372
  };
@@ -29782,6 +29802,10 @@ function createWebchatChannelRoutes(deps) {
29782
29802
  const finalBytes = typeof body?.finalBytes === "number" && Number.isFinite(body.finalBytes) ? body.finalBytes : 0;
29783
29803
  const k = key.slice(0, 8);
29784
29804
  const sid = sessionId.slice(0, 8);
29805
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
29806
+ console.error(
29807
+ `[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)}`
29808
+ );
29785
29809
  const repliedField = role === "public" ? ` replied=${replied ? "yes" : "no"}` : "";
29786
29810
  console.error(`[webchat-native] op=turn-end channel=webchat key=${k} role=${role} sessionId=${sid}${repliedField} trailing=${finalBytes}`);
29787
29811
  if (role === "admin" && finalBytes === 0) {
@@ -30964,6 +30988,10 @@ function createTelegramChannelRoutes(deps) {
30964
30988
  const interactive = body?.interactive === true;
30965
30989
  const finalText = typeof body?.finalText === "string" ? body.finalText : "";
30966
30990
  const sid = sessionId.slice(0, 8);
30991
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
30992
+ console.error(
30993
+ `[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)}`
30994
+ );
30967
30995
  const deliver = interactive && !replied && finalText.trim().length > 0;
30968
30996
  if (deliver) {
30969
30997
  const bytes = Buffer.byteLength(finalText, "utf8");
@@ -31585,6 +31613,50 @@ function runPublicAddressCollisionAudit(deps) {
31585
31613
  );
31586
31614
  }
31587
31615
 
31616
+ // server/telegram-audit.ts
31617
+ import { statSync as statSync25 } from "fs";
31618
+ import { basename as basename17, dirname as dirname17, join as join54 } from "path";
31619
+ function buildTelegramAuditLines(input) {
31620
+ const lines = [];
31621
+ for (const account of input.accounts) {
31622
+ const tg = account.telegram;
31623
+ if (!tg) continue;
31624
+ const botTypes = [];
31625
+ if (tg.adminBotToken) botTypes.push("admin");
31626
+ if (tg.publicBotToken) botTypes.push("public");
31627
+ if (botTypes.length === 0) continue;
31628
+ const last = input.lastInboundAt(account.accountId);
31629
+ const age = last === null ? "never" : String(input.now - last);
31630
+ for (const botType of botTypes) {
31631
+ const secretFile = input.secretFileExists(botType, account.accountId) ? "present" : "absent";
31632
+ lines.push(
31633
+ `[telegram-audit] op=configured accountId=${account.accountId} botType=${botType} secretFile=${secretFile} lastInboundAgeMs=${age}`
31634
+ );
31635
+ }
31636
+ }
31637
+ return lines;
31638
+ }
31639
+ function newestTelegramSessionByAccount() {
31640
+ const out = /* @__PURE__ */ new Map();
31641
+ const cfg = claudeConfigDir();
31642
+ if (!cfg) return out;
31643
+ const projectsRoot = join54(cfg, "projects");
31644
+ for (const { path } of enumerateJsonls(projectsRoot)) {
31645
+ const sessionId = basename17(path).replace(/\.jsonl$/, "");
31646
+ const meta = readSidecarMeta(join54(dirname17(path), `${sessionId}.meta.json`));
31647
+ if (meta.channel !== "telegram" || !meta.accountId) continue;
31648
+ let mtime;
31649
+ try {
31650
+ mtime = statSync25(path).mtimeMs;
31651
+ } catch {
31652
+ continue;
31653
+ }
31654
+ const prev = out.get(meta.accountId);
31655
+ if (prev === void 0 || mtime > prev) out.set(meta.accountId, mtime);
31656
+ }
31657
+ return out;
31658
+ }
31659
+
31588
31660
  // app/lib/whatsapp/recall-store-backfill.ts
31589
31661
  function backfillMisfiledRecords(params) {
31590
31662
  const { houseAccountId, isTargetAccount } = params;
@@ -31666,7 +31738,7 @@ function broadcastAdminShutdown(reason) {
31666
31738
 
31667
31739
  // ../lib/entitlement/src/index.ts
31668
31740
  import { createPublicKey, createHash as createHash6, verify as cryptoVerify } from "crypto";
31669
- import { existsSync as existsSync50, readFileSync as readFileSync46, statSync as statSync25 } from "fs";
31741
+ import { existsSync as existsSync50, readFileSync as readFileSync46, statSync as statSync26 } from "fs";
31670
31742
  import { resolve as resolve44 } from "path";
31671
31743
 
31672
31744
  // ../lib/entitlement/src/canonicalize.ts
@@ -31718,7 +31790,7 @@ function resolveEntitlement(brand, account) {
31718
31790
  if (!existsSync50(entitlementPath)) {
31719
31791
  return logResolved(anonymousFallback("missing"), { reason: "missing" });
31720
31792
  }
31721
- const stat11 = statSync25(entitlementPath);
31793
+ const stat11 = statSync26(entitlementPath);
31722
31794
  const key = memoKey(stat11.mtimeMs, account);
31723
31795
  if (memo && memo.key === key) {
31724
31796
  return memo.result;
@@ -31906,7 +31978,7 @@ function clientFrom(c) {
31906
31978
  }
31907
31979
  installMediaDownloadGuard();
31908
31980
  var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT || "";
31909
- var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join54(PLATFORM_ROOT6, "config", "brand.json") : "";
31981
+ var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join55(PLATFORM_ROOT6, "config", "brand.json") : "";
31910
31982
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", marketingUrl: "https://getmaxy.com" };
31911
31983
  if (BRAND_JSON_PATH && !existsSync51(BRAND_JSON_PATH)) {
31912
31984
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
@@ -31945,7 +32017,7 @@ var brandLoginOpts = {
31945
32017
  appleTouchIconPath: brandAppIcon512Path,
31946
32018
  themeColor: BRAND.defaultColors?.primary
31947
32019
  };
31948
- var ALIAS_DOMAINS_PATH = join54(homedir4(), BRAND.configDir, "alias-domains.json");
32020
+ var ALIAS_DOMAINS_PATH = join55(homedir4(), BRAND.configDir, "alias-domains.json");
31949
32021
  function loadAliasDomains() {
31950
32022
  try {
31951
32023
  if (!existsSync51(ALIAS_DOMAINS_PATH)) return null;
@@ -32040,7 +32112,7 @@ var waGateway = new WaGateway({
32040
32112
  sendToParty: (accountId, phone, text) => sendWhatsAppTextForRelay(accountId, phone, text),
32041
32113
  resolveSessionOverride: (accountId, senderId) => resolveChannelOverride(accountId, "whatsapp", senderId),
32042
32114
  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"),
32115
+ serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve45(process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
32044
32116
  // Task 751 / 1390 — file delivery on the native channel. `maxyAccountId` (the
32045
32117
  // path-validation scope) is the sender's effective SESSION account, resolved
32046
32118
  // once at the inbound gate and threaded here via the gateway's per-sender doc
@@ -32055,7 +32127,7 @@ var waGateway = new WaGateway({
32055
32127
  caption,
32056
32128
  accountId,
32057
32129
  maxyAccountId,
32058
- platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, ".."))
32130
+ platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, ".."))
32059
32131
  });
32060
32132
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
32061
32133
  },
@@ -32222,7 +32294,7 @@ function runDuplicateSenderAudit() {
32222
32294
  try {
32223
32295
  const configDir = process.env.CLAUDE_CONFIG_DIR;
32224
32296
  if (!configDir) return;
32225
- const projectsRoot = join54(configDir, "projects");
32297
+ const projectsRoot = join55(configDir, "projects");
32226
32298
  const records = [];
32227
32299
  for (const { path: jsonlPath, isSubagent, archived } of enumerateJsonls(projectsRoot)) {
32228
32300
  if (isSubagent || archived) continue;
@@ -32233,6 +32305,10 @@ function runDuplicateSenderAudit() {
32233
32305
  }
32234
32306
  logDuplicateSenderGroups(findDuplicateSenderGroups(records));
32235
32307
  logCrossAccountSenderGroups(findCrossAccountSenderGroups(records));
32308
+ const outbound = waGateway.takeOutboundAccountCensus();
32309
+ console.error(
32310
+ `[whatsapp-native] op=outbound-account-mismatch sends=${outbound.sends} mismatched=${outbound.mismatched}`
32311
+ );
32236
32312
  } catch (err) {
32237
32313
  console.error(`[whatsapp-native] op=duplicate-sender-audit-error error=${err instanceof Error ? err.message : String(err)}`);
32238
32314
  }
@@ -32435,7 +32511,26 @@ var scheduleInjectRoutes = createScheduleInjectRoutes({
32435
32511
  // than returning the house id this closure used to be handed. See
32436
32512
  // effective-account.ts for why `resolveAccount()` was the wrong side to move,
32437
32513
  // and why the real dependencies are bound there rather than assembled here.
32438
- effectiveAccountFor: resolveScheduledEffectiveAccountForInstall
32514
+ effectiveAccountFor: resolveScheduledEffectiveAccountForInstall,
32515
+ // Task 2365 — the account whose own telegram.adminUsers claims this chat id.
32516
+ // Reads the same list, through the same readChannelAdmins call, that
32517
+ // validateAgentDestination uses (agent-dispatch-rule), so the routine picker,
32518
+ // the save and this dispatch cannot disagree. Two claimants is reported to
32519
+ // the route and refused there, never picked between.
32520
+ telegramAccountForDestination: (destination) => {
32521
+ const asNumber = Number(destination);
32522
+ if (!Number.isInteger(asNumber)) return { kind: "none" };
32523
+ const claimants = listValidAccounts().filter(
32524
+ (a) => readChannelAdmins(a.accountDir, "telegram").adminUsers.includes(asNumber)
32525
+ );
32526
+ if (claimants.length === 0) return { kind: "none" };
32527
+ if (claimants.length > 1) return { kind: "ambiguous", count: claimants.length };
32528
+ return {
32529
+ kind: "one",
32530
+ accountId: claimants[0].accountId,
32531
+ botToken: claimants[0].config.telegram?.adminBotToken ?? null
32532
+ };
32533
+ }
32439
32534
  });
32440
32535
  app76.route("/api/channel/schedule-inject", scheduleInjectRoutes);
32441
32536
  app76.use("*", clientIpMiddleware);
@@ -32710,15 +32805,10 @@ app76.post("/api/remote-auth/set-password", async (c) => {
32710
32805
  });
32711
32806
  app76.route("/api/_client-error", client_error_default);
32712
32807
  console.log("[client-error-route] mounted");
32713
- var PWA_PUBLIC_PATHS = /* @__PURE__ */ new Set(["/sw.js", ...PWA_SURFACES.map((s) => s.manifestPath)]);
32714
32808
  app76.use("*", async (c, next) => {
32715
32809
  const host = (c.req.header("host") ?? "").split(":")[0];
32716
32810
  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)) {
32811
+ if (isRemoteAuthBypassPath(path)) {
32722
32812
  await next();
32723
32813
  return;
32724
32814
  }
@@ -32903,7 +32993,7 @@ var SW_SOURCE = (() => {
32903
32993
  function readInstalledVersion() {
32904
32994
  try {
32905
32995
  if (!PLATFORM_ROOT6) return "unknown";
32906
- const versionFile = join54(PLATFORM_ROOT6, "config", `.${BRAND.hostname}-version`);
32996
+ const versionFile = join55(PLATFORM_ROOT6, "config", `.${BRAND.hostname}-version`);
32907
32997
  if (!existsSync51(versionFile)) return "unknown";
32908
32998
  const content = readFileSync47(versionFile, "utf-8").trim();
32909
32999
  return content || "unknown";
@@ -32933,12 +33023,12 @@ ${clientErrorReporterScript}
32933
33023
  return html;
32934
33024
  }
32935
33025
  function loadBrandingCache(agentSlug) {
32936
- const configDir = join54(homedir4(), BRAND.configDir);
33026
+ const configDir = join55(homedir4(), BRAND.configDir);
32937
33027
  try {
32938
33028
  const verdict = resolvePublicAddressFromDisk(agentSlug);
32939
33029
  const accountId = verdict.kind === "served" ? verdict.owner.accountId : getDefaultAccountId();
32940
33030
  if (!accountId) return null;
32941
- const cachePath = join54(configDir, "branding-cache", accountId, `${agentSlug}.json`);
33031
+ const cachePath = join55(configDir, "branding-cache", accountId, `${agentSlug}.json`);
32942
33032
  if (!existsSync51(cachePath)) return null;
32943
33033
  return JSON.parse(readFileSync47(cachePath, "utf-8"));
32944
33034
  } catch {
@@ -33188,7 +33278,7 @@ var hostname = process.env.HOSTNAME ?? "127.0.0.1";
33188
33278
  var httpServer = serve({ fetch: app76.fetch, port, hostname });
33189
33279
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33190
33280
  {
33191
- const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33281
+ const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33192
33282
  const reconcileScript = resolve45(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
33193
33283
  const runReconcile = (ctx) => {
33194
33284
  if (!existsSync51(reconcileScript)) return;
@@ -33211,7 +33301,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33211
33301
  detached: true,
33212
33302
  run: runReconcile
33213
33303
  });
33214
- const reconcileStatePath = join54(MAXY_DIR, "booking-reconcile-state.json");
33304
+ const reconcileStatePath = join55(MAXY_DIR, "booking-reconcile-state.json");
33215
33305
  registerLoop({
33216
33306
  name: "calendar-reconcile-liveness",
33217
33307
  intervalMs: 6e5,
@@ -33229,7 +33319,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33229
33319
  }
33230
33320
  {
33231
33321
  const DREAM_CYCLE_SWEEP_INTERVAL_MS = 36e5;
33232
- const dreamPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33322
+ const dreamPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33233
33323
  const dreamSweepScript = resolve45(dreamPlatformRoot, "plugins/memory/mcp/dist/scripts/dream-cycle-sweep.js");
33234
33324
  registerLoop({
33235
33325
  name: "dream-cycle-sweep",
@@ -33253,7 +33343,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33253
33343
  });
33254
33344
  }
33255
33345
  {
33256
- const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33346
+ const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33257
33347
  const outlookScript = resolve45(outlookPlatformRoot, "plugins/outlook/mcp/dist/scripts/complete-registration.js");
33258
33348
  const OUTLOOK_COMPLETE_INTERVAL_MS = 3e4;
33259
33349
  const runOutlookComplete = (ctx) => {
@@ -33279,7 +33369,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33279
33369
  });
33280
33370
  }
33281
33371
  {
33282
- const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33372
+ const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33283
33373
  const strandedAccountsDir = resolve45(auditRoot, "..", "data/accounts");
33284
33374
  const STRANDED_AUDIT_INTERVAL_MS = 3e5;
33285
33375
  const STRANDED_AGE_MS = 16 * 6e4;
@@ -33292,7 +33382,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33292
33382
  if (!STRANDED_UUID_RE.test(name)) continue;
33293
33383
  const pendingPath2 = resolve45(strandedAccountsDir, name, "secrets/outlook/pending-devicecode.enc");
33294
33384
  if (!existsSync51(pendingPath2)) continue;
33295
- const ageMs2 = now - statSync26(pendingPath2).mtimeMs;
33385
+ const ageMs2 = now - statSync27(pendingPath2).mtimeMs;
33296
33386
  if (ageMs2 > STRANDED_AGE_MS) {
33297
33387
  console.error(`[outlook-mcp] devicecode-stranded account=${name} ageSec=${Math.floor(ageMs2 / 1e3)}`);
33298
33388
  }
@@ -33309,7 +33399,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33309
33399
  });
33310
33400
  }
33311
33401
  {
33312
- const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33402
+ const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33313
33403
  const googleAccountsDir = resolve45(googleRoot, "..", "data/accounts");
33314
33404
  const GOOGLE_PENDING_AUDIT_INTERVAL_MS = 3e5;
33315
33405
  const runGooglePendingAuditSafe = () => {
@@ -33353,7 +33443,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33353
33443
  });
33354
33444
  }
33355
33445
  {
33356
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33446
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33357
33447
  const STORAGE_AUDIT_INTERVAL_MS = 3e5;
33358
33448
  const runStorageAuditSafe = () => runStorageAudit(auditPlatformRoot).catch((err) => {
33359
33449
  console.error(`[storage-audit] error="${err.message}"`);
@@ -33385,6 +33475,25 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33385
33475
  firstRunDelayMs: 5e4,
33386
33476
  run: runDataPortalAuditSafe
33387
33477
  });
33478
+ const runTelegramAudit = async () => {
33479
+ const accounts = listValidAccounts().map((a) => ({ accountId: a.accountId, telegram: a.config.telegram }));
33480
+ const configured = accounts.some((a) => a.telegram?.adminBotToken || a.telegram?.publicBotToken);
33481
+ if (!configured) return;
33482
+ const lastByAccount = newestTelegramSessionByAccount();
33483
+ const lines = buildTelegramAuditLines({
33484
+ accounts,
33485
+ secretFileExists: (botType, accountId) => existsSync51(secretPath(botType, accountId)),
33486
+ lastInboundAt: (accountId) => lastByAccount.get(accountId) ?? null,
33487
+ now: Date.now()
33488
+ });
33489
+ for (const line of lines) console.error(line);
33490
+ };
33491
+ registerLoop({
33492
+ name: "telegram-audit",
33493
+ intervalMs: STORAGE_AUDIT_INTERVAL_MS,
33494
+ firstRunDelayMs: 65e3,
33495
+ run: runTelegramAudit
33496
+ });
33388
33497
  registerLoop({
33389
33498
  name: "portal-uploads-pull",
33390
33499
  intervalMs: PORTAL_UPLOADS_PULL_INTERVAL_MS,
@@ -33450,7 +33559,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33450
33559
  });
33451
33560
  }
33452
33561
  {
33453
- const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33562
+ const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33454
33563
  const publishScript = resolve45(publishPlatformRoot, "plugins/scheduling/mcp/dist/scripts/publish-availability.js");
33455
33564
  const PUBLISH_INTERVAL_MS = 3e5;
33456
33565
  const bookingAccounts = () => {
@@ -33527,7 +33636,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33527
33636
  startTimeEntryCensus(() => getSession());
33528
33637
  startLedgerCensus(() => getSession());
33529
33638
  {
33530
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..");
33639
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33531
33640
  const auditScript = resolve45(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
33532
33641
  const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
33533
33642
  const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
@@ -33738,8 +33847,8 @@ var runIngestAuditTick = () => {
33738
33847
  },
33739
33848
  agentExists: (accountId, slug) => {
33740
33849
  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"));
33850
+ const dir = isHouseAccountKey(house.accountId, accountId) ? house.accountDir : join55(ACCOUNTS_DIR, accountId);
33851
+ return existsSync51(join55(dir, "agents", slug, "config.json"));
33743
33852
  }
33744
33853
  });
33745
33854
  } catch (err) {
@@ -33787,7 +33896,7 @@ registerLoop({
33787
33896
  runPublicAddressCollisionAudit({
33788
33897
  houseAccountId: () => resolveAccount()?.accountId ?? null,
33789
33898
  activeSlugs: () => listValidAccounts().map((a) => {
33790
- const agentsRoot = join54(a.accountDir, "agents");
33899
+ const agentsRoot = join55(a.accountDir, "agents");
33791
33900
  if (!existsSync51(agentsRoot)) return { accountId: a.accountId, slugs: [] };
33792
33901
  const slugs = readdirSync30(agentsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((slug) => isActiveAgentSlug(a.accountDir, slug));
33793
33902
  return { accountId: a.accountId, slugs };
@@ -33873,7 +33982,7 @@ try {
33873
33982
  } catch (err) {
33874
33983
  console.error(`[graph-health] account-enumeration unavailable reason=${err instanceof Error ? err.message : String(err)}`);
33875
33984
  }
33876
- var configDirForWhatsApp = basename17(MAXY_DIR) || ".maxy";
33985
+ var configDirForWhatsApp = basename18(MAXY_DIR) || ".maxy";
33877
33986
  var bootAccount = resolveAccount();
33878
33987
  if (bootAccount) {
33879
33988
  migrateRemovedConfigKeys(bootAccount.accountDir);
@@ -33939,7 +34048,7 @@ if (bootAccountConfig?.whatsapp) {
33939
34048
  }
33940
34049
  init({
33941
34050
  configDir: configDirForWhatsApp,
33942
- platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join54(__dirname, "..")),
34051
+ platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..")),
33943
34052
  accountConfig: bootAccountConfig,
33944
34053
  // Task 2074 — enrich with the sub-account name and fan out to every open
33945
34054
  // admin SSE stream. Fire-and-forget: the manager's persist path must not