opencode-swarm 7.164.11 → 7.164.12
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.
- package/.opencode/skills/engineering-conventions/SKILL.md +1 -1
- package/dist/background/pending-delegations.d.ts +7 -2
- package/dist/background/pr-subscriptions.d.ts +3 -3
- package/dist/cli/{coder-settlement-0rfk8y11.js → coder-settlement-yh1q5fr3.js} +15 -16
- package/dist/cli/{config-doctor-j17b7qq2.js → config-doctor-bd4yzmh7.js} +3 -3
- package/dist/cli/{core-evhjjrtt.js → core-y0s1vmyc.js} +2 -2
- package/dist/cli/{curation-policy-vj6z5hvr.js → curation-policy-gs1qakn7.js} +5 -6
- package/dist/cli/{curator-vqd5yeck.js → curator-brqack0j.js} +48 -46
- package/dist/cli/{curator-drift-ez81vg2t.js → curator-drift-1sg9yk2m.js} +14 -12
- package/dist/cli/curator-llm-factory-dtsjc3zf.js +70 -0
- package/dist/cli/{dispatch-dqybvj8v.js → dispatch-c7xjt7js.js} +3 -3
- package/dist/cli/{evidence-summary-service-937x7rbg.js → evidence-summary-service-nqbcvp30.js} +24 -23
- package/dist/cli/{gate-evidence-q1cehr4k.js → gate-evidence-9stdtpej.js} +9 -10
- package/dist/cli/guardrail-explain-3ff1z3fm.js +71 -0
- package/dist/cli/{guardrail-log-3g93f3g2.js → guardrail-log-sf39x00b.js} +10 -10
- package/dist/cli/{guardrail-reset-qe2r5qzq.js → guardrail-reset-9wr6a60s.js} +48 -46
- package/dist/cli/hive-promoter-bqtj8261.js +85 -0
- package/dist/cli/{index-kv2tdkk5.js → index-03enxdaq.js} +1 -1
- package/dist/cli/{index-ggrp3v00.js → index-0byep4pe.js} +15 -17
- package/dist/cli/{index-2bgr671r.js → index-2bhz5k91.js} +20 -2
- package/dist/cli/{index-mmjamc9b.js → index-3fnym6ac.js} +5 -5
- package/dist/cli/{index-d9entpn3.js → index-40twgsyh.js} +4 -4
- package/dist/cli/{index-ggtmsm84.js → index-4mjmnysc.js} +1 -1
- package/dist/cli/{index-e90em0x4.js → index-4vb56w2k.js} +12 -17
- package/dist/cli/{index-r656jh9j.js → index-5wc5pbzc.js} +3700 -2609
- package/dist/cli/{index-w3gtqey0.js → index-6chhqq2n.js} +4 -4
- package/dist/cli/{index-qbw3yw2d.js → index-888pq4xx.js} +11 -11
- package/dist/cli/{index-3146wmgv.js → index-8d90gzaz.js} +1 -1
- package/dist/cli/{index-5jx22kwc.js → index-8vw2pjgr.js} +544 -15
- package/dist/cli/{index-b2nj04q6.js → index-9ss2m4rs.js} +1 -434
- package/dist/cli/{index-x43debgx.js → index-ad9064j9.js} +15 -17
- package/dist/cli/{index-5kcvrpkt.js → index-b25t91a3.js} +1 -1
- package/dist/cli/index-c45662wc.js +361 -0
- package/dist/cli/{index-aqppaaqx.js → index-cgewrpw5.js} +3 -2
- package/dist/cli/{index-zfs3zqeq.js → index-d3mpxsmy.js} +771 -21
- package/dist/cli/{index-y1yqmkv7.js → index-d7j3n5ra.js} +52 -50
- package/dist/cli/{index-4vb6qa73.js → index-ebce775n.js} +36 -2
- package/dist/cli/{index-dsg9dw3b.js → index-eed0gkv6.js} +2 -2
- package/dist/cli/index-es64amqt.js +675 -0
- package/dist/cli/{index-9gxbjkts.js → index-ewv8qa8q.js} +22 -19
- package/dist/cli/{index-1ww7wj8d.js → index-hb7bh4r2.js} +15 -15
- package/dist/cli/{index-kc2tgcm7.js → index-hg09xemc.js} +4 -4
- package/dist/cli/{index-w95vxjde.js → index-hvz4ad2t.js} +39 -8
- package/dist/cli/{index-77z98j76.js → index-k0s8n6m5.js} +314 -130
- package/dist/cli/{index-7m32fjb4.js → index-kxv193aq.js} +9 -9
- package/dist/cli/{index-hp22pzn5.js → index-m8xrbdmn.js} +2 -2
- package/dist/cli/{index-4gqxppcx.js → index-mee9maf5.js} +2 -2
- package/dist/cli/{index-r3mvsbtq.js → index-p612v1mv.js} +7 -1
- package/dist/cli/{index-r5q5gd2a.js → index-qgajv9rd.js} +2 -2
- package/dist/cli/{index-zjg4chy2.js → index-qqq04n70.js} +2 -2
- package/dist/cli/{index-ww6zm5hr.js → index-r2p0pgst.js} +3 -3
- package/dist/cli/{index-47kh2103.js → index-r69gzes2.js} +14 -14
- package/dist/cli/{index-vdn32nws.js → index-sphahrmq.js} +3 -3
- package/dist/cli/{index-rhqdsc1x.js → index-vw66q1rw.js} +9 -9
- package/dist/cli/{index-kch6yenm.js → index-wasdrhev.js} +7 -7
- package/dist/cli/{index-c5v1w13g.js → index-wknpkfm8.js} +807 -629
- package/dist/cli/{index-pp2nya9v.js → index-x39hjdf1.js} +2 -4
- package/dist/cli/{index-n8688d12.js → index-xxfk62zd.js} +2 -2
- package/dist/cli/index-yvqg6kn5.js +429 -0
- package/dist/cli/{index-nqsj0g5y.js → index-ztm0q26v.js} +8 -8
- package/dist/cli/index.js +50 -48
- package/dist/cli/{knowledge-escalator-j04g450p.js → knowledge-escalator-01vva51d.js} +17 -18
- package/dist/cli/{knowledge-events-53z45rs0.js → knowledge-events-dpfwh1hs.js} +15 -16
- package/dist/cli/{knowledge-link-a6kfg3qm.js → knowledge-link-2rvp966j.js} +4 -5
- package/dist/cli/{knowledge-store-whm0gagt.js → knowledge-store-aw58ae7b.js} +6 -7
- package/dist/cli/{knowledge-validator-h8m7065g.js → knowledge-validator-yhd31ryf.js} +8 -9
- package/dist/cli/{pending-delegations-6hkpe5ze.js → pending-delegations-fjetcz8e.js} +17 -8
- package/dist/cli/{pr-subscriptions-11zca1mh.js → pr-subscriptions-cwwtfs8w.js} +17 -8
- package/dist/cli/{pr-workflow-gate-6g29rkg7.js → pr-workflow-gate-5xwtg1k2.js} +50 -48
- package/dist/cli/{runner-5abw3t35.js → runner-mb51twxt.js} +6 -7
- package/dist/cli/{scan-cursor-mkfx410t.js → scan-cursor-j11qwf0v.js} +9 -10
- package/dist/cli/{schema-jbbam3ev.js → schema-0xp3bde3.js} +1 -1
- package/dist/cli/{scope-persistence-q1ct4k42.js → scope-persistence-brrk806y.js} +19 -12
- package/dist/cli/{skill-generator-cv8fg96x.js → skill-generator-j8qzmet9.js} +20 -21
- package/dist/cli/snapshot-coordination-init-h0xmtftq.js +84 -0
- package/dist/cli/{telemetry-mbqqamsj.js → telemetry-m84ap0pp.js} +1 -1
- package/dist/cli/{workspace-snapshot-eh18vgkn.js → workspace-snapshot-6z9rk7xw.js} +3 -3
- package/dist/cli/{worktree-collision-ownership-xgnn8dsq.js → worktree-collision-ownership-j9zykj18.js} +21 -12
- package/dist/cli/{worktree-isolation-k2dgfmeg.js → worktree-isolation-jd39xz6h.js} +48 -46
- package/dist/commands/close.d.ts +13 -6
- package/dist/commands/reset-session.d.ts +4 -0
- package/dist/db/coordination-store.d.ts +85 -0
- package/dist/db/durability.d.ts +22 -5
- package/dist/db/index.d.ts +1 -0
- package/dist/db/project-db.d.ts +12 -0
- package/dist/index.js +810 -705
- package/dist/memory/schema.d.ts +3 -3
- package/dist/pr-review/authorization.d.ts +5 -4
- package/dist/pr-review/persistence.d.ts +10 -1
- package/dist/scope/scope-binding.d.ts +10 -0
- package/dist/scope/scope-persistence.d.ts +10 -0
- package/dist/services/handoff-service.d.ts +2 -0
- package/dist/services/status-service.d.ts +4 -0
- package/dist/session/snapshot-coordination-init.d.ts +49 -0
- package/dist/session/snapshot-reader.d.ts +2 -0
- package/dist/session/snapshot-store.d.ts +42 -0
- package/dist/session/snapshot-writer.d.ts +4 -1
- package/dist/state.d.ts +6 -3
- package/dist/turbo/epic/state.d.ts +4 -20
- package/dist/utils/bun-compat.d.ts +10 -0
- package/dist/utils/external-tool-runner.d.ts +2 -0
- package/package.json +3 -2
- package/dist/cli/curator-llm-factory-sxcpmcqq.js +0 -68
- package/dist/cli/guardrail-explain-xv9aw1bf.js +0 -69
- package/dist/cli/hive-promoter-09yc9mp5.js +0 -83
- package/dist/cli/index-xnb3tk7x.js +0 -24
- package/dist/cli/{ast-diff-ekd9v10f.js → ast-diff-b0esr2kf.js} +3 -3
|
@@ -1,31 +1,42 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
computePlanStructureHash
|
|
4
|
-
|
|
5
|
-
} from "./index-e90em0x4.js";
|
|
6
|
-
import {
|
|
7
|
-
canonicalExistingFilesystemPath,
|
|
8
|
-
legacyCanonicalExistingFilesystemPath
|
|
9
|
-
} from "./index-ry16d01s.js";
|
|
3
|
+
computePlanStructureHash
|
|
4
|
+
} from "./index-4vb56w2k.js";
|
|
10
5
|
import {
|
|
11
6
|
appendCoreEventSync
|
|
12
|
-
} from "./index-
|
|
7
|
+
} from "./index-mee9maf5.js";
|
|
13
8
|
import {
|
|
14
9
|
unsafePathTextReason
|
|
15
10
|
} from "./index-556ja8an.js";
|
|
16
11
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
deleteCoordinationState,
|
|
13
|
+
derivePlanId,
|
|
14
|
+
importCoordinationOnce,
|
|
15
|
+
listCoordinationStates,
|
|
16
|
+
transitionCoordinationState,
|
|
17
|
+
transitionCoordinationStateWithinTransaction,
|
|
18
|
+
withCoordinationTransaction
|
|
19
|
+
} from "./index-c45662wc.js";
|
|
20
|
+
import {
|
|
21
|
+
getProjectDb
|
|
22
|
+
} from "./index-es64amqt.js";
|
|
23
|
+
import {
|
|
24
|
+
canonicalExistingFilesystemPath,
|
|
25
|
+
legacyCanonicalExistingFilesystemPath
|
|
26
|
+
} from "./index-ry16d01s.js";
|
|
19
27
|
import {
|
|
20
28
|
assertProjectRoot
|
|
21
|
-
} from "./index-
|
|
29
|
+
} from "./index-2bhz5k91.js";
|
|
22
30
|
import {
|
|
23
31
|
atomicWriteSwarmFile,
|
|
24
32
|
atomicWriteSwarmFileSync
|
|
25
|
-
} from "./index-
|
|
33
|
+
} from "./index-ewv8qa8q.js";
|
|
26
34
|
import {
|
|
27
35
|
require_proper_lockfile
|
|
28
36
|
} from "./index-7g4c7s5r.js";
|
|
37
|
+
import {
|
|
38
|
+
PlanSchema
|
|
39
|
+
} from "./index-vw66q1rw.js";
|
|
29
40
|
import {
|
|
30
41
|
__toESM
|
|
31
42
|
} from "./index-a76rekgs.js";
|
|
@@ -158,6 +169,17 @@ function registerScopeBinding(binding) {
|
|
|
158
169
|
pendingScopeBindings.set(key, binding);
|
|
159
170
|
return { ok: true, binding };
|
|
160
171
|
}
|
|
172
|
+
function getActiveScopeBindingsForSession(input) {
|
|
173
|
+
sweepExpired();
|
|
174
|
+
const workspaceIdentity = canonicalWorkspaceIdentity(input.directory);
|
|
175
|
+
if (!workspaceIdentity || !input.activeSessionId.trim())
|
|
176
|
+
return [];
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
return [...pendingScopeBindings.values()].filter((binding) => binding.workspaceIdentity === workspaceIdentity && binding.ownerSessionId === input.activeSessionId && binding.activation === "active" && binding.lifecycleState === "live" && binding.expiresAt > now && isDispatchCorrelated(binding));
|
|
179
|
+
}
|
|
180
|
+
function isDispatchCorrelated(binding) {
|
|
181
|
+
return 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;
|
|
182
|
+
}
|
|
161
183
|
function clearScopeBindings(predicate) {
|
|
162
184
|
const removed = [];
|
|
163
185
|
if (!predicate) {
|
|
@@ -278,28 +300,22 @@ var MAX_SCOPE_BYTES = 2 * 1024 * 1024;
|
|
|
278
300
|
var MAX_BINDING_FILES_TO_SCAN = 1e4;
|
|
279
301
|
var MAX_TOMBSTONE_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
280
302
|
var MAX_DURABLE_TOMBSTONES = 256;
|
|
303
|
+
var SCOPE_BINDING_COORDINATION_NAMESPACE = "scope-binding";
|
|
304
|
+
var SCOPE_BINDING_COORDINATION_IMPORT_SOURCE = "scope-binding-v2";
|
|
281
305
|
var liveBindingCapacity = MAX_PENDING_SCOPE_BINDINGS;
|
|
282
306
|
var bindingFileScanCapacity = MAX_BINDING_FILES_TO_SCAN;
|
|
283
307
|
var maintenanceFileScanCapacity = MAX_BINDING_FILES_TO_SCAN * 2;
|
|
284
308
|
var MAX_MAINTENANCE_JOBS = 64;
|
|
285
309
|
var maintenanceJobs = new Map;
|
|
286
310
|
var lockfileWithSync = import_proper_lockfile.default;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
realpath: false
|
|
295
|
-
});
|
|
296
|
-
} catch (error) {
|
|
297
|
-
lastError = error;
|
|
298
|
-
if (attempt + 1 < attempts)
|
|
299
|
-
Atomics.wait(syncLockWait, 0, 0, 25);
|
|
300
|
-
}
|
|
311
|
+
|
|
312
|
+
class ScopeCoordinationAbort extends Error {
|
|
313
|
+
failure;
|
|
314
|
+
constructor(failure) {
|
|
315
|
+
super(failure.message);
|
|
316
|
+
this.failure = failure;
|
|
317
|
+
this.name = "ScopeCoordinationAbort";
|
|
301
318
|
}
|
|
302
|
-
throw lastError instanceof Error ? lastError : new Error("Lock unavailable");
|
|
303
319
|
}
|
|
304
320
|
var WINDOWS_RESERVED = new Set([
|
|
305
321
|
"CON",
|
|
@@ -438,9 +454,6 @@ function ensureLockTargetSync(targetPath) {
|
|
|
438
454
|
return false;
|
|
439
455
|
}
|
|
440
456
|
}
|
|
441
|
-
function exactBindingPrefix(taskId) {
|
|
442
|
-
return `binding-${taskId}-`;
|
|
443
|
-
}
|
|
444
457
|
function readBoundedFile(filePath) {
|
|
445
458
|
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
446
459
|
let fd;
|
|
@@ -464,6 +477,365 @@ function readBoundedFile(filePath) {
|
|
|
464
477
|
} catch {}
|
|
465
478
|
}
|
|
466
479
|
}
|
|
480
|
+
function scopeBindingArchivePath(filePath) {
|
|
481
|
+
return `${filePath}.imported`;
|
|
482
|
+
}
|
|
483
|
+
function collisionSafeScopeBindingArchivePath(filePath) {
|
|
484
|
+
const canonical = scopeBindingArchivePath(filePath);
|
|
485
|
+
if (!fs.existsSync(canonical))
|
|
486
|
+
return canonical;
|
|
487
|
+
for (let suffix = 1;suffix <= 1e4; suffix += 1) {
|
|
488
|
+
const candidate = `${canonical}.${suffix}`;
|
|
489
|
+
if (!fs.existsSync(candidate))
|
|
490
|
+
return candidate;
|
|
491
|
+
}
|
|
492
|
+
throw new Error(`No collision-safe archive path available for ${path2.basename(filePath)}.`);
|
|
493
|
+
}
|
|
494
|
+
function scopeBindingShadowPayload(binding) {
|
|
495
|
+
return JSON.stringify(binding, null, 2);
|
|
496
|
+
}
|
|
497
|
+
function coordinationScopeEntityKey(binding) {
|
|
498
|
+
return binding.generationId;
|
|
499
|
+
}
|
|
500
|
+
function parseCoordinationScopeBinding(directory, row) {
|
|
501
|
+
try {
|
|
502
|
+
const binding = validateScopeBindingPayload(directory, JSON.parse(row.payload));
|
|
503
|
+
if (!binding || binding.generationId !== row.entityKey || binding.revision !== row.revision || binding.lifecycleState !== row.status) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
return binding;
|
|
507
|
+
} catch {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function transitionScopeBindingState(directory, binding, expectedRevision) {
|
|
512
|
+
const payload = scopeBindingShadowPayload(binding);
|
|
513
|
+
const idempotencyKey = createHash("sha256").update(`${binding.bindingId}\x00${binding.generationId}\x00${binding.revision}\x00${binding.lifecycleState}\x00${payload}`).digest("hex");
|
|
514
|
+
try {
|
|
515
|
+
const result = transitionCoordinationState(directory, {
|
|
516
|
+
namespace: SCOPE_BINDING_COORDINATION_NAMESPACE,
|
|
517
|
+
entityKey: coordinationScopeEntityKey(binding),
|
|
518
|
+
expectedRevision,
|
|
519
|
+
generation: 1,
|
|
520
|
+
status: binding.lifecycleState,
|
|
521
|
+
payload,
|
|
522
|
+
event: {
|
|
523
|
+
streamId: `scope-binding:${binding.bindingId}`,
|
|
524
|
+
idempotencyKey,
|
|
525
|
+
eventType: binding.lifecycleState,
|
|
526
|
+
payload
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
if (result.outcome !== "applied" && result.outcome !== "duplicate") {
|
|
530
|
+
return {
|
|
531
|
+
ok: false,
|
|
532
|
+
code: result.outcome === "revision_conflict" || result.outcome === "stale_generation" ? "SCOPE_BINDING_STALE" : "SCOPE_BINDING_PERSISTENCE_FAILED",
|
|
533
|
+
message: `Scope binding transition failed: ${result.outcome}.`
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const persisted = result.state && parseCoordinationScopeBinding(directory, result.state);
|
|
537
|
+
return persisted ? { ok: true, value: persisted } : persistenceFailure("Scope binding transition could not be re-read from coordination state.");
|
|
538
|
+
} catch (error) {
|
|
539
|
+
return persistenceFailure(`Scope binding transition failed: ${error instanceof Error ? error.message : "unknown coordination error"}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function transitionScopeBindingStateWithinTransaction(directory, binding, expectedRevision) {
|
|
543
|
+
const payload = scopeBindingShadowPayload(binding);
|
|
544
|
+
const idempotencyKey = createHash("sha256").update(`${binding.bindingId}\x00${binding.generationId}\x00${binding.revision}\x00${binding.lifecycleState}\x00${payload}`).digest("hex");
|
|
545
|
+
try {
|
|
546
|
+
const result = transitionCoordinationStateWithinTransaction(directory, {
|
|
547
|
+
namespace: SCOPE_BINDING_COORDINATION_NAMESPACE,
|
|
548
|
+
entityKey: coordinationScopeEntityKey(binding),
|
|
549
|
+
expectedRevision,
|
|
550
|
+
generation: 1,
|
|
551
|
+
status: binding.lifecycleState,
|
|
552
|
+
payload,
|
|
553
|
+
event: {
|
|
554
|
+
streamId: `scope-binding:${binding.bindingId}`,
|
|
555
|
+
idempotencyKey,
|
|
556
|
+
eventType: binding.lifecycleState,
|
|
557
|
+
payload
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
if (result.outcome !== "applied" && result.outcome !== "duplicate") {
|
|
561
|
+
return {
|
|
562
|
+
ok: false,
|
|
563
|
+
code: result.outcome === "revision_conflict" || result.outcome === "stale_generation" ? "SCOPE_BINDING_STALE" : "SCOPE_BINDING_PERSISTENCE_FAILED",
|
|
564
|
+
message: `Scope binding transition failed: ${result.outcome}.`
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const persisted = result.state && parseCoordinationScopeBinding(directory, result.state);
|
|
568
|
+
return persisted ? { ok: true, value: persisted } : persistenceFailure("Scope binding transition could not be re-read from coordination state.");
|
|
569
|
+
} catch (error) {
|
|
570
|
+
return persistenceFailure(`Scope binding transition failed: ${error instanceof Error ? error.message : "unknown coordination error"}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function coordinationTimestampFromMs(value, label) {
|
|
574
|
+
if (!Number.isFinite(value)) {
|
|
575
|
+
throw new Error(`${label} must be a finite epoch-millisecond timestamp.`);
|
|
576
|
+
}
|
|
577
|
+
const iso = new Date(value).toISOString();
|
|
578
|
+
if (Number.isNaN(Date.parse(iso))) {
|
|
579
|
+
throw new Error(`${label} must be a valid epoch-millisecond timestamp.`);
|
|
580
|
+
}
|
|
581
|
+
return iso;
|
|
582
|
+
}
|
|
583
|
+
function importScopeBindingStateRows(directory, bindings) {
|
|
584
|
+
const db = getProjectDb(directory);
|
|
585
|
+
for (const binding of bindings) {
|
|
586
|
+
if (!Number.isSafeInteger(binding.revision) || binding.revision < 1) {
|
|
587
|
+
throw new Error(`Legacy binding ${binding.generationId} has invalid revision ${binding.revision}.`);
|
|
588
|
+
}
|
|
589
|
+
db.run(`INSERT INTO coordination_state
|
|
590
|
+
(namespace, entity_key, revision, generation, status, payload, updated_at)
|
|
591
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
592
|
+
SCOPE_BINDING_COORDINATION_NAMESPACE,
|
|
593
|
+
coordinationScopeEntityKey(binding),
|
|
594
|
+
binding.revision,
|
|
595
|
+
1,
|
|
596
|
+
binding.lifecycleState,
|
|
597
|
+
scopeBindingShadowPayload(binding),
|
|
598
|
+
coordinationTimestampFromMs(binding.updatedAt, "binding.updatedAt")
|
|
599
|
+
]);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function writeScopeBindingShadow(directory, binding) {
|
|
603
|
+
try {
|
|
604
|
+
const scopesDir = getScopesDir(directory);
|
|
605
|
+
fs.mkdirSync(scopesDir, { recursive: true });
|
|
606
|
+
if (!isScopesDirSafe(directory, scopesDir))
|
|
607
|
+
return;
|
|
608
|
+
atomicWriteSync(getBindingFilePath(directory, binding), scopeBindingShadowPayload(binding));
|
|
609
|
+
} catch {}
|
|
610
|
+
}
|
|
611
|
+
function archiveImportedScopeAuthorityFiles(paths) {
|
|
612
|
+
for (const filePath of paths) {
|
|
613
|
+
try {
|
|
614
|
+
if (!fs.existsSync(filePath))
|
|
615
|
+
continue;
|
|
616
|
+
const archivedPath = collisionSafeScopeBindingArchivePath(filePath);
|
|
617
|
+
fs.renameSync(filePath, archivedPath);
|
|
618
|
+
} catch {}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function fingerprintCommittedScopeImportSource(directory) {
|
|
622
|
+
const scopesDir = getScopesDir(directory);
|
|
623
|
+
try {
|
|
624
|
+
fs.lstatSync(scopesDir);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (error.code === "ENOENT")
|
|
627
|
+
return null;
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
if (!isScopesDirSafe(directory, scopesDir))
|
|
631
|
+
return null;
|
|
632
|
+
const names = fs.readdirSync(scopesDir).sort();
|
|
633
|
+
if (names.length > bindingFileScanCapacity)
|
|
634
|
+
return null;
|
|
635
|
+
const authorityFiles = [];
|
|
636
|
+
for (const name of names) {
|
|
637
|
+
if (!name.startsWith("binding-") || !name.endsWith(".json"))
|
|
638
|
+
continue;
|
|
639
|
+
const filePath = path2.join(scopesDir, name);
|
|
640
|
+
authorityFiles.push(filePath);
|
|
641
|
+
const intentPath = getRetirementIntentPath(filePath);
|
|
642
|
+
if (fs.existsSync(intentPath))
|
|
643
|
+
authorityFiles.push(intentPath);
|
|
644
|
+
}
|
|
645
|
+
for (const name of names) {
|
|
646
|
+
if (/^claim-[a-f0-9]{40}\.json$/i.test(name)) {
|
|
647
|
+
authorityFiles.push(path2.join(scopesDir, name));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (authorityFiles.length === 0)
|
|
651
|
+
return null;
|
|
652
|
+
const digest = createHash("sha256");
|
|
653
|
+
for (const filePath of authorityFiles) {
|
|
654
|
+
const raw = readBoundedFile(filePath);
|
|
655
|
+
if (raw === null)
|
|
656
|
+
return null;
|
|
657
|
+
digest.update(path2.basename(filePath));
|
|
658
|
+
digest.update(raw);
|
|
659
|
+
}
|
|
660
|
+
return { authorityFiles, sourceDigest: digest.digest("hex") };
|
|
661
|
+
}
|
|
662
|
+
function collectLegacyScopeBindingsForImport(directory) {
|
|
663
|
+
const scopesDir = getScopesDir(directory);
|
|
664
|
+
try {
|
|
665
|
+
fs.lstatSync(scopesDir);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
if (error.code === "ENOENT") {
|
|
668
|
+
return {
|
|
669
|
+
bindings: [],
|
|
670
|
+
authorityFiles: [],
|
|
671
|
+
sourceDigest: "empty",
|
|
672
|
+
rowCount: 0
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
677
|
+
if (!isScopesDirSafe(directory, scopesDir)) {
|
|
678
|
+
throw new Error("The durable scope store failed containment checks.");
|
|
679
|
+
}
|
|
680
|
+
if (!migrateLegacyBindingsSync(directory)) {
|
|
681
|
+
throw new Error("The durable binding set could not be completely evaluated.");
|
|
682
|
+
}
|
|
683
|
+
const names = fs.readdirSync(scopesDir).sort();
|
|
684
|
+
if (names.length > bindingFileScanCapacity) {
|
|
685
|
+
throw new Error("The durable binding set exceeds the scan capacity.");
|
|
686
|
+
}
|
|
687
|
+
const authorityFiles = [];
|
|
688
|
+
const bindings = [];
|
|
689
|
+
for (const name of names) {
|
|
690
|
+
if (!name.startsWith("binding-") || !name.endsWith(".json"))
|
|
691
|
+
continue;
|
|
692
|
+
const filePath = path2.join(scopesDir, name);
|
|
693
|
+
authorityFiles.push(filePath);
|
|
694
|
+
const raw = readBoundedFile(filePath);
|
|
695
|
+
if (!raw) {
|
|
696
|
+
throw new Error(`Legacy scope binding ${name} is unreadable.`);
|
|
697
|
+
}
|
|
698
|
+
const binding = validateScopeBindingPayload(directory, JSON.parse(raw));
|
|
699
|
+
if (!binding || !exactFilenameMatches(directory, filePath, binding)) {
|
|
700
|
+
throw new Error(`Legacy scope binding ${name} is malformed.`);
|
|
701
|
+
}
|
|
702
|
+
const intentPath = getRetirementIntentPath(filePath);
|
|
703
|
+
if (hasDurableRetirementIntent(filePath, binding)) {
|
|
704
|
+
authorityFiles.push(intentPath);
|
|
705
|
+
let createdAt = Date.now();
|
|
706
|
+
const rawIntent = readBoundedFile(intentPath);
|
|
707
|
+
if (!rawIntent) {
|
|
708
|
+
throw new Error(`Legacy retirement intent ${path2.basename(intentPath)} is unreadable.`);
|
|
709
|
+
}
|
|
710
|
+
const parsedIntent = JSON.parse(rawIntent);
|
|
711
|
+
if (parsedIntent.bindingId !== binding.bindingId || parsedIntent.generationId !== binding.generationId) {
|
|
712
|
+
throw new Error(`Legacy retirement intent ${path2.basename(intentPath)} mismatches its generation.`);
|
|
713
|
+
}
|
|
714
|
+
if (typeof parsedIntent.createdAt === "number")
|
|
715
|
+
createdAt = parsedIntent.createdAt;
|
|
716
|
+
bindings.push({
|
|
717
|
+
...binding,
|
|
718
|
+
lifecycleState: "revoked",
|
|
719
|
+
updatedAt: createdAt,
|
|
720
|
+
expiresAt: Math.min(binding.expiresAt, createdAt)
|
|
721
|
+
});
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
bindings.push(binding);
|
|
725
|
+
}
|
|
726
|
+
for (const name of names) {
|
|
727
|
+
if (!/^claim-[a-f0-9]{40}\.json$/i.test(name))
|
|
728
|
+
continue;
|
|
729
|
+
const filePath = path2.join(scopesDir, name);
|
|
730
|
+
authorityFiles.push(filePath);
|
|
731
|
+
const raw = readBoundedFile(filePath);
|
|
732
|
+
if (!raw)
|
|
733
|
+
throw new Error(`Legacy claim receipt ${name} is unreadable.`);
|
|
734
|
+
const parsed = JSON.parse(raw);
|
|
735
|
+
if (parsed.version !== 1 || !isScopeBindingIdentity(parsed.predecessorGenerationId) || !isScopeBindingIdentity(parsed.winnerGenerationId) || typeof parsed.childSessionId !== "string" || !parsed.childSessionId.trim() || typeof parsed.dispatchCallId !== "string" || !parsed.dispatchCallId.trim()) {
|
|
736
|
+
throw new Error(`Legacy claim receipt ${name} is malformed.`);
|
|
737
|
+
}
|
|
738
|
+
const winner = bindings.find((binding) => binding.generationId === parsed.winnerGenerationId);
|
|
739
|
+
if (!winner) {
|
|
740
|
+
throw new Error(`Legacy claim receipt ${name} has no winner generation.`);
|
|
741
|
+
}
|
|
742
|
+
if (winner.lifecycleState === "live" && winner.ownerSessionId !== parsed.childSessionId) {
|
|
743
|
+
throw new Error(`Legacy claim receipt ${name} disagrees with the winner generation.`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const digest = createHash("sha256");
|
|
747
|
+
for (const filePath of authorityFiles) {
|
|
748
|
+
if (!fs.existsSync(filePath))
|
|
749
|
+
continue;
|
|
750
|
+
digest.update(path2.basename(filePath));
|
|
751
|
+
digest.update(fs.readFileSync(filePath));
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
bindings,
|
|
755
|
+
authorityFiles,
|
|
756
|
+
sourceDigest: digest.digest("hex"),
|
|
757
|
+
rowCount: bindings.length
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function ensureScopeBindingAuthorityImported(directory) {
|
|
761
|
+
try {
|
|
762
|
+
const authorityExists = listCoordinationStates(directory, SCOPE_BINDING_COORDINATION_NAMESPACE, 1).length > 0;
|
|
763
|
+
if (authorityExists) {
|
|
764
|
+
const importStatement = getProjectDb(directory).prepare("SELECT source_digest FROM coordination_import WHERE source = ?");
|
|
765
|
+
let importRow;
|
|
766
|
+
try {
|
|
767
|
+
importRow = importStatement.get(SCOPE_BINDING_COORDINATION_IMPORT_SOURCE);
|
|
768
|
+
} finally {
|
|
769
|
+
importStatement.finalize();
|
|
770
|
+
}
|
|
771
|
+
const committedSource = fingerprintCommittedScopeImportSource(directory);
|
|
772
|
+
if (!importRow || !committedSource || importRow.source_digest !== committedSource.sourceDigest) {
|
|
773
|
+
return { ok: true, value: undefined };
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const legacy = collectLegacyScopeBindingsForImport(directory);
|
|
777
|
+
if (legacy.rowCount === 0 && legacy.authorityFiles.length === 0) {
|
|
778
|
+
return { ok: true, value: undefined };
|
|
779
|
+
}
|
|
780
|
+
const importedBindings = legacy.bindings.map((binding) => ({ ...binding }));
|
|
781
|
+
if (authorityExists) {
|
|
782
|
+
archiveImportedScopeAuthorityFiles(legacy.authorityFiles);
|
|
783
|
+
for (const binding of importedBindings) {
|
|
784
|
+
writeScopeBindingShadow(directory, binding);
|
|
785
|
+
}
|
|
786
|
+
return { ok: true, value: undefined };
|
|
787
|
+
}
|
|
788
|
+
const outcome = importCoordinationOnce(directory, {
|
|
789
|
+
source: SCOPE_BINDING_COORDINATION_IMPORT_SOURCE,
|
|
790
|
+
sourceDigest: legacy.sourceDigest,
|
|
791
|
+
rowCount: legacy.rowCount,
|
|
792
|
+
emptyNamespace: SCOPE_BINDING_COORDINATION_NAMESPACE
|
|
793
|
+
}, () => {
|
|
794
|
+
importScopeBindingStateRows(directory, importedBindings);
|
|
795
|
+
});
|
|
796
|
+
if (outcome === "imported" || outcome === "already_imported") {
|
|
797
|
+
archiveImportedScopeAuthorityFiles(legacy.authorityFiles);
|
|
798
|
+
for (const binding of importedBindings) {
|
|
799
|
+
writeScopeBindingShadow(directory, binding);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return { ok: true, value: undefined };
|
|
803
|
+
} catch (error) {
|
|
804
|
+
return persistenceFailure(`Scope binding import failed closed: ${error instanceof Error ? error.message : "unknown import error"}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function readAllAuthoritativeScopeBindings(directory, options = {}) {
|
|
808
|
+
const imported = ensureScopeBindingAuthorityImported(directory);
|
|
809
|
+
if (!imported.ok)
|
|
810
|
+
return imported;
|
|
811
|
+
try {
|
|
812
|
+
const scanCapacity = options.maintenanceScan ? maintenanceFileScanCapacity : bindingFileScanCapacity;
|
|
813
|
+
const rows = listCoordinationStates(directory, SCOPE_BINDING_COORDINATION_NAMESPACE, scanCapacity + 1);
|
|
814
|
+
if (rows.length > scanCapacity) {
|
|
815
|
+
return {
|
|
816
|
+
ok: false,
|
|
817
|
+
code: "SCOPE_BINDING_STORE_OVERLOADED",
|
|
818
|
+
message: "The durable binding set exceeds the scan capacity."
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
const bindings = rows.map((row) => parseCoordinationScopeBinding(directory, row));
|
|
822
|
+
if (bindings.some((binding) => binding === null)) {
|
|
823
|
+
return persistenceFailure("The authoritative scope binding set contains malformed coordination rows.");
|
|
824
|
+
}
|
|
825
|
+
const complete = bindings;
|
|
826
|
+
const liveCount = complete.filter((binding) => binding.lifecycleState === "live" && binding.expiresAt > Date.now()).length;
|
|
827
|
+
if (options.enforceLiveCapacity !== false && liveCount > liveBindingCapacity) {
|
|
828
|
+
return {
|
|
829
|
+
ok: false,
|
|
830
|
+
code: "SCOPE_BINDING_STORE_OVERLOADED",
|
|
831
|
+
message: "The durable binding set exceeds the live binding capacity."
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
return { ok: true, value: complete };
|
|
835
|
+
} catch (error) {
|
|
836
|
+
return persistenceFailure(`Authoritative scope binding read failed: ${error instanceof Error ? error.message : "unknown coordination error"}`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
467
839
|
function deterministicIdentity(value) {
|
|
468
840
|
const hex = createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
469
841
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20)}`;
|
|
@@ -683,49 +1055,38 @@ async function writeScopeBindingToDisk(directory, binding) {
|
|
|
683
1055
|
const files = normalizeScopeFiles(binding.files);
|
|
684
1056
|
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())
|
|
685
1057
|
return persistenceFailure("Binding identity, workspace, scope, or lease is invalid.");
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1058
|
+
const imported = ensureScopeBindingAuthorityImported(directory);
|
|
1059
|
+
if (!imported.ok)
|
|
1060
|
+
return imported;
|
|
1061
|
+
const all = readAllAuthoritativeScopeBindings(directory, {
|
|
1062
|
+
enforceLiveCapacity: false
|
|
1063
|
+
});
|
|
1064
|
+
if (!all.ok)
|
|
1065
|
+
return all;
|
|
1066
|
+
const current = all.value.find((candidate) => candidate.generationId === binding.generationId);
|
|
1067
|
+
if (current && current.bindingId !== binding.bindingId) {
|
|
1068
|
+
return {
|
|
1069
|
+
ok: false,
|
|
1070
|
+
code: "SCOPE_BINDING_STALE",
|
|
1071
|
+
message: "The exact generation identity collides with another binding."
|
|
1072
|
+
};
|
|
692
1073
|
}
|
|
693
|
-
if (
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
try {
|
|
697
|
-
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT;
|
|
698
|
-
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
699
|
-
const fd = fs.openSync(scopePath, flags | nofollow);
|
|
700
|
-
fs.closeSync(fd);
|
|
701
|
-
} catch {
|
|
702
|
-
return persistenceFailure("Could not create the exact generation record.");
|
|
1074
|
+
if (current && current.revision === binding.revision && current.lifecycleState === binding.lifecycleState && JSON.stringify(current.files) === JSON.stringify(files)) {
|
|
1075
|
+
writeScopeBindingShadow(directory, current);
|
|
1076
|
+
return { ok: true, value: current };
|
|
703
1077
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
retries: { retries: 3, minTimeout: 50, maxTimeout: 200 },
|
|
712
|
-
realpath: false
|
|
713
|
-
});
|
|
714
|
-
await atomicWrite(scopePath, content);
|
|
715
|
-
const verifiedRaw = readBoundedFile(scopePath);
|
|
716
|
-
const verified = verifiedRaw ? validateScopeBindingPayload(directory, JSON.parse(verifiedRaw)) : null;
|
|
717
|
-
if (!verified || verified.bindingId !== binding.bindingId || verified.generationId !== binding.generationId || verified.revision !== binding.revision)
|
|
718
|
-
return persistenceFailure("Exact generation verification failed after write.");
|
|
719
|
-
return { ok: true, value: verified };
|
|
720
|
-
} catch (error) {
|
|
721
|
-
return persistenceFailure(`Exact generation write failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
|
|
722
|
-
} finally {
|
|
723
|
-
if (release) {
|
|
724
|
-
try {
|
|
725
|
-
await release();
|
|
726
|
-
} catch {}
|
|
727
|
-
}
|
|
1078
|
+
const expectedRevision = current ? binding.revision - 1 : null;
|
|
1079
|
+
if (binding.revision !== (current?.revision ?? 0) + 1) {
|
|
1080
|
+
return {
|
|
1081
|
+
ok: false,
|
|
1082
|
+
code: "SCOPE_BINDING_STALE",
|
|
1083
|
+
message: "Exact generation write requires the next CAS revision."
|
|
1084
|
+
};
|
|
728
1085
|
}
|
|
1086
|
+
const persisted = transitionScopeBindingState(directory, { ...binding, files }, expectedRevision);
|
|
1087
|
+
if (persisted.ok)
|
|
1088
|
+
writeScopeBindingShadow(directory, persisted.value);
|
|
1089
|
+
return persisted;
|
|
729
1090
|
}
|
|
730
1091
|
function persistenceFailure(message) {
|
|
731
1092
|
return { ok: false, code: "SCOPE_BINDING_PERSISTENCE_FAILED", message };
|
|
@@ -746,6 +1107,69 @@ async function persistAndRegisterScopeBinding(directory, binding) {
|
|
|
746
1107
|
message: rollback.ok ? admission.message : `Memory admission failed and rollback failed: ${rollback.message}`
|
|
747
1108
|
};
|
|
748
1109
|
}
|
|
1110
|
+
function rollbackAdmissionFailure(directory, persisted, predecessors, admission) {
|
|
1111
|
+
const now = Date.now();
|
|
1112
|
+
const revokedCandidate = {
|
|
1113
|
+
...persisted,
|
|
1114
|
+
revision: persisted.revision + 1,
|
|
1115
|
+
lifecycleState: "revoked",
|
|
1116
|
+
updatedAt: now,
|
|
1117
|
+
expiresAt: Math.min(persisted.expiresAt, now)
|
|
1118
|
+
};
|
|
1119
|
+
const restoredCandidates = predecessors.map(({ original, superseded }) => ({
|
|
1120
|
+
...original,
|
|
1121
|
+
revision: superseded.revision + 1,
|
|
1122
|
+
lifecycleState: "live",
|
|
1123
|
+
updatedAt: now
|
|
1124
|
+
}));
|
|
1125
|
+
let failureMessage = null;
|
|
1126
|
+
try {
|
|
1127
|
+
withCoordinationTransaction(directory, () => {
|
|
1128
|
+
const revoked = _scopePersistenceInternals.transitionScopeBindingStateWithinTransaction(directory, revokedCandidate, persisted.revision);
|
|
1129
|
+
if (!revoked.ok) {
|
|
1130
|
+
failureMessage = revoked.message;
|
|
1131
|
+
throw new Error("Scope rollback revocation transition failed.");
|
|
1132
|
+
}
|
|
1133
|
+
for (let index = 0;index < restoredCandidates.length; index += 1) {
|
|
1134
|
+
const restored = restoredCandidates[index];
|
|
1135
|
+
const superseded = predecessors[index].superseded;
|
|
1136
|
+
const result = _scopePersistenceInternals.transitionScopeBindingStateWithinTransaction(directory, restored, superseded.revision);
|
|
1137
|
+
if (!result.ok) {
|
|
1138
|
+
failureMessage = result.message;
|
|
1139
|
+
throw new Error("Scope rollback predecessor restore failed.");
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
return persistenceFailure(`Memory admission failed and durable rollback failed: ${failureMessage ?? (error instanceof Error ? error.message : "unknown coordination error")}`);
|
|
1145
|
+
}
|
|
1146
|
+
installScopeBindingTombstone(revokedCandidate);
|
|
1147
|
+
writeScopeBindingShadow(directory, revokedCandidate);
|
|
1148
|
+
const admitted = [];
|
|
1149
|
+
for (const restored of restoredCandidates) {
|
|
1150
|
+
writeScopeBindingShadow(directory, restored);
|
|
1151
|
+
const memory = updateExactScopeBinding(restored);
|
|
1152
|
+
const admissionResult = memory.ok ? memory : registerScopeBinding(restored);
|
|
1153
|
+
if (!admissionResult.ok) {
|
|
1154
|
+
for (const applied of admitted) {
|
|
1155
|
+
installScopeBindingTombstone({
|
|
1156
|
+
...applied,
|
|
1157
|
+
revision: applied.revision + 1,
|
|
1158
|
+
lifecycleState: "revoked",
|
|
1159
|
+
updatedAt: Date.now(),
|
|
1160
|
+
expiresAt: Math.min(applied.expiresAt, Date.now())
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
return persistenceFailure(`Memory admission failed and predecessor re-admission failed: ${admissionResult.message}`);
|
|
1164
|
+
}
|
|
1165
|
+
admitted.push(restored);
|
|
1166
|
+
}
|
|
1167
|
+
return {
|
|
1168
|
+
ok: false,
|
|
1169
|
+
code: admission.code,
|
|
1170
|
+
message: admission.message
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
749
1173
|
function readScopeBindingFromDisk(input) {
|
|
750
1174
|
const resolved = resolveScopeBindingFromDisk({
|
|
751
1175
|
...input,
|
|
@@ -756,12 +1180,14 @@ function readScopeBindingFromDisk(input) {
|
|
|
756
1180
|
function resolveScopeBindingFromDisk(input) {
|
|
757
1181
|
if (!isSafeTaskId(input.taskId))
|
|
758
1182
|
return { status: "not_declared" };
|
|
759
|
-
const completeSet =
|
|
760
|
-
if (!completeSet) {
|
|
761
|
-
|
|
1183
|
+
const completeSet = readAllAuthoritativeScopeBindings(input.directory);
|
|
1184
|
+
if (!completeSet.ok) {
|
|
1185
|
+
if (completeSet.code === "SCOPE_BINDING_STORE_OVERLOADED") {
|
|
1186
|
+
scheduleScopeBindingMaintenance(input.directory);
|
|
1187
|
+
}
|
|
762
1188
|
return { status: "overloaded" };
|
|
763
1189
|
}
|
|
764
|
-
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 ||
|
|
1190
|
+
const candidates = completeSet.value.filter((parsed) => parsed.taskId === input.taskId && parsed.planId === derivePlanId(input.plan) && parsed.planStructureHash === computePlanStructureHash(input.plan) && parsed.ownerSessionId === input.ownerSessionId && (input.requireDispatchCorrelation !== true || isDispatchCorrelated2(parsed)) && (input.requireDeclaration !== true || parsed.activation === "declaration" && parsed.dispatchCallId === undefined) && (input.parentCallId === undefined || parsed.parentCallId === input.parentCallId));
|
|
765
1191
|
const live = candidates.filter((candidate) => candidate.lifecycleState === "live" && candidate.expiresAt > Date.now());
|
|
766
1192
|
if (live.length > 1)
|
|
767
1193
|
return {
|
|
@@ -772,13 +1198,29 @@ function resolveScopeBindingFromDisk(input) {
|
|
|
772
1198
|
if (live.length === 1)
|
|
773
1199
|
return { status: "found", binding: live[0] };
|
|
774
1200
|
const expired = candidates.filter((candidate) => candidate.lifecycleState !== "live" || candidate.expiresAt <= Date.now());
|
|
1201
|
+
if (candidates.length > 0 && !input.includeExpired) {
|
|
1202
|
+
return { status: "not_declared" };
|
|
1203
|
+
}
|
|
775
1204
|
return input.includeExpired && expired.length > 0 ? {
|
|
776
1205
|
status: "expired",
|
|
777
1206
|
candidates: expired.slice(0, 8),
|
|
778
1207
|
totalCandidates: expired.length
|
|
779
|
-
} :
|
|
1208
|
+
} : (() => {
|
|
1209
|
+
const inMemory = input.requireDeclaration === true ? [] : getActiveScopeBindingsForSession({
|
|
1210
|
+
directory: input.directory,
|
|
1211
|
+
activeSessionId: input.ownerSessionId
|
|
1212
|
+
}).filter((binding) => binding.taskId === input.taskId && binding.planId === derivePlanId(input.plan) && binding.planStructureHash === computePlanStructureHash(input.plan) && (input.requireDispatchCorrelation !== true || isDispatchCorrelated2(binding)) && (input.parentCallId === undefined || binding.parentCallId === input.parentCallId));
|
|
1213
|
+
if (inMemory.length > 1) {
|
|
1214
|
+
return {
|
|
1215
|
+
status: "ambiguous",
|
|
1216
|
+
candidates: inMemory.slice(0, 8),
|
|
1217
|
+
totalCandidates: inMemory.length
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
return inMemory.length === 1 ? { status: "found", binding: inMemory[0] } : { status: "not_declared" };
|
|
1221
|
+
})();
|
|
780
1222
|
}
|
|
781
|
-
function
|
|
1223
|
+
function isDispatchCorrelated2(binding) {
|
|
782
1224
|
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;
|
|
783
1225
|
}
|
|
784
1226
|
function clearScopeBindingFromDisk(input) {
|
|
@@ -788,285 +1230,156 @@ function clearScopeBindingFromDisk(input) {
|
|
|
788
1230
|
} catch (error) {
|
|
789
1231
|
return persistenceFailure(`Scope retirement project-root validation failed: ${error instanceof Error ? error.message : "unknown validation error"}`);
|
|
790
1232
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
createdAt: Date.now()
|
|
803
|
-
}));
|
|
804
|
-
scheduleScopeBindingMaintenance(resolvedWorkspace);
|
|
805
|
-
const lockPath = getGenerationLockPath(scopePath);
|
|
806
|
-
if (!ensureLockTargetSync(lockPath))
|
|
807
|
-
return persistenceFailure("Could not prepare exact retirement lock.");
|
|
808
|
-
release = lockSyncWithBoundedRetry(lockPath);
|
|
809
|
-
const raw = readBoundedFile(scopePath);
|
|
810
|
-
if (!raw) {
|
|
811
|
-
try {
|
|
812
|
-
fs.unlinkSync(retirementIntentPath);
|
|
813
|
-
} catch {}
|
|
814
|
-
return localRetirement ? { ok: true, value: localRetirement } : persistenceFailure("Exact generation disappeared before serialized retirement.");
|
|
815
|
-
}
|
|
816
|
-
const current = validateScopeBindingPayload(resolvedWorkspace, JSON.parse(raw));
|
|
817
|
-
if (!current || current.bindingId !== input.binding.bindingId || current.generationId !== input.binding.generationId)
|
|
818
|
-
return persistenceFailure("Exact generation identity changed before serialized retirement.");
|
|
819
|
-
if (current.lifecycleState !== "live") {
|
|
820
|
-
installScopeBindingTombstone(current);
|
|
821
|
-
try {
|
|
822
|
-
fs.unlinkSync(retirementIntentPath);
|
|
823
|
-
} catch {}
|
|
824
|
-
return { ok: true, value: current };
|
|
825
|
-
}
|
|
826
|
-
const now = Date.now();
|
|
827
|
-
const tombstone = {
|
|
828
|
-
...current,
|
|
829
|
-
revision: current.revision + 1,
|
|
830
|
-
lifecycleState: "revoked",
|
|
831
|
-
updatedAt: now,
|
|
832
|
-
expiresAt: Math.min(current.expiresAt, now)
|
|
833
|
-
};
|
|
834
|
-
atomicWriteSync(scopePath, JSON.stringify(tombstone, null, 2));
|
|
835
|
-
const verifiedRaw = readBoundedFile(scopePath);
|
|
836
|
-
const verified = verifiedRaw ? validateScopeBindingPayload(resolvedWorkspace, JSON.parse(verifiedRaw)) : null;
|
|
837
|
-
if (!verified || verified.revision !== tombstone.revision || verified.lifecycleState !== "revoked")
|
|
838
|
-
return persistenceFailure("Serialized retirement verification failed.");
|
|
839
|
-
installScopeBindingTombstone(verified);
|
|
840
|
-
try {
|
|
841
|
-
fs.unlinkSync(retirementIntentPath);
|
|
842
|
-
} catch {}
|
|
843
|
-
scheduleScopeBindingMaintenance(resolvedWorkspace);
|
|
844
|
-
return { ok: true, value: verified };
|
|
845
|
-
} catch (error) {
|
|
846
|
-
return persistenceFailure(`Serialized retirement failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
|
|
847
|
-
} finally {
|
|
848
|
-
try {
|
|
849
|
-
release?.();
|
|
850
|
-
} catch {}
|
|
1233
|
+
const imported = ensureScopeBindingAuthorityImported(input.directory);
|
|
1234
|
+
if (!imported.ok)
|
|
1235
|
+
return imported;
|
|
1236
|
+
const all = readAllAuthoritativeScopeBindings(input.directory, {
|
|
1237
|
+
enforceLiveCapacity: false
|
|
1238
|
+
});
|
|
1239
|
+
if (!all.ok)
|
|
1240
|
+
return all;
|
|
1241
|
+
const current = all.value.find((candidate) => candidate.generationId === input.binding.generationId && candidate.bindingId === input.binding.bindingId);
|
|
1242
|
+
if (!current) {
|
|
1243
|
+
return localRetirement ? { ok: true, value: localRetirement } : persistenceFailure("Exact generation disappeared before serialized retirement.");
|
|
851
1244
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const lockReady = await ensureLockTarget(generationLockPath);
|
|
857
|
-
if (!lockReady) {
|
|
858
|
-
installFailedRevocationOverlay(binding, reason);
|
|
859
|
-
return persistenceFailure("Could not prepare exact generation lock.");
|
|
1245
|
+
if (current.lifecycleState !== "live") {
|
|
1246
|
+
installScopeBindingTombstone(current);
|
|
1247
|
+
writeScopeBindingShadow(input.directory, current);
|
|
1248
|
+
return { ok: true, value: current };
|
|
860
1249
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1250
|
+
const now = Date.now();
|
|
1251
|
+
const tombstone = {
|
|
1252
|
+
...current,
|
|
1253
|
+
revision: current.revision + 1,
|
|
1254
|
+
lifecycleState: "revoked",
|
|
1255
|
+
updatedAt: now,
|
|
1256
|
+
expiresAt: Math.min(current.expiresAt, now)
|
|
1257
|
+
};
|
|
1258
|
+
const retired = transitionScopeBindingState(input.directory, tombstone, current.revision);
|
|
1259
|
+
if (!retired.ok)
|
|
1260
|
+
return retired;
|
|
1261
|
+
installScopeBindingTombstone(retired.value);
|
|
1262
|
+
writeScopeBindingShadow(input.directory, retired.value);
|
|
1263
|
+
scheduleScopeBindingMaintenance(input.directory);
|
|
1264
|
+
return retired;
|
|
1265
|
+
}
|
|
1266
|
+
async function tombstoneScopeBinding(directory, binding, reason) {
|
|
1267
|
+
const imported = ensureScopeBindingAuthorityImported(directory);
|
|
1268
|
+
if (!imported.ok) {
|
|
1269
|
+
installFailedRevocationOverlay(binding, reason);
|
|
1270
|
+
return imported;
|
|
1271
|
+
}
|
|
1272
|
+
const all = readAllAuthoritativeScopeBindings(directory, {
|
|
1273
|
+
enforceLiveCapacity: false
|
|
1274
|
+
});
|
|
1275
|
+
if (!all.ok) {
|
|
1276
|
+
installFailedRevocationOverlay(binding, reason);
|
|
1277
|
+
return all;
|
|
1278
|
+
}
|
|
1279
|
+
const current = all.value.find((candidate) => candidate.generationId === binding.generationId && candidate.bindingId === binding.bindingId);
|
|
1280
|
+
if (!current) {
|
|
1281
|
+
installFailedRevocationOverlay(binding, reason);
|
|
1282
|
+
return persistenceFailure("Exact generation disappeared before tombstone.");
|
|
1283
|
+
}
|
|
1284
|
+
if (current.revision !== binding.revision) {
|
|
1285
|
+
return {
|
|
1286
|
+
ok: false,
|
|
1287
|
+
code: "SCOPE_BINDING_STALE",
|
|
1288
|
+
message: "Tombstone CAS revision is stale."
|
|
893
1289
|
};
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1290
|
+
}
|
|
1291
|
+
if (current.lifecycleState !== "live")
|
|
1292
|
+
return { ok: true, value: current };
|
|
1293
|
+
const now = Date.now();
|
|
1294
|
+
const tombstone = {
|
|
1295
|
+
...current,
|
|
1296
|
+
revision: current.revision + 1,
|
|
1297
|
+
lifecycleState: reason,
|
|
1298
|
+
updatedAt: now,
|
|
1299
|
+
expiresAt: Math.min(current.expiresAt, now)
|
|
1300
|
+
};
|
|
1301
|
+
const persisted = transitionScopeBindingState(directory, tombstone, current.revision);
|
|
1302
|
+
if (!persisted.ok) {
|
|
906
1303
|
installFailedRevocationOverlay(binding, reason);
|
|
907
|
-
return
|
|
908
|
-
} finally {
|
|
909
|
-
if (release)
|
|
910
|
-
try {
|
|
911
|
-
await release();
|
|
912
|
-
} catch {}
|
|
1304
|
+
return persisted;
|
|
913
1305
|
}
|
|
1306
|
+
clearExactScopeBinding(binding);
|
|
1307
|
+
installScopeBindingTombstone(persisted.value);
|
|
1308
|
+
writeScopeBindingShadow(directory, persisted.value);
|
|
1309
|
+
scheduleScopeBindingMaintenance(directory);
|
|
1310
|
+
return persisted;
|
|
914
1311
|
}
|
|
915
1312
|
async function ensureLockTarget(targetPath) {
|
|
916
1313
|
return ensureLockTargetSync(targetPath);
|
|
917
1314
|
}
|
|
918
1315
|
async function refreshScopeBindingLease(input) {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
retries: { retries: 3, minTimeout: 50, maxTimeout: 200 },
|
|
934
|
-
realpath: false
|
|
935
|
-
});
|
|
936
|
-
if (hasDurableRetirementIntent(scopePath, input))
|
|
937
|
-
return {
|
|
938
|
-
ok: false,
|
|
939
|
-
code: "SCOPE_BINDING_STALE",
|
|
940
|
-
message: "Refresh cannot renew a generation with durable retirement intent."
|
|
941
|
-
};
|
|
942
|
-
const raw = readBoundedFile(scopePath);
|
|
943
|
-
const current = raw ? validateScopeBindingPayload(input.directory, JSON.parse(raw)) : null;
|
|
944
|
-
if (!current || current.bindingId !== input.bindingId || current.generationId !== input.generationId || current.taskId !== input.taskId || current.ownerSessionId !== input.activeSessionId)
|
|
945
|
-
return {
|
|
946
|
-
ok: false,
|
|
947
|
-
code: "SCOPE_BINDING_STALE",
|
|
948
|
-
message: "Refresh identity is not the exact current session/task generation."
|
|
949
|
-
};
|
|
950
|
-
if (current.lifecycleState !== "live" || current.expiresAt <= Date.now() || current.revision !== input.expectedRevision)
|
|
951
|
-
return {
|
|
952
|
-
ok: false,
|
|
953
|
-
code: current.expiresAt <= Date.now() ? "SCOPE_BINDING_EXPIRED" : "SCOPE_BINDING_STALE",
|
|
954
|
-
message: "Refresh cannot renew an expired, tombstoned, or stale generation."
|
|
955
|
-
};
|
|
956
|
-
const now = Date.now();
|
|
957
|
-
const refreshed = {
|
|
958
|
-
...current,
|
|
959
|
-
revision: current.revision + 1,
|
|
960
|
-
updatedAt: now,
|
|
961
|
-
leaseStartedAt: now,
|
|
962
|
-
expiresAt: now + Math.max(1, input.ttlMs ?? DEFAULT_SCOPE_BINDING_TTL_MS)
|
|
1316
|
+
const imported = ensureScopeBindingAuthorityImported(input.directory);
|
|
1317
|
+
if (!imported.ok)
|
|
1318
|
+
return imported;
|
|
1319
|
+
const all = readAllAuthoritativeScopeBindings(input.directory, {
|
|
1320
|
+
enforceLiveCapacity: false
|
|
1321
|
+
});
|
|
1322
|
+
if (!all.ok)
|
|
1323
|
+
return all;
|
|
1324
|
+
const current = all.value.find((candidate) => candidate.bindingId === input.bindingId && candidate.generationId === input.generationId && candidate.taskId === input.taskId && candidate.ownerSessionId === input.activeSessionId);
|
|
1325
|
+
if (!current) {
|
|
1326
|
+
return {
|
|
1327
|
+
ok: false,
|
|
1328
|
+
code: "SCOPE_BINDING_STALE",
|
|
1329
|
+
message: "Refresh identity is not the exact current session/task generation."
|
|
963
1330
|
};
|
|
964
|
-
await atomicWrite(scopePath, JSON.stringify(refreshed, null, 2));
|
|
965
|
-
const verifyRaw = readBoundedFile(scopePath);
|
|
966
|
-
const verified = verifyRaw ? validateScopeBindingPayload(input.directory, JSON.parse(verifyRaw)) : null;
|
|
967
|
-
if (!verified || verified.revision !== refreshed.revision || verified.expiresAt !== refreshed.expiresAt)
|
|
968
|
-
return persistenceFailure("Refreshed lease verification failed.");
|
|
969
|
-
updateExactScopeBinding(verified);
|
|
970
|
-
return { ok: true, value: verified };
|
|
971
|
-
} catch (error) {
|
|
972
|
-
return persistenceFailure(`Lease refresh failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
|
|
973
|
-
} finally {
|
|
974
|
-
if (release)
|
|
975
|
-
try {
|
|
976
|
-
await release();
|
|
977
|
-
} catch {}
|
|
978
|
-
if (createdGenerationLockTarget && !fs.existsSync(scopePath)) {
|
|
979
|
-
try {
|
|
980
|
-
fs.unlinkSync(generationLockPath);
|
|
981
|
-
} catch {}
|
|
982
|
-
}
|
|
983
1331
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
const scopesDir = getScopesDir(directory);
|
|
991
|
-
try {
|
|
992
|
-
fs.lstatSync(scopesDir);
|
|
993
|
-
} catch (error) {
|
|
994
|
-
return error.code === "ENOENT" ? [] : null;
|
|
995
|
-
}
|
|
996
|
-
if (!isScopesDirSafe(directory, scopesDir))
|
|
997
|
-
return null;
|
|
998
|
-
const scanCapacity = options.maintenanceScan ? maintenanceFileScanCapacity : bindingFileScanCapacity;
|
|
999
|
-
if (!migrateLegacyBindingsSync(directory, scanCapacity))
|
|
1000
|
-
return null;
|
|
1001
|
-
let names;
|
|
1002
|
-
try {
|
|
1003
|
-
names = fs.readdirSync(scopesDir).sort();
|
|
1004
|
-
} catch {
|
|
1005
|
-
return [];
|
|
1006
|
-
}
|
|
1007
|
-
if (names.length > scanCapacity)
|
|
1008
|
-
return null;
|
|
1009
|
-
const bindings = [];
|
|
1010
|
-
for (const name of names) {
|
|
1011
|
-
if (!name.startsWith("binding-") || !name.endsWith(".json"))
|
|
1012
|
-
continue;
|
|
1013
|
-
const filePath = path2.join(scopesDir, name);
|
|
1014
|
-
const raw = readBoundedFile(filePath);
|
|
1015
|
-
if (!raw)
|
|
1016
|
-
continue;
|
|
1017
|
-
try {
|
|
1018
|
-
const binding = validateScopeBindingPayload(directory, JSON.parse(raw));
|
|
1019
|
-
if (binding && exactFilenameMatches(directory, filePath, binding)) {
|
|
1020
|
-
if (hasDurableRetirementIntent(filePath, binding)) {
|
|
1021
|
-
let createdAt = Date.now();
|
|
1022
|
-
try {
|
|
1023
|
-
const rawIntent = readBoundedFile(getRetirementIntentPath(filePath));
|
|
1024
|
-
const parsedIntent = rawIntent ? JSON.parse(rawIntent) : null;
|
|
1025
|
-
if (typeof parsedIntent?.createdAt === "number")
|
|
1026
|
-
createdAt = parsedIntent.createdAt;
|
|
1027
|
-
} catch {}
|
|
1028
|
-
bindings.push({
|
|
1029
|
-
...binding,
|
|
1030
|
-
lifecycleState: "revoked",
|
|
1031
|
-
updatedAt: createdAt,
|
|
1032
|
-
expiresAt: Math.min(binding.expiresAt, createdAt)
|
|
1033
|
-
});
|
|
1034
|
-
} else
|
|
1035
|
-
bindings.push(binding);
|
|
1036
|
-
}
|
|
1037
|
-
} catch {}
|
|
1332
|
+
if (current.lifecycleState !== "live" || current.expiresAt <= Date.now() || current.revision !== input.expectedRevision) {
|
|
1333
|
+
return {
|
|
1334
|
+
ok: false,
|
|
1335
|
+
code: current.expiresAt <= Date.now() ? "SCOPE_BINDING_EXPIRED" : "SCOPE_BINDING_STALE",
|
|
1336
|
+
message: "Refresh cannot renew an expired, tombstoned, or stale generation."
|
|
1337
|
+
};
|
|
1038
1338
|
}
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1339
|
+
const now = Date.now();
|
|
1340
|
+
const refreshed = {
|
|
1341
|
+
...current,
|
|
1342
|
+
revision: current.revision + 1,
|
|
1343
|
+
updatedAt: now,
|
|
1344
|
+
leaseStartedAt: now,
|
|
1345
|
+
expiresAt: now + Math.max(1, input.ttlMs ?? DEFAULT_SCOPE_BINDING_TTL_MS)
|
|
1346
|
+
};
|
|
1347
|
+
const persisted = transitionScopeBindingState(input.directory, refreshed, current.revision);
|
|
1348
|
+
if (!persisted.ok)
|
|
1349
|
+
return persisted;
|
|
1350
|
+
updateExactScopeBinding(persisted.value);
|
|
1351
|
+
writeScopeBindingShadow(input.directory, persisted.value);
|
|
1352
|
+
return persisted;
|
|
1043
1353
|
}
|
|
1044
1354
|
async function claimScopeBindingForChildDurably(input) {
|
|
1045
1355
|
if (!input.parentSessionId.trim() || !input.childSessionId.trim() || !input.dispatchCallId.trim() || input.parentSessionId === input.childSessionId)
|
|
1046
1356
|
return persistenceFailure("Claim requires distinct non-empty parent/child identities and an exact dispatch call.");
|
|
1047
|
-
const
|
|
1048
|
-
if (!
|
|
1049
|
-
return
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1357
|
+
const imported = ensureScopeBindingAuthorityImported(input.directory);
|
|
1358
|
+
if (!imported.ok)
|
|
1359
|
+
return imported;
|
|
1360
|
+
const initial = readAllAuthoritativeScopeBindings(input.directory, {
|
|
1361
|
+
enforceLiveCapacity: false
|
|
1362
|
+
});
|
|
1363
|
+
if (!initial.ok)
|
|
1364
|
+
return initial;
|
|
1365
|
+
const pending = initial.value.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);
|
|
1366
|
+
if (pending.length > 1) {
|
|
1056
1367
|
return {
|
|
1057
1368
|
ok: false,
|
|
1058
1369
|
code: "SCOPE_BINDING_AMBIGUOUS",
|
|
1059
1370
|
message: "Multiple pending generations match this exact Task dispatch."
|
|
1060
1371
|
};
|
|
1372
|
+
}
|
|
1061
1373
|
let predecessor = pending[0];
|
|
1062
1374
|
if (!predecessor) {
|
|
1063
|
-
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);
|
|
1064
|
-
if (successors.length > 1)
|
|
1375
|
+
const successors = initial.value.filter((binding) => binding.lifecycleState === "live" && binding.expiresAt > Date.now() && binding.activation === "active" && binding.parentOwnerSessionId === input.parentSessionId && binding.dispatchCallId === input.dispatchCallId && binding.predecessorGenerationId);
|
|
1376
|
+
if (successors.length > 1) {
|
|
1065
1377
|
return {
|
|
1066
1378
|
ok: false,
|
|
1067
1379
|
code: "SCOPE_BINDING_AMBIGUOUS",
|
|
1068
1380
|
message: "Multiple active successors match this Task dispatch."
|
|
1069
1381
|
};
|
|
1382
|
+
}
|
|
1070
1383
|
const existing = successors[0];
|
|
1071
1384
|
if (existing?.ownerSessionId === input.childSessionId) {
|
|
1072
1385
|
const admission = registerScopeBinding(existing);
|
|
@@ -1080,7 +1393,7 @@ async function claimScopeBindingForChildDurably(input) {
|
|
|
1080
1393
|
ok: false,
|
|
1081
1394
|
code: "SCOPE_BINDING_ALREADY_CLAIMED",
|
|
1082
1395
|
message: "This Task dispatch was already claimed by a different child session."
|
|
1083
|
-
} : initial.some((candidate) => candidate.ownerSessionId === input.parentSessionId && candidate.dispatchCallId === input.dispatchCallId) ? {
|
|
1396
|
+
} : initial.value.some((candidate) => candidate.ownerSessionId === input.parentSessionId && candidate.dispatchCallId === input.dispatchCallId) ? {
|
|
1084
1397
|
ok: false,
|
|
1085
1398
|
code: "SCOPE_BINDING_EXPIRED",
|
|
1086
1399
|
message: "No live pending generation remains for this Task dispatch."
|
|
@@ -1090,136 +1403,91 @@ async function claimScopeBindingForChildDurably(input) {
|
|
|
1090
1403
|
message: "No pending generation matches this Task dispatch."
|
|
1091
1404
|
};
|
|
1092
1405
|
}
|
|
1093
|
-
const claimPath = path2.join(getScopesDir(input.directory), `${claimBaseName(predecessor)}.json`);
|
|
1094
|
-
if (!await ensureLockTarget(claimPath))
|
|
1095
|
-
return persistenceFailure("Could not prepare the stable predecessor claim lock.");
|
|
1096
|
-
let release;
|
|
1097
1406
|
try {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
code: "SCOPE_BINDING_STORE_OVERLOADED",
|
|
1108
|
-
message: "The durable binding set became incomplete during claim."
|
|
1109
|
-
};
|
|
1110
|
-
const currentPredecessor = currentSet.find((binding) => binding.generationId === predecessor.generationId && binding.bindingId === predecessor.bindingId);
|
|
1111
|
-
const allSuccessors = currentSet.filter((binding) => binding.predecessorGenerationId === predecessor.generationId && binding.bindingId === predecessor.bindingId);
|
|
1112
|
-
const successors = allSuccessors.filter((binding) => binding.lifecycleState === "live" && binding.expiresAt > Date.now());
|
|
1113
|
-
if (successors.length > 1)
|
|
1114
|
-
return {
|
|
1115
|
-
ok: false,
|
|
1116
|
-
code: "SCOPE_BINDING_AMBIGUOUS",
|
|
1117
|
-
message: "Incompatible successors exist for one predecessor generation."
|
|
1118
|
-
};
|
|
1119
|
-
const receiptRaw = readBoundedFile(claimPath);
|
|
1120
|
-
let receipt = null;
|
|
1121
|
-
if (receiptRaw) {
|
|
1122
|
-
try {
|
|
1123
|
-
const parsed = JSON.parse(receiptRaw);
|
|
1124
|
-
if (parsed.version !== 1 || parsed.predecessorGenerationId !== predecessor.generationId || !isScopeBindingIdentity(parsed.winnerGenerationId) || typeof parsed.childSessionId !== "string" || parsed.dispatchCallId !== input.dispatchCallId)
|
|
1125
|
-
return persistenceFailure("Claim winner receipt is malformed or belongs to another predecessor.");
|
|
1126
|
-
receipt = parsed;
|
|
1127
|
-
} catch {
|
|
1128
|
-
return persistenceFailure("Claim winner receipt is malformed.");
|
|
1407
|
+
let claimed = null;
|
|
1408
|
+
let retired = null;
|
|
1409
|
+
let alreadyClaimed = null;
|
|
1410
|
+
withCoordinationTransaction(input.directory, () => {
|
|
1411
|
+
const currentSet = readAllAuthoritativeScopeBindings(input.directory, {
|
|
1412
|
+
enforceLiveCapacity: false
|
|
1413
|
+
});
|
|
1414
|
+
if (!currentSet.ok) {
|
|
1415
|
+
throw new ScopeCoordinationAbort(currentSet);
|
|
1129
1416
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1417
|
+
const currentPredecessor = currentSet.value.find((binding) => binding.generationId === predecessor.generationId && binding.bindingId === predecessor.bindingId);
|
|
1418
|
+
const liveSuccessors = currentSet.value.filter((binding) => binding.predecessorGenerationId === predecessor.generationId && binding.bindingId === predecessor.bindingId && binding.lifecycleState === "live" && binding.expiresAt > Date.now());
|
|
1419
|
+
if (liveSuccessors.length > 1) {
|
|
1420
|
+
throw new ScopeCoordinationAbort({
|
|
1421
|
+
ok: false,
|
|
1422
|
+
code: "SCOPE_BINDING_AMBIGUOUS",
|
|
1423
|
+
message: "Incompatible successors exist for one predecessor generation."
|
|
1424
|
+
});
|
|
1138
1425
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
return
|
|
1426
|
+
const existing = liveSuccessors[0];
|
|
1427
|
+
if (existing) {
|
|
1428
|
+
if (existing.ownerSessionId !== input.childSessionId) {
|
|
1429
|
+
throw new ScopeCoordinationAbort({
|
|
1430
|
+
ok: false,
|
|
1431
|
+
code: "SCOPE_BINDING_ALREADY_CLAIMED",
|
|
1432
|
+
message: "This pending generation has a winner in another child session."
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
alreadyClaimed = existing;
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
if (!currentPredecessor || currentPredecessor.lifecycleState !== "live" || currentPredecessor.expiresAt <= Date.now()) {
|
|
1439
|
+
throw new ScopeCoordinationAbort({
|
|
1150
1440
|
ok: false,
|
|
1151
|
-
code: "
|
|
1152
|
-
message: "
|
|
1153
|
-
};
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1441
|
+
code: "SCOPE_BINDING_EXPIRED",
|
|
1442
|
+
message: "The pending predecessor expired before claim."
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
predecessor = currentPredecessor;
|
|
1446
|
+
const nextClaimed = createClaimedScopeBinding(predecessor, input);
|
|
1447
|
+
const claimedWrite = _scopePersistenceInternals.transitionScopeBindingStateWithinTransaction(input.directory, nextClaimed, null);
|
|
1448
|
+
if (!claimedWrite.ok) {
|
|
1449
|
+
throw new ScopeCoordinationAbort(claimedWrite);
|
|
1450
|
+
}
|
|
1451
|
+
const superseded = {
|
|
1452
|
+
...predecessor,
|
|
1453
|
+
revision: predecessor.revision + 1,
|
|
1454
|
+
lifecycleState: "superseded",
|
|
1455
|
+
updatedAt: claimedWrite.value.updatedAt,
|
|
1456
|
+
expiresAt: Math.min(predecessor.expiresAt, claimedWrite.value.updatedAt)
|
|
1457
|
+
};
|
|
1458
|
+
const retiredWrite = _scopePersistenceInternals.transitionScopeBindingStateWithinTransaction(input.directory, superseded, predecessor.revision);
|
|
1459
|
+
if (!retiredWrite.ok) {
|
|
1460
|
+
throw new ScopeCoordinationAbort(retiredWrite);
|
|
1461
|
+
}
|
|
1462
|
+
claimed = claimedWrite.value;
|
|
1463
|
+
retired = retiredWrite.value;
|
|
1464
|
+
});
|
|
1465
|
+
if (alreadyClaimed) {
|
|
1466
|
+
const admission2 = registerScopeBinding(alreadyClaimed);
|
|
1467
|
+
return admission2.ok ? {
|
|
1468
|
+
ok: true,
|
|
1469
|
+
value: { previous: predecessor, claimed: alreadyClaimed }
|
|
1470
|
+
} : {
|
|
1159
1471
|
ok: false,
|
|
1160
1472
|
code: admission2.code,
|
|
1161
1473
|
message: admission2.message
|
|
1162
1474
|
};
|
|
1163
1475
|
}
|
|
1164
|
-
if (!
|
|
1165
|
-
return
|
|
1166
|
-
ok: false,
|
|
1167
|
-
code: "SCOPE_BINDING_EXPIRED",
|
|
1168
|
-
message: "The pending predecessor expired before claim."
|
|
1169
|
-
};
|
|
1170
|
-
predecessor = currentPredecessor;
|
|
1171
|
-
const claimed = createClaimedScopeBinding(predecessor, input);
|
|
1172
|
-
const persisted = await writeScopeBindingToDisk(input.directory, claimed);
|
|
1173
|
-
if (!persisted.ok)
|
|
1174
|
-
return persisted;
|
|
1175
|
-
const receiptWrite = await writeClaimReceipt(claimPath, predecessor, persisted.value, input);
|
|
1176
|
-
if (!receiptWrite.ok) {
|
|
1177
|
-
await tombstoneScopeBinding(input.directory, persisted.value, "revoked");
|
|
1178
|
-
return receiptWrite;
|
|
1476
|
+
if (!predecessor || !claimed || !retired) {
|
|
1477
|
+
return persistenceFailure("Claim transaction did not settle a winner.");
|
|
1179
1478
|
}
|
|
1180
|
-
|
|
1479
|
+
writeScopeBindingShadow(input.directory, claimed);
|
|
1480
|
+
writeScopeBindingShadow(input.directory, retired);
|
|
1481
|
+
const admission = registerScopeBinding(claimed);
|
|
1181
1482
|
if (!admission.ok) {
|
|
1182
|
-
|
|
1183
|
-
return {
|
|
1184
|
-
ok: false,
|
|
1185
|
-
code: admission.code,
|
|
1186
|
-
message: admission.message
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
const retired = await tombstoneScopeBinding(input.directory, predecessor, "superseded");
|
|
1190
|
-
if (!retired.ok) {
|
|
1191
|
-
await tombstoneScopeBinding(input.directory, persisted.value, "revoked");
|
|
1192
|
-
return persistenceFailure(`Successor was persisted but predecessor retirement failed: ${retired.message}`);
|
|
1483
|
+
return rollbackAdmissionFailure(input.directory, claimed, [{ original: predecessor, superseded: retired }], admission);
|
|
1193
1484
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
value: { previous: predecessor, claimed: persisted.value }
|
|
1197
|
-
};
|
|
1485
|
+
installScopeBindingTombstone(retired);
|
|
1486
|
+
return { ok: true, value: { previous: predecessor, claimed } };
|
|
1198
1487
|
} catch (error) {
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
try {
|
|
1203
|
-
await release();
|
|
1204
|
-
} catch {}
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
async function writeClaimReceipt(claimPath, previous, claimed, input) {
|
|
1208
|
-
const receipt = {
|
|
1209
|
-
version: 1,
|
|
1210
|
-
predecessorGenerationId: previous.generationId,
|
|
1211
|
-
winnerGenerationId: claimed.generationId,
|
|
1212
|
-
childSessionId: input.childSessionId,
|
|
1213
|
-
dispatchCallId: input.dispatchCallId,
|
|
1214
|
-
createdAt: Date.now()
|
|
1215
|
-
};
|
|
1216
|
-
try {
|
|
1217
|
-
await atomicWrite(claimPath, JSON.stringify(receipt, null, 2));
|
|
1218
|
-
const raw = readBoundedFile(claimPath);
|
|
1219
|
-
const verified = raw ? JSON.parse(raw) : null;
|
|
1220
|
-
return verified?.winnerGenerationId === receipt.winnerGenerationId && verified.childSessionId === receipt.childSessionId ? { ok: true, value: verified } : persistenceFailure("Claim winner receipt verification failed.");
|
|
1221
|
-
} catch (error) {
|
|
1222
|
-
return persistenceFailure(`Claim winner receipt write failed: ${error instanceof Error ? error.message : "unknown I/O error"}`);
|
|
1488
|
+
if (error instanceof ScopeCoordinationAbort)
|
|
1489
|
+
return error.failure;
|
|
1490
|
+
return persistenceFailure(`Claim transaction failed: ${error instanceof Error ? error.message : "unknown coordination error"}`);
|
|
1223
1491
|
}
|
|
1224
1492
|
}
|
|
1225
1493
|
async function replaceExistingScopeDeclaration(input) {
|
|
@@ -1231,58 +1499,69 @@ async function replaceExistingScopeDeclaration(input) {
|
|
|
1231
1499
|
const { binding } = input;
|
|
1232
1500
|
if (binding.activation !== "declaration" || binding.dispatchCallId !== undefined)
|
|
1233
1501
|
return persistenceFailure("declare_scope may persist only a declaration generation.");
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1502
|
+
const imported = ensureScopeBindingAuthorityImported(input.directory);
|
|
1503
|
+
if (!imported.ok)
|
|
1504
|
+
return imported;
|
|
1505
|
+
const all = readAllAuthoritativeScopeBindings(input.directory, {
|
|
1506
|
+
enforceLiveCapacity: false
|
|
1507
|
+
});
|
|
1508
|
+
if (!all.ok)
|
|
1509
|
+
return all;
|
|
1510
|
+
const owned = all.value.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);
|
|
1511
|
+
if (owned.length > 0 && !input.replaceExisting) {
|
|
1512
|
+
return {
|
|
1513
|
+
ok: false,
|
|
1514
|
+
code: "SCOPE_BINDING_AMBIGUOUS",
|
|
1515
|
+
message: "A live owned generation already exists; retry declare_scope with replace_existing=true to revoke it atomically."
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1239
1518
|
try {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
return persisted;
|
|
1262
|
-
for (const prior of owned) {
|
|
1263
|
-
const retired = await tombstoneScopeBinding(input.directory, prior, "superseded");
|
|
1264
|
-
if (!retired.ok) {
|
|
1265
|
-
await tombstoneScopeBinding(input.directory, persisted.value, "revoked");
|
|
1266
|
-
return persistenceFailure(`Replacement could not retire generation ${prior.generationId}: ${retired.message}`);
|
|
1519
|
+
let persisted = null;
|
|
1520
|
+
const retired = [];
|
|
1521
|
+
withCoordinationTransaction(input.directory, () => {
|
|
1522
|
+
const create = _scopePersistenceInternals.transitionScopeBindingStateWithinTransaction(input.directory, binding, null);
|
|
1523
|
+
if (!create.ok) {
|
|
1524
|
+
throw new ScopeCoordinationAbort(create);
|
|
1525
|
+
}
|
|
1526
|
+
persisted = create.value;
|
|
1527
|
+
for (const prior of owned) {
|
|
1528
|
+
const superseded = {
|
|
1529
|
+
...prior,
|
|
1530
|
+
revision: prior.revision + 1,
|
|
1531
|
+
lifecycleState: "superseded",
|
|
1532
|
+
updatedAt: create.value.updatedAt,
|
|
1533
|
+
expiresAt: Math.min(prior.expiresAt, create.value.updatedAt)
|
|
1534
|
+
};
|
|
1535
|
+
const result = _scopePersistenceInternals.transitionScopeBindingStateWithinTransaction(input.directory, superseded, prior.revision);
|
|
1536
|
+
if (!result.ok) {
|
|
1537
|
+
throw new ScopeCoordinationAbort(result);
|
|
1538
|
+
}
|
|
1539
|
+
retired.push(result.value);
|
|
1267
1540
|
}
|
|
1541
|
+
});
|
|
1542
|
+
if (!persisted) {
|
|
1543
|
+
return persistenceFailure("Declaration transaction did not persist a generation.");
|
|
1544
|
+
}
|
|
1545
|
+
writeScopeBindingShadow(input.directory, persisted);
|
|
1546
|
+
for (const prior of retired) {
|
|
1547
|
+
writeScopeBindingShadow(input.directory, prior);
|
|
1548
|
+
installScopeBindingTombstone(prior);
|
|
1268
1549
|
}
|
|
1269
|
-
const admission = registerScopeBinding(persisted
|
|
1550
|
+
const admission = registerScopeBinding(persisted);
|
|
1270
1551
|
if (!admission.ok) {
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1552
|
+
if (retired.length !== owned.length) {
|
|
1553
|
+
return persistenceFailure("Memory admission failed after an incomplete predecessor transition.");
|
|
1554
|
+
}
|
|
1555
|
+
return rollbackAdmissionFailure(input.directory, persisted, owned.map((original, index) => ({
|
|
1556
|
+
original,
|
|
1557
|
+
superseded: retired[index]
|
|
1558
|
+
})), admission);
|
|
1277
1559
|
}
|
|
1278
|
-
return { ok: true, value: persisted
|
|
1560
|
+
return { ok: true, value: persisted };
|
|
1279
1561
|
} catch (error) {
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
try {
|
|
1284
|
-
await release();
|
|
1285
|
-
} catch {}
|
|
1562
|
+
if (error instanceof ScopeCoordinationAbort)
|
|
1563
|
+
return error.failure;
|
|
1564
|
+
return persistenceFailure(`Declaration transaction failed: ${error instanceof Error ? error.message : "unknown coordination error"}`);
|
|
1286
1565
|
}
|
|
1287
1566
|
}
|
|
1288
1567
|
function scheduleScopeBindingMaintenance(directory) {
|
|
@@ -1322,32 +1601,13 @@ async function pruneScopeBindingTombstones(directory) {
|
|
|
1322
1601
|
retries: { retries: 3, minTimeout: 50, maxTimeout: 200 },
|
|
1323
1602
|
realpath: false
|
|
1324
1603
|
});
|
|
1325
|
-
|
|
1604
|
+
const readAll = readAllAuthoritativeScopeBindings(directory, {
|
|
1326
1605
|
enforceLiveCapacity: false,
|
|
1327
1606
|
maintenanceScan: true
|
|
1328
1607
|
});
|
|
1329
|
-
if (!
|
|
1330
|
-
return
|
|
1331
|
-
|
|
1332
|
-
code: "SCOPE_BINDING_STORE_OVERLOADED",
|
|
1333
|
-
message: "The durable binding set cannot be completely evaluated."
|
|
1334
|
-
};
|
|
1335
|
-
for (const binding of all) {
|
|
1336
|
-
const filePath = getBindingFilePath(directory, binding);
|
|
1337
|
-
if (!hasDurableRetirementIntent(filePath, binding))
|
|
1338
|
-
continue;
|
|
1339
|
-
const finalized = await tombstoneScopeBinding(directory, binding, "revoked");
|
|
1340
|
-
if (finalized.ok)
|
|
1341
|
-
try {
|
|
1342
|
-
fs.unlinkSync(getRetirementIntentPath(filePath));
|
|
1343
|
-
} catch {}
|
|
1344
|
-
}
|
|
1345
|
-
all = readAllExactBindings(directory, {
|
|
1346
|
-
enforceLiveCapacity: false,
|
|
1347
|
-
maintenanceScan: true
|
|
1348
|
-
});
|
|
1349
|
-
if (!all)
|
|
1350
|
-
return persistenceFailure("The durable binding set changed during maintenance reconciliation.");
|
|
1608
|
+
if (!readAll.ok)
|
|
1609
|
+
return readAll;
|
|
1610
|
+
const all = readAll.value;
|
|
1351
1611
|
const tombstones = all.filter((binding) => binding.lifecycleState !== "live").sort((left, right) => left.updatedAt - right.updatedAt);
|
|
1352
1612
|
const now = Date.now();
|
|
1353
1613
|
const removable = tombstones.filter((binding, index) => now - binding.updatedAt > MAX_TOMBSTONE_AGE_MS || index < tombstones.length - MAX_DURABLE_TOMBSTONES);
|
|
@@ -1371,6 +1631,7 @@ async function pruneScopeBindingTombstones(directory) {
|
|
|
1371
1631
|
const latest = latestRaw ? validateScopeBindingPayload(directory, JSON.parse(latestRaw)) : null;
|
|
1372
1632
|
if (!latest || latest.generationId !== binding.generationId || latest.revision !== binding.revision || latest.lifecycleState === "live" && !hasDurableRetirementIntent(filePath, latest))
|
|
1373
1633
|
continue;
|
|
1634
|
+
deleteCoordinationState(directory, SCOPE_BINDING_COORDINATION_NAMESPACE, binding.generationId, binding.revision);
|
|
1374
1635
|
fs.unlinkSync(filePath);
|
|
1375
1636
|
removedExact = true;
|
|
1376
1637
|
removed++;
|
|
@@ -1475,47 +1736,33 @@ function attemptIdleScopeBindingRevival(directory, resolution) {
|
|
|
1475
1736
|
return null;
|
|
1476
1737
|
if (hasDeliberateScopeBindingDenyOverlay(candidate))
|
|
1477
1738
|
return null;
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
release = lockSyncWithBoundedRetry(lockPath);
|
|
1489
|
-
const raw = readBoundedFile(scopePath);
|
|
1490
|
-
if (!raw)
|
|
1491
|
-
return null;
|
|
1492
|
-
const current = validateScopeBindingPayload(resolvedWorkspace, JSON.parse(raw));
|
|
1493
|
-
if (!current || current.bindingId !== candidate.bindingId || current.generationId !== candidate.generationId || current.revision !== candidate.revision || current.lifecycleState !== "live" || current.expiresAt > Date.now() || Date.now() - current.expiresAt > SCOPE_BINDING_AUTO_REVIVE_WINDOW_MS)
|
|
1494
|
-
return null;
|
|
1495
|
-
const revivedAt = Date.now();
|
|
1496
|
-
const revived = {
|
|
1497
|
-
...current,
|
|
1498
|
-
revision: current.revision + 1,
|
|
1499
|
-
updatedAt: revivedAt,
|
|
1500
|
-
leaseStartedAt: revivedAt,
|
|
1501
|
-
expiresAt: revivedAt + DEFAULT_SCOPE_BINDING_TTL_MS
|
|
1502
|
-
};
|
|
1503
|
-
atomicWriteSync(scopePath, JSON.stringify(revived, null, 2));
|
|
1504
|
-
const verifiedRaw = readBoundedFile(scopePath);
|
|
1505
|
-
const verified = verifiedRaw ? validateScopeBindingPayload(resolvedWorkspace, JSON.parse(verifiedRaw)) : null;
|
|
1506
|
-
if (!verified || verified.revision !== revived.revision || verified.lifecycleState !== "live" || verified.expiresAt !== revived.expiresAt)
|
|
1507
|
-
return null;
|
|
1508
|
-
clearSweepTombstoneForRevival(revived);
|
|
1509
|
-
appendScopeBindingRevivalEvent(resolvedWorkspace, revived);
|
|
1510
|
-
return revived;
|
|
1511
|
-
} catch {
|
|
1739
|
+
const imported = ensureScopeBindingAuthorityImported(directory);
|
|
1740
|
+
if (!imported.ok)
|
|
1741
|
+
return null;
|
|
1742
|
+
const all = readAllAuthoritativeScopeBindings(directory, {
|
|
1743
|
+
enforceLiveCapacity: false
|
|
1744
|
+
});
|
|
1745
|
+
if (!all.ok)
|
|
1746
|
+
return null;
|
|
1747
|
+
const current = all.value.find((binding) => binding.bindingId === candidate.bindingId && binding.generationId === candidate.generationId);
|
|
1748
|
+
if (!current || current.revision !== candidate.revision || current.lifecycleState !== "live" || current.expiresAt > Date.now() || Date.now() - current.expiresAt > SCOPE_BINDING_AUTO_REVIVE_WINDOW_MS) {
|
|
1512
1749
|
return null;
|
|
1513
|
-
} finally {
|
|
1514
|
-
if (release)
|
|
1515
|
-
try {
|
|
1516
|
-
release();
|
|
1517
|
-
} catch {}
|
|
1518
1750
|
}
|
|
1751
|
+
const revivedAt = Date.now();
|
|
1752
|
+
const revived = {
|
|
1753
|
+
...current,
|
|
1754
|
+
revision: current.revision + 1,
|
|
1755
|
+
updatedAt: revivedAt,
|
|
1756
|
+
leaseStartedAt: revivedAt,
|
|
1757
|
+
expiresAt: revivedAt + DEFAULT_SCOPE_BINDING_TTL_MS
|
|
1758
|
+
};
|
|
1759
|
+
const persisted = transitionScopeBindingState(directory, revived, current.revision);
|
|
1760
|
+
if (!persisted.ok)
|
|
1761
|
+
return null;
|
|
1762
|
+
writeScopeBindingShadow(directory, persisted.value);
|
|
1763
|
+
clearSweepTombstoneForRevival(persisted.value);
|
|
1764
|
+
appendScopeBindingRevivalEvent(directory, persisted.value);
|
|
1765
|
+
return persisted.value;
|
|
1519
1766
|
}
|
|
1520
1767
|
function resolveAuthorizedScopeBindingForPlan(input) {
|
|
1521
1768
|
const resolution = resolveAuthorizedScopeBindingForPlanDetailed(input);
|
|
@@ -1603,87 +1850,16 @@ function resolveAuthorizedScopeBindingForSessionDetailed(input) {
|
|
|
1603
1850
|
totalCandidates: expiredTotal
|
|
1604
1851
|
} : { status: "not_declared" };
|
|
1605
1852
|
}
|
|
1606
|
-
function readPrFeedbackBindingFile(directory, filePath, activeSessionId, taskId) {
|
|
1607
|
-
try {
|
|
1608
|
-
const leaf = fs.lstatSync(filePath);
|
|
1609
|
-
if (!leaf.isFile() || leaf.isSymbolicLink())
|
|
1610
|
-
return null;
|
|
1611
|
-
if (normalizePathForComparison(fs.realpathSync(filePath)) !== normalizePathForComparison(filePath))
|
|
1612
|
-
return null;
|
|
1613
|
-
} catch {
|
|
1614
|
-
return null;
|
|
1615
|
-
}
|
|
1616
|
-
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
1617
|
-
let fd;
|
|
1618
|
-
try {
|
|
1619
|
-
fd = fs.openSync(filePath, fs.constants.O_RDONLY | nofollow);
|
|
1620
|
-
} catch {
|
|
1621
|
-
return null;
|
|
1622
|
-
}
|
|
1623
|
-
let raw = "";
|
|
1624
|
-
try {
|
|
1625
|
-
const stat = fs.fstatSync(fd);
|
|
1626
|
-
if (!stat.isFile() || stat.size > MAX_SCOPE_BYTES)
|
|
1627
|
-
return null;
|
|
1628
|
-
const buffer = Buffer.alloc(stat.size);
|
|
1629
|
-
fs.readSync(fd, buffer, 0, stat.size, 0);
|
|
1630
|
-
raw = buffer.toString("utf8");
|
|
1631
|
-
} catch {
|
|
1632
|
-
return null;
|
|
1633
|
-
} finally {
|
|
1634
|
-
try {
|
|
1635
|
-
fs.closeSync(fd);
|
|
1636
|
-
} catch {}
|
|
1637
|
-
}
|
|
1638
|
-
let parsed;
|
|
1639
|
-
try {
|
|
1640
|
-
parsed = validateScopeBindingPayload(directory, JSON.parse(raw));
|
|
1641
|
-
} catch {
|
|
1642
|
-
return null;
|
|
1643
|
-
}
|
|
1644
|
-
if (!parsed)
|
|
1645
|
-
return null;
|
|
1646
|
-
if (hasScopeBindingDenyOverlay(parsed))
|
|
1647
|
-
return null;
|
|
1648
|
-
const now = Date.now();
|
|
1649
|
-
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")
|
|
1650
|
-
return null;
|
|
1651
|
-
const expectedPath = getBindingFilePath(directory, parsed);
|
|
1652
|
-
if (normalizePathForComparison(expectedPath) !== normalizePathForComparison(filePath))
|
|
1653
|
-
return null;
|
|
1654
|
-
return parsed;
|
|
1655
|
-
}
|
|
1656
1853
|
function resolveAuthorizedPrFeedbackScopeBindingFromDisk(input) {
|
|
1657
1854
|
if (!input.activeSessionId.trim())
|
|
1658
1855
|
return null;
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
if (!
|
|
1856
|
+
const authoritative = readAllAuthoritativeScopeBindings(input.directory, {
|
|
1857
|
+
enforceLiveCapacity: false
|
|
1858
|
+
});
|
|
1859
|
+
if (!authoritative.ok)
|
|
1663
1860
|
return null;
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
if (!isSafeTaskId(input.taskId))
|
|
1667
|
-
return null;
|
|
1668
|
-
try {
|
|
1669
|
-
const names = fs.readdirSync(scopesDir);
|
|
1670
|
-
if (names.length > MAX_BINDING_FILES_TO_SCAN)
|
|
1671
|
-
return null;
|
|
1672
|
-
candidates = names.filter((name) => name.startsWith(exactBindingPrefix(input.taskId)) && name.endsWith(".json")).map((name) => path2.join(scopesDir, name));
|
|
1673
|
-
} catch {
|
|
1674
|
-
return null;
|
|
1675
|
-
}
|
|
1676
|
-
} else {
|
|
1677
|
-
try {
|
|
1678
|
-
const names = fs.readdirSync(scopesDir);
|
|
1679
|
-
if (names.length > MAX_BINDING_FILES_TO_SCAN)
|
|
1680
|
-
return null;
|
|
1681
|
-
candidates = names.filter((name) => name.startsWith("binding-") && name.endsWith(".json")).map((name) => path2.join(scopesDir, name));
|
|
1682
|
-
} catch {
|
|
1683
|
-
return null;
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
const matches = candidates.map((candidate) => readPrFeedbackBindingFile(input.directory, candidate, input.activeSessionId, input.taskId)).filter((binding) => binding !== null);
|
|
1861
|
+
const now = Date.now();
|
|
1862
|
+
const matches = authoritative.value.filter((parsed) => parsed.source === "pr_feedback" && (!input.taskId || parsed.taskId === input.taskId) && parsed.ownerSessionId === input.activeSessionId && parsed.activation === "active" && typeof parsed.dispatchCallId === "string" && parsed.dispatchCallId.length > 0 && parsed.ownerMessageId === parsed.dispatchCallId && typeof parsed.parentOwnerSessionId === "string" && parsed.parentOwnerSessionId.length > 0 && parsed.ownerSessionId !== parsed.parentOwnerSessionId && parsed.parentCallId === parsed.dispatchCallId && parsed.workflowSessionId === parsed.parentOwnerSessionId && typeof parsed.workflowRevisionDigest === "string" && parsed.workflowRevisionDigest.length > 0 && parsed.planId === `pr-feedback:${parsed.workflowSessionId}` && parsed.planStructureHash === parsed.workflowRevisionDigest && parsed.declaredAt <= now && parsed.expiresAt > now && parsed.lifecycleState === "live" && !hasScopeBindingDenyOverlay(parsed));
|
|
1687
1863
|
if (matches.length !== 1)
|
|
1688
1864
|
return null;
|
|
1689
1865
|
const admission = registerScopeBinding(matches[0]);
|
|
@@ -1696,6 +1872,8 @@ function atomicWriteSync(targetPath, content) {
|
|
|
1696
1872
|
atomicWriteSwarmFileSync(targetPath, content);
|
|
1697
1873
|
}
|
|
1698
1874
|
var _scopePersistenceInternals = {
|
|
1875
|
+
transitionScopeBindingState,
|
|
1876
|
+
transitionScopeBindingStateWithinTransaction,
|
|
1699
1877
|
get liveBindingCapacity() {
|
|
1700
1878
|
return liveBindingCapacity;
|
|
1701
1879
|
},
|