@duckmind/dm-windows-x64 0.61.4 → 0.61.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dm.exe +0 -0
- package/extensions/.dm-extensions.json +211 -67
- package/extensions/dm-subagents/agents/claude-code-writer.md +15 -0
- package/extensions/dm-subagents/agents/claude-code.md +15 -0
- package/extensions/dm-subagents/agents/codex-exec-writer.md +15 -0
- package/extensions/dm-subagents/agents/codex-exec.md +15 -0
- package/extensions/dm-subagents/agents/cursor-agent-writer.md +14 -0
- package/extensions/dm-subagents/agents/cursor-agent.md +14 -0
- package/extensions/dm-subagents/agents/delegate.md +3 -2
- package/extensions/dm-subagents/agents/oracle.md +10 -5
- package/extensions/dm-subagents/agents/researcher.md +2 -2
- package/extensions/dm-subagents/agents/reviewer.md +17 -7
- package/extensions/dm-subagents/agents/scout.md +5 -5
- package/extensions/dm-subagents/agents/worker.md +6 -2
- package/extensions/dm-subagents/async-retention-discovery-worker.mjs +167 -0
- package/extensions/dm-subagents/index.js +4 -0
- package/extensions/dm-subagents/inspector-runner.mjs +10 -0
- package/extensions/dm-subagents/install.mjs +3 -2
- package/extensions/dm-subagents/package.json +2 -2
- package/extensions/dm-subagents/prompts/council.md +60 -0
- package/extensions/dm-subagents/prompts/parallel-review.md +5 -1
- package/extensions/dm-subagents/prompts/review-loop.md +13 -7
- package/extensions/dm-subagents/skills/council-mode/SKILL.md +59 -0
- package/extensions/dm-subagents/skills/council-mode/references/pass-contracts.md +150 -0
- package/extensions/dm-subagents/skills/dm-subagents/SKILL.md +96 -911
- package/extensions/dm-subagents/skills/dm-subagents/references/constraints-and-recipes.md +70 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/execution-controls.md +539 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/management-authoring-rpc.md +161 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/multi-lane-orchestration.md +51 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/prompting-and-roles.md +295 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/review-and-validation.md +73 -0
- package/extensions/dm-subagents/src/agents/agent-management.js +716 -484
- package/extensions/dm-subagents/src/agents/agent-refinements.js +563 -0
- package/extensions/dm-subagents/src/agents/agent-serializer.js +70 -5
- package/extensions/dm-subagents/src/agents/agents.js +1447 -299
- package/extensions/dm-subagents/src/agents/builtin-names.js +15 -0
- package/extensions/dm-subagents/src/agents/chain-serializer.js +12 -7
- package/extensions/dm-subagents/src/agents/frontmatter.js +64 -14
- package/extensions/dm-subagents/src/agents/identity.js +1 -1
- package/extensions/dm-subagents/src/agents/proactive-skills.js +14 -11
- package/extensions/dm-subagents/src/agents/runtime-agent-events.js +49 -0
- package/extensions/dm-subagents/src/agents/runtime-agent-registry.js +412 -0
- package/extensions/dm-subagents/src/agents/skills.js +52 -35
- package/extensions/dm-subagents/src/api/agents.js +6 -0
- package/extensions/dm-subagents/src/api/background-work.js +151 -0
- package/extensions/dm-subagents/src/api/capability-ceiling.js +12 -0
- package/extensions/dm-subagents/src/api/control-channel.js +3 -0
- package/extensions/dm-subagents/src/api/delegation.js +5 -0
- package/extensions/dm-subagents/src/api/dm-args.js +3 -0
- package/extensions/dm-subagents/src/api/external-job-provider.js +137 -0
- package/extensions/dm-subagents/src/api/external-runs.js +233 -0
- package/extensions/dm-subagents/src/api/intercom-bridge.js +3 -0
- package/extensions/dm-subagents/src/api/preflight.js +322 -0
- package/extensions/dm-subagents/src/api/project-panes.js +11 -0
- package/extensions/dm-subagents/src/api/shared-types.js +4 -0
- package/extensions/dm-subagents/src/extension/config.js +175 -0
- package/extensions/dm-subagents/src/extension/control-notices.js +4 -43
- package/extensions/dm-subagents/src/extension/doctor.js +71 -17
- package/extensions/dm-subagents/src/extension/fanout-child.js +49 -32
- package/extensions/dm-subagents/src/extension/index.js +711 -265
- package/extensions/dm-subagents/src/extension/public-execution.js +114 -0
- package/extensions/dm-subagents/src/extension/rpc.js +427 -22
- package/extensions/dm-subagents/src/extension/schemas.js +158 -65
- package/extensions/dm-subagents/src/extension/steering-notices.js +23 -0
- package/extensions/dm-subagents/src/extension/subagent-guide.js +31 -0
- package/extensions/dm-subagents/src/extension/tool-description.js +92 -74
- package/extensions/dm-subagents/src/extension/tool-result.js +7 -0
- package/extensions/dm-subagents/src/inspectors/herdr/actions.js +218 -0
- package/extensions/dm-subagents/src/inspectors/herdr/client.js +123 -0
- package/extensions/dm-subagents/src/inspectors/herdr/focus.js +47 -0
- package/extensions/dm-subagents/src/inspectors/herdr/inspector-runner.js +160 -0
- package/extensions/dm-subagents/src/inspectors/herdr/project-panes.js +618 -0
- package/extensions/dm-subagents/src/inspectors/herdr/session-roots-codec.js +21 -0
- package/extensions/dm-subagents/src/inspectors/herdr/shell-command.js +15 -0
- package/extensions/dm-subagents/src/integrations/herdr-status.js +377 -0
- package/extensions/dm-subagents/src/intercom/intercom-bridge.js +17 -13
- package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +371 -79
- package/extensions/dm-subagents/src/intercom/result-intercom.js +47 -7
- package/extensions/dm-subagents/src/missions/actions.js +394 -0
- package/extensions/dm-subagents/src/missions/goal-driver.js +149 -0
- package/extensions/dm-subagents/src/missions/lifecycle.js +331 -0
- package/extensions/dm-subagents/src/missions/store.js +548 -0
- package/extensions/dm-subagents/src/missions/types.js +9 -0
- package/extensions/dm-subagents/src/missions/workflow-state.js +245 -0
- package/extensions/dm-subagents/src/policy/authority.js +37 -0
- package/extensions/dm-subagents/src/profiles/profiles.js +36 -18
- package/extensions/dm-subagents/src/runs/background/active-async-capacity.js +427 -0
- package/extensions/dm-subagents/src/runs/background/active-run-index.js +122 -0
- package/extensions/dm-subagents/src/runs/background/async-execution.js +925 -137
- package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +515 -148
- package/extensions/dm-subagents/src/runs/background/async-resume.js +378 -51
- package/extensions/dm-subagents/src/runs/background/async-retention.js +828 -0
- package/extensions/dm-subagents/src/runs/background/async-status-snapshot.js +31 -0
- package/extensions/dm-subagents/src/runs/background/async-status.js +259 -22
- package/extensions/dm-subagents/src/runs/background/auto-drain.js +46 -0
- package/extensions/dm-subagents/src/runs/background/chain-append.js +50 -15
- package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +67 -12
- package/extensions/dm-subagents/src/runs/background/completion-batcher.js +5 -1
- package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +3 -11
- package/extensions/dm-subagents/src/runs/background/completion-replay.js +245 -0
- package/extensions/dm-subagents/src/runs/background/control-channel.js +423 -36
- package/extensions/dm-subagents/src/runs/background/fleet-view.js +132 -59
- package/extensions/dm-subagents/src/runs/background/index-segment.js +38 -0
- package/extensions/dm-subagents/src/runs/background/inspect-rpc.js +373 -0
- package/extensions/dm-subagents/src/runs/background/notify.js +357 -57
- package/extensions/dm-subagents/src/runs/background/owned-process-tree.js +86 -0
- package/extensions/dm-subagents/src/runs/background/process-terminal.js +269 -0
- package/extensions/dm-subagents/src/runs/background/result-delivery-ownership.js +34 -0
- package/extensions/dm-subagents/src/runs/background/result-files.js +469 -0
- package/extensions/dm-subagents/src/runs/background/result-watcher.js +540 -75
- package/extensions/dm-subagents/src/runs/background/resume-guidance.js +44 -0
- package/extensions/dm-subagents/src/runs/background/retained-children.js +119 -0
- package/extensions/dm-subagents/src/runs/background/run-id-query.js +5 -0
- package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +93 -9
- package/extensions/dm-subagents/src/runs/background/run-status.js +303 -32
- package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +784 -376
- package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +75 -34
- package/extensions/dm-subagents/src/runs/background/steering.js +221 -0
- package/extensions/dm-subagents/src/runs/background/subagent-runner.js +3106 -844
- package/extensions/dm-subagents/src/runs/background/subagent-wait.js +529 -0
- package/extensions/dm-subagents/src/runs/background/terminal-run-index.js +106 -0
- package/extensions/dm-subagents/src/runs/background/top-level-async.js +1 -1
- package/extensions/dm-subagents/src/runs/background/wait-completions.js +155 -0
- package/extensions/dm-subagents/src/runs/background/wait-config.js +46 -0
- package/extensions/dm-subagents/src/runs/background/wait-subscriptions.js +278 -0
- package/extensions/dm-subagents/src/runs/background/wait-tool.js +47 -0
- package/extensions/dm-subagents/src/runs/foreground/async-dismiss-action.js +81 -0
- package/extensions/dm-subagents/src/runs/foreground/async-steering-action.js +245 -0
- package/extensions/dm-subagents/src/runs/foreground/async-stop-action.js +74 -0
- package/extensions/dm-subagents/src/runs/foreground/execution.js +1401 -342
- package/extensions/dm-subagents/src/runs/foreground/foreground-control.js +133 -0
- package/extensions/dm-subagents/src/runs/foreground/foreground-history.js +148 -0
- package/extensions/dm-subagents/src/runs/foreground/prompt-audit.js +139 -0
- package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +4278 -1441
- package/extensions/dm-subagents/src/runs/foreground/workflow-detach-reconcile.js +278 -0
- package/extensions/dm-subagents/src/runs/foreground/workflow-foreground-steering.js +155 -0
- package/extensions/dm-subagents/src/runs/shared/abort-recovery.js +97 -0
- package/extensions/dm-subagents/src/runs/shared/acceptance.js +597 -148
- package/extensions/dm-subagents/src/runs/shared/agent-contract.js +35 -0
- package/extensions/dm-subagents/src/runs/shared/async-status-projection.js +472 -0
- package/extensions/dm-subagents/src/runs/shared/background-process-options.js +6 -0
- package/extensions/dm-subagents/src/runs/shared/capability-ceiling.js +175 -0
- package/extensions/dm-subagents/src/runs/shared/child-identity.js +32 -0
- package/extensions/dm-subagents/src/runs/shared/child-launch-plan.js +65 -0
- package/extensions/dm-subagents/src/runs/shared/child-protocol.js +447 -0
- package/extensions/dm-subagents/src/runs/shared/claude-code-adapter.js +120 -0
- package/extensions/dm-subagents/src/runs/shared/codex-exec-adapter.js +129 -0
- package/extensions/dm-subagents/src/runs/shared/completion-evidence.js +40 -0
- package/extensions/dm-subagents/src/runs/shared/completion-guard.js +140 -83
- package/extensions/dm-subagents/src/runs/shared/context-mode.js +38 -0
- package/extensions/dm-subagents/src/runs/shared/cursor-agent-adapter.js +101 -0
- package/extensions/dm-subagents/src/runs/shared/dm-args.js +445 -72
- package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +27 -16
- package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +19 -6
- package/extensions/dm-subagents/src/runs/shared/extension-bindings.js +81 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-contract.js +134 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-preflight.js +98 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-runner.js +419 -0
- package/extensions/dm-subagents/src/runs/shared/external-job-bridge.js +404 -0
- package/extensions/dm-subagents/src/runs/shared/external-job-runner.js +334 -0
- package/extensions/dm-subagents/src/runs/shared/fast-mode-extension.js +8 -0
- package/extensions/dm-subagents/src/runs/shared/host-step-status.js +228 -0
- package/extensions/dm-subagents/src/runs/shared/lane-metadata.js +104 -0
- package/extensions/dm-subagents/src/runs/shared/launch-cwd.js +17 -0
- package/extensions/dm-subagents/src/runs/shared/llm-intent-arbiter.js +190 -0
- package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +48 -3
- package/extensions/dm-subagents/src/runs/shared/mcp-config-sources.js +387 -0
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +212 -137
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-grant.js +131 -0
- package/extensions/dm-subagents/src/runs/shared/model-exclusions.js +207 -0
- package/extensions/dm-subagents/src/runs/shared/model-fallback.js +225 -55
- package/extensions/dm-subagents/src/runs/shared/model-scope.js +85 -28
- package/extensions/dm-subagents/src/runs/shared/mutation-evidence.js +182 -0
- package/extensions/dm-subagents/src/runs/shared/nested-events.js +264 -102
- package/extensions/dm-subagents/src/runs/shared/nested-render.js +15 -5
- package/extensions/dm-subagents/src/runs/shared/orca-progress-tabs.js +505 -0
- package/extensions/dm-subagents/src/runs/shared/parallel-handoff.js +653 -0
- package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +29 -12
- package/extensions/dm-subagents/src/runs/shared/permissions.js +108 -0
- package/extensions/dm-subagents/src/runs/shared/process-signal.js +13 -0
- package/extensions/dm-subagents/src/runs/shared/run-fanout-budget.js +257 -0
- package/extensions/dm-subagents/src/runs/shared/run-history.js +133 -13
- package/extensions/dm-subagents/src/runs/shared/runtime-acknowledged-extensions.js +62 -0
- package/extensions/dm-subagents/src/runs/shared/session-lease.js +225 -0
- package/extensions/dm-subagents/src/runs/shared/single-output.js +129 -27
- package/extensions/dm-subagents/src/runs/shared/spawn-budget.js +95 -0
- package/extensions/dm-subagents/src/runs/shared/structured-output.js +129 -10
- package/extensions/dm-subagents/src/runs/shared/subagent-control.js +66 -11
- package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +548 -70
- package/extensions/dm-subagents/src/runs/shared/subagent-startup-retry.js +50 -0
- package/extensions/dm-subagents/src/runs/shared/task-intent.js +130 -0
- package/extensions/dm-subagents/src/runs/shared/tool-availability.js +59 -0
- package/extensions/dm-subagents/src/runs/shared/tool-budget.js +7 -5
- package/extensions/dm-subagents/src/runs/shared/tool-timeout.js +64 -0
- package/extensions/dm-subagents/src/runs/shared/usage-budget.js +74 -0
- package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +22 -0
- package/extensions/dm-subagents/src/runs/shared/worktree-cleanup-plan.js +721 -0
- package/extensions/dm-subagents/src/runs/shared/worktree.js +168 -19
- package/extensions/dm-subagents/src/shared/accessible-dir.js +35 -0
- package/extensions/dm-subagents/src/shared/agent-stream-options.js +3 -0
- package/extensions/dm-subagents/src/shared/artifacts.js +170 -11
- package/extensions/dm-subagents/src/shared/atomic-json.js +36 -38
- package/extensions/dm-subagents/src/shared/capacity-resilient-json.js +77 -0
- package/extensions/dm-subagents/src/shared/child-session-name.js +15 -0
- package/extensions/dm-subagents/src/shared/child-transcript.js +57 -2
- package/extensions/dm-subagents/src/shared/completion-owner.js +7 -0
- package/extensions/dm-subagents/src/shared/display-text.js +142 -0
- package/extensions/dm-subagents/src/shared/extension-context.js +17 -0
- package/extensions/dm-subagents/src/shared/file-coalescer.js +9 -0
- package/extensions/dm-subagents/src/shared/file-system-retry.js +56 -0
- package/extensions/dm-subagents/src/shared/fork-context.js +96 -33
- package/extensions/dm-subagents/src/shared/formatters.js +21 -7
- package/extensions/dm-subagents/src/shared/launch-contract.js +94 -0
- package/extensions/dm-subagents/src/shared/model-info.js +10 -5
- package/extensions/dm-subagents/src/shared/node-executable.js +19 -0
- package/extensions/dm-subagents/src/shared/prompt-resources.js +10 -0
- package/extensions/dm-subagents/src/shared/pruned-fork.js +427 -0
- package/extensions/dm-subagents/src/shared/session-file-trust.js +19 -0
- package/extensions/dm-subagents/src/shared/session-tokens.js +14 -3
- package/extensions/dm-subagents/src/shared/settings.js +39 -47
- package/extensions/dm-subagents/src/shared/shortcuts.js +16 -0
- package/extensions/dm-subagents/src/shared/status-format.js +9 -2
- package/extensions/dm-subagents/src/shared/thinking-ceiling.js +41 -0
- package/extensions/dm-subagents/src/shared/types.js +47 -7
- package/extensions/dm-subagents/src/shared/utf8.js +12 -0
- package/extensions/dm-subagents/src/shared/utils.js +151 -131
- package/extensions/dm-subagents/src/shared/watch-strategy.js +3 -0
- package/extensions/dm-subagents/src/shared/workflow-child-permit.js +84 -0
- package/extensions/dm-subagents/src/slash/delegation-adapters.js +274 -0
- package/extensions/dm-subagents/src/slash/delegation-json.js +113 -0
- package/extensions/dm-subagents/src/slash/delegation-request.js +152 -0
- package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +294 -243
- package/extensions/dm-subagents/src/slash/prompt-workflows.js +35 -73
- package/extensions/dm-subagents/src/slash/selector.js +101 -0
- package/extensions/dm-subagents/src/slash/slash-bridge.js +17 -1
- package/extensions/dm-subagents/src/slash/slash-commands.js +722 -732
- package/extensions/dm-subagents/src/slash/slash-live-state.js +37 -19
- package/extensions/dm-subagents/src/slash/subagents-admin.js +410 -0
- package/extensions/dm-subagents/src/tui/fleet-status.js +824 -0
- package/extensions/dm-subagents/src/tui/fleet-transcript.js +479 -0
- package/extensions/dm-subagents/src/tui/fleet.js +1326 -0
- package/extensions/dm-subagents/src/tui/render-helpers.js +22 -0
- package/extensions/dm-subagents/src/tui/render.js +1511 -261
- package/extensions/dm-subagents/src/watchdog/change-signature.js +220 -0
- package/extensions/dm-subagents/src/watchdog/child-status.js +151 -0
- package/extensions/dm-subagents/src/watchdog/emission-guard.js +90 -0
- package/extensions/dm-subagents/src/watchdog/lsp-diagnostics.js +484 -0
- package/extensions/dm-subagents/src/watchdog/model-selection.js +154 -0
- package/extensions/dm-subagents/src/watchdog/permission-arbiter.js +138 -0
- package/extensions/dm-subagents/src/watchdog/register-child.js +112 -0
- package/extensions/dm-subagents/src/watchdog/register-main.js +419 -0
- package/extensions/dm-subagents/src/watchdog/render.js +54 -0
- package/extensions/dm-subagents/src/watchdog/review.js +251 -0
- package/extensions/dm-subagents/src/watchdog/runtime.js +803 -0
- package/extensions/dm-subagents/src/watchdog/scope.js +56 -0
- package/extensions/dm-subagents/src/watchdog/settings.js +515 -0
- package/extensions/dm-subagents/src/watchdog/tool-actions.js +151 -0
- package/extensions/dm-subagents/src/watchdog/turn-delta.js +169 -0
- package/extensions/dm-subagents/src/watchdog/types.js +29 -0
- package/extensions/dm-subagents/src/watchdog/warning-format.js +58 -0
- package/extensions/dm-subagents/src/workflows/chat-progress.js +116 -0
- package/extensions/dm-subagents/src/workflows/host-command.js +227 -0
- package/extensions/dm-subagents/src/workflows/scripted-workflow.js +2011 -0
- package/extensions/dm-subagents/src/workflows/workflow-child-summary.js +116 -0
- package/extensions/dm-subagents/src/workflows/workflow-preflight.js +243 -0
- package/extensions/dm-subagents/src/workflows/workflow-receipt.js +387 -0
- package/extensions/dm-subagents/src/workflows/workflow-settlement.js +189 -0
- package/package.json +4 -3
- package/extensions/dm-fff/package.json +0 -21
- package/extensions/dm-fff/src/index.js +0 -691
- package/extensions/dm-fff/src/query.js +0 -60
- package/extensions/dm-subagents/agents/context-builder.md +0 -46
- package/extensions/dm-subagents/agents/planner.md +0 -55
- package/extensions/dm-subagents/prompts/parallel-context-build.md +0 -55
- package/extensions/dm-subagents/prompts/parallel-handoff-plan.md +0 -61
- package/extensions/dm-subagents/src/runs/background/wait.js +0 -206
- package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +0 -1013
- package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +0 -981
- package/extensions/dm-subagents/src/runs/shared/turn-budget.js +0 -50
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { Worker } from "node:worker_threads";
|
|
7
|
+
import { writeAtomicJson } from "../../shared/atomic-json.js";
|
|
8
|
+
import { MISSION_BINDING_FILE } from "../../missions/lifecycle.js";
|
|
9
|
+
import { ACTIVE_RUN_INDEX_DIR } from "./active-run-index.js";
|
|
10
|
+
import { encodeIndexSegment } from "./index-segment.js";
|
|
11
|
+
import { reconcileAsyncRun } from "./stale-run-reconciler.js";
|
|
12
|
+
export const ASYNC_RETENTION_DAYS = 30;
|
|
13
|
+
export const ASYNC_RETENTION_BATCH_SIZE = 100;
|
|
14
|
+
export const ASYNC_RETENTION_DELAY_MS = 60000;
|
|
15
|
+
export const ASYNC_RETENTION_TOMBSTONE_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
const RETENTION_MS = ASYNC_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
17
|
+
const LOCK_NAME = ".async-retention.lock";
|
|
18
|
+
const CURSOR_NAME = ".async-retention-cursor.json";
|
|
19
|
+
const LOG_NAME = "async-retention-maintenance.jsonl";
|
|
20
|
+
const RUN_TOMBSTONE_PREFIX = ".deleting-run-";
|
|
21
|
+
const RESULT_TOMBSTONE_PREFIX = ".deleting-result-";
|
|
22
|
+
const RUN_TOMBSTONE_MARKERS_DIR = "async-retention-run-tombstones";
|
|
23
|
+
const LOCK_STALE_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
const RUN_MODES = new Set(["single", "parallel", "chain", "workflow"]);
|
|
25
|
+
const TERMINAL_STATES = new Set(["complete", "failed", "stopped", "rejected"]);
|
|
26
|
+
const RESULT_TIMESTAMP_FIELDS = ["endedAt", "completedAt", "createdAt", "writtenAt", "expiresAt", "timestamp"];
|
|
27
|
+
function isNotFound(error) {
|
|
28
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
29
|
+
}
|
|
30
|
+
function compactError(error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
return message.replace(/\s+/g, " ").slice(0, 240);
|
|
33
|
+
}
|
|
34
|
+
function listDir(dir) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (isNotFound(error))
|
|
39
|
+
return [];
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function readJson(filePath) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(fs.readFileSync(filePath).toString("utf8"));
|
|
46
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function readStatus(runDir) {
|
|
52
|
+
const value = readJson(path.join(runDir, "status.json"));
|
|
53
|
+
if (!value || typeof value.runId !== "string" || typeof value.state !== "string" || typeof value.mode !== "string" || !RUN_MODES.has(value.mode) || typeof value.startedAt !== "number" || !Number.isFinite(value.startedAt))
|
|
54
|
+
return;
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function existingRegularFile(filePath) {
|
|
58
|
+
if (!filePath)
|
|
59
|
+
return false;
|
|
60
|
+
try {
|
|
61
|
+
const stat = fs.lstatSync(filePath);
|
|
62
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return !isNotFound(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function validRunId(runId) {
|
|
68
|
+
return runId.length > 0 && runId !== "." && runId !== ".." && path.basename(runId) === runId;
|
|
69
|
+
}
|
|
70
|
+
function statusTimestamp(status, runDir) {
|
|
71
|
+
if (status.endedAt !== undefined && (typeof status.endedAt !== "number" || !Number.isFinite(status.endedAt)))
|
|
72
|
+
return;
|
|
73
|
+
if (status.lastUpdate !== undefined && (typeof status.lastUpdate !== "number" || !Number.isFinite(status.lastUpdate)))
|
|
74
|
+
return;
|
|
75
|
+
const logical = status.endedAt ?? status.lastUpdate;
|
|
76
|
+
try {
|
|
77
|
+
const physical = Math.max(fs.statSync(runDir).mtimeMs, fs.statSync(path.join(runDir, "status.json")).mtimeMs);
|
|
78
|
+
const timestamp = logical === undefined ? physical : Math.max(logical, physical);
|
|
79
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
80
|
+
} catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function hasNestedReferences(status) {
|
|
85
|
+
return status.isNested === true || (status.steps ?? []).some((step) => (step.children?.length ?? 0) > 0);
|
|
86
|
+
}
|
|
87
|
+
function hasResumableContract(runDir, status) {
|
|
88
|
+
const statusSessionFiles = [status.sessionFile, ...(status.steps ?? []).map((step) => step.sessionFile)];
|
|
89
|
+
if (statusSessionFiles.some(existingRegularFile))
|
|
90
|
+
return true;
|
|
91
|
+
const descriptorPath = path.join(runDir, "recovery-descriptor.json");
|
|
92
|
+
if (!fs.existsSync(descriptorPath))
|
|
93
|
+
return false;
|
|
94
|
+
const descriptor = readJson(descriptorPath);
|
|
95
|
+
if (!descriptor)
|
|
96
|
+
return true;
|
|
97
|
+
const sessionFiles = [
|
|
98
|
+
typeof descriptor.sessionFile === "string" ? descriptor.sessionFile : undefined
|
|
99
|
+
];
|
|
100
|
+
if (descriptor.sourceRunId !== status.runId)
|
|
101
|
+
return true;
|
|
102
|
+
return sessionFiles.some(existingRegularFile);
|
|
103
|
+
}
|
|
104
|
+
function activeMarkerExists(asyncDirRoot, runId) {
|
|
105
|
+
return fs.existsSync(path.join(asyncDirRoot, ACTIVE_RUN_INDEX_DIR, runId));
|
|
106
|
+
}
|
|
107
|
+
function missionObserverIndexExists(resultsDir, runId) {
|
|
108
|
+
return fs.existsSync(path.join(resultsDir, "result-index", "observers", "mission", `${encodeIndexSegment(runId)}.json`));
|
|
109
|
+
}
|
|
110
|
+
function runTombstoneMarkerPath(maintenanceRoot, runId) {
|
|
111
|
+
return path.join(maintenanceRoot, RUN_TOMBSTONE_MARKERS_DIR, `${encodeIndexSegment(runId)}.json`);
|
|
112
|
+
}
|
|
113
|
+
function writeRunTombstoneMarker(maintenanceRoot, runId, tombstonePath, now) {
|
|
114
|
+
const markerPath = runTombstoneMarkerPath(maintenanceRoot, runId);
|
|
115
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
116
|
+
writeAtomicJson(markerPath, { version: 1, runId, tombstonePath, createdAt: now });
|
|
117
|
+
}
|
|
118
|
+
function removeRunTombstoneMarker(maintenanceRoot, runId) {
|
|
119
|
+
fs.rmSync(runTombstoneMarkerPath(maintenanceRoot, runId), { force: true });
|
|
120
|
+
}
|
|
121
|
+
function readRunTombstoneMarker(maintenanceRoot, runId) {
|
|
122
|
+
const markerPath = runTombstoneMarkerPath(maintenanceRoot, runId);
|
|
123
|
+
try {
|
|
124
|
+
const stat = fs.lstatSync(markerPath);
|
|
125
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
126
|
+
return "unreadable";
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return isNotFound(error) ? undefined : "unreadable";
|
|
129
|
+
}
|
|
130
|
+
const marker = readJson(markerPath);
|
|
131
|
+
if (marker?.version !== 1 || marker.runId !== runId || typeof marker.tombstonePath !== "string" || !marker.tombstonePath)
|
|
132
|
+
return "unreadable";
|
|
133
|
+
return { runId, tombstonePath: marker.tombstonePath };
|
|
134
|
+
}
|
|
135
|
+
function runTombstoneMarkerMatches(maintenanceRoot, runId, tombstonePath) {
|
|
136
|
+
const marker = readRunTombstoneMarker(maintenanceRoot, runId);
|
|
137
|
+
return marker !== undefined && marker !== "unreadable" && path.resolve(marker.tombstonePath) === path.resolve(tombstonePath);
|
|
138
|
+
}
|
|
139
|
+
function runTombstoneMarkerBlocks(maintenanceRoot, runId) {
|
|
140
|
+
const marker = readRunTombstoneMarker(maintenanceRoot, runId);
|
|
141
|
+
if (marker === undefined)
|
|
142
|
+
return false;
|
|
143
|
+
if (marker === "unreadable")
|
|
144
|
+
return true;
|
|
145
|
+
try {
|
|
146
|
+
if (fs.existsSync(marker.tombstonePath))
|
|
147
|
+
return true;
|
|
148
|
+
removeRunTombstoneMarker(maintenanceRoot, runId);
|
|
149
|
+
return false;
|
|
150
|
+
} catch {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function unresolvedHandoff(manifestPath) {
|
|
155
|
+
const manifest = readJson(manifestPath);
|
|
156
|
+
if (!manifest || manifest.version !== 1 || !Array.isArray(manifest.groups) || manifest.groups.length === 0)
|
|
157
|
+
return true;
|
|
158
|
+
return manifest.groups.some((value) => {
|
|
159
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
160
|
+
return true;
|
|
161
|
+
const cleanup = value.cleanup;
|
|
162
|
+
return !cleanup || typeof cleanup !== "object" || Array.isArray(cleanup) || cleanup.state !== "complete";
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function hasUnresolvedRunHandoff(runDir, status) {
|
|
166
|
+
const paths = new Set;
|
|
167
|
+
if (status.parallelHandoff) {
|
|
168
|
+
if (typeof status.parallelHandoff.path !== "string" || !status.parallelHandoff.path)
|
|
169
|
+
return true;
|
|
170
|
+
paths.add(status.parallelHandoff.path);
|
|
171
|
+
}
|
|
172
|
+
const localPath = path.join(runDir, "handoff.json");
|
|
173
|
+
if (fs.existsSync(localPath))
|
|
174
|
+
paths.add(localPath);
|
|
175
|
+
return [...paths].some(unresolvedHandoff);
|
|
176
|
+
}
|
|
177
|
+
function runSkipReason(input) {
|
|
178
|
+
const { runDir, status } = input;
|
|
179
|
+
if (!status)
|
|
180
|
+
return "invalid-status";
|
|
181
|
+
if (!validRunId(status.runId))
|
|
182
|
+
return "identity-mismatch";
|
|
183
|
+
if (path.basename(runDir) !== status.runId && !path.basename(runDir).startsWith(RUN_TOMBSTONE_PREFIX))
|
|
184
|
+
return "identity-mismatch";
|
|
185
|
+
if (input.protectedRunIds.has(status.runId))
|
|
186
|
+
return "runtime-reference";
|
|
187
|
+
if (input.waitRunIds.has(status.runId))
|
|
188
|
+
return "wait-reference";
|
|
189
|
+
if (activeMarkerExists(input.asyncDirRoot, status.runId))
|
|
190
|
+
return "active-index";
|
|
191
|
+
if (!TERMINAL_STATES.has(status.state))
|
|
192
|
+
return "non-terminal";
|
|
193
|
+
if (status.mode === "workflow" || status.parentWorkflowRunId || status.workflowKey)
|
|
194
|
+
return "workflow-reference";
|
|
195
|
+
if (hasNestedReferences(status))
|
|
196
|
+
return "nested-reference";
|
|
197
|
+
if (fs.existsSync(path.join(runDir, MISSION_BINDING_FILE)) || missionObserverIndexExists(input.resultsDir, status.runId))
|
|
198
|
+
return "mission-reference";
|
|
199
|
+
if (hasUnresolvedRunHandoff(runDir, status))
|
|
200
|
+
return "handoff-reference";
|
|
201
|
+
if (hasResumableContract(runDir, status))
|
|
202
|
+
return "resumable";
|
|
203
|
+
const timestamp = statusTimestamp(status, runDir);
|
|
204
|
+
if (timestamp === undefined)
|
|
205
|
+
return "unknown-age";
|
|
206
|
+
if (timestamp > input.cutoff)
|
|
207
|
+
return "recent";
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
function parseWaitRunIds(dir) {
|
|
211
|
+
const runIds = new Set;
|
|
212
|
+
for (const entry of listDir(dir)) {
|
|
213
|
+
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
214
|
+
continue;
|
|
215
|
+
const record = readJson(path.join(dir, entry.name));
|
|
216
|
+
if (!record || typeof record.runId !== "string" || typeof record.expiresAt !== "number")
|
|
217
|
+
return { runIds, safe: false };
|
|
218
|
+
runIds.add(record.runId);
|
|
219
|
+
}
|
|
220
|
+
return { runIds, safe: true };
|
|
221
|
+
}
|
|
222
|
+
function readCursor(root) {
|
|
223
|
+
const value = readJson(path.join(root, CURSOR_NAME));
|
|
224
|
+
return value?.version === 1 ? value : { version: 1 };
|
|
225
|
+
}
|
|
226
|
+
function processStartIdentity(pid) {
|
|
227
|
+
if (process.platform === "linux") {
|
|
228
|
+
try {
|
|
229
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`).toString("utf8");
|
|
230
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
231
|
+
if (commandEnd === -1)
|
|
232
|
+
return;
|
|
233
|
+
const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
|
|
234
|
+
return fields[19] ? `linux:${fields[19]}` : undefined;
|
|
235
|
+
} catch {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (process.platform === "win32") {
|
|
240
|
+
const result = spawnSync("powershell.exe", ["-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate`], { encoding: "utf-8", windowsHide: true });
|
|
241
|
+
const started = result.status === 0 ? result.stdout.trim() : "";
|
|
242
|
+
return started ? `win:${started}` : undefined;
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
function processIsAlive(pid) {
|
|
247
|
+
try {
|
|
248
|
+
process.kill(pid, 0);
|
|
249
|
+
return true;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const code = error.code;
|
|
252
|
+
if (code === "ESRCH")
|
|
253
|
+
return false;
|
|
254
|
+
if (code === "EPERM")
|
|
255
|
+
return true;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function parseLockOwner(lockDir) {
|
|
260
|
+
const owner = readJson(path.join(lockDir, "owner.json"));
|
|
261
|
+
if (owner?.version !== 1 || typeof owner.token !== "string" || typeof owner.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0 || typeof owner.hostname !== "string" || typeof owner.startedAt !== "number" || !Number.isFinite(owner.startedAt))
|
|
262
|
+
return;
|
|
263
|
+
if (owner.processStartIdentity !== undefined && typeof owner.processStartIdentity !== "string")
|
|
264
|
+
return;
|
|
265
|
+
return owner;
|
|
266
|
+
}
|
|
267
|
+
function staleLock(lockDir, now, options) {
|
|
268
|
+
const owner = parseLockOwner(lockDir);
|
|
269
|
+
if (!owner) {
|
|
270
|
+
try {
|
|
271
|
+
return { stale: now - fs.statSync(lockDir).mtimeMs >= LOCK_STALE_MS };
|
|
272
|
+
} catch {
|
|
273
|
+
return { stale: false };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (owner.hostname !== options.hostname)
|
|
277
|
+
return { stale: false };
|
|
278
|
+
const alive = options.isProcessAlive(owner.pid);
|
|
279
|
+
if (alive === false)
|
|
280
|
+
return { stale: true, token: owner.token };
|
|
281
|
+
if (alive === true && owner.processStartIdentity) {
|
|
282
|
+
const currentIdentity = options.getProcessStartIdentity(owner.pid);
|
|
283
|
+
if (currentIdentity !== undefined && currentIdentity !== owner.processStartIdentity)
|
|
284
|
+
return { stale: true, token: owner.token };
|
|
285
|
+
}
|
|
286
|
+
return { stale: now - owner.startedAt >= LOCK_STALE_MS, token: owner.token };
|
|
287
|
+
}
|
|
288
|
+
function createLockDirectory(lockDir, owner) {
|
|
289
|
+
try {
|
|
290
|
+
fs.mkdirSync(lockDir, { mode: 448 });
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error.code === "EEXIST")
|
|
293
|
+
return false;
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
fs.writeFileSync(path.join(lockDir, "owner.json"), JSON.stringify(owner), { encoding: "utf-8", mode: 384 });
|
|
298
|
+
return true;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function acquireRetentionLock(lockDir, owner, options) {
|
|
305
|
+
for (let attempt = 0;attempt < 4; attempt += 1) {
|
|
306
|
+
if (createLockDirectory(lockDir, owner))
|
|
307
|
+
return true;
|
|
308
|
+
const stale = staleLock(lockDir, owner.startedAt, options);
|
|
309
|
+
if (!stale.stale)
|
|
310
|
+
return false;
|
|
311
|
+
const staleKey = (stale.token ?? owner.token).replace(/[^A-Za-z0-9._-]/g, "-");
|
|
312
|
+
const tombstone = `${lockDir}.stale-${staleKey}`;
|
|
313
|
+
try {
|
|
314
|
+
fs.renameSync(lockDir, tombstone);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const code = error.code;
|
|
317
|
+
if (code === "ENOENT" || fs.existsSync(tombstone))
|
|
318
|
+
continue;
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
function releaseRetentionLock(lockDir, token) {
|
|
325
|
+
if (parseLockOwner(lockDir)?.token !== token)
|
|
326
|
+
return;
|
|
327
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
function resultRunId(candidate, data) {
|
|
330
|
+
if (typeof data.runId === "string")
|
|
331
|
+
return data.runId;
|
|
332
|
+
if (typeof data.id === "string")
|
|
333
|
+
return data.id;
|
|
334
|
+
if ((candidate.kind === "public" || candidate.kind === "pending") && path.basename(candidate.path).endsWith(".json"))
|
|
335
|
+
return path.basename(candidate.path, ".json");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
function resultTimestamp(data, filePath) {
|
|
339
|
+
for (const field of RESULT_TIMESTAMP_FIELDS) {
|
|
340
|
+
const value = data[field];
|
|
341
|
+
if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value)))
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const logical = RESULT_TIMESTAMP_FIELDS.map((field) => data[field]).filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
345
|
+
try {
|
|
346
|
+
return Math.max(fs.statSync(filePath).mtimeMs, ...logical);
|
|
347
|
+
} catch {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function terminalResult(data) {
|
|
352
|
+
return data.success === true || data.success === false || typeof data.state === "string" && TERMINAL_STATES.has(data.state);
|
|
353
|
+
}
|
|
354
|
+
function resultHasResumableSession(data) {
|
|
355
|
+
if (existingRegularFile(typeof data.sessionFile === "string" ? data.sessionFile : undefined))
|
|
356
|
+
return true;
|
|
357
|
+
return Array.isArray(data.results) && data.results.some((value) => {
|
|
358
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
359
|
+
return false;
|
|
360
|
+
const sessionFile = value.sessionFile;
|
|
361
|
+
return existingRegularFile(typeof sessionFile === "string" ? sessionFile : undefined);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function hasUnresolvedResultHandoff(data) {
|
|
365
|
+
if (data.parallelHandoff === undefined)
|
|
366
|
+
return false;
|
|
367
|
+
if (!data.parallelHandoff || typeof data.parallelHandoff !== "object" || Array.isArray(data.parallelHandoff))
|
|
368
|
+
return true;
|
|
369
|
+
const manifestPath = data.parallelHandoff.path;
|
|
370
|
+
return typeof manifestPath !== "string" || !manifestPath || unresolvedHandoff(manifestPath);
|
|
371
|
+
}
|
|
372
|
+
function completionMode(data) {
|
|
373
|
+
return data.completion && typeof data.completion === "object" && !Array.isArray(data.completion) ? data.completion.mode : undefined;
|
|
374
|
+
}
|
|
375
|
+
function resultSkipReason(input) {
|
|
376
|
+
if (!input.data)
|
|
377
|
+
return "invalid-result";
|
|
378
|
+
const runId = resultRunId(input.candidate, input.data);
|
|
379
|
+
if (!runId || !validRunId(runId))
|
|
380
|
+
return "invalid-result";
|
|
381
|
+
if (runTombstoneMarkerBlocks(input.maintenanceRoot, runId))
|
|
382
|
+
return "run-tombstone-present";
|
|
383
|
+
if (input.protectedRunIds.has(runId))
|
|
384
|
+
return "runtime-reference";
|
|
385
|
+
if (input.waitRunIds.has(runId))
|
|
386
|
+
return "wait-reference";
|
|
387
|
+
if (activeMarkerExists(input.asyncDirRoot, runId))
|
|
388
|
+
return "active-index";
|
|
389
|
+
if (fs.existsSync(path.join(input.asyncDirRoot, runId)))
|
|
390
|
+
return "run-present";
|
|
391
|
+
if (missionObserverIndexExists(input.resultsDir, runId))
|
|
392
|
+
return "mission-reference";
|
|
393
|
+
if (input.data.mode === "workflow" || completionMode(input.data) === "workflow" || typeof input.data.parentWorkflowRunId === "string" || typeof input.data.workflowKey === "string")
|
|
394
|
+
return "workflow-reference";
|
|
395
|
+
if (hasUnresolvedResultHandoff(input.data))
|
|
396
|
+
return "handoff-reference";
|
|
397
|
+
if (resultHasResumableSession(input.data))
|
|
398
|
+
return "resumable";
|
|
399
|
+
if (input.candidate.kind === "archive" && fs.existsSync(path.join(input.resultsDir, "completion-replay", `${encodeURIComponent(runId)}.json`)))
|
|
400
|
+
return "replay-reference";
|
|
401
|
+
if (!terminalResult(input.data) && input.candidate.kind !== "replay" && input.candidate.kind !== "archive" && input.candidate.kind !== "tombstone")
|
|
402
|
+
return "non-terminal-result";
|
|
403
|
+
const timestamp = resultTimestamp(input.data, input.candidate.path);
|
|
404
|
+
if (timestamp === undefined)
|
|
405
|
+
return "unknown-age";
|
|
406
|
+
if (timestamp > input.cutoff)
|
|
407
|
+
return "recent";
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
function appendMaintenanceLog(root, result, now, deletedIds) {
|
|
411
|
+
fs.mkdirSync(root, { recursive: true });
|
|
412
|
+
fs.appendFileSync(path.join(root, LOG_NAME), `${JSON.stringify({
|
|
413
|
+
at: new Date(now).toISOString(),
|
|
414
|
+
acquired: result.acquired,
|
|
415
|
+
scanned: result.scanned,
|
|
416
|
+
repairedRuns: result.repairedRuns,
|
|
417
|
+
deletedRuns: result.deletedRuns,
|
|
418
|
+
deletedResults: result.deletedResults,
|
|
419
|
+
reapedTombstones: result.reapedTombstones,
|
|
420
|
+
skipped: result.skipped,
|
|
421
|
+
deleted: deletedIds.slice(0, ASYNC_RETENTION_BATCH_SIZE),
|
|
422
|
+
errors: result.errors.length,
|
|
423
|
+
rawReads: result.rawReads,
|
|
424
|
+
sourceExhausted: result.sourceExhausted,
|
|
425
|
+
discoveryDurationMs: result.discoveryDurationMs,
|
|
426
|
+
commitDurationMs: result.commitDurationMs,
|
|
427
|
+
cancelled: result.cancelled,
|
|
428
|
+
workerFailed: result.workerFailed,
|
|
429
|
+
durationMs: result.durationMs
|
|
430
|
+
})}
|
|
431
|
+
`, "utf-8");
|
|
432
|
+
}
|
|
433
|
+
function increment(record, key) {
|
|
434
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
class RetentionCancelledError extends Error {
|
|
438
|
+
}
|
|
439
|
+
function isRecord(value) {
|
|
440
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
441
|
+
}
|
|
442
|
+
function safeRelative(relative) {
|
|
443
|
+
return relative.length > 0 && !path.isAbsolute(relative) && !relative.split(path.sep).includes("..");
|
|
444
|
+
}
|
|
445
|
+
function parseDiscoveryResult(message, passId, runBudget, resultBudget) {
|
|
446
|
+
if (!isRecord(message) || message.type !== "result" || message.passId !== passId)
|
|
447
|
+
throw new Error("Retention discovery worker returned a stale or malformed pass id.");
|
|
448
|
+
if (!Number.isFinite(message.discoveryDurationMs) || message.discoveryDurationMs < 0)
|
|
449
|
+
throw new Error("Retention discovery worker returned an invalid duration.");
|
|
450
|
+
if (!Number.isInteger(message.rawReads) || message.rawReads < 0)
|
|
451
|
+
throw new Error("Retention discovery worker returned an invalid raw-read count.");
|
|
452
|
+
if (!isRecord(message.sourceExhausted) || Object.values(message.sourceExhausted).some((value) => typeof value !== "boolean"))
|
|
453
|
+
throw new Error("Retention discovery worker returned invalid source telemetry.");
|
|
454
|
+
if (!Array.isArray(message.runCandidates) || message.runCandidates.length > runBudget)
|
|
455
|
+
throw new Error("Retention discovery worker exceeded the run budget.");
|
|
456
|
+
if (!Array.isArray(message.resultCandidates) || message.resultCandidates.length > resultBudget)
|
|
457
|
+
throw new Error("Retention discovery worker exceeded the result budget.");
|
|
458
|
+
if (!Array.isArray(message.cursorOps) || message.cursorOps.length > runBudget + resultBudget + 8)
|
|
459
|
+
throw new Error("Retention discovery worker returned invalid cursor operations.");
|
|
460
|
+
for (const candidate of message.runCandidates) {
|
|
461
|
+
if (!isRecord(candidate) || typeof candidate.name !== "string" || typeof candidate.relative !== "string" || !validRunId(candidate.name) || candidate.relative !== candidate.name)
|
|
462
|
+
throw new Error("Retention discovery worker returned an unsafe run candidate.");
|
|
463
|
+
}
|
|
464
|
+
const resultKinds = new Set(["public", "pending", "replay", "archive", "tombstone"]);
|
|
465
|
+
const resultKeys = new Set(["resultPublicAfter", "resultReplayAfter", "resultArchiveAfter"]);
|
|
466
|
+
for (const candidate of message.resultCandidates) {
|
|
467
|
+
if (!isRecord(candidate) || typeof candidate.name !== "string" || typeof candidate.relative !== "string" || path.basename(candidate.name) !== candidate.name || !safeRelative(candidate.relative) || path.basename(candidate.relative) !== candidate.name || !resultKinds.has(candidate.kind) || !isRecord(candidate.cursor))
|
|
468
|
+
throw new Error("Retention discovery worker returned an unsafe result candidate.");
|
|
469
|
+
const cursor = candidate.cursor;
|
|
470
|
+
if (cursor.type === "pending") {
|
|
471
|
+
if (typeof cursor.session !== "string" || !validRunId(cursor.session) || path.dirname(candidate.relative) !== path.join("result-pending", cursor.session))
|
|
472
|
+
throw new Error("Retention discovery worker returned an invalid pending candidate.");
|
|
473
|
+
} else if (cursor.type === "result") {
|
|
474
|
+
if (typeof cursor.key !== "string" || !resultKeys.has(cursor.key))
|
|
475
|
+
throw new Error("Retention discovery worker returned an invalid result cursor.");
|
|
476
|
+
const expectedDir = cursor.key === "resultPublicAfter" ? "." : cursor.key === "resultReplayAfter" ? "completion-replay" : "output-archives";
|
|
477
|
+
if (path.dirname(candidate.relative) !== expectedDir)
|
|
478
|
+
throw new Error("Retention discovery worker returned a result from the wrong source.");
|
|
479
|
+
} else
|
|
480
|
+
throw new Error("Retention discovery worker returned an invalid result cursor target.");
|
|
481
|
+
}
|
|
482
|
+
const scalarKeys = new Set(["runAfter", "resultPublicAfter", "resultReplayAfter", "resultArchiveAfter", "pendingSessionAfter"]);
|
|
483
|
+
for (const operation of message.cursorOps) {
|
|
484
|
+
if (!isRecord(operation) || typeof operation.type !== "string")
|
|
485
|
+
throw new Error("Retention discovery worker returned a malformed cursor operation.");
|
|
486
|
+
if (operation.type === "set" || operation.type === "delete") {
|
|
487
|
+
if (typeof operation.key !== "string" || !scalarKeys.has(operation.key) || operation.type === "set" && (typeof operation.value !== "string" || !safeRelative(operation.value)))
|
|
488
|
+
throw new Error("Retention discovery worker returned an invalid scalar cursor operation.");
|
|
489
|
+
} else if (operation.type === "set-pending" || operation.type === "delete-pending") {
|
|
490
|
+
if (typeof operation.session !== "string" || !validRunId(operation.session) || operation.type === "set-pending" && (typeof operation.value !== "string" || !safeRelative(operation.value)))
|
|
491
|
+
throw new Error("Retention discovery worker returned an invalid pending cursor operation.");
|
|
492
|
+
} else
|
|
493
|
+
throw new Error("Retention discovery worker returned an unknown cursor operation.");
|
|
494
|
+
}
|
|
495
|
+
return message;
|
|
496
|
+
}
|
|
497
|
+
function runRetentionDiscovery(input) {
|
|
498
|
+
return new Promise((resolve, reject) => {
|
|
499
|
+
if (input.signal?.aborted) {
|
|
500
|
+
reject(new RetentionCancelledError("Retention discovery was cancelled."));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const worker = new Worker(input.workerUrl);
|
|
504
|
+
let settled = false;
|
|
505
|
+
const cleanup = () => {
|
|
506
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
507
|
+
worker.terminate();
|
|
508
|
+
};
|
|
509
|
+
const fail = (error) => {
|
|
510
|
+
if (settled)
|
|
511
|
+
return;
|
|
512
|
+
settled = true;
|
|
513
|
+
cleanup();
|
|
514
|
+
reject(error);
|
|
515
|
+
};
|
|
516
|
+
const onAbort = () => fail(new RetentionCancelledError("Retention discovery was cancelled."));
|
|
517
|
+
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
518
|
+
worker.once("error", (error) => fail(error instanceof Error ? error : new Error(String(error))));
|
|
519
|
+
worker.once("exit", (code) => {
|
|
520
|
+
if (!settled)
|
|
521
|
+
fail(new Error(`Retention discovery worker exited before replying (${code}).`));
|
|
522
|
+
});
|
|
523
|
+
worker.once("message", (message) => {
|
|
524
|
+
if (settled)
|
|
525
|
+
return;
|
|
526
|
+
try {
|
|
527
|
+
if (isRecord(message) && message.type === "error" && message.passId === input.passId)
|
|
528
|
+
throw new Error(`Retention discovery failed: ${String(message.error)}`);
|
|
529
|
+
const discovery = parseDiscoveryResult(message, input.passId, input.runBudget, input.resultBudget);
|
|
530
|
+
settled = true;
|
|
531
|
+
cleanup();
|
|
532
|
+
resolve(discovery);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
worker.postMessage({ type: "discover", passId: input.passId, asyncDirRoot: input.asyncDirRoot, resultsDir: input.resultsDir, cursor: input.cursor, runBudget: input.runBudget, resultBudget: input.resultBudget });
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function applyCursorOperations(cursor, operations) {
|
|
541
|
+
for (const operation of operations) {
|
|
542
|
+
if (operation.type === "set")
|
|
543
|
+
cursor[operation.key] = operation.value;
|
|
544
|
+
else if (operation.type === "delete")
|
|
545
|
+
delete cursor[operation.key];
|
|
546
|
+
else if (operation.type === "set-pending") {
|
|
547
|
+
cursor.resultPendingAfterBySession ??= {};
|
|
548
|
+
cursor.resultPendingAfterBySession[operation.session] = operation.value;
|
|
549
|
+
} else {
|
|
550
|
+
delete cursor.resultPendingAfterBySession?.[operation.session];
|
|
551
|
+
if (cursor.resultPendingAfterBySession && Object.keys(cursor.resultPendingAfterBySession).length === 0)
|
|
552
|
+
delete cursor.resultPendingAfterBySession;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
export async function cleanupAsyncRetention(options) {
|
|
557
|
+
const startedWall = Date.now();
|
|
558
|
+
const now = options.now ?? Date.now;
|
|
559
|
+
const currentTime = now();
|
|
560
|
+
const retentionMs = options.retentionMs ?? RETENTION_MS;
|
|
561
|
+
const tombstoneGraceMs = options.tombstoneGraceMs ?? ASYNC_RETENTION_TOMBSTONE_GRACE_MS;
|
|
562
|
+
const batchSize = Math.min(ASYNC_RETENTION_BATCH_SIZE, Math.max(1, Math.trunc(options.batchSize ?? ASYNC_RETENTION_BATCH_SIZE)));
|
|
563
|
+
const randomId = options.randomId ?? randomUUID;
|
|
564
|
+
const maintenanceRoot = options.maintenanceRoot ?? path.dirname(options.asyncDirRoot);
|
|
565
|
+
const lockDir = path.join(maintenanceRoot, LOCK_NAME);
|
|
566
|
+
const lockToken = randomId();
|
|
567
|
+
const pid = options.pid ?? process.pid;
|
|
568
|
+
const hostname = options.hostname ?? os.hostname();
|
|
569
|
+
const getProcessStartIdentity = options.getProcessStartIdentity ?? processStartIdentity;
|
|
570
|
+
const currentProcessStartIdentity = options.processStartIdentity ?? getProcessStartIdentity(pid) ?? (pid === process.pid ? `runtime:${Math.round(Date.now() - process.uptime() * 1000)}` : undefined);
|
|
571
|
+
const lockOwner = { version: 1, token: lockToken, pid, hostname, startedAt: currentTime, ...currentProcessStartIdentity ? { processStartIdentity: currentProcessStartIdentity } : {} };
|
|
572
|
+
const lockOptions = { hostname, isProcessAlive: options.isProcessAlive ?? processIsAlive, getProcessStartIdentity };
|
|
573
|
+
const result = {
|
|
574
|
+
acquired: false,
|
|
575
|
+
scanned: 0,
|
|
576
|
+
repairedRuns: 0,
|
|
577
|
+
deletedRuns: 0,
|
|
578
|
+
deletedResults: 0,
|
|
579
|
+
reapedTombstones: 0,
|
|
580
|
+
skipped: {},
|
|
581
|
+
errors: [],
|
|
582
|
+
rawReads: 0,
|
|
583
|
+
sourceExhausted: {},
|
|
584
|
+
discoveryDurationMs: 0,
|
|
585
|
+
commitDurationMs: 0,
|
|
586
|
+
cancelled: false,
|
|
587
|
+
workerFailed: false,
|
|
588
|
+
durationMs: 0
|
|
589
|
+
};
|
|
590
|
+
const deletedIds = [];
|
|
591
|
+
const lstatSync = options.lstatSync ?? fs.lstatSync;
|
|
592
|
+
let commitStartedAt;
|
|
593
|
+
let cursorCommitUnsafe = false;
|
|
594
|
+
const finish = () => {
|
|
595
|
+
if (commitStartedAt !== undefined)
|
|
596
|
+
result.commitDurationMs = Math.max(0, Date.now() - commitStartedAt);
|
|
597
|
+
result.durationMs = Math.max(0, Date.now() - startedWall);
|
|
598
|
+
appendMaintenanceLog(maintenanceRoot, result, currentTime, deletedIds);
|
|
599
|
+
return result;
|
|
600
|
+
};
|
|
601
|
+
const markCancelled = () => {
|
|
602
|
+
if (!options.signal?.aborted)
|
|
603
|
+
return false;
|
|
604
|
+
result.cancelled = true;
|
|
605
|
+
increment(result.skipped, "cancelled");
|
|
606
|
+
return true;
|
|
607
|
+
};
|
|
608
|
+
fs.mkdirSync(maintenanceRoot, { recursive: true });
|
|
609
|
+
if (!acquireRetentionLock(lockDir, lockOwner, lockOptions)) {
|
|
610
|
+
increment(result.skipped, "lock-busy");
|
|
611
|
+
return finish();
|
|
612
|
+
}
|
|
613
|
+
result.acquired = true;
|
|
614
|
+
try {
|
|
615
|
+
if (markCancelled())
|
|
616
|
+
return finish();
|
|
617
|
+
const protectedRunIds = new Set(options.protectedRunIds ?? []);
|
|
618
|
+
const cutoff = currentTime - retentionMs;
|
|
619
|
+
const cursor = readCursor(maintenanceRoot);
|
|
620
|
+
const runBudget = Math.ceil(batchSize / 2);
|
|
621
|
+
const resultBudget = batchSize - runBudget;
|
|
622
|
+
const discoveryStartedAt = Date.now();
|
|
623
|
+
let discovery;
|
|
624
|
+
try {
|
|
625
|
+
discovery = await runRetentionDiscovery({
|
|
626
|
+
passId: randomUUID(),
|
|
627
|
+
asyncDirRoot: options.asyncDirRoot,
|
|
628
|
+
resultsDir: options.resultsDir,
|
|
629
|
+
cursor,
|
|
630
|
+
runBudget,
|
|
631
|
+
resultBudget,
|
|
632
|
+
workerUrl: options.discoveryWorkerUrl ?? new URL("../../../async-retention-discovery-worker.mjs", import.meta.url),
|
|
633
|
+
signal: options.signal
|
|
634
|
+
});
|
|
635
|
+
} catch (error) {
|
|
636
|
+
result.discoveryDurationMs = Math.max(0, Date.now() - discoveryStartedAt);
|
|
637
|
+
if (error instanceof RetentionCancelledError) {
|
|
638
|
+
result.cancelled = true;
|
|
639
|
+
increment(result.skipped, "cancelled");
|
|
640
|
+
} else {
|
|
641
|
+
result.workerFailed = true;
|
|
642
|
+
increment(result.skipped, "worker-failure");
|
|
643
|
+
result.errors.push(compactError(error));
|
|
644
|
+
}
|
|
645
|
+
return finish();
|
|
646
|
+
}
|
|
647
|
+
result.discoveryDurationMs = Math.max(0, Date.now() - discoveryStartedAt);
|
|
648
|
+
result.rawReads = discovery.rawReads;
|
|
649
|
+
result.sourceExhausted = discovery.sourceExhausted;
|
|
650
|
+
commitStartedAt = Date.now();
|
|
651
|
+
if (markCancelled())
|
|
652
|
+
return finish();
|
|
653
|
+
if (parseLockOwner(lockDir)?.token !== lockToken) {
|
|
654
|
+
increment(result.skipped, "lock-owner-changed");
|
|
655
|
+
return finish();
|
|
656
|
+
}
|
|
657
|
+
const waitSubscriptionsDir = options.waitSubscriptionsDir ?? path.join(maintenanceRoot, "wait-subscriptions");
|
|
658
|
+
const waitReferences = parseWaitRunIds(waitSubscriptionsDir);
|
|
659
|
+
if (!waitReferences.safe) {
|
|
660
|
+
increment(result.skipped, "wait-references-unknown");
|
|
661
|
+
return finish();
|
|
662
|
+
}
|
|
663
|
+
for (const entry of discovery.runCandidates) {
|
|
664
|
+
if (markCancelled())
|
|
665
|
+
return finish();
|
|
666
|
+
result.scanned += 1;
|
|
667
|
+
const runDir = path.join(options.asyncDirRoot, entry.name);
|
|
668
|
+
let mutationStateChanged = false;
|
|
669
|
+
try {
|
|
670
|
+
const stat = lstatSync(runDir);
|
|
671
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
672
|
+
increment(result.skipped, "unsafe-run-path");
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
let status = readStatus(runDir);
|
|
676
|
+
if (status?.state === "running" && validRunId(status.runId) && path.basename(runDir) === status.runId && !protectedRunIds.has(status.runId)) {
|
|
677
|
+
const reconciliation = reconcileAsyncRun(runDir, {
|
|
678
|
+
resultsDir: options.resultsDir,
|
|
679
|
+
kill: options.reconcileKill,
|
|
680
|
+
now
|
|
681
|
+
});
|
|
682
|
+
status = reconciliation.status ?? undefined;
|
|
683
|
+
if (reconciliation.repaired)
|
|
684
|
+
result.repairedRuns += 1;
|
|
685
|
+
}
|
|
686
|
+
const initialReason = runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
|
|
687
|
+
if (initialReason) {
|
|
688
|
+
increment(result.skipped, initialReason);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (entry.name.startsWith(RUN_TOMBSTONE_PREFIX)) {
|
|
692
|
+
if (!runTombstoneMarkerMatches(maintenanceRoot, status.runId, runDir)) {
|
|
693
|
+
increment(result.skipped, "run-tombstone-marker");
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (currentTime - stat.mtimeMs < tombstoneGraceMs) {
|
|
697
|
+
increment(result.skipped, "tombstone-grace");
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
|
|
701
|
+
const finalReason = freshWaitReferences.safe ? runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
702
|
+
if (finalReason) {
|
|
703
|
+
increment(result.skipped, `recheck-${finalReason}`);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
mutationStateChanged = true;
|
|
707
|
+
fs.rmSync(runDir, { recursive: true });
|
|
708
|
+
removeRunTombstoneMarker(maintenanceRoot, status.runId);
|
|
709
|
+
result.reapedTombstones += 1;
|
|
710
|
+
deletedIds.push(`run-tombstone:${status.runId}`);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const tombstone = path.join(options.asyncDirRoot, `${RUN_TOMBSTONE_PREFIX}${randomId()}`);
|
|
714
|
+
writeRunTombstoneMarker(maintenanceRoot, status.runId, tombstone, currentTime);
|
|
715
|
+
mutationStateChanged = true;
|
|
716
|
+
try {
|
|
717
|
+
fs.renameSync(runDir, tombstone);
|
|
718
|
+
} catch (error) {
|
|
719
|
+
removeRunTombstoneMarker(maintenanceRoot, status.runId);
|
|
720
|
+
mutationStateChanged = false;
|
|
721
|
+
throw error;
|
|
722
|
+
}
|
|
723
|
+
const recheckStatus = readStatus(tombstone);
|
|
724
|
+
const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
|
|
725
|
+
const recheckReason = freshWaitReferences.safe ? runSkipReason({ runDir: tombstone, status: recheckStatus, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
726
|
+
if (recheckReason) {
|
|
727
|
+
if (!fs.existsSync(runDir)) {
|
|
728
|
+
fs.renameSync(tombstone, runDir);
|
|
729
|
+
mutationStateChanged = false;
|
|
730
|
+
}
|
|
731
|
+
removeRunTombstoneMarker(maintenanceRoot, status.runId);
|
|
732
|
+
if (mutationStateChanged)
|
|
733
|
+
cursorCommitUnsafe = true;
|
|
734
|
+
increment(result.skipped, `recheck-${recheckReason}`);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
fs.rmSync(tombstone, { recursive: true });
|
|
738
|
+
removeRunTombstoneMarker(maintenanceRoot, status.runId);
|
|
739
|
+
result.deletedRuns += 1;
|
|
740
|
+
deletedIds.push(`run:${status.runId}`);
|
|
741
|
+
} catch (error) {
|
|
742
|
+
if (mutationStateChanged)
|
|
743
|
+
cursorCommitUnsafe = true;
|
|
744
|
+
if (!isNotFound(error))
|
|
745
|
+
result.errors.push(compactError(error));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
for (const discoveredCandidate of discovery.resultCandidates) {
|
|
749
|
+
if (markCancelled())
|
|
750
|
+
return finish();
|
|
751
|
+
result.scanned += 1;
|
|
752
|
+
const candidate = { ...discoveredCandidate, path: path.join(options.resultsDir, discoveredCandidate.relative) };
|
|
753
|
+
let mutationStateChanged = false;
|
|
754
|
+
try {
|
|
755
|
+
const stat = lstatSync(candidate.path);
|
|
756
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
757
|
+
increment(result.skipped, "unsafe-result-path");
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const data = readJson(candidate.path);
|
|
761
|
+
const reason = resultSkipReason({ candidate, data, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
|
|
762
|
+
if (reason) {
|
|
763
|
+
increment(result.skipped, reason);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
if (candidate.kind === "tombstone") {
|
|
767
|
+
if (currentTime - stat.mtimeMs < tombstoneGraceMs) {
|
|
768
|
+
increment(result.skipped, "tombstone-grace");
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
|
|
772
|
+
const finalReason = freshWaitReferences.safe ? resultSkipReason({ candidate, data, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
773
|
+
if (finalReason) {
|
|
774
|
+
increment(result.skipped, `recheck-${finalReason}`);
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
fs.rmSync(candidate.path);
|
|
778
|
+
mutationStateChanged = true;
|
|
779
|
+
result.reapedTombstones += 1;
|
|
780
|
+
deletedIds.push(`result-tombstone:${resultRunId(candidate, data)}`);
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const tombstone = path.join(path.dirname(candidate.path), `${RESULT_TOMBSTONE_PREFIX}${candidate.kind}-${randomId()}`);
|
|
784
|
+
fs.renameSync(candidate.path, tombstone);
|
|
785
|
+
mutationStateChanged = true;
|
|
786
|
+
const tombstoneCandidate = { ...candidate, path: tombstone, kind: "tombstone" };
|
|
787
|
+
const recheckData = readJson(tombstone);
|
|
788
|
+
const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
|
|
789
|
+
const recheckReason = freshWaitReferences.safe ? resultSkipReason({ candidate: tombstoneCandidate, data: recheckData, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
790
|
+
if (recheckReason) {
|
|
791
|
+
if (!fs.existsSync(candidate.path)) {
|
|
792
|
+
fs.renameSync(tombstone, candidate.path);
|
|
793
|
+
mutationStateChanged = false;
|
|
794
|
+
}
|
|
795
|
+
if (mutationStateChanged)
|
|
796
|
+
cursorCommitUnsafe = true;
|
|
797
|
+
increment(result.skipped, `recheck-${recheckReason}`);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
fs.rmSync(tombstone);
|
|
801
|
+
result.deletedResults += 1;
|
|
802
|
+
deletedIds.push(`result:${resultRunId(candidate, data)}`);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
if (mutationStateChanged)
|
|
805
|
+
cursorCommitUnsafe = true;
|
|
806
|
+
if (!isNotFound(error))
|
|
807
|
+
result.errors.push(compactError(error));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (cursorCommitUnsafe) {
|
|
811
|
+
increment(result.skipped, "commit-failure");
|
|
812
|
+
return finish();
|
|
813
|
+
}
|
|
814
|
+
if (markCancelled())
|
|
815
|
+
return finish();
|
|
816
|
+
if (parseLockOwner(lockDir)?.token !== lockToken) {
|
|
817
|
+
increment(result.skipped, "lock-owner-changed");
|
|
818
|
+
return finish();
|
|
819
|
+
}
|
|
820
|
+
applyCursorOperations(cursor, discovery.cursorOps);
|
|
821
|
+
writeAtomicJson(path.join(maintenanceRoot, CURSOR_NAME), cursor);
|
|
822
|
+
return finish();
|
|
823
|
+
} finally {
|
|
824
|
+
try {
|
|
825
|
+
releaseRetentionLock(lockDir, lockToken);
|
|
826
|
+
} catch {}
|
|
827
|
+
}
|
|
828
|
+
}
|