@0xmaxma/claude-gateway 2.0.4 → 2.0.6

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 (276) hide show
  1. package/README.md +10 -1
  2. package/dist/agent/builtin-commands.d.ts +1 -1
  3. package/dist/agent/builtin-commands.d.ts.map +1 -1
  4. package/dist/agent/builtin-commands.js +3 -2
  5. package/dist/agent/builtin-commands.js.map +1 -1
  6. package/dist/agent/runner.d.ts +123 -9
  7. package/dist/agent/runner.d.ts.map +1 -1
  8. package/dist/agent/runner.js +215 -182
  9. package/dist/agent/runner.js.map +1 -1
  10. package/dist/agent/session-compaction-scheduler.d.ts +22 -0
  11. package/dist/agent/session-compaction-scheduler.d.ts.map +1 -0
  12. package/dist/agent/session-compaction-scheduler.js +40 -0
  13. package/dist/agent/session-compaction-scheduler.js.map +1 -0
  14. package/dist/agent/skill-learning/index.d.ts.map +1 -1
  15. package/dist/agent/skill-learning/index.js +27 -8
  16. package/dist/agent/skill-learning/index.js.map +1 -1
  17. package/dist/agent/skill-learning/reviewer.d.ts +3 -1
  18. package/dist/agent/skill-learning/reviewer.d.ts.map +1 -1
  19. package/dist/agent/skill-learning/reviewer.js +6 -2
  20. package/dist/agent/skill-learning/reviewer.js.map +1 -1
  21. package/dist/agent/turn-stream.d.ts +3 -1
  22. package/dist/agent/turn-stream.d.ts.map +1 -1
  23. package/dist/agent/turn-stream.js +8 -4
  24. package/dist/agent/turn-stream.js.map +1 -1
  25. package/dist/api/gateway-router.d.ts +2 -0
  26. package/dist/api/gateway-router.d.ts.map +1 -1
  27. package/dist/api/gateway-router.js +323 -16
  28. package/dist/api/gateway-router.js.map +1 -1
  29. package/dist/api/memory-activity.d.ts +18 -0
  30. package/dist/api/memory-activity.d.ts.map +1 -0
  31. package/dist/api/memory-activity.js +97 -0
  32. package/dist/api/memory-activity.js.map +1 -0
  33. package/dist/api/router.js +11 -11
  34. package/dist/api/router.js.map +1 -1
  35. package/dist/api/slack-webhook-router.js +1 -1
  36. package/dist/api/slack-webhook-router.js.map +1 -1
  37. package/dist/api/voice-router.d.ts +1 -0
  38. package/dist/api/voice-router.d.ts.map +1 -1
  39. package/dist/api/voice-router.js +129 -7
  40. package/dist/api/voice-router.js.map +1 -1
  41. package/dist/config/claude-settings.d.ts +14 -0
  42. package/dist/config/claude-settings.d.ts.map +1 -1
  43. package/dist/config/claude-settings.js +20 -0
  44. package/dist/config/claude-settings.js.map +1 -1
  45. package/dist/orchestration/agent-cli-session.d.ts +46 -0
  46. package/dist/orchestration/agent-cli-session.d.ts.map +1 -0
  47. package/dist/orchestration/agent-cli-session.js +121 -0
  48. package/dist/orchestration/agent-cli-session.js.map +1 -0
  49. package/dist/orchestration/agent-tool-schemas.d.ts +6 -0
  50. package/dist/orchestration/agent-tool-schemas.d.ts.map +1 -0
  51. package/dist/orchestration/agent-tool-schemas.js +23 -0
  52. package/dist/orchestration/agent-tool-schemas.js.map +1 -0
  53. package/dist/orchestration/bridge.d.ts +6 -3
  54. package/dist/orchestration/bridge.d.ts.map +1 -1
  55. package/dist/orchestration/bridge.js +31 -14
  56. package/dist/orchestration/bridge.js.map +1 -1
  57. package/dist/orchestration/browser-voice.d.ts +14 -0
  58. package/dist/orchestration/browser-voice.d.ts.map +1 -0
  59. package/dist/orchestration/browser-voice.js +30 -0
  60. package/dist/orchestration/browser-voice.js.map +1 -0
  61. package/dist/orchestration/compact-measurements.d.ts +21 -0
  62. package/dist/orchestration/compact-measurements.d.ts.map +1 -0
  63. package/dist/orchestration/compact-measurements.js +62 -0
  64. package/dist/orchestration/compact-measurements.js.map +1 -0
  65. package/dist/orchestration/config.d.ts +4 -0
  66. package/dist/orchestration/config.d.ts.map +1 -1
  67. package/dist/orchestration/config.js +1 -1
  68. package/dist/orchestration/config.js.map +1 -1
  69. package/dist/orchestration/context-delivery.d.ts +32 -0
  70. package/dist/orchestration/context-delivery.d.ts.map +1 -0
  71. package/dist/orchestration/context-delivery.js +101 -0
  72. package/dist/orchestration/context-delivery.js.map +1 -0
  73. package/dist/orchestration/context-footprint.d.ts +14 -0
  74. package/dist/orchestration/context-footprint.d.ts.map +1 -0
  75. package/dist/orchestration/context-footprint.js +57 -0
  76. package/dist/orchestration/context-footprint.js.map +1 -0
  77. package/dist/orchestration/conversation-intake.d.ts +10 -6
  78. package/dist/orchestration/conversation-intake.d.ts.map +1 -1
  79. package/dist/orchestration/conversation-intake.js +22 -8
  80. package/dist/orchestration/conversation-intake.js.map +1 -1
  81. package/dist/orchestration/dashboard-charts.d.ts +32 -0
  82. package/dist/orchestration/dashboard-charts.d.ts.map +1 -0
  83. package/dist/orchestration/dashboard-charts.js +77 -0
  84. package/dist/orchestration/dashboard-charts.js.map +1 -0
  85. package/dist/orchestration/dashboard-reader-worker.d.ts +2 -0
  86. package/dist/orchestration/dashboard-reader-worker.d.ts.map +1 -0
  87. package/dist/orchestration/dashboard-reader-worker.js +173 -0
  88. package/dist/orchestration/dashboard-reader-worker.js.map +1 -0
  89. package/dist/orchestration/dashboard-reader.d.ts +15 -0
  90. package/dist/orchestration/dashboard-reader.d.ts.map +1 -0
  91. package/dist/orchestration/dashboard-reader.js +87 -0
  92. package/dist/orchestration/dashboard-reader.js.map +1 -0
  93. package/dist/orchestration/decision-context.d.ts +15 -0
  94. package/dist/orchestration/decision-context.d.ts.map +1 -0
  95. package/dist/orchestration/decision-context.js +57 -0
  96. package/dist/orchestration/decision-context.js.map +1 -0
  97. package/dist/orchestration/display-stream.d.ts +21 -2
  98. package/dist/orchestration/display-stream.d.ts.map +1 -1
  99. package/dist/orchestration/display-stream.js +61 -8
  100. package/dist/orchestration/display-stream.js.map +1 -1
  101. package/dist/orchestration/inference-errors.d.ts.map +1 -1
  102. package/dist/orchestration/inference-errors.js +19 -21
  103. package/dist/orchestration/inference-errors.js.map +1 -1
  104. package/dist/orchestration/mutation-recovery.d.ts +13 -0
  105. package/dist/orchestration/mutation-recovery.d.ts.map +1 -0
  106. package/dist/orchestration/mutation-recovery.js +49 -0
  107. package/dist/orchestration/mutation-recovery.js.map +1 -0
  108. package/dist/orchestration/native-compact.d.ts +8 -0
  109. package/dist/orchestration/native-compact.d.ts.map +1 -0
  110. package/dist/orchestration/native-compact.js +33 -0
  111. package/dist/orchestration/native-compact.js.map +1 -0
  112. package/dist/orchestration/notification-mailbox.d.ts +3 -1
  113. package/dist/orchestration/notification-mailbox.d.ts.map +1 -1
  114. package/dist/orchestration/notification-mailbox.js +4 -2
  115. package/dist/orchestration/notification-mailbox.js.map +1 -1
  116. package/dist/orchestration/process-turn.d.ts +16 -2
  117. package/dist/orchestration/process-turn.d.ts.map +1 -1
  118. package/dist/orchestration/process-turn.js +78 -14
  119. package/dist/orchestration/process-turn.js.map +1 -1
  120. package/dist/orchestration/progress-review.d.ts +8 -18
  121. package/dist/orchestration/progress-review.d.ts.map +1 -1
  122. package/dist/orchestration/progress-review.js +12 -14
  123. package/dist/orchestration/progress-review.js.map +1 -1
  124. package/dist/orchestration/provider-message.d.ts +7 -0
  125. package/dist/orchestration/provider-message.d.ts.map +1 -0
  126. package/dist/orchestration/provider-message.js +63 -0
  127. package/dist/orchestration/provider-message.js.map +1 -0
  128. package/dist/orchestration/response-errors.d.ts +6 -0
  129. package/dist/orchestration/response-errors.d.ts.map +1 -0
  130. package/dist/orchestration/response-errors.js +88 -0
  131. package/dist/orchestration/response-errors.js.map +1 -0
  132. package/dist/orchestration/response-schema.d.ts +33 -0
  133. package/dist/orchestration/response-schema.d.ts.map +1 -0
  134. package/dist/orchestration/response-schema.js +61 -0
  135. package/dist/orchestration/response-schema.js.map +1 -0
  136. package/dist/orchestration/runtime.d.ts +79 -0
  137. package/dist/orchestration/runtime.d.ts.map +1 -1
  138. package/dist/orchestration/runtime.js +415 -68
  139. package/dist/orchestration/runtime.js.map +1 -1
  140. package/dist/orchestration/session-compaction.d.ts +59 -0
  141. package/dist/orchestration/session-compaction.d.ts.map +1 -0
  142. package/dist/orchestration/session-compaction.js +212 -0
  143. package/dist/orchestration/session-compaction.js.map +1 -0
  144. package/dist/orchestration/skills.d.ts.map +1 -1
  145. package/dist/orchestration/skills.js +3 -2
  146. package/dist/orchestration/skills.js.map +1 -1
  147. package/dist/orchestration/source-policy.d.ts +1 -1
  148. package/dist/orchestration/source-policy.d.ts.map +1 -1
  149. package/dist/orchestration/source-policy.js +1 -1
  150. package/dist/orchestration/speech.d.ts +15 -17
  151. package/dist/orchestration/speech.d.ts.map +1 -1
  152. package/dist/orchestration/speech.js +90 -30
  153. package/dist/orchestration/speech.js.map +1 -1
  154. package/dist/orchestration/store.d.ts +2 -0
  155. package/dist/orchestration/store.d.ts.map +1 -1
  156. package/dist/orchestration/store.js +16 -1
  157. package/dist/orchestration/store.js.map +1 -1
  158. package/dist/orchestration/tasks/driver.d.ts.map +1 -1
  159. package/dist/orchestration/tasks/driver.js +8 -3
  160. package/dist/orchestration/tasks/driver.js.map +1 -1
  161. package/dist/orchestration/tasks/pool.d.ts.map +1 -1
  162. package/dist/orchestration/tasks/pool.js +8 -4
  163. package/dist/orchestration/tasks/pool.js.map +1 -1
  164. package/dist/orchestration/tasks/service.d.ts +48 -1
  165. package/dist/orchestration/tasks/service.d.ts.map +1 -1
  166. package/dist/orchestration/tasks/service.js +41 -17
  167. package/dist/orchestration/tasks/service.js.map +1 -1
  168. package/dist/orchestration/token-ledger.d.ts +132 -0
  169. package/dist/orchestration/token-ledger.d.ts.map +1 -0
  170. package/dist/orchestration/token-ledger.js +233 -0
  171. package/dist/orchestration/token-ledger.js.map +1 -0
  172. package/dist/orchestration/token-usage.d.ts +46 -0
  173. package/dist/orchestration/token-usage.d.ts.map +1 -0
  174. package/dist/orchestration/token-usage.js +102 -0
  175. package/dist/orchestration/token-usage.js.map +1 -0
  176. package/dist/orchestration/tool-activity.d.ts.map +1 -1
  177. package/dist/orchestration/tool-activity.js +5 -3
  178. package/dist/orchestration/tool-activity.js.map +1 -1
  179. package/dist/orchestration/tool-name.d.ts +10 -0
  180. package/dist/orchestration/tool-name.d.ts.map +1 -0
  181. package/dist/orchestration/tool-name.js +14 -0
  182. package/dist/orchestration/tool-name.js.map +1 -0
  183. package/dist/orchestration/transcript-checkpoint.d.ts +14 -0
  184. package/dist/orchestration/transcript-checkpoint.d.ts.map +1 -0
  185. package/dist/orchestration/transcript-checkpoint.js +83 -0
  186. package/dist/orchestration/transcript-checkpoint.js.map +1 -0
  187. package/dist/orchestration/unanswered-inputs.d.ts +6 -0
  188. package/dist/orchestration/unanswered-inputs.d.ts.map +1 -0
  189. package/dist/orchestration/unanswered-inputs.js +18 -0
  190. package/dist/orchestration/unanswered-inputs.js.map +1 -0
  191. package/dist/session/process.d.ts +10 -0
  192. package/dist/session/process.d.ts.map +1 -1
  193. package/dist/session/process.js +91 -9
  194. package/dist/session/process.js.map +1 -1
  195. package/dist/session/request-tool-capture.d.ts +31 -0
  196. package/dist/session/request-tool-capture.d.ts.map +1 -0
  197. package/dist/session/request-tool-capture.js +194 -0
  198. package/dist/session/request-tool-capture.js.map +1 -0
  199. package/dist/session/runtime-profile.d.ts +7 -3
  200. package/dist/session/runtime-profile.d.ts.map +1 -1
  201. package/dist/session/runtime-profile.js +3 -1
  202. package/dist/session/runtime-profile.js.map +1 -1
  203. package/dist/session/store.d.ts +7 -0
  204. package/dist/session/store.d.ts.map +1 -1
  205. package/dist/session/store.js +27 -0
  206. package/dist/session/store.js.map +1 -1
  207. package/dist/shell/tailer.d.ts +7 -5
  208. package/dist/shell/tailer.d.ts.map +1 -1
  209. package/dist/shell/tailer.js +6 -17
  210. package/dist/shell/tailer.js.map +1 -1
  211. package/dist/types.d.ts +3 -0
  212. package/dist/types.d.ts.map +1 -1
  213. package/dist/ui/cli-viewer-ui.js +1 -1
  214. package/dist/ui/dashboard-charts.d.ts +3 -0
  215. package/dist/ui/dashboard-charts.d.ts.map +1 -0
  216. package/dist/ui/dashboard-charts.js +130 -0
  217. package/dist/ui/dashboard-charts.js.map +1 -0
  218. package/dist/ui/dashboard-client.d.ts +3 -0
  219. package/dist/ui/dashboard-client.d.ts.map +1 -0
  220. package/dist/ui/dashboard-client.js +184 -0
  221. package/dist/ui/dashboard-client.js.map +1 -0
  222. package/dist/ui/dashboard-controls.d.ts +4 -0
  223. package/dist/ui/dashboard-controls.d.ts.map +1 -0
  224. package/dist/ui/dashboard-controls.js +27 -0
  225. package/dist/ui/dashboard-controls.js.map +1 -0
  226. package/dist/ui/dashboard-presentation.d.ts +12 -0
  227. package/dist/ui/dashboard-presentation.d.ts.map +1 -0
  228. package/dist/ui/dashboard-presentation.js +62 -0
  229. package/dist/ui/dashboard-presentation.js.map +1 -0
  230. package/dist/ui/dashboard-range.d.ts +5 -0
  231. package/dist/ui/dashboard-range.d.ts.map +1 -0
  232. package/dist/ui/dashboard-range.js +36 -0
  233. package/dist/ui/dashboard-range.js.map +1 -0
  234. package/dist/ui/dashboard-theme.d.ts +6 -0
  235. package/dist/ui/dashboard-theme.d.ts.map +1 -0
  236. package/dist/ui/dashboard-theme.js +562 -0
  237. package/dist/ui/dashboard-theme.js.map +1 -0
  238. package/dist/ui/memory-activity.d.ts +4 -0
  239. package/dist/ui/memory-activity.d.ts.map +1 -0
  240. package/dist/ui/memory-activity.js +115 -0
  241. package/dist/ui/memory-activity.js.map +1 -0
  242. package/dist/ui/token-report-client.d.ts +3 -0
  243. package/dist/ui/token-report-client.d.ts.map +1 -0
  244. package/dist/ui/token-report-client.js +34 -0
  245. package/dist/ui/token-report-client.js.map +1 -0
  246. package/dist/ui/token-report.d.ts +90 -0
  247. package/dist/ui/token-report.d.ts.map +1 -0
  248. package/dist/ui/token-report.js +203 -0
  249. package/dist/ui/token-report.js.map +1 -0
  250. package/dist/ui/web-ui.d.ts +1 -1
  251. package/dist/ui/web-ui.d.ts.map +1 -1
  252. package/dist/ui/web-ui.js +129 -409
  253. package/dist/ui/web-ui.js.map +1 -1
  254. package/dist/voice/replay.d.ts +8 -0
  255. package/dist/voice/replay.d.ts.map +1 -0
  256. package/dist/voice/replay.js +48 -0
  257. package/dist/voice/replay.js.map +1 -0
  258. package/dist/voice/session.d.ts +9 -0
  259. package/dist/voice/session.d.ts.map +1 -1
  260. package/dist/voice/session.js +72 -14
  261. package/dist/voice/session.js.map +1 -1
  262. package/mcp/lazy-connector.test.ts +58 -0
  263. package/mcp/lazy-connector.ts +67 -0
  264. package/mcp/lazy-tools.test.ts +152 -0
  265. package/mcp/lazy-tools.ts +41 -0
  266. package/mcp/server.ts +53 -34
  267. package/mcp/tools/discord/commands.ts +2 -0
  268. package/mcp/tools/tasks/module.ts +1 -12
  269. package/mcp/tools/telegram/receiver-server.ts +45 -31
  270. package/mcp/tools/telegram/session-browser.ts +10 -0
  271. package/package.json +2 -1
  272. package/resource/dashboard/OFL.txt +93 -0
  273. package/resource/dashboard/poppins-300.woff2 +0 -0
  274. package/resource/dashboard/poppins-400.woff2 +0 -0
  275. package/resource/dashboard/poppins-500.woff2 +0 -0
  276. package/resource/dashboard/poppins-600.woff2 +0 -0
@@ -38,6 +38,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.GatewayRouter = void 0;
40
40
  exports.resolveBindHost = resolveBindHost;
41
+ const memory_activity_1 = require("./memory-activity");
42
+ const dashboard_range_1 = require("../ui/dashboard-range");
43
+ const dashboard_reader_1 = require("../orchestration/dashboard-reader");
41
44
  const express_1 = __importDefault(require("express"));
42
45
  const http = __importStar(require("node:http"));
43
46
  const child_process_1 = require("child_process");
@@ -51,6 +54,7 @@ const pty_stream_registry_1 = require("../shell/pty-stream-registry");
51
54
  const control_channel_1 = require("../shell/control-channel");
52
55
  const factory_1 = require("../watch/factory");
53
56
  const web_ui_1 = require("../ui/web-ui");
57
+ const token_report_1 = require("../ui/token-report");
54
58
  const knowledge_1 = require("../agent/knowledge");
55
59
  const logger_1 = require("../logger");
56
60
  const report_1 = require("../agent/dreaming/report");
@@ -228,6 +232,8 @@ class RateLimiter {
228
232
  }
229
233
  class GatewayRouter {
230
234
  constructor(agents, configs, schedulers, gatewayConfig, cronManager, configPath, appsRegistry, appInstaller, appRegistryClient) {
235
+ this.dashboardReader = new dashboard_reader_1.DashboardReader();
236
+ this.dashboardStreams = new Set();
231
237
  // ─── App proxy ──────────────────────────────────────────────────────────
232
238
  /** "appName:portName" → ProxyRoute */
233
239
  this.routeMap = new Map();
@@ -774,6 +780,19 @@ class GatewayRouter {
774
780
  this.app.get('/health', (_req, res) => {
775
781
  res.json({ status: 'ok' });
776
782
  });
783
+ // Public static typeface assets contain no gateway data.
784
+ this.app.get('/dashboard/fonts.css', (_req, res) => {
785
+ res.setHeader('Cache-Control', 'public, max-age=86400');
786
+ res.type('css').send([300, 400, 500, 600].map(weight => `@font-face{font-family:Poppins;font-style:normal;font-weight:${weight};font-display:swap;src:url(fonts/poppins-${weight}.woff2) format('woff2')}`).join('\n'));
787
+ });
788
+ this.app.get('/dashboard/fonts/:font', (req, res) => {
789
+ if (!/^poppins-(300|400|500|600)\.woff2$/.test(req.params.font)) {
790
+ res.sendStatus(404);
791
+ return;
792
+ }
793
+ res.setHeader('Cache-Control', 'public, max-age=86400');
794
+ res.sendFile(path.resolve(__dirname, '../../resource/dashboard', req.params.font));
795
+ });
777
796
  // Web dashboard. When API keys are configured, require a live dashboard
778
797
  // session cookie; otherwise serve the login page (API-key form). Keyless
779
798
  // installs have no credential to check, so the dashboard stays open there —
@@ -797,10 +816,123 @@ class GatewayRouter {
797
816
  delete require.cache[webUiPath];
798
817
  // eslint-disable-next-line @typescript-eslint/no-require-imports
799
818
  const { generateDashboardHtml: fresh } = require('../ui/web-ui');
800
- res.send(fresh());
819
+ res.send(fresh(this.gatewayConfig?.gateway?.timezone));
801
820
  }
802
821
  else {
803
- res.send((0, web_ui_1.generateDashboardHtml)());
822
+ res.send((0, web_ui_1.generateDashboardHtml)(this.gatewayConfig?.gateway?.timezone));
823
+ }
824
+ });
825
+ // Reports expose cross-agent usage/tool inventories: the same admin gate as status.
826
+ for (const reportPath of ['/dashboard/token-report', '/token-report']) {
827
+ this.app.get(reportPath, async (req, res) => {
828
+ res.setHeader('Cache-Control', 'no-store');
829
+ // The HTML page requires an explicit dashboard login, just like /dashboard.
830
+ // Reverse proxies may inject an admin API key: that must not log a browser in.
831
+ // The JSON /token-report endpoint retains API-key access for API clients.
832
+ if (reportPath === '/dashboard/token-report' && this.apiKeys.length > 0 && !this.hasValidDashSession(req)) {
833
+ // Relative redirect preserves deployment prefixes (e.g. /gateway) without
834
+ // trusting forwarded headers. Also handle Express's optional trailing slash.
835
+ const query = new URLSearchParams();
836
+ for (const key of ['agentId', 'sessionId', 'scope', 'offset'])
837
+ if (typeof req.query[key] === 'string')
838
+ query.set(key, req.query[key]);
839
+ res.redirect(303, (req.path.endsWith('/') ? '../' : './') + '?returnTo=' + encodeURIComponent('token-report?' + query.toString()));
840
+ return;
841
+ }
842
+ if (!this.requireDashOrApiKey(req, res))
843
+ return;
844
+ const { agentId, sessionId } = req.query;
845
+ const offset = Number(req.query.offset ?? 0);
846
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > 1000000) {
847
+ res.status(400).json({ error: 'Invalid offset' });
848
+ return;
849
+ }
850
+ if (typeof agentId !== 'string' || typeof sessionId !== 'string' || !sessionId || sessionId.length > 256) {
851
+ res.status(400).json({ error: 'agentId and sessionId are required' });
852
+ return;
853
+ }
854
+ const runner = this.agents.get(agentId);
855
+ if (!runner) {
856
+ res.status(404).json({ error: 'Unknown agent' });
857
+ return;
858
+ }
859
+ try {
860
+ const source = runner.getDashboardSource?.();
861
+ let report = source ? await this.dashboardReader.read('report', source.filename, { workspace: source.workspace, sessionId, offset, since: (0, dashboard_range_1.dashboardSince)(req.query.scope ?? (reportPath === '/token-report' ? 'all' : '24h'), Date.now(), this.gatewayConfig?.gateway?.timezone), historyFilename: source.historyFilename }) : await runner.getTokenReport(sessionId);
862
+ if (!report) {
863
+ res.status(404).json({ error: 'No recorded session token report' });
864
+ return;
865
+ }
866
+ if (runner.isSessionCompacting?.(sessionId))
867
+ report = { ...report, activityStatus: 'compacting' };
868
+ if (report.contextWindow)
869
+ report.contextWindow.total = report.contextWindow.model ? await runner.dashboardContextWindow?.(report.contextWindow.model) ?? null : null;
870
+ if (reportPath === '/token-report')
871
+ res.json(report);
872
+ else
873
+ res.type('html').send((0, token_report_1.generateTokenReportHtml)(agentId, { ...report, timezone: this.gatewayConfig?.gateway?.timezone, scope: (0, dashboard_range_1.dashboardRange)(req.query.scope), sessionStatus: runner.agentSessionLiveStatus?.(sessionId) ?? 'stopped' }));
874
+ }
875
+ catch {
876
+ res.status(500).json({ error: 'Unable to load session token report' });
877
+ }
878
+ });
879
+ }
880
+ this.app.get('/dashboard/session', async (req, res) => {
881
+ if (!this.requireDashOrApiKey(req, res))
882
+ return;
883
+ res.setHeader('Cache-Control', 'no-store');
884
+ const { agentId, sessionId } = req.query, offset = Number(req.query.offset ?? 0);
885
+ if ([agentId, sessionId].some(v => typeof v !== 'string' || !v || v.length > 256) || !Number.isSafeInteger(offset) || offset < 0 || offset > 1000000) {
886
+ res.status(400).json({ error: 'Invalid session query' });
887
+ return;
888
+ }
889
+ const detailRunner = this.agents.get(String(agentId));
890
+ const source = detailRunner?.getDashboardSource?.();
891
+ if (!source) {
892
+ res.status(404).json({ error: 'Unknown agent' });
893
+ return;
894
+ }
895
+ try {
896
+ const detail = await this.dashboardReader.read('session', source.filename, { sessionId, offset, since: (0, dashboard_range_1.dashboardSince)(req.query.scope, Date.now(), this.gatewayConfig?.gateway?.timezone), historyFilename: source.historyFilename });
897
+ if (!detail) {
898
+ res.status(404).json({ error: 'Session not found' });
899
+ return;
900
+ }
901
+ if (detailRunner?.isSessionCompacting?.(String(sessionId)))
902
+ detail.activityStatus = 'compacting';
903
+ if (detail.contextWindow)
904
+ detail.contextWindow.total = detail.contextWindow.model ? await detailRunner?.dashboardContextWindow?.(detail.contextWindow.model) ?? null : null;
905
+ res.json({ ...detail, sessionStatus: detailRunner?.agentSessionLiveStatus?.(String(sessionId)) ?? 'stopped' });
906
+ }
907
+ catch {
908
+ res.status(503).json({ error: 'Dashboard data temporarily unavailable' });
909
+ }
910
+ });
911
+ this.app.get('/dashboard/task', async (req, res) => {
912
+ if (!this.requireDashOrApiKey(req, res))
913
+ return;
914
+ res.setHeader('Cache-Control', 'no-store');
915
+ const { agentId, sessionId, taskId } = req.query;
916
+ const offset = Number(req.query.offset ?? 0);
917
+ if ([agentId, sessionId, taskId].some(v => typeof v !== 'string' || !v || v.length > 256) || !Number.isSafeInteger(offset) || offset < 0 || offset > 1000000) {
918
+ res.status(400).json({ error: 'Valid agentId, sessionId, taskId and offset are required' });
919
+ return;
920
+ }
921
+ const source = this.agents.get(String(agentId))?.getDashboardSource?.();
922
+ if (!source) {
923
+ res.status(404).json({ error: 'Unknown agent' });
924
+ return;
925
+ }
926
+ try {
927
+ const task = await this.dashboardReader.read('task', source.filename, { sessionId, taskId, offset, since: (0, dashboard_range_1.dashboardSince)(req.query.scope, Date.now(), this.gatewayConfig?.gateway?.timezone) });
928
+ if (!task) {
929
+ res.status(404).json({ error: 'Task not found in this session' });
930
+ return;
931
+ }
932
+ res.json(task);
933
+ }
934
+ catch {
935
+ res.status(503).json({ error: 'Dashboard data temporarily unavailable' });
804
936
  }
805
937
  });
806
938
  // Dashboard login — exchange a configured API key for an HttpOnly session
@@ -1030,6 +1162,66 @@ class GatewayRouter {
1030
1162
  res.status(500).json({ error: 'Failed to list knowledge sources' });
1031
1163
  }
1032
1164
  });
1165
+ let memorySnapshot;
1166
+ let memorySnapshotUntil = 0;
1167
+ this.app.get('/dashboard/memory-activity', async (req, res) => {
1168
+ if (!this.requireDashOrApiKey(req, res))
1169
+ return;
1170
+ res.setHeader('Cache-Control', 'no-store');
1171
+ const { agentId, kind, status, id, scope, completedOnly } = req.query, page = Number(req.query.page ?? 0);
1172
+ if (!Number.isSafeInteger(page) || page < 0 || page > 1000000 ||
1173
+ [agentId, kind, status, id, scope].some(v => v !== undefined && (typeof v !== 'string' || v.length > 256)) ||
1174
+ (completedOnly !== undefined && completedOnly !== 'true' && completedOnly !== 'false') ||
1175
+ (kind !== undefined && !['all', 'memory_dream', 'session_compaction'].includes(String(kind))) ||
1176
+ (status !== undefined && !String(status).split(',').every(s => ['all', 'none', 'pending', 'running', 'completed', 'failed', 'partial_failure', 'interrupted', 'skipped'].includes(s))) ||
1177
+ (scope !== undefined && !['24h', '7d', '30d', '90d', 'all'].includes(String(scope)))) {
1178
+ res.status(400).json({ error: 'Invalid activity filters' });
1179
+ return;
1180
+ }
1181
+ if (agentId && !this.agents.has(String(agentId))) {
1182
+ res.status(404).json({ error: 'Unknown agent' });
1183
+ return;
1184
+ }
1185
+ try {
1186
+ if (!memorySnapshot || Date.now() >= memorySnapshotUntil) {
1187
+ memorySnapshotUntil = Infinity;
1188
+ const snapshot = (0, memory_activity_1.readMemoryActivity)(this.agents, this.agentsRoot(), (filename, agentId) => this.dashboardReader.read('compaction', filename, { agentId })).then(data => { if (memorySnapshot === snapshot)
1189
+ memorySnapshotUntil = Date.now() + 5000; return data; }, error => { if (memorySnapshot === snapshot)
1190
+ memorySnapshot = undefined; throw error; });
1191
+ memorySnapshot = snapshot;
1192
+ }
1193
+ const data = await memorySnapshot;
1194
+ if (id) {
1195
+ let run = data.runs.find(r => r.id === id && r.agent === agentId);
1196
+ if (!run) {
1197
+ res.status(404).json({ error: 'Activity not found' });
1198
+ return;
1199
+ }
1200
+ const source = this.agents.get(String(agentId))?.getDashboardSource?.();
1201
+ if (run.kind === 'session_compaction' && source)
1202
+ run = await this.dashboardReader.read('compaction', source.filename, { agentId, runId: id });
1203
+ if (!run) {
1204
+ res.status(404).json({ error: 'Activity not found' });
1205
+ return;
1206
+ }
1207
+ if (completedOnly === 'true' && run.kind === 'session_compaction') {
1208
+ run = { ...run, items: (run.items ?? []).filter((item) => item.status === 'completed'), itemCount: run.completedSessions };
1209
+ }
1210
+ res.json({ run });
1211
+ return;
1212
+ }
1213
+ const timezone = this.gatewayConfig?.gateway?.timezone || 'UTC';
1214
+ const since = (0, dashboard_range_1.dashboardSince)(scope, Date.now(), timezone);
1215
+ const runs = data.runs.filter(r => (completedOnly !== 'true' || r.kind !== 'session_compaction' || r.completedSessions > 0 || (r.items ?? []).some((i) => i.status === 'completed')) && (!agentId || r.agent === agentId) && (!kind || kind === 'all' || r.kind === kind) && (!status || status === 'all' || String(status).split(',').includes(r.status)) && r.startedAt >= since);
1216
+ res.json({ timezone, since, runs: runs.slice(page * 25, page * 25 + 25).map(memory_activity_1.activitySummary), total: runs.length, page, pageSize: 25,
1217
+ agents: data.agents, schedules: data.schedules.filter(s => !agentId || s.agent === agentId), unavailable: data.unavailable,
1218
+ counts: { compactedSessions: runs.reduce((n, r) => n + (r.completedSessions ?? r.items?.filter((i) => i.status === 'completed').length ?? 0), 0), measuredReduction: runs.reduce((n, r) => n + ((0, memory_activity_1.activitySummary)(r).measuredReduction ?? 0), 0), measuredSessions: runs.reduce((n, r) => n + ((0, memory_activity_1.activitySummary)(r).measuredSessions ?? 0), 0), runs: runs.length, pendingProposals: runs.reduce((n, r) => n + (r.pendingProposals ?? 0), 0), failed: runs.filter(r => ['failed', 'partial_failure', 'interrupted'].includes(r.status)).length },
1219
+ historyLimit: 100 });
1220
+ }
1221
+ catch {
1222
+ res.status(503).json({ error: 'Memory activity temporarily unavailable' });
1223
+ }
1224
+ });
1033
1225
  // Nightly dreaming report — parses every agent's `.dreaming/` audit trail
1034
1226
  // (DREAMS.md + promotions.jsonl) into newest-first runs for the dashboard's
1035
1227
  // "Nightly dreaming" tab. Auth: dashboard session cookie OR API key. Payload is
@@ -1044,7 +1236,7 @@ class GatewayRouter {
1044
1236
  const runs = [];
1045
1237
  const agents = [];
1046
1238
  for (const id of this.agents.keys()) {
1047
- const dir = path.join(root, id, 'workspace', audit_1.DREAMING_DIR);
1239
+ const dir = path.join(this.agents.get(id)?.workspacePath ?? path.join(root, id, 'workspace'), audit_1.DREAMING_DIR);
1048
1240
  let dreams;
1049
1241
  try {
1050
1242
  dreams = fs.readFileSync(path.join(dir, 'DREAMS.md'), 'utf8');
@@ -1100,6 +1292,7 @@ class GatewayRouter {
1100
1292
  this.app.post('/knowledge/dreams/apply', (req, res) => {
1101
1293
  if (!this.requireDashOrApiKey(req, res))
1102
1294
  return;
1295
+ memorySnapshot = undefined;
1103
1296
  try {
1104
1297
  const body = (req.body ?? {});
1105
1298
  const agentId = typeof body.agentId === 'string' ? body.agentId : '';
@@ -1123,7 +1316,7 @@ class GatewayRouter {
1123
1316
  }
1124
1317
  indexes = body.indexes;
1125
1318
  }
1126
- const workspaceDir = path.join(this.agentsRoot(), agentId, 'workspace');
1319
+ const workspaceDir = this.agents.get(agentId)?.workspacePath ?? path.join(this.agentsRoot(), agentId, 'workspace');
1127
1320
  const budget = (0, workspace_loader_1.resolveMemoryBudget)({
1128
1321
  ...this.gatewayConfig?.gateway?.memory,
1129
1322
  ...this.configs.get(agentId)?.memory,
@@ -1177,12 +1370,9 @@ class GatewayRouter {
1177
1370
  res.status(500).json({ error: 'Failed to read screen state' });
1178
1371
  });
1179
1372
  });
1180
- // Status endpoint per-agent stats + heartbeat history
1181
- this.app.get('/status', (req, res) => {
1182
- if (!this.requireDashOrApiKey(req, res))
1183
- return;
1373
+ const dashboardSnapshot = async (offset, scope = 'all') => {
1184
1374
  const uptimeMs = Date.now() - this.startedAt.getTime();
1185
- const agentsStatus = [...this.agents.entries()].map(([id, runner]) => {
1375
+ const agentsStatus = await Promise.all([...this.agents.entries()].map(async ([id, runner]) => {
1186
1376
  const scheduler = this.schedulers.get(id);
1187
1377
  const history = scheduler?.getHistory();
1188
1378
  const agentConfig = this.configs.get(id);
@@ -1206,10 +1396,20 @@ class GatewayRouter {
1206
1396
  }).filter(Boolean);
1207
1397
  const lastActivity = this.lastActivityAt.get(id);
1208
1398
  // PTY streams are keyed per session, so liveness is per session too.
1209
- const orchestration = runner.getOrchestrationSummary?.();
1210
- const managedSessions = orchestration?.sessions ?? [];
1211
- const managedIds = new Set(managedSessions.map(s => s.sessionId));
1212
- const sessions = [...runner.getSessionsSummary().filter(s => !managedIds.has(s.sessionId)), ...managedSessions].map((s) => ({
1399
+ const source = runner.getDashboardSource?.();
1400
+ const legacySessions = runner.getSessionsSummary();
1401
+ const liveSessionIds = new Set(runner.liveSessionIds?.() ?? []);
1402
+ const orchestration = source ? (source.enabled ? await this.dashboardReader.read('summary', source.filename, { ...source, timezone: this.gatewayConfig?.gateway?.timezone, offset, since: (0, dashboard_range_1.dashboardSince)(scope, Date.now(), this.gatewayConfig?.gateway?.timezone), legacyIds: legacySessions.map(s => s.sessionId) }) : undefined) : runner.getOrchestrationSummary?.();
1403
+ const managedSessions = (orchestration?.sessions ?? []).map((s) => ({
1404
+ ...s,
1405
+ // Live liveness from the runner's in-memory map: the DB-derived status
1406
+ // reports 'idle' for both a kept-alive session and one already killed by
1407
+ // the idle cleaner; sessionAlive disambiguates them for the dashboard.
1408
+ sessionAlive: liveSessionIds.has(s.sessionId),
1409
+ ...(runner.isSessionCompacting?.(s.sessionId) ? { status: 'compacting', isRunning: true } : {}),
1410
+ }));
1411
+ const managedIds = new Set([...managedSessions.map((s) => s.sessionId), ...(orchestration?.managedLegacyIds ?? [])]);
1412
+ const sessions = [...legacySessions.filter(s => !managedIds.has(s.sessionId) && (!(0, dashboard_range_1.dashboardSince)(scope, Date.now(), this.gatewayConfig?.gateway?.timezone) || Number(s.updatedAt || s.spawnedAt || 0) >= (0, dashboard_range_1.dashboardSince)(scope, Date.now(), this.gatewayConfig?.gateway?.timezone))), ...managedSessions].map((s) => ({
1213
1413
  ...s,
1214
1414
  hasPtyStream: pty_stream_registry_1.ptyStreamRegistry.hasSockets(s.sessionId),
1215
1415
  }));
@@ -1235,15 +1435,118 @@ class GatewayRouter {
1235
1435
  },
1236
1436
  sessions,
1237
1437
  };
1238
- });
1239
- res.json({
1438
+ }));
1439
+ return {
1240
1440
  agents: agentsStatus,
1241
1441
  uptime: Math.floor(uptimeMs / 1000),
1242
1442
  startedAt: this.startedAt.toISOString(),
1243
1443
  version: GATEWAY_VERSION,
1244
1444
  // Degraded file watchers (e.g. inotify ENOSPC). Empty array when healthy.
1245
1445
  watchers: (0, factory_1.getWatcherHealth)(),
1246
- });
1446
+ };
1447
+ };
1448
+ this.app.get('/dashboard/charts', async (req, res) => {
1449
+ if (!this.requireDashOrApiKey(req, res))
1450
+ return;
1451
+ res.setHeader('Cache-Control', 'no-store');
1452
+ const scope = req.query.scope ?? '24h';
1453
+ if (typeof scope !== 'string' || !['24h', '7d', '30d', '90d'].includes(scope)) {
1454
+ res.status(400).json({ error: 'Invalid chart range' });
1455
+ return;
1456
+ }
1457
+ const timezone = this.gatewayConfig?.gateway?.timezone || 'UTC';
1458
+ // Stable snapshot timestamp coalesces all four cards and concurrent browsers.
1459
+ const now = Math.floor(Date.now() / 10000) * 10000;
1460
+ try {
1461
+ const agents = [];
1462
+ for (const [id, runner] of this.agents) {
1463
+ const source = runner.getDashboardSource?.();
1464
+ if (!source?.enabled)
1465
+ continue;
1466
+ const usage = await this.dashboardReader.read('charts', source.filename, { scope, timezone, now });
1467
+ if (usage)
1468
+ agents.push({ id, ...usage });
1469
+ }
1470
+ res.json({ scope, timezone, asOf: now, since: (0, dashboard_range_1.dashboardSince)(scope, now, timezone), agents });
1471
+ }
1472
+ catch {
1473
+ res.status(503).json({ error: 'Chart data temporarily unavailable' });
1474
+ }
1475
+ });
1476
+ this.app.get('/status', async (req, res) => {
1477
+ if (!this.requireDashOrApiKey(req, res))
1478
+ return;
1479
+ res.setHeader('Cache-Control', 'no-store');
1480
+ const offset = Number(req.query.offset ?? 0);
1481
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > 1000000) {
1482
+ res.status(400).json({ error: 'Invalid offset' });
1483
+ return;
1484
+ }
1485
+ try {
1486
+ res.json(await dashboardSnapshot(offset, req.query.scope ?? 'all'));
1487
+ }
1488
+ catch {
1489
+ res.status(503).json({ error: 'Dashboard data temporarily unavailable' });
1490
+ }
1491
+ });
1492
+ this.app.get('/dashboard/events', async (req, res) => {
1493
+ if (!this.requireDashOrApiKey(req, res))
1494
+ return;
1495
+ const offset = Number(req.query.offset ?? 0);
1496
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > 1000000) {
1497
+ res.status(400).json({ error: 'Invalid offset' });
1498
+ return;
1499
+ }
1500
+ if (this.dashboardStreams.size >= 32) {
1501
+ res.status(503).json({ error: 'Too many dashboard streams' });
1502
+ return;
1503
+ }
1504
+ res.setHeader('Content-Type', 'text/event-stream');
1505
+ res.setHeader('Cache-Control', 'no-store');
1506
+ res.setHeader('X-Accel-Buffering', 'no');
1507
+ res.flushHeaders();
1508
+ this.dashboardStreams.add(res);
1509
+ let closed = false, busy = false, last = String(req.headers['last-event-id'] ?? '');
1510
+ const stop = () => { closed = true; clearInterval(timer); this.dashboardStreams.delete(res); res.end(); };
1511
+ const tick = async () => {
1512
+ if (closed || busy)
1513
+ return;
1514
+ // Recheck cookie expiration/revocation without writing HTTP errors into SSE.
1515
+ if (this.apiKeys.length && !this.hasValidDashSession(req) && !timingSafeAdminKeyMatch(this.apiKeys, this.extractApiToken(req))) {
1516
+ res.write('event: unauthorized\ndata: {}\n\n');
1517
+ stop();
1518
+ return;
1519
+ }
1520
+ // Never queue snapshots behind a slow browser.
1521
+ if (res.writableLength > 256 * 1024) {
1522
+ stop();
1523
+ return;
1524
+ }
1525
+ busy = true;
1526
+ try {
1527
+ const data = await dashboardSnapshot(offset, req.query.scope ?? 'all');
1528
+ if (closed)
1529
+ return;
1530
+ const payload = JSON.stringify(data), id = crypto.createHash('sha256').update(JSON.stringify({ agents: data.agents, watchers: data.watchers, version: data.version })).digest('hex').slice(0, 24);
1531
+ if (id !== last) {
1532
+ res.write('id: ' + id + '\nevent: snapshot\ndata: ' + payload + '\n\n');
1533
+ last = id;
1534
+ }
1535
+ else
1536
+ res.write(': heartbeat\n\n');
1537
+ }
1538
+ catch {
1539
+ if (!closed)
1540
+ res.write('event: unavailable\ndata: {}\n\n');
1541
+ }
1542
+ finally {
1543
+ busy = false;
1544
+ }
1545
+ };
1546
+ const timer = setInterval(() => void tick(), 5000);
1547
+ timer.unref();
1548
+ req.on('close', stop);
1549
+ void tick();
1247
1550
  });
1248
1551
  }
1249
1552
  /** The port the HTTP server is actually listening on, or null when it is not
@@ -1435,6 +1738,10 @@ class GatewayRouter {
1435
1738
  }
1436
1739
  }
1437
1740
  async stop() {
1741
+ for (const response of this.dashboardStreams)
1742
+ response.end();
1743
+ this.dashboardStreams.clear();
1744
+ await this.dashboardReader.close();
1438
1745
  await this.voiceApi?.close();
1439
1746
  if (this.ticketPruner)
1440
1747
  clearInterval(this.ticketPruner);