mixdog 0.9.44 → 0.9.46
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 +5 -2
- package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
- package/scripts/agent-model-liveness-test.mjs +618 -0
- package/scripts/agent-trace-io-test.mjs +68 -4
- package/scripts/bench-run.mjs +30 -3
- package/scripts/compact-pressure-test.mjs +187 -4
- package/scripts/compact-prior-context-flatten-test.mjs +252 -0
- package/scripts/compact-trigger-migration-smoke.mjs +8 -6
- package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
- package/scripts/context-mcp-metering-test.mjs +75 -0
- package/scripts/explore-prompt-policy-test.mjs +15 -16
- package/scripts/explore-timeout-cancel-test.mjs +345 -0
- package/scripts/headless-pristine-execution-test.mjs +614 -0
- package/scripts/internal-comms-smoke.mjs +15 -6
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/memory-core-input-test.mjs +137 -0
- package/scripts/memory-rule-contract-test.mjs +5 -5
- package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
- package/scripts/parent-abort-link-test.mjs +22 -0
- package/scripts/provider-toolcall-test.mjs +22 -0
- package/scripts/reactive-compact-persist-smoke.mjs +8 -4
- package/scripts/session-bench.mjs +3 -70
- package/scripts/task-bench.mjs +0 -2
- package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
- package/scripts/tool-smoke.mjs +21 -21
- package/scripts/tool-tui-presentation-test.mjs +68 -0
- package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
- package/src/app.mjs +28 -103
- package/src/cli.mjs +17 -13
- package/src/headless-command.mjs +139 -0
- package/src/headless-role.mjs +121 -10
- package/src/help.mjs +4 -1
- package/src/rules/agent/00-common.md +3 -3
- package/src/rules/agent/00-core.md +8 -9
- package/src/rules/agent/20-skip-protocol.md +2 -3
- package/src/rules/agent/30-explorer.md +50 -56
- package/src/rules/agent/40-cycle1-agent.md +10 -12
- package/src/rules/agent/41-cycle2-agent.md +12 -9
- package/src/rules/agent/42-cycle3-agent.md +4 -6
- package/src/rules/lead/01-general.md +5 -6
- package/src/rules/lead/02-channels.md +1 -1
- package/src/rules/lead/lead-brief.md +14 -17
- package/src/rules/lead/lead-tool.md +3 -3
- package/src/rules/shared/01-tool.md +41 -43
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
- package/src/runtime/agent/orchestrator/config.mjs +96 -30
- package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/runtime/memory/index.mjs +0 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
- package/src/runtime/memory/lib/http-router.mjs +0 -193
- package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
- package/src/runtime/memory/tool-defs.mjs +5 -6
- package/src/runtime/shared/config.mjs +11 -34
- package/src/runtime/shared/pristine-execution-contract.json +75 -0
- package/src/runtime/shared/pristine-execution.mjs +356 -0
- package/src/runtime/shared/provider-api-key.mjs +43 -0
- package/src/runtime/shared/provider-auth-binding.mjs +21 -0
- package/src/runtime/shared/update-checker.mjs +40 -10
- package/src/session-runtime/context-status.mjs +61 -13
- package/src/session-runtime/mcp-glue.mjs +29 -2
- package/src/session-runtime/plugin-mcp.mjs +7 -0
- package/src/session-runtime/resource-api.mjs +38 -5
- package/src/session-runtime/runtime-core.mjs +5 -1
- package/src/session-runtime/session-turn-api.mjs +14 -2
- package/src/session-runtime/settings-api.mjs +5 -0
- package/src/session-runtime/tool-catalog.mjs +13 -2
- package/src/session-runtime/tool-defs.mjs +1 -3
- package/src/standalone/agent-task-status.mjs +50 -11
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/explore-tool.mjs +257 -49
- package/src/standalone/seeds.mjs +1 -0
- package/src/tui/App.jsx +26 -16
- package/src/tui/app/use-transcript-scroll.mjs +4 -3
- package/src/tui/app/use-transcript-window.mjs +12 -21
- package/src/tui/components/ContextPanel.jsx +19 -25
- package/src/tui/dist/index.mjs +89 -78
- package/src/tui/engine/agent-envelope.mjs +16 -5
- package/src/tui/engine/labels.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +21 -51
- package/src/workflows/solo/WORKFLOW.md +12 -17
- package/src/workflows/solo-review/WORKFLOW.md +0 -47
|
@@ -124,22 +124,22 @@ function estimateCompactionPromptTokens(input, perMessageChars) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
function previousSummaryBodyForCompactionPrompt(previousSummary) {
|
|
127
|
-
const text = String(previousSummary || '')
|
|
128
|
-
if (!text) return '';
|
|
127
|
+
const text = String(previousSummary || '');
|
|
128
|
+
if (!text.trim()) return '';
|
|
129
129
|
return stripNestedSummaryHeaderLines(text);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
function priorSummaryNeedsNormalization(text) {
|
|
133
|
-
const body = String(text || '')
|
|
134
|
-
if (!body) return false;
|
|
133
|
+
const body = String(text || '');
|
|
134
|
+
if (!body.trim()) return false;
|
|
135
135
|
if (!/^##\s+/m.test(body)) return true;
|
|
136
136
|
if (!summaryIsSchemaValid(body)) return true;
|
|
137
137
|
return summaryHasUnrecognizedHeadings(body);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function normalizePriorSummaryForCompactionPrompt(fullBody) {
|
|
141
|
-
const text = String(fullBody || '')
|
|
142
|
-
if (!text) return '';
|
|
141
|
+
const text = String(fullBody || '');
|
|
142
|
+
if (!text.trim()) return '';
|
|
143
143
|
if (!priorSummaryNeedsNormalization(text)) return text;
|
|
144
144
|
return repairSemanticSummary(text, { head: [], tail: [] });
|
|
145
145
|
}
|
|
@@ -154,9 +154,9 @@ function fitPreviousSummaryForCompactionPrompt(input, perMessageChars, targetTok
|
|
|
154
154
|
previousSummaryBodyForCompactionPrompt(input.previousSummary),
|
|
155
155
|
);
|
|
156
156
|
const withSummary = (summaryText) => {
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
159
|
-
return { ...input, previousSummary:
|
|
157
|
+
const value = String(summaryText || '');
|
|
158
|
+
if (!value.trim()) return { ...input, previousSummary: null };
|
|
159
|
+
return { ...input, previousSummary: value };
|
|
160
160
|
};
|
|
161
161
|
|
|
162
162
|
if (estimateCompactionPromptTokens(withSummary(fullBody), perMessageChars) <= targetTokens) {
|
|
@@ -412,23 +412,162 @@ export const RECALL_TAIL_SHORT_TRUNCATION_MARKER = '[truncated]';
|
|
|
412
412
|
|
|
413
413
|
const PRIOR_COMPACTED_CONTEXT_OPEN = '<prior-compacted-context>';
|
|
414
414
|
const PRIOR_COMPACTED_CONTEXT_CLOSE = '</prior-compacted-context>';
|
|
415
|
+
// Matches ONLY a structural wrapper BOUNDARY line — a line whose sole content is
|
|
416
|
+
// the open or close tag (optionally surrounded by whitespace). It deliberately
|
|
417
|
+
// does NOT match an inline occurrence embedded in real content (e.g. a user note
|
|
418
|
+
// like "keep <prior-compacted-context> literal here"), so flattening the wrapper
|
|
419
|
+
// can never splice words together or corrupt marker-like text. The production
|
|
420
|
+
// wrapper is always emitted on its own line, so boundary-line stripping removes
|
|
421
|
+
// every real wrapper while leaving inline literals verbatim.
|
|
422
|
+
const PRIOR_COMPACTED_CONTEXT_BOUNDARY_RE = /^[ \t]*<\/?prior-compacted-context>[ \t]*$/;
|
|
423
|
+
|
|
424
|
+
// Strip the STRUCTURAL <prior-compacted-context> / </prior-compacted-context>
|
|
425
|
+
// boundary lines from prior text so re-wrapping never nests, while preserving
|
|
426
|
+
// any inline marker-like text inside real content exactly as written. Returns
|
|
427
|
+
// the bare inner content with the blank lines the removed boundary lines leave
|
|
428
|
+
// behind collapsed.
|
|
429
|
+
export function stripPriorCompactedContextWrappers(text) {
|
|
430
|
+
const raw = String(text ?? '');
|
|
431
|
+
if (!raw) return '';
|
|
432
|
+
// Keep every non-structural byte, including leading/trailing and repeated
|
|
433
|
+
// newlines. Removing only the structural line itself (not trim/collapse)
|
|
434
|
+
// preserves the layout of all remaining content.
|
|
435
|
+
return raw
|
|
436
|
+
.split('\n')
|
|
437
|
+
.filter((line) => !PRIOR_COMPACTED_CONTEXT_BOUNDARY_RE.test(line))
|
|
438
|
+
.join('\n');
|
|
439
|
+
}
|
|
415
440
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
441
|
+
// Remove only STRUCTURALLY IDENTICAL blank-line-separated blocks (byte-for-byte
|
|
442
|
+
// repeats) so a prior context re-fed across many compaction cycles keeps every
|
|
443
|
+
// distinct requirement/fact and cannot grow without bound. The dedupe is
|
|
444
|
+
// content-preserving: the key is the block's EXACT text — internal whitespace is
|
|
445
|
+
// never collapsed and meaningful block content is never trimmed — so distinct
|
|
446
|
+
// strings such as `printf 'a b'` and `printf 'a b'` are BOTH kept verbatim.
|
|
447
|
+
// Only an exact repeat of a previously emitted block is dropped; blank
|
|
448
|
+
// separators (whitespace-only splits) are skipped, never real content.
|
|
449
|
+
function dedupePriorCompactedBlocks(text) {
|
|
450
|
+
const raw = String(text ?? '');
|
|
451
|
+
if (!raw.trim()) return '';
|
|
452
|
+
const seen = new Set();
|
|
453
|
+
const parts = raw.split(/(\n{2,})/);
|
|
454
|
+
const out = [];
|
|
455
|
+
let separator = '';
|
|
456
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
457
|
+
const part = parts[i];
|
|
458
|
+
if (i % 2 === 1) {
|
|
459
|
+
separator += part;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
// Whitespace-only parts are layout, not a duplicate candidate.
|
|
463
|
+
if (!part.trim()) {
|
|
464
|
+
out.push(separator, part);
|
|
465
|
+
separator = '';
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (seen.has(part)) {
|
|
469
|
+
// Drop only the exact repeated structural block and its preceding
|
|
470
|
+
// separator; all retained text and whitespace stay byte-for-byte.
|
|
471
|
+
separator = '';
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
out.push(separator, part);
|
|
475
|
+
separator = '';
|
|
476
|
+
seen.add(part);
|
|
477
|
+
}
|
|
478
|
+
out.push(separator);
|
|
479
|
+
return out.join('');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Canonicalize prior compacted context to AT MOST ONE wrapper: flatten any
|
|
483
|
+
// nested/duplicated wrappers accumulated by earlier cycles, dedupe repeated
|
|
484
|
+
// blocks, then wrap the surviving content exactly once. Repeated compaction can
|
|
485
|
+
// therefore never nest or duplicate the prior context, each distinct
|
|
486
|
+
// requirement/fact is preserved exactly once, and repeated-cycle token size
|
|
487
|
+
// stays bounded.
|
|
488
|
+
//
|
|
489
|
+
// Optimization-safe empty-prior interpretation: when there is NO prior content
|
|
490
|
+
// (empty / blank / boundary-tag-only input) this returns '' so the generated
|
|
491
|
+
// summary carries ZERO wrappers instead of an empty
|
|
492
|
+
// <prior-compacted-context></prior-compacted-context> pair. The production
|
|
493
|
+
// summary body joins only non-empty parts (makeRecall*SummaryMessageParts), so
|
|
494
|
+
// an empty wrapper cannot be carried and would only waste tokens. "Exactly one
|
|
495
|
+
// wrapper" is thus realized as: exactly one when prior content exists, none when
|
|
496
|
+
// it does not — and never more than one, never nested.
|
|
497
|
+
export function formatPriorCompactedContextBlock(priorText) {
|
|
498
|
+
const flattened = dedupePriorCompactedBlocks(stripPriorCompactedContextWrappers(priorText));
|
|
499
|
+
if (!flattened) return '';
|
|
500
|
+
return `${PRIOR_COMPACTED_CONTEXT_OPEN}\n${flattened}\n${PRIOR_COMPACTED_CONTEXT_CLOSE}`;
|
|
420
501
|
}
|
|
421
502
|
|
|
422
503
|
export function stripNestedSummaryHeaderLines(text) {
|
|
423
|
-
const
|
|
504
|
+
const raw = String(text ?? '');
|
|
505
|
+
// A generated recall summary has a canonical, provenance-bearing shape:
|
|
506
|
+
// header + "\n\n" + OPEN + "\n" + prior + "\n" + CLOSE + "\n\n" + recall
|
|
507
|
+
// Extracting it as lines loses ownership of a run of newlines: in
|
|
508
|
+
// `X\n` + join + live `X`, generic block dedupe cannot know that one
|
|
509
|
+
// newline belongs to X and must survive dropping the duplicate live X.
|
|
510
|
+
// Peel only the emitted wrapper/header/join bytes and retain the inner
|
|
511
|
+
// prior slice verbatim. The live recall is dropped only when it is the
|
|
512
|
+
// same payload the wrapper already carries.
|
|
513
|
+
const openMatch = /^[ \t]*<prior-compacted-context>[ \t]*\n/m.exec(raw);
|
|
514
|
+
if (openMatch) {
|
|
515
|
+
const closeRe = /\n[ \t]*<\/prior-compacted-context>[ \t]*(?=\n|$)/g;
|
|
516
|
+
closeRe.lastIndex = openMatch.index + openMatch[0].length;
|
|
517
|
+
const closeMatch = closeRe.exec(raw);
|
|
518
|
+
if (closeMatch) {
|
|
519
|
+
const prior = raw.slice(openMatch.index + openMatch[0].length, closeMatch.index);
|
|
520
|
+
const remainder = raw.slice(closeMatch.index + closeMatch[0].length);
|
|
521
|
+
// The only bytes between generated wrapper and live recall are
|
|
522
|
+
// this part-join's two newlines. Do not consume any other newline:
|
|
523
|
+
// those belong to either prior or live content.
|
|
524
|
+
const live = remainder.startsWith('\n\n') ? remainder.slice(2) : remainder;
|
|
525
|
+
if (!live || prior.trim() === live) return prior;
|
|
526
|
+
if (!prior) return live;
|
|
527
|
+
return `${prior}\n\n${live}`;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const lines = raw.split('\n');
|
|
424
532
|
const out = [];
|
|
533
|
+
let followsStructuralHeader = false;
|
|
425
534
|
for (const line of lines) {
|
|
426
|
-
if (line.startsWith(SUMMARY_PREFIX))
|
|
427
|
-
|
|
428
|
-
|
|
535
|
+
if (line.startsWith(SUMMARY_PREFIX)) {
|
|
536
|
+
followsStructuralHeader = true;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (/^messages=\d+\s+(?:sha256=|compact_type=)/.test(line.trim())) {
|
|
540
|
+
followsStructuralHeader = true;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (/^compact_type=/.test(line.trim())) {
|
|
544
|
+
followsStructuralHeader = true;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
// Summary parts are joined with "\n\n". The first empty line after
|
|
548
|
+
// stripped summary metadata is that join's structural separator, not
|
|
549
|
+
// prior content; retaining it makes every refeed gain a newline when
|
|
550
|
+
// formatPriorCompactedContextBlock wraps the result again.
|
|
551
|
+
if (followsStructuralHeader && line === '') {
|
|
552
|
+
followsStructuralHeader = false;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
followsStructuralHeader = false;
|
|
556
|
+
// A prior summary re-fed as <previous-summary>/prior may still carry
|
|
557
|
+
// the canonical <prior-compacted-context> wrapper from an earlier
|
|
558
|
+
// cycle; drop those tag-only lines so the caller re-wraps exactly once
|
|
559
|
+
// (or treats the body as bare prior) instead of nesting.
|
|
560
|
+
if (/^<prior-compacted-context>$/.test(line.trim())) {
|
|
561
|
+
// The immediately preceding blank is the header→wrapper join from
|
|
562
|
+
// a generated summary. Remove only that wrapper-owned separator;
|
|
563
|
+
// blank lines inside the wrapper body remain untouched.
|
|
564
|
+
if (out.length > 0 && out[out.length - 1] === '') out.pop();
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (/^<\/prior-compacted-context>$/.test(line.trim())) continue;
|
|
429
568
|
out.push(line);
|
|
430
569
|
}
|
|
431
|
-
return out.join('\n')
|
|
570
|
+
return out.join('\n');
|
|
432
571
|
}
|
|
433
572
|
|
|
434
573
|
function makeRecallFastTrackSummaryMessageParts(oldHistory, recallPart, priorPart, recallMeta = {}) {
|
|
@@ -444,7 +583,7 @@ function makeRecallFastTrackSummaryMessageParts(oldHistory, recallPart, priorPar
|
|
|
444
583
|
|
|
445
584
|
export function fitRecallFastTrackSummaryMessage(oldHistory, recallText, remainingTokens, recallMeta = {}, priorPart = '') {
|
|
446
585
|
const recall = String(recallText || '').trim();
|
|
447
|
-
const prior = String(priorPart || '')
|
|
586
|
+
const prior = String(priorPart || '');
|
|
448
587
|
|
|
449
588
|
let fittedPrior = prior;
|
|
450
589
|
if (prior) {
|
|
@@ -594,7 +733,7 @@ function makeRecallRootsSummaryMessageParts(oldHistory, rootsPart, priorPart, re
|
|
|
594
733
|
// blocks can only shrink (never grow) the serialized size, the minimal-drop
|
|
595
734
|
// threshold is found via binary search on the drop count.
|
|
596
735
|
export function fitRecallRootsMessage(oldHistory, recallText, remainingTokens, recallMeta = {}, priorPart = '') {
|
|
597
|
-
const prior = String(priorPart || '')
|
|
736
|
+
const prior = String(priorPart || '');
|
|
598
737
|
|
|
599
738
|
let fittedPrior = prior;
|
|
600
739
|
if (prior) {
|
|
@@ -253,6 +253,7 @@ function contextMessageContribution(message) {
|
|
|
253
253
|
role,
|
|
254
254
|
tokens,
|
|
255
255
|
reminderBuckets: null,
|
|
256
|
+
systemWorkflowTokens: 0,
|
|
256
257
|
toolCallCount: 0,
|
|
257
258
|
toolCallTokens: 0,
|
|
258
259
|
toolResultCount: role === 'tool' ? 1 : 0,
|
|
@@ -270,6 +271,13 @@ function contextMessageContribution(message) {
|
|
|
270
271
|
buckets.otherTokens = Math.max(0, contribution.tokens - sectionTokens);
|
|
271
272
|
contribution.reminderBuckets = buckets;
|
|
272
273
|
}
|
|
274
|
+
if (role === 'system') {
|
|
275
|
+
for (const section of splitMarkdownSections(text)) {
|
|
276
|
+
if (reminderSectionBucket(section) === 'workflow') {
|
|
277
|
+
contribution.systemWorkflowTokens += estimateTokens(section);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
273
281
|
if (fingerprint.role === 'assistant' && Array.isArray(message?.toolCalls) && message.toolCalls.length) {
|
|
274
282
|
contribution.toolCallCount = message.toolCalls.length;
|
|
275
283
|
try { contribution.toolCallTokens = estimateTokens(JSON.stringify(message.toolCalls)); }
|
|
@@ -317,7 +325,8 @@ function applyContextMessageContribution(state, contribution, direction) {
|
|
|
317
325
|
state.rows[role].tokens += direction * tokens;
|
|
318
326
|
if (role === 'system') {
|
|
319
327
|
state.semantic.system.count += direction;
|
|
320
|
-
state.semantic.system.tokens += direction * tokens;
|
|
328
|
+
state.semantic.system.tokens += direction * (tokens - contribution.systemWorkflowTokens);
|
|
329
|
+
state.semantic.workflow.tokens += direction * contribution.systemWorkflowTokens;
|
|
321
330
|
} else if (role === 'user') {
|
|
322
331
|
if (contribution.reminderBuckets) {
|
|
323
332
|
state.semantic.reminders.count += direction;
|
|
@@ -358,8 +367,11 @@ function contextSummaryResult(state, count) {
|
|
|
358
367
|
|
|
359
368
|
export const DEFAULT_COMPACTION_BUFFER_TOKENS = 0;
|
|
360
369
|
export const DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
|
|
361
|
-
|
|
370
|
+
// Main/user recall-fasttrack compacts at 95% by default. Lower triggers remain
|
|
371
|
+
// opt-in through the mainBuffer* configuration and MIXDOG_MAIN_COMPACT_BUFFER_*.
|
|
372
|
+
export const DEFAULT_MAIN_COMPACTION_BUFFER_RATIO = 0.05;
|
|
362
373
|
export const MAX_COMPACTION_BUFFER_RATIO = 0.25;
|
|
374
|
+
const MAX_BUFFER_INPUT_RATIO = 0.999_999;
|
|
363
375
|
export const DEFAULT_COMPACTION_KEEP_TOKENS = 8_000;
|
|
364
376
|
const LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
|
|
365
377
|
|
|
@@ -384,11 +396,11 @@ export function normalizeCompactionBufferRatio(value, fallback = DEFAULT_COMPACT
|
|
|
384
396
|
export function resolveBufferRatioCandidate(percentInputs = [], ratioInputs = []) {
|
|
385
397
|
for (const raw of percentInputs) {
|
|
386
398
|
const n = Number(raw);
|
|
387
|
-
if (Number.isFinite(n) && n > 0) return Math.min(
|
|
399
|
+
if (Number.isFinite(n) && n > 0) return Math.min(MAX_BUFFER_INPUT_RATIO, n / 100);
|
|
388
400
|
}
|
|
389
401
|
for (const raw of ratioInputs) {
|
|
390
402
|
const n = Number(raw);
|
|
391
|
-
if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
|
|
403
|
+
if (Number.isFinite(n) && n > 0) return Math.min(MAX_BUFFER_INPUT_RATIO, n > 1 ? n / 100 : n);
|
|
392
404
|
}
|
|
393
405
|
return null;
|
|
394
406
|
}
|
|
@@ -401,6 +413,42 @@ export function resolveCompactBufferRatio(cfg = {}) {
|
|
|
401
413
|
return normalizeCompactionBufferRatio(resolved, DEFAULT_COMPACTION_BUFFER_RATIO);
|
|
402
414
|
}
|
|
403
415
|
|
|
416
|
+
// Main/user settings are deliberately separate from the semantic-agent buffer:
|
|
417
|
+
// `buffer*` and MIXDOG_AGENT_COMPACT_BUFFER_* remain agent-only controls.
|
|
418
|
+
// Source precedence is config before environment; within each source the fixed
|
|
419
|
+
// unit precedence is tokens, percent, then ratio. Invalid/zero values are
|
|
420
|
+
// ignored, and percent/ratio inputs are capped below 100%/1.0.
|
|
421
|
+
function positiveTokenCandidate(values = []) {
|
|
422
|
+
for (const value of values) {
|
|
423
|
+
const tokens = positiveTokenInt(value);
|
|
424
|
+
if (tokens) return tokens;
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function resolveMainBufferSetting(cfg = {}) {
|
|
430
|
+
const configTokens = positiveTokenCandidate([cfg.mainBufferTokens, cfg.mainBuffer]);
|
|
431
|
+
if (configTokens) return { tokens: configTokens };
|
|
432
|
+
const configRatio = resolveBufferRatioCandidate(
|
|
433
|
+
[cfg.mainBufferPercent, cfg.mainBufferPct],
|
|
434
|
+
[cfg.mainBufferRatio, cfg.mainBufferFraction],
|
|
435
|
+
);
|
|
436
|
+
if (configRatio !== null) return { ratio: configRatio };
|
|
437
|
+
|
|
438
|
+
const envTokens = positiveTokenCandidate([process.env.MIXDOG_MAIN_COMPACT_BUFFER_TOKENS]);
|
|
439
|
+
if (envTokens) return { tokens: envTokens };
|
|
440
|
+
const envRatio = resolveBufferRatioCandidate(
|
|
441
|
+
[process.env.MIXDOG_MAIN_COMPACT_BUFFER_PERCENT],
|
|
442
|
+
[process.env.MIXDOG_MAIN_COMPACT_BUFFER_RATIO],
|
|
443
|
+
);
|
|
444
|
+
return envRatio === null ? null : { ratio: envRatio };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function resolveMainCompactBufferRatio(cfg = {}) {
|
|
448
|
+
const setting = resolveMainBufferSetting(cfg);
|
|
449
|
+
return setting?.ratio ?? DEFAULT_MAIN_COMPACTION_BUFFER_RATIO;
|
|
450
|
+
}
|
|
451
|
+
|
|
404
452
|
export function compactionBufferTokensForBoundary(boundaryTokens, opts = {}) {
|
|
405
453
|
const boundary = Math.max(0, Math.floor(Number(boundaryTokens) || 0));
|
|
406
454
|
const explicit = Math.max(0, Math.floor(Number(opts.explicitTokens) || 0));
|
|
@@ -483,6 +531,18 @@ export function resolveCompactBufferTokens(boundaryTokens, cfg = {}, opts = {})
|
|
|
483
531
|
});
|
|
484
532
|
}
|
|
485
533
|
|
|
534
|
+
export function resolveMainCompactBufferTokens(boundaryTokens, cfg = {}, opts = {}) {
|
|
535
|
+
const boundary = positiveTokenInt(boundaryTokens);
|
|
536
|
+
const setting = resolveMainBufferSetting(cfg);
|
|
537
|
+
const configured = setting?.tokens || 0;
|
|
538
|
+
if (!boundary) return configured || positiveTokenInt(opts.defaultTokens) || DEFAULT_COMPACTION_BUFFER_TOKENS;
|
|
539
|
+
return compactionBufferTokensForBoundary(boundary, {
|
|
540
|
+
explicitTokens: configured,
|
|
541
|
+
ratio: setting?.ratio ?? DEFAULT_MAIN_COMPACTION_BUFFER_RATIO,
|
|
542
|
+
maxRatio: opts.maxRatio ?? MAX_COMPACTION_BUFFER_RATIO,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
486
546
|
export function resolveCompactTriggerTokens(sessionOrConfig = {}, boundaryTokens = 0) {
|
|
487
547
|
return resolveSessionCompactPolicy(sessionOrConfig, boundaryTokens).triggerTokens;
|
|
488
548
|
}
|
|
@@ -495,8 +555,8 @@ export function resolveCompactTriggerTokens(sessionOrConfig = {}, boundaryTokens
|
|
|
495
555
|
// (trigger = limit) for every session type;
|
|
496
556
|
// - agent-owned semantic sessions otherwise keep the default early-trigger
|
|
497
557
|
// buffer (config-driven, default 10% -> compact at 90% of the boundary);
|
|
498
|
-
// - main/user recall-fasttrack sessions
|
|
499
|
-
// of the effective boundary.
|
|
558
|
+
// - main/user recall-fasttrack sessions use their independently configurable
|
|
559
|
+
// buffer (default 25% -> compact at 75% of the effective boundary).
|
|
500
560
|
// Returns the sanitized explicit limit (null when absent/legacy full-window)
|
|
501
561
|
// plus triggerTokens / bufferTokens / bufferRatio for the given boundary.
|
|
502
562
|
export function resolveSessionCompactPolicy(sessionOrConfig = {}, boundaryTokens = 0) {
|
|
@@ -507,7 +567,9 @@ export function resolveSessionCompactPolicy(sessionOrConfig = {}, boundaryTokens
|
|
|
507
567
|
autoCompactTokenLimit: null,
|
|
508
568
|
triggerTokens: null,
|
|
509
569
|
bufferTokens: 0,
|
|
510
|
-
bufferRatio:
|
|
570
|
+
bufferRatio: isAgentOwner(sessionOrConfig)
|
|
571
|
+
? resolveCompactBufferRatio(cfg)
|
|
572
|
+
: resolveMainCompactBufferRatio(cfg),
|
|
511
573
|
};
|
|
512
574
|
}
|
|
513
575
|
const rawLimit = positiveTokenInt(sessionOrConfig?.autoCompactTokenLimit ?? cfg?.autoCompactTokenLimit);
|
|
@@ -518,7 +580,7 @@ export function resolveSessionCompactPolicy(sessionOrConfig = {}, boundaryTokens
|
|
|
518
580
|
} else if (isAgentOwner(sessionOrConfig)) {
|
|
519
581
|
triggerTokens = Math.max(1, boundary - resolveCompactBufferTokens(boundary, cfg));
|
|
520
582
|
} else {
|
|
521
|
-
triggerTokens = Math.max(1,
|
|
583
|
+
triggerTokens = Math.max(1, boundary - resolveMainCompactBufferTokens(boundary, cfg));
|
|
522
584
|
}
|
|
523
585
|
const bufferTokens = Math.max(0, boundary - triggerTokens);
|
|
524
586
|
const bufferRatio = bufferTokens / boundary;
|
|
@@ -617,8 +679,19 @@ const toolSchemaTokenMemo = new WeakMap();
|
|
|
617
679
|
const requestReserveTokenMemo = new WeakMap();
|
|
618
680
|
|
|
619
681
|
function serializeToolSchemas(tools) {
|
|
620
|
-
|
|
621
|
-
|
|
682
|
+
const list = Array.isArray(tools) ? tools : [];
|
|
683
|
+
try {
|
|
684
|
+
return JSON.stringify(list.map(tool => {
|
|
685
|
+
const wireTool = {
|
|
686
|
+
name: tool?.name,
|
|
687
|
+
description: tool?.description,
|
|
688
|
+
input_schema: tool?.inputSchema ?? tool?.input_schema ?? tool?.parameters ?? tool?.schema,
|
|
689
|
+
};
|
|
690
|
+
if (tool?.deferLoading === true || tool?.defer_loading === true) wireTool.defer_loading = true;
|
|
691
|
+
return wireTool;
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
catch { return list.map(t => String(t?.name ?? '')).join(''); }
|
|
622
695
|
}
|
|
623
696
|
|
|
624
697
|
export function toolSchemaSignature(tools) {
|
|
@@ -81,6 +81,32 @@ function resolveCompactKeepTokens(cfg = {}) {
|
|
|
81
81
|
|| envTokenInt('MIXDOG_AGENT_COMPACT_KEEP_TOKENS')
|
|
82
82
|
|| DEFAULT_COMPACTION_KEEP_TOKENS;
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
function compactTriggerMarginTokens(boundaryTokens) {
|
|
86
|
+
const boundary = positiveTokenInt(boundaryTokens);
|
|
87
|
+
if (!boundary) return 1;
|
|
88
|
+
return Math.max(1, Math.min(1_024, Math.floor(boundary * 0.01)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function legacyCompactTargetBudget(boundaryTokens, targetTokens, reserveTokens) {
|
|
92
|
+
const boundary = positiveTokenInt(boundaryTokens);
|
|
93
|
+
if (!boundary) return null;
|
|
94
|
+
return Math.max(1, Math.min(boundary, targetTokens + reserveTokens));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function compactTargetBudgetForTrigger(boundaryTokens, targetTokens, reserveTokens, triggerTokens, singleShot = false, force = false) {
|
|
98
|
+
const legacyTarget = legacyCompactTargetBudget(boundaryTokens, targetTokens, reserveTokens);
|
|
99
|
+
const trigger = positiveTokenInt(triggerTokens);
|
|
100
|
+
// Degenerate reserve/window combinations cannot leave any post-compact
|
|
101
|
+
// headroom. Keep the legacy target and let the caller compact once only,
|
|
102
|
+
// rather than inventing a zero/negative margin that would immediately loop.
|
|
103
|
+
if (singleShot || !trigger) return legacyTarget;
|
|
104
|
+
const boundedTarget = Math.max(1, Math.min(legacyTarget, trigger - 1));
|
|
105
|
+
// Forced/manual compaction must retain a viable legacy budget when the
|
|
106
|
+
// strict no-repeat clamp would consume all non-reserve working space.
|
|
107
|
+
return force && boundedTarget <= reserveTokens ? legacyTarget : boundedTarget;
|
|
108
|
+
}
|
|
109
|
+
|
|
84
110
|
export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
85
111
|
if (!sessionRef) return null;
|
|
86
112
|
const cfg = sessionRef.compaction || {};
|
|
@@ -95,19 +121,37 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
95
121
|
if (!boundaryTokens) return null;
|
|
96
122
|
const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
|
|
97
123
|
// Shared session-compaction policy (context-utils): agent semantic keeps the
|
|
98
|
-
// default early-trigger buffer (90%); main/user
|
|
124
|
+
// default early-trigger buffer (90%); main/user use their independently
|
|
125
|
+
// configurable 25% default headroom (75%);
|
|
99
126
|
// a truly-explicit sub-boundary limit wins. explicitAutoCompactTokenLimit
|
|
100
127
|
// is the sanitized (null when legacy full-window) value so telemetry never
|
|
101
128
|
// re-persists a boundary-collapsing limit.
|
|
102
129
|
const policy = resolveSessionCompactPolicy(sessionRef, compactBoundaryTokens);
|
|
103
130
|
const explicitAutoCompactTokenLimit = policy.autoCompactTokenLimit;
|
|
104
|
-
const bufferTokens = policy.bufferTokens;
|
|
105
|
-
const bufferRatio = policy.bufferRatio;
|
|
106
|
-
const triggerTokens = policy.triggerTokens;
|
|
107
131
|
const configuredReserve = positiveTokenInt(cfg.reservedTokens)
|
|
108
132
|
|| envTokenInt('MIXDOG_AGENT_COMPACT_RESERVED_TOKENS')
|
|
109
133
|
|| 0;
|
|
110
134
|
const requestReserve = estimateRequestReserveTokens(tools);
|
|
135
|
+
const reserveTokens = requestReserve + configuredReserve;
|
|
136
|
+
const compactTargetTokens = resolveCompactTargetTokens(compactBoundaryTokens, cfg) || compactBoundaryTokens;
|
|
137
|
+
const legacyTargetBudget = legacyCompactTargetBudget(compactBoundaryTokens, compactTargetTokens, reserveTokens);
|
|
138
|
+
// Reserve is included in every next-send pressure calculation, so its
|
|
139
|
+
// relationship to the actual trigger (not the raw boundary) determines
|
|
140
|
+
// whether a post-compact transcript can ever fall below the trigger.
|
|
141
|
+
const singleShot = reserveTokens >= policy.triggerTokens;
|
|
142
|
+
// Main/user recall-fasttrack must not compact into its next trigger. Keep a
|
|
143
|
+
// 1%-of-boundary (up to 1,024-token) gap above the effective post-compact
|
|
144
|
+
// target. Explicit sub-boundary limits and agent semantic triggers retain
|
|
145
|
+
// their established precedence/behavior.
|
|
146
|
+
const minMainTrigger = Math.min(
|
|
147
|
+
compactBoundaryTokens,
|
|
148
|
+
(legacyTargetBudget || 0) + compactTriggerMarginTokens(compactBoundaryTokens),
|
|
149
|
+
);
|
|
150
|
+
const triggerTokens = !singleShot && !isAgentOwner(sessionRef) && !explicitAutoCompactTokenLimit
|
|
151
|
+
? Math.max(policy.triggerTokens, minMainTrigger)
|
|
152
|
+
: policy.triggerTokens;
|
|
153
|
+
const bufferTokens = Math.max(0, compactBoundaryTokens - triggerTokens);
|
|
154
|
+
const bufferRatio = bufferTokens / compactBoundaryTokens;
|
|
111
155
|
const keepTokens = resolveCompactKeepTokens(cfg);
|
|
112
156
|
const compactType = resolveCompactTypeSetting(sessionRef, cfg);
|
|
113
157
|
return {
|
|
@@ -119,6 +163,8 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
119
163
|
triggerTokens,
|
|
120
164
|
bufferTokens,
|
|
121
165
|
bufferRatio,
|
|
166
|
+
compactTargetTokens,
|
|
167
|
+
singleShot,
|
|
122
168
|
contextWindow,
|
|
123
169
|
rawContextWindow: positiveTokenInt(sessionRef.rawContextWindow ?? cfg.rawContextWindow) || contextWindow,
|
|
124
170
|
effectiveContextWindowPercent: Number.isFinite(Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent))
|
|
@@ -131,7 +177,7 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
131
177
|
tailTurns: positiveTokenInt(cfg.tailTurns) || envTokenInt('MIXDOG_AGENT_COMPACT_TAIL_TURNS') || 2,
|
|
132
178
|
keepTokens,
|
|
133
179
|
preserveRecentTokens: positiveTokenInt(cfg.preserveRecentTokens) || envTokenInt('MIXDOG_AGENT_COMPACT_PRESERVE_RECENT_TOKENS') || keepTokens,
|
|
134
|
-
reserveTokens
|
|
180
|
+
reserveTokens,
|
|
135
181
|
requestReserveTokens: requestReserve,
|
|
136
182
|
configuredReserveTokens: configuredReserve,
|
|
137
183
|
toolSchemaSignature: toolSchemaSignature(tools),
|
|
@@ -257,8 +303,20 @@ export function compactTargetBudget(policy) {
|
|
|
257
303
|
const boundary = positiveTokenInt(policy?.boundaryTokens);
|
|
258
304
|
if (!boundary) return null;
|
|
259
305
|
const reserve = Math.max(0, Number(policy?.reserveTokens) || 0);
|
|
260
|
-
const targetEffective =
|
|
261
|
-
|
|
306
|
+
const targetEffective = positiveTokenInt(policy?.compactTargetTokens)
|
|
307
|
+
|| resolveCompactTargetTokens(boundary, policy)
|
|
308
|
+
|| boundary;
|
|
309
|
+
const trigger = positiveTokenInt(policy?.triggerTokens);
|
|
310
|
+
const singleShot = policy?.singleShot === true
|
|
311
|
+
|| (trigger > 0 && reserve >= trigger);
|
|
312
|
+
return compactTargetBudgetForTrigger(
|
|
313
|
+
boundary,
|
|
314
|
+
targetEffective,
|
|
315
|
+
reserve,
|
|
316
|
+
trigger,
|
|
317
|
+
singleShot,
|
|
318
|
+
policy?.force === true,
|
|
319
|
+
);
|
|
262
320
|
}
|
|
263
321
|
export function shouldCompactForSession(messageTokensEst, policy, {
|
|
264
322
|
forceReactive = false,
|
|
@@ -267,7 +325,14 @@ export function shouldCompactForSession(messageTokensEst, policy, {
|
|
|
267
325
|
pressureTokens,
|
|
268
326
|
} = {}) {
|
|
269
327
|
if (!policy?.auto || !policy.boundaryTokens) return false;
|
|
328
|
+
// send-with-recovery permits exactly one context-overflow retry per send
|
|
329
|
+
// (`contextOverflowRetryUsed`), so this can consume at most one additional
|
|
330
|
+
// reactive compact after a one-shot attempt; a second overflow is surfaced.
|
|
270
331
|
if (forceReactive) return true;
|
|
332
|
+
// A reserve at/above the trigger (or a one-token boundary)
|
|
333
|
+
// can never satisfy target < trigger. Permit one legacy compact attempt,
|
|
334
|
+
// then suppress automatic repeats until an operator intervenes.
|
|
335
|
+
if (policy.singleShot === true && sessionRef?.compaction?.singleShotConsumed === true) return false;
|
|
271
336
|
if (messageTokensEst === null) return true;
|
|
272
337
|
const pressure = Number.isFinite(Number(pressureTokens))
|
|
273
338
|
? Number(pressureTokens)
|
|
@@ -339,6 +404,9 @@ export function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
|
339
404
|
? Math.max(0, Math.round(Number(meta.durationMs)))
|
|
340
405
|
: null,
|
|
341
406
|
compactCount: (prev.compactCount || 0) + (changed ? 1 : 0),
|
|
407
|
+
singleShotConsumed: policy.singleShot === true && meta.stage === 'compacting'
|
|
408
|
+
? true
|
|
409
|
+
: prev.singleShotConsumed === true,
|
|
342
410
|
};
|
|
343
411
|
if (changed) {
|
|
344
412
|
const changedAt = Date.now();
|