@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.
@@ -58,6 +58,7 @@ const trust_gate_1 = require("./trust-gate");
58
58
  const pipeline_1 = require("./pipeline");
59
59
  const mcp_manager_1 = require("../repertoire/mcp-manager");
60
60
  const runtime_1 = require("../nerves/runtime");
61
+ const session_transaction_1 = require("../mind/session-transaction");
61
62
  const RESPONSE_CAP = 50_000;
62
63
  const OUTWARD_DELIVERY_TOOL_ACKS = new Map([
63
64
  ["settle", "(delivered)"],
@@ -234,184 +235,190 @@ async function runSenseTurn(options) {
234
235
  const sessionDir = path.join(agentRoot, "state", "sessions", friendId, channel);
235
236
  fs.mkdirSync(sessionDir, { recursive: true });
236
237
  const sessPath = path.join(sessionDir, `${(0, config_1.sanitizeKey)(sessionKey)}.json`);
237
- const existing = (0, context_1.loadSession)(sessPath);
238
- let sessionState = existing?.state;
239
- let persistPromise;
240
- const sessionMessages = existing?.messages && existing.messages.length > 0
241
- ? existing.messages
242
- : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)(channel, {}, undefined)) }];
243
- // Pending dir
244
- const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
245
- // Accumulate outward text through the same callback boundary used by chat
246
- // channels. `speak` flushes pending text immediately; `settle` is delivered
247
- // once the turn completes.
248
- let committedResponseText = "";
249
- let pendingResponseText = "";
250
- let terminalDeliveryKind = "text";
251
- const deliveries = [];
252
- const deliveryFailures = [];
253
- const commitResponseText = (text) => {
254
- const cleaned = stripThinkBlocks(text);
255
- /* v8 ignore next -- deliverPending strips first; this is a defensive direct-call guard @preserve */
256
- if (!cleaned)
257
- return;
258
- committedResponseText = committedResponseText
259
- ? `${committedResponseText}\n${cleaned}`
260
- : cleaned;
261
- };
262
- const deliveryErrorMessage = (error) => error instanceof Error ? error.message : String(error);
263
- const deliverPending = async (kind, optionsForDelivery) => {
264
- const text = stripThinkBlocks(pendingResponseText);
265
- pendingResponseText = "";
266
- if (!text)
267
- return;
268
- const delivery = { kind, text };
269
- try {
270
- await options.deliverySink?.onDelivery(delivery);
271
- deliveries.push(delivery);
272
- commitResponseText(text);
273
- }
274
- catch (error) {
275
- const failure = { ...delivery, error: deliveryErrorMessage(error) };
276
- deliveryFailures.push(failure);
277
- (0, runtime_1.emitNervesEvent)({
278
- level: "error",
279
- component: "senses",
280
- event: "senses.shared_turn_delivery_error",
281
- message: "shared turn outward delivery failed",
282
- meta: { agentName, channel, sessionKey, friendId, kind, error: failure.error, textLength: text.length },
283
- });
284
- if (optionsForDelivery.throwOnError)
285
- throw error;
286
- commitResponseText(text);
287
- }
288
- };
289
- /* v8 ignore start — no-op callback stubs; only onTextChunk does real work (covered via mock) */
290
- const callbacks = {
291
- settleOutputMode: "retractable_buffer",
292
- onModelStart: () => { },
293
- onModelStreamStart: () => { },
294
- onTextChunk: (chunk) => { pendingResponseText += chunk; },
295
- onReasoningChunk: () => { },
296
- onToolStart: () => { },
297
- onToolEnd: (name, _summary, success) => {
298
- if (name === "settle" && success)
299
- terminalDeliveryKind = "settle";
300
- },
301
- onError: () => { },
302
- onClearText: () => { pendingResponseText = ""; },
303
- flushNow: () => deliverPending("speak", { throwOnError: true }),
304
- };
305
- /* v8 ignore stop */
306
- // Run the pipeline
307
- const userMsg = { role: "user", content: userMessage };
308
- (0, session_events_1.stampIngressTime)(userMsg);
309
- const turnResult = await (0, pipeline_1.handleInboundTurn)({
310
- channel,
311
- latencyMode: options.latencyMode,
312
- sessionKey,
313
- capabilities,
314
- messages: [userMsg],
315
- callbacks,
316
- /* v8 ignore start — delegation wrappers; pipeline integration tested separately */
317
- friendResolver: { resolve: () => resolver.resolve() },
318
- sessionLoader: {
319
- loadOrCreate: () => Promise.resolve({
320
- messages: sessionMessages,
321
- sessionPath: sessPath,
322
- state: sessionState,
323
- events: existing?.events,
324
- structuredOutputs: existing?.structuredOutputs,
325
- }),
326
- },
327
- /* v8 ignore stop */
328
- pendingDir,
329
- friendStore,
330
- provider: resolverParams.provider,
331
- externalId: resolverParams.externalId,
332
- tenantId: resolverParams.tenantId,
333
- enforceTrustGate: trust_gate_1.enforceTrustGate,
334
- drainPending: pending_1.drainPending,
335
- runAgentOptions: {
336
- mcpManager,
337
- ...(options.latencyMode === "live" ? { skipKeptNotes: true } : {}),
338
- toolContext: {
339
- signin: async () => undefined,
340
- ...(options.toolContext ? options.toolContext : {}),
341
- currentUserMessage: userMessage,
238
+ const runWithLease = options._withSessionTurnLease ?? session_transaction_1.withSessionTurnLease;
239
+ return runWithLease(sessPath, async (sessionTurnLease) => {
240
+ const existing = (0, context_1.loadSession)(sessPath);
241
+ let sessionState = existing?.state;
242
+ let persistPromise;
243
+ const sessionMessages = existing?.messages && existing.messages.length > 0
244
+ ? existing.messages
245
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)(channel, {}, undefined)) }];
246
+ // Pending dir
247
+ const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
248
+ // Accumulate outward text through the same callback boundary used by chat
249
+ // channels. `speak` flushes pending text immediately; `settle` is delivered
250
+ // once the turn completes.
251
+ let committedResponseText = "";
252
+ let pendingResponseText = "";
253
+ let terminalDeliveryKind = "text";
254
+ const deliveries = [];
255
+ const deliveryFailures = [];
256
+ const commitResponseText = (text) => {
257
+ const cleaned = stripThinkBlocks(text);
258
+ /* v8 ignore next -- deliverPending strips first; this is a defensive direct-call guard @preserve */
259
+ if (!cleaned)
260
+ return;
261
+ committedResponseText = committedResponseText
262
+ ? `${committedResponseText}\n${cleaned}`
263
+ : cleaned;
264
+ };
265
+ const deliveryErrorMessage = (error) => error instanceof Error ? error.message : String(error);
266
+ const deliverPending = async (kind, optionsForDelivery) => {
267
+ const text = stripThinkBlocks(pendingResponseText);
268
+ pendingResponseText = "";
269
+ if (!text)
270
+ return;
271
+ const delivery = { kind, text };
272
+ try {
273
+ await options.deliverySink?.onDelivery(delivery);
274
+ deliveries.push(delivery);
275
+ commitResponseText(text);
276
+ }
277
+ catch (error) {
278
+ const failure = { ...delivery, error: deliveryErrorMessage(error) };
279
+ deliveryFailures.push(failure);
280
+ (0, runtime_1.emitNervesEvent)({
281
+ level: "error",
282
+ component: "senses",
283
+ event: "senses.shared_turn_delivery_error",
284
+ message: "shared turn outward delivery failed",
285
+ meta: { agentName, channel, sessionKey, friendId, kind, error: failure.error, textLength: text.length },
286
+ });
287
+ if (optionsForDelivery.throwOnError)
288
+ throw error;
289
+ commitResponseText(text);
290
+ }
291
+ };
292
+ /* v8 ignore start — no-op callback stubs; only onTextChunk does real work (covered via mock) */
293
+ const callbacks = {
294
+ settleOutputMode: "retractable_buffer",
295
+ onModelStart: () => { },
296
+ onModelStreamStart: () => { },
297
+ onTextChunk: (chunk) => { pendingResponseText += chunk; },
298
+ onReasoningChunk: () => { },
299
+ onToolStart: () => { },
300
+ onToolEnd: (name, _summary, success) => {
301
+ if (name === "settle" && success)
302
+ terminalDeliveryKind = "settle";
342
303
  },
343
- },
344
- /* v8 ignore start — delegation wrappers; these just forward to the real functions */
345
- runAgent: (msgs, cb, ch, sig, opts) => (0, core_1.runAgent)(msgs, cb, ch, sig, opts),
346
- postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
347
- const prepared = (0, context_2.postTurnTrim)(turnMessages, usage, hooks);
348
- sessionState = state;
349
- persistPromise = (0, context_2.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
350
- },
351
- /* v8 ignore stop */
352
- accumulateFriendTokens: friends_1.accumulateFriendTokens,
353
- });
354
- if (turnResult.gateResult && !turnResult.gateResult.allowed) {
355
- const blockedResponse = "autoReply" in turnResult.gateResult
356
- ? turnResult.gateResult.autoReply
357
- : `(blocked by trust gate: ${turnResult.gateResult.reason})`;
358
- return {
359
- response: blockedResponse,
360
- ponderDeferred: false,
361
- deliveries,
362
- deliveryFailures,
304
+ onError: () => { },
305
+ onClearText: () => { pendingResponseText = ""; },
306
+ flushNow: () => deliverPending("speak", { throwOnError: true }),
363
307
  };
364
- }
365
- await deliverPending(terminalDeliveryKind, { throwOnError: false });
366
- const ponderDeferred = false;
367
- // Build response
368
- let finalResponse;
369
- if (committedResponseText.length === 0) {
370
- // Agent settled but no text came through callbacks — check session transcript for the settle answer
371
- // Await deferred persist so the session file is up-to-date before readback
372
- /* v8 ignore next -- persistPromise set inside v8-ignored postTurn callback; tested via pipeline integration @preserve */
308
+ /* v8 ignore stop */
309
+ // Run the pipeline
310
+ const userMsg = { role: "user", content: userMessage };
311
+ (0, session_events_1.stampIngressTime)(userMsg);
312
+ const turnResult = await (0, pipeline_1.handleInboundTurn)({
313
+ channel,
314
+ latencyMode: options.latencyMode,
315
+ sessionKey,
316
+ capabilities,
317
+ messages: [userMsg],
318
+ callbacks,
319
+ sessionTurnLease,
320
+ /* v8 ignore start — delegation wrappers; pipeline integration tested separately */
321
+ friendResolver: { resolve: () => resolver.resolve() },
322
+ sessionLoader: {
323
+ loadOrCreate: () => Promise.resolve({
324
+ messages: sessionMessages,
325
+ sessionPath: sessPath,
326
+ state: sessionState,
327
+ events: existing?.events,
328
+ structuredOutputs: existing?.structuredOutputs,
329
+ }),
330
+ },
331
+ /* v8 ignore stop */
332
+ pendingDir,
333
+ friendStore,
334
+ provider: resolverParams.provider,
335
+ externalId: resolverParams.externalId,
336
+ tenantId: resolverParams.tenantId,
337
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
338
+ drainPending: pending_1.drainPending,
339
+ runAgentOptions: {
340
+ mcpManager,
341
+ ...(options.latencyMode === "live" ? { skipKeptNotes: true } : {}),
342
+ toolContext: {
343
+ signin: async () => undefined,
344
+ ...(options.toolContext ? options.toolContext : {}),
345
+ currentUserMessage: userMessage,
346
+ },
347
+ },
348
+ /* v8 ignore start — delegation wrappers; these just forward to the real functions */
349
+ runAgent: (msgs, cb, ch, sig, opts) => (0, core_1.runAgent)(msgs, cb, ch, sig, opts),
350
+ postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
351
+ const prepared = (0, context_2.postTurnTrim)(turnMessages, usage, hooks);
352
+ sessionState = state;
353
+ persistPromise = (0, context_2.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
354
+ },
355
+ /* v8 ignore stop */
356
+ accumulateFriendTokens: friends_1.accumulateFriendTokens,
357
+ });
358
+ if (turnResult.gateResult && !turnResult.gateResult.allowed) {
359
+ const blockedResponse = "autoReply" in turnResult.gateResult
360
+ ? turnResult.gateResult.autoReply
361
+ : `(blocked by trust gate: ${turnResult.gateResult.reason})`;
362
+ return {
363
+ response: blockedResponse,
364
+ ponderDeferred: false,
365
+ deliveries,
366
+ deliveryFailures,
367
+ };
368
+ }
373
369
  if (persistPromise)
374
370
  await persistPromise;
375
- const postTurnSession = (0, context_1.loadSession)(sessPath);
376
- if (postTurnSession?.messages) {
377
- finalResponse = extractOutwardSenseDeliveryText(postTurnSession.messages)
378
- ?? "(agent responded but response was empty)";
371
+ await deliverPending(terminalDeliveryKind, { throwOnError: false });
372
+ const ponderDeferred = false;
373
+ // Build response
374
+ let finalResponse;
375
+ if (committedResponseText.length === 0) {
376
+ // Agent settled but no text came through callbacks — check session transcript for the settle answer
377
+ // Await deferred persist so the session file is up-to-date before readback
378
+ /* v8 ignore next -- persistPromise set inside v8-ignored postTurn callback; tested via pipeline integration @preserve */
379
+ if (persistPromise)
380
+ await persistPromise;
381
+ const postTurnSession = (0, context_1.loadSession)(sessPath);
382
+ if (postTurnSession?.messages) {
383
+ finalResponse = extractOutwardSenseDeliveryText(postTurnSession.messages)
384
+ ?? "(agent responded but response was empty)";
385
+ }
386
+ else {
387
+ finalResponse = "(agent responded but response was empty)";
388
+ }
379
389
  }
380
390
  else {
381
- finalResponse = "(agent responded but response was empty)";
391
+ finalResponse = committedResponseText;
392
+ }
393
+ // Strip MiniMax-style <think>...</think> blocks from the final response.
394
+ // When a reasoning-style model emits only a think block and no final answer
395
+ // (no settle tool call, no post-think text), the readback path above
396
+ // surfaces the raw saved assistant content — which includes the think tags
397
+ // and renders as empty (or as raw reasoning) on MCP/CLI clients. Strip
398
+ // here so the caller sees the actual delivered text. If only reasoning
399
+ // came through and nothing else, surface a clear diagnostic message
400
+ // instead of a blank response so the operator knows what happened.
401
+ finalResponse = stripThinkBlocks(finalResponse);
402
+ if (finalResponse.length === 0) {
403
+ (0, runtime_1.emitNervesEvent)({
404
+ level: "warn",
405
+ component: "senses",
406
+ event: "senses.shared_turn_only_reasoning",
407
+ message: "agent produced only <think> reasoning with no final answer — likely a model that closed the think tag without continuing",
408
+ meta: { agentName, channel, sessionKey, friendId },
409
+ });
410
+ finalResponse = "(agent produced reasoning but no final answer this turn — try again, or check the session transcript for the trace)";
411
+ }
412
+ // Cap response length
413
+ if (finalResponse.length > RESPONSE_CAP) {
414
+ finalResponse = finalResponse.slice(0, RESPONSE_CAP) + "\n\n[truncated — response exceeded 50K characters]";
382
415
  }
383
- }
384
- else {
385
- finalResponse = committedResponseText;
386
- }
387
- // Strip MiniMax-style <think>...</think> blocks from the final response.
388
- // When a reasoning-style model emits only a think block and no final answer
389
- // (no settle tool call, no post-think text), the readback path above
390
- // surfaces the raw saved assistant content — which includes the think tags
391
- // and renders as empty (or as raw reasoning) on MCP/CLI clients. Strip
392
- // here so the caller sees the actual delivered text. If only reasoning
393
- // came through and nothing else, surface a clear diagnostic message
394
- // instead of a blank response so the operator knows what happened.
395
- finalResponse = stripThinkBlocks(finalResponse);
396
- if (finalResponse.length === 0) {
397
416
  (0, runtime_1.emitNervesEvent)({
398
- level: "warn",
399
417
  component: "senses",
400
- event: "senses.shared_turn_only_reasoning",
401
- message: "agent produced only <think> reasoning with no final answer — likely a model that closed the think tag without continuing",
402
- meta: { agentName, channel, sessionKey, friendId },
418
+ event: "senses.shared_turn_end",
419
+ message: "shared turn runner complete",
420
+ meta: { agentName, channel, sessionKey, friendId, ponderDeferred, responseLength: finalResponse.length },
403
421
  });
404
- finalResponse = "(agent produced reasoning but no final answer this turn — try again, or check the session transcript for the trace)";
405
- }
406
- // Cap response length
407
- if (finalResponse.length > RESPONSE_CAP) {
408
- finalResponse = finalResponse.slice(0, RESPONSE_CAP) + "\n\n[truncated — response exceeded 50K characters]";
409
- }
410
- (0, runtime_1.emitNervesEvent)({
411
- component: "senses",
412
- event: "senses.shared_turn_end",
413
- message: "shared turn runner complete",
414
- meta: { agentName, channel, sessionKey, friendId, ponderDeferred, responseLength: finalResponse.length },
422
+ return { response: finalResponse, ponderDeferred, deliveries, deliveryFailures };
415
423
  });
416
- return { response: finalResponse, ponderDeferred, deliveries, deliveryFailures };
417
424
  }