@nowcrew/daemon 0.6.102 → 0.6.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/codex-home-launch.js +7 -0
  2. package/dist/codex-home-mode-controller.js +68 -0
  3. package/dist/daemon-migration-controller.js +210 -0
  4. package/dist/daemon-migration-wiring.js +22 -0
  5. package/dist/execution-runner.js +5 -2
  6. package/dist/local-executor.js +8 -1
  7. package/dist/project-skills/capability.js +109 -0
  8. package/dist/project-skills/controller-convergence.js +57 -0
  9. package/dist/project-skills/projection-set-switch.js +419 -0
  10. package/dist/project-skills/projection-state-domain.js +170 -0
  11. package/dist/project-skills/projection-state-store.js +842 -0
  12. package/dist/project-skills/projection-state-transaction.js +322 -0
  13. package/dist/project-skills/projection-state.js +3 -0
  14. package/dist/project-skills/runtime-root-bootstrap.js +47 -0
  15. package/dist/project-skills/runtime-root-domain.js +268 -0
  16. package/dist/project-skills/runtime-root-gc.js +293 -0
  17. package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
  18. package/dist/project-skills/runtime-root-leases.js +487 -0
  19. package/dist/project-skills/runtime-root-source-identity.js +60 -0
  20. package/dist/project-skills/runtime-root-startup.js +49 -0
  21. package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
  22. package/dist/project-skills/runtime-root-state-index.js +356 -0
  23. package/dist/project-skills/runtime-root-store.js +722 -0
  24. package/dist/project-skills/serve-capability.js +28 -0
  25. package/dist/project-skills/serve-startup.js +37 -0
  26. package/dist/runtime-observation.js +219 -0
  27. package/dist/runtimes/claude-observation-runner.js +55 -0
  28. package/dist/runtimes/claude-thread-runner.js +17 -3
  29. package/dist/runtimes/claude.js +5 -3
  30. package/dist/runtimes/codex-app-server-runner.js +16 -2
  31. package/dist/runtimes/codex-home-migration-cli.js +26 -0
  32. package/dist/runtimes/codex-home-migration.js +112 -0
  33. package/dist/runtimes/codex-home.js +236 -0
  34. package/dist/runtimes/deepseek-harness-runner.js +16 -0
  35. package/dist/runtimes/kimi-acp-runner.js +19 -0
  36. package/dist/runtimes/observation-context.js +18 -0
  37. package/dist/runtimes/observation-mapper.js +314 -0
  38. package/dist/runtimes/opencode-runner.js +18 -4
  39. package/dist/runtimes/pi-runner.js +17 -1
  40. package/dist/storage-control.js +212 -0
  41. package/dist/storage-copy.js +628 -0
  42. package/dist/storage-credential-guard.js +21 -0
  43. package/dist/storage-doctor.js +228 -0
  44. package/dist/storage-manifest.js +1027 -0
  45. package/dist/storage-migration-controller.js +441 -0
  46. package/dist/storage-migration-manifest.js +105 -0
  47. package/dist/storage-migration-policy.js +100 -0
  48. package/dist/storage-runtime-controller.js +361 -0
  49. package/dist/storage-runtime-readiness.js +206 -0
  50. package/dist/storage-safe-fs.js +302 -0
  51. package/dist/storage-status-delivery.js +224 -0
  52. package/dist/storage-status-replay.js +140 -0
  53. package/dist/storage-target-confirmation.js +129 -0
  54. package/dist/storage-worker-command.js +37 -0
  55. package/dist/storage-worker-executor.js +431 -0
  56. package/dist/storage-worker-handoff.js +151 -0
  57. package/dist/storage-worker-operation.js +288 -0
  58. package/dist/storage-worker-status.js +259 -0
  59. package/dist/token.js +4 -1
  60. package/package.json +1 -1
@@ -0,0 +1,7 @@
1
+ import { awaitWithCancellation } from "./runtime-cancellation.js";
2
+ import { materializeDefaultCodexHome } from "./runtimes/codex-home.js";
3
+ export async function materializePrivateCodexHome(input) {
4
+ if (input.runtime !== "codex" || input.mode !== "private")
5
+ return;
6
+ await awaitWithCancellation((input.materialize ?? materializeDefaultCodexHome)(input.homeDir, input.sourceHome, input.sessionId), input.cancellation);
7
+ }
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ import { daemonHome, loadProfile, saveProfile } from "./computer-profile.js";
3
+ const MessageSchema = z.object({
4
+ type: z.literal("daemon:codex-home-mode"),
5
+ operationId: z.string().uuid(),
6
+ mode: z.enum(["shared", "private"]),
7
+ }).strict();
8
+ export function createCodexHomeModeController(input) {
9
+ let running = null;
10
+ const handled = new Set();
11
+ const fail = (operationId, errorCode) => {
12
+ input.sendStatus({ type: "daemon:codex-home-mode-status", operationId, status: "failed", errorCode });
13
+ };
14
+ const execute = async (message) => {
15
+ if (input.profileName === undefined) {
16
+ fail(message.operationId, "profile_unavailable");
17
+ return;
18
+ }
19
+ if (input.isBusy()) {
20
+ fail(message.operationId, "busy");
21
+ return;
22
+ }
23
+ input.sendStatus({ type: "daemon:codex-home-mode-status", operationId: message.operationId, status: "saving" });
24
+ try {
25
+ const home = input.profileHome ?? daemonHome();
26
+ const profile = await loadProfile(input.profileName, home);
27
+ await saveProfile({ ...profile, codexHomeMode: message.mode }, home);
28
+ }
29
+ catch {
30
+ fail(message.operationId, "save_failed");
31
+ return;
32
+ }
33
+ input.sendStatus({ type: "daemon:codex-home-mode-status", operationId: message.operationId, status: "restarting" });
34
+ try {
35
+ const serviceSpec = await input.getServiceSpec();
36
+ if (serviceSpec === null) {
37
+ fail(message.operationId, "restart_failed");
38
+ return;
39
+ }
40
+ const handoff = input.scheduleRestart(serviceSpec);
41
+ void handoff.failed.then(() => fail(message.operationId, "restart_failed"), () => fail(message.operationId, "restart_failed"));
42
+ }
43
+ catch {
44
+ fail(message.operationId, "restart_failed");
45
+ }
46
+ };
47
+ return {
48
+ handle: async (raw) => {
49
+ const parsed = MessageSchema.safeParse(raw);
50
+ if (!parsed.success)
51
+ return false;
52
+ if (handled.has(parsed.data.operationId))
53
+ return true;
54
+ if (running !== null) {
55
+ fail(parsed.data.operationId, "busy");
56
+ handled.add(parsed.data.operationId);
57
+ return true;
58
+ }
59
+ running = execute(parsed.data).finally(() => {
60
+ handled.add(parsed.data.operationId);
61
+ running = null;
62
+ });
63
+ await running;
64
+ return true;
65
+ },
66
+ drain: async () => { await running; },
67
+ };
68
+ }
@@ -0,0 +1,210 @@
1
+ import { createHash } from "node:crypto";
2
+ import { cp, lstat, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, relative, resolve } from "node:path";
5
+ import { z } from "zod";
6
+ import { loadProfile } from "./computer-profile.js";
7
+ import { builtDaemonEntry } from "./computer-cli.js";
8
+ import { installDaemonAutomatically } from "./automatic-daemon-installer.js";
9
+ import { buildServiceSpec, captureExactService, serviceStatus, systemCommandRunner } from "./computer-service.js";
10
+ import { readManagedServiceRegistry } from "./managed-service-registry.js";
11
+ import { uninstallManagedService } from "./managed-service-lifecycle.js";
12
+ import { resolveAutomaticInstallTarget } from "./automatic-install-target.js";
13
+ import { buildStorageMigrationMappings, createSafeServiceManifest, CredentialMaterialError, } from "./storage-migration-manifest.js";
14
+ const MigrationMessageSchema = z.object({
15
+ type: z.literal("daemon:migrate"),
16
+ migrationId: z.string().uuid(),
17
+ targetProfile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/),
18
+ targetDaemonHome: z.string().min(1),
19
+ targetAgentsRoot: z.string().min(1),
20
+ }).strict();
21
+ function migrationBackupRoot(input, id) {
22
+ return resolve(input.backupRoot ?? resolve(input.userHome ?? homedir(), ".crew", "migration-backups"), id);
23
+ }
24
+ function legacyLocalHome(input) {
25
+ return resolve(input.userHome ?? homedir(), ".crew");
26
+ }
27
+ async function assertEmptyOrMissing(path) {
28
+ try {
29
+ const entries = await readdir(path);
30
+ if (entries.length > 0)
31
+ throw new Error(`target path is not empty: ${path}`);
32
+ }
33
+ catch (error) {
34
+ if (error.code !== "ENOENT")
35
+ throw error;
36
+ }
37
+ }
38
+ async function hashedFiles(root, directory = root) {
39
+ const entries = await readdir(directory, { withFileTypes: true });
40
+ const files = [];
41
+ for (const entry of entries) {
42
+ const path = resolve(directory, entry.name);
43
+ const stat = await lstat(path);
44
+ if (stat.isSymbolicLink())
45
+ throw new Error(`backup source contains a symbolic link: ${relative(root, path)}`);
46
+ if (stat.isDirectory())
47
+ files.push(...await hashedFiles(root, path));
48
+ else if (stat.isFile())
49
+ files.push({ path: relative(root, path), sha256: createHash("sha256").update(await readFile(path)).digest("hex") });
50
+ }
51
+ return files;
52
+ }
53
+ async function backupAndCopy(input, id, targetHome, targetAgents, service) {
54
+ // Validate and project service identity before creating any backup bytes. A descriptor containing
55
+ // a token must stop the migration before it can be copied into the local backup root.
56
+ const safeService = createSafeServiceManifest({
57
+ spec: service.spec,
58
+ snapshot: service.snapshot,
59
+ managedRecord: service.managedRecord,
60
+ });
61
+ const backup = migrationBackupRoot(input, id);
62
+ await mkdir(backup, { recursive: true, mode: 0o700 });
63
+ await cp(input.currentDaemonHome, resolve(backup, "daemon-home"), { recursive: true, force: false });
64
+ await cp(input.currentAgentsRoot, resolve(backup, "agents-root"), { recursive: true, force: false });
65
+ await cp(input.currentDaemonHome, targetHome, { recursive: true, force: false });
66
+ await cp(input.currentAgentsRoot, targetAgents, { recursive: true, force: false });
67
+ const daemonFiles = await hashedFiles(resolve(backup, "daemon-home"));
68
+ const agentFiles = await hashedFiles(resolve(backup, "agents-root"));
69
+ const mappings = buildStorageMigrationMappings({ daemonFiles, agentFiles });
70
+ await writeFile(resolve(backup, "manifest.json"), `${JSON.stringify({
71
+ version: 2,
72
+ migrationId: id,
73
+ profile: input.profileName,
74
+ daemonHome: input.currentDaemonHome,
75
+ agentsRoot: input.currentAgentsRoot,
76
+ service: safeService,
77
+ mappings,
78
+ }, null, 2)}\n`, { mode: 0o600 });
79
+ return backup;
80
+ }
81
+ async function cleanupCurrentServiceDefault(spec, input) {
82
+ const userHome = input.userHome ?? homedir();
83
+ const records = await readManagedServiceRegistry({ userHome, localHome: legacyLocalHome(input) });
84
+ const record = records.find((candidate) => candidate.profile === spec.profile && candidate.daemonHome === input.currentDaemonHome);
85
+ if (record !== undefined && (spec.platform === "darwin" || spec.platform === "linux")) {
86
+ await uninstallManagedService({ spec, record, runner: systemCommandRunner, registry: { userHome, localHome: legacyLocalHome(input) } });
87
+ return;
88
+ }
89
+ // Windows and legacy descriptors are handled by the normal CLI/service lifecycle.
90
+ const { uninstallService } = await import("./computer-service.js");
91
+ const status = await serviceStatus(spec, systemCommandRunner);
92
+ if (status.installed)
93
+ await uninstallService(spec, systemCommandRunner);
94
+ }
95
+ export function createDaemonMigrationController(input) {
96
+ const handled = new Set();
97
+ let running = null;
98
+ const sendFailure = (id, errorCode, error) => {
99
+ input.sendStatus({ type: "daemon:migration-status", migrationId: id, status: "failed", errorCode, errorMessage: error instanceof Error ? error.message : String(error) });
100
+ };
101
+ const execute = async (raw) => {
102
+ const parsed = MigrationMessageSchema.safeParse(raw);
103
+ if (!parsed.success)
104
+ return false;
105
+ const message = parsed.data;
106
+ if (handled.has(message.migrationId))
107
+ return true;
108
+ if (running !== null) {
109
+ sendFailure(message.migrationId, "target_conflict", new Error("another migration is already running"));
110
+ return true;
111
+ }
112
+ running = (async () => {
113
+ input.sendStatus({ type: "daemon:migration-status", migrationId: message.migrationId, status: "preflight" });
114
+ if (input.isBusy()) {
115
+ sendFailure(message.migrationId, "runtime_busy", new Error("active executions must finish before migration"));
116
+ return;
117
+ }
118
+ const platform = input.platform ?? process.platform;
119
+ const userHome = input.userHome ?? homedir();
120
+ let targetPath;
121
+ let targetAgents;
122
+ let currentSpec;
123
+ let currentService;
124
+ let managedRecord = null;
125
+ let currentRegistryUrl;
126
+ try {
127
+ const currentProfile = await (input.loadCurrentProfile ?? loadProfile)(input.profileName, input.currentDaemonHome);
128
+ if (currentProfile.machineToken !== input.machineToken)
129
+ throw new Error("active profile token does not match this daemon");
130
+ currentRegistryUrl = currentProfile.registryUrl;
131
+ const target = resolveAutomaticInstallTarget({
132
+ serverUrl: input.serverUrl,
133
+ requestedProfile: message.targetProfile,
134
+ userHome,
135
+ platform,
136
+ // This controller migrates the legacy service layout; the new local-home
137
+ // standard is handled by the storage migration controller.
138
+ requestedLocalHomeMode: "legacy",
139
+ });
140
+ if (!isAbsolute(message.targetDaemonHome) || !isAbsolute(message.targetAgentsRoot)
141
+ || resolve(message.targetDaemonHome) !== resolve(target.daemonHome)
142
+ || resolve(message.targetAgentsRoot) !== resolve(target.agentsRoot))
143
+ throw new Error("requested target does not match the standard profile layout");
144
+ targetPath = resolve(target.daemonHome);
145
+ targetAgents = resolve(target.agentsRoot);
146
+ await assertEmptyOrMissing(targetPath);
147
+ await assertEmptyOrMissing(targetAgents);
148
+ currentSpec = buildServiceSpec({ platform, profile: input.profileName, userHome, uid: process.getuid?.(), nodePath: process.execPath, entryPath: builtDaemonEntry(), profileHome: input.currentDaemonHome });
149
+ const exact = await (input.captureCurrentService ?? ((spec) => captureExactService(spec, systemCommandRunner)))(currentSpec);
150
+ if (!exact.installed || !exact.running || !exact.exact)
151
+ throw new Error("current managed service identity is not exact and running");
152
+ currentService = { installed: exact.installed, running: exact.running };
153
+ if (platform === "darwin" || platform === "linux") {
154
+ const records = await (input.readManagedRecords ?? (() => readManagedServiceRegistry({ userHome, localHome: legacyLocalHome(input) })))();
155
+ managedRecord = records.find((record) => record.serviceId === currentSpec.id && record.profile === input.profileName && record.daemonHome === input.currentDaemonHome) ?? null;
156
+ if (managedRecord === null)
157
+ throw new Error("current managed service registry record is missing");
158
+ }
159
+ }
160
+ catch (error) {
161
+ sendFailure(message.migrationId, error instanceof CredentialMaterialError ? "credential_material" : "target_conflict", error);
162
+ return;
163
+ }
164
+ input.sendStatus({ type: "daemon:migration-status", migrationId: message.migrationId, status: "backing_up" });
165
+ let backupPath;
166
+ try {
167
+ backupPath = await backupAndCopy(input, message.migrationId, targetPath, targetAgents, { spec: currentSpec, snapshot: currentService, managedRecord });
168
+ input.sendStatus({ type: "daemon:migration-status", migrationId: message.migrationId, status: "installing", backupPath });
169
+ }
170
+ catch (error) {
171
+ await Promise.allSettled([rm(targetPath, { recursive: true, force: true }), rm(targetAgents, { recursive: true, force: true })]);
172
+ sendFailure(message.migrationId, error instanceof CredentialMaterialError ? "credential_material" : "backup_failed", error);
173
+ return;
174
+ }
175
+ const install = input.install ?? installDaemonAutomatically;
176
+ try {
177
+ const result = await install({
178
+ serverUrl: input.serverUrl,
179
+ machineToken: input.machineToken,
180
+ requestedProfile: message.targetProfile,
181
+ ...(currentRegistryUrl === undefined ? {} : { requestedRegistryUrl: currentRegistryUrl }),
182
+ });
183
+ if (result.profile !== message.targetProfile || result.serviceId.length === 0) {
184
+ throw new Error("target service identity did not match the requested profile");
185
+ }
186
+ }
187
+ catch (error) {
188
+ await Promise.allSettled([rm(targetPath, { recursive: true, force: true }), rm(targetAgents, { recursive: true, force: true })]);
189
+ sendFailure(message.migrationId, "install_failed", error);
190
+ return;
191
+ }
192
+ input.sendStatus({ type: "daemon:migration-status", migrationId: message.migrationId, status: "restarting" });
193
+ try {
194
+ await (input.cleanupCurrentService ?? ((spec) => cleanupCurrentServiceDefault(spec, input)))(currentSpec);
195
+ }
196
+ catch (error) {
197
+ const targetSpec = buildServiceSpec({ platform, profile: message.targetProfile, userHome, uid: process.getuid?.(), nodePath: process.execPath, entryPath: builtDaemonEntry(), profileHome: targetPath });
198
+ await (input.cleanupTargetService ?? ((spec) => cleanupCurrentServiceDefault(spec, { ...input, profileName: message.targetProfile, currentDaemonHome: targetPath })))(targetSpec).catch(() => undefined);
199
+ sendFailure(message.migrationId, "service_cleanup_failed", error);
200
+ return;
201
+ }
202
+ })().finally(() => {
203
+ handled.add(message.migrationId);
204
+ running = null;
205
+ });
206
+ await running;
207
+ return true;
208
+ };
209
+ return { handle: execute, drain: async () => { await running; } };
210
+ }
@@ -0,0 +1,22 @@
1
+ import { daemonHome } from "./computer-profile.js";
2
+ import { createDaemonMigrationController } from "./daemon-migration-controller.js";
3
+ export function createServeMigrationController(input) {
4
+ if (input.profileName === undefined)
5
+ return null;
6
+ return createDaemonMigrationController({
7
+ serverUrl: input.config.serverUrl,
8
+ machineToken: input.config.machineToken,
9
+ profileName: input.profileName,
10
+ currentDaemonHome: daemonHome(),
11
+ currentAgentsRoot: input.config.agentsRoot,
12
+ isBusy: input.isBusy,
13
+ sendStatus: input.sendStatus,
14
+ });
15
+ }
16
+ export async function handleMigrationControlMessage(controller, decoded) {
17
+ if (controller === null || typeof decoded !== "object" || decoded === null
18
+ || !("type" in decoded) || decoded.type !== "daemon:migrate")
19
+ return false;
20
+ await controller.handle(decoded);
21
+ return true;
22
+ }
@@ -709,9 +709,11 @@ export async function runExecution(config, input, dependencies) {
709
709
  catch { /* best-effort activity omitted when its envelope cannot fit */ }
710
710
  },
711
711
  } : {}),
712
- ...(spec.reporting.streamConsole ? {
712
+ ...(spec.reporting.streamConsole || credential.observability ? {
713
713
  onConsole: (chunk) => {
714
- if (firstOutputTimer !== undefined) {
714
+ if (!spec.reporting.streamConsole && chunk.payload?.kind !== "agent_observation")
715
+ return;
716
+ if (chunk.payload?.kind !== "agent_observation" && firstOutputTimer !== undefined) {
715
717
  clearTimeout(firstOutputTimer);
716
718
  firstOutputTimer = undefined;
717
719
  }
@@ -859,6 +861,7 @@ export async function runExecution(config, input, dependencies) {
859
861
  return appendAgentMemoryContext(base, recalledMemory, Math.max(Buffer.byteLength(base, "utf8"), baseSystemPromptBudget - (context.reservedPromptBytes ?? 0)));
860
862
  };
861
863
  const localInput = {
864
+ ...(credential.observability ? { observability: credential.observability } : {}),
862
865
  executionId: spec.executionId,
863
866
  handle: spec.agent.handle,
864
867
  channelId: spec.context.channelId,
@@ -46,6 +46,7 @@ import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace,
46
46
  import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
47
47
  import { reportLocalMemoryV2Deferred, resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
48
48
  import { localMemoryV2Environment } from "./config.js";
49
+ import { observationEnvironment, safeRuntimeObservation } from "./runtime-observation.js";
49
50
  export class RuntimeRunningCallbackTimeoutError extends Error {
50
51
  constructor(runtime, timeoutMs) {
51
52
  super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
@@ -592,7 +593,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
592
593
  systemPromptPath: workspace.systemPromptPath,
593
594
  systemPrompt: effectiveSystemPrompt,
594
595
  wakePrompt,
595
- env: runtimeEnv,
596
+ env: observationEnvironment(runtimeEnv, input.observability),
596
597
  effectivePermission: input.effectivePermission,
597
598
  ...(launchModel === undefined ? {} : { model: launchModel }),
598
599
  ...(launchReasoning === undefined ? {} : { reasoning: launchReasoning }),
@@ -787,6 +788,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
787
788
  const event = parseLine(line);
788
789
  if (!event)
789
790
  return;
791
+ if (typeof event === "object" && "type" in event && event.type === "nowwork.observation") {
792
+ const payload = safeRuntimeObservation(event.observation, input.observability);
793
+ if (payload)
794
+ callbacks.onConsole?.({ stream: "system", text: "", payload });
795
+ return;
796
+ }
790
797
  nativeThreadContext?.observe(event);
791
798
  localMemoryTelemetry?.observe(event);
792
799
  if (isRuntimeReadyEvent(runtime.name, event))
@@ -0,0 +1,109 @@
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
+ }
@@ -0,0 +1,57 @@
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
+ }