imprint-mcp 0.7.0 → 0.7.2
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 +66 -2
- package/package.json +1 -1
- package/prompts/auth-compile-agent.md +173 -22
- package/prompts/compile-agent.md +45 -8
- package/prompts/component-recovery.md +40 -0
- package/prompts/master-teach-decision.md +28 -3
- package/prompts/master-teach-focused-planner.md +18 -5
- package/src/cli.ts +26 -18
- package/src/imprint/agent-tool.ts +15 -0
- package/src/imprint/agent.ts +2 -11
- package/src/imprint/audit.ts +35 -13
- package/src/imprint/auth-browser-discovery.ts +276 -0
- package/src/imprint/auth-compile-agent.ts +513 -224
- package/src/imprint/auth-compile-tools.ts +213 -25
- package/src/imprint/auth-discovery-evidence.ts +81 -0
- package/src/imprint/auth-plan.ts +28 -0
- package/src/imprint/auth-verifier.ts +234 -20
- package/src/imprint/authentication-interaction.ts +137 -0
- package/src/imprint/backend-ladder.ts +271 -43
- package/src/imprint/build-plan.ts +2 -22
- package/src/imprint/cdp-browser-fetch.ts +242 -60
- package/src/imprint/chromium.ts +93 -29
- package/src/imprint/claude-cli-compile.ts +2 -0
- package/src/imprint/codex-cli-compile.ts +99 -46
- package/src/imprint/compile-agent-types.ts +9 -5
- package/src/imprint/compile-agent.ts +33 -0
- package/src/imprint/compile-tool-executor.ts +783 -0
- package/src/imprint/compile-tools.ts +42 -9
- package/src/imprint/compile-verification.ts +2 -0
- package/src/imprint/compile.ts +3 -0
- package/src/imprint/compiler-log.ts +16 -0
- package/src/imprint/component-recovery.ts +120 -0
- package/src/imprint/cookie-jar.ts +117 -7
- package/src/imprint/credential-extract.ts +9 -2
- package/src/imprint/credential-lock.ts +147 -0
- package/src/imprint/credential-observation.ts +261 -0
- package/src/imprint/credential-store.ts +284 -44
- package/src/imprint/live-component-recovery.ts +73 -0
- package/src/imprint/llm.ts +4 -3
- package/src/imprint/log.ts +16 -1
- package/src/imprint/login.ts +9 -0
- package/src/imprint/master-teach-agent-contracts.ts +40 -3
- package/src/imprint/master-teach-agents.ts +229 -14
- package/src/imprint/master-teach-checks.ts +8 -1
- package/src/imprint/master-teach-controller.ts +681 -138
- package/src/imprint/master-teach-prompt-projections.ts +2 -0
- package/src/imprint/mcp-cdp-sessions.ts +7 -2
- package/src/imprint/mcp-compile-server.ts +62 -850
- package/src/imprint/mcp-server.ts +667 -91
- package/src/imprint/mcp-session-recovery.ts +167 -0
- package/src/imprint/native-teach-agents.ts +232 -108
- package/src/imprint/runtime.ts +416 -47
- package/src/imprint/stealth-chromium.ts +67 -7
- package/src/imprint/teach-browser-resources.ts +88 -0
- package/src/imprint/teach-stage-progress.ts +38 -0
- package/src/imprint/terminal-user-prompt.ts +42 -0
- package/src/imprint/types.ts +51 -18
- package/src/imprint/user-interaction.ts +836 -0
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import { ensurePlaywrightChromiumInstalled } from './chromium.ts';
|
|
2
2
|
|
|
3
|
+
interface CompatCdpClient {
|
|
4
|
+
send(method: string, parameters?: unknown): Promise<unknown>;
|
|
5
|
+
}
|
|
6
|
+
interface CompatPage {
|
|
7
|
+
_client(): CompatCdpClient;
|
|
8
|
+
}
|
|
9
|
+
interface PageSetupPlugin {
|
|
10
|
+
onPageCreated(page: CompatPage): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Keep a plugin's fire-and-forget CDP calls inside its caught lifecycle hook. */
|
|
14
|
+
export function awaitPluginPageSetup<T extends PageSetupPlugin>(plugin: T): T {
|
|
15
|
+
const original = plugin.onPageCreated.bind(plugin);
|
|
16
|
+
plugin.onPageCreated = async (page) => {
|
|
17
|
+
const commands: Promise<unknown>[] = [];
|
|
18
|
+
const guardedPage = new Proxy(page, {
|
|
19
|
+
get(target, property, receiver) {
|
|
20
|
+
if (property !== '_client') return Reflect.get(target, property, receiver);
|
|
21
|
+
return () => {
|
|
22
|
+
const client = target._client();
|
|
23
|
+
return new Proxy(client, {
|
|
24
|
+
get(clientTarget, clientProperty, clientReceiver) {
|
|
25
|
+
if (clientProperty !== 'send')
|
|
26
|
+
return Reflect.get(clientTarget, clientProperty, clientReceiver);
|
|
27
|
+
return (method: string, parameters?: unknown) => {
|
|
28
|
+
const command = clientTarget.send(method, parameters);
|
|
29
|
+
// Observe immediately, even if the hook yields before it returns.
|
|
30
|
+
void command.catch(() => {});
|
|
31
|
+
commands.push(command);
|
|
32
|
+
return command;
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
await original(guardedPage);
|
|
41
|
+
} finally {
|
|
42
|
+
await Promise.all(commands);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return plugin;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let chromiumPromise: Promise<typeof import('playwright').chromium> | undefined;
|
|
49
|
+
let stealthAvailable = false;
|
|
50
|
+
|
|
3
51
|
/**
|
|
4
52
|
* Shared loader for Playwright's chromium with the stealth plugin applied.
|
|
5
53
|
*
|
|
@@ -18,13 +66,30 @@ import { ensurePlaywrightChromiumInstalled } from './chromium.ts';
|
|
|
18
66
|
* thrown error into their own result shape.
|
|
19
67
|
*/
|
|
20
68
|
export async function getStealthChromium(): Promise<typeof import('playwright').chromium> {
|
|
69
|
+
chromiumPromise ??= loadStealthChromium();
|
|
70
|
+
return await chromiumPromise;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function loadStealthChromium(): Promise<typeof import('playwright').chromium> {
|
|
21
74
|
try {
|
|
22
75
|
const pwExtra = await import('playwright-extra');
|
|
23
76
|
const stealthMod = await import('puppeteer-extra-plugin-stealth');
|
|
24
77
|
const stealthFactory =
|
|
25
78
|
(stealthMod as { default?: () => unknown }).default ??
|
|
26
79
|
(stealthMod as unknown as () => unknown);
|
|
80
|
+
// This upstream evasion does not await client.send. Playwright-extra catches
|
|
81
|
+
// rejected hook promises, so attach the command to that hook rather than
|
|
82
|
+
// allowing a browser-close race to become an unhandled rejection.
|
|
83
|
+
const userAgentModule = await import(
|
|
84
|
+
// @ts-expect-error The upstream evasion does not publish TypeScript declarations.
|
|
85
|
+
'puppeteer-extra-plugin-stealth/evasions/user-agent-override/index.js'
|
|
86
|
+
);
|
|
87
|
+
const userAgentFactory = userAgentModule.default as (options: unknown) => PageSetupPlugin;
|
|
88
|
+
pwExtra.chromium.plugins.setDependencyResolution('stealth/evasions/user-agent-override', ((
|
|
89
|
+
options: unknown,
|
|
90
|
+
) => awaitPluginPageSetup(userAgentFactory(options))) as never);
|
|
27
91
|
pwExtra.chromium.use(stealthFactory() as never);
|
|
92
|
+
stealthAvailable = true;
|
|
28
93
|
return pwExtra.chromium as unknown as typeof import('playwright').chromium;
|
|
29
94
|
} catch {
|
|
30
95
|
const pw = await import('playwright');
|
|
@@ -45,13 +110,8 @@ export async function getStealthChromium(): Promise<typeof import('playwright').
|
|
|
45
110
|
* protection. Import resolution is cached, so probing here is cheap.
|
|
46
111
|
*/
|
|
47
112
|
export async function isStealthPluginAvailable(): Promise<boolean> {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await import('puppeteer-extra-plugin-stealth');
|
|
51
|
-
return true;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
113
|
+
await getStealthChromium();
|
|
114
|
+
return stealthAvailable;
|
|
55
115
|
}
|
|
56
116
|
|
|
57
117
|
/**
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { CdpBrowserFetch } from './cdp-browser-fetch.ts';
|
|
2
|
+
import { abortSignalError, withAbortSignal } from './concurrency.ts';
|
|
3
|
+
|
|
4
|
+
let previousCheck: Promise<void> = Promise.resolve();
|
|
5
|
+
// The pass owns its pool and drains it on exit. A weak reference lets the host
|
|
6
|
+
// evict an idle pass without extending its lifetime or sharing its browser.
|
|
7
|
+
let retainedPool: WeakRef<Map<string, CdpBrowserFetch>> | undefined;
|
|
8
|
+
const activeBudgetPools = new WeakSet<Map<string, CdpBrowserFetch>>();
|
|
9
|
+
|
|
10
|
+
/** Evict idle contexts before a transport launches its next browser. Omitting
|
|
11
|
+
* the key reserves the budget for a transport that does not use the CDP pool. */
|
|
12
|
+
export async function prepareTeachBrowserContext(
|
|
13
|
+
pool: Map<string, CdpBrowserFetch> | undefined,
|
|
14
|
+
selectedKey?: string,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
if (!pool || !activeBudgetPools.has(pool)) return;
|
|
17
|
+
const keep = selectedKey === undefined ? undefined : pool.get(selectedKey);
|
|
18
|
+
const older = new Set<CdpBrowserFetch>();
|
|
19
|
+
for (const [key, browser] of pool) {
|
|
20
|
+
if (browser === keep) continue;
|
|
21
|
+
pool.delete(key);
|
|
22
|
+
older.add(browser);
|
|
23
|
+
}
|
|
24
|
+
await Promise.allSettled([...older].map((browser) => browser.close()));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function drain(pool: Map<string, CdpBrowserFetch>): Promise<void> {
|
|
28
|
+
const browsers = [...new Set(pool.values())];
|
|
29
|
+
pool.clear();
|
|
30
|
+
await Promise.allSettled(browsers.map((browser) => browser.close()));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function retainLatestBrowser(pool: Map<string, CdpBrowserFetch>): Promise<void> {
|
|
34
|
+
const latest = [...pool.entries()].at(-1);
|
|
35
|
+
if (!latest) return;
|
|
36
|
+
const older = new Set<CdpBrowserFetch>();
|
|
37
|
+
for (const [key, browser] of pool) {
|
|
38
|
+
if (browser === latest[1]) continue;
|
|
39
|
+
pool.delete(key);
|
|
40
|
+
older.add(browser);
|
|
41
|
+
}
|
|
42
|
+
await Promise.allSettled([...older].map((browser) => browser.close()));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Optional host resource limit; agent scheduling and auth checkpoints are unchanged. */
|
|
46
|
+
export async function withTeachBrowserResources<T>(
|
|
47
|
+
callerPool: Map<string, CdpBrowserFetch> | undefined,
|
|
48
|
+
signal: AbortSignal | undefined,
|
|
49
|
+
work: (pool: Map<string, CdpBrowserFetch> | undefined) => Promise<T>,
|
|
50
|
+
): Promise<T> {
|
|
51
|
+
if (process.env.IMPRINT_TEACH_LOW_MEMORY !== '1') return await work(callerPool);
|
|
52
|
+
|
|
53
|
+
const ahead = previousCheck;
|
|
54
|
+
let release = () => {};
|
|
55
|
+
const held = new Promise<void>((resolve) => {
|
|
56
|
+
release = resolve;
|
|
57
|
+
});
|
|
58
|
+
previousCheck = ahead.then(() => held);
|
|
59
|
+
const pool = callerPool ?? new Map<string, CdpBrowserFetch>();
|
|
60
|
+
let admitted = false;
|
|
61
|
+
let completed = false;
|
|
62
|
+
try {
|
|
63
|
+
await withAbortSignal(() => ahead, signal);
|
|
64
|
+
if (signal?.aborted) throw abortSignalError(signal);
|
|
65
|
+
admitted = true;
|
|
66
|
+
const previous = retainedPool?.deref();
|
|
67
|
+
if (previous && previous !== pool) await drain(previous);
|
|
68
|
+
retainedPool = undefined;
|
|
69
|
+
activeBudgetPools.add(pool);
|
|
70
|
+
const result = await work(pool);
|
|
71
|
+
completed = true;
|
|
72
|
+
return result;
|
|
73
|
+
} finally {
|
|
74
|
+
try {
|
|
75
|
+
if (admitted) {
|
|
76
|
+
activeBudgetPools.delete(pool);
|
|
77
|
+
if (callerPool && completed && !signal?.aborted) {
|
|
78
|
+
await retainLatestBrowser(pool);
|
|
79
|
+
retainedPool = new WeakRef(pool);
|
|
80
|
+
} else {
|
|
81
|
+
await drain(pool);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
release();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Keep long agent stages visible without changing their execution or deadline. */
|
|
2
|
+
export async function withTeachStageProgress<T>(
|
|
3
|
+
options: {
|
|
4
|
+
label: string;
|
|
5
|
+
report?: (message: string) => void;
|
|
6
|
+
intervalMs?: number;
|
|
7
|
+
},
|
|
8
|
+
work: () => Promise<T>,
|
|
9
|
+
): Promise<T> {
|
|
10
|
+
if (!options.report) return await work();
|
|
11
|
+
const startedAt = performance.now();
|
|
12
|
+
const elapsed = (): string => {
|
|
13
|
+
const seconds = Math.floor((performance.now() - startedAt) / 1_000);
|
|
14
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
15
|
+
};
|
|
16
|
+
const report = (message: string): void => {
|
|
17
|
+
try {
|
|
18
|
+
options.report?.(message);
|
|
19
|
+
} catch {
|
|
20
|
+
// A diagnostic callback must not interrupt work or replace its error.
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
report(`${options.label} started`);
|
|
24
|
+
const timer = setInterval(
|
|
25
|
+
() => report(`${options.label} pending (${elapsed()} elapsed)`),
|
|
26
|
+
options.intervalMs ?? 30_000,
|
|
27
|
+
);
|
|
28
|
+
timer.unref();
|
|
29
|
+
let completed = false;
|
|
30
|
+
try {
|
|
31
|
+
const result = await work();
|
|
32
|
+
completed = true;
|
|
33
|
+
return result;
|
|
34
|
+
} finally {
|
|
35
|
+
clearInterval(timer);
|
|
36
|
+
report(`${options.label} ${completed ? 'finished' : 'stopped'} (${elapsed()} elapsed)`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { abortSignalError } from './concurrency.ts';
|
|
3
|
+
import { pauseLogsForPrompt } from './log.ts';
|
|
4
|
+
import type { OnUserPrompt } from './user-interaction.ts';
|
|
5
|
+
|
|
6
|
+
/** The parent terminal owns input; model/stdio child processes never read it. */
|
|
7
|
+
export const terminalUserPrompt: OnUserPrompt = async (prompt, signal) => {
|
|
8
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
9
|
+
return {
|
|
10
|
+
status: 'unavailable',
|
|
11
|
+
reason:
|
|
12
|
+
'User input requires an interactive terminal. Run imprint teach without --no-interactive in a terminal, or provide an onPrompt callback.',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (signal?.aborted) throw abortSignalError(signal);
|
|
16
|
+
const resumeLogs = pauseLogsForPrompt();
|
|
17
|
+
try {
|
|
18
|
+
const common = {
|
|
19
|
+
message: prompt.message,
|
|
20
|
+
signal,
|
|
21
|
+
input: process.stdin,
|
|
22
|
+
output: process.stderr,
|
|
23
|
+
};
|
|
24
|
+
const answer = prompt.options
|
|
25
|
+
? await p.select({
|
|
26
|
+
...common,
|
|
27
|
+
options: prompt.options.map((value) => ({ value, label: value })),
|
|
28
|
+
})
|
|
29
|
+
: prompt.sensitive
|
|
30
|
+
? await p.password({
|
|
31
|
+
...common,
|
|
32
|
+
validate: (value) => (value?.length ? undefined : 'Enter a value or cancel.'),
|
|
33
|
+
})
|
|
34
|
+
: await p.text(common);
|
|
35
|
+
if (signal?.aborted) throw abortSignalError(signal);
|
|
36
|
+
if (p.isCancel(answer))
|
|
37
|
+
return { status: 'cancelled', reason: 'The user cancelled the question.' };
|
|
38
|
+
return { status: 'answered', value: String(answer) };
|
|
39
|
+
} finally {
|
|
40
|
+
resumeLogs();
|
|
41
|
+
}
|
|
42
|
+
};
|
package/src/imprint/types.ts
CHANGED
|
@@ -251,6 +251,9 @@ const BootstrapCaptureSchema = z.discriminatedUnion('source', [
|
|
|
251
251
|
]);
|
|
252
252
|
export type BootstrapCapture = z.infer<typeof BootstrapCaptureSchema>;
|
|
253
253
|
|
|
254
|
+
/** Managed authentication can disable transport retry for an ambiguous dispatch. */
|
|
255
|
+
export type BrowserRequestInit = RequestInit & { allowAmbiguousRetry?: false };
|
|
256
|
+
|
|
254
257
|
const WorkflowRequestBaseSchema = z.object({
|
|
255
258
|
method: z.string(),
|
|
256
259
|
/** Sequence number of the captured request that grounds this outgoing
|
|
@@ -365,6 +368,25 @@ export type NavigationNetworkResponseMatcher = NonNullable<
|
|
|
365
368
|
NonNullable<WorkflowRequest['navigation']>['networkResponse']
|
|
366
369
|
>;
|
|
367
370
|
|
|
371
|
+
const AuthOutcomeSchema = z.discriminatedUnion('type', [
|
|
372
|
+
z
|
|
373
|
+
.object({
|
|
374
|
+
type: z.literal('pause'),
|
|
375
|
+
next: z.string().min(1),
|
|
376
|
+
resume: z.literal('automatic').optional(),
|
|
377
|
+
evidence: z.array(z.string().min(1)).optional().default([]),
|
|
378
|
+
carry: z.array(z.string().min(1)).optional().default([]),
|
|
379
|
+
message: z.string().min(1),
|
|
380
|
+
})
|
|
381
|
+
.strict(),
|
|
382
|
+
z
|
|
383
|
+
.object({
|
|
384
|
+
type: z.literal('success'),
|
|
385
|
+
evidence: z.array(z.string().min(1)).optional().default([]),
|
|
386
|
+
})
|
|
387
|
+
.strict(),
|
|
388
|
+
]);
|
|
389
|
+
|
|
368
390
|
const AuthConfigSchema = z
|
|
369
391
|
.object({
|
|
370
392
|
/** Recording-grounded auth program. Action names, boundaries, evidence,
|
|
@@ -393,23 +415,13 @@ const AuthConfigSchema = z
|
|
|
393
415
|
.strict(),
|
|
394
416
|
)
|
|
395
417
|
.min(1),
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
message: z.string().min(1),
|
|
404
|
-
})
|
|
405
|
-
.strict(),
|
|
406
|
-
z
|
|
407
|
-
.object({
|
|
408
|
-
type: z.literal('success'),
|
|
409
|
-
evidence: z.array(z.string().min(1)).optional().default([]),
|
|
410
|
-
})
|
|
411
|
-
.strict(),
|
|
412
|
-
]),
|
|
418
|
+
/** First matching capture against this action's final step response
|
|
419
|
+
* selects an outcome. A match also supplies the named capture. */
|
|
420
|
+
branches: z
|
|
421
|
+
.array(z.object({ when: RequestCaptureSchema, outcome: AuthOutcomeSchema }).strict())
|
|
422
|
+
.optional(),
|
|
423
|
+
/** Fallback when no branch matches; its evidence is still required. */
|
|
424
|
+
outcome: AuthOutcomeSchema,
|
|
413
425
|
})
|
|
414
426
|
.strict(),
|
|
415
427
|
),
|
|
@@ -430,6 +442,10 @@ const AuthConfigSchema = z
|
|
|
430
442
|
.optional();
|
|
431
443
|
export type AuthConfig = z.infer<typeof AuthConfigSchema>;
|
|
432
444
|
|
|
445
|
+
export function authActionOutcomes(action: NonNullable<AuthConfig>['actions'][string]) {
|
|
446
|
+
return [...(action.branches ?? []).map((branch) => branch.outcome), action.outcome];
|
|
447
|
+
}
|
|
448
|
+
|
|
433
449
|
export function persistedCaptureName(
|
|
434
450
|
config: NonNullable<AuthConfig>,
|
|
435
451
|
credentialName: string,
|
|
@@ -538,7 +554,7 @@ export interface StateMissingItem {
|
|
|
538
554
|
* values, and response content. */
|
|
539
555
|
export interface RequestStageFact {
|
|
540
556
|
requestIndex: number;
|
|
541
|
-
stage: 'preparation' | 'transform' | 'send';
|
|
557
|
+
stage: 'preparation' | 'transform' | 'send' | 'capture' | 'repeat' | 'outcome';
|
|
542
558
|
outcome: 'passed' | 'failed' | 'skipped' | 'unavailable';
|
|
543
559
|
/** Final body metadata at this stage. No body bytes are retained. */
|
|
544
560
|
bodyPresent?: boolean;
|
|
@@ -547,6 +563,10 @@ export interface RequestStageFact {
|
|
|
547
563
|
bodyChanged?: boolean;
|
|
548
564
|
/** HTTP status proves transport completed without exposing response content. */
|
|
549
565
|
httpStatus?: number;
|
|
566
|
+
/** One-based attempt within the declared step; no request or response values. */
|
|
567
|
+
attempt?: number;
|
|
568
|
+
stepIndex?: number;
|
|
569
|
+
transportInvoked?: boolean;
|
|
550
570
|
}
|
|
551
571
|
|
|
552
572
|
/** Discriminated union returned by every generated tool. */
|
|
@@ -582,10 +602,23 @@ export type ToolResult<T = unknown> =
|
|
|
582
602
|
};
|
|
583
603
|
/** Value-free stage receipts for requests attempted before this failure. */
|
|
584
604
|
requestStageFacts?: RequestStageFact[];
|
|
605
|
+
/** Exact count plus bounded recent stage facts; retries remain model-authored. */
|
|
606
|
+
authExecution?: {
|
|
607
|
+
action: string;
|
|
608
|
+
stepIndex: number;
|
|
609
|
+
requestIndex: number;
|
|
610
|
+
attempts: number;
|
|
611
|
+
phase: RequestStageFact['stage'];
|
|
612
|
+
transportInvoked: boolean;
|
|
613
|
+
responseReceived: boolean;
|
|
614
|
+
httpStatus?: number;
|
|
615
|
+
lastFailure?: { error: string; message: string };
|
|
616
|
+
};
|
|
585
617
|
/** Recording-derived state and generic action progress. Echo this object
|
|
586
618
|
* unchanged on the next auth call. */
|
|
587
619
|
continuation?: Record<string, unknown>;
|
|
588
620
|
nextAction?: string;
|
|
621
|
+
resume?: 'automatic';
|
|
589
622
|
};
|
|
590
623
|
|
|
591
624
|
// ─── Cron config (input to `imprint cron`) ───────────────────────────────────
|