mixdog 1.2.0 → 1.2.1
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 +7 -5
- package/package.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/pptx/SKILL.md +6 -6
- package/src/defaults/skills/pptx/references/review.md +6 -4
- package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
- package/src/defaults/skills/setup/references/actions.md +13 -3
- package/src/defaults/skills/visualize/SKILL.md +12 -9
- package/src/rules/shared/60-verification.md +3 -1
- package/src/rules/shared/70-delivery.md +4 -2
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
- package/src/runtime/agent/orchestrator/config.mjs +10 -4
- package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
- package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
- package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
- package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
- package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
- package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
- package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
- package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
- package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
- package/src/runtime/attachments/store.d.mts +3 -0
- package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
- package/src/runtime/channels/lib/worker-main.mjs +17 -14
- package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
- package/src/runtime/effort-judge/judge-client.mjs +56 -18
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +2 -2
- package/src/runtime/shared/atomic-file.mjs +25 -4
- package/src/runtime/shared/extension-scopes.mjs +5 -3
- package/src/runtime/shared/file-lock.mjs +37 -11
- package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
- package/src/runtime/shared/llm/model-catalog.mjs +0 -12
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
- package/src/runtime/shared/plugin-manifest.mjs +18 -9
- package/src/runtime/shared/pristine-execution-contract.json +9 -2
- package/src/runtime/shared/pristine-execution.mjs +28 -4
- package/src/runtime/shared/provider-accounts.mjs +124 -20
- package/src/runtime/shared/provider-api-key.mjs +12 -0
- package/src/runtime/shared/shell-display.mjs +9 -3
- package/src/runtime/shared/skill-mentions.d.mts +10 -0
- package/src/runtime/shared/skill-mentions.mjs +133 -0
- package/src/runtime/shared/skill-script-runtime.mjs +264 -0
- package/src/runtime/shared/tool-primitives.mjs +35 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
- package/src/runtime/shared/worker-requests.mjs +15 -2
- package/src/session-runtime/automation-agents.mjs +8 -2
- package/src/session-runtime/boot/apis.mjs +55 -0
- package/src/session-runtime/boot/config.mjs +5 -2
- package/src/session-runtime/boot/facade.mjs +1 -0
- package/src/session-runtime/boot/infrastructure.mjs +29 -18
- package/src/session-runtime/conversation-import/discovery.mjs +698 -0
- package/src/session-runtime/conversation-import/service.mjs +369 -0
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
- package/src/session-runtime/mcp-glue/connect.mjs +57 -10
- package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
- package/src/session-runtime/mcp-glue.mjs +22 -2
- package/src/session-runtime/new-session-config.mjs +3 -2
- package/src/session-runtime/notification-bus.mjs +9 -2
- package/src/session-runtime/provider-auth/accounts.mjs +33 -15
- package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
- package/src/session-runtime/provider-readiness.mjs +16 -10
- package/src/session-runtime/resource-extension-sync.mjs +20 -5
- package/src/session-runtime/resource-mcp-api.mjs +101 -77
- package/src/session-runtime/resource-plugins-api.mjs +29 -41
- package/src/session-runtime/resource-scope-decorate.mjs +6 -8
- package/src/session-runtime/resource-skills-api.mjs +23 -2
- package/src/session-runtime/schedule-session-run.mjs +1 -1
- package/src/session-runtime/services/channel-session-router.mjs +4 -1
- package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
- package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
- package/src/session-runtime/services/provider-admin.mjs +64 -18
- package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
- package/src/session-runtime/setup-tool/executor.mjs +12 -1
- package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
- package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
- package/src/session-runtime/skills-api.mjs +7 -3
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/webhook-session-run.mjs +16 -4
- package/src/skill-script-entry.mjs +36 -0
- package/src/standalone/daemon-stored-session-views.mjs +11 -3
- package/src/standalone/daemon.mjs +41 -4
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
- package/src/tui/App.jsx +9 -1
- package/src/tui/app/app-view/floating-panel.jsx +9 -1
- package/src/tui/app/app-view.jsx +6 -0
- package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/skill-mention-palette.mjs +88 -0
- package/src/tui/app/slash-palette.mjs +23 -7
- package/src/tui/app/transcript-row-estimate.mjs +11 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
- package/src/tui/app/use-prompt-state.mjs +5 -0
- package/src/tui/components/Message.jsx +22 -1
- package/src/tui/components/PromptInput.jsx +21 -3
- package/src/tui/components/SlashCommandPalette.jsx +12 -3
- package/src/tui/components/TranscriptItem.jsx +9 -1
- package/src/tui/components/prompt-input/key-handler.mjs +21 -8
- package/src/tui/components/user-message-skills.mjs +38 -0
- package/src/tui/dist/index.mjs +507 -171
- package/src/tui/session/queue-helpers.mjs +7 -2
- package/src/tui/session/session-api/extensions.mjs +4 -2
- package/src/tui/session/session-api/settings.mjs +7 -0
- package/src/tui/session/session-flow/drain.mjs +14 -4
- package/src/tui/session/tui-steering-persist.mjs +25 -9
- package/src/tui/session/turn.mjs +17 -0
- package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
- package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { isActualUserInstructionMessage, isInjectedSkillBodyMessage, isSummaryMessage } from './compact/messages.mjs';
|
|
17
17
|
import { ACTIVE_TURN_CONTINUATION_ANCHOR, ACTIVE_TURN_CONTINUATION_SOURCE } from './compact/continuation.mjs';
|
|
18
18
|
import { parseTaskNotification } from '../../../shared/task-notification-envelope.mjs';
|
|
19
|
+
import { isImportedMessage } from './imported-message.mjs';
|
|
19
20
|
|
|
20
21
|
export const SYNTHETIC_USER_ENVELOPE_TAG = 'mixdog-runtime';
|
|
21
22
|
|
|
@@ -62,7 +63,8 @@ function isAlreadyEnveloped(text) {
|
|
|
62
63
|
|
|
63
64
|
// null => a real user instruction (or nothing to wrap); otherwise the kind.
|
|
64
65
|
export function classifySyntheticUserMessage(message) {
|
|
65
|
-
|
|
66
|
+
// Imported prose stays the user's own (unwrapped) voice.
|
|
67
|
+
if (message?.role !== 'user' || isImportedMessage(message)) return null;
|
|
66
68
|
const text = messageText(message);
|
|
67
69
|
if (!text.trim()) return null;
|
|
68
70
|
if (isAlreadyEnveloped(text)) return null;
|
|
@@ -92,7 +92,7 @@ function crossTurnStub(batch, call, ctSig) {
|
|
|
92
92
|
// (which spans ONE assistant turn): when the model re-issues an IDENTICAL
|
|
93
93
|
// normalized call that has already failed repeatFailLimit times in a row
|
|
94
94
|
// across iterations, stop re-executing — the result will not change, and
|
|
95
|
-
// each retry burns a full LLM round-trip
|
|
95
|
+
// each retry burns a full LLM round-trip with no iteration cap to end it. The
|
|
96
96
|
// call stays unresolved; the guard only skips re-execution.
|
|
97
97
|
function repeatFailureSkip(batch, call, sigs) {
|
|
98
98
|
const { sessionRef, repeatFailLimit } = batch;
|
|
@@ -6,7 +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
|
+
import { restoredUserImageRefs, restoredUserText } from './user-text.mjs';
|
|
10
10
|
import {
|
|
11
11
|
completionCardFromExecution,
|
|
12
12
|
parseModelVisibleCompletionWrapper,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from '../../../../shared/tool-execution-contract.mjs';
|
|
39
39
|
import { toolResultTerminalStatus } from '../../../../shared/tool-status.mjs';
|
|
40
40
|
import { transcriptRouteMetadataFields } from '../../../../shared/transcript-metadata.mjs';
|
|
41
|
+
import { isImportedMessage } from '../imported-message.mjs';
|
|
41
42
|
|
|
42
43
|
function restoredTranscriptMetadata(message) {
|
|
43
44
|
const value = message?.meta?.transcript;
|
|
@@ -62,6 +63,10 @@ function restoredTranscriptMetadata(message) {
|
|
|
62
63
|
...(typeof value.sender === 'string' && value.sender ? { sender: value.sender } : {}),
|
|
63
64
|
...(typeof value.device === 'string' && value.device ? { device: value.device } : {}),
|
|
64
65
|
...(typeof value.checkpointId === 'string' && value.checkpointId ? { checkpointId: value.checkpointId } : {}),
|
|
66
|
+
// Skills this user turn explicitly loaded (header selection or `$name`).
|
|
67
|
+
...(Array.isArray(value.appliedSkills) && value.appliedSkills.length
|
|
68
|
+
? { appliedSkills: value.appliedSkills.filter((name) => typeof name === 'string' && name) }
|
|
69
|
+
: {}),
|
|
65
70
|
...(completion ? { completion } : {}),
|
|
66
71
|
};
|
|
67
72
|
}
|
|
@@ -322,6 +327,11 @@ function restoredUserTranscriptItems(message, nextId) {
|
|
|
322
327
|
// skill bodies (meta:'skill'), hook/system reminders, and tag-wrapped context
|
|
323
328
|
// blocks must not restore as user bubbles in any client.
|
|
324
329
|
if (message?.meta === 'skill' || message?.meta === 'hook') return [];
|
|
330
|
+
// Imported prose renders verbatim: never a control row, card or display rewrite.
|
|
331
|
+
if (isImportedMessage(message)) {
|
|
332
|
+
const text = (typeof message.content === 'string' ? message.content : toolResultText(message.content)).trim();
|
|
333
|
+
return text ? [{ kind: 'user', id: nextId(), text, ...restoredTranscriptMetadata(message) }] : [];
|
|
334
|
+
}
|
|
325
335
|
const text = restoredUserText(message?.content).trim();
|
|
326
336
|
// Persisted async-completion wrappers ("Async shell task ... finished. Result:
|
|
327
337
|
// > ...") are the only durable record of a background completion — the live
|
|
@@ -353,7 +363,16 @@ function restoredUserTranscriptItems(message, nextId) {
|
|
|
353
363
|
}
|
|
354
364
|
const synthetic = parseSyntheticAgentMessage(text);
|
|
355
365
|
if (!synthetic) {
|
|
356
|
-
|
|
366
|
+
const imageRefs = restoredUserImageRefs(message?.content);
|
|
367
|
+
return [
|
|
368
|
+
{
|
|
369
|
+
kind: 'user',
|
|
370
|
+
id: nextId(),
|
|
371
|
+
text,
|
|
372
|
+
...restoredTranscriptMetadata(message),
|
|
373
|
+
...(imageRefs.length ? { imageRefs } : {}),
|
|
374
|
+
},
|
|
375
|
+
];
|
|
357
376
|
}
|
|
358
377
|
return [restoredNotificationToolItem(synthetic, text, message, nextId, /^(failed|error|killed|cancelled)$/i)];
|
|
359
378
|
}
|
|
@@ -21,9 +21,7 @@ const FEEDBACK_BLOCK = /<visualize_feedback>\n([\s\S]*?)\n?<\/visualize_feedback
|
|
|
21
21
|
// on the blob itself: the reference's claimed sizeBytes is not trusted.
|
|
22
22
|
const FEEDBACK_MAX_BYTES = 128 * 1024;
|
|
23
23
|
const QUOTED = '("(?:[^"\\\\]|\\\\.)*")';
|
|
24
|
-
const CHOICE_LINE = new RegExp(
|
|
25
|
-
`· group: (?:${QUOTED}|\\(none\\)) → choice: ${QUOTED}(?: \\(${QUOTED}\\))?$`
|
|
26
|
-
);
|
|
24
|
+
const CHOICE_LINE = new RegExp(`· group: (?:${QUOTED}|\\(none\\)) → choice: ${QUOTED}(?: \\(${QUOTED}\\))?$`);
|
|
27
25
|
|
|
28
26
|
function unquote(value) {
|
|
29
27
|
if (!value) return '';
|
|
@@ -53,7 +51,7 @@ function feedbackDisplay(body) {
|
|
|
53
51
|
continue;
|
|
54
52
|
}
|
|
55
53
|
if (tweaks) {
|
|
56
|
-
const change = /^
|
|
54
|
+
const change = /^ {4}(.+?) \([^)]*\): (.*) \(was (.*)\)$/.exec(line);
|
|
57
55
|
if (change) tweaks.push(`${change[1]}: ${change[2]} (was ${change[3]})`);
|
|
58
56
|
else if (line.startsWith(' - tab: ')) {
|
|
59
57
|
tweaks = [];
|
|
@@ -118,23 +116,46 @@ function referencedFeedbackText(part) {
|
|
|
118
116
|
}
|
|
119
117
|
}
|
|
120
118
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
/** The display lines of a stored prompt's parts, and for each image part the
|
|
120
|
+
* line that draws it: the composer's metadata line right before it, else a
|
|
121
|
+
* bare line of its own (a bare line never stands for the next image). */
|
|
122
|
+
function restoredUserParts(content) {
|
|
125
123
|
const texts = [];
|
|
126
|
-
|
|
124
|
+
const images = [];
|
|
125
|
+
let pendingMetadata = '';
|
|
127
126
|
for (const part of content) {
|
|
128
|
-
let text;
|
|
129
127
|
if (part?.type === 'image' || part?.type === 'input_image') {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
let line = pendingMetadata;
|
|
129
|
+
if (!line) {
|
|
130
|
+
line = `[Image: ${part.mimeType || part.mediaType || 'image'}]`;
|
|
131
|
+
texts.push(line);
|
|
132
|
+
}
|
|
133
|
+
pendingMetadata = '';
|
|
134
|
+
images.push({ part, line });
|
|
135
|
+
continue;
|
|
135
136
|
}
|
|
136
|
-
|
|
137
|
+
const text = isAttachmentReference(part) ? referencedFeedbackText(part) : toolResultText([part]);
|
|
137
138
|
if (text) texts.push(text);
|
|
139
|
+
pendingMetadata = IMAGE_METADATA_LINE.test(text) ? text.trimEnd().split(/\r?\n/).pop().trim() : '';
|
|
138
140
|
}
|
|
139
|
-
return
|
|
141
|
+
return { texts, images };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function restoredUserText(content) {
|
|
145
|
+
if (!Array.isArray(content)) {
|
|
146
|
+
return withVisualizeFeedbackDisplay(typeof content === 'string' ? content : toolResultText(content));
|
|
147
|
+
}
|
|
148
|
+
return withVisualizeFeedbackDisplay(restoredUserParts(content).texts.join('\n'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The stored original of each image the restored bubble draws, by the
|
|
152
|
+
* `[Image: …]` line that draws it. Every image keeps its slot (one without a
|
|
153
|
+
* stored original has no `ref`) so later images do not shift onto it; empty
|
|
154
|
+
* when no image has a stored original. */
|
|
155
|
+
export function restoredUserImageRefs(content) {
|
|
156
|
+
if (!Array.isArray(content)) return [];
|
|
157
|
+
const slots = restoredUserParts(content).images.map(({ part, line }) =>
|
|
158
|
+
isAttachmentReference(part) ? { line, ref: part.attachmentRef } : { line }
|
|
159
|
+
);
|
|
160
|
+
return slots.some((slot) => slot.ref) ? slots : [];
|
|
140
161
|
}
|
|
@@ -3,13 +3,25 @@ import { buildPowerShellFilterTeePlan } from '../shell-analysis.mjs';
|
|
|
3
3
|
import { normalizeErrorMessage } from '../path-diagnostics.mjs';
|
|
4
4
|
import { planDirectExeSpawn } from '../shell-direct-exe.mjs';
|
|
5
5
|
import { POWERSHELL_UTF8_PREFIX } from '../../../../../shared/shell-display.mjs';
|
|
6
|
+
import { renderPowerShellSkillFunction, skillScriptRuntime } from '../../../../../shared/skill-script-runtime.mjs';
|
|
6
7
|
import { buildShellSpawnEnv } from './spawn-env.mjs';
|
|
7
8
|
import { formatShellToolFailure } from './result-format.mjs';
|
|
8
9
|
|
|
10
|
+
// Every PowerShell model command defines `mixdog-skill-node` first, so the
|
|
11
|
+
// skill launcher runs without the .cmd's cmd.exe argument re-parsing.
|
|
12
|
+
// Injected whenever absent, independent of the UTF-8 prefix deduplication.
|
|
13
|
+
function prefixPowerShellSkillFunction(command) {
|
|
14
|
+
const fn = renderPowerShellSkillFunction(skillScriptRuntime());
|
|
15
|
+
const text = String(command || '');
|
|
16
|
+
return text.trimStart().startsWith(`${fn}\n`) ? text : `${fn}\n${text}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
function prefixPowerShellUtf8(command) {
|
|
10
20
|
const prefix = POWERSHELL_UTF8_PREFIX;
|
|
11
21
|
const text = String(command || '');
|
|
12
|
-
|
|
22
|
+
const trimmed = text.trimStart();
|
|
23
|
+
const body = trimmed.startsWith(prefix) ? trimmed.slice(prefix.length).replace(/^[ \t]*\r?\n/, '') : text;
|
|
24
|
+
return `${prefix}\n${prefixPowerShellSkillFunction(body)}`;
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
// How the command reaches the child: a direct exe spawn, PowerShell with the
|
|
@@ -54,6 +66,10 @@ export async function planShellInvocation({ command, resolvedSpec, cwd }) {
|
|
|
54
66
|
plan.wrappedCommand = prefixPowerShellUtf8(plan.teePlan ? plan.teePlan.command : command);
|
|
55
67
|
return plan;
|
|
56
68
|
}
|
|
69
|
+
if (shellType === 'powershell') {
|
|
70
|
+
plan.wrappedCommand = prefixPowerShellSkillFunction(command);
|
|
71
|
+
return plan;
|
|
72
|
+
}
|
|
57
73
|
if (process.platform !== 'win32' && (shell.includes('bash') || shell.includes('zsh'))) {
|
|
58
74
|
try {
|
|
59
75
|
plan.wrappedCommand = await wrapCommandWithSnapshot(shell, command);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resourceAdmission } from '../../../../../shared/resource-admission.mjs';
|
|
2
|
+
import { withSkillLauncherPath } from '../../../../../shared/skill-script-runtime.mjs';
|
|
2
3
|
import {
|
|
3
4
|
applyShellEgressPolicy,
|
|
4
5
|
scrubLoaderVars,
|
|
@@ -24,5 +25,8 @@ export function buildShellSpawnEnv(cwd, baseEnv = process.env) {
|
|
|
24
25
|
scrubLoaderVars(spawnEnv);
|
|
25
26
|
scrubRuntimeRootVars(spawnEnv);
|
|
26
27
|
applyShellEgressPolicy(spawnEnv);
|
|
28
|
+
// `mixdog-skill-node` (a skill's ${MIXDOG_SKILL_NODE}) resolves in every
|
|
29
|
+
// model shell; only this env gets the entry, never the daemon's own.
|
|
30
|
+
withSkillLauncherPath(spawnEnv);
|
|
27
31
|
return spawnEnv;
|
|
28
32
|
}
|
|
@@ -8,7 +8,7 @@ import { normalizeOutputPath, resolveAgainstCwd } from '../path-utils.mjs';
|
|
|
8
8
|
/** Report the requested path's result; suggestions never retarget the operation. */
|
|
9
9
|
export async function readFamilyPathEnoentOrError(workDir, fullPath, inputPath, err) {
|
|
10
10
|
const safeMsg = normalizeErrorMessage(err instanceof Error ? err.message : String(err));
|
|
11
|
-
const hint = buildNotFoundHint(workDir, fullPath, 'List', err?.code);
|
|
11
|
+
const hint = await buildNotFoundHint(workDir, fullPath, 'List', err?.code);
|
|
12
12
|
const tail = finalizeReadFamilyEnoentTail(hint, inputPath, err?.code);
|
|
13
13
|
// Conclusive absence is the list's ANSWER, not a tool failure (matches
|
|
14
14
|
// read's `[path absent]` and git's `repo:false` policy). Genuine
|
|
@@ -78,4 +78,4 @@ export async function countTextFileLines(fullPath, size) {
|
|
|
78
78
|
} finally {
|
|
79
79
|
await handle?.close().catch(() => {});
|
|
80
80
|
}
|
|
81
|
-
}
|
|
81
|
+
}
|
|
@@ -97,7 +97,7 @@ const BASENAME_SCAN_SKIP_DIRS = new Set([
|
|
|
97
97
|
// directories scanned and wall-clock. `visitEntry` decides what counts as a
|
|
98
98
|
// match and which directories to queue, and returns true to stop reading the
|
|
99
99
|
// current directory (its own limit break).
|
|
100
|
-
function scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, visitEntry) {
|
|
100
|
+
async function scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, visitEntry) {
|
|
101
101
|
const matches = [];
|
|
102
102
|
const queue = [searchRoot];
|
|
103
103
|
const stopAt = Date.now() + Math.max(1, deadlineMs);
|
|
@@ -112,7 +112,7 @@ function scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, visitEntry)
|
|
|
112
112
|
scanned++;
|
|
113
113
|
let entries;
|
|
114
114
|
try {
|
|
115
|
-
entries =
|
|
115
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
116
116
|
} catch {
|
|
117
117
|
continue;
|
|
118
118
|
}
|
|
@@ -127,7 +127,7 @@ function scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, visitEntry)
|
|
|
127
127
|
return matches;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
export function findFileByBasename(
|
|
130
|
+
export async function findFileByBasename(
|
|
131
131
|
searchRoot,
|
|
132
132
|
fullPath,
|
|
133
133
|
{ limit = 3, maxDirs = 6000, deadlineMs = BASENAME_SCAN_DEADLINE_MS } = {}
|
|
@@ -136,7 +136,7 @@ export function findFileByBasename(
|
|
|
136
136
|
if (typeof searchRoot !== 'string' || !searchRoot) return [];
|
|
137
137
|
const target = basename(fullPath).toLowerCase();
|
|
138
138
|
if (!target) return [];
|
|
139
|
-
return scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, (dir, ent, matches, queue) => {
|
|
139
|
+
return await scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, (dir, ent, matches, queue) => {
|
|
140
140
|
const name = ent.name;
|
|
141
141
|
if (ent.isDirectory()) {
|
|
142
142
|
if (name.startsWith('.') || BASENAME_SCAN_SKIP_DIRS.has(name)) return false;
|
|
@@ -205,7 +205,7 @@ export function findBySuffixStrip(searchRoot, fullPath, { maxIterations = 12 } =
|
|
|
205
205
|
|
|
206
206
|
// Same as findFileByBasename but for directories sharing the missing path's
|
|
207
207
|
// final segment name (e.g. guessed `src/shared` → the real `lib/shared`).
|
|
208
|
-
export function findDirectoryByBasename(
|
|
208
|
+
export async function findDirectoryByBasename(
|
|
209
209
|
searchRoot,
|
|
210
210
|
fullPath,
|
|
211
211
|
{ limit = 3, maxDirs = 6000, deadlineMs = BASENAME_SCAN_DEADLINE_MS } = {}
|
|
@@ -214,7 +214,7 @@ export function findDirectoryByBasename(
|
|
|
214
214
|
if (typeof searchRoot !== 'string' || !searchRoot) return [];
|
|
215
215
|
const target = basename(fullPath).toLowerCase();
|
|
216
216
|
if (!target) return [];
|
|
217
|
-
return scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, (dir, ent, matches, queue) => {
|
|
217
|
+
return await scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, (dir, ent, matches, queue) => {
|
|
218
218
|
const name = ent.name;
|
|
219
219
|
if (!ent.isDirectory()) return false;
|
|
220
220
|
if (name.toLowerCase() === target) {
|
|
@@ -19,6 +19,15 @@ const READ_RANGE_INDEX_SWEEP_CONCURRENCY = 8;
|
|
|
19
19
|
const READ_RANGE_INDEX_CACHE = new Map();
|
|
20
20
|
const READ_RANGE_INDEX_DRAFTS = new WeakMap();
|
|
21
21
|
const READ_RANGE_INDEX_PERSIST_PENDING = new Map();
|
|
22
|
+
const READ_RANGE_INDEX_INFLIGHT = new Map();
|
|
23
|
+
const READ_RANGE_INDEX_LAST_PERSISTED = new Map();
|
|
24
|
+
// key -> outstanding deletes whose unlink / cancelled write has not settled.
|
|
25
|
+
const READ_RANGE_INDEX_DELETING = new Map();
|
|
26
|
+
let readRangeIndexDeleteSeq = 0;
|
|
27
|
+
const READ_RANGE_INDEX_LAST_PERSISTED_MAX = 256;
|
|
28
|
+
const READ_RANGE_INDEX_WRITE_CONCURRENCY = 2;
|
|
29
|
+
const READ_RANGE_INDEX_WRITE_WAITERS = [];
|
|
30
|
+
let readRangeIndexWritesActive = 0;
|
|
22
31
|
let readRangeIndexDiskSwept = false;
|
|
23
32
|
|
|
24
33
|
let traceReadRangeIndex = () => {};
|
|
@@ -99,7 +108,9 @@ async function computePrefixHashForIndex(fullPath, st, handle = null, prefixBuff
|
|
|
99
108
|
if (!handle) {
|
|
100
109
|
try {
|
|
101
110
|
await fh.close();
|
|
102
|
-
} catch {
|
|
111
|
+
} catch {
|
|
112
|
+
/* close failure after a completed read is irrelevant */
|
|
113
|
+
}
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
} catch {
|
|
@@ -123,14 +134,21 @@ async function loadReadRangeIndexFromDisk(fullPath, st, handle, prefixBuffer) {
|
|
|
123
134
|
// No existsSync preflight: a missing file surfaces as an ENOENT from
|
|
124
135
|
// readFile below, caught by the same try/catch — one FS pass, not two.
|
|
125
136
|
if (!file || !st) return null;
|
|
137
|
+
// A delete wins over any disk content: while a cancelled write may still be
|
|
138
|
+
// landing the file is invisible, and a read that straddled a delete is void.
|
|
139
|
+
const key = canonicalCachePath(fullPath);
|
|
140
|
+
const deleteSeq = readRangeIndexDeleteSeq;
|
|
141
|
+
const deleted = () => READ_RANGE_INDEX_DELETING.has(key) || readRangeIndexDeleteSeq !== deleteSeq;
|
|
142
|
+
if (deleted()) return null;
|
|
126
143
|
try {
|
|
127
144
|
const row = JSON.parse(await readFile(file, 'utf-8'));
|
|
128
|
-
if (!readRangeIndexMatches(row, fullPath, st)) return null;
|
|
145
|
+
if (deleted() || !readRangeIndexMatches(row, fullPath, st)) return null;
|
|
129
146
|
// The prefix is an extra check, not proof that later byte offsets survived.
|
|
130
147
|
if (typeof row.prefixHash === 'string' && row.prefixHash) {
|
|
131
148
|
const cur = await computePrefixHashForIndex(fullPath, st, handle, prefixBuffer);
|
|
132
149
|
if (!cur || cur !== row.prefixHash) return null;
|
|
133
150
|
}
|
|
151
|
+
if (deleted()) return null;
|
|
134
152
|
const anchors = new Map([[0, 0]]);
|
|
135
153
|
for (const entry of row.anchors) {
|
|
136
154
|
if (!Array.isArray(entry) || entry.length < 2) continue;
|
|
@@ -161,35 +179,133 @@ function currentReadRangeIndex(index) {
|
|
|
161
179
|
return !!index?.published && READ_RANGE_INDEX_CACHE.get(canonicalCachePath(index.fullPath)) === index;
|
|
162
180
|
}
|
|
163
181
|
|
|
164
|
-
function
|
|
182
|
+
function rememberPersisted(key, text) {
|
|
183
|
+
READ_RANGE_INDEX_LAST_PERSISTED.delete(key);
|
|
184
|
+
READ_RANGE_INDEX_LAST_PERSISTED.set(key, text);
|
|
185
|
+
if (READ_RANGE_INDEX_LAST_PERSISTED.size > READ_RANGE_INDEX_LAST_PERSISTED_MAX) {
|
|
186
|
+
READ_RANGE_INDEX_LAST_PERSISTED.delete(READ_RANGE_INDEX_LAST_PERSISTED.keys().next().value);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// job = { index, row }: row is a pre-serialised evicted row (index no longer cached).
|
|
191
|
+
// Returns false only when a write was needed but failed (e.g. lock contended).
|
|
192
|
+
function persistReadRangeIndexSync(job) {
|
|
193
|
+
const { index, row: evictedRow } = job;
|
|
165
194
|
const file = readRangeIndexFilePath(index?.fullPath);
|
|
166
|
-
if (!file || !currentReadRangeIndex(index)) return;
|
|
195
|
+
if (!file || (!evictedRow && !currentReadRangeIndex(index))) return true;
|
|
167
196
|
try {
|
|
168
|
-
if (!sameFileVersion(index.fileVersion, statSync(index.fullPath))) return;
|
|
169
|
-
const row = serialiseReadRangeIndex(index);
|
|
170
|
-
if (row)
|
|
171
|
-
|
|
197
|
+
if (!sameFileVersion(evictedRow?.fileVersion || index.fileVersion, statSync(index.fullPath))) return true;
|
|
198
|
+
const row = evictedRow || serialiseReadRangeIndex(index);
|
|
199
|
+
if (!row) return true;
|
|
200
|
+
const key = canonicalCachePath(index.fullPath);
|
|
201
|
+
const text = JSON.stringify(row);
|
|
202
|
+
if (READ_RANGE_INDEX_LAST_PERSISTED.get(key) === text) return true;
|
|
203
|
+
writeJsonAtomicSync(file, row, { compact: true, lock: true, fsync: false });
|
|
204
|
+
rememberPersisted(key, text);
|
|
205
|
+
return true;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
172
209
|
}
|
|
173
210
|
|
|
174
|
-
|
|
211
|
+
function acquireReadRangeIndexWriteSlot() {
|
|
212
|
+
if (readRangeIndexWritesActive < READ_RANGE_INDEX_WRITE_CONCURRENCY) {
|
|
213
|
+
readRangeIndexWritesActive += 1;
|
|
214
|
+
return Promise.resolve();
|
|
215
|
+
}
|
|
216
|
+
return new Promise((resolve) => READ_RANGE_INDEX_WRITE_WAITERS.push(resolve));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function releaseReadRangeIndexWriteSlot() {
|
|
220
|
+
const next = READ_RANGE_INDEX_WRITE_WAITERS.shift();
|
|
221
|
+
if (next) next();
|
|
222
|
+
else readRangeIndexWritesActive -= 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// One write attempt. `state.epoch` changes when the path is deleted: a write
|
|
226
|
+
// that straddles a delete removes what it just wrote instead of resurrecting it.
|
|
227
|
+
async function persistReadRangeIndexOnce(key, job, state) {
|
|
228
|
+
const { index, row: evictedRow } = job;
|
|
175
229
|
const file = readRangeIndexFilePath(index?.fullPath);
|
|
176
230
|
if (!file || (!evictedRow && !currentReadRangeIndex(index))) return;
|
|
231
|
+
const epoch = state.epoch;
|
|
232
|
+
// Admission precedes every stat/lock so waiting writers hold no file lock.
|
|
233
|
+
await acquireReadRangeIndexWriteSlot();
|
|
177
234
|
try {
|
|
235
|
+
if (state.epoch !== epoch || (!evictedRow && !currentReadRangeIndex(index))) return;
|
|
178
236
|
if (!sameFileVersion(evictedRow?.fileVersion || index.fileVersion, await stat(index.fullPath))) return;
|
|
179
237
|
await mkdir(READ_RANGE_INDEX_DISK_DIR, { recursive: true });
|
|
180
|
-
if (!evictedRow && !currentReadRangeIndex(index)) return;
|
|
238
|
+
if (state.epoch !== epoch || (!evictedRow && !currentReadRangeIndex(index))) return;
|
|
181
239
|
const row = evictedRow || serialiseReadRangeIndex(index);
|
|
182
|
-
if (row)
|
|
183
|
-
|
|
240
|
+
if (!row) return;
|
|
241
|
+
const text = JSON.stringify(row);
|
|
242
|
+
if (READ_RANGE_INDEX_LAST_PERSISTED.get(key) === text) return;
|
|
243
|
+
readRangeIndexPersistStats.concurrent += 1;
|
|
244
|
+
readRangeIndexPersistStats.maxConcurrent = Math.max(
|
|
245
|
+
readRangeIndexPersistStats.maxConcurrent,
|
|
246
|
+
readRangeIndexPersistStats.concurrent
|
|
247
|
+
);
|
|
248
|
+
try {
|
|
249
|
+
await writeJsonAtomicAsync(file, row, { compact: true, lock: true, fsync: false });
|
|
250
|
+
readRangeIndexPersistStats.writes += 1;
|
|
251
|
+
} finally {
|
|
252
|
+
readRangeIndexPersistStats.concurrent -= 1;
|
|
253
|
+
}
|
|
254
|
+
if (state.epoch !== epoch) {
|
|
255
|
+
await unlink(file).catch(() => {});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
rememberPersisted(key, text);
|
|
259
|
+
} catch {
|
|
260
|
+
/* persisting the index is best-effort */
|
|
261
|
+
} finally {
|
|
262
|
+
releaseReadRangeIndexWriteSlot();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Single-flight per cache file: a request during an in-flight write only
|
|
267
|
+
// replaces the pending job; one follow-up write persists the latest state.
|
|
268
|
+
function requestReadRangeIndexPersist(key, job) {
|
|
269
|
+
const state = READ_RANGE_INDEX_INFLIGHT.get(key);
|
|
270
|
+
if (state) {
|
|
271
|
+
state.next = job;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const fresh = { epoch: 0, next: null, cancelWaiters: [] };
|
|
275
|
+
READ_RANGE_INDEX_INFLIGHT.set(key, fresh);
|
|
276
|
+
const settleCancelled = () => {
|
|
277
|
+
for (const resolve of fresh.cancelWaiters.splice(0)) resolve();
|
|
278
|
+
};
|
|
279
|
+
void (async () => {
|
|
280
|
+
try {
|
|
281
|
+
let cur = job;
|
|
282
|
+
while (cur) {
|
|
283
|
+
await persistReadRangeIndexOnce(key, cur, fresh);
|
|
284
|
+
// A cancelled write has finished (and cleaned up) once Once returns.
|
|
285
|
+
settleCancelled();
|
|
286
|
+
cur = fresh.next;
|
|
287
|
+
fresh.next = null;
|
|
288
|
+
}
|
|
289
|
+
} finally {
|
|
290
|
+
settleCancelled();
|
|
291
|
+
READ_RANGE_INDEX_INFLIGHT.delete(key);
|
|
292
|
+
}
|
|
293
|
+
})();
|
|
184
294
|
}
|
|
185
295
|
|
|
186
296
|
export function scheduleReadRangeIndexPersist(index) {
|
|
187
297
|
if (!READ_RANGE_INDEX_DISK_DIR || !currentReadRangeIndex(index)) return;
|
|
188
298
|
const key = canonicalCachePath(index.fullPath);
|
|
299
|
+
const inflight = READ_RANGE_INDEX_INFLIGHT.get(key);
|
|
300
|
+
if (inflight) {
|
|
301
|
+
inflight.next = { index, row: null };
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
189
304
|
if (READ_RANGE_INDEX_PERSIST_PENDING.has(key)) return;
|
|
190
305
|
const t = setTimeout(() => {
|
|
191
306
|
READ_RANGE_INDEX_PERSIST_PENDING.delete(key);
|
|
192
|
-
|
|
307
|
+
const latest = READ_RANGE_INDEX_CACHE.get(key);
|
|
308
|
+
if (latest) requestReadRangeIndexPersist(key, { index: latest, row: null });
|
|
193
309
|
}, READ_RANGE_INDEX_PERSIST_DEBOUNCE_MS);
|
|
194
310
|
t.unref();
|
|
195
311
|
READ_RANGE_INDEX_PERSIST_PENDING.set(key, t);
|
|
@@ -199,10 +315,19 @@ export function flushReadRangeIndexesSync() {
|
|
|
199
315
|
for (const [key, timer] of READ_RANGE_INDEX_PERSIST_PENDING) {
|
|
200
316
|
try {
|
|
201
317
|
clearTimeout(timer);
|
|
202
|
-
} catch {
|
|
318
|
+
} catch {
|
|
319
|
+
/* a timer that cannot be cleared must not block the flush */
|
|
320
|
+
}
|
|
203
321
|
READ_RANGE_INDEX_PERSIST_PENDING.delete(key);
|
|
204
322
|
const index = READ_RANGE_INDEX_CACHE.get(key);
|
|
205
|
-
if (index) persistReadRangeIndexSync(index);
|
|
323
|
+
if (index) persistReadRangeIndexSync({ index, row: null });
|
|
324
|
+
}
|
|
325
|
+
// Writes queued behind an in-flight one are still pending state at exit.
|
|
326
|
+
for (const state of READ_RANGE_INDEX_INFLIGHT.values()) {
|
|
327
|
+
// Keep the job unless it was durably handled: when the async writer holds
|
|
328
|
+
// the lock, its follow-up still persists it if the process keeps running.
|
|
329
|
+
const job = state.next;
|
|
330
|
+
if (job && persistReadRangeIndexSync(job) && state.next === job) state.next = null;
|
|
206
331
|
}
|
|
207
332
|
}
|
|
208
333
|
|
|
@@ -213,11 +338,32 @@ function dropReadRangeIndexSlot(key) {
|
|
|
213
338
|
}
|
|
214
339
|
|
|
215
340
|
export function deleteReadRangeIndexForPath(fullPath) {
|
|
216
|
-
|
|
341
|
+
const key = canonicalCachePath(fullPath);
|
|
342
|
+
dropReadRangeIndexSlot(key);
|
|
343
|
+
READ_RANGE_INDEX_LAST_PERSISTED.delete(key);
|
|
344
|
+
readRangeIndexDeleteSeq += 1;
|
|
345
|
+
// Disk loads for this path are void until the unlink and any cancelled
|
|
346
|
+
// in-flight write (which removes its own output) have settled.
|
|
347
|
+
READ_RANGE_INDEX_DELETING.set(key, (READ_RANGE_INDEX_DELETING.get(key) || 0) + 1);
|
|
348
|
+
const inflight = READ_RANGE_INDEX_INFLIGHT.get(key);
|
|
349
|
+
let cancelled = Promise.resolve();
|
|
350
|
+
if (inflight) {
|
|
351
|
+
inflight.epoch += 1;
|
|
352
|
+
inflight.next = null;
|
|
353
|
+
cancelled = new Promise((resolve) => inflight.cancelWaiters.push(resolve));
|
|
354
|
+
}
|
|
217
355
|
const file = readRangeIndexFilePath(fullPath);
|
|
218
|
-
|
|
356
|
+
const unlinked = file ? unlink(file).catch(() => {}) : Promise.resolve();
|
|
357
|
+
void Promise.all([unlinked, cancelled]).then(() => {
|
|
358
|
+
const left = (READ_RANGE_INDEX_DELETING.get(key) || 1) - 1;
|
|
359
|
+
if (left > 0) READ_RANGE_INDEX_DELETING.set(key, left);
|
|
360
|
+
else READ_RANGE_INDEX_DELETING.delete(key);
|
|
361
|
+
});
|
|
219
362
|
}
|
|
220
363
|
|
|
364
|
+
// Test observability for write coalescing and the concurrency cap.
|
|
365
|
+
export const readRangeIndexPersistStats = { writes: 0, concurrent: 0, maxConcurrent: 0 };
|
|
366
|
+
|
|
221
367
|
export async function sweepStaleReadRangeIndexes(directory = READ_RANGE_INDEX_DISK_DIR) {
|
|
222
368
|
if (!directory) return;
|
|
223
369
|
let entries;
|
|
@@ -235,8 +381,14 @@ export async function sweepStaleReadRangeIndexes(directory = READ_RANGE_INDEX_DI
|
|
|
235
381
|
const full = join(directory, name);
|
|
236
382
|
try {
|
|
237
383
|
const st = await stat(full);
|
|
238
|
-
if (now - st.mtimeMs > READ_RANGE_INDEX_DISK_STALE_MS)
|
|
239
|
-
|
|
384
|
+
if (now - st.mtimeMs > READ_RANGE_INDEX_DISK_STALE_MS) {
|
|
385
|
+
await unlink(full);
|
|
386
|
+
// A removed file invalidates the unchanged-content skip.
|
|
387
|
+
READ_RANGE_INDEX_LAST_PERSISTED.clear();
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
/* best-effort sweep: a file that vanished or cannot be removed is skipped */
|
|
391
|
+
}
|
|
240
392
|
}
|
|
241
393
|
}
|
|
242
394
|
await Promise.all(Array.from({ length: Math.min(READ_RANGE_INDEX_SWEEP_CONCURRENCY, entries.length) }, worker));
|
|
@@ -255,7 +407,7 @@ function refreshReadRangeIndexSlot(key, index) {
|
|
|
255
407
|
// their slot immediately. Disk IO must not delay the foreground lookup.
|
|
256
408
|
const row = serialiseReadRangeIndex(evicted);
|
|
257
409
|
dropReadRangeIndexSlot(oldest);
|
|
258
|
-
if (row)
|
|
410
|
+
if (row) requestReadRangeIndexPersist(oldest, { index: evicted, row });
|
|
259
411
|
}
|
|
260
412
|
return index;
|
|
261
413
|
}
|
|
@@ -346,8 +498,7 @@ export async function publishReadRangeIndex(index, handle, signal = null, before
|
|
|
346
498
|
// result requesting producer certification still owes its terminal stats.
|
|
347
499
|
if (slot && READ_RANGE_INDEX_CACHE.get(key) !== slot && !onValidated) return false;
|
|
348
500
|
try {
|
|
349
|
-
if (!sameFileVersion(version, await handle.stat()) || !sameFileVersion(version, await stat(fullPath)))
|
|
350
|
-
return false;
|
|
501
|
+
if (!sameFileVersion(version, await handle.stat()) || !sameFileVersion(version, await stat(fullPath))) return false;
|
|
351
502
|
} catch {
|
|
352
503
|
return false;
|
|
353
504
|
}
|
|
@@ -108,7 +108,9 @@ async function symlinkTargetGuard(fullPath, filePath, helpers) {
|
|
|
108
108
|
if (typeof isSpecialFileStat === 'function' && isSpecialFileStat(targetStat)) {
|
|
109
109
|
return guardResponse('special', helpers, filePath, realTarget);
|
|
110
110
|
}
|
|
111
|
-
} catch {
|
|
111
|
+
} catch {
|
|
112
|
+
/* vanished target: see above */
|
|
113
|
+
}
|
|
112
114
|
return null;
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -138,7 +140,9 @@ export async function statReadTarget(fullPath, filePath, options, helpers) {
|
|
|
138
140
|
const error = await symlinkTargetGuard(fullPath, filePath, helpers);
|
|
139
141
|
if (error) return { st, error };
|
|
140
142
|
}
|
|
141
|
-
} catch {
|
|
143
|
+
} catch {
|
|
144
|
+
/* lstat failure is non-fatal: see above */
|
|
145
|
+
}
|
|
142
146
|
return { st };
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -148,12 +152,12 @@ export async function statReadTarget(fullPath, filePath, options, helpers) {
|
|
|
148
152
|
* recovery mode for an ordinary absence probe. Genuine not-found only —
|
|
149
153
|
* EACCES/EPERM etc. keep failure semantics.
|
|
150
154
|
*/
|
|
151
|
-
export function readNotFoundResponse(err, { fullPath, filePath, workDir }, helpers) {
|
|
155
|
+
export async function readNotFoundResponse(err, { fullPath, filePath, workDir }, helpers) {
|
|
152
156
|
const { findSimilarFile, normalizeErrorMessage, normalizeOutputPath } = helpers;
|
|
153
157
|
const similar = findSimilarFile(fullPath);
|
|
154
158
|
let hint = similar
|
|
155
159
|
? appendNotFoundCwdNote(` Did you mean "${normalizeOutputPath(similar)}"?`, workDir, err?.code)
|
|
156
|
-
: buildNotFoundHint(workDir, fullPath, 'Read', err?.code);
|
|
160
|
+
: await buildNotFoundHint(workDir, fullPath, 'Read', err?.code);
|
|
157
161
|
hint = finalizeReadFamilyEnoentTail(hint, filePath, err?.code);
|
|
158
162
|
const safeMsg = normalizeErrorMessage(err instanceof Error ? err.message : String(err), workDir);
|
|
159
163
|
if (err?.code === 'ENOENT' || err?.code === 'ENOTDIR') return `[path absent] ${safeMsg}${hint}`;
|
|
@@ -30,9 +30,10 @@ async function prefetchRawBody(fullPath, st, helpers, forceFresh = false) {
|
|
|
30
30
|
const cachedRaw = !forceFresh && _rawContentCacheGet ? _rawContentCacheGet(fullPath, st) : null;
|
|
31
31
|
if (cachedRaw) return { buf: cachedRaw, fromCache: true };
|
|
32
32
|
try {
|
|
33
|
-
const buf =
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const buf =
|
|
34
|
+
!forceFresh && _runRawContentInFlight
|
|
35
|
+
? await _runRawContentInFlight(fullPath, readFile, st)
|
|
36
|
+
: await readFile(fullPath);
|
|
36
37
|
return { buf, fromCache: false };
|
|
37
38
|
} catch {
|
|
38
39
|
return { buf: null, fromCache: false };
|
|
@@ -84,7 +85,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
84
85
|
let widenNote = '';
|
|
85
86
|
const target = await statReadTarget(fullPath, filePath, options, helpers);
|
|
86
87
|
if (target.error) return target.error;
|
|
87
|
-
if (!target.st) return readNotFoundResponse(target.statErr, { fullPath, filePath, workDir }, helpers);
|
|
88
|
+
if (!target.st) return await readNotFoundResponse(target.statErr, { fullPath, filePath, workDir }, helpers);
|
|
88
89
|
const st = target.st;
|
|
89
90
|
// Reactive widen + window-history bookkeeping (ranged text reads only).
|
|
90
91
|
if (
|