@ouro.bot/cli 0.1.0-alpha.346 → 0.1.0-alpha.348
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/changelog.json +13 -0
- package/dist/heart/core.js +116 -132
- package/dist/heart/provider-attempt.js +133 -0
- package/dist/heart/provider-ping.js +116 -92
- package/dist/heart/session-events.js +5 -5
- package/dist/mind/context.js +56 -6
- package/dist/senses/cli.js +14 -6
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.348",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Provider attempts now share one bounded retry runner across real runtime turns, provider pings, health inventory, working-provider discovery, and GitHub Copilot model validation, retrying every provider failure class before terminal handling while preserving attempt metadata and nerves events.",
|
|
8
|
+
"Provider checks can now ping the selected lane model instead of drifting to provider defaults, and readiness/model pings use zero-delay retries so health checks stay fast."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.347",
|
|
13
|
+
"changes": [
|
|
14
|
+
"Post-turn session persist functions now return the events array directly, eliminating a redundant `loadSession` file read after every CLI TUI turn. `postTurnPersist` returns `SessionEvent[]` and `deferPostTurnPersist` returns `Promise<SessionEvent[]>`, so the CLI sense uses the returned data instead of re-reading the file it just wrote."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"version": "0.1.0-alpha.346",
|
|
6
19
|
"changes": [
|
package/dist/heart/core.js
CHANGED
|
@@ -10,7 +10,6 @@ exports.isExternalStateQuery = isExternalStateQuery;
|
|
|
10
10
|
exports.getSettleRetryError = getSettleRetryError;
|
|
11
11
|
exports.stripLastToolCalls = stripLastToolCalls;
|
|
12
12
|
exports.repairOrphanedToolCalls = repairOrphanedToolCalls;
|
|
13
|
-
exports.isRetryBlocked = isRetryBlocked;
|
|
14
13
|
exports.runAgent = runAgent;
|
|
15
14
|
const config_1 = require("./config");
|
|
16
15
|
const identity_1 = require("./identity");
|
|
@@ -33,6 +32,7 @@ const tool_loop_1 = require("./tool-loop");
|
|
|
33
32
|
const packets_1 = require("../arc/packets");
|
|
34
33
|
const tool_friction_1 = require("./tool-friction");
|
|
35
34
|
const provider_models_1 = require("./provider-models");
|
|
35
|
+
const provider_attempt_1 = require("./provider-attempt");
|
|
36
36
|
const _providerRuntimes = {
|
|
37
37
|
human: null,
|
|
38
38
|
agent: null,
|
|
@@ -366,38 +366,6 @@ function isContextOverflow(err) {
|
|
|
366
366
|
return true;
|
|
367
367
|
return false;
|
|
368
368
|
}
|
|
369
|
-
// HTTP statuses that will never become retryable on their own — the request is
|
|
370
|
-
// semantically wrong (malformed, unauthorized, missing route, etc.) and the
|
|
371
|
-
// caller has to do something different before it can succeed.
|
|
372
|
-
const NON_RETRYABLE_HTTP_STATUSES = new Set([
|
|
373
|
-
400, // Bad Request — malformed payload
|
|
374
|
-
401, // Unauthorized — credentials invalid/expired
|
|
375
|
-
403, // Forbidden — credentials lack permission
|
|
376
|
-
404, // Not Found — model/route doesn't exist
|
|
377
|
-
422, // Unprocessable Entity — semantic validation failure
|
|
378
|
-
]);
|
|
379
|
-
// Provider-classified error categories that we never retry. usage-limit is
|
|
380
|
-
// distinct from rate-limit: rate limits clear in seconds (retryable), usage
|
|
381
|
-
// limits are billing quotas that take hours/days to reset.
|
|
382
|
-
const NON_RETRYABLE_CLASSIFICATIONS = new Set([
|
|
383
|
-
"auth-failure",
|
|
384
|
-
"usage-limit",
|
|
385
|
-
]);
|
|
386
|
-
// Default policy: retry every error from the provider, EXCEPT the small set
|
|
387
|
-
// above. The user explicitly requested this — past behavior was to retry only
|
|
388
|
-
// on a known-transient list, which silently dropped real harness/SDK timeouts
|
|
389
|
-
// (e.g. OpenAI SDK's "Request timed out." has no err.code and no status, so
|
|
390
|
-
// the substring matchers missed it).
|
|
391
|
-
function isRetryBlocked(error, classification) {
|
|
392
|
-
const status = error.status;
|
|
393
|
-
if (status !== undefined && NON_RETRYABLE_HTTP_STATUSES.has(status))
|
|
394
|
-
return true;
|
|
395
|
-
if (NON_RETRYABLE_CLASSIFICATIONS.has(classification))
|
|
396
|
-
return true;
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
399
|
-
const MAX_RETRIES = 3;
|
|
400
|
-
const RETRY_BASE_MS = 2000;
|
|
401
369
|
const RETRY_LABELS = {
|
|
402
370
|
"auth-failure": "auth error",
|
|
403
371
|
"usage-limit": "usage limit",
|
|
@@ -406,6 +374,29 @@ const RETRY_LABELS = {
|
|
|
406
374
|
"network-error": "network error",
|
|
407
375
|
"unknown": "error",
|
|
408
376
|
};
|
|
377
|
+
function waitForProviderRetry(delayMs, signal) {
|
|
378
|
+
if (!signal) {
|
|
379
|
+
return new Promise((resolve) => {
|
|
380
|
+
setTimeout(resolve, delayMs);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return new Promise((resolve, reject) => {
|
|
384
|
+
let timer;
|
|
385
|
+
const onAbort = () => {
|
|
386
|
+
clearTimeout(timer);
|
|
387
|
+
reject(new provider_attempt_1.ProviderAttemptAbortError());
|
|
388
|
+
};
|
|
389
|
+
timer = setTimeout(() => {
|
|
390
|
+
signal.removeEventListener("abort", onAbort);
|
|
391
|
+
resolve();
|
|
392
|
+
}, delayMs);
|
|
393
|
+
if (signal.aborted) {
|
|
394
|
+
onAbort();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
398
|
+
});
|
|
399
|
+
}
|
|
409
400
|
function buildAuthFailureGuidance(provider, model, agentName, detail) {
|
|
410
401
|
const mismatch = (0, provider_models_1.getProviderModelMismatchMessage)(provider, model);
|
|
411
402
|
const modelLabel = model
|
|
@@ -500,7 +491,6 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
500
491
|
let done = false;
|
|
501
492
|
let lastUsage;
|
|
502
493
|
let overflowRetried = false;
|
|
503
|
-
let retryCount = 0;
|
|
504
494
|
let outcome = "settled";
|
|
505
495
|
let completion;
|
|
506
496
|
let terminalError;
|
|
@@ -515,6 +505,35 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
515
505
|
let sawExternalStateQuery = false;
|
|
516
506
|
const toolLoopState = (0, tool_loop_1.createToolLoopState)();
|
|
517
507
|
const toolFrictionLedger = (0, tool_friction_1.createToolFrictionLedger)();
|
|
508
|
+
const finishTerminalProviderError = (error, classification) => {
|
|
509
|
+
terminalError = error;
|
|
510
|
+
terminalErrorClassification = classification;
|
|
511
|
+
/* v8 ignore start — auth-failure guidance: tested via provider error classification tests @preserve */
|
|
512
|
+
if (terminalErrorClassification === "auth-failure") {
|
|
513
|
+
const agentName = (0, identity_2.getAgentName)();
|
|
514
|
+
const currentProvider = providerRuntime.id;
|
|
515
|
+
callbacks.onError(new Error(buildAuthFailureGuidance(currentProvider, providerRuntime.model, agentName, terminalError.message)), "terminal");
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
callbacks.onError(terminalError, "terminal");
|
|
519
|
+
}
|
|
520
|
+
/* v8 ignore stop */
|
|
521
|
+
(0, runtime_1.emitNervesEvent)({
|
|
522
|
+
level: "error",
|
|
523
|
+
event: "engine.error",
|
|
524
|
+
trace_id: traceId,
|
|
525
|
+
component: "engine",
|
|
526
|
+
message: terminalError.message,
|
|
527
|
+
meta: {
|
|
528
|
+
provider: providerRuntime.id,
|
|
529
|
+
model: providerRuntime.model,
|
|
530
|
+
errorClassification: terminalErrorClassification,
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
stripLastToolCalls(messages);
|
|
534
|
+
outcome = "errored";
|
|
535
|
+
done = true;
|
|
536
|
+
};
|
|
518
537
|
// Prevent MaxListenersExceeded warning — each iteration adds a listener
|
|
519
538
|
try {
|
|
520
539
|
require("events").setMaxListeners(50, signal);
|
|
@@ -581,21 +600,71 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
581
600
|
break;
|
|
582
601
|
}
|
|
583
602
|
try {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
603
|
+
const callProviderTurn = async () => {
|
|
604
|
+
callbacks.onModelStart();
|
|
605
|
+
try {
|
|
606
|
+
return await providerRuntime.streamTurn({
|
|
607
|
+
messages,
|
|
608
|
+
activeTools,
|
|
609
|
+
callbacks,
|
|
610
|
+
signal,
|
|
611
|
+
traceId,
|
|
612
|
+
toolChoiceRequired,
|
|
613
|
+
reasoningEffort: currentReasoningEffort,
|
|
614
|
+
eagerSettleStreaming: true,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
if (signal?.aborted)
|
|
619
|
+
throw new provider_attempt_1.ProviderAttemptAbortError();
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
const callProviderTurnWithOverflowRecovery = async () => {
|
|
624
|
+
try {
|
|
625
|
+
return await callProviderTurn();
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
if (error instanceof provider_attempt_1.ProviderAttemptAbortError)
|
|
629
|
+
throw error;
|
|
630
|
+
if (isContextOverflow(error) && !overflowRetried) {
|
|
631
|
+
overflowRetried = true;
|
|
632
|
+
stripLastToolCalls(messages);
|
|
633
|
+
const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
|
|
634
|
+
const trimmed = (0, context_1.trimMessages)(messages, maxTokens, contextMargin, maxTokens * 2);
|
|
635
|
+
messages.splice(0, messages.length, ...trimmed);
|
|
636
|
+
providerRuntime.resetTurnState(messages);
|
|
637
|
+
callbacks.onError(new Error("context trimmed, retrying..."), "transient");
|
|
638
|
+
return callProviderTurn();
|
|
639
|
+
}
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
const attempt = await (0, provider_attempt_1.runProviderAttempt)({
|
|
644
|
+
operation: "turn",
|
|
645
|
+
provider: providerRuntime.id,
|
|
646
|
+
model: providerRuntime.model,
|
|
647
|
+
run: callProviderTurnWithOverflowRecovery,
|
|
648
|
+
classifyError: (error) => providerRuntime.classifyError(error),
|
|
649
|
+
onRetry: (record, maxAttempts) => {
|
|
650
|
+
const delayMs = record.delayMs;
|
|
651
|
+
const seconds = delayMs / 1000;
|
|
652
|
+
const cause = RETRY_LABELS[record.classification];
|
|
653
|
+
callbacks.onError(new Error(`${cause}, retrying in ${seconds}s (${record.attempt}/${maxAttempts})...`), "transient");
|
|
654
|
+
},
|
|
655
|
+
sleep: async (delayMs) => {
|
|
656
|
+
await waitForProviderRetry(delayMs, signal);
|
|
657
|
+
providerRuntime.resetTurnState(messages);
|
|
658
|
+
},
|
|
594
659
|
});
|
|
660
|
+
if (!attempt.ok) {
|
|
661
|
+
finishTerminalProviderError(attempt.error, attempt.classification);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const result = attempt.value;
|
|
595
665
|
// Track usage from the latest API call
|
|
596
666
|
if (result.usage)
|
|
597
667
|
lastUsage = result.usage;
|
|
598
|
-
retryCount = 0; // reset on success
|
|
599
668
|
// SHARED: build CC-format assistant message from TurnResult
|
|
600
669
|
const msg = {
|
|
601
670
|
role: "assistant",
|
|
@@ -978,26 +1047,11 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
978
1047
|
}
|
|
979
1048
|
catch (e) {
|
|
980
1049
|
// Abort is not an error — just stop cleanly
|
|
981
|
-
if (signal?.aborted) {
|
|
1050
|
+
if (e instanceof provider_attempt_1.ProviderAttemptAbortError || signal?.aborted) {
|
|
982
1051
|
stripLastToolCalls(messages);
|
|
983
1052
|
outcome = "aborted";
|
|
984
1053
|
break;
|
|
985
1054
|
}
|
|
986
|
-
// Context overflow: trim aggressively and retry once
|
|
987
|
-
if (isContextOverflow(e) && !overflowRetried) {
|
|
988
|
-
overflowRetried = true;
|
|
989
|
-
stripLastToolCalls(messages);
|
|
990
|
-
const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
|
|
991
|
-
const trimmed = (0, context_1.trimMessages)(messages, maxTokens, contextMargin, maxTokens * 2);
|
|
992
|
-
messages.splice(0, messages.length, ...trimmed);
|
|
993
|
-
providerRuntime.resetTurnState(messages);
|
|
994
|
-
callbacks.onError(new Error("context trimmed, retrying..."), "transient");
|
|
995
|
-
continue;
|
|
996
|
-
}
|
|
997
|
-
// Retry policy: retry every error EXCEPT those on the blocklist
|
|
998
|
-
// (NON_RETRYABLE_HTTP_STATUSES / NON_RETRYABLE_CLASSIFICATIONS).
|
|
999
|
-
// The classification still drives the user-facing label and the
|
|
1000
|
-
// auth-failure guidance message below — it just no longer gates retries.
|
|
1001
1055
|
const errorForClassification = e instanceof Error ? e : /* v8 ignore next -- defensive @preserve */ new Error(String(e));
|
|
1002
1056
|
let providerClassification;
|
|
1003
1057
|
try {
|
|
@@ -1007,77 +1061,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1007
1061
|
/* v8 ignore next -- defensive: classifyError should not throw @preserve */
|
|
1008
1062
|
providerClassification = "unknown";
|
|
1009
1063
|
}
|
|
1010
|
-
|
|
1011
|
-
const shouldRetry = !blocked && retryCount < MAX_RETRIES;
|
|
1012
|
-
(0, runtime_1.emitNervesEvent)({
|
|
1013
|
-
level: shouldRetry ? "info" : "warn",
|
|
1014
|
-
event: shouldRetry ? "engine.provider_retry" : "engine.provider_retry_skip",
|
|
1015
|
-
component: "engine",
|
|
1016
|
-
message: shouldRetry
|
|
1017
|
-
? `provider error is retryable (attempt ${retryCount + 1}/${MAX_RETRIES})`
|
|
1018
|
-
: blocked
|
|
1019
|
-
? `provider error is on retry blocklist`
|
|
1020
|
-
: `provider error retries exhausted`,
|
|
1021
|
-
meta: {
|
|
1022
|
-
provider: providerRuntime.id,
|
|
1023
|
-
model: providerRuntime.model,
|
|
1024
|
-
retryCount,
|
|
1025
|
-
maxRetries: MAX_RETRIES,
|
|
1026
|
-
blocked,
|
|
1027
|
-
providerClassification,
|
|
1028
|
-
errorMessage: errorForClassification.message.slice(0, 200),
|
|
1029
|
-
httpStatus: e.status ?? null,
|
|
1030
|
-
},
|
|
1031
|
-
});
|
|
1032
|
-
if (shouldRetry) {
|
|
1033
|
-
retryCount++;
|
|
1034
|
-
const delay = RETRY_BASE_MS * Math.pow(2, retryCount - 1);
|
|
1035
|
-
const cause = RETRY_LABELS[providerClassification];
|
|
1036
|
-
callbacks.onError(new Error(`${cause}, retrying in ${delay / 1000}s (${retryCount}/${MAX_RETRIES})...`), "transient");
|
|
1037
|
-
// Wait with abort support
|
|
1038
|
-
const aborted = await new Promise((resolve) => {
|
|
1039
|
-
const timer = setTimeout(() => resolve(false), delay);
|
|
1040
|
-
if (signal) {
|
|
1041
|
-
const onAbort = () => { clearTimeout(timer); resolve(true); };
|
|
1042
|
-
if (signal.aborted) {
|
|
1043
|
-
clearTimeout(timer);
|
|
1044
|
-
resolve(true);
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1048
|
-
}
|
|
1049
|
-
});
|
|
1050
|
-
if (aborted) {
|
|
1051
|
-
stripLastToolCalls(messages);
|
|
1052
|
-
outcome = "aborted";
|
|
1053
|
-
break;
|
|
1054
|
-
}
|
|
1055
|
-
providerRuntime.resetTurnState(messages);
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1058
|
-
terminalError = errorForClassification;
|
|
1059
|
-
terminalErrorClassification = providerClassification;
|
|
1060
|
-
/* v8 ignore start — auth-failure guidance: tested via provider error classification tests @preserve */
|
|
1061
|
-
if (terminalErrorClassification === "auth-failure") {
|
|
1062
|
-
const agentName = (0, identity_2.getAgentName)();
|
|
1063
|
-
const currentProvider = providerRuntime.id;
|
|
1064
|
-
callbacks.onError(new Error(buildAuthFailureGuidance(currentProvider, providerRuntime.model, agentName, terminalError.message)), "terminal");
|
|
1065
|
-
}
|
|
1066
|
-
else {
|
|
1067
|
-
callbacks.onError(terminalError, "terminal");
|
|
1068
|
-
}
|
|
1069
|
-
/* v8 ignore stop */
|
|
1070
|
-
(0, runtime_1.emitNervesEvent)({
|
|
1071
|
-
level: "error",
|
|
1072
|
-
event: "engine.error",
|
|
1073
|
-
trace_id: traceId,
|
|
1074
|
-
component: "engine",
|
|
1075
|
-
message: terminalError.message,
|
|
1076
|
-
meta: { errorClassification: terminalErrorClassification },
|
|
1077
|
-
});
|
|
1078
|
-
stripLastToolCalls(messages);
|
|
1079
|
-
outcome = "errored";
|
|
1080
|
-
done = true;
|
|
1064
|
+
finishTerminalProviderError(errorForClassification, providerClassification);
|
|
1081
1065
|
}
|
|
1082
1066
|
}
|
|
1083
1067
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_PROVIDER_ATTEMPT_POLICY = exports.ProviderAttemptAbortError = void 0;
|
|
4
|
+
exports.runProviderAttempt = runProviderAttempt;
|
|
5
|
+
const runtime_1 = require("../nerves/runtime");
|
|
6
|
+
class ProviderAttemptAbortError extends Error {
|
|
7
|
+
constructor(message = "provider attempt aborted") {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ProviderAttemptAbortError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.ProviderAttemptAbortError = ProviderAttemptAbortError;
|
|
13
|
+
exports.DEFAULT_PROVIDER_ATTEMPT_POLICY = {
|
|
14
|
+
maxAttempts: 3,
|
|
15
|
+
baseDelayMs: 2_000,
|
|
16
|
+
backoffMultiplier: 2,
|
|
17
|
+
};
|
|
18
|
+
function sleep(delayMs) {
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
20
|
+
}
|
|
21
|
+
function normalizePolicy(policy) {
|
|
22
|
+
return {
|
|
23
|
+
...exports.DEFAULT_PROVIDER_ATTEMPT_POLICY,
|
|
24
|
+
...policy,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function toError(error) {
|
|
28
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
29
|
+
}
|
|
30
|
+
function classify(error, classifyError) {
|
|
31
|
+
if (!(error instanceof Error))
|
|
32
|
+
return "unknown";
|
|
33
|
+
try {
|
|
34
|
+
return classifyError(error);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return "unknown";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function httpStatus(error) {
|
|
41
|
+
const status = error.status;
|
|
42
|
+
return typeof status === "number" ? status : null;
|
|
43
|
+
}
|
|
44
|
+
function delayForAttempt(policy, attempt) {
|
|
45
|
+
return policy.baseDelayMs * Math.pow(policy.backoffMultiplier, attempt - 1);
|
|
46
|
+
}
|
|
47
|
+
async function runProviderAttempt(input) {
|
|
48
|
+
const policy = normalizePolicy(input.policy);
|
|
49
|
+
const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));
|
|
50
|
+
const wait = input.sleep ?? sleep;
|
|
51
|
+
const attempts = [];
|
|
52
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
53
|
+
try {
|
|
54
|
+
const value = await input.run();
|
|
55
|
+
attempts.push({
|
|
56
|
+
attempt,
|
|
57
|
+
provider: input.provider,
|
|
58
|
+
model: input.model,
|
|
59
|
+
operation: input.operation,
|
|
60
|
+
ok: true,
|
|
61
|
+
willRetry: false,
|
|
62
|
+
});
|
|
63
|
+
(0, runtime_1.emitNervesEvent)({
|
|
64
|
+
component: "engine",
|
|
65
|
+
event: "engine.provider_attempt_succeeded",
|
|
66
|
+
message: "provider attempt succeeded",
|
|
67
|
+
meta: { provider: input.provider, model: input.model, operation: input.operation, attempt, maxAttempts },
|
|
68
|
+
});
|
|
69
|
+
return { ok: true, value, attempts };
|
|
70
|
+
}
|
|
71
|
+
catch (caught) {
|
|
72
|
+
if (caught instanceof ProviderAttemptAbortError)
|
|
73
|
+
throw caught;
|
|
74
|
+
const error = toError(caught);
|
|
75
|
+
const classification = classify(caught, input.classifyError);
|
|
76
|
+
const willRetry = attempt < maxAttempts;
|
|
77
|
+
const delayMs = willRetry ? delayForAttempt(policy, attempt) : undefined;
|
|
78
|
+
const record = {
|
|
79
|
+
attempt,
|
|
80
|
+
provider: input.provider,
|
|
81
|
+
model: input.model,
|
|
82
|
+
operation: input.operation,
|
|
83
|
+
ok: false,
|
|
84
|
+
classification,
|
|
85
|
+
errorMessage: error.message,
|
|
86
|
+
httpStatus: httpStatus(error),
|
|
87
|
+
willRetry,
|
|
88
|
+
...(delayMs !== undefined ? { delayMs } : {}),
|
|
89
|
+
};
|
|
90
|
+
attempts.push(record);
|
|
91
|
+
if (!willRetry) {
|
|
92
|
+
(0, runtime_1.emitNervesEvent)({
|
|
93
|
+
level: "warn",
|
|
94
|
+
component: "engine",
|
|
95
|
+
event: "engine.provider_attempt_failed",
|
|
96
|
+
message: "provider attempt failed",
|
|
97
|
+
meta: {
|
|
98
|
+
provider: input.provider,
|
|
99
|
+
model: input.model,
|
|
100
|
+
operation: input.operation,
|
|
101
|
+
attempt,
|
|
102
|
+
maxAttempts,
|
|
103
|
+
classification,
|
|
104
|
+
errorMessage: error.message.slice(0, 200),
|
|
105
|
+
httpStatus: httpStatus(error),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return { ok: false, error, classification, attempts };
|
|
109
|
+
}
|
|
110
|
+
const retryDelayMs = delayMs;
|
|
111
|
+
(0, runtime_1.emitNervesEvent)({
|
|
112
|
+
component: "engine",
|
|
113
|
+
event: "engine.provider_attempt_retry",
|
|
114
|
+
message: "provider attempt failed; retrying",
|
|
115
|
+
meta: {
|
|
116
|
+
provider: input.provider,
|
|
117
|
+
model: input.model,
|
|
118
|
+
operation: input.operation,
|
|
119
|
+
attempt,
|
|
120
|
+
maxAttempts,
|
|
121
|
+
classification,
|
|
122
|
+
errorMessage: error.message.slice(0, 200),
|
|
123
|
+
httpStatus: httpStatus(error),
|
|
124
|
+
delayMs: retryDelayMs,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
await input.onRetry?.(record, maxAttempts);
|
|
128
|
+
await wait(retryDelayMs);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/* v8 ignore next 2 -- defensive: loop always returns on success or final failure @preserve */
|
|
132
|
+
return { ok: false, error: new Error("provider attempt loop ended unexpectedly"), classification: "unknown", attempts };
|
|
133
|
+
}
|
|
@@ -13,6 +13,7 @@ const github_copilot_1 = require("./providers/github-copilot");
|
|
|
13
13
|
const auth_flow_1 = require("./auth/auth-flow");
|
|
14
14
|
const provider_models_1 = require("./provider-models");
|
|
15
15
|
const runtime_1 = require("../nerves/runtime");
|
|
16
|
+
const provider_attempt_1 = require("./provider-attempt");
|
|
16
17
|
const PING_TIMEOUT_MS = 10_000;
|
|
17
18
|
const PING_PROMPT = "ping";
|
|
18
19
|
const CHAT_PING_MAX_TOKENS = 1;
|
|
@@ -67,6 +68,30 @@ function sanitizeErrorMessage(message) {
|
|
|
67
68
|
// Already clean (e.g., "401 Provided authentication token is expired.")
|
|
68
69
|
return message;
|
|
69
70
|
}
|
|
71
|
+
async function readGithubCopilotModelPingError(response) {
|
|
72
|
+
let detail = `HTTP ${response.status}`;
|
|
73
|
+
try {
|
|
74
|
+
const json = await response.json();
|
|
75
|
+
/* v8 ignore start -- error format parsing: all branches tested via config-models.test.ts @preserve */
|
|
76
|
+
if (typeof json.error === "string")
|
|
77
|
+
detail = json.error;
|
|
78
|
+
else if (typeof json.error === "object" && json.error !== null) {
|
|
79
|
+
const errObj = json.error;
|
|
80
|
+
if (typeof errObj.message === "string")
|
|
81
|
+
detail = errObj.message;
|
|
82
|
+
}
|
|
83
|
+
else if (typeof json.message === "string")
|
|
84
|
+
detail = json.message;
|
|
85
|
+
/* v8 ignore stop */
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// response body not JSON — keep HTTP status
|
|
89
|
+
}
|
|
90
|
+
return detail;
|
|
91
|
+
}
|
|
92
|
+
function createStatusError(message, status) {
|
|
93
|
+
return Object.assign(new Error(message), { status });
|
|
94
|
+
}
|
|
70
95
|
async function pingGithubCopilotModel(baseUrl, token, model, fetchImpl = fetch) {
|
|
71
96
|
const base = baseUrl.replace(/\/+$/, "");
|
|
72
97
|
const isClaude = model.startsWith("claude");
|
|
@@ -74,41 +99,31 @@ async function pingGithubCopilotModel(baseUrl, token, model, fetchImpl = fetch)
|
|
|
74
99
|
const body = isClaude
|
|
75
100
|
? JSON.stringify(createChatPingRequest(model))
|
|
76
101
|
: JSON.stringify(createResponsePingRequest(model));
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
const attempt = await (0, provider_attempt_1.runProviderAttempt)({
|
|
103
|
+
operation: "model-ping",
|
|
104
|
+
provider: "github-copilot",
|
|
105
|
+
model,
|
|
106
|
+
classifyError: github_copilot_1.classifyGithubCopilotError,
|
|
107
|
+
policy: {
|
|
108
|
+
maxAttempts: 3,
|
|
109
|
+
baseDelayMs: 0,
|
|
110
|
+
backoffMultiplier: 2,
|
|
111
|
+
},
|
|
112
|
+
run: async () => {
|
|
113
|
+
const response = await fetchImpl(url, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: `Bearer ${token}`,
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
},
|
|
119
|
+
body,
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw createStatusError(await readGithubCopilotModelPingError(response), response.status);
|
|
98
123
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
// response body not JSON — keep HTTP status
|
|
105
|
-
}
|
|
106
|
-
return { ok: false, error: detail };
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
/* v8 ignore next -- defensive: fetch errors are always Error instances @preserve */
|
|
110
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
111
|
-
}
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return attempt.ok ? { ok: true } : { ok: false, error: attempt.error.message };
|
|
112
127
|
}
|
|
113
128
|
function hasEmptyCredentials(provider, config) {
|
|
114
129
|
const record = config;
|
|
@@ -121,37 +136,37 @@ function hasEmptyCredentials(provider, config) {
|
|
|
121
136
|
}
|
|
122
137
|
return identity_1.PROVIDER_CREDENTIALS[provider].required.some((key) => !record[key]);
|
|
123
138
|
}
|
|
124
|
-
function createRuntimeForPing(provider, config) {
|
|
139
|
+
function createRuntimeForPing(provider, config, model) {
|
|
125
140
|
// Use the same provider defaults as auth switch and hatch so verification
|
|
126
141
|
// cannot drift to stale provider/model pairings, and pass the checked
|
|
127
142
|
// credentials directly so daemon-side pings do not depend on --agent globals.
|
|
128
|
-
const
|
|
143
|
+
const resolvedModel = model ?? (0, provider_models_1.getDefaultModelForProvider)(provider);
|
|
129
144
|
switch (provider) {
|
|
130
145
|
case "anthropic":
|
|
131
|
-
return (0, anthropic_1.createAnthropicProviderRuntime)(
|
|
146
|
+
return (0, anthropic_1.createAnthropicProviderRuntime)(resolvedModel, config);
|
|
132
147
|
case "azure":
|
|
133
|
-
return (0, azure_1.createAzureProviderRuntime)(
|
|
148
|
+
return (0, azure_1.createAzureProviderRuntime)(resolvedModel, {
|
|
134
149
|
...config,
|
|
135
150
|
apiVersion: config.apiVersion ?? DEFAULT_AZURE_API_VERSION,
|
|
136
151
|
});
|
|
137
152
|
case "minimax":
|
|
138
|
-
return (0, minimax_1.createMinimaxProviderRuntime)(
|
|
153
|
+
return (0, minimax_1.createMinimaxProviderRuntime)(resolvedModel, config);
|
|
139
154
|
case "openai-codex":
|
|
140
|
-
return (0, openai_codex_1.createOpenAICodexProviderRuntime)(
|
|
155
|
+
return (0, openai_codex_1.createOpenAICodexProviderRuntime)(resolvedModel, config);
|
|
141
156
|
case "github-copilot":
|
|
142
|
-
return (0, github_copilot_1.createGithubCopilotProviderRuntime)(
|
|
157
|
+
return (0, github_copilot_1.createGithubCopilotProviderRuntime)(resolvedModel, config);
|
|
143
158
|
/* v8 ignore next 2 -- exhaustive: all providers handled above @preserve */
|
|
144
159
|
default:
|
|
145
160
|
throw new Error(`unsupported provider for ping: ${provider}`);
|
|
146
161
|
}
|
|
147
162
|
}
|
|
148
|
-
async function pingProvider(provider, config) {
|
|
163
|
+
async function pingProvider(provider, config, options = {}) {
|
|
149
164
|
if (hasEmptyCredentials(provider, config)) {
|
|
150
165
|
return { ok: false, classification: "auth-failure", message: "no credentials configured" };
|
|
151
166
|
}
|
|
152
167
|
let runtime;
|
|
153
168
|
try {
|
|
154
|
-
runtime = createRuntimeForPing(provider, config);
|
|
169
|
+
runtime = createRuntimeForPing(provider, config, options.model);
|
|
155
170
|
/* v8 ignore start -- factory creation failure: tested via individual provider init tests @preserve */
|
|
156
171
|
}
|
|
157
172
|
catch (error) {
|
|
@@ -162,58 +177,67 @@ async function pingProvider(provider, config) {
|
|
|
162
177
|
};
|
|
163
178
|
}
|
|
164
179
|
/* v8 ignore stop */
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
180
|
+
const attempt = await (0, provider_attempt_1.runProviderAttempt)({
|
|
181
|
+
operation: "ping",
|
|
182
|
+
provider,
|
|
183
|
+
model: runtime.model,
|
|
184
|
+
classifyError: (error) => runtime.classifyError(error),
|
|
185
|
+
policy: {
|
|
186
|
+
maxAttempts: 3,
|
|
187
|
+
baseDelayMs: 0,
|
|
188
|
+
backoffMultiplier: 2,
|
|
189
|
+
...options.attemptPolicy,
|
|
190
|
+
},
|
|
191
|
+
sleep: options.sleep,
|
|
192
|
+
run: async () => {
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
/* v8 ignore next -- timeout callback: only fires after 10s, tests resolve faster @preserve */
|
|
195
|
+
const timeout = setTimeout(() => controller.abort(), PING_TIMEOUT_MS);
|
|
196
|
+
try {
|
|
197
|
+
// Minimal API call — no thinking, no reasoning, no tools.
|
|
198
|
+
if (provider === "anthropic") {
|
|
199
|
+
// Use haiku for the ping — setup tokens may not have access to newer
|
|
200
|
+
// models, but if haiku works, the credentials are valid.
|
|
201
|
+
// Override the beta header to exclude thinking (which requires a
|
|
202
|
+
// thinking param in the request body).
|
|
203
|
+
const client = runtime.client;
|
|
204
|
+
await client.messages.create(createChatPingRequest(ANTHROPIC_SETUP_PING_MODEL), { signal: controller.signal, headers: { "anthropic-beta": "claude-code-20250219,oauth-2025-04-20" } });
|
|
205
|
+
}
|
|
206
|
+
else if (provider === "openai-codex") {
|
|
207
|
+
await runtime.streamTurn({
|
|
208
|
+
messages: createPingMessages(),
|
|
209
|
+
activeTools: [],
|
|
210
|
+
callbacks: PING_CALLBACKS,
|
|
211
|
+
signal: controller.signal,
|
|
212
|
+
toolChoiceRequired: false,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// OpenAI-compatible providers (azure, minimax, github-copilot)
|
|
217
|
+
const client = runtime.client;
|
|
218
|
+
await client.chat.completions.create(createChatPingRequest(runtime.model), { signal: controller.signal });
|
|
219
|
+
}
|
|
178
220
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
messages: createPingMessages(),
|
|
182
|
-
activeTools: [],
|
|
183
|
-
callbacks: PING_CALLBACKS,
|
|
184
|
-
signal: controller.signal,
|
|
185
|
-
toolChoiceRequired: false,
|
|
186
|
-
});
|
|
221
|
+
finally {
|
|
222
|
+
clearTimeout(timeout);
|
|
187
223
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
return { ok: true };
|
|
194
|
-
}
|
|
195
|
-
finally {
|
|
196
|
-
clearTimeout(timeout);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
catch (error) {
|
|
200
|
-
const err = error instanceof Error ? error : /* v8 ignore next -- defensive @preserve */ new Error(String(error));
|
|
201
|
-
let classification;
|
|
202
|
-
try {
|
|
203
|
-
classification = runtime.classifyError(err);
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
/* v8 ignore next -- defensive: classifyError should not throw @preserve */
|
|
207
|
-
classification = "unknown";
|
|
208
|
-
}
|
|
209
|
-
(0, runtime_1.emitNervesEvent)({
|
|
210
|
-
component: "engine",
|
|
211
|
-
event: "engine.provider_ping_fail",
|
|
212
|
-
message: `provider ping failed: ${provider}`,
|
|
213
|
-
meta: { provider, classification, error: err.message },
|
|
214
|
-
});
|
|
215
|
-
return { ok: false, classification, message: sanitizeErrorMessage(err.message) };
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
if (attempt.ok) {
|
|
227
|
+
return { ok: true, attempts: attempt.attempts };
|
|
216
228
|
}
|
|
229
|
+
(0, runtime_1.emitNervesEvent)({
|
|
230
|
+
component: "engine",
|
|
231
|
+
event: "engine.provider_ping_fail",
|
|
232
|
+
message: `provider ping failed: ${provider}`,
|
|
233
|
+
meta: { provider, classification: attempt.classification, error: attempt.error.message },
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
classification: attempt.classification,
|
|
238
|
+
message: sanitizeErrorMessage(attempt.error.message),
|
|
239
|
+
attempts: attempt.attempts,
|
|
240
|
+
};
|
|
217
241
|
}
|
|
218
242
|
const PINGABLE_PROVIDERS = ["anthropic", "openai-codex", "azure", "minimax", "github-copilot"];
|
|
219
243
|
async function runHealthInventory(agentName, currentProvider, deps = {}) {
|
|
@@ -676,11 +676,11 @@ function selectProjectedEventIds(currentMessages, currentEventIds, trimmedMessag
|
|
|
676
676
|
}
|
|
677
677
|
function buildCanonicalSessionEnvelope(options) {
|
|
678
678
|
const existing = options.existing;
|
|
679
|
-
//
|
|
680
|
-
const currentIngressTimes = options.currentMessages.map(getIngressTime);
|
|
681
|
-
const previousMessages =
|
|
682
|
-
const currentMessages =
|
|
683
|
-
const trimmedMessages =
|
|
679
|
+
// Callers pass pre-sanitized messages + pre-captured ingress times.
|
|
680
|
+
const currentIngressTimes = options.currentIngressTimes ?? options.currentMessages.map(getIngressTime);
|
|
681
|
+
const previousMessages = options.previousMessages;
|
|
682
|
+
const currentMessages = options.currentMessages;
|
|
683
|
+
const trimmedMessages = options.trimmedMessages;
|
|
684
684
|
const previousProjectionIds = existing?.projection.eventIds.length
|
|
685
685
|
? [...existing.projection.eventIds]
|
|
686
686
|
: existing?.events.map((event) => event.id) ?? [];
|
package/dist/mind/context.js
CHANGED
|
@@ -39,6 +39,9 @@ exports.saveSession = saveSession;
|
|
|
39
39
|
exports.appendSyntheticAssistantMessage = appendSyntheticAssistantMessage;
|
|
40
40
|
exports.loadSession = loadSession;
|
|
41
41
|
exports.postTurn = postTurn;
|
|
42
|
+
exports.postTurnTrim = postTurnTrim;
|
|
43
|
+
exports.postTurnPersist = postTurnPersist;
|
|
44
|
+
exports.deferPostTurnPersist = deferPostTurnPersist;
|
|
42
45
|
exports.deleteSession = deleteSession;
|
|
43
46
|
const config_1 = require("../heart/config");
|
|
44
47
|
const session_events_1 = require("../heart/session-events");
|
|
@@ -193,12 +196,14 @@ function writeSessionEnvelope(filePath, envelope) {
|
|
|
193
196
|
function saveSession(filePath, messages, lastUsage, state) {
|
|
194
197
|
const existing = (0, session_events_1.loadSessionEnvelopeFile)(filePath);
|
|
195
198
|
const previousMessages = existing ? (0, session_events_1.projectProviderMessages)(existing) : [];
|
|
199
|
+
const currentIngressTimes = messages.map(session_events_1.getIngressTime);
|
|
196
200
|
const sanitized = (0, session_events_1.sanitizeProviderMessages)(messages);
|
|
197
201
|
const envelope = (0, session_events_1.buildCanonicalSessionEnvelope)({
|
|
198
202
|
existing,
|
|
199
203
|
previousMessages,
|
|
200
204
|
currentMessages: sanitized,
|
|
201
205
|
trimmedMessages: sanitized,
|
|
206
|
+
currentIngressTimes,
|
|
202
207
|
recordedAt: new Date().toISOString(),
|
|
203
208
|
lastUsage: lastUsage ?? null,
|
|
204
209
|
state,
|
|
@@ -247,7 +252,19 @@ function loadSession(filePath) {
|
|
|
247
252
|
return null;
|
|
248
253
|
}
|
|
249
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Synchronous post-turn: sanitize, trim (mutates messages in place), and persist to disk.
|
|
257
|
+
* For non-blocking persist, use postTurnTrim() + deferPostTurnPersist() instead.
|
|
258
|
+
*/
|
|
250
259
|
function postTurn(messages, sessPath, usage, hooks, state) {
|
|
260
|
+
const prepared = postTurnTrim(messages, usage, hooks);
|
|
261
|
+
postTurnPersist(sessPath, prepared, usage, state);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Synchronous phase: run hooks, sanitize, trim, and mutate the messages array in place.
|
|
265
|
+
* Returns the data needed by postTurnPersist / deferPostTurnPersist.
|
|
266
|
+
*/
|
|
267
|
+
function postTurnTrim(messages, usage, hooks) {
|
|
251
268
|
const preTrimMessages = [...messages];
|
|
252
269
|
if (hooks?.beforeTrim) {
|
|
253
270
|
try {
|
|
@@ -266,26 +283,59 @@ function postTurn(messages, sessPath, usage, hooks, state) {
|
|
|
266
283
|
}
|
|
267
284
|
}
|
|
268
285
|
const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
|
|
286
|
+
const currentIngressTimes = messages.map(session_events_1.getIngressTime);
|
|
269
287
|
const currentMessages = (0, session_events_1.sanitizeProviderMessages)(messages);
|
|
270
|
-
const
|
|
271
|
-
messages.splice(0, messages.length, ...
|
|
288
|
+
const trimmedMessages = trimMessages(currentMessages, maxTokens, contextMargin, usage?.input_tokens);
|
|
289
|
+
messages.splice(0, messages.length, ...trimmedMessages);
|
|
290
|
+
return { currentMessages, trimmedMessages, currentIngressTimes, maxTokens, contextMargin };
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Synchronous persist: load existing envelope, build canonical envelope, write to disk.
|
|
294
|
+
*/
|
|
295
|
+
function postTurnPersist(sessPath, prepared, usage, state) {
|
|
272
296
|
const existing = (0, session_events_1.loadSessionEnvelopeFile)(sessPath);
|
|
273
297
|
const previousMessages = existing ? (0, session_events_1.projectProviderMessages)(existing) : [];
|
|
274
298
|
const envelope = (0, session_events_1.buildCanonicalSessionEnvelope)({
|
|
275
299
|
existing,
|
|
276
300
|
previousMessages,
|
|
277
|
-
currentMessages,
|
|
278
|
-
trimmedMessages:
|
|
301
|
+
currentMessages: prepared.currentMessages,
|
|
302
|
+
trimmedMessages: prepared.trimmedMessages,
|
|
303
|
+
currentIngressTimes: prepared.currentIngressTimes,
|
|
279
304
|
recordedAt: new Date().toISOString(),
|
|
280
305
|
lastUsage: usage ?? null,
|
|
281
306
|
state,
|
|
282
307
|
projectionBasis: {
|
|
283
|
-
maxTokens,
|
|
284
|
-
contextMargin,
|
|
308
|
+
maxTokens: prepared.maxTokens,
|
|
309
|
+
contextMargin: prepared.contextMargin,
|
|
285
310
|
inputTokens: usage?.input_tokens ?? null,
|
|
286
311
|
},
|
|
287
312
|
});
|
|
288
313
|
writeSessionEnvelope(sessPath, envelope);
|
|
314
|
+
return envelope.events;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Deferred persist: same as postTurnPersist but runs on the next event loop tick.
|
|
318
|
+
* Returns a promise that resolves when the persist completes.
|
|
319
|
+
*/
|
|
320
|
+
function deferPostTurnPersist(sessPath, prepared, usage, state) {
|
|
321
|
+
return new Promise((resolve) => {
|
|
322
|
+
setImmediate(() => {
|
|
323
|
+
try {
|
|
324
|
+
const events = postTurnPersist(sessPath, prepared, usage, state);
|
|
325
|
+
resolve(events);
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
(0, runtime_1.emitNervesEvent)({
|
|
329
|
+
level: "warn",
|
|
330
|
+
component: "mind",
|
|
331
|
+
event: "mind.deferred_persist_error",
|
|
332
|
+
message: "deferred session persist failed",
|
|
333
|
+
meta: { error: err instanceof Error ? err.message : String(err) },
|
|
334
|
+
});
|
|
335
|
+
resolve([]);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
});
|
|
289
339
|
}
|
|
290
340
|
function deleteSession(filePath) {
|
|
291
341
|
try {
|
package/dist/senses/cli.js
CHANGED
|
@@ -1009,8 +1009,10 @@ async function main(agentName, options) {
|
|
|
1009
1009
|
lastActivityAt: sessionState?.lastFriendActivityAt,
|
|
1010
1010
|
_testInputSource: options?._testInputSource,
|
|
1011
1011
|
onAsyncAssistantMessage: async (messages, _assistantMessage) => {
|
|
1012
|
-
(0, context_1.
|
|
1013
|
-
|
|
1012
|
+
const prepared = (0, context_1.postTurnTrim)(messages);
|
|
1013
|
+
const events = (0, context_1.postTurnPersist)(sessPath, prepared, undefined, sessionState);
|
|
1014
|
+
/* v8 ignore next -- defensive: postTurnPersist always returns events in practice @preserve */
|
|
1015
|
+
sessionEvents = events.length > 0 ? events : sessionEvents;
|
|
1014
1016
|
},
|
|
1015
1017
|
runTurn: async (messages, userInput, callbacks, signal, toolContext, userContent) => {
|
|
1016
1018
|
// Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
|
|
@@ -1022,9 +1024,10 @@ async function main(agentName, options) {
|
|
|
1022
1024
|
/* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
|
|
1023
1025
|
const failoverAwareCallbacks = {
|
|
1024
1026
|
...callbacks,
|
|
1025
|
-
// Save session after each tool result for crash recovery
|
|
1027
|
+
// Save session after each tool result for crash recovery (deferred to avoid blocking)
|
|
1026
1028
|
onToolResult: (turnMessages) => {
|
|
1027
|
-
(0, context_1.
|
|
1029
|
+
const prepared = (0, context_1.postTurnTrim)(turnMessages);
|
|
1030
|
+
(0, context_1.deferPostTurnPersist)(sessPath, prepared, undefined, sessionState);
|
|
1028
1031
|
},
|
|
1029
1032
|
onError: (error, severity) => {
|
|
1030
1033
|
if (severity === "terminal" && failoverState) {
|
|
@@ -1069,9 +1072,14 @@ async function main(agentName, options) {
|
|
|
1069
1072
|
},
|
|
1070
1073
|
}),
|
|
1071
1074
|
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
1072
|
-
(
|
|
1075
|
+
// Trim synchronously (mutates turnMessages for next turn),
|
|
1076
|
+
// then defer envelope build + disk I/O to avoid blocking the TUI.
|
|
1077
|
+
const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
|
|
1073
1078
|
sessionState = state;
|
|
1074
|
-
|
|
1079
|
+
(0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state).then((events) => {
|
|
1080
|
+
/* v8 ignore next -- defensive: deferPostTurnPersist always resolves events in practice @preserve */
|
|
1081
|
+
sessionEvents = events.length > 0 ? events : sessionEvents;
|
|
1082
|
+
});
|
|
1075
1083
|
},
|
|
1076
1084
|
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
1077
1085
|
signal,
|