@nowcrew/daemon 0.6.98 → 0.6.99-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -1
- package/dist/automatic-daemon-installer.js +41 -10
- package/dist/automatic-service-migration.js +7 -6
- package/dist/cloud-report-upload.js +1 -10
- package/dist/computer-service.js +40 -42
- package/dist/computer-setup-client.js +43 -9
- package/dist/computer-setup-existing.js +154 -0
- package/dist/computer-setup-orchestration.js +32 -6
- package/dist/computer-setup.js +37 -6
- package/dist/computer-token-setup.js +46 -0
- package/dist/execution-journal.js +3 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +4 -1
- package/dist/i18n.js +16 -0
- package/dist/local-execution-prompt.js +8 -3
- package/dist/local-executor.js +64 -39
- package/dist/local-memory/commit-telemetry.js +30 -0
- package/dist/local-memory/execution-workspace.js +62 -0
- package/dist/local-memory/full-index-search.js +47 -0
- package/dist/local-memory/index-budget.js +9 -3
- package/dist/local-memory/initialization-identity.js +36 -0
- package/dist/local-memory/inventory-cli.js +52 -8
- package/dist/local-memory/inventory-source.js +15 -8
- package/dist/local-memory/inventory-v2.js +362 -0
- package/dist/local-memory/legacy-adapter.js +233 -0
- package/dist/local-memory/migration-access.js +242 -0
- package/dist/local-memory/migration-cli.js +54 -0
- package/dist/local-memory/migration-io.js +199 -0
- package/dist/local-memory/migration-pending.js +9 -0
- package/dist/local-memory/migration-plan.js +432 -0
- package/dist/local-memory/migration-schema.js +55 -0
- package/dist/local-memory/migration-state-invariants.js +65 -0
- package/dist/local-memory/migration-store.js +414 -0
- package/dist/local-memory/migration-tool-entry.js +21 -0
- package/dist/local-memory/pending-query.js +5 -2
- package/dist/local-memory/pending-schema.js +1 -1
- package/dist/local-memory/pending.js +16 -2
- package/dist/local-memory/prompt.js +21 -5
- package/dist/local-memory/read-cli.js +98 -76
- package/dist/local-memory/rollout.js +79 -11
- package/dist/local-memory/runtime.js +181 -13
- package/dist/local-memory/source-path.js +13 -0
- package/dist/local-memory/storage-format.js +249 -0
- package/dist/local-memory/write.js +189 -21
- package/dist/local-memory/writer-mode.js +13 -0
- package/dist/local-memory-diagnostics.js +96 -15
- package/dist/local-memory-telemetry.js +20 -3
- package/dist/machine-info.js +6 -3
- package/dist/marketplace-controller.js +8 -21
- package/dist/marketplace-protocol.js +1 -1
- package/dist/marketplace-remote.js +1 -1
- package/dist/marketplace-runner.js +1 -1
- package/dist/marketplace-serve.js +2 -4
- package/dist/memory-migration-controller.js +89 -0
- package/dist/memory-migration-inspect.js +108 -0
- package/dist/memory-migration-journal.js +14 -0
- package/dist/memory-migration-protocol.js +14 -0
- package/dist/memory-migration-runtime.js +305 -0
- package/dist/memory-prune-diagnostics.js +60 -4
- package/dist/message-file-preview.js +11 -1
- package/dist/npm-command.js +5 -5
- package/dist/powershell-command.js +12 -0
- package/dist/project-skills/machine-hello-publisher.js +95 -6
- package/dist/prompt.js +9 -17
- package/dist/runner.js +1 -1
- package/dist/runtimes/claude-thread-runner.js +25 -4
- package/dist/serve.js +92 -10
- package/dist/token.js +1 -0
- package/dist/windows-scheduled-task.js +3 -3
- package/dist/windows-task-principal.js +26 -0
- package/dist/workspace-control-handler.js +16 -3
- package/dist/workspace.js +13 -2
- package/package.json +1 -1
- package/dist/marketplace-maintenance-bridge.js +0 -79
- package/dist/marketplace-maintenance-protocol.js +0 -40
package/dist/runner.js
CHANGED
|
@@ -80,7 +80,7 @@ async function runAgentWithMachineEnvironment(config, input, onActivity = defaul
|
|
|
80
80
|
? { scheduledExternalNotificationPolicy: input.scheduled.externalNotificationPolicy }
|
|
81
81
|
: {}),
|
|
82
82
|
...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
|
|
83
|
-
memory: capMemoryForInject(workspace.memory),
|
|
83
|
+
memory: capMemoryForInject(workspace.memory, undefined, undefined, baseWake),
|
|
84
84
|
...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
|
|
85
85
|
collaborationToolsEnabled: config.codexCollaborationTools && runtime === "codex",
|
|
86
86
|
localMemoryV2Enabled: workspace.localMemoryV2Enabled ?? localMemoryV2Enabled,
|
|
@@ -12,6 +12,14 @@ const InputSchema = z.object({ bin: z.string().min(1), args: z.array(z.string())
|
|
|
12
12
|
wakePrompt: z.string().min(1), sessionId: z.string().min(1).optional(), resume: z.boolean() }).strict();
|
|
13
13
|
class PreflightError extends Error {
|
|
14
14
|
}
|
|
15
|
+
// Claude waits for every merged SessionStart hook before acknowledging SDK
|
|
16
|
+
// initialization. Those hooks may legitimately outlive the old 30s adapter
|
|
17
|
+
// deadline. Cap this phase below the daemon's default 120s fail-closed startup
|
|
18
|
+
// window so wrapper failure propagation and supervisor-owned tree cleanup retain
|
|
19
|
+
// margin. The 100s cap leaves a nominal 20s outside this phase; this runner's
|
|
20
|
+
// local TERM/KILL cleanup is itself bounded to 2s.
|
|
21
|
+
// Resume and cold recovery share this one budget; hook activity never extends it.
|
|
22
|
+
export const CLAUDE_INITIALIZE_TIMEOUT_MS = 100_000;
|
|
15
23
|
function hookCommand(path, nonce) {
|
|
16
24
|
const source = import.meta.url.endsWith(".ts");
|
|
17
25
|
const hook = fileURLToPath(new URL(`./claude-session-start-hook.${source ? "ts" : "js"}`, import.meta.url));
|
|
@@ -52,6 +60,9 @@ async function bounded(promise, milliseconds, label) {
|
|
|
52
60
|
clearTimeout(timer);
|
|
53
61
|
}
|
|
54
62
|
}
|
|
63
|
+
export function waitForClaudeInitialize(initialized, timeoutMs = CLAUDE_INITIALIZE_TIMEOUT_MS) {
|
|
64
|
+
return bounded(initialized, timeoutMs, "claude_initialize_timeout");
|
|
65
|
+
}
|
|
55
66
|
async function attempt(input, sessionId, resume, options) {
|
|
56
67
|
const proofDir = await mkdtemp(join(tmpdir(), "nowwork-claude-session-"));
|
|
57
68
|
const proofPath = join(proofDir, "proof.json");
|
|
@@ -188,7 +199,7 @@ async function attempt(input, sessionId, resume, options) {
|
|
|
188
199
|
try {
|
|
189
200
|
try {
|
|
190
201
|
await send({ type: "control_request", request_id: initId, request: { subtype: "initialize" } });
|
|
191
|
-
await
|
|
202
|
+
await waitForClaudeInitialize(initialized.promise, options.initializeTimeoutMs);
|
|
192
203
|
const proof = JSON.parse(await readFile(proofPath, "utf8"));
|
|
193
204
|
if (proof.nonce !== nonce || proof.sessionId !== sessionId || proof.source !== (resume ? "resume" : "startup")) {
|
|
194
205
|
throw new Error("claude_session_proof_mismatch");
|
|
@@ -238,15 +249,25 @@ async function attempt(input, sessionId, resume, options) {
|
|
|
238
249
|
}
|
|
239
250
|
export async function runClaudeThread(input, options = {}) {
|
|
240
251
|
const sessionId = input.sessionId ?? randomUUID();
|
|
252
|
+
const initializeBudgetMs = options.initializeTimeoutMs ?? CLAUDE_INITIALIZE_TIMEOUT_MS;
|
|
253
|
+
const now = options.now ?? (() => performance.now());
|
|
254
|
+
const initializeDeadline = now() + Math.max(0, initializeBudgetMs);
|
|
255
|
+
const runAttempt = (attemptSessionId, resume) => {
|
|
256
|
+
const remainingMs = Math.max(0, Math.ceil(initializeDeadline - now()));
|
|
257
|
+
if (remainingMs === 0)
|
|
258
|
+
return Promise.reject(new PreflightError("claude_native_session_unconfirmed"));
|
|
259
|
+
return attempt(input, attemptSessionId, resume, { ...options, initializeTimeoutMs: remainingMs });
|
|
260
|
+
};
|
|
241
261
|
try {
|
|
242
|
-
return await
|
|
262
|
+
return await runAttempt(sessionId, input.resume);
|
|
243
263
|
}
|
|
244
264
|
catch (error) {
|
|
245
265
|
// The failed attempt never received a business user turn or entered its
|
|
246
|
-
// model/tool loop. Native startup hooks may run again on cold recovery
|
|
266
|
+
// model/tool loop. Native startup hooks may run again on cold recovery,
|
|
267
|
+
// within the original aggregate initialize deadline.
|
|
247
268
|
if (!input.resume || !(error instanceof PreflightError || error instanceof ThreadDeliveryColdStartRequired))
|
|
248
269
|
throw error;
|
|
249
|
-
return
|
|
270
|
+
return runAttempt(randomUUID(), false);
|
|
250
271
|
}
|
|
251
272
|
}
|
|
252
273
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
package/dist/serve.js
CHANGED
|
@@ -9,11 +9,12 @@ 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, } from "./machine-info.js";
|
|
12
|
+
import { daemonCapabilities, daemonCapabilityBindings, MACHINE_HELLO_ACK_CAPABILITY, } 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";
|
|
16
16
|
import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
|
|
17
|
+
import { withMemoryAccess } from "./local-memory/migration-access.js";
|
|
17
18
|
import { normalizeScheduledContext } from "./scheduled-report.js";
|
|
18
19
|
import { formatDaemonLogLine } from "./log-format.js";
|
|
19
20
|
import { reportAgentRunComplete } from "./scheduled-run-report.js";
|
|
@@ -50,6 +51,8 @@ import { probeRuntimeHealth } from "./runtime-health.js";
|
|
|
50
51
|
import { createServeMarketplace } from "./marketplace-serve.js";
|
|
51
52
|
import { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
52
53
|
import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
|
|
54
|
+
import { createMemoryMigrationController } from "./memory-migration-controller.js";
|
|
55
|
+
import { createMemoryMigrationJournal, replayExecutionJournals } from "./memory-migration-journal.js";
|
|
53
56
|
import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
|
|
54
57
|
export { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
55
58
|
// normalize.ts 的活动种类 → activity 枚举
|
|
@@ -65,12 +68,18 @@ export function serve(config, opts = {}) {
|
|
|
65
68
|
let serverCapabilities = new Set();
|
|
66
69
|
let backoff = 1000;
|
|
67
70
|
const maxBackoff = opts.maxBackoffMs ?? 30_000;
|
|
71
|
+
// hello 重试跑满仍未 ack 时主动断连重连(破 server 侧连接归属/fence 死锁)。收到 ack 清零;
|
|
72
|
+
// 连续多次仍拿不到 ack 则停手,退回 connected-offline,避免事故期把控制面打成重连风暴。
|
|
73
|
+
const maxUnackedHelloReconnects = 3;
|
|
74
|
+
let unackedHelloReconnects = 0;
|
|
68
75
|
const testShutdown = readTestShutdownConfiguration(process.env);
|
|
69
76
|
const shutdownTimeoutMs = opts.shutdownTimeoutMs ?? testShutdown.timeoutMs ?? 30_000;
|
|
70
77
|
const createWebSocket = opts.createWebSocket ?? ((url) => new WebSocket(url));
|
|
71
78
|
let reconnectTimer = null;
|
|
72
79
|
let stopPromise = null;
|
|
73
80
|
let executionJournal;
|
|
81
|
+
let migrationExecutionJournal;
|
|
82
|
+
let migrationJournalReady = false;
|
|
74
83
|
const injectedExecutionJournal = opts.execution?.journal;
|
|
75
84
|
const agentMemory = opts.execution?.dependencies?.agentMemory
|
|
76
85
|
?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
|
|
@@ -85,6 +94,15 @@ export function serve(config, opts = {}) {
|
|
|
85
94
|
startupGapMs: config.executionLimits.startupGapMs,
|
|
86
95
|
});
|
|
87
96
|
const localSlots = createSharedSlotManager(config.executionLimits);
|
|
97
|
+
const memoryMigrations = createMemoryMigrationController({ config, coordinator: hostCoordinator,
|
|
98
|
+
isAdmissionClosed: () => stopped || storageIdentityAdmission.isAdmissionClosed(),
|
|
99
|
+
journal: () => { if (!migrationJournalReady)
|
|
100
|
+
throw new Error("memory_migration_journal_unavailable"); return migrationExecutionJournal; }, send: frame => {
|
|
101
|
+
if (frame.type === "execution:completed")
|
|
102
|
+
completionRetransmitter.track(frame);
|
|
103
|
+
else if (ws?.readyState === WebSocket.OPEN)
|
|
104
|
+
ws.send(JSON.stringify(frame));
|
|
105
|
+
} });
|
|
88
106
|
const sharedSlots = hostCoordinatedSlotManager(localSlots, hostCoordinator);
|
|
89
107
|
const concurrencyTelemetry = createExecutionConcurrencyTelemetry(sharedSlots, {
|
|
90
108
|
...(opts.execution?.concurrencySnapshotIntervalMs === undefined
|
|
@@ -118,7 +136,7 @@ export function serve(config, opts = {}) {
|
|
|
118
136
|
catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
|
|
119
137
|
},
|
|
120
138
|
});
|
|
121
|
-
const marketplace = createServeMarketplace(config, localSlots, hostCoordinator
|
|
139
|
+
const marketplace = createServeMarketplace(config, localSlots, hostCoordinator);
|
|
122
140
|
const storageIdentityAdmission = createStorageIdentityAdmission(opts.storageRuntime);
|
|
123
141
|
const projectionCoordinator = createAgentProjectionCoordinator();
|
|
124
142
|
const { projectRegistry, projectWorkspaceRegistry, projectDependencyProvisioner } = createProjectRegistryWiring(config.agentsRoot, opts.execution?.dependencies);
|
|
@@ -160,7 +178,41 @@ export function serve(config, opts = {}) {
|
|
|
160
178
|
: configuredIdentity;
|
|
161
179
|
return currentIdentity === undefined ? hello : { ...hello, ...currentIdentity };
|
|
162
180
|
},
|
|
163
|
-
onSent: (hello) =>
|
|
181
|
+
onSent: (hello, attempt) => dslog("daemon.machine_hello_send_queued", "机器 hello 已进入发送队列", {
|
|
182
|
+
attempt, hostname: hello.hostname, daemon_version: hello.daemonVersion,
|
|
183
|
+
}),
|
|
184
|
+
onSendFailed: (hello, attempt, error) => dslog("daemon.machine_hello_send_failed", "机器 hello 发送失败", {
|
|
185
|
+
level: "WARN", attempt, hostname: hello.hostname, daemon_version: hello.daemonVersion,
|
|
186
|
+
error_message: error instanceof Error ? error.message : String(error),
|
|
187
|
+
}),
|
|
188
|
+
onAck: (hello) => {
|
|
189
|
+
unackedHelloReconnects = 0; // 拿到确认 = 本连接登记成功,清零强制重连计数。
|
|
190
|
+
dslog("daemon.machine_hello_ack_received", "机器 hello 已收到控制面确认", {
|
|
191
|
+
hostname: hello.hostname, daemon_version: hello.daemonVersion,
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
// 仅当当前 server 声明支持 ack 才重试;老 server 收不到 ack,重试无意义(退回单次上报)。
|
|
195
|
+
ackSupported: () => serverCapabilities.has(MACHINE_HELLO_ACK_CAPABILITY),
|
|
196
|
+
// 重试跑满仍未 ack → 断开当前连接,靠重连换新连接(新 epoch/token)破 server 侧 fence 死锁。
|
|
197
|
+
onExhausted: (socket) => {
|
|
198
|
+
if (stopped || ws !== socket || socket.readyState !== WebSocket.OPEN)
|
|
199
|
+
return;
|
|
200
|
+
if (unackedHelloReconnects >= maxUnackedHelloReconnects) {
|
|
201
|
+
dslog("daemon.machine_hello_unacked_giveup", "机器 hello 连续多次重连仍未确认,停止强制重连(保持连接,等待下次事件或人工介入)", {
|
|
202
|
+
level: "ERROR", reconnects: unackedHelloReconnects,
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
unackedHelloReconnects += 1;
|
|
207
|
+
// 抖动重连间隔:控制面回滚时多台 daemon 会同时耗尽重试,固定间隔会同步重连造成羊群。
|
|
208
|
+
// close handler 用当前 backoff 排重连,这里注入 1~4s 随机值把各机错峰(open 会再把它复位)。
|
|
209
|
+
const jitteredBackoff = 1_000 + Math.floor(Math.random() * 3_000);
|
|
210
|
+
backoff = Math.min(jitteredBackoff, maxBackoff);
|
|
211
|
+
dslog("daemon.machine_hello_unacked_reconnect", "机器 hello 重试跑满仍未确认,断开重连换新连接", {
|
|
212
|
+
level: "WARN", reconnects: unackedHelloReconnects, backoff_ms: backoff,
|
|
213
|
+
});
|
|
214
|
+
socket.close(1011, "machine hello unacknowledged");
|
|
215
|
+
},
|
|
164
216
|
});
|
|
165
217
|
const machineRuntime = createMachineRuntimeEnvironment({
|
|
166
218
|
config,
|
|
@@ -232,7 +284,7 @@ export function serve(config, opts = {}) {
|
|
|
232
284
|
return null;
|
|
233
285
|
};
|
|
234
286
|
const sendSnapshot = async (reqId) => {
|
|
235
|
-
const entries = (await executionJournal
|
|
287
|
+
const entries = (await replayExecutionJournals(executionJournal, migrationExecutionJournal)).map(snapshotEntry).filter((entry) => entry !== null);
|
|
236
288
|
safeExecutionSend(ExecutionSnapshotSchema.parse({
|
|
237
289
|
type: "execution:snapshot", protocolVersion: 1, reqId, entries,
|
|
238
290
|
}));
|
|
@@ -310,9 +362,16 @@ export function serve(config, opts = {}) {
|
|
|
310
362
|
// 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
|
|
311
363
|
void drainSpool();
|
|
312
364
|
// 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
|
|
365
|
+
machineHelloPublisher.attach(openedSocket);
|
|
313
366
|
machineRuntime.open(openedSocket, { capabilities: capabilities.machineHello, beforePublish: Promise.resolve(), connectionId });
|
|
314
367
|
opts.onOpen?.(ws);
|
|
315
368
|
void sendSnapshot(`reconnect-${randomUUID()}`);
|
|
369
|
+
// 普通回执不等待维护日志IO;两类记录分别重放到同一发送器。
|
|
370
|
+
void migrationExecutionJournal.replay().then(entries => {
|
|
371
|
+
for (const entry of entries)
|
|
372
|
+
if (entry.completion && !entry.completionAcknowledged)
|
|
373
|
+
completionRetransmitter.track(entry.completion);
|
|
374
|
+
}).catch(() => dslog("memory_migration.journal_unavailable", "维护回执重放失败,普通回执继续", { level: "ERROR" }));
|
|
316
375
|
void executionJournal.replay()
|
|
317
376
|
.then((entries) => {
|
|
318
377
|
for (const entry of entries) {
|
|
@@ -352,6 +411,11 @@ export function serve(config, opts = {}) {
|
|
|
352
411
|
if (typeof decoded !== "object" || decoded === null)
|
|
353
412
|
return;
|
|
354
413
|
const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
|
|
414
|
+
if (rawType === "machine:hello_ack") {
|
|
415
|
+
const ackSeq = "helloSeq" in decoded && typeof decoded.helloSeq === "number" ? decoded.helloSeq : undefined;
|
|
416
|
+
machineHelloPublisher.acknowledge(connectionSocket, ackSeq);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
355
419
|
if (rawType === "machine:environment:get" || rawType === "machine:environment:refresh") {
|
|
356
420
|
await handleMachineEnvironmentFrame(decoded, machineRuntime.provider, sendToConnection);
|
|
357
421
|
return;
|
|
@@ -387,7 +451,8 @@ export function serve(config, opts = {}) {
|
|
|
387
451
|
return;
|
|
388
452
|
if (frame.type === "execution:completion-ack") {
|
|
389
453
|
try {
|
|
390
|
-
await executionJournal.
|
|
454
|
+
const journal = await executionJournal.get(frame.executionId) ? executionJournal : migrationExecutionJournal;
|
|
455
|
+
await journal.acknowledgeCompletion(frame.executionId);
|
|
391
456
|
const attempts = completionRetransmitter.acknowledge(frame.executionId);
|
|
392
457
|
dslog("execution.completion_acknowledged", "execution completion ACK 已持久化", {
|
|
393
458
|
execution_id: frame.executionId,
|
|
@@ -413,6 +478,8 @@ export function serve(config, opts = {}) {
|
|
|
413
478
|
return;
|
|
414
479
|
}
|
|
415
480
|
if (frame.type === "execution:cancel") {
|
|
481
|
+
if (memoryMigrations.cancel(frame.executionId))
|
|
482
|
+
return;
|
|
416
483
|
if (!knownExecutionHashes.has(frame.executionId)
|
|
417
484
|
&& !executionRuns.has(frame.executionId)
|
|
418
485
|
&& await executionJournal.get(frame.executionId) === null)
|
|
@@ -645,6 +712,7 @@ export function serve(config, opts = {}) {
|
|
|
645
712
|
const execution = executeProtocol(config, spec, {
|
|
646
713
|
...opts.execution?.dependencies,
|
|
647
714
|
machineEnvironment: machineRuntime.provider,
|
|
715
|
+
...(memoryMigrations.getIdentity() === null ? {} : { controlIdentity: memoryMigrations.getIdentity() }),
|
|
648
716
|
capabilities: capabilities.executionRunner,
|
|
649
717
|
projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkills,
|
|
650
718
|
abilityRelease: opts.execution?.dependencies?.abilityRelease ?? agentAbilityRuntime.materializer,
|
|
@@ -735,6 +803,13 @@ export function serve(config, opts = {}) {
|
|
|
735
803
|
// ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
|
|
736
804
|
const r = msg;
|
|
737
805
|
setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
|
|
806
|
+
if (r.workspaceId && r.machineId)
|
|
807
|
+
memoryMigrations.setIdentity({ workspaceId: r.workspaceId, machineId: r.machineId });
|
|
808
|
+
// 旧 server 无此字段 → 空集合(daemon 保持全部旧行为)。ackSupported() 依赖它判断是否重试,
|
|
809
|
+
// 必须在任何 republish() 之前就位;否则本轮 republish 看到空集合→不排重试→整套 ack/重连机制哑火。
|
|
810
|
+
serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
|
|
811
|
+
? r.serverCapabilities.filter((c) => typeof c === "string")
|
|
812
|
+
: []);
|
|
738
813
|
if (opts.storageRuntime !== undefined && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
|
|
739
814
|
const bindingSocket = ws;
|
|
740
815
|
bindStorageServerIdentity(opts.storageRuntime, { workspaceId: r.workspaceId, machineId: r.machineId }, {
|
|
@@ -751,10 +826,7 @@ export function serve(config, opts = {}) {
|
|
|
751
826
|
},
|
|
752
827
|
});
|
|
753
828
|
}
|
|
754
|
-
|
|
755
|
-
serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
|
|
756
|
-
? r.serverCapabilities.filter((c) => typeof c === "string")
|
|
757
|
-
: []);
|
|
829
|
+
machineHelloPublisher.republish();
|
|
758
830
|
if (serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)) {
|
|
759
831
|
void projectSkillsController.initialize()
|
|
760
832
|
.then(() => projectSkillsController.publishCurrent())
|
|
@@ -801,6 +873,8 @@ export function serve(config, opts = {}) {
|
|
|
801
873
|
}
|
|
802
874
|
catch { /* server 会按请求超时处理 */ } }))
|
|
803
875
|
return;
|
|
876
|
+
if (await memoryMigrations.handle(msg))
|
|
877
|
+
return;
|
|
804
878
|
if (await agentAbilityRuntime.tryHandleControlMessage(msg, serverCapabilities, (frame) => ws?.send(JSON.stringify(frame))))
|
|
805
879
|
return;
|
|
806
880
|
// 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
|
|
@@ -815,7 +889,7 @@ export function serve(config, opts = {}) {
|
|
|
815
889
|
try {
|
|
816
890
|
const data2 = req.type === "raft:inspect"
|
|
817
891
|
? await inspectRaftWorkspace(req.path)
|
|
818
|
-
: await importRaftWorkspace(req.path, join(config.agentsRoot, req.handle));
|
|
892
|
+
: await withMemoryAccess(join(config.agentsRoot, req.handle), () => importRaftWorkspace(req.path, join(config.agentsRoot, req.handle)), { create: true });
|
|
819
893
|
if (req.type === "raft:import") {
|
|
820
894
|
log(`📦 已导入 raft 工作区 → ${req.handle}: 复制 ${data2.copied.length} 项`);
|
|
821
895
|
}
|
|
@@ -1203,10 +1277,12 @@ export function serve(config, opts = {}) {
|
|
|
1203
1277
|
});
|
|
1204
1278
|
connectionSocket.on("close", (code) => {
|
|
1205
1279
|
machineRuntime.close(connectionSocket);
|
|
1280
|
+
machineHelloPublisher.detach(connectionSocket);
|
|
1206
1281
|
if (stopped || ws !== connectionSocket)
|
|
1207
1282
|
return;
|
|
1208
1283
|
// server 能力随连接失效;下个连接的 ready 帧重新声明(防降级重连后沿用过期能力)。
|
|
1209
1284
|
serverCapabilities = new Set();
|
|
1285
|
+
memoryMigrations.clearIdentity();
|
|
1210
1286
|
completionRetransmitter.pause();
|
|
1211
1287
|
// 4001 = 控制面应用级「鉴权失败」关闭码(见 server control-plane.ts)。即使上面的
|
|
1212
1288
|
// error 帧因 close 抢先而丢失,也能据关闭码识别这是凭证失效——退避拉满,不再每秒热循环。
|
|
@@ -1259,6 +1335,9 @@ export function serve(config, opts = {}) {
|
|
|
1259
1335
|
writeStderr: (line) => process.stderr.write(line),
|
|
1260
1336
|
});
|
|
1261
1337
|
await runProjectSkillStartupCleanup(config.agentsRoot, executionJournal, dslog);
|
|
1338
|
+
migrationExecutionJournal = createMemoryMigrationJournal(config.agentsRoot);
|
|
1339
|
+
await migrationExecutionJournal.reconcileAfterRestart().then(() => { migrationJournalReady = true; })
|
|
1340
|
+
.catch(() => dslog("memory_migration.journal_unavailable", "迁移日志恢复失败,普通执行继续,迁移请求单独报错", { level: "ERROR" }));
|
|
1262
1341
|
await connect();
|
|
1263
1342
|
})();
|
|
1264
1343
|
const stop = () => {
|
|
@@ -1276,6 +1355,7 @@ export function serve(config, opts = {}) {
|
|
|
1276
1355
|
const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
|
|
1277
1356
|
const pending = [
|
|
1278
1357
|
machineRuntime.stop(),
|
|
1358
|
+
memoryMigrations.stop(),
|
|
1279
1359
|
webSocketClosed,
|
|
1280
1360
|
updateController.drain(),
|
|
1281
1361
|
marketplace.stop(),
|
|
@@ -1294,6 +1374,8 @@ export function serve(config, opts = {}) {
|
|
|
1294
1374
|
if (executionJournal !== undefined) {
|
|
1295
1375
|
await executionJournal.close({ signal: deadline.signal });
|
|
1296
1376
|
}
|
|
1377
|
+
if (migrationExecutionJournal !== undefined)
|
|
1378
|
+
await migrationExecutionJournal.close({ signal: deadline.signal });
|
|
1297
1379
|
}
|
|
1298
1380
|
finally {
|
|
1299
1381
|
deadline.dispose();
|
package/dist/token.js
CHANGED
|
@@ -7,6 +7,7 @@ export async function mintAgentToken(serverUrl, machineToken, handle, displayNam
|
|
|
7
7
|
},
|
|
8
8
|
// 把本轮唤醒的线程根钉到 per-launch 令牌:服务端据此让 task/消息默认绑/回当前线程。
|
|
9
9
|
body: JSON.stringify({
|
|
10
|
+
memoryStateGuardVersion: 1,
|
|
10
11
|
handle,
|
|
11
12
|
...(displayName ? { displayName } : {}),
|
|
12
13
|
...(options.wakeThreadRoot ? { wakeThreadRoot: options.wakeThreadRoot } : {}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { systemCommandRunner, windowsCommandArg, } from "./computer-service.js";
|
|
2
2
|
import { win32 } from "node:path";
|
|
3
|
+
import { powershellCommandArgs } from "./powershell-command.js";
|
|
3
4
|
function encoded(value) {
|
|
4
5
|
return Buffer.from(value, "utf16le").toString("base64");
|
|
5
6
|
}
|
|
@@ -33,10 +34,9 @@ export async function inspectWindowsServiceUpdateScope(spec, npmPrefix, daemonPi
|
|
|
33
34
|
"$enginePid=if($instances.Count -eq 1){[int64]$instances[0].EnginePID}else{0};",
|
|
34
35
|
"[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;",
|
|
35
36
|
].join("");
|
|
36
|
-
const result = await runner("powershell.exe", [
|
|
37
|
-
"-NoProfile", "-NonInteractive", "-Command", script,
|
|
37
|
+
const result = await runner("powershell.exe", powershellCommandArgs(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;
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/** 控制面文件/技能查看的按需处理,独立于执行生命周期。 */
|
|
2
|
-
import {
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
3
4
|
import { readSharedMessageFile } from "./message-file-preview.js";
|
|
4
5
|
import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
|
|
5
6
|
import { listSkills } from "./skills.js";
|
|
6
7
|
import { listRuntimeModels } from "./list-models.js";
|
|
8
|
+
import { withMemoryAccess } from "./local-memory/migration-access.js";
|
|
9
|
+
async function listProtectedWorkspace(root, path) {
|
|
10
|
+
try {
|
|
11
|
+
await lstat(root);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code !== "ENOENT")
|
|
15
|
+
throw error;
|
|
16
|
+
return { root: resolve(root), path: path.replace(/^\/+/, ""), entries: [] };
|
|
17
|
+
}
|
|
18
|
+
return withMemoryAccess(root, () => listWorkspace(root, path));
|
|
19
|
+
}
|
|
7
20
|
export async function handleWorkspaceControlRequest(raw, agentsRoot, registry, models = listRuntimeModels) {
|
|
8
21
|
if (!raw || typeof raw !== "object")
|
|
9
22
|
return null;
|
|
@@ -15,8 +28,8 @@ export async function handleWorkspaceControlRequest(raw, agentsRoot, registry, m
|
|
|
15
28
|
try {
|
|
16
29
|
const root = join(agentsRoot, request.handle);
|
|
17
30
|
const data = request.type === "fs:read-shared" ? await readSharedMessageFile(agentsRoot, raw, registry)
|
|
18
|
-
: request.type === "fs:list" ? await
|
|
19
|
-
: request.type === "fs:read" ? await readWorkspaceFile(root, request.path)
|
|
31
|
+
: request.type === "fs:list" ? await listProtectedWorkspace(root, request.path)
|
|
32
|
+
: request.type === "fs:read" ? await withMemoryAccess(root, () => readWorkspaceFile(root, request.path))
|
|
20
33
|
: request.type === "skills:list" ? await listSkills(request.runtime)
|
|
21
34
|
: { models: await models(request.runtime ?? "") };
|
|
22
35
|
return { type: "fs:result", reqId: request.reqId, ok: true, data };
|
package/dist/workspace.js
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
*
|
|
9
9
|
* spawn runtime 时把 <ws>/.crew 置于 PATH 最前,于是 agent 的 `crew ...` 命中 wrapper。
|
|
10
10
|
*/
|
|
11
|
-
import { mkdir, writeFile, readFile, chmod, access, lstat } from "node:fs/promises";
|
|
11
|
+
import { mkdir, writeFile, readFile, chmod, access, lstat, realpath } from "node:fs/promises";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
15
|
import { createRequire } from "node:module";
|
|
16
16
|
import { dslog } from "./slog.js";
|
|
17
|
+
import { MigrationStateSchema } from "./local-memory/migration-schema.js";
|
|
18
|
+
import { assertMigrationStateIntegrity } from "./local-memory/migration-state-invariants.js";
|
|
19
|
+
import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
|
|
17
20
|
/** 文件系统安全的 taskKey:仅留 [\w.-],其余转 _,截断,避免路径穿越/超长。 */
|
|
18
21
|
export function safeKey(key) {
|
|
19
22
|
return key.replace(/[^\w.-]+/g, "_").replace(/^[-.]+/, "").slice(0, 80) || "default";
|
|
@@ -49,7 +52,15 @@ async function hasLocalMemoryV2Marker(agentDir) {
|
|
|
49
52
|
const stat = await lstat(markerPath);
|
|
50
53
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
51
54
|
return false;
|
|
52
|
-
|
|
55
|
+
if ((await readFile(markerPath, "utf8")) !== "1\n")
|
|
56
|
+
return false;
|
|
57
|
+
const root = await realpath(agentDir);
|
|
58
|
+
const state = MigrationStateSchema.parse(JSON.parse(await readFile(join(memoryDir, "migration.v1.json"), "utf8")));
|
|
59
|
+
assertMigrationStateIntegrity(state, state.identity);
|
|
60
|
+
return state.phase === "committed" && state.storageVersion === 2
|
|
61
|
+
&& state.identity.rootFingerprint === createHash("sha256").update(root).digest("hex")
|
|
62
|
+
&& state.identity.ownerIdentity === String((await lstat(root)).uid)
|
|
63
|
+
&& detectLocalMemoryStorageVersion(await readFile(join(root, "MEMORY.md"), "utf8")) === 2;
|
|
53
64
|
}
|
|
54
65
|
catch {
|
|
55
66
|
return false;
|
package/package.json
CHANGED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { augmentedPath } from "./runtime-path.js";
|
|
7
|
-
import { MaintenanceReportSchema } from "./marketplace-maintenance-protocol.js";
|
|
8
|
-
export function maintenanceEnvironment(env) {
|
|
9
|
-
// Keep the machine user's login/config, never forward control-plane credentials.
|
|
10
|
-
return Object.fromEntries(Object.entries(env).filter(([key]) => !/^(CREW_|NOWCREW_|NOWWORK_)/i.test(key)));
|
|
11
|
-
}
|
|
12
|
-
async function supervise(file, id, cwd, env) {
|
|
13
|
-
await new Promise((resolve, reject) => {
|
|
14
|
-
const child = spawn(process.execPath, [file, "--run", id], { cwd, env, detached: true, stdio: "ignore" });
|
|
15
|
-
let expired = false;
|
|
16
|
-
const kill = () => { if (child.pid) {
|
|
17
|
-
try {
|
|
18
|
-
process.kill(-child.pid, "SIGKILL");
|
|
19
|
-
}
|
|
20
|
-
catch { /* already gone */ }
|
|
21
|
-
} };
|
|
22
|
-
const timer = setTimeout(() => { expired = true; kill(); }, 18 * 60_000);
|
|
23
|
-
child.once("error", () => { clearTimeout(timer); kill(); reject(new Error("worker_failed")); });
|
|
24
|
-
child.once("close", code => {
|
|
25
|
-
clearTimeout(timer);
|
|
26
|
-
// Never release the machine lock while descendants can still write shared configuration.
|
|
27
|
-
kill();
|
|
28
|
-
if (expired || code !== 0)
|
|
29
|
-
reject(new Error("worker_failed"));
|
|
30
|
-
else
|
|
31
|
-
resolve();
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
export async function runMaintenanceBridge(config, request, acquireEnv) {
|
|
36
|
-
const fail = (errorCode) => ({ version: 2, operationId: request.operationId, status: "failed", results: [], errorCode });
|
|
37
|
-
let dir;
|
|
38
|
-
let started = false;
|
|
39
|
-
try {
|
|
40
|
-
const response = await fetch(`${config.serverUrl.replace(/\/+$/, "")}/machine/marketplace-maintenance/${request.operationId}/runner/${request.plan.runner.releaseId}`, {
|
|
41
|
-
headers: { authorization: `Bearer ${config.machineToken}` }, redirect: "error", signal: AbortSignal.timeout(15_000),
|
|
42
|
-
});
|
|
43
|
-
if (!response.ok)
|
|
44
|
-
return fail("runner_unavailable");
|
|
45
|
-
// Bound even a malformed server response before buffering or executing it.
|
|
46
|
-
if (!response.body)
|
|
47
|
-
return fail("runner_unavailable");
|
|
48
|
-
const chunks = [];
|
|
49
|
-
let bytes = 0;
|
|
50
|
-
for await (const chunk of response.body) {
|
|
51
|
-
bytes += chunk.length;
|
|
52
|
-
if (bytes > 256_000)
|
|
53
|
-
return fail("runner_unavailable");
|
|
54
|
-
chunks.push(chunk);
|
|
55
|
-
}
|
|
56
|
-
const content = Buffer.concat(chunks);
|
|
57
|
-
if (createHash("sha256").update(content).digest("hex") !== request.plan.runner.sha256)
|
|
58
|
-
return fail("runner_unavailable");
|
|
59
|
-
const env = maintenanceEnvironment(await acquireEnv());
|
|
60
|
-
env.PATH = augmentedPath(env, {}, process.platform, "append");
|
|
61
|
-
dir = await mkdtemp(join(tmpdir(), "nowwork-maintenance-"));
|
|
62
|
-
const file = join(dir, "runner.mjs");
|
|
63
|
-
await writeFile(file, content, { mode: 0o600 });
|
|
64
|
-
started = true;
|
|
65
|
-
await supervise(file, request.operationId, dir, env);
|
|
66
|
-
const report = MaintenanceReportSchema.parse(JSON.parse(await readFile(join(dir, "report.json"), "utf8")));
|
|
67
|
-
if (report.operationId !== request.operationId)
|
|
68
|
-
throw new Error("operation_mismatch");
|
|
69
|
-
return report;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// A killed worker may have changed files: require readback/manual confirmation before another write.
|
|
73
|
-
return started ? { ...fail("operation_unknown"), status: "unknown" } : fail("runner_unavailable");
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
if (dir)
|
|
77
|
-
await rm(dir, { recursive: true, force: true });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
// 与 daemon 同名文件逐字一致;这是独立于旧单插件协议的版本化契约。
|
|
3
|
-
export const MAINTENANCE_CAPABILITY = "marketplace_codex_maintenance_v1";
|
|
4
|
-
export const MaintenancePlanSchema = z.object({
|
|
5
|
-
marketplace: z.literal("nowwork-pm"),
|
|
6
|
-
actions: z.tuple([z.literal("install-missing"), z.literal("update-installed")]),
|
|
7
|
-
hosts: z.tuple([z.literal("claude"), z.literal("codex"), z.literal("pi")]),
|
|
8
|
-
runner: z.object({ releaseId: z.string().regex(/^[a-f0-9]{64}$/), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict(),
|
|
9
|
-
}).strict();
|
|
10
|
-
export const MaintenanceRequestSchema = z.discriminatedUnion("action", [
|
|
11
|
-
z.object({ type: z.literal("marketplace:maintenance"), action: z.literal("start"), operationId: z.string().uuid(), plan: MaintenancePlanSchema }).strict(),
|
|
12
|
-
z.object({ type: z.literal("marketplace:maintenance"), action: z.literal("status"), operationId: z.string().uuid() }).strict(),
|
|
13
|
-
]);
|
|
14
|
-
export const MaintenanceErrorSchema = z.enum(["runtime_busy", "execution_failed", "operation_unknown", "runner_unavailable",
|
|
15
|
-
"manager_missing", "cli_missing", "command_failed", "verification_failed", "source_conflict", "unsupported_platform"]);
|
|
16
|
-
const Version = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/).nullable();
|
|
17
|
-
export const MaintenanceResultSchema = z.object({
|
|
18
|
-
runtime: z.enum(["claude", "codex", "pi"]), plugin: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,127}$/).nullable(),
|
|
19
|
-
status: z.enum(["installed", "updated", "unchanged", "empty", "missing_cli", "failed"]),
|
|
20
|
-
beforeVersion: Version, afterVersion: Version, targetVersion: Version,
|
|
21
|
-
activation: z.enum(["reload_required", "not_changed", "unverified"]), errorCode: MaintenanceErrorSchema.nullable(),
|
|
22
|
-
}).strict();
|
|
23
|
-
export const MaintenanceReportSchema = z.object({
|
|
24
|
-
version: z.literal(2), operationId: z.string().uuid(),
|
|
25
|
-
status: z.enum(["queued", "running", "completed", "partial", "failed", "unknown"]),
|
|
26
|
-
results: z.array(MaintenanceResultSchema).max(1500), errorCode: MaintenanceErrorSchema.nullable(),
|
|
27
|
-
}).strict().superRefine((r, ctx) => {
|
|
28
|
-
const keys = r.results.map(x => `${x.runtime}:${x.plugin}`);
|
|
29
|
-
if (new Set(keys).size !== keys.length)
|
|
30
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Duplicate plugin result" });
|
|
31
|
-
for (const x of r.results) {
|
|
32
|
-
if (["installed", "updated", "unchanged"].includes(x.status) && (!x.plugin || !x.targetVersion || x.targetVersion !== x.afterVersion || x.errorCode)) {
|
|
33
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Successful plugin result requires a verified target version" });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (r.status === "completed" && (r.errorCode || ["claude", "codex", "pi"].some(host => !r.results.some(x => x.runtime === host))
|
|
37
|
-
|| r.results.some(x => x.errorCode || x.status === "failed" || x.status === "missing_cli"))) {
|
|
38
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Completion requires verified coverage of every host" });
|
|
39
|
-
}
|
|
40
|
-
});
|