mixdog 1.1.2 → 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 -1
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- 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/help.mjs +2 -0
- 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 +10 -3
- 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/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/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/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/attachments/store.mjs +38 -0
- 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/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 +7 -0
- 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/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/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 +4 -0
- 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 +20 -7
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- 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/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-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/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/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-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- 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 +11 -0
- 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 +16 -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/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +17 -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/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
|
@@ -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) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The bounded cache validator can certify the whole file only within this
|
|
2
|
+
// byte budget. Larger reads must not reuse metadata-only or prefix-only
|
|
3
|
+
// content/line-position evidence: equal stats can hide edits past the prefix.
|
|
4
|
+
export const READ_PREFIX_HASH_BYTES = 65536;
|
|
5
|
+
|
|
6
|
+
export function canCacheReadFile(stat) {
|
|
7
|
+
return Number.isFinite(stat?.size) && stat.size >= 0 && stat.size <= READ_PREFIX_HASH_BYTES;
|
|
8
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, open, readFile, readdir, stat, unlink } from 'node:fs/promises';
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import { fileVersion, sameFileVersion } from './file-version.mjs';
|
|
4
|
+
import { canCacheReadFile, READ_PREFIX_HASH_BYTES } from './read-cache-policy.mjs';
|
|
4
5
|
import { hashText as sha256Hex } from './hash-utils.mjs';
|
|
5
6
|
import { join, normalize, resolve } from 'node:path';
|
|
6
7
|
import { getPluginData } from '../../config.mjs';
|
|
@@ -83,7 +84,7 @@ function readRangeIndexMatches(row, fullPath, version) {
|
|
|
83
84
|
// `prefixHash` they persist into the anchor index.
|
|
84
85
|
async function computePrefixHashForIndex(fullPath, st, handle = null, prefixBuffer = null) {
|
|
85
86
|
try {
|
|
86
|
-
const cap = Math.min(Number(st?.size) || 0,
|
|
87
|
+
const cap = Math.min(Number(st?.size) || 0, READ_PREFIX_HASH_BYTES);
|
|
87
88
|
if (cap <= 0) return '';
|
|
88
89
|
if (Buffer.isBuffer(prefixBuffer) && prefixBuffer.length === cap) {
|
|
89
90
|
return sha256Hex(prefixBuffer);
|
|
@@ -269,6 +270,8 @@ function freshReadRangeIndex(fullPath, version = null) {
|
|
|
269
270
|
export async function getReadRangeIndex(fullPath, st, handle = null, prefixBuffer = null) {
|
|
270
271
|
const key = canonicalCachePath(fullPath);
|
|
271
272
|
let slot = READ_RANGE_INDEX_CACHE.get(key);
|
|
273
|
+
// Reserve ownership before awaiting the real stat even when the hint is
|
|
274
|
+
// large: a now-small file must not renew a lookup invalidated during IO.
|
|
272
275
|
if (!slot) slot = refreshReadRangeIndexSlot(key, freshReadRangeIndex(fullPath));
|
|
273
276
|
const hint = fileVersion(st);
|
|
274
277
|
const readStat = () => (handle ? handle.stat() : stat(fullPath));
|
|
@@ -280,8 +283,18 @@ export async function getReadRangeIndex(fullPath, st, handle = null, prefixBuffe
|
|
|
280
283
|
} catch {
|
|
281
284
|
return null;
|
|
282
285
|
}
|
|
283
|
-
if (!version
|
|
286
|
+
if (!version) return null;
|
|
284
287
|
version = Object.freeze(version);
|
|
288
|
+
// Metadata and a bounded prefix cannot certify suffix line positions.
|
|
289
|
+
// Large scans own only this call's handle identity and byte-zero draft:
|
|
290
|
+
// never load, hash-validate, or publish cross-read anchors/counts.
|
|
291
|
+
if (!canCacheReadFile(version)) {
|
|
292
|
+
if (slot && READ_RANGE_INDEX_CACHE.get(key) === slot) dropReadRangeIndexSlot(key);
|
|
293
|
+
const draft = freshReadRangeIndex(fullPath, version);
|
|
294
|
+
READ_RANGE_INDEX_DRAFTS.set(draft, { slot: null, version, fullPath });
|
|
295
|
+
return draft;
|
|
296
|
+
}
|
|
297
|
+
if (READ_RANGE_INDEX_CACHE.get(key) !== slot) return null;
|
|
285
298
|
if (!slot.fileVersion) slot.fileVersion = version;
|
|
286
299
|
if (!sameFileVersion(slot.fileVersion, version)) {
|
|
287
300
|
dropReadRangeIndexSlot(key);
|
|
@@ -314,27 +327,33 @@ export async function getReadRangeIndex(fullPath, st, handle = null, prefixBuffe
|
|
|
314
327
|
anchors: new Map(source.anchors),
|
|
315
328
|
...(Number.isFinite(source.totalLines) ? { totalLines: source.totalLines } : {}),
|
|
316
329
|
};
|
|
317
|
-
READ_RANGE_INDEX_DRAFTS.set(draft, { slot, version });
|
|
330
|
+
READ_RANGE_INDEX_DRAFTS.set(draft, { slot, version, fullPath });
|
|
318
331
|
return draft;
|
|
319
332
|
}
|
|
320
333
|
|
|
321
|
-
// Successful readers publish once, before closing their handle.
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
334
|
+
// Successful small-file readers publish once, before closing their handle.
|
|
335
|
+
// Large or detached scans remain private, but terminal validation can still
|
|
336
|
+
// certify their producer identity via onValidated. Post-stat failures never
|
|
337
|
+
// authorize relabelling bytes with a later version.
|
|
338
|
+
export async function publishReadRangeIndex(index, handle, signal = null, beforePublish = null, onValidated = null) {
|
|
325
339
|
const owned = READ_RANGE_INDEX_DRAFTS.get(index);
|
|
326
340
|
if (!owned) return false;
|
|
327
341
|
READ_RANGE_INDEX_DRAFTS.delete(index);
|
|
328
|
-
const { slot, version } = owned;
|
|
329
|
-
const key = canonicalCachePath(
|
|
330
|
-
if (signal?.aborted
|
|
342
|
+
const { slot, version, fullPath } = owned;
|
|
343
|
+
const key = canonicalCachePath(fullPath);
|
|
344
|
+
if (signal?.aborted) return false;
|
|
345
|
+
// Detached cache-only publishers must not wait on validation IO. A streamed
|
|
346
|
+
// result requesting producer certification still owes its terminal stats.
|
|
347
|
+
if (slot && READ_RANGE_INDEX_CACHE.get(key) !== slot && !onValidated) return false;
|
|
331
348
|
try {
|
|
332
|
-
if (!sameFileVersion(version, await handle.stat()) || !sameFileVersion(version, await stat(
|
|
349
|
+
if (!sameFileVersion(version, await handle.stat()) || !sameFileVersion(version, await stat(fullPath)))
|
|
333
350
|
return false;
|
|
334
351
|
} catch {
|
|
335
352
|
return false;
|
|
336
353
|
}
|
|
337
|
-
if (signal?.aborted
|
|
354
|
+
if (signal?.aborted) return false;
|
|
355
|
+
onValidated?.(fileVersion(version));
|
|
356
|
+
if (!slot || READ_RANGE_INDEX_CACHE.get(key) !== slot) return false;
|
|
338
357
|
if (slot.prefixHash && index.prefixHash && slot.prefixHash !== index.prefixHash) return false;
|
|
339
358
|
// The reader's synchronous deadline/cancellation decision belongs after all
|
|
340
359
|
// awaited validation and outside its best-effort stat catch. Once committed,
|