spexcode 0.6.7 → 0.7.0-next.0

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 (247) hide show
  1. package/README.md +12 -7
  2. package/node_modules/@spexcode/session-application/dist/index.d.ts +17 -0
  3. package/node_modules/@spexcode/session-application/dist/index.js +35 -0
  4. package/node_modules/@spexcode/session-application/dist/migration.d.ts +51 -0
  5. package/node_modules/@spexcode/session-application/dist/migration.js +694 -0
  6. package/node_modules/@spexcode/session-application/dist/production.d.ts +109 -0
  7. package/node_modules/@spexcode/session-application/dist/production.js +438 -0
  8. package/node_modules/@spexcode/session-application/dist/schema.d.ts +2 -0
  9. package/node_modules/@spexcode/session-application/dist/schema.js +39 -0
  10. package/node_modules/@spexcode/session-application/package.json +29 -0
  11. package/node_modules/@spexcode/session-events/dist/errors.d.ts +6 -0
  12. package/node_modules/@spexcode/session-events/dist/errors.js +11 -0
  13. package/node_modules/@spexcode/session-events/dist/index.d.ts +46 -0
  14. package/node_modules/@spexcode/session-events/dist/index.js +245 -0
  15. package/node_modules/@spexcode/session-events/dist/schema.d.ts +3 -0
  16. package/node_modules/@spexcode/session-events/dist/schema.js +44 -0
  17. package/node_modules/@spexcode/session-events/package.json +24 -0
  18. package/node_modules/@spexcode/session-protocol/dist/canonical.d.ts +36 -0
  19. package/node_modules/@spexcode/session-protocol/dist/canonical.js +139 -0
  20. package/node_modules/@spexcode/session-protocol/dist/engine.d.ts +22 -0
  21. package/node_modules/@spexcode/session-protocol/dist/engine.js +478 -0
  22. package/node_modules/@spexcode/session-protocol/dist/errors.d.ts +8 -0
  23. package/node_modules/@spexcode/session-protocol/dist/errors.js +39 -0
  24. package/node_modules/@spexcode/session-protocol/dist/index.d.ts +60 -0
  25. package/node_modules/@spexcode/session-protocol/dist/index.js +4 -0
  26. package/node_modules/@spexcode/session-protocol/dist/schema.d.ts +36 -0
  27. package/node_modules/@spexcode/session-protocol/dist/schema.js +192 -0
  28. package/node_modules/@spexcode/{session-core → session-protocol}/package.json +4 -8
  29. package/node_modules/@spexcode/session-runtime/dist/errors.d.ts +6 -0
  30. package/node_modules/@spexcode/session-runtime/dist/errors.js +11 -0
  31. package/node_modules/@spexcode/session-runtime/dist/index.d.ts +33 -0
  32. package/node_modules/@spexcode/session-runtime/dist/index.js +193 -0
  33. package/node_modules/@spexcode/session-runtime/dist/schema.d.ts +2 -0
  34. package/node_modules/@spexcode/session-runtime/dist/schema.js +37 -0
  35. package/node_modules/@spexcode/session-runtime/package.json +24 -0
  36. package/node_modules/@spexcode/session-selflaunch/bin/spex-session.mjs +4 -0
  37. package/node_modules/@spexcode/session-selflaunch/dist/cli.d.ts +25 -0
  38. package/node_modules/@spexcode/session-selflaunch/dist/cli.js +156 -0
  39. package/node_modules/@spexcode/session-selflaunch/dist/index.d.ts +28 -0
  40. package/node_modules/@spexcode/session-selflaunch/dist/index.js +25 -0
  41. package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +31 -0
  42. package/node_modules/@spexcode/session-selflaunch/dist/locality.js +135 -0
  43. package/node_modules/@spexcode/session-selflaunch/dist/path.d.ts +13 -0
  44. package/node_modules/@spexcode/session-selflaunch/dist/path.js +45 -0
  45. package/node_modules/@spexcode/session-selflaunch/package.json +35 -0
  46. package/node_modules/@spexcode/session-topology/dist/errors.d.ts +6 -0
  47. package/node_modules/@spexcode/session-topology/dist/errors.js +11 -0
  48. package/node_modules/@spexcode/session-topology/dist/index.d.ts +24 -0
  49. package/node_modules/@spexcode/session-topology/dist/index.js +173 -0
  50. package/node_modules/@spexcode/session-topology/dist/schema.d.ts +3 -0
  51. package/node_modules/@spexcode/session-topology/dist/schema.js +27 -0
  52. package/node_modules/@spexcode/session-topology/package.json +24 -0
  53. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +175 -21
  54. package/node_modules/@spexcode/spec-cli/dist/cli.js +121 -59
  55. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +1 -3
  56. package/node_modules/@spexcode/spec-cli/dist/client.js +49 -30
  57. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +11 -0
  58. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +46 -9
  59. package/node_modules/@spexcode/spec-cli/dist/delivery-lock.d.ts +2 -0
  60. package/node_modules/@spexcode/spec-cli/dist/delivery-lock.js +58 -0
  61. package/node_modules/@spexcode/spec-cli/dist/doctor.js +53 -11
  62. package/node_modules/@spexcode/spec-cli/dist/file-write.js +14 -2
  63. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +2 -1
  64. package/node_modules/@spexcode/spec-cli/dist/gateway.js +6 -3
  65. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +32 -2
  66. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +57 -2
  67. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +2 -0
  68. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +83 -3
  69. package/node_modules/@spexcode/spec-cli/dist/guide.js +29 -18
  70. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
  71. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +24 -6
  72. package/node_modules/@spexcode/spec-cli/dist/harness.js +510 -72
  73. package/node_modules/@spexcode/spec-cli/dist/help.js +11 -8
  74. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +8 -0
  75. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +29 -8
  76. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +7 -0
  77. package/node_modules/@spexcode/spec-cli/dist/host.js +98 -1
  78. package/node_modules/@spexcode/spec-cli/dist/index.js +294 -36
  79. package/node_modules/@spexcode/spec-cli/dist/init.js +1 -1
  80. package/node_modules/@spexcode/spec-cli/dist/lint.js +70 -35
  81. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +3 -2
  82. package/node_modules/@spexcode/spec-cli/dist/listen.js +14 -2
  83. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +2 -1
  84. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +46 -15
  85. package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +2 -2
  86. package/node_modules/@spexcode/spec-cli/dist/materialize.js +176 -35
  87. package/node_modules/@spexcode/spec-cli/dist/opencode.js +10 -2
  88. package/node_modules/@spexcode/spec-cli/dist/pi-harness.js +18 -5
  89. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +2 -0
  90. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +27 -0
  91. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +14 -14
  92. package/node_modules/@spexcode/spec-cli/dist/reviews.js +12 -7
  93. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +11 -0
  94. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +79 -1
  95. package/node_modules/@spexcode/spec-cli/dist/session-application.d.ts +23 -0
  96. package/node_modules/@spexcode/spec-cli/dist/session-application.js +189 -0
  97. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +13 -1
  98. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +6 -0
  99. package/node_modules/@spexcode/spec-cli/dist/session-files.js +13 -1
  100. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +39 -22
  101. package/node_modules/@spexcode/{session-core/dist/record-lock.d.ts → spec-cli/dist/session-record-lock.d.ts} +0 -4
  102. package/node_modules/@spexcode/spec-cli/dist/session-record-lock.js +94 -0
  103. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.d.ts +44 -0
  104. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.js +37 -0
  105. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +25 -2
  106. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +68 -11
  107. package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +57 -0
  108. package/node_modules/@spexcode/spec-cli/dist/session-transcript.js +137 -0
  109. package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
  110. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +112 -15
  111. package/node_modules/@spexcode/spec-cli/dist/sessions.js +1553 -764
  112. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.js +14 -0
  113. package/node_modules/@spexcode/spec-cli/dist/source-list.d.ts +13 -0
  114. package/node_modules/@spexcode/spec-cli/dist/source-list.js +99 -0
  115. package/node_modules/@spexcode/spec-cli/dist/source-read.d.ts +16 -0
  116. package/node_modules/@spexcode/spec-cli/dist/source-read.js +84 -0
  117. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.d.ts +7 -0
  118. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.js +89 -0
  119. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.d.ts +23 -0
  120. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.js +138 -0
  121. package/node_modules/@spexcode/spec-cli/dist/supervise.js +15 -6
  122. package/node_modules/@spexcode/spec-cli/hooks/dispatch.sh +19 -31
  123. package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
  124. package/node_modules/@spexcode/spec-cli/package.json +8 -6
  125. package/node_modules/@spexcode/spec-cli/templates/hooks/post-checkout +4 -2
  126. package/node_modules/@spexcode/spec-cli/templates/hooks/post-merge +2 -1
  127. package/node_modules/@spexcode/spec-cli/templates/hooks/pre-commit +5 -3
  128. package/node_modules/@spexcode/spec-cli/templates/hooks/reference-transaction +5 -3
  129. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/spec.md +2 -7
  130. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
  131. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
  132. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
  133. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
  134. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
  135. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
  136. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
  137. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
  138. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -0
  139. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
  140. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
  141. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
  142. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/spec.md +2 -6
  143. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/spec.md +7 -0
  144. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +13 -2
  145. package/node_modules/@spexcode/spec-core/dist/anchors.js +311 -15
  146. package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
  147. package/node_modules/@spexcode/spec-core/dist/git.js +9 -9
  148. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +12 -1
  149. package/node_modules/@spexcode/spec-core/dist/layout.js +4 -2
  150. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +1 -0
  151. package/node_modules/@spexcode/spec-core/dist/resilience.js +8 -0
  152. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +1 -0
  153. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +7 -3
  154. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +1 -0
  155. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +4 -0
  156. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +2 -0
  157. package/node_modules/@spexcode/spec-core/dist/specs.js +30 -0
  158. package/node_modules/@spexcode/spec-core/package.json +4 -1
  159. package/node_modules/@spexcode/spec-core/templates/spexcode.json +36 -13
  160. package/node_modules/@spexcode/spec-eval/dist/cli.js +1 -1
  161. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +1 -0
  162. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +6 -1
  163. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +1 -1
  164. package/node_modules/@spexcode/spec-eval/dist/freshness.js +12 -12
  165. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +2 -1
  166. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +2 -1
  167. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +20 -3
  168. package/node_modules/@spexcode/spec-eval/package.json +2 -2
  169. package/node_modules/@spexcode/spec-forge/package.json +2 -2
  170. package/node_modules/@spexcode/transcript/dist/frames.d.ts +44 -0
  171. package/node_modules/@spexcode/transcript/dist/frames.js +85 -0
  172. package/node_modules/@spexcode/transcript/dist/index.d.ts +5 -0
  173. package/node_modules/@spexcode/transcript/dist/index.js +5 -0
  174. package/node_modules/@spexcode/transcript/dist/live.d.ts +16 -0
  175. package/node_modules/@spexcode/transcript/dist/live.js +65 -0
  176. package/node_modules/@spexcode/transcript/dist/parsers.d.ts +47 -0
  177. package/node_modules/@spexcode/transcript/dist/parsers.js +285 -0
  178. package/node_modules/@spexcode/transcript/dist/readers.d.ts +10 -0
  179. package/node_modules/@spexcode/transcript/dist/readers.js +296 -0
  180. package/node_modules/@spexcode/transcript/dist/turns.d.ts +42 -0
  181. package/node_modules/@spexcode/transcript/dist/turns.js +11 -0
  182. package/node_modules/@spexcode/transcript/package.json +30 -0
  183. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  184. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  185. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  186. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  187. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  188. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  189. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  190. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  191. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  192. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  193. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  194. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  195. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  196. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  197. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  198. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  199. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  200. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  201. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  202. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  203. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  204. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  205. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  206. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  207. package/node_modules/smol-toml/LICENSE +24 -0
  208. package/node_modules/smol-toml/README.md +418 -0
  209. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  210. package/node_modules/smol-toml/dist/date.js +127 -0
  211. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  212. package/node_modules/smol-toml/dist/error.js +63 -0
  213. package/node_modules/smol-toml/dist/extract.js +69 -0
  214. package/node_modules/smol-toml/dist/index.cjs +734 -0
  215. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  216. package/node_modules/smol-toml/dist/index.js +33 -0
  217. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  218. package/node_modules/smol-toml/dist/parse.js +149 -0
  219. package/node_modules/smol-toml/dist/primitive.js +238 -0
  220. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  221. package/node_modules/smol-toml/dist/stringify.js +181 -0
  222. package/node_modules/smol-toml/dist/struct.js +179 -0
  223. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  224. package/node_modules/smol-toml/dist/util.js +89 -0
  225. package/node_modules/smol-toml/package.json +68 -0
  226. package/package.json +9 -3
  227. package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +0 -24
  228. package/node_modules/@spexcode/session-core/dist/delivery-queue.js +0 -206
  229. package/node_modules/@spexcode/session-core/dist/index.d.ts +0 -5
  230. package/node_modules/@spexcode/session-core/dist/index.js +0 -5
  231. package/node_modules/@spexcode/session-core/dist/internal.d.ts +0 -3
  232. package/node_modules/@spexcode/session-core/dist/internal.js +0 -3
  233. package/node_modules/@spexcode/session-core/dist/message.d.ts +0 -22
  234. package/node_modules/@spexcode/session-core/dist/message.js +0 -53
  235. package/node_modules/@spexcode/session-core/dist/record-lock.js +0 -152
  236. package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +0 -62
  237. package/node_modules/@spexcode/session-core/dist/runtime-session.js +0 -326
  238. package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +0 -14
  239. package/node_modules/@spexcode/session-core/dist/session-cursors.js +0 -82
  240. package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +0 -47
  241. package/node_modules/@spexcode/session-core/dist/session-timeline.js +0 -216
  242. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +0 -26
  243. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +0 -475
  244. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +0 -10
  245. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +0 -70
  246. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
  247. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-sed-v0.fixture +0 -46
@@ -71,7 +71,19 @@ export function listSessionFiles(id) {
71
71
  requireSession(id);
72
72
  return readSessionFiles(id);
73
73
  }
74
- // Session projections already hold a parsed record; re-checking session.json here would make an in-memory
74
+ export function inspectSessionFiles(id) {
75
+ requireSession(id);
76
+ return readFiles(id).map((path) => {
77
+ try {
78
+ currentFile(path);
79
+ return { path, valid: true };
80
+ }
81
+ catch (error) {
82
+ return { path, valid: false, reason: error instanceof Error ? error.message : String(error) };
83
+ }
84
+ });
85
+ }
86
+ // Session projections already hold a parsed runtime envelope; re-checking it here would make an in-memory
75
87
  // projection depend on the public route's existence guard. The route-facing list keeps that guard above.
76
88
  export function readSessionFiles(id) {
77
89
  return readFiles(id);
@@ -1,13 +1,9 @@
1
- import { existsSync } from 'node:fs';
2
- import { sessionStoreDir } from '@spexcode/spec-core';
3
- import { advanceFollow, followCursor, unreadSince } from '@spexcode/session-core';
1
+ import { configuredSessionApplicationIfCutover } from './session-application.js';
4
2
  import { timelineDisplay, timelineEvents, timelineStamp } from './session-timeline.js';
5
3
  import { sessionTitle } from './sessions.js';
6
- // @@@ session-follow - supervision is FOLLOWING a log past a cursor, never polling a derived board. One tick
7
- // costs ONE stat per target: if timeline.ndjson has not grown, nothing is opened and nothing is parsed. No
8
- // call here reaches the backend, a rendezvous socket, or tmux, which is the whole point M followers over N
9
- // sessions cost the control plane zero, where the old poll cost one board build (a connect + a tmux spawn per
10
- // live session) per follower per interval.
4
+ // @@@ session-follow - supervision follows canonical SQLite events past an application-owned cursor, never a
5
+ // derived board. The bounded observation tick reads the event sequence and cursor only; it never opens a backend,
6
+ // rendezvous socket, or tmux. M followers over N sessions therefore cost the control plane zero native probes.
11
7
  // Actionable = a state whose arrival means "a human/supervisor must now act". `offline` is deliberately ABSENT
12
8
  // where the old poll had it: liveness is a present-tense probe derivation, never authored, so it can never
13
9
  // appear on a log ([[state]]). A follower learns what a session DECLARED and never that it died.
@@ -51,11 +47,19 @@ export async function followSessions(emit, opts) {
51
47
  return f.path;
52
48
  return [];
53
49
  };
54
- const readCursor = (id) => (self ? followCursor(self, id) : memo.get(id) ?? null);
55
- const writeCursor = (id, to) => { if (self)
56
- advanceFollow(self, id, to);
57
- else
58
- memo.set(id, to); };
50
+ const application = configuredSessionApplicationIfCutover();
51
+ const canonicalSelf = self && application?.readState(self) ? self : null;
52
+ const readCursor = (id) => {
53
+ if (!self)
54
+ return memo.get(id) ?? null;
55
+ return canonicalSelf ? application.readFollowCursor(canonicalSelf, id) : null;
56
+ };
57
+ const writeCursor = (id, to) => {
58
+ if (!self)
59
+ memo.set(id, to);
60
+ else if (canonicalSelf)
61
+ application.advanceFollowCursor(canonicalSelf, id, to);
62
+ };
59
63
  const line = (id, st, note, first) => {
60
64
  const s = row?.(id, st, note);
61
65
  if (!s)
@@ -74,7 +78,7 @@ export async function followSessions(emit, opts) {
74
78
  if (id === self)
75
79
  continue; // the follower's own log is its INBOX, followed below on its own cursor rule
76
80
  seen.add(id);
77
- if (!existsSync(sessionStoreDir(id))) {
81
+ if (!application?.readState(id)) {
78
82
  if (state.delete(id))
79
83
  emit(`${tag}[spex] closed · removed [id ${id}]`);
80
84
  if (take)
@@ -111,7 +115,12 @@ export async function followSessions(emit, opts) {
111
115
  continue; // THE cheap tick: nothing appended, nothing parsed
112
116
  f.stamp = stamp;
113
117
  const evs = timelineEvents(id);
114
- const slice = unreadSince(evs, f.pos);
118
+ const unread = evs.slice(f.pos);
119
+ const slice = {
120
+ events: unread,
121
+ at: unread.map((_, index) => f.pos + index),
122
+ next: evs.length,
123
+ };
115
124
  let hit = null;
116
125
  for (let k = 0; k < slice.events.length && !hit; k++) {
117
126
  const e = slice.events[k];
@@ -151,18 +160,26 @@ export async function followSessions(emit, opts) {
151
160
  state.delete(id);
152
161
  emit(`${tag}[spex] closed · removed [id ${id}]`);
153
162
  }
154
- // THE FOLLOWER'S OWN LOG — watched exactly like any other target, on its own entry in `cursors.json`. It is
155
- // a WATCH, not a delivery: a message reaches the agent as a prompt through the adapter ([[delivery-queue]]),
156
- // so this position only decides what THIS process has already reported and can never make an agent miss
157
- // mail. Never advanced here in take mode — the waiter stops on the event and the next wait resumes on it.
158
- if (self && existsSync(sessionStoreDir(self))) {
159
- const mine = unreadSince(timelineEvents(self), followCursor(self, self) ?? 0);
163
+ // THE FOLLOWER'S OWN LOG — watched exactly like any other target, on its own canonical cursor row. In take
164
+ // mode the returned message is this wait's consumed result, so advance past it before returning; otherwise
165
+ // an empty cursor returns the same oldest inbox message forever. A turn-boundary delivery may still present
166
+ // the message through the adapter, but it does not own this read cursor.
167
+ if (self && application?.readState(self)) {
168
+ const ownEvents = timelineEvents(self);
169
+ const ownStart = readCursor(self) ?? 0;
170
+ const ownUnread = ownEvents.slice(ownStart);
171
+ const mine = {
172
+ events: ownUnread,
173
+ at: ownUnread.map((_, index) => ownStart + index),
174
+ };
160
175
  for (let k = 0; k < mine.events.length; k++) {
161
176
  const e = mine.events[k];
162
177
  if (e.kind !== 'sent')
163
178
  continue;
164
- if (take)
179
+ if (take) {
180
+ writeCursor(self, mine.at[k] + 1);
165
181
  return { mail: { from: e.from, text: e.text } };
182
+ }
166
183
  // a stream has no turn boundary to advance that cursor, so its own high-water mark — process-local,
167
184
  // never written — is what keeps an unread line from being re-printed on every tick.
168
185
  if (mine.at[k] <= shown)
@@ -1,7 +1,3 @@
1
- export declare class SessionRecordLockTimeout extends Error {
2
- constructor(id: string);
3
- }
4
1
  export declare function withSessionRecordLock<T>(id: string, body: () => Promise<T>, signal?: AbortSignal): Promise<T>;
5
2
  export declare function withSessionRecordLockSync<T>(id: string, body: () => T): T;
6
3
  export declare function trySessionRecordLockSync(id: string): (() => void) | null;
7
- export declare function withSessionRecordLocks<T>(rawIds: string[], body: () => Promise<T>, index?: number, ids?: string[]): Promise<T>;
@@ -0,0 +1,94 @@
1
+ import { closeSync, mkdirSync, openSync, readFileSync, unlinkSync, writeSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { runtimeRoot } from '@spexcode/spec-core';
4
+ const root = () => join(runtimeRoot(), '.session-record-locks');
5
+ const pathFor = (id) => join(root(), `${id}.lock`);
6
+ const pause = (signal) => new Promise((resolve, reject) => {
7
+ if (signal?.aborted) {
8
+ reject(Object.assign(new Error('The operation was aborted'), { name: 'AbortError', code: 'ABORT_ERR' }));
9
+ return;
10
+ }
11
+ const timer = setTimeout(resolve, 10);
12
+ if (signal)
13
+ signal.addEventListener('abort', () => { clearTimeout(timer); reject(Object.assign(new Error('The operation was aborted'), { name: 'AbortError', code: 'ABORT_ERR' })); }, { once: true });
14
+ });
15
+ async function acquire(id, timeoutMs = 30_000, signal) {
16
+ mkdirSync(root(), { recursive: true });
17
+ const path = pathFor(id), deadline = Date.now() + timeoutMs;
18
+ for (;;) {
19
+ if (signal?.aborted)
20
+ throw Object.assign(new Error('The operation was aborted'), { name: 'AbortError', code: 'ABORT_ERR' });
21
+ try {
22
+ const fd = openSync(path, 'wx');
23
+ writeSync(fd, String(process.pid));
24
+ closeSync(fd);
25
+ return () => { try {
26
+ unlinkSync(path);
27
+ }
28
+ catch { /* dead-owner recovery may have removed it */ } };
29
+ }
30
+ catch (error) {
31
+ if (error.code !== 'EEXIST')
32
+ throw error;
33
+ let owner = 0;
34
+ try {
35
+ owner = Number(readFileSync(path, 'utf8').trim()) || 0;
36
+ }
37
+ catch { /* creator/releaser race */ }
38
+ if (owner && owner !== process.pid) {
39
+ try {
40
+ process.kill(owner, 0);
41
+ }
42
+ catch {
43
+ try {
44
+ unlinkSync(path);
45
+ }
46
+ catch { /* race */ }
47
+ ;
48
+ continue;
49
+ }
50
+ }
51
+ if (Date.now() >= deadline)
52
+ throw new Error(`session record ${id}: timed out waiting for transaction lock`);
53
+ await pause(signal);
54
+ }
55
+ }
56
+ }
57
+ export async function withSessionRecordLock(id, body, signal) {
58
+ const release = await acquire(id, 30_000, signal);
59
+ try {
60
+ return await body();
61
+ }
62
+ finally {
63
+ release();
64
+ }
65
+ }
66
+ export function withSessionRecordLockSync(id, body) {
67
+ const release = trySessionRecordLockSync(id);
68
+ if (!release)
69
+ throw new Error(`session record ${id}: transaction lock is busy`);
70
+ try {
71
+ return body();
72
+ }
73
+ finally {
74
+ release();
75
+ }
76
+ }
77
+ export function trySessionRecordLockSync(id) {
78
+ mkdirSync(root(), { recursive: true });
79
+ const path = pathFor(id);
80
+ try {
81
+ const fd = openSync(path, 'wx');
82
+ writeSync(fd, String(process.pid));
83
+ closeSync(fd);
84
+ return () => { try {
85
+ unlinkSync(path);
86
+ }
87
+ catch { /* dead-owner recovery may have removed it */ } };
88
+ }
89
+ catch (error) {
90
+ if (error.code === 'EEXIST')
91
+ return null;
92
+ throw error;
93
+ }
94
+ }
@@ -0,0 +1,44 @@
1
+ export interface GovernedRuntimeIdentity {
2
+ protocolSessionId: string;
3
+ harnessId: string;
4
+ harnessSessionId: string;
5
+ nativeStartToken: string;
6
+ metadata?: Record<string, unknown>;
7
+ }
8
+ export interface GovernedRuntimeBindingOptions {
9
+ expectedGeneration?: number;
10
+ now?: number;
11
+ }
12
+ export interface GovernedRuntimeBinding {
13
+ namespace: string;
14
+ protocolSessionId: string;
15
+ runtimeKind: string;
16
+ nativeSessionId: string;
17
+ nativeStartToken: string;
18
+ bindingGeneration: number;
19
+ status: 'bound' | 'unbound';
20
+ boundAtMs: number;
21
+ unboundAtMs: number | null;
22
+ metadata: Record<string, unknown>;
23
+ }
24
+ export interface GovernedProtocolTransactionHost<Transaction = unknown> {
25
+ withTransaction<T>(body: (transaction: Transaction) => T): T;
26
+ }
27
+ export interface GovernedRuntimeBindingStore<Transaction = unknown> {
28
+ bind(transaction: Transaction, protocolSessionId: string, identity: {
29
+ namespace: string;
30
+ runtimeKind: string;
31
+ nativeSessionId: string;
32
+ nativeStartToken: string;
33
+ metadata?: Record<string, unknown>;
34
+ }, options?: GovernedRuntimeBindingOptions): GovernedRuntimeBinding;
35
+ resolve(namespace: string, protocolSessionId: string, transaction?: Transaction): GovernedRuntimeBinding | null;
36
+ unbind(transaction: Transaction, namespace: string, protocolSessionId: string, options?: GovernedRuntimeBindingOptions): GovernedRuntimeBinding;
37
+ }
38
+ /**
39
+ * Bind an identity whose storage and native start token were already established by Spex composition.
40
+ * Database placement, locality, launch, and legacy record publication deliberately remain outside this seam.
41
+ */
42
+ export declare function bindSpexGovernedRuntime<Transaction>(protocol: GovernedProtocolTransactionHost<Transaction>, bindings: GovernedRuntimeBindingStore<Transaction>, identity: GovernedRuntimeIdentity, options?: GovernedRuntimeBindingOptions): GovernedRuntimeBinding;
43
+ export declare function resolveSpexGovernedRuntime<Transaction>(bindings: GovernedRuntimeBindingStore<Transaction>, protocolSessionId: string, transaction?: Transaction): GovernedRuntimeBinding | null;
44
+ export declare function unbindSpexGovernedRuntime<Transaction>(protocol: GovernedProtocolTransactionHost<Transaction>, bindings: GovernedRuntimeBindingStore<Transaction>, protocolSessionId: string, options?: GovernedRuntimeBindingOptions): GovernedRuntimeBinding;
@@ -0,0 +1,37 @@
1
+ const SPEX_GOVERNED_NAMESPACE = 'spex-governed';
2
+ function requireNonempty(value, field) {
3
+ if (typeof value !== 'string' || !value.trim()) {
4
+ throw new TypeError(`Spex governed runtime binding requires ${field}`);
5
+ }
6
+ }
7
+ /**
8
+ * Bind an identity whose storage and native start token were already established by Spex composition.
9
+ * Database placement, locality, launch, and legacy record publication deliberately remain outside this seam.
10
+ */
11
+ export function bindSpexGovernedRuntime(protocol, bindings, identity, options = {}) {
12
+ requireNonempty(identity.protocolSessionId, 'protocolSessionId');
13
+ requireNonempty(identity.harnessId, 'harnessId');
14
+ requireNonempty(identity.harnessSessionId, 'harnessSessionId');
15
+ requireNonempty(identity.nativeStartToken, 'nativeStartToken');
16
+ if (!protocol || typeof protocol.withTransaction !== 'function') {
17
+ throw new TypeError('Spex governed runtime binding requires an open protocol transaction host');
18
+ }
19
+ if (!bindings || typeof bindings.bind !== 'function') {
20
+ throw new TypeError('Spex governed runtime binding requires an open runtime binding store');
21
+ }
22
+ return protocol.withTransaction(transaction => bindings.bind(transaction, identity.protocolSessionId, {
23
+ namespace: SPEX_GOVERNED_NAMESPACE,
24
+ runtimeKind: identity.harnessId,
25
+ nativeSessionId: identity.harnessSessionId,
26
+ nativeStartToken: identity.nativeStartToken,
27
+ metadata: identity.metadata,
28
+ }, options));
29
+ }
30
+ export function resolveSpexGovernedRuntime(bindings, protocolSessionId, transaction) {
31
+ requireNonempty(protocolSessionId, 'protocolSessionId');
32
+ return bindings.resolve(SPEX_GOVERNED_NAMESPACE, protocolSessionId, transaction);
33
+ }
34
+ export function unbindSpexGovernedRuntime(protocol, bindings, protocolSessionId, options = {}) {
35
+ requireNonempty(protocolSessionId, 'protocolSessionId');
36
+ return protocol.withTransaction(transaction => bindings.unbind(transaction, SPEX_GOVERNED_NAMESPACE, protocolSessionId, options));
37
+ }
@@ -1,6 +1,28 @@
1
1
  import { type SessionLifecycle, type SessionProposal } from '@spexcode/spec-core';
2
- import { type TimelineEvent } from '@spexcode/session-core';
3
- export * from '@spexcode/session-core';
2
+ export type TimelineEvent = {
3
+ ts: string;
4
+ kind: 'status';
5
+ status: SessionLifecycle;
6
+ proposal: SessionProposal | null;
7
+ note: string | null;
8
+ display?: string;
9
+ } | {
10
+ ts: string;
11
+ kind: 'sent';
12
+ mid: string;
13
+ text: string;
14
+ from: string | null;
15
+ replyVia?: 'note';
16
+ };
17
+ export declare const timelineEvents: (id: string) => TimelineEvent[];
18
+ export declare const timelineStamp: (id: string) => string | null;
19
+ export declare const timelineTail: (id: string, limit?: number) => TimelineEvent[];
20
+ export declare const lastHumanSendVia: (id: string) => "note" | null;
21
+ export declare const currentHumanTurn: (id: string) => {
22
+ token: string;
23
+ acceptedAt: string;
24
+ } | null;
25
+ export declare const recordStatus: (id: string, status: SessionLifecycle, proposal: SessionProposal | null, note: string | null) => void;
4
26
  type DisplayWord = 'working' | 'idle' | 'review' | 'done' | 'close-pending' | 'parked' | 'error' | 'asking' | 'queued';
5
27
  export declare const timelineDisplay: (event: {
6
28
  status: SessionLifecycle;
@@ -9,3 +31,4 @@ export declare const timelineDisplay: (event: {
9
31
  export declare function readTimeline(id: string, limit?: number): {
10
32
  events: TimelineEvent[];
11
33
  } | null;
34
+ export {};
@@ -1,20 +1,77 @@
1
1
  import { readAliasedRawRecord } from '@spexcode/spec-core';
2
- import { timelineTail } from '@spexcode/session-core';
3
- export * from '@spexcode/session-core';
2
+ import { decodeEventJson } from '@spexcode/session-events';
3
+ import { MIGRATED_MESSAGE_EVENT, MIGRATED_STATE_EVENT } from '@spexcode/session-application';
4
+ import { configuredSessionApplicationIfCutover } from './session-application.js';
5
+ // The timeline is a history: events read in occurrence order (sequence breaks ties). Migrated legacy history
6
+ // lands after the live events in sequence but before them in time, and it is shown where it happened.
7
+ const canonicalSessionId = (id) => readAliasedRawRecord(id)?.session_id ?? id;
8
+ const canonicalTimelineEvents = (id) => {
9
+ const canonicalId = canonicalSessionId(id);
10
+ const application = configuredSessionApplicationIfCutover();
11
+ if (!application || !application.readState(canonicalId))
12
+ return null;
13
+ const ordered = [...application.readEvents(canonicalId)].sort((a, b) => a.occurredAtMs - b.occurredAtMs || a.eventSeq - b.eventSeq);
14
+ return ordered.flatMap((event) => {
15
+ const decoded = decodeEventJson(event.payload);
16
+ if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded))
17
+ return [];
18
+ const payload = decoded;
19
+ if (event.type === 'session.state.changed.v1' || event.type === MIGRATED_STATE_EVENT)
20
+ return [{
21
+ ts: new Date(event.occurredAtMs).toISOString(),
22
+ kind: 'status',
23
+ status: String(payload.status),
24
+ proposal: payload.proposal === null || payload.proposal === undefined ? null : String(payload.proposal),
25
+ note: payload.note === null || payload.note === undefined ? null : String(payload.note),
26
+ }];
27
+ if (event.type === 'session.message.sent.v1' || event.type === MIGRATED_MESSAGE_EVENT)
28
+ return [{
29
+ ts: new Date(event.occurredAtMs).toISOString(),
30
+ kind: 'sent',
31
+ mid: String(payload.messageId),
32
+ text: String(payload.text),
33
+ from: payload.from === null || payload.from === undefined ? null : String(payload.from),
34
+ ...(payload.replyVia === 'note' ? { replyVia: 'note' } : {}),
35
+ }];
36
+ return [];
37
+ });
38
+ };
39
+ export const timelineEvents = (id) => canonicalTimelineEvents(id) ?? [];
40
+ export const timelineStamp = (id) => {
41
+ const canonicalId = canonicalSessionId(id);
42
+ const application = configuredSessionApplicationIfCutover();
43
+ if (application?.readState(canonicalId)) {
44
+ const events = application.readEvents(canonicalId);
45
+ return events.length === 0 ? null : String(events.at(-1).eventSeq);
46
+ }
47
+ return null;
48
+ };
49
+ export const timelineTail = (id, limit = 500) => timelineEvents(id).slice(-Math.max(1, limit));
50
+ export const lastHumanSendVia = (id) => {
51
+ const events = timelineEvents(id);
52
+ const sent = [...events].reverse().find((event) => event.kind === 'sent' && event.from === null);
53
+ return sent?.replyVia ?? null;
54
+ };
55
+ export const currentHumanTurn = (id) => {
56
+ const sent = [...timelineEvents(id)].reverse().find((event) => event.kind === 'sent' && event.from === null);
57
+ return sent ? { token: sent.mid, acceptedAt: sent.ts } : null;
58
+ };
59
+ export const recordStatus = (id, status, proposal, note) => {
60
+ const application = configuredSessionApplicationIfCutover();
61
+ if (!application?.readState(id))
62
+ throw new Error(`cannot record status for unknown canonical session ${id}`);
63
+ application.transitionSession(id, { status, proposal, note, reason: 'cli-status' });
64
+ };
4
65
  const PROPOSAL_DISPLAY = { merge: 'review', nothing: 'done', close: 'close-pending' };
5
66
  export const timelineDisplay = (event) => event.status === 'awaiting' ? (PROPOSAL_DISPLAY[event.proposal ?? 'nothing'] ?? 'done')
6
67
  : event.status === 'active' ? 'working' : event.status;
7
68
  // The HTTP projection remains a SpexCode concern: it resolves aliases, hides unmanaged records, and adds the
8
69
  // board's display vocabulary. The package beneath it only reads the durable file protocol.
9
70
  export function readTimeline(id, limit = 500) {
10
- let raw;
11
- try {
12
- raw = readAliasedRawRecord(id);
71
+ const application = configuredSessionApplicationIfCutover();
72
+ const canonicalId = canonicalSessionId(id);
73
+ if (application && application.readState(canonicalId)) {
74
+ return { events: canonicalTimelineEvents(id).slice(-Math.max(1, limit)).map((event) => event.kind === 'status' ? { ...event, display: timelineDisplay(event) } : event) };
13
75
  }
14
- catch {
15
- return null;
16
- }
17
- if (!raw || !raw.governed)
18
- return null;
19
- return { events: timelineTail(raw.session_id, limit).map((event) => event.kind === 'status' ? { ...event, display: timelineDisplay(event) } : event) };
76
+ return null;
20
77
  }
@@ -0,0 +1,57 @@
1
+ import type { Context } from 'hono';
2
+ import { type Harness } from './harness.js';
3
+ type Target = {
4
+ ok: true;
5
+ harness: Harness;
6
+ threadId: string;
7
+ } | {
8
+ ok: false;
9
+ status: 404 | 409 | 500;
10
+ error: string;
11
+ };
12
+ export declare function resolveTranscriptTarget(id: string): Target;
13
+ export declare function readSessionTranscript(c: Context): Promise<(Response & import("hono").TypedResponse<{
14
+ error: string;
15
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
16
+ error: string;
17
+ }, 500 | 404 | 409, "json">) | (Response & import("hono").TypedResponse<{
18
+ readonly revision: string;
19
+ readonly from: number;
20
+ readonly to: number;
21
+ readonly turns: readonly {
22
+ readonly id: string;
23
+ readonly at: number;
24
+ readonly role: "user" | "assistant";
25
+ readonly text?: string | undefined;
26
+ readonly tools?: readonly {
27
+ readonly id: string;
28
+ readonly name: string;
29
+ readonly input?: string | undefined;
30
+ readonly output?: string | undefined;
31
+ readonly outputLines: number;
32
+ readonly outputBytes: number;
33
+ }[] | undefined;
34
+ }[];
35
+ readonly truncated: boolean;
36
+ readonly omittedTurns: number;
37
+ readonly omittedBytes: number;
38
+ readonly outOfOrderEvents: number;
39
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
40
+ error: string;
41
+ reason: string;
42
+ }, 409 | 422 | 501, "json">)>;
43
+ export declare function readSessionTranscriptTool(c: Context): Promise<(Response & import("hono").TypedResponse<{
44
+ error: string;
45
+ reason: string;
46
+ }, 409 | 422 | 501, "json">) | (Response & import("hono").TypedResponse<{
47
+ error: string;
48
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
49
+ error: string;
50
+ }, 500 | 404 | 409, "json">) | (Response & import("hono").TypedResponse<{
51
+ id: string;
52
+ output: string | null;
53
+ outputLines: number;
54
+ outputBytes: number;
55
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
56
+ export declare function sessionTranscriptStream(c: Context): Promise<Response>;
57
+ export {};
@@ -0,0 +1,137 @@
1
+ import { streamSSE } from 'hono/streaming';
2
+ import { readAliasedRawRecord } from '@spexcode/spec-core';
3
+ import { defaultHarness, harnessById } from './harness.js';
4
+ import { TranscriptReadError, openFrameStream } from '@spexcode/transcript';
5
+ export function resolveTranscriptTarget(id) {
6
+ let raw;
7
+ try {
8
+ raw = readAliasedRawRecord(id);
9
+ }
10
+ catch (error) {
11
+ return { ok: false, status: 500, error: `session ${id} record is unreadable: ${error instanceof Error ? error.message : String(error)}` };
12
+ }
13
+ if (!raw || !raw.governed)
14
+ return { ok: false, status: 404, error: `session ${id} does not exist` };
15
+ let harness;
16
+ try {
17
+ harness = harnessById(typeof raw.harness === 'string' && raw.harness ? raw.harness : defaultHarness.id);
18
+ }
19
+ catch (error) {
20
+ return { ok: false, status: 500, error: error instanceof Error ? error.message : String(error) };
21
+ }
22
+ const threadId = harness.exactNativeTargetId({
23
+ session: raw.session_id,
24
+ harnessSessionId: typeof raw.harness_session_id === 'string' ? raw.harness_session_id : null,
25
+ stopped: !!raw.stopped,
26
+ archived: !!raw.archived,
27
+ });
28
+ if (!threadId)
29
+ return { ok: false, status: 409, error: `session ${id} transcript is unavailable: native harness identity is missing` };
30
+ return { ok: true, harness, threadId };
31
+ }
32
+ const epoch = (value) => {
33
+ if (value == null || value === '')
34
+ return null;
35
+ const number = Number(value);
36
+ return Number.isFinite(number) && Number.isInteger(number) ? number : null;
37
+ };
38
+ const failure = (error) => {
39
+ if (!(error instanceof TranscriptReadError))
40
+ throw error;
41
+ return { body: { error: error.message, reason: error.reason }, status: error.reason === 'unsupported' ? 501 : error.reason === 'invalid' ? 422 : 409 };
42
+ };
43
+ // GET /api/sessions/:id/transcript?from=<ms>&to=<ms> — a closed interval, both bounds explicit so the route
44
+ // never guesses which stretch the caller meant.
45
+ export async function readSessionTranscript(c) {
46
+ const id = c.req.param('id') || '';
47
+ const fromRaw = c.req.query('from');
48
+ const toRaw = c.req.query('to');
49
+ if (fromRaw == null || toRaw == null || fromRaw === '' || toRaw === '')
50
+ return c.json({ error: 'transcript needs both from and to epoch milliseconds' }, 400);
51
+ const from = epoch(fromRaw);
52
+ const to = epoch(toRaw);
53
+ if (from === null || to === null || from >= to)
54
+ return c.json({ error: 'transcript interval is invalid: from and to must be integer epoch milliseconds with from < to' }, 400);
55
+ const target = resolveTranscriptTarget(id);
56
+ if (!target.ok)
57
+ return c.json({ error: target.error }, target.status);
58
+ try {
59
+ return c.json(await target.harness.transcript.read(target.threadId, { from, to }));
60
+ }
61
+ catch (error) {
62
+ const { body, status } = failure(error);
63
+ return c.json(body, status);
64
+ }
65
+ }
66
+ // GET /api/sessions/:id/transcript/tool/:toolId?from=<ms> — one call's recorded output, read when a person
67
+ // opens it. The live stream withholds output bodies (below), so this is where a body comes from; the interval
68
+ // is `[from, now]`, the same stretch the stream reads, and a call outside it is a 404.
69
+ export async function readSessionTranscriptTool(c) {
70
+ const id = c.req.param('id') || '';
71
+ const toolId = c.req.param('toolId') || '';
72
+ const from = epoch(c.req.query('from'));
73
+ if (from === null || !toolId)
74
+ return c.json({ error: 'transcript tool needs a tool id and from as integer epoch milliseconds' }, 400);
75
+ const target = resolveTranscriptTarget(id);
76
+ if (!target.ok)
77
+ return c.json({ error: target.error }, target.status);
78
+ try {
79
+ const read = await target.harness.transcript.read(target.threadId, { from, to: Math.max(from + 1, Date.now()) });
80
+ for (const turn of read.turns)
81
+ for (const tool of turn.tools ?? []) {
82
+ if (tool.id !== toolId)
83
+ continue;
84
+ return c.json({ id: tool.id, output: tool.output ?? null, outputLines: tool.outputLines, outputBytes: tool.outputBytes });
85
+ }
86
+ return c.json({ error: `tool ${toolId} is not in this interval` }, 404);
87
+ }
88
+ catch (error) {
89
+ const { body, status } = failure(error);
90
+ return c.json(body, status);
91
+ }
92
+ }
93
+ const TICK_MS = 500;
94
+ const HEARTBEAT_TICKS = 20;
95
+ // GET /api/sessions/:id/transcript/stream?from=<ms> — the open interval [from, now], carried over SSE. What a
96
+ // frame holds, when one is worth sending, and how a subscriber merges it are [[transcript-frames]]'
97
+ // `openFrameStream`, shared with every other transport; this route owns only the carriage: a tick that asks
98
+ // the stream to publish, one `transcript` SSE event per frame it yields, and periodic `ping` heartbeats so the
99
+ // browser's dead-man can reopen a silent stream. Reading happens only while a client is subscribed and stops
100
+ // on abort, closing the cursor.
101
+ export async function sessionTranscriptStream(c) {
102
+ const id = c.req.param('id') || '';
103
+ const from = epoch(c.req.query('from'));
104
+ if (from === null)
105
+ return c.json({ error: 'transcript stream needs from as integer epoch milliseconds' }, 400);
106
+ const target = resolveTranscriptTarget(id);
107
+ if (!target.ok)
108
+ return c.json({ error: target.error }, target.status);
109
+ const frames = openFrameStream(target.harness.transcript, target.threadId, from);
110
+ return streamSSE(c, async (stream) => {
111
+ let aborted = false;
112
+ let ticks = 0;
113
+ const publish = async () => {
114
+ const frame = await frames.publish();
115
+ if (frame)
116
+ await stream.writeSSE({ event: 'transcript', data: JSON.stringify(frame) });
117
+ };
118
+ stream.onAbort(() => { aborted = true; frames.close(); });
119
+ try {
120
+ await publish();
121
+ while (!aborted) {
122
+ await stream.sleep(TICK_MS);
123
+ if (aborted)
124
+ break;
125
+ await publish();
126
+ if (++ticks % HEARTBEAT_TICKS === 0)
127
+ await stream.writeSSE({ event: 'ping', data: 'x' });
128
+ }
129
+ }
130
+ catch {
131
+ // EventSource reconnects a dropped stream; a native read must never take the session server down.
132
+ }
133
+ finally {
134
+ frames.close();
135
+ }
136
+ });
137
+ }
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { readRecordEntry, sessionArtifactPath, sessionStoreDir } from '@spexcode/spec-core';
4
+ import { readRecordEntry, sessionArtifactPath, sessionRecordPath } from '@spexcode/spec-core';
5
5
  import { projectRuntimeRoot } from '@spexcode/spec-core';
6
6
  export class SessionWebError extends Error {
7
7
  status;
@@ -15,8 +15,8 @@ const webPath = (id, projectRoot) => projectRoot
15
15
  ? join(projectRuntimeRoot(join(projectRoot, '.git')), 'sessions', id, 'web.json')
16
16
  : sessionArtifactPath(id, 'web.json');
17
17
  const recordPath = (id, projectRoot) => projectRoot
18
- ? join(projectRuntimeRoot(join(projectRoot, '.git')), 'sessions', id, 'session.json')
19
- : join(sessionStoreDir(id), 'session.json');
18
+ ? join(projectRuntimeRoot(join(projectRoot, '.git')), 'sessions', id, 'runtime.json')
19
+ : sessionRecordPath(id);
20
20
  function validSessionId(id) {
21
21
  return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id);
22
22
  }
@@ -104,7 +104,7 @@ export function listSessionWebs(id, projectRoot) {
104
104
  requireSession(id, projectRoot);
105
105
  return readSessionWebs(id, projectRoot);
106
106
  }
107
- // Session projections already hold a parsed record; re-checking session.json here would make an in-memory
107
+ // Session projections already hold a parsed runtime envelope; re-checking it here would make an in-memory
108
108
  // projection depend on the public route's existence guard. The route-facing list keeps that guard above.
109
109
  export function readSessionWebs(id, projectRoot) {
110
110
  return readWebs(id, projectRoot).map(toWeb);