@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
@@ -127,6 +127,7 @@ import {
127
127
  streamSimple,
128
128
  } from "@gajae-code/ai/core";
129
129
  import { normalizeAnthropicBaseUrl } from "@gajae-code/ai/providers/anthropic";
130
+ import { isExplicitCodexTerminalVeto } from "@gajae-code/ai/providers/openai-codex-responses";
130
131
  import {
131
132
  type AuthDisposition,
132
133
  beginAttempt,
@@ -1426,6 +1427,8 @@ const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream time
1426
1427
  const BARE_DEFAULT_CODEX_RETRYABLE_ERROR =
1427
1428
  /^Codex error event(?:: .*)? \(code=(?:server_is_overloaded|server_error|internal_error)(?:, [^)]+)*\)$/;
1428
1429
  const BARE_DEFAULT_CODEX_RETRYABLE_CODES = new Set(["server_is_overloaded", "server_error", "internal_error"]);
1430
+ const BARE_DEFAULT_CODEX_OVERLOAD_ERROR =
1431
+ /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
1429
1432
  const BARE_DEFAULT_OVERLOAD_CODES = new Set([SERVER_OVERLOADED_PROVIDER_CODE]);
1430
1433
  /** Anthropic's typed capacity-overload `error.type`, the only Anthropic overload code admitted below. */
1431
1434
  const ANTHROPIC_OVERLOADED_ERROR_TYPE = "overloaded_error";
@@ -1555,9 +1558,38 @@ function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined
1555
1558
  );
1556
1559
  }
1557
1560
 
1561
+ // Deterministic auth/request/model diagnostics must surface even when a provider
1562
+ // labels the failure with a transient code.
1563
+ const TERMINAL_ERROR_MESSAGE =
1564
+ /unauthorized|forbidden|authentication_error|permission_error|permission denied|invalid api key|invalid_request_error|invalid request|bad request|bad_request|validation_error|unprocessable|payload too large|payment required|insufficient_quota|insufficient credits|missing required (parameter|field)|invalid schema|invalid tool_choice|unsupported (parameter|value|model)|model_not_found|no such model|unknown model|does not (exist|support)|request was aborted|request aborted|the user aborted/i;
1565
+
1566
+ function isTerminalCodexProviderFailure(message: AssistantMessage): boolean {
1567
+ if (message.api !== "openai-codex-responses") return false;
1568
+ if (isExplicitCodexTerminalVeto(message.transportFailure?.providerCode, message.errorMessage)) return true;
1569
+ return (
1570
+ (BARE_DEFAULT_CODEX_RETRYABLE_CODES.has(message.transportFailure?.providerCode ?? "") ||
1571
+ BARE_DEFAULT_CODEX_RETRYABLE_ERROR.test(message.errorMessage ?? "")) &&
1572
+ TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "")
1573
+ );
1574
+ }
1575
+
1558
1576
  function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
1559
1577
  return (
1560
1578
  message.api === "openai-codex-responses" &&
1579
+ !TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "") &&
1580
+ !isExplicitCodexTerminalVeto(undefined, message.errorMessage) &&
1581
+ (isExactTypedOverloadFacts(message, BARE_DEFAULT_OVERLOAD_CODES) ||
1582
+ (BARE_DEFAULT_CODEX_OVERLOAD_ERROR.test(message.errorMessage ?? "") &&
1583
+ !hasBareDefaultRetryDisqualifyingFacts(message))) &&
1584
+ !assistantMessageHasVisibleOrToolContent(message)
1585
+ );
1586
+ }
1587
+
1588
+ function isBareDefaultCodexProviderFailure(message: AssistantMessage): boolean {
1589
+ return (
1590
+ message.api === "openai-codex-responses" &&
1591
+ !TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "") &&
1592
+ !isExplicitCodexTerminalVeto(message.transportFailure?.providerCode, message.errorMessage) &&
1561
1593
  (isExactTypedOverloadFacts(message, BARE_DEFAULT_CODEX_RETRYABLE_CODES) ||
1562
1594
  (BARE_DEFAULT_CODEX_RETRYABLE_ERROR.test(message.errorMessage ?? "") &&
1563
1595
  !hasBareDefaultRetryDisqualifyingFacts(message))) &&
@@ -1620,7 +1652,10 @@ function isBareDefaultAnthropicOverload(message: AssistantMessage): boolean {
1620
1652
  function assistantMessageHasVisibleOrToolContent(message: AssistantMessage): boolean {
1621
1653
  return message.content.some(content => {
1622
1654
  if (content.type === "text") return content.text.length > 0;
1623
- return content.type === "thinking" || content.type === "redactedThinking" || content.type === "toolCall";
1655
+ if (content.type === "thinking") {
1656
+ return content.thinking.length > 0 || (content.thinkingSignature?.length ?? 0) > 0;
1657
+ }
1658
+ return content.type === "redactedThinking" || content.type === "toolCall";
1624
1659
  });
1625
1660
  }
1626
1661
 
@@ -22994,6 +23029,7 @@ export class AgentSession {
22994
23029
  }
22995
23030
 
22996
23031
  #isRetryableError(message: AssistantMessage): boolean {
23032
+ if (isTerminalCodexProviderFailure(message)) return false;
22997
23033
  if (this.#isTerminalProviderFirstEventTimeout(message)) return false;
22998
23034
  if (message.errorMessage?.startsWith("Model fallback chain exhausted;")) return false;
22999
23035
  if (message.errorMessage?.startsWith("Managed fallback retried the escaped non-ASCII")) return false;
@@ -23004,7 +23040,11 @@ export class AgentSession {
23004
23040
  return false;
23005
23041
  const contextWindow = this.model?.contextWindow ?? 0;
23006
23042
  if (classifyContextOverflow(message, transportFailure, contextWindow)) return false;
23007
- if (isBareDefaultCodexOverload(message) && this.settings.get("retry.enabled") !== false) return true;
23043
+ if (
23044
+ (isBareDefaultCodexOverload(message) || isBareDefaultCodexProviderFailure(message)) &&
23045
+ this.settings.get("retry.enabled") !== false
23046
+ )
23047
+ return true;
23008
23048
  const managedFallback = this.#defaultFallbackChain().chain.entries.length > 1;
23009
23049
  // An account-specific model rejection that cannot rotate to another
23010
23050
  // credential stays terminal only on the session's own retry path; managed
@@ -23117,9 +23157,7 @@ export class AgentSession {
23117
23157
  // Errors that will never succeed on retry (auth/permission, malformed
23118
23158
  // request, unknown/unsupported model). These surface immediately rather
23119
23159
  // than retry forever.
23120
- return /unauthorized|forbidden|authentication_error|permission_error|permission denied|invalid api key|invalid_request_error|invalid request|bad request|bad_request|validation_error|unprocessable|payload too large|payment required|insufficient_quota|insufficient credits|missing required (parameter|field)|invalid schema|invalid tool_choice|unsupported (parameter|value|model)|model_not_found|no such model|unknown model|does not (exist|support)|request was aborted|request aborted|the user aborted/i.test(
23121
- errorMessage,
23122
- );
23160
+ return TERMINAL_ERROR_MESSAGE.test(errorMessage);
23123
23161
  }
23124
23162
 
23125
23163
  #extractExplicitHttpStatusFromErrorMessage(errorMessage: string): number | undefined {
@@ -24308,6 +24346,13 @@ export class AgentSession {
24308
24346
  const classification = this.#classifyErrorForRetry(message);
24309
24347
  const localSnapshot = classification === "local_snapshot";
24310
24348
  const localBufferOverflow = classification === "local_buffer_overflow";
24349
+ // Managed outcomes can reach retry handling without the ordinary admission
24350
+ // check. Do not let retained transport facts bypass the same terminal veto.
24351
+ if (isTerminalCodexProviderFailure(message)) {
24352
+ return managedOutcome
24353
+ ? { type: "terminal", terminal: { stopReason: "error", messages: [message] } }
24354
+ : false;
24355
+ }
24311
24356
  if (retryCancelled()) {
24312
24357
  return managedOutcome ? { type: "terminal", terminal: { stopReason: "cancelled" } } : false;
24313
24358
  }
@@ -24369,6 +24414,8 @@ export class AgentSession {
24369
24414
  isBareDefaultCodexOverload(message) ||
24370
24415
  isBareDefaultAnthropicOverload(message) ||
24371
24416
  isBareDefaultOpenAIResponsesOverload(message);
24417
+ const canReplayBareDefaultCodexFailure = isBareDefaultCodexProviderFailure(message);
24418
+ const boundBareDefaultCodexFailure = canReplayBareDefaultCodexFailure && !legacyRetryConfigured;
24372
24419
  const canReplayCodexProviderOverload = isBareDefaultCodexOverload(message);
24373
24420
  const reportedRetryMaxAttempts = transportFailure?.retryMaxAttempts;
24374
24421
  if (reportedRetryMaxAttempts !== undefined) {
@@ -24412,7 +24459,9 @@ export class AgentSession {
24412
24459
  const fallbackTrigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
24413
24460
  const trigger:
24414
24461
  | { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition }
24415
- | undefined = canReplayCodexProviderOverload ? { class: "server" } : fallbackTrigger;
24462
+ | undefined = canReplayCodexProviderOverload || canReplayBareDefaultCodexFailure
24463
+ ? { class: "server" }
24464
+ : fallbackTrigger;
24416
24465
  // OpenAI's typed statusless capacity-overload code (issue #5018) must not
24417
24466
  // gain managed-chain retry/advance authority from its new facts. Before
24418
24467
  // the code survived transport, this failure reached the session as an
@@ -24516,6 +24565,7 @@ export class AgentSession {
24516
24565
  if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential && !canReplayEmptyResponse) {
24517
24566
  if (
24518
24567
  (!canReplayProviderOverload &&
24568
+ !canReplayBareDefaultCodexFailure &&
24519
24569
  !canReplayUnexpectedSocketClose &&
24520
24570
  !this.#isTypedFirstEventTimeout(message) &&
24521
24571
  !messageOnlyWatchdogTimeout &&
@@ -24534,6 +24584,7 @@ export class AgentSession {
24534
24584
  !managedFallback &&
24535
24585
  classification === "transient" &&
24536
24586
  !canReplayProviderOverload &&
24587
+ !boundBareDefaultCodexFailure &&
24537
24588
  !canReplayUnexpectedSocketClose &&
24538
24589
  !this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
24539
24590
 
@@ -1474,6 +1474,7 @@ export class ManagedSessionDescendantStore {
1474
1474
  retained?: { authority: RecoveryFsRoot; authorityBaseDir: string },
1475
1475
  policy?: ManagedSessionSecurityPolicy,
1476
1476
  profileAgentDir?: string,
1477
+ expectedSubtreeRoot?: ManagedDirectoryRoot,
1477
1478
  ) {
1478
1479
  managedRelativePath(root, baseDir);
1479
1480
 
@@ -1512,13 +1513,46 @@ export class ManagedSessionDescendantStore {
1512
1513
  });
1513
1514
  }
1514
1515
  this.#authority = retained.authority;
1516
+ if (
1517
+ expectedSubtreeRoot &&
1518
+ (expectedSubtreeRoot.canonicalPath !== this.#baseDir ||
1519
+ expectedSubtreeRoot.dev !== this.#subtreeRoot.dev ||
1520
+ expectedSubtreeRoot.ino !== this.#subtreeRoot.ino)
1521
+ )
1522
+ throw new Error("Managed subtree authority changed during establishment");
1515
1523
  this.#assertBound();
1516
1524
 
1517
1525
  return;
1518
1526
  }
1519
1527
  assertManagedDirectoryRoot(root);
1520
- ensureManagedDirectory(this.#baseDir, root, this.#policy);
1528
+ if (expectedSubtreeRoot) {
1529
+ if (expectedSubtreeRoot.canonicalPath !== this.#baseDir)
1530
+ throw new Error("Managed subtree authority path mismatch");
1531
+ assertManagedDirectoryRoot(expectedSubtreeRoot);
1532
+ const verified = validateNativeSecurityResult(
1533
+ process.platform === "win32"
1534
+ ? nativeSessionStorage().verifyOwnerOnlyPathSecurityExpected(
1535
+ this.#baseDir,
1536
+ "directory",
1537
+ expectedSubtreeRoot.dev,
1538
+ expectedSubtreeRoot.ino,
1539
+ )
1540
+ : nativeSessionStorage().verifyOwnerOnlyPathSecurity(this.#baseDir, "directory"),
1541
+ "verify",
1542
+ "directory",
1543
+ );
1544
+ if (!verified.ok) throw securityError(this.#baseDir, verified);
1545
+ assertManagedDirectoryRoot(expectedSubtreeRoot);
1546
+ } else {
1547
+ ensureManagedDirectory(this.#baseDir, root, this.#policy);
1548
+ }
1521
1549
  const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
1550
+ if (
1551
+ expectedSubtreeRoot &&
1552
+ (canonicalFileId(subtreeStat.dev) !== expectedSubtreeRoot.dev ||
1553
+ canonicalFileId(subtreeStat.ino) !== expectedSubtreeRoot.ino)
1554
+ )
1555
+ throw new Error("Managed subtree authority changed during establishment");
1522
1556
  this.#subtreeRoot = Object.freeze({
1523
1557
  canonicalPath: this.#baseDir,
1524
1558
  dev: canonicalFileId(subtreeStat.dev),
@@ -1634,6 +1668,50 @@ export class ManagedSessionDescendantStore {
1634
1668
  }
1635
1669
  this.#assertBound();
1636
1670
  }
1671
+
1672
+ /** Capture a directory identity through this store's retained managed root without repairing it. */
1673
+ captureDirectoryIdentity(relativePath: string): { dev: string; ino: string } {
1674
+ this.#assertBound();
1675
+ const resolved = this.#resolve(relativePath);
1676
+ if (!this.#authority) this.#assertPathBackedDirectoryChain(resolved);
1677
+ const named = fs.lstatSync(resolved, { bigint: true });
1678
+ if (!named.isDirectory() || named.isSymbolicLink()) throw new Error("managed_directory_identity_unavailable");
1679
+ const dev = canonicalFileId(named.dev);
1680
+ const ino = canonicalFileId(named.ino);
1681
+ if (dev !== this.#subtreeRoot.dev) throw new Error("managed_directory_identity_unavailable");
1682
+ if (this.#authority) {
1683
+ const retainedRelative = this.#relative(resolved);
1684
+ if (retainedRelative === "") {
1685
+ if (dev !== this.#subtreeRoot.dev || ino !== this.#subtreeRoot.ino)
1686
+ throw new Error("Managed descendant root binding changed");
1687
+ } else {
1688
+ const retained = this.#authority.retainManagedDirectory(retainedRelative, dev.toString(), ino.toString());
1689
+ try {
1690
+ const identity = retained.identity();
1691
+ if (
1692
+ !identity.ok ||
1693
+ !identity.identity ||
1694
+ identity.identity.dev !== dev.toString() ||
1695
+ identity.identity.ino !== ino.toString()
1696
+ )
1697
+ throw new Error(identity.code ?? "managed_directory_identity_unavailable");
1698
+ } finally {
1699
+ retained.close();
1700
+ }
1701
+ }
1702
+ }
1703
+ const after = fs.lstatSync(resolved, { bigint: true });
1704
+ if (
1705
+ !after.isDirectory() ||
1706
+ after.isSymbolicLink() ||
1707
+ canonicalFileId(after.dev) !== dev ||
1708
+ canonicalFileId(after.ino) !== ino
1709
+ )
1710
+ throw new Error("managed_directory_identity_changed");
1711
+ this.#assertBound();
1712
+ return { dev: dev.toString(), ino: ino.toString() };
1713
+ }
1714
+
1637
1715
  #assertBound(): void {
1638
1716
  if (!this.#authority) {
1639
1717
  const named = fs.statSync(this.#baseDir, { bigint: true });
@@ -3019,6 +3097,34 @@ export class ManagedSessionDescendantStore {
3019
3097
  if (!removed.ok) throw new Error(removed.code ?? "managed_remove_failed");
3020
3098
  this.#assertBound();
3021
3099
  }
3100
+
3101
+ /** Remove one exact managed tree through the parent-identity-bound native protocol. */
3102
+ removeTreeExpectedWithParentIdentity(
3103
+ relativePath: string,
3104
+ expected: NativeDirectoryTreeSnapshot,
3105
+ parentIdentity: { dev: bigint; ino: bigint },
3106
+ ): NativeExactUnlinkResult {
3107
+ this.#beforeMutation();
3108
+ this.#assertBound();
3109
+ const resolved = this.#resolve(relativePath);
3110
+ const parentPath = path.dirname(resolved);
3111
+ const parentRelative = path.relative(this.#baseDir, parentPath);
3112
+ if (path.isAbsolute(parentRelative) || parentRelative === ".." || parentRelative.startsWith(`..${path.sep}`))
3113
+ throw new Error("managed_remove_parent_outside_store");
3114
+ const currentParent = this.captureDirectoryIdentity(parentRelative);
3115
+ if (
3116
+ currentParent.dev !== canonicalFileId(parentIdentity.dev).toString() ||
3117
+ currentParent.ino !== canonicalFileId(parentIdentity.ino).toString()
3118
+ )
3119
+ throw new Error("managed_remove_parent_identity_mismatch");
3120
+ const removed = nativeSessionStorage().exactRemoveDirectoryTree(resolved, expected, {
3121
+ dev: BigInt(currentParent.dev),
3122
+ ino: BigInt(currentParent.ino),
3123
+ });
3124
+ this.#assertBound();
3125
+ return removed;
3126
+ }
3127
+
3022
3128
  fsyncTree(): NativeDirectoryTreeSnapshot {
3023
3129
  this.#beforeMutation();
3024
3130
  this.#assertBound();
@@ -0,0 +1,229 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
4
+ import {
5
+ assertSafeRelativePath,
6
+ assertSessionRoot,
7
+ freezeTreeSnapshot,
8
+ immutableDeletionEvidence,
9
+ isOwnerRetainedRoot,
10
+ manifestRelativePath,
11
+ OWNER_DIRECTORY,
12
+ OWNER_MANIFEST,
13
+ OWNER_SCHEMA_VERSION,
14
+ ownerIdForSession,
15
+ ownerRelativePath,
16
+ parseTaskArtifactOwnerLocator,
17
+ sameOwnerParentIdentity,
18
+ type TaskArtifactOwnerDeletionEvidence,
19
+ type TaskArtifactOwnerLocator,
20
+ type TaskArtifactOwnerParentIdentity,
21
+ type TaskArtifactOwnerStorageContext,
22
+ } from "../task-artifact-owner-codec";
23
+ import { ManagedSessionDescendantStore } from "./managed-session-storage";
24
+
25
+ export interface OwnerManifest extends TaskArtifactOwnerLocator {
26
+ readonly sessionId: string;
27
+ }
28
+
29
+ export function newSessionRootStore(context: TaskArtifactOwnerStorageContext): ManagedSessionDescendantStore {
30
+ assertSessionRoot(context);
31
+ if (path.resolve(context.profileAgentDir) !== context.profileAgentDir)
32
+ throw new Error("task_artifact_owner_profile_invalid");
33
+ const profile = fs.lstatSync(context.profileAgentDir, { bigint: true });
34
+ if (!profile.isDirectory() || profile.isSymbolicLink()) throw new Error("task_artifact_owner_profile_invalid");
35
+ const stat = fs.lstatSync(context.sessionsRoot, { bigint: true });
36
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("task_artifact_owner_root_invalid");
37
+ return new ManagedSessionDescendantStore(
38
+ context.rootAuthority,
39
+ context.sessionsRoot,
40
+ undefined,
41
+ context.securityPolicy,
42
+ context.profileAgentDir,
43
+ {
44
+ canonicalPath: context.sessionsRoot,
45
+ dev: BigInt.asUintN(64, stat.dev),
46
+ ino: BigInt.asUintN(64, stat.ino),
47
+ },
48
+ );
49
+ }
50
+
51
+ function parseOwnerManifest(bytes: Uint8Array): OwnerManifest {
52
+ let parsed: unknown;
53
+ try {
54
+ parsed = JSON.parse(Buffer.from(bytes).toString("utf8"));
55
+ } catch {
56
+ throw new Error("task_artifact_owner_manifest_invalid");
57
+ }
58
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || Object.keys(parsed).length !== 5)
59
+ throw new Error("task_artifact_owner_manifest_invalid");
60
+ const record = parsed as Record<string, unknown>;
61
+ if (typeof record.sessionId !== "string" || record.sessionId.length === 0)
62
+ throw new Error("task_artifact_owner_manifest_invalid");
63
+ let locator: TaskArtifactOwnerLocator | undefined;
64
+ try {
65
+ locator = parseTaskArtifactOwnerLocator({
66
+ schemaVersion: record.schemaVersion,
67
+ ownerId: record.ownerId,
68
+ directoryDev: record.directoryDev,
69
+ directoryIno: record.directoryIno,
70
+ });
71
+ } catch {
72
+ throw new Error("task_artifact_owner_manifest_invalid");
73
+ }
74
+ if (!locator) throw new Error("task_artifact_owner_manifest_invalid");
75
+ return { ...locator, sessionId: record.sessionId };
76
+ }
77
+
78
+ export function readOwnerManifest(
79
+ store: ManagedSessionDescendantStore,
80
+ locator: TaskArtifactOwnerLocator,
81
+ relativePath = manifestRelativePath(locator.ownerId),
82
+ ): OwnerManifest {
83
+ const manifest = store.readExpected(relativePath);
84
+ if (!manifest) throw new Error("task_artifact_owner_manifest_missing");
85
+ const parsed = parseOwnerManifest(manifest.bytes);
86
+ if (
87
+ parsed.ownerId !== locator.ownerId ||
88
+ parsed.directoryDev !== locator.directoryDev ||
89
+ parsed.directoryIno !== locator.directoryIno
90
+ )
91
+ throw new Error("task_artifact_owner_manifest_mismatch");
92
+ return parsed;
93
+ }
94
+
95
+ export function assertOwnerDirectoryExists(context: TaskArtifactOwnerStorageContext, ownerId: string): void {
96
+ const parent = path.join(context.sessionsRoot, OWNER_DIRECTORY);
97
+ const owner = path.join(parent, ownerId);
98
+ for (const candidate of [parent, owner]) {
99
+ let stat: fs.BigIntStats;
100
+ try {
101
+ stat = fs.lstatSync(candidate, { bigint: true });
102
+ } catch (error) {
103
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new Error("task_artifact_owner_missing");
104
+ throw error;
105
+ }
106
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("task_artifact_owner_replaced");
107
+ }
108
+ }
109
+
110
+ export function assertOwnerIdentity(store: ManagedSessionDescendantStore, locator: TaskArtifactOwnerLocator): void {
111
+ const identity = store.subtreeRootAuthority;
112
+ if (identity.dev.toString() !== locator.directoryDev || identity.ino.toString() !== locator.directoryIno)
113
+ throw new Error("task_artifact_owner_identity_mismatch");
114
+ }
115
+
116
+ export function openOwnerStore(
117
+ context: TaskArtifactOwnerStorageContext,
118
+ rootStore: ManagedSessionDescendantStore,
119
+ locator: TaskArtifactOwnerLocator,
120
+ sessionId: string,
121
+ ): ManagedSessionDescendantStore {
122
+ if (locator.ownerId !== ownerIdForSession(sessionId)) throw new Error("task_artifact_owner_session_mismatch");
123
+ assertOwnerDirectoryExists(context, locator.ownerId);
124
+ const manifest = readOwnerManifest(rootStore, locator);
125
+ if (manifest.sessionId !== sessionId) throw new Error("task_artifact_owner_session_mismatch");
126
+ const identity = rootStore.captureDirectoryIdentity(ownerRelativePath(locator.ownerId));
127
+ if (identity.dev !== locator.directoryDev || identity.ino !== locator.directoryIno)
128
+ throw new Error("task_artifact_owner_identity_mismatch");
129
+ const ownerPath = path.join(context.sessionsRoot, ownerRelativePath(locator.ownerId));
130
+ const ownerStore = new ManagedSessionDescendantStore(
131
+ context.rootAuthority,
132
+ ownerPath,
133
+ undefined,
134
+ context.securityPolicy,
135
+ context.profileAgentDir,
136
+ {
137
+ canonicalPath: ownerPath,
138
+ dev: BigInt(locator.directoryDev),
139
+ ino: BigInt(locator.directoryIno),
140
+ },
141
+ );
142
+ try {
143
+ assertOwnerIdentity(ownerStore, locator);
144
+ readOwnerManifest(ownerStore, locator, OWNER_MANIFEST);
145
+ return ownerStore;
146
+ } catch (error) {
147
+ ownerStore.close();
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ export function locatorFromManifest(manifest: OwnerManifest): TaskArtifactOwnerLocator {
153
+ return {
154
+ schemaVersion: OWNER_SCHEMA_VERSION,
155
+ ownerId: manifest.ownerId,
156
+ directoryDev: manifest.directoryDev,
157
+ directoryIno: manifest.directoryIno,
158
+ };
159
+ }
160
+
161
+ export function captureValidatedOwnerTree(
162
+ context: TaskArtifactOwnerStorageContext,
163
+ rootStore: ManagedSessionDescendantStore,
164
+ sessionId: string,
165
+ locator: TaskArtifactOwnerLocator,
166
+ ): NativeDirectoryTreeSnapshot {
167
+ if (locator.ownerId !== ownerIdForSession(sessionId)) throw new Error("task_artifact_owner_session_mismatch");
168
+ assertOwnerDirectoryExists(context, locator.ownerId);
169
+ const manifest = readOwnerManifest(rootStore, locator);
170
+ if (manifest.sessionId !== sessionId) throw new Error("task_artifact_owner_session_mismatch");
171
+ const tree = rootStore.captureTree(ownerRelativePath(locator.ownerId));
172
+ if (tree.rootDev !== locator.directoryDev || tree.rootIno !== locator.directoryIno)
173
+ throw new Error("task_artifact_owner_identity_mismatch");
174
+ return tree;
175
+ }
176
+
177
+ export function captureOwnerTreeIfPresent(
178
+ context: TaskArtifactOwnerStorageContext,
179
+ rootStore: ManagedSessionDescendantStore,
180
+ ownerId: string,
181
+ pathname: string,
182
+ ): NativeDirectoryTreeSnapshot | undefined {
183
+ if (!isOwnerRetainedRoot(context, ownerId, pathname))
184
+ throw new Error("task_artifact_owner_retained_root_unrecognized");
185
+ let named: fs.BigIntStats;
186
+ try {
187
+ named = fs.lstatSync(pathname, { bigint: true });
188
+ } catch (error) {
189
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
190
+ throw error;
191
+ }
192
+ if (!named.isDirectory() || named.isSymbolicLink()) throw new Error("task_artifact_owner_retained_root_replaced");
193
+ const relative = path.relative(context.sessionsRoot, pathname).split(path.sep).join("/");
194
+ assertSafeRelativePath(relative);
195
+ return freezeTreeSnapshot(rootStore.captureTree(relative));
196
+ }
197
+
198
+ export function ownerTreeHasPendingManagedPublication(snapshot: NativeDirectoryTreeSnapshot): boolean {
199
+ return snapshot.entries.some(entry =>
200
+ /^\..+\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(?:staging|replacement)$/u.test(
201
+ path.posix.basename(entry.relativePath),
202
+ ),
203
+ );
204
+ }
205
+
206
+ /** Capture exact owner evidence without repairing its security, timestamps, or namespace. */
207
+ export function captureTaskArtifactOwnerDeletionEvidence(
208
+ context: TaskArtifactOwnerStorageContext,
209
+ sessionId: string,
210
+ locatorValue: unknown,
211
+ ): TaskArtifactOwnerDeletionEvidence | undefined {
212
+ const locator = parseTaskArtifactOwnerLocator(locatorValue);
213
+ if (!locator) return undefined;
214
+ const rootStore = newSessionRootStore(context);
215
+ try {
216
+ rootStore.verifyRootSecurity();
217
+ const parentIdentity: TaskArtifactOwnerParentIdentity = rootStore.captureDirectoryIdentity(OWNER_DIRECTORY);
218
+ const treeSnapshot = captureValidatedOwnerTree(context, rootStore, sessionId, locator);
219
+ // This refuses to capture during publication; it does not revoke independent open descriptors.
220
+ if (ownerTreeHasPendingManagedPublication(treeSnapshot))
221
+ throw new Error("task_artifact_owner_writer_not_quiescent");
222
+ const parentAfter = rootStore.captureDirectoryIdentity(OWNER_DIRECTORY);
223
+ if (!sameOwnerParentIdentity(parentIdentity, parentAfter))
224
+ throw new Error("task_artifact_owner_parent_changed_during_capture");
225
+ return immutableDeletionEvidence(sessionId, locator, parentIdentity, treeSnapshot);
226
+ } finally {
227
+ rootStore.close();
228
+ }
229
+ }
@@ -0,0 +1,68 @@
1
+ import { parseFirstJsonlLine } from "../session-transcript-header";
2
+ import { parseTaskArtifactOwnerLocator, type TaskArtifactOwnerLocator } from "../task-artifact-owner-codec";
3
+
4
+ const transcriptDecoder = new TextDecoder("utf-8", { fatal: true });
5
+
6
+ /** Resolve the path-free owner locator using transcript replay's header-patch semantics. */
7
+ export function taskArtifactOwnerLocatorFromTranscriptBytes(
8
+ bytes: Uint8Array,
9
+ expectedSessionId: string,
10
+ ): TaskArtifactOwnerLocator | undefined {
11
+ const header = parseFirstJsonlLine(bytes);
12
+ if (header?.type !== "session" || header.id !== expectedSessionId)
13
+ throw new Error("task_artifact_owner_transcript_header_invalid");
14
+ let locator = parseTaskArtifactOwnerLocator(header.taskArtifactOwner);
15
+ if (typeof header.version !== "number" || header.version < 4) return locator;
16
+ const firstEnd = bytes.indexOf(0x0a);
17
+ let start = firstEnd < 0 ? bytes.byteLength : firstEnd + 1;
18
+ while (start < bytes.byteLength) {
19
+ const newline = bytes.indexOf(0x0a, start);
20
+ const end = newline < 0 ? bytes.byteLength : newline;
21
+ const line = bytes.subarray(start, end);
22
+ let record: Record<string, unknown> | undefined;
23
+ try {
24
+ if (line.byteLength > 0) {
25
+ const parsed: unknown = JSON.parse(transcriptDecoder.decode(line));
26
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
27
+ record = parsed as Record<string, unknown>;
28
+ }
29
+ } catch {
30
+ const text = Buffer.from(line).toString("utf8");
31
+ if (text.includes("header_patch") && text.includes("taskArtifactOwner"))
32
+ throw new Error("task_artifact_owner_patch_invalid");
33
+ }
34
+ if (
35
+ record?.type === "header_patch" &&
36
+ typeof record.patch === "object" &&
37
+ record.patch !== null &&
38
+ !Array.isArray(record.patch) &&
39
+ Object.hasOwn(record.patch, "taskArtifactOwner")
40
+ ) {
41
+ const patch = record.patch as Record<string, unknown>;
42
+ if (
43
+ !Object.keys(record).every(key => key === "type" || key === "patch") ||
44
+ !Object.keys(patch).every(
45
+ key =>
46
+ key === "cwd" ||
47
+ key === "title" ||
48
+ key === "titleSource" ||
49
+ key === "starred" ||
50
+ key === "taskArtifactOwner",
51
+ ) ||
52
+ (patch.cwd !== undefined && typeof patch.cwd !== "string") ||
53
+ (patch.title !== undefined && typeof patch.title !== "string") ||
54
+ (patch.titleSource !== undefined && patch.titleSource !== "auto" && patch.titleSource !== "user") ||
55
+ (patch.starred !== undefined && typeof patch.starred !== "boolean")
56
+ )
57
+ throw new Error("task_artifact_owner_patch_invalid");
58
+ try {
59
+ locator = parseTaskArtifactOwnerLocator(patch.taskArtifactOwner);
60
+ } catch {
61
+ throw new Error("task_artifact_owner_patch_invalid");
62
+ }
63
+ }
64
+ if (newline < 0) break;
65
+ start = newline + 1;
66
+ }
67
+ return locator;
68
+ }