@nowcrew/daemon 0.5.39 → 0.5.41
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 +9 -14
- package/dist/daemon-update-eligibility.js +57 -26
- package/dist/local-executor.js +58 -16
- package/dist/prompt.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,23 +95,18 @@ crew-daemon doctor --profile work
|
|
|
95
95
|
crew-daemon status --profile work
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
For a non-default daemon home, the
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
For a non-default daemon home, the daemon now recognizes the pre-registry service ID when its descriptor
|
|
99
|
+
exactly matches the running profile's node path, package entry, profile name, and `--daemon-home`. No
|
|
100
|
+
uninstall/reinstall migration is required. The first self-update uses that exact legacy service ID for the
|
|
101
|
+
restart; future service lifecycle commands can continue to migrate it explicitly if desired:
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon
|
|
105
|
-
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon
|
|
106
|
-
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon install --profile work
|
|
107
|
-
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon start --profile work
|
|
108
|
-
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon doctor --profile work
|
|
109
|
-
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon status --profile work
|
|
104
|
+
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon doctor --profile dev
|
|
105
|
+
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon status --profile dev
|
|
110
106
|
```
|
|
111
107
|
|
|
112
|
-
If
|
|
113
|
-
|
|
114
|
-
prevent either path from removing a drifted descriptor.
|
|
108
|
+
If the descriptor has drifted, the profile is stopped, or another host descriptor is not registered, the
|
|
109
|
+
daemon fails closed and remains manually upgradeable.
|
|
115
110
|
|
|
116
111
|
The daemon advertises `daemon_update_v1` only when all of these conditions hold:
|
|
117
112
|
|
|
@@ -120,7 +115,7 @@ The daemon advertises `daemon_update_v1` only when all of these conditions hold:
|
|
|
120
115
|
| Platform | macOS LaunchAgent or Linux systemd user service |
|
|
121
116
|
| Entrypoint | global `@nowcrew/daemon/dist/main.js`, not npx or source/tsx |
|
|
122
117
|
| Startup | `crew-daemon serve --profile <name>` through the installed service |
|
|
123
|
-
| Registry | every NowCrew descriptor on the OS user account is registered and conflict-free |
|
|
118
|
+
| Registry | every other NowCrew descriptor on the OS user account is registered and conflict-free; the exact target legacy descriptor may be adopted once |
|
|
124
119
|
| Identity | service ID, descriptor, daemon home, Agent root, entrypoint, package root, and npm prefix match |
|
|
125
120
|
| Service | the selected profile is running and holds the installation lease |
|
|
126
121
|
| Install root | a standard writable Unix global npm prefix can be derived from the running entrypoint |
|
|
@@ -53,9 +53,8 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
53
53
|
entryPath: deps.entryPath,
|
|
54
54
|
profileHome: deps.profileHome,
|
|
55
55
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return { eligible: false, reason: "service_not_running" };
|
|
56
|
+
let selectedSpec = spec;
|
|
57
|
+
let status = await deps.serviceStatus(spec);
|
|
59
58
|
let services;
|
|
60
59
|
try {
|
|
61
60
|
services = await deps.readManagedServices();
|
|
@@ -65,34 +64,66 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
65
64
|
}
|
|
66
65
|
const registered = services.find((record) => record.serviceId === spec.id
|
|
67
66
|
|| (record.daemonHome === deps.profileHome && record.profile === profileName));
|
|
68
|
-
if (registered
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
if (registered !== undefined) {
|
|
68
|
+
try {
|
|
69
|
+
assertRegistryCoversDescriptors(services, await deps.listManagedDescriptorPaths());
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { eligible: false, reason: "managed_service_registry_incomplete" };
|
|
73
|
+
}
|
|
74
|
+
const descriptor = await deps.readServiceDescriptor(spec).catch(() => null);
|
|
75
|
+
if (descriptor === null || spec.descriptorPath === null || spec.descriptor === null
|
|
76
|
+
|| !managedServiceIdentityMatches(registered, {
|
|
77
|
+
version: 1,
|
|
78
|
+
platform: deps.platform,
|
|
79
|
+
serviceId: spec.id,
|
|
80
|
+
profile: profileName,
|
|
81
|
+
daemonHome: deps.profileHome,
|
|
82
|
+
agentsRoot: resolveAgentsRoot(profile.agentsRoot, deps.userHome, deps.platform),
|
|
83
|
+
nodePath: deps.nodePath,
|
|
84
|
+
entryPath: deps.entryPath,
|
|
85
|
+
packageRoot: installation.packageRoot,
|
|
86
|
+
npmPrefix: installation.npmPrefix,
|
|
87
|
+
descriptorPath: spec.descriptorPath,
|
|
88
|
+
descriptorSha256: serviceDescriptorSha256(descriptor),
|
|
89
|
+
})
|
|
90
|
+
|| descriptor !== spec.descriptor) {
|
|
91
|
+
return { eligible: false, reason: "managed_service_identity_mismatch" };
|
|
92
|
+
}
|
|
76
93
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
else {
|
|
95
|
+
// Releases before the managed-service registry used the unsuffixed service id.
|
|
96
|
+
// Adopt only an exact descriptor for this profile; never infer ownership from a
|
|
97
|
+
// running process or accept a descriptor with a different entry/home.
|
|
98
|
+
const legacySpec = buildServiceSpec({
|
|
81
99
|
platform: deps.platform,
|
|
82
|
-
serviceId: spec.id,
|
|
83
100
|
profile: profileName,
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
userHome: deps.userHome,
|
|
102
|
+
uid: deps.uid,
|
|
86
103
|
nodePath: deps.nodePath,
|
|
87
104
|
entryPath: deps.entryPath,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
profileHome: deps.profileHome,
|
|
106
|
+
legacyServiceId: true,
|
|
107
|
+
});
|
|
108
|
+
const legacyDescriptor = await deps.readServiceDescriptor(legacySpec).catch(() => null);
|
|
109
|
+
if (legacyDescriptor !== legacySpec.descriptor) {
|
|
110
|
+
return { eligible: false, reason: "managed_service_not_registered" };
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const descriptorPaths = await deps.listManagedDescriptorPaths();
|
|
114
|
+
const targetPath = legacySpec.descriptorPath;
|
|
115
|
+
assertRegistryCoversDescriptors(services, descriptorPaths.filter((descriptorPath) => descriptorPath !== targetPath));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return { eligible: false, reason: "managed_service_registry_incomplete" };
|
|
119
|
+
}
|
|
120
|
+
status = await deps.serviceStatus(legacySpec);
|
|
121
|
+
if (!status.running)
|
|
122
|
+
return { eligible: false, reason: "service_not_running" };
|
|
123
|
+
selectedSpec = legacySpec;
|
|
95
124
|
}
|
|
125
|
+
if (!status.running)
|
|
126
|
+
return { eligible: false, reason: "service_not_running" };
|
|
96
127
|
if (!deps.installationLeaseHeld(installation.npmPrefix)) {
|
|
97
128
|
return { eligible: false, reason: "installation_lease_missing" };
|
|
98
129
|
}
|
|
@@ -106,6 +137,6 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
106
137
|
eligible: true,
|
|
107
138
|
profileName,
|
|
108
139
|
...installation,
|
|
109
|
-
serviceSpec:
|
|
140
|
+
serviceSpec: selectedSpec,
|
|
110
141
|
};
|
|
111
142
|
}
|
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";
|
|
@@ -221,6 +221,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
221
221
|
let memoryPruneRuntimeExitCode;
|
|
222
222
|
let memoryPruneExecutorCompleted = false;
|
|
223
223
|
let memoryPruneFailurePhase = "diagnostics_before";
|
|
224
|
+
let executionWorkspace = workspace;
|
|
224
225
|
try {
|
|
225
226
|
if (isDeepSeekCodex && !providerConfig.providerApiKey) {
|
|
226
227
|
throw new Error("DeepSeek API key is not configured for this Agent");
|
|
@@ -230,24 +231,62 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
230
231
|
}
|
|
231
232
|
const supportsNativeResume = runtime.name === "claude"
|
|
232
233
|
|| (dependencies.launchRuntime !== undefined && runtimeCapability(runtime.name).nativeResume);
|
|
233
|
-
const
|
|
234
|
+
const currentPrior = input.session.enabled && supportsNativeResume
|
|
234
235
|
? await readSession(workspace.sessionDir)
|
|
235
236
|
: null;
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
// Interactive V1 uses collision-resistant session directories, while the
|
|
238
|
+
// older legacy path stored metadata under tasks/<safe thread key>. Import
|
|
239
|
+
// that metadata once so an existing thread can cross the protocol boundary
|
|
240
|
+
// without losing its native runtime conversation.
|
|
241
|
+
const legacyRunDir = input.session.enabled
|
|
242
|
+
&& supportsNativeResume
|
|
243
|
+
&& input.keyMode === "opaque"
|
|
244
|
+
&& input.resumeKey !== undefined
|
|
245
|
+
? join(input.launch.agentsRoot, input.handle, "tasks", safeKey(input.resumeKey))
|
|
246
|
+
: null;
|
|
247
|
+
const legacyCwdMarker = legacyRunDir === null
|
|
248
|
+
? null
|
|
249
|
+
: join(workspace.sessionDir, ".legacy-runtime-cwd");
|
|
250
|
+
const legacyCwdPinned = legacyCwdMarker !== null
|
|
251
|
+
&& await readFile(legacyCwdMarker, "utf8").then((value) => value === "1").catch(() => false);
|
|
252
|
+
const legacyPrior = legacyRunDir === null || (currentPrior !== null && !legacyCwdPinned)
|
|
253
|
+
? null
|
|
254
|
+
: await readSession(legacyRunDir);
|
|
255
|
+
const prior = currentPrior ?? legacyPrior;
|
|
256
|
+
const sessionExists = workspace.sessionResume || legacyPrior !== null;
|
|
257
|
+
const usesLegacyCwd = legacyRunDir !== null
|
|
258
|
+
&& (legacyCwdPinned || (currentPrior === null && legacyPrior !== null));
|
|
259
|
+
executionWorkspace = !usesLegacyCwd || legacyRunDir === null
|
|
260
|
+
? workspace
|
|
261
|
+
: {
|
|
262
|
+
...workspace,
|
|
263
|
+
runDir: legacyRunDir,
|
|
264
|
+
workLogPath: join(legacyRunDir, "work-log.md"),
|
|
265
|
+
workLog: await readFile(join(legacyRunDir, "work-log.md"), "utf8").catch(() => ""),
|
|
266
|
+
};
|
|
267
|
+
// Claude Code accepts --resume and --model together. Compare against the
|
|
268
|
+
// prior model only when this turn has an explicit model, so the remaining
|
|
269
|
+
// safety checks can still decide whether the same session is reusable.
|
|
270
|
+
// An omitted model stays model-sensitive because it cannot reliably switch
|
|
271
|
+
// a resumed session back to the runtime default.
|
|
272
|
+
const resumeComparisonModel = runtime.name === "claude" && currentModel !== null
|
|
273
|
+
? prior?.model ?? currentModel
|
|
274
|
+
: currentModel;
|
|
275
|
+
const resumeSessionId = supportsNativeResume && sessionExists
|
|
276
|
+
? pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, input.session.budgetTokens, input.session.maxTurns, providerFp)
|
|
238
277
|
: null;
|
|
239
278
|
const resuming = resumeSessionId !== null;
|
|
240
|
-
const rotatedForBudget = !resuming && supportsNativeResume &&
|
|
241
|
-
&& pickResumeId(prior, Date.now(), input.session.warmMs,
|
|
279
|
+
const rotatedForBudget = !resuming && supportsNativeResume && sessionExists
|
|
280
|
+
&& pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
242
281
|
const nearBudget = resuming && isNearBudget(prior, input.session.softTokens);
|
|
243
|
-
const rotated = Boolean(workspace.agentSessionId &&
|
|
282
|
+
const rotated = Boolean(workspace.agentSessionId && sessionExists && !resuming);
|
|
244
283
|
// Codex and Kimi choose their own id on a fresh start, so the native id persisted in
|
|
245
284
|
// .crew-session.json can differ from the bootstrap id in .session. Resume the former.
|
|
246
285
|
const launchSessionId = resumeSessionId ?? (rotated
|
|
247
286
|
? await rotateAgentSession(workspace.sessionDir)
|
|
248
287
|
: workspace.agentSessionId);
|
|
249
288
|
const promptContext = {
|
|
250
|
-
workspace,
|
|
289
|
+
workspace: executionWorkspace,
|
|
251
290
|
resuming,
|
|
252
291
|
rotatedForBudget,
|
|
253
292
|
nearBudget,
|
|
@@ -256,12 +295,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
256
295
|
if (input.attachments && input.attachments.length > 0) {
|
|
257
296
|
if (dependencies.cancellation?.isRequested())
|
|
258
297
|
throw new RuntimeCancelledError();
|
|
259
|
-
knownAttachmentDirectory = executionAttachmentDirectory(
|
|
298
|
+
knownAttachmentDirectory = executionAttachmentDirectory(executionWorkspace.runDir, input.executionId);
|
|
260
299
|
const controller = new AbortController();
|
|
261
300
|
const materialization = Promise.resolve().then(() => (dependencies.materializeAttachments ?? materializeAttachments)({
|
|
262
301
|
serverUrl: input.launch.serverUrl,
|
|
263
302
|
token: input.launch.token,
|
|
264
|
-
runDir:
|
|
303
|
+
runDir: executionWorkspace.runDir,
|
|
265
304
|
executionId: input.executionId,
|
|
266
305
|
attachments: input.attachments,
|
|
267
306
|
signal: controller.signal,
|
|
@@ -287,7 +326,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
287
326
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
288
327
|
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
289
328
|
if (memoryPruneTraceId !== null) {
|
|
290
|
-
const snapshot = await inspectMemoryPruneFiles(
|
|
329
|
+
const snapshot = await inspectMemoryPruneFiles(executionWorkspace.dir, executionWorkspace.workLogPath);
|
|
291
330
|
dslog("memory_prune.files_before", "长期记忆收尾执行前文件指纹", {
|
|
292
331
|
execution_id: input.executionId,
|
|
293
332
|
agent_handle: input.handle,
|
|
@@ -313,7 +352,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
313
352
|
CREW_TOKEN: input.launch.token,
|
|
314
353
|
CREW_CHANNEL: input.channelId,
|
|
315
354
|
CREW_HOME: workspace.dir,
|
|
316
|
-
CREW_TASK_LOG:
|
|
355
|
+
CREW_TASK_LOG: executionWorkspace.workLogPath,
|
|
317
356
|
...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
|
|
318
357
|
XDG_CONFIG_HOME: join(workspace.homeDir, ".config"),
|
|
319
358
|
XDG_DATA_HOME: join(workspace.homeDir, ".local", "share"),
|
|
@@ -355,7 +394,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
355
394
|
const launchRequest = {
|
|
356
395
|
runtime: runtime.name,
|
|
357
396
|
bin: runtime.name,
|
|
358
|
-
cwd:
|
|
397
|
+
cwd: executionWorkspace.runDir,
|
|
359
398
|
...(input.projectSkills === undefined ? {} : { agentRoot: workspace.dir }),
|
|
360
399
|
systemPromptPath: workspace.systemPromptPath,
|
|
361
400
|
systemPrompt,
|
|
@@ -516,10 +555,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
516
555
|
lastExitOk: exitCode === 0,
|
|
517
556
|
...(contextTokens === undefined ? {} : { contextTokens }),
|
|
518
557
|
});
|
|
558
|
+
if (exitCode === 0 && usesLegacyCwd && !legacyCwdPinned && legacyCwdMarker !== null) {
|
|
559
|
+
await writeFile(legacyCwdMarker, "1", "utf8");
|
|
560
|
+
}
|
|
519
561
|
}
|
|
520
562
|
memoryPruneExecutorCompleted = true;
|
|
521
563
|
return {
|
|
522
|
-
workspaceRunDir:
|
|
564
|
+
workspaceRunDir: executionWorkspace.runDir,
|
|
523
565
|
exitCode,
|
|
524
566
|
...(terminationSignal === undefined ? {} : { terminationSignal }),
|
|
525
567
|
activities,
|
|
@@ -541,7 +583,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
541
583
|
finally {
|
|
542
584
|
startupReservation?.release();
|
|
543
585
|
if (memoryPruneTraceId !== null) {
|
|
544
|
-
const snapshot = await inspectMemoryPruneFiles(
|
|
586
|
+
const snapshot = await inspectMemoryPruneFiles(executionWorkspace.dir, executionWorkspace.workLogPath);
|
|
545
587
|
dslog("memory_prune.files_after", "长期记忆收尾执行后文件指纹", {
|
|
546
588
|
execution_id: input.executionId,
|
|
547
589
|
agent_handle: input.handle,
|
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;
|