@gajae-code/coding-agent 0.18.6 → 0.18.7

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/config/settings.d.ts +4 -0
  4. package/dist/types/discovery/helpers.d.ts +2 -2
  5. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  6. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  7. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  8. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  9. package/dist/types/sdk/host/host.d.ts +6 -0
  10. package/dist/types/sdk/router/session-router.d.ts +7 -0
  11. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  12. package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
  13. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
  14. package/dist/types/session/session-retirement.d.ts +47 -0
  15. package/dist/types/session/session-storage.d.ts +0 -46
  16. package/dist/types/session/session-transcript-header.d.ts +2 -0
  17. package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
  18. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  19. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  20. package/package.json +7 -7
  21. package/scripts/resident-memory-bench.ts +174 -20
  22. package/src/capability/index.ts +1 -0
  23. package/src/capability/types.ts +4 -0
  24. package/src/cli/config-cli.ts +8 -18
  25. package/src/cli-main.ts +7 -8
  26. package/src/commands/sdk.ts +112 -21
  27. package/src/config/settings.ts +44 -4
  28. package/src/discovery/helpers.ts +9 -3
  29. package/src/discovery/ssh.ts +8 -3
  30. package/src/gjc-runtime/gc-runtime.ts +2 -1
  31. package/src/hindsight/config.ts +9 -3
  32. package/src/internal-urls/docs-index.generated.ts +2 -2
  33. package/src/modes/acp/acp-agent.ts +75 -3
  34. package/src/runtime-mcp/client.ts +13 -4
  35. package/src/runtime-mcp/elicitation.ts +142 -0
  36. package/src/sdk/broker/broker.ts +8 -1
  37. package/src/sdk/broker/lifecycle.ts +109 -7
  38. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  39. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  40. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  41. package/src/sdk/bus/index.ts +3 -3
  42. package/src/sdk/bus/telegram-daemon.ts +4 -2
  43. package/src/sdk/host/host.ts +6 -0
  44. package/src/sdk/host/session-runtime.test.ts +43 -2
  45. package/src/sdk/host/session-runtime.ts +2 -2
  46. package/src/sdk/mcp/server.ts +15 -3
  47. package/src/sdk/router/session-router.ts +19 -2
  48. package/src/sdk/session.ts +14 -0
  49. package/src/session/agent-session.ts +57 -6
  50. package/src/session/internal/managed-session-storage.ts +107 -1
  51. package/src/session/internal/task-artifact-owner-access.ts +229 -0
  52. package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
  53. package/src/session/session-retirement.ts +155 -0
  54. package/src/session/session-storage.ts +1 -160
  55. package/src/session/session-transcript-header.ts +17 -0
  56. package/src/session/task-artifact-owner-codec.ts +578 -0
  57. package/src/session/task-artifact-owner-retirement.ts +475 -0
  58. package/src/session/task-artifact-owner.ts +289 -0
@@ -0,0 +1,578 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as path from "node:path";
3
+ import type { NativeExactUnlinkResult } from "@gajae-code/natives";
4
+ import type { ManagedDirectoryRoot, ManagedSessionSecurityPolicy } from "./internal/managed-session-storage";
5
+ import type { NativeDirectoryTreeSnapshot } from "./session-storage";
6
+
7
+ export const OWNER_DIRECTORY = ".task-artifact-owners";
8
+ export const OWNER_LOCK_DIRECTORY = ".task-artifact-owner-locks";
9
+ export const OWNER_MANIFEST = ".gjc-task-artifact-owner-v1.json";
10
+ export const OWNER_SCHEMA_VERSION = 1 as const;
11
+ export const OWNER_DELETION_SCHEMA_VERSION = 2 as const;
12
+ export const OWNER_RETIREMENT_SCHEMA_VERSION = 1 as const;
13
+ export const EMPTY_PAYLOAD_SHA256 = crypto.createHash("sha256").update("").digest("hex");
14
+
15
+ const TREE_ENTRY_KEYS = new Set([
16
+ "relativePath",
17
+ "kind",
18
+ "dev",
19
+ "ino",
20
+ "nlink",
21
+ "size",
22
+ "mtimeNs",
23
+ "ctimeNs",
24
+ "sha256",
25
+ ]);
26
+ const RETIREMENT_CONTINUATION_KEYS = new Set([
27
+ "schemaVersion",
28
+ "parentIdentity",
29
+ "retainedRootPath",
30
+ "retainedTreeSnapshot",
31
+ "detachedPaths",
32
+ "nativeCodes",
33
+ "payloadDurable",
34
+ "retainedSuccessorPaths",
35
+ "retainedPlaceholderPaths",
36
+ "retainedUnknownPaths",
37
+ "windowsErrorCodes",
38
+ ]);
39
+ const U64_MAX = 18_446_744_073_709_551_615n;
40
+
41
+ type NativeDirectoryTreeEntry = NativeDirectoryTreeSnapshot["entries"][number];
42
+
43
+ export interface TaskArtifactOwnerLocator {
44
+ readonly schemaVersion: typeof OWNER_SCHEMA_VERSION;
45
+ readonly ownerId: string;
46
+ readonly directoryDev: string;
47
+ readonly directoryIno: string;
48
+ }
49
+
50
+ export interface TaskArtifactOwnerStorageContext {
51
+ readonly rootAuthority: ManagedDirectoryRoot;
52
+ readonly sessionsRoot: string;
53
+ readonly securityPolicy: ManagedSessionSecurityPolicy;
54
+ readonly profileAgentDir: string;
55
+ }
56
+
57
+ /** Exact parent directory identity authorizing direct native owner-tree removal. */
58
+ export interface TaskArtifactOwnerParentIdentity {
59
+ readonly dev: string;
60
+ readonly ino: string;
61
+ }
62
+
63
+ /** Immutable exact deletion input retained by managed lifecycle tombstones and retries. */
64
+ export interface TaskArtifactOwnerDeletionEvidence {
65
+ readonly schemaVersion: typeof OWNER_DELETION_SCHEMA_VERSION;
66
+ readonly sessionId: string;
67
+ readonly locator: TaskArtifactOwnerLocator;
68
+ readonly parentIdentity: TaskArtifactOwnerParentIdentity;
69
+ readonly treeSnapshot: NativeDirectoryTreeSnapshot;
70
+ }
71
+
72
+ /** Native retained-root and side-path state safe to persist for a later retirement attempt. */
73
+ export interface TaskArtifactOwnerRetirementContinuation {
74
+ readonly schemaVersion: typeof OWNER_RETIREMENT_SCHEMA_VERSION;
75
+ /** Must remain equal to the immutable evidence's managed owner-parent identity. */
76
+ readonly parentIdentity: TaskArtifactOwnerParentIdentity;
77
+ /** Only the original owner root or its native `.removing` sibling is actionable. */
78
+ readonly retainedRootPath: string;
79
+ /** Last identity-checked tree snapshot; it may only shrink from the original evidence. */
80
+ readonly retainedTreeSnapshot: NativeDirectoryTreeSnapshot;
81
+ /** Historical exact native detached-root roles; these paths are diagnostics, not extra authority. */
82
+ readonly detachedPaths?: readonly string[];
83
+ readonly nativeCodes?: readonly string[];
84
+ readonly payloadDurable?: boolean;
85
+ readonly retainedSuccessorPaths?: readonly string[];
86
+ readonly retainedPlaceholderPaths?: readonly string[];
87
+ readonly retainedUnknownPaths?: readonly string[];
88
+ readonly windowsErrorCodes?: readonly string[];
89
+ }
90
+
91
+ /** `completed` is decoded persisted data, not proof of physical retirement or writer closure. */
92
+ export type TaskArtifactOwnerRetirementOutcome =
93
+ | {
94
+ readonly kind: "completed";
95
+ readonly evidence: TaskArtifactOwnerDeletionEvidence;
96
+ readonly nativeOutcome?: NativeExactUnlinkResult;
97
+ }
98
+ | {
99
+ /** Live payload is durably destroyed; native namespace cleanup remains pending. */
100
+ readonly kind: "payload_retired";
101
+ readonly namespace: "retained";
102
+ readonly evidence: TaskArtifactOwnerDeletionEvidence;
103
+ readonly continuation: TaskArtifactOwnerRetirementContinuation;
104
+ readonly nativeOutcome: NativeExactUnlinkResult;
105
+ }
106
+ | {
107
+ /** Native explicitly retained cleanup state with a validated, non-expanding continuation. */
108
+ readonly kind: "cleanup_pending";
109
+ readonly evidence: TaskArtifactOwnerDeletionEvidence;
110
+ readonly continuation: TaskArtifactOwnerRetirementContinuation;
111
+ readonly nativeOutcome?: NativeExactUnlinkResult;
112
+ }
113
+ | {
114
+ /** No safe completion proof exists; callers must retain evidence and continuation. */
115
+ readonly kind: "uncertain";
116
+ readonly evidence: TaskArtifactOwnerDeletionEvidence;
117
+ readonly continuation: TaskArtifactOwnerRetirementContinuation;
118
+ readonly reason: string;
119
+ readonly nativeOutcome?: NativeExactUnlinkResult;
120
+ };
121
+
122
+ function isRecord(value: unknown): value is Record<string, unknown> {
123
+ return typeof value === "object" && value !== null && !Array.isArray(value);
124
+ }
125
+
126
+ /** True only for canonical unsigned decimal strings within the native u64 range. */
127
+ export function isCanonicalDecimal(value: unknown): value is string {
128
+ return typeof value === "string" && /^(0|[1-9][0-9]*)$/u.test(value);
129
+ }
130
+
131
+ export function isCanonicalNativeId(value: unknown): value is string {
132
+ return isCanonicalDecimal(value) && value.length <= 20 && BigInt(value) <= U64_MAX;
133
+ }
134
+
135
+ /** Deterministic owner identity bound to one non-empty logical session ID. */
136
+ export function ownerIdForSession(sessionId: string): string {
137
+ if (sessionId.length === 0) throw new Error("task_artifact_owner_session_id_invalid");
138
+ return crypto.createHash("sha256").update(sessionId, "utf8").digest("hex");
139
+ }
140
+
141
+ export function ownerRelativePath(ownerId: string): string {
142
+ return `${OWNER_DIRECTORY}/${ownerId}`;
143
+ }
144
+
145
+ export function manifestRelativePath(ownerId: string): string {
146
+ return `${ownerRelativePath(ownerId)}/${OWNER_MANIFEST}`;
147
+ }
148
+
149
+ export function assertSafeRelativePath(relativePath: string): void {
150
+ if (
151
+ relativePath.length === 0 ||
152
+ path.posix.isAbsolute(relativePath) ||
153
+ relativePath.split("/").some(component => component.length === 0 || component === "." || component === "..") ||
154
+ relativePath.includes("\\")
155
+ )
156
+ throw new Error("task_artifact_owner_relative_path_invalid");
157
+ }
158
+
159
+ export function assertSessionRoot(context: TaskArtifactOwnerStorageContext): void {
160
+ if (path.resolve(context.sessionsRoot) !== context.sessionsRoot) throw new Error("task_artifact_owner_root_invalid");
161
+ }
162
+
163
+ function parseTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot {
164
+ if (
165
+ !isRecord(value) ||
166
+ Object.keys(value).length !== 3 ||
167
+ !isCanonicalNativeId(value.rootDev) ||
168
+ !isCanonicalNativeId(value.rootIno)
169
+ )
170
+ throw new Error("task_artifact_owner_evidence_invalid");
171
+ if (!Array.isArray(value.entries) || value.entries.length === 0 || value.entries.length > 50_000)
172
+ throw new Error("task_artifact_owner_evidence_invalid");
173
+ const entries: NativeDirectoryTreeEntry[] = [];
174
+ const seen = new Set<string>();
175
+ for (const rawEntry of value.entries) {
176
+ if (
177
+ !isRecord(rawEntry) ||
178
+ Object.keys(rawEntry).length < 8 ||
179
+ Object.keys(rawEntry).length > 9 ||
180
+ typeof rawEntry.relativePath !== "string" ||
181
+ (rawEntry.kind !== "directory" && rawEntry.kind !== "file") ||
182
+ !isCanonicalNativeId(rawEntry.dev) ||
183
+ !isCanonicalNativeId(rawEntry.ino) ||
184
+ !isCanonicalNativeId(rawEntry.nlink) ||
185
+ !isCanonicalNativeId(rawEntry.size) ||
186
+ !isCanonicalDecimal(rawEntry.mtimeNs) ||
187
+ !isCanonicalDecimal(rawEntry.ctimeNs) ||
188
+ (rawEntry.sha256 !== undefined &&
189
+ (typeof rawEntry.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(rawEntry.sha256))) ||
190
+ Object.keys(rawEntry).some(key => !TREE_ENTRY_KEYS.has(key))
191
+ )
192
+ throw new Error("task_artifact_owner_evidence_invalid");
193
+ if (
194
+ rawEntry.relativePath.includes("\\") ||
195
+ (rawEntry.relativePath !== "" &&
196
+ (rawEntry.relativePath.startsWith("/") ||
197
+ rawEntry.relativePath.split("/").some(part => part.length === 0 || part === "." || part === ".."))) ||
198
+ seen.has(rawEntry.relativePath)
199
+ )
200
+ throw new Error("task_artifact_owner_evidence_invalid");
201
+ seen.add(rawEntry.relativePath);
202
+ entries.push({
203
+ relativePath: rawEntry.relativePath,
204
+ kind: rawEntry.kind,
205
+ dev: rawEntry.dev,
206
+ ino: rawEntry.ino,
207
+ nlink: rawEntry.nlink,
208
+ size: rawEntry.size,
209
+ mtimeNs: rawEntry.mtimeNs,
210
+ ctimeNs: rawEntry.ctimeNs,
211
+ ...(typeof rawEntry.sha256 === "string" ? { sha256: rawEntry.sha256 } : {}),
212
+ });
213
+ }
214
+ const root = entries.find(entry => entry.relativePath === "");
215
+ if (root?.kind !== "directory" || root.dev !== value.rootDev || root.ino !== value.rootIno)
216
+ throw new Error("task_artifact_owner_evidence_invalid");
217
+ const entryKinds = new Map(entries.map(entry => [entry.relativePath, entry.kind]));
218
+ for (const entry of entries) {
219
+ if (entry.relativePath === "") continue;
220
+ const separator = entry.relativePath.lastIndexOf("/");
221
+ const parent = separator < 0 ? "" : entry.relativePath.slice(0, separator);
222
+ if (entryKinds.get(parent) !== "directory") throw new Error("task_artifact_owner_evidence_invalid");
223
+ }
224
+ return { rootDev: value.rootDev, rootIno: value.rootIno, entries };
225
+ }
226
+
227
+ export function freezeTreeSnapshot(snapshot: NativeDirectoryTreeSnapshot): NativeDirectoryTreeSnapshot {
228
+ const canonical = parseTreeSnapshot(snapshot);
229
+ for (const entry of canonical.entries) Object.freeze(entry);
230
+ Object.freeze(canonical.entries);
231
+ return Object.freeze(canonical);
232
+ }
233
+
234
+ export function retainedTreeDoesNotExpandAuthority(
235
+ expected: NativeDirectoryTreeSnapshot,
236
+ retained: NativeDirectoryTreeSnapshot,
237
+ ): boolean {
238
+ if (expected.rootDev !== retained.rootDev || expected.rootIno !== retained.rootIno) return false;
239
+ const expectedEntries = new Map(expected.entries.map(entry => [entry.relativePath, entry]));
240
+ if (retained.entries.length > expected.entries.length) return false;
241
+ return retained.entries.every(entry => {
242
+ if (entry.relativePath === "") return entry.kind === "directory";
243
+ const authorized = expectedEntries.get(entry.relativePath);
244
+ if (
245
+ authorized === undefined ||
246
+ authorized.kind !== entry.kind ||
247
+ authorized.dev !== entry.dev ||
248
+ authorized.ino !== entry.ino ||
249
+ authorized.nlink !== entry.nlink
250
+ )
251
+ return false;
252
+ if (entry.kind !== "file") return entry.size === authorized.size;
253
+ const scrubbed = entry.size === "0" && entry.sha256 === EMPTY_PAYLOAD_SHA256;
254
+ if (scrubbed) return true;
255
+ if (entry.size !== authorized.size || entry.sha256 !== authorized.sha256) return false;
256
+ return (
257
+ entry.sha256 !== undefined || (entry.mtimeNs === authorized.mtimeNs && entry.ctimeNs === authorized.ctimeNs)
258
+ );
259
+ });
260
+ }
261
+
262
+ export function sameTreeContents(left: NativeDirectoryTreeSnapshot, right: NativeDirectoryTreeSnapshot): boolean {
263
+ const project = (snapshot: NativeDirectoryTreeSnapshot) =>
264
+ snapshot.entries.map(entry =>
265
+ entry.kind === "directory"
266
+ ? { relativePath: entry.relativePath, kind: entry.kind }
267
+ : { relativePath: entry.relativePath, kind: entry.kind, size: entry.size, sha256: entry.sha256 },
268
+ );
269
+ return JSON.stringify(project(left)) === JSON.stringify(project(right));
270
+ }
271
+
272
+ function parseParentIdentity(value: unknown): TaskArtifactOwnerParentIdentity {
273
+ if (
274
+ !isRecord(value) ||
275
+ Object.keys(value).length !== 2 ||
276
+ !isCanonicalNativeId(value.dev) ||
277
+ !isCanonicalNativeId(value.ino)
278
+ )
279
+ throw new Error("task_artifact_owner_evidence_invalid");
280
+ return Object.freeze({ dev: value.dev, ino: value.ino });
281
+ }
282
+
283
+ export function sameOwnerParentIdentity(
284
+ left: TaskArtifactOwnerParentIdentity,
285
+ right: TaskArtifactOwnerParentIdentity,
286
+ ): boolean {
287
+ return left.dev === right.dev && left.ino === right.ino;
288
+ }
289
+
290
+ export function immutableDeletionEvidence(
291
+ sessionId: string,
292
+ locatorValue: TaskArtifactOwnerLocator,
293
+ parentIdentityValue: TaskArtifactOwnerParentIdentity,
294
+ treeSnapshotValue: NativeDirectoryTreeSnapshot,
295
+ ): TaskArtifactOwnerDeletionEvidence {
296
+ if (sessionId.length === 0) throw new Error("task_artifact_owner_evidence_invalid");
297
+ const locator = parseTaskArtifactOwnerLocator(locatorValue);
298
+ if (!locator || locator.ownerId !== ownerIdForSession(sessionId))
299
+ throw new Error("task_artifact_owner_evidence_invalid");
300
+ const parentIdentity = parseParentIdentity(parentIdentityValue);
301
+ const treeSnapshot = freezeTreeSnapshot(treeSnapshotValue);
302
+ if (treeSnapshot.rootDev !== locator.directoryDev || treeSnapshot.rootIno !== locator.directoryIno)
303
+ throw new Error("task_artifact_owner_evidence_invalid");
304
+ return Object.freeze({
305
+ schemaVersion: OWNER_DELETION_SCHEMA_VERSION,
306
+ sessionId,
307
+ locator,
308
+ parentIdentity,
309
+ treeSnapshot,
310
+ });
311
+ }
312
+
313
+ /** Parse a persisted locator. Missing metadata returns undefined; malformed metadata fails closed. */
314
+ export function parseTaskArtifactOwnerLocator(value: unknown): TaskArtifactOwnerLocator | undefined {
315
+ if (value === undefined) return undefined;
316
+ if (
317
+ !isRecord(value) ||
318
+ Object.keys(value).length !== 4 ||
319
+ value.schemaVersion !== OWNER_SCHEMA_VERSION ||
320
+ typeof value.ownerId !== "string" ||
321
+ !/^[0-9a-f]{64}$/u.test(value.ownerId) ||
322
+ !isCanonicalNativeId(value.directoryDev) ||
323
+ !isCanonicalNativeId(value.directoryIno)
324
+ )
325
+ throw new Error("task_artifact_owner_locator_invalid");
326
+ return Object.freeze({
327
+ schemaVersion: OWNER_SCHEMA_VERSION,
328
+ ownerId: value.ownerId,
329
+ directoryDev: value.directoryDev,
330
+ directoryIno: value.directoryIno,
331
+ });
332
+ }
333
+
334
+ /** Validate JSON-restored tombstone evidence before it is used for exact retirement. */
335
+ export function parseTaskArtifactOwnerDeletionEvidence(value: unknown): TaskArtifactOwnerDeletionEvidence {
336
+ if (
337
+ !isRecord(value) ||
338
+ Object.keys(value).length !== 5 ||
339
+ value.schemaVersion !== OWNER_DELETION_SCHEMA_VERSION ||
340
+ typeof value.sessionId !== "string"
341
+ )
342
+ throw new Error("task_artifact_owner_evidence_invalid");
343
+ const locator = parseTaskArtifactOwnerLocator(value.locator);
344
+ if (!locator) throw new Error("task_artifact_owner_evidence_invalid");
345
+ return immutableDeletionEvidence(
346
+ value.sessionId,
347
+ locator,
348
+ parseParentIdentity(value.parentIdentity),
349
+ parseTreeSnapshot(value.treeSnapshot),
350
+ );
351
+ }
352
+
353
+ export function ownerAbsolutePath(context: TaskArtifactOwnerStorageContext, ownerId: string): string {
354
+ return path.join(context.sessionsRoot, ownerRelativePath(ownerId));
355
+ }
356
+
357
+ export function isOwnerRetainedRoot(
358
+ context: TaskArtifactOwnerStorageContext,
359
+ ownerId: string,
360
+ pathname: string,
361
+ ): boolean {
362
+ const owner = ownerAbsolutePath(context, ownerId);
363
+ return pathname === owner || pathname === `${owner}.removing`;
364
+ }
365
+
366
+ export function isKnownNativeSidePath(
367
+ context: TaskArtifactOwnerStorageContext,
368
+ ownerId: string,
369
+ pathname: string,
370
+ ): boolean {
371
+ if (!path.isAbsolute(pathname) || path.resolve(pathname) !== pathname) return false;
372
+ if (isOwnerRetainedRoot(context, ownerId, pathname)) return true;
373
+ const ownerParent = path.dirname(ownerAbsolutePath(context, ownerId));
374
+ const sideName = path.basename(pathname);
375
+ if (path.dirname(pathname) === ownerParent && sideName.startsWith(".gjc-") && sideName.length > 5) return true;
376
+ const recoveryParent = path.join(context.sessionsRoot, ".gjc-recovery");
377
+ const recoveryRelative = path.relative(recoveryParent, pathname);
378
+ return (
379
+ recoveryRelative !== "" &&
380
+ !path.isAbsolute(recoveryRelative) &&
381
+ recoveryRelative !== ".." &&
382
+ !recoveryRelative.startsWith(`..${path.sep}`) &&
383
+ !recoveryRelative.includes(path.sep)
384
+ );
385
+ }
386
+
387
+ /** Validate JSON-restored native continuation data against immutable owner authorization. */
388
+ export function parseTaskArtifactOwnerRetirementContinuation(
389
+ context: TaskArtifactOwnerStorageContext,
390
+ evidenceValue: TaskArtifactOwnerDeletionEvidence,
391
+ value: unknown,
392
+ ): TaskArtifactOwnerRetirementContinuation {
393
+ assertSessionRoot(context);
394
+ const evidence = parseTaskArtifactOwnerDeletionEvidence(evidenceValue);
395
+ if (
396
+ !isRecord(value) ||
397
+ Object.keys(value).length < 4 ||
398
+ Object.keys(value).length > 11 ||
399
+ Object.keys(value).some(key => !RETIREMENT_CONTINUATION_KEYS.has(key)) ||
400
+ value.schemaVersion !== OWNER_RETIREMENT_SCHEMA_VERSION ||
401
+ typeof value.retainedRootPath !== "string"
402
+ )
403
+ throw new Error("task_artifact_owner_continuation_invalid");
404
+ const parentIdentity = parseParentIdentity(value.parentIdentity);
405
+ if (!sameOwnerParentIdentity(parentIdentity, evidence.parentIdentity))
406
+ throw new Error("task_artifact_owner_continuation_invalid");
407
+ if (!isOwnerRetainedRoot(context, evidence.locator.ownerId, value.retainedRootPath))
408
+ throw new Error("task_artifact_owner_continuation_invalid");
409
+ const retainedTreeSnapshot = freezeTreeSnapshot(parseTreeSnapshot(value.retainedTreeSnapshot));
410
+ if (!retainedTreeDoesNotExpandAuthority(evidence.treeSnapshot, retainedTreeSnapshot))
411
+ throw new Error("task_artifact_owner_continuation_invalid");
412
+ const optionalStringList = (
413
+ key: string,
414
+ maximum: number,
415
+ validate: (candidate: string) => boolean,
416
+ ): readonly string[] | undefined => {
417
+ const candidates = value[key];
418
+ if (candidates === undefined) return undefined;
419
+ if (!Array.isArray(candidates) || candidates.length === 0 || candidates.length > maximum)
420
+ throw new Error("task_artifact_owner_continuation_invalid");
421
+ const parsed = candidates.map(candidate => {
422
+ if (typeof candidate !== "string" || !validate(candidate))
423
+ throw new Error("task_artifact_owner_continuation_invalid");
424
+ return candidate;
425
+ });
426
+ if (new Set(parsed).size !== parsed.length) throw new Error("task_artifact_owner_continuation_invalid");
427
+ return Object.freeze(parsed);
428
+ };
429
+ const isNativeSidePath = (candidate: string) => isKnownNativeSidePath(context, evidence.locator.ownerId, candidate);
430
+ const detachedPaths = optionalStringList("detachedPaths", 2, candidate =>
431
+ isOwnerRetainedRoot(context, evidence.locator.ownerId, candidate),
432
+ );
433
+ const nativeCodes = optionalStringList("nativeCodes", 64, candidate => /^[A-Za-z0-9_.:-]{1,128}$/u.test(candidate));
434
+ const retainedSuccessorPaths = optionalStringList("retainedSuccessorPaths", 256, isNativeSidePath);
435
+ const retainedPlaceholderPaths = optionalStringList("retainedPlaceholderPaths", 256, isNativeSidePath);
436
+ const retainedUnknownPaths = optionalStringList("retainedUnknownPaths", 256, isNativeSidePath);
437
+ const windowsErrorCodes = optionalStringList("windowsErrorCodes", 64, candidate =>
438
+ /^0x[0-9A-Fa-f]{8}$/u.test(candidate),
439
+ );
440
+ if (value.payloadDurable !== undefined && typeof value.payloadDurable !== "boolean")
441
+ throw new Error("task_artifact_owner_continuation_invalid");
442
+ return Object.freeze({
443
+ schemaVersion: OWNER_RETIREMENT_SCHEMA_VERSION,
444
+ parentIdentity,
445
+ retainedRootPath: value.retainedRootPath,
446
+ retainedTreeSnapshot,
447
+ ...(detachedPaths !== undefined ? { detachedPaths } : {}),
448
+ ...(nativeCodes !== undefined ? { nativeCodes } : {}),
449
+ ...(value.payloadDurable !== undefined ? { payloadDurable: value.payloadDurable } : {}),
450
+ ...(retainedSuccessorPaths !== undefined ? { retainedSuccessorPaths } : {}),
451
+ ...(retainedPlaceholderPaths !== undefined ? { retainedPlaceholderPaths } : {}),
452
+ ...(retainedUnknownPaths !== undefined ? { retainedUnknownPaths } : {}),
453
+ ...(windowsErrorCodes !== undefined ? { windowsErrorCodes } : {}),
454
+ });
455
+ }
456
+
457
+ /**
458
+ * Strictly decode persisted managed-journal data. Parsing does not establish current physical retirement,
459
+ * payload destruction, or writer quiescence.
460
+ */
461
+ export function parseTaskArtifactOwnerRetirementOutcome(
462
+ context: TaskArtifactOwnerStorageContext,
463
+ evidence: TaskArtifactOwnerDeletionEvidence,
464
+ value: unknown,
465
+ ): TaskArtifactOwnerRetirementOutcome {
466
+ const invalid = (): never => {
467
+ throw new Error("task_artifact_owner_retirement_outcome_invalid");
468
+ };
469
+ if (!isRecord(value)) return invalid();
470
+ const kind = value.kind;
471
+ if (kind !== "completed" && kind !== "payload_retired" && kind !== "cleanup_pending" && kind !== "uncertain")
472
+ return invalid();
473
+ const expectedKeys = [
474
+ "kind",
475
+ "evidence",
476
+ ...(kind !== "completed" ? ["continuation"] : []),
477
+ ...(kind === "payload_retired" ? ["namespace"] : []),
478
+ ...(kind === "uncertain" ? ["reason"] : []),
479
+ ...(value.nativeOutcome !== undefined ? ["nativeOutcome"] : []),
480
+ ];
481
+ if (Object.keys(value).length !== expectedKeys.length || !expectedKeys.every(key => key in value)) return invalid();
482
+ const original = parseTaskArtifactOwnerDeletionEvidence(value.evidence);
483
+ const expectedEvidence = parseTaskArtifactOwnerDeletionEvidence(evidence);
484
+ if (JSON.stringify(original) !== JSON.stringify(expectedEvidence)) return invalid();
485
+ const continuation =
486
+ kind !== "completed"
487
+ ? parseTaskArtifactOwnerRetirementContinuation(context, expectedEvidence, value.continuation)
488
+ : undefined;
489
+ let nativeOutcome: NativeExactUnlinkResult | undefined;
490
+ if (value.nativeOutcome !== undefined) {
491
+ if (!isRecord(value.nativeOutcome)) return invalid();
492
+ const record = value.nativeOutcome;
493
+ const allowed = [
494
+ "ok",
495
+ "code",
496
+ "detachedPath",
497
+ "retainedSuccessorPath",
498
+ "retainedPlaceholderPath",
499
+ "retainedUnknownPath",
500
+ "payloadDurable",
501
+ "windowsErrorCode",
502
+ ];
503
+ if (Object.keys(record).some(key => !allowed.includes(key)) || typeof record.ok !== "boolean") return invalid();
504
+ for (const [key, paths] of [
505
+ ["detachedPath", continuation?.detachedPaths],
506
+ ["retainedSuccessorPath", continuation?.retainedSuccessorPaths],
507
+ ["retainedPlaceholderPath", continuation?.retainedPlaceholderPaths],
508
+ ["retainedUnknownPath", continuation?.retainedUnknownPaths],
509
+ ] as const) {
510
+ const pathname = record[key];
511
+ if (pathname !== undefined && (typeof pathname !== "string" || !paths?.includes(pathname))) return invalid();
512
+ }
513
+ if (
514
+ record.code !== undefined &&
515
+ (record.ok || typeof record.code !== "string" || !continuation?.nativeCodes?.includes(record.code))
516
+ )
517
+ return invalid();
518
+ if (
519
+ record.payloadDurable !== undefined &&
520
+ (typeof record.payloadDurable !== "boolean" ||
521
+ (record.payloadDurable && (record.ok || continuation?.payloadDurable !== true)))
522
+ )
523
+ return invalid();
524
+ if (
525
+ record.windowsErrorCode !== undefined &&
526
+ (record.ok ||
527
+ typeof record.windowsErrorCode !== "string" ||
528
+ !continuation?.windowsErrorCodes?.includes(record.windowsErrorCode))
529
+ )
530
+ return invalid();
531
+ nativeOutcome = Object.freeze({
532
+ ok: record.ok,
533
+ ...(typeof record.code === "string" ? { code: record.code } : {}),
534
+ ...(typeof record.detachedPath === "string" ? { detachedPath: record.detachedPath } : {}),
535
+ ...(typeof record.retainedSuccessorPath === "string"
536
+ ? { retainedSuccessorPath: record.retainedSuccessorPath }
537
+ : {}),
538
+ ...(typeof record.retainedPlaceholderPath === "string"
539
+ ? { retainedPlaceholderPath: record.retainedPlaceholderPath }
540
+ : {}),
541
+ ...(typeof record.retainedUnknownPath === "string" ? { retainedUnknownPath: record.retainedUnknownPath } : {}),
542
+ ...(typeof record.payloadDurable === "boolean" ? { payloadDurable: record.payloadDurable } : {}),
543
+ ...(typeof record.windowsErrorCode === "string" ? { windowsErrorCode: record.windowsErrorCode } : {}),
544
+ }) as NativeExactUnlinkResult;
545
+ }
546
+ if (kind === "completed") {
547
+ if (!nativeOutcome?.ok) return invalid();
548
+ return Object.freeze({ kind, evidence: original, nativeOutcome });
549
+ }
550
+ if (!continuation || nativeOutcome?.ok) return invalid();
551
+ if (kind === "payload_retired") {
552
+ if (
553
+ value.namespace !== "retained" ||
554
+ nativeOutcome?.code !== "cleanup_pending" ||
555
+ nativeOutcome.payloadDurable !== true ||
556
+ continuation.retainedRootPath !== `${ownerAbsolutePath(context, expectedEvidence.locator.ownerId)}.removing` ||
557
+ continuation.retainedSuccessorPaths?.length ||
558
+ continuation.retainedPlaceholderPaths?.length ||
559
+ continuation.retainedUnknownPaths?.length ||
560
+ continuation.retainedTreeSnapshot.entries.some(
561
+ entry => entry.kind === "file" && (entry.size !== "0" || entry.sha256 !== EMPTY_PAYLOAD_SHA256),
562
+ )
563
+ )
564
+ return invalid();
565
+ return Object.freeze({ kind, namespace: "retained", evidence: original, continuation, nativeOutcome });
566
+ }
567
+ if (kind === "uncertain") {
568
+ if (typeof value.reason !== "string" || value.reason.length === 0 || value.reason.length > 4096) return invalid();
569
+ return Object.freeze({
570
+ kind,
571
+ evidence: original,
572
+ continuation,
573
+ reason: value.reason,
574
+ ...(nativeOutcome ? { nativeOutcome } : {}),
575
+ });
576
+ }
577
+ return Object.freeze({ kind, evidence: original, continuation, ...(nativeOutcome ? { nativeOutcome } : {}) });
578
+ }