@nowcrew/daemon 0.5.34 → 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/README.md +46 -0
- package/dist/atomic-private-write.js +18 -0
- package/dist/computer-profile.js +3 -17
- package/dist/execution-protocol.js +7 -1
- package/dist/execution-runner.js +2 -0
- package/dist/local-executor.js +65 -2
- package/dist/machine-info.js +5 -1
- package/dist/memory-prune-diagnostics.js +57 -0
- package/dist/project-skills/agent-projection-coordinator.js +10 -0
- package/dist/project-skills/controller.js +167 -0
- package/dist/project-skills/reconciler.js +116 -0
- package/dist/project-skills/registry.js +113 -0
- package/dist/project-skills/scanner.js +126 -0
- package/dist/project-skills/types.js +12 -0
- package/dist/promise-tail.js +25 -0
- package/dist/runner.js +4 -0
- package/dist/runtimes/claude.js +2 -0
- package/dist/runtimes/codex-app-server-runner.js +10 -1
- package/dist/scheduled-report.js +3 -0
- package/dist/serve.js +188 -10
- package/dist/skill-frontmatter.js +23 -0
- package/dist/skills.js +2 -19
- package/dist/supervised-runtime.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,6 +166,52 @@ Old daemons that omit `executionRuntimes` are conservatively interpreted as the
|
|
|
166
166
|
CLIs and server-supported adapters. Unknown required protocol semantics are rejected before side effects.
|
|
167
167
|
Protocol-0 `agent:start` remains only for the server-governed compatibility window.
|
|
168
168
|
|
|
169
|
+
## Project Skills
|
|
170
|
+
|
|
171
|
+
On macOS and Linux, an owner or admin can register multiple local project repositories from a Computer
|
|
172
|
+
profile and bind selected Skills to individual Agents. The daemon keeps the only durable copy of each
|
|
173
|
+
absolute path in:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
<CREW_AGENTS_ROOT>/.crew/projects.json
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Each project is scanned at `<project>/.agents/skills/*/SKILL.md`. The server stores only a bounded,
|
|
180
|
+
path-free inventory and logical `(projectId, skillName)` bindings. The local path crosses the server only
|
|
181
|
+
while an add command is forwarded to the selected online Computer; it is not written to the workspace
|
|
182
|
+
database, structured logs, inventory frames, or responses. Project Skills are disabled on Windows.
|
|
183
|
+
|
|
184
|
+
One binding set is projected into both Runtime discovery layouts:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
<agentsRoot>/<handle>/.agents/skills/<name> # Codex symlink
|
|
188
|
+
<agentsRoot>/<handle>/.crew/claude-skills/.claude/skills/<name> # Claude symlink
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The two complete directories switch under one per-Agent reconcile/launch lock. A failed switch restores
|
|
192
|
+
the prior complete projection; a missing project or Skill removes only that link and does not block other
|
|
193
|
+
work. These are ordinary writable symlinks, not snapshots or a sandbox: an Agent running as the daemon
|
|
194
|
+
user can modify the source Skill through them. That write-through risk is intentionally accepted.
|
|
195
|
+
|
|
196
|
+
Codex protocol-v1 task directories are opaque
|
|
197
|
+
`<agentsRoot>/<handle>/tasks/<safe-prefix>-<sha256>` paths. The daemon writes `.nowwork-root` at the Agent
|
|
198
|
+
root and adds it to Codex `project_root_markers`, so upward discovery reaches `.agents/skills` without
|
|
199
|
+
changing the task cwd. Claude receives
|
|
200
|
+
`--add-dir <agentsRoot>/<handle>/.crew/claude-skills`; Claude treats those Skills as `projectSettings`,
|
|
201
|
+
so a machine policy that disables project settings also disables this source even when the projection is
|
|
202
|
+
healthy.
|
|
203
|
+
|
|
204
|
+
Existing user-level discovery is unchanged. Claude may still load `~/.claude/skills`; Codex may still
|
|
205
|
+
load `$CODEX_HOME/skills`. The DeepSeek Codex variant uses its Agent-private `.codex-deepseek` home.
|
|
206
|
+
Project bindings do not hide or rewrite any of those sources.
|
|
207
|
+
|
|
208
|
+
Binding changes reconcile immediately when the Computer is online. On reconnect the server sends the
|
|
209
|
+
complete binding set for every Agent assigned to that Computer, including empty sets that clear stale
|
|
210
|
+
links left by offline unbinds. Every non-empty execution snapshot is also reconciled immediately before
|
|
211
|
+
Runtime launch, which recovers from a missed notification or daemon restart. Skill file content changes
|
|
212
|
+
are visible through the symlink immediately; adding, deleting, renaming, or changing frontmatter requires
|
|
213
|
+
a rescan. Web descriptions remain at the last inventory value until that rescan.
|
|
214
|
+
|
|
169
215
|
## Reliability
|
|
170
216
|
|
|
171
217
|
Protocol-v1 lifecycle is `accepted → started → completed`, followed by a server
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
export async function atomicPrivateWrite(path, content, beforeCommit) {
|
|
5
|
+
const directory = dirname(path);
|
|
6
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
7
|
+
await chmod(directory, 0o700);
|
|
8
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
9
|
+
try {
|
|
10
|
+
await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
11
|
+
await beforeCommit?.(temporary);
|
|
12
|
+
await rename(temporary, path);
|
|
13
|
+
await chmod(path, 0o600);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await rm(temporary, { force: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/computer-profile.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { access,
|
|
1
|
+
import { access, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import {
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { posix, resolve, win32 } from "node:path";
|
|
7
6
|
import { spawn } from "node:child_process";
|
|
8
7
|
import { z } from "zod";
|
|
9
8
|
import { acquireProfileSaveLock, } from "./computer-profile-lock.js";
|
|
10
9
|
import { formatDaemonText } from "./i18n.js";
|
|
10
|
+
import { atomicPrivateWrite } from "./atomic-private-write.js";
|
|
11
11
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
12
12
|
const ServerUrlSchema = z.string().url().max(2048).refine((value) => {
|
|
13
13
|
const url = new URL(value);
|
|
@@ -130,20 +130,6 @@ export function validateProfileName(name) {
|
|
|
130
130
|
export function profilePath(name, home = daemonHome()) {
|
|
131
131
|
return resolve(home, "profiles", `${validateProfileName(name)}.json`);
|
|
132
132
|
}
|
|
133
|
-
async function atomicPrivateWrite(path, content, beforeCommit) {
|
|
134
|
-
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
135
|
-
await chmod(dirname(path), 0o700);
|
|
136
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
137
|
-
try {
|
|
138
|
-
await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
139
|
-
await beforeCommit?.(temporary);
|
|
140
|
-
await rename(temporary, path);
|
|
141
|
-
await chmod(path, 0o600);
|
|
142
|
-
}
|
|
143
|
-
finally {
|
|
144
|
-
await rm(temporary, { force: true });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
133
|
const DPAPI_PROTECT = "$p=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($p);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Convert]::ToBase64String($e)";
|
|
148
134
|
const DPAPI_UNPROTECT = "$p=[Console]::In.ReadToEnd();$b=[Convert]::FromBase64String($p);$d=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($d))";
|
|
149
135
|
async function powershellStdin(script, input) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { MAX_AGENT_PROJECT_SKILL_BINDINGS } from "./project-skills/types.js";
|
|
2
3
|
const ExecutionIdSchema = z.string().uuid();
|
|
3
4
|
const ProtocolVersionSchema = z.literal(1);
|
|
4
5
|
const TimestampSchema = z.string().datetime({ offset: true });
|
|
@@ -10,13 +11,17 @@ const ExitCodeSchema = z.number().int().min(MIN_SIGNED_32_INTEGER).max(MAX_PG_IN
|
|
|
10
11
|
const RuntimeSchema = z.enum(["claude", "codex", "kimi"]);
|
|
11
12
|
const UnsafePathCharacterSchema = /[\p{Cc}<>:"/\\|?*]/u;
|
|
12
13
|
const WindowsReservedNameSchema = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
|
|
13
|
-
const AgentHandleSchema = z.string().min(1).max(64).refine((handle) => handle !== "."
|
|
14
|
+
export const AgentHandleSchema = z.string().min(1).max(64).refine((handle) => handle !== "."
|
|
14
15
|
&& handle !== ".."
|
|
15
16
|
&& !UnsafePathCharacterSchema.test(handle)
|
|
16
17
|
&& !WindowsReservedNameSchema.test(handle)
|
|
17
18
|
&& !handle.endsWith(".")
|
|
18
19
|
&& !handle.endsWith(" ")
|
|
19
20
|
&& Buffer.byteLength(handle, "utf8") <= 255, "agent handle must be a cross-platform safe filesystem component");
|
|
21
|
+
const ProjectSkillRefSchema = z.object({
|
|
22
|
+
projectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/u),
|
|
23
|
+
skillName: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/u),
|
|
24
|
+
}).strict();
|
|
20
25
|
export const ReasoningSchema = z.enum([
|
|
21
26
|
"default",
|
|
22
27
|
"none",
|
|
@@ -99,6 +104,7 @@ export const ExecutionStartSchema = z.object({
|
|
|
99
104
|
id: z.string().min(1),
|
|
100
105
|
handle: AgentHandleSchema,
|
|
101
106
|
memoryEnabled: z.boolean().optional(),
|
|
107
|
+
projectSkills: z.array(ProjectSkillRefSchema).max(MAX_AGENT_PROJECT_SKILL_BINDINGS).optional(),
|
|
102
108
|
}).strict(),
|
|
103
109
|
workspace: z.object({
|
|
104
110
|
taskKey: z.string().min(1).max(200),
|
package/dist/execution-runner.js
CHANGED
|
@@ -505,6 +505,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
505
505
|
...(dependencies.startupTimeoutMs === undefined
|
|
506
506
|
? {}
|
|
507
507
|
: { startupTimeoutMs: dependencies.startupTimeoutMs }),
|
|
508
|
+
...(dependencies.projectSkills === undefined ? {} : { projectSkills: dependencies.projectSkills }),
|
|
508
509
|
launchRuntime: async (request) => {
|
|
509
510
|
if (launchClosed || dependencies.cancellation?.isRequested())
|
|
510
511
|
throw new ExecutionCancelledError();
|
|
@@ -572,6 +573,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
572
573
|
...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
|
|
573
574
|
...(spec.context.wakeMessageId === undefined ? {} : { wakeMessageId: spec.context.wakeMessageId }),
|
|
574
575
|
...(spec.context.attachments === undefined ? {} : { attachments: spec.context.attachments }),
|
|
576
|
+
...(spec.agent.projectSkills === undefined ? {} : { projectSkills: spec.agent.projectSkills }),
|
|
575
577
|
systemPrompt: boundedSystemPrompt,
|
|
576
578
|
wakePrompt: spec.instructions.wakePrompt,
|
|
577
579
|
runtime: {
|
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 "";
|
|
@@ -116,6 +129,9 @@ async function launchLegacyRuntime(request) {
|
|
|
116
129
|
...common,
|
|
117
130
|
systemPromptPath: request.systemPromptPath,
|
|
118
131
|
wakePrompt: request.wakePrompt,
|
|
132
|
+
...(request.agentRoot === undefined ? {} : {
|
|
133
|
+
projectSkillsDirectory: join(request.agentRoot, ".crew", "claude-skills"),
|
|
134
|
+
}),
|
|
119
135
|
...(request.sessionId === undefined ? {} : {
|
|
120
136
|
sessionId: request.sessionId,
|
|
121
137
|
resume: request.resume,
|
|
@@ -201,6 +217,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
201
217
|
let materialized = null;
|
|
202
218
|
let knownAttachmentDirectory = null;
|
|
203
219
|
let startupReservation = null;
|
|
220
|
+
let memoryPruneTraceId = null;
|
|
221
|
+
let memoryPruneRuntimeExitCode;
|
|
222
|
+
let memoryPruneExecutorCompleted = false;
|
|
223
|
+
let memoryPruneFailurePhase = "diagnostics_before";
|
|
204
224
|
try {
|
|
205
225
|
if (isDeepSeekCodex && !providerConfig.providerApiKey) {
|
|
206
226
|
throw new Error("DeepSeek API key is not configured for this Agent");
|
|
@@ -265,7 +285,20 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
265
285
|
}
|
|
266
286
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
267
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";
|
|
268
300
|
await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
|
|
301
|
+
memoryPruneFailurePhase = "runtime_prepare";
|
|
269
302
|
const inheritedEnv = { ...process.env };
|
|
270
303
|
for (const key of Object.keys(inheritedEnv)) {
|
|
271
304
|
if (key.startsWith("CREW_AGENT_MEMORY_"))
|
|
@@ -318,10 +351,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
318
351
|
}
|
|
319
352
|
}
|
|
320
353
|
const runtimeLaunchAt = Date.now();
|
|
321
|
-
|
|
354
|
+
memoryPruneFailurePhase = "runtime_launch";
|
|
355
|
+
const launchRequest = {
|
|
322
356
|
runtime: runtime.name,
|
|
323
357
|
bin: runtime.name,
|
|
324
358
|
cwd: workspace.runDir,
|
|
359
|
+
...(input.projectSkills === undefined ? {} : { agentRoot: workspace.dir }),
|
|
325
360
|
systemPromptPath: workspace.systemPromptPath,
|
|
326
361
|
systemPrompt,
|
|
327
362
|
wakePrompt,
|
|
@@ -334,7 +369,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
334
369
|
...(attachmentPlan.nativeImagePaths.length > 0
|
|
335
370
|
? { imagePaths: attachmentPlan.nativeImagePaths }
|
|
336
371
|
: {}),
|
|
337
|
-
}
|
|
372
|
+
};
|
|
373
|
+
const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
|
|
374
|
+
? await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, () => launchRuntime(launchRequest))
|
|
375
|
+
: await launchRuntime(launchRequest);
|
|
376
|
+
memoryPruneFailurePhase = "runtime_execution";
|
|
338
377
|
if (child.cancel !== undefined) {
|
|
339
378
|
dependencies.cancellation?.register(child.cancel);
|
|
340
379
|
}
|
|
@@ -449,6 +488,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
449
488
|
throw error;
|
|
450
489
|
}
|
|
451
490
|
const { exitCode, spawnError, terminationSignal } = runtimeExit;
|
|
491
|
+
memoryPruneRuntimeExitCode = exitCode;
|
|
452
492
|
const errorTail = [
|
|
453
493
|
stderrTail.trim(),
|
|
454
494
|
spawnError,
|
|
@@ -463,6 +503,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
463
503
|
}
|
|
464
504
|
}
|
|
465
505
|
if (input.session.enabled && supportsNativeResume && sessionId) {
|
|
506
|
+
memoryPruneFailurePhase = "session_finalize";
|
|
466
507
|
const contextTokens = usage
|
|
467
508
|
? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
|
|
468
509
|
: (resuming ? prior?.contextTokens : undefined);
|
|
@@ -476,6 +517,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
476
517
|
...(contextTokens === undefined ? {} : { contextTokens }),
|
|
477
518
|
});
|
|
478
519
|
}
|
|
520
|
+
memoryPruneExecutorCompleted = true;
|
|
479
521
|
return {
|
|
480
522
|
workspaceRunDir: workspace.runDir,
|
|
481
523
|
exitCode,
|
|
@@ -498,6 +540,27 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
498
540
|
}
|
|
499
541
|
finally {
|
|
500
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
|
+
}
|
|
501
564
|
const attachmentDirectories = new Set([
|
|
502
565
|
...(knownAttachmentDirectory === null ? [] : [knownAttachmentDirectory]),
|
|
503
566
|
...(materialized === null ? [] : [materialized.directory]),
|
package/dist/machine-info.js
CHANGED
|
@@ -27,7 +27,11 @@ export const DAEMON_CAPABILITIES = [
|
|
|
27
27
|
"execution_answer_stream_v1",
|
|
28
28
|
"execution_machine_queue_v1",
|
|
29
29
|
"execution_agent_memory_policy_v1",
|
|
30
|
+
"project_skills_v1",
|
|
30
31
|
];
|
|
32
|
+
export const daemonCapabilities = (runtimePlatform = process.platform) => runtimePlatform === "darwin" || runtimePlatform === "linux"
|
|
33
|
+
? DAEMON_CAPABILITIES
|
|
34
|
+
: DAEMON_CAPABILITIES.filter((capability) => capability !== "project_skills_v1");
|
|
31
35
|
export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
|
|
32
36
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|
|
33
37
|
const RUNTIME_BINS = [
|
|
@@ -117,7 +121,7 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
|
|
|
117
121
|
daemonVersion: daemonVersion(),
|
|
118
122
|
runtimes,
|
|
119
123
|
executionRuntimes,
|
|
120
|
-
capabilities: [...
|
|
124
|
+
capabilities: [...daemonCapabilities(runtimePlatform), ...additionalCapabilities],
|
|
121
125
|
...(backend.supported ? {
|
|
122
126
|
executionProtocol: EXECUTION_PROTOCOL,
|
|
123
127
|
executionLimits: Object.freeze({
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createKeyedPromiseTail } from "../promise-tail.js";
|
|
2
|
+
export function createAgentProjectionCoordinator() {
|
|
3
|
+
const tails = createKeyedPromiseTail();
|
|
4
|
+
return {
|
|
5
|
+
runExclusive(agentsRoot, handle, operation) {
|
|
6
|
+
const key = JSON.stringify([agentsRoot, handle]);
|
|
7
|
+
return tails.enqueue(key, operation);
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { scanProjects } from "./scanner.js";
|
|
3
|
+
import { ProjectRegistryError } from "./registry.js";
|
|
4
|
+
import { isProjectId, isProjectSkillName, MAX_AGENT_PROJECT_SKILL_BINDINGS, } from "./types.js";
|
|
5
|
+
import { AgentHandleSchema } from "../execution-protocol.js";
|
|
6
|
+
import { createPromiseTail } from "../promise-tail.js";
|
|
7
|
+
const ProjectIdSchema = z.string().refine(isProjectId);
|
|
8
|
+
const ProjectCommandSchema = z.discriminatedUnion("type", [
|
|
9
|
+
z.object({
|
|
10
|
+
type: z.literal("project:add"),
|
|
11
|
+
reqId: z.string().min(1).max(128),
|
|
12
|
+
projectId: ProjectIdSchema,
|
|
13
|
+
root: z.string().min(1).max(4_096),
|
|
14
|
+
}).strict(),
|
|
15
|
+
z.object({
|
|
16
|
+
type: z.literal("project:remove"),
|
|
17
|
+
reqId: z.string().min(1).max(128),
|
|
18
|
+
projectId: ProjectIdSchema,
|
|
19
|
+
}).strict(),
|
|
20
|
+
z.object({
|
|
21
|
+
type: z.literal("project:rescan"),
|
|
22
|
+
reqId: z.string().min(1).max(128),
|
|
23
|
+
projectId: ProjectIdSchema,
|
|
24
|
+
}).strict(),
|
|
25
|
+
z.object({
|
|
26
|
+
type: z.literal("agent:skills:sync"),
|
|
27
|
+
reqId: z.string().min(1).max(128),
|
|
28
|
+
handle: AgentHandleSchema,
|
|
29
|
+
bindings: z.array(z.object({
|
|
30
|
+
projectId: ProjectIdSchema,
|
|
31
|
+
skillName: z.string().refine(isProjectSkillName),
|
|
32
|
+
}).strict()).max(MAX_AGENT_PROJECT_SKILL_BINDINGS),
|
|
33
|
+
}).strict(),
|
|
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
|
+
};
|
|
59
|
+
export function createProjectSkillsController(deps) {
|
|
60
|
+
let scanned = Object.freeze([]);
|
|
61
|
+
const operationTail = createPromiseTail();
|
|
62
|
+
let initialization = null;
|
|
63
|
+
let initializationState = "idle";
|
|
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;
|
|
75
|
+
};
|
|
76
|
+
const publishCurrent = async () => {
|
|
77
|
+
await deps.publish(Object.freeze({
|
|
78
|
+
type: "machine:projects",
|
|
79
|
+
projects: Object.freeze(scanned.map((project) => project.inventory)),
|
|
80
|
+
}));
|
|
81
|
+
};
|
|
82
|
+
const enqueue = (operation) => operationTail.enqueue(operation);
|
|
83
|
+
const initialize = () => {
|
|
84
|
+
if (initializationState === "ready" && initialization !== null)
|
|
85
|
+
return initialization;
|
|
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,
|
|
100
|
+
publishCurrent: () => enqueue(publishCurrent),
|
|
101
|
+
scannedProjects: () => scanned,
|
|
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 {
|
|
111
|
+
return { ok: false, error: "project_skills_unavailable" };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return enqueue(async () => {
|
|
115
|
+
const command = parsed.data;
|
|
116
|
+
try {
|
|
117
|
+
if (command.type === "agent:skills:sync") {
|
|
118
|
+
if (deps.reconcile === undefined)
|
|
119
|
+
return { ok: false, error: "skill_projection_failed" };
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
data: { bindings: await deps.reconcile(command.handle, command.bindings) },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (command.type === "project:add") {
|
|
126
|
+
const existed = (await deps.registry.list())
|
|
127
|
+
.some((project) => project.projectId === command.projectId);
|
|
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;
|
|
136
|
+
}
|
|
137
|
+
else if (command.type === "project:remove") {
|
|
138
|
+
await deps.registry.remove(command.projectId);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const projects = await deps.registry.list();
|
|
142
|
+
if (!projects.some((project) => project.projectId === command.projectId)) {
|
|
143
|
+
return { ok: false, error: "project_not_found" };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (command.type !== "project:add") {
|
|
147
|
+
await refresh(command.type === "project:rescan" ? command.projectId : undefined);
|
|
148
|
+
}
|
|
149
|
+
await publishCurrent();
|
|
150
|
+
return { ok: true, data: { projectId: command.projectId } };
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
error: error instanceof ProjectRegistryError
|
|
156
|
+
? error.code
|
|
157
|
+
: error.code === "skill_name_conflict"
|
|
158
|
+
? "skill_name_conflict"
|
|
159
|
+
: error.code === "skill_projection_failed"
|
|
160
|
+
? "skill_projection_failed"
|
|
161
|
+
: "project_operation_failed",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|