@ouro.bot/cli 0.1.0-alpha.804 → 0.1.0-alpha.805

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.
@@ -60,11 +60,16 @@ const pipeline_1 = require("./pipeline");
60
60
  const mcp_manager_1 = require("../repertoire/mcp-manager");
61
61
  const runtime_1 = require("../nerves/runtime");
62
62
  const session_transaction_1 = require("../mind/session-transaction");
63
+ const turn_execution_lease_1 = require("../heart/turn-execution-lease");
63
64
  const RESPONSE_CAP = 50_000;
64
65
  const OUTWARD_DELIVERY_TOOL_ACKS = new Map([
65
66
  ["settle", "(delivered)"],
66
67
  ["speak", "(spoken)"],
67
68
  ]);
69
+ async function releaseRuntimeMcpServersAfterTurn() {
70
+ const manager = await Promise.resolve().then(() => __importStar(require("../repertoire/mcp-manager")));
71
+ await manager.releaseRuntimeMcpServers();
72
+ }
68
73
  /**
69
74
  * Strip MiniMax-style `<think>...</think>` reasoning blocks from a response
70
75
  * string. Handles unclosed open tags (treats everything from `<think>` to
@@ -315,6 +320,19 @@ function getSenseSessionPath(agentName, friendId, channel, sessionKey, agentRoot
315
320
  * this function handles all pipeline wiring.
316
321
  */
317
322
  async function runSenseTurn(options) {
323
+ return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
324
+ (0, identity_1.setAgentName)(options.agentName);
325
+ try {
326
+ return await runSenseTurnExclusive(options);
327
+ }
328
+ finally {
329
+ if (options.runtimeMcpServers && !options.disableTools) {
330
+ await releaseRuntimeMcpServersAfterTurn();
331
+ }
332
+ }
333
+ });
334
+ }
335
+ async function runSenseTurnExclusive(options) {
318
336
  const { agentName, channel, sessionKey, friendId, userMessage } = options;
319
337
  (0, runtime_1.emitNervesEvent)({
320
338
  component: "senses",
@@ -364,279 +382,373 @@ async function runSenseTurn(options) {
364
382
  const resolver = new friends_1.FriendResolver(friendStore, resolverParams);
365
383
  // Initialize MCP manager so MCP tools appear as first-class tools in the agent's tool list.
366
384
  // Runtime MCP servers (e.g. Workbench's ouro_workbench) are passed per-turn for THIS agent only.
367
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)(options.runtimeMcpServers ? { runtimeServers: options.runtimeMcpServers } : undefined) ?? undefined;
385
+ const mcpManager = options.disableTools
386
+ ? undefined
387
+ : await (0, mcp_manager_1.getSharedMcpManager)(options.runtimeMcpServers ? { runtimeServers: options.runtimeMcpServers } : undefined) ?? undefined;
368
388
  // Session path and loading
369
- const sessionDir = path.join(agentRoot, "state", "sessions", friendId, channel);
389
+ const ephemeralRoot = options.disablePersistence
390
+ ? fs.mkdtempSync(path.join(os.tmpdir(), "ouro-observe-only-"))
391
+ : null;
392
+ const sessionDir = ephemeralRoot ?? path.join(agentRoot, "state", "sessions", friendId, channel);
370
393
  fs.mkdirSync(sessionDir, { recursive: true });
371
- const sessPath = getSenseSessionPath(agentName, friendId, channel, sessionKey, agentRoot);
394
+ const sessPath = ephemeralRoot
395
+ ? path.join(ephemeralRoot, "session.json")
396
+ : getSenseSessionPath(agentName, friendId, channel, sessionKey, agentRoot);
397
+ const reportedSessionPath = ephemeralRoot ? undefined : sessPath;
372
398
  const runWithLease = options._withSessionTurnLease ?? session_transaction_1.withSessionTurnLease;
373
- return runWithLease(sessPath, async (sessionTurnLease) => {
374
- const baseSessionRevision = (0, session_transaction_1.readSessionTransaction)(sessPath, sessionTurnLease).revision;
375
- const existing = (0, context_1.loadSession)(sessPath);
376
- const precommittedIngressEvent = options.precommittedIngress
377
- ? existing?.events?.find((candidate) => candidate.id === options.precommittedIngress.eventId)
378
- : undefined;
379
- if (options.precommittedIngress) {
380
- const latestUserEvent = existing?.events?.filter((candidate) => candidate.role === "user").at(-1);
381
- if (!precommittedIngressEvent || precommittedIngressEvent !== latestUserEvent || precommittedIngressEvent.role !== "user" || precommittedIngressEvent.content !== userMessage
382
- || !precommittedIngressEvent.relations.references.includes(options.precommittedIngress.reference)) {
383
- throw new Error("shared turn precommitted ingress is missing, mismatched, or no longer current");
399
+ try {
400
+ return await runWithLease(sessPath, async (sessionTurnLease) => {
401
+ const baseSessionRevision = (0, session_transaction_1.readSessionTransaction)(sessPath, sessionTurnLease).revision;
402
+ const existing = options.disablePersistence ? undefined : (0, context_1.loadSession)(sessPath);
403
+ const precommittedIngressEvent = options.precommittedIngress
404
+ ? existing?.events?.find((candidate) => candidate.id === options.precommittedIngress.eventId)
405
+ : undefined;
406
+ if (options.precommittedIngress) {
407
+ const latestUserEvent = existing?.events?.filter((candidate) => candidate.role === "user").at(-1);
408
+ if (!precommittedIngressEvent || precommittedIngressEvent !== latestUserEvent || precommittedIngressEvent.role !== "user" || precommittedIngressEvent.content !== userMessage
409
+ || !precommittedIngressEvent.relations.references.includes(options.precommittedIngress.reference)) {
410
+ throw new Error("shared turn precommitted ingress is missing, mismatched, or no longer current");
411
+ }
384
412
  }
385
- }
386
- const existingEventIds = new Set(existing?.events?.map((event) => event.id) ?? []);
387
- let sessionState = existing?.state;
388
- let persistPromise;
389
- const sessionMessages = existing?.messages && existing.messages.length > 0
390
- ? existing.messages
391
- : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)(channel, {}, undefined)) }];
392
- if (precommittedIngressEvent) {
393
- const projectedIngress = exactProjectedIngressMessage(existing, sessionMessages, precommittedIngressEvent.id);
394
- if (!projectedIngress || projectedIngress.role !== "user" || projectedIngress.content !== userMessage)
395
- throw new Error("shared turn precommitted ingress is absent from the provider projection");
396
- (0, session_events_1.stampIngressRelations)(projectedIngress, {
397
- replyToEventId: precommittedIngressEvent.relations.replyToEventId,
398
- threadRootEventId: precommittedIngressEvent.relations.threadRootEventId,
399
- references: precommittedIngressEvent.relations.references,
413
+ const existingEventIds = new Set(existing?.events?.map((event) => event.id) ?? []);
414
+ const existingStructuredOutputIds = new Set(existing?.structuredOutputs?.map((output) => output.id) ?? []);
415
+ let sessionState = existing?.state;
416
+ let persistPromise;
417
+ const sessionMessages = existing?.messages && existing.messages.length > 0
418
+ ? existing.messages
419
+ : [{
420
+ role: "system",
421
+ content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)(channel, options.disableTools ? { tools: [], hardDisableTools: true } : {}, undefined)),
422
+ }];
423
+ if (precommittedIngressEvent) {
424
+ const projectedIngress = exactProjectedIngressMessage(existing, sessionMessages, precommittedIngressEvent.id);
425
+ if (!projectedIngress || projectedIngress.role !== "user" || projectedIngress.content !== userMessage)
426
+ throw new Error("shared turn precommitted ingress is absent from the provider projection");
427
+ (0, session_events_1.stampIngressRelations)(projectedIngress, {
428
+ replyToEventId: precommittedIngressEvent.relations.replyToEventId,
429
+ threadRootEventId: precommittedIngressEvent.relations.threadRootEventId,
430
+ references: precommittedIngressEvent.relations.references,
431
+ });
432
+ }
433
+ // Pending dir
434
+ const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
435
+ // Accumulate outward text through the same callback boundary used by chat
436
+ // channels. `speak` flushes pending text immediately; `settle` is delivered
437
+ // once the turn completes.
438
+ let committedResponseText = "";
439
+ let pendingResponseText = "";
440
+ let terminalDeliveryKind = "text";
441
+ const deliveries = [];
442
+ const deliveryFailures = [];
443
+ const deliveryAttempts = [];
444
+ let providerInvocationCount = 0;
445
+ let toolInvocationCount = 0;
446
+ let hadReasoningChunk = false;
447
+ const commitResponseText = (text) => {
448
+ const cleaned = stripThinkBlocks(text);
449
+ /* v8 ignore next -- deliverPending strips first; this is a defensive direct-call guard @preserve */
450
+ if (!cleaned)
451
+ return;
452
+ committedResponseText = committedResponseText
453
+ ? `${committedResponseText}\n${cleaned}`
454
+ : cleaned;
455
+ };
456
+ const deliveryErrorMessage = (error) => error instanceof Error ? error.message : String(error);
457
+ const emitFrontendEvent = (event) => {
458
+ options.frontendEventSink?.onEvent(event);
459
+ };
460
+ const deliverPending = async (kind, optionsForDelivery) => {
461
+ const text = stripThinkBlocks(pendingResponseText);
462
+ pendingResponseText = "";
463
+ if (!text)
464
+ return undefined;
465
+ const delivery = { kind, text };
466
+ const attempt = { kind, text, delivered: false };
467
+ const attemptIndex = deliveryAttempts.length;
468
+ deliveryAttempts.push(attempt);
469
+ try {
470
+ await options.deliverySink?.onDelivery(delivery);
471
+ attempt.delivered = true;
472
+ deliveries.push(delivery);
473
+ commitResponseText(text);
474
+ emitFrontendEvent({ type: "assistant_delivery", data: { kind, text } });
475
+ }
476
+ catch (error) {
477
+ const failure = { ...delivery, error: deliveryErrorMessage(error) };
478
+ deliveryFailures.push(failure);
479
+ (0, runtime_1.emitNervesEvent)({
480
+ level: "error",
481
+ component: "senses",
482
+ event: "senses.shared_turn_delivery_error",
483
+ message: "shared turn outward delivery failed",
484
+ meta: { agentName, channel, sessionKey, friendId, kind, error: failure.error, textLength: text.length },
485
+ });
486
+ if (optionsForDelivery.throwOnError)
487
+ throw error;
488
+ commitResponseText(text);
489
+ }
490
+ return attemptIndex;
491
+ };
492
+ /* v8 ignore start — callback stubs are exercised through the pipeline integration */
493
+ const callbacks = {
494
+ settleOutputMode: "retractable_buffer",
495
+ onModelStart: () => {
496
+ providerInvocationCount += 1;
497
+ if (options.turnMetricsObserver)
498
+ options.turnMetricsObserver.providerInvocationCount += 1;
499
+ emitFrontendEvent({ type: "model_started", data: {} });
500
+ },
501
+ onModelStreamStart: () => emitFrontendEvent({ type: "model_stream_started", data: {} }),
502
+ onTextChunk: (chunk) => {
503
+ pendingResponseText += chunk;
504
+ emitFrontendEvent({ type: "text_delta", data: { text: chunk } });
505
+ },
506
+ onReasoningChunk: (chunk) => {
507
+ hadReasoningChunk = true;
508
+ emitFrontendEvent({ type: "reasoning_delta", data: { text: chunk } });
509
+ },
510
+ onToolStart: (name, args) => {
511
+ toolInvocationCount += 1;
512
+ if (options.turnMetricsObserver)
513
+ options.turnMetricsObserver.toolInvocationCount += 1;
514
+ emitFrontendEvent({ type: "tool_started", data: { name, args } });
515
+ },
516
+ onToolEnd: (name, _summary, success) => {
517
+ if (name === "settle" && success)
518
+ terminalDeliveryKind = "settle";
519
+ emitFrontendEvent({ type: "tool_completed", data: { name, summary: _summary, success } });
520
+ },
521
+ onError: (error, severity) => {
522
+ emitFrontendEvent({ type: "error", data: { message: error.message, severity } });
523
+ },
524
+ onClearText: () => {
525
+ pendingResponseText = "";
526
+ emitFrontendEvent({ type: "text_cleared", data: {} });
527
+ },
528
+ flushNow: async () => { await deliverPending("speak", { throwOnError: true }); },
529
+ };
530
+ /* v8 ignore stop */
531
+ // Run the pipeline
532
+ const inboundMessages = [];
533
+ if (!options.precommittedIngress) {
534
+ const userMsg = { role: "user", content: userMessage };
535
+ (0, session_events_1.stampIngressTime)(userMsg);
536
+ if (options.ingressRelations)
537
+ (0, session_events_1.stampIngressRelations)(userMsg, options.ingressRelations);
538
+ inboundMessages.push(userMsg);
539
+ }
540
+ const turnResult = await (0, pipeline_1.handleInboundTurn)({
541
+ channel,
542
+ latencyMode: options.latencyMode,
543
+ sessionKey,
544
+ capabilities,
545
+ messages: inboundMessages,
546
+ callbacks,
547
+ sessionTurnLease,
548
+ /* v8 ignore start — delegation wrappers; pipeline integration tested separately */
549
+ friendResolver: { resolve: () => resolver.resolve() },
550
+ sessionLoader: {
551
+ loadOrCreate: () => Promise.resolve({
552
+ messages: sessionMessages,
553
+ sessionPath: sessPath,
554
+ state: sessionState,
555
+ events: existing?.events,
556
+ structuredOutputs: existing?.structuredOutputs,
557
+ }),
558
+ },
559
+ /* v8 ignore stop */
560
+ pendingDir,
561
+ friendStore,
562
+ signal: options.signal,
563
+ provider: resolverParams.provider,
564
+ externalId: resolverParams.externalId,
565
+ tenantId: resolverParams.tenantId,
566
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
567
+ drainPending: options.disablePersistence ? () => [] : pending_1.drainPending,
568
+ runAgentOptions: {
569
+ mcpManager,
570
+ ...(options.disableTools ? { tools: [], hardDisableTools: true } : {}),
571
+ ...(options.approvalCoordinatorFactory && !options.disablePersistence ? { approvalCoordinator: options.approvalCoordinatorFactory({ sessionPath: sessPath, baseSessionRevision }) } : {}),
572
+ ...(options.latencyMode === "live" ? { skipKeptNotes: true } : {}),
573
+ ...(options.orientationFrame ? { orientationFrame: options.orientationFrame } : {}),
574
+ toolContext: {
575
+ signin: async () => undefined,
576
+ ...(options.toolContext ? options.toolContext : {}),
577
+ currentUserMessage: userMessage,
578
+ },
579
+ },
580
+ ...(options.prepareRunAgentOptions ? { prepareRunAgentOptions: options.prepareRunAgentOptions } : {}),
581
+ /* v8 ignore start — delegation wrappers; these just forward to the real functions */
582
+ runAgent: (msgs, cb, ch, sig, opts) => (0, core_1.runAgent)(msgs, cb, ch, sig, opts),
583
+ postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
584
+ const prepared = (0, context_2.postTurnTrim)(turnMessages, usage, hooks);
585
+ sessionState = state;
586
+ if (options.disablePersistence)
587
+ return;
588
+ persistPromise = (0, context_2.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
589
+ },
590
+ /* v8 ignore stop */
591
+ accumulateFriendTokens: options.disablePersistence ? async () => undefined : friends_1.accumulateFriendTokens,
400
592
  });
401
- }
402
- // Pending dir
403
- const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
404
- // Accumulate outward text through the same callback boundary used by chat
405
- // channels. `speak` flushes pending text immediately; `settle` is delivered
406
- // once the turn completes.
407
- let committedResponseText = "";
408
- let pendingResponseText = "";
409
- let terminalDeliveryKind = "text";
410
- const deliveries = [];
411
- const deliveryFailures = [];
412
- const deliveryAttempts = [];
413
- let providerInvocationCount = 0;
414
- let toolInvocationCount = 0;
415
- let hadReasoningChunk = false;
416
- const commitResponseText = (text) => {
417
- const cleaned = stripThinkBlocks(text);
418
- /* v8 ignore next -- deliverPending strips first; this is a defensive direct-call guard @preserve */
419
- if (!cleaned)
420
- return;
421
- committedResponseText = committedResponseText
422
- ? `${committedResponseText}\n${cleaned}`
423
- : cleaned;
424
- };
425
- const deliveryErrorMessage = (error) => error instanceof Error ? error.message : String(error);
426
- const deliverPending = async (kind, optionsForDelivery) => {
427
- const text = stripThinkBlocks(pendingResponseText);
428
- pendingResponseText = "";
429
- if (!text)
430
- return undefined;
431
- const delivery = { kind, text };
432
- const attempt = { kind, text, delivered: false };
433
- const attemptIndex = deliveryAttempts.length;
434
- deliveryAttempts.push(attempt);
435
- try {
436
- await options.deliverySink?.onDelivery(delivery);
437
- attempt.delivered = true;
438
- deliveries.push(delivery);
439
- commitResponseText(text);
593
+ if (turnResult.gateResult && !turnResult.gateResult.allowed) {
594
+ const blockedResponse = "autoReply" in turnResult.gateResult
595
+ ? turnResult.gateResult.autoReply
596
+ : `(blocked by trust gate: ${turnResult.gateResult.reason})`;
597
+ return {
598
+ response: blockedResponse,
599
+ ponderDeferred: false,
600
+ deliveries,
601
+ deliveryFailures,
602
+ providerInvocationCount,
603
+ toolInvocationCount,
604
+ sessionPath: reportedSessionPath,
605
+ turnOutcome: turnResult.turnOutcome ?? "blocked",
606
+ };
607
+ }
608
+ if (turnResult.turnOutcome === "aborted") {
609
+ pendingResponseText = "";
610
+ if (persistPromise)
611
+ await persistPromise;
612
+ return {
613
+ response: "",
614
+ ponderDeferred: false,
615
+ deliveries,
616
+ deliveryFailures,
617
+ providerInvocationCount,
618
+ toolInvocationCount,
619
+ sessionPath: reportedSessionPath,
620
+ turnOutcome: "aborted",
621
+ };
622
+ }
623
+ if (turnResult.turnOutcome === "suspended") {
624
+ if (!turnResult.suspension)
625
+ throw new Error("suspended shared turn omitted durable approval suspension");
626
+ pendingResponseText = "";
627
+ return {
628
+ response: "",
629
+ ponderDeferred: false,
630
+ deliveries,
631
+ deliveryFailures,
632
+ providerInvocationCount,
633
+ toolInvocationCount,
634
+ sessionPath: reportedSessionPath,
635
+ turnOutcome: "suspended",
636
+ suspension: turnResult.suspension,
637
+ };
638
+ }
639
+ const persistedEvents = persistPromise ? await persistPromise : [];
640
+ const terminalEvents = persistedEvents.length > 0
641
+ ? persistedEvents
642
+ : rawSessionEvents((0, session_transaction_1.readSessionTransaction)(sessPath, sessionTurnLease).value);
643
+ const eventView = currentTurnEventView(terminalEvents, existingEventIds, userMessage, options.precommittedIngress, options.ingressRelations);
644
+ let finalDeliveryKind = terminalDeliveryKind;
645
+ const acceptedTerminalOutcome = turnResult.turnOutcome === "settled" || turnResult.turnOutcome === "blocked";
646
+ const failoverText = turnResult.turnOutcome === "errored" ? turnResult.failoverMessage?.trim() : undefined;
647
+ const expectsOutwardResponse = acceptedTerminalOutcome || turnResult.turnOutcome === "command" || Boolean(failoverText);
648
+ let finalCausalCoordinate;
649
+ if (acceptedTerminalOutcome) {
650
+ const completionText = stripThinkBlocks(turnResult.completion?.answer ?? "");
651
+ let authoritativeText = completionText;
652
+ if (!authoritativeText && eventView?.terminal) {
653
+ authoritativeText = eventView.terminal.text;
654
+ finalDeliveryKind = eventView.terminal.kind;
655
+ }
656
+ if (eventView?.terminal?.kind === finalDeliveryKind && eventView.terminal.text === authoritativeText)
657
+ finalCausalCoordinate = eventView.terminal;
658
+ const terminalAlreadyDelivered = finalCausalCoordinate
659
+ ? alignedDeliveryCoordinates(eventView, deliveryAttempts).includes(finalCausalCoordinate)
660
+ : false;
661
+ pendingResponseText = authoritativeText && !terminalAlreadyDelivered ? authoritativeText : "";
662
+ }
663
+ else if (turnResult.turnOutcome === "command") {
664
+ // Slash-command text is emitted directly by the pipeline and has no assistant event.
665
+ }
666
+ else if (failoverText) {
667
+ pendingResponseText = failoverText;
668
+ }
669
+ else {
670
+ pendingResponseText = "";
671
+ }
672
+ if (persistPromise && options.frontendEventSink && !options.disablePersistence) {
673
+ const currentStructuredOutputs = (0, context_1.loadSession)(sessPath)?.structuredOutputs ?? [];
674
+ for (const output of currentStructuredOutputs) {
675
+ if (!existingStructuredOutputIds.has(output.id)) {
676
+ emitFrontendEvent({ type: "structured_output", data: { output } });
677
+ }
678
+ }
440
679
  }
441
- catch (error) {
442
- const failure = { ...delivery, error: deliveryErrorMessage(error) };
443
- deliveryFailures.push(failure);
680
+ const finalDeliveryAttemptIndex = await deliverPending(finalDeliveryKind, { throwOnError: false });
681
+ const ponderDeferred = false;
682
+ // Build response
683
+ let finalResponse;
684
+ const finalDeliveryAttempt = finalDeliveryAttemptIndex === undefined ? undefined : deliveryAttempts[finalDeliveryAttemptIndex];
685
+ const responseDeliveryFailure = finalDeliveryAttempt && !finalDeliveryAttempt.delivered ? deliveryFailures.at(-1) : undefined;
686
+ const responseCausalSessionEventId = finalDeliveryAttempt && !finalDeliveryAttempt.delivered ? finalCausalCoordinate?.eventId : undefined;
687
+ if (committedResponseText.length === 0) {
688
+ if (!expectsOutwardResponse) {
689
+ finalResponse = "";
690
+ }
691
+ else {
692
+ const emptyFallback = options.emptyResponseFallback?.();
693
+ finalResponse = emptyFallback ?? (hadReasoningChunk ? "" : "(agent responded but response was empty)");
694
+ }
695
+ }
696
+ else {
697
+ finalResponse = committedResponseText;
698
+ }
699
+ // Strip MiniMax-style <think>...</think> blocks from the final response.
700
+ // When a reasoning-style model emits only a think block and no final answer
701
+ // (no settle tool call, no post-think text), the readback path above
702
+ // surfaces the raw saved assistant content — which includes the think tags
703
+ // and renders as empty (or as raw reasoning) on MCP/CLI clients. Strip
704
+ // here so the caller sees the actual delivered text. If only reasoning
705
+ // came through and nothing else, surface a clear diagnostic message
706
+ // instead of a blank response so the operator knows what happened.
707
+ finalResponse = stripThinkBlocks(finalResponse);
708
+ if (finalResponse.length === 0 && expectsOutwardResponse) {
444
709
  (0, runtime_1.emitNervesEvent)({
445
- level: "error",
710
+ level: "warn",
446
711
  component: "senses",
447
- event: "senses.shared_turn_delivery_error",
448
- message: "shared turn outward delivery failed",
449
- meta: { agentName, channel, sessionKey, friendId, kind, error: failure.error, textLength: text.length },
712
+ event: "senses.shared_turn_only_reasoning",
713
+ message: "agent produced only <think> reasoning with no final answer — likely a model that closed the think tag without continuing",
714
+ meta: { agentName, channel, sessionKey, friendId },
450
715
  });
451
- if (optionsForDelivery.throwOnError)
452
- throw error;
453
- commitResponseText(text);
716
+ finalResponse = "(agent produced reasoning but no final answer this turn — try again, or check the session transcript for the trace)";
454
717
  }
455
- return attemptIndex;
456
- };
457
- /* v8 ignore start — callback stubs are exercised through the pipeline integration */
458
- const callbacks = {
459
- settleOutputMode: "retractable_buffer",
460
- onModelStart: () => { providerInvocationCount += 1; if (options.turnMetricsObserver)
461
- options.turnMetricsObserver.providerInvocationCount += 1; },
462
- onModelStreamStart: () => { },
463
- onTextChunk: (chunk) => { pendingResponseText += chunk; },
464
- onReasoningChunk: () => { hadReasoningChunk = true; },
465
- onToolStart: () => { toolInvocationCount += 1; if (options.turnMetricsObserver)
466
- options.turnMetricsObserver.toolInvocationCount += 1; },
467
- onToolEnd: (name, _summary, success) => {
468
- if (name === "settle" && success)
469
- terminalDeliveryKind = "settle";
470
- },
471
- onError: () => { },
472
- onClearText: () => { pendingResponseText = ""; },
473
- flushNow: async () => { await deliverPending("speak", { throwOnError: true }); },
474
- };
475
- /* v8 ignore stop */
476
- // Run the pipeline
477
- const inboundMessages = [];
478
- if (!options.precommittedIngress) {
479
- const userMsg = { role: "user", content: userMessage };
480
- (0, session_events_1.stampIngressTime)(userMsg);
481
- if (options.ingressRelations)
482
- (0, session_events_1.stampIngressRelations)(userMsg, options.ingressRelations);
483
- inboundMessages.push(userMsg);
484
- }
485
- const turnResult = await (0, pipeline_1.handleInboundTurn)({
486
- channel,
487
- latencyMode: options.latencyMode,
488
- sessionKey,
489
- capabilities,
490
- messages: inboundMessages,
491
- callbacks,
492
- sessionTurnLease,
493
- /* v8 ignore start — delegation wrappers; pipeline integration tested separately */
494
- friendResolver: { resolve: () => resolver.resolve() },
495
- sessionLoader: {
496
- loadOrCreate: () => Promise.resolve({
497
- messages: sessionMessages,
498
- sessionPath: sessPath,
499
- state: sessionState,
500
- events: existing?.events,
501
- structuredOutputs: existing?.structuredOutputs,
502
- }),
503
- },
504
- /* v8 ignore stop */
505
- pendingDir,
506
- friendStore,
507
- provider: resolverParams.provider,
508
- externalId: resolverParams.externalId,
509
- tenantId: resolverParams.tenantId,
510
- enforceTrustGate: trust_gate_1.enforceTrustGate,
511
- drainPending: pending_1.drainPending,
512
- runAgentOptions: {
513
- mcpManager,
514
- ...(options.approvalCoordinatorFactory ? { approvalCoordinator: options.approvalCoordinatorFactory({ sessionPath: sessPath, baseSessionRevision }) } : {}),
515
- ...(options.latencyMode === "live" ? { skipKeptNotes: true } : {}),
516
- ...(options.orientationFrame ? { orientationFrame: options.orientationFrame } : {}),
517
- toolContext: {
518
- signin: async () => undefined,
519
- ...(options.toolContext ? options.toolContext : {}),
520
- currentUserMessage: userMessage,
521
- },
522
- },
523
- ...(options.prepareRunAgentOptions ? { prepareRunAgentOptions: options.prepareRunAgentOptions } : {}),
524
- /* v8 ignore start — delegation wrappers; these just forward to the real functions */
525
- runAgent: (msgs, cb, ch, sig, opts) => (0, core_1.runAgent)(msgs, cb, ch, sig, opts),
526
- postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
527
- const prepared = (0, context_2.postTurnTrim)(turnMessages, usage, hooks);
528
- sessionState = state;
529
- persistPromise = (0, context_2.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
530
- },
531
- /* v8 ignore stop */
532
- accumulateFriendTokens: friends_1.accumulateFriendTokens,
533
- });
534
- if (turnResult.gateResult && !turnResult.gateResult.allowed) {
535
- const blockedResponse = "autoReply" in turnResult.gateResult
536
- ? turnResult.gateResult.autoReply
537
- : `(blocked by trust gate: ${turnResult.gateResult.reason})`;
718
+ // Cap response length
719
+ if (finalResponse.length > RESPONSE_CAP) {
720
+ finalResponse = finalResponse.slice(0, RESPONSE_CAP) + "\n\n[truncated — response exceeded 50K characters]";
721
+ }
722
+ (0, runtime_1.emitNervesEvent)({
723
+ component: "senses",
724
+ event: "senses.shared_turn_end",
725
+ message: "shared turn runner complete",
726
+ meta: { agentName, channel, sessionKey, friendId, ponderDeferred, responseLength: finalResponse.length },
727
+ });
538
728
  return {
539
- response: blockedResponse,
540
- ponderDeferred: false,
729
+ response: finalResponse,
730
+ ponderDeferred,
541
731
  deliveries,
542
732
  deliveryFailures,
733
+ ...(responseDeliveryFailure ? { responseDeliveryFailure } : {}),
543
734
  providerInvocationCount,
544
735
  toolInvocationCount,
545
- sessionPath: sessPath,
736
+ sessionPath: reportedSessionPath,
737
+ turnOutcome: turnResult.turnOutcome,
738
+ ...(deliveries.length > 0 ? { causalSessionEventIds: causalSessionEventIds(eventView, deliveryAttempts, finalDeliveryAttemptIndex, finalCausalCoordinate) } : {}),
739
+ ...(responseCausalSessionEventId ? { responseCausalSessionEventId } : {}),
546
740
  };
547
- }
548
- const persistedEvents = persistPromise ? await persistPromise : [];
549
- const terminalEvents = persistedEvents.length > 0
550
- ? persistedEvents
551
- : rawSessionEvents((0, session_transaction_1.readSessionTransaction)(sessPath, sessionTurnLease).value);
552
- const eventView = currentTurnEventView(terminalEvents, existingEventIds, userMessage, options.precommittedIngress, options.ingressRelations);
553
- let finalDeliveryKind = terminalDeliveryKind;
554
- const acceptedTerminalOutcome = turnResult.turnOutcome === "settled" || turnResult.turnOutcome === "blocked";
555
- const failoverText = turnResult.turnOutcome === "errored" ? turnResult.failoverMessage?.trim() : undefined;
556
- const expectsOutwardResponse = acceptedTerminalOutcome || turnResult.turnOutcome === "command" || Boolean(failoverText);
557
- let finalCausalCoordinate;
558
- if (acceptedTerminalOutcome) {
559
- const completionText = stripThinkBlocks(turnResult.completion?.answer ?? "");
560
- let authoritativeText = completionText;
561
- if (!authoritativeText && eventView?.terminal) {
562
- authoritativeText = eventView.terminal.text;
563
- finalDeliveryKind = eventView.terminal.kind;
564
- }
565
- if (eventView?.terminal?.kind === finalDeliveryKind && eventView.terminal.text === authoritativeText)
566
- finalCausalCoordinate = eventView.terminal;
567
- const terminalAlreadyDelivered = finalCausalCoordinate
568
- ? alignedDeliveryCoordinates(eventView, deliveryAttempts).includes(finalCausalCoordinate)
569
- : false;
570
- pendingResponseText = authoritativeText && !terminalAlreadyDelivered ? authoritativeText : "";
571
- }
572
- else if (turnResult.turnOutcome === "command") {
573
- // Slash-command text is emitted directly by the pipeline and has no assistant event.
574
- }
575
- else if (failoverText) {
576
- pendingResponseText = failoverText;
577
- }
578
- else {
579
- pendingResponseText = "";
580
- }
581
- const finalDeliveryAttemptIndex = await deliverPending(finalDeliveryKind, { throwOnError: false });
582
- const ponderDeferred = false;
583
- // Build response
584
- let finalResponse;
585
- const finalDeliveryAttempt = finalDeliveryAttemptIndex === undefined ? undefined : deliveryAttempts[finalDeliveryAttemptIndex];
586
- const responseDeliveryFailure = finalDeliveryAttempt && !finalDeliveryAttempt.delivered ? deliveryFailures.at(-1) : undefined;
587
- const responseCausalSessionEventId = finalDeliveryAttempt && !finalDeliveryAttempt.delivered ? finalCausalCoordinate?.eventId : undefined;
588
- if (committedResponseText.length === 0) {
589
- if (!expectsOutwardResponse) {
590
- finalResponse = "";
591
- }
592
- else {
593
- const emptyFallback = options.emptyResponseFallback?.();
594
- finalResponse = emptyFallback ?? (hadReasoningChunk ? "" : "(agent responded but response was empty)");
741
+ });
742
+ }
743
+ finally {
744
+ if (ephemeralRoot) {
745
+ for (const entry of fs.readdirSync(ephemeralRoot, { withFileTypes: true })) {
746
+ if (!entry.isFile() && !entry.isSymbolicLink()) {
747
+ throw new Error(`observe-only session cleanup found unexpected entry: ${entry.name}`);
748
+ }
749
+ fs.unlinkSync(path.join(ephemeralRoot, entry.name));
595
750
  }
751
+ fs.rmdirSync(ephemeralRoot);
596
752
  }
597
- else {
598
- finalResponse = committedResponseText;
599
- }
600
- // Strip MiniMax-style <think>...</think> blocks from the final response.
601
- // When a reasoning-style model emits only a think block and no final answer
602
- // (no settle tool call, no post-think text), the readback path above
603
- // surfaces the raw saved assistant content — which includes the think tags
604
- // and renders as empty (or as raw reasoning) on MCP/CLI clients. Strip
605
- // here so the caller sees the actual delivered text. If only reasoning
606
- // came through and nothing else, surface a clear diagnostic message
607
- // instead of a blank response so the operator knows what happened.
608
- finalResponse = stripThinkBlocks(finalResponse);
609
- if (finalResponse.length === 0 && expectsOutwardResponse) {
610
- (0, runtime_1.emitNervesEvent)({
611
- level: "warn",
612
- component: "senses",
613
- event: "senses.shared_turn_only_reasoning",
614
- message: "agent produced only <think> reasoning with no final answer — likely a model that closed the think tag without continuing",
615
- meta: { agentName, channel, sessionKey, friendId },
616
- });
617
- finalResponse = "(agent produced reasoning but no final answer this turn — try again, or check the session transcript for the trace)";
618
- }
619
- // Cap response length
620
- if (finalResponse.length > RESPONSE_CAP) {
621
- finalResponse = finalResponse.slice(0, RESPONSE_CAP) + "\n\n[truncated — response exceeded 50K characters]";
622
- }
623
- (0, runtime_1.emitNervesEvent)({
624
- component: "senses",
625
- event: "senses.shared_turn_end",
626
- message: "shared turn runner complete",
627
- meta: { agentName, channel, sessionKey, friendId, ponderDeferred, responseLength: finalResponse.length },
628
- });
629
- return {
630
- response: finalResponse,
631
- ponderDeferred,
632
- deliveries,
633
- deliveryFailures,
634
- ...(responseDeliveryFailure ? { responseDeliveryFailure } : {}),
635
- providerInvocationCount,
636
- toolInvocationCount,
637
- sessionPath: sessPath,
638
- ...(deliveries.length > 0 ? { causalSessionEventIds: causalSessionEventIds(eventView, deliveryAttempts, finalDeliveryAttemptIndex, finalCausalCoordinate) } : {}),
639
- ...(responseCausalSessionEventId ? { responseCausalSessionEventId } : {}),
640
- };
641
- });
753
+ }
642
754
  }