@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
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
5
5
|
import { nativeProcessBindings } from "@gajae-code/utils/native-process";
|
|
6
|
-
import { withFileLock } from "../../config/file-lock";
|
|
6
|
+
import { type FileLockOptions, isFileLockAcquireTimeout, withFileLock } from "../../config/file-lock";
|
|
7
7
|
import { repo } from "../../utils/git";
|
|
8
8
|
import { endpointIncarnation } from "./endpoint-authority";
|
|
9
9
|
import { processIncarnation } from "./process-incarnation";
|
|
@@ -653,6 +653,7 @@ function reduceEvents(
|
|
|
653
653
|
now: number,
|
|
654
654
|
agentDir: string,
|
|
655
655
|
probedIncarnations?: ReadonlyMap<string, string | undefined>,
|
|
656
|
+
sessionIds?: ReadonlySet<string>,
|
|
656
657
|
): SessionIndexProjection {
|
|
657
658
|
// Resolved once: the fence-row check runs per root, and only this side of the
|
|
658
659
|
// comparison is ours to normalize.
|
|
@@ -669,7 +670,12 @@ function reduceEvents(
|
|
|
669
670
|
}
|
|
670
671
|
return identity;
|
|
671
672
|
};
|
|
672
|
-
|
|
673
|
+
// Admission and projection are keyed by sessionId (tuple, tombstone, identity),
|
|
674
|
+
// so a scoped read reduces only the requested sessions' events and skips the
|
|
675
|
+
// other sessions' pid/incarnation probes without changing their rows.
|
|
676
|
+
const { admitted } = admitEvents(
|
|
677
|
+
sessionIds === undefined ? events : events.filter(event => sessionIds.has(event.sessionId)),
|
|
678
|
+
);
|
|
673
679
|
const latestByIdentity = new Map<string, SessionIndexEvent>();
|
|
674
680
|
const latestHeartbeatByIdentity = new Map<string, SessionIndexEvent>();
|
|
675
681
|
for (const event of admitted) {
|
|
@@ -797,6 +803,16 @@ const SESSION_INDEX_PROBE_FRESHNESS_MS = 50;
|
|
|
797
803
|
* and the next pass re-probes from scratch.
|
|
798
804
|
*/
|
|
799
805
|
const SESSION_INDEX_REPLAY_FRESHNESS_MS = 2_000;
|
|
806
|
+
/**
|
|
807
|
+
* Fresh re-probe attempts one heartbeat checkpoint pass makes when its
|
|
808
|
+
* observation batch goes stale under lock contention. Each attempt is fully
|
|
809
|
+
* fail-closed; the bound only prevents a contended machine from writing no
|
|
810
|
+
* heartbeat for long enough that live hosts project as dead.
|
|
811
|
+
*/
|
|
812
|
+
const SESSION_HEARTBEAT_CHECKPOINT_ATTEMPTS = 4;
|
|
813
|
+
/** One elapsed budget for queueing, probing and every reacquisition in a pass. */
|
|
814
|
+
const SESSION_HEARTBEAT_CHECKPOINT_BUDGET_MS = 15_000;
|
|
815
|
+
const STALE_OBSERVATION: unique symbol = Symbol("stale-heartbeat-observation");
|
|
800
816
|
/** Maximum distinct live identity probes admitted by one public status query. */
|
|
801
817
|
const SESSION_GENERATION_PROBE_LIMIT = 32;
|
|
802
818
|
|
|
@@ -807,10 +823,16 @@ const SESSION_GENERATION_PROBE_LIMIT = 32;
|
|
|
807
823
|
* observation window and (b) the lock budget cannot silently drift per call
|
|
808
824
|
* site.
|
|
809
825
|
*/
|
|
810
|
-
function withSessionIndexLock<T>(
|
|
826
|
+
function withSessionIndexLock<T>(
|
|
827
|
+
operation: string,
|
|
828
|
+
agentDir: string,
|
|
829
|
+
callback: () => Promise<T>,
|
|
830
|
+
options: FileLockOptions = SESSION_INDEX_LOCK_OPTIONS,
|
|
831
|
+
): Promise<T> {
|
|
811
832
|
return withFileLock(
|
|
812
833
|
logFor(agentDir),
|
|
813
834
|
async () => {
|
|
835
|
+
options.signal?.throwIfAborted();
|
|
814
836
|
// Armed only once the lock is actually held: queueing time behind another
|
|
815
837
|
// legitimate holder must not be attributed to this operation.
|
|
816
838
|
const note = setTimeout(
|
|
@@ -826,7 +848,7 @@ function withSessionIndexLock<T>(operation: string, agentDir: string, callback:
|
|
|
826
848
|
clearTimeout(note);
|
|
827
849
|
}
|
|
828
850
|
},
|
|
829
|
-
|
|
851
|
+
options,
|
|
830
852
|
);
|
|
831
853
|
}
|
|
832
854
|
function isValidSnapshot(snapshot: unknown): snapshot is { indexSeq: number; events: SessionIndexEvent[] } {
|
|
@@ -1195,32 +1217,39 @@ export class SessionIndex {
|
|
|
1195
1217
|
* stamp of the last completed locked pass, the in-memory projection is
|
|
1196
1218
|
* already current and the locked rescan is skipped entirely — this is what
|
|
1197
1219
|
* keeps an idle SessionRouter reconcile (2s cadence) from re-parsing and
|
|
1198
|
-
* re-checksumming the whole index forever.
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1220
|
+
* re-checksumming the whole index forever. The unchanged check runs before
|
|
1221
|
+
* the local queue so unrelated heartbeat work cannot delay ordinary creates.
|
|
1222
|
+
* Admission that requires serialized authority must use refresh() instead.
|
|
1223
|
+
* An append committed before the stat always changes
|
|
1224
|
+
* the stamp; a change landing after the stat is seen on the next poll, the
|
|
1225
|
+
* same TOCTOU envelope a locked read has. A corrupt suffix never takes the fast path: re-scanning
|
|
1202
1226
|
* preserves the existing re-diagnosis behavior. Returns true when state was
|
|
1203
1227
|
* reloaded. Authority revalidation that needs the strongest available
|
|
1204
1228
|
* snapshot inside an already-locked write (append, unregister) keeps using
|
|
1205
1229
|
* the exact locked paths.
|
|
1206
1230
|
*/
|
|
1207
1231
|
async refreshIfChanged(): Promise<boolean> {
|
|
1208
|
-
if (this.#changeStamp
|
|
1232
|
+
if (this.#changeStamp === undefined || this.#corruptSuffix) {
|
|
1233
|
+
await this.open();
|
|
1234
|
+
await this.refresh();
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
const stamp = await readIndexChangeStamp(this.#agentDir);
|
|
1238
|
+
if (sameIndexChangeStamp(this.#changeStamp, stamp)) return false;
|
|
1239
|
+
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
1240
|
+
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
1241
|
+
if (this.#changeStamp === undefined || this.#corruptSuffix) {
|
|
1242
|
+
await withSessionIndexLock("poll-refresh", this.#agentDir, () => this.#refreshUnderLock());
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1209
1245
|
const stamp = await readIndexChangeStamp(this.#agentDir);
|
|
1210
1246
|
if (sameIndexChangeStamp(this.#changeStamp, stamp)) return false;
|
|
1211
1247
|
// A possible change is re-classified UNDER the lock (#4689 review):
|
|
1212
1248
|
// writers mutate these files only while holding it, so the locked
|
|
1213
|
-
// observation is atomic with the tail/replay it selects.
|
|
1214
|
-
|
|
1215
|
-
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
1216
|
-
await SessionIndex.#enqueue(indexPath, () =>
|
|
1217
|
-
withSessionIndexLock("poll-refresh", this.#agentDir, () => this.#refreshOrReplayUnderLock()),
|
|
1218
|
-
);
|
|
1249
|
+
// observation is atomic with the tail/replay it selects.
|
|
1250
|
+
await withSessionIndexLock("poll-refresh", this.#agentDir, () => this.#refreshOrReplayUnderLock());
|
|
1219
1251
|
return true;
|
|
1220
|
-
}
|
|
1221
|
-
await this.open();
|
|
1222
|
-
await this.refresh();
|
|
1223
|
-
return true;
|
|
1252
|
+
});
|
|
1224
1253
|
}
|
|
1225
1254
|
/**
|
|
1226
1255
|
* Locked change classification (#4689). Append-only log growth with an
|
|
@@ -1327,6 +1356,10 @@ export class SessionIndex {
|
|
|
1327
1356
|
}
|
|
1328
1357
|
/** Seed the audit dedupe set once, then append records for newly-rejected events. */
|
|
1329
1358
|
async #writeAuditUnderLock(): Promise<void> {
|
|
1359
|
+
const candidates = auditRecords(this.#events, this.#policy.clock());
|
|
1360
|
+
// The audit log only dedupes rejections, so a reader with none never pays
|
|
1361
|
+
// for loading it.
|
|
1362
|
+
if (candidates.length === 0) return;
|
|
1330
1363
|
if (this.#auditedSeq === null) {
|
|
1331
1364
|
this.#auditedSeq = new Set();
|
|
1332
1365
|
try {
|
|
@@ -1344,9 +1377,7 @@ export class SessionIndex {
|
|
|
1344
1377
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1345
1378
|
}
|
|
1346
1379
|
}
|
|
1347
|
-
const pending =
|
|
1348
|
-
record => !this.#auditedSeq!.has(record.indexSeq),
|
|
1349
|
-
);
|
|
1380
|
+
const pending = candidates.filter(record => !this.#auditedSeq!.has(record.indexSeq));
|
|
1350
1381
|
if (pending.length === 0) return;
|
|
1351
1382
|
await appendSync(auditFor(this.#agentDir), pending.map(record => JSON.stringify(record)).join("\n"));
|
|
1352
1383
|
for (const record of pending) this.#auditedSeq.add(record.indexSeq);
|
|
@@ -1956,10 +1987,18 @@ export class SessionIndex {
|
|
|
1956
1987
|
this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
|
|
1957
1988
|
}
|
|
1958
1989
|
|
|
1959
|
-
|
|
1990
|
+
/**
|
|
1991
|
+
* Public session rows. `sessionIds` scopes the projection to those sessions:
|
|
1992
|
+
* rows for them are identical to the unscoped read, other sessions are absent.
|
|
1993
|
+
*/
|
|
1994
|
+
listSessions(
|
|
1995
|
+
probedIncarnations?: ReadonlyMap<string, string | undefined>,
|
|
1996
|
+
sessionIds?: ReadonlySet<string>,
|
|
1997
|
+
): SessionList {
|
|
1960
1998
|
return {
|
|
1961
1999
|
indexSeq: this.indexSeq,
|
|
1962
|
-
sessions: reduceEvents(this.#events, this.#policy.clock(), this.#agentDir, probedIncarnations)
|
|
2000
|
+
sessions: reduceEvents(this.#events, this.#policy.clock(), this.#agentDir, probedIncarnations, sessionIds)
|
|
2001
|
+
.sessions,
|
|
1963
2002
|
warnings: this.#warnings,
|
|
1964
2003
|
};
|
|
1965
2004
|
}
|
|
@@ -2334,9 +2373,64 @@ export class SessionIndex {
|
|
|
2334
2373
|
* while the broker was down keep their stale or missing heartbeat and read as
|
|
2335
2374
|
* unknown/not-live (never fresh forever). Returns the number of checkpoints written.
|
|
2336
2375
|
*/
|
|
2337
|
-
async checkpointLiveHeartbeats(
|
|
2376
|
+
async checkpointLiveHeartbeats(
|
|
2377
|
+
now = Date.now(),
|
|
2378
|
+
abortSignal?: AbortSignal,
|
|
2379
|
+
deadlineAt = performance.now() + SESSION_HEARTBEAT_CHECKPOINT_BUDGET_MS,
|
|
2380
|
+
): Promise<number> {
|
|
2381
|
+
// A stale observation batch fails closed (no heartbeat), but on a busy
|
|
2382
|
+
// machine a single contended lock acquisition is enough to discard the
|
|
2383
|
+
// whole cycle. With the broker's 5s cadence repeatedly losing that race,
|
|
2384
|
+
// every live host's heartbeat aged past the 2-interval freshness window
|
|
2385
|
+
// and the router retired attached sessions as "removed" mid-prompt.
|
|
2386
|
+
// Re-probe from scratch a bounded number of times inside this pass: each
|
|
2387
|
+
// attempt keeps the exact fail-closed contract (fresh probes, freshness
|
|
2388
|
+
// bounds re-checked under the lock); only the starvation is removed.
|
|
2389
|
+
const deadline = Math.min(deadlineAt, performance.now() + SESSION_HEARTBEAT_CHECKPOINT_BUDGET_MS);
|
|
2390
|
+
if (abortSignal?.aborted || performance.now() >= deadline) return 0;
|
|
2391
|
+
const budget = new AbortController();
|
|
2392
|
+
const signal = abortSignal ? AbortSignal.any([abortSignal, budget.signal]) : budget.signal;
|
|
2393
|
+
const timer = setTimeout(() => budget.abort(), deadline - performance.now());
|
|
2394
|
+
timer.unref?.();
|
|
2395
|
+
const checkpoint = async (): Promise<number> => {
|
|
2396
|
+
for (let attempt = 0; ; attempt++) {
|
|
2397
|
+
if (signal.aborted || performance.now() >= deadline) return 0;
|
|
2398
|
+
const result = await this.#checkpointLiveHeartbeatsOnce(now, signal, deadline);
|
|
2399
|
+
if (result !== STALE_OBSERVATION || attempt + 1 >= SESSION_HEARTBEAT_CHECKPOINT_ATTEMPTS)
|
|
2400
|
+
return result === STALE_OBSERVATION ? 0 : result;
|
|
2401
|
+
now = Date.now();
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
try {
|
|
2405
|
+
// The deadline aborts queued work and prevents a late callback from
|
|
2406
|
+
// entering its write section, but it cannot cancel an append/fsync that
|
|
2407
|
+
// has already started. Keep the caller tied to the lock-owning operation
|
|
2408
|
+
// so startup readiness never overtakes lock release.
|
|
2409
|
+
return await checkpoint();
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
if (signal.aborted && error === signal.reason) return 0;
|
|
2412
|
+
if (
|
|
2413
|
+
performance.now() >= deadline &&
|
|
2414
|
+
isFileLockAcquireTimeout(error) &&
|
|
2415
|
+
!error.orphanPath &&
|
|
2416
|
+
!error.removalFailure
|
|
2417
|
+
)
|
|
2418
|
+
return 0;
|
|
2419
|
+
throw error;
|
|
2420
|
+
} finally {
|
|
2421
|
+
clearTimeout(timer);
|
|
2422
|
+
budget.abort();
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
async #checkpointLiveHeartbeatsOnce(
|
|
2427
|
+
now: number,
|
|
2428
|
+
signal: AbortSignal,
|
|
2429
|
+
deadline: number,
|
|
2430
|
+
): Promise<number | typeof STALE_OBSERVATION> {
|
|
2338
2431
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
2339
2432
|
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
2433
|
+
if (signal.aborted || performance.now() >= deadline) return 0;
|
|
2340
2434
|
// An absent index holds no registration to checkpoint, so this pass must read
|
|
2341
2435
|
// it as "nothing to do" instead of creating one. Recreating the directory
|
|
2342
2436
|
// resurrects a state root its owner already retired: the broker's 5s
|
|
@@ -2371,62 +2465,78 @@ export class SessionIndex {
|
|
|
2371
2465
|
probed.set(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`, processIncarnation(row.pid));
|
|
2372
2466
|
}
|
|
2373
2467
|
const probedAt = performance.now();
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
if (
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
const
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2468
|
+
if (signal.aborted || probedAt >= deadline) return 0;
|
|
2469
|
+
return await withSessionIndexLock(
|
|
2470
|
+
"heartbeat checkpoint",
|
|
2471
|
+
this.#agentDir,
|
|
2472
|
+
async () => {
|
|
2473
|
+
if (signal.aborted || performance.now() >= deadline) return 0;
|
|
2474
|
+
// Contended or delayed acquisition invalidates the observation set:
|
|
2475
|
+
// never trust identity evidence gathered meaningfully before the
|
|
2476
|
+
// lock was held. The checkpoint is skipped this cycle (fail-closed);
|
|
2477
|
+
// the OS is never probed while the machine-global lock is held. The
|
|
2478
|
+
// bound tolerates scheduler jitter on an uncontended acquisition —
|
|
2479
|
+
// which is the only path where the evidence is trustworthy.
|
|
2480
|
+
if (performance.now() - probedAt > SESSION_INDEX_PROBE_FRESHNESS_MS) return STALE_OBSERVATION;
|
|
2481
|
+
await this.#replayUnderLock();
|
|
2482
|
+
if (signal.aborted || performance.now() >= deadline) return 0;
|
|
2483
|
+
// Recheck AFTER the awaited replay too (#4544 review round 4): the
|
|
2484
|
+
// replay re-reads the whole log (up to the 4 MiB rotation bound) and
|
|
2485
|
+
// fsyncs pending audit rows, so the probe→write interval can stretch
|
|
2486
|
+
// well past the acquisition bound above while the lock is held. The
|
|
2487
|
+
// same pid-reuse window exists across that awaited replay; consuming
|
|
2488
|
+
// the cached observation then would checkpoint the replacement
|
|
2489
|
+
// process as the dead row. The replay bound covers the full locked
|
|
2490
|
+
// replay cost envelope of a healthy machine; anything slower fails
|
|
2491
|
+
// closed (no heartbeat this cycle) and the next pass re-probes.
|
|
2492
|
+
if (performance.now() - probedAt > SESSION_INDEX_REPLAY_FRESHNESS_MS) return STALE_OBSERVATION;
|
|
2493
|
+
if (this.#corruptSuffix) return 0;
|
|
2494
|
+
const events: SessionIndexEvent[] = [];
|
|
2495
|
+
const rows = reduceEvents(this.#events, now, this.#agentDir, probed).sessions;
|
|
2496
|
+
for (const row of rows) {
|
|
2497
|
+
if (!isSessionAuthorityEligible(row) || row.terminal || row.terminalUncertain) continue;
|
|
2498
|
+
if (row.lastHeartbeatAt !== undefined && now - row.lastHeartbeatAt < SESSION_HEARTBEAT_INTERVAL_MS)
|
|
2499
|
+
continue;
|
|
2500
|
+
if (!alive(row.pid)) continue;
|
|
2501
|
+
const recordedIncarnation = row.hostIncarnation ?? row.processIncarnation;
|
|
2502
|
+
if (recordedIncarnation === undefined) continue;
|
|
2503
|
+
const current = probed.get(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`);
|
|
2504
|
+
if (current === undefined || current !== recordedIncarnation) continue;
|
|
2505
|
+
const unsigned: Omit<SessionIndexEvent, "checksum"> = {
|
|
2506
|
+
version: SESSION_INDEX_EVENT_VERSION,
|
|
2507
|
+
indexSeq: this.indexSeq + events.length + 1,
|
|
2508
|
+
type: "host_heartbeat",
|
|
2509
|
+
sessionId: row.sessionId,
|
|
2510
|
+
locator: row.locator,
|
|
2511
|
+
endpointGeneration: row.endpointGeneration,
|
|
2512
|
+
pid: row.pid,
|
|
2513
|
+
...(row.processIncarnation === undefined ? {} : { processIncarnation: row.processIncarnation }),
|
|
2514
|
+
...(row.hostIncarnation === undefined ? {} : { hostIncarnation: row.hostIncarnation }),
|
|
2515
|
+
...(row.masterRole === undefined ? {} : { masterRole: row.masterRole }),
|
|
2516
|
+
// Preserve the host's last observed activity state while this
|
|
2517
|
+
// broker-owned heartbeat only renews liveness. A live idle host
|
|
2518
|
+
// must not be rewritten as active merely because its pid is alive.
|
|
2519
|
+
activity: row.activity ?? { state: "active", at: now },
|
|
2520
|
+
ts: now,
|
|
2521
|
+
};
|
|
2522
|
+
events.push({ ...unsigned, checksum: sessionIndexChecksum(unsigned) });
|
|
2523
|
+
}
|
|
2524
|
+
if (events.length === 0) return 0;
|
|
2525
|
+
if (signal.aborted || performance.now() >= deadline) return 0;
|
|
2526
|
+
for (const event of events) await appendSync(logFor(this.#agentDir), JSON.stringify(event));
|
|
2527
|
+
await this.#refreshUnderLock();
|
|
2528
|
+
if ((await fs.stat(logFor(this.#agentDir))).size >= ROTATE_BYTES) await this.#rotate();
|
|
2529
|
+
return events.length;
|
|
2530
|
+
},
|
|
2531
|
+
{
|
|
2532
|
+
...SESSION_INDEX_LOCK_OPTIONS,
|
|
2533
|
+
retries: Math.min(
|
|
2534
|
+
SESSION_INDEX_LOCK_OPTIONS.retries,
|
|
2535
|
+
Math.max(1, Math.ceil((deadline - performance.now()) / SESSION_INDEX_LOCK_OPTIONS.retryDelayMs)),
|
|
2536
|
+
),
|
|
2537
|
+
signal,
|
|
2538
|
+
},
|
|
2539
|
+
);
|
|
2430
2540
|
});
|
|
2431
2541
|
}
|
|
2432
2542
|
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { NativeExactUnlinkResult } from "@gajae-code/natives";
|
|
2
|
+
import {
|
|
3
|
+
assertSessionRoot,
|
|
4
|
+
parseTaskArtifactOwnerDeletionEvidence,
|
|
5
|
+
parseTaskArtifactOwnerRetirementContinuation,
|
|
6
|
+
parseTaskArtifactOwnerRetirementOutcome,
|
|
7
|
+
type TaskArtifactOwnerDeletionEvidence,
|
|
8
|
+
type TaskArtifactOwnerRetirementContinuation,
|
|
9
|
+
type TaskArtifactOwnerRetirementOutcome,
|
|
10
|
+
type TaskArtifactOwnerStorageContext,
|
|
11
|
+
} from "../../session/task-artifact-owner-codec";
|
|
12
|
+
|
|
13
|
+
export type BrokerCleanupPhase = "artifacts" | "transcript" | "metadata" | "lifecycle";
|
|
14
|
+
|
|
15
|
+
/** Exact owner-disposition wire shape. Evidence and continuation are supplied separately by cleanup replay. */
|
|
16
|
+
export type BrokerTaskArtifactOwnerRetirementDisposition =
|
|
17
|
+
| { readonly kind: "completed"; readonly nativeOutcome?: NativeExactUnlinkResult }
|
|
18
|
+
| {
|
|
19
|
+
readonly kind: "payload_retired";
|
|
20
|
+
readonly namespace: "retained";
|
|
21
|
+
readonly nativeOutcome: NativeExactUnlinkResult;
|
|
22
|
+
}
|
|
23
|
+
| { readonly kind: "cleanup_pending"; readonly nativeOutcome?: NativeExactUnlinkResult }
|
|
24
|
+
| { readonly kind: "uncertain"; readonly reason: string; readonly nativeOutcome?: NativeExactUnlinkResult };
|
|
25
|
+
|
|
26
|
+
/** Owner-only projection of durable cleanup state accepted by the pure validator. */
|
|
27
|
+
export interface BrokerTaskArtifactOwnerCleanupFields {
|
|
28
|
+
readonly taskArtifactOwnerDeletionEvidence?: unknown;
|
|
29
|
+
readonly taskArtifactOwnerRetirementContinuation?: unknown;
|
|
30
|
+
readonly taskArtifactOwnerRetirementOutcome?: unknown;
|
|
31
|
+
readonly taskArtifactOwnerPayloadRetired?: unknown;
|
|
32
|
+
readonly taskArtifactOwnerNamespaceRetained?: unknown;
|
|
33
|
+
readonly taskArtifactOwnerRetired?: unknown;
|
|
34
|
+
readonly taskArtifactOwnerTranscriptDeleted?: unknown;
|
|
35
|
+
readonly taskArtifactOwnerCleanupError?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Fully decoded owner-only cleanup state for an authorized session and phase. */
|
|
39
|
+
export interface BrokerTaskArtifactOwnerCleanupValidation {
|
|
40
|
+
readonly deletionEvidence?: TaskArtifactOwnerDeletionEvidence;
|
|
41
|
+
readonly retirementContinuation?: TaskArtifactOwnerRetirementContinuation;
|
|
42
|
+
readonly retirementOutcome?: TaskArtifactOwnerRetirementOutcome;
|
|
43
|
+
readonly payloadRetired?: true;
|
|
44
|
+
readonly namespaceRetained?: true;
|
|
45
|
+
readonly retired?: true;
|
|
46
|
+
readonly transcriptDeleted?: true;
|
|
47
|
+
readonly cleanupDiagnostic?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
55
|
+
return Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function invalidDisposition(): never {
|
|
59
|
+
throw new Error("task_artifact_owner_retirement_disposition_invalid");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Decode the owner-only wire disposition using its exact trusted evidence and continuation.
|
|
64
|
+
* A decoded `completed` value is historical DTO data, not proof of physical retirement or writer closure.
|
|
65
|
+
*/
|
|
66
|
+
export function parseBrokerTaskArtifactOwnerRetirementDisposition(
|
|
67
|
+
value: unknown,
|
|
68
|
+
context: TaskArtifactOwnerStorageContext,
|
|
69
|
+
evidence: TaskArtifactOwnerDeletionEvidence,
|
|
70
|
+
continuation: TaskArtifactOwnerRetirementContinuation | undefined,
|
|
71
|
+
): TaskArtifactOwnerRetirementOutcome {
|
|
72
|
+
assertSessionRoot(context);
|
|
73
|
+
if (!isRecord(value)) return invalidDisposition();
|
|
74
|
+
const kind = value.kind;
|
|
75
|
+
let expectedKeys: readonly string[];
|
|
76
|
+
switch (kind) {
|
|
77
|
+
case "completed":
|
|
78
|
+
case "cleanup_pending":
|
|
79
|
+
expectedKeys = Object.hasOwn(value, "nativeOutcome") ? ["kind", "nativeOutcome"] : ["kind"];
|
|
80
|
+
break;
|
|
81
|
+
case "payload_retired":
|
|
82
|
+
expectedKeys = ["kind", "namespace", "nativeOutcome"];
|
|
83
|
+
break;
|
|
84
|
+
case "uncertain":
|
|
85
|
+
expectedKeys = Object.hasOwn(value, "nativeOutcome")
|
|
86
|
+
? ["kind", "reason", "nativeOutcome"]
|
|
87
|
+
: ["kind", "reason"];
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
return invalidDisposition();
|
|
91
|
+
}
|
|
92
|
+
if (!hasExactKeys(value, expectedKeys)) return invalidDisposition();
|
|
93
|
+
const reconstructed = {
|
|
94
|
+
...value,
|
|
95
|
+
evidence,
|
|
96
|
+
...(continuation !== undefined ? { continuation } : {}),
|
|
97
|
+
};
|
|
98
|
+
return parseTaskArtifactOwnerRetirementOutcome(context, evidence, reconstructed);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function serializeNativeOutcome(outcome: NativeExactUnlinkResult): NativeExactUnlinkResult {
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
ok: outcome.ok,
|
|
104
|
+
...(outcome.code !== undefined ? { code: outcome.code } : {}),
|
|
105
|
+
...(outcome.detachedPath !== undefined ? { detachedPath: outcome.detachedPath } : {}),
|
|
106
|
+
...(outcome.retainedSuccessorPath !== undefined ? { retainedSuccessorPath: outcome.retainedSuccessorPath } : {}),
|
|
107
|
+
...(outcome.retainedPlaceholderPath !== undefined
|
|
108
|
+
? { retainedPlaceholderPath: outcome.retainedPlaceholderPath }
|
|
109
|
+
: {}),
|
|
110
|
+
...(outcome.retainedUnknownPath !== undefined ? { retainedUnknownPath: outcome.retainedUnknownPath } : {}),
|
|
111
|
+
...(outcome.payloadDurable !== undefined ? { payloadDurable: outcome.payloadDurable } : {}),
|
|
112
|
+
...(outcome.windowsErrorCode !== undefined ? { windowsErrorCode: outcome.windowsErrorCode } : {}),
|
|
113
|
+
}) as NativeExactUnlinkResult;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Project an already validated internal outcome to the exact broker wire disposition. */
|
|
117
|
+
export function serializeBrokerTaskArtifactOwnerRetirementDisposition(
|
|
118
|
+
outcome: TaskArtifactOwnerRetirementOutcome,
|
|
119
|
+
): BrokerTaskArtifactOwnerRetirementDisposition {
|
|
120
|
+
switch (outcome.kind) {
|
|
121
|
+
case "completed":
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
kind: outcome.kind,
|
|
124
|
+
...(outcome.nativeOutcome ? { nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome) } : {}),
|
|
125
|
+
});
|
|
126
|
+
case "payload_retired":
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
kind: outcome.kind,
|
|
129
|
+
namespace: outcome.namespace,
|
|
130
|
+
nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome),
|
|
131
|
+
});
|
|
132
|
+
case "cleanup_pending":
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
kind: outcome.kind,
|
|
135
|
+
...(outcome.nativeOutcome ? { nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome) } : {}),
|
|
136
|
+
});
|
|
137
|
+
case "uncertain":
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
kind: outcome.kind,
|
|
140
|
+
reason: outcome.reason,
|
|
141
|
+
...(outcome.nativeOutcome ? { nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome) } : {}),
|
|
142
|
+
});
|
|
143
|
+
default: {
|
|
144
|
+
const exhaustive: never = outcome;
|
|
145
|
+
throw new Error(`task_artifact_owner_retirement_outcome_unknown:${String(exhaustive)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const OWNER_CLEANUP_KEYS = new Set([
|
|
151
|
+
"taskArtifactOwnerDeletionEvidence",
|
|
152
|
+
"taskArtifactOwnerRetirementContinuation",
|
|
153
|
+
"taskArtifactOwnerRetirementOutcome",
|
|
154
|
+
"taskArtifactOwnerPayloadRetired",
|
|
155
|
+
"taskArtifactOwnerNamespaceRetained",
|
|
156
|
+
"taskArtifactOwnerRetired",
|
|
157
|
+
"taskArtifactOwnerTranscriptDeleted",
|
|
158
|
+
"taskArtifactOwnerCleanupError",
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Strictly decode the owner-field projection of cleanup state against trusted session, scope context, phase,
|
|
163
|
+
* and artifact-removal state. Empty/legacy owner fields decode to an empty result. A decoded completion
|
|
164
|
+
* remains historical data: any later effect must separately verify current physical absence, fresh scrub
|
|
165
|
+
* proof, native success and current owner-parent/side-path authority before it mutates storage or transcripts.
|
|
166
|
+
*/
|
|
167
|
+
export function decodeBrokerTaskArtifactOwnerCleanupFields(
|
|
168
|
+
value: unknown,
|
|
169
|
+
context: TaskArtifactOwnerStorageContext,
|
|
170
|
+
expectedSessionId: string,
|
|
171
|
+
phase: BrokerCleanupPhase,
|
|
172
|
+
artifactsRemoved: boolean | undefined,
|
|
173
|
+
): BrokerTaskArtifactOwnerCleanupValidation {
|
|
174
|
+
assertSessionRoot(context);
|
|
175
|
+
if (!isRecord(value) || Object.keys(value).some(key => !OWNER_CLEANUP_KEYS.has(key)))
|
|
176
|
+
throw new Error("task_artifact_owner_cleanup_fields_invalid");
|
|
177
|
+
if (expectedSessionId.length === 0 || (artifactsRemoved !== undefined && typeof artifactsRemoved !== "boolean"))
|
|
178
|
+
throw new Error("task_artifact_owner_cleanup_fields_invalid");
|
|
179
|
+
|
|
180
|
+
const has = (key: string) => Object.hasOwn(value, key);
|
|
181
|
+
const trueFlag = (key: string): true | undefined => {
|
|
182
|
+
if (!has(key)) return undefined;
|
|
183
|
+
if (value[key] !== true) throw new Error("task_artifact_owner_cleanup_flags_invalid");
|
|
184
|
+
return true;
|
|
185
|
+
};
|
|
186
|
+
let deletionEvidence: TaskArtifactOwnerDeletionEvidence | undefined;
|
|
187
|
+
if (has("taskArtifactOwnerDeletionEvidence")) {
|
|
188
|
+
deletionEvidence = parseTaskArtifactOwnerDeletionEvidence(value.taskArtifactOwnerDeletionEvidence);
|
|
189
|
+
if (deletionEvidence.sessionId !== expectedSessionId)
|
|
190
|
+
throw new Error("task_artifact_owner_cleanup_session_mismatch");
|
|
191
|
+
}
|
|
192
|
+
let retirementContinuation: TaskArtifactOwnerRetirementContinuation | undefined;
|
|
193
|
+
if (has("taskArtifactOwnerRetirementContinuation")) {
|
|
194
|
+
if (!deletionEvidence) throw new Error("task_artifact_owner_cleanup_evidence_missing");
|
|
195
|
+
retirementContinuation = parseTaskArtifactOwnerRetirementContinuation(
|
|
196
|
+
context,
|
|
197
|
+
deletionEvidence,
|
|
198
|
+
value.taskArtifactOwnerRetirementContinuation,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
let retirementOutcome: TaskArtifactOwnerRetirementOutcome | undefined;
|
|
202
|
+
if (has("taskArtifactOwnerRetirementOutcome")) {
|
|
203
|
+
if (!deletionEvidence) throw new Error("task_artifact_owner_cleanup_evidence_missing");
|
|
204
|
+
retirementOutcome = parseBrokerTaskArtifactOwnerRetirementDisposition(
|
|
205
|
+
value.taskArtifactOwnerRetirementOutcome,
|
|
206
|
+
context,
|
|
207
|
+
deletionEvidence,
|
|
208
|
+
retirementContinuation,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const payloadRetired = trueFlag("taskArtifactOwnerPayloadRetired");
|
|
213
|
+
const namespaceRetained = trueFlag("taskArtifactOwnerNamespaceRetained");
|
|
214
|
+
const retired = trueFlag("taskArtifactOwnerRetired");
|
|
215
|
+
const transcriptDeleted = trueFlag("taskArtifactOwnerTranscriptDeleted");
|
|
216
|
+
let cleanupDiagnostic: string | undefined;
|
|
217
|
+
if (has("taskArtifactOwnerCleanupError")) {
|
|
218
|
+
const diagnostic = value.taskArtifactOwnerCleanupError;
|
|
219
|
+
if (typeof diagnostic !== "string" || diagnostic.length === 0 || diagnostic.length > 4096)
|
|
220
|
+
throw new Error("task_artifact_owner_cleanup_diagnostic_invalid");
|
|
221
|
+
cleanupDiagnostic = diagnostic;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (
|
|
225
|
+
(retired !== undefined && (payloadRetired !== undefined || namespaceRetained !== undefined)) ||
|
|
226
|
+
(retired !== undefined && retirementOutcome?.kind !== "completed") ||
|
|
227
|
+
(payloadRetired !== undefined) !== (namespaceRetained !== undefined) ||
|
|
228
|
+
((retired !== undefined ||
|
|
229
|
+
payloadRetired !== undefined ||
|
|
230
|
+
namespaceRetained !== undefined ||
|
|
231
|
+
transcriptDeleted !== undefined) &&
|
|
232
|
+
(!deletionEvidence || !retirementOutcome)) ||
|
|
233
|
+
(transcriptDeleted !== undefined &&
|
|
234
|
+
(phase !== "artifacts" || artifactsRemoved !== true || retirementOutcome === undefined))
|
|
235
|
+
)
|
|
236
|
+
throw new Error("task_artifact_owner_cleanup_state_invalid");
|
|
237
|
+
|
|
238
|
+
if (retirementOutcome) {
|
|
239
|
+
if (
|
|
240
|
+
(retired !== undefined && retirementOutcome.kind !== "completed") ||
|
|
241
|
+
(retirementOutcome.kind === "completed" && retired === undefined) ||
|
|
242
|
+
(payloadRetired !== undefined &&
|
|
243
|
+
(retirementOutcome.kind !== "payload_retired" || retirementContinuation === undefined)) ||
|
|
244
|
+
(retirementOutcome.kind === "payload_retired" && payloadRetired === undefined)
|
|
245
|
+
)
|
|
246
|
+
throw new Error("task_artifact_owner_cleanup_disposition_mismatch");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return Object.freeze({
|
|
250
|
+
deletionEvidence,
|
|
251
|
+
retirementContinuation,
|
|
252
|
+
retirementOutcome,
|
|
253
|
+
payloadRetired,
|
|
254
|
+
namespaceRetained,
|
|
255
|
+
retired,
|
|
256
|
+
transcriptDeleted,
|
|
257
|
+
cleanupDiagnostic,
|
|
258
|
+
});
|
|
259
|
+
}
|