@nowcrew/daemon 0.6.88-beta.2 → 0.6.88-beta.21

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 (81) hide show
  1. package/README.md +43 -13
  2. package/dist/agent-memory/policy.js +3 -1
  3. package/dist/computer-profile.js +9 -4
  4. package/dist/config.js +0 -9
  5. package/dist/daemon-update-eligibility.js +1 -1
  6. package/dist/execution-journal.js +9 -6
  7. package/dist/execution-protocol.js +2 -1
  8. package/dist/execution-runner.js +5 -4
  9. package/dist/list-models.js +2 -0
  10. package/dist/local-execution-lease.js +22 -0
  11. package/dist/local-executor.js +58 -45
  12. package/dist/local-memory/candidate.js +43 -0
  13. package/dist/local-memory/index-budget.js +8 -0
  14. package/dist/local-memory/inventory-cli.js +61 -0
  15. package/dist/local-memory/inventory-preview.js +154 -0
  16. package/dist/local-memory/inventory-source.js +159 -0
  17. package/dist/local-memory/pending-match.js +29 -0
  18. package/dist/local-memory/pending-query.js +100 -0
  19. package/dist/local-memory/pending-schema.js +29 -0
  20. package/dist/local-memory/pending-source.js +126 -0
  21. package/dist/local-memory/pending.js +34 -84
  22. package/dist/local-memory/prompt.js +14 -11
  23. package/dist/local-memory/read-cli.js +97 -12
  24. package/dist/local-memory/runtime.js +24 -17
  25. package/dist/local-memory/work-log-execution.js +31 -0
  26. package/dist/local-memory/work-log.js +58 -0
  27. package/dist/local-memory/write.js +27 -58
  28. package/dist/machine-info.js +54 -17
  29. package/dist/main.js +4 -69
  30. package/dist/native-thread-context.js +59 -0
  31. package/dist/pre-admission-rejection.js +64 -0
  32. package/dist/project-skills/execution-adapter.js +3 -2
  33. package/dist/project-skills/runtime-launch.js +14 -0
  34. package/dist/prompt.js +15 -15
  35. package/dist/provider-env.js +3 -6
  36. package/dist/runner.js +14 -16
  37. package/dist/runtime-probe.js +34 -5
  38. package/dist/runtimes/acp-probe-transport.js +69 -0
  39. package/dist/runtimes/claude-session-start-hook.js +24 -0
  40. package/dist/runtimes/claude-thread-runner.js +266 -0
  41. package/dist/runtimes/claude.js +23 -4
  42. package/dist/runtimes/codex-app-server-runner.js +43 -4
  43. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  44. package/dist/runtimes/deepseek-harness-runner.js +224 -0
  45. package/dist/runtimes/deepseek-harness.js +64 -11
  46. package/dist/runtimes/harness-probe-retry.js +42 -0
  47. package/dist/runtimes/kimi-acp-runner.js +63 -10
  48. package/dist/runtimes/opencode-runner.js +7 -1
  49. package/dist/serve.js +59 -101
  50. package/dist/session.js +4 -0
  51. package/dist/storage-identity.js +0 -3
  52. package/dist/storage-serve-adapter.js +6 -72
  53. package/dist/supervised-runtime.js +28 -18
  54. package/dist/thread-input-delivery.js +217 -0
  55. package/package.json +2 -3
  56. package/dist/codex-home-launch.js +0 -7
  57. package/dist/codex-home-mode-controller.js +0 -68
  58. package/dist/daemon-migration-controller.js +0 -211
  59. package/dist/daemon-migration-wiring.js +0 -22
  60. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  61. package/dist/runtimes/codex-home-migration.js +0 -112
  62. package/dist/runtimes/codex-home.js +0 -236
  63. package/dist/storage-control.js +0 -212
  64. package/dist/storage-copy.js +0 -628
  65. package/dist/storage-credential-guard.js +0 -21
  66. package/dist/storage-doctor.js +0 -228
  67. package/dist/storage-manifest.js +0 -1027
  68. package/dist/storage-migration-controller.js +0 -440
  69. package/dist/storage-migration-manifest.js +0 -105
  70. package/dist/storage-migration-policy.js +0 -100
  71. package/dist/storage-runtime-controller.js +0 -381
  72. package/dist/storage-runtime-readiness.js +0 -240
  73. package/dist/storage-safe-fs.js +0 -302
  74. package/dist/storage-status-delivery.js +0 -224
  75. package/dist/storage-status-replay.js +0 -140
  76. package/dist/storage-target-confirmation.js +0 -129
  77. package/dist/storage-worker-command.js +0 -37
  78. package/dist/storage-worker-executor.js +0 -449
  79. package/dist/storage-worker-handoff.js +0 -151
  80. package/dist/storage-worker-operation.js +0 -294
  81. package/dist/storage-worker-status.js +0 -259
@@ -1,3 +1,6 @@
1
+ import { AcpProbeTransport } from "./acp-probe-transport.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { dslog } from "../slog.js";
1
4
  import { once } from "node:events";
2
5
  import { isAbsolute } from "node:path";
3
6
  import { parseArgs } from "node:util";
@@ -6,6 +9,7 @@ import { pathToFileURL } from "node:url";
6
9
  import spawn from "cross-spawn";
7
10
  import { PROTOCOL_VERSION, RequestError, client, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
8
11
  import { augmentedPath } from "../runtime-path.js";
12
+ import { prepareThreadDelivery } from "../thread-input-delivery.js";
9
13
  import { startFirstProgressWatchdog } from "./progress-watchdog.js";
10
14
  import { assertKimiLegacyPromptFits, buildKimiArgs } from "./kimi.js";
11
15
  const ERROR_MESSAGE_CAP = 2_000;
@@ -193,18 +197,38 @@ async function stopChild(child) {
193
197
  await closed;
194
198
  }
195
199
  }
200
+ export class AcpProbeCleanupError extends Error {
201
+ constructor() { super("ACP probe process cleanup unconfirmed"); this.name = "AcpProbeCleanupError"; }
202
+ }
196
203
  /** Probe the ACP transport without starting a session or forcing an optional interactive login flow. */
197
204
  export async function probeKimiAcp(options, spawnProcess = spawn) {
198
205
  const provider = options.provider ?? "kimi";
199
206
  const permission = options.effectivePermission ?? "sandboxed";
200
- const child = spawnProcess(options.bin, providerArgs(provider, options.configPath), {
201
- cwd: process.cwd(),
202
- // probe 在 daemon 自身 PATH 下运行,补上用户级 CLI 目录,与 which 探测保持一致。
203
- env: { ...providerEnv(provider, permission), PATH: augmentedPath() },
207
+ const probeId = options.probeId ?? randomUUID();
208
+ const started = performance.now();
209
+ const elapsed = () => Math.round(performance.now() - started);
210
+ const transport = new AcpProbeTransport(elapsed);
211
+ let spawnConfirmedMs;
212
+ let reason = "initializing", protocolVersion;
213
+ let stderrSeen = false;
214
+ const fields = () => ({ probe_id: probeId, runtime: provider, duration_ms: elapsed(),
215
+ ...transport.fields(), spawn_confirmed_ms: spawnConfirmedMs, child_pid: child.pid,
216
+ reason, protocol_version: protocolVersion, stderr_seen: stderrSeen, aborted: options.signal?.aborted ?? false });
217
+ dslog("execution.acp_probe_started", "ACP 探测开始", { probe_id: probeId, runtime: provider, node_version: process.version });
218
+ const launchEnv = options.launchEnv ?? providerEnv(provider, permission);
219
+ const child = spawnProcess(options.bin, options.launchArgs ?? providerArgs(provider, options.configPath), {
220
+ cwd: options.launchCwd ?? process.cwd(),
221
+ // 显式 launchEnv 也决定 PATH/HOME;不能把它与父进程 PATH 混用。
222
+ env: { ...launchEnv, PATH: augmentedPath(launchEnv) },
204
223
  stdio: ["pipe", "pipe", "pipe"],
205
224
  });
225
+ child.once("spawn", () => {
226
+ spawnConfirmedMs = elapsed();
227
+ dslog("execution.acp_probe_spawned", "ACP 探测子进程已启动", fields());
228
+ });
206
229
  if (child.stdin === null || child.stdout === null || child.stderr === null)
207
230
  return false;
231
+ child.stderr.on("data", () => { stderrSeen = true; });
208
232
  child.stderr.resume();
209
233
  const app = client({ name: `nowcrew-daemon-${provider}-probe` })
210
234
  .onRequest(methods.client.session.requestPermission, () => ({
@@ -214,25 +238,36 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
214
238
  let timeout;
215
239
  let onAbort;
216
240
  try {
217
- const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
241
+ const stream = ndJsonStream(transport.writable(child.stdin), transport.readable(child.stdout));
218
242
  const connected = app.connectWith(stream, async (context) => {
219
- await context.request(methods.agent.initialize, {
243
+ transport.initializeRequested();
244
+ const initialized = await context.request(methods.agent.initialize, {
220
245
  protocolVersion: PROTOCOL_VERSION,
221
246
  clientCapabilities: {},
222
247
  clientInfo: { name: "nowcrew-daemon", version: "1" },
223
248
  });
224
- return true;
249
+ transport.initializeResponse();
250
+ protocolVersion = initialized.protocolVersion;
251
+ const ready = protocolVersion === PROTOCOL_VERSION;
252
+ if (reason === "initializing")
253
+ reason = ready ? "ready" : "protocol_mismatch";
254
+ dslog("execution.acp_probe_handshake", "ACP 握手结果", { ...fields(), ready });
255
+ return ready;
225
256
  });
226
257
  const result = await Promise.race([
227
258
  connected,
228
259
  new Promise((resolve) => {
229
260
  timeout = setTimeout(() => {
261
+ if (reason === "initializing")
262
+ reason = "timeout";
230
263
  void stopChild(child);
231
264
  resolve(false);
232
265
  }, PROBE_TIMEOUT_MS);
233
266
  }),
234
267
  new Promise((resolve) => {
235
268
  onAbort = () => {
269
+ if (reason === "initializing")
270
+ reason = "aborted";
236
271
  void stopChild(child);
237
272
  resolve(false);
238
273
  };
@@ -245,6 +280,8 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
245
280
  return result;
246
281
  }
247
282
  catch {
283
+ if (reason === "initializing")
284
+ reason = options.signal?.aborted ? "aborted" : "transport_error";
248
285
  return false;
249
286
  }
250
287
  finally {
@@ -252,17 +289,33 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
252
289
  clearTimeout(timeout);
253
290
  if (onAbort !== undefined)
254
291
  options.signal?.removeEventListener("abort", onAbort);
255
- await stopChild(child);
292
+ dslog("execution.acp_probe_cleanup_started", "ACP 探测清理开始", fields());
293
+ let cleanupFinished = false;
294
+ try {
295
+ await stopChild(child);
296
+ cleanupFinished = true;
297
+ }
298
+ catch {
299
+ throw new AcpProbeCleanupError();
300
+ }
301
+ finally {
302
+ dslog("execution.acp_probe_finished", "ACP 探测结束", { ...fields(),
303
+ cleanup_finished: cleanupFinished, exit_code: child.exitCode, exit_signal: child.signalCode,
304
+ level: reason === "ready" && cleanupFinished ? "INFO" : "WARN" });
305
+ }
256
306
  }
257
307
  }
258
- export async function runKimiAcp(options) {
308
+ export async function runKimiAcp(requestedOptions) {
309
+ // Until this adapter verifies native restoration, thread-bound runs are cold.
310
+ const options = process.env.CREW_THREAD_ROOT ? { ...requestedOptions, resume: false } : requestedOptions;
259
311
  const provider = options.provider ?? "kimi";
260
312
  const displayName = providerDisplayName(provider);
261
313
  const permission = options.effectivePermission ?? "full_access";
262
314
  if (provider === "deepseek-harness" && options.resume) {
263
315
  throw new Error("DeepSeek Harness ACP does not support session resume");
264
316
  }
265
- const prompt = await readPrompt();
317
+ const prompt = (await prepareThreadDelivery({ env: process.env, resumed: false,
318
+ sessionId: "unverified-cold-acp", wakePrompt: await readPrompt() })).prompt;
266
319
  const child = spawn(options.bin, providerArgs(provider, options.configPath), {
267
320
  cwd: process.cwd(),
268
321
  env: providerEnv(provider, permission),
@@ -4,6 +4,7 @@ import { parseArgs } from "node:util";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import spawn from "cross-spawn";
6
6
  import { augmentedPath } from "../runtime-path.js";
7
+ import { prepareThreadDelivery } from "../thread-input-delivery.js";
7
8
  import { buildOpenCodeArgs, createOpenCodeEventDecoder } from "./opencode.js";
8
9
  const ERROR_CAP = 2_000;
9
10
  async function readPrompt() {
@@ -86,7 +87,12 @@ async function runAttempt(options, prompt) {
86
87
  return { code: 0, staleSession: false };
87
88
  }
88
89
  export async function runOpenCode(options) {
89
- const prompt = await readPrompt();
90
+ const prompt = (await prepareThreadDelivery({ env: process.env, resumed: false,
91
+ sessionId: "unverified-cold-opencode", wakePrompt: await readPrompt() })).prompt;
92
+ if (process.env.CREW_THREAD_ROOT) {
93
+ const { sessionId: _unverified, ...coldOptions } = options;
94
+ return (await runAttempt(coldOptions, prompt)).code;
95
+ }
90
96
  const first = await runAttempt(options, prompt);
91
97
  if (!first.staleSession)
92
98
  return first.code;
package/dist/serve.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * 断线指数退避重连。一个 agent:start 进来就跑一次现有的 runAgent。
4
4
  */
5
5
  import { WebSocket } from "ws";
6
+ import { persistPreAdmissionRejection, rejectQueuedAdmission } from "./pre-admission-rejection.js";
6
7
  import { join } from "node:path";
7
8
  import { randomUUID } from "node:crypto";
8
9
  import { dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
@@ -51,10 +52,8 @@ import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
51
52
  import { probeRuntimeHealth } from "./runtime-health.js";
52
53
  import { handleRuntimeHealthProbeFrame } from "./runtime-health-handler.js";
53
54
  import { buildControlPlaneUrl } from "./control-plane-url.js";
54
- import { createServeMigrationController, handleMigrationControlMessage } from "./daemon-migration-wiring.js";
55
- import { createCodexHomeModeController } from "./codex-home-mode-controller.js";
56
55
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
57
- import { attachStorageMigrationRuntime, bindStorageMigrationServerIdentity, createServeStorageStatusDelivery, handleStorageMigrationControl, replayStorageMigrationStatus, storageMigrationCapabilities, } from "./storage-serve-adapter.js";
56
+ import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
58
57
  export { buildControlPlaneUrl } from "./control-plane-url.js";
59
58
  // normalize.ts 的活动种类 → activity 枚举
60
59
  const ACTIVITY_MAP = {
@@ -127,28 +126,13 @@ export function serve(config, opts = {}) {
127
126
  catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
128
127
  },
129
128
  });
130
- let storageMigrationAdmissionClosed = () => false;
131
- const migrationController = createServeMigrationController({
132
- config,
133
- ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
134
- isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0,
135
- sendStatus: (frame) => { try {
136
- if (ws?.readyState === WebSocket.OPEN)
137
- ws.send(JSON.stringify(frame));
138
- }
139
- catch { /* reconnect */ } },
140
- });
141
- const codexHomeModeController = createCodexHomeModeController({ ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), scheduleRestart: opts.update?.scheduleRestart ?? scheduleServiceRestart, getServiceSpec: async () => { const eligibility = await updateEligibility(); return eligibility.eligible ? eligibility.serviceSpec : null; }, isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0, sendStatus: (frame) => { try {
142
- if (ws?.readyState === WebSocket.OPEN)
143
- ws.send(JSON.stringify(frame));
144
- }
145
- catch { /* reconnect */ } } });
129
+ const storageIdentityAdmission = createStorageIdentityAdmission(opts.storageRuntime);
146
130
  const projectionCoordinator = createAgentProjectionCoordinator();
147
131
  const { projectRegistry, projectWorkspaceRegistry, projectDependencyProvisioner } = createProjectRegistryWiring(config.agentsRoot, opts.execution?.dependencies);
148
132
  const agentAbilityRuntime = createAgentAbilityRuntime(config.agentsRoot, {
149
133
  ...opts.agentAbility,
150
134
  coordinator: projectionCoordinator,
151
- isAdmissionClosed: () => storageMigrationAdmissionClosed(),
135
+ isAdmissionClosed: storageIdentityAdmission.isAdmissionClosed,
152
136
  });
153
137
  let projectSkillsController;
154
138
  const projectSkillsReconciler = opts.projectSkills?.reconciler ?? createProjectSkillsReconciler({
@@ -169,7 +153,7 @@ export function serve(config, opts = {}) {
169
153
  }
170
154
  catch { /* 下一次 ready 或项目操作会重新发送完整快照 */ }
171
155
  },
172
- isAdmissionClosed: () => storageMigrationAdmissionClosed(),
156
+ isAdmissionClosed: storageIdentityAdmission.isAdmissionClosed,
173
157
  reconcile: (handle, bindings) => projectSkillsReconciler.reconcile(handle, bindings),
174
158
  });
175
159
  const logRegistryFailure = (error) => dslog("project_skills.registry_failed", "本机项目注册表读取失败,本次不上报项目清单(能力不受影响)", {
@@ -177,7 +161,7 @@ export function serve(config, opts = {}) {
177
161
  });
178
162
  const machineHelloPublisher = createMachineHelloPublisher({
179
163
  effective: (hello) => {
180
- const configuredIdentity = opts.storageMigration?.identity;
164
+ const configuredIdentity = opts.storageRuntime?.identity;
181
165
  const currentIdentity = typeof configuredIdentity === "function"
182
166
  ? configuredIdentity()
183
167
  : configuredIdentity;
@@ -187,23 +171,7 @@ export function serve(config, opts = {}) {
187
171
  });
188
172
  const initializedProjectSkills = initializedProjectSkillsReconciler(() => projectSkillsController.ensureScanned(), projectSkillsReconciler);
189
173
  const configuredCapabilities = opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform);
190
- const storageCapabilities = storageMigrationCapabilities(configuredCapabilities, opts.storageMigration);
191
- const storageStatusDelivery = createServeStorageStatusDelivery(opts.storageMigration, {
192
- onPersistenceError: (error) => dslog("daemon.storage_status_outbox_failed", "存储迁移状态 outbox 持久化失败", { level: "ERROR", error_message: error.message }),
193
- transmit: (frame) => { try {
194
- if (ws?.readyState === WebSocket.OPEN)
195
- ws.send(JSON.stringify(frame));
196
- }
197
- catch { /* retry */ } },
198
- });
199
- const storageMigrationRuntime = attachStorageMigrationRuntime(opts.storageMigration, {
200
- isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0,
201
- waitForExecutionFrames: () => executionFrameQueue,
202
- flushLocalState: flushSlog,
203
- sendStatus: storageStatusDelivery.send,
204
- });
205
- storageMigrationAdmissionClosed = storageMigrationRuntime.isAdmissionClosed;
206
- const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...storageCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
174
+ const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...configuredCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
207
175
  const safeExecutionSend = (frame) => {
208
176
  try {
209
177
  if (ws?.readyState !== WebSocket.OPEN)
@@ -237,6 +205,11 @@ export function serve(config, opts = {}) {
237
205
  return;
238
206
  }
239
207
  safeExecutionSend(frame);
208
+ if (frame.type === "execution:rejected" && frame.preAdmissionProof) {
209
+ const entry = await executionJournal.get(frame.executionId);
210
+ if (entry?.completion)
211
+ completionRetransmitter.track(entry.completion);
212
+ }
240
213
  };
241
214
  const replayExecutionTelemetry = async () => {
242
215
  const frames = await executionTelemetry.replay();
@@ -316,57 +289,64 @@ export function serve(config, opts = {}) {
316
289
  let connectionEpochToken;
317
290
  ws.on("open", () => {
318
291
  connectionEpochToken = projectSkillsController.beginConnectionEpoch();
319
- storageMigrationRuntime.beginIdentityBinding();
292
+ storageIdentityAdmission.beginIdentityBinding();
320
293
  const openedSocket = ws;
294
+ const connectionId = randomUUID();
321
295
  backoff = 1000;
322
296
  connectedAt = Date.now();
323
297
  log(`🔌 已连接控制面 ${config.serverUrl}`);
324
- dslog("daemon.ws_open", "已连接控制面", { server_url: config.serverUrl });
298
+ dslog("daemon.ws_open", "已连接控制面", { server_url: config.serverUrl, connection_id: connectionId });
325
299
  // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
326
300
  void drainSpool();
327
301
  // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
328
- storageStatusDelivery.replayPending();
329
- const storageReplay = replayStorageMigrationStatus(opts.storageMigration, openedSocket, () => ws === openedSocket, (error) => {
330
- dslog("daemon.storage_status_replay_failed", "存储迁移状态重放失败", {
331
- level: "ERROR", error_message: error.message,
332
- });
333
- }, storageStatusDelivery.send);
334
302
  const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
335
303
  const helloPromise = (opts.machineInfo?.collect ?? collectMachineHello)(config.agentsRoot, config.executionLimits, process.platform, {
336
304
  ...(detectInstalled ? { detectInstalled } : {}),
337
305
  ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
338
306
  // First hello must not wait for third-party handshakes; optional transports arrive in the refresh.
339
307
  detectExecutable: async (installed) => conservativeExecutionRuntimes(installed),
340
- additionalCapabilities: async () => [...(await managedDaemonCapabilities(updateEligibility)), ...(opts.profileName === undefined ? [] : ["codex_home_mode_v1"])],
308
+ additionalCapabilities: async () => managedDaemonCapabilities(updateEligibility),
341
309
  capabilities: capabilities.machineHello,
342
- codexHomeMode: config.codexHomeMode,
343
310
  });
344
311
  runtimeFacts = helloPromise
345
312
  .then(async (hello) => {
346
313
  if (hello.executionProtocol === undefined) {
347
314
  executionRuntimesKnown = true;
315
+ dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
316
+ connection_id: connectionId, reason: "protocol_unavailable",
317
+ current_socket: ws === openedSocket, socket_open: openedSocket.readyState === WebSocket.OPEN,
318
+ socket_ready_state: openedSocket.readyState, installed_runtimes: JSON.stringify(hello.runtimes),
319
+ execution_runtimes: JSON.stringify(hello.executionRuntimes),
320
+ });
348
321
  return [];
349
322
  }
350
- const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable);
323
+ const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable, connectionId);
351
324
  executionRuntimesKnown = true;
352
- await storageReplay;
353
- if (ws === openedSocket && openedSocket.readyState === WebSocket.OPEN
354
- && detected.some((runtime) => !hello.executionRuntimes.includes(runtime))) {
325
+ const currentSocket = ws === openedSocket;
326
+ const socketOpen = openedSocket.readyState === WebSocket.OPEN;
327
+ const hasNewRuntime = detected.some((runtime) => !hello.executionRuntimes.includes(runtime));
328
+ dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
329
+ connection_id: connectionId, current_socket: currentSocket, socket_open: socketOpen,
330
+ socket_ready_state: openedSocket.readyState, installed_runtimes: JSON.stringify(hello.runtimes),
331
+ initial_execution_runtimes: JSON.stringify(hello.executionRuntimes), execution_runtimes: JSON.stringify(detected),
332
+ reason: !currentSocket ? "stale_connection" : !socketOpen ? "socket_not_open" : !hasNewRuntime ? "unchanged" : "publish",
333
+ });
334
+ if (currentSocket && socketOpen && hasNewRuntime) {
355
335
  detectedExecutionRuntimes = detected;
356
336
  machineHelloPublisher.publish({ ...hello, executionRuntimes: [...detected] }, openedSocket);
357
337
  }
358
338
  return detected;
359
339
  })
360
- .catch((error) => {
340
+ .catch(() => {
361
341
  executionRuntimesKnown = true;
362
342
  dslog("execution.runtime_detection_failed", "runtime 探测失败", {
363
- level: "ERROR", error_message: error.message,
343
+ level: "ERROR", reason: "detection_threw", connection_id: connectionId,
344
+ current_socket: ws === openedSocket, socket_open: openedSocket.readyState === WebSocket.OPEN,
364
345
  });
365
346
  return [];
366
347
  });
367
348
  void helloPromise
368
349
  .then(async (hello) => {
369
- await storageReplay;
370
350
  if (ws !== openedSocket || openedSocket.readyState !== WebSocket.OPEN)
371
351
  return;
372
352
  detectedExecutionRuntimes = hello.executionRuntimes;
@@ -414,8 +394,6 @@ export function serve(config, opts = {}) {
414
394
  if (typeof decoded !== "object" || decoded === null)
415
395
  return;
416
396
  const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
417
- if (storageStatusDelivery.acknowledge(decoded))
418
- return;
419
397
  if (rawType === "daemon:update" || rawType === "daemon:restart") {
420
398
  void updateController.handle(decoded).catch((error) => {
421
399
  dslog("daemon.control_failed", "daemon 控制操作处理失败", {
@@ -424,19 +402,6 @@ export function serve(config, opts = {}) {
424
402
  });
425
403
  return;
426
404
  }
427
- if (rawType === "daemon:migrate") {
428
- void handleMigrationControlMessage(migrationController, decoded).catch((error) => dslog("daemon.migration_failed", "daemon 布局迁移失败", { level: "ERROR", error_message: error.message }));
429
- return;
430
- }
431
- if (rawType === "daemon:codex-home-mode") {
432
- void codexHomeModeController.handle(decoded).catch((error) => dslog("daemon.codex_home_mode_failed", "Codex Home 模式切换失败", { level: "ERROR", error_message: error.message }));
433
- return;
434
- }
435
- if (handleStorageMigrationControl(opts.storageMigration, rawType, decoded, storageStatusDelivery.send, (kind, error) => {
436
- dslog(kind === "migration" ? "daemon.storage_migration_failed" : "daemon.storage_migration_confirmation_failed", kind === "migration" ? "存储迁移控制操作失败" : "存储迁移 hello 确认处理失败", { level: "ERROR", error_message: error.message });
437
- })) {
438
- return;
439
- }
440
405
  if (rawType.startsWith("execution:")) {
441
406
  const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
442
407
  if (!parsedExecution.success) {
@@ -498,10 +463,10 @@ export function serve(config, opts = {}) {
498
463
  return;
499
464
  }
500
465
  const spec = frame;
501
- if (storageMigrationRuntime.isAdmissionClosed()) {
466
+ if (storageIdentityAdmission.isAdmissionClosed()) {
502
467
  safeExecutionSend(ExecutionRejectedSchema.parse({
503
468
  type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
504
- reason: "resource_limit", message: "Storage migration is quiescing this machine",
469
+ reason: "resource_limit", message: "Storage identity binding is not complete",
505
470
  at: new Date().toISOString(),
506
471
  }));
507
472
  return;
@@ -560,18 +525,8 @@ export function serve(config, opts = {}) {
560
525
  : spec.context.scheduledRunId === undefined ? "normal" : "scheduled";
561
526
  let reservation = sharedSlots.reserve(spec.agent.handle, "execution", executionClass);
562
527
  if (!reservation.accepted) {
563
- dslog("execution.machine_queue_rejected", "机器执行队列已满", {
564
- level: "WARN",
565
- execution_id: spec.executionId,
566
- agent_handle: spec.agent.handle,
567
- execution_class: executionClass,
568
- ...reservation.facts,
569
- });
570
- safeExecutionSend(ExecutionRejectedSchema.parse({
571
- type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
572
- reason: "resource_limit", message: "Local machine execution queue is full",
573
- at: new Date().toISOString(),
574
- }));
528
+ await rejectQueuedAdmission({ journal: executionJournal, knownExecutionHashes, executionRuns,
529
+ report: reportExecutionFrame, replay: sendJournalStatus }, spec, executionClass, reservation.facts);
575
530
  return;
576
531
  }
577
532
  knownExecutionHashes.set(spec.executionId, hash);
@@ -741,6 +696,13 @@ export function serve(config, opts = {}) {
741
696
  ...reservation.facts,
742
697
  },
743
698
  report: reportExecutionFrame,
699
+ confirmPreAdmission: async (rejection) => {
700
+ const prior = await executionJournal.get(spec.executionId);
701
+ if (prior !== null || stopped || knownExecutionHashes.get(spec.executionId) !== hash
702
+ || executionRuns.get(spec.executionId) !== execution)
703
+ return false;
704
+ return await persistPreAdmissionRejection(executionJournal, spec, rejection) !== null;
705
+ },
744
706
  startupGate: runtimeStartupGate,
745
707
  startupTimeoutMs: config.executionLimits.startupTimeoutMs,
746
708
  onRuntimePhase: (phase, runtime) => {
@@ -812,17 +774,17 @@ export function serve(config, opts = {}) {
812
774
  // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
813
775
  const r = msg;
814
776
  setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
815
- if (opts.storageMigration !== undefined && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
777
+ if (opts.storageRuntime !== undefined && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
816
778
  const bindingSocket = ws;
817
- bindStorageMigrationServerIdentity(opts.storageMigration, { workspaceId: r.workspaceId, machineId: r.machineId }, {
779
+ bindStorageServerIdentity(opts.storageRuntime, { workspaceId: r.workspaceId, machineId: r.machineId }, {
818
780
  onBound: () => {
819
781
  if (bindingSocket !== ws || bindingSocket?.readyState !== WebSocket.OPEN)
820
782
  return;
821
- storageMigrationRuntime.completeIdentityBinding();
783
+ storageIdentityAdmission.completeIdentityBinding();
822
784
  machineHelloPublisher.republish();
823
785
  },
824
786
  onError: (error) => {
825
- dslog("daemon.storage_identity_bind_failed", "存储迁移 Workspace 身份绑定失败", { level: "ERROR", error_message: error.message });
787
+ dslog("daemon.storage_identity_bind_failed", "存储 Workspace 身份绑定失败", { level: "ERROR", error_message: error.message });
826
788
  if (bindingSocket === ws && bindingSocket?.readyState === WebSocket.OPEN)
827
789
  bindingSocket.close(1011, "storage identity binding failed");
828
790
  },
@@ -925,7 +887,7 @@ export function serve(config, opts = {}) {
925
887
  }
926
888
  if (msg.type !== "agent:start")
927
889
  return; // ready/error 等忽略
928
- if (storageMigrationRuntime.isAdmissionClosed())
890
+ if (storageIdentityAdmission.isAdmissionClosed())
929
891
  return;
930
892
  // 任务键:scheduled run 用 runId(每次运行独立 cwd/work-log;overlap 由 server 端 skip_if_running 管,
931
893
  // daemon 的 running 去重只兜底"同一 run 重复投递");普通唤醒仍是 线程锚点 ?? 频道。
@@ -1090,15 +1052,14 @@ export function serve(config, opts = {}) {
1090
1052
  const ackHint = `\n**协作礼仪:决定接手后,先用 \`${sendCmd}\` 在该任务线程说一声你来跟进**——用日常语言带上你对问题的理解或打算先查什么,不要写"我接 task #N"这类内部编号;写不出实质内容就不单发,并进第一条实质进展,但不要长时间闷头干活把线程空着。`;
1091
1053
  // 图片/文件附件:crew message read 会在消息下列出附件及其 id;图片需下载后用 Read 工具查看,才能真正"看到"内容。
1092
1054
  const attHint = `\n若消息带图片/文件附件(read 会列出 id),用 \`crew attachment get <id>\` 下载到本地,图片再用 Read 工具打开查看后再处理。`;
1093
- // 线程隔离:有 threadId 时用 `crew thread read` 只读本线程(避免被其他线程消息干扰);
1094
- // 顶层消息(无 threadId)用 `crew message read` 读整个频道。
1095
- const readCmd = threadId
1096
- ? `crew thread read`
1097
- : `crew message read --channel ${msg.channelId}`;
1055
+ // 启动上下文由执行层交付;legacy 提示词也不能要求模型补读或退回频道历史。
1056
+ const contextHint = threadId
1057
+ ? "直接依据程序交付的本线程上下文与新消息处理,无需启动补读历史。"
1058
+ : "没有线程锚点,只处理明确来信或请求补充当前任务线程,不读取频道历史猜测。";
1098
1059
  const wakeText = scheduled
1099
1060
  ? buildScheduledPrompt(msg.channelId, msg.wake?.content ?? "", scheduled.outputPolicy, scheduled.externalNotificationPolicy)
1100
1061
  : (msg.wake?.content
1101
- ? `你被唤醒(${msg.reason}): ${msg.wake.content}\n用 ${readCmd} 读${threadId ? "本线程" : "频道"}后按需处理。${reasonHint}${ackHint}${threadHint}${attHint}`
1062
+ ? `你被唤醒(${msg.reason}): ${msg.wake.content}\n${contextHint}${reasonHint}${ackHint}${threadHint}${attHint}`
1102
1063
  : undefined);
1103
1064
  const wakeOrigin = scheduled ? undefined : msg.wake?.origin;
1104
1065
  const guarded = await runWithOriginDecisionGuard(wakeOrigin, async (attempt) => {
@@ -1124,8 +1085,8 @@ export function serve(config, opts = {}) {
1124
1085
  },
1125
1086
  } : {}),
1126
1087
  ...(wakeOrigin ? { wakeOrigin, originDecisionAttempt: attempt } : {}),
1127
- // 唤醒锚点是具体消息(非纯频道唤醒)时,把它透传下去,供 `crew task create` 锚定到该消息。
1128
- ...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
1088
+ // legacy wake.threadId 已由 Server 归一到线程根;与普通触发消息 id 分开透传。
1089
+ ...(!scheduled && threadId ? { threadId, wakeMessageId: threadId } : {}),
1129
1090
  ...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
1130
1091
  ...(attemptWake ? { wake: attemptWake } : {}),
1131
1092
  }, reportActivity, reportConsole, {
@@ -1358,7 +1319,6 @@ export function serve(config, opts = {}) {
1358
1319
  concurrencyTelemetry.stop();
1359
1320
  runtimeProbe.stop();
1360
1321
  completionRetransmitter.stop();
1361
- storageStatusDelivery.stop();
1362
1322
  const deadline = createShutdownDeadline(shutdownTimeoutMs);
1363
1323
  if (reconnectTimer !== null) {
1364
1324
  clearTimeout(reconnectTimer);
@@ -1368,8 +1328,6 @@ export function serve(config, opts = {}) {
1368
1328
  const pending = [
1369
1329
  webSocketClosed,
1370
1330
  updateController.drain(),
1371
- ...(migrationController === null ? [] : [migrationController.drain()]),
1372
- codexHomeModeController.drain(),
1373
1331
  executionFrameQueue,
1374
1332
  ...executionRuns.values(),
1375
1333
  ...[...legacyRuns.values()].map((run) => run.done),
package/dist/session.js CHANGED
@@ -24,6 +24,10 @@ export async function readSession(runDir) {
24
24
  if (typeof o.sessionId !== "string" || !o.sessionId)
25
25
  return null;
26
26
  return {
27
+ ...(typeof o.runtime === "string" ? { runtime: o.runtime } : {}),
28
+ ...(typeof o.threadRoot === "string" ? { threadRoot: o.threadRoot } : {}),
29
+ ...(Number.isSafeInteger(o.threadContextSeq) && o.threadContextSeq >= 0 ? { threadContextSeq: o.threadContextSeq } : {}),
30
+ ...(typeof o.threadHistoryComplete === "boolean" ? { threadHistoryComplete: o.threadHistoryComplete } : {}),
27
31
  sessionId: o.sessionId,
28
32
  lastRunAt: typeof o.lastRunAt === "number" ? o.lastRunAt : 0,
29
33
  turns: typeof o.turns === "number" ? o.turns : 0,
@@ -39,8 +39,5 @@ export function storageHelloIdentity(input) {
39
39
  daemonRootFingerprint: fingerprint("daemon-root", layout.daemonRoot),
40
40
  agentRootFingerprint: fingerprint("agent-root", layout.agentItemsRoot),
41
41
  ...(ownership === undefined ? {} : { storageOwnership: ownership }),
42
- ...(installation.storageMigrationId === undefined
43
- ? {}
44
- : { storageMigrationId: installation.storageMigrationId }),
45
42
  });
46
43
  }
@@ -1,55 +1,12 @@
1
- import { STORAGE_MIGRATION_CAPABILITY, STORAGE_MIGRATION_STATUS_CAPABILITY, } from "./storage-control.js";
2
- import { createStorageStatusDelivery } from "./storage-status-delivery.js";
3
- export function storageMigrationCapabilities(configured, options) {
4
- if (options === undefined)
5
- return configured;
6
- return Object.freeze([...new Set([
7
- ...configured,
8
- STORAGE_MIGRATION_STATUS_CAPABILITY,
9
- ...(options.ready ? [STORAGE_MIGRATION_CAPABILITY] : []),
10
- ])]);
1
+ export function bindStorageServerIdentity(options, identity, callbacks) {
2
+ void Promise.resolve().then(() => options.bindServerIdentity?.(identity))
3
+ .then(callbacks.onBound).catch(callbacks.onError);
11
4
  }
12
- export function bindStorageMigrationServerIdentity(options, identity, callbacks) {
13
- const bind = options?.bindServerIdentity;
14
- if (bind === undefined) {
15
- if (options?.ready === true)
16
- callbacks.onBound();
17
- return;
18
- }
19
- void Promise.resolve().then(() => bind(identity)).then(callbacks.onBound).catch(callbacks.onError);
20
- }
21
- export function createServeStorageStatusDelivery(options, dependencies) {
22
- return createStorageStatusDelivery({
23
- ...(options?.statusOutboxPath === undefined ? {} : { outboxPath: options.statusOutboxPath }),
24
- ...(options?.statusRetryMs === undefined ? {} : { retryMs: options.statusRetryMs }),
25
- ...dependencies,
26
- });
27
- }
28
- export function attachStorageMigrationRuntime(options, dependencies) {
29
- let migrationAdmissionClosed = false;
5
+ /** 新连接必须重新验证 Server 身份,不能沿用上一次连接的执行准入。 */
6
+ export function createStorageIdentityAdmission(options) {
30
7
  let identityAdmissionClosed = options?.bindServerIdentity !== undefined;
31
- if (options?.ready === true) {
32
- options.attach?.({
33
- gate: {
34
- isBusy: dependencies.isBusy,
35
- closeAndFlush: async () => {
36
- migrationAdmissionClosed = true;
37
- await dependencies.waitForExecutionFrames();
38
- if (dependencies.isBusy()) {
39
- migrationAdmissionClosed = false;
40
- throw Object.assign(new Error("active execution entered before storage migration quiesce"), {
41
- code: "runtime_busy",
42
- });
43
- }
44
- await dependencies.flushLocalState();
45
- },
46
- reopen: () => { migrationAdmissionClosed = false; },
47
- },
48
- sendStatus: dependencies.sendStatus,
49
- });
50
- }
51
8
  return Object.freeze({
52
- isAdmissionClosed: () => migrationAdmissionClosed || identityAdmissionClosed,
9
+ isAdmissionClosed: () => identityAdmissionClosed,
53
10
  beginIdentityBinding: () => {
54
11
  if (options?.bindServerIdentity !== undefined)
55
12
  identityAdmissionClosed = true;
@@ -57,26 +14,3 @@ export function attachStorageMigrationRuntime(options, dependencies) {
57
14
  completeIdentityBinding: () => { identityAdmissionClosed = false; },
58
15
  });
59
16
  }
60
- export function replayStorageMigrationStatus(options, socket, isCurrent, onError, deliver) {
61
- return options?.replayStatus?.((frame) => {
62
- if (!isCurrent())
63
- return;
64
- if (deliver !== undefined)
65
- deliver(frame);
66
- else if (socket.readyState === socket.OPEN)
67
- socket.send(JSON.stringify(frame));
68
- }).catch(onError) ?? Promise.resolve();
69
- }
70
- export function handleStorageMigrationControl(options, rawType, raw, sendStatus, onError) {
71
- if (rawType === "daemon:storage-doctor" || rawType === "daemon:storage-migrate") {
72
- if (options?.ready === true)
73
- void options.controller.handle(raw).catch((error) => onError("migration", error));
74
- return true;
75
- }
76
- if (rawType !== "daemon:storage-migration-confirmed")
77
- return false;
78
- const confirm = options?.ready === false ? options.confirm : undefined;
79
- if (confirm !== undefined)
80
- void confirm(raw, sendStatus).catch((error) => onError("confirmation", error));
81
- return true;
82
- }