@gajae-code/coding-agent 0.12.6 → 0.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +8 -10
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
- package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -3
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +31 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -23
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/artifacts.d.ts +5 -8
- package/dist/types/session/client-bridge.d.ts +0 -14
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/messages.d.ts +1 -4
- package/dist/types/session/session-manager.d.ts +7 -6
- package/dist/types/session/session-storage.d.ts +2 -0
- package/dist/types/session/streaming-output.d.ts +0 -32
- package/dist/types/task/receipt.d.ts +1 -2
- package/dist/types/task/types.d.ts +43 -178
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +0 -3
- package/dist/types/tools/output-meta.d.ts +5 -37
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/dist/types/tools/tool-result.d.ts +0 -1
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/src/async/job-manager.ts +430 -46
- package/src/cli/notify-cli.ts +204 -94
- package/src/commands/notify.ts +19 -8
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-registry.ts +862 -75
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/cursor.ts +13 -20
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +151 -425
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
- package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
- package/src/gjc-runtime/workflow-manifest.ts +0 -12
- package/src/internal-urls/agent-protocol.ts +38 -98
- package/src/internal-urls/artifact-protocol.ts +17 -28
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/registry-helpers.ts +0 -62
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/acp/acp-agent.ts +0 -24
- package/src/modes/acp/acp-client-bridge.ts +11 -154
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +464 -50
- package/src/prompts/tools/bash.md +2 -2
- package/src/sdk/acp/adapter.ts +9 -200
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -8
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +105 -359
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +431 -130
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/host.ts +9 -187
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/host/reverse-leases.ts +42 -249
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +10 -7
- package/src/session/agent-session.ts +59 -202
- package/src/session/artifacts.ts +74 -211
- package/src/session/client-bridge.ts +0 -15
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +362 -124
- package/src/session/internal/native-publish-outcome.ts +3 -1
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/messages.ts +7 -32
- package/src/session/session-manager.ts +48 -15
- package/src/session/session-storage.ts +72 -5
- package/src/session/streaming-output.ts +40 -198
- package/src/slash-commands/builtin-registry.ts +82 -4
- package/src/task/index.ts +237 -65
- package/src/task/receipt.ts +2 -4
- package/src/task/render.ts +0 -30
- package/src/task/types.ts +10 -34
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +239 -818
- package/src/tools/eval.ts +4 -99
- package/src/tools/fetch.ts +15 -77
- package/src/tools/find.ts +3 -7
- package/src/tools/output-meta.ts +114 -696
- package/src/tools/search.ts +4 -9
- package/src/tools/subagent.ts +97 -75
- package/src/tools/telegram-send.ts +4 -2
- package/src/tools/todo-write.ts +47 -0
- package/src/tools/tool-result.ts +0 -1
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
- package/dist/types/utils/artifact-id.d.ts +0 -2
- package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
- package/src/utils/artifact-id.ts +0 -6
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
applyOwnerOnlyFdSecurity,
|
|
7
7
|
applyOwnerOnlyPathSecurity,
|
|
8
8
|
exactRemoveDirectoryTree,
|
|
9
|
+
exactReplacePath,
|
|
10
|
+
exactRestore,
|
|
9
11
|
exactUnlink,
|
|
10
12
|
type NativeDirectoryTreeSnapshot,
|
|
11
13
|
type NativeOwnerOnlySecurityResult,
|
|
@@ -34,7 +36,6 @@ const LOCK_LEASE_MS = 60_000;
|
|
|
34
36
|
const LOCK_HEARTBEAT_MS = 10_000;
|
|
35
37
|
const LOCK_WAIT_MS = 5_000;
|
|
36
38
|
|
|
37
|
-
const LOCK_STALE_RECHECK_MS = 100;
|
|
38
39
|
export class ManagedPublishError extends Error {
|
|
39
40
|
readonly classification:
|
|
40
41
|
| "destination_conflict"
|
|
@@ -138,7 +139,38 @@ export interface ManagedStorageLock {
|
|
|
138
139
|
|
|
139
140
|
export interface ManagedFileSnapshot {
|
|
140
141
|
bytes: Buffer;
|
|
141
|
-
identity: {
|
|
142
|
+
identity: {
|
|
143
|
+
dev: bigint;
|
|
144
|
+
ino: bigint;
|
|
145
|
+
nlink: bigint;
|
|
146
|
+
size: number;
|
|
147
|
+
mtimeNs: bigint;
|
|
148
|
+
ctimeNs: bigint;
|
|
149
|
+
sha256: string;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type ReplacementCleanupReceipt = {
|
|
154
|
+
version: 1;
|
|
155
|
+
predecessor: string;
|
|
156
|
+
successor: string;
|
|
157
|
+
identity: {
|
|
158
|
+
dev: string;
|
|
159
|
+
ino: string;
|
|
160
|
+
nlink: string;
|
|
161
|
+
size: string;
|
|
162
|
+
mtimeNs: string;
|
|
163
|
+
ctimeNs: string;
|
|
164
|
+
sha256: string;
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const U64_MAX = 18_446_744_073_709_551_615n;
|
|
169
|
+
|
|
170
|
+
function parseCanonicalU64(value: unknown): bigint | undefined {
|
|
171
|
+
if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value)) return undefined;
|
|
172
|
+
const parsed = BigInt(value);
|
|
173
|
+
return parsed <= U64_MAX ? parsed : undefined;
|
|
142
174
|
}
|
|
143
175
|
|
|
144
176
|
const ACL_FAILURE_CODES = new Set(["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown"]);
|
|
@@ -264,8 +296,19 @@ type LockRecord = {
|
|
|
264
296
|
createdAt: number;
|
|
265
297
|
heartbeatAt: number;
|
|
266
298
|
leaseExpiresAt: number;
|
|
299
|
+
released?: boolean;
|
|
267
300
|
};
|
|
268
301
|
|
|
302
|
+
export interface ManagedLockRetirementTestEvent {
|
|
303
|
+
readonly path: string;
|
|
304
|
+
readonly attemptId: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Test-only seam immediately before exact retirement of one observed lock identity. */
|
|
308
|
+
export const ManagedLockTestHooks: {
|
|
309
|
+
beforeObservedRetirement?: (event: ManagedLockRetirementTestEvent) => void;
|
|
310
|
+
} = {};
|
|
311
|
+
|
|
269
312
|
/** Captured configured-root authority for managed paths only. */
|
|
270
313
|
export interface ManagedDirectoryRoot {
|
|
271
314
|
readonly canonicalPath: string;
|
|
@@ -475,11 +518,11 @@ export class ManagedSessionDescendantStore {
|
|
|
475
518
|
readonly #authority: RecoveryFsRoot | undefined;
|
|
476
519
|
#ownsAuthority = false;
|
|
477
520
|
#closed = false;
|
|
521
|
+
#reconcilingReplacementCleanup = false;
|
|
478
522
|
readonly #authorityBaseDir: string;
|
|
479
523
|
/** Logical profile root inherited by nested managed session destinations. */
|
|
480
524
|
readonly #profileAgentDir: string;
|
|
481
525
|
readonly #subtreeRoot: ManagedDirectoryRoot;
|
|
482
|
-
#portableMutationFence?: ManagedStorageLock;
|
|
483
526
|
|
|
484
527
|
constructor(
|
|
485
528
|
root: ManagedDirectoryRoot,
|
|
@@ -548,40 +591,6 @@ export class ManagedSessionDescendantStore {
|
|
|
548
591
|
return this.#policy;
|
|
549
592
|
}
|
|
550
593
|
|
|
551
|
-
get hasRetainedAuthority(): boolean {
|
|
552
|
-
return this.#authority !== undefined;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async acquireMutationFence(name: string): Promise<ManagedStorageLock | undefined> {
|
|
556
|
-
if (this.#authority) return undefined;
|
|
557
|
-
if (!/^[a-zA-Z0-9_.-]+$/.test(name)) throw new Error("invalid_mutation_fence");
|
|
558
|
-
this.#assertBound();
|
|
559
|
-
if (this.#portableMutationFence) throw new Error("migration_busy");
|
|
560
|
-
const lock = await acquireManagedLock(this.#baseDir, `.gjc-${name}`, this.#subtreeRoot, this.#policy);
|
|
561
|
-
try {
|
|
562
|
-
this.#assertBound();
|
|
563
|
-
lock.assertOwned();
|
|
564
|
-
} catch (error) {
|
|
565
|
-
await lock.release();
|
|
566
|
-
throw error;
|
|
567
|
-
}
|
|
568
|
-
this.#portableMutationFence = lock;
|
|
569
|
-
return {
|
|
570
|
-
path: lock.path,
|
|
571
|
-
attemptId: lock.attemptId,
|
|
572
|
-
assertOwned: () => {
|
|
573
|
-
this.#assertBound();
|
|
574
|
-
lock.assertOwned();
|
|
575
|
-
},
|
|
576
|
-
release: async () => {
|
|
577
|
-
try {
|
|
578
|
-
await lock.release();
|
|
579
|
-
} finally {
|
|
580
|
-
if (this.#portableMutationFence === lock) this.#portableMutationFence = undefined;
|
|
581
|
-
}
|
|
582
|
-
},
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
594
|
get profileAgentDir(): string {
|
|
586
595
|
return this.#profileAgentDir;
|
|
587
596
|
}
|
|
@@ -659,18 +668,7 @@ export class ManagedSessionDescendantStore {
|
|
|
659
668
|
this.#assertBound();
|
|
660
669
|
}
|
|
661
670
|
#assertBound(): void {
|
|
662
|
-
if (!this.#authority)
|
|
663
|
-
const named = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
664
|
-
if (
|
|
665
|
-
!named.isDirectory() ||
|
|
666
|
-
named.isSymbolicLink() ||
|
|
667
|
-
named.dev !== this.#subtreeRoot.dev ||
|
|
668
|
-
named.ino !== this.#subtreeRoot.ino
|
|
669
|
-
) {
|
|
670
|
-
throw new Error("Managed descendant root binding changed");
|
|
671
|
-
}
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
671
|
+
if (!this.#authority) return;
|
|
674
672
|
const named = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
675
673
|
const retained =
|
|
676
674
|
this.#authorityBaseDir === this.#baseDir
|
|
@@ -696,7 +694,86 @@ export class ManagedSessionDescendantStore {
|
|
|
696
694
|
}
|
|
697
695
|
}
|
|
698
696
|
|
|
697
|
+
#reconcileReplacementCleanupReceipts(): void {
|
|
698
|
+
if (this.#reconcilingReplacementCleanup) return;
|
|
699
|
+
this.#reconcilingReplacementCleanup = true;
|
|
700
|
+
try {
|
|
701
|
+
for (const name of fs.readdirSync(this.#baseDir)) {
|
|
702
|
+
const match = /^\.gjc-replace-cleanup-([0-9a-f]+)-([0-9a-f]+)\.json$/.exec(name);
|
|
703
|
+
if (!match) continue;
|
|
704
|
+
const receiptPath = path.join(this.#baseDir, name);
|
|
705
|
+
const parsed = JSON.parse(
|
|
706
|
+
captureManagedFileNoFollow(receiptPath).bytes.toString("utf8"),
|
|
707
|
+
) as Partial<ReplacementCleanupReceipt>;
|
|
708
|
+
const identity = parsed.identity;
|
|
709
|
+
const dev = parseCanonicalU64(identity?.dev);
|
|
710
|
+
const ino = parseCanonicalU64(identity?.ino);
|
|
711
|
+
const nlink = parseCanonicalU64(identity?.nlink);
|
|
712
|
+
const size = parseCanonicalU64(identity?.size);
|
|
713
|
+
const mtimeNs = parseCanonicalU64(identity?.mtimeNs);
|
|
714
|
+
const ctimeNs = parseCanonicalU64(identity?.ctimeNs);
|
|
715
|
+
const expectedPredecessor =
|
|
716
|
+
dev !== undefined && ino !== undefined
|
|
717
|
+
? path.join(this.#baseDir, `.gjc-exact-replace-destination-${dev.toString(16)}-${ino.toString(16)}`)
|
|
718
|
+
: undefined;
|
|
719
|
+
if (
|
|
720
|
+
parsed.version !== 1 ||
|
|
721
|
+
dev === undefined ||
|
|
722
|
+
ino === undefined ||
|
|
723
|
+
nlink === undefined ||
|
|
724
|
+
size === undefined ||
|
|
725
|
+
mtimeNs === undefined ||
|
|
726
|
+
ctimeNs === undefined ||
|
|
727
|
+
typeof identity?.sha256 !== "string" ||
|
|
728
|
+
!match[1] ||
|
|
729
|
+
!match[2] ||
|
|
730
|
+
BigInt(`0x${match[1]}`) !== dev ||
|
|
731
|
+
BigInt(`0x${match[2]}`) !== ino ||
|
|
732
|
+
typeof parsed.predecessor !== "string" ||
|
|
733
|
+
path.resolve(parsed.predecessor) !== expectedPredecessor ||
|
|
734
|
+
typeof parsed.successor !== "string" ||
|
|
735
|
+
path.dirname(path.resolve(parsed.successor)) !== this.#baseDir
|
|
736
|
+
)
|
|
737
|
+
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
738
|
+
const retired = exactUnlink(expectedPredecessor, {
|
|
739
|
+
dev,
|
|
740
|
+
ino,
|
|
741
|
+
nlink,
|
|
742
|
+
parentDev: this.#subtreeRoot.dev,
|
|
743
|
+
parentIno: this.#subtreeRoot.ino,
|
|
744
|
+
size,
|
|
745
|
+
mtimeNs,
|
|
746
|
+
sha256: identity.sha256,
|
|
747
|
+
quarantineName: `.gjc-replace-retry-${dev.toString(16)}-${ino.toString(16)}`,
|
|
748
|
+
});
|
|
749
|
+
if (!retired.ok) throw new Error(`managed_replace_cleanup_pending:${retired.code ?? "unknown"}`);
|
|
750
|
+
const receipt = captureManagedFileNoFollow(receiptPath);
|
|
751
|
+
const removed = exactUnlink(receiptPath, {
|
|
752
|
+
dev: receipt.identity.dev,
|
|
753
|
+
ino: receipt.identity.ino,
|
|
754
|
+
nlink: receipt.identity.nlink,
|
|
755
|
+
parentDev: this.#subtreeRoot.dev,
|
|
756
|
+
parentIno: this.#subtreeRoot.ino,
|
|
757
|
+
size: BigInt(receipt.identity.size),
|
|
758
|
+
mtimeNs: receipt.identity.mtimeNs,
|
|
759
|
+
sha256: receipt.identity.sha256,
|
|
760
|
+
quarantineName: `.gjc-receipt-remove-${receipt.identity.dev.toString(16)}-${receipt.identity.ino.toString(16)}`,
|
|
761
|
+
});
|
|
762
|
+
if (!removed.ok) throw new Error(`managed_replace_receipt_cleanup_pending:${removed.code ?? "unknown"}`);
|
|
763
|
+
}
|
|
764
|
+
} finally {
|
|
765
|
+
this.#reconcilingReplacementCleanup = false;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
#beforeMutation(): void {
|
|
770
|
+
this.#assertBound();
|
|
771
|
+
this.#reconcileReplacementCleanupReceipts();
|
|
772
|
+
this.#assertBound();
|
|
773
|
+
}
|
|
774
|
+
|
|
699
775
|
ensureDirectory(relativePath = ""): ManagedDirectoryRoot {
|
|
776
|
+
this.#beforeMutation();
|
|
700
777
|
this.#assertBound();
|
|
701
778
|
if (this.#authority) {
|
|
702
779
|
const relative = this.#relative(this.#resolve(relativePath));
|
|
@@ -716,50 +793,42 @@ export class ManagedSessionDescendantStore {
|
|
|
716
793
|
}
|
|
717
794
|
|
|
718
795
|
async publishNoReplace(relativePath: string, bytes: Uint8Array): Promise<void> {
|
|
719
|
-
this.#
|
|
720
|
-
this.#portableMutationFence?.assertOwned();
|
|
796
|
+
this.#beforeMutation();
|
|
721
797
|
const resolved = this.#resolve(relativePath);
|
|
722
798
|
if (this.#authority) {
|
|
799
|
+
this.#assertBound();
|
|
723
800
|
this.#publishRetainedNoReplace(this.#relative(resolved), bytes);
|
|
724
801
|
this.#assertBound();
|
|
725
802
|
return;
|
|
726
803
|
}
|
|
727
|
-
await publishManagedFileNoReplace(
|
|
728
|
-
resolved,
|
|
729
|
-
bytes,
|
|
730
|
-
this.#portableMutationFence ? () => this.#portableMutationFence?.assertOwned() : undefined,
|
|
731
|
-
this.#subtreeRoot,
|
|
732
|
-
this.#policy,
|
|
733
|
-
);
|
|
734
|
-
this.#assertBound();
|
|
735
|
-
this.#portableMutationFence?.assertOwned();
|
|
804
|
+
await publishManagedFileNoReplace(resolved, bytes, undefined, this.#root, this.#policy);
|
|
736
805
|
}
|
|
737
806
|
|
|
738
807
|
publishNoReplaceSync(relativePath: string, bytes: Uint8Array): void {
|
|
739
|
-
this.#
|
|
740
|
-
this.#portableMutationFence?.assertOwned();
|
|
808
|
+
this.#beforeMutation();
|
|
741
809
|
const resolved = this.#resolve(relativePath);
|
|
742
810
|
if (!this.#authority) {
|
|
743
|
-
publishManagedFileNoReplaceSync(
|
|
744
|
-
resolved,
|
|
745
|
-
bytes,
|
|
746
|
-
this.#subtreeRoot,
|
|
747
|
-
this.#policy,
|
|
748
|
-
this.#portableMutationFence ? () => this.#portableMutationFence?.assertOwned() : undefined,
|
|
749
|
-
);
|
|
811
|
+
publishManagedFileNoReplaceSync(resolved, bytes, this.#root, this.#policy);
|
|
750
812
|
this.#assertBound();
|
|
751
|
-
this.#portableMutationFence?.assertOwned();
|
|
752
813
|
return;
|
|
753
814
|
}
|
|
815
|
+
this.#assertBound();
|
|
754
816
|
this.#publishRetainedNoReplace(this.#relative(resolved), bytes);
|
|
755
817
|
this.#assertBound();
|
|
756
818
|
}
|
|
757
819
|
|
|
758
820
|
async replace(relativePath: string, bytes: Uint8Array): Promise<void> {
|
|
821
|
+
this.#beforeMutation();
|
|
759
822
|
this.#assertBound();
|
|
760
823
|
const resolved = this.#resolve(relativePath);
|
|
761
824
|
if (!this.#authority) {
|
|
762
|
-
|
|
825
|
+
try {
|
|
826
|
+
const expected = captureManagedFileNoFollow(resolved);
|
|
827
|
+
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy, undefined, expected.identity);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
830
|
+
await publishManagedFileNoReplace(resolved, bytes, undefined, this.#subtreeRoot, this.#policy);
|
|
831
|
+
}
|
|
763
832
|
this.#assertBound();
|
|
764
833
|
return;
|
|
765
834
|
}
|
|
@@ -769,17 +838,28 @@ export class ManagedSessionDescendantStore {
|
|
|
769
838
|
}
|
|
770
839
|
|
|
771
840
|
replaceExpected(relativePath: string, bytes: Uint8Array, expected: ManagedFileSnapshot): void {
|
|
841
|
+
this.#beforeMutation();
|
|
772
842
|
this.#assertBound();
|
|
773
843
|
const resolved = this.#resolve(relativePath);
|
|
774
844
|
if (!this.#authority) {
|
|
775
|
-
const fence = this.#portableMutationFence;
|
|
776
|
-
if (!fence) throw new Error("managed_replace_exact_unavailable");
|
|
777
|
-
fence.assertOwned();
|
|
778
845
|
const current = captureManagedFileNoFollow(resolved);
|
|
779
846
|
if (!sameIdentity(current.identity, expected.identity) || current.identity.sha256 !== expected.identity.sha256)
|
|
780
847
|
throw new Error("managed_replace_identity_mismatch");
|
|
781
|
-
replaceManagedFileSync(
|
|
782
|
-
|
|
848
|
+
replaceManagedFileSync(
|
|
849
|
+
resolved,
|
|
850
|
+
bytes,
|
|
851
|
+
this.#subtreeRoot,
|
|
852
|
+
this.#policy,
|
|
853
|
+
() => {
|
|
854
|
+
const currentAtCommit = captureManagedFileNoFollow(resolved);
|
|
855
|
+
if (
|
|
856
|
+
!sameIdentity(currentAtCommit.identity, expected.identity) ||
|
|
857
|
+
currentAtCommit.identity.sha256 !== expected.identity.sha256
|
|
858
|
+
)
|
|
859
|
+
throw new Error("managed_replace_identity_mismatch");
|
|
860
|
+
},
|
|
861
|
+
expected.identity,
|
|
862
|
+
);
|
|
783
863
|
return;
|
|
784
864
|
}
|
|
785
865
|
const replaced = (this.#authority as RecoveryFsRoot & RetainedManagedReplacer).replaceManaged(
|
|
@@ -795,12 +875,18 @@ export class ManagedSessionDescendantStore {
|
|
|
795
875
|
if (!replaced.ok) throw new Error(replaced.code ?? "managed_replace_failed");
|
|
796
876
|
this.#assertBound();
|
|
797
877
|
}
|
|
798
|
-
|
|
799
878
|
replaceSync(relativePath: string, bytes: Uint8Array): void {
|
|
879
|
+
this.#beforeMutation();
|
|
800
880
|
this.#assertBound();
|
|
801
881
|
const resolved = this.#resolve(relativePath);
|
|
802
882
|
if (!this.#authority) {
|
|
803
|
-
|
|
883
|
+
try {
|
|
884
|
+
const expected = captureManagedFileNoFollow(resolved);
|
|
885
|
+
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy, undefined, expected.identity);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
888
|
+
publishManagedFileNoReplaceSync(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
889
|
+
}
|
|
804
890
|
this.#assertBound();
|
|
805
891
|
return;
|
|
806
892
|
}
|
|
@@ -809,6 +895,7 @@ export class ManagedSessionDescendantStore {
|
|
|
809
895
|
}
|
|
810
896
|
|
|
811
897
|
appendSync(relativePath: string, bytes: Uint8Array): void {
|
|
898
|
+
this.#beforeMutation();
|
|
812
899
|
this.#assertBound();
|
|
813
900
|
const resolved = this.#resolve(relativePath);
|
|
814
901
|
let existing = this.readExpected(relativePath);
|
|
@@ -1006,6 +1093,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1006
1093
|
identity: {
|
|
1007
1094
|
dev: BigInt(read.identity.dev),
|
|
1008
1095
|
ino: BigInt(read.identity.ino),
|
|
1096
|
+
nlink: BigInt(read.identity.nlink),
|
|
1009
1097
|
size: Number(read.identity.size),
|
|
1010
1098
|
mtimeNs: BigInt(read.identity.mtimeNs),
|
|
1011
1099
|
ctimeNs: BigInt(read.identity.ctimeNs),
|
|
@@ -1016,6 +1104,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1016
1104
|
|
|
1017
1105
|
/** Remove an exact captured file without reopening its pathname as authority. */
|
|
1018
1106
|
removeExpected(relativePath: string, expected: ManagedFileSnapshot): void {
|
|
1107
|
+
this.#beforeMutation();
|
|
1019
1108
|
this.#assertBound();
|
|
1020
1109
|
if (!this.#authority) {
|
|
1021
1110
|
const removed = exactUnlink(this.#resolve(relativePath), {
|
|
@@ -1055,6 +1144,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1055
1144
|
}
|
|
1056
1145
|
/** Read and remove one managed descendant through retained authority. */
|
|
1057
1146
|
async consume(relativePath: string): Promise<Uint8Array | null> {
|
|
1147
|
+
this.#beforeMutation();
|
|
1058
1148
|
this.#assertBound();
|
|
1059
1149
|
const resolved = this.#resolve(relativePath);
|
|
1060
1150
|
if (this.#authority) {
|
|
@@ -1093,6 +1183,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1093
1183
|
|
|
1094
1184
|
/** Remove one managed descendant through retained authority when it exists. */
|
|
1095
1185
|
async remove(relativePath: string): Promise<void> {
|
|
1186
|
+
this.#beforeMutation();
|
|
1096
1187
|
await this.consume(relativePath);
|
|
1097
1188
|
}
|
|
1098
1189
|
|
|
@@ -1116,6 +1207,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1116
1207
|
destinationRelativePath: string,
|
|
1117
1208
|
snapshot: NativeDirectoryTreeSnapshot,
|
|
1118
1209
|
): Promise<void> {
|
|
1210
|
+
this.#beforeMutation();
|
|
1119
1211
|
const actual = this.captureTree(sourceRelativePath);
|
|
1120
1212
|
if (JSON.stringify(actual) !== JSON.stringify(snapshot)) throw new Error("artifact_source_changed");
|
|
1121
1213
|
this.ensureDirectory(destinationRelativePath);
|
|
@@ -1162,6 +1254,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1162
1254
|
destinationRelativePath: string,
|
|
1163
1255
|
expected: NativeDirectoryTreeSnapshot,
|
|
1164
1256
|
): NativeDirectoryTreeSnapshot {
|
|
1257
|
+
this.#beforeMutation();
|
|
1165
1258
|
this.#assertBound();
|
|
1166
1259
|
const moved = this.#authority
|
|
1167
1260
|
? this.#authority.renameManagedTreeNoReplace(
|
|
@@ -1189,6 +1282,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1189
1282
|
}
|
|
1190
1283
|
|
|
1191
1284
|
removeTreeExpected(relativePath: string, expected: NativeDirectoryTreeSnapshot): void {
|
|
1285
|
+
this.#beforeMutation();
|
|
1192
1286
|
this.#assertBound();
|
|
1193
1287
|
if (!this.#authority) {
|
|
1194
1288
|
const removed = exactRemoveDirectoryTree(this.#resolve(relativePath), expected);
|
|
@@ -1201,6 +1295,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1201
1295
|
this.#assertBound();
|
|
1202
1296
|
}
|
|
1203
1297
|
fsyncTree(): NativeDirectoryTreeSnapshot {
|
|
1298
|
+
this.#beforeMutation();
|
|
1204
1299
|
this.#assertBound();
|
|
1205
1300
|
if (!this.#authority) return fsyncManagedArtifactTree(this.#baseDir);
|
|
1206
1301
|
const baseRelative = this.#relative(this.#baseDir);
|
|
@@ -1296,6 +1391,7 @@ function identity(stat: fs.BigIntStats, sha256 = ""): ManagedFileSnapshot["ident
|
|
|
1296
1391
|
return {
|
|
1297
1392
|
dev: stat.dev,
|
|
1298
1393
|
ino: stat.ino,
|
|
1394
|
+
nlink: stat.nlink,
|
|
1299
1395
|
size: Number(stat.size),
|
|
1300
1396
|
mtimeNs: stat.mtimeNs,
|
|
1301
1397
|
ctimeNs: stat.ctimeNs,
|
|
@@ -1307,6 +1403,7 @@ function sameIdentity(left: ManagedFileSnapshot["identity"], right: ManagedFileS
|
|
|
1307
1403
|
return (
|
|
1308
1404
|
left.dev === right.dev &&
|
|
1309
1405
|
left.ino === right.ino &&
|
|
1406
|
+
left.nlink === right.nlink &&
|
|
1310
1407
|
left.size === right.size &&
|
|
1311
1408
|
left.mtimeNs === right.mtimeNs &&
|
|
1312
1409
|
left.ctimeNs === right.ctimeNs
|
|
@@ -1331,11 +1428,9 @@ function isCtimeOnlyIdentityMismatch(
|
|
|
1331
1428
|
return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
|
|
1332
1429
|
}
|
|
1333
1430
|
|
|
1334
|
-
function
|
|
1431
|
+
function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
|
|
1335
1432
|
try {
|
|
1336
|
-
const
|
|
1337
|
-
if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
|
|
1338
|
-
const value: unknown = JSON.parse(fs.readFileSync(pathname, "utf8"));
|
|
1433
|
+
const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
1339
1434
|
if (!value || typeof value !== "object") return undefined;
|
|
1340
1435
|
const record = value as Partial<LockRecord>;
|
|
1341
1436
|
return typeof record.attemptId === "string" &&
|
|
@@ -1343,7 +1438,8 @@ function parseLock(pathname: string): LockRecord | undefined {
|
|
|
1343
1438
|
typeof record.processStartId === "string" &&
|
|
1344
1439
|
typeof record.leaseExpiresAt === "number" &&
|
|
1345
1440
|
typeof record.heartbeatAt === "number" &&
|
|
1346
|
-
typeof record.createdAt === "number"
|
|
1441
|
+
typeof record.createdAt === "number" &&
|
|
1442
|
+
(record.released === undefined || typeof record.released === "boolean")
|
|
1347
1443
|
? (record as LockRecord)
|
|
1348
1444
|
: undefined;
|
|
1349
1445
|
} catch {
|
|
@@ -1351,6 +1447,26 @@ function parseLock(pathname: string): LockRecord | undefined {
|
|
|
1351
1447
|
}
|
|
1352
1448
|
}
|
|
1353
1449
|
|
|
1450
|
+
function parseLock(pathname: string): LockRecord | undefined {
|
|
1451
|
+
try {
|
|
1452
|
+
const stat = fs.lstatSync(pathname);
|
|
1453
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
|
|
1454
|
+
return parseLockBytes(fs.readFileSync(pathname));
|
|
1455
|
+
} catch {
|
|
1456
|
+
return undefined;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function captureLock(pathname: string): { record: LockRecord; snapshot: ManagedFileSnapshot } | undefined {
|
|
1461
|
+
try {
|
|
1462
|
+
const snapshot = captureManagedFileNoFollow(pathname);
|
|
1463
|
+
const record = parseLockBytes(snapshot.bytes);
|
|
1464
|
+
return record ? { record, snapshot } : undefined;
|
|
1465
|
+
} catch {
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1354
1470
|
function ownerDefinitelyGone(record: LockRecord): boolean {
|
|
1355
1471
|
if (record.bootId && bootId() && record.bootId !== bootId()) return true;
|
|
1356
1472
|
try {
|
|
@@ -1525,7 +1641,6 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1525
1641
|
bytes: Uint8Array,
|
|
1526
1642
|
root?: ManagedDirectoryRoot,
|
|
1527
1643
|
policy: ManagedSessionSecurityPolicy = "default",
|
|
1528
|
-
assertOwned?: () => void,
|
|
1529
1644
|
): void {
|
|
1530
1645
|
const parent = path.dirname(destination);
|
|
1531
1646
|
ensureManagedDirectory(parent, root, policy);
|
|
@@ -1536,7 +1651,6 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1536
1651
|
let outcome: NativePublishOutcome | undefined;
|
|
1537
1652
|
|
|
1538
1653
|
try {
|
|
1539
|
-
assertOwned?.();
|
|
1540
1654
|
fd = fs.openSync(
|
|
1541
1655
|
staging,
|
|
1542
1656
|
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
|
|
@@ -1550,9 +1664,7 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1550
1664
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1551
1665
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1552
1666
|
|
|
1553
|
-
assertOwned?.();
|
|
1554
1667
|
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1555
|
-
assertOwned?.();
|
|
1556
1668
|
if (!outcome.ok) throw publishFailure(outcome);
|
|
1557
1669
|
|
|
1558
1670
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
@@ -1589,14 +1701,15 @@ export function replaceManagedFileSync(
|
|
|
1589
1701
|
root: ManagedDirectoryRoot,
|
|
1590
1702
|
policy: ManagedSessionSecurityPolicy = "default",
|
|
1591
1703
|
assertFence?: () => void,
|
|
1704
|
+
expectedDestination?: ManagedFileSnapshot["identity"],
|
|
1592
1705
|
): void {
|
|
1593
1706
|
const parent = path.dirname(destination);
|
|
1594
1707
|
ensureManagedDirectory(parent, root, policy);
|
|
1595
1708
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.replacement`);
|
|
1596
1709
|
let fd: number | undefined;
|
|
1597
1710
|
let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1711
|
+
let preserveStaging = false;
|
|
1598
1712
|
try {
|
|
1599
|
-
assertFence?.();
|
|
1600
1713
|
fd = fs.openSync(
|
|
1601
1714
|
staging,
|
|
1602
1715
|
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
|
|
@@ -1609,22 +1722,137 @@ export function replaceManagedFileSync(
|
|
|
1609
1722
|
secureFileDescriptor(staging, fd, "verify");
|
|
1610
1723
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1611
1724
|
stagedIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1725
|
+
if (process.platform === "win32" && expectedDestination) {
|
|
1726
|
+
fs.closeSync(fd);
|
|
1727
|
+
fd = undefined;
|
|
1728
|
+
}
|
|
1612
1729
|
assertManagedDirectoryRoot(root);
|
|
1613
1730
|
assertFence?.();
|
|
1614
|
-
|
|
1731
|
+
if (process.platform !== "win32" && expectedDestination) {
|
|
1732
|
+
throw new Error("managed_replace_exact_unavailable");
|
|
1733
|
+
}
|
|
1734
|
+
if (process.platform === "win32" && expectedDestination) {
|
|
1735
|
+
const parentIdentity = fs.lstatSync(parent, { bigint: true });
|
|
1736
|
+
const replaced = exactReplacePath(
|
|
1737
|
+
staging,
|
|
1738
|
+
destination,
|
|
1739
|
+
{
|
|
1740
|
+
dev: staged.dev,
|
|
1741
|
+
ino: staged.ino,
|
|
1742
|
+
nlink: staged.nlink,
|
|
1743
|
+
parentDev: parentIdentity.dev,
|
|
1744
|
+
parentIno: parentIdentity.ino,
|
|
1745
|
+
size: staged.size,
|
|
1746
|
+
mtimeNs: staged.mtimeNs,
|
|
1747
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
dev: expectedDestination.dev,
|
|
1751
|
+
ino: expectedDestination.ino,
|
|
1752
|
+
nlink: expectedDestination.nlink,
|
|
1753
|
+
parentDev: parentIdentity.dev,
|
|
1754
|
+
parentIno: parentIdentity.ino,
|
|
1755
|
+
size: BigInt(expectedDestination.size),
|
|
1756
|
+
mtimeNs: expectedDestination.mtimeNs,
|
|
1757
|
+
sha256: expectedDestination.sha256,
|
|
1758
|
+
},
|
|
1759
|
+
);
|
|
1760
|
+
if (!replaced.ok) {
|
|
1761
|
+
const committedWithPredecessor =
|
|
1762
|
+
replaced.detachedPath === replaced.retainedPlaceholderPath &&
|
|
1763
|
+
replaced.retainedSuccessorPath === destination;
|
|
1764
|
+
if (committedWithPredecessor) {
|
|
1765
|
+
const retired = exactUnlink(replaced.retainedPlaceholderPath!, {
|
|
1766
|
+
dev: expectedDestination.dev,
|
|
1767
|
+
ino: expectedDestination.ino,
|
|
1768
|
+
nlink: expectedDestination.nlink,
|
|
1769
|
+
parentDev: parentIdentity.dev,
|
|
1770
|
+
parentIno: parentIdentity.ino,
|
|
1771
|
+
size: BigInt(expectedDestination.size),
|
|
1772
|
+
mtimeNs: expectedDestination.mtimeNs,
|
|
1773
|
+
sha256: expectedDestination.sha256,
|
|
1774
|
+
quarantineName: `.gjc-replace-cleanup-${expectedDestination.dev.toString(16)}-${expectedDestination.ino.toString(16)}`,
|
|
1775
|
+
});
|
|
1776
|
+
if (!retired.ok) {
|
|
1777
|
+
const predecessor =
|
|
1778
|
+
retired.detachedPath ?? retired.retainedPlaceholderPath ?? replaced.retainedPlaceholderPath;
|
|
1779
|
+
const receiptPath = path.join(
|
|
1780
|
+
parent,
|
|
1781
|
+
`.gjc-replace-cleanup-${expectedDestination.dev.toString(16)}-${expectedDestination.ino.toString(16)}.json`,
|
|
1782
|
+
);
|
|
1783
|
+
publishManagedFileNoReplaceSync(
|
|
1784
|
+
receiptPath,
|
|
1785
|
+
Buffer.from(
|
|
1786
|
+
JSON.stringify({
|
|
1787
|
+
version: 1,
|
|
1788
|
+
predecessor,
|
|
1789
|
+
successor: destination,
|
|
1790
|
+
identity: {
|
|
1791
|
+
dev: expectedDestination.dev.toString(),
|
|
1792
|
+
ino: expectedDestination.ino.toString(),
|
|
1793
|
+
nlink: expectedDestination.nlink.toString(),
|
|
1794
|
+
size: String(expectedDestination.size),
|
|
1795
|
+
mtimeNs: expectedDestination.mtimeNs.toString(),
|
|
1796
|
+
ctimeNs: expectedDestination.ctimeNs.toString(),
|
|
1797
|
+
sha256: expectedDestination.sha256,
|
|
1798
|
+
},
|
|
1799
|
+
}),
|
|
1800
|
+
),
|
|
1801
|
+
root,
|
|
1802
|
+
policy,
|
|
1803
|
+
);
|
|
1804
|
+
throw new Error(
|
|
1805
|
+
`managed_replace_cleanup_pending:receipt=${receiptPath}:predecessor=${predecessor}:successor=${destination}:code=${retired.code ?? "unknown"}`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
if (!committedWithPredecessor) {
|
|
1810
|
+
if (replaced.detachedPath) {
|
|
1811
|
+
const restored = exactRestore(replaced.detachedPath, staging, {
|
|
1812
|
+
dev: staged.dev,
|
|
1813
|
+
ino: staged.ino,
|
|
1814
|
+
nlink: staged.nlink,
|
|
1815
|
+
parentDev: parentIdentity.dev,
|
|
1816
|
+
parentIno: parentIdentity.ino,
|
|
1817
|
+
size: staged.size,
|
|
1818
|
+
mtimeNs: staged.mtimeNs,
|
|
1819
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1820
|
+
});
|
|
1821
|
+
if (restored.ok) preserveStaging = true;
|
|
1822
|
+
if (!restored.ok)
|
|
1823
|
+
throw new Error(
|
|
1824
|
+
`managed_replace_failed:${replaced.code ?? "unknown"}:retained=${replaced.detachedPath}:restore=${restored.code ?? "unknown"}`,
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
const retained = [
|
|
1828
|
+
replaced.detachedPath,
|
|
1829
|
+
replaced.retainedSuccessorPath,
|
|
1830
|
+
replaced.retainedPlaceholderPath,
|
|
1831
|
+
replaced.retainedUnknownPath,
|
|
1832
|
+
]
|
|
1833
|
+
.filter((pathname): pathname is string => typeof pathname === "string")
|
|
1834
|
+
.join(",");
|
|
1835
|
+
throw new Error(
|
|
1836
|
+
`managed_replace_failed:${replaced.code ?? "unknown"}${retained ? `:retained=${retained}` : ""}`,
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
} else fs.renameSync(staging, destination);
|
|
1615
1841
|
assertFence?.();
|
|
1616
1842
|
assertManagedDirectoryRoot(root);
|
|
1617
1843
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1618
1844
|
if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
|
|
1619
1845
|
throw new Error("destination_identity_changed");
|
|
1620
1846
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1847
|
+
if (fd !== undefined) {
|
|
1848
|
+
secureFileDescriptor(destination, fd, "verify");
|
|
1849
|
+
fs.closeSync(fd);
|
|
1850
|
+
fd = undefined;
|
|
1851
|
+
}
|
|
1624
1852
|
fsyncDirectory(parent);
|
|
1625
1853
|
} finally {
|
|
1626
1854
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1627
|
-
if (stagedIdentity) {
|
|
1855
|
+
if (stagedIdentity && !preserveStaging) {
|
|
1628
1856
|
try {
|
|
1629
1857
|
const named = fs.lstatSync(staging, { bigint: true });
|
|
1630
1858
|
if (named.dev === stagedIdentity.dev && named.ino === stagedIdentity.ino) fs.unlinkSync(staging);
|
|
@@ -1670,7 +1898,6 @@ export async function acquireManagedLock(
|
|
|
1670
1898
|
ensureManagedDirectory(locksDirectory, root, policy);
|
|
1671
1899
|
const lockPath = path.join(locksDirectory, `${name}.lock`);
|
|
1672
1900
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
1673
|
-
let staleObservedAt: number | undefined;
|
|
1674
1901
|
while (true) {
|
|
1675
1902
|
const attemptId = randomUUID();
|
|
1676
1903
|
const now = Date.now();
|
|
@@ -1703,12 +1930,8 @@ export async function acquireManagedLock(
|
|
|
1703
1930
|
let descriptorClosed = false;
|
|
1704
1931
|
const closeDescriptor = (): void => {
|
|
1705
1932
|
if (descriptorClosed) return;
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
} finally {
|
|
1709
|
-
fs.closeSync(fd);
|
|
1710
|
-
descriptorClosed = true;
|
|
1711
|
-
}
|
|
1933
|
+
fs.closeSync(fd);
|
|
1934
|
+
descriptorClosed = true;
|
|
1712
1935
|
};
|
|
1713
1936
|
const assertOwned = (): void => {
|
|
1714
1937
|
const current = parseLock(lockPath);
|
|
@@ -1722,17 +1945,23 @@ export async function acquireManagedLock(
|
|
|
1722
1945
|
released ||
|
|
1723
1946
|
descriptorClosed ||
|
|
1724
1947
|
!current ||
|
|
1948
|
+
current.released === true ||
|
|
1725
1949
|
!sameFileIdentity(lockIdentity, named) ||
|
|
1726
|
-
current.attemptId !== attemptId
|
|
1727
|
-
current.leaseExpiresAt < Date.now()
|
|
1950
|
+
current.attemptId !== attemptId
|
|
1728
1951
|
)
|
|
1729
1952
|
throw new Error("migration_busy");
|
|
1953
|
+
const now = Date.now();
|
|
1954
|
+
if (current.leaseExpiresAt < now + LOCK_HEARTBEAT_MS) {
|
|
1955
|
+
writeLockDescriptor(fd, {
|
|
1956
|
+
...record,
|
|
1957
|
+
heartbeatAt: now,
|
|
1958
|
+
leaseExpiresAt: now + LOCK_LEASE_MS,
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1730
1961
|
};
|
|
1731
1962
|
const heartbeat = setInterval(() => {
|
|
1732
1963
|
try {
|
|
1733
1964
|
assertOwned();
|
|
1734
|
-
const now = Date.now();
|
|
1735
|
-
writeLockDescriptor(fd, { ...record, heartbeatAt: now, leaseExpiresAt: now + LOCK_LEASE_MS });
|
|
1736
1965
|
} catch {
|
|
1737
1966
|
/* fencing rejects later publication */
|
|
1738
1967
|
}
|
|
@@ -1745,10 +1974,16 @@ export async function acquireManagedLock(
|
|
|
1745
1974
|
clearInterval(heartbeat);
|
|
1746
1975
|
try {
|
|
1747
1976
|
assertOwned();
|
|
1977
|
+
secureFileDescriptor(lockPath, fd, "verify");
|
|
1748
1978
|
const now = Date.now();
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1751
|
-
writeLockDescriptor(fd, {
|
|
1979
|
+
// A released record is the only live-process reclaim authority. Expiry alone
|
|
1980
|
+
// never authorizes stealing from a holder whose process is still present.
|
|
1981
|
+
writeLockDescriptor(fd, {
|
|
1982
|
+
...record,
|
|
1983
|
+
released: true,
|
|
1984
|
+
heartbeatAt: now,
|
|
1985
|
+
leaseExpiresAt: now,
|
|
1986
|
+
});
|
|
1752
1987
|
fsyncDirectory(locksDirectory);
|
|
1753
1988
|
} finally {
|
|
1754
1989
|
released = true;
|
|
@@ -1758,24 +1993,27 @@ export async function acquireManagedLock(
|
|
|
1758
1993
|
};
|
|
1759
1994
|
} catch (error) {
|
|
1760
1995
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1996
|
+
const observed = captureLock(lockPath);
|
|
1997
|
+
const owner = observed?.record;
|
|
1998
|
+
const reclaimable =
|
|
1999
|
+
owner?.released === true ||
|
|
2000
|
+
(owner !== undefined && owner.leaseExpiresAt < Date.now() && ownerDefinitelyGone(owner));
|
|
2001
|
+
if (observed && owner && reclaimable) {
|
|
2002
|
+
try {
|
|
2003
|
+
ManagedLockTestHooks.beforeObservedRetirement?.({ path: lockPath, attemptId: owner.attemptId });
|
|
2004
|
+
const removed = exactUnlink(lockPath, {
|
|
2005
|
+
dev: observed.snapshot.identity.dev,
|
|
2006
|
+
ino: observed.snapshot.identity.ino,
|
|
2007
|
+
size: BigInt(observed.snapshot.identity.size),
|
|
2008
|
+
mtimeNs: observed.snapshot.identity.mtimeNs,
|
|
2009
|
+
sha256: observed.snapshot.identity.sha256,
|
|
2010
|
+
quarantineName: `.gjc-lock-${randomUUID()}.stale`,
|
|
2011
|
+
});
|
|
2012
|
+
if (removed.ok || removed.code === "cleanup_pending") fsyncDirectory(locksDirectory);
|
|
2013
|
+
} catch {
|
|
2014
|
+
/* retry owner observation */
|
|
1777
2015
|
}
|
|
1778
|
-
}
|
|
2016
|
+
}
|
|
1779
2017
|
if (Date.now() >= deadline) throw new Error("migration_busy");
|
|
1780
2018
|
await new Promise<void>(resolve => setTimeout(resolve, 50));
|
|
1781
2019
|
}
|