@nowcrew/daemon 0.6.18 → 0.6.20

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 (37) hide show
  1. package/dist/atomic-no-replace-rename.js +91 -0
  2. package/dist/control-plane-url.js +4 -2
  3. package/dist/directory-projection-publication.js +105 -0
  4. package/dist/directory-projection.js +20 -4
  5. package/dist/execution-journal.js +40 -4
  6. package/dist/execution-posix-stop-proof.js +82 -0
  7. package/dist/execution-runner.js +68 -8
  8. package/dist/local-executor.js +73 -52
  9. package/dist/machine-info.js +8 -5
  10. package/dist/project-skills/capability.js +109 -0
  11. package/dist/project-skills/controller-convergence.js +57 -0
  12. package/dist/project-skills/controller.js +80 -24
  13. package/dist/project-skills/initialized-reconciler.js +4 -4
  14. package/dist/project-skills/projection-state-domain.js +19 -2
  15. package/dist/project-skills/projection-state-store.js +3 -2
  16. package/dist/project-skills/projection-state-transaction.js +5 -1
  17. package/dist/project-skills/projection-state.js +1 -1
  18. package/dist/project-skills/reconciler.js +275 -102
  19. package/dist/project-skills/runtime-launch.js +102 -0
  20. package/dist/project-skills/runtime-root-bootstrap.js +47 -0
  21. package/dist/project-skills/runtime-root-domain.js +268 -0
  22. package/dist/project-skills/runtime-root-gc.js +293 -0
  23. package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
  24. package/dist/project-skills/runtime-root-leases.js +487 -0
  25. package/dist/project-skills/runtime-root-source-identity.js +60 -0
  26. package/dist/project-skills/runtime-root-startup.js +49 -0
  27. package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
  28. package/dist/project-skills/runtime-root-state-index.js +356 -0
  29. package/dist/project-skills/runtime-root-store.js +722 -0
  30. package/dist/project-skills/serve-capability.js +28 -0
  31. package/dist/project-skills/serve-startup.js +22 -0
  32. package/dist/project-skills/types.js +1 -0
  33. package/dist/provider-env.js +3 -0
  34. package/dist/runtimes/codex-home.js +50 -0
  35. package/dist/serve.js +58 -73
  36. package/dist/supervised-runtime.js +1 -5
  37. package/package.json +2 -2
@@ -2,9 +2,10 @@ import { createInterface } from "node:readline";
2
2
  import { readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { delimiter, join } from "node:path";
4
4
  import { prepareWorkspace, rotateAgentSession, safeKey, } from "./workspace.js";
5
- import { CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV, isClaudeAdditionalDirectoryInstructionsSupported, probeClaudeVersion, resolveOrderedUniqueClaudeDirectories, spawnClaude, } from "./runtimes/claude.js";
5
+ import { CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV, isClaudeAdditionalDirectoryInstructionsSupported, probeClaudeVersion, spawnClaude, } from "./runtimes/claude.js";
6
6
  import { spawnCodex } from "./runtimes/codex.js";
7
7
  import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
8
+ import { materializeDefaultCodexHome } from "./runtimes/codex-home.js";
8
9
  import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
9
10
  import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
10
11
  import { augmentedPath } from "./runtime-path.js";
@@ -20,7 +21,9 @@ import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancell
20
21
  import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
21
22
  import { dslog } from "./slog.js";
22
23
  import { boundedDiagnosticJsonArray } from "./diagnostic-json.js";
24
+ import { ProjectProjectionError, } from "./project-skills/reconciler.js";
23
25
  import { formatProjectSkillRuntimeWarning } from "./project-skills/runtime-warning.js";
26
+ import { projectSkillRuntimeDirectories, redactProjectSkillRuntimeRootText, redactProjectSkillRuntimeRootValue, } from "./project-skills/runtime-launch.js";
24
27
  import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
25
28
  import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
26
29
  import { CodexStartupStageParser } from "./codex-startup-stage.js";
@@ -194,6 +197,19 @@ async function withKeyedLease(key, operation, cancellation) {
194
197
  }
195
198
  }
196
199
  export async function executeLocal(input, callbacks = {}, dependencies = {}) {
200
+ const versionedProjectSkills = input.projectSkills !== undefined
201
+ && !Array.isArray(input.projectSkills);
202
+ if (versionedProjectSkills && (input.runtime.name !== "codex" && input.runtime.name !== "claude")) {
203
+ throw new ProjectProjectionError("skill_projection_failed");
204
+ }
205
+ if (versionedProjectSkills && dependencies.projectSkills === undefined) {
206
+ throw new ProjectProjectionError("skill_projection_failed");
207
+ }
208
+ if (input.projectSkills !== undefined
209
+ && dependencies.projectSkills !== undefined
210
+ && dependencies.projectSkillLeaseSafeLaunchOwner === undefined) {
211
+ throw new ProjectProjectionError("skill_projection_failed");
212
+ }
197
213
  const stableProtocolRuntime = dependencies.launchRuntime !== undefined;
198
214
  const supportsNativeResume = input.runtime.name === "claude"
199
215
  || (stableProtocolRuntime && runtimeCapability(input.runtime.name).nativeResume);
@@ -257,6 +273,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
257
273
  let memoryPruneSharedWriteKey = null;
258
274
  let executionWorkspace = workspace;
259
275
  let localMemoryTelemetry = null;
276
+ const inheritedEnv = { ...process.env };
277
+ for (const key of Object.keys(inheritedEnv)) {
278
+ if (key.startsWith("CREW_AGENT_MEMORY_"))
279
+ delete inheritedEnv[key];
280
+ }
260
281
  try {
261
282
  if (isDeepSeekCodex && !providerConfig.providerApiKey) {
262
283
  throw new Error("DeepSeek API key is not configured for this Agent");
@@ -264,6 +285,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
264
285
  if (isDeepSeekCodex) {
265
286
  await awaitWithCancellation((dependencies.materializeDeepSeekCodexHome ?? materializeDeepSeekCodexHome)(workspace.homeDir), dependencies.cancellation);
266
287
  }
288
+ else if (runtime.name === "codex") {
289
+ const sourceCodexHome = inheritedEnv.CODEX_HOME
290
+ ?? (inheritedEnv.HOME === undefined ? undefined : join(inheritedEnv.HOME, ".codex"));
291
+ await awaitWithCancellation((dependencies.materializeDefaultCodexHome ?? materializeDefaultCodexHome)(workspace.homeDir, sourceCodexHome), dependencies.cancellation);
292
+ }
267
293
  const supportsNativeResume = runtime.name === "claude"
268
294
  || (dependencies.launchRuntime !== undefined && runtimeCapability(runtime.name).nativeResume);
269
295
  const storedCurrentPrior = input.session.enabled && supportsNativeResume
@@ -422,11 +448,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
422
448
  }
423
449
  }
424
450
  memoryPruneFailurePhase = "runtime_prepare";
425
- const inheritedEnv = { ...process.env };
426
- for (const key of Object.keys(inheritedEnv)) {
427
- if (key.startsWith("CREW_AGENT_MEMORY_"))
428
- delete inheritedEnv[key];
429
- }
430
451
  const baseEnv = {
431
452
  ...inheritedEnv,
432
453
  ...sanitizeEnvVars(providerConfig.envVars),
@@ -479,7 +500,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
479
500
  }
480
501
  const runtimeLaunchAt = Date.now();
481
502
  memoryPruneFailurePhase = "runtime_launch";
482
- const launchPreparedRuntime = async (abilityContext) => {
503
+ let activeProjectSkillRuntimeRoot;
504
+ const launchPreparedRuntime = async (runtimeRoot, abilityContext) => {
505
+ activeProjectSkillRuntimeRoot = runtimeRoot;
483
506
  const effectiveSystemPrompt = abilityContext === undefined
484
507
  ? systemPrompt
485
508
  : `${systemPrompt}\n\n${abilityContext.prompt}`;
@@ -515,31 +538,16 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
515
538
  skill_count: abilityContext.skillCount,
516
539
  });
517
540
  }
518
- const codexSkillRoots = runtime.name === "codex" && input.projectContext !== undefined
519
- ? [
520
- ...(input.projectSkills === undefined
521
- ? []
522
- : [join(workspace.dir, ".agents", "skills")]),
523
- ...(abilityContext?.skillRoot === undefined ? [] : [abilityContext.skillRoot]),
524
- ]
525
- : [];
526
- const claudeAdditionalDirectories = runtime.name === "claude"
527
- ? input.projectContext === undefined
528
- ? abilityContext?.skillRoot === undefined
529
- ? []
530
- : await resolveOrderedUniqueClaudeDirectories([
531
- join(workspace.dir, ".crew", "claude-skills"),
532
- workspace.dir,
533
- abilityContext.skillRoot,
534
- ])
535
- : await resolveOrderedUniqueClaudeDirectories([
536
- ...input.projectContext.secondary.map((project) => project.root),
537
- ...(input.projectSkills === undefined
538
- ? []
539
- : [join(workspace.dir, ".crew", "claude-skills")]),
540
- ...(abilityContext?.skillRoot === undefined ? [] : [abilityContext.skillRoot]),
541
- ], input.projectContext.primary === undefined ? [] : [input.projectContext.primary.root])
542
- : [];
541
+ const { codexSkillRoots, claudeAdditionalDirectories } = await projectSkillRuntimeDirectories({
542
+ runtime: runtime.name,
543
+ ...(runtimeRoot === undefined ? {} : { runtimeRoot }),
544
+ agentRoot: workspace.dir,
545
+ projectSkillsPresent: input.projectSkills !== undefined,
546
+ ...(input.projectContext === undefined ? {} : { projectContext: input.projectContext }),
547
+ ...(abilityContext?.skillRoot === undefined
548
+ ? {}
549
+ : { abilitySkillRoot: abilityContext.skillRoot }),
550
+ });
543
551
  let runtimeEnv = childEnv;
544
552
  if (runtime.name === "claude"
545
553
  && input.projectContext !== undefined
@@ -565,7 +573,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
565
573
  runtime: runtime.name,
566
574
  bin: runtime.name === "deepseek-harness" ? "dsh-acp-demo" : runtime.name,
567
575
  cwd: input.projectContext?.primary?.root ?? executionWorkspace.runDir,
568
- ...(input.projectSkills === undefined && input.abilityRelease === undefined ? {} : { agentRoot: workspace.dir }),
576
+ ...(runtimeRoot !== undefined
577
+ || (input.projectSkills === undefined && input.abilityRelease === undefined)
578
+ ? {}
579
+ : { agentRoot: workspace.dir }),
569
580
  systemPromptPath: workspace.systemPromptPath,
570
581
  systemPrompt: effectiveSystemPrompt,
571
582
  wakePrompt,
@@ -592,17 +603,22 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
592
603
  memoryPruneFailurePhase = "runtime_launch";
593
604
  return launchRuntime(launchRequest);
594
605
  };
595
- const launchWithAbility = () => input.abilityRelease !== undefined && dependencies.abilityRelease !== undefined
596
- ? dependencies.abilityRelease.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.abilityRelease, launchPreparedRuntime)
597
- : launchPreparedRuntime();
598
- const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
599
- ? await (Array.isArray(input.projectSkills)
600
- ? dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, launchWithAbility)
601
- : dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, launchWithAbility, (warning) => callbacks.onConsole?.({
602
- stream: "system",
603
- text: formatProjectSkillRuntimeWarning(warning),
604
- })))
605
- : await launchWithAbility();
606
+ const launchWithAbility = (runtimeRoot) => input.abilityRelease !== undefined && dependencies.abilityRelease !== undefined
607
+ ? dependencies.abilityRelease.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.abilityRelease, (abilityContext) => launchPreparedRuntime(runtimeRoot, abilityContext))
608
+ : launchPreparedRuntime(runtimeRoot);
609
+ let child;
610
+ if (input.projectSkills !== undefined && dependencies.projectSkills !== undefined) {
611
+ const launchOwner = dependencies.projectSkillLeaseSafeLaunchOwner;
612
+ if (launchOwner === undefined)
613
+ throw new ProjectProjectionError("skill_projection_failed");
614
+ child = await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.executionId, input.handle, input.projectSkills, launchOwner.claim(launchWithAbility), (warning) => callbacks.onConsole?.({
615
+ stream: "system",
616
+ text: formatProjectSkillRuntimeWarning(warning),
617
+ }));
618
+ }
619
+ else {
620
+ child = await launchWithAbility();
621
+ }
606
622
  localMemoryTelemetry?.markRuntimeStarted();
607
623
  memoryPruneFailurePhase = "runtime_execution";
608
624
  if (child.cancel !== undefined) {
@@ -654,7 +670,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
654
670
  usage = meta.usage;
655
671
  if (meta.model)
656
672
  observedModel = meta.model;
657
- for (const activity of normalizeEvent(event)) {
673
+ for (const rawActivity of normalizeEvent(event)) {
674
+ const activity = redactProjectSkillRuntimeRootValue(rawActivity, activeProjectSkillRuntimeRoot);
658
675
  if (activity.kind === "sending")
659
676
  sentViaCrew = true;
660
677
  activities.push(activity);
@@ -662,18 +679,20 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
662
679
  }
663
680
  const extracted = extractFinalText(event);
664
681
  if (extracted) {
682
+ const safeExtracted = redactProjectSkillRuntimeRootText(extracted, activeProjectSkillRuntimeRoot);
665
683
  const incremental = typeof event === "object" && event !== null
666
684
  && "type" in event && (event.type === "kimi.acp.text_delta"
667
685
  || event.type === "hermes.acp.text_delta"
668
686
  || event.type === "deepseek-harness.acp.text_delta"
669
687
  || event.type === "opencode.text_delta");
670
- finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
688
+ finalText = incremental ? `${finalText ?? ""}${safeExtracted}` : safeExtracted;
671
689
  }
672
690
  for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
673
- callbacks.onExternalOutput?.(text);
691
+ callbacks.onExternalOutput?.(redactProjectSkillRuntimeRootText(text, activeProjectSkillRuntimeRoot));
692
+ }
693
+ for (const chunk of consoleFormatter.format(event)) {
694
+ callbacks.onConsole?.(redactProjectSkillRuntimeRootValue(chunk, activeProjectSkillRuntimeRoot));
674
695
  }
675
- for (const chunk of consoleFormatter.format(event))
676
- callbacks.onConsole?.(chunk);
677
696
  });
678
697
  let stderrTail = "";
679
698
  const codexStartupStageState = { last: null };
@@ -694,8 +713,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
694
713
  })
695
714
  : null;
696
715
  child.stderr.on("data", (data) => {
697
- process.stderr.write(data);
698
- const text = String(data);
716
+ const text = redactProjectSkillRuntimeRootText(String(data), activeProjectSkillRuntimeRoot);
717
+ process.stderr.write(text);
699
718
  stderrTail = (stderrTail + text).slice(-STDERR_TAIL_CAP);
700
719
  codexStartupStageParser?.push(text);
701
720
  });
@@ -755,7 +774,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
755
774
  memoryPruneRuntimeExitCode = exitCode;
756
775
  const errorTail = [
757
776
  stderrTail.trim(),
758
- spawnError,
777
+ spawnError === undefined
778
+ ? undefined
779
+ : redactProjectSkillRuntimeRootText(spawnError, activeProjectSkillRuntimeRoot),
759
780
  terminationSignal ? `terminated by ${terminationSignal}` : undefined,
760
781
  ].filter(Boolean).join(" ").trim();
761
782
  const finish = exitActivity(runtime.name, exitCode, errorTail);
@@ -61,7 +61,9 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
61
61
  export const daemonCapabilityBindings = (runtimePlatform = process.platform, injectedCapabilities) => {
62
62
  const snapshot = injectedCapabilities === undefined
63
63
  ? daemonCapabilities(runtimePlatform)
64
- : Object.freeze([...injectedCapabilities]);
64
+ : (Object.isFrozen(injectedCapabilities)
65
+ ? injectedCapabilities
66
+ : Object.freeze([...injectedCapabilities]));
65
67
  return Object.freeze({
66
68
  controlPlaneUrl: snapshot,
67
69
  machineHello: snapshot,
@@ -169,6 +171,10 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
169
171
  const executionRuntimes = backend.supported
170
172
  ? await (dependencies.detectExecutable ?? detectExecutionRuntimes)(runtimes)
171
173
  : [];
174
+ const baseCapabilities = dependencies.capabilities ?? daemonCapabilities(runtimePlatform);
175
+ const capabilities = additionalCapabilities.length === 0 && Object.isFrozen(baseCapabilities)
176
+ ? baseCapabilities
177
+ : Object.freeze([...baseCapabilities, ...additionalCapabilities]);
172
178
  return {
173
179
  type: "machine:hello",
174
180
  hostname: hostname(),
@@ -176,10 +182,7 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
176
182
  daemonVersion: daemonVersion(),
177
183
  runtimes,
178
184
  executionRuntimes,
179
- capabilities: Object.freeze([
180
- ...(dependencies.capabilities ?? daemonCapabilities(runtimePlatform)),
181
- ...additionalCapabilities,
182
- ]),
185
+ capabilities,
183
186
  ...(dependencies.profileName === undefined ? {} : {
184
187
  layoutVersion: 1,
185
188
  profileName: dependencies.profileName,
@@ -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
+ }
@@ -5,6 +5,7 @@ import { isProjectId, isProjectSkillName, compareProjectSkillRefs, MAX_AGENT_PRO
5
5
  import { ProjectProjectionError, } from "./reconciler.js";
6
6
  import { AgentHandleSchema } from "../execution-protocol.js";
7
7
  import { createPromiseTail } from "../promise-tail.js";
8
+ import { createProjectSkillSnapshotEpoch, MAX_REMEMBERED_PROJECT_SKILL_AGENTS, toPathPrivateConvergenceResults, } from "./controller-convergence.js";
8
9
  const ProjectIdSchema = z.string().refine(isProjectId);
9
10
  const AgentSkillsSyncSchema = z.object({
10
11
  type: z.literal("agent:skills:sync"),
@@ -56,6 +57,13 @@ class ProjectScanTimeoutError extends Error {
56
57
  this.name = "ProjectScanTimeoutError";
57
58
  }
58
59
  }
60
+ class ProjectSkillStaleEpochError extends Error {
61
+ code = "skill_projection_stale";
62
+ constructor() {
63
+ super("skill_projection_stale");
64
+ this.name = "ProjectSkillStaleEpochError";
65
+ }
66
+ }
59
67
  const withinScanDeadline = async (operation, timeoutMs) => {
60
68
  let timer;
61
69
  try {
@@ -79,7 +87,12 @@ export function createProjectSkillsController(deps) {
79
87
  let initializationState = "idle";
80
88
  const scan = deps.scan ?? scanProjects;
81
89
  const scanTimeoutMs = deps.scanTimeoutMs ?? DEFAULT_PROJECT_SCAN_TIMEOUT_MS;
82
- const v2Snapshots = new Map();
90
+ const snapshotEpoch = createProjectSkillSnapshotEpoch();
91
+ let connectionEpochIdentity = Object.freeze({});
92
+ const assertCurrentEpoch = (token) => {
93
+ if (token !== undefined && token !== connectionEpochIdentity)
94
+ throw new ProjectSkillStaleEpochError();
95
+ };
83
96
  const scanCurrent = async (deferredProjectId) => withinScanDeadline(scan(await deps.registry.list(), undefined, [
84
97
  ...scanned
85
98
  .map((project) => project.inventory.projectId)
@@ -97,17 +110,25 @@ export function createProjectSkillsController(deps) {
97
110
  }));
98
111
  };
99
112
  const enqueue = (operation) => operationTail.enqueue(operation);
100
- const applyV2Snapshot = async (handle, snapshot) => {
113
+ const applyV2Snapshot = async (handle, snapshot, epochToken) => {
101
114
  if (deps.ensureSnapshot === undefined)
102
115
  throw new ProjectProjectionError("skill_projection_failed");
103
- await deps.ensureSnapshot(handle, snapshot);
104
- const previous = v2Snapshots.get(handle);
105
- if (previous === undefined || snapshot.generation >= previous.generation) {
106
- v2Snapshots.set(handle, Object.freeze({
107
- bindings: Object.freeze(snapshot.bindings.map((binding) => Object.freeze({ ...binding }))),
108
- generation: snapshot.generation,
109
- }));
116
+ assertCurrentEpoch(epochToken);
117
+ const acceptedEpochIdentity = connectionEpochIdentity;
118
+ const rememberedEntries = snapshotEpoch.entries();
119
+ const firstAcceptedInEpoch = !rememberedEntries.some(([candidate]) => candidate === handle);
120
+ if (firstAcceptedInEpoch && rememberedEntries.length >= MAX_REMEMBERED_PROJECT_SKILL_AGENTS) {
121
+ throw new ProjectProjectionError("skill_projection_failed");
122
+ }
123
+ if (firstAcceptedInEpoch) {
124
+ await deps.ensureSnapshot(handle, snapshot, Object.freeze({ refreshCopyProjection: true }));
125
+ }
126
+ else {
127
+ await deps.ensureSnapshot(handle, snapshot);
110
128
  }
129
+ assertCurrentEpoch(epochToken);
130
+ if (acceptedEpochIdentity === connectionEpochIdentity)
131
+ snapshotEpoch.remember(handle, snapshot);
111
132
  return Object.freeze(projectSkillResolutionRecords(snapshot.bindings, scanned)
112
133
  .map((record) => Object.freeze({
113
134
  projectId: record.projectId,
@@ -115,15 +136,24 @@ export function createProjectSkillsController(deps) {
115
136
  status: record.mode === "resolved" ? "linked" : "unavailable",
116
137
  })));
117
138
  };
118
- const reapplyV2Snapshots = async () => {
119
- if (deps.ensureSnapshot === undefined) {
120
- if (v2Snapshots.size > 0)
121
- throw new ProjectProjectionError("skill_projection_failed");
139
+ const convergeV2Snapshots = async (refreshCopyProjection, epochToken) => {
140
+ assertCurrentEpoch(epochToken);
141
+ const acceptedEpochIdentity = connectionEpochIdentity;
142
+ const entries = snapshotEpoch.entries();
143
+ if (deps.ensureSnapshot === undefined || entries.length === 0)
144
+ return;
145
+ const results = await Promise.allSettled(entries.map(([handle, snapshot]) => refreshCopyProjection
146
+ ? deps.ensureSnapshot(handle, snapshot, Object.freeze({ refreshCopyProjection: true }))
147
+ : deps.ensureSnapshot(handle, snapshot)));
148
+ if (acceptedEpochIdentity !== connectionEpochIdentity) {
149
+ if (epochToken !== undefined)
150
+ throw new ProjectSkillStaleEpochError();
122
151
  return;
123
152
  }
124
- for (const [handle, snapshot] of [...v2Snapshots.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
125
- await deps.ensureSnapshot(handle, snapshot);
153
+ try {
154
+ deps.onConvergence?.(toPathPrivateConvergenceResults(entries, results));
126
155
  }
156
+ catch { /* convergence diagnostics must not change a successful inventory mutation */ }
127
157
  };
128
158
  const initialize = () => {
129
159
  if (initializationState === "ready" && initialization !== null)
@@ -141,24 +171,40 @@ export function createProjectSkillsController(deps) {
141
171
  return attempt;
142
172
  };
143
173
  return {
174
+ beginConnectionEpoch: () => {
175
+ connectionEpochIdentity = Object.freeze({});
176
+ snapshotEpoch.begin();
177
+ return connectionEpochIdentity;
178
+ },
144
179
  initialize,
145
180
  publishCurrent: () => enqueue(publishCurrent),
146
181
  scannedProjects: () => scanned,
147
- async handle(input) {
182
+ async handle(input, epochToken) {
148
183
  const parsed = ProjectCommandSchema.safeParse(input);
149
184
  if (!parsed.success)
150
185
  return { ok: false, error: "invalid_project_command" };
186
+ try {
187
+ assertCurrentEpoch(epochToken);
188
+ }
189
+ catch (error) {
190
+ return { ok: false, error: error.code };
191
+ }
151
192
  if (parsed.data.type === "agent:skills:sync") {
152
193
  try {
153
194
  await initialize();
195
+ assertCurrentEpoch(epochToken);
154
196
  }
155
197
  catch {
198
+ if (epochToken !== undefined && epochToken !== connectionEpochIdentity) {
199
+ return { ok: false, error: "skill_projection_stale" };
200
+ }
156
201
  return { ok: false, error: "project_skills_unavailable" };
157
202
  }
158
203
  }
159
204
  return enqueue(async () => {
160
205
  const command = parsed.data;
161
206
  try {
207
+ assertCurrentEpoch(epochToken);
162
208
  if (command.type === "agent:skills:sync") {
163
209
  if (command.generation !== undefined) {
164
210
  const snapshot = Object.freeze({
@@ -167,13 +213,14 @@ export function createProjectSkillsController(deps) {
167
213
  });
168
214
  return {
169
215
  ok: true,
170
- data: { bindings: await applyV2Snapshot(command.handle, snapshot) },
216
+ data: { bindings: await applyV2Snapshot(command.handle, snapshot, epochToken) },
171
217
  };
172
218
  }
173
219
  if (deps.reconcile === undefined)
174
220
  return { ok: false, error: "skill_projection_failed" };
175
221
  const bindings = await deps.reconcile(command.handle, command.bindings);
176
- v2Snapshots.delete(command.handle);
222
+ assertCurrentEpoch(epochToken);
223
+ snapshotEpoch.forget(command.handle);
177
224
  return {
178
225
  ok: true,
179
226
  data: { bindings },
@@ -182,8 +229,11 @@ export function createProjectSkillsController(deps) {
182
229
  if (command.type === "project:add") {
183
230
  const existed = (await deps.registry.list())
184
231
  .some((project) => project.projectId === command.projectId);
232
+ assertCurrentEpoch(epochToken);
185
233
  await deps.registry.add(command.projectId, command.root);
234
+ assertCurrentEpoch(epochToken);
186
235
  const next = await scanCurrent();
236
+ assertCurrentEpoch(epochToken);
187
237
  const added = next.find((project) => project.inventory.projectId === command.projectId);
188
238
  if (!existed && added?.inventory.errorCode === "machine_project_skill_limit_exceeded") {
189
239
  await deps.registry.remove(command.projectId);
@@ -193,9 +243,11 @@ export function createProjectSkillsController(deps) {
193
243
  }
194
244
  else if (command.type === "project:remove") {
195
245
  await deps.registry.remove(command.projectId);
246
+ assertCurrentEpoch(epochToken);
196
247
  }
197
248
  else {
198
249
  const projects = await deps.registry.list();
250
+ assertCurrentEpoch(epochToken);
199
251
  if (!projects.some((project) => project.projectId === command.projectId)) {
200
252
  return { ok: false, error: "project_not_found" };
201
253
  }
@@ -203,8 +255,10 @@ export function createProjectSkillsController(deps) {
203
255
  if (command.type !== "project:add") {
204
256
  await refresh(command.type === "project:rescan" ? command.projectId : undefined);
205
257
  }
206
- await reapplyV2Snapshots();
258
+ assertCurrentEpoch(epochToken);
207
259
  await publishCurrent();
260
+ assertCurrentEpoch(epochToken);
261
+ await convergeV2Snapshots(command.type === "project:rescan", epochToken);
208
262
  return { ok: true, data: { projectId: command.projectId } };
209
263
  }
210
264
  catch (error) {
@@ -214,11 +268,13 @@ export function createProjectSkillsController(deps) {
214
268
  ? error.code
215
269
  : error.code === "skill_name_conflict"
216
270
  ? "skill_name_conflict"
217
- : error.code === "skill_projection_failed"
218
- ? "skill_projection_failed"
219
- : error.code === "skill_projection_snapshot_corrupt"
220
- ? "skill_projection_snapshot_corrupt"
221
- : "project_operation_failed",
271
+ : error.code === "skill_projection_stale"
272
+ ? "skill_projection_stale"
273
+ : error.code === "skill_projection_failed"
274
+ ? "skill_projection_failed"
275
+ : error.code === "skill_projection_snapshot_corrupt"
276
+ ? "skill_projection_snapshot_corrupt"
277
+ : "project_operation_failed",
222
278
  };
223
279
  }
224
280
  });