@gajae-code/coding-agent 0.11.6 → 0.11.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 (131) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/commands/daemon.d.ts +2 -2
  5. package/dist/types/commands/sdk.d.ts +20 -2
  6. package/dist/types/config/keybindings.d.ts +1 -1
  7. package/dist/types/config/settings-schema.d.ts +56 -6
  8. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  9. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
  10. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
  12. package/dist/types/main.d.ts +24 -1
  13. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  14. package/dist/types/modes/components/model-selector.d.ts +2 -0
  15. package/dist/types/modes/components/tool-status-header.d.ts +1 -0
  16. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  17. package/dist/types/modes/interactive-mode.d.ts +2 -0
  18. package/dist/types/modes/types.d.ts +1 -0
  19. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  21. package/dist/types/sdk/bus/index.d.ts +1 -1
  22. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  23. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  27. package/dist/types/sdk/client/discovery.d.ts +2 -0
  28. package/dist/types/sdk/client/liveness.d.ts +7 -0
  29. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  30. package/dist/types/sdk/transport/index.d.ts +12 -0
  31. package/dist/types/sdk/transport/relay.d.ts +43 -0
  32. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  33. package/dist/types/sdk/transport/socket.d.ts +7 -0
  34. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  35. package/dist/types/session/agent-session.d.ts +28 -5
  36. package/dist/types/session/btw-contract.d.ts +16 -0
  37. package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
  38. package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
  39. package/dist/types/session/session-manager.d.ts +25 -4
  40. package/dist/types/setup/credential-import.d.ts +5 -1
  41. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  42. package/dist/types/ssh/utils.d.ts +1 -0
  43. package/dist/types/task/discovery.d.ts +3 -1
  44. package/dist/types/tools/fetch.d.ts +5 -8
  45. package/dist/types/tools/image-gen.d.ts +16 -0
  46. package/dist/types/tools/skill-discovery.d.ts +7 -0
  47. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  48. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  49. package/dist/types/web/insane/bridge.d.ts +1 -0
  50. package/dist/types/web/insane/url-guard.d.ts +17 -0
  51. package/package.json +8 -8
  52. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  53. package/src/capability/index.ts +50 -42
  54. package/src/capability/types.ts +3 -0
  55. package/src/cli/setup-cli.ts +10 -4
  56. package/src/cli.ts +34 -9
  57. package/src/commands/daemon.ts +22 -5
  58. package/src/commands/sdk.ts +42 -9
  59. package/src/config/keybindings.ts +1 -1
  60. package/src/config/model-profiles.ts +18 -0
  61. package/src/config/settings-schema.ts +57 -6
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
  63. package/src/discovery/ssh.ts +12 -1
  64. package/src/eval/py/kernel.ts +1 -21
  65. package/src/eval/py/runner-artifact.ts +84 -0
  66. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  67. package/src/gjc-runtime/ultragoal-guard.ts +88 -10
  68. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
  69. package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
  70. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  71. package/src/gjc-runtime/workflow-manifest.ts +23 -0
  72. package/src/internal-urls/docs-index.generated.ts +8 -7
  73. package/src/internal-urls/local-protocol.ts +32 -6
  74. package/src/main.ts +86 -27
  75. package/src/modes/action-registry.ts +1 -1
  76. package/src/modes/components/btw-panel.ts +82 -16
  77. package/src/modes/components/model-selector.ts +31 -3
  78. package/src/modes/components/tool-status-header.ts +6 -1
  79. package/src/modes/controllers/btw-controller.ts +119 -36
  80. package/src/modes/controllers/input-controller.ts +31 -0
  81. package/src/modes/controllers/selector-controller.ts +106 -12
  82. package/src/modes/interactive-mode.ts +16 -7
  83. package/src/modes/types.ts +1 -0
  84. package/src/prompts/system/btw-user.md +3 -8
  85. package/src/prompts/tools/skill-discovery.md +1 -0
  86. package/src/runtime-mcp/client.ts +41 -55
  87. package/src/runtime-mcp/content-limits.ts +71 -0
  88. package/src/runtime-mcp/json-rpc.ts +23 -9
  89. package/src/runtime-mcp/transports/http.ts +50 -16
  90. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  91. package/src/sdk/bus/index.ts +4 -5
  92. package/src/sdk/bus/lifecycle-commands.ts +16 -7
  93. package/src/sdk/bus/notification-service.ts +41 -18
  94. package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
  95. package/src/sdk/bus/telegram-daemon-control.ts +2 -2
  96. package/src/sdk/bus/telegram-daemon.ts +1663 -375
  97. package/src/sdk/bus/topic-registry.ts +451 -12
  98. package/src/sdk/client/discovery.ts +19 -3
  99. package/src/sdk/client/liveness.ts +49 -0
  100. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  101. package/src/sdk/session.ts +29 -5
  102. package/src/sdk/transport/auth-preface.ts +82 -0
  103. package/src/sdk/transport/index.ts +18 -0
  104. package/src/sdk/transport/relay.ts +252 -0
  105. package/src/sdk/transport/serve-cli.ts +105 -0
  106. package/src/sdk/transport/socket.ts +197 -0
  107. package/src/sdk/transport/stdio.ts +17 -0
  108. package/src/session/agent-session.ts +240 -149
  109. package/src/session/blob-store.ts +19 -6
  110. package/src/session/btw-contract.ts +59 -0
  111. package/src/session/internal/managed-session-scope.ts +313 -57
  112. package/src/session/internal/managed-session-storage.ts +146 -35
  113. package/src/session/session-manager.ts +319 -85
  114. package/src/setup/credential-import.ts +28 -3
  115. package/src/setup/provider-onboarding.ts +30 -1
  116. package/src/setup/provider-presets.json +2 -1
  117. package/src/slash-commands/builtin-registry.ts +1 -1
  118. package/src/ssh/utils.ts +14 -0
  119. package/src/task/discovery.ts +8 -2
  120. package/src/tools/ask.ts +1 -1
  121. package/src/tools/fetch.ts +13 -131
  122. package/src/tools/image-gen.ts +301 -20
  123. package/src/tools/read.ts +2 -0
  124. package/src/tools/skill-discovery.ts +39 -3
  125. package/src/tools/sqlite-reader.ts +63 -5
  126. package/src/utils/pasted-image-path.ts +15 -6
  127. package/src/utils/shell-snapshot.ts +173 -63
  128. package/src/web/insane/bridge.ts +5 -1
  129. package/src/web/insane/url-guard.ts +138 -27
  130. package/src/web/scrapers/types.ts +19 -49
  131. package/src/web/scrapers/utils.ts +12 -26
@@ -18,7 +18,6 @@ import * as native from "@gajae-code/natives";
18
18
  import { getTerminalId } from "@gajae-code/tui";
19
19
  import {
20
20
  getBlobsDir,
21
- getAgentDir as getDefaultAgentDir,
22
21
  getProjectDir,
23
22
  getSessionsDir,
24
23
  getTerminalSessionsDir,
@@ -58,6 +57,7 @@ import {
58
57
  canonicalizeTrustedPath,
59
58
  deleteManagedSessionCandidate,
60
59
  listManagedCandidates,
60
+ type ManagedCandidateWriteAuthority,
61
61
  type ManagedMigrationPolicy,
62
62
  type ManagedScope,
63
63
  managedDirectoryAuthorityForScope,
@@ -65,11 +65,14 @@ import {
65
65
  openManagedCandidateForWrite,
66
66
  prepareManagedSessionScopeForWriteSync,
67
67
  resolveManagedScope,
68
+ resolveManagedScopeForWrite,
68
69
  } from "./internal/managed-session-scope";
69
70
  import {
71
+ assertManagedDirectoryRoot,
70
72
  type ManagedDirectoryRoot,
71
73
  type ManagedFileSnapshot,
72
74
  ManagedSessionDescendantStore,
75
+ type ManagedSessionSecurityPolicy,
73
76
  retainManagedDirectoryAuthority,
74
77
  } from "./internal/managed-session-storage";
75
78
 
@@ -1324,9 +1327,12 @@ function computeDefaultSessionDir(
1324
1327
  sessionsRoot: string = getSessionsDir(),
1325
1328
  ): string {
1326
1329
  if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
1327
- const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
1330
+ const resolved = resolveManagedScopeForWrite({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
1328
1331
  if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
1329
- const prepared = prepareManagedSessionScopeForWriteSync(resolved.scope);
1332
+ const prepared = prepareManagedSessionScopeForWriteSync(
1333
+ resolved.scope,
1334
+ process.platform === "win32" ? "windows-existing-verify-first" : "default",
1335
+ );
1330
1336
  if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
1331
1337
  return prepared.scope.directoryPath;
1332
1338
  }
@@ -1408,6 +1414,13 @@ function managedFileSnapshotEquals(left: ManagedFileSnapshot | null, right: Mana
1408
1414
  );
1409
1415
  }
1410
1416
  const trustedSessionDestinations = new WeakSet<SessionDestination>();
1417
+ const managedSecurityPolicies = new WeakMap<ManagedSessionSecurityContext, ManagedSessionSecurityPolicy>();
1418
+
1419
+ function managedSecurityPolicyForContext(securityContext: ManagedSessionSecurityContext): ManagedSessionSecurityPolicy {
1420
+ const policy = managedSecurityPolicies.get(securityContext);
1421
+ if (!policy) throw new Error("Managed session security policy unavailable");
1422
+ return policy;
1423
+ }
1411
1424
 
1412
1425
  function managedStoreFromContext(
1413
1426
  securityContext: ManagedSessionSecurityContext,
@@ -1419,6 +1432,7 @@ function managedStoreFromContext(
1419
1432
  securityContext.retainedAuthority
1420
1433
  ? { authority: securityContext.retainedAuthority, authorityBaseDir: directory }
1421
1434
  : undefined,
1435
+ managedSecurityPolicyForContext(securityContext),
1422
1436
  );
1423
1437
  }
1424
1438
 
@@ -1431,6 +1445,10 @@ function freezeManagedDestination(scope: ManagedScope): SessionDestination {
1431
1445
  rootAuthority,
1432
1446
  retainedAuthority: managedDirectoryAuthorityForScope(scope),
1433
1447
  });
1448
+ managedSecurityPolicies.set(
1449
+ securityContext,
1450
+ scope.platform === "win32" ? "windows-existing-verify-first" : "default",
1451
+ );
1434
1452
  const destination = Object.freeze({ kind: "managed" as const, directory: scope.directoryPath, securityContext });
1435
1453
  trustedSessionDestinations.add(destination);
1436
1454
  return destination;
@@ -1445,9 +1463,16 @@ function explicitDestination(directory: string): SessionDestination {
1445
1463
  function managedDestination(cwd: string, storage: SessionStorage, agentDir?: string): SessionDestination {
1446
1464
  if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
1447
1465
  const sessionsRoot = getSessionsDir(agentDir);
1448
- const resolved = resolveManagedScope({ cwd, agentDir: agentDir ?? path.resolve(sessionsRoot, ".."), sessionsRoot });
1466
+ const resolved = resolveManagedScopeForWrite({
1467
+ cwd,
1468
+ agentDir: agentDir ?? path.resolve(sessionsRoot, ".."),
1469
+ sessionsRoot,
1470
+ });
1449
1471
  if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
1450
- const prepared = prepareManagedSessionScopeForWriteSync(resolved.scope);
1472
+ const prepared = prepareManagedSessionScopeForWriteSync(
1473
+ resolved.scope,
1474
+ process.platform === "win32" ? "windows-existing-verify-first" : "default",
1475
+ );
1451
1476
  if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
1452
1477
  return freezeManagedDestination(prepared.scope);
1453
1478
  }
@@ -1814,12 +1839,46 @@ function inspectResumeSessionFile(
1814
1839
  mtimeNs: snapshot.mtimeNs,
1815
1840
  sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
1816
1841
  };
1817
- return { identity, content: Uint8Array.from(bytes), entries, context, migrationApplied };
1842
+ return { identity, content: bytes, entries, context, migrationApplied };
1818
1843
  } catch {
1819
1844
  return { kind: "error", reason: "malformed" };
1820
1845
  }
1821
1846
  }
1822
1847
 
1848
+ /** Revalidate previously parsed authority without another decode, parse, migration, or context build. */
1849
+ function revalidateResumeSessionIdentity(
1850
+ filePath: string,
1851
+ storage: SessionStorage,
1852
+ expected: ResumeSessionIdentity,
1853
+ ): StrictSessionOpenFailure | { kind: "valid" } {
1854
+ const canonicalPath = resolveEquivalentPath(path.resolve(filePath));
1855
+ let before: SessionStorageStat;
1856
+ let snapshot: SessionStorageSnapshot;
1857
+ let after: SessionStorageStat;
1858
+ try {
1859
+ before = storage.statSync(canonicalPath);
1860
+ if (!before.isFile || !storage.readSnapshotSync) return { kind: "error", reason: "read-failed" };
1861
+ snapshot = storage.readSnapshotSync(canonicalPath);
1862
+ after = storage.statSync(canonicalPath);
1863
+ } catch (error) {
1864
+ return resumeReadFailure(error, storage, canonicalPath);
1865
+ }
1866
+ if (!sameResumeStat(before, snapshot.stat) || !sameResumeStat(snapshot.stat, after)) {
1867
+ return { kind: "error", reason: "unstable" };
1868
+ }
1869
+ const observed: ResumeSessionIdentity = {
1870
+ canonicalPath,
1871
+ sessionId: expected.sessionId,
1872
+ dev: snapshot.stat.dev,
1873
+ ino: snapshot.stat.ino,
1874
+ size: snapshot.stat.size,
1875
+ mtimeMs: snapshot.stat.mtimeMs,
1876
+ mtimeNs: snapshot.stat.mtimeNs,
1877
+ sha256: crypto.createHash("sha256").update(snapshot.bytes).digest("hex"),
1878
+ };
1879
+ return sameResumeIdentity(expected, observed) ? { kind: "valid" } : { kind: "error", reason: "identity-mismatch" };
1880
+ }
1881
+
1823
1882
  /**
1824
1883
  * Convert legacy persisted blob references in loaded entries into resident sentinels.
1825
1884
  * Images then materialize lazily at provider/display/export chokepoints instead of
@@ -3867,9 +3926,11 @@ export async function resolveResumableSession(
3867
3926
  cwd: string,
3868
3927
  sessionDir?: string,
3869
3928
  storage: SessionStorage = new FileSessionStorage(),
3929
+ managedAgentDir?: string,
3870
3930
  ): Promise<ResolvedSessionMatch | undefined> {
3871
- const localSessionDir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
3872
- const localSessions = await SessionManager.list(cwd, localSessionDir, storage);
3931
+ const localSessions = sessionDir
3932
+ ? await SessionManager.list(cwd, sessionDir, storage)
3933
+ : await SessionManager.listManagedForResumePickerReadOnly(cwd, managedAgentDir, storage);
3873
3934
  const localMatch = localSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
3874
3935
  if (localMatch) {
3875
3936
  return { session: localMatch, scope: "local" };
@@ -3879,7 +3940,7 @@ export async function resolveResumableSession(
3879
3940
  return undefined;
3880
3941
  }
3881
3942
 
3882
- const globalSessions = await SessionManager.listAll(storage);
3943
+ const globalSessions = await SessionManager.listAll(storage, managedAgentDir);
3883
3944
  const globalMatch = globalSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
3884
3945
  if (!globalMatch) {
3885
3946
  return undefined;
@@ -3911,6 +3972,7 @@ export class SessionManager {
3911
3972
  #ensuredOnDisk: boolean = false;
3912
3973
  #recoveryHydrationContext: RecoveryHydrationContext | undefined;
3913
3974
  #fileEntries: FileEntry[] = [];
3975
+ #pendingStrictAdoption: { canonicalPath: string; identity: ResumeSessionIdentity } | undefined;
3914
3976
  #byId: Map<string, SessionEntry> = new Map();
3915
3977
  #labelsById: Map<string, string> = new Map();
3916
3978
  #leafId: string | null = null;
@@ -4157,15 +4219,16 @@ export class SessionManager {
4157
4219
  }
4158
4220
 
4159
4221
  async #hydrateExistingSession(sessionFile: string, entries: FileEntry[], migrationApplied: boolean): Promise<void> {
4160
- const ownedEntries = structuredClone(entries) as FileEntry[];
4161
- const header = ownedEntries[0] as SessionHeader;
4222
+ // Strict inspection creates these entries solely for this hydration path. Adopt that
4223
+ // final, validated representation instead of cloning the complete transcript again.
4224
+ const header = entries[0] as SessionHeader;
4162
4225
  this.#sessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
4163
4226
  this.#sessionId = header.id;
4164
4227
  this.#sessionName = header.title;
4165
4228
  this.#titleSource = header.titleSource;
4166
4229
  this.#needsFullRewriteOnNextPersist = migrationApplied;
4167
- await resolveBlobRefsInEntries(ownedEntries, this.#blobStore);
4168
- this.#fileEntries = ownedEntries;
4230
+ await resolveBlobRefsInEntries(entries, this.#blobStore);
4231
+ this.#fileEntries = entries;
4169
4232
  this.#resetResidentTextBlobStore();
4170
4233
  this.#fileEntries = this.#fileEntries.map(entry =>
4171
4234
  prepareEntryForResidentSync(entry, this.#residentBlobStores()),
@@ -4184,12 +4247,25 @@ export class SessionManager {
4184
4247
 
4185
4248
  /** Switch to a different session file (used for resume and branching) */
4186
4249
  async setSessionFile(sessionFile: string): Promise<void> {
4250
+ const resolvedSessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
4251
+ const strictAdoption = this.#pendingStrictAdoption;
4252
+ this.#pendingStrictAdoption = undefined;
4253
+ if (strictAdoption && resolvedSessionFile === strictAdoption.canonicalPath) {
4254
+ const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
4255
+ if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
4256
+ throw new Error("Prepared managed session changed before strict adoption.");
4257
+ }
4187
4258
  await this.#closePersistWriter();
4188
4259
  this.#persistError = undefined;
4189
4260
  this.#persistErrorReported = false;
4190
- this.#sessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
4261
+ this.#sessionFile = resolvedSessionFile;
4191
4262
  writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
4192
4263
  this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
4264
+ if (strictAdoption) {
4265
+ const inspected = inspectResumeSessionFile(this.#sessionFile, this.storage);
4266
+ if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
4267
+ throw new Error("Prepared managed session changed during strict adoption.");
4268
+ }
4193
4269
  if (this.#fileEntries.length > 0) {
4194
4270
  const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
4195
4271
  this.#sessionId = header?.id ?? createSessionId();
@@ -4533,17 +4609,14 @@ export class SessionManager {
4533
4609
  const previousDestination = this.destination;
4534
4610
  const previousArtifactDir = previousSessionFile?.slice(0, -6);
4535
4611
 
4536
- const nextDestination = (() => {
4537
- if (this.destination.kind === "managed") {
4538
- return managedDestination(resolvedCwd, this.storage, this.destination.securityContext.agentDir);
4539
- }
4540
- if (!(this.storage instanceof FileSessionStorage)) return this.destination;
4541
- const managedSessionsRoot = resolveManagedSessionRoot(this.sessionDir, this.cwd);
4542
- const directory = managedSessionsRoot
4543
- ? computeDefaultSessionDir(resolvedCwd, this.storage, managedSessionsRoot)
4544
- : computeDefaultSessionDir(resolvedCwd, this.storage);
4545
- return explicitDestination(directory);
4546
- })();
4612
+ const nextDestination =
4613
+ this.storage instanceof FileSessionStorage
4614
+ ? managedDestination(
4615
+ resolvedCwd,
4616
+ this.storage,
4617
+ this.destination.kind === "managed" ? this.destination.securityContext.agentDir : undefined,
4618
+ )
4619
+ : this.destination;
4547
4620
  const newSessionDir = nextDestination.directory;
4548
4621
  let hadSessionFile = false;
4549
4622
  const managedMove = this.destination.kind === "managed" && nextDestination.kind === "managed";
@@ -5523,6 +5596,17 @@ export class SessionManager {
5523
5596
  return this.sessionDir;
5524
5597
  }
5525
5598
 
5599
+ /** Lists picker candidates within this manager's captured destination authority. */
5600
+ async listForResumePickerReadOnly(): Promise<SessionInfo[]> {
5601
+ return this.destination.kind === "managed"
5602
+ ? await SessionManager.listManagedForResumePickerReadOnly(
5603
+ this.cwd,
5604
+ this.destination.securityContext.agentDir,
5605
+ this.storage,
5606
+ )
5607
+ : await SessionManager.listForResumePickerReadOnly(this.cwd, this.destination.directory, this.storage);
5608
+ }
5609
+
5526
5610
  getSessionId(): string {
5527
5611
  return this.#sessionId;
5528
5612
  }
@@ -5552,13 +5636,22 @@ export class SessionManager {
5552
5636
  getManagedLegacyLocalMigrationSource(): ManagedLegacyLocalMigrationSource | null {
5553
5637
  if (this.destination.kind !== "managed" || !this.#sessionFile || this.#adoptedArtifactManager) return null;
5554
5638
  const store = this.#managedTranscriptStore();
5639
+ const legacyArtifactsRoot = path.basename(this.#sessionFile.slice(0, -6));
5640
+ const legacyLocalRoot = path.posix.join(legacyArtifactsRoot, "local");
5555
5641
  return {
5556
5642
  capture: async () => {
5557
5643
  let snapshot: native.NativeDirectoryTreeSnapshot;
5558
5644
  try {
5559
- snapshot = store.captureTree("local");
5645
+ snapshot = store.captureTree(legacyLocalRoot);
5560
5646
  } catch (error) {
5561
5647
  if (error instanceof Error && error.message === "not_found") return null;
5648
+ if (error instanceof Error && error.message === "reparse_point") {
5649
+ try {
5650
+ store.captureTree(legacyArtifactsRoot);
5651
+ } catch (artifactsError) {
5652
+ if (artifactsError instanceof Error && artifactsError.message === "not_found") return null;
5653
+ }
5654
+ }
5562
5655
  throw error;
5563
5656
  }
5564
5657
  let totalBytes = 0;
@@ -5566,7 +5659,7 @@ export class SessionManager {
5566
5659
  if (totalBytes > 64 * 1024 * 1024) throw new Error("Legacy local:// migration exceeds the safe size limit");
5567
5660
  const entries = snapshot.entries.map(entry => {
5568
5661
  if (entry.kind === "directory") return { relativePath: entry.relativePath, kind: "directory" as const };
5569
- const captured = store.readExpected(path.posix.join("local", entry.relativePath));
5662
+ const captured = store.readExpected(path.posix.join(legacyLocalRoot, entry.relativePath));
5570
5663
  if (
5571
5664
  !captured ||
5572
5665
  captured.identity.dev.toString() !== entry.dev ||
@@ -5582,12 +5675,12 @@ export class SessionManager {
5582
5675
  sha256: entry.sha256,
5583
5676
  };
5584
5677
  });
5585
- const verified = store.captureTree("local");
5678
+ const verified = store.captureTree(legacyLocalRoot);
5586
5679
  if (JSON.stringify(verified) !== JSON.stringify(snapshot))
5587
5680
  throw new Error("Legacy local:// migration source changed during capture");
5588
5681
  return { snapshot, entries };
5589
5682
  },
5590
- retire: snapshot => store.removeTreeExpected("local", snapshot),
5683
+ retire: snapshot => store.removeTreeExpected(legacyLocalRoot, snapshot),
5591
5684
  };
5592
5685
  }
5593
5686
 
@@ -7080,6 +7173,12 @@ export class SessionManager {
7080
7173
  authority instanceof ManagedSessionDescendantStore
7081
7174
  ? authority.retainAuthority()
7082
7175
  : retainManagedDirectoryAuthority(rootAuthority, directory);
7176
+ const policy =
7177
+ authority instanceof ManagedSessionDescendantStore
7178
+ ? authority.securityPolicy
7179
+ : process.platform === "win32"
7180
+ ? "windows-existing-verify-first"
7181
+ : "default";
7083
7182
  const securityContext = createManagedSessionSecurityContext({
7084
7183
  agentDir: rootAuthority.canonicalPath,
7085
7184
  sessionsRoot: rootAuthority.canonicalPath,
@@ -7087,6 +7186,7 @@ export class SessionManager {
7087
7186
  rootAuthority,
7088
7187
  retainedAuthority,
7089
7188
  });
7189
+ managedSecurityPolicies.set(securityContext, policy);
7090
7190
  const destination = Object.freeze({ kind: "managed" as const, directory, securityContext });
7091
7191
  trustedSessionDestinations.add(destination);
7092
7192
  return destination;
@@ -7115,20 +7215,59 @@ export class SessionManager {
7115
7215
  return manager;
7116
7216
  }
7117
7217
 
7218
+ /** Prepare a selected candidate using this manager's captured managed destination authority. */
7219
+ async prepareManagedCandidateForWrite(
7220
+ filePath: string,
7221
+ migrationPolicy: SessionDirectoryMigrationPolicy,
7222
+ expectedIdentity?: ResumeSessionIdentity,
7223
+ ): Promise<string> {
7224
+ return SessionManager.prepareManagedCandidateForWrite(
7225
+ filePath,
7226
+ migrationPolicy,
7227
+ this.destination,
7228
+ expectedIdentity,
7229
+ );
7230
+ }
7231
+
7232
+ /** Prepare a managed candidate and retain its exact post-preparation identity for the next adoption. */
7233
+ async prepareManagedCandidateForStrictAdoption(
7234
+ filePath: string,
7235
+ migrationPolicy: SessionDirectoryMigrationPolicy,
7236
+ expectedIdentity: ResumeSessionIdentity,
7237
+ ): Promise<string> {
7238
+ const preparedPath = await this.prepareManagedCandidateForWrite(filePath, migrationPolicy, expectedIdentity);
7239
+ const inspected = inspectResumeSessionFile(preparedPath, this.storage);
7240
+ if ("kind" in inspected) throw new Error(`Could not inspect prepared managed session: ${inspected.reason}`);
7241
+ this.#pendingStrictAdoption = { canonicalPath: inspected.identity.canonicalPath, identity: inspected.identity };
7242
+ return preparedPath;
7243
+ }
7244
+
7118
7245
  /** Resolve a default-managed candidate through binding validation and copy-retain migration before mutation. */
7119
7246
  static async prepareManagedCandidateForWrite(
7120
7247
  filePath: string,
7121
7248
  migrationPolicy: SessionDirectoryMigrationPolicy,
7249
+ destination: SessionDestination,
7250
+ expectedIdentity?: ResumeSessionIdentity,
7122
7251
  ): Promise<string> {
7252
+ if (!trustedSessionDestinations.has(destination) || destination.kind !== "managed")
7253
+ throw new Error("Managed candidate preparation requires a trusted managed destination authority");
7123
7254
  const storage = new FileSessionStorage();
7255
+ const managedDestinationStore = managedStoreFromContext(destination.securityContext, destination.directory);
7256
+ const assertManagedDestinationBound = (): void => {
7257
+ assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
7258
+ managedDestinationStore.assertBound();
7259
+ };
7260
+ assertManagedDestinationBound();
7124
7261
  const inspected = inspectResumeSessionFile(filePath, storage);
7125
7262
  if ("kind" in inspected) {
7126
7263
  if (inspected.reason === "missing") return filePath;
7127
7264
  throw new Error(`Could not inspect managed session: ${inspected.reason}`);
7128
7265
  }
7266
+ if (expectedIdentity && !sameResumeIdentity(expectedIdentity, inspected.identity))
7267
+ throw new Error("Managed session changed before migration authority was adopted.");
7129
7268
  const header = inspected.entries.find(entry => entry.type === "session") as SessionHeader | undefined;
7130
7269
  if (!header?.cwd) return filePath;
7131
- const sessionsRoot = getSessionsDir();
7270
+ const sessionsRoot = destination.securityContext.sessionsRoot;
7132
7271
  const resolvedPath = path.resolve(filePath);
7133
7272
  const relativeToManagedRoot = path.relative(sessionsRoot, resolvedPath);
7134
7273
  const isManagedPath =
@@ -7136,14 +7275,17 @@ export class SessionManager {
7136
7275
  (!relativeToManagedRoot.startsWith("..") && !path.isAbsolute(relativeToManagedRoot));
7137
7276
 
7138
7277
  if (!isManagedPath) return filePath;
7278
+ assertManagedDestinationBound();
7139
7279
  const resolved = resolveManagedScope({
7140
7280
  cwd: header.cwd,
7141
- agentDir: path.resolve(sessionsRoot, ".."),
7281
+ agentDir: destination.securityContext.agentDir,
7142
7282
  sessionsRoot,
7143
7283
  });
7144
7284
  if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
7145
7285
 
7286
+ assertManagedDestinationBound();
7146
7287
  const listing = listManagedCandidates(resolved.scope);
7288
+ assertManagedDestinationBound();
7147
7289
  if (listing.kind !== "complete") throw new Error(`Managed session scan failed: ${listing.message}`);
7148
7290
 
7149
7291
  const candidate = listing.owned.find(item => path.resolve(item.path) === resolvedPath);
@@ -7160,7 +7302,24 @@ export class SessionManager {
7160
7302
  candidate.identity.sha256 !== inspected.identity.sha256
7161
7303
  )
7162
7304
  throw new Error("Managed session changed before migration authority was adopted.");
7163
- const opened = await openManagedCandidateForWrite(resolved.scope, candidate, migrationPolicy);
7305
+ assertManagedDestinationBound();
7306
+ const authority: ManagedCandidateWriteAuthority = {
7307
+ rootAuthority: destination.securityContext.rootAuthority,
7308
+ ...(path.resolve(destination.directory) === path.resolve(resolved.scope.directoryPath)
7309
+ ? {
7310
+ retainedAuthority: destination.securityContext.retainedAuthority,
7311
+ retainedDirectory: destination.directory,
7312
+ }
7313
+ : {}),
7314
+ };
7315
+ const opened = await openManagedCandidateForWrite(
7316
+ resolved.scope,
7317
+ candidate,
7318
+ expectedIdentity ?? inspected.identity,
7319
+ migrationPolicy,
7320
+ authority,
7321
+ );
7322
+ assertManagedDestinationBound();
7164
7323
  if (opened.kind === "error") {
7165
7324
  if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
7166
7325
  throw new Error(`Could not open managed session: ${opened.message}`);
@@ -7182,7 +7341,7 @@ export class SessionManager {
7182
7341
  const destination = destinationFor(cwd, destinationInput, storage);
7183
7342
  const managedSourcePath =
7184
7343
  destination.kind === "managed" && storage instanceof FileSessionStorage
7185
- ? await SessionManager.prepareManagedCandidateForWrite(sourcePath, migrationPolicy)
7344
+ ? await SessionManager.prepareManagedCandidateForWrite(sourcePath, migrationPolicy, destination)
7186
7345
  : sourcePath;
7187
7346
  const dir = destination.directory;
7188
7347
  const manager = new SessionManager(cwd, dir, true, storage, destination);
@@ -7246,10 +7405,9 @@ export class SessionManager {
7246
7405
  return manager;
7247
7406
  }
7248
7407
 
7249
- const managedPath = await SessionManager.prepareManagedCandidateForWrite(filePath, migrationPolicy);
7250
- const inspected = inspectResumeSessionFile(managedPath, storage);
7408
+ const inspected = inspectResumeSessionFile(filePath, storage);
7251
7409
  if ("kind" in inspected) {
7252
- if (inspected.reason === "missing" && path.resolve(managedPath) === path.resolve(filePath)) {
7410
+ if (inspected.reason === "missing") {
7253
7411
  const manager = new SessionManager(getProjectDir(), destination.directory, true, storage, destination);
7254
7412
  await manager.#initSessionFile(filePath);
7255
7413
  return manager;
@@ -7317,46 +7475,47 @@ export class SessionManager {
7317
7475
  return manager;
7318
7476
  }
7319
7477
  /**
7320
- * List sessions for the resume picker without recovery or other maintenance writes.
7478
+ * List default-managed sessions for the resume picker without recovery or other
7479
+ * maintenance writes. This is the only picker inventory that includes legacy
7480
+ * sibling directories.
7481
+ */
7482
+ static async listManagedForResumePickerReadOnly(
7483
+ cwd: string,
7484
+ managedAgentDir?: string,
7485
+ storage: SessionStorage = new FileSessionStorage(),
7486
+ ): Promise<SessionInfo[]> {
7487
+ if (!(storage instanceof FileSessionStorage)) return [];
7488
+ const sessionsRoot = getSessionsDir(managedAgentDir);
7489
+ const resolved = resolveManagedScope({
7490
+ cwd,
7491
+ agentDir: managedAgentDir ?? path.resolve(sessionsRoot, ".."),
7492
+ sessionsRoot,
7493
+ });
7494
+ if (resolved.kind === "error") return [];
7495
+ const listing = listManagedCandidates(resolved.scope);
7496
+ if (listing.kind === "error") return [];
7497
+ return await collectSessionsFromFiles(
7498
+ listing.owned.map(candidate => candidate.path),
7499
+ storage,
7500
+ );
7501
+ }
7502
+
7503
+ /**
7504
+ * List sessions from an explicitly supplied picker directory without recovery
7505
+ * or other maintenance writes. Unlike managed inventory, this never scans
7506
+ * legacy sibling directories.
7321
7507
  */
7322
7508
  static async listForResumePickerReadOnly(
7323
7509
  cwd: string,
7324
7510
  sessionDir?: string,
7325
7511
  storage: SessionStorage = new FileSessionStorage(),
7326
7512
  ): Promise<SessionInfo[]> {
7327
- if (!sessionDir && !(storage instanceof FileSessionStorage)) return [];
7328
- if (sessionDir && !resolveManagedSessionRoot(sessionDir, cwd)) {
7329
- try {
7330
- return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
7331
- } catch {
7332
- return [];
7333
- }
7334
- }
7335
- const sessionsRoot = sessionDir
7336
- ? (resolveManagedSessionRoot(sessionDir, cwd) ?? getSessionsDir())
7337
- : getSessionsDir();
7338
- const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
7339
- if (resolved.kind === "error") return [];
7340
- if (storage instanceof FileSessionStorage) {
7341
- const listing = listManagedCandidates(resolved.scope);
7342
- if (listing.kind === "error") return [];
7343
- return await collectSessionsFromFiles(
7344
- listing.owned.map(candidate => candidate.path),
7345
- storage,
7346
- );
7347
- }
7348
- const legacyDirectory = `--${resolved.scope.canonicalCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
7349
- const files = new Set<string>();
7513
+ if (!sessionDir) return await SessionManager.listManagedForResumePickerReadOnly(cwd, undefined, storage);
7350
7514
  try {
7351
- for (const directory of [resolved.scope.directoryPath, path.join(sessionsRoot, legacyDirectory)]) {
7352
- for (const file of storage.listFilesSync(directory, "*.jsonl")) files.add(file);
7353
- }
7515
+ return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
7354
7516
  } catch {
7355
7517
  return [];
7356
7518
  }
7357
- return (await collectSessionsFromFiles([...files], storage)).filter(
7358
- session => resolveEquivalentPath(session.cwd) === resolved.scope.canonicalCwd,
7359
- );
7360
7519
  }
7361
7520
 
7362
7521
  /** Tombstone and exact-delete a default-managed candidate. */
@@ -7658,36 +7817,39 @@ export class SessionManager {
7658
7817
  let sessionPath = identity.canonicalPath;
7659
7818
  if (destination.kind === "managed" && storage instanceof FileSessionStorage) {
7660
7819
  try {
7661
- sessionPath = await SessionManager.prepareManagedCandidateForWrite(sessionPath, migrationPolicy);
7820
+ sessionPath = await SessionManager.prepareManagedCandidateForWrite(
7821
+ sessionPath,
7822
+ migrationPolicy,
7823
+ destination,
7824
+ identity,
7825
+ );
7662
7826
  } catch (error) {
7663
7827
  if (error instanceof SessionMigrationPolicyError)
7664
7828
  return { kind: "error", reason: "legacy_migration_disabled" };
7829
+ if (
7830
+ error instanceof Error &&
7831
+ (error.message.includes("source_changed") || error.message.includes("changed before migration"))
7832
+ )
7833
+ return { kind: "error", reason: "identity-mismatch" };
7665
7834
  throw error;
7666
7835
  }
7667
7836
  }
7668
7837
  const inspected = inspectResumeSessionFile(sessionPath, storage);
7669
7838
  if ("kind" in inspected) return inspected;
7670
- if (
7671
- (sessionPath === identity.canonicalPath && !sameResumeIdentity(identity, inspected.identity)) ||
7672
- (sessionPath !== identity.canonicalPath &&
7673
- (identity.sessionId !== inspected.identity.sessionId || identity.sha256 !== inspected.identity.sha256))
7674
- ) {
7839
+ if (sessionPath === identity.canonicalPath && !sameResumeIdentity(identity, inspected.identity)) {
7675
7840
  return { kind: "error", reason: "identity-mismatch" };
7676
7841
  }
7842
+
7677
7843
  const entries = inspected.entries;
7678
7844
  const header = entries[0] as SessionHeader;
7679
7845
  const dir = destination.directory;
7680
7846
  const manager = new SessionManager(header.cwd || getProjectDir(), dir, true, storage, destination);
7681
7847
  await manager.#hydrateExistingSession(sessionPath, entries, inspected.migrationApplied);
7682
- const ownershipInspection = inspectResumeSessionFile(sessionPath, storage);
7683
- if ("kind" in ownershipInspection) {
7848
+ const ownershipInspection = revalidateResumeSessionIdentity(sessionPath, storage, inspected.identity);
7849
+ if (ownershipInspection.kind === "error") {
7684
7850
  await manager.close();
7685
7851
  return ownershipInspection;
7686
7852
  }
7687
- if (!sameResumeIdentity(inspected.identity, ownershipInspection.identity)) {
7688
- await manager.close();
7689
- return { kind: "error", reason: "identity-mismatch" };
7690
- }
7691
7853
  try {
7692
7854
  await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
7693
7855
  writeTerminalBreadcrumb(manager.cwd, sessionPath);
@@ -7731,9 +7893,20 @@ export class SessionManager {
7731
7893
  const managedCandidates =
7732
7894
  destination.kind === "explicit"
7733
7895
  ? []
7734
- : await SessionManager.listForResumePickerReadOnly(cwd, destination.directory, storage);
7896
+ : await SessionManager.listManagedForResumePickerReadOnly(
7897
+ cwd,
7898
+ destination.securityContext.agentDir,
7899
+ storage,
7900
+ );
7901
+
7735
7902
  const terminalSession = await readTerminalBreadcrumb(cwd);
7736
- if (terminalSession) {
7903
+ const terminalSessionIsInExplicitRoot = (() => {
7904
+ if (destination.kind === "managed" || !terminalSession) return destination.kind === "managed";
7905
+ const canonicalRoot = canonicalizeTrustedPath(destination.directory);
7906
+ const canonicalSessionPath = canonicalizeTrustedPath(terminalSession);
7907
+ return pathIsWithin(canonicalRoot, canonicalSessionPath);
7908
+ })();
7909
+ if (terminalSession && terminalSessionIsInExplicitRoot) {
7737
7910
  const opened = await openSelectedStrictly(terminalSession);
7738
7911
  if (opened) return opened;
7739
7912
  }
@@ -7771,7 +7944,7 @@ export class SessionManager {
7771
7944
  sessionDir?: string,
7772
7945
  storage: SessionStorage = new FileSessionStorage(),
7773
7946
  ): Promise<SessionInfo[]> {
7774
- if (!sessionDir) return await SessionManager.listForResumePickerReadOnly(cwd, undefined, storage);
7947
+ if (!sessionDir) return await SessionManager.listManagedForResumePickerReadOnly(cwd, undefined, storage);
7775
7948
  try {
7776
7949
  await recoverOrphanedBackups(sessionDir, storage);
7777
7950
  return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
@@ -7783,9 +7956,12 @@ export class SessionManager {
7783
7956
  /**
7784
7957
  * List all sessions across all project directories.
7785
7958
  */
7786
- static async listAll(storage: SessionStorage = new FileSessionStorage()): Promise<SessionInfo[]> {
7959
+ static async listAll(
7960
+ storage: SessionStorage = new FileSessionStorage(),
7961
+ managedAgentDir?: string,
7962
+ ): Promise<SessionInfo[]> {
7787
7963
  if (!(storage instanceof FileSessionStorage)) return [];
7788
- const sessionsRoot = path.join(getDefaultAgentDir(), "sessions");
7964
+ const sessionsRoot = getSessionsDir(managedAgentDir);
7789
7965
  try {
7790
7966
  const directories = await fs.promises.readdir(sessionsRoot, { withFileTypes: true });
7791
7967
  const seedFiles = directories
@@ -7821,6 +7997,19 @@ export class SessionManager {
7821
7997
  return [];
7822
7998
  }
7823
7999
  }
8000
+ /**
8001
+ * Strict inventory bound to this manager's captured session authority. Managed
8002
+ * managers include authorized legacy and current candidates; explicit managers
8003
+ * authorize only their exact directory.
8004
+ */
8005
+ inventorySessionsStrict(): StrictInventoryResult {
8006
+ return SessionManager.inventorySessionsStrict(this.cwd, {
8007
+ sessionDir: this.destination.kind === "explicit" ? this.destination.directory : undefined,
8008
+ storage: this.storage,
8009
+ destination: this.destination,
8010
+ });
8011
+ }
8012
+
7824
8013
  /**
7825
8014
  * Strict raw scoped inventory for ACP authorization. Enumerates the scoped
7826
8015
  * session directory without suppressing any root/scan/lstat/read/parse/stat/
@@ -7830,10 +8019,55 @@ export class SessionManager {
7830
8019
  */
7831
8020
  static inventorySessionsStrict(
7832
8021
  cwd: string,
7833
- options?: { sessionDir?: string; storage?: SessionStorage },
8022
+ options?: { sessionDir?: string; storage?: SessionStorage; destination?: SessionDestination },
7834
8023
  ): StrictInventoryResult {
7835
8024
  const storage = options?.storage ?? new FileSessionStorage();
7836
- const dir = options?.sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
8025
+ const destination = options?.destination;
8026
+ const dir =
8027
+ options?.sessionDir ?? destination?.directory ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
8028
+ if (destination?.kind === "managed") {
8029
+ if (!trustedSessionDestinations.has(destination) || !(storage instanceof FileSessionStorage)) {
8030
+ return {
8031
+ kind: "failure",
8032
+ failures: [{ kind: "scan", message: "Managed strict inventory authority is unavailable", path: dir }],
8033
+ };
8034
+ }
8035
+ try {
8036
+ const store = managedStoreFromContext(destination.securityContext, destination.directory);
8037
+ assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
8038
+ store.assertBound();
8039
+ const resolved = resolveManagedScope({
8040
+ cwd,
8041
+ agentDir: destination.securityContext.agentDir,
8042
+ sessionsRoot: destination.securityContext.sessionsRoot,
8043
+ });
8044
+ if (resolved.kind === "error")
8045
+ return { kind: "failure", failures: [{ kind: "root", message: resolved.message, path: dir }] };
8046
+ const listing = listManagedCandidates(resolved.scope);
8047
+ if (listing.kind === "error")
8048
+ return { kind: "failure", failures: [{ kind: "scan", message: listing.message, path: dir }] };
8049
+ const failures: StrictInventoryFailure[] = [];
8050
+ const candidates: StrictInventoryCandidate[] = [];
8051
+ for (const managedCandidate of listing.owned) {
8052
+ const candidate = inventoryReadCandidate(
8053
+ storage,
8054
+ managedCandidate.path,
8055
+ cwd,
8056
+ path.dirname(managedCandidate.path),
8057
+ );
8058
+ if ("failures" in candidate) failures.push(...candidate.failures);
8059
+ else candidates.push(candidate.candidate);
8060
+ }
8061
+ assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
8062
+ store.assertBound();
8063
+ return failures.length > 0 ? { kind: "failure", failures } : { kind: "complete", candidates };
8064
+ } catch {
8065
+ return {
8066
+ kind: "failure",
8067
+ failures: [{ kind: "root", message: "Managed strict inventory authority changed", path: dir }],
8068
+ };
8069
+ }
8070
+ }
7837
8071
  if (!storage.listFilesStrictSync) {
7838
8072
  return {
7839
8073
  kind: "failure",