@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.
- package/CHANGELOG.md +49 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +22 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +8 -0
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +15 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +44 -10
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +25 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +1 -1
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +23 -1
- package/src/config/settings.ts +246 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/types.ts +28 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +742 -85
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +8 -9
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +43 -7
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +19 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +19 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +341 -38
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +13 -1
- package/src/session/agent-session.ts +466 -50
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +764 -17
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- 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
|
-
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
|