@nowcrew/daemon 0.6.101-beta.4 → 0.6.101-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.
- package/dist/cloud-report-upload.js +1 -10
- package/dist/computer-main.js +0 -0
- package/dist/execution-journal.js +3 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +4 -1
- package/dist/local-execution-prompt.js +8 -3
- package/dist/local-executor.js +78 -55
- package/dist/local-memory/commit-telemetry.js +30 -0
- package/dist/local-memory/execution-workspace.js +117 -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 +58 -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/release-artifact-guard.js +55 -0
- package/dist/local-memory/rollout.js +79 -11
- package/dist/local-memory/runtime.js +191 -17
- package/dist/local-memory/source-path.js +13 -0
- package/dist/local-memory/storage-format.js +249 -0
- package/dist/local-memory/write.js +190 -21
- package/dist/local-memory/writer-mode.js +13 -0
- package/dist/local-memory-diagnostics.js +96 -15
- package/dist/local-memory-telemetry.js +31 -3
- package/dist/machine-info.js +3 -0
- package/dist/main.js +0 -0
- package/dist/marketplace-protocol.js +1 -1
- package/dist/marketplace-remote.js +1 -1
- package/dist/marketplace-runner.js +1 -1
- package/dist/memory-migration-controller.js +89 -0
- package/dist/memory-migration-inspect.js +116 -0
- package/dist/memory-migration-journal.js +14 -0
- package/dist/memory-migration-protocol.js +14 -0
- package/dist/memory-migration-runtime.js +367 -0
- package/dist/memory-prune-diagnostics.js +74 -4
- package/dist/message-file-preview.js +11 -1
- 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 +38 -3
- package/dist/token.js +1 -0
- package/dist/workspace-control-handler.js +16 -3
- package/dist/workspace.js +23 -4
- package/package.json +9 -10
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, lstat, realpath, readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { ExecutionCompletedSchema } from "./execution-protocol.js";
|
|
8
|
+
import { startDormantSupervisor } from "./execution-supervisor.js";
|
|
9
|
+
import { MemoryMigrationStartSchema } from "./memory-migration-protocol.js";
|
|
10
|
+
import { MigrationHistorySchema, MigrationIdentitySchema, MigrationStateSchema } from "./local-memory/migration-schema.js";
|
|
11
|
+
import { readMemoryInventorySource, inventoryLimits } from "./local-memory/inventory-source.js";
|
|
12
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
13
|
+
const PROTECTED_WINDOW_PROBE_MS = 5 * 60_000;
|
|
14
|
+
const protectedWindows = new Map();
|
|
15
|
+
const Operation = z.object({ id: z.string(), revision: z.number().int(), executionId: z.string().nullable(),
|
|
16
|
+
status: z.string(), sourceDigest: z.string().nullable(), planDigest: z.string().nullable(),
|
|
17
|
+
writeIntentConfirmedAt: z.string().nullable(), newAgentConfirmed: z.boolean().optional(),
|
|
18
|
+
cancelRequestedAt: z.string().nullable(),
|
|
19
|
+
}).passthrough();
|
|
20
|
+
const Summary = z.object({ result: z.enum(["success", "failed", "needs_review", "repair_required", "missing"]),
|
|
21
|
+
phase: z.string().nullable().optional(), issues: z.array(z.object({ path: z.string(), reason: z.string() }).passthrough()) }).passthrough();
|
|
22
|
+
const HistoryEnvelope = z.object({ operations: z.array(Operation) }).passthrough();
|
|
23
|
+
function canReleaseProtectedWindow(operation) {
|
|
24
|
+
if (!operation)
|
|
25
|
+
return true;
|
|
26
|
+
if (["committed", "rolled_back"].includes(operation.status))
|
|
27
|
+
return true;
|
|
28
|
+
return operation.writeIntentConfirmedAt === null
|
|
29
|
+
&& ["pending", "needs_review", "waiting_maintenance_window"].includes(operation.status);
|
|
30
|
+
}
|
|
31
|
+
function clearProtectedWindow(key, window) {
|
|
32
|
+
if (window.timer !== undefined)
|
|
33
|
+
clearTimeout(window.timer);
|
|
34
|
+
if (protectedWindows.get(key) === window)
|
|
35
|
+
protectedWindows.delete(key);
|
|
36
|
+
return window.reservation.release();
|
|
37
|
+
}
|
|
38
|
+
function scheduleProtectedWindowProbe(key, window, deps, common) {
|
|
39
|
+
if (window.timer !== undefined)
|
|
40
|
+
clearTimeout(window.timer);
|
|
41
|
+
const timer = setTimeout(async () => {
|
|
42
|
+
if (protectedWindows.get(key) !== window || window.active)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
const history = HistoryEnvelope.parse(await deps.control("history", {}));
|
|
46
|
+
if (protectedWindows.get(key) !== window || window.active)
|
|
47
|
+
return;
|
|
48
|
+
const operation = history.operations.find(entry => entry.id === common.migrationId);
|
|
49
|
+
if (canReleaseProtectedWindow(operation)) {
|
|
50
|
+
await clearProtectedWindow(key, window);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Keep the protection when the control plane is unavailable. A later
|
|
56
|
+
// probe or the next recovery request must re-establish the proof.
|
|
57
|
+
}
|
|
58
|
+
scheduleProtectedWindowProbe(key, window, deps, common);
|
|
59
|
+
}, PROTECTED_WINDOW_PROBE_MS);
|
|
60
|
+
timer.unref?.();
|
|
61
|
+
window.timer = timer;
|
|
62
|
+
}
|
|
63
|
+
/** 系统维护在普通执行槽之前分流;只有 server 持久确认意图后才运行活动替换。 */
|
|
64
|
+
export async function runMemoryMigration(raw, deps) {
|
|
65
|
+
const frame = MemoryMigrationStartSchema.parse(raw);
|
|
66
|
+
const common = { agentId: frame.agentId, migrationId: frame.migrationId, machineId: frame.machineId,
|
|
67
|
+
executionId: frame.executionId, attemptKey: frame.attemptKey };
|
|
68
|
+
const protectionKey = `${deps.agentsRoot}:${frame.migrationId}`;
|
|
69
|
+
const protectedWindow = protectedWindows.get(protectionKey);
|
|
70
|
+
if (protectedWindow) {
|
|
71
|
+
protectedWindow.active = true;
|
|
72
|
+
if (protectedWindow.timer !== undefined)
|
|
73
|
+
clearTimeout(protectedWindow.timer);
|
|
74
|
+
protectedWindow.timer = undefined;
|
|
75
|
+
}
|
|
76
|
+
const exclusive = protectedWindow?.reservation ?? await deps.coordinator.tryAcquireExclusiveExecution();
|
|
77
|
+
if (!exclusive) {
|
|
78
|
+
await deps.control("busy", { ...common, expectedRevision: frame.expectedRevision });
|
|
79
|
+
return { kind: "busy" };
|
|
80
|
+
}
|
|
81
|
+
let revision = frame.expectedRevision, intentConfirmed = false, acquired = false, acquisitionRequested = false, keepProtection = false;
|
|
82
|
+
let bound;
|
|
83
|
+
let identity, state;
|
|
84
|
+
let serverHistory;
|
|
85
|
+
let intentReceipt;
|
|
86
|
+
let lastPid = null, stopped = true, phaseCount = 0;
|
|
87
|
+
const startedAt = new Date().toISOString();
|
|
88
|
+
const dir = join(deps.agentsRoot, frame.handle);
|
|
89
|
+
const timeoutMs = deps.timeoutMs ?? 60_000;
|
|
90
|
+
const guard = () => ({ ...common, expectedRevision: revision, ...bound });
|
|
91
|
+
const control = async (step, extra = {}) => {
|
|
92
|
+
const response = await deps.control(step, { ...guard(), ...extra });
|
|
93
|
+
const parsed = z.object({ operation: Operation }).parse(response);
|
|
94
|
+
revision = parsed.operation.revision;
|
|
95
|
+
return parsed.operation;
|
|
96
|
+
};
|
|
97
|
+
const readKnownState = async (path) => {
|
|
98
|
+
const source = await readMemoryInventorySource(await realpath(dir), path, inventoryLimits.fileBytes);
|
|
99
|
+
return MigrationStateSchema.parse(JSON.parse(source.content));
|
|
100
|
+
};
|
|
101
|
+
const readState = () => readKnownState(".crew/memory/migration.v1.json");
|
|
102
|
+
const runTool = async (command, extra = {}) => {
|
|
103
|
+
if (!identity)
|
|
104
|
+
throw new Error("migration_identity_missing");
|
|
105
|
+
if (command === "recover" && deps.cancellation?.isRequested())
|
|
106
|
+
extra = { ...extra, cancelled: true };
|
|
107
|
+
const rollback = command === "recover" && extra.cancelled === true;
|
|
108
|
+
if (!rollback && deps.cancellation?.isRequested())
|
|
109
|
+
throw new Error("migration_cancelled");
|
|
110
|
+
if (phaseCount++ > 0) {
|
|
111
|
+
if (!stopped || !deps.journal.retry)
|
|
112
|
+
throw new Error("migration_stop_unproven");
|
|
113
|
+
await deps.journal.retry(frame.executionId);
|
|
114
|
+
}
|
|
115
|
+
const sourceMode = import.meta.url.endsWith(".ts");
|
|
116
|
+
const entry = fileURLToPath(new URL(`./local-memory/migration-tool-entry.${sourceMode ? "ts" : "js"}`, import.meta.url));
|
|
117
|
+
const args = sourceMode ? ["--import", pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href, entry] : [entry];
|
|
118
|
+
let stdout = "";
|
|
119
|
+
const launched = await deps.journal.startGuarded(frame.executionId, new Date().toISOString(), () => (deps.startSupervisor ?? startDormantSupervisor)({ command: process.execPath, args, cwd: dir,
|
|
120
|
+
env: { PATH: process.env.PATH ?? "", CREW_SLS_LOG: "off" },
|
|
121
|
+
stdinText: JSON.stringify({ command, agentDir: dir, identity, ...extra }),
|
|
122
|
+
}, sourceMode ? {
|
|
123
|
+
childEntry: fileURLToPath(new URL("./execution-supervisor-child.ts", import.meta.url)),
|
|
124
|
+
execArgv: ["--import", pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href],
|
|
125
|
+
} : {}), { beforeRelease: async ({ handle }) => {
|
|
126
|
+
lastPid = handle.pid;
|
|
127
|
+
stopped = false;
|
|
128
|
+
handle.stdout.on("data", chunk => { stdout += String(chunk); if (stdout.length > 2 * 1024 * 1024)
|
|
129
|
+
void handle.cancel(); });
|
|
130
|
+
handle.stderr.resume();
|
|
131
|
+
} });
|
|
132
|
+
if (launched.kind !== "started")
|
|
133
|
+
throw new Error("migration_execution_already_running");
|
|
134
|
+
const processHandle = launched.handle;
|
|
135
|
+
deps.send({ type: "execution:started", protocolVersion: 1, executionId: frame.executionId, at: new Date().toISOString() });
|
|
136
|
+
let timedOut = false;
|
|
137
|
+
const timer = setTimeout(() => { timedOut = true; void processHandle.cancel(); }, timeoutMs);
|
|
138
|
+
if (!rollback)
|
|
139
|
+
deps.cancellation?.register(async () => { if (!stopped)
|
|
140
|
+
await processHandle.cancel(); });
|
|
141
|
+
try {
|
|
142
|
+
await processHandle.exit;
|
|
143
|
+
await processHandle.cancel();
|
|
144
|
+
stopped = true;
|
|
145
|
+
if (timedOut)
|
|
146
|
+
throw new Error("migration_window_timeout");
|
|
147
|
+
return Summary.parse(JSON.parse(stdout.trim()));
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const finish = async (outcome, reason) => {
|
|
154
|
+
const completion = ExecutionCompletedSchema.parse({ type: "execution:completed", protocolVersion: 1,
|
|
155
|
+
executionId: frame.executionId, runtime: "memory_migration", resumed: false, outcome,
|
|
156
|
+
exitCode: outcome === "succeeded" ? 0 : 1, ...(reason ? { errorCode: reason, errorMessage: reason } : {}),
|
|
157
|
+
startedAt, finishedAt: new Date().toISOString() });
|
|
158
|
+
await deps.journal.complete(frame.executionId, completion);
|
|
159
|
+
deps.send(completion);
|
|
160
|
+
};
|
|
161
|
+
try {
|
|
162
|
+
const history = z.object({ operations: z.array(Operation), serverHistory: MigrationHistorySchema }).passthrough().parse(await deps.control("history", {}));
|
|
163
|
+
serverHistory = history.serverHistory;
|
|
164
|
+
const operation = history.operations.find(op => op.id === frame.migrationId);
|
|
165
|
+
if (!operation || operation.revision !== revision || operation.executionId !== frame.executionId)
|
|
166
|
+
throw new Error("migration_server_identity_mismatch");
|
|
167
|
+
intentConfirmed = operation.writeIntentConfirmedAt !== null;
|
|
168
|
+
await mkdir(dir, { recursive: true });
|
|
169
|
+
const root = await realpath(dir);
|
|
170
|
+
if ((await lstat(dir)).isSymbolicLink())
|
|
171
|
+
throw new Error("migration_root_invalid");
|
|
172
|
+
bound = { storageRootFingerprint: digest(root), ownerIdentity: String((await lstat(root)).uid) };
|
|
173
|
+
identity = MigrationIdentitySchema.parse({ agentId: frame.agentId, migrationId: frame.migrationId,
|
|
174
|
+
machineId: frame.machineId, executionId: frame.executionId, workspaceId: frame.workspaceId,
|
|
175
|
+
rootFingerprint: bound.storageRootFingerprint, ownerIdentity: bound.ownerIdentity,
|
|
176
|
+
sourceStorageVersion: frame.sourceStorageVersion, targetStorageVersion: 2 });
|
|
177
|
+
const { reqId: _transportRequest, ...stableFrame } = frame;
|
|
178
|
+
const accepted = await deps.journal.accept(frame.executionId, digest(JSON.stringify(stableFrame)), { runtime: "memory_migration" });
|
|
179
|
+
if (accepted.kind === "existing" && accepted.completion) {
|
|
180
|
+
deps.send(accepted.completion);
|
|
181
|
+
return { kind: "completed" };
|
|
182
|
+
}
|
|
183
|
+
deps.send({ type: "execution:accepted", protocolVersion: 1, executionId: frame.executionId,
|
|
184
|
+
state: "ready", effectivePermission: "workspace_write", at: new Date().toISOString() });
|
|
185
|
+
acquisitionRequested = true;
|
|
186
|
+
await control("acquired");
|
|
187
|
+
acquired = true;
|
|
188
|
+
if (frame.action === "recover") {
|
|
189
|
+
state = await readState().catch(() => undefined);
|
|
190
|
+
if (state && state.migrationId !== frame.migrationId && state.phase === "rolled_back"
|
|
191
|
+
&& history.serverHistory.rolledBackOperations?.some(previous => previous.migrationId === state.migrationId
|
|
192
|
+
&& previous.sourceDigest === state.sourceDigest && previous.planDigest === state.planDigest))
|
|
193
|
+
state = undefined;
|
|
194
|
+
if (state) {
|
|
195
|
+
if (state.migrationId !== frame.migrationId || state.identity.rootFingerprint !== bound.storageRootFingerprint
|
|
196
|
+
|| state.identity.machineId !== frame.machineId || state.identity.agentId !== frame.agentId)
|
|
197
|
+
throw new Error("migration_state_identity_mismatch");
|
|
198
|
+
identity = state.identity;
|
|
199
|
+
}
|
|
200
|
+
if (!state) {
|
|
201
|
+
state = await readKnownState(`.crew/memory/migrations/${frame.migrationId}/terminal.json`).catch((e) => {
|
|
202
|
+
if (e.code !== "ENOENT")
|
|
203
|
+
throw e;
|
|
204
|
+
return readKnownState(`.crew/memory/migrations/${frame.migrationId}/manifest.json`);
|
|
205
|
+
});
|
|
206
|
+
identity = state.identity;
|
|
207
|
+
}
|
|
208
|
+
if (state.planDigest !== operation.planDigest || state.sourceDigest !== operation.sourceDigest)
|
|
209
|
+
throw new Error("migration_server_plan_mismatch");
|
|
210
|
+
const prior = await deps.journal.get(identity.executionId);
|
|
211
|
+
if (state.phase !== "committed" && (!prior || (prior.state !== "completed" && prior.state !== "interrupted"))) {
|
|
212
|
+
throw new Error("migration_stop_unproven");
|
|
213
|
+
}
|
|
214
|
+
const result = await runTool("recover", { serverHistory: history.serverHistory, cancelled: !intentConfirmed || operation.cancelRequestedAt !== null,
|
|
215
|
+
stoppedReceipt: { kind: "migration_process_stopped", migrationId: frame.migrationId, machineId: frame.machineId,
|
|
216
|
+
executionId: identity.executionId, ownerIdentity: identity.ownerIdentity, pid: null,
|
|
217
|
+
stoppedAt: new Date().toISOString(), proof: "journal_reconciled" } });
|
|
218
|
+
if (result.result !== "success")
|
|
219
|
+
throw new Error("migration_recovery_failed");
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const prepared = await runTool("prepare", { serverHistory: history.serverHistory,
|
|
223
|
+
...(frame.recordMappings ? { recordMappings: frame.recordMappings } : {}) });
|
|
224
|
+
if (prepared.result === "needs_review") {
|
|
225
|
+
await control("needs-review", { issues: prepared.issues });
|
|
226
|
+
await finish("failed", "migration_needs_review");
|
|
227
|
+
return { kind: "needs_review" };
|
|
228
|
+
}
|
|
229
|
+
if (prepared.result !== "success")
|
|
230
|
+
throw new Error("migration_prepare_failed");
|
|
231
|
+
state = await readState();
|
|
232
|
+
const confirmed = await control("intent", { sourceDigest: state.sourceDigest, planDigest: state.planDigest,
|
|
233
|
+
minReaderCapabilities: ["memory_storage_v2", "memory_state_guard_v1"], minWriterCapabilities: ["memory_storage_v2", "memory_state_guard_v1"] });
|
|
234
|
+
intentConfirmed = true;
|
|
235
|
+
intentReceipt = { ...identity, kind: "server_write_intent", sourceDigest: state.sourceDigest,
|
|
236
|
+
planDigest: state.planDigest, intentVersion: confirmed.revision, confirmedAt: confirmed.writeIntentConfirmedAt };
|
|
237
|
+
const result = await runTool("apply", { intentReceipt });
|
|
238
|
+
if (result.result !== "success")
|
|
239
|
+
throw new Error("migration_apply_failed");
|
|
240
|
+
}
|
|
241
|
+
state = await readState();
|
|
242
|
+
if (state.phase !== "committed" && state.phase !== "rolled_back")
|
|
243
|
+
throw new Error("migration_terminal_unverified");
|
|
244
|
+
if (!intentConfirmed) {
|
|
245
|
+
if (state.phase !== "rolled_back")
|
|
246
|
+
throw new Error("migration_commit_without_intent");
|
|
247
|
+
await control("no-write", { sourceDigest: state.sourceDigest, planDigest: state.planDigest, verified: true });
|
|
248
|
+
}
|
|
249
|
+
else
|
|
250
|
+
await control("result", { outcome: state.phase, storageVersion: state.storageVersion,
|
|
251
|
+
sourceDigest: state.sourceDigest, planDigest: state.planDigest, origin: state.origin, verified: true });
|
|
252
|
+
await finish("succeeded");
|
|
253
|
+
return { kind: "completed", phase: state.phase };
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
// A recovery window must remain protected when the control plane cannot
|
|
257
|
+
// prove that no migration is active. The next request or bounded probe
|
|
258
|
+
// can release it after reading a terminal/no-write operation.
|
|
259
|
+
if (protectedWindow !== undefined && !acquisitionRequested)
|
|
260
|
+
keepProtection = true;
|
|
261
|
+
const reason = error instanceof Error && /^[a-z_]+$/u.test(error.message) ? error.message : "migration_execution_failed";
|
|
262
|
+
if (acquisitionRequested) {
|
|
263
|
+
try {
|
|
264
|
+
const latest = z.object({ operations: z.array(Operation) }).passthrough().parse(await deps.control("history", {}))
|
|
265
|
+
.operations.find(op => op.id === frame.migrationId);
|
|
266
|
+
if (!latest || latest.executionId !== frame.executionId)
|
|
267
|
+
throw new Error("migration_control_owner_changed");
|
|
268
|
+
revision = latest.revision;
|
|
269
|
+
intentConfirmed = latest.writeIntentConfirmedAt !== null;
|
|
270
|
+
acquired = !["needs_review", "rolled_back"].includes(latest.status);
|
|
271
|
+
if (intentConfirmed && identity && state && latest.sourceDigest === state.sourceDigest && latest.planDigest === state.planDigest) {
|
|
272
|
+
intentReceipt = { ...identity, kind: "server_write_intent", sourceDigest: state.sourceDigest, planDigest: state.planDigest,
|
|
273
|
+
intentVersion: latest.revision, confirmedAt: latest.writeIntentConfirmedAt };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
keepProtection = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (acquired) {
|
|
281
|
+
if (intentConfirmed) {
|
|
282
|
+
keepProtection = true;
|
|
283
|
+
if (stopped && identity && lastPid !== null) {
|
|
284
|
+
try {
|
|
285
|
+
const recovered = await runTool("recover", { cancelled: true, ...(intentReceipt ? { intentReceipt } : {}),
|
|
286
|
+
stoppedReceipt: { kind: "migration_process_stopped", migrationId: frame.migrationId, machineId: frame.machineId,
|
|
287
|
+
executionId: identity.executionId, ownerIdentity: identity.ownerIdentity, pid: lastPid,
|
|
288
|
+
stoppedAt: new Date().toISOString(), proof: "supervisor_exit" } });
|
|
289
|
+
state = await readState();
|
|
290
|
+
if (recovered.result === "success" && (state.phase === "committed" || state.phase === "rolled_back")) {
|
|
291
|
+
await control("result", { outcome: state.phase, storageVersion: state.storageVersion,
|
|
292
|
+
sourceDigest: state.sourceDigest, planDigest: state.planDigest, origin: state.origin, verified: true });
|
|
293
|
+
keepProtection = false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch { /* 未证明提交或完整回退时保留保护。 */ }
|
|
297
|
+
}
|
|
298
|
+
if (keepProtection)
|
|
299
|
+
await control("repair", { reason }).catch(() => undefined);
|
|
300
|
+
}
|
|
301
|
+
else if (stopped) {
|
|
302
|
+
let digests;
|
|
303
|
+
try {
|
|
304
|
+
let local = await readState().catch((error) => { if (error.code === "ENOENT")
|
|
305
|
+
return undefined; throw error; });
|
|
306
|
+
if (local?.migrationId !== frame.migrationId) {
|
|
307
|
+
local = await readKnownState(`.crew/memory/migrations/${frame.migrationId}/manifest.json`).catch(async (error) => {
|
|
308
|
+
if (error.code !== "ENOENT")
|
|
309
|
+
throw error;
|
|
310
|
+
const residue = await lstat(join(dir, ".crew/memory/migrations", frame.migrationId)).catch((statError) => {
|
|
311
|
+
if (statError.code === "ENOENT")
|
|
312
|
+
return undefined;
|
|
313
|
+
throw statError;
|
|
314
|
+
});
|
|
315
|
+
if (residue)
|
|
316
|
+
throw new Error("migration_prepare_residue_unverified");
|
|
317
|
+
return undefined;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (local?.migrationId === frame.migrationId) {
|
|
321
|
+
if (!identity || lastPid === null || Object.keys(local.identity).some(key => local.identity[key] !== identity[key])
|
|
322
|
+
|| local.intentReceipt || !["prepared", "rolled_back"].includes(local.phase))
|
|
323
|
+
throw new Error("migration_no_write_state_unverified");
|
|
324
|
+
digests = { sourceDigest: local.sourceDigest, planDigest: local.planDigest };
|
|
325
|
+
const recovered = await runTool("recover", { cancelled: true,
|
|
326
|
+
serverHistory: { kind: "existing", migrationId: frame.migrationId, writeIntentConfirmed: false,
|
|
327
|
+
...(serverHistory?.rolledBackOperations ? { rolledBackOperations: serverHistory.rolledBackOperations } : {}) },
|
|
328
|
+
stoppedReceipt: { kind: "migration_process_stopped", migrationId: frame.migrationId, machineId: frame.machineId,
|
|
329
|
+
executionId: identity.executionId, ownerIdentity: identity.ownerIdentity, pid: lastPid,
|
|
330
|
+
stoppedAt: new Date().toISOString(), proof: "supervisor_exit" } });
|
|
331
|
+
state = await readState();
|
|
332
|
+
if (recovered.result !== "success" || state.phase !== "rolled_back")
|
|
333
|
+
throw new Error("migration_no_write_rollback_unverified");
|
|
334
|
+
digests = { sourceDigest: state.sourceDigest, planDigest: state.planDigest };
|
|
335
|
+
}
|
|
336
|
+
await control("no-write", { verified: true, ...digests });
|
|
337
|
+
keepProtection = false;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
keepProtection = true;
|
|
341
|
+
await control("repair", { reason: "migration_no_write_rollback_unverified", ...digests }).catch(() => undefined);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else
|
|
345
|
+
keepProtection = true;
|
|
346
|
+
}
|
|
347
|
+
if (stopped)
|
|
348
|
+
await finish("failed", reason).catch(() => undefined);
|
|
349
|
+
return { kind: "failed", reason, processStopped: stopped, lastPid };
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
if (!keepProtection && stopped) {
|
|
353
|
+
if (protectedWindow !== undefined) {
|
|
354
|
+
await clearProtectedWindow(protectionKey, protectedWindow);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
await exclusive.release();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
const window = protectedWindow ?? { reservation: exclusive, executionId: frame.executionId, active: false, timer: undefined };
|
|
362
|
+
window.active = false;
|
|
363
|
+
protectedWindows.set(protectionKey, window);
|
|
364
|
+
scheduleProtectedWindowProbe(protectionKey, window, deps, common);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -7,10 +7,25 @@ import { dslog } from "./slog.js";
|
|
|
7
7
|
const TRACE_PATTERN = /\[memory-prune trace_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\]/i;
|
|
8
8
|
export const MEMORY_PRUNE_HASH_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
9
9
|
export const MEMORY_PRUNE_NOTES_MANIFEST_LIMIT = 128;
|
|
10
|
+
export const MEMORY_PRUNE_PENDING_MANIFEST_LIMIT = 128;
|
|
10
11
|
export const MEMORY_PRUNE_NOTE_CHANGE_LIMIT = 128;
|
|
11
12
|
function compareCodeUnits(left, right) {
|
|
12
13
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Release evidence is a task artifact, not a durable memory note. Keep this
|
|
17
|
+
* deliberately narrow: only conventional delivery-artifact filename families
|
|
18
|
+
* are blocked; unrelated note names are still handled by the normal candidate
|
|
19
|
+
* protocol instead of being deleted or rewritten.
|
|
20
|
+
*/
|
|
21
|
+
const RELEASE_ARTIFACT_NOTE_PATTERN = /^(?:release|publish|build|deploy|artifact)(?:[-_.].*)?\.md$/iu;
|
|
22
|
+
export function isReleaseArtifactNotePath(relativePath) {
|
|
23
|
+
const basename = relativePath.split("/").pop() ?? relativePath;
|
|
24
|
+
return RELEASE_ARTIFACT_NOTE_PATTERN.test(basename);
|
|
25
|
+
}
|
|
26
|
+
export function releaseArtifactNoteChanges(diff) {
|
|
27
|
+
return diff.changes.filter((change) => isReleaseArtifactNotePath(change.relative_path));
|
|
28
|
+
}
|
|
14
29
|
export function parseMemoryPruneTraceId(wakePrompt) {
|
|
15
30
|
return TRACE_PATTERN.exec(wakePrompt)?.[1] ?? null;
|
|
16
31
|
}
|
|
@@ -192,6 +207,50 @@ export async function inspectMemoryPruneNotes(homeDir, signal) {
|
|
|
192
207
|
...(signal?.aborted ? { error: "ABORT_ERR" } : {}),
|
|
193
208
|
};
|
|
194
209
|
}
|
|
210
|
+
export async function inspectMemoryPrunePending(homeDir, signal) {
|
|
211
|
+
const pendingDir = join(homeDir, ".crew", "memory", "pending");
|
|
212
|
+
try {
|
|
213
|
+
const metadata = await lstat(pendingDir);
|
|
214
|
+
if (metadata.isSymbolicLink())
|
|
215
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: "symbolic_link" };
|
|
216
|
+
if (!metadata.isDirectory())
|
|
217
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: "ENOTDIR" };
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error.code === "ENOENT")
|
|
221
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false };
|
|
222
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: diagnosticError(error) };
|
|
223
|
+
}
|
|
224
|
+
let names;
|
|
225
|
+
try {
|
|
226
|
+
names = (await readdir(pendingDir, { withFileTypes: true }))
|
|
227
|
+
.filter(entry => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".json"))
|
|
228
|
+
.map(entry => entry.name).sort(compareCodeUnits);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: diagnosticError(error) };
|
|
232
|
+
}
|
|
233
|
+
const selected = names.slice(0, MEMORY_PRUNE_PENDING_MANIFEST_LIMIT);
|
|
234
|
+
const files = [];
|
|
235
|
+
for (const name of selected) {
|
|
236
|
+
if (signal?.aborted)
|
|
237
|
+
break;
|
|
238
|
+
const path = join(pendingDir, name);
|
|
239
|
+
try {
|
|
240
|
+
if ((await lstat(path)).isSymbolicLink()) {
|
|
241
|
+
files.push({ relative_path: `.crew/memory/pending/${name}`, exists: false, error: "symbolic_link" });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
files.push({ relative_path: `.crew/memory/pending/${name}`, exists: false, error: diagnosticError(error) });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
files.push({ relative_path: `.crew/memory/pending/${name}`, ...await inspectFile(path, false, undefined, signal) });
|
|
250
|
+
}
|
|
251
|
+
return { files, total_count: names.length, omitted_count: Math.max(0, names.length - selected.length),
|
|
252
|
+
truncated: names.length > selected.length, ...(signal?.aborted ? { error: "ABORT_ERR" } : {}) };
|
|
253
|
+
}
|
|
195
254
|
async function inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal) {
|
|
196
255
|
const cwdIsHome = runDir === homeDir;
|
|
197
256
|
const [memory, lessons, cwdMemory, cwdLessons, workLog] = await Promise.all([
|
|
@@ -210,15 +269,17 @@ async function inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal
|
|
|
210
269
|
};
|
|
211
270
|
}
|
|
212
271
|
export async function inspectMemoryPruneFiles(homeDir, runDir, workLogPath, signal) {
|
|
213
|
-
const [fixedFiles, notesManifest] = await Promise.all([
|
|
272
|
+
const [fixedFiles, notesManifest, pendingManifest] = await Promise.all([
|
|
214
273
|
inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal),
|
|
215
274
|
inspectMemoryPruneNotes(homeDir, signal),
|
|
275
|
+
inspectMemoryPrunePending(homeDir, signal),
|
|
216
276
|
]);
|
|
217
|
-
return { ...fixedFiles, notes_manifest: notesManifest };
|
|
277
|
+
return { ...fixedFiles, notes_manifest: notesManifest, pending_manifest: pendingManifest };
|
|
218
278
|
}
|
|
219
279
|
export async function inspectMemoryPruneFilesWithinDeadline(homeDir, runDir, workLogPath, timeoutMs) {
|
|
220
280
|
const fixedFiles = await withinMemoryPruneDiagnosticsDeadline((signal) => inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal), timeoutMs);
|
|
221
281
|
let notesManifest;
|
|
282
|
+
let pendingManifest;
|
|
222
283
|
try {
|
|
223
284
|
notesManifest = await withinMemoryPruneDiagnosticsDeadline((signal) => inspectMemoryPruneNotes(homeDir, signal), timeoutMs);
|
|
224
285
|
}
|
|
@@ -231,7 +292,13 @@ export async function inspectMemoryPruneFilesWithinDeadline(homeDir, runDir, wor
|
|
|
231
292
|
error: diagnosticError(error),
|
|
232
293
|
};
|
|
233
294
|
}
|
|
234
|
-
|
|
295
|
+
try {
|
|
296
|
+
pendingManifest = await withinMemoryPruneDiagnosticsDeadline(signal => inspectMemoryPrunePending(homeDir, signal), timeoutMs);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
pendingManifest = { files: [], total_count: 0, omitted_count: 0, truncated: false, error: diagnosticError(error) };
|
|
300
|
+
}
|
|
301
|
+
return { ...fixedFiles, notes_manifest: notesManifest, pending_manifest: pendingManifest };
|
|
235
302
|
}
|
|
236
303
|
export async function withinMemoryPruneDiagnosticsDeadline(operation, timeoutMs) {
|
|
237
304
|
const controller = new AbortController();
|
|
@@ -361,6 +428,7 @@ export function evaluateMemoryPrunePostcondition(before, after, execution) {
|
|
|
361
428
|
&& (after.cwd_memory.lessons_reference_count ?? 0) > 0,
|
|
362
429
|
cwd_lessons_changed: !after.cwd_lessons.same_as_shared_path
|
|
363
430
|
&& fileChanged(before.cwd_lessons, after.cwd_lessons),
|
|
431
|
+
pending_changed: diffMemoryPruneNotes(before.pending_manifest ?? { files: [], total_count: 0, omitted_count: 0, truncated: false }, after.pending_manifest ?? { files: [], total_count: 0, omitted_count: 0, truncated: false }).change_count > 0,
|
|
364
432
|
};
|
|
365
433
|
let outcome;
|
|
366
434
|
if (!execution.executorCompleted)
|
|
@@ -388,6 +456,8 @@ export function evaluateMemoryPrunePostcondition(before, after, execution) {
|
|
|
388
456
|
outcome = "promoted_without_index_change";
|
|
389
457
|
else if (facts.shared_memory_changed)
|
|
390
458
|
outcome = "shared_memory_updated";
|
|
459
|
+
else if (facts.pending_changed)
|
|
460
|
+
outcome = "pending_updated";
|
|
391
461
|
else
|
|
392
462
|
outcome = "no_files_changed";
|
|
393
463
|
return { outcome, ...facts };
|
|
@@ -408,7 +478,7 @@ function memoryPruneFileFactFields(fileFacts) {
|
|
|
408
478
|
}
|
|
409
479
|
return fields;
|
|
410
480
|
}
|
|
411
|
-
export function memoryPruneSnapshotFields({ notes_manifest: _notesManifest, ...fileFacts }) {
|
|
481
|
+
export function memoryPruneSnapshotFields({ notes_manifest: _notesManifest, pending_manifest: _pendingManifest, ...fileFacts }) {
|
|
412
482
|
return memoryPruneFileFactFields(fileFacts);
|
|
413
483
|
}
|
|
414
484
|
export function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
|
|
@@ -9,6 +9,7 @@ import { z } from "zod";
|
|
|
9
9
|
import { isInternalEnvironmentPath } from "./workspace-fs.js";
|
|
10
10
|
import { opaqueKey, safeKey } from "./workspace.js";
|
|
11
11
|
import { AgentHandleSchema } from "./execution-protocol.js";
|
|
12
|
+
import { withMemoryAccess } from "./local-memory/migration-access.js";
|
|
12
13
|
const MAX_BYTES = 256 * 1024;
|
|
13
14
|
const MAX_BINARY_BYTES = 8 * 1024 * 1024;
|
|
14
15
|
const MAX_CONCURRENT_READS = 4;
|
|
@@ -231,7 +232,16 @@ export async function readSharedMessageFile(agentsRoot, rawRequest, registry) {
|
|
|
231
232
|
throw new SharedFileError("file_preview_busy");
|
|
232
233
|
activeReads++;
|
|
233
234
|
try {
|
|
234
|
-
|
|
235
|
+
const candidate = await resolveCandidate(agentsRoot, parsed.data, registry);
|
|
236
|
+
const agentRoot = await realpath(resolve(agentsRoot, parsed.data.handle)).catch((error) => {
|
|
237
|
+
if (error.code === "ENOENT")
|
|
238
|
+
return undefined;
|
|
239
|
+
throw error;
|
|
240
|
+
});
|
|
241
|
+
// 记忆保护只用于该Agent根内资料,独立项目文件仍按原执行授权读取。
|
|
242
|
+
return await (agentRoot && inside(agentRoot, candidate.path)
|
|
243
|
+
? withMemoryAccess(agentRoot, () => readCandidate(candidate))
|
|
244
|
+
: readCandidate(candidate));
|
|
235
245
|
}
|
|
236
246
|
catch (error) {
|
|
237
247
|
if (error instanceof SharedFileError)
|
package/dist/prompt.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* claim-before-work、freshness/draft、"做完所有事再停"、inbox notice 语义、
|
|
6
6
|
* 分层记忆与压缩安全、协作礼仪。措辞为本项目原创。
|
|
7
7
|
*/
|
|
8
|
+
import { detectLocalMemoryStorageVersion, selectLegacyMemoryPreview, selectLocalMemoryPreview, } from "./local-memory/storage-format.js";
|
|
8
9
|
/** work-log 注入上限(字符):防冷启动注入随文件膨胀。超限保头(目标/结论)+尾(最近进展),
|
|
9
10
|
* 截中段并提示读全文——全文仍在盘上,只限"默认推送量"不限"可获取量"。纯函数。 */
|
|
10
11
|
export const WORKLOG_INJECT_CAP = 8000;
|
|
@@ -67,23 +68,14 @@ export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
|
|
|
67
68
|
const tail = workLog.slice(-WORKLOG_TAIL);
|
|
68
69
|
return `${head}\n…(work-log 过长已截去中段,全文用 Read 读 $CREW_TASK_LOG)\n${tail}`;
|
|
69
70
|
}
|
|
70
|
-
export function capMemoryForInject(memory, cap = MEMORY_INJECT_CAP, maxBytes = Number.POSITIVE_INFINITY) {
|
|
71
|
-
if (
|
|
72
|
-
return memory;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return "";
|
|
77
|
-
let end = 0;
|
|
78
|
-
let bytes = noticeBytes;
|
|
79
|
-
for (const character of memory) {
|
|
80
|
-
const size = Buffer.byteLength(character, "utf8");
|
|
81
|
-
if (notice.length + end + character.length > cap || bytes + size > maxBytes)
|
|
82
|
-
break;
|
|
83
|
-
end += character.length;
|
|
84
|
-
bytes += size;
|
|
71
|
+
export function capMemoryForInject(memory, cap = MEMORY_INJECT_CAP, maxBytes = Number.POSITIVE_INFINITY, query = "") {
|
|
72
|
+
if (detectLocalMemoryStorageVersion(memory) === 2) {
|
|
73
|
+
return selectLocalMemoryPreview(memory, query, { maxCodePoints: cap, maxBytes }).memory;
|
|
74
|
+
}
|
|
75
|
+
if (memory.length > cap || Buffer.byteLength(memory, "utf8") > maxBytes) {
|
|
76
|
+
return selectLegacyMemoryPreview(memory, query, { maxCodePoints: cap, maxBytes });
|
|
85
77
|
}
|
|
86
|
-
return
|
|
78
|
+
return memory;
|
|
87
79
|
}
|
|
88
80
|
export function buildSystemPrompt(ctx) {
|
|
89
81
|
const product = ctx.productName ?? "nowwork";
|
|
@@ -342,7 +334,7 @@ ${interactiveTaskRules}${freshnessRule}${externalReplyRule}${collaborationEtique
|
|
|
342
334
|
|
|
343
335
|
## Workspace 与分层记忆(CRITICAL — 索引+按需,配合上面的并行规则)
|
|
344
336
|
你的持久记忆在 \`$CREW_HOME\`(跨你所有任务共享),按“常驻索引 + 功能明细 + 当前工作”分层:
|
|
345
|
-
1. **MEMORY.md = 常驻索引 +
|
|
337
|
+
1. **MEMORY.md = 常驻索引 + 角色**:提示词末尾可能只有它的有界预览,预算不足时也可能省略。预览未命中不等于没有记忆。旧版索引可能包含 \`scope\`;V2 规范字段为 \`id/file/type/status/summary/triggers/priority/revision/anchor/updated\`(可选 \`review_after\`),不含 \`scope\`。其中 file 是相对于 \`$CREW_HOME\` 的路径(例如 \`notes/testing.md\`);需要历史约束、经验或偏好时先检索完整索引,再按需分段读取。
|
|
346
338
|
2. **notes/<topic>.md = 功能明细**:按用户偏好、项目规则、测试、调试、Git、事故复盘等主题拆分。优先读取与当前任务匹配的文件;\`notes/lessons.md\` 只保留未分类经验和历史兼容内容,不是所有新经验的默认目的地。普通任务默认只读不写,但当前用户明确要求时可以按最小范围更新;任务结束后的专门收尾执行仍按收尾指令复核。
|
|
347
339
|
- 对旧版 MEMORY.md,如果条目没有完整结构化字段但明确指向 \`notes/<file>.md\`,把它视为 legacy 索引;仍需结合条目标题/链接和当前请求判断相关性,不要因为缺少 summary/triggers 就假定正文已注入。
|
|
348
340
|
3. **本任务 work-log = \`$CREW_TASK_LOG\`**:每任务一份(别的运行不会碰),记当前阶段/下一步/卡点/关键结论。系统已把它的当前内容注入末尾让你续上;有进展就更新它——这是你**唯一**该在任务中持续写的记忆文件。**work-log 是现状快照,不是流水账**:保持 ≤100 行,更新时重写全文替换过时内容,不要无限追加(过长会被注入截断)。
|
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) {
|