mixdog 0.9.94 → 0.9.95
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/NOTICE.md +72 -0
- package/package.json +16 -6
- package/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +28 -34
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -15
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +12 -32
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +40 -13
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +5 -9
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -6
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +1 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +3 -4
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +35 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +53 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +148 -15
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +109 -13
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -0
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -1
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -2
- package/src/runtime/memory/tool-defs.mjs +6 -6
- package/src/runtime/shared/atomic-file.mjs +53 -0
- package/src/runtime/shared/background-tasks.mjs +6 -3
- package/src/runtime/shared/resource-admission.mjs +40 -1
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +7 -7
- package/src/runtime/shared/turn-snapshot.mjs +385 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +540 -0
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/runtime-core.mjs +46 -8
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +2 -0
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +106 -22
- package/src/session-runtime/workflow.mjs +11 -4
- package/src/standalone/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +570 -0
- package/src/standalone/channel-daemon-transport.mjs +141 -1
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +894 -0
- package/src/standalone/engine-daemon-local-bridge.mjs +20 -0
- package/src/standalone/engine-daemon-protocol.mjs +33 -0
- package/src/standalone/engine-daemon-service.mjs +864 -0
- package/src/standalone/engine-daemon-transport.mjs +603 -0
- package/src/tui/App.jsx +62 -47
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +5 -0
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +4 -4
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/onboarding-steps.mjs +23 -18
- package/src/tui/app/prompt-submit.mjs +13 -2
- package/src/tui/app/route-pickers.mjs +13 -8
- package/src/tui/app/settings-picker.mjs +55 -58
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +19 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- package/src/tui/app/use-prompt-queue-history.mjs +31 -16
- package/src/tui/app/use-transcript-scroll.mjs +30 -8
- package/src/tui/app/use-transcript-window.mjs +15 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +10 -0
- package/src/tui/components/Spinner.jsx +89 -95
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +1534 -9801
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +76 -3
- package/src/tui/engine/session-api-ext.mjs +23 -1
- package/src/tui/engine/session-api.mjs +81 -41
- package/src/tui/engine/session-flow.mjs +43 -4
- package/src/tui/engine/tool-card-results.mjs +6 -0
- package/src/tui/engine/turn.mjs +8 -1
- package/src/tui/engine-local-session.mjs +1108 -0
- package/src/tui/engine.mjs +16 -1065
- package/src/tui/index.jsx +40 -2
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +35 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- package/src/standalone/channel-daemon.mjs +0 -226
|
@@ -35,6 +35,12 @@ let _initChain = Promise.resolve();
|
|
|
35
35
|
let _inFlightPromise = null;
|
|
36
36
|
let _inFlightSig = null;
|
|
37
37
|
let _lastAppliedSig = null;
|
|
38
|
+
// Provider instances are process-global inside the backend daemon. Catalog
|
|
39
|
+
// readers use this revision to share one raw model snapshot while still
|
|
40
|
+
// rebuilding their cheap route/config projection after auth/config changes.
|
|
41
|
+
let _providerCatalogRevision = 0;
|
|
42
|
+
let _startupCatalogRefreshPromise = null;
|
|
43
|
+
let _catalogRefreshPromise = null;
|
|
38
44
|
|
|
39
45
|
// Deterministic structural signature of a provider config. Recursively sorts
|
|
40
46
|
// object keys so signature equality reflects config-value equality regardless
|
|
@@ -141,7 +147,12 @@ export async function initProviders(config, { signal = null } = {}) {
|
|
|
141
147
|
}
|
|
142
148
|
};
|
|
143
149
|
const tracked = next.then(
|
|
144
|
-
(v) => {
|
|
150
|
+
(v) => {
|
|
151
|
+
if (_lastAppliedSig !== sig) _providerCatalogRevision += 1;
|
|
152
|
+
_lastAppliedSig = sig;
|
|
153
|
+
settle();
|
|
154
|
+
return v;
|
|
155
|
+
},
|
|
145
156
|
(err) => { settle(); throw err; },
|
|
146
157
|
);
|
|
147
158
|
_inFlightSig = sig;
|
|
@@ -236,6 +247,7 @@ export function getProvider(name) {
|
|
|
236
247
|
if (!Ctor) return undefined;
|
|
237
248
|
const inst = wrapProviderAdmission(new Ctor({}), name);
|
|
238
249
|
providers.set(name, inst);
|
|
250
|
+
_providerCatalogRevision += 1;
|
|
239
251
|
return inst;
|
|
240
252
|
}
|
|
241
253
|
if (name === 'openai-oauth' && hasOpenAIOAuthCredentials()) {
|
|
@@ -243,6 +255,7 @@ export function getProvider(name) {
|
|
|
243
255
|
if (!Ctor) return undefined;
|
|
244
256
|
const inst = wrapProviderAdmission(new Ctor({}), name);
|
|
245
257
|
providers.set(name, inst);
|
|
258
|
+
_providerCatalogRevision += 1;
|
|
246
259
|
return inst;
|
|
247
260
|
}
|
|
248
261
|
if (name === 'grok-oauth' && hasGrokOAuthCredentials()) {
|
|
@@ -250,6 +263,7 @@ export function getProvider(name) {
|
|
|
250
263
|
if (!Ctor) return undefined;
|
|
251
264
|
const inst = wrapProviderAdmission(new Ctor({}), name);
|
|
252
265
|
providers.set(name, inst);
|
|
266
|
+
_providerCatalogRevision += 1;
|
|
253
267
|
return inst;
|
|
254
268
|
}
|
|
255
269
|
return undefined;
|
|
@@ -287,6 +301,9 @@ export function getAllProviders() {
|
|
|
287
301
|
// stale entries across re-init (initProviders rebuilds the map in place).
|
|
288
302
|
return new Map(providers);
|
|
289
303
|
}
|
|
304
|
+
export function providerCatalogRevision() {
|
|
305
|
+
return _providerCatalogRevision;
|
|
306
|
+
}
|
|
290
307
|
// Narrow synchronous test seam for the lazy-OAuth boundary. It models a
|
|
291
308
|
// constructor whose module is already loaded while guaranteeing every touched
|
|
292
309
|
// registry entry is restored, even when the assertion callback throws.
|
|
@@ -311,6 +328,20 @@ export function _withLoadedProviderCtorForTest(name, Ctor, fn) {
|
|
|
311
328
|
else signatures.delete(name);
|
|
312
329
|
}
|
|
313
330
|
}
|
|
331
|
+
// Companion seam for registry walks that iterate live INSTANCES (what
|
|
332
|
+
// initProviders leaves behind) rather than constructors — the startup catalog
|
|
333
|
+
// refresh is one. Restores the prior entry even when the callback throws.
|
|
334
|
+
export function _withRegisteredProviderForTest(name, instance, fn) {
|
|
335
|
+
const hadProvider = providers.has(name);
|
|
336
|
+
const priorProvider = providers.get(name);
|
|
337
|
+
providers.set(name, instance);
|
|
338
|
+
try {
|
|
339
|
+
return fn();
|
|
340
|
+
} finally {
|
|
341
|
+
if (hadProvider) providers.set(name, priorProvider);
|
|
342
|
+
else providers.delete(name);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
314
345
|
// Background catalog warm-up. Each provider's listModels() either hits its
|
|
315
346
|
// own cached model list (no-op) or fires a single HTTP refresh. Called from
|
|
316
347
|
// agent.init() after providers are registered so the first agent dispatch call
|
|
@@ -336,7 +367,9 @@ function warmupCatalogs() {
|
|
|
336
367
|
}
|
|
337
368
|
}
|
|
338
369
|
|
|
339
|
-
// Force-refresh each provider's /models catalog
|
|
370
|
+
// Force-refresh each provider's /models catalog ONCE per backend-daemon
|
|
371
|
+
// lifetime. Every session runtime joins this process-global promise and then
|
|
372
|
+
// reads the same provider-instance caches. Unlike
|
|
340
373
|
// warmupCatalogs (which calls listModels() and so respects the 24h provider
|
|
341
374
|
// TTL → no-op when the cache is fresh), this bypasses the TTL via
|
|
342
375
|
// _refreshModelCache so a model released since the last refresh is picked up
|
|
@@ -345,6 +378,7 @@ function warmupCatalogs() {
|
|
|
345
378
|
// context metadata stays on its own 24h TTL. Fire-and-forget: never awaited,
|
|
346
379
|
// per-provider failures logged to stderr like warmupCatalogs.
|
|
347
380
|
export function refreshProviderCatalogsOnStartup() {
|
|
381
|
+
if (_startupCatalogRefreshPromise) return _startupCatalogRefreshPromise;
|
|
348
382
|
const pending = [];
|
|
349
383
|
for (const [name, provider] of providers) {
|
|
350
384
|
const refreshFn = typeof provider?._refreshModelCache === 'function'
|
|
@@ -361,7 +395,11 @@ export function refreshProviderCatalogsOnStartup() {
|
|
|
361
395
|
// Returns a completion promise so callers can invalidate stale model
|
|
362
396
|
// caches once the fresh catalogs land. Still fire-and-forget: unawaited
|
|
363
397
|
// callers keep the previous nonblocking startup behavior.
|
|
364
|
-
|
|
398
|
+
_startupCatalogRefreshPromise = Promise.allSettled(pending).then((results) => {
|
|
399
|
+
_providerCatalogRevision += 1;
|
|
400
|
+
return results;
|
|
401
|
+
});
|
|
402
|
+
return _startupCatalogRefreshPromise;
|
|
365
403
|
}
|
|
366
404
|
|
|
367
405
|
// Force-refresh provider catalogs after an operator changes model/provider
|
|
@@ -369,6 +407,7 @@ export function refreshProviderCatalogsOnStartup() {
|
|
|
369
407
|
// the shared LiteLLM metadata cache first so context/pricing metadata follows
|
|
370
408
|
// newly released models without waiting for the next process restart.
|
|
371
409
|
export function refreshCatalogs() {
|
|
410
|
+
if (_catalogRefreshPromise) return _catalogRefreshPromise;
|
|
372
411
|
const pending = [];
|
|
373
412
|
const metadataReady = Promise.resolve()
|
|
374
413
|
.then(() => refreshMetadataCatalog())
|
|
@@ -391,5 +430,13 @@ export function refreshCatalogs() {
|
|
|
391
430
|
}));
|
|
392
431
|
}
|
|
393
432
|
// Completion promise: lets callers drop stale model caches after refresh.
|
|
394
|
-
|
|
433
|
+
_catalogRefreshPromise = Promise.allSettled([metadataReady, ...pending])
|
|
434
|
+
.then((results) => {
|
|
435
|
+
_providerCatalogRevision += 1;
|
|
436
|
+
return results;
|
|
437
|
+
})
|
|
438
|
+
.finally(() => {
|
|
439
|
+
_catalogRefreshPromise = null;
|
|
440
|
+
});
|
|
441
|
+
return _catalogRefreshPromise;
|
|
395
442
|
}
|
|
@@ -252,7 +252,7 @@ function isRetryable(err) {
|
|
|
252
252
|
return classifyError(err) === 'transient'
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
/**
|
|
255
|
+
/** Anthropic request budget: 10 retries (11 attempts).
|
|
256
256
|
* CLAUDE_CODE_MAX_RETRIES is intentionally read per request for reload/tests.
|
|
257
257
|
* The upper bound prevents an accidental unbounded retry loop. */
|
|
258
258
|
export function anthropicMaxAttempts() {
|
|
@@ -262,16 +262,16 @@ export function anthropicMaxAttempts() {
|
|
|
262
262
|
return retries + 1
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
//
|
|
265
|
+
// Anthropic retry defaults: 500ms exponential backoff,
|
|
266
266
|
// capped at 32s, with positive-only jitter up to 25% of the base delay.
|
|
267
|
-
// The leading duplicate accounts for
|
|
267
|
+
// The leading duplicate accounts for the sleep-before-attempt index:
|
|
268
268
|
// retry attempt 2 reads index 1.
|
|
269
269
|
export const ANTHROPIC_RETRY_BACKOFF_MS = Object.freeze([
|
|
270
270
|
500, 500, 1000, 2000, 4000, 8000, 16000, 32000, 32000, 32000, 32000,
|
|
271
271
|
])
|
|
272
272
|
export const ANTHROPIC_RETRY_JITTER_RATIO = 0.25
|
|
273
273
|
|
|
274
|
-
//
|
|
274
|
+
// The Anthropic SDK client defaults API_TIMEOUT_MS to ten minutes.
|
|
275
275
|
// Read per request, like CLAUDE_CODE_MAX_RETRIES, so env reload/tests work.
|
|
276
276
|
export function anthropicRequestTimeoutMs() {
|
|
277
277
|
const parsed = Number.parseInt(process.env.API_TIMEOUT_MS || '', 10)
|
|
@@ -318,10 +318,10 @@ export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 's
|
|
|
318
318
|
// Mid-stream 'stream_stalled' recoveries retry in place, which is right for a
|
|
319
319
|
// one-off blip but lets a chronically dying stream burn a whole task budget
|
|
320
320
|
// slowly (observed live: one send stretched 149s→298s→556s across stall
|
|
321
|
-
// retries before the agent deadline killed the task).
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
321
|
+
// retries before the agent deadline killed the task). A stalling stream is
|
|
322
|
+
// bounded instead of retried forever: a request is capped at ~300s wall
|
|
323
|
+
// clock (API_TIMEOUT_MS) and a stream dies after one 300s silent gap
|
|
324
|
+
// (stream idle timeout). This guard is the equivalent for our in-place
|
|
325
325
|
// recovery: the clock starts at the FIRST stall of a send, and stall-classified
|
|
326
326
|
// retries are allowed only inside that window; past it the stall error
|
|
327
327
|
// surfaces so loop-level transport retry issues a FRESH request. Healthy
|
|
@@ -357,7 +357,7 @@ export function createStallRetryBudget(budgetMs = STREAM_STALL_RETRY_BUDGET_MS,
|
|
|
357
357
|
// branched on a hardcoded provider name.
|
|
358
358
|
|
|
359
359
|
// F) Retry-budget profiles as DATA. The numbers live ONLY here now.
|
|
360
|
-
// ws.*Retries (5) — one
|
|
360
|
+
// ws.*Retries (5) — one Responses stream retry budget.
|
|
361
361
|
// sse.defaultRetries (3) — anthropic single-shot SSE mid-stream budget.
|
|
362
362
|
export const MIDSTREAM_RETRY_POLICY = {
|
|
363
363
|
ws: { transientCloseRetries: 5, defaultRetries: 5, backoff: [250, 1000, 2000, 4000, 5000] },
|
|
@@ -490,7 +490,7 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
// Explicit `response.failed` error codes/types that describe a transport-level
|
|
493
|
-
// interruption (
|
|
493
|
+
// interruption (these are retryable); every other code is terminal.
|
|
494
494
|
const RESPONSE_FAILED_CODE_CLASSIFIERS = new Map([
|
|
495
495
|
['stream_disconnected', 'response_failed_disconnected'],
|
|
496
496
|
['network_error', 'response_failed_network'],
|
|
@@ -550,7 +550,7 @@ export function shouldFallbackTransport(err, { signal, enabled = true } = {}) {
|
|
|
550
550
|
if (signal?.aborted) return false
|
|
551
551
|
// Transport fallback re-issues the request on another transport: it is a
|
|
552
552
|
// replay, so the exposure deny applies. Eligibility itself stays typed
|
|
553
|
-
// (status / errno / classifier)
|
|
553
|
+
// (status / errno / classifier) for the WS→HTTPS switch.
|
|
554
554
|
if (readStreamOutcome(err).replaySafe !== true) return false
|
|
555
555
|
const status = Number(err?.httpStatus || err?.status || 0)
|
|
556
556
|
// 401 is auth recovery, never transport fallback. 426 is the explicit
|
|
@@ -660,7 +660,7 @@ function _sleepChunkWithAbort(ms, signal, sleepFn, abortMessage) {
|
|
|
660
660
|
// E) Handshake classifier (moved here from openai-oauth-ws). Default-deny:
|
|
661
661
|
// anything not recognized as transient returns null. HTTP 401 is reserved
|
|
662
662
|
// for auth recovery and 426 for immediate HTTPS fallback. The OpenAI OAuth
|
|
663
|
-
// caller opts out of 429 retries (
|
|
663
|
+
// caller opts out of 429 retries (retry429:false); all other callers
|
|
664
664
|
// retain the historical retryable UnexpectedStatus policy.
|
|
665
665
|
export function classifyHandshakeError(err, { retry429 = true } = {}) {
|
|
666
666
|
if (!err) return null
|
|
@@ -764,8 +764,8 @@ export async function withRetry(fn, opts = {}) {
|
|
|
764
764
|
// an eligible failure is actually retried remains the typed question
|
|
765
765
|
// resolved by classifyError()/status below.
|
|
766
766
|
if (readStreamOutcome(caught).replaySafe !== true) throw caught
|
|
767
|
-
//
|
|
768
|
-
//
|
|
767
|
+
// x-should-retry:false is an explicit server veto on retrying and is
|
|
768
|
+
// honored as-is.
|
|
769
769
|
// Keep this ahead of status defaults, including the request-local 429 path.
|
|
770
770
|
const shouldRetryHeader = _headerValue(
|
|
771
771
|
caught?.headers || caught?.response?.headers || caught?.data?.responseHeaders,
|
|
@@ -786,7 +786,7 @@ export async function withRetry(fn, opts = {}) {
|
|
|
786
786
|
}
|
|
787
787
|
continue
|
|
788
788
|
}
|
|
789
|
-
//
|
|
789
|
+
// The optional model fallback fires on the third 529. This
|
|
790
790
|
// remains opt-in: providers pass fallbackModel only when the caller set
|
|
791
791
|
// one. The hard progress veto above must run first so fallback can never
|
|
792
792
|
// replay partial thinking/tool output.
|
|
@@ -91,7 +91,6 @@ import {
|
|
|
91
91
|
isOutputLimitStopReason,
|
|
92
92
|
providerContinuationSignal,
|
|
93
93
|
} from './loop/termination.mjs';
|
|
94
|
-
import { createSteeringLadder } from './loop/steering-ladder.mjs';
|
|
95
94
|
import { runPreSendCompactPass } from './pre-send-compact.mjs';
|
|
96
95
|
import { createEagerDispatcher } from './eager-dispatch.mjs';
|
|
97
96
|
import { sendWithRecovery } from './send-with-recovery.mjs';
|
|
@@ -238,7 +237,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
238
237
|
if (compactSettledToolCallBodies(messages) && !opts.cacheBreakIntent) {
|
|
239
238
|
opts.cacheBreakIntent = 'deferred_body_compaction';
|
|
240
239
|
}
|
|
241
|
-
// ----
|
|
240
|
+
// ---- Turn stop hook ----------------------------------------------------
|
|
242
241
|
// A no-tool assistant message is TERMINAL. Only a structured provider
|
|
243
242
|
// follow-up signal (end_turn=false / pause_turn), pending input, tool
|
|
244
243
|
// calls/results, or a stop hook that blocks with a continuation prompt keep
|
|
@@ -280,8 +279,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
280
279
|
}
|
|
281
280
|
// Tag steering-origin user messages so provider lowering keeps them
|
|
282
281
|
// distinct from preceding tool results. Keep each queued command as
|
|
283
|
-
// its own user turn
|
|
284
|
-
//
|
|
282
|
+
// its own user turn instead of collapsing priority/mode buckets
|
|
283
|
+
// together.
|
|
285
284
|
messages.push({
|
|
286
285
|
role: 'user',
|
|
287
286
|
content: merged.content,
|
|
@@ -366,9 +365,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
366
365
|
};
|
|
367
366
|
const maxLoopIterations = resolveSessionMaxLoopIterations(sessionRef);
|
|
368
367
|
// ---- Completion-first loop guards (worker runaway prevention) ----
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
368
|
+
// Behavior-steering hints (missed-parallelism, all-read-only, read-only
|
|
369
|
+
// shell, level-2 "stop exploring") were removed: they nudged tool shape
|
|
370
|
+
// instead of protecting resources. Only the staged iteration warnings, the
|
|
371
|
+
// hard cap, and the cross-turn dedup stub remain.
|
|
372
372
|
// _editCount counts any executed tool call whose def lacks readOnlyHint
|
|
373
373
|
// (i.e. edit/progress: apply_patch, bash, MCP writes, skills, ...).
|
|
374
374
|
let _editCount = 0;
|
|
@@ -406,7 +406,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
406
406
|
// Loop-level transport replays consumed this ask (see send-with-recovery
|
|
407
407
|
// TRANSPORT_RETRY_MAX): bounded per turn, reset only with a fresh ask.
|
|
408
408
|
let _transportRetriesUsed = 0;
|
|
409
|
-
//
|
|
409
|
+
// Queued prompt/task notifications are attached after a
|
|
410
410
|
// tool batch, before the continuation provider send. Normal batches drain
|
|
411
411
|
// up to 'next'; a Sleep-like tool grants a 'later' flush.
|
|
412
412
|
let _toolBatchJustCompleted = false;
|
|
@@ -415,20 +415,6 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
415
415
|
const name = String(call?.name || call?.toolName || call?.function?.name || '').toLowerCase();
|
|
416
416
|
return name === 'sleep' || name.endsWith('/sleep') || name.endsWith('.sleep');
|
|
417
417
|
};
|
|
418
|
-
// Completion-first steering ladder controller. Owns the (cumulative) level-1
|
|
419
|
-
// fire count, the all-read-only / serial-single / same-file-grep streaks,
|
|
420
|
-
// and the level-2 latch. Threaded via live getters so it reads the loop's
|
|
421
|
-
// current `iterations` / `_editCount` on every call (no stale snapshots).
|
|
422
|
-
const _steeringLadder = createSteeringLadder({
|
|
423
|
-
sessionId,
|
|
424
|
-
sessionAgent,
|
|
425
|
-
tools,
|
|
426
|
-
getIterations: () => iterations,
|
|
427
|
-
getEditCount: () => _editCount,
|
|
428
|
-
readOnlyRole: String(sessionRef?.permission || sessionRef?.toolPermission || '') === 'read',
|
|
429
|
-
pushUserMessage: (msg) => messages.push(msg),
|
|
430
|
-
pushSystemReminder: (text) => messages.push({ role: 'user', content: `<system-reminder>\n${text}\n</system-reminder>`, meta: 'hook' }),
|
|
431
|
-
});
|
|
432
418
|
// Tool execution must use the session cwd even when the caller omitted the
|
|
433
419
|
// legacy positional cwd argument. Agent workers always carry their cwd on
|
|
434
420
|
// sessionRef; falling through to pwd()/process.cwd() resolves relatives
|
|
@@ -503,8 +489,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
503
489
|
} catch { /* best-effort */ }
|
|
504
490
|
}
|
|
505
491
|
// Drain queued steering/prompts BEFORE the pre-send compact check, but
|
|
506
|
-
// only immediately after a tool batch has completed
|
|
507
|
-
// Claude Code's query.ts queued_command attachment drain: queued entries
|
|
492
|
+
// only immediately after a tool batch has completed: queued entries
|
|
508
493
|
// are attached after tool results are appended and before the recursive
|
|
509
494
|
// continuation, not on arbitrary non-tool continuations (empty nudges,
|
|
510
495
|
// iteration-cap final text turns, etc.).
|
|
@@ -792,11 +777,6 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
792
777
|
// tool-call-blocked vs contract-required oscillation.
|
|
793
778
|
if (!response.toolCalls?.length) {
|
|
794
779
|
// No tool calls. Decide between final-answer accept vs nudge.
|
|
795
|
-
// Reviewer fix: a zero-tool turn (final-pre-send steering drain or
|
|
796
|
-
// contract nudge `continue`) must not bridge the all-read-only
|
|
797
|
-
// streak across non-tool turns — that would fire level-2 early on
|
|
798
|
-
// a worker that paused to synthesize text mid-run.
|
|
799
|
-
_steeringLadder.resetAllReadOnlyStreak();
|
|
800
780
|
// - has content + non-hidden role → valid final, break.
|
|
801
781
|
// - empty content + hidden role → contract allows text-only
|
|
802
782
|
// terminal turn, break.
|
|
@@ -915,7 +895,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
915
895
|
messages.push({ role: 'user', content: nudgeMsg });
|
|
916
896
|
continue;
|
|
917
897
|
}
|
|
918
|
-
//
|
|
898
|
+
// Pending-input rule: queued user input is
|
|
919
899
|
// folded into needs_follow_up and evaluated BEFORE the stop hooks,
|
|
920
900
|
// so real steering always wins over a synthetic continuation prompt.
|
|
921
901
|
// Commit the terminal text first (beforeAppend), then resume.
|
|
@@ -926,7 +906,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
926
906
|
_emptyNudgeStreak = 0;
|
|
927
907
|
continue;
|
|
928
908
|
}
|
|
929
|
-
//
|
|
909
|
+
// This no-tool message ends the turn
|
|
930
910
|
// unless a stop hook blocks it. The unresolved-tool-failure hook may
|
|
931
911
|
// block exactly once — commit the assistant text, record the
|
|
932
912
|
// structural continuation prompt, resume sampling. Skipped on the
|
|
@@ -1064,7 +1044,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1064
1044
|
pending: eager.pending, epoch: eager.epoch, startEagerRun: eager.startEagerRun,
|
|
1065
1045
|
crossTurnCalls: _crossTurnCalls, crossTurnCap: _CROSS_TURN_CAP,
|
|
1066
1046
|
dedupStubTotal: _dedupStubTotal, editCount: _editCount,
|
|
1067
|
-
sessionAgent,
|
|
1047
|
+
sessionAgent,
|
|
1068
1048
|
pushToolResultMessage, throwIfAborted,
|
|
1069
1049
|
repeatFailLimit: REPEAT_FAIL_LIMIT,
|
|
1070
1050
|
}));
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { _normalizeAbs, _statTuple, _statEqual } from './util.mjs';
|
|
4
4
|
import { clearScopedToolsForSession, clearScopedCounters } from './scoped-cache.mjs';
|
|
5
5
|
import { clearPostEditMarks } from './post-edit-marks.mjs';
|
|
6
|
+
import { registerSessionPurgeHook } from '../store.mjs';
|
|
7
|
+
import { releaseReadSnapshotScope } from '../../tools/builtin/snapshot-store.mjs';
|
|
6
8
|
|
|
7
9
|
const MAX_PER_SESSION = 100;
|
|
8
10
|
|
|
@@ -260,6 +262,11 @@ export function clearReadDedupSession(sessionId) {
|
|
|
260
262
|
clearScopedCounters(sessionId);
|
|
261
263
|
}
|
|
262
264
|
|
|
265
|
+
registerSessionPurgeHook((sessionId) => {
|
|
266
|
+
clearReadDedupSession(sessionId);
|
|
267
|
+
releaseReadSnapshotScope(sessionId, { deletePersisted: true, persist: false });
|
|
268
|
+
});
|
|
269
|
+
|
|
263
270
|
/**
|
|
264
271
|
* Extract the set of touched filesystem paths from a unified-diff patch text.
|
|
265
272
|
* Handles git-style `--- a/<path>` / `+++ b/<path>` headers and `/dev/null` markers.
|
|
@@ -2,7 +2,9 @@ import { isAgentOwner } from '../agent-owner.mjs';
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_COMPACTION_BUFFER_TOKENS = 0;
|
|
4
4
|
export const DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
|
|
5
|
-
|
|
5
|
+
// Main/user sessions default to full-window trigger (buffer 0): RFT compact
|
|
6
|
+
// runs at the boundary. Agent sessions keep DEFAULT_COMPACTION_BUFFER_RATIO.
|
|
7
|
+
export const DEFAULT_MAIN_COMPACTION_BUFFER_RATIO = 0;
|
|
6
8
|
export const MAX_COMPACTION_BUFFER_RATIO = 0.25;
|
|
7
9
|
const MAX_BUFFER_INPUT_RATIO = 0.999_999;
|
|
8
10
|
export const DEFAULT_COMPACTION_KEEP_TOKENS = 8_000;
|
|
@@ -16,9 +18,14 @@ function envTokenInt(name) {
|
|
|
16
18
|
return positiveTokenInt(process.env[name]);
|
|
17
19
|
}
|
|
18
20
|
export function normalizeCompactionBufferRatio(value, fallback = DEFAULT_COMPACTION_BUFFER_RATIO) {
|
|
21
|
+
// Explicit 0 is a valid buffer (main full-window trigger). Only nullish /
|
|
22
|
+
// empty / non-finite / negative values fall back. Do not treat Number(null)
|
|
23
|
+
// as zero here — null must mean "unset".
|
|
24
|
+
if (value == null || value === '') return fallback;
|
|
19
25
|
const n = Number(value);
|
|
20
|
-
if (Number.isFinite(n)
|
|
21
|
-
return
|
|
26
|
+
if (!Number.isFinite(n) || n < 0) return fallback;
|
|
27
|
+
if (n === 0) return 0;
|
|
28
|
+
return n > 1 ? n / 100 : n;
|
|
22
29
|
}
|
|
23
30
|
export function resolveBufferRatioCandidate(percentInputs = [], ratioInputs = []) {
|
|
24
31
|
for (const raw of percentInputs) {
|
|
@@ -160,11 +160,10 @@ export function providerTokenCalibration(provider) {
|
|
|
160
160
|
if (p.startsWith('gemini') || p.startsWith('google')) return calibrationEnv('MIXDOG_TOKEN_CALIBRATION_GEMINI') ?? 1.15;
|
|
161
161
|
return calibrationEnv('MIXDOG_TOKEN_CALIBRATION_DEFAULT') ?? 1.0;
|
|
162
162
|
}
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// the 2000x2000 resize ceiling (5333 tokens).
|
|
163
|
+
// Images/documents count a flat 2000 tokens when dimensions are unknown —
|
|
164
|
+
// the conservative constant also used by the micro-compaction path. Known
|
|
165
|
+
// dimensions may only RAISE the allowance via Anthropic's real vision
|
|
166
|
+
// formula (w*h/750), capped at the 2000x2000 resize ceiling (5333 tokens).
|
|
168
167
|
export const IMAGE_VISUAL_TOKEN_ALLOWANCE = 2_000;
|
|
169
168
|
const IMAGE_MAX_TOKEN_ALLOWANCE = 5_333;
|
|
170
169
|
|
|
@@ -249,8 +248,8 @@ function legacyEstimateTokens(s) {
|
|
|
249
248
|
// for those runs without penalizing ordinary spaced prose.
|
|
250
249
|
let denseAsciiFloor = 0;
|
|
251
250
|
for (const match of s.matchAll(/[\x21-\x7e]{16,}/g)) {
|
|
252
|
-
//
|
|
253
|
-
//
|
|
251
|
+
// Dense JSON/JSONL prices at chars/2; 0.5/char keeps that ratio for
|
|
252
|
+
// long unmerged printable runs.
|
|
254
253
|
denseAsciiFloor += match[0].length * 0.5;
|
|
255
254
|
}
|
|
256
255
|
const encodedWords = s.match(/\b(?=[A-Za-z0-9]{8,}\b)(?=[A-Za-z0-9]*[A-Za-z])(?=[A-Za-z0-9]*\d)[A-Za-z0-9]+\b/g) || [];
|
|
@@ -272,7 +271,7 @@ function legacyEstimateTokens(s) {
|
|
|
272
271
|
&& (jsonLikeLines >= Math.ceil(lines.length / 2) || structural / nonWhitespace >= 0.12)) {
|
|
273
272
|
// JSONL, compact tables and generated line protocols can consist
|
|
274
273
|
// entirely of short runs while still tokenizing like minified data.
|
|
275
|
-
// chars/2 on the dense payload chars (
|
|
274
|
+
// chars/2 on the dense payload chars (dense-JSON pricing).
|
|
276
275
|
denseAsciiFloor = Math.max(denseAsciiFloor, (nonWhitespace * 0.5) + ((s.length - nonWhitespace) * 0.25));
|
|
277
276
|
}
|
|
278
277
|
const asciiFloor = s.length / 4; // never below the legacy chars/4 lower bound
|
|
@@ -355,8 +354,8 @@ function messageEstimateText(m) {
|
|
|
355
354
|
function imageDescriptorAllowance(descriptor) {
|
|
356
355
|
if (descriptor.width && descriptor.height) {
|
|
357
356
|
// Anthropic vision cost: tokens = (width * height) / 750, with images
|
|
358
|
-
// resized down to at most 2000x2000 (5333 tokens)
|
|
359
|
-
//
|
|
357
|
+
// resized down to at most 2000x2000 (5333 tokens). Caller-supplied
|
|
358
|
+
// dimensions may RAISE the
|
|
360
359
|
// allowance above the unknown-image floor but never lower it (the
|
|
361
360
|
// provider normalizer may not preserve caller metadata).
|
|
362
361
|
const formula = Math.ceil((descriptor.width * descriptor.height) / 750);
|
|
@@ -365,7 +364,7 @@ function imageDescriptorAllowance(descriptor) {
|
|
|
365
364
|
Math.max(IMAGE_VISUAL_TOKEN_ALLOWANCE, formula),
|
|
366
365
|
);
|
|
367
366
|
}
|
|
368
|
-
// Unknown-size images: flat conservative allowance
|
|
367
|
+
// Unknown-size images: flat conservative allowance.
|
|
369
368
|
return IMAGE_VISUAL_TOKEN_ALLOWANCE;
|
|
370
369
|
}
|
|
371
370
|
function messageImageDescriptors(m) {
|
|
@@ -100,6 +100,7 @@ export function createEagerDispatcher({
|
|
|
100
100
|
if (_dedupEligible) _eagerInFlightSigs.set(_sig, call.id);
|
|
101
101
|
entry.promise = (async () => {
|
|
102
102
|
try {
|
|
103
|
+
await opts.beforeToolExecution?.();
|
|
103
104
|
return { ok: true, value: await executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: getNextIteration(), deferShellCwdCommit: true }) };
|
|
104
105
|
} catch (error) {
|
|
105
106
|
return { ok: false, error };
|
|
@@ -112,8 +112,8 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
112
112
|
if (!boundaryTokens) return null;
|
|
113
113
|
const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
|
|
114
114
|
// Shared session-compaction policy (context-utils): agent semantic keeps the
|
|
115
|
-
// default early-trigger buffer (90%); main/user
|
|
116
|
-
//
|
|
115
|
+
// default early-trigger buffer (90%); main/user default to full-window
|
|
116
|
+
// trigger (buffer 0 / 100%), still overridable via mainBuffer*;
|
|
117
117
|
// a truly-explicit sub-boundary limit wins. explicitAutoCompactTokenLimit
|
|
118
118
|
// is the sanitized (null when legacy full-window) value so telemetry never
|
|
119
119
|
// re-persists a boundary-collapsing limit.
|
|
@@ -216,7 +216,7 @@ function providerPressureTokens(sessionRef, usage) {
|
|
|
216
216
|
/**
|
|
217
217
|
* Align an authoritative provider usage snapshot to the message prefix it
|
|
218
218
|
* covers. Later pressure checks add estimates only for messages after this
|
|
219
|
-
* baseline
|
|
219
|
+
* baseline: actual-usage-plus-growth accounting.
|
|
220
220
|
*/
|
|
221
221
|
export function recordProviderContextBaseline(sessionRef, messages, usage, {
|
|
222
222
|
boundary = 'complete',
|
|
@@ -38,20 +38,6 @@ export function isEditProgressTool(name, isEager) {
|
|
|
38
38
|
return !NON_PROGRESS_TOOLS.has(bare);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// Step 1 — level-2 escalation steering. N = cumulative level-1 fires.
|
|
42
|
-
// `readOnlyRole` swaps the edit-oriented directive for a report-oriented one:
|
|
43
|
-
// read-permission sessions (reviewer-style) cannot apply_patch, so telling
|
|
44
|
-
// them to edit is self-contradictory and pushes premature termination.
|
|
45
|
-
const LEVEL2_DIMINISHING_RETURNS = ' Further reads add cost, not evidence.';
|
|
46
|
-
|
|
47
|
-
export function level2SteerMessage(n, readOnlyRole = false, level2Fires = 1) {
|
|
48
|
-
const tail = Number(level2Fires) >= 2 ? LEVEL2_DIMINISHING_RETURNS : '';
|
|
49
|
-
if (readOnlyRole) {
|
|
50
|
-
return `<system-reminder>\nEnough evidence gathered — report findings now; name any gaps.${tail}\n</system-reminder>`;
|
|
51
|
-
}
|
|
52
|
-
return `<system-reminder>\nStop exploring — apply the edit you know, or report blocked with what's missing.${tail}\n</system-reminder>`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
41
|
// Step 2 — cross-turn dedup stub. `stuck` appends the escalation tail at the
|
|
56
42
|
// 5th+ dedup stub in the session.
|
|
57
43
|
export function crossTurnDedupStub(name, firstIteration, stuck) {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Turn stop hook.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// Before breaking, the turn runs its stop hooks; a hook may block ONCE
|
|
5
|
-
// returning continuation fragments, which are recorded as a prompt message
|
|
6
|
-
// before sampling resumes (
|
|
7
|
-
//
|
|
8
|
-
// completion strikes, no lexical inspection of
|
|
3
|
+
// Termination semantics: a sampling request that needs no follow-up ends the
|
|
4
|
+
// turn. Before breaking, the turn runs its stop hooks; a hook may block ONCE
|
|
5
|
+
// by returning continuation fragments, which are recorded as a prompt message
|
|
6
|
+
// before sampling resumes (the hook stays armed for the rest of the turn and
|
|
7
|
+
// is never cleared inside it). Nothing else can force another turn: no
|
|
8
|
+
// mandatory terminal tool, no completion strikes, no lexical inspection of
|
|
9
|
+
// the assistant text.
|
|
9
10
|
//
|
|
10
11
|
// The single hook implemented here preserves the original failed-apply_patch
|
|
11
12
|
// protection: while a real tool failure is unresolved, the first terminal
|
|
@@ -41,7 +42,7 @@ export function toolFailureContinuationPrompt(failedTool) {
|
|
|
41
42
|
* success clears it.
|
|
42
43
|
* - takeContinuationPrompt(): returns the continuation prompt the FIRST time
|
|
43
44
|
* a terminal assistant message arrives while a failure is unresolved, then
|
|
44
|
-
* stays silent for the rest of the turn
|
|
45
|
+
* stays silent for the rest of the turn.
|
|
45
46
|
*/
|
|
46
47
|
export function createToolFailureStopHook() {
|
|
47
48
|
let unresolvedFailure = false;
|
|
@@ -672,11 +672,14 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
672
672
|
},
|
|
673
673
|
onToolResult: _trackToolResult,
|
|
674
674
|
onToolApproval: typeof askOpts?.onToolApproval === 'function' ? askOpts.onToolApproval : undefined,
|
|
675
|
+
beforeToolExecution: typeof askOpts?.beforeToolExecution === 'function'
|
|
676
|
+
? askOpts.beforeToolExecution
|
|
677
|
+
: undefined,
|
|
675
678
|
onCompactEvent: (event) => {
|
|
676
679
|
_scheduleTurnCheckpoint(true);
|
|
677
680
|
try { askOpts?.onCompactEvent?.(event); } catch {}
|
|
678
681
|
},
|
|
679
|
-
//
|
|
682
|
+
// Mid-chain queued prompt/notification
|
|
680
683
|
// drain is owned by agentLoop at provider-continuation
|
|
681
684
|
// boundaries (after a tool batch, before the next send).
|
|
682
685
|
// The post-loop _pendingTail drain below still handles
|
|
@@ -692,9 +695,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
692
695
|
// Manager/pending-messages entries carry no
|
|
693
696
|
// mode/priority/slash metadata, so they stay OUT of the
|
|
694
697
|
// mid-chain (post-tool-batch) drain — that would bypass
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
+
// the queued-command filters. At the TERMINAL boundary
|
|
699
|
+
// they are exactly pending input: an `agent type=send`
|
|
700
|
+
// queued while
|
|
698
701
|
// the terminal sample was in flight must be folded into
|
|
699
702
|
// THIS turn before any stop hook runs, instead of losing
|
|
700
703
|
// its slot to a synthetic continuation prompt. The mutex
|
|
@@ -210,7 +210,7 @@ export function resolveSessionContextMeta(provider, model, seed = {}) {
|
|
|
210
210
|
}
|
|
211
211
|
export function compactTriggerForSession(session, boundaryTokens) {
|
|
212
212
|
// Delegates to the shared session-compaction policy (context-utils):
|
|
213
|
-
// agent semantic -> 90% (default buffer), main/user ->
|
|
213
|
+
// agent semantic -> 90% (default buffer), main/user -> 100% (buffer 0),
|
|
214
214
|
// truly-explicit sub-boundary limit wins.
|
|
215
215
|
return resolveSessionCompactPolicy(session, boundaryTokens).triggerTokens;
|
|
216
216
|
}
|
|
@@ -150,7 +150,7 @@ export function _runCleanupCycle() {
|
|
|
150
150
|
// Drain every settled runtime entry on each pass, not just the one or two
|
|
151
151
|
// sessions whose on-disk idle TTL happened to expire in this interval.
|
|
152
152
|
_sweepTerminalSessionRuntimes();
|
|
153
|
-
sweepOrphanedPendingMessages();
|
|
153
|
+
await sweepOrphanedPendingMessages();
|
|
154
154
|
await sweepIdleSessions({ includeTombstones: true });
|
|
155
155
|
// Reclaim same-process session snapshots whose state is durable on disk
|
|
156
156
|
// (memory-leak guard: _liveSessions used to grow for process lifetime).
|