mixdog 1.2.5 → 1.2.6
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/package.json +1 -1
- package/scripts/lib/gate-scheduler.mjs +2 -2
- package/scripts/lib/run-node-tests.mjs +5 -14
- package/scripts/provider-pingpong-live.mjs +9 -9
- package/src/agents/advisor/AGENT.md +4 -4
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/reviewer/AGENT.md +2 -3
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +10 -0
- package/src/defaults/skills/browser-use/SKILL.md +2 -1
- package/src/defaults/skills/computer-use/SKILL.md +3 -1
- package/src/defaults/skills/visualize/SKILL.md +3 -2
- package/src/lib/rules-builder.cjs +1 -1
- package/src/output-styles/common.md +22 -6
- package/src/output-styles/detailed.md +7 -5
- package/src/output-styles/minimal.md +7 -3
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/AGENT.md +3 -1
- package/src/rules/lead/LEAD.md +4 -3
- package/src/rules/shared/10-tool-workflow.md +2 -1
- package/src/rules/shared/60-verification.md +4 -5
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
- package/src/runtime/agent/orchestrator/config.mjs +6 -0
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
- package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
- package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
- package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
- package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
- package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
- package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
- package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +258 -43
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
- package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
- package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
- package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
- package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
- package/src/runtime/attachments/office-extract.mjs +11 -9
- package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
- package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
- package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
- package/src/runtime/office/core/tabular.mjs +1 -1
- package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
- package/src/runtime/office/portable/docx-formatting.mjs +1 -1
- package/src/runtime/office/portable/docx-runs.mjs +1 -1
- package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
- package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
- package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
- package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
- package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
- package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
- package/src/runtime/office/portable/portable-opc.mjs +27 -13
- package/src/runtime/office/portable/portable-pivot.mjs +3 -3
- package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
- package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
- package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
- package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
- package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
- package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
- package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
- package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
- package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
- package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
- package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
- package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
- package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
- package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
- package/src/runtime/office/portable/portable-xml.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +22 -13
- package/src/runtime/shared/background-tasks.mjs +60 -20
- package/src/runtime/shared/child-spawn-gate.mjs +26 -16
- package/src/runtime/shared/config.mjs +0 -10
- package/src/runtime/shared/file-lock.mjs +7 -1
- package/src/runtime/shared/file-permissions-worker.mjs +38 -0
- package/src/runtime/shared/file-permissions.d.mts +19 -4
- package/src/runtime/shared/file-permissions.mjs +310 -4
- package/src/runtime/shared/git-runner-worker.mjs +6 -78
- package/src/runtime/shared/llm/http-agent.mjs +7 -3
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
- package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
- package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
- package/src/runtime/shared/memory-snapshot.mjs +5 -1
- package/src/runtime/shared/notify-trace.mjs +8 -1
- package/src/runtime/shared/pristine-execution-contract.json +5 -1
- package/src/runtime/shared/process-runner-worker.mjs +7 -0
- package/src/runtime/shared/process-runner.mjs +38 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -1
- package/src/runtime/shared/provider-api-key.mjs +0 -1
- package/src/runtime/shared/read-row-numbers.mjs +1 -1
- package/src/runtime/shared/resource-admission.mjs +60 -18
- package/src/runtime/shared/run-child.mjs +84 -0
- package/src/runtime/shared/session-activity-signal.mjs +23 -0
- package/src/runtime/shared/shell-output-summary.mjs +23 -0
- package/src/runtime/shared/singleton-owner.mjs +126 -42
- package/src/runtime/shared/tool-output-limit.mjs +4 -2
- package/src/runtime/shared/tool-output-marker.mjs +58 -0
- package/src/runtime/shared/user-data-guard.mjs +28 -9
- package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
- package/src/runtime/shared/worktrees/git-head.mjs +20 -2
- package/src/runtime/shared/worktrees/lock.mjs +3 -2
- package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
- package/src/runtime/tidy/runners/biome.mjs +3 -1
- package/src/runtime/tidy/shadow.mjs +2 -1
- package/src/runtime/web-search/index.mjs +5 -7
- package/src/runtime/web-search/lib/formatter.mjs +42 -6
- package/src/runtime/web-search/tool-defs.mjs +6 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
- package/src/session-runtime/lifecycle/shared.mjs +19 -2
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
- package/src/session-runtime/lifecycle/teardown.mjs +5 -2
- package/src/session-runtime/lifecycle-api.mjs +3 -1
- package/src/session-runtime/provider-auth/login.mjs +10 -6
- package/src/session-runtime/provider-auth/setup.mjs +19 -2
- package/src/session-runtime/provider-catalog-cache.mjs +101 -25
- package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
- package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
- package/src/session-runtime/provider-models.mjs +76 -10
- package/src/session-runtime/provider-readiness.mjs +4 -0
- package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
- package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
- package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
- package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
- package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
- package/src/session-runtime/services/agent-tool.mjs +20 -0
- package/src/session-runtime/services/provider-admin.mjs +92 -20
- package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
- package/src/session-runtime/services/usage-dashboard.mjs +27 -69
- package/src/session-runtime/turn/session-ops.mjs +2 -2
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/usage-stats-api.mjs +6 -1
- package/src/standalone/channel-transport/http-routes.mjs +10 -9
- package/src/standalone/daemon-desktop-runtime.mjs +20 -7
- package/src/standalone/daemon-owner.mjs +117 -25
- package/src/standalone/daemon.mjs +43 -13
- package/src/standalone/session-call-cache.mjs +6 -0
- package/src/standalone/session-protocol.mjs +1 -1
- package/src/standalone/session-service/retention/busy.mjs +7 -2
- package/src/standalone/session-transport/routes.mjs +7 -6
- package/src/tui/app/model-options.mjs +39 -2
- package/src/tui/app/model-picker.mjs +16 -2
- package/src/tui/app/provider-setup-picker.mjs +1 -2
- package/src/tui/dist/index.mjs +385 -104
- package/src/tui/session/notice-text.mjs +0 -1
- package/src/tui/session/session-api/intake/submission.mjs +7 -13
- package/src/tui/session/session-api/integrations.mjs +1 -5
- package/src/tui/session/session-flow/drain.mjs +8 -0
- package/src/tui/session/session-flow/steering.mjs +46 -0
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/turn-stream.mjs +33 -0
- package/src/tui/session/turn.mjs +30 -3
- package/src/ui/statusline.mjs +1 -6
- package/src/ui/streaming-markdown-heal.mjs +24 -7
- package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
- package/src/workflows/default/WORKFLOW.md +9 -13
|
@@ -25,7 +25,14 @@ import {
|
|
|
25
25
|
releaseWriteCommit as _releaseWriteCommit,
|
|
26
26
|
} from './write-guards.mjs';
|
|
27
27
|
import { _sessionForDisk } from './serialize.mjs';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
_commitSessionWrite,
|
|
30
|
+
_commitSessionWriteAttempt,
|
|
31
|
+
_discardSaveTmp,
|
|
32
|
+
_trackSaveTmp,
|
|
33
|
+
_untrackSaveTmp,
|
|
34
|
+
} from './save-fault.mjs';
|
|
35
|
+
import { sleep } from '../../../../shared/sleep.mjs';
|
|
29
36
|
import {
|
|
30
37
|
_droppedSaveIds,
|
|
31
38
|
_clearLiveSession,
|
|
@@ -100,15 +107,25 @@ function _readLifecycleAuthority(id, reason, action, lifecycleOnly = false) {
|
|
|
100
107
|
return authority;
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
/** Commit a barrier record, bypassing the save queue + guard. False when it did not land.
|
|
104
|
-
|
|
110
|
+
/** Commit a barrier record, bypassing the save queue + guard. False when it did not land.
|
|
111
|
+
* `attempt` (a number) commits with ONE rename of the retry schedule: a
|
|
112
|
+
* retryable refusal with attempts left discards the scratch file, records
|
|
113
|
+
* nothing and returns a retry ({ retryInMs }) instead of sleeping. */
|
|
114
|
+
function _writeLifecycleRecord(id, record, existing, reason, attempt = null) {
|
|
105
115
|
const target = sessionPath(id);
|
|
106
116
|
const tmp = _trackSaveTmp(`${target}.${randomBytes(6).toString('hex')}.tmp`);
|
|
107
117
|
try {
|
|
108
118
|
const disk = _sessionForDisk(record);
|
|
109
119
|
writeFileSync(tmp, JSON.stringify(disk), 'utf-8');
|
|
110
120
|
const scratch = _stampSessionScratch(tmp);
|
|
111
|
-
_commitSessionWrite(tmp, target, id);
|
|
121
|
+
if (attempt === null) _commitSessionWrite(tmp, target, id);
|
|
122
|
+
else {
|
|
123
|
+
const retryInMs = _commitSessionWriteAttempt(tmp, target, id, attempt);
|
|
124
|
+
if (retryInMs !== null) {
|
|
125
|
+
_discardSaveTmp(tmp);
|
|
126
|
+
return { retryInMs };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
112
129
|
// Read-only lifecycle checks (pending-message gates right after a close/
|
|
113
130
|
// detach) reuse what this barrier wrote; write authority still re-reads.
|
|
114
131
|
_readCanonicalRecord.rememberOwnBarrier(target, scratch, _lifecycleOfSessionDocument(disk));
|
|
@@ -179,6 +196,10 @@ function _recordAbsent(options) {
|
|
|
179
196
|
* saveSession() calls.
|
|
180
197
|
*/
|
|
181
198
|
export function markSessionClosed(id, reason = 'manual', options = {}) {
|
|
199
|
+
return _markSessionClosed(id, reason, options, null);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _markSessionClosed(id, reason, options, attempt) {
|
|
182
203
|
// Only a fresh failure from THIS attempt may be observed by closeSession's
|
|
183
204
|
// durable-barrier check — a veto must not surface a stale error.
|
|
184
205
|
clearSessionLifecycleCommitError(id);
|
|
@@ -232,7 +253,8 @@ export function markSessionClosed(id, reason = 'manual', options = {}) {
|
|
|
232
253
|
updatedAt: closeTime,
|
|
233
254
|
..._cancelStatusFields(base, reason, options.cancelStatus, closeTime),
|
|
234
255
|
};
|
|
235
|
-
|
|
256
|
+
const wrote = _writeLifecycleRecord(id, tombstone, existing, reason, attempt);
|
|
257
|
+
if (wrote !== true) return wrote || null;
|
|
236
258
|
_savePending.delete(id);
|
|
237
259
|
clearSessionSaveError(id);
|
|
238
260
|
_clearLiveSession(id);
|
|
@@ -262,6 +284,10 @@ export function markSessionClosed(id, reason = 'manual', options = {}) {
|
|
|
262
284
|
* Returns the new generation, or null if the session file doesn't exist.
|
|
263
285
|
*/
|
|
264
286
|
export function bumpSessionGeneration(id, reason = 'detach', options = {}) {
|
|
287
|
+
return _bumpSessionGeneration(id, reason, options, null);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function _bumpSessionGeneration(id, reason, options, attempt) {
|
|
265
291
|
clearSessionLifecycleCommitError(id);
|
|
266
292
|
// The detach barrier is a canonical write and MUST take the same commit
|
|
267
293
|
// lock as markSessionClosed. Cancellation alone is not a barrier: a writer
|
|
@@ -289,7 +315,8 @@ export function bumpSessionGeneration(id, reason = 'detach', options = {}) {
|
|
|
289
315
|
if (!existing) return _recordAbsent(options);
|
|
290
316
|
const newGen = (typeof existing.generation === 'number' ? existing.generation : 0) + 1;
|
|
291
317
|
const detached = { ...existing, generation: newGen, updatedAt: Date.now(), detachedReason: reason };
|
|
292
|
-
|
|
318
|
+
const wrote = _writeLifecycleRecord(id, detached, existing, reason, attempt);
|
|
319
|
+
if (wrote !== true) return wrote || null;
|
|
293
320
|
_savePending.delete(id);
|
|
294
321
|
clearSessionSaveError(id);
|
|
295
322
|
_clearLiveSession(id);
|
|
@@ -300,3 +327,43 @@ export function bumpSessionGeneration(id, reason = 'detach', options = {}) {
|
|
|
300
327
|
_releaseWriteCommit(commitControl);
|
|
301
328
|
}
|
|
302
329
|
}
|
|
330
|
+
|
|
331
|
+
// ── Non-blocking barriers ────────────────────────────────────────────────────
|
|
332
|
+
// The synchronous barriers above wait out a refused rename (Windows EPERM/
|
|
333
|
+
// EBUSY while a scanner holds the file) with sleepSync INSIDE the commit lock,
|
|
334
|
+
// freezing the whole daemon for up to the full backoff schedule (4.6 s seen).
|
|
335
|
+
// An attempt instead runs the complete barrier — authority read, cancellation,
|
|
336
|
+
// record built from the current copy, ONE rename — under the commit lock and,
|
|
337
|
+
// when the rename is refused with attempts left, returns a retry
|
|
338
|
+
// ({ retryInMs }) having recorded nothing. The lock is released between
|
|
339
|
+
// attempts; the next attempt re-reads everything, so a save that landed in
|
|
340
|
+
// between is folded into the barrier instead of overwriting it. Same backoff
|
|
341
|
+
// schedule and the same final error as the synchronous barrier.
|
|
342
|
+
|
|
343
|
+
/** Whether a barrier attempt asked to be retried after `retryInMs`. */
|
|
344
|
+
export function isLifecycleRetry(result) {
|
|
345
|
+
return typeof result?.retryInMs === 'number';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Attempt `attempt` of markSessionClosed (see isLifecycleRetry). */
|
|
349
|
+
export function markSessionClosedAttempt(id, reason = 'manual', options = {}, attempt = 0) {
|
|
350
|
+
return _markSessionClosed(id, reason, options, attempt);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Attempt `attempt` of bumpSessionGeneration (see isLifecycleRetry). */
|
|
354
|
+
export function bumpSessionGenerationAttempt(id, reason = 'detach', options = {}, attempt = 0) {
|
|
355
|
+
return _bumpSessionGeneration(id, reason, options, attempt);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function _runBarrierAttempts(run) {
|
|
359
|
+
for (let attempt = 0; ; attempt++) {
|
|
360
|
+
const result = run(attempt);
|
|
361
|
+
if (!isLifecycleRetry(result)) return result;
|
|
362
|
+
await sleep(result.retryInMs);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** markSessionClosed without ever blocking the event loop on a rename retry. */
|
|
367
|
+
export function markSessionClosedAsync(id, reason = 'manual', options = {}) {
|
|
368
|
+
return _runBarrierAttempts((attempt) => _markSessionClosed(id, reason, options, attempt));
|
|
369
|
+
}
|
|
@@ -9,6 +9,7 @@ import { _savePending } from './pending-saves.mjs';
|
|
|
9
9
|
import { _liveSessions, _droppedSaveIds, getFailedSaveSnapshot } from './live-state.mjs';
|
|
10
10
|
import { _readStoredSessionCached } from './load-cache.mjs';
|
|
11
11
|
import { _ensureLifecycleFields } from './serialize.mjs';
|
|
12
|
+
import { isSettledSessionStamp, sameSessionStamp, statSessionStamp } from './canonical-reader.mjs';
|
|
12
13
|
|
|
13
14
|
export function loadSession(id) {
|
|
14
15
|
const path = sessionPath(id);
|
|
@@ -58,3 +59,42 @@ export function loadSession(id) {
|
|
|
58
59
|
if (live?.id === id) _liveSessions.delete(id);
|
|
59
60
|
return disk.session ? _ensureLifecycleFields(disk.session) : null;
|
|
60
61
|
}
|
|
62
|
+
|
|
63
|
+
// `status` of loadSession(id), for the agent-status pulse: it reads every
|
|
64
|
+
// worker's status every two seconds, and an idle worker's whole transcript
|
|
65
|
+
// (released from the load cache's small document budget) was re-read and
|
|
66
|
+
// strictly re-parsed on every beat. The answer loadSession gave is reused only
|
|
67
|
+
// while the file keeps the exact settled full stamp it was read under and no
|
|
68
|
+
// in-memory copy (pending save, live snapshot, failed-save evidence) can speak
|
|
69
|
+
// for the id instead; anything else asks loadSession again.
|
|
70
|
+
const STATUS_CACHE_LIMIT = 4096;
|
|
71
|
+
const _statusByPath = new Map(); // path -> { stamp, status }
|
|
72
|
+
|
|
73
|
+
function _observeStamp(path) {
|
|
74
|
+
try {
|
|
75
|
+
return statSessionStamp(path);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function loadSessionStatus(id) {
|
|
82
|
+
const path = sessionPath(id);
|
|
83
|
+
const inMemory = _savePending.has(id) || _liveSessions.has(id) || getFailedSaveSnapshot(id);
|
|
84
|
+
const before = inMemory ? null : _observeStamp(path);
|
|
85
|
+
const cached = before ? _statusByPath.get(path) : null;
|
|
86
|
+
if (cached && sameSessionStamp(cached.stamp, before)) return cached.status;
|
|
87
|
+
_statusByPath.delete(path);
|
|
88
|
+
const status = loadSession(id)?.status ?? null;
|
|
89
|
+
if (
|
|
90
|
+
before &&
|
|
91
|
+
isSettledSessionStamp(before) &&
|
|
92
|
+
!_savePending.has(id) &&
|
|
93
|
+
!_liveSessions.has(id) &&
|
|
94
|
+
sameSessionStamp(before, _observeStamp(path))
|
|
95
|
+
) {
|
|
96
|
+
if (_statusByPath.size >= STATUS_CACHE_LIMIT) _statusByPath.delete(_statusByPath.keys().next().value);
|
|
97
|
+
_statusByPath.set(path, { stamp: before, status });
|
|
98
|
+
}
|
|
99
|
+
return status;
|
|
100
|
+
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* another process's ACTIVE commit, which no registry here can see.
|
|
22
22
|
*/
|
|
23
23
|
import { unlinkSync } from 'node:fs';
|
|
24
|
+
import { renameAttempt } from '../../../../shared/atomic-file.mjs';
|
|
24
25
|
import { basename } from 'node:path';
|
|
25
26
|
import { _renameWithRetrySync } from './serialize.mjs';
|
|
26
27
|
|
|
@@ -318,3 +319,17 @@ export function _commitSessionWrite(tmp, target, id) {
|
|
|
318
319
|
if (injected) throw injected;
|
|
319
320
|
return _renameWithRetrySync(tmp, target);
|
|
320
321
|
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* One attempt of _commitSessionWrite's rename schedule (renameAttempt): null
|
|
325
|
+
* when the scratch file landed, else the delay before attempt + 1; throws the
|
|
326
|
+
* final error. The injected fault is taken once per commit, on attempt 0,
|
|
327
|
+
* exactly as _commitSessionWrite takes it.
|
|
328
|
+
*/
|
|
329
|
+
export function _commitSessionWriteAttempt(tmp, target, id, attempt) {
|
|
330
|
+
if (attempt === 0) {
|
|
331
|
+
const injected = _takeInjectedCommitFault(id);
|
|
332
|
+
if (injected) throw injected;
|
|
333
|
+
}
|
|
334
|
+
return renameAttempt(tmp, target, attempt);
|
|
335
|
+
}
|
|
@@ -34,9 +34,21 @@ function _memoFilePath(dir, id, extension) {
|
|
|
34
34
|
}
|
|
35
35
|
return path;
|
|
36
36
|
}
|
|
37
|
+
// Every session path lookup ensured the store dir with a synchronous
|
|
38
|
+
// existsSync: the agent-status pulse resolves hundreds of paths per beat, and
|
|
39
|
+
// those probes alone held the daemon loop for seconds per minute. The check is
|
|
40
|
+
// re-run at most once per interval; a dir removed in between is recreated by
|
|
41
|
+
// the next check.
|
|
42
|
+
const STORE_DIR_RECHECK_MS = 1000;
|
|
43
|
+
let _ensuredStoreDir = null;
|
|
44
|
+
let _ensuredStoreDirAt = 0;
|
|
37
45
|
export function getStoreDir() {
|
|
38
46
|
const dir = _storeDirPath();
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
if (dir === _ensuredStoreDir && now - _ensuredStoreDirAt < STORE_DIR_RECHECK_MS) return dir;
|
|
39
49
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
50
|
+
_ensuredStoreDir = dir;
|
|
51
|
+
_ensuredStoreDirAt = now;
|
|
40
52
|
return dir;
|
|
41
53
|
}
|
|
42
54
|
function _sessionFileName(id, extension) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level field read of a serialized session record without materializing
|
|
3
|
+
* its transcript.
|
|
4
|
+
*
|
|
5
|
+
* The agent pool needs a handful of top-level fields per worker session, yet
|
|
6
|
+
* `JSON.parse` of the whole record built every message object — megabytes per
|
|
7
|
+
* worker — on each rewrite the worker persisted. The record is instead proven
|
|
8
|
+
* valid by the strict grammar walk of store-record-strict-scan.mjs, which
|
|
9
|
+
* decodes only the requested members; a record it cannot prove (malformed,
|
|
10
|
+
* duplicate top-level keys, pretty-printed, non-object root) is read by the
|
|
11
|
+
* full parse itself.
|
|
12
|
+
*
|
|
13
|
+
* Contract: the result equals picking `fields` (in `fields` order, own
|
|
14
|
+
* properties only) from `JSON.parse(bytes.toString('utf8'))` when that is an
|
|
15
|
+
* object or an array (an array root picks none), and is `null` otherwise —
|
|
16
|
+
* a document the full parse rejects included.
|
|
17
|
+
*/
|
|
18
|
+
import { provenTopLevelObject } from './store-record-strict-scan.mjs';
|
|
19
|
+
|
|
20
|
+
export function readTopLevelFields(bytes, fields) {
|
|
21
|
+
const wanted = new Set(fields);
|
|
22
|
+
let doc = provenTopLevelObject(bytes, (key) => wanted.has(key));
|
|
23
|
+
if (!doc) {
|
|
24
|
+
try {
|
|
25
|
+
doc = JSON.parse(bytes.toString('utf8'));
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (!doc || typeof doc !== 'object') return null;
|
|
30
|
+
}
|
|
31
|
+
const picked = {};
|
|
32
|
+
for (const field of fields) {
|
|
33
|
+
if (Object.hasOwn(doc, field)) picked[field] = doc[field];
|
|
34
|
+
}
|
|
35
|
+
return picked;
|
|
36
|
+
}
|
|
@@ -16,10 +16,17 @@
|
|
|
16
16
|
* field name and row key is a consumed contract and is used verbatim.
|
|
17
17
|
*/
|
|
18
18
|
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
19
20
|
import { join } from 'node:path';
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
isSettledSessionStamp,
|
|
23
|
+
sameSessionStamp,
|
|
24
|
+
sessionStampOf,
|
|
25
|
+
statSessionStamp,
|
|
26
|
+
} from './store/canonical-reader.mjs';
|
|
21
27
|
import { readJsonProjection } from '../../../shared/pending-json-projection.mjs';
|
|
22
28
|
import { listStoredAgentWorkers as assembleStoredAgentWorkers } from './store-agent-worker-pool.mjs';
|
|
29
|
+
import { readTopLevelFields } from './store-agent-worker-header.mjs';
|
|
23
30
|
import {
|
|
24
31
|
TAG_TOMBSTONE_TTL_MS,
|
|
25
32
|
agentPaneVisible,
|
|
@@ -166,16 +173,83 @@ function cancelStage(cancel, declaredStage) {
|
|
|
166
173
|
return /^cancel/i.test(declaredStage) || CANCELLED_AGENT_STATUS.test(declaredStage) ? declaredStage : cancel.status;
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
// The worker indexes were re-parsed in full on every pool listing and ancestry
|
|
177
|
+
// lookup, changed or not. Each keeps its last parse beside the bytes it came
|
|
178
|
+
// from: the file is still read every time (a few hundred KB), but an
|
|
179
|
+
// unchanged one — byte for byte, so no timestamp collision can serve a stale
|
|
180
|
+
// document — reuses that parse.
|
|
181
|
+
// Before the read, the full file stamp is checked under the same canonical
|
|
182
|
+
// policy the session headers use: a file that still has the exact settled
|
|
183
|
+
// { dev, ino, size, mtimeNs, ctimeNs } its bytes were proven to come from is
|
|
184
|
+
// not read again (each pool listing re-read the whole index synchronously).
|
|
185
|
+
const workerIndexCache = new Map(); // path -> { bytes, doc, stamp|null }
|
|
186
|
+
// The index documents an asynchronous listing read before its (synchronous)
|
|
187
|
+
// assembly; live only during that assembly.
|
|
188
|
+
let passIndexes = null; // path -> doc | null
|
|
189
|
+
|
|
190
|
+
function readWorkerIndexFile(path) {
|
|
191
|
+
if (passIndexes?.has(path)) return passIndexes.get(path);
|
|
171
192
|
// This process's own queued (not yet persisted) writes win over the file.
|
|
172
193
|
const projected = readJsonProjection(path);
|
|
173
194
|
if (projected) return projected;
|
|
195
|
+
const before = observeStamp(path);
|
|
196
|
+
const cached = workerIndexCache.get(path);
|
|
197
|
+
if (before && cached?.stamp && sameSessionStamp(cached.stamp, before)) return cached.doc;
|
|
198
|
+
let bytes;
|
|
174
199
|
try {
|
|
175
|
-
|
|
200
|
+
bytes = readFileSync(path);
|
|
176
201
|
} catch {
|
|
202
|
+
workerIndexCache.delete(path);
|
|
177
203
|
return null;
|
|
178
204
|
}
|
|
205
|
+
const stamp = before && isSettledSessionStamp(before) && sameSessionStamp(before, observeStamp(path)) ? before : null;
|
|
206
|
+
if (cached?.bytes.equals(bytes)) {
|
|
207
|
+
cached.stamp = stamp;
|
|
208
|
+
return cached.doc;
|
|
209
|
+
}
|
|
210
|
+
let doc = null;
|
|
211
|
+
try {
|
|
212
|
+
doc = JSON.parse(bytes.toString('utf8'));
|
|
213
|
+
} catch {
|
|
214
|
+
/* malformed: no rows */
|
|
215
|
+
}
|
|
216
|
+
workerIndexCache.set(path, { bytes, doc, stamp });
|
|
217
|
+
return doc;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** readWorkerIndexFile with every stat and read awaited, under the same
|
|
221
|
+
* stamp and byte-equality cache. */
|
|
222
|
+
async function readWorkerIndexFileAsync(path) {
|
|
223
|
+
const projected = readJsonProjection(path);
|
|
224
|
+
if (projected) return projected;
|
|
225
|
+
const before = await observeStampAsync(path);
|
|
226
|
+
const cached = workerIndexCache.get(path);
|
|
227
|
+
if (before && cached?.stamp && sameSessionStamp(cached.stamp, before)) return cached.doc;
|
|
228
|
+
let bytes;
|
|
229
|
+
try {
|
|
230
|
+
bytes = await readFile(path);
|
|
231
|
+
} catch {
|
|
232
|
+
workerIndexCache.delete(path);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const settled = before && isSettledSessionStamp(before);
|
|
236
|
+
const stamp = settled && sameSessionStamp(before, await observeStampAsync(path)) ? before : null;
|
|
237
|
+
if (cached?.bytes.equals(bytes)) {
|
|
238
|
+
cached.stamp = stamp;
|
|
239
|
+
return cached.doc;
|
|
240
|
+
}
|
|
241
|
+
let doc = null;
|
|
242
|
+
try {
|
|
243
|
+
doc = JSON.parse(bytes.toString('utf8'));
|
|
244
|
+
} catch {
|
|
245
|
+
/* malformed: no rows */
|
|
246
|
+
}
|
|
247
|
+
workerIndexCache.set(path, { bytes, doc, stamp });
|
|
248
|
+
return doc;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function storedAgentWorkerIndex() {
|
|
252
|
+
return readWorkerIndexFile(storedAgentWorkerIndexPath());
|
|
179
253
|
}
|
|
180
254
|
|
|
181
255
|
function storedAgentWorkerIndexRows() {
|
|
@@ -211,7 +285,8 @@ export function listStoredAgentWorkerLinks() {
|
|
|
211
285
|
|
|
212
286
|
// The agent pool only reads top-level session fields, yet each refresh used to
|
|
213
287
|
// JSON.parse every active worker's whole transcript (megabytes per worker,
|
|
214
|
-
// every catalog publish). Cache the header per file
|
|
288
|
+
// every catalog publish). Cache the header per full file stamp instead (see
|
|
289
|
+
// probeWorkerSessionHeader).
|
|
215
290
|
// The cache holds exactly the headers the latest pool assembly read: a fixed
|
|
216
291
|
// LRU cap below the index size (263 headers against 256 slots) evicted every
|
|
217
292
|
// entry just before its reuse, so each refresh re-parsed every worker
|
|
@@ -250,30 +325,120 @@ const WORKER_HEADER_FIELDS = [
|
|
|
250
325
|
...SESSION_CANCEL_FIELDS,
|
|
251
326
|
];
|
|
252
327
|
|
|
328
|
+
// One assembly reads a session's header up to three times (reap check, row
|
|
329
|
+
// projection, orphan prune); each read used to stat the file again. Within an
|
|
330
|
+
// assembly the first answer stands, absent or unreadable included.
|
|
331
|
+
let passHeaders = null; // path -> header | null, live only during an assembly
|
|
332
|
+
|
|
333
|
+
function workerSessionHeaderPath(sessionId) {
|
|
334
|
+
return join(dataDir(), 'sessions', `${sessionId}.json`);
|
|
335
|
+
}
|
|
336
|
+
|
|
253
337
|
function readWorkerSessionHeader(sessionId) {
|
|
254
|
-
const path =
|
|
255
|
-
|
|
256
|
-
|
|
338
|
+
const path = workerSessionHeaderPath(sessionId);
|
|
339
|
+
if (passHeaders?.has(path)) return passHeaders.get(path);
|
|
340
|
+
const header = probeWorkerSessionHeader(path);
|
|
341
|
+
passHeaders?.set(path, header);
|
|
342
|
+
return header;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let workerHeaderReads = 0;
|
|
346
|
+
|
|
347
|
+
function observeStamp(path) {
|
|
348
|
+
try {
|
|
349
|
+
return statSessionStamp(path);
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// A header is reused only under the canonical full-stamp policy: the file
|
|
356
|
+
// still has the exact { dev, ino, size, mtimeNs, ctimeNs } its bytes were
|
|
357
|
+
// proven to come from (stat → read → stat), and that stamp was past the racy
|
|
358
|
+
// window when read. Size + mtime alone let an equal-length rewrite that kept
|
|
359
|
+
// its mtime serve stale ownership, status and cancel fields.
|
|
360
|
+
function probeWorkerSessionHeader(path) {
|
|
361
|
+
// Absent and unreadable alike: no header.
|
|
362
|
+
const before = observeStamp(path);
|
|
363
|
+
if (!before) return null;
|
|
257
364
|
const cached = workerSessionHeaderCache.get(path);
|
|
258
|
-
if (cached && cached.
|
|
365
|
+
if (cached && sameSessionStamp(cached.stamp, before)) {
|
|
259
366
|
cached.pass = headerPass;
|
|
260
367
|
return cached.header;
|
|
261
368
|
}
|
|
262
|
-
|
|
369
|
+
workerSessionHeaderCache.delete(path);
|
|
370
|
+
// A rewrite re-reads only this record, and only its top-level fields are
|
|
371
|
+
// decoded: the transcript bytes are validated, never materialized.
|
|
372
|
+
let header = null;
|
|
373
|
+
try {
|
|
374
|
+
header = readTopLevelFields(readFileSync(path), WORKER_HEADER_FIELDS);
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
if (!header) return null;
|
|
379
|
+
workerHeaderReads += 1;
|
|
380
|
+
if (isSettledSessionStamp(before) && sameSessionStamp(before, observeStamp(path))) {
|
|
381
|
+
workerSessionHeaderCache.set(path, { stamp: before, header, pass: headerPass });
|
|
382
|
+
}
|
|
383
|
+
return header;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function observeStampAsync(path) {
|
|
387
|
+
try {
|
|
388
|
+
return sessionStampOf(await stat(path, { bigint: true }));
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** probeWorkerSessionHeader with every stat and read awaited, under the
|
|
395
|
+
* same full-stamp cache policy. */
|
|
396
|
+
async function probeWorkerSessionHeaderAsync(path) {
|
|
397
|
+
const before = await observeStampAsync(path);
|
|
398
|
+
if (!before) return null;
|
|
399
|
+
const cached = workerSessionHeaderCache.get(path);
|
|
400
|
+
if (cached && sameSessionStamp(cached.stamp, before)) return cached.header;
|
|
401
|
+
workerSessionHeaderCache.delete(path);
|
|
402
|
+
let header = null;
|
|
263
403
|
try {
|
|
264
|
-
|
|
404
|
+
header = readTopLevelFields(await readFile(path), WORKER_HEADER_FIELDS);
|
|
265
405
|
} catch {
|
|
266
406
|
return null;
|
|
267
407
|
}
|
|
268
|
-
if (!
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
408
|
+
if (!header) return null;
|
|
409
|
+
workerHeaderReads += 1;
|
|
410
|
+
if (isSettledSessionStamp(before) && sameSessionStamp(before, await observeStampAsync(path))) {
|
|
411
|
+
workerSessionHeaderCache.set(path, { stamp: before, header, pass: headerPass });
|
|
272
412
|
}
|
|
273
|
-
workerSessionHeaderCache.set(path, { mtimeMs: probe.mtimeMs, size: probe.size, header, pass: headerPass });
|
|
274
413
|
return header;
|
|
275
414
|
}
|
|
276
415
|
|
|
416
|
+
// Header reads in flight at once: each is a stat (and a read only when the
|
|
417
|
+
// file changed) on the shared libuv pool, which other file I/O also needs.
|
|
418
|
+
const HEADER_READ_CONCURRENCY = 4;
|
|
419
|
+
|
|
420
|
+
async function prefetchWorkerHeaders(sessionIds, headers) {
|
|
421
|
+
const paths = [...new Set([...sessionIds].map(workerSessionHeaderPath))].filter((path) => !headers.has(path));
|
|
422
|
+
let next = 0;
|
|
423
|
+
const drain = async () => {
|
|
424
|
+
while (next < paths.length) {
|
|
425
|
+
const path = paths[next++];
|
|
426
|
+
headers.set(path, await probeWorkerSessionHeaderAsync(path));
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
await Promise.all(Array.from({ length: Math.min(HEADER_READ_CONCURRENCY, paths.length) }, drain));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Test/diagnostic seam: cached headers and the records this process read. */
|
|
433
|
+
export function storedAgentWorkerHeaderStats() {
|
|
434
|
+
return {
|
|
435
|
+
entries: workerSessionHeaderCache.size,
|
|
436
|
+
reads: workerHeaderReads,
|
|
437
|
+
stickyIdle: stickyIdleSince.size,
|
|
438
|
+
indexes: workerIndexCache.size,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
277
442
|
/** Owner record of a child row that is a Lead CONVERSATION. Only such an owner
|
|
278
443
|
* lets a missing pool row mean "the Lead's lease expired"; an Agent-owned or
|
|
279
444
|
* unreadable owner keeps its child rows visible exactly as before. */
|
|
@@ -377,16 +542,8 @@ function projectChildWorkerRow(row, context) {
|
|
|
377
542
|
}
|
|
378
543
|
|
|
379
544
|
function storedLeadWorkerRows() {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (!parsed) {
|
|
383
|
-
try {
|
|
384
|
-
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
385
|
-
} catch {
|
|
386
|
-
/* no resident Lead pool */
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return workerRows(parsed);
|
|
545
|
+
// Absent or unreadable: no resident Lead pool.
|
|
546
|
+
return workerRows(readWorkerIndexFile(storedLeadWorkerIndexPath()));
|
|
390
547
|
}
|
|
391
548
|
|
|
392
549
|
/** A Lead-index row whose durable session turned out to be an agent's.
|
|
@@ -464,6 +621,72 @@ function pruneOrphanChildRows(bySessionId, { liveLeadSessionIds }) {
|
|
|
464
621
|
* never projected into either pool. `runningSessionIds` is the daemon's set of
|
|
465
622
|
* sessions whose turn is in flight; omitted, no row is shown as running. */
|
|
466
623
|
export function listStoredAgentWorkers({ runningSessionIds = null } = {}) {
|
|
624
|
+
headerPass += 1;
|
|
625
|
+
passHeaders = new Map();
|
|
626
|
+
try {
|
|
627
|
+
return assembleListedWorkers(runningSessionIds);
|
|
628
|
+
} finally {
|
|
629
|
+
passHeaders = null;
|
|
630
|
+
forgetUnreadHeaders();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Keep what the latest assembly read, whatever the index size; forget the rest.
|
|
635
|
+
function forgetUnreadHeaders() {
|
|
636
|
+
for (const [path, entry] of workerSessionHeaderCache) {
|
|
637
|
+
if (entry.pass !== headerPass) workerSessionHeaderCache.delete(path);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** listStoredAgentWorkers without blocking the event loop on the store: both
|
|
642
|
+
* indexes and every header the assembly can consult (each row's session,
|
|
643
|
+
* each Lead row's, and each child's owner) are read first with awaited
|
|
644
|
+
* stats and reads; the assembly then runs over what was read, with no file
|
|
645
|
+
* access left in it (any header it still asks for falls back to a read).
|
|
646
|
+
* `runningSessionIds` may be a function, asked once the reads are done. */
|
|
647
|
+
export async function listStoredAgentWorkersAsync({ runningSessionIds = null } = {}) {
|
|
648
|
+
const agentIndexPath = storedAgentWorkerIndexPath();
|
|
649
|
+
const leadIndexPath = storedLeadWorkerIndexPath();
|
|
650
|
+
const [agentIndex, leadIndex] = await Promise.all([
|
|
651
|
+
readWorkerIndexFileAsync(agentIndexPath),
|
|
652
|
+
readWorkerIndexFileAsync(leadIndexPath),
|
|
653
|
+
]);
|
|
654
|
+
const headers = new Map();
|
|
655
|
+
const childRows = workerRows(agentIndex);
|
|
656
|
+
const sessionIds = new Set(childRows.map((row) => cleanValue(row?.sessionId)));
|
|
657
|
+
for (const row of workerRows(leadIndex)) {
|
|
658
|
+
const sessionId = cleanValue(row?.sessionId);
|
|
659
|
+
if (sessionId && isStoredSessionId(sessionId)) sessionIds.add(sessionId);
|
|
660
|
+
}
|
|
661
|
+
await prefetchWorkerHeaders(sessionIds, headers);
|
|
662
|
+
const owners = new Set();
|
|
663
|
+
for (const row of childRows) {
|
|
664
|
+
const header = headers.get(workerSessionHeaderPath(cleanValue(row?.sessionId)));
|
|
665
|
+
const owner = row && typeof row === 'object' ? workerOwnerSessionId(row, header) : null;
|
|
666
|
+
if (owner) owners.add(owner);
|
|
667
|
+
}
|
|
668
|
+
await prefetchWorkerHeaders(owners, headers);
|
|
669
|
+
const running = typeof runningSessionIds === 'function' ? runningSessionIds() : runningSessionIds;
|
|
670
|
+
headerPass += 1;
|
|
671
|
+
for (const path of headers.keys()) {
|
|
672
|
+
const entry = workerSessionHeaderCache.get(path);
|
|
673
|
+
if (entry) entry.pass = headerPass;
|
|
674
|
+
}
|
|
675
|
+
passHeaders = headers;
|
|
676
|
+
passIndexes = new Map([
|
|
677
|
+
[agentIndexPath, agentIndex],
|
|
678
|
+
[leadIndexPath, leadIndex],
|
|
679
|
+
]);
|
|
680
|
+
try {
|
|
681
|
+
return assembleListedWorkers(running);
|
|
682
|
+
} finally {
|
|
683
|
+
passHeaders = null;
|
|
684
|
+
passIndexes = null;
|
|
685
|
+
forgetUnreadHeaders();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function assembleListedWorkers(runningSessionIds) {
|
|
467
690
|
const now = Date.now();
|
|
468
691
|
const index = storedAgentWorkerIndex();
|
|
469
692
|
const rows = workerRows(index);
|
|
@@ -487,21 +710,13 @@ export function listStoredAgentWorkers({ runningSessionIds = null } = {}) {
|
|
|
487
710
|
};
|
|
488
711
|
return tombstoneBlocksWork(work, findTagTombstone(work, tombstones));
|
|
489
712
|
};
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
pruneOrphanChildRows,
|
|
500
|
-
});
|
|
501
|
-
} finally {
|
|
502
|
-
// Keep what this assembly read, whatever the index size; forget the rest.
|
|
503
|
-
for (const [path, entry] of workerSessionHeaderCache) {
|
|
504
|
-
if (entry.pass !== headerPass) workerSessionHeaderCache.delete(path);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
713
|
+
return assembleStoredAgentWorkers({
|
|
714
|
+
now,
|
|
715
|
+
runningSessionIds,
|
|
716
|
+
storedAgentWorkerIndexRows: () => rows.filter((row) => !isReaped(cleanValue(row.sessionId))),
|
|
717
|
+
projectChildWorkerRow,
|
|
718
|
+
storedLeadWorkerRows,
|
|
719
|
+
projectLeadWorkerRow,
|
|
720
|
+
pruneOrphanChildRows,
|
|
721
|
+
});
|
|
507
722
|
}
|