@ouro.bot/cli 0.1.0-alpha.314 → 0.1.0-alpha.316

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.
@@ -1,65 +1,17 @@
1
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
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.recallSession = recallSession;
37
4
  exports.searchSessionTranscript = searchSessionTranscript;
38
- const fs = __importStar(require("fs"));
39
5
  const runtime_1 = require("../nerves/runtime");
40
- function normalizeContent(content) {
41
- if (typeof content === "string")
42
- return content;
43
- if (!Array.isArray(content))
44
- return "";
45
- return content
46
- .map((part) => (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part
47
- ? String(part.text ?? "")
48
- : ""))
49
- .filter((text) => text.length > 0)
50
- .join("");
51
- }
52
- function normalizeSessionMessages(messages) {
53
- if (!Array.isArray(messages))
54
- return [];
55
- return messages
56
- .map((message) => {
57
- const record = message && typeof message === "object" ? message : {};
58
- return {
59
- role: typeof record.role === "string" ? record.role : "",
60
- content: normalizeContent(record.content),
61
- };
62
- })
6
+ const session_events_1 = require("./session-events");
7
+ function normalizeSessionMessages(events) {
8
+ return events
9
+ .map((event) => ({
10
+ id: event.id,
11
+ role: event.role,
12
+ content: (0, session_events_1.extractEventText)(event),
13
+ timestamp: (0, session_events_1.formatSessionEventTimestamp)(event),
14
+ }))
63
15
  .filter((message) => message.role !== "system" && message.content.length > 0);
64
16
  }
65
17
  function buildSummaryInstruction(friendId, channel, trustLevel) {
@@ -114,6 +66,10 @@ function buildSearchExcerpts(messages, query, maxMatches) {
114
66
  const start = Math.max(0, i - 1);
115
67
  const end = Math.min(messages.length, i + 2);
116
68
  const excerpt = messages
69
+ .slice(start, end)
70
+ .map((message) => `[${message.timestamp} | ${message.role} | ${message.id}] ${clip(message.content, 200)}`)
71
+ .join("\n");
72
+ const signature = messages
117
73
  .slice(start, end)
118
74
  .map((message) => `[${message.role}] ${clip(message.content, 200)}`)
119
75
  .join("\n");
@@ -121,15 +77,15 @@ function buildSearchExcerpts(messages, query, maxMatches) {
121
77
  .slice(start, end)
122
78
  .filter((message) => message.content.toLowerCase().includes(normalizedQuery))
123
79
  .length;
124
- candidates.push({ excerpt, score, index: i });
80
+ candidates.push({ excerpt, signature, score, index: i });
125
81
  }
126
82
  const seen = new Set();
127
83
  return candidates
128
84
  .sort((a, b) => b.score - a.score || a.index - b.index)
129
85
  .filter((candidate) => {
130
- if (seen.has(candidate.excerpt))
86
+ if (seen.has(candidate.signature))
131
87
  return false;
132
- seen.add(candidate.excerpt);
88
+ seen.add(candidate.signature);
133
89
  return true;
134
90
  })
135
91
  .slice(0, maxMatches)
@@ -147,20 +103,15 @@ async function recallSession(options) {
147
103
  messageCount: options.messageCount,
148
104
  },
149
105
  });
150
- let raw;
151
- try {
152
- raw = fs.readFileSync(options.sessionPath, "utf-8");
153
- }
154
- catch {
106
+ const envelope = (0, session_events_1.loadSessionEnvelopeFile)(options.sessionPath);
107
+ if (!envelope)
155
108
  return { kind: "missing" };
156
- }
157
- const parsed = JSON.parse(raw);
158
- const tailMessages = normalizeSessionMessages(parsed.messages).slice(-options.messageCount);
109
+ const tailMessages = normalizeSessionMessages(envelope.events).slice(-options.messageCount);
159
110
  if (tailMessages.length === 0) {
160
111
  return { kind: "empty" };
161
112
  }
162
113
  const transcript = tailMessages
163
- .map((message) => `[${message.role}] ${message.content}`)
114
+ .map((message) => `[${message.timestamp} | ${message.role} | ${message.id}] ${message.content}`)
164
115
  .join("\n");
165
116
  const summary = options.summarize
166
117
  ? await options.summarize(transcript, buildSummaryInstruction(options.friendId, options.channel, options.trustLevel ?? "family"))
@@ -186,15 +137,10 @@ async function searchSessionTranscript(options) {
186
137
  maxMatches: options.maxMatches ?? 5,
187
138
  },
188
139
  });
189
- let raw;
190
- try {
191
- raw = fs.readFileSync(options.sessionPath, "utf-8");
192
- }
193
- catch {
140
+ const envelope = (0, session_events_1.loadSessionEnvelopeFile)(options.sessionPath);
141
+ if (!envelope)
194
142
  return { kind: "missing" };
195
- }
196
- const parsed = JSON.parse(raw);
197
- const messages = normalizeSessionMessages(parsed.messages);
143
+ const messages = normalizeSessionMessages(envelope.events);
198
144
  if (messages.length === 0) {
199
145
  return { kind: "empty" };
200
146
  }
@@ -192,6 +192,7 @@ function buildStartOfTurnPacket(view, opts) {
192
192
  cares: buildCaresSection(view.activeCares),
193
193
  presence: buildPresenceSection(view.peerPresence),
194
194
  resumeHint: buildResumeHint(view, opts?.canonicalObligations ? effectiveObligations : undefined),
195
+ currentSessionTiming: opts?.currentSessionTiming,
195
196
  tempo,
196
197
  tokenBudget,
197
198
  assembledAt: new Date().toISOString(),
@@ -230,6 +231,7 @@ function renderStartOfTurnPacket(packet) {
230
231
  { label: "bundleState", content: (0, bundle_state_1.renderBundleStateHint)(packet.bundleState ?? []), priority: 7 },
231
232
  { label: "syncFailure", content: packet.syncFailure ?? "", priority: 7 },
232
233
  { label: "resume", content: packet.resumeHint, priority: 6 },
234
+ { label: "sessionTiming", content: packet.currentSessionTiming ?? "", priority: 5 },
233
235
  { label: "obligations", content: packet.obligations, priority: 5 },
234
236
  { label: "cares", content: packet.cares, priority: 4 },
235
237
  { label: "plot", content: packet.plotLine, priority: 3 },
@@ -33,20 +33,23 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.validateSessionMessages = exports.repairSessionMessages = exports.migrateToolNames = void 0;
36
37
  exports.trimMessages = trimMessages;
37
- exports.validateSessionMessages = validateSessionMessages;
38
- exports.repairSessionMessages = repairSessionMessages;
39
- exports.migrateToolNames = migrateToolNames;
40
38
  exports.saveSession = saveSession;
41
39
  exports.appendSyntheticAssistantMessage = appendSyntheticAssistantMessage;
42
40
  exports.loadSession = loadSession;
43
41
  exports.postTurn = postTurn;
44
42
  exports.deleteSession = deleteSession;
45
43
  const config_1 = require("../heart/config");
44
+ const session_events_1 = require("../heart/session-events");
46
45
  const runtime_1 = require("../nerves/runtime");
47
46
  const fs = __importStar(require("fs"));
48
47
  const path = __importStar(require("path"));
49
48
  const token_estimate_1 = require("./token-estimate");
49
+ var session_events_2 = require("../heart/session-events");
50
+ Object.defineProperty(exports, "migrateToolNames", { enumerable: true, get: function () { return session_events_2.migrateToolNames; } });
51
+ Object.defineProperty(exports, "repairSessionMessages", { enumerable: true, get: function () { return session_events_2.repairSessionMessages; } });
52
+ Object.defineProperty(exports, "validateSessionMessages", { enumerable: true, get: function () { return session_events_2.validateSessionMessages; } });
50
53
  function buildTrimmableBlocks(messages) {
51
54
  const blocks = [];
52
55
  let i = 0;
@@ -175,163 +178,47 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
175
178
  * user → assistant (with optional tool calls/results) → user → assistant...
176
179
  * Never assistant → assistant without a user in between.
177
180
  */
178
- function validateSessionMessages(messages) {
179
- const violations = [];
180
- let prevNonToolRole = null;
181
- let prevAssistantHadToolCalls = false;
182
- let sawToolResultSincePrevAssistant = false;
183
- for (let i = 0; i < messages.length; i++) {
184
- const msg = messages[i];
185
- if (msg.role === "system")
186
- continue;
187
- if (msg.role === "tool") {
188
- sawToolResultSincePrevAssistant = true;
189
- continue;
190
- }
191
- if (msg.role === "assistant" && prevNonToolRole === "assistant") {
192
- // assistant → tool(s) → assistant is valid (tool call flow)
193
- if (!(prevAssistantHadToolCalls && sawToolResultSincePrevAssistant)) {
194
- violations.push(`back-to-back assistant at index ${i}`);
195
- }
196
- }
197
- prevAssistantHadToolCalls = msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
198
- sawToolResultSincePrevAssistant = false;
199
- prevNonToolRole = msg.role;
200
- }
201
- return violations;
202
- }
203
- /**
204
- * Repairs session invariant violations by merging consecutive assistant messages.
205
- */
206
- function repairSessionMessages(messages) {
207
- const violations = validateSessionMessages(messages);
208
- if (violations.length === 0)
209
- return messages;
210
- const result = [];
211
- for (const msg of messages) {
212
- if (msg.role === "assistant" && result.length > 0) {
213
- const prev = result[result.length - 1];
214
- if (prev.role === "assistant" && !("tool_calls" in prev)) {
215
- const prevContent = typeof prev.content === "string" ? prev.content : "";
216
- const curContent = typeof msg.content === "string" ? msg.content : "";
217
- prev.content = `${prevContent}\n\n${curContent}`;
218
- continue;
219
- }
220
- }
221
- result.push(msg);
222
- }
223
- (0, runtime_1.emitNervesEvent)({
224
- level: "info",
225
- event: "mind.session_invariant_repair",
226
- component: "mind",
227
- message: "repaired session invariant violations",
228
- meta: { violations },
229
- });
230
- return result;
231
- }
232
- function stripOrphanedToolResults(messages) {
233
- const validCallIds = new Set();
234
- for (const msg of messages) {
235
- if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
236
- continue;
237
- for (const toolCall of msg.tool_calls)
238
- validCallIds.add(toolCall.id);
239
- }
240
- let removed = 0;
241
- const repaired = messages.filter((msg) => {
242
- if (msg.role !== "tool")
243
- return true;
244
- const keep = validCallIds.has(msg.tool_call_id);
245
- if (!keep)
246
- removed++;
247
- return keep;
248
- });
249
- if (removed > 0) {
250
- (0, runtime_1.emitNervesEvent)({
251
- level: "info",
252
- event: "mind.session_orphan_tool_result_repair",
253
- component: "mind",
254
- message: "removed orphaned tool results from session history",
255
- meta: { removed },
256
- });
257
- }
258
- return repaired;
259
- }
260
- // Tool renames that have shipped. Old names in session history confuse the
261
- // model into calling tools that no longer exist. Applied on session load so
262
- // the transcript uses the current vocabulary.
263
- const TOOL_NAME_MIGRATIONS = {
264
- final_answer: "settle",
265
- no_response: "observe",
266
- go_inward: "ponder",
267
- descend: "ponder",
268
- memory_save: "diary_write",
269
- memory_search: "recall",
270
- };
271
- function migrateToolNames(messages) {
272
- let migrated = 0;
273
- const result = messages.map((msg) => {
274
- if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0)
275
- return msg;
276
- let changed = false;
277
- const updatedCalls = msg.tool_calls.map((tc) => {
278
- if (tc.type !== "function")
279
- return tc;
280
- const newName = TOOL_NAME_MIGRATIONS[tc.function.name];
281
- if (!newName)
282
- return tc;
283
- changed = true;
284
- migrated++;
285
- return { ...tc, function: { ...tc.function, name: newName } };
286
- });
287
- return changed ? { ...msg, tool_calls: updatedCalls } : msg;
288
- });
289
- if (migrated > 0) {
290
- (0, runtime_1.emitNervesEvent)({
291
- level: "info",
292
- event: "mind.session_tool_name_migration",
293
- component: "mind",
294
- message: "migrated deprecated tool names in session history",
295
- meta: { migrated },
296
- });
297
- }
298
- return result;
181
+ function denormalizeContinuityState(state) {
182
+ if (!state.mustResolveBeforeHandoff && typeof state.lastFriendActivityAt !== "string")
183
+ return undefined;
184
+ return {
185
+ ...(state.mustResolveBeforeHandoff ? { mustResolveBeforeHandoff: true } : {}),
186
+ ...(typeof state.lastFriendActivityAt === "string" ? { lastFriendActivityAt: state.lastFriendActivityAt } : {}),
187
+ };
299
188
  }
300
- function saveSession(filePath, messages, lastUsage, state) {
301
- const violations = validateSessionMessages(messages);
302
- if (violations.length > 0) {
303
- (0, runtime_1.emitNervesEvent)({
304
- level: "info",
305
- event: "mind.session_invariant_violation",
306
- component: "mind",
307
- message: "session invariant violated on save",
308
- meta: { path: filePath, violations },
309
- });
310
- messages = repairSessionMessages(messages);
311
- }
312
- messages = stripOrphanedToolResults(messages);
189
+ function writeSessionEnvelope(filePath, envelope) {
313
190
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
314
- const envelope = { version: 1, messages };
315
- if (lastUsage)
316
- envelope.lastUsage = lastUsage;
317
- if (state?.mustResolveBeforeHandoff === true || typeof state?.lastFriendActivityAt === "string") {
318
- envelope.state = {
319
- ...(state?.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
320
- ...(typeof state?.lastFriendActivityAt === "string" ? { lastFriendActivityAt: state.lastFriendActivityAt } : {}),
321
- };
322
- }
323
191
  fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
324
192
  }
193
+ function saveSession(filePath, messages, lastUsage, state) {
194
+ const existing = (0, session_events_1.loadSessionEnvelopeFile)(filePath);
195
+ const previousMessages = existing ? (0, session_events_1.projectProviderMessages)(existing) : [];
196
+ const sanitized = (0, session_events_1.sanitizeProviderMessages)(messages);
197
+ const envelope = (0, session_events_1.buildCanonicalSessionEnvelope)({
198
+ existing,
199
+ previousMessages,
200
+ currentMessages: sanitized,
201
+ trimmedMessages: sanitized,
202
+ recordedAt: new Date().toISOString(),
203
+ lastUsage: lastUsage ?? null,
204
+ state,
205
+ projectionBasis: {
206
+ maxTokens: null,
207
+ contextMargin: null,
208
+ inputTokens: lastUsage?.input_tokens ?? null,
209
+ },
210
+ });
211
+ writeSessionEnvelope(filePath, envelope);
212
+ }
325
213
  function appendSyntheticAssistantMessage(filePath, content) {
326
214
  try {
327
215
  if (!fs.existsSync(filePath))
328
216
  return false;
329
- const raw = fs.readFileSync(filePath, "utf-8");
330
- const data = JSON.parse(raw);
331
- if (data.version !== 1 || !Array.isArray(data.messages))
217
+ const envelope = (0, session_events_1.loadSessionEnvelopeFile)(filePath);
218
+ if (!envelope)
332
219
  return false;
333
- data.messages.push({ role: "assistant", content });
334
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
220
+ const updated = (0, session_events_1.appendSyntheticAssistantEvent)(envelope, content, new Date().toISOString());
221
+ writeSessionEnvelope(filePath, updated);
335
222
  (0, runtime_1.emitNervesEvent)({
336
223
  component: "mind",
337
224
  event: "mind.session_synthetic_message_appended",
@@ -346,44 +233,25 @@ function appendSyntheticAssistantMessage(filePath, content) {
346
233
  }
347
234
  function loadSession(filePath) {
348
235
  try {
349
- const raw = fs.readFileSync(filePath, "utf-8");
350
- const data = JSON.parse(raw);
351
- if (data.version !== 1)
236
+ const envelope = (0, session_events_1.loadSessionEnvelopeFile)(filePath);
237
+ if (!envelope)
352
238
  return null;
353
- let messages = data.messages;
354
- const violations = validateSessionMessages(messages);
355
- if (violations.length > 0) {
356
- (0, runtime_1.emitNervesEvent)({
357
- level: "info",
358
- event: "mind.session_invariant_violation",
359
- component: "mind",
360
- message: "session invariant violated on load",
361
- meta: { path: filePath, violations },
362
- });
363
- messages = repairSessionMessages(messages);
364
- }
365
- messages = stripOrphanedToolResults(messages);
366
- messages = migrateToolNames(messages);
367
- const rawState = data?.state && typeof data.state === "object" && data.state !== null
368
- ? data.state
369
- : undefined;
370
- const state = rawState && (rawState.mustResolveBeforeHandoff === true
371
- || typeof rawState.lastFriendActivityAt === "string")
372
- ? {
373
- ...(rawState.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
374
- ...(typeof rawState.lastFriendActivityAt === "string" ? { lastFriendActivityAt: rawState.lastFriendActivityAt } : {}),
375
- }
376
- : undefined;
377
- return { messages, lastUsage: data.lastUsage, state };
239
+ return {
240
+ messages: (0, session_events_1.projectProviderMessages)(envelope),
241
+ events: envelope.events,
242
+ lastUsage: envelope.lastUsage ?? undefined,
243
+ state: denormalizeContinuityState(envelope.state),
244
+ };
378
245
  }
379
246
  catch {
380
247
  return null;
381
248
  }
382
249
  }
383
250
  function postTurn(messages, sessPath, usage, hooks, state) {
251
+ const preTrimMessages = [...messages];
384
252
  if (hooks?.beforeTrim) {
385
253
  try {
386
- hooks.beforeTrim([...messages]);
254
+ hooks.beforeTrim(preTrimMessages);
387
255
  }
388
256
  catch (error) {
389
257
  (0, runtime_1.emitNervesEvent)({
@@ -398,9 +266,26 @@ function postTurn(messages, sessPath, usage, hooks, state) {
398
266
  }
399
267
  }
400
268
  const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
401
- const trimmed = trimMessages(messages, maxTokens, contextMargin, usage?.input_tokens);
269
+ const currentMessages = (0, session_events_1.sanitizeProviderMessages)(messages);
270
+ const trimmed = trimMessages(currentMessages, maxTokens, contextMargin, usage?.input_tokens);
402
271
  messages.splice(0, messages.length, ...trimmed);
403
- saveSession(sessPath, messages, usage, state);
272
+ const existing = (0, session_events_1.loadSessionEnvelopeFile)(sessPath);
273
+ const previousMessages = existing ? (0, session_events_1.projectProviderMessages)(existing) : [];
274
+ const envelope = (0, session_events_1.buildCanonicalSessionEnvelope)({
275
+ existing,
276
+ previousMessages,
277
+ currentMessages,
278
+ trimmedMessages: trimmed,
279
+ recordedAt: new Date().toISOString(),
280
+ lastUsage: usage ?? null,
281
+ state,
282
+ projectionBasis: {
283
+ maxTokens,
284
+ contextMargin,
285
+ inputTokens: usage?.input_tokens ?? null,
286
+ },
287
+ });
288
+ writeSessionEnvelope(sessPath, envelope);
404
289
  }
405
290
  function deleteSession(filePath) {
406
291
  try {
@@ -127,8 +127,20 @@ function buildSessionSummary(options) {
127
127
  return "";
128
128
  const lines = ["## active sessions"];
129
129
  for (const entry of entries) {
130
- const ago = formatTimeAgo(now - entry.lastActivityMs);
131
- lines.push(`- ${entry.friendName}/${entry.channel}/${entry.key} (last: ${ago})`);
130
+ const parts = [];
131
+ if (entry.lastInboundAt) {
132
+ parts.push(`in ${formatTimeAgo(now - Date.parse(entry.lastInboundAt))}`);
133
+ }
134
+ else {
135
+ parts.push(`last ${formatTimeAgo(now - entry.lastActivityMs)}`);
136
+ }
137
+ if (entry.lastOutboundAt) {
138
+ parts.push(`out ${formatTimeAgo(now - Date.parse(entry.lastOutboundAt))}`);
139
+ }
140
+ if (entry.unansweredInboundCount > 0) {
141
+ parts.push(`${entry.unansweredInboundCount} waiting`);
142
+ }
143
+ lines.push(`- ${entry.friendName}/${entry.channel}/${entry.key} (${parts.join(" · ")})`);
132
144
  }
133
145
  return lines.join("\n");
134
146
  }
@@ -91,6 +91,10 @@ const DISPATCH_EXEMPT_PATTERNS = [
91
91
  "heart/attachments/originals",
92
92
  "heart/attachments/sources/index",
93
93
  "heart/attachments/sources/cli-local-file",
94
+ // Browser-safe Outlook contract helpers: shared types/formatting helpers
95
+ // consumed by server readers and the UI. Outlook read/render modules own
96
+ // the observability for these projections.
97
+ "heart/outlook/outlook-types",
94
98
  ];
95
99
  function isDispatchExempt(filePath) {
96
100
  return DISPATCH_EXEMPT_PATTERNS.some((pattern) => filePath.includes(pattern));
@@ -751,6 +751,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
751
751
  messages: sessionMessages,
752
752
  sessionPath: sessPath,
753
753
  state: existing?.state,
754
+ events: existing?.events,
754
755
  }),
755
756
  },
756
757
  pendingDir,
@@ -253,6 +253,16 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
253
253
  // PageUp/PageDown: suppress (no text insertion, no action)
254
254
  if (key.pageUp || key.pageDown)
255
255
  return;
256
+ // Alt+Enter (single data event): Ink checks `return: input === '\r'` before
257
+ // stripping the \x1b prefix, so key.return is false when the terminal sends
258
+ // \x1b\r as one chunk. Detect via the stripped inputChar instead.
259
+ if (inputChar === "\r" && key.meta) {
260
+ lastEscTime.current = 0;
261
+ const before = inputRef.current.slice(0, cursorRef.current);
262
+ const after = inputRef.current.slice(cursorRef.current);
263
+ updateInput(before + "\n" + after, cursorRef.current + 1);
264
+ return;
265
+ }
256
266
  if (key.return) {
257
267
  // Alt+Enter: detect via key.meta OR recent ESC (within 50ms — Ink splits \x1b\r)
258
268
  const recentEsc = (Date.now() - lastEscTime.current) < 50;
@@ -985,6 +985,7 @@ async function main(agentName, options) {
985
985
  // Load existing session or start fresh
986
986
  const existing = (0, context_1.loadSession)(sessPath);
987
987
  let sessionState = existing?.state;
988
+ let sessionEvents = existing?.events ?? [];
988
989
  const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
989
990
  const sessionMessages = existing?.messages && existing.messages.length > 0
990
991
  ? existing.messages
@@ -1006,6 +1007,7 @@ async function main(agentName, options) {
1006
1007
  _testInputSource: options?._testInputSource,
1007
1008
  onAsyncAssistantMessage: async (messages, _assistantMessage) => {
1008
1009
  (0, context_1.postTurn)(messages, sessPath, undefined, undefined, sessionState);
1010
+ sessionEvents = (0, context_1.loadSession)(sessPath)?.events ?? sessionEvents;
1009
1011
  },
1010
1012
  runTurn: async (messages, userInput, callbacks, signal, toolContext, userContent) => {
1011
1013
  // Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
@@ -1044,6 +1046,7 @@ async function main(agentName, options) {
1044
1046
  messages,
1045
1047
  sessionPath: sessPath,
1046
1048
  state: sessionState,
1049
+ events: sessionEvents,
1047
1050
  }),
1048
1051
  },
1049
1052
  pendingDir,
@@ -1065,6 +1068,7 @@ async function main(agentName, options) {
1065
1068
  postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
1066
1069
  (0, context_1.postTurn)(turnMessages, sessionPathArg, usage, hooks, state);
1067
1070
  sessionState = state;
1071
+ sessionEvents = (0, context_1.loadSession)(sessionPathArg)?.events ?? sessionEvents;
1068
1072
  },
1069
1073
  accumulateFriendTokens: tokens_1.accumulateFriendTokens,
1070
1074
  signal,
@@ -60,6 +60,7 @@ const start_of_turn_packet_1 = require("../heart/start-of-turn-packet");
60
60
  const bundle_state_1 = require("../heart/bundle-state");
61
61
  const sync_1 = require("../heart/sync");
62
62
  const config_1 = require("../heart/config");
63
+ const session_events_1 = require("../heart/session-events");
63
64
  const presence_1 = require("../arc/presence");
64
65
  const episodes_1 = require("../arc/episodes");
65
66
  const turn_context_1 = require("../heart/turn-context");
@@ -258,6 +259,7 @@ async function handleInboundTurn(input) {
258
259
  // Step 3: Load/create session
259
260
  const session = await input.sessionLoader.loadOrCreate();
260
261
  const sessionMessages = session.messages;
262
+ const sessionEvents = session.events ?? [];
261
263
  let mustResolveBeforeHandoff = (0, continuity_1.resolveMustResolveBeforeHandoff)(session.state?.mustResolveBeforeHandoff === true, input.continuityIngressTexts);
262
264
  const lastFriendActivityAt = input.channel === "inner"
263
265
  ? session.state?.lastFriendActivityAt
@@ -272,6 +274,7 @@ async function handleInboundTurn(input) {
272
274
  key: input.sessionKey ?? "session",
273
275
  sessionPath: session.sessionPath,
274
276
  };
277
+ const currentSessionTiming = (0, session_events_1.describeCurrentSessionTiming)(sessionEvents);
275
278
  // Step 3b: Pre-turn sync pull (opt-in) — MUST happen before any continuity state reads
276
279
  // so that obligations, episodes, cares, etc. reflect the latest remote state.
277
280
  let syncFailure;
@@ -384,6 +387,7 @@ async function handleInboundTurn(input) {
384
387
  primary: activeWorkFrame.primaryObligation,
385
388
  all: activeWorkFrame.pendingObligations,
386
389
  },
390
+ currentSessionTiming,
387
391
  });
388
392
  /* v8 ignore next 3 -- syncFailure propagation tested in sync.test.ts @preserve */
389
393
  if (syncFailure) {
@@ -140,6 +140,7 @@ async function runSenseTurn(options) {
140
140
  messages: sessionMessages,
141
141
  sessionPath: sessPath,
142
142
  state: sessionState,
143
+ events: existing?.events,
143
144
  }),
144
145
  },
145
146
  /* v8 ignore stop */
@@ -660,6 +660,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
660
660
  messages,
661
661
  sessionPath: sessPath,
662
662
  state: existing?.state,
663
+ events: existing?.events,
663
664
  };
664
665
  },
665
666
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.314",
3
+ "version": "0.1.0-alpha.316",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -26,6 +26,7 @@
26
26
  "teams": "tsc && node dist/senses/teams-entry.js --agent ouroboros",
27
27
  "bluebubbles": "tsc && node dist/senses/bluebubbles/entry.js --agent ouroboros",
28
28
  "test": "vitest run",
29
+ "test:outlook-ui": "npm test --prefix packages/outlook-ui",
29
30
  "test:coverage:vitest": "vitest run --coverage",
30
31
  "test:coverage": "node scripts/run-coverage-gate.cjs",
31
32
  "build": "tsc && (cd packages/outlook-ui && npm install --ignore-scripts 2>/dev/null && npm run build && cp -r dist ../../dist/outlook-ui) || echo 'outlook-ui build skipped'",
@@ -50,11 +51,19 @@
50
51
  "url": "https://github.com/ouroborosbot/ouroboros"
51
52
  },
52
53
  "devDependencies": {
54
+ "@testing-library/react": "^16.3.2",
53
55
  "@types/semver": "^7.7.1",
54
56
  "@vitest/coverage-v8": "^4.0.18",
55
57
  "eslint": "^10.0.2",
58
+ "jsdom": "^29.0.2",
56
59
  "typescript": "^5.7.0",
57
60
  "typescript-eslint": "^8.56.1",
58
61
  "vitest": "^4.0.18"
62
+ },
63
+ "overrides": {
64
+ "@testing-library/react": {
65
+ "react": "$react",
66
+ "@types/react": "$@types/react"
67
+ }
59
68
  }
60
69
  }