@rubytech/create-realagent-code 0.1.578 → 0.1.579

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 (187) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +9 -1
  3. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +10 -0
  4. package/payload/platform/plugins/docs/references/admin-ui.md +6 -0
  5. package/payload/platform/plugins/docs/references/troubleshooting.md +2 -0
  6. package/payload/platform/scripts/check-no-esm-require.mjs +6 -0
  7. package/payload/platform/services/webchat-channel/dist/server.js +4 -4
  8. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  9. package/payload/server/public/activity.html +6 -6
  10. package/payload/server/public/agents.html +5 -5
  11. package/payload/server/public/assets/{AdminLoginScreens-CAZ3DSwA.js → AdminLoginScreens-Bus-Y-rG.js} +1 -1
  12. package/payload/server/public/assets/AdminLoginScreens-Bus-Y-rG.js.br +0 -0
  13. package/payload/server/public/assets/AdminLoginScreens-Bus-Y-rG.js.gz +0 -0
  14. package/payload/server/public/assets/AdminShell-Bsm7q3CW.js +3 -0
  15. package/payload/server/public/assets/AdminShell-Bsm7q3CW.js.br +0 -0
  16. package/payload/server/public/assets/AdminShell-Bsm7q3CW.js.gz +0 -0
  17. package/payload/server/public/assets/{activity-BwPck2yT.js → activity-BWqrCyYM.js} +1 -1
  18. package/payload/server/public/assets/activity-BWqrCyYM.js.br +0 -0
  19. package/payload/server/public/assets/activity-BWqrCyYM.js.gz +0 -0
  20. package/payload/server/public/assets/admin-zly7Y6EZ.js +1 -0
  21. package/payload/server/public/assets/admin-zly7Y6EZ.js.br +0 -0
  22. package/payload/server/public/assets/admin-zly7Y6EZ.js.gz +0 -0
  23. package/payload/server/public/assets/{agents-IQYaRvoh.js → agents-BsxGD1V1.js} +1 -1
  24. package/payload/server/public/assets/agents-BsxGD1V1.js.br +0 -0
  25. package/payload/server/public/assets/agents-BsxGD1V1.js.gz +0 -0
  26. package/payload/server/public/assets/{browser-CPG8w8rQ.js → browser-CRLxTaw2.js} +1 -1
  27. package/payload/server/public/assets/browser-CRLxTaw2.js.br +0 -0
  28. package/payload/server/public/assets/browser-CRLxTaw2.js.gz +0 -0
  29. package/payload/server/public/assets/{calendar-CRwrgNdq.js → calendar-Bmp6qI_P.js} +1 -1
  30. package/payload/server/public/assets/calendar-Bmp6qI_P.js.br +0 -0
  31. package/payload/server/public/assets/calendar-Bmp6qI_P.js.gz +0 -0
  32. package/payload/server/public/assets/chat-ChObDKHy.js +1 -0
  33. package/payload/server/public/assets/chat-ChObDKHy.js.br +0 -0
  34. package/payload/server/public/assets/chat-ChObDKHy.js.gz +0 -0
  35. package/payload/server/public/assets/chevron-left-D2l4jMcd.js +1 -0
  36. package/payload/server/public/assets/chevron-left-D2l4jMcd.js.br +0 -0
  37. package/payload/server/public/assets/clock-CMdfsroW.js +1 -0
  38. package/payload/server/public/assets/clock-CMdfsroW.js.br +0 -0
  39. package/payload/server/public/assets/clock-CMdfsroW.js.gz +0 -0
  40. package/payload/server/public/assets/{copy-JyiMTV0k.js → copy-Bcjpxqzn.js} +1 -1
  41. package/payload/server/public/assets/copy-Bcjpxqzn.js.br +0 -0
  42. package/payload/server/public/assets/copy-Bcjpxqzn.js.gz +0 -0
  43. package/payload/server/public/assets/data-CFB5Lgw-.js +1 -0
  44. package/payload/server/public/assets/data-CFB5Lgw-.js.br +0 -0
  45. package/payload/server/public/assets/data-CFB5Lgw-.js.gz +0 -0
  46. package/payload/server/public/assets/{field-B490-evJ.js → field-ChKB2l_d.js} +1 -1
  47. package/payload/server/public/assets/field-ChKB2l_d.js.br +0 -0
  48. package/payload/server/public/assets/field-ChKB2l_d.js.gz +0 -0
  49. package/payload/server/public/assets/{file-text-cnHv9ZS6.js → file-text-COZcf7mU.js} +1 -1
  50. package/payload/server/public/assets/file-text-COZcf7mU.js.br +0 -0
  51. package/payload/server/public/assets/file-text-COZcf7mU.js.gz +0 -0
  52. package/payload/server/public/assets/{graph-labels-C6qUA0vO.js → graph-labels-x-1MT-3c.js} +1 -1
  53. package/payload/server/public/assets/graph-labels-x-1MT-3c.js.br +0 -0
  54. package/payload/server/public/assets/graph-labels-x-1MT-3c.js.gz +0 -0
  55. package/payload/server/public/assets/{graph-lFwEUDSH.js → graph-nnV47iqM.js} +1 -1
  56. package/payload/server/public/assets/graph-nnV47iqM.js.br +0 -0
  57. package/payload/server/public/assets/graph-nnV47iqM.js.gz +0 -0
  58. package/payload/server/public/assets/{jsx-runtime-ChVh0xOm.css → jsx-runtime-CeNZasfC.css} +1 -1
  59. package/payload/server/public/assets/jsx-runtime-CeNZasfC.css.br +0 -0
  60. package/payload/server/public/assets/jsx-runtime-CeNZasfC.css.gz +0 -0
  61. package/payload/server/public/assets/{operator-BdoEy5mh.js → operator-BAO0-mjU.js} +1 -1
  62. package/payload/server/public/assets/operator-BAO0-mjU.js.br +0 -0
  63. package/payload/server/public/assets/operator-BAO0-mjU.js.gz +0 -0
  64. package/payload/server/public/assets/{page-DMrnnXNA.js → page-B-F08pod.js} +1 -1
  65. package/payload/server/public/assets/page-B-F08pod.js.br +0 -0
  66. package/payload/server/public/assets/page-B-F08pod.js.gz +0 -0
  67. package/payload/server/public/assets/page-BMOCy2aD.js +1 -0
  68. package/payload/server/public/assets/page-BMOCy2aD.js.br +0 -0
  69. package/payload/server/public/assets/page-BMOCy2aD.js.gz +0 -0
  70. package/payload/server/public/assets/play-DFtlk8Zs.js +1 -0
  71. package/payload/server/public/assets/play-DFtlk8Zs.js.br +0 -0
  72. package/payload/server/public/assets/play-DFtlk8Zs.js.gz +0 -0
  73. package/payload/server/public/assets/{public-B-X1Cxfw.js → public-_HJsUohA.js} +1 -1
  74. package/payload/server/public/assets/public-_HJsUohA.js.br +0 -0
  75. package/payload/server/public/assets/public-_HJsUohA.js.gz +0 -0
  76. package/payload/server/public/assets/{rotate-ccw-DVFvQAHG.js → rotate-ccw-DJeAnYLQ.js} +1 -1
  77. package/payload/server/public/assets/rotate-ccw-DJeAnYLQ.js.br +0 -0
  78. package/payload/server/public/assets/rotate-ccw-DJeAnYLQ.js.gz +0 -0
  79. package/payload/server/public/assets/{routines-CgkkGJaS.js → routines-CoIVXRCB.js} +2 -2
  80. package/payload/server/public/assets/routines-CoIVXRCB.js.br +0 -0
  81. package/payload/server/public/assets/routines-CoIVXRCB.js.gz +0 -0
  82. package/payload/server/public/assets/{skills-CW7gVcaG.js → skills-BjHt5lDG.js} +1 -1
  83. package/payload/server/public/assets/skills-BjHt5lDG.js.br +0 -0
  84. package/payload/server/public/assets/skills-BjHt5lDG.js.gz +0 -0
  85. package/payload/server/public/assets/{tasks-Ddjp0Rwh.js → tasks-iUFfeA9_.js} +2 -2
  86. package/payload/server/public/assets/tasks-iUFfeA9_.js.br +0 -0
  87. package/payload/server/public/assets/tasks-iUFfeA9_.js.gz +0 -0
  88. package/payload/server/public/assets/{triangle-alert-DUrUutX4.js → triangle-alert-Dwa986Gg.js} +1 -1
  89. package/payload/server/public/assets/triangle-alert-Dwa986Gg.js.br +0 -0
  90. package/payload/server/public/assets/triangle-alert-Dwa986Gg.js.gz +0 -0
  91. package/payload/server/public/assets/{useCopyFeedback-ClVINDrQ.js → useCopyFeedback-B1Jp6CsE.js} +1 -1
  92. package/payload/server/public/assets/useCopyFeedback-B1Jp6CsE.js.br +0 -0
  93. package/payload/server/public/assets/useCopyFeedback-B1Jp6CsE.js.gz +0 -0
  94. package/payload/server/public/assets/{useMediaQuery-BUFkZYRs.js → useMediaQuery-CVZYjRf6.js} +2 -2
  95. package/payload/server/public/assets/useMediaQuery-CVZYjRf6.js.br +0 -0
  96. package/payload/server/public/assets/useMediaQuery-CVZYjRf6.js.gz +0 -0
  97. package/payload/server/public/assets/useVoiceRecorder-D5b4r4z0.js +2 -0
  98. package/payload/server/public/assets/useVoiceRecorder-D5b4r4z0.js.br +0 -0
  99. package/payload/server/public/assets/useVoiceRecorder-D5b4r4z0.js.gz +0 -0
  100. package/payload/server/public/assets/{wrench-BW28s_d0.js → wrench-BD7xwbV1.js} +1 -1
  101. package/payload/server/public/assets/wrench-BD7xwbV1.js.br +0 -0
  102. package/payload/server/public/assets/wrench-BD7xwbV1.js.gz +0 -0
  103. package/payload/server/public/browser.html +5 -5
  104. package/payload/server/public/calendar.html +8 -8
  105. package/payload/server/public/chat.html +14 -14
  106. package/payload/server/public/data.html +12 -12
  107. package/payload/server/public/field.html +3 -3
  108. package/payload/server/public/graph.html +10 -10
  109. package/payload/server/public/index.html +14 -14
  110. package/payload/server/public/operator.html +16 -16
  111. package/payload/server/public/public.html +14 -14
  112. package/payload/server/public/routines.html +8 -8
  113. package/payload/server/public/skills.html +6 -6
  114. package/payload/server/public/tasks.html +7 -7
  115. package/payload/server/server.js +650 -248
  116. package/payload/server/public/assets/AdminLoginScreens-CAZ3DSwA.js.br +0 -0
  117. package/payload/server/public/assets/AdminLoginScreens-CAZ3DSwA.js.gz +0 -0
  118. package/payload/server/public/assets/AdminShell-BLs5v0qF.js +0 -3
  119. package/payload/server/public/assets/AdminShell-BLs5v0qF.js.br +0 -0
  120. package/payload/server/public/assets/AdminShell-BLs5v0qF.js.gz +0 -0
  121. package/payload/server/public/assets/activity-BwPck2yT.js.br +0 -0
  122. package/payload/server/public/assets/activity-BwPck2yT.js.gz +0 -0
  123. package/payload/server/public/assets/admin-DjtOoabA.js +0 -1
  124. package/payload/server/public/assets/admin-DjtOoabA.js.br +0 -0
  125. package/payload/server/public/assets/admin-DjtOoabA.js.gz +0 -0
  126. package/payload/server/public/assets/agents-IQYaRvoh.js.br +0 -0
  127. package/payload/server/public/assets/agents-IQYaRvoh.js.gz +0 -0
  128. package/payload/server/public/assets/browser-CPG8w8rQ.js.br +0 -0
  129. package/payload/server/public/assets/browser-CPG8w8rQ.js.gz +0 -0
  130. package/payload/server/public/assets/calendar-CRwrgNdq.js.br +0 -0
  131. package/payload/server/public/assets/calendar-CRwrgNdq.js.gz +0 -0
  132. package/payload/server/public/assets/chat-B7yZ4KLl.js +0 -1
  133. package/payload/server/public/assets/chat-B7yZ4KLl.js.br +0 -3
  134. package/payload/server/public/assets/chat-B7yZ4KLl.js.gz +0 -0
  135. package/payload/server/public/assets/chevron-left-BzKUQqLy.js +0 -1
  136. package/payload/server/public/assets/chevron-left-BzKUQqLy.js.br +0 -0
  137. package/payload/server/public/assets/clock-BafWFNGo.js +0 -1
  138. package/payload/server/public/assets/clock-BafWFNGo.js.br +0 -0
  139. package/payload/server/public/assets/copy-JyiMTV0k.js.br +0 -0
  140. package/payload/server/public/assets/copy-JyiMTV0k.js.gz +0 -0
  141. package/payload/server/public/assets/data-DHtGCVrl.js +0 -1
  142. package/payload/server/public/assets/data-DHtGCVrl.js.br +0 -0
  143. package/payload/server/public/assets/data-DHtGCVrl.js.gz +0 -0
  144. package/payload/server/public/assets/field-B490-evJ.js.br +0 -0
  145. package/payload/server/public/assets/field-B490-evJ.js.gz +0 -0
  146. package/payload/server/public/assets/file-text-cnHv9ZS6.js.br +0 -0
  147. package/payload/server/public/assets/file-text-cnHv9ZS6.js.gz +0 -0
  148. package/payload/server/public/assets/graph-lFwEUDSH.js.br +0 -0
  149. package/payload/server/public/assets/graph-lFwEUDSH.js.gz +0 -0
  150. package/payload/server/public/assets/graph-labels-C6qUA0vO.js.br +0 -0
  151. package/payload/server/public/assets/graph-labels-C6qUA0vO.js.gz +0 -0
  152. package/payload/server/public/assets/jsx-runtime-ChVh0xOm.css.br +0 -0
  153. package/payload/server/public/assets/jsx-runtime-ChVh0xOm.css.gz +0 -0
  154. package/payload/server/public/assets/operator-BdoEy5mh.js.br +0 -0
  155. package/payload/server/public/assets/operator-BdoEy5mh.js.gz +0 -0
  156. package/payload/server/public/assets/page-DMrnnXNA.js.br +0 -0
  157. package/payload/server/public/assets/page-DMrnnXNA.js.gz +0 -0
  158. package/payload/server/public/assets/page-DeMikGYL.js +0 -1
  159. package/payload/server/public/assets/page-DeMikGYL.js.br +0 -0
  160. package/payload/server/public/assets/page-DeMikGYL.js.gz +0 -0
  161. package/payload/server/public/assets/play-D6OAQVJ_.js +0 -1
  162. package/payload/server/public/assets/play-D6OAQVJ_.js.br +0 -0
  163. package/payload/server/public/assets/play-D6OAQVJ_.js.gz +0 -0
  164. package/payload/server/public/assets/public-B-X1Cxfw.js.br +0 -0
  165. package/payload/server/public/assets/public-B-X1Cxfw.js.gz +0 -0
  166. package/payload/server/public/assets/rotate-ccw-DVFvQAHG.js.br +0 -0
  167. package/payload/server/public/assets/rotate-ccw-DVFvQAHG.js.gz +0 -0
  168. package/payload/server/public/assets/routines-CgkkGJaS.js.br +0 -0
  169. package/payload/server/public/assets/routines-CgkkGJaS.js.gz +0 -0
  170. package/payload/server/public/assets/skills-CW7gVcaG.js.br +0 -0
  171. package/payload/server/public/assets/skills-CW7gVcaG.js.gz +0 -0
  172. package/payload/server/public/assets/tasks-Ddjp0Rwh.js.br +0 -0
  173. package/payload/server/public/assets/tasks-Ddjp0Rwh.js.gz +0 -0
  174. package/payload/server/public/assets/triangle-alert-DUrUutX4.js.br +0 -0
  175. package/payload/server/public/assets/triangle-alert-DUrUutX4.js.gz +0 -0
  176. package/payload/server/public/assets/useCopyFeedback-ClVINDrQ.js.br +0 -0
  177. package/payload/server/public/assets/useCopyFeedback-ClVINDrQ.js.gz +0 -0
  178. package/payload/server/public/assets/useMediaQuery-BUFkZYRs.js.br +0 -0
  179. package/payload/server/public/assets/useMediaQuery-BUFkZYRs.js.gz +0 -0
  180. package/payload/server/public/assets/useVoiceRecorder-DnyqQFet.js +0 -2
  181. package/payload/server/public/assets/useVoiceRecorder-DnyqQFet.js.br +0 -0
  182. package/payload/server/public/assets/useVoiceRecorder-DnyqQFet.js.gz +0 -0
  183. package/payload/server/public/assets/wrench-BW28s_d0.js.br +0 -0
  184. package/payload/server/public/assets/wrench-BW28s_d0.js.gz +0 -0
  185. /package/payload/server/public/assets/{jsx-runtime-YcUntiud.js → jsx-runtime-CjJ246F7.js} +0 -0
  186. /package/payload/server/public/assets/{jsx-runtime-YcUntiud.js.br → jsx-runtime-CjJ246F7.js.br} +0 -0
  187. /package/payload/server/public/assets/{jsx-runtime-YcUntiud.js.gz → jsx-runtime-CjJ246F7.js.gz} +0 -0
@@ -5517,11 +5517,11 @@ var require_dist7 = __commonJS({
5517
5517
 
5518
5518
  // server/index.ts
5519
5519
  init_dist();
5520
- import { readFileSync as readFileSync56, existsSync as existsSync56, watchFile, readdirSync as readdirSync32, statSync as statSync30 } from "fs";
5520
+ import { readFileSync as readFileSync57, existsSync as existsSync56, watchFile, readdirSync as readdirSync33, statSync as statSync30 } from "fs";
5521
5521
  import { spawn as spawn3 } from "child_process";
5522
5522
  import { createHash as createHash8 } from "crypto";
5523
- import { resolve as resolve51, join as join64, basename as basename17 } from "path";
5524
- import { homedir as homedir5 } from "os";
5523
+ import { resolve as resolve51, join as join65, basename as basename17 } from "path";
5524
+ import { homedir as homedir6 } from "os";
5525
5525
 
5526
5526
  // server/lib/route-cache-watchers.ts
5527
5527
  init_route_cache();
@@ -6777,7 +6777,16 @@ function readCredentials() {
6777
6777
  expiresAt
6778
6778
  };
6779
6779
  }
6780
- function writeOAuthCredentials(tokens) {
6780
+ function logCredentialWrite(site, refreshTokenLen) {
6781
+ console.log(`[claude-auth] op=cred-write site=${site} refreshTokenLen=${refreshTokenLen}`);
6782
+ }
6783
+ function writeHostCredsFile(credentialsFile, blob, refreshTokenLen) {
6784
+ const tmp = `${credentialsFile}.${process.pid}.tmp`;
6785
+ writeFileSync2(tmp, blob, { mode: 384 });
6786
+ renameSync(tmp, credentialsFile);
6787
+ logCredentialWrite("login-writeback", refreshTokenLen);
6788
+ }
6789
+ function writeOAuthCredentials(tokens, site) {
6781
6790
  const newExpiresAt = typeof tokens.expiresIn === "number" && tokens.expiresIn > 0 ? Date.now() + tokens.expiresIn * 1e3 : Date.now() + 3600 * 1e3;
6782
6791
  try {
6783
6792
  let fileData = {};
@@ -6787,10 +6796,13 @@ function writeOAuthCredentials(tokens) {
6787
6796
  } catch {
6788
6797
  }
6789
6798
  const existingOauth = fileData.claudeAiOauth;
6799
+ const existingRefresh = typeof existingOauth?.refreshToken === "string" ? existingOauth.refreshToken : "";
6800
+ const incomingRefresh = typeof tokens.refreshToken === "string" ? tokens.refreshToken : "";
6801
+ const blankingRefused = incomingRefresh.length === 0 && existingRefresh.length > 0;
6790
6802
  fileData.claudeAiOauth = {
6791
6803
  ...existingOauth,
6792
6804
  accessToken: tokens.accessToken,
6793
- ...tokens.refreshToken != null ? { refreshToken: tokens.refreshToken } : {},
6805
+ ...incomingRefresh.length > 0 ? { refreshToken: incomingRefresh } : {},
6794
6806
  expiresAt: newExpiresAt
6795
6807
  };
6796
6808
  const tmpPath = `${CLAUDE_CREDENTIALS_FILE}.${process.pid}.tmp`;
@@ -6804,6 +6816,13 @@ function writeOAuthCredentials(tokens) {
6804
6816
  }
6805
6817
  throw writeErr;
6806
6818
  }
6819
+ if (blankingRefused) {
6820
+ console.warn(
6821
+ `[claude-auth] op=cred-write-refused site=${site} reason=blank-refresh-token storedLen=${existingRefresh.length}`
6822
+ );
6823
+ }
6824
+ const landed = fileData.claudeAiOauth.refreshToken;
6825
+ logCredentialWrite(site, typeof landed === "string" ? landed.length : 0);
6807
6826
  return newExpiresAt;
6808
6827
  } catch (err) {
6809
6828
  console.error(`[claude-auth] credential write failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -6933,9 +6952,11 @@ async function doRefreshLocked() {
6933
6952
  }
6934
6953
  const newExpiresAt = writeOAuthCredentials({
6935
6954
  accessToken: newAccessToken,
6936
- refreshToken: newRefreshToken ?? creds.refreshToken,
6955
+ // Task 2572 — `??` does not fall back on the empty string, so a response
6956
+ // carrying `refresh_token: ""` used to overwrite a good stored token.
6957
+ refreshToken: typeof newRefreshToken === "string" && newRefreshToken.length > 0 ? newRefreshToken : creds.refreshToken,
6937
6958
  expiresIn: typeof expiresIn === "number" && expiresIn > 0 ? expiresIn : void 0
6938
- });
6959
+ }, "refresh");
6939
6960
  if (newExpiresAt) {
6940
6961
  console.log(`[auth-refresh] op=renewed expiresAt=${new Date(newExpiresAt).toISOString()}`);
6941
6962
  } else {
@@ -7240,13 +7261,30 @@ function killChromium() {
7240
7261
  var AUTH_TARGET_ID_FILE = resolve4(LOG_DIR, "claude-auth-target.id");
7241
7262
  function readAuthTargetId() {
7242
7263
  try {
7243
- const id = readFileSync4(AUTH_TARGET_ID_FILE, "utf8").trim();
7264
+ const parts = readFileSync4(AUTH_TARGET_ID_FILE, "utf8").trim().split(/\s+/);
7265
+ const id = parts[parts.length - 1];
7244
7266
  return id.length > 0 ? id : null;
7245
7267
  } catch {
7246
7268
  return null;
7247
7269
  }
7248
7270
  }
7249
- function writeChromiumWrapper() {
7271
+ function readAuthAttemptId() {
7272
+ try {
7273
+ const parts = readFileSync4(AUTH_TARGET_ID_FILE, "utf8").trim().split(/\s+/);
7274
+ return parts.length === 2 && parts[0].length > 0 ? parts[0] : null;
7275
+ } catch {
7276
+ return null;
7277
+ }
7278
+ }
7279
+ async function closeAuthTarget(targetId) {
7280
+ try {
7281
+ const res = await fetch(`http://127.0.0.1:${CDP_PORT}/json/close/${encodeURIComponent(targetId)}`);
7282
+ return res.ok;
7283
+ } catch {
7284
+ return false;
7285
+ }
7286
+ }
7287
+ function writeChromiumWrapper(attemptId) {
7250
7288
  mkdirSync2(BIN_DIR, { recursive: true });
7251
7289
  rmSync2(AUTH_TARGET_ID_FILE, { force: true });
7252
7290
  const wrapperPath = resolve4(BIN_DIR, "chromium");
@@ -7257,6 +7295,9 @@ LOG_DIR="${LOG_DIR}"
7257
7295
  LOG="$LOG_DIR/chromium.log"
7258
7296
  TARGET_ID_FILE="$LOG_DIR/claude-auth-target.id"
7259
7297
  CDP="http://127.0.0.1:${CDP_PORT}/json/new"
7298
+ # The sign-in this wrapper was generated for. Server-minted hex from
7299
+ # randomUUID().slice(0, 8), never caller-supplied, so it carries no shell.
7300
+ ATTEMPT_ID="${attemptId}"
7260
7301
 
7261
7302
  mkdir -p "$LOG_DIR"
7262
7303
  echo "==== [$(date)] chromium wrapper (cdp-handover) ====" >> "$LOG"
@@ -7288,9 +7329,9 @@ if [ -z "$TARGET_ID" ]; then
7288
7329
  exit 65
7289
7330
  fi
7290
7331
 
7291
- printf '%s' "$TARGET_ID" > "$TARGET_ID_FILE"
7292
- echo " opened target $TARGET_ID" >> "$LOG"
7293
- echo "[chromium-wrapper] op=cdp-handover outcome=ready targetId=$TARGET_ID" >> "$LOG"
7332
+ printf '%s %s' "$ATTEMPT_ID" "$TARGET_ID" > "$TARGET_ID_FILE"
7333
+ echo " opened target $TARGET_ID for attempt $ATTEMPT_ID" >> "$LOG"
7334
+ echo "[chromium-wrapper] op=cdp-handover outcome=ready attempt=$ATTEMPT_ID targetId=$TARGET_ID" >> "$LOG"
7294
7335
  `, { mode: 493 });
7295
7336
  return wrapperPath;
7296
7337
  }
@@ -18842,7 +18883,7 @@ var quickbooks_default = app12;
18842
18883
  // server/routes/onboarding.ts
18843
18884
  init_dist();
18844
18885
  import { spawn, spawnSync as spawnSync2, execFileSync as execFileSync4 } from "child_process";
18845
- import { openSync as openSync8, closeSync as closeSync8, writeFileSync as writeFileSync14, writeSync, existsSync as existsSync26, readFileSync as readFileSync32, unlinkSync as unlinkSync3, renameSync as renameSync8 } from "fs";
18886
+ import { openSync as openSync8, closeSync as closeSync8, appendFileSync as appendFileSync3, writeFileSync as writeFileSync14, writeSync, existsSync as existsSync26, readFileSync as readFileSync32, unlinkSync as unlinkSync3 } from "fs";
18846
18887
  import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
18847
18888
  init_claude_agent();
18848
18889
  init_neo4j_store();
@@ -18915,8 +18956,10 @@ function bridgeKeychainToFile(deps) {
18915
18956
  deps.log(`[onboarding] op=login-writeback result=no-claudeAiOauth service=${JSON.stringify(service)}`);
18916
18957
  return { wrote: false, reason: "no-claudeAiOauth", bytes: 0, service };
18917
18958
  }
18959
+ const oauth = parsed.claudeAiOauth;
18960
+ const refreshTokenLen = typeof oauth.refreshToken === "string" ? oauth.refreshToken.length : 0;
18918
18961
  try {
18919
- deps.writeFile(blob);
18962
+ deps.writeFile(blob, refreshTokenLen);
18920
18963
  } catch (err) {
18921
18964
  deps.log(`[onboarding] op=login-writeback result=write-failed service=${JSON.stringify(service)} err=${err instanceof Error ? err.message : String(err)}`);
18922
18965
  return { wrote: false, reason: "write-failed", bytes: 0, service };
@@ -19180,7 +19223,35 @@ function readUsersFile2() {
19180
19223
  if (!raw) return [];
19181
19224
  return JSON.parse(raw);
19182
19225
  }
19183
- var darwinAuthChild = null;
19226
+ var pendingAuthLogin = null;
19227
+ async function endLogin(pending) {
19228
+ if (!pending) return { priorId: null, priorPid: null, tabClosed: null };
19229
+ let tabClosed = null;
19230
+ const targetId = readAuthTargetId();
19231
+ if (targetId && readAuthAttemptId() === pending.attemptId) {
19232
+ tabClosed = await closeAuthTarget(targetId);
19233
+ }
19234
+ const priorPid = pending.child?.pid ?? null;
19235
+ try {
19236
+ pending.child?.kill("SIGTERM");
19237
+ } catch {
19238
+ }
19239
+ return { priorId: pending.attemptId, priorPid, tabClosed };
19240
+ }
19241
+ function adoptOrStopChild(attemptId, child) {
19242
+ if (pendingAuthLogin?.attemptId === attemptId) {
19243
+ pendingAuthLogin.child = child;
19244
+ return true;
19245
+ }
19246
+ try {
19247
+ child.kill("SIGTERM");
19248
+ } catch {
19249
+ }
19250
+ return false;
19251
+ }
19252
+ function formatEnded(ended) {
19253
+ return `superseded=${ended.priorId ?? "none"} priorPid=${ended.priorPid ?? "none"} tabClosed=${ended.tabClosed === null ? "na" : ended.tabClosed}`;
19254
+ }
19184
19255
  var darwinCredsBefore = {};
19185
19256
  async function checkAuthStatus() {
19186
19257
  let status;
@@ -19221,7 +19292,11 @@ app13.post("/claude-auth", async (c) => {
19221
19292
  const remoteAddress = c.env?.incoming?.socket?.remoteAddress;
19222
19293
  const transport = resolveBrowserTransport(c.req.raw, remoteAddress);
19223
19294
  if (body.action === "stop") {
19224
- return c.json({ stopped: true });
19295
+ const superseded2 = pendingAuthLogin;
19296
+ pendingAuthLogin = null;
19297
+ const ended2 = await endLogin(superseded2);
19298
+ console.log(`[onboarding] op=auth-login-stop attempt=${ended2.priorId ?? "none"} outcome=${ended2.priorId ? "stopped" : "no-pending"} priorPid=${ended2.priorPid ?? "none"} tabClosed=${ended2.tabClosed === null ? "na" : ended2.tabClosed}`);
19299
+ return c.json({ stopped: ended2.priorId !== null });
19225
19300
  }
19226
19301
  if (body.action === "logout") {
19227
19302
  const start = Date.now();
@@ -19246,7 +19321,7 @@ app13.post("/claude-auth", async (c) => {
19246
19321
  }
19247
19322
  if (body.action === "paste-code") {
19248
19323
  const code = typeof body.code === "string" ? body.code : "";
19249
- const child = darwinAuthChild;
19324
+ const child = pendingAuthLogin?.child ?? null;
19250
19325
  if (!child || !child.stdin || child.stdin.destroyed) {
19251
19326
  console.log("[onboarding] op=claude-paste-code accepted=false reason=no-active-login");
19252
19327
  return c.json({ accepted: false, error: "No active login to paste into. Start sign-in first." }, 400);
@@ -19268,8 +19343,13 @@ app13.post("/claude-auth", async (c) => {
19268
19343
  const cdpReady = await ensureCdp(transport);
19269
19344
  return c.json({ launched: cdpReady });
19270
19345
  }
19346
+ const attemptId = randomUUID4().slice(0, 8);
19347
+ const superseded = pendingAuthLogin;
19348
+ pendingAuthLogin = { child: null, attemptId, startedAt: Date.now() };
19349
+ const ended = await endLogin(superseded);
19271
19350
  ensureLogDir();
19272
- writeFileSync14(logPath("claude-auth"), "");
19351
+ appendFileSync3(logPath("claude-auth"), `==== attempt ${attemptId} ${(/* @__PURE__ */ new Date()).toISOString()} ====
19352
+ `);
19273
19353
  const claudeAuthLogFd = openSync8(logPath("claude-auth"), "a");
19274
19354
  const onClaudeOutput = (chunk) => writeSync(claudeAuthLogFd, chunk);
19275
19355
  if (process.platform === "darwin") {
@@ -19280,7 +19360,8 @@ app13.post("/claude-auth", async (c) => {
19280
19360
  stdio: ["pipe", "pipe", "pipe"]
19281
19361
  });
19282
19362
  claudeProc2.unref();
19283
- darwinAuthChild = claudeProc2;
19363
+ const adopted2 = adoptOrStopChild(attemptId, claudeProc2);
19364
+ console.log(`[onboarding] op=auth-login-start attempt=${attemptId} platform=darwin transport=native outcome=${adopted2 ? "started" : "self-superseded"} ${formatEnded(ended)}`);
19284
19365
  claudeProc2.stdin?.on("error", () => {
19285
19366
  });
19286
19367
  claudeProc2.stdout?.on("data", onClaudeOutput);
@@ -19292,11 +19373,9 @@ app13.post("/claude-auth", async (c) => {
19292
19373
  before: darwinCredsBefore,
19293
19374
  listMdats: listHostCredsMdats,
19294
19375
  readBlob: readHostCredsBlob,
19295
- writeFile: (blob) => {
19296
- const tmp = `${CLAUDE_CREDENTIALS_FILE}.${process.pid}.tmp`;
19297
- writeFileSync14(tmp, blob, { mode: 384 });
19298
- renameSync8(tmp, CLAUDE_CREDENTIALS_FILE);
19299
- },
19376
+ // Task 2572 — the write itself lives in claude-auth.ts beside the other
19377
+ // writer of this file, so both emit provenance and both are testable.
19378
+ writeFile: (blob, refreshTokenLen) => writeHostCredsFile(CLAUDE_CREDENTIALS_FILE, blob, refreshTokenLen),
19300
19379
  log: (line) => console.log(line)
19301
19380
  });
19302
19381
  persistKeychainService(
@@ -19304,7 +19383,7 @@ app13.post("/claude-auth", async (c) => {
19304
19383
  (service) => writeFileSync14(join37(dirname13(CLAUDE_CREDENTIALS_FILE), ".keychain-service"), service, { mode: 384 }),
19305
19384
  (line) => console.log(line)
19306
19385
  );
19307
- if (darwinAuthChild === claudeProc2) darwinAuthChild = null;
19386
+ if (pendingAuthLogin?.child === claudeProc2) pendingAuthLogin = null;
19308
19387
  });
19309
19388
  return c.json({ started: true, transport: "native" });
19310
19389
  }
@@ -19313,7 +19392,7 @@ app13.post("/claude-auth", async (c) => {
19313
19392
  if (!vncReady) return c.json({ error: "VNC display failed to start" }, 500);
19314
19393
  }
19315
19394
  await ensureCdp(transport);
19316
- const chromiumWrapper = writeChromiumWrapper();
19395
+ const chromiumWrapper = writeChromiumWrapper(attemptId);
19317
19396
  const x11Env = buildX11Env(chromiumWrapper, transport);
19318
19397
  vncLog("claude-auth", { action: "start", transport });
19319
19398
  const claudeProc = spawn("claude", ["auth", "login"], {
@@ -19321,9 +19400,14 @@ app13.post("/claude-auth", async (c) => {
19321
19400
  stdio: ["ignore", "pipe", "pipe"]
19322
19401
  });
19323
19402
  claudeProc.unref();
19403
+ const adopted = adoptOrStopChild(attemptId, claudeProc);
19404
+ console.log(`[onboarding] op=auth-login-start attempt=${attemptId} platform=${process.platform} transport=${transport} outcome=${adopted ? "started" : "self-superseded"} ${formatEnded(ended)}`);
19324
19405
  claudeProc.stdout?.on("data", onClaudeOutput);
19325
19406
  claudeProc.stderr?.on("data", onClaudeOutput);
19326
- claudeProc.once("close", () => closeSync8(claudeAuthLogFd));
19407
+ claudeProc.once("close", () => {
19408
+ closeSync8(claudeAuthLogFd);
19409
+ if (pendingAuthLogin?.child === claudeProc) pendingAuthLogin = null;
19410
+ });
19327
19411
  await waitForAuthPage(2e4);
19328
19412
  return c.json({ started: true, transport });
19329
19413
  });
@@ -20003,7 +20087,7 @@ init_dist();
20003
20087
  init_paths();
20004
20088
  init_remote_auth();
20005
20089
  init_loop_registry();
20006
- import { appendFileSync as appendFileSync3, existsSync as existsSync27, renameSync as renameSync9, statSync as statSync19 } from "fs";
20090
+ import { appendFileSync as appendFileSync4, existsSync as existsSync27, renameSync as renameSync9, statSync as statSync19 } from "fs";
20007
20091
  import { join as join38 } from "path";
20008
20092
  var CLIENT_ERRORS_LOG = join38(LOG_DIR, "client-errors.log");
20009
20093
  var MAX_LOG_SIZE = 10 * 1024 * 1024;
@@ -20149,7 +20233,7 @@ app15.post("/", async (c) => {
20149
20233
  const safe = typeof v === "string" ? truncate(v, 200) : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v).slice(0, 200);
20150
20234
  return `${k}=${safe}`;
20151
20235
  }).join(" ");
20152
- const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["stream-log-click", "pwa", "graph-settle", "dashboard-load", "routines", "calendar-legibility"]);
20236
+ const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["stream-log-click", "pwa", "graph-settle", "dashboard-load", "routines", "calendar-legibility", "channels-card"]);
20153
20237
  if (TAGGED_PREFIX_SOURCES.has(source)) {
20154
20238
  console.log(
20155
20239
  `[${source}] ts=${ts} ip=${ip} version=${version || "unknown"}${extra ? " " + extra : ""}`
@@ -20182,7 +20266,7 @@ app15.post("/", async (c) => {
20182
20266
  tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
20183
20267
  status: typeof body.status === "number" ? body.status : void 0
20184
20268
  };
20185
- appendFileSync3(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
20269
+ appendFileSync4(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
20186
20270
  } catch (err) {
20187
20271
  console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
20188
20272
  }
@@ -23275,18 +23359,18 @@ var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
23275
23359
  ]);
23276
23360
  var app27 = new Hono();
23277
23361
  app27.post("/", async (c) => {
23278
- const TAG66 = "[admin:events]";
23362
+ const TAG68 = "[admin:events]";
23279
23363
  let body;
23280
23364
  try {
23281
23365
  body = await c.req.json();
23282
23366
  } catch (err) {
23283
23367
  const detail = err instanceof Error ? err.message : String(err);
23284
- console.error(`${TAG66} reject reason=body-not-json detail=${detail}`);
23368
+ console.error(`${TAG68} reject reason=body-not-json detail=${detail}`);
23285
23369
  return c.json({ ok: false, detail: "Request body was not valid JSON" }, 400);
23286
23370
  }
23287
23371
  const event = typeof body.event === "string" ? body.event : "";
23288
23372
  if (!ALLOWED_EVENTS.has(event)) {
23289
- console.error(`${TAG66} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
23373
+ console.error(`${TAG68} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
23290
23374
  return c.json({ ok: false, detail: `Event "${event}" is not allowed` }, 400);
23291
23375
  }
23292
23376
  const rawFields = body.fields && typeof body.fields === "object" ? body.fields : {};
@@ -34985,6 +35069,68 @@ function startSharedFolderCensus() {
34985
35069
  });
34986
35070
  }
34987
35071
 
35072
+ // app/lib/cred-refresh-census.ts
35073
+ init_loop_registry();
35074
+ import { readdirSync as readdirSync31, readFileSync as readFileSync53 } from "fs";
35075
+ import { homedir as homedir5 } from "os";
35076
+ import { join as join59 } from "path";
35077
+ var TAG60 = "[claude-auth]";
35078
+ function refreshTokenLength(raw) {
35079
+ let data;
35080
+ try {
35081
+ data = JSON.parse(raw);
35082
+ } catch {
35083
+ return null;
35084
+ }
35085
+ const oauth = data.claudeAiOauth;
35086
+ if (!oauth || typeof oauth !== "object") return null;
35087
+ return typeof oauth.refreshToken === "string" ? oauth.refreshToken.length : null;
35088
+ }
35089
+ function surveyBrandCredentials(home) {
35090
+ let entries2;
35091
+ try {
35092
+ entries2 = readdirSync31(home);
35093
+ } catch {
35094
+ return [];
35095
+ }
35096
+ const rows = [];
35097
+ for (const entry2 of entries2.sort()) {
35098
+ if (!entry2.startsWith(".")) continue;
35099
+ let raw;
35100
+ try {
35101
+ raw = readFileSync53(join59(home, entry2, ".claude", ".credentials.json"), "utf-8");
35102
+ } catch {
35103
+ continue;
35104
+ }
35105
+ rows.push({ brand: entry2.slice(1), refreshTokenLen: refreshTokenLength(raw) });
35106
+ }
35107
+ return rows;
35108
+ }
35109
+ function isBlank(row) {
35110
+ return row.refreshTokenLen === null || row.refreshTokenLen === 0;
35111
+ }
35112
+ function censusResult(rows) {
35113
+ if (rows.length === 0) return "no-brands-found";
35114
+ return rows.some(isBlank) ? "blank-tokens" : "ok";
35115
+ }
35116
+ function runCredRefreshCensus(home = homedir5()) {
35117
+ const rows = surveyBrandCredentials(home);
35118
+ const blank = rows.filter(isBlank).length;
35119
+ const detail = rows.map((r) => `${r.brand}:${r.refreshTokenLen === null ? "unreadable" : r.refreshTokenLen}`).join(",");
35120
+ console.log(
35121
+ `${TAG60} op=cred-refresh-census result=${censusResult(rows)} brands=${rows.length} blank=${blank} detail=${detail}`
35122
+ );
35123
+ return `${rows.length} brand(s), ${blank} blank`;
35124
+ }
35125
+ function startCredRefreshCensus() {
35126
+ registerLoop({
35127
+ name: "cred-refresh-census",
35128
+ intervalMs: 60 * 60 * 1e3,
35129
+ firstRunDelayMs: 0,
35130
+ run: () => runCredRefreshCensus()
35131
+ });
35132
+ }
35133
+
34988
35134
  // app/lib/file-watcher.ts
34989
35135
  init_data_path();
34990
35136
  import * as fsp2 from "fs/promises";
@@ -35222,7 +35368,7 @@ init_account();
35222
35368
  init_neo4j_store();
35223
35369
  import { readdir as readdir7, readFile as readFile12, rename as rename5, writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
35224
35370
  import { existsSync as existsSync50 } from "fs";
35225
- import { join as join59 } from "path";
35371
+ import { join as join60 } from "path";
35226
35372
  var ADMIN_ROLE2 = "admin";
35227
35373
  var WEBCHAT_CHANNEL2 = "webchat";
35228
35374
  var SESSION_META_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.meta\.json$/i;
@@ -35267,7 +35413,7 @@ async function collectLiveSidecars(projectsRoot) {
35267
35413
  }
35268
35414
  for (const slug of slugs) {
35269
35415
  if (!slug.isDirectory()) continue;
35270
- const slugDir = join59(projectsRoot, slug.name);
35416
+ const slugDir = join60(projectsRoot, slug.name);
35271
35417
  let entries2;
35272
35418
  try {
35273
35419
  entries2 = await readdir7(slugDir, { withFileTypes: true });
@@ -35276,9 +35422,9 @@ async function collectLiveSidecars(projectsRoot) {
35276
35422
  }
35277
35423
  for (const entry2 of entries2) {
35278
35424
  if (entry2.isFile() && SESSION_META_RE.test(entry2.name)) {
35279
- out.push(join59(slugDir, entry2.name));
35425
+ out.push(join60(slugDir, entry2.name));
35280
35426
  } else if (entry2.isDirectory() && entry2.name === "subagents") {
35281
- const subDir = join59(slugDir, entry2.name);
35427
+ const subDir = join60(slugDir, entry2.name);
35282
35428
  let subs;
35283
35429
  try {
35284
35430
  subs = await readdir7(subDir, { withFileTypes: true });
@@ -35286,7 +35432,7 @@ async function collectLiveSidecars(projectsRoot) {
35286
35432
  continue;
35287
35433
  }
35288
35434
  for (const s of subs) {
35289
- if (s.isFile() && SESSION_META_RE.test(s.name)) out.push(join59(subDir, s.name));
35435
+ if (s.isFile() && SESSION_META_RE.test(s.name)) out.push(join60(subDir, s.name));
35290
35436
  }
35291
35437
  }
35292
35438
  }
@@ -35298,7 +35444,7 @@ function shortId(path) {
35298
35444
  return base.slice(0, 8);
35299
35445
  }
35300
35446
  async function migrateAdminWebchatSidecars(opts = {}) {
35301
- const projectsRoot = opts.projectsRoot ?? (process.env.CLAUDE_CONFIG_DIR ? join59(process.env.CLAUDE_CONFIG_DIR, "projects") : null) ?? "";
35447
+ const projectsRoot = opts.projectsRoot ?? (process.env.CLAUDE_CONFIG_DIR ? join60(process.env.CLAUDE_CONFIG_DIR, "projects") : null) ?? "";
35302
35448
  const usersFile = opts.usersFile ?? USERS_FILE;
35303
35449
  const accountId = opts.accountId ?? resolveAccount()?.accountId ?? null;
35304
35450
  const resolveName = opts.resolveName ?? defaultResolveName;
@@ -35376,7 +35522,7 @@ async function migrateAdminWebchatSidecars(opts = {}) {
35376
35522
  init_account();
35377
35523
  import { readdir as readdir8, readFile as readFile13, rename as rename6, writeFile as writeFile6, unlink as unlink5 } from "fs/promises";
35378
35524
  import { existsSync as existsSync51 } from "fs";
35379
- import { join as join60 } from "path";
35525
+ import { join as join61 } from "path";
35380
35526
  var SESSION_META_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.meta\.json$/i;
35381
35527
  function projectSlugForCwd(cwd) {
35382
35528
  return cwd.replace(/\//g, "-");
@@ -35422,7 +35568,7 @@ async function collectSidecars(projectsRoot) {
35422
35568
  }
35423
35569
  for (const slug of slugs) {
35424
35570
  if (!slug.isDirectory()) continue;
35425
- const slugDir = join60(projectsRoot, slug.name);
35571
+ const slugDir = join61(projectsRoot, slug.name);
35426
35572
  let entries2;
35427
35573
  try {
35428
35574
  entries2 = await readdir8(slugDir, { withFileTypes: true });
@@ -35431,9 +35577,9 @@ async function collectSidecars(projectsRoot) {
35431
35577
  }
35432
35578
  for (const entry2 of entries2) {
35433
35579
  if (entry2.isFile() && SESSION_META_RE2.test(entry2.name)) {
35434
- out.push({ path: join60(slugDir, entry2.name), slug: slug.name });
35580
+ out.push({ path: join61(slugDir, entry2.name), slug: slug.name });
35435
35581
  } else if (entry2.isDirectory() && (entry2.name === "subagents" || entry2.name === "archive")) {
35436
- const subDir = join60(slugDir, entry2.name);
35582
+ const subDir = join61(slugDir, entry2.name);
35437
35583
  let subs;
35438
35584
  try {
35439
35585
  subs = await readdir8(subDir, { withFileTypes: true });
@@ -35442,7 +35588,7 @@ async function collectSidecars(projectsRoot) {
35442
35588
  }
35443
35589
  for (const s of subs) {
35444
35590
  if (s.isFile() && SESSION_META_RE2.test(s.name)) {
35445
- out.push({ path: join60(subDir, s.name), slug: slug.name });
35591
+ out.push({ path: join61(subDir, s.name), slug: slug.name });
35446
35592
  }
35447
35593
  }
35448
35594
  }
@@ -35451,7 +35597,7 @@ async function collectSidecars(projectsRoot) {
35451
35597
  return out;
35452
35598
  }
35453
35599
  async function migrateSidecarAccountIds(opts = {}) {
35454
- const projectsRoot = opts.projectsRoot ?? (process.env.CLAUDE_CONFIG_DIR ? join60(process.env.CLAUDE_CONFIG_DIR, "projects") : null) ?? "";
35600
+ const projectsRoot = opts.projectsRoot ?? (process.env.CLAUDE_CONFIG_DIR ? join61(process.env.CLAUDE_CONFIG_DIR, "projects") : null) ?? "";
35455
35601
  const accounts = opts.accounts ?? listValidAccounts();
35456
35602
  const result = {
35457
35603
  scanned: 0,
@@ -35546,13 +35692,13 @@ var InboundHub = class {
35546
35692
  if (s.subscriber && s.ready) {
35547
35693
  if (payload.accountId !== s.accountId) this.crossAccountDelivered++;
35548
35694
  s.subscriber(payload);
35549
- s.inFlight.push({ payload, state: "drained", enqueuedAt: now, drainedAt: now });
35695
+ s.inFlight.push({ payload, state: "drained", enqueuedAt: now, drainedAt: now, spawnUndeliveredAt: null });
35550
35696
  return;
35551
35697
  }
35552
35698
  if (this.subscriberAccounts(payload.senderId).some((a) => a !== payload.accountId)) {
35553
35699
  this.crossAccountQueued++;
35554
35700
  }
35555
- s.inFlight.push({ payload, state: "queued", enqueuedAt: now, drainedAt: null });
35701
+ s.inFlight.push({ payload, state: "queued", enqueuedAt: now, drainedAt: null, spawnUndeliveredAt: null });
35556
35702
  }
35557
35703
  /** This account's channel server for the sender connected. Register the
35558
35704
  * subscriber but do NOT drain yet — the drain waits for markReady so nothing
@@ -35611,13 +35757,16 @@ var InboundHub = class {
35611
35757
  * is attached -> inbound-undelivered (the readiness handoff broke);
35612
35758
  * - a still-queued message older than queuedThresholdMs with NO subscriber
35613
35759
  * attached -> spawn-undelivered (the channel server never attached: spawn
35614
- * failed, or the session ended and never resumed);
35760
+ * failed, or the session ended and never resumed). Task 2575 — reported
35761
+ * ONCE and RETAINED, because the gateway retries the spawn for it;
35762
+ * - a still-queued message with NO subscriber older than giveUpMs ->
35763
+ * spawn-given-up (Task 2575: the retry window closed), removed;
35615
35764
  * - a drained message unacked past ackThresholdMs -> drain-undelivered (the
35616
35765
  * channel server never confirmed receipt — the silent cold-start drop).
35617
35766
  * Task 2290 — "a subscriber" means one attached for the SAME account, so a
35618
35767
  * message waiting on its own account's cold start is not masked by another
35619
35768
  * account's live subscriber on the same phone. Each message is emitted at
35620
- * most once (removed when emitted). */
35769
+ * most once per kind; every kind but spawn-undelivered removes it. */
35621
35770
  sweep(now, t) {
35622
35771
  const events = [];
35623
35772
  for (const s of this.senders.values()) {
@@ -35627,9 +35776,17 @@ var InboundHub = class {
35627
35776
  events.push({ kind: "inbound-undelivered", accountId, senderId, waMessageId: e.payload.waMessageId, ageMs: now - e.enqueuedAt });
35628
35777
  return false;
35629
35778
  }
35630
- if (e.state === "queued" && !s.subscriber && now - e.enqueuedAt > t.queuedThresholdMs) {
35631
- events.push({ kind: "spawn-undelivered", accountId, senderId, waMessageId: e.payload.waMessageId, ageMs: now - e.enqueuedAt });
35632
- return false;
35779
+ if (e.state === "queued" && !s.subscriber) {
35780
+ const age = now - e.enqueuedAt;
35781
+ if (age > t.giveUpMs) {
35782
+ events.push({ kind: "spawn-given-up", accountId, senderId, waMessageId: e.payload.waMessageId, ageMs: age });
35783
+ return false;
35784
+ }
35785
+ if (age > t.queuedThresholdMs && e.spawnUndeliveredAt == null) {
35786
+ e.spawnUndeliveredAt = now;
35787
+ events.push({ kind: "spawn-undelivered", accountId, senderId, waMessageId: e.payload.waMessageId, ageMs: age });
35788
+ }
35789
+ return true;
35633
35790
  }
35634
35791
  if (e.state === "drained" && e.drainedAt != null && now - e.drainedAt > t.ackThresholdMs) {
35635
35792
  events.push({ kind: "drain-undelivered", accountId, senderId, waMessageId: e.payload.waMessageId, ageMs: now - e.drainedAt });
@@ -35640,6 +35797,23 @@ var InboundHub = class {
35640
35797
  }
35641
35798
  return events;
35642
35799
  }
35800
+ /** Task 2575 — every key holding queued work with NO subscriber whose oldest
35801
+ * message is older than `thresholdMs`. This is the population a refused spawn
35802
+ * leaves behind: the gateway's sweep re-invokes the retained spawn arguments
35803
+ * for each one. Read-only — it emits nothing and mutates nothing, so calling
35804
+ * it cannot consume a terminal event the sweep is about to report. */
35805
+ heldSpawns(now, thresholdMs) {
35806
+ const out = [];
35807
+ for (const s of this.senders.values()) {
35808
+ if (s.subscriber) continue;
35809
+ const queued = s.inFlight.filter((e) => e.state === "queued");
35810
+ if (queued.length === 0) continue;
35811
+ const oldestAgeMs = Math.max(...queued.map((e) => now - e.enqueuedAt));
35812
+ if (oldestAgeMs <= thresholdMs) continue;
35813
+ out.push({ accountId: s.accountId, senderId: s.senderId, oldestAgeMs, count: queued.length });
35814
+ }
35815
+ return out;
35816
+ }
35643
35817
  /** Task 2290 — every account holding a live subscriber for this phone, sorted.
35644
35818
  * Read-only. Feeds the cross-account field on the inbound log line and the
35645
35819
  * queued census, so a phone shared across accounts is greppable at the moment
@@ -36142,6 +36316,7 @@ init_loop_registry();
36142
36316
  var QUEUED_THRESHOLD_MS = 3e4;
36143
36317
  var ACK_THRESHOLD_MS = 3e4;
36144
36318
  var SWEEP_INTERVAL_MS = 15e3;
36319
+ var GIVE_UP_MS = 3e5;
36145
36320
  var MEDIA_STANZA_STALE_MS = 12e4;
36146
36321
  var WaGateway = class {
36147
36322
  constructor(deps) {
@@ -36163,6 +36338,14 @@ var WaGateway = class {
36163
36338
  // effective session account — the file-path validation scope.
36164
36339
  docContexts = /* @__PURE__ */ new Map();
36165
36340
  spawning = /* @__PURE__ */ new Set();
36341
+ /** Task 2575 — the arguments of the last `ensureChannelSession` call per hub
36342
+ * key, so the sweep can re-invoke a spawn that was refused. The refusal is not
36343
+ * visible here — `ensureChannelSession` returns void and server/index.ts logs
36344
+ * and returns on the `{ error }` envelope — so the retry keys off queue state
36345
+ * (a queued message with no subscriber), which is the observable consequence
36346
+ * of the refusal. `attempts` is the per-key retry counter that rides the log
36347
+ * line, reset whenever a fresh inbound takes the spawn path. */
36348
+ heldSpawnArgs = /* @__PURE__ */ new Map();
36166
36349
  seq = 0;
36167
36350
  // Task 2110 — senderId -> the arrival ms of every still-unemitted non-audio
36168
36351
  // media stanza for that sender, oldest first. This is the race window: a reply
@@ -36299,7 +36482,8 @@ var WaGateway = class {
36299
36482
  reconcile(now) {
36300
36483
  const events = this.hub.sweep(now, {
36301
36484
  queuedThresholdMs: QUEUED_THRESHOLD_MS,
36302
- ackThresholdMs: ACK_THRESHOLD_MS
36485
+ ackThresholdMs: ACK_THRESHOLD_MS,
36486
+ giveUpMs: GIVE_UP_MS
36303
36487
  });
36304
36488
  for (const e of events) {
36305
36489
  console.error(
@@ -36321,6 +36505,53 @@ var WaGateway = class {
36321
36505
  );
36322
36506
  return events;
36323
36507
  }
36508
+ /** Task 2575 — re-invoke the retained spawn for every key holding queued work
36509
+ * with no channel server attached. Driven by startSweeper() beside reconcile();
36510
+ * called directly with an injected clock in tests. Returns the number of
36511
+ * ensureChannelSession calls made.
36512
+ *
36513
+ * This is the missing action behind `op=spawn-undelivered`. Before it, that
36514
+ * line was emitted and nothing consumed it: on 2026-08-08 it fired for wa-1 at
36515
+ * ageMs=37743 and the message was never delivered.
36516
+ *
36517
+ * A retry while auth is still dead re-enters the same refusal and returns; the
36518
+ * cost is one refusal line plus one op=spawn-retry line per key per sweep,
36519
+ * bounded by the hub's 5-minute give-up. */
36520
+ async retryHeldSpawns(now) {
36521
+ const held = this.hub.heldSpawns(now, QUEUED_THRESHOLD_MS);
36522
+ let calls = 0;
36523
+ for (const h of held) {
36524
+ const key2 = hubKey(h.accountId, h.senderId);
36525
+ if (this.spawning.has(key2)) continue;
36526
+ const entry2 = this.heldSpawnArgs.get(key2);
36527
+ if (!entry2) {
36528
+ console.error(
36529
+ `[whatsapp-native] op=spawn-retry-skipped senderId=${h.senderId} accountId=${h.accountId} ageMs=${h.oldestAgeMs} queued=${h.count} reason=no-args`
36530
+ );
36531
+ continue;
36532
+ }
36533
+ entry2.attempts++;
36534
+ this.spawning.add(key2);
36535
+ calls++;
36536
+ try {
36537
+ await this.deps.ensureChannelSession(entry2.args);
36538
+ console.error(
36539
+ `[whatsapp-native] op=spawn-retry senderId=${h.senderId} accountId=${h.accountId} attempt=${entry2.attempts} ageMs=${h.oldestAgeMs} queued=${h.count} ok=true`
36540
+ );
36541
+ } catch (err) {
36542
+ console.error(
36543
+ `[whatsapp-native] op=spawn-retry senderId=${h.senderId} accountId=${h.accountId} attempt=${entry2.attempts} ageMs=${h.oldestAgeMs} queued=${h.count} ok=false reason=${err instanceof Error ? err.message : String(err)}`
36544
+ );
36545
+ } finally {
36546
+ this.spawning.delete(key2);
36547
+ }
36548
+ }
36549
+ const heldKeys = new Set(held.map((h) => hubKey(h.accountId, h.senderId)));
36550
+ for (const k of [...this.heldSpawnArgs.keys()]) {
36551
+ if (!heldKeys.has(k) && !this.spawning.has(k)) this.heldSpawnArgs.delete(k);
36552
+ }
36553
+ return calls;
36554
+ }
36324
36555
  /** Arm the periodic reconciliation sweep in the long-lived UI process.
36325
36556
  *
36326
36557
  * Task 1814 — armed through the registry, so it appears on /activity. This
@@ -36338,9 +36569,14 @@ var WaGateway = class {
36338
36569
  return registerLoop({
36339
36570
  name: "whatsapp-native-sweep",
36340
36571
  intervalMs,
36341
- run: () => {
36342
- const events = this.reconcile(Date.now());
36343
- return events.length > 0 ? `${events.length} terminal events` : void 0;
36572
+ run: async () => {
36573
+ const now = Date.now();
36574
+ const events = this.reconcile(now);
36575
+ const retried = await this.retryHeldSpawns(now);
36576
+ const parts = [];
36577
+ if (events.length > 0) parts.push(`${events.length} terminal events`);
36578
+ if (retried > 0) parts.push(`${retried} spawn retries`);
36579
+ return parts.length > 0 ? parts.join(", ") : void 0;
36344
36580
  }
36345
36581
  });
36346
36582
  }
@@ -36448,33 +36684,35 @@ var WaGateway = class {
36448
36684
  console.error(
36449
36685
  `[whatsapp-native] op=spawn-trigger senderId=${senderId} accountId=${input.effectiveAccountId}`
36450
36686
  );
36687
+ const spawnArgs = {
36688
+ accountId: input.accountId,
36689
+ effectiveAccountId: input.effectiveAccountId,
36690
+ senderId,
36691
+ role: input.role ?? "admin",
36692
+ personId: input.personId ?? null,
36693
+ gatewayUrl: this.deps.gatewayUrl,
36694
+ serverPath: this.deps.serverPath,
36695
+ // Task 1575 — the gate's owner-self verdict; drives owner attribution
36696
+ // for an unbound owner self-phone at the spawn site.
36697
+ ownerSelfPhone: input.ownerSelfPhone,
36698
+ // Task 2262 — resolved once in onMessage; its presence routes this
36699
+ // spawn to /public-spawn under the named public agent.
36700
+ publicAgentSlug: input.publicAgentSlug,
36701
+ // Task 2281 — the correlation id, so the spawn outcome lines join the
36702
+ // rest of this inbound's lifeline. Only this call site takes it: the
36703
+ // passive branch above never reaches the public spawn, which is where
36704
+ // op=spawn-ok and op=spawn-failed are emitted.
36705
+ inboundId: input.inboundId,
36706
+ // Task 2234 — the registered-party label, forwarded verbatim so the
36707
+ // spawn env carries WA_SENDER_PARTY. Undefined for every other sender.
36708
+ // Only this call site takes it: the passive branch above hardcodes
36709
+ // role:'admin', and a registered party is public, so the combination
36710
+ // cannot arise there.
36711
+ party: input.party
36712
+ };
36713
+ this.heldSpawnArgs.set(spawnKey, { args: spawnArgs, attempts: 0 });
36451
36714
  try {
36452
- await this.deps.ensureChannelSession({
36453
- accountId: input.accountId,
36454
- effectiveAccountId: input.effectiveAccountId,
36455
- senderId,
36456
- role: input.role ?? "admin",
36457
- personId: input.personId ?? null,
36458
- gatewayUrl: this.deps.gatewayUrl,
36459
- serverPath: this.deps.serverPath,
36460
- // Task 1575 — the gate's owner-self verdict; drives owner attribution
36461
- // for an unbound owner self-phone at the spawn site.
36462
- ownerSelfPhone: input.ownerSelfPhone,
36463
- // Task 2262 — resolved once in onMessage; its presence routes this
36464
- // spawn to /public-spawn under the named public agent.
36465
- publicAgentSlug: input.publicAgentSlug,
36466
- // Task 2281 — the correlation id, so the spawn outcome lines join the
36467
- // rest of this inbound's lifeline. Only this call site takes it: the
36468
- // passive branch above never reaches the public spawn, which is where
36469
- // op=spawn-ok and op=spawn-failed are emitted.
36470
- inboundId: input.inboundId,
36471
- // Task 2234 — the registered-party label, forwarded verbatim so the
36472
- // spawn env carries WA_SENDER_PARTY. Undefined for every other sender.
36473
- // Only this call site takes it: the passive branch above hardcodes
36474
- // role:'admin', and a registered party is public, so the combination
36475
- // cannot arise there.
36476
- party: input.party
36477
- });
36715
+ await this.deps.ensureChannelSession(spawnArgs);
36478
36716
  } finally {
36479
36717
  this.spawning.delete(spawnKey);
36480
36718
  }
@@ -37002,8 +37240,41 @@ function createWebchatChannelRoutes(deps) {
37002
37240
  }
37003
37241
 
37004
37242
  // app/lib/webchat/gateway/resolve-turn-answer.ts
37005
- import { existsSync as existsSync52, openSync as openSync9, readSync as readSync8, closeSync as closeSync9, readdirSync as readdirSync31 } from "fs";
37006
- import { join as join61 } from "path";
37243
+ import { existsSync as existsSync52, openSync as openSync9, readSync as readSync8, closeSync as closeSync9, readdirSync as readdirSync32 } from "fs";
37244
+ import { join as join62 } from "path";
37245
+
37246
+ // app/lib/channel-delivery/outbound-guard.ts
37247
+ var INTERNAL_LEAK_PATTERNS = [
37248
+ { name: "task-id", re: /\bTasks?[\s-]+\d{2,4}\b/g },
37249
+ { name: "media-path", re: /\/tmp\/maxy-media[A-Za-z0-9_./-]*/g },
37250
+ { name: "repo-path", re: /\bplatform\/[A-Za-z0-9_./-]+/g },
37251
+ { name: "bundle", re: /\bserver\.(?:js|cjs|mjs)\b/g },
37252
+ { name: "source-file", re: /\b[A-Za-z0-9_-]+\.tsx?\b/g },
37253
+ { name: "log-tag", re: /\[[a-z]+(?:-[a-z]+)+\]/g },
37254
+ { name: "op-tag", re: /\bop=[a-z][a-z-]*\b/g }
37255
+ ];
37256
+ function redactInternalLeaks(text) {
37257
+ const redactions = [];
37258
+ let clean = text;
37259
+ for (const { name, re } of INTERNAL_LEAK_PATTERNS) {
37260
+ clean = clean.replace(re, (m) => {
37261
+ redactions.push({ pattern: name, match: m });
37262
+ return "[redacted]";
37263
+ });
37264
+ }
37265
+ return { clean, redactions };
37266
+ }
37267
+ function guardOutbound(senderId, text) {
37268
+ const { clean, redactions } = redactInternalLeaks(text);
37269
+ for (const r of redactions) {
37270
+ console.error(
37271
+ `[channel-guard] op=redact senderId=${senderId} pattern=${r.pattern} bytes=${Buffer.byteLength(r.match, "utf8")}`
37272
+ );
37273
+ }
37274
+ return clean;
37275
+ }
37276
+
37277
+ // app/lib/webchat/gateway/resolve-turn-answer.ts
37007
37278
  function resolveTurnAnswer(p) {
37008
37279
  if (!SESSION_ID_RE2.test(p.sessionId)) return "";
37009
37280
  const path = findJsonl(p.projectsRoot, p.sessionId);
@@ -37028,18 +37299,18 @@ function resolveTurnAnswer(p) {
37028
37299
  for (const turn of turns) {
37029
37300
  if (turn.kind === "agent-webchat-final" || turn.kind === "agent-reply") parts.push(turn.text);
37030
37301
  }
37031
- return segmentText(parts.join("\n\n")).join("\n\n");
37302
+ return segmentText(guardOutbound(p.senderId, parts.join("\n\n"))).join("\n\n");
37032
37303
  }
37033
37304
  function findJsonl(projectsRoot, sessionId) {
37034
37305
  if (!existsSync52(projectsRoot)) return null;
37035
37306
  let slugs;
37036
37307
  try {
37037
- slugs = readdirSync31(projectsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
37308
+ slugs = readdirSync32(projectsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
37038
37309
  } catch {
37039
37310
  return null;
37040
37311
  }
37041
37312
  for (const slug of slugs) {
37042
- const live = join61(projectsRoot, slug, `${sessionId}.jsonl`);
37313
+ const live = join62(projectsRoot, slug, `${sessionId}.jsonl`);
37043
37314
  if (existsSync52(live)) return live;
37044
37315
  }
37045
37316
  return null;
@@ -37187,7 +37458,7 @@ var WebchatGateway = class _WebchatGateway {
37187
37458
  if (rec) rec.lastActivityAt = Date.now();
37188
37459
  const awaiter = this.replyAwaiters.get(key2);
37189
37460
  const projectsRoot = this.deps.publicProjectsRoot?.() ?? "";
37190
- const text = projectsRoot ? resolveTurnAnswer({ ...turn, projectsRoot }) : "";
37461
+ const text = projectsRoot ? resolveTurnAnswer({ ...turn, senderId: keyDisplay(key2), projectsRoot }) : "";
37191
37462
  const bytes = Buffer.byteLength(text, "utf8");
37192
37463
  const deliveredAt = this.lastDeliveredAt.get(key2);
37193
37464
  const latency = deliveredAt != null ? Date.now() - deliveredAt : -1;
@@ -37682,37 +37953,6 @@ var AnswerAccumulator = class {
37682
37953
  }
37683
37954
  };
37684
37955
 
37685
- // app/lib/channel-delivery/outbound-guard.ts
37686
- var INTERNAL_LEAK_PATTERNS = [
37687
- { name: "task-id", re: /\bTasks?[\s-]+\d{2,4}\b/g },
37688
- { name: "media-path", re: /\/tmp\/maxy-media[A-Za-z0-9_./-]*/g },
37689
- { name: "repo-path", re: /\bplatform\/[A-Za-z0-9_./-]+/g },
37690
- { name: "bundle", re: /\bserver\.(?:js|cjs|mjs)\b/g },
37691
- { name: "source-file", re: /\b[A-Za-z0-9_-]+\.tsx?\b/g },
37692
- { name: "log-tag", re: /\[[a-z]+(?:-[a-z]+)+\]/g },
37693
- { name: "op-tag", re: /\bop=[a-z][a-z-]*\b/g }
37694
- ];
37695
- function redactInternalLeaks(text) {
37696
- const redactions = [];
37697
- let clean = text;
37698
- for (const { name, re } of INTERNAL_LEAK_PATTERNS) {
37699
- clean = clean.replace(re, (m) => {
37700
- redactions.push({ pattern: name, match: m });
37701
- return "[redacted]";
37702
- });
37703
- }
37704
- return { clean, redactions };
37705
- }
37706
- function guardOutbound(senderId, text) {
37707
- const { clean, redactions } = redactInternalLeaks(text);
37708
- for (const r of redactions) {
37709
- console.error(
37710
- `[channel-guard] op=redact senderId=${senderId} pattern=${r.pattern} bytes=${Buffer.byteLength(r.match, "utf8")}`
37711
- );
37712
- }
37713
- return clean;
37714
- }
37715
-
37716
37956
  // app/lib/channel-delivery/emitter.ts
37717
37957
  function followerPendingMaxMs() {
37718
37958
  return Number(process.env.CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS ?? String(3e5));
@@ -38054,14 +38294,14 @@ function makeFileDelivery(opts) {
38054
38294
  }
38055
38295
 
38056
38296
  // app/lib/webchat/file-delivery.ts
38057
- var TAG60 = "[webchat-adaptor]";
38297
+ var TAG61 = "[webchat-adaptor]";
38058
38298
  function platformRoot2() {
38059
38299
  return process.env.MAXY_PLATFORM_ROOT || "";
38060
38300
  }
38061
38301
  function makeWebchatSendFile(entry2) {
38062
38302
  return async (filePath) => {
38063
38303
  if (!entry2.accountId) {
38064
- console.error(`${TAG60} file-delivery reject reason=no-account sender=${entry2.senderId}`);
38304
+ console.error(`${TAG61} file-delivery reject reason=no-account sender=${entry2.senderId}`);
38065
38305
  return { ok: false, error: "no-account" };
38066
38306
  }
38067
38307
  const accountDir = resolve47(platformRoot2(), "..", "data/accounts", entry2.accountId);
@@ -38069,14 +38309,14 @@ function makeWebchatSendFile(entry2) {
38069
38309
  const resolved = realpathSync9(filePath);
38070
38310
  const accountResolved = realpathSync9(accountDir);
38071
38311
  if (!resolved.startsWith(accountResolved + "/")) {
38072
- console.error(`${TAG60} file-delivery reject reason=outside_account_directory sender=${entry2.senderId}`);
38312
+ console.error(`${TAG61} file-delivery reject reason=outside_account_directory sender=${entry2.senderId}`);
38073
38313
  return { ok: false, error: "outside-account" };
38074
38314
  }
38075
38315
  return { ok: true };
38076
38316
  } catch (err) {
38077
38317
  const code = err.code;
38078
38318
  console.error(
38079
- `${TAG60} file-delivery reject reason=${code === "ENOENT" ? "not-found" : "path-error"} sender=${entry2.senderId}`
38319
+ `${TAG61} file-delivery reject reason=${code === "ENOENT" ? "not-found" : "path-error"} sender=${entry2.senderId}`
38080
38320
  );
38081
38321
  return { ok: false, error: code === "ENOENT" ? "not-found" : "path-error" };
38082
38322
  }
@@ -38085,7 +38325,7 @@ function makeWebchatSendFile(entry2) {
38085
38325
  function makeWebchatFileDelivery(entry2) {
38086
38326
  return makeFileDelivery({
38087
38327
  entry: entry2,
38088
- tag: TAG60,
38328
+ tag: TAG61,
38089
38329
  channel: "webchat",
38090
38330
  sendFile: makeWebchatSendFile(entry2),
38091
38331
  deferUntilVerdict: true
@@ -38165,7 +38405,7 @@ function buildPublicWebchatSpawnRequest(input) {
38165
38405
  }
38166
38406
 
38167
38407
  // app/lib/whatsapp/inbound/file-delivery-bridge.ts
38168
- var TAG61 = "[whatsapp-adaptor]";
38408
+ var TAG62 = "[whatsapp-adaptor]";
38169
38409
  var SEND_USER_FILE2 = "SendUserFile";
38170
38410
  var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
38171
38411
  function platformRoot3() {
@@ -38183,7 +38423,7 @@ function makeWhatsAppSendFile(entry2, maxyAccountId) {
38183
38423
  });
38184
38424
  if (result.ok) return { ok: true };
38185
38425
  console.error(
38186
- `${TAG61} file-delivery reject reason=send-failed sender=${entry2.senderId} status=${result.status} message=${result.error}`
38426
+ `${TAG62} file-delivery reject reason=send-failed sender=${entry2.senderId} status=${result.status} message=${result.error}`
38187
38427
  );
38188
38428
  return { ok: false, error: result.error };
38189
38429
  };
@@ -38191,7 +38431,7 @@ function makeWhatsAppSendFile(entry2, maxyAccountId) {
38191
38431
  function makeWhatsAppFileDelivery(entry2, maxyAccountId) {
38192
38432
  const shared = makeFileDelivery({
38193
38433
  entry: entry2,
38194
- tag: TAG61,
38434
+ tag: TAG62,
38195
38435
  channel: "whatsapp",
38196
38436
  sendFile: makeWhatsAppSendFile(entry2, maxyAccountId)
38197
38437
  });
@@ -38226,7 +38466,7 @@ function makeWhatsAppFileDelivery(entry2, maxyAccountId) {
38226
38466
  if (!delivered) {
38227
38467
  const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
38228
38468
  console.error(
38229
- `${TAG61} file-delivery-unreconciled sender=${entry2.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
38469
+ `${TAG62} file-delivery-unreconciled sender=${entry2.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
38230
38470
  );
38231
38471
  }
38232
38472
  }
@@ -38391,6 +38631,34 @@ function whatsappDoorLabel(doorId) {
38391
38631
  return decoded ? `${id82(decoded.accountId)}:${id82(decoded.senderId)}` : id82(doorId);
38392
38632
  }
38393
38633
 
38634
+ // app/lib/channel-delivery/refusal-notice.ts
38635
+ var TAG63 = "[channel-delivery]";
38636
+ var notified = /* @__PURE__ */ new Set();
38637
+ function refusalNoticeText(reason) {
38638
+ return reason === "claude-auth-dead" ? "Your message arrived but no assistant could be started: the Claude sign-in on this install is not active. An operator has to sign in again; send this again once they have." : `Your message arrived but no assistant could be started (${reason}). Nothing was lost; try again shortly.`;
38639
+ }
38640
+ async function sendRefusalNotice(input) {
38641
+ if (notified.has(input.doorId)) return;
38642
+ const fields = `channel=${input.channel} sender=${input.sender} reason=${input.reason}`;
38643
+ if (!input.send) {
38644
+ console.error(`${TAG63} op=refusal-notice-failed ${fields} error=no-target`);
38645
+ return;
38646
+ }
38647
+ try {
38648
+ await input.send(guardOutbound(input.sender, refusalNoticeText(input.reason)));
38649
+ } catch (e) {
38650
+ console.error(
38651
+ `${TAG63} op=refusal-notice-failed ${fields} error=${e instanceof Error ? e.message : String(e)}`
38652
+ );
38653
+ return;
38654
+ }
38655
+ notified.add(input.doorId);
38656
+ console.error(`${TAG63} op=refusal-notice ${fields} status=${input.status}`);
38657
+ }
38658
+ function clearRefusalNotice(doorId) {
38659
+ notified.delete(doorId);
38660
+ }
38661
+
38394
38662
  // app/lib/telegram/gateway/inbound-hub.ts
38395
38663
  var InboundHub3 = class {
38396
38664
  keys = /* @__PURE__ */ new Map();
@@ -38411,9 +38679,9 @@ var InboundHub3 = class {
38411
38679
  const s = this.state(payload.key);
38412
38680
  if (s.subscriber && s.ready) {
38413
38681
  s.subscriber(payload);
38414
- s.inFlight.push({ payload, state: "drained", enqueuedAt: now, drainedAt: now });
38682
+ s.inFlight.push({ payload, state: "drained", enqueuedAt: now, drainedAt: now, spawnUndeliveredAt: null });
38415
38683
  } else {
38416
- s.inFlight.push({ payload, state: "queued", enqueuedAt: now, drainedAt: null });
38684
+ s.inFlight.push({ payload, state: "queued", enqueuedAt: now, drainedAt: null, spawnUndeliveredAt: null });
38417
38685
  }
38418
38686
  }
38419
38687
  /** A key's channel server connected. Register the subscriber but do NOT
@@ -38473,9 +38741,18 @@ var InboundHub3 = class {
38473
38741
  * that never reached the agent:
38474
38742
  * - a still-queued message older than queuedThresholdMs WHILE a subscriber
38475
38743
  * is attached -> inbound-undelivered (the readiness handoff broke);
38744
+ * - a still-queued message older than queuedThresholdMs with NO subscriber
38745
+ * attached -> spawn-undelivered (the channel server never attached: the
38746
+ * spawn was refused, or the session ended and never resumed). Task 2575 —
38747
+ * this case did not exist on this door at all before, which is why tg-1
38748
+ * survived the 2026-08-08 outage unreported. Reported ONCE and RETAINED,
38749
+ * because the gateway retries the spawn for it;
38750
+ * - a still-queued message with NO subscriber older than giveUpMs ->
38751
+ * spawn-given-up (Task 2575: the retry window closed), removed;
38476
38752
  * - a drained message unacked past ackThresholdMs -> drain-undelivered (the
38477
38753
  * channel server never confirmed receipt — the silent cold-start drop).
38478
- * Each message is emitted at most once (removed when emitted). */
38754
+ * Each message is emitted at most once per kind; every kind but
38755
+ * spawn-undelivered removes it. */
38479
38756
  sweep(now, t) {
38480
38757
  const events = [];
38481
38758
  for (const [key2, s] of this.keys) {
@@ -38484,6 +38761,18 @@ var InboundHub3 = class {
38484
38761
  events.push({ kind: "inbound-undelivered", key: key2, messageId: e.payload.messageId, ageMs: now - e.enqueuedAt });
38485
38762
  return false;
38486
38763
  }
38764
+ if (e.state === "queued" && !s.subscriber) {
38765
+ const age = now - e.enqueuedAt;
38766
+ if (age > t.giveUpMs) {
38767
+ events.push({ kind: "spawn-given-up", key: key2, messageId: e.payload.messageId, ageMs: age });
38768
+ return false;
38769
+ }
38770
+ if (age > t.queuedThresholdMs && e.spawnUndeliveredAt == null) {
38771
+ e.spawnUndeliveredAt = now;
38772
+ events.push({ kind: "spawn-undelivered", key: key2, messageId: e.payload.messageId, ageMs: age });
38773
+ }
38774
+ return true;
38775
+ }
38487
38776
  if (e.state === "drained" && e.drainedAt != null && now - e.drainedAt > t.ackThresholdMs) {
38488
38777
  events.push({ kind: "drain-undelivered", key: key2, messageId: e.payload.messageId, ageMs: now - e.drainedAt });
38489
38778
  return false;
@@ -38493,6 +38782,23 @@ var InboundHub3 = class {
38493
38782
  }
38494
38783
  return events;
38495
38784
  }
38785
+ /** Task 2575 — every key holding queued work with NO subscriber whose oldest
38786
+ * message is older than `thresholdMs`. The gateway's sweep re-invokes the
38787
+ * retained spawn arguments for each one. Read-only — it emits nothing and
38788
+ * mutates nothing, so calling it cannot consume a terminal event the sweep is
38789
+ * about to report. */
38790
+ heldSpawns(now, thresholdMs) {
38791
+ const out = [];
38792
+ for (const [key2, s] of this.keys) {
38793
+ if (s.subscriber) continue;
38794
+ const queued = s.inFlight.filter((e) => e.state === "queued");
38795
+ if (queued.length === 0) continue;
38796
+ const oldestAgeMs = Math.max(...queued.map((e) => now - e.enqueuedAt));
38797
+ if (oldestAgeMs <= thresholdMs) continue;
38798
+ out.push({ key: key2, oldestAgeMs, count: queued.length });
38799
+ }
38800
+ return out;
38801
+ }
38496
38802
  /** Whether a key currently has a live channel server attached. The gateway
38497
38803
  * uses this to decide whether an inbound needs a cold-start spawn/resume. */
38498
38804
  hasSubscriber(key2) {
@@ -38666,6 +38972,7 @@ init_loop_registry();
38666
38972
  var QUEUED_THRESHOLD_MS3 = 3e4;
38667
38973
  var ACK_THRESHOLD_MS3 = 3e4;
38668
38974
  var SWEEP_INTERVAL_MS3 = 15e3;
38975
+ var GIVE_UP_MS2 = 3e5;
38669
38976
  function telegramSubjectSessionId(input) {
38670
38977
  if (input.role === "admin") {
38671
38978
  const adminUserId = input.adminUserId ?? void 0;
@@ -38679,16 +38986,6 @@ var TelegramGateway = class {
38679
38986
  }
38680
38987
  hub = new InboundHub3();
38681
38988
  replies = /* @__PURE__ */ new Map();
38682
- /** Task 2559 — the keys whose last inbound was `role:'admin'`. The follower
38683
- * census's bound set is the doors that SHOULD have a reader, and a public
38684
- * Telegram session is given none by design: the public branch of
38685
- * `buildTelegramEnsureSession` returns before the attach block, because the
38686
- * file follower is an admin surface. Including a public key would report it
38687
- * missing on every tick for ever and stop `missing=` ever reaching zero,
38688
- * which is the one signal the census exists to give. Removed on a public
38689
- * inbound, because the role is read from account config on every inbound and
38690
- * a bot can be flipped between the two. */
38691
- adminKeys = /* @__PURE__ */ new Set();
38692
38989
  spawning = /* @__PURE__ */ new Set();
38693
38990
  /** Task 2428 — keys whose channel server signalled ready while their spawn was
38694
38991
  * still in flight. The harness discards a channel notification that arrives
@@ -38699,6 +38996,14 @@ var TelegramGateway = class {
38699
38996
  * `resume-bound`. Holding the signal here rather than draining leaves
38700
38997
  * `hub.ready` false, so every message in the window queues and none is lost. */
38701
38998
  readyWhileSpawning = /* @__PURE__ */ new Set();
38999
+ /** Task 2575 — the arguments of the last `ensureChannelSession` call per key,
39000
+ * so the sweep can re-invoke a spawn that was refused. The refusal is not
39001
+ * visible here — `ensureChannelSession` returns void and the refusal sites in
39002
+ * server/telegram-ensure-session.ts log and return — so the retry keys off
39003
+ * queue state, the observable consequence of the refusal. `attempts` is the
39004
+ * per-key retry counter that rides the log line, reset whenever a fresh
39005
+ * inbound takes the spawn path. */
39006
+ heldSpawnArgs = /* @__PURE__ */ new Map();
38702
39007
  seq = 0;
38703
39008
  /** Hono sub-app exposing the loopback channel routes; mount on the UI app. */
38704
39009
  routes() {
@@ -38745,7 +39050,8 @@ var TelegramGateway = class {
38745
39050
  reconcile(now) {
38746
39051
  const events = this.hub.sweep(now, {
38747
39052
  queuedThresholdMs: QUEUED_THRESHOLD_MS3,
38748
- ackThresholdMs: ACK_THRESHOLD_MS3
39053
+ ackThresholdMs: ACK_THRESHOLD_MS3,
39054
+ giveUpMs: GIVE_UP_MS2
38749
39055
  });
38750
39056
  for (const e of events) {
38751
39057
  console.error(
@@ -38754,6 +39060,49 @@ var TelegramGateway = class {
38754
39060
  }
38755
39061
  return events;
38756
39062
  }
39063
+ /** Task 2575 — re-invoke the retained spawn for every key holding queued work
39064
+ * with no channel server attached. Driven by startSweeper() beside reconcile();
39065
+ * called directly with an injected clock in tests. Returns the number of
39066
+ * ensureChannelSession calls made.
39067
+ *
39068
+ * This door kept its held message across the 2026-08-08 outage but still
39069
+ * waited 4m41s for it, because the drain fires only when a later inbound
39070
+ * spawns successfully. This is the trigger that removes that wait. */
39071
+ async retryHeldSpawns(now) {
39072
+ const held = this.hub.heldSpawns(now, QUEUED_THRESHOLD_MS3);
39073
+ let calls = 0;
39074
+ for (const h of held) {
39075
+ if (this.spawning.has(h.key)) continue;
39076
+ const entry2 = this.heldSpawnArgs.get(h.key);
39077
+ if (!entry2) {
39078
+ console.error(
39079
+ `[telegram-native] op=spawn-retry-skipped key=${h.key} ageMs=${h.oldestAgeMs} queued=${h.count} reason=no-args`
39080
+ );
39081
+ continue;
39082
+ }
39083
+ entry2.attempts++;
39084
+ this.spawning.add(h.key);
39085
+ calls++;
39086
+ try {
39087
+ await this.deps.ensureChannelSession(entry2.args);
39088
+ console.error(
39089
+ `[telegram-native] op=spawn-retry key=${h.key} attempt=${entry2.attempts} ageMs=${h.oldestAgeMs} queued=${h.count} ok=true`
39090
+ );
39091
+ } catch (err) {
39092
+ console.error(
39093
+ `[telegram-native] op=spawn-retry key=${h.key} attempt=${entry2.attempts} ageMs=${h.oldestAgeMs} queued=${h.count} ok=false reason=${err instanceof Error ? err.message : String(err)}`
39094
+ );
39095
+ } finally {
39096
+ this.spawning.delete(h.key);
39097
+ if (this.readyWhileSpawning.delete(h.key)) this.drain(h.key);
39098
+ }
39099
+ }
39100
+ const heldKeys = new Set(held.map((h) => h.key));
39101
+ for (const k of [...this.heldSpawnArgs.keys()]) {
39102
+ if (!heldKeys.has(k) && !this.spawning.has(k)) this.heldSpawnArgs.delete(k);
39103
+ }
39104
+ return calls;
39105
+ }
38757
39106
  /** Arm the periodic reconciliation sweep in the long-lived UI process.
38758
39107
  *
38759
39108
  * Task 1814 — armed through the registry, so it appears on /activity. This
@@ -38771,9 +39120,14 @@ var TelegramGateway = class {
38771
39120
  return registerLoop({
38772
39121
  name: "telegram-native-sweep",
38773
39122
  intervalMs,
38774
- run: () => {
38775
- const events = this.reconcile(Date.now());
38776
- return events.length > 0 ? `${events.length} terminal events` : void 0;
39123
+ run: async () => {
39124
+ const now = Date.now();
39125
+ const events = this.reconcile(now);
39126
+ const retried = await this.retryHeldSpawns(now);
39127
+ const parts = [];
39128
+ if (events.length > 0) parts.push(`${events.length} terminal events`);
39129
+ if (retried > 0) parts.push(`${retried} spawn retries`);
39130
+ return parts.length > 0 ? parts.join(", ") : void 0;
38777
39131
  }
38778
39132
  });
38779
39133
  }
@@ -38800,8 +39154,6 @@ var TelegramGateway = class {
38800
39154
  const willDeliverNow = this.hub.isReady(key2);
38801
39155
  const source = input.source ?? "user";
38802
39156
  this.replies.set(key2, input.reply);
38803
- if (input.role === "admin") this.adminKeys.add(key2);
38804
- else this.adminKeys.delete(key2);
38805
39157
  let standingRules;
38806
39158
  if (input.role === "admin" && this.deps.fetchStandingRules) {
38807
39159
  const res = await this.deps.fetchStandingRules(input.accountId);
@@ -38847,21 +39199,23 @@ var TelegramGateway = class {
38847
39199
  this.spawning.add(key2);
38848
39200
  console.error(`[telegram-native] op=spawn-trigger key=${key2} botId=${input.botId} role=${input.role}`);
38849
39201
  noteSpawnAttempt2(input.botId);
39202
+ const spawnArgs = {
39203
+ accountId: input.accountId,
39204
+ accountDir: input.accountDir,
39205
+ botId: input.botId,
39206
+ botToken: input.botToken,
39207
+ agentSlug: input.agentSlug,
39208
+ channelKey: key2,
39209
+ senderId: input.senderId,
39210
+ role: input.role,
39211
+ personId: input.personId ?? null,
39212
+ chatId: input.chatId,
39213
+ gatewayUrl: this.deps.gatewayUrl,
39214
+ serverPath: this.deps.serverPath
39215
+ };
39216
+ this.heldSpawnArgs.set(key2, { args: spawnArgs, attempts: 0 });
38850
39217
  try {
38851
- await this.deps.ensureChannelSession({
38852
- accountId: input.accountId,
38853
- accountDir: input.accountDir,
38854
- botId: input.botId,
38855
- botToken: input.botToken,
38856
- agentSlug: input.agentSlug,
38857
- channelKey: key2,
38858
- senderId: input.senderId,
38859
- role: input.role,
38860
- personId: input.personId ?? null,
38861
- chatId: input.chatId,
38862
- gatewayUrl: this.deps.gatewayUrl,
38863
- serverPath: this.deps.serverPath
38864
- });
39218
+ await this.deps.ensureChannelSession(spawnArgs);
38865
39219
  } finally {
38866
39220
  this.spawning.delete(key2);
38867
39221
  if (this.readyWhileSpawning.delete(key2)) this.drain(key2);
@@ -38876,17 +39230,21 @@ var TelegramGateway = class {
38876
39230
  liveReplyTarget(key2) {
38877
39231
  return this.replies.get(key2) ?? null;
38878
39232
  }
38879
- /** Task 2559 — every admin door this gateway currently holds a live delivery
38880
- * closure for, as the `<botId>:<senderId>` channel key the follower registry
38881
- * keys on. The twin of `WaGateway.boundTargets()`: the census reconciles
38882
- * against exactly this set, so "should have a reader" and "can be sent to"
38883
- * are one fact and cannot drift. A key `parseTelegramChannelKey` rejects is
38884
- * the pre-2368 bare-sender shape, skipped rather than halved at an arbitrary
38885
- * offset, which would yield a door id nothing keys on. */
39233
+ /** Task 2559 — every door this gateway currently holds a live delivery closure
39234
+ * for, as the `<botId>:<senderId>` channel key the follower registry keys on.
39235
+ * The twin of `WaGateway.boundTargets()`: the census reconciles against
39236
+ * exactly this set, so "should have a reader" and "can be sent to" are one
39237
+ * fact and cannot drift. A key `parseTelegramChannelKey` rejects is the
39238
+ * pre-2368 bare-sender shape, skipped rather than halved at an arbitrary
39239
+ * offset, which would yield a door id nothing keys on.
39240
+ *
39241
+ * Task 2541 — every role, not admin only. 2559 filtered public keys out of
39242
+ * this set because a public session was given no reader; it has one now, so
39243
+ * the filter would hide the door instead of the absence. That is what it did:
39244
+ * `bound=1 attached=1 missing=0` printed on every tick while a public
39245
+ * visitor's answer reached no chat at all. */
38886
39246
  boundTargets() {
38887
- return [...this.replies.keys()].filter(
38888
- (key2) => this.adminKeys.has(key2) && parseTelegramChannelKey(key2) !== null
38889
- );
39247
+ return [...this.replies.keys()].filter((key2) => parseTelegramChannelKey(key2) !== null);
38890
39248
  }
38891
39249
  async sendOutbound(key2, text) {
38892
39250
  const reply = this.replies.get(key2);
@@ -38928,7 +39286,7 @@ function buildTelegramSpawnRequest(input) {
38928
39286
  }
38929
39287
 
38930
39288
  // server/telegram-ensure-session.ts
38931
- var TAG62 = "[telegram-inbound]";
39289
+ var TAG64 = "[telegram-inbound]";
38932
39290
  function buildTelegramEnsureSession(deps) {
38933
39291
  return async (input) => {
38934
39292
  const req = buildTelegramSpawnRequest({
@@ -38942,6 +39300,18 @@ function buildTelegramEnsureSession(deps) {
38942
39300
  // Task 1789 — admin rows only; see the WhatsApp twin.
38943
39301
  overrideSessionId: input.role === "admin" ? deps.resolveSessionOverride(input.accountId, input.channelKey) : null
38944
39302
  });
39303
+ const notifySend = async (text) => {
39304
+ const sent = await deps.sendText(input.botToken, Number(input.chatId), text);
39305
+ if (!sent.ok) throw new Error(sent.error ?? "send failed");
39306
+ };
39307
+ const notifyRefused = (result2) => sendRefusalNotice({
39308
+ channel: "telegram",
39309
+ doorId: req.channelKey,
39310
+ sender: req.channelKey,
39311
+ reason: result2.error,
39312
+ status: result2.status,
39313
+ send: notifySend
39314
+ });
38945
39315
  if (input.role === "public") {
38946
39316
  const result2 = await deps.managerRcSpawn({
38947
39317
  senderId: req.channelKey,
@@ -38959,15 +39329,24 @@ function buildTelegramEnsureSession(deps) {
38959
39329
  `[telegram-native] op=spawn-failed botId=${input.botId} key=${req.channelKey} role=public agent=${input.agentSlug} error=${result2.error} status=${result2.status}`
38960
39330
  );
38961
39331
  noteSpawnFailed2(input.botId);
39332
+ await notifyRefused(result2);
38962
39333
  return;
38963
39334
  }
38964
39335
  console.error(
38965
- `${TAG62} op=telegram-spawn-door botId=${input.botId} role=public door=rc agent=${input.agentSlug}`
39336
+ `${TAG64} op=telegram-spawn-door botId=${input.botId} role=public door=rc agent=${input.agentSlug}`
38966
39337
  );
38967
39338
  console.error(
38968
- `${TAG62} op=spawn botId=${input.botId} sessionId=${result2.sessionId} targetAccountId=${input.accountId} role=public`
39339
+ `${TAG64} op=spawn botId=${input.botId} sessionId=${result2.sessionId} targetAccountId=${input.accountId} role=public`
38969
39340
  );
38970
39341
  noteSpawnOk2(input.botId);
39342
+ clearRefusalNotice(req.channelKey);
39343
+ deps.followers.attach({
39344
+ sessionId: result2.sessionId,
39345
+ senderId: req.channelKey,
39346
+ accountId: input.accountId,
39347
+ chatId: input.chatId,
39348
+ botToken: input.botToken
39349
+ });
38971
39350
  return;
38972
39351
  }
38973
39352
  const result = await deps.managerRcSpawn(req);
@@ -38976,12 +39355,14 @@ function buildTelegramEnsureSession(deps) {
38976
39355
  `[telegram-native] op=spawn-failed botId=${input.botId} key=${req.channelKey} role=admin error=${result.error} status=${result.status}`
38977
39356
  );
38978
39357
  noteSpawnFailed2(input.botId);
39358
+ await notifyRefused(result);
38979
39359
  return;
38980
39360
  }
38981
39361
  console.error(
38982
- `${TAG62} op=spawn botId=${input.botId} sessionId=${req.sessionId} targetAccountId=${input.accountId} role=admin`
39362
+ `${TAG64} op=spawn botId=${input.botId} sessionId=${req.sessionId} targetAccountId=${input.accountId} role=admin`
38983
39363
  );
38984
39364
  noteSpawnOk2(input.botId);
39365
+ clearRefusalNotice(req.channelKey);
38985
39366
  deps.followers.attach({
38986
39367
  sessionId: req.sessionId,
38987
39368
  senderId: req.channelKey,
@@ -38997,7 +39378,7 @@ init_attachments();
38997
39378
  import { realpathSync as realpathSync10 } from "fs";
38998
39379
  import { readFile as readFile14, stat as stat10 } from "fs/promises";
38999
39380
  import { resolve as resolve48, basename as basename16 } from "path";
39000
- var TAG63 = "[telegram:outbound]";
39381
+ var TAG65 = "[telegram:outbound]";
39001
39382
  var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
39002
39383
  async function sendTelegramDocument(input) {
39003
39384
  const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot5 } = input;
@@ -39014,10 +39395,10 @@ async function sendTelegramDocument(input) {
39014
39395
  } catch (err) {
39015
39396
  const code = err.code;
39016
39397
  if (code === "ENOENT") {
39017
- console.error(`${TAG63} document ENOENT scope=account-dir accountDir=${accountDir} maxyAccountId=${maxyAccountId}`);
39398
+ console.error(`${TAG65} document ENOENT scope=account-dir accountDir=${accountDir} maxyAccountId=${maxyAccountId}`);
39018
39399
  return { ok: false, status: 500, error: `Account directory not resolvable for ${maxyAccountId}` };
39019
39400
  }
39020
- console.error(`${TAG63} document path error scope=account-dir maxyAccountId=${maxyAccountId}: ${String(err)}`);
39401
+ console.error(`${TAG65} document path error scope=account-dir maxyAccountId=${maxyAccountId}: ${String(err)}`);
39021
39402
  return { ok: false, status: 500, error: String(err) };
39022
39403
  }
39023
39404
  let resolvedPath;
@@ -39026,14 +39407,14 @@ async function sendTelegramDocument(input) {
39026
39407
  } catch (err) {
39027
39408
  const code = err.code;
39028
39409
  if (code === "ENOENT") {
39029
- console.error(`${TAG63} document ENOENT scope=file path=${filePath} maxyAccountId=${maxyAccountId}`);
39410
+ console.error(`${TAG65} document ENOENT scope=file path=${filePath} maxyAccountId=${maxyAccountId}`);
39030
39411
  return { ok: false, status: 404, error: `File not found: ${filePath}` };
39031
39412
  }
39032
- console.error(`${TAG63} document path error scope=file maxyAccountId=${maxyAccountId}: ${String(err)}`);
39413
+ console.error(`${TAG65} document path error scope=file maxyAccountId=${maxyAccountId}: ${String(err)}`);
39033
39414
  return { ok: false, status: 500, error: String(err) };
39034
39415
  }
39035
39416
  if (!resolvedPath.startsWith(accountResolved + "/")) {
39036
- console.error(`${TAG63} document REJECTED reason=outside_account_directory maxyAccountId=${maxyAccountId}`);
39417
+ console.error(`${TAG65} document REJECTED reason=outside_account_directory maxyAccountId=${maxyAccountId}`);
39037
39418
  return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
39038
39419
  }
39039
39420
  const fileStat = await stat10(resolvedPath);
@@ -39066,24 +39447,24 @@ async function sendTelegramDocument(input) {
39066
39447
  error = e instanceof Error ? e.message : String(e);
39067
39448
  }
39068
39449
  console.error(
39069
- `${TAG63} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok2}` + (messageId !== void 0 ? ` id=${messageId}` : "")
39450
+ `${TAG65} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok2}` + (messageId !== void 0 ? ` id=${messageId}` : "")
39070
39451
  );
39071
39452
  return ok2 ? { ok: true, messageId } : { ok: false, status: 500, error };
39072
39453
  }
39073
39454
 
39074
39455
  // app/lib/telegram/outbound/file-delivery.ts
39075
- var TAG64 = "[telegram:outbound]";
39456
+ var TAG66 = "[telegram:outbound]";
39076
39457
  function platformRoot4() {
39077
39458
  return process.env.MAXY_PLATFORM_ROOT || "";
39078
39459
  }
39079
39460
  function makeTelegramSendFile(entry2, botToken) {
39080
39461
  return async (filePath, caption) => {
39081
39462
  if (!botToken) {
39082
- console.error(`${TAG64} file-delivery reject reason=no-bot-token sender=${entry2.senderId} role=${entry2.role}`);
39463
+ console.error(`${TAG66} file-delivery reject reason=no-bot-token sender=${entry2.senderId} role=${entry2.role}`);
39083
39464
  return { ok: false, error: "no-bot-token" };
39084
39465
  }
39085
39466
  if (entry2.replyTarget == null) {
39086
- console.error(`${TAG64} file-delivery reject reason=no-reply-target sender=${entry2.senderId} role=${entry2.role}`);
39467
+ console.error(`${TAG66} file-delivery reject reason=no-reply-target sender=${entry2.senderId} role=${entry2.role}`);
39087
39468
  return { ok: false, error: "no-reply-target" };
39088
39469
  }
39089
39470
  const result = await sendTelegramDocument({
@@ -39100,7 +39481,7 @@ function makeTelegramSendFile(entry2, botToken) {
39100
39481
  function makeTelegramFileDelivery(entry2, botToken) {
39101
39482
  return makeFileDelivery({
39102
39483
  entry: entry2,
39103
- tag: TAG64,
39484
+ tag: TAG66,
39104
39485
  channel: "telegram",
39105
39486
  sendFile: makeTelegramSendFile(entry2, botToken)
39106
39487
  });
@@ -39190,7 +39571,7 @@ init_webchat();
39190
39571
  // app/lib/channel-pty-bridge/public-session-end-review.ts
39191
39572
  init_src3();
39192
39573
  import { randomUUID as randomUUID14 } from "crypto";
39193
- var TAG65 = "[public-session-review]";
39574
+ var TAG67 = "[public-session-review]";
39194
39575
  var CONSUMED_CAP = 500;
39195
39576
  var consumed = /* @__PURE__ */ new Set();
39196
39577
  function consumeOnce(sessionId) {
@@ -39217,7 +39598,7 @@ async function fetchJsonl(sessionId) {
39217
39598
  return await res.text();
39218
39599
  } catch (err) {
39219
39600
  console.error(
39220
- `${TAG65} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
39601
+ `${TAG67} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
39221
39602
  );
39222
39603
  return null;
39223
39604
  }
@@ -39241,7 +39622,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
39241
39622
  const res = await fetch(url);
39242
39623
  if (!res.ok) {
39243
39624
  console.error(
39244
- `${TAG65} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
39625
+ `${TAG67} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
39245
39626
  );
39246
39627
  return [];
39247
39628
  }
@@ -39249,7 +39630,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
39249
39630
  return Array.isArray(body.writes) ? body.writes : [];
39250
39631
  } catch (err) {
39251
39632
  console.error(
39252
- `${TAG65} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
39633
+ `${TAG67} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
39253
39634
  );
39254
39635
  return [];
39255
39636
  }
@@ -39278,7 +39659,7 @@ function composeInitialMessage(input) {
39278
39659
  }
39279
39660
  async function dispatchReviewer(input, initialMessage) {
39280
39661
  const sessionId = randomUUID14();
39281
- console.log(`${TAG65} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
39662
+ console.log(`${TAG67} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
39282
39663
  const spawned = await managerRcSpawn({
39283
39664
  sessionId,
39284
39665
  initialMessage,
@@ -39302,21 +39683,21 @@ async function firePublicSessionEndReview(input) {
39302
39683
  const dispatchedAt = Date.now();
39303
39684
  if (consumed.has(input.sessionId)) {
39304
39685
  console.log(
39305
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
39686
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
39306
39687
  );
39307
39688
  return;
39308
39689
  }
39309
39690
  const jsonl = await fetchJsonl(input.sessionId);
39310
39691
  if (!jsonl) {
39311
39692
  console.log(
39312
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
39693
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
39313
39694
  );
39314
39695
  return;
39315
39696
  }
39316
39697
  const operatorTurns = countOperatorTurns(jsonl);
39317
39698
  if (operatorTurns === 0) {
39318
39699
  console.log(
39319
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
39700
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
39320
39701
  );
39321
39702
  return;
39322
39703
  }
@@ -39330,19 +39711,19 @@ async function firePublicSessionEndReview(input) {
39330
39711
  });
39331
39712
  if (!consumeOnce(input.sessionId)) {
39332
39713
  console.log(
39333
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
39714
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
39334
39715
  );
39335
39716
  return;
39336
39717
  }
39337
39718
  const dispatched = await dispatchReviewer(input, initialMessage);
39338
39719
  if (!dispatched.ok) {
39339
39720
  console.error(
39340
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
39721
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
39341
39722
  );
39342
39723
  return;
39343
39724
  }
39344
39725
  console.log(
39345
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
39726
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
39346
39727
  );
39347
39728
  }
39348
39729
 
@@ -39494,7 +39875,7 @@ init_account();
39494
39875
 
39495
39876
  // server/telegram-audit.ts
39496
39877
  import { statSync as statSync28 } from "fs";
39497
- import { join as join62 } from "path";
39878
+ import { join as join63 } from "path";
39498
39879
  init_channel_key();
39499
39880
  init_sidebar_sessions();
39500
39881
  function buildTelegramAuditLines(input) {
@@ -39560,7 +39941,7 @@ function countTelegramSessionsForAccount(accountId) {
39560
39941
  function* walkTelegramSidecars() {
39561
39942
  const cfg = claudeConfigDir();
39562
39943
  if (!cfg) return;
39563
- const projectsRoot = join62(cfg, "projects");
39944
+ const projectsRoot = join63(cfg, "projects");
39564
39945
  for (const { path } of enumerateSessionSidecars(projectsRoot)) {
39565
39946
  const meta = readSidecarMeta(path);
39566
39947
  if (meta.channel !== "telegram" || !meta.accountId) continue;
@@ -39595,15 +39976,15 @@ function answeringOriginTarget(send, key2, noteAnswered3) {
39595
39976
  }
39596
39977
 
39597
39978
  // server/telegram-migrate.ts
39598
- import { readFileSync as readFileSync53, writeFileSync as writeFileSync19, existsSync as existsSync53, renameSync as renameSync11, mkdirSync as mkdirSync11 } from "fs";
39599
- import { dirname as dirname18, join as join63 } from "path";
39979
+ import { readFileSync as readFileSync54, writeFileSync as writeFileSync19, existsSync as existsSync53, renameSync as renameSync11, mkdirSync as mkdirSync11 } from "fs";
39980
+ import { dirname as dirname18, join as join64 } from "path";
39600
39981
  function migrateTelegramTwoSlot(input) {
39601
39982
  const out = [];
39602
39983
  for (const account of input.accounts) {
39603
- const configPath3 = join63(account.accountDir, "account.json");
39984
+ const configPath3 = join64(account.accountDir, "account.json");
39604
39985
  let config;
39605
39986
  try {
39606
- config = JSON.parse(readFileSync53(configPath3, "utf-8"));
39987
+ config = JSON.parse(readFileSync54(configPath3, "utf-8"));
39607
39988
  } catch {
39608
39989
  console.error(`[telegram-audit] op=migrate-unreadable accountId=${account.accountId}`);
39609
39990
  continue;
@@ -39705,14 +40086,14 @@ function backfillMisfiledRecords(params) {
39705
40086
  init_message_store();
39706
40087
 
39707
40088
  // server/lib/booking-site-accounts.ts
39708
- import { existsSync as existsSync54, readFileSync as readFileSync54 } from "fs";
40089
+ import { existsSync as existsSync54, readFileSync as readFileSync55 } from "fs";
39709
40090
  import { resolve as resolve49 } from "path";
39710
40091
  function listBookingSiteAccounts(accounts) {
39711
40092
  return accounts.filter((a) => {
39712
40093
  const availPath = resolve49(a.accountDir, "calendar-availability.json");
39713
40094
  if (!existsSync54(availPath)) return false;
39714
40095
  try {
39715
- const cfg = JSON.parse(readFileSync54(availPath, "utf-8"));
40096
+ const cfg = JSON.parse(readFileSync55(availPath, "utf-8"));
39716
40097
  return typeof cfg.bookingDbName === "string" && cfg.bookingDbName.length > 0;
39717
40098
  } catch {
39718
40099
  return false;
@@ -39757,7 +40138,7 @@ init_paths();
39757
40138
 
39758
40139
  // ../lib/entitlement/src/index.ts
39759
40140
  import { createPublicKey, createHash as createHash7, verify as cryptoVerify } from "crypto";
39760
- import { existsSync as existsSync55, readFileSync as readFileSync55, statSync as statSync29 } from "fs";
40141
+ import { existsSync as existsSync55, readFileSync as readFileSync56, statSync as statSync29 } from "fs";
39761
40142
  import { resolve as resolve50 } from "path";
39762
40143
 
39763
40144
  // ../lib/entitlement/src/canonicalize.ts
@@ -39821,7 +40202,7 @@ function resolveEntitlement(brand, account) {
39821
40202
  function verifyAndResolve(brand, entitlementPath, account) {
39822
40203
  let pubkeyPem;
39823
40204
  try {
39824
- pubkeyPem = readFileSync55(pubkeyPath(brand), "utf-8");
40205
+ pubkeyPem = readFileSync56(pubkeyPath(brand), "utf-8");
39825
40206
  } catch (err) {
39826
40207
  return logResolved(anonymousFallback("pubkey-missing"), {
39827
40208
  reason: "pubkey-missing"
@@ -39835,7 +40216,7 @@ function verifyAndResolve(brand, entitlementPath, account) {
39835
40216
  }
39836
40217
  let envelope;
39837
40218
  try {
39838
- envelope = JSON.parse(readFileSync55(entitlementPath, "utf-8"));
40219
+ envelope = JSON.parse(readFileSync56(entitlementPath, "utf-8"));
39839
40220
  } catch {
39840
40221
  return logResolved(anonymousFallback("malformed"), { reason: "malformed" });
39841
40222
  }
@@ -39997,14 +40378,14 @@ function clientFrom(c) {
39997
40378
  }
39998
40379
  installMediaDownloadGuard();
39999
40380
  var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT || "";
40000
- var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join64(PLATFORM_ROOT6, "config", "brand.json") : "";
40381
+ var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join65(PLATFORM_ROOT6, "config", "brand.json") : "";
40001
40382
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", marketingUrl: "https://getmaxy.com" };
40002
40383
  if (BRAND_JSON_PATH && !existsSync56(BRAND_JSON_PATH)) {
40003
40384
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
40004
40385
  }
40005
40386
  if (BRAND_JSON_PATH && existsSync56(BRAND_JSON_PATH)) {
40006
40387
  try {
40007
- const parsed = JSON.parse(readFileSync56(BRAND_JSON_PATH, "utf-8"));
40388
+ const parsed = JSON.parse(readFileSync57(BRAND_JSON_PATH, "utf-8"));
40008
40389
  BRAND = { ...BRAND, ...parsed };
40009
40390
  } catch (err) {
40010
40391
  console.error(`[brand] Failed to parse brand.json: ${err.message}`);
@@ -40036,11 +40417,11 @@ var brandLoginOpts = {
40036
40417
  appleTouchIconPath: brandAppIcon512Path,
40037
40418
  themeColor: BRAND.defaultColors?.primary
40038
40419
  };
40039
- var ALIAS_DOMAINS_PATH = join64(homedir5(), BRAND.configDir, "alias-domains.json");
40420
+ var ALIAS_DOMAINS_PATH = join65(homedir6(), BRAND.configDir, "alias-domains.json");
40040
40421
  function loadAliasDomains() {
40041
40422
  try {
40042
40423
  if (!existsSync56(ALIAS_DOMAINS_PATH)) return null;
40043
- const parsed = JSON.parse(readFileSync56(ALIAS_DOMAINS_PATH, "utf-8"));
40424
+ const parsed = JSON.parse(readFileSync57(ALIAS_DOMAINS_PATH, "utf-8"));
40044
40425
  if (!Array.isArray(parsed)) {
40045
40426
  console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
40046
40427
  return null;
@@ -40181,7 +40562,7 @@ var waGateway = new WaGateway({
40181
40562
  // resolver and `fanTargetsFor` is a constructor dep, not a callable method.
40182
40563
  fanTargetsFor: whatsappFanTargets,
40183
40564
  gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
40184
- serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve51(process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
40565
+ serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve51(process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
40185
40566
  // Task 751 / 1390 — file delivery on the native channel. `maxyAccountId` (the
40186
40567
  // path-validation scope) is the sender's effective SESSION account, resolved
40187
40568
  // once at the inbound gate and threaded here via the gateway's per-sender doc
@@ -40196,7 +40577,7 @@ var waGateway = new WaGateway({
40196
40577
  caption,
40197
40578
  accountId,
40198
40579
  maxyAccountId,
40199
- platformRoot: resolve51(process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, ".."))
40580
+ platformRoot: resolve51(process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, ".."))
40200
40581
  });
40201
40582
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
40202
40583
  },
@@ -40286,11 +40667,20 @@ var waGateway = new WaGateway({
40286
40667
  `[whatsapp:public-agent] op=spawn-failed inboundId=${inboundId} senderId=${senderId} slug=${publicAgentSlug} error=${publicResult.error}`
40287
40668
  );
40288
40669
  noteSpawnFailed();
40670
+ await sendRefusalNotice({
40671
+ channel: "whatsapp",
40672
+ doorId: whatsappDoorId({ effectiveAccountId, senderId }),
40673
+ sender: senderId,
40674
+ reason: publicResult.error,
40675
+ status: publicResult.status,
40676
+ send: waGateway.liveReplyTarget(effectiveAccountId, senderId)
40677
+ });
40289
40678
  } else {
40290
40679
  console.error(
40291
40680
  `[whatsapp:public-agent] op=spawn-ok inboundId=${inboundId} senderId=${senderId} slug=${publicAgentSlug} ms=${Date.now() - publicSpawnStartedAt}`
40292
40681
  );
40293
40682
  noteSpawnOk();
40683
+ clearRefusalNotice(whatsappDoorId({ effectiveAccountId, senderId }));
40294
40684
  }
40295
40685
  return;
40296
40686
  }
@@ -40329,8 +40719,17 @@ var waGateway = new WaGateway({
40329
40719
  const result = await managerRcSpawn({ ...req, userId, party });
40330
40720
  if ("error" in result) {
40331
40721
  console.error(`[whatsapp-native] spawn-failed senderId=${senderId} role=${role} error=${result.error} status=${result.status}`);
40722
+ await sendRefusalNotice({
40723
+ channel: "whatsapp",
40724
+ doorId: whatsappDoorId({ effectiveAccountId, senderId }),
40725
+ sender: senderId,
40726
+ reason: result.error,
40727
+ status: result.status,
40728
+ send: waGateway.liveReplyTarget(effectiveAccountId, senderId)
40729
+ });
40332
40730
  return;
40333
40731
  }
40732
+ clearRefusalNotice(whatsappDoorId({ effectiveAccountId, senderId }));
40334
40733
  nativeFileFollowers.attach({
40335
40734
  sessionId: req.sessionId,
40336
40735
  senderId,
@@ -40353,7 +40752,7 @@ function runDuplicateSenderAudit() {
40353
40752
  try {
40354
40753
  const configDir = process.env.CLAUDE_CONFIG_DIR;
40355
40754
  if (!configDir) return;
40356
- const projectsRoot = join64(configDir, "projects");
40755
+ const projectsRoot = join65(configDir, "projects");
40357
40756
  const records2 = [];
40358
40757
  for (const { path: jsonlPath, isSubagent, archived } of enumerateJsonls(projectsRoot)) {
40359
40758
  if (isSubagent || archived) continue;
@@ -40405,7 +40804,7 @@ var webchatGateway = new WebchatGateway({
40405
40804
  // rather than hanging the POST.
40406
40805
  publicProjectsRoot: () => {
40407
40806
  const cfg = claudeConfigDir();
40408
- return cfg ? join64(cfg, "projects") : "";
40807
+ return cfg ? join65(cfg, "projects") : "";
40409
40808
  },
40410
40809
  gatewayUrl: webchatGatewayUrl(),
40411
40810
  serverPath: webchatServerPath(),
@@ -40562,6 +40961,8 @@ var telegramGateway = new TelegramGateway({
40562
40961
  managerRcSpawn,
40563
40962
  managerSpawn,
40564
40963
  resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
40964
+ // Task 2573 — the send the refusal notice goes out on.
40965
+ sendText: sendTelegramText,
40565
40966
  followers: telegramFileFollowers
40566
40967
  })
40567
40968
  });
@@ -41047,7 +41448,7 @@ app80.get("/agent-assets/:slug/:filename", (c) => {
41047
41448
  const ext = "." + filename.split(".").pop()?.toLowerCase();
41048
41449
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
41049
41450
  console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
41050
- const body = readFileSync56(filePath);
41451
+ const body = readFileSync57(filePath);
41051
41452
  return c.body(body, 200, {
41052
41453
  "Content-Type": contentType,
41053
41454
  "Cache-Control": "public, max-age=3600"
@@ -41077,7 +41478,7 @@ app80.get("/generated/:filename", (c) => {
41077
41478
  const ext = "." + filename.split(".").pop()?.toLowerCase();
41078
41479
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
41079
41480
  console.log(`[generated] serve file=${filename} status=200`);
41080
- const body = readFileSync56(filePath);
41481
+ const body = readFileSync57(filePath);
41081
41482
  return c.body(body, 200, {
41082
41483
  "Content-Type": contentType,
41083
41484
  "Cache-Control": "public, max-age=86400"
@@ -41092,7 +41493,7 @@ var brandLogoPath = "/brand/maxy-monochrome.png";
41092
41493
  var brandIconPath = "/brand/maxy-monochrome.png";
41093
41494
  if (BRAND_JSON_PATH && existsSync56(BRAND_JSON_PATH)) {
41094
41495
  try {
41095
- const fullBrand = JSON.parse(readFileSync56(BRAND_JSON_PATH, "utf-8"));
41496
+ const fullBrand = JSON.parse(readFileSync57(BRAND_JSON_PATH, "utf-8"));
41096
41497
  if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
41097
41498
  brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
41098
41499
  } catch {
@@ -41123,7 +41524,7 @@ var brandAppIconsExist = [brandAppIcon192Path, brandAppIcon512Path, brandMaskabl
41123
41524
  );
41124
41525
  var SW_SOURCE = (() => {
41125
41526
  try {
41126
- return readFileSync56(resolve51(process.cwd(), "public", "sw.js"), "utf-8");
41527
+ return readFileSync57(resolve51(process.cwd(), "public", "sw.js"), "utf-8");
41127
41528
  } catch {
41128
41529
  return null;
41129
41530
  }
@@ -41131,9 +41532,9 @@ var SW_SOURCE = (() => {
41131
41532
  function readInstalledVersion() {
41132
41533
  try {
41133
41534
  if (!PLATFORM_ROOT6) return "unknown";
41134
- const versionFile = join64(PLATFORM_ROOT6, "config", `.${BRAND.hostname}-version`);
41535
+ const versionFile = join65(PLATFORM_ROOT6, "config", `.${BRAND.hostname}-version`);
41135
41536
  if (!existsSync56(versionFile)) return "unknown";
41136
- const content = readFileSync56(versionFile, "utf-8").trim();
41537
+ const content = readFileSync57(versionFile, "utf-8").trim();
41137
41538
  return content || "unknown";
41138
41539
  } catch {
41139
41540
  return "unknown";
@@ -41144,7 +41545,7 @@ var versionScript = `<script>window.__MAXY_VERSION__=${JSON.stringify(INSTALLED_
41144
41545
  function cachedHtml(file) {
41145
41546
  let html = htmlCache.get(file);
41146
41547
  if (!html) {
41147
- html = readFileSync56(resolve51(process.cwd(), "public", file), "utf-8");
41548
+ html = readFileSync57(resolve51(process.cwd(), "public", file), "utf-8");
41148
41549
  const productNameEsc = escapeHtml(BRAND.productName);
41149
41550
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
41150
41551
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -41161,14 +41562,14 @@ ${clientErrorReporterScript}
41161
41562
  return html;
41162
41563
  }
41163
41564
  function loadBrandingCache(agentSlug) {
41164
- const configDir = join64(homedir5(), BRAND.configDir);
41565
+ const configDir = join65(homedir6(), BRAND.configDir);
41165
41566
  try {
41166
41567
  const verdict = resolvePublicAddressFromDisk(agentSlug);
41167
41568
  const accountId = verdict.kind === "served" ? verdict.owner.accountId : getDefaultAccountId();
41168
41569
  if (!accountId) return null;
41169
- const cachePath = join64(configDir, "branding-cache", accountId, `${agentSlug}.json`);
41570
+ const cachePath = join65(configDir, "branding-cache", accountId, `${agentSlug}.json`);
41170
41571
  if (!existsSync56(cachePath)) return null;
41171
- return JSON.parse(readFileSync56(cachePath, "utf-8"));
41572
+ return JSON.parse(readFileSync57(cachePath, "utf-8"));
41172
41573
  } catch {
41173
41574
  return null;
41174
41575
  }
@@ -41263,7 +41664,7 @@ app80.use("/vnc-popout.html", logViewerFetch);
41263
41664
  app80.get("/vnc-popout.html", (c) => {
41264
41665
  let html = htmlCache.get("vnc-popout.html");
41265
41666
  if (!html) {
41266
- html = readFileSync56(resolve51(process.cwd(), "public", "vnc-popout.html"), "utf-8");
41667
+ html = readFileSync57(resolve51(process.cwd(), "public", "vnc-popout.html"), "utf-8");
41267
41668
  const name = escapeHtml(BRAND.productName);
41268
41669
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
41269
41670
  html = html.replace("</head>", ` ${brandScript}
@@ -41417,7 +41818,7 @@ var hostname = process.env.HOSTNAME ?? "127.0.0.1";
41417
41818
  var httpServer = serve({ fetch: app80.fetch, port, hostname });
41418
41819
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41419
41820
  {
41420
- const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41821
+ const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41421
41822
  const reconcileScript = resolve51(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
41422
41823
  const runReconcile = (ctx) => {
41423
41824
  if (!existsSync56(reconcileScript)) return;
@@ -41440,7 +41841,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41440
41841
  detached: true,
41441
41842
  run: runReconcile
41442
41843
  });
41443
- const reconcileStatePath = join64(MAXY_DIR, "booking-reconcile-state.json");
41844
+ const reconcileStatePath = join65(MAXY_DIR, "booking-reconcile-state.json");
41444
41845
  registerLoop({
41445
41846
  name: "calendar-reconcile-liveness",
41446
41847
  intervalMs: 6e5,
@@ -41458,7 +41859,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41458
41859
  }
41459
41860
  {
41460
41861
  const DREAM_CYCLE_SWEEP_INTERVAL_MS = 36e5;
41461
- const dreamPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41862
+ const dreamPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41462
41863
  const dreamSweepScript = resolve51(dreamPlatformRoot, "plugins/memory/mcp/dist/scripts/dream-cycle-sweep.js");
41463
41864
  registerLoop({
41464
41865
  name: "dream-cycle-sweep",
@@ -41482,7 +41883,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41482
41883
  });
41483
41884
  }
41484
41885
  {
41485
- const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41886
+ const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41486
41887
  const outlookScript = resolve51(outlookPlatformRoot, "plugins/outlook/mcp/dist/scripts/complete-registration.js");
41487
41888
  const OUTLOOK_COMPLETE_INTERVAL_MS = 3e4;
41488
41889
  const runOutlookComplete = (ctx) => {
@@ -41508,7 +41909,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41508
41909
  });
41509
41910
  }
41510
41911
  {
41511
- const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41912
+ const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41512
41913
  const strandedAccountsDir = resolve51(auditRoot, "..", "data/accounts");
41513
41914
  const STRANDED_AUDIT_INTERVAL_MS = 3e5;
41514
41915
  const STRANDED_AGE_MS = 16 * 6e4;
@@ -41517,7 +41918,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41517
41918
  try {
41518
41919
  if (!existsSync56(strandedAccountsDir)) return;
41519
41920
  const now = Date.now();
41520
- for (const name of readdirSync32(strandedAccountsDir)) {
41921
+ for (const name of readdirSync33(strandedAccountsDir)) {
41521
41922
  if (!STRANDED_UUID_RE.test(name)) continue;
41522
41923
  const pendingPath2 = resolve51(strandedAccountsDir, name, "secrets/outlook/pending-devicecode.enc");
41523
41924
  if (!existsSync56(pendingPath2)) continue;
@@ -41538,7 +41939,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41538
41939
  });
41539
41940
  }
41540
41941
  {
41541
- const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41942
+ const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41542
41943
  const googleAccountsDir = resolve51(googleRoot, "..", "data/accounts");
41543
41944
  const GOOGLE_PENDING_AUDIT_INTERVAL_MS = 3e5;
41544
41945
  const runGooglePendingAuditSafe = () => {
@@ -41582,7 +41983,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41582
41983
  });
41583
41984
  }
41584
41985
  {
41585
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41986
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41586
41987
  const STORAGE_AUDIT_INTERVAL_MS = 3e5;
41587
41988
  const runStorageAuditSafe = () => runStorageAudit(auditPlatformRoot).catch((err) => {
41588
41989
  console.error(`[storage-audit] error="${err.message}"`);
@@ -41618,7 +42019,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41618
42019
  accounts: listValidAccounts().map((a) => ({ accountId: a.accountId, accountDir: a.accountDir })),
41619
42020
  secretPathFor: secretPath,
41620
42021
  legacySecretPathFor: (botType, accountId) => resolve51(
41621
- homedir5(),
42022
+ homedir6(),
41622
42023
  configDirName2(),
41623
42024
  `${botType === "admin" ? ".telegram-admin-webhook-secret" : ".telegram-webhook-secret"}.${accountId}`
41624
42025
  ),
@@ -41641,7 +42042,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41641
42042
  const lastByBot = newestTelegramSessionByBot();
41642
42043
  let secretFilesOnDisk = [];
41643
42044
  try {
41644
- secretFilesOnDisk = readdirSync32(telegramSecretsDir());
42045
+ secretFilesOnDisk = readdirSync33(telegramSecretsDir());
41645
42046
  } catch {
41646
42047
  }
41647
42048
  const lines = buildTelegramAuditLines({
@@ -41784,7 +42185,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41784
42185
  });
41785
42186
  }
41786
42187
  {
41787
- const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
42188
+ const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41788
42189
  const publishScript = resolve51(publishPlatformRoot, "plugins/scheduling/mcp/dist/scripts/publish-availability.js");
41789
42190
  const PUBLISH_INTERVAL_MS = 3e5;
41790
42191
  const bookingAccounts = () => {
@@ -41819,7 +42220,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41819
42220
  let hasBookingSite = false;
41820
42221
  try {
41821
42222
  if (existsSync56(availPath)) {
41822
- const cfg = JSON.parse(readFileSync56(availPath, "utf-8"));
42223
+ const cfg = JSON.parse(readFileSync57(availPath, "utf-8"));
41823
42224
  hasBookingSite = typeof cfg.bookingDbName === "string" && cfg.bookingDbName.length > 0;
41824
42225
  }
41825
42226
  } catch {
@@ -41829,7 +42230,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41829
42230
  let lastSuccessAt = null;
41830
42231
  if (existsSync56(statePath)) {
41831
42232
  try {
41832
- const rec = JSON.parse(readFileSync56(statePath, "utf-8"));
42233
+ const rec = JSON.parse(readFileSync57(statePath, "utf-8"));
41833
42234
  lastSuccessAt = rec.lastSuccessAt ?? null;
41834
42235
  } catch {
41835
42236
  console.error(`[calendar-availability] op=audit accountId=${account.accountId} snapshotAgeMs=na reason=bad-state-file`);
@@ -41864,7 +42265,7 @@ startDispatchCensus(() => getSession());
41864
42265
  startDispatchFieldCensus(() => getSession());
41865
42266
  startDispatchTgCensus(() => getSession());
41866
42267
  {
41867
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
42268
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41868
42269
  const auditScript = resolve51(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
41869
42270
  const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
41870
42271
  const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
@@ -41995,7 +42396,7 @@ async function runAdminUserReconcileTick() {
41995
42396
  console.error("[adminuser-self-heal] skip reason=no-users-file");
41996
42397
  return;
41997
42398
  }
41998
- const usersRaw = readFileSync56(USERS_FILE, "utf-8").trim();
42399
+ const usersRaw = readFileSync57(USERS_FILE, "utf-8").trim();
41999
42400
  if (!usersRaw) {
42000
42401
  console.error("[adminuser-self-heal] skip reason=empty-users-file");
42001
42402
  return;
@@ -42077,8 +42478,8 @@ var runIngestAuditTick = () => {
42077
42478
  },
42078
42479
  agentExists: (accountId, slug) => {
42079
42480
  if (!house) return false;
42080
- const dir = isHouseAccountKey(house.accountId, accountId) ? house.accountDir : join64(ACCOUNTS_DIR, accountId);
42081
- return existsSync56(join64(dir, "agents", slug, "config.json"));
42481
+ const dir = isHouseAccountKey(house.accountId, accountId) ? house.accountDir : join65(ACCOUNTS_DIR, accountId);
42482
+ return existsSync56(join65(dir, "agents", slug, "config.json"));
42082
42483
  }
42083
42484
  });
42084
42485
  } catch (err) {
@@ -42126,9 +42527,9 @@ registerLoop({
42126
42527
  runPublicAddressCollisionAudit({
42127
42528
  houseAccountId: () => resolveAccount()?.accountId ?? null,
42128
42529
  activeSlugs: () => listValidAccounts().map((a) => {
42129
- const agentsRoot = join64(a.accountDir, "agents");
42530
+ const agentsRoot = join65(a.accountDir, "agents");
42130
42531
  if (!existsSync56(agentsRoot)) return { accountId: a.accountId, slugs: [] };
42131
- const slugs = readdirSync32(agentsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((slug) => isActiveAgentSlug(a.accountDir, slug));
42532
+ const slugs = readdirSync33(agentsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((slug) => isActiveAgentSlug(a.accountDir, slug));
42132
42533
  return { accountId: a.accountId, slugs };
42133
42534
  })
42134
42535
  });
@@ -42142,7 +42543,7 @@ registerLoop({
42142
42543
  var USERS_RECONCILE_INTERVAL_MS = 60 * 60 * 1e3;
42143
42544
  function countUsersRows() {
42144
42545
  if (!existsSync56(USERS_FILE)) return 0;
42145
- const raw = readFileSync56(USERS_FILE, "utf-8").trim();
42546
+ const raw = readFileSync57(USERS_FILE, "utf-8").trim();
42146
42547
  if (!raw) return 0;
42147
42548
  const users = JSON.parse(raw);
42148
42549
  return users.filter((u) => typeof u.userId === "string").length;
@@ -42173,6 +42574,7 @@ registerLoop({
42173
42574
  });
42174
42575
  startGraphHealthTimer();
42175
42576
  startSharedFolderCensus();
42577
+ startCredRefreshCensus();
42176
42578
  void migrateUploads().catch((err) => console.error(`[uploads-migrate] failed err="${err instanceof Error ? err.message : String(err)}"`)).finally(() => {
42177
42579
  void startFileWatcher().catch((err) => {
42178
42580
  console.error(`[file-watcher] start-failed err="${err instanceof Error ? err.message : String(err)}"`);
@@ -42295,7 +42697,7 @@ if (bootAccountConfig?.whatsapp) {
42295
42697
  }
42296
42698
  init({
42297
42699
  configDir: configDirForWhatsApp,
42298
- platformRoot: resolve51(process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..")),
42700
+ platformRoot: resolve51(process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..")),
42299
42701
  accountConfig: bootAccountConfig,
42300
42702
  // Task 2074 — enrich with the sub-account name and fan out to every open
42301
42703
  // admin SSE stream. Fire-and-forget: the manager's persist path must not