mixdog 0.9.96 → 0.9.99
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 +0 -17
- package/package.json +16 -8
- package/scripts/build-tui.mjs +6 -6
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +43 -32
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +11 -10
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +42 -26
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -105
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
- package/src/runtime/attachments/pdf-extract.mjs +83 -0
- package/src/runtime/attachments/store.mjs +515 -0
- package/src/runtime/attachments/store.test.mjs +188 -0
- package/src/runtime/channels/lib/config.mjs +33 -33
- package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
- package/src/runtime/channels/lib/interaction-handlers.mjs +4 -194
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +5 -34
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +4 -70
- package/src/runtime/channels/lib/worker-main.mjs +39 -58
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +72 -211
- package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
- package/src/runtime/memory/lib/http-router.mjs +1 -6
- package/src/runtime/memory/lib/http-wire.mjs +39 -4
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/child-spawn-gate.mjs +101 -63
- package/src/runtime/shared/config.mjs +11 -24
- package/src/runtime/shared/json-metrics.mjs +94 -0
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +6 -5
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -22
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +30 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-models.mjs +12 -2
- package/src/session-runtime/quick-model-rows.mjs +38 -7
- package/src/session-runtime/quick-search-models.mjs +15 -17
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +61 -32
- package/src/session-runtime/runtime-tunables.mjs +9 -6
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +36 -15
- package/src/session-runtime/session-title.mjs +28 -4
- package/src/session-runtime/session-turn-api.mjs +55 -15
- package/src/session-runtime/settings-api.mjs +7 -7
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/channel-admin.mjs +42 -53
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +73 -58
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +190 -132
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
- package/src/standalone/session-protocol.mjs +9 -4
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +192 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +142 -63
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +4 -4
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/onboarding-steps.mjs +2 -2
- package/src/tui/app/project-picker.mjs +8 -8
- package/src/tui/app/prompt-submit.mjs +15 -17
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-handlers.mjs +7 -3
- package/src/tui/app/use-prompt-queue-history.mjs +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +403 -170
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- package/src/tui/paste-attachments.mjs +13 -7
- package/src/tui/prompt-history-store.mjs +52 -4
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +9 -5
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +28 -14
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +8 -8
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +42 -21
- package/src/tui/{engine → session}/session-api.mjs +17 -61
- package/src/tui/{engine → session}/session-flow.mjs +5 -3
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +29 -107
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +72 -78
- package/src/tui/session.mjs +22 -0
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-protocol.mjs +0 -32
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Symbol search / callers / callees / references / impact query layer over a
|
|
2
2
|
// built graph. Pure over {graph,cwd,args}; owns no cache state. Extracted
|
|
3
3
|
// verbatim from code-graph.mjs.
|
|
4
|
-
import {
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { codeGraphSourceIoAdmission } from '../../../../shared/tool-workload-gates.mjs';
|
|
5
6
|
import { _isJsLike } from './lang-predicates.mjs';
|
|
6
7
|
import { _maskNonCodeText } from './text-mask.mjs';
|
|
7
8
|
import {
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
_keywordMatchesSymbolName,
|
|
31
32
|
} from './keyword-match.mjs';
|
|
32
33
|
|
|
33
|
-
export { _formatRelated, _formatImpact, _findSymbolAcrossGraph, _resolveReferenceLanguageNode, _formatCallerReferences, _formatTransitiveCallers } from './search-references.mjs';
|
|
34
|
+
export { _formatRelated, _formatImpact, _impactSourceNodes, _findSymbolAcrossGraph, _resolveReferenceLanguageNode, _formatCallerReferences, _formatTransitiveCallers } from './search-references.mjs';
|
|
34
35
|
export function _extractCallees(graph, declHit, _cwd, { cap = 200, callerSymbol = null, language = null } = {}) {
|
|
35
36
|
if (!declHit || !_CALLEES_BRACE_LANGS.has(declHit.lang)) return [];
|
|
36
37
|
const declNode = graph.nodes.get(declHit.rel);
|
|
@@ -253,28 +254,64 @@ export function _formatCalleeRow(row) {
|
|
|
253
254
|
const enclosing = row.enclosing ? `(in ${row.enclosing})` : '(in ?)';
|
|
254
255
|
return `${row.name}\t${callsite}\t${decl}\t${enclosing}`;
|
|
255
256
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
257
|
+
const CODE_GRAPH_SOURCE_READ_CONCURRENCY = Math.max(
|
|
258
|
+
1,
|
|
259
|
+
Math.min(32, Math.floor(Number(process.env.MIXDOG_CODE_GRAPH_SOURCE_READ_CONCURRENCY) || 8)),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
export async function _prewarmSourceTextNodes(graph, nodes, {
|
|
263
|
+
concurrency = CODE_GRAPH_SOURCE_READ_CONCURRENCY,
|
|
264
|
+
readFileImpl = readFile,
|
|
265
|
+
signal = null,
|
|
266
|
+
ownerKey = null,
|
|
267
|
+
} = {}) {
|
|
268
|
+
const sourceNodes = [];
|
|
269
|
+
const seen = new Set();
|
|
270
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
271
|
+
if (!node?.rel || !node?.abs || seen.has(node.rel)) continue;
|
|
272
|
+
seen.add(node.rel);
|
|
273
|
+
sourceNodes.push(node);
|
|
274
|
+
}
|
|
263
275
|
const uncached = [];
|
|
264
|
-
for (const node of
|
|
276
|
+
for (const node of sourceNodes) {
|
|
265
277
|
const cached = graph._sourceTextCache?.get(node.rel);
|
|
266
278
|
if (!cached || cached.fingerprint !== (node.fingerprint || '')) {
|
|
267
279
|
uncached.push(node);
|
|
268
280
|
}
|
|
269
281
|
}
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
let next = 0;
|
|
283
|
+
const worker = async () => {
|
|
284
|
+
while (!signal?.aborted) {
|
|
285
|
+
const index = next++;
|
|
286
|
+
if (index >= uncached.length) return;
|
|
287
|
+
const node = uncached[index];
|
|
288
|
+
try {
|
|
289
|
+
const text = await codeGraphSourceIoAdmission.run(
|
|
290
|
+
ownerKey,
|
|
291
|
+
() => readFileImpl(node.abs, 'utf8'),
|
|
292
|
+
{ signal },
|
|
293
|
+
);
|
|
294
|
+
graph._sourceTextCache?.set(node.rel, { fingerprint: node.fingerprint || '', text });
|
|
295
|
+
} catch { /* skip unreadable/aborted file */ }
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const workerCount = Math.min(
|
|
299
|
+
Math.max(1, Math.floor(Number(concurrency) || CODE_GRAPH_SOURCE_READ_CONCURRENCY)),
|
|
300
|
+
Math.max(1, uncached.length),
|
|
301
|
+
);
|
|
302
|
+
if (uncached.length > 0) {
|
|
303
|
+
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
304
|
+
}
|
|
305
|
+
return sourceNodes;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function _prewarmReferenceSourceText(graph, symbol, language, options = {}) {
|
|
309
|
+
const candidateNodes = _lookupCandidateNodes(graph, symbol, language);
|
|
310
|
+
// Return the resolved candidate set so the immediately-following
|
|
311
|
+
// _cheapReferenceSearch (references/callers dispatch) can reuse it instead
|
|
312
|
+
// of recomputing _lookupCandidateNodes for the same (symbol, language) —
|
|
313
|
+
// which on a token-index miss is a full-graph scan run twice per symbol.
|
|
314
|
+
await _prewarmSourceTextNodes(graph, candidateNodes, options);
|
|
278
315
|
return candidateNodes;
|
|
279
316
|
}
|
|
280
317
|
|
|
@@ -14,6 +14,7 @@ export const CODE_GRAPH_TOOL_DEFS = [
|
|
|
14
14
|
limit: { type: 'number', minimum: 1, description: 'Max results.' },
|
|
15
15
|
depth: { type: 'number', minimum: 1, maximum: 5, description: 'Caller depth.' },
|
|
16
16
|
page: { type: 'number', minimum: 1, description: 'Caller page.' },
|
|
17
|
+
cwd: { type: 'string', description: 'Explicit root.' },
|
|
17
18
|
},
|
|
18
19
|
required: ['mode'],
|
|
19
20
|
additionalProperties: false,
|
|
@@ -120,8 +120,7 @@ export function scrubRuntimeRootVars(env) {
|
|
|
120
120
|
// Daemon IDENTITY leak: user-facing children must not inherit host/worker
|
|
121
121
|
// ownership or the daemon's terminal-lead PID. Internal daemon/worker
|
|
122
122
|
// spawns set every identity value explicitly after this boundary.
|
|
123
|
-
delete env.
|
|
124
|
-
delete env.MIXDOG_CHANNEL_DAEMON;
|
|
123
|
+
delete env.MIXDOG_DAEMON_HOST;
|
|
125
124
|
delete env.MIXDOG_WORKER_MODE;
|
|
126
125
|
delete env.MIXDOG_DAEMON_SPAWNED_FOR;
|
|
127
126
|
delete env.MIXDOG_SUPERVISOR_PID;
|
|
@@ -43,11 +43,11 @@ export function _resolvePwshPoolTarget({
|
|
|
43
43
|
if (Number.isFinite(n) && n >= 0) return Math.min(8, n);
|
|
44
44
|
}
|
|
45
45
|
if (platform !== 'win32') return 0;
|
|
46
|
-
// Tools now execute in the machine-global
|
|
46
|
+
// Tools now execute in the machine-global daemon. Keep client/test
|
|
47
47
|
// processes at zero by default, but let that ONE host maintain a short-lived
|
|
48
48
|
// warm burst sized to the machine instead of making every shell call pay the
|
|
49
49
|
// ~300ms pwsh startup cost.
|
|
50
|
-
if (env?.
|
|
50
|
+
if (env?.MIXDOG_DAEMON_HOST !== '1') return 0;
|
|
51
51
|
let cores = Number(parallelism);
|
|
52
52
|
if (!Number.isFinite(cores) || cores < 1) {
|
|
53
53
|
try { cores = availableParallelism(); } catch { cores = 1; }
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// mode gating, binary resolution, prewarm/idle lifecycle, and the char-indexed
|
|
3
3
|
// EDIT client. Split out of patch.mjs; behavior is identical.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Executor: NATIVE-ONLY. Every supported apply/edit case is dispatched to the
|
|
6
6
|
// mixdog-patch Rust engine via the persistent stdio server. There is NO JS
|
|
7
7
|
// apply fallback: unsupported / unsafe input returns a clean Error string.
|
|
8
8
|
|
|
@@ -144,8 +144,8 @@ function restorePatchRollbackState(snapshots, readStateScope) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// Apply one "wave" (a set of unique-target parsed entries) via the native
|
|
147
|
-
// (+ JS out-of-base) split. Returns {
|
|
148
|
-
// {
|
|
147
|
+
// (+ JS out-of-base) split. Returns { executor, text } on success or
|
|
148
|
+
// { executor, error } so the caller decides whether earlier waves already
|
|
149
149
|
// committed to disk. Extracted verbatim from the inline applyWave closure so
|
|
150
150
|
// both the default wave loop and sequence mode share identical apply
|
|
151
151
|
// semantics.
|
|
@@ -156,7 +156,7 @@ async function applyParsedWave({ parsed: wparsed, entries: wentries, headerRewri
|
|
|
156
156
|
const parsedInside = (wparsed || []).filter(
|
|
157
157
|
(entry) => !isResolvedPathOutsideBase(parsedEntryResolvedPath(entry, basePath), basePath),
|
|
158
158
|
);
|
|
159
|
-
const
|
|
159
|
+
const executor = outsideEntries.length > 0
|
|
160
160
|
? (insideEntries.length > 0 ? 'native+js-patch' : 'js-patch')
|
|
161
161
|
: 'native-patch';
|
|
162
162
|
const resultParts = [];
|
|
@@ -173,7 +173,7 @@ async function applyParsedWave({ parsed: wparsed, entries: wentries, headerRewri
|
|
|
173
173
|
signal: abortSignal,
|
|
174
174
|
parsed: parsedInside,
|
|
175
175
|
});
|
|
176
|
-
if (isPatchErrorText(nativeResult)) return {
|
|
176
|
+
if (isPatchErrorText(nativeResult)) return { executor, error: nativeResult };
|
|
177
177
|
resultParts.push(nativeResult);
|
|
178
178
|
}
|
|
179
179
|
if (outsideEntries.length > 0) {
|
|
@@ -187,10 +187,10 @@ async function applyParsedWave({ parsed: wparsed, entries: wentries, headerRewri
|
|
|
187
187
|
fuzzy,
|
|
188
188
|
readStateScope,
|
|
189
189
|
});
|
|
190
|
-
if (isPatchErrorText(jsResult)) return {
|
|
190
|
+
if (isPatchErrorText(jsResult)) return { executor, error: jsResult };
|
|
191
191
|
resultParts.push(jsResult);
|
|
192
192
|
}
|
|
193
|
-
return {
|
|
193
|
+
return { executor, text: resultParts.join('\n') };
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
// Default ordered section mode. Apply each file section in listed order,
|
|
@@ -200,7 +200,7 @@ async function applyParsedWave({ parsed: wparsed, entries: wentries, headerRewri
|
|
|
200
200
|
async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
201
201
|
const {
|
|
202
202
|
v4aConvertOpts, dryRun, fuzz, fuzzy, rejectPartial,
|
|
203
|
-
readStateScope, abortSignal, mutationPlan,
|
|
203
|
+
readStateScope, abortSignal, mutationPlan,
|
|
204
204
|
toolCallId, sessionId,
|
|
205
205
|
} = ctx;
|
|
206
206
|
|
|
@@ -328,52 +328,8 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
328
328
|
|
|
329
329
|
return withBuiltinPathLocks(lockPaths, () =>
|
|
330
330
|
withAdvisoryLocks(lockPaths, async () => {
|
|
331
|
-
// Most multi-file failures are stale context in a later, independent
|
|
332
|
-
// file. For unique targets, validate every section against current bytes
|
|
333
|
-
// before the first write. Same-target sequences retain the existing
|
|
334
|
-
// ordered conversion/rollback path because later sections may
|
|
335
|
-
// intentionally depend on earlier edits.
|
|
336
|
-
const preparedUnits = new Map();
|
|
337
|
-
const uniqueTargets = new Set(lockPaths.map(patchPathKey)).size === units.length;
|
|
338
|
-
if (!dryRun && rollbackOnFailure && uniqueTargets) {
|
|
339
|
-
for (let i = 0; i < units.length; i++) {
|
|
340
|
-
const unit = units[i];
|
|
341
|
-
if (abortSignal?.aborted) return 'Error: apply_patch aborted during preflight; no files were written';
|
|
342
|
-
let parsed;
|
|
343
|
-
try {
|
|
344
|
-
parsed = await unit.buildParsed();
|
|
345
|
-
} catch (err) {
|
|
346
|
-
return `Error: apply_patch preflight rejected section ${i + 1}/${units.length} (${unit.displayPath}); no files were written.\n${err?.message || String(err)}`;
|
|
347
|
-
}
|
|
348
|
-
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
349
|
-
preparedUnits.set(i, { parsed, wave: null });
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
let wave;
|
|
353
|
-
try {
|
|
354
|
-
const { entries, headerRewrites } = await preValidateNativeBatch(parsed, basePath);
|
|
355
|
-
wave = { parsed, entries, headerRewrites };
|
|
356
|
-
} catch (err) {
|
|
357
|
-
return `Error: apply_patch preflight rejected section ${i + 1}/${units.length} (${unit.displayPath}); no files were written.\n${err?.message || String(err)}`;
|
|
358
|
-
}
|
|
359
|
-
const checked = await applyParsedWave(wave, basePath, { ...waveOpts, dryRun: true });
|
|
360
|
-
if (checked.error) {
|
|
361
|
-
const detail = checked.error.replace(/^Error:\s*/, '');
|
|
362
|
-
return `Error: apply_patch preflight rejected section ${i + 1}/${units.length} (${unit.displayPath}); no files were written.\n${detail}`;
|
|
363
|
-
}
|
|
364
|
-
preparedUnits.set(i, { parsed, wave });
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
let rollbackSnapshots = [];
|
|
368
331
|
let uiBeforeSnapshots = [];
|
|
369
|
-
if (!dryRun &&
|
|
370
|
-
try {
|
|
371
|
-
rollbackSnapshots = capturePatchRollbackState(lockPaths);
|
|
372
|
-
uiBeforeSnapshots = rollbackSnapshots;
|
|
373
|
-
} catch (err) {
|
|
374
|
-
return `Error: ${err?.message || String(err)}`;
|
|
375
|
-
}
|
|
376
|
-
} else if (!dryRun && toolCallId && sessionId) {
|
|
332
|
+
if (!dryRun && toolCallId && sessionId) {
|
|
377
333
|
try {
|
|
378
334
|
uiBeforeSnapshots = capturePatchRollbackState(lockPaths);
|
|
379
335
|
} catch {
|
|
@@ -384,7 +340,7 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
384
340
|
const skipped = [];
|
|
385
341
|
let failed = null;
|
|
386
342
|
let failedIndex = -1;
|
|
387
|
-
let
|
|
343
|
+
let executor = 'native-patch';
|
|
388
344
|
for (let i = 0; i < units.length; i++) {
|
|
389
345
|
const unit = units[i];
|
|
390
346
|
if (failed) { skipped.push(unit.displayPath); continue; }
|
|
@@ -393,15 +349,13 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
393
349
|
failedIndex = i;
|
|
394
350
|
continue;
|
|
395
351
|
}
|
|
396
|
-
let parsed
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
352
|
+
let parsed;
|
|
353
|
+
try {
|
|
354
|
+
parsed = await unit.buildParsed();
|
|
355
|
+
} catch (err) {
|
|
356
|
+
failed = { displayPath: unit.displayPath, error: `Error: ${err?.message || String(err)}` };
|
|
357
|
+
failedIndex = i;
|
|
358
|
+
continue;
|
|
405
359
|
}
|
|
406
360
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
407
361
|
// Section produced no applicable hunks (all skipped / no-op). Nothing
|
|
@@ -409,19 +363,17 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
409
363
|
applied.push({ displayPath: unit.displayPath, text: `(no changes) ${unit.displayPath}` });
|
|
410
364
|
continue;
|
|
411
365
|
}
|
|
412
|
-
let wave
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
366
|
+
let wave;
|
|
367
|
+
try {
|
|
368
|
+
const { entries, headerRewrites } = await preValidateNativeBatch(parsed, basePath);
|
|
369
|
+
wave = { parsed, entries, headerRewrites };
|
|
370
|
+
} catch (err) {
|
|
371
|
+
failed = { displayPath: unit.displayPath, error: `Error: ${err?.message || String(err)}` };
|
|
372
|
+
failedIndex = i;
|
|
373
|
+
continue;
|
|
422
374
|
}
|
|
423
375
|
const res = await applyParsedWave(wave, basePath, waveOpts);
|
|
424
|
-
|
|
376
|
+
executor = res.executor;
|
|
425
377
|
if (res.error) {
|
|
426
378
|
failed = { displayPath: unit.displayPath, error: res.error };
|
|
427
379
|
failedIndex = i;
|
|
@@ -459,16 +411,13 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
459
411
|
}
|
|
460
412
|
const head = `apply_patch: ${verb} ${units.length} section(s)`;
|
|
461
413
|
const body = (appliedTexts ? `${head}\n${appliedTexts}` : head) + dryNote + rejectedTail;
|
|
462
|
-
return wrapPatchMutationOutput(body, mutationPlan, {
|
|
414
|
+
return wrapPatchMutationOutput(body, mutationPlan, { executor });
|
|
463
415
|
}
|
|
464
416
|
const failMsg = failed.error.replace(/^Error:\s*/, '');
|
|
465
|
-
const rollbackErrors = (!dryRun && rollbackOnFailure)
|
|
466
|
-
? restorePatchRollbackState(rollbackSnapshots, readStateScope)
|
|
467
|
-
: [];
|
|
468
417
|
if (
|
|
469
418
|
!dryRun
|
|
470
419
|
&& uiBeforeSnapshots.length > 0
|
|
471
|
-
&&
|
|
420
|
+
&& applied.length > 0
|
|
472
421
|
) {
|
|
473
422
|
registerCommittedPatchUiDiff({
|
|
474
423
|
callId: toolCallId,
|
|
@@ -480,26 +429,22 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
480
429
|
}
|
|
481
430
|
const committedPhrase = dryRun
|
|
482
431
|
? `${applied.length} earlier section(s) were validated`
|
|
483
|
-
: (
|
|
484
|
-
? (rollbackErrors.length === 0
|
|
485
|
-
? `${applied.length} earlier section(s) were applied, then all touched paths were rolled back to their pre-patch state`
|
|
486
|
-
: `${applied.length} earlier section(s) were applied, but rollback was incomplete`)
|
|
487
|
-
: `${applied.length} earlier section(s) were applied to disk (committed) and left in place`);
|
|
432
|
+
: `${applied.length} earlier section(s) were applied to disk (committed) and left in place`;
|
|
488
433
|
const lines = [
|
|
489
434
|
`Error: apply_patch sequence stopped at section ${failedIndex + 1}/${units.length} (${failed.displayPath}); `
|
|
490
435
|
+ `${committedPhrase}; ${skipped.length} later section(s) were skipped (not attempted).`,
|
|
491
436
|
];
|
|
437
|
+
if (!dryRun) {
|
|
438
|
+
lines.push('Retry only the failed and skipped sections; do not resend committed sections.');
|
|
439
|
+
}
|
|
492
440
|
if (appliedTexts) {
|
|
493
|
-
lines.push(`--- ${dryRun ? 'validated' :
|
|
441
|
+
lines.push(`--- ${dryRun ? 'validated' : 'applied (committed to disk)'} ---`, appliedTexts);
|
|
494
442
|
}
|
|
495
443
|
lines.push(`--- failed section: ${failed.displayPath} ---`, failMsg);
|
|
496
|
-
if (rollbackErrors.length > 0) {
|
|
497
|
-
lines.push('--- rollback incomplete ---', ...rollbackErrors);
|
|
498
|
-
}
|
|
499
444
|
if (skipped.length > 0) {
|
|
500
445
|
lines.push(`--- skipped (not attempted): ${skipped.join(', ')} ---`);
|
|
501
446
|
}
|
|
502
|
-
return wrapPatchMutationOutput(lines.join('\n') + dryNote + rejectedTail, mutationPlan, {
|
|
447
|
+
return wrapPatchMutationOutput(lines.join('\n') + dryNote + rejectedTail, mutationPlan, { executor });
|
|
503
448
|
}));
|
|
504
449
|
}
|
|
505
450
|
|
|
@@ -723,9 +668,41 @@ function salvageShatteredV4APatchArgs(args) {
|
|
|
723
668
|
return cleaned;
|
|
724
669
|
}
|
|
725
670
|
|
|
671
|
+
function extractInlinePatchRoot(patchText) {
|
|
672
|
+
const text = String(patchText || '');
|
|
673
|
+
const match = /^(\*\*\* Begin Patch\r?\n)\*\*\* Root:[ \t]*(.*?)\r?\n/.exec(text);
|
|
674
|
+
if (!match) return { patch: text, root: null };
|
|
675
|
+
const root = String(match[2] || '').trim();
|
|
676
|
+
if (!root) throw new Error('apply_patch: "*** Root:" requires a containing directory');
|
|
677
|
+
return {
|
|
678
|
+
patch: `${match[1]}${text.slice(match[0].length)}`,
|
|
679
|
+
root,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function isFilesystemRootSpecifier(value, cwd) {
|
|
684
|
+
const raw = String(value || '').trim();
|
|
685
|
+
if (!raw) return false;
|
|
686
|
+
// Keep Windows semantics testable on every host and reject the ambiguous
|
|
687
|
+
// drive-relative spelling (`C:`) alongside an actual drive root.
|
|
688
|
+
if (/^[A-Za-z]:(?:[\\/]?)$/.test(raw)) return true;
|
|
689
|
+
const resolved = resolveBasePath(cwd, raw);
|
|
690
|
+
return pathDirname(resolved) === resolved;
|
|
691
|
+
}
|
|
692
|
+
|
|
726
693
|
async function apply_patch(args, cwd, options = {}) {
|
|
727
694
|
args = salvageShatteredV4APatchArgs(args);
|
|
728
|
-
|
|
695
|
+
let patchStr = salvageV4AOpening((typeof args?.patch === 'string' ? args.patch : '').replace(/^\uFEFF/, ''));
|
|
696
|
+
const inlineRoot = extractInlinePatchRoot(patchStr);
|
|
697
|
+
patchStr = inlineRoot.patch;
|
|
698
|
+
if (inlineRoot.root) {
|
|
699
|
+
const argumentRoot = typeof args?.root === 'string' ? args.root.trim() : '';
|
|
700
|
+
if (argumentRoot
|
|
701
|
+
&& pathResolve(resolveBasePath(cwd, argumentRoot)) !== pathResolve(resolveBasePath(cwd, inlineRoot.root))) {
|
|
702
|
+
throw new Error('apply_patch: root argument conflicts with the "*** Root:" directive');
|
|
703
|
+
}
|
|
704
|
+
args = { ...(args || {}), root: inlineRoot.root };
|
|
705
|
+
}
|
|
729
706
|
if (!patchStr.trim()) {
|
|
730
707
|
throw new Error('apply_patch: "patch" is required (unified diff or V4A patch string)');
|
|
731
708
|
}
|
|
@@ -761,7 +738,21 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
761
738
|
// a mis-scoped working directory can never silently rewrite another tree.
|
|
762
739
|
// Editing outside stays possible, but only as a deliberate act: name the
|
|
763
740
|
// root that contains those targets in the same call.
|
|
764
|
-
const
|
|
741
|
+
const explicitRoot = typeof args?.root === 'string' && args.root.trim() ? args.root.trim() : null;
|
|
742
|
+
if (explicitRoot && isFilesystemRootSpecifier(explicitRoot, cwd)) {
|
|
743
|
+
throw new Error(`apply_patch: refusing filesystem root as write root: ${normalizeOutputPath(explicitRoot)}`);
|
|
744
|
+
}
|
|
745
|
+
const writeRoot = resolveBasePath(cwd, explicitRoot ?? args?.base_path ?? null);
|
|
746
|
+
if (explicitRoot) {
|
|
747
|
+
try {
|
|
748
|
+
await assertPathReachable(writeRoot);
|
|
749
|
+
if (!statSync(writeRoot).isDirectory()) {
|
|
750
|
+
throw new Error(`not a directory — ${normalizeOutputPath(writeRoot)}`);
|
|
751
|
+
}
|
|
752
|
+
} catch (err) {
|
|
753
|
+
throw new Error(`apply_patch: invalid write root ${normalizeOutputPath(writeRoot)}: ${err?.message || String(err)}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
765
756
|
const outsideTargets = [];
|
|
766
757
|
for (const rel of patchTargetPaths(patchStr, basePath)) {
|
|
767
758
|
const abs = isAbsolute(rel) ? pathResolve(rel) : pathResolve(basePath, rel);
|
|
@@ -773,7 +764,7 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
773
764
|
const more = shown.length > 3 ? ` (+${shown.length - 3} more)` : '';
|
|
774
765
|
throw new Error(
|
|
775
766
|
`apply_patch: ${shown.length} target(s) fall outside the write root ${normalizeOutputPath(writeRoot)}: ${head}${more}. `
|
|
776
|
-
+ 'Check the paths first; if
|
|
767
|
+
+ 'Check the paths first; if intended, set root (JSON) or add "*** Root: <containing directory>" after "*** Begin Patch" (freeform).',
|
|
777
768
|
);
|
|
778
769
|
}
|
|
779
770
|
const rejectPartial = args?.reject_partial !== false;
|
|
@@ -784,11 +775,10 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
784
775
|
let inputPatchStr = patchStr;
|
|
785
776
|
const rejectedV4AHunks = [];
|
|
786
777
|
const v4aConvertOpts = { rejectPartial, rejectedHunks: rejectedV4AHunks, fuzzy, dryRun, readStateScope };
|
|
787
|
-
// Default
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
// The legacy bulk/atomic path remains available as an explicit escape hatch.
|
|
778
|
+
// Default ordered mode mirrors Codex's lower executor: apply sections in
|
|
779
|
+
// listed order, stop at the first failure, and keep the committed prefix.
|
|
780
|
+
// The model-visible schema stays unchanged; legacy bulk/atomic rollback
|
|
781
|
+
// remains an internal escape hatch.
|
|
792
782
|
const patchMode = String(args?.mode || '').toLowerCase();
|
|
793
783
|
const legacyBulkMode = args?.sequence === false
|
|
794
784
|
|| ['atomic', 'bulk'].includes(patchMode);
|
|
@@ -796,7 +786,6 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
796
786
|
const seqOut = await applyPatchSequence(patchStr, requestedFormat, basePath, {
|
|
797
787
|
v4aConvertOpts, dryRun, fuzz, fuzzy, rejectPartial,
|
|
798
788
|
readStateScope, abortSignal, mutationPlan,
|
|
799
|
-
rollbackOnFailure: patchMode !== 'partial',
|
|
800
789
|
toolCallId: options?.toolCallId || null,
|
|
801
790
|
sessionId: options?.sessionId || null,
|
|
802
791
|
});
|
|
@@ -917,17 +906,17 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
917
906
|
if (v4aRenameOnly) {
|
|
918
907
|
const lines = formatV4ARenameSuccessLines(v4aRenameResults);
|
|
919
908
|
if (lines.length === 0) return 'Error: patch contained no applicable file sections';
|
|
920
|
-
return wrapPatchMutationOutput(`${lines.join('\n')}\n`, mutationPlan, {
|
|
909
|
+
return wrapPatchMutationOutput(`${lines.join('\n')}\n`, mutationPlan, { executor: 'v4a-rename' });
|
|
921
910
|
}
|
|
922
911
|
// Apply one wave (a set of unique targets) via applyParsedWave (native +
|
|
923
|
-
// JS split). Returns {
|
|
912
|
+
// JS split). Returns { executor, text } on success or { executor, error }.
|
|
924
913
|
const applyWave = (wave) => applyParsedWave(wave, basePath, { fuzz, rejectPartial, dryRun, fuzzy, readStateScope, abortSignal });
|
|
925
914
|
|
|
926
915
|
// Duplicate-target blocks were split into contiguous sequential groups
|
|
927
916
|
// (listed order preserved); apply them in order, each against the prior
|
|
928
917
|
// group's on-disk result.
|
|
929
918
|
const waveTexts = [];
|
|
930
|
-
let
|
|
919
|
+
let executor = 'native-patch';
|
|
931
920
|
// dry_run never writes, so a later group would validate against unchanged
|
|
932
921
|
// disk and false-fail on any block that depends on an earlier edit. Only
|
|
933
922
|
// the first group is validated under dry_run; the rest are reported as
|
|
@@ -935,9 +924,9 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
935
924
|
const groupCount = (dryRun && waveDispatch.length > 1) ? 1 : waveDispatch.length;
|
|
936
925
|
for (let w = 0; w < groupCount; w++) {
|
|
937
926
|
const res = await applyWave(waveDispatch[w]);
|
|
938
|
-
|
|
927
|
+
executor = res.executor;
|
|
939
928
|
if (res.error) {
|
|
940
|
-
if (w === 0) return wrapPatchMutationOutput(res.error, mutationPlan, {
|
|
929
|
+
if (w === 0) return wrapPatchMutationOutput(res.error, mutationPlan, { executor });
|
|
941
930
|
// A later group failed. rejectPartial makes each group all-or-nothing,
|
|
942
931
|
// so every block in groups 1..w is fully committed to disk and left in
|
|
943
932
|
// place. List them all so the caller knows the true on-disk state.
|
|
@@ -948,7 +937,7 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
948
937
|
'--- failing block ---',
|
|
949
938
|
failMsg,
|
|
950
939
|
].join('\n');
|
|
951
|
-
return wrapPatchMutationOutput(note, mutationPlan, {
|
|
940
|
+
return wrapPatchMutationOutput(note, mutationPlan, { executor });
|
|
952
941
|
}
|
|
953
942
|
waveTexts.push(res.text);
|
|
954
943
|
}
|
|
@@ -970,9 +959,9 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
970
959
|
`hunk-level rejected (rejectPartial=false, V4A): ${rejectedV4AHunks.length}`,
|
|
971
960
|
...rejectedV4AHunks.map((r) => ` REJECT ${r.file || '(unknown)'} — ${String(r.reason || '').split(';')[0].trim()}`),
|
|
972
961
|
];
|
|
973
|
-
return wrapPatchMutationOutput(`${combined}\n${tail.join('\n')}`, mutationPlan, {
|
|
962
|
+
return wrapPatchMutationOutput(`${combined}\n${tail.join('\n')}`, mutationPlan, { executor });
|
|
974
963
|
}
|
|
975
|
-
return wrapPatchMutationOutput(combined, mutationPlan, {
|
|
964
|
+
return wrapPatchMutationOutput(combined, mutationPlan, { executor });
|
|
976
965
|
};
|
|
977
966
|
|
|
978
967
|
return withBuiltinPathLocks(_lockPaths, () =>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const APPLY_PATCH_LARK_GRAMMAR = `start: begin_patch hunk+ end_patch
|
|
1
|
+
const APPLY_PATCH_LARK_GRAMMAR = `start: begin_patch root_line? hunk+ end_patch
|
|
2
2
|
begin_patch: "*** Begin Patch" LF
|
|
3
|
+
root_line: "*** Root: " filename LF
|
|
3
4
|
end_patch: "*** End Patch" LF?
|
|
4
5
|
|
|
5
6
|
hunk: add_hunk | delete_hunk | update_hunk
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// ./patch/orchestrator.mjs — apply_patch orchestration + executePatchTool
|
|
21
21
|
// + replay capture + UI-diff side-channel.
|
|
22
22
|
//
|
|
23
|
-
//
|
|
23
|
+
// Executor is unchanged: NATIVE-ONLY dispatch to the mixdog-patch Rust engine,
|
|
24
24
|
// no JS apply fallback. Public exports below match the pre-split surface.
|
|
25
25
|
|
|
26
26
|
// Side-effect import: schedules the native-patch prewarm on module load, as
|
|
@@ -258,6 +258,7 @@ export function execShellCommand({
|
|
|
258
258
|
ownerSessionId,
|
|
259
259
|
backgroundOnTimeout,
|
|
260
260
|
promotedTimeoutMs = 0,
|
|
261
|
+
backgroundDeadlineMs = 0,
|
|
261
262
|
admission = resourceAdmission,
|
|
262
263
|
}) {
|
|
263
264
|
return new Promise(async (resolve) => {
|
|
@@ -791,7 +792,12 @@ export function execShellCommand({
|
|
|
791
792
|
const stdoutPath = taskOutput.spilled ? taskOutput.stdoutPath : null;
|
|
792
793
|
const stderrPath = taskOutput.spilled ? taskOutput.stderrPath : null;
|
|
793
794
|
let job = null;
|
|
794
|
-
const
|
|
795
|
+
const elapsedMs = Math.max(0, Date.now() - _startMs);
|
|
796
|
+
const adoptedTimeoutMs = reason === 'timeout'
|
|
797
|
+
? promotedTimeoutMs
|
|
798
|
+
: (backgroundDeadlineMs > 0
|
|
799
|
+
? Math.max(1, backgroundDeadlineMs - elapsedMs)
|
|
800
|
+
: 0);
|
|
795
801
|
try {
|
|
796
802
|
job = adoptForegroundShellJob({
|
|
797
803
|
command,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded PDF inspection/text extraction shared by prompt intake and `read`.
|
|
3
|
+
*
|
|
4
|
+
* Native PDF-capable providers receive the original content-addressed file.
|
|
5
|
+
* OpenAI-compatible providers without a document contract receive page-ordered
|
|
6
|
+
* text extracted here, so they never see an unsupported inline Base64 block.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_MAX_PAGES = 100;
|
|
9
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
function boundedUtf8(value, maxBytes) {
|
|
12
|
+
const buffer = Buffer.from(String(value || ''), 'utf8');
|
|
13
|
+
if (buffer.length <= maxBytes) return { text: buffer.toString('utf8'), bytes: buffer.length, truncated: false };
|
|
14
|
+
if (maxBytes <= 0) return { text: '', bytes: 0, truncated: true };
|
|
15
|
+
const text = buffer.subarray(0, maxBytes).toString('utf8').replace(/\uFFFD+$/g, '');
|
|
16
|
+
return { text, bytes: Buffer.byteLength(text, 'utf8'), truncated: true };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function inspectPdfBuffer(buffer, {
|
|
20
|
+
extractText = false,
|
|
21
|
+
maxPages = DEFAULT_MAX_PAGES,
|
|
22
|
+
maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
|
|
23
|
+
pageRange = null,
|
|
24
|
+
} = {}) {
|
|
25
|
+
if (!Buffer.isBuffer(buffer) || buffer.length === 0) throw new TypeError('PDF payload is empty');
|
|
26
|
+
const { getDocumentProxy } = await import('unpdf');
|
|
27
|
+
const pdf = await getDocumentProxy(new Uint8Array(buffer));
|
|
28
|
+
try {
|
|
29
|
+
const pageCount = Math.max(0, Number(pdf?.numPages) || 0);
|
|
30
|
+
const pageLimit = Number.isFinite(Number(maxPages)) ? Math.max(1, Math.floor(Number(maxPages))) : Infinity;
|
|
31
|
+
if (pageCount > pageLimit) {
|
|
32
|
+
throw new RangeError(`PDF has ${pageCount} pages; maximum supported attachment is ${pageLimit} pages`);
|
|
33
|
+
}
|
|
34
|
+
if (!extractText) return { pageCount, text: '', truncated: false };
|
|
35
|
+
|
|
36
|
+
const from = Math.max(1, Number(pageRange?.from) || 1);
|
|
37
|
+
const to = Math.min(pageCount, Math.max(from, Number(pageRange?.to) || pageCount));
|
|
38
|
+
const byteLimit = Math.max(1, Number(maxOutputBytes) || DEFAULT_MAX_OUTPUT_BYTES);
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let bytes = 0;
|
|
41
|
+
let truncated = false;
|
|
42
|
+
for (let pageNumber = from; pageNumber <= to; pageNumber += 1) {
|
|
43
|
+
const page = await pdf.getPage(pageNumber);
|
|
44
|
+
try {
|
|
45
|
+
const content = await page.getTextContent();
|
|
46
|
+
const body = (content?.items || [])
|
|
47
|
+
.map((item) => typeof item?.str === 'string' ? item.str : '')
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join(' ')
|
|
50
|
+
.trim();
|
|
51
|
+
const block = `--- Page ${pageNumber} ---\n${body || '(no extractable text on this page)'}`;
|
|
52
|
+
const separatorBytes = chunks.length ? 2 : 0;
|
|
53
|
+
const remaining = byteLimit - bytes - separatorBytes;
|
|
54
|
+
if (remaining <= 0) {
|
|
55
|
+
truncated = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const bounded = boundedUtf8(block, remaining);
|
|
59
|
+
if (chunks.length) {
|
|
60
|
+
chunks.push('\n\n');
|
|
61
|
+
bytes += 2;
|
|
62
|
+
}
|
|
63
|
+
chunks.push(bounded.text);
|
|
64
|
+
bytes += bounded.bytes;
|
|
65
|
+
if (bounded.truncated) {
|
|
66
|
+
truncated = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
try { page.cleanup?.(); } catch {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (truncated) {
|
|
74
|
+
const suffix = '\n\n... [PDF text truncated to the prompt input budget]';
|
|
75
|
+
const room = Math.max(0, byteLimit - Buffer.byteLength(suffix, 'utf8'));
|
|
76
|
+
const bounded = boundedUtf8(chunks.join(''), room);
|
|
77
|
+
return { pageCount, text: `${bounded.text}${suffix}`, truncated: true };
|
|
78
|
+
}
|
|
79
|
+
return { pageCount, text: chunks.join(''), truncated: false };
|
|
80
|
+
} finally {
|
|
81
|
+
try { await pdf.destroy?.(); } catch {}
|
|
82
|
+
}
|
|
83
|
+
}
|