immune-brain 4.5.0 → 4.6.0
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 +3 -2
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +0 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +47 -2
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +23 -18
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -3
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +679 -762
- package/plugins/immune-brain/dist/imm-loop.md +11 -4
- package/plugins/immune-brain/dist/imm-planner.md +37 -12
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +55 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +73 -27
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +3 -4
- package/plugins/immune-brain/runtime/claude/review_host.ts +15 -0
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +738 -677
- package/plugins/immune-brain/runtime/kernel/application.ts +2 -5
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +1 -2
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +21 -12
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
- package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
- package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +9 -1
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +120 -31
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +6 -7
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +72 -119
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +36 -14
- package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +44 -8
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
|
@@ -23,7 +23,7 @@ import { readTaskIntent } from "../kernel/intent";
|
|
|
23
23
|
import { localRunId, readAuditTaskPair, readTaskRecordRaw, readWorkspaceStateRaw, reconcileKernelAuthority } from "../kernel/storage";
|
|
24
24
|
import { pathMatchesScope } from "../workspace_scope";
|
|
25
25
|
import { captureBatchReconfirmation, retainReconfirmation, ownUnpersistedBatchHead, type ReconfirmationSnapshot } from "./batch_reconfirmation";
|
|
26
|
-
import {
|
|
26
|
+
import { projectBatchPlan } from "./batch_plan";
|
|
27
27
|
import { batchReason, type BatchReasonKey } from "./batch_reasons";
|
|
28
28
|
import { isTerminalBatchState, readBatchRunState, type BatchRunStateRecord } from "./batch_state";
|
|
29
29
|
import { readRunRowByTask, withKernelRead } from "../kernel/sqlite_store";
|
|
@@ -98,6 +98,29 @@ export function readActiveClaimTaskId(root: string): string | null {
|
|
|
98
98
|
return workspace.state.current_working || (claim?.lifecycle_status === "active" ? claim.task_id : null);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The initiative whose resumable batch holds `taskId` as an in-flight child, or
|
|
103
|
+
* null. Read-only; a Host uses it to re-enter the batch after a foreground
|
|
104
|
+
* child settles, and anything unreadable simply means there is nothing to continue.
|
|
105
|
+
*/
|
|
106
|
+
export function findResumableBatchSlugForTask(root: string, taskId: string): string | null {
|
|
107
|
+
const batchesDir = join(root, ".imm", "state", "batches");
|
|
108
|
+
if (!existsSync(batchesDir)) return null;
|
|
109
|
+
for (const file of readdirSync(batchesDir)) {
|
|
110
|
+
if (!file.endsWith(".json")) continue;
|
|
111
|
+
try {
|
|
112
|
+
const record = JSON.parse(readFileSync(join(batchesDir, file), "utf8")) as BatchRunStateRecord;
|
|
113
|
+
if (record?.contract !== "assurance_kernel/batch_run_state/v1") continue;
|
|
114
|
+
if (record.batch_state !== "running" && record.batch_state !== "needs_human") continue;
|
|
115
|
+
if (record.children.some((child) => child.task_id === taskId && (child.state === "enrolled" || child.state === "settled")))
|
|
116
|
+
return record.initiative_slug;
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
/**
|
|
102
125
|
* The batch record for this initiative, or a corruption marker. A terminal
|
|
103
126
|
* record (completed, budget_stopped, failed, rejected) is not active: returning
|
|
@@ -182,10 +205,78 @@ export function findSettledBatchRecord(root: string, initiativeSlug: string): Ba
|
|
|
182
205
|
return newest;
|
|
183
206
|
}
|
|
184
207
|
|
|
208
|
+
export type BatchLineage =
|
|
209
|
+
| { kind: "equal"; head: string }
|
|
210
|
+
| { kind: "fast_forward"; head: string }
|
|
211
|
+
| { kind: "broken"; message: string };
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Classify live HEAD against the batch's recorded head. `fast_forward` is HEAD
|
|
215
|
+
* on the same batch branch with the recorded head as an ancestor and every
|
|
216
|
+
* recorded child commit still reachable: the user's own commit, adopted rather
|
|
217
|
+
* than refused. Anything else is the fatal `batch_head_lineage_broken`.
|
|
218
|
+
*/
|
|
219
|
+
export function classifyBatchLineage(input: {
|
|
220
|
+
root: string;
|
|
221
|
+
branch: string;
|
|
222
|
+
expectedHead: string;
|
|
223
|
+
childCommits: readonly string[];
|
|
224
|
+
/** When set, a fast-forward carrying this batch's own trailer is refused. */
|
|
225
|
+
batchId?: string;
|
|
226
|
+
}): BatchLineage {
|
|
227
|
+
const { root, branch, expectedHead, childCommits } = input;
|
|
228
|
+
const run = (args: string[]) =>
|
|
229
|
+
spawnSync("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
230
|
+
const broken = (reason: string): BatchLineage => ({
|
|
231
|
+
kind: "broken",
|
|
232
|
+
message: `batch_head_lineage_broken: ${reason}`,
|
|
233
|
+
});
|
|
234
|
+
const branchResult = run(["symbolic-ref", "--short", "HEAD"]);
|
|
235
|
+
const currentBranch = branchResult.stdout.trim();
|
|
236
|
+
if (branchResult.status !== 0 || currentBranch !== branch)
|
|
237
|
+
return broken(`current branch ${currentBranch} does not match expected branch ${branch}`);
|
|
238
|
+
const headResult = run(["rev-parse", "HEAD"]);
|
|
239
|
+
const head = headResult.stdout.trim();
|
|
240
|
+
if (headResult.status !== 0 || !head) return broken("current HEAD is unreadable");
|
|
241
|
+
if (head !== expectedHead && run(["merge-base", "--is-ancestor", expectedHead, head]).status !== 0)
|
|
242
|
+
return broken(`current HEAD ${head} does not descend from expected batch head ${expectedHead}`);
|
|
243
|
+
for (const commit of childCommits) {
|
|
244
|
+
if (run(["merge-base", "--is-ancestor", commit, head]).status !== 0)
|
|
245
|
+
return broken(`recorded commit ${commit} is no longer reachable from HEAD`);
|
|
246
|
+
}
|
|
247
|
+
if (head === expectedHead) return { kind: "equal", head };
|
|
248
|
+
// Only the runner writes this batch's trailer, and every commit it recorded
|
|
249
|
+
// is already in childCommits: a fast-forward claiming it is an impersonation
|
|
250
|
+
// (or an unverified own commit), never the user's own work.
|
|
251
|
+
if (input.batchId) {
|
|
252
|
+
const claimed = run([
|
|
253
|
+
"log",
|
|
254
|
+
"--fixed-strings",
|
|
255
|
+
`--grep=Immune-Brain-Batch: ${input.batchId}`,
|
|
256
|
+
"--format=%H",
|
|
257
|
+
`${expectedHead}..${head}`,
|
|
258
|
+
]);
|
|
259
|
+
const claimant = claimed.stdout.split(/\s+/).find(Boolean);
|
|
260
|
+
if (claimed.status !== 0 || claimant)
|
|
261
|
+
return broken(`commit ${claimant ?? head} claims batch ${input.batchId} without recorded evidence`);
|
|
262
|
+
}
|
|
263
|
+
return { kind: "fast_forward", head };
|
|
264
|
+
}
|
|
265
|
+
|
|
185
266
|
/** The HEAD a resumable batch must still sit on: its last child commit, or its base. */
|
|
186
|
-
export function expectedBatchHead(record: {
|
|
267
|
+
export function expectedBatchHead(record: {
|
|
268
|
+
base_head: string;
|
|
269
|
+
commits?: unknown;
|
|
270
|
+
adopted_heads?: ReadonlyArray<{ from: string; to: string }>;
|
|
271
|
+
}): string {
|
|
187
272
|
const commits = Array.isArray(record.commits) ? (record.commits as string[]) : [];
|
|
188
|
-
|
|
273
|
+
let head = commits.length > 0 ? commits[commits.length - 1]! : record.base_head;
|
|
274
|
+
// An adoption is keyed by the head it moved from, so one recorded before the
|
|
275
|
+
// last child commit can never resurface as the expected head.
|
|
276
|
+
for (const adoption of record.adopted_heads ?? []) {
|
|
277
|
+
if (adoption.from === head) head = adoption.to;
|
|
278
|
+
}
|
|
279
|
+
return head;
|
|
189
280
|
}
|
|
190
281
|
|
|
191
282
|
/**
|
|
@@ -234,9 +325,9 @@ export function isOwnBatchClaim(
|
|
|
234
325
|
return false;
|
|
235
326
|
}
|
|
236
327
|
if (!rec) return false;
|
|
237
|
-
const lineageHeads = [existingBatch.base_head]
|
|
238
|
-
Array.isArray(existingBatch.commits) ? existingBatch.commits : []
|
|
239
|
-
|
|
328
|
+
const lineageHeads = [existingBatch.base_head]
|
|
329
|
+
.concat(Array.isArray(existingBatch.commits) ? existingBatch.commits : [])
|
|
330
|
+
.concat((existingBatch.adopted_heads ?? []).map((adoption) => adoption.to));
|
|
240
331
|
if (!lineageHeads.includes(rec.git_base_head)) return false;
|
|
241
332
|
if (claim.enrollment_event_id !== `enroll-${taskId}-${claim.created_at}`) return false;
|
|
242
333
|
const createdAt = Date.parse(claim.created_at);
|
|
@@ -347,11 +438,11 @@ async function projectPlanSurface(input: {
|
|
|
347
438
|
let budget: BatchPlanBudget;
|
|
348
439
|
|
|
349
440
|
if (isResuming && existingBatch) {
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
441
|
+
// A resume keeps the confirmed count limits of the persisted record.
|
|
442
|
+
budget = {
|
|
443
|
+
max_children: existingBatch.budget.max_children,
|
|
444
|
+
qa_failure_limit: existingBatch.budget.qa_failure_limit,
|
|
445
|
+
};
|
|
355
446
|
try {
|
|
356
447
|
recoveryChildren = existingBatch.children.map((c) => {
|
|
357
448
|
const intentPath = `docs/plans/${c.task_id}.intent.json`;
|
|
@@ -635,7 +726,6 @@ export interface BatchConfirmationFacts {
|
|
|
635
726
|
batch_branch: string;
|
|
636
727
|
plan_digest: string;
|
|
637
728
|
budget: BatchPlanBudget;
|
|
638
|
-
expires_at: string;
|
|
639
729
|
reuse_blockers: string[];
|
|
640
730
|
children: Array<{ task_id: string; slice_id: string; risk: string; status: BatchPlanChildStatus }>;
|
|
641
731
|
excluded: Array<{ task_id: string; slice_id: string; reason: string }>;
|
|
@@ -656,7 +746,6 @@ export type BatchAuthorizationOutcome<HostRejection> =
|
|
|
656
746
|
batch_id: string;
|
|
657
747
|
reuse_authorization: boolean;
|
|
658
748
|
reuse_blockers: string[];
|
|
659
|
-
expires_at: string;
|
|
660
749
|
binding: BatchAuthorizationBinding;
|
|
661
750
|
}
|
|
662
751
|
| { outcome: "rejected"; rejection: BatchPreflightRejection }
|
|
@@ -678,7 +767,7 @@ export interface BatchAuthorizationOptions<HostRejection> {
|
|
|
678
767
|
|
|
679
768
|
/**
|
|
680
769
|
* The one authorization flow both Host adapters call after the shared
|
|
681
|
-
* preflight: ADR-0005's reuse
|
|
770
|
+
* preflight: ADR-0005's reuse decision, the literal-user gate, the
|
|
682
771
|
* post-gate claim/drift cascade, and the BatchAuthorizationBinding. A Host
|
|
683
772
|
* supplies its gate, its confirmation reference, and its nonce; every decision
|
|
684
773
|
* below stays Host-independent.
|
|
@@ -696,10 +785,6 @@ export async function authorizeBatch<HostRejection>(
|
|
|
696
785
|
} = projection;
|
|
697
786
|
const existingBatch = projection.existing_batch;
|
|
698
787
|
|
|
699
|
-
// ADR-0005 Decision 1: one Batch Authorization spans the work it authorizes,
|
|
700
|
-
// so its expiry is the deadline the literal user confirmed rather than a fixed
|
|
701
|
-
// window that lapses while a child is parked on a foreground Review.
|
|
702
|
-
const isExistingExpired = isResuming && Date.parse(existingBatch!.authorization_expires_at) <= Date.now();
|
|
703
788
|
const branchBefore = spawnSync("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
704
789
|
if (branchBefore.status !== 0)
|
|
705
790
|
return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
|
|
@@ -709,20 +794,29 @@ export async function authorizeBatch<HostRejection>(
|
|
|
709
794
|
// binds falls through to the gate below, which names the reason.
|
|
710
795
|
const ownsUnpersistedHead = isResuming && existingBatch !== null && existingBatch.plan_digest === planDigest &&
|
|
711
796
|
expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
|
|
797
|
+
// A fast-forward on the batch branch is the user's own work: the runner adopts
|
|
798
|
+
// it, so it neither blocks reuse nor counts as moved provenance.
|
|
799
|
+
const fastForwardsRecordedHead = isResuming && existingBatch !== null && existingBatch.branch === batchBranch &&
|
|
800
|
+
expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead &&
|
|
801
|
+
classifyBatchLineage({
|
|
802
|
+
root,
|
|
803
|
+
branch: batchBranch,
|
|
804
|
+
expectedHead: expectedBatchHead(existingBatch),
|
|
805
|
+
childCommits: existingBatch.commits,
|
|
806
|
+
batchId: existingBatch.batch_id,
|
|
807
|
+
}).kind === "fast_forward";
|
|
712
808
|
if (isResuming && existingBatch && !existingBatch.commits.length && existingBatch.children[0]?.state === "settled" &&
|
|
713
|
-
expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead)
|
|
809
|
+
expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && !fastForwardsRecordedHead)
|
|
714
810
|
return { outcome: "rejected", rejection: batchRejection("head_moved", "first unpersisted commit provenance is invalid") };
|
|
715
811
|
const reuseBlockers: string[] = [];
|
|
716
812
|
if (isResuming && existingBatch) {
|
|
717
|
-
if (isExistingExpired) reuseBlockers.push("batch_authorization_expired");
|
|
718
|
-
if (Date.parse(existingBatch.budget.deadline_at) <= Date.now()) reuseBlockers.push("batch_budget_expired");
|
|
719
813
|
if (existingBatch.batch_state !== "running") reuseBlockers.push("batch_not_running");
|
|
720
814
|
if (existingBatch.plan_digest !== planDigest) reuseBlockers.push("batch_plan_digest_changed");
|
|
721
815
|
if (existingBatch.branch !== batchBranch) reuseBlockers.push("batch_branch_changed");
|
|
722
|
-
if (expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead
|
|
816
|
+
if (expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && !fastForwardsRecordedHead)
|
|
817
|
+
reuseBlockers.push("batch_head_lineage_moved");
|
|
723
818
|
}
|
|
724
819
|
const reuseAuthorization = isResuming && reuseBlockers.length === 0;
|
|
725
|
-
const expiresAt = reuseAuthorization ? existingBatch!.authorization_expires_at : budget.deadline_at;
|
|
726
820
|
|
|
727
821
|
const batchId = isResuming && existingBatch ? existingBatch.batch_id : `batch-${initiativeSlug}-${randomUUID()}`;
|
|
728
822
|
const facts: BatchConfirmationFacts = {
|
|
@@ -730,7 +824,6 @@ export async function authorizeBatch<HostRejection>(
|
|
|
730
824
|
batch_branch: batchBranch,
|
|
731
825
|
plan_digest: planDigest,
|
|
732
826
|
budget,
|
|
733
|
-
expires_at: expiresAt,
|
|
734
827
|
reuse_blockers: reuseBlockers,
|
|
735
828
|
children: projection.recovery_children.map((child) => ({
|
|
736
829
|
task_id: child.task_id,
|
|
@@ -777,8 +870,8 @@ export async function authorizeBatch<HostRejection>(
|
|
|
777
870
|
if (finalClaimTaskId && !finalOwnClaim)
|
|
778
871
|
return { outcome: "rejected", rejection: batchRejection("claim_appeared_during_confirmation", finalClaimTaskId) };
|
|
779
872
|
|
|
780
|
-
// Confirmation cannot adopt a changed record
|
|
781
|
-
//
|
|
873
|
+
// Confirmation cannot adopt a changed record after the user answers. Re-read
|
|
874
|
+
// the record and branch after the async checks.
|
|
782
875
|
if (isResuming) {
|
|
783
876
|
const current = findExistingActiveBatch(root, initiativeSlug);
|
|
784
877
|
if (!current || current.corrupt || JSON.stringify(current.record) !== JSON.stringify(existingBatch))
|
|
@@ -787,8 +880,6 @@ export async function authorizeBatch<HostRejection>(
|
|
|
787
880
|
const branchAfter = spawnSync("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
788
881
|
if (branchAfter.status !== 0 || branchAfter.stdout !== branchBefore.stdout)
|
|
789
882
|
return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "Git branch moved after native confirmation") };
|
|
790
|
-
if (Date.parse(expiresAt) <= Date.now())
|
|
791
|
-
return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "confirmed batch deadline expired before authorization issuance") };
|
|
792
883
|
|
|
793
884
|
if (projection.reconfirmation) {
|
|
794
885
|
try { projection.reconfirmation.assertUnchanged(); }
|
|
@@ -803,16 +894,14 @@ export async function authorizeBatch<HostRejection>(
|
|
|
803
894
|
budget,
|
|
804
895
|
actor_id: LITERAL_USER_ACTOR_ID,
|
|
805
896
|
confirmation_ref: options.confirmationRef({ batch_id: batchId, request_id: requestId }),
|
|
806
|
-
expires_at: expiresAt,
|
|
807
897
|
nonce: options.nonce,
|
|
808
898
|
};
|
|
809
|
-
if (projection.reconfirmation) retainReconfirmation(binding.nonce,
|
|
899
|
+
if (projection.reconfirmation) retainReconfirmation(binding.nonce, projection.reconfirmation);
|
|
810
900
|
return {
|
|
811
901
|
outcome: "authorized",
|
|
812
902
|
batch_id: batchId,
|
|
813
903
|
reuse_authorization: reuseAuthorization,
|
|
814
904
|
reuse_blockers: reuseBlockers,
|
|
815
|
-
expires_at: expiresAt,
|
|
816
905
|
binding,
|
|
817
906
|
};
|
|
818
907
|
}
|
|
@@ -24,7 +24,6 @@ export type BatchReasonKey =
|
|
|
24
24
|
| "empty_enrollable_set"
|
|
25
25
|
| "plan_projection_failed"
|
|
26
26
|
| "confirmation_port_unavailable"
|
|
27
|
-
| "confirmation_timed_out"
|
|
28
27
|
| "confirmation_cancelled"
|
|
29
28
|
| "confirmation_declined"
|
|
30
29
|
| "confirmation_no_decision"
|
|
@@ -115,11 +114,6 @@ export const BATCH_REASONS: Readonly<Record<BatchReasonKey, BatchReasonSpec>> =
|
|
|
115
114
|
reason: "native confirmation port is unavailable",
|
|
116
115
|
recovery_action: "retry through a fresh native gate in the current Host",
|
|
117
116
|
},
|
|
118
|
-
confirmation_timed_out: {
|
|
119
|
-
state: "rejected",
|
|
120
|
-
reason: "native confirmation timed out waiting for user interaction",
|
|
121
|
-
recovery_action: "retry through a fresh native gate in the current Host",
|
|
122
|
-
},
|
|
123
117
|
confirmation_cancelled: {
|
|
124
118
|
state: "cancelled",
|
|
125
119
|
reason: "native interaction cancelled",
|
|
@@ -67,17 +67,16 @@ export interface ReconfirmationSnapshot {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Ephemeral captured observations, not authority: the runner independently
|
|
70
|
-
// validates the native capability. Entries
|
|
71
|
-
const confirmations = new Map<string,
|
|
72
|
-
export function retainReconfirmation(nonce: string,
|
|
73
|
-
for (const [key, value] of confirmations) if (value.expires <= Date.now()) confirmations.delete(key);
|
|
70
|
+
// validates the native capability. Entries are discarded on application.
|
|
71
|
+
const confirmations = new Map<string, ReconfirmationSnapshot>();
|
|
72
|
+
export function retainReconfirmation(nonce: string, snapshot: ReconfirmationSnapshot): void {
|
|
74
73
|
if (confirmations.has(nonce)) refuse();
|
|
75
|
-
confirmations.set(nonce,
|
|
74
|
+
confirmations.set(nonce, snapshot);
|
|
76
75
|
}
|
|
77
76
|
export function takeReconfirmation(nonce: string): ReconfirmationSnapshot {
|
|
78
77
|
const found = confirmations.get(nonce); confirmations.delete(nonce);
|
|
79
|
-
if (!found
|
|
80
|
-
return found
|
|
78
|
+
if (!found) refuse();
|
|
79
|
+
return found;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
export async function captureBatchReconfirmation(root: string, record: BatchRunStateRecord, children: BatchPlanChild[]): Promise<ReconfirmationSnapshot> {
|