@ouro.bot/cli 0.1.0-alpha.45 → 0.1.0-alpha.47

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,28 @@
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.47",
6
+ "changes": [
7
+ "Self-messages now wake inner dialog now/soon instead of only queueing: the harness requests a daemon-managed inner wake when available and falls back to an inline inner turn when it is not.",
8
+ "`send_message(friendId=self)` and `query_session(friendId=self, channel=inner, mode=status)` now share a truthful four-line status contract: `queue`, `wake`, `processing`, and `surfaced`.",
9
+ "Inner dialog persists live runtime activity beside the transcript, so status checks can report active processing instead of stale last-turn history.",
10
+ "Inline fallback surfaced previews now extract `final_answer` text too, so completed inner turns no longer show `no outward result` when the response lived in the final tool call."
11
+ ]
12
+ },
13
+ {
14
+ "version": "0.1.0-alpha.46",
15
+ "changes": [
16
+ "Inner dialog now knows which task triggered it: taskId flows from daemon poke through the worker into the turn, and the agent gets the full task file content instead of a generic heartbeat prompt.",
17
+ "Inner dialog boot message includes aspirations and state summary instead of a vacuous placeholder, so the agent wakes up with context about what matters and what's happening.",
18
+ "Vestigial `drainInbox` removed from inner dialog — pipeline already handles pending drain correctly.",
19
+ "Inner dialog nerves events now include assistant response preview, tool call names, token usage, and taskId for meaningful observability.",
20
+ "`ouro thoughts` command reads and formats inner dialog session turns with `--last`, `--json`, `--follow`, and `--agent` flags — humans can now see what the agent has been thinking.",
21
+ "`readTaskFile` searches collection subdirectories (one-shots, ongoing, habits) since the scheduler sends bare task stems without collection prefixes.",
22
+ "`ouro reminder create` accepts `--requester` to track who requested a reminder for notification round-trip.",
23
+ "Response extraction handles `tool_choice=required` models by falling back to `final_answer` tool call arguments when assistant message content is empty."
24
+ ]
25
+ },
4
26
  {
5
27
  "version": "0.1.0-alpha.45",
6
28
  "changes": [
@@ -41,7 +41,6 @@ exports.runOuroCli = runOuroCli;
41
41
  const child_process_1 = require("child_process");
42
42
  const crypto_1 = require("crypto");
43
43
  const fs = __importStar(require("fs"));
44
- const net = __importStar(require("net"));
45
44
  const os = __importStar(require("os"));
46
45
  const path = __importStar(require("path"));
47
46
  const identity_1 = require("../identity");
@@ -62,8 +61,10 @@ const update_hooks_1 = require("./update-hooks");
62
61
  const bundle_meta_1 = require("./hooks/bundle-meta");
63
62
  const bundle_manifest_1 = require("../../mind/bundle-manifest");
64
63
  const tasks_1 = require("../../repertoire/tasks");
64
+ const thoughts_1 = require("./thoughts");
65
65
  const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
66
66
  const launchd_1 = require("./launchd");
67
+ const socket_client_1 = require("./socket-client");
67
68
  function stringField(value) {
68
69
  return typeof value === "string" ? value : null;
69
70
  }
@@ -258,6 +259,7 @@ function usage() {
258
259
  " ouro friend list [--agent <name>]",
259
260
  " ouro friend show <id> [--agent <name>]",
260
261
  " ouro friend create --name <name> [--trust <level>] [--agent <name>]",
262
+ " ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
261
263
  " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
262
264
  " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
263
265
  " ouro whoami [--agent <name>]",
@@ -520,6 +522,7 @@ function parseReminderCommand(args) {
520
522
  let scheduledAt;
521
523
  let cadence;
522
524
  let category;
525
+ let requester;
523
526
  for (let i = 1; i < rest.length; i++) {
524
527
  if (rest[i] === "--body" && rest[i + 1]) {
525
528
  body = rest[i + 1];
@@ -537,6 +540,10 @@ function parseReminderCommand(args) {
537
540
  category = rest[i + 1];
538
541
  i += 1;
539
542
  }
543
+ else if (rest[i] === "--requester" && rest[i + 1]) {
544
+ requester = rest[i + 1];
545
+ i += 1;
546
+ }
540
547
  }
541
548
  if (!body)
542
549
  throw new Error(`Usage\n${usage()}`);
@@ -549,6 +556,7 @@ function parseReminderCommand(args) {
549
556
  ...(scheduledAt ? { scheduledAt } : {}),
550
557
  ...(cadence ? { cadence } : {}),
551
558
  ...(category ? { category } : {}),
559
+ ...(requester ? { requester } : {}),
552
560
  ...(agent ? { agent } : {}),
553
561
  };
554
562
  }
@@ -563,6 +571,23 @@ function parseSessionCommand(args) {
563
571
  return { kind: "session.list", ...(agent ? { agent } : {}) };
564
572
  throw new Error(`Usage\n${usage()}`);
565
573
  }
574
+ function parseThoughtsCommand(args) {
575
+ const { agent, rest: cleaned } = extractAgentFlag(args);
576
+ let last;
577
+ let json = false;
578
+ let follow = false;
579
+ for (let i = 0; i < cleaned.length; i++) {
580
+ if (cleaned[i] === "--last" && i + 1 < cleaned.length) {
581
+ last = Number.parseInt(cleaned[i + 1], 10);
582
+ i++;
583
+ }
584
+ if (cleaned[i] === "--json")
585
+ json = true;
586
+ if (cleaned[i] === "--follow" || cleaned[i] === "-f")
587
+ follow = true;
588
+ }
589
+ return { kind: "thoughts", ...(agent ? { agent } : {}), ...(last ? { last } : {}), ...(json ? { json } : {}), ...(follow ? { follow } : {}) };
590
+ }
566
591
  function parseFriendCommand(args) {
567
592
  const { agent, rest: cleaned } = extractAgentFlag(args);
568
593
  const [sub, ...rest] = cleaned;
@@ -630,6 +655,8 @@ function parseOuroCommand(args) {
630
655
  }
631
656
  if (head === "session")
632
657
  return parseSessionCommand(args.slice(1));
658
+ if (head === "thoughts")
659
+ return parseThoughtsCommand(args.slice(1));
633
660
  if (head === "chat") {
634
661
  if (!second)
635
662
  throw new Error(`Usage\n${usage()}`);
@@ -643,38 +670,6 @@ function parseOuroCommand(args) {
643
670
  return parseLinkCommand(args.slice(1));
644
671
  throw new Error(`Unknown command '${args.join(" ")}'.\n${usage()}`);
645
672
  }
646
- function defaultSendCommand(socketPath, command) {
647
- return new Promise((resolve, reject) => {
648
- const client = net.createConnection(socketPath);
649
- let raw = "";
650
- client.on("connect", () => {
651
- client.write(JSON.stringify(command));
652
- client.end();
653
- });
654
- client.on("data", (chunk) => {
655
- raw += chunk.toString("utf-8");
656
- });
657
- client.on("error", reject);
658
- client.on("end", () => {
659
- const trimmed = raw.trim();
660
- if (trimmed.length === 0 && command.kind === "daemon.stop") {
661
- resolve({ ok: true, message: "daemon stopped" });
662
- return;
663
- }
664
- if (trimmed.length === 0) {
665
- reject(new Error("Daemon returned empty response."));
666
- return;
667
- }
668
- try {
669
- const parsed = JSON.parse(trimmed);
670
- resolve(parsed);
671
- }
672
- catch (error) {
673
- reject(error);
674
- }
675
- });
676
- });
677
- }
678
673
  function defaultStartDaemonProcess(socketPath) {
679
674
  const entry = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
680
675
  const child = (0, child_process_1.spawn)("node", [entry, "--socket", socketPath], {
@@ -688,46 +683,6 @@ function defaultWriteStdout(text) {
688
683
  // eslint-disable-next-line no-console -- terminal UX: CLI command output
689
684
  console.log(text);
690
685
  }
691
- function defaultCheckSocketAlive(socketPath) {
692
- return new Promise((resolve) => {
693
- const client = net.createConnection(socketPath);
694
- let raw = "";
695
- let done = false;
696
- const finalize = (alive) => {
697
- if (done)
698
- return;
699
- done = true;
700
- resolve(alive);
701
- };
702
- if ("setTimeout" in client && typeof client.setTimeout === "function") {
703
- client.setTimeout(800, () => {
704
- client.destroy();
705
- finalize(false);
706
- });
707
- }
708
- client.on("connect", () => {
709
- client.write(JSON.stringify({ kind: "daemon.status" }));
710
- client.end();
711
- });
712
- client.on("data", (chunk) => {
713
- raw += chunk.toString("utf-8");
714
- });
715
- client.on("error", () => finalize(false));
716
- client.on("end", () => {
717
- if (raw.trim().length === 0) {
718
- finalize(false);
719
- return;
720
- }
721
- try {
722
- JSON.parse(raw);
723
- finalize(true);
724
- }
725
- catch {
726
- finalize(false);
727
- }
728
- });
729
- });
730
- }
731
686
  function defaultCleanupStaleSocket(socketPath) {
732
687
  if (fs.existsSync(socketPath)) {
733
688
  fs.unlinkSync(socketPath);
@@ -1048,13 +1003,13 @@ async function defaultRunAdoptionSpecialist() {
1048
1003
  }
1049
1004
  }
1050
1005
  /* v8 ignore stop */
1051
- function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
1006
+ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_PATH) {
1052
1007
  return {
1053
1008
  socketPath,
1054
- sendCommand: defaultSendCommand,
1009
+ sendCommand: socket_client_1.sendDaemonCommand,
1055
1010
  startDaemonProcess: defaultStartDaemonProcess,
1056
1011
  writeStdout: defaultWriteStdout,
1057
- checkSocketAlive: defaultCheckSocketAlive,
1012
+ checkSocketAlive: socket_client_1.checkDaemonSocketAlive,
1058
1013
  cleanupStaleSocket: defaultCleanupStaleSocket,
1059
1014
  fallbackPendingMessage: defaultFallbackPendingMessage,
1060
1015
  installSubagents: defaultInstallSubagents,
@@ -1338,6 +1293,7 @@ function executeReminderCommand(command, taskMod) {
1338
1293
  body: command.body,
1339
1294
  scheduledAt: command.scheduledAt,
1340
1295
  cadence: command.cadence,
1296
+ requester: command.requester,
1341
1297
  });
1342
1298
  return `created: ${created}`;
1343
1299
  }
@@ -1536,6 +1492,49 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1536
1492
  }
1537
1493
  /* v8 ignore stop */
1538
1494
  }
1495
+ // ── thoughts (local, no daemon socket needed) ──
1496
+ if (command.kind === "thoughts") {
1497
+ try {
1498
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
1499
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
1500
+ const sessionFilePath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
1501
+ if (command.json) {
1502
+ try {
1503
+ const raw = fs.readFileSync(sessionFilePath, "utf-8");
1504
+ deps.writeStdout(raw);
1505
+ return raw;
1506
+ }
1507
+ catch {
1508
+ const message = "no inner dialog session found";
1509
+ deps.writeStdout(message);
1510
+ return message;
1511
+ }
1512
+ }
1513
+ const turns = (0, thoughts_1.parseInnerDialogSession)(sessionFilePath);
1514
+ const message = (0, thoughts_1.formatThoughtTurns)(turns, command.last ?? 10);
1515
+ deps.writeStdout(message);
1516
+ if (command.follow) {
1517
+ deps.writeStdout("\n\n--- following (ctrl+c to stop) ---\n");
1518
+ /* v8 ignore start -- callback tested via followThoughts unit tests @preserve */
1519
+ const stop = (0, thoughts_1.followThoughts)(sessionFilePath, (formatted) => {
1520
+ deps.writeStdout("\n" + formatted);
1521
+ });
1522
+ /* v8 ignore stop */
1523
+ // Block until process exit; cleanup watcher on SIGINT/SIGTERM
1524
+ return new Promise((resolve) => {
1525
+ const cleanup = () => { stop(); resolve(message); };
1526
+ process.once("SIGINT", cleanup);
1527
+ process.once("SIGTERM", cleanup);
1528
+ });
1529
+ }
1530
+ return message;
1531
+ }
1532
+ catch {
1533
+ const message = "error: no agent context — use --agent <name> to specify";
1534
+ deps.writeStdout(message);
1535
+ return message;
1536
+ }
1537
+ }
1539
1538
  // ── session list (local, no daemon socket needed) ──
1540
1539
  if (command.kind === "session.list") {
1541
1540
  /* v8 ignore start -- production default: requires full identity setup @preserve */
@@ -443,6 +443,13 @@ class OuroDaemon {
443
443
  data: messages,
444
444
  };
445
445
  }
446
+ case "inner.wake":
447
+ await this.processManager.startAgent(command.agent);
448
+ this.processManager.sendToAgent?.(command.agent, { type: "message" });
449
+ return {
450
+ ok: true,
451
+ message: `woke inner dialog for ${command.agent}`,
452
+ };
446
453
  case "chat.connect":
447
454
  await this.processManager.startAgent(command.agent);
448
455
  return {
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_DAEMON_SOCKET_PATH = void 0;
37
+ exports.sendDaemonCommand = sendDaemonCommand;
38
+ exports.checkDaemonSocketAlive = checkDaemonSocketAlive;
39
+ exports.requestInnerWake = requestInnerWake;
40
+ const fs = __importStar(require("fs"));
41
+ const net = __importStar(require("net"));
42
+ const runtime_1 = require("../../nerves/runtime");
43
+ exports.DEFAULT_DAEMON_SOCKET_PATH = "/tmp/ouroboros-daemon.sock";
44
+ function sendDaemonCommand(socketPath, command) {
45
+ (0, runtime_1.emitNervesEvent)({
46
+ component: "daemon",
47
+ event: "daemon.socket_command_start",
48
+ message: "sending daemon command over socket",
49
+ meta: { socketPath, kind: command.kind },
50
+ });
51
+ return new Promise((resolve, reject) => {
52
+ const client = net.createConnection(socketPath);
53
+ let raw = "";
54
+ client.on("connect", () => {
55
+ client.write(JSON.stringify(command));
56
+ client.end();
57
+ });
58
+ client.on("data", (chunk) => {
59
+ raw += chunk.toString("utf-8");
60
+ });
61
+ client.on("error", (error) => {
62
+ (0, runtime_1.emitNervesEvent)({
63
+ level: "error",
64
+ component: "daemon",
65
+ event: "daemon.socket_command_error",
66
+ message: "daemon socket command failed",
67
+ meta: {
68
+ socketPath,
69
+ kind: command.kind,
70
+ error: error.message,
71
+ },
72
+ });
73
+ reject(error);
74
+ });
75
+ client.on("end", () => {
76
+ const trimmed = raw.trim();
77
+ if (trimmed.length === 0 && command.kind === "daemon.stop") {
78
+ (0, runtime_1.emitNervesEvent)({
79
+ component: "daemon",
80
+ event: "daemon.socket_command_end",
81
+ message: "daemon socket command completed",
82
+ meta: { socketPath, kind: command.kind, ok: true },
83
+ });
84
+ resolve({ ok: true, message: "daemon stopped" });
85
+ return;
86
+ }
87
+ if (trimmed.length === 0) {
88
+ const error = new Error("Daemon returned empty response.");
89
+ (0, runtime_1.emitNervesEvent)({
90
+ level: "error",
91
+ component: "daemon",
92
+ event: "daemon.socket_command_error",
93
+ message: "daemon socket command returned empty response",
94
+ meta: {
95
+ socketPath,
96
+ kind: command.kind,
97
+ error: error.message,
98
+ },
99
+ });
100
+ reject(error);
101
+ return;
102
+ }
103
+ try {
104
+ const parsed = JSON.parse(trimmed);
105
+ (0, runtime_1.emitNervesEvent)({
106
+ component: "daemon",
107
+ event: "daemon.socket_command_end",
108
+ message: "daemon socket command completed",
109
+ meta: {
110
+ socketPath,
111
+ kind: command.kind,
112
+ ok: parsed.ok,
113
+ },
114
+ });
115
+ resolve(parsed);
116
+ }
117
+ catch (error) {
118
+ (0, runtime_1.emitNervesEvent)({
119
+ level: "error",
120
+ component: "daemon",
121
+ event: "daemon.socket_command_error",
122
+ message: "daemon socket command returned invalid JSON",
123
+ meta: {
124
+ socketPath,
125
+ kind: command.kind,
126
+ error: error instanceof Error ? error.message : String(error),
127
+ },
128
+ });
129
+ reject(error);
130
+ }
131
+ });
132
+ });
133
+ }
134
+ function checkDaemonSocketAlive(socketPath) {
135
+ (0, runtime_1.emitNervesEvent)({
136
+ component: "daemon",
137
+ event: "daemon.socket_alive_check_start",
138
+ message: "checking daemon socket health",
139
+ meta: { socketPath },
140
+ });
141
+ return new Promise((resolve) => {
142
+ const client = net.createConnection(socketPath);
143
+ let raw = "";
144
+ let done = false;
145
+ const finalize = (alive) => {
146
+ if (done)
147
+ return;
148
+ done = true;
149
+ (0, runtime_1.emitNervesEvent)({
150
+ component: "daemon",
151
+ event: "daemon.socket_alive_check_end",
152
+ message: "daemon socket health check completed",
153
+ meta: { socketPath, alive },
154
+ });
155
+ resolve(alive);
156
+ };
157
+ if ("setTimeout" in client && typeof client.setTimeout === "function") {
158
+ client.setTimeout(800, () => {
159
+ client.destroy();
160
+ finalize(false);
161
+ });
162
+ }
163
+ client.on("connect", () => {
164
+ client.write(JSON.stringify({ kind: "daemon.status" }));
165
+ client.end();
166
+ });
167
+ client.on("data", (chunk) => {
168
+ raw += chunk.toString("utf-8");
169
+ });
170
+ client.on("error", () => finalize(false));
171
+ client.on("end", () => {
172
+ if (raw.trim().length === 0) {
173
+ finalize(false);
174
+ return;
175
+ }
176
+ try {
177
+ JSON.parse(raw);
178
+ finalize(true);
179
+ }
180
+ catch {
181
+ finalize(false);
182
+ }
183
+ });
184
+ });
185
+ }
186
+ async function requestInnerWake(agent, socketPath = exports.DEFAULT_DAEMON_SOCKET_PATH) {
187
+ const socketAvailable = fs.existsSync(socketPath);
188
+ (0, runtime_1.emitNervesEvent)({
189
+ component: "daemon",
190
+ event: "daemon.inner_wake_request",
191
+ message: "requesting daemon-managed inner wake",
192
+ meta: {
193
+ agent,
194
+ socketPath,
195
+ socketAvailable,
196
+ },
197
+ });
198
+ if (!socketAvailable) {
199
+ return null;
200
+ }
201
+ return sendDaemonCommand(socketPath, { kind: "inner.wake", agent });
202
+ }
@@ -0,0 +1,371 @@
1
+ "use strict";
2
+ // Formats inner dialog session turns for human consumption.
3
+ // Used by `ouro thoughts` CLI command to show what the agent has been thinking.
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.formatSurfacedValue = formatSurfacedValue;
39
+ exports.deriveInnerDialogStatus = deriveInnerDialogStatus;
40
+ exports.formatInnerDialogStatus = formatInnerDialogStatus;
41
+ exports.extractThoughtResponseFromMessages = extractThoughtResponseFromMessages;
42
+ exports.parseInnerDialogSession = parseInnerDialogSession;
43
+ exports.formatThoughtTurns = formatThoughtTurns;
44
+ exports.getInnerDialogSessionPath = getInnerDialogSessionPath;
45
+ exports.readInnerDialogStatus = readInnerDialogStatus;
46
+ exports.followThoughts = followThoughts;
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const runtime_1 = require("../../nerves/runtime");
50
+ function contentToText(content) {
51
+ if (typeof content === "string")
52
+ return content;
53
+ if (!Array.isArray(content))
54
+ return "";
55
+ return content
56
+ .map((part) => {
57
+ if (typeof part === "string")
58
+ return part;
59
+ if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
60
+ return part.text;
61
+ }
62
+ return "";
63
+ })
64
+ .join("\n");
65
+ }
66
+ function extractToolFunction(toolCall) {
67
+ if (!toolCall || typeof toolCall !== "object" || !("function" in toolCall))
68
+ return null;
69
+ const maybeFunction = toolCall.function;
70
+ if (!maybeFunction || typeof maybeFunction !== "object")
71
+ return null;
72
+ const name = "name" in maybeFunction && typeof maybeFunction.name === "string"
73
+ ? maybeFunction.name
74
+ : undefined;
75
+ const argumentsValue = "arguments" in maybeFunction && typeof maybeFunction.arguments === "string"
76
+ ? maybeFunction.arguments
77
+ : undefined;
78
+ return { name, arguments: argumentsValue };
79
+ }
80
+ function classifyTurn(userText) {
81
+ if (userText.includes("waking up."))
82
+ return { type: "boot" };
83
+ const taskMatch = /## task: (.+)$/m.exec(userText);
84
+ if (taskMatch)
85
+ return { type: "task", taskId: taskMatch[1] };
86
+ return { type: "heartbeat" };
87
+ }
88
+ function extractToolNames(messages) {
89
+ const names = [];
90
+ for (const msg of messages) {
91
+ if (msg.role === "assistant" && Array.isArray(msg.tool_calls)) {
92
+ for (const tc of msg.tool_calls) {
93
+ const toolFunction = extractToolFunction(tc);
94
+ if (toolFunction?.name && toolFunction.name !== "final_answer")
95
+ names.push(toolFunction.name);
96
+ }
97
+ }
98
+ }
99
+ return names;
100
+ }
101
+ function extractPendingPromptMessages(prompt) {
102
+ return prompt
103
+ .split("\n")
104
+ .map((line) => line.trim())
105
+ .filter((line) => line.startsWith("[pending from "))
106
+ .map((line) => {
107
+ const separator = line.indexOf("]: ");
108
+ return separator >= 0 ? line.slice(separator + 3).trim() : "";
109
+ })
110
+ .filter((line) => line.length > 0);
111
+ }
112
+ function readPendingMessagesForStatus(pendingDir) {
113
+ if (!fs.existsSync(pendingDir))
114
+ return [];
115
+ let entries;
116
+ try {
117
+ entries = fs.readdirSync(pendingDir);
118
+ }
119
+ catch {
120
+ return [];
121
+ }
122
+ const files = [
123
+ ...entries.filter((entry) => entry.endsWith(".json.processing")),
124
+ ...entries.filter((entry) => entry.endsWith(".json") && !entry.endsWith(".json.processing")),
125
+ ].sort((a, b) => a.localeCompare(b));
126
+ const messages = [];
127
+ for (const file of files) {
128
+ try {
129
+ const raw = fs.readFileSync(path.join(pendingDir, file), "utf-8");
130
+ const parsed = JSON.parse(raw);
131
+ if (typeof parsed.content === "string") {
132
+ messages.push(parsed);
133
+ }
134
+ }
135
+ catch {
136
+ // unreadable pending files should not break status queries
137
+ }
138
+ }
139
+ return messages;
140
+ }
141
+ function formatSurfacedValue(text, maxLength = 120) {
142
+ const firstLine = text
143
+ .split("\n")
144
+ .map((line) => line.trim())
145
+ .find((line) => line.length > 0);
146
+ if (!firstLine)
147
+ return "no outward result";
148
+ if (firstLine.length <= maxLength)
149
+ return `"${firstLine}"`;
150
+ return `"${firstLine.slice(0, maxLength - 3)}..."`;
151
+ }
152
+ function deriveInnerDialogStatus(pendingMessages, turns, runtimeState) {
153
+ if (runtimeState?.status === "running") {
154
+ return {
155
+ queue: pendingMessages.length > 0 ? "queued to inner/dialog" : "clear",
156
+ wake: "in progress",
157
+ processing: "started",
158
+ surfaced: "nothing yet",
159
+ };
160
+ }
161
+ if (pendingMessages.length > 0) {
162
+ return {
163
+ queue: "queued to inner/dialog",
164
+ wake: "awaiting inner session",
165
+ processing: "pending",
166
+ surfaced: "nothing yet",
167
+ };
168
+ }
169
+ const latestProcessedPendingTurn = [...turns]
170
+ .reverse()
171
+ .find((turn) => extractPendingPromptMessages(turn.prompt).length > 0);
172
+ if (!latestProcessedPendingTurn) {
173
+ return {
174
+ queue: "clear",
175
+ wake: "idle",
176
+ processing: "idle",
177
+ surfaced: "nothing recent",
178
+ };
179
+ }
180
+ return {
181
+ queue: "clear",
182
+ wake: "completed",
183
+ processing: "processed",
184
+ surfaced: formatSurfacedValue(latestProcessedPendingTurn.response),
185
+ };
186
+ }
187
+ function formatInnerDialogStatus(status) {
188
+ return [
189
+ `queue: ${status.queue}`,
190
+ `wake: ${status.wake}`,
191
+ `processing: ${status.processing}`,
192
+ `surfaced: ${status.surfaced}`,
193
+ ].join("\n");
194
+ }
195
+ /** Extract text from a final_answer tool call's arguments. */
196
+ function extractFinalAnswer(messages) {
197
+ for (let k = messages.length - 1; k >= 0; k--) {
198
+ const msg = messages[k];
199
+ if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
200
+ continue;
201
+ for (const tc of msg.tool_calls) {
202
+ const toolFunction = extractToolFunction(tc);
203
+ if (toolFunction?.name !== "final_answer")
204
+ continue;
205
+ try {
206
+ const parsed = JSON.parse(toolFunction.arguments ?? "{}");
207
+ if (typeof parsed.answer === "string" && parsed.answer.trim())
208
+ return parsed.answer.trim();
209
+ }
210
+ catch {
211
+ // malformed arguments — skip
212
+ }
213
+ }
214
+ }
215
+ return "";
216
+ }
217
+ function extractThoughtResponseFromMessages(messages) {
218
+ const assistantMsgs = messages.filter((message) => message.role === "assistant");
219
+ const lastAssistant = assistantMsgs.reverse().find((message) => contentToText(message.content).trim().length > 0);
220
+ return lastAssistant
221
+ ? contentToText(lastAssistant.content).trim()
222
+ : extractFinalAnswer(messages);
223
+ }
224
+ function parseInnerDialogSession(sessionPath) {
225
+ (0, runtime_1.emitNervesEvent)({
226
+ component: "daemon",
227
+ event: "daemon.thoughts_parse",
228
+ message: "parsing inner dialog session",
229
+ meta: { sessionPath },
230
+ });
231
+ let raw;
232
+ try {
233
+ raw = fs.readFileSync(sessionPath, "utf-8");
234
+ }
235
+ catch {
236
+ return [];
237
+ }
238
+ let data;
239
+ try {
240
+ data = JSON.parse(raw);
241
+ }
242
+ catch {
243
+ return [];
244
+ }
245
+ if (data.version !== 1 || !Array.isArray(data.messages))
246
+ return [];
247
+ const turns = [];
248
+ const messages = data.messages;
249
+ // Walk messages, pairing user → (tool calls) → assistant sequences
250
+ let i = 0;
251
+ while (i < messages.length) {
252
+ const msg = messages[i];
253
+ if (msg.role === "system") {
254
+ i++;
255
+ continue;
256
+ }
257
+ if (msg.role !== "user") {
258
+ i++;
259
+ continue;
260
+ }
261
+ const userText = contentToText(msg.content);
262
+ const classification = classifyTurn(userText);
263
+ // Collect all messages until the next user message (or end)
264
+ const turnMessages = [];
265
+ let j = i + 1;
266
+ while (j < messages.length && messages[j].role !== "user") {
267
+ turnMessages.push(messages[j]);
268
+ j++;
269
+ }
270
+ // Find the last assistant text response in this turn.
271
+ // With tool_choice="required", the response may be inside a final_answer tool call.
272
+ const response = extractThoughtResponseFromMessages(turnMessages);
273
+ const tools = extractToolNames(turnMessages);
274
+ turns.push({
275
+ type: classification.type,
276
+ prompt: userText.trim(),
277
+ response,
278
+ tools,
279
+ ...(classification.taskId ? { taskId: classification.taskId } : {}),
280
+ });
281
+ i = j;
282
+ }
283
+ return turns;
284
+ }
285
+ function formatThoughtTurns(turns, lastN) {
286
+ if (turns.length === 0)
287
+ return "no inner dialog activity";
288
+ const selected = lastN > 0 ? turns.slice(-lastN) : turns;
289
+ /* v8 ignore next -- unreachable: turns.length > 0 checked above, slice always returns ≥1 @preserve */
290
+ if (selected.length === 0)
291
+ return "no inner dialog activity";
292
+ const lines = [];
293
+ for (const turn of selected) {
294
+ const typeLabel = turn.type === "task" && turn.taskId
295
+ ? `task: ${turn.taskId}`
296
+ : turn.type;
297
+ lines.push(`--- ${typeLabel} ---`);
298
+ if (turn.tools.length > 0) {
299
+ lines.push(`tools: ${turn.tools.join(", ")}`);
300
+ }
301
+ if (turn.response) {
302
+ lines.push(turn.response);
303
+ }
304
+ else {
305
+ lines.push("(no response)");
306
+ }
307
+ lines.push("");
308
+ }
309
+ return lines.join("\n").trim();
310
+ }
311
+ function getInnerDialogSessionPath(agentRoot) {
312
+ return path.join(agentRoot, "state", "sessions", "self", "inner", "dialog.json");
313
+ }
314
+ function getInnerDialogRuntimeStatePath(sessionPath) {
315
+ return path.join(path.dirname(sessionPath), "runtime.json");
316
+ }
317
+ function readInnerDialogRuntimeState(runtimePath) {
318
+ try {
319
+ const raw = fs.readFileSync(runtimePath, "utf-8");
320
+ const parsed = JSON.parse(raw);
321
+ if (parsed.status !== "running" && parsed.status !== "idle")
322
+ return null;
323
+ return {
324
+ status: parsed.status,
325
+ reason: parsed.reason === "boot" || parsed.reason === "heartbeat" || parsed.reason === "instinct"
326
+ ? parsed.reason
327
+ : undefined,
328
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : undefined,
329
+ lastCompletedAt: typeof parsed.lastCompletedAt === "string" ? parsed.lastCompletedAt : undefined,
330
+ };
331
+ }
332
+ catch {
333
+ return null;
334
+ }
335
+ }
336
+ function readInnerDialogStatus(sessionPath, pendingDir, runtimePath = getInnerDialogRuntimeStatePath(sessionPath)) {
337
+ const pendingMessages = readPendingMessagesForStatus(pendingDir);
338
+ const turns = parseInnerDialogSession(sessionPath);
339
+ const runtimeState = readInnerDialogRuntimeState(runtimePath);
340
+ return deriveInnerDialogStatus(pendingMessages, turns, runtimeState);
341
+ }
342
+ /**
343
+ * Watch a session file and emit new turns as they appear.
344
+ * Returns a cleanup function that stops the watcher.
345
+ */
346
+ function followThoughts(sessionPath, onNewTurns, pollIntervalMs = 1000) {
347
+ let displayedCount = parseInnerDialogSession(sessionPath).length;
348
+ (0, runtime_1.emitNervesEvent)({
349
+ component: "daemon",
350
+ event: "daemon.thoughts_follow_start",
351
+ message: "started following inner dialog session",
352
+ meta: { sessionPath, initialTurns: displayedCount },
353
+ });
354
+ fs.watchFile(sessionPath, { interval: pollIntervalMs }, () => {
355
+ const turns = parseInnerDialogSession(sessionPath);
356
+ if (turns.length > displayedCount) {
357
+ const newTurns = turns.slice(displayedCount);
358
+ onNewTurns(formatThoughtTurns(newTurns, 0));
359
+ displayedCount = turns.length;
360
+ }
361
+ });
362
+ return () => {
363
+ fs.unwatchFile(sessionPath);
364
+ (0, runtime_1.emitNervesEvent)({
365
+ component: "daemon",
366
+ event: "daemon.thoughts_follow_stop",
367
+ message: "stopped following inner dialog session",
368
+ meta: { sessionPath, totalTurns: displayedCount },
369
+ });
370
+ };
371
+ }
@@ -42,6 +42,8 @@ const skills_1 = require("./skills");
42
42
  const config_1 = require("../heart/config");
43
43
  const runtime_1 = require("../nerves/runtime");
44
44
  const identity_1 = require("../heart/identity");
45
+ const socket_client_1 = require("../heart/daemon/socket-client");
46
+ const thoughts_1 = require("../heart/daemon/thoughts");
45
47
  const tools_1 = require("./coding/tools");
46
48
  const memory_1 = require("../mind/memory");
47
49
  const pending_1 = require("../mind/pending");
@@ -596,6 +598,7 @@ exports.baseToolDefinitions = [
596
598
  channel: { type: "string", description: "the channel: cli, teams, or inner" },
597
599
  key: { type: "string", description: "session key (defaults to 'session')" },
598
600
  messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
601
+ mode: { type: "string", enum: ["transcript", "status"], description: "transcript (default) or lightweight status for self/inner checks" },
599
602
  },
600
603
  required: ["friendId", "channel"],
601
604
  },
@@ -607,6 +610,15 @@ exports.baseToolDefinitions = [
607
610
  const channel = args.channel;
608
611
  const key = args.key || "session";
609
612
  const count = parseInt(args.messageCount || "20", 10);
613
+ const mode = args.mode || "transcript";
614
+ if (mode === "status") {
615
+ if (friendId !== "self" || channel !== "inner") {
616
+ return "status mode is only available for self/inner dialog.";
617
+ }
618
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)((0, identity_1.getAgentRoot)());
619
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
620
+ return (0, thoughts_1.formatInnerDialogStatus)((0, thoughts_1.readInnerDialogStatus)(sessionPath, pendingDir));
621
+ }
610
622
  const sessFile = (0, config_1.resolveSessionPath)(friendId, channel, key);
611
623
  const raw = fs.readFileSync(sessFile, "utf-8");
612
624
  const data = JSON.parse(raw);
@@ -650,7 +662,7 @@ exports.baseToolDefinitions = [
650
662
  },
651
663
  },
652
664
  },
653
- handler: async (args) => {
665
+ handler: async (args, ctx) => {
654
666
  const friendId = args.friendId;
655
667
  const channel = args.channel;
656
668
  const key = args.key || "session";
@@ -675,8 +687,46 @@ exports.baseToolDefinitions = [
675
687
  timestamp: now,
676
688
  };
677
689
  fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
690
+ if (isSelf) {
691
+ let wakeResponse = null;
692
+ try {
693
+ wakeResponse = await (0, socket_client_1.requestInnerWake)(agentName);
694
+ }
695
+ catch {
696
+ wakeResponse = null;
697
+ }
698
+ if (!wakeResponse?.ok) {
699
+ const { runInnerDialogTurn } = await Promise.resolve().then(() => __importStar(require("../senses/inner-dialog")));
700
+ if (ctx?.context?.channel.channel === "inner") {
701
+ queueMicrotask(() => {
702
+ void runInnerDialogTurn({ reason: "instinct" });
703
+ });
704
+ return (0, thoughts_1.formatInnerDialogStatus)({
705
+ queue: "queued to inner/dialog",
706
+ wake: "inline scheduled",
707
+ processing: "pending",
708
+ surfaced: "nothing yet",
709
+ });
710
+ }
711
+ else {
712
+ const turnResult = await runInnerDialogTurn({ reason: "instinct" });
713
+ return (0, thoughts_1.formatInnerDialogStatus)({
714
+ queue: "queued to inner/dialog",
715
+ wake: "inline fallback",
716
+ processing: "processed",
717
+ surfaced: (0, thoughts_1.formatSurfacedValue)((0, thoughts_1.extractThoughtResponseFromMessages)(turnResult?.messages ?? [])),
718
+ });
719
+ }
720
+ }
721
+ return (0, thoughts_1.formatInnerDialogStatus)({
722
+ queue: "queued to inner/dialog",
723
+ wake: "daemon requested",
724
+ processing: "pending",
725
+ surfaced: "nothing yet",
726
+ });
727
+ }
678
728
  const preview = content.length > 80 ? content.slice(0, 80) + "…" : content;
679
- const target = isSelf ? "inner/dialog" : `${channel}/${key}`;
729
+ const target = `${channel}/${key}`;
680
730
  return `message queued for delivery to ${friendId} on ${target}. preview: "${preview}". it will be delivered when their session is next active.`;
681
731
  },
682
732
  },
@@ -6,12 +6,12 @@ const inner_dialog_1 = require("./inner-dialog");
6
6
  const runtime_1 = require("../nerves/runtime");
7
7
  function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options)) {
8
8
  let running = false;
9
- async function run(reason) {
9
+ async function run(reason, taskId) {
10
10
  if (running)
11
11
  return;
12
12
  running = true;
13
13
  try {
14
- await runTurn({ reason });
14
+ await runTurn({ reason, taskId });
15
15
  }
16
16
  catch (error) {
17
17
  (0, runtime_1.emitNervesEvent)({
@@ -37,8 +37,11 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
37
37
  await run("heartbeat");
38
38
  return;
39
39
  }
40
- if (maybeMessage.type === "poke" ||
41
- maybeMessage.type === "chat" ||
40
+ if (maybeMessage.type === "poke") {
41
+ await run("instinct", maybeMessage.taskId);
42
+ return;
43
+ }
44
+ if (maybeMessage.type === "chat" ||
42
45
  maybeMessage.type === "message") {
43
46
  await run("instinct");
44
47
  return;
@@ -37,6 +37,8 @@ exports.loadInnerDialogInstincts = loadInnerDialogInstincts;
37
37
  exports.buildInnerDialogBootstrapMessage = buildInnerDialogBootstrapMessage;
38
38
  exports.buildNonCanonicalCleanupNudge = buildNonCanonicalCleanupNudge;
39
39
  exports.buildInstinctUserMessage = buildInstinctUserMessage;
40
+ exports.readTaskFile = readTaskFile;
41
+ exports.buildTaskTriggeredMessage = buildTaskTriggeredMessage;
40
42
  exports.deriveResumeCheckpoint = deriveResumeCheckpoint;
41
43
  exports.innerDialogSessionPath = innerDialogSessionPath;
42
44
  exports.runInnerDialogTurn = runInnerDialogTurn;
@@ -73,8 +75,16 @@ function readAspirations(agentRoot) {
73
75
  function loadInnerDialogInstincts() {
74
76
  return [...DEFAULT_INNER_DIALOG_INSTINCTS];
75
77
  }
76
- function buildInnerDialogBootstrapMessage(_aspirations, _stateSummary) {
77
- return "waking up. settling in.\n\nwhat needs my attention?";
78
+ function buildInnerDialogBootstrapMessage(aspirations, stateSummary) {
79
+ const lines = ["waking up."];
80
+ if (aspirations) {
81
+ lines.push("", "## what matters to me", aspirations);
82
+ }
83
+ if (stateSummary) {
84
+ lines.push("", "## what i know so far", stateSummary);
85
+ }
86
+ lines.push("", "what needs my attention?");
87
+ return lines.join("\n");
78
88
  }
79
89
  function buildNonCanonicalCleanupNudge(nonCanonicalPaths) {
80
90
  if (nonCanonicalPaths.length === 0)
@@ -98,6 +108,33 @@ function buildInstinctUserMessage(instincts, _reason, state) {
98
108
  }
99
109
  return lines.join("\n");
100
110
  }
111
+ function readTaskFile(agentRoot, taskId) {
112
+ // Task files live in collection subdirectories (one-shots, ongoing, habits).
113
+ // Try each collection, then fall back to root tasks/ for legacy layout.
114
+ const collections = ["one-shots", "ongoing", "habits", ""];
115
+ for (const collection of collections) {
116
+ try {
117
+ return fs.readFileSync(path.join(agentRoot, "tasks", collection, `${taskId}.md`), "utf8").trim();
118
+ }
119
+ catch {
120
+ // not in this collection — try next
121
+ }
122
+ }
123
+ return "";
124
+ }
125
+ function buildTaskTriggeredMessage(taskId, taskContent, checkpoint) {
126
+ const lines = ["a task needs my attention."];
127
+ if (taskContent) {
128
+ lines.push("", `## task: ${taskId}`, taskContent);
129
+ }
130
+ else {
131
+ lines.push("", `## task: ${taskId}`, "(task file not found)");
132
+ }
133
+ if (checkpoint) {
134
+ lines.push("", `last i remember: ${checkpoint}`);
135
+ }
136
+ return lines.join("\n");
137
+ }
101
138
  function contentToText(content) {
102
139
  if (typeof content === "string")
103
140
  return content.trim();
@@ -143,6 +180,31 @@ function deriveResumeCheckpoint(messages) {
143
180
  return firstLine;
144
181
  return `${firstLine.slice(0, 217)}...`;
145
182
  }
183
+ function extractAssistantPreview(messages, maxLength = 120) {
184
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
185
+ if (!lastAssistant)
186
+ return "";
187
+ const text = contentToText(lastAssistant.content);
188
+ if (!text)
189
+ return "";
190
+ /* v8 ignore next -- unreachable: contentToText().trim() guarantees a non-empty line @preserve */
191
+ const firstLine = text.split("\n").find((line) => line.trim().length > 0) ?? "";
192
+ if (firstLine.length <= maxLength)
193
+ return firstLine;
194
+ return `${firstLine.slice(0, maxLength - 3)}...`;
195
+ }
196
+ function extractToolCallNames(messages) {
197
+ const names = [];
198
+ for (const msg of messages) {
199
+ if (msg.role === "assistant" && "tool_calls" in msg && Array.isArray(msg.tool_calls)) {
200
+ for (const tc of msg.tool_calls) {
201
+ if ("function" in tc && tc.function?.name)
202
+ names.push(tc.function.name);
203
+ }
204
+ }
205
+ }
206
+ return [...new Set(names)];
207
+ }
146
208
  function createInnerDialogCallbacks() {
147
209
  return {
148
210
  onModelStart: () => { },
@@ -157,6 +219,31 @@ function createInnerDialogCallbacks() {
157
219
  function innerDialogSessionPath() {
158
220
  return (0, config_1.sessionPath)(pending_1.INNER_DIALOG_PENDING.friendId, pending_1.INNER_DIALOG_PENDING.channel, pending_1.INNER_DIALOG_PENDING.key);
159
221
  }
222
+ function innerDialogRuntimeStatePath(sessionFilePath) {
223
+ return path.join(path.dirname(sessionFilePath), "runtime.json");
224
+ }
225
+ function writeInnerDialogRuntimeState(sessionFilePath, state) {
226
+ const filePath = innerDialogRuntimeStatePath(sessionFilePath);
227
+ try {
228
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
229
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf8");
230
+ }
231
+ catch (error) {
232
+ (0, runtime_1.emitNervesEvent)({
233
+ level: "warn",
234
+ component: "senses",
235
+ event: "senses.inner_dialog_runtime_state_error",
236
+ message: "failed to write inner dialog runtime state",
237
+ meta: {
238
+ status: state.status,
239
+ reason: state.reason ?? null,
240
+ path: filePath,
241
+ /* v8 ignore next -- Node fs APIs throw Error objects for mkdirSync/writeFileSync failures @preserve */
242
+ error: error instanceof Error ? error.message : String(error),
243
+ },
244
+ });
245
+ }
246
+ }
160
247
  // Self-referencing friend record for inner dialog (agent talking to itself).
161
248
  // No real friend to resolve -- this satisfies the pipeline's friend resolver contract.
162
249
  function createSelfFriend(agentName) {
@@ -187,94 +274,119 @@ async function runInnerDialogTurn(options) {
187
274
  const now = options?.now ?? (() => new Date());
188
275
  const reason = options?.reason ?? "heartbeat";
189
276
  const sessionFilePath = innerDialogSessionPath();
190
- const loaded = (0, context_1.loadSession)(sessionFilePath);
191
- const existingMessages = loaded?.messages ? [...loaded.messages] : [];
192
- const instincts = options?.instincts ?? loadInnerDialogInstincts();
193
- const state = {
194
- cycleCount: 1,
195
- resting: false,
196
- lastHeartbeatAt: now().toISOString(),
197
- };
198
- // ── Adapter concern: build user message ──────────────────────────
199
- let userContent;
200
- if (existingMessages.length === 0) {
201
- // Fresh session: bootstrap message with non-canonical cleanup nudge
202
- const aspirations = readAspirations((0, identity_1.getAgentRoot)());
203
- const nonCanonical = (0, bundle_manifest_1.findNonCanonicalBundlePaths)((0, identity_1.getAgentRoot)());
204
- const cleanupNudge = buildNonCanonicalCleanupNudge(nonCanonical);
205
- userContent = [
206
- buildInnerDialogBootstrapMessage(aspirations, "No prior inner dialog session found."),
207
- cleanupNudge,
208
- ].filter(Boolean).join("\n\n");
209
- }
210
- else {
211
- // Resumed session: instinct message with checkpoint context
212
- const assistantTurns = existingMessages.filter((message) => message.role === "assistant").length;
213
- state.cycleCount = assistantTurns + 1;
214
- state.checkpoint = deriveResumeCheckpoint(existingMessages);
215
- userContent = buildInstinctUserMessage(instincts, reason, state);
277
+ writeInnerDialogRuntimeState(sessionFilePath, {
278
+ status: "running",
279
+ reason,
280
+ startedAt: now().toISOString(),
281
+ });
282
+ try {
283
+ const loaded = (0, context_1.loadSession)(sessionFilePath);
284
+ const existingMessages = loaded?.messages ? [...loaded.messages] : [];
285
+ const instincts = options?.instincts ?? loadInnerDialogInstincts();
286
+ const state = {
287
+ cycleCount: 1,
288
+ resting: false,
289
+ lastHeartbeatAt: now().toISOString(),
290
+ };
291
+ // ── Adapter concern: build user message ──────────────────────────
292
+ let userContent;
293
+ if (existingMessages.length === 0) {
294
+ // Fresh session: bootstrap message with non-canonical cleanup nudge
295
+ const aspirations = readAspirations((0, identity_1.getAgentRoot)());
296
+ const nonCanonical = (0, bundle_manifest_1.findNonCanonicalBundlePaths)((0, identity_1.getAgentRoot)());
297
+ const cleanupNudge = buildNonCanonicalCleanupNudge(nonCanonical);
298
+ userContent = [
299
+ buildInnerDialogBootstrapMessage(aspirations, "No prior inner dialog session found."),
300
+ cleanupNudge,
301
+ ].filter(Boolean).join("\n\n");
302
+ }
303
+ else {
304
+ // Resumed session: task-triggered or instinct message with checkpoint context
305
+ const assistantTurns = existingMessages.filter((message) => message.role === "assistant").length;
306
+ state.cycleCount = assistantTurns + 1;
307
+ state.checkpoint = deriveResumeCheckpoint(existingMessages);
308
+ if (options?.taskId) {
309
+ const taskContent = readTaskFile((0, identity_1.getAgentRoot)(), options.taskId);
310
+ userContent = buildTaskTriggeredMessage(options.taskId, taskContent, state.checkpoint);
311
+ }
312
+ else {
313
+ userContent = buildInstinctUserMessage(instincts, reason, state);
314
+ }
315
+ }
316
+ const userMessage = { role: "user", content: userContent };
317
+ // ── Session loader: wraps existing session logic ──────────────────
318
+ const innerCapabilities = (0, channel_1.getChannelCapabilities)("inner");
319
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
320
+ const selfFriend = createSelfFriend((0, identity_1.getAgentName)());
321
+ const selfContext = { friend: selfFriend, channel: innerCapabilities };
322
+ const sessionLoader = {
323
+ loadOrCreate: async () => {
324
+ if (existingMessages.length > 0) {
325
+ return { messages: existingMessages, sessionPath: sessionFilePath };
326
+ }
327
+ // Fresh session: build system prompt
328
+ const systemPrompt = await (0, prompt_1.buildSystem)("inner", { toolChoiceRequired: true });
329
+ return {
330
+ messages: [{ role: "system", content: systemPrompt }],
331
+ sessionPath: sessionFilePath,
332
+ };
333
+ },
334
+ };
335
+ // ── Call shared pipeline ──────────────────────────────────────────
336
+ const callbacks = createInnerDialogCallbacks();
337
+ const traceId = (0, nerves_1.createTraceId)();
338
+ const result = await (0, pipeline_1.handleInboundTurn)({
339
+ channel: "inner",
340
+ capabilities: innerCapabilities,
341
+ messages: [userMessage],
342
+ continuityIngressTexts: [],
343
+ callbacks,
344
+ friendResolver: { resolve: () => Promise.resolve(selfContext) },
345
+ sessionLoader,
346
+ pendingDir,
347
+ friendStore: createNoOpFriendStore(),
348
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
349
+ drainPending: pending_1.drainPending,
350
+ runAgent: core_1.runAgent,
351
+ postTurn: context_1.postTurn,
352
+ accumulateFriendTokens: tokens_1.accumulateFriendTokens,
353
+ signal: options?.signal,
354
+ runAgentOptions: {
355
+ traceId,
356
+ toolChoiceRequired: true,
357
+ skipConfirmation: true,
358
+ },
359
+ });
360
+ const resultMessages = result.messages ?? [];
361
+ const assistantPreview = extractAssistantPreview(resultMessages);
362
+ const toolCalls = extractToolCallNames(resultMessages);
363
+ (0, runtime_1.emitNervesEvent)({
364
+ component: "senses",
365
+ event: "senses.inner_dialog_turn",
366
+ message: "inner dialog turn completed",
367
+ meta: {
368
+ reason,
369
+ session: sessionFilePath,
370
+ ...(options?.taskId && { taskId: options.taskId }),
371
+ ...(assistantPreview && { assistantPreview }),
372
+ ...(toolCalls.length > 0 && { toolCalls }),
373
+ ...(result.usage && {
374
+ promptTokens: result.usage.input_tokens,
375
+ completionTokens: result.usage.output_tokens,
376
+ totalTokens: result.usage.total_tokens,
377
+ }),
378
+ },
379
+ });
380
+ return {
381
+ messages: resultMessages,
382
+ usage: result.usage,
383
+ sessionPath: result.sessionPath ?? sessionFilePath,
384
+ };
216
385
  }
217
- // ── Adapter concern: inbox drain (inner-dialog-specific) ─────────
218
- const inboxMessages = options?.drainInbox?.() ?? [];
219
- if (inboxMessages.length > 0) {
220
- const section = inboxMessages
221
- .map((msg) => `- **${msg.from}**: ${msg.content}`)
222
- .join("\n");
223
- userContent = `${userContent}\n\n## incoming messages\n${section}`;
386
+ finally {
387
+ writeInnerDialogRuntimeState(sessionFilePath, {
388
+ status: "idle",
389
+ lastCompletedAt: now().toISOString(),
390
+ });
224
391
  }
225
- const userMessage = { role: "user", content: userContent };
226
- // ── Session loader: wraps existing session logic ──────────────────
227
- const innerCapabilities = (0, channel_1.getChannelCapabilities)("inner");
228
- const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
229
- const selfFriend = createSelfFriend((0, identity_1.getAgentName)());
230
- const selfContext = { friend: selfFriend, channel: innerCapabilities };
231
- const sessionLoader = {
232
- loadOrCreate: async () => {
233
- if (existingMessages.length > 0) {
234
- return { messages: existingMessages, sessionPath: sessionFilePath };
235
- }
236
- // Fresh session: build system prompt
237
- const systemPrompt = await (0, prompt_1.buildSystem)("inner", { toolChoiceRequired: true });
238
- return {
239
- messages: [{ role: "system", content: systemPrompt }],
240
- sessionPath: sessionFilePath,
241
- };
242
- },
243
- };
244
- // ── Call shared pipeline ──────────────────────────────────────────
245
- const callbacks = createInnerDialogCallbacks();
246
- const traceId = (0, nerves_1.createTraceId)();
247
- const result = await (0, pipeline_1.handleInboundTurn)({
248
- channel: "inner",
249
- capabilities: innerCapabilities,
250
- messages: [userMessage],
251
- continuityIngressTexts: [],
252
- callbacks,
253
- friendResolver: { resolve: () => Promise.resolve(selfContext) },
254
- sessionLoader,
255
- pendingDir,
256
- friendStore: createNoOpFriendStore(),
257
- enforceTrustGate: trust_gate_1.enforceTrustGate,
258
- drainPending: pending_1.drainPending,
259
- runAgent: core_1.runAgent,
260
- postTurn: context_1.postTurn,
261
- accumulateFriendTokens: tokens_1.accumulateFriendTokens,
262
- signal: options?.signal,
263
- runAgentOptions: {
264
- traceId,
265
- toolChoiceRequired: true,
266
- skipConfirmation: true,
267
- },
268
- });
269
- (0, runtime_1.emitNervesEvent)({
270
- component: "senses",
271
- event: "senses.inner_dialog_turn",
272
- message: "inner dialog turn completed",
273
- meta: { reason, session: sessionFilePath },
274
- });
275
- return {
276
- messages: result.messages ?? [],
277
- usage: result.usage,
278
- sessionPath: result.sessionPath ?? sessionFilePath,
279
- };
280
392
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.45",
3
+ "version": "0.1.0-alpha.47",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",