@gajae-code/coding-agent 0.11.11 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +49 -3
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/commands/deep-interview.d.ts +9 -0
  4. package/dist/types/commands/harness.d.ts +22 -0
  5. package/dist/types/commands/read.d.ts +6 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/provider-auth-health.d.ts +14 -0
  8. package/dist/types/config/provider-ranking.d.ts +59 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/config/settings.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +22 -2
  12. package/dist/types/extensibility/shared-events.d.ts +6 -0
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  14. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  15. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  16. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  17. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  19. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
  20. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  21. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  22. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  23. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  24. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  26. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  27. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  28. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  29. package/dist/types/main.d.ts +1 -1
  30. package/dist/types/memories/index.d.ts +2 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +8 -0
  32. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  35. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  36. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  38. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  39. package/dist/types/modes/interactive-mode.d.ts +2 -0
  40. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  41. package/dist/types/modes/types.d.ts +2 -0
  42. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  43. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  44. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  45. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  46. package/dist/types/sdk/bus/index.d.ts +15 -1
  47. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  49. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  50. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  51. package/dist/types/sdk/host/host.d.ts +2 -0
  52. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +44 -10
  54. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  55. package/dist/types/session/session-manager.d.ts +25 -13
  56. package/dist/types/session/streaming-output.d.ts +60 -5
  57. package/dist/types/skill-state/active-state.d.ts +0 -4
  58. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  59. package/dist/types/tools/browser/launch.d.ts +7 -0
  60. package/dist/types/tools/fetch.d.ts +17 -1
  61. package/dist/types/tools/output-meta.d.ts +21 -1
  62. package/dist/types/tools/read-internals.d.ts +12 -0
  63. package/dist/types/tools/read.d.ts +45 -1
  64. package/dist/types/tools/resource-gc.d.ts +4 -0
  65. package/dist/types/tools/tool-result.d.ts +7 -1
  66. package/package.json +9 -13
  67. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  68. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  69. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  71. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  72. package/src/capability/index.ts +8 -0
  73. package/src/cli/read-cli.ts +2 -1
  74. package/src/cli.ts +1 -0
  75. package/src/commands/deep-interview.ts +29 -9
  76. package/src/commands/harness.ts +35 -12
  77. package/src/commands/read.ts +11 -2
  78. package/src/commands/team.ts +1 -1
  79. package/src/config/model-registry.ts +21 -2
  80. package/src/config/provider-auth-health.ts +42 -0
  81. package/src/config/provider-ranking.ts +119 -0
  82. package/src/config/settings-schema.ts +23 -1
  83. package/src/config/settings.ts +246 -108
  84. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  85. package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
  86. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  87. package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
  88. package/src/eval/py/executor.ts +7 -2
  89. package/src/extensibility/extensions/types.ts +28 -4
  90. package/src/extensibility/shared-events.ts +5 -0
  91. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  92. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  93. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  94. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  95. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  96. package/src/gjc-runtime/gc-render.ts +1 -0
  97. package/src/gjc-runtime/gc-runtime.ts +11 -1
  98. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  99. package/src/gjc-runtime/ralplan-runtime.ts +742 -85
  100. package/src/gjc-runtime/state-runtime.ts +2 -1
  101. package/src/gjc-runtime/state-writer.ts +70 -222
  102. package/src/gjc-runtime/team-launch.ts +34 -5
  103. package/src/gjc-runtime/team-runtime.ts +970 -33
  104. package/src/gjc-runtime/team-store.ts +32 -0
  105. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  106. package/src/gjc-runtime/team-workers.ts +1 -0
  107. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  108. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  109. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  110. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  111. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  112. package/src/hooks/native-skill-hook.ts +17 -4
  113. package/src/hooks/skill-state.ts +1 -23
  114. package/src/internal-urls/docs-index.generated.ts +8 -9
  115. package/src/internal-urls/local-root-gc.ts +127 -0
  116. package/src/main.ts +9 -5
  117. package/src/memories/index.ts +10 -0
  118. package/src/modes/DESIGN.md +47 -0
  119. package/src/modes/acp/acp-agent.ts +43 -7
  120. package/src/modes/acp/acp-event-mapper.ts +14 -7
  121. package/src/modes/acp/acp-mode.ts +39 -3
  122. package/src/modes/components/assistant-message.ts +123 -22
  123. package/src/modes/components/irc-sidebar.ts +176 -62
  124. package/src/modes/components/model-selector.ts +61 -4
  125. package/src/modes/components/oauth-selector.ts +42 -12
  126. package/src/modes/components/read-tool-group.ts +82 -14
  127. package/src/modes/components/session-observer-overlay.ts +187 -32
  128. package/src/modes/components/settings-selector.ts +60 -18
  129. package/src/modes/components/tool-execution.ts +138 -27
  130. package/src/modes/controllers/event-controller.ts +109 -27
  131. package/src/modes/controllers/extension-ui-controller.ts +19 -2
  132. package/src/modes/controllers/input-controller.ts +18 -3
  133. package/src/modes/controllers/selector-controller.ts +83 -37
  134. package/src/modes/interactive-mode.ts +62 -3
  135. package/src/modes/irc-observation-ledger.ts +71 -15
  136. package/src/modes/runtime-init.ts +19 -2
  137. package/src/modes/types.ts +2 -0
  138. package/src/modes/utils/ui-helpers.ts +94 -1
  139. package/src/prompts/agents/architect.md +9 -1
  140. package/src/prompts/agents/critic.md +9 -0
  141. package/src/prompts/system/system-prompt.md +0 -1
  142. package/src/prompts/tools/read.md +4 -2
  143. package/src/runtime-mcp/smithery-auth.ts +17 -2
  144. package/src/runtime-mcp/smithery-connect.ts +11 -1
  145. package/src/sdk/broker/lifecycle.ts +25 -6
  146. package/src/sdk/bus/control-drain-lease.ts +104 -0
  147. package/src/sdk/bus/index.ts +341 -38
  148. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  149. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  150. package/src/sdk/bus/reconciliation-store.ts +224 -0
  151. package/src/sdk/host/control/dispatch.ts +5 -1
  152. package/src/sdk/host/control/operations.ts +1 -1
  153. package/src/sdk/host/host.ts +23 -1
  154. package/src/sdk/host/query/handlers.ts +55 -0
  155. package/src/sdk/mcp/server.ts +46 -18
  156. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  157. package/src/sdk/protocol/operation-registry.ts +7 -2
  158. package/src/sdk/session.ts +13 -1
  159. package/src/session/agent-session.ts +466 -50
  160. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  161. package/src/session/session-manager.ts +764 -17
  162. package/src/session/streaming-output.ts +262 -43
  163. package/src/setup/provider-onboarding.ts +19 -4
  164. package/src/skill-state/active-state.ts +0 -10
  165. package/src/skill-state/workflow-hud.ts +25 -2
  166. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  167. package/src/tools/ask.ts +89 -69
  168. package/src/tools/browser/launch.ts +41 -6
  169. package/src/tools/fetch.ts +114 -23
  170. package/src/tools/output-meta.ts +176 -9
  171. package/src/tools/path-utils.ts +5 -1
  172. package/src/tools/read-internals.ts +44 -0
  173. package/src/tools/read.ts +1542 -310
  174. package/src/tools/resource-gc.ts +182 -18
  175. package/src/tools/tool-result.ts +14 -1
  176. package/src/web/search/providers/codex.ts +1 -1
  177. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  178. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  179. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  180. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  181. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  182. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -31,9 +31,9 @@ import {
31
31
  toError,
32
32
  } from "@gajae-code/utils";
33
33
  import type { TtsrInjectionRecord } from "../export/ttsr";
34
+ import { assertSafePathComponent } from "../gjc-runtime/session-layout";
34
35
  import { writeTextAtomic } from "../gjc-runtime/state-writer";
35
36
  import type { ManagedLegacyLocalMigrationSource } from "../internal-urls/local-protocol";
36
-
37
37
  import * as git from "../utils/git";
38
38
  import { ArtifactManager } from "./artifacts";
39
39
  import {
@@ -79,6 +79,23 @@ import {
79
79
  retainManagedDirectoryAuthority,
80
80
  } from "./internal/managed-session-storage";
81
81
  import { classifyNativePublishOutcome, formatNativePublishDiagnostic } from "./internal/native-publish-outcome";
82
+ import {
83
+ hasOnlyKeys as hasOnlyMemoryGuardKeys,
84
+ isMemoryGuardDecimalString,
85
+ isMemoryGuardRelativePath,
86
+ isMemoryGuardSha256Hex,
87
+ type MemoryGuardCheckpointBlobAuthorityV1,
88
+ type MemoryGuardCheckpointBlobManifestEntryV1,
89
+ type MemoryGuardCheckpointBlobManifestV1,
90
+ type MemoryGuardCreateCheckpointInput,
91
+ type MemoryGuardParticipantDescriptorV1,
92
+ type MemoryGuardParticipantIngressLease,
93
+ type MemoryGuardRestoreInput,
94
+ type MemoryGuardRestoreResult,
95
+ type MemoryGuardSessionManagerCheckpointV1,
96
+ memoryGuardCanonicalJson,
97
+ memoryGuardSha256Hex,
98
+ } from "./memory-guard-checkpoint-participant";
82
99
 
83
100
  import {
84
101
  type BashExecutionMessage,
@@ -480,6 +497,238 @@ export type DefaultModelSelectionStage = {
480
497
  readonly tempPath: string | undefined;
481
498
  readonly persistsToExistingFile: boolean;
482
499
  };
500
+ export interface SessionManagerRevisionSnapshot {
501
+ entry: number;
502
+ leaf: number;
503
+ headerExport: number;
504
+ label: number;
505
+ replayMetadata: number;
506
+ }
507
+
508
+ export interface SessionManagerCheckpointRevisionStrings {
509
+ entry: string;
510
+ leaf: string;
511
+ headerExport: string;
512
+ label: string;
513
+ replayMetadata: string;
514
+ }
515
+
516
+ function assertMemoryGuardSessionId(sessionId: string): void {
517
+ assertSafePathComponent(sessionId, "memory guard checkpoint session id");
518
+ }
519
+
520
+ function memoryGuardParticipantRoot(checkpointRoot: string, sessionId: string): string {
521
+ assertMemoryGuardSessionId(sessionId);
522
+ return path.join(checkpointRoot, "participants", sessionId);
523
+ }
524
+
525
+ function memoryGuardParticipantRelativePath(sessionId: string, suffix: string): string {
526
+ assertMemoryGuardSessionId(sessionId);
527
+ return `participants/${sessionId}/${suffix}`;
528
+ }
529
+
530
+ async function fsyncDirectoryPath(directoryPath: string): Promise<void> {
531
+ const directory = await fs.promises.open(directoryPath, "r");
532
+ try {
533
+ await directory.sync();
534
+ } finally {
535
+ await directory.close();
536
+ }
537
+ }
538
+
539
+ async function ensureOwnerOnlyDirectory(directoryPath: string): Promise<void> {
540
+ await fs.promises.mkdir(directoryPath, { recursive: true, mode: 0o700 });
541
+ await fs.promises.chmod(directoryPath, 0o700).catch(() => undefined);
542
+ await fsyncDirectoryPath(directoryPath);
543
+ }
544
+ interface CreatedDirectoryIdentity {
545
+ dev: bigint;
546
+ ino: bigint;
547
+ }
548
+
549
+ async function ensureOwnerOnlyDirectoryTracked(directoryPath: string): Promise<CreatedDirectoryIdentity | undefined> {
550
+ const createdPath = await fs.promises.mkdir(directoryPath, { recursive: true, mode: 0o700 });
551
+ await fs.promises.chmod(directoryPath, 0o700).catch(() => undefined);
552
+ await fsyncDirectoryPath(directoryPath);
553
+ if (createdPath === undefined) return undefined;
554
+ const stat = await fs.promises.lstat(directoryPath, { bigint: true });
555
+ return { dev: stat.dev, ino: stat.ino };
556
+ }
557
+
558
+ async function removeCreatedDirectoryIfEmpty(
559
+ directoryPath: string,
560
+ created: CreatedDirectoryIdentity | undefined,
561
+ ): Promise<void> {
562
+ if (!created) return;
563
+ const stat = await fs.promises.lstat(directoryPath, { bigint: true }).catch(() => undefined);
564
+ if (!stat || stat.dev !== created.dev || stat.ino !== created.ino) return;
565
+ await fs.promises.rmdir(directoryPath).catch(() => undefined);
566
+ await fsyncDirectoryPath(path.dirname(directoryPath)).catch(() => undefined);
567
+ }
568
+
569
+ async function writeOwnerOnlyFileNoReplace(filePath: string, content: Uint8Array | string): Promise<void> {
570
+ await ensureOwnerOnlyDirectory(path.dirname(filePath));
571
+ const handle = await fs.promises.open(filePath, "wx", 0o600);
572
+ try {
573
+ await handle.writeFile(content);
574
+ await handle.sync();
575
+ } finally {
576
+ await handle.close();
577
+ }
578
+ await fs.promises.chmod(filePath, 0o600).catch(() => undefined);
579
+ await fsyncDirectoryPath(path.dirname(filePath));
580
+ }
581
+
582
+ const MEMORY_GUARD_CHECKPOINT_FILE_MAX_BYTES = 64 * 1024 * 1024;
583
+ const MEMORY_GUARD_CHECKPOINT_BLOB_MAX_ENTRIES = 4096;
584
+ const MEMORY_GUARD_CHECKPOINT_BLOB_TOTAL_MAX_BYTES = 64 * 1024 * 1024;
585
+
586
+ function collectCheckpointBlobRefs(value: unknown, refs: Set<string> = new Set(), key?: string): Set<string> {
587
+ const addRef = (candidate: unknown): void => {
588
+ if (typeof candidate === "string" && parseBlobRef(candidate)) refs.add(candidate);
589
+ };
590
+ if (Array.isArray(value)) {
591
+ for (const item of value) collectCheckpointBlobRefs(item, refs, key);
592
+ return refs;
593
+ }
594
+ if (!value || typeof value !== "object") return refs;
595
+ if (isRecord(value) && value.kind === "cold_spill" && typeof value.ref === "string" && parseBlobRef(value.ref))
596
+ refs.add(value.ref);
597
+ if (isImageBlock(value) && key === TEXT_CONTENT_KEY) addRef(value.data);
598
+ if (hasImageUrl(value)) {
599
+ if (typeof value.image_url === "string") addRef(value.image_url);
600
+ else addRef(value.image_url.url);
601
+ }
602
+ for (const [childKey, item] of Object.entries(value as Record<string, unknown>)) {
603
+ if (childKey === "data" && key !== TEXT_CONTENT_KEY) addRef(item);
604
+ collectCheckpointBlobRefs(item, refs, childKey);
605
+ }
606
+ return refs;
607
+ }
608
+
609
+ function decodeCheckpointUtf8(data: Uint8Array): string | null {
610
+ try {
611
+ return new TextDecoder("utf-8", { fatal: true }).decode(data);
612
+ } catch {
613
+ return null;
614
+ }
615
+ }
616
+
617
+ function validateMemoryGuardBlobAuthority(value: unknown): value is MemoryGuardCheckpointBlobAuthorityV1 {
618
+ if (
619
+ !isRecord(value) ||
620
+ !hasOnlyMemoryGuardKeys(value, ["kind", "manifest_relative_path", "manifest_sha256", "root_relative_path"])
621
+ )
622
+ return false;
623
+ return (
624
+ value.kind === "checkpoint_blob_tree_v1" &&
625
+ isMemoryGuardRelativePath(value.manifest_relative_path) &&
626
+ isMemoryGuardSha256Hex(value.manifest_sha256) &&
627
+ isMemoryGuardRelativePath(value.root_relative_path)
628
+ );
629
+ }
630
+
631
+ function validateMemoryGuardTranscriptDescriptor(
632
+ value: unknown,
633
+ ): value is MemoryGuardSessionManagerCheckpointV1["transcript"] {
634
+ if (!isRecord(value) || !hasOnlyMemoryGuardKeys(value, ["bytes", "relative_path", "sha256"])) return false;
635
+ return (
636
+ isMemoryGuardDecimalString(value.bytes) &&
637
+ isMemoryGuardRelativePath(value.relative_path) &&
638
+ isMemoryGuardSha256Hex(value.sha256)
639
+ );
640
+ }
641
+
642
+ function validateMemoryGuardRevisions(value: unknown): value is SessionManagerCheckpointRevisionStrings {
643
+ if (!isRecord(value) || !hasOnlyMemoryGuardKeys(value, ["entry", "leaf", "headerExport", "label", "replayMetadata"]))
644
+ return false;
645
+ return (
646
+ isMemoryGuardDecimalString(value.entry) &&
647
+ isMemoryGuardDecimalString(value.leaf) &&
648
+ isMemoryGuardDecimalString(value.headerExport) &&
649
+ isMemoryGuardDecimalString(value.label) &&
650
+ isMemoryGuardDecimalString(value.replayMetadata)
651
+ );
652
+ }
653
+
654
+ function validateMemoryGuardCheckpoint(value: unknown): value is MemoryGuardSessionManagerCheckpointV1 {
655
+ if (
656
+ !isRecord(value) ||
657
+ !hasOnlyMemoryGuardKeys(value, [
658
+ "blob_authority",
659
+ "revisions",
660
+ "schema_version",
661
+ "session_id",
662
+ "session_name",
663
+ "transcript",
664
+ ])
665
+ )
666
+ return false;
667
+ return (
668
+ value.schema_version === 1 &&
669
+ typeof value.session_id === "string" &&
670
+ value.session_id.length > 0 &&
671
+ (value.session_name === null || typeof value.session_name === "string") &&
672
+ validateMemoryGuardBlobAuthority(value.blob_authority) &&
673
+ validateMemoryGuardRevisions(value.revisions) &&
674
+ validateMemoryGuardTranscriptDescriptor(value.transcript)
675
+ );
676
+ }
677
+
678
+ function validateMemoryGuardBlobManifestEntry(value: unknown): value is MemoryGuardCheckpointBlobManifestEntryV1 {
679
+ if (!isRecord(value) || !hasOnlyMemoryGuardKeys(value, ["bytes", "relative_path", "sha256"])) return false;
680
+ return (
681
+ isMemoryGuardDecimalString(value.bytes) &&
682
+ isMemoryGuardRelativePath(value.relative_path) &&
683
+ isMemoryGuardSha256Hex(value.sha256)
684
+ );
685
+ }
686
+
687
+ function validateMemoryGuardBlobManifest(value: unknown): value is MemoryGuardCheckpointBlobManifestV1 {
688
+ if (!isRecord(value) || !hasOnlyMemoryGuardKeys(value, ["entries", "schema_version"])) return false;
689
+ if (value.schema_version !== 1 || !Array.isArray(value.entries)) return false;
690
+ return value.entries.every(validateMemoryGuardBlobManifestEntry);
691
+ }
692
+
693
+ function memoryGuardParticipantMatchesCheckpoint(
694
+ participant: MemoryGuardParticipantDescriptorV1,
695
+ checkpoint: MemoryGuardSessionManagerCheckpointV1,
696
+ ): boolean {
697
+ return (
698
+ participant.session_id === checkpoint.session_id &&
699
+ participant.session_name === checkpoint.session_name &&
700
+ JSON.stringify(participant.checkpoint) === JSON.stringify(checkpoint.blob_authority) &&
701
+ JSON.stringify(participant.revisions) === JSON.stringify(checkpoint.revisions) &&
702
+ JSON.stringify(participant.transcript) === JSON.stringify(checkpoint.transcript)
703
+ );
704
+ }
705
+
706
+ function readCheckpointAuthorityFile(
707
+ authority: native.RecoveryFsRoot,
708
+ relativePath: string,
709
+ maxBytes: number,
710
+ ): Uint8Array | null {
711
+ const result = authority.readManaged(relativePath);
712
+ if (!result.ok || !result.data || result.data.byteLength > maxBytes) return null;
713
+ return result.data;
714
+ }
715
+
716
+ function toCanonicalRevisionString(name: keyof SessionManagerRevisionSnapshot, value: number): string {
717
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`invalid_session_manager_revision:${name}`);
718
+ return String(value);
719
+ }
720
+
721
+ export function toSessionManagerCheckpointRevisionStrings(
722
+ snapshot: SessionManagerRevisionSnapshot,
723
+ ): SessionManagerCheckpointRevisionStrings {
724
+ return {
725
+ entry: toCanonicalRevisionString("entry", snapshot.entry),
726
+ leaf: toCanonicalRevisionString("leaf", snapshot.leaf),
727
+ headerExport: toCanonicalRevisionString("headerExport", snapshot.headerExport),
728
+ label: toCanonicalRevisionString("label", snapshot.label),
729
+ replayMetadata: toCanonicalRevisionString("replayMetadata", snapshot.replayMetadata),
730
+ };
731
+ }
483
732
 
484
733
  export type DefaultModelSelectionPromotion =
485
734
  | { readonly kind: "promoted" }
@@ -3506,6 +3755,81 @@ class NdjsonFileWriter {
3506
3755
  }
3507
3756
  }
3508
3757
 
3758
+ const PROJECT_SESSION_SCAN_MAX_DIRECTORIES = 4096;
3759
+ const PROJECT_SESSION_SCAN_MAX_FILES = 1000;
3760
+
3761
+ function isProjectSessionTranscriptPath(projectGjcDir: string, filePath: string): boolean {
3762
+ const relative = path.relative(projectGjcDir, filePath);
3763
+ if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
3764
+ const segments = relative.split(path.sep);
3765
+ if (segments.length === 1) return true;
3766
+ const parent = segments.at(-2);
3767
+ return parent === "agent-session" || segments.includes("sessions");
3768
+ }
3769
+
3770
+ /**
3771
+ * Discover resumable transcripts intentionally stored inside a project's `.gjc`.
3772
+ * Runtime token/audit JSONL files are excluded by requiring a known transcript
3773
+ * container (`agent-session` or `sessions`).
3774
+ */
3775
+ function listProjectSessionTranscriptFiles(cwd: string): string[] {
3776
+ const projectGjcDir = path.join(path.resolve(cwd), ".gjc");
3777
+ let rootStat: fs.Stats;
3778
+ try {
3779
+ rootStat = fs.lstatSync(projectGjcDir);
3780
+ } catch {
3781
+ return [];
3782
+ }
3783
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return [];
3784
+
3785
+ const directories = [projectGjcDir];
3786
+ const files: string[] = [];
3787
+ let scannedDirectories = 0;
3788
+ while (directories.length > 0 && scannedDirectories < PROJECT_SESSION_SCAN_MAX_DIRECTORIES) {
3789
+ const directory = directories.pop()!;
3790
+ scannedDirectories++;
3791
+ let entries: fs.Dirent[];
3792
+ try {
3793
+ entries = fs.readdirSync(directory, { withFileTypes: true });
3794
+ } catch {
3795
+ continue;
3796
+ }
3797
+ for (const entry of entries) {
3798
+ if (entry.isSymbolicLink()) continue;
3799
+ const entryPath = path.join(directory, entry.name);
3800
+ if (entry.isDirectory()) {
3801
+ directories.push(entryPath);
3802
+ continue;
3803
+ }
3804
+ if (
3805
+ entry.isFile() &&
3806
+ !entry.name.startsWith(".") &&
3807
+ entry.name.endsWith(".jsonl") &&
3808
+ isProjectSessionTranscriptPath(projectGjcDir, entryPath)
3809
+ ) {
3810
+ files.push(entryPath);
3811
+ if (files.length >= PROJECT_SESSION_SCAN_MAX_FILES) return files;
3812
+ }
3813
+ }
3814
+ }
3815
+ return files;
3816
+ }
3817
+
3818
+ async function collectProjectSessions(cwd: string, storage: FileSessionStorage): Promise<SessionInfo[]> {
3819
+ return await collectSessionsFromFiles(listProjectSessionTranscriptFiles(cwd), storage);
3820
+ }
3821
+
3822
+ function mergeSessionInventories(...inventories: SessionInfo[][]): SessionInfo[] {
3823
+ const sessions = new Map<string, SessionInfo>();
3824
+ for (const inventory of inventories) {
3825
+ for (const session of inventory) {
3826
+ const current = sessions.get(session.id);
3827
+ if (!current || session.modified.getTime() > current.modified.getTime()) sessions.set(session.id, session);
3828
+ }
3829
+ }
3830
+ return [...sessions.values()].sort((left, right) => right.modified.getTime() - left.modified.getTime());
3831
+ }
3832
+
3509
3833
  const DEFAULT_WELCOME_RECENT_SESSION_LIMIT = 20;
3510
3834
 
3511
3835
  /** Get recent sessions for display in welcome screen */
@@ -3517,6 +3841,21 @@ export async function getRecentSessions(
3517
3841
  return getSortedSessions(sessionDir, storage, limit);
3518
3842
  }
3519
3843
 
3844
+ export function getRecentSessionDisplay(
3845
+ sessions: readonly SessionInfo[],
3846
+ limit = DEFAULT_WELCOME_RECENT_SESSION_LIMIT,
3847
+ ): Array<{ name: string; timeAgo: string }> {
3848
+ return sessions.slice(0, limit).map(session => {
3849
+ const recent = new RecentSessionInfo(
3850
+ session.path,
3851
+ session.modified.getTime(),
3852
+ { title: session.title, timestamp: session.created.toISOString() },
3853
+ session.firstMessage,
3854
+ );
3855
+ return { name: recent.name, timeAgo: recent.timeAgo };
3856
+ });
3857
+ }
3858
+
3520
3859
  /**
3521
3860
  * Manages conversation sessions as append-only trees stored in JSONL files.
3522
3861
  *
@@ -4015,6 +4354,8 @@ export class SessionManager {
4015
4354
  #needsFullRewriteOnNextPersist: boolean = false;
4016
4355
  #ensuredOnDisk: boolean = false;
4017
4356
  #recoveryHydrationContext: RecoveryHydrationContext | undefined;
4357
+ #recoveryPromotionTranscriptPath: string | undefined;
4358
+ #memoryGuardParticipantIngressToken: symbol | undefined;
4018
4359
  #fileEntries: FileEntry[] = [];
4019
4360
  #pendingStrictAdoption: { canonicalPath: string; identity: ResumeSessionIdentity } | undefined;
4020
4361
  #byId: Map<string, SessionEntry> = new Map();
@@ -4049,7 +4390,8 @@ export class SessionManager {
4049
4390
  #inMemoryArtifactCounter = 0;
4050
4391
  readonly #blobStore: BlobStore;
4051
4392
  #residentTextBlobStore: BlobStore = new MemoryBlobStore();
4052
- readonly #residentImageBlobStore: BlobStore;
4393
+ #residentImageBlobStore: BlobStore;
4394
+ #memoryGuardCheckpointBlobs: Map<string, Buffer> | undefined;
4053
4395
  #entryRevision = 0;
4054
4396
  #leafRevision = 0;
4055
4397
  /** Export/header cache invalidation contract; consumers may arrive after the revision field. */
@@ -4122,6 +4464,9 @@ export class SessionManager {
4122
4464
  },
4123
4465
  };
4124
4466
  }
4467
+ #coldSpillReadStore(): BlobStore {
4468
+ return this.#memoryGuardCheckpointBlobs ? this.#residentImageBlobStore : this.#blobStore;
4469
+ }
4125
4470
 
4126
4471
  #residentCacheDir(sessionFile: string): string {
4127
4472
  const instance = ++residentCacheInstanceCounter;
@@ -4178,13 +4523,7 @@ export class SessionManager {
4178
4523
  * revision contract). Tests assert the invalidation mapping through this;
4179
4524
  * future export/label-view caches key off their respective domains.
4180
4525
  */
4181
- revisionSnapshot(): {
4182
- entry: number;
4183
- leaf: number;
4184
- headerExport: number;
4185
- label: number;
4186
- replayMetadata: number;
4187
- } {
4526
+ revisionSnapshot(): SessionManagerRevisionSnapshot {
4188
4527
  return {
4189
4528
  entry: this.#entryRevision,
4190
4529
  leaf: this.#leafRevision,
@@ -4308,6 +4647,7 @@ export class SessionManager {
4308
4647
 
4309
4648
  /** Switch to a different session file (used for resume and branching) */
4310
4649
  async setSessionFile(sessionFile: string): Promise<void> {
4650
+ this.#assertRecoveryHydrationWritable();
4311
4651
  const resolvedSessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
4312
4652
  const strictAdoption = this.#pendingStrictAdoption;
4313
4653
  this.#pendingStrictAdoption = undefined;
@@ -4361,6 +4701,7 @@ export class SessionManager {
4361
4701
 
4362
4702
  /** Start a new session. Closes any existing writer first. */
4363
4703
  async newSession(options?: NewSessionOptions): Promise<string | undefined> {
4704
+ this.#assertRecoveryHydrationWritable();
4364
4705
  const prepared = await this.prepareNewSession(options);
4365
4706
  try {
4366
4707
  this.commitPreparedNewSession(prepared);
@@ -4385,6 +4726,7 @@ export class SessionManager {
4385
4726
  * @internal
4386
4727
  */
4387
4728
  async prepareNewSession(options?: NewSessionOptions): Promise<PreparedNewSession> {
4729
+ this.#assertRecoveryHydrationWritable();
4388
4730
  await this.#retryPreparedNewSessionCleanups();
4389
4731
  await this.#closePersistWriter();
4390
4732
  const preallocated = this.#lifecycleIdAdopted ? undefined : lifecyclePreallocatedSessionId();
@@ -4655,6 +4997,7 @@ export class SessionManager {
4655
4997
 
4656
4998
  /** Prepare a path-only branch successor without publishing it through public manager state. @internal */
4657
4999
  async prepareBranchedSession(leafId: string): Promise<PreparedNewSession> {
5000
+ this.#assertRecoveryHydrationWritable();
4658
5001
  await this.#retryPreparedNewSessionCleanups();
4659
5002
  const branchPath = this.#getCanonicalBranchClones(leafId);
4660
5003
  if (branchPath.length === 0) throw new Error(`Entry ${leafId} not found`);
@@ -4738,6 +5081,7 @@ export class SessionManager {
4738
5081
 
4739
5082
  /** Publish a prepared successor synchronously after all readiness awaits succeed. @internal */
4740
5083
  commitPreparedNewSession(prepared: PreparedNewSession): void {
5084
+ this.#assertRecoveryHydrationWritable();
4741
5085
  const stage = prepared as PreparedNewSessionState;
4742
5086
  if (!this.#preparedNewSessions.has(stage) || stage.committed || stage.discarded) {
4743
5087
  throw new Error("Prepared session is no longer available.");
@@ -6230,6 +6574,160 @@ export class SessionManager {
6230
6574
  return this.#sessionFile;
6231
6575
  }
6232
6576
 
6577
+ acquireMemoryGuardParticipantIngressLease(): MemoryGuardParticipantIngressLease {
6578
+ if (this.#memoryGuardParticipantIngressToken)
6579
+ throw new Error("memory_guard_participant_ingress_lease_already_held");
6580
+ const token = Symbol("memory_guard_participant_ingress_lease");
6581
+ this.#memoryGuardParticipantIngressToken = token;
6582
+ let released = false;
6583
+ return Object.freeze({
6584
+ token,
6585
+ release: () => {
6586
+ if (released) return;
6587
+ released = true;
6588
+ if (this.#memoryGuardParticipantIngressToken === token)
6589
+ this.#memoryGuardParticipantIngressToken = undefined;
6590
+ },
6591
+ });
6592
+ }
6593
+
6594
+ #assertMemoryGuardParticipantIngressLease(lease: MemoryGuardParticipantIngressLease): void {
6595
+ if (this.#memoryGuardParticipantIngressToken !== lease.token) {
6596
+ throw new Error("memory_guard_participant_ingress_lease_invalid");
6597
+ }
6598
+ }
6599
+
6600
+ #stageMemoryGuardCheckpointBlobs(blobs: Map<string, Buffer>): void {
6601
+ const stagedImageStore = new MemoryBlobStore();
6602
+ for (const blob of blobs.values()) {
6603
+ this.#residentTextBlobStore.putSync(blob);
6604
+ stagedImageStore.putSync(blob);
6605
+ }
6606
+ this.#residentImageBlobStore = stagedImageStore;
6607
+ this.#memoryGuardCheckpointBlobs = blobs;
6608
+ }
6609
+
6610
+ async createMemoryGuardCheckpoint(
6611
+ input: MemoryGuardCreateCheckpointInput,
6612
+ ): Promise<MemoryGuardSessionManagerCheckpointV1> {
6613
+ this.#assertMemoryGuardParticipantIngressLease(input.ingressLease);
6614
+ if (!this.#sessionFile) throw new Error("memory_guard_checkpoint_session_file_unavailable");
6615
+ await this.flush();
6616
+ const captured = SessionManager.captureTranscriptStrict(this.#sessionFile, this.storage);
6617
+ if (captured.kind !== "captured") {
6618
+ throw new Error(`memory_guard_checkpoint_capture_failed:${captured.reason}`);
6619
+ }
6620
+ if (captured.snapshot.content.byteLength > MEMORY_GUARD_CHECKPOINT_FILE_MAX_BYTES)
6621
+ throw new Error("memory_guard_checkpoint_transcript_capacity_exceeded");
6622
+ const transcriptText = decodeCheckpointUtf8(captured.snapshot.content);
6623
+ if (transcriptText === null) throw new Error("memory_guard_checkpoint_transcript_unreadable");
6624
+ const entries = parseSessionEntries(transcriptText);
6625
+ const sessionId = this.getSessionId();
6626
+ assertMemoryGuardSessionId(sessionId);
6627
+ const sessionName = this.getSessionName() ?? null;
6628
+ const revisions = this.revisionSnapshot();
6629
+ const participantRoot = memoryGuardParticipantRoot(input.checkpointRoot, sessionId);
6630
+ const transcriptRelativePath = memoryGuardParticipantRelativePath(sessionId, "transcript.jsonl");
6631
+ const blobRootRelativePath = memoryGuardParticipantRelativePath(sessionId, "blobs");
6632
+ const blobManifestRelativePath = memoryGuardParticipantRelativePath(sessionId, "blob-manifest.json");
6633
+ const refs = [...collectCheckpointBlobRefs(entries)].sort();
6634
+ if (refs.length > MEMORY_GUARD_CHECKPOINT_BLOB_MAX_ENTRIES)
6635
+ throw new Error("memory_guard_checkpoint_blob_count_exceeded");
6636
+ const blobManifestEntries: MemoryGuardCheckpointBlobManifestEntryV1[] = [];
6637
+ const blobWrites: Array<{ data: Buffer; relativePath: string }> = [];
6638
+ let aggregateBlobBytes = 0;
6639
+ for (const ref of refs) {
6640
+ const hash = parseBlobRef(ref);
6641
+ if (!hash) continue;
6642
+ const data =
6643
+ this.#residentTextBlobStore.getCheckedSync(hash) ??
6644
+ this.#residentImageBlobStore.getCheckedSync(hash) ??
6645
+ this.#blobStore.getCheckedSync(hash);
6646
+ if (!data) throw new Error(`memory_guard_checkpoint_blob_missing:${hash}`);
6647
+ if (data.byteLength > MEMORY_GUARD_CHECKPOINT_FILE_MAX_BYTES)
6648
+ throw new Error(`memory_guard_checkpoint_blob_capacity_exceeded:${hash}`);
6649
+ aggregateBlobBytes += data.byteLength;
6650
+ if (aggregateBlobBytes > MEMORY_GUARD_CHECKPOINT_BLOB_TOTAL_MAX_BYTES)
6651
+ throw new Error("memory_guard_checkpoint_blob_total_capacity_exceeded");
6652
+ const relativePath = hash;
6653
+ blobWrites.push({ data, relativePath });
6654
+ blobManifestEntries.push({
6655
+ bytes: String(data.byteLength),
6656
+ relative_path: relativePath,
6657
+ sha256: memoryGuardSha256Hex(data),
6658
+ });
6659
+ }
6660
+ const publishedCheckpointPaths: string[] = [];
6661
+ const cleanupPublishedCheckpointPaths = async (): Promise<void> => {
6662
+ for (const publishedPath of publishedCheckpointPaths.reverse())
6663
+ await fs.promises.rm(publishedPath, { force: true }).catch(() => undefined);
6664
+ await fs.promises.rmdir(path.join(input.checkpointRoot, blobRootRelativePath)).catch(() => undefined);
6665
+ await fs.promises.rmdir(participantRoot).catch(() => undefined);
6666
+ };
6667
+ const publishCheckpointFile = async (filePath: string, data: Uint8Array | string): Promise<void> => {
6668
+ try {
6669
+ await writeOwnerOnlyFileNoReplace(filePath, data);
6670
+ publishedCheckpointPaths.push(filePath);
6671
+ } catch (error) {
6672
+ await cleanupPublishedCheckpointPaths();
6673
+ throw error;
6674
+ }
6675
+ };
6676
+ await ensureOwnerOnlyDirectory(participantRoot);
6677
+ await publishCheckpointFile(path.join(input.checkpointRoot, transcriptRelativePath), captured.snapshot.content);
6678
+ for (const write of blobWrites)
6679
+ await publishCheckpointFile(
6680
+ path.join(input.checkpointRoot, blobRootRelativePath, write.relativePath),
6681
+ write.data,
6682
+ );
6683
+ blobManifestEntries.sort((left, right) => left.relative_path.localeCompare(right.relative_path));
6684
+ const blobManifest: MemoryGuardCheckpointBlobManifestV1 = {
6685
+ entries: blobManifestEntries,
6686
+ schema_version: 1,
6687
+ };
6688
+ const blobManifestText = memoryGuardCanonicalJson(blobManifest);
6689
+ await publishCheckpointFile(path.join(input.checkpointRoot, blobManifestRelativePath), blobManifestText);
6690
+ const checkpoint: MemoryGuardSessionManagerCheckpointV1 = {
6691
+ blob_authority: {
6692
+ kind: "checkpoint_blob_tree_v1",
6693
+ manifest_relative_path: blobManifestRelativePath,
6694
+ manifest_sha256: memoryGuardSha256Hex(blobManifestText),
6695
+ root_relative_path: blobRootRelativePath,
6696
+ },
6697
+ revisions: toSessionManagerCheckpointRevisionStrings(revisions),
6698
+ schema_version: 1,
6699
+ session_id: sessionId,
6700
+ session_name: sessionName,
6701
+ transcript: {
6702
+ bytes: String(captured.snapshot.content.byteLength),
6703
+ relative_path: transcriptRelativePath,
6704
+ sha256: captured.snapshot.identity.sha256,
6705
+ },
6706
+ };
6707
+ const checkpointPath = path.join(
6708
+ input.checkpointRoot,
6709
+ memoryGuardParticipantRelativePath(sessionId, "session-manager.json"),
6710
+ );
6711
+ await publishCheckpointFile(checkpointPath, memoryGuardCanonicalJson(checkpoint));
6712
+ const currentRevisions = this.revisionSnapshot();
6713
+ const recaptured = SessionManager.captureTranscriptStrict(this.#sessionFile, this.storage);
6714
+ if (
6715
+ this.getSessionId() !== sessionId ||
6716
+ (this.getSessionName() ?? null) !== sessionName ||
6717
+ Object.keys(revisions).some(
6718
+ key =>
6719
+ currentRevisions[key as keyof SessionManagerRevisionSnapshot] !==
6720
+ revisions[key as keyof SessionManagerRevisionSnapshot],
6721
+ ) ||
6722
+ recaptured.kind !== "captured" ||
6723
+ !sameResumeIdentity(captured.snapshot.identity, recaptured.snapshot.identity)
6724
+ ) {
6725
+ await cleanupPublishedCheckpointPaths();
6726
+ throw new Error("memory_guard_checkpoint_state_changed_during_capture");
6727
+ }
6728
+ return checkpoint;
6729
+ }
6730
+
6233
6731
  /**
6234
6732
  * Returns the session artifacts directory path (session file path without .jsonl).
6235
6733
  * Returns null when the session is not persisted to a file.
@@ -6491,7 +6989,11 @@ export class SessionManager {
6491
6989
  * @param source - "user" for explicit renames (/rename command, RPC); "auto" for generated titles.
6492
6990
  * Auto-generated titles are silently ignored when the user has already set a name.
6493
6991
  */
6992
+ #assertRecoveryHydrationWritable(): void {
6993
+ if (this.#recoveryHydrationContext) throw new Error("recovery_hydration_not_promoted");
6994
+ }
6494
6995
  async setSessionName(name: string, source: "auto" | "user" = "auto"): Promise<boolean> {
6996
+ this.#assertRecoveryHydrationWritable();
6495
6997
  // User-set names take permanent precedence over auto-generated ones.
6496
6998
  if (this.#titleSource === "user" && source === "auto") return false;
6497
6999
 
@@ -6617,6 +7119,7 @@ export class SessionManager {
6617
7119
  }
6618
7120
 
6619
7121
  #appendEntry(entry: SessionEntry): void {
7122
+ this.#assertRecoveryHydrationWritable();
6620
7123
  const normalizedEntry = normalizeSessionEntryForStorage(entry);
6621
7124
  const residentEntry = prepareEntryForResidentSync(normalizedEntry, this.#residentBlobStores()) as SessionEntry;
6622
7125
  this.#fileEntries.push(residentEntry);
@@ -6835,6 +7338,7 @@ export class SessionManager {
6835
7338
  * the canonical store. This applies such mutations for real.
6836
7339
  */
6837
7340
  applyEntryMessageUpdates(entries: readonly SessionMessageEntry[]): void {
7341
+ this.#assertRecoveryHydrationWritable();
6838
7342
  for (const updated of entries) {
6839
7343
  const canonical = this.#byId.get(updated.id);
6840
7344
  if (canonical?.type !== "message") continue;
@@ -6851,6 +7355,7 @@ export class SessionManager {
6851
7355
 
6852
7356
  /** Write mutated custom-message entries back into the canonical entry store by id. */
6853
7357
  applyCustomMessageEntryUpdates(entries: readonly CustomMessageEntry[]): void {
7358
+ this.#assertRecoveryHydrationWritable();
6854
7359
  for (const updated of entries) {
6855
7360
  const canonical = this.#byId.get(updated.id);
6856
7361
  if (canonical?.type !== "custom_message") continue;
@@ -6870,6 +7375,7 @@ export class SessionManager {
6870
7375
  * Use sparingly (e.g., pruning old tool outputs).
6871
7376
  */
6872
7377
  async rewriteEntries(): Promise<void> {
7378
+ this.#assertRecoveryHydrationWritable();
6873
7379
  if (!this.persist || !this.#sessionFile) return;
6874
7380
  await this.#rewriteFile();
6875
7381
  }
@@ -7035,6 +7541,7 @@ export class SessionManager {
7035
7541
  }
7036
7542
 
7037
7543
  evictCompactedContent(firstKeptEntryId: string, compactionEntryId: string): EvictCompactedContentResult {
7544
+ this.#assertRecoveryHydrationWritable();
7038
7545
  const firstKept = this.#byId.get(firstKeptEntryId);
7039
7546
  const compaction = this.#byId.get(compactionEntryId);
7040
7547
  if (!firstKept) throw new Error(`Entry ${firstKeptEntryId} not found`);
@@ -7185,7 +7692,7 @@ export class SessionManager {
7185
7692
  ? cloneSessionEntry(
7186
7693
  rehydrateColdSpillEntry(
7187
7694
  materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()),
7188
- this.#blobStore,
7695
+ this.#coldSpillReadStore(),
7189
7696
  this.#residentBlobStoresForColdRehydrate(),
7190
7697
  ),
7191
7698
  )
@@ -7204,7 +7711,7 @@ export class SessionManager {
7204
7711
  cloneSessionEntry(
7205
7712
  rehydrateColdSpillEntry(
7206
7713
  materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache),
7207
- this.#blobStore,
7714
+ this.#coldSpillReadStore(),
7208
7715
  this.#residentBlobStoresForColdRehydrate(),
7209
7716
  ),
7210
7717
  ),
@@ -7246,7 +7753,7 @@ export class SessionManager {
7246
7753
  cloneSessionEntry(
7247
7754
  rehydrateColdSpillEntry(
7248
7755
  materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache),
7249
- this.#blobStore,
7756
+ this.#coldSpillReadStore(),
7250
7757
  this.#residentBlobStoresForColdRehydrate(),
7251
7758
  ),
7252
7759
  ),
@@ -7412,7 +7919,7 @@ export class SessionManager {
7412
7919
  const materialized = materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map());
7413
7920
  const rehydrated = rehydrateColdSpillEntry(
7414
7921
  materialized,
7415
- this.#blobStore,
7922
+ this.#coldSpillReadStore(),
7416
7923
  this.#residentBlobStoresForColdRehydrate(),
7417
7924
  );
7418
7925
  if (rehydrated !== materialized) this.#coldSpillReadCount += this.#countColdSpillPayloads(entry);
@@ -7428,6 +7935,7 @@ export class SessionManager {
7428
7935
  }
7429
7936
  /** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries without persisting it. */
7430
7937
  sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean {
7938
+ this.#assertRecoveryHydrationWritable();
7431
7939
  return this.#sanitizeLoadedOpenAIResponsesReplayMetadata().length > 0;
7432
7940
  }
7433
7941
 
@@ -7583,6 +8091,7 @@ export class SessionManager {
7583
8091
  * are not modified or deleted.
7584
8092
  */
7585
8093
  branch(branchFromId: string): void {
8094
+ this.#assertRecoveryHydrationWritable();
7586
8095
  if (!this.#byId.has(branchFromId)) {
7587
8096
  throw new Error(`Entry ${branchFromId} not found`);
7588
8097
  }
@@ -7596,6 +8105,7 @@ export class SessionManager {
7596
8105
  * Use this when navigating to re-edit the first user message.
7597
8106
  */
7598
8107
  resetLeaf(): void {
8108
+ this.#assertRecoveryHydrationWritable();
7599
8109
  this.#leafId = null;
7600
8110
  this.#leafRevision++;
7601
8111
  }
@@ -7606,6 +8116,7 @@ export class SessionManager {
7606
8116
  * context from the abandoned conversation path.
7607
8117
  */
7608
8118
  branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromExtension?: boolean): string {
8119
+ this.#assertRecoveryHydrationWritable();
7609
8120
  if (branchFromId !== null && !this.#byId.has(branchFromId)) {
7610
8121
  throw new Error(`Entry ${branchFromId} not found`);
7611
8122
  }
@@ -7630,6 +8141,7 @@ export class SessionManager {
7630
8141
  * Returns the new session file path, or undefined if not persisting.
7631
8142
  */
7632
8143
  createBranchedSession(leafId: string): string | undefined {
8144
+ this.#assertRecoveryHydrationWritable();
7633
8145
  const previousSessionFile = this.#sessionFile;
7634
8146
  const branchPath = this.#getCanonicalBranchClones(leafId);
7635
8147
  if (branchPath.length === 0) {
@@ -8112,10 +8624,11 @@ export class SessionManager {
8112
8624
  if (resolved.kind === "error") return [];
8113
8625
  const listing = listManagedCandidates(resolved.scope);
8114
8626
  if (listing.kind === "error") return [];
8115
- return await collectSessionsFromFiles(
8627
+ const managed = await collectSessionsFromFiles(
8116
8628
  listing.owned.map(candidate => candidate.path),
8117
8629
  storage,
8118
8630
  );
8631
+ return mergeSessionInventories(managed, await collectProjectSessions(cwd, storage));
8119
8632
  }
8120
8633
 
8121
8634
  /**
@@ -8136,12 +8649,35 @@ export class SessionManager {
8136
8649
  }
8137
8650
  }
8138
8651
 
8139
- /** Tombstone and exact-delete a default-managed candidate. */
8652
+ /** Delete an authorized managed or project-local picker candidate. */
8140
8653
  static async deleteManagedCandidate(sessionPath: string): Promise<void> {
8141
8654
  const storage = new FileSessionStorage();
8142
8655
  const entries = await loadEntriesFromFile(sessionPath, storage);
8143
8656
  const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
8144
- if (!header?.cwd) throw new Error("Session has no valid managed workspace header.");
8657
+ if (!header?.cwd) throw new Error("Session has no valid workspace header.");
8658
+ const projectGjcDir = path.join(path.resolve(header.cwd), ".gjc");
8659
+ if (isProjectSessionTranscriptPath(projectGjcDir, sessionPath)) {
8660
+ const relativePath = path.relative(projectGjcDir, path.resolve(sessionPath)).split(path.sep).join("/");
8661
+ const authority = native.openRecoveryFsRoot(projectGjcDir);
8662
+ try {
8663
+ const captured = authority.readManaged(relativePath);
8664
+ if (!captured.ok || !captured.identity || !captured.data || !captured.identity.sha256)
8665
+ throw new Error("Project session is no longer an authorized candidate.");
8666
+ const removed = authority.removeManaged(
8667
+ relativePath,
8668
+ captured.identity.dev,
8669
+ captured.identity.ino,
8670
+ captured.identity.size,
8671
+ captured.identity.mtimeNs,
8672
+ captured.identity.ctimeNs,
8673
+ captured.identity.sha256,
8674
+ );
8675
+ if (!removed.ok) throw new Error(removed.code ?? "Could not delete project session.");
8676
+ return;
8677
+ } finally {
8678
+ authority.close();
8679
+ }
8680
+ }
8145
8681
  const sessionsRoot = path.resolve(sessionPath, "../..");
8146
8682
  const resolved = resolveManagedScope({
8147
8683
  cwd: header.cwd,
@@ -8349,6 +8885,194 @@ export class SessionManager {
8349
8885
  }
8350
8886
  }
8351
8887
 
8888
+ static async restoreMemoryGuardCheckpoint(input: MemoryGuardRestoreInput): Promise<MemoryGuardRestoreResult> {
8889
+ try {
8890
+ assertMemoryGuardSessionId(input.checkpoint.session_id);
8891
+ } catch {
8892
+ return { kind: "blocked", reason: "checkpoint-mismatch" };
8893
+ }
8894
+ if (!validateMemoryGuardCheckpoint(input.checkpoint)) return { kind: "blocked", reason: "checkpoint-mismatch" };
8895
+ if (!memoryGuardParticipantMatchesCheckpoint(input.participant, input.checkpoint)) {
8896
+ return { kind: "blocked", reason: "participant-mismatch" };
8897
+ }
8898
+ const checkpointText = readCheckpointAuthorityFile(
8899
+ input.incidentAuthority,
8900
+ memoryGuardParticipantRelativePath(input.checkpoint.session_id, "session-manager.json"),
8901
+ 256 * 1024,
8902
+ );
8903
+ if (!checkpointText) return { kind: "blocked", reason: "checkpoint-mismatch" };
8904
+ const checkpointCanonical = memoryGuardCanonicalJson(input.checkpoint);
8905
+ if (decodeCheckpointUtf8(checkpointText) !== checkpointCanonical) {
8906
+ return { kind: "blocked", reason: "checkpoint-mismatch" };
8907
+ }
8908
+ const transcriptBytes = Number(input.checkpoint.transcript.bytes);
8909
+ if (!Number.isSafeInteger(transcriptBytes) || transcriptBytes < 0) {
8910
+ return { kind: "blocked", reason: "transcript-mismatch" };
8911
+ }
8912
+ const transcriptData = readCheckpointAuthorityFile(
8913
+ input.incidentAuthority,
8914
+ input.checkpoint.transcript.relative_path,
8915
+ transcriptBytes + 1,
8916
+ );
8917
+ if (!transcriptData || transcriptData.byteLength !== transcriptBytes) {
8918
+ return { kind: "blocked", reason: "transcript-mismatch" };
8919
+ }
8920
+ if (memoryGuardSha256Hex(transcriptData) !== input.checkpoint.transcript.sha256) {
8921
+ return { kind: "blocked", reason: "transcript-mismatch" };
8922
+ }
8923
+ const transcriptText = decodeCheckpointUtf8(transcriptData);
8924
+ if (transcriptText === null) return { kind: "blocked", reason: "transcript-mismatch" };
8925
+ let transcriptEntries: FileEntry[];
8926
+ try {
8927
+ transcriptEntries = parseSessionEntries(transcriptText);
8928
+ } catch {
8929
+ return { kind: "blocked", reason: "malformed" };
8930
+ }
8931
+ const transcriptHeader = transcriptEntries[0] as SessionHeader | undefined;
8932
+ if (
8933
+ transcriptHeader?.type !== "session" ||
8934
+ transcriptHeader.id !== input.checkpoint.session_id ||
8935
+ (transcriptHeader.title ?? null) !== input.checkpoint.session_name
8936
+ ) {
8937
+ return { kind: "blocked", reason: "transcript-mismatch" };
8938
+ }
8939
+ const manifestData = readCheckpointAuthorityFile(
8940
+ input.incidentAuthority,
8941
+ input.checkpoint.blob_authority.manifest_relative_path,
8942
+ 8 * 1024 * 1024,
8943
+ );
8944
+ if (!manifestData) return { kind: "blocked", reason: "blob-authority-mismatch" };
8945
+ if (memoryGuardSha256Hex(manifestData) !== input.checkpoint.blob_authority.manifest_sha256) {
8946
+ return { kind: "blocked", reason: "blob-authority-mismatch" };
8947
+ }
8948
+ const manifestText = decodeCheckpointUtf8(manifestData);
8949
+ if (manifestText === null) return { kind: "blocked", reason: "blob-manifest-mismatch" };
8950
+ let blobManifest: unknown;
8951
+ try {
8952
+ blobManifest = JSON.parse(manifestText);
8953
+ } catch {
8954
+ return { kind: "blocked", reason: "blob-manifest-mismatch" };
8955
+ }
8956
+ if (!validateMemoryGuardBlobManifest(blobManifest)) return { kind: "blocked", reason: "blob-manifest-mismatch" };
8957
+ if (memoryGuardCanonicalJson(blobManifest) !== manifestText)
8958
+ return { kind: "blocked", reason: "blob-manifest-mismatch" };
8959
+ if (blobManifest.entries.length > MEMORY_GUARD_CHECKPOINT_BLOB_MAX_ENTRIES)
8960
+ return { kind: "blocked", reason: "blob-manifest-mismatch" };
8961
+ let checkpointBlobBytes = 0;
8962
+ const seenBlobPaths = new Set<string>();
8963
+ const checkpointBlobs = new Map<string, Buffer>();
8964
+ for (const entry of blobManifest.entries) {
8965
+ if (
8966
+ entry.relative_path !== entry.sha256 ||
8967
+ seenBlobPaths.has(entry.relative_path) ||
8968
+ checkpointBlobs.has(entry.sha256)
8969
+ )
8970
+ return { kind: "blocked", reason: "blob-manifest-mismatch" };
8971
+ seenBlobPaths.add(entry.relative_path);
8972
+ const blobBytes = Number(entry.bytes);
8973
+ if (!Number.isSafeInteger(blobBytes) || blobBytes < 0)
8974
+ return { kind: "blocked", reason: "blob-manifest-mismatch" };
8975
+ checkpointBlobBytes += blobBytes;
8976
+ if (checkpointBlobBytes > MEMORY_GUARD_CHECKPOINT_BLOB_TOTAL_MAX_BYTES)
8977
+ return { kind: "blocked", reason: "blob-manifest-mismatch" };
8978
+ const blobPath = `${input.checkpoint.blob_authority.root_relative_path}/${entry.relative_path}`;
8979
+ const blobData = readCheckpointAuthorityFile(input.incidentAuthority, blobPath, blobBytes + 1);
8980
+ if (!blobData || blobData.byteLength !== blobBytes) return { kind: "blocked", reason: "blob-missing" };
8981
+ if (memoryGuardSha256Hex(blobData) !== entry.sha256) return { kind: "blocked", reason: "blob-hash-mismatch" };
8982
+ checkpointBlobs.set(entry.sha256, Buffer.from(blobData));
8983
+ }
8984
+ for (const ref of collectCheckpointBlobRefs(transcriptEntries)) {
8985
+ const hash = parseBlobRef(ref);
8986
+ if (!hash || !checkpointBlobs.has(hash)) return { kind: "blocked", reason: "blob-manifest-mismatch" };
8987
+ }
8988
+ const storage = new FileSessionStorage();
8989
+ let destination: SessionDestination;
8990
+ try {
8991
+ destination =
8992
+ input.destination === undefined
8993
+ ? explicitDestination(path.join(os.tmpdir(), `gjc-memory-guard-${input.checkpoint.session_id}`))
8994
+ : destinationFor(getProjectDir(), input.destination, storage);
8995
+ } catch {
8996
+ return { kind: "blocked", reason: "destination-unavailable" };
8997
+ }
8998
+ const transcriptPath = path.join(
8999
+ destination.directory,
9000
+ `.${input.checkpoint.session_id}.memory-guard.${crypto.randomUUID()}.jsonl`,
9001
+ );
9002
+ const transcriptName = path.basename(transcriptPath);
9003
+ const managedStagingStore =
9004
+ destination.kind === "managed"
9005
+ ? managedStoreFromContext(destination.securityContext, destination.directory)
9006
+ : undefined;
9007
+ const cleanupTranscript = async (): Promise<void> => {
9008
+ if (managedStagingStore) {
9009
+ await managedStagingStore.remove(transcriptName);
9010
+ return;
9011
+ }
9012
+ await fs.promises.rm(transcriptPath, { force: true });
9013
+ };
9014
+ let opened: RecoveryHydrationOpenResult;
9015
+ let transcriptIdentity: ResumeSessionIdentity;
9016
+ let createdDestination: CreatedDirectoryIdentity | undefined;
9017
+ try {
9018
+ if (managedStagingStore) await managedStagingStore.publishNoReplace(transcriptName, transcriptData);
9019
+ else {
9020
+ createdDestination = await ensureOwnerOnlyDirectoryTracked(destination.directory);
9021
+ await writeOwnerOnlyFileNoReplace(transcriptPath, transcriptData);
9022
+ }
9023
+ const captured = SessionManager.captureTranscriptStrict(transcriptPath, storage);
9024
+ if (captured.kind !== "captured") {
9025
+ await cleanupTranscript();
9026
+ await removeCreatedDirectoryIfEmpty(destination.directory, createdDestination);
9027
+ return { kind: "blocked", reason: captured.reason };
9028
+ }
9029
+ transcriptIdentity = captured.snapshot.identity;
9030
+ opened = await SessionManager.openExistingForRecoveryHydrationStrict(
9031
+ transcriptIdentity,
9032
+ input.destination,
9033
+ storage,
9034
+ );
9035
+ } catch {
9036
+ await cleanupTranscript();
9037
+ await removeCreatedDirectoryIfEmpty(destination.directory, createdDestination);
9038
+ return { kind: "blocked", reason: "destination-unavailable" };
9039
+ }
9040
+ if (opened.kind === "error") {
9041
+ await cleanupTranscript();
9042
+ await removeCreatedDirectoryIfEmpty(destination.directory, createdDestination);
9043
+ return { kind: "blocked", reason: opened.reason };
9044
+ }
9045
+ if (
9046
+ opened.manager.getSessionId() !== input.checkpoint.session_id ||
9047
+ (opened.manager.getSessionName() ?? null) !== input.checkpoint.session_name
9048
+ ) {
9049
+ await opened.manager.close();
9050
+ await cleanupTranscript();
9051
+ await removeCreatedDirectoryIfEmpty(destination.directory, createdDestination);
9052
+ return { kind: "blocked", reason: "transcript-mismatch" };
9053
+ }
9054
+ opened.manager.#stageMemoryGuardCheckpointBlobs(checkpointBlobs);
9055
+ opened.manager.#recoveryPromotionTranscriptPath = path.join(
9056
+ destination.directory,
9057
+ `${new Date().toISOString().replace(/[:.]/g, "-")}_${input.checkpoint.session_id}.jsonl`,
9058
+ );
9059
+ let cleanupConsumed = false;
9060
+ const cleanup = async (): Promise<void> => {
9061
+ if (cleanupConsumed) return;
9062
+ await opened.manager.close();
9063
+ await cleanupTranscript();
9064
+ await removeCreatedDirectoryIfEmpty(destination.directory, createdDestination);
9065
+ cleanupConsumed = true;
9066
+ };
9067
+ return {
9068
+ kind: "staged",
9069
+ manager: opened.manager,
9070
+ hydrationContext: opened.context,
9071
+ transcriptIdentity,
9072
+ cleanup,
9073
+ };
9074
+ }
9075
+
8352
9076
  /** Inspect a selected session without acquiring write-capable ownership. */
8353
9077
  static async inspectSessionTailReadOnly(
8354
9078
  filePath: string,
@@ -8417,8 +9141,31 @@ export class SessionManager {
8417
9141
  if ("kind" in inspected || !sameResumeIdentity(context.identity, inspected.identity)) {
8418
9142
  throw new Error("Recovery transcript authority changed before promotion.");
8419
9143
  }
9144
+ if (this.#memoryGuardCheckpointBlobs) {
9145
+ for (const blob of this.#memoryGuardCheckpointBlobs.values()) this.#blobStore.putImmutableSync(blob);
9146
+ this.#residentImageBlobStore = this.#blobStore;
9147
+ this.#memoryGuardCheckpointBlobs = undefined;
9148
+ }
8420
9149
  await this.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
8421
- writeTerminalBreadcrumb(this.cwd, context.identity.canonicalPath);
9150
+ const promotionPath = this.#recoveryPromotionTranscriptPath;
9151
+ if (!promotionPath) throw new Error("Recovery transcript promotion path is unavailable.");
9152
+ const promotedSource = inspectResumeSessionFile(context.identity.canonicalPath, this.storage);
9153
+ if ("kind" in promotedSource) throw new Error("Recovery transcript became unavailable before publication.");
9154
+ if (this.destination.kind === "managed") {
9155
+ const store = this.#managedTranscriptStore();
9156
+ const sourceName = path.basename(context.identity.canonicalPath);
9157
+ const sourceSnapshot = store.readExpected(sourceName);
9158
+ if (!sourceSnapshot) throw new Error("Recovery transcript authority changed before publication.");
9159
+ await store.publishNoReplace(path.basename(promotionPath), promotedSource.content);
9160
+ store.removeExpected(sourceName, sourceSnapshot);
9161
+ } else {
9162
+ await writeOwnerOnlyFileNoReplace(promotionPath, promotedSource.content);
9163
+ await fs.promises.rm(context.identity.canonicalPath, { force: true });
9164
+ await fsyncDirectoryPath(path.dirname(context.identity.canonicalPath));
9165
+ }
9166
+ this.#sessionFile = promotionPath;
9167
+ this.#recoveryPromotionTranscriptPath = undefined;
9168
+ writeTerminalBreadcrumb(this.cwd, promotionPath);
8422
9169
  this.#recoveryHydrationContext = undefined;
8423
9170
  }
8424
9171