@nowcrew/daemon 0.6.87 → 0.6.88-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/README.md +45 -10
- package/dist/agent-memory/policy.js +3 -1
- package/dist/cloud-report-receipt.js +83 -0
- package/dist/cloud-report-uploader.js +36 -25
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +9 -5
- package/dist/list-models.js +2 -0
- package/dist/local-execution-prompt.js +19 -25
- package/dist/local-executor.js +67 -85
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +40 -0
- package/dist/local-memory/read-cli.js +281 -0
- package/dist/local-memory/runtime.js +56 -7
- package/dist/local-memory/write.js +16 -48
- package/dist/local-memory-diagnostics.js +79 -12
- package/dist/native-thread-context.js +59 -0
- package/dist/prompt.js +39 -24
- package/dist/runner.js +14 -15
- package/dist/runtime-environment-telemetry.js +27 -0
- package/dist/runtimes/claude-session-start-hook.js +24 -0
- package/dist/runtimes/claude-thread-runner.js +266 -0
- package/dist/runtimes/claude.js +23 -4
- package/dist/runtimes/codex-app-server-runner.js +43 -4
- package/dist/runtimes/kimi-acp-runner.js +6 -2
- package/dist/runtimes/opencode-runner.js +7 -1
- package/dist/serve.js +7 -8
- package/dist/session.js +4 -0
- package/dist/supervised-runtime.js +11 -16
- package/dist/thread-input-delivery.js +217 -0
- package/dist/workspace.js +15 -0
- package/package.json +2 -2
package/dist/local-executor.js
CHANGED
|
@@ -12,8 +12,11 @@ import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
|
12
12
|
import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
|
|
13
13
|
import { augmentedPath } from "./runtime-path.js";
|
|
14
14
|
import { deliverCloudReports, prepareCloudReportExecution } from "./cloud-report-uploader.js";
|
|
15
|
+
import { reportDeliveryContext } from "./cloud-report-receipt.js";
|
|
15
16
|
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
16
17
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
18
|
+
import { NativeThreadContext, isolateMaintenanceSession } from "./native-thread-context.js";
|
|
19
|
+
import { prepareThreadDelivery } from "./thread-input-delivery.js";
|
|
17
20
|
import { providerErrorFromExit } from "./provider-error.js";
|
|
18
21
|
import { createConsoleFormatter } from "./console-formatter.js";
|
|
19
22
|
import { withLocalExecutionFacts, } from "./local-execution-prompt.js";
|
|
@@ -34,10 +37,11 @@ import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPr
|
|
|
34
37
|
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
35
38
|
import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
36
39
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
37
|
-
import { machinePackageManagerEnv
|
|
40
|
+
import { machinePackageManagerEnv } from "./package-manager-env.js";
|
|
41
|
+
import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.js";
|
|
38
42
|
import { sanitizeEnvVars } from "./runtime-env.js";
|
|
39
|
-
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath,
|
|
40
|
-
import {
|
|
43
|
+
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
|
|
44
|
+
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
41
45
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
42
46
|
constructor(runtime, timeoutMs) {
|
|
43
47
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -106,6 +110,9 @@ async function launchLegacyRuntime(request) {
|
|
|
106
110
|
if (request.runtime === "deepseek-harness") {
|
|
107
111
|
throw new Error("DeepSeek Harness requires execution protocol v1");
|
|
108
112
|
}
|
|
113
|
+
if (request.runtime !== "claude")
|
|
114
|
+
request = { ...request, wakePrompt: (await prepareThreadDelivery({ env: request.env,
|
|
115
|
+
resumed: false, sessionId: "unverified-cold-legacy", wakePrompt: request.wakePrompt })).prompt };
|
|
109
116
|
const common = {
|
|
110
117
|
bin: request.bin,
|
|
111
118
|
cwd: request.cwd,
|
|
@@ -173,6 +180,7 @@ async function withKeyedLease(key, operation, cancellation) {
|
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
182
|
export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
183
|
+
input = isolateMaintenanceSession(input);
|
|
176
184
|
const versionedProjectSkills = input.projectSkills !== undefined
|
|
177
185
|
&& !Array.isArray(input.projectSkills);
|
|
178
186
|
if (versionedProjectSkills && (input.runtime.name !== "codex" && input.runtime.name !== "claude")) {
|
|
@@ -253,9 +261,15 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
253
261
|
let localMemoryResultPath = null;
|
|
254
262
|
let executionWorkspace = workspace;
|
|
255
263
|
let localMemoryTelemetry = null;
|
|
264
|
+
let nativeThreadContext = null;
|
|
256
265
|
const inheritedEnv = { ...process.env };
|
|
257
|
-
const
|
|
266
|
+
const reportRoots = {
|
|
267
|
+
root: workspace.dir,
|
|
268
|
+
...(input.launch.qaReportRoot === undefined ? {} : { qaReportRoot: input.launch.qaReportRoot }),
|
|
269
|
+
};
|
|
270
|
+
const cloudReportPreparation = await prepareCloudReportExecution({ env: inheritedEnv, ...reportRoots, executionId: input.executionId, agentHandle: input.handle });
|
|
258
271
|
const { facts: cloudReportUpload, snapshot: reportSnapshot } = cloudReportPreparation;
|
|
272
|
+
const reportContext = reportDeliveryContext(input.executionId, workspace.runDir, cloudReportUpload);
|
|
259
273
|
for (const key of Object.keys(inheritedEnv)) {
|
|
260
274
|
if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
|
|
261
275
|
delete inheritedEnv[key];
|
|
@@ -287,12 +301,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
287
301
|
: null;
|
|
288
302
|
const currentPrior = storedCurrentPrior !== null
|
|
289
303
|
&& storedCurrentPrior.sessionContextFingerprint === input.sessionContextFingerprint
|
|
304
|
+
&& storedCurrentPrior.runtime === runtime.name
|
|
305
|
+
&& storedCurrentPrior.threadRoot === input.resumeKey
|
|
290
306
|
? storedCurrentPrior
|
|
291
307
|
: null;
|
|
292
|
-
//
|
|
293
|
-
// older legacy path stored metadata under tasks/<safe thread key>. Import
|
|
294
|
-
// that metadata once so an existing thread can cross the protocol boundary
|
|
295
|
-
// without losing its native runtime conversation.
|
|
308
|
+
// Import compatible legacy metadata across the protocol boundary.
|
|
296
309
|
const legacyRunDir = input.session.enabled
|
|
297
310
|
&& supportsNativeResume
|
|
298
311
|
&& input.keyMode === "opaque"
|
|
@@ -308,7 +321,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
308
321
|
const legacyPrior = legacyRunDir === null || (currentPrior !== null && !legacyCwdPinned)
|
|
309
322
|
? null
|
|
310
323
|
: await readSession(legacyRunDir);
|
|
311
|
-
const
|
|
324
|
+
const compatibleLegacyPrior = legacyPrior?.runtime === runtime.name && legacyPrior?.threadRoot === input.resumeKey
|
|
325
|
+
? legacyPrior : null;
|
|
326
|
+
const prior = currentPrior ?? compatibleLegacyPrior;
|
|
312
327
|
const sessionExists = workspace.sessionResume || legacyPrior !== null;
|
|
313
328
|
const usesLegacyCwd = legacyRunDir !== null
|
|
314
329
|
&& (legacyCwdPinned || (currentPrior === null && legacyPrior !== null));
|
|
@@ -329,11 +344,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
329
344
|
? prior?.model ?? currentModel
|
|
330
345
|
: currentModel;
|
|
331
346
|
const resumeSessionId = supportsNativeResume && sessionExists
|
|
332
|
-
? pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, input.session.budgetTokens, input.session.maxTurns, providerFp)
|
|
347
|
+
? pickResumeId(input.resumeKey && (!Number.isSafeInteger(prior?.threadContextSeq) || prior?.threadHistoryComplete !== true) ? null : prior, Date.now(), input.resumeKey ? 0 : input.session.warmMs, resumeComparisonModel, input.resumeKey ? 0 : input.session.budgetTokens, input.resumeKey ? 0 : input.session.maxTurns, providerFp)
|
|
333
348
|
: null;
|
|
334
349
|
const resuming = resumeSessionId !== null;
|
|
335
350
|
const rotatedForBudget = !resuming && supportsNativeResume && sessionExists
|
|
336
|
-
&& pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
351
|
+
&& pickResumeId(prior, Date.now(), input.resumeKey ? 0 : input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
337
352
|
const nearBudget = resuming && isNearBudget(prior, input.session.softTokens);
|
|
338
353
|
const rotated = Boolean(workspace.agentSessionId && sessionExists && !resuming);
|
|
339
354
|
// Codex and Kimi choose their own id on a fresh start, so the native id persisted in
|
|
@@ -381,28 +396,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
381
396
|
}
|
|
382
397
|
}
|
|
383
398
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
if (input.localMemoryV2Enabled === true) {
|
|
387
|
-
try {
|
|
388
|
-
const pendingCandidates = await searchPendingMemoryCandidates({
|
|
389
|
-
agentDir: executionWorkspace.dir,
|
|
390
|
-
query: wakePrompt,
|
|
391
|
-
limit: 5,
|
|
392
|
-
});
|
|
393
|
-
pendingMemoryContext = renderPendingMemoryContext(pendingCandidates);
|
|
394
|
-
}
|
|
395
|
-
catch (error) {
|
|
396
|
-
dslog("local_memory.pending_recall_failed", "本地归档记忆召回失败", {
|
|
397
|
-
level: "WARN",
|
|
398
|
-
execution_id: input.executionId,
|
|
399
|
-
agent_handle: input.handle,
|
|
400
|
-
error_name: error instanceof Error ? error.name : "unknown",
|
|
401
|
-
error_code: error?.code,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
399
|
+
const baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
400
|
+
memoryPruneTraceId = parseMemoryPruneTraceId(baseWakePrompt);
|
|
406
401
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
407
402
|
&& isLocalMemoryV2Eligible({
|
|
408
403
|
enabledForHandle: true,
|
|
@@ -413,26 +408,15 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
413
408
|
})) {
|
|
414
409
|
localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
|
|
415
410
|
}
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
:
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// The structured prune contract is mandatory for V2. Drop optional archived
|
|
426
|
-
// recall before dropping the daemon result file when the prompt budget is tight.
|
|
427
|
-
if (!fitsPromptBudget(baseSystemPrompt) && pendingMemoryContext !== "") {
|
|
428
|
-
baseSystemPrompt = promptWithoutPending;
|
|
429
|
-
}
|
|
430
|
-
const structuredPromptFits = fitsPromptBudget(baseSystemPrompt);
|
|
431
|
-
if (!structuredPromptFits)
|
|
432
|
-
localMemoryResultPath = null;
|
|
433
|
-
const systemPrompt = localMemoryResultPath === null
|
|
434
|
-
? baseSystemPrompt
|
|
435
|
-
: `${baseSystemPrompt}${structuredMemoryInstructions ?? ""}`;
|
|
411
|
+
const memoryPrompt = await prepareLocalMemoryPrompt({
|
|
412
|
+
agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
|
|
413
|
+
enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
|
|
414
|
+
resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
|
|
415
|
+
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
416
|
+
});
|
|
417
|
+
localMemoryResultPath = memoryPrompt.resultPath;
|
|
418
|
+
const systemPrompt = memoryPrompt.systemPrompt;
|
|
419
|
+
const wakePrompt = baseWakePrompt + memoryPrompt.wakePromptSuffix;
|
|
436
420
|
if (!isolatedMaintenance)
|
|
437
421
|
try {
|
|
438
422
|
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
@@ -491,6 +475,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
491
475
|
}
|
|
492
476
|
memoryPruneFailurePhase = "runtime_prepare";
|
|
493
477
|
const crewHome = isolatedMaintenance ? executionWorkspace.runDir : workspace.dir;
|
|
478
|
+
const contextThreadId = input.threadId ?? input.resumeKey;
|
|
479
|
+
if (contextThreadId && input.maintenancePurpose === undefined) {
|
|
480
|
+
nativeThreadContext = new NativeThreadContext(join(executionWorkspace.runDir, `.crew-context-${input.executionId}.json`), contextThreadId, runtime.name, resumeSessionId, prior);
|
|
481
|
+
await nativeThreadContext.initialize();
|
|
482
|
+
}
|
|
494
483
|
const baseEnv = {
|
|
495
484
|
...inheritedEnv,
|
|
496
485
|
...sanitizeEnvVars(providerConfig.envVars),
|
|
@@ -503,6 +492,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
503
492
|
CREW_TOKEN: input.launch.token,
|
|
504
493
|
CREW_CHANNEL: input.channelId,
|
|
505
494
|
CREW_HOME: crewHome,
|
|
495
|
+
// Empty values override any stale inherited hints from an enclosing runtime.
|
|
496
|
+
CREW_THREAD_ROOT: contextThreadId ?? "",
|
|
497
|
+
CREW_THREAD_CONTEXT_FILE: nativeThreadContext?.path ?? "",
|
|
506
498
|
// Keep the daemon-selected storage root authoritative for runtimes.
|
|
507
499
|
// In profile-backed NowWork sessions the daemon config may differ from
|
|
508
500
|
// the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
|
|
@@ -513,6 +505,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
513
505
|
// Expose only the training root; ability-specific workspace layout stays
|
|
514
506
|
// owned by the training package rather than the NowWork daemon.
|
|
515
507
|
QA_RUNTIME_ROOT: join(crewHome, "training"),
|
|
508
|
+
QA_REPORT_DELIVERY_CONTEXT: JSON.stringify(reportContext),
|
|
516
509
|
CREW_TASK_DIR: executionWorkspace.runDir,
|
|
517
510
|
CREW_TASK_LOG: executionWorkspace.workLogPath,
|
|
518
511
|
...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
|
|
@@ -532,30 +525,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
532
525
|
const childEnv = runtime.name === "deepseek-harness"
|
|
533
526
|
? applyDeepSeekHarnessMachineEnv(providerEnv, inheritedEnv)
|
|
534
527
|
: providerEnv;
|
|
535
|
-
|
|
536
|
-
execution_id: input.executionId,
|
|
537
|
-
agent_handle: input.handle,
|
|
538
|
-
runtime: runtime.name,
|
|
539
|
-
qa_runtime_root: childEnv.QA_RUNTIME_ROOT ?? null,
|
|
540
|
-
qa_runtime_root_present: childEnv.QA_RUNTIME_ROOT !== undefined,
|
|
541
|
-
inherited_qa_runtime_root_present: inheritedEnv.QA_RUNTIME_ROOT !== undefined,
|
|
542
|
-
...packageManagerEnvFacts(childEnv),
|
|
543
|
-
training_root: join(crewHome, "training"),
|
|
544
|
-
cloud_provider: cloudReportUpload.provider,
|
|
545
|
-
oss_report_upload_enabled: cloudReportUpload.enabled,
|
|
546
|
-
project_context_present: input.projectContext !== undefined,
|
|
547
|
-
project_ids: JSON.stringify(input.projectContext === undefined
|
|
548
|
-
? []
|
|
549
|
-
: [
|
|
550
|
-
...(input.projectContext.primary === undefined
|
|
551
|
-
? []
|
|
552
|
-
: [input.projectContext.primary.projectId]),
|
|
553
|
-
...input.projectContext.secondary.map((project) => project.projectId),
|
|
554
|
-
]),
|
|
555
|
-
ability_release_present: input.abilityRelease !== undefined,
|
|
556
|
-
ability_release_id: input.abilityRelease?.releaseId,
|
|
557
|
-
ability_root_commit: input.abilityRelease?.rootCommit,
|
|
558
|
-
});
|
|
528
|
+
logRuntimeEnvironmentContract(input, childEnv, inheritedEnv, crewHome, cloudReportUpload);
|
|
559
529
|
const warn = dependencies.warnRuntime ?? warnRuntimeLocally;
|
|
560
530
|
const launchRuntime = dependencies.launchRuntime ?? (async (launchReq) => {
|
|
561
531
|
if (launchReq.runtime === "codex" && launchReq.codexCollaborationTools === true) {
|
|
@@ -572,7 +542,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
572
542
|
const effectiveSystemPrompt = abilityContext === undefined
|
|
573
543
|
? systemPrompt
|
|
574
544
|
: `${systemPrompt}\n\n${abilityContext.prompt}`;
|
|
575
|
-
if (Buffer.byteLength(effectiveSystemPrompt, "utf8")
|
|
545
|
+
if (Buffer.byteLength(effectiveSystemPrompt, "utf8") + Buffer.byteLength(memoryPrompt.wakePromptSuffix, "utf8")
|
|
546
|
+
> (input.maxSystemPromptBytes ?? Number.MAX_SAFE_INTEGER)) {
|
|
576
547
|
throw new Error("ability_runtime_context_size_exceeded");
|
|
577
548
|
}
|
|
578
549
|
memoryPruneFailurePhase = "prompt_write";
|
|
@@ -848,6 +819,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
848
819
|
const event = parseLine(line);
|
|
849
820
|
if (!event)
|
|
850
821
|
return;
|
|
822
|
+
nativeThreadContext?.observe(event);
|
|
851
823
|
localMemoryTelemetry?.observe(event);
|
|
852
824
|
if (isRuntimeReadyEvent(runtime.name, event))
|
|
853
825
|
markRuntimeReady();
|
|
@@ -964,22 +936,27 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
964
936
|
localMemoryTelemetry?.markRuntimeExited(exitCode);
|
|
965
937
|
memoryPruneRuntimeExitCode = exitCode;
|
|
966
938
|
if (localMemoryResultPath !== null) {
|
|
939
|
+
memoryPruneFailurePhase = "memory_commit";
|
|
967
940
|
const committed = await commitLocalMemoryResultAfterExit({
|
|
968
941
|
resultPath: localMemoryResultPath,
|
|
969
942
|
agentDir: executionWorkspace.dir,
|
|
970
943
|
executionId: input.executionId,
|
|
971
944
|
pruneTraceId: memoryPruneTraceId,
|
|
972
945
|
exitCode,
|
|
946
|
+
workLogPath: executionWorkspace.workLogPath,
|
|
973
947
|
});
|
|
974
|
-
if (committed.status === "failed") {
|
|
975
|
-
|
|
976
|
-
|
|
948
|
+
if (committed.status === "failed" || committed.status === "missing") {
|
|
949
|
+
const reason = committed.error ?? "local_memory_result_missing";
|
|
950
|
+
dslog("local_memory.commit_failed", "结构化本地记忆提交或清理未完成,保留可用恢复材料", {
|
|
951
|
+
level: "ERROR",
|
|
977
952
|
execution_id: input.executionId,
|
|
978
953
|
agent_handle: input.handle,
|
|
979
|
-
error_code:
|
|
954
|
+
error_code: reason,
|
|
980
955
|
result_path: localMemoryResultPath,
|
|
981
956
|
});
|
|
957
|
+
throw new Error(`local_memory_commit_failed: ${reason}`);
|
|
982
958
|
}
|
|
959
|
+
memoryPruneFailurePhase = "runtime_finalize";
|
|
983
960
|
}
|
|
984
961
|
const errorTail = [
|
|
985
962
|
stderrTail.trim(),
|
|
@@ -1001,9 +978,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1001
978
|
? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
|
|
1002
979
|
: (resuming ? prior?.contextTokens : undefined);
|
|
1003
980
|
await writeSession(workspace.sessionDir, {
|
|
981
|
+
runtime: runtime.name,
|
|
982
|
+
...(input.resumeKey === undefined ? {} : { threadRoot: input.resumeKey }),
|
|
983
|
+
...(exitCode === 0 ? await nativeThreadContext?.checkpoint() ?? {} : {}),
|
|
1004
984
|
sessionId,
|
|
1005
985
|
lastRunAt: Date.now(),
|
|
1006
|
-
turns:
|
|
986
|
+
turns: nativeThreadContext?.confirmed ? (prior?.turns ?? 0) + 1 : 1,
|
|
1007
987
|
model: currentModel,
|
|
1008
988
|
providerFingerprint: providerFp,
|
|
1009
989
|
...(input.sessionContextFingerprint === undefined
|
|
@@ -1018,7 +998,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1018
998
|
}
|
|
1019
999
|
const cloudReportResult = exitCode === 0
|
|
1020
1000
|
? await deliverCloudReports({
|
|
1021
|
-
facts: cloudReportUpload,
|
|
1001
|
+
facts: cloudReportUpload, ...reportRoots, before: reportSnapshot, finalText,
|
|
1002
|
+
receiptFile: reportContext.receiptFile,
|
|
1022
1003
|
externalAnswer: externalOutput.lastClosedBlock
|
|
1023
1004
|
?? (finalText === null ? null : extractExternalAnswer(finalText)),
|
|
1024
1005
|
captureFinal: input.captureFinal, executionId: input.executionId, agentHandle: input.handle,
|
|
@@ -1036,7 +1017,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1036
1017
|
...(usage === undefined ? {} : { usage }),
|
|
1037
1018
|
model: observedModel,
|
|
1038
1019
|
runtime: runtime.name,
|
|
1039
|
-
resumed:
|
|
1020
|
+
resumed: nativeThreadContext?.confirmed ?? false,
|
|
1040
1021
|
sessionId,
|
|
1041
1022
|
errorMessage: errorTail || null,
|
|
1042
1023
|
...(providerError === undefined ? {} : { providerError }),
|
|
@@ -1048,6 +1029,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1048
1029
|
};
|
|
1049
1030
|
}
|
|
1050
1031
|
finally {
|
|
1032
|
+
await nativeThreadContext?.cleanup().catch(() => undefined);
|
|
1051
1033
|
startupReservationState.current?.release();
|
|
1052
1034
|
try {
|
|
1053
1035
|
await localMemoryTelemetry?.finish();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { containsLikelySecret } from "../agent-memory/policy.js";
|
|
3
|
+
const CategorySchema = z.enum([
|
|
4
|
+
"preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
|
|
5
|
+
]);
|
|
6
|
+
export const LocalMemorySingleLineText = z.string().trim().min(1).max(500).regex(/^[^\r\n]+$/u);
|
|
7
|
+
export const LocalMemoryChangeSchema = z.object({
|
|
8
|
+
operation: z.enum(["create", "update", "supersede"]),
|
|
9
|
+
memoryId: LocalMemorySingleLineText.regex(/^[^<>]+$/u).nullable(),
|
|
10
|
+
baseRevision: z.number().int().nonnegative().nullable(),
|
|
11
|
+
supersedesMemoryId: LocalMemorySingleLineText.regex(/^[^<>]+$/u).nullable(),
|
|
12
|
+
title: LocalMemorySingleLineText.max(120),
|
|
13
|
+
kind: z.enum(["procedural", "semantic", "episodic", "preference"]),
|
|
14
|
+
summary: LocalMemorySingleLineText,
|
|
15
|
+
triggers: z.array(LocalMemorySingleLineText.max(80)).min(1).max(16),
|
|
16
|
+
priority: z.enum(["high", "medium", "low"]),
|
|
17
|
+
detailCategory: CategorySchema,
|
|
18
|
+
detailBody: z.string().trim().min(1).max(12_000),
|
|
19
|
+
reviewAfter: LocalMemorySingleLineText.nullable(),
|
|
20
|
+
}).strict();
|
|
21
|
+
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
22
|
+
const RESERVED_MEMORY_MARKER = /NOWWORK:(?:MEMORY_ENTRY|MEMORY_INDEX):/iu;
|
|
23
|
+
export function validateLocalMemoryChange(change) {
|
|
24
|
+
const text = [change.title, change.summary, ...change.triggers, change.detailBody].join("\n");
|
|
25
|
+
if (containsLikelySecret(text))
|
|
26
|
+
throw new Error("local_memory_secret_candidate");
|
|
27
|
+
if (CONTROL_CHARACTER_PATTERN.test(text))
|
|
28
|
+
throw new Error("local_memory_control_character");
|
|
29
|
+
if (RESERVED_MEMORY_MARKER.test(text))
|
|
30
|
+
throw new Error("local_memory_reserved_marker");
|
|
31
|
+
if (change.operation === "create"
|
|
32
|
+
&& (change.memoryId !== null || change.baseRevision !== null || change.supersedesMemoryId !== null)) {
|
|
33
|
+
throw new Error("local_memory_create_revision_invalid");
|
|
34
|
+
}
|
|
35
|
+
if (change.operation === "update"
|
|
36
|
+
&& (change.memoryId === null || change.baseRevision === null || change.supersedesMemoryId !== null)) {
|
|
37
|
+
throw new Error("local_memory_update_revision_missing");
|
|
38
|
+
}
|
|
39
|
+
if (change.operation === "supersede"
|
|
40
|
+
&& (change.memoryId !== null || change.baseRevision === null || change.supersedesMemoryId === null)) {
|
|
41
|
+
throw new Error("local_memory_supersede_revision_missing");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function normalizePendingQuery(value) {
|
|
2
|
+
return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
|
|
3
|
+
}
|
|
4
|
+
function containsTerm(text, term) {
|
|
5
|
+
if (!term)
|
|
6
|
+
return false;
|
|
7
|
+
const startBoundary = /^[a-z0-9]/u.test(term);
|
|
8
|
+
const endBoundary = /[a-z0-9]$/u.test(term);
|
|
9
|
+
let position = text.indexOf(term);
|
|
10
|
+
while (position >= 0) {
|
|
11
|
+
const before = text[position - 1] ?? "";
|
|
12
|
+
const after = text[position + term.length] ?? "";
|
|
13
|
+
if ((!startBoundary || !/[a-z0-9_-]/u.test(before)) && (!endBoundary || !/[a-z0-9_-]/u.test(after)))
|
|
14
|
+
return true;
|
|
15
|
+
position = text.indexOf(term, position + 1);
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
export function pendingRecallScore(item, query) {
|
|
20
|
+
const normalized = normalizePendingQuery(query);
|
|
21
|
+
if (!normalized)
|
|
22
|
+
return 0;
|
|
23
|
+
const candidate = item.candidate;
|
|
24
|
+
const terms = normalized.split(/[\s,,、。!?;::()[\]{}"'`]+/u).filter(Boolean);
|
|
25
|
+
const text = normalizePendingQuery([candidate.title, candidate.summary, ...candidate.triggers, candidate.detailCategory, candidate.detailBody].join("\n"));
|
|
26
|
+
const explicit = [candidate.title, ...candidate.triggers].map(normalizePendingQuery);
|
|
27
|
+
const explicitScore = explicit.reduce((score, term) => score + (containsTerm(normalized, term) ? 10 : 0), 0);
|
|
28
|
+
return explicitScore + terms.reduce((score, term) => score + (containsTerm(text, term) ? 1 : 0), 0);
|
|
29
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { isRecallablePending, PendingIdSchema } from "./pending-schema.js";
|
|
4
|
+
import { normalizePendingQuery, pendingRecallScore } from "./pending-match.js";
|
|
5
|
+
import { pendingDirectorySnapshot, pendingSourceError, PENDING_FILE_BYTES, readPendingSource } from "./pending-source.js";
|
|
6
|
+
const PAGE_FILES = 128;
|
|
7
|
+
const PAGE_BYTES = 1_000_000;
|
|
8
|
+
const OUTPUT_BYTES = 12 * 1024;
|
|
9
|
+
const CursorSchema = z.object({
|
|
10
|
+
v: z.literal(1), queryHash: z.string().regex(/^[a-f0-9]{64}$/u), version: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
11
|
+
after: z.string().min(1).max(255), hadWarnings: z.boolean(),
|
|
12
|
+
}).strict();
|
|
13
|
+
const SearchSchema = z.object({
|
|
14
|
+
agentDir: z.string().min(1), query: z.string().trim().min(1).max(256),
|
|
15
|
+
cursor: z.string().max(2048).optional(), limit: z.number().int().min(1).max(40).default(10),
|
|
16
|
+
}).strict();
|
|
17
|
+
const excerpt = (text, cap) => Array.from(text).slice(0, cap).join("");
|
|
18
|
+
export async function searchPendingMemoryPage(input) {
|
|
19
|
+
const parsed = SearchSchema.safeParse(input);
|
|
20
|
+
if (!parsed.success)
|
|
21
|
+
throw new Error("memory_pending_arguments_invalid");
|
|
22
|
+
const args = parsed.data;
|
|
23
|
+
const queryHash = createHash("sha256").update(normalizePendingQuery(args.query)).digest("hex");
|
|
24
|
+
let cursor;
|
|
25
|
+
if (args.cursor !== undefined) {
|
|
26
|
+
try {
|
|
27
|
+
cursor = CursorSchema.parse(JSON.parse(Buffer.from(args.cursor, "base64url").toString("utf8")));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error("memory_pending_cursor_invalid");
|
|
31
|
+
}
|
|
32
|
+
if (cursor.queryHash !== queryHash)
|
|
33
|
+
throw new Error("memory_pending_cursor_invalid");
|
|
34
|
+
}
|
|
35
|
+
const snapshot = await pendingDirectorySnapshot(args.agentDir);
|
|
36
|
+
if (cursor && cursor.version !== snapshot.version)
|
|
37
|
+
throw new Error("memory_pending_source_changed");
|
|
38
|
+
const initial = cursor ? snapshot.names.indexOf(cursor.after) + 1 : 0;
|
|
39
|
+
if (cursor && initial === 0)
|
|
40
|
+
throw new Error("memory_pending_cursor_invalid");
|
|
41
|
+
const matches = [];
|
|
42
|
+
const warnings = [];
|
|
43
|
+
let position = initial;
|
|
44
|
+
let scannedFiles = 0;
|
|
45
|
+
let scannedBytes = 0;
|
|
46
|
+
// 为游标、错误与元数据预留空间;达到输出上限前停止消费来源,不跳过未输出的命中。
|
|
47
|
+
let outputBytes = 1600;
|
|
48
|
+
while (position < snapshot.names.length && scannedFiles < PAGE_FILES && scannedBytes + PENDING_FILE_BYTES <= PAGE_BYTES) {
|
|
49
|
+
const name = snapshot.names[position];
|
|
50
|
+
let source;
|
|
51
|
+
let warning;
|
|
52
|
+
try {
|
|
53
|
+
source = await readPendingSource(args.agentDir, name);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const code = pendingSourceError(error);
|
|
57
|
+
if (code === "memory_pending_source_changed" || code === "memory_pending_not_found")
|
|
58
|
+
throw new Error("memory_pending_source_changed");
|
|
59
|
+
warning = { file: name, code };
|
|
60
|
+
}
|
|
61
|
+
const match = source && isRecallablePending(source.candidate) && pendingRecallScore(source.candidate, args.query) > 0
|
|
62
|
+
? { pendingId: source.candidate.pendingId, sha256: source.sha256, title: source.candidate.candidate.title,
|
|
63
|
+
summary: excerpt(source.candidate.candidate.summary, 180), triggers: source.candidate.candidate.triggers.slice(0, 4),
|
|
64
|
+
operation: "create", state: "capacity_deferred" }
|
|
65
|
+
: null;
|
|
66
|
+
const addition = match ?? warning;
|
|
67
|
+
const size = addition ? Buffer.byteLength(JSON.stringify(addition), "utf8") + 2 : 0;
|
|
68
|
+
if (outputBytes + size > OUTPUT_BYTES)
|
|
69
|
+
break;
|
|
70
|
+
if (match)
|
|
71
|
+
matches.push(match);
|
|
72
|
+
if (warning)
|
|
73
|
+
warnings.push(warning);
|
|
74
|
+
outputBytes += size;
|
|
75
|
+
scannedFiles += 1;
|
|
76
|
+
scannedBytes += source?.bytes ?? PENDING_FILE_BYTES;
|
|
77
|
+
position += 1;
|
|
78
|
+
if (matches.length >= args.limit)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if ((await pendingDirectorySnapshot(args.agentDir)).version !== snapshot.version)
|
|
82
|
+
throw new Error("memory_pending_source_changed");
|
|
83
|
+
const hasMore = position < snapshot.names.length;
|
|
84
|
+
const hadWarnings = cursor?.hadWarnings === true || warnings.length > 0;
|
|
85
|
+
const nextCursor = hasMore ? Buffer.from(JSON.stringify({ v: 1, queryHash, version: snapshot.version,
|
|
86
|
+
after: snapshot.names[position - 1], hadWarnings })).toString("base64url") : null;
|
|
87
|
+
return { matches, warnings, scannedFiles, scannedBytes, hasMore, nextCursor, complete: !hasMore && !hadWarnings };
|
|
88
|
+
}
|
|
89
|
+
export async function readPendingMemoryBody(input) {
|
|
90
|
+
if (!PendingIdSchema.safeParse(input.pendingId).success)
|
|
91
|
+
throw new Error("memory_pending_id_invalid");
|
|
92
|
+
const source = await readPendingSource(input.agentDir, `${input.pendingId}.json`);
|
|
93
|
+
if (input.ifHash !== undefined && input.ifHash !== source.sha256)
|
|
94
|
+
throw new Error("memory_pending_source_changed");
|
|
95
|
+
if (!isRecallablePending(source.candidate))
|
|
96
|
+
throw new Error("memory_pending_not_recallable");
|
|
97
|
+
const candidate = source.candidate.candidate;
|
|
98
|
+
return { pendingId: source.candidate.pendingId, sha256: source.sha256, title: candidate.title,
|
|
99
|
+
operation: "create", state: "capacity_deferred", content: candidate.detailBody };
|
|
100
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { LocalMemoryChangeSchema, validateLocalMemoryChange } from "./candidate.js";
|
|
3
|
+
// 索引会按分隔符展开触发词;16 个最多 80 UTF-16 单元的输入可产生 0..640 项。
|
|
4
|
+
const ArchivedMemoryChangeSchema = LocalMemoryChangeSchema.extend({
|
|
5
|
+
triggers: z.array(LocalMemoryChangeSchema.shape.triggers.element).max(640)
|
|
6
|
+
.refine((triggers) => triggers.reduce((length, trigger) => length + trigger.length, 0) <= 16 * 80),
|
|
7
|
+
});
|
|
8
|
+
export const PendingIdSchema = z.string().max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u);
|
|
9
|
+
export const PendingMemorySchema = z.object({
|
|
10
|
+
schemaVersion: z.literal(1), pendingId: PendingIdSchema,
|
|
11
|
+
createdAt: z.string().min(1).max(100), reason: z.literal("index_over_budget"),
|
|
12
|
+
source: z.object({ executionId: z.string().min(1).max(500), pruneTraceId: z.string().min(1).max(500).nullable() }).strict(),
|
|
13
|
+
candidate: ArchivedMemoryChangeSchema,
|
|
14
|
+
}).strict();
|
|
15
|
+
export function parsePendingMemoryCandidate(value) {
|
|
16
|
+
const parsed = PendingMemorySchema.safeParse(value);
|
|
17
|
+
if (!parsed.success)
|
|
18
|
+
throw new Error("local_memory_pending_invalid");
|
|
19
|
+
validateLocalMemoryChange(parsed.data.candidate);
|
|
20
|
+
return parsed.data;
|
|
21
|
+
}
|
|
22
|
+
export function isRecallablePending(value) {
|
|
23
|
+
try {
|
|
24
|
+
return parsePendingMemoryCandidate(value).candidate.operation === "create";
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open, opendir, realpath } from "node:fs/promises";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { TextDecoder } from "node:util";
|
|
6
|
+
import { parsePendingMemoryCandidate, PendingIdSchema } from "./pending-schema.js";
|
|
7
|
+
export const PENDING_FILE_BYTES = 128 * 1024;
|
|
8
|
+
const DIRECTORY_ENTRY_CAP = 10_000;
|
|
9
|
+
export function pendingSourceError(error) {
|
|
10
|
+
if (error instanceof Error && /^memory_pending_[a-z_]+$/u.test(error.message))
|
|
11
|
+
return error.message;
|
|
12
|
+
return "memory_pending_file_invalid";
|
|
13
|
+
}
|
|
14
|
+
export async function pendingReadDirectory(agentDir) {
|
|
15
|
+
const root = await realpath(agentDir);
|
|
16
|
+
let directory = root;
|
|
17
|
+
for (const part of [".crew", "memory", "pending"]) {
|
|
18
|
+
directory = join(directory, part);
|
|
19
|
+
let stat;
|
|
20
|
+
try {
|
|
21
|
+
stat = await lstat(directory);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === "ENOENT")
|
|
25
|
+
return { root, directory: null };
|
|
26
|
+
throw new Error("memory_pending_directory_unreadable");
|
|
27
|
+
}
|
|
28
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(directory) !== directory) {
|
|
29
|
+
throw new Error("memory_pending_path_invalid");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { root, directory };
|
|
33
|
+
}
|
|
34
|
+
export async function pendingFileNames(directory) {
|
|
35
|
+
if (directory === null)
|
|
36
|
+
return [];
|
|
37
|
+
const names = [];
|
|
38
|
+
let inspected = 0;
|
|
39
|
+
try {
|
|
40
|
+
for await (const item of await opendir(directory)) {
|
|
41
|
+
if (++inspected > DIRECTORY_ENTRY_CAP)
|
|
42
|
+
throw new Error("memory_pending_directory_over_budget");
|
|
43
|
+
if (item.name.endsWith(".json"))
|
|
44
|
+
names.push(item.name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof Error && error.message.startsWith("memory_pending_"))
|
|
49
|
+
throw error;
|
|
50
|
+
throw new Error("memory_pending_directory_unreadable");
|
|
51
|
+
}
|
|
52
|
+
return names.sort();
|
|
53
|
+
}
|
|
54
|
+
function version(stat) {
|
|
55
|
+
return [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
|
|
56
|
+
}
|
|
57
|
+
export async function pendingDirectorySnapshot(agentDir) {
|
|
58
|
+
const location = await pendingReadDirectory(agentDir);
|
|
59
|
+
const names = await pendingFileNames(location.directory);
|
|
60
|
+
const digest = createHash("sha256").update(location.root);
|
|
61
|
+
if (location.directory !== null)
|
|
62
|
+
digest.update(version(await lstat(location.directory)));
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
try {
|
|
65
|
+
digest.update(JSON.stringify([name, version(await lstat(join(location.directory, name)))]));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new Error("memory_pending_source_changed");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { ...location, names, version: digest.digest("hex") };
|
|
72
|
+
}
|
|
73
|
+
export async function readPendingSource(agentDir, name, byteBudget = PENDING_FILE_BYTES) {
|
|
74
|
+
if (!name.endsWith(".json") || !PendingIdSchema.safeParse(name.slice(0, -5)).success) {
|
|
75
|
+
throw new Error("memory_pending_id_invalid");
|
|
76
|
+
}
|
|
77
|
+
const { directory } = await pendingReadDirectory(agentDir);
|
|
78
|
+
if (directory === null)
|
|
79
|
+
throw new Error("memory_pending_not_found");
|
|
80
|
+
const path = join(directory, name);
|
|
81
|
+
const before = await lstat(path).catch(() => { throw new Error("memory_pending_not_found"); });
|
|
82
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
83
|
+
throw new Error("memory_pending_path_invalid");
|
|
84
|
+
const limit = Math.min(byteBudget, PENDING_FILE_BYTES);
|
|
85
|
+
if (before.size > limit)
|
|
86
|
+
throw new Error("memory_pending_file_over_budget");
|
|
87
|
+
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
88
|
+
try {
|
|
89
|
+
const opened = await handle.stat();
|
|
90
|
+
if (!opened.isFile() || version(opened) !== version(before))
|
|
91
|
+
throw new Error("memory_pending_source_changed");
|
|
92
|
+
const chunks = [];
|
|
93
|
+
let bytes = 0;
|
|
94
|
+
for (;;) {
|
|
95
|
+
const chunk = Buffer.alloc(Math.min(16 * 1024, limit + 1 - bytes));
|
|
96
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
|
|
97
|
+
if (bytesRead === 0)
|
|
98
|
+
break;
|
|
99
|
+
bytes += bytesRead;
|
|
100
|
+
if (bytes > limit)
|
|
101
|
+
throw new Error("memory_pending_file_over_budget");
|
|
102
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
103
|
+
}
|
|
104
|
+
const after = await handle.stat();
|
|
105
|
+
const currentDirectory = await pendingReadDirectory(agentDir);
|
|
106
|
+
const current = await lstat(path);
|
|
107
|
+
if (currentDirectory.directory !== directory || version(after) !== version(opened) || version(current) !== version(opened)) {
|
|
108
|
+
throw new Error("memory_pending_source_changed");
|
|
109
|
+
}
|
|
110
|
+
const buffer = Buffer.concat(chunks);
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
const raw = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
114
|
+
parsed = parsePendingMemoryCandidate(JSON.parse(raw));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new Error("memory_pending_file_invalid");
|
|
118
|
+
}
|
|
119
|
+
if (`${parsed.pendingId}.json` !== name)
|
|
120
|
+
throw new Error("memory_pending_id_invalid");
|
|
121
|
+
return { candidate: parsed, bytes, sha256: createHash("sha256").update(buffer).digest("hex") };
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await handle.close();
|
|
125
|
+
}
|
|
126
|
+
}
|