mixdog 0.9.52 → 0.9.53
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 +1 -1
- package/scripts/bench-run.mjs +2 -2
- package/scripts/compact-pressure-test.mjs +104 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/lifecycle-api-test.mjs +65 -4
- package/scripts/max-output-recovery-test.mjs +31 -0
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +80 -22
- package/scripts/provider-contract-test.mjs +257 -0
- package/scripts/provider-toolcall-test.mjs +172 -10
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +24 -198
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +92 -19
- package/src/runtime/shared/process-shutdown.mjs +6 -0
- package/src/runtime/shared/resource-admission.mjs +7 -2
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +36 -9
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/runtime-core.mjs +44 -103
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +15 -89
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +12 -162
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +202 -65
- package/src/tui/engine/session-api-ext.mjs +37 -4
- package/src/tui/index.jsx +1 -0
- package/src/tui/lib/voice-setup.mjs +5 -5
- package/scripts/_devtools-stub.mjs +0 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -288
|
@@ -28,16 +28,6 @@ import { providerInputExcludesCache } from '../../providers/registry.mjs';
|
|
|
28
28
|
// the recall-fasttrack injection cap uses (loop.mjs recallTokenCap). One
|
|
29
29
|
// number governs every "share of model context" budget.
|
|
30
30
|
|
|
31
|
-
function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
|
|
32
|
-
// Types are hard-locked (agent -> semantic, main/user -> recall-fasttrack).
|
|
33
|
-
// Semantic must always be available as a compact path or agent sessions
|
|
34
|
-
// would have none (loop.mjs throws when no type is available). Env/cfg
|
|
35
|
-
// off-switches no longer apply.
|
|
36
|
-
void sessionRef;
|
|
37
|
-
void cfg;
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
31
|
function resolveCompactTypeSetting(sessionRef, cfg = {}) {
|
|
42
32
|
// Agent-owned sessions are ALWAYS semantic. recall-fasttrack rebuilds
|
|
43
33
|
// context from Memory recall, which is scoped to the user's main-session
|
|
@@ -171,7 +161,7 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
171
161
|
? Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent)
|
|
172
162
|
: null,
|
|
173
163
|
autoCompactTokenLimit: explicitAutoCompactTokenLimit,
|
|
174
|
-
semantic: compactTypeIsSemantic(compactType)
|
|
164
|
+
semantic: compactTypeIsSemantic(compactType),
|
|
175
165
|
recallFastTrack: compactTypeIsRecallFastTrack(compactType),
|
|
176
166
|
semanticTimeoutMs: positiveTokenInt(cfg.timeoutMs) || envTokenInt('MIXDOG_AGENT_COMPACT_TIMEOUT_MS') || 30_000,
|
|
177
167
|
tailTurns: positiveTokenInt(cfg.tailTurns) || envTokenInt('MIXDOG_AGENT_COMPACT_TAIL_TURNS') || 2,
|
|
@@ -191,15 +181,15 @@ function compactPressureTokens(messageTokensEst, policy) {
|
|
|
191
181
|
|
|
192
182
|
function providerPressureTokens(sessionRef, usage) {
|
|
193
183
|
if (!usage || typeof usage !== 'object') return 0;
|
|
194
|
-
const input = Math.max(0, Number(usage.inputTokens) || 0);
|
|
195
|
-
const cachedRead = Math.max(0, Number(usage.cachedTokens) || 0);
|
|
196
|
-
const cacheWrite = Math.max(0, Number(usage.cacheWriteTokens) || 0);
|
|
197
|
-
const explicitPrompt = Math.max(0, Number(usage.promptTokens) || 0);
|
|
184
|
+
const input = Math.max(0, Number(usage.mainInputTokens ?? usage.inputTokens) || 0);
|
|
185
|
+
const cachedRead = Math.max(0, Number(usage.mainCachedTokens ?? usage.cachedTokens) || 0);
|
|
186
|
+
const cacheWrite = Math.max(0, Number(usage.mainCacheWriteTokens ?? usage.cacheWriteTokens) || 0);
|
|
187
|
+
const explicitPrompt = Math.max(0, Number(usage.mainPromptTokens ?? usage.promptTokens) || 0);
|
|
198
188
|
const normalizedPrompt = providerInputExcludesCache(sessionRef?.provider)
|
|
199
189
|
? input + cachedRead + cacheWrite
|
|
200
190
|
: input;
|
|
201
191
|
const prompt = Math.max(explicitPrompt, normalizedPrompt);
|
|
202
|
-
const output = Math.max(0, Number(usage.outputTokens) || 0);
|
|
192
|
+
const output = Math.max(0, Number(usage.mainOutputTokens ?? usage.outputTokens) || 0);
|
|
203
193
|
return Math.max(0, Math.round(prompt + output));
|
|
204
194
|
}
|
|
205
195
|
|
|
@@ -213,10 +203,14 @@ export function recordProviderContextBaseline(sessionRef, messages, usage, {
|
|
|
213
203
|
sendTools = sessionRef?.tools,
|
|
214
204
|
} = {}) {
|
|
215
205
|
if (!sessionRef || !Array.isArray(messages)) return false;
|
|
206
|
+
if (usage?.mainUsageAvailable === false) {
|
|
207
|
+
invalidateProviderContextBaseline(sessionRef);
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
216
210
|
const tokens = providerPressureTokens(sessionRef, usage);
|
|
217
211
|
if (!tokens) return false;
|
|
218
212
|
sessionRef.contextPressureBaselineTokens = tokens;
|
|
219
|
-
sessionRef.contextPressureBaselineOutputTokens = Math.max(0, Math.round(Number(usage?.outputTokens) || 0));
|
|
213
|
+
sessionRef.contextPressureBaselineOutputTokens = Math.max(0, Math.round(Number(usage?.mainOutputTokens ?? usage?.outputTokens) || 0));
|
|
220
214
|
sessionRef.contextPressureBaselineMessageCount = messages.length;
|
|
221
215
|
sessionRef.contextPressureBaselinePrefixSignature = contextMessagesSignature(messages);
|
|
222
216
|
sessionRef.contextPressureBaselineProvider = sessionRef.provider || null;
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
_touchRuntime,
|
|
38
38
|
_unlinkParentAbortListener,
|
|
39
39
|
_getRuntimeEntry,
|
|
40
|
+
_evictTerminalSessionRuntime,
|
|
40
41
|
} from './runtime-liveness.mjs';
|
|
41
42
|
import { SessionClosedError } from './session-errors.mjs';
|
|
42
43
|
import { acquireSessionLock } from './session-lock.mjs';
|
|
@@ -378,11 +379,12 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
378
379
|
// tool results/steering.
|
|
379
380
|
if (d?.source === 'provider_send') {
|
|
380
381
|
recordProviderContextBaseline(session, outgoing, {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
382
|
+
mainUsageAvailable: d.contextUsageAvailable,
|
|
383
|
+
inputTokens: d.contextInputTokens ?? d.deltaInput,
|
|
384
|
+
outputTokens: d.contextOutputTokens ?? d.deltaOutput,
|
|
385
|
+
promptTokens: d.contextPromptTokens ?? d.deltaPrompt,
|
|
386
|
+
cachedTokens: d.contextCachedReadTokens ?? d.deltaCachedRead,
|
|
387
|
+
cacheWriteTokens: d.contextCacheWriteTokens ?? d.deltaCacheWrite,
|
|
386
388
|
}, { boundary: 'request', sendTools: d.sendTools });
|
|
387
389
|
}
|
|
388
390
|
try { askOpts?.onUsageDelta?.(d); } catch {}
|
|
@@ -747,6 +749,11 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
747
749
|
// Detach the live session reference; ask is over.
|
|
748
750
|
entry.session = null;
|
|
749
751
|
}
|
|
752
|
+
// Final-stage runtime diagnostics are useful only while the turn is
|
|
753
|
+
// unwinding. Once its controller is detached, retaining the full entry
|
|
754
|
+
// (and any accidental references hanging from it) for the host lifetime
|
|
755
|
+
// turns one-shot agent traffic into an unbounded manager Map.
|
|
756
|
+
_evictTerminalSessionRuntime(sessionId);
|
|
750
757
|
unlock();
|
|
751
758
|
}
|
|
752
759
|
}
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
// from session-close.mjs (one-way dependency, no cycle).
|
|
5
5
|
import { sweepStaleSessions } from '../store.mjs';
|
|
6
6
|
import { sweepOrphanedPendingMessages } from './pending-messages.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
_getRuntimeEntry,
|
|
9
|
+
_clearSessionRuntime,
|
|
10
|
+
_runtimeEntries,
|
|
11
|
+
_sweepTerminalSessionRuntimes,
|
|
12
|
+
} from './runtime-liveness.mjs';
|
|
8
13
|
import { _closeBashSessionLazy } from './runtime-loaders.mjs';
|
|
9
14
|
import { closeSession } from './session-close.mjs';
|
|
10
15
|
import { nonNegativeIntEnv } from './env-utils.mjs';
|
|
@@ -36,10 +41,11 @@ function _previewIds(items, limit = 5) {
|
|
|
36
41
|
return ` (${ids.join(', ')}${more})`;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
function sweepIdleSessions({ includeTombstones = true } = {}) {
|
|
44
|
+
function sweepIdleSessions({ includeTombstones = true, sweepIdle = true } = {}) {
|
|
40
45
|
const startedAt = Date.now();
|
|
41
46
|
try {
|
|
42
47
|
const result = sweepStaleSessions({
|
|
48
|
+
sweepIdle,
|
|
43
49
|
tombstoneMaxAgeMs: includeTombstones ? TOMBSTONE_MAX_AGE_MS : 0,
|
|
44
50
|
isSessionLive: _isSessionLive,
|
|
45
51
|
});
|
|
@@ -71,7 +77,7 @@ function sweepIdleSessions({ includeTombstones = true } = {}) {
|
|
|
71
77
|
}
|
|
72
78
|
if (tombstonesCleaned > 0) {
|
|
73
79
|
for (const d of tombstoneDetails) {
|
|
74
|
-
if (d?.id) _clearSessionRuntime(d.id);
|
|
80
|
+
if (d?.id && !_isSessionLive(d.id)) _clearSessionRuntime(d.id);
|
|
75
81
|
}
|
|
76
82
|
process.stderr.write(`[session-sweep] unlinked ${tombstonesCleaned} tombstone(s)${_previewIds(tombstoneDetails)}\n`);
|
|
77
83
|
}
|
|
@@ -105,9 +111,10 @@ export function sweepTombstones() {
|
|
|
105
111
|
const { tombstonesCleaned = 0, tombstoneDetails = [], tombstoneErrors = [] } = sweepStaleSessions({
|
|
106
112
|
sweepIdle: false,
|
|
107
113
|
tombstoneMaxAgeMs: TOMBSTONE_MAX_AGE_MS,
|
|
114
|
+
isSessionLive: _isSessionLive,
|
|
108
115
|
});
|
|
109
116
|
for (const d of tombstoneDetails) {
|
|
110
|
-
if (d?.id) _clearSessionRuntime(d.id);
|
|
117
|
+
if (d?.id && !_isSessionLive(d.id)) _clearSessionRuntime(d.id);
|
|
111
118
|
}
|
|
112
119
|
if (tombstonesCleaned > 0) {
|
|
113
120
|
process.stderr.write(`[session-sweep] unlinked ${tombstonesCleaned} tombstone(s)${_previewIds(tombstoneDetails)}\n`);
|
|
@@ -133,7 +140,16 @@ function hasActiveRuntimeWork() {
|
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
function _runCleanupCycle() {
|
|
136
|
-
|
|
143
|
+
// Drain every settled runtime entry on each pass, not just the one or two
|
|
144
|
+
// sessions whose on-disk idle TTL happened to expire in this interval.
|
|
145
|
+
_sweepTerminalSessionRuntimes();
|
|
146
|
+
// A busy host may always have some unrelated agent in flight. Tombstones
|
|
147
|
+
// are already closed and can still be batch-reclaimed safely; only the
|
|
148
|
+
// open-session idle sweep remains gated while active work exists.
|
|
149
|
+
if (hasActiveRuntimeWork()) {
|
|
150
|
+
sweepIdleSessions({ includeTombstones: true, sweepIdle: false });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
137
153
|
sweepOrphanedPendingMessages();
|
|
138
154
|
sweepIdleSessions({ includeTombstones: true });
|
|
139
155
|
}
|
|
@@ -1,48 +1,12 @@
|
|
|
1
1
|
// manager/prefetch-bridge.mjs
|
|
2
2
|
// Explicit-prefetch bridge extracted verbatim from manager.mjs. Runs Lead-
|
|
3
|
-
// supplied files[]/callers[]/references[] prefetch
|
|
4
|
-
// with a pass-through permission guard for API compatibility.
|
|
5
|
-
import { createRequire } from 'module';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
3
|
+
// supplied files[]/callers[]/references[] prefetch outside the agent loop.
|
|
7
4
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
8
5
|
import { executeInternalTool } from '../../internal-tools.mjs';
|
|
9
6
|
import { classifyResultKind } from '../result-classification.mjs';
|
|
10
7
|
import { tryPrefetchCached, setPrefetchCached } from '../read-dedup.mjs';
|
|
11
8
|
import { _executeCodeGraphToolLazy } from './runtime-loaders.mjs';
|
|
12
9
|
|
|
13
|
-
// ── Prefetch permission guard ─────────────────────────────────────────────────
|
|
14
|
-
// Runs the shared permission evaluator for tool calls that originate in the
|
|
15
|
-
// prefetch path (outside the agent loop). Permission enforcement is disabled
|
|
16
|
-
// (the evaluator always returns allow), so this is effectively a pass-through
|
|
17
|
-
// kept for API compatibility. Returns an error string if blocked, or null.
|
|
18
|
-
const _permEvalForPrefetch = (() => {
|
|
19
|
-
const _req = createRequire(import.meta.url);
|
|
20
|
-
try {
|
|
21
|
-
const { dirname: _pdir, resolve: _pres } = _req('path');
|
|
22
|
-
const _hooksLib = _pres(_pdir(fileURLToPath(import.meta.url)), '../../../../../hooks/lib/permission-evaluator.cjs');
|
|
23
|
-
return _req(_hooksLib).evaluatePermission;
|
|
24
|
-
} catch { return null; }
|
|
25
|
-
})();
|
|
26
|
-
function _guardedPrefetchTool(toolName, toolArgs, session) {
|
|
27
|
-
if (!_permEvalForPrefetch) return null;
|
|
28
|
-
// When no explicit mode is attached to the session, run the evaluator
|
|
29
|
-
// under 'default'. The evaluator now always allows, so this never blocks.
|
|
30
|
-
const permissionMode = session?.permissionMode || 'default';
|
|
31
|
-
const projectDir = session?.cwd || undefined;
|
|
32
|
-
const userCwd = session?.cwd || undefined;
|
|
33
|
-
const MCP_PFX = 'mcp__plugin_mixdog_mixdog__';
|
|
34
|
-
const fullName = toolName.startsWith(MCP_PFX) || toolName.startsWith('mcp__') ? toolName : `${MCP_PFX}${toolName}`;
|
|
35
|
-
try {
|
|
36
|
-
const { decision, reason } = _permEvalForPrefetch({ toolName: fullName, toolInput: toolArgs || {}, permissionMode, projectDir, userCwd });
|
|
37
|
-
if (decision === 'deny' || decision === 'ask') {
|
|
38
|
-
return `Error: prefetch tool "${toolName}" blocked (decision=${decision}): ${reason}`;
|
|
39
|
-
}
|
|
40
|
-
} catch (e) {
|
|
41
|
-
process.stderr.write(`[prefetch-guard] evaluator error: ${e?.message}\n`);
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
10
|
export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
47
11
|
if (!explicitPrefetch || typeof explicitPrefetch !== 'object') return null;
|
|
48
12
|
if (!isAgentOwner(session)) return null;
|
|
@@ -85,12 +49,6 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
85
49
|
}
|
|
86
50
|
}
|
|
87
51
|
if (files.length > 0) {
|
|
88
|
-
const _pfGuard = _guardedPrefetchTool('read', { path: files }, session);
|
|
89
|
-
if (_pfGuard) {
|
|
90
|
-
process.stderr.write(`[agent-prefetch] files read blocked: ${_pfGuard}\n`);
|
|
91
|
-
failed.push(...files);
|
|
92
|
-
totalEntries.push(...files);
|
|
93
|
-
} else {
|
|
94
52
|
totalEntries.push(...files);
|
|
95
53
|
// R20: per-file prefetch cache (cross-dispatch, process-local).
|
|
96
54
|
// Try each file from cache first; batch misses into one disk read.
|
|
@@ -183,7 +141,6 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
183
141
|
session.prefetchStats.miss += fileMisses.length;
|
|
184
142
|
session.prefetchStats.failed += failed.length;
|
|
185
143
|
}
|
|
186
|
-
}
|
|
187
144
|
}
|
|
188
145
|
// callers[]
|
|
189
146
|
const callers = Array.isArray(explicitPrefetch.callers) ? explicitPrefetch.callers.filter(c => c && typeof c.symbol === 'string') : [];
|
|
@@ -193,11 +150,6 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
193
150
|
if (file) cgArgs.file = file;
|
|
194
151
|
if (session?.cwd) cgArgs.cwd = session.cwd;
|
|
195
152
|
totalEntries.push(symbol);
|
|
196
|
-
const blocked = _guardedPrefetchTool('code_graph', cgArgs, session);
|
|
197
|
-
if (blocked) {
|
|
198
|
-
process.stderr.write(`[agent-prefetch] callers(${symbol}) blocked: ${blocked}\n`);
|
|
199
|
-
return Promise.resolve({ symbol, out: null, blocked: true });
|
|
200
|
-
}
|
|
201
153
|
return _executeCodeGraphToolLazy('code_graph', cgArgs, session?.cwd)
|
|
202
154
|
.then(out => ({ symbol, out }))
|
|
203
155
|
.catch(e => {
|
|
@@ -207,8 +159,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
207
159
|
});
|
|
208
160
|
const callerResults = await Promise.allSettled(callerTasks);
|
|
209
161
|
for (const r of callerResults) {
|
|
210
|
-
const { symbol, out
|
|
211
|
-
if (blocked) { failed.push(symbol); continue; }
|
|
162
|
+
const { symbol, out } = r.status === 'fulfilled' ? r.value : { symbol: '?', out: null };
|
|
212
163
|
if (out && classifyResultKind(String(out)) !== 'error') {
|
|
213
164
|
parts.push(`### prefetch callers ${symbol}\n${out}`);
|
|
214
165
|
} else {
|
|
@@ -224,11 +175,6 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
224
175
|
if (file) cgArgs.file = file;
|
|
225
176
|
if (session?.cwd) cgArgs.cwd = session.cwd;
|
|
226
177
|
totalEntries.push(symbol);
|
|
227
|
-
const blocked = _guardedPrefetchTool('code_graph', cgArgs, session);
|
|
228
|
-
if (blocked) {
|
|
229
|
-
process.stderr.write(`[agent-prefetch] references(${symbol}) blocked: ${blocked}\n`);
|
|
230
|
-
return Promise.resolve({ symbol, out: null, blocked: true });
|
|
231
|
-
}
|
|
232
178
|
return _executeCodeGraphToolLazy('code_graph', cgArgs, session?.cwd)
|
|
233
179
|
.then(out => ({ symbol, out }))
|
|
234
180
|
.catch(e => {
|
|
@@ -238,8 +184,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
238
184
|
});
|
|
239
185
|
const refResults = await Promise.allSettled(refTasks);
|
|
240
186
|
for (const r of refResults) {
|
|
241
|
-
const { symbol, out
|
|
242
|
-
if (blocked) { failed.push(symbol); continue; }
|
|
187
|
+
const { symbol, out } = r.status === 'fulfilled' ? r.value : { symbol: '?', out: null };
|
|
243
188
|
if (out && classifyResultKind(String(out)) !== 'error') {
|
|
244
189
|
parts.push(`### prefetch references ${symbol}\n${out}`);
|
|
245
190
|
} else {
|
|
@@ -34,6 +34,7 @@ const _toolActivityHeartbeats = new Map();
|
|
|
34
34
|
const VALID_STAGES = new Set([
|
|
35
35
|
'connecting', 'requesting', 'streaming', 'tool_running', 'idle', 'error', 'done', 'cancelling',
|
|
36
36
|
]);
|
|
37
|
+
const TERMINAL_STAGES = new Set(['done', 'error']);
|
|
37
38
|
|
|
38
39
|
// Injected deps that would otherwise pull manager.mjs's store/provider surface
|
|
39
40
|
// back into this module (circular). Wired once from manager.mjs at load time.
|
|
@@ -502,6 +503,29 @@ export function _clearSessionRuntime(id) {
|
|
|
502
503
|
}
|
|
503
504
|
}
|
|
504
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Evict a settled terminal runtime entry without ever touching an in-flight
|
|
508
|
+
* controller. askSession calls this after detaching its controller; the
|
|
509
|
+
* periodic cleanup also uses it to drain any backlog left by older paths.
|
|
510
|
+
*/
|
|
511
|
+
export function _evictTerminalSessionRuntime(id) {
|
|
512
|
+
if (!id) return false;
|
|
513
|
+
const entry = _runtimeState.get(id);
|
|
514
|
+
if (!entry) return false;
|
|
515
|
+
if (entry.controller && !entry.controller.signal?.aborted) return false;
|
|
516
|
+
if (entry.closed !== true && !TERMINAL_STAGES.has(entry.stage)) return false;
|
|
517
|
+
_clearSessionRuntime(id);
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function _sweepTerminalSessionRuntimes() {
|
|
522
|
+
let cleaned = 0;
|
|
523
|
+
for (const [id] of _runtimeState) {
|
|
524
|
+
if (_evictTerminalSessionRuntime(id)) cleaned++;
|
|
525
|
+
}
|
|
526
|
+
return cleaned;
|
|
527
|
+
}
|
|
528
|
+
|
|
505
529
|
// Direct-Map accessors for manager.mjs internals (askSession / closeSession /
|
|
506
530
|
// idle-sweep) that mutate entries in place. Kept as thin exports so the Map
|
|
507
531
|
// stays private to this module.
|
|
@@ -258,11 +258,22 @@ export function applyAskTerminalUsageTotals(session, result, options = {}) {
|
|
|
258
258
|
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
|
|
259
259
|
}
|
|
260
260
|
const _lastTurn = result.lastTurnUsage || result.usage || {};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
if (_lastTurn.mainUsageAvailable === false) {
|
|
262
|
+
session.lastInputTokens = null;
|
|
263
|
+
session.lastOutputTokens = null;
|
|
264
|
+
session.lastCachedReadTokens = null;
|
|
265
|
+
session.lastCacheWriteTokens = null;
|
|
266
|
+
session.lastUncachedInputTokens = null;
|
|
267
|
+
session.lastContextTokens = null;
|
|
268
|
+
session.lastContextTokensUpdatedAt = Date.now();
|
|
269
|
+
session.lastContextTokensStaleAfterCompact = true;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const _lastInputTokens = _lastTurn.mainInputTokens ?? _lastTurn.inputTokens ?? 0;
|
|
273
|
+
const _lastCachedReadTokens = _lastTurn.mainCachedTokens ?? _lastTurn.cachedTokens ?? 0;
|
|
274
|
+
const _lastCacheWriteTokens = _lastTurn.mainCacheWriteTokens ?? _lastTurn.cacheWriteTokens ?? 0;
|
|
264
275
|
session.lastInputTokens = _lastInputTokens;
|
|
265
|
-
session.lastOutputTokens = _lastTurn.outputTokens
|
|
276
|
+
session.lastOutputTokens = _lastTurn.mainOutputTokens ?? _lastTurn.outputTokens ?? 0;
|
|
266
277
|
session.lastCachedReadTokens = _lastCachedReadTokens;
|
|
267
278
|
session.lastCacheWriteTokens = _lastCacheWriteTokens;
|
|
268
279
|
session.lastUncachedInputTokens = uncachedInputTokensForProvider(
|
|
@@ -286,7 +297,20 @@ export function applyAskTerminalUsageTotals(session, result, options = {}) {
|
|
|
286
297
|
*/
|
|
287
298
|
export async function persistIterationMetrics(delta) {
|
|
288
299
|
if (!delta || !delta.sessionId) return;
|
|
289
|
-
const {
|
|
300
|
+
const {
|
|
301
|
+
sessionId,
|
|
302
|
+
iterationIndex,
|
|
303
|
+
deltaInput,
|
|
304
|
+
deltaOutput,
|
|
305
|
+
deltaCachedRead,
|
|
306
|
+
deltaCacheWrite,
|
|
307
|
+
contextInputTokens = deltaInput,
|
|
308
|
+
contextOutputTokens = deltaOutput,
|
|
309
|
+
contextCachedReadTokens = deltaCachedRead,
|
|
310
|
+
contextCacheWriteTokens = deltaCacheWrite,
|
|
311
|
+
contextUsageAvailable = true,
|
|
312
|
+
ts,
|
|
313
|
+
} = delta;
|
|
290
314
|
const runtimeEntry = _getRuntimeEntry(sessionId);
|
|
291
315
|
const session = runtimeEntry?.session ?? loadSession(sessionId);
|
|
292
316
|
if (!session || session.closed) return;
|
|
@@ -320,23 +344,40 @@ export async function persistIterationMetrics(delta) {
|
|
|
320
344
|
// Window snapshot updated per iteration so agent type=list reflects the
|
|
321
345
|
// most-recent provider-reported input size even for short dispatches
|
|
322
346
|
// that finish before askSession's terminal save lands.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
347
|
+
if (contextUsageAvailable === false) {
|
|
348
|
+
session.lastInputTokens = null;
|
|
349
|
+
session.lastOutputTokens = null;
|
|
350
|
+
session.lastCachedReadTokens = null;
|
|
351
|
+
session.lastCacheWriteTokens = null;
|
|
352
|
+
session.lastUncachedInputTokens = null;
|
|
353
|
+
session.lastContextTokens = null;
|
|
354
|
+
session.lastContextTokensUpdatedAt = ts || Date.now();
|
|
355
|
+
session.lastContextTokensStaleAfterCompact = true;
|
|
356
|
+
} else {
|
|
357
|
+
const contextUncachedInput = uncachedInputTokensForProvider(
|
|
358
|
+
session.provider,
|
|
359
|
+
contextInputTokens,
|
|
360
|
+
contextCachedReadTokens,
|
|
361
|
+
contextCacheWriteTokens,
|
|
362
|
+
);
|
|
363
|
+
session.lastInputTokens = contextInputTokens || 0;
|
|
364
|
+
session.lastOutputTokens = contextOutputTokens || 0;
|
|
365
|
+
session.lastCachedReadTokens = contextCachedReadTokens || 0;
|
|
366
|
+
session.lastCacheWriteTokens = contextCacheWriteTokens || 0;
|
|
367
|
+
session.lastUncachedInputTokens = contextUncachedInput;
|
|
328
368
|
// Normalized last-call context footprint: how many prompt tokens the
|
|
329
369
|
// model actually saw on the most-recent send, comparable ACROSS
|
|
330
370
|
// providers. Anthropic reports input_tokens EXCLUDING cache (cache_read
|
|
331
371
|
// is a separate field), so the cached portion must be added back to
|
|
332
372
|
// reflect real context size; openai/grok/gemini already fold cached
|
|
333
373
|
// tokens INTO the input count, so input alone is the footprint.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
374
|
+
const _inputExcludesCache = providerInputExcludesCache(session.provider);
|
|
375
|
+
session.lastContextTokens = _inputExcludesCache
|
|
376
|
+
? (contextInputTokens || 0) + (contextCachedReadTokens || 0) + (contextCacheWriteTokens || 0)
|
|
377
|
+
: (contextInputTokens || 0);
|
|
378
|
+
session.lastContextTokensUpdatedAt = ts || Date.now();
|
|
379
|
+
session.lastContextTokensStaleAfterCompact = false;
|
|
380
|
+
}
|
|
340
381
|
}
|
|
341
382
|
session.lastIterationIndex = iterationIndex;
|
|
342
383
|
session.updatedAt = ts || Date.now();
|
|
@@ -10,8 +10,8 @@ import { _saveSessionSync } from './store.mjs';
|
|
|
10
10
|
|
|
11
11
|
parentPort.on('message', ({ session, opts, reqId }) => {
|
|
12
12
|
try {
|
|
13
|
-
_saveSessionSync(session, opts);
|
|
14
|
-
parentPort.postMessage({ ok: true, reqId });
|
|
13
|
+
const saved = _saveSessionSync(session, opts);
|
|
14
|
+
parentPort.postMessage({ ok: true, saved, reqId });
|
|
15
15
|
} catch (err) {
|
|
16
16
|
parentPort.postMessage({ ok: false, error: err?.message ?? String(err), reqId });
|
|
17
17
|
}
|
|
@@ -13,9 +13,15 @@ import { isOutputLimitStopReason } from './loop/termination.mjs';
|
|
|
13
13
|
|
|
14
14
|
function normalizedIncompleteUsage(raw) {
|
|
15
15
|
if (!raw || typeof raw !== 'object') return undefined;
|
|
16
|
-
const
|
|
16
|
+
const directInputTokens = Number(raw.promptTokenCount ?? raw.prompt_token_count ?? raw.input_tokens ?? raw.prompt_tokens ?? 0) || 0;
|
|
17
17
|
const candidateTokens = Number(raw.candidatesTokenCount ?? raw.candidates_token_count ?? 0) || 0;
|
|
18
18
|
const thoughtTokens = Number(raw.thoughtsTokenCount ?? raw.thoughts_token_count ?? 0) || 0;
|
|
19
|
+
const totalTokens = Number(raw.totalTokenCount ?? raw.total_token_count ?? 0) || 0;
|
|
20
|
+
const hasExplicitGeminiPromptTokens = Object.prototype.hasOwnProperty.call(raw, 'promptTokenCount')
|
|
21
|
+
|| Object.prototype.hasOwnProperty.call(raw, 'prompt_token_count');
|
|
22
|
+
const inputTokens = hasExplicitGeminiPromptTokens || directInputTokens > 0
|
|
23
|
+
? directInputTokens
|
|
24
|
+
: Math.max(0, totalTokens - candidateTokens - thoughtTokens);
|
|
19
25
|
const outputFallback = Number(raw.output_tokens ?? raw.completion_tokens ?? 0) || 0;
|
|
20
26
|
const cachedTokens = Number(raw.cachedContentTokenCount ?? raw.cached_content_token_count ?? raw.cached_tokens ?? 0) || 0;
|
|
21
27
|
return {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, unlinkSync } from 'fs';
|
|
2
|
+
import * as fsp from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { getPluginData } from '../../config.mjs';
|
|
5
|
+
|
|
6
|
+
export function getStoreDir() {
|
|
7
|
+
const dir = join(getPluginData(), 'sessions');
|
|
8
|
+
if (!existsSync(dir))
|
|
9
|
+
mkdirSync(dir, { recursive: true });
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
export function sessionPath(id) {
|
|
13
|
+
// Enforce minted session-id shape before using it in a path to prevent
|
|
14
|
+
// `../` traversal. session IDs are generated by createSession as
|
|
15
|
+
// `sess_<timestamp>_<hex>` — reject anything that doesn't match.
|
|
16
|
+
if (!id || typeof id !== 'string' || !/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
17
|
+
throw new Error(`[session-store] invalid session id: ${JSON.stringify(id)}`);
|
|
18
|
+
}
|
|
19
|
+
return join(getStoreDir(), `${id}.json`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Heartbeat publish ─────────────────────────────────────
|
|
23
|
+
// Lightweight per-session timestamp file (`<id>.hb`) consumed by the
|
|
24
|
+
// status aggregator for fresh-session detection. Decoupled from the
|
|
25
|
+
// full session JSON save so it can fire at a tight cadence (≤5s)
|
|
26
|
+
// without serialising the whole payload. The .hb file holds a single
|
|
27
|
+
// ASCII line: `<msTimestamp>\n`. Aggregator scans the same sessions/
|
|
28
|
+
// directory and matches `<id>.hb` to `<id>.json`.
|
|
29
|
+
const _HEARTBEAT_THROTTLE_MS = 5_000;
|
|
30
|
+
const _hbLastAt = new Map();
|
|
31
|
+
|
|
32
|
+
function _heartbeatPath(id) {
|
|
33
|
+
if (!id || typeof id !== 'string' || !/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
34
|
+
throw new Error(`[session-store] invalid session id: ${JSON.stringify(id)}`);
|
|
35
|
+
}
|
|
36
|
+
return join(getStoreDir(), `${id}.hb`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function publishHeartbeat(id, ts) {
|
|
40
|
+
if (!id) return;
|
|
41
|
+
const now = ts || Date.now();
|
|
42
|
+
const last = _hbLastAt.get(id) || 0;
|
|
43
|
+
if (now - last < _HEARTBEAT_THROTTLE_MS) return;
|
|
44
|
+
const target = _heartbeatPath(id);
|
|
45
|
+
_hbLastAt.set(id, now);
|
|
46
|
+
void fsp.writeFile(target, `${now}\n`, 'utf8').catch(() => {});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function deleteHeartbeat(id) {
|
|
50
|
+
try { unlinkSync(_heartbeatPath(id)); } catch { /* ignore */ }
|
|
51
|
+
_hbLastAt.delete(id);
|
|
52
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Module-level map tracking per-session cancellation generations and commit locks. */
|
|
2
|
+
const _writeControls = new Map();
|
|
3
|
+
|
|
4
|
+
export function writeControl(id) {
|
|
5
|
+
let control = _writeControls.get(id);
|
|
6
|
+
if (!control) {
|
|
7
|
+
// [0] cancellation generation, [1] commit lock
|
|
8
|
+
control = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 2));
|
|
9
|
+
_writeControls.set(id, control);
|
|
10
|
+
}
|
|
11
|
+
return control;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function guardedSaveOptions(id, opts) {
|
|
15
|
+
const control = writeControl(id);
|
|
16
|
+
return {
|
|
17
|
+
...(opts || {}),
|
|
18
|
+
_sessionWriteGuard: { buffer: control.buffer, version: Atomics.load(control, 0) },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function cancelSessionWrites(id) {
|
|
23
|
+
Atomics.add(writeControl(id), 0, 1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isCancelledWrite(opts) {
|
|
27
|
+
const guard = opts?._sessionWriteGuard;
|
|
28
|
+
if (!guard?.buffer || !Number.isInteger(guard.version)) return false;
|
|
29
|
+
try {
|
|
30
|
+
return Atomics.load(new Int32Array(guard.buffer), 0) !== guard.version;
|
|
31
|
+
} catch {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function acquireWriteCommit(opts) {
|
|
37
|
+
const guard = opts?._sessionWriteGuard;
|
|
38
|
+
if (!guard?.buffer) return null;
|
|
39
|
+
const control = new Int32Array(guard.buffer);
|
|
40
|
+
while (Atomics.compareExchange(control, 1, 0, 1) !== 0) {
|
|
41
|
+
Atomics.wait(control, 1, 1, 25);
|
|
42
|
+
}
|
|
43
|
+
if (isCancelledWrite(opts)) {
|
|
44
|
+
Atomics.store(control, 1, 0);
|
|
45
|
+
Atomics.notify(control, 1);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return control;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function releaseWriteCommit(control) {
|
|
52
|
+
if (!control) return;
|
|
53
|
+
Atomics.store(control, 1, 0);
|
|
54
|
+
Atomics.notify(control, 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function waitForWriteCommit(id) {
|
|
58
|
+
const control = writeControl(id);
|
|
59
|
+
while (Atomics.load(control, 1) !== 0) {
|
|
60
|
+
Atomics.wait(control, 1, 1, 25);
|
|
61
|
+
}
|
|
62
|
+
}
|