@pentoshi/clai 3.9.2 → 3.9.4
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/dist/agent/compaction-attempt.d.ts +22 -0
- package/dist/agent/compaction-attempt.js +69 -0
- package/dist/agent/compaction-attempt.js.map +1 -0
- package/dist/agent/compaction-summary.d.ts +27 -0
- package/dist/agent/compaction-summary.js +62 -0
- package/dist/agent/compaction-summary.js.map +1 -1
- package/dist/agent/context-breakdown.d.ts +2 -0
- package/dist/agent/context-breakdown.js +37 -0
- package/dist/agent/context-breakdown.js.map +1 -1
- package/dist/agent/context-manager.d.ts +7 -1
- package/dist/agent/context-manager.js +64 -15
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/continue-orient.d.ts +8 -0
- package/dist/agent/continue-orient.js +26 -3
- package/dist/agent/continue-orient.js.map +1 -1
- package/dist/agent/durable-envelope.d.ts +39 -0
- package/dist/agent/durable-envelope.js +195 -0
- package/dist/agent/durable-envelope.js.map +1 -0
- package/dist/agent/events.d.ts +21 -0
- package/dist/agent/loop-guard.d.ts +13 -5
- package/dist/agent/loop-guard.js +114 -1
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/outcomes.d.ts +29 -0
- package/dist/agent/outcomes.js +118 -3
- package/dist/agent/outcomes.js.map +1 -1
- package/dist/agent/plan-decision.js +8 -3
- package/dist/agent/plan-decision.js.map +1 -1
- package/dist/agent/plan-tool.d.ts +5 -0
- package/dist/agent/plan-tool.js +166 -6
- package/dist/agent/plan-tool.js.map +1 -1
- package/dist/agent/reliability-policy.d.ts +11 -6
- package/dist/agent/reliability-policy.js +19 -13
- package/dist/agent/reliability-policy.js.map +1 -1
- package/dist/agent/request-budget.d.ts +24 -0
- package/dist/agent/request-budget.js +59 -0
- package/dist/agent/request-budget.js.map +1 -0
- package/dist/agent/responder-context.js +2 -2
- package/dist/agent/responder-context.js.map +1 -1
- package/dist/agent/responder-parent.d.ts +30 -0
- package/dist/agent/responder-parent.js +82 -0
- package/dist/agent/responder-parent.js.map +1 -0
- package/dist/agent/runner.d.ts +15 -2
- package/dist/agent/runner.js +766 -213
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/scope-context.d.ts +5 -0
- package/dist/agent/scope-context.js +7 -1
- package/dist/agent/scope-context.js.map +1 -1
- package/dist/agent/session-policy.js +1 -0
- package/dist/agent/session-policy.js.map +1 -1
- package/dist/agent/stop-summary.js +13 -7
- package/dist/agent/stop-summary.js.map +1 -1
- package/dist/agent/task-analyzer.js +7 -3
- package/dist/agent/task-analyzer.js.map +1 -1
- package/dist/agent/task-evidence.js +2 -0
- package/dist/agent/task-evidence.js.map +1 -1
- package/dist/agent/task-sync.d.ts +11 -0
- package/dist/agent/task-sync.js +31 -0
- package/dist/agent/task-sync.js.map +1 -1
- package/dist/agent/tool-call-parser.d.ts +6 -13
- package/dist/agent/tool-call-parser.js +186 -82
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/agent/tool-history.d.ts +8 -2
- package/dist/agent/tool-history.js +8 -1
- package/dist/agent/tool-history.js.map +1 -1
- package/dist/agent/turn-outcome.d.ts +4 -1
- package/dist/agent/turn-outcome.js +16 -2
- package/dist/agent/turn-outcome.js.map +1 -1
- package/dist/app/adapters/agent-event-adapter.js +27 -0
- package/dist/app/adapters/agent-event-adapter.js.map +1 -1
- package/dist/app/adapters/current-agent-adapter.js +1 -0
- package/dist/app/adapters/current-agent-adapter.js.map +1 -1
- package/dist/app/adapters/current-interactive-sessions-adapter.d.ts +4 -0
- package/dist/app/adapters/current-interactive-sessions-adapter.js +11 -0
- package/dist/app/adapters/current-interactive-sessions-adapter.js.map +1 -0
- package/dist/app/adapters/current-jobs-adapter.js +4 -3
- package/dist/app/adapters/current-jobs-adapter.js.map +1 -1
- package/dist/app/adapters/current-store-adapter.js +4 -3
- package/dist/app/adapters/current-store-adapter.js.map +1 -1
- package/dist/app/adapters/current-updates-adapter.d.ts +1 -1
- package/dist/app/adapters/current-updates-adapter.js +34 -5
- package/dist/app/adapters/current-updates-adapter.js.map +1 -1
- package/dist/app/adapters/quiet-meta-tools.js +2 -1
- package/dist/app/adapters/quiet-meta-tools.js.map +1 -1
- package/dist/app/controllers/cancel-all-result.d.ts +8 -0
- package/dist/app/controllers/cancel-all-result.js +32 -0
- package/dist/app/controllers/cancel-all-result.js.map +1 -0
- package/dist/app/controllers/job-controller.d.ts +4 -3
- package/dist/app/controllers/job-controller.js +9 -6
- package/dist/app/controllers/job-controller.js.map +1 -1
- package/dist/app/controllers/plan-controller.js +14 -0
- package/dist/app/controllers/plan-controller.js.map +1 -1
- package/dist/app/controllers/responder-settlement.js +2 -2
- package/dist/app/controllers/responder-settlement.js.map +1 -1
- package/dist/app/controllers/session-compact-helper.d.ts +23 -1
- package/dist/app/controllers/session-compact-helper.js +117 -7
- package/dist/app/controllers/session-compact-helper.js.map +1 -1
- package/dist/app/controllers/session-context-usage.d.ts +24 -0
- package/dist/app/controllers/session-context-usage.js +89 -0
- package/dist/app/controllers/session-context-usage.js.map +1 -0
- package/dist/app/controllers/session-controller.d.ts +25 -17
- package/dist/app/controllers/session-controller.js +142 -128
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/controllers/session-prompt-queue.d.ts +3 -0
- package/dist/app/controllers/session-prompt-queue.js +20 -1
- package/dist/app/controllers/session-prompt-queue.js.map +1 -1
- package/dist/app/controllers/session-responder.js +16 -6
- package/dist/app/controllers/session-responder.js.map +1 -1
- package/dist/app/controllers/session-turn-request.d.ts +18 -0
- package/dist/app/controllers/session-turn-request.js +31 -0
- package/dist/app/controllers/session-turn-request.js.map +1 -0
- package/dist/app/controllers/turn-continuation.d.ts +14 -0
- package/dist/app/controllers/turn-continuation.js +46 -0
- package/dist/app/controllers/turn-continuation.js.map +1 -0
- package/dist/app/events/app-event.d.ts +21 -0
- package/dist/app/events/app-event.js +1 -0
- package/dist/app/events/app-event.js.map +1 -1
- package/dist/app/ports/agent-port.d.ts +7 -0
- package/dist/app/ports/interactive-sessions-port.d.ts +21 -0
- package/dist/app/ports/interactive-sessions-port.js +9 -0
- package/dist/app/ports/interactive-sessions-port.js.map +1 -0
- package/dist/app/ports/jobs-port.d.ts +4 -3
- package/dist/app/ports/persistence-port.d.ts +11 -0
- package/dist/app/ports/updates-port.d.ts +8 -0
- package/dist/attachments/service.js +13 -2
- package/dist/attachments/service.js.map +1 -1
- package/dist/commands/providers.js +13 -10
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/update.d.ts +4 -0
- package/dist/commands/update.js +14 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +21 -8
- package/dist/index.js.map +1 -1
- package/dist/interactive-session/artifact-writer.d.ts +54 -0
- package/dist/interactive-session/artifact-writer.js +161 -0
- package/dist/interactive-session/artifact-writer.js.map +1 -0
- package/dist/interactive-session/cleanup.d.ts +36 -0
- package/dist/interactive-session/cleanup.js +157 -0
- package/dist/interactive-session/cleanup.js.map +1 -0
- package/dist/interactive-session/config.d.ts +118 -0
- package/dist/interactive-session/config.js +141 -0
- package/dist/interactive-session/config.js.map +1 -0
- package/dist/interactive-session/input-policy.d.ts +41 -0
- package/dist/interactive-session/input-policy.js +163 -0
- package/dist/interactive-session/input-policy.js.map +1 -0
- package/dist/interactive-session/manager.d.ts +107 -0
- package/dist/interactive-session/manager.js +927 -0
- package/dist/interactive-session/manager.js.map +1 -0
- package/dist/interactive-session/output-store.d.ts +81 -0
- package/dist/interactive-session/output-store.js +239 -0
- package/dist/interactive-session/output-store.js.map +1 -0
- package/dist/interactive-session/output-view.d.ts +20 -0
- package/dist/interactive-session/output-view.js +142 -0
- package/dist/interactive-session/output-view.js.map +1 -0
- package/dist/interactive-session/recovery-journal.d.ts +55 -0
- package/dist/interactive-session/recovery-journal.js +115 -0
- package/dist/interactive-session/recovery-journal.js.map +1 -0
- package/dist/interactive-session/registry.d.ts +59 -0
- package/dist/interactive-session/registry.js +170 -0
- package/dist/interactive-session/registry.js.map +1 -0
- package/dist/interactive-session/runtime.d.ts +107 -0
- package/dist/interactive-session/runtime.js +191 -0
- package/dist/interactive-session/runtime.js.map +1 -0
- package/dist/interactive-session/session-runtime.d.ts +50 -0
- package/dist/interactive-session/session-runtime.js +63 -0
- package/dist/interactive-session/session-runtime.js.map +1 -0
- package/dist/interactive-session/streaming-redactor.d.ts +53 -0
- package/dist/interactive-session/streaming-redactor.js +263 -0
- package/dist/interactive-session/streaming-redactor.js.map +1 -0
- package/dist/interactive-session/telemetry.d.ts +30 -0
- package/dist/interactive-session/telemetry.js +47 -0
- package/dist/interactive-session/telemetry.js.map +1 -0
- package/dist/interactive-session/transport-factory.d.ts +8 -0
- package/dist/interactive-session/transport-factory.js +16 -0
- package/dist/interactive-session/transport-factory.js.map +1 -0
- package/dist/interactive-session/transport-node-pty.d.ts +49 -0
- package/dist/interactive-session/transport-node-pty.js +219 -0
- package/dist/interactive-session/transport-node-pty.js.map +1 -0
- package/dist/interactive-session/transport-pipe.d.ts +9 -0
- package/dist/interactive-session/transport-pipe.js +215 -0
- package/dist/interactive-session/transport-pipe.js.map +1 -0
- package/dist/interactive-session/transport.d.ts +104 -0
- package/dist/interactive-session/transport.js +89 -0
- package/dist/interactive-session/transport.js.map +1 -0
- package/dist/interactive-session/types.d.ts +254 -0
- package/dist/interactive-session/types.js +191 -0
- package/dist/interactive-session/types.js.map +1 -0
- package/dist/llm/adapters/anthropic-tools.d.ts +14 -1
- package/dist/llm/adapters/anthropic-tools.js +36 -5
- package/dist/llm/adapters/anthropic-tools.js.map +1 -1
- package/dist/llm/adapters/gemini-tools.d.ts +3 -1
- package/dist/llm/adapters/gemini-tools.js +5 -2
- package/dist/llm/adapters/gemini-tools.js.map +1 -1
- package/dist/llm/adapters/ollama-tools.js +8 -3
- package/dist/llm/adapters/ollama-tools.js.map +1 -1
- package/dist/llm/adapters/openai-tools.js +8 -1
- package/dist/llm/adapters/openai-tools.js.map +1 -1
- package/dist/llm/agentrouter.js +2 -0
- package/dist/llm/agentrouter.js.map +1 -1
- package/dist/llm/anthropic.d.ts +8 -0
- package/dist/llm/anthropic.js +68 -9
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/aws-mantle.js +60 -12
- package/dist/llm/aws-mantle.js.map +1 -1
- package/dist/llm/bynara.js +2 -0
- package/dist/llm/bynara.js.map +1 -1
- package/dist/llm/capabilities.d.ts +7 -0
- package/dist/llm/capabilities.js +22 -0
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/gemini.d.ts +3 -0
- package/dist/llm/gemini.js +71 -11
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/groq.d.ts +20 -1
- package/dist/llm/groq.js +32 -4
- package/dist/llm/groq.js.map +1 -1
- package/dist/llm/http.d.ts +29 -0
- package/dist/llm/http.js +122 -29
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/kimchi.js +2 -0
- package/dist/llm/kimchi.js.map +1 -1
- package/dist/llm/nvidia.js +2 -0
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/ollama.d.ts +5 -0
- package/dist/llm/ollama.js +53 -6
- package/dist/llm/ollama.js.map +1 -1
- package/dist/llm/openai.js +2 -0
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/openrouter.js +2 -0
- package/dist/llm/openrouter.js.map +1 -1
- package/dist/llm/qwen-cloud.js +2 -0
- package/dist/llm/qwen-cloud.js.map +1 -1
- package/dist/llm/router.d.ts +16 -0
- package/dist/llm/router.js +79 -23
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/sampling.d.ts +32 -0
- package/dist/llm/sampling.js +48 -0
- package/dist/llm/sampling.js.map +1 -0
- package/dist/llm/stream-progress.d.ts +4 -0
- package/dist/llm/stream-progress.js +43 -0
- package/dist/llm/stream-progress.js.map +1 -0
- package/dist/llm/system-messages.d.ts +29 -0
- package/dist/llm/system-messages.js +51 -0
- package/dist/llm/system-messages.js.map +1 -0
- package/dist/llm/token-usage.d.ts +4 -1
- package/dist/llm/token-usage.js +56 -4
- package/dist/llm/token-usage.js.map +1 -1
- package/dist/llm/tool-protocol.js +30 -0
- package/dist/llm/tool-protocol.js.map +1 -1
- package/dist/os/process-identity.d.ts +46 -0
- package/dist/os/process-identity.js +147 -0
- package/dist/os/process-identity.js.map +1 -0
- package/dist/os/process-tree.d.ts +15 -0
- package/dist/os/process-tree.js +77 -0
- package/dist/os/process-tree.js.map +1 -0
- package/dist/prompts/embedded.js +1 -1
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/system.agent.md +13 -12
- package/dist/repl/prompt-line.d.ts +5 -0
- package/dist/repl/prompt-line.js +7 -1
- package/dist/repl/prompt-line.js.map +1 -1
- package/dist/repl.js +71 -41
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.js +98 -44
- package/dist/safety/classifier.js.map +1 -1
- package/dist/safety/engagement-policy.d.ts +8 -0
- package/dist/safety/engagement-policy.js +52 -7
- package/dist/safety/engagement-policy.js.map +1 -1
- package/dist/safety/patterns.d.ts +44 -0
- package/dist/safety/patterns.js +168 -37
- package/dist/safety/patterns.js.map +1 -1
- package/dist/store/config.d.ts +9 -1
- package/dist/store/config.js +51 -1
- package/dist/store/config.js.map +1 -1
- package/dist/store/history-index.d.ts +12 -0
- package/dist/store/history-index.js +47 -18
- package/dist/store/history-index.js.map +1 -1
- package/dist/store/history.d.ts +5 -2
- package/dist/store/history.js +153 -62
- package/dist/store/history.js.map +1 -1
- package/dist/store/plan.d.ts +81 -5
- package/dist/store/plan.js +406 -46
- package/dist/store/plan.js.map +1 -1
- package/dist/store/redaction-cache.d.ts +12 -0
- package/dist/store/redaction-cache.js +51 -0
- package/dist/store/redaction-cache.js.map +1 -0
- package/dist/store/responder-settlement.d.ts +5 -1
- package/dist/store/responder-settlement.js +42 -30
- package/dist/store/responder-settlement.js.map +1 -1
- package/dist/store/scope.js +36 -27
- package/dist/store/scope.js.map +1 -1
- package/dist/store/task-transitions.d.ts +19 -0
- package/dist/store/task-transitions.js +46 -0
- package/dist/store/task-transitions.js.map +1 -0
- package/dist/tools/capabilities.js +33 -18
- package/dist/tools/capabilities.js.map +1 -1
- package/dist/tools/command-intent.d.ts +24 -0
- package/dist/tools/command-intent.js +158 -4
- package/dist/tools/command-intent.js.map +1 -1
- package/dist/tools/definitions.d.ts +5 -1
- package/dist/tools/definitions.js +257 -21
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/fs.d.ts +16 -0
- package/dist/tools/fs.js +122 -46
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/image.js +1 -1
- package/dist/tools/image.js.map +1 -1
- package/dist/tools/interactive-session-tools.d.ts +11 -0
- package/dist/tools/interactive-session-tools.js +261 -0
- package/dist/tools/interactive-session-tools.js.map +1 -0
- package/dist/tools/jobs.d.ts +78 -4
- package/dist/tools/jobs.js +486 -127
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/net-ping-sweep.d.ts +3 -1
- package/dist/tools/net-ping-sweep.js +64 -26
- package/dist/tools/net-ping-sweep.js.map +1 -1
- package/dist/tools/nmap-runner.js +5 -2
- package/dist/tools/nmap-runner.js.map +1 -1
- package/dist/tools/pdf.js +50 -7
- package/dist/tools/pdf.js.map +1 -1
- package/dist/tools/registry.js +158 -96
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.d.ts +24 -2
- package/dist/tools/shell.js +113 -28
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/tool-types.d.ts +2 -0
- package/dist/tools/validate.d.ts +8 -5
- package/dist/tools/validate.js +177 -7
- package/dist/tools/validate.js.map +1 -1
- package/dist/tools/wordlists.js +69 -17
- package/dist/tools/wordlists.js.map +1 -1
- package/dist/tui/state.d.ts +4 -0
- package/dist/tui/state.js +88 -2
- package/dist/tui/state.js.map +1 -1
- package/dist/tui-v2/actions/keymap.js +3 -8
- package/dist/tui-v2/actions/keymap.js.map +1 -1
- package/dist/tui-v2/app/App.js +52 -4
- package/dist/tui-v2/app/App.js.map +1 -1
- package/dist/tui-v2/app/commands/config-commands.js +5 -2
- package/dist/tui-v2/app/commands/config-commands.js.map +1 -1
- package/dist/tui-v2/app/commands/picker-commands.js +3 -0
- package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
- package/dist/tui-v2/app/plan-lifecycle.d.ts +1 -1
- package/dist/tui-v2/app/plan-lifecycle.js +2 -1
- package/dist/tui-v2/app/plan-lifecycle.js.map +1 -1
- package/dist/tui-v2/bootstrap/composition-root.d.ts +3 -0
- package/dist/tui-v2/bootstrap/composition-root.js +3 -0
- package/dist/tui-v2/bootstrap/composition-root.js.map +1 -1
- package/dist/tui-v2/bootstrap/start-tui-v2.js +10 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
- package/dist/tui-v2/components/composer/composer-input-box.d.ts +1 -1
- package/dist/tui-v2/components/composer/composer-input-box.js +12 -3
- package/dist/tui-v2/components/composer/composer-input-box.js.map +1 -1
- package/dist/tui-v2/components/jobs/jobs-panel.js +19 -6
- package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
- package/dist/tui-v2/components/plan/plan-view.js +9 -6
- package/dist/tui-v2/components/plan/plan-view.js.map +1 -1
- package/dist/tui-v2/components/status/status-line.d.ts +15 -0
- package/dist/tui-v2/components/status/status-line.js +33 -25
- package/dist/tui-v2/components/status/status-line.js.map +1 -1
- package/dist/tui-v2/components/transcript/assistant-message.js +17 -9
- package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
- package/dist/tui-v2/components/transcript/compacted-row.js +49 -22
- package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
- package/dist/tui-v2/components/transcript/intro-card.js +3 -0
- package/dist/tui-v2/components/transcript/intro-card.js.map +1 -1
- package/dist/tui-v2/components/transcript/thinking-block.js +2 -1
- package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
- package/dist/tui-v2/components/transcript/transcript-view.js +27 -2
- package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -1
- package/dist/tui-v2/components/transcript/use-transcript-selection.js +57 -6
- package/dist/tui-v2/components/transcript/use-transcript-selection.js.map +1 -1
- package/dist/tui-v2/composer/composer-editor.js +12 -6
- package/dist/tui-v2/composer/composer-editor.js.map +1 -1
- package/dist/tui-v2/rendering/incremental-strip.d.ts +11 -0
- package/dist/tui-v2/rendering/incremental-strip.js +89 -0
- package/dist/tui-v2/rendering/incremental-strip.js.map +1 -0
- package/dist/tui-v2/rendering/plan-view.d.ts +12 -0
- package/dist/tui-v2/rendering/plan-view.js +71 -2
- package/dist/tui-v2/rendering/plan-view.js.map +1 -1
- package/dist/tui-v2/rendering/streaming-markdown.d.ts +25 -0
- package/dist/tui-v2/rendering/streaming-markdown.js +108 -0
- package/dist/tui-v2/rendering/streaming-markdown.js.map +1 -0
- package/dist/tui-v2/rendering/thinking-tail.d.ts +5 -0
- package/dist/tui-v2/rendering/thinking-tail.js +26 -0
- package/dist/tui-v2/rendering/thinking-tail.js.map +1 -0
- package/dist/tui-v2/state/transcript-hydrate.js +6 -2
- package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
- package/dist/tui-v2/state/transcript-reducer.js +146 -25
- package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
- package/dist/tui-v2/state/transcript-types.d.ts +6 -0
- package/dist/tui-v2/state/transcript-types.js +17 -0
- package/dist/tui-v2/state/transcript-types.js.map +1 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.js.map +1 -1
- package/dist/ui/intro-header.d.ts +1 -0
- package/dist/ui/intro-header.js +10 -13
- package/dist/ui/intro-header.js.map +1 -1
- package/dist/ui/markdown.js +9 -2
- package/dist/ui/markdown.js.map +1 -1
- package/dist/ui/mentions.d.ts +5 -0
- package/dist/ui/mentions.js +66 -7
- package/dist/ui/mentions.js.map +1 -1
- package/dist/ui/thinking.d.ts +5 -1
- package/dist/ui/thinking.js +3 -2
- package/dist/ui/thinking.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/package.json +2 -1
package/dist/tools/jobs.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { spawn
|
|
2
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { mkdir, open, stat } from "node:fs/promises";
|
|
4
|
-
import { basename, join } from "node:path";
|
|
4
|
+
import { basename, join, sep } from "node:path";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { StringDecoder } from "node:string_decoder";
|
|
7
7
|
import { redactSecrets } from "../llm/provider.js";
|
|
8
8
|
import { safeCwd } from "../os/cwd.js";
|
|
9
9
|
import { getJobsDir } from "../store/paths.js";
|
|
10
10
|
import { resolveShell } from "./shell.js";
|
|
11
|
+
import { terminateProcessTree } from "../os/process-tree.js";
|
|
12
|
+
import { forgetProcessIdentity, processIdentity } from "../os/process-identity.js";
|
|
11
13
|
export function formatJobElapsed(job, now = Date.now()) {
|
|
12
14
|
const startedAt = Date.parse(job.startedAt);
|
|
13
15
|
const endedAt = job.endedAt ? Date.parse(job.endedAt) : now;
|
|
@@ -31,6 +33,15 @@ function commandDisplay(command) {
|
|
|
31
33
|
return command;
|
|
32
34
|
return command.display ?? [command.command, ...command.argv].map(displayArg).join(" ");
|
|
33
35
|
}
|
|
36
|
+
function launchFollowUp(id, responder) {
|
|
37
|
+
return responder
|
|
38
|
+
? "Responder owns completion tracking for this self-completing job. " +
|
|
39
|
+
"Do not poll it with shell.tail/shell.jobs, sleep, or read its artifact. " +
|
|
40
|
+
"Continue other work; if not already delivered, its terminal result will be delivered automatically."
|
|
41
|
+
: "OS launch does not prove application readiness or continued liveness. " +
|
|
42
|
+
`This is a normal background job: use shell.tail {"id":"${id}"} with nextOffset and shell.jobs for status; ` +
|
|
43
|
+
"for a server or watcher, also run an application readiness probe. Do not launch a duplicate.";
|
|
44
|
+
}
|
|
34
45
|
const PER_FILE_BYTES = 1024 * 1024;
|
|
35
46
|
const MAX_STREAM_BYTES = 16 * 1024 * 1024;
|
|
36
47
|
const DEFAULT_TAIL_BYTES = 8_000;
|
|
@@ -42,10 +53,16 @@ const MAX_DURABLE_TERMINAL_JOBS = 80;
|
|
|
42
53
|
const TERMINAL_JOB_MAX_AGE_MS = 48 * 60 * 60 * 1000;
|
|
43
54
|
const ARCHIVED_UNSETTLED_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
44
55
|
const MAX_ARCHIVED_UNSETTLED_NOTIFICATIONS = 40;
|
|
56
|
+
const MAX_SUPERSEDED_REVISIONS = 5;
|
|
57
|
+
const SETTLEMENT_MAX_BACKOFF_MS = 30_000;
|
|
58
|
+
const LIVENESS_WATCH_INTERVAL_MS = 2_000;
|
|
59
|
+
const SETTLEMENT_DEAD_LETTER_MS = 10 * 60_000;
|
|
45
60
|
/** Max lines shell.jobs returns to the model (running first, then recent). */
|
|
46
61
|
const LIST_JOBS_MAX_LINES = 40;
|
|
47
62
|
/** Coalesce window for chatty stdout/stderr progress persistence + UI events. */
|
|
48
63
|
const PROGRESS_FLUSH_MS = 250;
|
|
64
|
+
/** Durable registry cadence for running-job progress (UI updates stay at 250ms). */
|
|
65
|
+
const DURABLE_PROGRESS_FLUSH_MS = 5_000;
|
|
49
66
|
/**
|
|
50
67
|
* A live job with no in-process ChildProcess handle (resumed session, or a
|
|
51
68
|
* handle that was released) is only declared "lost" after failing the liveness
|
|
@@ -55,6 +72,8 @@ const PROGRESS_FLUSH_MS = 250;
|
|
|
55
72
|
* elapsed, and a ✗ status=lost that later flipped back to exit=0).
|
|
56
73
|
*/
|
|
57
74
|
const LIVENESS_LOST_GRACE_MS = 8_000;
|
|
75
|
+
/** Minimum spacing between liveness probes for the same job. */
|
|
76
|
+
const LIVENESS_PROBE_INTERVAL_MS = 1_000;
|
|
58
77
|
/**
|
|
59
78
|
* Number of trailing bytes in `buf` that form an incomplete multi-byte UTF-8
|
|
60
79
|
* sequence (a lead byte whose continuation bytes were cut off by the read
|
|
@@ -91,27 +110,6 @@ function processAlive(pid) {
|
|
|
91
110
|
return error.code === "EPERM";
|
|
92
111
|
}
|
|
93
112
|
}
|
|
94
|
-
function processIdentity(pid) {
|
|
95
|
-
if (!pid || process.platform === "win32")
|
|
96
|
-
return undefined;
|
|
97
|
-
try {
|
|
98
|
-
// Start time only: it is invariant for a pid's lifetime and changes on
|
|
99
|
-
// reuse, so it is a sufficient pid-reuse guard. The command line is
|
|
100
|
-
// deliberately excluded — `sh -c "cmd"` execs into `cmd`, mutating
|
|
101
|
-
// `ps command=` mid-run, which made a live job fail its OWN identity check
|
|
102
|
-
// (stop refused with "process identity no longer matches", liveness falsely
|
|
103
|
-
// marked lost, and double-Esc cancel unable to kill the process).
|
|
104
|
-
const value = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
105
|
-
encoding: "utf8",
|
|
106
|
-
timeout: 2_000,
|
|
107
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
108
|
-
}).trim();
|
|
109
|
-
return value ? createHash("sha256").update(value).digest("hex") : undefined;
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
113
|
/**
|
|
116
114
|
* Heuristic for legacy registry rows that were actually agent tool-stall
|
|
117
115
|
* trackers (commandDisplay = "fs.list /path", "shell.jobs {}", …), not real
|
|
@@ -268,7 +266,9 @@ export class JobManager {
|
|
|
268
266
|
authorizationTimers = new Map();
|
|
269
267
|
responderLeases = new Map();
|
|
270
268
|
settlementTimers = new Map();
|
|
271
|
-
|
|
269
|
+
pendingSettlements = new Map();
|
|
270
|
+
consumedResponderResults = new Map();
|
|
271
|
+
livenessWatchTimer;
|
|
272
272
|
finalizations = new Map();
|
|
273
273
|
listeners = new Set();
|
|
274
274
|
/**
|
|
@@ -278,6 +278,7 @@ export class JobManager {
|
|
|
278
278
|
* observed alive again or finalized.
|
|
279
279
|
*/
|
|
280
280
|
livenessMisses = new Map();
|
|
281
|
+
livenessCheckedAt = new Map();
|
|
281
282
|
registryRetryTimer;
|
|
282
283
|
registryPath;
|
|
283
284
|
transientV2RegistryPath;
|
|
@@ -286,10 +287,84 @@ export class JobManager {
|
|
|
286
287
|
this.registryPath = join(this.jobsDir, REGISTRY_FILE);
|
|
287
288
|
this.transientV2RegistryPath = join(this.jobsDir, TRANSIENT_V2_REGISTRY_FILE);
|
|
288
289
|
this.loadAndReconcile();
|
|
290
|
+
this.sweepOrphanArtifacts();
|
|
289
291
|
}
|
|
290
292
|
isDurable(job) {
|
|
291
293
|
return job.kind !== "ephemeral";
|
|
292
294
|
}
|
|
295
|
+
artifactPathsOf(job) {
|
|
296
|
+
return [
|
|
297
|
+
job.artifactPath,
|
|
298
|
+
job.stdoutArtifact,
|
|
299
|
+
job.stderrArtifact,
|
|
300
|
+
...(job.artifacts?.stdout.chunks ?? []),
|
|
301
|
+
...(job.artifacts?.stderr.chunks ?? []),
|
|
302
|
+
].filter((path) => Boolean(path));
|
|
303
|
+
}
|
|
304
|
+
/** Delete a dropped job's artifact chunks, never one still referenced. */
|
|
305
|
+
removeJobArtifacts(job) {
|
|
306
|
+
const paths = this.artifactPathsOf(job).filter((path) => path.startsWith(`${this.jobsDir}${sep}`));
|
|
307
|
+
if (paths.length === 0)
|
|
308
|
+
return;
|
|
309
|
+
const referenced = new Set();
|
|
310
|
+
for (const other of this.jobs.values()) {
|
|
311
|
+
for (const path of this.artifactPathsOf(other))
|
|
312
|
+
referenced.add(path);
|
|
313
|
+
}
|
|
314
|
+
for (const path of new Set(paths)) {
|
|
315
|
+
if (referenced.has(path))
|
|
316
|
+
continue;
|
|
317
|
+
try {
|
|
318
|
+
rmSync(path, { force: true });
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Artifact cleanup is best-effort; a locked file is retried on boot.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** Drop a job row plus its per-job caches and artifacts. */
|
|
326
|
+
forgetJob(id) {
|
|
327
|
+
const job = this.jobs.get(id);
|
|
328
|
+
this.jobs.delete(id);
|
|
329
|
+
for (const [notificationId, notification] of this.notifications) {
|
|
330
|
+
if (notification.jobId === id)
|
|
331
|
+
this.notifications.delete(notificationId);
|
|
332
|
+
}
|
|
333
|
+
this.consumedResponderResults.delete(id);
|
|
334
|
+
this.livenessMisses.delete(id);
|
|
335
|
+
this.livenessCheckedAt.delete(id);
|
|
336
|
+
if (!job)
|
|
337
|
+
return;
|
|
338
|
+
forgetProcessIdentity(job.pid);
|
|
339
|
+
this.removeJobArtifacts(job);
|
|
340
|
+
}
|
|
341
|
+
/** Remove artifact files whose job row no longer exists. */
|
|
342
|
+
sweepOrphanArtifacts() {
|
|
343
|
+
let names;
|
|
344
|
+
try {
|
|
345
|
+
names = readdirSync(this.jobsDir);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const known = new Set(this.jobs.keys());
|
|
351
|
+
const now = Date.now();
|
|
352
|
+
for (const name of names) {
|
|
353
|
+
if (!/\.(stdout|stderr)\.log(\.\d+)?$/.test(name))
|
|
354
|
+
continue;
|
|
355
|
+
if ([...known].some((id) => name.includes(`-${id}.`)))
|
|
356
|
+
continue;
|
|
357
|
+
const path = join(this.jobsDir, name);
|
|
358
|
+
try {
|
|
359
|
+
if (now - statSync(path).mtimeMs <= TERMINAL_JOB_MAX_AGE_MS)
|
|
360
|
+
continue;
|
|
361
|
+
rmSync(path, { force: true });
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Ignore unreadable/locked leftovers.
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
293
368
|
isLive(job) {
|
|
294
369
|
return (job.status === "running" ||
|
|
295
370
|
job.status === "starting" ||
|
|
@@ -355,18 +430,17 @@ export class JobManager {
|
|
|
355
430
|
if (!current || (leaseId && current !== leaseId))
|
|
356
431
|
return;
|
|
357
432
|
this.responderLeases.delete(sessionId);
|
|
358
|
-
const archivedAt = new Date().toISOString();
|
|
359
433
|
let changed = false;
|
|
360
434
|
for (const notification of this.notifications.values()) {
|
|
361
|
-
if (notification.ownerSessionId
|
|
362
|
-
notification.responderLeaseId
|
|
363
|
-
|
|
435
|
+
if (notification.ownerSessionId !== sessionId ||
|
|
436
|
+
notification.responderLeaseId !== current) {
|
|
437
|
+
continue;
|
|
364
438
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
notification.
|
|
439
|
+
this.claimedNotifications.delete(notification.id);
|
|
440
|
+
// Releasing a runtime lease must not discard an unread durable result;
|
|
441
|
+
// detach it so the next lease for this session adopts it.
|
|
442
|
+
if (!notification.acknowledgedAt && !notification.archivedAt) {
|
|
443
|
+
delete notification.responderLeaseId;
|
|
370
444
|
changed = true;
|
|
371
445
|
}
|
|
372
446
|
}
|
|
@@ -399,10 +473,60 @@ export class JobManager {
|
|
|
399
473
|
JSON.stringify(stdoutArtifact) ||
|
|
400
474
|
JSON.stringify(existing.stderrArtifact) !==
|
|
401
475
|
JSON.stringify(stderrArtifact)));
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
476
|
+
const resultHash = createHash("sha256")
|
|
477
|
+
.update([
|
|
478
|
+
job.status,
|
|
479
|
+
endedAt,
|
|
480
|
+
String(job.exitCode ?? ""),
|
|
481
|
+
String(job.signal ?? ""),
|
|
482
|
+
JSON.stringify(stdoutArtifact),
|
|
483
|
+
JSON.stringify(stderrArtifact),
|
|
484
|
+
].join("\0"))
|
|
485
|
+
.digest("hex")
|
|
486
|
+
.slice(0, 24);
|
|
487
|
+
const consumed = this.consumedResponderResults.get(job.id);
|
|
488
|
+
if (!existing && consumed?.resultHash === resultHash) {
|
|
489
|
+
return { created: false, updated: false };
|
|
490
|
+
}
|
|
491
|
+
const correctsConsumed = Boolean(!existing && consumed && consumed.resultHash !== resultHash);
|
|
492
|
+
if (correctsConsumed)
|
|
493
|
+
this.consumedResponderResults.delete(job.id);
|
|
494
|
+
// A materially different authoritative result is a new revision: every
|
|
495
|
+
// delivery/read/analysis marker belonged to the superseded result and must
|
|
496
|
+
// not suppress review of the corrected one.
|
|
497
|
+
const supersedes = Boolean(existing &&
|
|
498
|
+
completionChanged &&
|
|
499
|
+
(existing.resultHash === undefined || existing.resultHash !== resultHash));
|
|
500
|
+
const resultRevision = supersedes
|
|
501
|
+
? (existing?.resultRevision ?? 1) + 1
|
|
502
|
+
: correctsConsumed
|
|
503
|
+
? (consumed?.resultRevision ?? 1) + 1
|
|
504
|
+
: (existing?.resultRevision ?? 1);
|
|
505
|
+
const supersededRevisions = supersedes && existing
|
|
506
|
+
? [
|
|
507
|
+
...(existing.supersededRevisions ?? []),
|
|
508
|
+
{
|
|
509
|
+
resultRevision: existing.resultRevision ?? 1,
|
|
510
|
+
resultHash: existing.resultHash ?? "",
|
|
511
|
+
status: existing.status,
|
|
512
|
+
endedAt: existing.endedAt,
|
|
513
|
+
...(existing.exitCode !== undefined
|
|
514
|
+
? { exitCode: existing.exitCode }
|
|
515
|
+
: {}),
|
|
516
|
+
...(existing.signal !== undefined ? { signal: existing.signal } : {}),
|
|
517
|
+
...(existing.deliveredAt ? { deliveredAt: existing.deliveredAt } : {}),
|
|
518
|
+
...(existing.readAt ? { readAt: existing.readAt } : {}),
|
|
519
|
+
...(existing.analyzedAt ? { analyzedAt: existing.analyzedAt } : {}),
|
|
520
|
+
...(existing.acknowledgedAt
|
|
521
|
+
? { acknowledgedAt: existing.acknowledgedAt }
|
|
522
|
+
: {}),
|
|
523
|
+
...(existing.settledAt ? { settledAt: existing.settledAt } : {}),
|
|
524
|
+
},
|
|
525
|
+
].slice(-MAX_SUPERSEDED_REVISIONS)
|
|
526
|
+
: existing?.supersededRevisions;
|
|
527
|
+
const carried = supersedes ? undefined : existing;
|
|
528
|
+
const activeLeaseId = this.responderLeases.get(job.ownerSessionId);
|
|
529
|
+
const responderLeaseId = activeLeaseId ?? job.responderLeaseId ?? existing?.responderLeaseId;
|
|
406
530
|
const notification = {
|
|
407
531
|
id: existing?.id ??
|
|
408
532
|
`completion:${createHash("sha256").update(`${job.ownerSessionId}\0${job.id}\0${job.startedAt}`).digest("hex").slice(0, 24)}`,
|
|
@@ -423,21 +547,34 @@ export class JobManager {
|
|
|
423
547
|
...(job.signal !== undefined ? { signal: job.signal } : {}),
|
|
424
548
|
...(job.monitor !== undefined ? { monitor: job.monitor } : {}),
|
|
425
549
|
...(responderLeaseId ? { responderLeaseId } : {}),
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
...(
|
|
429
|
-
|
|
550
|
+
resultRevision,
|
|
551
|
+
resultHash,
|
|
552
|
+
...(supersededRevisions?.length ? { supersededRevisions } : {}),
|
|
553
|
+
...(carried?.deliveryStartedAt
|
|
554
|
+
? { deliveryStartedAt: carried.deliveryStartedAt }
|
|
555
|
+
: {}),
|
|
556
|
+
...(carried?.deliveredAt ? { deliveredAt: carried.deliveredAt } : {}),
|
|
557
|
+
...(carried?.readAt ? { readAt: carried.readAt } : {}),
|
|
558
|
+
...((carried?.analyzedAt ?? carried?.acknowledgedAt)
|
|
559
|
+
? { analyzedAt: carried?.analyzedAt ?? carried?.acknowledgedAt }
|
|
560
|
+
: {}),
|
|
561
|
+
...(carried?.acknowledgedAt
|
|
562
|
+
? { acknowledgedAt: carried.acknowledgedAt }
|
|
430
563
|
: {}),
|
|
431
|
-
...(
|
|
432
|
-
|
|
564
|
+
...(carried?.settledAt ? { settledAt: carried.settledAt } : {}),
|
|
565
|
+
...(carried?.discardedAt
|
|
566
|
+
? {
|
|
567
|
+
discardedAt: carried.discardedAt,
|
|
568
|
+
...(carried.discardReason
|
|
569
|
+
? { discardReason: carried.discardReason }
|
|
570
|
+
: {}),
|
|
571
|
+
wakeOnCompletion: false,
|
|
572
|
+
}
|
|
433
573
|
: {}),
|
|
434
|
-
...(
|
|
435
|
-
...(!leaseIsActive
|
|
436
|
-
? { archivedAt: existing?.archivedAt ?? endedAt }
|
|
437
|
-
: existing?.archivedAt
|
|
438
|
-
? { archivedAt: existing.archivedAt }
|
|
439
|
-
: {}),
|
|
574
|
+
...(carried?.archivedAt ? { archivedAt: carried.archivedAt } : {}),
|
|
440
575
|
};
|
|
576
|
+
if (supersedes)
|
|
577
|
+
this.claimedNotifications.delete(notification.id);
|
|
441
578
|
const updated = Boolean(existing && JSON.stringify(existing) !== JSON.stringify(notification));
|
|
442
579
|
this.notifications.set(notification.id, notification);
|
|
443
580
|
return { notification, created: !existing, updated };
|
|
@@ -514,11 +651,22 @@ export class JobManager {
|
|
|
514
651
|
}
|
|
515
652
|
}
|
|
516
653
|
/** Reconcile a restored process that no longer has a ChildProcess close event. */
|
|
517
|
-
refreshJobLiveness(job) {
|
|
654
|
+
refreshJobLiveness(job, options = {}) {
|
|
518
655
|
if (!this.isLive(job) || this.processes.has(job.id)) {
|
|
519
656
|
this.livenessMisses.delete(job.id);
|
|
657
|
+
this.livenessCheckedAt.delete(job.id);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
// Subscribers can ask for state many times per frame; one probe per job per
|
|
661
|
+
// interval is enough because the lost grace window is far longer.
|
|
662
|
+
const checkedAt = this.livenessCheckedAt.get(job.id);
|
|
663
|
+
const probeAt = Date.now();
|
|
664
|
+
if (!options.force &&
|
|
665
|
+
checkedAt !== undefined &&
|
|
666
|
+
probeAt - checkedAt < LIVENESS_PROBE_INTERVAL_MS) {
|
|
520
667
|
return;
|
|
521
668
|
}
|
|
669
|
+
this.livenessCheckedAt.set(job.id, probeAt);
|
|
522
670
|
// Primary signal is the pid itself. If the process is alive we keep the job
|
|
523
671
|
// running and only ever declare "lost" on a PROVEN pid reuse (both the
|
|
524
672
|
// stored and the current identity are present AND differ). An identity read
|
|
@@ -532,6 +680,9 @@ export class JobManager {
|
|
|
532
680
|
return;
|
|
533
681
|
}
|
|
534
682
|
}
|
|
683
|
+
else {
|
|
684
|
+
forgetProcessIdentity(job.pid);
|
|
685
|
+
}
|
|
535
686
|
// The process looks gone (or is a proven pid reuse). Require the miss to
|
|
536
687
|
// persist across a grace window before finalizing — a single transient
|
|
537
688
|
// hiccup must not kill a job whose real close event is still in flight.
|
|
@@ -570,47 +721,122 @@ export class JobManager {
|
|
|
570
721
|
clearTimeout(existing);
|
|
571
722
|
const timer = setTimeout(() => {
|
|
572
723
|
this.settlementTimers.delete(job.id);
|
|
724
|
+
const revision = this.notificationForJob(job.id)?.resultRevision ?? 1;
|
|
573
725
|
void import("../store/responder-settlement.js")
|
|
574
|
-
.then(({ settleResponderJob }) => settleResponderJob({ ...job }))
|
|
726
|
+
.then(({ settleResponderJob }) => settleResponderJob({ ...job }, { resultRevision: revision }))
|
|
575
727
|
.then((result) => {
|
|
576
728
|
if (result === "applied" || result === "noop") {
|
|
577
|
-
this.
|
|
578
|
-
const notification = this.notificationForJob(job.id);
|
|
579
|
-
if (notification && !notification.settledAt) {
|
|
580
|
-
notification.settledAt = new Date().toISOString();
|
|
581
|
-
if (!this.persistSync())
|
|
582
|
-
this.scheduleRegistryRetry();
|
|
583
|
-
this.emit({
|
|
584
|
-
type: "notification",
|
|
585
|
-
jobId: job.id,
|
|
586
|
-
notificationId: notification.id,
|
|
587
|
-
});
|
|
588
|
-
}
|
|
729
|
+
this.completeSettlement(job.id, revision);
|
|
589
730
|
return;
|
|
590
731
|
}
|
|
591
|
-
|
|
592
|
-
this.settlementAttempts.set(job.id, attempt);
|
|
593
|
-
if (attempt < 8) {
|
|
594
|
-
this.scheduleTaskSettlement(job, Math.min(5_000, 50 * 2 ** attempt));
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
this.settlementAttempts.delete(job.id);
|
|
598
|
-
}
|
|
732
|
+
this.retrySettlement(job, revision, result);
|
|
599
733
|
})
|
|
600
|
-
.catch(() => {
|
|
601
|
-
|
|
602
|
-
this.settlementAttempts.set(job.id, attempt);
|
|
603
|
-
if (attempt < 8) {
|
|
604
|
-
this.scheduleTaskSettlement(job, Math.min(5_000, 50 * 2 ** attempt));
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
this.settlementAttempts.delete(job.id);
|
|
608
|
-
}
|
|
734
|
+
.catch((error) => {
|
|
735
|
+
this.retrySettlement(job, revision, error instanceof Error ? error.message : String(error));
|
|
609
736
|
});
|
|
610
737
|
}, delayMs);
|
|
611
738
|
timer.unref?.();
|
|
612
739
|
this.settlementTimers.set(job.id, timer);
|
|
613
740
|
}
|
|
741
|
+
completeSettlement(jobId, resultRevision) {
|
|
742
|
+
this.pendingSettlements.delete(jobId);
|
|
743
|
+
const notification = this.notificationForJob(jobId);
|
|
744
|
+
if (!notification ||
|
|
745
|
+
(notification.resultRevision ?? 1) !== resultRevision ||
|
|
746
|
+
notification.settledAt) {
|
|
747
|
+
if (!this.persistSync())
|
|
748
|
+
this.scheduleRegistryRetry();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
notification.settledAt = new Date().toISOString();
|
|
752
|
+
if (!this.persistSync())
|
|
753
|
+
this.scheduleRegistryRetry();
|
|
754
|
+
this.emit({
|
|
755
|
+
type: "notification",
|
|
756
|
+
jobId,
|
|
757
|
+
notificationId: notification.id,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Retry until the projection lands. The marker is durable, so a crash or a
|
|
762
|
+
* long-lived plan-write conflict cannot silently strand a green child, and an
|
|
763
|
+
* unrecoverable case is dead-lettered with a visible reason.
|
|
764
|
+
*/
|
|
765
|
+
retrySettlement(job, resultRevision, reason) {
|
|
766
|
+
const now = new Date().toISOString();
|
|
767
|
+
const previous = this.pendingSettlements.get(job.id);
|
|
768
|
+
const carryOver = previous && previous.resultRevision === resultRevision ? previous : undefined;
|
|
769
|
+
const attempts = (carryOver?.attempts ?? 0) + 1;
|
|
770
|
+
const firstAttemptAt = carryOver?.firstAttemptAt ?? now;
|
|
771
|
+
const elapsed = Date.now() - Date.parse(firstAttemptAt);
|
|
772
|
+
const deadLettered = Number.isFinite(elapsed) && elapsed > SETTLEMENT_DEAD_LETTER_MS;
|
|
773
|
+
this.pendingSettlements.set(job.id, {
|
|
774
|
+
jobId: job.id,
|
|
775
|
+
resultRevision,
|
|
776
|
+
attempts,
|
|
777
|
+
firstAttemptAt,
|
|
778
|
+
lastAttemptAt: now,
|
|
779
|
+
lastReason: reason,
|
|
780
|
+
...(deadLettered ? { deadLetteredAt: now } : {}),
|
|
781
|
+
});
|
|
782
|
+
if (!this.persistSync())
|
|
783
|
+
this.scheduleRegistryRetry();
|
|
784
|
+
this.emit({ type: "job", jobId: job.id });
|
|
785
|
+
if (deadLettered)
|
|
786
|
+
return;
|
|
787
|
+
this.scheduleTaskSettlement(job, Math.min(SETTLEMENT_MAX_BACKOFF_MS, 50 * 2 ** Math.min(attempts, 10)));
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* One coalesced, unref'd watcher that reconciles restored live jobs without a
|
|
791
|
+
* UI read. Without it a headless session can leave a finished responder job
|
|
792
|
+
* "running" forever, because liveness was only refreshed by list/get calls.
|
|
793
|
+
*/
|
|
794
|
+
scheduleLivenessWatch() {
|
|
795
|
+
if (this.livenessWatchTimer)
|
|
796
|
+
return;
|
|
797
|
+
const restored = [...this.jobs.values()].filter((job) => this.isDurable(job) && this.isLive(job) && !this.processes.has(job.id));
|
|
798
|
+
if (restored.length === 0)
|
|
799
|
+
return;
|
|
800
|
+
const timer = setInterval(() => {
|
|
801
|
+
const pending = [...this.jobs.values()].filter((job) => this.isDurable(job) && this.isLive(job) && !this.processes.has(job.id));
|
|
802
|
+
for (const job of pending)
|
|
803
|
+
this.refreshJobLiveness(job);
|
|
804
|
+
const remaining = [...this.jobs.values()].some((job) => this.isDurable(job) && this.isLive(job) && !this.processes.has(job.id));
|
|
805
|
+
if (remaining)
|
|
806
|
+
return;
|
|
807
|
+
clearInterval(timer);
|
|
808
|
+
this.livenessWatchTimer = undefined;
|
|
809
|
+
}, LIVENESS_WATCH_INTERVAL_MS);
|
|
810
|
+
timer.unref?.();
|
|
811
|
+
this.livenessWatchTimer = timer;
|
|
812
|
+
}
|
|
813
|
+
/** Ownership guard: a receipt may only be mutated by its owning session. */
|
|
814
|
+
ownsNotification(notification, sessionId) {
|
|
815
|
+
if (!notification)
|
|
816
|
+
return false;
|
|
817
|
+
return sessionId === undefined || notification.ownerSessionId === sessionId;
|
|
818
|
+
}
|
|
819
|
+
/** Receipt for a launch that was deduplicated by its delegation key. */
|
|
820
|
+
startJobReceipt(job, reason) {
|
|
821
|
+
return {
|
|
822
|
+
ok: true,
|
|
823
|
+
output: `Reusing background job id=${job.id} (${reason}) pid=${job.pid ?? "?"} status=${job.status}\n` +
|
|
824
|
+
`Command: ${job.commandDisplay}\nArtifact: ${job.stdoutArtifact}\n` +
|
|
825
|
+
`The duplicate launch was not started. ${launchFollowUp(job.id, job.responder === true)}`,
|
|
826
|
+
outputPath: job.stdoutArtifact,
|
|
827
|
+
backgroundJob: {
|
|
828
|
+
id: job.id,
|
|
829
|
+
status: job.status,
|
|
830
|
+
artifactPath: job.stdoutArtifact,
|
|
831
|
+
...(job.responder ? { responder: true } : {}),
|
|
832
|
+
nextOffset: 0,
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
/** Terminal results whose plan child could not be settled yet. */
|
|
837
|
+
getPendingSettlements() {
|
|
838
|
+
return [...this.pendingSettlements.values()];
|
|
839
|
+
}
|
|
614
840
|
/**
|
|
615
841
|
* Register an in-flight tool for stall tracking only.
|
|
616
842
|
* Never appears in shell.jobs and never touches the durable registry.
|
|
@@ -644,7 +870,7 @@ export class JobManager {
|
|
|
644
870
|
if (!this.isLive(job))
|
|
645
871
|
this.clearAuthorizationTimer(id);
|
|
646
872
|
if (!this.isDurable(job) && !this.isLive(job)) {
|
|
647
|
-
this.
|
|
873
|
+
this.forgetJob(id);
|
|
648
874
|
this.emit({ type: "job", jobId: id });
|
|
649
875
|
return;
|
|
650
876
|
}
|
|
@@ -661,6 +887,14 @@ export class JobManager {
|
|
|
661
887
|
return { ok: false, output: `Engagement authorization for ${options.authorization.target} has expired.`, exitCode: 1 };
|
|
662
888
|
}
|
|
663
889
|
}
|
|
890
|
+
if (options?.delegationId) {
|
|
891
|
+
// A retried tool call must not spawn a second scanner: the delegation key
|
|
892
|
+
// identifies the work, so the existing job is returned unchanged.
|
|
893
|
+
const existing = [...this.jobs.values()].find((candidate) => candidate.delegationId === options.delegationId &&
|
|
894
|
+
candidate.ownerSessionId === (options.ownerSessionId ?? "unknown"));
|
|
895
|
+
if (existing)
|
|
896
|
+
return this.startJobReceipt(existing, "existing delegation");
|
|
897
|
+
}
|
|
664
898
|
const id = randomUUID().slice(0, 8);
|
|
665
899
|
const cwd = options?.cwd ?? safeCwd();
|
|
666
900
|
try {
|
|
@@ -691,7 +925,7 @@ export class JobManager {
|
|
|
691
925
|
exitCode: 127,
|
|
692
926
|
};
|
|
693
927
|
}
|
|
694
|
-
await mkdir(this.jobsDir, { recursive: true });
|
|
928
|
+
await mkdir(this.jobsDir, { recursive: true, mode: 0o700 });
|
|
695
929
|
const prefix = `${new Date().toISOString().replace(/[:.]/g, "-")}-${id}`;
|
|
696
930
|
const stdoutArtifact = join(this.jobsDir, `${prefix}.stdout.log`);
|
|
697
931
|
const stderrArtifact = join(this.jobsDir, `${prefix}.stderr.log`);
|
|
@@ -721,6 +955,7 @@ export class JobManager {
|
|
|
721
955
|
...(options?.name ? { name: options.name } : {}),
|
|
722
956
|
...(options?.taskId ? { taskId: options.taskId } : {}),
|
|
723
957
|
...(options?.parentTaskId ? { parentTaskId: options.parentTaskId } : {}),
|
|
958
|
+
...(options?.delegationId ? { delegationId: options.delegationId } : {}),
|
|
724
959
|
...(options?.wakeOnCompletion !== undefined ? { wakeOnCompletion: options.wakeOnCompletion } : {}),
|
|
725
960
|
...(options?.responder !== undefined ? { responder: options.responder } : {}),
|
|
726
961
|
...(monitor !== undefined ? { monitor } : {}),
|
|
@@ -735,7 +970,7 @@ export class JobManager {
|
|
|
735
970
|
await this.persist();
|
|
736
971
|
}
|
|
737
972
|
catch {
|
|
738
|
-
this.
|
|
973
|
+
this.forgetJob(id);
|
|
739
974
|
return {
|
|
740
975
|
ok: false,
|
|
741
976
|
output: "Background command launch error [PERSIST_FAILED]: could not persist the job registry; the command was not started.",
|
|
@@ -801,10 +1036,14 @@ export class JobManager {
|
|
|
801
1036
|
launchError.syscall ? `syscall=${JSON.stringify(launchError.syscall)}` : undefined,
|
|
802
1037
|
launchError.path ? `path=${JSON.stringify(launchError.path)}` : undefined,
|
|
803
1038
|
].filter((value) => Boolean(value));
|
|
1039
|
+
const ownership = job.responder
|
|
1040
|
+
? "Responder owns this terminal launch failure; do not poll or retry it. Its result will be delivered automatically."
|
|
1041
|
+
: "Inspect the failed job receipt before deciding whether changed evidence supports a retry.";
|
|
804
1042
|
const detail = `${launchRetried ? "Automatic retry after a transient launch ENOENT also failed.\n" : ""}` +
|
|
805
1043
|
`Background command launch error [${code}]: ${launchError.message}\n` +
|
|
806
1044
|
`${fields.join("; ")}\n` +
|
|
807
|
-
"The command did not start. Do not rewrite its syntax to work around this infrastructure error; verify the target and cwd
|
|
1045
|
+
"The command did not start. Do not rewrite its syntax to work around this infrastructure error; verify the target and cwd.\n" +
|
|
1046
|
+
ownership;
|
|
808
1047
|
stderr.append(`${detail}\n`);
|
|
809
1048
|
await this.finalizeJob(job, "failed", { exitCode: 127 });
|
|
810
1049
|
return {
|
|
@@ -817,6 +1056,7 @@ export class JobManager {
|
|
|
817
1056
|
status: "failed",
|
|
818
1057
|
exitCode: 127,
|
|
819
1058
|
artifactPath: stdoutArtifact,
|
|
1059
|
+
...(job.responder ? { responder: true } : {}),
|
|
820
1060
|
nextOffset: 0,
|
|
821
1061
|
},
|
|
822
1062
|
};
|
|
@@ -831,17 +1071,23 @@ export class JobManager {
|
|
|
831
1071
|
job.processGroupId = detached ? child.pid : undefined;
|
|
832
1072
|
job.status = "running";
|
|
833
1073
|
job.heartbeatAt = new Date().toISOString();
|
|
834
|
-
job.processIdentity = processIdentity(child.pid);
|
|
1074
|
+
job.processIdentity = processIdentity(child.pid, { refresh: true });
|
|
835
1075
|
this.processes.set(id, child);
|
|
836
1076
|
this.writers.set(id, { stdout, stderr });
|
|
837
1077
|
this.scheduleAuthorizationExpiry(job);
|
|
838
1078
|
let lastProgressFlush = 0;
|
|
1079
|
+
let lastDurableFlush = Date.now();
|
|
839
1080
|
let progressDirty = false;
|
|
840
1081
|
let progressTimer;
|
|
1082
|
+
// UI freshness is cheap; rewriting the whole registry is not. Terminal
|
|
1083
|
+
// transitions always persist authoritatively below.
|
|
841
1084
|
const flushProgress = () => {
|
|
842
1085
|
progressDirty = false;
|
|
843
1086
|
lastProgressFlush = Date.now();
|
|
844
|
-
|
|
1087
|
+
if (lastProgressFlush - lastDurableFlush >= DURABLE_PROGRESS_FLUSH_MS) {
|
|
1088
|
+
lastDurableFlush = lastProgressFlush;
|
|
1089
|
+
this.persistSync();
|
|
1090
|
+
}
|
|
845
1091
|
this.emit({ type: "job", jobId: id });
|
|
846
1092
|
};
|
|
847
1093
|
// Coalesce high-frequency stdout/stderr chunks: a chatty job (scanner,
|
|
@@ -937,10 +1183,13 @@ export class JobManager {
|
|
|
937
1183
|
const earlyStatus = this.getJob(id)?.status;
|
|
938
1184
|
if (earlyStatus === "failed") {
|
|
939
1185
|
await this.persist();
|
|
1186
|
+
const failureFollowUp = job.responder
|
|
1187
|
+
? "Responder owns this terminal failure; do not poll or retry it. Its result will be delivered automatically."
|
|
1188
|
+
: `Use shell.tail {"id":"${id}"} for captured stderr; do not retry unchanged.`;
|
|
940
1189
|
return {
|
|
941
1190
|
ok: false,
|
|
942
1191
|
output: `Background job failed immediately: id=${id} exit=${job.exitCode ?? "?"}\n` +
|
|
943
|
-
`Command: ${job.commandDisplay}\
|
|
1192
|
+
`Command: ${job.commandDisplay}\n${failureFollowUp}`,
|
|
944
1193
|
exitCode: job.exitCode ?? 1,
|
|
945
1194
|
outputPath: stdoutArtifact,
|
|
946
1195
|
backgroundJob: {
|
|
@@ -948,6 +1197,7 @@ export class JobManager {
|
|
|
948
1197
|
status: "failed",
|
|
949
1198
|
exitCode: job.exitCode,
|
|
950
1199
|
artifactPath: stdoutArtifact,
|
|
1200
|
+
...(job.responder ? { responder: true } : {}),
|
|
951
1201
|
nextOffset: 0,
|
|
952
1202
|
},
|
|
953
1203
|
};
|
|
@@ -957,14 +1207,14 @@ export class JobManager {
|
|
|
957
1207
|
return {
|
|
958
1208
|
ok: true,
|
|
959
1209
|
output: `OS process launch confirmed: id=${id} (canonical job ID) pid=${child.pid ?? "?"}\n` +
|
|
960
|
-
`
|
|
961
|
-
|
|
962
|
-
`Command: ${job.commandDisplay}\nArtifact: ${stdoutArtifact}`,
|
|
1210
|
+
`Command: ${job.commandDisplay}\nArtifact: ${stdoutArtifact}\n` +
|
|
1211
|
+
launchFollowUp(id, job.responder === true),
|
|
963
1212
|
outputPath: stdoutArtifact,
|
|
964
1213
|
backgroundJob: {
|
|
965
1214
|
id,
|
|
966
1215
|
status: job.status,
|
|
967
1216
|
artifactPath: stdoutArtifact,
|
|
1217
|
+
...(job.responder ? { responder: true } : {}),
|
|
968
1218
|
...(options?.profile ? { profile: options.profile } : {}),
|
|
969
1219
|
...(options?.estimatedSeconds !== undefined ? { estimatedSeconds: options.estimatedSeconds } : {}),
|
|
970
1220
|
nextOffset: 0,
|
|
@@ -983,10 +1233,12 @@ export class JobManager {
|
|
|
983
1233
|
this.persistSync();
|
|
984
1234
|
this.emit({ type: "job", jobId: id });
|
|
985
1235
|
}
|
|
1236
|
+
const followUp = launchFollowUp(id, job.responder === true);
|
|
986
1237
|
return {
|
|
987
1238
|
ok: alive,
|
|
988
1239
|
output: `Background command launched as pid=${job.pid ?? "?"}, but job setup/persistence failed: ${detail}\n` +
|
|
989
|
-
`${alive ? `The process is still running as job ${id}; do not launch a duplicate.
|
|
1240
|
+
`${alive ? `The process is still running as job ${id}; do not launch a duplicate. ` : "The process has already stopped. "}` +
|
|
1241
|
+
followUp,
|
|
990
1242
|
exitCode: alive ? undefined : job.exitCode,
|
|
991
1243
|
outputPath: stdoutArtifact,
|
|
992
1244
|
backgroundJob: {
|
|
@@ -994,6 +1246,7 @@ export class JobManager {
|
|
|
994
1246
|
status: job.status,
|
|
995
1247
|
exitCode: job.exitCode,
|
|
996
1248
|
artifactPath: stdoutArtifact,
|
|
1249
|
+
...(job.responder ? { responder: true } : {}),
|
|
997
1250
|
nextOffset: 0,
|
|
998
1251
|
},
|
|
999
1252
|
};
|
|
@@ -1002,8 +1255,18 @@ export class JobManager {
|
|
|
1002
1255
|
return {
|
|
1003
1256
|
ok: false,
|
|
1004
1257
|
output: `Background command launch error [UNKNOWN]: ${detail}\n` +
|
|
1005
|
-
`cwd=${JSON.stringify(cwd)}\nThe command did not start
|
|
1258
|
+
`cwd=${JSON.stringify(cwd)}\nThe command did not start.\n` +
|
|
1259
|
+
launchFollowUp(id, job.responder === true),
|
|
1006
1260
|
exitCode: 127,
|
|
1261
|
+
outputPath: stdoutArtifact,
|
|
1262
|
+
backgroundJob: {
|
|
1263
|
+
id,
|
|
1264
|
+
status: "failed",
|
|
1265
|
+
exitCode: 127,
|
|
1266
|
+
artifactPath: stdoutArtifact,
|
|
1267
|
+
...(job.responder ? { responder: true } : {}),
|
|
1268
|
+
nextOffset: 0,
|
|
1269
|
+
},
|
|
1007
1270
|
};
|
|
1008
1271
|
}
|
|
1009
1272
|
}
|
|
@@ -1039,7 +1302,27 @@ export class JobManager {
|
|
|
1039
1302
|
? "alive"
|
|
1040
1303
|
: "unresponsive"
|
|
1041
1304
|
: "terminal";
|
|
1042
|
-
|
|
1305
|
+
const notification = this.notificationForJob(job.id);
|
|
1306
|
+
const receipt = notification
|
|
1307
|
+
? notification.acknowledgedAt
|
|
1308
|
+
? "consumed"
|
|
1309
|
+
: notification.analyzedAt
|
|
1310
|
+
? "analyzed"
|
|
1311
|
+
: notification.readAt
|
|
1312
|
+
? "read"
|
|
1313
|
+
: notification.deliveredAt
|
|
1314
|
+
? "delivered-unread"
|
|
1315
|
+
: notification.deliveryStartedAt
|
|
1316
|
+
? "delivering"
|
|
1317
|
+
: "ready"
|
|
1318
|
+
: this.consumedResponderResults.has(job.id)
|
|
1319
|
+
? "consumed"
|
|
1320
|
+
: job.responder
|
|
1321
|
+
? this.isLive(job)
|
|
1322
|
+
? "running"
|
|
1323
|
+
: "pending"
|
|
1324
|
+
: undefined;
|
|
1325
|
+
return `[${job.id}] ${job.status} health=${health} exit=${job.exitCode ?? "?"}${receipt ? ` responder=${receipt}` : ""} ${formatJobElapsed(job)} ${job.commandDisplay.slice(0, 80)}`;
|
|
1043
1326
|
};
|
|
1044
1327
|
const header = sessionId
|
|
1045
1328
|
? `Session background jobs (${durable.length} total, session ${sessionId}):`
|
|
@@ -1082,6 +1365,7 @@ export class JobManager {
|
|
|
1082
1365
|
getPendingNotifications(sessionId) {
|
|
1083
1366
|
return [...this.notifications.values()]
|
|
1084
1367
|
.filter((notification) => !notification.acknowledgedAt &&
|
|
1368
|
+
!notification.discardedAt &&
|
|
1085
1369
|
(!sessionId || notification.ownerSessionId === sessionId))
|
|
1086
1370
|
.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1087
1371
|
}
|
|
@@ -1091,22 +1375,24 @@ export class JobManager {
|
|
|
1091
1375
|
claimNextResponderNotification(sessionId, leaseId) {
|
|
1092
1376
|
if (this.responderLeases.get(sessionId) !== leaseId)
|
|
1093
1377
|
return undefined;
|
|
1094
|
-
|
|
1095
|
-
let archived = false;
|
|
1378
|
+
let adopted = false;
|
|
1096
1379
|
for (const notification of this.notifications.values()) {
|
|
1380
|
+
// Receipts belong to the session, not to a runtime lease: a new lease
|
|
1381
|
+
// adopts everything unread that an earlier lease left behind.
|
|
1097
1382
|
if (notification.ownerSessionId === sessionId &&
|
|
1098
1383
|
!notification.acknowledgedAt &&
|
|
1099
|
-
notification.
|
|
1100
|
-
!notification.
|
|
1101
|
-
notification.
|
|
1102
|
-
|
|
1384
|
+
!notification.archivedAt &&
|
|
1385
|
+
!notification.discardedAt &&
|
|
1386
|
+
notification.responderLeaseId !== leaseId) {
|
|
1387
|
+
notification.responderLeaseId = leaseId;
|
|
1388
|
+
adopted = true;
|
|
1103
1389
|
}
|
|
1104
1390
|
}
|
|
1105
1391
|
const notification = [...this.notifications.values()]
|
|
1106
1392
|
.filter((candidate) => candidate.ownerSessionId === sessionId &&
|
|
1107
1393
|
candidate.responderLeaseId === leaseId &&
|
|
1108
1394
|
!candidate.archivedAt &&
|
|
1109
|
-
!candidate.
|
|
1395
|
+
!candidate.discardedAt &&
|
|
1110
1396
|
!candidate.readAt &&
|
|
1111
1397
|
!candidate.analyzedAt &&
|
|
1112
1398
|
!candidate.acknowledgedAt &&
|
|
@@ -1114,26 +1400,53 @@ export class JobManager {
|
|
|
1114
1400
|
.sort((left, right) => left.createdAt.localeCompare(right.createdAt))[0];
|
|
1115
1401
|
if (notification)
|
|
1116
1402
|
this.claimedNotifications.add(notification.id);
|
|
1117
|
-
if (
|
|
1403
|
+
if (adopted && !this.persistSync())
|
|
1118
1404
|
this.scheduleRegistryRetry();
|
|
1119
1405
|
return notification;
|
|
1120
1406
|
}
|
|
1121
1407
|
releaseResponderNotificationClaim(notificationId) {
|
|
1122
1408
|
this.claimedNotifications.delete(notificationId);
|
|
1123
1409
|
}
|
|
1124
|
-
|
|
1410
|
+
/**
|
|
1411
|
+
* Record that a delivery attempt started. This is deliberately weaker than
|
|
1412
|
+
* `markDelivered`: an aborted or failed analysis turn must remain deliverable.
|
|
1413
|
+
*/
|
|
1414
|
+
markDeliveryStarted(notificationId, sessionId) {
|
|
1125
1415
|
const notification = this.notifications.get(notificationId);
|
|
1126
|
-
if (!notification)
|
|
1416
|
+
if (!this.ownsNotification(notification, sessionId))
|
|
1417
|
+
return false;
|
|
1418
|
+
if (notification.deliveryStartedAt)
|
|
1419
|
+
return true;
|
|
1420
|
+
notification.deliveryStartedAt = new Date().toISOString();
|
|
1421
|
+
if (!this.persistSync()) {
|
|
1422
|
+
notification.deliveryStartedAt = undefined;
|
|
1423
|
+
return false;
|
|
1424
|
+
}
|
|
1425
|
+
this.emit({
|
|
1426
|
+
type: "notification",
|
|
1427
|
+
jobId: notification.jobId,
|
|
1428
|
+
notificationId,
|
|
1429
|
+
});
|
|
1430
|
+
return true;
|
|
1431
|
+
}
|
|
1432
|
+
markDelivered(notificationId, sessionId) {
|
|
1433
|
+
const notification = this.notifications.get(notificationId);
|
|
1434
|
+
if (!this.ownsNotification(notification, sessionId))
|
|
1127
1435
|
return false;
|
|
1128
1436
|
if (!notification.deliveredAt) {
|
|
1129
1437
|
notification.deliveredAt = new Date().toISOString();
|
|
1438
|
+
notification.deliveryStartedAt ??= notification.deliveredAt;
|
|
1130
1439
|
if (!this.persistSync()) {
|
|
1131
1440
|
notification.deliveredAt = undefined;
|
|
1132
1441
|
return false;
|
|
1133
1442
|
}
|
|
1134
1443
|
this.emit({ type: "notification", jobId: notification.jobId, notificationId });
|
|
1135
1444
|
}
|
|
1136
|
-
|
|
1445
|
+
if (notification.readAt ||
|
|
1446
|
+
notification.analyzedAt ||
|
|
1447
|
+
notification.acknowledgedAt) {
|
|
1448
|
+
this.claimedNotifications.delete(notificationId);
|
|
1449
|
+
}
|
|
1137
1450
|
return true;
|
|
1138
1451
|
}
|
|
1139
1452
|
markRead(notificationId, sessionId) {
|
|
@@ -1141,7 +1454,8 @@ export class JobManager {
|
|
|
1141
1454
|
if (!notification ||
|
|
1142
1455
|
notification.ownerSessionId !== sessionId ||
|
|
1143
1456
|
!notification.responder ||
|
|
1144
|
-
notification.archivedAt
|
|
1457
|
+
notification.archivedAt ||
|
|
1458
|
+
notification.discardedAt) {
|
|
1145
1459
|
return false;
|
|
1146
1460
|
}
|
|
1147
1461
|
const previousDeliveredAt = notification.deliveredAt;
|
|
@@ -1162,10 +1476,14 @@ export class JobManager {
|
|
|
1162
1476
|
});
|
|
1163
1477
|
return true;
|
|
1164
1478
|
}
|
|
1165
|
-
markAnalyzed(notificationId) {
|
|
1479
|
+
markAnalyzed(notificationId, sessionId) {
|
|
1166
1480
|
const notification = this.notifications.get(notificationId);
|
|
1167
|
-
if (!notification
|
|
1481
|
+
if (!notification || !this.ownsNotification(notification, sessionId)) {
|
|
1168
1482
|
return false;
|
|
1483
|
+
}
|
|
1484
|
+
if (!notification.deliveredAt && !notification.deliveryStartedAt) {
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1169
1487
|
this.claimedNotifications.delete(notificationId);
|
|
1170
1488
|
if (!notification.analyzedAt) {
|
|
1171
1489
|
notification.analyzedAt = new Date().toISOString();
|
|
@@ -1179,13 +1497,22 @@ export class JobManager {
|
|
|
1179
1497
|
}
|
|
1180
1498
|
return true;
|
|
1181
1499
|
}
|
|
1182
|
-
acknowledge(notificationId) {
|
|
1500
|
+
acknowledge(notificationId, sessionId) {
|
|
1183
1501
|
const notification = this.notifications.get(notificationId);
|
|
1184
|
-
if (!notification
|
|
1502
|
+
if (!notification || !this.ownsNotification(notification, sessionId)) {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
if (!notification.analyzedAt)
|
|
1185
1506
|
return false;
|
|
1186
1507
|
this.claimedNotifications.delete(notificationId);
|
|
1187
1508
|
if (!notification.acknowledgedAt) {
|
|
1188
1509
|
notification.acknowledgedAt = new Date().toISOString();
|
|
1510
|
+
this.consumedResponderResults.set(notification.jobId, {
|
|
1511
|
+
jobId: notification.jobId,
|
|
1512
|
+
resultHash: notification.resultHash ?? "",
|
|
1513
|
+
resultRevision: notification.resultRevision ?? 1,
|
|
1514
|
+
acknowledgedAt: notification.acknowledgedAt,
|
|
1515
|
+
});
|
|
1189
1516
|
const persisted = this.persistSync();
|
|
1190
1517
|
if (!persisted)
|
|
1191
1518
|
this.scheduleRegistryRetry();
|
|
@@ -1377,7 +1704,7 @@ export class JobManager {
|
|
|
1377
1704
|
const identityMatches = () => {
|
|
1378
1705
|
if (process.platform === "win32")
|
|
1379
1706
|
return processAlive(pid);
|
|
1380
|
-
const current = processIdentity(pid);
|
|
1707
|
+
const current = processIdentity(pid, { refresh: true });
|
|
1381
1708
|
return Boolean(current && job.processIdentity && current === job.processIdentity);
|
|
1382
1709
|
};
|
|
1383
1710
|
if (targetAlive() && (!processAlive(pid) || !identityMatches())) {
|
|
@@ -1415,13 +1742,13 @@ export class JobManager {
|
|
|
1415
1742
|
else if (!(processGroupId && processGroupVerified && allowVerifiedGroup)) {
|
|
1416
1743
|
return "identity-mismatch";
|
|
1417
1744
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1745
|
+
const outcome = terminateProcessTree(pid, {
|
|
1746
|
+
signal,
|
|
1747
|
+
processGroupId,
|
|
1748
|
+
});
|
|
1749
|
+
if (outcome === "failed")
|
|
1423
1750
|
return targetAlive() ? "failed" : "gone";
|
|
1424
|
-
|
|
1751
|
+
return outcome;
|
|
1425
1752
|
};
|
|
1426
1753
|
const restoreRunning = () => {
|
|
1427
1754
|
job.status = "running";
|
|
@@ -1536,15 +1863,16 @@ export class JobManager {
|
|
|
1536
1863
|
acknowledgedAt: notification.acknowledgedAt,
|
|
1537
1864
|
});
|
|
1538
1865
|
notification.wakeOnCompletion = false;
|
|
1539
|
-
notification.
|
|
1540
|
-
notification.
|
|
1866
|
+
notification.discardedAt = acknowledgedAt;
|
|
1867
|
+
notification.discardReason = "session-cancelled";
|
|
1868
|
+
this.claimedNotifications.delete(notification.id);
|
|
1541
1869
|
}
|
|
1542
1870
|
const acknowledgementsPersisted = this.persistSync();
|
|
1543
1871
|
if (!acknowledgementsPersisted) {
|
|
1544
1872
|
for (const snapshot of notificationSnapshots) {
|
|
1545
1873
|
snapshot.notification.wakeOnCompletion = snapshot.wakeOnCompletion;
|
|
1546
|
-
snapshot.notification.
|
|
1547
|
-
snapshot.notification.
|
|
1874
|
+
snapshot.notification.discardedAt = undefined;
|
|
1875
|
+
snapshot.notification.discardReason = undefined;
|
|
1548
1876
|
}
|
|
1549
1877
|
}
|
|
1550
1878
|
else {
|
|
@@ -1611,21 +1939,26 @@ export class JobManager {
|
|
|
1611
1939
|
if (timer)
|
|
1612
1940
|
clearTimeout(timer);
|
|
1613
1941
|
this.settlementTimers.delete(notification.jobId);
|
|
1614
|
-
this.
|
|
1942
|
+
this.pendingSettlements.delete(notification.jobId);
|
|
1615
1943
|
}
|
|
1616
1944
|
for (const [id, notification] of this.notifications) {
|
|
1617
1945
|
const job = this.jobs.get(notification.jobId);
|
|
1618
1946
|
if (!job ||
|
|
1947
|
+
notification.discardedAt ||
|
|
1619
1948
|
(notification.acknowledgedAt && job.status !== "lost")) {
|
|
1620
1949
|
this.notifications.delete(id);
|
|
1621
1950
|
}
|
|
1622
1951
|
}
|
|
1952
|
+
for (const [jobId] of this.consumedResponderResults) {
|
|
1953
|
+
if (!this.jobs.has(jobId))
|
|
1954
|
+
this.consumedResponderResults.delete(jobId);
|
|
1955
|
+
}
|
|
1623
1956
|
const durableTerminal = [];
|
|
1624
1957
|
for (const [id, job] of this.jobs) {
|
|
1625
1958
|
if (!this.isDurable(job)) {
|
|
1626
1959
|
// Safety: never keep ephemeral rows that are not live.
|
|
1627
1960
|
if (!this.isLive(job))
|
|
1628
|
-
this.
|
|
1961
|
+
this.forgetJob(id);
|
|
1629
1962
|
continue;
|
|
1630
1963
|
}
|
|
1631
1964
|
if (this.isLive(job))
|
|
@@ -1638,7 +1971,7 @@ export class JobManager {
|
|
|
1638
1971
|
}
|
|
1639
1972
|
const ended = job.endedAt ? Date.parse(job.endedAt) : Date.parse(job.startedAt);
|
|
1640
1973
|
if (Number.isFinite(ended) && now - ended > TERMINAL_JOB_MAX_AGE_MS) {
|
|
1641
|
-
this.
|
|
1974
|
+
this.forgetJob(id);
|
|
1642
1975
|
continue;
|
|
1643
1976
|
}
|
|
1644
1977
|
durableTerminal.push(job);
|
|
@@ -1647,7 +1980,7 @@ export class JobManager {
|
|
|
1647
1980
|
return;
|
|
1648
1981
|
durableTerminal.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
1649
1982
|
for (const job of durableTerminal.slice(MAX_DURABLE_TERMINAL_JOBS)) {
|
|
1650
|
-
this.
|
|
1983
|
+
this.forgetJob(job.id);
|
|
1651
1984
|
}
|
|
1652
1985
|
}
|
|
1653
1986
|
loadAndReconcile() {
|
|
@@ -1667,6 +2000,23 @@ export class JobManager {
|
|
|
1667
2000
|
catch { /* a corrupt current registry must not replay stale v1 state */ }
|
|
1668
2001
|
if (!parsed)
|
|
1669
2002
|
return;
|
|
2003
|
+
if (parsed.schemaVersion === 2 && Array.isArray(parsed.settlements)) {
|
|
2004
|
+
for (const settlement of parsed.settlements) {
|
|
2005
|
+
if (!settlement || typeof settlement.jobId !== "string")
|
|
2006
|
+
continue;
|
|
2007
|
+
this.pendingSettlements.set(settlement.jobId, settlement);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
if (parsed.schemaVersion === 2 && Array.isArray(parsed.consumedResults)) {
|
|
2011
|
+
for (const consumed of parsed.consumedResults) {
|
|
2012
|
+
if (!consumed ||
|
|
2013
|
+
typeof consumed.jobId !== "string" ||
|
|
2014
|
+
typeof consumed.resultHash !== "string") {
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
this.consumedResponderResults.set(consumed.jobId, consumed);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
1670
2020
|
if (parsed.schemaVersion === 2 && Array.isArray(parsed.notifications)) {
|
|
1671
2021
|
const notifiedJobs = new Set();
|
|
1672
2022
|
for (const notification of parsed.notifications) {
|
|
@@ -1676,7 +2026,9 @@ export class JobManager {
|
|
|
1676
2026
|
continue;
|
|
1677
2027
|
notifiedJobs.add(notification.jobId);
|
|
1678
2028
|
if (!notification.acknowledgedAt && !notification.archivedAt) {
|
|
1679
|
-
|
|
2029
|
+
// A durable receipt must survive the restart it exists for. Drop the
|
|
2030
|
+
// dead runtime lease so the next activated lease can adopt it.
|
|
2031
|
+
delete notification.responderLeaseId;
|
|
1680
2032
|
}
|
|
1681
2033
|
this.notifications.set(notification.id, notification);
|
|
1682
2034
|
}
|
|
@@ -1713,6 +2065,7 @@ export class JobManager {
|
|
|
1713
2065
|
if (!job?.responder)
|
|
1714
2066
|
this.notifications.delete(id);
|
|
1715
2067
|
}
|
|
2068
|
+
this.scheduleLivenessWatch();
|
|
1716
2069
|
this.pruneTerminalJobs();
|
|
1717
2070
|
this.persistSync();
|
|
1718
2071
|
}
|
|
@@ -1724,6 +2077,12 @@ export class JobManager {
|
|
|
1724
2077
|
schemaVersion: 2,
|
|
1725
2078
|
jobs: [...this.jobs.values()].filter((job) => this.isDurable(job)),
|
|
1726
2079
|
notifications: [...this.notifications.values()],
|
|
2080
|
+
...(this.pendingSettlements.size
|
|
2081
|
+
? { settlements: [...this.pendingSettlements.values()] }
|
|
2082
|
+
: {}),
|
|
2083
|
+
...(this.consumedResponderResults.size
|
|
2084
|
+
? { consumedResults: [...this.consumedResponderResults.values()] }
|
|
2085
|
+
: {}),
|
|
1727
2086
|
};
|
|
1728
2087
|
}
|
|
1729
2088
|
scheduleRegistryRetry() {
|
|
@@ -1739,7 +2098,7 @@ export class JobManager {
|
|
|
1739
2098
|
}
|
|
1740
2099
|
persistSync() {
|
|
1741
2100
|
try {
|
|
1742
|
-
mkdirSync(this.jobsDir, { recursive: true });
|
|
2101
|
+
mkdirSync(this.jobsDir, { recursive: true, mode: 0o700 });
|
|
1743
2102
|
const temp = `${this.registryPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1744
2103
|
writeFileSync(temp, `${JSON.stringify(this.registry(), null, 2)}\n`, { mode: 0o600 });
|
|
1745
2104
|
renameSync(temp, this.registryPath);
|