botmux 2.33.0 → 2.33.1

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 (281) hide show
  1. package/README.en.md +12 -1
  2. package/README.md +45 -1
  3. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  4. package/dist/adapters/cli/claude-code.js +11 -0
  5. package/dist/adapters/cli/claude-code.js.map +1 -1
  6. package/dist/cli/bots-list-output.d.ts +21 -0
  7. package/dist/cli/bots-list-output.d.ts.map +1 -0
  8. package/dist/cli/bots-list-output.js +23 -0
  9. package/dist/cli/bots-list-output.js.map +1 -0
  10. package/dist/cli/workflow.d.ts +13 -0
  11. package/dist/cli/workflow.d.ts.map +1 -0
  12. package/dist/cli/workflow.js +781 -0
  13. package/dist/cli/workflow.js.map +1 -0
  14. package/dist/cli.js +69 -14
  15. package/dist/cli.js.map +1 -1
  16. package/dist/core/command-handler.d.ts.map +1 -1
  17. package/dist/core/command-handler.js +211 -4
  18. package/dist/core/command-handler.js.map +1 -1
  19. package/dist/core/session-manager.d.ts +6 -1
  20. package/dist/core/session-manager.d.ts.map +1 -1
  21. package/dist/core/session-manager.js +22 -12
  22. package/dist/core/session-manager.js.map +1 -1
  23. package/dist/core/worker-pool.d.ts +13 -0
  24. package/dist/core/worker-pool.d.ts.map +1 -1
  25. package/dist/core/worker-pool.js +100 -6
  26. package/dist/core/worker-pool.js.map +1 -1
  27. package/dist/daemon.d.ts +3 -0
  28. package/dist/daemon.d.ts.map +1 -1
  29. package/dist/daemon.js +884 -3
  30. package/dist/daemon.js.map +1 -1
  31. package/dist/dashboard/auth.d.ts +36 -0
  32. package/dist/dashboard/auth.d.ts.map +1 -1
  33. package/dist/dashboard/auth.js +22 -0
  34. package/dist/dashboard/auth.js.map +1 -1
  35. package/dist/dashboard/web/app.js +20 -1
  36. package/dist/dashboard/web/app.js.map +1 -1
  37. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  38. package/dist/dashboard/web/i18n.js +356 -0
  39. package/dist/dashboard/web/i18n.js.map +1 -1
  40. package/dist/dashboard/web/workflow-catalog.d.ts +2 -0
  41. package/dist/dashboard/web/workflow-catalog.d.ts.map +1 -0
  42. package/dist/dashboard/web/workflow-catalog.js +323 -0
  43. package/dist/dashboard/web/workflow-catalog.js.map +1 -0
  44. package/dist/dashboard/web/workflows.d.ts +2 -0
  45. package/dist/dashboard/web/workflows.d.ts.map +1 -0
  46. package/dist/dashboard/web/workflows.js +1618 -0
  47. package/dist/dashboard/web/workflows.js.map +1 -0
  48. package/dist/dashboard/workflow-api.d.ts +23 -0
  49. package/dist/dashboard/workflow-api.d.ts.map +1 -0
  50. package/dist/dashboard/workflow-api.js +463 -0
  51. package/dist/dashboard/workflow-api.js.map +1 -0
  52. package/dist/dashboard-web/app.js +494 -199
  53. package/dist/dashboard-web/index.html +1 -0
  54. package/dist/dashboard-web/style.css +160 -6
  55. package/dist/dashboard-web/terminal-replay.html +227 -0
  56. package/dist/dashboard.js +29 -12
  57. package/dist/dashboard.js.map +1 -1
  58. package/dist/i18n/en.d.ts.map +1 -1
  59. package/dist/i18n/en.js +12 -0
  60. package/dist/i18n/en.js.map +1 -1
  61. package/dist/i18n/zh.d.ts.map +1 -1
  62. package/dist/i18n/zh.js +12 -0
  63. package/dist/i18n/zh.js.map +1 -1
  64. package/dist/im/lark/card-handler.d.ts +3 -0
  65. package/dist/im/lark/card-handler.d.ts.map +1 -1
  66. package/dist/im/lark/card-handler.js +27 -1
  67. package/dist/im/lark/card-handler.js.map +1 -1
  68. package/dist/im/lark/client.d.ts +19 -2
  69. package/dist/im/lark/client.d.ts.map +1 -1
  70. package/dist/im/lark/client.js +21 -2
  71. package/dist/im/lark/client.js.map +1 -1
  72. package/dist/im/lark/workflow-card-handler.d.ts +50 -0
  73. package/dist/im/lark/workflow-card-handler.d.ts.map +1 -0
  74. package/dist/im/lark/workflow-card-handler.js +152 -0
  75. package/dist/im/lark/workflow-card-handler.js.map +1 -0
  76. package/dist/im/lark/workflow-cards.d.ts +46 -0
  77. package/dist/im/lark/workflow-cards.d.ts.map +1 -0
  78. package/dist/im/lark/workflow-cards.js +226 -0
  79. package/dist/im/lark/workflow-cards.js.map +1 -0
  80. package/dist/im/lark/workflow-progress-card.d.ts +76 -0
  81. package/dist/im/lark/workflow-progress-card.d.ts.map +1 -0
  82. package/dist/im/lark/workflow-progress-card.js +279 -0
  83. package/dist/im/lark/workflow-progress-card.js.map +1 -0
  84. package/dist/im/lark/workflow-slash-command.d.ts +92 -0
  85. package/dist/im/lark/workflow-slash-command.d.ts.map +1 -0
  86. package/dist/im/lark/workflow-slash-command.js +185 -0
  87. package/dist/im/lark/workflow-slash-command.js.map +1 -0
  88. package/dist/services/group-creator.d.ts.map +1 -1
  89. package/dist/services/group-creator.js +17 -4
  90. package/dist/services/group-creator.js.map +1 -1
  91. package/dist/services/groups-store.d.ts +11 -0
  92. package/dist/services/groups-store.d.ts.map +1 -1
  93. package/dist/services/groups-store.js +26 -0
  94. package/dist/services/groups-store.js.map +1 -1
  95. package/dist/services/jsonl-cursor.d.ts +12 -0
  96. package/dist/services/jsonl-cursor.d.ts.map +1 -0
  97. package/dist/services/jsonl-cursor.js +45 -0
  98. package/dist/services/jsonl-cursor.js.map +1 -0
  99. package/dist/services/schedule-store.d.ts +35 -0
  100. package/dist/services/schedule-store.d.ts.map +1 -1
  101. package/dist/services/schedule-store.js +108 -1
  102. package/dist/services/schedule-store.js.map +1 -1
  103. package/dist/skills/definitions.d.ts.map +1 -1
  104. package/dist/skills/definitions.js +399 -0
  105. package/dist/skills/definitions.js.map +1 -1
  106. package/dist/types.d.ts +4 -0
  107. package/dist/types.d.ts.map +1 -1
  108. package/dist/utils/cli-usage-limit.d.ts.map +1 -1
  109. package/dist/utils/cli-usage-limit.js +4 -0
  110. package/dist/utils/cli-usage-limit.js.map +1 -1
  111. package/dist/worker.js +118 -14
  112. package/dist/worker.js.map +1 -1
  113. package/dist/workflows/attempt-resume.d.ts +114 -0
  114. package/dist/workflows/attempt-resume.d.ts.map +1 -0
  115. package/dist/workflows/attempt-resume.js +385 -0
  116. package/dist/workflows/attempt-resume.js.map +1 -0
  117. package/dist/workflows/attempt-terminal.d.ts +21 -0
  118. package/dist/workflows/attempt-terminal.d.ts.map +1 -0
  119. package/dist/workflows/attempt-terminal.js +7 -0
  120. package/dist/workflows/attempt-terminal.js.map +1 -0
  121. package/dist/workflows/blob.d.ts +27 -0
  122. package/dist/workflows/blob.d.ts.map +1 -0
  123. package/dist/workflows/blob.js +39 -0
  124. package/dist/workflows/blob.js.map +1 -0
  125. package/dist/workflows/cancel-run.d.ts +45 -0
  126. package/dist/workflows/cancel-run.d.ts.map +1 -0
  127. package/dist/workflows/cancel-run.js +99 -0
  128. package/dist/workflows/cancel-run.js.map +1 -0
  129. package/dist/workflows/cancel.d.ts +111 -0
  130. package/dist/workflows/cancel.d.ts.map +1 -0
  131. package/dist/workflows/cancel.js +120 -0
  132. package/dist/workflows/cancel.js.map +1 -0
  133. package/dist/workflows/catalog.d.ts +60 -0
  134. package/dist/workflows/catalog.d.ts.map +1 -0
  135. package/dist/workflows/catalog.js +119 -0
  136. package/dist/workflows/catalog.js.map +1 -0
  137. package/dist/workflows/cold-attach.d.ts +30 -0
  138. package/dist/workflows/cold-attach.d.ts.map +1 -0
  139. package/dist/workflows/cold-attach.js +40 -0
  140. package/dist/workflows/cold-attach.js.map +1 -0
  141. package/dist/workflows/cold-scan.d.ts +21 -0
  142. package/dist/workflows/cold-scan.d.ts.map +1 -0
  143. package/dist/workflows/cold-scan.js +70 -0
  144. package/dist/workflows/cold-scan.js.map +1 -0
  145. package/dist/workflows/daemon-spawn.d.ts +117 -0
  146. package/dist/workflows/daemon-spawn.d.ts.map +1 -0
  147. package/dist/workflows/daemon-spawn.js +551 -0
  148. package/dist/workflows/daemon-spawn.js.map +1 -0
  149. package/dist/workflows/definition.d.ts +1309 -0
  150. package/dist/workflows/definition.d.ts.map +1 -0
  151. package/dist/workflows/definition.js +334 -0
  152. package/dist/workflows/definition.js.map +1 -0
  153. package/dist/workflows/effect-input.d.ts +4 -0
  154. package/dist/workflows/effect-input.d.ts.map +1 -0
  155. package/dist/workflows/effect-input.js +18 -0
  156. package/dist/workflows/effect-input.js.map +1 -0
  157. package/dist/workflows/events/append.d.ts +77 -0
  158. package/dist/workflows/events/append.d.ts.map +1 -0
  159. package/dist/workflows/events/append.js +214 -0
  160. package/dist/workflows/events/append.js.map +1 -0
  161. package/dist/workflows/events/idempotency.d.ts +77 -0
  162. package/dist/workflows/events/idempotency.d.ts.map +1 -0
  163. package/dist/workflows/events/idempotency.js +116 -0
  164. package/dist/workflows/events/idempotency.js.map +1 -0
  165. package/dist/workflows/events/index.d.ts +7 -0
  166. package/dist/workflows/events/index.d.ts.map +1 -0
  167. package/dist/workflows/events/index.js +7 -0
  168. package/dist/workflows/events/index.js.map +1 -0
  169. package/dist/workflows/events/payloads.d.ts +917 -0
  170. package/dist/workflows/events/payloads.d.ts.map +1 -0
  171. package/dist/workflows/events/payloads.js +337 -0
  172. package/dist/workflows/events/payloads.js.map +1 -0
  173. package/dist/workflows/events/replay.d.ts +238 -0
  174. package/dist/workflows/events/replay.d.ts.map +1 -0
  175. package/dist/workflows/events/replay.js +608 -0
  176. package/dist/workflows/events/replay.js.map +1 -0
  177. package/dist/workflows/events/schema.d.ts +5242 -0
  178. package/dist/workflows/events/schema.d.ts.map +1 -0
  179. package/dist/workflows/events/schema.js +295 -0
  180. package/dist/workflows/events/schema.js.map +1 -0
  181. package/dist/workflows/events/types.d.ts +34 -0
  182. package/dist/workflows/events/types.d.ts.map +1 -0
  183. package/dist/workflows/events/types.js +2 -0
  184. package/dist/workflows/events/types.js.map +1 -0
  185. package/dist/workflows/fanout.d.ts +36 -0
  186. package/dist/workflows/fanout.d.ts.map +1 -0
  187. package/dist/workflows/fanout.js +114 -0
  188. package/dist/workflows/fanout.js.map +1 -0
  189. package/dist/workflows/hostExecutors/botmux-schedule.d.ts +41 -0
  190. package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -0
  191. package/dist/workflows/hostExecutors/botmux-schedule.js +121 -0
  192. package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -0
  193. package/dist/workflows/hostExecutors/feishu-im.d.ts +12 -0
  194. package/dist/workflows/hostExecutors/feishu-im.d.ts.map +1 -0
  195. package/dist/workflows/hostExecutors/feishu-im.js +49 -0
  196. package/dist/workflows/hostExecutors/feishu-im.js.map +1 -0
  197. package/dist/workflows/hostExecutors/feishu-reply.d.ts +24 -0
  198. package/dist/workflows/hostExecutors/feishu-reply.d.ts.map +1 -0
  199. package/dist/workflows/hostExecutors/feishu-reply.js +88 -0
  200. package/dist/workflows/hostExecutors/feishu-reply.js.map +1 -0
  201. package/dist/workflows/hostExecutors/feishu-send.d.ts +23 -0
  202. package/dist/workflows/hostExecutors/feishu-send.d.ts.map +1 -0
  203. package/dist/workflows/hostExecutors/feishu-send.js +124 -0
  204. package/dist/workflows/hostExecutors/feishu-send.js.map +1 -0
  205. package/dist/workflows/hostExecutors/index.d.ts +8 -0
  206. package/dist/workflows/hostExecutors/index.d.ts.map +1 -0
  207. package/dist/workflows/hostExecutors/index.js +8 -0
  208. package/dist/workflows/hostExecutors/index.js.map +1 -0
  209. package/dist/workflows/hostExecutors/protocol.d.ts +42 -0
  210. package/dist/workflows/hostExecutors/protocol.d.ts.map +1 -0
  211. package/dist/workflows/hostExecutors/protocol.js +181 -0
  212. package/dist/workflows/hostExecutors/protocol.js.map +1 -0
  213. package/dist/workflows/hostExecutors/registry.d.ts +10 -0
  214. package/dist/workflows/hostExecutors/registry.d.ts.map +1 -0
  215. package/dist/workflows/hostExecutors/registry.js +36 -0
  216. package/dist/workflows/hostExecutors/registry.js.map +1 -0
  217. package/dist/workflows/hostExecutors/types.d.ts +78 -0
  218. package/dist/workflows/hostExecutors/types.d.ts.map +1 -0
  219. package/dist/workflows/hostExecutors/types.js +2 -0
  220. package/dist/workflows/hostExecutors/types.js.map +1 -0
  221. package/dist/workflows/loader.d.ts +16 -0
  222. package/dist/workflows/loader.d.ts.map +1 -0
  223. package/dist/workflows/loader.js +56 -0
  224. package/dist/workflows/loader.js.map +1 -0
  225. package/dist/workflows/loop.d.ts +50 -0
  226. package/dist/workflows/loop.d.ts.map +1 -0
  227. package/dist/workflows/loop.js +350 -0
  228. package/dist/workflows/loop.js.map +1 -0
  229. package/dist/workflows/ops-projection.d.ts +168 -0
  230. package/dist/workflows/ops-projection.d.ts.map +1 -0
  231. package/dist/workflows/ops-projection.js +707 -0
  232. package/dist/workflows/ops-projection.js.map +1 -0
  233. package/dist/workflows/orchestrator.d.ts +107 -0
  234. package/dist/workflows/orchestrator.d.ts.map +1 -0
  235. package/dist/workflows/orchestrator.js +197 -0
  236. package/dist/workflows/orchestrator.js.map +1 -0
  237. package/dist/workflows/output-binding.d.ts +70 -0
  238. package/dist/workflows/output-binding.d.ts.map +1 -0
  239. package/dist/workflows/output-binding.js +265 -0
  240. package/dist/workflows/output-binding.js.map +1 -0
  241. package/dist/workflows/params.d.ts +61 -0
  242. package/dist/workflows/params.d.ts.map +1 -0
  243. package/dist/workflows/params.js +195 -0
  244. package/dist/workflows/params.js.map +1 -0
  245. package/dist/workflows/resume.d.ts +263 -0
  246. package/dist/workflows/resume.d.ts.map +1 -0
  247. package/dist/workflows/resume.js +808 -0
  248. package/dist/workflows/resume.js.map +1 -0
  249. package/dist/workflows/run-id.d.ts +2 -0
  250. package/dist/workflows/run-id.d.ts.map +1 -0
  251. package/dist/workflows/run-id.js +7 -0
  252. package/dist/workflows/run-id.js.map +1 -0
  253. package/dist/workflows/run-init.d.ts +48 -0
  254. package/dist/workflows/run-init.d.ts.map +1 -0
  255. package/dist/workflows/run-init.js +99 -0
  256. package/dist/workflows/run-init.js.map +1 -0
  257. package/dist/workflows/runs-dir.d.ts +4 -0
  258. package/dist/workflows/runs-dir.d.ts.map +1 -0
  259. package/dist/workflows/runs-dir.js +15 -0
  260. package/dist/workflows/runs-dir.js.map +1 -0
  261. package/dist/workflows/runtime.d.ts +211 -0
  262. package/dist/workflows/runtime.d.ts.map +1 -0
  263. package/dist/workflows/runtime.js +594 -0
  264. package/dist/workflows/runtime.js.map +1 -0
  265. package/dist/workflows/spawn-bot.d.ts +165 -0
  266. package/dist/workflows/spawn-bot.d.ts.map +1 -0
  267. package/dist/workflows/spawn-bot.js +215 -0
  268. package/dist/workflows/spawn-bot.js.map +1 -0
  269. package/dist/workflows/system.d.ts +49 -0
  270. package/dist/workflows/system.d.ts.map +1 -0
  271. package/dist/workflows/system.js +48 -0
  272. package/dist/workflows/system.js.map +1 -0
  273. package/dist/workflows/trigger-run.d.ts +70 -0
  274. package/dist/workflows/trigger-run.d.ts.map +1 -0
  275. package/dist/workflows/trigger-run.js +88 -0
  276. package/dist/workflows/trigger-run.js.map +1 -0
  277. package/dist/workflows/wait.d.ts +120 -0
  278. package/dist/workflows/wait.d.ts.map +1 -0
  279. package/dist/workflows/wait.js +181 -0
  280. package/dist/workflows/wait.js.map +1 -0
  281. package/package.json +3 -3
@@ -0,0 +1,707 @@
1
+ /**
2
+ * Shared projection helpers for workflow operator surfaces.
3
+ *
4
+ * Used by CLI (`botmux workflow ls` / `tail`) and the dashboard backend.
5
+ * All readers in here are pure: they never `mkdir` and never validate
6
+ * caller-provided runIds as filesystem paths without going through
7
+ * `isValidRunId` first. Callers built on top of this module can hand
8
+ * the resulting DTOs to JSON responses or to plain stdout printers
9
+ * without worrying about side effects.
10
+ *
11
+ * Side-effect contract:
12
+ * - listRuns / readRunSnapshot / readEventWindow all return null / []
13
+ * instead of throwing when a run is missing or its event log is
14
+ * corrupt. Corrupt = "any line fails parseEvent" — same boundary
15
+ * `EventLog.readAll` uses, except we don't crash the caller.
16
+ * - We DO NOT use `EventLog` here; EventLog's constructor mkdirs
17
+ * runDir + blobDir, which is wrong for a read-only API.
18
+ */
19
+ import { promises as fs } from 'node:fs';
20
+ import { relative, resolve, sep, join, dirname } from 'node:path';
21
+ import { parseWorkflowDefinition, } from './definition.js';
22
+ import { parseEvent, } from './events/schema.js';
23
+ import { replay, } from './events/replay.js';
24
+ import { workActivityId } from './orchestrator.js';
25
+ import { attemptTerminalSidecarPath, } from './attempt-terminal.js';
26
+ export const TERMINAL_RUN_STATUSES = new Set(['succeeded', 'failed', 'cancelled']);
27
+ /**
28
+ * runId allowlist — must be passed BEFORE concatenating into a path.
29
+ *
30
+ * The runtime generates runIds via `crypto.randomUUID()` or operator-
31
+ * supplied slugs (CLI / dogfood scripts); both fit `[A-Za-z0-9._-]`.
32
+ * This guard rejects `.`, `..`, slashes, and anything else that could
33
+ * escape `runsDir` via path traversal, plus empty strings.
34
+ */
35
+ const RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
36
+ export function isValidRunId(runId) {
37
+ return RUN_ID_RE.test(runId);
38
+ }
39
+ /**
40
+ * Activity / attempt id allowlist — accepts `<runId>::work::<nodeId>` and
41
+ * `<...>::att-N` shaped strings the orchestrator emits, while still rejecting
42
+ * `/`, `..`, whitespace and anything else that could escape the run dir when
43
+ * concatenated into an attempt sidecar path.
44
+ */
45
+ const SEGMENT_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
46
+ export function isValidPathSegment(s) {
47
+ return SEGMENT_RE.test(s);
48
+ }
49
+ /**
50
+ * Path-traversal guard — returns true iff `child`, after `..`/`.` resolution,
51
+ * still lives inside `parent`. Exported so dashboard surfaces that build
52
+ * paths from caller-supplied ids (e.g. attempt terminal-log raw endpoint)
53
+ * can apply the same defense-in-depth check on top of `isValidRunId` /
54
+ * `isValidPathSegment`.
55
+ */
56
+ export function isPathInsideDir(parent, child) {
57
+ return isPathInside(parent, child);
58
+ }
59
+ /**
60
+ * Resolve the on-disk `terminal.log` path for a given attempt sidecar.
61
+ * Production callers MUST validate runId / activityId / attemptId with
62
+ * `isValidRunId` + `isValidPathSegment` first, and re-check `isPathInsideDir`
63
+ * after joining to defend against any future segment-regex relaxation.
64
+ */
65
+ export function attemptTerminalLogPath(runsDir, runId, activityId, attemptId) {
66
+ return join(runsDir, runId, 'attempts', activityId, attemptId, 'terminal.log');
67
+ }
68
+ /**
69
+ * Resolve the on-disk raw `pty.log` path for a given attempt sidecar.
70
+ * Same validation contract as `attemptTerminalLogPath` — callers MUST
71
+ * pre-validate ids and re-check `isPathInsideDir` after joining.
72
+ */
73
+ export function attemptPtyLogPath(runsDir, runId, activityId, attemptId) {
74
+ return join(runsDir, runId, 'attempts', activityId, attemptId, 'pty.log');
75
+ }
76
+ /**
77
+ * Project every run in `runsDir` to a row. Most-recently-updated first.
78
+ *
79
+ * - ENOENT on `runsDir` → `[]` (nothing to list).
80
+ * - Non-directory entries / unreadable / corrupt event logs → skipped.
81
+ * - Filter precedence: explicit `statuses` (any) > `all` (terminal kept) >
82
+ * default (terminal hidden).
83
+ */
84
+ export async function listRuns(runsDir, opts = {}) {
85
+ let entries;
86
+ try {
87
+ entries = await fs.readdir(runsDir, { withFileTypes: true });
88
+ }
89
+ catch (err) {
90
+ if (err.code === 'ENOENT')
91
+ return [];
92
+ throw err;
93
+ }
94
+ const wantStatuses = opts.statuses;
95
+ const all = !!opts.all;
96
+ const rows = [];
97
+ for (const entry of entries) {
98
+ if (!entry.isDirectory())
99
+ continue;
100
+ const runId = entry.name;
101
+ if (!isValidRunId(runId))
102
+ continue;
103
+ const events = await readRunEventsPure(join(runsDir, runId));
104
+ if (!events || events.length === 0)
105
+ continue;
106
+ let snap;
107
+ try {
108
+ snap = replay(events);
109
+ }
110
+ catch {
111
+ continue;
112
+ }
113
+ const status = snap.run.status;
114
+ if (wantStatuses) {
115
+ if (!wantStatuses.has(status))
116
+ continue;
117
+ }
118
+ else if (!all && TERMINAL_RUN_STATUSES.has(status)) {
119
+ continue;
120
+ }
121
+ const row = projectRunRow(runId, events, snap);
122
+ if (opts.includeBinding) {
123
+ const binding = await readChatBindingPure(join(runsDir, runId));
124
+ if (binding) {
125
+ row.chatId = binding.chatId;
126
+ row.larkAppId = binding.larkAppId;
127
+ }
128
+ }
129
+ rows.push(row);
130
+ }
131
+ rows.sort((a, b) => b.updatedAt - a.updatedAt);
132
+ return rows;
133
+ }
134
+ export function projectRunRow(runId, events, snap) {
135
+ // dAct = non-wait, non-effect dangling activities (worker-style bucket;
136
+ // gates show up in dWait, effects in dEf).
137
+ const effectSet = new Set(snap.danglingEffectAttempted);
138
+ const waitSet = new Set(snap.danglingWaits);
139
+ const dAct = snap.danglingActivities.filter((a) => !effectSet.has(a) && !waitSet.has(a)).length;
140
+ const error = snap.run.status === 'failed' || snap.run.status === 'cancelled'
141
+ ? findRunError(snap)
142
+ : undefined;
143
+ return {
144
+ runId,
145
+ workflowId: snap.run.workflowId ?? '?',
146
+ status: snap.run.status,
147
+ lastSeq: snap.lastSeq,
148
+ dEf: snap.danglingEffectAttempted.length,
149
+ dAct,
150
+ dWait: snap.danglingWaits.length,
151
+ updatedAt: events[events.length - 1].timestamp,
152
+ failedNodeId: snap.run.failedNodeId,
153
+ errorCode: error?.errorCode,
154
+ errorClass: error?.errorClass,
155
+ errorMessage: error?.errorMessage,
156
+ };
157
+ }
158
+ function findRunError(snap) {
159
+ const activities = [...snap.activities.values()];
160
+ const preferredActivities = snap.run.failedNodeId
161
+ ? activities.filter((activity) => activity.ownerNodeId === snap.run.failedNodeId)
162
+ : [];
163
+ const fallbackActivities = activities.filter((activity) => !preferredActivities.includes(activity));
164
+ for (const activity of [...preferredActivities, ...fallbackActivities]) {
165
+ for (const attempt of [...activity.attempts].reverse()) {
166
+ if (attempt.error)
167
+ return attempt.error;
168
+ }
169
+ }
170
+ return undefined;
171
+ }
172
+ const BLOB_PREVIEW_MAX_BYTES = 64 * 1024;
173
+ /**
174
+ * Build a JSON-serializable snapshot for a single run. Returns null when
175
+ * the run is missing / has no events / has a corrupt log. Callers
176
+ * (dashboard `/snapshot` endpoint) should map null → 404.
177
+ */
178
+ export async function readRunSnapshot(runsDir, runId) {
179
+ if (!isValidRunId(runId))
180
+ return null;
181
+ const runDir = join(runsDir, runId);
182
+ const events = await readRunEventsPure(runDir);
183
+ if (!events || events.length === 0)
184
+ return null;
185
+ let snap;
186
+ try {
187
+ snap = replay(events);
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ const binding = await readChatBindingPure(runDir);
193
+ const outputs = {};
194
+ for (const [aid, ref] of snap.outputs)
195
+ outputs[aid] = ref;
196
+ const def = await readWorkflowDefinitionPure(runDir);
197
+ const attemptIO = await buildAttemptIO(runDir, snap, def);
198
+ return {
199
+ runId,
200
+ run: snap.run,
201
+ lastSeq: snap.lastSeq,
202
+ nodes: [...snap.nodes.values()],
203
+ activities: [...snap.activities.values()],
204
+ dangling: {
205
+ activities: snap.danglingActivities,
206
+ effectAttempted: snap.danglingEffectAttempted,
207
+ waits: snap.danglingWaits,
208
+ cancels: snap.danglingCancels,
209
+ },
210
+ outputs,
211
+ attemptIO,
212
+ chatBinding: binding ?? undefined,
213
+ updatedAt: events[events.length - 1].timestamp,
214
+ };
215
+ }
216
+ async function buildAttemptIO(runDir, snap, def) {
217
+ const out = {};
218
+ const cache = new Map();
219
+ for (const activity of snap.activities.values()) {
220
+ for (const attempt of activity.attempts) {
221
+ const io = {};
222
+ io.input = await previewRef(runDir, attempt.inputRef, cache);
223
+ if (attempt.output) {
224
+ io.output = await previewRef(runDir, attempt.output, cache);
225
+ }
226
+ io.log = await previewAttemptLog(runDir, activity.activityId, attempt.attemptId);
227
+ io.terminal = await readAttemptTerminal(runDir, activity.activityId, attempt.attemptId);
228
+ if (io.input?.value !== undefined && def) {
229
+ io.resolvedInput = await previewResolvedInput(runDir, snap, def, io.input.value, cache);
230
+ }
231
+ if (attempt.wait?.promptRef) {
232
+ io.waitPrompt = await previewRef(runDir, attempt.wait.promptRef, cache);
233
+ }
234
+ out[attempt.attemptId] = io;
235
+ }
236
+ }
237
+ return out;
238
+ }
239
+ async function readAttemptTerminal(runDir, activityId, attemptId) {
240
+ const path = attemptTerminalSidecarPath(runDir, activityId, attemptId);
241
+ if (!isPathInside(runDir, path)) {
242
+ return { sessionId: '', webPort: 0, status: 'closed', startedAt: 0, updatedAt: 0, error: 'terminal sidecar is outside run directory' };
243
+ }
244
+ try {
245
+ const raw = await fs.readFile(path, 'utf-8');
246
+ const parsed = JSON.parse(raw);
247
+ // Schema bumps need an explicit migration story; reject unknown shapes for now.
248
+ if (parsed.schemaVersion !== 1 ||
249
+ typeof parsed.sessionId !== 'string' ||
250
+ typeof parsed.webPort !== 'number' ||
251
+ (parsed.status !== 'live' && parsed.status !== 'closed') ||
252
+ typeof parsed.startedAt !== 'number' ||
253
+ typeof parsed.updatedAt !== 'number') {
254
+ return { sessionId: '', webPort: 0, status: 'closed', startedAt: 0, updatedAt: 0, error: 'invalid terminal sidecar' };
255
+ }
256
+ const ptyLog = join(dirname(path), 'pty.log');
257
+ let hasPtyLog = false;
258
+ try {
259
+ const st = await fs.stat(ptyLog);
260
+ hasPtyLog = st.isFile() && st.size > 0;
261
+ }
262
+ catch { /* ENOENT or other — treat as absent */ }
263
+ return {
264
+ sessionId: parsed.sessionId,
265
+ cliSessionId: typeof parsed.cliSessionId === 'string' ? parsed.cliSessionId : undefined,
266
+ webPort: parsed.webPort,
267
+ status: parsed.status,
268
+ larkAppId: parsed.larkAppId,
269
+ botName: parsed.botName,
270
+ cliId: parsed.cliId,
271
+ workingDir: parsed.workingDir,
272
+ logPath: parsed.logPath,
273
+ startedAt: parsed.startedAt,
274
+ updatedAt: parsed.updatedAt,
275
+ closedAt: parsed.closedAt,
276
+ hasPtyLog,
277
+ };
278
+ }
279
+ catch (err) {
280
+ if (err.code === 'ENOENT')
281
+ return undefined;
282
+ return {
283
+ sessionId: '',
284
+ webPort: 0,
285
+ status: 'closed',
286
+ startedAt: 0,
287
+ updatedAt: 0,
288
+ error: err instanceof Error ? err.message : String(err),
289
+ };
290
+ }
291
+ }
292
+ async function previewAttemptLog(runDir, activityId, attemptId) {
293
+ const logPath = join(runDir, 'attempts', activityId, attemptId, 'terminal.log');
294
+ if (!isPathInside(runDir, logPath)) {
295
+ return { contentType: 'text/plain', error: 'attempt log is outside run directory' };
296
+ }
297
+ try {
298
+ const handle = await fs.open(logPath, 'r');
299
+ try {
300
+ const stat = await handle.stat();
301
+ const bytesToRead = Math.min(stat.size, BLOB_PREVIEW_MAX_BYTES);
302
+ const start = Math.max(0, stat.size - bytesToRead);
303
+ const buf = Buffer.alloc(bytesToRead);
304
+ await handle.read(buf, 0, bytesToRead, start);
305
+ return {
306
+ contentType: 'text/plain',
307
+ outputBytes: stat.size,
308
+ truncated: stat.size > BLOB_PREVIEW_MAX_BYTES,
309
+ text: buf.toString('utf-8'),
310
+ };
311
+ }
312
+ finally {
313
+ await handle.close();
314
+ }
315
+ }
316
+ catch (err) {
317
+ if (err.code === 'ENOENT')
318
+ return undefined;
319
+ return {
320
+ contentType: 'text/plain',
321
+ error: err instanceof Error ? err.message : String(err),
322
+ };
323
+ }
324
+ }
325
+ async function previewRef(runDir, ref, cache) {
326
+ const key = ref.outputHash;
327
+ const cached = cache.get(key);
328
+ if (cached)
329
+ return cached;
330
+ const base = {
331
+ outputHash: ref.outputHash,
332
+ outputBytes: ref.outputBytes,
333
+ contentType: ref.contentType,
334
+ };
335
+ if (!ref.outputPath) {
336
+ const res = { ...base, error: 'outputRef has no outputPath' };
337
+ cache.set(key, res);
338
+ return res;
339
+ }
340
+ if (!isPathInside(runDir, ref.outputPath)) {
341
+ const res = { ...base, error: 'outputPath is outside run directory' };
342
+ cache.set(key, res);
343
+ return res;
344
+ }
345
+ try {
346
+ const handle = await fs.open(ref.outputPath, 'r');
347
+ try {
348
+ const stat = await handle.stat();
349
+ const bytesToRead = Math.min(stat.size, BLOB_PREVIEW_MAX_BYTES);
350
+ const buf = Buffer.alloc(bytesToRead);
351
+ await handle.read(buf, 0, bytesToRead, 0);
352
+ const text = buf.toString('utf-8');
353
+ const truncated = stat.size > BLOB_PREVIEW_MAX_BYTES;
354
+ const res = {
355
+ ...base,
356
+ outputBytes: stat.size,
357
+ truncated,
358
+ };
359
+ if (!truncated && isJsonContent(ref.contentType)) {
360
+ try {
361
+ res.value = JSON.parse(text);
362
+ }
363
+ catch (err) {
364
+ res.text = text;
365
+ res.error = `invalid JSON: ${err instanceof Error ? err.message : String(err)}`;
366
+ }
367
+ }
368
+ else {
369
+ res.text = text;
370
+ }
371
+ cache.set(key, res);
372
+ return res;
373
+ }
374
+ finally {
375
+ await handle.close();
376
+ }
377
+ }
378
+ catch (err) {
379
+ const res = {
380
+ ...base,
381
+ error: err instanceof Error ? err.message : String(err),
382
+ };
383
+ cache.set(key, res);
384
+ return res;
385
+ }
386
+ }
387
+ function isPathInside(parent, child) {
388
+ const rel = relative(resolve(parent), resolve(child));
389
+ return rel === '' || (!!rel && !rel.startsWith('..') && !rel.startsWith(sep));
390
+ }
391
+ function isJsonContent(contentType) {
392
+ return (contentType ?? '').toLowerCase().includes('json');
393
+ }
394
+ async function readWorkflowDefinitionPure(runDir) {
395
+ try {
396
+ const raw = await fs.readFile(join(runDir, 'workflow.json'), 'utf-8');
397
+ return parseWorkflowDefinition(JSON.parse(raw));
398
+ }
399
+ catch {
400
+ return null;
401
+ }
402
+ }
403
+ async function previewResolvedInput(runDir, snap, def, rawInput, cache) {
404
+ try {
405
+ const value = await resolveDashboardBindings(rawInput, { runDir, snap, def, cache });
406
+ return {
407
+ contentType: 'application/json',
408
+ value,
409
+ outputBytes: Buffer.byteLength(JSON.stringify(value), 'utf-8'),
410
+ };
411
+ }
412
+ catch (err) {
413
+ return {
414
+ contentType: 'application/json',
415
+ value: rawInput,
416
+ error: `failed to resolve bindings: ${err instanceof Error ? err.message : String(err)}`,
417
+ };
418
+ }
419
+ }
420
+ async function resolveDashboardBindings(value, ctx) {
421
+ if (isRefSpec(value))
422
+ return resolveDashboardRef(value.$ref, ctx);
423
+ if (typeof value === 'string')
424
+ return interpolateDashboardStringRefs(value, ctx);
425
+ if (Array.isArray(value)) {
426
+ const out = [];
427
+ for (const item of value)
428
+ out.push(await resolveDashboardBindings(item, ctx));
429
+ return out;
430
+ }
431
+ if (value !== null && typeof value === 'object') {
432
+ const out = {};
433
+ for (const [key, item] of Object.entries(value)) {
434
+ out[key] = await resolveDashboardBindings(item, ctx);
435
+ }
436
+ return out;
437
+ }
438
+ return value;
439
+ }
440
+ async function interpolateDashboardStringRefs(value, ctx) {
441
+ if (!value.includes('${'))
442
+ return value;
443
+ let out = '';
444
+ let cursor = 0;
445
+ while (cursor < value.length) {
446
+ const start = value.indexOf('${', cursor);
447
+ if (start < 0) {
448
+ out += value.slice(cursor);
449
+ break;
450
+ }
451
+ out += value.slice(cursor, start);
452
+ const end = value.indexOf('}', start + 2);
453
+ if (end < 0)
454
+ throw new Error(`unterminated string ref interpolation in '${value}'`);
455
+ const ref = value.slice(start + 2, end);
456
+ if (!ref)
457
+ throw new Error(`empty string ref interpolation in '${value}'`);
458
+ out += stringifyDashboardInterpolatedValue(ref, await resolveDashboardRef(ref, ctx));
459
+ cursor = end + 1;
460
+ }
461
+ return out;
462
+ }
463
+ function stringifyDashboardInterpolatedValue(ref, value) {
464
+ if (value === null)
465
+ return 'null';
466
+ const t = typeof value;
467
+ if (t === 'string')
468
+ return value;
469
+ if (t === 'number' || t === 'boolean')
470
+ return String(value);
471
+ throw new Error(`string interpolation '\${${ref}}' resolved to ${Array.isArray(value) ? 'array' : t} ` +
472
+ `(expected string/number/boolean/null; use whole-field $ref for structured values)`);
473
+ }
474
+ function isRefSpec(value) {
475
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
476
+ return false;
477
+ const entries = Object.entries(value);
478
+ return entries.length === 1 && entries[0]?.[0] === '$ref' && typeof entries[0]?.[1] === 'string';
479
+ }
480
+ async function resolveDashboardRef(ref, ctx) {
481
+ if (ref.startsWith('params.')) {
482
+ const inputRef = ctx.snap.run.input;
483
+ if (!inputRef)
484
+ throw new Error(`$ref '${ref}' requires run input`);
485
+ const params = (await previewRef(ctx.runDir, inputRef, ctx.cache)).value;
486
+ return walkPreviewPath(params, ref.slice('params.'.length).split('.'), ref);
487
+ }
488
+ const sepIdx = ref.indexOf('.output.');
489
+ if (sepIdx < 0) {
490
+ throw new Error(`$ref '${ref}' missing '.output.' separator`);
491
+ }
492
+ const nodeId = ref.slice(0, sepIdx);
493
+ const path = ref.slice(sepIdx + '.output.'.length).split('.');
494
+ const node = ctx.def.nodes[nodeId];
495
+ if (!node)
496
+ throw new Error(`$ref '${ref}' targets unknown node '${nodeId}'`);
497
+ const outputRef = ctx.snap.outputs.get(workActivityId(ctx.snap.run.runId, nodeId));
498
+ if (!outputRef)
499
+ throw new Error(`$ref '${ref}' has no successful output yet`);
500
+ const preview = await previewRef(ctx.runDir, outputRef, ctx.cache);
501
+ if (preview.value === undefined) {
502
+ throw new Error(preview.error ?? `$ref '${ref}' output preview has no JSON value`);
503
+ }
504
+ const root = node.type === 'hostExecutor' &&
505
+ preview.value !== null &&
506
+ typeof preview.value === 'object' &&
507
+ Object.prototype.hasOwnProperty.call(preview.value, 'output')
508
+ ? preview.value.output
509
+ : preview.value;
510
+ return walkPreviewPath(root, path, ref);
511
+ }
512
+ function walkPreviewPath(value, segments, ref) {
513
+ let cursor = value;
514
+ for (const seg of segments) {
515
+ if (cursor === null || cursor === undefined) {
516
+ throw new Error(`$ref '${ref}' hit ${cursor === null ? 'null' : 'undefined'} at '${seg}'`);
517
+ }
518
+ if (Array.isArray(cursor)) {
519
+ const idx = Number(seg);
520
+ if (!Number.isInteger(idx) || idx < 0 || idx >= cursor.length) {
521
+ throw new Error(`$ref '${ref}' array index '${seg}' out of bounds`);
522
+ }
523
+ cursor = cursor[idx];
524
+ continue;
525
+ }
526
+ if (typeof cursor !== 'object' || !Object.prototype.hasOwnProperty.call(cursor, seg)) {
527
+ throw new Error(`$ref '${ref}' segment '${seg}' not found`);
528
+ }
529
+ cursor = cursor[seg];
530
+ }
531
+ return cursor;
532
+ }
533
+ const DEFAULT_LIMIT = 200;
534
+ const MAX_LIMIT = 1000;
535
+ const DEFAULT_TAIL = 100;
536
+ /**
537
+ * Slice a run's event log into a paginated window.
538
+ *
539
+ * Mode precedence: `afterSeq` > `beforeSeq` > `tail` (default). This
540
+ * matches the dashboard usage: detail page first loads `?tail=100`,
541
+ * then polls `?afterSeq=<newest>` and back-scrolls `?beforeSeq=<oldest>`.
542
+ *
543
+ * Pagination bookkeeping (`hasOlder` / `hasNewer`) is computed from the
544
+ * full event list and the returned slice's bounds. Returns null if the
545
+ * runId is invalid or the run is missing.
546
+ */
547
+ export async function readEventWindow(runsDir, runId, opts = {}) {
548
+ if (!isValidRunId(runId))
549
+ return null;
550
+ const events = await readRunEventsPure(join(runsDir, runId));
551
+ if (!events)
552
+ return null;
553
+ const total = events.length;
554
+ if (total === 0) {
555
+ return {
556
+ events: [],
557
+ oldestSeq: null,
558
+ newestSeq: null,
559
+ totalCount: 0,
560
+ hasOlder: false,
561
+ hasNewer: false,
562
+ };
563
+ }
564
+ const limit = clampLimit(opts.limit);
565
+ if (opts.afterSeq !== undefined && Number.isFinite(opts.afterSeq)) {
566
+ const after = opts.afterSeq;
567
+ const idx = events.findIndex((e) => eventSeqFromId(e.eventId) > after);
568
+ if (idx < 0) {
569
+ return {
570
+ events: [],
571
+ oldestSeq: null,
572
+ newestSeq: null,
573
+ totalCount: total,
574
+ hasOlder: true,
575
+ hasNewer: false,
576
+ };
577
+ }
578
+ const slice = events.slice(idx, idx + limit);
579
+ return {
580
+ events: slice,
581
+ oldestSeq: eventSeqFromId(slice[0].eventId),
582
+ newestSeq: eventSeqFromId(slice[slice.length - 1].eventId),
583
+ totalCount: total,
584
+ hasOlder: idx > 0,
585
+ hasNewer: idx + slice.length < total,
586
+ };
587
+ }
588
+ if (opts.beforeSeq !== undefined && Number.isFinite(opts.beforeSeq)) {
589
+ const before = opts.beforeSeq;
590
+ const endIdx = events.findIndex((e) => eventSeqFromId(e.eventId) >= before);
591
+ const exclusiveEnd = endIdx < 0 ? total : endIdx;
592
+ const startIdx = Math.max(0, exclusiveEnd - limit);
593
+ const slice = events.slice(startIdx, exclusiveEnd);
594
+ if (slice.length === 0) {
595
+ return {
596
+ events: [],
597
+ oldestSeq: null,
598
+ newestSeq: null,
599
+ totalCount: total,
600
+ hasOlder: false,
601
+ hasNewer: true,
602
+ };
603
+ }
604
+ return {
605
+ events: slice,
606
+ oldestSeq: eventSeqFromId(slice[0].eventId),
607
+ newestSeq: eventSeqFromId(slice[slice.length - 1].eventId),
608
+ totalCount: total,
609
+ hasOlder: startIdx > 0,
610
+ hasNewer: exclusiveEnd < total,
611
+ };
612
+ }
613
+ const tail = opts.tail !== undefined && Number.isFinite(opts.tail) && opts.tail > 0
614
+ ? Math.min(Math.floor(opts.tail), MAX_LIMIT)
615
+ : DEFAULT_TAIL;
616
+ const startIdx = Math.max(0, total - tail);
617
+ const slice = events.slice(startIdx);
618
+ return {
619
+ events: slice,
620
+ oldestSeq: eventSeqFromId(slice[0].eventId),
621
+ newestSeq: eventSeqFromId(slice[slice.length - 1].eventId),
622
+ totalCount: total,
623
+ hasOlder: startIdx > 0,
624
+ hasNewer: false,
625
+ };
626
+ }
627
+ function clampLimit(raw) {
628
+ if (raw === undefined || !Number.isFinite(raw) || raw <= 0)
629
+ return DEFAULT_LIMIT;
630
+ return Math.min(Math.floor(raw), MAX_LIMIT);
631
+ }
632
+ // ─── pure readers (no mkdir side effects) ──────────────────────────────────
633
+ async function readRunEventsPure(runDir) {
634
+ const file = join(runDir, 'events.ndjson');
635
+ let raw;
636
+ try {
637
+ raw = await fs.readFile(file, 'utf-8');
638
+ }
639
+ catch (err) {
640
+ if (err.code === 'ENOENT')
641
+ return null;
642
+ throw err;
643
+ }
644
+ const events = [];
645
+ for (const line of raw.split('\n')) {
646
+ if (!line)
647
+ continue;
648
+ let obj;
649
+ try {
650
+ obj = JSON.parse(line);
651
+ }
652
+ catch {
653
+ return null;
654
+ }
655
+ try {
656
+ events.push(parseEvent(obj));
657
+ }
658
+ catch {
659
+ return null;
660
+ }
661
+ }
662
+ return events;
663
+ }
664
+ async function readChatBindingPure(runDir) {
665
+ try {
666
+ const raw = await fs.readFile(join(runDir, 'chat-binding.json'), 'utf-8');
667
+ const parsed = JSON.parse(raw);
668
+ if (!parsed.chatId || !parsed.larkAppId)
669
+ return null;
670
+ return { chatId: parsed.chatId, larkAppId: parsed.larkAppId };
671
+ }
672
+ catch {
673
+ return null;
674
+ }
675
+ }
676
+ // ─── event helpers ─────────────────────────────────────────────────────────
677
+ /**
678
+ * Extract `<seq>` from a WorkflowEvent `eventId` of the form
679
+ * `<runId>-<seq>` (events doc v0.1.2 §3.1). Returns 0 for malformed
680
+ * ids; callers should treat that as "unknown" rather than position 0.
681
+ */
682
+ export function eventSeqFromId(eventId) {
683
+ const dash = eventId.lastIndexOf('-');
684
+ if (dash < 0)
685
+ return 0;
686
+ const n = Number(eventId.slice(dash + 1));
687
+ return Number.isFinite(n) ? n : 0;
688
+ }
689
+ export function extractEventContext(payload) {
690
+ if (!payload || typeof payload !== 'object' || 'ref' in payload) {
691
+ return {};
692
+ }
693
+ const p = payload;
694
+ const out = {};
695
+ if (typeof p.nodeId === 'string')
696
+ out.nodeId = p.nodeId;
697
+ if (typeof p.activityId === 'string')
698
+ out.activityId = p.activityId;
699
+ if (typeof p.failedNodeId === 'string')
700
+ out.nodeId = p.failedNodeId;
701
+ const err = p.error;
702
+ if (err && typeof err === 'object' && 'errorCode' in err) {
703
+ out.errorCode = String(err.errorCode);
704
+ }
705
+ return out;
706
+ }
707
+ //# sourceMappingURL=ops-projection.js.map