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
@@ -86,7 +86,9 @@ export async function issuesReview(query, requestedPage) {
86
86
  }),
87
87
  };
88
88
  }
89
- const byNewest = (a, b) => Number(b.filterKind === EVAL_FILTER_KIND.RESULT) - Number(a.filterKind === EVAL_FILTER_KIND.RESULT)
89
+ const orderableEval = (item) => item.filterKind === EVAL_FILTER_KIND.RESULT
90
+ || item.filterKind === EVAL_FILTER_KIND.DEFERRED;
91
+ const byNewest = (a, b) => Number(orderableEval(b)) - Number(orderableEval(a))
90
92
  || String(b.ts ?? '').localeCompare(String(a.ts ?? ''))
91
93
  || String(a.node ?? '').localeCompare(String(b.node ?? ''))
92
94
  || String(a.scenario ?? '').localeCompare(String(b.scenario ?? ''));
@@ -114,10 +116,11 @@ export function trunkEvalReviewItems(nodes) {
114
116
  ...reading,
115
117
  expected: scenario.expected ?? reading.expected,
116
118
  tags: scenario.tags,
117
- state: evalReviewState(reading),
119
+ ...(reading.freshnessDeferred
120
+ ? { state: 'deferred', filterKind: EVAL_FILTER_KIND.DEFERRED }
121
+ : { state: evalReviewState(reading), filterKind: EVAL_FILTER_KIND.RESULT }),
118
122
  node: node.id,
119
123
  hue: node.hue,
120
- filterKind: EVAL_FILTER_KIND.RESULT,
121
124
  });
122
125
  }
123
126
  }
@@ -149,10 +152,11 @@ export function scopedEvalReviewItems(model) {
149
152
  expected: scenario.expected ?? reading.expected,
150
153
  tags: scenario.tags,
151
154
  impact: scenario.impact,
152
- state: evalReviewState(reading),
155
+ ...(reading.freshnessDeferred
156
+ ? { state: 'deferred', filterKind: EVAL_FILTER_KIND.DEFERRED }
157
+ : { state: evalReviewState(reading), filterKind: EVAL_FILTER_KIND.RESULT }),
153
158
  node: node.id,
154
159
  hue: node.hue,
155
- filterKind: EVAL_FILTER_KIND.RESULT,
156
160
  };
157
161
  items.push(item);
158
162
  }
@@ -278,9 +282,10 @@ export function timelineEvalReviewItems(timeline, node) {
278
282
  })),
279
283
  ...[...latest.values()].map((reading) => ({
280
284
  ...reading,
281
- state: evalReviewState(reading),
285
+ ...(reading.freshnessDeferred
286
+ ? { state: 'deferred', filterKind: EVAL_FILTER_KIND.DEFERRED }
287
+ : { state: evalReviewState(reading), filterKind: EVAL_FILTER_KIND.RESULT }),
282
288
  node,
283
- filterKind: EVAL_FILTER_KIND.RESULT,
284
289
  filterKey: `${EVAL_FILTER_KIND.RESULT}:${reading.scenario}`,
285
290
  })),
286
291
  ...(timeline.dangling ?? []).map((track) => ({
@@ -10,6 +10,16 @@ export type BackendInstanceRecord = {
10
10
  export declare function readBackendInstanceRecords(root?: string): BackendInstanceRecord[];
11
11
  export declare function registerBackendInstance(instanceId: string, pid?: number, root?: string): BackendInstanceRecord;
12
12
  export declare function unregisterBackendInstance(instanceId: string, pid?: number): void;
13
+ type ProcBackendCandidate = BackendInstanceRecord & {
14
+ candidatePid: number;
15
+ command: string;
16
+ };
17
+ /**
18
+ * Reap only backend children whose supervisor identity is provably stale. A dead registry PID alone is not
19
+ * enough: the live process must carry the exact instance id and project root, run the backend entrypoint, and
20
+ * have been orphaned to PID 1. Ordinary launchers, shells, and app-server generations do not match.
21
+ */
22
+ export declare function reapOrphanBackendInstances(root?: string): ProcBackendCandidate[];
13
23
  export declare function spawnDetachedRuntime(opts: {
14
24
  cwd: string;
15
25
  logFile: string;
@@ -19,3 +29,4 @@ export declare function spawnDetachedRuntime(opts: {
19
29
  args: string[];
20
30
  env?: NodeJS.ProcessEnv;
21
31
  }): ProcessIdentity;
32
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { spawn } from 'node:child_process';
3
- import { closeSync, mkdirSync, openSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { closeSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { repoRoot } from '@spexcode/spec-core';
6
6
  import { readJsonConfig, runtimeRoot } from '@spexcode/spec-core';
@@ -47,6 +47,84 @@ export function unregisterBackendInstance(instanceId, pid = process.pid) {
47
47
  }
48
48
  catch { /* not ours / already removed */ }
49
49
  }
50
+ const procRows = () => {
51
+ try {
52
+ return readdirSync('/proc').filter((name) => /^\d+$/.test(name)).map(Number);
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ };
58
+ const procEnv = (pid) => {
59
+ try {
60
+ return Object.fromEntries(readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0').flatMap((entry) => {
61
+ const split = entry.indexOf('=');
62
+ return split > 0 ? [[entry.slice(0, split), entry.slice(split + 1)]] : [];
63
+ }));
64
+ }
65
+ catch {
66
+ return {};
67
+ }
68
+ };
69
+ const procCommand = (pid) => {
70
+ try {
71
+ return readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\0', ' ').trim();
72
+ }
73
+ catch {
74
+ return '';
75
+ }
76
+ };
77
+ const procParent = (pid) => {
78
+ try {
79
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
80
+ const close = stat.lastIndexOf(')');
81
+ const fields = stat.slice(close + 2).split(' ');
82
+ return Number(fields[1]) || null;
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ };
88
+ const isBackendCommand = (command) => /(?:^|\/)spec-cli\/(?:src\/index\.ts|dist\/index\.js)(?:\s|$)/.test(command);
89
+ /**
90
+ * Reap only backend children whose supervisor identity is provably stale. A dead registry PID alone is not
91
+ * enough: the live process must carry the exact instance id and project root, run the backend entrypoint, and
92
+ * have been orphaned to PID 1. Ordinary launchers, shells, and app-server generations do not match.
93
+ */
94
+ export function reapOrphanBackendInstances(root = repoRoot()) {
95
+ const records = readBackendInstanceRecords(root);
96
+ const liveCurrent = records.filter((record) => processStartToken(record.pid) === record.startToken);
97
+ const currentIds = new Set(liveCurrent.map((record) => record.instanceId));
98
+ const candidates = [];
99
+ for (const record of records) {
100
+ if (currentIds.has(record.instanceId))
101
+ continue;
102
+ if (processStartToken(record.pid) === record.startToken)
103
+ continue;
104
+ for (const pid of procRows()) {
105
+ const env = procEnv(pid);
106
+ if (env.SPEXCODE_INSTANCE_ID !== record.instanceId || env.SPEXCODE_PROJECT_ROOT !== root)
107
+ continue;
108
+ if (procParent(pid) !== 1)
109
+ continue;
110
+ const command = procCommand(pid);
111
+ if (!isBackendCommand(command))
112
+ continue;
113
+ candidates.push({ ...record, candidatePid: pid, command });
114
+ }
115
+ }
116
+ for (const candidate of candidates) {
117
+ try {
118
+ process.kill(candidate.candidatePid, 'SIGTERM');
119
+ }
120
+ catch { /* exact process already exited */ }
121
+ try {
122
+ rmSync(backendInstancePath(candidate.instanceId), { force: true });
123
+ }
124
+ catch { /* stale record is advisory */ }
125
+ }
126
+ return candidates;
127
+ }
50
128
  export function spawnDetachedRuntime(opts) {
51
129
  mkdirSync(dirname(opts.logFile), { recursive: true });
52
130
  mkdirSync(dirname(opts.pidFile), { recursive: true });
@@ -0,0 +1,23 @@
1
+ import { type CommittedSessionChange, type ProductionSessionApplication } from '@spexcode/session-application';
2
+ type SessionApplicationCommitWake = (recipients: readonly string[]) => void;
3
+ /** The application owns commit ordering; Spex supplies the adopter transport wake. */
4
+ export declare function setSessionApplicationCommitWake(wake: SessionApplicationCommitWake): void;
5
+ /** Allow the adopter to refresh its board stream after the canonical transaction commits. */
6
+ export declare function setSessionApplicationCommitObserver(observer: (change: Pick<CommittedSessionChange, 'recipients'>) => void): void;
7
+ /** The backend's sole session application composition. Path selection is shared with self-launch. */
8
+ export declare function configuredSessionApplication(): ProductionSessionApplication;
9
+ export type SessionApplicationCutoverState = 'ready' | 'residue' | 'fresh' | 'migration-required' | 'fenced' | 'ambiguous';
10
+ /** Inspect cutover state without initializing a fresh database as a side effect of a rejected request. */
11
+ export declare function sessionApplicationCutoverState(): SessionApplicationCutoverState;
12
+ /** Resolve the canonical application. A legacy tree is cut over at its first canonical access; runtime code
13
+ * never gets to choose between JSON and SQLite based on the caller or request path. */
14
+ export declare function configuredSessionApplicationIfCutover(): ProductionSessionApplication | undefined;
15
+ /** Initialize a fresh canonical store only after an accepted create has crossed its no-side-effect boundary. */
16
+ export declare function initializeFreshSessionApplication(): ProductionSessionApplication;
17
+ export declare function acquireFreshSessionApplicationForCreate(): {
18
+ application: ProductionSessionApplication;
19
+ owned: boolean;
20
+ };
21
+ export declare function releaseFreshSessionApplicationForCreate(owned: boolean, committed: boolean): void;
22
+ export declare function resetConfiguredSessionApplicationForTest(): void;
23
+ export {};
@@ -0,0 +1,189 @@
1
+ import { existsSync, readdirSync, rmSync, unlinkSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { openProjectSessionApplication, legacyResidueExists, migrateJsonSessionRecords, } from '@spexcode/session-application';
4
+ import { requireLocalDatabasePath, resolveDatabasePath } from '@spexcode/session-selflaunch';
5
+ import { runtimeRoot } from '@spexcode/spec-core';
6
+ import { jsonMigrationFencePath } from '@spexcode/session-application';
7
+ let cached;
8
+ let cachedPath;
9
+ let freshStoreOwned = false;
10
+ let freshStoreCommitted = false;
11
+ let freshStoreLeases = 0;
12
+ let commitWake = () => { };
13
+ let commitObserver;
14
+ /** The application owns commit ordering; Spex supplies the adopter transport wake. */
15
+ export function setSessionApplicationCommitWake(wake) {
16
+ commitWake = wake;
17
+ }
18
+ /** Allow the adopter to refresh its board stream after the canonical transaction commits. */
19
+ export function setSessionApplicationCommitObserver(observer) {
20
+ commitObserver = observer;
21
+ }
22
+ /** The backend's sole session application composition. Path selection is shared with self-launch. */
23
+ export function configuredSessionApplication() {
24
+ const databasePath = resolveDatabasePath();
25
+ if (cached !== undefined && cachedPath === databasePath)
26
+ return cached;
27
+ if (cached !== undefined)
28
+ cached.close();
29
+ cached = openProjectSessionApplication({
30
+ databasePath,
31
+ locality: path => { requireLocalDatabasePath(path); },
32
+ onCommitted: result => {
33
+ commitWake(result.recipients);
34
+ if (commitObserver)
35
+ setImmediate(() => commitObserver?.(result));
36
+ },
37
+ onRuntimeBound: sessionId => commitWake([sessionId]),
38
+ });
39
+ cachedPath = databasePath;
40
+ return cached;
41
+ }
42
+ // A marked store is `ready` only once its legacy tree holds nothing left to absorb. The scan runs once per process
43
+ // per store: after residue is migrated (or none was found) no legacy writer exists to create more, so later calls
44
+ // answer from memory instead of walking every session directory on the hot path.
45
+ const settledResidueStores = new Set();
46
+ /** Inspect cutover state without initializing a fresh database as a side effect of a rejected request. */
47
+ export function sessionApplicationCutoverState() {
48
+ const databasePath = resolveDatabasePath();
49
+ const recordsRoot = join(runtimeRoot(), 'sessions');
50
+ const storeKey = `${databasePath}\0${recordsRoot}`;
51
+ if (existsSync(`${databasePath}.json-migration.json`)) {
52
+ if (settledResidueStores.has(storeKey))
53
+ return 'ready';
54
+ if (legacyResidueExists(recordsRoot))
55
+ return 'residue';
56
+ settledResidueStores.add(storeKey);
57
+ return 'ready';
58
+ }
59
+ if (existsSync(jsonMigrationFencePath(recordsRoot)))
60
+ return 'fenced';
61
+ if (existsSync(recordsRoot) && readdirSync(recordsRoot, { withFileTypes: true }).some(entry => entry.isDirectory()))
62
+ return 'migration-required';
63
+ if (existsSync(databasePath))
64
+ return 'ambiguous';
65
+ return 'fresh';
66
+ }
67
+ /** Resolve the canonical application. A legacy tree is cut over at its first canonical access; runtime code
68
+ * never gets to choose between JSON and SQLite based on the caller or request path. */
69
+ export function configuredSessionApplicationIfCutover() {
70
+ const state = sessionApplicationCutoverState();
71
+ if (state === 'ready')
72
+ return configuredSessionApplication();
73
+ if (state === 'migration-required' || state === 'residue')
74
+ return initializeMigratedSessionApplication(state);
75
+ if (state === 'fresh')
76
+ return initializeFreshSessionApplication();
77
+ if (state === 'fenced' || state === 'ambiguous') {
78
+ throw new Error(`session application cutover is ${state}; refusing a legacy/runtime split`);
79
+ }
80
+ return undefined;
81
+ }
82
+ // One importer for both shapes of legacy tree. Before the marker it installs the canonical store; after the
83
+ // marker it absorbs whatever residue the tree still holds into the store this process already owns, then
84
+ // retires the tree. Either way the tree is gone when this returns, so the cutover state settles to `ready`.
85
+ function initializeMigratedSessionApplication(state) {
86
+ const databasePath = resolveDatabasePath();
87
+ const recordsRoot = join(runtimeRoot(), 'sessions');
88
+ const locality = (path) => { requireLocalDatabasePath(path); };
89
+ if (state === 'migration-required') {
90
+ migrateJsonSessionRecords({ databasePath, recordsRoot, locality });
91
+ return configuredSessionApplication();
92
+ }
93
+ const application = configuredSessionApplication();
94
+ const report = migrateJsonSessionRecords({ databasePath, recordsRoot, locality, application });
95
+ settledResidueStores.add(`${databasePath}\0${recordsRoot}`);
96
+ const residue = report.residue;
97
+ if (residue) {
98
+ console.log(`[session-application] migrated legacy residue from ${recordsRoot}: ${residue.records} record(s), ${residue.events} event(s), ${residue.parentEdges} parent edge(s), ${residue.watchEdges} watch edge(s), ${residue.pending} pending; ${residue.unclaimed.length} unclaimed dir(s)${residue.unclaimed.length ? ` (${residue.unclaimed.join(', ')})` : ''}; backup ${residue.backupRoot}`);
99
+ }
100
+ return application;
101
+ }
102
+ /** Initialize a fresh canonical store only after an accepted create has crossed its no-side-effect boundary. */
103
+ export function initializeFreshSessionApplication() {
104
+ const state = sessionApplicationCutoverState();
105
+ if (state === 'ready')
106
+ return configuredSessionApplication();
107
+ if (state === 'residue')
108
+ return initializeMigratedSessionApplication(state);
109
+ if (state !== 'fresh')
110
+ throw new Error(`cannot initialize a fresh session store from cutover state: ${state}`);
111
+ const databasePath = resolveDatabasePath();
112
+ const recordsRoot = join(runtimeRoot(), 'sessions');
113
+ migrateJsonSessionRecords({ databasePath, recordsRoot, locality: path => { requireLocalDatabasePath(path); } });
114
+ return configuredSessionApplication();
115
+ }
116
+ export function acquireFreshSessionApplicationForCreate() {
117
+ const state = sessionApplicationCutoverState();
118
+ if (state === 'fresh') {
119
+ const application = initializeFreshSessionApplication();
120
+ freshStoreOwned = true;
121
+ freshStoreCommitted = false;
122
+ freshStoreLeases++;
123
+ return { application, owned: true };
124
+ }
125
+ if (state === 'ready' && freshStoreOwned && !freshStoreCommitted) {
126
+ freshStoreLeases++;
127
+ return { application: configuredSessionApplication(), owned: true };
128
+ }
129
+ if (state === 'residue')
130
+ return { application: initializeMigratedSessionApplication(state), owned: false };
131
+ if (state !== 'ready')
132
+ throw new Error(`cannot initialize a session store from cutover state: ${state}`);
133
+ return { application: configuredSessionApplication(), owned: false };
134
+ }
135
+ export function releaseFreshSessionApplicationForCreate(owned, committed) {
136
+ if (!owned)
137
+ return;
138
+ if (committed)
139
+ freshStoreCommitted = true;
140
+ freshStoreLeases = Math.max(0, freshStoreLeases - 1);
141
+ if (freshStoreLeases !== 0)
142
+ return;
143
+ if (freshStoreCommitted) {
144
+ try {
145
+ unlinkSync(jsonMigrationFencePath(join(runtimeRoot(), 'sessions')));
146
+ }
147
+ catch (error) {
148
+ if (error.code !== 'ENOENT')
149
+ throw error;
150
+ }
151
+ return;
152
+ }
153
+ const application = cached;
154
+ const rows = application?.protocol.withTransaction(tx => Number(tx.query('SELECT COUNT(*) AS count FROM session_application_state')[0]?.count ?? 0)) ?? 0;
155
+ if (rows !== 0) {
156
+ freshStoreCommitted = true;
157
+ try {
158
+ unlinkSync(jsonMigrationFencePath(join(runtimeRoot(), 'sessions')));
159
+ }
160
+ catch (error) {
161
+ if (error.code !== 'ENOENT')
162
+ throw error;
163
+ }
164
+ return;
165
+ }
166
+ application?.close();
167
+ cached = undefined;
168
+ const databasePath = resolveDatabasePath();
169
+ const recordsRoot = join(runtimeRoot(), 'sessions');
170
+ for (const path of [databasePath, `${databasePath}.json-migration.json`, jsonMigrationFencePath(recordsRoot)]) {
171
+ try {
172
+ unlinkSync(path);
173
+ }
174
+ catch (error) {
175
+ if (error.code !== 'ENOENT')
176
+ throw error;
177
+ }
178
+ }
179
+ rmSync(`${databasePath}.json-migration-backup`, { recursive: true, force: true });
180
+ freshStoreOwned = false;
181
+ }
182
+ export function resetConfiguredSessionApplicationForTest() {
183
+ cached?.close();
184
+ cached = undefined;
185
+ settledResidueStores.clear();
186
+ freshStoreOwned = false;
187
+ freshStoreCommitted = false;
188
+ freshStoreLeases = 0;
189
+ }
@@ -27,7 +27,11 @@ export async function sessionStateKit(sessionId) {
27
27
  // the agent-authored state writers resolve WHICH session by id: a `--session <id>` flag (the lifecycle
28
28
  // hooks pass it, parsed from the payload, since they no longer have a cwd `.session`) wins, else the
29
29
  // harness env var (ownSessionId — the agent's own `spex session …` carries the harness session id).
30
- const sess = sessionId;
30
+ // Hook payloads from Codex carry the native thread id, while the canonical
31
+ // application is keyed by the Spex session id. Resolve that alias once at
32
+ // the state-writer boundary; shell hooks stay transport-only and every
33
+ // lifecycle producer writes the same canonical record.
34
+ const sess = sessionId ? (l.readAliasedRawRecord(sessionId)?.session_id ?? sessionId) : undefined;
31
35
  // @@@ no-record diagnosis ([[state]]) - the session store resolves from the CURRENT directory (runtimeRoot
32
36
  // ← the cwd's git common dir), so the classic declaration failure is a cd OUTSIDE the session's project —
33
37
  // and a bare "no session record" told the author none of that (field-reported). Name the actual cause and
@@ -99,6 +103,14 @@ export async function runSessionDeclaration(verb, argv = process.argv) {
99
103
  if (proposal === 'nothing')
100
104
  nothingProposalTrap();
101
105
  const { s, sess, mark, noRecord, noteEcho } = await sessionStateKit(sessionId);
106
+ if (proposal === 'merge') {
107
+ const readiness = s.mergeReadiness('merge');
108
+ if (!readiness.ready) {
109
+ console.error(`review declaration refused: ${readiness.reason}`);
110
+ process.exitCode = 1;
111
+ return;
112
+ }
113
+ }
102
114
  let closeNote = proposal === 'close' ? CLOSE_CLEANUP : '';
103
115
  if (proposal === 'close') {
104
116
  // The closeout nudge is advisory: a declaration must land even if the local issue store fails.
@@ -6,6 +6,12 @@ type SessionFileLock = <T>(id: string, body: () => T) => T;
6
6
  export declare const SESSION_FILE_PREVIEW_MAX_BYTES: number;
7
7
  export type SessionFilePreviewKind = 'text' | 'image' | 'html';
8
8
  export declare function listSessionFiles(id: string): string[];
9
+ export type SessionFileStatus = {
10
+ path: string;
11
+ valid: boolean;
12
+ reason?: string;
13
+ };
14
+ export declare function inspectSessionFiles(id: string): SessionFileStatus[];
9
15
  export declare function readSessionFiles(id: string): string[];
10
16
  export declare function addSessionFile(id: string, input: string, lock: SessionFileLock, cwd?: string): {
11
17
  path: string;
@@ -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
+ }