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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep coalesced child observations private until the caller's exact windows
|
|
3
|
+
* have been restored. A fresh child may outlive the batch's original cache
|
|
4
|
+
* generation; retain that child's lease, never renew the whole batch.
|
|
5
|
+
*/
|
|
6
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
7
|
+
import { canPublishReadCache, runReadCachePublication } from '../cache-layers.mjs';
|
|
8
|
+
import { fileVersion, sameFileVersion } from '../file-version.mjs';
|
|
9
|
+
import { mergeReadRanges } from '../read-ranges.mjs';
|
|
10
|
+
|
|
11
|
+
// Streamed reads have numbered rows but no total-line footer. Prefer the
|
|
12
|
+
// rows themselves so smart-elided gaps never become delivered evidence.
|
|
13
|
+
export function renderedBatchReadRanges(body) {
|
|
14
|
+
if (typeof body !== 'string') return [];
|
|
15
|
+
const rows = [...body.matchAll(/^(\d+)[\t│→]/gm)]
|
|
16
|
+
.map((match) => Number(match[1]))
|
|
17
|
+
.filter((line) => Number.isSafeInteger(line) && line > 0)
|
|
18
|
+
.map((line) => ({ startLine: line, endLine: line }));
|
|
19
|
+
if (rows.length) return mergeReadRanges(rows);
|
|
20
|
+
const footer = /\[lines\s+(\d+)-(\d+)\s+of\s+\d+/.exec(body);
|
|
21
|
+
return footer ? mergeReadRanges([{ startLine: Number(footer[1]), endLine: Number(footer[2]) }]) : [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hasRenderedLines(body) {
|
|
25
|
+
return renderedBatchReadRanges(body).length > 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function runBatchReadSnapshotPublication(executeChildBuiltinTool, helpers, run) {
|
|
29
|
+
const tasks = [];
|
|
30
|
+
const records = [];
|
|
31
|
+
const corrections = new Map();
|
|
32
|
+
let closed = false;
|
|
33
|
+
|
|
34
|
+
function capture(task, fullPath, st, scope, meta) {
|
|
35
|
+
if (closed) return;
|
|
36
|
+
const identity = fileVersion(st);
|
|
37
|
+
const record = {
|
|
38
|
+
fullPath,
|
|
39
|
+
scope: scope ?? null,
|
|
40
|
+
identity,
|
|
41
|
+
ranges: mergeReadRanges(meta?.ranges),
|
|
42
|
+
isCurrent: () => false,
|
|
43
|
+
publish: () => {},
|
|
44
|
+
release: () => {},
|
|
45
|
+
done: Promise.resolve(),
|
|
46
|
+
};
|
|
47
|
+
task.records.push(record);
|
|
48
|
+
records.push(record);
|
|
49
|
+
// Acquiring a new lease must not rehabilitate an already stale producer.
|
|
50
|
+
if (!identity || !canPublishReadCache(fullPath)) return;
|
|
51
|
+
const decision = Promise.withResolvers();
|
|
52
|
+
record.publish = (replacement = meta) => decision.resolve(replacement);
|
|
53
|
+
record.release = () => decision.resolve(null);
|
|
54
|
+
record.done = runReadCachePublication(fullPath, async () => {
|
|
55
|
+
record.isCurrent = AsyncLocalStorage.bind(() => canPublishReadCache(fullPath));
|
|
56
|
+
const selected = await decision.promise;
|
|
57
|
+
if (selected) helpers._recordReadSnapshot(fullPath, identity, scope, selected);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const executeChild = async (name, args, workDir, options = {}) => {
|
|
62
|
+
if (name !== 'read' || (args.mode && args.mode !== 'full')) {
|
|
63
|
+
return executeChildBuiltinTool(name, args, workDir, options);
|
|
64
|
+
}
|
|
65
|
+
const task = {
|
|
66
|
+
entry: args,
|
|
67
|
+
fullPath: helpers.resolveAgainstCwd(args.path, workDir),
|
|
68
|
+
records: [],
|
|
69
|
+
body: null,
|
|
70
|
+
};
|
|
71
|
+
tasks.push(task);
|
|
72
|
+
task.body = await executeChildBuiltinTool(name, args, workDir, {
|
|
73
|
+
...options,
|
|
74
|
+
_readSnapshotRecorder: (...snapshot) => capture(task, ...snapshot),
|
|
75
|
+
});
|
|
76
|
+
return task.body;
|
|
77
|
+
};
|
|
78
|
+
const snapshotHelpers = {
|
|
79
|
+
...helpers,
|
|
80
|
+
_filterReadSnapshotRanges: (entry, fullPath, scope, ranges) => {
|
|
81
|
+
const task = tasks.find((task) => task.entry === entry);
|
|
82
|
+
const observation = task?.records.findLast(
|
|
83
|
+
(record) => record.fullPath === fullPath && record.scope === (scope ?? null)
|
|
84
|
+
);
|
|
85
|
+
// A numbered row may end halfway through its content at the byte cap.
|
|
86
|
+
// Only this slot's producer can certify complete lines; another read of
|
|
87
|
+
// the same path must not authorize hashing this slot's partial text.
|
|
88
|
+
return mergeReadRanges(ranges.flatMap((range) => (observation?.ranges ?? []).map((complete) => ({
|
|
89
|
+
startLine: Math.max(range.startLine, complete.startLine),
|
|
90
|
+
endLine: Math.min(range.endLine, complete.endLine),
|
|
91
|
+
}))));
|
|
92
|
+
},
|
|
93
|
+
_recordReadSnapshot: (fullPath, _st, scope, meta) => {
|
|
94
|
+
corrections.set(fullPath, { scope: scope ?? null, meta });
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const result = await run(executeChild, snapshotHelpers);
|
|
100
|
+
// reject_partial refuses the entire payload: staged bodies were not
|
|
101
|
+
// delivered, so none of their snapshots may become session evidence.
|
|
102
|
+
if (result?.isError === true || (typeof result === 'string' && helpers.classifyResultKind(result) === 'error')) {
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
for (const [fullPath, correction] of corrections) {
|
|
106
|
+
const delivered = tasks.filter((task) => task.fullPath === fullPath && hasRenderedLines(task.body));
|
|
107
|
+
const observations = delivered.map((task) => task.records.findLast(
|
|
108
|
+
(record) => record.fullPath === fullPath && record.scope === correction.scope
|
|
109
|
+
));
|
|
110
|
+
// Never combine different versions or bless stale/missing observations.
|
|
111
|
+
// The eventual recorder rechecks the retained lease after all awaits.
|
|
112
|
+
if (observations.length && observations.every((record) =>
|
|
113
|
+
record?.isCurrent() && sameFileVersion(record.identity, observations[0].identity)
|
|
114
|
+
)) {
|
|
115
|
+
observations[0].publish(correction.meta);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Rich media/binary results have no line slices. Preserve their original
|
|
119
|
+
// evidence, still under the producer's lease rather than the batch's.
|
|
120
|
+
for (const task of tasks) {
|
|
121
|
+
if (hasRenderedLines(task.body) || task.body?.isError === true ||
|
|
122
|
+
(typeof task.body === 'string' && helpers.classifyResultKind(task.body) === 'error')) continue;
|
|
123
|
+
for (const record of task.records) {
|
|
124
|
+
if (!corrections.has(record.fullPath)) record.publish();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const record of records) record.release();
|
|
128
|
+
await Promise.all(records.map((record) => record.done));
|
|
129
|
+
return result;
|
|
130
|
+
} finally {
|
|
131
|
+
// Promise.all in the dispatcher may reject while other children remain
|
|
132
|
+
// active. Late captures then become no-ops and cannot leak a held lease.
|
|
133
|
+
closed = true;
|
|
134
|
+
for (const record of records) record.release();
|
|
135
|
+
await Promise.allSettled(records.map((record) => record.done));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { readFile } from 'node:fs/promises';
|
|
8
8
|
import { rangeTextOf } from '../snapshot-helpers.mjs';
|
|
9
|
+
import { renderedBatchReadRanges } from './batch-publication.mjs';
|
|
9
10
|
|
|
10
11
|
// Coalesced batch reads fetch the union window from disk; every caller slot
|
|
11
12
|
// must be sliced back to its original request window (_orig*), not the
|
|
@@ -32,22 +33,22 @@ export function restoreCallerSlots(results, origEntries, entryMap, options, { sl
|
|
|
32
33
|
function deliveredRangesByPath(
|
|
33
34
|
orderedResults,
|
|
34
35
|
workDir,
|
|
35
|
-
|
|
36
|
+
readStateScope,
|
|
37
|
+
{ classifyResultKind, resolveAgainstCwd, _rangeHashesFromRenderedReadText, _filterReadSnapshotRanges }
|
|
36
38
|
) {
|
|
37
39
|
const exactRangesByPath = new Map();
|
|
38
40
|
const rangeHashesByPath = new Map();
|
|
39
41
|
for (const r of orderedResults) {
|
|
40
42
|
if (r?.mode !== 'full' || classifyResultKind(String(r.body || '')) === 'error') continue;
|
|
41
|
-
const m = String(r.body || '').match(/\[lines\s+(\d+)-(\d+)\s+of\s+(\d+)/);
|
|
42
|
-
if (!m) continue;
|
|
43
|
-
const startLine = Number(m[1]);
|
|
44
|
-
const endLine = Number(m[2]);
|
|
45
|
-
if (!Number.isFinite(startLine) || !Number.isFinite(endLine)) continue;
|
|
46
43
|
const fullPath = resolveAgainstCwd(r.path, workDir);
|
|
44
|
+
const renderedRanges = renderedBatchReadRanges(r.body);
|
|
45
|
+
const ranges = _filterReadSnapshotRanges
|
|
46
|
+
? _filterReadSnapshotRanges(r.readEntry, fullPath, readStateScope, renderedRanges)
|
|
47
|
+
: renderedRanges;
|
|
48
|
+
if (ranges.length === 0) continue;
|
|
47
49
|
if (!exactRangesByPath.has(fullPath)) exactRangesByPath.set(fullPath, []);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const renderedHashes = _rangeHashesFromRenderedReadText(r.body, [range]);
|
|
50
|
+
exactRangesByPath.get(fullPath).push(...ranges);
|
|
51
|
+
const renderedHashes = _rangeHashesFromRenderedReadText(r.body, ranges);
|
|
51
52
|
if (renderedHashes.length > 0) {
|
|
52
53
|
if (!rangeHashesByPath.has(fullPath)) rangeHashesByPath.set(fullPath, []);
|
|
53
54
|
rangeHashesByPath.get(fullPath).push(...renderedHashes);
|
|
@@ -68,7 +69,7 @@ async function rawRangeHashes(fullPath, mergedRanges, { _hashText }) {
|
|
|
68
69
|
|
|
69
70
|
export async function recordSlicedBatchSnapshots(orderedResults, workDir, readStateScope, helpers) {
|
|
70
71
|
const { _mergeReadRanges, _recordReadSnapshot } = helpers;
|
|
71
|
-
const { exactRangesByPath, rangeHashesByPath } = deliveredRangesByPath(orderedResults, workDir, helpers);
|
|
72
|
+
const { exactRangesByPath, rangeHashesByPath } = deliveredRangesByPath(orderedResults, workDir, readStateScope, helpers);
|
|
72
73
|
for (const [fullPath, ranges] of exactRangesByPath) {
|
|
73
74
|
const mergedRanges = _mergeReadRanges(ranges);
|
|
74
75
|
let rangeHashes = rangeHashesByPath.get(fullPath) || [];
|
|
@@ -18,6 +18,8 @@ import { normalizeObjectBatchArgs, batchEntriesFromArgs, primaryReadIndexes } fr
|
|
|
18
18
|
import { dispatchBatchReads } from './read-tool/batch-dispatch.mjs';
|
|
19
19
|
import { restoreCallerSlots, recordSlicedBatchSnapshots } from './read-tool/batch-snapshot.mjs';
|
|
20
20
|
import { renderBatchResults } from './read-tool/batch-render.mjs';
|
|
21
|
+
import { runBatchReadSnapshotPublication } from './read-tool/batch-publication.mjs';
|
|
22
|
+
import { runReadCachePublication } from './cache-layers.mjs';
|
|
21
23
|
|
|
22
24
|
function hasLineCoordinate(path) {
|
|
23
25
|
return typeof path === 'string' && /(?:#L\d+|:\d+(?:-\d+)?(?::|$))/i.test(path);
|
|
@@ -54,17 +56,25 @@ async function executeBatchRead(args, workDir, readStateScope, executeChildBuilt
|
|
|
54
56
|
const origEntries = Array.isArray(args._readsOrigEntries) ? args._readsOrigEntries : null;
|
|
55
57
|
const entryMap = Array.isArray(args._readsEntryToDeduped) ? args._readsEntryToDeduped : null;
|
|
56
58
|
const readIndexFor = primaryReadIndexes(entries, overrides, (p) => helpers.resolveAgainstCwd(p, workDir));
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
const run = async (executeChild = executeChildBuiltinTool, snapshotHelpers = helpers) => {
|
|
60
|
+
const results = await dispatchBatchReads({
|
|
61
|
+
entries,
|
|
62
|
+
readIndexFor,
|
|
63
|
+
workDir,
|
|
64
|
+
options,
|
|
65
|
+
helpers,
|
|
66
|
+
executeChildBuiltinTool: executeChild,
|
|
67
|
+
});
|
|
68
|
+
const orderedResults = origEntries ? restoreCallerSlots(results, origEntries, entryMap, options, helpers) : results;
|
|
69
|
+
if (origEntries) await recordSlicedBatchSnapshots(orderedResults, workDir, readStateScope, snapshotHelpers);
|
|
70
|
+
return renderBatchResults(orderedResults, args, helpers);
|
|
71
|
+
};
|
|
72
|
+
if (!origEntries) return run();
|
|
73
|
+
// Keep the original batch lease for glance/media paths outside the scalar
|
|
74
|
+
// pipeline. Sliced evidence uses the producing child's retained lease, so
|
|
75
|
+
// a hash rejection never requires renewing this potentially stale parent.
|
|
76
|
+
const paths = entries.filter((entry) => entry?.path).map((entry) => helpers.resolveAgainstCwd(entry.path, workDir));
|
|
77
|
+
return runReadCachePublication(paths, () => runBatchReadSnapshotPublication(executeChildBuiltinTool, helpers, run));
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
// Glance modes served by their own child tool: the tool name and the args it
|
|
@@ -102,7 +102,7 @@ function findRepositoryRoot(startDirectory, { maxDepth = 64 } = {}) {
|
|
|
102
102
|
const marker = pathJoin(current, '.git');
|
|
103
103
|
try {
|
|
104
104
|
const stats = statSync(marker);
|
|
105
|
-
return { root: current, gitDirectory: stats.isDirectory() ? marker :
|
|
105
|
+
return { root: current, gitDirectory: stats.isDirectory() ? marker : _linkedGitDirectory(marker, current) };
|
|
106
106
|
} catch {}
|
|
107
107
|
const parent = pathDirname(current);
|
|
108
108
|
if (parent === current) break;
|
|
@@ -111,6 +111,17 @@ function findRepositoryRoot(startDirectory, { maxDepth = 64 } = {}) {
|
|
|
111
111
|
return null;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// A linked worktree's `.git` file names its own git directory (`gitdir: …`),
|
|
115
|
+
// which holds that worktree's HEAD.
|
|
116
|
+
function _linkedGitDirectory(markerFile, root) {
|
|
117
|
+
try {
|
|
118
|
+
const target = /^gitdir:\s*(.+)$/m.exec(readFileSync(markerFile, 'utf8'))?.[1]?.trim();
|
|
119
|
+
return target ? pathResolveAbsolute(root, target) : null;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
function _headBranch(gitDirectory) {
|
|
115
126
|
if (!gitDirectory) return null;
|
|
116
127
|
try {
|
|
@@ -180,7 +180,10 @@ async function waitForShellTask(task, taskId, args, options) {
|
|
|
180
180
|
return interruptedByUser;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
// Outcome of a shell cancel: `outcome` is 'cancelled' only when the process is
|
|
184
|
+
// confirmed gone (or never had one), 'unconfirmed' when the kill was delivered
|
|
185
|
+
// but the exit could not be confirmed, 'not-found' for an unknown task.
|
|
186
|
+
async function cancelShellTaskOutcome(task, taskId, options) {
|
|
184
187
|
// A tracked task with no native job is still cancellable: the
|
|
185
188
|
// registry's own cancel hook owns it (descendants that outlived
|
|
186
189
|
// their shell). Remember that it WAS live so the outcome is
|
|
@@ -196,11 +199,14 @@ async function cancelShellTask(task, taskId, options) {
|
|
|
196
199
|
if (confirmed && confirmed.status === 'running') {
|
|
197
200
|
// Watcher and notify context stay armed: the eventual exit
|
|
198
201
|
// must still reach the owner.
|
|
199
|
-
return
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
return {
|
|
203
|
+
outcome: 'unconfirmed',
|
|
204
|
+
text: [
|
|
205
|
+
'status: cancel-unconfirmed',
|
|
206
|
+
`task_id: ${taskId}`,
|
|
207
|
+
'the cancellation was delivered but the process has not exited yet. It stays tracked and its completion is still delivered; re-run task cancel or task read to check again.',
|
|
208
|
+
].join('\n'),
|
|
209
|
+
};
|
|
204
210
|
}
|
|
205
211
|
if (!confirmed) {
|
|
206
212
|
// No terminal event and no observable task: absence of
|
|
@@ -209,19 +215,56 @@ async function cancelShellTask(task, taskId, options) {
|
|
|
209
215
|
cancelBackgroundShellJobWatch(taskId);
|
|
210
216
|
clearShellJobNotifyCtx(taskId);
|
|
211
217
|
cancelBackgroundTask(taskId, 'cancelled by task control; termination unconfirmed');
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
return {
|
|
219
|
+
outcome: 'unconfirmed',
|
|
220
|
+
text: [
|
|
221
|
+
'status: cancel-unconfirmed',
|
|
222
|
+
`task_id: ${taskId}`,
|
|
223
|
+
'the cancellation was delivered but this task is no longer observable, so its termination could not be confirmed. Verify the process directly if it must be gone.',
|
|
224
|
+
].join('\n'),
|
|
225
|
+
};
|
|
217
226
|
}
|
|
218
227
|
}
|
|
219
228
|
cancelBackgroundShellJobWatch(taskId);
|
|
220
229
|
clearShellJobNotifyCtx(taskId);
|
|
221
230
|
cancelBackgroundTask(taskId, 'cancelled by task control');
|
|
222
231
|
return job || wasRunning
|
|
223
|
-
?
|
|
224
|
-
|
|
232
|
+
? {
|
|
233
|
+
outcome: 'cancelled',
|
|
234
|
+
text: renderTaskCancelSuccess(taskId, getBackgroundTask(taskId, { context: options }) || task),
|
|
235
|
+
}
|
|
236
|
+
: { outcome: 'not-found', text: buildJobNotFoundMessage(taskId) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function cancelShellTask(task, taskId, options) {
|
|
240
|
+
return (await cancelShellTaskOutcome(task, taskId, options)).text;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Stop one BACKGROUND shell job of the caller's session (desktop stop button).
|
|
245
|
+
* The same cancel the `task` tool runs, with a structured answer: `stopped` is
|
|
246
|
+
* true only when the job was actually cancelled. Only promoted/async jobs hold
|
|
247
|
+
* a background task, so a foreground command can never match; the session
|
|
248
|
+
* scope is the caller's, so another session's job reads as unknown.
|
|
249
|
+
*/
|
|
250
|
+
export async function stopBackgroundShellJob(taskId, options = {}) {
|
|
251
|
+
const id = typeof taskId === 'string' ? taskId.trim() : '';
|
|
252
|
+
if (!id) return { stopped: false, error: 'task_id is required' };
|
|
253
|
+
const task = getBackgroundTask(id, { context: options });
|
|
254
|
+
if (!task || task.surface !== 'shell') return { stopped: false, error: `task not found: ${id}` };
|
|
255
|
+
const job = peekShellJob(id);
|
|
256
|
+
const status = job ? job.status : task.status;
|
|
257
|
+
if (status !== 'running') return { stopped: false, error: `task already ${status}: ${id}` };
|
|
258
|
+
try {
|
|
259
|
+
const { outcome, text } = await cancelShellTaskOutcome(task, id, options);
|
|
260
|
+
if (outcome === 'cancelled') return { stopped: true };
|
|
261
|
+
return {
|
|
262
|
+
stopped: false,
|
|
263
|
+
error: outcome === 'unconfirmed' ? `cancellation not confirmed: ${id}` : text.replace(/^Error:\s*/, ''),
|
|
264
|
+
};
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return { stopped: false, error: error instanceof Error ? error.message : String(error) };
|
|
267
|
+
}
|
|
225
268
|
}
|
|
226
269
|
|
|
227
270
|
export async function executeTaskTool(args, options = {}) {
|
|
@@ -573,6 +573,64 @@ export function listCachedCodeGraphRoots() {
|
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
// Remove one root (exact canonical match) from the disk cache: its resident
|
|
577
|
+
// entry, its staged call-site sidecar, its manifest row and its <hash>.json /
|
|
578
|
+
// <hash>.calls.json files. The resident drop is synchronous and happens first,
|
|
579
|
+
// so a flush already scheduled for other roots can no longer write this one
|
|
580
|
+
// back; the manifest is edited under the same cross-process lock the persist
|
|
581
|
+
// path takes, so a concurrent writer merges the result instead of restoring
|
|
582
|
+
// the row. Never creates the cache directory. Returns { removed, kept }; kept
|
|
583
|
+
// are cache files that matched but could not be deleted.
|
|
584
|
+
export function forgetDiskCodeGraphRoot(root) {
|
|
585
|
+
const key = _canonicalGraphCwd(root);
|
|
586
|
+
const removed = [];
|
|
587
|
+
const kept = [];
|
|
588
|
+
if (_diskCodeGraphCache.has(key)) removed.push(`resident:${key}`);
|
|
589
|
+
_dropDiskEntry(key);
|
|
590
|
+
if (_pendingCallsSidecars.delete(key)) removed.push(`pending-calls-sidecar:${key}`);
|
|
591
|
+
const dir = _codeGraphDiskDir();
|
|
592
|
+
if (!existsSync(dir)) return { removed, kept };
|
|
593
|
+
withFileLockSync(
|
|
594
|
+
join(dir, '.persist.lock'),
|
|
595
|
+
() => {
|
|
596
|
+
const manifest = _readPreservedManifest(dir);
|
|
597
|
+
const hashes = new Set([_hashCwd(key)]);
|
|
598
|
+
let changed = false;
|
|
599
|
+
for (const [cwd, meta] of Object.entries(manifest)) {
|
|
600
|
+
let same = false;
|
|
601
|
+
try {
|
|
602
|
+
same = _canonicalGraphCwd(cwd) === key;
|
|
603
|
+
} catch {
|
|
604
|
+
/* a malformed key is not this root */
|
|
605
|
+
}
|
|
606
|
+
if (!same) continue;
|
|
607
|
+
if (_isCodeGraphCacheHash(meta?.hash)) hashes.add(meta.hash);
|
|
608
|
+
delete manifest[cwd];
|
|
609
|
+
changed = true;
|
|
610
|
+
removed.push(`manifest:${cwd}`);
|
|
611
|
+
}
|
|
612
|
+
for (const hash of hashes) {
|
|
613
|
+
for (const file of [join(dir, `${hash}.json`), callsSidecarPath(dir, hash)]) {
|
|
614
|
+
if (!existsSync(file)) continue;
|
|
615
|
+
try {
|
|
616
|
+
unlinkSync(file);
|
|
617
|
+
removed.push(file);
|
|
618
|
+
} catch {
|
|
619
|
+
kept.push(file);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (changed) writeJsonAtomicSync(join(dir, 'manifest.json'), manifest, { compact: true, lock: false });
|
|
624
|
+
if (_diskManifest) {
|
|
625
|
+
delete _diskManifest[key];
|
|
626
|
+
if (changed) _diskManifest = manifest;
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
{ timeoutMs: 10_000 }
|
|
630
|
+
);
|
|
631
|
+
return { removed, kept };
|
|
632
|
+
}
|
|
633
|
+
|
|
576
634
|
// ── call-site sidecar ───────────────────────────────────────────────────────
|
|
577
635
|
// Resolve the sidecar file of a canonical cwd. The manifest hash wins when the
|
|
578
636
|
// entry is known (it is what the persist loop wrote); otherwise the hash is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
-
import { listProjects } from '../../../../shared/projects.mjs';
|
|
3
|
+
import { listProjects, removeProject } from '../../../../shared/projects.mjs';
|
|
4
4
|
import { explicitSessionCwd, readLastSessionCwd } from '../../../../shared/user-cwd.mjs';
|
|
5
5
|
import { canonicalGraphPath } from '../code-graph-state.mjs';
|
|
6
6
|
import { listCachedCodeGraphRoots } from './disk-cache.mjs';
|
|
@@ -85,6 +85,30 @@ export function collectTrustedCodeGraphRoots(
|
|
|
85
85
|
return [...found.values()].sort((a, b) => a.localeCompare(b));
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Stop trusting one root as a federation target. Trust is derived from the
|
|
90
|
+
* Project registry, the session's selected cwd and the cache manifest; the
|
|
91
|
+
* manifest row goes with forgetDiskCodeGraphRoot() and a selected cwd is
|
|
92
|
+
* dropped by the directory check once the folder is gone, so the registry is
|
|
93
|
+
* the only stored source left. Only the exact root is removed (nested Projects
|
|
94
|
+
* stay). Returns { removed, kept }; kept are registered paths that matched but
|
|
95
|
+
* could not be unregistered.
|
|
96
|
+
*/
|
|
97
|
+
export function forgetTrustedCodeGraphRoot(
|
|
98
|
+
root,
|
|
99
|
+
{ registered = () => listProjects().map((entry) => entry.path), unregister = removeProject } = {}
|
|
100
|
+
) {
|
|
101
|
+
const removed = [];
|
|
102
|
+
const kept = [];
|
|
103
|
+
const key = canonicalGraphPath(root);
|
|
104
|
+
for (const path of registered()) {
|
|
105
|
+
if (!path || canonicalGraphPath(path) !== key) continue;
|
|
106
|
+
if (unregister(path)) removed.push(path);
|
|
107
|
+
else kept.push(path);
|
|
108
|
+
}
|
|
109
|
+
return { removed, kept };
|
|
110
|
+
}
|
|
111
|
+
|
|
88
112
|
export function owningTrustedCodeGraphRoot(file, roots) {
|
|
89
113
|
const matches = (roots || []).filter((root) => _pathIsWithin(root, file));
|
|
90
114
|
matches.sort((a, b) => resolve(b).length - resolve(a).length);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// UI-diff side channel: a committed apply_patch (or edit-tool write) records
|
|
2
2
|
// a turn-review change entry and a per-call diff the desktop/TUI tool cards
|
|
3
3
|
// pick up. Display only — never affects edit semantics.
|
|
4
|
+
import { createTwoFilesPatch } from 'diff';
|
|
4
5
|
import { boundReviewPatch } from '../../../../shared/review-diff.mjs';
|
|
5
6
|
import { recordTurnDiffChanges } from '../../../../shared/turn-snapshot.mjs';
|
|
6
7
|
import { pathKey, resolveV4AEntryPath } from './paths.mjs';
|
|
@@ -29,6 +30,77 @@ export function takeApplyPatchUiDiff(callId) {
|
|
|
29
30
|
return value;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
// A display path is a plain relative or absolute path; the patch header only
|
|
34
|
+
// needs forward slashes. Its own leading "a/" is part of the name.
|
|
35
|
+
function headerPath(value) {
|
|
36
|
+
return String(value || '')
|
|
37
|
+
.trim()
|
|
38
|
+
.replace(/\\/g, '/');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DIFF_TIMEOUT_MS = 100;
|
|
42
|
+
|
|
43
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
44
|
+
|
|
45
|
+
function isOmitted(value) {
|
|
46
|
+
return Boolean(value) && typeof value === 'object' && !(value instanceof Uint8Array) && value.omitted === true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Text of one side: '' for an absent file, null when it is not UTF-8 text.
|
|
50
|
+
function sideText(value) {
|
|
51
|
+
if (value == null) return '';
|
|
52
|
+
if (typeof value === 'string') return value;
|
|
53
|
+
if (value instanceof Uint8Array) {
|
|
54
|
+
try {
|
|
55
|
+
return utf8.decode(value);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// One file's change, from the call's own before and after states: absent
|
|
64
|
+
// (null), bytes or text, or an omitted marker when the bytes were not kept.
|
|
65
|
+
function renderCallChange({ displayPath, newDisplayPath, before, after }) {
|
|
66
|
+
const oldName = headerPath(displayPath) || 'unknown file';
|
|
67
|
+
const newName = headerPath(newDisplayPath) || oldName;
|
|
68
|
+
const oldHeader = before == null ? '/dev/null' : `a/${oldName}`;
|
|
69
|
+
const newHeader = after == null ? '/dev/null' : `b/${newName}`;
|
|
70
|
+
let head = `diff --git a/${oldName} b/${newName}\n`;
|
|
71
|
+
if (before == null && after != null) head += 'new file mode 100644\n';
|
|
72
|
+
else if (before != null && after == null) head += 'deleted file mode 100644\n';
|
|
73
|
+
if (isOmitted(before) || isOmitted(after)) {
|
|
74
|
+
return `${head}Binary files ${oldHeader} and ${newHeader} differ\n`;
|
|
75
|
+
}
|
|
76
|
+
const oldText = sideText(before);
|
|
77
|
+
const newText = sideText(after);
|
|
78
|
+
if (oldText === null || newText === null) return `${head}Binary files ${oldHeader} and ${newHeader} differ\n`;
|
|
79
|
+
if (oldText === newText) {
|
|
80
|
+
// An empty file created or deleted still changed: git writes the mode line alone.
|
|
81
|
+
if ((before == null) !== (after == null)) return head;
|
|
82
|
+
return oldName === newName ? '' : `${head}similarity index 100%\nrename from ${oldName}\nrename to ${newName}\n`;
|
|
83
|
+
}
|
|
84
|
+
const rename = oldName === newName ? '' : `rename from ${oldName}\nrename to ${newName}\n`;
|
|
85
|
+
try {
|
|
86
|
+
const generated = createTwoFilesPatch(oldHeader, newHeader, oldText, newText, '', '', {
|
|
87
|
+
context: 3,
|
|
88
|
+
timeout: DIFF_TIMEOUT_MS,
|
|
89
|
+
});
|
|
90
|
+
const start = generated.search(/^--- /m);
|
|
91
|
+
if (start >= 0) return `${head}${rename}${generated.slice(start).replace(/\n*$/, '\n')}`;
|
|
92
|
+
} catch {
|
|
93
|
+
// A pathological diff timing out must not stall tool completion.
|
|
94
|
+
}
|
|
95
|
+
return `${head}${rename}Binary files ${oldHeader} and ${newHeader} differ\n`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A call's card shows the change that call made — its own before against its
|
|
99
|
+
// own after — never the turn's running total for the same files.
|
|
100
|
+
export function callUiDiff(changes) {
|
|
101
|
+
return (changes || []).map(renderCallChange).join('');
|
|
102
|
+
}
|
|
103
|
+
|
|
32
104
|
// Edit-tool UI diff: a successful single-file str-replace edit records the
|
|
33
105
|
// same turn-review change entry and per-call UI diff side channel that a
|
|
34
106
|
// committed apply_patch gets, so desktop/TUI tool cards render both edit
|
|
@@ -36,15 +108,16 @@ export function takeApplyPatchUiDiff(callId) {
|
|
|
36
108
|
export function registerEditToolUiDiff({ callId, sessionId, basePath, fullPath, before, after }) {
|
|
37
109
|
if (!callId || !sessionId || typeof fullPath !== 'string') return;
|
|
38
110
|
try {
|
|
39
|
-
const
|
|
111
|
+
const changes = [
|
|
40
112
|
{
|
|
41
113
|
path: fullPath,
|
|
42
114
|
displayPath: patchHeaderPathForResolved(basePath || '', fullPath),
|
|
43
115
|
before: before ?? null,
|
|
44
116
|
after: after ?? null,
|
|
45
117
|
},
|
|
46
|
-
]
|
|
47
|
-
|
|
118
|
+
];
|
|
119
|
+
recordTurnDiffChanges(sessionId, changes);
|
|
120
|
+
registerApplyPatchUiDiff(callId, callUiDiff(changes));
|
|
48
121
|
} catch {
|
|
49
122
|
// best-effort display channel
|
|
50
123
|
}
|
|
@@ -111,8 +184,8 @@ export function registerCommittedPatchUiDiff({
|
|
|
111
184
|
after: snapshotContent(after),
|
|
112
185
|
});
|
|
113
186
|
}
|
|
114
|
-
|
|
115
|
-
registerApplyPatchUiDiff(callId,
|
|
187
|
+
recordTurnDiffChanges(sessionId, changes);
|
|
188
|
+
registerApplyPatchUiDiff(callId, callUiDiff(changes));
|
|
116
189
|
} catch {
|
|
117
190
|
// Review collection is a side channel and must never affect committed
|
|
118
191
|
// apply_patch success/failure semantics.
|
|
@@ -75,6 +75,8 @@ export function formatToolStartProgress(name, args = {}) {
|
|
|
75
75
|
}
|
|
76
76
|
case 'terminal':
|
|
77
77
|
return a.action === 'read' ? `reading terminal${a.tab ? ` ${_t(a.tab)}` : ''}` : 'listing terminals';
|
|
78
|
+
case 'visualize':
|
|
79
|
+
return `visualize ${_t(a.action || 'command')}`;
|
|
78
80
|
case 'office':
|
|
79
81
|
return a.path ? `office ${_t(a.action || 'command')} ${_t(a.path)}` : `office ${_t(a.action || 'command')}`;
|
|
80
82
|
case 'media': {
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { createHash, randomBytes } from 'node:crypto';
|
|
10
10
|
import {
|
|
11
|
+
closeSync,
|
|
11
12
|
existsSync,
|
|
13
|
+
fstatSync,
|
|
12
14
|
mkdirSync,
|
|
15
|
+
openSync,
|
|
13
16
|
readFileSync,
|
|
17
|
+
readSync,
|
|
14
18
|
renameSync,
|
|
15
19
|
statSync,
|
|
16
20
|
unlinkSync,
|
|
@@ -303,6 +307,40 @@ function readAttachmentText(value) {
|
|
|
303
307
|
return readAttachmentBuffer(value).toString('utf8');
|
|
304
308
|
}
|
|
305
309
|
|
|
310
|
+
/**
|
|
311
|
+
* A referenced blob's UTF-8 text when the blob on disk holds at most
|
|
312
|
+
* `maxBytes`, else null. The size comes from the opened file, never from the
|
|
313
|
+
* reference's claimed sizeBytes, and at most `maxBytes + 1` bytes are read, so
|
|
314
|
+
* a blob that grows between the check and the read is rejected too. Missing,
|
|
315
|
+
* invalid or tampered blobs throw like readAttachmentBuffer.
|
|
316
|
+
*/
|
|
317
|
+
export function readAttachmentTextWithin(value, maxBytes) {
|
|
318
|
+
const ref = typeof value === 'string' ? value : value?.attachmentRef;
|
|
319
|
+
const fd = openSync(attachmentPath(ref), 'r');
|
|
320
|
+
try {
|
|
321
|
+
const info = fstatSync(fd);
|
|
322
|
+
if (!info.isFile() || info.size <= 0) {
|
|
323
|
+
throw Object.assign(new Error('prompt attachment blob is invalid'), { code: ATTACHMENT_INVALID });
|
|
324
|
+
}
|
|
325
|
+
if (info.size > maxBytes) return null;
|
|
326
|
+
const buffer = Buffer.alloc(info.size + 1);
|
|
327
|
+
let length = 0;
|
|
328
|
+
while (length < buffer.length) {
|
|
329
|
+
const read = readSync(fd, buffer, length, buffer.length - length, length);
|
|
330
|
+
if (read === 0) break;
|
|
331
|
+
length += read;
|
|
332
|
+
}
|
|
333
|
+
if (length > maxBytes) return null;
|
|
334
|
+
const bytes = buffer.subarray(0, length);
|
|
335
|
+
if (attachmentRefForBuffer(bytes) !== ref) {
|
|
336
|
+
throw Object.assign(new Error('prompt attachment integrity check failed'), { code: ATTACHMENT_INTEGRITY });
|
|
337
|
+
}
|
|
338
|
+
return bytes.toString('utf8');
|
|
339
|
+
} finally {
|
|
340
|
+
closeSync(fd);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
306
344
|
export function attachmentTextForPart(part) {
|
|
307
345
|
if (!isAttachmentReference(part)) return typeof part?.text === 'string' ? part.text : '';
|
|
308
346
|
const text = readAttachmentText(part);
|