@ouro.bot/cli 0.1.0-alpha.933 → 0.1.0-alpha.935
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 +17 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/mind/session-transaction.js +73 -15
- package/dist/repertoire/tools-shell.js +9 -4
- package/dist/senses/teams.js +234 -126
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
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.935",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Keep Teams requests running when advisory typing updates are rejected, with correlated warnings and unchanged single-attempt reply custody.",
|
|
8
|
+
"Run foreground shell commands without blocking receiver I/O, preserving timeouts, closed stdin and command error output."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.934",
|
|
13
|
+
"changes": [
|
|
14
|
+
"Hold duplicate Teams activities durably before model execution, steering or quoted queuing, preserving receipts across restart and session reset.",
|
|
15
|
+
"Do not replay uncertain Teams sends after a lost acknowledgement; split-message recovery requires an explicit HTTP 413 and smaller payloads.",
|
|
16
|
+
"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.",
|
|
17
|
+
"Keep distinct Teams fallback actors separate when optional AAD metadata is empty.",
|
|
18
|
+
"Include bot and request correlation context on Teams handler and SDK error telemetry."
|
|
19
|
+
]
|
|
20
|
+
},
|
|
4
21
|
{
|
|
5
22
|
"version": "0.1.0-alpha.933",
|
|
6
23
|
"changes": [
|
|
@@ -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.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.935</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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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;
|
|
@@ -48,7 +48,7 @@ exports.shellToolDefinitions = [
|
|
|
48
48
|
}
|
|
49
49
|
return { kind: "not_required" };
|
|
50
50
|
},
|
|
51
|
-
handler: (a, ctx) => {
|
|
51
|
+
handler: async (a, ctx) => {
|
|
52
52
|
(0, tool_arguments_1.assertRelationshipToolOwner)(ctx);
|
|
53
53
|
// Destructive pattern detection (friction, not a block)
|
|
54
54
|
const destructivePatterns = (0, shell_sessions_1.detectDestructivePatterns)(a.command);
|
|
@@ -84,9 +84,14 @@ exports.shellToolDefinitions = [
|
|
|
84
84
|
}
|
|
85
85
|
const baseTimeout = requestedTimeout > 0 ? requestedTimeout : configDefault;
|
|
86
86
|
const timeout = Math.min(baseTimeout, MAX_TIMEOUT);
|
|
87
|
-
const output = (
|
|
88
|
-
encoding: "utf-8",
|
|
89
|
-
|
|
87
|
+
const output = await new Promise((resolve, reject) => {
|
|
88
|
+
const child = (0, child_process_1.exec)(a.command, { encoding: "utf-8", timeout }, (error, stdout, stderr) => {
|
|
89
|
+
if (error)
|
|
90
|
+
reject(Object.assign(error, { stdout, stderr }));
|
|
91
|
+
else
|
|
92
|
+
resolve(stdout);
|
|
93
|
+
});
|
|
94
|
+
child.stdin?.end();
|
|
90
95
|
});
|
|
91
96
|
if (destructivePatterns.length > 0) {
|
|
92
97
|
return `${output}\n\n--- destructive pattern detected: ${destructivePatterns.join(", ")} ---`;
|
package/dist/senses/teams.js
CHANGED
|
@@ -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
|
|
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)?.
|
|
650
|
-
|
|
651
|
-
|
|
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
|
}
|
|
@@ -786,7 +828,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
786
828
|
catch (error) {
|
|
787
829
|
if (!(error instanceof feedback_reply_1.FeedbackReplyRefusal))
|
|
788
830
|
throw error;
|
|
789
|
-
stream.emit(error.message);
|
|
831
|
+
await stream.emit(error.message);
|
|
790
832
|
return;
|
|
791
833
|
}
|
|
792
834
|
// Do not emit any activity until this turn owns the durable session.
|
|
@@ -818,7 +860,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
818
860
|
},
|
|
819
861
|
mcpManager,
|
|
820
862
|
drainSteeringFollowUps: () => {
|
|
821
|
-
drainedSteeringFollowUps =
|
|
863
|
+
drainedSteeringFollowUps = drainTeamsSteering(turnKey)
|
|
822
864
|
.map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
|
|
823
865
|
return drainedSteeringFollowUps;
|
|
824
866
|
},
|
|
@@ -881,16 +923,24 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
881
923
|
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
882
924
|
drainPending: pending_1.drainPending,
|
|
883
925
|
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(agentName, deferredFriendId),
|
|
884
|
-
runAgent: (msgs, cb, channel, sig, opts) =>
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
+
},
|
|
894
944
|
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
895
945
|
const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
|
|
896
946
|
return (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
|
|
@@ -916,7 +966,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
916
966
|
if (result.turnOutcome === "command") {
|
|
917
967
|
if (result.commandAction === "new") {
|
|
918
968
|
(0, context_1.deleteSession)(sessPath);
|
|
919
|
-
stream.emit("session cleared");
|
|
969
|
+
await stream.emit("session cleared");
|
|
920
970
|
}
|
|
921
971
|
// For "response" commands: pipeline already emitted the response via onTextChunk
|
|
922
972
|
await callbacks.flush();
|
|
@@ -924,7 +974,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
924
974
|
}
|
|
925
975
|
/* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
|
|
926
976
|
if (result.failoverMessage) {
|
|
927
|
-
stream.emit(result.failoverMessage);
|
|
977
|
+
await stream.emit(result.failoverMessage);
|
|
928
978
|
}
|
|
929
979
|
else if (capturedTerminalError) {
|
|
930
980
|
callbacks.onError(capturedTerminalError, "terminal");
|
|
@@ -933,7 +983,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
933
983
|
// ── Handle gate result ────────────────────────────────────────
|
|
934
984
|
if (!result.gateResult.allowed) {
|
|
935
985
|
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
936
|
-
stream.emit(result.gateResult.autoReply);
|
|
986
|
+
await stream.emit(result.gateResult.autoReply);
|
|
937
987
|
}
|
|
938
988
|
return;
|
|
939
989
|
}
|
|
@@ -981,7 +1031,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
981
1031
|
catch (error) {
|
|
982
1032
|
if (error instanceof session_transaction_1.SessionTurnBusyError || (error instanceof Error && error.name === "SessionTurnBusyError")) {
|
|
983
1033
|
reportTeamsRequest("failed", conversationId, teamsContext, { traceId, reason: "lease_busy" });
|
|
984
|
-
stream.emit("this conversation is busy finishing another turn; please try again");
|
|
1034
|
+
await stream.emit("this conversation is busy finishing another turn; please try again");
|
|
985
1035
|
return;
|
|
986
1036
|
}
|
|
987
1037
|
reportTeamsRequest("failed", conversationId, teamsContext, { traceId, reason: "turn_failed" });
|
|
@@ -1173,7 +1223,7 @@ function registerBotHandlers(app, label) {
|
|
|
1173
1223
|
});
|
|
1174
1224
|
/* v8 ignore stop */
|
|
1175
1225
|
app.on("message", async (ctx) => {
|
|
1176
|
-
const {
|
|
1226
|
+
const { activity, api, signin } = ctx;
|
|
1177
1227
|
const text = activity.text || "";
|
|
1178
1228
|
const convId = activity.conversation?.id || "unknown";
|
|
1179
1229
|
const turnKey = teamsTurnKey(convId);
|
|
@@ -1183,126 +1233,184 @@ function registerBotHandlers(app, label) {
|
|
|
1183
1233
|
inboundActivityId: activity.id, tenantId: activity.conversation?.tenantId,
|
|
1184
1234
|
botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
|
|
1185
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: async () => {
|
|
1252
|
+
try {
|
|
1253
|
+
await ctx.send({ type: "typing" });
|
|
1254
|
+
}
|
|
1255
|
+
catch (error) {
|
|
1256
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1257
|
+
level: "warn", component: "channels", event: "channel.teams_typing_failed",
|
|
1258
|
+
message: "Teams rejected an advisory typing update; the reply is still pending",
|
|
1259
|
+
meta: { conversationId: convId, inboundActivityId: activity.id,
|
|
1260
|
+
error: error instanceof Error ? error.message : String(error) },
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
},
|
|
1264
|
+
close: async () => undefined,
|
|
1265
|
+
};
|
|
1186
1266
|
(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 } });
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1267
|
+
let work;
|
|
1268
|
+
await _turnCoordinator.withTurnLock(teamsAdmissionKey(convId), async () => {
|
|
1269
|
+
try {
|
|
1270
|
+
if (process.env.OURO_TEAMS_DEVTOOLS !== "1" && [
|
|
1271
|
+
app.id, activity.id, activity.channelId, activity.conversation?.id,
|
|
1272
|
+
activity.conversation?.tenantId, activity.from?.aadObjectId || activity.from?.id,
|
|
1273
|
+
].some(value => typeof value !== "string" || !value.trim())) {
|
|
1274
|
+
throw new session_transaction_1.SessionTransactionError("Teams activity identity is incomplete");
|
|
1195
1275
|
}
|
|
1196
|
-
if (
|
|
1197
|
-
const
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
provider: commandProvider,
|
|
1202
|
-
externalId: commandExternalId,
|
|
1276
|
+
if (activity.id) {
|
|
1277
|
+
const store = getFriendStore();
|
|
1278
|
+
const identity = await new friends_1.FriendResolver(store, {
|
|
1279
|
+
provider: activity.from?.aadObjectId ? "aad" : "teams-conversation",
|
|
1280
|
+
externalId: activity.from?.aadObjectId || convId,
|
|
1203
1281
|
tenantId: activity.conversation?.tenantId,
|
|
1204
1282
|
displayName: activity.from?.name || "Unknown",
|
|
1205
1283
|
channel: "teams",
|
|
1284
|
+
}).resolve();
|
|
1285
|
+
const admission = (0, session_transaction_1.admitSessionActivity)((0, config_2.sessionPath)(identity.friend.id, "teams", convId), {
|
|
1286
|
+
namespace: JSON.stringify(["teams.message", channelId, app.id?.toLowerCase() ?? null,
|
|
1287
|
+
activity.conversation?.tenantId?.toLowerCase() ?? null,
|
|
1288
|
+
activity.from?.aadObjectId?.toLowerCase() || userId, convId]),
|
|
1289
|
+
activityId: activity.id,
|
|
1290
|
+
payloadDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify([text, activity.replyToId ?? null])).digest("hex"),
|
|
1206
1291
|
});
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
if (_turnCoordinator.isTurnActive(turnKey)) {
|
|
1211
|
-
_turnCoordinator.enqueueFollowUp(turnKey, {
|
|
1212
|
-
conversationId: convId,
|
|
1213
|
-
text,
|
|
1214
|
-
receivedAt: Date.now(),
|
|
1215
|
-
effect: "clear_and_supersede",
|
|
1216
|
-
});
|
|
1292
|
+
if (admission.state === "duplicate") {
|
|
1293
|
+
reportTeamsRequest("duplicate_held", convId, requestContext, { receiptId: admission.receiptId, outcome: "not_replayed" });
|
|
1294
|
+
return;
|
|
1217
1295
|
}
|
|
1218
|
-
return;
|
|
1219
1296
|
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
await coordinateTeamsTurn(convId, text, {
|
|
1223
|
-
aadObjectId: activity.from?.aadObjectId, userId, tenantId: activity.conversation?.tenantId,
|
|
1224
|
-
replyToId: activity.replyToId,
|
|
1225
|
-
}, async () => {
|
|
1226
|
-
try {
|
|
1227
1297
|
reportTeamsRequest("admitted", convId, requestContext);
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1298
|
+
}
|
|
1299
|
+
catch (error) {
|
|
1300
|
+
reportTeamsRequest("failed", convId, requestContext, { reason: "activity_admission_failed" });
|
|
1301
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1302
|
+
level: "error", component: "channels", event: "channel.activity_admission_error",
|
|
1303
|
+
message: "Teams activity was not safely admitted; no new turn or steering was started",
|
|
1304
|
+
meta: { conversationId: convId, inboundActivityId: activity.id, reason: "activity_admission_failed" },
|
|
1305
|
+
});
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
1308
|
+
const commandRegistry = createTeamsCommandRegistry();
|
|
1309
|
+
const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
|
|
1310
|
+
if (parsedSlashCommand) {
|
|
1311
|
+
const dispatchResult = commandRegistry.dispatch(parsedSlashCommand.command, { channel: "teams" });
|
|
1312
|
+
if (dispatchResult.handled && dispatchResult.result) {
|
|
1313
|
+
if (dispatchResult.result.action === "response") {
|
|
1314
|
+
await stream.emit(dispatchResult.result.message || "");
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (dispatchResult.result.action === "new") {
|
|
1318
|
+
const commandStore = getFriendStore();
|
|
1319
|
+
const commandProvider = activity.from?.aadObjectId ? "aad" : "teams-conversation";
|
|
1320
|
+
const commandExternalId = activity.from?.aadObjectId || convId;
|
|
1321
|
+
const commandResolver = new friends_1.FriendResolver(commandStore, {
|
|
1322
|
+
provider: commandProvider,
|
|
1323
|
+
externalId: commandExternalId,
|
|
1324
|
+
tenantId: activity.conversation?.tenantId,
|
|
1325
|
+
displayName: activity.from?.name || "Unknown",
|
|
1326
|
+
channel: "teams",
|
|
1327
|
+
});
|
|
1328
|
+
const commandContext = await commandResolver.resolve();
|
|
1329
|
+
(0, context_1.deleteSession)((0, config_2.sessionPath)(commandContext.friend.id, "teams", convId));
|
|
1330
|
+
await stream.emit("session cleared");
|
|
1331
|
+
if (_turnCoordinator.isTurnActive(turnKey)) {
|
|
1332
|
+
_turnCoordinator.enqueueFollowUp(turnKey, {
|
|
1333
|
+
conversationId: convId,
|
|
1334
|
+
text,
|
|
1335
|
+
receivedAt: Date.now(),
|
|
1336
|
+
effect: "clear_and_supersede",
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1244
1341
|
}
|
|
1245
|
-
|
|
1342
|
+
}
|
|
1343
|
+
work = coordinateTeamsTurn(convId, text, {
|
|
1344
|
+
aadObjectId: activity.from?.aadObjectId, userId, tenantId: activity.conversation?.tenantId,
|
|
1345
|
+
replyToId: activity.replyToId,
|
|
1346
|
+
}, async () => {
|
|
1246
1347
|
try {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
graphToken,
|
|
1256
|
-
adoToken,
|
|
1257
|
-
githubToken,
|
|
1258
|
-
signin: async (cn) => {
|
|
1259
|
-
try {
|
|
1260
|
-
const result = await signin({ connectionName: cn });
|
|
1261
|
-
(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 } });
|
|
1262
|
-
return result;
|
|
1263
|
-
}
|
|
1264
|
-
catch (e) {
|
|
1265
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1266
|
-
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.signin_error", component: "channels", message: `signin(${cn}) failed`, meta: { connectionName: cn, reason: msg.slice(0, 100) } });
|
|
1267
|
-
return undefined;
|
|
1268
|
-
}
|
|
1269
|
-
},
|
|
1270
|
-
aadObjectId: activity.from?.aadObjectId,
|
|
1271
|
-
tenantId: activity.conversation?.tenantId,
|
|
1272
|
-
displayName: activity.from?.name,
|
|
1273
|
-
graphConnectionName: tenantOAuth.graphConnectionName,
|
|
1274
|
-
adoConnectionName: tenantOAuth.adoConnectionName,
|
|
1275
|
-
githubConnectionName: tenantOAuth.githubConnectionName,
|
|
1276
|
-
/* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
|
|
1277
|
-
botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
|
|
1278
|
-
teamsSendImage: (0, teams_image_1.createTeamsImageSender)((message) => ctx.send(message), activity.id, aiLabelEntities()),
|
|
1279
|
-
};
|
|
1280
|
-
/* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
|
|
1281
|
-
const ctxSend = async (t) => {
|
|
1282
|
-
// Use send with replyToId (not reply, which adds a blockquote).
|
|
1283
|
-
// replyToId anchors the message after the user's message in Copilot Chat.
|
|
1348
|
+
// Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
|
|
1349
|
+
const tenantId = activity.conversation?.tenantId;
|
|
1350
|
+
const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
|
|
1351
|
+
// Fetch tokens for both OAuth connections independently.
|
|
1352
|
+
// Failures are silently caught -- the tool handler will request signin if needed.
|
|
1353
|
+
let graphToken;
|
|
1354
|
+
let adoToken;
|
|
1355
|
+
let githubToken;
|
|
1284
1356
|
try {
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
reportTeamsRequest(messageId ? "replied" : "reply_emitted", convId, teamsContext, { delivery: messageId ? "accepted" : "unconfirmed", ...(messageId ? { outboundActivityId: messageId } : {}) });
|
|
1357
|
+
const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
|
|
1358
|
+
graphToken = graphRes?.token;
|
|
1288
1359
|
}
|
|
1289
|
-
catch
|
|
1290
|
-
|
|
1291
|
-
|
|
1360
|
+
catch { /* no token yet — tool handler will trigger signin */ }
|
|
1361
|
+
try {
|
|
1362
|
+
const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
|
|
1363
|
+
adoToken = adoRes?.token;
|
|
1292
1364
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1365
|
+
catch { /* no token yet — tool handler will trigger signin */ }
|
|
1366
|
+
try {
|
|
1367
|
+
const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
|
|
1368
|
+
githubToken = githubRes?.token;
|
|
1369
|
+
}
|
|
1370
|
+
catch { /* no token yet — tool handler will trigger signin */ }
|
|
1371
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
|
|
1372
|
+
const teamsContext = {
|
|
1373
|
+
inboundActivityId: activity.id,
|
|
1374
|
+
replyToId: activity.replyToId,
|
|
1375
|
+
graphToken,
|
|
1376
|
+
adoToken,
|
|
1377
|
+
githubToken,
|
|
1378
|
+
signin: async (cn) => {
|
|
1379
|
+
try {
|
|
1380
|
+
const result = await signin({ connectionName: cn });
|
|
1381
|
+
(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 } });
|
|
1382
|
+
return result;
|
|
1383
|
+
}
|
|
1384
|
+
catch (e) {
|
|
1385
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1386
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.signin_error", component: "channels", message: `signin(${cn}) failed`, meta: { connectionName: cn, reason: msg.slice(0, 100) } });
|
|
1387
|
+
return undefined;
|
|
1388
|
+
}
|
|
1389
|
+
},
|
|
1390
|
+
aadObjectId: activity.from?.aadObjectId,
|
|
1391
|
+
tenantId: activity.conversation?.tenantId,
|
|
1392
|
+
displayName: activity.from?.name,
|
|
1393
|
+
graphConnectionName: tenantOAuth.graphConnectionName,
|
|
1394
|
+
adoConnectionName: tenantOAuth.adoConnectionName,
|
|
1395
|
+
githubConnectionName: tenantOAuth.githubConnectionName,
|
|
1396
|
+
/* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
|
|
1397
|
+
botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
|
|
1398
|
+
teamsSendImage: (0, teams_image_1.createTeamsImageSender)((message) => ctx.send(message), activity.id, aiLabelEntities()),
|
|
1399
|
+
};
|
|
1400
|
+
await handleTeamsMessage(text, stream, convId, teamsContext, sendText);
|
|
1401
|
+
}
|
|
1402
|
+
catch (err) {
|
|
1403
|
+
reportTeamsRequest("failed", convId, requestContext, { reason: "handler_failed" });
|
|
1404
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1405
|
+
(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 } });
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1301
1408
|
});
|
|
1409
|
+
await work;
|
|
1302
1410
|
});
|
|
1303
1411
|
app.event("error", ({ error }) => {
|
|
1304
1412
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1305
|
-
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
|
|
1413
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: { botId: app.id, label } });
|
|
1306
1414
|
});
|
|
1307
1415
|
}
|
|
1308
1416
|
function findAadObjectId(friend) {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.935",
|
|
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.
|
|
9
|
+
"version": "0.1.0-alpha.935",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|