@cohortapp/agent-sdk 2.16.0 → 2.18.4

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 (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +29 -2
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -0,0 +1,299 @@
1
+ /**
2
+ * lib/engine/session/store.mjs — JSONL session transcripts.
3
+ *
4
+ * One file per session: `<dir>/<sessionId>.jsonl`, keyed by a CALLER-SUPPLIED
5
+ * id (maestro's dispatcher chooses the id so it can resume a thread later).
6
+ * Append-only; each line is one record:
7
+ *
8
+ * { type:'session', version:1, sessionId, ts, cwd, model, wire } first line
9
+ * { type:'resume', sessionId, ts } each resume
10
+ * { type:'message', sessionId, ts, message } conversation
11
+ * { type:'result', sessionId, ts, result } per run summary
12
+ *
13
+ * Appends go through `appendJsonl` (one write per line), so a crash can at
14
+ * worst leave a torn LAST line. Parsing skips unparseable lines and counts
15
+ * them rather than refusing the session — losing one half-written message is
16
+ * recoverable; losing a thread is not. A tool call left unanswered by a crash
17
+ * is closed by `normaliseHistory` when the history is next sent.
18
+ *
19
+ * Parsing and reconstruction are pure; `openSession` / `append*` are the edge.
20
+ *
21
+ * One writer per session (W4 integration, conformance row 4): a run holds
22
+ * `<dir>/<sessionId>.lock` for its whole life (`acquireSessionLock`). The lock
23
+ * names the holder's pid and its process start time; a lock whose holder is no
24
+ * longer alive — or whose pid now belongs to a process that started at another
25
+ * time (pid reuse after a crash, W4-E1) — is stale and is taken over, so a
26
+ * crashed run never strands its thread. A live holder
27
+ * refuses a second writer (`session_locked`), whether it continues by
28
+ * `--session-id` or by `--resume`.
29
+ *
30
+ * @module lib/engine/session/store
31
+ */
32
+
33
+ import { closeSync, linkSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
34
+ import { randomUUID } from "node:crypto";
35
+ import path from "node:path";
36
+ import { appendJsonl } from "../../fs-atomic.mjs";
37
+ import { processAlive, processStartToken, sameStartTime } from "../process-identity.mjs";
38
+
39
+ export const TRANSCRIPT_VERSION = 1;
40
+
41
+ /** Ids become file names: no separators, no dot-dot, bounded length. */
42
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
43
+
44
+ /** @param {unknown} id @returns {id is string} */
45
+ export function isValidSessionId(id) {
46
+ return typeof id === "string" && ID_RE.test(id) && !id.includes("..");
47
+ }
48
+
49
+ /** @param {string} dir @param {string} id */
50
+ export function sessionPath(dir, id) {
51
+ return path.join(dir, `${id}.jsonl`);
52
+ }
53
+
54
+ /**
55
+ * @param {string} text
56
+ * @returns {{ records: any[], skipped: number }}
57
+ */
58
+ export function parseTranscript(text) {
59
+ const records = [];
60
+ let skipped = 0;
61
+ for (const line of text.split("\n")) {
62
+ if (line.trim() === "") continue;
63
+ try {
64
+ const r = JSON.parse(line);
65
+ if (r && typeof r === "object" && typeof r.type === "string") records.push(r);
66
+ else skipped++;
67
+ } catch {
68
+ skipped++; // a torn line from a crashed append; the rest of the thread is intact
69
+ }
70
+ }
71
+ return { records, skipped };
72
+ }
73
+
74
+ /** @param {any[]} records */
75
+ export function messagesFromRecords(records) {
76
+ return records
77
+ .filter((r) => r.type === "message" && r.message && (r.message.role === "user" || r.message.role === "assistant"))
78
+ .map((r) => r.message);
79
+ }
80
+
81
+ /**
82
+ * @typedef {{ id:string, path:string, messages:any[], skipped:number, writeFailures:number }} Session
83
+ * @typedef {{ id:string, path:string, messages:any[], skipped:number, writeFailures:number, continued?:boolean }} OpenedSession
84
+ * @typedef {{ code:'invalid_session_id'|'session_exists'|'session_not_found'|'session_unreadable'|'session_unwritable'|'session_locked', message:string }} SessionError
85
+ */
86
+
87
+ /**
88
+ * Open a session for a run.
89
+ *
90
+ * new claim a fresh id; an existing one is `session_exists`
91
+ * resume continue an existing id; a missing one is `session_not_found`
92
+ * continue claim the id, or continue it when it exists — what a caller-chosen
93
+ * `--session-id` reused across turns means (maestro's dispatcher
94
+ * crash recovery and responder continuation re-spawn the same id
95
+ * with a new prompt). The caller holds the session lock first.
96
+ *
97
+ * @param {object} p
98
+ * @param {string} p.dir
99
+ * @param {string} p.sessionId
100
+ * @param {'new'|'resume'|'continue'} p.mode
101
+ * @param {{cwd:string, model:string, wire:string}} p.meta
102
+ * @param {() => number} [p.now]
103
+ * @returns {{ ok:true, session: Session } | { ok:false, error: SessionError }}
104
+ */
105
+ export function openSession({ dir, sessionId, mode, meta, now = Date.now }) {
106
+ if (!isValidSessionId(sessionId)) {
107
+ return { ok: false, error: { code: "invalid_session_id", message: `invalid session id "${sessionId}"` } };
108
+ }
109
+ const file = sessionPath(dir, sessionId);
110
+ const ts = new Date(now()).toISOString();
111
+ if (mode === "new" || mode === "continue") {
112
+ // Claim the id with an exclusive create (O_EXCL), not exists-then-append:
113
+ // two runs started with the same --session-id would both pass an existence
114
+ // check and interleave one transcript. The claim writes no bytes; the
115
+ // header goes through the append seam like every other record.
116
+ try {
117
+ mkdirSync(dir, { recursive: true });
118
+ closeSync(openSync(file, "wx"));
119
+ } catch (e) {
120
+ if (/** @type any */ (e)?.code === "EEXIST") {
121
+ if (mode === "continue") return resumeSession({ file, dir, sessionId, ts, meta, continued: true });
122
+ return { ok: false, error: { code: "session_exists", message: `session ${sessionId} already exists; use --resume` } };
123
+ }
124
+ return { ok: false, error: { code: "session_unwritable", message: `cannot create ${file}: ${e instanceof Error ? e.message : e}` } };
125
+ }
126
+ const header = { type: "session", version: TRANSCRIPT_VERSION, sessionId, ts, ...meta };
127
+ if (!appendJsonl(file, header)) {
128
+ return { ok: false, error: { code: "session_unwritable", message: `cannot write ${file}` } };
129
+ }
130
+ return { ok: true, session: { id: sessionId, path: file, messages: [], skipped: 0, writeFailures: 0 } };
131
+ }
132
+ return resumeSession({ file, dir, sessionId, ts, meta, continued: false });
133
+ }
134
+
135
+ /**
136
+ * @param {{file:string, dir:string, sessionId:string, ts:string, meta:{cwd:string, model:string, wire:string}, continued:boolean}} p
137
+ * @returns {{ ok:true, session: OpenedSession } | { ok:false, error: SessionError }}
138
+ */
139
+ function resumeSession({ file, dir, sessionId, ts, meta, continued }) {
140
+ let text;
141
+ try {
142
+ text = readFileSync(file, "utf8");
143
+ } catch (e) {
144
+ const missing = /** @type any */ (e)?.code === "ENOENT";
145
+ return {
146
+ ok: false,
147
+ error: missing
148
+ ? { code: "session_not_found", message: `no session ${sessionId} in ${dir}` }
149
+ : { code: "session_unreadable", message: `cannot read ${file}: ${e instanceof Error ? e.message : e}` },
150
+ };
151
+ }
152
+ const { records, skipped } = parseTranscript(text);
153
+ /** @type {OpenedSession} */
154
+ const session = { id: sessionId, path: file, messages: messagesFromRecords(records), skipped, writeFailures: 0, ...(continued ? { continued: true } : {}) };
155
+ // A claim whose run died before its header was written: the continuing run writes it.
156
+ if (continued && !records.some((r) => r.type === "session")) {
157
+ if (!appendJsonl(file, { type: "session", version: TRANSCRIPT_VERSION, sessionId, ts, ...meta })) session.writeFailures++;
158
+ return { ok: true, session };
159
+ }
160
+ if (!appendJsonl(file, { type: "resume", sessionId, ts })) session.writeFailures++;
161
+ return { ok: true, session };
162
+ }
163
+
164
+ /** @param {string} dir @param {string} id */
165
+ export function sessionLockPath(dir, id) {
166
+ return path.join(dir, `${id}.lock`);
167
+ }
168
+
169
+ // The process-identity primitives (the fixed `ps` probe environment, epoch parsing with its 1 s
170
+ // tolerance, /proc on Linux) have one implementation, shared with task states and workflow
171
+ // journals: ../process-identity.mjs. The session lock's own policy (`lockHolderLive`) stays here.
172
+ /**
173
+ * Is the lock holder the process that took the lock? Pure over its inputs.
174
+ * A pid that is not alive is not the holder. A pid that is alive is the holder
175
+ * unless both start times are known, comparable, and differ — the pid was
176
+ * reused by an unrelated process after the holder died. An unknown or
177
+ * incomparable start time (an older lock, a platform with no answer) keeps the
178
+ * pid check alone: the safe direction is to refuse a second writer.
179
+ *
180
+ * Why not process-identity.mjs `judgeOwner`: that judgement treats a record from
181
+ * this PID with another nonce as a dead predecessor, which is right for a task
182
+ * state read back after a restart but wrong for a lock — a second run inside the
183
+ * same process (an in-process host, a test) is a second writer and must be
184
+ * refused. The primitives (probe, parsing, comparison) are the shared ones.
185
+ * @param {{pid:number, startedAt?:string|null}} holder
186
+ * @param {{isAlive:(pid:number)=>boolean, startTimeOf:(pid:number)=>string|null}} probe
187
+ */
188
+ export function lockHolderLive(holder, { isAlive, startTimeOf }) {
189
+ if (!isAlive(holder.pid)) return false;
190
+ const recorded = typeof holder.startedAt === "string" && holder.startedAt ? holder.startedAt : null;
191
+ if (recorded === null) return true;
192
+ const current = startTimeOf(holder.pid);
193
+ if (current === null) return true;
194
+ return sameStartTime(recorded, current) !== false;
195
+ }
196
+
197
+ /**
198
+ * Hold a session's single-writer lock for the life of a run.
199
+ *
200
+ * The lock body is written to a private temp file and linked into place
201
+ * (`link` fails when the name exists), so a reader never sees a half-written
202
+ * lock. A lock that names a pid no longer alive, or that cannot be parsed, is
203
+ * stale: it is removed only if it still has the content that was judged stale,
204
+ * then claimed again. `release` removes the lock only while it is still ours.
205
+ *
206
+ * @param {object} p
207
+ * @param {string} p.dir
208
+ * @param {string} p.sessionId
209
+ * @param {number} [p.pid]
210
+ * @param {(pid:number) => boolean} [p.isAlive]
211
+ * @param {(pid:number) => string|null} [p.processStartTime] a process's start time
212
+ * (default process-identity.mjs `processStartToken`; when only `isAlive` is injected, none: the pid check alone)
213
+ * @param {() => number} [p.now]
214
+ * @returns {{ ok:true, release:() => void, tookOver:number|null } | { ok:false, error: SessionError }}
215
+ */
216
+ export function acquireSessionLock({ dir, sessionId, pid = process.pid, isAlive = processAlive, processStartTime: startTimeOf, now = Date.now }) {
217
+ if (!isValidSessionId(sessionId)) {
218
+ return { ok: false, error: { code: "invalid_session_id", message: `invalid session id "${sessionId}"` } };
219
+ }
220
+ // Pid reuse: the lock records the holder's start time, and a holder is live only
221
+ // while the process with that pid still has it. A test that injects `isAlive`
222
+ // alone keeps a pure pid table (the real process table would answer for its fake pids).
223
+ const startOf = typeof startTimeOf === "function" ? startTimeOf : isAlive === processAlive ? processStartToken : () => null;
224
+ const file = sessionLockPath(dir, sessionId);
225
+ const body = JSON.stringify({ pid, startedAt: startOf(pid), sessionId, acquiredAt: new Date(now()).toISOString(), nonce: randomUUID() });
226
+ const unwritable = (/** @type unknown */ e) => ({ ok: /** @type {const} */ (false), error: /** @type {SessionError} */ ({ code: "session_unwritable", message: `cannot lock ${file}: ${e instanceof Error ? e.message : e}` }) });
227
+ const tmp = `${file}.${randomUUID()}.tmp`;
228
+ try {
229
+ mkdirSync(dir, { recursive: true });
230
+ writeFileSync(tmp, body, { mode: 0o600 });
231
+ } catch (e) {
232
+ return unwritable(e);
233
+ }
234
+ /** @type {number|null} */
235
+ let tookOver = null;
236
+ try {
237
+ for (let attempt = 0; attempt < 3; attempt++) {
238
+ try {
239
+ linkSync(tmp, file);
240
+ const release = () => {
241
+ try {
242
+ if (readFileSync(file, "utf8") === body) unlinkSync(file);
243
+ } catch {
244
+ /* already gone */
245
+ }
246
+ };
247
+ return { ok: true, release, tookOver };
248
+ } catch (e) {
249
+ if (/** @type any */ (e)?.code !== "EEXIST") return unwritable(e);
250
+ }
251
+ let held;
252
+ try {
253
+ held = readFileSync(file, "utf8");
254
+ } catch (e) {
255
+ if (/** @type any */ (e)?.code === "ENOENT") continue; // released between our link and read
256
+ return unwritable(e);
257
+ }
258
+ /** @type {any} */
259
+ let holder = null;
260
+ try {
261
+ holder = JSON.parse(held);
262
+ } catch {
263
+ holder = null;
264
+ }
265
+ const holderPid = holder && Number.isInteger(holder.pid) && holder.pid > 0 ? holder.pid : null;
266
+ if (holderPid !== null && lockHolderLive({ pid: holderPid, startedAt: holder.startedAt ?? null }, { isAlive, startTimeOf: startOf })) {
267
+ return { ok: false, error: { code: "session_locked", message: `session ${sessionId} is in use by a live process (pid ${holderPid}); a session has one writer at a time` } };
268
+ }
269
+ // Stale: remove it only if it is still the lock we judged.
270
+ try {
271
+ if (readFileSync(file, "utf8") === held) unlinkSync(file);
272
+ } catch {
273
+ /* gone already, or replaced: the next attempt decides */
274
+ }
275
+ tookOver = holderPid;
276
+ }
277
+ return { ok: false, error: { code: "session_locked", message: `session ${sessionId}: the lock kept changing hands; try again` } };
278
+ } finally {
279
+ try {
280
+ unlinkSync(tmp);
281
+ } catch {
282
+ /* best effort */
283
+ }
284
+ }
285
+ }
286
+
287
+ /** @param {Session} session @param {any} message @param {() => number} [now] */
288
+ export function appendMessage(session, message, now = Date.now) {
289
+ const ok = appendJsonl(session.path, { type: "message", sessionId: session.id, ts: new Date(now()).toISOString(), message });
290
+ if (!ok) session.writeFailures++;
291
+ return ok;
292
+ }
293
+
294
+ /** @param {Session} session @param {any} result @param {() => number} [now] */
295
+ export function appendResult(session, result, now = Date.now) {
296
+ const ok = appendJsonl(session.path, { type: "result", sessionId: session.id, ts: new Date(now()).toISOString(), result });
297
+ if (!ok) session.writeFailures++;
298
+ return ok;
299
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * lib/engine/session-runtime/args.mjs — the `cohort session` command line.
3
+ *
4
+ * cohort session --name <name> [--session-id <uuid> | --resume <uuid>] [--replace]
5
+ * [every `cohort run` flag except -p/--print, --output-format, --input-format,
6
+ * --include-partial-messages] ["<initial prompt>"]
7
+ *
8
+ * The session's run is `cohort run` in multi-turn stream-json mode, so the
9
+ * shared flags are validated by `parseRunArgs` itself (one parser, no drift).
10
+ * A session with neither --session-id nor --resume gets a fresh id.
11
+ *
12
+ * Pure.
13
+ *
14
+ * @module lib/engine/session-runtime/args
15
+ */
16
+
17
+ import { randomUUID } from "node:crypto";
18
+ import { parseRunArgs, RUN_FLAGS } from "../cli.mjs";
19
+ import { isValidSessionName } from "./registry.mjs";
20
+
21
+ /** Flags the session sets itself. */
22
+ export const SESSION_REFUSED_FLAGS = Object.freeze(["-p", "--print", "--output-format", "--input-format", "--include-partial-messages"]);
23
+ /** Every flag `parseSessionArgs` accepts (the adapter's drift test reads this). */
24
+ export const SESSION_FLAGS = Object.freeze([...RUN_FLAGS.filter((f) => !SESSION_REFUSED_FLAGS.includes(f)), "--name", "--replace"]);
25
+
26
+ export const SESSION_USAGE = `Usage: cohort session --name <name> [--session-id <id> | --resume <id>] [flags] ["<initial prompt>"]
27
+
28
+ A long-lived Cohort Engine session. In a terminal it is interactive: type a
29
+ message and press Enter (paste keeps line breaks; end a line with \\ to continue
30
+ it), Ctrl-C interrupts the current turn, Ctrl-D exits. It also listens on a local
31
+ control socket (see docs/engine/README.md) for messages from other sessions and
32
+ for interrupt / compact / status / shutdown. Without a terminal it runs from the
33
+ socket and the initial prompt only, printing stream-json events.
34
+
35
+ Session flags:
36
+ --name <name> The session's unique name on this machine (required)
37
+ --replace Take the name from a running session (it is shut down)
38
+ --session-id <id> Start a new transcript with this id (default: a new id)
39
+ --resume <id> Continue a transcript; a pending wakeup is armed again
40
+
41
+ Every other \`cohort run\` flag applies (model, permissions, MCP, settings, agents, …);
42
+ see \`cohort run --help\`. --max-turns and --wall-clock-ms apply to each turn.
43
+
44
+ Environment:
45
+ COHORT_SESSION_DIR Session registry and sockets (default ~/.cohort/sessions)
46
+ `;
47
+
48
+ /**
49
+ * @param {string[]} argv arguments after `session` (a leading `session` is tolerated)
50
+ * @param {{newId?:()=>string}} [o]
51
+ * @returns {{ok:true, help:true} | {ok:true, help:false, name:string, replace:boolean, sessionId:string, resume:boolean, prompt:string|null, runArgv:string[], opts:Record<string,any>} | {ok:false, error:string}}
52
+ */
53
+ export function parseSessionArgs(argv, { newId = randomUUID } = {}) {
54
+ const args = [...argv];
55
+ if (args[0] === "session") args.shift();
56
+ let name = null;
57
+ let replace = false;
58
+ let help = false;
59
+ /** @type {string[]} */
60
+ const rest = [];
61
+ for (let i = 0; i < args.length; i++) {
62
+ const a = args[i];
63
+ if (a === "--") {
64
+ rest.push(...args.slice(i));
65
+ break;
66
+ }
67
+ const eq = a.startsWith("--") ? a.indexOf("=") : -1;
68
+ const flag = eq === -1 ? a : a.slice(0, eq);
69
+ if (flag === "--name") {
70
+ const v = eq === -1 ? args[++i] : a.slice(eq + 1);
71
+ if (v === undefined) return { ok: false, error: "--name needs a value" };
72
+ name = v;
73
+ } else if (flag === "--replace") {
74
+ replace = true;
75
+ } else if (flag === "-h" || flag === "--help") {
76
+ help = true;
77
+ } else if (SESSION_REFUSED_FLAGS.includes(flag)) {
78
+ return { ok: false, error: `${flag} is not a session flag: a session always reads and writes stream-json turns` };
79
+ } else {
80
+ rest.push(a);
81
+ }
82
+ }
83
+ if (help) return { ok: true, help: true };
84
+ if (name === null) return { ok: false, error: "--name is required" };
85
+ if (!isValidSessionName(name)) return { ok: false, error: `invalid --name "${name}" (letters, digits, dot, dash, underscore; at most 64)` };
86
+ const parsed = parseRunArgs(rest);
87
+ if (!parsed.ok) return { ok: false, error: parsed.error };
88
+ const o = parsed.opts;
89
+ const resume = typeof o.resume === "string";
90
+ const sessionId = o.resume ?? o.sessionId ?? newId();
91
+ const idFlags = o.resume || o.sessionId ? [] : ["--session-id", sessionId];
92
+ const cut = rest.indexOf("--");
93
+ const head = cut === -1 ? rest : rest.slice(0, cut);
94
+ const tail = cut === -1 ? [] : rest.slice(cut);
95
+ const runArgv = [...head, ...idFlags, "--input-format", "stream-json", "--output-format", "stream-json", "--include-partial-messages", ...tail];
96
+ return { ok: true, help: false, name, replace, sessionId, resume, prompt: o.prompt ?? null, runArgv, opts: o };
97
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * lib/engine/session-runtime/host.mjs — what `cohort session` plugs into a run.
3
+ *
4
+ * cli.mjs runs one session exactly like a multi-turn stream-json run; a host
5
+ * (passed as `deps.host`) adds the front-door parts:
6
+ *
7
+ * notices the event queue the loop drains (shell exits, monitor lines,
8
+ * background subagents' reports) — and that wakes the idle inbox
9
+ * inbox the turn source (inbox.mjs): typed input, socket messages,
10
+ * wakeups, event batches; one turn at a time
11
+ * tools(ctx) ListAgents, SendMessage, ScheduleWakeup, Monitor — created
12
+ * once the session is open (the scheduler writes its transcript,
13
+ * and re-arms a pending wakeup on --resume)
14
+ * toolNames their names, so `--tools` may name them
15
+ * askPermission the terminal's y / n / always answer to a standing `ask`
16
+ * (null when headless: asks are refused as in `cohort run`)
17
+ * turnSignal() a fresh abort signal per turn; `interrupt()` aborts it
18
+ * without ending the session
19
+ *
20
+ * In a session, background subagents are not stopped when a turn ends and a
21
+ * turn does not wait for them: their report arrives as an event.
22
+ *
23
+ * @module lib/engine/session-runtime/host
24
+ */
25
+
26
+ import { readFileSync } from "node:fs";
27
+ import { appendJsonl } from "../../fs-atomic.mjs";
28
+ import { parseTranscript } from "../session/store.mjs";
29
+ import { createNotificationQueue } from "./notifications.mjs";
30
+ import { createSessionInbox } from "./inbox.mjs";
31
+ import { createWakeupScheduler, pendingWakeup, wakeupTurnText } from "./wakeup.mjs";
32
+ import { createListAgentsTool } from "../tools/list-agents.mjs";
33
+ import { createSendMessageTool } from "../tools/send-message.mjs";
34
+ import { createScheduleWakeupTool } from "../tools/schedule-wakeup.mjs";
35
+ import { createMonitorTool } from "../tools/monitor.mjs";
36
+
37
+ export const FRONT_DOOR_TOOL_NAMES = Object.freeze(["ListAgents", "SendMessage", "ScheduleWakeup", "Monitor"]);
38
+
39
+ /**
40
+ * @param {{name:string, registry:any, busy?:boolean, signal?:AbortSignal,
41
+ * ask?:((q:{toolName:string, input:unknown, reason:string, signal?:AbortSignal})=>Promise<'allow'|'deny'|'always'>)|null,
42
+ * msPerSecond?:number, now?:()=>number, batchMs?:number,
43
+ * onState?:(state:'idle'|'busy', why:any)=>void, onWakeup?:(w:{prompt:string, reason:string})=>void,
44
+ * send?:any}} o
45
+ */
46
+ export function createSessionHost({ name, registry, busy = false, signal, ask = null, msPerSecond = 1000, now = Date.now, batchMs, onState, onWakeup, send }) {
47
+ const notices = createNotificationQueue();
48
+ const inbox = createSessionInbox({ notices, busy, batchMs, onState });
49
+ /** @type {ReturnType<typeof createWakeupScheduler>|null} */
50
+ let wakeups = null;
51
+ /** @type {any} */
52
+ let shells = null;
53
+ /** @type {AbortController|null} */
54
+ let current = null;
55
+ /** @type {(() => void)|null} */
56
+ let unlink = null;
57
+ /**
58
+ * What the user allowed for the rest of the session, per asking agent and tool:
59
+ * "main:Bash", or "<subagent id>:Bash". An `a` given to the main loop does not
60
+ * also approve a background subagent's call, and the reverse.
61
+ */
62
+ const always = new Set();
63
+ const alwaysKey = (/** @type {{toolName:string, agentId?:string|null}} */ q) => `${q.agentId ?? "main"}:${q.toolName}`;
64
+
65
+ return {
66
+ notices,
67
+ inbox,
68
+ toolNames: FRONT_DOOR_TOOL_NAMES,
69
+ /**
70
+ * @param {{session:{id:string, path:string}, kit:any, checkCommand?:(command:string)=>{behavior:'allow'|'deny'|'ask', reason:string}}} ctx
71
+ */
72
+ tools({ session, kit, checkCommand }) {
73
+ shells = kit.shells;
74
+ wakeups = createWakeupScheduler({
75
+ append: (record) => appendJsonl(session.path, { ...record, sessionId: session.id }),
76
+ fire: (w, started) => {
77
+ if (inbox.state === "ended") return false;
78
+ onWakeup?.(w);
79
+ return inbox.deliver({ text: wakeupTurnText(w), kind: "wakeup", onStart: started }) !== "ended";
80
+ },
81
+ now,
82
+ msPerSecond,
83
+ });
84
+ let records = [];
85
+ try {
86
+ records = parseTranscript(readFileSync(session.path, "utf8")).records;
87
+ } catch {
88
+ /* a new transcript */
89
+ }
90
+ const pending = pendingWakeup(records);
91
+ if (pending) wakeups.arm(pending);
92
+ return [
93
+ createListAgentsTool({ selfName: name, registry, now }),
94
+ createSendMessageTool({ selfName: name, registry, ...(send ? { send } : {}) }),
95
+ createScheduleWakeupTool({ scheduler: wakeups }),
96
+ createMonitorTool({ shells: kit.shells, notices, cwd: () => kit.bash.cwd, checkCommand: checkCommand ?? null }),
97
+ ];
98
+ },
99
+ askPermission: ask
100
+ ? async (/** @type {{toolName:string, input:unknown, reason:string, signal?:AbortSignal, agentId?:string|null}} */ q) => {
101
+ if (always.has(alwaysKey(q))) return "allow";
102
+ const a = await ask(q);
103
+ if (a === "always") {
104
+ always.add(alwaysKey(q));
105
+ return "allow";
106
+ }
107
+ return a === "allow" ? "allow" : "deny";
108
+ }
109
+ : null,
110
+ /** A new signal for the turn that is starting. */
111
+ turnSignal() {
112
+ unlink?.();
113
+ const c = new AbortController();
114
+ current = c;
115
+ const onAbort = () => c.abort();
116
+ if (signal?.aborted) c.abort();
117
+ else signal?.addEventListener("abort", onAbort, { once: true });
118
+ unlink = () => signal?.removeEventListener("abort", onAbort);
119
+ return c.signal;
120
+ },
121
+ /** Stop the running turn; the session stays. @returns {boolean} whether a turn was running */
122
+ interrupt() {
123
+ if (inbox.state !== "busy" || !current || current.signal.aborted) return false;
124
+ current.abort();
125
+ return true;
126
+ },
127
+ status() {
128
+ const w = wakeups?.current() ?? null;
129
+ return {
130
+ state: inbox.state,
131
+ queued: inbox.queued,
132
+ pendingEvents: notices.size,
133
+ wakeup: w ? { dueAt: new Date(w.dueAt).toISOString(), reason: w.reason } : null,
134
+ monitors: shells ? shells.list().filter((/** @type any */ s) => s.kind === "monitor" && s.status === "running").map((/** @type any */ s) => ({ id: s.id, description: s.description })) : [],
135
+ };
136
+ },
137
+ dispose() {
138
+ wakeups?.dispose();
139
+ unlink?.();
140
+ inbox.end();
141
+ },
142
+ };
143
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * lib/engine/session-runtime/inbox.mjs — what a `cohort session` works on next.
3
+ *
4
+ * The session's run (cli.mjs, `--input-format stream-json`) reads user turns
5
+ * from `lines()`. The inbox hands it a turn only when the session is idle:
6
+ *
7
+ * idle ──(a message, a wakeup, or a batch of events)──► busy ──(the turn's result)──► idle
8
+ *
9
+ * · `deliver` — a user turn (typed, from the socket, a wakeup). Idle with
10
+ * nothing waiting: it starts now ("delivered"). Otherwise it waits its turn
11
+ * ("queued"), first in first out.
12
+ * · events (notifications.mjs) — while busy they reach the running turn at
13
+ * its next model request; while idle, events arriving within `batchMs` of
14
+ * each other are gathered into one turn. Queued user turns go first.
15
+ * · `control` — a compact request goes to the run at once; the run applies it
16
+ * at the next turn boundary.
17
+ * · `end` — no further turns; the run finishes after the current one.
18
+ *
19
+ * Nothing here calls a model: while idle the session makes no requests.
20
+ *
21
+ * @module lib/engine/session-runtime/inbox
22
+ */
23
+
24
+ /**
25
+ * @param {{notices:ReturnType<typeof import('./notifications.mjs').createNotificationQueue>, busy?:boolean, batchMs?:number,
26
+ * onState?:(state:'idle'|'busy', why:{kind:string, from?:string|null})=>void,
27
+ * setTimer?:(fn:()=>void, ms:number)=>any, clearTimer?:(t:any)=>void}} o
28
+ */
29
+ export function createSessionInbox({ notices, busy = false, batchMs = 150, onState = () => {}, setTimer = setTimeout, clearTimer = clearTimeout }) {
30
+ /** @type {'idle'|'busy'|'ended'} */
31
+ let state = busy ? "busy" : "idle";
32
+ /** @type {Array<{text:string, kind:string, from:string|null, onStart?:()=>void}>} */
33
+ const waiting = [];
34
+ /** @type {string[]} */
35
+ const out = [];
36
+ /** @type {(() => void)|null} */
37
+ let wake = null;
38
+ /** @type {any} */
39
+ let batchTimer = null;
40
+
41
+ const emit = (/** @type string */ line) => {
42
+ out.push(line);
43
+ const w = wake;
44
+ wake = null;
45
+ w?.();
46
+ };
47
+ const startTurn = (/** @type string */ text, /** @type {{kind:string, from?:string|null}} */ why, /** @type {(() => void)|undefined} */ onStart = undefined) => {
48
+ state = "busy";
49
+ emit(JSON.stringify({ type: "user", message: { role: "user", content: text } }));
50
+ onStart?.();
51
+ onState("busy", why);
52
+ };
53
+ const pump = () => {
54
+ if (state !== "idle") return;
55
+ const next = waiting.shift();
56
+ if (next) {
57
+ startTurn(next.text, { kind: next.kind, from: next.from }, next.onStart);
58
+ return;
59
+ }
60
+ if (!notices.hasPending() || batchTimer) return;
61
+ batchTimer = setTimer(() => {
62
+ batchTimer = null;
63
+ if (state !== "idle") return;
64
+ if (waiting.length > 0) return pump();
65
+ const texts = notices.drain();
66
+ if (texts.length > 0) startTurn(texts.join("\n\n"), { kind: "events" });
67
+ }, batchMs);
68
+ };
69
+ const unsubscribe = notices.onAvailable(pump);
70
+
71
+ return {
72
+ get state() {
73
+ return state;
74
+ },
75
+ get queued() {
76
+ return waiting.length;
77
+ },
78
+ /**
79
+ * @param {{text:string, kind?:string, from?:string|null, onStart?:()=>void}} m
80
+ * `onStart` runs when the turn actually starts (never, if the session ends first).
81
+ * @returns {'delivered'|'queued'|'ended'}
82
+ */
83
+ deliver({ text, kind = "user", from = null, onStart }) {
84
+ if (state === "ended") return "ended";
85
+ if (state === "idle" && waiting.length === 0) {
86
+ startTurn(text, { kind, from }, onStart);
87
+ return "delivered";
88
+ }
89
+ waiting.push({ text, kind, from, onStart });
90
+ return "queued";
91
+ },
92
+ /** A control line for the run (compact). @param {object} line */
93
+ control(line) {
94
+ if (state !== "ended") emit(JSON.stringify(line));
95
+ },
96
+ /** The run wrote a turn's result. */
97
+ turnEnded() {
98
+ if (state !== "busy") return;
99
+ state = "idle";
100
+ onState("idle", { kind: "result" });
101
+ pump();
102
+ },
103
+ end() {
104
+ if (state === "ended") return;
105
+ state = "ended";
106
+ if (batchTimer) clearTimer(batchTimer);
107
+ batchTimer = null;
108
+ unsubscribe();
109
+ const w = wake;
110
+ wake = null;
111
+ w?.();
112
+ },
113
+ /** The run's input: one NDJSON line per turn or control message, until `end`. */
114
+ async *lines() {
115
+ for (;;) {
116
+ while (out.length > 0) yield /** @type string */ (out.shift());
117
+ if (state === "ended") return;
118
+ await new Promise((resolve) => (wake = () => resolve(undefined)));
119
+ }
120
+ },
121
+ };
122
+ }