@nowcrew/daemon 0.6.89 → 0.6.90
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/README.md +64 -3
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-environment-contract.js +55 -0
- package/dist/agent-environment-control.js +24 -0
- package/dist/agent-environment-execution.js +98 -0
- package/dist/agent-environments.js +275 -0
- package/dist/automatic-daemon-installer.js +49 -1
- package/dist/computer-main.js +18 -0
- package/dist/computer-profile.js +17 -4
- package/dist/computer-setup-client.js +318 -0
- package/dist/computer-setup-orchestration.js +203 -0
- package/dist/computer-setup.js +244 -0
- package/dist/config.js +30 -12
- package/dist/console-formatter.js +3 -3
- package/dist/console.js +15 -2
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +1 -0
- package/dist/execution-runner.js +13 -8
- package/dist/i18n.js +19 -0
- package/dist/list-models.js +12 -12
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +57 -84
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/index-budget.js +8 -0
- package/dist/local-memory/inventory-cli.js +61 -0
- package/dist/local-memory/inventory-preview.js +159 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +19 -12
- package/dist/local-memory/read-cli.js +97 -12
- package/dist/local-memory/rollout.js +82 -0
- package/dist/local-memory/runtime.js +23 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- package/dist/local-runtime-environment.js +50 -0
- package/dist/machine-environment-cache.js +147 -0
- package/dist/machine-environment-contract.js +10 -0
- package/dist/machine-environment-control.js +27 -0
- package/dist/machine-environment-policy.js +75 -0
- package/dist/machine-environment-process.js +110 -0
- package/dist/machine-info.js +70 -25
- package/dist/machine-runtime-environment.js +189 -0
- package/dist/machine-shell-environment-capture.js +135 -0
- package/dist/machine-windows-environment-capture.js +215 -0
- package/dist/machine-windows-environment-script.js +158 -0
- package/dist/main.js +4 -69
- package/dist/marketplace-command.js +21 -0
- package/dist/marketplace-controller.js +80 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-remote.js +48 -0
- package/dist/marketplace-runner.js +235 -0
- package/dist/marketplace-serve.js +17 -0
- package/dist/marketplace-source.js +103 -0
- package/dist/message-file-preview.js +246 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/provider-env.js +26 -15
- package/dist/runner.js +17 -4
- package/dist/runtime-environment-merge.js +16 -0
- package/dist/runtime-health-handler.js +10 -0
- package/dist/runtime-health.js +3 -1
- package/dist/runtime-path.js +22 -12
- package/dist/runtime-probe.js +35 -6
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/deepseek-harness-launch.js +10 -0
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +226 -0
- package/dist/runtimes/deepseek-harness.js +76 -15
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +78 -13
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +88 -176
- package/dist/storage-identity.js +0 -3
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +17 -2
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/dist/workspace.js +1 -1
- package/package.json +13 -12
- package/dist/codex-home-launch.js +0 -7
- package/dist/codex-home-mode-controller.js +0 -68
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/runtimes/codex-home-migration-cli.js +0 -26
- package/dist/runtimes/codex-home-migration.js +0 -112
- package/dist/runtimes/codex-home.js +0 -236
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -241
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -447
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ExecutionCompletedSchema, ExecutionRejectedSchema } from "./execution-protocol.js";
|
|
2
|
+
import { hashExecutionSpec } from "./execution-runner.js";
|
|
3
|
+
import { dslog } from "./slog.js";
|
|
4
|
+
/** Queue rejection is admission work too: own the ID before any asynchronous journal write. */
|
|
5
|
+
export async function rejectQueuedAdmission(context, spec, executionClass, facts) {
|
|
6
|
+
const id = spec.executionId;
|
|
7
|
+
const hash = hashExecutionSpec(spec);
|
|
8
|
+
if (context.knownExecutionHashes.has(id) || context.executionRuns.has(id)) {
|
|
9
|
+
// A same-ID request won while the caller awaited journal.get. Its journal may
|
|
10
|
+
// still be pending; silence lets its real result arrive instead of rejecting it.
|
|
11
|
+
const existing = await context.journal.get(id);
|
|
12
|
+
if (existing?.specHash === hash)
|
|
13
|
+
context.replay(existing);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
context.knownExecutionHashes.set(id, hash);
|
|
17
|
+
const rejected = ExecutionRejectedSchema.parse({
|
|
18
|
+
type: "execution:rejected", protocolVersion: 1, executionId: id,
|
|
19
|
+
reason: "resource_limit", message: "Local machine execution queue is full", at: new Date().toISOString(),
|
|
20
|
+
});
|
|
21
|
+
dslog("execution.machine_queue_rejected", "机器执行队列已满", {
|
|
22
|
+
level: "WARN", execution_id: id, agent_handle: spec.agent.handle, execution_class: executionClass, ...facts,
|
|
23
|
+
});
|
|
24
|
+
const rejecting = persistPreAdmissionRejection(context.journal, spec, rejected)
|
|
25
|
+
.then(async (entry) => {
|
|
26
|
+
if (entry)
|
|
27
|
+
await context.report({ ...rejected, preAdmissionProof: { specHash: hash } });
|
|
28
|
+
else {
|
|
29
|
+
const existing = await context.journal.get(id);
|
|
30
|
+
if (existing?.specHash === hash)
|
|
31
|
+
context.replay(existing);
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
.catch(error => {
|
|
35
|
+
// A failed durable write has no safe rejection proof; a restart/sync will
|
|
36
|
+
// reconcile an accepted tombstone if this admission created one.
|
|
37
|
+
dslog("execution.admission_rejection_failed", "准入拒绝未取得持久停止证明", {
|
|
38
|
+
level: "ERROR", execution_id: id, error_message: String(error),
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
.finally(() => {
|
|
42
|
+
if (context.executionRuns.get(id) === rejecting) {
|
|
43
|
+
context.executionRuns.delete(id);
|
|
44
|
+
context.knownExecutionHashes.delete(id);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
context.executionRuns.set(id, rejecting);
|
|
48
|
+
await rejecting;
|
|
49
|
+
}
|
|
50
|
+
/** The caller synchronously owns this admission ID. Never complete somebody else's record. */
|
|
51
|
+
export async function persistPreAdmissionRejection(journal, spec, rejection) {
|
|
52
|
+
const accepted = await journal.accept(spec.executionId, hashExecutionSpec(spec), {
|
|
53
|
+
runtime: spec.runtime.name, ...(spec.runtime.model ? { model: spec.runtime.model } : {}), resumed: false,
|
|
54
|
+
});
|
|
55
|
+
if (accepted.kind !== "created")
|
|
56
|
+
return null;
|
|
57
|
+
return journal.complete(spec.executionId, ExecutionCompletedSchema.parse({
|
|
58
|
+
type: "execution:completed", protocolVersion: 1, executionId: spec.executionId,
|
|
59
|
+
outcome: "failed", errorCode: rejection.reason, errorMessage: rejection.message ?? "Admission rejected before runtime start",
|
|
60
|
+
runtime: spec.runtime.name, ...(spec.runtime.model ? { model: spec.runtime.model } : {}), resumed: false,
|
|
61
|
+
startedAt: accepted.acceptedAt,
|
|
62
|
+
finishedAt: new Date(Math.max(Date.now(), Date.parse(accepted.acceptedAt))).toISOString(),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
@@ -9,10 +9,11 @@ export function projectSkillExecutionProjection(bindings, generation) {
|
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* 投影门控与 ownership 判定的唯一谓词:只有绑定了 Project Skills
|
|
12
|
+
* 投影门控与 ownership 判定的唯一谓词:只有绑定了 Project Skills 且运行时支持受控目录的执行
|
|
13
13
|
* 才会有投影目录,也才需要在停止无法证明时保留它。两处判定分开写过一次,结果 kimi + projectSkills
|
|
14
14
|
* 会在取消路径上错抛 ownership 未确认。
|
|
15
15
|
*/
|
|
16
16
|
export const projectSkillProjectionApplies = (spec) => spec.agent.projectSkills !== undefined
|
|
17
|
-
&& (spec.runtime.name === "codex" || spec.runtime.name === "claude"
|
|
17
|
+
&& (spec.runtime.name === "codex" || spec.runtime.name === "claude"
|
|
18
|
+
|| spec.runtime.name === "deepseek-harness");
|
|
18
19
|
export const projectSkillProjectionErrorCode = (error) => error instanceof ProjectProjectionError ? error.code : null;
|
|
@@ -11,12 +11,26 @@ export function projectSkillRuntimeLaunchFacts(runtime, root) {
|
|
|
11
11
|
if (runtime === "claude") {
|
|
12
12
|
return Object.freeze({ claudeAdditionalDirectory: root.claudeAdditionalDirectory });
|
|
13
13
|
}
|
|
14
|
+
if (runtime === "deepseek-harness") {
|
|
15
|
+
return Object.freeze({ harnessSkillRoot: root.codexSkillRoot });
|
|
16
|
+
}
|
|
14
17
|
throw new ProjectProjectionError("skill_projection_failed");
|
|
15
18
|
}
|
|
16
19
|
/** Builds Runtime-native arrays while keeping v1 shared roots separate from v2 exact roots. */
|
|
17
20
|
export async function projectSkillRuntimeDirectories(input) {
|
|
18
21
|
const trainingProjectRoots = input.trainingProjectRoots ?? [];
|
|
19
22
|
const facts = projectSkillRuntimeLaunchFacts(input.runtime, input.runtimeRoot);
|
|
23
|
+
if (input.runtime === "deepseek-harness") {
|
|
24
|
+
// Harness 的 local provider 接受直接包含 Skill 子目录的受控 root,不扫描项目/用户目录。
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
codexSkillRoots: Object.freeze([]),
|
|
27
|
+
claudeAdditionalDirectories: Object.freeze([]),
|
|
28
|
+
harnessSkillRoots: Object.freeze([
|
|
29
|
+
...(facts.harnessSkillRoot === undefined ? [] : [facts.harnessSkillRoot]),
|
|
30
|
+
...(input.abilitySkillRoot === undefined ? [] : [input.abilitySkillRoot]),
|
|
31
|
+
]),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
20
34
|
const codexSkillRoots = input.runtime === "codex"
|
|
21
35
|
&& (input.runtimeRoot !== undefined || input.projectContext !== undefined)
|
|
22
36
|
? Object.freeze([
|
package/dist/provider-env.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { DEEPSEEK_CODEX_KEY_ENV, deepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
|
|
13
|
-
import {
|
|
13
|
+
import { mergeRuntimeEnvironments } from "./runtime-environment-merge.js";
|
|
14
14
|
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
15
|
const RESERVED_ENV = new Set([
|
|
16
16
|
"PATH",
|
|
@@ -57,26 +57,26 @@ const CONFLICTING_CODEX_ENV = [
|
|
|
57
57
|
DEEPSEEK_CODEX_KEY_ENV,
|
|
58
58
|
];
|
|
59
59
|
/** 返回叠加了 provider 隔离的新 env(不改入参);不支持的 runtime/provider 组合原样返回。 */
|
|
60
|
-
export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverride) {
|
|
60
|
+
export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverride, platform = process.platform) {
|
|
61
61
|
if (runtime === "codex" && cfg.provider === "deepseek") {
|
|
62
62
|
const env = { ...base };
|
|
63
|
-
for (const key of
|
|
64
|
-
|
|
63
|
+
for (const key of Object.keys(env))
|
|
64
|
+
if (CONFLICTING_CODEX_ENV.includes(platform === "win32" ? key.toUpperCase() : key))
|
|
65
|
+
delete env[key];
|
|
65
66
|
env.CODEX_HOME = deepSeekHomeOverride ?? deepSeekCodexHome(homeDir);
|
|
66
67
|
if (cfg.providerApiKey)
|
|
67
68
|
env[DEEPSEEK_CODEX_KEY_ENV] = cfg.providerApiKey;
|
|
68
69
|
return env;
|
|
69
70
|
}
|
|
70
71
|
if (runtime === "codex") {
|
|
71
|
-
if (cfg.codexHomeMode === "private")
|
|
72
|
-
return { ...base, CODEX_HOME: join(homeDir, ".codex") };
|
|
73
72
|
return base;
|
|
74
73
|
}
|
|
75
74
|
if (runtime !== "claude" || cfg.provider !== "custom")
|
|
76
75
|
return base;
|
|
77
76
|
const env = { ...base };
|
|
78
|
-
for (const
|
|
79
|
-
|
|
77
|
+
for (const key of Object.keys(env))
|
|
78
|
+
if (CONFLICTING_ENV.includes(platform === "win32" ? key.toUpperCase() : key))
|
|
79
|
+
delete env[key];
|
|
80
80
|
if (cfg.providerBaseUrl)
|
|
81
81
|
env.ANTHROPIC_BASE_URL = cfg.providerBaseUrl;
|
|
82
82
|
if (cfg.providerApiKey) {
|
|
@@ -94,20 +94,31 @@ export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverri
|
|
|
94
94
|
env.ANTHROPIC_SMALL_FAST_MODEL = cfg.providerSmallFastModel;
|
|
95
95
|
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = cfg.providerSmallFastModel;
|
|
96
96
|
}
|
|
97
|
-
return env;
|
|
97
|
+
return mergeRuntimeEnvironments([env], platform);
|
|
98
|
+
}
|
|
99
|
+
const deepSeekHarnessMachineOwnedKey = (key, platform) => {
|
|
100
|
+
const identity = platform === "win32" ? key.toUpperCase() : key;
|
|
101
|
+
return identity.startsWith("DEEPSEEK_") || identity.startsWith("NOWCREW_DEEPSEEK_HARNESS_") || identity.startsWith("DSH_");
|
|
102
|
+
};
|
|
103
|
+
/** 子命令切换目录后仍必须保留 provider 的鉴权/路由边界,包括被清除的键。 */
|
|
104
|
+
export function providerOwnedEnvironmentKeys(runtime, cfg, candidateKeys, platform = process.platform) {
|
|
105
|
+
if (runtime === "claude" && cfg.provider === "custom")
|
|
106
|
+
return [...CONFLICTING_ENV, "CLAUDE_CONFIG_DIR"];
|
|
107
|
+
if (runtime === "codex" && cfg.provider === "deepseek")
|
|
108
|
+
return CONFLICTING_CODEX_ENV;
|
|
109
|
+
if (runtime === "deepseek-harness")
|
|
110
|
+
return candidateKeys.filter(key => deepSeekHarnessMachineOwnedKey(key, platform));
|
|
111
|
+
return [];
|
|
98
112
|
}
|
|
99
|
-
const deepSeekHarnessMachineOwnedKey = (key) => (key.startsWith("DEEPSEEK_")
|
|
100
|
-
|| key === DEEPSEEK_HARNESS_CONFIG_ENV
|
|
101
|
-
|| key.startsWith("DSH_"));
|
|
102
113
|
/** Agent env remains available to tools, but cannot replace Harness process policy or credentials. */
|
|
103
|
-
export function applyDeepSeekHarnessMachineEnv(base, machineEnv) {
|
|
114
|
+
export function applyDeepSeekHarnessMachineEnv(base, machineEnv, platform = process.platform) {
|
|
104
115
|
const env = { ...base };
|
|
105
116
|
for (const key of Object.keys(env)) {
|
|
106
|
-
if (deepSeekHarnessMachineOwnedKey(key))
|
|
117
|
+
if (deepSeekHarnessMachineOwnedKey(key, platform))
|
|
107
118
|
delete env[key];
|
|
108
119
|
}
|
|
109
120
|
for (const [key, value] of Object.entries(machineEnv)) {
|
|
110
|
-
if (deepSeekHarnessMachineOwnedKey(key) && value !== undefined)
|
|
121
|
+
if (deepSeekHarnessMachineOwnedKey(key, platform) && value !== undefined)
|
|
111
122
|
env[key] = value;
|
|
112
123
|
}
|
|
113
124
|
return env;
|
package/dist/runner.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { createMachineEnvironmentProvider } from "./machine-environment-cache.js";
|
|
1
2
|
/** Legacy agent orchestration around the business-neutral local executor. */
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { join } from "node:path";
|
|
4
|
-
import { resolveReposRoot } from "./config.js";
|
|
5
|
+
import { isLocalMemoryV2Enabled, resolveReposRoot } from "./config.js";
|
|
5
6
|
import { mintAgentToken } from "./token.js";
|
|
6
7
|
import { buildSystemPrompt, buildWakePrompt, capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
7
8
|
import { deliverScheduledReport, } from "./scheduled-report.js";
|
|
@@ -24,6 +25,18 @@ function runtimeName(value) {
|
|
|
24
25
|
throw new Error(`unsupported runtime: ${value}`);
|
|
25
26
|
}
|
|
26
27
|
export async function runAgent(config, input, onActivity = defaultPrint, onConsole = () => { }, dependencies = {}) {
|
|
28
|
+
const owned = dependencies.machineEnvironment === undefined;
|
|
29
|
+
const machineEnvironment = dependencies.machineEnvironment
|
|
30
|
+
?? createMachineEnvironmentProvider({ mode: config.machineEnvironmentMode ?? "off" });
|
|
31
|
+
try {
|
|
32
|
+
return await runAgentWithMachineEnvironment(config, input, onActivity, onConsole, { ...dependencies, machineEnvironment });
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
if (owned)
|
|
36
|
+
await machineEnvironment.stop();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function runAgentWithMachineEnvironment(config, input, onActivity = defaultPrint, onConsole = () => { }, dependencies = {}) {
|
|
27
40
|
const threadId = input.scheduled ? undefined : input.threadId;
|
|
28
41
|
const taskAnchor = threadId ?? input.wakeMessageId;
|
|
29
42
|
// Old servers have no typed maintenance purpose. A legacy marker can only
|
|
@@ -36,7 +49,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
36
49
|
}), dependencies.cancellation);
|
|
37
50
|
const providerConfig = credential.config ?? {};
|
|
38
51
|
const runtime = runtimeName(providerConfig.runtime ?? config.runtimeBin);
|
|
39
|
-
const localMemoryV2Enabled = config
|
|
52
|
+
const localMemoryV2Enabled = isLocalMemoryV2Enabled(config, input.handle);
|
|
40
53
|
const reasoning = ReasoningSchema.safeParse(providerConfig.reasoning);
|
|
41
54
|
const baseWake = input.wake ?? buildWakePrompt(input.channelId);
|
|
42
55
|
const executionId = input.runId ?? `legacy-${randomUUID()}`;
|
|
@@ -70,7 +83,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
70
83
|
memory: capMemoryForInject(workspace.memory),
|
|
71
84
|
...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
|
|
72
85
|
collaborationToolsEnabled: config.codexCollaborationTools && runtime === "codex",
|
|
73
|
-
localMemoryV2Enabled,
|
|
86
|
+
localMemoryV2Enabled: workspace.localMemoryV2Enabled ?? localMemoryV2Enabled,
|
|
74
87
|
}),
|
|
75
88
|
// Native adapters, not a launch-intent prompt, choose history versus delta.
|
|
76
89
|
wakePrompt: () => baseWake,
|
|
@@ -91,7 +104,6 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
91
104
|
...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
|
|
92
105
|
cliPath: config.cliPath,
|
|
93
106
|
providerConfig,
|
|
94
|
-
codexHomeMode: config.codexHomeMode,
|
|
95
107
|
...(providerConfig.description ? { description: providerConfig.description } : {}),
|
|
96
108
|
systemEnv: {
|
|
97
109
|
...(providerConfig.fastMode ? { CREW_FAST_MODE: "1" } : {}),
|
|
@@ -119,6 +131,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
119
131
|
onRuntimeStarting: () => dependencies.onRuntimePhase?.("starting", runtime),
|
|
120
132
|
onRuntimeRunning: () => dependencies.onRuntimePhase?.("running", runtime),
|
|
121
133
|
}, {
|
|
134
|
+
...(dependencies.machineEnvironment === undefined ? {} : { machineEnvironment: dependencies.machineEnvironment }),
|
|
122
135
|
launchRuntime: dependencies.launchRuntime ?? ((request) => launchSupervisedRuntime(request, dependencies.startSupervisor, dependencies.cancellation, dependencies.platform)),
|
|
123
136
|
...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
|
|
124
137
|
...(dependencies.startupGate === undefined ? {} : { startupGate: dependencies.startupGate }),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Keep layer order authoritative even when Windows keys use different casing. */
|
|
2
|
+
export function mergeRuntimeEnvironments(layers, platform = process.platform) {
|
|
3
|
+
const entries = new Map();
|
|
4
|
+
for (const layer of layers) {
|
|
5
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
6
|
+
if (value !== undefined)
|
|
7
|
+
entries.set(platform === "win32" ? key.toUpperCase() : key, [key, value]);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return Object.fromEntries(entries.values());
|
|
11
|
+
}
|
|
12
|
+
export function runtimeEnvironmentValue(env, key, platform = process.platform) {
|
|
13
|
+
if (platform !== "win32")
|
|
14
|
+
return env?.[key];
|
|
15
|
+
return Object.entries(env ?? {}).find(([candidate]) => candidate.toUpperCase() === key.toUpperCase())?.[1];
|
|
16
|
+
}
|
|
@@ -3,3 +3,13 @@ export const runtimeHealthProbeHandler = (configuredProbe, executionRuntimesKnow
|
|
|
3
3
|
...(executionRuntimesKnown ? { availableRuntimes: detectedExecutionRuntimes } : {}),
|
|
4
4
|
}));
|
|
5
5
|
export const handleRuntimeHealthProbeFrame = (decoded, configuredProbe, executionRuntimesKnown, detectedExecutionRuntimes) => handleRuntimeProbeFrame(decoded, runtimeHealthProbeHandler(configuredProbe, executionRuntimesKnown, detectedExecutionRuntimes));
|
|
6
|
+
/** 将探针结果按既有 RPC 帧返回,serve 只保留路由选择。 */
|
|
7
|
+
export async function replyRuntimeHealthProbe(decoded, configuredProbe, known, runtimes, send) {
|
|
8
|
+
const result = await handleRuntimeHealthProbeFrame(decoded, configuredProbe, known, runtimes);
|
|
9
|
+
if (result !== null) {
|
|
10
|
+
try {
|
|
11
|
+
send({ type: "fs:result", ...result });
|
|
12
|
+
}
|
|
13
|
+
catch { /* Server 查询超时处理断连 */ }
|
|
14
|
+
}
|
|
15
|
+
}
|
package/dist/runtime-health.js
CHANGED
|
@@ -123,7 +123,9 @@ export async function probeRuntimeHealth(request, dependencies = {}) {
|
|
|
123
123
|
const now = dependencies.now ?? (() => new Date());
|
|
124
124
|
const monotonicNow = dependencies.monotonicNow ?? (() => performance.now());
|
|
125
125
|
const sourceEnv = dependencies.env ?? process.env;
|
|
126
|
-
const env =
|
|
126
|
+
const env = dependencies.env === undefined
|
|
127
|
+
? { ...sourceEnv, PATH: augmentedPath(sourceEnv) }
|
|
128
|
+
: { ...sourceEnv };
|
|
127
129
|
const availableRuntimes = dependencies.availableRuntimes ?? parsed.availableRuntimes;
|
|
128
130
|
const results = [];
|
|
129
131
|
for (const target of parsed.runtimes) {
|
package/dist/runtime-path.js
CHANGED
|
@@ -8,24 +8,27 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { win32, posix } from "node:path";
|
|
11
|
+
import { runtimeEnvironmentValue } from "./runtime-environment-merge.js";
|
|
11
12
|
const pathApi = (platform) => (platform === "win32" ? win32 : posix);
|
|
12
13
|
/** 该平台常见 CLI 安装目录候选(是否真实存在稍后由 augmentedPath 校验)。 */
|
|
13
14
|
export function commonBinDirs(env = process.env, platform = process.platform) {
|
|
14
15
|
const p = pathApi(platform);
|
|
16
|
+
const value = (key) => runtimeEnvironmentValue(env, key, platform);
|
|
15
17
|
if (platform === "win32") {
|
|
16
18
|
const dirs = [];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
p.join(
|
|
19
|
+
const userProfile = value("USERPROFILE"), appData = value("APPDATA"), localAppData = value("LOCALAPPDATA");
|
|
20
|
+
if (userProfile) {
|
|
21
|
+
dirs.push(p.join(userProfile, ".kimi-code", "bin"), // Kimi 官方安装器默认落点
|
|
22
|
+
p.join(userProfile, ".local", "bin"));
|
|
20
23
|
}
|
|
21
|
-
if (
|
|
22
|
-
dirs.push(p.join(
|
|
23
|
-
if (
|
|
24
|
-
dirs.push(p.join(
|
|
24
|
+
if (appData)
|
|
25
|
+
dirs.push(p.join(appData, "npm"));
|
|
26
|
+
if (localAppData)
|
|
27
|
+
dirs.push(p.join(localAppData, "Microsoft", "WindowsApps"));
|
|
25
28
|
return dirs;
|
|
26
29
|
}
|
|
27
30
|
const dirs = [];
|
|
28
|
-
const home =
|
|
31
|
+
const home = value("HOME");
|
|
29
32
|
if (home) {
|
|
30
33
|
dirs.push(p.join(home, ".kimi-code", "bin"), // Kimi 官方安装器默认落点
|
|
31
34
|
p.join(home, ".local", "bin"), // Claude 官方原生安装器默认落点
|
|
@@ -41,19 +44,26 @@ export function commonBinDirs(env = process.env, platform = process.platform) {
|
|
|
41
44
|
* 在 `env.PATH` 基础上,把候选目录里「真实存在且尚未在 PATH 中」的去重后 prepend,返回新的 PATH 字符串。
|
|
42
45
|
* 用户装的目录优先命中(prepend);全部已存在或都不在磁盘上时,原样返回 `env.PATH`。
|
|
43
46
|
*/
|
|
44
|
-
export function augmentedPath(env = process.env, deps = {}, platform = process.platform) {
|
|
47
|
+
export function augmentedPath(env = process.env, deps = {}, platform = process.platform, placement = "prepend") {
|
|
45
48
|
const dirExists = deps.dirExists ?? existsSync;
|
|
46
49
|
const sep = pathApi(platform).delimiter;
|
|
47
|
-
const current = env
|
|
48
|
-
const
|
|
50
|
+
const current = runtimeEnvironmentValue(env, "PATH", platform) ?? "";
|
|
51
|
+
const keyOf = (entry) => platform === "win32" ? entry.replace(/\//gu, "\\").toLowerCase() : entry;
|
|
52
|
+
const currentEntries = current.split(sep).filter((entry) => entry.length > 0);
|
|
53
|
+
const existing = new Set(currentEntries.map(keyOf));
|
|
49
54
|
const extra = [];
|
|
50
55
|
for (const dir of commonBinDirs(env, platform)) {
|
|
51
|
-
if (existing.has(dir) || extra.
|
|
56
|
+
if (existing.has(keyOf(dir)) || extra.some(entry => keyOf(entry) === keyOf(dir)))
|
|
52
57
|
continue;
|
|
53
58
|
if (!dirExists(dir))
|
|
54
59
|
continue;
|
|
55
60
|
extra.push(dir);
|
|
56
61
|
}
|
|
62
|
+
if (placement === "append") {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
return [...currentEntries, ...extra].filter(entry => { const key = keyOf(entry); if (seen.has(key))
|
|
65
|
+
return false; seen.add(key); return true; }).join(sep);
|
|
66
|
+
}
|
|
57
67
|
if (extra.length === 0)
|
|
58
68
|
return current;
|
|
59
69
|
return current.length > 0 ? `${extra.join(sep)}${sep}${current}` : extra.join(sep);
|
package/dist/runtime-probe.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { dslog } from "./slog.js";
|
|
1
3
|
import { executableRuntimes } from "./runtime-capabilities.js";
|
|
2
4
|
export function conservativeExecutionRuntimes(installed) {
|
|
3
5
|
return executableRuntimes(installed)
|
|
@@ -7,16 +9,43 @@ export function createRuntimeProbeCoordinator() {
|
|
|
7
9
|
const abort = new AbortController();
|
|
8
10
|
const flights = new Map();
|
|
9
11
|
return {
|
|
10
|
-
detect(installed, detector) {
|
|
11
|
-
const key = [...installed].sort()
|
|
12
|
+
detect(installed, detector, connectionId, environmentGeneration = "") {
|
|
13
|
+
const key = JSON.stringify([environmentGeneration, [...installed].sort()]);
|
|
12
14
|
const existing = flights.get(key);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const probeId = existing?.probeId ?? randomUUID();
|
|
16
|
+
const fields = { probe_id: probeId, connection_id: connectionId, installed_runtimes: JSON.stringify(installed) };
|
|
17
|
+
dslog("runtime_probe.request", "请求 runtime 执行能力探测", fields);
|
|
18
|
+
if (existing) {
|
|
19
|
+
dslog("runtime_probe.cache_hit", "复用 runtime 探测结果", { ...fields, cache_state: existing.state });
|
|
20
|
+
return existing.promise;
|
|
21
|
+
}
|
|
22
|
+
const startedAt = Date.now();
|
|
23
|
+
const complete = (outcome, executionRuntimes) => {
|
|
24
|
+
dslog("runtime_probe.complete", "runtime 探测聚合结束", {
|
|
25
|
+
...fields, outcome, duration_ms: Date.now() - startedAt, aborted: abort.signal.aborted,
|
|
26
|
+
...(executionRuntimes === undefined ? {} : { execution_runtimes: JSON.stringify(executionRuntimes) }),
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
let detection;
|
|
30
|
+
try {
|
|
31
|
+
detection = detector(installed, abort.signal, probeId);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
complete("threw");
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const pending = detection.then((result) => {
|
|
38
|
+
const flight = flights.get(key);
|
|
39
|
+
if (flight)
|
|
40
|
+
flights.set(key, { ...flight, state: "resolved" });
|
|
41
|
+
complete("resolved", result);
|
|
42
|
+
return result;
|
|
43
|
+
}, (error) => {
|
|
16
44
|
flights.delete(key);
|
|
45
|
+
complete("threw");
|
|
17
46
|
throw error;
|
|
18
47
|
});
|
|
19
|
-
flights.set(key, pending);
|
|
48
|
+
flights.set(key, { promise: pending, probeId, state: "pending" });
|
|
20
49
|
return pending;
|
|
21
50
|
},
|
|
22
51
|
stop() {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Readable as NodeReadable } from "node:stream";
|
|
2
|
+
import { ReadableStream, WritableStream } from "node:stream/web";
|
|
3
|
+
/** Probe-only counters. Never retain/log chunks, JSON-RPC params, stderr or env values. */
|
|
4
|
+
export class AcpProbeTransport {
|
|
5
|
+
elapsed;
|
|
6
|
+
initializeRequestedMs;
|
|
7
|
+
initializeResponseMs;
|
|
8
|
+
firstStdinWriteMs;
|
|
9
|
+
firstStdoutMs;
|
|
10
|
+
stdinBytesWritten = 0;
|
|
11
|
+
stdoutBytes = 0;
|
|
12
|
+
stdoutEnded = false;
|
|
13
|
+
constructor(elapsed) {
|
|
14
|
+
this.elapsed = elapsed;
|
|
15
|
+
}
|
|
16
|
+
initializeRequested() { this.initializeRequestedMs = this.elapsed(); }
|
|
17
|
+
initializeResponse() { this.initializeResponseMs = this.elapsed(); }
|
|
18
|
+
fields() {
|
|
19
|
+
return {
|
|
20
|
+
initialize_requested_ms: this.initializeRequestedMs,
|
|
21
|
+
initialize_response_ms: this.initializeResponseMs,
|
|
22
|
+
first_stdin_write_ms: this.firstStdinWriteMs,
|
|
23
|
+
first_stdout_ms: this.firstStdoutMs,
|
|
24
|
+
stdin_bytes_written: this.stdinBytesWritten,
|
|
25
|
+
stdout_bytes: this.stdoutBytes,
|
|
26
|
+
stdout_ended: this.stdoutEnded,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** A write callback confirms acceptance by the pipe, NOT receipt by the agent. */
|
|
30
|
+
writable(stdin) {
|
|
31
|
+
return new WritableStream({
|
|
32
|
+
start: (controller) => { stdin.once("error", (error) => controller.error(error)); },
|
|
33
|
+
write: (chunk) => new Promise((resolve, reject) => {
|
|
34
|
+
stdin.write(chunk, (error) => {
|
|
35
|
+
if (error) {
|
|
36
|
+
reject(error);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.firstStdinWriteMs ??= this.elapsed();
|
|
40
|
+
this.stdinBytesWritten += chunk.byteLength;
|
|
41
|
+
resolve();
|
|
42
|
+
});
|
|
43
|
+
}),
|
|
44
|
+
close: () => new Promise((resolve, reject) => {
|
|
45
|
+
stdin.end((error) => error ? reject(error) : resolve());
|
|
46
|
+
}),
|
|
47
|
+
abort: () => { stdin.destroy(); },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** Observe before NDJSON decoding, without adding a competing 'data' listener. */
|
|
51
|
+
readable(stdout) {
|
|
52
|
+
const reader = NodeReadable.toWeb(stdout).getReader();
|
|
53
|
+
return new ReadableStream({
|
|
54
|
+
pull: async (controller) => {
|
|
55
|
+
const item = await reader.read();
|
|
56
|
+
if (item.done) {
|
|
57
|
+
this.stdoutEnded = true;
|
|
58
|
+
controller.close();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const chunk = item.value;
|
|
62
|
+
this.firstStdoutMs ??= this.elapsed();
|
|
63
|
+
this.stdoutBytes += chunk.byteLength;
|
|
64
|
+
controller.enqueue(chunk);
|
|
65
|
+
},
|
|
66
|
+
cancel: (reason) => reader.cancel(reason),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -14,6 +14,7 @@ import { tmpdir } from "node:os";
|
|
|
14
14
|
import { createInterface } from "node:readline";
|
|
15
15
|
import { promisify } from "node:util";
|
|
16
16
|
import { isWin } from "../platform.js";
|
|
17
|
+
import { augmentedPath } from "../runtime-path.js";
|
|
17
18
|
const execFileP = promisify(execFile);
|
|
18
19
|
const PROBE_TIMEOUT_MS = 8_000;
|
|
19
20
|
// 本机丢弃端口:连不上也无所谓,init 事件在任何网络请求之前就已输出。
|
|
@@ -26,18 +27,27 @@ const CLAUDE_ALIASES = [
|
|
|
26
27
|
];
|
|
27
28
|
const DEFAULT_ALIAS = "sonnet";
|
|
28
29
|
const cacheByVersion = new Map();
|
|
30
|
+
let cacheByEnvironment = new WeakMap();
|
|
29
31
|
/** "2.1.258 (Claude Code)" → "2.1.258";空/异常输出 → ""。 */
|
|
30
32
|
export function normalizeClaudeVersion(raw) {
|
|
31
33
|
return raw.replace(/\s*\(Claude Code\)\s*$/, "").trim();
|
|
32
34
|
}
|
|
33
|
-
export async function listClaudeModels(deps =
|
|
34
|
-
const
|
|
35
|
-
|
|
35
|
+
export async function listClaudeModels(deps, env, environmentGeneration = "") {
|
|
36
|
+
const probes = deps ?? { probeVersion: () => probeVersion(env), resolveAlias: (alias) => resolveAlias(alias, env) };
|
|
37
|
+
let cache = env ? cacheByEnvironment.get(env) : cacheByVersion;
|
|
38
|
+
if (!cache) {
|
|
39
|
+
cache = new Map();
|
|
40
|
+
if (env)
|
|
41
|
+
cacheByEnvironment.set(env, cache);
|
|
42
|
+
}
|
|
43
|
+
const version = normalizeClaudeVersion(await probes.probeVersion());
|
|
44
|
+
const cacheKey = JSON.stringify([environmentGeneration, version]);
|
|
45
|
+
const cached = version ? cache.get(cacheKey) : undefined;
|
|
36
46
|
if (cached)
|
|
37
47
|
return cached;
|
|
38
48
|
const resolved = await Promise.all(CLAUDE_ALIASES.map(async ([alias]) => {
|
|
39
49
|
try {
|
|
40
|
-
return await
|
|
50
|
+
return await probes.resolveAlias(alias);
|
|
41
51
|
}
|
|
42
52
|
catch {
|
|
43
53
|
return null;
|
|
@@ -53,22 +63,24 @@ export async function listClaudeModels(deps = defaultDeps) {
|
|
|
53
63
|
};
|
|
54
64
|
});
|
|
55
65
|
if (version && resolved.every((model) => model !== null))
|
|
56
|
-
|
|
66
|
+
cache.set(cacheKey, models);
|
|
57
67
|
return models;
|
|
58
68
|
}
|
|
59
69
|
/** 仅测试用:清空版本缓存。 */
|
|
60
70
|
export function resetClaudeModelCache() {
|
|
61
71
|
cacheByVersion.clear();
|
|
72
|
+
cacheByEnvironment = new WeakMap();
|
|
62
73
|
}
|
|
63
|
-
async function probeVersion() {
|
|
74
|
+
async function probeVersion(env = { ...process.env, PATH: augmentedPath() }) {
|
|
64
75
|
const { stdout } = await execFileP("claude", ["--version"], {
|
|
65
76
|
shell: isWin(),
|
|
66
77
|
timeout: PROBE_TIMEOUT_MS,
|
|
67
78
|
killSignal: "SIGKILL",
|
|
79
|
+
env,
|
|
68
80
|
});
|
|
69
81
|
return stdout;
|
|
70
82
|
}
|
|
71
|
-
function resolveAlias(alias) {
|
|
83
|
+
function resolveAlias(alias, env = { ...process.env, PATH: augmentedPath() }) {
|
|
72
84
|
return new Promise((resolve) => {
|
|
73
85
|
const child = spawn("claude", [
|
|
74
86
|
"--model", alias,
|
|
@@ -78,7 +90,7 @@ function resolveAlias(alias) {
|
|
|
78
90
|
], {
|
|
79
91
|
cwd: tmpdir(),
|
|
80
92
|
env: {
|
|
81
|
-
...
|
|
93
|
+
...env,
|
|
82
94
|
ANTHROPIC_BASE_URL: UNROUTABLE_API_BASE,
|
|
83
95
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
84
96
|
},
|
|
@@ -120,4 +132,3 @@ export function initModelOf(line) {
|
|
|
120
132
|
return null;
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
|
-
const defaultDeps = { probeVersion, resolveAlias };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* launchd Background policy can stall dsh's synchronous startup reads before ACP.
|
|
3
|
+
* Apply application resource policy to this short-lived child, never the daemon.
|
|
4
|
+
* taskpolicy execs the target; no shell, alternate PATH, or protocol bypass.
|
|
5
|
+
*/
|
|
6
|
+
export function deepSeekHarnessLaunch(bin, args, platform = process.platform) {
|
|
7
|
+
return platform === "darwin"
|
|
8
|
+
? { bin: "/usr/sbin/taskpolicy", args: ["-a", "--", bin, ...args] }
|
|
9
|
+
: { bin, args: [...args] };
|
|
10
|
+
}
|