@nowcrew/daemon 0.6.76 → 0.6.77-beta.10
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 +27 -0
- package/dist/agent-ability/resolver.js +1 -1
- package/dist/attachment-limits.js +4 -0
- package/dist/attachments.js +33 -27
- package/dist/automatic-daemon-installer.js +44 -24
- package/dist/automatic-install-target.js +34 -9
- package/dist/computer-cli.js +43 -14
- package/dist/computer-profile.js +15 -2
- package/dist/computer-service.js +26 -5
- package/dist/config.js +34 -6
- package/dist/daemon-installation-lease.js +7 -4
- package/dist/daemon-local-home.js +108 -0
- package/dist/daemon-migration-controller.js +15 -4
- package/dist/daemon-registry.js +20 -0
- package/dist/daemon-update-controller.js +1 -0
- package/dist/daemon-update-eligibility.js +17 -7
- package/dist/daemon-updater.js +56 -30
- package/dist/execution-protocol.js +5 -2
- package/dist/execution-runner.js +7 -3
- package/dist/host-execution-coordinator.js +4 -3
- package/dist/local-executor.js +7 -0
- package/dist/local-memory/runtime.js +5 -3
- package/dist/machine-info.js +2 -0
- package/dist/main.js +33 -14
- package/dist/managed-service-diagnostics.js +1 -0
- package/dist/managed-service-registry.js +12 -7
- package/dist/managed-service-startup.js +4 -1
- package/dist/npm-command.js +9 -0
- package/dist/project-dependencies.js +4 -4
- package/dist/project-skills/runtime-root-bootstrap.js +6 -0
- package/dist/project-skills/runtime-root-startup.js +2 -1
- package/dist/project-skills/serve-startup.js +23 -0
- package/dist/repository-identity.js +2 -0
- package/dist/runner.js +3 -0
- package/dist/runtime-capabilities.js +3 -1
- package/dist/serve.js +7 -6
- package/dist/slog.js +14 -1
- package/dist/storage-control.js +12 -1
- package/dist/storage-identity.js +15 -1
- package/dist/storage-layout.js +2 -1
- package/dist/storage-migration-controller.js +25 -4
- package/dist/storage-ownership.js +55 -0
- package/dist/storage-runtime-controller.js +29 -7
- package/dist/storage-runtime-readiness.js +26 -4
- package/dist/storage-runtime.js +18 -2
- package/dist/storage-status-delivery.js +4 -0
- package/dist/storage-status-replay.js +16 -1
- package/dist/storage-worker-executor.js +1 -0
- package/dist/storage-worker-operation.js +4 -0
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -30,6 +30,7 @@ import { storageMigrationStatusReplay } from "./storage-status-replay.js";
|
|
|
30
30
|
import { runStorageWorkerCommand } from "./storage-worker-command.js";
|
|
31
31
|
import { probeStorageMigrationRuntime } from "./storage-runtime-readiness.js";
|
|
32
32
|
import { createStorageTargetConfirmationHandler } from "./storage-target-confirmation.js";
|
|
33
|
+
import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
|
|
33
34
|
async function main() {
|
|
34
35
|
const workerResult = await runStorageWorkerCommand(process.argv.slice(2));
|
|
35
36
|
if (workerResult !== null) {
|
|
@@ -73,8 +74,16 @@ async function main() {
|
|
|
73
74
|
if (values["daemon-home"])
|
|
74
75
|
process.env.CREW_DAEMON_HOME = values["daemon-home"];
|
|
75
76
|
let activeDaemonHome;
|
|
77
|
+
const localHome = resolveDaemonLocalStateRoot({
|
|
78
|
+
userHome: homedir(),
|
|
79
|
+
platform: process.platform,
|
|
80
|
+
explicit: process.env.NOWWORK_LOCAL_HOME,
|
|
81
|
+
mode: process.env.NOWWORK_LOCAL_HOME_MODE
|
|
82
|
+
?? process.env.NOWWORK_STORAGE_MODE
|
|
83
|
+
?? (values["daemon-home"] === undefined ? undefined : "legacy"),
|
|
84
|
+
});
|
|
76
85
|
if (values.profile) {
|
|
77
|
-
const home = daemonHome();
|
|
86
|
+
const home = daemonHome(process.env, values.profile);
|
|
78
87
|
activeDaemonHome = home;
|
|
79
88
|
const profile = await loadProfile(values.profile, home);
|
|
80
89
|
if (cmd === "serve") {
|
|
@@ -83,6 +92,7 @@ async function main() {
|
|
|
83
92
|
platform: process.platform,
|
|
84
93
|
profile: profile.name,
|
|
85
94
|
userHome: homedir(),
|
|
95
|
+
localHome,
|
|
86
96
|
uid: process.getuid?.(),
|
|
87
97
|
daemonHome: home,
|
|
88
98
|
agentsRoot: resolveAgentsRoot(profile.agentsRoot, homedir(), process.platform),
|
|
@@ -117,6 +127,23 @@ async function main() {
|
|
|
117
127
|
}
|
|
118
128
|
const provenance = daemonReleaseProvenance(daemonVersion());
|
|
119
129
|
assertReleaseTagForServerUrl(config.serverUrl, provenance.releaseTag);
|
|
130
|
+
const storageRuntime = values.profile === undefined
|
|
131
|
+
? null
|
|
132
|
+
: await loadStorageRuntime({
|
|
133
|
+
profile: values.profile,
|
|
134
|
+
serverUrl: config.serverUrl,
|
|
135
|
+
userHome: homedir(),
|
|
136
|
+
platform: process.platform,
|
|
137
|
+
environment: process.env,
|
|
138
|
+
});
|
|
139
|
+
const runtimeConfig = storageRuntime === null
|
|
140
|
+
? config
|
|
141
|
+
: Object.freeze({
|
|
142
|
+
...config,
|
|
143
|
+
agentsRoot: storageRuntime.layout.agentItemsRoot,
|
|
144
|
+
reposRoot: storageRuntime.layout.repoRoot,
|
|
145
|
+
qaReportRoot: storageRuntime.layout.qaReportRoot,
|
|
146
|
+
});
|
|
120
147
|
if (cmd === "serve") {
|
|
121
148
|
process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}, ${provenance.releaseChannel}/${provenance.releaseTag}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
|
|
122
149
|
const installation = values.profile === undefined
|
|
@@ -124,16 +151,7 @@ async function main() {
|
|
|
124
151
|
: daemonGlobalInstallation(builtDaemonEntry(), process.platform);
|
|
125
152
|
const installationLease = installation === null
|
|
126
153
|
? null
|
|
127
|
-
: await acquireDaemonInstallationLease(installation.npmPrefix);
|
|
128
|
-
const storageRuntime = values.profile === undefined
|
|
129
|
-
? null
|
|
130
|
-
: await loadStorageRuntime({
|
|
131
|
-
profile: values.profile,
|
|
132
|
-
serverUrl: config.serverUrl,
|
|
133
|
-
userHome: homedir(),
|
|
134
|
-
platform: process.platform,
|
|
135
|
-
environment: process.env,
|
|
136
|
-
});
|
|
154
|
+
: await acquireDaemonInstallationLease(installation.npmPrefix, { localHome });
|
|
137
155
|
const migrationRuntime = values.profile === undefined || activeDaemonHome === undefined || storageRuntime !== null
|
|
138
156
|
? { ready: false, reason: "not_legacy_source" }
|
|
139
157
|
: await probeStorageMigrationRuntime({
|
|
@@ -188,8 +206,9 @@ async function main() {
|
|
|
188
206
|
}
|
|
189
207
|
: undefined;
|
|
190
208
|
await runWithDaemonInstallationLease(installationLease, async () => {
|
|
191
|
-
const service = serve(
|
|
209
|
+
const service = serve(runtimeConfig, {
|
|
192
210
|
...(values.profile === undefined ? {} : { profileName: values.profile }),
|
|
211
|
+
localHome,
|
|
193
212
|
...(storageMigration === undefined ? {} : { storageMigration }),
|
|
194
213
|
});
|
|
195
214
|
await runServeLifecycle(service);
|
|
@@ -209,9 +228,9 @@ async function main() {
|
|
|
209
228
|
gitSha: provenance.gitSha,
|
|
210
229
|
artifactDigest: provenance.artifactDigest,
|
|
211
230
|
...(values.profile === undefined ? {} : { profileName: values.profile }),
|
|
212
|
-
agentsRoot:
|
|
231
|
+
agentsRoot: runtimeConfig.agentsRoot,
|
|
213
232
|
}); // 一次性 run 模式也上报 SLS(runner 里的埋点生效)
|
|
214
|
-
const result = await runAgent(
|
|
233
|
+
const result = await runAgent(runtimeConfig, {
|
|
215
234
|
handle: values.agent,
|
|
216
235
|
channelId: values.channel,
|
|
217
236
|
...(values.wake ? { wake: values.wake } : {}),
|
|
@@ -16,6 +16,7 @@ export async function inspectManagedServiceDiagnostics(input) {
|
|
|
16
16
|
const inspectLease = input.inspectInstallationLease
|
|
17
17
|
?? (() => inspectDaemonInstallationLease(input.expectedRecord.npmPrefix, {
|
|
18
18
|
...(userHome === undefined ? {} : { userHome }),
|
|
19
|
+
...(registry.localHome === undefined ? {} : { localHome: registry.localHome }),
|
|
19
20
|
}));
|
|
20
21
|
let services = null;
|
|
21
22
|
let registryCheck;
|
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { acquireProfileSaveLock } from "./computer-profile-lock.js";
|
|
8
|
+
import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
|
|
8
9
|
const AbsolutePathSchema = z.string().min(1).refine(isAbsolute, "must be an absolute path");
|
|
9
10
|
const ManagedServiceRecordSchema = z.object({
|
|
10
11
|
version: z.literal(1),
|
|
@@ -42,11 +43,15 @@ export class ManagedServiceConflictError extends ManagedServiceRegistryError {
|
|
|
42
43
|
this.owner = owner;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
function registryDirectory(userHome) {
|
|
46
|
-
|
|
46
|
+
function registryDirectory(userHome, localHome) {
|
|
47
|
+
const root = localHome ?? resolveDaemonLocalStateRoot({
|
|
48
|
+
userHome,
|
|
49
|
+
platform: process.platform,
|
|
50
|
+
});
|
|
51
|
+
return resolve(root, "daemon", "managed-services");
|
|
47
52
|
}
|
|
48
|
-
export function managedServiceRegistryPath(userHome = homedir()) {
|
|
49
|
-
return resolve(registryDirectory(userHome), "registry.json");
|
|
53
|
+
export function managedServiceRegistryPath(userHome = homedir(), localHome) {
|
|
54
|
+
return resolve(registryDirectory(userHome, localHome), "registry.json");
|
|
50
55
|
}
|
|
51
56
|
export async function listManagedServiceDescriptorPaths(platform, userHome = homedir()) {
|
|
52
57
|
const directory = platform === "darwin"
|
|
@@ -219,14 +224,14 @@ export function managedServiceConflict(services, candidate) {
|
|
|
219
224
|
}
|
|
220
225
|
export async function withManagedServiceRegistryLock(options, operation) {
|
|
221
226
|
const userHome = options.userHome ?? homedir();
|
|
222
|
-
const directory = registryDirectory(userHome);
|
|
227
|
+
const directory = registryDirectory(userHome, options.localHome);
|
|
223
228
|
const release = await acquireProfileSaveLock(directory, {
|
|
224
229
|
...(options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs }),
|
|
225
230
|
...(options.lockRetryMs === undefined ? {} : { retryMs: options.lockRetryMs }),
|
|
226
231
|
});
|
|
227
232
|
let operationError;
|
|
228
233
|
try {
|
|
229
|
-
const path = managedServiceRegistryPath(userHome);
|
|
234
|
+
const path = managedServiceRegistryPath(userHome, options.localHome);
|
|
230
235
|
const services = await readRegistryFile(path);
|
|
231
236
|
return await operation({
|
|
232
237
|
services,
|
|
@@ -258,7 +263,7 @@ export async function withManagedServiceRegistryLock(options, operation) {
|
|
|
258
263
|
}
|
|
259
264
|
}
|
|
260
265
|
export async function readManagedServiceRegistry(options = {}) {
|
|
261
|
-
return readRegistryFile(managedServiceRegistryPath(options.userHome ?? homedir()));
|
|
266
|
+
return readRegistryFile(managedServiceRegistryPath(options.userHome ?? homedir(), options.localHome));
|
|
262
267
|
}
|
|
263
268
|
export async function registerManagedService(input, options = {}) {
|
|
264
269
|
const candidate = normalizeManagedServiceRecord(input);
|
|
@@ -17,7 +17,10 @@ export async function assertManagedServiceStartup(input) {
|
|
|
17
17
|
...(input.environment === undefined ? {} : { environment: input.environment }),
|
|
18
18
|
});
|
|
19
19
|
const readRegistry = input.readManagedServices
|
|
20
|
-
?? (() => readManagedServiceRegistry({
|
|
20
|
+
?? (() => readManagedServiceRegistry({
|
|
21
|
+
userHome: input.userHome,
|
|
22
|
+
...(input.localHome === undefined ? {} : { localHome: input.localHome }),
|
|
23
|
+
}));
|
|
21
24
|
const listDescriptors = input.listManagedDescriptorPaths
|
|
22
25
|
?? (() => listManagedServiceDescriptorPaths(input.platform, input.userHome));
|
|
23
26
|
const readDescriptor = input.readServiceDescriptor ?? readServiceDescriptor;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { posix, win32 } from "node:path";
|
|
2
|
+
/** Execute POSIX npm through its owning Node binary so service PATH is irrelevant. */
|
|
3
|
+
export function npmInvocation(platform, nodePath = process.execPath) {
|
|
4
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
5
|
+
const npmPath = paths.join(paths.dirname(nodePath), platform === "win32" ? "npm.cmd" : "npm");
|
|
6
|
+
return platform === "win32"
|
|
7
|
+
? { command: npmPath, argsPrefix: [] }
|
|
8
|
+
: { command: nodePath, argsPrefix: [npmPath] };
|
|
9
|
+
}
|
|
@@ -111,8 +111,8 @@ async function ensureWorktreeExclude(mainRoot) {
|
|
|
111
111
|
const suffix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
|
|
112
112
|
await writeFile(excludePath, current + suffix + ".worktrees\n", "utf8");
|
|
113
113
|
}
|
|
114
|
-
async function ensureMainClone(git, repo, repositoryKey, reposRoot) {
|
|
115
|
-
const mainRoot = resolve(reposRoot,
|
|
114
|
+
async function ensureMainClone(git, repo, repositoryKey, directoryName, reposRoot) {
|
|
115
|
+
const mainRoot = resolve(reposRoot, directoryName);
|
|
116
116
|
if (!isWithin(reposRoot, mainRoot) || await existingAncestorIsSymlink(reposRoot, mainRoot)) {
|
|
117
117
|
throw new ProjectDependencyError("project_dependency_checkout_conflict", "repository path is unsafe", repositoryKey);
|
|
118
118
|
}
|
|
@@ -123,7 +123,7 @@ async function ensureMainClone(git, repo, repositoryKey, reposRoot) {
|
|
|
123
123
|
if (info === null) {
|
|
124
124
|
await mkdir(dirname(mainRoot), { recursive: true });
|
|
125
125
|
try {
|
|
126
|
-
await git.run(["clone",
|
|
126
|
+
await git.run(["clone", repo, mainRoot]);
|
|
127
127
|
}
|
|
128
128
|
catch {
|
|
129
129
|
await rm(mainRoot, { recursive: true, force: true }).catch(() => { });
|
|
@@ -243,7 +243,7 @@ export function createProjectDependencyProvisioner(options = {}) {
|
|
|
243
243
|
for (const { identity, projects } of grouped.values()) {
|
|
244
244
|
try {
|
|
245
245
|
await withProjectDependencyLock(lockRoot, identity.repositoryKey, async () => {
|
|
246
|
-
const mainRoot = await ensureMainClone(git, projects[0].repo, identity.repositoryKey, input.reposRoot);
|
|
246
|
+
const mainRoot = await ensureMainClone(git, projects[0].repo, identity.repositoryKey, identity.directoryName, input.reposRoot);
|
|
247
247
|
const seenVersions = new Set();
|
|
248
248
|
for (const project of projects) {
|
|
249
249
|
const branchSource = project.branch === undefined ? "remote-default" : "manifest";
|
|
@@ -16,12 +16,18 @@ export async function initializeAfterProjectSkillLeaseProtection(input) {
|
|
|
16
16
|
/**
|
|
17
17
|
* Runs only after execution-journal restart reconciliation has settled. A lease without a terminal
|
|
18
18
|
* journal fact remains durable and prevents the caller from marking Project Skill v2 ready.
|
|
19
|
+
*
|
|
20
|
+
* 重连时本进程仍在跑的 execution 合法持有 lease(`liveExecutionIds`):既不释放,也不算 unresolved。
|
|
21
|
+
* 否则一次网络抖动就会让 daemon 在整条连接上摘掉 Project Skills 能力(incident: 2026-09-02,
|
|
22
|
+
* 绑定项目的 Agent 全部 execution_spec_incompatible)。
|
|
19
23
|
*/
|
|
20
24
|
export async function recoverProjectSkillRuntimeRoots(input) {
|
|
21
25
|
for (const agentRoot of input.agentRoots) {
|
|
22
26
|
await input.roots.recover(agentRoot);
|
|
23
27
|
let unresolvedLease = false;
|
|
24
28
|
await input.leases.recover(agentRoot, async (executionId) => {
|
|
29
|
+
if (input.liveExecutionIds?.has(executionId))
|
|
30
|
+
return false;
|
|
25
31
|
const entry = await input.journal.get(executionId);
|
|
26
32
|
const releasable = entry?.state === "completed" || entry?.state === "interrupted";
|
|
27
33
|
if (!releasable)
|
|
@@ -22,13 +22,14 @@ export function createProjectSkillRuntimeRootStartup() {
|
|
|
22
22
|
leases,
|
|
23
23
|
initialize,
|
|
24
24
|
}),
|
|
25
|
-
recover: async (agentsRoot, journal, protectedExecutionIds) => {
|
|
25
|
+
recover: async (agentsRoot, journal, protectedExecutionIds, liveExecutionIds) => {
|
|
26
26
|
await recoverProjectSkillRuntimeRoots({
|
|
27
27
|
agentRoots: await agentRoots(agentsRoot),
|
|
28
28
|
journal,
|
|
29
29
|
roots,
|
|
30
30
|
leases,
|
|
31
31
|
gc,
|
|
32
|
+
...(liveExecutionIds === undefined ? {} : { liveExecutionIds }),
|
|
32
33
|
currentRootId: async (agentRoot) => (await readAppliedProjectSkillManifest(agentRoot))?.rootId ?? null,
|
|
33
34
|
afterLeaseRecovery: async () => {
|
|
34
35
|
const remaining = await initializeAfterProjectSkillLeaseProtection({
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { createExecutionJournal } from "../execution-journal.js";
|
|
2
2
|
import { reconcileExecutionJournal } from "../execution-recovery.js";
|
|
3
3
|
import { dslog, flushSlog } from "../slog.js";
|
|
4
|
+
/**
|
|
5
|
+
* 每次连接 preflight 的 Runtime root / lease 恢复。重连时本进程仍在跑的 execution 合法持有 lease,
|
|
6
|
+
* 必须当作存活而非孤儿;失败也必须留痕,否则整条连接静默摘掉 Project Skills 能力
|
|
7
|
+
* (incident: 2026-09-02,绑定项目的 Agent 全部 execution_spec_incompatible)。
|
|
8
|
+
*/
|
|
9
|
+
export function createProjectSkillReconnectRecovery(startup, agentsRoot, journal, protectedExecutionIds, liveExecutionIds) {
|
|
10
|
+
return async () => {
|
|
11
|
+
const live = liveExecutionIds();
|
|
12
|
+
try {
|
|
13
|
+
await startup.recover(agentsRoot, journal(), protectedExecutionIds, live);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
dslog("project_skills.preflight_recovery_failed", "Project Skill runtime root 恢复失败,本连接不宣告 Project Skills 能力", {
|
|
18
|
+
level: "ERROR",
|
|
19
|
+
error_code: error.code ?? "unknown",
|
|
20
|
+
error_message: error.message,
|
|
21
|
+
live_execution_count: live.size,
|
|
22
|
+
});
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
4
27
|
export async function initializeProjectSkillProtectedExecutionJournal(input) {
|
|
5
28
|
return input.startup.initializeAfterLeaseProtection(input.config.agentsRoot, async (snapshot) => {
|
|
6
29
|
input.protectedExecutionIds.clear();
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { AbilityRepositoryUrlSchema } from "./agent-ability/types.js";
|
|
3
3
|
const SCP_REPOSITORY = /^git@([^:]+):(.+)$/u;
|
|
4
4
|
const COMMIT = /^[a-f0-9]{40}$/u;
|
|
5
|
+
const DIRECTORY_HASH_LENGTH = 12;
|
|
5
6
|
function normalizePath(value) {
|
|
6
7
|
const path = value.replace(/^\/+|\/+$/gu, "").replace(/\.git$/iu, "");
|
|
7
8
|
if (!path || path.split("/").some((part) => part.length === 0 || part === "." || part === ".." || part.includes("\\"))) {
|
|
@@ -31,6 +32,7 @@ export function normalizeRepositoryIdentity(repository) {
|
|
|
31
32
|
canonical,
|
|
32
33
|
repositoryKey: `repo-${hash}`,
|
|
33
34
|
displayName,
|
|
35
|
+
directoryName: `${displayName}--${hash.slice(0, DIRECTORY_HASH_LENGTH)}`,
|
|
34
36
|
});
|
|
35
37
|
}
|
|
36
38
|
export function worktreeDirectoryName(branch, resolvedCommit) {
|
package/dist/runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Legacy agent orchestration around the business-neutral local executor. */
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { resolveReposRoot } from "./config.js";
|
|
4
5
|
import { mintAgentToken } from "./token.js";
|
|
5
6
|
import { buildSystemPrompt, buildWakePrompt, capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
6
7
|
import { deliverScheduledReport, } from "./scheduled-report.js";
|
|
@@ -84,6 +85,8 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
84
85
|
token: credential.token,
|
|
85
86
|
agentId: credential.agentId,
|
|
86
87
|
agentsRoot: config.agentsRoot,
|
|
88
|
+
reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
|
|
89
|
+
...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
|
|
87
90
|
cliPath: config.cliPath,
|
|
88
91
|
providerConfig,
|
|
89
92
|
codexHomeMode: config.codexHomeMode,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ATTACHMENT_NATIVE_IMAGE_MAX_BYTES } from "./attachment-limits.js";
|
|
1
2
|
export const LOCAL_EXECUTION_RUNTIMES = [
|
|
2
3
|
"claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness",
|
|
3
4
|
];
|
|
@@ -46,7 +47,8 @@ export function routeRuntimeAttachments(runtime, attachments) {
|
|
|
46
47
|
const lines = attachments.map((attachment) => `- ${JSON.stringify(attachment.path)} (${attachment.mime}, ${attachment.sizeBytes} bytes)`);
|
|
47
48
|
return {
|
|
48
49
|
nativeImagePaths: runtime === "codex"
|
|
49
|
-
? attachments.filter((attachment) => attachment.mime.startsWith("image/")
|
|
50
|
+
? attachments.filter((attachment) => attachment.mime.startsWith("image/")
|
|
51
|
+
&& attachment.sizeBytes <= ATTACHMENT_NATIVE_IMAGE_MAX_BYTES)
|
|
50
52
|
.map((attachment) => attachment.path)
|
|
51
53
|
: [],
|
|
52
54
|
promptSuffix: [
|
package/dist/serve.js
CHANGED
|
@@ -42,7 +42,7 @@ import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
|
|
|
42
42
|
import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
|
|
43
43
|
import { createProjectSkillRuntimeRootStartup, } from "./project-skills/runtime-root-startup.js";
|
|
44
44
|
import { createProjectSkillControllerReadiness, withProjectSkillCapabilityStatus } from "./project-skills/capability.js";
|
|
45
|
-
import { initializeProjectSkillProtectedExecutionJournal } from "./project-skills/serve-startup.js";
|
|
45
|
+
import { createProjectSkillReconnectRecovery, initializeProjectSkillProtectedExecutionJournal } from "./project-skills/serve-startup.js";
|
|
46
46
|
import { createProjectSkillConnectionCapabilityProvider, createProjectSkillMachineHelloPublisher } from "./project-skills/serve-capability.js";
|
|
47
47
|
import { createAgentAbilityRuntime } from "./agent-ability/runtime.js";
|
|
48
48
|
import { createProjectSkillsReconciler, } from "./project-skills/reconciler.js";
|
|
@@ -53,6 +53,7 @@ import { handleRuntimeHealthProbeFrame } from "./runtime-health-handler.js";
|
|
|
53
53
|
import { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
54
54
|
import { createServeMigrationController, handleMigrationControlMessage } from "./daemon-migration-wiring.js";
|
|
55
55
|
import { createCodexHomeModeController } from "./codex-home-mode-controller.js";
|
|
56
|
+
import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
|
|
56
57
|
import { attachStorageMigrationRuntime, createServeStorageStatusDelivery, handleStorageMigrationControl, replayStorageMigrationStatus, storageMigrationCapabilities, } from "./storage-serve-adapter.js";
|
|
57
58
|
export { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
58
59
|
// normalize.ts 的活动种类 → activity 枚举
|
|
@@ -83,8 +84,10 @@ export function serve(config, opts = {}) {
|
|
|
83
84
|
let detectedExecutionRuntimes = [], executionRuntimesKnown = false;
|
|
84
85
|
let runtimeFacts = null;
|
|
85
86
|
const runtimeProbe = createRuntimeProbeCoordinator();
|
|
87
|
+
const localHome = opts.localHome ?? resolveDaemonLocalStateRootFromEnv();
|
|
86
88
|
const hostCoordinator = opts.execution?.hostCoordinator
|
|
87
89
|
?? createHostExecutionCoordinator({
|
|
90
|
+
localHome,
|
|
88
91
|
executionSlots: config.executionLimits.maxParallelTotal,
|
|
89
92
|
startupSlots: config.executionLimits.maxStartingTotal,
|
|
90
93
|
startupGapMs: config.executionLimits.startupGapMs,
|
|
@@ -215,9 +218,7 @@ export function serve(config, opts = {}) {
|
|
|
215
218
|
additionalCapabilities: () => managedDaemonCapabilities(updateEligibility),
|
|
216
219
|
probeRuntimeRootStorePrimitive: projectSkillRuntimeRootStartup.probeNativePublicationPrimitiveReadiness,
|
|
217
220
|
initializeController: projectSkillControllerReadiness.ensure,
|
|
218
|
-
recoverRuntimeRootsAndLeases:
|
|
219
|
-
.recover(config.agentsRoot, executionJournal, protectedExecutionIds)
|
|
220
|
-
.then(() => true, () => false),
|
|
221
|
+
recoverRuntimeRootsAndLeases: createProjectSkillReconnectRecovery(projectSkillRuntimeRootStartup, config.agentsRoot, () => executionJournal, protectedExecutionIds, () => new Set(executionRuns.keys())),
|
|
221
222
|
applyReadiness: projectSkillControllerReadiness.applyPreflight,
|
|
222
223
|
});
|
|
223
224
|
const safeExecutionSend = (frame) => {
|
|
@@ -827,9 +828,9 @@ export function serve(config, opts = {}) {
|
|
|
827
828
|
// ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
|
|
828
829
|
const r = msg;
|
|
829
830
|
setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
|
|
830
|
-
if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string") {
|
|
831
|
+
if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
|
|
831
832
|
try {
|
|
832
|
-
opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId });
|
|
833
|
+
opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId, machineId: r.machineId });
|
|
833
834
|
machineHelloPublisher.republish();
|
|
834
835
|
}
|
|
835
836
|
catch (error) {
|
package/dist/slog.js
CHANGED
|
@@ -15,6 +15,7 @@ import { homedir } from "node:os";
|
|
|
15
15
|
import { join, resolve } from "node:path";
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
18
|
+
import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
|
|
18
19
|
const FLUSH_INTERVAL_MS = 1500;
|
|
19
20
|
const BATCH_SIZE = 50;
|
|
20
21
|
const SPOOL_MAX_FILES = 50; // spool 目录文件数上限,超出丢最旧(日志非关键数据,防无限膨胀)
|
|
@@ -27,6 +28,7 @@ let flushPromise = null;
|
|
|
27
28
|
let inFlightBatch = [];
|
|
28
29
|
let lastErrorAt = 0;
|
|
29
30
|
let exitHookInstalled = false;
|
|
31
|
+
let localStateRoot;
|
|
30
32
|
function deploymentEnvironment(serverHost) {
|
|
31
33
|
if (serverHost === "nowwork-in.nowcoder.com")
|
|
32
34
|
return "prod";
|
|
@@ -40,12 +42,23 @@ function deploymentEnvironment(serverHost) {
|
|
|
40
42
|
return "unknown";
|
|
41
43
|
}
|
|
42
44
|
function spoolDir() {
|
|
43
|
-
return process.env.CREW_SLS_SPOOL_DIR ?? join(
|
|
45
|
+
return process.env.CREW_SLS_SPOOL_DIR ?? join(localStateRoot ?? resolveDaemonLocalStateRoot({
|
|
46
|
+
userHome: homedir(),
|
|
47
|
+
platform: process.platform,
|
|
48
|
+
explicit: process.env.NOWWORK_LOCAL_HOME,
|
|
49
|
+
mode: process.env.NOWWORK_LOCAL_HOME_MODE ?? process.env.NOWWORK_STORAGE_MODE,
|
|
50
|
+
}), "logs", "sls-spool");
|
|
44
51
|
}
|
|
45
52
|
/** serve/run 启动时调用一次;disabled(CREW_SLS_LOG=off)则保持 no-op。 */
|
|
46
53
|
export function initSlog(serverUrl, machineToken, identity = {}) {
|
|
47
54
|
if (process.env.CREW_SLS_LOG === "off" || process.env.CREW_SLS_LOG === "0")
|
|
48
55
|
return;
|
|
56
|
+
localStateRoot = resolveDaemonLocalStateRoot({
|
|
57
|
+
userHome: homedir(),
|
|
58
|
+
platform: process.platform,
|
|
59
|
+
explicit: process.env.NOWWORK_LOCAL_HOME,
|
|
60
|
+
mode: process.env.NOWWORK_LOCAL_HOME_MODE ?? process.env.NOWWORK_STORAGE_MODE,
|
|
61
|
+
});
|
|
49
62
|
cfg = { serverUrl: serverUrl.replace(/\/+$/, ""), token: machineToken };
|
|
50
63
|
let serverHost;
|
|
51
64
|
try {
|
package/dist/storage-control.js
CHANGED
|
@@ -5,6 +5,8 @@ const IdentitySchema = z.object({
|
|
|
5
5
|
migrationId: z.string().uuid(),
|
|
6
6
|
environment: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,31}$/u),
|
|
7
7
|
workspaceId: z.string().uuid(),
|
|
8
|
+
ownerEpoch: z.number().int().positive().safe().optional(),
|
|
9
|
+
ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u).optional(),
|
|
8
10
|
});
|
|
9
11
|
const DoctorRequestSchema = IdentitySchema.extend({ type: z.literal("daemon:storage-doctor") }).strict();
|
|
10
12
|
const MigrateRequestSchema = IdentitySchema.extend({
|
|
@@ -26,6 +28,11 @@ export function createStorageControlController(input) {
|
|
|
26
28
|
const profile = z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u).parse(input.profile);
|
|
27
29
|
const sourceStorageFingerprint = z.string().regex(/^sha256:[0-9a-f]{64}$/u).parse(input.sourceStorageFingerprint);
|
|
28
30
|
const targetStorageFingerprint = z.string().regex(/^sha256:[0-9a-f]{64}$/u).parse(input.targetStorageFingerprint);
|
|
31
|
+
const ownership = input.ownership === undefined ? undefined : Object.freeze({
|
|
32
|
+
ownerEpoch: z.number().int().positive().safe().parse(input.ownership.ownerEpoch),
|
|
33
|
+
ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u)
|
|
34
|
+
.parse(input.ownership.ownershipClaimFingerprint),
|
|
35
|
+
});
|
|
29
36
|
const preflights = new Map();
|
|
30
37
|
const preflightRuns = new Map();
|
|
31
38
|
const appliedMigrationIds = new Set();
|
|
@@ -39,6 +46,7 @@ export function createStorageControlController(input) {
|
|
|
39
46
|
environment,
|
|
40
47
|
workspaceId,
|
|
41
48
|
profile,
|
|
49
|
+
...(ownership === undefined ? {} : ownership),
|
|
42
50
|
...extra,
|
|
43
51
|
});
|
|
44
52
|
const failure = (identity, errorCode, error, reopenAdmission = false, extra = {}) => {
|
|
@@ -59,7 +67,10 @@ export function createStorageControlController(input) {
|
|
|
59
67
|
input.onStatusError?.(error, statusFrame);
|
|
60
68
|
}
|
|
61
69
|
};
|
|
62
|
-
const matchesIdentity = (identity) => identity.environment === environment && identity.workspaceId === workspaceId
|
|
70
|
+
const matchesIdentity = (identity) => identity.environment === environment && identity.workspaceId === workspaceId
|
|
71
|
+
&& (ownership === undefined
|
|
72
|
+
|| (identity.ownerEpoch === ownership.ownerEpoch
|
|
73
|
+
&& identity.ownershipClaimFingerprint === ownership.ownershipClaimFingerprint));
|
|
63
74
|
const preflightKey = (identity) => `${identity.migrationId}\0${identity.environment}\0${identity.workspaceId}`;
|
|
64
75
|
const runPreflight = (identity, failureFallback = "doctor_failed") => {
|
|
65
76
|
const key = preflightKey(identity);
|
package/dist/storage-identity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { STORAGE_LAYOUT_VERSION } from "./storage-layout.js";
|
|
3
|
+
import { createStorageOwnershipClaim } from "./storage-ownership.js";
|
|
3
4
|
function fingerprint(namespace, value) {
|
|
4
5
|
return `sha256:${createHash("sha256").update(`${namespace}\0${value}`).digest("hex")}`;
|
|
5
6
|
}
|
|
@@ -14,14 +15,27 @@ export function storageHelloIdentity(input) {
|
|
|
14
15
|
|| installation.runtimeRoot !== layout.runtimeRoot) {
|
|
15
16
|
throw new Error("installation storage identity does not match resolved layout");
|
|
16
17
|
}
|
|
18
|
+
const workspaceRootFingerprint = fingerprint("workspace-root", layout.workspaceRoot);
|
|
19
|
+
let ownership;
|
|
20
|
+
if (input.ownership !== undefined) {
|
|
21
|
+
const { claimFingerprint: reportedFingerprint, ...claimIdentity } = input.ownership;
|
|
22
|
+
ownership = createStorageOwnershipClaim(claimIdentity);
|
|
23
|
+
if (ownership.claimFingerprint !== reportedFingerprint
|
|
24
|
+
|| ownership.workspaceId !== layout.workspaceId
|
|
25
|
+
|| ownership.environment !== layout.environment
|
|
26
|
+
|| ownership.workspaceRootFingerprint !== workspaceRootFingerprint) {
|
|
27
|
+
throw new Error("storage ownership identity does not match resolved layout");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
17
30
|
return Object.freeze({
|
|
18
31
|
storageLayoutVersion: STORAGE_LAYOUT_VERSION,
|
|
19
32
|
storageEnvironment: layout.environment,
|
|
20
33
|
storageWorkspaceId: layout.workspaceId,
|
|
21
34
|
storageIdFingerprint: storageIdFingerprint(installation.storageId),
|
|
22
|
-
workspaceRootFingerprint
|
|
35
|
+
workspaceRootFingerprint,
|
|
23
36
|
daemonRootFingerprint: fingerprint("daemon-root", layout.daemonRoot),
|
|
24
37
|
agentRootFingerprint: fingerprint("agent-root", layout.agentItemsRoot),
|
|
38
|
+
...(ownership === undefined ? {} : { storageOwnership: ownership }),
|
|
25
39
|
...(installation.storageMigrationId === undefined
|
|
26
40
|
? {}
|
|
27
41
|
: { storageMigrationId: installation.storageMigrationId }),
|
package/dist/storage-layout.js
CHANGED
|
@@ -121,6 +121,7 @@ export function storageLayout(input) {
|
|
|
121
121
|
agentItemsRoot: paths.join(workspaceRoot, "agent-items"),
|
|
122
122
|
daemonRoot: paths.join(workspaceRoot, "daemon"),
|
|
123
123
|
repoRoot: paths.join(workspaceRoot, "repos"),
|
|
124
|
+
qaReportRoot: paths.join(workspaceRoot, "qa-report-server-v2"),
|
|
124
125
|
suiteToolsRoot: paths.join(workspaceRoot, "suite-tools"),
|
|
125
126
|
migrationsRoot: paths.join(persistentRoot, "migrations"),
|
|
126
127
|
runtimeRoot,
|
|
@@ -132,7 +133,7 @@ export function agentStorageRoot(layout, agentId) {
|
|
|
132
133
|
}
|
|
133
134
|
const COMPARABLE_FIELDS = [
|
|
134
135
|
"layoutVersion", "environment", "workspaceId", "persistentRoot", "environmentRoot", "workspaceRoot",
|
|
135
|
-
"agentItemsRoot", "daemonRoot", "repoRoot", "suiteToolsRoot", "migrationsRoot", "runtimeRoot",
|
|
136
|
+
"agentItemsRoot", "daemonRoot", "repoRoot", "qaReportRoot", "suiteToolsRoot", "migrationsRoot", "runtimeRoot",
|
|
136
137
|
];
|
|
137
138
|
export function compareStorageLayout(current, expected) {
|
|
138
139
|
if (current === null || current.layoutVersion !== STORAGE_LAYOUT_VERSION)
|
|
@@ -27,6 +27,8 @@ const ReceiptSchema = z.object({
|
|
|
27
27
|
environment: z.string().min(1),
|
|
28
28
|
workspaceId: z.string().uuid(),
|
|
29
29
|
profile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u),
|
|
30
|
+
ownerEpoch: z.number().int().positive().safe().optional(),
|
|
31
|
+
ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u).optional(),
|
|
30
32
|
manifestDigest: z.string().regex(/^[0-9a-f]{64}$/u),
|
|
31
33
|
targetFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u),
|
|
32
34
|
status: ReceiptStatusSchema,
|
|
@@ -37,13 +39,23 @@ const ReceiptSchema = z.object({
|
|
|
37
39
|
sourceStopAttempted: z.boolean().optional(),
|
|
38
40
|
sourceStopped: z.boolean().optional(),
|
|
39
41
|
targetTouched: z.boolean().optional(),
|
|
40
|
-
}).strict()
|
|
42
|
+
}).strict().superRefine((receipt, context) => {
|
|
43
|
+
if ((receipt.ownerEpoch === undefined) !== (receipt.ownershipClaimFingerprint === undefined)) {
|
|
44
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "storage receipt ownership identity must be complete" });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
41
47
|
const MigrationLockSchema = z.object({
|
|
42
48
|
version: z.literal(1),
|
|
43
49
|
migrationId: z.string().uuid(),
|
|
44
50
|
pid: z.number().int().positive(),
|
|
45
51
|
createdAt: z.string().datetime({ offset: true }),
|
|
46
52
|
}).strict();
|
|
53
|
+
const REQUIRED_BACKUP_ARTIFACTS = Object.freeze([
|
|
54
|
+
"profile.json",
|
|
55
|
+
"service-descriptor",
|
|
56
|
+
"managed-service-registry.json",
|
|
57
|
+
"installations.json",
|
|
58
|
+
]);
|
|
47
59
|
export class StorageMigrationFailure extends Error {
|
|
48
60
|
code;
|
|
49
61
|
receipt;
|
|
@@ -204,6 +216,16 @@ function assertOwnedStaging(input) {
|
|
|
204
216
|
}
|
|
205
217
|
}
|
|
206
218
|
async function backupArtifacts(input, receipt) {
|
|
219
|
+
const providedNames = new Set(input.backupArtifacts.map((artifact) => artifact.name));
|
|
220
|
+
const missing = REQUIRED_BACKUP_ARTIFACTS.filter((name) => !providedNames.has(name));
|
|
221
|
+
if (missing.length > 0) {
|
|
222
|
+
throw new StorageMigrationFailure("backup_artifact_missing", `required rollback artifacts are missing: ${missing.join(", ")}`);
|
|
223
|
+
}
|
|
224
|
+
for (const artifact of input.backupArtifacts) {
|
|
225
|
+
if (!(await lstat(artifact.path)).isFile()) {
|
|
226
|
+
throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact must be a file");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
207
229
|
const directory = join(input.backupsRoot, receipt.migrationId);
|
|
208
230
|
const artifactsDirectory = join(directory, "artifacts");
|
|
209
231
|
await mkdir(artifactsDirectory, { recursive: true, mode: 0o700 });
|
|
@@ -217,8 +239,6 @@ async function backupArtifacts(input, receipt) {
|
|
|
217
239
|
throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact names must be unique safe file names");
|
|
218
240
|
}
|
|
219
241
|
names.add(artifact.name);
|
|
220
|
-
if (!(await lstat(artifact.path)).isFile())
|
|
221
|
-
throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact must be a file");
|
|
222
242
|
const destination = join(artifactsDirectory, artifact.name);
|
|
223
243
|
await copyFile(artifact.path, destination);
|
|
224
244
|
await chmod(destination, 0o600);
|
|
@@ -271,6 +291,7 @@ export async function runStorageMigration(input, dependencies) {
|
|
|
271
291
|
environment: input.doctor.environment,
|
|
272
292
|
workspaceId: input.doctor.workspaceId,
|
|
273
293
|
profile: input.doctor.profile,
|
|
294
|
+
...(input.ownership === undefined ? {} : input.ownership),
|
|
274
295
|
manifestDigest: input.doctor.manifestDigest,
|
|
275
296
|
targetFingerprint: targetFingerprint(input.targetWorkspaceRoot),
|
|
276
297
|
status: "preflight_passed",
|
|
@@ -301,7 +322,7 @@ export async function runStorageMigration(input, dependencies) {
|
|
|
301
322
|
input.assertHealthy?.();
|
|
302
323
|
await transition("backing_up");
|
|
303
324
|
receipt = await backupArtifacts(input, receipt).catch((error) => {
|
|
304
|
-
failureCode = "backup_failed";
|
|
325
|
+
failureCode = error instanceof StorageMigrationFailure ? error.code : "backup_failed";
|
|
305
326
|
throw error;
|
|
306
327
|
});
|
|
307
328
|
await writeReceipt(path, receipt);
|