mixdog 1.2.4 → 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/mobile-release.mjs +5 -4
- 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 +11 -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 -5
- 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 +10 -7
- 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 +255 -32
- 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/desktop-service-registry.mjs +32 -5
- 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 +436 -123
- 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 +94 -25
- 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
|
@@ -1,67 +1,393 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Off-thread parse + projection of a
|
|
2
|
+
* Off-thread read + parse + projection of a stored transcript.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* A cold pane read used to cost the daemon event loop the whole pipeline: a
|
|
5
|
+
* chunked async read whose every 512 KB chunk waited for a loop turn, a
|
|
6
|
+
* SHA-256 of the text, the strict record parse and a projection that meters
|
|
7
|
+
* every message of the transcript (`slow stored projection`). Under a busy
|
|
8
|
+
* loop each read stretched to seconds. This worker runs exactly that pipeline
|
|
9
|
+
* (stat → read → stat, digest, readTopLevelLifecycleRecord →
|
|
10
|
+
* projectStoredTranscript with no turn checkpoint) and hands back the
|
|
11
|
+
* digest and the same projection value; only the finished window crosses
|
|
12
|
+
* back.
|
|
13
|
+
*
|
|
14
|
+
* Inside the worker, one projection base (recovery, prepared session and
|
|
15
|
+
* whole-transcript context gauge) per session content is shared by every
|
|
16
|
+
* window and every concurrent request of it, and a new version of a session
|
|
17
|
+
* reuses the parsed messages its previous version already metered wherever
|
|
18
|
+
* they are unchanged, so an appended turn re-meters only the new tail.
|
|
19
|
+
* Bodies read but not yet built are bounded (`createBaseBuilds`): room is
|
|
20
|
+
* reserved before a record is read, and a newer version of a session
|
|
21
|
+
* replaces its queued older build.
|
|
10
22
|
*
|
|
11
23
|
* Client (main thread): `projectStoredTranscriptOffThread`. Worker side is
|
|
12
24
|
* active only when this file is loaded as a worker of that client.
|
|
13
25
|
*/
|
|
26
|
+
import { availableParallelism } from 'node:os';
|
|
14
27
|
import { isMainThread, workerData } from 'node:worker_threads';
|
|
15
28
|
import { createWorkerRequestClient, serveWorkerRequests } from '../../../shared/worker-requests.mjs';
|
|
16
29
|
|
|
17
|
-
/** Records at least this many
|
|
18
|
-
*
|
|
19
|
-
|
|
30
|
+
/** Records of at least this many bytes go to the worker. Smaller ones cost
|
|
31
|
+
* only a few ms here, and their parse can then be handed to the resume that
|
|
32
|
+
* follows a pane read. */
|
|
33
|
+
export const OFFLOAD_MIN_CHARS = 256 * 1024;
|
|
20
34
|
const WORKER_KIND = 'stored-transcript-projection';
|
|
21
35
|
const IDLE_EXIT_MS = 120_000;
|
|
36
|
+
// A restore burst projects every pane at once; two threads halve its tail.
|
|
37
|
+
// A session always goes to the same worker, which holds its base.
|
|
38
|
+
const POOL_SIZE = Math.max(1, Math.min(2, availableParallelism() - 1));
|
|
22
39
|
|
|
23
|
-
|
|
40
|
+
const clients = [];
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*/
|
|
31
|
-
export function projectStoredTranscriptOffThread({ sessionId, text, itemLimit }) {
|
|
32
|
-
client ||= createWorkerRequestClient(new URL(import.meta.url), {
|
|
42
|
+
function clientFor(sessionId) {
|
|
43
|
+
let hash = 0;
|
|
44
|
+
for (let index = 0; index < sessionId.length; index += 1) hash = (hash * 31 + sessionId.charCodeAt(index)) >>> 0;
|
|
45
|
+
const slot = hash % POOL_SIZE;
|
|
46
|
+
clients[slot] ||= createWorkerRequestClient(new URL(import.meta.url), {
|
|
33
47
|
idleExitMs: IDLE_EXIT_MS,
|
|
34
48
|
workerData: { kind: WORKER_KIND },
|
|
35
49
|
});
|
|
36
|
-
return
|
|
50
|
+
return clients[slot];
|
|
37
51
|
}
|
|
38
52
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Read, digest and project the record at `recordPath` for `sessionId`.
|
|
55
|
+
* Resolves `{ state, timing }` with state absent/unreadable when the file
|
|
56
|
+
* cannot be read; otherwise `{ state, textChars, textHash, stamp, timing }`
|
|
57
|
+
* plus `same: true` when the content is `known` ({ textChars, textHash }),
|
|
58
|
+
* `unreadable: true` when it is not a strict, identity-matching session
|
|
59
|
+
* record, or `{ value, lifecycle }` (`{ id, closed, generation }`). `stamp` is
|
|
60
|
+
* the full file stamp the bytes provably came from, or null. Rejects when the
|
|
61
|
+
* worker cannot answer; callers then run the pipeline on their own thread.
|
|
62
|
+
*/
|
|
63
|
+
export function projectStoredTranscriptOffThread({ sessionId, recordPath, itemLimit, known = null }) {
|
|
64
|
+
return clientFor(sessionId)({ sessionId, recordPath, itemLimit, known });
|
|
42
65
|
}
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
/** Whether the projection worker of `sessionId` is attached (a request now
|
|
68
|
+
* skips its start). */
|
|
69
|
+
export function storedTranscriptWorkerRunning(sessionId) {
|
|
70
|
+
return clientFor(sessionId).running() === true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Drop this session's projection base and queued builds in its worker (it
|
|
74
|
+
* went live). Never starts a worker just to forget. */
|
|
75
|
+
export function forgetStoredTranscriptOffThread(sessionId) {
|
|
76
|
+
const request = clientFor(sessionId);
|
|
77
|
+
if (request.running() === true) request({ forget: sessionId }).catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Worker side ─────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/** The answer a superseded or forgotten queued build gives its waiters: read
|
|
83
|
+
* the record again (it now holds that newer content, or none). */
|
|
84
|
+
export const RESTART_READ = Symbol('restart-read');
|
|
85
|
+
|
|
86
|
+
// Source characters of records read but not yet built: reserved from the
|
|
87
|
+
// file size BEFORE a record is read and held until its build settles, it
|
|
88
|
+
// joins another build, or it is dropped. A record alone larger than this is
|
|
89
|
+
// admitted only when nothing else is reserved.
|
|
90
|
+
const BUILD_CHARS_LIMIT = 16 * 1024 * 1024;
|
|
91
|
+
// Source characters of the retained bases (their parsed records and memos
|
|
92
|
+
// stay alive with them). The newest base is kept even when it alone exceeds
|
|
93
|
+
// this; the worker's idle exit releases everything.
|
|
94
|
+
const BASE_CHARS_LIMIT = 16 * 1024 * 1024;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Admission, build queue and retained bases of one worker.
|
|
98
|
+
* `build(sessionId, text, previousMessages, timing)` builds a base; it runs
|
|
99
|
+
* one at a time, in arrival order (metering yields between slices, so
|
|
100
|
+
* concurrent builds would share the thread round-robin and every pane of a
|
|
101
|
+
* restore burst would get its answer only at the end).
|
|
102
|
+
*
|
|
103
|
+
* A caller `admit`s the record's size before reading it, then `submit`s the
|
|
104
|
+
* text with its ticket (or `release`s it): retained or in-progress builds of
|
|
105
|
+
* the same content are shared; a newer version of a session replaces its
|
|
106
|
+
* queued older build, whose waiters get RESTART_READ. The previous version's
|
|
107
|
+
* messages are looked up when a build starts, so no queued build keeps an
|
|
108
|
+
* evicted or replaced base alive.
|
|
109
|
+
*/
|
|
110
|
+
export function createBaseBuilds({ build, buildCharsLimit = BUILD_CHARS_LIMIT, baseCharsLimit = BASE_CHARS_LIMIT }) {
|
|
111
|
+
/** sessionId -> { textHash, chars, built } (Map order is LRU). */
|
|
112
|
+
const bases = new Map();
|
|
113
|
+
let baseChars = 0;
|
|
114
|
+
/** Unstarted builds: { sessionId, text, textHash, chars, ticket, timing, waiters, retain }. */
|
|
115
|
+
const queue = [];
|
|
116
|
+
let running = null;
|
|
117
|
+
let draining = false;
|
|
118
|
+
/** Waiting for room: { chars, resolve } in arrival order. */
|
|
119
|
+
const admissions = [];
|
|
120
|
+
let reservedChars = 0;
|
|
121
|
+
let peakReservedChars = 0;
|
|
122
|
+
let builds = 0;
|
|
123
|
+
|
|
124
|
+
const dropBase = (sessionId) => {
|
|
125
|
+
const entry = bases.get(sessionId);
|
|
126
|
+
if (!entry) return;
|
|
127
|
+
bases.delete(sessionId);
|
|
128
|
+
baseChars -= entry.chars;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const retainBase = (sessionId, entry) => {
|
|
132
|
+
dropBase(sessionId);
|
|
133
|
+
bases.set(sessionId, entry);
|
|
134
|
+
baseChars += entry.chars;
|
|
135
|
+
while (baseChars > baseCharsLimit && bases.size > 1) dropBase(bases.keys().next().value);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const reserve = (ticket, chars) => {
|
|
139
|
+
reservedChars += chars - ticket.chars;
|
|
140
|
+
ticket.chars = chars;
|
|
141
|
+
peakReservedChars = Math.max(peakReservedChars, reservedChars);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const pump = () => {
|
|
145
|
+
while (admissions.length > 0 && (reservedChars === 0 || reservedChars + admissions[0].chars <= buildCharsLimit)) {
|
|
146
|
+
const { chars, resolve } = admissions.shift();
|
|
147
|
+
const ticket = { chars: 0, released: false };
|
|
148
|
+
reserve(ticket, chars);
|
|
149
|
+
resolve(ticket);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const release = (ticket) => {
|
|
154
|
+
if (!ticket || ticket.released) return;
|
|
155
|
+
ticket.released = true;
|
|
156
|
+
reservedChars -= ticket.chars;
|
|
157
|
+
ticket.chars = 0;
|
|
158
|
+
pump();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// An unstarted build nobody needs any more: its body and reservation go,
|
|
162
|
+
// its waiters read again.
|
|
163
|
+
const drop = (job) => {
|
|
164
|
+
queue.splice(queue.indexOf(job), 1);
|
|
165
|
+
job.text = null;
|
|
166
|
+
release(job.ticket);
|
|
167
|
+
for (const waiter of job.waiters.splice(0)) waiter.resolve(RESTART_READ);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
async function drain() {
|
|
171
|
+
if (draining) return;
|
|
172
|
+
draining = true;
|
|
173
|
+
try {
|
|
174
|
+
while (queue.length > 0) {
|
|
175
|
+
const job = queue.shift();
|
|
176
|
+
running = job;
|
|
177
|
+
let outcome;
|
|
178
|
+
try {
|
|
179
|
+
const previous = bases.get(job.sessionId)?.built.messages ?? null;
|
|
180
|
+
outcome = { value: await build(job.sessionId, job.text, previous, job.timing) };
|
|
181
|
+
} catch (error) {
|
|
182
|
+
outcome = { error };
|
|
183
|
+
}
|
|
184
|
+
running = null;
|
|
185
|
+
job.text = null;
|
|
186
|
+
release(job.ticket);
|
|
187
|
+
builds += 1;
|
|
188
|
+
const { value, error } = outcome;
|
|
189
|
+
if (job.retain && value && !value.unreadable) {
|
|
190
|
+
retainBase(job.sessionId, { textHash: job.textHash, chars: job.chars, built: value });
|
|
191
|
+
}
|
|
192
|
+
for (const waiter of job.waiters.splice(0)) {
|
|
193
|
+
if (error) waiter.reject(error);
|
|
194
|
+
else waiter.resolve(value);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
draining = false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
/** Resolves a ticket reserving `chars` once they fit the budget. */
|
|
204
|
+
admit(chars) {
|
|
205
|
+
const admitted = new Promise((resolve) => admissions.push({ chars: Math.max(0, chars || 0), resolve }));
|
|
206
|
+
pump();
|
|
207
|
+
return admitted;
|
|
208
|
+
},
|
|
209
|
+
/** The record read is `chars` long (its size was only an estimate). */
|
|
210
|
+
resize(ticket, chars) {
|
|
211
|
+
if (!ticket.released) reserve(ticket, chars);
|
|
212
|
+
},
|
|
213
|
+
release,
|
|
214
|
+
/** The base of this exact content (the ticket passes to the build or is
|
|
215
|
+
* released), or RESTART_READ. */
|
|
216
|
+
submit(sessionId, text, textHash, ticket, timing) {
|
|
217
|
+
const chars = text.length;
|
|
218
|
+
const retained = bases.get(sessionId);
|
|
219
|
+
if (retained && retained.textHash === textHash && retained.chars === chars) {
|
|
220
|
+
release(ticket);
|
|
221
|
+
retainBase(sessionId, retained);
|
|
222
|
+
timing.base = 'shared';
|
|
223
|
+
return Promise.resolve(retained.built);
|
|
224
|
+
}
|
|
225
|
+
const sameContent = (job) =>
|
|
226
|
+
!!job && job.sessionId === sessionId && job.textHash === textHash && job.chars === chars;
|
|
227
|
+
let job = sameContent(running) ? running : queue.find(sameContent);
|
|
228
|
+
if (job) {
|
|
229
|
+
release(ticket);
|
|
230
|
+
timing.base = 'shared';
|
|
231
|
+
} else {
|
|
232
|
+
for (const older of queue.filter((queued) => queued.sessionId === sessionId)) drop(older);
|
|
233
|
+
job = { sessionId, text, textHash, chars, ticket, timing, waiters: [], retain: true };
|
|
234
|
+
queue.push(job);
|
|
235
|
+
}
|
|
236
|
+
const answer = new Promise((resolve, reject) => job.waiters.push({ resolve, reject }));
|
|
237
|
+
void drain();
|
|
238
|
+
return answer;
|
|
239
|
+
},
|
|
240
|
+
/** The session went live: drop its base and queued builds; one already
|
|
241
|
+
* running answers its waiters but is not retained. */
|
|
242
|
+
forget(sessionId) {
|
|
243
|
+
dropBase(sessionId);
|
|
244
|
+
for (const queued of queue.filter((job) => job.sessionId === sessionId)) drop(queued);
|
|
245
|
+
if (running?.sessionId === sessionId) running.retain = false;
|
|
246
|
+
},
|
|
247
|
+
stats() {
|
|
248
|
+
return {
|
|
249
|
+
reservedChars,
|
|
250
|
+
peakReservedChars,
|
|
251
|
+
queuedBuilds: queue.length,
|
|
252
|
+
queuedChars: queue.reduce((sum, job) => sum + job.chars, 0),
|
|
253
|
+
waitingAdmissions: admissions.length,
|
|
254
|
+
builds,
|
|
255
|
+
retainedBases: bases.size,
|
|
256
|
+
baseChars,
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Whether two JSON.parse results are the same value, key order included
|
|
263
|
+
* (estimators serialize them). */
|
|
264
|
+
export function sameJsonValue(left, right) {
|
|
265
|
+
if (left === right) return typeof left !== 'number' || Object.is(left, right);
|
|
266
|
+
if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false;
|
|
267
|
+
if (Array.isArray(left)) {
|
|
268
|
+
if (!Array.isArray(right) || left.length !== right.length) return false;
|
|
269
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
270
|
+
if (!sameJsonValue(left[index], right[index])) return false;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
if (Array.isArray(right)) return false;
|
|
275
|
+
const leftKeys = Object.keys(left);
|
|
276
|
+
const rightKeys = Object.keys(right);
|
|
277
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
278
|
+
for (let index = 0; index < leftKeys.length; index += 1) {
|
|
279
|
+
const key = leftKeys[index];
|
|
280
|
+
if (rightKeys[index] !== key || !sameJsonValue(left[key], right[key])) return false;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Replace the leading messages of `next` that are the same value as those of
|
|
287
|
+
* `previous` with the previous objects, whose per-message meter memos the
|
|
288
|
+
* projection then reads instead of re-metering. Returns how many were reused.
|
|
289
|
+
* Projections never mutate messages, and the comparison is against each
|
|
290
|
+
* previous object as it is now, so the result is the projection of `next`.
|
|
291
|
+
*/
|
|
292
|
+
export function reuseParsedPrefix(next, previous) {
|
|
293
|
+
if (!Array.isArray(next) || !Array.isArray(previous)) return 0;
|
|
294
|
+
const shared = Math.min(next.length, previous.length);
|
|
295
|
+
let index = 0;
|
|
296
|
+
while (index < shared && sameJsonValue(next[index], previous[index])) {
|
|
297
|
+
next[index] = previous[index];
|
|
298
|
+
index += 1;
|
|
299
|
+
}
|
|
300
|
+
return index;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function buildBase(sessionId, text, previousMessages, timing) {
|
|
304
|
+
const [{ readTopLevelLifecycleRecord, isLifecycleUnreadable }, { prepareStoredTranscriptBase }] = await Promise.all([
|
|
47
305
|
import('./lifecycle-scan.mjs'),
|
|
48
306
|
import('./store-transcript-projection.mjs'),
|
|
49
307
|
]);
|
|
50
308
|
const parseStartedAt = performance.now();
|
|
51
309
|
const record = readTopLevelLifecycleRecord(text);
|
|
52
310
|
if (isLifecycleUnreadable(record) || record.id !== sessionId) return { unreadable: true };
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
parseMs: projectStartedAt - parseStartedAt,
|
|
62
|
-
projectMs: performance.now() - projectStartedAt,
|
|
311
|
+
const messages = record.doc.messages;
|
|
312
|
+
timing.reusedMessages = reuseParsedPrefix(messages, previousMessages);
|
|
313
|
+
timing.parseMs = performance.now() - parseStartedAt;
|
|
314
|
+
const base = await prepareStoredTranscriptBase(sessionId, record.doc, { checkpointAbsent: true });
|
|
315
|
+
return {
|
|
316
|
+
base,
|
|
317
|
+
messages,
|
|
318
|
+
lifecycle: { id: record.id, closed: record.closed, generation: record.generation },
|
|
63
319
|
};
|
|
64
|
-
return { value, lifecycle: { id: record.id, closed: record.closed, generation: record.generation }, timing };
|
|
65
320
|
}
|
|
66
321
|
|
|
67
|
-
|
|
322
|
+
const baseBuilds = createBaseBuilds({ build: buildBase });
|
|
323
|
+
|
|
324
|
+
async function project({ sessionId, recordPath, itemLimit, known }) {
|
|
325
|
+
const importStartedAt = performance.now();
|
|
326
|
+
const [{ statSessionStamp, sameSessionStamp }, { readTextFile, PROBE_PRESENT }, { transcriptTextHash }, projection] =
|
|
327
|
+
await Promise.all([
|
|
328
|
+
import('./store/canonical-reader.mjs'),
|
|
329
|
+
import('./store/fs-probe.mjs'),
|
|
330
|
+
import('./store-transcript-cache.mjs'),
|
|
331
|
+
import('./store-transcript-projection.mjs'),
|
|
332
|
+
]);
|
|
333
|
+
const timing = { importMs: performance.now() - importStartedAt };
|
|
334
|
+
const observe = () => {
|
|
335
|
+
try {
|
|
336
|
+
return statSessionStamp(recordPath);
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
for (let restarts = 0; ; restarts += 1) {
|
|
342
|
+
if (restarts > 0) timing.restarts = restarts;
|
|
343
|
+
// Room for the body is reserved before it is read (bytes bound chars).
|
|
344
|
+
const sized = observe();
|
|
345
|
+
const admitStartedAt = performance.now();
|
|
346
|
+
const ticket = await baseBuilds.admit(sized ? Number(sized.size) : 0);
|
|
347
|
+
timing.admitMs = performance.now() - admitStartedAt;
|
|
348
|
+
let submitted = false;
|
|
349
|
+
try {
|
|
350
|
+
const readStartedAt = performance.now();
|
|
351
|
+
const before = observe();
|
|
352
|
+
let body = readTextFile(recordPath);
|
|
353
|
+
if (body.state !== PROBE_PRESENT) return { state: body.state, timing };
|
|
354
|
+
const stamp = before && sameSessionStamp(before, observe()) ? before : null;
|
|
355
|
+
let text = body.text;
|
|
356
|
+
body = null;
|
|
357
|
+
baseBuilds.resize(ticket, text.length);
|
|
358
|
+
const digest = {
|
|
359
|
+
state: PROBE_PRESENT,
|
|
360
|
+
textChars: text.length,
|
|
361
|
+
textHash: transcriptTextHash(text),
|
|
362
|
+
stamp,
|
|
363
|
+
timing,
|
|
364
|
+
};
|
|
365
|
+
timing.readMs = performance.now() - readStartedAt;
|
|
366
|
+
if (known && known.textHash === digest.textHash && known.textChars === digest.textChars) {
|
|
367
|
+
return { ...digest, same: true };
|
|
368
|
+
}
|
|
369
|
+
const projectStartedAt = performance.now();
|
|
370
|
+
submitted = true;
|
|
371
|
+
const pending = baseBuilds.submit(sessionId, text, digest.textHash, ticket, timing);
|
|
372
|
+
// Only the build may keep the body; a joined read lets go of its copy.
|
|
373
|
+
text = null;
|
|
374
|
+
const built = await pending;
|
|
375
|
+
if (built === RESTART_READ) continue;
|
|
376
|
+
if (built.unreadable) return { ...digest, unreadable: true };
|
|
377
|
+
const value = await projection.windowStoredTranscript(sessionId, built.base, {
|
|
378
|
+
itemLimit,
|
|
379
|
+
includeMessages: false,
|
|
380
|
+
});
|
|
381
|
+
timing.projectMs = performance.now() - projectStartedAt;
|
|
382
|
+
return { ...digest, value, lifecycle: built.lifecycle };
|
|
383
|
+
} finally {
|
|
384
|
+
if (!submitted) baseBuilds.release(ticket);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!isMainThread && workerData?.kind === WORKER_KIND) {
|
|
390
|
+
serveWorkerRequests((payload) => {
|
|
391
|
+
return payload?.forget ? baseBuilds.forget(payload.forget) : project(payload);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
@@ -701,9 +701,16 @@ async function _doSave(payload) {
|
|
|
701
701
|
// Durable lifecycle barriers (tombstone plant + detach generation bump) live
|
|
702
702
|
// in store/lifecycle-barriers.mjs; re-exported so prior importers of
|
|
703
703
|
// store.mjs stay unchanged.
|
|
704
|
-
export {
|
|
704
|
+
export {
|
|
705
|
+
markSessionClosed,
|
|
706
|
+
bumpSessionGeneration,
|
|
707
|
+
markSessionClosedAsync,
|
|
708
|
+
markSessionClosedAttempt,
|
|
709
|
+
bumpSessionGenerationAttempt,
|
|
710
|
+
isLifecycleRetry,
|
|
711
|
+
} from './store/lifecycle-barriers.mjs';
|
|
705
712
|
// Session reads live in store/load-session.mjs, same facade contract.
|
|
706
|
-
export { loadSession } from './store/load-session.mjs';
|
|
713
|
+
export { loadSession, loadSessionStatus } from './store/load-session.mjs';
|
|
707
714
|
|
|
708
715
|
// Hard delete + owned-agent enumeration live in store/session-delete.mjs;
|
|
709
716
|
// re-exported here so prior importers of store.mjs stay unchanged.
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
// Per-session wake control for `task wait
|
|
1
|
+
// Per-session wake control for `task wait` and the foreground shell hold.
|
|
2
2
|
//
|
|
3
3
|
// This is intentionally separate from the turn AbortController: new user input
|
|
4
|
-
//
|
|
5
|
-
// and then
|
|
6
|
-
// task itself keeps running.
|
|
4
|
+
// or a queued task notification releases only the passive wait, lets the tool
|
|
5
|
+
// return a running snapshot, and then flows through the normal post-tool
|
|
6
|
+
// steering boundary. The background task itself keeps running.
|
|
7
7
|
const waitersBySession = new Map();
|
|
8
8
|
const steeringChecks = new Set();
|
|
9
9
|
|
|
10
|
+
// Why a wait was released early: user input/steering, or a deliverable task
|
|
11
|
+
// notification queued for the session.
|
|
12
|
+
export const TASK_WAIT_WAKE_USER = 'user-message';
|
|
13
|
+
export const TASK_WAIT_WAKE_NOTIFICATION = 'notification';
|
|
14
|
+
|
|
10
15
|
// Scoped to an active turn. Read the live queue at wait entry rather than
|
|
11
16
|
// retaining a wake signal that could outlive a consumed or reclaimed prompt.
|
|
17
|
+
// A check returns a wake reason (or true for user input) when something
|
|
18
|
+
// deliverable is already queued for the session, else a falsy value.
|
|
12
19
|
export function registerTaskWaitSteeringCheck(check) {
|
|
13
20
|
steeringChecks.add(check);
|
|
14
21
|
return () => steeringChecks.delete(check);
|
|
@@ -32,6 +39,8 @@ export function beginInterruptibleTaskWait(sessionId, parentSignal = null) {
|
|
|
32
39
|
const waiter = {
|
|
33
40
|
controller,
|
|
34
41
|
interruptedByUser: false,
|
|
42
|
+
wakeReason: null,
|
|
43
|
+
wakeDetail: null,
|
|
35
44
|
};
|
|
36
45
|
let waiters = waitersBySession.get(key);
|
|
37
46
|
if (!waiters) {
|
|
@@ -53,9 +62,18 @@ export function beginInterruptibleTaskWait(sessionId, parentSignal = null) {
|
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
if (!controller.signal.aborted
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
if (!controller.signal.aborted) {
|
|
66
|
+
let queued = null;
|
|
67
|
+
for (const check of steeringChecks) {
|
|
68
|
+
const found = check(key);
|
|
69
|
+
if (!found) continue;
|
|
70
|
+
const reason = found === true ? TASK_WAIT_WAKE_USER : String(found);
|
|
71
|
+
// User input outranks a notification: its result text asks the model
|
|
72
|
+
// to handle the new message first.
|
|
73
|
+
if (!queued || reason === TASK_WAIT_WAKE_USER) queued = reason;
|
|
74
|
+
if (queued === TASK_WAIT_WAKE_USER) break;
|
|
75
|
+
}
|
|
76
|
+
if (queued) wakeWaiter(waiter, queued, null);
|
|
59
77
|
}
|
|
60
78
|
|
|
61
79
|
return {
|
|
@@ -63,6 +81,12 @@ export function beginInterruptibleTaskWait(sessionId, parentSignal = null) {
|
|
|
63
81
|
get interruptedByUser() {
|
|
64
82
|
return waiter.interruptedByUser;
|
|
65
83
|
},
|
|
84
|
+
get wakeReason() {
|
|
85
|
+
return waiter.wakeReason;
|
|
86
|
+
},
|
|
87
|
+
get wakeDetail() {
|
|
88
|
+
return waiter.wakeDetail;
|
|
89
|
+
},
|
|
66
90
|
dispose() {
|
|
67
91
|
if (disposed) return;
|
|
68
92
|
disposed = true;
|
|
@@ -77,17 +101,24 @@ export function beginInterruptibleTaskWait(sessionId, parentSignal = null) {
|
|
|
77
101
|
};
|
|
78
102
|
}
|
|
79
103
|
|
|
80
|
-
|
|
104
|
+
function wakeWaiter(waiter, reason, detail) {
|
|
105
|
+
waiter.interruptedByUser = reason !== TASK_WAIT_WAKE_NOTIFICATION;
|
|
106
|
+
waiter.wakeReason = reason;
|
|
107
|
+
waiter.wakeDetail = detail || null;
|
|
108
|
+
try {
|
|
109
|
+
waiter.controller.abort(reason);
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// `detail` names what woke the wait (the notified task id) for its result text.
|
|
114
|
+
export function interruptTaskWaitForSession(sessionId, reason = TASK_WAIT_WAKE_USER, detail = null) {
|
|
81
115
|
const waiters = waitersBySession.get(sessionKey(sessionId));
|
|
82
116
|
if (!waiters?.size) return 0;
|
|
83
117
|
let interrupted = 0;
|
|
84
118
|
for (const waiter of [...waiters]) {
|
|
85
119
|
if (waiter.controller.signal.aborted) continue;
|
|
86
|
-
waiter.interruptedByUser = true;
|
|
87
120
|
interrupted += 1;
|
|
88
|
-
|
|
89
|
-
waiter.controller.abort(reason);
|
|
90
|
-
} catch {}
|
|
121
|
+
wakeWaiter(waiter, reason, detail);
|
|
91
122
|
}
|
|
92
123
|
return interrupted;
|
|
93
124
|
}
|
|
@@ -19,11 +19,12 @@ import { getPluginData } from '../config.mjs';
|
|
|
19
19
|
import { countSplitLines, normalizeOutputPath } from '../tools/builtin/path-utils.mjs';
|
|
20
20
|
import { classifyResultKind } from './result-classification.mjs';
|
|
21
21
|
import { registerSessionPurgeHook } from './store.mjs';
|
|
22
|
-
import { SETUP_OUTPUT_MAX_BYTES } from '../../../shared/tool-output-limit.mjs';
|
|
22
|
+
import { SETUP_OUTPUT_MAX_BYTES, capMarker } from '../../../shared/tool-output-limit.mjs';
|
|
23
23
|
|
|
24
24
|
const TOOL_RESULT_OFFLOAD_THRESHOLD_CHARS = 50_000;
|
|
25
25
|
const TOOL_RESULT_SETUP_THRESHOLD_CHARS = SETUP_OUTPUT_MAX_BYTES;
|
|
26
26
|
const TOOL_RESULT_PREVIEW_CHARS = 512;
|
|
27
|
+
const TOOL_RESULT_GIT_PREVIEW_CHARS = 8_000;
|
|
27
28
|
const TOOL_RESULT_SHELL_THRESHOLD_CHARS = 30_000;
|
|
28
29
|
const TOOL_RESULT_SEARCH_THRESHOLD_CHARS = 50_000;
|
|
29
30
|
const TOOL_RESULT_GREP_THRESHOLD_CHARS = 20_000;
|
|
@@ -38,7 +39,8 @@ const ARTIFACT_READ_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (
|
|
|
38
39
|
// Per-tool persistence limits are per-tool maxResultSizeChars values rather
|
|
39
40
|
// than a single global value: grep persists at 20k, glob and list/find_* at
|
|
40
41
|
// the 50k system default (deliberately tighter than the common 100k), and
|
|
41
|
-
//
|
|
42
|
+
// bash_session/terminal at 30k. shell/task stay inline: the shell delivery
|
|
43
|
+
// helper already capped them, with the full log on disk. Read/head/tail/diff stay inline
|
|
42
44
|
// (Infinity) — they are self-bound by FileRead semantics and the upstream
|
|
43
45
|
// READ_MAX_SIZE_BYTES cap, so persisting to a sidecar to be re-read would be
|
|
44
46
|
// circular. These values keep context-rich IO tools from turning into "read
|
|
@@ -59,10 +61,13 @@ const INLINE_THRESHOLD_BY_TOOL = new Map([
|
|
|
59
61
|
['tree', TOOL_RESULT_SEARCH_THRESHOLD_CHARS],
|
|
60
62
|
['find_files', TOOL_RESULT_SEARCH_THRESHOLD_CHARS],
|
|
61
63
|
['code_graph', TOOL_RESULT_SEARCH_THRESHOLD_CHARS],
|
|
62
|
-
|
|
64
|
+
// shell and task results are already capped by the shared shell delivery
|
|
65
|
+
// (tools/builtin/lib/shell-delivery.mjs) and name their full log on disk;
|
|
66
|
+
// offloading them again would cut a capped result down to a preview.
|
|
67
|
+
['shell', Infinity],
|
|
63
68
|
['bash_session', TOOL_RESULT_SHELL_THRESHOLD_CHARS],
|
|
64
69
|
['terminal', TOOL_RESULT_SHELL_THRESHOLD_CHARS],
|
|
65
|
-
['task',
|
|
70
|
+
['task', Infinity],
|
|
66
71
|
['setup', TOOL_RESULT_SETUP_THRESHOLD_CHARS],
|
|
67
72
|
]);
|
|
68
73
|
|
|
@@ -110,6 +115,8 @@ function offloadableTextLength(result) {
|
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
const AGGREGATE_OFFLOAD_EXCLUDED_TOOLS = new Set([
|
|
118
|
+
'shell',
|
|
119
|
+
'task',
|
|
113
120
|
'read',
|
|
114
121
|
'head',
|
|
115
122
|
'tail',
|
|
@@ -321,11 +328,21 @@ async function offloadText(sessionId, toolCallId, toolName, text, channel) {
|
|
|
321
328
|
});
|
|
322
329
|
if (!artifact) return text;
|
|
323
330
|
|
|
324
|
-
|
|
331
|
+
// git offloads were fetched 95% of the time from a 512-char preview, so git
|
|
332
|
+
// keeps a head+tail large enough to answer from directly.
|
|
333
|
+
const previewChars = String(toolName || '').toLowerCase() === 'git' ? TOOL_RESULT_GIT_PREVIEW_CHARS : undefined;
|
|
334
|
+
const { preview, truncated } = buildPreview(text, previewChars);
|
|
325
335
|
const sizeKb = Math.max(1, Math.round(text.length / 1024));
|
|
326
336
|
const displayPath = normalizeOutputPath(artifact.path);
|
|
327
337
|
const header = `${TOOL_RESULT_OFFLOAD_PREFIX} ${toolName} → ${displayPath} (${sizeKb} KB, ${artifact.lines} lines)]`;
|
|
328
|
-
|
|
338
|
+
// The header already names the path; git repeats it after its long preview,
|
|
339
|
+
// where the header is far above the cut.
|
|
340
|
+
let suffix = '';
|
|
341
|
+
if (truncated) {
|
|
342
|
+
suffix = previewChars
|
|
343
|
+
? `\n${capMarker({ limit: `${previewChars} chars`, narrow: 'read it with offset/limit', fullPath: displayPath })}`
|
|
344
|
+
: '\n[preview truncated; full output preserved at the artifact path above]';
|
|
345
|
+
}
|
|
329
346
|
return `${header}\n\n${preview}${suffix}`;
|
|
330
347
|
}
|
|
331
348
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { coerceReadFamilyPathArg, coerceShapeFlex, hasGlobMagic, normalizeGrepArgs } from './path-utils.mjs';
|
|
13
13
|
import { hasOwn } from '../../../../shared/object.mjs';
|
|
14
14
|
import { CODE_GRAPH_FILE_MODES, CODE_GRAPH_MODES } from '../code-graph-tool-defs.mjs';
|
|
15
|
+
import { SHELL_HOLD_MAX_MS, SHELL_HOLD_MIN_MS } from './wait-budgets.mjs';
|
|
15
16
|
|
|
16
17
|
const MAX_INT = 100000;
|
|
17
18
|
export const PUBLIC_PATH_BATCH_LIMIT = 10;
|
|
@@ -737,7 +738,7 @@ function guardRead(a) {
|
|
|
737
738
|
if (err) return err;
|
|
738
739
|
for (const ek of ['offset', 'limit']) {
|
|
739
740
|
const eErr = checkIntInRange(entry, ek, 0, ek === 'offset' ? READ_OFFSET_MAX : MAX_INT);
|
|
740
|
-
if (eErr) return eErr.replace(`"${ek}"`, `"path[${i}].${ek}"`);
|
|
741
|
+
if (eErr) return eErr.replace(`"${ek}"`, () => `"path[${i}].${ek}"`);
|
|
741
742
|
}
|
|
742
743
|
}
|
|
743
744
|
}
|
|
@@ -779,16 +780,21 @@ function guardRead(a) {
|
|
|
779
780
|
}
|
|
780
781
|
|
|
781
782
|
function guardShell(a) {
|
|
782
|
-
const allowed = new Set(['command', 'timeout_ms', 'wait_ms']);
|
|
783
|
+
const allowed = new Set(['command', 'timeout_ms', 'wait_ms', 'background']);
|
|
783
784
|
const unsupported = Object.keys(a).find((key) => !allowed.has(key));
|
|
784
785
|
if (unsupported) {
|
|
785
|
-
return `Error: shell arg "${unsupported}" is unsupported; use only command, timeout_ms, and
|
|
786
|
+
return `Error: shell arg "${unsupported}" is unsupported; use only command, timeout_ms, wait_ms, and background`;
|
|
787
|
+
}
|
|
788
|
+
if (hasOwn(a, 'background') && typeof a.background !== 'boolean') {
|
|
789
|
+
return `Error: shell arg "background" must be a boolean (got ${describeType(a.background)})`;
|
|
786
790
|
}
|
|
787
791
|
// Over-max is an unambiguous "wait as long as allowed"; clamp like the other
|
|
788
792
|
// saturating integer args instead of failing the call.
|
|
789
|
-
if (hasOwn(a, 'wait_ms') && Number.isInteger(a.wait_ms) && a.wait_ms >
|
|
790
|
-
|
|
791
|
-
|
|
793
|
+
if (hasOwn(a, 'wait_ms') && Number.isInteger(a.wait_ms) && a.wait_ms > SHELL_HOLD_MAX_MS) {
|
|
794
|
+
a.wait_ms = SHELL_HOLD_MAX_MS;
|
|
795
|
+
}
|
|
796
|
+
if (hasOwn(a, 'wait_ms') && (!Number.isInteger(a.wait_ms) || a.wait_ms < SHELL_HOLD_MIN_MS)) {
|
|
797
|
+
return `Error: shell arg "wait_ms" must be an integer from ${SHELL_HOLD_MIN_MS} to ${SHELL_HOLD_MAX_MS}, e.g. wait_ms:120000 (got ${JSON.stringify(a.wait_ms)})`;
|
|
792
798
|
}
|
|
793
799
|
if (!hasOwn(a, 'command')) {
|
|
794
800
|
return 'Error: shell requires "command"';
|