@rubytech/create-maxy-code 0.1.353 → 0.1.354

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 (45) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/admin-access-password/__tests__/index.test.ts +96 -14
  3. package/payload/platform/lib/admin-access-password/dist/index.d.ts +20 -4
  4. package/payload/platform/lib/admin-access-password/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/admin-access-password/dist/index.js +43 -4
  6. package/payload/platform/lib/admin-access-password/dist/index.js.map +1 -1
  7. package/payload/platform/lib/admin-access-password/src/index.ts +67 -4
  8. package/payload/platform/lib/admins-write/__tests__/index.test.ts +182 -0
  9. package/payload/platform/lib/admins-write/dist/index.d.ts +65 -11
  10. package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
  11. package/payload/platform/lib/admins-write/dist/index.js +93 -31
  12. package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
  13. package/payload/platform/lib/admins-write/src/index.ts +133 -36
  14. package/payload/platform/plugins/admin/PLUGIN.md +4 -0
  15. package/payload/platform/plugins/admin/mcp/dist/index.js +15 -3
  16. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  17. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +16 -1
  18. package/payload/platform/plugins/docs/references/troubleshooting.md +15 -0
  19. package/payload/server/{chunk-QAKVFSEJ.js → chunk-7GNXBVGY.js} +38 -11
  20. package/payload/server/maxy-edge.js +1 -1
  21. package/payload/server/public/assets/{AdminLoginScreens-CzdOQKyO.js → AdminLoginScreens-6WgO-Z3m.js} +1 -1
  22. package/payload/server/public/assets/{AdminShell-CC-CuN6Y.js → AdminShell-GGnPXCOe.js} +1 -1
  23. package/payload/server/public/assets/{Checkbox-sQBc9xSy.js → Checkbox-D3XFVUMT.js} +1 -1
  24. package/payload/server/public/assets/{OperatorConversations-DPVKGsRS.css → OperatorConversations-CFWXdnjB.css} +1 -1
  25. package/payload/server/public/assets/{admin-qW6jM6l0.js → admin-B2yNhVvH.js} +1 -1
  26. package/payload/server/public/assets/{browser-BsW6KaxZ.js → browser-ByaQ0ItQ.js} +1 -1
  27. package/payload/server/public/assets/{calendar-BPAiN1cL.js → calendar-BjqCT8cW.js} +1 -1
  28. package/payload/server/public/assets/chat-kl-Ips-o.js +1 -0
  29. package/payload/server/public/assets/{data-D_cR_NCZ.js → data-p9p-181v.js} +1 -1
  30. package/payload/server/public/assets/{graph-CmpAy2WP.js → graph-BuunC8eP.js} +1 -1
  31. package/payload/server/public/assets/{graph-labels-C0SLq_VP.js → graph-labels-BF3Ak8q0.js} +1 -1
  32. package/payload/server/public/assets/{operator-CrjtJcEY.js → operator-D7c8mN4J.js} +1 -1
  33. package/payload/server/public/assets/{page-Cf3fVDSo.js → page-C6uMZdT1.js} +3 -3
  34. package/payload/server/public/assets/{public-BkNtvSYp.js → public-B9_UhjEa.js} +1 -1
  35. package/payload/server/public/browser.html +4 -4
  36. package/payload/server/public/calendar.html +4 -4
  37. package/payload/server/public/chat.html +5 -5
  38. package/payload/server/public/data.html +4 -4
  39. package/payload/server/public/graph.html +6 -6
  40. package/payload/server/public/index.html +6 -6
  41. package/payload/server/public/operator.html +7 -7
  42. package/payload/server/public/public.html +5 -5
  43. package/payload/server/server.js +1010 -966
  44. package/payload/server/public/assets/chat-DVD9yoVL.js +0 -1
  45. /package/payload/server/public/assets/{OperatorConversations-CA_2c3sp.js → OperatorConversations-DeZ2wDEK.js} +0 -0
@@ -21,6 +21,7 @@ import {
21
21
  REMOTE_PASSWORD_FILE,
22
22
  RFB_PORT,
23
23
  SYSTEM_PATH_SLUGS,
24
+ USERS_AUDIT_LOG,
24
25
  USERS_FILE,
25
26
  VISITOR_TOKEN_SECRET_FILE,
26
27
  VNC_DISPLAY,
@@ -105,7 +106,7 @@ import {
105
106
  vncLog,
106
107
  walkPremiumBundles,
107
108
  writeAdminUserAndPerson
108
- } from "./chunk-QAKVFSEJ.js";
109
+ } from "./chunk-7GNXBVGY.js";
109
110
  import {
110
111
  __commonJS,
111
112
  __toESM
@@ -361,9 +362,9 @@ var require_dist = __commonJS({
361
362
  try {
362
363
  await onEmpty(c);
363
364
  } catch (err) {
364
- const id8 = c.elementId.slice(0, 8);
365
+ const id82 = c.elementId.slice(0, 8);
365
366
  const message = err instanceof Error ? err.message : String(err);
366
- process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${id8} labels=${c.labels.join(",")} message=${message}
367
+ process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${id82} labels=${c.labels.join(",")} message=${message}
367
368
  `);
368
369
  continue;
369
370
  }
@@ -1303,7 +1304,7 @@ var serveStatic = (options = { root: "" }) => {
1303
1304
  // server/index.ts
1304
1305
  import { readFileSync as readFileSync33, existsSync as existsSync32, watchFile } from "fs";
1305
1306
  import { spawn as spawn3 } from "child_process";
1306
- import { resolve as resolve34, join as join32, basename as basename10 } from "path";
1307
+ import { resolve as resolve32, join as join32, basename as basename10 } from "path";
1307
1308
  import { homedir as homedir3 } from "os";
1308
1309
  import { monitorEventLoopDelay } from "perf_hooks";
1309
1310
 
@@ -1961,7 +1962,7 @@ var credsSaveQueue = Promise.resolve();
1961
1962
  async function drainCredsSaveQueue(timeoutMs = 5e3) {
1962
1963
  console.error(`${TAG2} draining credential save queue\u2026`);
1963
1964
  const timer2 = new Promise(
1964
- (resolve35) => setTimeout(() => resolve35("timeout"), timeoutMs)
1965
+ (resolve33) => setTimeout(() => resolve33("timeout"), timeoutMs)
1965
1966
  );
1966
1967
  const result = await Promise.race([
1967
1968
  credsSaveQueue.then(() => "drained"),
@@ -2089,11 +2090,11 @@ async function createWaSocket(opts) {
2089
2090
  return sock;
2090
2091
  }
2091
2092
  async function waitForConnection(sock) {
2092
- return new Promise((resolve35, reject) => {
2093
+ return new Promise((resolve33, reject) => {
2093
2094
  const handler = (update) => {
2094
2095
  if (update.connection === "open") {
2095
2096
  sock.ev.off("connection.update", handler);
2096
- resolve35();
2097
+ resolve33();
2097
2098
  }
2098
2099
  if (update.connection === "close") {
2099
2100
  sock.ev.off("connection.update", handler);
@@ -2207,14 +2208,14 @@ ${inspected}`;
2207
2208
  return inspect2(err, INSPECT_OPTS2);
2208
2209
  }
2209
2210
  function withTimeout(label, promise, timeoutMs) {
2210
- return new Promise((resolve35, reject) => {
2211
+ return new Promise((resolve33, reject) => {
2211
2212
  const timer2 = setTimeout(() => {
2212
2213
  reject(new Error(`${label} timed out after ${timeoutMs}ms`));
2213
2214
  }, timeoutMs);
2214
2215
  promise.then(
2215
2216
  (value) => {
2216
2217
  clearTimeout(timer2);
2217
- resolve35(value);
2218
+ resolve33(value);
2218
2219
  },
2219
2220
  (err) => {
2220
2221
  clearTimeout(timer2);
@@ -3098,8 +3099,8 @@ async function persistWhatsAppMessage(input) {
3098
3099
  const { givenName, familyName } = splitName(input.pushName);
3099
3100
  const prev = sessionWriteLocks.get(input.cacheKey);
3100
3101
  let release;
3101
- const mine = new Promise((resolve35) => {
3102
- release = resolve35;
3102
+ const mine = new Promise((resolve33) => {
3103
+ release = resolve33;
3103
3104
  });
3104
3105
  const chained = (prev ?? Promise.resolve()).then(() => mine);
3105
3106
  sessionWriteLocks.set(input.cacheKey, chained);
@@ -4209,11 +4210,11 @@ async function connectWithReconnect(conn) {
4209
4210
  console.error(
4210
4211
  `${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
4211
4212
  );
4212
- await new Promise((resolve35) => {
4213
- const timer2 = setTimeout(resolve35, delay);
4213
+ await new Promise((resolve33) => {
4214
+ const timer2 = setTimeout(resolve33, delay);
4214
4215
  conn.abortController.signal.addEventListener("abort", () => {
4215
4216
  clearTimeout(timer2);
4216
- resolve35();
4217
+ resolve33();
4217
4218
  }, { once: true });
4218
4219
  });
4219
4220
  }
@@ -4221,16 +4222,16 @@ async function connectWithReconnect(conn) {
4221
4222
  }
4222
4223
  }
4223
4224
  function waitForDisconnectEvent(conn) {
4224
- return new Promise((resolve35) => {
4225
+ return new Promise((resolve33) => {
4225
4226
  if (!conn.sock) {
4226
- resolve35();
4227
+ resolve33();
4227
4228
  return;
4228
4229
  }
4229
4230
  const sock = conn.sock;
4230
4231
  const handler = (update) => {
4231
4232
  if (update.connection === "close") {
4232
4233
  sock.ev.off("connection.update", handler);
4233
- resolve35();
4234
+ resolve33();
4234
4235
  }
4235
4236
  };
4236
4237
  sock.ev.on("connection.update", handler);
@@ -4490,8 +4491,8 @@ async function handleInboundMessage(conn, msg) {
4490
4491
  const conversationKey = isGroup ? remoteJid : senderPhone;
4491
4492
  const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
4492
4493
  let resolvePending;
4493
- const sttPending = new Promise((resolve35) => {
4494
- resolvePending = resolve35;
4494
+ const sttPending = new Promise((resolve33) => {
4495
+ resolvePending = resolve33;
4495
4496
  });
4496
4497
  if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
4497
4498
  try {
@@ -4891,20 +4892,20 @@ function buildX11Env(chromiumWrapperPath, transport = "vnc") {
4891
4892
 
4892
4893
  // server/routes/health.ts
4893
4894
  function checkPort(port2, timeoutMs = 500) {
4894
- return new Promise((resolve35) => {
4895
+ return new Promise((resolve33) => {
4895
4896
  const socket = createConnection2(port2, "127.0.0.1");
4896
4897
  socket.setTimeout(timeoutMs);
4897
4898
  socket.once("connect", () => {
4898
4899
  socket.destroy();
4899
- resolve35(true);
4900
+ resolve33(true);
4900
4901
  });
4901
4902
  socket.once("error", () => {
4902
4903
  socket.destroy();
4903
- resolve35(false);
4904
+ resolve33(false);
4904
4905
  });
4905
4906
  socket.once("timeout", () => {
4906
4907
  socket.destroy();
4907
- resolve35(false);
4908
+ resolve33(false);
4908
4909
  });
4909
4910
  });
4910
4911
  }
@@ -6044,8 +6045,8 @@ async function startLogin(opts) {
6044
6045
  await clearAuth(authDir);
6045
6046
  let resolveCode = null;
6046
6047
  let rejectCode = null;
6047
- const codePromise = new Promise((resolve35, reject) => {
6048
- resolveCode = resolve35;
6048
+ const codePromise = new Promise((resolve33, reject) => {
6049
+ resolveCode = resolve33;
6049
6050
  rejectCode = reject;
6050
6051
  });
6051
6052
  const codeTimer = setTimeout(
@@ -9471,8 +9472,31 @@ import { openSync as openSync3, closeSync as closeSync3, writeFileSync as writeF
9471
9472
  import { createHash as createHash3, randomUUID as randomUUID7 } from "crypto";
9472
9473
 
9473
9474
  // ../lib/admins-write/src/index.ts
9474
- import { existsSync as existsSync13, readFileSync as readFileSync21, writeFileSync as writeFileSync9, renameSync as renameSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync12, statSync as statSync7 } from "fs";
9475
+ import { existsSync as existsSync13, readFileSync as readFileSync21, writeFileSync as writeFileSync9, renameSync as renameSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync12, statSync as statSync7, appendFileSync } from "fs";
9475
9476
  import { dirname as dirname4, join as join20 } from "path";
9477
+ function id8(value) {
9478
+ return value.slice(0, 8);
9479
+ }
9480
+ function formatAuditRowIds(userIds) {
9481
+ return userIds.map(id8).join(",");
9482
+ }
9483
+ function appendUsersAuditLine(audit, fields) {
9484
+ const sess = audit.session ? ` session=${id8(audit.session)}` : "";
9485
+ const actor = audit.actor.includes("-") ? id8(audit.actor) : audit.actor;
9486
+ const line = `[users-audit] action=${fields.action} actor=${actor}${sess} field=${fields.field} rowsBefore=${fields.rowsBefore} rowsAfter=${fields.rowsAfter} ts=${(/* @__PURE__ */ new Date()).toISOString()}
9487
+ `;
9488
+ try {
9489
+ mkdirSync4(dirname4(audit.logFile), { recursive: true, mode: 448 });
9490
+ appendFileSync(audit.logFile, line, { mode: 384 });
9491
+ } catch (err) {
9492
+ console.error(
9493
+ `[users-audit] op=write-failed logFile=${audit.logFile} error=${err instanceof Error ? err.message : String(err)}`
9494
+ );
9495
+ }
9496
+ }
9497
+ function logUsersAudit(audit, fields) {
9498
+ appendUsersAuditLine(audit, fields);
9499
+ }
9476
9500
  function logLine(input, result) {
9477
9501
  const userIdShort = input.userId.slice(0, 8);
9478
9502
  console.error(
@@ -9499,6 +9523,7 @@ function writeAdminEntry(input) {
9499
9523
  users = parsed;
9500
9524
  }
9501
9525
  }
9526
+ const rowsBefore = users.map((u) => id8(u.userId)).join(",");
9502
9527
  const existing = users.findIndex((u) => u.userId === input.userId);
9503
9528
  if (existing === -1) {
9504
9529
  users.push({ userId: input.userId, pin: input.pin });
@@ -9507,6 +9532,12 @@ function writeAdminEntry(input) {
9507
9532
  }
9508
9533
  writeFileAtomic(input.usersFile, JSON.stringify(users, null, 2) + "\n", 384);
9509
9534
  result.usersJsonResult = "ok";
9535
+ appendUsersAuditLine(input.audit, {
9536
+ action: existing === -1 ? "add" : "set-pin",
9537
+ field: existing === -1 ? "row" : "pin",
9538
+ rowsBefore,
9539
+ rowsAfter: users.map((u) => id8(u.userId)).join(",")
9540
+ });
9510
9541
  } catch (err) {
9511
9542
  result.usersJsonResult = "fail";
9512
9543
  result.usersError = err instanceof Error ? err.message : String(err);
@@ -9535,12 +9566,8 @@ function writeAdminEntry(input) {
9535
9566
  logLine(input, result);
9536
9567
  return result;
9537
9568
  }
9538
- function checkAdminAuthInvariant(input) {
9539
- const result = {
9540
- divergences: 0,
9541
- accountWithoutUsers: [],
9542
- usersWithoutAccount: []
9543
- };
9569
+ function computeAdminStoreDivergence(input) {
9570
+ const result = { divergences: 0, accountWithoutUsers: [], usersWithoutAccount: [], errors: [] };
9544
9571
  const usersUserIds = /* @__PURE__ */ new Set();
9545
9572
  if (existsSync13(input.usersFile)) {
9546
9573
  try {
@@ -9552,8 +9579,7 @@ function checkAdminAuthInvariant(input) {
9552
9579
  }
9553
9580
  }
9554
9581
  } catch (err) {
9555
- const msg = err instanceof Error ? err.message : String(err);
9556
- console.error(`[${input.tag}] users.json unreadable usersFile=${input.usersFile} error=${msg}`);
9582
+ result.errors.push({ source: input.usersFile, detail: err instanceof Error ? err.message : String(err) });
9557
9583
  }
9558
9584
  }
9559
9585
  const accountUserIds = /* @__PURE__ */ new Set();
@@ -9562,9 +9588,7 @@ function checkAdminAuthInvariant(input) {
9562
9588
  try {
9563
9589
  entries = readdirSync12(input.accountsDir);
9564
9590
  } catch (err) {
9565
- const msg = err instanceof Error ? err.message : String(err);
9566
- console.error(`[${input.tag}] accounts-dir unreadable accountsDir=${input.accountsDir} error=${msg}`);
9567
- console.error(`[${input.tag}] check complete divergences=0 (accounts-dir unreadable)`);
9591
+ result.errors.push({ source: input.accountsDir, detail: err instanceof Error ? err.message : String(err) });
9568
9592
  return result;
9569
9593
  }
9570
9594
  for (const entry of entries) {
@@ -9582,18 +9606,13 @@ function checkAdminAuthInvariant(input) {
9582
9606
  const config = JSON.parse(readFileSync21(accountJsonPath, "utf-8"));
9583
9607
  admins = config.admins ?? [];
9584
9608
  } catch (err) {
9585
- const msg = err instanceof Error ? err.message : String(err);
9586
- console.error(`[${input.tag}] account.json unreadable source=${accountJsonPath} error=${msg}`);
9609
+ result.errors.push({ source: accountJsonPath, detail: err instanceof Error ? err.message : String(err) });
9587
9610
  continue;
9588
9611
  }
9589
9612
  for (const a of admins) {
9590
9613
  if (typeof a.userId !== "string") continue;
9591
9614
  accountUserIds.add(a.userId);
9592
9615
  if (!usersUserIds.has(a.userId)) {
9593
- const userIdShort = a.userId.slice(0, 8);
9594
- console.error(
9595
- `[${input.tag}] direction=account-without-users userId=${userIdShort} source=${accountJsonPath}`
9596
- );
9597
9616
  result.accountWithoutUsers.push({ userId: a.userId, source: accountJsonPath });
9598
9617
  result.divergences += 1;
9599
9618
  }
@@ -9602,17 +9621,30 @@ function checkAdminAuthInvariant(input) {
9602
9621
  }
9603
9622
  for (const uid of usersUserIds) {
9604
9623
  if (!accountUserIds.has(uid)) {
9605
- const userIdShort = uid.slice(0, 8);
9606
- console.error(
9607
- `[${input.tag}] direction=users-without-account userId=${userIdShort} source=${input.usersFile}`
9608
- );
9609
9624
  result.usersWithoutAccount.push({ userId: uid });
9610
9625
  result.divergences += 1;
9611
9626
  }
9612
9627
  }
9613
- console.error(`[${input.tag}] check complete divergences=${result.divergences}`);
9614
9628
  return result;
9615
9629
  }
9630
+ function checkAdminAuthInvariant(input) {
9631
+ const d = computeAdminStoreDivergence({ usersFile: input.usersFile, accountsDir: input.accountsDir });
9632
+ for (const e of d.errors) {
9633
+ console.error(`[${input.tag}] unreadable source=${e.source} error=${e.detail}`);
9634
+ }
9635
+ for (const a of d.accountWithoutUsers) {
9636
+ console.error(`[${input.tag}] direction=account-without-users userId=${a.userId.slice(0, 8)} source=${a.source}`);
9637
+ }
9638
+ for (const u of d.usersWithoutAccount) {
9639
+ console.error(`[${input.tag}] direction=users-without-account userId=${u.userId.slice(0, 8)} source=${input.usersFile}`);
9640
+ }
9641
+ console.error(`[${input.tag}] check complete divergences=${d.divergences}`);
9642
+ return {
9643
+ divergences: d.divergences,
9644
+ accountWithoutUsers: d.accountWithoutUsers,
9645
+ usersWithoutAccount: d.usersWithoutAccount
9646
+ };
9647
+ }
9616
9648
 
9617
9649
  // server/routes/onboarding.ts
9618
9650
  function hashPin2(pin) {
@@ -9767,7 +9799,8 @@ app9.post("/set-pin", async (c) => {
9767
9799
  role: "owner",
9768
9800
  usersFile: USERS_FILE,
9769
9801
  accountDir: account.accountDir,
9770
- caller: "set-pin"
9802
+ caller: "set-pin",
9803
+ audit: { actor: userId, logFile: USERS_AUDIT_LOG }
9771
9804
  });
9772
9805
  if (result.usersJsonResult !== "ok") {
9773
9806
  console.error(`[set-pin] users.json write failed: ${result.usersError ?? "unknown"}`);
@@ -9858,12 +9891,16 @@ app9.delete("/set-pin", async (c) => {
9858
9891
  writeFileSync10(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
9859
9892
  console.log(`[set-pin] removed entry from users.json: userId=${matchedUser.userId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
9860
9893
  }
9894
+ logUsersAudit(
9895
+ { actor: matchedUser.userId, logFile: USERS_AUDIT_LOG },
9896
+ { action: "remove", field: "row", rowsBefore: formatAuditRowIds(users.map((u) => u.userId)), rowsAfter: formatAuditRowIds(remaining.map((u) => u.userId)) }
9897
+ );
9861
9898
  return c.json({ ok: true });
9862
9899
  });
9863
9900
  var onboarding_default = app9;
9864
9901
 
9865
9902
  // server/routes/client-error.ts
9866
- import { appendFileSync, existsSync as existsSync15, renameSync as renameSync6, statSync as statSync8 } from "fs";
9903
+ import { appendFileSync as appendFileSync2, existsSync as existsSync15, renameSync as renameSync6, statSync as statSync8 } from "fs";
9867
9904
  import { join as join21 } from "path";
9868
9905
  var CLIENT_ERRORS_LOG = join21(LOG_DIR, "client-errors.log");
9869
9906
  var MAX_LOG_SIZE = 10 * 1024 * 1024;
@@ -10012,7 +10049,7 @@ app10.post("/", async (c) => {
10012
10049
  tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
10013
10050
  status: typeof body.status === "number" ? body.status : void 0
10014
10051
  };
10015
- appendFileSync(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
10052
+ appendFileSync2(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
10016
10053
  } catch (err) {
10017
10054
  console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
10018
10055
  }
@@ -10511,7 +10548,7 @@ import { existsSync as existsSync20, mkdirSync as mkdirSync5, createWriteStream
10511
10548
  async function purgeAdminConversationJsonls(args) {
10512
10549
  const fetchImpl = args.fetchImpl ?? fetch;
10513
10550
  const log = args.logger ?? ((line) => console.log(line));
10514
- const id8 = args.sessionId.slice(0, 8);
10551
+ const id82 = args.sessionId.slice(0, 8);
10515
10552
  const purged = [];
10516
10553
  for (const sessionId of args.claudeSessionIds) {
10517
10554
  const url = `${args.managerBase}/${encodeURIComponent(sessionId)}`;
@@ -10521,7 +10558,7 @@ async function purgeAdminConversationJsonls(args) {
10521
10558
  } catch (err) {
10522
10559
  const message2 = err instanceof Error ? err.message : String(err);
10523
10560
  log(
10524
- `[admin-conversation-delete] sessionId=${id8} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message2}`
10561
+ `[admin-conversation-delete] sessionId=${id82} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message2}`
10525
10562
  );
10526
10563
  return {
10527
10564
  ok: false,
@@ -10538,7 +10575,7 @@ async function purgeAdminConversationJsonls(args) {
10538
10575
  }
10539
10576
  if (res.status === 409) {
10540
10577
  log(
10541
- `[admin-conversation-delete] sessionId=${id8} reason=pty-still-alive claudeSessionId=${sessionId.slice(0, 8)}`
10578
+ `[admin-conversation-delete] sessionId=${id82} reason=pty-still-alive claudeSessionId=${sessionId.slice(0, 8)}`
10542
10579
  );
10543
10580
  return {
10544
10581
  ok: false,
@@ -10555,7 +10592,7 @@ async function purgeAdminConversationJsonls(args) {
10555
10592
  } catch {
10556
10593
  }
10557
10594
  log(
10558
- `[admin-conversation-delete] sessionId=${id8} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`
10595
+ `[admin-conversation-delete] sessionId=${id82} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`
10559
10596
  );
10560
10597
  return {
10561
10598
  ok: false,
@@ -10574,13 +10611,13 @@ async function cascadeAdminConversationDelete(args) {
10574
10611
  const started = Date.now();
10575
10612
  const fetchImpl = args.fetchImpl ?? fetch;
10576
10613
  const log = args.logger ?? ((line) => console.log(line));
10577
- const id8 = args.sessionId.slice(0, 8);
10614
+ const id82 = args.sessionId.slice(0, 8);
10578
10615
  let claudeSessionIds;
10579
10616
  try {
10580
10617
  claudeSessionIds = args.claudeSessionIds ?? await getConversationClaudeSessionIds(args.sessionId, args.accountId);
10581
10618
  } catch (err) {
10582
10619
  const message = err instanceof Error ? err.message : String(err);
10583
- log(`[admin-conversation-delete] sessionId=${id8} reason=graph-delete-failed claudeSessionIds-purged= message=transcript-resolve:${message}`);
10620
+ log(`[admin-conversation-delete] sessionId=${id82} reason=graph-delete-failed claudeSessionIds-purged= message=transcript-resolve:${message}`);
10584
10621
  return { ok: false, reason: "graph-delete-failed", claudeSessionIdsPurged: [], message };
10585
10622
  }
10586
10623
  const purgeOutcome = await purgeAdminConversationJsonls({
@@ -10611,7 +10648,7 @@ async function cascadeAdminConversationDelete(args) {
10611
10648
  } catch (err) {
10612
10649
  const message = err instanceof Error ? err.message : String(err);
10613
10650
  const csv2 = purged.map((s) => s.slice(0, 8)).join(",");
10614
- log(`[admin-conversation-delete] sessionId=${id8} reason=graph-delete-failed claudeSessionIds-purged=${csv2} message=${message}`);
10651
+ log(`[admin-conversation-delete] sessionId=${id82} reason=graph-delete-failed claudeSessionIds-purged=${csv2} message=${message}`);
10615
10652
  return {
10616
10653
  ok: false,
10617
10654
  reason: "graph-delete-failed",
@@ -10624,7 +10661,7 @@ async function cascadeAdminConversationDelete(args) {
10624
10661
  }
10625
10662
  const ms = Date.now() - started;
10626
10663
  const csv = purged.map((s) => s.slice(0, 8)).join(",");
10627
- log(`[admin-conversation-delete] sessionId=${id8} claudeSessionIds=${csv} ms=${ms}`);
10664
+ log(`[admin-conversation-delete] sessionId=${id82} claudeSessionIds=${csv} ms=${ms}`);
10628
10665
  return { ok: true, claudeSessionIds: purged, ms };
10629
10666
  }
10630
10667
 
@@ -16607,777 +16644,158 @@ app38.get("/", async (c) => {
16607
16644
  });
16608
16645
  var public_session_context_default = app38;
16609
16646
 
16610
- // app/lib/channel-pty-bridge/bridge.ts
16611
- import { resolve as resolve26 } from "path";
16612
-
16613
- // app/lib/channel-pty-bridge/public-session-end-review.ts
16614
- import { randomUUID as randomUUID13 } from "crypto";
16615
- var TAG29 = "[public-session-review]";
16616
- var CONSUMED_CAP = 500;
16617
- var consumed = /* @__PURE__ */ new Set();
16618
- function consumeOnce(sessionId) {
16619
- if (consumed.has(sessionId)) return false;
16620
- consumed.add(sessionId);
16621
- if (consumed.size > CONSUMED_CAP) {
16622
- const oldest = consumed.values().next().value;
16623
- if (oldest !== void 0) consumed.delete(oldest);
16624
- }
16625
- return true;
16647
+ // app/lib/webchat/gateway/instance.ts
16648
+ var instance2 = null;
16649
+ function setWebchatGateway(gw) {
16650
+ instance2 = gw;
16626
16651
  }
16627
- function managerBase3() {
16628
- const port2 = requirePortEnv("CLAUDE_SESSION_MANAGER_PORT", { tag: "public-session-end-review:manager" });
16629
- return `http://127.0.0.1:${port2}`;
16652
+ function getWebchatGateway() {
16653
+ return instance2;
16630
16654
  }
16631
- function uiBase() {
16632
- const port2 = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "public-session-end-review:ui" });
16633
- return `http://127.0.0.1:${port2}`;
16655
+
16656
+ // server/routes/admin/public-session-exit.ts
16657
+ var TAG29 = "[public-session-exit-route]";
16658
+ function isLoopbackAddr4(addr) {
16659
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
16634
16660
  }
16635
- async function fetchJsonl(sessionId) {
16661
+ var app39 = new Hono();
16662
+ app39.post("/", async (c) => {
16663
+ const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
16664
+ if (!isLoopbackAddr4(remoteAddr)) {
16665
+ console.error(`${TAG29} reject reason=non-loopback remoteAddr=${remoteAddr}`);
16666
+ return c.json({ error: "public-session-exit-loopback-only" }, 403);
16667
+ }
16668
+ const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
16669
+ const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
16670
+ if (xffRaw.length > 0) {
16671
+ const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
16672
+ const offender = tokens.find((t) => !isLoopbackAddr4(t));
16673
+ if (offender !== void 0) {
16674
+ console.error(`${TAG29} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
16675
+ return c.json({ error: "public-session-exit-loopback-only" }, 403);
16676
+ }
16677
+ }
16678
+ let body;
16636
16679
  try {
16637
- const res = await fetch(`${managerBase3()}/${sessionId}/log?download=1`);
16638
- if (!res.ok) return null;
16639
- return await res.text();
16640
- } catch (err) {
16641
- console.error(
16642
- `${TAG29} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
16643
- );
16644
- return null;
16680
+ body = await c.req.json();
16681
+ } catch {
16682
+ return c.json({ error: "invalid-json" }, 400);
16683
+ }
16684
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
16685
+ if (!sessionId) return c.json({ error: "sessionId required" }, 400);
16686
+ const gateway = getWebchatGateway();
16687
+ if (!gateway) {
16688
+ console.error(`${TAG29} reject reason=gateway-unset sessionId=${sessionId.slice(0, 8)}`);
16689
+ return c.json({ error: "webchat-gateway-unset" }, 503);
16645
16690
  }
16691
+ gateway.handlePublicSessionExit(sessionId);
16692
+ return c.json({ ok: true });
16693
+ });
16694
+ var public_session_exit_default = app39;
16695
+
16696
+ // server/routes/admin/access-session-evict.ts
16697
+ var TAG30 = "[access-session-evict]";
16698
+ function isLoopbackAddr5(addr) {
16699
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
16646
16700
  }
16647
- function countOperatorTurns(jsonl) {
16648
- let count = 0;
16649
- for (const raw of jsonl.split("\n")) {
16650
- const line = raw.trim();
16651
- if (!line) continue;
16652
- try {
16653
- const obj = JSON.parse(line);
16654
- if (obj.type === "user") count += 1;
16655
- } catch {
16701
+ var app40 = new Hono();
16702
+ app40.post("/", async (c) => {
16703
+ const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
16704
+ if (!isLoopbackAddr5(remoteAddr)) {
16705
+ console.error(`${TAG30} reject reason=non-loopback remoteAddr=${remoteAddr}`);
16706
+ return c.json({ error: "access-session-evict-loopback-only" }, 403);
16707
+ }
16708
+ const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
16709
+ const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
16710
+ if (xffRaw.length > 0) {
16711
+ const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
16712
+ const offender = tokens.find((t) => !isLoopbackAddr5(t));
16713
+ if (offender !== void 0) {
16714
+ console.error(`${TAG30} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
16715
+ return c.json({ error: "access-session-evict-loopback-only" }, 403);
16656
16716
  }
16657
16717
  }
16658
- return count;
16659
- }
16660
- async function fetchPriorWrites(sliceToken, accountId) {
16661
- const url = `${uiBase()}/api/admin/public-session-context?sliceToken=${encodeURIComponent(sliceToken)}&accountId=${encodeURIComponent(accountId)}`;
16718
+ let body;
16662
16719
  try {
16663
- const res = await fetch(url);
16664
- if (!res.ok) {
16665
- console.error(
16666
- `${TAG29} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
16667
- );
16668
- return [];
16669
- }
16670
- const body = await res.json();
16671
- return Array.isArray(body.writes) ? body.writes : [];
16672
- } catch (err) {
16673
- console.error(
16674
- `${TAG29} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
16675
- );
16676
- return [];
16720
+ body = await c.req.json();
16721
+ } catch {
16722
+ return c.json({ error: "invalid-json" }, 400);
16677
16723
  }
16724
+ const grantId = typeof body.grantId === "string" ? body.grantId.trim() : "";
16725
+ if (!grantId) return c.json({ error: "grantId required" }, 400);
16726
+ const dropped = evictAccessSessionsByGrant(grantId);
16727
+ console.log(`${TAG30} grantId=${grantId} dropped=${dropped}`);
16728
+ return c.json({ ok: true, dropped });
16729
+ });
16730
+ var access_session_evict_default = app40;
16731
+
16732
+ // server/routes/admin/enrol-person.ts
16733
+ var TAG31 = "[enrol]";
16734
+ function isLoopbackAddr6(addr) {
16735
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
16678
16736
  }
16679
- function composeInitialMessage(input) {
16680
- const priorJson = JSON.stringify(input.priorWrites, null, 2);
16681
- return [
16682
- `The gated public-agent session for agent "${input.agentSlug}" has just been reaped. Review the transcript and decide what is worth saving to the visitor's per-visitor memory slice. Dispatch \`database-operator\` for each individual write via the Task tool. Do not call memory-write or memory-update directly.`,
16683
- "",
16684
- "<slice-token>",
16685
- input.sliceToken,
16686
- "</slice-token>",
16687
- "",
16688
- "<person-id>",
16689
- input.personId ?? "(unknown)",
16690
- "</person-id>",
16691
- "",
16692
- "<prior-writes>",
16693
- priorJson,
16694
- "</prior-writes>",
16695
- "",
16696
- "<transcript>",
16697
- input.jsonl,
16698
- "</transcript>"
16699
- ].join("\n");
16700
- }
16701
- async function dispatchReviewer(input, initialMessage) {
16702
- const sessionId = randomUUID13();
16703
- console.log(`${TAG29} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
16704
- const spawned = await managerRcSpawn({
16705
- sessionId,
16706
- initialMessage,
16707
- closeAfterTurn: true,
16708
- sliceToken: input.sliceToken,
16709
- personId: input.personId ?? void 0,
16710
- logContext: `public-session-end sourceSession=${input.sessionId.slice(0, 8)}`
16711
- });
16712
- if ("error" in spawned) {
16713
- return { ok: false, reason: `rc-spawn-${spawned.status}-${spawned.error}` };
16737
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
16738
+ var app41 = new Hono();
16739
+ app41.post("/", async (c) => {
16740
+ const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
16741
+ if (!isLoopbackAddr6(remoteAddr)) {
16742
+ console.error(`${TAG31} reject reason=non-loopback remoteAddr=${remoteAddr}`);
16743
+ return c.json({ error: "enrol-person-loopback-only" }, 403);
16714
16744
  }
16715
- return { ok: true, managerSessionId: spawned.sessionId };
16716
- }
16717
- async function firePublicSessionEndReview(input) {
16718
- const sliceShort = input.sliceToken.slice(0, 8);
16719
- const personField = input.personId ?? "none";
16720
- const dispatchedAt = Date.now();
16721
- if (consumed.has(input.sessionId)) {
16722
- console.log(
16723
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
16724
- );
16725
- return;
16745
+ const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
16746
+ const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
16747
+ if (xffRaw.length > 0) {
16748
+ const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
16749
+ const offender = tokens.find((t) => !isLoopbackAddr6(t));
16750
+ if (offender !== void 0) {
16751
+ console.error(`${TAG31} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
16752
+ return c.json({ error: "enrol-person-loopback-only" }, 403);
16753
+ }
16726
16754
  }
16727
- const jsonl = await fetchJsonl(input.sessionId);
16728
- if (!jsonl) {
16729
- console.log(
16730
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
16731
- );
16732
- return;
16755
+ let body;
16756
+ try {
16757
+ body = await c.req.json();
16758
+ } catch {
16759
+ return c.json({ error: "invalid-json" }, 400);
16733
16760
  }
16734
- const operatorTurns = countOperatorTurns(jsonl);
16735
- if (operatorTurns === 0) {
16736
- console.log(
16737
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
16738
- );
16739
- return;
16761
+ const rawPhone = typeof body.phone === "string" ? body.phone : "";
16762
+ const phone = normalizeE164(rawPhone);
16763
+ if (phone.length <= 1) {
16764
+ return c.json({ error: "invalid-phone" }, 400);
16740
16765
  }
16741
- const priorWrites = await fetchPriorWrites(input.sliceToken, input.accountId);
16742
- const initialMessage = composeInitialMessage({
16743
- agentSlug: input.agentSlug,
16744
- sliceToken: input.sliceToken,
16745
- personId: input.personId,
16746
- jsonl,
16747
- priorWrites
16748
- });
16749
- if (!consumeOnce(input.sessionId)) {
16750
- console.log(
16751
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
16766
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
16767
+ if (!EMAIL_RE.test(email)) {
16768
+ return c.json({ error: "invalid-email" }, 400);
16769
+ }
16770
+ const agentSlug = typeof body.agentSlug === "string" ? body.agentSlug.trim() : "";
16771
+ if (!agentSlug) {
16772
+ return c.json({ error: "agentSlug required" }, 400);
16773
+ }
16774
+ const accountId = process.env.ACCOUNT_ID ?? "";
16775
+ if (!accountId) {
16776
+ console.error(`${TAG31} op=person result=no-account-id`);
16777
+ return c.json({ error: "no-account-id" }, 500);
16778
+ }
16779
+ let personId;
16780
+ try {
16781
+ personId = await enrolPerson({ accountId, phone, email, agentSlug });
16782
+ } catch (err) {
16783
+ console.error(
16784
+ `${TAG31} op=person result=write-failed agentSlug=${agentSlug} contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
16752
16785
  );
16753
- return;
16786
+ return c.json({ error: "enrol-failed" }, 500);
16754
16787
  }
16755
- const dispatched = await dispatchReviewer(input, initialMessage);
16756
- if (!dispatched.ok) {
16788
+ let grant = null;
16789
+ try {
16790
+ const g = await findGrantByContact(email, agentSlug, accountId);
16791
+ if (g) grant = { grantId: g.grantId, status: g.status, contactValue: g.contactValue };
16792
+ } catch (err) {
16757
16793
  console.error(
16758
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
16794
+ `${TAG31} op=person result=grant-lookup-failed contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
16759
16795
  );
16760
- return;
16761
16796
  }
16762
16797
  console.log(
16763
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
16764
- );
16765
- }
16766
-
16767
- // app/lib/channel-pty-bridge/follower.ts
16768
- function followerPendingMaxMs() {
16769
- return Number(process.env.CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS ?? String(3e5));
16770
- }
16771
- function followerRetryMs() {
16772
- return Number(process.env.CHANNEL_PTY_FOLLOWER_RETRY_MS ?? String(1e3));
16773
- }
16774
- function startFollower(opts) {
16775
- const abort = new AbortController();
16776
- const { entry, tag } = opts;
16777
- void (async () => {
16778
- try {
16779
- const sid = entry.sessionId.slice(0, 8);
16780
- const deadline = Date.now() + followerPendingMaxMs();
16781
- const retryMs = followerRetryMs();
16782
- let res;
16783
- let attempt = 0;
16784
- for (; ; ) {
16785
- res = await fetch(
16786
- managerLogFollowUrl(entry.sessionId, { boundary: opts.suppressResumeReplay === true }),
16787
- { signal: abort.signal }
16788
- );
16789
- console.error(`${tag} follower-connect sessionId=${sid} status=${res.status}`);
16790
- if (res.status === 202) {
16791
- attempt += 1;
16792
- await res.body?.cancel().catch(() => {
16793
- });
16794
- if (abort.signal.aborted) return;
16795
- if (Date.now() >= deadline) {
16796
- console.error(`${tag} follower-give-up sessionId=${sid} reason=pending-timeout attempts=${attempt}`);
16797
- opts.onError?.("follow-pending-timeout");
16798
- return;
16799
- }
16800
- console.error(`${tag} follower-retry sessionId=${sid} attempt=${attempt} reason=pending`);
16801
- await new Promise((r) => setTimeout(r, retryMs));
16802
- if (abort.signal.aborted) return;
16803
- continue;
16804
- }
16805
- break;
16806
- }
16807
- if (!res.ok || !res.body) {
16808
- opts.onError?.(`follow-status-${res.status}`);
16809
- return;
16810
- }
16811
- console.error(`${tag} follower-open sessionId=${sid}`);
16812
- const reader = res.body.getReader();
16813
- const decoder = new TextDecoder("utf8");
16814
- let buffered = "";
16815
- const fileDelivery = opts.fileDelivery ?? null;
16816
- let firedFileTools = [];
16817
- let suppressing = opts.suppressResumeReplay === true;
16818
- let discardedTurns = 0;
16819
- let discardedFileTools = 0;
16820
- while (!abort.signal.aborted) {
16821
- const { value, done } = await reader.read();
16822
- if (done) break;
16823
- buffered += decoder.decode(value, { stream: true });
16824
- let nl = buffered.indexOf("\n");
16825
- while (nl !== -1) {
16826
- const line = buffered.slice(0, nl);
16827
- buffered = buffered.slice(nl + 1);
16828
- nl = buffered.indexOf("\n");
16829
- if (!line) continue;
16830
- let event;
16831
- try {
16832
- event = JSON.parse(line);
16833
- } catch (err) {
16834
- console.error(
16835
- `${tag} jsonl-parse-skip sessionId=${entry.sessionId.slice(0, 8)} bytes=${line.length} message=${err instanceof Error ? err.message : String(err)}`
16836
- );
16837
- continue;
16838
- }
16839
- if (event.type === "__channel_resume_boundary__") {
16840
- if (suppressing) {
16841
- suppressing = false;
16842
- console.error(
16843
- `${tag} follower-resume-boundary sessionId=${sid} discarded-head-turns=${discardedTurns} discarded-head-filetools=${discardedFileTools}`
16844
- );
16845
- }
16846
- continue;
16847
- }
16848
- if (event.type === "user") {
16849
- entry.pendingTurnText = "";
16850
- firedFileTools = [];
16851
- continue;
16852
- }
16853
- if (event.type !== "assistant") continue;
16854
- const msg = event.message;
16855
- if (!msg) continue;
16856
- let sawContent = false;
16857
- if (Array.isArray(msg.content)) {
16858
- for (const block of msg.content) {
16859
- if (block?.type === "text" && typeof block.text === "string") {
16860
- entry.pendingTurnText += block.text;
16861
- sawContent = true;
16862
- } else if (block?.type === "tool_use") {
16863
- sawContent = true;
16864
- if (fileDelivery && typeof block.name === "string" && fileDelivery.isFileDeliveryTool(block.name)) {
16865
- if (suppressing) {
16866
- discardedFileTools += 1;
16867
- continue;
16868
- }
16869
- firedFileTools.push(block.name);
16870
- try {
16871
- await fileDelivery.onFileToolUse({ toolName: block.name, input: block.input });
16872
- } catch (err) {
16873
- console.error(
16874
- `${tag} file-delivery-error sessionId=${sid} tool=${block.name} message=${err instanceof Error ? err.message : String(err)}`
16875
- );
16876
- }
16877
- }
16878
- }
16879
- }
16880
- }
16881
- if (msg.stop_reason === "end_turn") {
16882
- if (suppressing) {
16883
- entry.pendingTurnText = "";
16884
- firedFileTools = [];
16885
- discardedTurns += 1;
16886
- continue;
16887
- }
16888
- const flush = entry.pendingTurnText;
16889
- entry.pendingTurnText = "";
16890
- if (flush.trim()) {
16891
- await fanOut(entry.subscribers, flush, opts.onError, tag);
16892
- }
16893
- if (fileDelivery && firedFileTools.length > 0) {
16894
- const fired = firedFileTools;
16895
- firedFileTools = [];
16896
- try {
16897
- fileDelivery.onTurnEnd(fired);
16898
- } catch (err) {
16899
- console.error(
16900
- `${tag} file-delivery-error sessionId=${sid} phase=turn-end message=${err instanceof Error ? err.message : String(err)}`
16901
- );
16902
- }
16903
- }
16904
- opts.onTurnComplete?.();
16905
- } else if (sawContent && !suppressing) {
16906
- opts.onActivity?.();
16907
- }
16908
- }
16909
- }
16910
- } catch (err) {
16911
- if (!abort.signal.aborted) {
16912
- opts.onError?.(`follower-error: ${err instanceof Error ? err.message : String(err)}`);
16913
- }
16914
- } finally {
16915
- opts.onClose();
16916
- }
16917
- })();
16918
- return abort;
16919
- }
16920
- async function fanOut(subscribers, text, onError, tag) {
16921
- const callbacks = Array.from(subscribers);
16922
- await Promise.all(
16923
- callbacks.map(
16924
- (cb) => Promise.resolve().then(() => cb(text)).catch((err) => {
16925
- const m = err instanceof Error ? err.message : String(err);
16926
- console.error(`${tag} subscriber-error message=${m}`);
16927
- onError?.(`subscriber-error: ${m}`);
16928
- })
16929
- )
16930
- );
16931
- }
16932
-
16933
- // app/lib/channel-pty-bridge/file-delivery.ts
16934
- var SEND_USER_FILE = "SendUserFile";
16935
- function makeFileDelivery(opts) {
16936
- const { entry, tag, channel, sendFile } = opts;
16937
- let failedFiles = [];
16938
- let attempts = 0;
16939
- return {
16940
- isFileDeliveryTool(toolName) {
16941
- return toolName === SEND_USER_FILE;
16942
- },
16943
- async onFileToolUse(use) {
16944
- if (use.toolName !== SEND_USER_FILE) return;
16945
- const input = use.input ?? {};
16946
- const files = Array.isArray(input.files) ? input.files.filter((f) => typeof f === "string") : [];
16947
- const caption = typeof input.caption === "string" ? input.caption : void 0;
16948
- for (let i = 0; i < files.length; i++) {
16949
- attempts++;
16950
- const result = await sendFile(files[i], i === 0 ? caption : void 0);
16951
- const name = files[i].split("/").pop() ?? files[i];
16952
- console.error(`${tag} op=file-forward channel=${channel} file=${name} outcome=${result.ok ? "ok" : "fail"}`);
16953
- if (!result.ok) failedFiles.push(files[i]);
16954
- }
16955
- },
16956
- onTurnEnd(firedTools) {
16957
- const failed = failedFiles;
16958
- const tried = attempts;
16959
- failedFiles = [];
16960
- attempts = 0;
16961
- const sid = entry.sessionId.slice(0, 8);
16962
- for (const file of failed) {
16963
- console.error(
16964
- `${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE} file=${file}`
16965
- );
16966
- }
16967
- if (firedTools.includes(SEND_USER_FILE) && tried === 0) {
16968
- console.error(
16969
- `${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE}`
16970
- );
16971
- }
16972
- }
16973
- };
16974
- }
16975
-
16976
- // app/lib/whatsapp/inbound/file-delivery-bridge.ts
16977
- var TAG30 = "[whatsapp-adaptor]";
16978
- var SEND_USER_FILE2 = "SendUserFile";
16979
- var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
16980
- function platformRoot() {
16981
- return process.env.MAXY_PLATFORM_ROOT || "";
16982
- }
16983
- function makeWhatsAppSendFile(entry) {
16984
- return async (filePath, caption) => {
16985
- let maxyAccountId;
16986
- try {
16987
- maxyAccountId = resolvePlatformAccountId();
16988
- } catch (err) {
16989
- console.error(
16990
- `${TAG30} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
16991
- );
16992
- return { ok: false, error: "account-unresolved" };
16993
- }
16994
- const result = await sendWhatsAppDocument({
16995
- to: entry.senderId,
16996
- filePath,
16997
- caption,
16998
- accountId: entry.accountId,
16999
- maxyAccountId,
17000
- platformRoot: platformRoot()
17001
- });
17002
- if (result.ok) return { ok: true };
17003
- console.error(
17004
- `${TAG30} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
17005
- );
17006
- return { ok: false, error: result.error };
17007
- };
17008
- }
17009
- function makeWhatsAppFileDelivery(entry) {
17010
- const shared = makeFileDelivery({
17011
- entry,
17012
- tag: TAG30,
17013
- channel: "whatsapp",
17014
- sendFile: makeWhatsAppSendFile(entry)
17015
- });
17016
- let turnStartedAt = null;
17017
- let routeCalls = [];
17018
- return {
17019
- isFileDeliveryTool(toolName) {
17020
- return toolName === SEND_USER_FILE2 || toolName === WHATSAPP_SEND_DOCUMENT;
17021
- },
17022
- async onFileToolUse(use) {
17023
- if (turnStartedAt === null) turnStartedAt = Date.now();
17024
- if (use.toolName === WHATSAPP_SEND_DOCUMENT) {
17025
- const input = use.input ?? {};
17026
- routeCalls.push({
17027
- to: typeof input.to === "string" ? input.to : void 0,
17028
- filePath: typeof input.filePath === "string" ? input.filePath : void 0
17029
- });
17030
- return;
17031
- }
17032
- await shared.onFileToolUse(use);
17033
- },
17034
- onTurnEnd(firedTools) {
17035
- const startedAt = turnStartedAt ?? 0;
17036
- const routes = routeCalls;
17037
- turnStartedAt = null;
17038
- routeCalls = [];
17039
- const sid = entry.sessionId.slice(0, 8);
17040
- shared.onTurnEnd(firedTools);
17041
- for (const call of routes) {
17042
- const routeAt = call.to !== void 0 && call.filePath !== void 0 ? routeDocumentOutboundAt(call.to, call.filePath) : void 0;
17043
- const delivered = routeAt !== void 0 && routeAt >= startedAt;
17044
- if (!delivered) {
17045
- const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
17046
- console.error(
17047
- `${TAG30} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
17048
- );
17049
- }
17050
- }
17051
- }
17052
- };
17053
- }
17054
-
17055
- // app/lib/telegram/outbound/send-document.ts
17056
- import { realpathSync as realpathSync5 } from "fs";
17057
- import { readFile as readFile6, stat as stat6 } from "fs/promises";
17058
- import { resolve as resolve24, basename as basename9 } from "path";
17059
- var TAG31 = "[telegram:outbound]";
17060
- var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
17061
- async function sendTelegramDocument(input) {
17062
- const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot3 } = input;
17063
- if (!botToken || !filePath) {
17064
- return { ok: false, status: 400, error: "Missing required fields: botToken, filePath" };
17065
- }
17066
- if (!maxyAccountId || !platformRoot3) {
17067
- return { ok: false, status: 400, error: "Cannot validate file path: missing account or platform context" };
17068
- }
17069
- const accountDir = resolve24(platformRoot3, "..", "data/accounts", maxyAccountId);
17070
- let resolvedPath;
17071
- try {
17072
- resolvedPath = realpathSync5(filePath);
17073
- const accountResolved = realpathSync5(accountDir);
17074
- if (!resolvedPath.startsWith(accountResolved + "/")) {
17075
- console.error(`${TAG31} document REJECTED reason=outside_account_directory`);
17076
- return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
17077
- }
17078
- } catch (err) {
17079
- const code = err.code;
17080
- if (code === "ENOENT") {
17081
- console.error(`${TAG31} document ENOENT path=${filePath}`);
17082
- return { ok: false, status: 404, error: `File not found: ${filePath}` };
17083
- }
17084
- console.error(`${TAG31} document path error: ${String(err)}`);
17085
- return { ok: false, status: 500, error: String(err) };
17086
- }
17087
- const fileStat = await stat6(resolvedPath);
17088
- if (fileStat.size > TELEGRAM_DOCUMENT_MAX_BYTES) {
17089
- return {
17090
- ok: false,
17091
- status: 400,
17092
- error: `File exceeds 50 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)`
17093
- };
17094
- }
17095
- const filename = basename9(resolvedPath);
17096
- const buffer = Buffer.from(await readFile6(resolvedPath));
17097
- const form = new FormData();
17098
- form.append("chat_id", String(chatId));
17099
- if (caption) form.append("caption", caption);
17100
- form.append("document", new File([buffer], filename, { type: detectMimeType(resolvedPath) }));
17101
- let ok = false;
17102
- let messageId;
17103
- let error = "send failed";
17104
- try {
17105
- const res = await fetch(`https://api.telegram.org/bot${botToken}/sendDocument`, {
17106
- method: "POST",
17107
- body: form
17108
- });
17109
- const data = await res.json();
17110
- ok = data.ok;
17111
- messageId = data.result?.message_id;
17112
- if (!data.ok) error = data.description ?? "Unknown Telegram error";
17113
- } catch (e) {
17114
- error = e instanceof Error ? e.message : String(e);
17115
- }
17116
- console.error(
17117
- `${TAG31} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok}` + (messageId !== void 0 ? ` id=${messageId}` : "")
17118
- );
17119
- return ok ? { ok: true, messageId } : { ok: false, status: 500, error };
17120
- }
17121
-
17122
- // app/lib/telegram/outbound/file-delivery.ts
17123
- var TAG32 = "[telegram:outbound]";
17124
- function platformRoot2() {
17125
- return process.env.MAXY_PLATFORM_ROOT || "";
17126
- }
17127
- function makeTelegramSendFile(entry) {
17128
- let botToken;
17129
- return async (filePath, caption) => {
17130
- if (botToken === void 0) {
17131
- const tg = resolveAccount()?.config.telegram;
17132
- botToken = (entry.role === "admin" ? tg?.adminBotToken : tg?.publicBotToken) ?? null;
17133
- }
17134
- if (!botToken) {
17135
- console.error(`${TAG32} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
17136
- return { ok: false, error: "no-bot-token" };
17137
- }
17138
- if (entry.replyTarget == null) {
17139
- console.error(`${TAG32} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
17140
- return { ok: false, error: "no-reply-target" };
17141
- }
17142
- let maxyAccountId;
17143
- try {
17144
- maxyAccountId = resolvePlatformAccountId();
17145
- } catch (err) {
17146
- console.error(
17147
- `${TAG32} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
17148
- );
17149
- return { ok: false, error: "account-unresolved" };
17150
- }
17151
- const result = await sendTelegramDocument({
17152
- botToken,
17153
- chatId: Number(entry.replyTarget),
17154
- filePath,
17155
- caption,
17156
- maxyAccountId,
17157
- platformRoot: platformRoot2()
17158
- });
17159
- return result.ok ? { ok: true } : { ok: false, error: result.error };
17160
- };
17161
- }
17162
- function makeTelegramFileDelivery(entry) {
17163
- return makeFileDelivery({
17164
- entry,
17165
- tag: TAG32,
17166
- channel: "telegram",
17167
- sendFile: makeTelegramSendFile(entry)
17168
- });
17169
- }
17170
-
17171
- // app/lib/webchat/file-delivery.ts
17172
- import { realpathSync as realpathSync6 } from "fs";
17173
- import { resolve as resolve25 } from "path";
17174
-
17175
- // app/lib/channel-pty-bridge/bridge.ts
17176
- function tagFor(channel) {
17177
- return `[${channel}-adaptor]`;
17178
- }
17179
- function publicIdleMs() {
17180
- return Number(process.env.CHANNEL_PTY_IDLE_MS ?? String(5 * 6e4));
17181
- }
17182
- var index = /* @__PURE__ */ new Map();
17183
- var reaperHandle = null;
17184
- function startReaper() {
17185
- if (reaperHandle) return;
17186
- reaperHandle = setInterval(() => {
17187
- const now = Date.now();
17188
- const cutoff = now - publicIdleMs();
17189
- for (const [key, entry] of index) {
17190
- if (entry.role !== "public") continue;
17191
- if (entry.subscribers.size > 0) continue;
17192
- if (entry.lastInboundAt > cutoff) continue;
17193
- const idleMs = now - entry.lastInboundAt;
17194
- const tag = tagFor(entry.channel);
17195
- console.error(
17196
- `${tag} reap senderId=${entry.senderId} idle-ms=${idleMs} sessionId=${entry.sessionId.slice(0, 8)}`
17197
- );
17198
- if (entry.followerAbort) entry.followerAbort.abort();
17199
- index.delete(key);
17200
- if (entry.role === "public" && entry.sliceToken.length > 0) {
17201
- void firePublicSessionEndReview({
17202
- sessionId: entry.sessionId,
17203
- sliceToken: entry.sliceToken,
17204
- personId: entry.personId,
17205
- accountId: entry.accountId,
17206
- agentSlug: entry.agentSlug,
17207
- dispatchFor: "eviction"
17208
- });
17209
- }
17210
- void managerDelete(entry.sessionId);
17211
- }
17212
- }, 6e4);
17213
- if (reaperHandle.unref) reaperHandle.unref();
17214
- }
17215
- function handlePublicSessionExit(sessionId) {
17216
- let found = null;
17217
- for (const [key2, entry2] of index) {
17218
- if (entry2.sessionId === sessionId) {
17219
- found = { key: key2, entry: entry2 };
17220
- break;
17221
- }
17222
- }
17223
- if (!found) {
17224
- console.error(`[public-session-exit] sessionId=${sessionId} result=no-entry`);
17225
- return;
17226
- }
17227
- const { key, entry } = found;
17228
- if (entry.role !== "public" || entry.sliceToken.length === 0) {
17229
- return;
17230
- }
17231
- if (entry.followerAbort) entry.followerAbort.abort();
17232
- index.delete(key);
17233
- void firePublicSessionEndReview({
17234
- sessionId: entry.sessionId,
17235
- sliceToken: entry.sliceToken,
17236
- personId: entry.personId,
17237
- accountId: entry.accountId,
17238
- agentSlug: entry.agentSlug,
17239
- dispatchFor: "exit"
17240
- });
17241
- }
17242
-
17243
- // server/routes/admin/public-session-exit.ts
17244
- var TAG33 = "[public-session-exit-route]";
17245
- function isLoopbackAddr4(addr) {
17246
- return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17247
- }
17248
- var app39 = new Hono();
17249
- app39.post("/", async (c) => {
17250
- const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17251
- if (!isLoopbackAddr4(remoteAddr)) {
17252
- console.error(`${TAG33} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17253
- return c.json({ error: "public-session-exit-loopback-only" }, 403);
17254
- }
17255
- const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17256
- const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17257
- if (xffRaw.length > 0) {
17258
- const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17259
- const offender = tokens.find((t) => !isLoopbackAddr4(t));
17260
- if (offender !== void 0) {
17261
- console.error(`${TAG33} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17262
- return c.json({ error: "public-session-exit-loopback-only" }, 403);
17263
- }
17264
- }
17265
- let body;
17266
- try {
17267
- body = await c.req.json();
17268
- } catch {
17269
- return c.json({ error: "invalid-json" }, 400);
17270
- }
17271
- const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
17272
- if (!sessionId) return c.json({ error: "sessionId required" }, 400);
17273
- handlePublicSessionExit(sessionId);
17274
- return c.json({ ok: true });
17275
- });
17276
- var public_session_exit_default = app39;
17277
-
17278
- // server/routes/admin/access-session-evict.ts
17279
- var TAG34 = "[access-session-evict]";
17280
- function isLoopbackAddr5(addr) {
17281
- return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17282
- }
17283
- var app40 = new Hono();
17284
- app40.post("/", async (c) => {
17285
- const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17286
- if (!isLoopbackAddr5(remoteAddr)) {
17287
- console.error(`${TAG34} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17288
- return c.json({ error: "access-session-evict-loopback-only" }, 403);
17289
- }
17290
- const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17291
- const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17292
- if (xffRaw.length > 0) {
17293
- const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17294
- const offender = tokens.find((t) => !isLoopbackAddr5(t));
17295
- if (offender !== void 0) {
17296
- console.error(`${TAG34} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17297
- return c.json({ error: "access-session-evict-loopback-only" }, 403);
17298
- }
17299
- }
17300
- let body;
17301
- try {
17302
- body = await c.req.json();
17303
- } catch {
17304
- return c.json({ error: "invalid-json" }, 400);
17305
- }
17306
- const grantId = typeof body.grantId === "string" ? body.grantId.trim() : "";
17307
- if (!grantId) return c.json({ error: "grantId required" }, 400);
17308
- const dropped = evictAccessSessionsByGrant(grantId);
17309
- console.log(`${TAG34} grantId=${grantId} dropped=${dropped}`);
17310
- return c.json({ ok: true, dropped });
17311
- });
17312
- var access_session_evict_default = app40;
17313
-
17314
- // server/routes/admin/enrol-person.ts
17315
- var TAG35 = "[enrol]";
17316
- function isLoopbackAddr6(addr) {
17317
- return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17318
- }
17319
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
17320
- var app41 = new Hono();
17321
- app41.post("/", async (c) => {
17322
- const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17323
- if (!isLoopbackAddr6(remoteAddr)) {
17324
- console.error(`${TAG35} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17325
- return c.json({ error: "enrol-person-loopback-only" }, 403);
17326
- }
17327
- const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17328
- const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17329
- if (xffRaw.length > 0) {
17330
- const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17331
- const offender = tokens.find((t) => !isLoopbackAddr6(t));
17332
- if (offender !== void 0) {
17333
- console.error(`${TAG35} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17334
- return c.json({ error: "enrol-person-loopback-only" }, 403);
17335
- }
17336
- }
17337
- let body;
17338
- try {
17339
- body = await c.req.json();
17340
- } catch {
17341
- return c.json({ error: "invalid-json" }, 400);
17342
- }
17343
- const rawPhone = typeof body.phone === "string" ? body.phone : "";
17344
- const phone = normalizeE164(rawPhone);
17345
- if (phone.length <= 1) {
17346
- return c.json({ error: "invalid-phone" }, 400);
17347
- }
17348
- const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
17349
- if (!EMAIL_RE.test(email)) {
17350
- return c.json({ error: "invalid-email" }, 400);
17351
- }
17352
- const agentSlug = typeof body.agentSlug === "string" ? body.agentSlug.trim() : "";
17353
- if (!agentSlug) {
17354
- return c.json({ error: "agentSlug required" }, 400);
17355
- }
17356
- const accountId = process.env.ACCOUNT_ID ?? "";
17357
- if (!accountId) {
17358
- console.error(`${TAG35} op=person result=no-account-id`);
17359
- return c.json({ error: "no-account-id" }, 500);
17360
- }
17361
- let personId;
17362
- try {
17363
- personId = await enrolPerson({ accountId, phone, email, agentSlug });
17364
- } catch (err) {
17365
- console.error(
17366
- `${TAG35} op=person result=write-failed agentSlug=${agentSlug} contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
17367
- );
17368
- return c.json({ error: "enrol-failed" }, 500);
17369
- }
17370
- let grant = null;
17371
- try {
17372
- const g = await findGrantByContact(email, agentSlug, accountId);
17373
- if (g) grant = { grantId: g.grantId, status: g.status, contactValue: g.contactValue };
17374
- } catch (err) {
17375
- console.error(
17376
- `${TAG35} op=person result=grant-lookup-failed contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
17377
- );
17378
- }
17379
- console.log(
17380
- `${TAG35} op=person personId=${personId} account=${accountId} agentSlug=${agentSlug} contact=${maskContact(email)}`
16798
+ `${TAG31} op=person personId=${personId} account=${accountId} agentSlug=${agentSlug} contact=${maskContact(email)}`
17381
16799
  );
17382
16800
  return c.json({ personId, grant }, 200);
17383
16801
  });
@@ -17945,7 +17363,7 @@ app44.route("/calendar", calendar_default);
17945
17363
  var admin_default = app44;
17946
17364
 
17947
17365
  // server/routes/access/verify-token.ts
17948
- var TAG36 = "[access-verify]";
17366
+ var TAG32 = "[access-verify]";
17949
17367
  var MINT_TAG = "[access-session-mint]";
17950
17368
  var COOKIE_NAME = "__access_session";
17951
17369
  var app45 = new Hono();
@@ -17964,39 +17382,39 @@ app45.post("/", async (c) => {
17964
17382
  }
17965
17383
  const rateMsg = checkAccessRateLimit(ip, agentSlug);
17966
17384
  if (rateMsg) {
17967
- console.error(`${TAG36} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
17385
+ console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
17968
17386
  return c.json({ error: rateMsg }, 429);
17969
17387
  }
17970
17388
  const grant = await findGrantByMagicToken(token);
17971
17389
  if (!grant) {
17972
17390
  recordAccessFailedAttempt(ip, agentSlug);
17973
- console.error(`${TAG36} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
17391
+ console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
17974
17392
  return c.json({ error: "invalid-or-expired-link" }, 401);
17975
17393
  }
17976
17394
  if (grant.agentSlug !== agentSlug) {
17977
17395
  recordAccessFailedAttempt(ip, agentSlug);
17978
17396
  console.error(
17979
- `${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=agent-mismatch grantAgent=${grant.agentSlug} ip=${ip}`
17397
+ `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=agent-mismatch grantAgent=${grant.agentSlug} ip=${ip}`
17980
17398
  );
17981
17399
  return c.json({ error: "invalid-or-expired-link" }, 401);
17982
17400
  }
17983
17401
  if (grant.status === "expired" || grant.status === "revoked") {
17984
17402
  recordAccessFailedAttempt(ip, agentSlug);
17985
17403
  console.error(
17986
- `${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired status=${grant.status} ip=${ip}`
17404
+ `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired status=${grant.status} ip=${ip}`
17987
17405
  );
17988
17406
  return c.json({ error: "access-no-longer-valid" }, 401);
17989
17407
  }
17990
17408
  if (grant.expiresAt !== null && grant.expiresAt < Date.now()) {
17991
17409
  recordAccessFailedAttempt(ip, agentSlug);
17992
17410
  console.error(
17993
- `${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired reason=expiresAt-past ip=${ip}`
17411
+ `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired reason=expiresAt-past ip=${ip}`
17994
17412
  );
17995
17413
  return c.json({ error: "access-no-longer-valid" }, 401);
17996
17414
  }
17997
17415
  if (!grant.sliceToken) {
17998
17416
  console.error(
17999
- `${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=no-slice-token reason=schema-violation`
17417
+ `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=no-slice-token reason=schema-violation`
18000
17418
  );
18001
17419
  return c.json({ error: "grant-misconfigured" }, 500);
18002
17420
  }
@@ -18012,12 +17430,12 @@ app45.post("/", async (c) => {
18012
17430
  await consumeMagicTokenAndActivate(grant.grantId);
18013
17431
  } catch (err) {
18014
17432
  console.error(
18015
- `${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=consume-failed err="${err instanceof Error ? err.message : String(err)}"`
17433
+ `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=consume-failed err="${err instanceof Error ? err.message : String(err)}"`
18016
17434
  );
18017
17435
  return c.json({ error: "verification-failed" }, 500);
18018
17436
  }
18019
17437
  clearAccessRateLimit(ip, agentSlug);
18020
- console.log(`${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=ok ip=${ip}`);
17438
+ console.log(`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=ok ip=${ip}`);
18021
17439
  console.log(
18022
17440
  `${MINT_TAG} grantId=${grant.grantId} sliceToken=${grant.sliceToken.slice(0, 8)} agentSlug=${agentSlug} personId=${grant.personId ?? "none"}`
18023
17441
  );
@@ -18035,9 +17453,9 @@ var verify_token_default = app45;
18035
17453
 
18036
17454
  // app/lib/access-email.ts
18037
17455
  import { spawn as spawn2 } from "child_process";
18038
- import { resolve as resolve27 } from "path";
18039
- var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve27(process.cwd(), "..");
18040
- var SEND_SCRIPT = resolve27(
17456
+ import { resolve as resolve24 } from "path";
17457
+ var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve24(process.cwd(), "..");
17458
+ var SEND_SCRIPT = resolve24(
18041
17459
  PLATFORM_ROOT7,
18042
17460
  "plugins",
18043
17461
  "email",
@@ -18093,7 +17511,7 @@ async function sendMagicLinkEmail(payload) {
18093
17511
  }
18094
17512
 
18095
17513
  // server/routes/access/request-magic-link.ts
18096
- var TAG37 = "[access-request-link]";
17514
+ var TAG33 = "[access-request-link]";
18097
17515
  var app46 = new Hono();
18098
17516
  var VISITOR_MESSAGE = "If that email is on the invite list, a fresh link is on the way.";
18099
17517
  app46.post("/", async (c) => {
@@ -18110,18 +17528,18 @@ app46.post("/", async (c) => {
18110
17528
  }
18111
17529
  const rateMsg = checkRequestLinkRateLimit(contactValue);
18112
17530
  if (rateMsg) {
18113
- console.error(`${TAG37} contactValue=${maskContact(contactValue)} result=rate-limited`);
17531
+ console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=rate-limited`);
18114
17532
  return c.json({ error: rateMsg }, 429);
18115
17533
  }
18116
17534
  recordRequestLinkAttempt(contactValue);
18117
17535
  const accountId = process.env.ACCOUNT_ID ?? "";
18118
17536
  if (!accountId) {
18119
- console.error(`${TAG37} contactValue=${maskContact(contactValue)} result=no-account-id`);
17537
+ console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=no-account-id`);
18120
17538
  return c.json({ message: VISITOR_MESSAGE }, 200);
18121
17539
  }
18122
17540
  const grant = await findActiveGrantByContact(contactValue, agentSlug, accountId);
18123
17541
  if (!grant) {
18124
- console.log(`${TAG37} contactValue=${maskContact(contactValue)} result=notfound`);
17542
+ console.log(`${TAG33} contactValue=${maskContact(contactValue)} result=notfound`);
18125
17543
  return c.json({ message: VISITOR_MESSAGE }, 200);
18126
17544
  }
18127
17545
  let token;
@@ -18129,7 +17547,7 @@ app46.post("/", async (c) => {
18129
17547
  token = await generateNewMagicToken(grant.grantId);
18130
17548
  } catch (err) {
18131
17549
  console.error(
18132
- `${TAG37} contactValue=${maskContact(contactValue)} result=mint-failed err="${err instanceof Error ? err.message : String(err)}"`
17550
+ `${TAG33} contactValue=${maskContact(contactValue)} result=mint-failed err="${err instanceof Error ? err.message : String(err)}"`
18133
17551
  );
18134
17552
  return c.json({ message: VISITOR_MESSAGE }, 200);
18135
17553
  }
@@ -18161,12 +17579,12 @@ app46.post("/", async (c) => {
18161
17579
  });
18162
17580
  if (!sendResult.ok) {
18163
17581
  console.error(
18164
- `${TAG37} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
17582
+ `${TAG33} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
18165
17583
  );
18166
17584
  return c.json({ message: VISITOR_MESSAGE }, 200);
18167
17585
  }
18168
17586
  console.log(
18169
- `${TAG37} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
17587
+ `${TAG33} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
18170
17588
  );
18171
17589
  return c.json({ message: VISITOR_MESSAGE }, 200);
18172
17590
  });
@@ -18179,8 +17597,8 @@ app47.route("/request-magic-link", request_magic_link_default);
18179
17597
  var access_default = app47;
18180
17598
 
18181
17599
  // server/routes/sites.ts
18182
- import { existsSync as existsSync27, readFileSync as readFileSync29, realpathSync as realpathSync7, statSync as statSync11 } from "fs";
18183
- import { resolve as resolve28 } from "path";
17600
+ import { existsSync as existsSync27, readFileSync as readFileSync29, realpathSync as realpathSync5, statSync as statSync11 } from "fs";
17601
+ import { resolve as resolve25 } from "path";
18184
17602
  var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
18185
17603
  var MIME = {
18186
17604
  ".html": "text/html; charset=utf-8",
@@ -18237,8 +17655,8 @@ app48.get("/:rel{.*}", (c) => {
18237
17655
  }
18238
17656
  segments.push(seg);
18239
17657
  }
18240
- const rootDir = resolve28(account.accountDir, "sites");
18241
- let filePath = segments.length === 0 ? rootDir : resolve28(rootDir, ...segments);
17658
+ const rootDir = resolve25(account.accountDir, "sites");
17659
+ let filePath = segments.length === 0 ? rootDir : resolve25(rootDir, ...segments);
18242
17660
  if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
18243
17661
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
18244
17662
  return c.text("Forbidden", 403);
@@ -18258,7 +17676,7 @@ app48.get("/:rel{.*}", (c) => {
18258
17676
  return c.redirect(target, 301);
18259
17677
  }
18260
17678
  if (stat8?.isDirectory()) {
18261
- filePath = resolve28(filePath, "index.html");
17679
+ filePath = resolve25(filePath, "index.html");
18262
17680
  }
18263
17681
  if (!filePath.startsWith(rootDir + "/")) {
18264
17682
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
@@ -18271,8 +17689,8 @@ app48.get("/:rel{.*}", (c) => {
18271
17689
  let realPath;
18272
17690
  let realRoot;
18273
17691
  try {
18274
- realPath = realpathSync7(filePath);
18275
- realRoot = realpathSync7(rootDir);
17692
+ realPath = realpathSync5(filePath);
17693
+ realRoot = realpathSync5(rootDir);
18276
17694
  } catch {
18277
17695
  console.error(`[sites] not-found path=${reqPath} status=404`);
18278
17696
  return c.text("Not found", 404);
@@ -18895,14 +18313,14 @@ async function writeEvent(opts) {
18895
18313
  var visitor_event_default = app51;
18896
18314
 
18897
18315
  // server/routes/session.ts
18898
- import { resolve as resolve29 } from "path";
18316
+ import { resolve as resolve26 } from "path";
18899
18317
  import { existsSync as existsSync29, writeFileSync as writeFileSync12, mkdirSync as mkdirSync7 } from "fs";
18900
18318
  var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18901
18319
  function writeBrandingCache(accountId, agentSlug, branding) {
18902
18320
  try {
18903
- const cacheDir = resolve29(MAXY_DIR, "branding-cache", accountId);
18321
+ const cacheDir = resolve26(MAXY_DIR, "branding-cache", accountId);
18904
18322
  mkdirSync7(cacheDir, { recursive: true });
18905
- writeFileSync12(resolve29(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
18323
+ writeFileSync12(resolve26(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
18906
18324
  } catch (err) {
18907
18325
  console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
18908
18326
  }
@@ -18979,8 +18397,8 @@ app52.post("/", async (c) => {
18979
18397
  }
18980
18398
  let agentConfig = null;
18981
18399
  if (account) {
18982
- const agentDir = resolve29(account.accountDir, "agents", agentSlug);
18983
- if (!existsSync29(agentDir) || !existsSync29(resolve29(agentDir, "config.json"))) {
18400
+ const agentDir = resolve26(account.accountDir, "agents", agentSlug);
18401
+ if (!existsSync29(agentDir) || !existsSync29(resolve26(agentDir, "config.json"))) {
18984
18402
  return c.json({ error: "Agent not found" }, 404);
18985
18403
  }
18986
18404
  agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
@@ -19342,7 +18760,7 @@ function startGraphHealthTimer() {
19342
18760
 
19343
18761
  // app/lib/file-watcher.ts
19344
18762
  import * as fsp2 from "fs/promises";
19345
- import { resolve as resolve30, sep as sep8 } from "path";
18763
+ import { resolve as resolve27, sep as sep8 } from "path";
19346
18764
  var ACCOUNT_UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19347
18765
  var DEFAULT_COALESCE_MS = 500;
19348
18766
  var ROOTS = ["accounts"];
@@ -19361,7 +18779,7 @@ async function startFileWatcher(opts = {}) {
19361
18779
  const dropFn = opts.drop ?? dropFileIndex;
19362
18780
  for (const r of ROOTS) {
19363
18781
  try {
19364
- await fsp2.mkdir(resolve30(dataRoot, r), { recursive: true });
18782
+ await fsp2.mkdir(resolve27(dataRoot, r), { recursive: true });
19365
18783
  } catch (err) {
19366
18784
  console.error(
19367
18785
  `[file-watcher] start-failed root="${r}" err="${err.message}" \u2014 index will be maintained by the 5-min reconcile backstop only`
@@ -19382,7 +18800,7 @@ async function startFileWatcher(opts = {}) {
19382
18800
  timers.set(relativePath, t);
19383
18801
  }
19384
18802
  async function runHook(relativePath, accountId) {
19385
- const absolute = resolve30(dataRoot, relativePath);
18803
+ const absolute = resolve27(dataRoot, relativePath);
19386
18804
  let exists = false;
19387
18805
  try {
19388
18806
  const st = await fsp2.stat(absolute);
@@ -19406,7 +18824,7 @@ async function startFileWatcher(opts = {}) {
19406
18824
  }
19407
18825
  }
19408
18826
  async function watchRoot(rootName) {
19409
- const absRoot = resolve30(dataRoot, rootName);
18827
+ const absRoot = resolve27(dataRoot, rootName);
19410
18828
  try {
19411
18829
  const iter = fsp2.watch(absRoot, { recursive: true, signal: controller.signal });
19412
18830
  for await (const event of iter) {
@@ -19445,7 +18863,7 @@ async function startFileWatcher(opts = {}) {
19445
18863
 
19446
18864
  // app/lib/migrate-uploads.ts
19447
18865
  import { mkdir as mkdir5, readdir as readdir5, rename as rename2, rm as rm3 } from "fs/promises";
19448
- import { dirname as dirname8, relative as relative6, resolve as resolve31 } from "path";
18866
+ import { dirname as dirname8, relative as relative6, resolve as resolve28 } from "path";
19449
18867
  var ACCOUNT_UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19450
18868
  async function walkFiles(dir) {
19451
18869
  const out = [];
@@ -19456,7 +18874,7 @@ async function walkFiles(dir) {
19456
18874
  return out;
19457
18875
  }
19458
18876
  for (const e of entries) {
19459
- const abs = resolve31(dir, e.name);
18877
+ const abs = resolve28(dir, e.name);
19460
18878
  if (e.isDirectory()) {
19461
18879
  out.push(...await walkFiles(abs));
19462
18880
  } else if (e.isFile()) {
@@ -19476,7 +18894,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
19476
18894
  let moved = 0;
19477
18895
  for (const oldAbs of files) {
19478
18896
  const suffix = relative6(srcDir, oldAbs);
19479
- const newAbs = resolve31(destDir, suffix);
18897
+ const newAbs = resolve28(destDir, suffix);
19480
18898
  await mkdir5(dirname8(newAbs), { recursive: true });
19481
18899
  await rename2(oldAbs, newAbs);
19482
18900
  moved++;
@@ -19503,7 +18921,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
19503
18921
  }
19504
18922
  async function migrateUploads(opts = {}) {
19505
18923
  const dataRoot = opts.dataRoot ?? DATA_ROOT;
19506
- const oldRoot = resolve31(dataRoot, "uploads");
18924
+ const oldRoot = resolve28(dataRoot, "uploads");
19507
18925
  let topEntries;
19508
18926
  try {
19509
18927
  topEntries = await readdir5(oldRoot, { withFileTypes: true });
@@ -19524,8 +18942,8 @@ async function migrateUploads(opts = {}) {
19524
18942
  const name = entry.name;
19525
18943
  if (ACCOUNT_UUID_RE4.test(name)) {
19526
18944
  moved += await relocateTree(
19527
- resolve31(oldRoot, name),
19528
- resolve31(dataRoot, "accounts", name, "uploads"),
18945
+ resolve28(oldRoot, name),
18946
+ resolve28(dataRoot, "accounts", name, "uploads"),
19529
18947
  dataRoot,
19530
18948
  name,
19531
18949
  session
@@ -19537,8 +18955,8 @@ async function migrateUploads(opts = {}) {
19537
18955
  continue;
19538
18956
  }
19539
18957
  moved += await relocateTree(
19540
- resolve31(oldRoot, "public"),
19541
- resolve31(dataRoot, "accounts", installAccountId, "uploads", "public"),
18958
+ resolve28(oldRoot, "public"),
18959
+ resolve28(dataRoot, "accounts", installAccountId, "uploads", "public"),
19542
18960
  dataRoot,
19543
18961
  null,
19544
18962
  // public uploads carry no graph nodes
@@ -19567,7 +18985,7 @@ async function migrateUploads(opts = {}) {
19567
18985
  }
19568
18986
 
19569
18987
  // app/lib/migrate-admin-webchat-sidecars.ts
19570
- import { readdir as readdir6, readFile as readFile7, rename as rename3, writeFile as writeFile4, unlink as unlink3 } from "fs/promises";
18988
+ import { readdir as readdir6, readFile as readFile6, rename as rename3, writeFile as writeFile4, unlink as unlink3 } from "fs/promises";
19571
18989
  import { existsSync as existsSync30 } from "fs";
19572
18990
  import { join as join30 } from "path";
19573
18991
  var ADMIN_ROLE2 = "admin";
@@ -19580,7 +18998,7 @@ var defaultResolveName = async (accountId, userId) => {
19580
18998
  async function readRaw2(path2) {
19581
18999
  let raw;
19582
19000
  try {
19583
- raw = await readFile7(path2, "utf8");
19001
+ raw = await readFile6(path2, "utf8");
19584
19002
  } catch {
19585
19003
  return null;
19586
19004
  }
@@ -20549,16 +19967,16 @@ var WebchatGateway = class _WebchatGateway {
20549
19967
  * The public /api/chat route awaits this to return the reply on the same SSE
20550
19968
  * response, preserving the pre-756 POST→single-blob browser contract. */
20551
19969
  awaitReply(key, timeoutMs) {
20552
- return new Promise((resolve35) => {
19970
+ return new Promise((resolve33) => {
20553
19971
  const timer2 = setTimeout(() => {
20554
19972
  this.replyAwaiters.delete(key);
20555
- resolve35({ timeout: true });
19973
+ resolve33({ timeout: true });
20556
19974
  }, timeoutMs);
20557
19975
  if (timer2.unref) timer2.unref();
20558
19976
  this.replyAwaiters.set(key, (r) => {
20559
19977
  clearTimeout(timer2);
20560
19978
  this.replyAwaiters.delete(key);
20561
- resolve35(r);
19979
+ resolve33(r);
20562
19980
  });
20563
19981
  });
20564
19982
  }
@@ -20569,11 +19987,11 @@ var WebchatGateway = class _WebchatGateway {
20569
19987
  * terminal dialog stays answerable). */
20570
19988
  awaitPermissionVerdict(p, timeoutMs) {
20571
19989
  const k = _WebchatGateway.promptKey(p.key, p.requestId);
20572
- return new Promise((resolve35) => {
19990
+ return new Promise((resolve33) => {
20573
19991
  this.pendingPrompts.get(k)?.resolve({ timeout: true });
20574
19992
  const timer2 = setTimeout(() => {
20575
19993
  this.pendingPrompts.delete(k);
20576
- resolve35({ timeout: true });
19994
+ resolve33({ timeout: true });
20577
19995
  }, timeoutMs);
20578
19996
  if (timer2.unref) timer2.unref();
20579
19997
  this.pendingPrompts.set(k, {
@@ -20582,7 +20000,7 @@ var WebchatGateway = class _WebchatGateway {
20582
20000
  resolve: (r) => {
20583
20001
  clearTimeout(timer2);
20584
20002
  this.pendingPrompts.delete(k);
20585
- resolve35(r);
20003
+ resolve33(r);
20586
20004
  }
20587
20005
  });
20588
20006
  console.error(`[webchat:perm] op=open key=${keyDisplay(p.key)} id=${p.requestId} tool=${p.toolName}`);
@@ -20654,6 +20072,45 @@ var WebchatGateway = class _WebchatGateway {
20654
20072
  if (handle.unref) handle.unref();
20655
20073
  return handle;
20656
20074
  }
20075
+ /** A gated public PTY exited explicitly on the manager side (operator stop,
20076
+ * claude crash, manager DELETE), not via the idle reaper. The manager POSTs
20077
+ * the sessionId to the loopback /api/admin/public-session-exit route, which
20078
+ * reaches this through the process-wide gateway handle. The slice ringfence
20079
+ * the reviewer needs lives only in this gateway's publicSessions map — the
20080
+ * manager has none of it — so resolve the record here and dispatch the same
20081
+ * reviewer the idle reaper uses, tagged dispatchFor='exit'. Dedup is internal
20082
+ * to firePublicSessionEndReview, so a session already reviewed by the reaper
20083
+ * (or a racing respawn) is a no-op.
20084
+ *
20085
+ * Only gated records (non-empty slice) are reviewed and evicted here, the
20086
+ * same gate the reaper applies. An open-mode public record is left for the
20087
+ * idle reaper. No deleteSession — the PTY is already gone and the JSONL is
20088
+ * the audit record the reviewer just read. */
20089
+ handlePublicSessionExit(sessionId) {
20090
+ let found = null;
20091
+ for (const [key2, rec2] of this.publicSessions) {
20092
+ if (rec2.sessionId === sessionId) {
20093
+ found = { key: key2, rec: rec2 };
20094
+ break;
20095
+ }
20096
+ }
20097
+ if (!found) {
20098
+ console.error(`[webchat:inbound] op=public-exit sessionId=${sessionId.slice(0, 8)} result=no-record`);
20099
+ return;
20100
+ }
20101
+ const { key, rec } = found;
20102
+ if (!rec.sliceToken || rec.sliceToken.length === 0) return;
20103
+ this.publicSessions.delete(key);
20104
+ console.error(`[webchat:inbound] op=public-exit key=${keyDisplay(key)} sessionId=${sessionId.slice(0, 8)} result=review`);
20105
+ void this.deps.firePublicSessionEndReview?.({
20106
+ sessionId: rec.sessionId,
20107
+ sliceToken: rec.sliceToken,
20108
+ personId: rec.personId ?? null,
20109
+ accountId: rec.accountId,
20110
+ agentSlug: rec.agentSlug,
20111
+ dispatchFor: "exit"
20112
+ });
20113
+ }
20657
20114
  /** Hono sub-app exposing the loopback channel routes; mount on the UI app. */
20658
20115
  routes() {
20659
20116
  return createWebchatChannelRoutes({
@@ -20770,97 +20227,385 @@ var WebchatGateway = class _WebchatGateway {
20770
20227
  );
20771
20228
  }
20772
20229
  }
20773
- for (const key of this.recoverable.keys()) {
20774
- if (!this.hub.hasInFlight(key)) this.recoverable.delete(key);
20775
- }
20776
- if (this.recoveryAttempted.size > 0) {
20777
- const liveIds = this.hub.inFlightIds();
20778
- for (const attemptKey of this.recoveryAttempted) {
20779
- if (!liveIds.has(attemptKey)) this.recoveryAttempted.delete(attemptKey);
20780
- }
20230
+ for (const key of this.recoverable.keys()) {
20231
+ if (!this.hub.hasInFlight(key)) this.recoverable.delete(key);
20232
+ }
20233
+ if (this.recoveryAttempted.size > 0) {
20234
+ const liveIds = this.hub.inFlightIds();
20235
+ for (const attemptKey of this.recoveryAttempted) {
20236
+ if (!liveIds.has(attemptKey)) this.recoveryAttempted.delete(attemptKey);
20237
+ }
20238
+ }
20239
+ } finally {
20240
+ this.recovering = false;
20241
+ }
20242
+ }
20243
+ /** Handle one inbound webchat message (browser POST → here). */
20244
+ async handleInbound(input) {
20245
+ const bytes = Buffer.byteLength(input.text, "utf8");
20246
+ const hadSubscriber = this.hub.hasSubscriber(input.key);
20247
+ const willDeliverNow = this.hub.isReady(input.key);
20248
+ this.hub.deliver(
20249
+ { key: input.key, text: input.text, messageId: `wc-${++this.seq}` },
20250
+ Date.now()
20251
+ );
20252
+ if (willDeliverNow) this.lastDeliveredAt.set(input.key, Date.now());
20253
+ console.error(
20254
+ `[webchat:inbound] op=received key=${keyDisplay(input.key)} role=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
20255
+ );
20256
+ if (input.role === "admin") {
20257
+ this.recoverable.set(input.key, {
20258
+ accountId: input.accountId,
20259
+ key: input.key,
20260
+ role: input.role,
20261
+ ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
20262
+ ...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {}
20263
+ });
20264
+ }
20265
+ const existing = this.publicSessions.get(input.key);
20266
+ if (existing) existing.lastActivityAt = Date.now();
20267
+ if (!hadSubscriber && !this.spawning.has(input.key)) {
20268
+ this.spawning.add(input.key);
20269
+ console.error(`[webchat:inbound] op=spawn-trigger key=${input.key} role=${input.role}`);
20270
+ try {
20271
+ const spawned = await this.deps.ensureChannelSession({
20272
+ accountId: input.accountId,
20273
+ key: input.key,
20274
+ role: input.role,
20275
+ gatewayUrl: this.deps.gatewayUrl,
20276
+ serverPath: this.deps.serverPath,
20277
+ // Task 800 — only present on a session-targeted launch, so the
20278
+ // default-path call shape stays byte-identical.
20279
+ ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
20280
+ // The authenticated admin's userId on an admin bootstrap inbound; the
20281
+ // default-path call shape stays byte-identical when absent.
20282
+ ...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {},
20283
+ // Task 756 — public spawn params on a role:'public' inbound.
20284
+ ...input.public !== void 0 ? { public: input.public } : {}
20285
+ });
20286
+ if (input.role === "public" && input.public && spawned && "sessionId" in spawned) {
20287
+ this.publicSessions.set(input.key, {
20288
+ sessionId: spawned.sessionId,
20289
+ accountId: input.accountId,
20290
+ agentSlug: input.public.agentSlug,
20291
+ sliceToken: input.public.sliceToken,
20292
+ personId: input.public.personId,
20293
+ lastActivityAt: Date.now()
20294
+ });
20295
+ }
20296
+ } finally {
20297
+ this.spawning.delete(input.key);
20298
+ }
20299
+ }
20300
+ }
20301
+ };
20302
+
20303
+ // app/lib/webchat/gateway/public-spawn-request.ts
20304
+ function buildPublicWebchatSpawnRequest(input) {
20305
+ const { agentSlug, sliceToken, personId, visitorId, name } = input.public;
20306
+ return {
20307
+ senderId: input.key,
20308
+ role: "public",
20309
+ channel: "webchat",
20310
+ accountId: input.accountId,
20311
+ agentSlug,
20312
+ model: SONNET_MODEL,
20313
+ // sliceToken/personId are present only for a gated visitor; open-mode
20314
+ // public agents carry neither (mirrors the bridge's open-mode key).
20315
+ ...sliceToken ? { sliceToken } : {},
20316
+ ...personId ? { personId } : {},
20317
+ ...visitorId ? { visitorId } : {},
20318
+ ...input.previousContext ? { previousContext: input.previousContext } : {},
20319
+ name,
20320
+ webchatChannel: { key: input.key, gatewayUrl: input.gatewayUrl, serverPath: input.serverPath }
20321
+ };
20322
+ }
20323
+
20324
+ // app/lib/channel-pty-bridge/follower.ts
20325
+ function followerPendingMaxMs() {
20326
+ return Number(process.env.CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS ?? String(3e5));
20327
+ }
20328
+ function followerRetryMs() {
20329
+ return Number(process.env.CHANNEL_PTY_FOLLOWER_RETRY_MS ?? String(1e3));
20330
+ }
20331
+ function startFollower(opts) {
20332
+ const abort = new AbortController();
20333
+ const { entry, tag } = opts;
20334
+ void (async () => {
20335
+ try {
20336
+ const sid = entry.sessionId.slice(0, 8);
20337
+ const deadline = Date.now() + followerPendingMaxMs();
20338
+ const retryMs = followerRetryMs();
20339
+ let res;
20340
+ let attempt = 0;
20341
+ for (; ; ) {
20342
+ res = await fetch(
20343
+ managerLogFollowUrl(entry.sessionId, { boundary: opts.suppressResumeReplay === true }),
20344
+ { signal: abort.signal }
20345
+ );
20346
+ console.error(`${tag} follower-connect sessionId=${sid} status=${res.status}`);
20347
+ if (res.status === 202) {
20348
+ attempt += 1;
20349
+ await res.body?.cancel().catch(() => {
20350
+ });
20351
+ if (abort.signal.aborted) return;
20352
+ if (Date.now() >= deadline) {
20353
+ console.error(`${tag} follower-give-up sessionId=${sid} reason=pending-timeout attempts=${attempt}`);
20354
+ opts.onError?.("follow-pending-timeout");
20355
+ return;
20356
+ }
20357
+ console.error(`${tag} follower-retry sessionId=${sid} attempt=${attempt} reason=pending`);
20358
+ await new Promise((r) => setTimeout(r, retryMs));
20359
+ if (abort.signal.aborted) return;
20360
+ continue;
20361
+ }
20362
+ break;
20363
+ }
20364
+ if (!res.ok || !res.body) {
20365
+ opts.onError?.(`follow-status-${res.status}`);
20366
+ return;
20367
+ }
20368
+ console.error(`${tag} follower-open sessionId=${sid}`);
20369
+ const reader = res.body.getReader();
20370
+ const decoder = new TextDecoder("utf8");
20371
+ let buffered = "";
20372
+ const fileDelivery = opts.fileDelivery ?? null;
20373
+ let firedFileTools = [];
20374
+ let suppressing = opts.suppressResumeReplay === true;
20375
+ let discardedTurns = 0;
20376
+ let discardedFileTools = 0;
20377
+ while (!abort.signal.aborted) {
20378
+ const { value, done } = await reader.read();
20379
+ if (done) break;
20380
+ buffered += decoder.decode(value, { stream: true });
20381
+ let nl = buffered.indexOf("\n");
20382
+ while (nl !== -1) {
20383
+ const line = buffered.slice(0, nl);
20384
+ buffered = buffered.slice(nl + 1);
20385
+ nl = buffered.indexOf("\n");
20386
+ if (!line) continue;
20387
+ let event;
20388
+ try {
20389
+ event = JSON.parse(line);
20390
+ } catch (err) {
20391
+ console.error(
20392
+ `${tag} jsonl-parse-skip sessionId=${entry.sessionId.slice(0, 8)} bytes=${line.length} message=${err instanceof Error ? err.message : String(err)}`
20393
+ );
20394
+ continue;
20395
+ }
20396
+ if (event.type === "__channel_resume_boundary__") {
20397
+ if (suppressing) {
20398
+ suppressing = false;
20399
+ console.error(
20400
+ `${tag} follower-resume-boundary sessionId=${sid} discarded-head-turns=${discardedTurns} discarded-head-filetools=${discardedFileTools}`
20401
+ );
20402
+ }
20403
+ continue;
20404
+ }
20405
+ if (event.type === "user") {
20406
+ entry.pendingTurnText = "";
20407
+ firedFileTools = [];
20408
+ continue;
20409
+ }
20410
+ if (event.type !== "assistant") continue;
20411
+ const msg = event.message;
20412
+ if (!msg) continue;
20413
+ let sawContent = false;
20414
+ if (Array.isArray(msg.content)) {
20415
+ for (const block of msg.content) {
20416
+ if (block?.type === "text" && typeof block.text === "string") {
20417
+ entry.pendingTurnText += block.text;
20418
+ sawContent = true;
20419
+ } else if (block?.type === "tool_use") {
20420
+ sawContent = true;
20421
+ if (fileDelivery && typeof block.name === "string" && fileDelivery.isFileDeliveryTool(block.name)) {
20422
+ if (suppressing) {
20423
+ discardedFileTools += 1;
20424
+ continue;
20425
+ }
20426
+ firedFileTools.push(block.name);
20427
+ try {
20428
+ await fileDelivery.onFileToolUse({ toolName: block.name, input: block.input });
20429
+ } catch (err) {
20430
+ console.error(
20431
+ `${tag} file-delivery-error sessionId=${sid} tool=${block.name} message=${err instanceof Error ? err.message : String(err)}`
20432
+ );
20433
+ }
20434
+ }
20435
+ }
20436
+ }
20437
+ }
20438
+ if (msg.stop_reason === "end_turn") {
20439
+ if (suppressing) {
20440
+ entry.pendingTurnText = "";
20441
+ firedFileTools = [];
20442
+ discardedTurns += 1;
20443
+ continue;
20444
+ }
20445
+ const flush = entry.pendingTurnText;
20446
+ entry.pendingTurnText = "";
20447
+ if (flush.trim()) {
20448
+ await fanOut(entry.subscribers, flush, opts.onError, tag);
20449
+ }
20450
+ if (fileDelivery && firedFileTools.length > 0) {
20451
+ const fired = firedFileTools;
20452
+ firedFileTools = [];
20453
+ try {
20454
+ fileDelivery.onTurnEnd(fired);
20455
+ } catch (err) {
20456
+ console.error(
20457
+ `${tag} file-delivery-error sessionId=${sid} phase=turn-end message=${err instanceof Error ? err.message : String(err)}`
20458
+ );
20459
+ }
20460
+ }
20461
+ opts.onTurnComplete?.();
20462
+ } else if (sawContent && !suppressing) {
20463
+ opts.onActivity?.();
20464
+ }
20465
+ }
20466
+ }
20467
+ } catch (err) {
20468
+ if (!abort.signal.aborted) {
20469
+ opts.onError?.(`follower-error: ${err instanceof Error ? err.message : String(err)}`);
20781
20470
  }
20782
20471
  } finally {
20783
- this.recovering = false;
20784
- }
20785
- }
20786
- /** Handle one inbound webchat message (browser POST → here). */
20787
- async handleInbound(input) {
20788
- const bytes = Buffer.byteLength(input.text, "utf8");
20789
- const hadSubscriber = this.hub.hasSubscriber(input.key);
20790
- const willDeliverNow = this.hub.isReady(input.key);
20791
- this.hub.deliver(
20792
- { key: input.key, text: input.text, messageId: `wc-${++this.seq}` },
20793
- Date.now()
20794
- );
20795
- if (willDeliverNow) this.lastDeliveredAt.set(input.key, Date.now());
20796
- console.error(
20797
- `[webchat:inbound] op=received key=${keyDisplay(input.key)} role=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
20798
- );
20799
- if (input.role === "admin") {
20800
- this.recoverable.set(input.key, {
20801
- accountId: input.accountId,
20802
- key: input.key,
20803
- role: input.role,
20804
- ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
20805
- ...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {}
20806
- });
20472
+ opts.onClose();
20807
20473
  }
20808
- const existing = this.publicSessions.get(input.key);
20809
- if (existing) existing.lastActivityAt = Date.now();
20810
- if (!hadSubscriber && !this.spawning.has(input.key)) {
20811
- this.spawning.add(input.key);
20812
- console.error(`[webchat:inbound] op=spawn-trigger key=${input.key} role=${input.role}`);
20813
- try {
20814
- const spawned = await this.deps.ensureChannelSession({
20815
- accountId: input.accountId,
20816
- key: input.key,
20817
- role: input.role,
20818
- gatewayUrl: this.deps.gatewayUrl,
20819
- serverPath: this.deps.serverPath,
20820
- // Task 800 — only present on a session-targeted launch, so the
20821
- // default-path call shape stays byte-identical.
20822
- ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
20823
- // The authenticated admin's userId on an admin bootstrap inbound; the
20824
- // default-path call shape stays byte-identical when absent.
20825
- ...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {},
20826
- // Task 756 — public spawn params on a role:'public' inbound.
20827
- ...input.public !== void 0 ? { public: input.public } : {}
20828
- });
20829
- if (input.role === "public" && input.public && spawned && "sessionId" in spawned) {
20830
- this.publicSessions.set(input.key, {
20831
- sessionId: spawned.sessionId,
20832
- accountId: input.accountId,
20833
- agentSlug: input.public.agentSlug,
20834
- sliceToken: input.public.sliceToken,
20835
- personId: input.public.personId,
20836
- lastActivityAt: Date.now()
20837
- });
20838
- }
20839
- } finally {
20840
- this.spawning.delete(input.key);
20474
+ })();
20475
+ return abort;
20476
+ }
20477
+ async function fanOut(subscribers, text, onError, tag) {
20478
+ const callbacks = Array.from(subscribers);
20479
+ await Promise.all(
20480
+ callbacks.map(
20481
+ (cb) => Promise.resolve().then(() => cb(text)).catch((err) => {
20482
+ const m = err instanceof Error ? err.message : String(err);
20483
+ console.error(`${tag} subscriber-error message=${m}`);
20484
+ onError?.(`subscriber-error: ${m}`);
20485
+ })
20486
+ )
20487
+ );
20488
+ }
20489
+
20490
+ // app/lib/channel-pty-bridge/file-delivery.ts
20491
+ var SEND_USER_FILE = "SendUserFile";
20492
+ function makeFileDelivery(opts) {
20493
+ const { entry, tag, channel, sendFile } = opts;
20494
+ let failedFiles = [];
20495
+ let attempts = 0;
20496
+ return {
20497
+ isFileDeliveryTool(toolName) {
20498
+ return toolName === SEND_USER_FILE;
20499
+ },
20500
+ async onFileToolUse(use) {
20501
+ if (use.toolName !== SEND_USER_FILE) return;
20502
+ const input = use.input ?? {};
20503
+ const files = Array.isArray(input.files) ? input.files.filter((f) => typeof f === "string") : [];
20504
+ const caption = typeof input.caption === "string" ? input.caption : void 0;
20505
+ for (let i = 0; i < files.length; i++) {
20506
+ attempts++;
20507
+ const result = await sendFile(files[i], i === 0 ? caption : void 0);
20508
+ const name = files[i].split("/").pop() ?? files[i];
20509
+ console.error(`${tag} op=file-forward channel=${channel} file=${name} outcome=${result.ok ? "ok" : "fail"}`);
20510
+ if (!result.ok) failedFiles.push(files[i]);
20511
+ }
20512
+ },
20513
+ onTurnEnd(firedTools) {
20514
+ const failed = failedFiles;
20515
+ const tried = attempts;
20516
+ failedFiles = [];
20517
+ attempts = 0;
20518
+ const sid = entry.sessionId.slice(0, 8);
20519
+ for (const file of failed) {
20520
+ console.error(
20521
+ `${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE} file=${file}`
20522
+ );
20523
+ }
20524
+ if (firedTools.includes(SEND_USER_FILE) && tried === 0) {
20525
+ console.error(
20526
+ `${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE}`
20527
+ );
20841
20528
  }
20842
20529
  }
20843
- }
20844
- };
20530
+ };
20531
+ }
20845
20532
 
20846
- // app/lib/webchat/gateway/public-spawn-request.ts
20847
- function buildPublicWebchatSpawnRequest(input) {
20848
- const { agentSlug, sliceToken, personId, visitorId, name } = input.public;
20533
+ // app/lib/whatsapp/inbound/file-delivery-bridge.ts
20534
+ var TAG34 = "[whatsapp-adaptor]";
20535
+ var SEND_USER_FILE2 = "SendUserFile";
20536
+ var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
20537
+ function platformRoot() {
20538
+ return process.env.MAXY_PLATFORM_ROOT || "";
20539
+ }
20540
+ function makeWhatsAppSendFile(entry) {
20541
+ return async (filePath, caption) => {
20542
+ let maxyAccountId;
20543
+ try {
20544
+ maxyAccountId = resolvePlatformAccountId();
20545
+ } catch (err) {
20546
+ console.error(
20547
+ `${TAG34} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
20548
+ );
20549
+ return { ok: false, error: "account-unresolved" };
20550
+ }
20551
+ const result = await sendWhatsAppDocument({
20552
+ to: entry.senderId,
20553
+ filePath,
20554
+ caption,
20555
+ accountId: entry.accountId,
20556
+ maxyAccountId,
20557
+ platformRoot: platformRoot()
20558
+ });
20559
+ if (result.ok) return { ok: true };
20560
+ console.error(
20561
+ `${TAG34} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
20562
+ );
20563
+ return { ok: false, error: result.error };
20564
+ };
20565
+ }
20566
+ function makeWhatsAppFileDelivery(entry) {
20567
+ const shared = makeFileDelivery({
20568
+ entry,
20569
+ tag: TAG34,
20570
+ channel: "whatsapp",
20571
+ sendFile: makeWhatsAppSendFile(entry)
20572
+ });
20573
+ let turnStartedAt = null;
20574
+ let routeCalls = [];
20849
20575
  return {
20850
- senderId: input.key,
20851
- role: "public",
20852
- channel: "webchat",
20853
- accountId: input.accountId,
20854
- agentSlug,
20855
- model: SONNET_MODEL,
20856
- // sliceToken/personId are present only for a gated visitor; open-mode
20857
- // public agents carry neither (mirrors the bridge's open-mode key).
20858
- ...sliceToken ? { sliceToken } : {},
20859
- ...personId ? { personId } : {},
20860
- ...visitorId ? { visitorId } : {},
20861
- ...input.previousContext ? { previousContext: input.previousContext } : {},
20862
- name,
20863
- webchatChannel: { key: input.key, gatewayUrl: input.gatewayUrl, serverPath: input.serverPath }
20576
+ isFileDeliveryTool(toolName) {
20577
+ return toolName === SEND_USER_FILE2 || toolName === WHATSAPP_SEND_DOCUMENT;
20578
+ },
20579
+ async onFileToolUse(use) {
20580
+ if (turnStartedAt === null) turnStartedAt = Date.now();
20581
+ if (use.toolName === WHATSAPP_SEND_DOCUMENT) {
20582
+ const input = use.input ?? {};
20583
+ routeCalls.push({
20584
+ to: typeof input.to === "string" ? input.to : void 0,
20585
+ filePath: typeof input.filePath === "string" ? input.filePath : void 0
20586
+ });
20587
+ return;
20588
+ }
20589
+ await shared.onFileToolUse(use);
20590
+ },
20591
+ onTurnEnd(firedTools) {
20592
+ const startedAt = turnStartedAt ?? 0;
20593
+ const routes = routeCalls;
20594
+ turnStartedAt = null;
20595
+ routeCalls = [];
20596
+ const sid = entry.sessionId.slice(0, 8);
20597
+ shared.onTurnEnd(firedTools);
20598
+ for (const call of routes) {
20599
+ const routeAt = call.to !== void 0 && call.filePath !== void 0 ? routeDocumentOutboundAt(call.to, call.filePath) : void 0;
20600
+ const delivered = routeAt !== void 0 && routeAt >= startedAt;
20601
+ if (!delivered) {
20602
+ const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
20603
+ console.error(
20604
+ `${TAG34} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
20605
+ );
20606
+ }
20607
+ }
20608
+ }
20864
20609
  };
20865
20610
  }
20866
20611
 
@@ -21211,6 +20956,122 @@ function buildTelegramSpawnRequest(input) {
21211
20956
  };
21212
20957
  }
21213
20958
 
20959
+ // app/lib/telegram/outbound/send-document.ts
20960
+ import { realpathSync as realpathSync6 } from "fs";
20961
+ import { readFile as readFile7, stat as stat7 } from "fs/promises";
20962
+ import { resolve as resolve29, basename as basename9 } from "path";
20963
+ var TAG35 = "[telegram:outbound]";
20964
+ var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
20965
+ async function sendTelegramDocument(input) {
20966
+ const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot3 } = input;
20967
+ if (!botToken || !filePath) {
20968
+ return { ok: false, status: 400, error: "Missing required fields: botToken, filePath" };
20969
+ }
20970
+ if (!maxyAccountId || !platformRoot3) {
20971
+ return { ok: false, status: 400, error: "Cannot validate file path: missing account or platform context" };
20972
+ }
20973
+ const accountDir = resolve29(platformRoot3, "..", "data/accounts", maxyAccountId);
20974
+ let resolvedPath;
20975
+ try {
20976
+ resolvedPath = realpathSync6(filePath);
20977
+ const accountResolved = realpathSync6(accountDir);
20978
+ if (!resolvedPath.startsWith(accountResolved + "/")) {
20979
+ console.error(`${TAG35} document REJECTED reason=outside_account_directory`);
20980
+ return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
20981
+ }
20982
+ } catch (err) {
20983
+ const code = err.code;
20984
+ if (code === "ENOENT") {
20985
+ console.error(`${TAG35} document ENOENT path=${filePath}`);
20986
+ return { ok: false, status: 404, error: `File not found: ${filePath}` };
20987
+ }
20988
+ console.error(`${TAG35} document path error: ${String(err)}`);
20989
+ return { ok: false, status: 500, error: String(err) };
20990
+ }
20991
+ const fileStat = await stat7(resolvedPath);
20992
+ if (fileStat.size > TELEGRAM_DOCUMENT_MAX_BYTES) {
20993
+ return {
20994
+ ok: false,
20995
+ status: 400,
20996
+ error: `File exceeds 50 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)`
20997
+ };
20998
+ }
20999
+ const filename = basename9(resolvedPath);
21000
+ const buffer = Buffer.from(await readFile7(resolvedPath));
21001
+ const form = new FormData();
21002
+ form.append("chat_id", String(chatId));
21003
+ if (caption) form.append("caption", caption);
21004
+ form.append("document", new File([buffer], filename, { type: detectMimeType(resolvedPath) }));
21005
+ let ok = false;
21006
+ let messageId;
21007
+ let error = "send failed";
21008
+ try {
21009
+ const res = await fetch(`https://api.telegram.org/bot${botToken}/sendDocument`, {
21010
+ method: "POST",
21011
+ body: form
21012
+ });
21013
+ const data = await res.json();
21014
+ ok = data.ok;
21015
+ messageId = data.result?.message_id;
21016
+ if (!data.ok) error = data.description ?? "Unknown Telegram error";
21017
+ } catch (e) {
21018
+ error = e instanceof Error ? e.message : String(e);
21019
+ }
21020
+ console.error(
21021
+ `${TAG35} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok}` + (messageId !== void 0 ? ` id=${messageId}` : "")
21022
+ );
21023
+ return ok ? { ok: true, messageId } : { ok: false, status: 500, error };
21024
+ }
21025
+
21026
+ // app/lib/telegram/outbound/file-delivery.ts
21027
+ var TAG36 = "[telegram:outbound]";
21028
+ function platformRoot2() {
21029
+ return process.env.MAXY_PLATFORM_ROOT || "";
21030
+ }
21031
+ function makeTelegramSendFile(entry) {
21032
+ let botToken;
21033
+ return async (filePath, caption) => {
21034
+ if (botToken === void 0) {
21035
+ const tg = resolveAccount()?.config.telegram;
21036
+ botToken = (entry.role === "admin" ? tg?.adminBotToken : tg?.publicBotToken) ?? null;
21037
+ }
21038
+ if (!botToken) {
21039
+ console.error(`${TAG36} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
21040
+ return { ok: false, error: "no-bot-token" };
21041
+ }
21042
+ if (entry.replyTarget == null) {
21043
+ console.error(`${TAG36} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
21044
+ return { ok: false, error: "no-reply-target" };
21045
+ }
21046
+ let maxyAccountId;
21047
+ try {
21048
+ maxyAccountId = resolvePlatformAccountId();
21049
+ } catch (err) {
21050
+ console.error(
21051
+ `${TAG36} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
21052
+ );
21053
+ return { ok: false, error: "account-unresolved" };
21054
+ }
21055
+ const result = await sendTelegramDocument({
21056
+ botToken,
21057
+ chatId: Number(entry.replyTarget),
21058
+ filePath,
21059
+ caption,
21060
+ maxyAccountId,
21061
+ platformRoot: platformRoot2()
21062
+ });
21063
+ return result.ok ? { ok: true } : { ok: false, error: result.error };
21064
+ };
21065
+ }
21066
+ function makeTelegramFileDelivery(entry) {
21067
+ return makeFileDelivery({
21068
+ entry,
21069
+ tag: TAG36,
21070
+ channel: "telegram",
21071
+ sendFile: makeTelegramSendFile(entry)
21072
+ });
21073
+ }
21074
+
21214
21075
  // app/lib/telegram/gateway/native-file-follower.ts
21215
21076
  function startTelegramNativeFileFollower(input) {
21216
21077
  const entry = {
@@ -21245,12 +21106,166 @@ function startTelegramNativeFileFollower(input) {
21245
21106
  }
21246
21107
 
21247
21108
  // server/telegram-descriptor.ts
21248
- import { resolve as resolve32, join as join31 } from "path";
21109
+ import { resolve as resolve30, join as join31 } from "path";
21249
21110
  function telegramGatewayUrl() {
21250
21111
  return `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`;
21251
21112
  }
21252
21113
  function telegramServerPath() {
21253
- return process.env.MAXY_TELEGRAM_CHANNEL_SERVER_PATH ?? resolve32(process.env.MAXY_PLATFORM_ROOT ?? join31(__dirname, ".."), "services/telegram-channel/dist/server.js");
21114
+ return process.env.MAXY_TELEGRAM_CHANNEL_SERVER_PATH ?? resolve30(process.env.MAXY_PLATFORM_ROOT ?? join31(__dirname, ".."), "services/telegram-channel/dist/server.js");
21115
+ }
21116
+
21117
+ // app/lib/channel-pty-bridge/public-session-end-review.ts
21118
+ import { randomUUID as randomUUID13 } from "crypto";
21119
+ var TAG37 = "[public-session-review]";
21120
+ var CONSUMED_CAP = 500;
21121
+ var consumed = /* @__PURE__ */ new Set();
21122
+ function consumeOnce(sessionId) {
21123
+ if (consumed.has(sessionId)) return false;
21124
+ consumed.add(sessionId);
21125
+ if (consumed.size > CONSUMED_CAP) {
21126
+ const oldest = consumed.values().next().value;
21127
+ if (oldest !== void 0) consumed.delete(oldest);
21128
+ }
21129
+ return true;
21130
+ }
21131
+ function managerBase3() {
21132
+ const port2 = requirePortEnv("CLAUDE_SESSION_MANAGER_PORT", { tag: "public-session-end-review:manager" });
21133
+ return `http://127.0.0.1:${port2}`;
21134
+ }
21135
+ function uiBase() {
21136
+ const port2 = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "public-session-end-review:ui" });
21137
+ return `http://127.0.0.1:${port2}`;
21138
+ }
21139
+ async function fetchJsonl(sessionId) {
21140
+ try {
21141
+ const res = await fetch(`${managerBase3()}/${sessionId}/log?download=1`);
21142
+ if (!res.ok) return null;
21143
+ return await res.text();
21144
+ } catch (err) {
21145
+ console.error(
21146
+ `${TAG37} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
21147
+ );
21148
+ return null;
21149
+ }
21150
+ }
21151
+ function countOperatorTurns(jsonl) {
21152
+ let count = 0;
21153
+ for (const raw of jsonl.split("\n")) {
21154
+ const line = raw.trim();
21155
+ if (!line) continue;
21156
+ try {
21157
+ const obj = JSON.parse(line);
21158
+ if (obj.type === "user") count += 1;
21159
+ } catch {
21160
+ }
21161
+ }
21162
+ return count;
21163
+ }
21164
+ async function fetchPriorWrites(sliceToken, accountId) {
21165
+ const url = `${uiBase()}/api/admin/public-session-context?sliceToken=${encodeURIComponent(sliceToken)}&accountId=${encodeURIComponent(accountId)}`;
21166
+ try {
21167
+ const res = await fetch(url);
21168
+ if (!res.ok) {
21169
+ console.error(
21170
+ `${TAG37} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
21171
+ );
21172
+ return [];
21173
+ }
21174
+ const body = await res.json();
21175
+ return Array.isArray(body.writes) ? body.writes : [];
21176
+ } catch (err) {
21177
+ console.error(
21178
+ `${TAG37} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
21179
+ );
21180
+ return [];
21181
+ }
21182
+ }
21183
+ function composeInitialMessage(input) {
21184
+ const priorJson = JSON.stringify(input.priorWrites, null, 2);
21185
+ return [
21186
+ `The gated public-agent session for agent "${input.agentSlug}" has just been reaped. Review the transcript and decide what is worth saving to the visitor's per-visitor memory slice. Dispatch \`database-operator\` for each individual write via the Task tool. Do not call memory-write or memory-update directly.`,
21187
+ "",
21188
+ "<slice-token>",
21189
+ input.sliceToken,
21190
+ "</slice-token>",
21191
+ "",
21192
+ "<person-id>",
21193
+ input.personId ?? "(unknown)",
21194
+ "</person-id>",
21195
+ "",
21196
+ "<prior-writes>",
21197
+ priorJson,
21198
+ "</prior-writes>",
21199
+ "",
21200
+ "<transcript>",
21201
+ input.jsonl,
21202
+ "</transcript>"
21203
+ ].join("\n");
21204
+ }
21205
+ async function dispatchReviewer(input, initialMessage) {
21206
+ const sessionId = randomUUID13();
21207
+ console.log(`${TAG37} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
21208
+ const spawned = await managerRcSpawn({
21209
+ sessionId,
21210
+ initialMessage,
21211
+ closeAfterTurn: true,
21212
+ sliceToken: input.sliceToken,
21213
+ personId: input.personId ?? void 0,
21214
+ logContext: `public-session-end sourceSession=${input.sessionId.slice(0, 8)}`
21215
+ });
21216
+ if ("error" in spawned) {
21217
+ return { ok: false, reason: `rc-spawn-${spawned.status}-${spawned.error}` };
21218
+ }
21219
+ return { ok: true, managerSessionId: spawned.sessionId };
21220
+ }
21221
+ async function firePublicSessionEndReview(input) {
21222
+ const sliceShort = input.sliceToken.slice(0, 8);
21223
+ const personField = input.personId ?? "none";
21224
+ const dispatchedAt = Date.now();
21225
+ if (consumed.has(input.sessionId)) {
21226
+ console.log(
21227
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
21228
+ );
21229
+ return;
21230
+ }
21231
+ const jsonl = await fetchJsonl(input.sessionId);
21232
+ if (!jsonl) {
21233
+ console.log(
21234
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
21235
+ );
21236
+ return;
21237
+ }
21238
+ const operatorTurns = countOperatorTurns(jsonl);
21239
+ if (operatorTurns === 0) {
21240
+ console.log(
21241
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
21242
+ );
21243
+ return;
21244
+ }
21245
+ const priorWrites = await fetchPriorWrites(input.sliceToken, input.accountId);
21246
+ const initialMessage = composeInitialMessage({
21247
+ agentSlug: input.agentSlug,
21248
+ sliceToken: input.sliceToken,
21249
+ personId: input.personId,
21250
+ jsonl,
21251
+ priorWrites
21252
+ });
21253
+ if (!consumeOnce(input.sessionId)) {
21254
+ console.log(
21255
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
21256
+ );
21257
+ return;
21258
+ }
21259
+ const dispatched = await dispatchReviewer(input, initialMessage);
21260
+ if (!dispatched.ok) {
21261
+ console.error(
21262
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
21263
+ );
21264
+ return;
21265
+ }
21266
+ console.log(
21267
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
21268
+ );
21254
21269
  }
21255
21270
 
21256
21271
  // app/lib/whatsapp/inbound/channel-admin-binding-drift.ts
@@ -21319,7 +21334,7 @@ function broadcastAdminShutdown(reason) {
21319
21334
  // ../lib/entitlement/src/index.ts
21320
21335
  import { createPublicKey, createHash as createHash5, verify as cryptoVerify } from "crypto";
21321
21336
  import { existsSync as existsSync31, readFileSync as readFileSync32, statSync as statSync12 } from "fs";
21322
- import { resolve as resolve33 } from "path";
21337
+ import { resolve as resolve31 } from "path";
21323
21338
 
21324
21339
  // ../lib/entitlement/src/canonicalize.ts
21325
21340
  function canonicalize(value) {
@@ -21354,7 +21369,7 @@ var PUBKEY_SHA256 = "8eee6bcb33545fd13b16d3199a5735ca5db5062834c7b49dfe4f23801d9
21354
21369
  var GRACE_DAYS = 7;
21355
21370
  var GRACE_MS = GRACE_DAYS * 24 * 60 * 60 * 1e3;
21356
21371
  function pubkeyPath(brand) {
21357
- return resolve33(brand.platformRoot, "lib", "entitlement", "rubytech-pubkey.pem");
21372
+ return resolve31(brand.platformRoot, "lib", "entitlement", "rubytech-pubkey.pem");
21358
21373
  }
21359
21374
  var memo = null;
21360
21375
  function memoKey(mtimeMs, account) {
@@ -21366,7 +21381,7 @@ function resolveEntitlement(brand, account) {
21366
21381
  if (brand.commercialMode !== true) {
21367
21382
  return logResolved(implicitTrust(account), null);
21368
21383
  }
21369
- const entitlementPath = resolve33(brand.configDir, "entitlement.json");
21384
+ const entitlementPath = resolve31(brand.configDir, "entitlement.json");
21370
21385
  if (!existsSync31(entitlementPath)) {
21371
21386
  return logResolved(anonymousFallback("missing"), { reason: "missing" });
21372
21387
  }
@@ -21628,7 +21643,7 @@ var app54 = new Hono();
21628
21643
  var nativeFileFollowers = /* @__PURE__ */ new Map();
21629
21644
  var waGateway = new WaGateway({
21630
21645
  gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
21631
- serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve34(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
21646
+ serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
21632
21647
  // Task 751 — file delivery on the native channel: resolve the platform
21633
21648
  // account + path validation here and funnel through the shared send core.
21634
21649
  sendDocument: async ({ senderId, accountId, filePath, caption }) => {
@@ -21644,7 +21659,7 @@ var waGateway = new WaGateway({
21644
21659
  caption,
21645
21660
  accountId,
21646
21661
  maxyAccountId,
21647
- platformRoot: resolve34(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."))
21662
+ platformRoot: resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."))
21648
21663
  });
21649
21664
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
21650
21665
  },
@@ -21747,6 +21762,7 @@ var webchatGateway = new WebchatGateway({
21747
21762
  deleteSession: (sessionId) => managerDelete(sessionId),
21748
21763
  firePublicSessionEndReview: (input) => firePublicSessionEndReview(input)
21749
21764
  });
21765
+ setWebchatGateway(webchatGateway);
21750
21766
  app54.route("/", webchatGateway.routes());
21751
21767
  webchatGateway.startSweeper();
21752
21768
  webchatGateway.startPublicReaper();
@@ -21958,7 +21974,7 @@ app54.post("/__remote-auth/change-password", async (c) => {
21958
21974
  }
21959
21975
  try {
21960
21976
  if (targetUserId) {
21961
- await setAccessPassword(targetUserId, newPassword, USERS_FILE);
21977
+ await setAccessPassword(targetUserId, newPassword, USERS_FILE, { actor: targetUserId, logFile: USERS_AUDIT_LOG });
21962
21978
  } else {
21963
21979
  await setRemotePassword(newPassword);
21964
21980
  }
@@ -22045,7 +22061,7 @@ app54.post("/api/remote-auth/set-password", async (c) => {
22045
22061
  }
22046
22062
  try {
22047
22063
  if (sessionUserId) {
22048
- await setAccessPassword(sessionUserId, body.password, USERS_FILE);
22064
+ await setAccessPassword(sessionUserId, body.password, USERS_FILE, { actor: sessionUserId, logFile: USERS_AUDIT_LOG });
22049
22065
  } else {
22050
22066
  await setRemotePassword(body.password);
22051
22067
  }
@@ -22150,8 +22166,8 @@ app54.get("/agent-assets/:slug/:filename", (c) => {
22150
22166
  console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
22151
22167
  return c.text("Not found", 404);
22152
22168
  }
22153
- const filePath = resolve34(account.accountDir, "agents", slug, "assets", filename);
22154
- const expectedDir = resolve34(account.accountDir, "agents", slug, "assets");
22169
+ const filePath = resolve32(account.accountDir, "agents", slug, "assets", filename);
22170
+ const expectedDir = resolve32(account.accountDir, "agents", slug, "assets");
22155
22171
  if (!filePath.startsWith(expectedDir + "/")) {
22156
22172
  console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
22157
22173
  return c.text("Forbidden", 403);
@@ -22180,8 +22196,8 @@ app54.get("/generated/:filename", (c) => {
22180
22196
  console.error(`[generated] serve file=${filename} status=404`);
22181
22197
  return c.text("Not found", 404);
22182
22198
  }
22183
- const filePath = resolve34(account.accountDir, "generated", filename);
22184
- const expectedDir = resolve34(account.accountDir, "generated");
22199
+ const filePath = resolve32(account.accountDir, "generated", filename);
22200
+ const expectedDir = resolve32(account.accountDir, "generated");
22185
22201
  if (!filePath.startsWith(expectedDir + "/")) {
22186
22202
  console.error(`[generated] serve file=${filename} status=403`);
22187
22203
  return c.text("Forbidden", 403);
@@ -22235,11 +22251,11 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
22235
22251
  var brandThemeColor = BRAND.defaultColors?.primary ?? "#000000";
22236
22252
  var brandBackgroundColor = BRAND.defaultColors?.background ?? "#ffffff";
22237
22253
  var brandAppIconsExist = [brandAppIcon192Path, brandAppIcon512Path, brandMaskableIconPath].every(
22238
- (p) => existsSync32(resolve34(process.cwd(), "public", p.replace(/^\//, "")))
22254
+ (p) => existsSync32(resolve32(process.cwd(), "public", p.replace(/^\//, "")))
22239
22255
  );
22240
22256
  var SW_SOURCE = (() => {
22241
22257
  try {
22242
- return readFileSync33(resolve34(process.cwd(), "public", "sw.js"), "utf-8");
22258
+ return readFileSync33(resolve32(process.cwd(), "public", "sw.js"), "utf-8");
22243
22259
  } catch {
22244
22260
  return null;
22245
22261
  }
@@ -22290,7 +22306,7 @@ var clientErrorReporterScript = `<script>
22290
22306
  function cachedHtml(file) {
22291
22307
  let html = htmlCache.get(file);
22292
22308
  if (!html) {
22293
- html = readFileSync33(resolve34(process.cwd(), "public", file), "utf-8");
22309
+ html = readFileSync33(resolve32(process.cwd(), "public", file), "utf-8");
22294
22310
  const productNameEsc = escapeHtml(BRAND.productName);
22295
22311
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
22296
22312
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -22408,7 +22424,7 @@ app54.use("/vnc-popout.html", logViewerFetch);
22408
22424
  app54.get("/vnc-popout.html", (c) => {
22409
22425
  let html = htmlCache.get("vnc-popout.html");
22410
22426
  if (!html) {
22411
- html = readFileSync33(resolve34(process.cwd(), "public", "vnc-popout.html"), "utf-8");
22427
+ html = readFileSync33(resolve32(process.cwd(), "public", "vnc-popout.html"), "utf-8");
22412
22428
  const name = escapeHtml(BRAND.productName);
22413
22429
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
22414
22430
  html = html.replace("</head>", ` ${brandScript}
@@ -22532,7 +22548,7 @@ var httpServer = serve({ fetch: app54.fetch, port, hostname });
22532
22548
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
22533
22549
  {
22534
22550
  const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..");
22535
- const reconcileScript = resolve34(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
22551
+ const reconcileScript = resolve32(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
22536
22552
  const RECONCILE_INTERVAL_MS = 12e4;
22537
22553
  const runReconcile = () => {
22538
22554
  if (!existsSync32(reconcileScript)) return;
@@ -22554,7 +22570,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
22554
22570
  }
22555
22571
  {
22556
22572
  const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..");
22557
- const auditScript = resolve34(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
22573
+ const auditScript = resolve32(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
22558
22574
  const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
22559
22575
  const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
22560
22576
  const runConnectorAudit = () => {
@@ -22657,7 +22673,7 @@ try {
22657
22673
  const migAccount = resolveAccount();
22658
22674
  const ownerUserId = migAccount?.config.admins?.find((a) => a.role === "owner")?.userId;
22659
22675
  if (ownerUserId) {
22660
- const result = migrateLegacyRemotePassword(USERS_FILE, REMOTE_PASSWORD_FILE, ownerUserId);
22676
+ const result = migrateLegacyRemotePassword(USERS_FILE, REMOTE_PASSWORD_FILE, ownerUserId, { actor: "boot", logFile: USERS_AUDIT_LOG });
22661
22677
  console.error(`[remote-access-migrate] result=${result} ownerUserId=${ownerUserId.slice(0, 8)}`);
22662
22678
  } else {
22663
22679
  console.error("[remote-access-migrate] result=noop-no-owner ownerUserId=none");
@@ -22698,6 +22714,35 @@ var adminUserReconcileTimer = setInterval(() => {
22698
22714
  void runAdminUserReconcileTick();
22699
22715
  }, ADMINUSER_RECONCILE_INTERVAL_MS);
22700
22716
  if (typeof adminUserReconcileTimer.unref === "function") adminUserReconcileTimer.unref();
22717
+ var USERS_RECONCILE_INTERVAL_MS = 60 * 60 * 1e3;
22718
+ function countUsersRows() {
22719
+ if (!existsSync32(USERS_FILE)) return 0;
22720
+ const raw = readFileSync33(USERS_FILE, "utf-8").trim();
22721
+ if (!raw) return 0;
22722
+ const users = JSON.parse(raw);
22723
+ return users.filter((u) => typeof u.userId === "string").length;
22724
+ }
22725
+ function runUsersReconcileTick() {
22726
+ try {
22727
+ const d = computeAdminStoreDivergence({ usersFile: USERS_FILE, accountsDir: ACCOUNTS_DIR });
22728
+ if (d.errors.length > 0) {
22729
+ console.error(`[users-reconcile] op=error detail=${d.errors.map((e) => `${e.source}:${e.detail}`).join(";")}`);
22730
+ return;
22731
+ }
22732
+ if (d.divergences === 0) {
22733
+ console.error(`[users-reconcile] op=ok admins=${countUsersRows()}`);
22734
+ return;
22735
+ }
22736
+ const accountOnly = d.accountWithoutUsers.map((a) => a.userId.slice(0, 8)).join(",");
22737
+ const usersOnly = d.usersWithoutAccount.map((u) => u.userId.slice(0, 8)).join(",");
22738
+ console.error(`[users-reconcile] op=divergence accountOnly=${accountOnly} usersOnly=${usersOnly}`);
22739
+ } catch (err) {
22740
+ console.error(`[users-reconcile] op=error detail=${err instanceof Error ? err.message : String(err)}`);
22741
+ }
22742
+ }
22743
+ runUsersReconcileTick();
22744
+ var usersReconcileTimer = setInterval(runUsersReconcileTick, USERS_RECONCILE_INTERVAL_MS);
22745
+ if (typeof usersReconcileTimer.unref === "function") usersReconcileTimer.unref();
22701
22746
  startGraphHealthTimer();
22702
22747
  void migrateUploads().catch((err) => console.error(`[uploads-migrate] failed err="${err instanceof Error ? err.message : String(err)}"`)).finally(() => {
22703
22748
  void startFileWatcher().catch((err) => {
@@ -22786,7 +22831,7 @@ if (bootAccountConfig?.whatsapp) {
22786
22831
  }
22787
22832
  init({
22788
22833
  configDir: configDirForWhatsApp,
22789
- platformRoot: resolve34(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..")),
22834
+ platformRoot: resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..")),
22790
22835
  accountConfig: bootAccountConfig,
22791
22836
  onMessage: async (msg) => {
22792
22837
  if (msg.isOwnerMirror) {
@@ -22827,7 +22872,6 @@ init({
22827
22872
  }).catch((err) => {
22828
22873
  console.error(`[whatsapp] init failed: ${String(err)}`);
22829
22874
  });
22830
- startReaper();
22831
22875
  var shuttingDown = false;
22832
22876
  process.on("SIGTERM", async () => {
22833
22877
  if (shuttingDown) {