@ouro.bot/cli 0.1.0-alpha.932 → 0.1.0-alpha.934

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.json CHANGED
@@ -1,6 +1,25 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.934",
6
+ "changes": [
7
+ "Hold duplicate Teams activities durably before model execution, steering or quoted queuing, preserving receipts across restart and session reset.",
8
+ "Do not replay uncertain Teams sends after a lost acknowledgement; split-message recovery requires an explicit HTTP 413 and smaller payloads.",
9
+ "Preserve unconsumed Teams steering in its original request context, reject incomplete production identities, and bypass the SDK stream hidden retry loop with acknowledged single-attempt messages.",
10
+ "Keep distinct Teams fallback actors separate when optional AAD metadata is empty.",
11
+ "Include bot and request correlation context on Teams handler and SDK error telemetry."
12
+ ]
13
+ },
14
+ {
15
+ "version": "0.1.0-alpha.933",
16
+ "changes": [
17
+ "Recover existing supervised Teams tunnels after permanent rejection or sustained readiness loss, with scoped and confirmed bot endpoint registration.",
18
+ "Recover accepted Shepherd notice intent without importing audit history, and compact pending observations into bounded historical session digests with durable receipt custody.",
19
+ "Preserve valid premature judgments independently of invalid suggested replies; keep length-only periodic fixed-text continuation behind all existing input safety guards.",
20
+ "Correlate Teams request and reply stages without message bodies, and contain notice-ledger failures so later returns remain runnable."
21
+ ]
22
+ },
4
23
  {
5
24
  "version": "0.1.0-alpha.932",
6
25
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.932",
2
+ "runtimeVersion": "0.1.0-alpha.934",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.932</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.934</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.SessionTransactionError = exports.SessionTurnBusyError = void 0;
40
+ exports.admitSessionActivity = admitSessionActivity;
40
41
  exports.acquireSessionTurnLease = acquireSessionTurnLease;
41
42
  exports.withSessionTurnLease = withSessionTurnLease;
42
43
  exports.withImmediateSessionTurnLease = withImmediateSessionTurnLease;
@@ -176,21 +177,78 @@ function checkTemporary(pin, filePath, identity) {
176
177
  function openLeaseDatabase(lockPath, busyTimeoutMs = 0, confinement = null) {
177
178
  checkConfinement(confinement);
178
179
  const database = new better_sqlite3_1.default(lockPath);
179
- database.pragma(`busy_timeout = ${busyTimeoutMs}`);
180
- database.exec(`
181
- CREATE TABLE IF NOT EXISTS session_turn_lease (
182
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
183
- pid INTEGER NOT NULL,
184
- owner_id TEXT NOT NULL,
185
- owner_token TEXT NOT NULL
186
- )
187
- `);
188
- const columns = new Set(database.prepare(`PRAGMA table_info(session_turn_lease)`).all().map((column) => column.name));
189
- if (!columns.has("boot_identity"))
190
- database.exec(`ALTER TABLE session_turn_lease ADD COLUMN boot_identity TEXT`);
191
- if (!columns.has("process_started_at"))
192
- database.exec(`ALTER TABLE session_turn_lease ADD COLUMN process_started_at TEXT`);
193
- return database;
180
+ try {
181
+ database.pragma(`busy_timeout = ${busyTimeoutMs}`);
182
+ database.transaction(() => {
183
+ database.exec(`
184
+ CREATE TABLE IF NOT EXISTS session_turn_lease (
185
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
186
+ pid INTEGER NOT NULL,
187
+ owner_id TEXT NOT NULL,
188
+ owner_token TEXT NOT NULL
189
+ )
190
+ `);
191
+ const columns = new Set(database.prepare(`PRAGMA table_info(session_turn_lease)`).all().map((column) => column.name));
192
+ if (!columns.has("boot_identity"))
193
+ database.exec(`ALTER TABLE session_turn_lease ADD COLUMN boot_identity TEXT`);
194
+ if (!columns.has("process_started_at"))
195
+ database.exec(`ALTER TABLE session_turn_lease ADD COLUMN process_started_at TEXT`);
196
+ }).immediate();
197
+ return database;
198
+ }
199
+ catch (error) {
200
+ database.close();
201
+ throw error;
202
+ }
203
+ }
204
+ /** Immutable admission, not proof of execution or delivery. Unknown effects must never be replayed. */
205
+ function admitSessionActivity(sessionPath, input) {
206
+ if (typeof input.namespace !== "string" || !input.namespace.trim() || Buffer.byteLength(input.namespace) > 4096
207
+ || typeof input.activityId !== "string" || !input.activityId.trim() || Buffer.byteLength(input.activityId) > 1024
208
+ || typeof input.payloadDigest !== "string" || !/^[a-f0-9]{64}$/.test(input.payloadDigest)) {
209
+ throw new SessionTransactionError("invalid session activity admission");
210
+ }
211
+ const canonical = canonicalSessionPath(sessionPath);
212
+ const lockPath = `${canonical}.turn.lock`;
213
+ fs.mkdirSync(path.dirname(canonical), { recursive: true, mode: 0o700 });
214
+ const receiptId = revision(JSON.stringify([input.namespace, input.activityId]));
215
+ const database = openLeaseDatabase(lockPath, 1000);
216
+ try {
217
+ database.pragma("synchronous = FULL");
218
+ const state = database.transaction(() => {
219
+ database.exec(`
220
+ CREATE TABLE IF NOT EXISTS session_activity_receipts (
221
+ receipt_id TEXT PRIMARY KEY,
222
+ payload_digest TEXT NOT NULL,
223
+ admitted_at TEXT NOT NULL
224
+ )
225
+ `);
226
+ const existing = database.prepare("SELECT payload_digest FROM session_activity_receipts WHERE receipt_id = ?")
227
+ .get(receiptId);
228
+ if (existing) {
229
+ if (typeof existing.payload_digest !== "string" || !/^[a-f0-9]{64}$/.test(existing.payload_digest)) {
230
+ throw new SessionTransactionError("invalid session activity receipt");
231
+ }
232
+ if (existing.payload_digest !== input.payloadDigest)
233
+ throw new SessionTransactionError("conflicting session activity payload");
234
+ return "duplicate";
235
+ }
236
+ // Never evict an admission: losing its identity would authorize uncertain work again.
237
+ if (fs.statSync(lockPath).size >= 64 * 1024 * 1024)
238
+ throw new SessionTransactionError("session activity receipt capacity exceeded");
239
+ database.prepare("INSERT INTO session_activity_receipts (receipt_id, payload_digest, admitted_at) VALUES (?, ?, ?)")
240
+ .run(receiptId, input.payloadDigest, new Date().toISOString());
241
+ return "admitted";
242
+ }).immediate();
243
+ (0, runtime_1.emitNervesEvent)({
244
+ component: "mind", event: "mind.session_activity_admission", message: "recorded session activity admission decision",
245
+ meta: { receiptId, state },
246
+ });
247
+ return { state, receiptId };
248
+ }
249
+ finally {
250
+ database.close();
251
+ }
194
252
  }
195
253
  function acquisitionRecord(options) {
196
254
  const pid = options.pid ?? process.pid;
@@ -25,11 +25,11 @@ const CALLBACKS = { onModelStart: NOOP, onModelStreamStart: NOOP, onTextChunk: N
25
25
  function replyTextProblem(text) {
26
26
  const line = text.replace(/`/g, "").replace(/\s+/g, " ").trim();
27
27
  if (!line)
28
- return { problem: "an empty reply" };
29
- if (line.length > exports.REPLY_MAX_CHARS)
30
- return { problem: `a reply of ${line.length} characters (the limit is ${exports.REPLY_MAX_CHARS})` };
28
+ return { problem: "an empty reply", issue: "missing" };
31
29
  if (/[\\@;$|<>&\p{Cc}\p{Cf}]/u.test(line) || /^[/!#]/.test(line))
32
- return { problem: "a reply with characters a terminal could read as a command" };
30
+ return { problem: "a reply with characters a terminal could read as a command", issue: "unsafe" };
31
+ if (line.length > exports.REPLY_MAX_CHARS)
32
+ return { problem: `a reply of ${line.length} characters (the limit is ${exports.REPLY_MAX_CHARS})`, issue: "too_long" };
33
33
  return { line };
34
34
  }
35
35
  function safeReplyText(text) {
@@ -81,8 +81,10 @@ function parseVerdict(content) {
81
81
  return { kind: "unclear", reply: null, reason: `premature, but the key came from JSON inside prose: ${reason}` };
82
82
  return { kind, reply: { key: reply.key }, reason };
83
83
  }
84
- const checked = typeof reply.text === "string" ? replyTextProblem(reply.text) : { problem: "no reply" };
85
- return "line" in checked ? { kind, reply: { text: checked.line }, reason } : { kind: "unclear", reply: null, reason: `premature, but the judge gave ${checked.problem}: ${reason}` };
84
+ const checked = Object.hasOwn(reply, "key") ? { problem: "an unsupported key", issue: "unsafe" }
85
+ : typeof reply.text === "string" ? replyTextProblem(reply.text) : { problem: "no reply", issue: "missing" };
86
+ return "line" in checked ? { kind, reply: { text: checked.line }, reason }
87
+ : { kind: parsed.inProse ? "unclear" : kind, reply: null, replyIssue: checked.issue, reason: `premature, but the judge gave ${checked.problem}: ${reason}` };
86
88
  }
87
89
  function judgeMessages(packet) {
88
90
  return [
@@ -47,7 +47,6 @@ const friends_1 = require("@ouro.bot/friends");
47
47
  const await_alert_1 = require("../../heart/awaiting/await-alert");
48
48
  const config_1 = require("../../heart/config");
49
49
  const runtime_1 = require("../../nerves/runtime");
50
- const returns_1 = require("./returns");
51
50
  const notice_outbox_1 = require("./notice-outbox");
52
51
  /**
53
52
  * The owner notice: after a judged return, the agent's owner hears what happened in one short plain message on their
@@ -55,12 +54,23 @@ const notice_outbox_1 = require("./notice-outbox");
55
54
  * policy covers whether or not the Ouro agent decides it matters. Returns that arrive close together share one message.
56
55
  */
57
56
  exports.NOTICE_COALESCE_MS = 5_000;
58
- /** Returns named one by one in a coalesced message; the rest are counted. */
57
+ /** Distinct sessions named in a digest; the remaining sessions are counted. */
59
58
  exports.NOTICE_LIST_MAX = 6;
60
59
  exports.NOTICE_RETRY_MS = 30_000;
61
60
  const NAME_MAX = 60;
62
61
  const REASON_MAX = 300;
63
62
  const REPLY_MAX = 240;
63
+ const BLOCK_DESCRIPTION = {
64
+ reply_invalid: "the suggested reply was invalid",
65
+ menu_disallowed: "scheduled continuation cannot approve a menu",
66
+ host_unverified: "the host could not verify safe input state",
67
+ guidance: "operator guidance blocked input",
68
+ loop_guard: "the reply limit was reached",
69
+ focus: "the human owns the focused session",
70
+ state_changed: "the target state changed",
71
+ custody: "input delivery was already claimed or needs reconciliation",
72
+ stopped: "Shepherd stopped",
73
+ };
64
74
  /** Kinds a "needs-me" owner still hears about. */
65
75
  const NEEDS_ME = new Set(["gate", "done", "loop_guard"]);
66
76
  function noticeWanted(policy, record) {
@@ -103,6 +113,8 @@ function noticeLine(record, location) {
103
113
  const sent = record.reply.startsWith("key:") ? `pressed ${plain(record.reply.slice(4), 10)}` : `sent it back: "${plain(record.reply, REPLY_MAX)}"`;
104
114
  return `${subject} stopped early, so I ${sent}. ${why}`;
105
115
  }
116
+ if (record.actionBlockReason)
117
+ return `${subject} stopped early; no input sent because ${BLOCK_DESCRIPTION[record.actionBlockReason]}. ${why}`;
106
118
  return `${subject} stopped early, but I did not answer it, so it is waiting for you. ${why}`;
107
119
  case "gate":
108
120
  return `${subject} needs you. ${why}`;
@@ -120,13 +132,30 @@ function noticeLine(record, location) {
120
132
  return `${subject} stopped but I could not judge it, so I left it. ${why}`;
121
133
  }
122
134
  }
123
- /** One message for a burst: a single return is its line; several are a short list, the extra ones counted. */
124
- function noticeMessage(records, refs = new Map()) {
125
- const lines = records.slice(0, exports.NOTICE_LIST_MAX).map((record) => noticeLine(record, refs.get(record.session)));
126
- if (records.length === 1)
127
- return lines[0];
128
- const more = records.length - lines.length;
129
- return [`${records.length} coding agents stopped:`, ...lines.map((line) => `- ${line}`), ...(more > 0 ? [`- and ${more} more.`] : [])].join("\n");
135
+ /** A bounded display of latest observations; all input records retain delivery custody. */
136
+ function noticeMessage(records, refs = new Map(), context = {}) {
137
+ const latest = new Map();
138
+ for (const record of records) {
139
+ const key = JSON.stringify([record.host, record.session]);
140
+ const previous = latest.get(key);
141
+ const at = Date.parse(record.at);
142
+ const previousAt = previous ? Date.parse(previous.at) : NaN;
143
+ if (!previous || (Number.isFinite(at) && (!Number.isFinite(previousAt) || at > previousAt))
144
+ || ((!Number.isFinite(at) && !Number.isFinite(previousAt) || at === previousAt) && record.transition > previous.transition))
145
+ latest.set(key, record);
146
+ }
147
+ const sessions = [...latest].sort(([a], [b]) => a < b ? -1 : 1).map(([, record]) => record);
148
+ const lines = sessions.slice(0, exports.NOTICE_LIST_MAX).map(record => {
149
+ const line = noticeLine(record, refs.get(record.session));
150
+ if (!context.recovered)
151
+ return line;
152
+ const at = Date.parse(record.at);
153
+ return `${line} (${Number.isFinite(at) ? `observed at ${new Date(at).toISOString()}` : "observation time unknown"})`;
154
+ });
155
+ const more = sessions.length - lines.length;
156
+ const body = sessions.length === 1 ? lines[0]
157
+ : [`Updates from ${sessions.length} coding sessions:`, ...lines.map(line => `- ${line}`), ...(more > 0 ? [`- and ${more} more.`] : [])].join("\n");
158
+ return context.recovered ? `Includes recovered historical observations, not live status:\n${body}` : body;
130
159
  }
131
160
  function teamsAadId(friend) {
132
161
  const ext = friend.externalIds.find((entry) => entry.provider === "aad" && !entry.externalId.startsWith("group:"));
@@ -192,30 +221,43 @@ function createTeamsNoticeSender(agentRoot, deps = {}) {
192
221
  return sendProactiveTeamsMessageToSession({ friendId: target.friendId, sessionKey: target.key, text, textFormat: "plain", intent: "generic_outreach", captureReceipt: true }, { botApi: await bot, store: new friends_1.FileFriendStore(path.join(agentRoot, "friends")) });
193
222
  };
194
223
  }
195
- /** Collects notices for NOTICE_COALESCE_MS after the first, then sends them as one message. It never throws. */
224
+ /** Collects durable notice intents; acceptance is distinct from transport delivery. */
196
225
  function createOwnerNotifier(options) {
197
226
  const { agent, policy } = options;
198
227
  const resolveTargets = options.resolveTargets ?? (() => resolveNoticeTargets(options.agentRoot));
199
228
  const send = options.send ?? createTeamsNoticeSender(options.agentRoot);
200
229
  const retryWrites = new Map();
230
+ const recoveredNoticeIds = new Set();
201
231
  const writes = new Set();
202
232
  let cancel = null;
203
233
  let stopped = false;
204
234
  let flushing = null;
235
+ let drainRequested = false;
205
236
  function schedule(ms) {
206
237
  if (!stopped && policy !== "off")
207
- cancel ??= options.schedule(() => { void flush(); }, ms);
238
+ cancel ??= options.schedule(() => {
239
+ cancel?.();
240
+ cancel = null;
241
+ if (flushing)
242
+ drainRequested = true;
243
+ else
244
+ void flush();
245
+ }, ms);
208
246
  }
209
247
  function persistIntent(record) {
210
248
  const key = `${record.host}:${record.session}:${record.transition}:${record.at}`;
211
249
  retryWrites.set(key, record);
212
- const write = (0, notice_outbox_1.enqueueNotice)(options.agentRoot, record).then(() => {
250
+ const write = (0, notice_outbox_1.enqueueNotice)(options.agentRoot, record).then(id => {
213
251
  retryWrites.delete(key);
252
+ return id;
214
253
  }).catch(error => {
215
254
  (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "could not persist a Shepherd owner notice intent; it remains waiting", meta: { agent, error: error.message } });
216
255
  schedule(exports.NOTICE_RETRY_MS);
217
- }).finally(() => { writes.delete(write); });
256
+ throw error;
257
+ });
218
258
  writes.add(write);
259
+ // Observe cleanup immediately, while the producer still receives the rejecting promise.
260
+ void write.then(() => { writes.delete(write); }, () => { writes.delete(write); });
219
261
  return write;
220
262
  }
221
263
  async function runFlush() {
@@ -223,10 +265,11 @@ function createOwnerNotifier(options) {
223
265
  cancel = null;
224
266
  if (stopped || policy === "off")
225
267
  return;
226
- await Promise.all([...writes]);
227
- for (const record of [...retryWrites.values()])
228
- await persistIntent(record);
229
268
  try {
269
+ await Promise.allSettled([...writes]);
270
+ const retried = await Promise.allSettled([...retryWrites.values()].map(persistIntent));
271
+ if (retried.some(result => result.status === "rejected"))
272
+ schedule(exports.NOTICE_RETRY_MS);
230
273
  const entries = (0, notice_outbox_1.readNoticeOutbox)(options.agentRoot).filter(entry => noticeWanted(policy, entry.record) && (Object.keys(entry.deliveries).length === 0 || Object.values(entry.deliveries).some(value => value.state !== "sent")));
231
274
  if (entries.length === 0)
232
275
  return;
@@ -239,6 +282,8 @@ function createOwnerNotifier(options) {
239
282
  // Without the host's handles, each notice names a short session id instead.
240
283
  }
241
284
  const targets = await resolveTargets();
285
+ if (stopped)
286
+ return;
242
287
  if (targets.length === 0) {
243
288
  (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_notice_no_target", message: "no family friend is reachable on Teams, so the Shepherd owner notice was not sent", meta: { agent, returns: entries.length } });
244
289
  schedule(exports.NOTICE_RETRY_MS);
@@ -248,15 +293,21 @@ function createOwnerNotifier(options) {
248
293
  const reachable = new Set(targets.map(target => target.friendId));
249
294
  let retry = entries.some(entry => Object.entries(entry.deliveries).some(([friendId, delivery]) => delivery.state === "pending" && !reachable.has(friendId)));
250
295
  for (const target of targets) {
296
+ if (stopped)
297
+ break;
251
298
  const pending = entries.filter(entry => (0, notice_outbox_1.noticeDeliveryState)(entry, target.friendId) === "pending");
252
- for (let start = 0; start < pending.length && !stopped; start += exports.NOTICE_LIST_MAX) {
299
+ if (pending.length > 0) {
253
300
  // Only the worker that atomically claims a pending entry may initiate transport.
254
- const batch = await (0, notice_outbox_1.claimPendingNotices)(options.agentRoot, pending.slice(start, start + exports.NOTICE_LIST_MAX).map(entry => entry.id), target.friendId, new Date().toISOString());
301
+ const batch = await (0, notice_outbox_1.claimPendingNotices)(options.agentRoot, pending.map(entry => entry.id), target.friendId, new Date().toISOString());
255
302
  if (batch.length === 0)
256
303
  continue;
257
304
  const ids = batch.map(entry => entry.id);
305
+ if (stopped) {
306
+ await (0, notice_outbox_1.recordNoticeDelivery)(options.agentRoot, ids, target.friendId, { state: "pending", at: new Date().toISOString(), reason: "stopped before transport" });
307
+ break;
308
+ }
258
309
  try {
259
- const result = await send(target, noticeMessage(batch.map(entry => entry.record), refs));
310
+ const result = await send(target, noticeMessage(batch.map(entry => entry.record), refs, { recovered: ids.some(id => recoveredNoticeIds.has(id)) }));
260
311
  const state = result.delivered ? "sent"
261
312
  : result.delivery ? result.delivery.state === "not_sent" ? "pending" : "unknown"
262
313
  : result.reason && result.reason !== "send_error" ? "pending" : "unknown";
@@ -288,19 +339,20 @@ function createOwnerNotifier(options) {
288
339
  function flush() {
289
340
  if (flushing)
290
341
  return flushing;
291
- flushing = runFlush().finally(() => { flushing = null; });
342
+ flushing = runFlush().finally(() => {
343
+ flushing = null;
344
+ if (drainRequested) {
345
+ drainRequested = false;
346
+ schedule(exports.NOTICE_COALESCE_MS);
347
+ }
348
+ });
292
349
  return flushing;
293
350
  }
294
351
  try {
295
352
  const existing = (0, notice_outbox_1.readNoticeOutbox)(options.agentRoot);
296
- let recovered = false;
297
- for (const record of (0, returns_1.readReturns)(options.agentRoot)) {
298
- if (record.noticeEligible === true && noticeWanted(policy, record) && !(0, notice_outbox_1.knownNotice)(options.agentRoot, (0, notice_outbox_1.noticeRecordId)(record), existing)) {
299
- void persistIntent(record);
300
- recovered = true;
301
- }
302
- }
303
- if (recovered || existing.some(entry => Object.keys(entry.deliveries).length === 0 || Object.values(entry.deliveries).some(value => value.state === "pending")))
353
+ for (const entry of existing)
354
+ recoveredNoticeIds.add(entry.id);
355
+ if (existing.some(entry => Object.keys(entry.deliveries).length === 0 || Object.values(entry.deliveries).some(value => value.state === "pending")))
304
356
  schedule(exports.NOTICE_RETRY_MS);
305
357
  }
306
358
  catch (error) {
@@ -309,11 +361,13 @@ function createOwnerNotifier(options) {
309
361
  }
310
362
  return {
311
363
  notify(record) {
312
- if (stopped || !noticeWanted(policy, record))
313
- return Promise.resolve();
364
+ if (stopped)
365
+ return Promise.resolve({ state: "suppressed", reason: "stopped" });
366
+ if (!noticeWanted(policy, record))
367
+ return Promise.resolve({ state: "suppressed", reason: "policy" });
314
368
  const write = persistIntent(record);
315
369
  schedule(exports.NOTICE_COALESCE_MS);
316
- return write;
370
+ return write.then(noticeId => ({ state: "queued", noticeId }));
317
371
  },
318
372
  flush,
319
373
  stop() {
@@ -368,6 +368,7 @@ async function startShepherdSenseApp(options) {
368
368
  guidanceRevision: String(record.guidanceRevision), reply: "key" in reply ? `key:${reply.key}` : reply.text,
369
369
  trigger: record.trigger, transition: record.transition, at: record.at }, dispatchOwner);
370
370
  if (!claim) {
371
+ record.actionBlockReason = "custody";
371
372
  record.reason = `durable dispatch custody refused duplicate or unresolved input: ${record.reason}`;
372
373
  return false;
373
374
  }
@@ -385,6 +386,7 @@ async function startShepherdSenseApp(options) {
385
386
  }
386
387
  }
387
388
  catch (error) {
389
+ record.actionBlockReason = "custody";
388
390
  record.dispatch.state = "unknown";
389
391
  try {
390
392
  custody.completeDispatch(claim.id, dispatchOwner, { state: "unknown", reason: field((0, redact_1.redactSecrets)(String(error)), 200) });
@@ -399,6 +401,7 @@ async function startShepherdSenseApp(options) {
399
401
  ...(result.reason ? { reason: field((0, redact_1.redactSecrets)(result.reason), 200) } : {}) });
400
402
  record.dispatch.state = result.state;
401
403
  if (result.state === "not_sent") {
404
+ record.actionBlockReason = "state_changed";
402
405
  record.reason = `guarded host refused before input: ${result.reason ?? "target changed"}; ${record.reason}`;
403
406
  return false;
404
407
  }
@@ -420,18 +423,26 @@ async function startShepherdSenseApp(options) {
420
423
  /** False for a return nothing new was decided about: the loop guard still holds, or the same background wait. */
421
424
  let notice = true;
422
425
  let noticeSignature = null;
426
+ let observationSignature = null;
427
+ let noticeBasis = null;
428
+ let status = null;
429
+ let quietWorkingStatus = false;
430
+ let escalateOutcome = false;
431
+ let wakeOutcome = false;
423
432
  let judged = false;
424
433
  try {
425
434
  let current;
426
435
  if (periodic) {
427
436
  notice = false;
428
437
  if (periodic.generation !== generationFor(event.sessionId) || now() - periodic.observedAt < cmux_1.QUIET_MS) {
438
+ record.actionBlockReason = "state_changed";
429
439
  record.reason = "the check-in observation was invalidated or has not been quiet long enough";
430
440
  return;
431
441
  }
432
442
  current = await host.inspect(event.sessionId);
433
443
  if (stopped || periodic.generation !== generationFor(event.sessionId) ||
434
444
  inspectionFingerprint(current) !== inspectionFingerprint(periodic.first) || current.session.focused) {
445
+ record.actionBlockReason = stopped ? "stopped" : current.session.focused ? "focus" : "state_changed";
435
446
  record.reason = "the check-in target changed during the quiet interval";
436
447
  return;
437
448
  }
@@ -440,6 +451,7 @@ async function startShepherdSenseApp(options) {
440
451
  if (state.tripped) {
441
452
  // Tripped: no judge call and no new escalation until the human looks at the session.
442
453
  Object.assign(record, { kind: "loop_guard", reason: `Shepherd stopped answering this session until ${human} focuses it` });
454
+ record.actionBlockReason = "loop_guard";
443
455
  notice = false;
444
456
  return;
445
457
  }
@@ -456,11 +468,21 @@ async function startShepherdSenseApp(options) {
456
468
  record.task = [...text.matchAll(/Desk-Task:\s*(\S+)/g)].pop()?.[1] ?? null;
457
469
  const screen = (0, redact_1.screenTail)(text, exports.SCREEN_LINES);
458
470
  if (recovery && screen !== recovery.screen) {
471
+ record.actionBlockReason = "state_changed";
459
472
  record.reason = "the screen changed since the network stop, so recovery was not sent";
460
473
  return;
461
474
  }
462
475
  const target = { host: host.name, session: event.sessionId, cwd: record.cwd, task: record.task };
463
- const guidance = guidanceFor(target);
476
+ const checkedGuidance = () => {
477
+ try {
478
+ return guidanceFor(target);
479
+ }
480
+ catch (error) {
481
+ record.actionBlockReason = "guidance";
482
+ throw error;
483
+ }
484
+ };
485
+ const guidance = checkedGuidance();
464
486
  record.guidanceRevision = guidance.revision;
465
487
  let verdict;
466
488
  try {
@@ -478,117 +500,172 @@ async function startShepherdSenseApp(options) {
478
500
  }
479
501
  judged = true;
480
502
  if (periodic && (stopped || periodic.generation !== generationFor(event.sessionId))) {
503
+ record.actionBlockReason = stopped ? "stopped" : "state_changed";
481
504
  record.reason = "the check-in was invalidated during judgment; no input or notice sent";
482
505
  return;
483
506
  }
484
507
  // The host's own word that background work is running outranks a reading of the screen: such a stop is never a return.
485
508
  const kind = (current ? current.backgroundWork : event.backgroundWork) ? "working" : verdict.kind;
486
509
  const news = verdict.news === true || (kind === "working" && verdict.kind === "gate");
487
- Object.assign(record, { kind, reason: verdict.reason, latencyMs: verdict.latencyMs, inputTokens: verdict.inputTokens });
510
+ Object.assign(record, { kind, judgeKind: verdict.kind, reason: verdict.reason, latencyMs: verdict.latencyMs, inputTokens: verdict.inputTokens,
511
+ ...(verdict.replyIssue ? { replyIssue: verdict.replyIssue } : {}) });
488
512
  if (!recovery && !periodic && kind === "waiting" && verdict.waitingOnNetwork)
489
513
  rememberNetworkWait(event, screen);
490
- noticeSignature = fingerprint([host.name, event.sessionId, record.agent, record.cwd, record.task, screen, guidance.revision, kind]);
491
- notice = !periodic || custody.shouldNotify(noticeSignature);
514
+ const noticeScreen = screen.replace(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\b/g, "<observation-time>");
515
+ noticeBasis = [host.name, event.sessionId, record.agent, record.cwd, record.task, noticeScreen, guidance.revision];
516
+ notice = true;
492
517
  // A terminal that keeps going quiet without needing anything is checked less often.
493
518
  if (!periodic)
494
519
  host.pace?.(event.sessionId, kind === "working" || kind === "unclear");
495
520
  state.answers = state.answers.filter((at) => now() - at < 3_600_000);
496
521
  if (kind === "working") {
497
522
  // Still working: a progress update, never an answer. It reaches the Ouro agent only when it carries news, and wakes nothing.
498
- if (news && notice)
499
- escalate(escalationContent(record, human));
523
+ if (news)
524
+ escalateOutcome = true;
500
525
  else
501
526
  notice = false;
502
- if (!periodic || notice)
503
- await host.signal(event.sessionId, field(`${record.agent ?? "agent"} waiting on its background work: ${verdict.reason}`, 120));
527
+ quietWorkingStatus = !periodic && !news;
528
+ status = `${record.agent ?? "agent"} waiting on its background work: ${verdict.reason}`;
504
529
  }
505
- if (kind === "premature" && verdict.reply) {
506
- if ((recovery || periodic) && "key" in verdict.reply) {
530
+ const actionableReply = verdict.reply ?? (periodic && verdict.replyIssue === "too_long" ? { text: exports.PERIODIC_RESUME } : null);
531
+ if (kind === "premature" && !actionableReply)
532
+ record.actionBlockReason = "reply_invalid";
533
+ if (kind === "premature" && actionableReply) {
534
+ if ((recovery || periodic) && "key" in actionableReply) {
535
+ record.actionBlockReason = "menu_disallowed";
507
536
  record.reason = `${periodic ? "periodic check-in" : "connectivity recovery"} never presses approval or menu keys: ${verdict.reason}`;
508
537
  }
509
538
  else if (periodic && (!current.instance || current.activity !== "idle" || current.composer !== "empty" ||
510
539
  current.backgroundWork !== false || !current.session.agent || !host.promptIfUnchanged)) {
540
+ record.actionBlockReason = "host_unverified";
511
541
  record.reason = `the host cannot verify an idle, empty, supported agent process for guarded input: ${verdict.reason}`;
512
542
  }
513
543
  else if (guidance.paused) {
544
+ record.actionBlockReason = "guidance";
514
545
  record.reason = `Shepherd guidance has this session paused, so nothing was sent: ${verdict.reason}`;
515
546
  }
516
547
  else if (state.streak >= exports.STREAK_MAX || state.answers.length >= exports.HOURLY_MAX) {
517
548
  Object.assign(record, { kind: "loop_guard", reason: `loop guard: ${verdict.reason}` });
549
+ record.actionBlockReason = "loop_guard";
518
550
  state.tripped = true;
519
551
  }
520
552
  else {
521
553
  const sessions = await host.list();
522
554
  const session = sessions.find(session => session.id === event.sessionId);
523
555
  if (now() - state.focusedAt < exports.FOCUS_HOLD_MS || session?.focused) {
556
+ record.actionBlockReason = "focus";
524
557
  // Never type into the session the human is in, or one they just left.
525
558
  record.reason = `${human} is in this session or just left it, so it is theirs: ${verdict.reason}`;
526
559
  }
527
560
  else if (periodic && (!session || session.agent !== current.session.agent || session.cwd !== current.session.cwd)) {
561
+ record.actionBlockReason = "state_changed";
528
562
  record.reason = `the check-in target changed while judging, so nothing was sent: ${verdict.reason}`;
529
563
  }
530
564
  else if (!periodic && (0, redact_1.withoutNumbers)((0, redact_1.screenTail)((0, redact_1.redactSecrets)(await host.read(event.sessionId, exports.READ_LINES)), exports.SCREEN_LINES)) !== (0, redact_1.withoutNumbers)(screen)) {
565
+ record.actionBlockReason = "state_changed";
531
566
  record.reason = `the screen changed while judging, so nothing was sent: ${verdict.reason}`;
532
567
  }
533
568
  else {
534
569
  const finalInspection = periodic ? await host.inspect(event.sessionId) : undefined;
535
570
  if (periodic && (stopped || periodic.generation !== generationFor(event.sessionId) ||
536
571
  finalInspection.session.focused || inspectionFingerprint(finalInspection) !== inspectionFingerprint(current))) {
572
+ record.actionBlockReason = stopped ? "stopped" : finalInspection.session.focused ? "focus" : "state_changed";
537
573
  record.reason = `the check-in changed while judging, so nothing was sent: ${verdict.reason}`;
538
574
  return;
539
575
  }
540
576
  // No awaited host reads between this final check and initiating the send.
541
- const latest = guidanceFor(target);
577
+ const latest = checkedGuidance();
542
578
  if (stopped || (recovery && (latestReturns.get(event.sessionId) !== event || now() - state.focusedAt < exports.FOCUS_HOLD_MS)) ||
543
579
  periodic && (periodic.generation !== generationFor(event.sessionId) || now() - state.focusedAt < exports.FOCUS_HOLD_MS)) {
580
+ record.actionBlockReason = stopped ? "stopped" : now() - state.focusedAt < exports.FOCUS_HOLD_MS ? "focus" : "state_changed";
544
581
  record.reason = `Shepherd stopped, the human focused this session, or it returned again while judging, so nothing was sent: ${verdict.reason}`;
545
582
  }
546
583
  else if (latest.paused || latest.revision !== guidance.revision) {
584
+ record.actionBlockReason = "guidance";
547
585
  record.reason = `Shepherd guidance paused or changed while judging (revision ${guidance.revision} to ${latest.revision}), so nothing was sent: ${verdict.reason}`;
548
586
  }
549
587
  else {
550
- const reply = periodic ? { text: exports.PERIODIC_RESUME } : recovery ? { text: NETWORK_RESUME } : verdict.reply;
588
+ const reply = periodic ? { text: exports.PERIODIC_RESUME } : recovery ? { text: NETWORK_RESUME } : actionableReply;
551
589
  const canSend = periodic ? () => {
552
590
  if (stopped || periodic.generation !== generationFor(event.sessionId) || now() - state.focusedAt < exports.FOCUS_HOLD_MS)
553
591
  return false;
554
- const live = guidanceFor(target);
592
+ const live = checkedGuidance();
555
593
  return !live.paused && live.revision === guidance.revision;
556
594
  } : undefined;
557
595
  if (await dispatch(record, reply, current?.instance ?? session?.instance ?? null, fingerprint(screen), periodic ? current : undefined, canSend)) {
558
596
  Object.assign(record, { action: "respond", reply: "key" in reply ? `key:${reply.key}` : reply.text });
559
597
  state.streak += 1;
560
598
  state.answers.push(now());
561
- if (!periodic || notice)
562
- await host.signal(event.sessionId, field(`answered for ${human}: ${verdict.reason}`, 120));
599
+ status = `answered for ${human}: ${verdict.reason}`;
563
600
  }
564
601
  }
565
602
  }
566
603
  }
567
604
  }
568
- if ((!periodic || notice) && (record.kind === "gate" || record.kind === "done" || record.kind === "waiting" || record.kind === "loop_guard")) {
569
- escalate(escalationContent(record, human));
570
- requestWake(record.transition);
571
- await host.signal(event.sessionId, field(`${record.kind === "gate" ? `needs ${human}` : record.kind === "done" ? "done" : record.kind === "waiting" ? "waiting on an outside dependency" : "stopped answering"}: ${verdict.reason}`, 120));
605
+ if (record.kind === "gate" || record.kind === "done" || record.kind === "waiting" || record.kind === "loop_guard") {
606
+ escalateOutcome = true;
607
+ wakeOutcome = true;
608
+ status = `${record.kind === "gate" ? `needs ${human}` : record.kind === "done" ? "done" : record.kind === "waiting" ? "waiting on an outside dependency" : "stopped answering"}: ${verdict.reason}`;
572
609
  }
573
610
  }
574
611
  catch (error) {
575
612
  Object.assign(record, { kind: record.reason ? record.kind : "error", reason: `${record.reason ? `${record.reason}; then ` : ""}${error.message}` });
576
613
  }
577
614
  finally {
615
+ if (noticeBasis && notice) {
616
+ observationSignature = fingerprint([...noticeBasis, record.kind, record.actionBlockReason === "custody" ? null : record.actionBlockReason ?? null]);
617
+ noticeSignature = record.action === "respond" ? fingerprint([observationSignature, "respond", record.reply])
618
+ : record.dispatch?.state === "unknown" ? fingerprint([observationSignature, "unknown"]) : observationSignature;
619
+ try {
620
+ notice = custody.shouldNotify(noticeSignature);
621
+ }
622
+ catch (error) {
623
+ notice = false;
624
+ record.noticeState = "failed";
625
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "could not compare owner-notice suppression; no new delivery intent accepted", meta: { agent, reason: "signature_read_failed", error: error.message } });
626
+ }
627
+ }
628
+ try {
629
+ if (notice && escalateOutcome) {
630
+ escalate(escalationContent(record, human));
631
+ if (wakeOutcome)
632
+ requestWake(record.transition);
633
+ }
634
+ if (status && (notice || quietWorkingStatus))
635
+ await host.signal(event.sessionId, field(status, 120));
636
+ }
637
+ catch (error) {
638
+ record.reason += `; outcome signaling failed: ${error.message}`;
639
+ }
578
640
  record.noticeEligible = notice;
579
- log(record);
580
- if (judged || !periodic)
581
- history.set(event.sessionId, record);
582
641
  if (notice) {
583
642
  try {
584
- await notifyOwner(record);
585
- if (noticeSignature)
586
- custody.markNotified(noticeSignature, record.at);
643
+ const acceptance = await notifyOwner(record);
644
+ record.noticeState = acceptance.state;
645
+ if (acceptance.state === "queued")
646
+ record.noticeId = acceptance.noticeId;
647
+ else
648
+ record.noticeSuppressionReason = acceptance.reason;
649
+ if (noticeSignature && (acceptance.state === "queued" || acceptance.reason === "policy")) {
650
+ try {
651
+ custody.markNotified(noticeSignature, record.at);
652
+ // A sent/uncertain effect also settles the unchanged observation, not just that action outcome.
653
+ if (observationSignature && observationSignature !== noticeSignature)
654
+ custody.markNotified(observationSignature, record.at);
655
+ }
656
+ catch (error) {
657
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "owner notice was accepted but its suppression signature could not be recorded", meta: { agent, error: error.message } });
658
+ }
659
+ }
587
660
  }
588
661
  catch (error) {
589
- (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "owner notice custody failed; the eligible return remains logged for recovery", meta: { agent, error: error.message } });
662
+ record.noticeState = "failed";
663
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "owner notice custody failed; the observation is not accepted delivery intent", meta: { agent, error: error.message } });
590
664
  }
591
665
  }
666
+ log(record);
667
+ if (judged || !periodic)
668
+ history.set(event.sessionId, record);
592
669
  }
593
670
  }
594
671
  function guidanceFor(target) {
@@ -72,6 +72,7 @@ const tool_activity_callbacks_1 = require("../heart/tool-activity-callbacks");
72
72
  const commands_2 = require("./commands");
73
73
  const http = __importStar(require("http"));
74
74
  const path = __importStar(require("path"));
75
+ const node_crypto_1 = require("node:crypto");
75
76
  const trust_gate_1 = require("./trust-gate");
76
77
  const pipeline_1 = require("./pipeline");
77
78
  const session_transaction_1 = require("../mind/session-transaction");
@@ -195,6 +196,11 @@ function buildWelcomeCard() {
195
196
  // safeSend (ctx.send). Tool results, kicks, and errors use safeUpdate
196
197
  // (transient status) or safeSend (terminal errors). Reasoning is accumulated
197
198
  // and periodically pushed via safeUpdate on the same flush timer tick.
199
+ function isTeamsSizeRejection(error) {
200
+ return typeof error === "object" && error !== null && "response" in error
201
+ && typeof error.response === "object" && error.response !== null && "status" in error.response
202
+ && error.response.status === 413;
203
+ }
198
204
  function createTeamsCallbacks(stream, controller, sendMessage, options) {
199
205
  const MIN_INITIAL_CHARS = 20;
200
206
  let stopped = false; // set when stream signals cancellation (403)
@@ -298,18 +304,22 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
298
304
  /* v8 ignore next -- defensive guard: stopped set by prior error; tested via flush abort path @preserve */
299
305
  if (stopped)
300
306
  return false;
307
+ let awaitingTransport = false;
301
308
  try {
302
309
  // stream.emit() is typed as void but the Teams SDK returns a Promise
303
310
  // internally (async HTTP). Cast to capture the result for awaiting.
304
311
  const result = stream.emit(streamChunk(text));
305
312
  streamHasContent = true;
306
313
  if (result && typeof result.then === "function") {
314
+ awaitingTransport = true;
307
315
  await result;
308
316
  }
309
317
  return true;
310
318
  }
311
319
  catch (err) {
312
320
  markStopped("tryEmit", err);
321
+ if (awaitingTransport && !isTeamsSizeRejection(err))
322
+ throw err;
313
323
  return false;
314
324
  }
315
325
  }
@@ -321,15 +331,21 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
321
331
  // which has a sendMessage fallback that may still succeed. Caller (flushNow) is
322
332
  // responsible for the `!stopped` precondition; no defensive guard here.
323
333
  async function tryEmitNoAbort(text) {
334
+ let awaitingTransport = false;
324
335
  try {
325
336
  const result = stream.emit(streamChunk(text));
326
337
  streamHasContent = true;
327
338
  if (result && typeof result.then === "function") {
339
+ awaitingTransport = true;
328
340
  await result;
329
341
  }
330
342
  return true;
331
343
  }
332
- catch {
344
+ catch (error) {
345
+ if (awaitingTransport && !isTeamsSizeRejection(error)) {
346
+ markStopped("speak-unknown", error);
347
+ throw error;
348
+ }
333
349
  return false;
334
350
  }
335
351
  }
@@ -606,7 +622,10 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
606
622
  try {
607
623
  await sendMessage(text);
608
624
  }
609
- catch {
625
+ catch (error) {
626
+ // A lost acknowledgement is not a rejection. Only a definite size rejection permits a smaller retry.
627
+ if (!isTeamsSizeRejection(error) || text.length <= RECOVERY_CHUNK_SIZE)
628
+ throw error;
610
629
  const chunks = splitMessage(text, RECOVERY_CHUNK_SIZE);
611
630
  for (const chunk of chunks)
612
631
  await sendMessage(chunk);
@@ -636,19 +655,34 @@ const _turnCoordinator = (0, turn_coordinator_1.createTurnCoordinator)();
636
655
  function teamsTurnKey(conversationId) {
637
656
  return `teams:${conversationId}`;
638
657
  }
658
+ function teamsAdmissionKey(conversationId) {
659
+ return `teams-admission:${conversationId}`;
660
+ }
639
661
  const queuedTeamsTurns = new Map();
640
662
  const activeTeamsActors = new Map();
663
+ const consumingTeamsSteering = new Set();
664
+ function drainTeamsSteering(key) {
665
+ const followUps = _turnCoordinator.drainFollowUps(key);
666
+ const consumed = new Set(followUps);
667
+ const queue = queuedTeamsTurns.get(key);
668
+ if (queue)
669
+ queuedTeamsTurns.set(key, queue.filter(turn => !turn.steering || !consumed.has(turn.steering)));
670
+ return followUps;
671
+ }
641
672
  /** Context-bearing replies cannot steer an already-running tool loop. Keep the
642
673
  * original SDK context/transport alive until a fresh handleTeamsMessage lease.
643
674
  * Ordinary same-actor steering still uses the existing coordinator buffer. */
644
675
  async function coordinateTeamsTurn(conversationId, text, identity, run) {
645
676
  const key = teamsTurnKey(conversationId);
646
- const actorKey = JSON.stringify([identity.aadObjectId ?? identity.userId ?? "", identity.tenantId ?? ""]);
677
+ const actorKey = JSON.stringify([identity.aadObjectId || identity.userId || "", identity.tenantId ?? ""]);
647
678
  if (!_turnCoordinator.tryBeginTurn(key)) {
648
- if (!identity.replyToId && !/\bfeedback:/i.test(text) &&
649
- activeTeamsActors.get(key) === actorKey && !queuedTeamsTurns.get(key)?.length) {
650
- _turnCoordinator.enqueueFollowUp(key, { conversationId, text, receivedAt: Date.now(),
651
- effect: (0, continuity_1.classifySteeringFollowUpEffect)(text) });
679
+ if (!identity.replyToId && !/\bfeedback:/i.test(text) && consumingTeamsSteering.has(key) &&
680
+ activeTeamsActors.get(key) === actorKey && !queuedTeamsTurns.get(key)?.some(turn => !turn.steering)) {
681
+ const steering = { conversationId, text, receivedAt: Date.now(), effect: (0, continuity_1.classifySteeringFollowUpEffect)(text) };
682
+ _turnCoordinator.enqueueFollowUp(key, steering);
683
+ const queue = queuedTeamsTurns.get(key) ?? [];
684
+ queue.push({ actorKey, run, steering, resolve: () => { }, reject: () => { } });
685
+ queuedTeamsTurns.set(key, queue);
652
686
  return;
653
687
  }
654
688
  await new Promise((resolve, reject) => {
@@ -664,6 +698,12 @@ async function coordinateTeamsTurn(conversationId, text, identity, run) {
664
698
  try {
665
699
  while (next) {
666
700
  activeTeamsActors.set(key, next.actorKey);
701
+ if (next.steering) {
702
+ for (const followUp of _turnCoordinator.drainFollowUps(key)) {
703
+ if (followUp !== next.steering)
704
+ _turnCoordinator.enqueueFollowUp(key, followUp);
705
+ }
706
+ }
667
707
  try {
668
708
  await next.run();
669
709
  next.resolve();
@@ -674,6 +714,8 @@ async function coordinateTeamsTurn(conversationId, text, identity, run) {
674
714
  next.reject(error);
675
715
  }
676
716
  first = false;
717
+ // Preserve received order while identity/admission I/O finishes, without holding admission through a model turn.
718
+ await _turnCoordinator.withTurnLock(teamsAdmissionKey(conversationId), async () => { });
677
719
  next = queuedTeamsTurns.get(key)?.shift();
678
720
  }
679
721
  }
@@ -694,6 +736,14 @@ function getFriendStore() {
694
736
  const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
695
737
  return new friends_1.FileFriendStore(friendsPath);
696
738
  }
739
+ function reportTeamsRequest(stage, conversationId, context, details = {}) {
740
+ if (!context?.inboundActivityId)
741
+ return;
742
+ (0, runtime_1.emitNervesEvent)({
743
+ component: "channels", event: "channel.teams_request_stage", message: "Teams conversational request stage",
744
+ meta: { stage, inboundActivityId: context.inboundActivityId, conversationId, tenantId: context.tenantId, botId: context.botApi?.id, ...details },
745
+ });
746
+ }
697
747
  function createTeamsCommandRegistry() {
698
748
  const registry = (0, commands_1.createCommandRegistry)();
699
749
  (0, commands_1.registerDefaultCommands)(registry);
@@ -750,14 +800,24 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
750
800
  // ── Teams adapter concerns: controller, callbacks, session path ──────────
751
801
  const controller = new AbortController();
752
802
  const channelConfig = (0, config_2.getTeamsChannelConfig)();
753
- const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs, ...(reactionOverrides?.suppressEmptyStreamMessage ? { suppressEmptyStreamMessage: true } : {}) });
754
803
  const traceId = (0, nerves_1.createTraceId)();
804
+ const observedStream = {
805
+ emit(activity) {
806
+ const result = stream.emit(activity);
807
+ reportTeamsRequest("reply_emitted", conversationId, teamsContext, { traceId, delivery: "unconfirmed_stream" });
808
+ return result;
809
+ },
810
+ update(text) { return stream.update(text); },
811
+ close: stream.close.bind(stream),
812
+ };
813
+ const callbacks = createTeamsCallbacks(observedStream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs, ...(reactionOverrides?.suppressEmptyStreamMessage ? { suppressEmptyStreamMessage: true } : {}) });
755
814
  const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
756
815
  const teamsCapabilities = (0, friends_1.getChannelCapabilities)("teams");
757
816
  const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, "teams", conversationId);
758
817
  const runWithLease = runtimeOverrides?._withSessionTurnLease ?? session_transaction_1.withSessionTurnLease;
759
818
  try {
760
819
  await runWithLease(sessPath, async (sessionTurnLease) => {
820
+ reportTeamsRequest("running", conversationId, teamsContext, { traceId, friendId });
761
821
  // Proactive feedback acknowledgements are not magically in this model session's
762
822
  // history. Recover their frozen packet only after identity and lease ownership.
763
823
  let feedbackReply;
@@ -768,7 +828,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
768
828
  catch (error) {
769
829
  if (!(error instanceof feedback_reply_1.FeedbackReplyRefusal))
770
830
  throw error;
771
- stream.emit(error.message);
831
+ await stream.emit(error.message);
772
832
  return;
773
833
  }
774
834
  // Do not emit any activity until this turn owns the durable session.
@@ -800,7 +860,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
800
860
  },
801
861
  mcpManager,
802
862
  drainSteeringFollowUps: () => {
803
- drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
863
+ drainedSteeringFollowUps = drainTeamsSteering(turnKey)
804
864
  .map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
805
865
  return drainedSteeringFollowUps;
806
866
  },
@@ -863,16 +923,24 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
863
923
  enforceTrustGate: trust_gate_1.enforceTrustGate,
864
924
  drainPending: pending_1.drainPending,
865
925
  drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(agentName, deferredFriendId),
866
- runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
867
- ...opts,
868
- toolContext: {
869
- /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
870
- signin: async () => undefined,
871
- ...opts?.toolContext,
872
- ...owner,
873
- summarize: teamsToolContext.summarize,
874
- },
875
- }),
926
+ runAgent: async (msgs, cb, channel, sig, opts) => {
927
+ consumingTeamsSteering.add(turnKey);
928
+ try {
929
+ return await (0, core_1.runAgent)(msgs, cb, channel, sig, {
930
+ ...opts,
931
+ toolContext: {
932
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
933
+ signin: async () => undefined,
934
+ ...opts?.toolContext,
935
+ ...owner,
936
+ summarize: teamsToolContext.summarize,
937
+ },
938
+ });
939
+ }
940
+ finally {
941
+ consumingTeamsSteering.delete(turnKey);
942
+ }
943
+ },
876
944
  postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
877
945
  const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
878
946
  return (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
@@ -898,7 +966,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
898
966
  if (result.turnOutcome === "command") {
899
967
  if (result.commandAction === "new") {
900
968
  (0, context_1.deleteSession)(sessPath);
901
- stream.emit("session cleared");
969
+ await stream.emit("session cleared");
902
970
  }
903
971
  // For "response" commands: pipeline already emitted the response via onTextChunk
904
972
  await callbacks.flush();
@@ -906,7 +974,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
906
974
  }
907
975
  /* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
908
976
  if (result.failoverMessage) {
909
- stream.emit(result.failoverMessage);
977
+ await stream.emit(result.failoverMessage);
910
978
  }
911
979
  else if (capturedTerminalError) {
912
980
  callbacks.onError(capturedTerminalError, "terminal");
@@ -915,7 +983,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
915
983
  // ── Handle gate result ────────────────────────────────────────
916
984
  if (!result.gateResult.allowed) {
917
985
  if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
918
- stream.emit(result.gateResult.autoReply);
986
+ await stream.emit(result.gateResult.autoReply);
919
987
  }
920
988
  return;
921
989
  }
@@ -962,9 +1030,11 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
962
1030
  }
963
1031
  catch (error) {
964
1032
  if (error instanceof session_transaction_1.SessionTurnBusyError || (error instanceof Error && error.name === "SessionTurnBusyError")) {
965
- stream.emit("this conversation is busy finishing another turn; please try again");
1033
+ reportTeamsRequest("failed", conversationId, teamsContext, { traceId, reason: "lease_busy" });
1034
+ await stream.emit("this conversation is busy finishing another turn; please try again");
966
1035
  return;
967
1036
  }
1037
+ reportTeamsRequest("failed", conversationId, teamsContext, { traceId, reason: "turn_failed" });
968
1038
  throw error;
969
1039
  }
970
1040
  }
@@ -1153,121 +1223,182 @@ function registerBotHandlers(app, label) {
1153
1223
  });
1154
1224
  /* v8 ignore stop */
1155
1225
  app.on("message", async (ctx) => {
1156
- const { stream, activity, api, signin } = ctx;
1226
+ const { activity, api, signin } = ctx;
1157
1227
  const text = activity.text || "";
1158
1228
  const convId = activity.conversation?.id || "unknown";
1159
1229
  const turnKey = teamsTurnKey(convId);
1160
1230
  const userId = activity.from?.id || "";
1161
1231
  const channelId = activity.channelId || "msteams";
1162
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
1163
- const commandRegistry = createTeamsCommandRegistry();
1164
- const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
1165
- if (parsedSlashCommand) {
1166
- const dispatchResult = commandRegistry.dispatch(parsedSlashCommand.command, { channel: "teams" });
1167
- if (dispatchResult.handled && dispatchResult.result) {
1168
- if (dispatchResult.result.action === "response") {
1169
- stream.emit(dispatchResult.result.message || "");
1170
- return;
1232
+ const requestContext = {
1233
+ inboundActivityId: activity.id, tenantId: activity.conversation?.tenantId,
1234
+ botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
1235
+ };
1236
+ const sendText = async (text) => {
1237
+ try {
1238
+ const receipt = await ctx.send({ type: "message", text, replyToId: activity.id, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
1239
+ const messageId = receipt && typeof receipt === "object" && "id" in receipt && typeof receipt.id === "string" ? receipt.id : undefined;
1240
+ reportTeamsRequest(messageId ? "replied" : "reply_emitted", convId, requestContext, { delivery: messageId ? "accepted" : "unconfirmed", ...(messageId ? { outboundActivityId: messageId } : {}) });
1241
+ }
1242
+ catch (error) {
1243
+ reportTeamsRequest("failed", convId, requestContext, { reason: "send_unknown" });
1244
+ throw error;
1245
+ }
1246
+ };
1247
+ // HttpStream queues void emits and retries unknown HTTP failures internally.
1248
+ // Use the SDK's single-attempt, awaitable send instead of that hidden retry loop.
1249
+ const stream = {
1250
+ emit: value => sendText(typeof value === "string" ? value : value.text),
1251
+ update: () => ctx.send({ type: "typing" }),
1252
+ close: async () => undefined,
1253
+ };
1254
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { stage: "received", inboundActivityId: activity.id, botId: app.id, tenantId: requestContext.tenantId, userId: userId.slice(0, 12), conversationId: convId } });
1255
+ let work;
1256
+ await _turnCoordinator.withTurnLock(teamsAdmissionKey(convId), async () => {
1257
+ try {
1258
+ if (process.env.OURO_TEAMS_DEVTOOLS !== "1" && [
1259
+ app.id, activity.id, activity.channelId, activity.conversation?.id,
1260
+ activity.conversation?.tenantId, activity.from?.aadObjectId || activity.from?.id,
1261
+ ].some(value => typeof value !== "string" || !value.trim())) {
1262
+ throw new session_transaction_1.SessionTransactionError("Teams activity identity is incomplete");
1171
1263
  }
1172
- if (dispatchResult.result.action === "new") {
1173
- const commandStore = getFriendStore();
1174
- const commandProvider = activity.from?.aadObjectId ? "aad" : "teams-conversation";
1175
- const commandExternalId = activity.from?.aadObjectId || convId;
1176
- const commandResolver = new friends_1.FriendResolver(commandStore, {
1177
- provider: commandProvider,
1178
- externalId: commandExternalId,
1264
+ if (activity.id) {
1265
+ const store = getFriendStore();
1266
+ const identity = await new friends_1.FriendResolver(store, {
1267
+ provider: activity.from?.aadObjectId ? "aad" : "teams-conversation",
1268
+ externalId: activity.from?.aadObjectId || convId,
1179
1269
  tenantId: activity.conversation?.tenantId,
1180
1270
  displayName: activity.from?.name || "Unknown",
1181
1271
  channel: "teams",
1272
+ }).resolve();
1273
+ const admission = (0, session_transaction_1.admitSessionActivity)((0, config_2.sessionPath)(identity.friend.id, "teams", convId), {
1274
+ namespace: JSON.stringify(["teams.message", channelId, app.id?.toLowerCase() ?? null,
1275
+ activity.conversation?.tenantId?.toLowerCase() ?? null,
1276
+ activity.from?.aadObjectId?.toLowerCase() || userId, convId]),
1277
+ activityId: activity.id,
1278
+ payloadDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify([text, activity.replyToId ?? null])).digest("hex"),
1182
1279
  });
1183
- const commandContext = await commandResolver.resolve();
1184
- (0, context_1.deleteSession)((0, config_2.sessionPath)(commandContext.friend.id, "teams", convId));
1185
- stream.emit("session cleared");
1186
- if (_turnCoordinator.isTurnActive(turnKey)) {
1187
- _turnCoordinator.enqueueFollowUp(turnKey, {
1188
- conversationId: convId,
1189
- text,
1190
- receivedAt: Date.now(),
1191
- effect: "clear_and_supersede",
1192
- });
1280
+ if (admission.state === "duplicate") {
1281
+ reportTeamsRequest("duplicate_held", convId, requestContext, { receiptId: admission.receiptId, outcome: "not_replayed" });
1282
+ return;
1193
1283
  }
1194
- return;
1195
1284
  }
1285
+ reportTeamsRequest("admitted", convId, requestContext);
1196
1286
  }
1197
- }
1198
- await coordinateTeamsTurn(convId, text, {
1199
- aadObjectId: activity.from?.aadObjectId, userId, tenantId: activity.conversation?.tenantId,
1200
- replyToId: activity.replyToId,
1201
- }, async () => {
1202
- try {
1203
- // Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
1204
- const tenantId = activity.conversation?.tenantId;
1205
- const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
1206
- // Fetch tokens for both OAuth connections independently.
1207
- // Failures are silently caught -- the tool handler will request signin if needed.
1208
- let graphToken;
1209
- let adoToken;
1210
- let githubToken;
1211
- try {
1212
- const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
1213
- graphToken = graphRes?.token;
1287
+ catch (error) {
1288
+ reportTeamsRequest("failed", convId, requestContext, { reason: "activity_admission_failed" });
1289
+ (0, runtime_1.emitNervesEvent)({
1290
+ level: "error", component: "channels", event: "channel.activity_admission_error",
1291
+ message: "Teams activity was not safely admitted; no new turn or steering was started",
1292
+ meta: { conversationId: convId, inboundActivityId: activity.id, reason: "activity_admission_failed" },
1293
+ });
1294
+ throw error;
1295
+ }
1296
+ const commandRegistry = createTeamsCommandRegistry();
1297
+ const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
1298
+ if (parsedSlashCommand) {
1299
+ const dispatchResult = commandRegistry.dispatch(parsedSlashCommand.command, { channel: "teams" });
1300
+ if (dispatchResult.handled && dispatchResult.result) {
1301
+ if (dispatchResult.result.action === "response") {
1302
+ await stream.emit(dispatchResult.result.message || "");
1303
+ return;
1304
+ }
1305
+ if (dispatchResult.result.action === "new") {
1306
+ const commandStore = getFriendStore();
1307
+ const commandProvider = activity.from?.aadObjectId ? "aad" : "teams-conversation";
1308
+ const commandExternalId = activity.from?.aadObjectId || convId;
1309
+ const commandResolver = new friends_1.FriendResolver(commandStore, {
1310
+ provider: commandProvider,
1311
+ externalId: commandExternalId,
1312
+ tenantId: activity.conversation?.tenantId,
1313
+ displayName: activity.from?.name || "Unknown",
1314
+ channel: "teams",
1315
+ });
1316
+ const commandContext = await commandResolver.resolve();
1317
+ (0, context_1.deleteSession)((0, config_2.sessionPath)(commandContext.friend.id, "teams", convId));
1318
+ await stream.emit("session cleared");
1319
+ if (_turnCoordinator.isTurnActive(turnKey)) {
1320
+ _turnCoordinator.enqueueFollowUp(turnKey, {
1321
+ conversationId: convId,
1322
+ text,
1323
+ receivedAt: Date.now(),
1324
+ effect: "clear_and_supersede",
1325
+ });
1326
+ }
1327
+ return;
1328
+ }
1214
1329
  }
1215
- catch { /* no token yet — tool handler will trigger signin */ }
1330
+ }
1331
+ work = coordinateTeamsTurn(convId, text, {
1332
+ aadObjectId: activity.from?.aadObjectId, userId, tenantId: activity.conversation?.tenantId,
1333
+ replyToId: activity.replyToId,
1334
+ }, async () => {
1216
1335
  try {
1217
- const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
1218
- adoToken = adoRes?.token;
1336
+ // Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
1337
+ const tenantId = activity.conversation?.tenantId;
1338
+ const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
1339
+ // Fetch tokens for both OAuth connections independently.
1340
+ // Failures are silently caught -- the tool handler will request signin if needed.
1341
+ let graphToken;
1342
+ let adoToken;
1343
+ let githubToken;
1344
+ try {
1345
+ const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
1346
+ graphToken = graphRes?.token;
1347
+ }
1348
+ catch { /* no token yet — tool handler will trigger signin */ }
1349
+ try {
1350
+ const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
1351
+ adoToken = adoRes?.token;
1352
+ }
1353
+ catch { /* no token yet — tool handler will trigger signin */ }
1354
+ try {
1355
+ const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
1356
+ githubToken = githubRes?.token;
1357
+ }
1358
+ catch { /* no token yet — tool handler will trigger signin */ }
1359
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
1360
+ const teamsContext = {
1361
+ inboundActivityId: activity.id,
1362
+ replyToId: activity.replyToId,
1363
+ graphToken,
1364
+ adoToken,
1365
+ githubToken,
1366
+ signin: async (cn) => {
1367
+ try {
1368
+ const result = await signin({ connectionName: cn });
1369
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.signin_result", component: "channels", message: `signin(${cn}): ${result ? "token received" : "no token"}`, meta: { connectionName: cn, hasToken: !!result } });
1370
+ return result;
1371
+ }
1372
+ catch (e) {
1373
+ const msg = e instanceof Error ? e.message : String(e);
1374
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.signin_error", component: "channels", message: `signin(${cn}) failed`, meta: { connectionName: cn, reason: msg.slice(0, 100) } });
1375
+ return undefined;
1376
+ }
1377
+ },
1378
+ aadObjectId: activity.from?.aadObjectId,
1379
+ tenantId: activity.conversation?.tenantId,
1380
+ displayName: activity.from?.name,
1381
+ graphConnectionName: tenantOAuth.graphConnectionName,
1382
+ adoConnectionName: tenantOAuth.adoConnectionName,
1383
+ githubConnectionName: tenantOAuth.githubConnectionName,
1384
+ /* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
1385
+ botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
1386
+ teamsSendImage: (0, teams_image_1.createTeamsImageSender)((message) => ctx.send(message), activity.id, aiLabelEntities()),
1387
+ };
1388
+ await handleTeamsMessage(text, stream, convId, teamsContext, sendText);
1219
1389
  }
1220
- catch { /* no token yet — tool handler will trigger signin */ }
1221
- try {
1222
- const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
1223
- githubToken = githubRes?.token;
1390
+ catch (err) {
1391
+ reportTeamsRequest("failed", convId, requestContext, { reason: "handler_failed" });
1392
+ const msg = err instanceof Error ? err.message : String(err);
1393
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.handler_error", component: "channels", message: msg.slice(0, 200), meta: { conversationId: convId, inboundActivityId: activity.id, botId: app.id } });
1224
1394
  }
1225
- catch { /* no token yet — tool handler will trigger signin */ }
1226
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
1227
- const teamsContext = {
1228
- replyToId: activity.replyToId,
1229
- graphToken,
1230
- adoToken,
1231
- githubToken,
1232
- signin: async (cn) => {
1233
- try {
1234
- const result = await signin({ connectionName: cn });
1235
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.signin_result", component: "channels", message: `signin(${cn}): ${result ? "token received" : "no token"}`, meta: { connectionName: cn, hasToken: !!result } });
1236
- return result;
1237
- }
1238
- catch (e) {
1239
- const msg = e instanceof Error ? e.message : String(e);
1240
- (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.signin_error", component: "channels", message: `signin(${cn}) failed`, meta: { connectionName: cn, reason: msg.slice(0, 100) } });
1241
- return undefined;
1242
- }
1243
- },
1244
- aadObjectId: activity.from?.aadObjectId,
1245
- tenantId: activity.conversation?.tenantId,
1246
- displayName: activity.from?.name,
1247
- graphConnectionName: tenantOAuth.graphConnectionName,
1248
- adoConnectionName: tenantOAuth.adoConnectionName,
1249
- githubConnectionName: tenantOAuth.githubConnectionName,
1250
- /* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
1251
- botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
1252
- teamsSendImage: (0, teams_image_1.createTeamsImageSender)((message) => ctx.send(message), activity.id, aiLabelEntities()),
1253
- };
1254
- /* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
1255
- const ctxSend = async (t) => {
1256
- // Use send with replyToId (not reply, which adds a blockquote).
1257
- // replyToId anchors the message after the user's message in Copilot Chat.
1258
- await ctx.send({ type: "message", text: t, replyToId: activity.id, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
1259
- };
1260
- await handleTeamsMessage(text, stream, convId, teamsContext, ctxSend);
1261
- }
1262
- catch (err) {
1263
- const msg = err instanceof Error ? err.message : String(err);
1264
- (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
1265
- }
1395
+ });
1266
1396
  });
1397
+ await work;
1267
1398
  });
1268
1399
  app.event("error", ({ error }) => {
1269
1400
  const msg = error instanceof Error ? error.message : String(error);
1270
- (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
1401
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: { botId: app.id, label } });
1271
1402
  });
1272
1403
  }
1273
1404
  function findAadObjectId(friend) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.932",
3
+ "version": "0.1.0-alpha.934",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.932",
9
+ "version": "0.1.0-alpha.934",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.932",
3
+ "version": "0.1.0-alpha.934",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },