gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27

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 (212) hide show
  1. package/README.md +5 -7
  2. package/dist/help-text.js +1 -1
  3. package/dist/resource-loader.js +6 -1
  4. package/dist/resources/.managed-resources-content-hash +1 -1
  5. package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
  6. package/dist/resources/extensions/gsd/auto/loop.js +235 -36
  7. package/dist/resources/extensions/gsd/auto/phases.js +14 -7
  8. package/dist/resources/extensions/gsd/auto/session.js +36 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
  11. package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
  12. package/dist/resources/extensions/gsd/auto.js +139 -49
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  16. package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
  17. package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
  18. package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
  19. package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
  20. package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
  21. package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
  22. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  23. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  24. package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  25. package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
  26. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
  27. package/dist/resources/extensions/gsd/doctor.js +12 -2
  28. package/dist/resources/extensions/gsd/gsd-db.js +355 -3
  29. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  30. package/dist/resources/extensions/gsd/guided-flow.js +116 -26
  31. package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
  32. package/dist/resources/extensions/gsd/metrics.js +287 -1
  33. package/dist/resources/extensions/gsd/paths.js +79 -8
  34. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  35. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  37. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  38. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  40. package/dist/resources/extensions/gsd/state.js +21 -6
  41. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  42. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  43. package/dist/resources/extensions/gsd/workspace.js +59 -0
  44. package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
  45. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  46. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  47. package/dist/web/standalone/.next/BUILD_ID +1 -1
  48. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  49. package/dist/web/standalone/.next/build-manifest.json +2 -2
  50. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  51. package/dist/web/standalone/.next/required-server-files.json +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.html +1 -1
  69. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  76. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  78. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  79. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  80. package/dist/web/standalone/server.js +1 -1
  81. package/package.json +1 -1
  82. package/packages/mcp-server/README.md +2 -11
  83. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  84. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  85. package/packages/mcp-server/dist/remote-questions.js +28 -0
  86. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  87. package/packages/mcp-server/dist/server.d.ts +28 -0
  88. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  89. package/packages/mcp-server/dist/server.js +94 -4
  90. package/packages/mcp-server/dist/server.js.map +1 -1
  91. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  92. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  93. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  94. package/packages/mcp-server/src/remote-questions.ts +35 -0
  95. package/packages/mcp-server/src/server.ts +129 -6
  96. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  97. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  98. package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
  99. package/src/resources/extensions/gsd/auto/loop.ts +263 -41
  100. package/src/resources/extensions/gsd/auto/phases.ts +15 -7
  101. package/src/resources/extensions/gsd/auto/session.ts +40 -0
  102. package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
  103. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
  104. package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
  105. package/src/resources/extensions/gsd/auto.ts +166 -43
  106. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  107. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
  108. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  109. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  110. package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
  111. package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
  112. package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
  113. package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
  114. package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
  115. package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
  116. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  117. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  118. package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  119. package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
  120. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
  121. package/src/resources/extensions/gsd/doctor.ts +10 -2
  122. package/src/resources/extensions/gsd/gsd-db.ts +354 -3
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  124. package/src/resources/extensions/gsd/guided-flow.ts +152 -26
  125. package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
  126. package/src/resources/extensions/gsd/metrics.ts +321 -1
  127. package/src/resources/extensions/gsd/paths.ts +67 -8
  128. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  129. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  130. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  131. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  134. package/src/resources/extensions/gsd/state.ts +44 -6
  135. package/src/resources/extensions/gsd/templates/project.md +10 -0
  136. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  137. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
  138. package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
  139. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  140. package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
  141. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  142. package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
  143. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
  144. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
  145. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  146. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  147. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  148. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  149. package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
  150. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  151. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  152. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  153. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  154. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  155. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  156. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  157. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  158. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  159. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  160. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
  161. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
  162. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
  163. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
  164. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
  165. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
  166. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
  167. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  168. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  169. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  170. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  171. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  172. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  173. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
  174. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  175. package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
  176. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  177. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  178. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  179. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
  180. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  181. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
  182. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
  183. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  184. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
  185. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  186. package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
  187. package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
  188. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
  189. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
  190. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  192. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
  193. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  194. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
  196. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
  197. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  198. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  199. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  200. package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
  201. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  202. package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
  203. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  204. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  205. package/src/resources/extensions/gsd/workspace.ts +95 -0
  206. package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
  207. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  208. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
  209. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
  210. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
  211. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
  212. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
@@ -0,0 +1,141 @@
1
+ // gsd-2 + Command queue tests (Phase B coordination — IPC inbox + broadcast NULL semantics)
2
+
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+
9
+ import { openDatabase, closeDatabase } from "../gsd-db.ts";
10
+ import {
11
+ enqueueCommand,
12
+ claimNextCommand,
13
+ completeCommand,
14
+ getCommand,
15
+ } from "../db/command-queue.ts";
16
+
17
+ function makeBase(): string {
18
+ const base = mkdtempSync(join(tmpdir(), "gsd-cmd-q-"));
19
+ mkdirSync(join(base, ".gsd"), { recursive: true });
20
+ return base;
21
+ }
22
+
23
+ function cleanup(base: string): void {
24
+ try { closeDatabase(); } catch { /* noop */ }
25
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
26
+ }
27
+
28
+ test("enqueue + claim + complete round-trip for targeted command", (t) => {
29
+ const base = makeBase();
30
+ t.after(() => cleanup(base));
31
+ openDatabase(join(base, ".gsd", "gsd.db"));
32
+
33
+ const id = enqueueCommand({
34
+ targetWorker: "worker-A",
35
+ command: "cancel",
36
+ args: { reason: "user-request" },
37
+ });
38
+ assert.ok(id > 0);
39
+
40
+ const claimed = claimNextCommand("worker-A");
41
+ assert.ok(claimed);
42
+ assert.equal(claimed!.id, id);
43
+ assert.equal(claimed!.command, "cancel");
44
+ assert.equal(claimed!.claimed_by, "worker-A");
45
+ assert.ok(claimed!.claimed_at);
46
+
47
+ completeCommand(id, "worker-A", { acknowledged: true });
48
+ const final = getCommand(id);
49
+ assert.ok(final!.completed_at);
50
+ assert.equal(final!.result_json, JSON.stringify({ acknowledged: true }));
51
+ });
52
+
53
+ test("targeted command is invisible to other workers", (t) => {
54
+ const base = makeBase();
55
+ t.after(() => cleanup(base));
56
+ openDatabase(join(base, ".gsd", "gsd.db"));
57
+
58
+ enqueueCommand({ targetWorker: "worker-A", command: "for-A" });
59
+ const wrong = claimNextCommand("worker-B");
60
+ assert.equal(wrong, null, "worker-B sees nothing for worker-A");
61
+
62
+ const right = claimNextCommand("worker-A");
63
+ assert.ok(right);
64
+ assert.equal(right!.command, "for-A");
65
+ });
66
+
67
+ test("broadcast command (target=null) is visible to ANY worker, claimed exactly once", (t) => {
68
+ const base = makeBase();
69
+ t.after(() => cleanup(base));
70
+ openDatabase(join(base, ".gsd", "gsd.db"));
71
+
72
+ enqueueCommand({ targetWorker: null, command: "broadcast-cancel" });
73
+
74
+ const a = claimNextCommand("worker-A");
75
+ assert.ok(a, "first poller wins");
76
+ assert.equal(a!.command, "broadcast-cancel");
77
+
78
+ // Second poller (different worker) sees nothing — broadcast is single-delivery
79
+ const b = claimNextCommand("worker-B");
80
+ assert.equal(b, null);
81
+ });
82
+
83
+ test("oldest-first ordering across mixed targeted + broadcast queue", (t) => {
84
+ const base = makeBase();
85
+ t.after(() => cleanup(base));
86
+ openDatabase(join(base, ".gsd", "gsd.db"));
87
+
88
+ enqueueCommand({ targetWorker: null, command: "first" });
89
+ enqueueCommand({ targetWorker: "worker-A", command: "second" });
90
+ enqueueCommand({ targetWorker: null, command: "third" });
91
+
92
+ const c1 = claimNextCommand("worker-A")!;
93
+ const c2 = claimNextCommand("worker-A")!;
94
+ const c3 = claimNextCommand("worker-A")!;
95
+ assert.equal(c1.command, "first");
96
+ assert.equal(c2.command, "second");
97
+ assert.equal(c3.command, "third");
98
+ assert.equal(claimNextCommand("worker-A"), null);
99
+ });
100
+
101
+ test("completeCommand is idempotent — second call does not overwrite", (t) => {
102
+ const base = makeBase();
103
+ t.after(() => cleanup(base));
104
+ openDatabase(join(base, ".gsd", "gsd.db"));
105
+
106
+ const id = enqueueCommand({ targetWorker: "w", command: "x" });
107
+ claimNextCommand("w");
108
+ completeCommand(id, "w", { result: 1 });
109
+ completeCommand(id, "w", { result: 2 }); // second call should no-op
110
+ const row = getCommand(id)!;
111
+ assert.equal(row.result_json, JSON.stringify({ result: 1 }));
112
+ });
113
+
114
+ test("completed commands cannot be reclaimed or completed by a different worker", (t) => {
115
+ const base = makeBase();
116
+ t.after(() => cleanup(base));
117
+ openDatabase(join(base, ".gsd", "gsd.db"));
118
+
119
+ const id = enqueueCommand({ targetWorker: "worker-A", command: "x" });
120
+ const claimed = claimNextCommand("worker-A");
121
+ assert.ok(claimed);
122
+
123
+ completeCommand(id, "worker-A", { result: 1 });
124
+ completeCommand(id, "worker-B", { result: 2 });
125
+
126
+ assert.equal(claimNextCommand("worker-A"), null);
127
+ const row = getCommand(id)!;
128
+ assert.equal(row.result_json, JSON.stringify({ result: 1 }));
129
+ });
130
+
131
+ test("completeCommand does not complete an unclaimed command", (t) => {
132
+ const base = makeBase();
133
+ t.after(() => cleanup(base));
134
+ openDatabase(join(base, ".gsd", "gsd.db"));
135
+
136
+ const id = enqueueCommand({ targetWorker: "w", command: "x" });
137
+ completeCommand(id, "w", { result: 1 });
138
+ const row = getCommand(id)!;
139
+ assert.equal(row.completed_at, null);
140
+ assert.equal(row.result_json, null);
141
+ });
@@ -0,0 +1,203 @@
1
+ // gsd-2 + Crash recovery via DB (Phase C pt 2 — auto.lock migration)
2
+ //
3
+ // auto.lock file IO is gone. readCrashLock now synthesizes a LockData
4
+ // from the workers + unit_dispatches + runtime_kv tables. These tests
5
+ // verify the synthesis end-to-end: register a worker, simulate it going
6
+ // stale (heartbeat lapsed), and confirm readCrashLock returns the
7
+ // correct LockData with PID, started_at, unit details, and session
8
+ // file derived from the DB.
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ import {
17
+ openDatabase,
18
+ closeDatabase,
19
+ insertMilestone,
20
+ _getAdapter,
21
+ } from "../gsd-db.ts";
22
+ import { registerAutoWorker } from "../db/auto-workers.ts";
23
+ import { claimMilestoneLease } from "../db/milestone-leases.ts";
24
+ import { recordDispatchClaim } from "../db/unit-dispatches.ts";
25
+ import { setRuntimeKv, getRuntimeKv } from "../db/runtime-kv.ts";
26
+ import {
27
+ writeLock,
28
+ readCrashLock,
29
+ clearLock,
30
+ isLockProcessAlive,
31
+ } from "../crash-recovery.ts";
32
+ import { normalizeRealPath } from "../paths.ts";
33
+
34
+ function makeBase(): string {
35
+ const base = mkdtempSync(join(tmpdir(), "gsd-crash-recovery-"));
36
+ mkdirSync(join(base, ".gsd"), { recursive: true });
37
+ return base;
38
+ }
39
+
40
+ function cleanup(base: string): void {
41
+ try { closeDatabase(); } catch { /* noop */ }
42
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
43
+ }
44
+
45
+ /** Force a worker's last_heartbeat_at into the past so the stale-detector picks it up. */
46
+ function expireWorker(workerId: string): void {
47
+ const db = _getAdapter()!;
48
+ db.prepare(
49
+ `UPDATE workers SET last_heartbeat_at = '1970-01-01T00:00:00.000Z' WHERE worker_id = :w`,
50
+ ).run({ ":w": workerId });
51
+ }
52
+
53
+ function setWorkerPid(workerId: string, pid: number): void {
54
+ const db = _getAdapter()!;
55
+ db.prepare(
56
+ `UPDATE workers SET pid = :pid WHERE worker_id = :w`,
57
+ ).run({ ":pid": pid, ":w": workerId });
58
+ }
59
+
60
+ test("readCrashLock returns null when no workers exist", (t) => {
61
+ const base = makeBase();
62
+ t.after(() => cleanup(base));
63
+ openDatabase(join(base, ".gsd", "gsd.db"));
64
+ assert.equal(readCrashLock(base), null);
65
+ });
66
+
67
+ test("readCrashLock returns null when only fresh (un-expired) workers exist", (t) => {
68
+ const base = makeBase();
69
+ t.after(() => cleanup(base));
70
+ openDatabase(join(base, ".gsd", "gsd.db"));
71
+ registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
72
+ // Heartbeat is fresh — not stale yet.
73
+ assert.equal(readCrashLock(base), null);
74
+ });
75
+
76
+ test("readCrashLock ignores a stale heartbeat when the worker PID is still alive", (t) => {
77
+ const base = makeBase();
78
+ t.after(() => cleanup(base));
79
+ openDatabase(join(base, ".gsd", "gsd.db"));
80
+ const projectRoot = normalizeRealPath(base);
81
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
82
+ expireWorker(workerId);
83
+
84
+ assert.equal(readCrashLock(base), null);
85
+ });
86
+
87
+ test("readCrashLock synthesizes LockData from a stale dead worker (no dispatches yet)", (t) => {
88
+ const base = makeBase();
89
+ t.after(() => cleanup(base));
90
+ openDatabase(join(base, ".gsd", "gsd.db"));
91
+ const projectRoot = normalizeRealPath(base);
92
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
93
+ setWorkerPid(workerId, 99999);
94
+ expireWorker(workerId);
95
+
96
+ const lock = readCrashLock(base);
97
+ assert.ok(lock, "stale worker surfaced as a crash lock");
98
+ assert.equal(lock!.pid, 99999);
99
+ // Bootstrap default — no dispatches recorded
100
+ assert.equal(lock!.unitType, "starting");
101
+ assert.equal(lock!.unitId, "bootstrap");
102
+ assert.ok(lock!.startedAt, "startedAt populated from workers.started_at");
103
+ });
104
+
105
+ test("readCrashLock includes the most recent dispatch as unitType/unitId", (t) => {
106
+ const base = makeBase();
107
+ t.after(() => cleanup(base));
108
+ openDatabase(join(base, ".gsd", "gsd.db"));
109
+ insertMilestone({ id: "M001", title: "T", status: "active" });
110
+ const projectRoot = normalizeRealPath(base);
111
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
112
+ const lease = claimMilestoneLease(workerId, "M001");
113
+ assert.equal(lease.ok, true);
114
+ if (!lease.ok) return;
115
+ recordDispatchClaim({
116
+ traceId: "t1", workerId, milestoneLeaseToken: lease.token,
117
+ milestoneId: "M001", unitType: "plan-slice", unitId: "M001/S01",
118
+ });
119
+ setWorkerPid(workerId, 99999);
120
+ expireWorker(workerId);
121
+
122
+ const lock = readCrashLock(base);
123
+ assert.ok(lock);
124
+ assert.equal(lock!.unitType, "plan-slice");
125
+ assert.equal(lock!.unitId, "M001/S01");
126
+ });
127
+
128
+ test("readCrashLock surfaces sessionFile from runtime_kv", (t) => {
129
+ const base = makeBase();
130
+ t.after(() => cleanup(base));
131
+ openDatabase(join(base, ".gsd", "gsd.db"));
132
+ const projectRoot = normalizeRealPath(base);
133
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
134
+ setRuntimeKv("worker", workerId, "session_file", "/tmp/pi-session-abc.jsonl");
135
+ setWorkerPid(workerId, 99999);
136
+ expireWorker(workerId);
137
+
138
+ const lock = readCrashLock(base);
139
+ assert.ok(lock);
140
+ assert.equal(lock!.sessionFile, "/tmp/pi-session-abc.jsonl");
141
+ });
142
+
143
+ test("isLockProcessAlive returns true for the current process", () => {
144
+ const lock = {
145
+ pid: process.pid,
146
+ startedAt: new Date().toISOString(),
147
+ unitType: "starting",
148
+ unitId: "bootstrap",
149
+ unitStartedAt: new Date().toISOString(),
150
+ };
151
+ assert.equal(isLockProcessAlive(lock), true);
152
+ });
153
+
154
+ test("isLockProcessAlive returns false for a dead PID", () => {
155
+ // PID 99999 is essentially guaranteed dead on a fresh test box.
156
+ const lock = {
157
+ pid: 99999,
158
+ startedAt: new Date().toISOString(),
159
+ unitType: "starting",
160
+ unitId: "bootstrap",
161
+ unitStartedAt: new Date().toISOString(),
162
+ };
163
+ assert.equal(isLockProcessAlive(lock), false);
164
+ });
165
+
166
+ test("writeLock stores the session_file in runtime_kv (worker scope)", (t) => {
167
+ const base = makeBase();
168
+ t.after(() => cleanup(base));
169
+ openDatabase(join(base, ".gsd", "gsd.db"));
170
+ const projectRoot = normalizeRealPath(base);
171
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
172
+
173
+ writeLock(base, "plan-slice", "M001/S01", "/tmp/session-xyz.jsonl");
174
+
175
+ // Verify the value was written for the live worker.
176
+ const stored = getRuntimeKv<string>("worker", workerId, "session_file");
177
+ assert.equal(stored, "/tmp/session-xyz.jsonl");
178
+
179
+ // Confirm a stale read picks it up via readCrashLock.
180
+ setWorkerPid(workerId, 99999);
181
+ expireWorker(workerId);
182
+ const lock = readCrashLock(base);
183
+ assert.ok(lock);
184
+ assert.equal(lock!.sessionFile, "/tmp/session-xyz.jsonl");
185
+ });
186
+
187
+ test("clearLock removes the session_file row for the active worker", (t) => {
188
+ const base = makeBase();
189
+ t.after(() => cleanup(base));
190
+ openDatabase(join(base, ".gsd", "gsd.db"));
191
+ const projectRoot = normalizeRealPath(base);
192
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
193
+
194
+ writeLock(base, "plan-slice", "M001/S01", "/tmp/session-xyz.jsonl");
195
+ assert.equal(getRuntimeKv("worker", workerId, "session_file"), "/tmp/session-xyz.jsonl");
196
+
197
+ // clearLock operates on the active worker (this process) — must run
198
+ // BEFORE expiring the heartbeat, mirroring stopAuto's order: clearLock
199
+ // → markWorkerStopping → done.
200
+ clearLock(base);
201
+ assert.equal(getRuntimeKv("worker", workerId, "session_file"), null,
202
+ "session_file row deleted by clearLock");
203
+ });
@@ -18,39 +18,96 @@ import {
18
18
  hasResumableDerivedState,
19
19
  isBootstrapCrashLock,
20
20
  readPausedSessionMetadata,
21
+ PAUSED_SESSION_KV_KEY,
21
22
  } from "../interrupted-session.ts";
22
- import { gsdRoot } from "../paths.ts";
23
+ import {
24
+ openDatabase,
25
+ closeDatabase,
26
+ insertMilestone,
27
+ _getAdapter,
28
+ } from "../gsd-db.ts";
29
+ import { registerAutoWorker } from "../db/auto-workers.ts";
30
+ import { claimMilestoneLease } from "../db/milestone-leases.ts";
31
+ import { recordDispatchClaim } from "../db/unit-dispatches.ts";
32
+ import { insertSlice, insertTask } from "../gsd-db.ts";
33
+ import { setRuntimeKv } from "../db/runtime-kv.ts";
34
+ import { normalizeRealPath } from "../paths.ts";
23
35
  import type { GSDState } from "../types.ts";
24
36
  import { _synthesizePausedSessionRecoveryForTest } from "../auto.ts";
25
37
 
26
38
  function makeTmpBase(): string {
27
39
  const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
28
40
  mkdirSync(join(base, ".gsd"), { recursive: true });
41
+ // Phase C pt 2: lock and paused-session live in the DB now. Open it
42
+ // for every test base so the helpers below can write through.
43
+ openDatabase(join(base, ".gsd", "gsd.db"));
29
44
  return base;
30
45
  }
31
46
 
32
47
  function cleanup(base: string): void {
48
+ try { closeDatabase(); } catch { /* */ }
33
49
  try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
34
50
  }
35
51
 
52
+ /**
53
+ * Phase C pt 2 fixture: insert a stale worker row + dispatch + session_file
54
+ * directly via SQL so it appears as a crashed PEER process, not as the
55
+ * current test process. assessInterruptedSession filters out
56
+ * `rawLock.pid === process.pid` to avoid classifying its own process as
57
+ * a previous crash; using PID 999999999 (functionally guaranteed dead)
58
+ * bypasses that guard exactly the way the old file-based writeTestLock
59
+ * did with the same PID.
60
+ */
36
61
  function writeTestLock(
37
62
  base: string,
38
63
  unitType: string,
39
64
  unitId: string,
40
65
  sessionFile?: string,
41
66
  ): void {
42
- writeFileSync(
43
- join(gsdRoot(base), "auto.lock"),
44
- JSON.stringify({
45
- pid: 999999999,
46
- startedAt: new Date().toISOString(),
47
- unitType,
48
- unitId,
49
- unitStartedAt: new Date().toISOString(),
50
- sessionFile,
51
- }, null, 2),
52
- "utf-8",
53
- );
67
+ const projectRoot = normalizeRealPath(base);
68
+ const workerId = `test-fake-${randomUUID().slice(0, 8)}`;
69
+ const fakePid = 999999999;
70
+ const stalePast = "1970-01-01T00:00:00.000Z";
71
+ const db = _getAdapter()!;
72
+ db.prepare(
73
+ `INSERT INTO workers (
74
+ worker_id, host, pid, started_at, version,
75
+ last_heartbeat_at, status, project_root_realpath
76
+ ) VALUES (
77
+ :w, 'test-host', :pid, :started_at, 'test',
78
+ :stale, 'active', :project_root
79
+ )`,
80
+ ).run({
81
+ ":w": workerId,
82
+ ":pid": fakePid,
83
+ ":started_at": new Date().toISOString(),
84
+ ":stale": stalePast,
85
+ ":project_root": projectRoot,
86
+ });
87
+
88
+ // Ensure milestones referenced by the unitId exist so the dispatch
89
+ // FK is satisfied. Parse "M###/S##" or "M###" or "starting" / etc.
90
+ const midMatch = unitId.match(/^(M\d+)/);
91
+ if (midMatch && unitType !== "starting") {
92
+ const mid = midMatch[1];
93
+ try { insertMilestone({ id: mid, title: `Test ${mid}`, status: "active" }); }
94
+ catch { /* may already exist */ }
95
+ try {
96
+ const lease = claimMilestoneLease(workerId, mid);
97
+ recordDispatchClaim({
98
+ traceId: randomUUID(),
99
+ workerId,
100
+ milestoneLeaseToken: lease.ok ? lease.token : 0,
101
+ milestoneId: mid,
102
+ unitType,
103
+ unitId,
104
+ });
105
+ } catch { /* ignore — best-effort */ }
106
+ }
107
+
108
+ if (sessionFile) {
109
+ setRuntimeKv("worker", workerId, "session_file", sessionFile);
110
+ }
54
111
  }
55
112
 
56
113
  function writeRoadmap(base: string, checked = false): void {
@@ -82,6 +139,25 @@ function writeRoadmap(base: string, checked = false): void {
82
139
  ].join("\n"),
83
140
  "utf-8",
84
141
  );
142
+ // Phase C pt 2: makeTmpBase() opens the DB so writeTestLock can write
143
+ // the workers row. deriveState then goes DB-first; mirror the markdown
144
+ // fixture into the DB so the assessment sees the same milestone state.
145
+ // Use direct upsert SQL so calling writeRoadmap twice (e.g. once for
146
+ // base + once for a paused worktree) actually flips the status.
147
+ const status = checked ? "complete" : "active";
148
+ const adapter = _getAdapter();
149
+ if (adapter) {
150
+ adapter.prepare(
151
+ `INSERT INTO milestones (id, title, status, created_at)
152
+ VALUES (:id, :title, :status, :now)
153
+ ON CONFLICT(id) DO UPDATE SET status = excluded.status, title = excluded.title`,
154
+ ).run({ ":id": "M001", ":title": "Test Milestone", ":status": status, ":now": new Date().toISOString() });
155
+ adapter.prepare(
156
+ `INSERT INTO slices (milestone_id, id, title, status, created_at)
157
+ VALUES (:mid, :sid, :title, :status, :now)
158
+ ON CONFLICT(milestone_id, id) DO UPDATE SET status = excluded.status, title = excluded.title`,
159
+ ).run({ ":mid": "M001", ":sid": "S01", ":title": "Test slice", ":status": status, ":now": new Date().toISOString() });
160
+ }
85
161
  }
86
162
 
87
163
  function writeCompleteSliceArtifacts(base: string): void {
@@ -105,13 +181,16 @@ function writePausedSession(
105
181
  unitType?: string,
106
182
  unitId?: string,
107
183
  ): void {
108
- const runtimeDir = join(base, ".gsd", "runtime");
109
- mkdirSync(runtimeDir, { recursive: true });
110
- writeFileSync(
111
- join(runtimeDir, "paused-session.json"),
112
- JSON.stringify({ milestoneId, originalBasePath: base, stepMode, worktreePath, unitType, unitId }, null, 2),
113
- "utf-8",
114
- );
184
+ // Phase C pt 2: paused-session.json migrated to runtime_kv
185
+ // (global scope, key PAUSED_SESSION_KV_KEY).
186
+ setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, {
187
+ milestoneId,
188
+ originalBasePath: base,
189
+ stepMode,
190
+ worktreePath,
191
+ unitType,
192
+ unitId,
193
+ });
115
194
  }
116
195
 
117
196
  function writeActivityLog(base: string, entries: Record<string, unknown>[]): void {
@@ -231,14 +310,12 @@ test("readPausedSessionMetadata preserves unitType and unitId through round-trip
231
310
  test("readPausedSessionMetadata handles legacy metadata without unitType/unitId", () => {
232
311
  const base = makeTmpBase();
233
312
  try {
234
- // Write metadata without unitType/unitId (simulates older version)
235
- const runtimeDir = join(base, ".gsd", "runtime");
236
- mkdirSync(runtimeDir, { recursive: true });
237
- writeFileSync(
238
- join(runtimeDir, "paused-session.json"),
239
- JSON.stringify({ milestoneId: "M001", originalBasePath: base }),
240
- "utf-8",
241
- );
313
+ // Phase C pt 2: write directly to runtime_kv (simulates older payload
314
+ // missing the now-canonical unitType/unitId fields).
315
+ setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, {
316
+ milestoneId: "M001",
317
+ originalBasePath: base,
318
+ });
242
319
  const meta = readPausedSessionMetadata(base);
243
320
  assert.equal(meta?.milestoneId, "M001");
244
321
  assert.equal(meta?.unitType, undefined);
@@ -251,23 +328,23 @@ test("readPausedSessionMetadata handles legacy metadata without unitType/unitId"
251
328
  test("readPausedSessionMetadata drops stale discuss-milestone pseudo PROJECT metadata", () => {
252
329
  const base = makeTmpBase();
253
330
  try {
254
- const runtimeDir = join(base, ".gsd", "runtime");
255
- const pausedPath = join(runtimeDir, "paused-session.json");
256
- mkdirSync(runtimeDir, { recursive: true });
257
- writeFileSync(
258
- pausedPath,
259
- JSON.stringify({
260
- milestoneId: null,
261
- originalBasePath: base,
262
- unitType: "discuss-milestone",
263
- unitId: "PROJECT",
264
- }, null, 2),
265
- "utf-8",
266
- );
331
+ // Phase C pt 2: write directly to runtime_kv (the file location is gone)
332
+ setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, {
333
+ milestoneId: null,
334
+ originalBasePath: base,
335
+ unitType: "discuss-milestone",
336
+ unitId: "PROJECT",
337
+ });
267
338
 
268
339
  const meta = readPausedSessionMetadata(base);
269
340
  assert.equal(meta, null);
270
- assert.equal(existsSync(pausedPath), false);
341
+ // Confirm the row was deleted by readPausedSessionMetadata's
342
+ // isStalePseudoMilestonePause branch.
343
+ const adapter = _getAdapter()!;
344
+ const row = adapter.prepare(
345
+ `SELECT 1 FROM runtime_kv WHERE scope = 'global' AND scope_id = '' AND key = :k`,
346
+ ).get({ ":k": PAUSED_SESSION_KV_KEY });
347
+ assert.equal(row, undefined);
271
348
  } finally {
272
349
  cleanup(base);
273
350
  }
@@ -276,23 +353,20 @@ test("readPausedSessionMetadata drops stale discuss-milestone pseudo PROJECT met
276
353
  test("readPausedSessionMetadata drops stale deep setup pseudo-unit metadata", () => {
277
354
  const base = makeTmpBase();
278
355
  try {
279
- const runtimeDir = join(base, ".gsd", "runtime");
280
- const pausedPath = join(runtimeDir, "paused-session.json");
281
- mkdirSync(runtimeDir, { recursive: true });
282
- writeFileSync(
283
- pausedPath,
284
- JSON.stringify({
285
- milestoneId: "WORKFLOW-PREFS",
286
- originalBasePath: base,
287
- unitType: "workflow-preferences",
288
- unitId: "WORKFLOW-PREFS",
289
- }, null, 2),
290
- "utf-8",
291
- );
356
+ setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, {
357
+ milestoneId: "WORKFLOW-PREFS",
358
+ originalBasePath: base,
359
+ unitType: "workflow-preferences",
360
+ unitId: "WORKFLOW-PREFS",
361
+ });
292
362
 
293
363
  const meta = readPausedSessionMetadata(base);
294
364
  assert.equal(meta, null);
295
- assert.equal(existsSync(pausedPath), false);
365
+ const adapter = _getAdapter()!;
366
+ const row = adapter.prepare(
367
+ `SELECT 1 FROM runtime_kv WHERE scope = 'global' AND scope_id = '' AND key = :k`,
368
+ ).get({ ":k": PAUSED_SESSION_KV_KEY });
369
+ assert.equal(row, undefined);
296
370
  } finally {
297
371
  cleanup(base);
298
372
  }
@@ -504,10 +578,29 @@ test("assessInterruptedSession treats bootstrap crash as stale without paused me
504
578
  // ─── writeLock / readCrashLock ────────────────────────────────────────────
505
579
 
506
580
  test("writeLock creates lock file and readCrashLock reads it", (t) => {
581
+ // Phase C pt 2: lock state is reconstructed from workers + unit_dispatches
582
+ // + runtime_kv. The fresh worker is not stale yet — we register, dispatch,
583
+ // write the session_file, then expire the heartbeat to simulate a crash.
507
584
  const base = makeTmpBase();
508
585
  t.after(() => cleanup(base));
509
586
 
587
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
588
+ const projectRoot = normalizeRealPath(base);
589
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
590
+ const lease = claimMilestoneLease(workerId, "M001");
591
+ assert.equal(lease.ok, true);
592
+ if (!lease.ok) return;
593
+ recordDispatchClaim({
594
+ traceId: "t1", workerId, milestoneLeaseToken: lease.token,
595
+ milestoneId: "M001", unitType: "execute-task", unitId: "M001/S01/T01",
596
+ });
510
597
  writeLock(base, "execute-task", "M001/S01/T01", "/tmp/session.jsonl");
598
+
599
+ // Force stale so readCrashLock surfaces it.
600
+ _getAdapter()!.prepare(
601
+ `UPDATE workers SET last_heartbeat_at = '1970-01-01T00:00:00.000Z' WHERE worker_id = :w`,
602
+ ).run({ ":w": workerId });
603
+
511
604
  const lock = readCrashLock(base);
512
605
  assert.ok(lock, "lock should exist");
513
606
  assert.equal(lock!.unitType, "execute-task");
@@ -527,13 +620,30 @@ test("readCrashLock returns null when no lock exists", (t) => {
527
620
  // ─── clearLock ────────────────────────────────────────────────────────────
528
621
 
529
622
  test("clearLock removes existing lock file", (t) => {
623
+ // Phase C pt 2: clearLock now drops the session_file runtime_kv row
624
+ // for the LIVE worker (not the stale one). The "lock state" itself
625
+ // (pid, unitType, etc.) lives in workers + unit_dispatches; those are
626
+ // managed by markWorkerStopping (called from stopAuto, not here).
530
627
  const base = makeTmpBase();
531
628
  t.after(() => cleanup(base));
532
629
 
533
- writeLock(base, "plan-slice", "M001/S01");
534
- assert.ok(readCrashLock(base), "lock should exist before clear");
630
+ const projectRoot = normalizeRealPath(base);
631
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
632
+
633
+ writeLock(base, "plan-slice", "M001/S01", "/tmp/session.jsonl");
634
+ // Confirm the session_file row landed for the live worker.
635
+ const adapter = _getAdapter()!;
636
+ const before = adapter.prepare(
637
+ `SELECT 1 FROM runtime_kv WHERE scope = 'worker' AND scope_id = :w AND key = 'session_file'`,
638
+ ).get({ ":w": workerId });
639
+ assert.ok(before, "session_file row exists before clear");
640
+
535
641
  clearLock(base);
536
- assert.equal(readCrashLock(base), null, "lock should be gone after clear");
642
+
643
+ const after = adapter.prepare(
644
+ `SELECT 1 FROM runtime_kv WHERE scope = 'worker' AND scope_id = :w AND key = 'session_file'`,
645
+ ).get({ ":w": workerId });
646
+ assert.equal(after, undefined, "session_file row gone after clearLock");
537
647
  });
538
648
 
539
649
  test("clearLock is safe when no lock exists", (t) => {