mixdog 0.9.32 → 0.9.34
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/package.json +4 -2
- package/scripts/compact-trigger-migration-smoke.mjs +37 -31
- package/scripts/provider-toolcall-test.mjs +535 -36
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +2 -3
- package/src/repl.mjs +9 -1
- package/src/rules/shared/01-tool.md +4 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +21 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +130 -18
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -12
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +131 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +102 -26
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +194 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +39 -6
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +15 -23
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +16 -8
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +50 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +53 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +270 -47
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +7 -1
- package/src/runtime/channels/lib/owned-runtime.mjs +5 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +18 -3
- package/src/runtime/memory/lib/memory-embed.mjs +89 -8
- package/src/session-runtime/context-status.mjs +7 -6
- package/src/session-runtime/mcp-glue.mjs +5 -5
- package/src/session-runtime/plugin-mcp.mjs +36 -1
- package/src/session-runtime/resource-api.mjs +14 -9
- package/src/session-runtime/runtime-core.mjs +0 -10
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/model-picker.mjs +48 -12
- package/src/tui/app/route-pickers.mjs +4 -0
- package/src/tui/app/slash-dispatch.mjs +6 -1
- package/src/tui/app/use-transcript-window.mjs +13 -1
- package/src/tui/components/StatusLine.jsx +5 -5
- package/src/tui/dist/index.mjs +46 -15
- package/src/ui/statusline.mjs +4 -10
- package/src/vendor/statusline/bin/statusline-route.mjs +4 -5
- package/src/vendor/statusline/src/gateway/route-meta.mjs +3 -2
|
@@ -11,7 +11,7 @@ import { withBuiltinPathLocks } from '../builtin.mjs';
|
|
|
11
11
|
import { withAdvisoryLocks } from '../builtin/advisory-lock.mjs';
|
|
12
12
|
import { wrapMutationRouteOutput } from '../mutation-planner.mjs';
|
|
13
13
|
import { getPluginData } from '../../config.mjs';
|
|
14
|
-
import { prepareInput, isV4APatchInput, hasUnifiedBareV4AHunk, canFallbackCountedUnified, parseV4APatch, isCompactedPlaceholderPatch, salvageV4AOpening } from './parsing.mjs';
|
|
14
|
+
import { prepareInput, isV4APatchInput, hasUnifiedBareV4AHunk, canFallbackCountedUnified, parseV4APatch, parseUnifiedBareV4APatch, parseUnifiedCountedAsV4APatch, isCompactedPlaceholderPatch, salvageV4AOpening } from './parsing.mjs';
|
|
15
15
|
import {
|
|
16
16
|
resolveBasePath,
|
|
17
17
|
resolveV4AEntryPath,
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
renderParsedUnifiedPatch,
|
|
22
22
|
rewriteHeaderPaths,
|
|
23
23
|
preValidateNativeBatch,
|
|
24
|
+
classifyEntry,
|
|
25
|
+
stripDiffPrefix,
|
|
24
26
|
} from './paths.mjs';
|
|
25
27
|
import { ensureNativePatchBinaryAvailable } from './native-server.mjs';
|
|
26
28
|
import { assertPathReachable } from '../builtin/fs-reachability.mjs';
|
|
@@ -33,12 +35,264 @@ import {
|
|
|
33
35
|
convertV4ASectionsToUnifiedPatch,
|
|
34
36
|
convertUnifiedBareV4AToUnifiedPatch,
|
|
35
37
|
convertUnifiedCountedToUnifiedPatchViaV4A,
|
|
38
|
+
isV4ARenameSection,
|
|
36
39
|
} from './v4a-convert.mjs';
|
|
37
40
|
|
|
38
41
|
function isPatchErrorText(text) {
|
|
39
42
|
return /^Error:/i.test(String(text ?? '').trimStart());
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
// Apply one "wave" (a set of unique-target parsed entries) via the native
|
|
46
|
+
// (+ JS out-of-base) split. Returns { backend, text } on success or
|
|
47
|
+
// { backend, error } so the caller decides whether earlier waves already
|
|
48
|
+
// committed to disk. Extracted verbatim from the inline applyWave closure so
|
|
49
|
+
// both the default wave loop and sequence mode share identical apply
|
|
50
|
+
// semantics.
|
|
51
|
+
async function applyParsedWave({ parsed: wparsed, entries: wentries, headerRewrites: whr }, basePath, opts) {
|
|
52
|
+
const { fuzz, rejectPartial, dryRun, fuzzy, readStateScope, abortSignal } = opts;
|
|
53
|
+
const insideEntries = wentries.filter((entry) => !isResolvedPathOutsideBase(entry.fullPath, basePath));
|
|
54
|
+
const outsideEntries = wentries.filter((entry) => isResolvedPathOutsideBase(entry.fullPath, basePath));
|
|
55
|
+
const parsedInside = (wparsed || []).filter(
|
|
56
|
+
(entry) => !isResolvedPathOutsideBase(parsedEntryResolvedPath(entry, basePath), basePath),
|
|
57
|
+
);
|
|
58
|
+
const backend = outsideEntries.length > 0
|
|
59
|
+
? (insideEntries.length > 0 ? 'native+js-patch' : 'js-patch')
|
|
60
|
+
: 'native-patch';
|
|
61
|
+
const resultParts = [];
|
|
62
|
+
if (insideEntries.length > 0) {
|
|
63
|
+
const nativePatchStr = rewriteHeaderPaths(renderParsedUnifiedPatch(parsedInside), whr);
|
|
64
|
+
const nativeResult = await dispatchNativePatch({
|
|
65
|
+
entries: insideEntries,
|
|
66
|
+
basePath,
|
|
67
|
+
nativePatchStr,
|
|
68
|
+
fuzz,
|
|
69
|
+
rejectPartial,
|
|
70
|
+
dryRun,
|
|
71
|
+
readStateScope,
|
|
72
|
+
signal: abortSignal,
|
|
73
|
+
parsed: parsedInside,
|
|
74
|
+
});
|
|
75
|
+
if (isPatchErrorText(nativeResult)) return { backend, error: nativeResult };
|
|
76
|
+
resultParts.push(nativeResult);
|
|
77
|
+
}
|
|
78
|
+
if (outsideEntries.length > 0) {
|
|
79
|
+
// Out-of-base targets are applied via the JS dispatcher (no base-path
|
|
80
|
+
// confinement); write permission is enforced at the hook layer.
|
|
81
|
+
const jsResult = await dispatchJsPatchEntries({
|
|
82
|
+
rows: outsideEntries,
|
|
83
|
+
parsed: wparsed,
|
|
84
|
+
basePath,
|
|
85
|
+
dryRun,
|
|
86
|
+
fuzzy,
|
|
87
|
+
readStateScope,
|
|
88
|
+
});
|
|
89
|
+
if (isPatchErrorText(jsResult)) return { backend, error: jsResult };
|
|
90
|
+
resultParts.push(jsResult);
|
|
91
|
+
}
|
|
92
|
+
return { backend, text: resultParts.join('\n') };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Default ordered section mode. Apply each file section in listed order,
|
|
96
|
+
// converting every V4A section against the CURRENT on-disk state (i.e. after
|
|
97
|
+
// all earlier sections have committed), and stop at the first section that
|
|
98
|
+
// fails. Reports applied / failed / skipped reflecting true disk state.
|
|
99
|
+
async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
100
|
+
const {
|
|
101
|
+
v4aConvertOpts, dryRun, fuzz, fuzzy, rejectPartial,
|
|
102
|
+
readStateScope, abortSignal, mutationPlan,
|
|
103
|
+
} = ctx;
|
|
104
|
+
|
|
105
|
+
// Build the ordered section "units". Each unit resolves its own parsed
|
|
106
|
+
// unified entry lazily via buildParsed(), so a V4A section is converted
|
|
107
|
+
// only when it is its turn — against disk mutated by the earlier sections.
|
|
108
|
+
const units = [];
|
|
109
|
+
// Build a per-section unit from a V4A-style section. Conversion is deferred
|
|
110
|
+
// into buildParsed() so each section is resolved against the disk state the
|
|
111
|
+
// earlier sections left behind — a later section that fails to convert/apply
|
|
112
|
+
// never blocks the earlier ones from committing (ordered-stop). This is
|
|
113
|
+
// shared by the V4A path AND the bare-@@/counted-unified fallbacks, so those
|
|
114
|
+
// salvageable formats keep ordered-stop instead of aborting whole-patch.
|
|
115
|
+
const pushSectionUnit = (section) => {
|
|
116
|
+
const displayPath = normalizeOutputPath(section.path);
|
|
117
|
+
const fullPath = resolveV4AEntryPath(basePath, section.path);
|
|
118
|
+
// A V4A rename cannot be sequenced, but ordered-stop requires we still
|
|
119
|
+
// apply every section BEFORE it and surface the rename as the failed
|
|
120
|
+
// section — never abort before earlier sections commit. Defer the
|
|
121
|
+
// rejection into buildParsed() so the loop marks it failed and keeps
|
|
122
|
+
// earlier commits.
|
|
123
|
+
if (isV4ARenameSection(section)) {
|
|
124
|
+
units.push({
|
|
125
|
+
displayPath,
|
|
126
|
+
fullPath,
|
|
127
|
+
buildParsed: async () => {
|
|
128
|
+
throw new Error('sequence mode does not support V4A rename (*** Move to:) sections; apply the rename in a separate non-sequence patch');
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
units.push({
|
|
134
|
+
displayPath,
|
|
135
|
+
fullPath,
|
|
136
|
+
// Honor reject_partial via v4aConvertOpts: a bad hunk throws under
|
|
137
|
+
// reject_partial=true (section fails → sequence stops) but is recorded in
|
|
138
|
+
// rejectedHunks and skipped under reject_partial=false.
|
|
139
|
+
buildParsed: async () => {
|
|
140
|
+
const unified = await convertV4ASectionsToUnifiedPatch([section], basePath, v4aConvertOpts);
|
|
141
|
+
return parsePatch(prepareInput(unified));
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
if (isV4APatchInput(patchStr, requestedFormat)) {
|
|
146
|
+
let allSections;
|
|
147
|
+
try {
|
|
148
|
+
allSections = parseV4APatch(patchStr);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
throw new Error(`apply_patch: V4A parse failed — ${err?.message || String(err)}`);
|
|
151
|
+
}
|
|
152
|
+
for (const section of allSections) pushSectionUnit(section);
|
|
153
|
+
} else if (requestedFormat !== 'unified' && hasUnifiedBareV4AHunk(patchStr)) {
|
|
154
|
+
// Bare `@@` V4A hunks in a unified body: parse to sections and defer each
|
|
155
|
+
// section's conversion into its own unit (ordered-stop preserved) rather
|
|
156
|
+
// than converting the whole patch up front.
|
|
157
|
+
let sections;
|
|
158
|
+
try {
|
|
159
|
+
sections = parseUnifiedBareV4APatch(patchStr);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
throw new Error(`apply_patch: bare @@ parse failed — ${err?.message || String(err)}`);
|
|
162
|
+
}
|
|
163
|
+
for (const section of sections) pushSectionUnit(section);
|
|
164
|
+
} else {
|
|
165
|
+
let parsed = null;
|
|
166
|
+
let countedSections = null;
|
|
167
|
+
try {
|
|
168
|
+
parsed = parsePatch(prepareInput(patchStr));
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (!canFallbackCountedUnified(patchStr, requestedFormat, err)) {
|
|
171
|
+
throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; prefer V4A envelope for multi-hunk edits (no @@ line counts)`);
|
|
172
|
+
}
|
|
173
|
+
// Counted-unified (`@@ -a,b +c,d @@`) that parsePatch rejects: parse to
|
|
174
|
+
// V4A-style sections and defer per-section conversion — same ordered-stop
|
|
175
|
+
// guarantee as the V4A path (no whole-patch up-front convert).
|
|
176
|
+
try {
|
|
177
|
+
countedSections = parseUnifiedCountedAsV4APatch(patchStr);
|
|
178
|
+
} catch (fallbackErr) {
|
|
179
|
+
throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; V4A fallback failed — ${fallbackErr?.message || String(fallbackErr)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (countedSections) {
|
|
183
|
+
for (const section of countedSections) pushSectionUnit(section);
|
|
184
|
+
} else {
|
|
185
|
+
for (const entry of parsed || []) {
|
|
186
|
+
const kind = classifyEntry(entry);
|
|
187
|
+
const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
|
|
188
|
+
units.push({
|
|
189
|
+
displayPath: normalizeOutputPath(stripDiffPrefix(headerName || '')),
|
|
190
|
+
fullPath: parsedEntryResolvedPath(entry, basePath),
|
|
191
|
+
buildParsed: async () => [entry],
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (units.length === 0) return 'Error: patch contained no file sections';
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await ensureNativePatchBinaryAvailable();
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return `Error: ${err?.message || String(err)}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const lockPaths = [...new Set(units.map((u) => u.fullPath))];
|
|
205
|
+
const waveOpts = { fuzz, rejectPartial, dryRun, fuzzy, readStateScope, abortSignal };
|
|
206
|
+
|
|
207
|
+
return withBuiltinPathLocks(lockPaths, () =>
|
|
208
|
+
withAdvisoryLocks(lockPaths, async () => {
|
|
209
|
+
const applied = [];
|
|
210
|
+
const skipped = [];
|
|
211
|
+
let failed = null;
|
|
212
|
+
let failedIndex = -1;
|
|
213
|
+
let backend = 'native-patch';
|
|
214
|
+
for (let i = 0; i < units.length; i++) {
|
|
215
|
+
const unit = units[i];
|
|
216
|
+
if (failed) { skipped.push(unit.displayPath); continue; }
|
|
217
|
+
if (abortSignal?.aborted) {
|
|
218
|
+
failed = { displayPath: unit.displayPath, error: 'Error: apply_patch aborted' };
|
|
219
|
+
failedIndex = i;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
let parsed;
|
|
223
|
+
try {
|
|
224
|
+
parsed = await unit.buildParsed();
|
|
225
|
+
} catch (err) {
|
|
226
|
+
failed = { displayPath: unit.displayPath, error: `Error: ${err?.message || String(err)}` };
|
|
227
|
+
failedIndex = i;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
231
|
+
// Section produced no applicable hunks (all skipped / no-op). Nothing
|
|
232
|
+
// to commit; record and continue.
|
|
233
|
+
applied.push({ displayPath: unit.displayPath, text: `(no changes) ${unit.displayPath}` });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
let wave;
|
|
237
|
+
try {
|
|
238
|
+
const { entries, headerRewrites } = await preValidateNativeBatch(parsed, basePath);
|
|
239
|
+
wave = { parsed, entries, headerRewrites };
|
|
240
|
+
} catch (err) {
|
|
241
|
+
failed = { displayPath: unit.displayPath, error: `Error: ${err?.message || String(err)}` };
|
|
242
|
+
failedIndex = i;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const res = await applyParsedWave(wave, basePath, waveOpts);
|
|
246
|
+
backend = res.backend;
|
|
247
|
+
if (res.error) {
|
|
248
|
+
failed = { displayPath: unit.displayPath, error: res.error };
|
|
249
|
+
failedIndex = i;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
applied.push({ displayPath: unit.displayPath, text: res.text });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const verb = dryRun ? 'validated' : 'applied';
|
|
256
|
+
const dryNote = (dryRun && units.length > 1)
|
|
257
|
+
? '\n(dry_run: each section validated against unchanged disk; a section depending on an earlier section\'s edits may report a false failure)'
|
|
258
|
+
: '';
|
|
259
|
+
const appliedTexts = applied.map((a) => a.text).filter(Boolean).join('\n');
|
|
260
|
+
// reject_partial=false may have skipped individual V4A hunks in ANY
|
|
261
|
+
// already-processed section; surface them in BOTH the success and failure
|
|
262
|
+
// reports so the reported disk state stays complete even when a later
|
|
263
|
+
// section fails.
|
|
264
|
+
const rejected = Array.isArray(v4aConvertOpts?.rejectedHunks) ? v4aConvertOpts.rejectedHunks : [];
|
|
265
|
+
const rejectedTail = rejected.length > 0
|
|
266
|
+
? '\n' + [
|
|
267
|
+
'',
|
|
268
|
+
`hunk-level rejected (rejectPartial=false, V4A): ${rejected.length}`,
|
|
269
|
+
...rejected.map((r) => ` REJECT ${r.file || '(unknown)'} — ${String(r.reason || '').split(';')[0].trim()}`),
|
|
270
|
+
].join('\n')
|
|
271
|
+
: '';
|
|
272
|
+
if (!failed) {
|
|
273
|
+
const head = `apply_patch sequence: ${verb} ${units.length} section(s) in listed order`;
|
|
274
|
+
const body = (appliedTexts ? `${head}\n${appliedTexts}` : head) + dryNote + rejectedTail;
|
|
275
|
+
return wrapPatchMutationOutput(body, mutationPlan, { backend });
|
|
276
|
+
}
|
|
277
|
+
const failMsg = failed.error.replace(/^Error:\s*/, '');
|
|
278
|
+
const committedPhrase = dryRun
|
|
279
|
+
? `${applied.length} earlier section(s) were validated`
|
|
280
|
+
: `${applied.length} earlier section(s) were applied to disk (committed) and left in place`;
|
|
281
|
+
const lines = [
|
|
282
|
+
`Error: apply_patch sequence stopped at section ${failedIndex + 1}/${units.length} (${failed.displayPath}); `
|
|
283
|
+
+ `${committedPhrase}; ${skipped.length} later section(s) were skipped (not attempted).`,
|
|
284
|
+
];
|
|
285
|
+
if (appliedTexts) {
|
|
286
|
+
lines.push(`--- ${dryRun ? 'validated' : 'applied (committed to disk)'} ---`, appliedTexts);
|
|
287
|
+
}
|
|
288
|
+
lines.push(`--- failed section: ${failed.displayPath} ---`, failMsg);
|
|
289
|
+
if (skipped.length > 0) {
|
|
290
|
+
lines.push(`--- skipped (not attempted): ${skipped.join(', ')} ---`);
|
|
291
|
+
}
|
|
292
|
+
return wrapPatchMutationOutput(lines.join('\n') + dryNote + rejectedTail, mutationPlan, { backend });
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
|
|
42
296
|
const APPLY_PATCH_UI_DIFF_MAX_CHARS = 64 * 1024;
|
|
43
297
|
// Reject oversized patch bodies before parse / native Buffer.from
|
|
44
298
|
// (native-server.mjs Buffer.from(patchText)). A few MB covers any legitimate
|
|
@@ -82,7 +336,7 @@ function wrapPatchMutationOutput(text, plan, extras = {}) {
|
|
|
82
336
|
return wrapMutationRouteOutput(text, plan, extras);
|
|
83
337
|
}
|
|
84
338
|
|
|
85
|
-
const APPLY_PATCH_SCHEMA_KEYS = new Set(['patch', 'format', 'base_path', 'dry_run', 'reject_partial', 'fuzzy']);
|
|
339
|
+
const APPLY_PATCH_SCHEMA_KEYS = new Set(['patch', 'format', 'base_path', 'dry_run', 'reject_partial', 'fuzzy', 'sequence', 'mode']);
|
|
86
340
|
function salvageShatteredV4APatchArgs(args) {
|
|
87
341
|
if (!args || typeof args !== 'object') return args;
|
|
88
342
|
const rawPatch = typeof args.patch === 'string' ? args.patch : '';
|
|
@@ -142,6 +396,17 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
142
396
|
let inputPatchStr = patchStr;
|
|
143
397
|
const rejectedV4AHunks = [];
|
|
144
398
|
const v4aConvertOpts = { rejectPartial, rejectedHunks: rejectedV4AHunks, fuzzy, dryRun, readStateScope };
|
|
399
|
+
// Default internal ordered mode: apply sections in listed order, stop at the
|
|
400
|
+
// first failure, and report applied/failed/skipped with true disk state.
|
|
401
|
+
// The legacy bulk/atomic path remains available as an explicit escape hatch.
|
|
402
|
+
const legacyBulkMode = args?.sequence === false
|
|
403
|
+
|| ['atomic', 'bulk'].includes(String(args?.mode || '').toLowerCase());
|
|
404
|
+
if (!legacyBulkMode) {
|
|
405
|
+
return applyPatchSequence(patchStr, requestedFormat, basePath, {
|
|
406
|
+
v4aConvertOpts, dryRun, fuzz, fuzzy, rejectPartial,
|
|
407
|
+
readStateScope, abortSignal, mutationPlan,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
145
410
|
let v4aRenamePlan = null;
|
|
146
411
|
if (isV4APatchInput(patchStr, requestedFormat)) {
|
|
147
412
|
try {
|
|
@@ -243,51 +508,9 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
243
508
|
if (lines.length === 0) return 'Error: patch contained no applicable file sections';
|
|
244
509
|
return wrapPatchMutationOutput(`${lines.join('\n')}\n`, mutationPlan, { backend: 'v4a-rename' });
|
|
245
510
|
}
|
|
246
|
-
// Apply one wave (a set of unique targets) via
|
|
247
|
-
// split. Returns { backend, text } on success or { backend, error }
|
|
248
|
-
|
|
249
|
-
const applyWave = async ({ parsed: wparsed, entries: wentries, headerRewrites: whr }) => {
|
|
250
|
-
const insideEntries = wentries.filter((entry) => !isResolvedPathOutsideBase(entry.fullPath, basePath));
|
|
251
|
-
const outsideEntries = wentries.filter((entry) => isResolvedPathOutsideBase(entry.fullPath, basePath));
|
|
252
|
-
const parsedInside = (wparsed || []).filter(
|
|
253
|
-
(entry) => !isResolvedPathOutsideBase(parsedEntryResolvedPath(entry, basePath), basePath),
|
|
254
|
-
);
|
|
255
|
-
const backend = outsideEntries.length > 0
|
|
256
|
-
? (insideEntries.length > 0 ? 'native+js-patch' : 'js-patch')
|
|
257
|
-
: 'native-patch';
|
|
258
|
-
const resultParts = [];
|
|
259
|
-
if (insideEntries.length > 0) {
|
|
260
|
-
const nativePatchStr = rewriteHeaderPaths(renderParsedUnifiedPatch(parsedInside), whr);
|
|
261
|
-
const nativeResult = await dispatchNativePatch({
|
|
262
|
-
entries: insideEntries,
|
|
263
|
-
basePath,
|
|
264
|
-
nativePatchStr,
|
|
265
|
-
fuzz,
|
|
266
|
-
rejectPartial,
|
|
267
|
-
dryRun,
|
|
268
|
-
readStateScope,
|
|
269
|
-
signal: abortSignal,
|
|
270
|
-
parsed: parsedInside,
|
|
271
|
-
});
|
|
272
|
-
if (isPatchErrorText(nativeResult)) return { backend, error: nativeResult };
|
|
273
|
-
resultParts.push(nativeResult);
|
|
274
|
-
}
|
|
275
|
-
if (outsideEntries.length > 0) {
|
|
276
|
-
// Out-of-base targets are applied via the JS dispatcher (no base-path
|
|
277
|
-
// confinement); write permission is enforced at the hook layer.
|
|
278
|
-
const jsResult = await dispatchJsPatchEntries({
|
|
279
|
-
rows: outsideEntries,
|
|
280
|
-
parsed: wparsed,
|
|
281
|
-
basePath,
|
|
282
|
-
dryRun,
|
|
283
|
-
fuzzy,
|
|
284
|
-
readStateScope,
|
|
285
|
-
});
|
|
286
|
-
if (isPatchErrorText(jsResult)) return { backend, error: jsResult };
|
|
287
|
-
resultParts.push(jsResult);
|
|
288
|
-
}
|
|
289
|
-
return { backend, text: resultParts.join('\n') };
|
|
290
|
-
};
|
|
511
|
+
// Apply one wave (a set of unique targets) via applyParsedWave (native +
|
|
512
|
+
// JS split). Returns { backend, text } on success or { backend, error }.
|
|
513
|
+
const applyWave = (wave) => applyParsedWave(wave, basePath, { fuzz, rejectPartial, dryRun, fuzzy, readStateScope, abortSignal });
|
|
291
514
|
|
|
292
515
|
// Duplicate-target blocks were split into contiguous sequential groups
|
|
293
516
|
// (listed order preserved); apply them in order, each against the prior
|
|
@@ -26,7 +26,7 @@ export const PATCH_TOOL_DEFS = [
|
|
|
26
26
|
name: 'apply_patch',
|
|
27
27
|
title: 'Mixdog Apply Patch',
|
|
28
28
|
annotations: { title: 'Mixdog Apply Patch', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, compressible: false, compressibleLossless: true },
|
|
29
|
-
description: 'Apply file patches.
|
|
29
|
+
description: 'Apply file patches. Put known edits in one patch, not multiple turns; sections run in listed order.',
|
|
30
30
|
freeformDescription: APPLY_PATCH_FREEFORM_DESCRIPTION,
|
|
31
31
|
freeform: {
|
|
32
32
|
type: 'grammar',
|
|
@@ -36,10 +36,12 @@ export const PATCH_TOOL_DEFS = [
|
|
|
36
36
|
inputSchema: {
|
|
37
37
|
type: 'object',
|
|
38
38
|
properties: {
|
|
39
|
-
patch: { type: 'string', description: 'Patch text. V4A preferred
|
|
39
|
+
patch: { type: 'string', description: 'Patch text. V4A preferred; include all known edits in listed order.' },
|
|
40
40
|
format: { type: 'string', enum: ['unified', 'v4a'], description: 'Auto-detected.' },
|
|
41
41
|
base_path: { type: 'string', description: 'Repo root.' },
|
|
42
42
|
dry_run: { type: 'boolean', description: 'Default false. true = validate only, no write.' },
|
|
43
|
+
fuzzy: { type: 'boolean', description: 'Default true. Allows limited context fuzz.' },
|
|
44
|
+
reject_partial: { type: 'boolean', description: 'Default true. false allows V4A hunk-level rejects.' },
|
|
43
45
|
},
|
|
44
46
|
required: ['patch'],
|
|
45
47
|
},
|
|
@@ -157,7 +157,13 @@ class OutputForwarder {
|
|
|
157
157
|
const noSendEvidence = (!Number.isFinite(sentCount) || sentCount <= 0)
|
|
158
158
|
&& (!Number.isFinite(lastSentTime) || lastSentTime <= 0)
|
|
159
159
|
&& !hasSentHash;
|
|
160
|
-
|
|
160
|
+
// Recover a genuinely-unsynced tail ONLY for the SAME transcript we are
|
|
161
|
+
// resuming (a reply this session generated but never delivered — crash
|
|
162
|
+
// recovery). NEVER pull the cursor back for a DIFFERENT transcript: on
|
|
163
|
+
// connect/change/rebind that would forward the newly-bound (possibly
|
|
164
|
+
// prior/unrelated) transcript's old tail. A fresh binding must only
|
|
165
|
+
// forward outputs created after the binding, so leave the cursor at EOF.
|
|
166
|
+
if (sameTranscript && noSendEvidence) {
|
|
161
167
|
const tailOffset = this.findLastAssistantTextOffset(currentSize);
|
|
162
168
|
if (tailOffset >= 0 && tailOffset < currentSize) {
|
|
163
169
|
fileSize = tailOffset;
|
|
@@ -156,8 +156,11 @@ async function selfHealOwnedRuntime(options = {}) {
|
|
|
156
156
|
process.stderr.write(`mixdog: self-heal getBackend() reset failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
157
157
|
});
|
|
158
158
|
}
|
|
159
|
-
//
|
|
160
|
-
|
|
159
|
+
// Do NOT nudge forwardNewText() here. bindPersistedTranscriptIfAny() above
|
|
160
|
+
// already forwards the same-session catch-up from the persisted cursor; an
|
|
161
|
+
// extra drain risks surfacing the old tail of a freshly (re)bound transcript
|
|
162
|
+
// on connect/change/rebind. Only outputs created after the new binding
|
|
163
|
+
// should be forwarded, and the normal watch/poll path handles those.
|
|
161
164
|
} finally {
|
|
162
165
|
_ownedRuntimeSelfHealing = false;
|
|
163
166
|
}
|
|
@@ -50,6 +50,21 @@ const MODEL_CACHE_DIR = join(resolvePluginData(), 'memory-models')
|
|
|
50
50
|
const _envIdleMs = Number(process.env.MIXDOG_EMBED_IDLE_TIMEOUT_MS)
|
|
51
51
|
const IDLE_TIMEOUT_MS = Number.isFinite(_envIdleMs) && _envIdleMs >= 0 ? _envIdleMs : 0
|
|
52
52
|
|
|
53
|
+
// Defensive belt against giant model inputs. Callers should already bound text
|
|
54
|
+
// (see memory-embed truncateForEmbed), but the worker is the last line before
|
|
55
|
+
// ORT: an unbounded string builds a [batch, seq] tensor large enough to trigger
|
|
56
|
+
// multi-GB allocations and an input-tensor dump in the ORT error path. Cap each
|
|
57
|
+
// text to a char budget and ask the tokenizer to truncate to the model window.
|
|
58
|
+
const _envWorkerMaxChars = Number(process.env.MIXDOG_EMBED_MAX_CHARS)
|
|
59
|
+
const WORKER_MAX_CHARS = (Number.isFinite(_envWorkerMaxChars) && _envWorkerMaxChars > 0)
|
|
60
|
+
? Math.floor(_envWorkerMaxChars)
|
|
61
|
+
: 8000
|
|
62
|
+
const EXTRACT_OPTS = { pooling: 'mean', normalize: true, truncation: true }
|
|
63
|
+
function capEmbedText(text) {
|
|
64
|
+
if (typeof text !== 'string') return ''
|
|
65
|
+
return text.length > WORKER_MAX_CHARS ? text.slice(0, WORKER_MAX_CHARS) : text
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
let extractorPromise = null
|
|
54
69
|
let configuredDtype = DEFAULT_DTYPE
|
|
55
70
|
let _device = 'cpu'
|
|
@@ -262,7 +277,7 @@ async function processMessage(msg) {
|
|
|
262
277
|
break
|
|
263
278
|
}
|
|
264
279
|
const t0 = Date.now()
|
|
265
|
-
const output = await extractor(texts,
|
|
280
|
+
const output = await extractor(texts.map(capEmbedText), EXTRACT_OPTS)
|
|
266
281
|
const wallMs = Date.now() - t0
|
|
267
282
|
if (!output.data?.length) throw new Error(`embed-batch output missing data (model=${MODEL_ID})`)
|
|
268
283
|
const total = output.data.length
|
|
@@ -284,7 +299,7 @@ async function processMessage(msg) {
|
|
|
284
299
|
resetIdleTimer()
|
|
285
300
|
const extractor = await loadExtractor()
|
|
286
301
|
const t0 = Date.now()
|
|
287
|
-
const output = await extractor(msg.text,
|
|
302
|
+
const output = await extractor(capEmbedText(msg.text), EXTRACT_OPTS)
|
|
288
303
|
const wallMs = Date.now() - t0
|
|
289
304
|
if (!output.data?.length) throw new Error(`embed output missing data (model=${MODEL_ID})`)
|
|
290
305
|
const dims = output.data.length
|
|
@@ -302,7 +317,7 @@ async function processMessage(msg) {
|
|
|
302
317
|
resetIdleTimer()
|
|
303
318
|
const extractor = await loadExtractor()
|
|
304
319
|
const t0 = Date.now()
|
|
305
|
-
const warmupOutput = await extractor('warmup',
|
|
320
|
+
const warmupOutput = await extractor('warmup', EXTRACT_OPTS)
|
|
306
321
|
const wallMs = Date.now() - t0
|
|
307
322
|
if (!warmupOutput.data?.length) throw new Error(`warmup output missing data (model=${MODEL_ID})`)
|
|
308
323
|
const measuredDims = warmupOutput.data.length
|
|
@@ -75,6 +75,64 @@ const _rawTimeout = Number(process.env.MIXDOG_EMBED_FLUSH_TIMEOUT_MS)
|
|
|
75
75
|
const EMBED_FLUSH_TIMEOUT_MS = (Number.isFinite(_rawTimeout) && _rawTimeout > 0) ? _rawTimeout : 30_000
|
|
76
76
|
|
|
77
77
|
const BATCH_SIZE = 32
|
|
78
|
+
const RAW_EMBEDDING_ENABLED = process.env.MIXDOG_ENABLE_RAW_EMBEDDINGS === '1'
|
|
79
|
+
let _rawEmbeddingDisabledLogged = false
|
|
80
|
+
|
|
81
|
+
// Conservative per-text length cap applied before any text reaches the
|
|
82
|
+
// embedding provider. Raw transcript rows and tool-output content can be
|
|
83
|
+
// arbitrarily large; feeding a 14k-token string to the transformer builds a
|
|
84
|
+
// giant [batch, seq] tensor (observed: dims [8,14363] → ORT attempting a 79GB
|
|
85
|
+
// allocation before dumping the input BigInt64Array). The model only attends to
|
|
86
|
+
// its first ~512 tokens anyway, so truncating to a bounded char budget preserves
|
|
87
|
+
// recall while capping tensor size and worker RSS. ~4 chars/token → 8000 chars
|
|
88
|
+
// ≈ 2000 tokens, comfortably above the model window yet far below the blowup.
|
|
89
|
+
const _envEmbedMaxChars = Number(process.env.MIXDOG_EMBED_MAX_CHARS)
|
|
90
|
+
const EMBED_MAX_CHARS = (Number.isFinite(_envEmbedMaxChars) && _envEmbedMaxChars > 0)
|
|
91
|
+
? Math.floor(_envEmbedMaxChars)
|
|
92
|
+
: 8000
|
|
93
|
+
|
|
94
|
+
export function truncateForEmbed(text) {
|
|
95
|
+
if (typeof text !== 'string') return ''
|
|
96
|
+
return text.length > EMBED_MAX_CHARS ? text.slice(0, EMBED_MAX_CHARS) : text
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Raw-row dense-embed eligibility. Storage/recall rows are NEVER deleted or
|
|
100
|
+
// unindexed here — but the dense embedding leg should only cover useful
|
|
101
|
+
// CONVERSATIONAL text. Tool call/result traces and runtime/log/offload/debug/
|
|
102
|
+
// system notifications carry no recall value as dense vectors and pollute the
|
|
103
|
+
// shared embedding cache with mechanical noise, so they are excluded before
|
|
104
|
+
// embedding AND before batch vector mapping. An excluded row keeps its row +
|
|
105
|
+
// embedding-NULL storage state untouched (no delete, no schema change).
|
|
106
|
+
const RAW_EMBED_EXCLUDE_ROLES = new Set(['tool', 'tool_result', 'tool-result', 'function', 'system', 'log', 'offload', 'debug'])
|
|
107
|
+
const RAW_EMBED_EXCLUDE_CONTENT_RES = [
|
|
108
|
+
/^\s*\[tool_call\b/i,
|
|
109
|
+
/^\s*\[tool_result\b/i,
|
|
110
|
+
/^\s*\[mixdog-runtime\]/i,
|
|
111
|
+
/^\s*The async (?:shell task|agent task|\S+ execution|\S+) .*has finished\b.*review this result in your next step/i,
|
|
112
|
+
/^\s*background task\b/i,
|
|
113
|
+
]
|
|
114
|
+
const RAW_EMBED_EXCLUDE_NON_CONVERSATION_CONTENT_RES = [
|
|
115
|
+
/^\s*\[(?:system|log|offload|debug|trace|info|warn|warning|error|fatal)\]/i,
|
|
116
|
+
]
|
|
117
|
+
const RAW_EMBED_CONVERSATION_ROLES = new Set(['user', 'assistant'])
|
|
118
|
+
|
|
119
|
+
export function isRawEmbeddable(role, content) {
|
|
120
|
+
const normRole = String(role ?? '').trim().toLowerCase()
|
|
121
|
+
if (RAW_EMBED_EXCLUDE_ROLES.has(normRole)) return false
|
|
122
|
+
const text = typeof content === 'string' ? content : ''
|
|
123
|
+
if (!text.trim()) return false
|
|
124
|
+
for (const re of RAW_EMBED_EXCLUDE_CONTENT_RES) if (re.test(text)) return false
|
|
125
|
+
if (!RAW_EMBED_CONVERSATION_ROLES.has(normRole)) {
|
|
126
|
+
for (const re of RAW_EMBED_EXCLUDE_NON_CONVERSATION_CONTENT_RES) if (re.test(text)) return false
|
|
127
|
+
}
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const RAW_EMBED_SQL_EXCLUDE_ROLE_VALUES = [...RAW_EMBED_EXCLUDE_ROLES]
|
|
132
|
+
const RAW_EMBED_SQL_ALWAYS_EXCLUDE_CONTENT_RE =
|
|
133
|
+
'^\\s*(\\[tool_call\\b|\\[tool_result\\b|\\[mixdog-runtime\\]|The async (shell task|agent task|\\S+ execution|\\S+) .*has finished\\b.*review this result in your next step|background task\\b)'
|
|
134
|
+
const RAW_EMBED_SQL_NON_CONVERSATION_EXCLUDE_CONTENT_RE =
|
|
135
|
+
'^\\s*\\[(system|log|offload|debug|trace|info|warn|warning|error|fatal)\\]'
|
|
78
136
|
|
|
79
137
|
function throwIfAborted(signal) {
|
|
80
138
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
@@ -106,10 +164,13 @@ export async function cachedEmbedTextBatch(db, texts, options = {}) {
|
|
|
106
164
|
// env value that doesn't match the provider would tag vectors with a
|
|
107
165
|
// stale/wrong label across model/env changes.
|
|
108
166
|
const modelId = getEmbeddingModelId() || process.env.MIXDOG_EMBED_MODEL || 'default'
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
167
|
+
// Bound each text before hashing/embedding: the cache key and the provider
|
|
168
|
+
// input are both the truncated string, so oversized rows can never build a
|
|
169
|
+
// giant tensor and identical truncations dedup in-cache.
|
|
170
|
+
const entries = texts.map(t => {
|
|
171
|
+
const text = truncateForEmbed(t)
|
|
172
|
+
return { text, hash: Buffer.from(createHash('sha256').update(text).digest()) }
|
|
173
|
+
})
|
|
113
174
|
|
|
114
175
|
// Single SELECT for all hashes
|
|
115
176
|
const hashBufs = entries.map(e => e.hash)
|
|
@@ -285,6 +346,14 @@ export async function flushEmbeddingDirty(db, options = {}) {
|
|
|
285
346
|
// dense matching, not staleness.
|
|
286
347
|
export async function flushRawEmbeddings(db, options = {}) {
|
|
287
348
|
const { limit = 200, signal } = options ?? {}
|
|
349
|
+
throwIfAborted(signal)
|
|
350
|
+
if (!RAW_EMBEDDING_ENABLED) {
|
|
351
|
+
if (!_rawEmbeddingDisabledLogged) {
|
|
352
|
+
__mixdogMemoryLog('[embed] raw transcript embedding disabled; only root/summary entries are embedded\n')
|
|
353
|
+
_rawEmbeddingDisabledLogged = true
|
|
354
|
+
}
|
|
355
|
+
return { attempted: 0, embedded: 0, skipped: 'raw-embedding-disabled' }
|
|
356
|
+
}
|
|
288
357
|
// Optional id allow-list: restrict the SKIP LOCKED claim to a specific set of
|
|
289
358
|
// rows (e.g. exactly the rows a single ingest_session call inserted) so a
|
|
290
359
|
// caller can flush ONLY its own rows instead of inheriting the whole raw
|
|
@@ -294,7 +363,6 @@ export async function flushRawEmbeddings(db, options = {}) {
|
|
|
294
363
|
? options.ids.map(Number).filter(Number.isFinite)
|
|
295
364
|
: null
|
|
296
365
|
if (idFilter && idFilter.length === 0) return { attempted: 0, embedded: 0 }
|
|
297
|
-
throwIfAborted(signal)
|
|
298
366
|
await ensureEmbCacheTable(db)
|
|
299
367
|
throwIfAborted(signal)
|
|
300
368
|
|
|
@@ -310,7 +378,14 @@ export async function flushRawEmbeddings(db, options = {}) {
|
|
|
310
378
|
try {
|
|
311
379
|
throwIfAborted(signal)
|
|
312
380
|
await client.query('BEGIN')
|
|
313
|
-
const params = [
|
|
381
|
+
const params = [
|
|
382
|
+
batchCap,
|
|
383
|
+
cursor,
|
|
384
|
+
RAW_EMBED_SQL_EXCLUDE_ROLE_VALUES,
|
|
385
|
+
[...RAW_EMBED_CONVERSATION_ROLES],
|
|
386
|
+
RAW_EMBED_SQL_ALWAYS_EXCLUDE_CONTENT_RE,
|
|
387
|
+
RAW_EMBED_SQL_NON_CONVERSATION_EXCLUDE_CONTENT_RE,
|
|
388
|
+
]
|
|
314
389
|
let idClause = ''
|
|
315
390
|
if (idFilter) {
|
|
316
391
|
params.push(idFilter)
|
|
@@ -322,6 +397,9 @@ export async function flushRawEmbeddings(db, options = {}) {
|
|
|
322
397
|
AND NULLIF(btrim(session_id), '') IS NOT NULL
|
|
323
398
|
AND content IS NOT NULL
|
|
324
399
|
AND id > $2${idClause}
|
|
400
|
+
AND lower(COALESCE(role, '')) <> ALL($3::text[])
|
|
401
|
+
AND content !~* $5
|
|
402
|
+
AND (lower(COALESCE(role, '')) = ANY($4::text[]) OR content !~* $6)
|
|
325
403
|
ORDER BY id
|
|
326
404
|
LIMIT $1
|
|
327
405
|
FOR UPDATE SKIP LOCKED`,
|
|
@@ -374,7 +452,7 @@ async function syncRawBatchEmbeddings(db, ids, options = {}) {
|
|
|
374
452
|
const signal = options?.signal
|
|
375
453
|
throwIfAborted(signal)
|
|
376
454
|
const rows = (await db.query(
|
|
377
|
-
`SELECT id, content FROM memory.entries
|
|
455
|
+
`SELECT id, role, content FROM memory.entries
|
|
378
456
|
WHERE id = ANY($1::bigint[]) AND is_root = 0 AND chunk_root IS NULL`,
|
|
379
457
|
[ids],
|
|
380
458
|
)).rows
|
|
@@ -385,7 +463,10 @@ async function syncRawBatchEmbeddings(db, ids, options = {}) {
|
|
|
385
463
|
throwIfAborted(signal)
|
|
386
464
|
const expected = Number(dimsRow?.value ?? 0)
|
|
387
465
|
|
|
388
|
-
|
|
466
|
+
// Exclude tool/tool_result/log/offload/debug/system-like rows from the dense
|
|
467
|
+
// embed leg (empty text ⇒ dropped below and never vector-mapped). The length
|
|
468
|
+
// cap still applies to survivors via cachedEmbedTextBatch → truncateForEmbed.
|
|
469
|
+
const texts = rows.map(row => (isRawEmbeddable(row.role, row.content) ? row.content.trim() : ''))
|
|
389
470
|
const vectors = await cachedEmbedTextBatch(db, texts.filter(Boolean), { signal })
|
|
390
471
|
throwIfAborted(signal)
|
|
391
472
|
let vecIdx = 0
|
|
@@ -2,8 +2,7 @@ import {
|
|
|
2
2
|
estimateRequestReserveTokens,
|
|
3
3
|
estimateToolSchemaTokens,
|
|
4
4
|
estimateTranscriptContextUsage,
|
|
5
|
-
|
|
6
|
-
resolveCompactTriggerTokens,
|
|
5
|
+
resolveSessionCompactPolicy,
|
|
7
6
|
summarizeContextMessages,
|
|
8
7
|
} from '../runtime/agent/orchestrator/session/context-utils.mjs';
|
|
9
8
|
import { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
|
|
@@ -122,10 +121,12 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
|
|
|
122
121
|
// Use the same shared compact-policy math as manager/loop. Do not trust
|
|
123
122
|
// persisted trigger telemetry as an independent policy input: it is an
|
|
124
123
|
// output snapshot and was the source of repeated /context false positives.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
// Shared session-compaction policy (same math as manager/loop): agent-owned
|
|
125
|
+
// semantic sessions report/fire at 90% of the boundary; main/user
|
|
126
|
+
// recall-fasttrack report/fire on the boundary itself (100%).
|
|
127
|
+
const compactPolicy = resolveSessionCompactPolicy(session || {}, compactBoundaryTokens);
|
|
128
|
+
const compactTriggerTokens = compactPolicy.triggerTokens || 0;
|
|
129
|
+
const compactBufferTokens = compactPolicy.bufferTokens || 0;
|
|
129
130
|
const value = {
|
|
130
131
|
sessionId: session?.id || null,
|
|
131
132
|
provider: route.provider,
|