taphound 0.2.0-dev.1
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/LICENSE +202 -0
- package/README.md +200 -0
- package/README.zh-CN.md +200 -0
- package/assets/brand/taphound-mark.svg +9 -0
- package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
- package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
- package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
- package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
- package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
- package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
- package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
- package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
- package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
- package/dist/adapters/adb/adb-adapter.d.ts +23 -0
- package/dist/adapters/adb/adb-adapter.js +218 -0
- package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
- package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
- package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
- package/dist/adapters/android-cli/layout-parser.js +174 -0
- package/dist/adapters/clock/system-clock.d.ts +5 -0
- package/dist/adapters/clock/system-clock.js +13 -0
- package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
- package/dist/adapters/filesystem/artifact-store.js +73 -0
- package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
- package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
- package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
- package/dist/adapters/filesystem/generation-session-store.js +1603 -0
- package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
- package/dist/adapters/filesystem/journey-writer.js +34 -0
- package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
- package/dist/adapters/filesystem/project-bound-file.js +240 -0
- package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
- package/dist/adapters/filesystem/project-file-inspector.js +198 -0
- package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
- package/dist/adapters/filesystem/skill-installer.js +39 -0
- package/dist/adapters/process/node-process-runner.d.ts +8 -0
- package/dist/adapters/process/node-process-runner.js +155 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
- package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
- package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
- package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
- package/dist/application/assertion/expectation-evaluator.js +207 -0
- package/dist/application/collector/logcat-collector.d.ts +39 -0
- package/dist/application/collector/logcat-collector.js +107 -0
- package/dist/application/context/context-validator.d.ts +27 -0
- package/dist/application/context/context-validator.js +120 -0
- package/dist/application/doctor/doctor-service.d.ts +35 -0
- package/dist/application/doctor/doctor-service.js +178 -0
- package/dist/application/generation/focused-input.d.ts +2 -0
- package/dist/application/generation/focused-input.js +9 -0
- package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
- package/dist/application/generation/generation-confirmation-service.js +383 -0
- package/dist/application/generation/generation-finalizer.d.ts +67 -0
- package/dist/application/generation/generation-finalizer.js +646 -0
- package/dist/application/generation/generation-publisher.d.ts +46 -0
- package/dist/application/generation/generation-publisher.js +178 -0
- package/dist/application/generation/generation-starter.d.ts +31 -0
- package/dist/application/generation/generation-starter.js +102 -0
- package/dist/application/generation/generation-step-executor.d.ts +52 -0
- package/dist/application/generation/generation-step-executor.js +668 -0
- package/dist/application/generation/proposed-step-validator.d.ts +11 -0
- package/dist/application/generation/proposed-step-validator.js +88 -0
- package/dist/application/generation/risk-evaluator.d.ts +9 -0
- package/dist/application/generation/risk-evaluator.js +14 -0
- package/dist/application/generation/runtime-observer.d.ts +39 -0
- package/dist/application/generation/runtime-observer.js +157 -0
- package/dist/application/init/init-service.d.ts +17 -0
- package/dist/application/init/init-service.js +44 -0
- package/dist/application/interaction/action-executor.d.ts +25 -0
- package/dist/application/interaction/action-executor.js +103 -0
- package/dist/application/interaction/fallback-resolver.d.ts +24 -0
- package/dist/application/interaction/fallback-resolver.js +48 -0
- package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
- package/dist/application/interaction/scroll-to-executor.js +191 -0
- package/dist/application/locator/locator-resolver.d.ts +20 -0
- package/dist/application/locator/locator-resolver.js +89 -0
- package/dist/application/project/project-describer.d.ts +14 -0
- package/dist/application/project/project-describer.js +8 -0
- package/dist/application/recorder/locator-selector.d.ts +10 -0
- package/dist/application/recorder/locator-selector.js +80 -0
- package/dist/application/recorder/recorder-service.d.ts +41 -0
- package/dist/application/recorder/recorder-service.js +331 -0
- package/dist/application/report/report-writer.d.ts +10 -0
- package/dist/application/report/report-writer.js +39 -0
- package/dist/application/runtime/activity-waiter.d.ts +33 -0
- package/dist/application/runtime/activity-waiter.js +73 -0
- package/dist/application/runtime/launch-failure.d.ts +2 -0
- package/dist/application/runtime/launch-failure.js +20 -0
- package/dist/application/runtime/process-waiter.d.ts +27 -0
- package/dist/application/runtime/process-waiter.js +73 -0
- package/dist/application/runtime/step-runner.d.ts +50 -0
- package/dist/application/runtime/step-runner.js +429 -0
- package/dist/application/runtime/verify-runtime.d.ts +44 -0
- package/dist/application/runtime/verify-runtime.js +350 -0
- package/dist/application/wait/idle-waiter.d.ts +30 -0
- package/dist/application/wait/idle-waiter.js +90 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +94 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +51 -0
- package/dist/cli/commands/generation.d.ts +3 -0
- package/dist/cli/commands/generation.js +518 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +104 -0
- package/dist/cli/commands/project.d.ts +3 -0
- package/dist/cli/commands/project.js +58 -0
- package/dist/cli/commands/record.d.ts +3 -0
- package/dist/cli/commands/record.js +88 -0
- package/dist/cli/commands/verify.d.ts +3 -0
- package/dist/cli/commands/verify.js +106 -0
- package/dist/cli/dependencies.d.ts +66 -0
- package/dist/cli/dependencies.js +204 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +73 -0
- package/dist/cli/output.d.ts +16 -0
- package/dist/cli/output.js +22 -0
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +43 -0
- package/dist/domain/activity.d.ts +7 -0
- package/dist/domain/activity.js +65 -0
- package/dist/domain/app-process.d.ts +7 -0
- package/dist/domain/app-process.js +10 -0
- package/dist/domain/config.d.ts +15 -0
- package/dist/domain/config.js +17 -0
- package/dist/domain/failure.d.ts +4 -0
- package/dist/domain/failure.js +43 -0
- package/dist/domain/generation.d.ts +912 -0
- package/dist/domain/generation.js +342 -0
- package/dist/domain/init.d.ts +42 -0
- package/dist/domain/init.js +114 -0
- package/dist/domain/journey.d.ts +673 -0
- package/dist/domain/journey.js +109 -0
- package/dist/domain/layout.d.ts +35 -0
- package/dist/domain/layout.js +33 -0
- package/dist/domain/locator.d.ts +8 -0
- package/dist/domain/locator.js +5 -0
- package/dist/domain/project-context.d.ts +101 -0
- package/dist/domain/project-context.js +64 -0
- package/dist/domain/proposed-step.d.ts +341 -0
- package/dist/domain/proposed-step.js +87 -0
- package/dist/domain/report.d.ts +349 -0
- package/dist/domain/report.js +135 -0
- package/dist/domain/runtime-snapshot.d.ts +16 -0
- package/dist/domain/runtime-snapshot.js +46 -0
- package/dist/ports/adb.d.ts +42 -0
- package/dist/ports/adb.js +1 -0
- package/dist/ports/android-cli.d.ts +21 -0
- package/dist/ports/android-cli.js +1 -0
- package/dist/ports/artifact-store.d.ts +12 -0
- package/dist/ports/artifact-store.js +1 -0
- package/dist/ports/clock.d.ts +4 -0
- package/dist/ports/clock.js +1 -0
- package/dist/ports/generation-meta-writer.d.ts +10 -0
- package/dist/ports/generation-meta-writer.js +1 -0
- package/dist/ports/generation-prompt.d.ts +19 -0
- package/dist/ports/generation-prompt.js +6 -0
- package/dist/ports/generation-session-store.d.ts +32 -0
- package/dist/ports/generation-session-store.js +26 -0
- package/dist/ports/init-prompt.d.ts +8 -0
- package/dist/ports/init-prompt.js +6 -0
- package/dist/ports/journey-writer.d.ts +10 -0
- package/dist/ports/journey-writer.js +1 -0
- package/dist/ports/process-runner.d.ts +32 -0
- package/dist/ports/process-runner.js +1 -0
- package/dist/ports/project-bound-file.d.ts +8 -0
- package/dist/ports/project-bound-file.js +1 -0
- package/dist/ports/project-file-inspector.d.ts +33 -0
- package/dist/ports/project-file-inspector.js +1 -0
- package/dist/ports/recorder-prompt.d.ts +29 -0
- package/dist/ports/recorder-prompt.js +1 -0
- package/dist/ports/skill-installer.d.ts +8 -0
- package/dist/ports/skill-installer.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { appProcessPids, primaryAppPid } from "../../domain/app-process.js";
|
|
4
|
+
import { GenerationSessionSchema, GenerationInFlightSchema, expandProposedStepVariables, hashGenerationConfirmationEvidence } from "../../domain/generation.js";
|
|
5
|
+
import { JourneyStepSchema } from "../../domain/journey.js";
|
|
6
|
+
import { ProposedStepSchema, hashProposedStep } from "../../domain/proposed-step.js";
|
|
7
|
+
import { RuntimeSnapshotSchema, hashRuntimeSnapshot } from "../../domain/runtime-snapshot.js";
|
|
8
|
+
import { ExpectationEvaluator } from "../assertion/expectation-evaluator.js";
|
|
9
|
+
import { LogcatCollector } from "../collector/logcat-collector.js";
|
|
10
|
+
import { ActionExecutor } from "../interaction/action-executor.js";
|
|
11
|
+
import { ScrollToExecutor } from "../interaction/scroll-to-executor.js";
|
|
12
|
+
import { resolveLocator } from "../locator/locator-resolver.js";
|
|
13
|
+
import { IdleWaiter } from "../wait/idle-waiter.js";
|
|
14
|
+
import { summarizeProposedStep } from "./generation-confirmation-service.js";
|
|
15
|
+
import { GenerationOperationError } from "./generation-starter.js";
|
|
16
|
+
import { RiskEvaluator } from "./risk-evaluator.js";
|
|
17
|
+
import { hasExactlyOneEnabledFocusedElement } from "./focused-input.js";
|
|
18
|
+
class StepCancelledError extends Error {
|
|
19
|
+
name = "StepCancelledError";
|
|
20
|
+
}
|
|
21
|
+
function cancellation() {
|
|
22
|
+
return {
|
|
23
|
+
status: "cancelled",
|
|
24
|
+
failure: {
|
|
25
|
+
code: "RECOVERY_REQUIRED",
|
|
26
|
+
message: "Step was cancelled"
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function isCancelled(signal) {
|
|
31
|
+
return signal?.aborted === true;
|
|
32
|
+
}
|
|
33
|
+
function throwIfCancelled(signal) {
|
|
34
|
+
if (isCancelled(signal)) {
|
|
35
|
+
throw new StepCancelledError("Step was cancelled");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function executionPath(inFlight, suffix) {
|
|
39
|
+
return `evidence/steps/${String(inFlight.stepIndex)}-${inFlight.attemptId}/${suffix}`;
|
|
40
|
+
}
|
|
41
|
+
function sha256(value) {
|
|
42
|
+
return createHash("sha256").update(value).digest("hex");
|
|
43
|
+
}
|
|
44
|
+
function canonicalEvidenceBytes(value) {
|
|
45
|
+
const canonicalize = (input) => {
|
|
46
|
+
if (Array.isArray(input))
|
|
47
|
+
return input.map(canonicalize);
|
|
48
|
+
if (input !== null && typeof input === "object") {
|
|
49
|
+
return Object.fromEntries(Object.keys(input)
|
|
50
|
+
.sort((left, right) => left.localeCompare(right))
|
|
51
|
+
.map((key) => [
|
|
52
|
+
key,
|
|
53
|
+
canonicalize(input[key])
|
|
54
|
+
]));
|
|
55
|
+
}
|
|
56
|
+
return input;
|
|
57
|
+
};
|
|
58
|
+
return Buffer.from(`${JSON.stringify(canonicalize(value), null, 2)}\n`);
|
|
59
|
+
}
|
|
60
|
+
function asFailure(error) {
|
|
61
|
+
if (error !== null
|
|
62
|
+
&& typeof error === "object"
|
|
63
|
+
&& "code" in error
|
|
64
|
+
&& typeof error.code === "string") {
|
|
65
|
+
return {
|
|
66
|
+
code: error.code,
|
|
67
|
+
message: error instanceof Error ? error.message : error.code
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
code: "INTERNAL_ERROR",
|
|
72
|
+
message: error instanceof Error ? error.message : "Unknown step failure"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function fail(code, message) {
|
|
76
|
+
throw Object.assign(new Error(message), { code });
|
|
77
|
+
}
|
|
78
|
+
function sameSession(left, right) {
|
|
79
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
80
|
+
}
|
|
81
|
+
function exactApprovedChallenge(session, proposal, snapshot, source, now) {
|
|
82
|
+
const challenge = session.pendingConfirmation;
|
|
83
|
+
if (challenge === null
|
|
84
|
+
|| challenge.status !== "approved"
|
|
85
|
+
|| session.revision !== proposal.binding.baseRevision + 2
|
|
86
|
+
|| challenge.stepIndex !== session.candidateSteps.length
|
|
87
|
+
|| challenge.proposalHash !== hashProposedStep(proposal)
|
|
88
|
+
|| challenge.snapshotHash !== hashRuntimeSnapshot(snapshot)
|
|
89
|
+
|| challenge.snapshotHash !== proposal.binding.snapshotHash
|
|
90
|
+
|| challenge.evidenceHash !== hashGenerationConfirmationEvidence({
|
|
91
|
+
version: 1,
|
|
92
|
+
proposal,
|
|
93
|
+
snapshot,
|
|
94
|
+
source
|
|
95
|
+
})
|
|
96
|
+
|| challenge.actionSummary !== summarizeProposedStep(proposal)
|
|
97
|
+
|| now.getTime() >= new Date(challenge.expiresAt).getTime()) {
|
|
98
|
+
throw new GenerationOperationError("RISK_CONFIRMATION_REQUIRED", "An exact non-expired approved confirmation is required");
|
|
99
|
+
}
|
|
100
|
+
return challenge;
|
|
101
|
+
}
|
|
102
|
+
function isBoundApprovedChallenge(challenge, session, proposal, snapshot) {
|
|
103
|
+
return challenge !== null
|
|
104
|
+
&& challenge.status === "approved"
|
|
105
|
+
&& challenge.stepIndex === session.candidateSteps.length
|
|
106
|
+
&& challenge.proposalHash === hashProposedStep(proposal)
|
|
107
|
+
&& challenge.snapshotHash === hashRuntimeSnapshot(snapshot)
|
|
108
|
+
&& challenge.snapshotHash === proposal.binding.snapshotHash
|
|
109
|
+
&& challenge.actionSummary === summarizeProposedStep(proposal);
|
|
110
|
+
}
|
|
111
|
+
function assertBaseAuthorization(session, proposal, snapshot) {
|
|
112
|
+
if (session.id !== proposal.binding.generationId
|
|
113
|
+
|| session.id !== snapshot.generationId
|
|
114
|
+
|| proposal.binding.generationId !== snapshot.generationId
|
|
115
|
+
|| proposal.binding.baseRevision !== snapshot.baseRevision
|
|
116
|
+
|| proposal.binding.snapshotHash !== hashRuntimeSnapshot(snapshot)
|
|
117
|
+
|| proposal.binding.snapshotHash !== session.bindings.snapshotHash
|
|
118
|
+
|| snapshot.expectedPackageName !== session.target.packageName
|
|
119
|
+
|| snapshot.deviceSerial !== session.target.deviceSerial
|
|
120
|
+
|| snapshot.activity !== proposal.activity.before
|
|
121
|
+
|| session.state !== "active"
|
|
122
|
+
|| session.inFlight !== null
|
|
123
|
+
|| session.verification.status !== "notRun"
|
|
124
|
+
|| session.publication.status !== "notRun") {
|
|
125
|
+
throw new GenerationOperationError("SNAPSHOT_STALE", "Proposal is not bound to the authoritative generation snapshot");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function executableStep(proposal, variables, after) {
|
|
129
|
+
const expanded = expandProposedStepVariables(proposal, variables);
|
|
130
|
+
const action = Object.fromEntries(Object.entries(expanded).filter(([key]) => key !== "binding" && key !== "activity"));
|
|
131
|
+
return JourneyStepSchema.parse({
|
|
132
|
+
...action,
|
|
133
|
+
activity: {
|
|
134
|
+
before: expanded.activity.before,
|
|
135
|
+
after
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function requireTarget(layout, step) {
|
|
140
|
+
if (step.action !== "click"
|
|
141
|
+
&& step.action !== "longClick"
|
|
142
|
+
&& step.action !== "swipe") {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const resolution = resolveLocator(layout, step.locator);
|
|
146
|
+
if (resolution.status !== "found") {
|
|
147
|
+
fail(resolution.code, resolution.message);
|
|
148
|
+
}
|
|
149
|
+
if (step.action === "click" && resolution.element.clickable !== true) {
|
|
150
|
+
fail("ACTION_FAILED", "click target is not clickable");
|
|
151
|
+
}
|
|
152
|
+
if (step.action === "longClick"
|
|
153
|
+
&& resolution.element.longClickable !== true) {
|
|
154
|
+
fail("ACTION_FAILED", "longClick target is not longClickable");
|
|
155
|
+
}
|
|
156
|
+
if (step.action === "swipe"
|
|
157
|
+
&& (resolution.element.scrollable !== true
|
|
158
|
+
|| resolution.element.bounds === undefined)) {
|
|
159
|
+
fail("ACTION_FAILED", "swipe target lacks scrollable bounds");
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
point: resolution.point,
|
|
163
|
+
...(resolution.element.bounds === undefined
|
|
164
|
+
? {}
|
|
165
|
+
: { bounds: resolution.element.bounds })
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export class GenerationStepExecutor {
|
|
169
|
+
dependencies;
|
|
170
|
+
riskEvaluator = new RiskEvaluator();
|
|
171
|
+
constructor(dependencies) {
|
|
172
|
+
this.dependencies = dependencies;
|
|
173
|
+
}
|
|
174
|
+
execute = async (input) => {
|
|
175
|
+
const proposal = ProposedStepSchema.parse(input.proposal);
|
|
176
|
+
const snapshot = RuntimeSnapshotSchema.parse(input.snapshot);
|
|
177
|
+
const source = z.enum(["planner", "manualOverride"]).parse(input.source);
|
|
178
|
+
const session = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
|
|
179
|
+
assertBaseAuthorization(session, proposal, snapshot);
|
|
180
|
+
const risk = this.riskEvaluator.evaluate(proposal.action, session.target.interactionPolicy);
|
|
181
|
+
if (risk.effectiveRisk === "forbidden") {
|
|
182
|
+
throw new GenerationOperationError("ACTION_FORBIDDEN", `${proposal.action} is forbidden by the Core interaction policy`);
|
|
183
|
+
}
|
|
184
|
+
let approved;
|
|
185
|
+
if (risk.effectiveRisk === "confirmationRequired") {
|
|
186
|
+
try {
|
|
187
|
+
approved = exactApprovedChallenge(session, proposal, snapshot, source, this.dependencies.now());
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (isBoundApprovedChallenge(session.pendingConfirmation, session, proposal, snapshot)) {
|
|
191
|
+
await this.dependencies.clearApprovedConfirmation(session.id, session.pendingConfirmation);
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (session.revision !== proposal.binding.baseRevision
|
|
197
|
+
|| session.pendingConfirmation !== null) {
|
|
198
|
+
throw new GenerationOperationError("SNAPSHOT_STALE", "Safe proposal revision is no longer current");
|
|
199
|
+
}
|
|
200
|
+
const clearApproved = async () => {
|
|
201
|
+
if (approved !== undefined) {
|
|
202
|
+
await this.dependencies.clearApprovedConfirmation(session.id, approved);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
if (isCancelled(input.signal)) {
|
|
206
|
+
await clearApproved();
|
|
207
|
+
return cancellation();
|
|
208
|
+
}
|
|
209
|
+
let fresh;
|
|
210
|
+
try {
|
|
211
|
+
fresh = await this.dependencies.freshnessGuard.assertFresh(proposal.binding, input.signal, approved);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
await clearApproved();
|
|
215
|
+
if (isCancelled(input.signal)) {
|
|
216
|
+
return cancellation();
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
if (isCancelled(input.signal)) {
|
|
221
|
+
await clearApproved();
|
|
222
|
+
return cancellation();
|
|
223
|
+
}
|
|
224
|
+
if (fresh.foregroundPackageName !== session.target.packageName) {
|
|
225
|
+
await clearApproved();
|
|
226
|
+
throw new GenerationOperationError("PACKAGE_ESCAPE", "Fresh foreground package escaped the generation target");
|
|
227
|
+
}
|
|
228
|
+
if (fresh.pid === null) {
|
|
229
|
+
await clearApproved();
|
|
230
|
+
throw new GenerationOperationError("APP_CRASHED", "Fresh generation process is not running");
|
|
231
|
+
}
|
|
232
|
+
if (fresh.activity !== proposal.activity.before) {
|
|
233
|
+
await clearApproved();
|
|
234
|
+
throw new GenerationOperationError("SNAPSHOT_STALE", "Fresh Activity does not match the accepted proposal");
|
|
235
|
+
}
|
|
236
|
+
const authoritativePid = fresh.pid;
|
|
237
|
+
let inFlight;
|
|
238
|
+
try {
|
|
239
|
+
inFlight = GenerationInFlightSchema.parse({
|
|
240
|
+
stepIndex: session.candidateSteps.length,
|
|
241
|
+
snapshotHash: proposal.binding.snapshotHash,
|
|
242
|
+
proposalHash: hashProposedStep(proposal),
|
|
243
|
+
attemptId: this.dependencies.generateAttemptId()
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
await clearApproved();
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
if (isCancelled(input.signal)) {
|
|
251
|
+
await clearApproved();
|
|
252
|
+
return cancellation();
|
|
253
|
+
}
|
|
254
|
+
let begun;
|
|
255
|
+
try {
|
|
256
|
+
begun = await this.dependencies.store.beginStep(session.id, session.revision, inFlight, approved);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const latest = GenerationSessionSchema.parse(await this.dependencies.store.read(session.id));
|
|
260
|
+
if (JSON.stringify(latest.inFlight) === JSON.stringify(inFlight)) {
|
|
261
|
+
await this.markRecovery(session.id, inFlight);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
await clearApproved();
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
if (isCancelled(input.signal)) {
|
|
269
|
+
await this.markRecovery(session.id, inFlight);
|
|
270
|
+
return cancellation();
|
|
271
|
+
}
|
|
272
|
+
const proposalEvidencePath = executionPath(inFlight, "proposal.json");
|
|
273
|
+
const snapshotEvidencePath = executionPath(inFlight, "snapshot.json");
|
|
274
|
+
let proposalEvidenceSha256;
|
|
275
|
+
let snapshotEvidenceSha256;
|
|
276
|
+
try {
|
|
277
|
+
await this.dependencies.store.writeEvidence(session.id, proposalEvidencePath, proposal);
|
|
278
|
+
proposalEvidenceSha256 = sha256(canonicalEvidenceBytes(proposal));
|
|
279
|
+
await this.dependencies.store.writeEvidence(session.id, snapshotEvidencePath, snapshot);
|
|
280
|
+
snapshotEvidenceSha256 = sha256(canonicalEvidenceBytes(snapshot));
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
await this.markRecovery(session.id, inFlight);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
if (isCancelled(input.signal)) {
|
|
287
|
+
await this.markRecovery(session.id, inFlight);
|
|
288
|
+
return cancellation();
|
|
289
|
+
}
|
|
290
|
+
const logcat = new LogcatCollector(this.dependencies.adb, this.dependencies.clock);
|
|
291
|
+
let logcatStarted = false;
|
|
292
|
+
let finalStep;
|
|
293
|
+
let outcome;
|
|
294
|
+
const stepStartedAt = this.dependencies.clock.now();
|
|
295
|
+
try {
|
|
296
|
+
await logcat.start({
|
|
297
|
+
deviceSerial: session.target.deviceSerial,
|
|
298
|
+
...(input.signal === undefined ? {} : { signal: input.signal })
|
|
299
|
+
});
|
|
300
|
+
logcatStarted = true;
|
|
301
|
+
throwIfCancelled(input.signal);
|
|
302
|
+
const preAction = await this.observeLive(session, authoritativePid, proposal.activity.before, input.signal, fresh);
|
|
303
|
+
logcat.scopeToPids(preAction.pids);
|
|
304
|
+
throwIfCancelled(input.signal);
|
|
305
|
+
const provisional = executableStep(proposal, session.variables, proposal.activity.before);
|
|
306
|
+
const actionExecutor = new ActionExecutor(this.dependencies.adb, session.target.deviceSerial);
|
|
307
|
+
const idleWaiter = new IdleWaiter(this.dependencies.androidCli, this.dependencies.clock, session.target.deviceSerial);
|
|
308
|
+
if (provisional.action === "scrollTo") {
|
|
309
|
+
const scroll = await new ScrollToExecutor({
|
|
310
|
+
androidCli: this.dependencies.androidCli,
|
|
311
|
+
actionExecutor,
|
|
312
|
+
idleWaiter,
|
|
313
|
+
deviceSerial: session.target.deviceSerial,
|
|
314
|
+
idle: this.dependencies.idle,
|
|
315
|
+
beforeSwipe: async () => {
|
|
316
|
+
const guarded = await this.observeLive(session, authoritativePid, proposal.activity.before, input.signal);
|
|
317
|
+
return guarded.layout;
|
|
318
|
+
},
|
|
319
|
+
beforeMutation: async () => {
|
|
320
|
+
await this.assertForegroundIdentity(session, authoritativePid, proposal.activity.before, input.signal);
|
|
321
|
+
},
|
|
322
|
+
requireLiveContainerCapability: true
|
|
323
|
+
}).execute(provisional, input.signal, preAction.layout);
|
|
324
|
+
throwIfCancelled(input.signal);
|
|
325
|
+
if (scroll.status === "cancelled") {
|
|
326
|
+
outcome = {
|
|
327
|
+
status: "cancelled",
|
|
328
|
+
failure: { code: "RECOVERY_REQUIRED", message: "Step was cancelled" }
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
else if (scroll.status === "failed") {
|
|
332
|
+
fail(scroll.code, scroll.message);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const target = requireTarget(preAction.layout, provisional);
|
|
337
|
+
if (provisional.action === "inputText") {
|
|
338
|
+
if (!hasExactlyOneEnabledFocusedElement(preAction.layout)) {
|
|
339
|
+
fail("ACTION_FAILED", "inputText requires exactly one enabled focused Layout element");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (outcome === undefined) {
|
|
343
|
+
throwIfCancelled(input.signal);
|
|
344
|
+
await this.assertForegroundIdentity(session, authoritativePid, proposal.activity.before, input.signal);
|
|
345
|
+
throwIfCancelled(input.signal);
|
|
346
|
+
const action = await actionExecutor.execute(provisional, target, input.signal);
|
|
347
|
+
if (isCancelled(input.signal)) {
|
|
348
|
+
outcome = {
|
|
349
|
+
status: "cancelled",
|
|
350
|
+
failure: {
|
|
351
|
+
code: "RECOVERY_REQUIRED",
|
|
352
|
+
message: "Step was cancelled"
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
else if (action.status === "failed") {
|
|
357
|
+
fail(action.code, action.message);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (outcome === undefined) {
|
|
361
|
+
const idle = await idleWaiter.waitUntilIdle(this.dependencies.idle, input.signal);
|
|
362
|
+
if (idle.status === "cancelled") {
|
|
363
|
+
outcome = {
|
|
364
|
+
status: "cancelled",
|
|
365
|
+
failure: {
|
|
366
|
+
code: "RECOVERY_REQUIRED",
|
|
367
|
+
message: "Step was cancelled"
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
else if (idle.status === "timeout") {
|
|
372
|
+
fail(idle.code, "Layout did not become stable before timeout");
|
|
373
|
+
}
|
|
374
|
+
throwIfCancelled(input.signal);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (outcome === undefined) {
|
|
378
|
+
throwIfCancelled(input.signal);
|
|
379
|
+
const after = await this.observeLive(session, authoritativePid, undefined, input.signal);
|
|
380
|
+
logcat.scopeToPids(after.pids);
|
|
381
|
+
finalStep = executableStep(proposal, session.variables, after.activity);
|
|
382
|
+
if (finalStep.expect !== undefined) {
|
|
383
|
+
const expectation = await new ExpectationEvaluator(this.dependencies.adb, this.dependencies.androidCli, logcat, this.dependencies.clock).evaluate(finalStep.expect, {
|
|
384
|
+
packageName: session.target.packageName,
|
|
385
|
+
deviceSerial: session.target.deviceSerial,
|
|
386
|
+
stepStartedAt
|
|
387
|
+
}, input.signal, {
|
|
388
|
+
activity: async (observation) => {
|
|
389
|
+
try {
|
|
390
|
+
const activity = await this.assertForegroundIdentity(session, authoritativePid, undefined, observation.signal, observation.timeoutMs);
|
|
391
|
+
return { status: "observed", activity };
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (error instanceof StepCancelledError)
|
|
395
|
+
throw error;
|
|
396
|
+
return {
|
|
397
|
+
status: "failed",
|
|
398
|
+
message: error instanceof Error
|
|
399
|
+
? error.message
|
|
400
|
+
: "Generated Expect Activity observation failed"
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
layout: async (observation) => {
|
|
405
|
+
try {
|
|
406
|
+
const guarded = await this.observeLive(session, authoritativePid, after.activity, observation.signal, undefined, observation.timeoutMs);
|
|
407
|
+
return { status: "observed", layout: guarded.layout };
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (error instanceof StepCancelledError)
|
|
411
|
+
throw error;
|
|
412
|
+
return {
|
|
413
|
+
status: "failed",
|
|
414
|
+
message: error instanceof Error
|
|
415
|
+
? error.message
|
|
416
|
+
: "Generated Expect Layout observation failed"
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
if (expectation.status === "cancelled") {
|
|
422
|
+
outcome = {
|
|
423
|
+
status: "cancelled",
|
|
424
|
+
failure: {
|
|
425
|
+
code: "RECOVERY_REQUIRED",
|
|
426
|
+
message: "Expectation evaluation was cancelled"
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
else if (expectation.status === "failed") {
|
|
431
|
+
fail(expectation.code, expectation.message);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
throwIfCancelled(input.signal);
|
|
435
|
+
await this.observeLive(session, authoritativePid, finalStep.expect.type === "activity"
|
|
436
|
+
? finalStep.expect.value
|
|
437
|
+
: after.activity, input.signal);
|
|
438
|
+
throwIfCancelled(input.signal);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
outcome = error instanceof StepCancelledError
|
|
445
|
+
? cancellation()
|
|
446
|
+
: { status: "failed", failure: asFailure(error) };
|
|
447
|
+
}
|
|
448
|
+
let stopFailure;
|
|
449
|
+
if (logcatStarted) {
|
|
450
|
+
try {
|
|
451
|
+
const stopped = await logcat.stop();
|
|
452
|
+
if (isCancelled(input.signal)) {
|
|
453
|
+
outcome = cancellation();
|
|
454
|
+
}
|
|
455
|
+
if (stopped.exitCode !== 0
|
|
456
|
+
|| stopped.timedOut
|
|
457
|
+
|| stopped.cancelled
|
|
458
|
+
|| stopped.spawnError !== undefined) {
|
|
459
|
+
stopFailure = {
|
|
460
|
+
code: "COLLECTION_FAILED",
|
|
461
|
+
message: stopped.stderr.trim()
|
|
462
|
+
|| stopped.spawnError
|
|
463
|
+
|| "Logcat collector failed to stop"
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
stopFailure = {
|
|
469
|
+
code: "COLLECTION_FAILED",
|
|
470
|
+
message: error instanceof Error
|
|
471
|
+
? error.message
|
|
472
|
+
: "Logcat collector failed to stop"
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
outcome ??= stopFailure === undefined
|
|
477
|
+
? { status: "succeeded", step: finalStep }
|
|
478
|
+
: { status: "failed", failure: stopFailure };
|
|
479
|
+
const log = logcatStarted
|
|
480
|
+
? logcat.lines().map((line) => line.raw).join("\n")
|
|
481
|
+
: "";
|
|
482
|
+
try {
|
|
483
|
+
await this.dependencies.store.writeTextEvidence(session.id, executionPath(inFlight, "logcat.txt"), log.length === 0 ? "" : `${log}\n`);
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
await this.markRecovery(session.id, inFlight);
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
if (isCancelled(input.signal)) {
|
|
490
|
+
await this.markRecovery(session.id, inFlight);
|
|
491
|
+
return cancellation();
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
await this.dependencies.store.writeEvidence(session.id, executionPath(inFlight, "result.json"), {
|
|
495
|
+
version: 1,
|
|
496
|
+
stepIndex: inFlight.stepIndex,
|
|
497
|
+
proposalHash: inFlight.proposalHash,
|
|
498
|
+
snapshotHash: inFlight.snapshotHash,
|
|
499
|
+
attemptId: inFlight.attemptId,
|
|
500
|
+
source,
|
|
501
|
+
proposalEvidence: {
|
|
502
|
+
path: proposalEvidencePath,
|
|
503
|
+
sha256: proposalEvidenceSha256
|
|
504
|
+
},
|
|
505
|
+
snapshotEvidence: {
|
|
506
|
+
path: snapshotEvidencePath,
|
|
507
|
+
sha256: snapshotEvidenceSha256
|
|
508
|
+
},
|
|
509
|
+
outcome,
|
|
510
|
+
...(stopFailure === undefined
|
|
511
|
+
? {}
|
|
512
|
+
: { logcatStopFailure: stopFailure })
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
await this.markRecovery(session.id, inFlight);
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
if (isCancelled(input.signal)) {
|
|
520
|
+
await this.markRecovery(session.id, inFlight);
|
|
521
|
+
return cancellation();
|
|
522
|
+
}
|
|
523
|
+
if (outcome.status === "succeeded") {
|
|
524
|
+
const next = GenerationSessionSchema.parse({
|
|
525
|
+
...begun,
|
|
526
|
+
revision: begun.revision + 1,
|
|
527
|
+
inFlight: null,
|
|
528
|
+
candidateSteps: [...begun.candidateSteps, outcome.step],
|
|
529
|
+
candidateSources: [...begun.candidateSources, source]
|
|
530
|
+
});
|
|
531
|
+
if (isCancelled(input.signal)) {
|
|
532
|
+
await this.markRecovery(session.id, inFlight);
|
|
533
|
+
return cancellation();
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
await this.dependencies.store.completeStep(session.id, begun.revision, inFlight, next);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
let latest;
|
|
540
|
+
try {
|
|
541
|
+
latest = GenerationSessionSchema.parse(await this.dependencies.store.read(session.id));
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
throw new GenerationOperationError("RECOVERY_REQUIRED", "Unable to reconcile step completion state");
|
|
545
|
+
}
|
|
546
|
+
if (sameSession(latest, next)) {
|
|
547
|
+
return outcome;
|
|
548
|
+
}
|
|
549
|
+
if (latest.state === "active"
|
|
550
|
+
&& JSON.stringify(latest.inFlight) === JSON.stringify(inFlight)) {
|
|
551
|
+
await this.markRecovery(session.id, inFlight);
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
throw new GenerationOperationError("RECOVERY_REQUIRED", "Step completion state is ambiguous and requires recovery");
|
|
555
|
+
}
|
|
556
|
+
return outcome;
|
|
557
|
+
}
|
|
558
|
+
await this.markRecovery(session.id, inFlight);
|
|
559
|
+
return outcome;
|
|
560
|
+
};
|
|
561
|
+
async observeLive(session, expectedPid, expectedActivity, signal, authoritativeSnapshot, timeoutMs = this.dependencies.idle.timeoutMs) {
|
|
562
|
+
const deadline = this.dependencies.clock.now() + timeoutMs;
|
|
563
|
+
const identity = () => ({
|
|
564
|
+
packageName: session.target.packageName,
|
|
565
|
+
deviceSerial: session.target.deviceSerial,
|
|
566
|
+
...(signal === undefined ? {} : { signal }),
|
|
567
|
+
timeoutMs: Math.max(1, deadline - this.dependencies.clock.now())
|
|
568
|
+
});
|
|
569
|
+
const foreground = await this.dependencies.adb.foregroundComponent(identity());
|
|
570
|
+
throwIfCancelled(signal);
|
|
571
|
+
if (foreground.packageName !== session.target.packageName) {
|
|
572
|
+
fail("PACKAGE_ESCAPE", "Foreground package escaped generation target");
|
|
573
|
+
}
|
|
574
|
+
const processes = await this.dependencies.adb.appProcesses(identity());
|
|
575
|
+
const pid = primaryAppPid(processes, session.target.packageName);
|
|
576
|
+
throwIfCancelled(signal);
|
|
577
|
+
if (pid === null || pid !== expectedPid) {
|
|
578
|
+
fail("APP_CRASHED", "Generation process identity changed");
|
|
579
|
+
}
|
|
580
|
+
if (expectedActivity !== undefined
|
|
581
|
+
&& foreground.activity !== expectedActivity) {
|
|
582
|
+
fail("SNAPSHOT_STALE", "Generation Activity changed unexpectedly");
|
|
583
|
+
}
|
|
584
|
+
const layout = await this.dependencies.androidCli.layout({
|
|
585
|
+
deviceSerial: session.target.deviceSerial,
|
|
586
|
+
...(signal === undefined ? {} : { signal }),
|
|
587
|
+
timeoutMs: Math.max(1, deadline - this.dependencies.clock.now())
|
|
588
|
+
});
|
|
589
|
+
throwIfCancelled(signal);
|
|
590
|
+
const confirmedForeground = await this.dependencies.adb
|
|
591
|
+
.foregroundComponent(identity());
|
|
592
|
+
throwIfCancelled(signal);
|
|
593
|
+
if (confirmedForeground.packageName !== session.target.packageName) {
|
|
594
|
+
fail("PACKAGE_ESCAPE", "Foreground package escaped generation target");
|
|
595
|
+
}
|
|
596
|
+
const confirmedPid = primaryAppPid(await this.dependencies.adb.appProcesses(identity()), session.target.packageName);
|
|
597
|
+
throwIfCancelled(signal);
|
|
598
|
+
if (confirmedPid === null || confirmedPid !== expectedPid) {
|
|
599
|
+
fail("APP_CRASHED", "Generation process identity changed");
|
|
600
|
+
}
|
|
601
|
+
if (confirmedForeground.activity
|
|
602
|
+
!== (expectedActivity ?? foreground.activity)) {
|
|
603
|
+
fail("SNAPSHOT_STALE", "Generation Activity changed unexpectedly");
|
|
604
|
+
}
|
|
605
|
+
if (authoritativeSnapshot !== undefined) {
|
|
606
|
+
const liveHash = hashRuntimeSnapshot({
|
|
607
|
+
...authoritativeSnapshot,
|
|
608
|
+
foregroundPackageName: foreground.packageName,
|
|
609
|
+
activity: foreground.activity,
|
|
610
|
+
pid,
|
|
611
|
+
layout
|
|
612
|
+
});
|
|
613
|
+
const authoritativeHash = hashRuntimeSnapshot(authoritativeSnapshot);
|
|
614
|
+
if (liveHash !== authoritativeHash) {
|
|
615
|
+
fail("SNAPSHOT_STALE", `Generation Layout changed before action (${authoritativeHash} -> ${liveHash})`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
foregroundPackageName: foreground.packageName,
|
|
620
|
+
activity: foreground.activity,
|
|
621
|
+
pid,
|
|
622
|
+
pids: appProcessPids(processes),
|
|
623
|
+
layout
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
async assertForegroundIdentity(session, expectedPid, expectedActivity, signal, timeoutMs = this.dependencies.idle.timeoutMs) {
|
|
627
|
+
const deadline = this.dependencies.clock.now() + timeoutMs;
|
|
628
|
+
const identity = () => ({
|
|
629
|
+
packageName: session.target.packageName,
|
|
630
|
+
deviceSerial: session.target.deviceSerial,
|
|
631
|
+
...(signal === undefined ? {} : { signal }),
|
|
632
|
+
timeoutMs: Math.max(1, deadline - this.dependencies.clock.now())
|
|
633
|
+
});
|
|
634
|
+
const foreground = await this.dependencies.adb.foregroundComponent(identity());
|
|
635
|
+
throwIfCancelled(signal);
|
|
636
|
+
if (foreground.packageName !== session.target.packageName) {
|
|
637
|
+
fail("PACKAGE_ESCAPE", "Foreground package escaped generation target");
|
|
638
|
+
}
|
|
639
|
+
if (expectedActivity !== undefined
|
|
640
|
+
&& foreground.activity !== expectedActivity) {
|
|
641
|
+
fail("SNAPSHOT_STALE", "Generation Activity changed before mutation");
|
|
642
|
+
}
|
|
643
|
+
const pid = primaryAppPid(await this.dependencies.adb.appProcesses(identity()), session.target.packageName);
|
|
644
|
+
throwIfCancelled(signal);
|
|
645
|
+
if (pid === null || pid !== expectedPid) {
|
|
646
|
+
fail("APP_CRASHED", "Generation process identity changed");
|
|
647
|
+
}
|
|
648
|
+
return foreground.activity;
|
|
649
|
+
}
|
|
650
|
+
async markRecovery(generationId, inFlight) {
|
|
651
|
+
const latest = GenerationSessionSchema.parse(await this.dependencies.store.read(generationId));
|
|
652
|
+
if (latest.state === "recoveryRequired"
|
|
653
|
+
&& JSON.stringify(latest.inFlight) === JSON.stringify(inFlight)) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (latest.state !== "active"
|
|
657
|
+
|| JSON.stringify(latest.inFlight) !== JSON.stringify(inFlight)) {
|
|
658
|
+
throw new GenerationOperationError("RECOVERY_REQUIRED", "Step execution state changed before recovery could be persisted");
|
|
659
|
+
}
|
|
660
|
+
const recovery = GenerationSessionSchema.parse({
|
|
661
|
+
...latest,
|
|
662
|
+
revision: latest.revision + 1,
|
|
663
|
+
state: "recoveryRequired",
|
|
664
|
+
inFlight
|
|
665
|
+
});
|
|
666
|
+
await this.dependencies.store.update(generationId, latest.revision, recovery);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type GenerationSession } from "../../domain/generation.js";
|
|
2
|
+
import { type ProposedStep } from "../../domain/proposed-step.js";
|
|
3
|
+
import { type RuntimeSnapshot } from "../../domain/runtime-snapshot.js";
|
|
4
|
+
export interface ProposedStepValidationInput {
|
|
5
|
+
session: GenerationSession;
|
|
6
|
+
snapshot: RuntimeSnapshot;
|
|
7
|
+
proposal: ProposedStep;
|
|
8
|
+
}
|
|
9
|
+
export declare class ProposedStepValidator {
|
|
10
|
+
validate(input: ProposedStepValidationInput): ProposedStep;
|
|
11
|
+
}
|