@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.
- package/dist/atomic-no-replace-rename.js +91 -0
- package/dist/control-plane-url.js +4 -2
- package/dist/directory-projection-publication.js +105 -0
- package/dist/directory-projection.js +20 -4
- package/dist/execution-journal.js +40 -4
- package/dist/execution-posix-stop-proof.js +82 -0
- package/dist/execution-runner.js +68 -8
- package/dist/local-executor.js +62 -47
- package/dist/machine-info.js +8 -5
- package/dist/main.js +0 -0
- package/dist/project-skills/capability.js +109 -0
- package/dist/project-skills/controller-convergence.js +57 -0
- package/dist/project-skills/controller.js +80 -24
- package/dist/project-skills/initialized-reconciler.js +4 -4
- package/dist/project-skills/projection-state-domain.js +19 -2
- package/dist/project-skills/projection-state-store.js +3 -2
- package/dist/project-skills/projection-state-transaction.js +5 -1
- package/dist/project-skills/projection-state.js +1 -1
- package/dist/project-skills/reconciler.js +275 -102
- package/dist/project-skills/runtime-launch.js +102 -0
- package/dist/project-skills/runtime-root-bootstrap.js +47 -0
- package/dist/project-skills/runtime-root-domain.js +268 -0
- package/dist/project-skills/runtime-root-gc.js +293 -0
- package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
- package/dist/project-skills/runtime-root-leases.js +487 -0
- package/dist/project-skills/runtime-root-source-identity.js +60 -0
- package/dist/project-skills/runtime-root-startup.js +49 -0
- package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
- package/dist/project-skills/runtime-root-state-index.js +356 -0
- package/dist/project-skills/runtime-root-store.js +722 -0
- package/dist/project-skills/serve-capability.js +28 -0
- package/dist/project-skills/serve-startup.js +22 -0
- package/dist/project-skills/types.js +1 -0
- package/dist/serve.js +58 -73
- package/dist/supervised-runtime.js +1 -5
- package/package.json +9 -8
package/dist/local-executor.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
|
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 ?? ""}${
|
|
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
|
-
|
|
704
|
-
|
|
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);
|
package/dist/machine-info.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
218
|
-
? "
|
|
219
|
-
: error.code === "
|
|
220
|
-
? "
|
|
221
|
-
: "
|
|
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
|
}
|