@nowcrew/daemon 0.5.40 → 0.5.42
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 +5 -3
- package/dist/daemon-update-eligibility.js +20 -10
- package/dist/legacy-service-installation.js +56 -0
- package/dist/local-executor.js +146 -26
- package/dist/memory-prune-diagnostics.js +140 -12
- package/dist/prompt.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,8 +105,10 @@ CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon doctor --profile dev
|
|
|
105
105
|
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon status --profile dev
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
If the descriptor has drifted
|
|
109
|
-
|
|
108
|
+
If the target descriptor has drifted or the profile is stopped, the daemon fails closed and remains
|
|
109
|
+
manually upgradeable. On macOS, another unregistered legacy LaunchAgent may coexist only when its plist
|
|
110
|
+
exactly matches a generated NowCrew service and resolves to a different npm prefix. An unverifiable or
|
|
111
|
+
same-prefix descriptor still fails closed.
|
|
110
112
|
|
|
111
113
|
The daemon advertises `daemon_update_v1` only when all of these conditions hold:
|
|
112
114
|
|
|
@@ -115,7 +117,7 @@ The daemon advertises `daemon_update_v1` only when all of these conditions hold:
|
|
|
115
117
|
| Platform | macOS LaunchAgent or Linux systemd user service |
|
|
116
118
|
| Entrypoint | global `@nowcrew/daemon/dist/main.js`, not npx or source/tsx |
|
|
117
119
|
| Startup | `crew-daemon serve --profile <name>` through the installed service |
|
|
118
|
-
| Registry |
|
|
120
|
+
| Registry | registered descriptors remain conflict-free; the exact target legacy descriptor may be adopted once; an exact macOS legacy descriptor on another npm prefix may coexist |
|
|
119
121
|
| Identity | service ID, descriptor, daemon home, Agent root, entrypoint, package root, and npm prefix match |
|
|
120
122
|
| Service | the selected profile is running and holds the installation lease |
|
|
121
123
|
| Install root | a standard writable Unix global npm prefix can be derived from the running entrypoint |
|
|
@@ -6,6 +6,7 @@ import { builtDaemonEntry } from "./computer-cli.js";
|
|
|
6
6
|
import { buildServiceSpec, readServiceDescriptor, serviceStatus, } from "./computer-service.js";
|
|
7
7
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
8
8
|
import { daemonInstallationLeaseHeld } from "./daemon-installation-lease.js";
|
|
9
|
+
import { inspectLegacyServiceInstallation } from "./legacy-service-installation.js";
|
|
9
10
|
import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
|
|
10
11
|
function defaults() {
|
|
11
12
|
return {
|
|
@@ -22,8 +23,25 @@ function defaults() {
|
|
|
22
23
|
serviceStatus,
|
|
23
24
|
assertWritable: (path) => access(path, constants.W_OK),
|
|
24
25
|
installationLeaseHeld: daemonInstallationLeaseHeld,
|
|
26
|
+
inspectLegacyDescriptorInstallation: (descriptorPath) => inspectLegacyServiceInstallation(descriptorPath, process.platform, homedir(), process.getuid?.()),
|
|
25
27
|
};
|
|
26
28
|
}
|
|
29
|
+
async function registryCoversUpdateScope(services, descriptorPaths, targetDescriptorPath, npmPrefix, inspectLegacyDescriptorInstallation) {
|
|
30
|
+
for (const descriptorPath of descriptorPaths) {
|
|
31
|
+
if (descriptorPath === targetDescriptorPath)
|
|
32
|
+
continue;
|
|
33
|
+
try {
|
|
34
|
+
assertRegistryCoversDescriptors(services, [descriptorPath]);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
const legacy = await inspectLegacyDescriptorInstallation(descriptorPath).catch(() => null);
|
|
39
|
+
if (legacy === null || legacy.npmPrefix === npmPrefix)
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
27
45
|
export async function detectDaemonUpdateEligibility(profileName, overrides = {}) {
|
|
28
46
|
const deps = { ...defaults(), ...overrides };
|
|
29
47
|
if (deps.platform !== "darwin" && deps.platform !== "linux") {
|
|
@@ -65,10 +83,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
65
83
|
const registered = services.find((record) => record.serviceId === spec.id
|
|
66
84
|
|| (record.daemonHome === deps.profileHome && record.profile === profileName));
|
|
67
85
|
if (registered !== undefined) {
|
|
68
|
-
|
|
69
|
-
assertRegistryCoversDescriptors(services, await deps.listManagedDescriptorPaths());
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
86
|
+
if (!await registryCoversUpdateScope(services, await deps.listManagedDescriptorPaths(), spec.descriptorPath, installation.npmPrefix, deps.inspectLegacyDescriptorInstallation)) {
|
|
72
87
|
return { eligible: false, reason: "managed_service_registry_incomplete" };
|
|
73
88
|
}
|
|
74
89
|
const descriptor = await deps.readServiceDescriptor(spec).catch(() => null);
|
|
@@ -109,12 +124,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
109
124
|
if (legacyDescriptor !== legacySpec.descriptor) {
|
|
110
125
|
return { eligible: false, reason: "managed_service_not_registered" };
|
|
111
126
|
}
|
|
112
|
-
|
|
113
|
-
const descriptorPaths = await deps.listManagedDescriptorPaths();
|
|
114
|
-
const targetPath = legacySpec.descriptorPath;
|
|
115
|
-
assertRegistryCoversDescriptors(services, descriptorPaths.filter((descriptorPath) => descriptorPath !== targetPath));
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
127
|
+
if (!await registryCoversUpdateScope(services, await deps.listManagedDescriptorPaths(), legacySpec.descriptorPath, installation.npmPrefix, deps.inspectLegacyDescriptorInstallation)) {
|
|
118
128
|
return { eligible: false, reason: "managed_service_registry_incomplete" };
|
|
119
129
|
}
|
|
120
130
|
status = await deps.serviceStatus(legacySpec);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { buildServiceSpec, systemCommandRunner, } from "./computer-service.js";
|
|
6
|
+
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
7
|
+
const LegacyLaunchAgentSchema = z.object({
|
|
8
|
+
Label: z.string().min(1),
|
|
9
|
+
ProgramArguments: z.tuple([
|
|
10
|
+
z.string().refine(isAbsolute),
|
|
11
|
+
z.string().refine(isAbsolute),
|
|
12
|
+
z.literal("serve"),
|
|
13
|
+
z.literal("--profile"),
|
|
14
|
+
z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/),
|
|
15
|
+
z.literal("--daemon-home"),
|
|
16
|
+
z.string().refine(isAbsolute),
|
|
17
|
+
]),
|
|
18
|
+
KeepAlive: z.literal(true),
|
|
19
|
+
ProcessType: z.literal("Background"),
|
|
20
|
+
}).strict();
|
|
21
|
+
export async function inspectLegacyServiceInstallation(descriptorPath, platform, userHome = homedir(), uid = process.getuid?.(), runner = systemCommandRunner) {
|
|
22
|
+
if (platform !== "darwin" || uid === undefined)
|
|
23
|
+
return null;
|
|
24
|
+
const result = await runner("/usr/bin/plutil", ["-convert", "json", "-o", "-", descriptorPath]);
|
|
25
|
+
if (result.exitCode !== 0)
|
|
26
|
+
return null;
|
|
27
|
+
let decoded;
|
|
28
|
+
try {
|
|
29
|
+
decoded = JSON.parse(result.stdout);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const parsed = LegacyLaunchAgentSchema.safeParse(decoded);
|
|
35
|
+
if (!parsed.success)
|
|
36
|
+
return null;
|
|
37
|
+
const [nodePath, entryPath, , , profile, , profileHome] = parsed.data.ProgramArguments;
|
|
38
|
+
const installation = daemonGlobalInstallation(entryPath, platform);
|
|
39
|
+
if (installation === null)
|
|
40
|
+
return null;
|
|
41
|
+
const spec = buildServiceSpec({
|
|
42
|
+
platform,
|
|
43
|
+
profile,
|
|
44
|
+
userHome,
|
|
45
|
+
uid,
|
|
46
|
+
nodePath,
|
|
47
|
+
entryPath,
|
|
48
|
+
profileHome,
|
|
49
|
+
legacyServiceId: true,
|
|
50
|
+
});
|
|
51
|
+
if (spec.id !== parsed.data.Label || spec.descriptorPath !== descriptorPath)
|
|
52
|
+
return null;
|
|
53
|
+
if (await readFile(descriptorPath, "utf8").catch(() => null) !== spec.descriptor)
|
|
54
|
+
return null;
|
|
55
|
+
return { npmPrefix: installation.npmPrefix };
|
|
56
|
+
}
|
package/dist/local-executor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
|
-
import { rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { delimiter, join } from "node:path";
|
|
4
|
-
import { prepareWorkspace, rotateAgentSession, } from "./workspace.js";
|
|
4
|
+
import { prepareWorkspace, rotateAgentSession, safeKey, } from "./workspace.js";
|
|
5
5
|
import { spawnClaude } from "./runtimes/claude.js";
|
|
6
6
|
import { spawnCodex } from "./runtimes/codex.js";
|
|
7
7
|
import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
|
|
@@ -18,7 +18,7 @@ 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 {
|
|
21
|
+
import { evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
|
|
22
22
|
function memoryPruneSnapshotFields(snapshot) {
|
|
23
23
|
const fields = {};
|
|
24
24
|
for (const [label, fact] of Object.entries(snapshot)) {
|
|
@@ -28,9 +28,25 @@ function memoryPruneSnapshotFields(snapshot) {
|
|
|
28
28
|
fields[`${label}_sha256`] = fact.sha256;
|
|
29
29
|
fields[`${label}_hash_skipped_reason`] = fact.hash_skipped_reason;
|
|
30
30
|
fields[`${label}_error`] = fact.error;
|
|
31
|
+
fields[`${label}_markdown_heading_count`] = fact.markdown_heading_count;
|
|
32
|
+
fields[`${label}_lessons_reference_count`] = fact.lessons_reference_count;
|
|
33
|
+
fields[`${label}_same_as_shared_path`] = fact.same_as_shared_path;
|
|
31
34
|
}
|
|
32
35
|
return fields;
|
|
33
36
|
}
|
|
37
|
+
function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
|
|
38
|
+
const errorCode = error?.code;
|
|
39
|
+
dslog("memory_prune.diagnostics_failed", "长期记忆收尾诊断失败", {
|
|
40
|
+
level: "WARN",
|
|
41
|
+
execution_id: input.executionId,
|
|
42
|
+
agent_handle: input.handle,
|
|
43
|
+
task_key: input.taskKey,
|
|
44
|
+
prune_trace_id: traceId,
|
|
45
|
+
diagnostics_phase: phase,
|
|
46
|
+
error_name: error instanceof Error ? error.name : "unknown",
|
|
47
|
+
error_code: errorCode,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
34
50
|
function truncateUtf8(value, maxBytes) {
|
|
35
51
|
if (maxBytes <= 0)
|
|
36
52
|
return "";
|
|
@@ -62,6 +78,7 @@ export function withLocalExecutionFacts(serverPrompt, maxBytes) {
|
|
|
62
78
|
};
|
|
63
79
|
}
|
|
64
80
|
const STDERR_TAIL_CAP = 2_000;
|
|
81
|
+
const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
|
|
65
82
|
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
66
83
|
const RESERVED_ENV = new Set([
|
|
67
84
|
"PATH",
|
|
@@ -158,6 +175,7 @@ function resolvePrompt(prompt, context) {
|
|
|
158
175
|
return typeof prompt === "string" ? prompt : prompt(context);
|
|
159
176
|
}
|
|
160
177
|
const nativeSessionLeaseTails = new Map();
|
|
178
|
+
const activeMemoryPrunes = new Map();
|
|
161
179
|
async function withKeyedLease(key, operation, cancellation) {
|
|
162
180
|
const predecessor = nativeSessionLeaseTails.get(key) ?? Promise.resolve();
|
|
163
181
|
let release;
|
|
@@ -221,6 +239,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
221
239
|
let memoryPruneRuntimeExitCode;
|
|
222
240
|
let memoryPruneExecutorCompleted = false;
|
|
223
241
|
let memoryPruneFailurePhase = "diagnostics_before";
|
|
242
|
+
let memoryPruneBeforeSnapshot = null;
|
|
243
|
+
let memoryPruneSharedWriteKey = null;
|
|
244
|
+
let executionWorkspace = workspace;
|
|
224
245
|
try {
|
|
225
246
|
if (isDeepSeekCodex && !providerConfig.providerApiKey) {
|
|
226
247
|
throw new Error("DeepSeek API key is not configured for this Agent");
|
|
@@ -230,24 +251,62 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
230
251
|
}
|
|
231
252
|
const supportsNativeResume = runtime.name === "claude"
|
|
232
253
|
|| (dependencies.launchRuntime !== undefined && runtimeCapability(runtime.name).nativeResume);
|
|
233
|
-
const
|
|
254
|
+
const currentPrior = input.session.enabled && supportsNativeResume
|
|
234
255
|
? await readSession(workspace.sessionDir)
|
|
235
256
|
: null;
|
|
236
|
-
|
|
237
|
-
|
|
257
|
+
// Interactive V1 uses collision-resistant session directories, while the
|
|
258
|
+
// older legacy path stored metadata under tasks/<safe thread key>. Import
|
|
259
|
+
// that metadata once so an existing thread can cross the protocol boundary
|
|
260
|
+
// without losing its native runtime conversation.
|
|
261
|
+
const legacyRunDir = input.session.enabled
|
|
262
|
+
&& supportsNativeResume
|
|
263
|
+
&& input.keyMode === "opaque"
|
|
264
|
+
&& input.resumeKey !== undefined
|
|
265
|
+
? join(input.launch.agentsRoot, input.handle, "tasks", safeKey(input.resumeKey))
|
|
266
|
+
: null;
|
|
267
|
+
const legacyCwdMarker = legacyRunDir === null
|
|
268
|
+
? null
|
|
269
|
+
: join(workspace.sessionDir, ".legacy-runtime-cwd");
|
|
270
|
+
const legacyCwdPinned = legacyCwdMarker !== null
|
|
271
|
+
&& await readFile(legacyCwdMarker, "utf8").then((value) => value === "1").catch(() => false);
|
|
272
|
+
const legacyPrior = legacyRunDir === null || (currentPrior !== null && !legacyCwdPinned)
|
|
273
|
+
? null
|
|
274
|
+
: await readSession(legacyRunDir);
|
|
275
|
+
const prior = currentPrior ?? legacyPrior;
|
|
276
|
+
const sessionExists = workspace.sessionResume || legacyPrior !== null;
|
|
277
|
+
const usesLegacyCwd = legacyRunDir !== null
|
|
278
|
+
&& (legacyCwdPinned || (currentPrior === null && legacyPrior !== null));
|
|
279
|
+
executionWorkspace = !usesLegacyCwd || legacyRunDir === null
|
|
280
|
+
? workspace
|
|
281
|
+
: {
|
|
282
|
+
...workspace,
|
|
283
|
+
runDir: legacyRunDir,
|
|
284
|
+
workLogPath: join(legacyRunDir, "work-log.md"),
|
|
285
|
+
workLog: await readFile(join(legacyRunDir, "work-log.md"), "utf8").catch(() => ""),
|
|
286
|
+
};
|
|
287
|
+
// Claude Code accepts --resume and --model together. Compare against the
|
|
288
|
+
// prior model only when this turn has an explicit model, so the remaining
|
|
289
|
+
// safety checks can still decide whether the same session is reusable.
|
|
290
|
+
// An omitted model stays model-sensitive because it cannot reliably switch
|
|
291
|
+
// a resumed session back to the runtime default.
|
|
292
|
+
const resumeComparisonModel = runtime.name === "claude" && currentModel !== null
|
|
293
|
+
? prior?.model ?? currentModel
|
|
294
|
+
: currentModel;
|
|
295
|
+
const resumeSessionId = supportsNativeResume && sessionExists
|
|
296
|
+
? pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, input.session.budgetTokens, input.session.maxTurns, providerFp)
|
|
238
297
|
: null;
|
|
239
298
|
const resuming = resumeSessionId !== null;
|
|
240
|
-
const rotatedForBudget = !resuming && supportsNativeResume &&
|
|
241
|
-
&& pickResumeId(prior, Date.now(), input.session.warmMs,
|
|
299
|
+
const rotatedForBudget = !resuming && supportsNativeResume && sessionExists
|
|
300
|
+
&& pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
242
301
|
const nearBudget = resuming && isNearBudget(prior, input.session.softTokens);
|
|
243
|
-
const rotated = Boolean(workspace.agentSessionId &&
|
|
302
|
+
const rotated = Boolean(workspace.agentSessionId && sessionExists && !resuming);
|
|
244
303
|
// Codex and Kimi choose their own id on a fresh start, so the native id persisted in
|
|
245
304
|
// .crew-session.json can differ from the bootstrap id in .session. Resume the former.
|
|
246
305
|
const launchSessionId = resumeSessionId ?? (rotated
|
|
247
306
|
? await rotateAgentSession(workspace.sessionDir)
|
|
248
307
|
: workspace.agentSessionId);
|
|
249
308
|
const promptContext = {
|
|
250
|
-
workspace,
|
|
309
|
+
workspace: executionWorkspace,
|
|
251
310
|
resuming,
|
|
252
311
|
rotatedForBudget,
|
|
253
312
|
nearBudget,
|
|
@@ -256,12 +315,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
256
315
|
if (input.attachments && input.attachments.length > 0) {
|
|
257
316
|
if (dependencies.cancellation?.isRequested())
|
|
258
317
|
throw new RuntimeCancelledError();
|
|
259
|
-
knownAttachmentDirectory = executionAttachmentDirectory(
|
|
318
|
+
knownAttachmentDirectory = executionAttachmentDirectory(executionWorkspace.runDir, input.executionId);
|
|
260
319
|
const controller = new AbortController();
|
|
261
320
|
const materialization = Promise.resolve().then(() => (dependencies.materializeAttachments ?? materializeAttachments)({
|
|
262
321
|
serverUrl: input.launch.serverUrl,
|
|
263
322
|
token: input.launch.token,
|
|
264
|
-
runDir:
|
|
323
|
+
runDir: executionWorkspace.runDir,
|
|
265
324
|
executionId: input.executionId,
|
|
266
325
|
attachments: input.attachments,
|
|
267
326
|
signal: controller.signal,
|
|
@@ -287,14 +346,36 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
287
346
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
288
347
|
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
289
348
|
if (memoryPruneTraceId !== null) {
|
|
290
|
-
|
|
291
|
-
|
|
349
|
+
memoryPruneSharedWriteKey = JSON.stringify([input.launch.agentsRoot, input.handle]);
|
|
350
|
+
const activePruneCount = (activeMemoryPrunes.get(memoryPruneSharedWriteKey) ?? 0) + 1;
|
|
351
|
+
activeMemoryPrunes.set(memoryPruneSharedWriteKey, activePruneCount);
|
|
352
|
+
dslog(activePruneCount > 1
|
|
353
|
+
? "memory_prune.shared_write_overlap_detected"
|
|
354
|
+
: "memory_prune.shared_write_started", activePruneCount > 1
|
|
355
|
+
? "同一 Agent 存在重叠的共享记忆收尾"
|
|
356
|
+
: "Agent 共享记忆收尾已开始", {
|
|
357
|
+
...(activePruneCount > 1 ? { level: "WARN" } : {}),
|
|
292
358
|
execution_id: input.executionId,
|
|
293
359
|
agent_handle: input.handle,
|
|
294
360
|
task_key: input.taskKey,
|
|
295
361
|
prune_trace_id: memoryPruneTraceId,
|
|
296
|
-
|
|
362
|
+
active_prune_count: activePruneCount,
|
|
297
363
|
});
|
|
364
|
+
try {
|
|
365
|
+
const snapshot = await inspectMemoryPruneFilesWithinDeadline(executionWorkspace.dir, executionWorkspace.runDir, executionWorkspace.workLogPath, dependencies.memoryPruneDiagnosticsTimeoutMs
|
|
366
|
+
?? MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS);
|
|
367
|
+
memoryPruneBeforeSnapshot = snapshot;
|
|
368
|
+
dslog("memory_prune.files_before", "长期记忆收尾执行前文件指纹", {
|
|
369
|
+
execution_id: input.executionId,
|
|
370
|
+
agent_handle: input.handle,
|
|
371
|
+
task_key: input.taskKey,
|
|
372
|
+
prune_trace_id: memoryPruneTraceId,
|
|
373
|
+
...memoryPruneSnapshotFields(snapshot),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
logMemoryPruneDiagnosticsFailure(input, memoryPruneTraceId, "before", error);
|
|
378
|
+
}
|
|
298
379
|
}
|
|
299
380
|
memoryPruneFailurePhase = "prompt_write";
|
|
300
381
|
await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
|
|
@@ -313,7 +394,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
313
394
|
CREW_TOKEN: input.launch.token,
|
|
314
395
|
CREW_CHANNEL: input.channelId,
|
|
315
396
|
CREW_HOME: workspace.dir,
|
|
316
|
-
CREW_TASK_LOG:
|
|
397
|
+
CREW_TASK_LOG: executionWorkspace.workLogPath,
|
|
317
398
|
...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
|
|
318
399
|
XDG_CONFIG_HOME: join(workspace.homeDir, ".config"),
|
|
319
400
|
XDG_DATA_HOME: join(workspace.homeDir, ".local", "share"),
|
|
@@ -355,7 +436,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
355
436
|
const launchRequest = {
|
|
356
437
|
runtime: runtime.name,
|
|
357
438
|
bin: runtime.name,
|
|
358
|
-
cwd:
|
|
439
|
+
cwd: executionWorkspace.runDir,
|
|
359
440
|
...(input.projectSkills === undefined ? {} : { agentRoot: workspace.dir }),
|
|
360
441
|
systemPromptPath: workspace.systemPromptPath,
|
|
361
442
|
systemPrompt,
|
|
@@ -516,10 +597,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
516
597
|
lastExitOk: exitCode === 0,
|
|
517
598
|
...(contextTokens === undefined ? {} : { contextTokens }),
|
|
518
599
|
});
|
|
600
|
+
if (exitCode === 0 && usesLegacyCwd && !legacyCwdPinned && legacyCwdMarker !== null) {
|
|
601
|
+
await writeFile(legacyCwdMarker, "1", "utf8");
|
|
602
|
+
}
|
|
519
603
|
}
|
|
520
604
|
memoryPruneExecutorCompleted = true;
|
|
521
605
|
return {
|
|
522
|
-
workspaceRunDir:
|
|
606
|
+
workspaceRunDir: executionWorkspace.runDir,
|
|
523
607
|
exitCode,
|
|
524
608
|
...(terminationSignal === undefined ? {} : { terminationSignal }),
|
|
525
609
|
activities,
|
|
@@ -541,24 +625,60 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
541
625
|
finally {
|
|
542
626
|
startupReservation?.release();
|
|
543
627
|
if (memoryPruneTraceId !== null) {
|
|
544
|
-
|
|
545
|
-
|
|
628
|
+
try {
|
|
629
|
+
const snapshot = await inspectMemoryPruneFilesWithinDeadline(executionWorkspace.dir, executionWorkspace.runDir, executionWorkspace.workLogPath, dependencies.memoryPruneDiagnosticsTimeoutMs
|
|
630
|
+
?? MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS);
|
|
631
|
+
dslog("memory_prune.files_after", "长期记忆收尾执行后文件指纹", {
|
|
632
|
+
execution_id: input.executionId,
|
|
633
|
+
agent_handle: input.handle,
|
|
634
|
+
task_key: input.taskKey,
|
|
635
|
+
prune_trace_id: memoryPruneTraceId,
|
|
636
|
+
runtime_exit_code: memoryPruneRuntimeExitCode,
|
|
637
|
+
executor_outcome: memoryPruneExecutorCompleted ? "succeeded" : "failed",
|
|
638
|
+
...memoryPruneSnapshotFields(snapshot),
|
|
639
|
+
});
|
|
640
|
+
if (memoryPruneBeforeSnapshot !== null) {
|
|
641
|
+
const postcondition = evaluateMemoryPrunePostcondition(memoryPruneBeforeSnapshot, snapshot, {
|
|
642
|
+
executorCompleted: memoryPruneExecutorCompleted,
|
|
643
|
+
...(memoryPruneRuntimeExitCode === undefined
|
|
644
|
+
? {}
|
|
645
|
+
: { runtimeExitCode: memoryPruneRuntimeExitCode }),
|
|
646
|
+
});
|
|
647
|
+
dslog("memory_prune.postcondition_evaluated", "长期记忆收尾后置条件已评估", {
|
|
648
|
+
execution_id: input.executionId,
|
|
649
|
+
agent_handle: input.handle,
|
|
650
|
+
task_key: input.taskKey,
|
|
651
|
+
prune_trace_id: memoryPruneTraceId,
|
|
652
|
+
postcondition_outcome: postcondition.outcome,
|
|
653
|
+
...Object.fromEntries(Object.entries(postcondition).filter(([key]) => key !== "outcome")),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
logMemoryPruneDiagnosticsFailure(input, memoryPruneTraceId, "after", error);
|
|
659
|
+
}
|
|
660
|
+
dslog("memory_prune.execution_completed", "长期记忆收尾执行结束", {
|
|
546
661
|
execution_id: input.executionId,
|
|
547
662
|
agent_handle: input.handle,
|
|
548
663
|
task_key: input.taskKey,
|
|
549
664
|
prune_trace_id: memoryPruneTraceId,
|
|
550
|
-
runtime_exit_code: memoryPruneRuntimeExitCode,
|
|
551
665
|
executor_outcome: memoryPruneExecutorCompleted ? "succeeded" : "failed",
|
|
552
|
-
|
|
666
|
+
runtime_exit_code: memoryPruneRuntimeExitCode,
|
|
667
|
+
...(memoryPruneExecutorCompleted ? {} : { failure_phase: memoryPruneFailurePhase }),
|
|
553
668
|
});
|
|
554
|
-
|
|
669
|
+
}
|
|
670
|
+
if (memoryPruneSharedWriteKey !== null) {
|
|
671
|
+
const activePruneCount = Math.max(0, (activeMemoryPrunes.get(memoryPruneSharedWriteKey) ?? 1) - 1);
|
|
672
|
+
if (activePruneCount === 0)
|
|
673
|
+
activeMemoryPrunes.delete(memoryPruneSharedWriteKey);
|
|
674
|
+
else
|
|
675
|
+
activeMemoryPrunes.set(memoryPruneSharedWriteKey, activePruneCount);
|
|
676
|
+
dslog("memory_prune.shared_write_finished", "Agent 共享记忆收尾已结束", {
|
|
555
677
|
execution_id: input.executionId,
|
|
556
678
|
agent_handle: input.handle,
|
|
557
679
|
task_key: input.taskKey,
|
|
558
680
|
prune_trace_id: memoryPruneTraceId,
|
|
559
|
-
|
|
560
|
-
runtime_exit_code: memoryPruneRuntimeExitCode,
|
|
561
|
-
...(memoryPruneExecutorCompleted ? {} : { failure_phase: memoryPruneFailurePhase }),
|
|
681
|
+
active_prune_count: activePruneCount,
|
|
562
682
|
});
|
|
563
683
|
}
|
|
564
684
|
const attachmentDirectories = new Set([
|
|
@@ -2,23 +2,36 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
3
|
import { stat } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { StringDecoder } from "node:string_decoder";
|
|
5
6
|
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
7
|
export const MEMORY_PRUNE_HASH_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
7
8
|
export function parseMemoryPruneTraceId(wakePrompt) {
|
|
8
9
|
return TRACE_PATTERN.exec(wakePrompt)?.[1] ?? null;
|
|
9
10
|
}
|
|
10
|
-
async function inspectFile(path) {
|
|
11
|
+
async function inspectFile(path, markdownFacts = false, sameAsSharedPath, signal) {
|
|
11
12
|
let metadata;
|
|
12
13
|
try {
|
|
13
14
|
metadata = await stat(path);
|
|
14
15
|
}
|
|
15
16
|
catch (error) {
|
|
16
17
|
const code = error.code;
|
|
18
|
+
const pathFact = sameAsSharedPath === undefined
|
|
19
|
+
? {}
|
|
20
|
+
: { same_as_shared_path: sameAsSharedPath };
|
|
17
21
|
if (code === "ENOENT")
|
|
18
|
-
return { exists: false };
|
|
19
|
-
return {
|
|
22
|
+
return { exists: false, ...pathFact };
|
|
23
|
+
return {
|
|
24
|
+
exists: false,
|
|
25
|
+
...pathFact,
|
|
26
|
+
error: code ?? (error instanceof Error ? error.name : "unknown"),
|
|
27
|
+
};
|
|
20
28
|
}
|
|
21
|
-
const fact = {
|
|
29
|
+
const fact = {
|
|
30
|
+
exists: true,
|
|
31
|
+
size: metadata.size,
|
|
32
|
+
mtime_ms: metadata.mtimeMs,
|
|
33
|
+
...(sameAsSharedPath === undefined ? {} : { same_as_shared_path: sameAsSharedPath }),
|
|
34
|
+
};
|
|
22
35
|
if (!metadata.isFile())
|
|
23
36
|
return { ...fact, hash_skipped_reason: "not_regular_file" };
|
|
24
37
|
if (metadata.size > MEMORY_PRUNE_HASH_LIMIT_BYTES) {
|
|
@@ -27,7 +40,29 @@ async function inspectFile(path) {
|
|
|
27
40
|
try {
|
|
28
41
|
const hash = createHash("sha256");
|
|
29
42
|
let bytesRead = 0;
|
|
30
|
-
|
|
43
|
+
let markdownHeadingCount = 0;
|
|
44
|
+
let lessonsReferenceCount = 0;
|
|
45
|
+
let pendingText = "";
|
|
46
|
+
const decoder = new StringDecoder("utf8");
|
|
47
|
+
const inspectText = (text, final) => {
|
|
48
|
+
if (!markdownFacts)
|
|
49
|
+
return;
|
|
50
|
+
pendingText += text;
|
|
51
|
+
const lines = pendingText.split(/\r?\n/);
|
|
52
|
+
pendingText = final ? "" : (lines.pop() ?? "");
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (/^\s{0,3}#{1,6}(?:\s|$)/.test(line))
|
|
55
|
+
markdownHeadingCount += 1;
|
|
56
|
+
lessonsReferenceCount += line.match(/notes\/lessons\.md/gi)?.length ?? 0;
|
|
57
|
+
}
|
|
58
|
+
if (final && pendingText !== "") {
|
|
59
|
+
if (/^\s{0,3}#{1,6}(?:\s|$)/.test(pendingText))
|
|
60
|
+
markdownHeadingCount += 1;
|
|
61
|
+
lessonsReferenceCount += pendingText.match(/notes\/lessons\.md/gi)?.length ?? 0;
|
|
62
|
+
pendingText = "";
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const stream = createReadStream(path, signal === undefined ? {} : { signal });
|
|
31
66
|
try {
|
|
32
67
|
for await (const chunk of stream) {
|
|
33
68
|
bytesRead += chunk.length;
|
|
@@ -35,23 +70,116 @@ async function inspectFile(path) {
|
|
|
35
70
|
return { ...fact, hash_skipped_reason: "grew_too_large" };
|
|
36
71
|
}
|
|
37
72
|
hash.update(chunk);
|
|
73
|
+
inspectText(decoder.write(chunk), false);
|
|
38
74
|
}
|
|
75
|
+
inspectText(decoder.end(), true);
|
|
39
76
|
}
|
|
40
77
|
finally {
|
|
41
78
|
stream.destroy();
|
|
42
79
|
}
|
|
43
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
...fact,
|
|
82
|
+
sha256: hash.digest("hex"),
|
|
83
|
+
...(markdownFacts ? {
|
|
84
|
+
markdown_heading_count: markdownHeadingCount,
|
|
85
|
+
lessons_reference_count: lessonsReferenceCount,
|
|
86
|
+
} : {}),
|
|
87
|
+
};
|
|
44
88
|
}
|
|
45
89
|
catch (error) {
|
|
46
90
|
const code = error.code;
|
|
47
91
|
return { ...fact, error: code ?? (error instanceof Error ? error.name : "unknown") };
|
|
48
92
|
}
|
|
49
93
|
}
|
|
50
|
-
export async function inspectMemoryPruneFiles(homeDir, workLogPath) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
inspectFile(join(homeDir, "
|
|
54
|
-
inspectFile(
|
|
94
|
+
export async function inspectMemoryPruneFiles(homeDir, runDir, workLogPath, signal) {
|
|
95
|
+
const cwdIsHome = runDir === homeDir;
|
|
96
|
+
const [memory, lessons, cwdMemory, cwdLessons, workLog] = await Promise.all([
|
|
97
|
+
inspectFile(join(homeDir, "MEMORY.md"), true, undefined, signal),
|
|
98
|
+
inspectFile(join(homeDir, "notes", "lessons.md"), true, undefined, signal),
|
|
99
|
+
inspectFile(join(runDir, "MEMORY.md"), true, cwdIsHome, signal),
|
|
100
|
+
inspectFile(join(runDir, "notes", "lessons.md"), true, cwdIsHome, signal),
|
|
101
|
+
inspectFile(workLogPath, false, undefined, signal),
|
|
55
102
|
]);
|
|
56
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
memory,
|
|
105
|
+
lessons,
|
|
106
|
+
cwd_memory: cwdMemory,
|
|
107
|
+
cwd_lessons: cwdLessons,
|
|
108
|
+
work_log: workLog,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export async function inspectMemoryPruneFilesWithinDeadline(homeDir, runDir, workLogPath, timeoutMs) {
|
|
112
|
+
return withinMemoryPruneDiagnosticsDeadline((signal) => inspectMemoryPruneFiles(homeDir, runDir, workLogPath, signal), timeoutMs);
|
|
113
|
+
}
|
|
114
|
+
export async function withinMemoryPruneDiagnosticsDeadline(operation, timeoutMs) {
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
let timer;
|
|
117
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
118
|
+
timer = setTimeout(() => {
|
|
119
|
+
const error = Object.assign(new Error("Memory prune diagnostics timed out"), {
|
|
120
|
+
code: "diagnostics_timeout",
|
|
121
|
+
});
|
|
122
|
+
reject(error);
|
|
123
|
+
controller.abort(error);
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
timer.unref?.();
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
return await Promise.race([
|
|
129
|
+
operation(controller.signal),
|
|
130
|
+
deadline,
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
if (timer !== undefined)
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function fileChanged(before, after) {
|
|
139
|
+
if (before.exists !== after.exists)
|
|
140
|
+
return true;
|
|
141
|
+
if (!before.exists && !after.exists)
|
|
142
|
+
return false;
|
|
143
|
+
if (before.sha256 !== undefined && after.sha256 !== undefined) {
|
|
144
|
+
return before.sha256 !== after.sha256;
|
|
145
|
+
}
|
|
146
|
+
return before.size !== after.size || before.mtime_ms !== after.mtime_ms;
|
|
147
|
+
}
|
|
148
|
+
export function evaluateMemoryPrunePostcondition(before, after, execution) {
|
|
149
|
+
const facts = {
|
|
150
|
+
work_log_before_nonempty: before.work_log.exists && (before.work_log.size ?? 0) > 0,
|
|
151
|
+
work_log_cleared: !after.work_log.exists || after.work_log.size === 0,
|
|
152
|
+
lessons_changed: fileChanged(before.lessons, after.lessons),
|
|
153
|
+
shared_memory_changed: fileChanged(before.memory, after.memory),
|
|
154
|
+
shared_memory_has_lessons_reference: (after.memory.lessons_reference_count ?? 0) > 0,
|
|
155
|
+
cwd_memory_changed: !after.cwd_memory.same_as_shared_path
|
|
156
|
+
&& fileChanged(before.cwd_memory, after.cwd_memory),
|
|
157
|
+
cwd_memory_has_lessons_reference: !after.cwd_memory.same_as_shared_path
|
|
158
|
+
&& (after.cwd_memory.lessons_reference_count ?? 0) > 0,
|
|
159
|
+
cwd_lessons_changed: !after.cwd_lessons.same_as_shared_path
|
|
160
|
+
&& fileChanged(before.cwd_lessons, after.cwd_lessons),
|
|
161
|
+
};
|
|
162
|
+
let outcome;
|
|
163
|
+
if (!execution.executorCompleted)
|
|
164
|
+
outcome = "executor_failed";
|
|
165
|
+
else if (execution.runtimeExitCode !== 0)
|
|
166
|
+
outcome = "runtime_failed";
|
|
167
|
+
else if (!before.work_log.exists)
|
|
168
|
+
outcome = "source_work_log_missing";
|
|
169
|
+
else if (!facts.work_log_before_nonempty)
|
|
170
|
+
outcome = "source_work_log_empty";
|
|
171
|
+
else if (!facts.work_log_cleared)
|
|
172
|
+
outcome = "work_log_not_cleared";
|
|
173
|
+
else if (facts.lessons_changed && facts.shared_memory_has_lessons_reference) {
|
|
174
|
+
outcome = "promoted_and_indexed";
|
|
175
|
+
}
|
|
176
|
+
else if ((!facts.shared_memory_changed && facts.cwd_memory_changed)
|
|
177
|
+
|| (!facts.lessons_changed && facts.cwd_lessons_changed)) {
|
|
178
|
+
outcome = "possible_wrong_memory_path";
|
|
179
|
+
}
|
|
180
|
+
else if (facts.lessons_changed)
|
|
181
|
+
outcome = "promoted_without_index_change";
|
|
182
|
+
else
|
|
183
|
+
outcome = "no_files_changed";
|
|
184
|
+
return { outcome, ...facts };
|
|
57
185
|
}
|
package/dist/prompt.js
CHANGED
|
@@ -18,7 +18,8 @@ const EXTERNAL_RESULT_READABILITY = `
|
|
|
18
18
|
- 前三行依次说清结论、影响和需要谁做什么;不要用背景铺垫或复述任务开场。
|
|
19
19
|
- 除非用户明确要求完整明细,正文最多三个短小节、最多六个要点;总结日志、命令输出和长表格,不要整段粘贴。
|
|
20
20
|
- 有足够重点时,只加粗三到五处真正决定性的数字、最终状态、风险、截止时间、负责人或行动项;不足三处时宁缺毋滥。加粗范围要短,不要整段加粗,不要把每个数字都加粗,不得强化未经验证的判断。
|
|
21
|
-
-
|
|
21
|
+
- 需要颜色层级时,只在确有对应语义的短加粗重点前使用一个彩色圆点:🟢 绿色圆点表示已验证成功或健康,🟠 橙色圆点表示风险、临期或需要关注,🔴 红色圆点表示失败、阻塞或严重异常,🔵 蓝色圆点表示负责人、行动项或关键中性数据。每个短加粗重点前最多放一个;不要给普通段落、标题或每个要点都加标记。
|
|
22
|
+
- 使用标题、列表、引用、彩色圆点和加粗形成在企微原生流式消息中稳定可见的层级;不得输出 \`<font>\` 或其它 HTML 标色标签。`;
|
|
22
23
|
export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
|
|
23
24
|
if (workLog.length <= cap)
|
|
24
25
|
return workLog;
|