mixdog 0.9.4 → 0.9.5
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 +112 -38
- package/package.json +4 -1
- package/scripts/bench/r5-orchestrated-task.json +7 -0
- package/scripts/bench/round-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
- package/scripts/bench-run.mjs +40 -7
- package/scripts/internal-comms-bench.mjs +2 -4
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- package/scripts/recall-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/tool-smoke.mjs +2 -2
- package/src/agents/heavy-worker/AGENT.md +7 -7
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +9 -10
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/mixdog-session-runtime.mjs +15 -14
- package/src/output-styles/default.md +14 -18
- package/src/output-styles/minimal.md +4 -5
- package/src/output-styles/simple.md +7 -8
- package/src/rules/agent/00-common.md +6 -5
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +6 -3
- package/src/rules/lead/lead-tool.md +6 -4
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +53 -9
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +23 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +33 -4
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +93 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +27 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/channels/backends/discord-gateway.mjs +2 -2
- package/src/runtime/channels/backends/discord.mjs +15 -26
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +23 -43
- package/src/runtime/channels/lib/backend-dispatch.mjs +5 -3
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +4 -4
- package/src/runtime/channels/lib/event-queue.mjs +15 -9
- package/src/runtime/channels/lib/inbound-routing.mjs +2 -39
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- package/src/runtime/channels/lib/scheduler.mjs +27 -113
- package/src/runtime/channels/lib/webhook/deliveries.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +8 -47
- package/src/runtime/memory/index.mjs +9 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- package/src/runtime/memory/lib/ko-morph.mjs +195 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +47 -10
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/query-handlers.mjs +27 -5
- package/src/runtime/memory/tool-defs.mjs +2 -2
- package/src/runtime/shared/config.mjs +14 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/session-runtime/config-helpers.mjs +45 -10
- package/src/session-runtime/quick-model-rows.mjs +46 -11
- package/src/session-runtime/quick-search-models.mjs +28 -27
- package/src/session-runtime/settings-api.mjs +34 -1
- package/src/session-runtime/tool-defs.mjs +1 -1
- package/src/session-runtime/workflow.mjs +1 -1
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/agent-tool.mjs +30 -0
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/explore-tool.mjs +39 -10
- package/src/tui/App.jsx +126 -28
- package/src/tui/app/app-format.mjs +11 -4
- package/src/tui/app/channel-pickers.mjs +2 -4
- package/src/tui/app/maintenance-pickers.mjs +60 -28
- package/src/tui/app/model-options.mjs +5 -1
- package/src/tui/app/settings-picker.mjs +0 -1
- package/src/tui/app/transcript-window.mjs +8 -2
- package/src/tui/app/use-transcript-scroll.mjs +3 -1
- package/src/tui/app/use-transcript-window.mjs +19 -1
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +32 -11
- package/src/tui/components/PromptInput.jsx +10 -1
- package/src/tui/components/StatusLine.jsx +6 -3
- package/src/tui/dist/index.mjs +440 -293
- package/src/tui/engine.mjs +48 -12
- package/src/tui/index.jsx +11 -52
- package/src/ui/statusline.mjs +10 -0
- package/src/workflows/default/WORKFLOW.md +15 -17
- package/src/workflows/sequential/WORKFLOW.md +15 -17
- package/vendor/ink/build/ink.js +9 -1
- package/scripts/recall-usecase-probe.json +0 -6
- package/src/runtime/channels/lib/holidays.mjs +0 -138
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
# Tool Use
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
`
|
|
12
|
-
|
|
13
|
-
patterns or scopes.
|
|
3
|
+
- Batch independent lookups in one turn; serialize only when a call needs a
|
|
4
|
+
prior result — including edit loops: batch the post-edit verification read
|
|
5
|
+
with the next target's lookup.
|
|
6
|
+
- Pick by target: symbols/callers/deps → `code_graph`; exact text in a verified
|
|
7
|
+
scope → `grep`; unknown path/name → `find`; structure → `glob`; dirs →
|
|
8
|
+
`list`; verified file → `read`; broad unknown with no anchor → `explore`.
|
|
9
|
+
Never `grep`/`read` guessed paths.
|
|
10
|
+
- One concept → one batched `grep pattern:[...]` with
|
|
11
|
+
`output_mode:"content_with_context"` (or one `code_graph symbols[]`). Refine
|
|
12
|
+
from returned paths; never repeat equivalent patterns or scopes.
|
|
14
13
|
- On miss/error, normalize the target once and switch tool; on a plausible hit,
|
|
15
|
-
stop
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`{path,offset,limit}` region objects instead of serial reads. Adjacent spans
|
|
23
|
-
in the same file (within a few hundred lines) are ONE window, not repeated
|
|
24
|
-
reads.
|
|
14
|
+
stop and answer from the framed context.
|
|
15
|
+
- Retrieval serves the NEXT action (edit, answer, handoff), not certainty. One
|
|
16
|
+
anchor is enough to act on; re-reading/re-grepping an area seen this session
|
|
17
|
+
is waste. When acting and looking are both possible, act.
|
|
18
|
+
- `read` uses `offset`/`limit` only. For 2+ spans from known file(s), make one
|
|
19
|
+
batched `read` with `{path,offset,limit}` region objects. Adjacent spans in
|
|
20
|
+
one file (within a few hundred lines) are ONE window, not repeated reads.
|
|
25
21
|
- Don't mix `apply_patch` with shell or other state-changing calls in one turn.
|
|
@@ -356,6 +356,10 @@ export function makeAgentDispatch(opts = {}) {
|
|
|
356
356
|
if (_idleController) {
|
|
357
357
|
try { _linkSignal(session.id, _idleController.signal); } catch { /* ignore */ }
|
|
358
358
|
}
|
|
359
|
+
// Watchdog blind spot guard: when the runtime snapshot is missing AND
|
|
360
|
+
// no progress timestamp exists (pre-liveness hang, swept runtime), the
|
|
361
|
+
// dispatch start time anchors staleness so the abort still fires.
|
|
362
|
+
const _watchdogAnchorTs = Date.now();
|
|
359
363
|
const _idleTimer = (_idleController && (typeof _getProgressSnapshot === 'function' || typeof _getLastProgressAt === 'function'))
|
|
360
364
|
? setInterval(() => {
|
|
361
365
|
const now = Date.now();
|
|
@@ -363,9 +367,10 @@ export function makeAgentDispatch(opts = {}) {
|
|
|
363
367
|
const abortErr = snapshot
|
|
364
368
|
? evaluateAgentWatchdogAbort(snapshot, now, _watchdogPolicy)
|
|
365
369
|
: null;
|
|
366
|
-
if (!abortErr && !snapshot
|
|
367
|
-
const
|
|
368
|
-
|
|
370
|
+
if (!abortErr && !snapshot) {
|
|
371
|
+
const reported = typeof _getLastProgressAt === 'function' ? _getLastProgressAt(session.id) : 0;
|
|
372
|
+
const last = reported || _watchdogAnchorTs;
|
|
373
|
+
if (_watchdogPolicy.idleStaleMs > 0 && now - last > _watchdogPolicy.idleStaleMs) {
|
|
369
374
|
try { _idleController.abort(new Error(`agent task stale (${_watchdogPolicy.idleStaleMs}ms without progress)`)); } catch { /* ignore */ }
|
|
370
375
|
}
|
|
371
376
|
return;
|
|
@@ -18,6 +18,10 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
18
18
|
import { join } from 'path';
|
|
19
19
|
import { getPluginData } from '../config.mjs';
|
|
20
20
|
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
21
|
+
import {
|
|
22
|
+
providerCachedModelMetadataSync,
|
|
23
|
+
providerUsesEndpointScopedLimits,
|
|
24
|
+
} from './provider-catalog-cache.mjs';
|
|
21
25
|
|
|
22
26
|
const CATALOG_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
23
27
|
const CATALOG_CACHE_FILE = 'litellm-catalog.json';
|
|
@@ -367,6 +371,7 @@ export function getModelsDevProviderModelsSync(provider, _test) {
|
|
|
367
371
|
export function getModelMetadataSync(id, provider) {
|
|
368
372
|
if (!id) return null;
|
|
369
373
|
const mappedProvider = provider ? _modelsDevProviderId(provider) : null;
|
|
374
|
+
const providerNative = provider ? providerCachedModelMetadataSync(provider, id) : null;
|
|
370
375
|
let meta = null;
|
|
371
376
|
// 1. Manual overrides — authoritative + offline. Provider-guarded: when a
|
|
372
377
|
// provider hint is given, an override is only honoured if it belongs to
|
|
@@ -402,6 +407,15 @@ export function getModelMetadataSync(id, provider) {
|
|
|
402
407
|
const md = _modelsDevMetadataSync(id, provider);
|
|
403
408
|
if (md) meta = mergeModelMetadata(meta, md, { preserveBaseCosts: metaFromPricingOverride });
|
|
404
409
|
}
|
|
410
|
+
if (providerUsesEndpointScopedLimits(provider) && !providerNative && meta) {
|
|
411
|
+
// OAuth/backend routes can expose smaller account/backend windows than
|
|
412
|
+
// the public API SKU. External catalogs remain useful for costs and
|
|
413
|
+
// capabilities, but their limits are not authoritative for these routes.
|
|
414
|
+
meta = { ...meta, contextWindow: null, outputTokens: null };
|
|
415
|
+
}
|
|
416
|
+
if (providerNative) {
|
|
417
|
+
meta = mergeModelMetadata(meta, providerNative, { preserveBaseCosts: true });
|
|
418
|
+
}
|
|
405
419
|
return meta;
|
|
406
420
|
}
|
|
407
421
|
|
|
@@ -493,6 +507,11 @@ export async function enrichModels(models) {
|
|
|
493
507
|
const row = mappedProvider ? _mdCache?.[mappedProvider]?.models?.[id] : null;
|
|
494
508
|
const providerMeta = row ? _normalize(_modelsDevRowToOverride(row)) : null;
|
|
495
509
|
if (providerMeta) meta = mergeModelMetadata(meta, providerMeta, { preserveBaseCosts: metaFromPricingOverride });
|
|
510
|
+
const providerNative = providerCachedModelMetadataSync(m.provider, id);
|
|
511
|
+
if (providerUsesEndpointScopedLimits(m.provider) && !providerNative && meta) {
|
|
512
|
+
meta = { ...meta, contextWindow: null, outputTokens: null };
|
|
513
|
+
}
|
|
514
|
+
if (providerNative) meta = mergeModelMetadata(meta, providerNative, { preserveBaseCosts: true });
|
|
496
515
|
}
|
|
497
516
|
if (!meta) return m;
|
|
498
517
|
return {
|
|
@@ -145,6 +145,14 @@ function _releaseEpoch(row) {
|
|
|
145
145
|
return Number.isFinite(t) ? t : null;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function _stalenessFamily(row, id) {
|
|
149
|
+
const modelId = String(id || row?.id || row?.name || '').trim().toLowerCase();
|
|
150
|
+
const claude = modelId.match(/(?:^|\/)claude-(opus|sonnet|haiku|fable)(?:-|$)/);
|
|
151
|
+
if (claude) return `claude-${claude[1]}`;
|
|
152
|
+
const family = row && typeof row.family === 'string' ? row.family.trim().toLowerCase() : '';
|
|
153
|
+
return family || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
148
156
|
function _staleMonths() {
|
|
149
157
|
const raw = process.env.MIXDOG_MODEL_STALE_MONTHS;
|
|
150
158
|
if (raw == null || raw === '') return 12;
|
|
@@ -169,7 +177,7 @@ function _applyAutoStaleness(kept, provider, testCatalog) {
|
|
|
169
177
|
return {
|
|
170
178
|
row,
|
|
171
179
|
cat: cr,
|
|
172
|
-
family: cr
|
|
180
|
+
family: cr ? _stalenessFamily(cr, row.id) : null,
|
|
173
181
|
epoch: cr ? _releaseEpoch(cr) : null,
|
|
174
182
|
};
|
|
175
183
|
});
|
|
@@ -179,8 +187,8 @@ function _applyAutoStaleness(kept, provider, testCatalog) {
|
|
|
179
187
|
// Compute the newest release_date per family from the FULL catalog so a
|
|
180
188
|
// superseding model that isn't in this list still counts.
|
|
181
189
|
const familyNewest = new Map();
|
|
182
|
-
for (const cr of Object.
|
|
183
|
-
const fam = cr
|
|
190
|
+
for (const [id, cr] of Object.entries(catModels)) {
|
|
191
|
+
const fam = _stalenessFamily(cr, id);
|
|
184
192
|
const ep = _releaseEpoch(cr);
|
|
185
193
|
if (!fam || ep == null) continue;
|
|
186
194
|
const prev = familyNewest.get(fam);
|
|
@@ -125,7 +125,15 @@ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
|
125
125
|
'chatgpt-account-id': auth.account_id || '',
|
|
126
126
|
'x-client-request-id': randomBytes(16).toString('hex'),
|
|
127
127
|
};
|
|
128
|
-
if (cacheKey)
|
|
128
|
+
if (cacheKey) {
|
|
129
|
+
const sid = String(cacheKey);
|
|
130
|
+
// Codex-native anchors (see openai-ws-pool _buildHandshakeHeaders):
|
|
131
|
+
// `session-id`/`thread-id` (hyphen) match codex-rs headers.rs; legacy
|
|
132
|
+
// underscore `session_id` kept for backward compat.
|
|
133
|
+
headers.session_id = sid;
|
|
134
|
+
headers['session-id'] = sid;
|
|
135
|
+
headers['thread-id'] = sid;
|
|
136
|
+
}
|
|
129
137
|
return headers;
|
|
130
138
|
}
|
|
131
139
|
|
|
@@ -288,6 +288,11 @@ function _codexWsCompatibilityHeaders(context = {}) {
|
|
|
288
288
|
const headers = {};
|
|
289
289
|
if (metadata['x-codex-window-id']) headers['x-codex-window-id'] = metadata['x-codex-window-id'];
|
|
290
290
|
if (metadata['x-codex-turn-metadata']) headers['x-codex-turn-metadata'] = metadata['x-codex-turn-metadata'];
|
|
291
|
+
// codex sends x-codex-installation-id on EVERY request incl. the WS
|
|
292
|
+
// handshake (client.rs:582-584). We previously carried it only in the
|
|
293
|
+
// body client_metadata; the server may gate x-codex-turn-state issuance
|
|
294
|
+
// on handshake-level client identity, so mirror codex here.
|
|
295
|
+
if (metadata['x-codex-installation-id']) headers['x-codex-installation-id'] = metadata['x-codex-installation-id'];
|
|
291
296
|
return headers;
|
|
292
297
|
}
|
|
293
298
|
|
|
@@ -655,10 +660,16 @@ export async function sendViaWebSocket({
|
|
|
655
660
|
let skippedResponseItems = 0;
|
|
656
661
|
let wireFrameHadTurnState = false;
|
|
657
662
|
let wireFrameMetadataTrace = _metadataTrace(null);
|
|
663
|
+
let framePrefixHash = null;
|
|
664
|
+
let framePrefixHeadHash = null;
|
|
665
|
+
let framePrefixPrevMatch = null;
|
|
658
666
|
let result;
|
|
659
667
|
const streamTimeouts = null;
|
|
660
668
|
try {
|
|
661
|
-
|
|
669
|
+
// codex prewarm gate (client.rs:1686-1688): only when the session
|
|
670
|
+
// has no prior request state. A reused pooled socket with a live
|
|
671
|
+
// chain must go straight to the real request.
|
|
672
|
+
if (warmupBody && typeof warmupBody === 'object' && attemptIndex === 0 && !entry.lastResponseId) {
|
|
662
673
|
const warmupFrame = { type: 'response.create', ...warmupBody };
|
|
663
674
|
const wireWarmupFrame = _withCodexWsClientMetadata(warmupFrame, entry, useCodexWsClientMetadata, codexMetadataContext);
|
|
664
675
|
wireFrameHadTurnState = !!wireWarmupFrame?.client_metadata?.['x-codex-turn-state'];
|
|
@@ -723,13 +734,15 @@ export async function sendViaWebSocket({
|
|
|
723
734
|
payload: warmupPayload,
|
|
724
735
|
});
|
|
725
736
|
} catch {}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
737
|
+
// Do NOT rewrite the main request after warmup (R23 finding).
|
|
738
|
+
// The old prev_id+no-instructions rewrite made it=1's frame a
|
|
739
|
+
// different shape from it=2+ full frames, so the first real
|
|
740
|
+
// full-frame cache write only landed at it=2 and it=3 raced
|
|
741
|
+
// its propagation (cached=0 early misses). Keeping it=1 as a
|
|
742
|
+
// normal full frame makes it byte-identical to the warmup's
|
|
743
|
+
// prefix (instant full hit on the cache warmup just wrote)
|
|
744
|
+
// and keeps every subsequent frame one consistent shape.
|
|
745
|
+
// Delta opt-in still chains via entry.lastResponseId above.
|
|
733
746
|
}
|
|
734
747
|
|
|
735
748
|
const delta = _computeDelta({ entry, body: requestBody });
|
|
@@ -741,6 +754,30 @@ export async function sendViaWebSocket({
|
|
|
741
754
|
wireFrameHadTurnState = !!wireFrame?.client_metadata?.['x-codex-turn-state'];
|
|
742
755
|
wireFrameMetadataTrace = _metadataTrace(wireFrame?.client_metadata);
|
|
743
756
|
deltaTokens = _estimateFrameTokens(wireFrame);
|
|
757
|
+
// Prefix-consistency probe (item-level). Serialized-JSON byte
|
|
758
|
+
// prefixes can never match across appends (the shorter frame ends
|
|
759
|
+
// in "]}" where the longer has ","), so compare what the server's
|
|
760
|
+
// prefix cache actually sees: the non-input request header and the
|
|
761
|
+
// per-item content of the input array. prev_match=true means the
|
|
762
|
+
// current call's header is identical and its first N input items
|
|
763
|
+
// equal the previous call's N items (append-only history).
|
|
764
|
+
try {
|
|
765
|
+
const { client_metadata: _cm, input: frameInput, ...frameHeader } = frame;
|
|
766
|
+
const headerHash = _hashText(JSON.stringify(frameHeader), 16);
|
|
767
|
+
const itemHashes = (Array.isArray(frameInput) ? frameInput : [])
|
|
768
|
+
.map((item) => _hashText(JSON.stringify(item), 12));
|
|
769
|
+
framePrefixHash = headerHash;
|
|
770
|
+
framePrefixHeadHash = _hashText(itemHashes.join(','), 16);
|
|
771
|
+
const prevHeader = entry.lastFrameHeaderHash;
|
|
772
|
+
const prevItems = entry.lastFrameItemHashes;
|
|
773
|
+
if (prevHeader && Array.isArray(prevItems)) {
|
|
774
|
+
framePrefixPrevMatch = headerHash === prevHeader
|
|
775
|
+
&& itemHashes.length >= prevItems.length
|
|
776
|
+
&& prevItems.every((h, i) => itemHashes[i] === h);
|
|
777
|
+
}
|
|
778
|
+
entry.lastFrameHeaderHash = headerHash;
|
|
779
|
+
entry.lastFrameItemHashes = itemHashes;
|
|
780
|
+
} catch {}
|
|
744
781
|
|
|
745
782
|
// Re-check abort after acquire/warmup — narrow window where
|
|
746
783
|
// externalSignal could fire between successful acquire and
|
|
@@ -895,6 +932,11 @@ export async function sendViaWebSocket({
|
|
|
895
932
|
entry.lastInputPrefixHash = null;
|
|
896
933
|
}
|
|
897
934
|
|
|
935
|
+
// Cache observation must see the MAIN request's usage only. Folding
|
|
936
|
+
// warmup usage in first (R18) made prompt_tokens spike on it=1 and
|
|
937
|
+
// then "shrink" on it=2, faking prefix-rewrite/cache-drop signals in
|
|
938
|
+
// every warmup session (debugger 2026-07-03).
|
|
939
|
+
const cacheObservation = _cacheObservation({ entry, result });
|
|
898
940
|
if (warmupResult?.usage) {
|
|
899
941
|
result.usage = _combineUsageWithWarmup(result.usage, warmupResult.usage);
|
|
900
942
|
}
|
|
@@ -915,7 +957,6 @@ export async function sendViaWebSocket({
|
|
|
915
957
|
provider: traceProvider,
|
|
916
958
|
serviceTier: responseServiceTier,
|
|
917
959
|
});
|
|
918
|
-
const cacheObservation = _cacheObservation({ entry, result });
|
|
919
960
|
const requestHasPreviousResponseId = typeof frame.previous_response_id === 'string' && frame.previous_response_id.length > 0;
|
|
920
961
|
const transportCacheKeyHash = cacheKey
|
|
921
962
|
? createHash('sha256').update(String(cacheKey)).digest('hex').slice(0, 12)
|
|
@@ -987,6 +1028,9 @@ export async function sendViaWebSocket({
|
|
|
987
1028
|
actual_cache_miss_reason: cacheObservation.missReason,
|
|
988
1029
|
previous_max_cached_tokens: cacheObservation.previousMaxCached,
|
|
989
1030
|
cache_drop_threshold_tokens: cacheObservation.dropThreshold,
|
|
1031
|
+
frame_prefix_hash: framePrefixHash,
|
|
1032
|
+
frame_prefix_head_hash: framePrefixHeadHash,
|
|
1033
|
+
frame_prefix_prev_match: framePrefixPrevMatch,
|
|
990
1034
|
ws_client_metadata: useCodexWsClientMetadata,
|
|
991
1035
|
ws_client_metadata_key_count: wireFrameMetadataTrace.count,
|
|
992
1036
|
ws_client_metadata_hash: wireFrameMetadataTrace.hash,
|
|
@@ -564,9 +564,11 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
564
564
|
// normalizes `ultra` -> `max` on the wire (reasoning_effort_for_request
|
|
565
565
|
// in core/src/client.rs); the openai-oauth backend does not accept
|
|
566
566
|
// `ultra` as a wire value, so mirror that mapping here.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
567
|
+
// WIRE-VERIFIED (codex desktop logs_2.sqlite, 40 response.create
|
|
568
|
+
// captures, 2026-07-03): codex sends reasoning as {"effort":"..."}
|
|
569
|
+
// with NO summary field on gpt-5.5, regardless of what the repo's
|
|
570
|
+
// build_reasoning() suggests. Match the observed bytes.
|
|
571
|
+
reasoning: { effort: _normalizeReasoningEffort(opts.effort) },
|
|
570
572
|
store: process.env.MIXDOG_OAI_STORE === 'true' ? true : false,
|
|
571
573
|
stream: true,
|
|
572
574
|
include,
|
|
@@ -612,13 +614,12 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
612
614
|
cacheLaneSlot: promptCacheLane.slot,
|
|
613
615
|
cacheLaneShards: promptCacheLane.shards,
|
|
614
616
|
});
|
|
615
|
-
// codex
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
|
|
619
|
-
const verbosity = typeof opts.verbosity === 'string' && opts.verbosity.trim()
|
|
617
|
+
// WIRE-VERIFIED (codex desktop logs, 2026-07-03): every live gpt-5.5
|
|
618
|
+
// response.create carries text:{"verbosity":"low"} (or a schema variant);
|
|
619
|
+
// none omit the field. Default to codex's observed "low", allow override.
|
|
620
|
+
const verbosity = (typeof opts.verbosity === 'string' && opts.verbosity.trim()
|
|
620
621
|
? opts.verbosity.trim().toLowerCase()
|
|
621
|
-
: null;
|
|
622
|
+
: null) || 'low';
|
|
622
623
|
// Rebuild the body in codex struct order so JSON serialization is
|
|
623
624
|
// byte-compatible with codex: ... input, tools, tool_choice,
|
|
624
625
|
// parallel_tool_calls, reasoning, store, stream, include, service_tier,
|
|
@@ -636,7 +637,7 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
636
637
|
include: body.include,
|
|
637
638
|
...(body.service_tier ? { service_tier: body.service_tier } : {}),
|
|
638
639
|
prompt_cache_key: promptCacheKey,
|
|
639
|
-
|
|
640
|
+
text: { verbosity },
|
|
640
641
|
...(body.max_output_tokens ? { max_output_tokens: body.max_output_tokens } : {}),
|
|
641
642
|
};
|
|
642
643
|
// NOTE: prompt_cache_retention is a public OpenAI Responses API parameter,
|
|
@@ -911,6 +912,18 @@ export class OpenAIOAuthProvider {
|
|
|
911
912
|
useModel,
|
|
912
913
|
displayModel: _displayCodexModel,
|
|
913
914
|
forceFresh,
|
|
915
|
+
// codex-parity prewarm (generate:false full frame on a fresh
|
|
916
|
+
// socket, wire-verified 2026-07-03). DEFAULT ON: R19(off) vs
|
|
917
|
+
// R20(on) A/B shows prewarm removes ALL early-session zero-cache
|
|
918
|
+
// misses (4 -> 0 at it<=3; zero-miss 3 -> 0) by writing the cache
|
|
919
|
+
// and waiting for completion before the first real turn, exactly
|
|
920
|
+
// like codex prewarm_websocket (client.rs:1673-1705). Cost is one
|
|
921
|
+
// small (~5k tok) generate:false request per fresh socket, far
|
|
922
|
+
// below the 5-12k uncached tokens each early miss burned.
|
|
923
|
+
// Mid-session partial drops are server-side and unaffected.
|
|
924
|
+
warmupBody: _envFlag('MIXDOG_OPENAI_OAUTH_WS_WARMUP', true)
|
|
925
|
+
? { ...body, generate: false }
|
|
926
|
+
: null,
|
|
914
927
|
});
|
|
915
928
|
if (opts.forceHttpFallback === true
|
|
916
929
|
|| httpFallbackActive()
|
|
@@ -171,15 +171,17 @@ export function _stripResponseItemsFromHead(items, responseItems) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
export function _computeDelta({ entry, body }) {
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if
|
|
182
|
-
|
|
174
|
+
// DEFAULT: full-frame sends. codex's delta path is only cache-safe with
|
|
175
|
+
// the x-codex-turn-state sticky-routing token, which the backend issues to
|
|
176
|
+
// codex but never to us (R11-R14 2026-07-03: zero turn-state events across
|
|
177
|
+
// 200+ calls despite UA/version/beta-features/client_metadata parity;
|
|
178
|
+
// delta measured 18-28% warm miss vs full-frame 0.0%). Without the sticky
|
|
179
|
+
// token, previous_response_id requests hop cache nodes and only the first
|
|
180
|
+
// prefix blocks hit. Re-enable delta explicitly via MIXDOG_OAI_WS_DELTA=1
|
|
181
|
+
// for future probes if the backend starts issuing turn-state.
|
|
182
|
+
const deltaOptIn = ['1', 'true', 'yes', 'on'].includes(String(process.env.MIXDOG_OAI_WS_DELTA || '').trim().toLowerCase());
|
|
183
|
+
if (!deltaOptIn) {
|
|
184
|
+
return { mode: 'full', reason: 'full_default', frame: { type: 'response.create', ...body } };
|
|
183
185
|
}
|
|
184
186
|
if (!entry || !entry.lastRequestSansInput || !entry.lastResponseId) {
|
|
185
187
|
return { mode: 'full', reason: 'no_anchor', frame: { type: 'response.create', ...body } };
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import WebSocket from 'ws';
|
|
10
10
|
import { errText } from '../../../shared/err-text.mjs';
|
|
11
11
|
import { createHash, randomBytes } from 'crypto';
|
|
12
|
+
import { appendFileSync } from 'node:fs';
|
|
12
13
|
import { codexUserAgent, codexVersionHeader } from './codex-client-meta.mjs';
|
|
13
14
|
import {
|
|
14
15
|
PROVIDER_WS_ACQUIRE_TIMEOUT_MS,
|
|
@@ -162,12 +163,22 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
|
|
|
162
163
|
// (colder) cache-node class than codex.
|
|
163
164
|
'User-Agent': codexUserAgent(),
|
|
164
165
|
'version': codexVersionHeader(),
|
|
166
|
+
// codex advertises enabled beta features on every request incl.
|
|
167
|
+
// the WS handshake (client.rs:1038-1041 via build_responses_headers,
|
|
168
|
+
// session/mod.rs:1006-1027). With a default config the list is
|
|
169
|
+
// exactly "remote_compaction_v2" (features/src/lib.rs: the only
|
|
170
|
+
// always-advertised Stable default-on feature). Servers gate
|
|
171
|
+
// behavior (plausibly incl. x-codex-turn-state issuance) on it.
|
|
172
|
+
'x-codex-beta-features': process.env.MIXDOG_CODEX_BETA_FEATURES || 'remote_compaction_v2',
|
|
165
173
|
};
|
|
166
174
|
const isOpenAiOauth = auth.type !== 'xai' && auth.type !== 'openai-direct';
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
// codex-rs sends only the dashed session-id/thread-id pair
|
|
176
|
+
// (client.rs:1033-1057), but OUR backend measurements disagree with pure
|
|
177
|
+
// codex parity here: 2026-04-19 probes showed the OAuth backend dedupes
|
|
178
|
+
// its in-memory prefix state by the underscore session_id handshake
|
|
179
|
+
// header, and the only 0.0%-miss full-frame rounds (R7/R8, R15 regressed
|
|
180
|
+
// to 13% after this header was dropped) all had it present. Send both.
|
|
181
|
+
if (sessionToken) {
|
|
171
182
|
headers['session_id'] = String(sessionToken);
|
|
172
183
|
}
|
|
173
184
|
if (isOpenAiOauth && (sessionToken || _cacheKey)) {
|
|
@@ -249,6 +260,24 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey,
|
|
|
249
260
|
try {
|
|
250
261
|
const ts = res?.headers?.['x-codex-turn-state'];
|
|
251
262
|
if (typeof ts === 'string' && ts.length) capturedHeaders.turnState = ts;
|
|
263
|
+
// Probe: dump the full 101-upgrade response header set so we can
|
|
264
|
+
// see what the server actually issues (turn-state investigation).
|
|
265
|
+
if (process.env.MIXDOG_WS_UPGRADE_HEADER_PROBE) {
|
|
266
|
+
const all = res?.headers && typeof res.headers === 'object'
|
|
267
|
+
? Object.entries(res.headers).map(([k, v]) => `${k}: ${String(v).slice(0, 120)}`).join(' | ')
|
|
268
|
+
: '(none)';
|
|
269
|
+
const line = `[ws-upgrade-probe] ts=${new Date().toISOString()} status=${res?.statusCode} headers={ ${all} }\n`;
|
|
270
|
+
process.stderr.write(line);
|
|
271
|
+
// Bench runners swallow child stderr on success; persist to a
|
|
272
|
+
// file so the probe survives (value of the env var = path, or
|
|
273
|
+
// default under tmp).
|
|
274
|
+
try {
|
|
275
|
+
const probePath = process.env.MIXDOG_WS_UPGRADE_HEADER_PROBE !== '1'
|
|
276
|
+
? process.env.MIXDOG_WS_UPGRADE_HEADER_PROBE
|
|
277
|
+
: `${process.env.TEMP || process.env.TMPDIR || '.'}/mixdog-ws-upgrade-probe.log`;
|
|
278
|
+
appendFileSync(probePath, line);
|
|
279
|
+
} catch {}
|
|
280
|
+
}
|
|
252
281
|
} catch {}
|
|
253
282
|
});
|
|
254
283
|
socket.once('open', () => {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { getPluginData } from '../config.mjs';
|
|
4
|
+
|
|
5
|
+
const PROVIDER_CACHE_FILES = Object.freeze({
|
|
6
|
+
'openai-oauth': ['openai-oauth-models.json'],
|
|
7
|
+
'anthropic-oauth': ['anthropic-oauth-models.json'],
|
|
8
|
+
anthropic: ['anthropic-oauth-models.json'],
|
|
9
|
+
gemini: ['gemini-models.json'],
|
|
10
|
+
'grok-oauth': ['grok-oauth-models.json'],
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function providerUsesEndpointScopedLimits(provider) {
|
|
14
|
+
const p = String(provider || '').toLowerCase();
|
|
15
|
+
return p === 'openai-oauth' || p === 'anthropic-oauth' || p === 'grok-oauth';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function num(value) {
|
|
19
|
+
const n = Number(value);
|
|
20
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readModelsFromCacheFile(fileName) {
|
|
24
|
+
try {
|
|
25
|
+
const file = join(getPluginData(), fileName);
|
|
26
|
+
if (!existsSync(file)) return [];
|
|
27
|
+
const raw = JSON.parse(readFileSync(file, 'utf-8'));
|
|
28
|
+
return Array.isArray(raw?.models) ? raw.models : (Array.isArray(raw) ? raw : []);
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function modelAliases(id) {
|
|
35
|
+
const text = String(id || '').trim();
|
|
36
|
+
if (!text) return [];
|
|
37
|
+
const aliases = new Set([text]);
|
|
38
|
+
aliases.add(text.replace(/-\d{4}-\d{2}-\d{2}$/, ''));
|
|
39
|
+
aliases.add(text.replace(/-\d{8}$/, ''));
|
|
40
|
+
return [...aliases].filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rowId(row) {
|
|
44
|
+
return String(row?.id || row?.name || row?.slug || '').trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getProviderCachedModelSync(provider, model) {
|
|
48
|
+
const p = String(provider || '').toLowerCase();
|
|
49
|
+
const files = PROVIDER_CACHE_FILES[p] || [];
|
|
50
|
+
if (!files.length || !model) return null;
|
|
51
|
+
const aliases = new Set(modelAliases(model));
|
|
52
|
+
for (const fileName of files) {
|
|
53
|
+
for (const row of readModelsFromCacheFile(fileName)) {
|
|
54
|
+
const id = rowId(row);
|
|
55
|
+
if (!id || !aliases.has(id)) continue;
|
|
56
|
+
return row;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function providerCachedModelMetadataSync(provider, model) {
|
|
63
|
+
const row = getProviderCachedModelSync(provider, model);
|
|
64
|
+
if (!row) return null;
|
|
65
|
+
return {
|
|
66
|
+
contextWindow: num(row.contextWindow ?? row.context_window ?? row.maxContextWindow ?? row.max_context_window ?? row.max_input_tokens),
|
|
67
|
+
outputTokens: num(row.outputTokens ?? row.maxOutputTokens ?? row.max_output_tokens ?? row.output_token_limit),
|
|
68
|
+
supportsVision: row.supportsVision === true,
|
|
69
|
+
supportsFunctionCalling: row.supportsFunctionCalling === true,
|
|
70
|
+
supportsWebSearch: row.supportsWebSearch === true,
|
|
71
|
+
supportsPromptCaching: row.supportsPromptCaching === true,
|
|
72
|
+
supportsReasoning: row.supportsReasoning === true,
|
|
73
|
+
reasoningOptions: Array.isArray(row.reasoningOptions) ? row.reasoningOptions : [],
|
|
74
|
+
reasoningContentField: row.reasoningContentField || null,
|
|
75
|
+
mode: row.mode || null,
|
|
76
|
+
displayName: row.displayName || row.display || row.name || null,
|
|
77
|
+
name: row.name || row.display || null,
|
|
78
|
+
rawProviderModel: row,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -21,6 +21,53 @@ import {
|
|
|
21
21
|
compactDebugLog,
|
|
22
22
|
} from './compact-debug.mjs';
|
|
23
23
|
import { positiveTokenInt } from './env.mjs';
|
|
24
|
+
import { TOOL_OUTPUT_MAX_BYTES } from '../../tools/builtin/tool-output-limit.mjs';
|
|
25
|
+
|
|
26
|
+
// ── Digest mode (compaction.recallDigest=true) ─────────────────────────────
|
|
27
|
+
// Instead of folding the FULL chunked session dump into the compacted
|
|
28
|
+
// messages (heavy: cycle1 drain + up to CONTEXT_SHARE_RATIO of the context
|
|
29
|
+
// window), inject a small newest-first digest plus an instruction telling the
|
|
30
|
+
// model to pull details lazily via recall(sessionId/query/period). The memory
|
|
31
|
+
// DB already holds the full session (ingest_session below runs in both
|
|
32
|
+
// modes), and raw rows are embedded synchronously at ingest, so recall serves
|
|
33
|
+
// everything the big injection used to carry.
|
|
34
|
+
// Default digest cap = the SHARED tool-output limit (TOOL_OUTPUT_MAX_BYTES,
|
|
35
|
+
// 50KB default, env MIXDOG_TOOL_OUTPUT_MAX_BYTES) — the digest injection is
|
|
36
|
+
// budgeted like any other tool result, not a special context share.
|
|
37
|
+
// compaction.recallDigestMaxKb still overrides per-session.
|
|
38
|
+
const DIGEST_DEFAULT_MAX_KB = Math.max(1, Math.floor(TOOL_OUTPUT_MAX_BYTES / 1024));
|
|
39
|
+
|
|
40
|
+
// Byte-capped line-boundary truncation. Digest source is newest-first, so
|
|
41
|
+
// keeping the HEAD keeps the newest turns.
|
|
42
|
+
function truncateToKb(text, maxKb) {
|
|
43
|
+
const maxBytes = Math.max(1, maxKb) * 1024;
|
|
44
|
+
const s = String(text || '');
|
|
45
|
+
if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;
|
|
46
|
+
const lines = s.split('\n');
|
|
47
|
+
const out = [];
|
|
48
|
+
let used = 0;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const cost = Buffer.byteLength(line, 'utf8') + 1;
|
|
51
|
+
if (used + cost > maxBytes) break;
|
|
52
|
+
out.push(line);
|
|
53
|
+
used += cost;
|
|
54
|
+
}
|
|
55
|
+
return out.join('\n') + '\n[digest truncated at ' + maxKb + 'KB — pull the rest via recall]';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildRecallDigestText(sessionId, digestBody, maxKb) {
|
|
59
|
+
// No recall-usage instruction block here: the recall tool description
|
|
60
|
+
// already carries the usage-pattern cheatsheet (tool-defs.mjs), so
|
|
61
|
+
// repeating it per-compaction would be redundant injected tokens. The
|
|
62
|
+
// one-line header marks the compaction boundary and names the session id
|
|
63
|
+
// the model needs for a scoped recall.
|
|
64
|
+
return [
|
|
65
|
+
`[context compacted — session ${sessionId}]`,
|
|
66
|
+
`Full history is in memory — use the recall tool for details beyond this digest.`,
|
|
67
|
+
`Recent digest (newest first):`,
|
|
68
|
+
truncateToKb(digestBody, maxKb),
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
24
71
|
|
|
25
72
|
export async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTokens, compactPolicy, sessionId, signal }) {
|
|
26
73
|
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
@@ -73,6 +120,52 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
73
120
|
} finally {
|
|
74
121
|
diagnostics.ingestMs = Date.now() - t0;
|
|
75
122
|
}
|
|
123
|
+
// ── Digest mode: skip the dump + cycle1 drain entirely. Pull a small
|
|
124
|
+
// newest-first session browse (recall path: roots + raw fallback merged
|
|
125
|
+
// chronologically), cap it at recallDigestMaxKb, and inject it with a
|
|
126
|
+
// recall-usage instruction. The model pulls older/topical detail lazily.
|
|
127
|
+
const digestMode = sessionRef?.compaction?.recallDigest === true;
|
|
128
|
+
if (digestMode) {
|
|
129
|
+
const digestMaxKb = positiveTokenInt(sessionRef?.compaction?.recallDigestMaxKb) || DIGEST_DEFAULT_MAX_KB;
|
|
130
|
+
let digestBody = '';
|
|
131
|
+
t0 = Date.now();
|
|
132
|
+
try {
|
|
133
|
+
const browsed = await executeInternalTool('memory', {
|
|
134
|
+
action: 'search',
|
|
135
|
+
sessionId,
|
|
136
|
+
limit: positiveTokenInt(sessionRef?.compaction?.recallDigestLimit) || 30,
|
|
137
|
+
includeMembers: true,
|
|
138
|
+
}, callerCtx);
|
|
139
|
+
digestBody = typeof browsed === 'string' ? browsed : String(browsed?.text ?? browsed ?? '');
|
|
140
|
+
} catch (err) {
|
|
141
|
+
diagnostics.cycle1Error = compactDiagnosticError(err);
|
|
142
|
+
try { process.stderr.write(`[loop] recall-digest browse failed (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
143
|
+
}
|
|
144
|
+
diagnostics.initialDumpMs = Date.now() - t0;
|
|
145
|
+
diagnostics.cycle1Skipped = true;
|
|
146
|
+
diagnostics.cycle1SkipReason = 'digest mode';
|
|
147
|
+
diagnostics.cycle1Passes = 0;
|
|
148
|
+
const digestText = buildRecallDigestText(sessionId, digestBody, digestMaxKb);
|
|
149
|
+
diagnostics.finalRecallChars = digestText.length;
|
|
150
|
+
diagnostics.finalRecallBytes = compactByteLength(digestText);
|
|
151
|
+
const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
|
|
152
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
153
|
+
force: true,
|
|
154
|
+
recallText: digestText,
|
|
155
|
+
query,
|
|
156
|
+
querySha,
|
|
157
|
+
allowEmptyRecall: true,
|
|
158
|
+
tailTurns: compactPolicy.tailTurns,
|
|
159
|
+
keepTokens: compactPolicy.keepTokens,
|
|
160
|
+
preserveRecentTokens: compactPolicy.preserveRecentTokens,
|
|
161
|
+
});
|
|
162
|
+
diagnostics.totalMs = Date.now() - startedAt;
|
|
163
|
+
if (result && typeof result === 'object') {
|
|
164
|
+
result.diagnostics = { ...(result.diagnostics || {}), pipeline: { ...diagnostics, digestMode: true } };
|
|
165
|
+
}
|
|
166
|
+
compactDebugLog('recall-digest pipeline', diagnostics);
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
76
169
|
const dumpArgs = {
|
|
77
170
|
action: 'dump_session_roots',
|
|
78
171
|
sessionId,
|