@gajae-code/coding-agent 0.18.7 → 0.18.9
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 +377 -1
- package/dist/types/async/endpoint-id.d.ts +16 -0
- package/dist/types/async/job-manager.d.ts +1 -1
- package/dist/types/async/support.d.ts +0 -14
- 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 +2 -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/commands/mcp-serve.d.ts +4 -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/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 +6 -1
- package/dist/types/discovery/mcp-json.d.ts +1 -0
- package/dist/types/eval/py/kernel.d.ts +18 -0
- package/dist/types/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/html-export-destination.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
- 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 +4 -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/psmux-detect.d.ts +3 -3
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
- package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
- 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 +14 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
- 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/config.d.ts +8 -5
- 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/manager.d.ts +2 -1
- package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
- package/dist/types/runtime-mcp/project-host-values.d.ts +27 -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 +24 -2
- package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -1
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- 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/client/client.d.ts +2 -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 +2 -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 +26 -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 +2 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +31 -7
- package/dist/types/session/session-storage.d.ts +69 -2
- package/dist/types/session/task-artifact-owner-codec.d.ts +2 -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/skill-state/workflow-mutation-guard.d.ts +3 -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/task/isolation-dir.d.ts +26 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/browser/cdp-url.d.ts +2 -0
- package/dist/types/tools/github-side-effect.d.ts +22 -0
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/monitor-mutation.d.ts +9 -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/verify-managed-empty-stop.ts +28 -0
- package/src/async/endpoint-id.ts +26 -0
- package/src/async/job-manager.ts +247 -132
- package/src/async/support.ts +0 -25
- package/src/capability/index.ts +15 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +2 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/customize-doctor.ts +15 -2
- package/src/cli/doctor/repairs.ts +4 -1
- package/src/cli/doctor-supervisor.ts +3 -0
- package/src/cli/public-command-errors.ts +21 -1
- package/src/cli/public-command-registry.ts +28 -6
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/skills-cli.ts +5 -0
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/mcp-serve.ts +7 -0
- package/src/commands/sdk.ts +455 -397
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +316 -79
- 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/skill-settings-defaults.ts +7 -0
- package/src/coordinator-mcp/policy.ts +3 -2
- package/src/coordinator-mcp/server.ts +2 -0
- 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 +4 -13
- package/src/dap/config.ts +2 -2
- 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/register.ts +10 -6
- 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 +13 -1
- package/src/discovery/helpers.ts +171 -29
- package/src/discovery/mcp-json.ts +26 -12
- package/src/discovery/ssh.ts +1 -1
- package/src/discovery/substitute-plugin-root.ts +3 -1
- package/src/edit/file-read-cache.ts +1 -1
- package/src/eval/py/executor.ts +796 -154
- package/src/eval/py/kernel.ts +79 -13
- package/src/eval/py/tool-bridge.ts +4 -0
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/html-export-destination.ts +16 -0
- package/src/export/html/index.ts +7 -10
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
- package/src/extensibility/extensions/runner.ts +15 -1
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -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 +9 -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 -9
- package/src/gjc-runtime/launch-worktree.ts +70 -9
- package/src/gjc-runtime/psmux-detect.ts +28 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- package/src/gjc-runtime/session-state-sidecar.ts +3 -2
- package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
- 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 +105 -16
- package/src/gjc-runtime/tmux-common.ts +8 -5
- 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/internal-urls/docs-index.generated.ts +20 -19
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/config.ts +12 -63
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- package/src/memories/index.ts +88 -7
- package/src/modes/acp/acp-agent.ts +88 -4
- 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/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +1 -1
- 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 +76 -31
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
- package/src/modes/controllers/selector-controller.ts +24 -15
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -1
- 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 +4 -2
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/json-rpc.ts +2 -1
- package/src/runtime-mcp/manager.ts +29 -11
- package/src/runtime-mcp/mcp-redirect.ts +73 -0
- package/src/runtime-mcp/oauth-flow.ts +14 -2
- package/src/runtime-mcp/oauth-public-url.ts +14 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/pool.ts +6 -0
- package/src/runtime-mcp/project-host-values.ts +46 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +4 -3
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +95 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +81 -6
- package/src/sdk/broker/doctor-restart.ts +4 -0
- package/src/sdk/broker/ensure.ts +31 -2
- package/src/sdk/broker/lifecycle.ts +711 -66
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +238 -43
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2705 -303
- package/src/sdk/host/session-runtime.ts +490 -143
- 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/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 +82 -18
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +69 -45
- package/src/session/agent-session.ts +679 -238
- package/src/session/artifacts.ts +176 -71
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3779 -462
- package/src/session/internal/managed-session-storage.ts +501 -78
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +19 -3
- package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
- package/src/session/session-manager.ts +951 -133
- package/src/session/session-retirement.ts +573 -14
- package/src/session/session-storage.ts +447 -15
- package/src/session/task-artifact-owner-codec.ts +71 -8
- package/src/session/task-artifact-owner-retirement.ts +21 -19
- 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 +145 -2
- package/src/skill-state/workflow-mutation-guard.ts +102 -10
- package/src/slash-commands/builtin-registry.ts +39 -9
- package/src/slash-commands/helpers/mcp.ts +8 -4
- 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/task/isolation-dir.ts +143 -0
- package/src/task/worktree.ts +16 -7
- package/src/tools/ask.ts +205 -6
- package/src/tools/ast-edit-write-path.ts +68 -0
- package/src/tools/ast-edit.ts +10 -1
- 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/browser/cdp-url.ts +18 -0
- package/src/tools/browser.ts +8 -1
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/gh.ts +10 -0
- package/src/tools/github-side-effect.ts +112 -0
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/monitor-mutation.ts +27 -0
- package/src/tools/monitor.ts +7 -0
- package/src/tools/python.ts +188 -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/external-editor.ts +3 -2
- package/src/utils/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- package/src/web/insane/url-guard.ts +2 -1
- package/src/web/search/providers/codex.ts +3 -2
package/src/session/artifacts.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
publishManagedFileNoReplace,
|
|
16
16
|
publishManagedFileNoReplaceSync,
|
|
17
17
|
} from "./internal/managed-session-storage";
|
|
18
|
+
import { type SessionStorageStat, writeExplicitOwnerOnlyTextSync } from "./session-storage";
|
|
18
19
|
import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
|
|
19
20
|
|
|
20
21
|
export interface ManagedOutputGeneration {
|
|
@@ -67,6 +68,28 @@ function parseManagedOutputGeneration(value: Uint8Array, outputFilenamePrefix: s
|
|
|
67
68
|
function sameGeneration(left: ManagedOutputGeneration, right: ManagedOutputGeneration): boolean {
|
|
68
69
|
return left.outputFilename === right.outputFilename && left.metadataFilename === right.metadataFilename;
|
|
69
70
|
}
|
|
71
|
+
|
|
72
|
+
function normalizeByteRange(size: number, range: ArtifactByteRange): { start: number; endExclusive: number } {
|
|
73
|
+
const start = Math.max(0, Math.min(size, range.start ?? 0));
|
|
74
|
+
const endExclusive = Math.max(start, Math.min(size, range.endExclusive ?? size));
|
|
75
|
+
const normalizedStart = Number.isNaN(start) ? 0 : Math.trunc(start);
|
|
76
|
+
const normalizedEnd = Number.isNaN(endExclusive) ? 0 : Math.trunc(endExclusive);
|
|
77
|
+
return {
|
|
78
|
+
start: normalizedStart,
|
|
79
|
+
endExclusive: Math.max(normalizedStart, normalizedEnd),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ARTIFACT_STREAM_CHUNK_BYTES = 64 * 1024;
|
|
84
|
+
const ARTIFACT_RANGE_READ_CHUNK_BYTES = 64 * 1024 * 1024;
|
|
85
|
+
|
|
86
|
+
type AttemptStagingCleanupClassification = { kind: "authorized_quarantine" } | { kind: "failure"; error: unknown };
|
|
87
|
+
|
|
88
|
+
function classifyAttemptStagingCleanupFailure(error: unknown): AttemptStagingCleanupClassification {
|
|
89
|
+
return error instanceof Error && error.message === "cleanup_pending"
|
|
90
|
+
? { kind: "authorized_quarantine" }
|
|
91
|
+
: { kind: "failure", error };
|
|
92
|
+
}
|
|
70
93
|
export interface ArtifactSaveOptions {
|
|
71
94
|
maxBytes?: number;
|
|
72
95
|
}
|
|
@@ -223,7 +246,8 @@ export class ArtifactManager {
|
|
|
223
246
|
if (!/^[a-zA-Z0-9_.-]+$/.test(filename)) throw new Error("Unsafe named artifact");
|
|
224
247
|
await this.#ensureDir();
|
|
225
248
|
if (this.#store) await this.#store.replace(filename, Buffer.from(content, "utf8"));
|
|
226
|
-
|
|
249
|
+
// Named files, including draft.txt, are created here.
|
|
250
|
+
else writeExplicitOwnerOnlyTextSync(path.join(this.#dir, filename), content);
|
|
227
251
|
}
|
|
228
252
|
|
|
229
253
|
async replaceNamedBytes(filename: string, bytes: Uint8Array): Promise<void> {
|
|
@@ -309,26 +333,16 @@ export class ArtifactManager {
|
|
|
309
333
|
* Best-effort removal of a previously published named artifact. Used to roll
|
|
310
334
|
* back staged publications when a transactional operation (e.g. gated
|
|
311
335
|
* maintenance pruning) is rejected after publication succeeded. Returns false
|
|
312
|
-
* when the artifact could not be removed
|
|
313
|
-
*
|
|
336
|
+
* when the exact named artifact is absent or could not be removed; success does
|
|
337
|
+
* not claim that unrelated native cleanup residue was reclaimed.
|
|
314
338
|
*/
|
|
315
339
|
async removeNamedBestEffort(filename: string): Promise<boolean> {
|
|
316
340
|
if (!/^[a-zA-Z0-9_.-]+$/.test(filename)) return false;
|
|
317
341
|
try {
|
|
318
342
|
if (this.#store) {
|
|
319
|
-
const beforePaths = new Set(await fs.readdir(this.#store.dir));
|
|
320
343
|
const staged = this.#store.readExpected(filename);
|
|
321
|
-
if (staged)
|
|
322
|
-
|
|
323
|
-
// placeholders cannot make rollback fail before exact removal runs.
|
|
324
|
-
for (const basename of await fs.readdir(this.#store.dir)) {
|
|
325
|
-
const nativeResidue = /^\.gjc-/u.test(basename);
|
|
326
|
-
const ownQuarantine = basename === `${filename}.removing` || basename.startsWith(`${filename}.`);
|
|
327
|
-
if (beforePaths.has(basename) || (!nativeResidue && !ownQuarantine)) continue;
|
|
328
|
-
const residuePath = path.join(this.#store.dir, basename);
|
|
329
|
-
const stat = await fs.lstat(residuePath);
|
|
330
|
-
await fs.rm(residuePath, { recursive: stat.isDirectory(), force: true });
|
|
331
|
-
}
|
|
344
|
+
if (!staged) return false;
|
|
345
|
+
this.#store.removeExpected(filename, staged);
|
|
332
346
|
} else {
|
|
333
347
|
await fs.unlink(path.join(this.#dir, filename));
|
|
334
348
|
}
|
|
@@ -502,7 +516,13 @@ export class ArtifactManager {
|
|
|
502
516
|
publishedNames.push(mappedFilename);
|
|
503
517
|
}
|
|
504
518
|
this.#reservations.set(attemptId, { start, count: ids.length, names: [...publishedNames] });
|
|
505
|
-
|
|
519
|
+
try {
|
|
520
|
+
await staging.discardAttemptStaging();
|
|
521
|
+
} catch (error) {
|
|
522
|
+
const cleanup = classifyAttemptStagingCleanupFailure(error);
|
|
523
|
+
if (cleanup.kind === "authorized_quarantine") return frozenMap;
|
|
524
|
+
throw cleanup.error;
|
|
525
|
+
}
|
|
506
526
|
return frozenMap;
|
|
507
527
|
} catch (error) {
|
|
508
528
|
// Cleanup is best-effort, but a durable removal failure must never be silent: it leaves a
|
|
@@ -550,64 +570,34 @@ export class ArtifactManager {
|
|
|
550
570
|
|
|
551
571
|
async discardAttemptStaging(): Promise<void> {
|
|
552
572
|
if (this.#store) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
try {
|
|
559
|
-
parentBefore = cleanupStore.captureTree(parentCleanupPath);
|
|
560
|
-
} catch {
|
|
561
|
-
parentBefore = undefined;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
573
|
+
// Only an attempt manager owns a managed subtree that it may retire. A
|
|
574
|
+
// normal manager's store is borrowed by this cleanup method and its root
|
|
575
|
+
// must never become a deletion target.
|
|
576
|
+
if (!this.#stagingParentStore || !this.#stagingRelativePath) return;
|
|
577
|
+
const cleanupStore = this.#stagingParentStore;
|
|
564
578
|
try {
|
|
565
|
-
const snapshot = cleanupStore.captureTree(
|
|
566
|
-
|
|
579
|
+
const snapshot = cleanupStore.captureTree(this.#stagingRelativePath);
|
|
580
|
+
const issuedRoot = this.#store.subtreeRootAuthority;
|
|
581
|
+
if (
|
|
582
|
+
issuedRoot.canonicalPath !== path.resolve(this.#dir) ||
|
|
583
|
+
snapshot.rootDev !== issuedRoot.dev.toString() ||
|
|
584
|
+
snapshot.rootIno !== issuedRoot.ino.toString()
|
|
585
|
+
)
|
|
586
|
+
throw new Error("artifact_staging_root_changed");
|
|
587
|
+
// Stop using and release this attempt's retained writable root before
|
|
588
|
+
// asking the parent authority to retire the tree. The exact tree snapshot
|
|
589
|
+
// above is still checked by native removal; a concurrent change therefore
|
|
590
|
+
// remains a refusal rather than being adopted. Never close the borrowed
|
|
591
|
+
// parent store.
|
|
592
|
+
this.#store.close();
|
|
593
|
+
cleanupStore.removeTreeExpected(this.#stagingRelativePath, snapshot);
|
|
567
594
|
} catch (error) {
|
|
568
|
-
if (!(error instanceof Error &&
|
|
569
|
-
|
|
595
|
+
if (!(error instanceof Error && error.message === "not_found")) throw error;
|
|
596
|
+
} finally {
|
|
597
|
+
// Replacement, snapshot, and native cleanup failures also release only
|
|
598
|
+
// the capability owned by this attempt manager.
|
|
599
|
+
this.#store.close();
|
|
570
600
|
}
|
|
571
|
-
if (this.#stagingParentStore && parentBefore) {
|
|
572
|
-
try {
|
|
573
|
-
const after = cleanupStore.captureTree(parentCleanupPath);
|
|
574
|
-
const beforePaths = new Set(parentBefore.entries.map(entry => entry.relativePath));
|
|
575
|
-
for (const entry of after.entries) {
|
|
576
|
-
if (
|
|
577
|
-
entry.kind === "directory" &&
|
|
578
|
-
entry.relativePath.length > 0 &&
|
|
579
|
-
!beforePaths.has(entry.relativePath) &&
|
|
580
|
-
/\.removing$/u.test(path.posix.basename(entry.relativePath))
|
|
581
|
-
) {
|
|
582
|
-
await fs.rm(path.join(cleanupStore.dir, parentCleanupPath, entry.relativePath), {
|
|
583
|
-
recursive: true,
|
|
584
|
-
force: true,
|
|
585
|
-
});
|
|
586
|
-
continue;
|
|
587
|
-
}
|
|
588
|
-
if (
|
|
589
|
-
entry.kind !== "file" ||
|
|
590
|
-
beforePaths.has(entry.relativePath) ||
|
|
591
|
-
!/^\\.gjc-(?:exact-unlink-placeholder|remove)-/u.test(path.posix.basename(entry.relativePath))
|
|
592
|
-
)
|
|
593
|
-
continue;
|
|
594
|
-
const relative = path.posix.join(parentCleanupPath, entry.relativePath);
|
|
595
|
-
const expected = cleanupStore.readExpected(relative);
|
|
596
|
-
if (expected) {
|
|
597
|
-
try {
|
|
598
|
-
cleanupStore.removeExpected(relative, expected);
|
|
599
|
-
} catch (cleanupError) {
|
|
600
|
-
if (!(cleanupError instanceof Error && cleanupError.message === "cleanup_pending"))
|
|
601
|
-
throw cleanupError;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
await fs.rm(path.join(cleanupStore.dir, relative), { force: true }).catch(() => undefined);
|
|
605
|
-
}
|
|
606
|
-
} catch {
|
|
607
|
-
// Retained cleanup evidence is safe to leave for a later maintenance pass.
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
this.#store.close();
|
|
611
601
|
} else {
|
|
612
602
|
await fs.rm(this.#dir, { recursive: true, force: true });
|
|
613
603
|
}
|
|
@@ -650,7 +640,69 @@ export class ArtifactManager {
|
|
|
650
640
|
}
|
|
651
641
|
}
|
|
652
642
|
|
|
643
|
+
#captureManagedArtifact(id: string): {
|
|
644
|
+
filename: string;
|
|
645
|
+
size: number;
|
|
646
|
+
expectedDescriptor: Pick<SessionStorageStat, "dev" | "ino" | "nlink" | "size" | "mtimeNs" | "ctimeNs">;
|
|
647
|
+
} | null {
|
|
648
|
+
if (!this.#store) return null;
|
|
649
|
+
try {
|
|
650
|
+
const entry = this.#store
|
|
651
|
+
.captureTree("")
|
|
652
|
+
.entries.find(candidate => candidate.kind === "file" && candidate.relativePath.startsWith(`${id}.`));
|
|
653
|
+
if (!entry) return null;
|
|
654
|
+
const size = Number(BigInt(entry.size));
|
|
655
|
+
if (!Number.isSafeInteger(size) || size < 0) return null;
|
|
656
|
+
return {
|
|
657
|
+
filename: entry.relativePath,
|
|
658
|
+
size,
|
|
659
|
+
expectedDescriptor: {
|
|
660
|
+
dev: BigInt(entry.dev),
|
|
661
|
+
ino: BigInt(entry.ino),
|
|
662
|
+
nlink: BigInt(entry.nlink),
|
|
663
|
+
size,
|
|
664
|
+
mtimeNs: BigInt(entry.mtimeNs),
|
|
665
|
+
ctimeNs: BigInt(entry.ctimeNs),
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
} catch {
|
|
669
|
+
// listFiles historically hides an unavailable managed tree as no artifacts.
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
653
674
|
async readRange(id: string, range: ArtifactByteRange = {}): Promise<string> {
|
|
675
|
+
if (this.#store) {
|
|
676
|
+
const artifact = this.#captureManagedArtifact(id);
|
|
677
|
+
if (!artifact) throw new Error(`artifact://${id} not found`);
|
|
678
|
+
const { start, endExclusive } = normalizeByteRange(artifact.size, range);
|
|
679
|
+
const length = Math.max(0, endExclusive - start);
|
|
680
|
+
let bytes: Uint8Array;
|
|
681
|
+
if (length <= ARTIFACT_RANGE_READ_CHUNK_BYTES) {
|
|
682
|
+
bytes = this.#store.readRangeExpectedSync(
|
|
683
|
+
artifact.filename,
|
|
684
|
+
start,
|
|
685
|
+
length,
|
|
686
|
+
artifact.expectedDescriptor,
|
|
687
|
+
).bytes;
|
|
688
|
+
} else {
|
|
689
|
+
const complete = Buffer.allocUnsafe(length);
|
|
690
|
+
let offset = 0;
|
|
691
|
+
while (offset < length) {
|
|
692
|
+
const chunkLength = Math.min(ARTIFACT_RANGE_READ_CHUNK_BYTES, length - offset);
|
|
693
|
+
const chunk = this.#store.readRangeExpectedSync(
|
|
694
|
+
artifact.filename,
|
|
695
|
+
start + offset,
|
|
696
|
+
chunkLength,
|
|
697
|
+
artifact.expectedDescriptor,
|
|
698
|
+
).bytes;
|
|
699
|
+
complete.set(chunk, offset);
|
|
700
|
+
offset += chunk.byteLength;
|
|
701
|
+
}
|
|
702
|
+
bytes = complete;
|
|
703
|
+
}
|
|
704
|
+
return new TextDecoder().decode(bytes);
|
|
705
|
+
}
|
|
654
706
|
const artifactPath = await this.getPath(id);
|
|
655
707
|
if (!artifactPath) throw new Error(`artifact://${id} not found`);
|
|
656
708
|
const file = Bun.file(artifactPath);
|
|
@@ -661,6 +713,59 @@ export class ArtifactManager {
|
|
|
661
713
|
}
|
|
662
714
|
|
|
663
715
|
async openReadStream(id: string, range: ArtifactByteRange = {}): Promise<ReadableStream<Uint8Array>> {
|
|
716
|
+
if (this.#store) {
|
|
717
|
+
const artifact = this.#captureManagedArtifact(id);
|
|
718
|
+
if (!artifact) throw new Error(`artifact://${id} not found`);
|
|
719
|
+
const { start, endExclusive } = normalizeByteRange(artifact.size, range);
|
|
720
|
+
const lease = this.#store.openReadLease(artifact.filename, artifact.expectedDescriptor);
|
|
721
|
+
let offset = start;
|
|
722
|
+
let closed = false;
|
|
723
|
+
const closeLease = (): void => {
|
|
724
|
+
if (closed) return;
|
|
725
|
+
closed = true;
|
|
726
|
+
lease.close();
|
|
727
|
+
};
|
|
728
|
+
const closeAfterFailure = (): void => {
|
|
729
|
+
try {
|
|
730
|
+
closeLease();
|
|
731
|
+
} catch {
|
|
732
|
+
// Keep the read failure as the stream's observable error.
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
return new ReadableStream<Uint8Array>(
|
|
736
|
+
{
|
|
737
|
+
start: controller => {
|
|
738
|
+
if (offset !== endExclusive) return;
|
|
739
|
+
try {
|
|
740
|
+
closeLease();
|
|
741
|
+
controller.close();
|
|
742
|
+
} catch (error) {
|
|
743
|
+
closeAfterFailure();
|
|
744
|
+
controller.error(error);
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
pull: controller => {
|
|
748
|
+
if (closed) return;
|
|
749
|
+
try {
|
|
750
|
+
const length = Math.min(ARTIFACT_STREAM_CHUNK_BYTES, endExclusive - offset);
|
|
751
|
+
const bytes = lease.readRange(offset, length);
|
|
752
|
+
if (bytes.byteLength !== length) throw new Error("range_not_present");
|
|
753
|
+
offset += bytes.byteLength;
|
|
754
|
+
if (offset === endExclusive) closeLease();
|
|
755
|
+
controller.enqueue(bytes);
|
|
756
|
+
if (offset === endExclusive) controller.close();
|
|
757
|
+
} catch (error) {
|
|
758
|
+
closeAfterFailure();
|
|
759
|
+
controller.error(error);
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
cancel: () => {
|
|
763
|
+
closeLease();
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
{ highWaterMark: 0 },
|
|
767
|
+
);
|
|
768
|
+
}
|
|
664
769
|
const artifactPath = await this.getPath(id);
|
|
665
770
|
if (!artifactPath) throw new Error(`artifact://${id} not found`);
|
|
666
771
|
const file = Bun.file(artifactPath);
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { SessionStorageFileIdentity } from "../session-storage";
|
|
3
|
+
import {
|
|
4
|
+
assertSessionRoot,
|
|
5
|
+
EMPTY_PAYLOAD_SHA256,
|
|
6
|
+
parseTaskArtifactOwnerDeletionEvidence,
|
|
7
|
+
parseTaskArtifactOwnerLocator,
|
|
8
|
+
parseTaskArtifactOwnerRetirementContinuation,
|
|
9
|
+
parseTaskArtifactOwnerRetirementOutcome,
|
|
10
|
+
type TaskArtifactOwnerDeletionEvidence,
|
|
11
|
+
type TaskArtifactOwnerLocator,
|
|
12
|
+
type TaskArtifactOwnerRetirementContinuation,
|
|
13
|
+
type TaskArtifactOwnerRetirementOutcome,
|
|
14
|
+
type TaskArtifactOwnerStorageContext,
|
|
15
|
+
} from "../task-artifact-owner-codec";
|
|
16
|
+
|
|
17
|
+
export type ManagedGcSessionRetirementState = "prepared" | "artifacts_removed" | "owner_pending" | "owner_retired";
|
|
18
|
+
|
|
19
|
+
export interface ManagedGcSessionRetirementTarget {
|
|
20
|
+
readonly transcriptPath: string;
|
|
21
|
+
readonly sessionId: string;
|
|
22
|
+
readonly cwd: string;
|
|
23
|
+
readonly transcriptIdentity: SessionStorageFileIdentity;
|
|
24
|
+
readonly taskArtifactOwnerLocator: TaskArtifactOwnerLocator;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ManagedGcSessionRetirementReceipt extends ManagedGcSessionRetirementTarget {
|
|
28
|
+
readonly state: ManagedGcSessionRetirementState;
|
|
29
|
+
readonly taskArtifactOwnerDeletionEvidence: TaskArtifactOwnerDeletionEvidence;
|
|
30
|
+
readonly artifactsRemoved?: true;
|
|
31
|
+
readonly ownerRetirementAttempt?: number;
|
|
32
|
+
readonly taskArtifactOwnerRetirementOutcome?: TaskArtifactOwnerRetirementOutcome;
|
|
33
|
+
readonly taskArtifactOwnerRetirementContinuation?: TaskArtifactOwnerRetirementContinuation;
|
|
34
|
+
readonly taskArtifactOwnerPayloadRetired?: true;
|
|
35
|
+
readonly taskArtifactOwnerNamespaceRetained?: true;
|
|
36
|
+
readonly taskArtifactOwnerRetired?: true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
40
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function managedGcRetirementIdentityRecord(identity: SessionStorageFileIdentity): Record<string, unknown> {
|
|
44
|
+
return {
|
|
45
|
+
dev: identity.dev.toString(),
|
|
46
|
+
ino: identity.ino.toString(),
|
|
47
|
+
nlink: identity.nlink?.toString(),
|
|
48
|
+
size: identity.size,
|
|
49
|
+
mtimeNs: identity.mtimeNs.toString(),
|
|
50
|
+
sha256: identity.sha256,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function sameIdentity(left: SessionStorageFileIdentity, right: SessionStorageFileIdentity): boolean {
|
|
55
|
+
return (
|
|
56
|
+
left.dev === right.dev &&
|
|
57
|
+
left.ino === right.ino &&
|
|
58
|
+
left.nlink === right.nlink &&
|
|
59
|
+
left.size === right.size &&
|
|
60
|
+
left.mtimeNs === right.mtimeNs &&
|
|
61
|
+
left.sha256 === right.sha256
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function managedGcRetirementIdentityValue(value: unknown): SessionStorageFileIdentity {
|
|
66
|
+
if (!isRecord(value)) throw new Error("task_artifact_owner_continuation_corrupt");
|
|
67
|
+
if (
|
|
68
|
+
Object.keys(value).length !== 6 ||
|
|
69
|
+
!["dev", "ino", "nlink", "size", "mtimeNs", "sha256"].every(key => key in value) ||
|
|
70
|
+
typeof value.dev !== "string" ||
|
|
71
|
+
!/^(0|[1-9][0-9]*)$/u.test(value.dev) ||
|
|
72
|
+
typeof value.ino !== "string" ||
|
|
73
|
+
!/^(0|[1-9][0-9]*)$/u.test(value.ino) ||
|
|
74
|
+
typeof value.nlink !== "string" ||
|
|
75
|
+
!/^(0|[1-9][0-9]*)$/u.test(value.nlink) ||
|
|
76
|
+
value.nlink !== "1" ||
|
|
77
|
+
typeof value.size !== "number" ||
|
|
78
|
+
!Number.isSafeInteger(value.size) ||
|
|
79
|
+
value.size < 0 ||
|
|
80
|
+
typeof value.mtimeNs !== "string" ||
|
|
81
|
+
!/^(0|-?[1-9][0-9]*)$/u.test(value.mtimeNs) ||
|
|
82
|
+
typeof value.sha256 !== "string" ||
|
|
83
|
+
!/^[0-9a-f]{64}$/u.test(value.sha256)
|
|
84
|
+
)
|
|
85
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
86
|
+
return {
|
|
87
|
+
dev: BigInt(value.dev),
|
|
88
|
+
ino: BigInt(value.ino),
|
|
89
|
+
nlink: BigInt(value.nlink),
|
|
90
|
+
size: value.size,
|
|
91
|
+
mtimeNs: BigInt(value.mtimeNs),
|
|
92
|
+
sha256: value.sha256,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateTarget(
|
|
97
|
+
ownerContext: TaskArtifactOwnerStorageContext,
|
|
98
|
+
target: ManagedGcSessionRetirementTarget,
|
|
99
|
+
): { locator: TaskArtifactOwnerLocator; identity: SessionStorageFileIdentity } {
|
|
100
|
+
if (
|
|
101
|
+
!target ||
|
|
102
|
+
typeof target.transcriptPath !== "string" ||
|
|
103
|
+
typeof target.sessionId !== "string" ||
|
|
104
|
+
typeof target.cwd !== "string"
|
|
105
|
+
)
|
|
106
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
107
|
+
assertSessionRoot(ownerContext);
|
|
108
|
+
const relativeTranscript = path.relative(ownerContext.sessionsRoot, target.transcriptPath);
|
|
109
|
+
if (
|
|
110
|
+
target.sessionId.length === 0 ||
|
|
111
|
+
!path.isAbsolute(target.transcriptPath) ||
|
|
112
|
+
path.resolve(target.transcriptPath) !== target.transcriptPath ||
|
|
113
|
+
relativeTranscript === "" ||
|
|
114
|
+
path.isAbsolute(relativeTranscript) ||
|
|
115
|
+
relativeTranscript === ".." ||
|
|
116
|
+
relativeTranscript.startsWith(`..${path.sep}`)
|
|
117
|
+
)
|
|
118
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
119
|
+
let locator: TaskArtifactOwnerLocator | undefined;
|
|
120
|
+
try {
|
|
121
|
+
locator = parseTaskArtifactOwnerLocator(target.taskArtifactOwnerLocator);
|
|
122
|
+
} catch {
|
|
123
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
124
|
+
}
|
|
125
|
+
if (!locator) throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
126
|
+
let identity: SessionStorageFileIdentity;
|
|
127
|
+
try {
|
|
128
|
+
identity = managedGcRetirementIdentityValue(managedGcRetirementIdentityRecord(target.transcriptIdentity));
|
|
129
|
+
} catch {
|
|
130
|
+
throw new Error("task_artifact_owner_continuation_identity_invalid");
|
|
131
|
+
}
|
|
132
|
+
if (
|
|
133
|
+
identity.nlink !== 1n ||
|
|
134
|
+
identity.size < 0 ||
|
|
135
|
+
!Number.isSafeInteger(identity.size) ||
|
|
136
|
+
!/^[0-9a-f]{64}$/u.test(identity.sha256)
|
|
137
|
+
)
|
|
138
|
+
throw new Error("task_artifact_owner_continuation_identity_invalid");
|
|
139
|
+
return { locator, identity };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Strictly decode one persisted managed-GC receipt. A decoded `owner_retired` outcome is persisted DTO data only;
|
|
144
|
+
* this pure parser does not verify current filesystem state, payload destruction, or writer quiescence. A future
|
|
145
|
+
* journal reader must perform the live physical-retirement proof for the relevant state before treating it as done.
|
|
146
|
+
*/
|
|
147
|
+
export function parseManagedGcRetirementReceipt(
|
|
148
|
+
value: unknown,
|
|
149
|
+
scopeDigest: string,
|
|
150
|
+
ownerContext: TaskArtifactOwnerStorageContext,
|
|
151
|
+
target: ManagedGcSessionRetirementTarget,
|
|
152
|
+
): ManagedGcSessionRetirementReceipt {
|
|
153
|
+
if (!isRecord(value)) throw new Error("task_artifact_owner_continuation_corrupt");
|
|
154
|
+
const { locator: targetLocator, identity: targetIdentity } = validateTarget(ownerContext, target);
|
|
155
|
+
const record = value;
|
|
156
|
+
const state = record.state;
|
|
157
|
+
const commonKeys = [
|
|
158
|
+
"schemaVersion",
|
|
159
|
+
"state",
|
|
160
|
+
"scope",
|
|
161
|
+
"transcriptPath",
|
|
162
|
+
"sessionId",
|
|
163
|
+
"cwd",
|
|
164
|
+
"transcriptIdentity",
|
|
165
|
+
"taskArtifactOwnerDeletionEvidence",
|
|
166
|
+
];
|
|
167
|
+
const outcomeValue = record.taskArtifactOwnerRetirementOutcome;
|
|
168
|
+
const outcomeKind = isRecord(outcomeValue) ? outcomeValue.kind : undefined;
|
|
169
|
+
const expectedKeys =
|
|
170
|
+
state === "prepared"
|
|
171
|
+
? commonKeys
|
|
172
|
+
: state === "artifacts_removed"
|
|
173
|
+
? [...commonKeys, "artifactsRemoved"]
|
|
174
|
+
: state === "owner_pending"
|
|
175
|
+
? [
|
|
176
|
+
...commonKeys,
|
|
177
|
+
"ownerRetirementAttempt",
|
|
178
|
+
"artifactsRemoved",
|
|
179
|
+
"taskArtifactOwnerRetirementOutcome",
|
|
180
|
+
"taskArtifactOwnerRetirementContinuation",
|
|
181
|
+
...(outcomeKind === "payload_retired"
|
|
182
|
+
? ["taskArtifactOwnerPayloadRetired", "taskArtifactOwnerNamespaceRetained"]
|
|
183
|
+
: []),
|
|
184
|
+
]
|
|
185
|
+
: state === "owner_retired"
|
|
186
|
+
? [
|
|
187
|
+
...commonKeys,
|
|
188
|
+
"artifactsRemoved",
|
|
189
|
+
"taskArtifactOwnerRetired",
|
|
190
|
+
"taskArtifactOwnerRetirementOutcome",
|
|
191
|
+
]
|
|
192
|
+
: undefined;
|
|
193
|
+
if (
|
|
194
|
+
!expectedKeys ||
|
|
195
|
+
Object.keys(record).length !== expectedKeys.length ||
|
|
196
|
+
!expectedKeys.every(key => key in record) ||
|
|
197
|
+
record.schemaVersion !== 1 ||
|
|
198
|
+
record.scope !== scopeDigest ||
|
|
199
|
+
record.transcriptPath !== target.transcriptPath ||
|
|
200
|
+
record.sessionId !== target.sessionId ||
|
|
201
|
+
record.cwd !== target.cwd ||
|
|
202
|
+
(state !== "prepared" && record.artifactsRemoved !== true) ||
|
|
203
|
+
(state === "owner_retired" && record.taskArtifactOwnerRetired !== true) ||
|
|
204
|
+
(state === "owner_pending" &&
|
|
205
|
+
(!Number.isSafeInteger(record.ownerRetirementAttempt) || (record.ownerRetirementAttempt as number) < 1))
|
|
206
|
+
)
|
|
207
|
+
throw new Error("task_artifact_owner_continuation_mismatch");
|
|
208
|
+
const identity = managedGcRetirementIdentityValue(record.transcriptIdentity);
|
|
209
|
+
if (!sameIdentity(identity, targetIdentity)) throw new Error("task_artifact_owner_continuation_transcript_replaced");
|
|
210
|
+
let evidence: TaskArtifactOwnerDeletionEvidence;
|
|
211
|
+
try {
|
|
212
|
+
evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
|
|
213
|
+
} catch {
|
|
214
|
+
throw new Error("task_artifact_owner_continuation_evidence_corrupt");
|
|
215
|
+
}
|
|
216
|
+
if (evidence.sessionId !== target.sessionId || JSON.stringify(evidence.locator) !== JSON.stringify(targetLocator))
|
|
217
|
+
throw new Error("task_artifact_owner_continuation_locator_mismatch");
|
|
218
|
+
let outcome: TaskArtifactOwnerRetirementOutcome | undefined;
|
|
219
|
+
if (state === "owner_pending" || state === "owner_retired") {
|
|
220
|
+
outcome = parseTaskArtifactOwnerRetirementOutcome(ownerContext, evidence, outcomeValue);
|
|
221
|
+
if (
|
|
222
|
+
(state === "owner_pending" && outcome.kind === "completed") ||
|
|
223
|
+
(state === "owner_retired" && outcome.kind !== "completed")
|
|
224
|
+
)
|
|
225
|
+
throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
226
|
+
if (outcome.kind !== "completed") {
|
|
227
|
+
let receiptContinuation: TaskArtifactOwnerRetirementContinuation;
|
|
228
|
+
try {
|
|
229
|
+
receiptContinuation = parseTaskArtifactOwnerRetirementContinuation(
|
|
230
|
+
ownerContext,
|
|
231
|
+
evidence,
|
|
232
|
+
record.taskArtifactOwnerRetirementContinuation,
|
|
233
|
+
);
|
|
234
|
+
} catch {
|
|
235
|
+
throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
236
|
+
}
|
|
237
|
+
if (JSON.stringify(receiptContinuation) !== JSON.stringify(outcome.continuation))
|
|
238
|
+
throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
239
|
+
if (
|
|
240
|
+
(outcome.kind === "payload_retired" &&
|
|
241
|
+
(record.taskArtifactOwnerPayloadRetired !== true ||
|
|
242
|
+
record.taskArtifactOwnerNamespaceRetained !== true)) ||
|
|
243
|
+
(outcome.kind !== "payload_retired" &&
|
|
244
|
+
("taskArtifactOwnerPayloadRetired" in record || "taskArtifactOwnerNamespaceRetained" in record))
|
|
245
|
+
)
|
|
246
|
+
throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
state: state as ManagedGcSessionRetirementState,
|
|
251
|
+
transcriptPath: target.transcriptPath,
|
|
252
|
+
sessionId: target.sessionId,
|
|
253
|
+
cwd: target.cwd,
|
|
254
|
+
transcriptIdentity: identity,
|
|
255
|
+
taskArtifactOwnerLocator: targetLocator,
|
|
256
|
+
taskArtifactOwnerDeletionEvidence: evidence,
|
|
257
|
+
...(state !== "prepared" ? { artifactsRemoved: true as const } : {}),
|
|
258
|
+
...(state === "owner_pending" ? { ownerRetirementAttempt: record.ownerRetirementAttempt as number } : {}),
|
|
259
|
+
...(outcome ? { taskArtifactOwnerRetirementOutcome: outcome } : {}),
|
|
260
|
+
...(outcome && outcome.kind !== "completed"
|
|
261
|
+
? { taskArtifactOwnerRetirementContinuation: outcome.continuation }
|
|
262
|
+
: {}),
|
|
263
|
+
...(outcome?.kind === "payload_retired"
|
|
264
|
+
? { taskArtifactOwnerPayloadRetired: true as const, taskArtifactOwnerNamespaceRetained: true as const }
|
|
265
|
+
: {}),
|
|
266
|
+
...(state === "owner_retired" ? { taskArtifactOwnerRetired: true as const } : {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Check only monotone DTO history; this does not establish physical cleanup or current authority. */
|
|
271
|
+
export function continuationExtendsPrevious(
|
|
272
|
+
previous: TaskArtifactOwnerRetirementContinuation,
|
|
273
|
+
next: TaskArtifactOwnerRetirementContinuation,
|
|
274
|
+
): boolean {
|
|
275
|
+
const previousTree = previous.retainedTreeSnapshot;
|
|
276
|
+
const nextTree = next.retainedTreeSnapshot;
|
|
277
|
+
if (
|
|
278
|
+
previous.parentIdentity.dev !== next.parentIdentity.dev ||
|
|
279
|
+
previous.parentIdentity.ino !== next.parentIdentity.ino ||
|
|
280
|
+
previousTree.rootDev !== nextTree.rootDev ||
|
|
281
|
+
previousTree.rootIno !== nextTree.rootIno ||
|
|
282
|
+
nextTree.entries.length > previousTree.entries.length
|
|
283
|
+
)
|
|
284
|
+
return false;
|
|
285
|
+
const previousEntries = new Map(previousTree.entries.map(entry => [entry.relativePath, entry]));
|
|
286
|
+
for (const entry of nextTree.entries) {
|
|
287
|
+
const old = previousEntries.get(entry.relativePath);
|
|
288
|
+
if (
|
|
289
|
+
!old ||
|
|
290
|
+
old.kind !== entry.kind ||
|
|
291
|
+
old.dev !== entry.dev ||
|
|
292
|
+
old.ino !== entry.ino ||
|
|
293
|
+
old.nlink !== entry.nlink
|
|
294
|
+
)
|
|
295
|
+
return false;
|
|
296
|
+
if (
|
|
297
|
+
entry.kind === "file" &&
|
|
298
|
+
!(
|
|
299
|
+
(entry.size === old.size && entry.sha256 === old.sha256) ||
|
|
300
|
+
(entry.size === "0" && entry.sha256 === EMPTY_PAYLOAD_SHA256)
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
return false;
|
|
304
|
+
if (entry.kind !== "file" && entry.size !== old.size) return false;
|
|
305
|
+
}
|
|
306
|
+
for (const key of [
|
|
307
|
+
"detachedPaths",
|
|
308
|
+
"nativeCodes",
|
|
309
|
+
"retainedSuccessorPaths",
|
|
310
|
+
"retainedPlaceholderPaths",
|
|
311
|
+
"retainedUnknownPaths",
|
|
312
|
+
"windowsErrorCodes",
|
|
313
|
+
] as const) {
|
|
314
|
+
const oldValues = previous[key] ?? [];
|
|
315
|
+
const nextValues = next[key] ?? [];
|
|
316
|
+
if (!oldValues.every(item => nextValues.includes(item))) return false;
|
|
317
|
+
}
|
|
318
|
+
return previous.payloadDurable !== true || next.payloadDurable === true;
|
|
319
|
+
}
|