@nowcrew/daemon 0.6.98-beta.4 → 0.6.98-beta.6

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.
Files changed (68) hide show
  1. package/README.md +9 -0
  2. package/dist/automatic-daemon-installer.js +41 -10
  3. package/dist/cloud-report-upload.js +10 -1
  4. package/dist/computer-main.js +0 -0
  5. package/dist/computer-setup-client.js +22 -2
  6. package/dist/computer-setup-existing.js +154 -0
  7. package/dist/computer-setup-orchestration.js +21 -6
  8. package/dist/computer-setup.js +1 -0
  9. package/dist/computer-token-setup.js +2 -2
  10. package/dist/execution-journal.js +1 -3
  11. package/dist/execution-protocol.js +1 -1
  12. package/dist/execution-runner.js +29 -20
  13. package/dist/external-output.js +12 -1
  14. package/dist/i18n.js +6 -0
  15. package/dist/local-execution-prompt.js +3 -8
  16. package/dist/local-executor.js +52 -69
  17. package/dist/local-memory/index-budget.js +3 -9
  18. package/dist/local-memory/inventory-cli.js +8 -52
  19. package/dist/local-memory/inventory-source.js +8 -15
  20. package/dist/local-memory/pending-query.js +2 -5
  21. package/dist/local-memory/pending-schema.js +1 -1
  22. package/dist/local-memory/pending.js +2 -16
  23. package/dist/local-memory/prompt.js +5 -21
  24. package/dist/local-memory/read-cli.js +76 -98
  25. package/dist/local-memory/rollout.js +11 -79
  26. package/dist/local-memory/runtime.js +13 -181
  27. package/dist/local-memory/write.js +21 -189
  28. package/dist/local-memory-diagnostics.js +13 -94
  29. package/dist/local-memory-telemetry.js +2 -19
  30. package/dist/machine-info.js +0 -3
  31. package/dist/main.js +0 -0
  32. package/dist/marketplace-protocol.js +1 -1
  33. package/dist/marketplace-remote.js +1 -1
  34. package/dist/marketplace-runner.js +1 -1
  35. package/dist/memory-prune-diagnostics.js +4 -60
  36. package/dist/message-file-preview.js +1 -11
  37. package/dist/prompt.js +2 -6
  38. package/dist/runner.js +1 -1
  39. package/dist/runtimes/claude-thread-runner.js +4 -25
  40. package/dist/runtimes/pi.js +85 -17
  41. package/dist/serve.js +3 -38
  42. package/dist/token.js +0 -1
  43. package/dist/workspace-control-handler.js +3 -16
  44. package/dist/workspace.js +2 -13
  45. package/package.json +9 -8
  46. package/dist/local-memory/commit-telemetry.js +0 -30
  47. package/dist/local-memory/execution-workspace.js +0 -62
  48. package/dist/local-memory/full-index-search.js +0 -47
  49. package/dist/local-memory/initialization-identity.js +0 -36
  50. package/dist/local-memory/inventory-v2.js +0 -362
  51. package/dist/local-memory/legacy-adapter.js +0 -233
  52. package/dist/local-memory/migration-access.js +0 -242
  53. package/dist/local-memory/migration-cli.js +0 -54
  54. package/dist/local-memory/migration-io.js +0 -199
  55. package/dist/local-memory/migration-pending.js +0 -9
  56. package/dist/local-memory/migration-plan.js +0 -432
  57. package/dist/local-memory/migration-schema.js +0 -55
  58. package/dist/local-memory/migration-state-invariants.js +0 -65
  59. package/dist/local-memory/migration-store.js +0 -414
  60. package/dist/local-memory/migration-tool-entry.js +0 -21
  61. package/dist/local-memory/source-path.js +0 -13
  62. package/dist/local-memory/storage-format.js +0 -152
  63. package/dist/local-memory/writer-mode.js +0 -13
  64. package/dist/memory-migration-controller.js +0 -89
  65. package/dist/memory-migration-inspect.js +0 -108
  66. package/dist/memory-migration-journal.js +0 -14
  67. package/dist/memory-migration-protocol.js +0 -14
  68. package/dist/memory-migration-runtime.js +0 -305
package/README.md CHANGED
@@ -110,6 +110,15 @@ to confirm Workspace, Machine, environment, profile, and storage identity. It do
110
110
  existing `.crew` data. Existing `crew-daemon install --server-url ... --api-key ...` and profile lifecycle
111
111
  commands remain available as compatibility and recovery entry points.
112
112
 
113
+ Profiles created by older installers may omit `workspaceId` and `machineId`. During setup, the installer
114
+ uses the newly authorized machine credential to call `POST /daemon/computer-setup/legacy-profile` with
115
+ the SHA-256 of the previous profile token. The server checks historical ownership within that exact
116
+ Workspace and Machine, including revoked credentials, without reactivating them. After the server,
117
+ profile name, Agent root and any existing identity fields match, normal installation persists both UUIDs.
118
+ A mismatch or failed verification stops before changing the local profile or service; do not delete a
119
+ profile or fill in UUIDs by guessing. Deploy the server endpoint before publishing the compatible daemon;
120
+ an older server cannot verify legacy profiles, and already published daemon packages remain unchanged.
121
+
113
122
  ## Machine Environment
114
123
 
115
124
  The daemon loads exported variables from its OS account’s bash/zsh configuration on macOS/Linux,
@@ -1,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
+ import { createHash } from "node:crypto";
2
3
  import { posix, win32 } from "node:path";
3
4
  import { z } from "zod";
4
- import { assertProfileAgentsRootUnique, listProfiles, loadProfile, MachineTokenSchema, removeProfile, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
5
+ import { assertProfileAgentsRootUnique, listProfiles, loadProfile, MachineTokenSchema, removeProfile, resolveAgentsRoot, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
5
6
  import { buildServiceSpec, captureExactService, hardenWindowsProfile, replaceExactService, restoreExactService, serviceAction, serviceStatus, systemCommandRunner, uninstallService, } from "./computer-service.js";
6
7
  import { daemonGlobalInstallation } from "./daemon-installation.js";
7
8
  import { buildManagedServiceRecord, installManagedService, uninstallManagedService, } from "./managed-service-lifecycle.js";
@@ -15,8 +16,14 @@ import { daemonRegistryCandidates } from "./daemon-registry.js";
15
16
  import { npmInvocation } from "./npm-command.js";
16
17
  import { discoverExactLegacyService, } from "./automatic-service-migration.js";
17
18
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
18
- import { serverHostIdentity } from "./server-url.js";
19
- import { computerSetupRecoveryGuidance } from "./computer-setup-client.js";
19
+ import { serverHostIdentity, serverUrlsMatch } from "./server-url.js";
20
+ import { computerSetupRecoveryGuidance, createComputerSetupClient, } from "./computer-setup-client.js";
21
+ export const automaticDaemonInstallStageMessage = {
22
+ downloading: "Downloading the Daemon package...",
23
+ configuring: "Configuring the Daemon service...",
24
+ starting: "Starting the Daemon service...",
25
+ confirming: "Confirming the computer connection...",
26
+ };
20
27
  function profileStorage(platform, userHome, runner) {
21
28
  return {
22
29
  platform,
@@ -57,6 +64,7 @@ function defaultDependencies() {
57
64
  version: daemonVersion(),
58
65
  ...(process.env.PATH ? { runtimePath: process.env.PATH } : {}),
59
66
  runner,
67
+ verifyLegacyProfileIdentity: (serverUrl, input) => createComputerSetupClient({ serverUrl }).verifyLegacyProfileIdentity(input),
60
68
  captureProfile: async (target) => (await listProfiles(target.daemonHome)).includes(target.profile)
61
69
  ? loadProfile(target.profile, target.daemonHome, storage)
62
70
  : null,
@@ -260,18 +268,37 @@ function setupIdentity(input) {
260
268
  machineId: z.string().uuid().parse(input.machineId),
261
269
  };
262
270
  }
263
- function assertExistingProfileIdentity(previous, identity) {
271
+ async function assertExistingProfileIdentity(previous, identity, target, machineToken, dependencies) {
264
272
  if (previous === null || identity === null)
265
273
  return;
266
- if (previous.workspaceId === undefined || previous.machineId === undefined) {
267
- throw new Error(`Profile '${previous.name}' does not record its Workspace and Machine identity`);
268
- }
269
- if (previous.workspaceId !== identity.workspaceId) {
274
+ if (previous.workspaceId !== undefined && previous.workspaceId !== identity.workspaceId) {
270
275
  throw new Error(`Profile '${previous.name}' belongs to a different Workspace identity`);
271
276
  }
272
- if (previous.machineId !== identity.machineId) {
277
+ if (previous.machineId !== undefined && previous.machineId !== identity.machineId) {
273
278
  throw new Error(`Profile '${previous.name}' belongs to a different Machine identity`);
274
279
  }
280
+ if (previous.workspaceId !== undefined && previous.machineId !== undefined)
281
+ return;
282
+ const rootIdentity = (root) => {
283
+ const resolved = resolveAgentsRoot(root, dependencies.userHome, dependencies.platform);
284
+ return dependencies.platform === "win32" ? resolved.toLowerCase() : resolved;
285
+ };
286
+ if (previous.name !== target.profile || !serverUrlsMatch(previous.serverUrl, target.serverUrl)
287
+ || rootIdentity(previous.agentsRoot) !== rootIdentity(target.agentsRoot)) {
288
+ throw new Error(`Legacy profile '${previous.name}' does not match the requested server, profile, or Agent root`);
289
+ }
290
+ if (dependencies.verifyLegacyProfileIdentity === undefined) {
291
+ throw new Error(`Cannot verify legacy profile '${previous.name}' Workspace and Machine identity`);
292
+ }
293
+ // The old credential may already have been revoked by setup. Only the current credential
294
+ // authenticates this read-only request; the old hash proves historical ownership.
295
+ const verified = await dependencies.verifyLegacyProfileIdentity(target.serverUrl, {
296
+ machineToken,
297
+ previousTokenHash: createHash("sha256").update(previous.machineToken).digest("hex"),
298
+ });
299
+ if (verified.workspaceId !== identity.workspaceId || verified.machineId !== identity.machineId) {
300
+ throw new Error(`Legacy profile '${previous.name}' belongs to a different Workspace or Machine identity`);
301
+ }
275
302
  }
276
303
  export async function installDaemonAutomatically(input, dependencies = defaultDependencies()) {
277
304
  const machineToken = MachineTokenSchema.parse(input.machineToken);
@@ -308,7 +335,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
308
335
  });
309
336
  const previousProfile = await dependencies.captureProfile(target);
310
337
  await dependencies.assertProfileTarget(target, previousProfile);
311
- assertExistingProfileIdentity(previousProfile, identity);
338
+ await assertExistingProfileIdentity(previousProfile, identity, target, machineToken, dependencies);
312
339
  const previousService = await dependencies.captureService(spec, target);
313
340
  if (previousService.installed && !previousService.exact) {
314
341
  throw new Error(`Installed service '${spec.id}' does not match the requested identity`);
@@ -331,6 +358,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
331
358
  });
332
359
  let installFailure;
333
360
  try {
361
+ input.onProgress?.("downloading");
334
362
  await dependencies.installPackage(target.npmPrefix, dependencies.version, dependencies.platform, dependencies.runner, target.registryUrl);
335
363
  }
336
364
  catch (error) {
@@ -353,6 +381,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
353
381
  legacyRemovalStarted = true;
354
382
  await dependencies.removeLegacyService(legacyService);
355
383
  }
384
+ input.onProgress?.("configuring");
356
385
  profileSaved = true;
357
386
  await dependencies.saveTargetProfile({
358
387
  version: 1,
@@ -367,6 +396,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
367
396
  }, target.daemonHome);
368
397
  serviceReplacementStarted = true;
369
398
  await dependencies.replaceService(spec, target);
399
+ input.onProgress?.("starting");
370
400
  await dependencies.startService(spec);
371
401
  const status = await dependencies.verifyService(spec);
372
402
  if (!status.running)
@@ -380,6 +410,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
380
410
  daemonHome: target.daemonHome,
381
411
  agentsRoot: target.agentsRoot,
382
412
  };
413
+ input.onProgress?.("confirming");
383
414
  await input.verifyAfterStart?.(result);
384
415
  return result;
385
416
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir, hostname, platform } from "node:os";
3
+ import { join } from "node:path";
3
4
  const ALIYUN_MARKER = /alibaba|aliyun|alicloud/iu;
4
5
  function readMarker(path) {
5
6
  try {
@@ -28,7 +29,15 @@ export function detectCloudProvider(env = process.env, platformName = platform()
28
29
  }
29
30
  export function findOssUploadScript(env = process.env, exists = existsSync) {
30
31
  const explicit = env.CREW_OSS_UPLOAD_SCRIPT?.trim();
31
- return explicit && exists(explicit) ? explicit : undefined;
32
+ if (explicit)
33
+ return exists(explicit) ? explicit : undefined;
34
+ const home = env.HOME?.trim() || homedir();
35
+ const candidates = [
36
+ join(home, ".claude", "skills", "oss-upload", "scripts", "oss_upload.py"),
37
+ join(home, ".codex", "skills", "oss-upload", "scripts", "oss_upload.py"),
38
+ join(home, ".agents", "skills", "oss-upload", "scripts", "oss_upload.py"),
39
+ ];
40
+ return candidates.find(exists);
32
41
  }
33
42
  export function cloudReportUploadFacts(env = process.env, platformName = platform(), hostName = hostname(), readMarkerValue, exists) {
34
43
  const provider = detectCloudProvider(env, platformName, hostName, readMarkerValue);
File without changes
@@ -52,6 +52,13 @@ const SetupTokenTargetSchema = SetupBootstrapSchema.omit({ setupId: true, machin
52
52
  credentialMatchesConnection: z.boolean(),
53
53
  }).strict(),
54
54
  }).strict();
55
+ const SetupReceiptSchema = z.object({
56
+ setupId: z.string().uuid(),
57
+ credentialState: z.enum(["pending", "finalized"]),
58
+ target: SetupTokenTargetSchema.omit({ connection: true }),
59
+ connection: SetupTokenTargetSchema.shape.connection,
60
+ connectionConfirmed: z.boolean(),
61
+ }).strict();
55
62
  const SetupPendingSchema = z.object({
56
63
  status: z.enum(["authorization_pending", "pending"]),
57
64
  intervalSeconds: z.number().int().positive().max(300).optional(),
@@ -89,7 +96,8 @@ export class ComputerSetupHttpError extends Error {
89
96
  }
90
97
  }
91
98
  export function redactSetupSecrets(value, secrets = []) {
92
- let redacted = value.replace(/\bsk_machine_[A-Za-z0-9_-]+\b/gu, "<redacted>");
99
+ let redacted = value.replace(/\bsk_machine_[A-Za-z0-9_-]+\b/gu, "<redacted>")
100
+ .replace(/(--setup-token(?:=|\s+))[A-Za-z0-9_-]+/gu, "$1<redacted>");
93
101
  for (const secret of secrets) {
94
102
  if (secret.length > 0)
95
103
  redacted = redacted.replaceAll(secret, "<redacted>");
@@ -251,7 +259,8 @@ export function createComputerSetupClient(options) {
251
259
  }));
252
260
  const now = options.now ?? Date.now;
253
261
  const request = async (method, path, body, bearer) => {
254
- const secretInputs = [bearer ?? "", body?.deviceCode ?? ""];
262
+ const secretInputs = [bearer ?? "", body?.deviceCode ?? "",
263
+ body?.setupToken ?? ""];
255
264
  const controller = new AbortController();
256
265
  const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
257
266
  try {
@@ -283,7 +292,18 @@ export function createComputerSetupClient(options) {
283
292
  }
284
293
  };
285
294
  return {
295
+ receipt: async ({ setupToken, machineToken }) => {
296
+ const receipt = SetupReceiptSchema.safeParse(await request("POST", "/daemon/computer-setup/receipt", { setupToken }, machineToken));
297
+ if (!receipt.success)
298
+ throw new Error("Invalid computer setup receipt response");
299
+ return receipt.data;
300
+ },
286
301
  tokenTarget: async (machineToken) => SetupTokenTargetSchema.parse(await request("GET", "/daemon/computer-setup/target", undefined, machineToken)),
302
+ verifyLegacyProfileIdentity: async (input) => z.object({
303
+ workspaceId: z.string().uuid(), machineId: z.string().uuid(),
304
+ }).strict().parse(await request("POST", "/daemon/computer-setup/legacy-profile", {
305
+ previousTokenHash: input.previousTokenHash,
306
+ }, input.machineToken)),
287
307
  start: async (input) => {
288
308
  return parseStart(await request("POST", "/api/auth/computer-setup/start", {
289
309
  ...(input.workspace.kind === "id"
@@ -0,0 +1,154 @@
1
+ import { access, readFile, readdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { posix, win32 } from "node:path";
4
+ import { resolveAutomaticInstallTarget } from "./automatic-install-target.js";
5
+ import { stableDaemonEntry } from "./automatic-daemon-installer.js";
6
+ import { resolveDaemonLocalHome } from "./daemon-local-home.js";
7
+ import { daemonGlobalInstallation } from "./daemon-installation.js";
8
+ import { isDaemonReleaseVersion } from "./daemon-release-version.js";
9
+ import { listProfiles, loadProfile, profileIsPrivate, profilePath } from "./computer-profile.js";
10
+ import { buildServiceSpec, captureExactService, systemCommandRunner, windowsProfileIsPrivate } from "./computer-service.js";
11
+ import { buildManagedServiceRecord } from "./managed-service-lifecycle.js";
12
+ import { managedServiceIdentityMatches, readManagedServiceRegistry } from "./managed-service-registry.js";
13
+ import { serverUrlsMatch } from "./server-url.js";
14
+ import { assertBootstrapMatches, localPathsMatch } from "./computer-setup-orchestration.js";
15
+ const storageKeys = ["NOWWORK_HOME", "NOWWORK_ENV_TAG", "NOWWORK_STORAGE_POLICY_PATH", "NOWWORK_LOCAL_HOME", "NOWWORK_LOCAL_HOME_MODE"];
16
+ const decodeXml = (value) => value.replaceAll("&quot;", '"').replaceAll("&apos;", "'")
17
+ .replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
18
+ // Only recover the generated environment fields; the rebuilt descriptor must still match byte-for-byte.
19
+ function descriptorEnvironment(descriptor, platform) {
20
+ return Object.fromEntries(storageKeys.flatMap((key) => {
21
+ const value = platform === "darwin"
22
+ ? new RegExp(`<key>${key}</key><string>([^<]*)</string>`, "u").exec(descriptor)?.[1]
23
+ : new RegExp(`^Environment="${key}=((?:[^"\\\\]|\\\\.)*)"$`, "mu").exec(descriptor)?.[1];
24
+ return value === undefined ? [] : [[key, platform === "darwin" ? decodeXml(value) : value.replace(/\\(.)/gu, "$1").replaceAll("%%", "%")]];
25
+ }));
26
+ }
27
+ async function windowsNodePath(serviceId, runner) {
28
+ const script = "$ErrorActionPreference='Stop';$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));"
29
+ + "$t=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});"
30
+ + "if($t.Count -ne 1){throw 'scheduled task identity unknown'};$a=@($t[0].Actions);"
31
+ + "if($a.Count -ne 1){throw 'scheduled task action unknown'};$a[0].Execute|ConvertTo-Json -Compress;";
32
+ const result = await runner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script,
33
+ Buffer.from(serviceId, "utf16le").toString("base64")]);
34
+ const path = result.exitCode === 0 ? JSON.parse(result.stdout) : null;
35
+ if (typeof path !== "string" || !win32.isAbsolute(path))
36
+ throw new Error("Scheduled task executable unavailable");
37
+ return path;
38
+ }
39
+ /** One-shot inspection only. Never consumes credentials or changes local/service state. */
40
+ export async function inspectExistingComputerSetup(input, client, options = {}) {
41
+ if (input.args.setupToken === undefined)
42
+ return { status: "unavailable" };
43
+ const platform = options.platform ?? process.platform;
44
+ const userHome = options.userHome ?? homedir();
45
+ const environment = options.environment ?? process.env;
46
+ const runner = options.runner ?? systemCommandRunner;
47
+ const paths = platform === "win32" ? win32 : posix;
48
+ try {
49
+ const requestedLocalHome = environment.NOWWORK_LOCAL_HOME;
50
+ const requestedLocalHomeMode = environment.NOWWORK_LOCAL_HOME_MODE ?? environment.NOWWORK_STORAGE_MODE;
51
+ const localOptions = {
52
+ ...(requestedLocalHome === undefined ? {} : { requestedLocalHome }),
53
+ ...(requestedLocalHomeMode === undefined ? {} : { requestedLocalHomeMode }),
54
+ };
55
+ const configuredHome = resolveDaemonLocalHome({ userHome, platform, profile: "in", mode: "nowwork",
56
+ ...(requestedLocalHome === undefined ? {} : { explicit: requestedLocalHome }) }).daemonHome;
57
+ const legacyRoot = paths.join(userHome, ".crew");
58
+ const legacyEntries = await readdir(legacyRoot, { withFileTypes: true }).catch((error) => {
59
+ if (error.code === "ENOENT")
60
+ return [];
61
+ throw error;
62
+ });
63
+ const homes = [...new Set([configuredHome, paths.join(userHome, ".nowwork", "config"),
64
+ ...legacyEntries.filter((entry) => entry.isDirectory() && /^daemon(?:-[a-z0-9_-]+)?$/u.test(entry.name))
65
+ .map((entry) => paths.join(legacyRoot, entry.name))])];
66
+ const candidates = [];
67
+ for (const home of homes) {
68
+ for (const name of await listProfiles(home)) {
69
+ const privateProfile = platform === "win32"
70
+ ? await windowsProfileIsPrivate(profilePath(name, home), runner)
71
+ : await profileIsPrivate(name, home, platform);
72
+ if (!privateProfile)
73
+ continue;
74
+ const profile = await loadProfile(name, home, { platform });
75
+ if (profile.name !== name || profile.machineId !== input.args.machineId || profile.workspaceId === undefined
76
+ || !serverUrlsMatch(profile.serverUrl, input.serverUrl)
77
+ || (input.args.workspace.kind === "id" && profile.workspaceId !== input.args.workspace.value))
78
+ continue;
79
+ const target = resolveAutomaticInstallTarget({ serverUrl: input.serverUrl, requestedProfile: name,
80
+ userHome, platform, ...localOptions });
81
+ if (!localPathsMatch(target.daemonHome, home, platform))
82
+ continue;
83
+ if (profile.agentsRoot === undefined || !localPathsMatch(profile.agentsRoot, target.agentsRoot, platform))
84
+ continue;
85
+ candidates.push({ profile, target });
86
+ }
87
+ }
88
+ if (candidates.length !== 1)
89
+ return { status: "unavailable" };
90
+ const { profile, target } = candidates[0];
91
+ const receipt = await client.receipt({ setupToken: input.args.setupToken, machineToken: profile.machineToken });
92
+ assertBootstrapMatches(receipt.target, input);
93
+ if (receipt.target.workspaceId !== profile.workspaceId || receipt.target.profile !== profile.name)
94
+ return { status: "unavailable" };
95
+ if (receipt.credentialState === "pending")
96
+ return { status: "pending" };
97
+ const connection = receipt.connection;
98
+ if (receipt.credentialState !== "finalized" || !receipt.connectionConfirmed || connection.status !== "online"
99
+ || !connection.credentialMatchesConnection || connection.epoch <= receipt.target.minimumConnectionEpoch
100
+ || connection.profile !== profile.name || connection.daemonHome === null || connection.agentsRoot === null
101
+ || !localPathsMatch(connection.daemonHome, target.daemonHome, platform)
102
+ || !localPathsMatch(connection.agentsRoot, target.agentsRoot, platform)
103
+ || (receipt.target.storageId !== undefined && receipt.target.storageId !== connection.storageId))
104
+ return { status: "unavailable" };
105
+ const localHome = paths.resolve(target.npmPrefix, "..", "..");
106
+ const entryPath = stableDaemonEntry(target, platform);
107
+ const installation = daemonGlobalInstallation(entryPath, platform);
108
+ if (installation === null)
109
+ return { status: "unavailable" };
110
+ await access(entryPath);
111
+ const packageJson = JSON.parse(await readFile(paths.join(installation.packageRoot, "package.json"), "utf8"));
112
+ if (!packageJson || typeof packageJson !== "object" || !("name" in packageJson) || packageJson.name !== "@nowcrew/daemon"
113
+ || !("version" in packageJson) || typeof packageJson.version !== "string" || !isDaemonReleaseVersion(packageJson.version))
114
+ return { status: "unavailable" };
115
+ const base = { platform, profile: profile.name, profileHome: target.daemonHome, userHome,
116
+ uid: options.uid ?? process.getuid?.(), entryPath };
117
+ let spec;
118
+ if (platform === "win32") {
119
+ const identity = buildServiceSpec({ ...base, nodePath: process.execPath, environment: {} });
120
+ const nodePath = await windowsNodePath(identity.id, runner);
121
+ await access(nodePath);
122
+ spec = buildServiceSpec({ ...base, nodePath, environment: {} });
123
+ }
124
+ else {
125
+ const records = (await readManagedServiceRegistry({ userHome, localHome })).filter((record) => record.profile === profile.name
126
+ && localPathsMatch(record.daemonHome, target.daemonHome, platform));
127
+ if (records.length !== 1)
128
+ return { status: "unavailable" };
129
+ const record = records[0];
130
+ await access(record.nodePath);
131
+ const descriptor = await readFile(record.descriptorPath, "utf8");
132
+ spec = buildServiceSpec({ ...base, nodePath: record.nodePath, entryPath: record.entryPath,
133
+ environment: descriptorEnvironment(descriptor, platform) });
134
+ const expected = buildManagedServiceRecord({ spec, daemonHome: target.daemonHome, agentsRoot: target.agentsRoot,
135
+ nodePath: record.nodePath, entryPath, ...installation });
136
+ if (!managedServiceIdentityMatches(record, expected))
137
+ return { status: "unavailable" };
138
+ }
139
+ const service = await captureExactService(spec, runner);
140
+ if (!service.installed || !service.exact || !service.running)
141
+ return { status: "unavailable" };
142
+ return { status: "connected", result: {
143
+ workspaceId: receipt.target.workspaceId, workspaceSlug: receipt.target.workspaceSlug, machineId: receipt.target.machineId,
144
+ environment: receipt.target.environment, profile: profile.name, daemonVersion: packageJson.version,
145
+ serviceId: spec.id, serviceStatus: "running", helloConfirmed: true, localNowworkHome: localHome,
146
+ daemonHome: target.daemonHome, agentsRoot: target.agentsRoot,
147
+ ...(connection.storageId === undefined ? {} : { storageId: connection.storageId }),
148
+ } };
149
+ }
150
+ catch {
151
+ // Failed inspection is not proof of installation, connection, or credential ownership.
152
+ return { status: "unavailable" };
153
+ }
154
+ }
@@ -1,10 +1,12 @@
1
1
  import { computerSetupRecoveryGuidance, redactSetupSecrets, } from "./computer-setup-client.js";
2
+ import { automaticDaemonInstallStageMessage } from "./automatic-daemon-installer.js";
3
+ import { inspectExistingComputerSetup } from "./computer-setup-existing.js";
2
4
  import { isProductionServerHostname, serverUrlsMatch } from "./server-url.js";
3
5
  import { posix, win32 } from "node:path";
4
6
  import { resolveAgentsRoot } from "./computer-profile.js";
5
- function safeError(error, machineToken) {
6
- const message = redactSetupSecrets(error instanceof Error ? error.message : String(error), machineToken === undefined ? [] : [machineToken]);
7
- return new Error(message, error instanceof Error ? { cause: error } : undefined);
7
+ function safeError(error, machineToken, setupToken) {
8
+ const message = redactSetupSecrets(error instanceof Error ? error.message : String(error), [machineToken ?? "", setupToken ?? ""]);
9
+ return new Error(message);
8
10
  }
9
11
  export function assertBootstrapMatches(bootstrap, input) {
10
12
  if (bootstrap.machineId !== input.args.machineId) {
@@ -55,6 +57,18 @@ async function obtainBootstrap(input, deps) {
55
57
  deps.reportPairing("Checking computer connection information...");
56
58
  const result = await deps.client.poll(input.args.setupToken);
57
59
  if (result.status !== "authorized") {
60
+ deps.reportPairing("Checking the existing computer connection...");
61
+ const inspection = await (deps.inspectExistingSetup ?? ((candidate, client) => inspectExistingComputerSetup(candidate, client, {
62
+ ...(deps.platform === undefined ? {} : { platform: deps.platform }),
63
+ ...(deps.environment === undefined ? {} : { environment: deps.environment }),
64
+ })))(input, deps.client);
65
+ if (inspection.status === "connected") {
66
+ deps.reportPairing("This computer is already connected. No installation changes are needed.");
67
+ return inspection.result;
68
+ }
69
+ if (inspection.status === "pending") {
70
+ throw new Error("Setup is still in progress; check the original terminal and wait for it to finish");
71
+ }
58
72
  throw new Error("Setup command expired or unavailable; regenerate it in NowWork and run the new command");
59
73
  }
60
74
  return result.bootstrap;
@@ -120,12 +134,14 @@ export async function runComputerSetupOrchestration(input, deps) {
120
134
  };
121
135
  try {
122
136
  const authorized = await obtainBootstrap(input, deps);
137
+ if ("helloConfirmed" in authorized)
138
+ return authorized;
123
139
  const token = authorized.machineToken;
124
140
  bootstrap = authorized;
125
141
  machineToken = token;
126
142
  assertBootstrapMatches(authorized, input);
127
- deps.reportPairing("Downloading, installing, and starting the Daemon service...");
128
143
  const installation = await deps.installAutomatically({
144
+ onProgress: (stage) => deps.reportPairing(automaticDaemonInstallStageMessage[stage]),
129
145
  serverUrl: authorized.serverUrl,
130
146
  machineToken: token,
131
147
  workspaceId: authorized.workspaceId,
@@ -137,7 +153,6 @@ export async function runComputerSetupOrchestration(input, deps) {
137
153
  ...(authorized.registryUrl === undefined ? {} : { requestedRegistryUrl: authorized.registryUrl }),
138
154
  beforeRollback: confirmRollback,
139
155
  verifyAfterStart: async (candidate) => {
140
- deps.reportPairing("Confirming the computer connection...");
141
156
  const hello = await deps.client.waitForHello({
142
157
  machineToken: token,
143
158
  machineId: authorized.machineId,
@@ -209,6 +224,6 @@ export async function runComputerSetupOrchestration(input, deps) {
209
224
  + `local recovery blocked. ${computerSetupRecoveryGuidance(bootstrap)}`, { cause: error });
210
225
  }
211
226
  }
212
- throw safeError(error, machineToken);
227
+ throw safeError(error, machineToken, input.args.setupToken);
213
228
  }
214
229
  }
@@ -258,6 +258,7 @@ export async function runComputerSetupCommand(argv, overrides = {}) {
258
258
  reportPairing: (message) => deps[args.json === true ? "stderr" : "stdout"](`${localizedPairingMessage(lang, message)}\n`),
259
259
  now: deps.now,
260
260
  platform: deps.platform,
261
+ environment: deps.env,
261
262
  };
262
263
  const result = args.machineToken === undefined
263
264
  ? await runComputerSetupOrchestration({ args, serverUrl }, orchestrationDeps)
@@ -1,3 +1,4 @@
1
+ import { automaticDaemonInstallStageMessage } from "./automatic-daemon-installer.js";
1
2
  import { assertBootstrapMatches, localPathsMatch } from "./computer-setup-orchestration.js";
2
3
  import { redactSetupSecrets } from "./computer-setup-client.js";
3
4
  export async function runMachineTokenSetup(input, deps, machineToken) {
@@ -5,14 +6,13 @@ export async function runMachineTokenSetup(input, deps, machineToken) {
5
6
  deps.reportPairing("Checking computer connection information...");
6
7
  const target = await deps.client.tokenTarget(machineToken);
7
8
  assertBootstrapMatches(target, input);
8
- deps.reportPairing("Downloading, installing, and starting the Daemon service...");
9
9
  const installation = await deps.installAutomatically({
10
+ onProgress: (stage) => deps.reportPairing(automaticDaemonInstallStageMessage[stage]),
10
11
  serverUrl: target.serverUrl, machineToken, workspaceId: target.workspaceId, machineId: target.machineId,
11
12
  requestedProfile: target.profile,
12
13
  ...(target.releaseTag === undefined ? {} : { requestedReleaseTag: target.releaseTag }),
13
14
  ...(target.registryUrl === undefined ? {} : { requestedRegistryUrl: target.registryUrl }),
14
15
  verifyAfterStart: async (candidate) => {
15
- deps.reportPairing("Confirming the computer connection...");
16
16
  const deadline = deps.now() + 120_000;
17
17
  for (;;) {
18
18
  const current = await deps.client.tokenTarget(machineToken);
@@ -8,7 +8,7 @@ import { provePosixProcessGroupStopped } from "./execution-posix-stop-proof.js";
8
8
  export { JournalLockedError, JournalLockCorruptionError } from "./execution-journal-lock.js";
9
9
  const ExecutionIdSchema = z.string().uuid();
10
10
  const TimestampSchema = z.string().datetime({ offset: true });
11
- const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "memory_migration", "pi"]);
11
+ const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "pi"]);
12
12
  const RUNTIME_READY_SUFFIX = ".runtime-ready";
13
13
  const RawJournalEntrySchema = z.object({
14
14
  executionId: ExecutionIdSchema,
@@ -523,8 +523,6 @@ export function createExecutionJournal(agentsRoot, options = {}) {
523
523
  }
524
524
  };
525
525
  const pruneInternal = async () => {
526
- if (options.retainCompleted)
527
- return;
528
526
  const protectedExecutionIds = new Set(options.protectedExecutionIds?.() ?? []);
529
527
  const entries = await readAll();
530
528
  const acknowledged = entries
@@ -319,7 +319,7 @@ const RawExecutionCompletedSchema = z.object({
319
319
  providerErrorCategory: ProviderErrorCategorySchema.optional(),
320
320
  providerHttpStatus: ProviderHttpStatusSchema.optional(),
321
321
  providerErrorCode: ProviderErrorCodeSchema.optional(),
322
- runtime: z.union([RuntimeSchema, z.literal("memory_migration")]),
322
+ runtime: RuntimeSchema,
323
323
  model: z.string().optional(),
324
324
  resumed: z.boolean(),
325
325
  finalText: z.string().optional(),
@@ -1,7 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { isLocalMemoryV2Enabled, localMemoryV2Environment, resolveReposRoot } from "./config.js";
4
- import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
5
4
  import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
6
5
  import { JournalConflictError } from "./execution-journal.js";
7
6
  import { boundExecutionFrame } from "./execution-event-limit.js";
@@ -588,6 +587,26 @@ export async function runExecution(config, input, dependencies) {
588
587
  let activitySequence = 0;
589
588
  let consoleSequence = 0;
590
589
  let externalOutputSequence = 0;
590
+ const bufferedPiExternalOutput = [];
591
+ const reportExternalOutput = (text) => {
592
+ if (firstOutputTimer !== undefined) {
593
+ clearTimeout(firstOutputTimer);
594
+ firstOutputTimer = undefined;
595
+ }
596
+ const frame = DaemonToServerExecutionFrameSchema.parse({
597
+ type: "execution:output",
598
+ protocolVersion: 1,
599
+ executionId: spec.executionId,
600
+ channel: "external_answer",
601
+ text,
602
+ seq: externalOutputSequence++,
603
+ at: now().toISOString(),
604
+ });
605
+ try {
606
+ telemetry.enqueue(boundExecutionFrame(frame, config.executionLimits.maxEventBytes));
607
+ }
608
+ catch { /* final completion remains the authoritative repair */ }
609
+ };
591
610
  const callbacks = {
592
611
  onRuntimeStarting: () => {
593
612
  dependencies.onRuntimePhase?.("starting", spec.runtime.name);
@@ -712,23 +731,11 @@ export async function runExecution(config, input, dependencies) {
712
731
  } : {}),
713
732
  ...(spec.context.externalResponseSessionId || spec.context.answerStream ? {
714
733
  onExternalOutput: (text) => {
715
- if (firstOutputTimer !== undefined) {
716
- clearTimeout(firstOutputTimer);
717
- firstOutputTimer = undefined;
718
- }
719
- const frame = DaemonToServerExecutionFrameSchema.parse({
720
- type: "execution:output",
721
- protocolVersion: 1,
722
- executionId: spec.executionId,
723
- channel: "external_answer",
724
- text,
725
- seq: externalOutputSequence++,
726
- at: now().toISOString(),
727
- });
728
- try {
729
- telemetry.enqueue(boundExecutionFrame(frame, config.executionLimits.maxEventBytes));
734
+ if (spec.runtime.name === "pi") {
735
+ bufferedPiExternalOutput.push(text);
736
+ return;
730
737
  }
731
- catch { /* final completion remains the authoritative repair */ }
738
+ reportExternalOutput(text);
732
739
  },
733
740
  } : {}),
734
741
  };
@@ -839,9 +846,8 @@ export async function runExecution(config, input, dependencies) {
839
846
  const localMemoryGuardInBase = localMemoryV2Environment(config.serverUrl) === "custom";
840
847
  const boundedSystemPrompt = (context) => {
841
848
  const enabled = context.workspace.localMemoryV2Enabled ?? localMemoryV2Enabled;
842
- const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase && detectLocalMemoryStorageVersion(context.workspace.memory) !== 2 ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
849
+ const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
843
850
  ...(selfTraining ? { isolateLocalMemory: true } : {}),
844
- memoryQuery: spec.instructions.wakePrompt,
845
851
  collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
846
852
  });
847
853
  const base = typeof systemPromptWithLocalFacts === "string"
@@ -881,7 +887,6 @@ export async function runExecution(config, input, dependencies) {
881
887
  serverUrl: config.serverUrl,
882
888
  token: credential.token,
883
889
  agentId: credential.agentId,
884
- ...(dependencies.controlIdentity === undefined ? {} : dependencies.controlIdentity),
885
890
  agentsRoot: config.agentsRoot,
886
891
  reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
887
892
  ...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
@@ -912,6 +917,10 @@ export async function runExecution(config, input, dependencies) {
912
917
  clearTimeout(timeout);
913
918
  if (firstOutputTimer !== undefined)
914
919
  clearTimeout(firstOutputTimer);
920
+ if (spec.runtime.name === "pi" && result.exitCode === 0 && result.externalAnswer !== null) {
921
+ for (const text of bufferedPiExternalOutput)
922
+ reportExternalOutput(text);
923
+ }
915
924
  const finishedAt = now().toISOString();
916
925
  if (result.exitCode === 0 && result.finalText?.trim())
917
926
  memoryCaptureFinalText = result.finalText;
@@ -110,6 +110,7 @@ export class ExternalAnswerDecoder {
110
110
  pending = "";
111
111
  block = "";
112
112
  closed = null;
113
+ rejected = false;
113
114
  push(text) {
114
115
  if (!text)
115
116
  return [];
@@ -125,7 +126,7 @@ export class ExternalAnswerDecoder {
125
126
  }
126
127
  /** 整轮里最后一个完整闭合的标签块(已 trim);没有闭合过任何块则为 null。 */
127
128
  get lastClosedBlock() {
128
- return this.closed;
129
+ return this.rejected ? null : this.closed;
129
130
  }
130
131
  consume(value, output) {
131
132
  this.pending = "";
@@ -154,12 +155,21 @@ export class ExternalAnswerDecoder {
154
155
  this.block = "";
155
156
  this.state = "inside";
156
157
  }
158
+ else if (!tag.closing && this.state === "inside") {
159
+ // 嵌套答案标记说明模型正在讨论协议或混入了思考文本;
160
+ // 不要把内层短片段误当成最终答案。
161
+ this.rejected = true;
162
+ this.block = "";
163
+ this.state = "outside";
164
+ }
157
165
  else {
166
+ this.rejected = true;
158
167
  this.block = "";
159
168
  this.state = tag.closing ? "outside" : "inside";
160
169
  }
161
170
  }
162
171
  else if (tag.kind === "suspicious" || tag.kind === "known") {
172
+ this.rejected = true;
163
173
  this.block = "";
164
174
  this.state = "outside";
165
175
  }
@@ -171,6 +181,7 @@ export class ExternalAnswerDecoder {
171
181
  else if (this.pending.length > MAX_MARKER_TAG_LENGTH + 2) {
172
182
  const tag = classifyMarkerTag(this.pending.slice(1));
173
183
  if (tag.kind === "suspicious" || tag.kind === "known") {
184
+ this.rejected = true;
174
185
  this.block = "";
175
186
  this.state = "outside";
176
187
  }