@gajae-code/coding-agent 0.12.1 → 0.12.2

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 (63) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/types/config/settings-schema.d.ts +2 -2
  3. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  4. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
  5. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  6. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  7. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  8. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  9. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  10. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  11. package/dist/types/modes/interactive-mode.d.ts +9 -0
  12. package/dist/types/modes/theme/theme.d.ts +3 -1
  13. package/dist/types/modes/types.d.ts +32 -0
  14. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  15. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
  16. package/dist/types/session/agent-session.d.ts +2 -0
  17. package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
  18. package/dist/types/task/index.d.ts +4 -0
  19. package/dist/types/task/output-manager.d.ts +1 -0
  20. package/dist/types/task/receipt.d.ts +2 -0
  21. package/dist/types/tools/index.d.ts +2 -0
  22. package/package.json +8 -8
  23. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  24. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  25. package/scripts/run-acp-conformance.ts +5 -0
  26. package/src/async/job-manager.ts +22 -20
  27. package/src/config/settings-schema.ts +2 -2
  28. package/src/debug/index.ts +42 -17
  29. package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
  30. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
  31. package/src/gjc-runtime/launch-worktree.ts +103 -1
  32. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  33. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  34. package/src/gjc-runtime/ralplan-runtime.ts +80 -10
  35. package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
  36. package/src/gjc-runtime/ultragoal-runtime.ts +236 -3
  37. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  38. package/src/gjc-runtime/workflow-manifest.ts +17 -2
  39. package/src/internal-urls/docs-index.generated.ts +1 -1
  40. package/src/modes/DESIGN.md +53 -0
  41. package/src/modes/components/irc-sidebar.ts +30 -1
  42. package/src/modes/components/tool-status-header.ts +0 -1
  43. package/src/modes/controllers/command-controller.ts +38 -34
  44. package/src/modes/controllers/event-controller.ts +27 -16
  45. package/src/modes/controllers/extension-ui-controller.ts +186 -34
  46. package/src/modes/controllers/selector-controller.ts +115 -22
  47. package/src/modes/interactive-mode.ts +189 -62
  48. package/src/modes/jobs-observer.ts +1 -1
  49. package/src/modes/theme/theme.ts +11 -9
  50. package/src/modes/types.ts +88 -0
  51. package/src/sdk/broker/lifecycle.ts +18 -13
  52. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  53. package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
  54. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  55. package/src/sdk/session.ts +1 -0
  56. package/src/session/agent-session.ts +22 -1
  57. package/src/session/internal/managed-session-scope.ts +158 -107
  58. package/src/session/internal/managed-session-storage.ts +445 -57
  59. package/src/session/session-storage.ts +29 -44
  60. package/src/task/index.ts +257 -31
  61. package/src/task/output-manager.ts +24 -14
  62. package/src/task/receipt.ts +11 -1
  63. package/src/tools/index.ts +2 -0
@@ -8,7 +8,7 @@ import {
8
8
  verifyOwnerOnlyPathSecurity,
9
9
  verifyOwnerOnlyPathSecurityExpected,
10
10
  } from "@gajae-code/natives";
11
- import { logger, pathIsWithin } from "@gajae-code/utils";
11
+ import { hasFsCode, logger, pathIsWithin } from "@gajae-code/utils";
12
12
  import type { ResumeSessionIdentity } from "../session-manager";
13
13
  import {
14
14
  FileSessionStorage,
@@ -31,6 +31,7 @@ import {
31
31
  type ManagedFileSnapshot,
32
32
  ManagedPublishError,
33
33
  ManagedSessionDescendantStore,
34
+ ManagedSessionSecurityError,
34
35
  type ManagedSessionSecurityPolicy,
35
36
  type ManagedStorageLock,
36
37
  managedSecurityFailureClassification,
@@ -127,7 +128,7 @@ function configuredRootPath(scope: ManagedScope): string {
127
128
  const canonical = fs.realpathSync.native(candidate);
128
129
  return suffix.length === 0 ? canonical : path.join(canonical, ...suffix);
129
130
  } catch (error) {
130
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
131
+ if (!hasFsCode(error, "ENOENT")) throw error;
131
132
  const parent = path.dirname(candidate);
132
133
  if (parent === candidate) throw new Error("Configured managed root is unavailable.");
133
134
  suffix.unshift(path.basename(candidate));
@@ -177,6 +178,23 @@ export type ManagedScopeResolution =
177
178
  cause?: { readonly classification: string; readonly diagnostic?: string };
178
179
  };
179
180
 
181
+ function managedScopeFailureCause(error: unknown): { readonly classification: string } {
182
+ return { classification: managedSecurityFailureClassification(error) ?? "binding_invalid" };
183
+ }
184
+
185
+ const managedScopeFailureCodes = new Set([
186
+ "atomic_unavailable",
187
+ "invalid_request",
188
+ "durability_failed",
189
+ "durability_not_provable",
190
+ ]);
191
+
192
+ function managedScopeFailureMessage(error: unknown, fallback: string): string {
193
+ const classification = managedSecurityFailureClassification(error);
194
+ if (classification) return classification;
195
+ return error instanceof Error && managedScopeFailureCodes.has(error.message) ? error.message : fallback;
196
+ }
197
+
180
198
  export interface ManagedCandidate {
181
199
  sessionId: string;
182
200
  path: string;
@@ -277,7 +295,7 @@ function identityFor(cwd: string): NativeIdentity {
277
295
  function verifyExistingManagedScopeDirectory(pathname: string) {
278
296
  if (process.platform !== "win32") return verifyOwnerOnlyPathSecurity(pathname, "directory");
279
297
  const expected = fs.lstatSync(pathname, { bigint: true });
280
- if (!expected.isDirectory() || expected.isSymbolicLink()) throw new Error("Unsafe managed directory");
298
+ if (!expected.isDirectory() || expected.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
281
299
  const verified = verifyOwnerOnlyPathSecurityExpected(pathname, "directory", expected.dev, expected.ino);
282
300
  const current = fs.lstatSync(pathname, { bigint: true });
283
301
  if (
@@ -286,7 +304,7 @@ function verifyExistingManagedScopeDirectory(pathname: string) {
286
304
  current.dev !== expected.dev ||
287
305
  current.ino !== expected.ino
288
306
  )
289
- throw new Error("Managed session directory changed");
307
+ throw new ManagedSessionSecurityError("identity_mismatch");
290
308
  return verified;
291
309
  }
292
310
 
@@ -405,8 +423,18 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
405
423
  try {
406
424
  raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
407
425
  } catch (error) {
408
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
409
- return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
426
+ if (hasFsCode(error, "ENOENT")) return undefined;
427
+ const classification = hasFsCode(error, "EACCES")
428
+ ? "EACCES"
429
+ : hasFsCode(error, "EPERM")
430
+ ? "EPERM"
431
+ : "binding_invalid";
432
+ return {
433
+ kind: "error",
434
+ code: "binding_invalid",
435
+ message: "The managed scope binding is invalid JSON.",
436
+ cause: { classification },
437
+ };
410
438
  }
411
439
  return validateBindingRaw(scope, raw);
412
440
  }
@@ -429,14 +457,16 @@ function resolveManagedScopeInternal(
429
457
  kind: "error",
430
458
  code: "sessions_root_unavailable",
431
459
  message: "The sessions root is not a safe directory.",
460
+ cause: { classification: "reparse_point" },
432
461
  };
433
462
  }
434
463
  } catch (error) {
435
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
464
+ if (!hasFsCode(error, "ENOENT")) {
436
465
  return {
437
466
  kind: "error",
438
467
  code: "sessions_root_unavailable",
439
468
  message: "The sessions root could not be inspected.",
469
+ cause: managedScopeFailureCause(error),
440
470
  };
441
471
  }
442
472
  }
@@ -462,21 +492,28 @@ function resolveManagedScopeInternal(
462
492
  kind: "error",
463
493
  code: "sessions_root_unavailable",
464
494
  message: "The sessions root is not a safe directory.",
495
+ cause: { classification: "reparse_point" },
465
496
  };
466
497
  }
467
498
  } catch (error) {
468
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
499
+ if (!hasFsCode(error, "ENOENT")) {
469
500
  return {
470
501
  kind: "error",
471
502
  code: "sessions_root_unavailable",
472
503
  message: "The sessions root could not be inspected.",
504
+ cause: managedScopeFailureCause(error),
473
505
  };
474
506
  }
475
507
  }
476
508
  try {
477
509
  const directory = fs.lstatSync(scope.directoryPath);
478
510
  if (!directory.isDirectory() || directory.isSymbolicLink()) {
479
- return { kind: "error", code: "binding_invalid", message: "The managed scope path is not a safe directory." };
511
+ return {
512
+ kind: "error",
513
+ code: "binding_invalid",
514
+ message: "The managed scope path is not a safe directory.",
515
+ cause: { classification: "reparse_point" },
516
+ };
480
517
  }
481
518
  const security = validateNativeSecurityResult(
482
519
  verifyExistingManagedScopeDirectory(scope.directoryPath),
@@ -488,11 +525,17 @@ function resolveManagedScopeInternal(
488
525
  kind: "error",
489
526
  code: "binding_invalid",
490
527
  message: "The managed scope security could not be verified.",
528
+ cause: { classification: security.code ?? "acl_verify_failed" },
491
529
  };
492
530
  }
493
531
  } catch (error) {
494
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
495
- return { kind: "error", code: "binding_invalid", message: "The managed scope path could not be inspected." };
532
+ if (!hasFsCode(error, "ENOENT")) {
533
+ return {
534
+ kind: "error",
535
+ code: "binding_invalid",
536
+ message: "The managed scope path could not be inspected.",
537
+ cause: managedScopeFailureCause(error),
538
+ };
496
539
  }
497
540
  }
498
541
  return validateExistingBinding(scope) ?? { kind: "resolved", scope };
@@ -557,7 +600,7 @@ function fsyncManagedParent(pathname: string): void {
557
600
  try {
558
601
  descriptor = fs.openSync(parent, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
559
602
  } catch (error) {
560
- if ((error as NodeJS.ErrnoException).code === "ENOENT" && path.dirname(parent) !== parent) {
603
+ if (hasFsCode(error, "ENOENT") && path.dirname(parent) !== parent) {
561
604
  parent = path.dirname(parent);
562
605
  continue;
563
606
  }
@@ -779,7 +822,7 @@ export async function ensureManagedScope(
779
822
  try {
780
823
  await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root, policy);
781
824
  } catch (error) {
782
- if ((error as Error).message !== "destination_conflict") throw error;
825
+ if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
783
826
  bindingCollision = true;
784
827
  }
785
828
  const validated = validateExistingBinding(scope);
@@ -788,14 +831,14 @@ export async function ensureManagedScope(
788
831
 
789
832
  const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
790
833
  if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
791
- throw new Error("Managed session directory changed");
834
+ throw new ManagedSessionSecurityError("reparse_point");
792
835
  managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
793
836
  return { kind: "resolved", scope };
794
837
  } catch (error) {
795
838
  const publication = error instanceof ManagedPublishError ? error : undefined;
796
839
  const message =
797
840
  publication?.classification ??
798
- (error instanceof Error ? error.message : "The managed scope could not be initialized.");
841
+ managedScopeFailureMessage(error, "The managed scope could not be initialized.");
799
842
  const code =
800
843
  message === "atomic_unavailable" ||
801
844
  message === "invalid_request" ||
@@ -803,14 +846,13 @@ export async function ensureManagedScope(
803
846
  message === "durability_not_provable"
804
847
  ? message
805
848
  : "binding_invalid";
806
-
807
849
  return {
808
850
  kind: "error",
809
851
  code,
810
852
  message,
811
- ...(publication
812
- ? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
813
- : { cause: { classification: code } }),
853
+ cause: publication
854
+ ? { classification: publication.classification, diagnostic: publication.diagnostic }
855
+ : managedScopeFailureCause(error),
814
856
  };
815
857
  }
816
858
  }
@@ -946,7 +988,7 @@ export function prepareManagedSessionScopeForWriteSync(
946
988
  try {
947
989
  store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
948
990
  } catch (error) {
949
- if ((error as Error).message !== "destination_conflict") throw error;
991
+ if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
950
992
  }
951
993
  stage = "binding_read";
952
994
  const capturedBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
@@ -984,8 +1026,7 @@ export function prepareManagedSessionScopeForWriteSync(
984
1026
  } catch (error) {
985
1027
  const publication = error instanceof ManagedPublishError ? error : undefined;
986
1028
  const message =
987
- publication?.classification ??
988
- (error instanceof Error ? error.message : "Managed write protocol setup failed.");
1029
+ publication?.classification ?? managedScopeFailureMessage(error, "Managed write protocol setup failed.");
989
1030
  const code =
990
1031
  message === "atomic_unavailable" ||
991
1032
  message === "invalid_request" ||
@@ -993,14 +1034,14 @@ export function prepareManagedSessionScopeForWriteSync(
993
1034
  message === "durability_not_provable"
994
1035
  ? message
995
1036
  : "binding_invalid";
996
- const securityClassification = managedSecurityFailureClassification(error);
1037
+
997
1038
  return {
998
1039
  kind: "error",
999
1040
  code,
1000
1041
  message,
1001
1042
  cause: publication
1002
1043
  ? { classification: publication.classification, diagnostic: publication.diagnostic }
1003
- : { classification: securityClassification ?? code, diagnostic: `prepare:${stage}` },
1044
+ : { ...managedScopeFailureCause(error), diagnostic: `prepare:${stage}` },
1004
1045
  };
1005
1046
  }
1006
1047
  }
@@ -1763,6 +1804,14 @@ function sameArtifactTreeSnapshot(left: NativeDirectoryTreeSnapshot, right: Nati
1763
1804
  return leftEntries.every((entry, index) => entry === rightEntries[index]);
1764
1805
  }
1765
1806
 
1807
+ function artifactTreePayloadAbsent(snapshot: NativeDirectoryTreeSnapshot): boolean {
1808
+ return (
1809
+ snapshot.entries.length === 1 &&
1810
+ snapshot.entries[0]?.relativePath === "" &&
1811
+ snapshot.entries[0].kind === "directory"
1812
+ );
1813
+ }
1814
+
1766
1815
  function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
1767
1816
  if (!pending.detachedArtifactsPath) return;
1768
1817
  if (!pending.expectedArtifactsIdentity || !pending.expectedArtifactsTree) throw new Error("durability_failed");
@@ -1773,10 +1822,12 @@ function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
1773
1822
  throw new Error("durability_failed");
1774
1823
  }
1775
1824
  const observed = artifactIdentityAt(pending.detachedArtifactsPath);
1825
+ const observedTree = snapshotArtifactTree(pending.detachedArtifactsPath);
1776
1826
  if (
1777
1827
  !observed ||
1778
1828
  !sameArtifactRootIdentity(observed, pending.expectedArtifactsIdentity) ||
1779
- !sameArtifactTreeSnapshot(snapshotArtifactTree(pending.detachedArtifactsPath), pending.expectedArtifactsTree)
1829
+ (!sameArtifactTreeSnapshot(observedTree, pending.expectedArtifactsTree) &&
1830
+ !artifactTreePayloadAbsent(observedTree))
1780
1831
  )
1781
1832
  throw new Error("binding_invalid");
1782
1833
  }
@@ -2002,6 +2053,15 @@ function snapshotArtifactTree(pathname: string): NativeDirectoryTreeSnapshot {
2002
2053
  return result.snapshot;
2003
2054
  }
2004
2055
 
2056
+ function retainedArtifactPayloadAbsent(pathname: string): boolean {
2057
+ try {
2058
+ return artifactTreePayloadAbsent(snapshotArtifactTree(pathname));
2059
+ } catch (error) {
2060
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
2061
+ throw error;
2062
+ }
2063
+ }
2064
+
2005
2065
  function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | undefined): CleanupReceipt {
2006
2066
  const attempt = (pending?.attempt ?? 0) + 1;
2007
2067
  const directory = path.dirname(target.path);
@@ -2962,53 +3022,44 @@ export async function reconcileManagedTombstones(
2962
3022
  }
2963
3023
  if (deletion.kind === "cleanup_pending") {
2964
3024
  const retry = nextCleanupReceipt(target, active);
2965
- await publishCleanupPending(
2966
- scope,
2967
- tombstone,
2968
- {
2969
- ...retry,
2970
- expectedArtifactsIdentity:
2971
- deletion.phase === "artifacts"
2972
- ? deletion.artifactsIdentity
2973
- : active.expectedArtifactsIdentity,
2974
- expectedArtifactsTree:
2975
- deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
2976
- detachedArtifactsPath:
2977
- deletion.phase === "artifacts"
2978
- ? deletion.detachedArtifactsPath
2979
- : active.detachedArtifactsPath,
3025
+ const pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
3026
+ await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
3027
+ if (deletion.phase === "artifacts") {
3028
+ if (!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath)) continue;
3029
+ await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
3030
+ deletion = await new FileSessionStorage().deleteSessionVerified({
3031
+ sessionsRoot: scope.sessionsRoot,
3032
+ transcriptPath: target.path,
3033
+ sessionId: target.sessionId,
3034
+ cwd: target.cwd,
3035
+ transcriptIdentity: target.identity,
3036
+ plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
3037
+ plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
2980
3038
  detachedTranscriptPath:
2981
- deletion.phase === "transcript"
2982
- ? deletion.detachedTranscriptPath
2983
- : active.detachedTranscriptPath,
2984
- retainedArtifactsSuccessorPath:
2985
- deletion.phase === "artifacts"
2986
- ? deletion.retainedSuccessorPath
2987
- : active.retainedArtifactsSuccessorPath,
2988
- retainedArtifactsPlaceholderPath:
2989
- deletion.phase === "artifacts"
2990
- ? deletion.retainedPlaceholderPath
2991
- : active.retainedArtifactsPlaceholderPath,
2992
- retainedArtifactsUnknownPath:
2993
- deletion.phase === "artifacts"
2994
- ? deletion.retainedUnknownPath
2995
- : active.retainedArtifactsUnknownPath,
2996
- retainedTranscriptSuccessorPath:
2997
- deletion.phase === "transcript"
2998
- ? deletion.retainedSuccessorPath
2999
- : active.retainedTranscriptSuccessorPath,
3000
- retainedTranscriptPlaceholderPath:
3001
- deletion.phase === "transcript"
3002
- ? deletion.retainedPlaceholderPath
3003
- : active.retainedTranscriptPlaceholderPath,
3004
- retainedTranscriptUnknownPath:
3005
- deletion.phase === "transcript"
3006
- ? deletion.retainedUnknownPath
3007
- : active.retainedTranscriptUnknownPath,
3008
- },
3009
- lock,
3010
- );
3011
- continue;
3039
+ pendingEvidence.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
3040
+ retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
3041
+ retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
3042
+ retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
3043
+ retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
3044
+ retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
3045
+ retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
3046
+ artifactsRemoved: true,
3047
+ });
3048
+ if (deletion.kind === "cleanup_pending") {
3049
+ if (
3050
+ deletion.phase !== "transcript" ||
3051
+ deletion.detachedTranscriptPath !== pendingEvidence.plannedTranscriptPath
3052
+ )
3053
+ throw new Error("durability_failed");
3054
+ const followup = nextCleanupReceipt(target, pendingEvidence);
3055
+ await publishCleanupPending(
3056
+ scope,
3057
+ tombstone,
3058
+ cleanupPendingEvidence(followup, pendingEvidence, deletion),
3059
+ lock,
3060
+ );
3061
+ }
3062
+ }
3012
3063
  }
3013
3064
  fsyncManagedParent(target.path);
3014
3065
  await publishCleanupCompleted(scope, tombstone, target, lock);
@@ -3022,7 +3073,7 @@ export async function reconcileManagedTombstones(
3022
3073
  }
3023
3074
  }
3024
3075
  } finally {
3025
- if (lock) await lock.release().catch(() => undefined);
3076
+ if (lock) await lock.release();
3026
3077
  }
3027
3078
  }
3028
3079
  }
@@ -3051,8 +3102,7 @@ export async function prepareManagedSessionScopeForWrite(
3051
3102
  } catch (error) {
3052
3103
  const publication = error instanceof ManagedPublishError ? error : undefined;
3053
3104
  const message =
3054
- publication?.classification ??
3055
- (error instanceof Error ? error.message : "Managed write protocol setup failed.");
3105
+ publication?.classification ?? managedScopeFailureMessage(error, "Managed write protocol setup failed.");
3056
3106
  const code =
3057
3107
  message === "atomic_unavailable" ||
3058
3108
  message === "invalid_request" ||
@@ -3064,9 +3114,9 @@ export async function prepareManagedSessionScopeForWrite(
3064
3114
  kind: "error",
3065
3115
  code,
3066
3116
  message,
3067
- ...(publication
3068
- ? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
3069
- : { cause: { classification: code } }),
3117
+ cause: publication
3118
+ ? { classification: publication.classification, diagnostic: publication.diagnostic }
3119
+ : managedScopeFailureCause(error),
3070
3120
  };
3071
3121
  }
3072
3122
  }
@@ -3396,7 +3446,7 @@ export async function openManagedCandidateForWrite(
3396
3446
  : "Managed migration failed.",
3397
3447
  };
3398
3448
  } finally {
3399
- if (lock) await lock.release().catch(() => undefined);
3449
+ if (lock) await lock.release();
3400
3450
  }
3401
3451
  }
3402
3452
 
@@ -3548,51 +3598,52 @@ export async function deleteManagedSessionCandidate(
3548
3598
  )
3549
3599
  throw new Error("durability_failed");
3550
3600
  const retry = nextCleanupReceipt(target, active);
3551
- await publishCleanupPending(scope, tombstone, cleanupPendingEvidence(retry, active, deletion), lock);
3601
+ const pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
3602
+ await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
3552
3603
  if (deletion.phase === "artifacts") {
3553
- // A typed retained artifact root proves durable canonical absence; advance
3554
- // to the transcript phase instead of stalling on the retained evidence.
3555
- await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
3604
+ if (!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath))
3605
+ return {
3606
+ kind: "cleanup_pending",
3607
+ tombstonePath: tombstone,
3608
+ phase: deletion.phase,
3609
+ message: "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
3610
+ };
3611
+ await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
3556
3612
  deletion = await new FileSessionStorage().deleteSessionVerified({
3557
3613
  sessionsRoot: scope.sessionsRoot,
3558
3614
  transcriptPath: target.path,
3559
3615
  sessionId: target.sessionId,
3560
3616
  cwd: target.cwd,
3561
3617
  transcriptIdentity: target.identity,
3562
- plannedArtifactsPath: active.plannedArtifactsPath,
3563
- plannedTranscriptPath: active.plannedTranscriptPath,
3564
- detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
3565
- retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
3566
- retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
3567
- retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
3568
- retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
3569
- retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
3570
- retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
3618
+ plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
3619
+ plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
3620
+ detachedTranscriptPath:
3621
+ pendingEvidence.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
3622
+ retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
3623
+ retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
3624
+ retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
3625
+ retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
3626
+ retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
3627
+ retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
3571
3628
  artifactsRemoved: true,
3572
3629
  });
3573
3630
  if (deletion.kind === "cleanup_pending") {
3574
- const followup = nextCleanupReceipt(target, active);
3631
+ if (
3632
+ deletion.phase !== "transcript" ||
3633
+ deletion.detachedTranscriptPath !== pendingEvidence.plannedTranscriptPath
3634
+ )
3635
+ throw new Error("durability_failed");
3636
+ const followup = nextCleanupReceipt(target, pendingEvidence);
3575
3637
  await publishCleanupPending(
3576
3638
  scope,
3577
3639
  tombstone,
3578
- cleanupPendingEvidence(followup, active, deletion),
3640
+ cleanupPendingEvidence(followup, pendingEvidence, deletion),
3579
3641
  lock,
3580
3642
  );
3581
- if (
3582
- deletion.phase !== "transcript" ||
3583
- deletion.detachedTranscriptPath !== active.plannedTranscriptPath
3584
- )
3585
- return {
3586
- kind: "cleanup_pending",
3587
- tombstonePath: tombstone,
3588
- phase: deletion.phase,
3589
- message:
3590
- "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
3591
- };
3592
3643
  }
3593
3644
  }
3594
- // A typed retained transcript quarantine is durable canonical-absence
3595
- // evidence — never a terminal byte-deletion claim; complete the receipt.
3645
+ // A retained transcript quarantine proves canonical absence and remains
3646
+ // identity-bound in the durable pending receipt.
3596
3647
  }
3597
3648
  fsyncManagedParent(target.path);
3598
3649
  await publishCleanupCompleted(scope, tombstone, target, lock);
@@ -3602,6 +3653,6 @@ export async function deleteManagedSessionCandidate(
3602
3653
  const code = expectedFailure(error);
3603
3654
  return { kind: "error", code, message: error instanceof Error ? error.message : "Managed deletion failed." };
3604
3655
  } finally {
3605
- if (lock) await lock.release().catch(() => undefined);
3656
+ if (lock) await lock.release();
3606
3657
  }
3607
3658
  }