gsd-pi 2.80.0-dev.2be7caf18 → 2.80.0-dev.42bd34838

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 (112) hide show
  1. package/README.md +1 -0
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +29 -15
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
  6. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  7. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  8. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  9. package/dist/resources/extensions/gsd/auto.js +61 -2
  10. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  11. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  12. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  13. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  14. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  15. package/dist/resources/extensions/gsd/git-service.js +36 -4
  16. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  17. package/dist/resources/extensions/gsd/guided-flow.js +19 -4
  18. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  19. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  20. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  21. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  22. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  23. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  24. package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
  25. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  26. package/dist/web/standalone/.next/BUILD_ID +1 -1
  27. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  28. package/dist/web/standalone/.next/build-manifest.json +2 -2
  29. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  30. package/dist/web/standalone/.next/required-server-files.json +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.html +1 -1
  48. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  55. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  57. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  58. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  59. package/dist/web/standalone/server.js +1 -1
  60. package/package.json +3 -3
  61. package/packages/mcp-server/src/workflow-tools.test.ts +9 -2
  62. package/packages/native/tsconfig.tsbuildinfo +1 -1
  63. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  64. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/agent-session.js +5 -0
  67. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  68. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  69. package/packages/pi-coding-agent/src/core/agent-session.ts +5 -0
  70. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  71. package/src/resources/extensions/gsd/auto/phases.ts +35 -20
  72. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  73. package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
  74. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  75. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  76. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  77. package/src/resources/extensions/gsd/auto.ts +69 -1
  78. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  79. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  80. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  81. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  82. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  83. package/src/resources/extensions/gsd/git-service.ts +46 -8
  84. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  85. package/src/resources/extensions/gsd/guided-flow.ts +32 -4
  86. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  87. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  88. package/src/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  89. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
  90. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  91. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  92. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +3 -1
  93. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  94. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  95. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  96. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
  97. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  98. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  99. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  100. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  101. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  102. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +38 -0
  103. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  104. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  105. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  106. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
  107. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  108. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  109. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  110. package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
  111. /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → tht0ltP8EIjsohPOA_WZf}/_buildManifest.js +0 -0
  112. /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → tht0ltP8EIjsohPOA_WZf}/_ssgManifest.js +0 -0
@@ -21,6 +21,7 @@ import {
21
21
  markFailed,
22
22
  markStuck,
23
23
  markCanceled,
24
+ markLatestActiveForWorkerCanceled,
24
25
  getRecentForUnit,
25
26
  getLatestForUnit,
26
27
  } from "../db/unit-dispatches.ts";
@@ -196,6 +197,35 @@ test("markStuck and markCanceled set their respective statuses", (t) => {
196
197
  assert.equal(getLatestForUnit("M001/S01/T01")!.status, "canceled");
197
198
  });
198
199
 
200
+ test("markLatestActiveForWorkerCanceled cancels only the latest active dispatch for a worker", (t) => {
201
+ const base = makeBase();
202
+ t.after(() => cleanup(base));
203
+ const { workerId, leaseToken } = setup(base);
204
+
205
+ const first = recordDispatchClaim({
206
+ traceId: "tc-1", workerId, milestoneLeaseToken: leaseToken,
207
+ milestoneId: "M001", unitType: "plan-slice", unitId: "M001/S01",
208
+ });
209
+ assert.equal(first.ok, true);
210
+ if (!first.ok) return;
211
+ markCompleted(first.dispatchId);
212
+
213
+ const second = recordDispatchClaim({
214
+ traceId: "tc-2", workerId, milestoneLeaseToken: leaseToken,
215
+ milestoneId: "M001", unitType: "run-task", unitId: "M001/S01/T01",
216
+ });
217
+ assert.equal(second.ok, true);
218
+ if (!second.ok) return;
219
+ markRunning(second.dispatchId);
220
+
221
+ assert.equal(markLatestActiveForWorkerCanceled(workerId, "signal-exit"), true);
222
+ assert.equal(getLatestForUnit("M001/S01")!.status, "completed");
223
+ const latest = getLatestForUnit("M001/S01/T01")!;
224
+ assert.equal(latest.status, "canceled");
225
+ assert.equal(latest.exit_reason, "signal-exit");
226
+ assert.equal(markLatestActiveForWorkerCanceled(workerId, "signal-exit"), false);
227
+ });
228
+
199
229
  test("terminal transitions do not overwrite an already terminal dispatch", (t) => {
200
230
  const base = makeBase();
201
231
  t.after(() => cleanup(base));
@@ -9,6 +9,7 @@ import {
9
9
  readUnitRuntimeRecord,
10
10
  writeUnitRuntimeRecord,
11
11
  } from "../unit-runtime.ts";
12
+ import { closeDatabase, insertMilestone, insertSlice, insertTask, openDatabase } from "../gsd-db.ts";
12
13
  import { clearPathCache } from '../paths.ts';
13
14
  import { test } from 'node:test';
14
15
  import assert from 'node:assert/strict';
@@ -72,6 +73,35 @@ console.log("\n=== runtime record cleanup ===");
72
73
  assert.deepStrictEqual(loaded, null, "record removed");
73
74
  }
74
75
 
76
+ console.log("\n=== execute-task durability trusts closed DB task status ===");
77
+ {
78
+ const dbBase = mkdtempSync(join(tmpdir(), "gsd-unit-runtime-db-test-"));
79
+ mkdirSync(join(dbBase, ".gsd", "milestones", "M300", "slices", "S01", "tasks"), { recursive: true });
80
+ try {
81
+ openDatabase(join(dbBase, ".gsd", "gsd.db"));
82
+ insertMilestone({ id: "M300", title: "DB Milestone", status: "active" });
83
+ insertSlice({ id: "S01", milestoneId: "M300", title: "DB Slice", status: "in_progress" });
84
+ insertTask({ id: "T01", milestoneId: "M300", sliceId: "S01", title: "DB Task", status: "complete" });
85
+ writeFileSync(
86
+ join(dbBase, ".gsd", "milestones", "M300", "slices", "S01", "S01-PLAN.md"),
87
+ "# S01\n\n## Tasks\n\n- [ ] **T01: DB Task** `est:10m`\n",
88
+ "utf-8",
89
+ );
90
+ writeFileSync(join(dbBase, ".gsd", "STATE.md"), "## Next Action\nExecute T01 for S01: DB task\n", "utf-8");
91
+
92
+ const status = await inspectExecuteTaskDurability(dbBase, "M300/S01/T01");
93
+ assert.ok(status !== null, "db-complete: status exists");
94
+ assert.equal(status!.dbComplete, true, "db-complete: closed DB status is captured");
95
+ assert.equal(status!.summaryExists, false, "db-complete: summary can still be missing");
96
+ assert.equal(status!.taskChecked, false, "db-complete: checkbox can still be unchecked");
97
+ assert.equal(status!.nextActionAdvanced, false, "db-complete: next action can still point at task");
98
+ assert.equal(formatExecuteTaskRecoveryStatus(status!), "DB task status is closed");
99
+ } finally {
100
+ closeDatabase();
101
+ rmSync(dbBase, { recursive: true, force: true });
102
+ }
103
+ }
104
+
75
105
  console.log("\n=== hook unit type sanitization (slash in unitType) ===");
76
106
  {
77
107
  // Hook units have unitType like "hook/code-review" with a slash
@@ -165,6 +165,9 @@ test("executeMilestoneStatus returns milestone metadata and slice counts", async
165
165
  assert.equal(parsed.sliceCount, 1);
166
166
  assert.equal(parsed.slices[0].id, "S01");
167
167
  assert.equal(parsed.slices[0].taskCounts.pending, 1);
168
+ assert.equal(result.details.status, "active");
169
+ assert.equal(result.details.title, "Milestone One");
170
+ assert.deepEqual(result.details.slices, parsed.slices);
168
171
  } finally {
169
172
  closeDatabase();
170
173
  cleanup(base);
@@ -1,3 +1,5 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: WorktreeResolver unit and regression tests.
1
3
  import test from "node:test";
2
4
  import assert from "node:assert/strict";
3
5
  import { mkdtempSync, rmSync, mkdirSync, realpathSync } from "node:fs";
@@ -9,6 +11,17 @@ import {
9
11
  type NotifyCtx,
10
12
  } from "../worktree-resolver.js";
11
13
  import { AutoSession } from "../auto/session.js";
14
+ import {
15
+ closeDatabase,
16
+ insertMilestone,
17
+ openDatabase,
18
+ } from "../gsd-db.js";
19
+ import { registerAutoWorker } from "../db/auto-workers.js";
20
+ import {
21
+ claimMilestoneLease,
22
+ getMilestoneLease,
23
+ releaseMilestoneLease,
24
+ } from "../db/milestone-leases.js";
12
25
 
13
26
  // ─── Helpers ─────────────────────────────────────────────────────────────────
14
27
 
@@ -19,11 +32,12 @@ interface CallLog {
19
32
  }
20
33
 
21
34
  function makeSession(
22
- overrides?: Partial<{ basePath: string; originalBasePath: string }>,
35
+ overrides?: Partial<AutoSession>,
23
36
  ): AutoSession {
24
37
  const s = new AutoSession();
25
38
  s.basePath = overrides?.basePath ?? "/project";
26
39
  s.originalBasePath = overrides?.originalBasePath ?? "/project";
40
+ Object.assign(s, overrides);
27
41
  return s;
28
42
  }
29
43
 
@@ -182,6 +196,17 @@ function findCalls(calls: CallLog[], fn: string): CallLog[] {
182
196
  return calls.filter((c) => c.fn === fn);
183
197
  }
184
198
 
199
+ function makeDbBase(): string {
200
+ const base = mkdtempSync(join(tmpdir(), "gsd-worktree-resolver-"));
201
+ mkdirSync(join(base, ".gsd"), { recursive: true });
202
+ return base;
203
+ }
204
+
205
+ function cleanupDbBase(base: string): void {
206
+ try { closeDatabase(); } catch { /* noop */ }
207
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
208
+ }
209
+
185
210
  // ─── Getter Tests ────────────────────────────────────────────────────────────
186
211
 
187
212
  test("workPath returns s.basePath", () => {
@@ -371,6 +396,43 @@ test("enterMilestone does not create double-nested worktree when originalBasePat
371
396
  );
372
397
  });
373
398
 
399
+ test("enterMilestone reacquires a released same-milestone lease before worktree entry", (t) => {
400
+ const base = makeDbBase();
401
+ t.after(() => cleanupDbBase(base));
402
+ openDatabase(join(base, ".gsd", "gsd.db"));
403
+ insertMilestone({ id: "M001", title: "Test milestone", status: "active" });
404
+
405
+ const workerId = registerAutoWorker({ projectRootRealpath: base });
406
+ const originalClaim = claimMilestoneLease(workerId, "M001");
407
+ assert.equal(originalClaim.ok, true);
408
+ if (!originalClaim.ok) throw new Error("expected test lease claim");
409
+ assert.equal(releaseMilestoneLease(workerId, "M001", originalClaim.token), true);
410
+
411
+ const s = makeSession({
412
+ basePath: base,
413
+ originalBasePath: base,
414
+ workerId,
415
+ currentMilestoneId: "M001",
416
+ milestoneLeaseToken: originalClaim.token,
417
+ });
418
+ const deps = makeDeps({
419
+ createAutoWorktree: (basePath: string, milestoneId: string) => join(basePath, ".gsd", "worktrees", milestoneId),
420
+ });
421
+ const ctx = makeNotifyCtx();
422
+ const resolver = new WorktreeResolver(s, deps);
423
+
424
+ resolver.enterMilestone("M001", ctx);
425
+
426
+ const row = getMilestoneLease("M001");
427
+ assert.ok(row);
428
+ assert.equal(row.worker_id, workerId);
429
+ assert.equal(row.status, "held");
430
+ assert.equal(row.fencing_token, originalClaim.token + 1);
431
+ assert.equal(s.milestoneLeaseToken, originalClaim.token + 1);
432
+ assert.equal(s.basePath, join(base, ".gsd", "worktrees", "M001"));
433
+ assert.equal(ctx.messages.some((m) => m.level === "error"), false);
434
+ });
435
+
374
436
  // ─── enterMilestone Tests (branch mode) ──────────────────────────────────────
375
437
 
376
438
  test("enterMilestone in branch mode calls enterBranchModeForMilestone and rebuilds GitService", () => {
@@ -308,6 +308,7 @@ function includeSupersededMemories(rankedActive: Memory[]): Memory[] {
308
308
  scope: (row["scope"] as string) ?? "project",
309
309
  tags,
310
310
  structured_fields: structuredFields,
311
+ last_hit_at: (row["last_hit_at"] as string | null) ?? null,
311
312
  };
312
313
  });
313
314
  } catch {
@@ -813,7 +813,7 @@ export async function executeMilestoneStatus(
813
813
 
814
814
  return {
815
815
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
816
- details: { operation: "milestone_status", milestoneId: milestone.id, sliceCount: slices.length },
816
+ details: { operation: "milestone_status", ...result },
817
817
  };
818
818
  });
819
819
  } catch (err) {
@@ -10,6 +10,8 @@ import {
10
10
  } from "./paths.js";
11
11
  import { loadFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
12
12
  import { parseUnitId } from "./unit-id.js";
13
+ import { getTask, isDbAvailable, refreshOpenDatabaseFromDisk } from "./gsd-db.js";
14
+ import { isClosedStatus } from "./status-guards.js";
13
15
 
14
16
  // Per-record advisory lock — prevents read-modify-write races between
15
17
  // concurrent writers updating disjoint fields of the same runtime record.
@@ -92,6 +94,7 @@ export interface ExecuteTaskRecoveryStatus {
92
94
  summaryExists: boolean;
93
95
  taskChecked: boolean;
94
96
  nextActionAdvanced: boolean;
97
+ dbComplete: boolean;
95
98
  mustHaveCount: number;
96
99
  mustHavesMentionedInSummary: number;
97
100
  }
@@ -213,6 +216,12 @@ export async function inspectExecuteTaskDurability(
213
216
  const escapedTid = tid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
214
217
  const taskChecked = !!planContent && new RegExp(`^- \\[[xX]\\] \\*\\*${escapedTid}:`, "m").test(planContent);
215
218
  const nextActionAdvanced = !new RegExp(`Execute ${tid}\\b`).test(stateContent);
219
+ let dbComplete = false;
220
+ if (isDbAvailable()) {
221
+ refreshOpenDatabaseFromDisk();
222
+ const task = getTask(mid, sid, tid);
223
+ dbComplete = !!task && isClosedStatus(task.status);
224
+ }
216
225
 
217
226
  // Must-have coverage: load task plan and count mentions in summary
218
227
  let mustHaveCount = 0;
@@ -239,12 +248,14 @@ export async function inspectExecuteTaskDurability(
239
248
  summaryExists,
240
249
  taskChecked,
241
250
  nextActionAdvanced,
251
+ dbComplete,
242
252
  mustHaveCount,
243
253
  mustHavesMentionedInSummary,
244
254
  };
245
255
  }
246
256
 
247
257
  export function formatExecuteTaskRecoveryStatus(status: ExecuteTaskRecoveryStatus): string {
258
+ if (status.dbComplete) return "DB task status is closed";
248
259
  const missing = [] as string[];
249
260
  if (!status.summaryExists) missing.push(`summary missing (${status.summaryPath})`);
250
261
  if (!status.taskChecked) missing.push(`task checkbox unchecked in ${status.planPath}`);
@@ -25,7 +25,7 @@ import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js
25
25
  import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
26
26
  import { loadEffectiveGSDPreferences } from "./preferences.js";
27
27
  import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "./worktree-root.js";
28
- import { claimMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
28
+ import { claimMilestoneLease, refreshMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
29
29
 
30
30
  // ─── Path Comparison Helper ────────────────────────────────────────────────
31
31
  /**
@@ -207,23 +207,44 @@ export class WorktreeResolver {
207
207
  // milestone (re-entry within the same session).
208
208
  if (this.s.workerId) {
209
209
  if (this.s.currentMilestoneId === milestoneId && this.s.milestoneLeaseToken !== null) {
210
- // Already held — no-op, the heartbeat in loop.ts refreshes TTL.
211
- } else {
212
- // If we held a different milestone, release it first so other
213
- // workers don't have to wait for TTL.
214
- if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
215
- try {
216
- releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
217
- } catch (err) {
218
- debugLog("WorktreeResolver", {
219
- action: "enterMilestone",
220
- milestoneId,
221
- releasePriorLeaseError: err instanceof Error ? err.message : String(err),
222
- });
223
- }
210
+ const refreshed = refreshMilestoneLease(
211
+ this.s.workerId,
212
+ milestoneId,
213
+ this.s.milestoneLeaseToken,
214
+ );
215
+ if (refreshed) {
216
+ debugLog("WorktreeResolver", {
217
+ action: "enterMilestone",
218
+ milestoneId,
219
+ leaseRefreshed: true,
220
+ fencingToken: this.s.milestoneLeaseToken,
221
+ });
222
+ } else {
223
+ debugLog("WorktreeResolver", {
224
+ action: "enterMilestone",
225
+ milestoneId,
226
+ staleLeaseToken: this.s.milestoneLeaseToken,
227
+ });
224
228
  this.s.milestoneLeaseToken = null;
225
229
  }
230
+ }
231
+
232
+ // If we held a different milestone, release it first so other
233
+ // workers don't have to wait for TTL.
234
+ if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
235
+ try {
236
+ releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
237
+ } catch (err) {
238
+ debugLog("WorktreeResolver", {
239
+ action: "enterMilestone",
240
+ milestoneId,
241
+ releasePriorLeaseError: err instanceof Error ? err.message : String(err),
242
+ });
243
+ }
244
+ this.s.milestoneLeaseToken = null;
245
+ }
226
246
 
247
+ if (this.s.milestoneLeaseToken === null) {
227
248
  try {
228
249
  const claim = claimMilestoneLease(this.s.workerId, milestoneId);
229
250
  if (claim.ok) {