@nowcrew/daemon 0.5.35 → 0.5.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/local-executor.js +56 -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 +114 -24
- package/package.json +1 -1
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]),
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const TRACE_PATTERN = /\[memory-prune trace_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\]/i;
|
|
6
|
+
export const MEMORY_PRUNE_HASH_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
7
|
+
export function parseMemoryPruneTraceId(wakePrompt) {
|
|
8
|
+
return TRACE_PATTERN.exec(wakePrompt)?.[1] ?? null;
|
|
9
|
+
}
|
|
10
|
+
async function inspectFile(path) {
|
|
11
|
+
let metadata;
|
|
12
|
+
try {
|
|
13
|
+
metadata = await stat(path);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const code = error.code;
|
|
17
|
+
if (code === "ENOENT")
|
|
18
|
+
return { exists: false };
|
|
19
|
+
return { exists: false, error: code ?? (error instanceof Error ? error.name : "unknown") };
|
|
20
|
+
}
|
|
21
|
+
const fact = { exists: true, size: metadata.size, mtime_ms: metadata.mtimeMs };
|
|
22
|
+
if (!metadata.isFile())
|
|
23
|
+
return { ...fact, hash_skipped_reason: "not_regular_file" };
|
|
24
|
+
if (metadata.size > MEMORY_PRUNE_HASH_LIMIT_BYTES) {
|
|
25
|
+
return { ...fact, hash_skipped_reason: "file_too_large" };
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const hash = createHash("sha256");
|
|
29
|
+
let bytesRead = 0;
|
|
30
|
+
const stream = createReadStream(path);
|
|
31
|
+
try {
|
|
32
|
+
for await (const chunk of stream) {
|
|
33
|
+
bytesRead += chunk.length;
|
|
34
|
+
if (bytesRead > MEMORY_PRUNE_HASH_LIMIT_BYTES) {
|
|
35
|
+
return { ...fact, hash_skipped_reason: "grew_too_large" };
|
|
36
|
+
}
|
|
37
|
+
hash.update(chunk);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
stream.destroy();
|
|
42
|
+
}
|
|
43
|
+
return { ...fact, sha256: hash.digest("hex") };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const code = error.code;
|
|
47
|
+
return { ...fact, error: code ?? (error instanceof Error ? error.name : "unknown") };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function inspectMemoryPruneFiles(homeDir, workLogPath) {
|
|
51
|
+
const [memory, lessons, workLog] = await Promise.all([
|
|
52
|
+
inspectFile(join(homeDir, "MEMORY.md")),
|
|
53
|
+
inspectFile(join(homeDir, "notes", "lessons.md")),
|
|
54
|
+
inspectFile(workLogPath),
|
|
55
|
+
]);
|
|
56
|
+
return { memory, lessons, work_log: workLog };
|
|
57
|
+
}
|
|
@@ -32,13 +32,46 @@ const ProjectCommandSchema = z.discriminatedUnion("type", [
|
|
|
32
32
|
}).strict()).max(MAX_AGENT_PROJECT_SKILL_BINDINGS),
|
|
33
33
|
}).strict(),
|
|
34
34
|
]);
|
|
35
|
+
const DEFAULT_PROJECT_SCAN_TIMEOUT_MS = 10_000;
|
|
36
|
+
class ProjectScanTimeoutError extends Error {
|
|
37
|
+
code = "project_scan_timeout";
|
|
38
|
+
constructor() {
|
|
39
|
+
super("project_scan_timeout");
|
|
40
|
+
this.name = "ProjectScanTimeoutError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const withinScanDeadline = async (operation, timeoutMs) => {
|
|
44
|
+
let timer;
|
|
45
|
+
try {
|
|
46
|
+
return await Promise.race([
|
|
47
|
+
operation,
|
|
48
|
+
new Promise((_resolve, reject) => {
|
|
49
|
+
timer = setTimeout(() => reject(new ProjectScanTimeoutError()), timeoutMs);
|
|
50
|
+
timer.unref?.();
|
|
51
|
+
}),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
if (timer !== undefined)
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
35
59
|
export function createProjectSkillsController(deps) {
|
|
36
60
|
let scanned = Object.freeze([]);
|
|
37
61
|
const operationTail = createPromiseTail();
|
|
38
62
|
let initialization = null;
|
|
39
63
|
let initializationState = "idle";
|
|
40
|
-
const
|
|
41
|
-
|
|
64
|
+
const scan = deps.scan ?? scanProjects;
|
|
65
|
+
const scanTimeoutMs = deps.scanTimeoutMs ?? DEFAULT_PROJECT_SCAN_TIMEOUT_MS;
|
|
66
|
+
const scanCurrent = async (deferredProjectId) => withinScanDeadline(scan(await deps.registry.list(), undefined, [
|
|
67
|
+
...scanned
|
|
68
|
+
.map((project) => project.inventory.projectId)
|
|
69
|
+
.filter((projectId) => projectId !== deferredProjectId),
|
|
70
|
+
...(deferredProjectId === undefined ? [] : [deferredProjectId]),
|
|
71
|
+
]), scanTimeoutMs);
|
|
72
|
+
const refresh = async (deferredProjectId) => {
|
|
73
|
+
const next = await scanCurrent(deferredProjectId);
|
|
74
|
+
scanned = next;
|
|
42
75
|
};
|
|
43
76
|
const publishCurrent = async () => {
|
|
44
77
|
await deps.publish(Object.freeze({
|
|
@@ -47,27 +80,38 @@ export function createProjectSkillsController(deps) {
|
|
|
47
80
|
}));
|
|
48
81
|
};
|
|
49
82
|
const enqueue = (operation) => operationTail.enqueue(operation);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (initialization !== null)
|
|
53
|
-
return initialization;
|
|
54
|
-
initializationState = "initializing";
|
|
55
|
-
initialization = enqueue(refresh).then(() => { initializationState = "ready"; }, (error) => {
|
|
56
|
-
initializationState = "failed";
|
|
57
|
-
throw error;
|
|
58
|
-
});
|
|
83
|
+
const initialize = () => {
|
|
84
|
+
if (initializationState === "ready" && initialization !== null)
|
|
59
85
|
return initialization;
|
|
60
|
-
|
|
86
|
+
if (initializationState === "initializing" && initialization !== null)
|
|
87
|
+
return initialization;
|
|
88
|
+
initializationState = "initializing";
|
|
89
|
+
const attempt = enqueue(refresh).then(() => { initializationState = "ready"; }, (error) => {
|
|
90
|
+
initializationState = "failed";
|
|
91
|
+
if (initialization === attempt)
|
|
92
|
+
initialization = null;
|
|
93
|
+
throw error;
|
|
94
|
+
});
|
|
95
|
+
initialization = attempt;
|
|
96
|
+
return attempt;
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
initialize,
|
|
61
100
|
publishCurrent: () => enqueue(publishCurrent),
|
|
62
101
|
scannedProjects: () => scanned,
|
|
63
|
-
handle(input) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
102
|
+
async handle(input) {
|
|
103
|
+
const parsed = ProjectCommandSchema.safeParse(input);
|
|
104
|
+
if (!parsed.success)
|
|
105
|
+
return { ok: false, error: "invalid_project_command" };
|
|
106
|
+
if (parsed.data.type === "agent:skills:sync") {
|
|
107
|
+
try {
|
|
108
|
+
await initialize();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
69
111
|
return { ok: false, error: "project_skills_unavailable" };
|
|
70
112
|
}
|
|
113
|
+
}
|
|
114
|
+
return enqueue(async () => {
|
|
71
115
|
const command = parsed.data;
|
|
72
116
|
try {
|
|
73
117
|
if (command.type === "agent:skills:sync") {
|
|
@@ -79,7 +123,16 @@ export function createProjectSkillsController(deps) {
|
|
|
79
123
|
};
|
|
80
124
|
}
|
|
81
125
|
if (command.type === "project:add") {
|
|
126
|
+
const existed = (await deps.registry.list())
|
|
127
|
+
.some((project) => project.projectId === command.projectId);
|
|
82
128
|
await deps.registry.add(command.projectId, command.root);
|
|
129
|
+
const next = await scanCurrent();
|
|
130
|
+
const added = next.find((project) => project.inventory.projectId === command.projectId);
|
|
131
|
+
if (!existed && added?.inventory.errorCode === "machine_project_skill_limit_exceeded") {
|
|
132
|
+
await deps.registry.remove(command.projectId);
|
|
133
|
+
return { ok: false, error: "machine_project_skill_limit_exceeded" };
|
|
134
|
+
}
|
|
135
|
+
scanned = next;
|
|
83
136
|
}
|
|
84
137
|
else if (command.type === "project:remove") {
|
|
85
138
|
await deps.registry.remove(command.projectId);
|
|
@@ -90,7 +143,9 @@ export function createProjectSkillsController(deps) {
|
|
|
90
143
|
return { ok: false, error: "project_not_found" };
|
|
91
144
|
}
|
|
92
145
|
}
|
|
93
|
-
|
|
146
|
+
if (command.type !== "project:add") {
|
|
147
|
+
await refresh(command.type === "project:rescan" ? command.projectId : undefined);
|
|
148
|
+
}
|
|
94
149
|
await publishCurrent();
|
|
95
150
|
return { ok: true, data: { projectId: command.projectId } };
|
|
96
151
|
}
|
|
@@ -6,8 +6,7 @@ import { createPromiseTail } from "../promise-tail.js";
|
|
|
6
6
|
import { atomicPrivateWrite } from "../atomic-private-write.js";
|
|
7
7
|
const RegistryFileSchema = z.object({
|
|
8
8
|
version: z.literal(1),
|
|
9
|
-
projects: z.record(z.object({ root: z.string().min(1).max(4_096) }).strict())
|
|
10
|
-
.refine((projects) => Object.keys(projects).length <= MAX_MACHINE_PROJECTS),
|
|
9
|
+
projects: z.record(z.object({ root: z.string().min(1).max(4_096) }).strict()),
|
|
11
10
|
}).strict();
|
|
12
11
|
export class ProjectRegistryError extends Error {
|
|
13
12
|
code;
|
|
@@ -17,9 +16,22 @@ export class ProjectRegistryError extends Error {
|
|
|
17
16
|
this.name = "ProjectRegistryError";
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
|
-
const immutableList = (projects) =>
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const immutableList = (projects) => {
|
|
20
|
+
const sorted = Object.entries(projects)
|
|
21
|
+
.map(([projectId, project]) => Object.freeze({ projectId, root: project.root }))
|
|
22
|
+
.sort((left, right) => left.projectId.localeCompare(right.projectId));
|
|
23
|
+
if (sorted.length <= MAX_MACHINE_PROJECTS)
|
|
24
|
+
return Object.freeze(sorted);
|
|
25
|
+
const visible = sorted.slice(0, MAX_MACHINE_PROJECTS);
|
|
26
|
+
const boundary = visible.at(-1);
|
|
27
|
+
if (boundary !== undefined) {
|
|
28
|
+
visible[visible.length - 1] = Object.freeze({
|
|
29
|
+
...boundary,
|
|
30
|
+
errorCode: "project_registry_limit_exceeded",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze(visible);
|
|
34
|
+
};
|
|
23
35
|
export function createProjectRegistry(agentsRoot) {
|
|
24
36
|
const path = join(agentsRoot, ".crew", "projects.json");
|
|
25
37
|
const writeTail = createPromiseTail();
|
|
@@ -15,6 +15,8 @@ const unavailable = (projectId, scannedAt, errorCode) => Object.freeze({
|
|
|
15
15
|
});
|
|
16
16
|
export async function scanProject(project, now = () => new Date()) {
|
|
17
17
|
const scannedAt = now().toISOString();
|
|
18
|
+
if (project.errorCode !== undefined)
|
|
19
|
+
return unavailable(project.projectId, scannedAt, project.errorCode);
|
|
18
20
|
const projectStat = await stat(project.root).catch(() => null);
|
|
19
21
|
if (!projectStat?.isDirectory())
|
|
20
22
|
return unavailable(project.projectId, scannedAt, "project_path_unavailable");
|
|
@@ -51,8 +53,12 @@ export async function scanProject(project, now = () => new Date()) {
|
|
|
51
53
|
continue;
|
|
52
54
|
}
|
|
53
55
|
const frontmatter = parseSkillFrontmatter(markdown);
|
|
54
|
-
const name = frontmatter
|
|
55
|
-
const description = frontmatter
|
|
56
|
+
const name = frontmatter.name;
|
|
57
|
+
const description = frontmatter.description;
|
|
58
|
+
if (name === undefined || description === undefined) {
|
|
59
|
+
invalidSkillCount += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
56
62
|
if (!isProjectSkillName(name) || description.length > MAX_PROJECT_SKILL_DESCRIPTION_LENGTH) {
|
|
57
63
|
invalidSkillCount += 1;
|
|
58
64
|
continue;
|
|
@@ -84,20 +90,37 @@ export async function scanProject(project, now = () => new Date()) {
|
|
|
84
90
|
resolved: Object.freeze(resolved),
|
|
85
91
|
});
|
|
86
92
|
}
|
|
87
|
-
export function boundScannedProjects(projects) {
|
|
93
|
+
export function boundScannedProjects(projects, retainedProjectIds = []) {
|
|
94
|
+
const retainedOrder = new Map(retainedProjectIds.map((projectId, index) => [projectId, index]));
|
|
95
|
+
const allocationOrder = [...projects].sort((left, right) => {
|
|
96
|
+
const leftOrder = retainedOrder.get(left.inventory.projectId);
|
|
97
|
+
const rightOrder = retainedOrder.get(right.inventory.projectId);
|
|
98
|
+
if (leftOrder !== undefined && rightOrder !== undefined)
|
|
99
|
+
return leftOrder - rightOrder;
|
|
100
|
+
if (leftOrder !== undefined)
|
|
101
|
+
return -1;
|
|
102
|
+
if (rightOrder !== undefined)
|
|
103
|
+
return 1;
|
|
104
|
+
return left.inventory.projectId.localeCompare(right.inventory.projectId);
|
|
105
|
+
});
|
|
88
106
|
let skillCount = 0;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
const bounded = new Map();
|
|
108
|
+
for (const project of allocationOrder) {
|
|
109
|
+
if (project.inventory.status !== "available") {
|
|
110
|
+
bounded.set(project.inventory.projectId, project);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
92
113
|
const nextCount = skillCount + project.inventory.skills.length;
|
|
93
114
|
if (nextCount <= MAX_MACHINE_PROJECT_SKILLS) {
|
|
94
115
|
skillCount = nextCount;
|
|
95
|
-
|
|
116
|
+
bounded.set(project.inventory.projectId, project);
|
|
117
|
+
continue;
|
|
96
118
|
}
|
|
97
|
-
|
|
98
|
-
}
|
|
119
|
+
bounded.set(project.inventory.projectId, unavailable(project.inventory.projectId, project.inventory.scannedAt, "machine_project_skill_limit_exceeded"));
|
|
120
|
+
}
|
|
121
|
+
return Object.freeze(projects.map((project) => bounded.get(project.inventory.projectId) ?? project));
|
|
99
122
|
}
|
|
100
|
-
export async function scanProjects(projects, now = () => new Date()) {
|
|
123
|
+
export async function scanProjects(projects, now = () => new Date(), retainedProjectIds = []) {
|
|
101
124
|
const sorted = [...projects].sort((left, right) => left.projectId.localeCompare(right.projectId));
|
|
102
|
-
return boundScannedProjects(await Promise.all(sorted.map((project) => scanProject(project, now))));
|
|
125
|
+
return boundScannedProjects(await Promise.all(sorted.map((project) => scanProject(project, now))), retainedProjectIds);
|
|
103
126
|
}
|
package/dist/serve.js
CHANGED
|
@@ -40,6 +40,7 @@ import { createProjectSkillsController, } from "./project-skills/controller.js";
|
|
|
40
40
|
import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
|
|
41
41
|
import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
|
|
42
42
|
import { createProjectSkillsReconciler, ProjectProjectionError, } from "./project-skills/reconciler.js";
|
|
43
|
+
import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
|
|
43
44
|
// normalize.ts 的活动种类 → activity 枚举
|
|
44
45
|
const ACTIVITY_MAP = {
|
|
45
46
|
init: "working", text: "thinking", reading: "reading", sending: "sending",
|
|
@@ -126,21 +127,58 @@ export function serve(config, opts = {}) {
|
|
|
126
127
|
},
|
|
127
128
|
reconcile: (handle, bindings) => projectSkillsReconciler.reconcile(handle, bindings),
|
|
128
129
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
let projectSkillsStatus = "initializing";
|
|
131
|
+
let projectSkillsInitialization = null;
|
|
132
|
+
let latestMachineHello = null;
|
|
133
|
+
let latestHelloSocket = null;
|
|
134
|
+
const effectiveMachineHello = (hello) => ({
|
|
135
|
+
...hello,
|
|
136
|
+
projectSkillsStatus,
|
|
137
|
+
capabilities: projectSkillsStatus === "ready"
|
|
138
|
+
? hello.capabilities
|
|
139
|
+
: hello.capabilities.filter((capability) => capability !== PROJECT_SKILLS_CAPABILITY),
|
|
134
140
|
});
|
|
135
|
-
|
|
141
|
+
const sendEffectiveMachineHello = () => {
|
|
142
|
+
if (latestMachineHello === null || latestHelloSocket?.readyState !== WebSocket.OPEN)
|
|
143
|
+
return;
|
|
144
|
+
const hello = effectiveMachineHello(latestMachineHello);
|
|
145
|
+
try {
|
|
146
|
+
latestHelloSocket.send(JSON.stringify(hello));
|
|
147
|
+
log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`);
|
|
148
|
+
}
|
|
149
|
+
catch { /* 非 OPEN,忽略 */ }
|
|
150
|
+
};
|
|
151
|
+
const ensureProjectSkillsInitialized = () => {
|
|
152
|
+
if (projectSkillsStatus === "ready")
|
|
153
|
+
return Promise.resolve(true);
|
|
154
|
+
if (projectSkillsInitialization !== null)
|
|
155
|
+
return projectSkillsInitialization;
|
|
156
|
+
projectSkillsStatus = "initializing";
|
|
157
|
+
const attempt = projectSkillsController.initialize().then(() => {
|
|
158
|
+
projectSkillsStatus = "ready";
|
|
159
|
+
return true;
|
|
160
|
+
}, (error) => {
|
|
161
|
+
projectSkillsStatus = "unavailable";
|
|
162
|
+
dslog("project_skills.registry_failed", "本机项目注册表读取失败", {
|
|
163
|
+
level: "ERROR", error_code: error.code ?? "project_registry_corrupt",
|
|
164
|
+
});
|
|
165
|
+
return false;
|
|
166
|
+
}).finally(() => {
|
|
167
|
+
if (projectSkillsInitialization === attempt)
|
|
168
|
+
projectSkillsInitialization = null;
|
|
169
|
+
sendEffectiveMachineHello();
|
|
170
|
+
});
|
|
171
|
+
projectSkillsInitialization = attempt;
|
|
172
|
+
return attempt;
|
|
173
|
+
};
|
|
136
174
|
const initializedProjectSkillsReconciler = {
|
|
137
|
-
reconcile(handle, bindings) {
|
|
138
|
-
return
|
|
175
|
+
async reconcile(handle, bindings) {
|
|
176
|
+
return await ensureProjectSkillsInitialized()
|
|
139
177
|
? projectSkillsReconciler.reconcile(handle, bindings)
|
|
140
178
|
: Promise.reject(new ProjectProjectionError("skill_projection_failed"));
|
|
141
179
|
},
|
|
142
|
-
prepareAndLaunch(agentsRoot, handle, bindings, launch) {
|
|
143
|
-
return
|
|
180
|
+
async prepareAndLaunch(agentsRoot, handle, bindings, launch) {
|
|
181
|
+
return await ensureProjectSkillsInitialized()
|
|
144
182
|
? projectSkillsReconciler.prepareAndLaunch(agentsRoot, handle, bindings, launch)
|
|
145
183
|
: Promise.reject(new ProjectProjectionError("skill_projection_failed"));
|
|
146
184
|
},
|
|
@@ -248,12 +286,13 @@ export function serve(config, opts = {}) {
|
|
|
248
286
|
// scheduled 重复 run 仍由 running 去重;普通同线程 wake 用 legacyTaskTails 串成 FIFO。
|
|
249
287
|
// 每 agent 超过并行上限的任务继续进入 sharedQueues(不丢)。
|
|
250
288
|
const running = new Set();
|
|
289
|
+
const activeExecutionTaskKeys = new Map();
|
|
251
290
|
const legacyTaskTails = new Map();
|
|
252
291
|
const log = (s) => process.stdout.write(formatDaemonLogLine(s) + "\n");
|
|
253
292
|
function connect() {
|
|
254
293
|
if (stopped)
|
|
255
294
|
return;
|
|
256
|
-
const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken, process.platform, undefined,
|
|
295
|
+
const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken, process.platform, undefined, projectSkillsStatus === "ready");
|
|
257
296
|
ws = createWebSocket(wsUrl);
|
|
258
297
|
ws.on("open", () => {
|
|
259
298
|
const openedSocket = ws;
|
|
@@ -282,18 +321,12 @@ export function serve(config, opts = {}) {
|
|
|
282
321
|
});
|
|
283
322
|
void helloPromise
|
|
284
323
|
.then((hello) => {
|
|
324
|
+
if (ws !== openedSocket || openedSocket.readyState !== WebSocket.OPEN)
|
|
325
|
+
return;
|
|
285
326
|
detectedExecutionRuntimes = hello.executionRuntimes;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
...hello,
|
|
290
|
-
capabilities: hello.capabilities.filter((capability) => capability !== PROJECT_SKILLS_CAPABILITY),
|
|
291
|
-
};
|
|
292
|
-
try {
|
|
293
|
-
ws?.send(JSON.stringify(effectiveHello));
|
|
294
|
-
log(`📤 已上报机器信息: ${effectiveHello.hostname} · ${effectiveHello.os} · installed=[${effectiveHello.runtimes.join(",")}] · executable=[${effectiveHello.executionRuntimes.join(",")}] · agents=[${effectiveHello.agentHandles.join(",")}]`);
|
|
295
|
-
}
|
|
296
|
-
catch { /* 非 OPEN,忽略 */ }
|
|
327
|
+
latestMachineHello = hello;
|
|
328
|
+
latestHelloSocket = openedSocket;
|
|
329
|
+
sendEffectiveMachineHello();
|
|
297
330
|
})
|
|
298
331
|
.catch(() => { });
|
|
299
332
|
opts.onOpen?.(ws);
|
|
@@ -440,6 +473,18 @@ export function serve(config, opts = {}) {
|
|
|
440
473
|
}
|
|
441
474
|
return;
|
|
442
475
|
}
|
|
476
|
+
const pruneTraceId = parseMemoryPruneTraceId(spec.instructions.wakePrompt);
|
|
477
|
+
if (pruneTraceId !== null) {
|
|
478
|
+
dslog("memory_prune.received", "Daemon 已收到长期记忆收尾 execution", {
|
|
479
|
+
protocol: "execution_v1",
|
|
480
|
+
prune_trace_id: pruneTraceId,
|
|
481
|
+
execution_id: spec.executionId,
|
|
482
|
+
agent_handle: spec.agent.handle,
|
|
483
|
+
channel_id: spec.context.channelId,
|
|
484
|
+
thread_id: spec.context.threadId ?? null,
|
|
485
|
+
task_key: spec.workspace.taskKey,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
443
488
|
const reservation = sharedSlots.reserve(spec.agent.handle, "execution");
|
|
444
489
|
if (!reservation.accepted) {
|
|
445
490
|
dslog("execution.machine_queue_rejected", "机器执行队列已满", {
|
|
@@ -465,9 +510,41 @@ export function serve(config, opts = {}) {
|
|
|
465
510
|
});
|
|
466
511
|
}
|
|
467
512
|
executionReservations.set(spec.executionId, reservation);
|
|
513
|
+
const executionTaskKey = `${spec.agent.handle}:${spec.workspace.taskKey}`;
|
|
514
|
+
let executionTaskKeyActive = false;
|
|
515
|
+
let executionTaskKeyFinished = false;
|
|
516
|
+
const markExecutionTaskKeyActive = () => {
|
|
517
|
+
if (executionTaskKeyActive || executionTaskKeyFinished)
|
|
518
|
+
return;
|
|
519
|
+
executionTaskKeyActive = true;
|
|
520
|
+
const activeSameTask = (activeExecutionTaskKeys.get(executionTaskKey) ?? 0) + 1;
|
|
521
|
+
activeExecutionTaskKeys.set(executionTaskKey, activeSameTask);
|
|
522
|
+
dslog(activeSameTask > 1 ? "execution.task_key_overlap_detected" : "execution.task_key_started", activeSameTask > 1 ? "同一 Agent 任务键存在重叠 execution" : "Agent 任务键 execution 已开始", {
|
|
523
|
+
...(activeSameTask > 1 ? { level: "WARN" } : {}),
|
|
524
|
+
execution_id: spec.executionId,
|
|
525
|
+
agent_handle: spec.agent.handle,
|
|
526
|
+
task_key: spec.workspace.taskKey,
|
|
527
|
+
active_same_task: activeSameTask,
|
|
528
|
+
...reservation.facts,
|
|
529
|
+
});
|
|
530
|
+
};
|
|
468
531
|
const cancellation = cancellationFor(spec.executionId);
|
|
469
532
|
const cleanupExecutionReservation = () => {
|
|
533
|
+
executionTaskKeyFinished = true;
|
|
470
534
|
reservation.release();
|
|
535
|
+
if (executionTaskKeyActive) {
|
|
536
|
+
const remainingSameTask = Math.max(0, (activeExecutionTaskKeys.get(executionTaskKey) ?? 1) - 1);
|
|
537
|
+
if (remainingSameTask === 0)
|
|
538
|
+
activeExecutionTaskKeys.delete(executionTaskKey);
|
|
539
|
+
else
|
|
540
|
+
activeExecutionTaskKeys.set(executionTaskKey, remainingSameTask);
|
|
541
|
+
dslog("execution.task_key_finished", "Agent 任务键 execution 已结束", {
|
|
542
|
+
execution_id: spec.executionId,
|
|
543
|
+
agent_handle: spec.agent.handle,
|
|
544
|
+
task_key: spec.workspace.taskKey,
|
|
545
|
+
active_same_task: remainingSameTask,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
471
548
|
cancellations.delete(spec.executionId);
|
|
472
549
|
executionReservations.delete(spec.executionId);
|
|
473
550
|
knownExecutionHashes.delete(spec.executionId);
|
|
@@ -506,6 +583,7 @@ export function serve(config, opts = {}) {
|
|
|
506
583
|
slot: {
|
|
507
584
|
state: reservation.state,
|
|
508
585
|
ready: reservation.ready.then(() => {
|
|
586
|
+
markExecutionTaskKeyActive();
|
|
509
587
|
const snapshot = sharedSlots.snapshot();
|
|
510
588
|
dslog("execution.machine_slot_ready", "execution 获得机器执行名额", {
|
|
511
589
|
execution_id: spec.executionId,
|
|
@@ -567,7 +645,7 @@ export function serve(config, opts = {}) {
|
|
|
567
645
|
? r.serverCapabilities.filter((c) => typeof c === "string")
|
|
568
646
|
: []);
|
|
569
647
|
if (serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)) {
|
|
570
|
-
if (await
|
|
648
|
+
if (await ensureProjectSkillsInitialized())
|
|
571
649
|
await projectSkillsController.publishCurrent();
|
|
572
650
|
}
|
|
573
651
|
return;
|
|
@@ -591,6 +669,10 @@ export function serve(config, opts = {}) {
|
|
|
591
669
|
const result = serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)
|
|
592
670
|
? await projectSkillsController.handle(msg)
|
|
593
671
|
: { ok: false, error: "capability_unavailable" };
|
|
672
|
+
if (result.ok && (msg.type === "project:remove"
|
|
673
|
+
|| msg.type === "project:rescan")) {
|
|
674
|
+
void ensureProjectSkillsInitialized();
|
|
675
|
+
}
|
|
594
676
|
try {
|
|
595
677
|
ws?.send(JSON.stringify({ type: "fs:result", reqId: request.reqId, ...result }));
|
|
596
678
|
}
|
|
@@ -671,6 +753,14 @@ export function serve(config, opts = {}) {
|
|
|
671
753
|
run_id: runId, agent_handle: msg.agentHandle, channel_id: msg.channelId,
|
|
672
754
|
thread_id: threadId ?? null, task_key: taskKey,
|
|
673
755
|
};
|
|
756
|
+
const pruneTraceId = parseMemoryPruneTraceId(msg.wake?.content ?? "");
|
|
757
|
+
if (pruneTraceId !== null) {
|
|
758
|
+
dslog("memory_prune.received", "Daemon 已收到长期记忆收尾唤醒", {
|
|
759
|
+
...runKeys,
|
|
760
|
+
protocol: "legacy",
|
|
761
|
+
prune_trace_id: pruneTraceId,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
674
764
|
dslog("run.wake_received", `收到唤醒 ${msg.agentHandle}`, {
|
|
675
765
|
...runKeys, reason: msg.reason ?? "", sender: msg.wake?.senderHandle,
|
|
676
766
|
wake_origin: msg.wake?.origin ?? null,
|
|
@@ -1013,7 +1103,7 @@ export function serve(config, opts = {}) {
|
|
|
1013
1103
|
flush: flushSlog,
|
|
1014
1104
|
writeStderr: (line) => process.stderr.write(line),
|
|
1015
1105
|
});
|
|
1016
|
-
|
|
1106
|
+
void ensureProjectSkillsInitialized();
|
|
1017
1107
|
connect();
|
|
1018
1108
|
})();
|
|
1019
1109
|
const stop = () => {
|