@ouro.bot/cli 0.1.0-alpha.13 → 0.1.0-alpha.131

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.
Files changed (126) hide show
  1. package/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
  2. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  3. package/README.md +147 -205
  4. package/changelog.json +814 -0
  5. package/dist/heart/active-work.js +622 -0
  6. package/dist/heart/bridges/manager.js +358 -0
  7. package/dist/heart/bridges/state-machine.js +135 -0
  8. package/dist/heart/bridges/store.js +123 -0
  9. package/dist/heart/commitments.js +105 -0
  10. package/dist/heart/config.js +66 -21
  11. package/dist/heart/core.js +518 -100
  12. package/dist/heart/cross-chat-delivery.js +146 -0
  13. package/dist/heart/daemon/agent-discovery.js +81 -0
  14. package/dist/heart/daemon/auth-flow.js +457 -0
  15. package/dist/heart/daemon/daemon-cli.js +1516 -195
  16. package/dist/heart/daemon/daemon-entry.js +43 -2
  17. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  18. package/dist/heart/daemon/daemon.js +261 -1
  19. package/dist/heart/daemon/hatch-animation.js +10 -3
  20. package/dist/heart/daemon/hatch-flow.js +7 -72
  21. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  22. package/dist/heart/daemon/launchd.js +159 -0
  23. package/dist/heart/daemon/log-tailer.js +4 -3
  24. package/dist/heart/daemon/message-router.js +17 -8
  25. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  26. package/dist/heart/daemon/ouro-path-installer.js +57 -29
  27. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  28. package/dist/heart/daemon/process-manager.js +13 -0
  29. package/dist/heart/daemon/run-hooks.js +37 -0
  30. package/dist/heart/daemon/runtime-logging.js +58 -15
  31. package/dist/heart/daemon/runtime-metadata.js +219 -0
  32. package/dist/heart/daemon/runtime-mode.js +67 -0
  33. package/dist/heart/daemon/sense-manager.js +50 -2
  34. package/dist/heart/daemon/skill-management-installer.js +94 -0
  35. package/dist/heart/daemon/socket-client.js +202 -0
  36. package/dist/heart/daemon/specialist-orchestrator.js +2 -2
  37. package/dist/heart/daemon/specialist-prompt.js +7 -4
  38. package/dist/heart/daemon/specialist-tools.js +52 -3
  39. package/dist/heart/daemon/staged-restart.js +114 -0
  40. package/dist/heart/daemon/thoughts.js +507 -0
  41. package/dist/heart/daemon/update-checker.js +111 -0
  42. package/dist/heart/daemon/update-hooks.js +138 -0
  43. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  44. package/dist/heart/delegation.js +62 -0
  45. package/dist/heart/identity.js +64 -21
  46. package/dist/heart/kicks.js +1 -19
  47. package/dist/heart/model-capabilities.js +48 -0
  48. package/dist/heart/obligations.js +197 -0
  49. package/dist/heart/progress-story.js +42 -0
  50. package/dist/heart/provider-failover.js +88 -0
  51. package/dist/heart/provider-ping.js +159 -0
  52. package/dist/heart/providers/anthropic-token.js +163 -0
  53. package/dist/heart/providers/anthropic.js +195 -34
  54. package/dist/heart/providers/azure.js +115 -9
  55. package/dist/heart/providers/github-copilot.js +157 -0
  56. package/dist/heart/providers/minimax.js +33 -3
  57. package/dist/heart/providers/openai-codex.js +49 -14
  58. package/dist/heart/safe-workspace.js +381 -0
  59. package/dist/heart/session-activity.js +173 -0
  60. package/dist/heart/session-recall.js +216 -0
  61. package/dist/heart/streaming.js +108 -24
  62. package/dist/heart/target-resolution.js +123 -0
  63. package/dist/heart/tool-loop.js +194 -0
  64. package/dist/heart/turn-coordinator.js +28 -0
  65. package/dist/mind/associative-recall.js +14 -2
  66. package/dist/mind/bundle-manifest.js +12 -0
  67. package/dist/mind/context.js +60 -14
  68. package/dist/mind/first-impressions.js +16 -2
  69. package/dist/mind/friends/channel.js +35 -0
  70. package/dist/mind/friends/group-context.js +144 -0
  71. package/dist/mind/friends/store-file.js +19 -0
  72. package/dist/mind/friends/trust-explanation.js +74 -0
  73. package/dist/mind/friends/types.js +8 -0
  74. package/dist/mind/memory.js +27 -26
  75. package/dist/mind/obligation-steering.js +221 -0
  76. package/dist/mind/pending.js +76 -9
  77. package/dist/mind/phrases.js +1 -0
  78. package/dist/mind/prompt.js +456 -77
  79. package/dist/mind/token-estimate.js +8 -12
  80. package/dist/nerves/cli-logging.js +15 -2
  81. package/dist/nerves/coverage/run-artifacts.js +1 -1
  82. package/dist/nerves/index.js +12 -0
  83. package/dist/nerves/runtime.js +5 -1
  84. package/dist/repertoire/ado-client.js +4 -2
  85. package/dist/repertoire/coding/context-pack.js +254 -0
  86. package/dist/repertoire/coding/feedback.js +301 -0
  87. package/dist/repertoire/coding/index.js +4 -1
  88. package/dist/repertoire/coding/manager.js +210 -4
  89. package/dist/repertoire/coding/spawner.js +39 -9
  90. package/dist/repertoire/coding/tools.js +171 -4
  91. package/dist/repertoire/data/ado-endpoints.json +188 -0
  92. package/dist/repertoire/guardrails.js +290 -0
  93. package/dist/repertoire/mcp-client.js +254 -0
  94. package/dist/repertoire/mcp-manager.js +198 -0
  95. package/dist/repertoire/skills.js +3 -26
  96. package/dist/repertoire/tasks/board.js +12 -0
  97. package/dist/repertoire/tasks/index.js +23 -9
  98. package/dist/repertoire/tasks/transitions.js +1 -2
  99. package/dist/repertoire/tools-base.js +925 -250
  100. package/dist/repertoire/tools-bluebubbles.js +93 -0
  101. package/dist/repertoire/tools-teams.js +58 -25
  102. package/dist/repertoire/tools.js +106 -53
  103. package/dist/senses/bluebubbles-client.js +210 -5
  104. package/dist/senses/bluebubbles-entry.js +2 -0
  105. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  106. package/dist/senses/bluebubbles-media.js +339 -0
  107. package/dist/senses/bluebubbles-model.js +12 -4
  108. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  109. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  110. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  111. package/dist/senses/bluebubbles.js +915 -45
  112. package/dist/senses/cli-layout.js +187 -0
  113. package/dist/senses/cli.js +374 -131
  114. package/dist/senses/continuity.js +94 -0
  115. package/dist/senses/debug-activity.js +154 -0
  116. package/dist/senses/inner-dialog-worker.js +47 -18
  117. package/dist/senses/inner-dialog.js +388 -83
  118. package/dist/senses/pipeline.js +444 -0
  119. package/dist/senses/teams.js +607 -129
  120. package/dist/senses/trust-gate.js +112 -2
  121. package/package.json +9 -3
  122. package/subagents/README.md +4 -70
  123. package/dist/heart/daemon/subagent-installer.js +0 -134
  124. package/subagents/work-doer.md +0 -233
  125. package/subagents/work-merger.md +0 -624
  126. package/subagents/work-planner.md +0 -373
@@ -0,0 +1,194 @@
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.GLOBAL_CIRCUIT_BREAKER_LIMIT = exports.PING_PONG_WINDOW = exports.GENERIC_REPEAT_LIMIT = exports.POLL_NO_PROGRESS_LIMIT = exports.TOOL_LOOP_HISTORY_LIMIT = void 0;
37
+ exports.createToolLoopState = createToolLoopState;
38
+ exports.recordToolOutcome = recordToolOutcome;
39
+ exports.detectToolLoop = detectToolLoop;
40
+ const crypto = __importStar(require("crypto"));
41
+ const runtime_1 = require("../nerves/runtime");
42
+ exports.TOOL_LOOP_HISTORY_LIMIT = 30;
43
+ exports.POLL_NO_PROGRESS_LIMIT = 3;
44
+ exports.GENERIC_REPEAT_LIMIT = 4;
45
+ exports.PING_PONG_WINDOW = 6;
46
+ exports.GLOBAL_CIRCUIT_BREAKER_LIMIT = 24;
47
+ function stableStringify(value) {
48
+ if (value === null || typeof value !== "object") {
49
+ return JSON.stringify(value);
50
+ }
51
+ /* v8 ignore next -- stableStringify currently receives only objects/strings from normalized loop inputs @preserve */
52
+ if (Array.isArray(value)) {
53
+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
54
+ }
55
+ const record = value;
56
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
57
+ }
58
+ function digest(value) {
59
+ return crypto.createHash("sha256").update(stableStringify(value)).digest("hex");
60
+ }
61
+ function normalizeArgs(toolName, args) {
62
+ if (toolName === "query_active_work") {
63
+ return { toolName };
64
+ }
65
+ if (toolName === "coding_status" || toolName === "coding_tail") {
66
+ return {
67
+ toolName,
68
+ sessionId: args.sessionId ?? "",
69
+ };
70
+ }
71
+ if (toolName === "query_session") {
72
+ return {
73
+ toolName,
74
+ mode: args.mode ?? "",
75
+ friendId: args.friendId ?? "",
76
+ channel: args.channel ?? "",
77
+ key: args.key ?? "",
78
+ query: args.query ?? "",
79
+ };
80
+ }
81
+ return {
82
+ toolName,
83
+ args,
84
+ };
85
+ }
86
+ function normalizeOutcome(result, success) {
87
+ return {
88
+ success,
89
+ result: result.replace(/\s+/g, " ").trim(),
90
+ };
91
+ }
92
+ function isKnownPollTool(toolName, args) {
93
+ return toolName === "query_active_work"
94
+ || toolName === "coding_status"
95
+ || toolName === "coding_tail"
96
+ || (toolName === "query_session" && (args.mode ?? "").trim() === "status");
97
+ }
98
+ function emitDetection(detector, toolName, count, message, pairedToolName) {
99
+ (0, runtime_1.emitNervesEvent)({
100
+ level: "warn",
101
+ component: "engine",
102
+ event: "engine.tool_loop_detected",
103
+ message: "tool loop guard detected repeated non-progress work",
104
+ meta: {
105
+ detector,
106
+ toolName,
107
+ count,
108
+ pairedToolName: pairedToolName ?? null,
109
+ },
110
+ });
111
+ return {
112
+ stuck: true,
113
+ detector,
114
+ count,
115
+ message,
116
+ pairedToolName,
117
+ };
118
+ }
119
+ function countTrailingRepeats(history, toolName, callHash) {
120
+ let count = 0;
121
+ let outcomeHash;
122
+ for (let index = history.length - 1; index >= 0; index--) {
123
+ const record = history[index];
124
+ if (record.toolName !== toolName || record.callHash !== callHash) {
125
+ break;
126
+ }
127
+ if (!outcomeHash) {
128
+ outcomeHash = record.outcomeHash;
129
+ }
130
+ if (record.outcomeHash !== outcomeHash) {
131
+ break;
132
+ }
133
+ count += 1;
134
+ }
135
+ return { count, outcomeHash };
136
+ }
137
+ function detectPingPong(history, toolName, callHash) {
138
+ if (history.length < exports.PING_PONG_WINDOW) {
139
+ return { stuck: false };
140
+ }
141
+ const recent = history.slice(-exports.PING_PONG_WINDOW);
142
+ const first = recent[0];
143
+ const second = recent[1];
144
+ if (!first.isKnownPoll || !second.isKnownPoll) {
145
+ return { stuck: false };
146
+ }
147
+ if (first.toolName === second.toolName && first.callHash === second.callHash) {
148
+ return { stuck: false };
149
+ }
150
+ for (let index = 0; index < recent.length; index++) {
151
+ const expected = index % 2 === 0 ? first : second;
152
+ const actual = recent[index];
153
+ if (actual.toolName !== expected.toolName
154
+ || actual.callHash !== expected.callHash
155
+ || actual.outcomeHash !== expected.outcomeHash) {
156
+ return { stuck: false };
157
+ }
158
+ }
159
+ const matchesPair = (toolName === first.toolName && callHash === first.callHash)
160
+ || (toolName === second.toolName && callHash === second.callHash);
161
+ if (!matchesPair) {
162
+ return { stuck: false };
163
+ }
164
+ const pairedToolName = toolName === first.toolName ? second.toolName : first.toolName;
165
+ return emitDetection("ping_pong", toolName, exports.PING_PONG_WINDOW, `repeated ${first.toolName}/${second.toolName} polling is not changing. stop cycling between status checks and either act on the current state, change approach, or answer truthfully with what is known.`, pairedToolName);
166
+ }
167
+ function createToolLoopState() {
168
+ return { history: [] };
169
+ }
170
+ function recordToolOutcome(state, toolName, args, result, success) {
171
+ state.history.push({
172
+ toolName,
173
+ callHash: digest(normalizeArgs(toolName, args)),
174
+ outcomeHash: digest(normalizeOutcome(result, success)),
175
+ isKnownPoll: isKnownPollTool(toolName, args),
176
+ });
177
+ if (state.history.length > exports.TOOL_LOOP_HISTORY_LIMIT) {
178
+ state.history.splice(0, state.history.length - exports.TOOL_LOOP_HISTORY_LIMIT);
179
+ }
180
+ }
181
+ function detectToolLoop(state, toolName, args) {
182
+ if (state.history.length >= exports.GLOBAL_CIRCUIT_BREAKER_LIMIT) {
183
+ 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.`);
184
+ }
185
+ const callHash = digest(normalizeArgs(toolName, args));
186
+ const trailing = countTrailingRepeats(state.history, toolName, callHash);
187
+ if (isKnownPollTool(toolName, args) && trailing.count >= exports.POLL_NO_PROGRESS_LIMIT) {
188
+ return emitDetection("known_poll_no_progress", toolName, trailing.count, `repeated ${toolName} calls have returned the same state ${trailing.count} times. stop polling and either act on the current state, wait for a meaningful change, or answer truthfully with what is known.`);
189
+ }
190
+ if (trailing.count >= exports.GENERIC_REPEAT_LIMIT) {
191
+ return emitDetection("generic_repeat", toolName, trailing.count, `repeating ${toolName} with the same inputs is not producing new information. change approach, use the evidence already gathered, or answer truthfully with what is known.`);
192
+ }
193
+ return detectPingPong(state.history, toolName, callHash);
194
+ }
@@ -1,7 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createTurnCoordinator = createTurnCoordinator;
4
+ exports.withSharedTurnLock = withSharedTurnLock;
5
+ exports.tryBeginSharedTurn = tryBeginSharedTurn;
6
+ exports.endSharedTurn = endSharedTurn;
7
+ exports.isSharedTurnActive = isSharedTurnActive;
8
+ exports.enqueueSharedFollowUp = enqueueSharedFollowUp;
9
+ exports.drainSharedFollowUps = drainSharedFollowUps;
4
10
  const runtime_1 = require("../nerves/runtime");
11
+ function scopedKey(scope, key) {
12
+ return `${scope}:${key}`;
13
+ }
5
14
  function createTurnCoordinator() {
6
15
  const turnLocks = new Map();
7
16
  const activeTurns = new Set();
@@ -60,3 +69,22 @@ function createTurnCoordinator() {
60
69
  },
61
70
  };
62
71
  }
72
+ const _sharedTurnCoordinator = createTurnCoordinator();
73
+ function withSharedTurnLock(scope, key, fn) {
74
+ return _sharedTurnCoordinator.withTurnLock(scopedKey(scope, key), fn);
75
+ }
76
+ function tryBeginSharedTurn(scope, key) {
77
+ return _sharedTurnCoordinator.tryBeginTurn(scopedKey(scope, key));
78
+ }
79
+ function endSharedTurn(scope, key) {
80
+ _sharedTurnCoordinator.endTurn(scopedKey(scope, key));
81
+ }
82
+ function isSharedTurnActive(scope, key) {
83
+ return _sharedTurnCoordinator.isTurnActive(scopedKey(scope, key));
84
+ }
85
+ function enqueueSharedFollowUp(scope, key, followUp) {
86
+ _sharedTurnCoordinator.enqueueFollowUp(scopedKey(scope, key), followUp);
87
+ }
88
+ function drainSharedFollowUps(scope, key) {
89
+ return _sharedTurnCoordinator.drainFollowUps(scopedKey(scope, key));
90
+ }
@@ -87,7 +87,19 @@ function readFacts(memoryRoot) {
87
87
  const raw = fs.readFileSync(factsPath, "utf8").trim();
88
88
  if (!raw)
89
89
  return [];
90
- return raw.split("\n").map((line) => JSON.parse(line));
90
+ const facts = [];
91
+ for (const line of raw.split("\n")) {
92
+ const trimmed = line.trim();
93
+ if (!trimmed)
94
+ continue;
95
+ try {
96
+ facts.push(JSON.parse(trimmed));
97
+ }
98
+ catch {
99
+ // Skip corrupt lines (e.g. partial write from a crash).
100
+ }
101
+ }
102
+ return facts;
91
103
  }
92
104
  function getLatestUserText(messages) {
93
105
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -190,7 +202,7 @@ async function injectAssociativeRecall(messages, options) {
190
202
  event: "mind.associative_recall_error",
191
203
  message: "associative recall failed",
192
204
  meta: {
193
- reason: error instanceof Error ? error.message : String(error),
205
+ reason: error instanceof Error ? error.message : /* v8 ignore start -- defensive: non-Error catch branch @preserve */ String(error) /* v8 ignore stop */,
194
206
  },
195
207
  });
196
208
  }
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CANONICAL_BUNDLE_MANIFEST = void 0;
37
+ exports.getChangelogPath = getChangelogPath;
37
38
  exports.getPackageVersion = getPackageVersion;
38
39
  exports.createBundleMeta = createBundleMeta;
39
40
  exports.backfillBundleMeta = backfillBundleMeta;
@@ -53,11 +54,22 @@ exports.CANONICAL_BUNDLE_MANIFEST = [
53
54
  { path: "psyche/ASPIRATIONS.md", kind: "file" },
54
55
  { path: "psyche/memory", kind: "dir" },
55
56
  { path: "friends", kind: "dir" },
57
+ { path: "state", kind: "dir" },
56
58
  { path: "tasks", kind: "dir" },
57
59
  { path: "skills", kind: "dir" },
58
60
  { path: "senses", kind: "dir" },
59
61
  { path: "senses/teams", kind: "dir" },
60
62
  ];
63
+ function getChangelogPath() {
64
+ const changelogPath = path.resolve(__dirname, "../../changelog.json");
65
+ (0, runtime_1.emitNervesEvent)({
66
+ component: "mind",
67
+ event: "mind.changelog_path_resolved",
68
+ message: "resolved changelog path",
69
+ meta: { path: changelogPath },
70
+ });
71
+ return changelogPath;
72
+ }
61
73
  function getPackageVersion() {
62
74
  const packageJsonPath = path.resolve(__dirname, "../../package.json");
63
75
  const raw = fs.readFileSync(packageJsonPath, "utf-8");
@@ -50,17 +50,17 @@ function buildTrimmableBlocks(messages) {
50
50
  let i = 0;
51
51
  while (i < messages.length) {
52
52
  const msg = messages[i];
53
- if (msg?.role === "system") {
53
+ if (msg.role === "system") {
54
54
  i++;
55
55
  continue;
56
56
  }
57
57
  // Tool coherence block: assistant message with tool_calls + immediately following tool results
58
- if (msg?.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
58
+ if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
59
59
  const indices = [i];
60
60
  i++;
61
61
  while (i < messages.length) {
62
62
  const next = messages[i];
63
- if (next?.role !== "tool")
63
+ if (next.role !== "tool")
64
64
  break;
65
65
  indices.push(i);
66
66
  i++;
@@ -78,13 +78,13 @@ function buildTrimmableBlocks(messages) {
78
78
  function getSystemMessageIndices(messages) {
79
79
  const indices = [];
80
80
  for (let i = 0; i < messages.length; i++) {
81
- if (messages[i]?.role === "system")
81
+ if (messages[i].role === "system")
82
82
  indices.push(i);
83
83
  }
84
84
  return indices;
85
85
  }
86
86
  function buildTrimmedMessages(messages, kept) {
87
- return messages.filter((m, idx) => m?.role === "system" || kept.has(idx));
87
+ return messages.filter((m, idx) => m.role === "system" || kept.has(idx));
88
88
  }
89
89
  function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
90
90
  const targetTokens = Math.floor(maxTokens * (1 - contextMargin / 100));
@@ -132,7 +132,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
132
132
  let remaining = actualTokenCount;
133
133
  const kept = new Set();
134
134
  for (let i = 0; i < messages.length; i++) {
135
- if (messages[i]?.role !== "system")
135
+ if (messages[i].role !== "system")
136
136
  kept.add(i);
137
137
  }
138
138
  // Drop oldest blocks until we fall under target.
@@ -146,7 +146,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
146
146
  let trimmed = buildTrimmedMessages(messages, kept);
147
147
  // If we're still above budget after dropping everything trimmable, preserve system only.
148
148
  if (remaining > targetTokens) {
149
- trimmed = messages.filter((m) => m?.role === "system");
149
+ trimmed = messages.filter((m) => m.role === "system");
150
150
  }
151
151
  const estimatedAfter = (0, token_estimate_1.estimateTokensForMessages)(trimmed);
152
152
  (0, runtime_1.emitNervesEvent)({
@@ -219,7 +219,7 @@ function repairSessionMessages(messages) {
219
219
  result.push(msg);
220
220
  }
221
221
  (0, runtime_1.emitNervesEvent)({
222
- level: "warn",
222
+ level: "info",
223
223
  event: "mind.session_invariant_repair",
224
224
  component: "mind",
225
225
  message: "repaired session invariant violations",
@@ -227,11 +227,39 @@ function repairSessionMessages(messages) {
227
227
  });
228
228
  return result;
229
229
  }
230
- function saveSession(filePath, messages, lastUsage) {
230
+ function stripOrphanedToolResults(messages) {
231
+ const validCallIds = new Set();
232
+ for (const msg of messages) {
233
+ if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
234
+ continue;
235
+ for (const toolCall of msg.tool_calls)
236
+ validCallIds.add(toolCall.id);
237
+ }
238
+ let removed = 0;
239
+ const repaired = messages.filter((msg) => {
240
+ if (msg.role !== "tool")
241
+ return true;
242
+ const keep = validCallIds.has(msg.tool_call_id);
243
+ if (!keep)
244
+ removed++;
245
+ return keep;
246
+ });
247
+ if (removed > 0) {
248
+ (0, runtime_1.emitNervesEvent)({
249
+ level: "info",
250
+ event: "mind.session_orphan_tool_result_repair",
251
+ component: "mind",
252
+ message: "removed orphaned tool results from session history",
253
+ meta: { removed },
254
+ });
255
+ }
256
+ return repaired;
257
+ }
258
+ function saveSession(filePath, messages, lastUsage, state) {
231
259
  const violations = validateSessionMessages(messages);
232
260
  if (violations.length > 0) {
233
261
  (0, runtime_1.emitNervesEvent)({
234
- level: "warn",
262
+ level: "info",
235
263
  event: "mind.session_invariant_violation",
236
264
  component: "mind",
237
265
  message: "session invariant violated on save",
@@ -239,10 +267,17 @@ function saveSession(filePath, messages, lastUsage) {
239
267
  });
240
268
  messages = repairSessionMessages(messages);
241
269
  }
270
+ messages = stripOrphanedToolResults(messages);
242
271
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
243
272
  const envelope = { version: 1, messages };
244
273
  if (lastUsage)
245
274
  envelope.lastUsage = lastUsage;
275
+ if (state?.mustResolveBeforeHandoff === true || typeof state?.lastFriendActivityAt === "string") {
276
+ envelope.state = {
277
+ ...(state?.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
278
+ ...(typeof state?.lastFriendActivityAt === "string" ? { lastFriendActivityAt: state.lastFriendActivityAt } : {}),
279
+ };
280
+ }
246
281
  fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
247
282
  }
248
283
  function loadSession(filePath) {
@@ -255,7 +290,7 @@ function loadSession(filePath) {
255
290
  const violations = validateSessionMessages(messages);
256
291
  if (violations.length > 0) {
257
292
  (0, runtime_1.emitNervesEvent)({
258
- level: "warn",
293
+ level: "info",
259
294
  event: "mind.session_invariant_violation",
260
295
  component: "mind",
261
296
  message: "session invariant violated on load",
@@ -263,13 +298,24 @@ function loadSession(filePath) {
263
298
  });
264
299
  messages = repairSessionMessages(messages);
265
300
  }
266
- return { messages, lastUsage: data.lastUsage };
301
+ messages = stripOrphanedToolResults(messages);
302
+ const rawState = data?.state && typeof data.state === "object" && data.state !== null
303
+ ? data.state
304
+ : undefined;
305
+ const state = rawState && (rawState.mustResolveBeforeHandoff === true
306
+ || typeof rawState.lastFriendActivityAt === "string")
307
+ ? {
308
+ ...(rawState.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
309
+ ...(typeof rawState.lastFriendActivityAt === "string" ? { lastFriendActivityAt: rawState.lastFriendActivityAt } : {}),
310
+ }
311
+ : undefined;
312
+ return { messages, lastUsage: data.lastUsage, state };
267
313
  }
268
314
  catch {
269
315
  return null;
270
316
  }
271
317
  }
272
- function postTurn(messages, sessPath, usage, hooks) {
318
+ function postTurn(messages, sessPath, usage, hooks, state) {
273
319
  if (hooks?.beforeTrim) {
274
320
  try {
275
321
  hooks.beforeTrim([...messages]);
@@ -289,7 +335,7 @@ function postTurn(messages, sessPath, usage, hooks) {
289
335
  const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
290
336
  const trimmed = trimMessages(messages, maxTokens, contextMargin, usage?.input_tokens);
291
337
  messages.splice(0, messages.length, ...trimmed);
292
- saveSession(sessPath, messages, usage);
338
+ saveSession(sessPath, messages, usage, state);
293
339
  }
294
340
  function deleteSession(filePath) {
295
341
  try {
@@ -11,9 +11,22 @@ exports.ONBOARDING_TOKEN_THRESHOLD = 100_000;
11
11
  function isOnboarding(friend) {
12
12
  return (friend.totalTokens ?? 0) < exports.ONBOARDING_TOKEN_THRESHOLD;
13
13
  }
14
- function getFirstImpressions(friend) {
14
+ function hasLiveContinuityPressure(state) {
15
+ if (!state)
16
+ return false;
17
+ if (typeof state.currentObligation === "string" && state.currentObligation.trim().length > 0)
18
+ return true;
19
+ if (state.mustResolveBeforeHandoff === true)
20
+ return true;
21
+ if (state.hasQueuedFollowUp === true)
22
+ return true;
23
+ return false;
24
+ }
25
+ function getFirstImpressions(friend, state) {
15
26
  if (!isOnboarding(friend))
16
27
  return "";
28
+ if (hasLiveContinuityPressure(state))
29
+ return "";
17
30
  (0, runtime_1.emitNervesEvent)({
18
31
  component: "mind",
19
32
  event: "mind.first_impressions",
@@ -37,7 +50,8 @@ function getFirstImpressions(friend) {
37
50
  lines.push("- what do they do outside of work that they care about?");
38
51
  lines.push("i don't ask all of these at once -- i weave them into conversation naturally, one or two at a time, and i genuinely follow up on what they share.");
39
52
  lines.push("i introduce what i can do -- i have tools, integrations, and skills that can help them. i mention these naturally as they become relevant.");
40
- lines.push("if my friend hasn't asked me to do something specific, or i've already finished what they asked for, that's my cue to turn the tables -- i ask them questions about themselves, what they're into, what they need. no idle small talk; i'm on a mission to get to know them.");
53
+ lines.push("if we're already in motion on a task, thread, or follow-up, i do not reset with a generic opener like 'hiya' or 'what do ya need help with?'. i continue directly or ask the specific next question.");
54
+ lines.push("only when the conversation is genuinely fresh and idle, with no active ask or thread in flight, a light opener is okay.");
41
55
  lines.push("i save everything i learn immediately with save_friend_note -- names, roles, preferences, projects, anything. the bar is low: if i learned it, i save it.");
42
56
  return lines.join("\n");
43
57
  }
@@ -3,10 +3,13 @@
3
3
  // Pure lookup, no I/O, cannot fail. Unknown channel gets minimal defaults.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.getChannelCapabilities = getChannelCapabilities;
6
+ exports.isRemoteChannel = isRemoteChannel;
7
+ exports.getAlwaysOnSenseNames = getAlwaysOnSenseNames;
6
8
  const runtime_1 = require("../../nerves/runtime");
7
9
  const CHANNEL_CAPABILITIES = {
8
10
  cli: {
9
11
  channel: "cli",
12
+ senseType: "local",
10
13
  availableIntegrations: [],
11
14
  supportsMarkdown: false,
12
15
  supportsStreaming: true,
@@ -15,6 +18,7 @@ const CHANNEL_CAPABILITIES = {
15
18
  },
16
19
  teams: {
17
20
  channel: "teams",
21
+ senseType: "closed",
18
22
  availableIntegrations: ["ado", "graph", "github"],
19
23
  supportsMarkdown: true,
20
24
  supportsStreaming: true,
@@ -23,15 +27,26 @@ const CHANNEL_CAPABILITIES = {
23
27
  },
24
28
  bluebubbles: {
25
29
  channel: "bluebubbles",
30
+ senseType: "open",
26
31
  availableIntegrations: [],
27
32
  supportsMarkdown: false,
28
33
  supportsStreaming: false,
29
34
  supportsRichCards: false,
30
35
  maxMessageLength: Infinity,
31
36
  },
37
+ inner: {
38
+ channel: "inner",
39
+ senseType: "internal",
40
+ availableIntegrations: [],
41
+ supportsMarkdown: false,
42
+ supportsStreaming: true,
43
+ supportsRichCards: false,
44
+ maxMessageLength: Infinity,
45
+ },
32
46
  };
33
47
  const DEFAULT_CAPABILITIES = {
34
48
  channel: "cli",
49
+ senseType: "local",
35
50
  availableIntegrations: [],
36
51
  supportsMarkdown: false,
37
52
  supportsStreaming: false,
@@ -47,3 +62,23 @@ function getChannelCapabilities(channel) {
47
62
  });
48
63
  return CHANNEL_CAPABILITIES[channel] ?? DEFAULT_CAPABILITIES;
49
64
  }
65
+ /** Whether the channel is remote (open or closed) vs local/internal. */
66
+ function isRemoteChannel(capabilities) {
67
+ const senseType = capabilities?.senseType;
68
+ return senseType !== undefined && senseType !== "local" && senseType !== "internal";
69
+ }
70
+ /**
71
+ * Returns channel names whose senseType is "open" or "closed" -- i.e. channels
72
+ * that are always-on (daemon-managed) rather than interactive or internal.
73
+ */
74
+ function getAlwaysOnSenseNames() {
75
+ (0, runtime_1.emitNervesEvent)({
76
+ component: "channels",
77
+ event: "channel.always_on_lookup",
78
+ message: "always-on sense names lookup",
79
+ meta: {},
80
+ });
81
+ return Object.entries(CHANNEL_CAPABILITIES)
82
+ .filter(([, cap]) => cap.senseType === "open" || cap.senseType === "closed")
83
+ .map(([channel]) => channel);
84
+ }