ai-foreman 1.7.13 → 1.7.14
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 +33 -3
- package/dist/adapters/claude.d.ts +5 -0
- package/dist/adapters/claude.js +122 -16
- package/dist/adapters/codex.d.ts +2 -0
- package/dist/adapters/codex.js +30 -8
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +6 -0
- package/dist/adapters/types.d.ts +22 -0
- package/dist/agentRun.js +21 -3
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +9 -0
- package/dist/branch/finalization.d.ts +22 -0
- package/dist/branch/finalization.js +120 -0
- package/dist/branch/git.js +7 -2
- package/dist/branch/runner.d.ts +3 -2
- package/dist/branch/runner.js +277 -75
- package/dist/buildRuns.d.ts +1 -0
- package/dist/buildRuns.js +33 -3
- package/dist/cli/start.js +314 -86
- package/dist/cli/state.d.ts +4 -0
- package/dist/cli/state.js +69 -0
- package/dist/continuity.d.ts +4 -0
- package/dist/continuity.js +26 -3
- package/dist/foreman.d.ts +22 -4
- package/dist/foreman.js +105 -19
- package/dist/handoffs.d.ts +5 -2
- package/dist/handoffs.js +93 -12
- package/dist/index.js +2 -0
- package/dist/qaFailureDelivery.d.ts +100 -0
- package/dist/qaFailureDelivery.js +570 -0
- package/dist/qaProtocolV2.d.ts +233 -0
- package/dist/qaProtocolV2.js +114 -0
- package/dist/qaRecovery.d.ts +15 -3
- package/dist/qaRecovery.js +477 -83
- package/dist/qaReview.d.ts +42 -17
- package/dist/qaReview.js +1216 -457
- package/dist/qaRuntime.d.ts +14 -0
- package/dist/qaRuntime.js +44 -0
- package/dist/qaSnapshot.d.ts +34 -0
- package/dist/qaSnapshot.js +302 -34
- package/dist/stateTransfer.d.ts +57 -0
- package/dist/stateTransfer.js +644 -0
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +6 -0
- package/dist/workflowDb.d.ts +245 -2
- package/dist/workflowDb.js +647 -6
- package/package.json +15 -3
package/dist/handoffs.js
CHANGED
|
@@ -9,6 +9,34 @@ export const HANDOFF_CACHE_RETENTION_DAYS = 30;
|
|
|
9
9
|
export const HANDOFF_ACCEPTED = "HANDOFF_ACCEPTED";
|
|
10
10
|
export const HANDOFF_REQUEST_START = "RAFI_HANDOFF_REQUEST_START";
|
|
11
11
|
export const HANDOFF_REQUEST_END = "RAFI_HANDOFF_REQUEST_END";
|
|
12
|
+
function handoffSessionIdentity(ref) {
|
|
13
|
+
return JSON.stringify([ref.version, ref.provider, ref.sessionId, ref.role, ref.stream, ref.generation, resolve(ref.cwd), resolve(ref.configRoot), ref.workspaceIdentity, ref.ticketId, ref.deliveryUnitId]);
|
|
14
|
+
}
|
|
15
|
+
async function collectHandoffTerminalEvents(adapter, turnId) {
|
|
16
|
+
const events = [];
|
|
17
|
+
let timer;
|
|
18
|
+
const collect = (async () => {
|
|
19
|
+
for await (const event of adapter.events()) {
|
|
20
|
+
events.push(event);
|
|
21
|
+
if (event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === turnId)
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
try {
|
|
26
|
+
await Promise.race([collect, new Promise((resolve) => { timer = setTimeout(resolve, 1000); })]);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
events.push({ kind: "error", message: `handoff event stream failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
if (timer)
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
// A missing terminal event makes acceptance fail below and closes the
|
|
36
|
+
// successor, ending any pending iterator read before it can perform work.
|
|
37
|
+
void collect.catch(() => { });
|
|
38
|
+
return events;
|
|
39
|
+
}
|
|
12
40
|
export function parseBuilderHandoffRequest(text) {
|
|
13
41
|
const starts = [...text.matchAll(/RAFI_HANDOFF_REQUEST_START/g)];
|
|
14
42
|
const ends = [...text.matchAll(/RAFI_HANDOFF_REQUEST_END/g)];
|
|
@@ -113,11 +141,13 @@ export class HandoffService {
|
|
|
113
141
|
label: resource.label,
|
|
114
142
|
digest: resource.digest && /^[a-f0-9]{64}$/.test(resource.digest) ? resource.digest : digest(resource.content ?? ""),
|
|
115
143
|
authoritative: resource.authoritative,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
requiredForRecovery: resource.requiredForRecovery ?? false,
|
|
145
|
+
mediaType: resource.mediaType ?? "application/octet-stream",
|
|
146
|
+
path: resource.path ?? `embedded:${resource.label}`,
|
|
147
|
+
purpose: resource.purpose ?? `Declared ${input.role} handoff resource: ${resource.label}`,
|
|
148
|
+
bytes: resource.bytes ?? (resource.content !== undefined ? Buffer.byteLength(resource.content) : 0),
|
|
119
149
|
}));
|
|
120
|
-
resources.unshift({ label: "continuity-checkpoint", digest: head.digest, authoritative: true }, { label: "authoritative-run-state", digest: runHead.digest, authoritative: true });
|
|
150
|
+
resources.unshift({ label: "continuity-checkpoint", digest: head.digest, authoritative: true, requiredForRecovery: false, mediaType: "application/vnd.rafi.digest", path: "db:continuity-checkpoint", purpose: "Digest reference to the durable role continuity checkpoint", bytes: Buffer.byteLength(head.digest) }, { label: "authoritative-run-state", digest: runHead.digest, authoritative: true, requiredForRecovery: false, mediaType: "application/vnd.rafi.digest", path: "db:authoritative-run-state", purpose: "Digest reference to the durable run continuity state", bytes: Buffer.byteLength(runHead.digest) });
|
|
121
151
|
const manifest = {
|
|
122
152
|
version: 1,
|
|
123
153
|
runId: input.runId,
|
|
@@ -212,6 +242,47 @@ export class HandoffService {
|
|
|
212
242
|
async acceptStaged(staged, successor, options = {}) {
|
|
213
243
|
const db = new WorkflowDb(this.projectDir);
|
|
214
244
|
try {
|
|
245
|
+
const qaBoundary = staged.manifest.role === "qa";
|
|
246
|
+
const preparedRef = successor.sessionRef?.();
|
|
247
|
+
if (qaBoundary && (!preparedRef || preparedRef.version !== 1 || preparedRef.source !== "observed"
|
|
248
|
+
|| preparedRef.role !== "qa" || preparedRef.stream !== "qa" || preparedRef.provider !== successor.agent
|
|
249
|
+
|| !preparedRef.sessionId.trim() || /^(?:unknown|unavailable)$/i.test(preparedRef.sessionId.trim())
|
|
250
|
+
|| !preparedRef.cwd || !preparedRef.configRoot || !Number.isSafeInteger(preparedRef.generation) || preparedRef.generation < 0
|
|
251
|
+
|| !preparedRef.validatedAt || Number.isNaN(Date.parse(preparedRef.validatedAt)) || Number.isNaN(Date.parse(preparedRef.createdAt))
|
|
252
|
+
|| preparedRef.sessionId !== successor.sessionId())) {
|
|
253
|
+
throw new HandoffAcceptanceError("missing-scoped-successor-session", staged.manifest.runId, staged.manifest.generation, "QA acceptance requires a prepared, validated scoped successor");
|
|
254
|
+
}
|
|
255
|
+
const acceptanceAttempts = [];
|
|
256
|
+
const sendAcceptance = async (prompt) => {
|
|
257
|
+
const hostPromptDigest = db.putEvidence("handoff", Buffer.from(prompt));
|
|
258
|
+
db.appendContinuityEvent({ runId: staged.manifest.runId, role: "host", kind: "handoff_acceptance_intended", payload: { generation: staged.manifest.generation, attempt: acceptanceAttempts.length + 1, hostPromptDigest, preparedRef }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0 });
|
|
259
|
+
const response = await successor.sendTurn(prompt);
|
|
260
|
+
const events = qaBoundary && response.turnId ? await collectHandoffTerminalEvents(successor, response.turnId) : [];
|
|
261
|
+
const attempt = {
|
|
262
|
+
hostPromptDigest,
|
|
263
|
+
providerPromptDigest: db.putEvidence("handoff", Buffer.from(response.providerInstruction ?? response.hostInstruction ?? prompt)),
|
|
264
|
+
rawResponseDigest: db.putEvidence("handoff", Buffer.from(response.rawResponse ?? response.text)),
|
|
265
|
+
cleanedResponseDigest: db.putEvidence("handoff", Buffer.from(response.cleanedResponse ?? response.text)),
|
|
266
|
+
...(response.turnId ? { providerTurnId: response.turnId } : {}),
|
|
267
|
+
...(qaBoundary ? { eventStreamDigest: db.putEvidence("handoff", Buffer.from(JSON.stringify(events))) } : {}),
|
|
268
|
+
};
|
|
269
|
+
acceptanceAttempts.push(attempt);
|
|
270
|
+
db.appendContinuityEvent({ runId: staged.manifest.runId, role: "host", kind: "handoff_acceptance_observed", payload: { generation: staged.manifest.generation, attempt, isError: response.isError, failure: response.failure, providerMetadata: response.providerMetadata, activeSession: successor.sessionRef?.() }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0 });
|
|
271
|
+
if (response.isError || response.failure)
|
|
272
|
+
throw new HandoffAcceptanceError("provider-turn-failed", staged.manifest.runId, staged.manifest.generation, "provider rejected or failed the acknowledgement turn");
|
|
273
|
+
const activeRef = successor.sessionRef?.();
|
|
274
|
+
const reportedRef = response.providerMetadata?.sessionRef;
|
|
275
|
+
if ((preparedRef && (!activeRef || handoffSessionIdentity(activeRef) !== handoffSessionIdentity(preparedRef)))
|
|
276
|
+
|| (qaBoundary && (!response.providerMetadata || !reportedRef || response.providerMetadata.provider !== preparedRef.provider
|
|
277
|
+
|| response.providerMetadata.sessionId !== preparedRef.sessionId || successor.sessionId() !== preparedRef.sessionId
|
|
278
|
+
|| successor.agent !== preparedRef.provider || handoffSessionIdentity(reportedRef) !== handoffSessionIdentity(preparedRef)))) {
|
|
279
|
+
throw new HandoffAcceptanceError("successor-identity-mismatch", staged.manifest.runId, staged.manifest.generation, "acknowledgement did not come from the prepared successor identity");
|
|
280
|
+
}
|
|
281
|
+
if (qaBoundary && (!response.turnId || !events.some((event) => event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === response.turnId))) {
|
|
282
|
+
throw new HandoffAcceptanceError("missing-terminal-event", staged.manifest.runId, staged.manifest.generation, "QA acknowledgement has no correlated terminal provider event");
|
|
283
|
+
}
|
|
284
|
+
return response;
|
|
285
|
+
};
|
|
215
286
|
const acceptancePrompt = [
|
|
216
287
|
"Accept this validated Rafi handoff. Do not repeat completed work and reconcile host receipts before side effects.",
|
|
217
288
|
staged.markdown,
|
|
@@ -219,14 +290,15 @@ export class HandoffService {
|
|
|
219
290
|
...(options.guidance ? [`Human recovery guidance: ${options.guidance}`] : []),
|
|
220
291
|
`Reply with ${HANDOFF_ACCEPTED} on the first line, then ${continuityInstruction()}`,
|
|
221
292
|
].join("\n\n");
|
|
222
|
-
let response = await
|
|
293
|
+
let response = await sendAcceptance(acceptancePrompt);
|
|
223
294
|
let validation = validateHandoffAcceptance(staged, successor, response.text);
|
|
224
295
|
if (validation && (validation.code === "missing-acknowledgement" || validation.code === "invalid-continuity-delta")) {
|
|
225
|
-
|
|
296
|
+
const repairPrompt = [
|
|
226
297
|
`Your handoff acknowledgement was rejected: ${validation.message}.`,
|
|
227
298
|
"Correction only: do not use tools, repeat completed work, or perform implementation.",
|
|
228
299
|
`Reply with ${HANDOFF_ACCEPTED} on the first line, then ${continuityInstruction()}`,
|
|
229
|
-
].join("\n\n")
|
|
300
|
+
].join("\n\n");
|
|
301
|
+
response = await sendAcceptance(repairPrompt);
|
|
230
302
|
validation = validateHandoffAcceptance(staged, successor, response.text);
|
|
231
303
|
}
|
|
232
304
|
if (validation)
|
|
@@ -237,12 +309,20 @@ export class HandoffService {
|
|
|
237
309
|
const successorRef = observedSuccessorRef ? { ...observedSuccessorRef, generation: staged.manifest.generation, validatedAt: new Date().toISOString() } : undefined;
|
|
238
310
|
if (!parsed.delta || !sessionId)
|
|
239
311
|
throw new Error("validated handoff acceptance lost its parsed continuity state");
|
|
312
|
+
if (staged.manifest.resources.some((resource) => resource.requiredForRecovery)
|
|
313
|
+
&& (!successorRef || staged.manifest.resources.some((resource) => resource.requiredForRecovery && (!resource.purpose || !Number.isSafeInteger(resource.bytes) || resource.bytes < 0)))) {
|
|
314
|
+
throw new HandoffAcceptanceError("missing-scoped-successor-session", staged.manifest.runId, staged.manifest.generation, "recovery handoffs require a scoped successor and complete resource purpose/byte metadata");
|
|
315
|
+
}
|
|
240
316
|
if (successorRef)
|
|
241
317
|
successor.adoptSessionRef?.(successorRef);
|
|
242
|
-
db.appendContinuityEvent({ runId: staged.manifest.runId, role: staged.manifest.role, kind: "handoff_successor_accepted", payload: { generation: staged.manifest.generation, sessionId, sessionRef: successorRef, delta: parsed.delta }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef: successorRef });
|
|
318
|
+
db.appendContinuityEvent({ runId: staged.manifest.runId, role: staged.manifest.role, kind: "handoff_successor_accepted", payload: { generation: staged.manifest.generation, sessionId, sessionRef: successorRef, delta: parsed.delta, acceptanceAttempts }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef: successorRef });
|
|
243
319
|
const checkpoint = db.publishContinuityCheckpoint({ runId: staged.manifest.runId, role: staged.manifest.role, delta: parsed.delta, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef: successorRef });
|
|
244
|
-
const
|
|
245
|
-
|
|
320
|
+
const receipt = successorRef ? { version: 1, runId: staged.manifest.runId, generation: staged.manifest.generation, role: staged.manifest.role,
|
|
321
|
+
manifestDigest: staged.lineage.manifestDigest, continuityCheckpointDigest: staged.manifest.continuityCheckpointDigest, acceptanceCheckpointDigest: checkpoint.digest,
|
|
322
|
+
...(staged.manifest.predecessorSessionRef ? { predecessorSessionRef: staged.manifest.predecessorSessionRef } : {}), successorSessionRef: successorRef,
|
|
323
|
+
resources: staged.manifest.resources, acceptedAt: new Date().toISOString() } : undefined;
|
|
324
|
+
const lineage = db.acceptHandoff(staged.manifest.runId, staged.manifest.generation, successorRef ?? sessionId, undefined, receipt);
|
|
325
|
+
return { ...staged, lineage, successor, successorSessionId: sessionId, acceptanceCheckpointDigest: checkpoint.digest, ...(receipt ? { acceptanceReceipt: receipt } : {}) };
|
|
246
326
|
}
|
|
247
327
|
catch (error) {
|
|
248
328
|
if (error instanceof HandoffAcceptanceError)
|
|
@@ -368,8 +448,9 @@ export class HandoffService {
|
|
|
368
448
|
}
|
|
369
449
|
}
|
|
370
450
|
function validateHandoffAcceptance(staged, successor, text) {
|
|
371
|
-
|
|
372
|
-
|
|
451
|
+
const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
452
|
+
if (firstLine !== HANDOFF_ACCEPTED) {
|
|
453
|
+
return { code: "missing-acknowledgement", message: `the first non-whitespace line must equal ${HANDOFF_ACCEPTED}` };
|
|
373
454
|
}
|
|
374
455
|
const parsed = parseContinuityDelta(text);
|
|
375
456
|
if (!parsed.delta) {
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildStartCommand } from "./cli/start.js";
|
|
|
6
6
|
import { buildStatusCommand } from "./cli/status.js";
|
|
7
7
|
import { buildDoctorCommand } from "./cli/doctor.js";
|
|
8
8
|
import { buildManagerCommand } from "./cli/manager.js";
|
|
9
|
+
import { buildStateCommand } from "./cli/state.js";
|
|
9
10
|
import { buildAttachCommand, buildDecideCommand, buildStopCommand } from "./cli/recovery.js";
|
|
10
11
|
import { withActivityContext } from "./activity.js";
|
|
11
12
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))?.version;
|
|
@@ -19,6 +20,7 @@ program.addCommand(buildStartCommand());
|
|
|
19
20
|
program.addCommand(buildStatusCommand());
|
|
20
21
|
program.addCommand(buildDoctorCommand());
|
|
21
22
|
program.addCommand(buildManagerCommand({ requireProject: true }));
|
|
23
|
+
program.addCommand(buildStateCommand());
|
|
22
24
|
program.addCommand(buildAttachCommand());
|
|
23
25
|
program.addCommand(buildDecideCommand());
|
|
24
26
|
program.addCommand(buildStopCommand());
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type ProviderSessionRefV1, type QaFailureReportV1, type QaFindingRefV2, type SessionStrategy } from "rafi-spec";
|
|
2
|
+
import type { BuilderAdapter, BuilderEvent } from "./adapters/types.js";
|
|
3
|
+
import type { TicketDef } from "./tickets/ticketSchema.js";
|
|
4
|
+
export interface QaFailureDeliveryInput {
|
|
5
|
+
projectDir: string;
|
|
6
|
+
runId: string;
|
|
7
|
+
ticket: TicketDef;
|
|
8
|
+
builderWorktree: string;
|
|
9
|
+
report: QaFailureReportV1;
|
|
10
|
+
reportDigest: string;
|
|
11
|
+
reviewAttemptId: string;
|
|
12
|
+
reviewNumber: number;
|
|
13
|
+
reviewedSourceStateDigest: string;
|
|
14
|
+
reviewBasisDigest: string;
|
|
15
|
+
remediationGeneration: number;
|
|
16
|
+
latestBuilderResult: string;
|
|
17
|
+
history: unknown[];
|
|
18
|
+
plannerRemediation?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface QaFailureDeliveryController {
|
|
21
|
+
adapter(): BuilderAdapter | undefined;
|
|
22
|
+
setAdapter?(adapter: BuilderAdapter): void;
|
|
23
|
+
sessionStrategy: SessionStrategy;
|
|
24
|
+
prepareBoundary?(adapter: BuilderAdapter, instruction: string, strategy: SessionStrategy, worktree: string): Promise<BuilderAdapter>;
|
|
25
|
+
beforeTurn?(adapter: BuilderAdapter, instruction: string, worktree: string): Promise<BuilderAdapter>;
|
|
26
|
+
recordSession?(session: string | ProviderSessionRefV1, ticketId: string, worktree: string): void;
|
|
27
|
+
events?: BuilderEvent[];
|
|
28
|
+
}
|
|
29
|
+
export interface QaFailureDeliveryResult {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
detail?: string;
|
|
32
|
+
response?: string;
|
|
33
|
+
summary?: string;
|
|
34
|
+
providerTurnId?: string;
|
|
35
|
+
handoffId?: string;
|
|
36
|
+
operationId?: string;
|
|
37
|
+
}
|
|
38
|
+
interface QaFailureHandoffV2 {
|
|
39
|
+
version: 2;
|
|
40
|
+
handoffId: string;
|
|
41
|
+
scope: {
|
|
42
|
+
runId: string;
|
|
43
|
+
ticketId: string;
|
|
44
|
+
reviewNumber: number;
|
|
45
|
+
reviewAttemptId: string;
|
|
46
|
+
remediationGeneration: number;
|
|
47
|
+
};
|
|
48
|
+
ticket: {
|
|
49
|
+
digest: string;
|
|
50
|
+
definition: TicketDef;
|
|
51
|
+
};
|
|
52
|
+
source: {
|
|
53
|
+
reviewedContentDigest: string;
|
|
54
|
+
reviewBasisDigest: string;
|
|
55
|
+
capturedAt: string;
|
|
56
|
+
worktreePath: string;
|
|
57
|
+
gitDir: string;
|
|
58
|
+
changeSummary: string;
|
|
59
|
+
affectedPaths: string[];
|
|
60
|
+
};
|
|
61
|
+
report: {
|
|
62
|
+
digest: string;
|
|
63
|
+
rawEvidenceDigest: string;
|
|
64
|
+
value: QaFailureReportV1;
|
|
65
|
+
findingRefs: QaFindingRefV2[];
|
|
66
|
+
};
|
|
67
|
+
latestBuilderResult: {
|
|
68
|
+
rawResponseDigest: string;
|
|
69
|
+
summaryDigest: string;
|
|
70
|
+
summary: string;
|
|
71
|
+
summaryTruncated: boolean;
|
|
72
|
+
originalByteCount: number;
|
|
73
|
+
};
|
|
74
|
+
history: unknown[];
|
|
75
|
+
plannerRemediation?: {
|
|
76
|
+
supplementId: string;
|
|
77
|
+
digest: string;
|
|
78
|
+
reviewedContentDigest: string;
|
|
79
|
+
summary: string;
|
|
80
|
+
fixInstructions: string[];
|
|
81
|
+
advisoryOnly: true;
|
|
82
|
+
};
|
|
83
|
+
responseContract: {
|
|
84
|
+
schemaDigest: string;
|
|
85
|
+
};
|
|
86
|
+
createdAt: string;
|
|
87
|
+
}
|
|
88
|
+
export declare class QaFailureDeliveryService {
|
|
89
|
+
deliver(input: QaFailureDeliveryInput, controller: QaFailureDeliveryController): Promise<QaFailureDeliveryResult>;
|
|
90
|
+
private buildHandoff;
|
|
91
|
+
private commitIntent;
|
|
92
|
+
private tryCorrectResponse;
|
|
93
|
+
private finishSuccess;
|
|
94
|
+
private finishInvalidOrBlocked;
|
|
95
|
+
private finishUncertain;
|
|
96
|
+
private transitionHandoff;
|
|
97
|
+
private supersedeForDrift;
|
|
98
|
+
}
|
|
99
|
+
export declare function renderQaFailureHandoff(handoff: QaFailureHandoffV2): string;
|
|
100
|
+
export {};
|