@nowcrew/daemon 0.6.19 → 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 (36) 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 +62 -47
  9. package/dist/machine-info.js +8 -5
  10. package/dist/main.js +0 -0
  11. package/dist/project-skills/capability.js +109 -0
  12. package/dist/project-skills/controller-convergence.js +57 -0
  13. package/dist/project-skills/controller.js +80 -24
  14. package/dist/project-skills/initialized-reconciler.js +4 -4
  15. package/dist/project-skills/projection-state-domain.js +19 -2
  16. package/dist/project-skills/projection-state-store.js +3 -2
  17. package/dist/project-skills/projection-state-transaction.js +5 -1
  18. package/dist/project-skills/projection-state.js +1 -1
  19. package/dist/project-skills/reconciler.js +275 -102
  20. package/dist/project-skills/runtime-launch.js +102 -0
  21. package/dist/project-skills/runtime-root-bootstrap.js +47 -0
  22. package/dist/project-skills/runtime-root-domain.js +268 -0
  23. package/dist/project-skills/runtime-root-gc.js +293 -0
  24. package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
  25. package/dist/project-skills/runtime-root-leases.js +487 -0
  26. package/dist/project-skills/runtime-root-source-identity.js +60 -0
  27. package/dist/project-skills/runtime-root-startup.js +49 -0
  28. package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
  29. package/dist/project-skills/runtime-root-state-index.js +356 -0
  30. package/dist/project-skills/runtime-root-store.js +722 -0
  31. package/dist/project-skills/serve-capability.js +28 -0
  32. package/dist/project-skills/serve-startup.js +22 -0
  33. package/dist/project-skills/types.js +1 -0
  34. package/dist/serve.js +58 -73
  35. package/dist/supervised-runtime.js +1 -5
  36. package/package.json +9 -8
@@ -2,7 +2,7 @@ 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
8
  import { materializeDefaultCodexHome } from "./runtimes/codex-home.js";
@@ -21,7 +21,9 @@ import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancell
21
21
  import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
22
22
  import { dslog } from "./slog.js";
23
23
  import { boundedDiagnosticJsonArray } from "./diagnostic-json.js";
24
+ import { ProjectProjectionError, } from "./project-skills/reconciler.js";
24
25
  import { formatProjectSkillRuntimeWarning } from "./project-skills/runtime-warning.js";
26
+ import { projectSkillRuntimeDirectories, redactProjectSkillRuntimeRootText, redactProjectSkillRuntimeRootValue, } from "./project-skills/runtime-launch.js";
25
27
  import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
26
28
  import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
27
29
  import { CodexStartupStageParser } from "./codex-startup-stage.js";
@@ -195,6 +197,19 @@ async function withKeyedLease(key, operation, cancellation) {
195
197
  }
196
198
  }
197
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
+ }
198
213
  const stableProtocolRuntime = dependencies.launchRuntime !== undefined;
199
214
  const supportsNativeResume = input.runtime.name === "claude"
200
215
  || (stableProtocolRuntime && runtimeCapability(input.runtime.name).nativeResume);
@@ -485,7 +500,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
485
500
  }
486
501
  const runtimeLaunchAt = Date.now();
487
502
  memoryPruneFailurePhase = "runtime_launch";
488
- const launchPreparedRuntime = async (abilityContext) => {
503
+ let activeProjectSkillRuntimeRoot;
504
+ const launchPreparedRuntime = async (runtimeRoot, abilityContext) => {
505
+ activeProjectSkillRuntimeRoot = runtimeRoot;
489
506
  const effectiveSystemPrompt = abilityContext === undefined
490
507
  ? systemPrompt
491
508
  : `${systemPrompt}\n\n${abilityContext.prompt}`;
@@ -521,31 +538,16 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
521
538
  skill_count: abilityContext.skillCount,
522
539
  });
523
540
  }
524
- const codexSkillRoots = runtime.name === "codex" && input.projectContext !== undefined
525
- ? [
526
- ...(input.projectSkills === undefined
527
- ? []
528
- : [join(workspace.dir, ".agents", "skills")]),
529
- ...(abilityContext?.skillRoot === undefined ? [] : [abilityContext.skillRoot]),
530
- ]
531
- : [];
532
- const claudeAdditionalDirectories = runtime.name === "claude"
533
- ? input.projectContext === undefined
534
- ? abilityContext?.skillRoot === undefined
535
- ? []
536
- : await resolveOrderedUniqueClaudeDirectories([
537
- join(workspace.dir, ".crew", "claude-skills"),
538
- workspace.dir,
539
- abilityContext.skillRoot,
540
- ])
541
- : await resolveOrderedUniqueClaudeDirectories([
542
- ...input.projectContext.secondary.map((project) => project.root),
543
- ...(input.projectSkills === undefined
544
- ? []
545
- : [join(workspace.dir, ".crew", "claude-skills")]),
546
- ...(abilityContext?.skillRoot === undefined ? [] : [abilityContext.skillRoot]),
547
- ], input.projectContext.primary === undefined ? [] : [input.projectContext.primary.root])
548
- : [];
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
+ });
549
551
  let runtimeEnv = childEnv;
550
552
  if (runtime.name === "claude"
551
553
  && input.projectContext !== undefined
@@ -571,7 +573,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
571
573
  runtime: runtime.name,
572
574
  bin: runtime.name === "deepseek-harness" ? "dsh-acp-demo" : runtime.name,
573
575
  cwd: input.projectContext?.primary?.root ?? executionWorkspace.runDir,
574
- ...(input.projectSkills === undefined && input.abilityRelease === undefined ? {} : { agentRoot: workspace.dir }),
576
+ ...(runtimeRoot !== undefined
577
+ || (input.projectSkills === undefined && input.abilityRelease === undefined)
578
+ ? {}
579
+ : { agentRoot: workspace.dir }),
575
580
  systemPromptPath: workspace.systemPromptPath,
576
581
  systemPrompt: effectiveSystemPrompt,
577
582
  wakePrompt,
@@ -598,17 +603,22 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
598
603
  memoryPruneFailurePhase = "runtime_launch";
599
604
  return launchRuntime(launchRequest);
600
605
  };
601
- const launchWithAbility = () => input.abilityRelease !== undefined && dependencies.abilityRelease !== undefined
602
- ? dependencies.abilityRelease.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.abilityRelease, launchPreparedRuntime)
603
- : launchPreparedRuntime();
604
- const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
605
- ? await (Array.isArray(input.projectSkills)
606
- ? dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, launchWithAbility)
607
- : dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, launchWithAbility, (warning) => callbacks.onConsole?.({
608
- stream: "system",
609
- text: formatProjectSkillRuntimeWarning(warning),
610
- })))
611
- : 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
+ }
612
622
  localMemoryTelemetry?.markRuntimeStarted();
613
623
  memoryPruneFailurePhase = "runtime_execution";
614
624
  if (child.cancel !== undefined) {
@@ -660,7 +670,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
660
670
  usage = meta.usage;
661
671
  if (meta.model)
662
672
  observedModel = meta.model;
663
- for (const activity of normalizeEvent(event)) {
673
+ for (const rawActivity of normalizeEvent(event)) {
674
+ const activity = redactProjectSkillRuntimeRootValue(rawActivity, activeProjectSkillRuntimeRoot);
664
675
  if (activity.kind === "sending")
665
676
  sentViaCrew = true;
666
677
  activities.push(activity);
@@ -668,18 +679,20 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
668
679
  }
669
680
  const extracted = extractFinalText(event);
670
681
  if (extracted) {
682
+ const safeExtracted = redactProjectSkillRuntimeRootText(extracted, activeProjectSkillRuntimeRoot);
671
683
  const incremental = typeof event === "object" && event !== null
672
684
  && "type" in event && (event.type === "kimi.acp.text_delta"
673
685
  || event.type === "hermes.acp.text_delta"
674
686
  || event.type === "deepseek-harness.acp.text_delta"
675
687
  || event.type === "opencode.text_delta");
676
- finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
688
+ finalText = incremental ? `${finalText ?? ""}${safeExtracted}` : safeExtracted;
677
689
  }
678
690
  for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
679
- callbacks.onExternalOutput?.(text);
691
+ callbacks.onExternalOutput?.(redactProjectSkillRuntimeRootText(text, activeProjectSkillRuntimeRoot));
692
+ }
693
+ for (const chunk of consoleFormatter.format(event)) {
694
+ callbacks.onConsole?.(redactProjectSkillRuntimeRootValue(chunk, activeProjectSkillRuntimeRoot));
680
695
  }
681
- for (const chunk of consoleFormatter.format(event))
682
- callbacks.onConsole?.(chunk);
683
696
  });
684
697
  let stderrTail = "";
685
698
  const codexStartupStageState = { last: null };
@@ -700,8 +713,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
700
713
  })
701
714
  : null;
702
715
  child.stderr.on("data", (data) => {
703
- process.stderr.write(data);
704
- const text = String(data);
716
+ const text = redactProjectSkillRuntimeRootText(String(data), activeProjectSkillRuntimeRoot);
717
+ process.stderr.write(text);
705
718
  stderrTail = (stderrTail + text).slice(-STDERR_TAIL_CAP);
706
719
  codexStartupStageParser?.push(text);
707
720
  });
@@ -761,7 +774,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
761
774
  memoryPruneRuntimeExitCode = exitCode;
762
775
  const errorTail = [
763
776
  stderrTail.trim(),
764
- spawnError,
777
+ spawnError === undefined
778
+ ? undefined
779
+ : redactProjectSkillRuntimeRootText(spawnError, activeProjectSkillRuntimeRoot),
765
780
  terminationSignal ? `terminated by ${terminationSignal}` : undefined,
766
781
  ].filter(Boolean).join(" ").trim();
767
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,
package/dist/main.js CHANGED
File without changes
@@ -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
  });
@@ -10,11 +10,11 @@ export function initializedProjectSkillsReconciler(ensureInitialized, reconciler
10
10
  async reconcile(handle, bindings) {
11
11
  return (await ready()).reconcile(handle, bindings);
12
12
  },
13
- async ensureSnapshot(handle, snapshot) {
14
- return (await ready()).ensureSnapshot(handle, snapshot);
13
+ async ensureSnapshot(handle, snapshot, options) {
14
+ return (await ready()).ensureSnapshot(handle, snapshot, options);
15
15
  },
16
- async prepareAndLaunch(agentsRoot, handle, projection, launch, onWarning) {
17
- return (await ready()).prepareAndLaunch(agentsRoot, handle, projection, launch, onWarning);
16
+ async prepareAndLaunch(agentsRoot, executionId, handle, projection, launch, onWarning) {
17
+ return (await ready()).prepareAndLaunch(agentsRoot, executionId, handle, projection, launch, onWarning);
18
18
  },
19
19
  };
20
20
  }