@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 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.7",
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.13",
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
- };