@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
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 } 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
|
}
|
|
@@ -309,26 +332,16 @@ export class ArtifactManager {
|
|
|
309
332
|
* Best-effort removal of a previously published named artifact. Used to roll
|
|
310
333
|
* back staged publications when a transactional operation (e.g. gated
|
|
311
334
|
* maintenance pruning) is rejected after publication succeeded. Returns false
|
|
312
|
-
* when the artifact could not be removed
|
|
313
|
-
*
|
|
335
|
+
* when the exact named artifact is absent or could not be removed; success does
|
|
336
|
+
* not claim that unrelated native cleanup residue was reclaimed.
|
|
314
337
|
*/
|
|
315
338
|
async removeNamedBestEffort(filename: string): Promise<boolean> {
|
|
316
339
|
if (!/^[a-zA-Z0-9_.-]+$/.test(filename)) return false;
|
|
317
340
|
try {
|
|
318
341
|
if (this.#store) {
|
|
319
|
-
const beforePaths = new Set(await fs.readdir(this.#store.dir));
|
|
320
342
|
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
|
-
}
|
|
343
|
+
if (!staged) return false;
|
|
344
|
+
this.#store.removeExpected(filename, staged);
|
|
332
345
|
} else {
|
|
333
346
|
await fs.unlink(path.join(this.#dir, filename));
|
|
334
347
|
}
|
|
@@ -502,7 +515,13 @@ export class ArtifactManager {
|
|
|
502
515
|
publishedNames.push(mappedFilename);
|
|
503
516
|
}
|
|
504
517
|
this.#reservations.set(attemptId, { start, count: ids.length, names: [...publishedNames] });
|
|
505
|
-
|
|
518
|
+
try {
|
|
519
|
+
await staging.discardAttemptStaging();
|
|
520
|
+
} catch (error) {
|
|
521
|
+
const cleanup = classifyAttemptStagingCleanupFailure(error);
|
|
522
|
+
if (cleanup.kind === "authorized_quarantine") return frozenMap;
|
|
523
|
+
throw cleanup.error;
|
|
524
|
+
}
|
|
506
525
|
return frozenMap;
|
|
507
526
|
} catch (error) {
|
|
508
527
|
// Cleanup is best-effort, but a durable removal failure must never be silent: it leaves a
|
|
@@ -550,64 +569,34 @@ export class ArtifactManager {
|
|
|
550
569
|
|
|
551
570
|
async discardAttemptStaging(): Promise<void> {
|
|
552
571
|
if (this.#store) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
try {
|
|
559
|
-
parentBefore = cleanupStore.captureTree(parentCleanupPath);
|
|
560
|
-
} catch {
|
|
561
|
-
parentBefore = undefined;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
572
|
+
// Only an attempt manager owns a managed subtree that it may retire. A
|
|
573
|
+
// normal manager's store is borrowed by this cleanup method and its root
|
|
574
|
+
// must never become a deletion target.
|
|
575
|
+
if (!this.#stagingParentStore || !this.#stagingRelativePath) return;
|
|
576
|
+
const cleanupStore = this.#stagingParentStore;
|
|
564
577
|
try {
|
|
565
|
-
const snapshot = cleanupStore.captureTree(
|
|
566
|
-
|
|
578
|
+
const snapshot = cleanupStore.captureTree(this.#stagingRelativePath);
|
|
579
|
+
const issuedRoot = this.#store.subtreeRootAuthority;
|
|
580
|
+
if (
|
|
581
|
+
issuedRoot.canonicalPath !== path.resolve(this.#dir) ||
|
|
582
|
+
snapshot.rootDev !== issuedRoot.dev.toString() ||
|
|
583
|
+
snapshot.rootIno !== issuedRoot.ino.toString()
|
|
584
|
+
)
|
|
585
|
+
throw new Error("artifact_staging_root_changed");
|
|
586
|
+
// Stop using and release this attempt's retained writable root before
|
|
587
|
+
// asking the parent authority to retire the tree. The exact tree snapshot
|
|
588
|
+
// above is still checked by native removal; a concurrent change therefore
|
|
589
|
+
// remains a refusal rather than being adopted. Never close the borrowed
|
|
590
|
+
// parent store.
|
|
591
|
+
this.#store.close();
|
|
592
|
+
cleanupStore.removeTreeExpected(this.#stagingRelativePath, snapshot);
|
|
567
593
|
} catch (error) {
|
|
568
|
-
if (!(error instanceof Error &&
|
|
569
|
-
|
|
594
|
+
if (!(error instanceof Error && error.message === "not_found")) throw error;
|
|
595
|
+
} finally {
|
|
596
|
+
// Replacement, snapshot, and native cleanup failures also release only
|
|
597
|
+
// the capability owned by this attempt manager.
|
|
598
|
+
this.#store.close();
|
|
570
599
|
}
|
|
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
600
|
} else {
|
|
612
601
|
await fs.rm(this.#dir, { recursive: true, force: true });
|
|
613
602
|
}
|
|
@@ -650,7 +639,69 @@ export class ArtifactManager {
|
|
|
650
639
|
}
|
|
651
640
|
}
|
|
652
641
|
|
|
642
|
+
#captureManagedArtifact(id: string): {
|
|
643
|
+
filename: string;
|
|
644
|
+
size: number;
|
|
645
|
+
expectedDescriptor: Pick<SessionStorageStat, "dev" | "ino" | "nlink" | "size" | "mtimeNs" | "ctimeNs">;
|
|
646
|
+
} | null {
|
|
647
|
+
if (!this.#store) return null;
|
|
648
|
+
try {
|
|
649
|
+
const entry = this.#store
|
|
650
|
+
.captureTree("")
|
|
651
|
+
.entries.find(candidate => candidate.kind === "file" && candidate.relativePath.startsWith(`${id}.`));
|
|
652
|
+
if (!entry) return null;
|
|
653
|
+
const size = Number(BigInt(entry.size));
|
|
654
|
+
if (!Number.isSafeInteger(size) || size < 0) return null;
|
|
655
|
+
return {
|
|
656
|
+
filename: entry.relativePath,
|
|
657
|
+
size,
|
|
658
|
+
expectedDescriptor: {
|
|
659
|
+
dev: BigInt(entry.dev),
|
|
660
|
+
ino: BigInt(entry.ino),
|
|
661
|
+
nlink: BigInt(entry.nlink),
|
|
662
|
+
size,
|
|
663
|
+
mtimeNs: BigInt(entry.mtimeNs),
|
|
664
|
+
ctimeNs: BigInt(entry.ctimeNs),
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
} catch {
|
|
668
|
+
// listFiles historically hides an unavailable managed tree as no artifacts.
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
653
673
|
async readRange(id: string, range: ArtifactByteRange = {}): Promise<string> {
|
|
674
|
+
if (this.#store) {
|
|
675
|
+
const artifact = this.#captureManagedArtifact(id);
|
|
676
|
+
if (!artifact) throw new Error(`artifact://${id} not found`);
|
|
677
|
+
const { start, endExclusive } = normalizeByteRange(artifact.size, range);
|
|
678
|
+
const length = Math.max(0, endExclusive - start);
|
|
679
|
+
let bytes: Uint8Array;
|
|
680
|
+
if (length <= ARTIFACT_RANGE_READ_CHUNK_BYTES) {
|
|
681
|
+
bytes = this.#store.readRangeExpectedSync(
|
|
682
|
+
artifact.filename,
|
|
683
|
+
start,
|
|
684
|
+
length,
|
|
685
|
+
artifact.expectedDescriptor,
|
|
686
|
+
).bytes;
|
|
687
|
+
} else {
|
|
688
|
+
const complete = Buffer.allocUnsafe(length);
|
|
689
|
+
let offset = 0;
|
|
690
|
+
while (offset < length) {
|
|
691
|
+
const chunkLength = Math.min(ARTIFACT_RANGE_READ_CHUNK_BYTES, length - offset);
|
|
692
|
+
const chunk = this.#store.readRangeExpectedSync(
|
|
693
|
+
artifact.filename,
|
|
694
|
+
start + offset,
|
|
695
|
+
chunkLength,
|
|
696
|
+
artifact.expectedDescriptor,
|
|
697
|
+
).bytes;
|
|
698
|
+
complete.set(chunk, offset);
|
|
699
|
+
offset += chunk.byteLength;
|
|
700
|
+
}
|
|
701
|
+
bytes = complete;
|
|
702
|
+
}
|
|
703
|
+
return new TextDecoder().decode(bytes);
|
|
704
|
+
}
|
|
654
705
|
const artifactPath = await this.getPath(id);
|
|
655
706
|
if (!artifactPath) throw new Error(`artifact://${id} not found`);
|
|
656
707
|
const file = Bun.file(artifactPath);
|
|
@@ -661,6 +712,59 @@ export class ArtifactManager {
|
|
|
661
712
|
}
|
|
662
713
|
|
|
663
714
|
async openReadStream(id: string, range: ArtifactByteRange = {}): Promise<ReadableStream<Uint8Array>> {
|
|
715
|
+
if (this.#store) {
|
|
716
|
+
const artifact = this.#captureManagedArtifact(id);
|
|
717
|
+
if (!artifact) throw new Error(`artifact://${id} not found`);
|
|
718
|
+
const { start, endExclusive } = normalizeByteRange(artifact.size, range);
|
|
719
|
+
const lease = this.#store.openReadLease(artifact.filename, artifact.expectedDescriptor);
|
|
720
|
+
let offset = start;
|
|
721
|
+
let closed = false;
|
|
722
|
+
const closeLease = (): void => {
|
|
723
|
+
if (closed) return;
|
|
724
|
+
closed = true;
|
|
725
|
+
lease.close();
|
|
726
|
+
};
|
|
727
|
+
const closeAfterFailure = (): void => {
|
|
728
|
+
try {
|
|
729
|
+
closeLease();
|
|
730
|
+
} catch {
|
|
731
|
+
// Keep the read failure as the stream's observable error.
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
return new ReadableStream<Uint8Array>(
|
|
735
|
+
{
|
|
736
|
+
start: controller => {
|
|
737
|
+
if (offset !== endExclusive) return;
|
|
738
|
+
try {
|
|
739
|
+
closeLease();
|
|
740
|
+
controller.close();
|
|
741
|
+
} catch (error) {
|
|
742
|
+
closeAfterFailure();
|
|
743
|
+
controller.error(error);
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
pull: controller => {
|
|
747
|
+
if (closed) return;
|
|
748
|
+
try {
|
|
749
|
+
const length = Math.min(ARTIFACT_STREAM_CHUNK_BYTES, endExclusive - offset);
|
|
750
|
+
const bytes = lease.readRange(offset, length);
|
|
751
|
+
if (bytes.byteLength !== length) throw new Error("range_not_present");
|
|
752
|
+
offset += bytes.byteLength;
|
|
753
|
+
if (offset === endExclusive) closeLease();
|
|
754
|
+
controller.enqueue(bytes);
|
|
755
|
+
if (offset === endExclusive) controller.close();
|
|
756
|
+
} catch (error) {
|
|
757
|
+
closeAfterFailure();
|
|
758
|
+
controller.error(error);
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
cancel: () => {
|
|
762
|
+
closeLease();
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
{ highWaterMark: 0 },
|
|
766
|
+
);
|
|
767
|
+
}
|
|
664
768
|
const artifactPath = await this.getPath(id);
|
|
665
769
|
if (!artifactPath) throw new Error(`artifact://${id} not found`);
|
|
666
770
|
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
|
+
}
|