@afokapu/atdd-flow 0.3.53 → 0.3.55
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/package.json +1 -1
- package/src/exact-adoption-recovery.ts +234 -0
- package/src/handover.ts +45 -7
package/package.json
CHANGED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// URN: component:flow:e006:exact-adoption-recovery:backend:integration
|
|
2
|
+
// Tested-By:
|
|
3
|
+
// - test:flow:exact-adoption-resumed-proof-and-relaunch
|
|
4
|
+
// Runtime: bun
|
|
5
|
+
// Purpose: Classify the immutable evidence for one exact Path-A adoption and recovery.
|
|
6
|
+
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
9
|
+
import { basename, join, resolve } from "node:path";
|
|
10
|
+
import { type Seat, paths, readYaml, yaml } from "./core";
|
|
11
|
+
import type { HandoverReplacementRequest } from "./handover";
|
|
12
|
+
import { readRuntimeState } from "./runtime-state";
|
|
13
|
+
import type { Task } from "./tasks";
|
|
14
|
+
|
|
15
|
+
type ReplacementLaunchReceipt = {
|
|
16
|
+
schema?: string;
|
|
17
|
+
seat?: string;
|
|
18
|
+
tasks?: string[];
|
|
19
|
+
pi_session?: string;
|
|
20
|
+
pi_session_path?: string;
|
|
21
|
+
herdr_session?: string;
|
|
22
|
+
pane?: string;
|
|
23
|
+
handover_replacement?: { request?: HandoverReplacementRequest; successor_session?: string };
|
|
24
|
+
relocation?: {
|
|
25
|
+
source_herdr_session?: string;
|
|
26
|
+
source_pane?: string;
|
|
27
|
+
authorization?: { id?: string; hash?: string };
|
|
28
|
+
backup?: string;
|
|
29
|
+
backup_sha256?: string;
|
|
30
|
+
};
|
|
31
|
+
created_at?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type CanonicalSource = NonNullable<NonNullable<HandoverReplacementRequest["supersession"]>["canonical_source"]>;
|
|
35
|
+
type TaskEligibility = (task: Task) => boolean | Promise<boolean>;
|
|
36
|
+
|
|
37
|
+
/** Hash bytes exactly as the immutable records were published. */
|
|
38
|
+
function digest(content: string | Uint8Array) {
|
|
39
|
+
return createHash("sha256").update(content).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Match both the authorization content and its separately stored digest. */
|
|
43
|
+
async function exactAuthorizationMatches(root: string, request: HandoverReplacementRequest) {
|
|
44
|
+
const id = request.authorization.id;
|
|
45
|
+
const folder = join(root, ".atdd-flow", "exact-session-adoption-authorizations");
|
|
46
|
+
try {
|
|
47
|
+
const raw = await readFile(join(folder, `${id}.yaml`), "utf8");
|
|
48
|
+
const storedHash = (await readFile(join(folder, `${id}.sha256`), "utf8")).trim();
|
|
49
|
+
const record = object(yaml.parse<unknown>(raw));
|
|
50
|
+
const source = object(record?.source), target = object(record?.target);
|
|
51
|
+
const observed = {
|
|
52
|
+
storedHash, contentHash: digest(raw), schema: record?.schema, id: record?.id, issuedBy: record?.issued_by,
|
|
53
|
+
seat: record?.seat, session: record?.pi_session, sessionPath: record?.pi_session_path,
|
|
54
|
+
sourceSession: source?.herdr_session, sourcePane: source?.pane,
|
|
55
|
+
targetSession: target?.herdr_session, targetPane: target?.pane, targetCwd: target?.cwd,
|
|
56
|
+
};
|
|
57
|
+
const expected = {
|
|
58
|
+
storedHash: request.authorization.hash, contentHash: request.authorization.hash,
|
|
59
|
+
schema: "atdd-flow/exact-session-adoption-authorization/v1", id, issuedBy: "operator@desk",
|
|
60
|
+
seat: request.seat, session: request.source.pi_session, sessionPath: request.source.pi_session_path,
|
|
61
|
+
sourceSession: request.source.herdr_session, sourcePane: request.source.pane,
|
|
62
|
+
targetSession: request.target.herdr_session, targetPane: request.target.pane, targetCwd: request.target.cwd,
|
|
63
|
+
};
|
|
64
|
+
return JSON.stringify(observed) === JSON.stringify(expected);
|
|
65
|
+
} catch { return false; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function object(value: unknown): Record<string, unknown> | undefined {
|
|
69
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Validate the prior canonical receipt without trusting the active adopted receipt. */
|
|
73
|
+
function receiptMatchesCanonical(receipt: ReplacementLaunchReceipt, seat: string, canonical: CanonicalSource) {
|
|
74
|
+
return receipt.schema === "atdd-flow/pi-runtime-launch-receipt/v1" && receipt.seat === seat
|
|
75
|
+
&& receipt.pi_session === canonical.pi_session && receipt.pi_session_path === canonical.pi_session_path
|
|
76
|
+
&& receipt.herdr_session === canonical.herdr_session && receipt.pane === canonical.pane;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Bind the archived prior receipt to its archived request and successor. */
|
|
80
|
+
function receiptProvesRequest(receipt: ReplacementLaunchReceipt, request: HandoverReplacementRequest, canonical: CanonicalSource) {
|
|
81
|
+
return Array.isArray(receipt.tasks) && receipt.tasks.includes(request.task)
|
|
82
|
+
&& receipt.handover_replacement?.successor_session === canonical.pi_session
|
|
83
|
+
&& JSON.stringify(receipt.handover_replacement.request) === JSON.stringify(request);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validConsumedRequest(request: HandoverReplacementRequest, seat: string) {
|
|
87
|
+
return request.schema === "atdd-flow/context-handover-replacement-request/v1" && request.seat === seat
|
|
88
|
+
&& Boolean(request.task && request.handover?.id && request.handover.sha256);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Rebuild and compare every archived supersession byte and identity. */
|
|
92
|
+
async function exactArchiveMatches(request: HandoverReplacementRequest) {
|
|
93
|
+
const proof = request.supersession;
|
|
94
|
+
if (!proof) return false;
|
|
95
|
+
try {
|
|
96
|
+
const requestPath = join(proof.prior_request.archive, "request.yaml");
|
|
97
|
+
const receiptPath = join(proof.prior_request.archive, "launch-receipt.yaml");
|
|
98
|
+
const manifestPath = join(proof.prior_request.archive, "supersession.yaml");
|
|
99
|
+
const [files, requestBytes, receiptBytes, manifestBytes] = await Promise.all([
|
|
100
|
+
readdir(proof.prior_request.archive), readFile(requestPath, "utf8"),
|
|
101
|
+
readFile(receiptPath, "utf8"), readFile(manifestPath, "utf8"),
|
|
102
|
+
]);
|
|
103
|
+
const archivedRequest = yaml.parse<HandoverReplacementRequest>(requestBytes);
|
|
104
|
+
const archivedReceipt = yaml.parse<ReplacementLaunchReceipt>(receiptBytes);
|
|
105
|
+
const manifest = yaml.print({
|
|
106
|
+
schema: "atdd-flow/context-handover-replacement-supersession/v1", seat: request.seat,
|
|
107
|
+
prior_request: {
|
|
108
|
+
path: proof.prior_request.path, sha256: proof.prior_request.sha256, task: proof.prior_request.task,
|
|
109
|
+
handover: proof.prior_request.handover, handover_sha256: proof.prior_request.handover_sha256,
|
|
110
|
+
},
|
|
111
|
+
prior_receipt: proof.prior_receipt,
|
|
112
|
+
});
|
|
113
|
+
return [
|
|
114
|
+
files.sort().join("\n") === ["launch-receipt.yaml", "request.yaml", "supersession.yaml"].join("\n"),
|
|
115
|
+
basename(proof.prior_request.archive) === proof.prior_request.sha256,
|
|
116
|
+
digest(requestBytes) === proof.prior_request.sha256,
|
|
117
|
+
digest(receiptBytes) === proof.prior_receipt.sha256,
|
|
118
|
+
manifestBytes === manifest,
|
|
119
|
+
validConsumedRequest(archivedRequest, request.seat),
|
|
120
|
+
archivedRequest.task === proof.prior_request.task,
|
|
121
|
+
archivedRequest.handover.id === proof.prior_request.handover,
|
|
122
|
+
archivedRequest.handover.sha256 === proof.prior_request.handover_sha256,
|
|
123
|
+
receiptMatchesCanonical(archivedReceipt, request.seat, proof.canonical_source),
|
|
124
|
+
receiptProvesRequest(archivedReceipt, archivedRequest, proof.canonical_source),
|
|
125
|
+
].every(Boolean);
|
|
126
|
+
} catch { return false; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A single authorization may identify exactly one adoption receipt. */
|
|
130
|
+
async function uniqueAdoptionReceipt(root: string, authorizationId: string, expectedPath: string) {
|
|
131
|
+
try {
|
|
132
|
+
const folder = join(root, ".atdd-flow", "runtime-launch");
|
|
133
|
+
const candidates: string[] = [];
|
|
134
|
+
for (const file of await readdir(folder)) {
|
|
135
|
+
if (!file.endsWith(".yaml")) continue;
|
|
136
|
+
const path = join(folder, file);
|
|
137
|
+
try {
|
|
138
|
+
const receipt = yaml.parse<ReplacementLaunchReceipt>(await readFile(path, "utf8"));
|
|
139
|
+
if (receipt.relocation?.authorization?.id === authorizationId) candidates.push(path);
|
|
140
|
+
} catch { /* Malformed receipts cannot prove exact adoption. */ }
|
|
141
|
+
}
|
|
142
|
+
return candidates.length === 1 && resolve(candidates[0]!) === resolve(expectedPath);
|
|
143
|
+
} catch { return false; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The failed adopted runtime retains source identity at the exact target pane. */
|
|
147
|
+
function exactAdoptionRuntimeMatches(record: Seat, request: HandoverReplacementRequest) {
|
|
148
|
+
const runtime = record.runtime, locator = runtime?.addresses.herdr;
|
|
149
|
+
const observed = {
|
|
150
|
+
supersession: Boolean(request.supersession), application: runtime?.application, wake: runtime?.wake,
|
|
151
|
+
session: runtime?.pi_session, sessionPath: runtime?.pi_session_path,
|
|
152
|
+
herdrSession: typeof locator === "string" ? undefined : locator?.session,
|
|
153
|
+
pane: typeof locator === "string" ? undefined : locator?.pane,
|
|
154
|
+
};
|
|
155
|
+
const expected = {
|
|
156
|
+
supersession: true, application: "herdr", wake: "native",
|
|
157
|
+
session: request.source.pi_session, sessionPath: request.source.pi_session_path,
|
|
158
|
+
herdrSession: request.target.herdr_session, pane: request.target.pane,
|
|
159
|
+
};
|
|
160
|
+
return JSON.stringify(observed) === JSON.stringify(expected);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Require the active request and receipt to describe one exact relocation. */
|
|
164
|
+
function exactAdoptionReceiptMatches(receipt: ReplacementLaunchReceipt, activeBytes: string, address: string, request: HandoverReplacementRequest) {
|
|
165
|
+
const relocation = receipt.relocation;
|
|
166
|
+
return [
|
|
167
|
+
activeBytes === yaml.print(request),
|
|
168
|
+
yaml.print(receipt.handover_replacement?.request) === activeBytes,
|
|
169
|
+
receipt.schema === "atdd-flow/pi-runtime-launch-receipt/v1",
|
|
170
|
+
receipt.seat === address,
|
|
171
|
+
receipt.tasks?.length === 1,
|
|
172
|
+
receipt.tasks?.[0] === request.task,
|
|
173
|
+
receipt.pi_session === request.source.pi_session,
|
|
174
|
+
receipt.pi_session_path === request.source.pi_session_path,
|
|
175
|
+
receipt.herdr_session === request.target.herdr_session,
|
|
176
|
+
receipt.pane === request.target.pane,
|
|
177
|
+
receipt.handover_replacement?.successor_session === request.source.pi_session,
|
|
178
|
+
relocation?.source_herdr_session === request.source.herdr_session,
|
|
179
|
+
relocation?.source_pane === request.source.pane,
|
|
180
|
+
relocation?.authorization?.id === request.authorization.id,
|
|
181
|
+
relocation?.authorization?.hash === request.authorization.hash,
|
|
182
|
+
Boolean(relocation?.backup), Boolean(relocation?.backup_sha256), Boolean(receipt.created_at),
|
|
183
|
+
].every(Boolean);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Prove the immutable recovery copy is the prefix of the adopted source JSONL. */
|
|
187
|
+
async function exactBackupMatches(receipt: ReplacementLaunchReceipt, request: HandoverReplacementRequest) {
|
|
188
|
+
const relocation = receipt.relocation!;
|
|
189
|
+
const [backup, source] = await Promise.all([readFile(relocation.backup!), readFile(request.source.pi_session_path)]);
|
|
190
|
+
return digest(backup) === relocation.backup_sha256
|
|
191
|
+
&& source.length >= backup.length
|
|
192
|
+
&& source.subarray(0, backup.length).equals(backup);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** A live same-identity adoption is fresh only after its receipt and start. */
|
|
196
|
+
function freshAdoptionHeartbeat(runtimeState: Awaited<ReturnType<typeof readRuntimeState>>, receipt: ReplacementLaunchReceipt, address: string, cwd: string) {
|
|
197
|
+
if (!runtimeState) return false;
|
|
198
|
+
return [
|
|
199
|
+
runtimeState.seat === address,
|
|
200
|
+
resolve(runtimeState.cwd) === resolve(cwd),
|
|
201
|
+
Date.parse(runtimeState.started_at) >= Date.parse(receipt.created_at!),
|
|
202
|
+
Date.parse(runtimeState.heartbeat_at) >= Date.parse(runtimeState.started_at),
|
|
203
|
+
].every(Boolean);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Classify only the complete immutable evidence emitted by one exact Path-A
|
|
208
|
+
* adoption. The caller supplies the narrow task lifecycle accepted at its
|
|
209
|
+
* boundary; every receipt, archive, authorization, backup, and runtime check
|
|
210
|
+
* remains shared by adopted RESUMED proof and failed-adoption relaunch.
|
|
211
|
+
* Missing or contradictory evidence always returns no classification.
|
|
212
|
+
*/
|
|
213
|
+
export async function exactFailedAdoptionProof(root: string, record: Seat, address: string, request: HandoverReplacementRequest, taskEligible: TaskEligibility) {
|
|
214
|
+
const receiptPath = record.runtime?.launch_receipt;
|
|
215
|
+
if (!receiptPath || !exactAdoptionRuntimeMatches(record, request)) return undefined;
|
|
216
|
+
try {
|
|
217
|
+
const activePath = join(root, ".atdd-flow", "handover-replacements", `${encodeURIComponent(address)}.yaml`);
|
|
218
|
+
const [activeBytes, receiptBytes, runtimeState, task] = await Promise.all([
|
|
219
|
+
readFile(activePath, "utf8"), readFile(receiptPath, "utf8"), readRuntimeState(root, address),
|
|
220
|
+
readYaml<Task>(paths(root).taskFile(record.project, request.task)),
|
|
221
|
+
]);
|
|
222
|
+
const receipt = yaml.parse<ReplacementLaunchReceipt>(receiptBytes);
|
|
223
|
+
const evidence = [
|
|
224
|
+
exactAdoptionReceiptMatches(receipt, activeBytes, address, request),
|
|
225
|
+
await taskEligible(task),
|
|
226
|
+
await exactAuthorizationMatches(root, request),
|
|
227
|
+
await exactArchiveMatches(request),
|
|
228
|
+
await uniqueAdoptionReceipt(root, request.authorization.id, receiptPath),
|
|
229
|
+
await exactBackupMatches(receipt, request),
|
|
230
|
+
];
|
|
231
|
+
if (!evidence.every(Boolean)) return undefined;
|
|
232
|
+
return { receipt, runtimeState, heartbeat: freshAdoptionHeartbeat(runtimeState, receipt, address, request.target.cwd) };
|
|
233
|
+
} catch { return undefined; }
|
|
234
|
+
}
|
package/src/handover.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
10
10
|
import { type Runtime, type Seat, atomicYaml, canonicalAddress, desk, exists, has, now, paths, project, readYaml, required, runOutput, seat, words, yaml } from "./core";
|
|
11
11
|
import { type Task, terminal } from "./tasks";
|
|
12
12
|
import { resolveExactSessionAdoptionAuthorization } from "./adoption-authorizations";
|
|
13
|
+
import { exactFailedAdoptionProof } from "./exact-adoption-recovery";
|
|
13
14
|
import { readRuntimeState } from "./runtime-state";
|
|
14
15
|
|
|
15
16
|
export type Handover = {
|
|
@@ -211,13 +212,29 @@ export async function mainLaunchEligible(root: string, record: Seat, address: st
|
|
|
211
212
|
&& await isExactMainSeat(root, record, address);
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
function evidenceEmptyRecoveryTask(task: Task) {
|
|
216
|
+
return task.status === "in_progress" && !task.blocker && !task.handoff
|
|
217
|
+
&& task.done_when.every((criterion) => !criterion.proof);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function failedAdoptionTaskEligible(root: string, record: Seat, address: string, task: Task) {
|
|
221
|
+
return (task.status === "todo" || evidenceEmptyRecoveryTask(task))
|
|
222
|
+
&& task.coordinator === address && task.assignee === address
|
|
223
|
+
&& await isExactMainSeat(root, record, address);
|
|
224
|
+
}
|
|
225
|
+
|
|
214
226
|
export async function replacementForMainLaunch(root: string, record: Seat, address: string) {
|
|
215
227
|
if (!await isExactMainSeat(root, record, address)) return undefined;
|
|
216
228
|
const request = await readHandoverReplacement(root, address).catch(() => {
|
|
217
229
|
throw new Error(`${address} requires one validated Path-A replacement request; refusing runtime launch.`);
|
|
218
230
|
});
|
|
219
231
|
const task = await readYaml<Task>(paths(root).taskFile(record.project, request.task));
|
|
220
|
-
|
|
232
|
+
const failedAdoption = await exactFailedAdoptionProof(root, record, address, request,
|
|
233
|
+
(task) => failedAdoptionTaskEligible(root, record, address, task));
|
|
234
|
+
const canonicalLaunch = replacementMatchesRuntime(record.runtime, request)
|
|
235
|
+
&& await mainLaunchEligible(root, record, address, task);
|
|
236
|
+
const failedAdoptionRecovery = Boolean(failedAdoption && !failedAdoption.runtimeState);
|
|
237
|
+
if (!canonicalLaunch && !failedAdoptionRecovery) {
|
|
221
238
|
throw new Error("Path-A replacement source tuple no longer matches the recorded runtime or ready main takeover task.");
|
|
222
239
|
}
|
|
223
240
|
return { request, task };
|
|
@@ -286,7 +303,26 @@ function assertSinglePiWriter(raw: string) {
|
|
|
286
303
|
if (pi.length !== 1) throw new Error("Legacy source bootstrap requires exactly one live Pi writer on the authorized source pane.");
|
|
287
304
|
}
|
|
288
305
|
|
|
289
|
-
|
|
306
|
+
type PiSource = Pick<HandoverReplacementRequest["source"], "pi_session" | "pi_session_path">;
|
|
307
|
+
|
|
308
|
+
function assertCanonicalSourceNotLive(raw: string, canonical: PiSource) {
|
|
309
|
+
const value = JSON.parse(raw), agents = result(value)?.agents;
|
|
310
|
+
if (!Array.isArray(agents)) throw new Error("Consumed Path-A replacement requires complete global Herdr agent-list evidence.");
|
|
311
|
+
for (const entry of agents) {
|
|
312
|
+
const agent = object(entry);
|
|
313
|
+
if (!agent) throw new Error("Consumed Path-A replacement requires complete global Herdr agent-list evidence.");
|
|
314
|
+
if (text(agent.agent) !== "pi") continue;
|
|
315
|
+
const session = object(agent.agent_session);
|
|
316
|
+
const sessionPath = session?.kind === "path" ? text(session.value) : undefined;
|
|
317
|
+
if (!sessionPath) throw new Error("Consumed Path-A replacement requires complete global Herdr agent-list evidence.");
|
|
318
|
+
const sessionId = basename(sessionPath).match(/_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i)?.[1];
|
|
319
|
+
if (resolve(sessionPath) === resolve(canonical.pi_session_path) || sessionId === canonical.pi_session) {
|
|
320
|
+
throw new Error("Consumed Path-A replacement prior canonical source is still a live Pi agent.");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function verifyLegacySource(source: HandoverReplacementRequest["source"], target: HandoverReplacementRequest["target"], authorizationHash: string, dependencies: HandoverReplacementDependencies, canonical?: PiSource) {
|
|
290
326
|
const command = dependencies.command ?? runOutput;
|
|
291
327
|
const pane = legacyPaneObservation(await command(herdr(source.herdr_session, "pane", "get", source.pane)));
|
|
292
328
|
assertLegacyPane(pane, source, target.cwd);
|
|
@@ -296,6 +332,9 @@ async function verifyLegacySource(source: HandoverReplacementRequest["source"],
|
|
|
296
332
|
]);
|
|
297
333
|
const agent = legacyAgentName(agentRaw, source, target.cwd);
|
|
298
334
|
assertSinglePiWriter(processRaw);
|
|
335
|
+
if (canonical && (canonical.pi_session !== source.pi_session || resolve(canonical.pi_session_path) !== resolve(source.pi_session_path))) {
|
|
336
|
+
assertCanonicalSourceNotLive(await command(herdr(source.herdr_session, "agent", "list")), canonical);
|
|
337
|
+
}
|
|
299
338
|
return { authorization_hash: authorizationHash, ...source, cwd: target.cwd, agent, verified_at: dependencies.at?.() ?? now() };
|
|
300
339
|
}
|
|
301
340
|
|
|
@@ -421,9 +460,6 @@ function assertCanonicalPlacement(intent: Awaited<ReturnType<typeof prepareRepla
|
|
|
421
460
|
|| resolve(request.target.cwd) !== resolve(intent.record.worktree)) {
|
|
422
461
|
throw new Error("Consumed Path-A replacement does not match the unchanged canonical main runtime and placement.");
|
|
423
462
|
}
|
|
424
|
-
if (intent.handover.supersedes !== request.handover.id && !canonicalSourceMatchesRuntime(intent.record.runtime, intent.source)) {
|
|
425
|
-
throw new Error("Consumed Path-A replacement does not match the current handover lineage.");
|
|
426
|
-
}
|
|
427
463
|
}
|
|
428
464
|
|
|
429
465
|
function canonicalRuntimeForSupersession(intent: Awaited<ReturnType<typeof prepareReplacement>>, request: HandoverReplacementRequest) {
|
|
@@ -668,7 +704,7 @@ async function supersedeConsumedReplacement(root: string, resolved: string, file
|
|
|
668
704
|
seat: resolved, pi_session: intent.source.pi_session, pi_session_path: intent.source.pi_session_path,
|
|
669
705
|
source: { herdr_session: intent.source.herdr_session, pane: intent.source.pane }, target: intent.target,
|
|
670
706
|
});
|
|
671
|
-
const liveSource = await verifyLegacySource(intent.source, intent.target, authorization.hash, dependencies);
|
|
707
|
+
const liveSource = await verifyLegacySource(intent.source, intent.target, authorization.hash, dependencies, consumed.canonical);
|
|
672
708
|
const request = supersedingRequest(resolved, file, intent, authorization, consumed, liveSource, dependencies);
|
|
673
709
|
const manifest = supersessionManifest(resolved, file, consumed);
|
|
674
710
|
const staging = await stageSupersession(file, request, consumed, manifest, dependencies);
|
|
@@ -720,10 +756,12 @@ export async function resumedHandoverReplacement(root: string, address: string)
|
|
|
720
756
|
const receipt = runtime?.launch_receipt && await exists(runtime.launch_receipt) ? await readYaml<Record<string, unknown>>(runtime.launch_receipt) : undefined;
|
|
721
757
|
const replacement = receipt?.handover_replacement as Record<string, unknown> | undefined;
|
|
722
758
|
const runtimeState = await readRuntimeState(root, resolved);
|
|
759
|
+
const failedAdoption = await exactFailedAdoptionProof(root, record, resolved, request,
|
|
760
|
+
(task) => failedAdoptionTaskEligible(root, record, resolved, task));
|
|
723
761
|
const verified = [
|
|
724
762
|
request.schema === "atdd-flow/context-handover-replacement-request/v1" && request.seat === resolved,
|
|
725
763
|
runtime?.application === "herdr" && runtime.wake === "native",
|
|
726
|
-
runtime?.pi_session !== request.source.pi_session && Boolean(runtime?.pi_session_path),
|
|
764
|
+
runtime?.pi_session !== request.source.pi_session && Boolean(runtime?.pi_session_path) || Boolean(failedAdoption?.heartbeat),
|
|
727
765
|
typeof locator !== "string" && locator?.session === request.target.herdr_session && locator?.pane === request.target.pane,
|
|
728
766
|
replacement?.successor_session === runtime?.pi_session && replacement?.request && JSON.stringify(replacement.request) === JSON.stringify(request),
|
|
729
767
|
Boolean(runtimeState && runtimeState.seat === resolved),
|