mixdog 1.1.1 → 1.2.0
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/README.md +1 -0
- package/package.json +2 -2
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/release-paths.mjs +39 -2
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +6 -0
- package/src/defaults/skills/setup/references/actions.md +5 -2
- package/src/defaults/skills/visualize/SKILL.md +157 -0
- package/src/defaults/skills/visualize/references/craft.md +88 -0
- package/src/headless-command.mjs +56 -3
- package/src/headless-exec.mjs +17 -1
- package/src/headless-role.mjs +2 -2
- package/src/help.mjs +2 -0
- package/src/lib/rules-builder.cjs +0 -23
- package/src/rules/lead/LEAD.md +2 -2
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
- package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +9 -1
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +11 -8
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
- package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +0 -10
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/channels/index.mjs +1 -5
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
- package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
- package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
- package/src/runtime/channels/lib/worker-main.mjs +4 -34
- package/src/runtime/shared/atomic-file.mjs +8 -2
- package/src/runtime/shared/best-effort-stderr.mjs +8 -0
- package/src/runtime/shared/code-graph-state.mjs +12 -0
- package/src/runtime/shared/defaults-separation.mjs +2 -0
- package/src/runtime/shared/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- package/src/runtime/shared/llm/default-context-window.mjs +6 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
- package/src/runtime/shared/llm/model-catalog.mjs +240 -118
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
- package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
- package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
- package/src/runtime/shared/model-proxy-mode.mjs +19 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -2
- package/src/runtime/shared/provider-api-key.mjs +21 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/semver.d.mts +1 -0
- package/src/runtime/shared/semver.mjs +57 -0
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-surface.mjs +9 -2
- package/src/runtime/shared/tool-work-units.mjs +14 -0
- package/src/runtime/shared/transcript-metadata.mjs +17 -1
- package/src/runtime/shared/turn-changes.mjs +791 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
- package/src/runtime/shared/turn-snapshot.mjs +690 -81
- package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
- package/src/runtime/shared/update-checker.mjs +2 -31
- package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
- package/src/runtime/shared/worktrees/contract.mjs +551 -0
- package/src/runtime/shared/worktrees/disk.mjs +57 -0
- package/src/runtime/shared/worktrees/git.mjs +247 -0
- package/src/runtime/shared/worktrees/include.mjs +480 -0
- package/src/runtime/shared/worktrees/lock.mjs +46 -0
- package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
- package/src/runtime/shared/worktrees/manager.mjs +1215 -0
- package/src/runtime/shared/worktrees/merge.mjs +182 -0
- package/src/runtime/shared/worktrees/owner.mjs +46 -0
- package/src/runtime/shared/worktrees/paths.mjs +62 -0
- package/src/runtime/shared/worktrees/registry.mjs +110 -0
- package/src/runtime/shared/worktrees/settings.mjs +84 -0
- package/src/runtime/shared/worktrees/slug.mjs +62 -0
- package/src/runtime/shared/worktrees/test-support.mjs +96 -0
- package/src/runtime/visualize-bridge/client.mjs +181 -0
- package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
- package/src/session-runtime/boot/apis.mjs +35 -0
- package/src/session-runtime/boot/begin.mjs +0 -2
- package/src/session-runtime/boot/config.mjs +2 -0
- package/src/session-runtime/boot/facade.mjs +20 -0
- package/src/session-runtime/boot/infrastructure.mjs +1 -0
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +9 -4
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
- package/src/session-runtime/global-extensions.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
- package/src/session-runtime/internal-tool-executor.mjs +9 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
- package/src/session-runtime/mcp-glue/connect.mjs +11 -6
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- package/src/session-runtime/model-route/route-persist.mjs +3 -0
- package/src/session-runtime/model-route/set-route.mjs +15 -2
- package/src/session-runtime/notification-bus.mjs +9 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/resource-extension-sync.mjs +30 -7
- package/src/session-runtime/resource-mcp-api.mjs +27 -10
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-bootstrap.mjs +0 -4
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-paths.mjs +0 -1
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
- package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
- package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
- package/src/session-runtime/services/agent-tool.mjs +1 -0
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/daemon-candidate.mjs +3 -2
- package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
- package/src/session-runtime/session-create/session-options.mjs +10 -1
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/settings-api.mjs +4 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
- package/src/session-runtime/settings-profile-api.mjs +27 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +11 -4
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
- package/src/session-runtime/starter-guide-state.mjs +62 -0
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
- package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
- package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
- package/src/session-runtime/turn/turn-run.mjs +6 -7
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
- package/src/session-runtime/workspace/admission-gate.mjs +197 -0
- package/src/session-runtime/workspace/project-branches.mjs +136 -0
- package/src/session-runtime/workspace/transition-steps.mjs +364 -0
- package/src/session-runtime/workspace/transitions.mjs +651 -0
- package/src/session-runtime/workspace/workspace-state.mjs +112 -0
- package/src/standalone/channel-binding.mjs +1 -2
- package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-channels-loader.mjs +2 -3
- package/src/standalone/daemon-desktop-runtime.mjs +1 -0
- package/src/standalone/daemon-owner.mjs +372 -0
- package/src/standalone/daemon-stored-session-views.mjs +4 -1
- package/src/standalone/daemon.mjs +139 -5
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +13 -78
- package/src/standalone/session-client/attachment-calls.mjs +3 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
- package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
- package/src/standalone/session-client.mjs +104 -13
- package/src/standalone/session-protocol.mjs +17 -0
- package/src/standalone/session-proxy.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
- package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
- package/src/standalone/session-service/projection/revisions.mjs +2 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
- package/src/standalone/session-service/projection/workspace.mjs +14 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
- package/src/standalone/session-transport/call-route.mjs +13 -0
- package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
- package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
- package/src/standalone/session-transport/client-registry.mjs +20 -3
- package/src/standalone/session-transport/routes.mjs +79 -2
- package/src/standalone/session-transport/stack.mjs +2 -0
- package/src/standalone/session-transport.mjs +24 -1
- package/src/tui/app/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +35 -5
- package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
- package/src/tui/app/model-picker.mjs +4 -2
- package/src/tui/dist/index.mjs +332 -36
- package/src/tui/index.jsx +10 -2
- package/src/tui/session/compaction-view.mjs +147 -0
- package/src/tui/session/context-state.mjs +6 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/quota-resume.mjs +2 -1
- package/src/tui/session/session-api/commands.mjs +10 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +12 -8
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +18 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
- package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
- package/src/tui/session/session-api/routes.mjs +10 -1
- package/src/tui/session/session-api/settings.mjs +2 -0
- package/src/tui/session/session-api/shared.mjs +10 -6
- package/src/tui/session/session-api/worktree.mjs +60 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +31 -12
- package/src/tui/session/session-flow/drain.mjs +15 -1
- package/src/tui/session/session-flow/session-reset.mjs +4 -3
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/transcript-intake.mjs +14 -5
- package/src/tui/session/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +39 -10
- package/src/runtime/channels/tool-defs.mjs +0 -17
|
@@ -24,8 +24,9 @@ import {
|
|
|
24
24
|
} from './store-transcript-worker.mjs';
|
|
25
25
|
import { projectStoredTranscript } from './store-transcript-projection.mjs';
|
|
26
26
|
import { dataDir, sessionHeartbeatMtimes } from './store-summary-locations.mjs';
|
|
27
|
-
import { desktopSession, isStoredSessionId, positiveNumber } from './store-summary-fields.mjs';
|
|
27
|
+
import { desktopSession, isStoredSessionId, positiveNumber, workspaceSummary } from './store-summary-fields.mjs';
|
|
28
28
|
import { readArchivedAgentResult } from './store-summary-archived-agent.mjs';
|
|
29
|
+
import { compactionArchivePath, readCompactionArchivePage } from './compaction-archive.mjs';
|
|
29
30
|
import {
|
|
30
31
|
readCanonicalSessionRecord as readCanonicalLifecycle,
|
|
31
32
|
offerStrictSessionRecord,
|
|
@@ -132,6 +133,8 @@ function isLeadVisibleRow(row) {
|
|
|
132
133
|
const sourceType = String(row.sourceType || '')
|
|
133
134
|
.trim()
|
|
134
135
|
.toLowerCase();
|
|
136
|
+
// Mirror listLeadSessions: one-shot `mixdog exec` runs stay out of Recent.
|
|
137
|
+
if (sourceType === 'exec') return false;
|
|
135
138
|
const sourceName = String(row.sourceName || '')
|
|
136
139
|
.trim()
|
|
137
140
|
.toLowerCase();
|
|
@@ -200,6 +203,7 @@ function normalizedRow(row, heartbeatAt = 0) {
|
|
|
200
203
|
clientHostPid: positiveNumber(row.clientHostPid, 0) || null,
|
|
201
204
|
cwd: row.cwd || '',
|
|
202
205
|
desktopSession: desktopSession(row.desktopSession, row.cwd),
|
|
206
|
+
...(workspaceSummary(row.workspace) ? { workspace: workspaceSummary(row.workspace) } : {}),
|
|
203
207
|
provider: row.provider || null,
|
|
204
208
|
model: row.model || null,
|
|
205
209
|
agentTag: row.agentTag || null,
|
|
@@ -426,11 +430,13 @@ function storedSessionMetadata(sessionId, session) {
|
|
|
426
430
|
visibility: sessionVisibility(session),
|
|
427
431
|
agentTag: session.agentTag || null,
|
|
428
432
|
cwd: session.cwd || '',
|
|
433
|
+
...(workspaceSummary(session.workspace) ? { workspace: workspaceSummary(session.workspace) } : {}),
|
|
429
434
|
provider: session.provider || null,
|
|
430
435
|
model: session.model || null,
|
|
431
436
|
presetName: session.presetName || session.profileId || null,
|
|
432
437
|
effort: session.effort || null,
|
|
433
438
|
fast: session.fast === true,
|
|
439
|
+
...(session.autoEffort === false ? { autoEffort: false } : {}),
|
|
434
440
|
modelParameters: session.modelParameters || null,
|
|
435
441
|
taskType: session.taskType || null,
|
|
436
442
|
permission: session.permission || null,
|
|
@@ -452,6 +458,9 @@ function storedTranscriptInputPaths(sessionId) {
|
|
|
452
458
|
return {
|
|
453
459
|
recordPath: join(dataDir(), 'sessions', `${sessionId}.json`),
|
|
454
460
|
checkpointPath: join(dataDir(), 'turn-checkpoints', `${sessionId}.json`),
|
|
461
|
+
// Written after the compaction commit that names it, so its identity
|
|
462
|
+
// joins the projection's.
|
|
463
|
+
archivePath: compactionArchivePath(join(dataDir(), 'sessions'), sessionId),
|
|
455
464
|
};
|
|
456
465
|
}
|
|
457
466
|
|
|
@@ -464,7 +473,7 @@ const STAMP_ABSENT_CODES = new Set(['ENOENT', 'ENOTDIR']);
|
|
|
464
473
|
export function storedSessionTranscriptStamp(id) {
|
|
465
474
|
const sessionId = String(id || '').trim();
|
|
466
475
|
if (!isStoredSessionId(sessionId)) return null;
|
|
467
|
-
const { recordPath, checkpointPath } = storedTranscriptInputPaths(sessionId);
|
|
476
|
+
const { recordPath, checkpointPath, archivePath } = storedTranscriptInputPaths(sessionId);
|
|
468
477
|
const settled = (path, absent) => {
|
|
469
478
|
let stamp;
|
|
470
479
|
try {
|
|
@@ -477,7 +486,8 @@ export function storedSessionTranscriptStamp(id) {
|
|
|
477
486
|
};
|
|
478
487
|
const record = settled(recordPath, null);
|
|
479
488
|
const checkpoint = record && settled(checkpointPath, 'absent');
|
|
480
|
-
|
|
489
|
+
const archive = checkpoint && settled(archivePath, 'absent');
|
|
490
|
+
return archive ? `${record}|${checkpoint}|${archive}` : null;
|
|
481
491
|
}
|
|
482
492
|
|
|
483
493
|
/** Read exactly one persisted session for a visible desktop pane. This never
|
|
@@ -489,8 +499,13 @@ export async function readStoredSessionTranscript(id, options = {}) {
|
|
|
489
499
|
if (!isStoredSessionId(sessionId)) return null;
|
|
490
500
|
// Same strict authority as the store, and the same fail-closed rule: an
|
|
491
501
|
// absent, unreadable, ambiguous or foreign record yields no transcript.
|
|
492
|
-
const { recordPath, checkpointPath } = storedTranscriptInputPaths(sessionId);
|
|
502
|
+
const { recordPath, checkpointPath, archivePath } = storedTranscriptInputPaths(sessionId);
|
|
493
503
|
if (options.metadataOnly === true) return readStoredSessionMetadata(recordPath, sessionId);
|
|
504
|
+
// One page of the display-only compaction archive (the live head loader
|
|
505
|
+
// pages it once a runtime's in-memory rows are exhausted).
|
|
506
|
+
if (options.compactionArchive && typeof options.compactionArchive === 'object') {
|
|
507
|
+
return readCompactionArchivePage(join(dataDir(), 'sessions'), sessionId, options.compactionArchive);
|
|
508
|
+
}
|
|
494
509
|
// The checkpoint sidecar shapes the projection as much as the record does,
|
|
495
510
|
// so its identity joins the cache fingerprint. Only a PROVABLY absent
|
|
496
511
|
// checkpoint skips recovery: an unreadable probe must not silently
|
|
@@ -502,6 +517,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
|
|
|
502
517
|
if (recordStat.state === PROBE_ABSENT) return readArchivedAgentResult(sessionId);
|
|
503
518
|
if (recordStat.state !== PROBE_PRESENT) return null;
|
|
504
519
|
const checkpoint = probePath(checkpointPath);
|
|
520
|
+
const archive = probePath(archivePath);
|
|
505
521
|
const requestedLimit = Number(options.transcriptItemLimit);
|
|
506
522
|
const itemLimit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : Number.POSITIVE_INFINITY;
|
|
507
523
|
let readState = PROBE_PRESENT;
|
|
@@ -525,6 +541,9 @@ export async function readStoredSessionTranscript(id, options = {}) {
|
|
|
525
541
|
checkpoint.size,
|
|
526
542
|
checkpoint.ino,
|
|
527
543
|
checkpoint.dev,
|
|
544
|
+
archive.state,
|
|
545
|
+
archive.mtimeMs,
|
|
546
|
+
archive.size,
|
|
528
547
|
].join(':'),
|
|
529
548
|
fileStat: recordStat,
|
|
530
549
|
loadText: async () => {
|
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
* Kept apart from the cache/fingerprint policy in store-summary-reader.mjs
|
|
8
8
|
* because this runs exactly once per distinct content and is the only part
|
|
9
9
|
* that lazily enters the heavier reconnect-recovery boundary (dynamic imports
|
|
10
|
-
*
|
|
10
|
+
* keep the cold summary path leaf-only).
|
|
11
11
|
*/
|
|
12
12
|
import { sessionContextMeasurement, contextMeasurementStats } from '../../../shared/context-measurement.mjs';
|
|
13
13
|
import { nextProjectionStamp } from './store-transcript-cache.mjs';
|
|
14
|
+
import { join } from 'node:path';
|
|
14
15
|
import { cleanValue, desktopSession, positiveNumber } from './store-summary-fields.mjs';
|
|
16
|
+
import { compactionArchiveRange, readCompactionArchivePage } from './compaction-archive.mjs';
|
|
17
|
+
import { dataDir } from './store-summary-locations.mjs';
|
|
15
18
|
|
|
16
19
|
/** The parse/recovery/projection body of a stored transcript read. Runs once
|
|
17
20
|
* per distinct content; its result is shared read-only through the cache. */
|
|
@@ -90,17 +93,31 @@ export async function projectStoredTranscript(sessionId, doc, options) {
|
|
|
90
93
|
// tail is identical either way.
|
|
91
94
|
const itemLimit = Number.isFinite(options.itemLimit) ? options.itemLimit : Number.POSITIVE_INFINITY;
|
|
92
95
|
const restored = restoreTranscriptItems(messages, { sessionId, itemLimit: itemLimit + 1 });
|
|
93
|
-
|
|
96
|
+
let transcriptHasOlder = restored.length > itemLimit;
|
|
97
|
+
let items = transcriptHasOlder ? restored.slice(-itemLimit) : restored;
|
|
98
|
+
// A paged window continues past the first stored message into the
|
|
99
|
+
// display-only compaction archive (newest segment first). A legacy whole
|
|
100
|
+
// read never receives it.
|
|
101
|
+
const archiveRange = Number.isFinite(itemLimit) && !transcriptHasOlder ? compactionArchiveRange(session) : null;
|
|
102
|
+
if (archiveRange) {
|
|
103
|
+
const archived = await readCompactionArchivePage(join(dataDir(), 'sessions'), sessionId, {
|
|
104
|
+
...archiveRange,
|
|
105
|
+
limit: itemLimit - restored.length,
|
|
106
|
+
});
|
|
107
|
+
if (archived.items.length) items = [...archived.items, ...restored];
|
|
108
|
+
transcriptHasOlder = archived.hasOlder;
|
|
109
|
+
}
|
|
94
110
|
return {
|
|
95
111
|
sessionId,
|
|
96
112
|
projectionStamp: nextProjectionStamp(),
|
|
97
113
|
...(options.includeMessages ? { messages } : {}),
|
|
98
|
-
items
|
|
114
|
+
items,
|
|
99
115
|
transcriptHasOlder,
|
|
100
116
|
provider: session.provider || '',
|
|
101
117
|
model: session.model || '',
|
|
102
118
|
effort: session.effort || '',
|
|
103
119
|
fast: session.fast === true,
|
|
120
|
+
...(session.autoEffort === false ? { autoEffort: false } : {}),
|
|
104
121
|
modelParameters: session.modelParameters || {},
|
|
105
122
|
cwd: session.cwd || '',
|
|
106
123
|
desktopSession: desktopSession(session.desktopSession, session.cwd),
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { contextMeasurementStats } from '../../../../shared/context-measurement.mjs';
|
|
8
8
|
import { toolResultText } from './tool-result-text.mjs';
|
|
9
|
+
import { restoredUserText } from './user-text.mjs';
|
|
9
10
|
import {
|
|
10
11
|
completionCardFromExecution,
|
|
11
12
|
parseModelVisibleCompletionWrapper,
|
|
@@ -48,12 +49,19 @@ function restoredTranscriptMetadata(message) {
|
|
|
48
49
|
if (completionValue && completionStatus && Number.isFinite(completionElapsedMs)) {
|
|
49
50
|
completion = { status: completionStatus, elapsedMs: Math.max(0, completionElapsedMs) };
|
|
50
51
|
if (typeof completionValue.verb === 'string' && completionValue.verb) completion.verb = completionValue.verb;
|
|
52
|
+
if (typeof completionValue.checkpointId === 'string' && completionValue.checkpointId) {
|
|
53
|
+
completion.checkpointId = completionValue.checkpointId;
|
|
54
|
+
}
|
|
51
55
|
}
|
|
52
56
|
return {
|
|
57
|
+
// The stable message id lets a live view map a compaction's dropped
|
|
58
|
+
// messages back to these rows.
|
|
59
|
+
...(typeof value.messageId === 'string' && value.messageId ? { messageId: value.messageId } : {}),
|
|
53
60
|
...(Number.isFinite(Number(value.at)) ? { at: Number(value.at) } : {}),
|
|
54
61
|
...transcriptRouteMetadataFields(value),
|
|
55
62
|
...(typeof value.sender === 'string' && value.sender ? { sender: value.sender } : {}),
|
|
56
63
|
...(typeof value.device === 'string' && value.device ? { device: value.device } : {}),
|
|
64
|
+
...(typeof value.checkpointId === 'string' && value.checkpointId ? { checkpointId: value.checkpointId } : {}),
|
|
57
65
|
...(completion ? { completion } : {}),
|
|
58
66
|
};
|
|
59
67
|
}
|
|
@@ -70,15 +78,16 @@ function restoredAssistantTranscriptItems(message, nextId) {
|
|
|
70
78
|
...completion,
|
|
71
79
|
...(completion.status === 'done' ? { finalAssistantId: items[0].id } : {}),
|
|
72
80
|
...(metadata.at ? { at: metadata.at } : {}),
|
|
81
|
+
...(metadata.messageId ? { messageId: metadata.messageId } : {}),
|
|
73
82
|
});
|
|
74
83
|
}
|
|
75
84
|
return items;
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
// Restored tool cards: stored assistant messages keep their (compacted)
|
|
79
|
-
// tool_calls and the follow-up role:'tool' results
|
|
80
|
-
//
|
|
81
|
-
//
|
|
88
|
+
// tool_calls and the follow-up role:'tool' results. Rebuild one transcript
|
|
89
|
+
// tool item per call and attach its result by tool_call_id, so a reopened
|
|
90
|
+
// session keeps its tool markers.
|
|
82
91
|
function restoredMessageToolCalls(message) {
|
|
83
92
|
if (Array.isArray(message?.tool_calls)) return message.tool_calls;
|
|
84
93
|
if (Array.isArray(message?.toolCalls)) return message.toolCalls;
|
|
@@ -112,7 +121,12 @@ function restoredToolCallItems(message, nextId, pendingByCallId) {
|
|
|
112
121
|
...(Number.isFinite(at) ? { at, startedAt: at, completedAt: at } : {}),
|
|
113
122
|
};
|
|
114
123
|
const callId = typeof call?.id === 'string' ? call.id : '';
|
|
115
|
-
|
|
124
|
+
// The call id ties the card to its call and result messages (a live view
|
|
125
|
+
// greys it when a compaction drops them).
|
|
126
|
+
if (callId) {
|
|
127
|
+
item.callId = callId;
|
|
128
|
+
pendingByCallId.set(callId, item);
|
|
129
|
+
}
|
|
116
130
|
items.push(item);
|
|
117
131
|
}
|
|
118
132
|
return items;
|
|
@@ -203,6 +217,9 @@ function buildRestoredAggregateItem(members) {
|
|
|
203
217
|
...(loadingTargets.length > 0 ? { loadingTargets } : {}),
|
|
204
218
|
},
|
|
205
219
|
aggregate: true,
|
|
220
|
+
...(members.some(({ item }) => item.callId)
|
|
221
|
+
? { callIds: members.map(({ item }) => item.callId).filter(Boolean) }
|
|
222
|
+
: {}),
|
|
206
223
|
categories: Object.fromEntries(header.categories),
|
|
207
224
|
doneCategories: aggregateDoneCategories(calls),
|
|
208
225
|
completedCount: calls.length,
|
|
@@ -219,8 +236,8 @@ function buildRestoredAggregateItem(members) {
|
|
|
219
236
|
|
|
220
237
|
// Restored transcripts must mirror the live turn's category merging: the live
|
|
221
238
|
// session runtime (turn.mjs) collapses consecutive same-bucket tool calls into one
|
|
222
|
-
// aggregate card,
|
|
223
|
-
//
|
|
239
|
+
// aggregate card, so a reopened session must not show them un-merged. Walk the
|
|
240
|
+
// restored items and merge adjacent
|
|
224
241
|
// tool cards whose aggregateBucketForCategory matches; any non-tool item
|
|
225
242
|
// (user/assistant/turndone) is a block boundary, same as the live seal rule.
|
|
226
243
|
// Runs of 1 keep the plain per-call card (its argument summary stays visible).
|
|
@@ -279,9 +296,11 @@ function restoredMessageItemUpperBound(message) {
|
|
|
279
296
|
function restoredNotificationToolItem(parsed, text, message, nextId, errorLabel) {
|
|
280
297
|
const label = parsed.label || 'notification';
|
|
281
298
|
const at = Number(message?.meta?.transcript?.at);
|
|
299
|
+
const messageId = message?.meta?.transcript?.messageId;
|
|
282
300
|
return {
|
|
283
301
|
kind: 'tool',
|
|
284
302
|
id: nextId(),
|
|
303
|
+
...(typeof messageId === 'string' && messageId ? { messageId } : {}),
|
|
285
304
|
name: parsed.name || 'agent',
|
|
286
305
|
args: parsed.args || {
|
|
287
306
|
type: label,
|
|
@@ -303,7 +322,7 @@ function restoredUserTranscriptItems(message, nextId) {
|
|
|
303
322
|
// skill bodies (meta:'skill'), hook/system reminders, and tag-wrapped context
|
|
304
323
|
// blocks must not restore as user bubbles in any client.
|
|
305
324
|
if (message?.meta === 'skill' || message?.meta === 'hook') return [];
|
|
306
|
-
const text = (
|
|
325
|
+
const text = restoredUserText(message?.content).trim();
|
|
307
326
|
// Persisted async-completion wrappers ("Async shell task ... finished. Result:
|
|
308
327
|
// > ...") are the only durable record of a background completion — the live
|
|
309
328
|
// Response card is an event-time push that does not survive a transcript
|
|
@@ -329,7 +348,8 @@ function restoredUserTranscriptItems(message, nextId) {
|
|
|
329
348
|
// step, but render it like a live cancel tail (◈ Cancelled) instead of a
|
|
330
349
|
// raw user bubble with bracketed internals.
|
|
331
350
|
if (isTranscriptCancelledStatusText(text)) {
|
|
332
|
-
|
|
351
|
+
const { messageId } = restoredTranscriptMetadata(message);
|
|
352
|
+
return [{ kind: 'turndone', id: nextId(), status: 'cancelled', elapsedMs: 0, ...(messageId ? { messageId } : {}) }];
|
|
333
353
|
}
|
|
334
354
|
const synthetic = parseSyntheticAgentMessage(text);
|
|
335
355
|
if (!synthetic) {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text a restored user bubble shows for a stored prompt, matching what the
|
|
3
|
+
* live bubble showed for it:
|
|
4
|
+
* - An image part is drawn by the `[Image: …]` metadata line the composer put
|
|
5
|
+
* before it; the part itself adds no raw `[image: image/png]` marker (a bare
|
|
6
|
+
* one gets a metadata line of its own, which renders as an image chip).
|
|
7
|
+
* - A desktop `<visualize_feedback>` block (the agent-facing pin/choice
|
|
8
|
+
* record) is shown as the lines the live bubble showed: `📌 N · label —
|
|
9
|
+
* note` per pin, one `☑ group: label, …` line for the picks and one
|
|
10
|
+
* `🎚 id: value (was old), …` line for tweaks (drawn with an icon). Intake
|
|
11
|
+
* moves a long block into the attachment store; it is read back from there.
|
|
12
|
+
*/
|
|
13
|
+
import { isAttachmentReference, readAttachmentTextWithin } from '../../../../attachments/store.mjs';
|
|
14
|
+
import { toolResultText } from './tool-result-text.mjs';
|
|
15
|
+
|
|
16
|
+
const IMAGE_METADATA_LINE = /\[Image(?::| source:) [^\]\r\n]+\]\s*$/;
|
|
17
|
+
const FEEDBACK_OPEN = '<visualize_feedback>';
|
|
18
|
+
const FEEDBACK_BLOCK = /<visualize_feedback>\n([\s\S]*?)\n?<\/visualize_feedback>/g;
|
|
19
|
+
// Feedback blocks are bounded (20 pins, trimmed HTML); a larger blob is a long
|
|
20
|
+
// prompt or paste, which restores as before without being read. The cap holds
|
|
21
|
+
// on the blob itself: the reference's claimed sizeBytes is not trusted.
|
|
22
|
+
const FEEDBACK_MAX_BYTES = 128 * 1024;
|
|
23
|
+
const QUOTED = '("(?:[^"\\\\]|\\\\.)*")';
|
|
24
|
+
const CHOICE_LINE = new RegExp(
|
|
25
|
+
`· group: (?:${QUOTED}|\\(none\\)) → choice: ${QUOTED}(?: \\(${QUOTED}\\))?$`
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function unquote(value) {
|
|
29
|
+
if (!value) return '';
|
|
30
|
+
try {
|
|
31
|
+
return String(JSON.parse(value));
|
|
32
|
+
} catch {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The live bubble's lines for one feedback block body. */
|
|
38
|
+
function feedbackDisplay(body) {
|
|
39
|
+
const pins = [];
|
|
40
|
+
const choices = [];
|
|
41
|
+
let pin = null;
|
|
42
|
+
let note = false;
|
|
43
|
+
let inChoices = false;
|
|
44
|
+
const tweakSets = [];
|
|
45
|
+
let tweaks = null;
|
|
46
|
+
for (const line of body.split('\n')) {
|
|
47
|
+
if (line.startsWith('tweaks (')) {
|
|
48
|
+
pin = null;
|
|
49
|
+
inChoices = false;
|
|
50
|
+
tweaks = null;
|
|
51
|
+
tweakSets.push([]);
|
|
52
|
+
tweaks = tweakSets.at(-1);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (tweaks) {
|
|
56
|
+
const change = /^ (.+?) \([^)]*\): (.*) \(was (.*)\)$/.exec(line);
|
|
57
|
+
if (change) tweaks.push(`${change[1]}: ${change[2]} (was ${change[3]})`);
|
|
58
|
+
else if (line.startsWith(' - tab: ')) {
|
|
59
|
+
tweaks = [];
|
|
60
|
+
tweakSets.push(tweaks);
|
|
61
|
+
} else tweaks = null;
|
|
62
|
+
if (tweaks) continue;
|
|
63
|
+
}
|
|
64
|
+
const head = /^pin (\d+) · (.*)$/.exec(line);
|
|
65
|
+
if (head) {
|
|
66
|
+
pin = { n: head[1], label: head[2], note: '' };
|
|
67
|
+
pins.push(pin);
|
|
68
|
+
note = false;
|
|
69
|
+
inChoices = false;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (line === 'choices:') {
|
|
73
|
+
pin = null;
|
|
74
|
+
inChoices = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (pin && line.startsWith(' note: ')) {
|
|
78
|
+
pin.note = line.slice(8);
|
|
79
|
+
note = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (pin && note && line.startsWith(' ')) {
|
|
83
|
+
pin.note += `\n${line.slice(4)}`;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
note = false;
|
|
87
|
+
const choice = inChoices ? CHOICE_LINE.exec(line) : null;
|
|
88
|
+
if (choice) {
|
|
89
|
+
choices.push({ group: unquote(choice[1]), choice: unquote(choice[2]), label: unquote(choice[3]) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const lines = pins.map((entry) => `📌 ${entry.n} · ${entry.label} — ${entry.note.replace(/\s+/g, ' ')}`);
|
|
93
|
+
if (choices.length) {
|
|
94
|
+
lines.push(
|
|
95
|
+
`☑ ${choices.map((entry) => (entry.group ? `${entry.group}: ` : '') + (entry.label || entry.choice)).join(', ')}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const changed = tweakSets.filter((set) => set.length).map((set) => set.join(', '));
|
|
99
|
+
if (changed.length) lines.push(`🎚 ${changed.join('; ')}`);
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function withVisualizeFeedbackDisplay(text) {
|
|
104
|
+
const value = String(text ?? '');
|
|
105
|
+
if (!value.includes(FEEDBACK_OPEN)) return value;
|
|
106
|
+
return value.replace(FEEDBACK_BLOCK, (_block, body) => feedbackDisplay(body));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A stored text reference's body when it is a feedback block, else ''. */
|
|
110
|
+
function referencedFeedbackText(part) {
|
|
111
|
+
if (part?.type !== 'text' || part.textKind) return '';
|
|
112
|
+
try {
|
|
113
|
+
const text = readAttachmentTextWithin(part, FEEDBACK_MAX_BYTES);
|
|
114
|
+
return text?.startsWith(FEEDBACK_OPEN) ? text : '';
|
|
115
|
+
} catch {
|
|
116
|
+
// A blob gone from the store restores as it always did: without its text.
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function restoredUserText(content) {
|
|
122
|
+
if (!Array.isArray(content)) {
|
|
123
|
+
return withVisualizeFeedbackDisplay(typeof content === 'string' ? content : toolResultText(content));
|
|
124
|
+
}
|
|
125
|
+
const texts = [];
|
|
126
|
+
let previous = '';
|
|
127
|
+
for (const part of content) {
|
|
128
|
+
let text;
|
|
129
|
+
if (part?.type === 'image' || part?.type === 'input_image') {
|
|
130
|
+
text = IMAGE_METADATA_LINE.test(previous) ? '' : `[Image: ${part.mimeType || part.mediaType || 'image'}]`;
|
|
131
|
+
} else if (isAttachmentReference(part)) {
|
|
132
|
+
text = referencedFeedbackText(part);
|
|
133
|
+
} else {
|
|
134
|
+
text = toolResultText([part]);
|
|
135
|
+
}
|
|
136
|
+
previous = text;
|
|
137
|
+
if (text) texts.push(text);
|
|
138
|
+
}
|
|
139
|
+
return withVisualizeFeedbackDisplay(texts.join('\n'));
|
|
140
|
+
}
|
|
@@ -5,10 +5,15 @@ import { isAbsolute, sep } from 'node:path';
|
|
|
5
5
|
import { canonicalCachePath, deleteReadRangeIndexForPath } from './read-range-index.mjs';
|
|
6
6
|
import { resolveAgainstCwd } from './path-utils.mjs';
|
|
7
7
|
import { fileVersion, sameFileVersion } from './file-version.mjs';
|
|
8
|
+
import { canCacheReadFile } from './read-cache-policy.mjs';
|
|
8
9
|
|
|
9
10
|
const RESULT_CACHE = new Map(); // key → { ts, value, paths, scopes, readSnapshotMeta, contentPrefixHash, bytes }
|
|
10
11
|
const RESULT_CACHE_INFLIGHT = new Map(); // key → { promise, controller, subscribers, settled }
|
|
11
12
|
const RESULT_CACHE_COMPUTE = new AsyncLocalStorage();
|
|
13
|
+
const READ_CACHE_PUBLICATION = new AsyncLocalStorage();
|
|
14
|
+
// Only active reads retain a slot. Invalidating an inactive path creates
|
|
15
|
+
// no history, and the final reader releases its path in a finally block.
|
|
16
|
+
const ACTIVE_READ_CACHE_PATHS = new Map();
|
|
12
17
|
const RESULT_CACHE_TTL_MS = 30_000;
|
|
13
18
|
const RESULT_CACHE_MAX_ENTRIES = 200;
|
|
14
19
|
// A byte budget from the bytes env var, else the megabytes one, else the
|
|
@@ -72,6 +77,37 @@ function cachePathsOverlap(a, b) {
|
|
|
72
77
|
return a.startsWith(b.endsWith(sep) ? b : `${b}${sep}`) || b.startsWith(a.endsWith(sep) ? a : `${a}${sep}`);
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
export async function runReadCachePublication(fullPaths, read) {
|
|
81
|
+
const paths = normalizeCacheMetaPaths(Array.isArray(fullPaths) ? fullPaths : [fullPaths]);
|
|
82
|
+
const leases = paths.map((path) => {
|
|
83
|
+
let slot = ACTIVE_READ_CACHE_PATHS.get(path);
|
|
84
|
+
if (!slot) {
|
|
85
|
+
slot = { generation: 0, references: 0 };
|
|
86
|
+
ACTIVE_READ_CACHE_PATHS.set(path, slot);
|
|
87
|
+
}
|
|
88
|
+
slot.references++;
|
|
89
|
+
return { path, slot, generation: slot.generation, released: false };
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
return await READ_CACHE_PUBLICATION.run(leases, read);
|
|
93
|
+
} finally {
|
|
94
|
+
for (const lease of leases) {
|
|
95
|
+
lease.released = true;
|
|
96
|
+
if (--lease.slot.references === 0) ACTIVE_READ_CACHE_PATHS.delete(lease.path);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function canPublishReadCache(targets = null) {
|
|
102
|
+
const leases = READ_CACHE_PUBLICATION.getStore();
|
|
103
|
+
if (!leases) return true;
|
|
104
|
+
const paths = normalizeCacheMetaPaths(Array.isArray(targets) ? targets : [targets]);
|
|
105
|
+
return leases.every((lease) => {
|
|
106
|
+
if (paths.length && !paths.some((path) => cachePathsOverlap(path, lease.path))) return true;
|
|
107
|
+
return !lease.released && lease.generation === lease.slot.generation;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
75
111
|
function cacheEntryOverlapsPaths(entry, affectedPaths) {
|
|
76
112
|
const entryPaths = Array.isArray(entry?.paths) ? entry.paths : [];
|
|
77
113
|
const entryScopes = Array.isArray(entry?.scopes) ? entry.scopes : [];
|
|
@@ -107,6 +143,11 @@ export function cacheSet(key, value, meta = {}) {
|
|
|
107
143
|
// A detached/aborted computation must never repopulate a newer cache.
|
|
108
144
|
const computation = RESULT_CACHE_COMPUTE.getStore();
|
|
109
145
|
if (computation?.invalidated || computation?.controller?.signal?.aborted) return;
|
|
146
|
+
const publicationPaths = [
|
|
147
|
+
...(Array.isArray(meta.paths) ? meta.paths : []),
|
|
148
|
+
...(Array.isArray(meta.scopes) ? meta.scopes : []),
|
|
149
|
+
];
|
|
150
|
+
if (!canPublishReadCache(publicationPaths)) return;
|
|
110
151
|
// Replace-in-place: clear old entry's byte accounting before write.
|
|
111
152
|
if (RESULT_CACHE.has(key)) resultCacheDelete(key);
|
|
112
153
|
const bytes = estimateResultBytes(value);
|
|
@@ -239,7 +280,7 @@ export async function runRawContentInFlight(fullPath, loader = fsPromises.readFi
|
|
|
239
280
|
// Buffered reads provide their observed version: a later read must not
|
|
240
281
|
// join older bytes merely because a watcher has not fired yet. Preserve
|
|
241
282
|
// the unversioned helper contract for callers that supply no stat.
|
|
242
|
-
if (stat !== undefined && !version) return await loader(fullPath);
|
|
283
|
+
if (stat !== undefined && (!version || !canCacheReadFile(version))) return await loader(fullPath);
|
|
243
284
|
const key = version ? JSON.stringify([path, version]) : path;
|
|
244
285
|
return await runPathInFlight(RAW_CONTENT_INFLIGHT, key, path, () => loader(fullPath));
|
|
245
286
|
}
|
|
@@ -260,6 +301,10 @@ function rawContentCacheDelete(key) {
|
|
|
260
301
|
export function rawContentCacheGet(fullPath, stat, now = Date.now()) {
|
|
261
302
|
if (!fullPath || !stat) return null;
|
|
262
303
|
const key = canonicalCachePath(fullPath);
|
|
304
|
+
if (!canCacheReadFile(stat)) {
|
|
305
|
+
rawContentCacheDelete(key);
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
263
308
|
const entry = RAW_CONTENT_CACHE.get(key);
|
|
264
309
|
if (!entry) return null;
|
|
265
310
|
if (now - entry.ts > RAW_CONTENT_CACHE_TTL_MS) {
|
|
@@ -276,9 +321,10 @@ export function rawContentCacheGet(fullPath, stat, now = Date.now()) {
|
|
|
276
321
|
}
|
|
277
322
|
|
|
278
323
|
export function rawContentCacheSet(fullPath, stat, rawBuf, now = Date.now()) {
|
|
324
|
+
if (!canPublishReadCache(fullPath)) return;
|
|
279
325
|
if (!fullPath || !Buffer.isBuffer(rawBuf)) return;
|
|
280
326
|
const version = fileVersion(stat);
|
|
281
|
-
if (!version || rawBuf.length !== version.size || rawBuf.length > RAW_CONTENT_CACHE_MAX_BYTES) return;
|
|
327
|
+
if (!version || !canCacheReadFile(version) || rawBuf.length !== version.size || rawBuf.length > RAW_CONTENT_CACHE_MAX_BYTES) return;
|
|
282
328
|
const key = canonicalCachePath(fullPath);
|
|
283
329
|
rawContentCacheDelete(key);
|
|
284
330
|
RAW_CONTENT_CACHE.set(key, {
|
|
@@ -450,6 +496,7 @@ function runExtraInvalidationListeners(affectedPaths = null) {
|
|
|
450
496
|
}
|
|
451
497
|
|
|
452
498
|
function cacheInvalidateAll() {
|
|
499
|
+
for (const slot of ACTIVE_READ_CACHE_PATHS.values()) slot.generation++;
|
|
453
500
|
for (const entry of RESULT_CACHE_INFLIGHT.values()) {
|
|
454
501
|
entry.invalidated = true;
|
|
455
502
|
}
|
|
@@ -470,6 +517,9 @@ function cacheInvalidatePaths(paths) {
|
|
|
470
517
|
cacheInvalidateAll();
|
|
471
518
|
return;
|
|
472
519
|
}
|
|
520
|
+
for (const [path, slot] of ACTIVE_READ_CACHE_PATHS) {
|
|
521
|
+
if (affectedPaths.some((affected) => cachePathsOverlap(path, affected))) slot.generation++;
|
|
522
|
+
}
|
|
473
523
|
for (const inFlight of [RAW_CONTENT_INFLIGHT, READ_ONLY_STAT_INFLIGHT]) {
|
|
474
524
|
for (const [key, entry] of inFlight) {
|
|
475
525
|
if (affectedPaths.some((affected) => cachePathsOverlap(entry.path, affected))) inFlight.delete(key);
|
|
@@ -6,7 +6,7 @@ import { tokenizeDirectArgv } from './shell-direct-exe.mjs';
|
|
|
6
6
|
import { GIT_DIFF_EXIT_SIGNAL_FLAGS } from './bash-tool/benign-exit.mjs';
|
|
7
7
|
import { withBuiltinPathLocks } from './path-locks.mjs';
|
|
8
8
|
import { withAdvisoryLocks } from './advisory-lock.mjs';
|
|
9
|
-
import { withGitRepoReadLock, withGitRepoWriteLock } from './git-repo-rw-lock.mjs';
|
|
9
|
+
import { gitRepoLockKey, withGitRepoReadLock, withGitRepoWriteLock } from './git-repo-rw-lock.mjs';
|
|
10
10
|
import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
11
11
|
import { drainCodeGraphCache } from '../code-graph-state.mjs';
|
|
12
12
|
import { ensureNativeSpawnServer, tryNativeSpawn } from '../lib/native-spawn-client.mjs';
|
|
@@ -460,22 +460,22 @@ function nearestGitDir(startDir) {
|
|
|
460
460
|
|
|
461
461
|
function cachedRepoRoot(key, plan) {
|
|
462
462
|
if (!key) return null;
|
|
463
|
-
const
|
|
464
|
-
if (!
|
|
463
|
+
const hit = repoRootCache.get(key);
|
|
464
|
+
if (!hit) return null;
|
|
465
465
|
const nearest = nearestGitDir(plan.cwd);
|
|
466
|
-
if (!nearest || resolve(nearest) !== resolve(root)) {
|
|
466
|
+
if (!nearest || resolve(nearest) !== resolve(hit.root)) {
|
|
467
467
|
repoRootCache.delete(key);
|
|
468
468
|
return null;
|
|
469
469
|
}
|
|
470
470
|
repoRootCache.delete(key);
|
|
471
|
-
repoRootCache.set(key,
|
|
472
|
-
return
|
|
471
|
+
repoRootCache.set(key, hit);
|
|
472
|
+
return hit;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
function rememberRepoRoot(key,
|
|
476
|
-
if (!key || !root) return;
|
|
475
|
+
function rememberRepoRoot(key, hit) {
|
|
476
|
+
if (!key || !hit?.root) return;
|
|
477
477
|
repoRootCache.delete(key);
|
|
478
|
-
repoRootCache.set(key,
|
|
478
|
+
repoRootCache.set(key, hit);
|
|
479
479
|
while (repoRootCache.size > REPO_ROOT_CACHE_MAX) {
|
|
480
480
|
const oldest = repoRootCache.keys().next().value;
|
|
481
481
|
if (oldest === undefined) break;
|
|
@@ -483,15 +483,22 @@ function rememberRepoRoot(key, root) {
|
|
|
483
483
|
}
|
|
484
484
|
}
|
|
485
485
|
|
|
486
|
+
// `lockKey` is the repository's canonical common git dir: the main checkout
|
|
487
|
+
// and its linked worktrees share one object store and ref namespace, so their
|
|
488
|
+
// git calls serialize on one repository lock (managed worktree mutations take
|
|
489
|
+
// the same key).
|
|
486
490
|
async function resolveRepo(plan, signal) {
|
|
487
491
|
const key = repoRootCacheKey(plan);
|
|
488
492
|
const hit = cachedRepoRoot(key, plan);
|
|
489
|
-
if (hit) return { root: hit, probe: null };
|
|
490
|
-
const result = await runGit(plan, ['rev-parse', '--show-toplevel'], {
|
|
493
|
+
if (hit) return { root: hit.root, lockKey: hit.lockKey, probe: null };
|
|
494
|
+
const result = await runGit(plan, ['rev-parse', '--show-toplevel', '--path-format=absolute', '--git-common-dir'], {
|
|
495
|
+
signal,
|
|
496
|
+
});
|
|
491
497
|
if (succeeded(result)) {
|
|
492
|
-
const root = cleanText(result.stdout);
|
|
493
|
-
|
|
494
|
-
|
|
498
|
+
const [root, commonDir] = cleanText(result.stdout).split('\n');
|
|
499
|
+
const resolved = { root, lockKey: gitRepoLockKey(commonDir || root) };
|
|
500
|
+
rememberRepoRoot(key, resolved);
|
|
501
|
+
return { ...resolved, probe: result };
|
|
495
502
|
}
|
|
496
503
|
// A bare repository (mirror clone, server-side repo) has no work tree, so
|
|
497
504
|
// `--show-toplevel` refuses; that is still a repository. Answering
|
|
@@ -499,12 +506,13 @@ async function resolveRepo(plan, signal) {
|
|
|
499
506
|
// to inspect objects it already had.
|
|
500
507
|
if (/must be run in a work tree/i.test(String(result.stderr || ''))) {
|
|
501
508
|
const bare = await runGit(plan, ['rev-parse', '--absolute-git-dir'], { signal });
|
|
502
|
-
if (!succeeded(bare)) return { root: null, probe: bare };
|
|
509
|
+
if (!succeeded(bare)) return { root: null, lockKey: null, probe: bare };
|
|
503
510
|
const root = cleanText(bare.stdout);
|
|
504
|
-
|
|
505
|
-
|
|
511
|
+
const resolved = { root, lockKey: gitRepoLockKey(root) };
|
|
512
|
+
rememberRepoRoot(key, resolved);
|
|
513
|
+
return { ...resolved, probe: bare };
|
|
506
514
|
}
|
|
507
|
-
return { root: null, probe: result };
|
|
515
|
+
return { root: null, lockKey: null, probe: result };
|
|
508
516
|
}
|
|
509
517
|
|
|
510
518
|
function localizeConfigPlan(plan) {
|
|
@@ -650,8 +658,9 @@ async function executeGitStage(input, workDir, options = {}) {
|
|
|
650
658
|
const limit = outputLimit(input?.output_limit, 50);
|
|
651
659
|
const signal = abortSignalOf(options);
|
|
652
660
|
const repo = snapshot.repo;
|
|
661
|
+
const { lockKey } = await resolveRepo({ ...snapshot.plan, cwd: repo }, signal);
|
|
653
662
|
return withGitRepoWriteLock(
|
|
654
|
-
repo,
|
|
663
|
+
lockKey || repo,
|
|
655
664
|
() =>
|
|
656
665
|
withGitMutationLocks(repo, async () => {
|
|
657
666
|
const current = await runStageableDiff(snapshot.plan, snapshot.argv, repo, signal);
|
|
@@ -722,7 +731,7 @@ async function executeSingleGitTool(input, workDir, options = {}) {
|
|
|
722
731
|
// ("git rev-parse exited 128") for a `git status` in a plain directory hid
|
|
723
732
|
// git's own "fatal: not a git repository" line behind a command the model
|
|
724
733
|
// never issued — and re-ran the probe just to build that message.
|
|
725
|
-
const { root: repo, probe } = await resolveRepo(plan, signal);
|
|
734
|
+
const { root: repo, lockKey, probe } = await resolveRepo(plan, signal);
|
|
726
735
|
if (!repo) {
|
|
727
736
|
const missing = probe ?? (await runGit(plan, ['rev-parse', '--show-toplevel'], { signal }));
|
|
728
737
|
return commandResult(plan, missing, limit);
|
|
@@ -732,7 +741,7 @@ async function executeSingleGitTool(input, workDir, options = {}) {
|
|
|
732
741
|
if (stageableRequest) prepared.argv.splice(1, 0, '--no-textconv', '--src-prefix=a/', '--dst-prefix=b/');
|
|
733
742
|
if (isReadOnly(plan)) {
|
|
734
743
|
return withGitRepoReadLock(
|
|
735
|
-
|
|
744
|
+
lockKey,
|
|
736
745
|
async () => {
|
|
737
746
|
const result = stageableRequest
|
|
738
747
|
? await runStageableDiff(plan, prepared.argv, repo, signal)
|
|
@@ -754,7 +763,7 @@ async function executeSingleGitTool(input, workDir, options = {}) {
|
|
|
754
763
|
);
|
|
755
764
|
}
|
|
756
765
|
return withGitRepoWriteLock(
|
|
757
|
-
|
|
766
|
+
lockKey,
|
|
758
767
|
() =>
|
|
759
768
|
withGitMutationLocks(repo, async () => {
|
|
760
769
|
const result = await runGit(plan, prepared.argv, { signal });
|
|
@@ -896,4 +905,5 @@ export const _gitCommandInternals = {
|
|
|
896
905
|
localizeConfigPlan,
|
|
897
906
|
parseCommand,
|
|
898
907
|
prepare,
|
|
908
|
+
resolveRepo,
|
|
899
909
|
};
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pathKey } from '../../../../shared/worktrees/paths.mjs';
|
|
2
2
|
|
|
3
3
|
const states = new Map();
|
|
4
4
|
|
|
5
|
+
// Callers key by the canonical `git rev-parse --git-common-dir`, so the main
|
|
6
|
+
// checkout and every linked worktree of one repository share one lock. The key
|
|
7
|
+
// is the real path (8.3 names, symlinked parents) and case-insensitive on
|
|
8
|
+
// Windows, so two spellings of one directory never hold separate locks.
|
|
9
|
+
export function gitRepoLockKey(repo) {
|
|
10
|
+
return pathKey(String(repo || '.'));
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
function keyFor(repo) {
|
|
6
|
-
|
|
7
|
-
return process.platform === 'win32' ? key.toLowerCase() : key;
|
|
14
|
+
return gitRepoLockKey(repo);
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
function stateFor(key) {
|