@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 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 bootstrap release changes the native service ID to include the
99
- daemon-home fingerprint. Run the first command with the currently installed daemon: it upgrades and
100
- restarts the exact legacy service ID. Then use the upgraded CLI to migrate that profile instead of adopting
101
- the old ID:
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 upgrade --profile work
105
- CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon uninstall --profile work
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 migration is interrupted after `uninstall`, resume at `install` and `start`. If it is interrupted before
113
- `uninstall` completes, verify the exact legacy service with `status`, then repeat `uninstall`; identity checks
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
- const status = await deps.serviceStatus(spec);
57
- if (!status.running)
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 === undefined) {
69
- return { eligible: false, reason: "managed_service_not_registered" };
70
- }
71
- try {
72
- assertRegistryCoversDescriptors(services, await deps.listManagedDescriptorPaths());
73
- }
74
- catch {
75
- return { eligible: false, reason: "managed_service_registry_incomplete" };
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
- const descriptor = await deps.readServiceDescriptor(spec).catch(() => null);
78
- if (descriptor === null || spec.descriptorPath === null || spec.descriptor === null
79
- || !managedServiceIdentityMatches(registered, {
80
- version: 1,
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
- daemonHome: deps.profileHome,
85
- agentsRoot: resolveAgentsRoot(profile.agentsRoot, deps.userHome, deps.platform),
101
+ userHome: deps.userHome,
102
+ uid: deps.uid,
86
103
  nodePath: deps.nodePath,
87
104
  entryPath: deps.entryPath,
88
- packageRoot: installation.packageRoot,
89
- npmPrefix: installation.npmPrefix,
90
- descriptorPath: spec.descriptorPath,
91
- descriptorSha256: serviceDescriptorSha256(descriptor),
92
- })
93
- || descriptor !== spec.descriptor) {
94
- return { eligible: false, reason: "managed_service_identity_mismatch" };
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: spec,
140
+ serviceSpec: selectedSpec,
110
141
  };
111
142
  }
@@ -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 prior = input.session.enabled && supportsNativeResume
234
+ const currentPrior = input.session.enabled && supportsNativeResume
234
235
  ? await readSession(workspace.sessionDir)
235
236
  : null;
236
- const resumeSessionId = supportsNativeResume && workspace.sessionResume
237
- ? pickResumeId(prior, Date.now(), input.session.warmMs, currentModel, input.session.budgetTokens, input.session.maxTurns, providerFp)
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 && workspace.sessionResume
241
- && pickResumeId(prior, Date.now(), input.session.warmMs, currentModel, 0, 0, providerFp) !== null;
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 && workspace.sessionResume && !resuming);
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(workspace.runDir, input.executionId);
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: workspace.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(workspace.dir, workspace.workLogPath);
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: workspace.workLogPath,
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: workspace.runDir,
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: workspace.runDir,
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(workspace.dir, workspace.workLogPath);
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
- - 使用标题、列表、引用和加粗形成无颜色也清楚的层级;不得输出 \`<font>\` 或其它未确认可用于企微流式消息的 HTML 标色标签。`;
21
+ - 需要颜色层级时,只在确有对应语义的短加粗重点前使用一个彩色圆点:🟢 绿色圆点表示已验证成功或健康,🟠 橙色圆点表示风险、临期或需要关注,🔴 红色圆点表示失败、阻塞或严重异常,🔵 蓝色圆点表示负责人、行动项或关键中性数据。每个短加粗重点前最多放一个;不要给普通段落、标题或每个要点都加标记。
22
+ - 使用标题、列表、引用、彩色圆点和加粗形成在企微原生流式消息中稳定可见的层级;不得输出 \`<font>\` 或其它 HTML 标色标签。`;
22
23
  export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
23
24
  if (workLog.length <= cap)
24
25
  return workLog;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.39",
3
+ "version": "0.5.41",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",