@nowcrew/daemon 0.6.98-beta.3 → 0.6.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/serve.js CHANGED
@@ -9,7 +9,7 @@ import { randomUUID } from "node:crypto";
9
9
  import { dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
10
10
  import { mergeRunAgentResults, reportScheduledStartFailure, runAgent } from "./runner.js";
11
11
  import { buildOriginDecisionRetryPrompt, buildScheduledPrompt } from "./prompt.js";
12
- import { daemonCapabilities, daemonCapabilityBindings, MACHINE_HELLO_ACK_CAPABILITY, } from "./machine-info.js";
12
+ import { daemonCapabilities, daemonCapabilityBindings, } from "./machine-info.js";
13
13
  import { reconcileExecutionJournal } from "./execution-recovery.js";
14
14
  import { initReleaseSlog, reportDaemonConnectionClose, reportReleaseError } from "./release-channel-guard.js";
15
15
  import { handleWorkspaceControlRequest } from "./workspace-control-handler.js";
@@ -65,10 +65,6 @@ export function serve(config, opts = {}) {
65
65
  let serverCapabilities = new Set();
66
66
  let backoff = 1000;
67
67
  const maxBackoff = opts.maxBackoffMs ?? 30_000;
68
- // hello 重试跑满仍未 ack 时主动断连重连(破 server 侧连接归属/fence 死锁)。收到 ack 清零;
69
- // 连续多次仍拿不到 ack 则停手,退回 connected-offline,避免事故期把控制面打成重连风暴。
70
- const maxUnackedHelloReconnects = 3;
71
- let unackedHelloReconnects = 0;
72
68
  const testShutdown = readTestShutdownConfiguration(process.env);
73
69
  const shutdownTimeoutMs = opts.shutdownTimeoutMs ?? testShutdown.timeoutMs ?? 30_000;
74
70
  const createWebSocket = opts.createWebSocket ?? ((url) => new WebSocket(url));
@@ -122,7 +118,7 @@ export function serve(config, opts = {}) {
122
118
  catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
123
119
  },
124
120
  });
125
- const marketplace = createServeMarketplace(config, localSlots, hostCoordinator);
121
+ const marketplace = createServeMarketplace(config, localSlots, hostCoordinator, async () => ({ ...(await machineRuntime.provider.acquire()).env }));
126
122
  const storageIdentityAdmission = createStorageIdentityAdmission(opts.storageRuntime);
127
123
  const projectionCoordinator = createAgentProjectionCoordinator();
128
124
  const { projectRegistry, projectWorkspaceRegistry, projectDependencyProvisioner } = createProjectRegistryWiring(config.agentsRoot, opts.execution?.dependencies);
@@ -164,41 +160,7 @@ export function serve(config, opts = {}) {
164
160
  : configuredIdentity;
165
161
  return currentIdentity === undefined ? hello : { ...hello, ...currentIdentity };
166
162
  },
167
- onSent: (hello, attempt) => dslog("daemon.machine_hello_send_queued", "机器 hello 已进入发送队列", {
168
- attempt, hostname: hello.hostname, daemon_version: hello.daemonVersion,
169
- }),
170
- onSendFailed: (hello, attempt, error) => dslog("daemon.machine_hello_send_failed", "机器 hello 发送失败", {
171
- level: "WARN", attempt, hostname: hello.hostname, daemon_version: hello.daemonVersion,
172
- error_message: error instanceof Error ? error.message : String(error),
173
- }),
174
- onAck: (hello) => {
175
- unackedHelloReconnects = 0; // 拿到确认 = 本连接登记成功,清零强制重连计数。
176
- dslog("daemon.machine_hello_ack_received", "机器 hello 已收到控制面确认", {
177
- hostname: hello.hostname, daemon_version: hello.daemonVersion,
178
- });
179
- },
180
- // 仅当当前 server 声明支持 ack 才重试;老 server 收不到 ack,重试无意义(退回单次上报)。
181
- ackSupported: () => serverCapabilities.has(MACHINE_HELLO_ACK_CAPABILITY),
182
- // 重试跑满仍未 ack → 断开当前连接,靠重连换新连接(新 epoch/token)破 server 侧 fence 死锁。
183
- onExhausted: (socket) => {
184
- if (stopped || ws !== socket || socket.readyState !== WebSocket.OPEN)
185
- return;
186
- if (unackedHelloReconnects >= maxUnackedHelloReconnects) {
187
- dslog("daemon.machine_hello_unacked_giveup", "机器 hello 连续多次重连仍未确认,停止强制重连(保持连接,等待下次事件或人工介入)", {
188
- level: "ERROR", reconnects: unackedHelloReconnects,
189
- });
190
- return;
191
- }
192
- unackedHelloReconnects += 1;
193
- // 抖动重连间隔:控制面回滚时多台 daemon 会同时耗尽重试,固定间隔会同步重连造成羊群。
194
- // close handler 用当前 backoff 排重连,这里注入 1~4s 随机值把各机错峰(open 会再把它复位)。
195
- const jitteredBackoff = 1_000 + Math.floor(Math.random() * 3_000);
196
- backoff = Math.min(jitteredBackoff, maxBackoff);
197
- dslog("daemon.machine_hello_unacked_reconnect", "机器 hello 重试跑满仍未确认,断开重连换新连接", {
198
- level: "WARN", reconnects: unackedHelloReconnects, backoff_ms: backoff,
199
- });
200
- socket.close(1011, "machine hello unacknowledged");
201
- },
163
+ onSent: (hello) => log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`),
202
164
  });
203
165
  const machineRuntime = createMachineRuntimeEnvironment({
204
166
  config,
@@ -348,7 +310,6 @@ export function serve(config, opts = {}) {
348
310
  // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
349
311
  void drainSpool();
350
312
  // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
351
- machineHelloPublisher.attach(openedSocket);
352
313
  machineRuntime.open(openedSocket, { capabilities: capabilities.machineHello, beforePublish: Promise.resolve(), connectionId });
353
314
  opts.onOpen?.(ws);
354
315
  void sendSnapshot(`reconnect-${randomUUID()}`);
@@ -391,11 +352,6 @@ export function serve(config, opts = {}) {
391
352
  if (typeof decoded !== "object" || decoded === null)
392
353
  return;
393
354
  const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
394
- if (rawType === "machine:hello_ack") {
395
- const ackSeq = "helloSeq" in decoded && typeof decoded.helloSeq === "number" ? decoded.helloSeq : undefined;
396
- machineHelloPublisher.acknowledge(connectionSocket, ackSeq);
397
- return;
398
- }
399
355
  if (rawType === "machine:environment:get" || rawType === "machine:environment:refresh") {
400
356
  await handleMachineEnvironmentFrame(decoded, machineRuntime.provider, sendToConnection);
401
357
  return;
@@ -779,11 +735,6 @@ export function serve(config, opts = {}) {
779
735
  // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
780
736
  const r = msg;
781
737
  setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
782
- // 旧 server 无此字段 → 空集合(daemon 保持全部旧行为)。ackSupported() 依赖它判断是否重试,
783
- // 必须在任何 republish() 之前就位;否则本轮 republish 看到空集合→不排重试→整套 ack/重连机制哑火。
784
- serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
785
- ? r.serverCapabilities.filter((c) => typeof c === "string")
786
- : []);
787
738
  if (opts.storageRuntime !== undefined && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
788
739
  const bindingSocket = ws;
789
740
  bindStorageServerIdentity(opts.storageRuntime, { workspaceId: r.workspaceId, machineId: r.machineId }, {
@@ -800,7 +751,10 @@ export function serve(config, opts = {}) {
800
751
  },
801
752
  });
802
753
  }
803
- machineHelloPublisher.republish();
754
+ // 旧 server 无此字段 → 空集合(daemon 保持全部旧行为)。
755
+ serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
756
+ ? r.serverCapabilities.filter((c) => typeof c === "string")
757
+ : []);
804
758
  if (serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)) {
805
759
  void projectSkillsController.initialize()
806
760
  .then(() => projectSkillsController.publishCurrent())
@@ -1249,7 +1203,6 @@ export function serve(config, opts = {}) {
1249
1203
  });
1250
1204
  connectionSocket.on("close", (code) => {
1251
1205
  machineRuntime.close(connectionSocket);
1252
- machineHelloPublisher.detach(connectionSocket);
1253
1206
  if (stopped || ws !== connectionSocket)
1254
1207
  return;
1255
1208
  // server 能力随连接失效;下个连接的 ready 帧重新声明(防降级重连后沿用过期能力)。
@@ -1,6 +1,5 @@
1
1
  import { systemCommandRunner, windowsCommandArg, } from "./computer-service.js";
2
2
  import { win32 } from "node:path";
3
- import { powershellCommandArgs } from "./powershell-command.js";
4
3
  function encoded(value) {
5
4
  return Buffer.from(value, "utf16le").toString("base64");
6
5
  }
@@ -34,9 +33,10 @@ export async function inspectWindowsServiceUpdateScope(spec, npmPrefix, daemonPi
34
33
  "$enginePid=if($instances.Count -eq 1){[int64]$instances[0].EnginePID}else{0};",
35
34
  "[pscustomobject]@{count=1;state=[int]$task.State;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};instanceCount=$instances.Count;enginePid=$enginePid;engineOwned=($enginePid -eq $pidValue -or $ancestors -contains $enginePid);processIdentity=$processIdentity;conflictingPrefixes=$conflicts.Count}|ConvertTo-Json -Compress;",
36
35
  ].join("");
37
- const result = await runner("powershell.exe", powershellCommandArgs(script, [
36
+ const result = await runner("powershell.exe", [
37
+ "-NoProfile", "-NonInteractive", "-Command", script,
38
38
  encoded(spec.id), encoded(entryMarker), String(daemonPid),
39
- ]));
39
+ ]);
40
40
  if (result.exitCode !== 0)
41
41
  return { valid: false };
42
42
  let inspection;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.98-beta.3",
3
+ "version": "0.6.98",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -18,13 +18,6 @@
18
18
  "publishConfig": {
19
19
  "access": "public"
20
20
  },
21
- "scripts": {
22
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
23
- "build": "tsc -p tsconfig.json",
24
- "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
25
- "test": "vitest run",
26
- "typecheck": "tsc --noEmit"
27
- },
28
21
  "dependencies": {
29
22
  "@agentclientprotocol/sdk": "1.2.1",
30
23
  "@nowcrew/cli": "0.4.17",
@@ -46,5 +39,11 @@
46
39
  "tsx": "^4.19.0",
47
40
  "typescript": "^5.6.0",
48
41
  "vitest": "^2.1.0"
42
+ },
43
+ "scripts": {
44
+ "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
45
+ "build": "tsc -p tsconfig.json",
46
+ "test": "vitest run",
47
+ "typecheck": "tsc --noEmit"
49
48
  }
50
- }
49
+ }
@@ -1,46 +0,0 @@
1
- import { assertBootstrapMatches, localPathsMatch } from "./computer-setup-orchestration.js";
2
- import { redactSetupSecrets } from "./computer-setup-client.js";
3
- export async function runMachineTokenSetup(input, deps, machineToken) {
4
- try {
5
- deps.reportPairing("Checking computer connection information...");
6
- const target = await deps.client.tokenTarget(machineToken);
7
- assertBootstrapMatches(target, input);
8
- deps.reportPairing("Downloading, installing, and starting the Daemon service...");
9
- const installation = await deps.installAutomatically({
10
- serverUrl: target.serverUrl, machineToken, workspaceId: target.workspaceId, machineId: target.machineId,
11
- requestedProfile: target.profile,
12
- ...(target.releaseTag === undefined ? {} : { requestedReleaseTag: target.releaseTag }),
13
- ...(target.registryUrl === undefined ? {} : { requestedRegistryUrl: target.registryUrl }),
14
- verifyAfterStart: async (candidate) => {
15
- deps.reportPairing("Confirming the computer connection...");
16
- const deadline = deps.now() + 120_000;
17
- for (;;) {
18
- const current = await deps.client.tokenTarget(machineToken);
19
- assertBootstrapMatches(current, input);
20
- const connection = current.connection;
21
- const platform = deps.platform ?? process.platform;
22
- if (connection.status === "online" && connection.epoch > target.minimumConnectionEpoch
23
- && connection.credentialMatchesConnection && current.environment === target.environment
24
- && connection.profile === candidate.profile
25
- && connection.daemonHome !== null && localPathsMatch(connection.daemonHome, candidate.daemonHome, platform)
26
- && connection.agentsRoot !== null && localPathsMatch(connection.agentsRoot, candidate.agentsRoot, platform)
27
- && (target.storageId === undefined || connection.storageId === target.storageId))
28
- return;
29
- if (deps.now() >= deadline)
30
- throw new Error("Timed out confirming the new computer connection");
31
- await deps.sleep(Math.min(2_000, Math.max(0, deadline - deps.now())));
32
- }
33
- },
34
- });
35
- return {
36
- workspaceId: target.workspaceId, workspaceSlug: target.workspaceSlug, machineId: target.machineId,
37
- environment: target.environment, profile: installation.profile, daemonVersion: installation.version,
38
- serviceId: installation.serviceId, serviceStatus: "running", helloConfirmed: true,
39
- localNowworkHome: installation.localHome, daemonHome: installation.daemonHome, agentsRoot: installation.agentsRoot,
40
- ...(target.storageId === undefined ? {} : { storageId: target.storageId }),
41
- };
42
- }
43
- catch (error) {
44
- throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error), [machineToken]));
45
- }
46
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * powershell.exe -Command joins trailing argv into command text, rather than
3
- * binding it to $args. Invoke a scriptblock with literal arguments inside one
4
- * UTF-16LE encoded command. Encoding is transport, not encryption.
5
- */
6
- export function powershellCommandArgs(script, args = []) {
7
- // PowerShell also treats typographic single quotes as string delimiters.
8
- const literals = args.map((arg) => `'${arg.replace(/['\u2018-\u201b]/gu, (quote) => quote + quote)}'`).join(" ");
9
- const command = "[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false);\n"
10
- + `& {\n${script}\n} ${literals}`;
11
- return ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(command, "utf16le").toString("base64")];
12
- }
@@ -1,26 +0,0 @@
1
- /** Read only: resolve the task's account with Windows, without guessing or stripping its domain. */
2
- export const windowsTaskPrincipalScript = [
3
- "$currentUserSid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value;",
4
- "$principalSid=$null;",
5
- "try{",
6
- "$account=[string]$task.Principal.UserId;",
7
- "if($account -match '^S-\\d-'){",
8
- "$principalSid=([Security.Principal.SecurityIdentifier]::new($account)).Value;",
9
- "}else{",
10
- "$principalSid=([Security.Principal.NTAccount]::new($account)).Translate([Security.Principal.SecurityIdentifier]).Value;",
11
- "}",
12
- "}catch{$principalSid=$null};",
13
- ].join("");
14
- export const windowsTaskPrincipalFields = [
15
- "currentUserSid=$currentUserSid;",
16
- "principalSid=$principalSid;",
17
- "logonType=[int]$task.Principal.LogonType;",
18
- "runLevel=[int]$task.Principal.RunLevel;",
19
- ].join("");
20
- export function windowsTaskPrincipalMatches(principal) {
21
- return typeof principal.currentUserSid === "string"
22
- && /^S-1-(?:\d+-)*\d+$/.test(principal.currentUserSid)
23
- && principal.principalSid === principal.currentUserSid
24
- && principal.logonType === 3 // Interactive
25
- && principal.runLevel === 0; // Limited
26
- }