@nowcrew/daemon 0.5.35 → 0.5.37
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 +69 -9
- package/dist/computer-cli.js +133 -12
- package/dist/computer-service.js +88 -23
- package/dist/daemon-installation-lease.js +86 -0
- package/dist/daemon-installation.js +38 -0
- package/dist/daemon-update-controller.js +30 -2
- package/dist/daemon-update-eligibility.js +70 -39
- package/dist/daemon-updater.js +16 -0
- package/dist/i18n.js +1 -1
- package/dist/local-executor.js +56 -0
- package/dist/main.js +28 -6
- package/dist/managed-service-diagnostics.js +92 -0
- package/dist/managed-service-lifecycle.js +189 -0
- package/dist/managed-service-registry.js +285 -0
- package/dist/managed-service-startup.js +86 -0
- package/dist/memory-prune-diagnostics.js +57 -0
- package/dist/project-skills/controller.js +74 -19
- package/dist/project-skills/registry.js +17 -5
- package/dist/project-skills/scanner.js +34 -11
- package/dist/serve.js +115 -24
- package/package.json +11 -10
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
|
+
function canonicalPath(path, realpath) {
|
|
4
|
+
try {
|
|
5
|
+
return realpath(path);
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
const code = error.code;
|
|
9
|
+
return code === "ENOENT" || code === "ENOTDIR" ? resolve(path) : null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function daemonGlobalInstallation(entryPath, platform, realpath = realpathSync.native) {
|
|
13
|
+
if (platform !== "darwin" && platform !== "linux")
|
|
14
|
+
return null;
|
|
15
|
+
const packageRoot = resolve(dirname(entryPath), "..");
|
|
16
|
+
const globalNodeModules = resolve(packageRoot, "..", "..");
|
|
17
|
+
const libDirectory = dirname(globalNodeModules);
|
|
18
|
+
if (basename(globalNodeModules) !== "node_modules" || basename(libDirectory) !== "lib")
|
|
19
|
+
return null;
|
|
20
|
+
if (resolve(entryPath) !== resolve(packageRoot, "dist", "main.js"))
|
|
21
|
+
return null;
|
|
22
|
+
if (resolve(packageRoot) !== resolve(globalNodeModules, "@nowcrew", "daemon"))
|
|
23
|
+
return null;
|
|
24
|
+
const canonicalPackageRoot = canonicalPath(packageRoot, realpath);
|
|
25
|
+
const canonicalGlobalNodeModules = canonicalPath(globalNodeModules, realpath);
|
|
26
|
+
const canonicalNpmPrefix = canonicalPath(dirname(libDirectory), realpath);
|
|
27
|
+
if (canonicalPackageRoot === null || canonicalGlobalNodeModules === null || canonicalNpmPrefix === null)
|
|
28
|
+
return null;
|
|
29
|
+
if (canonicalPackageRoot !== resolve(canonicalGlobalNodeModules, "@nowcrew", "daemon"))
|
|
30
|
+
return null;
|
|
31
|
+
if (canonicalGlobalNodeModules !== resolve(canonicalNpmPrefix, "lib", "node_modules"))
|
|
32
|
+
return null;
|
|
33
|
+
return {
|
|
34
|
+
packageRoot: canonicalPackageRoot,
|
|
35
|
+
globalNodeModules: canonicalGlobalNodeModules,
|
|
36
|
+
npmPrefix: canonicalNpmPrefix,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -7,6 +7,7 @@ const DaemonUpdateMessageSchema = z.object({
|
|
|
7
7
|
export function createDaemonUpdateController(deps) {
|
|
8
8
|
const handled = new Set();
|
|
9
9
|
let running = null;
|
|
10
|
+
let restartHandoff = null;
|
|
10
11
|
const failed = (updateId, errorCode) => {
|
|
11
12
|
deps.sendStatus({ type: "daemon:update-status", updateId, status: "failed", errorCode });
|
|
12
13
|
};
|
|
@@ -19,6 +20,7 @@ export function createDaemonUpdateController(deps) {
|
|
|
19
20
|
const installed = await deps.install({
|
|
20
21
|
targetVersion: message.targetVersion,
|
|
21
22
|
packageRoot: eligibility.packageRoot,
|
|
23
|
+
npmPrefix: eligibility.npmPrefix,
|
|
22
24
|
onInstalling: () => {
|
|
23
25
|
deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "installing" });
|
|
24
26
|
},
|
|
@@ -27,16 +29,42 @@ export function createDaemonUpdateController(deps) {
|
|
|
27
29
|
failed(message.updateId, installed.errorCode);
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
32
|
+
let released = false;
|
|
33
|
+
const releaseOnce = async () => {
|
|
34
|
+
if (released)
|
|
35
|
+
return;
|
|
36
|
+
released = true;
|
|
37
|
+
await installed.release();
|
|
38
|
+
};
|
|
30
39
|
deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "restarting" });
|
|
31
40
|
try {
|
|
32
|
-
deps.scheduleRestart(eligibility.serviceSpec);
|
|
41
|
+
const handoff = deps.scheduleRestart(eligibility.serviceSpec);
|
|
42
|
+
const state = {
|
|
43
|
+
release: releaseOnce,
|
|
44
|
+
shutdownBegun: false,
|
|
45
|
+
failureTask: Promise.resolve(),
|
|
46
|
+
};
|
|
47
|
+
state.failureTask = handoff.failed.then(async () => {
|
|
48
|
+
if (state.shutdownBegun)
|
|
49
|
+
return;
|
|
50
|
+
await state.release();
|
|
51
|
+
failed(message.updateId, "restart_failed");
|
|
52
|
+
});
|
|
53
|
+
restartHandoff = state;
|
|
33
54
|
}
|
|
34
55
|
catch {
|
|
35
|
-
await
|
|
56
|
+
await releaseOnce();
|
|
36
57
|
failed(message.updateId, "restart_failed");
|
|
37
58
|
}
|
|
38
59
|
};
|
|
39
60
|
return {
|
|
61
|
+
drain: async () => {
|
|
62
|
+
await running?.done;
|
|
63
|
+
if (restartHandoff !== null) {
|
|
64
|
+
restartHandoff.shutdownBegun = true;
|
|
65
|
+
await restartHandoff.release();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
40
68
|
handle: async (input) => {
|
|
41
69
|
const parsed = DaemonUpdateMessageSchema.safeParse(input);
|
|
42
70
|
if (!parsed.success)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { daemonHome, loadProfile, resolveAgentsRoot } from "./computer-profile.js";
|
|
5
|
+
import { builtDaemonEntry } from "./computer-cli.js";
|
|
6
|
+
import { buildServiceSpec, readServiceDescriptor, serviceStatus, } from "./computer-service.js";
|
|
7
|
+
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
8
|
+
import { daemonInstallationLeaseHeld } from "./daemon-installation-lease.js";
|
|
9
|
+
import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
|
|
8
10
|
function defaults() {
|
|
9
|
-
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
10
11
|
return {
|
|
11
12
|
platform: process.platform,
|
|
12
13
|
profileHome: daemonHome(),
|
|
@@ -14,16 +15,13 @@ function defaults() {
|
|
|
14
15
|
uid: process.getuid?.(),
|
|
15
16
|
nodePath: process.execPath,
|
|
16
17
|
entryPath: builtDaemonEntry(),
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
return result.stdout.trim();
|
|
23
|
-
},
|
|
24
|
-
listProfiles,
|
|
18
|
+
loadProfile,
|
|
19
|
+
readManagedServices: () => readManagedServiceRegistry(),
|
|
20
|
+
listManagedDescriptorPaths: () => listManagedServiceDescriptorPaths(process.platform, homedir()),
|
|
21
|
+
readServiceDescriptor,
|
|
25
22
|
serviceStatus,
|
|
26
23
|
assertWritable: (path) => access(path, constants.W_OK),
|
|
24
|
+
installationLeaseHeld: daemonInstallationLeaseHeld,
|
|
27
25
|
};
|
|
28
26
|
}
|
|
29
27
|
export async function detectDaemonUpdateEligibility(profileName, overrides = {}) {
|
|
@@ -33,39 +31,73 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
33
31
|
}
|
|
34
32
|
if (!profileName)
|
|
35
33
|
return { eligible: false, reason: "profile_required" };
|
|
36
|
-
|
|
34
|
+
const installation = daemonGlobalInstallation(deps.entryPath, deps.platform);
|
|
35
|
+
if (installation === null)
|
|
36
|
+
return { eligible: false, reason: "global_install_required" };
|
|
37
|
+
let profile;
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
+
profile = await deps.loadProfile(profileName, deps.profileHome, {
|
|
40
|
+
platform: deps.platform,
|
|
41
|
+
userHome: deps.userHome,
|
|
42
|
+
});
|
|
39
43
|
}
|
|
40
44
|
catch {
|
|
41
|
-
return { eligible: false, reason: "
|
|
45
|
+
return { eligible: false, reason: "managed_service_identity_mismatch" };
|
|
42
46
|
}
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
const spec = buildServiceSpec({
|
|
48
|
+
platform: deps.platform,
|
|
49
|
+
profile: profileName,
|
|
50
|
+
userHome: deps.userHome,
|
|
51
|
+
uid: deps.uid,
|
|
52
|
+
nodePath: deps.nodePath,
|
|
53
|
+
entryPath: deps.entryPath,
|
|
54
|
+
profileHome: deps.profileHome,
|
|
55
|
+
});
|
|
56
|
+
const status = await deps.serviceStatus(spec);
|
|
57
|
+
if (!status.running)
|
|
58
|
+
return { eligible: false, reason: "service_not_running" };
|
|
59
|
+
let services;
|
|
60
|
+
try {
|
|
61
|
+
services = await deps.readManagedServices();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { eligible: false, reason: "managed_service_registry_invalid" };
|
|
45
65
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
const registered = services.find((record) => record.serviceId === spec.id
|
|
67
|
+
|| (record.daemonHome === deps.profileHome && record.profile === profileName));
|
|
68
|
+
if (registered === undefined) {
|
|
69
|
+
return { eligible: false, reason: "managed_service_not_registered" };
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
assertRegistryCoversDescriptors(services, await deps.listManagedDescriptorPaths());
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return { eligible: false, reason: "managed_service_registry_incomplete" };
|
|
76
|
+
}
|
|
77
|
+
const descriptor = await deps.readServiceDescriptor(spec).catch(() => null);
|
|
78
|
+
if (descriptor === null || spec.descriptorPath === null || spec.descriptor === null
|
|
79
|
+
|| !managedServiceIdentityMatches(registered, {
|
|
80
|
+
version: 1,
|
|
50
81
|
platform: deps.platform,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
82
|
+
serviceId: spec.id,
|
|
83
|
+
profile: profileName,
|
|
84
|
+
daemonHome: deps.profileHome,
|
|
85
|
+
agentsRoot: resolveAgentsRoot(profile.agentsRoot, deps.userHome, deps.platform),
|
|
54
86
|
nodePath: deps.nodePath,
|
|
55
87
|
entryPath: deps.entryPath,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
packageRoot: installation.packageRoot,
|
|
89
|
+
npmPrefix: installation.npmPrefix,
|
|
90
|
+
descriptorPath: spec.descriptorPath,
|
|
91
|
+
descriptorSha256: serviceDescriptorSha256(descriptor),
|
|
92
|
+
})
|
|
93
|
+
|| descriptor !== spec.descriptor) {
|
|
94
|
+
return { eligible: false, reason: "managed_service_identity_mismatch" };
|
|
95
|
+
}
|
|
96
|
+
if (!deps.installationLeaseHeld(installation.npmPrefix)) {
|
|
97
|
+
return { eligible: false, reason: "installation_lease_missing" };
|
|
61
98
|
}
|
|
62
|
-
const current = installed.find((entry) => entry.profile === profileName);
|
|
63
|
-
if (!current?.running)
|
|
64
|
-
return { eligible: false, reason: "service_not_running" };
|
|
65
|
-
if (installed.length !== 1)
|
|
66
|
-
return { eligible: false, reason: "multiple_managed_profiles" };
|
|
67
99
|
try {
|
|
68
|
-
await deps.assertWritable(globalNodeModules);
|
|
100
|
+
await deps.assertWritable(installation.globalNodeModules);
|
|
69
101
|
}
|
|
70
102
|
catch {
|
|
71
103
|
return { eligible: false, reason: "global_root_not_writable" };
|
|
@@ -73,8 +105,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
73
105
|
return {
|
|
74
106
|
eligible: true,
|
|
75
107
|
profileName,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
serviceSpec: current.spec,
|
|
108
|
+
...installation,
|
|
109
|
+
serviceSpec: spec,
|
|
79
110
|
};
|
|
80
111
|
}
|
package/dist/daemon-updater.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { systemCommandRunner } from "./computer-service.js";
|
|
4
|
+
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
4
5
|
const RELEASED_VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
5
6
|
async function readPackageVersion(packageRoot) {
|
|
6
7
|
const body = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
|
|
@@ -10,6 +11,12 @@ export async function installExactDaemonUpdate(input) {
|
|
|
10
11
|
if (!RELEASED_VERSION_RE.test(input.targetVersion)) {
|
|
11
12
|
return { ok: false, errorCode: "ineligible" };
|
|
12
13
|
}
|
|
14
|
+
const installation = daemonGlobalInstallation(input.currentEntryPath ?? process.argv[1] ?? "", input.platform ?? process.platform);
|
|
15
|
+
if (installation === null
|
|
16
|
+
|| installation.packageRoot !== input.packageRoot
|
|
17
|
+
|| installation.npmPrefix !== input.npmPrefix) {
|
|
18
|
+
return { ok: false, errorCode: "ineligible" };
|
|
19
|
+
}
|
|
13
20
|
const local = input.localSlots.tryAcquireExclusive();
|
|
14
21
|
if (local === null)
|
|
15
22
|
return { ok: false, errorCode: "runtime_busy" };
|
|
@@ -26,6 +33,13 @@ export async function installExactDaemonUpdate(input) {
|
|
|
26
33
|
await host.release();
|
|
27
34
|
local.release();
|
|
28
35
|
};
|
|
36
|
+
const lockedInstallation = daemonGlobalInstallation(input.currentEntryPath ?? process.argv[1] ?? "", input.platform ?? process.platform);
|
|
37
|
+
if (lockedInstallation === null
|
|
38
|
+
|| lockedInstallation.packageRoot !== input.packageRoot
|
|
39
|
+
|| lockedInstallation.npmPrefix !== input.npmPrefix) {
|
|
40
|
+
await release();
|
|
41
|
+
return { ok: false, errorCode: "ineligible" };
|
|
42
|
+
}
|
|
29
43
|
const runner = input.runner ?? systemCommandRunner;
|
|
30
44
|
try {
|
|
31
45
|
await input.onInstalling?.();
|
|
@@ -37,6 +51,8 @@ export async function installExactDaemonUpdate(input) {
|
|
|
37
51
|
const result = await runner(process.platform === "win32" ? "npm.cmd" : "npm", [
|
|
38
52
|
"install",
|
|
39
53
|
"--global",
|
|
54
|
+
"--prefix",
|
|
55
|
+
input.npmPrefix,
|
|
40
56
|
"--ignore-scripts",
|
|
41
57
|
"--no-audit",
|
|
42
58
|
"--no-fund",
|
package/dist/i18n.js
CHANGED
|
@@ -42,7 +42,7 @@ const zh = {
|
|
|
42
42
|
"Saved profile '{{name}}' with private credentials.": "已保存配置 '{{name}}',凭证仅私有可读。",
|
|
43
43
|
"Service '{{id}}' is not installed": "服务 '{{id}}' 尚未安装",
|
|
44
44
|
"Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.": "daemon 已升级,并已请求重启 '{{name}}';请用 status 确认。",
|
|
45
|
-
"
|
|
45
|
+
"Refused daemon upgrade for '{{name}}': {{reason}}": "已拒绝升级 daemon '{{name}}':{{reason}}",
|
|
46
46
|
"Upgraded daemon. Installed services were not restarted; pass --profile to restart one.": "daemon 已升级;已安装服务尚未重启,可传入 --profile 重启指定服务。",
|
|
47
47
|
"Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry": "服务生命周期必须使用已构建的 daemon 入口(.js),不能使用 TypeScript 开发入口",
|
|
48
48
|
"Installed '{{id}}'. Use status to confirm runtime state.": "已安装 '{{id}}';请用 status 确认运行状态。",
|
package/dist/local-executor.js
CHANGED
|
@@ -18,6 +18,19 @@ import { routeRuntimeAttachments, runtimeCapability, } from "./runtime-capabilit
|
|
|
18
18
|
import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
|
|
19
19
|
import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
|
|
20
20
|
import { dslog } from "./slog.js";
|
|
21
|
+
import { inspectMemoryPruneFiles, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
|
|
22
|
+
function memoryPruneSnapshotFields(snapshot) {
|
|
23
|
+
const fields = {};
|
|
24
|
+
for (const [label, fact] of Object.entries(snapshot)) {
|
|
25
|
+
fields[`${label}_exists`] = fact.exists;
|
|
26
|
+
fields[`${label}_size`] = fact.size;
|
|
27
|
+
fields[`${label}_mtime_ms`] = fact.mtime_ms;
|
|
28
|
+
fields[`${label}_sha256`] = fact.sha256;
|
|
29
|
+
fields[`${label}_hash_skipped_reason`] = fact.hash_skipped_reason;
|
|
30
|
+
fields[`${label}_error`] = fact.error;
|
|
31
|
+
}
|
|
32
|
+
return fields;
|
|
33
|
+
}
|
|
21
34
|
function truncateUtf8(value, maxBytes) {
|
|
22
35
|
if (maxBytes <= 0)
|
|
23
36
|
return "";
|
|
@@ -204,6 +217,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
204
217
|
let materialized = null;
|
|
205
218
|
let knownAttachmentDirectory = null;
|
|
206
219
|
let startupReservation = null;
|
|
220
|
+
let memoryPruneTraceId = null;
|
|
221
|
+
let memoryPruneRuntimeExitCode;
|
|
222
|
+
let memoryPruneExecutorCompleted = false;
|
|
223
|
+
let memoryPruneFailurePhase = "diagnostics_before";
|
|
207
224
|
try {
|
|
208
225
|
if (isDeepSeekCodex && !providerConfig.providerApiKey) {
|
|
209
226
|
throw new Error("DeepSeek API key is not configured for this Agent");
|
|
@@ -268,7 +285,20 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
268
285
|
}
|
|
269
286
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
270
287
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
288
|
+
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
289
|
+
if (memoryPruneTraceId !== null) {
|
|
290
|
+
const snapshot = await inspectMemoryPruneFiles(workspace.dir, workspace.workLogPath);
|
|
291
|
+
dslog("memory_prune.files_before", "长期记忆收尾执行前文件指纹", {
|
|
292
|
+
execution_id: input.executionId,
|
|
293
|
+
agent_handle: input.handle,
|
|
294
|
+
task_key: input.taskKey,
|
|
295
|
+
prune_trace_id: memoryPruneTraceId,
|
|
296
|
+
...memoryPruneSnapshotFields(snapshot),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
memoryPruneFailurePhase = "prompt_write";
|
|
271
300
|
await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
|
|
301
|
+
memoryPruneFailurePhase = "runtime_prepare";
|
|
272
302
|
const inheritedEnv = { ...process.env };
|
|
273
303
|
for (const key of Object.keys(inheritedEnv)) {
|
|
274
304
|
if (key.startsWith("CREW_AGENT_MEMORY_"))
|
|
@@ -321,6 +351,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
321
351
|
}
|
|
322
352
|
}
|
|
323
353
|
const runtimeLaunchAt = Date.now();
|
|
354
|
+
memoryPruneFailurePhase = "runtime_launch";
|
|
324
355
|
const launchRequest = {
|
|
325
356
|
runtime: runtime.name,
|
|
326
357
|
bin: runtime.name,
|
|
@@ -342,6 +373,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
342
373
|
const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
|
|
343
374
|
? await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, () => launchRuntime(launchRequest))
|
|
344
375
|
: await launchRuntime(launchRequest);
|
|
376
|
+
memoryPruneFailurePhase = "runtime_execution";
|
|
345
377
|
if (child.cancel !== undefined) {
|
|
346
378
|
dependencies.cancellation?.register(child.cancel);
|
|
347
379
|
}
|
|
@@ -456,6 +488,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
456
488
|
throw error;
|
|
457
489
|
}
|
|
458
490
|
const { exitCode, spawnError, terminationSignal } = runtimeExit;
|
|
491
|
+
memoryPruneRuntimeExitCode = exitCode;
|
|
459
492
|
const errorTail = [
|
|
460
493
|
stderrTail.trim(),
|
|
461
494
|
spawnError,
|
|
@@ -470,6 +503,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
470
503
|
}
|
|
471
504
|
}
|
|
472
505
|
if (input.session.enabled && supportsNativeResume && sessionId) {
|
|
506
|
+
memoryPruneFailurePhase = "session_finalize";
|
|
473
507
|
const contextTokens = usage
|
|
474
508
|
? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
|
|
475
509
|
: (resuming ? prior?.contextTokens : undefined);
|
|
@@ -483,6 +517,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
483
517
|
...(contextTokens === undefined ? {} : { contextTokens }),
|
|
484
518
|
});
|
|
485
519
|
}
|
|
520
|
+
memoryPruneExecutorCompleted = true;
|
|
486
521
|
return {
|
|
487
522
|
workspaceRunDir: workspace.runDir,
|
|
488
523
|
exitCode,
|
|
@@ -505,6 +540,27 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
505
540
|
}
|
|
506
541
|
finally {
|
|
507
542
|
startupReservation?.release();
|
|
543
|
+
if (memoryPruneTraceId !== null) {
|
|
544
|
+
const snapshot = await inspectMemoryPruneFiles(workspace.dir, workspace.workLogPath);
|
|
545
|
+
dslog("memory_prune.files_after", "长期记忆收尾执行后文件指纹", {
|
|
546
|
+
execution_id: input.executionId,
|
|
547
|
+
agent_handle: input.handle,
|
|
548
|
+
task_key: input.taskKey,
|
|
549
|
+
prune_trace_id: memoryPruneTraceId,
|
|
550
|
+
runtime_exit_code: memoryPruneRuntimeExitCode,
|
|
551
|
+
executor_outcome: memoryPruneExecutorCompleted ? "succeeded" : "failed",
|
|
552
|
+
...memoryPruneSnapshotFields(snapshot),
|
|
553
|
+
});
|
|
554
|
+
dslog("memory_prune.execution_completed", "长期记忆收尾执行结束", {
|
|
555
|
+
execution_id: input.executionId,
|
|
556
|
+
agent_handle: input.handle,
|
|
557
|
+
task_key: input.taskKey,
|
|
558
|
+
prune_trace_id: memoryPruneTraceId,
|
|
559
|
+
executor_outcome: memoryPruneExecutorCompleted ? "succeeded" : "failed",
|
|
560
|
+
runtime_exit_code: memoryPruneRuntimeExitCode,
|
|
561
|
+
...(memoryPruneExecutorCompleted ? {} : { failure_phase: memoryPruneFailurePhase }),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
508
564
|
const attachmentDirectories = new Set([
|
|
509
565
|
...(knownAttachmentDirectory === null ? [] : [knownAttachmentDirectory]),
|
|
510
566
|
...(materialized === null ? [] : [materialized.directory]),
|
package/dist/main.js
CHANGED
|
@@ -14,10 +14,13 @@ import { runAgent } from "./runner.js";
|
|
|
14
14
|
import { serve } from "./serve.js";
|
|
15
15
|
import { initSlog, flushSlog } from "./slog.js";
|
|
16
16
|
import { formatDaemonLogLine } from "./log-format.js";
|
|
17
|
-
import { applyProfileToEnv, assertProfileAgentsRootUnique, daemonHome, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, } from "./computer-profile.js";
|
|
18
|
-
import { runComputerCommand } from "./computer-cli.js";
|
|
17
|
+
import { applyProfileToEnv, assertProfileAgentsRootUnique, daemonHome, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, resolveAgentsRoot, } from "./computer-profile.js";
|
|
18
|
+
import { builtDaemonEntry, runComputerCommand } from "./computer-cli.js";
|
|
19
19
|
import { runServeLifecycle } from "./serve-lifecycle.js";
|
|
20
20
|
import { formatDaemonStartupError } from "./daemon-startup-error.js";
|
|
21
|
+
import { assertManagedServiceStartup } from "./managed-service-startup.js";
|
|
22
|
+
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
23
|
+
import { acquireDaemonInstallationLease, runWithDaemonInstallationLease, } from "./daemon-installation-lease.js";
|
|
21
24
|
async function main() {
|
|
22
25
|
const computerResult = await runComputerCommand(process.argv.slice(2));
|
|
23
26
|
if (computerResult !== null) {
|
|
@@ -57,8 +60,19 @@ async function main() {
|
|
|
57
60
|
if (values.profile) {
|
|
58
61
|
const home = daemonHome();
|
|
59
62
|
const profile = await loadProfile(values.profile, home);
|
|
60
|
-
if (cmd === "serve")
|
|
63
|
+
if (cmd === "serve") {
|
|
61
64
|
await assertProfileAgentsRootUnique(profile, home, homedir());
|
|
65
|
+
await assertManagedServiceStartup({
|
|
66
|
+
platform: process.platform,
|
|
67
|
+
profile: profile.name,
|
|
68
|
+
userHome: homedir(),
|
|
69
|
+
uid: process.getuid?.(),
|
|
70
|
+
daemonHome: home,
|
|
71
|
+
agentsRoot: resolveAgentsRoot(profile.agentsRoot, homedir(), process.platform),
|
|
72
|
+
nodePath: process.execPath,
|
|
73
|
+
entryPath: builtDaemonEntry(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
62
76
|
applyProfileToEnv(profile, process.env);
|
|
63
77
|
}
|
|
64
78
|
// 命令行参数优先于环境变量,填回 env 供 loadConfig 读取
|
|
@@ -80,10 +94,18 @@ async function main() {
|
|
|
80
94
|
}
|
|
81
95
|
if (cmd === "serve") {
|
|
82
96
|
process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
|
|
83
|
-
const
|
|
84
|
-
|
|
97
|
+
const installation = values.profile === undefined
|
|
98
|
+
? null
|
|
99
|
+
: daemonGlobalInstallation(builtDaemonEntry(), process.platform);
|
|
100
|
+
const installationLease = installation === null
|
|
101
|
+
? null
|
|
102
|
+
: await acquireDaemonInstallationLease(installation.npmPrefix);
|
|
103
|
+
await runWithDaemonInstallationLease(installationLease, async () => {
|
|
104
|
+
const service = serve(config, {
|
|
105
|
+
...(values.profile === undefined ? {} : { profileName: values.profile }),
|
|
106
|
+
});
|
|
107
|
+
await runServeLifecycle(service);
|
|
85
108
|
});
|
|
86
|
-
await runServeLifecycle(service);
|
|
87
109
|
return;
|
|
88
110
|
}
|
|
89
111
|
if (!values.agent || !values.channel) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { inspectDaemonInstallationLease } from "./daemon-installation-lease.js";
|
|
2
|
+
import { readServiceDescriptor } from "./computer-service.js";
|
|
3
|
+
import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, } from "./managed-service-registry.js";
|
|
4
|
+
function errorDetail(error) {
|
|
5
|
+
return error instanceof Error ? error.message : String(error);
|
|
6
|
+
}
|
|
7
|
+
export async function inspectManagedServiceDiagnostics(input) {
|
|
8
|
+
const registry = input.registry ?? {};
|
|
9
|
+
const userHome = registry.userHome;
|
|
10
|
+
const readServices = input.readManagedServices
|
|
11
|
+
?? (() => readManagedServiceRegistry(registry));
|
|
12
|
+
const listDescriptors = input.listManagedDescriptorPaths
|
|
13
|
+
?? (() => listManagedServiceDescriptorPaths(input.spec.platform, userHome));
|
|
14
|
+
const readDescriptor = input.readDescriptor
|
|
15
|
+
?? (() => readServiceDescriptor(input.spec));
|
|
16
|
+
const inspectLease = input.inspectInstallationLease
|
|
17
|
+
?? (() => inspectDaemonInstallationLease(input.expectedRecord.npmPrefix, {
|
|
18
|
+
...(userHome === undefined ? {} : { userHome }),
|
|
19
|
+
}));
|
|
20
|
+
let services = null;
|
|
21
|
+
let registryCheck;
|
|
22
|
+
try {
|
|
23
|
+
services = await readServices();
|
|
24
|
+
assertRegistryCoversDescriptors(services, await listDescriptors());
|
|
25
|
+
registryCheck = {
|
|
26
|
+
name: "managed-service-registry",
|
|
27
|
+
ok: true,
|
|
28
|
+
detail: `${services.length} registered service(s); all descriptors are covered`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
registryCheck = {
|
|
33
|
+
name: "managed-service-registry",
|
|
34
|
+
ok: false,
|
|
35
|
+
detail: errorDetail(error),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
let identityCheck;
|
|
39
|
+
const registered = services?.find((record) => record.serviceId === input.expectedRecord.serviceId
|
|
40
|
+
|| (record.daemonHome === input.expectedRecord.daemonHome
|
|
41
|
+
&& record.profile === input.expectedRecord.profile));
|
|
42
|
+
if (registered === undefined) {
|
|
43
|
+
identityCheck = {
|
|
44
|
+
name: "managed-service-identity",
|
|
45
|
+
ok: false,
|
|
46
|
+
detail: services === null
|
|
47
|
+
? "identity unavailable because the registry is invalid"
|
|
48
|
+
: `managed service '${input.expectedRecord.serviceId}' is not registered`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
try {
|
|
53
|
+
const { generation: _generation, createdAt: _createdAt, ...expectedIdentity } = input.expectedRecord;
|
|
54
|
+
const descriptor = await readDescriptor();
|
|
55
|
+
const matches = descriptor === input.spec.descriptor
|
|
56
|
+
&& managedServiceIdentityMatches(registered, expectedIdentity);
|
|
57
|
+
identityCheck = {
|
|
58
|
+
name: "managed-service-identity",
|
|
59
|
+
ok: matches,
|
|
60
|
+
detail: matches
|
|
61
|
+
? `registry and descriptor match '${input.expectedRecord.serviceId}'`
|
|
62
|
+
: `registry or descriptor identity does not match '${input.expectedRecord.serviceId}'`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
identityCheck = {
|
|
67
|
+
name: "managed-service-identity",
|
|
68
|
+
ok: false,
|
|
69
|
+
detail: errorDetail(error),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let installationLease;
|
|
74
|
+
try {
|
|
75
|
+
installationLease = await inspectLease();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
installationLease = { status: "corrupt", detail: errorDetail(error) };
|
|
79
|
+
}
|
|
80
|
+
const leaseCheck = {
|
|
81
|
+
name: "installation-lease",
|
|
82
|
+
ok: installationLease.status === "owned" && installationLease.ownerAlive === true,
|
|
83
|
+
detail: installationLease.detail,
|
|
84
|
+
};
|
|
85
|
+
const checks = [registryCheck, identityCheck, leaseCheck];
|
|
86
|
+
return {
|
|
87
|
+
healthy: checks.every((check) => check.ok),
|
|
88
|
+
npmPrefix: input.expectedRecord.npmPrefix,
|
|
89
|
+
checks,
|
|
90
|
+
installationLease,
|
|
91
|
+
};
|
|
92
|
+
}
|