prime-agent-dsh 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import {
3
+ closeSync, chmodSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync,
4
+ realpathSync, renameSync, rmSync, writeFileSync,
5
+ } from "node:fs";
6
+ import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
7
+ import type { BeforeAgentStartEvent, ContextEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ buildInheritanceCapsule, canonicalJsonDigest, renderInheritanceCapsule, validateCapsule,
10
+ validateCapsuleLineage, type InheritanceCapsuleV1, type InheritanceSourceRecord,
11
+ } from "./rlm-context-inheritance.js";
12
+
13
+ export const INHERITED_CONTEXT_CUSTOM_TYPE = "prime-agent-dsh/inherited-context-v1" as const;
14
+ export const INHERITANCE_PAYLOAD_VERSION = "prime-agent-dsh/inheritance-payload-v3" as const;
15
+ export const ROOT_INHERITANCE_GENERATION = -1 as const;
16
+ export const INHERITANCE_ADMISSION_VERSION = "prime-agent-dsh/inheritance-admission-v3" as const;
17
+ const PIN_VERSION = "prime-agent-dsh/observed-parent-pin-v2" as const;
18
+ const HEAD_VERSION = "prime-agent-dsh/inheritance-head-v1" as const;
19
+ const SESSION_BINDING_VERSION = "prime-agent-dsh/session-binding-v1" as const;
20
+ const DIRECTORY = "dsh-inheritance";
21
+ const MAX_PARENT_BYTES = 16 * 1024 * 1024;
22
+ const MAX_ARTIFACT_BYTES = 256 * 1024;
23
+ const MAX_PIN_RECORDS = 256;
24
+ const MAX_PIN_TEXT_BYTES = 512 * 1024;
25
+
26
+ type JsonObject = Record<string, unknown>;
27
+ type Header = { id?: string; parentSession?: string; rlmDepth?: number };
28
+ export interface ObservedParentPin {
29
+ readonly version: typeof PIN_VERSION;
30
+ readonly childSessionId: string; readonly childSessionFile: string;
31
+ readonly parentSessionId: string; readonly parentSessionFile: string;
32
+ readonly depth: number; readonly observedLeafId: string | null;
33
+ readonly sourceBytes: number; readonly sourceDigest: string; readonly branchDigest: string;
34
+ /** Only bounded, redacted, eligible records. Never the raw parent branch. */
35
+ readonly records: readonly InheritanceSourceRecord[];
36
+ readonly parentCapsule?: InheritanceCapsuleV1;
37
+ readonly ancestorSessionIds: readonly string[];
38
+ }
39
+ export interface InheritancePayload { readonly version: typeof INHERITANCE_PAYLOAD_VERSION; readonly capsule: InheritanceCapsuleV1 }
40
+ export interface InheritanceAdmission {
41
+ readonly version: typeof INHERITANCE_ADMISSION_VERSION;
42
+ readonly childSessionId: string; readonly childSessionFile: string;
43
+ readonly parentSessionId: string; readonly parentSessionFile: string;
44
+ readonly depth: number; readonly observedLeafId: string | null;
45
+ readonly pinDigest: string; readonly taskDigest: string; readonly taskText: string; readonly taskImageDigest: string; readonly payloadDigest: string;
46
+ readonly contentDigest: string; readonly generation: string; readonly digest: string;
47
+ }
48
+ interface ArtifactHead { readonly version: typeof HEAD_VERSION; readonly state: "PINNED" | "ADMITTED" | "OBSERVED"; readonly generation: string; readonly digest: string }
49
+ export type InheritanceStatus =
50
+ | { readonly state: "root" }
51
+ | { readonly state: "pinned"; readonly pin: ObservedParentPin }
52
+ | { readonly state: "admitted"; readonly capsule: InheritanceCapsuleV1; readonly admission: InheritanceAdmission }
53
+ | { readonly state: "observed"; readonly capsule: InheritanceCapsuleV1; readonly admission: InheritanceAdmission }
54
+ | { readonly state: "degraded" | "incompatible"; readonly reason: string };
55
+
56
+ type Runtime = { prompt?: string; taskDigest?: string; content?: string; admission?: InheritanceAdmission };
57
+ const object = (x: unknown): x is JsonObject => typeof x === "object" && x !== null && !Array.isArray(x);
58
+ const sha = (x: string | Buffer): string => createHash("sha256").update(x).digest("hex");
59
+ const safeId = (x: unknown): x is string => typeof x === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(x);
60
+ const lstatExists = (path: string): boolean => { try { lstatSync(path); return true; } catch (e) { if ((e as NodeJS.ErrnoException).code === "ENOENT") return false; throw e; } };
61
+
62
+ function header(ctx: ExtensionContext): Header { const raw = ctx.sessionManager.getHeader?.(); return object(raw) ? raw : {}; }
63
+ function ensureDirectory(path: string): void {
64
+ mkdirSync(path, { recursive: true, mode: 0o700 });
65
+ const stat = lstatSync(path); if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`unsafe inheritance directory: ${path}`); chmodSync(path, 0o700);
66
+ }
67
+ function syncDirectory(path: string): void {
68
+ try { const fd = openSync(path, "r"); try { fsyncSync(fd); } finally { closeSync(fd); } }
69
+ catch (error) { const code = (error as NodeJS.ErrnoException).code; if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EISDIR") throw error; }
70
+ }
71
+ function writeExclusive(path: string, raw: string): void { const fd = openSync(path, "wx", 0o600); try { writeFileSync(fd, raw, "utf8"); fsyncSync(fd); } finally { closeSync(fd); } }
72
+ function atomicReplace(path: string, raw: string): void {
73
+ ensureDirectory(dirname(path)); const temp = join(dirname(path), `.${basename(path)}.${randomBytes(8).toString("hex")}.tmp`);
74
+ try { writeExclusive(temp, raw); renameSync(temp, path); syncDirectory(dirname(path)); } catch (error) { rmSync(temp, { force: true }); throw error; }
75
+ }
76
+ function safeJson(path: string): JsonObject {
77
+ const stat = lstatSync(path); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_ARTIFACT_BYTES) throw new Error(`unsafe inheritance artifact: ${basename(path)}`);
78
+ const value: unknown = JSON.parse(readFileSync(path, "utf8")); if (!object(value)) throw new Error(`invalid inheritance artifact: ${basename(path)}`); return value;
79
+ }
80
+ function rootFor(file: string): string { return join(dirname(file), DIRECTORY); }
81
+ function sessionBinding(file: string, id: string, h: Header): JsonObject {
82
+ return { version: SESSION_BINDING_VERSION, sessionId: id, sessionFile: realpathSync(file), depth: Number.isSafeInteger(h.rlmDepth) ? h.rlmDepth : 0, parentSessionFile: h.parentSession ? realpathSync(resolve(dirname(realpathSync(file)), h.parentSession)) : null };
83
+ }
84
+ function ensureSessionBinding(file: string, id: string, h: Header): void {
85
+ const root = rootFor(file); ensureDirectory(root); const path = join(root, "SESSION.json"); const expected = sessionBinding(file, id, h); const raw = `${JSON.stringify(expected)}\n`;
86
+ if (lstatExists(path)) { const actual = safeJson(path); if (canonicalJsonDigest(actual) !== canonicalJsonDigest(expected)) throw new Error("DSH session binding mismatch"); }
87
+ else atomicReplace(path, raw);
88
+ }
89
+ function validateParentSessionBinding(file: string, id: string, h: Header): void {
90
+ const actual = safeJson(join(rootFor(file), "SESSION.json")); const expected = sessionBinding(file, id, h);
91
+ if (canonicalJsonDigest(actual) !== canonicalJsonDigest(expected)) throw new Error("parent DSH session binding mismatch");
92
+ }
93
+ function publish(root: string, state: "PINNED" | "ADMITTED" | "OBSERVED", files: Readonly<Record<string, unknown>>, digest: string): string {
94
+ ensureDirectory(root); const generations = join(root, "generations"); ensureDirectory(generations);
95
+ const generation = `${state.toLowerCase()}-${digest}`; const target = join(generations, generation);
96
+ if (!lstatExists(target)) {
97
+ const stage = join(generations, `.${generation}.${randomBytes(8).toString("hex")}.tmp`); mkdirSync(stage, { mode: 0o700 });
98
+ try { for (const [name, value] of Object.entries(files)) writeExclusive(join(stage, name), `${JSON.stringify(value)}\n`); syncDirectory(stage); renameSync(stage, target); syncDirectory(generations); }
99
+ catch (error) { rmSync(stage, { recursive: true, force: true }); throw error; }
100
+ } else {
101
+ const stat = lstatSync(target); if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("inheritance generation collision is unsafe");
102
+ for (const [name, value] of Object.entries(files)) {
103
+ const expected = `${JSON.stringify(value)}\n`; const item = join(target, name); const itemStat = lstatSync(item);
104
+ if (!itemStat.isFile() || itemStat.isSymbolicLink() || itemStat.size !== Buffer.byteLength(expected) || readFileSync(item, "utf8") !== expected) throw new Error("corrupt inheritance generation collision");
105
+ }
106
+ }
107
+ const head: ArtifactHead = { version: HEAD_VERSION, state, generation, digest };
108
+ atomicReplace(join(root, "HEAD"), `${JSON.stringify(head)}\n`); return generation;
109
+ }
110
+ function readHead(file: string): { head: ArtifactHead; dir: string } {
111
+ const root = rootFor(file); const raw = safeJson(join(root, "HEAD")) as unknown as ArtifactHead;
112
+ if (raw.version !== HEAD_VERSION || (raw.state !== "PINNED" && raw.state !== "ADMITTED" && raw.state !== "OBSERVED") || !/^[a-z]+-[a-f0-9]{64}$/.test(raw.generation) || !/^[a-f0-9]{64}$/.test(raw.digest)) throw new Error("invalid inheritance head");
113
+ const dir = join(root, "generations", raw.generation); const rel = relative(root, dir); if (rel.startsWith(`..${sep}`) || rel === "..") throw new Error("unsafe inheritance head");
114
+ const stat = lstatSync(dir); if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("unsafe inheritance generation"); return { head: raw, dir };
115
+ }
116
+ function stableRead(path: string): Buffer {
117
+ const real = realpathSync(path); if (real !== resolve(path) || extname(real) !== ".jsonl") throw new Error("parent session path is not a canonical JSONL file");
118
+ for (let attempt = 0; attempt < 3; attempt++) { const fd = openSync(real, "r"); try { const before = fstatSync(fd); if (!before.isFile() || before.size > MAX_PARENT_BYTES) throw new Error("parent session file is unsafe or too large"); const data = readFileSync(fd); const after = fstatSync(fd); if (before.ino === after.ino && before.dev === after.dev && before.size === after.size && before.mtimeMs === after.mtimeMs && data.length === after.size) return data; } finally { closeSync(fd); } }
119
+ throw new Error("parent session changed while it was pinned");
120
+ }
121
+ function activeBranch(entries: readonly unknown[]): readonly JsonObject[] {
122
+ const nodes = entries.filter((x): x is JsonObject => object(x) && x.type !== "session" && typeof x.id === "string"); if (!nodes.length) return [];
123
+ const byId = new Map(nodes.map(x => [x.id as string, x])); const output: JsonObject[] = []; const seen = new Set<string>(); let cursor: JsonObject | undefined = nodes.at(-1);
124
+ while (cursor) { const id = cursor.id as string; if (seen.has(id)) throw new Error("parent session branch contains a cycle"); seen.add(id); output.push(cursor); const p = cursor.parentId; cursor = typeof p === "string" ? byId.get(p) : undefined; }
125
+ return output.reverse();
126
+ }
127
+ function plainText(content: unknown): string {
128
+ if (typeof content === "string") return content;
129
+ if (!Array.isArray(content)) return "";
130
+ return content.flatMap(x => object(x) && x.type === "text" && typeof x.text === "string" ? [x.text] : []).join("\n");
131
+ }
132
+ function redact(value: string): string {
133
+ let output = value;
134
+ const replacements: Array<[RegExp, string]> = [
135
+ [/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/gu, "[REDACTED PRIVATE KEY]"],
136
+ [/\bBasic\s+[A-Za-z0-9+/=]{8,}/giu, "Basic [REDACTED]"],
137
+ [/\bBearer\s+[^\s,;]+/giu, "Bearer [REDACTED]"],
138
+ [/\bAKIA[0-9A-Z]{16}\b/gu, "[REDACTED AWS ACCESS KEY]"],
139
+ [/\b(AWS_SESSION_TOKEN|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY|GOOGLE_API_KEY|GITHUB_TOKEN|GH_TOKEN|NPM_TOKEN|DATABASE_URL|COOKIE|SET_COOKIE)\s*[:=]\s*[^\s,;]+/giu, "$1=[REDACTED]"],
140
+ [/(api[_-]?key|access[_-]?token|refresh[_-]?token|session[_-]?token|secret|password|passwd|cookie)\s*[:=]\s*[^\s,;]+/giu, "$1=[REDACTED]"],
141
+ [/([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/giu, "$1[REDACTED]@"],
142
+ [/\b(Cookie|Set-Cookie)\s*:\s*[^\r\n]+/giu, "$1: [REDACTED]"],
143
+ ];
144
+ for (const [pattern, replacement] of replacements) output = output.replace(pattern, replacement);
145
+ // Treat long high-entropy assignment values as secrets even when the name is unknown.
146
+ output = output.replace(/\b([A-Za-z_][A-Za-z0-9_.-]{1,64}\s*[:=]\s*)([A-Za-z0-9+/_=-]{32,})/gu,
147
+ (_match, prefix: string, candidate: string) => new Set(candidate).size >= 12 ? `${prefix}[REDACTED]` : `${prefix}${candidate}`);
148
+ return output;
149
+ }
150
+ function eligibleRecords(branch: readonly unknown[]): InheritanceSourceRecord[] {
151
+ const output: InheritanceSourceRecord[] = []; let bytes = 0;
152
+ for (let index = branch.length - 1; index >= 0 && output.length < MAX_PIN_RECORDS; index--) {
153
+ const raw = branch[index]; if (!object(raw)) continue;
154
+ let kind: "user" | "assistant" | "summary" | undefined; let value: string;
155
+ if (raw.type === "message" && object(raw.message) && (raw.message.role === "user" || raw.message.role === "assistant")) { kind = raw.message.role; value = plainText(raw.message.content); }
156
+ else if ((raw.type === "branch_summary" || raw.type === "compaction") && typeof raw.summary === "string") { kind = "summary"; value = raw.summary; }
157
+ else continue;
158
+ if (raw.customType === INHERITED_CONTEXT_CUSTOM_TYPE || !value) continue;
159
+ const cleaned = redact(value); const size = Buffer.byteLength(cleaned); if (bytes + size > MAX_PIN_TEXT_BYTES) continue;
160
+ bytes += size; output.push({ id: typeof raw.id === "string" ? raw.id : `entry-${index}`, kind, text: cleaned });
161
+ }
162
+ return output.reverse();
163
+ }
164
+ function rank(records: readonly InheritanceSourceRecord[], task: string): InheritanceSourceRecord[] {
165
+ const query = new Set(task.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []);
166
+ return records.map((x, index) => ({ ...x, priority: (index >= records.length - 2 ? 1000 : 0) + (x.text.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []).filter(t => query.has(t)).length * 100 + index }));
167
+ }
168
+ function imageDigest(images: unknown): string { return canonicalJsonDigest(images ?? []); }
169
+ function taskDigest(prompt: string, images: unknown): string { return canonicalJsonDigest({ prompt, imageDigest: imageDigest(images) }); }
170
+ function customMessages(branch: readonly unknown[]): JsonObject[] { return branch.filter((raw): raw is JsonObject => object(raw) && raw.type === "custom_message" && raw.customType === INHERITED_CONTEXT_CUSTOM_TYPE); }
171
+
172
+ /** Stock Prime 0.9.5 public-hook inheritance. It does not claim spawn-transaction atomicity. */
173
+ export class RlmContextInheritance {
174
+ private readonly statuses = new Map<string, InheritanceStatus>(); private readonly runtime = new Map<string, Runtime>();
175
+ register(pi: Pick<ExtensionAPI, "on">): void {
176
+ pi.on("session_start", async (event, ctx) => { await Promise.resolve(); this.start(ctx, event.reason); });
177
+ pi.on("before_agent_start", (event, ctx) => this.beforeStart(event.prompt, ctx, event.images));
178
+ pi.on("context", (event, ctx) => this.position(event.messages, ctx));
179
+ pi.on("message_end", (_event, ctx) => { this.observe(ctx); });
180
+ pi.on("agent_end", (_event, ctx) => { this.observe(ctx); });
181
+ pi.on("session_shutdown", async (_event, ctx) => { await Promise.resolve(); if (ctx?.sessionManager) { const id = this.id(ctx); this.statuses.delete(id); this.runtime.delete(id); } else { this.statuses.clear(); this.runtime.clear(); } });
182
+ }
183
+ compatibility(): "best-effort-public-hooks" { return "best-effort-public-hooks"; }
184
+ status(ctx: ExtensionContext): InheritanceStatus { return this.statuses.get(this.id(ctx)) ?? { state: "incompatible", reason: "session_start has not initialized DSH inheritance" }; }
185
+
186
+ start(ctx: ExtensionContext, reason = "startup"): InheritanceStatus {
187
+ const id = this.id(ctx); const file = ctx.sessionManager.getSessionFile?.(); const h = header(ctx);
188
+ if (!file || !safeId(id)) return this.save(id, { state: "incompatible", reason: "persistent Prime session identity is unavailable" });
189
+ const parent = h.parentSession; const headerDepth = Number.isSafeInteger(h.rlmDepth) ? h.rlmDepth : undefined;
190
+ if (!parent && (headerDepth === 0 || headerDepth === undefined)) { try { ensureSessionBinding(file, id, { ...h, rlmDepth: 0 }); return this.save(id, { state: "root" }); } catch (error) { return this.degrade(ctx, error instanceof Error ? error.message : String(error)); } }
191
+ if (!parent || headerDepth === undefined || headerDepth < 1) return this.degrade(ctx, "inconsistent Prime descendant header");
192
+ try {
193
+ const childFile = realpathSync(file); const requestedParent = resolve(dirname(childFile), parent); const parentFile = realpathSync(requestedParent);
194
+ if (childFile === parentFile) throw new Error("parent session path is self-referential");
195
+ const bytes = stableRead(parentFile); const entries = bytes.toString("utf8").split(/\r?\n/u).filter(Boolean).map(line => JSON.parse(line) as unknown);
196
+ const headers = entries.filter((entry): entry is JsonObject => object(entry) && entry.type === "session"); if (headers.length !== 1) throw new Error("parent session must contain exactly one header");
197
+ const parentHeader = headers[0] as Header; if (!safeId(parentHeader.id) || parentHeader.id === id) throw new Error("invalid parent session header");
198
+ validateParentSessionBinding(parentFile, parentHeader.id, parentHeader);
199
+ const branch = activeBranch(entries); const leaf = branch.at(-1); const observedLeafId = leaf && typeof leaf.id === "string" ? leaf.id : null;
200
+ let parentCapsule: InheritanceCapsuleV1 | undefined; let ancestorSessionIds = [parentHeader.id];
201
+ if (parentHeader.parentSession || (Number.isSafeInteger(parentHeader.rlmDepth) && (parentHeader.rlmDepth as number) > 0)) {
202
+ const chain = this.readAndValidateChain(parentFile, parentHeader.id); parentCapsule = chain.capsule; ancestorSessionIds = chain.ancestors;
203
+ }
204
+ if (parentCapsule && parentCapsule.generation + 2 !== headerDepth) throw new Error("descendant depth does not match validated capsule generation");
205
+ if (!parentCapsule && headerDepth !== 1) throw new Error("descendant depth lacks validated intermediate lineage");
206
+ const pinBase = { version: PIN_VERSION, childSessionId: id, childSessionFile: childFile, parentSessionId: parentHeader.id, parentSessionFile: parentFile, depth: headerDepth,
207
+ observedLeafId, sourceBytes: bytes.length, sourceDigest: sha(bytes), branchDigest: canonicalJsonDigest(branch), records: eligibleRecords(branch), ...(parentCapsule ? { parentCapsule } : {}), ancestorSessionIds } as const;
208
+ const pin = pinBase as ObservedParentPin; const root = rootFor(childFile); ensureSessionBinding(childFile, id, { ...h, parentSession: parentFile, rlmDepth: headerDepth });
209
+ if (reason === "resume" || reason === "reload") {
210
+ const existing = this.tryReadAdmitted(childFile, id); if (existing) {
211
+ if (existing.admission.parentSessionId !== pin.parentSessionId || existing.admission.parentSessionFile !== pin.parentSessionFile || existing.admission.depth !== pin.depth) throw new Error("resumed admission parent binding mismatch");
212
+ if (canonicalJsonDigest(existing.pin) !== existing.admission.pinDigest
213
+ || existing.pin.childSessionId !== id || existing.pin.childSessionFile !== childFile
214
+ || existing.pin.parentSessionId !== pin.parentSessionId || existing.pin.parentSessionFile !== pin.parentSessionFile
215
+ || existing.pin.depth !== pin.depth || existing.pin.sourceBytes > bytes.length
216
+ || sha(bytes.subarray(0, existing.pin.sourceBytes)) !== existing.pin.sourceDigest) throw new Error("resumed admission original pin mismatch");
217
+ const content = renderInheritanceCapsule(existing.capsule); const branchNow = (ctx.sessionManager.getBranch?.() ?? []) as readonly unknown[]; const inherited = customMessages(branchNow);
218
+ if (branchNow.length && (inherited.length !== 1 || inherited[0]?.content !== content || !object(inherited[0]?.details) || inherited[0]?.details?.admissionDigest !== existing.admission.digest || inherited[0]?.details?.taskDigest !== existing.admission.taskDigest)) throw new Error("canonical inherited-context message is missing or mismatched");
219
+ this.runtime.set(id, { prompt: existing.admission.taskText, taskDigest: existing.admission.taskDigest, content, admission: existing.admission }); return this.save(id, { state: branchNow.length ? "observed" : "admitted", ...existing });
220
+ }
221
+ if (customMessages(ctx.sessionManager.getBranch?.() ?? []).length) throw new Error("persisted inherited-context message has no admission");
222
+ throw new Error("persisted descendant inheritance admission is missing");
223
+ }
224
+ const pinDigest = canonicalJsonDigest(pin); publish(root, "PINNED", { "pin.json": pin }, pinDigest); return this.save(id, { state: "pinned", pin });
225
+ } catch (error) { return this.degrade(ctx, error instanceof Error ? error.message : String(error)); }
226
+ }
227
+
228
+ beforeStart(prompt: string, ctx: ExtensionContext, images?: BeforeAgentStartEvent["images"]): { message?: { customType: string; content: string; display: boolean; details: JsonObject } } | undefined {
229
+ const id = this.id(ctx); const state = this.status(ctx); if (state.state !== "pinned") return;
230
+ try {
231
+ const pin = state.pin; const capsule = buildInheritanceCapsule({ recipientSessionId: id, parentSessionId: pin.parentSessionId, ...(pin.parentCapsule ? { parentCapsule: pin.parentCapsule } : {}), parentRecords: rank(pin.records, prompt), ancestorSessionIds: pin.ancestorSessionIds,
232
+ limits: { maxEvidenceChars: 768, maxRecords: 6, reserveImmediateChars: 384, reserveImmediateRecords: 3 } });
233
+ const check = validateCapsuleLineage(capsule, { ...(pin.parentCapsule ? { parentCapsule: pin.parentCapsule } : {}), ancestorSessionIds: pin.ancestorSessionIds }); if (!check.ok) throw new Error(check.reason);
234
+ const expectedGeneration = (pin.parentCapsule?.generation ?? ROOT_INHERITANCE_GENERATION) + 1;
235
+ if (capsule.generation !== expectedGeneration || pin.depth !== capsule.generation + 1) throw new Error("capsule generation/header depth invariant mismatch");
236
+ const payload: InheritancePayload = { version: INHERITANCE_PAYLOAD_VERSION, capsule }; const content = renderInheritanceCapsule(capsule); const boundTask = taskDigest(prompt, images);
237
+ const payloadDigest = canonicalJsonDigest(payload); const pinDigest = canonicalJsonDigest(pin); const generationSeed = canonicalJsonDigest({ pinDigest, boundTask, payloadDigest, contentDigest: sha(content) });
238
+ const base = { version: INHERITANCE_ADMISSION_VERSION, childSessionId: id, childSessionFile: pin.childSessionFile, parentSessionId: pin.parentSessionId, parentSessionFile: pin.parentSessionFile, depth: pin.depth,
239
+ observedLeafId: pin.observedLeafId, pinDigest, taskDigest: boundTask, taskText: prompt, taskImageDigest: imageDigest(images), payloadDigest, contentDigest: sha(content), generation: `admitted-${generationSeed}` } as const;
240
+ const admission: InheritanceAdmission = { ...base, digest: canonicalJsonDigest(base) };
241
+ publish(rootFor(pin.childSessionFile), "ADMITTED", { "pin.json": pin, "payload.json": payload, "admission.json": admission }, admission.digest);
242
+ const details: JsonObject = { version: INHERITANCE_PAYLOAD_VERSION, capsuleDigest: capsule.digest, admissionDigest: admission.digest, taskDigest: boundTask };
243
+ this.runtime.set(id, { prompt, taskDigest: boundTask, content, admission }); this.save(id, { state: "admitted", capsule, admission }); return { message: { customType: INHERITED_CONTEXT_CUSTOM_TYPE, content, display: false, details } };
244
+ } catch (error) { this.degrade(ctx, error instanceof Error ? error.message : String(error)); return; }
245
+ }
246
+
247
+ position(messages: ContextEvent["messages"], ctx: ExtensionContext): { messages?: ContextEvent["messages"] } | undefined {
248
+ const run = this.runtime.get(this.id(ctx));
249
+ const typed = messages.filter((message): message is ContextEvent["messages"][number] & { role: "custom"; customType: string; content: string; details?: JsonObject } => message.role === "custom" && message.customType === INHERITED_CONTEXT_CUSTOM_TYPE);
250
+ const without = messages.filter(message => !(message.role === "custom" && message.customType === INHERITED_CONTEXT_CUSTOM_TYPE));
251
+ if (!run?.taskDigest || !run.content || !run.admission) {
252
+ if (typed.length) { this.degrade(ctx, "inherited-context message has no active admission"); return { messages: without }; }
253
+ return;
254
+ }
255
+ const valid = typed.filter(message => typeof message.content === "string" && object(message.details) && message.details.admissionDigest === run.admission!.digest && message.details.taskDigest === run.taskDigest && message.content === run.content);
256
+ if (typed.length !== 1 || valid.length !== 1) {
257
+ this.degrade(ctx, "suspicious or mismatched inherited-context message in request"); return { messages: without };
258
+ }
259
+ const capsule = valid[0];
260
+ // Current turn is the last exact task-shaped message, never the first duplicate prompt in history.
261
+ let target = -1;
262
+ for (let index = without.length - 1; index >= 0; index--) {
263
+ const message = without[index];
264
+ if (message.role === "user" && plainText(message.content) === run.prompt) { target = index; break; }
265
+ if (message.role === "custom" && typeof message.content === "string") {
266
+ const raw = message.content.startsWith("[task from parent]\n\n") ? message.content.slice(20) : message.content;
267
+ if (raw === run.prompt) { target = index; break; }
268
+ }
269
+ }
270
+ if (target < 0) { this.degrade(ctx, "current task is absent or ambiguous"); return { messages: without }; }
271
+ const output = [...without.slice(0, target), capsule, ...without.slice(target)];
272
+ if (output.length === messages.length && output.every((message, index) => message === messages[index])) return;
273
+ return { messages: output };
274
+ }
275
+
276
+ rebuild(ctx: ExtensionContext): InheritanceStatus { return this.start(ctx, "resume"); }
277
+ private observe(ctx: ExtensionContext): void {
278
+ const state = this.status(ctx); if (state.state !== "admitted") return;
279
+ try {
280
+ const id = this.id(ctx); const file = ctx.sessionManager.getSessionFile?.(); if (!file) throw new Error("session file unavailable during observation");
281
+ const branch = (ctx.sessionManager.getBranch?.() ?? []) as readonly unknown[]; const found = customMessages(branch);
282
+ const content = renderInheritanceCapsule(state.capsule); const message = found[0];
283
+ if (found.length !== 1 || message?.content !== content || !object(message?.details)
284
+ || message.details.admissionDigest !== state.admission.digest
285
+ || message.details.taskDigest !== state.admission.taskDigest
286
+ || message.details.capsuleDigest !== state.capsule.digest) throw new Error("persisted inherited context does not match admission");
287
+ let taskMatches = 0;
288
+ for (const entry of branch) {
289
+ if (!object(entry)) continue;
290
+ if (entry.type === "message" && object(entry.message) && entry.message.role === "user" && plainText(entry.message.content) === state.admission.taskText) taskMatches++;
291
+ if (entry.type === "custom_message" && entry.customType !== INHERITED_CONTEXT_CUSTOM_TYPE && typeof entry.content === "string") {
292
+ const raw = entry.content.startsWith("[task from parent]\n\n") ? entry.content.slice(20) : entry.content;
293
+ if (raw === state.admission.taskText) taskMatches++;
294
+ }
295
+ }
296
+ if (taskMatches < 1) throw new Error("observed admission has no bound task turn");
297
+ const admitted = this.readAdmitted(file, id); const payload: InheritancePayload = { version: INHERITANCE_PAYLOAD_VERSION, capsule: state.capsule };
298
+ publish(rootFor(file), "OBSERVED", { "pin.json": admitted.pin, "payload.json": payload, "admission.json": state.admission,
299
+ "observation.json": { admissionDigest: state.admission.digest, contentDigest: sha(content), taskDigest: state.admission.taskDigest, branchDigest: canonicalJsonDigest(branch) } }, state.admission.digest);
300
+ this.save(id, { state: "observed", capsule: state.capsule, admission: state.admission });
301
+ } catch (error) { this.degrade(ctx, error instanceof Error ? error.message : String(error)); }
302
+ }
303
+ private readAndValidateChain(file: string, id: string): { capsule: InheritanceCapsuleV1; ancestors: string[] } {
304
+ const seen = new Set<string>(); const reverse: Array<{ id: string; capsule: InheritanceCapsuleV1; admission: InheritanceAdmission }> = []; let currentFile = file; let currentId = id; let rootId: string | undefined;
305
+ for (;;) {
306
+ if (seen.has(currentId)) throw new Error("inheritance lineage cycle"); seen.add(currentId);
307
+ const bytes = stableRead(currentFile); const entries = bytes.toString("utf8").split(/\r?\n/u).filter(Boolean).map(line => JSON.parse(line) as unknown); const h = entries.find((x): x is JsonObject => object(x) && x.type === "session") as Header | undefined;
308
+ if (!h || h.id !== currentId) throw new Error("lineage session binding mismatch");
309
+ validateParentSessionBinding(currentFile, currentId, h);
310
+ if (!h.parentSession) { if (Number.isSafeInteger(h.rlmDepth) && h.rlmDepth !== 0) throw new Error("lineage root depth mismatch"); rootId = currentId; break; }
311
+ const admitted = this.readAdmitted(currentFile, currentId, true); if (admitted.admission.depth !== admitted.capsule.generation + 1) throw new Error("lineage admission depth mismatch"); reverse.push({ id: currentId, ...admitted });
312
+ const parentFile = realpathSync(resolve(dirname(currentFile), h.parentSession)); if (parentFile !== admitted.admission.parentSessionFile) throw new Error("lineage admission parent file mismatch");
313
+ const pbytes = stableRead(parentFile); const parentHeaders = pbytes.toString("utf8").split(/\r?\n/u).filter(Boolean).map(line => JSON.parse(line) as unknown).filter((x): x is JsonObject => object(x) && x.type === "session");
314
+ if (parentHeaders.length !== 1 || !safeId(parentHeaders[0]?.id) || parentHeaders[0]?.id !== admitted.admission.parentSessionId) throw new Error("invalid lineage parent header"); currentFile = parentFile; currentId = parentHeaders[0].id;
315
+ }
316
+ reverse.reverse(); let prior: InheritanceCapsuleV1 | undefined; if (!rootId) throw new Error("lineage root is unavailable"); const ancestors: string[] = [rootId];
317
+ for (const edge of reverse) { if (edge.capsule.parentSessionId !== ancestors.at(-1)) throw new Error("reparented capsule lineage"); const validation = validateCapsuleLineage(edge.capsule, { ...(prior ? { parentCapsule: prior } : {}), ancestorSessionIds: ancestors }); if (!validation.ok) throw new Error(`invalid capsule lineage: ${validation.reason}`); ancestors.push(edge.id); prior = edge.capsule; }
318
+ if (!prior) throw new Error("descendant lineage has no admission"); return { capsule: prior, ancestors };
319
+ }
320
+ private tryReadAdmitted(file: string, id: string): { pin: ObservedParentPin; capsule: InheritanceCapsuleV1; admission: InheritanceAdmission } | undefined { try { return this.readAdmitted(file, id); } catch (e) { if ((e as NodeJS.ErrnoException).code === "ENOENT") return undefined; throw e; } }
321
+ private readAdmitted(file: string, id: string, requireObserved = false): { pin: ObservedParentPin; capsule: InheritanceCapsuleV1; admission: InheritanceAdmission } {
322
+ const { head, dir } = readHead(file); if ((head.state !== "ADMITTED" && head.state !== "OBSERVED") || (requireObserved && head.state !== "OBSERVED")) throw Object.assign(new Error("inheritance admission is missing"), { code: "ENOENT" });
323
+ const pin = safeJson(join(dir, "pin.json")); const payload = safeJson(join(dir, "payload.json")); const raw = safeJson(join(dir, "admission.json"));
324
+ if (pin.version !== PIN_VERSION || payload.version !== INHERITANCE_PAYLOAD_VERSION || raw.version !== INHERITANCE_ADMISSION_VERSION) throw new Error("unsupported inheritance admission version");
325
+ const capsule = payload.capsule as InheritanceCapsuleV1; const valid = validateCapsule(capsule); if (!valid.ok || capsule.recipientSessionId !== id) throw new Error(valid.ok ? "capsule recipient mismatch" : valid.reason);
326
+ const admission = raw as unknown as InheritanceAdmission; const { digest, ...base } = admission; if (canonicalJsonDigest(base) !== digest || head.digest !== digest) throw new Error("admission/head digest mismatch");
327
+ if (admission.childSessionId !== id || realpathSync(admission.childSessionFile) !== realpathSync(file) || typeof admission.taskText !== "string" || !/^[a-f0-9]{64}$/.test(admission.taskImageDigest) || canonicalJsonDigest({ prompt: admission.taskText, imageDigest: admission.taskImageDigest }) !== admission.taskDigest || canonicalJsonDigest(pin) !== admission.pinDigest || canonicalJsonDigest(payload) !== admission.payloadDigest || sha(renderInheritanceCapsule(capsule)) !== admission.contentDigest) throw new Error("admission binding mismatch");
328
+ return { pin: pin as unknown as ObservedParentPin, capsule, admission };
329
+ }
330
+ private id(ctx: ExtensionContext): string { return ctx.sessionManager.getSessionId?.() ?? ctx.cwd; }
331
+ private save(id: string, status: InheritanceStatus): InheritanceStatus { this.statuses.set(id, status); return status; }
332
+ private degrade(ctx: ExtensionContext, reason: string): InheritanceStatus { ctx.ui?.notify?.(`DSH automatic inherited context degraded: ${reason}`, "warning"); return this.save(this.id(ctx), { state: "degraded", reason }); }
333
+ }