@rubytech/create-maxy-code 0.1.549 → 0.1.551

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 (55) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/agent-dispatch-rule/dist/index.d.ts +22 -8
  3. package/payload/platform/lib/agent-dispatch-rule/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/agent-dispatch-rule/dist/index.js +47 -13
  5. package/payload/platform/lib/agent-dispatch-rule/dist/index.js.map +1 -1
  6. package/payload/platform/lib/agent-dispatch-rule/src/__tests__/rule.test.ts +68 -9
  7. package/payload/platform/lib/agent-dispatch-rule/src/index.ts +60 -16
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -3
  9. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +8 -0
  10. package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
  11. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.js +2 -2
  12. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.js.map +1 -1
  13. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-sync-provision.test.js +2 -2
  14. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-sync-provision.test.js.map +1 -1
  15. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-sync-provision.d.ts.map +1 -1
  16. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-sync-provision.js +21 -8
  17. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-sync-provision.js.map +1 -1
  18. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-embedding.test.d.ts +2 -0
  19. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-embedding.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-embedding.test.js +142 -0
  21. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-embedding.test.js.map +1 -0
  22. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  23. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +41 -5
  24. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  25. package/payload/platform/plugins/telegram/.claude-plugin/plugin.json +1 -1
  26. package/payload/platform/plugins/telegram/PLUGIN.md +6 -4
  27. package/payload/platform/plugins/telegram/mcp/dist/index.js +62 -38
  28. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
  29. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.d.ts +18 -5
  30. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.d.ts.map +1 -1
  31. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.js +34 -6
  32. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.js.map +1 -1
  33. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts +25 -8
  34. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts.map +1 -1
  35. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js +28 -16
  36. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js.map +1 -1
  37. package/payload/platform/plugins/telegram/mcp/dist/lib/agent-active.d.ts +12 -0
  38. package/payload/platform/plugins/telegram/mcp/dist/lib/agent-active.d.ts.map +1 -0
  39. package/payload/platform/plugins/telegram/mcp/dist/lib/agent-active.js +26 -0
  40. package/payload/platform/plugins/telegram/mcp/dist/lib/agent-active.js.map +1 -0
  41. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.d.ts +16 -13
  42. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.d.ts.map +1 -1
  43. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.js +19 -23
  44. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.js.map +1 -1
  45. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.d.ts +9 -7
  46. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.d.ts.map +1 -1
  47. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.js +11 -8
  48. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.js.map +1 -1
  49. package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts +4 -0
  50. package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts.map +1 -1
  51. package/payload/platform/plugins/telegram/mcp/dist/tools/message.js +13 -3
  52. package/payload/platform/plugins/telegram/mcp/dist/tools/message.js.map +1 -1
  53. package/payload/platform/plugins/telegram/references/setup-guide.md +16 -8
  54. package/payload/platform/plugins/telegram/skills/configure/SKILL.md +30 -32
  55. package/payload/server/server.js +511 -240
@@ -1836,10 +1836,10 @@ var require_dist6 = __commonJS({
1836
1836
  });
1837
1837
 
1838
1838
  // server/index.ts
1839
- import { readFileSync as readFileSync47, existsSync as existsSync51, watchFile, readdirSync as readdirSync30, statSync as statSync27 } from "fs";
1839
+ import { readFileSync as readFileSync48, existsSync as existsSync52, watchFile, readdirSync as readdirSync30, statSync as statSync27 } from "fs";
1840
1840
  import { spawn as spawn3 } from "child_process";
1841
1841
  import { createHash as createHash7 } from "crypto";
1842
- import { resolve as resolve45, join as join55, basename as basename18 } from "path";
1842
+ import { resolve as resolve45, join as join56, basename as basename18 } from "path";
1843
1843
  import { homedir as homedir4 } from "os";
1844
1844
 
1845
1845
  // server/lib/route-cache-watchers.ts
@@ -3847,11 +3847,22 @@ ${result.result.text}` : result.result.text;
3847
3847
  import { statSync as statSync5 } from "fs";
3848
3848
  import { join as join10 } from "path";
3849
3849
 
3850
+ // app/lib/telegram/channel-key.ts
3851
+ function telegramChannelKey(botId, senderId) {
3852
+ return `${botId}:${senderId}`;
3853
+ }
3854
+ function parseTelegramChannelKey(key) {
3855
+ const i = key.indexOf(":");
3856
+ if (i <= 0 || i === key.length - 1) return null;
3857
+ return { botId: key.slice(0, i), senderId: key.slice(i + 1) };
3858
+ }
3859
+
3850
3860
  // app/lib/channel-pty-bridge/admin-session-id.ts
3851
3861
  import { createHash } from "crypto";
3852
- function adminSessionIdFor(accountId, senderId, personId) {
3862
+ function adminSessionIdFor(accountId, senderId, personId, botId) {
3853
3863
  const subject = personId && personId.length > 0 ? `person:${personId}` : senderId;
3854
- const b = createHash("sha256").update(`${accountId}:admin:${subject}`).digest().subarray(0, 16);
3864
+ const scope = botId && botId.length > 0 ? `admin:${botId}` : "admin";
3865
+ const b = createHash("sha256").update(`${accountId}:${scope}:${subject}`).digest().subarray(0, 16);
3855
3866
  const v = Buffer.from(b);
3856
3867
  v[6] = v[6] & 15 | 64;
3857
3868
  v[8] = v[8] & 63 | 128;
@@ -4598,7 +4609,8 @@ async function computeSessionList(scopeAccountId) {
4598
4609
  continue;
4599
4610
  }
4600
4611
  const metaPath = join9(projectDir, `${sessionId}.meta.json`);
4601
- const { bridgeIds, role, channel: sidecarChannel, senderId, personId, adminUserId, accountId: rowAccountId } = await readSidecarMetaAsync(metaPath);
4612
+ const { bridgeIds, role, channel: sidecarChannel, senderId: rawSenderId, personId, adminUserId, accountId: rowAccountId } = await readSidecarMetaAsync(metaPath);
4613
+ const senderId = sidecarChannel === "telegram" ? parseTelegramChannelKey(rawSenderId ?? "")?.senderId ?? rawSenderId : rawSenderId;
4602
4614
  if (multiAccount) {
4603
4615
  if (rowAccountId === null) untaggedCount += 1;
4604
4616
  if (rowAccountId !== scopeAccountId) continue;
@@ -4872,19 +4884,15 @@ function createScheduleInjectRoutes(deps) {
4872
4884
  console.error(`${TAG5} op=telegram-destination-ambiguous eventId=${eventId} destination=${destination} accounts=${resolution.count} dispatched=no`);
4873
4885
  return c.json({ ok: false, error: "telegram-destination-ambiguous" }, 403);
4874
4886
  }
4875
- const effectiveAccount = resolution.accountId;
4876
- console.error(`${TAG5} op=effective-account eventId=${eventId} effectiveAccount=${effectiveAccount}`);
4887
+ const { accountId: effectiveAccount, botId, botToken } = resolution;
4888
+ console.error(`${TAG5} op=effective-account eventId=${eventId} effectiveAccount=${effectiveAccount} botId=${botId}`);
4877
4889
  if (eventAccountId === null || eventAccountId !== effectiveAccount) {
4878
4890
  console.error(`${TAG5} op=event-account-scope-mismatch eventId=${eventId} eventAccount=${eventAccountId ?? "-"} effectiveAccount=${effectiveAccount} destination=${destination} dispatched=no`);
4879
4891
  return c.json({ ok: false, error: "event-account-scope-mismatch" }, 403);
4880
4892
  }
4881
- const botToken = resolution.botToken;
4882
- if (!botToken) {
4883
- console.error(`${TAG5} reject reason=no-admin-bot-token eventId=${eventId} accountId=${effectiveAccount}`);
4884
- return c.json({ ok: false, error: "no-admin-bot-token" }, 400);
4885
- }
4886
4893
  const chatId = Number(destination);
4887
- const sessionId = deps.resolveSessionOverride?.(effectiveAccount, "telegram", destination) ?? adminSessionIdFor(effectiveAccount, destination);
4894
+ const channelKey = telegramChannelKey(botId, destination);
4895
+ const sessionId = deps.resolveSessionOverride?.(effectiveAccount, "telegram", channelKey) ?? adminSessionIdFor(effectiveAccount, destination, void 0, botId);
4888
4896
  const reply = (text) => deps.sendTelegramText(botToken, chatId, text);
4889
4897
  try {
4890
4898
  await deps.tgHandleInbound({
@@ -4893,6 +4901,14 @@ function createScheduleInjectRoutes(deps) {
4893
4901
  // session scope are one account and Telegram needs no
4894
4902
  // effectiveAccountId companion field the way WhatsApp does above.
4895
4903
  accountId: effectiveAccount,
4904
+ // A scheduled dispatch is always role:'admin', and the admin branch of
4905
+ // the spawn never reads the account dir (only the public branch
4906
+ // re-checks its paired agent against it), so there is nothing to
4907
+ // resolve here.
4908
+ accountDir: "",
4909
+ botId,
4910
+ botToken,
4911
+ agentSlug: "admin",
4896
4912
  senderId: destination,
4897
4913
  chatId: destination,
4898
4914
  role: "admin",
@@ -12907,6 +12923,44 @@ import { homedir as homedir2 } from "os";
12907
12923
  import { resolve as resolve17, join as join29 } from "path";
12908
12924
  import { existsSync as existsSync21, readFileSync as readFileSync23 } from "fs";
12909
12925
 
12926
+ // app/lib/telegram/bot-registry.ts
12927
+ function botIdFromToken(token) {
12928
+ const i = token.indexOf(":");
12929
+ if (i <= 0) return null;
12930
+ const id = token.slice(0, i);
12931
+ return /^\d+$/.test(id) ? id : null;
12932
+ }
12933
+ function listBotEntries(config) {
12934
+ const raw = config?.bots;
12935
+ if (!Array.isArray(raw)) return [];
12936
+ const out = [];
12937
+ for (const e of raw) {
12938
+ if (!e || typeof e !== "object") continue;
12939
+ const { id, token, role } = e;
12940
+ if (typeof id !== "string" || id.length === 0) continue;
12941
+ if (typeof token !== "string" || token.length === 0) continue;
12942
+ if (role === "admin") {
12943
+ out.push(e);
12944
+ } else if (role === "public") {
12945
+ const agent = e.agent;
12946
+ if (typeof agent !== "string" || agent.length === 0) continue;
12947
+ out.push(e);
12948
+ }
12949
+ }
12950
+ return out;
12951
+ }
12952
+ function resolveBotEntry(accounts, botId) {
12953
+ const hits = [];
12954
+ for (const a of accounts) {
12955
+ for (const entry of listBotEntries(a.config.telegram)) {
12956
+ if (entry.id === botId) hits.push({ accountId: a.accountId, accountDir: a.accountDir, entry });
12957
+ }
12958
+ }
12959
+ if (hits.length === 0) return { kind: "none" };
12960
+ if (hits.length > 1) return { kind: "duplicate", count: hits.length };
12961
+ return { kind: "one", ...hits[0] };
12962
+ }
12963
+
12910
12964
  // app/lib/telegram/gateway/instance.ts
12911
12965
  var instance = null;
12912
12966
  function setTelegramGateway(gw) {
@@ -12918,20 +12972,20 @@ function getTelegramGateway() {
12918
12972
 
12919
12973
  // app/lib/telegram/access-control.ts
12920
12974
  function checkTelegramAccess(params) {
12921
- const { senderId, botType, config } = params;
12922
- if (botType === "admin") {
12923
- const adminUsers = config.adminUsers ?? [];
12975
+ const { senderId, entry } = params;
12976
+ if (entry.role === "admin") {
12977
+ const adminUsers = entry.adminUsers ?? [];
12924
12978
  if (adminUsers.includes(senderId)) {
12925
12979
  return { allowed: true, reason: "admin-binding", agentType: "admin" };
12926
12980
  }
12927
12981
  return { allowed: false, reason: "not-admin-user", agentType: "admin" };
12928
12982
  }
12929
- const policy = config.dmPolicy ?? "disabled";
12983
+ const policy = entry.dmPolicy ?? "disabled";
12930
12984
  switch (policy) {
12931
12985
  case "open":
12932
12986
  return { allowed: true, reason: "dm-policy-open", agentType: "public" };
12933
12987
  case "allowlist": {
12934
- const allowFrom = config.allowFrom ?? [];
12988
+ const allowFrom = entry.allowFrom ?? [];
12935
12989
  if (allowFrom.includes(senderId)) {
12936
12990
  return { allowed: true, reason: "allowlist-match", agentType: "public" };
12937
12991
  }
@@ -12953,11 +13007,11 @@ function routeTelegramUpdate(input) {
12953
13007
  if (typeof m.text !== "string" || m.text.length === 0) {
12954
13008
  return { kind: "ignore", reason: "no-text" };
12955
13009
  }
12956
- const access = checkTelegramAccess({ senderId: m.from.id, botType: input.botType, config: input.config });
13010
+ const access = checkTelegramAccess({ senderId: m.from.id, entry: input.entry });
12957
13011
  if (!access.allowed) {
12958
13012
  return { kind: "denied", reason: access.reason, agentType: access.agentType };
12959
13013
  }
12960
- return { kind: "dispatch", senderId: String(m.from.id), chatId: m.chat.id, text: m.text, agentType: access.agentType };
13014
+ return { kind: "dispatch", senderId: String(m.from.id), chatId: m.chat.id, text: m.text, agentType: access.agentType, reason: access.reason };
12961
13015
  }
12962
13016
 
12963
13017
  // server/routes/telegram.ts
@@ -12973,81 +13027,85 @@ function configDirName() {
12973
13027
  }
12974
13028
  return ".maxy";
12975
13029
  }
12976
- function secretPath(botType, accountId) {
12977
- const stem = botType === "admin" ? ".telegram-admin-webhook-secret" : ".telegram-webhook-secret";
12978
- return resolve17(homedir2(), configDirName(), `${stem}.${accountId}`);
13030
+ function telegramSecretsDir() {
13031
+ return resolve17(homedir2(), configDirName(), "telegram-secrets");
13032
+ }
13033
+ function secretPath(botId) {
13034
+ return resolve17(telegramSecretsDir(), botId);
12979
13035
  }
12980
13036
  var app10 = new Hono();
12981
13037
  app10.post("/", async (c) => {
12982
- const botParam = c.req.query("bot");
12983
- const botType = botParam === "admin" ? "admin" : botParam === "public" ? "public" : null;
12984
- if (!botType) {
13038
+ const botId = c.req.query("bot");
13039
+ if (!botId) {
12985
13040
  console.error(`${TAG19} op=reject reason=missing-bot-param`);
12986
13041
  return c.json({ ok: false }, 400);
12987
13042
  }
12988
- const accountParam = c.req.query("account");
12989
- if (!accountParam) {
12990
- console.error(`${TAG19} op=reject reason=missing-account-param botType=${botType}`);
13043
+ console.error(`${TAG19} op=received botId=${botId} path=/api/telegram/`);
13044
+ const resolved = resolveBotEntry(listValidAccounts(), botId);
13045
+ if (resolved.kind === "none") {
13046
+ console.error(`${TAG19} op=reject reason=unknown-bot botId=${botId}`);
12991
13047
  return c.json({ ok: false }, 401);
12992
13048
  }
12993
- const account = listValidAccounts().find((a) => a.accountId === accountParam) ?? null;
12994
- if (!account) {
12995
- console.error(`${TAG19} op=reject reason=bad-account botType=${botType}`);
13049
+ if (resolved.kind === "duplicate") {
13050
+ console.error(`${TAG19} op=reject reason=duplicate-bot-id botId=${botId} accounts=${resolved.count}`);
12996
13051
  return c.json({ ok: false }, 401);
12997
13052
  }
12998
- const sp = secretPath(botType, account.accountId);
13053
+ const { accountId, accountDir, entry } = resolved;
13054
+ const agentSlug = entry.role === "admin" ? "admin" : entry.agent;
13055
+ console.error(`${TAG19} op=entry botId=${botId} accountId=${accountId} role=${entry.role} agent=${agentSlug}`);
13056
+ const sp = secretPath(botId);
12999
13057
  if (!existsSync21(sp)) {
13000
- console.error(`${TAG19} op=reject reason=no-secret-file botType=${botType} accountId=${account.accountId}`);
13058
+ console.error(`${TAG19} op=secret botId=${botId} result=missing-file`);
13001
13059
  return c.json({ ok: false }, 401);
13002
13060
  }
13003
13061
  const expected = readFileSync23(sp, "utf-8").trim();
13004
13062
  const got = c.req.header("x-telegram-bot-api-secret-token") ?? "";
13005
13063
  if (got !== expected) {
13006
- console.error(`${TAG19} op=reject reason=bad-secret botType=${botType} accountId=${account.accountId}`);
13064
+ console.error(`${TAG19} op=secret botId=${botId} result=mismatch`);
13007
13065
  return c.json({ ok: false }, 401);
13008
13066
  }
13067
+ console.error(`${TAG19} op=secret botId=${botId} result=ok`);
13009
13068
  let update;
13010
13069
  try {
13011
13070
  update = await c.req.json();
13012
13071
  } catch {
13013
13072
  return c.json({ ok: true }, 200);
13014
13073
  }
13015
- const decision = routeTelegramUpdate({ update, botType, config: account.config.telegram ?? {} });
13074
+ const decision = routeTelegramUpdate({ update, entry });
13016
13075
  if (decision.kind === "ignore") {
13017
- console.error(`${TAG19} op=ignore reason=${decision.reason}`);
13076
+ console.error(`${TAG19} op=ignore botId=${botId} reason=${decision.reason}`);
13018
13077
  return c.json({ ok: true }, 200);
13019
13078
  }
13020
13079
  if (decision.kind === "denied") {
13021
- console.error(`${TAG19} op=denied reason=${decision.reason} agentType=${decision.agentType}`);
13080
+ console.error(`${TAG19} op=access botId=${botId} senderId=- allowed=false reason=${decision.reason}`);
13022
13081
  return c.json({ ok: true }, 200);
13023
13082
  }
13024
- const role = decision.agentType === "admin" ? "admin" : "public";
13025
- const agentSlug = role === "admin" ? "admin" : resolveDefaultAgentSlug(account.accountDir);
13026
- if (!agentSlug) {
13027
- console.error(`${TAG19} op=reject reason=no-default-agent`);
13083
+ const { senderId, chatId, text } = decision;
13084
+ console.error(`${TAG19} op=access botId=${botId} senderId=${senderId} allowed=true reason=${decision.reason}`);
13085
+ const active = entry.role === "admin" ? true : isActiveAgentSlug(accountDir, entry.agent);
13086
+ console.error(`${TAG19} op=agent botId=${botId} slug=${agentSlug} active=${active}`);
13087
+ if (!active) {
13088
+ console.error(`${TAG19} op=reject reason=agent-inactive botId=${botId} agent=${agentSlug}`);
13028
13089
  return c.json({ ok: true }, 200);
13029
13090
  }
13030
- console.error(`${TAG19} op=update accountId=${account.accountId} from=${decision.senderId}`);
13031
- const botToken = botType === "admin" ? account.config.telegram?.adminBotToken : account.config.telegram?.publicBotToken;
13032
- const { senderId, chatId, text } = decision;
13033
13091
  const gateway = getTelegramGateway();
13034
13092
  if (!gateway) {
13035
- console.error(`${TAG19} op=reject reason=gateway-not-ready from=${senderId}`);
13093
+ console.error(`${TAG19} op=reply-dropped botId=${botId} reason=gateway-not-ready`);
13036
13094
  return c.json({ ok: true }, 200);
13037
13095
  }
13038
13096
  const reply = async (replyText) => {
13039
- if (!botToken) {
13040
- const reason = account.config.telegram ? "no-bot-token" : "no-telegram-config";
13041
- console.error(`${TAG19} op=reply-dropped reason=${reason} botType=${botType} from=${senderId}`);
13042
- return;
13043
- }
13044
- const sent = await sendTelegramText(botToken, chatId, replyText);
13045
- console.error(`${TAG19} op=reply-sent from=${senderId} ok=${sent.ok}${sent.ok ? "" : ` error=${sent.error}`}`);
13097
+ const sent = await sendTelegramText(entry.token, chatId, replyText);
13098
+ if (sent.ok) console.error(`${TAG19} op=reply-sent botId=${botId} ok=true`);
13099
+ else console.error(`${TAG19} op=reply-dropped botId=${botId} reason=${sent.error}`);
13046
13100
  };
13047
13101
  void gateway.handleInbound({
13048
- accountId: account.accountId,
13102
+ accountId,
13103
+ accountDir,
13104
+ botId,
13105
+ botToken: entry.token,
13106
+ agentSlug,
13049
13107
  senderId,
13050
- role,
13108
+ role: entry.role,
13051
13109
  personId: null,
13052
13110
  chatId: String(chatId),
13053
13111
  text,
@@ -16927,18 +16985,18 @@ var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
16927
16985
  ]);
16928
16986
  var app24 = new Hono();
16929
16987
  app24.post("/", async (c) => {
16930
- const TAG53 = "[admin:events]";
16988
+ const TAG54 = "[admin:events]";
16931
16989
  let body;
16932
16990
  try {
16933
16991
  body = await c.req.json();
16934
16992
  } catch (err) {
16935
16993
  const detail = err instanceof Error ? err.message : String(err);
16936
- console.error(`${TAG53} reject reason=body-not-json detail=${detail}`);
16994
+ console.error(`${TAG54} reject reason=body-not-json detail=${detail}`);
16937
16995
  return c.json({ ok: false, detail: "Request body was not valid JSON" }, 400);
16938
16996
  }
16939
16997
  const event = typeof body.event === "string" ? body.event : "";
16940
16998
  if (!ALLOWED_EVENTS.has(event)) {
16941
- console.error(`${TAG53} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
16999
+ console.error(`${TAG54} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
16942
17000
  return c.json({ ok: false, detail: `Event "${event}" is not allowed` }, 400);
16943
17001
  }
16944
17002
  const rawFields = body.fields && typeof body.fields === "object" ? body.fields : {};
@@ -22405,6 +22463,7 @@ async function managerSpawn(opts) {
22405
22463
  name: opts.name,
22406
22464
  webchatChannel: opts.webchatChannel,
22407
22465
  waChannel: opts.waChannel,
22466
+ telegramChannel: opts.telegramChannel,
22408
22467
  sessionId: opts.sessionId,
22409
22468
  previousContext: opts.previousContext,
22410
22469
  initialMessage: opts.initialMessage,
@@ -22448,6 +22507,7 @@ async function managerRcSpawn(opts) {
22448
22507
  adminUserId: opts.adminUserId,
22449
22508
  waChannel: opts.waChannel,
22450
22509
  webchatChannel: opts.webchatChannel,
22510
+ telegramChannel: opts.telegramChannel,
22451
22511
  role: opts.role,
22452
22512
  channel: opts.channel,
22453
22513
  senderId: opts.senderId,
@@ -23934,8 +23994,24 @@ function readChannelAdmins(accountDir, channel) {
23934
23994
  return { adminPhones, accountManagers };
23935
23995
  }
23936
23996
  const tg = config.telegram ?? {};
23937
- const adminUsers = Array.isArray(tg.adminUsers) ? tg.adminUsers.filter((u) => typeof u === "number") : [];
23938
- return { adminUsers };
23997
+ const byBot = [];
23998
+ const seen = /* @__PURE__ */ new Set();
23999
+ const adminUsers = [];
24000
+ if (Array.isArray(tg.bots)) {
24001
+ for (const raw of tg.bots) {
24002
+ if (!raw || typeof raw !== "object") continue;
24003
+ const b = raw;
24004
+ if (b.role !== "admin" || typeof b.id !== "string" || b.id.length === 0) continue;
24005
+ const users = Array.isArray(b.adminUsers) ? b.adminUsers.filter((u) => typeof u === "number") : [];
24006
+ byBot.push({ botId: b.id, adminUsers: users });
24007
+ for (const u of users) {
24008
+ if (seen.has(u)) continue;
24009
+ seen.add(u);
24010
+ adminUsers.push(u);
24011
+ }
24012
+ }
24013
+ }
24014
+ return { adminUsers, byBot };
23939
24015
  }
23940
24016
  function registeredAccountForDestination(houseDir, houseAccountId, destination) {
23941
24017
  const { adminPhones, accountManagers } = readChannelAdmins(houseDir, "whatsapp");
@@ -23994,8 +24070,13 @@ function listDispatchDestinations(input) {
23994
24070
  admins.push({ channel: "whatsapp", destination: phone, label: "House admin" });
23995
24071
  }
23996
24072
  }
23997
- const { adminUsers } = readChannelAdmins(resolve31(accountsDir, accountId), "telegram");
23998
- const telegram = [...adminUsers].sort((a, b) => a - b).map((id) => ({ channel: "telegram", destination: String(id), label: "Telegram admin" }));
24073
+ const { byBot } = readChannelAdmins(resolve31(accountsDir, accountId), "telegram");
24074
+ const telegram = byBot.flatMap((b) => b.adminUsers.map((id) => ({ botId: b.botId, id }))).sort((a, b) => a.id - b.id || a.botId.localeCompare(b.botId)).map(({ botId, id }) => ({
24075
+ channel: "telegram",
24076
+ destination: String(id),
24077
+ label: "Telegram admin",
24078
+ botId
24079
+ }));
23999
24080
  const byDestination = (a, b) => a.destination.localeCompare(b.destination);
24000
24081
  return [...managers.sort(byDestination), ...admins.sort(byDestination), ...telegram];
24001
24082
  }
@@ -31102,19 +31183,22 @@ var TelegramGateway = class {
31102
31183
  * its own `sessionIdFor`.
31103
31184
  */
31104
31185
  sessionIdFor(input) {
31105
- return (input.role === "admin" ? this.deps.resolveSessionOverride?.(input.accountId, input.senderId) : null) || adminSessionIdFor(
31186
+ const key = telegramChannelKey(input.botId, input.senderId);
31187
+ return (input.role === "admin" ? this.deps.resolveSessionOverride?.(input.accountId, key) : null) || adminSessionIdFor(
31106
31188
  input.accountId,
31107
31189
  input.senderId,
31108
- input.role === "public" && input.personId ? input.personId : void 0
31190
+ input.role === "public" && input.personId ? input.personId : void 0,
31191
+ input.botId
31109
31192
  );
31110
31193
  }
31111
31194
  /** Handle one inbound telegram message. */
31112
31195
  async handleInbound(input) {
31113
31196
  const bytes = Buffer.byteLength(input.text, "utf8");
31114
- const hadSubscriber = this.hub.hasSubscriber(input.senderId);
31115
- const willDeliverNow = this.hub.isReady(input.senderId);
31197
+ const key = telegramChannelKey(input.botId, input.senderId);
31198
+ const hadSubscriber = this.hub.hasSubscriber(key);
31199
+ const willDeliverNow = this.hub.isReady(key);
31116
31200
  const source = input.source ?? "user";
31117
- this.replies.set(input.senderId, input.reply);
31201
+ this.replies.set(key, input.reply);
31118
31202
  let standingRules;
31119
31203
  if (input.role === "admin" && this.deps.fetchStandingRules) {
31120
31204
  const res = await this.deps.fetchStandingRules(input.accountId);
@@ -31126,19 +31210,24 @@ var TelegramGateway = class {
31126
31210
  );
31127
31211
  }
31128
31212
  this.hub.deliver(
31129
- { key: input.senderId, text: input.text, messageId: `tg-${++this.seq}`, source, standingRules, scheduleProvenance: input.scheduleProvenance },
31213
+ { key, text: input.text, messageId: `tg-${++this.seq}`, source, standingRules, scheduleProvenance: input.scheduleProvenance },
31130
31214
  Date.now()
31131
31215
  );
31132
31216
  const resolvedSessionId = this.sessionIdFor(input);
31133
31217
  console.error(
31134
- `[telegram-native] op=inbound key=${input.senderId} accountId=${input.accountId} botType=${input.role} bytes=${bytes} source=${source} delivery=${willDeliverNow ? "immediate" : "queued"} sessionId=${resolvedSessionId}`
31218
+ `[telegram-native] op=inbound key=${key} botId=${input.botId} accountId=${input.accountId} role=${input.role} agent=${input.agentSlug} bytes=${bytes} source=${source} delivery=${willDeliverNow ? "immediate" : "queued"} sessionId=${resolvedSessionId}`
31135
31219
  );
31136
- if (!hadSubscriber && !this.spawning.has(input.senderId)) {
31137
- this.spawning.add(input.senderId);
31138
- console.error(`[telegram-native] op=spawn-trigger key=${input.senderId} role=${input.role}`);
31220
+ if (!hadSubscriber && !this.spawning.has(key)) {
31221
+ this.spawning.add(key);
31222
+ console.error(`[telegram-native] op=spawn-trigger key=${key} botId=${input.botId} role=${input.role}`);
31139
31223
  try {
31140
31224
  await this.deps.ensureChannelSession({
31141
31225
  accountId: input.accountId,
31226
+ accountDir: input.accountDir,
31227
+ botId: input.botId,
31228
+ botToken: input.botToken,
31229
+ agentSlug: input.agentSlug,
31230
+ channelKey: key,
31142
31231
  senderId: input.senderId,
31143
31232
  role: input.role,
31144
31233
  personId: input.personId ?? null,
@@ -31147,7 +31236,7 @@ var TelegramGateway = class {
31147
31236
  serverPath: this.deps.serverPath
31148
31237
  });
31149
31238
  } finally {
31150
- this.spawning.delete(input.senderId);
31239
+ this.spawning.delete(key);
31151
31240
  }
31152
31241
  }
31153
31242
  }
@@ -31161,12 +31250,17 @@ var TelegramGateway = class {
31161
31250
  // app/lib/telegram/gateway/spawn-request.ts
31162
31251
  function buildTelegramSpawnRequest(input) {
31163
31252
  const personId = input.role === "public" && input.personId ? input.personId : void 0;
31253
+ const channelKey = telegramChannelKey(input.botId, input.senderId);
31164
31254
  return {
31165
31255
  // Task 1746 / 1789 — the deterministic per-sender id is the DEFAULT; an
31166
31256
  // operator re-seat sets a stable per-sender forward pointer that wins here.
31167
31257
  // Mirror of the WhatsApp builder; see its comment for the full distinction
31168
31258
  // between this pointer and Task 1526's removed per-firing seat.
31169
- sessionId: input.overrideSessionId || adminSessionIdFor(input.accountId, input.senderId, personId),
31259
+ // Task 2368 the bot id is a hash component of its own, not part of the
31260
+ // subject: with a resolved personId the subject is `person:<personId>` and
31261
+ // the sender never enters the hash at all.
31262
+ sessionId: input.overrideSessionId || adminSessionIdFor(input.accountId, input.senderId, personId, input.botId),
31263
+ channelKey,
31170
31264
  role: input.role,
31171
31265
  channel: "telegram",
31172
31266
  personId,
@@ -31176,8 +31270,78 @@ function buildTelegramSpawnRequest(input) {
31176
31270
  // session resolve under that account, not the manager's boot-time house
31177
31271
  // default. Mirror of the WhatsApp builder.
31178
31272
  targetAccountId: input.accountId,
31179
- telegramChannel: { senderId: input.senderId, gatewayUrl: input.gatewayUrl, serverPath: input.serverPath },
31180
- logContext: `channel=telegram-native role=${input.role} senderId=${input.senderId}`
31273
+ telegramChannel: { senderId: channelKey, gatewayUrl: input.gatewayUrl, serverPath: input.serverPath },
31274
+ logContext: `channel=telegram-native role=${input.role} botId=${input.botId} senderId=${input.senderId}`
31275
+ };
31276
+ }
31277
+
31278
+ // server/telegram-ensure-session.ts
31279
+ var TAG50 = "[telegram-inbound]";
31280
+ function buildTelegramEnsureSession(deps) {
31281
+ return async (input) => {
31282
+ const req = buildTelegramSpawnRequest({
31283
+ accountId: input.accountId,
31284
+ senderId: input.senderId,
31285
+ role: input.role,
31286
+ personId: input.personId,
31287
+ gatewayUrl: input.gatewayUrl,
31288
+ serverPath: input.serverPath,
31289
+ botId: input.botId,
31290
+ // Task 1789 — admin rows only; see the WhatsApp twin.
31291
+ overrideSessionId: input.role === "admin" ? deps.resolveSessionOverride(input.accountId, input.channelKey) : null
31292
+ });
31293
+ if (input.role === "public") {
31294
+ const result2 = await deps.managerSpawn({
31295
+ senderId: req.channelKey,
31296
+ role: "public",
31297
+ channel: "telegram",
31298
+ accountId: input.accountId,
31299
+ targetAccountId: input.accountId,
31300
+ // The slug came from THIS account's own bot entry, so the spawn reads
31301
+ // that account's agents directory, runs in its directory and files its
31302
+ // session under it. Mirror of the WhatsApp public-agent path.
31303
+ agentAccountId: input.accountId,
31304
+ agentSlug: input.agentSlug,
31305
+ sessionId: req.sessionId,
31306
+ personId: input.personId ?? void 0,
31307
+ telegramChannel: req.telegramChannel
31308
+ });
31309
+ if ("error" in result2) {
31310
+ console.error(
31311
+ `[telegram-native] op=spawn-failed botId=${input.botId} key=${req.channelKey} role=public agent=${input.agentSlug} error=${result2.error} status=${result2.status}`
31312
+ );
31313
+ return;
31314
+ }
31315
+ console.error(
31316
+ `${TAG50} op=spawn botId=${input.botId} sessionId=${result2.sessionId} targetAccountId=${input.accountId} role=public`
31317
+ );
31318
+ return;
31319
+ }
31320
+ const result = await deps.managerRcSpawn(req);
31321
+ if ("error" in result) {
31322
+ console.error(
31323
+ `[telegram-native] op=spawn-failed botId=${input.botId} key=${req.channelKey} role=admin error=${result.error} status=${result.status}`
31324
+ );
31325
+ return;
31326
+ }
31327
+ console.error(
31328
+ `${TAG50} op=spawn botId=${input.botId} sessionId=${req.sessionId} targetAccountId=${input.accountId} role=admin`
31329
+ );
31330
+ deps.followers.get(req.channelKey)?.abort();
31331
+ let ac;
31332
+ ac = deps.startFileFollower({
31333
+ sessionId: req.sessionId,
31334
+ senderId: req.channelKey,
31335
+ accountId: input.accountId,
31336
+ chatId: input.chatId,
31337
+ botToken: input.botToken,
31338
+ // Identity-guarded eviction: only delete if this controller is still the
31339
+ // current one, so a stale self-close never removes a live follower.
31340
+ onClose: () => {
31341
+ if (deps.followers.get(req.channelKey) === ac) deps.followers.delete(req.channelKey);
31342
+ }
31343
+ });
31344
+ deps.followers.set(req.channelKey, ac);
31181
31345
  };
31182
31346
  }
31183
31347
 
@@ -31185,7 +31349,7 @@ function buildTelegramSpawnRequest(input) {
31185
31349
  import { realpathSync as realpathSync10 } from "fs";
31186
31350
  import { readFile as readFile14, stat as stat10 } from "fs/promises";
31187
31351
  import { resolve as resolve42, basename as basename16 } from "path";
31188
- var TAG50 = "[telegram:outbound]";
31352
+ var TAG51 = "[telegram:outbound]";
31189
31353
  var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
31190
31354
  async function sendTelegramDocument(input) {
31191
31355
  const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot5 } = input;
@@ -31202,10 +31366,10 @@ async function sendTelegramDocument(input) {
31202
31366
  } catch (err) {
31203
31367
  const code = err.code;
31204
31368
  if (code === "ENOENT") {
31205
- console.error(`${TAG50} document ENOENT scope=account-dir accountDir=${accountDir} maxyAccountId=${maxyAccountId}`);
31369
+ console.error(`${TAG51} document ENOENT scope=account-dir accountDir=${accountDir} maxyAccountId=${maxyAccountId}`);
31206
31370
  return { ok: false, status: 500, error: `Account directory not resolvable for ${maxyAccountId}` };
31207
31371
  }
31208
- console.error(`${TAG50} document path error scope=account-dir maxyAccountId=${maxyAccountId}: ${String(err)}`);
31372
+ console.error(`${TAG51} document path error scope=account-dir maxyAccountId=${maxyAccountId}: ${String(err)}`);
31209
31373
  return { ok: false, status: 500, error: String(err) };
31210
31374
  }
31211
31375
  let resolvedPath;
@@ -31214,14 +31378,14 @@ async function sendTelegramDocument(input) {
31214
31378
  } catch (err) {
31215
31379
  const code = err.code;
31216
31380
  if (code === "ENOENT") {
31217
- console.error(`${TAG50} document ENOENT scope=file path=${filePath} maxyAccountId=${maxyAccountId}`);
31381
+ console.error(`${TAG51} document ENOENT scope=file path=${filePath} maxyAccountId=${maxyAccountId}`);
31218
31382
  return { ok: false, status: 404, error: `File not found: ${filePath}` };
31219
31383
  }
31220
- console.error(`${TAG50} document path error scope=file maxyAccountId=${maxyAccountId}: ${String(err)}`);
31384
+ console.error(`${TAG51} document path error scope=file maxyAccountId=${maxyAccountId}: ${String(err)}`);
31221
31385
  return { ok: false, status: 500, error: String(err) };
31222
31386
  }
31223
31387
  if (!resolvedPath.startsWith(accountResolved + "/")) {
31224
- console.error(`${TAG50} document REJECTED reason=outside_account_directory maxyAccountId=${maxyAccountId}`);
31388
+ console.error(`${TAG51} document REJECTED reason=outside_account_directory maxyAccountId=${maxyAccountId}`);
31225
31389
  return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
31226
31390
  }
31227
31391
  const fileStat = await stat10(resolvedPath);
@@ -31254,29 +31418,24 @@ async function sendTelegramDocument(input) {
31254
31418
  error = e instanceof Error ? e.message : String(e);
31255
31419
  }
31256
31420
  console.error(
31257
- `${TAG50} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok2}` + (messageId !== void 0 ? ` id=${messageId}` : "")
31421
+ `${TAG51} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok2}` + (messageId !== void 0 ? ` id=${messageId}` : "")
31258
31422
  );
31259
31423
  return ok2 ? { ok: true, messageId } : { ok: false, status: 500, error };
31260
31424
  }
31261
31425
 
31262
31426
  // app/lib/telegram/outbound/file-delivery.ts
31263
- var TAG51 = "[telegram:outbound]";
31427
+ var TAG52 = "[telegram:outbound]";
31264
31428
  function platformRoot4() {
31265
31429
  return process.env.MAXY_PLATFORM_ROOT || "";
31266
31430
  }
31267
- function makeTelegramSendFile(entry) {
31268
- let botToken;
31431
+ function makeTelegramSendFile(entry, botToken) {
31269
31432
  return async (filePath, caption) => {
31270
- if (botToken === void 0) {
31271
- const tg = resolveAccount()?.config.telegram;
31272
- botToken = (entry.role === "admin" ? tg?.adminBotToken : tg?.publicBotToken) ?? null;
31273
- }
31274
31433
  if (!botToken) {
31275
- console.error(`${TAG51} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
31434
+ console.error(`${TAG52} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
31276
31435
  return { ok: false, error: "no-bot-token" };
31277
31436
  }
31278
31437
  if (entry.replyTarget == null) {
31279
- console.error(`${TAG51} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
31438
+ console.error(`${TAG52} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
31280
31439
  return { ok: false, error: "no-reply-target" };
31281
31440
  }
31282
31441
  const result = await sendTelegramDocument({
@@ -31290,12 +31449,12 @@ function makeTelegramSendFile(entry) {
31290
31449
  return result.ok ? { ok: true } : { ok: false, error: result.error };
31291
31450
  };
31292
31451
  }
31293
- function makeTelegramFileDelivery(entry) {
31452
+ function makeTelegramFileDelivery(entry, botToken) {
31294
31453
  return makeFileDelivery({
31295
31454
  entry,
31296
- tag: TAG51,
31455
+ tag: TAG52,
31297
31456
  channel: "telegram",
31298
- sendFile: makeTelegramSendFile(entry)
31457
+ sendFile: makeTelegramSendFile(entry, botToken)
31299
31458
  });
31300
31459
  }
31301
31460
 
@@ -31323,7 +31482,7 @@ function startTelegramNativeFileFollower(input) {
31323
31482
  return startFollower({
31324
31483
  entry,
31325
31484
  tag: "[telegram-adaptor]",
31326
- fileDelivery: makeTelegramFileDelivery(entry),
31485
+ fileDelivery: makeTelegramFileDelivery(entry, input.botToken),
31327
31486
  // A resumed session's JSONL already holds prior SendUserFile tool_uses;
31328
31487
  // suppress replay so historical files are not re-sent on attach.
31329
31488
  suppressResumeReplay: true,
@@ -31334,7 +31493,7 @@ function startTelegramNativeFileFollower(input) {
31334
31493
 
31335
31494
  // app/lib/channel-pty-bridge/public-session-end-review.ts
31336
31495
  import { randomUUID as randomUUID14 } from "crypto";
31337
- var TAG52 = "[public-session-review]";
31496
+ var TAG53 = "[public-session-review]";
31338
31497
  var CONSUMED_CAP = 500;
31339
31498
  var consumed = /* @__PURE__ */ new Set();
31340
31499
  function consumeOnce(sessionId) {
@@ -31361,7 +31520,7 @@ async function fetchJsonl(sessionId) {
31361
31520
  return await res.text();
31362
31521
  } catch (err) {
31363
31522
  console.error(
31364
- `${TAG52} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
31523
+ `${TAG53} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
31365
31524
  );
31366
31525
  return null;
31367
31526
  }
@@ -31385,7 +31544,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
31385
31544
  const res = await fetch(url);
31386
31545
  if (!res.ok) {
31387
31546
  console.error(
31388
- `${TAG52} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
31547
+ `${TAG53} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
31389
31548
  );
31390
31549
  return [];
31391
31550
  }
@@ -31393,7 +31552,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
31393
31552
  return Array.isArray(body.writes) ? body.writes : [];
31394
31553
  } catch (err) {
31395
31554
  console.error(
31396
- `${TAG52} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
31555
+ `${TAG53} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
31397
31556
  );
31398
31557
  return [];
31399
31558
  }
@@ -31422,7 +31581,7 @@ function composeInitialMessage(input) {
31422
31581
  }
31423
31582
  async function dispatchReviewer(input, initialMessage) {
31424
31583
  const sessionId = randomUUID14();
31425
- console.log(`${TAG52} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
31584
+ console.log(`${TAG53} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
31426
31585
  const spawned = await managerRcSpawn({
31427
31586
  sessionId,
31428
31587
  initialMessage,
@@ -31446,21 +31605,21 @@ async function firePublicSessionEndReview(input) {
31446
31605
  const dispatchedAt = Date.now();
31447
31606
  if (consumed.has(input.sessionId)) {
31448
31607
  console.log(
31449
- `${TAG52} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
31608
+ `${TAG53} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
31450
31609
  );
31451
31610
  return;
31452
31611
  }
31453
31612
  const jsonl = await fetchJsonl(input.sessionId);
31454
31613
  if (!jsonl) {
31455
31614
  console.log(
31456
- `${TAG52} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
31615
+ `${TAG53} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
31457
31616
  );
31458
31617
  return;
31459
31618
  }
31460
31619
  const operatorTurns = countOperatorTurns(jsonl);
31461
31620
  if (operatorTurns === 0) {
31462
31621
  console.log(
31463
- `${TAG52} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
31622
+ `${TAG53} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
31464
31623
  );
31465
31624
  return;
31466
31625
  }
@@ -31474,19 +31633,19 @@ async function firePublicSessionEndReview(input) {
31474
31633
  });
31475
31634
  if (!consumeOnce(input.sessionId)) {
31476
31635
  console.log(
31477
- `${TAG52} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
31636
+ `${TAG53} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
31478
31637
  );
31479
31638
  return;
31480
31639
  }
31481
31640
  const dispatched = await dispatchReviewer(input, initialMessage);
31482
31641
  if (!dispatched.ok) {
31483
31642
  console.error(
31484
- `${TAG52} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
31643
+ `${TAG53} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
31485
31644
  );
31486
31645
  return;
31487
31646
  }
31488
31647
  console.log(
31489
- `${TAG52} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
31648
+ `${TAG53} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
31490
31649
  );
31491
31650
  }
31492
31651
 
@@ -31624,28 +31783,59 @@ import { statSync as statSync25 } from "fs";
31624
31783
  import { basename as basename17, dirname as dirname17, join as join54 } from "path";
31625
31784
  function buildTelegramAuditLines(input) {
31626
31785
  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";
31786
+ const entries2 = input.accounts.flatMap(
31787
+ (a) => listBotEntries(a.telegram).map((entry) => ({ accountId: a.accountId, accountDir: a.accountDir, entry }))
31788
+ );
31789
+ if (entries2.length === 0) return lines;
31790
+ const byId = /* @__PURE__ */ new Map();
31791
+ for (const { entry } of entries2) byId.set(entry.id, (byId.get(entry.id) ?? 0) + 1);
31792
+ for (const [botId, count] of byId) {
31793
+ if (count > 1) lines.push(`[telegram-audit] op=duplicate-bot-id botId=${botId} accounts=${count}`);
31794
+ }
31795
+ for (const { accountId, accountDir, entry } of entries2) {
31796
+ if (entry.role === "public" && !input.agentActive(accountDir, entry.agent)) {
31638
31797
  lines.push(
31639
- `[telegram-audit] op=configured accountId=${account.accountId} botType=${botType} secretFile=${secretFile} lastInboundAgeMs=${age}`
31798
+ `[telegram-audit] op=entry-agent-inactive accountId=${accountId} botId=${entry.id} agent=${entry.agent}`
31640
31799
  );
31641
31800
  }
31801
+ if (!input.secretFileExists(entry.id)) {
31802
+ lines.push(`[telegram-audit] op=secret-orphan botId=${entry.id} side=entry`);
31803
+ }
31804
+ }
31805
+ const known = new Set(entries2.map((e) => e.entry.id));
31806
+ for (const botId of input.secretFilesOnDisk) {
31807
+ if (!known.has(botId)) lines.push(`[telegram-audit] op=secret-orphan botId=${botId} side=file`);
31808
+ }
31809
+ for (const { accountId, entry } of entries2) {
31810
+ const last = input.lastInboundByBot(entry.id);
31811
+ const age = last === null ? "never" : String(input.now - last);
31812
+ const agent = entry.role === "admin" ? "admin" : entry.agent;
31813
+ const secretFile = input.secretFileExists(entry.id) ? "present" : "absent";
31814
+ lines.push(
31815
+ `[telegram-audit] op=configured botId=${entry.id} accountId=${accountId} agent=${agent} secretFile=${secretFile} lastInboundAgeMs=${age}`
31816
+ );
31642
31817
  }
31643
31818
  return lines;
31644
31819
  }
31645
- function newestTelegramSessionByAccount() {
31820
+ function newestTelegramSessionByBot() {
31646
31821
  const out = /* @__PURE__ */ new Map();
31822
+ for (const { botId, mtime } of walkTelegramSidecars()) {
31823
+ if (botId === null) continue;
31824
+ const prev = out.get(botId);
31825
+ if (prev === void 0 || mtime > prev) out.set(botId, mtime);
31826
+ }
31827
+ return out;
31828
+ }
31829
+ function countTelegramSessionsForAccount(accountId) {
31830
+ let n = 0;
31831
+ for (const { accountId: id } of walkTelegramSidecars()) {
31832
+ if (id === accountId) n += 1;
31833
+ }
31834
+ return n;
31835
+ }
31836
+ function* walkTelegramSidecars() {
31647
31837
  const cfg = claudeConfigDir();
31648
- if (!cfg) return out;
31838
+ if (!cfg) return;
31649
31839
  const projectsRoot = join54(cfg, "projects");
31650
31840
  for (const { path } of enumerateJsonls(projectsRoot)) {
31651
31841
  const sessionId = basename17(path).replace(/\.jsonl$/, "");
@@ -31657,11 +31847,80 @@ function newestTelegramSessionByAccount() {
31657
31847
  } catch {
31658
31848
  continue;
31659
31849
  }
31660
- const prev = out.get(meta.accountId);
31661
- if (prev === void 0 || mtime > prev) out.set(meta.accountId, mtime);
31850
+ yield {
31851
+ accountId: meta.accountId,
31852
+ botId: parseTelegramChannelKey(meta.senderId ?? "")?.botId ?? null,
31853
+ mtime
31854
+ };
31855
+ }
31856
+ }
31857
+
31858
+ // server/telegram-migrate.ts
31859
+ import { readFileSync as readFileSync45, writeFileSync as writeFileSync17, existsSync as existsSync49, renameSync as renameSync11, mkdirSync as mkdirSync10 } from "fs";
31860
+ import { dirname as dirname18, join as join55 } from "path";
31861
+ function migrateTelegramTwoSlot(input) {
31862
+ const out = [];
31863
+ for (const account of input.accounts) {
31864
+ const configPath3 = join55(account.accountDir, "account.json");
31865
+ let config;
31866
+ try {
31867
+ config = JSON.parse(readFileSync45(configPath3, "utf-8"));
31868
+ } catch {
31869
+ console.error(`[telegram-audit] op=migrate-unreadable accountId=${account.accountId}`);
31870
+ continue;
31871
+ }
31872
+ const tg = config.telegram;
31873
+ if (!tg) continue;
31874
+ const adminToken = typeof tg.adminBotToken === "string" ? tg.adminBotToken : null;
31875
+ const publicToken = typeof tg.publicBotToken === "string" ? tg.publicBotToken : null;
31876
+ if (!adminToken && !publicToken) continue;
31877
+ const outcome = { accountId: account.accountId, converted: 0, orphanSessions: 0 };
31878
+ const bots = Array.isArray(tg.bots) ? [...tg.bots] : [];
31879
+ if (adminToken) {
31880
+ const id = botIdFromToken(adminToken);
31881
+ if (!id) {
31882
+ outcome.skippedAdmin = "malformed-token";
31883
+ } else {
31884
+ const adminUsers = Array.isArray(tg.adminUsers) ? tg.adminUsers.filter((u) => typeof u === "number") : void 0;
31885
+ bots.push({ id, token: adminToken, role: "admin", ...adminUsers ? { adminUsers } : {} });
31886
+ moveSecret(input.legacySecretPathFor("admin", account.accountId), input.secretPathFor(id));
31887
+ outcome.converted += 1;
31888
+ }
31889
+ }
31890
+ if (publicToken) {
31891
+ const id = botIdFromToken(publicToken);
31892
+ const agent = typeof config.defaultAgent === "string" && config.defaultAgent.length > 0 ? config.defaultAgent : null;
31893
+ if (!id) {
31894
+ outcome.skippedPublic = "malformed-token";
31895
+ } else if (!agent) {
31896
+ outcome.skippedPublic = "no-default-agent";
31897
+ } else {
31898
+ const dmPolicy = typeof tg.dmPolicy === "string" ? tg.dmPolicy : void 0;
31899
+ const allowFrom = Array.isArray(tg.allowFrom) ? tg.allowFrom.filter((u) => typeof u === "number") : void 0;
31900
+ bots.push({
31901
+ id,
31902
+ token: publicToken,
31903
+ role: "public",
31904
+ agent,
31905
+ ...dmPolicy ? { dmPolicy } : {},
31906
+ ...allowFrom ? { allowFrom } : {}
31907
+ });
31908
+ moveSecret(input.legacySecretPathFor("public", account.accountId), input.secretPathFor(id));
31909
+ outcome.converted += 1;
31910
+ }
31911
+ }
31912
+ config.telegram = { bots };
31913
+ writeFileSync17(configPath3, JSON.stringify(config, null, 2) + "\n", "utf-8");
31914
+ outcome.orphanSessions = input.countOrphanSessions(account.accountId);
31915
+ out.push(outcome);
31662
31916
  }
31663
31917
  return out;
31664
31918
  }
31919
+ function moveSecret(from, to) {
31920
+ if (!existsSync49(from)) return;
31921
+ mkdirSync10(dirname18(to), { recursive: true, mode: 448 });
31922
+ renameSync11(from, to);
31923
+ }
31665
31924
 
31666
31925
  // app/lib/whatsapp/recall-store-backfill.ts
31667
31926
  function backfillMisfiledRecords(params) {
@@ -31699,14 +31958,14 @@ function backfillMisfiledRecords(params) {
31699
31958
  }
31700
31959
 
31701
31960
  // server/lib/booking-site-accounts.ts
31702
- import { existsSync as existsSync49, readFileSync as readFileSync45 } from "fs";
31961
+ import { existsSync as existsSync50, readFileSync as readFileSync46 } from "fs";
31703
31962
  import { resolve as resolve43 } from "path";
31704
31963
  function listBookingSiteAccounts(accounts) {
31705
31964
  return accounts.filter((a) => {
31706
31965
  const availPath = resolve43(a.accountDir, "calendar-availability.json");
31707
- if (!existsSync49(availPath)) return false;
31966
+ if (!existsSync50(availPath)) return false;
31708
31967
  try {
31709
- const cfg = JSON.parse(readFileSync45(availPath, "utf-8"));
31968
+ const cfg = JSON.parse(readFileSync46(availPath, "utf-8"));
31710
31969
  return typeof cfg.bookingDbName === "string" && cfg.bookingDbName.length > 0;
31711
31970
  } catch {
31712
31971
  return false;
@@ -31744,7 +32003,7 @@ function broadcastAdminShutdown(reason) {
31744
32003
 
31745
32004
  // ../lib/entitlement/src/index.ts
31746
32005
  import { createPublicKey, createHash as createHash6, verify as cryptoVerify } from "crypto";
31747
- import { existsSync as existsSync50, readFileSync as readFileSync46, statSync as statSync26 } from "fs";
32006
+ import { existsSync as existsSync51, readFileSync as readFileSync47, statSync as statSync26 } from "fs";
31748
32007
  import { resolve as resolve44 } from "path";
31749
32008
 
31750
32009
  // ../lib/entitlement/src/canonicalize.ts
@@ -31793,7 +32052,7 @@ function resolveEntitlement(brand, account) {
31793
32052
  return logResolved(implicitTrust(account), null);
31794
32053
  }
31795
32054
  const entitlementPath = resolve44(brand.configDir, "entitlement.json");
31796
- if (!existsSync50(entitlementPath)) {
32055
+ if (!existsSync51(entitlementPath)) {
31797
32056
  return logResolved(anonymousFallback("missing"), { reason: "missing" });
31798
32057
  }
31799
32058
  const stat11 = statSync26(entitlementPath);
@@ -31808,7 +32067,7 @@ function resolveEntitlement(brand, account) {
31808
32067
  function verifyAndResolve(brand, entitlementPath, account) {
31809
32068
  let pubkeyPem;
31810
32069
  try {
31811
- pubkeyPem = readFileSync46(pubkeyPath(brand), "utf-8");
32070
+ pubkeyPem = readFileSync47(pubkeyPath(brand), "utf-8");
31812
32071
  } catch (err) {
31813
32072
  return logResolved(anonymousFallback("pubkey-missing"), {
31814
32073
  reason: "pubkey-missing"
@@ -31822,7 +32081,7 @@ function verifyAndResolve(brand, entitlementPath, account) {
31822
32081
  }
31823
32082
  let envelope;
31824
32083
  try {
31825
- envelope = JSON.parse(readFileSync46(entitlementPath, "utf-8"));
32084
+ envelope = JSON.parse(readFileSync47(entitlementPath, "utf-8"));
31826
32085
  } catch {
31827
32086
  return logResolved(anonymousFallback("malformed"), { reason: "malformed" });
31828
32087
  }
@@ -31984,14 +32243,14 @@ function clientFrom(c) {
31984
32243
  }
31985
32244
  installMediaDownloadGuard();
31986
32245
  var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT || "";
31987
- var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join55(PLATFORM_ROOT6, "config", "brand.json") : "";
32246
+ var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join56(PLATFORM_ROOT6, "config", "brand.json") : "";
31988
32247
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", marketingUrl: "https://getmaxy.com" };
31989
- if (BRAND_JSON_PATH && !existsSync51(BRAND_JSON_PATH)) {
32248
+ if (BRAND_JSON_PATH && !existsSync52(BRAND_JSON_PATH)) {
31990
32249
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
31991
32250
  }
31992
- if (BRAND_JSON_PATH && existsSync51(BRAND_JSON_PATH)) {
32251
+ if (BRAND_JSON_PATH && existsSync52(BRAND_JSON_PATH)) {
31993
32252
  try {
31994
- const parsed = JSON.parse(readFileSync47(BRAND_JSON_PATH, "utf-8"));
32253
+ const parsed = JSON.parse(readFileSync48(BRAND_JSON_PATH, "utf-8"));
31995
32254
  BRAND = { ...BRAND, ...parsed };
31996
32255
  } catch (err) {
31997
32256
  console.error(`[brand] Failed to parse brand.json: ${err.message}`);
@@ -32023,11 +32282,11 @@ var brandLoginOpts = {
32023
32282
  appleTouchIconPath: brandAppIcon512Path,
32024
32283
  themeColor: BRAND.defaultColors?.primary
32025
32284
  };
32026
- var ALIAS_DOMAINS_PATH = join55(homedir4(), BRAND.configDir, "alias-domains.json");
32285
+ var ALIAS_DOMAINS_PATH = join56(homedir4(), BRAND.configDir, "alias-domains.json");
32027
32286
  function loadAliasDomains() {
32028
32287
  try {
32029
- if (!existsSync51(ALIAS_DOMAINS_PATH)) return null;
32030
- const parsed = JSON.parse(readFileSync47(ALIAS_DOMAINS_PATH, "utf-8"));
32288
+ if (!existsSync52(ALIAS_DOMAINS_PATH)) return null;
32289
+ const parsed = JSON.parse(readFileSync48(ALIAS_DOMAINS_PATH, "utf-8"));
32031
32290
  if (!Array.isArray(parsed)) {
32032
32291
  console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
32033
32292
  return null;
@@ -32118,7 +32377,7 @@ var waGateway = new WaGateway({
32118
32377
  sendToParty: (accountId, phone, text) => sendWhatsAppTextForRelay(accountId, phone, text),
32119
32378
  resolveSessionOverride: (accountId, senderId) => resolveChannelOverride(accountId, "whatsapp", senderId),
32120
32379
  gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
32121
- serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve45(process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
32380
+ serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve45(process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
32122
32381
  // Task 751 / 1390 — file delivery on the native channel. `maxyAccountId` (the
32123
32382
  // path-validation scope) is the sender's effective SESSION account, resolved
32124
32383
  // once at the inbound gate and threaded here via the gateway's per-sender doc
@@ -32133,7 +32392,7 @@ var waGateway = new WaGateway({
32133
32392
  caption,
32134
32393
  accountId,
32135
32394
  maxyAccountId,
32136
- platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, ".."))
32395
+ platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, ".."))
32137
32396
  });
32138
32397
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
32139
32398
  },
@@ -32300,7 +32559,7 @@ function runDuplicateSenderAudit() {
32300
32559
  try {
32301
32560
  const configDir = process.env.CLAUDE_CONFIG_DIR;
32302
32561
  if (!configDir) return;
32303
- const projectsRoot = join55(configDir, "projects");
32562
+ const projectsRoot = join56(configDir, "projects");
32304
32563
  const records = [];
32305
32564
  for (const { path: jsonlPath, isSubagent, archived } of enumerateJsonls(projectsRoot)) {
32306
32565
  if (isSubagent || archived) continue;
@@ -32441,42 +32700,22 @@ var telegramGateway = new TelegramGateway({
32441
32700
  // Task 2353 — no turn-instruction resolver on this door. See the dep comment in
32442
32701
  // telegram-gateway.ts: the account file governs the relay turn, which only the
32443
32702
  // WhatsApp door raises.
32444
- resolveSessionOverride: (accountId, senderId) => resolveChannelOverride(accountId, "telegram", senderId),
32703
+ // Task 2368 the pointer keys on the `<botId>:<senderId>` channel key, which
32704
+ // is what the classification sidecar records (the manager reads
32705
+ // telegramChannel.senderId) and therefore what session-reseat.ts writes.
32706
+ resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
32445
32707
  gatewayUrl: telegramGatewayUrl(),
32446
32708
  serverPath: telegramServerPath(),
32447
- ensureChannelSession: async ({ accountId, senderId, role, personId, chatId, gatewayUrl, serverPath }) => {
32448
- const req = buildTelegramSpawnRequest({
32449
- accountId,
32450
- senderId,
32451
- role,
32452
- personId,
32453
- gatewayUrl,
32454
- serverPath,
32455
- // Task 1789 — admin rows only; see the WhatsApp twin.
32456
- overrideSessionId: role === "admin" ? resolveChannelOverride(accountId, "telegram", senderId) : null
32457
- });
32458
- const result = await managerRcSpawn(req);
32459
- if ("error" in result) {
32460
- console.error(`[telegram-native] spawn-failed senderId=${senderId} role=${role} error=${result.error} status=${result.status}`);
32461
- return;
32462
- }
32463
- if (role !== "admin") return;
32464
- const prior = telegramFileFollowers.get(senderId);
32465
- if (prior) prior.abort();
32466
- let ac;
32467
- ac = startTelegramNativeFileFollower({
32468
- sessionId: req.sessionId,
32469
- senderId,
32470
- accountId,
32471
- chatId,
32472
- // Identity-guarded eviction: only delete if this controller is still the
32473
- // current one, so a stale self-close never removes a live follower.
32474
- onClose: () => {
32475
- if (telegramFileFollowers.get(senderId) === ac) telegramFileFollowers.delete(senderId);
32476
- }
32477
- });
32478
- telegramFileFollowers.set(senderId, ac);
32479
- }
32709
+ // Task 2368 the role branch lives in its own module so it has a test that
32710
+ // needs no live manager. An admin bot keeps /rc-spawn; a public bot goes to
32711
+ // /public-spawn, the only door that accepts an agent slug.
32712
+ ensureChannelSession: buildTelegramEnsureSession({
32713
+ managerRcSpawn,
32714
+ managerSpawn,
32715
+ resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
32716
+ startFileFollower: startTelegramNativeFileFollower,
32717
+ followers: telegramFileFollowers
32718
+ })
32480
32719
  });
32481
32720
  setTelegramGateway(telegramGateway);
32482
32721
  app76.route("/", telegramGateway.routes());
@@ -32518,24 +32757,29 @@ var scheduleInjectRoutes = createScheduleInjectRoutes({
32518
32757
  // effective-account.ts for why `resolveAccount()` was the wrong side to move,
32519
32758
  // and why the real dependencies are bound there rather than assembled here.
32520
32759
  effectiveAccountFor: resolveScheduledEffectiveAccountForInstall,
32521
- // Task 2365the account whose own telegram.adminUsers claims this chat id.
32522
- // Reads the same list, through the same readChannelAdmins call, that
32760
+ // Task 2368which BOT claims this chat id, across every account. Reads the
32761
+ // same per-bot split, through the same readChannelAdmins call, that
32523
32762
  // 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.
32763
+ // the save and this dispatch cannot disagree. Two claimants is reported to the
32764
+ // route and refused there, never picked between — and two bots in ONE account
32765
+ // is as ambiguous as two accounts, because the dispatch has no basis to choose
32766
+ // which bot the person hears from.
32526
32767
  telegramAccountForDestination: (destination) => {
32527
32768
  const asNumber = Number(destination);
32528
32769
  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
- };
32770
+ const hits = [];
32771
+ for (const a of listValidAccounts()) {
32772
+ const entries2 = listBotEntries(a.config.telegram);
32773
+ for (const bot of readChannelAdmins(a.accountDir, "telegram").byBot) {
32774
+ if (!bot.adminUsers.includes(asNumber)) continue;
32775
+ const entry = entries2.find((e) => e.id === bot.botId);
32776
+ if (!entry) continue;
32777
+ hits.push({ accountId: a.accountId, botId: bot.botId, botToken: entry.token });
32778
+ }
32779
+ }
32780
+ if (hits.length === 0) return { kind: "none" };
32781
+ if (hits.length > 1) return { kind: "ambiguous", count: hits.length };
32782
+ return { kind: "one", ...hits[0] };
32539
32783
  }
32540
32784
  });
32541
32785
  app76.route("/api/channel/schedule-inject", scheduleInjectRoutes);
@@ -32908,14 +33152,14 @@ app76.get("/agent-assets/:slug/:filename", (c) => {
32908
33152
  console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
32909
33153
  return c.text("Forbidden", 403);
32910
33154
  }
32911
- if (!existsSync51(filePath)) {
33155
+ if (!existsSync52(filePath)) {
32912
33156
  console.error(`[agent-assets] serve slug=${slug} file=${filename} status=404`);
32913
33157
  return c.text("Not found", 404);
32914
33158
  }
32915
33159
  const ext = "." + filename.split(".").pop()?.toLowerCase();
32916
33160
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
32917
33161
  console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
32918
- const body = readFileSync47(filePath);
33162
+ const body = readFileSync48(filePath);
32919
33163
  return c.body(body, 200, {
32920
33164
  "Content-Type": contentType,
32921
33165
  "Cache-Control": "public, max-age=3600"
@@ -32938,14 +33182,14 @@ app76.get("/generated/:filename", (c) => {
32938
33182
  console.error(`[generated] serve file=${filename} status=403`);
32939
33183
  return c.text("Forbidden", 403);
32940
33184
  }
32941
- if (!existsSync51(filePath)) {
33185
+ if (!existsSync52(filePath)) {
32942
33186
  console.error(`[generated] serve file=${filename} status=404`);
32943
33187
  return c.text("Not found", 404);
32944
33188
  }
32945
33189
  const ext = "." + filename.split(".").pop()?.toLowerCase();
32946
33190
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
32947
33191
  console.log(`[generated] serve file=${filename} status=200`);
32948
- const body = readFileSync47(filePath);
33192
+ const body = readFileSync48(filePath);
32949
33193
  return c.body(body, 200, {
32950
33194
  "Content-Type": contentType,
32951
33195
  "Cache-Control": "public, max-age=86400"
@@ -32958,9 +33202,9 @@ app76.route("/v", visitor_consent_default);
32958
33202
  var htmlCache = /* @__PURE__ */ new Map();
32959
33203
  var brandLogoPath = "/brand/maxy-monochrome.png";
32960
33204
  var brandIconPath = "/brand/maxy-monochrome.png";
32961
- if (BRAND_JSON_PATH && existsSync51(BRAND_JSON_PATH)) {
33205
+ if (BRAND_JSON_PATH && existsSync52(BRAND_JSON_PATH)) {
32962
33206
  try {
32963
- const fullBrand = JSON.parse(readFileSync47(BRAND_JSON_PATH, "utf-8"));
33207
+ const fullBrand = JSON.parse(readFileSync48(BRAND_JSON_PATH, "utf-8"));
32964
33208
  if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
32965
33209
  brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
32966
33210
  } catch {
@@ -32987,11 +33231,11 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
32987
33231
  var brandThemeColor = BRAND.defaultColors?.primary ?? "#000000";
32988
33232
  var brandBackgroundColor = BRAND.defaultColors?.background ?? "#ffffff";
32989
33233
  var brandAppIconsExist = [brandAppIcon192Path, brandAppIcon512Path, brandMaskableIconPath].every(
32990
- (p) => existsSync51(resolve45(process.cwd(), "public", p.replace(/^\//, "")))
33234
+ (p) => existsSync52(resolve45(process.cwd(), "public", p.replace(/^\//, "")))
32991
33235
  );
32992
33236
  var SW_SOURCE = (() => {
32993
33237
  try {
32994
- return readFileSync47(resolve45(process.cwd(), "public", "sw.js"), "utf-8");
33238
+ return readFileSync48(resolve45(process.cwd(), "public", "sw.js"), "utf-8");
32995
33239
  } catch {
32996
33240
  return null;
32997
33241
  }
@@ -32999,9 +33243,9 @@ var SW_SOURCE = (() => {
32999
33243
  function readInstalledVersion() {
33000
33244
  try {
33001
33245
  if (!PLATFORM_ROOT6) return "unknown";
33002
- const versionFile = join55(PLATFORM_ROOT6, "config", `.${BRAND.hostname}-version`);
33003
- if (!existsSync51(versionFile)) return "unknown";
33004
- const content = readFileSync47(versionFile, "utf-8").trim();
33246
+ const versionFile = join56(PLATFORM_ROOT6, "config", `.${BRAND.hostname}-version`);
33247
+ if (!existsSync52(versionFile)) return "unknown";
33248
+ const content = readFileSync48(versionFile, "utf-8").trim();
33005
33249
  return content || "unknown";
33006
33250
  } catch {
33007
33251
  return "unknown";
@@ -33012,7 +33256,7 @@ var versionScript = `<script>window.__MAXY_VERSION__=${JSON.stringify(INSTALLED_
33012
33256
  function cachedHtml(file) {
33013
33257
  let html = htmlCache.get(file);
33014
33258
  if (!html) {
33015
- html = readFileSync47(resolve45(process.cwd(), "public", file), "utf-8");
33259
+ html = readFileSync48(resolve45(process.cwd(), "public", file), "utf-8");
33016
33260
  const productNameEsc = escapeHtml(BRAND.productName);
33017
33261
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
33018
33262
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -33029,14 +33273,14 @@ ${clientErrorReporterScript}
33029
33273
  return html;
33030
33274
  }
33031
33275
  function loadBrandingCache(agentSlug) {
33032
- const configDir = join55(homedir4(), BRAND.configDir);
33276
+ const configDir = join56(homedir4(), BRAND.configDir);
33033
33277
  try {
33034
33278
  const verdict = resolvePublicAddressFromDisk(agentSlug);
33035
33279
  const accountId = verdict.kind === "served" ? verdict.owner.accountId : getDefaultAccountId();
33036
33280
  if (!accountId) return null;
33037
- const cachePath = join55(configDir, "branding-cache", accountId, `${agentSlug}.json`);
33038
- if (!existsSync51(cachePath)) return null;
33039
- return JSON.parse(readFileSync47(cachePath, "utf-8"));
33281
+ const cachePath = join56(configDir, "branding-cache", accountId, `${agentSlug}.json`);
33282
+ if (!existsSync52(cachePath)) return null;
33283
+ return JSON.parse(readFileSync48(cachePath, "utf-8"));
33040
33284
  } catch {
33041
33285
  return null;
33042
33286
  }
@@ -33131,7 +33375,7 @@ app76.use("/vnc-popout.html", logViewerFetch);
33131
33375
  app76.get("/vnc-popout.html", (c) => {
33132
33376
  let html = htmlCache.get("vnc-popout.html");
33133
33377
  if (!html) {
33134
- html = readFileSync47(resolve45(process.cwd(), "public", "vnc-popout.html"), "utf-8");
33378
+ html = readFileSync48(resolve45(process.cwd(), "public", "vnc-popout.html"), "utf-8");
33135
33379
  const name = escapeHtml(BRAND.productName);
33136
33380
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
33137
33381
  html = html.replace("</head>", ` ${brandScript}
@@ -33284,10 +33528,10 @@ var hostname = process.env.HOSTNAME ?? "127.0.0.1";
33284
33528
  var httpServer = serve({ fetch: app76.fetch, port, hostname });
33285
33529
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33286
33530
  {
33287
- const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33531
+ const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, "..");
33288
33532
  const reconcileScript = resolve45(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
33289
33533
  const runReconcile = (ctx) => {
33290
- if (!existsSync51(reconcileScript)) return;
33534
+ if (!existsSync52(reconcileScript)) return;
33291
33535
  try {
33292
33536
  const child = spawn3(process.execPath, [reconcileScript], {
33293
33537
  env: { ...process.env, PLATFORM_ROOT: reconcilePlatformRoot },
@@ -33307,7 +33551,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33307
33551
  detached: true,
33308
33552
  run: runReconcile
33309
33553
  });
33310
- const reconcileStatePath = join55(MAXY_DIR, "booking-reconcile-state.json");
33554
+ const reconcileStatePath = join56(MAXY_DIR, "booking-reconcile-state.json");
33311
33555
  registerLoop({
33312
33556
  name: "calendar-reconcile-liveness",
33313
33557
  intervalMs: 6e5,
@@ -33325,7 +33569,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33325
33569
  }
33326
33570
  {
33327
33571
  const DREAM_CYCLE_SWEEP_INTERVAL_MS = 36e5;
33328
- const dreamPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33572
+ const dreamPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, "..");
33329
33573
  const dreamSweepScript = resolve45(dreamPlatformRoot, "plugins/memory/mcp/dist/scripts/dream-cycle-sweep.js");
33330
33574
  registerLoop({
33331
33575
  name: "dream-cycle-sweep",
@@ -33333,7 +33577,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33333
33577
  firstRunDelayMs: 3e4,
33334
33578
  detached: true,
33335
33579
  run: (ctx) => {
33336
- if (!existsSync51(dreamSweepScript)) return;
33580
+ if (!existsSync52(dreamSweepScript)) return;
33337
33581
  try {
33338
33582
  const child = spawn3(process.execPath, [dreamSweepScript], {
33339
33583
  env: { ...process.env, PLATFORM_ROOT: dreamPlatformRoot },
@@ -33349,11 +33593,11 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33349
33593
  });
33350
33594
  }
33351
33595
  {
33352
- const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33596
+ const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, "..");
33353
33597
  const outlookScript = resolve45(outlookPlatformRoot, "plugins/outlook/mcp/dist/scripts/complete-registration.js");
33354
33598
  const OUTLOOK_COMPLETE_INTERVAL_MS = 3e4;
33355
33599
  const runOutlookComplete = (ctx) => {
33356
- if (!existsSync51(outlookScript)) return;
33600
+ if (!existsSync52(outlookScript)) return;
33357
33601
  try {
33358
33602
  const child = spawn3(process.execPath, [outlookScript], {
33359
33603
  env: { ...process.env, PLATFORM_ROOT: outlookPlatformRoot },
@@ -33375,19 +33619,19 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33375
33619
  });
33376
33620
  }
33377
33621
  {
33378
- const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33622
+ const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, "..");
33379
33623
  const strandedAccountsDir = resolve45(auditRoot, "..", "data/accounts");
33380
33624
  const STRANDED_AUDIT_INTERVAL_MS = 3e5;
33381
33625
  const STRANDED_AGE_MS = 16 * 6e4;
33382
33626
  const STRANDED_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
33383
33627
  const runStrandedAudit = () => {
33384
33628
  try {
33385
- if (!existsSync51(strandedAccountsDir)) return;
33629
+ if (!existsSync52(strandedAccountsDir)) return;
33386
33630
  const now = Date.now();
33387
33631
  for (const name of readdirSync30(strandedAccountsDir)) {
33388
33632
  if (!STRANDED_UUID_RE.test(name)) continue;
33389
33633
  const pendingPath2 = resolve45(strandedAccountsDir, name, "secrets/outlook/pending-devicecode.enc");
33390
- if (!existsSync51(pendingPath2)) continue;
33634
+ if (!existsSync52(pendingPath2)) continue;
33391
33635
  const ageMs2 = now - statSync27(pendingPath2).mtimeMs;
33392
33636
  if (ageMs2 > STRANDED_AGE_MS) {
33393
33637
  console.error(`[outlook-mcp] devicecode-stranded account=${name} ageSec=${Math.floor(ageMs2 / 1e3)}`);
@@ -33405,7 +33649,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33405
33649
  });
33406
33650
  }
33407
33651
  {
33408
- const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33652
+ const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, "..");
33409
33653
  const googleAccountsDir = resolve45(googleRoot, "..", "data/accounts");
33410
33654
  const GOOGLE_PENDING_AUDIT_INTERVAL_MS = 3e5;
33411
33655
  const runGooglePendingAuditSafe = () => {
@@ -33424,7 +33668,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33424
33668
  const googleAuditScript = resolve45(googleRoot, "plugins/google/mcp/dist/scripts/account-audit.js");
33425
33669
  const GOOGLE_ACCOUNT_AUDIT_INTERVAL_MS = 36e5;
33426
33670
  const runGoogleAccountAudit = (ctx) => {
33427
- if (!existsSync51(googleAuditScript)) return;
33671
+ if (!existsSync52(googleAuditScript)) return;
33428
33672
  try {
33429
33673
  const child = spawn3(process.execPath, [googleAuditScript], {
33430
33674
  env: { ...process.env, PLATFORM_ROOT: googleRoot },
@@ -33449,7 +33693,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33449
33693
  });
33450
33694
  }
33451
33695
  {
33452
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33696
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, "..");
33453
33697
  const STORAGE_AUDIT_INTERVAL_MS = 3e5;
33454
33698
  const runStorageAuditSafe = () => runStorageAudit(auditPlatformRoot).catch((err) => {
33455
33699
  console.error(`[storage-audit] error="${err.message}"`);
@@ -33481,15 +33725,42 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33481
33725
  firstRunDelayMs: 5e4,
33482
33726
  run: runDataPortalAuditSafe
33483
33727
  });
33728
+ for (const o of migrateTelegramTwoSlot({
33729
+ accounts: listValidAccounts().map((a) => ({ accountId: a.accountId, accountDir: a.accountDir })),
33730
+ secretPathFor: secretPath,
33731
+ legacySecretPathFor: (botType, accountId) => resolve45(
33732
+ homedir4(),
33733
+ configDirName(),
33734
+ `${botType === "admin" ? ".telegram-admin-webhook-secret" : ".telegram-webhook-secret"}.${accountId}`
33735
+ ),
33736
+ // Counted here and nowhere else: the session-key re-key strands these and no
33737
+ // later event reveals it, because the new key never resolves to the old JSONL.
33738
+ countOrphanSessions: countTelegramSessionsForAccount
33739
+ })) {
33740
+ console.error(
33741
+ `[telegram-audit] op=migrate-session-orphans accountId=${o.accountId} count=${o.orphanSessions} converted=${o.converted}${o.skippedAdmin ? ` skippedAdmin=${o.skippedAdmin}` : ""}${o.skippedPublic ? ` skippedPublic=${o.skippedPublic}` : ""}`
33742
+ );
33743
+ }
33484
33744
  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);
33745
+ const accounts = listValidAccounts().map((a) => ({
33746
+ accountId: a.accountId,
33747
+ accountDir: a.accountDir,
33748
+ telegram: a.config.telegram
33749
+ }));
33750
+ const configured = accounts.some((a) => listBotEntries(a.telegram).length > 0);
33487
33751
  if (!configured) return;
33488
- const lastByAccount = newestTelegramSessionByAccount();
33752
+ const lastByBot = newestTelegramSessionByBot();
33753
+ let secretFilesOnDisk = [];
33754
+ try {
33755
+ secretFilesOnDisk = readdirSync30(telegramSecretsDir());
33756
+ } catch {
33757
+ }
33489
33758
  const lines = buildTelegramAuditLines({
33490
33759
  accounts,
33491
- secretFileExists: (botType, accountId) => existsSync51(secretPath(botType, accountId)),
33492
- lastInboundAt: (accountId) => lastByAccount.get(accountId) ?? null,
33760
+ secretFileExists: (botId) => existsSync52(secretPath(botId)),
33761
+ agentActive: isActiveAgentSlug,
33762
+ secretFilesOnDisk,
33763
+ lastInboundByBot: (botId) => lastByBot.get(botId) ?? null,
33493
33764
  now: Date.now()
33494
33765
  });
33495
33766
  for (const line of lines) console.error(line);
@@ -33565,7 +33836,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33565
33836
  });
33566
33837
  }
33567
33838
  {
33568
- const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33839
+ const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, "..");
33569
33840
  const publishScript = resolve45(publishPlatformRoot, "plugins/scheduling/mcp/dist/scripts/publish-availability.js");
33570
33841
  const PUBLISH_INTERVAL_MS = 3e5;
33571
33842
  const bookingAccounts = () => {
@@ -33577,7 +33848,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33577
33848
  }
33578
33849
  };
33579
33850
  const spawnPublish = (account, ctx) => {
33580
- if (!existsSync51(publishScript)) return;
33851
+ if (!existsSync52(publishScript)) return;
33581
33852
  try {
33582
33853
  const child = spawn3(process.execPath, [publishScript], {
33583
33854
  env: {
@@ -33599,8 +33870,8 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33599
33870
  const availPath = resolve45(account.accountDir, "calendar-availability.json");
33600
33871
  let hasBookingSite = false;
33601
33872
  try {
33602
- if (existsSync51(availPath)) {
33603
- const cfg = JSON.parse(readFileSync47(availPath, "utf-8"));
33873
+ if (existsSync52(availPath)) {
33874
+ const cfg = JSON.parse(readFileSync48(availPath, "utf-8"));
33604
33875
  hasBookingSite = typeof cfg.bookingDbName === "string" && cfg.bookingDbName.length > 0;
33605
33876
  }
33606
33877
  } catch {
@@ -33608,9 +33879,9 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33608
33879
  if (!hasBookingSite) return;
33609
33880
  const statePath = resolve45(account.accountDir, "state", "booking-availability", "last-publish.json");
33610
33881
  let lastSuccessAt = null;
33611
- if (existsSync51(statePath)) {
33882
+ if (existsSync52(statePath)) {
33612
33883
  try {
33613
- const rec = JSON.parse(readFileSync47(statePath, "utf-8"));
33884
+ const rec = JSON.parse(readFileSync48(statePath, "utf-8"));
33614
33885
  lastSuccessAt = rec.lastSuccessAt ?? null;
33615
33886
  } catch {
33616
33887
  console.error(`[calendar-availability] op=audit accountId=${account.accountId} snapshotAgeMs=na reason=bad-state-file`);
@@ -33642,12 +33913,12 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
33642
33913
  startTimeEntryCensus(() => getSession());
33643
33914
  startLedgerCensus(() => getSession());
33644
33915
  {
33645
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..");
33916
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, "..");
33646
33917
  const auditScript = resolve45(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
33647
33918
  const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
33648
33919
  const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
33649
33920
  const runConnectorAudit = (ctx) => {
33650
- if (!existsSync51(auditScript)) return;
33921
+ if (!existsSync52(auditScript)) return;
33651
33922
  try {
33652
33923
  const child = spawn3(process.execPath, [auditScript], {
33653
33924
  env: { ...process.env, PLATFORM_ROOT: auditPlatformRoot, LOG_DIR: auditLogDir },
@@ -33767,11 +34038,11 @@ try {
33767
34038
  var ADMINUSER_RECONCILE_INTERVAL_MS = 60 * 60 * 1e3;
33768
34039
  async function runAdminUserReconcileTick() {
33769
34040
  try {
33770
- if (!existsSync51(USERS_FILE)) {
34041
+ if (!existsSync52(USERS_FILE)) {
33771
34042
  console.error("[adminuser-self-heal] skip reason=no-users-file");
33772
34043
  return;
33773
34044
  }
33774
- const usersRaw = readFileSync47(USERS_FILE, "utf-8").trim();
34045
+ const usersRaw = readFileSync48(USERS_FILE, "utf-8").trim();
33775
34046
  if (!usersRaw) {
33776
34047
  console.error("[adminuser-self-heal] skip reason=empty-users-file");
33777
34048
  return;
@@ -33853,8 +34124,8 @@ var runIngestAuditTick = () => {
33853
34124
  },
33854
34125
  agentExists: (accountId, slug) => {
33855
34126
  if (!house) return false;
33856
- const dir = isHouseAccountKey(house.accountId, accountId) ? house.accountDir : join55(ACCOUNTS_DIR, accountId);
33857
- return existsSync51(join55(dir, "agents", slug, "config.json"));
34127
+ const dir = isHouseAccountKey(house.accountId, accountId) ? house.accountDir : join56(ACCOUNTS_DIR, accountId);
34128
+ return existsSync52(join56(dir, "agents", slug, "config.json"));
33858
34129
  }
33859
34130
  });
33860
34131
  } catch (err) {
@@ -33902,8 +34173,8 @@ registerLoop({
33902
34173
  runPublicAddressCollisionAudit({
33903
34174
  houseAccountId: () => resolveAccount()?.accountId ?? null,
33904
34175
  activeSlugs: () => listValidAccounts().map((a) => {
33905
- const agentsRoot = join55(a.accountDir, "agents");
33906
- if (!existsSync51(agentsRoot)) return { accountId: a.accountId, slugs: [] };
34176
+ const agentsRoot = join56(a.accountDir, "agents");
34177
+ if (!existsSync52(agentsRoot)) return { accountId: a.accountId, slugs: [] };
33907
34178
  const slugs = readdirSync30(agentsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((slug) => isActiveAgentSlug(a.accountDir, slug));
33908
34179
  return { accountId: a.accountId, slugs };
33909
34180
  })
@@ -33917,8 +34188,8 @@ registerLoop({
33917
34188
  });
33918
34189
  var USERS_RECONCILE_INTERVAL_MS = 60 * 60 * 1e3;
33919
34190
  function countUsersRows() {
33920
- if (!existsSync51(USERS_FILE)) return 0;
33921
- const raw = readFileSync47(USERS_FILE, "utf-8").trim();
34191
+ if (!existsSync52(USERS_FILE)) return 0;
34192
+ const raw = readFileSync48(USERS_FILE, "utf-8").trim();
33922
34193
  if (!raw) return 0;
33923
34194
  const users = JSON.parse(raw);
33924
34195
  return users.filter((u) => typeof u.userId === "string").length;
@@ -34054,7 +34325,7 @@ if (bootAccountConfig?.whatsapp) {
34054
34325
  }
34055
34326
  init({
34056
34327
  configDir: configDirForWhatsApp,
34057
- platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join55(__dirname, "..")),
34328
+ platformRoot: resolve45(process.env.MAXY_PLATFORM_ROOT ?? join56(__dirname, "..")),
34058
34329
  accountConfig: bootAccountConfig,
34059
34330
  // Task 2074 — enrich with the sub-account name and fan out to every open
34060
34331
  // admin SSE stream. Fire-and-forget: the manager's persist path must not