opencode-swarm 7.138.3 → 7.138.4

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 (56) hide show
  1. package/.opencode/skills/execute/SKILL.md +11 -2
  2. package/.opencode/skills/swarm-implement/SKILL.md +1 -1
  3. package/dist/cli/{config-doctor-2mwgdbsn.js → config-doctor-31yppse4.js} +2 -2
  4. package/dist/cli/{core-v6bte964.js → core-65c7rvqf.js} +1 -1
  5. package/dist/cli/{curation-policy-5dqexw5j.js → curation-policy-hs9pc1rp.js} +2 -2
  6. package/dist/cli/{curator-jjtp9hn2.js → curator-54n1y4j8.js} +16 -15
  7. package/dist/cli/{curator-llm-factory-wkjme59b.js → curator-llm-factory-wm27ra25.js} +16 -15
  8. package/dist/cli/{evidence-summary-service-6cttrkzw.js → evidence-summary-service-hrxcf29a.js} +1 -1
  9. package/dist/cli/{guardrail-explain-xppwfnxr.js → guardrail-explain-1729nse8.js} +17 -16
  10. package/dist/cli/{guardrail-log-swet7rch.js → guardrail-log-yjj5srkw.js} +4 -3
  11. package/dist/cli/{hive-promoter-xvq8jzbs.js → hive-promoter-es2x2b0e.js} +16 -15
  12. package/dist/cli/{index-1djv5684.js → index-1g6xpjk3.js} +4 -4
  13. package/dist/cli/{index-js7th5my.js → index-33q2gmgd.js} +1 -1
  14. package/dist/cli/{index-adde7nm2.js → index-50mwt8fx.js} +5 -5
  15. package/dist/cli/index-556ja8an.js +62 -0
  16. package/dist/cli/{index-93ky909v.js → index-57stye65.js} +1 -1
  17. package/dist/cli/{index-ab57h0yq.js → index-5q009kc4.js} +1 -1
  18. package/dist/cli/{index-99af5gvd.js → index-a18hk2fw.js} +211 -63
  19. package/dist/cli/{index-1v89yst3.js → index-a9xny52r.js} +2 -2
  20. package/dist/cli/{index-rjratees.js → index-c60whfgh.js} +1 -1
  21. package/dist/cli/{index-gda7cwqz.js → index-e1kgxchq.js} +30 -68
  22. package/dist/cli/{index-31vfewmn.js → index-gm3pjfhb.js} +271 -94
  23. package/dist/cli/{index-jhb8kjc5.js → index-jbge0dyh.js} +3 -3
  24. package/dist/cli/{index-xgmx9had.js → index-k27a4e5g.js} +18 -17
  25. package/dist/cli/{index-y6dvs5tt.js → index-m1pwkwfe.js} +1 -1
  26. package/dist/cli/{index-wqg74mwj.js → index-m40ea3zk.js} +1 -1
  27. package/dist/cli/{index-btx8j39p.js → index-m8txs2a9.js} +1 -1
  28. package/dist/cli/{index-y06a43sk.js → index-mycqmsz5.js} +4 -0
  29. package/dist/cli/index-smsy2p8n.js +1725 -0
  30. package/dist/cli/{index-tc61fybd.js → index-tjwz5tct.js} +2 -2
  31. package/dist/cli/{index-j0xx9wpj.js → index-y28n2wmq.js} +1 -1
  32. package/dist/cli/index.js +16 -15
  33. package/dist/cli/{knowledge-escalator-7n384ys1.js → knowledge-escalator-s6z1x65b.js} +3 -3
  34. package/dist/cli/{knowledge-events-yaj0yjd6.js → knowledge-events-nnyn6hst.js} +1 -1
  35. package/dist/cli/{knowledge-store-d99k0z2d.js → knowledge-store-x4yfc7v3.js} +1 -1
  36. package/dist/cli/{knowledge-validator-e5fqy9h4.js → knowledge-validator-1v047gsa.js} +4 -4
  37. package/dist/cli/{scan-cursor-9y9msabt.js → scan-cursor-jxajj6s7.js} +2 -2
  38. package/dist/cli/{schema-xg5f11md.js → schema-fj32eyae.js} +1 -1
  39. package/dist/cli/{scope-persistence-n1bryhzf.js → scope-persistence-w7zen2wz.js} +25 -2
  40. package/dist/cli/{skill-generator-arvwfdfh.js → skill-generator-rt0jqcsa.js} +5 -5
  41. package/dist/hooks/guardrails/destructive-command.d.ts +35 -0
  42. package/dist/hooks/guardrails/file-authority.d.ts +71 -26
  43. package/dist/hooks/guardrails/index.d.ts +2 -0
  44. package/dist/hooks/guardrails/tool-before.d.ts +5 -0
  45. package/dist/hooks/scope-guard.d.ts +3 -1
  46. package/dist/index.js +407 -395
  47. package/dist/scope/path-identity.d.ts +27 -0
  48. package/dist/scope/scope-binding.d.ts +90 -2
  49. package/dist/scope/scope-lease-renewal.d.ts +28 -0
  50. package/dist/scope/scope-persistence.d.ts +102 -19
  51. package/dist/scope/scope-resolution-diagnostic.d.ts +7 -0
  52. package/dist/tools/declare-scope.d.ts +3 -1
  53. package/dist/tools/save-plan.d.ts +2 -0
  54. package/dist/turbo/lean/lane-scope.d.ts +1 -1
  55. package/package.json +1 -1
  56. package/dist/cli/index-2y4jy0de.js +0 -642
@@ -0,0 +1,1725 @@
1
+ // @bun
2
+ import {
3
+ unsafePathTextReason
4
+ } from "./index-556ja8an.js";
5
+ import {
6
+ computePlanStructureHash,
7
+ derivePlanId
8
+ } from "./index-4fmztnrq.js";
9
+ import {
10
+ PlanSchema
11
+ } from "./index-1kz6da87.js";
12
+ import {
13
+ require_proper_lockfile
14
+ } from "./index-7g4c7s5r.js";
15
+ import {
16
+ bunWrite
17
+ } from "./index-y111zefa.js";
18
+ import {
19
+ __toESM
20
+ } from "./index-a76rekgs.js";
21
+
22
+ // src/scope/scope-persistence.ts
23
+ var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
24
+ import { createHash } from "crypto";
25
+ import * as fs2 from "fs";
26
+ import * as path2 from "path";
27
+
28
+ // src/scope/scope-binding.ts
29
+ import { randomUUID } from "crypto";
30
+ import * as fs from "fs";
31
+ import * as path from "path";
32
+ var MAX_PENDING_SCOPE_BINDINGS = 256;
33
+ var DEFAULT_SCOPE_BINDING_TTL_MS = 60 * 60 * 1000;
34
+ var MAX_SCOPE_BINDING_TOMBSTONES = 256;
35
+ var pendingScopeBindings = new Map;
36
+ var scopeBindingTombstones = new Map;
37
+ var MAX_SCOPE_PATH_BYTES = 4096;
38
+ var MAX_SCOPE_FILES_BYTES = 1024 * 1024;
39
+ function newIdentity() {
40
+ return randomUUID();
41
+ }
42
+ function isScopeBindingIdentity(value) {
43
+ return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(value);
44
+ }
45
+ function canonicalWorkspaceIdentity(directory) {
46
+ try {
47
+ const real = fs.realpathSync(directory).replace(/\\/g, "/");
48
+ return process.platform === "win32" ? real.toLowerCase() : real;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ function normalizeScopePath(value) {
54
+ if (typeof value !== "string")
55
+ return null;
56
+ if (unsafePathTextReason(value))
57
+ return null;
58
+ const trimmed = value.trim().replace(/\\/g, "/");
59
+ if (trimmed.length === 0 || trimmed.includes("\x00") || trimmed.split("/").includes("..") || path.posix.isAbsolute(trimmed) || /^[A-Za-z]:\//.test(trimmed))
60
+ return null;
61
+ const normalized = path.posix.normalize(trimmed).replace(/^\.\//, "");
62
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../"))
63
+ return null;
64
+ return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
65
+ }
66
+ function normalizeScopeFiles(files) {
67
+ if (!Array.isArray(files) || files.length === 0 || files.length > 1e4)
68
+ return null;
69
+ const normalized = new Set;
70
+ let totalBytes = 0;
71
+ for (const file of files) {
72
+ const candidate = normalizeScopePath(file);
73
+ if (!candidate)
74
+ return null;
75
+ const bytes = Buffer.byteLength(candidate, "utf8");
76
+ totalBytes += bytes;
77
+ if (bytes > MAX_SCOPE_PATH_BYTES || totalBytes > MAX_SCOPE_FILES_BYTES) {
78
+ return null;
79
+ }
80
+ normalized.add(candidate);
81
+ }
82
+ return normalized.size > 0 ? [...normalized].sort() : null;
83
+ }
84
+ function bindingKey(binding) {
85
+ return binding.generationId;
86
+ }
87
+ function sweepExpired(now = Date.now()) {
88
+ for (const [key, binding] of pendingScopeBindings) {
89
+ if (binding.expiresAt <= now) {
90
+ pendingScopeBindings.delete(key);
91
+ const expired = {
92
+ ...binding,
93
+ revision: binding.revision + 1,
94
+ lifecycleState: "expired",
95
+ updatedAt: now
96
+ };
97
+ scopeBindingTombstones.set(bindingKey(expired), expired);
98
+ }
99
+ }
100
+ while (scopeBindingTombstones.size > MAX_SCOPE_BINDING_TOMBSTONES) {
101
+ const oldest = scopeBindingTombstones.keys().next().value;
102
+ if (!oldest)
103
+ break;
104
+ scopeBindingTombstones.delete(oldest);
105
+ }
106
+ }
107
+ function registerScopeBinding(binding) {
108
+ sweepExpired();
109
+ if (binding.lifecycleState !== "live" || binding.expiresAt <= Date.now()) {
110
+ return {
111
+ ok: false,
112
+ code: "SCOPE_BINDING_CAPACITY",
113
+ message: "Only an unexpired live binding can be admitted."
114
+ };
115
+ }
116
+ const exact = pendingScopeBindings.get(bindingKey(binding));
117
+ if (exact && exact.bindingId !== binding.bindingId) {
118
+ return {
119
+ ok: false,
120
+ code: "SCOPE_BINDING_STALE",
121
+ message: "The generation identity collides with a different binding."
122
+ };
123
+ }
124
+ if (exact && exact.revision > binding.revision) {
125
+ return {
126
+ ok: false,
127
+ code: "SCOPE_BINDING_STALE",
128
+ message: "A newer revision of this generation is already admitted."
129
+ };
130
+ }
131
+ if (binding.activation === "declaration" && binding.dispatchCallId === undefined) {
132
+ for (const [existingKey, existing] of pendingScopeBindings) {
133
+ if (existing.generationId !== binding.generationId && existing.activation === "declaration" && existing.dispatchCallId === undefined && existing.workspaceIdentity === binding.workspaceIdentity && existing.taskId === binding.taskId && existing.ownerSessionId === binding.ownerSessionId) {
134
+ pendingScopeBindings.delete(existingKey);
135
+ scopeBindingTombstones.set(existingKey, {
136
+ ...existing,
137
+ revision: existing.revision + 1,
138
+ lifecycleState: "superseded",
139
+ updatedAt: Date.now()
140
+ });
141
+ }
142
+ }
143
+ }
144
+ if (!exact && pendingScopeBindings.size >= MAX_PENDING_SCOPE_BINDINGS) {
145
+ return {
146
+ ok: false,
147
+ code: "SCOPE_BINDING_CAPACITY",
148
+ message: `Scope binding capacity ${MAX_PENDING_SCOPE_BINDINGS} is exhausted; complete or expire a live task before retrying.`
149
+ };
150
+ }
151
+ const key = bindingKey(binding);
152
+ pendingScopeBindings.delete(key);
153
+ pendingScopeBindings.set(key, binding);
154
+ return { ok: true, binding };
155
+ }
156
+ function clearScopeBindings(predicate) {
157
+ const removed = [];
158
+ if (!predicate) {
159
+ removed.push(...pendingScopeBindings.values());
160
+ pendingScopeBindings.clear();
161
+ scopeBindingTombstones.clear();
162
+ return removed;
163
+ }
164
+ for (const [key, binding] of pendingScopeBindings) {
165
+ if (predicate(binding)) {
166
+ removed.push(binding);
167
+ pendingScopeBindings.delete(key);
168
+ }
169
+ }
170
+ return removed;
171
+ }
172
+ function clearExactScopeBinding(input) {
173
+ const candidate = pendingScopeBindings.get(input.generationId);
174
+ if (!candidate || candidate.bindingId !== input.bindingId)
175
+ return null;
176
+ pendingScopeBindings.delete(input.generationId);
177
+ return candidate;
178
+ }
179
+ function installScopeBindingTombstone(binding) {
180
+ pendingScopeBindings.delete(binding.generationId);
181
+ scopeBindingTombstones.delete(binding.generationId);
182
+ scopeBindingTombstones.set(binding.generationId, binding);
183
+ while (scopeBindingTombstones.size > MAX_SCOPE_BINDING_TOMBSTONES) {
184
+ const oldest = scopeBindingTombstones.keys().next().value;
185
+ if (!oldest)
186
+ break;
187
+ scopeBindingTombstones.delete(oldest);
188
+ }
189
+ }
190
+ function hasScopeBindingDenyOverlay(binding) {
191
+ const tombstone = scopeBindingTombstones.get(binding.generationId);
192
+ return Boolean(tombstone && tombstone.bindingId === binding.bindingId && tombstone.revision >= binding.revision && tombstone.lifecycleState !== "live");
193
+ }
194
+ function installFailedRevocationOverlay(binding, reason) {
195
+ const current = pendingScopeBindings.get(binding.generationId);
196
+ if (!current || current.bindingId !== binding.bindingId || current.revision !== binding.revision)
197
+ return false;
198
+ const now = Date.now();
199
+ installScopeBindingTombstone({
200
+ ...current,
201
+ revision: current.revision + 1,
202
+ lifecycleState: reason,
203
+ updatedAt: now,
204
+ expiresAt: Math.min(current.expiresAt, now)
205
+ });
206
+ return true;
207
+ }
208
+ function installScopeBindingRetirementIntent(binding) {
209
+ const current = pendingScopeBindings.get(binding.generationId);
210
+ const source = current ?? (binding.version === 2 ? binding : null);
211
+ if (!source || source.bindingId !== binding.bindingId)
212
+ return null;
213
+ const now = Date.now();
214
+ const tombstone = {
215
+ ...source,
216
+ revision: source.revision + 1,
217
+ lifecycleState: "revoked",
218
+ updatedAt: now,
219
+ expiresAt: Math.min(source.expiresAt, now)
220
+ };
221
+ installScopeBindingTombstone(tombstone);
222
+ return tombstone;
223
+ }
224
+ function updateExactScopeBinding(binding) {
225
+ const current = pendingScopeBindings.get(binding.generationId);
226
+ if (!current || current.bindingId !== binding.bindingId || binding.revision <= current.revision)
227
+ return {
228
+ ok: false,
229
+ code: "SCOPE_BINDING_CAPACITY",
230
+ message: "The exact binding generation is stale or no longer active."
231
+ };
232
+ pendingScopeBindings.set(binding.generationId, binding);
233
+ return { ok: true, binding };
234
+ }
235
+ function createClaimedScopeBinding(previous, input) {
236
+ const now = Date.now();
237
+ return {
238
+ ...previous,
239
+ generationId: newIdentity(),
240
+ revision: 1,
241
+ ownerSessionId: input.childSessionId,
242
+ childSessionId: input.childSessionId,
243
+ ownerMessageId: input.dispatchCallId,
244
+ dispatchCallId: input.dispatchCallId,
245
+ parentOwnerSessionId: input.parentSessionId,
246
+ parentCallId: input.dispatchCallId,
247
+ activation: "active",
248
+ predecessorGenerationId: previous.generationId,
249
+ updatedAt: now,
250
+ leaseStartedAt: now,
251
+ expiresAt: now + DEFAULT_SCOPE_BINDING_TTL_MS
252
+ };
253
+ }
254
+
255
+ // src/scope/scope-persistence.ts
256
+ var SCOPE_SCHEMA_VERSION = 1;
257
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
258
+ var LOCK_STALE_MS = 30 * 1000;
259
+ var SCOPES_DIR = ".swarm/scopes";
260
+ var MAX_FILES_PER_SCOPE = 1e4;
261
+ var MAX_PLAN_BYTES = 10 * 1024 * 1024;
262
+ var MAX_SCOPE_BYTES = 2 * 1024 * 1024;
263
+ var MAX_BINDING_FILES_TO_SCAN = 1e4;
264
+ var MAX_TOMBSTONE_AGE_MS = 7 * 24 * 60 * 60 * 1000;
265
+ var MAX_DURABLE_TOMBSTONES = 256;
266
+ var liveBindingCapacity = MAX_PENDING_SCOPE_BINDINGS;
267
+ var bindingFileScanCapacity = MAX_BINDING_FILES_TO_SCAN;
268
+ var maintenanceFileScanCapacity = MAX_BINDING_FILES_TO_SCAN * 2;
269
+ var MAX_MAINTENANCE_JOBS = 64;
270
+ var maintenanceJobs = new Map;
271
+ var lockfileWithSync = import_proper_lockfile.default;
272
+ var syncLockWait = new Int32Array(new SharedArrayBuffer(4));
273
+ function lockSyncWithBoundedRetry(targetPath, attempts = 41) {
274
+ let lastError;
275
+ for (let attempt = 0;attempt < attempts; attempt++) {
276
+ try {
277
+ return lockfileWithSync.lockSync(targetPath, {
278
+ stale: LOCK_STALE_MS,
279
+ realpath: false
280
+ });
281
+ } catch (error) {
282
+ lastError = error;
283
+ if (attempt + 1 < attempts)
284
+ Atomics.wait(syncLockWait, 0, 0, 25);
285
+ }
286
+ }
287
+ throw lastError instanceof Error ? lastError : new Error("Lock unavailable");
288
+ }
289
+ var WINDOWS_RESERVED = new Set([
290
+ "CON",
291
+ "PRN",
292
+ "AUX",
293
+ "NUL",
294
+ "COM1",
295
+ "COM2",
296
+ "COM3",
297
+ "COM4",
298
+ "COM5",
299
+ "COM6",
300
+ "COM7",
301
+ "COM8",
302
+ "COM9",
303
+ "LPT1",
304
+ "LPT2",
305
+ "LPT3",
306
+ "LPT4",
307
+ "LPT5",
308
+ "LPT6",
309
+ "LPT7",
310
+ "LPT8",
311
+ "LPT9"
312
+ ]);
313
+ function getScopesDir(directory) {
314
+ return path2.join(directory, SCOPES_DIR);
315
+ }
316
+ function normalizePathForComparison(value) {
317
+ const normalized = path2.resolve(value);
318
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
319
+ }
320
+ function isSafeTaskId(taskId) {
321
+ if (typeof taskId !== "string")
322
+ return false;
323
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(taskId))
324
+ return false;
325
+ const head = taskId.replace(/\.+$/, "").split(".")[0].toUpperCase();
326
+ if (WINDOWS_RESERVED.has(head))
327
+ return false;
328
+ return true;
329
+ }
330
+ function isScopesDirSafe(directory, scopesDir) {
331
+ try {
332
+ const resolvedWorkspace = fs2.realpathSync(directory);
333
+ const expectedSwarmDir = path2.join(resolvedWorkspace, ".swarm");
334
+ const expectedScopesDir = path2.join(expectedSwarmDir, "scopes");
335
+ const swarmStat = fs2.lstatSync(expectedSwarmDir);
336
+ const scopesStat = fs2.lstatSync(expectedScopesDir);
337
+ if (!swarmStat.isDirectory() || swarmStat.isSymbolicLink() || !scopesStat.isDirectory() || scopesStat.isSymbolicLink())
338
+ return false;
339
+ const resolvedScopes = fs2.realpathSync(scopesDir);
340
+ if (normalizePathForComparison(resolvedScopes) !== normalizePathForComparison(expectedScopesDir))
341
+ return false;
342
+ const rel = path2.relative(resolvedWorkspace, resolvedScopes);
343
+ return rel.length > 0 && !rel.startsWith("..") && !path2.isAbsolute(rel);
344
+ } catch {
345
+ return false;
346
+ }
347
+ }
348
+ function resolveSafeScopeFileForUnlink(directory, targetPath) {
349
+ try {
350
+ const resolvedWorkspace = fs2.realpathSync(directory);
351
+ const scopesDir = path2.join(resolvedWorkspace, SCOPES_DIR);
352
+ if (!isScopesDirSafe(resolvedWorkspace, scopesDir))
353
+ return null;
354
+ const candidate = path2.resolve(targetPath);
355
+ const resolvedCandidate = fs2.realpathSync(candidate);
356
+ if (normalizePathForComparison(path2.dirname(candidate)) !== normalizePathForComparison(scopesDir) || normalizePathForComparison(resolvedCandidate) !== normalizePathForComparison(candidate))
357
+ return null;
358
+ const relative2 = path2.relative(resolvedWorkspace, resolvedCandidate);
359
+ if (relative2.length === 0 || relative2 === ".." || relative2.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative2))
360
+ return null;
361
+ const leafStat = fs2.lstatSync(candidate);
362
+ if (!leafStat.isFile() || leafStat.isSymbolicLink())
363
+ return null;
364
+ if (!isScopesDirSafe(resolvedWorkspace, scopesDir))
365
+ return null;
366
+ const finalResolvedCandidate = fs2.realpathSync(candidate);
367
+ const finalLeafStat = fs2.lstatSync(candidate);
368
+ if (normalizePathForComparison(finalResolvedCandidate) !== normalizePathForComparison(candidate) || !finalLeafStat.isFile() || finalLeafStat.isSymbolicLink() || finalLeafStat.dev !== leafStat.dev || finalLeafStat.ino !== leafStat.ino)
369
+ return null;
370
+ return candidate;
371
+ } catch {
372
+ return null;
373
+ }
374
+ }
375
+ function getScopeFilePath(directory, taskId) {
376
+ if (!isSafeTaskId(taskId)) {
377
+ throw new Error(`Invalid taskId for scope persistence: ${taskId}`);
378
+ }
379
+ return path2.join(getScopesDir(directory), `scope-${taskId}.json`);
380
+ }
381
+ function getLegacyBindingFilePath(directory, taskId, ownerSessionId) {
382
+ if (!isSafeTaskId(taskId) || !ownerSessionId.trim()) {
383
+ throw new Error("Invalid identity for scope binding persistence");
384
+ }
385
+ const ownerHash = createHash("sha256").update(ownerSessionId).digest("hex").slice(0, 24);
386
+ return path2.join(getScopesDir(directory), `binding-${taskId}-${ownerHash}.json`);
387
+ }
388
+ function getBindingFilePath(directory, binding) {
389
+ if (!isSafeTaskId(binding.taskId) || !isScopeBindingIdentity(binding.bindingId) || !isScopeBindingIdentity(binding.generationId))
390
+ throw new Error("Invalid exact identity for scope binding persistence");
391
+ return path2.join(getScopesDir(directory), `binding-${binding.taskId}-${binding.bindingId}-${binding.generationId}.json`);
392
+ }
393
+ function getGenerationLockPath(bindingPath) {
394
+ return `${bindingPath}.generation-lock`;
395
+ }
396
+ function getRetirementIntentPath(bindingPath) {
397
+ return `${bindingPath}.retirement-intent`;
398
+ }
399
+ function hasDurableRetirementIntent(bindingPath, binding) {
400
+ const raw = readBoundedFile(getRetirementIntentPath(bindingPath));
401
+ if (!raw)
402
+ return false;
403
+ try {
404
+ const intent = JSON.parse(raw);
405
+ return intent.bindingId === binding.bindingId && intent.generationId === binding.generationId;
406
+ } catch {
407
+ return false;
408
+ }
409
+ }
410
+ function ensureLockTargetSync(targetPath) {
411
+ try {
412
+ fs2.mkdirSync(path2.dirname(targetPath), { recursive: true });
413
+ const nofollow = fs2.constants.O_NOFOLLOW ?? 0;
414
+ const fd = fs2.openSync(targetPath, fs2.constants.O_WRONLY | fs2.constants.O_CREAT | nofollow);
415
+ fs2.closeSync(fd);
416
+ return true;
417
+ } catch {
418
+ return false;
419
+ }
420
+ }
421
+ function exactBindingPrefix(taskId) {
422
+ return `binding-${taskId}-`;
423
+ }
424
+ function readBoundedFile(filePath) {
425
+ const nofollow = fs2.constants.O_NOFOLLOW ?? 0;
426
+ let fd;
427
+ try {
428
+ fd = fs2.openSync(filePath, fs2.constants.O_RDONLY | nofollow);
429
+ } catch {
430
+ return null;
431
+ }
432
+ try {
433
+ const stat = fs2.fstatSync(fd);
434
+ if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_SCOPE_BYTES)
435
+ return null;
436
+ const buffer = Buffer.alloc(stat.size);
437
+ fs2.readSync(fd, buffer, 0, stat.size, 0);
438
+ return buffer.toString("utf8");
439
+ } catch {
440
+ return null;
441
+ } finally {
442
+ try {
443
+ fs2.closeSync(fd);
444
+ } catch {}
445
+ }
446
+ }
447
+ function deterministicIdentity(value) {
448
+ const hex = createHash("sha256").update(value).digest("hex").slice(0, 32);
449
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20)}`;
450
+ }
451
+ function validateScopeBindingPayload(directory, parsed, options = {}) {
452
+ const workspaceIdentity = canonicalWorkspaceIdentity(directory);
453
+ const files = Array.isArray(parsed.files) ? normalizeScopeFiles(parsed.files.filter((file) => typeof file === "string")) : null;
454
+ if (parsed.version !== 2 || !workspaceIdentity || parsed.workspaceIdentity !== workspaceIdentity || !isSafeTaskId(parsed.taskId ?? "") || !files || typeof parsed.planId !== "string" || !parsed.planId || typeof parsed.planStructureHash !== "string" || !parsed.planStructureHash || typeof parsed.ownerSessionId !== "string" || !parsed.ownerSessionId.trim() || typeof parsed.ownerMessageId !== "string" || !parsed.ownerMessageId.trim() || !["declaration", "pending_child", "active"].includes(parsed.activation ?? "") || ![
455
+ "declare_scope",
456
+ "plan",
457
+ "file_directive",
458
+ "pr_feedback",
459
+ "worktree_derived"
460
+ ].includes(parsed.source ?? "") || typeof parsed.declaredAt !== "number" || !Number.isFinite(parsed.declaredAt) || typeof parsed.expiresAt !== "number" || !Number.isFinite(parsed.expiresAt))
461
+ return null;
462
+ const legacySeed = JSON.stringify({
463
+ workspaceIdentity,
464
+ planId: parsed.planId,
465
+ planStructureHash: parsed.planStructureHash,
466
+ taskId: parsed.taskId,
467
+ ownerSessionId: parsed.ownerSessionId,
468
+ ownerMessageId: parsed.ownerMessageId,
469
+ dispatchCallId: parsed.dispatchCallId,
470
+ activation: parsed.activation,
471
+ source: parsed.source,
472
+ files,
473
+ declaredAt: parsed.declaredAt
474
+ });
475
+ const bindingId = isScopeBindingIdentity(parsed.bindingId) ? parsed.bindingId : options.allowLegacy ? deterministicIdentity(`binding\x00${legacySeed}`) : null;
476
+ const generationId = isScopeBindingIdentity(parsed.generationId) ? parsed.generationId : options.allowLegacy ? deterministicIdentity(`generation\x00${legacySeed}`) : null;
477
+ if (!bindingId || !generationId)
478
+ return null;
479
+ const lifecycleState = parsed.lifecycleState ?? (options.allowLegacy ? "live" : undefined);
480
+ if (!lifecycleState || !["live", "expired", "revoked", "superseded"].includes(lifecycleState))
481
+ return null;
482
+ const revision = parsed.revision ?? (options.allowLegacy ? 1 : undefined);
483
+ if (!Number.isSafeInteger(revision) || (revision ?? 0) < 1)
484
+ return null;
485
+ const updatedAt = parsed.updatedAt ?? parsed.declaredAt;
486
+ const leaseStartedAt = parsed.leaseStartedAt ?? parsed.declaredAt;
487
+ if (typeof updatedAt !== "number" || typeof leaseStartedAt !== "number")
488
+ return null;
489
+ const now = Date.now();
490
+ if (parsed.declaredAt > now || updatedAt < parsed.declaredAt || leaseStartedAt < parsed.declaredAt || parsed.activation === "declaration" && parsed.dispatchCallId !== undefined || parsed.activation === "pending_child" && (typeof parsed.dispatchCallId !== "string" || !parsed.dispatchCallId || parsed.ownerMessageId !== parsed.dispatchCallId) || parsed.activation === "active" && (typeof parsed.dispatchCallId !== "string" || !parsed.dispatchCallId || parsed.ownerMessageId !== parsed.dispatchCallId || typeof parsed.parentOwnerSessionId !== "string" || !parsed.parentOwnerSessionId || parsed.ownerSessionId === parsed.parentOwnerSessionId || parsed.parentCallId !== parsed.dispatchCallId))
491
+ return null;
492
+ return {
493
+ ...parsed,
494
+ bindingId,
495
+ generationId,
496
+ revision,
497
+ lifecycleState,
498
+ files,
499
+ workspaceIdentity,
500
+ updatedAt,
501
+ leaseStartedAt
502
+ };
503
+ }
504
+ function exactFilenameMatches(filePath, binding) {
505
+ return normalizePathForComparison(filePath) === normalizePathForComparison(getBindingFilePath(binding.workspaceIdentity, binding));
506
+ }
507
+ function migrateLegacyBindingsSync(directory, scanCapacity = bindingFileScanCapacity) {
508
+ const scopesDir = getScopesDir(directory);
509
+ if (!isScopesDirSafe(directory, scopesDir))
510
+ return false;
511
+ let names;
512
+ try {
513
+ names = fs2.readdirSync(scopesDir).sort();
514
+ } catch {
515
+ return true;
516
+ }
517
+ if (names.length > scanCapacity)
518
+ return false;
519
+ for (const name of names) {
520
+ const match = /^binding-(.+)-([a-f0-9]{24})\.json$/i.exec(name);
521
+ if (!match || !isSafeTaskId(match[1]))
522
+ continue;
523
+ const legacyPath = path2.join(scopesDir, name);
524
+ let releaseMigration;
525
+ try {
526
+ releaseMigration = lockfileWithSync.lockSync(legacyPath, {
527
+ stale: LOCK_STALE_MS,
528
+ realpath: false
529
+ });
530
+ } catch {
531
+ return false;
532
+ }
533
+ try {
534
+ const raw = readBoundedFile(legacyPath);
535
+ let binding = null;
536
+ try {
537
+ binding = raw ? validateScopeBindingPayload(directory, JSON.parse(raw), { allowLegacy: true }) : null;
538
+ } catch {
539
+ binding = null;
540
+ }
541
+ const ownerHash = binding ? createHash("sha256").update(binding.ownerSessionId).digest("hex").slice(0, 24) : "";
542
+ const valid = binding && binding.taskId === match[1] && ownerHash.toLowerCase() === match[2].toLowerCase() && normalizePathForComparison(getLegacyBindingFilePath(directory, binding.taskId, binding.ownerSessionId)) === normalizePathForComparison(legacyPath);
543
+ const archiveDir = path2.join(scopesDir, "archive");
544
+ fs2.mkdirSync(archiveDir, { recursive: true });
545
+ const archiveStat = fs2.lstatSync(archiveDir);
546
+ if (!archiveStat.isDirectory() || archiveStat.isSymbolicLink())
547
+ return false;
548
+ const archiveDigest = createHash("sha256").update(raw ?? name).digest("hex").slice(0, 32);
549
+ if (valid && binding) {
550
+ const exactPath = getBindingFilePath(directory, binding);
551
+ if (!fs2.existsSync(exactPath)) {
552
+ const temp = `${exactPath}.migration-${process.pid}`;
553
+ fs2.writeFileSync(temp, JSON.stringify(binding, null, 2), {
554
+ flag: "wx"
555
+ });
556
+ fs2.renameSync(temp, exactPath);
557
+ } else {
558
+ const existingRaw = readBoundedFile(exactPath);
559
+ const existing = existingRaw ? validateScopeBindingPayload(directory, JSON.parse(existingRaw)) : null;
560
+ if (!existing || existing.bindingId !== binding.bindingId || existing.generationId !== binding.generationId)
561
+ return false;
562
+ }
563
+ }
564
+ const archivePath = path2.join(archiveDir, `${valid ? "migrated" : "quarantined"}-${archiveDigest}.json`);
565
+ if (!fs2.existsSync(archivePath))
566
+ fs2.renameSync(legacyPath, archivePath);
567
+ else
568
+ fs2.unlinkSync(legacyPath);
569
+ fs2.writeFileSync(`${archivePath}.receipt.json`, JSON.stringify({
570
+ version: 1,
571
+ legacyName: name,
572
+ result: valid ? "migrated" : "quarantined",
573
+ bindingId: binding?.bindingId,
574
+ generationId: binding?.generationId
575
+ }, null, 2));
576
+ const archiveEntries = fs2.readdirSync(archiveDir).sort();
577
+ for (const stale of archiveEntries.slice(0, Math.max(0, archiveEntries.length - 2 * MAX_DURABLE_TOMBSTONES))) {
578
+ const stalePath = path2.join(archiveDir, stale);
579
+ try {
580
+ const stat = fs2.lstatSync(stalePath);
581
+ if (stat.isFile() && !stat.isSymbolicLink())
582
+ fs2.unlinkSync(stalePath);
583
+ } catch {}
584
+ }
585
+ } catch {
586
+ return false;
587
+ } finally {
588
+ try {
589
+ releaseMigration?.();
590
+ } catch {}
591
+ }
592
+ }
593
+ return true;
594
+ }
595
+ async function writeScopeToDisk(directory, taskId, files, ttlMs = DEFAULT_TTL_MS) {
596
+ if (!isSafeTaskId(taskId))
597
+ return;
598
+ if (!Array.isArray(files) || files.length === 0)
599
+ return;
600
+ if (files.length > MAX_FILES_PER_SCOPE)
601
+ return;
602
+ const scopesDir = getScopesDir(directory);
603
+ const scopePath = getScopeFilePath(directory, taskId);
604
+ try {
605
+ fs2.mkdirSync(scopesDir, { recursive: true });
606
+ } catch {
607
+ return;
608
+ }
609
+ if (!isScopesDirSafe(directory, scopesDir))
610
+ return;
611
+ const now = Date.now();
612
+ const payload = {
613
+ version: SCOPE_SCHEMA_VERSION,
614
+ taskId,
615
+ declaredAt: now,
616
+ expiresAt: now + ttlMs,
617
+ files: [...files]
618
+ };
619
+ const content = JSON.stringify(payload, null, 2);
620
+ try {
621
+ const flags = fs2.constants.O_WRONLY | fs2.constants.O_CREAT;
622
+ const nofollow = fs2.constants.O_NOFOLLOW ?? 0;
623
+ const fd = fs2.openSync(scopePath, flags | nofollow);
624
+ fs2.closeSync(fd);
625
+ } catch {
626
+ return;
627
+ }
628
+ let release;
629
+ try {
630
+ const lockPath = getGenerationLockPath(scopePath);
631
+ if (!await ensureLockTarget(lockPath))
632
+ return;
633
+ release = await import_proper_lockfile.default.lock(lockPath, {
634
+ stale: LOCK_STALE_MS,
635
+ retries: { retries: 3, minTimeout: 50, maxTimeout: 200 },
636
+ realpath: false
637
+ });
638
+ await atomicWrite(scopePath, content);
639
+ } catch {} finally {
640
+ if (release) {
641
+ try {
642
+ await release();
643
+ } catch {}
644
+ }
645
+ }
646
+ }
647
+ async function writeScopeBindingToDisk(directory, binding) {
648
+ if (!isSafeTaskId(binding.taskId) || binding.version !== 2)
649
+ return persistenceFailure("Invalid binding schema or task identity.");
650
+ const workspaceIdentity = canonicalWorkspaceIdentity(directory);
651
+ const files = normalizeScopeFiles(binding.files);
652
+ if (!workspaceIdentity || workspaceIdentity !== binding.workspaceIdentity || !files || !binding.ownerSessionId || !binding.ownerMessageId || !isScopeBindingIdentity(binding.bindingId) || !isScopeBindingIdentity(binding.generationId) || binding.revision < 1 || binding.lifecycleState === "live" && binding.expiresAt <= Date.now())
653
+ return persistenceFailure("Binding identity, workspace, scope, or lease is invalid.");
654
+ const scopesDir = getScopesDir(directory);
655
+ const scopePath = getBindingFilePath(directory, binding);
656
+ try {
657
+ fs2.mkdirSync(scopesDir, { recursive: true });
658
+ } catch {
659
+ return persistenceFailure("Could not create the durable scope store.");
660
+ }
661
+ if (!isScopesDirSafe(directory, scopesDir))
662
+ return persistenceFailure("The durable scope store failed containment checks.");
663
+ const content = JSON.stringify({ ...binding, files }, null, 2);
664
+ try {
665
+ const flags = fs2.constants.O_WRONLY | fs2.constants.O_CREAT;
666
+ const nofollow = fs2.constants.O_NOFOLLOW ?? 0;
667
+ const fd = fs2.openSync(scopePath, flags | nofollow);
668
+ fs2.closeSync(fd);
669
+ } catch {
670
+ return persistenceFailure("Could not create the exact generation record.");
671
+ }
672
+ let release;
673
+ try {
674
+ const lockPath = getGenerationLockPath(scopePath);
675
+ if (!await ensureLockTarget(lockPath))
676
+ return persistenceFailure("Could not prepare exact generation lock.");
677
+ release = await import_proper_lockfile.default.lock(lockPath, {
678
+ stale: LOCK_STALE_MS,
679
+ retries: { retries: 3, minTimeout: 50, maxTimeout: 200 },
680
+ realpath: false
681
+ });
682
+ await atomicWrite(scopePath, content);
683
+ const verifiedRaw = readBoundedFile(scopePath);
684
+ const verified = verifiedRaw ? validateScopeBindingPayload(directory, JSON.parse(verifiedRaw)) : null;
685
+ if (!verified || verified.bindingId !== binding.bindingId || verified.generationId !== binding.generationId || verified.revision !== binding.revision)
686
+ return persistenceFailure("Exact generation verification failed after write.");
687
+ return { ok: true, value: verified };
688
+ } catch (error) {
689
+ return persistenceFailure(`Exact generation write failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
690
+ } finally {
691
+ if (release) {
692
+ try {
693
+ await release();
694
+ } catch {}
695
+ }
696
+ }
697
+ }
698
+ function persistenceFailure(message) {
699
+ return { ok: false, code: "SCOPE_BINDING_PERSISTENCE_FAILED", message };
700
+ }
701
+ async function persistAndRegisterScopeBinding(directory, binding) {
702
+ const persisted = await writeScopeBindingToDisk(directory, binding);
703
+ if (!persisted.ok)
704
+ return persisted;
705
+ const admission = registerScopeBinding(persisted.value);
706
+ if (admission.ok) {
707
+ scheduleScopeBindingMaintenance(directory);
708
+ return { ok: true, value: persisted.value };
709
+ }
710
+ const rollback = await tombstoneScopeBinding(directory, persisted.value, "superseded");
711
+ return {
712
+ ok: false,
713
+ code: rollback.ok ? admission.code : "SCOPE_BINDING_PERSISTENCE_FAILED",
714
+ message: rollback.ok ? admission.message : `Memory admission failed and rollback failed: ${rollback.message}`
715
+ };
716
+ }
717
+ function readScopeBindingFromDisk(input) {
718
+ const resolved = resolveScopeBindingFromDisk({
719
+ ...input,
720
+ includeExpired: false
721
+ });
722
+ return resolved.status === "found" ? resolved.binding : null;
723
+ }
724
+ function resolveScopeBindingFromDisk(input) {
725
+ if (!isSafeTaskId(input.taskId))
726
+ return { status: "not_declared" };
727
+ const completeSet = readAllExactBindings(input.directory);
728
+ if (!completeSet) {
729
+ scheduleScopeBindingMaintenance(input.directory);
730
+ return { status: "overloaded" };
731
+ }
732
+ const candidates = completeSet.filter((parsed) => parsed.taskId === input.taskId && parsed.planId === derivePlanId(input.plan) && parsed.planStructureHash === computePlanStructureHash(input.plan) && parsed.ownerSessionId === input.ownerSessionId && (input.requireDispatchCorrelation !== true || isDispatchCorrelated(parsed)) && (input.requireDeclaration !== true || parsed.activation === "declaration" && parsed.dispatchCallId === undefined) && (input.parentCallId === undefined || parsed.parentCallId === input.parentCallId));
733
+ const live = candidates.filter((candidate) => candidate.lifecycleState === "live" && candidate.expiresAt > Date.now());
734
+ if (live.length > 1)
735
+ return {
736
+ status: "ambiguous",
737
+ candidates: live.slice(0, 8),
738
+ totalCandidates: live.length
739
+ };
740
+ if (live.length === 1)
741
+ return { status: "found", binding: live[0] };
742
+ const expired = candidates.filter((candidate) => candidate.lifecycleState !== "live" || candidate.expiresAt <= Date.now());
743
+ return input.includeExpired && expired.length > 0 ? {
744
+ status: "expired",
745
+ candidates: expired.slice(0, 8),
746
+ totalCandidates: expired.length
747
+ } : { status: "not_declared" };
748
+ }
749
+ function isDispatchCorrelated(binding) {
750
+ return binding.activation === "active" && typeof binding.dispatchCallId === "string" && binding.dispatchCallId.length > 0 && binding.ownerMessageId === binding.dispatchCallId && typeof binding.parentOwnerSessionId === "string" && binding.parentOwnerSessionId.length > 0 && binding.ownerSessionId !== binding.parentOwnerSessionId && binding.parentCallId === binding.dispatchCallId;
751
+ }
752
+ function clearScopeBindingFromDisk(input) {
753
+ const localRetirement = installScopeBindingRetirementIntent(input.binding);
754
+ let release;
755
+ try {
756
+ const resolvedWorkspace = fs2.realpathSync(input.directory);
757
+ const scopePath = getBindingFilePath(resolvedWorkspace, input.binding);
758
+ if (!isScopesDirSafe(resolvedWorkspace, getScopesDir(resolvedWorkspace)))
759
+ return persistenceFailure("The durable scope store failed containment checks.");
760
+ const retirementIntentPath = getRetirementIntentPath(scopePath);
761
+ atomicWriteSync(retirementIntentPath, JSON.stringify({
762
+ version: 1,
763
+ bindingId: input.binding.bindingId,
764
+ generationId: input.binding.generationId,
765
+ createdAt: Date.now()
766
+ }));
767
+ scheduleScopeBindingMaintenance(resolvedWorkspace);
768
+ const lockPath = getGenerationLockPath(scopePath);
769
+ if (!ensureLockTargetSync(lockPath))
770
+ return persistenceFailure("Could not prepare exact retirement lock.");
771
+ release = lockSyncWithBoundedRetry(lockPath);
772
+ const raw = readBoundedFile(scopePath);
773
+ if (!raw) {
774
+ try {
775
+ fs2.unlinkSync(retirementIntentPath);
776
+ } catch {}
777
+ return localRetirement ? { ok: true, value: localRetirement } : persistenceFailure("Exact generation disappeared before serialized retirement.");
778
+ }
779
+ const current = validateScopeBindingPayload(resolvedWorkspace, JSON.parse(raw));
780
+ if (!current || current.bindingId !== input.binding.bindingId || current.generationId !== input.binding.generationId)
781
+ return persistenceFailure("Exact generation identity changed before serialized retirement.");
782
+ if (current.lifecycleState !== "live") {
783
+ installScopeBindingTombstone(current);
784
+ try {
785
+ fs2.unlinkSync(retirementIntentPath);
786
+ } catch {}
787
+ return { ok: true, value: current };
788
+ }
789
+ const now = Date.now();
790
+ const tombstone = {
791
+ ...current,
792
+ revision: current.revision + 1,
793
+ lifecycleState: "revoked",
794
+ updatedAt: now,
795
+ expiresAt: Math.min(current.expiresAt, now)
796
+ };
797
+ atomicWriteSync(scopePath, JSON.stringify(tombstone, null, 2));
798
+ const verifiedRaw = readBoundedFile(scopePath);
799
+ const verified = verifiedRaw ? validateScopeBindingPayload(resolvedWorkspace, JSON.parse(verifiedRaw)) : null;
800
+ if (!verified || verified.revision !== tombstone.revision || verified.lifecycleState !== "revoked")
801
+ return persistenceFailure("Serialized retirement verification failed.");
802
+ installScopeBindingTombstone(verified);
803
+ try {
804
+ fs2.unlinkSync(retirementIntentPath);
805
+ } catch {}
806
+ scheduleScopeBindingMaintenance(resolvedWorkspace);
807
+ return { ok: true, value: verified };
808
+ } catch (error) {
809
+ return persistenceFailure(`Serialized retirement failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
810
+ } finally {
811
+ try {
812
+ release?.();
813
+ } catch {}
814
+ }
815
+ }
816
+ async function tombstoneScopeBinding(directory, binding, reason) {
817
+ const currentPath = getBindingFilePath(directory, binding);
818
+ const generationLockPath = getGenerationLockPath(currentPath);
819
+ const lockReady = await ensureLockTarget(generationLockPath);
820
+ if (!lockReady) {
821
+ installFailedRevocationOverlay(binding, reason);
822
+ return persistenceFailure("Could not prepare exact generation lock.");
823
+ }
824
+ let release;
825
+ try {
826
+ release = await import_proper_lockfile.default.lock(generationLockPath, {
827
+ stale: LOCK_STALE_MS,
828
+ retries: { retries: 3, minTimeout: 50, maxTimeout: 200 },
829
+ realpath: false
830
+ });
831
+ const raw = readBoundedFile(currentPath);
832
+ if (!raw) {
833
+ installFailedRevocationOverlay(binding, reason);
834
+ return persistenceFailure("Exact generation disappeared before tombstone.");
835
+ }
836
+ const current = validateScopeBindingPayload(directory, JSON.parse(raw));
837
+ if (!current || current.bindingId !== binding.bindingId || current.generationId !== binding.generationId) {
838
+ installFailedRevocationOverlay(binding, reason);
839
+ return persistenceFailure("Exact generation identity changed before tombstone.");
840
+ }
841
+ if (current.revision !== binding.revision)
842
+ return {
843
+ ok: false,
844
+ code: "SCOPE_BINDING_STALE",
845
+ message: "Tombstone CAS revision is stale."
846
+ };
847
+ if (current.lifecycleState !== "live")
848
+ return { ok: true, value: current };
849
+ const now = Date.now();
850
+ const tombstone = {
851
+ ...current,
852
+ revision: current.revision + 1,
853
+ lifecycleState: reason,
854
+ updatedAt: now,
855
+ expiresAt: Math.min(current.expiresAt, now)
856
+ };
857
+ await atomicWrite(currentPath, JSON.stringify(tombstone, null, 2));
858
+ const verifiedRaw = readBoundedFile(currentPath);
859
+ const verified = verifiedRaw ? validateScopeBindingPayload(directory, JSON.parse(verifiedRaw)) : null;
860
+ if (!verified || verified.revision !== tombstone.revision || verified.lifecycleState !== reason) {
861
+ installFailedRevocationOverlay(binding, reason);
862
+ return persistenceFailure("Tombstone verification failed.");
863
+ }
864
+ clearExactScopeBinding(binding);
865
+ installScopeBindingTombstone(verified);
866
+ scheduleScopeBindingMaintenance(directory);
867
+ return { ok: true, value: verified };
868
+ } catch (error) {
869
+ installFailedRevocationOverlay(binding, reason);
870
+ return persistenceFailure(`Tombstone failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
871
+ } finally {
872
+ if (release)
873
+ try {
874
+ await release();
875
+ } catch {}
876
+ }
877
+ }
878
+ async function ensureLockTarget(targetPath) {
879
+ return ensureLockTargetSync(targetPath);
880
+ }
881
+ async function refreshScopeBindingLease(input) {
882
+ let scopePath;
883
+ try {
884
+ scopePath = getBindingFilePath(input.directory, input);
885
+ } catch {
886
+ return persistenceFailure("Invalid refresh identity.");
887
+ }
888
+ const generationLockPath = getGenerationLockPath(scopePath);
889
+ const createdGenerationLockTarget = !fs2.existsSync(generationLockPath);
890
+ if (!await ensureLockTarget(generationLockPath))
891
+ return persistenceFailure("Could not prepare refresh lock.");
892
+ let release;
893
+ try {
894
+ release = await import_proper_lockfile.default.lock(generationLockPath, {
895
+ stale: LOCK_STALE_MS,
896
+ retries: { retries: 3, minTimeout: 50, maxTimeout: 200 },
897
+ realpath: false
898
+ });
899
+ if (hasDurableRetirementIntent(scopePath, input))
900
+ return {
901
+ ok: false,
902
+ code: "SCOPE_BINDING_STALE",
903
+ message: "Refresh cannot renew a generation with durable retirement intent."
904
+ };
905
+ const raw = readBoundedFile(scopePath);
906
+ const current = raw ? validateScopeBindingPayload(input.directory, JSON.parse(raw)) : null;
907
+ if (!current || current.bindingId !== input.bindingId || current.generationId !== input.generationId || current.taskId !== input.taskId || current.ownerSessionId !== input.activeSessionId)
908
+ return {
909
+ ok: false,
910
+ code: "SCOPE_BINDING_STALE",
911
+ message: "Refresh identity is not the exact current session/task generation."
912
+ };
913
+ if (current.lifecycleState !== "live" || current.expiresAt <= Date.now() || current.revision !== input.expectedRevision)
914
+ return {
915
+ ok: false,
916
+ code: current.expiresAt <= Date.now() ? "SCOPE_BINDING_EXPIRED" : "SCOPE_BINDING_STALE",
917
+ message: "Refresh cannot renew an expired, tombstoned, or stale generation."
918
+ };
919
+ const now = Date.now();
920
+ const refreshed = {
921
+ ...current,
922
+ revision: current.revision + 1,
923
+ updatedAt: now,
924
+ leaseStartedAt: now,
925
+ expiresAt: now + Math.max(1, input.ttlMs ?? DEFAULT_SCOPE_BINDING_TTL_MS)
926
+ };
927
+ await atomicWrite(scopePath, JSON.stringify(refreshed, null, 2));
928
+ const verifyRaw = readBoundedFile(scopePath);
929
+ const verified = verifyRaw ? validateScopeBindingPayload(input.directory, JSON.parse(verifyRaw)) : null;
930
+ if (!verified || verified.revision !== refreshed.revision || verified.expiresAt !== refreshed.expiresAt)
931
+ return persistenceFailure("Refreshed lease verification failed.");
932
+ updateExactScopeBinding(verified);
933
+ return { ok: true, value: verified };
934
+ } catch (error) {
935
+ return persistenceFailure(`Lease refresh failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
936
+ } finally {
937
+ if (release)
938
+ try {
939
+ await release();
940
+ } catch {}
941
+ if (createdGenerationLockTarget && !fs2.existsSync(scopePath)) {
942
+ try {
943
+ fs2.unlinkSync(generationLockPath);
944
+ } catch {}
945
+ }
946
+ }
947
+ }
948
+ function claimBaseName(binding) {
949
+ const digest = createHash("sha256").update(`${binding.bindingId}\x00${binding.generationId}\x00${binding.dispatchCallId ?? ""}`).digest("hex").slice(0, 40);
950
+ return `claim-${digest}`;
951
+ }
952
+ function readAllExactBindings(directory, options = {}) {
953
+ const scopesDir = getScopesDir(directory);
954
+ try {
955
+ fs2.lstatSync(scopesDir);
956
+ } catch (error) {
957
+ return error.code === "ENOENT" ? [] : null;
958
+ }
959
+ if (!isScopesDirSafe(directory, scopesDir))
960
+ return null;
961
+ const scanCapacity = options.maintenanceScan ? maintenanceFileScanCapacity : bindingFileScanCapacity;
962
+ if (!migrateLegacyBindingsSync(directory, scanCapacity))
963
+ return null;
964
+ let names;
965
+ try {
966
+ names = fs2.readdirSync(scopesDir).sort();
967
+ } catch {
968
+ return [];
969
+ }
970
+ if (names.length > scanCapacity)
971
+ return null;
972
+ const bindings = [];
973
+ for (const name of names) {
974
+ if (!name.startsWith("binding-") || !name.endsWith(".json"))
975
+ continue;
976
+ const filePath = path2.join(scopesDir, name);
977
+ const raw = readBoundedFile(filePath);
978
+ if (!raw)
979
+ continue;
980
+ try {
981
+ const binding = validateScopeBindingPayload(directory, JSON.parse(raw));
982
+ if (binding && exactFilenameMatches(filePath, binding)) {
983
+ if (hasDurableRetirementIntent(filePath, binding)) {
984
+ let createdAt = Date.now();
985
+ try {
986
+ const rawIntent = readBoundedFile(getRetirementIntentPath(filePath));
987
+ const parsedIntent = rawIntent ? JSON.parse(rawIntent) : null;
988
+ if (typeof parsedIntent?.createdAt === "number")
989
+ createdAt = parsedIntent.createdAt;
990
+ } catch {}
991
+ bindings.push({
992
+ ...binding,
993
+ lifecycleState: "revoked",
994
+ updatedAt: createdAt,
995
+ expiresAt: Math.min(binding.expiresAt, createdAt)
996
+ });
997
+ } else
998
+ bindings.push(binding);
999
+ }
1000
+ } catch {}
1001
+ }
1002
+ const liveCount = bindings.filter((binding) => binding.lifecycleState === "live" && binding.expiresAt > Date.now()).length;
1003
+ if (options.enforceLiveCapacity !== false && liveCount > liveBindingCapacity)
1004
+ return null;
1005
+ return bindings;
1006
+ }
1007
+ async function claimScopeBindingForChildDurably(input) {
1008
+ if (!input.parentSessionId.trim() || !input.childSessionId.trim() || !input.dispatchCallId.trim() || input.parentSessionId === input.childSessionId)
1009
+ return persistenceFailure("Claim requires distinct non-empty parent/child identities and an exact dispatch call.");
1010
+ const initial = readAllExactBindings(input.directory);
1011
+ if (!initial)
1012
+ return {
1013
+ ok: false,
1014
+ code: "SCOPE_BINDING_STORE_OVERLOADED",
1015
+ message: "The complete durable binding set could not be evaluated."
1016
+ };
1017
+ const pending = initial.filter((binding) => binding.lifecycleState === "live" && binding.expiresAt > Date.now() && binding.activation === "pending_child" && binding.ownerSessionId === input.parentSessionId && binding.dispatchCallId === input.dispatchCallId && binding.ownerMessageId === input.dispatchCallId);
1018
+ if (pending.length > 1)
1019
+ return {
1020
+ ok: false,
1021
+ code: "SCOPE_BINDING_AMBIGUOUS",
1022
+ message: "Multiple pending generations match this exact Task dispatch."
1023
+ };
1024
+ let predecessor = pending[0];
1025
+ if (!predecessor) {
1026
+ const successors = initial.filter((binding) => binding.lifecycleState === "live" && binding.expiresAt > Date.now() && binding.activation === "active" && binding.parentOwnerSessionId === input.parentSessionId && binding.dispatchCallId === input.dispatchCallId && binding.predecessorGenerationId);
1027
+ if (successors.length > 1)
1028
+ return {
1029
+ ok: false,
1030
+ code: "SCOPE_BINDING_AMBIGUOUS",
1031
+ message: "Multiple active successors match this Task dispatch."
1032
+ };
1033
+ const existing = successors[0];
1034
+ if (existing?.ownerSessionId === input.childSessionId) {
1035
+ const admission = registerScopeBinding(existing);
1036
+ return admission.ok ? { ok: true, value: { previous: existing, claimed: existing } } : {
1037
+ ok: false,
1038
+ code: admission.code,
1039
+ message: admission.message
1040
+ };
1041
+ }
1042
+ return existing ? {
1043
+ ok: false,
1044
+ code: "SCOPE_BINDING_ALREADY_CLAIMED",
1045
+ message: "This Task dispatch was already claimed by a different child session."
1046
+ } : initial.some((candidate) => candidate.ownerSessionId === input.parentSessionId && candidate.dispatchCallId === input.dispatchCallId) ? {
1047
+ ok: false,
1048
+ code: "SCOPE_BINDING_EXPIRED",
1049
+ message: "No live pending generation remains for this Task dispatch."
1050
+ } : {
1051
+ ok: false,
1052
+ code: "SCOPE_NOT_DECLARED",
1053
+ message: "No pending generation matches this Task dispatch."
1054
+ };
1055
+ }
1056
+ const claimPath = path2.join(getScopesDir(input.directory), `${claimBaseName(predecessor)}.json`);
1057
+ if (!await ensureLockTarget(claimPath))
1058
+ return persistenceFailure("Could not prepare the stable predecessor claim lock.");
1059
+ let release;
1060
+ try {
1061
+ release = await import_proper_lockfile.default.lock(claimPath, {
1062
+ stale: LOCK_STALE_MS,
1063
+ retries: { retries: 5, minTimeout: 50, maxTimeout: 250 },
1064
+ realpath: false
1065
+ });
1066
+ const currentSet = readAllExactBindings(input.directory);
1067
+ if (!currentSet)
1068
+ return {
1069
+ ok: false,
1070
+ code: "SCOPE_BINDING_STORE_OVERLOADED",
1071
+ message: "The durable binding set became incomplete during claim."
1072
+ };
1073
+ const currentPredecessor = currentSet.find((binding) => binding.generationId === predecessor.generationId && binding.bindingId === predecessor.bindingId);
1074
+ const allSuccessors = currentSet.filter((binding) => binding.predecessorGenerationId === predecessor.generationId && binding.bindingId === predecessor.bindingId);
1075
+ const successors = allSuccessors.filter((binding) => binding.lifecycleState === "live" && binding.expiresAt > Date.now());
1076
+ if (successors.length > 1)
1077
+ return {
1078
+ ok: false,
1079
+ code: "SCOPE_BINDING_AMBIGUOUS",
1080
+ message: "Incompatible successors exist for one predecessor generation."
1081
+ };
1082
+ const receiptRaw = readBoundedFile(claimPath);
1083
+ let receipt = null;
1084
+ if (receiptRaw) {
1085
+ try {
1086
+ const parsed = JSON.parse(receiptRaw);
1087
+ if (parsed.version !== 1 || parsed.predecessorGenerationId !== predecessor.generationId || !isScopeBindingIdentity(parsed.winnerGenerationId) || typeof parsed.childSessionId !== "string" || parsed.dispatchCallId !== input.dispatchCallId)
1088
+ return persistenceFailure("Claim winner receipt is malformed or belongs to another predecessor.");
1089
+ receipt = parsed;
1090
+ } catch {
1091
+ return persistenceFailure("Claim winner receipt is malformed.");
1092
+ }
1093
+ }
1094
+ if (receipt) {
1095
+ const receiptWinner = allSuccessors.find((candidate) => candidate.generationId === receipt?.winnerGenerationId);
1096
+ if (!receiptWinner)
1097
+ return persistenceFailure("Claim receipt has no verified successor; pending remains fail-closed for restart reconciliation.");
1098
+ if (receiptWinner.lifecycleState !== "live" || receiptWinner.expiresAt <= Date.now()) {
1099
+ fs2.writeFileSync(claimPath, "");
1100
+ receipt = null;
1101
+ }
1102
+ }
1103
+ if (receipt && successors[0] && receipt.winnerGenerationId !== successors[0].generationId)
1104
+ return {
1105
+ ok: false,
1106
+ code: "SCOPE_BINDING_AMBIGUOUS",
1107
+ message: "Claim receipt and durable successor disagree."
1108
+ };
1109
+ const existing = successors[0];
1110
+ if (existing) {
1111
+ if (existing.ownerSessionId !== input.childSessionId)
1112
+ return {
1113
+ ok: false,
1114
+ code: "SCOPE_BINDING_ALREADY_CLAIMED",
1115
+ message: "This pending generation has a winner in another child session."
1116
+ };
1117
+ await writeClaimReceipt(claimPath, predecessor, existing, input);
1118
+ if (currentPredecessor?.lifecycleState === "live")
1119
+ await tombstoneScopeBinding(input.directory, currentPredecessor, "superseded");
1120
+ const admission2 = registerScopeBinding(existing);
1121
+ return admission2.ok ? { ok: true, value: { previous: predecessor, claimed: existing } } : {
1122
+ ok: false,
1123
+ code: admission2.code,
1124
+ message: admission2.message
1125
+ };
1126
+ }
1127
+ if (!currentPredecessor || currentPredecessor.lifecycleState !== "live" || currentPredecessor.expiresAt <= Date.now())
1128
+ return {
1129
+ ok: false,
1130
+ code: "SCOPE_BINDING_EXPIRED",
1131
+ message: "The pending predecessor expired before claim."
1132
+ };
1133
+ predecessor = currentPredecessor;
1134
+ const claimed = createClaimedScopeBinding(predecessor, input);
1135
+ const persisted = await writeScopeBindingToDisk(input.directory, claimed);
1136
+ if (!persisted.ok)
1137
+ return persisted;
1138
+ const receiptWrite = await writeClaimReceipt(claimPath, predecessor, persisted.value, input);
1139
+ if (!receiptWrite.ok) {
1140
+ await tombstoneScopeBinding(input.directory, persisted.value, "revoked");
1141
+ return receiptWrite;
1142
+ }
1143
+ const admission = registerScopeBinding(persisted.value);
1144
+ if (!admission.ok) {
1145
+ await tombstoneScopeBinding(input.directory, persisted.value, "revoked");
1146
+ return {
1147
+ ok: false,
1148
+ code: admission.code,
1149
+ message: admission.message
1150
+ };
1151
+ }
1152
+ const retired = await tombstoneScopeBinding(input.directory, predecessor, "superseded");
1153
+ if (!retired.ok) {
1154
+ await tombstoneScopeBinding(input.directory, persisted.value, "revoked");
1155
+ return persistenceFailure(`Successor was persisted but predecessor retirement failed: ${retired.message}`);
1156
+ }
1157
+ return {
1158
+ ok: true,
1159
+ value: { previous: predecessor, claimed: persisted.value }
1160
+ };
1161
+ } catch (error) {
1162
+ return persistenceFailure(`Claim transaction failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
1163
+ } finally {
1164
+ if (release)
1165
+ try {
1166
+ await release();
1167
+ } catch {}
1168
+ }
1169
+ }
1170
+ async function writeClaimReceipt(claimPath, previous, claimed, input) {
1171
+ const receipt = {
1172
+ version: 1,
1173
+ predecessorGenerationId: previous.generationId,
1174
+ winnerGenerationId: claimed.generationId,
1175
+ childSessionId: input.childSessionId,
1176
+ dispatchCallId: input.dispatchCallId,
1177
+ createdAt: Date.now()
1178
+ };
1179
+ try {
1180
+ await atomicWrite(claimPath, JSON.stringify(receipt, null, 2));
1181
+ const raw = readBoundedFile(claimPath);
1182
+ const verified = raw ? JSON.parse(raw) : null;
1183
+ return verified?.winnerGenerationId === receipt.winnerGenerationId && verified.childSessionId === receipt.childSessionId ? { ok: true, value: verified } : persistenceFailure("Claim winner receipt verification failed.");
1184
+ } catch (error) {
1185
+ return persistenceFailure(`Claim winner receipt write failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
1186
+ }
1187
+ }
1188
+ async function replaceExistingScopeDeclaration(input) {
1189
+ const { binding } = input;
1190
+ if (binding.activation !== "declaration" || binding.dispatchCallId !== undefined)
1191
+ return persistenceFailure("declare_scope may persist only a declaration generation.");
1192
+ const digest = createHash("sha256").update(`${binding.workspaceIdentity}\x00${binding.taskId}\x00${binding.ownerSessionId}`).digest("hex").slice(0, 40);
1193
+ const lockPath = path2.join(getScopesDir(input.directory), `declaration-${digest}.lock-target`);
1194
+ if (!await ensureLockTarget(lockPath))
1195
+ return persistenceFailure("Could not prepare declaration transaction lock.");
1196
+ let release;
1197
+ try {
1198
+ release = await import_proper_lockfile.default.lock(lockPath, {
1199
+ stale: LOCK_STALE_MS,
1200
+ retries: { retries: 5, minTimeout: 50, maxTimeout: 250 },
1201
+ realpath: false
1202
+ });
1203
+ const all = readAllExactBindings(input.directory);
1204
+ if (!all)
1205
+ return {
1206
+ ok: false,
1207
+ code: "SCOPE_BINDING_STORE_OVERLOADED",
1208
+ message: "The durable binding set cannot be completely evaluated."
1209
+ };
1210
+ const owned = all.filter((candidate) => candidate.lifecycleState === "live" && candidate.expiresAt > Date.now() && candidate.workspaceIdentity === binding.workspaceIdentity && candidate.taskId === binding.taskId && candidate.ownerSessionId === binding.ownerSessionId && candidate.generationId !== binding.generationId);
1211
+ if (owned.length > 0 && !input.replaceExisting)
1212
+ return {
1213
+ ok: false,
1214
+ code: "SCOPE_BINDING_AMBIGUOUS",
1215
+ message: "A live owned generation already exists; retry declare_scope with replace_existing=true to revoke it atomically."
1216
+ };
1217
+ const persisted = await writeScopeBindingToDisk(input.directory, binding);
1218
+ if (!persisted.ok)
1219
+ return persisted;
1220
+ for (const prior of owned) {
1221
+ const retired = await tombstoneScopeBinding(input.directory, prior, "superseded");
1222
+ if (!retired.ok) {
1223
+ await tombstoneScopeBinding(input.directory, persisted.value, "revoked");
1224
+ return persistenceFailure(`Replacement could not retire generation ${prior.generationId}: ${retired.message}`);
1225
+ }
1226
+ }
1227
+ const admission = registerScopeBinding(persisted.value);
1228
+ if (!admission.ok) {
1229
+ await tombstoneScopeBinding(input.directory, persisted.value, "revoked");
1230
+ return {
1231
+ ok: false,
1232
+ code: admission.code,
1233
+ message: admission.message
1234
+ };
1235
+ }
1236
+ return { ok: true, value: persisted.value };
1237
+ } catch (error) {
1238
+ return persistenceFailure(`Declaration transaction failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
1239
+ } finally {
1240
+ if (release)
1241
+ try {
1242
+ await release();
1243
+ } catch {}
1244
+ }
1245
+ }
1246
+ function scheduleScopeBindingMaintenance(directory) {
1247
+ const key = normalizePathForComparison(directory);
1248
+ if (maintenanceJobs.has(key))
1249
+ return;
1250
+ while (maintenanceJobs.size >= MAX_MAINTENANCE_JOBS) {
1251
+ const oldest = maintenanceJobs.keys().next().value;
1252
+ if (!oldest)
1253
+ break;
1254
+ maintenanceJobs.delete(oldest);
1255
+ }
1256
+ const job = Promise.resolve().then(() => pruneScopeBindingTombstones(directory)).then(() => {
1257
+ return;
1258
+ }).catch(() => {
1259
+ return;
1260
+ }).finally(() => maintenanceJobs.delete(key));
1261
+ maintenanceJobs.set(key, job);
1262
+ }
1263
+ async function flushScopeBindingMaintenance(directory) {
1264
+ const key = directory ? normalizePathForComparison(directory) : undefined;
1265
+ const job = key ? maintenanceJobs.get(key) : undefined;
1266
+ await Promise.all(job ? [job] : [...maintenanceJobs.values()]);
1267
+ }
1268
+ async function pruneScopeBindingTombstones(directory) {
1269
+ const lockPath = path2.join(getScopesDir(directory), "maintenance.lock-target");
1270
+ if (!await ensureLockTarget(lockPath))
1271
+ return persistenceFailure("Could not prepare scope maintenance lock.");
1272
+ let release;
1273
+ try {
1274
+ release = await import_proper_lockfile.default.lock(lockPath, {
1275
+ stale: LOCK_STALE_MS,
1276
+ retries: { retries: 3, minTimeout: 50, maxTimeout: 200 },
1277
+ realpath: false
1278
+ });
1279
+ let all = readAllExactBindings(directory, {
1280
+ enforceLiveCapacity: false,
1281
+ maintenanceScan: true
1282
+ });
1283
+ if (!all)
1284
+ return {
1285
+ ok: false,
1286
+ code: "SCOPE_BINDING_STORE_OVERLOADED",
1287
+ message: "The durable binding set cannot be completely evaluated."
1288
+ };
1289
+ for (const binding of all) {
1290
+ const filePath = getBindingFilePath(directory, binding);
1291
+ if (!hasDurableRetirementIntent(filePath, binding))
1292
+ continue;
1293
+ const finalized = await tombstoneScopeBinding(directory, binding, "revoked");
1294
+ if (finalized.ok)
1295
+ try {
1296
+ fs2.unlinkSync(getRetirementIntentPath(filePath));
1297
+ } catch {}
1298
+ }
1299
+ all = readAllExactBindings(directory, {
1300
+ enforceLiveCapacity: false,
1301
+ maintenanceScan: true
1302
+ });
1303
+ if (!all)
1304
+ return persistenceFailure("The durable binding set changed during maintenance reconciliation.");
1305
+ const tombstones = all.filter((binding) => binding.lifecycleState !== "live").sort((left, right) => left.updatedAt - right.updatedAt);
1306
+ const now = Date.now();
1307
+ const removable = tombstones.filter((binding, index) => now - binding.updatedAt > MAX_TOMBSTONE_AGE_MS || index < tombstones.length - MAX_DURABLE_TOMBSTONES);
1308
+ let removed = 0;
1309
+ for (const binding of removable) {
1310
+ const filePath = resolveSafeScopeFileForUnlink(directory, getBindingFilePath(directory, binding));
1311
+ if (!filePath)
1312
+ continue;
1313
+ const exactLockPath = getGenerationLockPath(filePath);
1314
+ if (!await ensureLockTarget(exactLockPath))
1315
+ continue;
1316
+ let releaseExact;
1317
+ let removedExact = false;
1318
+ try {
1319
+ releaseExact = await import_proper_lockfile.default.lock(exactLockPath, {
1320
+ stale: LOCK_STALE_MS,
1321
+ retries: { retries: 0 },
1322
+ realpath: false
1323
+ });
1324
+ const latestRaw = readBoundedFile(filePath);
1325
+ const latest = latestRaw ? validateScopeBindingPayload(directory, JSON.parse(latestRaw)) : null;
1326
+ if (!latest || latest.generationId !== binding.generationId || latest.revision !== binding.revision || latest.lifecycleState === "live" && !hasDurableRetirementIntent(filePath, latest))
1327
+ continue;
1328
+ fs2.unlinkSync(filePath);
1329
+ removedExact = true;
1330
+ removed++;
1331
+ } catch {} finally {
1332
+ if (releaseExact)
1333
+ try {
1334
+ await releaseExact();
1335
+ } catch {}
1336
+ }
1337
+ if (removedExact && !fs2.existsSync(filePath)) {
1338
+ try {
1339
+ fs2.unlinkSync(exactLockPath);
1340
+ } catch {}
1341
+ try {
1342
+ fs2.unlinkSync(getRetirementIntentPath(filePath));
1343
+ } catch {}
1344
+ }
1345
+ }
1346
+ let receiptNames = [];
1347
+ try {
1348
+ receiptNames = fs2.readdirSync(getScopesDir(directory)).filter((name) => /^claim-[a-f0-9]{40}\.json$/i.test(name)).slice(0, MAX_BINDING_FILES_TO_SCAN);
1349
+ } catch {}
1350
+ for (const name of receiptNames) {
1351
+ const receiptPath = path2.join(getScopesDir(directory), name);
1352
+ let releaseReceipt;
1353
+ try {
1354
+ releaseReceipt = await import_proper_lockfile.default.lock(receiptPath, {
1355
+ stale: LOCK_STALE_MS,
1356
+ retries: { retries: 0 },
1357
+ realpath: false
1358
+ });
1359
+ const raw = readBoundedFile(receiptPath);
1360
+ if (!raw)
1361
+ continue;
1362
+ let receipt = null;
1363
+ try {
1364
+ receipt = JSON.parse(raw);
1365
+ } catch {}
1366
+ const winner = receipt ? all.find((binding) => binding.generationId === receipt?.winnerGenerationId) : undefined;
1367
+ const old = Date.now() - fs2.statSync(receiptPath).mtimeMs > MAX_TOMBSTONE_AGE_MS;
1368
+ if (old || Boolean(winner && (winner.lifecycleState !== "live" || winner.expiresAt <= Date.now()))) {
1369
+ fs2.unlinkSync(receiptPath);
1370
+ removed++;
1371
+ }
1372
+ } catch {} finally {
1373
+ if (releaseReceipt)
1374
+ try {
1375
+ await releaseReceipt();
1376
+ } catch {}
1377
+ }
1378
+ }
1379
+ return { ok: true, value: removed };
1380
+ } catch (error) {
1381
+ return persistenceFailure(`Scope maintenance failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
1382
+ } finally {
1383
+ if (release)
1384
+ try {
1385
+ await release();
1386
+ } catch {}
1387
+ }
1388
+ }
1389
+ function readCurrentPlan(directory) {
1390
+ try {
1391
+ const planPath = path2.join(directory, ".swarm", "plan.json");
1392
+ const stat = fs2.statSync(planPath);
1393
+ if (!stat.isFile() || stat.size > MAX_PLAN_BYTES)
1394
+ return null;
1395
+ const parsed = PlanSchema.safeParse(JSON.parse(fs2.readFileSync(planPath, "utf8")));
1396
+ return parsed.success ? parsed.data : null;
1397
+ } catch {
1398
+ return null;
1399
+ }
1400
+ }
1401
+ function resolveAuthorizedScopeBindingForPlan(input) {
1402
+ const resolution = resolveAuthorizedScopeBindingForPlanDetailed(input);
1403
+ return resolution.status === "found" ? resolution.binding : null;
1404
+ }
1405
+ function resolveAuthorizedScopeBindingForPlanDetailed(input) {
1406
+ const durable = resolveScopeBindingFromDisk({
1407
+ directory: input.directory,
1408
+ taskId: input.taskId,
1409
+ plan: input.plan,
1410
+ ownerSessionId: input.activeSessionId,
1411
+ requireDispatchCorrelation: true,
1412
+ includeExpired: true
1413
+ });
1414
+ if (durable.status === "found") {
1415
+ if (hasScopeBindingDenyOverlay(durable.binding))
1416
+ return {
1417
+ status: "expired",
1418
+ candidates: [durable.binding],
1419
+ totalCandidates: 1
1420
+ };
1421
+ const admission = registerScopeBinding(durable.binding);
1422
+ return admission.ok ? durable : { status: "overloaded" };
1423
+ }
1424
+ if (durable.status !== "not_declared")
1425
+ return durable;
1426
+ return { status: "not_declared" };
1427
+ }
1428
+ function resolveAuthorizedScopeBinding(input) {
1429
+ const plan = readCurrentPlan(input.directory);
1430
+ return plan ? resolveAuthorizedScopeBindingForPlan({ ...input, plan }) : null;
1431
+ }
1432
+ function resolveAuthorizedScopeBindingDetailed(input) {
1433
+ const plan = readCurrentPlan(input.directory);
1434
+ return plan ? resolveAuthorizedScopeBindingForPlanDetailed({ ...input, plan }) : { status: "not_declared" };
1435
+ }
1436
+ function resolveAuthorizedScopeBindingForSession(input) {
1437
+ const resolution = resolveAuthorizedScopeBindingForSessionDetailed(input);
1438
+ return resolution.status === "found" ? resolution.binding : null;
1439
+ }
1440
+ function resolveAuthorizedScopeBindingForSessionDetailed(input) {
1441
+ const plan = readCurrentPlan(input.directory);
1442
+ if (!plan)
1443
+ return { status: "not_declared" };
1444
+ const matches = [];
1445
+ const expired = [];
1446
+ let expiredTotal = 0;
1447
+ for (const task of plan.phases.flatMap((phase) => phase.tasks)) {
1448
+ const resolution = resolveAuthorizedScopeBindingForPlanDetailed({
1449
+ directory: input.directory,
1450
+ plan,
1451
+ taskId: task.id,
1452
+ activeSessionId: input.activeSessionId
1453
+ });
1454
+ if (resolution.status === "overloaded")
1455
+ return resolution;
1456
+ if (resolution.status === "ambiguous")
1457
+ return resolution;
1458
+ if (resolution.status === "found")
1459
+ matches.push(resolution.binding);
1460
+ if (resolution.status === "expired") {
1461
+ expired.push(...resolution.candidates);
1462
+ expiredTotal += resolution.totalCandidates;
1463
+ }
1464
+ if (matches.length > 1)
1465
+ return {
1466
+ status: "ambiguous",
1467
+ candidates: matches.slice(0, 8),
1468
+ totalCandidates: matches.length
1469
+ };
1470
+ }
1471
+ if (matches[0])
1472
+ return { status: "found", binding: matches[0] };
1473
+ return expired.length > 0 ? {
1474
+ status: "expired",
1475
+ candidates: expired.slice(0, 8),
1476
+ totalCandidates: expiredTotal
1477
+ } : { status: "not_declared" };
1478
+ }
1479
+ function readPrFeedbackBindingFile(directory, filePath, activeSessionId, taskId) {
1480
+ try {
1481
+ const leaf = fs2.lstatSync(filePath);
1482
+ if (!leaf.isFile() || leaf.isSymbolicLink())
1483
+ return null;
1484
+ if (normalizePathForComparison(fs2.realpathSync(filePath)) !== normalizePathForComparison(filePath))
1485
+ return null;
1486
+ } catch {
1487
+ return null;
1488
+ }
1489
+ const nofollow = fs2.constants.O_NOFOLLOW ?? 0;
1490
+ let fd;
1491
+ try {
1492
+ fd = fs2.openSync(filePath, fs2.constants.O_RDONLY | nofollow);
1493
+ } catch {
1494
+ return null;
1495
+ }
1496
+ let raw = "";
1497
+ try {
1498
+ const stat = fs2.fstatSync(fd);
1499
+ if (!stat.isFile() || stat.size > MAX_SCOPE_BYTES)
1500
+ return null;
1501
+ const buffer = Buffer.alloc(stat.size);
1502
+ fs2.readSync(fd, buffer, 0, stat.size, 0);
1503
+ raw = buffer.toString("utf8");
1504
+ } catch {
1505
+ return null;
1506
+ } finally {
1507
+ try {
1508
+ fs2.closeSync(fd);
1509
+ } catch {}
1510
+ }
1511
+ let parsed;
1512
+ try {
1513
+ parsed = validateScopeBindingPayload(directory, JSON.parse(raw));
1514
+ } catch {
1515
+ return null;
1516
+ }
1517
+ if (!parsed)
1518
+ return null;
1519
+ if (hasScopeBindingDenyOverlay(parsed))
1520
+ return null;
1521
+ const now = Date.now();
1522
+ if (parsed.source !== "pr_feedback" || taskId !== undefined && parsed.taskId !== taskId || parsed.ownerSessionId !== activeSessionId || parsed.activation !== "active" || typeof parsed.dispatchCallId !== "string" || !parsed.dispatchCallId || parsed.ownerMessageId !== parsed.dispatchCallId || typeof parsed.parentOwnerSessionId !== "string" || !parsed.parentOwnerSessionId || parsed.ownerSessionId === parsed.parentOwnerSessionId || parsed.parentCallId !== parsed.dispatchCallId || parsed.workflowSessionId !== parsed.parentOwnerSessionId || typeof parsed.workflowRevisionDigest !== "string" || !parsed.workflowRevisionDigest || parsed.planId !== `pr-feedback:${parsed.workflowSessionId}` || parsed.planStructureHash !== parsed.workflowRevisionDigest || parsed.declaredAt > now || parsed.expiresAt <= now || parsed.lifecycleState !== "live")
1523
+ return null;
1524
+ const expectedPath = getBindingFilePath(directory, parsed);
1525
+ if (normalizePathForComparison(expectedPath) !== normalizePathForComparison(filePath))
1526
+ return null;
1527
+ return parsed;
1528
+ }
1529
+ function resolveAuthorizedPrFeedbackScopeBindingFromDisk(input) {
1530
+ if (!input.activeSessionId.trim())
1531
+ return null;
1532
+ const scopesDir = getScopesDir(input.directory);
1533
+ if (!isScopesDirSafe(input.directory, scopesDir))
1534
+ return null;
1535
+ if (!migrateLegacyBindingsSync(input.directory))
1536
+ return null;
1537
+ let candidates;
1538
+ if (input.taskId) {
1539
+ if (!isSafeTaskId(input.taskId))
1540
+ return null;
1541
+ try {
1542
+ const names = fs2.readdirSync(scopesDir);
1543
+ if (names.length > MAX_BINDING_FILES_TO_SCAN)
1544
+ return null;
1545
+ candidates = names.filter((name) => name.startsWith(exactBindingPrefix(input.taskId)) && name.endsWith(".json")).map((name) => path2.join(scopesDir, name));
1546
+ } catch {
1547
+ return null;
1548
+ }
1549
+ } else {
1550
+ try {
1551
+ const names = fs2.readdirSync(scopesDir);
1552
+ if (names.length > MAX_BINDING_FILES_TO_SCAN)
1553
+ return null;
1554
+ candidates = names.filter((name) => name.startsWith("binding-") && name.endsWith(".json")).map((name) => path2.join(scopesDir, name));
1555
+ } catch {
1556
+ return null;
1557
+ }
1558
+ }
1559
+ const matches = candidates.map((candidate) => readPrFeedbackBindingFile(input.directory, candidate, input.activeSessionId, input.taskId)).filter((binding) => binding !== null);
1560
+ if (matches.length !== 1)
1561
+ return null;
1562
+ const admission = registerScopeBinding(matches[0]);
1563
+ return admission.ok ? matches[0] : null;
1564
+ }
1565
+ async function atomicWrite(targetPath, content) {
1566
+ const tempPath = `${targetPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
1567
+ try {
1568
+ await bunWrite(tempPath, content);
1569
+ fs2.renameSync(tempPath, targetPath);
1570
+ } finally {
1571
+ try {
1572
+ fs2.unlinkSync(tempPath);
1573
+ } catch {}
1574
+ }
1575
+ }
1576
+ function atomicWriteSync(targetPath, content) {
1577
+ const tempPath = `${targetPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
1578
+ try {
1579
+ fs2.writeFileSync(tempPath, content, { flag: "wx" });
1580
+ fs2.renameSync(tempPath, targetPath);
1581
+ } finally {
1582
+ try {
1583
+ fs2.unlinkSync(tempPath);
1584
+ } catch {}
1585
+ }
1586
+ }
1587
+ var _scopePersistenceInternals = {
1588
+ get liveBindingCapacity() {
1589
+ return liveBindingCapacity;
1590
+ },
1591
+ set liveBindingCapacity(value) {
1592
+ liveBindingCapacity = Math.max(1, Math.floor(value));
1593
+ },
1594
+ get bindingFileScanCapacity() {
1595
+ return bindingFileScanCapacity;
1596
+ },
1597
+ set bindingFileScanCapacity(value) {
1598
+ bindingFileScanCapacity = Math.max(1, Math.floor(value));
1599
+ },
1600
+ get maintenanceFileScanCapacity() {
1601
+ return maintenanceFileScanCapacity;
1602
+ },
1603
+ set maintenanceFileScanCapacity(value) {
1604
+ maintenanceFileScanCapacity = Math.max(1, Math.floor(value));
1605
+ }
1606
+ };
1607
+ function readScopeFromDisk(directory, taskId) {
1608
+ if (!isSafeTaskId(taskId))
1609
+ return null;
1610
+ const scopesDir = getScopesDir(directory);
1611
+ if (!isScopesDirSafe(directory, scopesDir))
1612
+ return null;
1613
+ const scopePath = getScopeFilePath(directory, taskId);
1614
+ const nofollow = fs2.constants.O_NOFOLLOW ?? 0;
1615
+ let fd;
1616
+ try {
1617
+ fd = fs2.openSync(scopePath, fs2.constants.O_RDONLY | nofollow);
1618
+ } catch {
1619
+ return null;
1620
+ }
1621
+ let raw;
1622
+ try {
1623
+ const stat = fs2.fstatSync(fd);
1624
+ if (!stat.isFile())
1625
+ return null;
1626
+ if (stat.size > MAX_SCOPE_BYTES)
1627
+ return null;
1628
+ const buf = Buffer.alloc(stat.size);
1629
+ fs2.readSync(fd, buf, 0, stat.size, 0);
1630
+ raw = buf.toString("utf-8");
1631
+ } catch {
1632
+ return null;
1633
+ } finally {
1634
+ try {
1635
+ fs2.closeSync(fd);
1636
+ } catch {}
1637
+ }
1638
+ if (!raw.trim())
1639
+ return null;
1640
+ let parsed;
1641
+ try {
1642
+ parsed = JSON.parse(raw);
1643
+ } catch {
1644
+ return null;
1645
+ }
1646
+ if (parsed.version !== SCOPE_SCHEMA_VERSION)
1647
+ return null;
1648
+ if (parsed.taskId !== taskId)
1649
+ return null;
1650
+ const now = Date.now();
1651
+ if (typeof parsed.declaredAt !== "number" || parsed.declaredAt > now) {
1652
+ return null;
1653
+ }
1654
+ if (typeof parsed.expiresAt !== "number" || now >= parsed.expiresAt) {
1655
+ return null;
1656
+ }
1657
+ if (!Array.isArray(parsed.files))
1658
+ return null;
1659
+ if (parsed.files.length > MAX_FILES_PER_SCOPE)
1660
+ return null;
1661
+ const files = parsed.files.filter((f) => typeof f === "string");
1662
+ return files.length > 0 ? files : null;
1663
+ }
1664
+ function readPlanScope(directory, taskId) {
1665
+ if (!isSafeTaskId(taskId))
1666
+ return null;
1667
+ try {
1668
+ const planPath = path2.join(directory, ".swarm", "plan.json");
1669
+ const stat = fs2.statSync(planPath);
1670
+ if (!stat.isFile())
1671
+ return null;
1672
+ if (stat.size > MAX_PLAN_BYTES)
1673
+ return null;
1674
+ const raw = fs2.readFileSync(planPath, "utf-8");
1675
+ const plan = JSON.parse(raw);
1676
+ for (const phase of plan.phases ?? []) {
1677
+ for (const task of phase.tasks ?? []) {
1678
+ if (task.id !== taskId)
1679
+ continue;
1680
+ const ft = task.files_touched;
1681
+ if (Array.isArray(ft) && ft.length > 0) {
1682
+ if (ft.length > MAX_FILES_PER_SCOPE)
1683
+ return null;
1684
+ return ft.filter((f) => typeof f === "string");
1685
+ }
1686
+ if (typeof ft === "string" && ft.length > 0)
1687
+ return [ft];
1688
+ return null;
1689
+ }
1690
+ }
1691
+ return null;
1692
+ } catch {
1693
+ return null;
1694
+ }
1695
+ }
1696
+ function clearScopeForTask(directory, taskId) {
1697
+ if (!isSafeTaskId(taskId))
1698
+ return;
1699
+ try {
1700
+ fs2.unlinkSync(getScopeFilePath(directory, taskId));
1701
+ } catch {}
1702
+ }
1703
+ function clearAllScopes(directory) {
1704
+ try {
1705
+ fs2.rmSync(getScopesDir(directory), { recursive: true, force: true });
1706
+ } catch {}
1707
+ }
1708
+ function resolveScopeWithFallbacks(input) {
1709
+ const { directory, taskId, inMemoryScope, pendingMapScope } = input;
1710
+ if (inMemoryScope && inMemoryScope.length > 0)
1711
+ return inMemoryScope;
1712
+ if (taskId) {
1713
+ const disk = readScopeFromDisk(directory, taskId);
1714
+ if (disk && disk.length > 0)
1715
+ return disk;
1716
+ const plan = readPlanScope(directory, taskId);
1717
+ if (plan && plan.length > 0)
1718
+ return plan;
1719
+ }
1720
+ if (pendingMapScope && pendingMapScope.length > 0)
1721
+ return pendingMapScope;
1722
+ return null;
1723
+ }
1724
+
1725
+ export { MAX_PENDING_SCOPE_BINDINGS, clearScopeBindings, writeScopeToDisk, writeScopeBindingToDisk, persistAndRegisterScopeBinding, readScopeBindingFromDisk, resolveScopeBindingFromDisk, clearScopeBindingFromDisk, tombstoneScopeBinding, refreshScopeBindingLease, claimScopeBindingForChildDurably, replaceExistingScopeDeclaration, flushScopeBindingMaintenance, pruneScopeBindingTombstones, resolveAuthorizedScopeBinding, resolveAuthorizedScopeBindingDetailed, resolveAuthorizedScopeBindingForSession, resolveAuthorizedScopeBindingForSessionDetailed, resolveAuthorizedPrFeedbackScopeBindingFromDisk, _scopePersistenceInternals, readScopeFromDisk, readPlanScope, clearScopeForTask, clearAllScopes, resolveScopeWithFallbacks };