@ouro.bot/cli 0.1.0-alpha.729 → 0.1.0-alpha.730
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 +7 -0
- package/dist/heart/approval-store.js +791 -0
- package/dist/heart/core.js +177 -0
- package/dist/heart/session-events.js +167 -0
- package/dist/heart/tool-approval.js +339 -0
- package/dist/mind/context.js +80 -67
- package/dist/mind/session-transaction.js +379 -0
- package/dist/repertoire/stripe-client.js +2 -2
- package/dist/repertoire/tool-arguments.js +104 -0
- package/dist/repertoire/tools-shell.js +13 -0
- package/dist/repertoire/tools.js +4 -0
- package/dist/senses/bluebubbles/index.js +504 -493
- package/dist/senses/cli.js +234 -214
- package/dist/senses/pipeline.js +23 -0
- package/dist/senses/private-runtime.js +330 -325
- package/dist/senses/shared-turn.js +176 -169
- package/dist/senses/teams.js +175 -163
- package/package.json +2 -1
package/dist/senses/cli.js
CHANGED
|
@@ -68,7 +68,7 @@ const cli_logging_1 = require("../nerves/cli-logging");
|
|
|
68
68
|
const runtime_1 = require("../nerves/runtime");
|
|
69
69
|
const trust_gate_1 = require("./trust-gate");
|
|
70
70
|
const pipeline_1 = require("./pipeline");
|
|
71
|
-
const
|
|
71
|
+
const session_transaction_1 = require("../mind/session-transaction");
|
|
72
72
|
const update_hooks_1 = require("../heart/versioning/update-hooks");
|
|
73
73
|
const bundle_meta_1 = require("../heart/daemon/hooks/bundle-meta");
|
|
74
74
|
const agent_config_v2_1 = require("../heart/daemon/hooks/agent-config-v2");
|
|
@@ -797,96 +797,102 @@ async function runCliSession(options) {
|
|
|
797
797
|
continue;
|
|
798
798
|
}
|
|
799
799
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
if (
|
|
804
|
-
const
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
800
|
+
await options.onTurnStart?.(messages);
|
|
801
|
+
try {
|
|
802
|
+
// Check for slash commands (legacy path only — pipeline handles commands for runTurn path)
|
|
803
|
+
if (!options.runTurn) {
|
|
804
|
+
const parsed = (0, commands_1.parseSlashCommand)(input);
|
|
805
|
+
if (parsed) {
|
|
806
|
+
const dispatchResult = registry.dispatch(parsed.command, { channel: "cli" });
|
|
807
|
+
if (dispatchResult.handled && dispatchResult.result) {
|
|
808
|
+
if (dispatchResult.result.action === "exit") {
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
else if (dispatchResult.result.action === "response") {
|
|
812
|
+
display.text(dispatchResult.result.message || "");
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
808
815
|
}
|
|
809
|
-
|
|
810
|
-
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
// Track user message in TUI for display
|
|
819
|
+
if (tuiStore)
|
|
820
|
+
tuiStore.addUserMessage(input);
|
|
821
|
+
currentAbort = new AbortController();
|
|
822
|
+
if (tuiStore)
|
|
823
|
+
tuiStore.suppressInput();
|
|
824
|
+
inputCtrl?.suppress(() => { currentAbort?.abort(); });
|
|
825
|
+
// Resolve pending image content before the turn executes
|
|
826
|
+
let contentParts = null;
|
|
827
|
+
const currentImages = pendingImages;
|
|
828
|
+
if (currentImages && currentImages.size > 0) {
|
|
829
|
+
contentParts = await (0, image_paste_1.resolveImageContent)(input, currentImages);
|
|
830
|
+
pendingImages = null;
|
|
831
|
+
}
|
|
832
|
+
let result;
|
|
833
|
+
try {
|
|
834
|
+
if (options.runTurn) {
|
|
835
|
+
// Pipeline-based turn: the runTurn callback handles user message assembly,
|
|
836
|
+
// pending drain, trust gate, runAgent, postTurn, and token accumulation.
|
|
837
|
+
result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal, effectiveToolContext, contentParts ?? undefined);
|
|
838
|
+
// Handle pipeline-intercepted commands with loop-control side effects
|
|
839
|
+
if (result?.turnOutcome === "command") {
|
|
840
|
+
if (result.commandAction === "exit") {
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
// For "response" commands: the pipeline already emitted the response via onTextChunk
|
|
844
|
+
cliCallbacks.flushMarkdown();
|
|
811
845
|
continue;
|
|
812
846
|
}
|
|
813
847
|
}
|
|
848
|
+
else {
|
|
849
|
+
// Legacy path: inline runAgent (used by serpent guide and tests)
|
|
850
|
+
const prefix = options.getContentPrefix?.();
|
|
851
|
+
const userContent = contentParts
|
|
852
|
+
? contentParts
|
|
853
|
+
: (prefix ? `${prefix}\n\n${input}` : input);
|
|
854
|
+
const userMsg = { role: "user", content: userContent };
|
|
855
|
+
(0, session_events_1.stampIngressTime)(userMsg);
|
|
856
|
+
messages.push(userMsg);
|
|
857
|
+
const traceId = (0, nerves_1.createTraceId)();
|
|
858
|
+
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
859
|
+
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
860
|
+
traceId,
|
|
861
|
+
tools: options.tools,
|
|
862
|
+
execTool: wrappedExecTool,
|
|
863
|
+
toolContext: effectiveToolContext,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
814
866
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
tuiStore.addUserMessage(input);
|
|
819
|
-
currentAbort = new AbortController();
|
|
820
|
-
if (tuiStore)
|
|
821
|
-
tuiStore.suppressInput();
|
|
822
|
-
inputCtrl?.suppress(() => { currentAbort?.abort(); });
|
|
823
|
-
// Resolve pending image content before the turn executes
|
|
824
|
-
let contentParts = null;
|
|
825
|
-
const currentImages = pendingImages;
|
|
826
|
-
if (currentImages && currentImages.size > 0) {
|
|
827
|
-
contentParts = await (0, image_paste_1.resolveImageContent)(input, currentImages);
|
|
828
|
-
pendingImages = null;
|
|
829
|
-
}
|
|
830
|
-
let result;
|
|
831
|
-
try {
|
|
832
|
-
if (options.runTurn) {
|
|
833
|
-
// Pipeline-based turn: the runTurn callback handles user message assembly,
|
|
834
|
-
// pending drain, trust gate, runAgent, postTurn, and token accumulation.
|
|
835
|
-
result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal, effectiveToolContext, contentParts ?? undefined);
|
|
836
|
-
// Handle pipeline-intercepted commands with loop-control side effects
|
|
837
|
-
if (result?.turnOutcome === "command") {
|
|
838
|
-
if (result.commandAction === "exit") {
|
|
839
|
-
break;
|
|
840
|
-
}
|
|
841
|
-
// For "response" commands: the pipeline already emitted the response via onTextChunk
|
|
842
|
-
cliCallbacks.flushMarkdown();
|
|
843
|
-
continue;
|
|
867
|
+
catch (err) {
|
|
868
|
+
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
869
|
+
display.error(err instanceof Error ? err.message : String(err));
|
|
844
870
|
}
|
|
845
871
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
858
|
-
traceId,
|
|
859
|
-
tools: options.tools,
|
|
860
|
-
execTool: wrappedExecTool,
|
|
861
|
-
toolContext: effectiveToolContext,
|
|
862
|
-
});
|
|
872
|
+
cliCallbacks.flushMarkdown();
|
|
873
|
+
if (!tuiStore)
|
|
874
|
+
process.stdout.write("\n"); // ensure response ends with newline before prompt (imperative only)
|
|
875
|
+
if (tuiStore)
|
|
876
|
+
tuiStore.restoreInput();
|
|
877
|
+
inputCtrl?.restore();
|
|
878
|
+
currentAbort = null;
|
|
879
|
+
// Check if exit tool was fired during this turn
|
|
880
|
+
if (exitToolFired) {
|
|
881
|
+
exitReason = "tool_exit";
|
|
882
|
+
break;
|
|
863
883
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
if (
|
|
867
|
-
display.
|
|
884
|
+
// Safety net: never silently swallow an empty response
|
|
885
|
+
const lastMsg = messages[messages.length - 1];
|
|
886
|
+
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
887
|
+
display.warn("(empty response)");
|
|
888
|
+
}
|
|
889
|
+
// Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
|
|
890
|
+
if (options.onTurnEnd) {
|
|
891
|
+
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
868
892
|
}
|
|
869
893
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
process.stdout.write("\n"); // ensure response ends with newline before prompt (imperative only)
|
|
873
|
-
if (tuiStore)
|
|
874
|
-
tuiStore.restoreInput();
|
|
875
|
-
inputCtrl?.restore();
|
|
876
|
-
currentAbort = null;
|
|
877
|
-
// Check if exit tool was fired during this turn
|
|
878
|
-
if (exitToolFired) {
|
|
879
|
-
exitReason = "tool_exit";
|
|
880
|
-
break;
|
|
881
|
-
}
|
|
882
|
-
// Safety net: never silently swallow an empty response
|
|
883
|
-
const lastMsg = messages[messages.length - 1];
|
|
884
|
-
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
885
|
-
display.warn("(empty response)");
|
|
886
|
-
}
|
|
887
|
-
// Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
|
|
888
|
-
if (options.onTurnEnd) {
|
|
889
|
-
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
894
|
+
finally {
|
|
895
|
+
await options.onTurnFinish?.();
|
|
890
896
|
}
|
|
891
897
|
if (closed)
|
|
892
898
|
break;
|
|
@@ -963,149 +969,163 @@ async function main(agentName, options) {
|
|
|
963
969
|
sinks: agentConfig.logging?.sinks,
|
|
964
970
|
});
|
|
965
971
|
const sessPath = (0, config_1.sessionPath)(friendId, "cli", "session");
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
if (error instanceof session_lock_1.SessionLockError) {
|
|
973
|
-
process.stderr.write(`${error.message}\n`);
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
throw error;
|
|
977
|
-
/* v8 ignore stop */
|
|
978
|
-
}
|
|
979
|
-
// Load existing session or start fresh
|
|
980
|
-
const existing = (0, context_1.loadSession)(sessPath);
|
|
981
|
-
let sessionState = existing?.state;
|
|
982
|
-
let sessionEvents = existing?.events ?? [];
|
|
972
|
+
const acquireTurnLease = options?._acquireSessionTurnLease ?? session_transaction_1.acquireSessionTurnLease;
|
|
973
|
+
let sessionTurnLease = null;
|
|
974
|
+
let pendingPersists = [];
|
|
975
|
+
let existing = null;
|
|
976
|
+
let sessionState;
|
|
977
|
+
let sessionEvents = [];
|
|
983
978
|
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
984
|
-
const sessionMessages =
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
// Repair any orphaned tool calls from a crash mid-turn
|
|
988
|
-
(0, core_1.repairOrphanedToolCalls)(sessionMessages);
|
|
979
|
+
const sessionMessages = [
|
|
980
|
+
{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("cli", {}, resolvedContext)) },
|
|
981
|
+
];
|
|
989
982
|
// Per-turn pipeline input: CLI capabilities and pending dir
|
|
990
983
|
const cliCapabilities = (0, friends_1.getChannelCapabilities)("cli");
|
|
991
984
|
const currentAgentName = (0, identity_1.getAgentName)();
|
|
992
985
|
const pendingDir = (0, pending_1.getPendingDir)(currentAgentName, friendId, "cli", "session");
|
|
993
986
|
const summarize = (0, core_1.createSummarize)("human");
|
|
994
987
|
const cliFailoverState = { pending: null };
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
988
|
+
await runCliSession({
|
|
989
|
+
agentName: currentAgentName,
|
|
990
|
+
pasteDebounceMs,
|
|
991
|
+
messages: sessionMessages,
|
|
992
|
+
lastActivityAt: sessionState?.lastFriendActivityAt,
|
|
993
|
+
_testInputSource: options?._testInputSource,
|
|
994
|
+
onTurnStart: async (messages) => {
|
|
995
|
+
sessionTurnLease = await acquireTurnLease(sessPath);
|
|
996
|
+
try {
|
|
997
|
+
pendingPersists = [];
|
|
998
|
+
existing = (0, context_1.loadSession)(sessPath);
|
|
999
|
+
sessionState = existing?.state;
|
|
1000
|
+
sessionEvents = existing?.events ?? [];
|
|
1001
|
+
const loadedMessages = existing?.messages && existing.messages.length > 0
|
|
1002
|
+
? existing.messages
|
|
1003
|
+
: [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("cli", {}, resolvedContext)) }];
|
|
1004
|
+
(0, core_1.repairOrphanedToolCalls)(loadedMessages);
|
|
1005
|
+
messages.splice(0, messages.length, ...loadedMessages);
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
const lease = sessionTurnLease;
|
|
1009
|
+
sessionTurnLease = null;
|
|
1010
|
+
await lease.release();
|
|
1011
|
+
throw error;
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
onTurnFinish: async () => {
|
|
1015
|
+
try {
|
|
1016
|
+
await Promise.all(pendingPersists);
|
|
1017
|
+
}
|
|
1018
|
+
finally {
|
|
1019
|
+
const lease = sessionTurnLease;
|
|
1020
|
+
sessionTurnLease = null;
|
|
1021
|
+
await lease.release();
|
|
1022
|
+
}
|
|
1023
|
+
},
|
|
1024
|
+
onAsyncAssistantMessage: async (messages, _assistantMessage) => {
|
|
1025
|
+
const prepared = (0, context_1.postTurnTrim)(messages);
|
|
1026
|
+
const events = (0, context_1.postTurnPersist)(sessPath, prepared, undefined, sessionState, sessionTurnLease ?? undefined);
|
|
1027
|
+
/* v8 ignore next -- defensive: postTurnPersist always returns events in practice @preserve */
|
|
1028
|
+
sessionEvents = events.length > 0 ? events : sessionEvents;
|
|
1029
|
+
},
|
|
1030
|
+
runTurn: async (messages, userInput, callbacks, signal, toolContext, userContent) => {
|
|
1031
|
+
// Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
|
|
1032
|
+
// User message passed via input.messages so the pipeline can prepend pending messages to it.
|
|
1033
|
+
const failoverState = cliFailoverState;
|
|
1034
|
+
// Capture terminal errors instead of displaying immediately — the failover
|
|
1035
|
+
// message replaces the raw error if failover triggers successfully.
|
|
1036
|
+
let capturedTerminalError = null;
|
|
1037
|
+
/* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
|
|
1038
|
+
const failoverAwareCallbacks = {
|
|
1039
|
+
...callbacks,
|
|
1040
|
+
// Save session after each tool result for crash recovery (deferred to avoid blocking)
|
|
1041
|
+
onToolResult: (turnMessages) => {
|
|
1042
|
+
const prepared = (0, context_1.postTurnTrim)(turnMessages);
|
|
1043
|
+
pendingPersists.push((0, context_1.deferPostTurnPersist)(sessPath, prepared, undefined, sessionState, sessionTurnLease ?? undefined));
|
|
1044
|
+
},
|
|
1045
|
+
onError: (error, severity) => {
|
|
1046
|
+
if (severity === "terminal" && failoverState) {
|
|
1047
|
+
capturedTerminalError = error;
|
|
1048
|
+
callbacks.onError(new Error(""), "transient");
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
callbacks.onError(error, severity);
|
|
1052
|
+
},
|
|
1053
|
+
};
|
|
1054
|
+
/* v8 ignore stop */
|
|
1055
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
1056
|
+
channel: "cli",
|
|
1057
|
+
sessionKey: "session",
|
|
1058
|
+
capabilities: cliCapabilities,
|
|
1059
|
+
messages: [{ role: "user", content: userContent ?? userInput }],
|
|
1060
|
+
continuityIngressTexts: getCliContinuityIngressTexts(userInput),
|
|
1061
|
+
callbacks: failoverAwareCallbacks,
|
|
1062
|
+
sessionTurnLease: sessionTurnLease,
|
|
1063
|
+
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
1064
|
+
sessionLoader: {
|
|
1065
|
+
loadOrCreate: () => Promise.resolve({
|
|
1066
|
+
messages,
|
|
1067
|
+
sessionPath: sessPath,
|
|
1068
|
+
state: sessionState,
|
|
1069
|
+
events: sessionEvents,
|
|
1070
|
+
structuredOutputs: existing?.structuredOutputs,
|
|
1065
1071
|
}),
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1072
|
+
},
|
|
1073
|
+
pendingDir,
|
|
1074
|
+
friendStore,
|
|
1075
|
+
provider: "local",
|
|
1076
|
+
externalId: localExternalId,
|
|
1077
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
1078
|
+
drainPending: pending_1.drainPending,
|
|
1079
|
+
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(currentAgentName, deferredFriendId),
|
|
1080
|
+
runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
|
|
1081
|
+
...opts,
|
|
1082
|
+
toolContext: {
|
|
1083
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
1084
|
+
signin: async () => undefined,
|
|
1085
|
+
...opts?.toolContext,
|
|
1086
|
+
summarize,
|
|
1075
1087
|
},
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1088
|
+
}),
|
|
1089
|
+
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
1090
|
+
// Trim synchronously (mutates turnMessages for next turn),
|
|
1091
|
+
// then defer envelope build + disk I/O to avoid blocking the TUI.
|
|
1092
|
+
const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
|
|
1093
|
+
sessionState = state;
|
|
1094
|
+
const persist = (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state, sessionTurnLease ?? undefined).then((events) => {
|
|
1095
|
+
/* v8 ignore next -- defensive: deferPostTurnPersist always resolves events in practice @preserve */
|
|
1096
|
+
sessionEvents = events.length > 0 ? events : sessionEvents;
|
|
1097
|
+
});
|
|
1098
|
+
pendingPersists.push(persist);
|
|
1099
|
+
},
|
|
1100
|
+
accumulateFriendTokens: friends_1.accumulateFriendTokens,
|
|
1101
|
+
signal,
|
|
1102
|
+
runAgentOptions: {
|
|
1103
|
+
toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
|
|
1104
|
+
traceId: (0, nerves_1.createTraceId)(),
|
|
1105
|
+
mcpManager,
|
|
1106
|
+
toolContext,
|
|
1107
|
+
},
|
|
1108
|
+
failoverState,
|
|
1109
|
+
});
|
|
1110
|
+
/* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
|
|
1111
|
+
if (result.failoverMessage) {
|
|
1112
|
+
// Failover handled it — show the actionable message instead of the raw error
|
|
1113
|
+
process.stdout.write(`\x1b[33m${result.failoverMessage}\x1b[0m\n`);
|
|
1114
|
+
}
|
|
1115
|
+
else if (capturedTerminalError) {
|
|
1116
|
+
// Failover didn't trigger (no failoverState, or sequence failed) — show the raw error
|
|
1117
|
+
process.stderr.write(`\x1b[31m${(0, format_1.formatError)(capturedTerminalError)}\x1b[0m\n`);
|
|
1118
|
+
}
|
|
1119
|
+
/* v8 ignore stop */
|
|
1120
|
+
// Handle gate rejection: display auto-reply if present
|
|
1121
|
+
if (!result.gateResult.allowed) {
|
|
1122
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
1123
|
+
process.stdout.write(`${result.gateResult.autoReply}\n`);
|
|
1101
1124
|
}
|
|
1102
|
-
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
finally {
|
|
1107
|
-
sessionLock?.release();
|
|
1108
|
-
}
|
|
1125
|
+
}
|
|
1126
|
+
return { usage: result.usage, turnOutcome: result.turnOutcome, commandAction: result.commandAction };
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1109
1129
|
// Force exit: lingering handles (Ink cleanup timers, MCP connections) keep the
|
|
1110
1130
|
// event loop alive after the interactive session ends. This is safe because all
|
|
1111
1131
|
// session persistence has already completed in the finally block above.
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -690,6 +690,9 @@ async function handleInboundTurn(input) {
|
|
|
690
690
|
}
|
|
691
691
|
// Step 3: Load/create session
|
|
692
692
|
const session = await input.sessionLoader.loadOrCreate();
|
|
693
|
+
if (input.sessionTurnLease && path.resolve(session.sessionPath) !== input.sessionTurnLease.sessionPath) {
|
|
694
|
+
throw new Error(`leased session path mismatch: expected ${input.sessionTurnLease.sessionPath}, loaded ${path.resolve(session.sessionPath)}`);
|
|
695
|
+
}
|
|
693
696
|
const sessionMessages = session.messages;
|
|
694
697
|
const sessionEvents = session.events ?? [];
|
|
695
698
|
let mustResolveBeforeHandoff = (0, continuity_1.resolveMustResolveBeforeHandoff)(session.state?.mustResolveBeforeHandoff === true, input.continuityIngressTexts);
|
|
@@ -1001,6 +1004,26 @@ async function handleInboundTurn(input) {
|
|
|
1001
1004
|
...(result.error ? { errorName: result.error.name } : {}),
|
|
1002
1005
|
...(result.errorClassification ? { errorCode: result.errorClassification } : {}),
|
|
1003
1006
|
}));
|
|
1007
|
+
if (result.outcome === "suspended") {
|
|
1008
|
+
if (!result.suspension)
|
|
1009
|
+
throw new Error("suspended agent turn omitted durable approval suspension");
|
|
1010
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1011
|
+
component: "senses",
|
|
1012
|
+
event: "senses.pipeline_approval_suspended",
|
|
1013
|
+
message: "pipeline returned a durably checkpointed approval suspension without duplicate post-turn persistence",
|
|
1014
|
+
meta: { approvalId: result.suspension.approvalId, sessionPath: session.sessionPath },
|
|
1015
|
+
});
|
|
1016
|
+
return {
|
|
1017
|
+
resolvedContext,
|
|
1018
|
+
gateResult,
|
|
1019
|
+
usage: result.usage,
|
|
1020
|
+
turnOutcome: result.outcome,
|
|
1021
|
+
suspension: result.suspension,
|
|
1022
|
+
sessionPath: session.sessionPath,
|
|
1023
|
+
messages: sessionMessages,
|
|
1024
|
+
drainedPending: pending,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1004
1027
|
// Step 5b: Failover on terminal error
|
|
1005
1028
|
if (result.outcome === "errored" && input.failoverState) {
|
|
1006
1029
|
try {
|