@ouro.bot/cli 0.1.0-alpha.140 → 0.1.0-alpha.142

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 CHANGED
@@ -1,6 +1,25 @@
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.142",
6
+ "changes": [
7
+ "Surface tool fulfills heart obligations on successful routing: findPendingObligationForOrigin + fulfillObligation called after inner obligation advance, wrapped in try/catch.",
8
+ "New fulfillHeartObligation callback on HandleSurfaceInput — origin-based lookup independent of inner obligationId.",
9
+ "ouro inner status command: reads runtime.json, journal dir, heartbeat cadence, attention count. Shows last turn, status, heartbeat health, journal listing, held thoughts."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.141",
14
+ "changes": [
15
+ "Ponder tool replaces descend: thought+say required from outer sessions, parameterless from inner dialog. Creates heart obligation, enqueues pending with delegatedFrom, emits say text outward.",
16
+ "Ponder from inner dialog enqueues synthetic pending message (no delegatedFrom) to continue thinking across turns.",
17
+ "Rest tool for inner dialog: parameterless, gated by attention queue. Rejected with guidance when unsurfaced thoughts remain.",
18
+ "Settle removed from inner dialog tool set (replaced by rest). Outer sessions still use settle.",
19
+ "Session migration: descend -> ponder added to migrateToolNames(). go_inward now maps to ponder.",
20
+ "Ponder and rest added to CIRCUIT_BREAKER_EXEMPT and SOLE_CALL_REJECTION."
21
+ ]
22
+ },
4
23
  {
5
24
  "version": "0.1.0-alpha.140",
6
25
  "changes": [
@@ -176,7 +176,8 @@ Object.defineProperty(exports, "buildSystem", { enumerable: true, get: function
176
176
  const SOLE_CALL_REJECTION = {
177
177
  settle: "rejected: settle must be the only tool call. finish your work first, then call settle alone.",
178
178
  observe: "rejected: observe must be the only tool call. call observe alone when you want to stay silent.",
179
- descend: "rejected: descend must be the only tool call. finish your other work first, then call descend alone.",
179
+ ponder: "rejected: ponder must be the only tool call. finish your other work first, then call ponder alone.",
180
+ rest: "rejected: rest must be the only tool call. finish your work first, then call rest alone.",
180
181
  };
181
182
  const DELEGATION_REASON_PROSE_HANDOFF = {
182
183
  explicit_reflection: "something in the conversation called for reflection",
@@ -186,7 +187,7 @@ const DELEGATION_REASON_PROSE_HANDOFF = {
186
187
  non_fast_path_tool: "this needs tools beyond a simple reply",
187
188
  unresolved_obligation: "there's an unresolved commitment from an earlier conversation",
188
189
  };
189
- function buildDescendHandoffPacket(params) {
190
+ function buildPonderHandoffPacket(params) {
190
191
  const reasons = params.delegationDecision?.reasons ?? [];
191
192
  const reasonProse = reasons.length > 0
192
193
  ? reasons.map((r) => DELEGATION_REASON_PROSE_HANDOFF[r]).join("; ")
@@ -245,14 +246,14 @@ function isExternalStateQuery(toolName, args) {
245
246
  const cmd = String(args.command ?? "");
246
247
  return /\bgh\s+(pr|run|api|issue)\b/.test(cmd) || /\bnpm\s+(view|info|show)\b/.test(cmd);
247
248
  }
248
- function getSettleRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollowUp, _delegationDecision, sawSendMessageSelf, sawDescend, _sawQuerySession, currentObligation, innerJob, sawExternalStateQuery) {
249
+ function getSettleRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollowUp, _delegationDecision, sawSendMessageSelf, sawPonder, _sawQuerySession, currentObligation, innerJob, sawExternalStateQuery) {
249
250
  // Delegation adherence removed: the delegation decision is surfaced in the
250
251
  // system prompt as a suggestion. Hard-gating settle caused infinite
251
252
  // rejection loops where the agent couldn't respond to the user at all.
252
253
  // The agent is free to follow or ignore the delegation hint.
253
254
  // 2. Pending obligation not addressed
254
- if (innerJob?.obligationStatus === "pending" && !sawSendMessageSelf && !sawDescend) {
255
- return "you're still holding something from an earlier conversation -- someone is waiting for your answer. finish the thought first, or descend to keep working on it privately.";
255
+ if (innerJob?.obligationStatus === "pending" && !sawSendMessageSelf && !sawPonder) {
256
+ return "you're still holding something from an earlier conversation -- someone is waiting for your answer. finish the thought first, or ponder to keep working on it privately.";
256
257
  }
257
258
  // 3. mustResolveBeforeHandoff + missing intent
258
259
  if (mustResolveBeforeHandoff && !intent) {
@@ -496,7 +497,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
496
497
  let mustResolveBeforeHandoffActive = options?.mustResolveBeforeHandoff === true;
497
498
  let currentReasoningEffort = "medium";
498
499
  let sawSendMessageSelf = false;
499
- let sawDescend = false;
500
+ let sawPonder = false;
500
501
  let sawQuerySession = false;
501
502
  let sawBridgeManage = false;
502
503
  let sawExternalStateQuery = false;
@@ -522,11 +523,13 @@ async function runAgent(messages, callbacks, channel, signal, options) {
522
523
  providerRuntime.resetTurnState(messages);
523
524
  while (!done) {
524
525
  // Channel-based tool filtering:
525
- // - Inner dialog: exclude descend (already inward), send_message (delivery via surface), observe (no one to observe)
526
+ // - Inner dialog: exclude send_message (delivery via surface), observe (no one to observe)
526
527
  // - 1:1 sessions: exclude observe (can't ignore someone talking directly to you)
527
528
  // - Group chats: observe available
528
529
  //
529
- // descend, settle, surface, and observe are always assembled based on channel context.
530
+ // ponder, settle/rest, surface, and observe are always assembled based on channel context.
531
+ // ponder is available in ALL channels (outer: think privately, inner: keep turning).
532
+ // Inner dialog gets restTool instead of settleTool (rest = end turn, gated by attention queue).
530
533
  // toolChoiceRequired only controls whether tool_choice: "required" is set in the API call.
531
534
  const isInnerDialog = channel === "inner";
532
535
  const filteredBaseTools = isInnerDialog
@@ -534,10 +537,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
534
537
  : baseTools;
535
538
  const activeTools = [
536
539
  ...filteredBaseTools,
537
- ...(!isInnerDialog ? [tools_1.descendTool] : []),
538
- ...(isInnerDialog ? [surface_tool_1.surfaceToolDef] : []),
540
+ tools_1.ponderTool,
541
+ ...(isInnerDialog ? [surface_tool_1.surfaceToolDef, tools_1.restTool] : []),
539
542
  ...(currentContext?.isGroupChat && !isInnerDialog ? [tools_1.observeTool] : []),
540
- tools_1.settleTool,
543
+ ...(!isInnerDialog ? [tools_1.settleTool] : []),
541
544
  ];
542
545
  const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
543
546
  if (steeringFollowUps.length > 0) {
@@ -652,7 +655,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
652
655
  done = true;
653
656
  continue;
654
657
  }
655
- const retryError = getSettleRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp, options?.delegationDecision, sawSendMessageSelf, sawDescend, sawQuerySession, options?.currentObligation ?? null, options?.activeWorkFrame?.inner?.job, sawExternalStateQuery);
658
+ const retryError = getSettleRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp, options?.delegationDecision, sawSendMessageSelf, sawPonder, sawQuerySession, options?.currentObligation ?? null, options?.activeWorkFrame?.inner?.job, sawExternalStateQuery);
656
659
  const deliveredAnswer = answer;
657
660
  const validDirectReply = mustResolveBeforeHandoffActive && intent === "direct_reply" && sawSteeringFollowUp;
658
661
  const validTerminalIntent = intent === "complete" || intent === "blocked";
@@ -734,94 +737,157 @@ async function runAgent(messages, callbacks, channel, signal, options) {
734
737
  done = true;
735
738
  continue;
736
739
  }
737
- // Check for descend sole call: intercept before tool execution
738
- const isSoleDescend = result.toolCalls.length === 1 && result.toolCalls[0].name === "descend";
739
- if (isSoleDescend) {
740
+ // Check for ponder sole call: intercept before tool execution
741
+ const isSolePonder = result.toolCalls.length === 1 && result.toolCalls[0].name === "ponder";
742
+ if (isSolePonder) {
740
743
  let parsedArgs = {};
741
744
  try {
742
745
  parsedArgs = JSON.parse(result.toolCalls[0].arguments);
743
746
  }
744
747
  catch { /* ignore */ }
745
- callbacks.onToolStart("descend", parsedArgs);
746
- /* v8 ignore next -- defensive: topic always string from model @preserve */
747
- const topic = typeof parsedArgs.topic === "string" ? parsedArgs.topic : "";
748
- const answer = typeof parsedArgs.answer === "string" ? parsedArgs.answer : undefined;
749
- const parsedMode = parsedArgs.mode === "reflect" || parsedArgs.mode === "plan" || parsedArgs.mode === "relay"
750
- ? parsedArgs.mode
751
- : undefined;
752
- const mode = parsedMode || "reflect";
753
- // Emit outward answer if provided
754
- if (answer) {
755
- callbacks.onClearText?.();
756
- callbacks.onTextChunk(answer);
757
- }
758
- // Build handoff packet and enqueue
759
- const handoffContent = buildDescendHandoffPacket({
760
- topic,
761
- mode,
762
- delegationDecision: options?.delegationDecision,
763
- currentSession: options?.toolContext?.currentSession ?? null,
764
- currentObligation: options?.currentObligation ?? null,
765
- outwardClosureRequired: options?.delegationDecision?.outwardClosureRequired ?? false,
766
- });
767
- const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_2.getAgentName)());
748
+ callbacks.onToolStart("ponder", parsedArgs);
768
749
  const currentSession = options?.toolContext?.currentSession;
769
750
  const isInnerChannel = currentSession?.friendId === "self" && currentSession?.channel === "inner";
770
- // Create obligation FIRST so we can attach its ID to the pending message
771
- let createdObligationId;
772
- if (currentSession && !isInnerChannel) {
773
- try {
774
- const obligation = (0, obligations_1.createObligation)((0, identity_2.getAgentRoot)(), {
775
- origin: {
751
+ if (!isInnerChannel) {
752
+ // Outer session: thought and say are required
753
+ const thought = typeof parsedArgs.thought === "string" ? parsedArgs.thought : undefined;
754
+ const say = typeof parsedArgs.say === "string" ? parsedArgs.say : undefined;
755
+ if (!thought || !say) {
756
+ callbacks.onToolEnd("ponder", (0, tools_1.summarizeArgs)("ponder", parsedArgs), false);
757
+ messages.push(msg);
758
+ const errorMsg = "ponder requires both thought and say when called from a conversation. thought is what needs more thinking; say is what you tell them before going quiet.";
759
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: errorMsg });
760
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, errorMsg);
761
+ continue;
762
+ }
763
+ // Emit outward say text
764
+ callbacks.onClearText?.();
765
+ callbacks.onTextChunk(say);
766
+ // Build handoff packet and enqueue
767
+ const handoffContent = buildPonderHandoffPacket({
768
+ topic: thought,
769
+ mode: "reflect",
770
+ delegationDecision: options?.delegationDecision,
771
+ currentSession: currentSession ?? null,
772
+ currentObligation: options?.currentObligation ?? null,
773
+ outwardClosureRequired: options?.delegationDecision?.outwardClosureRequired ?? false,
774
+ });
775
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_2.getAgentName)());
776
+ // Create obligation FIRST so we can attach its ID to the pending message
777
+ let createdObligationId;
778
+ if (currentSession) {
779
+ try {
780
+ const obligation = (0, obligations_1.createObligation)((0, identity_2.getAgentRoot)(), {
781
+ origin: {
782
+ friendId: currentSession.friendId,
783
+ channel: currentSession.channel,
784
+ key: currentSession.key,
785
+ },
786
+ content: thought,
787
+ });
788
+ createdObligationId = obligation.id;
789
+ }
790
+ catch {
791
+ /* v8 ignore next -- defensive: obligation store write failure should not break ponder @preserve */
792
+ }
793
+ }
794
+ const envelope = {
795
+ from: (0, identity_2.getAgentName)(),
796
+ friendId: "self",
797
+ channel: "inner",
798
+ key: "dialog",
799
+ content: handoffContent,
800
+ timestamp: Date.now(),
801
+ ...(currentSession ? {
802
+ delegatedFrom: {
776
803
  friendId: currentSession.friendId,
777
804
  channel: currentSession.channel,
778
805
  key: currentSession.key,
779
806
  },
780
- content: topic,
781
- });
782
- createdObligationId = obligation.id;
783
- }
784
- catch {
785
- /* v8 ignore next -- defensive: obligation store write failure should not break descend @preserve */
807
+ obligationStatus: "pending",
808
+ /* v8 ignore next -- defensive: createdObligationId is undefined only when obligation store write fails @preserve */
809
+ ...(createdObligationId ? { obligationId: createdObligationId } : {}),
810
+ } : {}),
811
+ };
812
+ (0, pending_1.queuePendingMessage)(pendingDir, envelope);
813
+ try {
814
+ await (0, socket_client_1.requestInnerWake)((0, identity_2.getAgentName)());
786
815
  }
816
+ catch { /* daemon may not be running */ }
817
+ callbacks.onToolEnd("ponder", (0, tools_1.summarizeArgs)("ponder", parsedArgs), true);
818
+ sawPonder = true;
819
+ messages.push(msg);
820
+ const ack = "(pondering)";
821
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: ack });
822
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, ack);
823
+ (0, runtime_1.emitNervesEvent)({
824
+ component: "engine",
825
+ event: "engine.pondered",
826
+ message: "pondering",
827
+ meta: { hasThought: true, hasSay: true, contentSnippet: thought.slice(0, 80) },
828
+ });
787
829
  }
788
- const envelope = {
789
- from: (0, identity_2.getAgentName)(),
790
- friendId: "self",
791
- channel: "inner",
792
- key: "dialog",
793
- content: handoffContent,
794
- timestamp: Date.now(),
795
- mode,
796
- ...(currentSession && !isInnerChannel ? {
797
- delegatedFrom: {
798
- friendId: currentSession.friendId,
799
- channel: currentSession.channel,
800
- key: currentSession.key,
801
- },
802
- obligationStatus: "pending",
803
- /* v8 ignore next -- defensive: createdObligationId is undefined only when obligation store write fails @preserve */
804
- ...(createdObligationId ? { obligationId: createdObligationId } : {}),
805
- } : {}),
806
- };
807
- (0, pending_1.queuePendingMessage)(pendingDir, envelope);
808
- try {
809
- await (0, socket_client_1.requestInnerWake)((0, identity_2.getAgentName)());
830
+ else {
831
+ // Inner dialog: parameterless, enqueue synthetic pending WITHOUT delegatedFrom
832
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_2.getAgentName)());
833
+ const envelope = {
834
+ from: (0, identity_2.getAgentName)(),
835
+ friendId: "self",
836
+ channel: "inner",
837
+ key: "dialog",
838
+ content: "continuing to think...",
839
+ timestamp: Date.now(),
840
+ };
841
+ (0, pending_1.queuePendingMessage)(pendingDir, envelope);
842
+ callbacks.onToolEnd("ponder", (0, tools_1.summarizeArgs)("ponder", parsedArgs), true);
843
+ sawPonder = true;
844
+ messages.push(msg);
845
+ const ack = "(pondering)";
846
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: ack });
847
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, ack);
848
+ (0, runtime_1.emitNervesEvent)({
849
+ component: "engine",
850
+ event: "engine.pondered",
851
+ message: "pondering from inner dialog",
852
+ meta: { inner: true },
853
+ });
854
+ }
855
+ outcome = "pondered";
856
+ done = true;
857
+ continue;
858
+ }
859
+ // Check for rest sole call: intercept before tool execution
860
+ const isSoleRest = result.toolCalls.length === 1 && result.toolCalls[0].name === "rest";
861
+ if (isSoleRest) {
862
+ const restArgs = (() => { try {
863
+ return JSON.parse(result.toolCalls[0].arguments);
864
+ }
865
+ catch {
866
+ return {};
867
+ } })();
868
+ callbacks.onToolStart("rest", restArgs);
869
+ // Attention queue gate: reject rest if items remain
870
+ const attentionQueue = (augmentedToolContext ?? options?.toolContext)?.delegatedOrigins;
871
+ if (attentionQueue && attentionQueue.length > 0) {
872
+ callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), false);
873
+ messages.push(msg);
874
+ const gateMessage = "you're holding thoughts someone is waiting for — surface them before you rest.";
875
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
876
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
877
+ continue;
810
878
  }
811
- catch { /* daemon may not be running */ }
812
- callbacks.onToolEnd("descend", (0, tools_1.summarizeArgs)("descend", parsedArgs), true);
813
- sawDescend = true;
879
+ callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), true);
814
880
  messages.push(msg);
815
- const ack = "(going inward)";
881
+ const ack = "(resting)";
816
882
  messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: ack });
817
883
  providerRuntime.appendToolOutput(result.toolCalls[0].id, ack);
818
884
  (0, runtime_1.emitNervesEvent)({
819
885
  component: "engine",
820
- event: "engine.descended",
821
- message: "taking thread inward",
822
- meta: { mode, hasAnswer: answer !== undefined, contentSnippet: topic.slice(0, 80) },
886
+ event: "engine.rested",
887
+ message: "resting until next heartbeat",
888
+ meta: {},
823
889
  });
824
- outcome = "descended";
890
+ outcome = "rested";
825
891
  done = true;
826
892
  continue;
827
893
  }
@@ -981,7 +1047,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
981
1047
  trace_id: traceId,
982
1048
  component: "engine",
983
1049
  message: "runAgent turn completed",
984
- meta: { done, sawDescend, sawQuerySession, sawBridgeManage },
1050
+ meta: { done, sawPonder, sawQuerySession, sawBridgeManage },
985
1051
  });
986
1052
  return {
987
1053
  usage: lastUsage,
@@ -295,6 +295,7 @@ function usage() {
295
295
  " ouro friend create --name <name> [--trust <level>] [--agent <name>]",
296
296
  " ouro friend update <id> --trust <level> [--agent <name>]",
297
297
  " ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
298
+ " ouro inner [--agent <name>]",
298
299
  " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
299
300
  " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
300
301
  " ouro whoami [--agent <name>]",
@@ -987,6 +988,10 @@ function parseOuroCommand(args) {
987
988
  return parseThoughtsCommand(args.slice(1));
988
989
  if (head === "attention")
989
990
  return parseAttentionCommand(args.slice(1));
991
+ if (head === "inner") {
992
+ const { agent } = extractAgentFlag(args.slice(1));
993
+ return { kind: "inner.status", ...(agent ? { agent } : {}) };
994
+ }
990
995
  if (head === "chat") {
991
996
  if (!second)
992
997
  throw new Error(`Usage\n${usage()}`);
@@ -2607,6 +2612,89 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
2607
2612
  }
2608
2613
  }
2609
2614
  /* v8 ignore stop */
2615
+ // ── inner dialog status (local, no daemon socket needed) ──
2616
+ /* v8 ignore start -- inner status handler: requires real agent state on disk @preserve */
2617
+ if (command.kind === "inner.status") {
2618
+ try {
2619
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
2620
+ const agentRoot = (0, identity_1.getAgentRoot)(agentName);
2621
+ const { buildInnerStatusOutput } = await Promise.resolve().then(() => __importStar(require("./inner-status")));
2622
+ const { sessionPath: getSessionPath } = await Promise.resolve().then(() => __importStar(require("../config")));
2623
+ const { parseCadenceMs, DEFAULT_CADENCE_MS } = await Promise.resolve().then(() => __importStar(require("./heartbeat-timer")));
2624
+ const { parseFrontmatter } = await Promise.resolve().then(() => __importStar(require("../../repertoire/tasks/parser")));
2625
+ const { listActiveObligations } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
2626
+ // Read runtime state
2627
+ const innerSessionPath = getSessionPath("inner-dialog", "inner", "session");
2628
+ const runtimeJsonPath = path.join(path.dirname(innerSessionPath), "runtime.json");
2629
+ let runtimeState = null;
2630
+ try {
2631
+ const raw = fs.readFileSync(runtimeJsonPath, "utf-8");
2632
+ runtimeState = JSON.parse(raw);
2633
+ }
2634
+ catch { /* missing or corrupt — will show "unknown" */ }
2635
+ // Read journal files
2636
+ const journalDir = path.join(agentRoot, "journal");
2637
+ let journalFiles = [];
2638
+ try {
2639
+ const entries = fs.readdirSync(journalDir, { withFileTypes: true });
2640
+ journalFiles = entries
2641
+ .filter((e) => e.isFile() && !e.name.startsWith("."))
2642
+ .map((e) => {
2643
+ const stat = fs.statSync(path.join(journalDir, e.name));
2644
+ return { name: e.name, mtimeMs: stat.mtimeMs };
2645
+ });
2646
+ }
2647
+ catch { /* missing dir — will show (empty) */ }
2648
+ // Read heartbeat cadence
2649
+ let heartbeat = null;
2650
+ try {
2651
+ const habitsDir = path.join(agentRoot, "habits");
2652
+ const habitFiles = fs.readdirSync(habitsDir);
2653
+ const heartbeatFile = habitFiles.find((f) => f.endsWith("-heartbeat.md"));
2654
+ let cadenceMs = DEFAULT_CADENCE_MS;
2655
+ if (heartbeatFile) {
2656
+ const content = fs.readFileSync(path.join(habitsDir, heartbeatFile), "utf-8");
2657
+ const lines = content.split(/\r?\n/);
2658
+ if (lines[0]?.trim() === "---") {
2659
+ const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
2660
+ if (closing !== -1) {
2661
+ const rawFrontmatter = lines.slice(1, closing).join("\n");
2662
+ const frontmatter = parseFrontmatter(rawFrontmatter);
2663
+ const parsed = parseCadenceMs(frontmatter.cadence);
2664
+ if (parsed !== null)
2665
+ cadenceMs = parsed;
2666
+ }
2667
+ }
2668
+ }
2669
+ let lastCompletedAt = null;
2670
+ if (runtimeState?.lastCompletedAt) {
2671
+ const ms = new Date(runtimeState.lastCompletedAt).getTime();
2672
+ if (!Number.isNaN(ms))
2673
+ lastCompletedAt = ms;
2674
+ }
2675
+ heartbeat = { cadenceMs, lastCompletedAt };
2676
+ }
2677
+ catch { /* no habits — heartbeat unknown */ }
2678
+ // Attention count
2679
+ const activeObligations = listActiveObligations(agentName);
2680
+ const message = buildInnerStatusOutput({
2681
+ agentName,
2682
+ runtimeState,
2683
+ journalFiles,
2684
+ heartbeat,
2685
+ attentionCount: activeObligations.length,
2686
+ now: Date.now(),
2687
+ });
2688
+ deps.writeStdout(message);
2689
+ return message;
2690
+ }
2691
+ catch {
2692
+ const message = "error: no agent context — use --agent <name> to specify";
2693
+ deps.writeStdout(message);
2694
+ return message;
2695
+ }
2696
+ }
2697
+ /* v8 ignore stop */
2610
2698
  // ── session list (local, no daemon socket needed) ──
2611
2699
  if (command.kind === "session.list") {
2612
2700
  /* v8 ignore start -- production default: requires full identity setup @preserve */
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildInnerStatusOutput = buildInnerStatusOutput;
4
+ const runtime_1 = require("../../nerves/runtime");
5
+ function formatRelativeTime(elapsedMs) {
6
+ const minutes = Math.floor(elapsedMs / (60 * 1000));
7
+ if (minutes < 1)
8
+ return "just now";
9
+ if (minutes === 1)
10
+ return "1 minute ago";
11
+ if (minutes < 60)
12
+ return `${minutes} minutes ago`;
13
+ const hours = Math.floor(minutes / 60);
14
+ if (hours === 1)
15
+ return "1 hour ago";
16
+ return `${hours} hours ago`;
17
+ }
18
+ function formatCadence(cadenceMs) {
19
+ const minutes = Math.round(cadenceMs / (60 * 1000));
20
+ if (minutes >= 60) {
21
+ const hours = Math.round(minutes / 60);
22
+ return `${hours}h`;
23
+ }
24
+ return `${minutes}m`;
25
+ }
26
+ function buildInnerStatusOutput(input) {
27
+ const { agentName, runtimeState, journalFiles, heartbeat, attentionCount, now } = input;
28
+ const lines = [];
29
+ lines.push(`inner dialog status: ${agentName}`);
30
+ // Last turn
31
+ if (runtimeState?.lastCompletedAt) {
32
+ const lastMs = new Date(runtimeState.lastCompletedAt).getTime();
33
+ const elapsed = now - lastMs;
34
+ const relativeTime = formatRelativeTime(elapsed);
35
+ const reasonSuffix = runtimeState.reason ? ` (${runtimeState.reason})` : "";
36
+ lines.push(` last turn: ${relativeTime}${reasonSuffix}`);
37
+ }
38
+ else {
39
+ lines.push(" last turn: unknown");
40
+ }
41
+ // Status
42
+ if (runtimeState) {
43
+ const reasonSuffix = runtimeState.status === "running" && runtimeState.reason ? ` (${runtimeState.reason})` : "";
44
+ lines.push(` status: ${runtimeState.status}${reasonSuffix}`);
45
+ }
46
+ else {
47
+ lines.push(" status: unknown");
48
+ }
49
+ // Heartbeat health
50
+ if (heartbeat && heartbeat.lastCompletedAt !== null) {
51
+ const elapsed = now - heartbeat.lastCompletedAt;
52
+ const threshold = heartbeat.cadenceMs * 1.5;
53
+ const health = elapsed < threshold ? "healthy" : "overdue";
54
+ const cadenceStr = formatCadence(heartbeat.cadenceMs);
55
+ const sinceStr = formatRelativeTime(elapsed);
56
+ lines.push(` heartbeat: ${health} (cadence ${cadenceStr}, ${sinceStr})`);
57
+ }
58
+ else {
59
+ lines.push(" heartbeat: unknown");
60
+ }
61
+ // Journal
62
+ if (journalFiles.length === 0) {
63
+ lines.push(" journal: (empty)");
64
+ }
65
+ else {
66
+ lines.push(" journal:");
67
+ const sorted = [...journalFiles].sort((a, b) => b.mtimeMs - a.mtimeMs);
68
+ for (const file of sorted) {
69
+ const elapsed = now - file.mtimeMs;
70
+ const relativeTime = formatRelativeTime(elapsed);
71
+ lines.push(` - ${file.name} (${relativeTime})`);
72
+ }
73
+ }
74
+ // Attention
75
+ const thoughtWord = attentionCount === 1 ? "thought" : "thoughts";
76
+ lines.push(` attention: ${attentionCount} held ${thoughtWord}`);
77
+ (0, runtime_1.emitNervesEvent)({
78
+ component: "daemon",
79
+ event: "daemon.inner_status_read",
80
+ message: "inner dialog status read",
81
+ meta: {
82
+ agentName,
83
+ status: runtimeState?.status ?? "unknown",
84
+ journalCount: journalFiles.length,
85
+ attentionCount,
86
+ },
87
+ });
88
+ return lines.join("\n");
89
+ }
@@ -180,8 +180,10 @@ function recordToolOutcome(state, toolName, args, result, success) {
180
180
  }
181
181
  // Tools that must never be blocked by the circuit breaker.
182
182
  // settle = end the turn, surface = deliver results outward.
183
- // Blocking these traps the agent: it can think all it wants but can never speak.
184
- const CIRCUIT_BREAKER_EXEMPT = new Set(["settle", "surface"]);
183
+ // ponder = continue thinking (inner dialog) or hand off to inner dialog (outer).
184
+ // rest = end inner dialog turn (added in Unit 8b).
185
+ // Blocking these traps the agent: it can think all it wants but can never speak or stop.
186
+ const CIRCUIT_BREAKER_EXEMPT = new Set(["settle", "surface", "ponder", "rest"]);
185
187
  function detectToolLoop(state, toolName, args) {
186
188
  if (state.history.length >= exports.GLOBAL_CIRCUIT_BREAKER_LIMIT && !CIRCUIT_BREAKER_EXEMPT.has(toolName)) {
187
189
  return emitDetection("global_circuit_breaker", toolName, state.history.length, `this turn has already made ${state.history.length} tool calls. stop thrashing, use the current evidence, and either change approach or answer truthfully with the best grounded status.`);
@@ -263,7 +263,8 @@ function stripOrphanedToolResults(messages) {
263
263
  const TOOL_NAME_MIGRATIONS = {
264
264
  final_answer: "settle",
265
265
  no_response: "observe",
266
- go_inward: "descend",
266
+ go_inward: "ponder",
267
+ descend: "ponder",
267
268
  memory_save: "diary_write",
268
269
  memory_search: "recall",
269
270
  };
@@ -566,7 +566,7 @@ i should orient around that live lane first, then decide what still needs to com
566
566
  return `## where my attention is
567
567
  i have unfinished work that needs attention before i move on.
568
568
 
569
- i can take it inward with descend to think privately, or address it directly here.`;
569
+ i can take it inward with ponder to think privately, or address it directly here.`;
570
570
  }
571
571
  if (cog === "shared-work") {
572
572
  /* v8 ignore stop */
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.settleTool = exports.observeTool = exports.descendTool = exports.tools = exports.baseToolDefinitions = exports.editFileReadTracker = void 0;
36
+ exports.restTool = exports.settleTool = exports.observeTool = exports.ponderTool = exports.tools = exports.baseToolDefinitions = exports.editFileReadTracker = void 0;
37
37
  exports.renderInnerProgressStatus = renderInnerProgressStatus;
38
38
  const fs = __importStar(require("fs"));
39
39
  const fg = __importStar(require("fast-glob"));
@@ -1362,29 +1362,23 @@ exports.baseToolDefinitions = [
1362
1362
  ...tools_1.codingToolDefinitions,
1363
1363
  ];
1364
1364
  exports.tools = exports.baseToolDefinitions.map((d) => d.tool);
1365
- exports.descendTool = {
1365
+ exports.ponderTool = {
1366
1366
  type: "function",
1367
1367
  function: {
1368
- name: "descend",
1369
- description: "i need to think about this privately. this takes the current thread inward -- i'll sit with it, work through it, or carry it to where it needs to go. must be the only tool call in the turn.",
1368
+ name: "ponder",
1369
+ description: "i need to sit with this. from a conversation, takes the thread inward with a thought and a parting word. from inner dialog, keeps the wheel turning for another pass. must be the only tool call in the turn.",
1370
1370
  parameters: {
1371
1371
  type: "object",
1372
1372
  properties: {
1373
- topic: {
1373
+ thought: {
1374
1374
  type: "string",
1375
- description: "the question or topic that needs private thought — brief framing, not your analysis. your inner dialog will do the actual thinking.",
1375
+ description: "the question or thread that needs more thought — brief framing, not analysis. required from a conversation, ignored from inner dialog.",
1376
1376
  },
1377
- answer: {
1377
+ say: {
1378
1378
  type: "string",
1379
- description: "if i want to say something outward before going inward -- an acknowledgment, a 'let me think about that', whatever feels right",
1380
- },
1381
- mode: {
1382
- type: "string",
1383
- enum: ["reflect", "plan", "relay"],
1384
- description: "reflect: something to sit with. plan: something to work through. relay: something to carry across.",
1379
+ description: "what you say before going quiet — speak to what caught your attention, not just that something did. required from a conversation, ignored from inner dialog.",
1385
1380
  },
1386
1381
  },
1387
- required: ["topic"],
1388
1382
  },
1389
1383
  },
1390
1384
  };
@@ -1416,3 +1410,14 @@ exports.settleTool = {
1416
1410
  },
1417
1411
  },
1418
1412
  };
1413
+ exports.restTool = {
1414
+ type: "function",
1415
+ function: {
1416
+ name: "rest",
1417
+ description: "put this down for now — the wheel stops until the next heartbeat. must be the only tool call in the turn.",
1418
+ parameters: {
1419
+ type: "object",
1420
+ properties: {},
1421
+ },
1422
+ },
1423
+ };
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.descendTool = exports.observeTool = exports.settleTool = exports.tools = void 0;
36
+ exports.restTool = exports.ponderTool = exports.observeTool = exports.settleTool = exports.tools = void 0;
37
37
  exports.getToolsForChannel = getToolsForChannel;
38
38
  exports.isConfirmationRequired = isConfirmationRequired;
39
39
  exports.execTool = execTool;
@@ -48,6 +48,7 @@ const guardrails_1 = require("./guardrails");
48
48
  const identity_1 = require("../heart/identity");
49
49
  const surface_tool_1 = require("../senses/surface-tool");
50
50
  const obligations_1 = require("../mind/obligations");
51
+ const obligations_2 = require("../heart/obligations");
51
52
  const session_activity_1 = require("../heart/session-activity");
52
53
  const path = __importStar(require("path"));
53
54
  function safeGetAgentRoot() {
@@ -63,7 +64,8 @@ var tools_base_2 = require("./tools-base");
63
64
  Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return tools_base_2.tools; } });
64
65
  Object.defineProperty(exports, "settleTool", { enumerable: true, get: function () { return tools_base_2.settleTool; } });
65
66
  Object.defineProperty(exports, "observeTool", { enumerable: true, get: function () { return tools_base_2.observeTool; } });
66
- Object.defineProperty(exports, "descendTool", { enumerable: true, get: function () { return tools_base_2.descendTool; } });
67
+ Object.defineProperty(exports, "ponderTool", { enumerable: true, get: function () { return tools_base_2.ponderTool; } });
68
+ Object.defineProperty(exports, "restTool", { enumerable: true, get: function () { return tools_base_2.restTool; } });
67
69
  // Surface tool handler: routes content to friend's freshest session
68
70
  /* v8 ignore start -- surface handler wiring: core logic tested via surface-tool.test.ts; this wires identity/routing deps @preserve */
69
71
  const surfaceToolDefinition = {
@@ -206,6 +208,20 @@ const surfaceToolDefinition = {
206
208
  }
207
209
  /* v8 ignore stop */
208
210
  },
211
+ fulfillHeartObligation: (origin) => {
212
+ /* v8 ignore start -- heart obligation fulfillment: tested via surface-tool.test.ts @preserve */
213
+ try {
214
+ const agentRoot = (0, identity_1.getAgentRoot)();
215
+ const heartObligation = (0, obligations_2.findPendingObligationForOrigin)(agentRoot, origin);
216
+ if (heartObligation) {
217
+ (0, obligations_2.fulfillObligation)(agentRoot, heartObligation.id);
218
+ }
219
+ }
220
+ catch {
221
+ // swallowed — heart obligation fulfillment must never break surface delivery
222
+ }
223
+ /* v8 ignore stop */
224
+ },
209
225
  });
210
226
  },
211
227
  summaryKeys: ["content", "delegationId"],
@@ -31,7 +31,7 @@ exports.surfaceToolDef = {
31
31
  },
32
32
  };
33
33
  async function handleSurface(input) {
34
- const { content, delegationId, friendId, queue, routeToFriend, advanceObligation } = input;
34
+ const { content, delegationId, friendId, queue, routeToFriend, advanceObligation, fulfillHeartObligation } = input;
35
35
  // Resolve target friend
36
36
  let targetFriendId;
37
37
  let queueItem;
@@ -74,6 +74,19 @@ async function handleSurface(input) {
74
74
  returnTarget: "surface",
75
75
  });
76
76
  }
77
+ // Fulfill the heart obligation for this origin (separate from inner/mind obligation)
78
+ if (fulfillHeartObligation) {
79
+ try {
80
+ fulfillHeartObligation({
81
+ friendId: queueItem.friendId,
82
+ channel: queueItem.channel,
83
+ key: queueItem.key,
84
+ });
85
+ }
86
+ catch {
87
+ // swallowed — heart obligation fulfillment must never break surface delivery
88
+ }
89
+ }
77
90
  (0, attention_queue_1.dequeueAttentionItem)(queue, delegationId);
78
91
  }
79
92
  // Return delivery status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.140",
3
+ "version": "0.1.0-alpha.142",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",