@nowcrew/daemon 0.6.93-beta.1 → 0.6.93-beta.10
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/agent-environment-execution.js +7 -2
- package/dist/cloud-report-upload.js +1 -10
- package/dist/execution-runner.js +1 -0
- package/dist/local-executor.js +25 -30
- package/dist/local-memory/commit-telemetry.js +30 -0
- package/dist/local-memory/execution-workspace.js +9 -2
- package/dist/local-memory/initialization-identity.js +18 -5
- package/dist/local-memory/inventory-cli.js +33 -17
- package/dist/local-memory/inventory-v2.js +282 -0
- package/dist/local-memory/migration-access.js +31 -8
- package/dist/local-memory/migration-pending.js +9 -0
- package/dist/local-memory/migration-plan.js +37 -3
- package/dist/local-memory/migration-state-invariants.js +65 -0
- package/dist/local-memory/migration-store.js +7 -35
- 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 +2 -2
- package/dist/local-memory/read-cli.js +4 -3
- package/dist/local-memory/rollout.js +32 -10
- package/dist/local-memory/runtime.js +46 -13
- package/dist/local-memory/write.js +63 -2
- package/dist/local-memory/writer-mode.js +13 -0
- package/dist/local-memory-diagnostics.js +55 -13
- 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 +2 -0
- package/dist/memory-migration-inspect.js +40 -7
- package/dist/prompt.js +1 -1
- package/dist/runtimes/claude-thread-runner.js +25 -4
- package/dist/serve.js +2 -0
- package/dist/workspace.js +3 -0
- package/package.json +1 -1
|
@@ -209,6 +209,25 @@ function stableId(executionId, index, change) {
|
|
|
209
209
|
const input = [executionId, String(index), change.operation, change.title, change.summary, change.detailBody].join("\0");
|
|
210
210
|
return `mem_${createHash("sha256").update(input, "utf8").digest("hex").slice(0, 12)}`;
|
|
211
211
|
}
|
|
212
|
+
function migrationPendingId(change) {
|
|
213
|
+
// Keep the digest independent of object insertion order for callers that
|
|
214
|
+
// reach applyLocalMemoryResult directly instead of the JSON/Zod boundary.
|
|
215
|
+
const canonicalChange = {
|
|
216
|
+
operation: change.operation,
|
|
217
|
+
memoryId: change.memoryId,
|
|
218
|
+
baseRevision: change.baseRevision,
|
|
219
|
+
supersedesMemoryId: change.supersedesMemoryId,
|
|
220
|
+
title: change.title,
|
|
221
|
+
kind: change.kind,
|
|
222
|
+
summary: change.summary,
|
|
223
|
+
triggers: change.triggers,
|
|
224
|
+
priority: change.priority,
|
|
225
|
+
detailCategory: change.detailCategory,
|
|
226
|
+
detailBody: change.detailBody,
|
|
227
|
+
reviewAfter: change.reviewAfter,
|
|
228
|
+
};
|
|
229
|
+
return `pending-migration-${createHash("sha256").update(JSON.stringify(canonicalChange), "utf8").digest("hex").slice(0, 12)}`;
|
|
230
|
+
}
|
|
212
231
|
function normalize(value) {
|
|
213
232
|
return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
|
|
214
233
|
}
|
|
@@ -259,6 +278,34 @@ function archivedChange(entry, detailBody) {
|
|
|
259
278
|
function emptyResult() {
|
|
260
279
|
return { createdIds: [], updatedIds: [], statusChangedIds: [], deferredIds: [], pendingIds: [] };
|
|
261
280
|
}
|
|
281
|
+
async function stageMigrationDeferredPending(input) {
|
|
282
|
+
if (input.result.outcome !== "proposed")
|
|
283
|
+
return emptyResult();
|
|
284
|
+
if (input.result.statusChanges.length > 0 || input.result.changes.some((change) => change.operation !== "create")) {
|
|
285
|
+
throw new Error("local_memory_migration_pending_create_only");
|
|
286
|
+
}
|
|
287
|
+
const executionId = input.executionId ?? "unknown";
|
|
288
|
+
const pending = input.result.changes.map((change) => ({
|
|
289
|
+
schemaVersion: 1,
|
|
290
|
+
pendingId: migrationPendingId(change),
|
|
291
|
+
createdAt: new Date().toISOString(),
|
|
292
|
+
reason: "legacy_requires_migration",
|
|
293
|
+
source: { executionId, pruneTraceId: input.pruneTraceId ?? null },
|
|
294
|
+
candidate: change,
|
|
295
|
+
}));
|
|
296
|
+
await assertMemoryAccessDirectory(input.agentDir);
|
|
297
|
+
await withMemoryAccess(input.agentDir, async () => {
|
|
298
|
+
const memory = await readFile(join(input.agentDir, "MEMORY.md"), "utf8").catch(() => null);
|
|
299
|
+
if (memory === null || detectLocalMemoryStorageVersion(memory) !== 1) {
|
|
300
|
+
throw new Error("local_memory_migration_pending_storage_invalid");
|
|
301
|
+
}
|
|
302
|
+
for (const candidate of pending) {
|
|
303
|
+
await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
|
|
304
|
+
}
|
|
305
|
+
}, { pendingOnly: true });
|
|
306
|
+
const pendingIds = [...new Set(pending.map((candidate) => candidate.pendingId))];
|
|
307
|
+
return { createdIds: [], updatedIds: [], statusChangedIds: [], deferredIds: pendingIds, pendingIds };
|
|
308
|
+
}
|
|
262
309
|
function isWithin(root, candidate) {
|
|
263
310
|
const path = relative(root, candidate);
|
|
264
311
|
return path === "" || (path !== ".." && !path.startsWith(`..${sep}`));
|
|
@@ -357,10 +404,13 @@ async function withApplyLease(agentDir, operation) {
|
|
|
357
404
|
}
|
|
358
405
|
export async function applyLocalMemoryResult(input) {
|
|
359
406
|
validateCandidate(input.result);
|
|
360
|
-
if (input.
|
|
407
|
+
if (input.migrationDeferredPending)
|
|
408
|
+
return stageMigrationDeferredPending(input);
|
|
409
|
+
if (input.result.outcome !== "proposed" && input.requiredStorageVersion !== 2)
|
|
361
410
|
return emptyResult();
|
|
362
411
|
const memoryPath = join(input.agentDir, "MEMORY.md");
|
|
363
|
-
|
|
412
|
+
if (input.result.outcome === "proposed")
|
|
413
|
+
await assertWritablePaths(input.agentDir, memoryPath);
|
|
364
414
|
await assertMemoryAccessDirectory(input.agentDir);
|
|
365
415
|
return withApplyLease(input.agentDir, async () => withMemoryAccess(input.agentDir, async () => {
|
|
366
416
|
await assertMemoryWriterAllowed(input.agentDir, input.writerCapabilities ?? []);
|
|
@@ -370,6 +420,13 @@ export async function applyLocalMemoryResult(input) {
|
|
|
370
420
|
const memory = await readFile(memoryPath, "utf8");
|
|
371
421
|
assertLocalMemoryStorageSize(memory);
|
|
372
422
|
const fullStorage = detectLocalMemoryStorageVersion(memory) === 2;
|
|
423
|
+
if (input.requiredStorageVersion === 2 && !fullStorage) {
|
|
424
|
+
throw new Error("local_memory_v2_state_invalid_before_commit");
|
|
425
|
+
}
|
|
426
|
+
// A V2 no-op is still a commit decision. Return only after the access
|
|
427
|
+
// lock and committed-state writer gate have validated the same snapshot.
|
|
428
|
+
if (input.result.outcome !== "proposed")
|
|
429
|
+
return emptyResult();
|
|
373
430
|
if (parseUnmarkedMemoryIndex(memory).length > 0)
|
|
374
431
|
throw new Error("local_memory_legacy_index_requires_migration");
|
|
375
432
|
let entries = [...parseLocalMemoryIndex(memory)];
|
|
@@ -535,6 +592,10 @@ export async function applyLocalMemoryResult(input) {
|
|
|
535
592
|
return [];
|
|
536
593
|
});
|
|
537
594
|
for (const pending of pendingCandidatesOnDisk) {
|
|
595
|
+
// Migration-deferred candidates require reviewed import; only capacity
|
|
596
|
+
// overflow is eligible for the existing automatic promotion path.
|
|
597
|
+
if (pending.reason !== "index_over_budget")
|
|
598
|
+
continue;
|
|
538
599
|
const change = pending.candidate;
|
|
539
600
|
if (change.operation !== "create")
|
|
540
601
|
continue;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { detectLocalMemoryStorageVersion } from "./storage-format.js";
|
|
2
|
+
/** Resolve format and rollout into one writer decision before a prune result is requested. */
|
|
3
|
+
export function resolveLocalMemoryWriterMode(workspace, rollout) {
|
|
4
|
+
if (rollout.legacyFallback)
|
|
5
|
+
return "legacy";
|
|
6
|
+
if (!rollout.enabled)
|
|
7
|
+
return rollout.deferred ? "blocked" : "legacy";
|
|
8
|
+
// Production workspaces always report this fact. Historical partial unit
|
|
9
|
+
// callers retain their already-resolved rollout witness.
|
|
10
|
+
if (workspace.localMemoryV2Initialized === undefined)
|
|
11
|
+
return "v2";
|
|
12
|
+
return detectLocalMemoryStorageVersion(workspace.memory) === 2 ? "v2" : "blocked";
|
|
13
|
+
}
|
|
@@ -8,6 +8,7 @@ const INJECTION_MARKERS = [
|
|
|
8
8
|
"## [注入] 你的 MEMORY.md(索引,只读参考)\n",
|
|
9
9
|
];
|
|
10
10
|
const LOCAL_MEMORY_HASH_CAP_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
export const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 2_000;
|
|
11
12
|
function errorCode(error) {
|
|
12
13
|
const code = error?.code;
|
|
13
14
|
return typeof code === "string" ? code : error instanceof Error ? error.name : "unknown";
|
|
@@ -77,31 +78,37 @@ async function inspectPendingDirectory(path, signal) {
|
|
|
77
78
|
return { ...base, error_code: errorCode(error) };
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
|
-
export async function
|
|
81
|
+
export async function withinLocalMemoryDiagnosticsDeadline(operation, timeoutMs) {
|
|
81
82
|
const controller = new AbortController();
|
|
82
83
|
let timer;
|
|
83
|
-
const inspection = Promise.all([
|
|
84
|
-
inspectFile(join(homeDir, "MEMORY.md"), controller.signal),
|
|
85
|
-
inspectFile(join(homeDir, "notes", "lessons.md"), controller.signal),
|
|
86
|
-
inspectPendingDirectory(join(homeDir, ".crew", "memory", "pending"), controller.signal),
|
|
87
|
-
]).then(([memory, lessons, pending]) => ({ memory, lessons, pending }));
|
|
88
84
|
const timeout = new Promise((_resolve, reject) => {
|
|
89
85
|
timer = setTimeout(() => {
|
|
90
|
-
|
|
91
|
-
reject(Object.assign(new Error("local memory diagnostics timed out"), {
|
|
86
|
+
const error = Object.assign(new Error("local memory diagnostics timed out"), {
|
|
92
87
|
code: "diagnostics_timeout",
|
|
93
|
-
})
|
|
88
|
+
});
|
|
89
|
+
controller.abort(error);
|
|
90
|
+
reject(error);
|
|
94
91
|
}, Math.max(1, timeoutMs));
|
|
95
92
|
timer.unref?.();
|
|
96
93
|
});
|
|
97
94
|
try {
|
|
98
|
-
return await Promise.race([
|
|
95
|
+
return await Promise.race([operation(controller.signal), timeout]);
|
|
99
96
|
}
|
|
100
97
|
finally {
|
|
101
98
|
if (timer !== undefined)
|
|
102
99
|
clearTimeout(timer);
|
|
103
100
|
}
|
|
104
101
|
}
|
|
102
|
+
export async function inspectLocalMemoryFilesWithinDeadline(homeDir, timeoutMs) {
|
|
103
|
+
return withinLocalMemoryDiagnosticsDeadline(async (signal) => {
|
|
104
|
+
const [memory, lessons, pending] = await Promise.all([
|
|
105
|
+
inspectFile(join(homeDir, "MEMORY.md"), signal),
|
|
106
|
+
inspectFile(join(homeDir, "notes", "lessons.md"), signal),
|
|
107
|
+
inspectPendingDirectory(join(homeDir, ".crew", "memory", "pending"), signal),
|
|
108
|
+
]);
|
|
109
|
+
return { memory, lessons, pending };
|
|
110
|
+
}, timeoutMs);
|
|
111
|
+
}
|
|
105
112
|
export function localMemoryContentFacts(content, maxScanBytes = LOCAL_MEMORY_HASH_CAP_BYTES) {
|
|
106
113
|
const sizeBytes = Buffer.byteLength(content, "utf8");
|
|
107
114
|
if (sizeBytes > Math.max(0, maxScanBytes)) {
|
|
@@ -256,15 +263,50 @@ function crewMemoryReadAccess(command, paths) {
|
|
|
256
263
|
return null;
|
|
257
264
|
}
|
|
258
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Decode only the shell wrapper shape emitted by supported runtimes. The body must be one complete
|
|
268
|
+
* quoted argument. Double-quoted bodies may escape only a quote or backslash; shell expansion and
|
|
269
|
+
* concatenated quoting stay unknown so telemetry never guesses what the shell actually executed.
|
|
270
|
+
*/
|
|
271
|
+
function unwrapLiteralShellCommand(command) {
|
|
272
|
+
const trimmed = command.trim();
|
|
273
|
+
const wrapper = /^\/bin\/(?:(?:zsh|bash)\s+-lc|sh\s+-c)\s+([\s\S]+)$/u.exec(trimmed);
|
|
274
|
+
if (wrapper === null)
|
|
275
|
+
return trimmed;
|
|
276
|
+
const quoted = wrapper[1];
|
|
277
|
+
const quote = quoted[0];
|
|
278
|
+
if ((quote !== "'" && quote !== '"') || quoted.at(-1) !== quote || quoted.length < 2)
|
|
279
|
+
return null;
|
|
280
|
+
const body = quoted.slice(1, -1);
|
|
281
|
+
if (quote === "'")
|
|
282
|
+
return body.includes("'") ? null : body;
|
|
283
|
+
let decoded = "";
|
|
284
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
285
|
+
const character = body[index];
|
|
286
|
+
if (character === '"' || character === "$" || character === "`")
|
|
287
|
+
return null;
|
|
288
|
+
if (character !== "\\") {
|
|
289
|
+
decoded += character;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const escaped = body[index + 1];
|
|
293
|
+
if (escaped !== '"' && escaped !== "\\")
|
|
294
|
+
return null;
|
|
295
|
+
decoded += escaped;
|
|
296
|
+
index += 1;
|
|
297
|
+
}
|
|
298
|
+
return decoded;
|
|
299
|
+
}
|
|
259
300
|
function commandAccesses(command, paths) {
|
|
260
|
-
|
|
301
|
+
const unwrapped = unwrapLiteralShellCommand(command);
|
|
302
|
+
if (unwrapped === null || /[;|&\n]/.test(unwrapped))
|
|
261
303
|
return [];
|
|
262
304
|
const accesses = new Map();
|
|
263
|
-
const memoryRead = crewMemoryReadAccess(
|
|
305
|
+
const memoryRead = crewMemoryReadAccess(unwrapped, paths);
|
|
264
306
|
if (memoryRead !== null) {
|
|
265
307
|
accesses.set(`${memoryRead.target}:${memoryRead.relative_path ?? ""}:read`, memoryRead);
|
|
266
308
|
}
|
|
267
|
-
const expanded =
|
|
309
|
+
const expanded = unwrapped
|
|
268
310
|
.replaceAll("${CREW_HOME}", paths.homeDir)
|
|
269
311
|
.replaceAll("$CREW_HOME", paths.homeDir)
|
|
270
312
|
.replace(/["']/g, "");
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
// 与另一个进程中的 marketplace-protocol.ts 保持一致;跨进程契约测试校验摘要。
|
|
3
3
|
export const MARKETPLACE_CAPABILITY = "marketplace_update_v1";
|
|
4
4
|
export const MARKETPLACE = "nowwork-pm";
|
|
5
|
-
export const PLUGIN = "
|
|
5
|
+
export const PLUGIN = "aiinterview-plugin";
|
|
6
6
|
export const MarketplaceModeSchema = z.enum(["inspect", "refresh", "update"]);
|
|
7
7
|
export const MarketplaceStatusSchema = z.enum(["queued", "running", "completed", "partial", "failed", "unknown"]);
|
|
8
8
|
export const MarketplaceTargetSchema = z.object({
|
|
@@ -10,7 +10,7 @@ const Target = z.object({
|
|
|
10
10
|
version: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/),
|
|
11
11
|
source: z.object({ source: z.literal("git-subdir"),
|
|
12
12
|
url: z.literal("git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git"),
|
|
13
|
-
path: z.literal("plugins/
|
|
13
|
+
path: z.literal("plugins/aiinterview-plugin"), sha: z.string().regex(/^[a-f0-9]{40}$/) }),
|
|
14
14
|
});
|
|
15
15
|
export async function readRemoteMarketplace(runtime, registered, env, command) {
|
|
16
16
|
const source = validateMarketplaceSource(registered.url, registered.ref);
|
|
@@ -12,7 +12,7 @@ import { readMarketplaceSource } from "./marketplace-source.js";
|
|
|
12
12
|
export { MarketplaceFailure, runMarketplaceCommand } from "./marketplace-command.js";
|
|
13
13
|
const SELECTOR = `${PLUGIN}@${MARKETPLACE}`;
|
|
14
14
|
const SOURCE = "git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git";
|
|
15
|
-
const SOURCE_PATH = "plugins/
|
|
15
|
+
const SOURCE_PATH = "plugins/aiinterview-plugin";
|
|
16
16
|
const obj = (x) => x !== null && typeof x === "object" && !Array.isArray(x) ? x : {};
|
|
17
17
|
const parse = (text) => { try {
|
|
18
18
|
return JSON.parse(text);
|
|
@@ -10,6 +10,8 @@ export function createMemoryMigrationController(input) {
|
|
|
10
10
|
const runs = new Map();
|
|
11
11
|
return {
|
|
12
12
|
setIdentity(value) { identity = value; },
|
|
13
|
+
clearIdentity() { identity = null; },
|
|
14
|
+
getIdentity() { return identity === null ? null : { ...identity }; },
|
|
13
15
|
cancel(executionId) { const run = runs.get(executionId); if (!run)
|
|
14
16
|
return false; run.cancel(); return true; },
|
|
15
17
|
async handle(raw) {
|
|
@@ -3,38 +3,71 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { withMemoryAccess } from "./local-memory/migration-access.js";
|
|
5
5
|
import { MigrationStateSchema } from "./local-memory/migration-schema.js";
|
|
6
|
+
import { assertMigrationStateIntegrity } from "./local-memory/migration-state-invariants.js";
|
|
6
7
|
import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./local-memory/storage-format.js";
|
|
7
8
|
import { resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
|
|
8
9
|
import { readMemoryInventorySource, inventoryLimits } from "./local-memory/inventory-source.js";
|
|
10
|
+
import { isLocalMemoryInitializationIdentity, localMemoryInitializationIdentity } from "./local-memory/initialization-identity.js";
|
|
9
11
|
export async function inspectMemoryForMigration(agentsRoot, input, journal) {
|
|
10
12
|
const parent = await realpath(agentsRoot);
|
|
11
13
|
const path = join(parent, input.handle);
|
|
12
|
-
const
|
|
14
|
+
const missingBase = { workspaceId: input.workspaceId, agentId: input.agentId, machineId: input.machineId,
|
|
13
15
|
storageRootFingerprint: createHash("sha256").update(resolve(path)).digest("hex"),
|
|
14
16
|
ownerIdentity: String((await lstat(parent)).uid) };
|
|
15
17
|
const prior = input.executionId ? await journal?.get(input.executionId) : null;
|
|
16
18
|
const stoppedExecutionId = prior?.requestedRuntime === "memory_migration" && (prior.state === "completed" || prior.state === "interrupted") ? prior.executionId : null;
|
|
17
|
-
const
|
|
19
|
+
const missing = { ...missingBase, storageVersion: null, classification: "empty", emptyWithoutResidue: true, localState: null, issueCodes: [], stoppedExecutionId };
|
|
18
20
|
try {
|
|
19
21
|
await lstat(path);
|
|
20
22
|
}
|
|
21
23
|
catch (e) {
|
|
22
24
|
if (e.code === "ENOENT")
|
|
23
|
-
return
|
|
25
|
+
return missing;
|
|
24
26
|
throw e;
|
|
25
27
|
}
|
|
28
|
+
const root = await realpath(path);
|
|
29
|
+
const base = { ...missingBase,
|
|
30
|
+
storageRootFingerprint: createHash("sha256").update(root).digest("hex"),
|
|
31
|
+
ownerIdentity: String((await lstat(root)).uid) };
|
|
32
|
+
const empty = { ...missing, ...base };
|
|
26
33
|
try {
|
|
27
34
|
return await withMemoryAccess(path, async () => {
|
|
28
35
|
let localState = null;
|
|
29
36
|
try {
|
|
30
37
|
const record = await readMemoryInventorySource(path, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
|
|
31
38
|
const state = MigrationStateSchema.parse(JSON.parse(record.content));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
assertMigrationStateIntegrity(state, state.identity);
|
|
40
|
+
const expectedInitialization = state.origin === "initialized"
|
|
41
|
+
? await localMemoryInitializationIdentity(path, input.agentId, {
|
|
42
|
+
workspaceId: input.workspaceId,
|
|
43
|
+
machineId: input.machineId,
|
|
44
|
+
})
|
|
45
|
+
: null;
|
|
46
|
+
const expectedLegacyInitialization = state.origin === "initialized"
|
|
47
|
+
? await localMemoryInitializationIdentity(path, input.agentId)
|
|
48
|
+
: null;
|
|
49
|
+
const localInitialization = expectedInitialization !== null
|
|
50
|
+
&& (isLocalMemoryInitializationIdentity(state.identity, expectedInitialization)
|
|
51
|
+
|| expectedLegacyInitialization !== null
|
|
52
|
+
&& isLocalMemoryInitializationIdentity(state.identity, expectedLegacyInitialization));
|
|
53
|
+
const serverBoundIdentity = state.identity.workspaceId === input.workspaceId
|
|
54
|
+
&& state.identity.machineId === input.machineId;
|
|
55
|
+
const operationIdentityValid = state.origin === "initialized" ? localInitialization : serverBoundIdentity;
|
|
56
|
+
if (state.identity.rootFingerprint !== base.storageRootFingerprint
|
|
57
|
+
|| state.identity.ownerIdentity !== base.ownerIdentity
|
|
58
|
+
|| state.identity.agentId !== input.agentId || !operationIdentityValid) {
|
|
35
59
|
throw new Error("migration_identity_mismatch");
|
|
60
|
+
}
|
|
36
61
|
localState = { migrationId: state.migrationId, phase: state.phase, storageVersion: state.storageVersion,
|
|
37
|
-
sourceDigest: state.sourceDigest, planDigest: state.planDigest
|
|
62
|
+
sourceDigest: state.sourceDigest, planDigest: state.planDigest, origin: state.origin,
|
|
63
|
+
issues: state.issues.map(issue => issue.reason),
|
|
64
|
+
initializationIdentity: localInitialization ? {
|
|
65
|
+
agentId: state.identity.agentId, migrationId: state.identity.migrationId,
|
|
66
|
+
workspaceId: state.identity.workspaceId, machineId: state.identity.machineId,
|
|
67
|
+
executionId: state.identity.executionId, sourceStorageVersion: state.identity.sourceStorageVersion,
|
|
68
|
+
targetStorageVersion: state.identity.targetStorageVersion,
|
|
69
|
+
rootFingerprint: state.identity.rootFingerprint, ownerIdentity: state.identity.ownerIdentity,
|
|
70
|
+
} : null };
|
|
38
71
|
if (state.phase === "applying" || state.phase === "repair_required")
|
|
39
72
|
return {
|
|
40
73
|
...empty, localState, classification: "repair_required", emptyWithoutResidue: false, issueCodes: ["migration_recovery_required"],
|
package/dist/prompt.js
CHANGED
|
@@ -346,7 +346,7 @@ ${interactiveTaskRules}${freshnessRule}${externalReplyRule}${collaborationEtique
|
|
|
346
346
|
|
|
347
347
|
## Workspace 与分层记忆(CRITICAL — 索引+按需,配合上面的并行规则)
|
|
348
348
|
你的持久记忆在 \`$CREW_HOME\`(跨你所有任务共享),按“常驻索引 + 功能明细 + 当前工作”分层:
|
|
349
|
-
1. **MEMORY.md = 常驻索引 +
|
|
349
|
+
1. **MEMORY.md = 常驻索引 + 角色**:提示词末尾可能只有它的有界预览,预算不足时也可能省略。预览未命中不等于没有记忆。旧版索引可能包含 \`scope\`;V2 规范字段为 \`id/file/type/status/summary/triggers/priority/revision/anchor/updated\`(可选 \`review_after\`),不含 \`scope\`。其中 file 是相对于 \`$CREW_HOME\` 的路径(例如 \`notes/testing.md\`);需要历史约束、经验或偏好时先检索完整索引,再按需分段读取。
|
|
350
350
|
2. **notes/<topic>.md = 功能明细**:按用户偏好、项目规则、测试、调试、Git、事故复盘等主题拆分。优先读取与当前任务匹配的文件;\`notes/lessons.md\` 只保留未分类经验和历史兼容内容,不是所有新经验的默认目的地。普通任务默认只读不写,但当前用户明确要求时可以按最小范围更新;任务结束后的专门收尾执行仍按收尾指令复核。
|
|
351
351
|
- 对旧版 MEMORY.md,如果条目没有完整结构化字段但明确指向 \`notes/<file>.md\`,把它视为 legacy 索引;仍需结合条目标题/链接和当前请求判断相关性,不要因为缺少 summary/triggers 就假定正文已注入。
|
|
352
352
|
3. **本任务 work-log = \`$CREW_TASK_LOG\`**:每任务一份(别的运行不会碰),记当前阶段/下一步/卡点/关键结论。系统已把它的当前内容注入末尾让你续上;有进展就更新它——这是你**唯一**该在任务中持续写的记忆文件。**work-log 是现状快照,不是流水账**:保持 ≤100 行,更新时重写全文替换过时内容,不要无限追加(过长会被注入截断)。
|
|
@@ -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
|
@@ -668,6 +668,7 @@ export function serve(config, opts = {}) {
|
|
|
668
668
|
const execution = executeProtocol(config, spec, {
|
|
669
669
|
...opts.execution?.dependencies,
|
|
670
670
|
machineEnvironment: machineRuntime.provider,
|
|
671
|
+
...(memoryMigrations.getIdentity() === null ? {} : { controlIdentity: memoryMigrations.getIdentity() }),
|
|
671
672
|
capabilities: capabilities.executionRunner,
|
|
672
673
|
projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkills,
|
|
673
674
|
abilityRelease: opts.execution?.dependencies?.abilityRelease ?? agentAbilityRuntime.materializer,
|
|
@@ -1234,6 +1235,7 @@ export function serve(config, opts = {}) {
|
|
|
1234
1235
|
return;
|
|
1235
1236
|
// server 能力随连接失效;下个连接的 ready 帧重新声明(防降级重连后沿用过期能力)。
|
|
1236
1237
|
serverCapabilities = new Set();
|
|
1238
|
+
memoryMigrations.clearIdentity();
|
|
1237
1239
|
completionRetransmitter.pause();
|
|
1238
1240
|
// 4001 = 控制面应用级「鉴权失败」关闭码(见 server control-plane.ts)。即使上面的
|
|
1239
1241
|
// error 帧因 close 抢先而丢失,也能据关闭码识别这是凭证失效——退避拉满,不再每秒热循环。
|
package/dist/workspace.js
CHANGED
|
@@ -15,6 +15,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
15
15
|
import { createRequire } from "node:module";
|
|
16
16
|
import { dslog } from "./slog.js";
|
|
17
17
|
import { MigrationStateSchema } from "./local-memory/migration-schema.js";
|
|
18
|
+
import { assertMigrationStateIntegrity } from "./local-memory/migration-state-invariants.js";
|
|
18
19
|
import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
|
|
19
20
|
/** 文件系统安全的 taskKey:仅留 [\w.-],其余转 _,截断,避免路径穿越/超长。 */
|
|
20
21
|
export function safeKey(key) {
|
|
@@ -55,8 +56,10 @@ async function hasLocalMemoryV2Marker(agentDir) {
|
|
|
55
56
|
return false;
|
|
56
57
|
const root = await realpath(agentDir);
|
|
57
58
|
const state = MigrationStateSchema.parse(JSON.parse(await readFile(join(memoryDir, "migration.v1.json"), "utf8")));
|
|
59
|
+
assertMigrationStateIntegrity(state, state.identity);
|
|
58
60
|
return state.phase === "committed" && state.storageVersion === 2
|
|
59
61
|
&& state.identity.rootFingerprint === createHash("sha256").update(root).digest("hex")
|
|
62
|
+
&& state.identity.ownerIdentity === String((await lstat(root)).uid)
|
|
60
63
|
&& detectLocalMemoryStorageVersion(await readFile(join(root, "MEMORY.md"), "utf8")) === 2;
|
|
61
64
|
}
|
|
62
65
|
catch {
|