spexcode 0.6.7 → 0.6.8

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 (205) 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 +20 -0
  42. package/node_modules/@spexcode/session-selflaunch/dist/locality.js +83 -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 +56 -19
  54. package/node_modules/@spexcode/spec-cli/dist/cli.js +102 -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/execution-trace.d.ts +1 -0
  63. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +2 -2
  64. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +2 -1
  65. package/node_modules/@spexcode/spec-cli/dist/gateway.js +6 -3
  66. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +32 -2
  67. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +57 -2
  68. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +2 -0
  69. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +83 -3
  70. package/node_modules/@spexcode/spec-cli/dist/guide.js +20 -7
  71. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
  72. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +15 -3
  73. package/node_modules/@spexcode/spec-cli/dist/harness.js +331 -50
  74. package/node_modules/@spexcode/spec-cli/dist/help.js +11 -8
  75. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +8 -0
  76. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +29 -8
  77. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +7 -0
  78. package/node_modules/@spexcode/spec-cli/dist/host.js +93 -0
  79. package/node_modules/@spexcode/spec-cli/dist/index.js +324 -22
  80. package/node_modules/@spexcode/spec-cli/dist/init.js +1 -1
  81. package/node_modules/@spexcode/spec-cli/dist/lint.js +70 -35
  82. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +3 -2
  83. package/node_modules/@spexcode/spec-cli/dist/listen.js +14 -2
  84. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +1 -1
  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/pty-bridge.js +14 -14
  88. package/node_modules/@spexcode/spec-cli/dist/reviews.js +12 -7
  89. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +11 -0
  90. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +79 -1
  91. package/node_modules/@spexcode/spec-cli/dist/session-application.d.ts +23 -0
  92. package/node_modules/@spexcode/spec-cli/dist/session-application.js +189 -0
  93. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +13 -1
  94. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +6 -0
  95. package/node_modules/@spexcode/spec-cli/dist/session-files.js +13 -1
  96. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +39 -22
  97. package/node_modules/@spexcode/{session-core/dist/record-lock.d.ts → spec-cli/dist/session-record-lock.d.ts} +0 -4
  98. package/node_modules/@spexcode/spec-cli/dist/session-record-lock.js +94 -0
  99. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.d.ts +44 -0
  100. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.js +37 -0
  101. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +25 -2
  102. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +68 -11
  103. package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
  104. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +108 -15
  105. package/node_modules/@spexcode/spec-cli/dist/sessions.js +1465 -744
  106. package/node_modules/@spexcode/spec-cli/dist/source-list.d.ts +13 -0
  107. package/node_modules/@spexcode/spec-cli/dist/source-list.js +99 -0
  108. package/node_modules/@spexcode/spec-cli/dist/source-read.d.ts +16 -0
  109. package/node_modules/@spexcode/spec-cli/dist/source-read.js +84 -0
  110. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.d.ts +7 -0
  111. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.js +89 -0
  112. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.d.ts +23 -0
  113. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.js +138 -0
  114. package/node_modules/@spexcode/spec-cli/dist/supervise.js +15 -6
  115. package/node_modules/@spexcode/spec-cli/dist/transcript-reader.d.ts +36 -0
  116. package/node_modules/@spexcode/spec-cli/dist/transcript-reader.js +251 -0
  117. package/node_modules/@spexcode/spec-cli/hooks/dispatch.sh +19 -31
  118. package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
  119. package/node_modules/@spexcode/spec-cli/package.json +6 -6
  120. package/node_modules/@spexcode/spec-cli/templates/hooks/post-checkout +4 -2
  121. package/node_modules/@spexcode/spec-cli/templates/hooks/post-merge +2 -1
  122. package/node_modules/@spexcode/spec-cli/templates/hooks/pre-commit +5 -3
  123. package/node_modules/@spexcode/spec-cli/templates/hooks/reference-transaction +5 -3
  124. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/spec.md +2 -7
  125. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
  126. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
  127. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
  128. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
  129. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
  130. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
  131. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
  132. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
  133. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -0
  134. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
  135. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
  136. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
  137. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/spec.md +2 -6
  138. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/spec.md +7 -0
  139. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +13 -2
  140. package/node_modules/@spexcode/spec-core/dist/anchors.js +311 -15
  141. package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
  142. package/node_modules/@spexcode/spec-core/dist/git.js +9 -9
  143. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +12 -0
  144. package/node_modules/@spexcode/spec-core/dist/layout.js +4 -2
  145. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +1 -0
  146. package/node_modules/@spexcode/spec-core/dist/resilience.js +8 -0
  147. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +1 -0
  148. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +7 -3
  149. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +1 -0
  150. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +4 -0
  151. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +2 -0
  152. package/node_modules/@spexcode/spec-core/dist/specs.js +30 -0
  153. package/node_modules/@spexcode/spec-core/package.json +4 -1
  154. package/node_modules/@spexcode/spec-eval/dist/cli.js +1 -1
  155. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +1 -0
  156. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +6 -1
  157. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +1 -1
  158. package/node_modules/@spexcode/spec-eval/dist/freshness.js +12 -12
  159. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +2 -1
  160. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +2 -1
  161. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +20 -3
  162. package/node_modules/@spexcode/spec-eval/package.json +2 -2
  163. package/node_modules/@spexcode/spec-forge/package.json +2 -2
  164. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  165. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  166. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  167. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  168. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  169. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  170. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  171. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  172. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  173. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  174. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  175. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  176. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  177. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  178. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  179. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  180. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  181. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  182. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  183. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  184. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  185. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  186. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  187. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  188. package/package.json +9 -3
  189. package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +0 -24
  190. package/node_modules/@spexcode/session-core/dist/delivery-queue.js +0 -206
  191. package/node_modules/@spexcode/session-core/dist/index.d.ts +0 -5
  192. package/node_modules/@spexcode/session-core/dist/index.js +0 -5
  193. package/node_modules/@spexcode/session-core/dist/internal.d.ts +0 -3
  194. package/node_modules/@spexcode/session-core/dist/internal.js +0 -3
  195. package/node_modules/@spexcode/session-core/dist/message.d.ts +0 -22
  196. package/node_modules/@spexcode/session-core/dist/message.js +0 -53
  197. package/node_modules/@spexcode/session-core/dist/record-lock.js +0 -152
  198. package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +0 -62
  199. package/node_modules/@spexcode/session-core/dist/runtime-session.js +0 -326
  200. package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +0 -14
  201. package/node_modules/@spexcode/session-core/dist/session-cursors.js +0 -82
  202. package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +0 -47
  203. package/node_modules/@spexcode/session-core/dist/session-timeline.js +0 -216
  204. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
  205. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-sed-v0.fixture +0 -46
@@ -0,0 +1,27 @@
1
+ export const TOPOLOGY_MIGRATION_SQL = `
2
+ CREATE TABLE topology_edges (
3
+ edge_id TEXT NOT NULL PRIMARY KEY,
4
+ from_session_id TEXT NOT NULL REFERENCES protocol_sessions(session_id),
5
+ to_session_id TEXT NOT NULL REFERENCES protocol_sessions(session_id),
6
+ relation_type TEXT NOT NULL,
7
+ created_at_ms INTEGER NOT NULL,
8
+ removed_at_ms INTEGER,
9
+ CHECK (length(edge_id) = 32 AND edge_id NOT GLOB '*[^0-9a-f]*'),
10
+ CHECK (from_session_id <> to_session_id),
11
+ CHECK (length(relation_type) BETWEEN 1 AND 64),
12
+ CHECK (relation_type NOT GLOB '*[^0-9A-Za-z._:-]*'),
13
+ CHECK (created_at_ms >= 0),
14
+ CHECK (removed_at_ms IS NULL OR removed_at_ms >= 0)
15
+ ) STRICT;
16
+
17
+ CREATE UNIQUE INDEX topology_active_edge
18
+ ON topology_edges (from_session_id, to_session_id, relation_type)
19
+ WHERE removed_at_ms IS NULL;
20
+
21
+ CREATE INDEX topology_active_to
22
+ ON topology_edges (to_session_id, relation_type, from_session_id)
23
+ WHERE removed_at_ms IS NULL;
24
+ `;
25
+ export const TOPOLOGY_MIGRATIONS = [
26
+ { version: 1, sql: TOPOLOGY_MIGRATION_SQL },
27
+ ];
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@spexcode/session-topology",
3
+ "version": "0.6.8",
4
+ "publishConfig": { "access": "public" },
5
+ "type": "module",
6
+ "description": "A neutral SQLite relation model for session addresses.",
7
+ "files": ["dist"],
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "engines": { "node": ">=22" },
13
+ "scripts": {
14
+ "build": "node ../../scripts/build-dist.mjs",
15
+ "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
+ "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
+ },
18
+ "dependencies": { "@spexcode/session-protocol": "0.6.8" },
19
+ "devDependencies": {
20
+ "@types/node": "^20.16.0",
21
+ "tsx": "^4.19.2",
22
+ "typescript": "^5.6.3"
23
+ }
24
+ }
@@ -2,7 +2,7 @@
2
2
  // @@@ spex launcher ([[release-launcher]]) - package installs execute the package's compiled CLI directly;
3
3
  // tsx remains a development tool and never enters an adopter's runtime closure.
4
4
  import { spawn, spawnSync } from 'node:child_process'
5
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
6
6
  import { fileURLToPath } from 'node:url'
7
7
  import { dirname, join } from 'node:path'
8
8
 
@@ -16,7 +16,7 @@ const workspace = join(pkg, '..')
16
16
  // carry unresolved conflict markers, so hooks keep the retryable exit-75 contract during a merge.
17
17
  const sourceRoot = join(pkg, 'src')
18
18
  if (existsSync(sourceRoot)) {
19
- const srcRoots = [sourceRoot, join(pkg, '..', 'packages', 'spec-core', 'src'), join(pkg, '..', 'packages', 'session-core', 'src'), join(pkg, '..', 'spec-eval', 'src'), join(pkg, '..', 'spec-forge', 'src')]
19
+ const srcRoots = [sourceRoot, join(pkg, '..', 'packages', 'spec-core', 'src'), join(pkg, '..', 'spec-eval', 'src'), join(pkg, '..', 'spec-forge', 'src')]
20
20
  const conflicted = srcRoots.flatMap((root) => {
21
21
  if (!existsSync(root)) return []
22
22
  return readdirSync(root, { recursive: true })
@@ -39,28 +39,65 @@ if (existsSync(sourceRoot)) {
39
39
  const runtimeEntries = [
40
40
  cli,
41
41
  join(workspace, 'packages', 'spec-core', 'dist', 'index.js'),
42
- join(workspace, 'packages', 'session-core', 'dist', 'index.js'),
43
42
  join(workspace, 'spec-eval', 'dist', 'index.js'),
44
43
  join(workspace, 'spec-forge', 'dist', 'index.js'),
45
44
  ]
46
- const newestSource = srcRoots.reduce((newest, root) => {
47
- if (!existsSync(root)) return newest
48
- for (const entry of readdirSync(root, { recursive: true })) {
49
- const path = join(root, String(entry))
45
+ // Tests and declaration files are typecheck inputs, not runtime build inputs. Counting them here makes a
46
+ // harmless test edit look like a stale CLI and rebuilds every workspace on the next hook invocation.
47
+ const isRuntimeSource = (path) => {
48
+ return /\.(ts|tsx|js|mjs)$/.test(path)
49
+ && !/\.d\.ts$/.test(path)
50
+ && !/(^|[./])[^/]+\.test\.(ts|tsx|js|mjs)$/.test(path)
51
+ }
52
+ const sourceIsStale = () => {
53
+ const newestSource = srcRoots.reduce((newest, root) => {
54
+ if (!existsSync(root)) return newest
55
+ for (const entry of readdirSync(root, { recursive: true })) {
56
+ const path = join(root, String(entry))
57
+ try {
58
+ if (isRuntimeSource(path)) newest = Math.max(newest, statSync(path).mtimeMs)
59
+ } catch { /* a concurrent source edit can only make the next invocation rebuild again */ }
60
+ }
61
+ return newest
62
+ }, 0)
63
+ return runtimeEntries.some((entry) => {
64
+ try { return statSync(entry).mtimeMs < newestSource } catch { return true }
65
+ })
66
+ }
67
+ if (sourceIsStale()) {
68
+ // A hook, poller, and human shell can all invoke spex at once. The lock is deliberately at the
69
+ // launcher boundary: only one process pays the full workspace build, and waiters re-check freshness
70
+ // after the owner exits. A dead owner is recoverable; a live build gets two minutes before it is stale.
71
+ const buildLock = join(workspace, '.spex-build.lock')
72
+ const wait = new Int32Array(new SharedArrayBuffer(4))
73
+ const deadline = Date.now() + 120_000
74
+ let owner = false
75
+ while (!owner) {
50
76
  try {
51
- if (/\.(ts|tsx|js|mjs)$/.test(path)) newest = Math.max(newest, statSync(path).mtimeMs)
52
- } catch { /* a concurrent source edit can only make the next invocation rebuild again */ }
77
+ mkdirSync(buildLock)
78
+ owner = true
79
+ } catch (error) {
80
+ if (error?.code !== 'EEXIST') throw error
81
+ try {
82
+ if (Date.now() - statSync(buildLock).mtimeMs > 120_000) rmSync(buildLock, { recursive: true, force: true })
83
+ } catch { /* the owner may have completed between the stat and the next check */ }
84
+ if (Date.now() >= deadline) {
85
+ console.error('spex: source workspace build lock did not clear; remove .spex-build.lock after confirming no build is running.')
86
+ process.exit(75)
87
+ }
88
+ Atomics.wait(wait, 0, 0, 100)
89
+ }
53
90
  }
54
- return newest
55
- }, 0)
56
- const rebuild = runtimeEntries.some((entry) => {
57
- try { return statSync(entry).mtimeMs < newestSource } catch { return true }
58
- })
59
- if (rebuild) {
60
- const build = spawnSync('npm', ['run', 'build'], { cwd: workspace, stdio: 'inherit' })
61
- if (build.error || build.status !== 0 || !existsSync(cli)) {
62
- console.error('spex: source workspace build failed; fix it, then retry.')
63
- process.exit(build.status ?? 1)
91
+ try {
92
+ if (sourceIsStale()) {
93
+ const build = spawnSync('npm', ['run', 'build'], { cwd: workspace, stdio: 'inherit' })
94
+ if (build.error || build.status !== 0 || !existsSync(cli)) {
95
+ console.error('spex: source workspace build failed; fix it, then retry.')
96
+ process.exit(build.status ?? 1)
97
+ }
98
+ }
99
+ } finally {
100
+ rmSync(buildLock, { recursive: true, force: true })
64
101
  }
65
102
  }
66
103
  }
@@ -67,7 +67,7 @@ async function assertDaemonDependencies(command) {
67
67
  // (loadConfig on a malformed spexcode.json) surfaces as uncaughtException, not unhandledRejection, so BOTH
68
68
  // paths route through the same printer.
69
69
  function fatal(e) {
70
- if (e instanceof Error && ['BackendError', 'ConfigError', 'UsageError', 'GuardError', 'DashboardAssetError', 'GitWorkspaceError'].includes(e.name))
70
+ if (e instanceof Error && ['BackendError', 'ConfigError', 'UsageError', 'GuardError', 'DashboardAssetError', 'GitWorkspaceError', 'SessionFileError'].includes(e.name))
71
71
  console.error(`spex: ${e.message}`);
72
72
  else
73
73
  console.error(e);
@@ -96,7 +96,7 @@ function flushExit(code = 0) {
96
96
  }
97
97
  const has = (name) => process.argv.includes(`--${name}`);
98
98
  // bare positionals after argv index `from`, skipping flags and their values (selectors for ls/watch).
99
- const VALUE_FLAGS = new Set(['--status', '--as', '--interval', '--propose', '--note', '--node', '--prompt', '--prompt-file', '--timeout', '--reason', '--out', '--content-dir', '--password', '--tls-cert', '--tls-key', '--harness', '--launcher', '--harness-session', '--port', '--api', '--api-port', '--host', '--preset', '--limit', '--session', '--depth', '--focus', '--keys', '--ssh', '--allow-stop', '--allow-resume', '--ttl-ms', '--wait-ms', '--adapter', '--thread', '--tmux', '--worktree', '--branch', '--to', '--name', '--base', '--path', '--owner', '--details', '--variant', '--cli', '--count', '--ids']);
99
+ const VALUE_FLAGS = new Set(['--status', '--as', '--interval', '--propose', '--note', '--node', '--prompt', '--prompt-file', '--timeout', '--reason', '--out', '--content-dir', '--password', '--tls-cert', '--tls-key', '--harness', '--launcher', '--harness-session', '--port', '--api', '--api-port', '--host', '--preset', '--limit', '--session', '--depth', '--focus', '--keys', '--ssh', '--allow-stop', '--allow-resume', '--ttl-ms', '--wait-ms', '--adapter', '--thread', '--tmux', '--worktree', '--branch', '--to', '--name', '--base', '--candidate', '--path', '--owner', '--details', '--variant', '--cli', '--count', '--ids']);
100
100
  const EXPLICIT_BACKEND_ROUTE_FLAGS = ['api', 'port', 'password', 'insecure'];
101
101
  const EXPLICIT_BACKEND_VALUE_FLAGS = EXPLICIT_BACKEND_ROUTE_FLAGS
102
102
  .filter((name) => VALUE_FLAGS.has(`--${name}`))
@@ -213,6 +213,8 @@ function parseSessionSendArgs(args) {
213
213
  }
214
214
  if (positionals.length !== 2)
215
215
  sessionSendUsage('plain send requires exactly one selector and one message');
216
+ if (!positionals[1].trim())
217
+ sessionSendUsage('message must not be empty or whitespace');
216
218
  return { selector: positionals[0], kind: 'text', text: positionals[1], ...(values.has('--ssh') ? { sshAddress: values.get('--ssh') } : {}) };
217
219
  }
218
220
  const FULL_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -339,6 +341,7 @@ async function followKit(selectors, verb) {
339
341
  const { localCachedSessions } = await import('./client.js');
340
342
  const { fromRaw, ownSessionId, selectSessions, toSession } = await import('./sessions.js');
341
343
  const { listSessionIds, readPublicRecordEntry } = await import('@spexcode/spec-core');
344
+ const { configuredSessionApplicationIfCutover, sessionApplicationCutoverState } = await import('./session-application.js');
342
345
  const real = selectors.filter((sel) => sel && sel !== '@all');
343
346
  let picked = [];
344
347
  if (real.length) {
@@ -353,9 +356,38 @@ async function followKit(selectors, verb) {
353
356
  // a fresh sample of the mutable record — that resample is what made the old poll lose moves.
354
357
  const row = (id, status, note) => {
355
358
  const entry = readPublicRecordEntry(id);
356
- if (entry.kind !== 'ok')
359
+ if (entry.kind === 'ok')
360
+ return { ...toSession(fromRaw(entry.raw), status, 'unknown'), note };
361
+ const cutover = sessionApplicationCutoverState();
362
+ const state = cutover === 'fresh' ? null : configuredSessionApplicationIfCutover()?.readState(id);
363
+ if (!state)
357
364
  return null;
358
- return { ...toSession(fromRaw(entry.raw), status, 'unknown'), note };
365
+ return {
366
+ id,
367
+ node: null,
368
+ branch: null,
369
+ path: '',
370
+ label: id,
371
+ title: id,
372
+ raw: { name: null, title: null },
373
+ parent: state.parentSessionId,
374
+ harness: 'unknown',
375
+ capabilities: { headless: false },
376
+ launcher: null,
377
+ lifecycle: state.status,
378
+ proposal: state.proposal,
379
+ merges: 0,
380
+ status,
381
+ liveness: 'unknown',
382
+ note,
383
+ archived: state.status === 'archived',
384
+ closedAt: null,
385
+ prompt: null,
386
+ promptPreview: null,
387
+ created: state.updatedAtMs,
388
+ activity: null,
389
+ sortKey: null,
390
+ };
359
391
  };
360
392
  const watcher = ownSessionId();
361
393
  return { watcher, follow: (emit, opts) => followSessions(emit, { ...opts, targets, self: watcher, row }) };
@@ -1062,9 +1094,9 @@ else if (cmd === 'session') {
1062
1094
  // the board comes from the backend (so it shows the sessions of whatever SPEXCODE_API_URL points at,
1063
1095
  // incl. a remote machine); selectSessions/formatTable are pure presentation, applied client-side.
1064
1096
  const { ownSessionId, resolveSession, selectChildren, selectSessions, formatTable } = await import('./sessions.js');
1065
- const { clientListSessions, clientListSessionsThroughPeer, clientSessionClosure } = await import('./client.js');
1066
- // The backend's default projection excludes cold archives. --all and an explicit selector request the
1067
- // history projection so an operator can still inspect or unarchive one deliberately.
1097
+ const { clientListSessions, clientListSessionsThroughPeer } = await import('./client.js');
1098
+ // The backend's default projection excludes closed records. --all and an explicit selector request the
1099
+ // retained record projection so an operator can inspect or resume one deliberately.
1068
1100
  const selectors = positionals(4);
1069
1101
  rejectUnknownBackendFlags('spex session ls', 4, ['status', 'all', 'json', 'ssh', 'children'], ['children']);
1070
1102
  const peerAnchor = parseSessionPeerAnchor('ls', selectors);
@@ -1117,19 +1149,10 @@ else if (cmd === 'session') {
1117
1149
  const selected = selectSessions(scoped, peerAnchor ? [] : selectors);
1118
1150
  const picked = flag('status') ? selected.filter((session) => flag('status').split(',').includes(session.status)) : selected;
1119
1151
  if (!peerAnchor && children === undefined && selectors.length === 1 && !selected.length && idLikeSelector(selectors[0])) {
1120
- const closure = await clientSessionClosure(selectors[0]);
1121
- if (closure) {
1122
- const result = { id: closure.id, status: 'closed', closedAt: closure.closedAt };
1123
- console.log(has('json') ? JSON.stringify([result], null, 2) : `${closure.id.slice(0, 8)}: closed at ${closure.closedAt}`);
1124
- }
1125
- else {
1126
- console.error(`spex session ls: ${selectors[0]} was not found in this project's live, archive, or terminal-close history`);
1127
- process.exit(2);
1128
- }
1129
- }
1130
- else {
1131
- console.log(has('json') ? JSON.stringify(picked, null, 2) : formatTable(picked, true, scope));
1152
+ console.error(`spex session ls: ${selectors[0]} was not found in this project's session records`);
1153
+ process.exit(2);
1132
1154
  }
1155
+ console.log(has('json') ? JSON.stringify(picked, null, 2) : formatTable(picked, true, scope));
1133
1156
  }
1134
1157
  else if (sub === 'resources') {
1135
1158
  rejectUnknownBackendFlags('spex session resources', 4, ['json']);
@@ -1155,8 +1178,8 @@ else if (cmd === 'session') {
1155
1178
  }
1156
1179
  const files = await import('./session-files.js');
1157
1180
  if (verb === 'ls') {
1158
- for (const file of files.listSessionFiles(id))
1159
- console.log(file);
1181
+ for (const file of files.inspectSessionFiles(id))
1182
+ console.log(file.valid ? file.path : `INVALID ${file.path} — ${file.reason}`);
1160
1183
  }
1161
1184
  else if (verb === 'add') {
1162
1185
  const result = files.addSessionFile(id, path, withSessionRecordLockSync);
@@ -1389,14 +1412,6 @@ else if (cmd === 'session') {
1389
1412
  console.log(r.ok ? `interrupted ${full}` : `interrupt failed: ${r.error}`);
1390
1413
  process.exit(r.ok ? 0 : 1);
1391
1414
  }
1392
- else if (sub === 'archive' || sub === 'unarchive') {
1393
- // ARCHIVING/legacy unarchive ([[archive]]) — archive exact-stops before filing; unarchive signposts to
1394
- // resume and recreates the same conversation.
1395
- const on = sub === 'archive';
1396
- const full = await resolveSelectorOrExit(id);
1397
- const ok = await c.clientArchive(full, on);
1398
- console.log(!ok ? `no such session ${full}` : `${on ? 'archived' : 'resumed'} ${full}`);
1399
- }
1400
1415
  else if (sub === 'close') {
1401
1416
  const full = targetArgs?.sshAddress
1402
1417
  ? (FULL_SESSION_ID.test(id ?? '') ? id : sessionTargetUsage('close', '--ssh requires a full session id, not a selector'))
@@ -1688,47 +1703,59 @@ else if (cmd === 'internal') {
1688
1703
  // materializing the rollout — then stage the id + exact-payload proof for the session lifecycle owner and
1689
1704
  // print the thread id. The launch script then `resume`s it in the visible TUI.
1690
1705
  const { codexStartThread, codexTurn, waitForCodexRollout, codexBinary, codexSupportsBypassHookTrust, codexLauncherThreadPolicy } = await import('./harness.js');
1691
- const { stageHarnessLaunchProof } = await import('./sessions.js');
1706
+ const { existingHarnessLaunchTarget, stageHarnessLaunchProof } = await import('./sessions.js');
1692
1707
  const sock = process.argv[4], cwd = process.argv[5];
1693
1708
  const prompt = process.argv.slice(6).join(' ');
1694
1709
  if (!sock || !cwd) {
1695
1710
  console.error('usage: spex internal codex-launch <sock> <cwd> [prompt...]');
1696
1711
  process.exit(2);
1697
1712
  }
1698
- // On the bypass-trust path (the codex install supports the flag → materialize skipped writeCodexTrust's hash),
1699
- // the thread the BACKEND owns must carry `bypass_hook_trust` in thread/start's config so the app-server fires
1700
- // the worktree's local hooks mirror materialize's capability decision so the two stay in lockstep.
1701
- const launcherCmd = process.env.SPEXCODE_CODEX_CMD || 'codex';
1702
- const bypassHookTrust = codexSupportsBypassHookTrust(codexBinary(launcherCmd));
1703
- // The governed record id rides into the thread's own shell environment (shell_environment_policy.set), so
1704
- // every command this thread spawns knows which session it is — the codex equivalent of the launch-injected
1705
- // id claude gets. codex-launch is exactly where both ids are known ([[harness-adapter]]).
1706
- const ownId = process.env.SPEXCODE_SESSION_ID?.trim();
1707
- const r = await codexStartThread(sock, cwd, bypassHookTrust, ownId ? { SPEXCODE_SESSION_ID: ownId } : undefined, codexLauncherThreadPolicy(launcherCmd));
1708
- if (!r.ok) {
1709
- console.error(r.error);
1710
- process.exit(1);
1713
+ const sid = process.env.SPEXCODE_SESSION_ID;
1714
+ // The visible TUI can fail after codex-launch has already staged the native id and first-turn proof
1715
+ // (for example, a remote-workspace validation error). The bounded shell retry re-enters this command;
1716
+ // reuse the proven target instead of minting a second thread or asking stageHarnessLaunchProof to consume
1717
+ // a payload that the lifecycle owner may already have committed.
1718
+ const existingTarget = sid ? existingHarnessLaunchTarget(sid) : null;
1719
+ if (existingTarget) {
1720
+ console.log(existingTarget);
1711
1721
  }
1712
- if (prompt) {
1713
- const t = await codexTurn(sock, r.threadId, prompt, cwd);
1714
- if (!t.ok) {
1715
- console.error(t.error);
1722
+ else {
1723
+ // On the bypass-trust path (the codex install supports the flag → materialize skipped writeCodexTrust's hash),
1724
+ // the thread the BACKEND owns must carry `bypass_hook_trust` in thread/start's config so the app-server fires
1725
+ // the worktree's local hooks — mirror materialize's capability decision so the two stay in lockstep.
1726
+ const launcherCmd = process.env.SPEXCODE_CODEX_CMD || 'codex';
1727
+ const bypassHookTrust = codexSupportsBypassHookTrust(codexBinary(launcherCmd));
1728
+ // The governed record id rides into the thread's own shell environment (shell_environment_policy.set), so
1729
+ // every command this thread spawns knows which session it is — the codex equivalent of the launch-injected
1730
+ // id claude gets. codex-launch is exactly where both ids are known ([[harness-adapter]]).
1731
+ const ownId = process.env.SPEXCODE_SESSION_ID?.trim();
1732
+ const { withCodexGenerationMutex } = await import('./codex-runtime-generations.js');
1733
+ const { runtimeRoot: projectRuntimeRoot } = await import('@spexcode/spec-core');
1734
+ const r = await withCodexGenerationMutex(projectRuntimeRoot(cwd), () => codexStartThread(sock, cwd, bypassHookTrust, ownId ? { SPEXCODE_SESSION_ID: ownId } : undefined, codexLauncherThreadPolicy(launcherCmd)));
1735
+ if (!r.ok) {
1736
+ console.error(r.error);
1716
1737
  process.exit(1);
1717
1738
  }
1718
- // The visible TUI resumes this thread from its ON-DISK rollout; a freshly-spawned app-server acks the turn
1719
- // but persists the rollout a few seconds LATE (verified live: the SAME thread's file lands at ~2-4s, not
1720
- // lost). WAIT for it to land BEFORE storing the id / printing it, else FAIL LOUD — never store a
1721
- // non-resumable harness_session_id (that permanently wedges every reopen). The 15s budget exceeds launch.sh's
1722
- // fast-fail threshold, so a real failure exits past it and the retry loop won't spray duplicate-prompt threads.
1723
- if (!await waitForCodexRollout(r.threadId, 20000)) {
1724
- console.error(`codex thread ${r.threadId} started but persisted no rollout within 20s app-server not ready; not storing a non-resumable id`);
1725
- process.exit(1);
1739
+ if (prompt) {
1740
+ const t = await codexTurn(sock, r.threadId, prompt, cwd);
1741
+ if (!t.ok) {
1742
+ console.error(t.error);
1743
+ process.exit(1);
1744
+ }
1745
+ // The visible TUI resumes this thread from its ON-DISK rollout; a freshly-spawned app-server acks the turn
1746
+ // but persists the rollout a few seconds LATE (verified live: the SAME thread's file lands at ~2-4s, not
1747
+ // lost). WAIT for it to land BEFORE storing the id / printing it, else FAIL LOUD — never store a
1748
+ // non-resumable harness_session_id (that permanently wedges every reopen). The 15s budget exceeds launch.sh's
1749
+ // fast-fail threshold, so a real failure exits past it and the retry loop won't spray duplicate-prompt threads.
1750
+ if (!await waitForCodexRollout(r.threadId, 20000)) {
1751
+ console.error(`codex thread ${r.threadId} started but persisted no rollout within 20s — app-server not ready; not storing a non-resumable id`);
1752
+ process.exit(1);
1753
+ }
1726
1754
  }
1755
+ if (sid)
1756
+ stageHarnessLaunchProof(sid, r.threadId, prompt);
1757
+ console.log(r.threadId);
1727
1758
  }
1728
- const sid = process.env.SPEXCODE_SESSION_ID;
1729
- if (sid)
1730
- stageHarnessLaunchProof(sid, r.threadId, prompt);
1731
- console.log(r.threadId);
1732
1759
  }
1733
1760
  else if (sub === 'opencode-capture') {
1734
1761
  // opencode MINTS its own session id (no launch flag pins it), so the generated plugin's FIRST event calls
@@ -1845,6 +1872,22 @@ else if (cmd === 'internal') {
1845
1872
  const idled = mark(() => s.markIdle(sess));
1846
1873
  console.log(idled.ok ? 'idle' : idled.reason ?? 'noop (no session record, or not active)');
1847
1874
  }
1875
+ else if (sub === 'session-hook-state') {
1876
+ // Stop/idle/failure hooks are thin CLI shims. They may use the envelope for governed identity, but
1877
+ // lifecycle claims must come from the canonical session application after the JSON cutover.
1878
+ const id = flag('session');
1879
+ if (!id) {
1880
+ console.error('usage: spex internal session-hook-state --session <id>');
1881
+ process.exit(2);
1882
+ }
1883
+ const { sessionHookState } = await import('./sessions.js');
1884
+ const state = sessionHookState(id);
1885
+ if (!state)
1886
+ process.exit(1);
1887
+ // Tab-separated fields are deliberately closed vocabulary: governed, lifecycle, proposal. Notes are
1888
+ // not needed by the hook and must not become shell-parsed input.
1889
+ console.log([state.governed ? '1' : '0', state.status, state.proposal ?? ''].join('\t'));
1890
+ }
1848
1891
  else if (sub === 'commit-gate') {
1849
1892
  // the Stop gate's deterministic commit check (from cwd = the worktree): exit 0 if the node branch is
1850
1893
  // ready to declare done, else print the reason and exit 1. Takes the PROPOSAL being judged — `merge`
@@ -1,6 +1,6 @@
1
1
  import { type CockpitReview } from './cockpit.js';
2
2
  import type { SessionEvalRevision } from '@spexcode/spec-eval/sessioneval';
3
- import { type Session, type SessionClosure, type Resolved, type DispatchResult } from './sessions.js';
3
+ import { type Session, type Resolved, type DispatchResult } from './sessions.js';
4
4
  export declare class BackendError extends Error {
5
5
  readonly status?: number | undefined;
6
6
  readonly transport?: unknown | undefined;
@@ -9,7 +9,6 @@ export declare class BackendError extends Error {
9
9
  export declare function backendConnectionRefused(error: unknown): boolean;
10
10
  export declare function localCachedSessions(includeArchived?: boolean): Session[];
11
11
  export declare function clientListSessions(includeArchived?: boolean): Promise<Session[]>;
12
- export declare function clientSessionClosure(selector: string): Promise<SessionClosure | null>;
13
12
  export declare function clientResources(): Promise<import('./host-resources.js').ResourceReport>;
14
13
  export declare function resolveClientSession(selector: string, includeArchived?: boolean): Promise<Resolved>;
15
14
  export type CaptureResult = {
@@ -94,7 +93,6 @@ export declare function clientRestoreQuarantine(id: string): Promise<{
94
93
  sha256: string;
95
94
  observedAt: string;
96
95
  }>;
97
- export declare function clientArchive(id: string, on?: boolean): Promise<boolean>;
98
96
  export declare function clientRename(id: string, name: string): Promise<boolean>;
99
97
  export declare function clientReparent(children: string[], parent: string | null): Promise<import('./sessions.js').SessionReparentResult>;
100
98
  export declare function clientSendRawKeys(id: string, keys: string[]): Promise<boolean>;
@@ -5,7 +5,8 @@ import { repoRoot } from '@spexcode/spec-core';
5
5
  import { resourceBudgets } from './host-resources.js';
6
6
  import { envSessionId, listSessionIds, readPublicRecordEntry } from '@spexcode/spec-core';
7
7
  import { cockpitReview } from './cockpit.js';
8
- import { apiBaseInfo, assertProjectMatch, findSessionClosure, fromRaw, optionArgv, resolveSession, toSession } from './sessions.js';
8
+ import { configuredSessionApplicationIfCutover, sessionApplicationCutoverState } from './session-application.js';
9
+ import { apiBaseInfo, assertProjectMatch, displayStatusForProposal, fromRaw, optionArgv, resolveSession, toSession } from './sessions.js';
9
10
  import { resolveMachinePeer } from './machine-peer.js';
10
11
  export class BackendError extends Error {
11
12
  status;
@@ -136,7 +137,7 @@ function cachedStatus(rec) {
136
137
  if (rec.archived)
137
138
  return 'offline';
138
139
  if (rec.status === 'awaiting')
139
- return rec.proposal === 'merge' ? 'review' : rec.proposal === 'close' ? 'close-pending' : 'done';
140
+ return displayStatusForProposal(rec.proposal);
140
141
  return rec.status === 'active' || rec.status === 'idle' ? 'unknown' : rec.status;
141
142
  }
142
143
  function corruptCachedSession(id, reason) {
@@ -145,7 +146,7 @@ function corruptCachedSession(id, reason) {
145
146
  id, node: null, branch: null, path: '', label, title: label, raw: { name: null, title: null },
146
147
  parent: null, harness: 'unknown', capabilities: { headless: false }, launcher: null,
147
148
  lifecycle: 'active', proposal: null, merges: 0, status: 'corrupt', liveness: 'unknown',
148
- note: `session record is unreadable: ${reason}`, archived: false, archiveHazard: null,
149
+ note: `session record is unreadable: ${reason}`, archived: false, closedAt: null, archiveHazard: null,
149
150
  prompt: null, promptPreview: null, created: 0, activity: null, sortKey: null,
150
151
  };
151
152
  }
@@ -154,18 +155,59 @@ function corruptCachedSession(id, reason) {
154
155
  // follow exists to eliminate. Every field here comes from the store — nothing is probed.
155
156
  export function localCachedSessions(includeArchived = false) {
156
157
  const rows = [];
158
+ // The runtime envelope is deliberately metadata-only after migration. A live session may therefore have
159
+ // a canonical application row before its runtime.json is materialized (or after it is retired). Selectors
160
+ // must still resolve that row; treating the missing envelope as "no such session" strands watch/cancel.
161
+ // Do not create a fresh SQLite store while merely reading an empty project. A canonical application is
162
+ // needed only when a marker or legacy records prove that this project already has session state.
163
+ const cutover = sessionApplicationCutoverState();
164
+ const application = cutover === 'fresh' ? undefined : configuredSessionApplicationIfCutover();
157
165
  for (const id of listSessionIds()) {
158
166
  const entry = readPublicRecordEntry(id);
159
167
  if (entry.kind === 'corrupt') {
160
168
  rows.push(corruptCachedSession(id, entry.error));
161
169
  continue;
162
170
  }
163
- if (entry.kind !== 'ok')
171
+ if (entry.kind === 'ok') {
172
+ const rec = fromRaw(entry.raw);
173
+ if (!rec.governed)
174
+ continue;
175
+ rows.push(toSession(rec, cachedStatus(rec), 'unknown'));
164
176
  continue;
165
- const rec = fromRaw(entry.raw);
166
- if (!rec.governed)
177
+ }
178
+ const state = application?.readState(id);
179
+ if (!state)
167
180
  continue;
168
- rows.push(toSession(rec, cachedStatus(rec), 'unknown'));
181
+ const lifecycle = state.status;
182
+ const status = state.status === 'awaiting'
183
+ ? displayStatusForProposal(state.proposal)
184
+ : state.status === 'archived' ? 'offline' : state.status;
185
+ rows.push({
186
+ id,
187
+ node: null,
188
+ branch: null,
189
+ path: '',
190
+ label: id,
191
+ title: id,
192
+ raw: { name: null, title: null },
193
+ parent: state.parentSessionId,
194
+ harness: 'unknown',
195
+ capabilities: { headless: false },
196
+ launcher: null,
197
+ lifecycle,
198
+ proposal: state.proposal,
199
+ merges: 0,
200
+ status,
201
+ liveness: 'unknown',
202
+ note: state.note,
203
+ archived: state.status === 'archived',
204
+ closedAt: null,
205
+ prompt: null,
206
+ promptPreview: null,
207
+ created: state.updatedAtMs,
208
+ activity: null,
209
+ sortKey: null,
210
+ });
169
211
  }
170
212
  return rows.filter((session) => includeArchived || !session.archived)
171
213
  .sort((a, b) => (a.sortKey ?? a.created) - (b.sortKey ?? b.created) || a.id.localeCompare(b.id));
@@ -226,20 +268,6 @@ export async function clientListSessions(includeArchived = false) {
226
268
  return await r.json();
227
269
  }, () => localCachedSessions(includeArchived));
228
270
  }
229
- // A terminal close removes the record from the board. The audit lookup is deliberately id-addressed and is
230
- // consulted only after an explicit selector missed that board, so ordinary lists stay a current-state read.
231
- export async function clientSessionClosure(selector) {
232
- return cachedRead(async () => {
233
- const r = await apiFetch(`/api/sessions/${seg(selector)}/closure`);
234
- if (r.headers.get('x-spexcode-close-history') !== 'v1')
235
- throw new BackendError(`backend does not support terminal close history; refusing to label ${selector} as never existed (update the backend)`, 501);
236
- if (r.status === 404)
237
- return null;
238
- if (!r.ok)
239
- throw new BackendError(`backend error ${r.status} retrieving close history for ${selector}`, r.status);
240
- return await r.json();
241
- }, () => findSessionClosure(selector));
242
- }
243
271
  export async function clientResources() {
244
272
  return cachedRead(async () => {
245
273
  const r = await apiFetch('/api/resources');
@@ -429,15 +457,6 @@ export async function clientRestoreQuarantine(id) {
429
457
  throw new BackendError(`backend returned an invalid quarantine restore for ${id}`, r.status);
430
458
  return { id: body.id, bundle: body.bundle, sha256: body.sha256, observedAt: body.observedAt };
431
459
  }
432
- // POST /api/sessions/:id/archive — cold-archive the session ([[archive]]). The legacy on=false spelling is a
433
- // signpost to the same resume transition; it never performs a record-only unarchive.
434
- export async function clientArchive(id, on = true) {
435
- await guarded(on ? 'session archive' : 'session unarchive');
436
- const r = await apiFetch(`/api/sessions/${seg(id)}/archive`, post({ on }));
437
- if (!r.ok)
438
- throw new BackendError(`backend refused to ${on ? 'archive' : 'unarchive'} ${id}: ${await r.text()}`, r.status);
439
- return !!(await r.json().catch(() => ({ ok: false })))?.ok;
440
- }
441
460
  // POST /api/sessions/:id/rename — set (or clear, with a blank) the session's display-name override
442
461
  // ([[session-rename]] as a CLI verb). {ok:false} = no such session (404).
443
462
  export async function clientRename(id, name) {
@@ -31,6 +31,8 @@ export type CodexGenerationLedger = Readonly<{
31
31
  export declare function codexGenerationSocketPath(root: string, generationId?: string): string;
32
32
  export declare function legacyCodexGenerationEndpoint(root: string): CodexGenerationEndpoint;
33
33
  export declare function readCodexGenerationLedger(root: string): CodexGenerationLedger;
34
+ export declare function ensureCodexGenerationLedger(root: string): Promise<CodexGenerationLedger>;
35
+ export declare function withCodexGenerationMutex<T>(root: string, body: () => Promise<T>): Promise<T>;
34
36
  export type CodexGenerationRotation = Readonly<{
35
37
  previous: CodexGenerationEndpoint;
36
38
  current: CodexGenerationEndpoint;
@@ -55,4 +57,13 @@ export declare function reclaimDrainingCodexGeneration(root: string, generationI
55
57
  reclaimed: boolean;
56
58
  reason: string;
57
59
  }>;
60
+ export declare function reclaimDrainingCodexGenerations(root: string, census: (endpoint: CodexGenerationEndpoint) => Promise<{
61
+ healthy: boolean;
62
+ referenceIds: string[];
63
+ peerCount: number;
64
+ }>): Promise<Array<{
65
+ generationId: string;
66
+ reclaimed: boolean;
67
+ reason: string;
68
+ }>>;
58
69
  export {};