@gajae-code/coding-agent 0.18.6 → 0.18.8
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 +318 -1
- package/dist/types/async/job-manager.d.ts +1 -1
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/capability/skill.d.ts +2 -0
- package/dist/types/capability/types.d.ts +6 -0
- package/dist/types/cli/public-command-errors.d.ts +3 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/autorouting-tier-map.d.ts +348 -9
- package/dist/types/config/file-lock.d.ts +1 -0
- package/dist/types/config/model-bindings-applier.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -3
- package/dist/types/config/model-registry.d.ts +4 -9
- package/dist/types/config/settings.d.ts +4 -0
- package/dist/types/config/skill-settings-defaults.d.ts +2 -0
- package/dist/types/cursor-delete-path.d.ts +1 -0
- package/dist/types/cursor-mcp-name.d.ts +8 -0
- package/dist/types/customization/inventory.d.ts +2 -0
- package/dist/types/debug/report-bundle-parent.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
- package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
- package/dist/types/discovery/helpers.d.ts +8 -3
- package/dist/types/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
- package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
- package/dist/types/extensibility/skill-management.d.ts +3 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
- package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
- package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
- package/dist/types/internal-urls/skill-path.d.ts +1 -0
- package/dist/types/lsp/workspace-path.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +9 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
- package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
- package/dist/types/progress/collect-project-progress.d.ts +38 -0
- package/dist/types/progress/progress-contract.d.ts +154 -0
- package/dist/types/progress/progress-snapshot.d.ts +11 -0
- package/dist/types/progress/project-progress.d.ts +141 -0
- package/dist/types/progress/render-progress.d.ts +28 -0
- package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk/acp/adapter.d.ts +1 -1
- package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
- package/dist/types/sdk/broker/broker.d.ts +18 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
- package/dist/types/sdk/bus/index.d.ts +12 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +6 -0
- package/dist/types/sdk/host/query/handlers.d.ts +3 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +38 -9
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
- package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +71 -0
- package/dist/types/session/session-storage.d.ts +62 -48
- package/dist/types/session/session-transcript-header.d.ts +2 -0
- package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
- package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
- package/dist/types/session/task-artifact-owner.d.ts +10 -0
- package/dist/types/setup/credential-auto-import.d.ts +1 -1
- package/dist/types/setup/credential-import.d.ts +6 -4
- package/dist/types/skill-state/active-state.d.ts +38 -0
- package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/python.d.ts +9 -1
- package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
- package/dist/types/tools/restricted-cwd.d.ts +1 -0
- package/dist/types/tools/todo-write.d.ts +6 -0
- package/dist/types/utils/tools-checksum.d.ts +7 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +21 -0
- package/scripts/resident-memory-bench.ts +174 -20
- package/scripts/verify-managed-empty-stop.ts +28 -0
- package/src/async/job-manager.ts +228 -122
- package/src/capability/index.ts +18 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +6 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/config-cli.ts +8 -18
- package/src/cli/customize-doctor.ts +15 -2
- package/src/cli/public-command-errors.ts +21 -1
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/skills-cli.ts +5 -0
- package/src/cli-main.ts +7 -8
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/sdk.ts +140 -29
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +307 -68
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-bindings-applier.ts +20 -0
- package/src/config/model-profile-activation.ts +240 -46
- package/src/config/model-registry.ts +47 -153
- package/src/config/settings.ts +44 -4
- package/src/config/skill-settings-defaults.ts +7 -0
- package/src/coordinator-mcp/policy.ts +3 -2
- package/src/cursor-delete-path.ts +10 -0
- package/src/cursor-mcp-name.ts +15 -0
- package/src/cursor.ts +13 -2
- package/src/customization/inventory.ts +3 -0
- package/src/debug/report-bundle-parent.ts +22 -0
- package/src/debug/report-bundle.ts +3 -0
- package/src/debug/system-info.ts +13 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
- package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
- package/src/defaults/gjc-defaults.test.ts +18 -1
- package/src/defaults/gjc-defaults.ts +3 -0
- package/src/defaults/gjc-skills.generated.ts +58 -36
- package/src/discovery/agents.ts +6 -1
- package/src/discovery/builtin.ts +10 -0
- package/src/discovery/helpers.ts +168 -23
- package/src/discovery/mcp-json.ts +6 -0
- package/src/discovery/ssh.ts +9 -4
- package/src/discovery/substitute-plugin-root.ts +3 -1
- package/src/edit/file-read-cache.ts +1 -1
- package/src/eval/py/executor.ts +380 -120
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/index.ts +5 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
- package/src/extensibility/plugins/manager.ts +3 -2
- package/src/extensibility/plugins/marketplace/cache.ts +15 -0
- package/src/extensibility/plugins/marketplace/manager.ts +11 -3
- package/src/extensibility/plugins/plugin-link-path.ts +12 -0
- package/src/extensibility/runtime-skill-discovery.ts +20 -2
- package/src/extensibility/skill-management.ts +6 -2
- package/src/extensibility/skills.ts +6 -1
- package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
- package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
- package/src/gjc-runtime/deep-interview-stage.ts +98 -21
- package/src/gjc-runtime/gc-runtime.ts +206 -8
- package/src/gjc-runtime/launch-worktree.ts +14 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- package/src/gjc-runtime/state-argv.ts +2 -0
- package/src/gjc-runtime/state-migrations.ts +91 -29
- package/src/gjc-runtime/state-runtime.ts +3716 -135
- package/src/gjc-runtime/state-writer.ts +83 -8
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
- package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
- package/src/gjc-runtime/workflow-manifest.ts +54 -21
- package/src/harness-control-plane/operate.ts +12 -4
- package/src/harness-control-plane/owner.ts +5 -1
- package/src/hashline/parser.ts +48 -9
- package/src/hashline/recovery.ts +13 -4
- package/src/hindsight/config.ts +9 -3
- package/src/internal-urls/docs-index.generated.ts +19 -18
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- package/src/modes/acp/acp-agent.ts +163 -7
- package/src/modes/components/custom-editor.ts +14 -0
- package/src/modes/components/customization/customization-dashboard.ts +12 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -2
- package/src/modes/components/extensions/state-manager.ts +15 -5
- package/src/modes/components/model-selector.ts +1 -4
- package/src/modes/components/provider-onboarding-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +4 -0
- package/src/modes/controllers/event-controller.ts +11 -4
- package/src/modes/controllers/input-controller.ts +74 -29
- package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
- package/src/modes/controllers/selector-controller.ts +28 -15
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/runtime-init.ts +4 -0
- package/src/modes/utils/abort-message.ts +3 -1
- package/src/modes/utils/interrupt-ownership.ts +12 -0
- package/src/modes/utils/keybinding-matchers.ts +4 -0
- package/src/modes/utils/macos-option-forwarding.ts +80 -0
- package/src/progress/collect-project-progress.ts +153 -0
- package/src/progress/progress-contract.ts +172 -0
- package/src/progress/progress-snapshot.ts +140 -0
- package/src/progress/project-progress.ts +446 -0
- package/src/progress/render-progress.ts +209 -0
- package/src/prompts/system/master-mode.md +1 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/runtime-mcp/client.ts +13 -4
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/elicitation.ts +142 -0
- package/src/runtime-mcp/oauth-flow.ts +5 -0
- package/src/runtime-mcp/oauth-public-url.ts +8 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +2 -2
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +93 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +83 -7
- package/src/sdk/broker/ensure.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +820 -73
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
- package/src/sdk/bus/index.ts +241 -46
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/bus/telegram-daemon.ts +4 -2
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/host.ts +6 -0
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2805 -411
- package/src/sdk/host/session-runtime.ts +428 -111
- package/src/sdk/host/surface-policy.ts +1 -0
- package/src/sdk/index.ts +8 -0
- package/src/sdk/lifecycle-session.ts +3 -1
- package/src/sdk/mcp/server.ts +15 -3
- package/src/sdk/prompt-deadline-manager.ts +15 -0
- package/src/sdk/protocol/operation-inventory.generated.json +146 -0
- package/src/sdk/protocol/operation-registry.ts +9 -2
- package/src/sdk/router/session-router.ts +94 -14
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +81 -38
- package/src/session/agent-session.ts +674 -224
- package/src/session/artifacts.ts +174 -70
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3457 -462
- package/src/session/internal/managed-session-storage.ts +482 -61
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +245 -0
- package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
- package/src/session/session-manager.ts +939 -133
- package/src/session/session-retirement.ts +714 -0
- package/src/session/session-storage.ts +418 -174
- package/src/session/session-transcript-header.ts +17 -0
- package/src/session/task-artifact-owner-codec.ts +641 -0
- package/src/session/task-artifact-owner-retirement.ts +477 -0
- package/src/session/task-artifact-owner.ts +289 -0
- package/src/setup/credential-auto-import.ts +12 -3
- package/src/setup/credential-import.ts +176 -5
- package/src/setup/paseo/announce.ts +6 -1
- package/src/skill-state/active-state.ts +132 -2
- package/src/slash-commands/builtin-registry.ts +39 -9
- package/src/slash-commands/helpers/progress-report.ts +23 -0
- package/src/stt/setup.ts +3 -5
- package/src/task/commands.ts +3 -1
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +6 -0
- package/src/tools/ask.ts +205 -6
- package/src/tools/ast-edit.ts +4 -0
- package/src/tools/bash-allowed-prefixes.ts +15 -0
- package/src/tools/bash-skill-urls.ts +56 -3
- package/src/tools/bash.ts +48 -79
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/python.ts +187 -66
- package/src/tools/recipe/index.ts +9 -0
- package/src/tools/recipe/recipe-guard.ts +19 -0
- package/src/tools/restricted-cwd.ts +10 -0
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +28 -1
- package/src/tools/todo-write.ts +10 -1
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/write.ts +1 -0
- package/src/utils/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- package/src/web/search/providers/codex.ts +3 -2
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
import {
|
|
118
118
|
assertManagedDirectoryRoot,
|
|
119
119
|
captureManagedFileNoFollow,
|
|
120
|
+
captureManagedFilePrefixNoFollow,
|
|
120
121
|
fsyncManagedArtifactTree,
|
|
121
122
|
MANAGED_ARTIFACT_MAX_FILE_BYTES,
|
|
122
123
|
ManagedAppendIdentityMismatchError,
|
|
@@ -130,6 +131,7 @@ import {
|
|
|
130
131
|
type ManagedSessionSecurityPolicy,
|
|
131
132
|
managedDirectoryRoot,
|
|
132
133
|
mayCleanManagedTreeStaging,
|
|
134
|
+
publishManagedFileNoReplaceSync,
|
|
133
135
|
retainManagedDirectoryAuthority,
|
|
134
136
|
} from "./internal/managed-session-storage";
|
|
135
137
|
import { classifyNativePublishOutcome, formatNativePublishDiagnostic } from "./internal/native-publish-outcome";
|
|
@@ -247,6 +249,7 @@ import {
|
|
|
247
249
|
replaceSessionCommitMarkerCheckedSync,
|
|
248
250
|
SESSION_RANGE_READ_MAX_BYTES,
|
|
249
251
|
} from "./session-storage";
|
|
252
|
+
import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
|
|
250
253
|
|
|
251
254
|
export const CURRENT_SESSION_VERSION = 5;
|
|
252
255
|
|
|
@@ -2140,6 +2143,23 @@ export interface ResumeSessionIdentity {
|
|
|
2140
2143
|
sha256: string;
|
|
2141
2144
|
}
|
|
2142
2145
|
|
|
2146
|
+
/** Descriptor-bound bounded fingerprint for private explicit-session rollback authority.
|
|
2147
|
+
* JSON-safe: all bigint fields are stored as strings to survive JSON serialization,
|
|
2148
|
+
* spread, structuredClone, and JSON round-trip. */
|
|
2149
|
+
export type ExplicitPersistIdentity = {
|
|
2150
|
+
// All bigint fields stored as base-10 strings for JSON safety
|
|
2151
|
+
dev: string;
|
|
2152
|
+
ino: string;
|
|
2153
|
+
nlink?: string;
|
|
2154
|
+
size: number;
|
|
2155
|
+
mtimeMs: number;
|
|
2156
|
+
mtimeNs: string;
|
|
2157
|
+
ctimeNs?: string;
|
|
2158
|
+
canonicalPath: string;
|
|
2159
|
+
sessionId: string;
|
|
2160
|
+
fingerprintSha256: string;
|
|
2161
|
+
};
|
|
2162
|
+
|
|
2143
2163
|
export interface ResumeTailResumable {
|
|
2144
2164
|
kind: "resumable";
|
|
2145
2165
|
identity: ResumeSessionIdentity;
|
|
@@ -2175,6 +2195,7 @@ export const RESUME_TRANSCRIPT_MAX_BYTES = MANAGED_ARTIFACT_MAX_FILE_BYTES;
|
|
|
2175
2195
|
*/
|
|
2176
2196
|
export const BOUNDED_RESUME_TRANSCRIPT_MAX_BYTES = 2 * 1024 * 1024 * 1024 + 1024 * 1024;
|
|
2177
2197
|
const EAGER_RESUME_TRANSCRIPT_MAX_BYTES = MANAGED_ARTIFACT_MAX_FILE_BYTES;
|
|
2198
|
+
const EXPLICIT_PERSIST_FINGERPRINT_RANGE_BYTES = 64 * 1024;
|
|
2178
2199
|
|
|
2179
2200
|
/**
|
|
2180
2201
|
* Recovery actions offered when a live transcript is at or past the managed
|
|
@@ -2639,8 +2660,10 @@ export function createReadonlySessionManager(manager: SessionManager): ReadonlyS
|
|
|
2639
2660
|
}
|
|
2640
2661
|
|
|
2641
2662
|
/** Internal artifact-writing capability. Read-only facades expose it only through a private weak-map lookup. */
|
|
2663
|
+
export type SessionArtifactPublication = (content: string, toolType: string) => Promise<string | undefined>;
|
|
2664
|
+
|
|
2642
2665
|
export type SessionArtifactCapability = Readonly<
|
|
2643
|
-
Pick<SessionManager, "allocateArtifactPath" | "saveArtifact" | "putBlob">
|
|
2666
|
+
Pick<SessionManager, "allocateArtifactPath" | "saveArtifact" | "putBlob" | "captureArtifactPublication">
|
|
2644
2667
|
>;
|
|
2645
2668
|
|
|
2646
2669
|
const sessionArtifactCapabilities = new WeakMap<SessionManager, SessionArtifactCapability>();
|
|
@@ -2658,6 +2681,7 @@ export function sessionArtifactCapability(value: unknown): SessionArtifactCapabi
|
|
|
2658
2681
|
allocateArtifactPath: value.allocateArtifactPath.bind(value),
|
|
2659
2682
|
saveArtifact: value.saveArtifact.bind(value),
|
|
2660
2683
|
putBlob: value.putBlob.bind(value),
|
|
2684
|
+
captureArtifactPublication: value.captureArtifactPublication.bind(value),
|
|
2661
2685
|
});
|
|
2662
2686
|
sessionArtifactCapabilities.set(value, capability);
|
|
2663
2687
|
}
|
|
@@ -3404,11 +3428,10 @@ function retainedTreeSnapshotEqualsAfterRename(
|
|
|
3404
3428
|
/**
|
|
3405
3429
|
* True when a cleanup error only reports the authorized POSIX quarantine.
|
|
3406
3430
|
*
|
|
3407
|
-
*
|
|
3408
|
-
*
|
|
3409
|
-
*
|
|
3410
|
-
*
|
|
3411
|
-
* supersede the primary failure that triggered it.
|
|
3431
|
+
* Exact managed-tree removal durably detaches its captured tree into native
|
|
3432
|
+
* quarantine and reports `cleanup_pending`. This is a completed cleanup
|
|
3433
|
+
* disposition, not proof of physical reclamation or authority to replay the
|
|
3434
|
+
* quarantine. Callers may accept it only at the specific cleanup boundary.
|
|
3412
3435
|
*/
|
|
3413
3436
|
function isAuthorizedPendingCleanup(cleanupError: Error): boolean {
|
|
3414
3437
|
return cleanupError.message === "cleanup_pending";
|
|
@@ -6510,17 +6533,110 @@ class NdjsonFileWriter {
|
|
|
6510
6533
|
const PROJECT_SESSION_SCAN_MAX_DIRECTORIES = 4096;
|
|
6511
6534
|
const PROJECT_SESSION_SCAN_MAX_FILES = 1000;
|
|
6512
6535
|
|
|
6513
|
-
function isProjectSessionTranscriptPath(projectGjcDir: string, filePath: string): boolean {
|
|
6536
|
+
export function isProjectSessionTranscriptPath(projectGjcDir: string, filePath: string): boolean {
|
|
6514
6537
|
if (isStagedSessionPath(filePath)) return false;
|
|
6515
6538
|
const relative = path.relative(projectGjcDir, filePath);
|
|
6516
6539
|
if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
|
|
6517
6540
|
const segments = relative.split(path.sep);
|
|
6518
6541
|
if (segments.includes(SESSION_STAGING_DIRNAME)) return false;
|
|
6519
|
-
if (segments.length === 1) return true;
|
|
6520
6542
|
const parent = segments.at(-2);
|
|
6521
6543
|
return parent === "agent-session" || segments.includes("sessions");
|
|
6522
6544
|
}
|
|
6523
6545
|
|
|
6546
|
+
export function readAuthorizedProjectSessionTranscript(
|
|
6547
|
+
projectGjcDir: string,
|
|
6548
|
+
filePath: string,
|
|
6549
|
+
maxBytes: number,
|
|
6550
|
+
): Buffer | undefined {
|
|
6551
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0 || maxBytes === Number.MAX_SAFE_INTEGER) return undefined;
|
|
6552
|
+
const root = path.resolve(projectGjcDir);
|
|
6553
|
+
const candidate = path.resolve(filePath);
|
|
6554
|
+
if (!isProjectSessionTranscriptPath(root, candidate)) return undefined;
|
|
6555
|
+
const relativePath = path.relative(root, candidate).split(path.sep).join("/");
|
|
6556
|
+
let authority: native.RecoveryFsRoot | undefined;
|
|
6557
|
+
try {
|
|
6558
|
+
authority = nativeSessionManager().openRecoveryFsRoot(root);
|
|
6559
|
+
} catch (error) {
|
|
6560
|
+
if (!(error instanceof Error) || error.message !== "unsupported_platform") throw error;
|
|
6561
|
+
}
|
|
6562
|
+
try {
|
|
6563
|
+
const result: native.RecoveryFsResult = authority?.readManaged(relativePath) ?? {
|
|
6564
|
+
ok: false,
|
|
6565
|
+
code: "unsupported_platform",
|
|
6566
|
+
};
|
|
6567
|
+
if (!result.ok && result.code === "unsupported_platform") {
|
|
6568
|
+
const relativeParts = path.relative(root, path.dirname(candidate)).split(path.sep).filter(Boolean);
|
|
6569
|
+
const parentPaths = [root];
|
|
6570
|
+
for (const part of relativeParts) parentPaths.push(path.join(parentPaths[parentPaths.length - 1]!, part));
|
|
6571
|
+
const parentIdentities = parentPaths.map(parentPath => {
|
|
6572
|
+
const stat = fs.lstatSync(parentPath, { bigint: true });
|
|
6573
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("project transcript parent is unsafe");
|
|
6574
|
+
return { path: parentPath, dev: stat.dev, ino: stat.ino, mode: stat.mode, ctimeNs: stat.ctimeNs };
|
|
6575
|
+
});
|
|
6576
|
+
let captured: ManagedFileSnapshot;
|
|
6577
|
+
if (os.platform() === "win32" || os.platform() === "darwin") {
|
|
6578
|
+
// Windows and Darwin cannot traverse directory handles through /dev/fd.
|
|
6579
|
+
// Bind the bounded capture to the inspected file and recheck every
|
|
6580
|
+
// parent (including junctions) before allowing bytes to leave this reader.
|
|
6581
|
+
const expected = fs.lstatSync(candidate, { bigint: true });
|
|
6582
|
+
if (!expected.isFile() || expected.isSymbolicLink() || expected.nlink > 1)
|
|
6583
|
+
throw new Error("project transcript file is unsafe");
|
|
6584
|
+
if (expected.size > BigInt(maxBytes)) return undefined;
|
|
6585
|
+
captured = captureManagedFilePrefixNoFollow(candidate, maxBytes + 1);
|
|
6586
|
+
if (
|
|
6587
|
+
captured.identity.dev !== BigInt.asUintN(64, expected.dev) ||
|
|
6588
|
+
captured.identity.ino !== BigInt.asUintN(64, expected.ino) ||
|
|
6589
|
+
captured.identity.nlink !== expected.nlink ||
|
|
6590
|
+
captured.identity.size !== Number(expected.size) ||
|
|
6591
|
+
captured.identity.mtimeNs !== expected.mtimeNs ||
|
|
6592
|
+
captured.identity.ctimeNs !== expected.ctimeNs
|
|
6593
|
+
)
|
|
6594
|
+
throw new Error("project transcript file identity changed during read");
|
|
6595
|
+
} else {
|
|
6596
|
+
const parentPath = path.dirname(candidate);
|
|
6597
|
+
const parentFd = fs.openSync(
|
|
6598
|
+
parentPath,
|
|
6599
|
+
fs.constants.O_RDONLY | (fs.constants.O_DIRECTORY ?? 0) | (fs.constants.O_NOFOLLOW ?? 0),
|
|
6600
|
+
);
|
|
6601
|
+
try {
|
|
6602
|
+
const expectedParent = parentIdentities[parentIdentities.length - 1]!;
|
|
6603
|
+
const openedParent = fs.fstatSync(parentFd, { bigint: true });
|
|
6604
|
+
if (
|
|
6605
|
+
openedParent.dev !== expectedParent.dev ||
|
|
6606
|
+
openedParent.ino !== expectedParent.ino ||
|
|
6607
|
+
openedParent.mode !== expectedParent.mode
|
|
6608
|
+
)
|
|
6609
|
+
throw new Error("project transcript parent handle identity mismatch");
|
|
6610
|
+
captured = captureManagedFilePrefixNoFollow(
|
|
6611
|
+
path.join("/dev/fd", String(parentFd), path.basename(candidate)),
|
|
6612
|
+
maxBytes + 1,
|
|
6613
|
+
);
|
|
6614
|
+
} finally {
|
|
6615
|
+
fs.closeSync(parentFd);
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
for (const expected of parentIdentities) {
|
|
6619
|
+
const stat = fs.lstatSync(expected.path, { bigint: true });
|
|
6620
|
+
if (
|
|
6621
|
+
!stat.isDirectory() ||
|
|
6622
|
+
stat.isSymbolicLink() ||
|
|
6623
|
+
stat.dev !== expected.dev ||
|
|
6624
|
+
stat.ino !== expected.ino ||
|
|
6625
|
+
stat.mode !== expected.mode ||
|
|
6626
|
+
stat.ctimeNs !== expected.ctimeNs
|
|
6627
|
+
)
|
|
6628
|
+
throw new Error("project transcript parent identity changed during read");
|
|
6629
|
+
}
|
|
6630
|
+
if (captured.identity.size > BigInt(maxBytes) || captured.bytes.byteLength > maxBytes) return undefined;
|
|
6631
|
+
return Buffer.from(captured.bytes);
|
|
6632
|
+
}
|
|
6633
|
+
if (!result.ok || !result.data || result.data.byteLength > maxBytes) return undefined;
|
|
6634
|
+
return Buffer.from(result.data);
|
|
6635
|
+
} finally {
|
|
6636
|
+
authority?.close();
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
|
|
6524
6640
|
/**
|
|
6525
6641
|
* Discover resumable transcripts intentionally stored inside a project's `.gjc`.
|
|
6526
6642
|
* Runtime token/audit JSONL files are excluded by requiring a known transcript
|
|
@@ -7209,10 +7325,12 @@ interface SessionManagerStateSnapshot {
|
|
|
7209
7325
|
flushed: boolean;
|
|
7210
7326
|
ensuredOnDisk: boolean;
|
|
7211
7327
|
needsFullRewriteOnNextPersist: boolean;
|
|
7212
|
-
fileEntries: FileEntry[];
|
|
7213
|
-
materializedFileEntries: FileEntry[];
|
|
7328
|
+
fileEntries: readonly FileEntry[];
|
|
7329
|
+
materializedFileEntries: readonly FileEntry[];
|
|
7214
7330
|
adoptedArtifactManager: ArtifactManager | null;
|
|
7215
7331
|
coldRestoreFile?: string;
|
|
7332
|
+
/** Explicit persistence identity for snapshot serialization safety. Preserved across cross-manager adoptions. */
|
|
7333
|
+
readonly explicitPersistIdentity?: ExplicitPersistIdentity;
|
|
7216
7334
|
}
|
|
7217
7335
|
|
|
7218
7336
|
/** Benchmark-derived cap for strong materialized session snapshots. */
|
|
@@ -7236,6 +7354,7 @@ export const SessionManagerTestHooks: {
|
|
|
7236
7354
|
beforeManagedMissingReturn?: (filePath: string, storage: SessionStorage) => void | Promise<void>;
|
|
7237
7355
|
afterManagedMissingAssertion?: (filePath: string, storage: SessionStorage) => void | Promise<void>;
|
|
7238
7356
|
beforeManagedSwitchIdentity?: (filePath: string, storage: SessionStorage) => void | Promise<void>;
|
|
7357
|
+
beforeColdRollbackCommit?: (filePath: string, storage: SessionStorage) => void | Promise<void>;
|
|
7239
7358
|
/** Internal first-open GC strategy override; omitted means current. */
|
|
7240
7359
|
firstOpenGcStrategy?: SessionMemoryGcStrategy;
|
|
7241
7360
|
/** Internal first-open secondary-artifact mode override; omitted means auto. */
|
|
@@ -7517,6 +7636,22 @@ function validateEvictedToolOutputHandle(
|
|
|
7517
7636
|
return { ok: true, handle: value as EvictedToolOutputHandle };
|
|
7518
7637
|
}
|
|
7519
7638
|
export class SessionManager {
|
|
7639
|
+
readonly #stateSnapshots = new WeakMap<
|
|
7640
|
+
SessionManagerStateSnapshot,
|
|
7641
|
+
Readonly<SessionManagerStateSnapshot> & { readonly explicitPersistIdentity?: ExplicitPersistIdentity }
|
|
7642
|
+
>();
|
|
7643
|
+
#artifactLifecycle = Symbol("session-artifact-lifecycle");
|
|
7644
|
+
#artifactLifecycleSessionId = "";
|
|
7645
|
+
#artifactLifecycleSessionFile: string | undefined;
|
|
7646
|
+
#artifactClosing = false;
|
|
7647
|
+
#managedRollbackIdentity:
|
|
7648
|
+
| Readonly<{
|
|
7649
|
+
sessionId: string;
|
|
7650
|
+
sessionFile: string;
|
|
7651
|
+
before: Readonly<ManagedFileIdentity>;
|
|
7652
|
+
after: Readonly<ManagedFileIdentity>;
|
|
7653
|
+
}>
|
|
7654
|
+
| undefined;
|
|
7520
7655
|
#sessionId: string = "";
|
|
7521
7656
|
/** True once a lifecycle pre-allocated id has been adopted (consume-once). */
|
|
7522
7657
|
#lifecycleIdAdopted: boolean = false;
|
|
@@ -7552,6 +7687,7 @@ export class SessionManager {
|
|
|
7552
7687
|
#persistWriterPath: string | undefined;
|
|
7553
7688
|
#persistChain: Promise<void> = Promise.resolve();
|
|
7554
7689
|
#persistError: Error | undefined;
|
|
7690
|
+
#closeRetryOriginError: Error | undefined;
|
|
7555
7691
|
#persistErrorReported = false;
|
|
7556
7692
|
/** Defense-in-depth (#4443): one-shot warn for adjacent private thinking blocks in persisted assistant transcripts. */
|
|
7557
7693
|
#warnedAdjacentThinkingPersist = false;
|
|
@@ -8213,6 +8349,16 @@ export class SessionManager {
|
|
|
8213
8349
|
prepared.releaseReferences();
|
|
8214
8350
|
}
|
|
8215
8351
|
|
|
8352
|
+
/**
|
|
8353
|
+
* Adopt the resident store of a fully prepared rollback candidate. The candidate
|
|
8354
|
+
* owns the store it built for the restored lifecycle, so the swap happens here,
|
|
8355
|
+
* inside the resident-store seams, instead of at the call site.
|
|
8356
|
+
*/
|
|
8357
|
+
#installRollbackCandidateResidentStore(candidate: SessionManager): void {
|
|
8358
|
+
this.#residentTextBlobStore = candidate.#residentTextBlobStore;
|
|
8359
|
+
candidate.#residentTextBlobStore = new MemoryBlobStore();
|
|
8360
|
+
}
|
|
8361
|
+
|
|
8216
8362
|
#releaseResidentTextStore(): void {
|
|
8217
8363
|
const predecessor = this.#residentTextBlobStore;
|
|
8218
8364
|
this.#residentTextBlobStore = new MemoryBlobStore();
|
|
@@ -8302,10 +8448,157 @@ export class SessionManager {
|
|
|
8302
8448
|
return this.#blobStore.put(data);
|
|
8303
8449
|
}
|
|
8304
8450
|
|
|
8451
|
+
#issueStateSnapshot(snapshot: SessionManagerStateSnapshot): SessionManagerStateSnapshot {
|
|
8452
|
+
const cloneEntry = (entry: FileEntry): FileEntry =>
|
|
8453
|
+
entry.type === "session" ? { ...entry } : cloneSessionEntry(entry);
|
|
8454
|
+
// Public rollback data must not alias either the live graph or its private issuer copy.
|
|
8455
|
+
const fileEntries = snapshot.fileEntries.map(cloneEntry);
|
|
8456
|
+
const materializedFileEntries = snapshot.materializedFileEntries.map(cloneEntry);
|
|
8457
|
+
snapshot.fileEntries = fileEntries;
|
|
8458
|
+
snapshot.materializedFileEntries = materializedFileEntries;
|
|
8459
|
+
snapshot.managedPersistExpectedIdentity = snapshot.managedPersistExpectedIdentity
|
|
8460
|
+
? { ...snapshot.managedPersistExpectedIdentity }
|
|
8461
|
+
: undefined;
|
|
8462
|
+
const explicitPersistIdentity =
|
|
8463
|
+
this.destination.kind === "explicit" &&
|
|
8464
|
+
this.#storage instanceof FileSessionStorage &&
|
|
8465
|
+
snapshot.ensuredOnDisk &&
|
|
8466
|
+
snapshot.sessionFile
|
|
8467
|
+
? this.#captureExplicitPersistIdentity(snapshot.sessionFile)
|
|
8468
|
+
: undefined;
|
|
8469
|
+
if (explicitPersistIdentity && explicitPersistIdentity.sessionId !== snapshot.sessionId)
|
|
8470
|
+
throw new Error("Session rollback persistence identity is unavailable.");
|
|
8471
|
+
// Store the explicit identity as an enumerable property on the snapshot itself
|
|
8472
|
+
// so that it survives documented caller-adjusted copies (spread, JSON round-trip,
|
|
8473
|
+
// structuredClone, etc.) and cross-manager adoptions can access the captured
|
|
8474
|
+
// identity instead of reconstructing it from the current file state.
|
|
8475
|
+
if (explicitPersistIdentity) {
|
|
8476
|
+
Object.defineProperty(snapshot, "explicitPersistIdentity", {
|
|
8477
|
+
value: Object.freeze({ ...explicitPersistIdentity }),
|
|
8478
|
+
enumerable: true,
|
|
8479
|
+
configurable: false,
|
|
8480
|
+
writable: false,
|
|
8481
|
+
});
|
|
8482
|
+
}
|
|
8483
|
+
this.#stateSnapshots.set(
|
|
8484
|
+
snapshot,
|
|
8485
|
+
Object.freeze({
|
|
8486
|
+
...snapshot,
|
|
8487
|
+
fileEntries: Object.freeze(fileEntries.map(cloneEntry)),
|
|
8488
|
+
materializedFileEntries: Object.freeze(materializedFileEntries.map(cloneEntry)),
|
|
8489
|
+
managedPersistExpectedIdentity: snapshot.managedPersistExpectedIdentity
|
|
8490
|
+
? Object.freeze({ ...snapshot.managedPersistExpectedIdentity })
|
|
8491
|
+
: undefined,
|
|
8492
|
+
explicitPersistIdentity: explicitPersistIdentity
|
|
8493
|
+
? Object.freeze({ ...explicitPersistIdentity })
|
|
8494
|
+
: undefined,
|
|
8495
|
+
}),
|
|
8496
|
+
);
|
|
8497
|
+
return snapshot;
|
|
8498
|
+
}
|
|
8499
|
+
|
|
8500
|
+
/**
|
|
8501
|
+
* Resolve the state source for an explicit adoption (`restoreState`). Adoption is
|
|
8502
|
+
* caller-driven state, not rollback authority: a snapshot may come from another
|
|
8503
|
+
* manager or from a caller-adjusted copy of one, so those are adopted as given and
|
|
8504
|
+
* constrained only by the live-state assertions inside `restoreState`. A snapshot
|
|
8505
|
+
* issued by this manager resolves to its frozen issuer copy, which carries the
|
|
8506
|
+
* explicit persistence identity captured at issuance. The rollback lane
|
|
8507
|
+
* (`restoreRollbackState`) keeps requiring an authenticated issuance.
|
|
8508
|
+
*/
|
|
8509
|
+
#resolveAdoptedStateSnapshot(
|
|
8510
|
+
snapshot: SessionManagerStateSnapshot,
|
|
8511
|
+
): Readonly<SessionManagerStateSnapshot> & { readonly explicitPersistIdentity?: ExplicitPersistIdentity } {
|
|
8512
|
+
const issued = this.#stateSnapshots.get(snapshot);
|
|
8513
|
+
if (issued) return issued;
|
|
8514
|
+
if (snapshot.adoptedArtifactManager !== null && !(snapshot.adoptedArtifactManager instanceof ArtifactManager)) {
|
|
8515
|
+
throw new Error("Session rollback adopted artifact manager is not live.");
|
|
8516
|
+
}
|
|
8517
|
+
// Preserve explicit identity from cross-manager adoptions. The identity is now
|
|
8518
|
+
// stored as an enumerable property on the snapshot so it survives documented
|
|
8519
|
+
// caller-adjusted copies (spread, JSON round-trip, structuredClone, etc.).
|
|
8520
|
+
// Adoption must restore the identity captured in the snapshot, not the current
|
|
8521
|
+
// state of the sessionFile, to ensure stale file checks use the captured identity.
|
|
8522
|
+
let explicit = snapshot.explicitPersistIdentity;
|
|
8523
|
+
// Only attempt reconstruction for explicit-storage sessions that don't already
|
|
8524
|
+
// have an explicit identity (e.g., snapshots captured before this change).
|
|
8525
|
+
if (!explicit && snapshot.sessionFile && !snapshot.managedPersistExpectedIdentity) {
|
|
8526
|
+
try {
|
|
8527
|
+
explicit = this.#captureExplicitPersistIdentity(snapshot.sessionFile);
|
|
8528
|
+
// Verify the reconstructed identity matches the snapshot's sessionId
|
|
8529
|
+
if (explicit.sessionId !== snapshot.sessionId) {
|
|
8530
|
+
// Session ID mismatch indicates the snapshot is for a different session
|
|
8531
|
+
explicit = undefined;
|
|
8532
|
+
}
|
|
8533
|
+
} catch {
|
|
8534
|
+
// If reconstruction fails (e.g., storage doesn't support readRangeSync),
|
|
8535
|
+
// continue without it; validation will handle missing identity
|
|
8536
|
+
explicit = undefined;
|
|
8537
|
+
}
|
|
8538
|
+
}
|
|
8539
|
+
if (explicit) {
|
|
8540
|
+
return Object.freeze({
|
|
8541
|
+
...snapshot,
|
|
8542
|
+
explicitPersistIdentity: explicit,
|
|
8543
|
+
}) as Readonly<SessionManagerStateSnapshot> & { readonly explicitPersistIdentity?: ExplicitPersistIdentity };
|
|
8544
|
+
}
|
|
8545
|
+
return snapshot;
|
|
8546
|
+
}
|
|
8547
|
+
|
|
8548
|
+
#authenticateStateSnapshot(snapshot: SessionManagerStateSnapshot): Readonly<SessionManagerStateSnapshot> {
|
|
8549
|
+
this.#assertArtifactOpen();
|
|
8550
|
+
const issued = this.#stateSnapshots.get(snapshot);
|
|
8551
|
+
if (
|
|
8552
|
+
!issued ||
|
|
8553
|
+
snapshot.sessionId !== issued.sessionId ||
|
|
8554
|
+
snapshot.sessionFile !== issued.sessionFile ||
|
|
8555
|
+
snapshot.sessionName !== issued.sessionName ||
|
|
8556
|
+
snapshot.titleSource !== issued.titleSource ||
|
|
8557
|
+
snapshot.coldRestoreFile !== issued.coldRestoreFile ||
|
|
8558
|
+
snapshot.adoptedArtifactManager !== issued.adoptedArtifactManager ||
|
|
8559
|
+
snapshot.flushed !== issued.flushed ||
|
|
8560
|
+
snapshot.ensuredOnDisk !== issued.ensuredOnDisk ||
|
|
8561
|
+
snapshot.needsFullRewriteOnNextPersist !== issued.needsFullRewriteOnNextPersist ||
|
|
8562
|
+
!util.isDeepStrictEqual(snapshot.managedPersistExpectedIdentity, issued.managedPersistExpectedIdentity) ||
|
|
8563
|
+
!util.isDeepStrictEqual(snapshot.fileEntries, issued.fileEntries) ||
|
|
8564
|
+
!util.isDeepStrictEqual(snapshot.materializedFileEntries, issued.materializedFileEntries)
|
|
8565
|
+
) {
|
|
8566
|
+
throw new Error("Session rollback snapshot is not authentic.");
|
|
8567
|
+
}
|
|
8568
|
+
return issued;
|
|
8569
|
+
}
|
|
8570
|
+
|
|
8571
|
+
#assertSnapshotPersistenceIdentity(
|
|
8572
|
+
snapshot: Readonly<SessionManagerStateSnapshot> & { readonly explicitPersistIdentity?: ExplicitPersistIdentity },
|
|
8573
|
+
store?: ManagedSessionDescendantStore,
|
|
8574
|
+
): ManagedFileIdentity | undefined {
|
|
8575
|
+
if (!snapshot.managedPersistExpectedIdentity || !snapshot.sessionFile) {
|
|
8576
|
+
if (snapshot.explicitPersistIdentity && snapshot.sessionFile) {
|
|
8577
|
+
const current = this.#captureExplicitPersistIdentity(snapshot.sessionFile);
|
|
8578
|
+
if (!util.isDeepStrictEqual(current, snapshot.explicitPersistIdentity))
|
|
8579
|
+
throw new Error("Session rollback persistence identity changed.");
|
|
8580
|
+
}
|
|
8581
|
+
return undefined;
|
|
8582
|
+
}
|
|
8583
|
+
const current = this.#captureManagedPersistIdentity(snapshot.sessionFile, store);
|
|
8584
|
+
if (util.isDeepStrictEqual(current, snapshot.managedPersistExpectedIdentity)) return current;
|
|
8585
|
+
const rollback = this.#managedRollbackIdentity;
|
|
8586
|
+
if (
|
|
8587
|
+
rollback &&
|
|
8588
|
+
rollback.sessionId === snapshot.sessionId &&
|
|
8589
|
+
rollback.sessionFile === snapshot.sessionFile &&
|
|
8590
|
+
util.isDeepStrictEqual(rollback.before, snapshot.managedPersistExpectedIdentity) &&
|
|
8591
|
+
util.isDeepStrictEqual(rollback.after, current)
|
|
8592
|
+
)
|
|
8593
|
+
return current;
|
|
8594
|
+
throw new Error("Session rollback persistence identity changed.");
|
|
8595
|
+
}
|
|
8596
|
+
|
|
8305
8597
|
/** Capture rollback authority without materializing an active cold transcript. @internal */
|
|
8306
8598
|
async captureRollbackState(): Promise<SessionManagerStateSnapshot> {
|
|
8599
|
+
this.#assertArtifactOpen();
|
|
8307
8600
|
if (this.#coldSidecarActive() && this.#sessionFile) {
|
|
8308
|
-
return {
|
|
8601
|
+
return this.#issueStateSnapshot({
|
|
8309
8602
|
sessionId: this.#sessionId,
|
|
8310
8603
|
sessionName: this.#sessionName,
|
|
8311
8604
|
titleSource: this.#titleSource,
|
|
@@ -8318,46 +8611,171 @@ export class SessionManager {
|
|
|
8318
8611
|
materializedFileEntries: [],
|
|
8319
8612
|
adoptedArtifactManager: this.#adoptedArtifactManager,
|
|
8320
8613
|
coldRestoreFile: this.#sessionFile,
|
|
8321
|
-
};
|
|
8614
|
+
});
|
|
8322
8615
|
}
|
|
8323
8616
|
return this.captureState();
|
|
8324
8617
|
}
|
|
8325
8618
|
|
|
8326
8619
|
/** Restore a rollback snapshot, reopening cold authority instead of hydrating it. @internal */
|
|
8327
8620
|
async restoreRollbackState(snapshot: SessionManagerStateSnapshot): Promise<void> {
|
|
8328
|
-
|
|
8621
|
+
const issued = this.#authenticateStateSnapshot(snapshot);
|
|
8622
|
+
if (!issued.coldRestoreFile) {
|
|
8329
8623
|
this.restoreState(snapshot);
|
|
8330
8624
|
return;
|
|
8331
8625
|
}
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
this.#persistErrorReported = false;
|
|
8626
|
+
const lifecycle = this.#syncArtifactLifecycle();
|
|
8627
|
+
const predecessorId = this.#sessionId;
|
|
8628
|
+
const predecessorFile = this.#sessionFile;
|
|
8336
8629
|
const managedTransition =
|
|
8337
8630
|
this.destination.kind === "managed"
|
|
8338
|
-
? this.#prepareManagedDestinationTransition(path.resolve(path.dirname(
|
|
8631
|
+
? this.#prepareManagedDestinationTransition(path.resolve(path.dirname(issued.coldRestoreFile)))
|
|
8339
8632
|
: undefined;
|
|
8633
|
+
const candidateDestination = managedTransition?.destination ?? this.destination;
|
|
8634
|
+
let candidate: SessionManager;
|
|
8635
|
+
try {
|
|
8636
|
+
candidate = new SessionManager(
|
|
8637
|
+
this.cwd,
|
|
8638
|
+
this.sessionDir,
|
|
8639
|
+
this.persist,
|
|
8640
|
+
this.#storage,
|
|
8641
|
+
candidateDestination,
|
|
8642
|
+
true,
|
|
8643
|
+
);
|
|
8644
|
+
} catch (error) {
|
|
8645
|
+
managedTransition?.dispose();
|
|
8646
|
+
throw error;
|
|
8647
|
+
}
|
|
8648
|
+
candidate.#sessionMemoryMode = this.#sessionMemoryMode;
|
|
8649
|
+
candidate.#sessionMemoryAutoDisabledReason = this.#sessionMemoryAutoDisabledReason;
|
|
8650
|
+
candidate.#residentImageBlobStore = this.#residentImageBlobStore;
|
|
8651
|
+
candidate.#memoryGuardCheckpointBlobs = this.#memoryGuardCheckpointBlobs;
|
|
8652
|
+
let installed = false;
|
|
8653
|
+
const disposeCandidate = (): void => {
|
|
8654
|
+
const residentStore = candidate.#residentTextBlobStore;
|
|
8655
|
+
candidate.#residentTextBlobStore = new MemoryBlobStore();
|
|
8656
|
+
candidate.#disposeResidentTextStore(residentStore);
|
|
8657
|
+
try {
|
|
8658
|
+
candidate.#releaseManagedSidecarCache();
|
|
8659
|
+
} catch (error) {
|
|
8660
|
+
logger.warn("Failed to dispose staged rollback sidecar authority", { error: toError(error).message });
|
|
8661
|
+
}
|
|
8662
|
+
try {
|
|
8663
|
+
candidate.#managedTranscriptStoreCache?.store.close();
|
|
8664
|
+
} catch (error) {
|
|
8665
|
+
logger.warn("Failed to dispose staged rollback transcript authority", { error: toError(error).message });
|
|
8666
|
+
}
|
|
8667
|
+
candidate.#managedTranscriptStoreCache = null;
|
|
8668
|
+
try {
|
|
8669
|
+
candidate.#releaseOwnedManagedAuthority();
|
|
8670
|
+
candidate.#clearBoundedManagedSource();
|
|
8671
|
+
} catch (error) {
|
|
8672
|
+
logger.warn("Failed to dispose staged rollback managed authority", { error: toError(error).message });
|
|
8673
|
+
}
|
|
8674
|
+
};
|
|
8340
8675
|
try {
|
|
8676
|
+
this.#assertSnapshotPersistenceIdentity(issued, managedTransition?.store);
|
|
8677
|
+
await candidate.#initSessionFile(issued.coldRestoreFile, false, undefined, true, true, true);
|
|
8678
|
+
await SessionManagerTestHooks.beforeColdRollbackCommit?.(issued.coldRestoreFile, this.#storage);
|
|
8679
|
+
this.#authenticateStateSnapshot(snapshot);
|
|
8680
|
+
if (
|
|
8681
|
+
lifecycle !== this.#syncArtifactLifecycle() ||
|
|
8682
|
+
this.#sessionId !== predecessorId ||
|
|
8683
|
+
this.#sessionFile !== predecessorFile
|
|
8684
|
+
)
|
|
8685
|
+
throw new Error("Session changed before rollback commit.");
|
|
8686
|
+
this.#assertSnapshotPersistenceIdentity(issued, managedTransition?.store);
|
|
8687
|
+
if (candidate.#sessionId !== issued.sessionId || candidate.#sessionFile !== issued.coldRestoreFile)
|
|
8688
|
+
throw new Error("Session rollback candidate identity changed.");
|
|
8689
|
+
await this.#closePersistWriter();
|
|
8690
|
+
this.#assertArtifactOpen();
|
|
8691
|
+
this.#authenticateStateSnapshot(snapshot);
|
|
8692
|
+
if (
|
|
8693
|
+
lifecycle !== this.#syncArtifactLifecycle() ||
|
|
8694
|
+
this.#sessionId !== predecessorId ||
|
|
8695
|
+
this.#sessionFile !== predecessorFile
|
|
8696
|
+
)
|
|
8697
|
+
throw new Error("Session changed before rollback commit.");
|
|
8698
|
+
const restoredIdentity = this.#assertSnapshotPersistenceIdentity(issued, managedTransition?.store);
|
|
8341
8699
|
managedTransition?.adopt();
|
|
8342
|
-
|
|
8343
|
-
|
|
8700
|
+
const predecessorStore = this.#residentTextBlobStore;
|
|
8701
|
+
try {
|
|
8702
|
+
this.#releaseManagedSidecarCache();
|
|
8703
|
+
} catch (error) {
|
|
8704
|
+
logger.warn("Failed to release predecessor rollback sidecar authority", { error: toError(error).message });
|
|
8705
|
+
}
|
|
8706
|
+
this.#sessionId = issued.sessionId;
|
|
8707
|
+
this.#sessionName = issued.sessionName;
|
|
8708
|
+
this.#titleSource = issued.titleSource;
|
|
8709
|
+
this.#sessionFile = issued.coldRestoreFile;
|
|
8710
|
+
this.#fileEntries = candidate.#fileEntries;
|
|
8711
|
+
this.#installRollbackCandidateResidentStore(candidate);
|
|
8712
|
+
this.#byId = candidate.#byId;
|
|
8713
|
+
this.#labelsById = candidate.#labelsById;
|
|
8714
|
+
this.#leafId = candidate.#leafId;
|
|
8715
|
+
this.#usageStatistics = candidate.#usageStatistics;
|
|
8716
|
+
this.#sidecarRuntime = candidate.#sidecarRuntime;
|
|
8717
|
+
candidate.#sidecarRuntime = undefined;
|
|
8718
|
+
this.#commitGen = candidate.#commitGen;
|
|
8719
|
+
this.#managedRangeExpectedDescriptor = candidate.#managedRangeExpectedDescriptor;
|
|
8720
|
+
this.#managedPersistExpectedIdentity = restoredIdentity;
|
|
8721
|
+
this.#flushed = issued.flushed;
|
|
8722
|
+
this.#ensuredOnDisk = issued.ensuredOnDisk;
|
|
8723
|
+
this.#needsFullRewriteOnNextPersist = issued.needsFullRewriteOnNextPersist;
|
|
8724
|
+
this.#sessionMemoryMode = candidate.#sessionMemoryMode;
|
|
8725
|
+
this.#sessionMemoryAutoDisabledReason = candidate.#sessionMemoryAutoDisabledReason;
|
|
8726
|
+
this.#lazyReopenAttempted = candidate.#lazyReopenAttempted;
|
|
8727
|
+
this.#lazyReopenSucceeded = candidate.#lazyReopenSucceeded;
|
|
8728
|
+
this.#lazyReopenFallbackReason = candidate.#lazyReopenFallbackReason;
|
|
8729
|
+
this.#boundedFirstOpenBuildSuppressed = candidate.#boundedFirstOpenBuildSuppressed;
|
|
8730
|
+
this.#consecutiveSidecarBuildFailures = candidate.#consecutiveSidecarBuildFailures;
|
|
8731
|
+
this.#retirementFallbackReason = candidate.#retirementFallbackReason;
|
|
8732
|
+
this.#sidecarBranchActivationDirty = candidate.#sidecarBranchActivationDirty;
|
|
8733
|
+
this.#firstOpenTelemetry = candidate.#firstOpenTelemetry;
|
|
8734
|
+
this.#managedSidecarAuthorityStore = candidate.#managedSidecarAuthorityStore;
|
|
8735
|
+
this.#managedSidecarSecurityContext = candidate.#managedSidecarSecurityContext;
|
|
8736
|
+
this.#managedSidecarCacheStore = candidate.#managedSidecarCacheStore;
|
|
8737
|
+
this.#managedSidecarCacheSessionFile = candidate.#managedSidecarCacheSessionFile;
|
|
8738
|
+
candidate.#managedSidecarAuthorityStore = undefined;
|
|
8739
|
+
candidate.#managedSidecarSecurityContext = undefined;
|
|
8740
|
+
candidate.#managedSidecarCacheStore = undefined;
|
|
8741
|
+
candidate.#managedSidecarCacheSessionFile = undefined;
|
|
8742
|
+
candidate.#managedRangeExpectedDescriptor = undefined;
|
|
8743
|
+
this.#boundedReadStorageProxy = undefined;
|
|
8744
|
+
this.#bumpAllRevisions();
|
|
8745
|
+
this.#residentBlobRevision++;
|
|
8746
|
+
this.#artifactManager = null;
|
|
8747
|
+
this.#artifactManagerSessionFile = null;
|
|
8748
|
+
this.#adoptedArtifactManager = issued.adoptedArtifactManager;
|
|
8749
|
+
this.#artifactLifecycle = Symbol("session-artifact-rollback");
|
|
8750
|
+
installed = true;
|
|
8751
|
+
this.#resetCloseRetryOriginForLifecycleCommit();
|
|
8752
|
+
this.#disposeResidentTextStore(predecessorStore);
|
|
8753
|
+
try {
|
|
8754
|
+
managedTransition?.settle();
|
|
8755
|
+
} catch (error) {
|
|
8756
|
+
logger.warn("Failed to settle managed authority after cold rollback", { error: toError(error).message });
|
|
8757
|
+
}
|
|
8758
|
+
try {
|
|
8759
|
+
this.#writeTerminalBreadcrumb(issued.coldRestoreFile);
|
|
8760
|
+
} catch (error) {
|
|
8761
|
+
logger.warn("Failed to update terminal breadcrumb after cold rollback", { error: toError(error).message });
|
|
8762
|
+
}
|
|
8344
8763
|
} catch (error) {
|
|
8345
|
-
managedTransition?.rollback();
|
|
8764
|
+
if (!installed) managedTransition?.rollback();
|
|
8346
8765
|
throw error;
|
|
8766
|
+
} finally {
|
|
8767
|
+
managedTransition?.dispose();
|
|
8768
|
+
disposeCandidate();
|
|
8347
8769
|
}
|
|
8348
|
-
this.#flushed = snapshot.flushed;
|
|
8349
|
-
this.#ensuredOnDisk = snapshot.ensuredOnDisk;
|
|
8350
|
-
this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
|
|
8351
|
-
this.#managedPersistExpectedIdentity = snapshot.managedPersistExpectedIdentity;
|
|
8352
|
-
this.#adoptedArtifactManager = snapshot.adoptedArtifactManager;
|
|
8353
8770
|
}
|
|
8354
8771
|
captureState(): SessionManagerStateSnapshot {
|
|
8772
|
+
this.#assertArtifactOpen();
|
|
8355
8773
|
this.#ensureFullHotView();
|
|
8356
8774
|
const materializedFileEntries = materializeResidentEntriesForReadSync(
|
|
8357
8775
|
this.#fileEntries,
|
|
8358
8776
|
this.#residentBlobStores(),
|
|
8359
8777
|
);
|
|
8360
|
-
return {
|
|
8778
|
+
return this.#issueStateSnapshot({
|
|
8361
8779
|
sessionId: this.#sessionId,
|
|
8362
8780
|
sessionName: this.#sessionName,
|
|
8363
8781
|
titleSource: this.#titleSource,
|
|
@@ -8366,54 +8784,76 @@ export class SessionManager {
|
|
|
8366
8784
|
ensuredOnDisk: this.#ensuredOnDisk,
|
|
8367
8785
|
needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
|
|
8368
8786
|
managedPersistExpectedIdentity: this.#managedPersistExpectedIdentity,
|
|
8369
|
-
|
|
8370
|
-
// so rollback does not need structured cloning of extension/custom details.
|
|
8371
|
-
fileEntries: [...this.#fileEntries],
|
|
8787
|
+
fileEntries: this.#fileEntries.map(entry => (entry.type === "session" ? { ...entry } : entry)),
|
|
8372
8788
|
// Rollback snapshots must own resident data before another session reset disposes
|
|
8373
8789
|
// the ephemeral store backing the resident sentinels above.
|
|
8374
|
-
materializedFileEntries
|
|
8790
|
+
materializedFileEntries: materializedFileEntries.map(entry =>
|
|
8791
|
+
entry.type === "session" ? { ...entry } : entry,
|
|
8792
|
+
),
|
|
8375
8793
|
adoptedArtifactManager: this.#adoptedArtifactManager,
|
|
8376
|
-
};
|
|
8794
|
+
});
|
|
8377
8795
|
}
|
|
8378
8796
|
|
|
8379
8797
|
restoreState(snapshot: SessionManagerStateSnapshot): void {
|
|
8380
|
-
|
|
8381
|
-
const
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8798
|
+
this.#assertArtifactOpen();
|
|
8799
|
+
const issued = this.#resolveAdoptedStateSnapshot(snapshot);
|
|
8800
|
+
if (issued.coldRestoreFile) throw new Error("Cold rollback requires restoreRollbackState.");
|
|
8801
|
+
const managedTransition =
|
|
8802
|
+
this.destination.kind === "managed" && issued.sessionFile
|
|
8803
|
+
? this.#prepareManagedDestinationTransition(path.resolve(path.dirname(issued.sessionFile)))
|
|
8804
|
+
: undefined;
|
|
8805
|
+
try {
|
|
8806
|
+
const restoredIdentity = this.#assertSnapshotPersistenceIdentity(issued, managedTransition?.store);
|
|
8807
|
+
const restoredFileEntries = issued.materializedFileEntries.map(entry =>
|
|
8808
|
+
entry.type === "session" ? { ...entry } : entry,
|
|
8809
|
+
);
|
|
8810
|
+
const prepared = this.#prepareResidentTextStoreTransition(
|
|
8811
|
+
{
|
|
8812
|
+
target: { sessionId: issued.sessionId, sessionFile: issued.sessionFile ?? "" },
|
|
8813
|
+
primary: {
|
|
8814
|
+
mode: "materialize",
|
|
8815
|
+
sourceEntries: restoredFileEntries,
|
|
8816
|
+
sourceStores: { textStore: null, imageStore: this.#residentImageBlobStore },
|
|
8817
|
+
},
|
|
8388
8818
|
},
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
this.#
|
|
8407
|
-
this.#
|
|
8819
|
+
"retain-and-throw",
|
|
8820
|
+
);
|
|
8821
|
+
const retainsPersistWriter =
|
|
8822
|
+
this.#persistWriter?.isOpen() === true &&
|
|
8823
|
+
this.#sessionId === issued.sessionId &&
|
|
8824
|
+
this.#sessionFile === issued.sessionFile &&
|
|
8825
|
+
this.#persistWriterPath === issued.sessionFile;
|
|
8826
|
+
if (!retainsPersistWriter) {
|
|
8827
|
+
try {
|
|
8828
|
+
this.#closePersistWriterInternalSync();
|
|
8829
|
+
} catch (error) {
|
|
8830
|
+
prepared.dispose();
|
|
8831
|
+
throw this.#recordPersistError(error);
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8834
|
+
managedTransition?.adopt();
|
|
8835
|
+
this.#sessionId = issued.sessionId;
|
|
8836
|
+
this.#sessionName = issued.sessionName;
|
|
8837
|
+
this.#titleSource = issued.titleSource;
|
|
8838
|
+
this.#sessionFile = issued.sessionFile;
|
|
8839
|
+
this.#flushed = issued.flushed;
|
|
8840
|
+
this.#ensuredOnDisk = issued.ensuredOnDisk;
|
|
8841
|
+
this.#needsFullRewriteOnNextPersist = issued.needsFullRewriteOnNextPersist;
|
|
8842
|
+
this.#managedPersistExpectedIdentity = restoredIdentity;
|
|
8843
|
+
this.#artifactManager = null;
|
|
8844
|
+
this.#artifactManagerSessionFile = null;
|
|
8845
|
+
this.#adoptedArtifactManager = issued.adoptedArtifactManager;
|
|
8846
|
+
this.#artifactLifecycle = Symbol("session-artifact-rollback");
|
|
8847
|
+
this.#commitResidentTextStoreTransition(prepared);
|
|
8848
|
+
this.#resetCloseRetryOriginForLifecycleCommit();
|
|
8849
|
+
managedTransition?.settle();
|
|
8850
|
+
if (this.#sessionFile) writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
8851
|
+
} catch (error) {
|
|
8852
|
+
managedTransition?.rollback();
|
|
8853
|
+
throw error;
|
|
8854
|
+
} finally {
|
|
8855
|
+
managedTransition?.dispose();
|
|
8408
8856
|
}
|
|
8409
|
-
this.#persistChain = Promise.resolve();
|
|
8410
|
-
this.#persistError = undefined;
|
|
8411
|
-
this.#persistErrorReported = false;
|
|
8412
|
-
this.#artifactManager = null;
|
|
8413
|
-
this.#artifactManagerSessionFile = null;
|
|
8414
|
-
this.#adoptedArtifactManager = snapshot.adoptedArtifactManager;
|
|
8415
|
-
this.#commitResidentTextStoreTransition(prepared);
|
|
8416
|
-
if (this.#sessionFile) writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
8417
8857
|
}
|
|
8418
8858
|
|
|
8419
8859
|
#freshSessionState(options?: NewSessionOptions, sessionFileOverride?: string): FreshSessionState {
|
|
@@ -8445,6 +8885,7 @@ export class SessionManager {
|
|
|
8445
8885
|
if (state.adoptsLifecycleId) this.#lifecycleIdAdopted = true;
|
|
8446
8886
|
this.#persistChain = Promise.resolve();
|
|
8447
8887
|
this.#persistError = undefined;
|
|
8888
|
+
this.#closeRetryOriginError = undefined;
|
|
8448
8889
|
this.#persistErrorReported = false;
|
|
8449
8890
|
this.#sessionId = state.sessionId;
|
|
8450
8891
|
this.#sessionName = state.header.title;
|
|
@@ -8479,7 +8920,7 @@ export class SessionManager {
|
|
|
8479
8920
|
);
|
|
8480
8921
|
}
|
|
8481
8922
|
|
|
8482
|
-
async #tryInitSessionFileFromSidecar(sessionFile: string): Promise<boolean> {
|
|
8923
|
+
async #tryInitSessionFileFromSidecar(sessionFile: string, stagedCandidate = false): Promise<boolean> {
|
|
8483
8924
|
SessionManagerTestHooks.lastSidecarInitError = undefined;
|
|
8484
8925
|
if (
|
|
8485
8926
|
(this.#sessionMemoryMode !== "enabled" && this.#sessionMemoryMode !== "auto") ||
|
|
@@ -8498,7 +8939,7 @@ export class SessionManager {
|
|
|
8498
8939
|
this.#lazyReopenSucceeded = false;
|
|
8499
8940
|
this.#lazyReopenFallbackReason = "proof_invalid";
|
|
8500
8941
|
this.#sessionFile = sessionFile;
|
|
8501
|
-
const runtime = this.#resetSidecarRuntime();
|
|
8942
|
+
const runtime = this.#resetSidecarRuntime(false, stagedCandidate);
|
|
8502
8943
|
runtime.enabled = true;
|
|
8503
8944
|
let initialized = false;
|
|
8504
8945
|
try {
|
|
@@ -9571,6 +10012,7 @@ export class SessionManager {
|
|
|
9571
10012
|
strictResume?: { inspection: ResumeInspectionSnapshot; storage: SessionStorage; reuseEntries?: boolean },
|
|
9572
10013
|
requireExisting = false,
|
|
9573
10014
|
deferPersistenceUntilAccepted = false,
|
|
10015
|
+
stagedRollbackCandidate = false,
|
|
9574
10016
|
): Promise<void> {
|
|
9575
10017
|
let strictManagedFallbackEntries: FileEntry[] | undefined;
|
|
9576
10018
|
let strictManagedFallbackMigrationApplied = false;
|
|
@@ -9602,18 +10044,25 @@ export class SessionManager {
|
|
|
9602
10044
|
boundedTranscriptAdmitted &&
|
|
9603
10045
|
this.#effectiveSessionMemoryMode(transcriptSize) === "enabled" &&
|
|
9604
10046
|
this.#storage.existsSync(`${sidecarRoot}/.session-memory.spill.commit`);
|
|
9605
|
-
if (
|
|
9606
|
-
|
|
10047
|
+
if (
|
|
10048
|
+
boundedTranscriptAdmitted &&
|
|
10049
|
+
(await this.#tryInitSessionFileFromSidecar(resolvedSessionFile, stagedRollbackCandidate))
|
|
10050
|
+
) {
|
|
10051
|
+
if (!stagedRollbackCandidate) this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
9607
10052
|
revalidateStrictResume();
|
|
9608
10053
|
return;
|
|
9609
10054
|
}
|
|
9610
10055
|
|
|
9611
|
-
if (
|
|
10056
|
+
if (
|
|
10057
|
+
!stagedRollbackCandidate &&
|
|
10058
|
+
boundedTranscriptAdmitted &&
|
|
10059
|
+
(await this.#tryBoundedFirstOpen(resolvedSessionFile))
|
|
10060
|
+
) {
|
|
9612
10061
|
if (publishedSidecarWasPresent && this.#lazyReopenAttempted && !this.#lazyReopenSucceeded) {
|
|
9613
10062
|
this.#sessionMemoryMode = "shadow";
|
|
9614
10063
|
this.#sessionMemoryAutoDisabledReason = "sidecar_reload_failures";
|
|
9615
10064
|
}
|
|
9616
|
-
this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
10065
|
+
if (!stagedRollbackCandidate) this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
9617
10066
|
revalidateStrictResume();
|
|
9618
10067
|
return;
|
|
9619
10068
|
}
|
|
@@ -9624,12 +10073,13 @@ export class SessionManager {
|
|
|
9624
10073
|
!this.#storage.existsSync(resolvedSessionFile);
|
|
9625
10074
|
if ((strictResume || requireExisting) && transcriptMissing) throw new Error("Could not open session: unstable");
|
|
9626
10075
|
if (initializeMissing && transcriptMissing) {
|
|
10076
|
+
if (stagedRollbackCandidate) throw new Error("Could not open session: unstable");
|
|
9627
10077
|
const fresh = this.#freshSessionState(undefined, resolvedSessionFile);
|
|
9628
10078
|
const prepared = this.#prepareFreshSessionTransition(fresh, "memory-fallback");
|
|
9629
10079
|
this.#applyFreshSessionMetadata(fresh);
|
|
9630
10080
|
this.#commitResidentTextStoreTransition(prepared);
|
|
9631
10081
|
this.#retireEphemeralArtifacts();
|
|
9632
|
-
this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
10082
|
+
if (!stagedRollbackCandidate) this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
9633
10083
|
await this.#rewriteFile();
|
|
9634
10084
|
this.#flushed = true;
|
|
9635
10085
|
this.#ensuredOnDisk = true;
|
|
@@ -9652,12 +10102,13 @@ export class SessionManager {
|
|
|
9652
10102
|
const entries = strictManagedFallbackEntries ?? (await loadEntriesFromFile(resolvedSessionFile, this.#storage));
|
|
9653
10103
|
revalidateStrictResume();
|
|
9654
10104
|
if (entries.length === 0) {
|
|
10105
|
+
if (stagedRollbackCandidate) throw new Error("Could not open session: unstable");
|
|
9655
10106
|
const fresh = this.#freshSessionState(undefined, resolvedSessionFile);
|
|
9656
10107
|
const prepared = this.#prepareFreshSessionTransition(fresh, "memory-fallback");
|
|
9657
10108
|
this.#applyFreshSessionMetadata(fresh);
|
|
9658
10109
|
this.#commitResidentTextStoreTransition(prepared);
|
|
9659
10110
|
this.#retireEphemeralArtifacts();
|
|
9660
|
-
this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
10111
|
+
if (!stagedRollbackCandidate) this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
9661
10112
|
await this.#rewriteFile();
|
|
9662
10113
|
this.#flushed = true;
|
|
9663
10114
|
this.#ensuredOnDisk = true;
|
|
@@ -9690,7 +10141,7 @@ export class SessionManager {
|
|
|
9690
10141
|
this.#titleSource = header?.titleSource;
|
|
9691
10142
|
this.#needsFullRewriteOnNextPersist = migrationApplied;
|
|
9692
10143
|
this.#commitResidentTextStoreTransition(prepared);
|
|
9693
|
-
this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
10144
|
+
if (!stagedRollbackCandidate) this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
9694
10145
|
this.#flushed = true;
|
|
9695
10146
|
this.#ensuredOnDisk = true;
|
|
9696
10147
|
this.#adoptManagedPersistIdentity(resolvedSessionFile);
|
|
@@ -9748,8 +10199,12 @@ export class SessionManager {
|
|
|
9748
10199
|
|
|
9749
10200
|
/** Switch to a different session file (used for resume and branching). */
|
|
9750
10201
|
async setSessionFile(sessionFile: string, options?: { deferEphemeralArtifactRetirement?: boolean }): Promise<void> {
|
|
10202
|
+
this.#assertArtifactOpen();
|
|
9751
10203
|
this.#assertRecoveryHydrationWritable();
|
|
9752
10204
|
const resolvedSessionFile = this.#storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
|
|
10205
|
+
const lifecycle = this.#syncArtifactLifecycle();
|
|
10206
|
+
const predecessorId = this.#sessionId;
|
|
10207
|
+
const predecessorFile = this.#sessionFile;
|
|
9753
10208
|
const strictAdoption = this.#pendingStrictAdoption;
|
|
9754
10209
|
let managedTransition: ManagedDestinationTransition | undefined;
|
|
9755
10210
|
if (this.destination.kind === "managed") {
|
|
@@ -9782,31 +10237,115 @@ export class SessionManager {
|
|
|
9782
10237
|
targetSize <= BOUNDED_RESUME_TRANSCRIPT_MAX_BYTES &&
|
|
9783
10238
|
this.#effectiveSessionMemoryMode(targetSize) === "enabled";
|
|
9784
10239
|
if (boundedTarget) {
|
|
10240
|
+
const expectedIdentity =
|
|
10241
|
+
this.destination.kind === "managed"
|
|
10242
|
+
? this.#captureManagedPersistIdentity(resolvedSessionFile, managedTransition?.store)
|
|
10243
|
+
: undefined;
|
|
10244
|
+
const expectedStat = this.#storage.statSync(resolvedSessionFile);
|
|
9785
10245
|
const previous = await this.captureRollbackState();
|
|
10246
|
+
let installedGeneration: symbol | undefined;
|
|
10247
|
+
let installedSessionId: string | undefined;
|
|
10248
|
+
let installedSessionFile: string | undefined;
|
|
9786
10249
|
try {
|
|
9787
10250
|
await this.#closePersistWriter();
|
|
10251
|
+
await SessionManagerTestHooks.beforeManagedSwitchIdentity?.(resolvedSessionFile, this.#storage);
|
|
10252
|
+
this.#assertArtifactOpen();
|
|
10253
|
+
if (
|
|
10254
|
+
lifecycle !== this.#syncArtifactLifecycle() ||
|
|
10255
|
+
predecessorId !== this.#sessionId ||
|
|
10256
|
+
predecessorFile !== this.#sessionFile
|
|
10257
|
+
)
|
|
10258
|
+
throw new Error("Session changed before adoption commit.");
|
|
10259
|
+
if (!util.isDeepStrictEqual(expectedStat, this.#storage.statSync(resolvedSessionFile)))
|
|
10260
|
+
throw new Error("Prepared session changed before adoption commit.");
|
|
10261
|
+
if (
|
|
10262
|
+
expectedIdentity &&
|
|
10263
|
+
!util.isDeepStrictEqual(
|
|
10264
|
+
expectedIdentity,
|
|
10265
|
+
this.#captureManagedPersistIdentity(resolvedSessionFile, managedTransition?.store),
|
|
10266
|
+
)
|
|
10267
|
+
)
|
|
10268
|
+
throw new Error("Prepared session changed before adoption commit.");
|
|
9788
10269
|
managedTransition?.adopt();
|
|
9789
|
-
await this.#initSessionFile(resolvedSessionFile);
|
|
10270
|
+
await this.#initSessionFile(resolvedSessionFile, false, undefined, true, true);
|
|
10271
|
+
if (this.#artifactLifecycle === lifecycle && this.#sessionFile === resolvedSessionFile) {
|
|
10272
|
+
installedGeneration = this.#artifactLifecycle;
|
|
10273
|
+
installedSessionId = this.#sessionId;
|
|
10274
|
+
installedSessionFile = this.#sessionFile;
|
|
10275
|
+
}
|
|
10276
|
+
this.#assertArtifactOpen();
|
|
10277
|
+
if (this.#artifactLifecycle !== lifecycle || this.#sessionFile !== resolvedSessionFile)
|
|
10278
|
+
throw new Error("Session changed before adoption commit.");
|
|
10279
|
+
if (expectedIdentity) {
|
|
10280
|
+
if (
|
|
10281
|
+
!util.isDeepStrictEqual(
|
|
10282
|
+
expectedIdentity,
|
|
10283
|
+
this.#captureManagedPersistIdentity(resolvedSessionFile),
|
|
10284
|
+
)
|
|
10285
|
+
)
|
|
10286
|
+
throw new Error("Prepared session changed before adoption commit.");
|
|
10287
|
+
} else if (!util.isDeepStrictEqual(expectedStat, this.#storage.statSync(resolvedSessionFile))) {
|
|
10288
|
+
throw new Error("Prepared session changed before adoption commit.");
|
|
10289
|
+
}
|
|
10290
|
+
this.#artifactManager = null;
|
|
10291
|
+
this.#artifactManagerSessionFile = null;
|
|
10292
|
+
this.#artifactLifecycle = Symbol("session-artifact-adoption");
|
|
10293
|
+
installedGeneration = this.#artifactLifecycle;
|
|
10294
|
+
installedSessionId = this.#sessionId;
|
|
10295
|
+
installedSessionFile = this.#sessionFile;
|
|
10296
|
+
this.#persistError = undefined;
|
|
10297
|
+
this.#closeRetryOriginError = undefined;
|
|
10298
|
+
this.#persistErrorReported = false;
|
|
9790
10299
|
if (!options?.deferEphemeralArtifactRetirement) this.#retireEphemeralArtifacts();
|
|
9791
|
-
managedTransition?.settle();
|
|
9792
10300
|
this.#pendingStrictAdoption = undefined;
|
|
10301
|
+
try {
|
|
10302
|
+
managedTransition?.settle();
|
|
10303
|
+
} catch (settleError) {
|
|
10304
|
+
logger.warn("Failed to settle managed authority after bounded adoption", {
|
|
10305
|
+
error: toError(settleError).message,
|
|
10306
|
+
});
|
|
10307
|
+
}
|
|
9793
10308
|
return;
|
|
9794
10309
|
} catch (error) {
|
|
9795
|
-
|
|
9796
|
-
|
|
10310
|
+
const ownsInstalledGeneration =
|
|
10311
|
+
installedGeneration !== undefined &&
|
|
10312
|
+
this.#artifactLifecycle === installedGeneration &&
|
|
10313
|
+
this.#sessionId === installedSessionId &&
|
|
10314
|
+
this.#sessionFile === installedSessionFile;
|
|
10315
|
+
const ownsPredecessorGeneration =
|
|
10316
|
+
this.#artifactLifecycle === lifecycle &&
|
|
10317
|
+
this.#sessionId === predecessorId &&
|
|
10318
|
+
this.#sessionFile === predecessorFile;
|
|
10319
|
+
if (ownsInstalledGeneration) {
|
|
10320
|
+
managedTransition?.rollback();
|
|
10321
|
+
await this.restoreRollbackState(previous);
|
|
10322
|
+
} else if (ownsPredecessorGeneration) {
|
|
10323
|
+
managedTransition?.rollback();
|
|
10324
|
+
} else {
|
|
10325
|
+
try {
|
|
10326
|
+
managedTransition?.settle();
|
|
10327
|
+
} catch (settleError) {
|
|
10328
|
+
logger.warn("Failed to settle superseded managed authority", {
|
|
10329
|
+
error: toError(settleError).message,
|
|
10330
|
+
});
|
|
10331
|
+
}
|
|
10332
|
+
}
|
|
9797
10333
|
throw error;
|
|
9798
10334
|
}
|
|
9799
10335
|
}
|
|
9800
10336
|
}
|
|
9801
10337
|
let entries: FileEntry[];
|
|
10338
|
+
let candidateIdentity: ResumeSessionIdentity | undefined;
|
|
9802
10339
|
let candidateMigrationApplied = false;
|
|
9803
10340
|
if (strictAdoption?.inspection && resolvedSessionFile === strictAdoption.canonicalPath) {
|
|
9804
10341
|
entries = strictAdoption.inspection.entries;
|
|
10342
|
+
candidateIdentity = strictAdoption.identity;
|
|
9805
10343
|
candidateMigrationApplied = strictAdoption.inspection.migrationApplied;
|
|
9806
10344
|
} else if (this.#storage.existsSync(resolvedSessionFile)) {
|
|
9807
10345
|
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.#storage);
|
|
9808
10346
|
if ("kind" in inspected) throw new Error(`Could not switch session: ${inspected.reason}`);
|
|
9809
10347
|
entries = inspected.entries;
|
|
10348
|
+
candidateIdentity = inspected.identity;
|
|
9810
10349
|
candidateMigrationApplied = inspected.migrationApplied;
|
|
9811
10350
|
} else {
|
|
9812
10351
|
entries = await loadEntriesFromFile(resolvedSessionFile, this.#storage);
|
|
@@ -9839,45 +10378,43 @@ export class SessionManager {
|
|
|
9839
10378
|
prepared.dispose();
|
|
9840
10379
|
throw error;
|
|
9841
10380
|
}
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
|
|
9846
|
-
|
|
10381
|
+
let expectedIdentity: ManagedFileIdentity | undefined;
|
|
10382
|
+
try {
|
|
10383
|
+
await SessionManagerTestHooks.beforeManagedSwitchIdentity?.(resolvedSessionFile, this.#storage);
|
|
10384
|
+
this.#assertArtifactOpen();
|
|
10385
|
+
if (
|
|
10386
|
+
lifecycle !== this.#syncArtifactLifecycle() ||
|
|
10387
|
+
predecessorId !== this.#sessionId ||
|
|
10388
|
+
predecessorFile !== this.#sessionFile
|
|
10389
|
+
)
|
|
10390
|
+
throw new Error("Session changed before adoption commit.");
|
|
10391
|
+
const expected = strictAdoption?.identity ?? candidateIdentity;
|
|
10392
|
+
if (expected) {
|
|
10393
|
+
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.#storage);
|
|
10394
|
+
if ("kind" in inspected || !sameResumeIdentity(expected, inspected.identity))
|
|
10395
|
+
throw new Error("Prepared session changed before adoption commit.");
|
|
9847
10396
|
}
|
|
10397
|
+
if (this.destination.kind === "managed")
|
|
10398
|
+
expectedIdentity = this.#captureManagedPersistIdentity(resolvedSessionFile, managedTransition?.store);
|
|
10399
|
+
writeTerminalBreadcrumb(this.cwd, resolvedSessionFile);
|
|
10400
|
+
} catch (error) {
|
|
10401
|
+
prepared.dispose();
|
|
10402
|
+
throw error;
|
|
9848
10403
|
}
|
|
9849
|
-
|
|
9850
|
-
sessionId: this.#sessionId,
|
|
9851
|
-
sessionName: this.#sessionName,
|
|
9852
|
-
titleSource: this.#titleSource,
|
|
9853
|
-
sessionFile: this.#sessionFile,
|
|
9854
|
-
needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
|
|
9855
|
-
managedPersistExpectedIdentity: this.#managedPersistExpectedIdentity,
|
|
9856
|
-
};
|
|
9857
|
-
this.#persistError = undefined;
|
|
9858
|
-
this.#persistErrorReported = false;
|
|
10404
|
+
managedTransition?.adopt();
|
|
9859
10405
|
this.#sessionFile = resolvedSessionFile;
|
|
9860
10406
|
this.#sessionId = sessionId;
|
|
9861
10407
|
this.#sessionName = header?.title;
|
|
9862
10408
|
this.#titleSource = header?.titleSource;
|
|
9863
10409
|
this.#needsFullRewriteOnNextPersist = migrationApplied;
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
this.#sessionId = previous.sessionId;
|
|
9873
|
-
this.#sessionName = previous.sessionName;
|
|
9874
|
-
this.#titleSource = previous.titleSource;
|
|
9875
|
-
this.#sessionFile = previous.sessionFile;
|
|
9876
|
-
this.#needsFullRewriteOnNextPersist = previous.needsFullRewriteOnNextPersist;
|
|
9877
|
-
this.#managedPersistExpectedIdentity = previous.managedPersistExpectedIdentity;
|
|
9878
|
-
prepared.dispose();
|
|
9879
|
-
throw error;
|
|
9880
|
-
}
|
|
10410
|
+
this.#managedPersistExpectedIdentity = expectedIdentity;
|
|
10411
|
+
this.#artifactManager = null;
|
|
10412
|
+
this.#artifactManagerSessionFile = null;
|
|
10413
|
+
this.#artifactLifecycle = Symbol("session-artifact-adoption");
|
|
10414
|
+
this.#commitResidentTextStoreTransition(prepared);
|
|
10415
|
+
this.#persistError = undefined;
|
|
10416
|
+
this.#closeRetryOriginError = undefined;
|
|
10417
|
+
this.#persistErrorReported = false;
|
|
9881
10418
|
entries.length = 0;
|
|
9882
10419
|
if (!options?.deferEphemeralArtifactRetirement) this.#retireEphemeralArtifacts();
|
|
9883
10420
|
managedTransition?.settle();
|
|
@@ -9891,6 +10428,15 @@ export class SessionManager {
|
|
|
9891
10428
|
const prepared = this.#prepareFreshSessionTransition(fresh, "retain-and-throw");
|
|
9892
10429
|
try {
|
|
9893
10430
|
await this.#closePersistWriter();
|
|
10431
|
+
this.#assertArtifactOpen();
|
|
10432
|
+
if (
|
|
10433
|
+
lifecycle !== this.#syncArtifactLifecycle() ||
|
|
10434
|
+
predecessorId !== this.#sessionId ||
|
|
10435
|
+
predecessorFile !== this.#sessionFile
|
|
10436
|
+
)
|
|
10437
|
+
throw new Error("Session changed before adoption commit.");
|
|
10438
|
+
if (this.#storage.existsSync(resolvedSessionFile))
|
|
10439
|
+
throw new Error("Prepared missing session changed before adoption commit.");
|
|
9894
10440
|
} catch (error) {
|
|
9895
10441
|
prepared.dispose();
|
|
9896
10442
|
throw error;
|
|
@@ -9899,6 +10445,7 @@ export class SessionManager {
|
|
|
9899
10445
|
lifecycleIdAdopted: this.#lifecycleIdAdopted,
|
|
9900
10446
|
persistChain: this.#persistChain,
|
|
9901
10447
|
persistError: this.#persistError,
|
|
10448
|
+
closeRetryOriginError: this.#closeRetryOriginError,
|
|
9902
10449
|
persistErrorReported: this.#persistErrorReported,
|
|
9903
10450
|
sessionId: this.#sessionId,
|
|
9904
10451
|
sessionName: this.#sessionName,
|
|
@@ -9917,12 +10464,16 @@ export class SessionManager {
|
|
|
9917
10464
|
managedTransition?.adopt();
|
|
9918
10465
|
writeTerminalBreadcrumb(this.cwd, resolvedSessionFile);
|
|
9919
10466
|
this.#commitResidentTextStoreTransition(prepared);
|
|
10467
|
+
// A session replacement starts a new writer lifecycle; never let a
|
|
10468
|
+
// certified close error from the predecessor authorize clearing its state.
|
|
10469
|
+
this.#closeRetryOriginError = undefined;
|
|
9920
10470
|
if (!options?.deferEphemeralArtifactRetirement) this.#retireEphemeralArtifacts();
|
|
9921
10471
|
} catch (error) {
|
|
9922
10472
|
managedTransition?.rollback();
|
|
9923
10473
|
this.#lifecycleIdAdopted = previous.lifecycleIdAdopted;
|
|
9924
10474
|
this.#persistChain = previous.persistChain;
|
|
9925
10475
|
this.#persistError = previous.persistError;
|
|
10476
|
+
this.#closeRetryOriginError = previous.closeRetryOriginError;
|
|
9926
10477
|
this.#persistErrorReported = previous.persistErrorReported;
|
|
9927
10478
|
this.#sessionId = previous.sessionId;
|
|
9928
10479
|
this.#sessionName = previous.sessionName;
|
|
@@ -10372,6 +10923,14 @@ export class SessionManager {
|
|
|
10372
10923
|
transition.dispose();
|
|
10373
10924
|
throw error;
|
|
10374
10925
|
}
|
|
10926
|
+
let expectedIdentity: ManagedFileIdentity | undefined;
|
|
10927
|
+
try {
|
|
10928
|
+
if (stage.flushed && stage.sessionFile && this.destination.kind === "managed")
|
|
10929
|
+
expectedIdentity = this.#captureManagedPersistIdentity(stage.sessionFile);
|
|
10930
|
+
} catch (error) {
|
|
10931
|
+
transition.dispose();
|
|
10932
|
+
throw error;
|
|
10933
|
+
}
|
|
10375
10934
|
const predecessorRuntime = this.#sidecarRuntime;
|
|
10376
10935
|
if (predecessorRuntime) this.#clearColdRuntimeAfterHydration(predecessorRuntime);
|
|
10377
10936
|
this.#sidecarRuntime = undefined;
|
|
@@ -10382,6 +10941,7 @@ export class SessionManager {
|
|
|
10382
10941
|
this.#lifecycleIdAdopted = true;
|
|
10383
10942
|
this.#persistChain = Promise.resolve();
|
|
10384
10943
|
this.#persistError = undefined;
|
|
10944
|
+
this.#closeRetryOriginError = undefined;
|
|
10385
10945
|
this.#persistErrorReported = false;
|
|
10386
10946
|
this.#sessionId = stage.sessionId;
|
|
10387
10947
|
this.#sessionName = stage.sessionName;
|
|
@@ -10396,8 +10956,8 @@ export class SessionManager {
|
|
|
10396
10956
|
this.#artifactManager = null;
|
|
10397
10957
|
this.#artifactManagerSessionFile = null;
|
|
10398
10958
|
this.#adoptedArtifactManager = null;
|
|
10959
|
+
this.#managedPersistExpectedIdentity = expectedIdentity;
|
|
10399
10960
|
this.#commitResidentTextStoreTransition(transition);
|
|
10400
|
-
if (stage.flushed) this.#adoptManagedPersistIdentity(stage.sessionFile);
|
|
10401
10961
|
this.#retireEphemeralArtifacts();
|
|
10402
10962
|
stage.committed = true;
|
|
10403
10963
|
this.#preparedNewSessions.delete(stage);
|
|
@@ -10607,6 +11167,7 @@ export class SessionManager {
|
|
|
10607
11167
|
await this.#closePersistWriter();
|
|
10608
11168
|
this.#persistChain = Promise.resolve();
|
|
10609
11169
|
this.#persistError = undefined;
|
|
11170
|
+
this.#closeRetryOriginError = undefined;
|
|
10610
11171
|
this.#persistErrorReported = false;
|
|
10611
11172
|
let forkArtifactPublication: ForkArtifactPublication | undefined;
|
|
10612
11173
|
let forkTranscriptPublication: ForkTranscriptPublication | undefined;
|
|
@@ -11480,6 +12041,7 @@ export class SessionManager {
|
|
|
11480
12041
|
const previousSessionDir = this.sessionDir;
|
|
11481
12042
|
const previousSessionFile = this.#sessionFile;
|
|
11482
12043
|
const previousDestination = this.destination;
|
|
12044
|
+
const previousPersistIdentity = this.#managedPersistExpectedIdentity;
|
|
11483
12045
|
|
|
11484
12046
|
const nextDestination =
|
|
11485
12047
|
this.#storage instanceof FileSessionStorage
|
|
@@ -11529,6 +12091,7 @@ export class SessionManager {
|
|
|
11529
12091
|
}
|
|
11530
12092
|
this.#persistChain = Promise.resolve();
|
|
11531
12093
|
this.#persistError = undefined;
|
|
12094
|
+
this.#closeRetryOriginError = undefined;
|
|
11532
12095
|
this.#persistErrorReported = false;
|
|
11533
12096
|
|
|
11534
12097
|
const oldSessionFile = this.#sessionFile;
|
|
@@ -11801,6 +12364,7 @@ export class SessionManager {
|
|
|
11801
12364
|
throw error;
|
|
11802
12365
|
}
|
|
11803
12366
|
}
|
|
12367
|
+
this.#artifactLifecycle = Symbol("session-artifact-move");
|
|
11804
12368
|
this.#cwdGeneration += 1;
|
|
11805
12369
|
const readLease = cwdReadLeaseAls.getStore();
|
|
11806
12370
|
if (readLease?.owner === this.#cwdReadLeaseOwner) {
|
|
@@ -11839,6 +12403,7 @@ export class SessionManager {
|
|
|
11839
12403
|
await this.#closePersistWriter().catch(() => {});
|
|
11840
12404
|
this.#persistChain = Promise.resolve();
|
|
11841
12405
|
this.#persistError = undefined;
|
|
12406
|
+
this.#closeRetryOriginError = undefined;
|
|
11842
12407
|
this.#persistErrorReported = false;
|
|
11843
12408
|
if (rollbackManagedMove) {
|
|
11844
12409
|
try {
|
|
@@ -11863,6 +12428,14 @@ export class SessionManager {
|
|
|
11863
12428
|
// transcript actually existed (and was thus restorable) before the failed move.
|
|
11864
12429
|
if (hadSessionFile) this.#adoptManagedPersistIdentity(previousSessionFile);
|
|
11865
12430
|
else this.#managedPersistExpectedIdentity = undefined;
|
|
12431
|
+
if (previousSessionFile && previousPersistIdentity && this.#managedPersistExpectedIdentity) {
|
|
12432
|
+
this.#managedRollbackIdentity = Object.freeze({
|
|
12433
|
+
sessionId: this.#sessionId,
|
|
12434
|
+
sessionFile: previousSessionFile,
|
|
12435
|
+
before: Object.freeze({ ...previousPersistIdentity }),
|
|
12436
|
+
after: Object.freeze({ ...this.#managedPersistExpectedIdentity }),
|
|
12437
|
+
});
|
|
12438
|
+
}
|
|
11866
12439
|
const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
11867
12440
|
if (header) applyHeaderPatch(header, { cwd: previousCwd });
|
|
11868
12441
|
this.#headerExportRevision++;
|
|
@@ -12037,6 +12610,15 @@ export class SessionManager {
|
|
|
12037
12610
|
}
|
|
12038
12611
|
return normalized;
|
|
12039
12612
|
}
|
|
12613
|
+
/** A successful lifecycle commit replaces the writer/session that certified this retry origin. */
|
|
12614
|
+
#resetCloseRetryOriginForLifecycleCommit(): void {
|
|
12615
|
+
const closeRetryOriginError = this.#closeRetryOriginError;
|
|
12616
|
+
this.#closeRetryOriginError = undefined;
|
|
12617
|
+
if (closeRetryOriginError && this.#persistError === closeRetryOriginError) {
|
|
12618
|
+
this.#persistError = undefined;
|
|
12619
|
+
this.#persistErrorReported = false;
|
|
12620
|
+
}
|
|
12621
|
+
}
|
|
12040
12622
|
|
|
12041
12623
|
#queuePersistTask(task: () => Promise<void>, options?: { ignoreError?: boolean }): Promise<void> {
|
|
12042
12624
|
const next = this.#persistChain.then(async () => {
|
|
@@ -12123,7 +12705,19 @@ export class SessionManager {
|
|
|
12123
12705
|
|
|
12124
12706
|
async #closePersistWriterInternal(): Promise<void> {
|
|
12125
12707
|
if (this.#persistWriter) {
|
|
12126
|
-
|
|
12708
|
+
const writer = this.#persistWriter;
|
|
12709
|
+
try {
|
|
12710
|
+
await writer.close();
|
|
12711
|
+
} catch (error) {
|
|
12712
|
+
if (writer.getCloseState() === "close_failed_retryable" && !this.#closeRetryOriginError) {
|
|
12713
|
+
const closeError = writer.getCloseError() ?? toError(error);
|
|
12714
|
+
this.#closeRetryOriginError = closeError;
|
|
12715
|
+
// Record the error immediately so the identity check in close() will pass
|
|
12716
|
+
// when the retry succeeds, even if toError() is called again later.
|
|
12717
|
+
if (!this.#persistError) this.#persistError = closeError;
|
|
12718
|
+
}
|
|
12719
|
+
throw error;
|
|
12720
|
+
}
|
|
12127
12721
|
this.#persistWriter = undefined;
|
|
12128
12722
|
}
|
|
12129
12723
|
this.#persistWriterPath = undefined;
|
|
@@ -12670,9 +13264,9 @@ export class SessionManager {
|
|
|
12670
13264
|
// =========================================================================
|
|
12671
13265
|
|
|
12672
13266
|
/** Create (or reset) the sidecar runtime for the current lifecycle. */
|
|
12673
|
-
#resetSidecarRuntime(ineligible = false): SessionMemorySidecarRuntime {
|
|
13267
|
+
#resetSidecarRuntime(ineligible = false, preserveLegacySidecars = false): SessionMemorySidecarRuntime {
|
|
12674
13268
|
this.#sidecarBranchActivationDirty = false;
|
|
12675
|
-
if (this.#sessionFile && this.destination.kind !== "managed") {
|
|
13269
|
+
if (!preserveLegacySidecars && this.#sessionFile && this.destination.kind !== "managed") {
|
|
12676
13270
|
for (const legacyPath of [
|
|
12677
13271
|
`${this.#sessionFile}.spill.idx`,
|
|
12678
13272
|
`${this.#sessionFile}.spill.tail`,
|
|
@@ -15708,9 +16302,92 @@ export class SessionManager {
|
|
|
15708
16302
|
this.#managedPersistExpectedIdentity = this.#captureManagedPersistIdentity(sessionFile);
|
|
15709
16303
|
}
|
|
15710
16304
|
|
|
16305
|
+
#captureExplicitPersistIdentity(sessionFile: string): ExplicitPersistIdentity {
|
|
16306
|
+
const storage = this.#storage;
|
|
16307
|
+
if (!(storage instanceof FileSessionStorage) || !storage.readRangeSync)
|
|
16308
|
+
throw new Error("Session rollback persistence identity is unavailable.");
|
|
16309
|
+
const canonicalPath = resolveEquivalentPath(path.resolve(sessionFile));
|
|
16310
|
+
let before: SessionStorageStat;
|
|
16311
|
+
try {
|
|
16312
|
+
before = storage.statSync(canonicalPath);
|
|
16313
|
+
} catch {
|
|
16314
|
+
throw new Error("Session rollback persistence identity is unavailable.");
|
|
16315
|
+
}
|
|
16316
|
+
const nlink = before.nlink;
|
|
16317
|
+
if (
|
|
16318
|
+
!before.isFile ||
|
|
16319
|
+
typeof nlink !== "bigint" ||
|
|
16320
|
+
nlink !== 1n ||
|
|
16321
|
+
before.size <= 0 ||
|
|
16322
|
+
before.size > BOUNDED_RESUME_TRANSCRIPT_MAX_BYTES
|
|
16323
|
+
)
|
|
16324
|
+
throw new Error("Session rollback persistence identity is unavailable.");
|
|
16325
|
+
|
|
16326
|
+
const fingerprint = crypto.createHash("sha256");
|
|
16327
|
+
const addRange = (start: number, length: number): Uint8Array => {
|
|
16328
|
+
const range = storage.readRangeSync!(canonicalPath, start, length);
|
|
16329
|
+
if (range.bytes.byteLength !== length || !sameResumeStat(before, range.stat))
|
|
16330
|
+
throw new Error("Session rollback persistence identity is unavailable.");
|
|
16331
|
+
const frame = Buffer.allocUnsafe(12);
|
|
16332
|
+
frame.writeBigUInt64BE(BigInt(start), 0);
|
|
16333
|
+
frame.writeUInt32BE(length, 8);
|
|
16334
|
+
fingerprint.update(frame).update(range.bytes);
|
|
16335
|
+
return range.bytes;
|
|
16336
|
+
};
|
|
16337
|
+
let sessionId: string | undefined;
|
|
16338
|
+
try {
|
|
16339
|
+
const prefixLength = Math.min(before.size, BOUNDED_FIRST_OPEN_MAX_LINE_BYTES + 1);
|
|
16340
|
+
const prefix = addRange(0, prefixLength);
|
|
16341
|
+
const consumer = createBoundedLineChunkConsumer((_lineStart, lineBytes) => {
|
|
16342
|
+
if (lineBytes.byteLength === 1 && lineBytes[0] === 0x0a) return;
|
|
16343
|
+
if (lineBytes.at(-1) !== 0x0a && prefixLength !== before.size) return false;
|
|
16344
|
+
try {
|
|
16345
|
+
const header = JSON.parse(decodeBoundedJsonLine(lineBytes)) as Partial<SessionHeader>;
|
|
16346
|
+
if (header.type !== "session" || typeof header.id !== "string") return false;
|
|
16347
|
+
sessionId = header.id;
|
|
16348
|
+
return false;
|
|
16349
|
+
} catch {
|
|
16350
|
+
return false;
|
|
16351
|
+
}
|
|
16352
|
+
});
|
|
16353
|
+
const consumeFailure = consumer.consume(Buffer.from(prefix.buffer, prefix.byteOffset, prefix.byteLength), 0);
|
|
16354
|
+
const prefixFailure = consumeFailure ?? consumer.finish(prefixLength === before.size);
|
|
16355
|
+
if (prefixFailure !== "aborted" || sessionId === undefined) throw new Error("invalid_session_header");
|
|
16356
|
+
|
|
16357
|
+
const rangeLength = Math.min(EXPLICIT_PERSIST_FINGERPRINT_RANGE_BYTES, before.size);
|
|
16358
|
+
const lastStart = before.size - rangeLength;
|
|
16359
|
+
const starts = new Set([Math.floor(lastStart / 3), Math.floor((lastStart * 2) / 3), lastStart]);
|
|
16360
|
+
for (const start of starts) {
|
|
16361
|
+
if (start + rangeLength <= prefixLength) continue;
|
|
16362
|
+
addRange(start, rangeLength);
|
|
16363
|
+
}
|
|
16364
|
+
const after = storage.statSync(canonicalPath);
|
|
16365
|
+
if (!sameResumeStat(before, after)) throw new Error("source_changed");
|
|
16366
|
+
} catch {
|
|
16367
|
+
throw new Error("Session rollback persistence identity is unavailable.");
|
|
16368
|
+
}
|
|
16369
|
+
if (sessionId === undefined) throw new Error("Session rollback persistence identity is unavailable.");
|
|
16370
|
+
return {
|
|
16371
|
+
canonicalPath,
|
|
16372
|
+
sessionId,
|
|
16373
|
+
// Convert bigints to strings for JSON safety
|
|
16374
|
+
dev: String(before.dev),
|
|
16375
|
+
ino: String(before.ino),
|
|
16376
|
+
nlink: nlink !== undefined ? String(nlink) : undefined,
|
|
16377
|
+
size: before.size,
|
|
16378
|
+
mtimeMs: before.mtimeMs,
|
|
16379
|
+
mtimeNs: String(before.mtimeNs),
|
|
16380
|
+
ctimeNs: before.ctimeNs !== undefined ? String(before.ctimeNs) : undefined,
|
|
16381
|
+
fingerprintSha256: fingerprint.digest("hex"),
|
|
16382
|
+
};
|
|
16383
|
+
}
|
|
16384
|
+
|
|
15711
16385
|
/** Capture one descriptor-bound digest for a future metadata-drift comparison. */
|
|
15712
|
-
#captureManagedPersistIdentity(
|
|
15713
|
-
|
|
16386
|
+
#captureManagedPersistIdentity(
|
|
16387
|
+
sessionFile: string,
|
|
16388
|
+
candidateStore?: ManagedSessionDescendantStore,
|
|
16389
|
+
): ManagedFileIdentity {
|
|
16390
|
+
const store = candidateStore ?? this.#managedTranscriptStore(sessionFile);
|
|
15714
16391
|
const relativePath = path.basename(sessionFile);
|
|
15715
16392
|
const bounded = store.captureBoundedAppendExpectation(relativePath);
|
|
15716
16393
|
const descriptor = store.descriptorExpected(relativePath);
|
|
@@ -16371,6 +17048,7 @@ export class SessionManager {
|
|
|
16371
17048
|
|
|
16372
17049
|
/** Close the persistent writer after flushing all pending data. */
|
|
16373
17050
|
async close(): Promise<void> {
|
|
17051
|
+
this.#beginArtifactClose();
|
|
16374
17052
|
await this.closeCwdMoveAdmission();
|
|
16375
17053
|
await this.joinCwdTransition();
|
|
16376
17054
|
await this.joinCwdReaders();
|
|
@@ -16386,6 +17064,7 @@ export class SessionManager {
|
|
|
16386
17064
|
}
|
|
16387
17065
|
let closeError: unknown;
|
|
16388
17066
|
let taskStarted = false;
|
|
17067
|
+
const closeRetryOriginError = this.#closeRetryOriginError;
|
|
16389
17068
|
try {
|
|
16390
17069
|
await this.#queuePersistTask(
|
|
16391
17070
|
async () => {
|
|
@@ -16398,8 +17077,19 @@ export class SessionManager {
|
|
|
16398
17077
|
},
|
|
16399
17078
|
{ ignoreError: this.#closeRetryPending },
|
|
16400
17079
|
);
|
|
16401
|
-
|
|
16402
|
-
|
|
17080
|
+
if (
|
|
17081
|
+
closeRetryOriginError &&
|
|
17082
|
+
!this.#persistWriter &&
|
|
17083
|
+
!this.#needsFullRewriteOnNextPersist &&
|
|
17084
|
+
!this.#strictResumeMutationPending &&
|
|
17085
|
+
this.#persistError === closeRetryOriginError
|
|
17086
|
+
) {
|
|
17087
|
+
// Only the original certified writer-close failure becomes obsolete
|
|
17088
|
+
// after closure. Lifecycle/publication errors must remain observable.
|
|
17089
|
+
this.#persistError = undefined;
|
|
17090
|
+
this.#persistErrorReported = false;
|
|
17091
|
+
this.#closeRetryOriginError = undefined;
|
|
17092
|
+
}
|
|
16403
17093
|
this.#closeRetryPending = false;
|
|
16404
17094
|
this.#retireEphemeralArtifacts();
|
|
16405
17095
|
await this.#drainEphemeralArtifactCleanups();
|
|
@@ -16409,6 +17099,8 @@ export class SessionManager {
|
|
|
16409
17099
|
}
|
|
16410
17100
|
const terminalError = closeError ?? this.#persistError;
|
|
16411
17101
|
if (terminalError) throw terminalError;
|
|
17102
|
+
this.#persistError = undefined;
|
|
17103
|
+
this.#persistErrorReported = false;
|
|
16412
17104
|
this.#releaseResidentTextStore();
|
|
16413
17105
|
if (this.#preparedNewSessions.size === 0) this.#releaseOwnedManagedAuthority();
|
|
16414
17106
|
this.#releaseClosedSessionState();
|
|
@@ -16438,6 +17130,7 @@ export class SessionManager {
|
|
|
16438
17130
|
* writer closure before any destructive operation.
|
|
16439
17131
|
*/
|
|
16440
17132
|
async closeStrict(): Promise<SessionManagerCloseOutcome> {
|
|
17133
|
+
this.#beginArtifactClose();
|
|
16441
17134
|
this.#strictClosePending = true;
|
|
16442
17135
|
await this.closeCwdMoveAdmission();
|
|
16443
17136
|
await this.joinCwdTransition();
|
|
@@ -16466,6 +17159,7 @@ export class SessionManager {
|
|
|
16466
17159
|
this.#strictResumeMutationPending = false;
|
|
16467
17160
|
this.#managedPersistExpectedIdentity = undefined;
|
|
16468
17161
|
this.#persistError = undefined;
|
|
17162
|
+
this.#closeRetryOriginError = undefined;
|
|
16469
17163
|
this.#persistErrorReported = false;
|
|
16470
17164
|
}
|
|
16471
17165
|
let priorPersistError = this.#persistError;
|
|
@@ -17077,27 +17771,72 @@ export class SessionManager {
|
|
|
17077
17771
|
* the parent session's artifact directory and ID counter.
|
|
17078
17772
|
*/
|
|
17079
17773
|
adoptArtifactManager(manager: ArtifactManager, parent?: ArtifactManager): void {
|
|
17774
|
+
this.#assertArtifactOpen();
|
|
17775
|
+
if (this.#adoptedArtifactManager !== manager) this.#artifactLifecycle = Symbol("artifact-adoption");
|
|
17080
17776
|
this.#adoptedArtifactManager = manager;
|
|
17081
17777
|
if (parent) this.#stagedArtifactParent = parent;
|
|
17082
17778
|
}
|
|
17083
17779
|
|
|
17084
17780
|
/** Release only the matching externally adopted manager. */
|
|
17085
17781
|
releaseArtifactManager(manager: ArtifactManager): void {
|
|
17086
|
-
if (this.#adoptedArtifactManager === manager)
|
|
17782
|
+
if (this.#adoptedArtifactManager === manager) {
|
|
17783
|
+
this.#adoptedArtifactManager = null;
|
|
17784
|
+
this.#artifactLifecycle = Symbol("artifact-release");
|
|
17785
|
+
}
|
|
17087
17786
|
}
|
|
17088
17787
|
|
|
17089
17788
|
/** Temporarily release adopted authority while an outer transition validates its successor. */
|
|
17090
17789
|
stageAdoptedArtifactManagerForTransition(): void {
|
|
17091
|
-
this.#adoptedArtifactManager
|
|
17790
|
+
if (this.#adoptedArtifactManager) this.releaseArtifactManager(this.#adoptedArtifactManager);
|
|
17092
17791
|
}
|
|
17093
17792
|
|
|
17094
17793
|
/** Prove manager authority by exact object identity, never by pathname shape. */
|
|
17095
17794
|
isArtifactManagerAuthorized(manager: ArtifactManager): boolean {
|
|
17096
|
-
return
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
manager === this.#
|
|
17100
|
-
|
|
17795
|
+
if (this.#artifactClosing || this.#strictClosePending) return false;
|
|
17796
|
+
if (this.#adoptedArtifactManager) return manager === this.#adoptedArtifactManager;
|
|
17797
|
+
if (this.#sessionFile)
|
|
17798
|
+
return manager === this.#artifactManager && this.#artifactManagerSessionFile === this.#sessionFile;
|
|
17799
|
+
return manager === this.#ephemeralArtifactManager;
|
|
17800
|
+
}
|
|
17801
|
+
|
|
17802
|
+
#syncArtifactLifecycle(): symbol {
|
|
17803
|
+
if (
|
|
17804
|
+
this.#artifactLifecycleSessionId !== this.#sessionId ||
|
|
17805
|
+
this.#artifactLifecycleSessionFile !== this.#sessionFile
|
|
17806
|
+
) {
|
|
17807
|
+
this.#artifactLifecycleSessionId = this.#sessionId;
|
|
17808
|
+
this.#artifactLifecycleSessionFile = this.#sessionFile;
|
|
17809
|
+
this.#artifactLifecycle = Symbol("session-artifact-transition");
|
|
17810
|
+
}
|
|
17811
|
+
return this.#artifactLifecycle;
|
|
17812
|
+
}
|
|
17813
|
+
|
|
17814
|
+
#assertArtifactOpen(): void {
|
|
17815
|
+
if (this.#artifactClosing || this.#strictClosePending) throw new Error("Session manager is closing.");
|
|
17816
|
+
}
|
|
17817
|
+
|
|
17818
|
+
#beginArtifactClose(): void {
|
|
17819
|
+
if (!this.#artifactClosing) {
|
|
17820
|
+
this.#artifactClosing = true;
|
|
17821
|
+
this.#artifactLifecycle = Symbol("session-artifact-close");
|
|
17822
|
+
}
|
|
17823
|
+
}
|
|
17824
|
+
|
|
17825
|
+
#assertArtifactContinuation(
|
|
17826
|
+
token: symbol,
|
|
17827
|
+
sessionId: string,
|
|
17828
|
+
sessionFile: string | undefined,
|
|
17829
|
+
manager: ArtifactManager,
|
|
17830
|
+
): void {
|
|
17831
|
+
this.#assertArtifactOpen();
|
|
17832
|
+
if (
|
|
17833
|
+
token !== this.#syncArtifactLifecycle() ||
|
|
17834
|
+
sessionId !== this.#sessionId ||
|
|
17835
|
+
sessionFile !== this.#sessionFile ||
|
|
17836
|
+
!this.isArtifactManagerAuthorized(manager)
|
|
17837
|
+
)
|
|
17838
|
+
throw new Error("Session artifact continuation is no longer authorized.");
|
|
17839
|
+
manager.assertManagedBinding();
|
|
17101
17840
|
}
|
|
17102
17841
|
|
|
17103
17842
|
/**
|
|
@@ -17105,16 +17844,26 @@ export class SessionManager {
|
|
|
17105
17844
|
* one bound to the current session file unless an external manager was
|
|
17106
17845
|
* adopted via `adoptArtifactManager`. Falls back to the lazily created
|
|
17107
17846
|
* ephemeral filesystem store once a non-persistent session has saved an
|
|
17108
|
-
* artifact, so `artifact://` stays resolvable. Returns null
|
|
17109
|
-
*
|
|
17847
|
+
* artifact, so `artifact://` stays resolvable. Returns null when no store has
|
|
17848
|
+
* been established yet and once the manager has released its artifact
|
|
17849
|
+
* authority while closing — released authority stays observable as absence
|
|
17850
|
+
* (matching `isArtifactManagerAuthorized`), while artifact *operations* are
|
|
17851
|
+
* fenced by `#assertArtifactOpen()` and keep throwing.
|
|
17110
17852
|
*/
|
|
17111
17853
|
getArtifactManager(): ArtifactManager | null {
|
|
17854
|
+
if (this.#artifactClosing || this.#strictClosePending) return null;
|
|
17112
17855
|
return this.#getOrCreateArtifactManager() ?? this.#ephemeralArtifactManager;
|
|
17113
17856
|
}
|
|
17114
17857
|
|
|
17115
17858
|
/** Linearizably establish this session's persistent or ephemeral artifact manager. */
|
|
17116
17859
|
async ensureArtifactManager(): Promise<ArtifactManager | null> {
|
|
17117
|
-
|
|
17860
|
+
this.#assertArtifactOpen();
|
|
17861
|
+
const token = this.#syncArtifactLifecycle();
|
|
17862
|
+
const sessionId = this.#sessionId;
|
|
17863
|
+
const sessionFile = this.#sessionFile;
|
|
17864
|
+
const manager = this.#getOrCreateArtifactManager() ?? (await this.#ensureEphemeralArtifactManager());
|
|
17865
|
+
if (manager) this.#assertArtifactContinuation(token, sessionId, sessionFile, manager);
|
|
17866
|
+
return manager;
|
|
17118
17867
|
}
|
|
17119
17868
|
|
|
17120
17869
|
/**
|
|
@@ -17122,6 +17871,8 @@ export class SessionManager {
|
|
|
17122
17871
|
* Recreates the manager when the active session file changes.
|
|
17123
17872
|
*/
|
|
17124
17873
|
#getOrCreateArtifactManager(): ArtifactManager | null {
|
|
17874
|
+
this.#assertArtifactOpen();
|
|
17875
|
+
this.#syncArtifactLifecycle();
|
|
17125
17876
|
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager;
|
|
17126
17877
|
const sessionFile = this.#sessionFile;
|
|
17127
17878
|
if (!sessionFile) {
|
|
@@ -17152,7 +17903,14 @@ export class SessionManager {
|
|
|
17152
17903
|
async allocateArtifactPath(toolType: string): Promise<{ id?: string; path?: string }> {
|
|
17153
17904
|
const manager = this.#getOrCreateArtifactManager();
|
|
17154
17905
|
if (!manager) return {};
|
|
17155
|
-
|
|
17906
|
+
const token = this.#syncArtifactLifecycle();
|
|
17907
|
+
const sessionId = this.#sessionId;
|
|
17908
|
+
const sessionFile = this.#sessionFile;
|
|
17909
|
+
const allocation = await manager.allocatePath(toolType);
|
|
17910
|
+
// A reservation is not a grant to a caller whose session changed during
|
|
17911
|
+
// initialization. No writable pathname escapes before the final fence.
|
|
17912
|
+
this.#assertArtifactContinuation(token, sessionId, sessionFile, manager);
|
|
17913
|
+
return allocation;
|
|
17156
17914
|
}
|
|
17157
17915
|
|
|
17158
17916
|
/**
|
|
@@ -17162,8 +17920,49 @@ export class SessionManager {
|
|
|
17162
17920
|
* read back from the filesystem instead of being retained in memory.
|
|
17163
17921
|
*/
|
|
17164
17922
|
async saveArtifact(content: string, toolType: string): Promise<string | undefined> {
|
|
17923
|
+
this.#assertArtifactOpen();
|
|
17924
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(toolType)) throw new Error("Unsafe artifact tool type");
|
|
17925
|
+
const token = this.#syncArtifactLifecycle();
|
|
17926
|
+
const sessionId = this.#sessionId;
|
|
17927
|
+
const sessionFile = this.#sessionFile;
|
|
17165
17928
|
const manager = this.#getOrCreateArtifactManager() ?? (await this.#ensureEphemeralArtifactManager());
|
|
17166
|
-
|
|
17929
|
+
if (!manager) return undefined;
|
|
17930
|
+
this.#assertArtifactContinuation(token, sessionId, sessionFile, manager);
|
|
17931
|
+
const { id } = await manager.allocatePath(toolType);
|
|
17932
|
+
this.#assertArtifactContinuation(token, sessionId, sessionFile, manager);
|
|
17933
|
+
const contentBytes = Buffer.byteLength(content, "utf-8");
|
|
17934
|
+
const truncated =
|
|
17935
|
+
contentBytes > DEFAULT_ARTIFACT_MAX_BYTES ? truncateHeadBytes(content, DEFAULT_ARTIFACT_MAX_BYTES) : undefined;
|
|
17936
|
+
const published = truncated
|
|
17937
|
+
? `${truncated.text}\n[artifact truncated after ${truncated.bytes} bytes; omitted at least ${contentBytes - truncated.bytes} bytes]\n`
|
|
17938
|
+
: content;
|
|
17939
|
+
const filename = `${id}.${toolType}.log`;
|
|
17940
|
+
const store = manager.getManagedStore();
|
|
17941
|
+
if (store) store.publishNoReplaceSync(filename, Buffer.from(published, "utf8"));
|
|
17942
|
+
else publishManagedFileNoReplaceSync(path.join(manager.dir, filename), Buffer.from(published, "utf8"));
|
|
17943
|
+
return id;
|
|
17944
|
+
}
|
|
17945
|
+
|
|
17946
|
+
/** Capture artifact publication authority for one session lifecycle. */
|
|
17947
|
+
captureArtifactPublication(): SessionArtifactPublication {
|
|
17948
|
+
this.#assertArtifactOpen();
|
|
17949
|
+
const lifecycle = this.#syncArtifactLifecycle();
|
|
17950
|
+
const sessionId = this.#sessionId;
|
|
17951
|
+
const sessionFile = this.#sessionFile;
|
|
17952
|
+
const manager = this.#getOrCreateArtifactManager() ?? this.#ephemeralArtifactManager;
|
|
17953
|
+
if (manager) this.#assertArtifactContinuation(lifecycle, sessionId, sessionFile, manager);
|
|
17954
|
+
|
|
17955
|
+
return async (content, toolType) => {
|
|
17956
|
+
this.#assertArtifactOpen();
|
|
17957
|
+
if (
|
|
17958
|
+
lifecycle !== this.#syncArtifactLifecycle() ||
|
|
17959
|
+
sessionId !== this.#sessionId ||
|
|
17960
|
+
sessionFile !== this.#sessionFile
|
|
17961
|
+
)
|
|
17962
|
+
throw new Error("Session artifact continuation is no longer authorized.");
|
|
17963
|
+
if (manager) this.#assertArtifactContinuation(lifecycle, sessionId, sessionFile, manager);
|
|
17964
|
+
return this.saveArtifact(content, toolType);
|
|
17965
|
+
};
|
|
17167
17966
|
}
|
|
17168
17967
|
|
|
17169
17968
|
async #validatedEvictedToolOutputHandle(
|
|
@@ -17271,6 +18070,7 @@ export class SessionManager {
|
|
|
17271
18070
|
* Returns null when the artifact is missing.
|
|
17272
18071
|
*/
|
|
17273
18072
|
async getArtifactPath(id: string): Promise<string | null> {
|
|
18073
|
+
this.#assertArtifactOpen();
|
|
17274
18074
|
const manager = this.getArtifactManager();
|
|
17275
18075
|
if (!manager) return null;
|
|
17276
18076
|
return manager.getPath(id);
|
|
@@ -20653,7 +21453,13 @@ export class SessionManager {
|
|
|
20653
21453
|
await parentArtifacts.commitAttemptStaging(stagedManager, staged.attemptId, {
|
|
20654
21454
|
beforePublish: idMap => this.remapStagedArtifactReferences(idMap),
|
|
20655
21455
|
});
|
|
20656
|
-
} else
|
|
21456
|
+
} else {
|
|
21457
|
+
try {
|
|
21458
|
+
await stagedManager.discardAttemptStaging();
|
|
21459
|
+
} catch (error) {
|
|
21460
|
+
if (!isAuthorizedPendingCleanup(toError(error))) throw error;
|
|
21461
|
+
}
|
|
21462
|
+
}
|
|
20657
21463
|
}
|
|
20658
21464
|
if (staged.managedParentStore && staged.managedStagingStore) {
|
|
20659
21465
|
const stagedName = path.basename(staged.stagedSessionFile);
|