mixdog 0.9.85 → 0.9.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +4 -3
- package/src/defaults/agents.json +12 -0
- package/src/defaults/skills/setup/SKILL.md +44 -12
- package/src/help.mjs +40 -9
- package/src/lib/keychain-cjs.cjs +11 -1
- package/src/rules/agent/43-title-agent.md +22 -0
- package/src/rules/shared/01-tool.md +3 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +135 -9
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
- package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +26 -0
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
- package/src/runtime/media/adapters/gemini-video.mjs +1 -1
- package/src/runtime/media/renditions.mjs +1 -1
- package/src/runtime/media/store.mjs +1 -1
- package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
- package/src/runtime/memory/tool-defs.mjs +19 -3
- package/src/runtime/shared/atomic-file.mjs +25 -14
- package/src/runtime/shared/automation-attachments.mjs +3 -3
- package/src/runtime/shared/child-guardian.mjs +15 -5
- package/src/runtime/shared/child-spawn-gate.mjs +6 -12
- package/src/runtime/shared/resource-admission.mjs +7 -3
- package/src/runtime/shared/turn-snapshot.mjs +0 -3
- package/src/session-runtime/lifecycle-api.mjs +49 -9
- package/src/session-runtime/prewarm.mjs +35 -0
- package/src/session-runtime/provider-auth-api.mjs +8 -0
- package/src/session-runtime/provider-usage.mjs +30 -5
- package/src/session-runtime/remote-control.mjs +8 -47
- package/src/session-runtime/remote-transcript.mjs +3 -7
- package/src/session-runtime/route-preparation.mjs +24 -0
- package/src/session-runtime/runtime-core.mjs +286 -548
- package/src/session-runtime/session-lifecycle.mjs +378 -0
- package/src/session-runtime/session-turn-api.mjs +9 -2
- package/src/session-runtime/workflow-agents-api.mjs +3 -0
- package/src/standalone/agent-shard/shard-child.mjs +300 -0
- package/src/standalone/agent-shard/shard-pool.mjs +443 -0
- package/src/standalone/agent-tool/job-views.mjs +329 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
- package/src/standalone/agent-tool/tag-registry.mjs +340 -0
- package/src/standalone/agent-tool.mjs +112 -941
- package/src/standalone/channel-daemon-transport.mjs +126 -185
- package/src/standalone/channel-daemon.mjs +9 -10
- package/src/standalone/usage-dashboard.mjs +23 -1
- package/src/tui/App.jsx +362 -2569
- package/src/tui/app/app-view.jsx +504 -0
- package/src/tui/app/channel-pickers.mjs +3 -8
- package/src/tui/app/create-app-pickers.mjs +313 -0
- package/src/tui/app/prompt-submit.mjs +501 -0
- package/src/tui/app/route-pickers.mjs +117 -152
- package/src/tui/app/settings-picker.mjs +90 -89
- package/src/tui/app/shell-layout.mjs +563 -0
- package/src/tui/app/slash-commands.mjs +7 -7
- package/src/tui/app/slash-dispatch.mjs +0 -45
- package/src/tui/app/usage-context-panels.mjs +288 -0
- package/src/tui/app/use-copy-selection.mjs +56 -0
- package/src/tui/app/use-global-key-input.mjs +151 -0
- package/src/tui/app/use-pasted-buffers.mjs +115 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
- package/src/tui/app/use-prompt-hint.mjs +58 -0
- package/src/tui/app/use-prompt-queue-history.mjs +100 -0
- package/src/tui/app/use-terminal-chrome.mjs +72 -0
- package/src/tui/app/use-transcript-activity.mjs +131 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
- package/src/tui/dist/index.mjs +9857 -9159
- package/src/tui/engine/session-api-ext.mjs +47 -9
- package/src/tui/engine.mjs +7 -1
- package/src/tui/figures.mjs +0 -1
- package/src/ui/statusline-format.mjs +0 -1
- package/src/hooks/lib/permission-evaluator.cjs +0 -24
- package/src/lib/config-cjs.cjs +0 -61
- package/src/runtime/shared/launcher-control.mjs +0 -258
- package/src/runtime/shared/workspace-router.mjs +0 -259
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
existsSync,
|
|
3
3
|
readFileSync,
|
|
4
4
|
} from 'fs';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
5
6
|
import { join } from 'path';
|
|
6
7
|
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
7
8
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
@@ -15,6 +16,8 @@ const STALE_DISK_CACHE_TTL_MS = 7 * 24 * 60 * 60_000;
|
|
|
15
16
|
const NEGATIVE_CACHE_TTL_MS = 5 * 60_000;
|
|
16
17
|
const FETCH_TIMEOUT_MS = 4500;
|
|
17
18
|
const WARN_TTL_MS = 5 * 60_000;
|
|
19
|
+
const CODEX_RESET_CREDITS_URL = 'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits';
|
|
20
|
+
const CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
|
|
18
21
|
|
|
19
22
|
const memoryCache = new Map();
|
|
20
23
|
const inflight = new Map();
|
|
@@ -199,6 +202,110 @@ function resetAtMs(value, fallbackSeconds = null) {
|
|
|
199
202
|
return secs > 0 ? Date.now() + secs * 1000 : null;
|
|
200
203
|
}
|
|
201
204
|
|
|
205
|
+
function codexAuthShape(auth) {
|
|
206
|
+
const token = auth?.access_token || auth?.accessToken;
|
|
207
|
+
if (!token) return null;
|
|
208
|
+
return {
|
|
209
|
+
token,
|
|
210
|
+
accountId: cleanString(auth?.account_id || auth?.accountId),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function codexHeaders(auth, beta = 'codex-1') {
|
|
215
|
+
return {
|
|
216
|
+
Authorization: `Bearer ${auth.token}`,
|
|
217
|
+
originator: 'Codex Desktop',
|
|
218
|
+
...(auth.accountId ? { 'chatgpt-account-id': auth.accountId } : {}),
|
|
219
|
+
'OpenAI-Beta': beta,
|
|
220
|
+
Accept: 'application/json',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizedResetCreditRows(value) {
|
|
225
|
+
return (Array.isArray(value) ? value : []).map((credit) => ({
|
|
226
|
+
status: cleanString(credit?.status).toLowerCase(),
|
|
227
|
+
expiresAt: resetAtMs(credit?.expires_at ?? credit?.expiresAt),
|
|
228
|
+
grantedAt: resetAtMs(credit?.granted_at ?? credit?.grantedAt),
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeOpenAICodexResetCredits(data, accountId = '') {
|
|
233
|
+
if (!data || typeof data !== 'object') return null;
|
|
234
|
+
const credits = normalizedResetCreditRows(data.credits);
|
|
235
|
+
const explicitCount = num(data.available_count ?? data.availableCount, null);
|
|
236
|
+
const availableRows = credits.filter((credit) => credit.status === 'available');
|
|
237
|
+
if (explicitCount === null && !credits.length) return null;
|
|
238
|
+
const availableCount = Math.max(0, Math.floor(explicitCount ?? availableRows.length));
|
|
239
|
+
const expiryCandidates = availableRows
|
|
240
|
+
.map((credit) => credit.expiresAt)
|
|
241
|
+
.filter((value) => Number.isFinite(value) && value > 0);
|
|
242
|
+
const nextExpiresAt = resetAtMs(data.next_expires_at ?? data.nextExpiresAt)
|
|
243
|
+
|| (expiryCandidates.length ? Math.min(...expiryCandidates) : null);
|
|
244
|
+
const offerRevision = `v1:${createHash('sha256').update(JSON.stringify({
|
|
245
|
+
accountId,
|
|
246
|
+
availableCount,
|
|
247
|
+
nextExpiresAt,
|
|
248
|
+
credits,
|
|
249
|
+
})).digest('hex')}`;
|
|
250
|
+
return {
|
|
251
|
+
availableCount,
|
|
252
|
+
...(nextExpiresAt ? { nextExpiresAt } : {}),
|
|
253
|
+
offerRevision,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function resolveOpenAICodexAuth(providerObj) {
|
|
258
|
+
return codexAuthShape(await providerObj?.ensureAuth?.({ reason: 'usage' }));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function fetchOpenAICodexResetCreditsWithAuth(auth) {
|
|
262
|
+
const response = await fetch(CODEX_RESET_CREDITS_URL, fetchOptions(codexHeaders(auth)));
|
|
263
|
+
if (!response.ok) return null;
|
|
264
|
+
return normalizeOpenAICodexResetCredits(await response.json(), auth.accountId);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function fetchOpenAICodexResetCredits(providerObj) {
|
|
268
|
+
const auth = await resolveOpenAICodexAuth(providerObj);
|
|
269
|
+
return auth ? await fetchOpenAICodexResetCreditsWithAuth(auth) : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function codexResetOutcome(code) {
|
|
273
|
+
if (code === 'reset') return 'reset';
|
|
274
|
+
if (code === 'nothing_to_reset') return 'nothingToReset';
|
|
275
|
+
if (code === 'no_credit') return 'noCredit';
|
|
276
|
+
if (code === 'already_redeemed') return 'alreadyRedeemed';
|
|
277
|
+
throw new Error(`Unknown Codex reset outcome: ${cleanString(code) || 'missing'}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function consumeOpenAICodexResetCredit(providerObj, options = {}) {
|
|
281
|
+
const expectedOfferRevision = cleanString(options?.expectedOfferRevision);
|
|
282
|
+
const idempotencyKey = cleanString(options?.idempotencyKey);
|
|
283
|
+
if (!/^v1:[a-f0-9]{64}$/i.test(expectedOfferRevision)) {
|
|
284
|
+
throw new TypeError('Codex reset offer revision is invalid');
|
|
285
|
+
}
|
|
286
|
+
if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(idempotencyKey)) {
|
|
287
|
+
throw new TypeError('Codex reset idempotency key is invalid');
|
|
288
|
+
}
|
|
289
|
+
const auth = await resolveOpenAICodexAuth(providerObj);
|
|
290
|
+
if (!auth) throw new Error('Codex is not signed in');
|
|
291
|
+
const current = await fetchOpenAICodexResetCreditsWithAuth(auth);
|
|
292
|
+
if (!current || current.availableCount < 1 || current.offerRevision !== expectedOfferRevision) {
|
|
293
|
+
return { status: 'offerChanged', resetCredits: current };
|
|
294
|
+
}
|
|
295
|
+
const response = await fetch(CODEX_RESET_CONSUME_URL, {
|
|
296
|
+
...fetchOptions({
|
|
297
|
+
...codexHeaders(auth),
|
|
298
|
+
'Content-Type': 'application/json',
|
|
299
|
+
}, 15_000),
|
|
300
|
+
method: 'POST',
|
|
301
|
+
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
302
|
+
});
|
|
303
|
+
if (!response.ok) throw new Error(`Codex reset failed: HTTP ${response.status}`);
|
|
304
|
+
const outcome = codexResetOutcome((await response.json())?.code);
|
|
305
|
+
const resetCredits = await fetchOpenAICodexResetCreditsWithAuth(auth).catch(() => null);
|
|
306
|
+
return { outcome, resetCredits };
|
|
307
|
+
}
|
|
308
|
+
|
|
202
309
|
function labelForDuration(seconds, fallback) {
|
|
203
310
|
const s = num(seconds, 0);
|
|
204
311
|
if (s > 0) {
|
|
@@ -482,19 +589,18 @@ function latestClaudeStatuslineUsage() {
|
|
|
482
589
|
}
|
|
483
590
|
|
|
484
591
|
async function fetchOpenAICodexUsage(providerObj) {
|
|
485
|
-
const auth = await providerObj
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
Accept: 'application/json',
|
|
494
|
-
}));
|
|
592
|
+
const auth = await resolveOpenAICodexAuth(providerObj);
|
|
593
|
+
if (!auth) return null;
|
|
594
|
+
const [res, resetCredits] = await Promise.all([
|
|
595
|
+
fetch('https://chatgpt.com/backend-api/wham/usage', fetchOptions(
|
|
596
|
+
codexHeaders(auth, 'responses=experimental'),
|
|
597
|
+
)),
|
|
598
|
+
fetchOpenAICodexResetCreditsWithAuth(auth).catch(() => null),
|
|
599
|
+
]);
|
|
495
600
|
if (!res.ok) throw new Error(`openai-oauth usage ${res.status}`);
|
|
496
601
|
const data = await res.json();
|
|
497
|
-
|
|
602
|
+
const usage = normalizeOpenAIWhamUsage(data);
|
|
603
|
+
return usage && resetCredits ? { ...usage, resetCredits } : usage;
|
|
498
604
|
}
|
|
499
605
|
|
|
500
606
|
async function fetchAnthropicUsage(providerObj) {
|
|
@@ -589,15 +695,18 @@ async function fetchGrokUsage(providerObj, routeInfo) {
|
|
|
589
695
|
return null;
|
|
590
696
|
}
|
|
591
697
|
|
|
592
|
-
export async function fetchOAuthUsageSnapshot(routeInfo, providerObj, log = () => {}) {
|
|
698
|
+
export async function fetchOAuthUsageSnapshot(routeInfo, providerObj, log = () => {}, options = {}) {
|
|
593
699
|
const provider = providerKey(routeInfo);
|
|
594
700
|
if (!provider.includes('oauth')) return null;
|
|
595
701
|
const key = routeKey(routeInfo);
|
|
596
702
|
const providerOnly = providerKey(routeInfo);
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
703
|
+
const force = options?.force === true;
|
|
704
|
+
if (!force) {
|
|
705
|
+
const cached = freshSnapshot(memoryCache.get(key), LIVE_CACHE_TTL_MS)
|
|
706
|
+
|| freshSnapshot(memoryCache.get(providerOnly), LIVE_CACHE_TTL_MS);
|
|
707
|
+
if (cached) return cached;
|
|
708
|
+
if (negativeFresh(key) || negativeFresh(providerOnly)) return null;
|
|
709
|
+
}
|
|
601
710
|
if (inflight.has(key)) return inflight.get(key);
|
|
602
711
|
|
|
603
712
|
const task = (async () => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Extracted from openai-oauth-ws.mjs, which now owns transport flow only.
|
|
5
5
|
import { createHash } from 'crypto';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
function _cleanMetaString(value) {
|
|
8
8
|
return typeof value === 'string' ? value.trim() : '';
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -38,7 +38,7 @@ function _codexInstallationId(sendOpts) {
|
|
|
38
38
|
// The identity block codex rebuilds per request (responses_metadata.rs
|
|
39
39
|
// client_metadata()): never cached on the pooled socket, or a later turn would
|
|
40
40
|
// replay the first turn's identity.
|
|
41
|
-
|
|
41
|
+
function _codexMetadataBase(entry, { poolKey, cacheKey, sendOpts, handshake = false } = {}) {
|
|
42
42
|
const sessionId = _cleanMetaString(sendOpts?.codexSessionId || sendOpts?.session?.codexSessionId || poolKey || cacheKey)
|
|
43
43
|
|| 'mixdog-session';
|
|
44
44
|
const threadId = _cleanMetaString(sendOpts?.threadId || sendOpts?.codexThreadId || sendOpts?.session?.threadId || cacheKey || sessionId)
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { isOffloadedToolResultText } from './tool-result-offload.mjs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
+
import { bpeEncodeCount } from './token-bpe.mjs';
|
|
5
|
+
import {
|
|
6
|
+
countTokensNative,
|
|
7
|
+
nativeTokenCounterEnabled,
|
|
8
|
+
prewarmNativeTokenCounter,
|
|
9
|
+
} from './token-native.mjs';
|
|
4
10
|
import {
|
|
5
11
|
isFinalizedProviderRequestTools,
|
|
6
12
|
providerNativeToolPrefixCount,
|
|
@@ -78,48 +84,49 @@ function bpeEncoder() {
|
|
|
78
84
|
const TOKEN_COUNT_CACHE_MIN_CHARS = 512;
|
|
79
85
|
const TOKEN_COUNT_CACHE_MAX_ENTRIES = 1_024;
|
|
80
86
|
const tokenCountCache = new Map();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Encoding fixed-size slices caps the worst-case word at the chunk size, so
|
|
86
|
-
// cost stays linear. Chunk cuts snap back to the nearest whitespace so the
|
|
87
|
-
// next slice starts at a natural ` word` boundary — o200k tokenizes that
|
|
88
|
-
// identically to the unsliced text, keeping estimates EXACT for prose
|
|
89
|
-
// (compact-smoke asserts est === real o200k count). Only a whitespace-free
|
|
90
|
-
// degenerate run falls back to a hard cut (±1 token per boundary, safely
|
|
91
|
-
// inside the estimate's safety multiplier). Hard cuts avoid splitting a
|
|
92
|
-
// surrogate pair so sliced emoji/CJK-ext code points still encode cleanly.
|
|
93
|
-
const BPE_ENCODE_CHUNK_CHARS = 4_096;
|
|
94
|
-
const BPE_CHUNK_BOUNDARY_SCAN = 512;
|
|
95
|
-
function bpeEncodeCount(enc, s) {
|
|
96
|
-
if (s.length <= BPE_ENCODE_CHUNK_CHARS) return enc.encode(s, undefined, []).length;
|
|
97
|
-
let total = 0;
|
|
98
|
-
let i = 0;
|
|
99
|
-
while (i < s.length) {
|
|
100
|
-
let end = Math.min(s.length, i + BPE_ENCODE_CHUNK_CHARS);
|
|
101
|
-
if (end < s.length) {
|
|
102
|
-
// Prefer cutting BEFORE a whitespace run: the next chunk then
|
|
103
|
-
// starts with ` word`, which o200k merges exactly as it would
|
|
104
|
-
// mid-text. Scan a bounded window so degenerate inputs stay O(1).
|
|
105
|
-
let ws = -1;
|
|
106
|
-
const scanFloor = Math.max(i + 1, end - BPE_CHUNK_BOUNDARY_SCAN);
|
|
107
|
-
for (let j = end - 1; j >= scanFloor; j -= 1) {
|
|
108
|
-
const c = s.charCodeAt(j);
|
|
109
|
-
if (c === 0x20 || c === 0x0A || c === 0x0D || c === 0x09) { ws = j; break; }
|
|
110
|
-
}
|
|
111
|
-
if (ws > i) {
|
|
112
|
-
end = ws; // next chunk starts at the whitespace
|
|
113
|
-
} else {
|
|
114
|
-
const last = s.charCodeAt(end - 1);
|
|
115
|
-
if (last >= 0xD800 && last <= 0xDBFF) end += 1;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
total += enc.encode(s.slice(i, end), undefined, []).length;
|
|
119
|
-
i = end;
|
|
87
|
+
|
|
88
|
+
function _tokenCacheSet(key, count) {
|
|
89
|
+
if (tokenCountCache.size >= TOKEN_COUNT_CACHE_MAX_ENTRIES) {
|
|
90
|
+
tokenCountCache.delete(tokenCountCache.keys().next().value);
|
|
120
91
|
}
|
|
121
|
-
|
|
92
|
+
tokenCountCache.set(key, count);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Native offload for large encodes ────────────────────────────────────────
|
|
96
|
+
// estimateTokens is a SYNC api polled from context gauges and compaction
|
|
97
|
+
// pressure checks. A large fresh string (big tool result) costs tens of ms to
|
|
98
|
+
// encode; under many parallel agents those encodes serialize the one shared
|
|
99
|
+
// event loop. Strings at/above BPE_ASYNC_THRESHOLD_CHARS with no cache entry
|
|
100
|
+
// are therefore counted by the mixdog-token native server (SINGLE offload
|
|
101
|
+
// engine — no worker-thread fallback by design): the caller immediately gets
|
|
102
|
+
// the conservative legacy heuristic, the precise count lands in
|
|
103
|
+
// tokenCountCache, and the next poll returns the exact value. Smaller
|
|
104
|
+
// strings (the vast majority) keep exact synchronous counting. With the
|
|
105
|
+
// native server unavailable (MIXDOG_TOKEN_NATIVE=0, missing binary before
|
|
106
|
+
// the first token-v release), large strings encode synchronously — exact,
|
|
107
|
+
// pre-offload semantics.
|
|
108
|
+
const BPE_ASYNC_THRESHOLD_CHARS = 32_768;
|
|
109
|
+
const _nativePendingKeys = new Set();
|
|
110
|
+
|
|
111
|
+
// Offload one large count to the native mixdog-token server. Returns false
|
|
112
|
+
// when the native path cannot be attempted (caller encodes synchronously). A
|
|
113
|
+
// mid-flight failure just drops the pending key: the next poll re-evaluates
|
|
114
|
+
// and encodes synchronously if the server is really gone.
|
|
115
|
+
function _offloadTokenCountNative(key, s) {
|
|
116
|
+
if (!nativeTokenCounterEnabled()) return false;
|
|
117
|
+
_nativePendingKeys.add(key);
|
|
118
|
+
countTokensNative(s).then((count) => {
|
|
119
|
+
if (!_nativePendingKeys.delete(key)) return;
|
|
120
|
+
if (Number.isFinite(count) && count >= 0) _tokenCacheSet(key, count);
|
|
121
|
+
}).catch(() => {
|
|
122
|
+
_nativePendingKeys.delete(key);
|
|
123
|
+
});
|
|
124
|
+
return true;
|
|
122
125
|
}
|
|
126
|
+
|
|
127
|
+
// Returns the exact count, or null when the string was handed to the native
|
|
128
|
+
// server
|
|
129
|
+
// (caller falls back to the legacy heuristic until the precise count lands).
|
|
123
130
|
function bpeTokenCount(enc, s) {
|
|
124
131
|
if (s.length < TOKEN_COUNT_CACHE_MIN_CHARS) return bpeEncodeCount(enc, s);
|
|
125
132
|
const key = `${s.length}:${createHash('sha1').update(s).digest('base64')}`;
|
|
@@ -129,11 +136,12 @@ function bpeTokenCount(enc, s) {
|
|
|
129
136
|
tokenCountCache.set(key, hit);
|
|
130
137
|
return hit;
|
|
131
138
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
if (s.length >= BPE_ASYNC_THRESHOLD_CHARS) {
|
|
140
|
+
if (!_nativePendingKeys.has(key)) _offloadTokenCountNative(key, s);
|
|
141
|
+
if (_nativePendingKeys.has(key)) return null;
|
|
135
142
|
}
|
|
136
|
-
|
|
143
|
+
const count = bpeEncodeCount(enc, s);
|
|
144
|
+
_tokenCacheSet(key, count);
|
|
137
145
|
return count;
|
|
138
146
|
}
|
|
139
147
|
|
|
@@ -203,12 +211,32 @@ export function estimateTokens(text) {
|
|
|
203
211
|
const enc = bpeEncoder();
|
|
204
212
|
if (enc) {
|
|
205
213
|
try {
|
|
206
|
-
|
|
214
|
+
const count = bpeTokenCount(enc, s);
|
|
215
|
+
// null = precise count is being computed in the worker; return the
|
|
216
|
+
// conservative heuristic until it lands in the cache.
|
|
217
|
+
if (count !== null) return Math.ceil(count * TOKEN_ESTIMATE_SAFETY_MULTIPLIER);
|
|
207
218
|
} catch { /* corrupt input — degrade to the heuristic below */ }
|
|
208
219
|
}
|
|
209
220
|
return legacyEstimateTokens(s);
|
|
210
221
|
}
|
|
211
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Boot prewarm: load the o200k encoder (WASM init), run one small encode
|
|
225
|
+
* (JIT), and spawn the token-count worker so the first large transcript
|
|
226
|
+
* estimate of a session neither blocks on WASM init nor waits for the
|
|
227
|
+
* worker spawn. Best-effort; returns true when the encoder is live.
|
|
228
|
+
*/
|
|
229
|
+
export function prewarmTokenEstimator() {
|
|
230
|
+
const enc = bpeEncoder();
|
|
231
|
+
if (enc) {
|
|
232
|
+
try { bpeEncodeCount(enc, 'mixdog tokenizer warmup — 워밍업 텍스트 0123456789'); } catch { /* best-effort */ }
|
|
233
|
+
}
|
|
234
|
+
// Native server boots its encoder (~100-300ms) off the first estimate;
|
|
235
|
+
// with no binary yet, this also kicks the one-shot release fetch.
|
|
236
|
+
try { prewarmNativeTokenCounter(); } catch { /* best-effort */ }
|
|
237
|
+
return !!enc;
|
|
238
|
+
}
|
|
239
|
+
|
|
212
240
|
// Legacy conservative Unicode-aware estimate (fallback only). Iterates by
|
|
213
241
|
// code point, takes the max of the weighted sum and the chars/4 ASCII floor,
|
|
214
242
|
// then applies the safety multiplier.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Eager tool-dispatch controller, extracted from agent-loop.mjs. Owns the
|
|
2
2
|
// per-turn pending promise map, the intra-turn in-flight signature set, and
|
|
3
|
-
// the mutation epoch.
|
|
4
|
-
// provider streams
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// the mutation epoch. FULL-PARALLEL policy: every tool call starts executing
|
|
4
|
+
// the instant the provider streams its tool_use event (or at batch start),
|
|
5
|
+
// shell/MCP/writes included — the model owns ordering by splitting dependent
|
|
6
|
+
// work into separate turns. Only apply_patch (ordered mutation) waits for the
|
|
7
|
+
// serial batch loop.
|
|
7
8
|
import { normalizeToolEnvelope } from './tool-envelope.mjs';
|
|
8
9
|
import { isInvalidToolArgsMarker } from '../providers/openai-compat-stream.mjs';
|
|
9
10
|
import { _intraTurnSig, _isReadTool, _isScopedCacheableTool, _stripMcpPrefix } from './loop/tool-classify.mjs';
|
|
@@ -11,7 +12,7 @@ import { tryReadCached, tryScopedToolCached } from './read-dedup.mjs';
|
|
|
11
12
|
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
12
13
|
import { executeTool } from './loop/tool-exec.mjs';
|
|
13
14
|
import { crossTurnSignature } from './loop/completion-guards.mjs';
|
|
14
|
-
import { getToolKind,
|
|
15
|
+
import { getToolKind, isParallelDispatchable, isToolCallDedupEligible } from './loop/tool-helpers.mjs';
|
|
15
16
|
|
|
16
17
|
export function createEagerDispatcher({
|
|
17
18
|
tools, cwd, sessionId, sessionRef, signal, opts,
|
|
@@ -40,7 +41,7 @@ export function createEagerDispatcher({
|
|
|
40
41
|
const _eagerInFlightSigs = new Map();
|
|
41
42
|
const epoch = { mutation: 0 };
|
|
42
43
|
const startEagerTool = (call) => {
|
|
43
|
-
if (!call?.id || pending.has(call.id) || !
|
|
44
|
+
if (!call?.id || pending.has(call.id) || !isParallelDispatchable(call.name)) return null;
|
|
44
45
|
// Never eager-execute a call whose arguments failed to parse
|
|
45
46
|
// (invalid-args marker). It has no usable arguments; the serial
|
|
46
47
|
// body handles it via the invalid-args feedback path.
|
|
@@ -157,26 +158,22 @@ export function createEagerDispatcher({
|
|
|
157
158
|
const startEagerRun = (calls, startIndex, dupSet) => {
|
|
158
159
|
for (let j = startIndex; j < calls.length; j += 1) {
|
|
159
160
|
const call = calls[j];
|
|
160
|
-
|
|
161
|
+
// Full-parallel: only the ordered mutation (apply_patch) is
|
|
162
|
+
// skipped — it executes in the serial batch body. No barrier:
|
|
163
|
+
// later calls keep starting in parallel past it.
|
|
164
|
+
if (!call?.id || !isParallelDispatchable(call.name)) continue;
|
|
161
165
|
if (dupSet && dupSet.has(call.id)) continue;
|
|
162
|
-
// A null return here is NOT a state barrier
|
|
163
|
-
// already breaks at the first non-eager (mutation/bash/unknown)
|
|
164
|
-
// tool, so every call reached here is read-only. A null means a
|
|
166
|
+
// A null return here is NOT a state barrier. It means a
|
|
165
167
|
// non-barrier stub — intra-turn in-flight dup, repeat-failure /
|
|
166
168
|
// cross-turn dedup, pre-dispatch-deny, invalid-args, or a cache
|
|
167
169
|
// short-circuit. `continue` (not `break`) so a stub in the
|
|
168
|
-
// middle of
|
|
169
|
-
//
|
|
170
|
+
// middle of the run does not stop LATER independent calls from
|
|
171
|
+
// starting early.
|
|
170
172
|
if (!startEagerTool(call) && !pending.has(call.id)) continue;
|
|
171
173
|
}
|
|
172
174
|
};
|
|
173
|
-
let _streamEagerBlocked = false;
|
|
174
175
|
const onToolCall = (call) => {
|
|
175
|
-
if (!
|
|
176
|
-
_streamEagerBlocked = true;
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (_streamEagerBlocked) return;
|
|
176
|
+
if (!isParallelDispatchable(call?.name)) return;
|
|
180
177
|
startEagerTool(call);
|
|
181
178
|
};
|
|
182
179
|
return { pending, epoch, startEagerTool, startEagerRun, onToolCall };
|
|
@@ -42,7 +42,7 @@ export function agentContextOverflowError({ stage, sessionId, sessionRef, model,
|
|
|
42
42
|
// etc.). This is NOT "latest turn cannot fit the context budget" — masking it
|
|
43
43
|
// as AGENT_CONTEXT_OVERFLOW hides the real failure and misroutes downstream
|
|
44
44
|
// overflow handling. Genuine provider send overflow keeps AgentContextOverflowError.
|
|
45
|
-
|
|
45
|
+
class AgentCompactFailedError extends Error {
|
|
46
46
|
constructor({ stage, sessionId, provider, model }, cause) {
|
|
47
47
|
const target = [provider, model].filter(Boolean).join('/') || 'target model';
|
|
48
48
|
const causeMsg = cause && cause.message ? `: ${cause.message}` : '';
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isSkillDisabled,
|
|
13
13
|
} from '../../context/collect.mjs';
|
|
14
14
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
15
|
+
import { _isMutationTool } from './tool-classify.mjs';
|
|
15
16
|
|
|
16
17
|
// Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
|
|
17
18
|
// to execute during SSE parsing so tool work overlaps with the rest of the
|
|
@@ -40,6 +41,18 @@ export function isEagerDispatchable(name, tools) {
|
|
|
40
41
|
return set.has(name);
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// Full-parallel dispatch policy: EVERY tool call in an assistant turn starts
|
|
45
|
+
// executing immediately — ordering across dependent work is the MODEL's job
|
|
46
|
+
// (split into separate turns). The only exception is the ordered mutation
|
|
47
|
+
// (apply_patch), which stays in the serial batch body so multi-patch turns
|
|
48
|
+
// keep deterministic order, the all-or-nothing failure gate, and cache
|
|
49
|
+
// invalidation sequencing. isEagerDispatchable above remains the READ-ONLY
|
|
50
|
+
// classifier used for dedup eligibility, steering, and post-mutation result
|
|
51
|
+
// invalidation.
|
|
52
|
+
export function isParallelDispatchable(name) {
|
|
53
|
+
return typeof name === 'string' && name.length > 0 && !_isMutationTool(name);
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
// Read-only is necessary but not sufficient for result deduplication. Loader
|
|
44
57
|
// calls mutate the active session tool surface and must execute on every
|
|
45
58
|
// explicit invocation so repeats can truthfully report `alreadyActive`.
|