@nowcrew/daemon 0.6.88-beta.7 → 0.6.88-beta.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/dist/main.js +0 -0
- package/package.json +11 -10
- package/dist/project-skills/capability.js +0 -109
- package/dist/project-skills/controller-convergence.js +0 -57
- package/dist/project-skills/projection-set-switch.js +0 -419
- package/dist/project-skills/projection-state-domain.js +0 -170
- package/dist/project-skills/projection-state-store.js +0 -842
- package/dist/project-skills/projection-state-transaction.js +0 -322
- package/dist/project-skills/projection-state.js +0 -3
- package/dist/project-skills/runtime-root-bootstrap.js +0 -47
- package/dist/project-skills/runtime-root-domain.js +0 -268
- package/dist/project-skills/runtime-root-gc.js +0 -293
- package/dist/project-skills/runtime-root-lease-artifact.js +0 -46
- package/dist/project-skills/runtime-root-leases.js +0 -487
- package/dist/project-skills/runtime-root-source-identity.js +0 -60
- package/dist/project-skills/runtime-root-startup.js +0 -49
- package/dist/project-skills/runtime-root-state-artifact-domain.js +0 -143
- package/dist/project-skills/runtime-root-state-index.js +0 -356
- package/dist/project-skills/runtime-root-store.js +0 -722
- package/dist/project-skills/serve-capability.js +0 -28
- package/dist/project-skills/serve-startup.js +0 -37
package/dist/main.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.6.88-beta.
|
|
3
|
+
"version": "0.6.88-beta.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -16,9 +16,17 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
|
|
23
|
+
"prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
},
|
|
19
27
|
"dependencies": {
|
|
20
28
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
21
|
-
"@nowcrew/cli": "0.4.
|
|
29
|
+
"@nowcrew/cli": "0.4.14",
|
|
22
30
|
"cross-spawn": "^7.0.6",
|
|
23
31
|
"semver": "7.8.5",
|
|
24
32
|
"ws": "^8",
|
|
@@ -36,12 +44,5 @@
|
|
|
36
44
|
"tsx": "^4.19.0",
|
|
37
45
|
"typescript": "^5.6.0",
|
|
38
46
|
"vitest": "^2.1.0"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
42
|
-
"build": "tsc -p tsconfig.json",
|
|
43
|
-
"codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
|
|
44
|
-
"test": "vitest run",
|
|
45
|
-
"typecheck": "tsc --noEmit"
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { PROJECT_SKILLS_CAPABILITY, PROJECT_SKILL_PROJECTION_V2_CAPABILITY, } from "./types.js";
|
|
2
|
-
export function projectSkillV2RolloutReady(platform, readiness) {
|
|
3
|
-
const platformReady = platform === "darwin"
|
|
4
|
-
|| platform === "linux"
|
|
5
|
-
|| (platform === "win32" && readiness.nativeWindowsSmokePassed);
|
|
6
|
-
return platformReady
|
|
7
|
-
&& readiness.runtimeRootStorePrimitiveReady
|
|
8
|
-
&& readiness.runtimeRootStoreRecoveryReady
|
|
9
|
-
&& readiness.leaseRecoveryReady
|
|
10
|
-
&& readiness.controllerReady
|
|
11
|
-
&& readiness.executionHandlerReady
|
|
12
|
-
&& readiness.codexAdapterReady
|
|
13
|
-
&& readiness.claudeAdapterReady;
|
|
14
|
-
}
|
|
15
|
-
export function effectiveProjectSkillCapabilities(capabilities, projectSkillsReady, projectSkillV2Ready) {
|
|
16
|
-
const projectSkillCapabilitiesReady = projectSkillsReady && projectSkillV2Ready;
|
|
17
|
-
if (!projectSkillCapabilitiesReady
|
|
18
|
-
&& !capabilities.includes(PROJECT_SKILLS_CAPABILITY)
|
|
19
|
-
&& !capabilities.includes(PROJECT_SKILL_PROJECTION_V2_CAPABILITY)
|
|
20
|
-
&& Object.isFrozen(capabilities))
|
|
21
|
-
return capabilities;
|
|
22
|
-
const filtered = capabilities.filter((capability) => capability !== PROJECT_SKILL_PROJECTION_V2_CAPABILITY
|
|
23
|
-
&& (projectSkillCapabilitiesReady || capability !== PROJECT_SKILLS_CAPABILITY));
|
|
24
|
-
if (!projectSkillCapabilitiesReady || !filtered.includes(PROJECT_SKILLS_CAPABILITY)) {
|
|
25
|
-
return Object.freeze(filtered);
|
|
26
|
-
}
|
|
27
|
-
const v1Index = filtered.indexOf(PROJECT_SKILLS_CAPABILITY);
|
|
28
|
-
return Object.freeze([
|
|
29
|
-
...filtered.slice(0, v1Index + 1),
|
|
30
|
-
PROJECT_SKILL_PROJECTION_V2_CAPABILITY,
|
|
31
|
-
...filtered.slice(v1Index + 1),
|
|
32
|
-
]);
|
|
33
|
-
}
|
|
34
|
-
export function withProjectSkillCapabilityStatus(value, status) {
|
|
35
|
-
return {
|
|
36
|
-
...value,
|
|
37
|
-
projectSkillsStatus: status,
|
|
38
|
-
capabilities: status === "ready"
|
|
39
|
-
? value.capabilities
|
|
40
|
-
: effectiveProjectSkillCapabilities(value.capabilities, false, false),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
const settleReadinessGate = async (operation) => {
|
|
44
|
-
try {
|
|
45
|
-
return await operation() === true;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
export async function createProjectSkillConnectionCapabilitySnapshot(input) {
|
|
52
|
-
const runtimeRootStorePrimitiveReady = await settleReadinessGate(input.probeRuntimeRootStorePrimitive);
|
|
53
|
-
const controllerReady = await settleReadinessGate(input.initializeController);
|
|
54
|
-
const recoveryReady = await settleReadinessGate(input.recoverRuntimeRootsAndLeases);
|
|
55
|
-
const readiness = Object.freeze({
|
|
56
|
-
runtimeRootStorePrimitiveReady,
|
|
57
|
-
runtimeRootStoreRecoveryReady: recoveryReady,
|
|
58
|
-
leaseRecoveryReady: recoveryReady,
|
|
59
|
-
controllerReady,
|
|
60
|
-
executionHandlerReady: true,
|
|
61
|
-
codexAdapterReady: true,
|
|
62
|
-
claudeAdapterReady: true,
|
|
63
|
-
nativeWindowsSmokePassed: false,
|
|
64
|
-
});
|
|
65
|
-
const projectSkillsReady = projectSkillV2RolloutReady(input.platform, readiness);
|
|
66
|
-
return Object.freeze({
|
|
67
|
-
capabilities: effectiveProjectSkillCapabilities(input.baseCapabilities, projectSkillsReady, projectSkillsReady),
|
|
68
|
-
projectSkillsReady,
|
|
69
|
-
readiness,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
export async function prepareProjectSkillConnectionCapabilities(input) {
|
|
73
|
-
const snapshot = await createProjectSkillConnectionCapabilitySnapshot({
|
|
74
|
-
...input,
|
|
75
|
-
baseCapabilities: [...input.baseCapabilities, ...await input.additionalCapabilities()],
|
|
76
|
-
});
|
|
77
|
-
input.applyReadiness(snapshot.projectSkillsReady);
|
|
78
|
-
return snapshot.capabilities;
|
|
79
|
-
}
|
|
80
|
-
export function createProjectSkillControllerReadiness(input) {
|
|
81
|
-
let status = "initializing";
|
|
82
|
-
let initialization = null;
|
|
83
|
-
const ensure = () => {
|
|
84
|
-
if (status === "ready")
|
|
85
|
-
return Promise.resolve(true);
|
|
86
|
-
if (initialization !== null)
|
|
87
|
-
return initialization;
|
|
88
|
-
status = "initializing";
|
|
89
|
-
const attempt = input.initialize().then(() => {
|
|
90
|
-
status = "ready";
|
|
91
|
-
return true;
|
|
92
|
-
}, (error) => {
|
|
93
|
-
status = "unavailable";
|
|
94
|
-
input.onUnavailable(error);
|
|
95
|
-
return false;
|
|
96
|
-
}).finally(() => {
|
|
97
|
-
if (initialization === attempt)
|
|
98
|
-
initialization = null;
|
|
99
|
-
input.onSettled();
|
|
100
|
-
});
|
|
101
|
-
initialization = attempt;
|
|
102
|
-
return attempt;
|
|
103
|
-
};
|
|
104
|
-
return Object.freeze({
|
|
105
|
-
status: () => status,
|
|
106
|
-
ensure,
|
|
107
|
-
applyPreflight: (ready) => { status = ready ? "ready" : "unavailable"; },
|
|
108
|
-
});
|
|
109
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { ProjectProjectionError, } from "./reconciler.js";
|
|
2
|
-
export const MAX_REMEMBERED_PROJECT_SKILL_AGENTS = 64;
|
|
3
|
-
const compareBinary = (left, right) => {
|
|
4
|
-
if (left < right)
|
|
5
|
-
return -1;
|
|
6
|
-
if (left > right)
|
|
7
|
-
return 1;
|
|
8
|
-
return 0;
|
|
9
|
-
};
|
|
10
|
-
const copySnapshot = (snapshot) => Object.freeze({
|
|
11
|
-
bindings: Object.freeze(snapshot.bindings.map((binding) => Object.freeze({ ...binding }))),
|
|
12
|
-
generation: snapshot.generation,
|
|
13
|
-
});
|
|
14
|
-
export function createProjectSkillSnapshotEpoch(maxEntries = MAX_REMEMBERED_PROJECT_SKILL_AGENTS) {
|
|
15
|
-
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
|
|
16
|
-
throw new ProjectProjectionError("skill_projection_failed");
|
|
17
|
-
}
|
|
18
|
-
const snapshots = new Map();
|
|
19
|
-
return Object.freeze({
|
|
20
|
-
begin() {
|
|
21
|
-
snapshots.clear();
|
|
22
|
-
},
|
|
23
|
-
remember(handle, snapshot) {
|
|
24
|
-
const previous = snapshots.get(handle);
|
|
25
|
-
if (previous !== undefined && snapshot.generation < previous.generation)
|
|
26
|
-
return;
|
|
27
|
-
if (previous === undefined && snapshots.size >= maxEntries) {
|
|
28
|
-
throw new ProjectProjectionError("skill_projection_failed");
|
|
29
|
-
}
|
|
30
|
-
snapshots.set(handle, copySnapshot(snapshot));
|
|
31
|
-
},
|
|
32
|
-
forget(handle) {
|
|
33
|
-
snapshots.delete(handle);
|
|
34
|
-
},
|
|
35
|
-
entries() {
|
|
36
|
-
return Object.freeze([...snapshots.entries()]
|
|
37
|
-
.sort(([left], [right]) => compareBinary(left, right))
|
|
38
|
-
.map(([handle, snapshot]) => Object.freeze([handle, copySnapshot(snapshot)])));
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
const stableErrorCode = (reason) => reason?.code === "skill_projection_snapshot_corrupt"
|
|
43
|
-
? "skill_projection_snapshot_corrupt"
|
|
44
|
-
: "skill_projection_failed";
|
|
45
|
-
export function toPathPrivateConvergenceResults(entries, results) {
|
|
46
|
-
return Object.freeze(entries.map(([handle], index) => {
|
|
47
|
-
const result = results[index];
|
|
48
|
-
if (result?.status === "fulfilled") {
|
|
49
|
-
return Object.freeze({ handle, status: "fulfilled" });
|
|
50
|
-
}
|
|
51
|
-
return Object.freeze({
|
|
52
|
-
handle,
|
|
53
|
-
status: "rejected",
|
|
54
|
-
errorCode: stableErrorCode(result?.reason),
|
|
55
|
-
});
|
|
56
|
-
}));
|
|
57
|
-
}
|
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
import { lstat, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { durableAtomicPrivateWrite, durableDirectorySync, durablePrivateUnlink, } from "../atomic-private-write.js";
|
|
4
|
-
import { commitPreparedProjectSkillManifestUpdate, parseProjectSkillManifestUpdateRecord, preparedProjectSkillManifestUpdateFromRecord, projectSkillManifestUpdateRecord, publishPreparedProjectSkillManifestUpdate, rollbackPreparedProjectSkillManifestUpdate, } from "./projection-state.js";
|
|
5
|
-
export const PROJECTION_ROOT_MARKER_NAME = ".nowcrew-project-skills-root.json";
|
|
6
|
-
const JOURNAL_NAME = "project-skills-switch.json";
|
|
7
|
-
const MAX_JOURNAL_BYTES = 768 * 1024;
|
|
8
|
-
const MAX_ROOT_MARKER_BYTES = 16 * 1024;
|
|
9
|
-
const NONCE = /^[A-Za-z0-9-]{1,128}$/u;
|
|
10
|
-
const PHASES = new Set([
|
|
11
|
-
"prepared",
|
|
12
|
-
"codex:backup_moved",
|
|
13
|
-
"codex:activated",
|
|
14
|
-
"claude:backup_moved",
|
|
15
|
-
"claude:activated",
|
|
16
|
-
"committed",
|
|
17
|
-
"rolling_back",
|
|
18
|
-
"rollback_failed",
|
|
19
|
-
]);
|
|
20
|
-
const errorCode = (error) => error.code;
|
|
21
|
-
export const projectionPathExists = async (path, statPath = lstat) => {
|
|
22
|
-
try {
|
|
23
|
-
await statPath(path);
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
if (errorCode(error) === "ENOENT")
|
|
28
|
-
return false;
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
const exists = projectionPathExists;
|
|
33
|
-
const sameIdentity = (left, right) => left.dev === right.dev
|
|
34
|
-
&& left.ino === right.ino
|
|
35
|
-
&& left.birthtimeNs === right.birthtimeNs;
|
|
36
|
-
const directoryIdentity = async (path) => {
|
|
37
|
-
try {
|
|
38
|
-
const info = await lstat(path, { bigint: true });
|
|
39
|
-
if (!info.isDirectory() || info.isSymbolicLink())
|
|
40
|
-
throw new Error("projection_switch_path_unmanaged");
|
|
41
|
-
return Object.freeze({
|
|
42
|
-
dev: String(info.dev),
|
|
43
|
-
ino: String(info.ino),
|
|
44
|
-
birthtimeNs: String(info.birthtimeNs),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
if (errorCode(error) === "ENOENT")
|
|
49
|
-
return null;
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
export const projectionSwitchJournalPath = (agentRoot) => join(agentRoot, ".crew", JOURNAL_NAME);
|
|
54
|
-
export const projectionTargets = (agentRoot, nonce) => Object.freeze([
|
|
55
|
-
Object.freeze({
|
|
56
|
-
runtime: "codex",
|
|
57
|
-
target: join(agentRoot, ".agents", "skills"),
|
|
58
|
-
staging: join(agentRoot, ".agents", `.skills-next-${nonce}`),
|
|
59
|
-
backup: join(agentRoot, ".agents", `.skills-previous-${nonce}`),
|
|
60
|
-
discard: join(agentRoot, ".agents", `.skills-discard-${nonce}`),
|
|
61
|
-
}),
|
|
62
|
-
Object.freeze({
|
|
63
|
-
runtime: "claude",
|
|
64
|
-
target: join(agentRoot, ".crew", "claude-skills", ".claude", "skills"),
|
|
65
|
-
staging: join(agentRoot, ".crew", "claude-skills", ".claude", `.skills-next-${nonce}`),
|
|
66
|
-
backup: join(agentRoot, ".crew", "claude-skills", ".claude", `.skills-previous-${nonce}`),
|
|
67
|
-
discard: join(agentRoot, ".crew", "claude-skills", ".claude", `.skills-discard-${nonce}`),
|
|
68
|
-
}),
|
|
69
|
-
]);
|
|
70
|
-
const rootMarker = (target, nonce) => Object.freeze({
|
|
71
|
-
managedBy: "nowcrew-project-skills-root",
|
|
72
|
-
version: 1,
|
|
73
|
-
nonce,
|
|
74
|
-
runtime: target.runtime,
|
|
75
|
-
target: target.target,
|
|
76
|
-
});
|
|
77
|
-
const readRootMarker = async (root) => {
|
|
78
|
-
const markerPath = join(root, PROJECTION_ROOT_MARKER_NAME);
|
|
79
|
-
try {
|
|
80
|
-
const info = await lstat(markerPath);
|
|
81
|
-
if (!info.isFile() || info.isSymbolicLink() || Number(info.size) > MAX_ROOT_MARKER_BYTES)
|
|
82
|
-
return null;
|
|
83
|
-
const candidate = JSON.parse(await readFile(markerPath, "utf8"));
|
|
84
|
-
return candidate.managedBy === "nowcrew-project-skills-root"
|
|
85
|
-
&& candidate.version === 1
|
|
86
|
-
&& NONCE.test(candidate.nonce ?? "")
|
|
87
|
-
&& (candidate.runtime === "codex" || candidate.runtime === "claude")
|
|
88
|
-
&& typeof candidate.target === "string"
|
|
89
|
-
? candidate
|
|
90
|
-
: null;
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
if (errorCode(error) === "ENOENT" || error instanceof SyntaxError)
|
|
94
|
-
return null;
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
const markerMatches = (marker, target, nonce) => marker !== null
|
|
99
|
-
&& marker.runtime === target.runtime
|
|
100
|
-
&& marker.target === target.target
|
|
101
|
-
&& (nonce === undefined || marker.nonce === nonce);
|
|
102
|
-
const assertOwnedRoot = async (root, target, nonce) => {
|
|
103
|
-
const info = await directoryIdentity(root);
|
|
104
|
-
if (info === null || !markerMatches(await readRootMarker(root), target, nonce)) {
|
|
105
|
-
throw new Error("projection_switch_path_unmanaged");
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
export const markProjectionStaging = async (target, nonce) => {
|
|
109
|
-
await writeFile(join(target.staging, PROJECTION_ROOT_MARKER_NAME), JSON.stringify(rootMarker(target, nonce)), {
|
|
110
|
-
encoding: "utf8",
|
|
111
|
-
mode: 0o600,
|
|
112
|
-
flag: "wx",
|
|
113
|
-
});
|
|
114
|
-
};
|
|
115
|
-
export const isManagedProjectionRoot = async (root, target) => markerMatches(await readRootMarker(root), target);
|
|
116
|
-
export const cleanupProjectionStaging = async (target, nonce, syncDirectory = durableDirectorySync) => {
|
|
117
|
-
if (!await exists(target.staging))
|
|
118
|
-
return;
|
|
119
|
-
await assertOwnedRoot(target.staging, target, nonce);
|
|
120
|
-
await rm(target.staging, { recursive: true, force: true });
|
|
121
|
-
await syncDirectory(dirname(target.target));
|
|
122
|
-
};
|
|
123
|
-
const exactJournalTargets = (agentRoot, nonce, candidates) => {
|
|
124
|
-
const expected = projectionTargets(agentRoot, nonce);
|
|
125
|
-
return candidates.length === expected.length && expected.every((target, index) => {
|
|
126
|
-
const candidate = candidates[index];
|
|
127
|
-
return candidate?.runtime === target.runtime
|
|
128
|
-
&& candidate.target === target.target
|
|
129
|
-
&& candidate.staging === target.staging
|
|
130
|
-
&& candidate.backup === target.backup
|
|
131
|
-
&& candidate.discard === target.discard
|
|
132
|
-
&& typeof candidate.hadPrevious === "boolean"
|
|
133
|
-
&& (candidate.previousIdentity === null
|
|
134
|
-
|| (typeof candidate.previousIdentity?.dev === "string"
|
|
135
|
-
&& typeof candidate.previousIdentity.ino === "string"
|
|
136
|
-
&& typeof candidate.previousIdentity.birthtimeNs === "string"))
|
|
137
|
-
&& typeof candidate.activatedIdentity?.dev === "string"
|
|
138
|
-
&& typeof candidate.activatedIdentity.ino === "string"
|
|
139
|
-
&& typeof candidate.activatedIdentity.birthtimeNs === "string";
|
|
140
|
-
});
|
|
141
|
-
};
|
|
142
|
-
const parseJournal = (raw, agentRoot) => {
|
|
143
|
-
const candidate = JSON.parse(raw);
|
|
144
|
-
if (candidate.managedBy !== "nowcrew-project-skills-switch"
|
|
145
|
-
|| candidate.version !== 1
|
|
146
|
-
|| !NONCE.test(candidate.nonce ?? "")
|
|
147
|
-
|| !PHASES.has(candidate.phase)
|
|
148
|
-
|| !Array.isArray(candidate.targets)
|
|
149
|
-
|| !exactJournalTargets(agentRoot, candidate.nonce, candidate.targets)
|
|
150
|
-
|| Object.keys(candidate).some((key) => ![
|
|
151
|
-
"managedBy", "version", "nonce", "phase", "targets", "manifestUpdate",
|
|
152
|
-
].includes(key))) {
|
|
153
|
-
throw new Error("projection_switch_journal_invalid");
|
|
154
|
-
}
|
|
155
|
-
const manifestUpdate = candidate.manifestUpdate === undefined
|
|
156
|
-
? undefined
|
|
157
|
-
: parseProjectSkillManifestUpdateRecord(candidate.manifestUpdate);
|
|
158
|
-
return Object.freeze({
|
|
159
|
-
managedBy: "nowcrew-project-skills-switch",
|
|
160
|
-
version: 1,
|
|
161
|
-
nonce: candidate.nonce,
|
|
162
|
-
phase: candidate.phase,
|
|
163
|
-
targets: candidate.targets,
|
|
164
|
-
...(manifestUpdate === undefined ? {} : { manifestUpdate }),
|
|
165
|
-
});
|
|
166
|
-
};
|
|
167
|
-
const readJournal = async (agentRoot) => {
|
|
168
|
-
const path = projectionSwitchJournalPath(agentRoot);
|
|
169
|
-
let info;
|
|
170
|
-
try {
|
|
171
|
-
info = await lstat(path);
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
if (errorCode(error) === "ENOENT")
|
|
175
|
-
return null;
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
178
|
-
if (!info.isFile() || info.isSymbolicLink() || Number(info.size) > MAX_JOURNAL_BYTES) {
|
|
179
|
-
throw new Error("projection_switch_journal_invalid");
|
|
180
|
-
}
|
|
181
|
-
return parseJournal(await readFile(path, "utf8"), agentRoot);
|
|
182
|
-
};
|
|
183
|
-
const writeJournal = async (agentRoot, journal) => durableAtomicPrivateWrite(projectionSwitchJournalPath(agentRoot), `${JSON.stringify(journal)}\n`);
|
|
184
|
-
const withPhase = (journal, phase) => Object.freeze({ ...journal, phase });
|
|
185
|
-
const unlinkExactJournal = async (agentRoot, nonce) => {
|
|
186
|
-
const current = await readJournal(agentRoot);
|
|
187
|
-
if (current === null)
|
|
188
|
-
return;
|
|
189
|
-
if (current.nonce !== nonce)
|
|
190
|
-
throw new Error("projection_switch_journal_invalid");
|
|
191
|
-
await durablePrivateUnlink(projectionSwitchJournalPath(agentRoot));
|
|
192
|
-
};
|
|
193
|
-
const assertPrevious = async (path, expected) => {
|
|
194
|
-
const actual = await directoryIdentity(path);
|
|
195
|
-
if (actual === null || !sameIdentity(actual, expected))
|
|
196
|
-
throw new Error("projection_switch_backup_unmanaged");
|
|
197
|
-
};
|
|
198
|
-
const assertActivated = async (path, target, nonce) => {
|
|
199
|
-
await assertOwnedRoot(path, target, nonce);
|
|
200
|
-
await assertPrevious(path, target.activatedIdentity);
|
|
201
|
-
};
|
|
202
|
-
const cleanupDiscard = async (target, journal, syncDirectory) => {
|
|
203
|
-
const discardIdentity = await directoryIdentity(target.discard);
|
|
204
|
-
if (discardIdentity === null)
|
|
205
|
-
return;
|
|
206
|
-
if (target.previousIdentity !== null && sameIdentity(discardIdentity, target.previousIdentity)) {
|
|
207
|
-
await rm(target.discard, { recursive: true, force: true });
|
|
208
|
-
await syncDirectory(dirname(target.target));
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
await assertActivated(target.discard, target, journal.nonce);
|
|
212
|
-
await rm(target.discard, { recursive: true, force: true });
|
|
213
|
-
await syncDirectory(dirname(target.target));
|
|
214
|
-
};
|
|
215
|
-
const finalizeCommitted = async (agentRoot, journal, syncDirectory, manifestOperations, unlinkJournal = unlinkExactJournal) => {
|
|
216
|
-
for (const target of journal.targets)
|
|
217
|
-
await assertActivated(target.target, target, journal.nonce);
|
|
218
|
-
const manifestUpdate = journal.manifestUpdate === undefined
|
|
219
|
-
? null
|
|
220
|
-
: preparedProjectSkillManifestUpdateFromRecord(agentRoot, journal.manifestUpdate);
|
|
221
|
-
if (manifestUpdate !== null)
|
|
222
|
-
await manifestOperations.publish(manifestUpdate);
|
|
223
|
-
for (const target of journal.targets) {
|
|
224
|
-
if (target.hadPrevious && await exists(target.backup)) {
|
|
225
|
-
if (target.previousIdentity === null)
|
|
226
|
-
throw new Error("projection_switch_backup_unmanaged");
|
|
227
|
-
await assertPrevious(target.backup, target.previousIdentity);
|
|
228
|
-
if (await exists(target.discard))
|
|
229
|
-
throw new Error("projection_switch_path_unmanaged");
|
|
230
|
-
await rename(target.backup, target.discard);
|
|
231
|
-
await syncDirectory(dirname(target.target));
|
|
232
|
-
await assertPrevious(target.discard, target.previousIdentity);
|
|
233
|
-
}
|
|
234
|
-
if (await exists(target.discard)) {
|
|
235
|
-
if (target.previousIdentity === null)
|
|
236
|
-
throw new Error("projection_switch_backup_unmanaged");
|
|
237
|
-
await assertPrevious(target.discard, target.previousIdentity);
|
|
238
|
-
await rm(target.discard, { recursive: true, force: true });
|
|
239
|
-
await syncDirectory(dirname(target.target));
|
|
240
|
-
}
|
|
241
|
-
await cleanupProjectionStaging(target, journal.nonce, syncDirectory);
|
|
242
|
-
}
|
|
243
|
-
for (const target of journal.targets)
|
|
244
|
-
await syncDirectory(dirname(target.target));
|
|
245
|
-
if (manifestUpdate !== null)
|
|
246
|
-
await manifestOperations.commit(manifestUpdate);
|
|
247
|
-
await unlinkJournal(agentRoot, journal.nonce);
|
|
248
|
-
};
|
|
249
|
-
const rollback = async (agentRoot, journal, syncDirectory, manifestOperations) => {
|
|
250
|
-
let currentJournal = withPhase(journal, "rolling_back");
|
|
251
|
-
await writeJournal(agentRoot, currentJournal);
|
|
252
|
-
try {
|
|
253
|
-
for (const target of [...currentJournal.targets].reverse()) {
|
|
254
|
-
const currentIdentity = await directoryIdentity(target.target);
|
|
255
|
-
const currentIsPrevious = currentIdentity !== null
|
|
256
|
-
&& target.previousIdentity !== null
|
|
257
|
-
&& sameIdentity(currentIdentity, target.previousIdentity);
|
|
258
|
-
if (!currentIsPrevious && currentIdentity !== null) {
|
|
259
|
-
await assertActivated(target.target, target, currentJournal.nonce);
|
|
260
|
-
if (await exists(target.discard))
|
|
261
|
-
throw new Error("projection_switch_path_unmanaged");
|
|
262
|
-
await rename(target.target, target.discard);
|
|
263
|
-
await syncDirectory(dirname(target.target));
|
|
264
|
-
await assertActivated(target.discard, target, currentJournal.nonce);
|
|
265
|
-
}
|
|
266
|
-
const afterActivationRemoved = await directoryIdentity(target.target);
|
|
267
|
-
if (target.hadPrevious) {
|
|
268
|
-
if (target.previousIdentity === null)
|
|
269
|
-
throw new Error("projection_switch_backup_unmanaged");
|
|
270
|
-
if (afterActivationRemoved === null) {
|
|
271
|
-
await assertPrevious(target.backup, target.previousIdentity);
|
|
272
|
-
await rename(target.backup, target.target);
|
|
273
|
-
await syncDirectory(dirname(target.target));
|
|
274
|
-
await assertPrevious(target.target, target.previousIdentity);
|
|
275
|
-
}
|
|
276
|
-
else if (!sameIdentity(afterActivationRemoved, target.previousIdentity)) {
|
|
277
|
-
throw new Error("projection_switch_path_unmanaged");
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
else if (afterActivationRemoved !== null) {
|
|
281
|
-
throw new Error("projection_switch_path_unmanaged");
|
|
282
|
-
}
|
|
283
|
-
await cleanupDiscard(target, currentJournal, syncDirectory);
|
|
284
|
-
await cleanupProjectionStaging(target, currentJournal.nonce, syncDirectory);
|
|
285
|
-
}
|
|
286
|
-
if (currentJournal.manifestUpdate !== undefined) {
|
|
287
|
-
await manifestOperations.rollback(preparedProjectSkillManifestUpdateFromRecord(agentRoot, currentJournal.manifestUpdate));
|
|
288
|
-
}
|
|
289
|
-
for (const target of currentJournal.targets)
|
|
290
|
-
await syncDirectory(dirname(target.target));
|
|
291
|
-
await unlinkExactJournal(agentRoot, currentJournal.nonce);
|
|
292
|
-
}
|
|
293
|
-
catch (error) {
|
|
294
|
-
currentJournal = withPhase(currentJournal, "rollback_failed");
|
|
295
|
-
await writeJournal(agentRoot, currentJournal);
|
|
296
|
-
throw error;
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
export const recoverProjectionSwitch = async (agentRoot, syncDirectory = durableDirectorySync, manifestOperations = {}) => {
|
|
300
|
-
const operations = {
|
|
301
|
-
publish: manifestOperations.publish ?? publishPreparedProjectSkillManifestUpdate,
|
|
302
|
-
commit: manifestOperations.commit ?? commitPreparedProjectSkillManifestUpdate,
|
|
303
|
-
rollback: manifestOperations.rollback ?? rollbackPreparedProjectSkillManifestUpdate,
|
|
304
|
-
};
|
|
305
|
-
const journal = await readJournal(agentRoot);
|
|
306
|
-
if (journal === null)
|
|
307
|
-
return;
|
|
308
|
-
if (journal.phase === "committed") {
|
|
309
|
-
await finalizeCommitted(agentRoot, journal, syncDirectory, operations);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
await rollback(agentRoot, journal, syncDirectory, operations);
|
|
313
|
-
};
|
|
314
|
-
/**
|
|
315
|
-
* Switches the Codex/Claude roots as one recoverable projection set while the Agent coordinator lock is held.
|
|
316
|
-
* This is not a mythical two-directory filesystem transaction: each root is individually published by rename,
|
|
317
|
-
* so a mixed pair may exist inside the locked switch interval. Every bounded, path-exact journal phase fsyncs its
|
|
318
|
-
* temporary contents before rename and its parent directory afterward. Each target-parent mutation is also fsynced:
|
|
319
|
-
* activations before the durable commit phase, and restoration/cleanup plus a final parent barrier before journal
|
|
320
|
-
* deletion. A v2 state publication is bound into the same journal: recovery rolls every pre-commit phase back to
|
|
321
|
-
* the recorded prior roots and manifest, while a durable committed phase can only finish the next pair's cleanup.
|
|
322
|
-
* No launch using this coordinator observes a successfully returned mixed set. Activated roots require both their
|
|
323
|
-
* nonce marker and their recorded filesystem identity before rollback cleanup.
|
|
324
|
-
*/
|
|
325
|
-
export const switchProjectionSet = async (agentRoot, targets, nonce, fault, syncDirectory = durableDirectorySync, publication) => {
|
|
326
|
-
const manifestOperations = {
|
|
327
|
-
publish: publication?.operations?.publish ?? publishPreparedProjectSkillManifestUpdate,
|
|
328
|
-
commit: publication?.operations?.commit ?? commitPreparedProjectSkillManifestUpdate,
|
|
329
|
-
rollback: publication?.operations?.rollback ?? rollbackPreparedProjectSkillManifestUpdate,
|
|
330
|
-
};
|
|
331
|
-
const journalTargets = [];
|
|
332
|
-
try {
|
|
333
|
-
if (!NONCE.test(nonce) || targets.length !== 2)
|
|
334
|
-
throw new Error("projection_switch_journal_invalid");
|
|
335
|
-
const expected = projectionTargets(agentRoot, nonce);
|
|
336
|
-
if (targets.some((target, index) => JSON.stringify(target) !== JSON.stringify(expected[index]))) {
|
|
337
|
-
throw new Error("projection_switch_journal_invalid");
|
|
338
|
-
}
|
|
339
|
-
for (const target of targets) {
|
|
340
|
-
await assertOwnedRoot(target.staging, target, nonce);
|
|
341
|
-
if (await exists(target.backup) || await exists(target.discard)) {
|
|
342
|
-
throw new Error("projection_switch_path_unmanaged");
|
|
343
|
-
}
|
|
344
|
-
const previousIdentity = await directoryIdentity(target.target);
|
|
345
|
-
const activatedIdentity = await directoryIdentity(target.staging);
|
|
346
|
-
if (activatedIdentity === null)
|
|
347
|
-
throw new Error("projection_switch_path_unmanaged");
|
|
348
|
-
journalTargets.push(Object.freeze({
|
|
349
|
-
...target,
|
|
350
|
-
hadPrevious: previousIdentity !== null,
|
|
351
|
-
previousIdentity,
|
|
352
|
-
activatedIdentity,
|
|
353
|
-
}));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
catch (error) {
|
|
357
|
-
if (publication !== undefined)
|
|
358
|
-
await manifestOperations.rollback(publication.update);
|
|
359
|
-
throw error;
|
|
360
|
-
}
|
|
361
|
-
let journal = Object.freeze({
|
|
362
|
-
managedBy: "nowcrew-project-skills-switch",
|
|
363
|
-
version: 1,
|
|
364
|
-
nonce,
|
|
365
|
-
phase: "prepared",
|
|
366
|
-
targets: Object.freeze(journalTargets),
|
|
367
|
-
...(publication === undefined
|
|
368
|
-
? {}
|
|
369
|
-
: { manifestUpdate: projectSkillManifestUpdateRecord(publication.update) }),
|
|
370
|
-
});
|
|
371
|
-
try {
|
|
372
|
-
await writeJournal(agentRoot, journal);
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
if (publication !== undefined)
|
|
376
|
-
await manifestOperations.rollback(publication.update);
|
|
377
|
-
throw error;
|
|
378
|
-
}
|
|
379
|
-
let committedDurable = false;
|
|
380
|
-
try {
|
|
381
|
-
await fault?.(journal.phase);
|
|
382
|
-
for (const target of journal.targets) {
|
|
383
|
-
if (target.hadPrevious) {
|
|
384
|
-
await rename(target.target, target.backup);
|
|
385
|
-
await syncDirectory(dirname(target.target));
|
|
386
|
-
await assertPrevious(target.backup, target.previousIdentity);
|
|
387
|
-
}
|
|
388
|
-
journal = withPhase(journal, `${target.runtime}:backup_moved`);
|
|
389
|
-
await writeJournal(agentRoot, journal);
|
|
390
|
-
await fault?.(journal.phase);
|
|
391
|
-
await rename(target.staging, target.target);
|
|
392
|
-
await syncDirectory(dirname(target.target));
|
|
393
|
-
await assertActivated(target.target, target, nonce);
|
|
394
|
-
journal = withPhase(journal, `${target.runtime}:activated`);
|
|
395
|
-
await writeJournal(agentRoot, journal);
|
|
396
|
-
await fault?.(journal.phase);
|
|
397
|
-
}
|
|
398
|
-
if (publication !== undefined) {
|
|
399
|
-
await manifestOperations.publish(publication.update);
|
|
400
|
-
await publication.verify();
|
|
401
|
-
await publication.afterPublishedBeforeCommit?.();
|
|
402
|
-
}
|
|
403
|
-
const committedJournal = withPhase(journal, "committed");
|
|
404
|
-
await writeJournal(agentRoot, committedJournal);
|
|
405
|
-
journal = committedJournal;
|
|
406
|
-
committedDurable = true;
|
|
407
|
-
await fault?.(journal.phase);
|
|
408
|
-
await finalizeCommitted(agentRoot, journal, syncDirectory, manifestOperations, publication?.unlinkJournal);
|
|
409
|
-
}
|
|
410
|
-
catch (error) {
|
|
411
|
-
if (committedDurable)
|
|
412
|
-
throw error;
|
|
413
|
-
const persisted = await readJournal(agentRoot);
|
|
414
|
-
if (persisted?.phase === "committed")
|
|
415
|
-
throw error;
|
|
416
|
-
await rollback(agentRoot, persisted ?? journal, syncDirectory, manifestOperations);
|
|
417
|
-
throw error;
|
|
418
|
-
}
|
|
419
|
-
};
|