@ouro.bot/cli 0.1.0-alpha.800 → 0.1.0-alpha.802
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 +12 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/session-events.js +7 -5
- package/dist/mind/context.js +1 -0
- package/dist/senses/shared-turn.js +139 -68
- package/dist/senses/telegram.js +26 -11
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
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.802",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Prefer validated completion text for settled replies and bind delivery causality only when the newest validated current-turn session event matches, preventing stale or duplicate Telegram answers."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.801",
|
|
12
|
+
"changes": [
|
|
13
|
+
"Keep shared-turn empty-response diagnostics internal on Telegram so tool-only investigation steps do not appear as user-visible Butler messages."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.800",
|
|
6
18
|
"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.802</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -52,6 +52,7 @@ exports.stampIngressTime = stampIngressTime;
|
|
|
52
52
|
exports.getIngressTime = getIngressTime;
|
|
53
53
|
exports.stampIngressRelations = stampIngressRelations;
|
|
54
54
|
exports.getIngressRelations = getIngressRelations;
|
|
55
|
+
exports.projectedSessionEventIds = projectedSessionEventIds;
|
|
55
56
|
exports.projectProviderMessages = projectProviderMessages;
|
|
56
57
|
exports.annotateMessageTimestamps = annotateMessageTimestamps;
|
|
57
58
|
exports.bestEventTimestamp = bestEventTimestamp;
|
|
@@ -936,10 +937,13 @@ function buildEventFromMessage(message, sequence, recordedAt, captureKind, sourc
|
|
|
936
937
|
},
|
|
937
938
|
};
|
|
938
939
|
}
|
|
939
|
-
function
|
|
940
|
-
|
|
940
|
+
function projectedSessionEventIds(envelope) {
|
|
941
|
+
return envelope.projection.eventIds.length > 0
|
|
941
942
|
? envelope.projection.eventIds
|
|
942
943
|
: envelope.events.map((event) => event.id);
|
|
944
|
+
}
|
|
945
|
+
function projectProviderMessages(envelope) {
|
|
946
|
+
const eventIds = projectedSessionEventIds(envelope);
|
|
943
947
|
const byId = new Map(envelope.events.map((event) => [event.id, event]));
|
|
944
948
|
return eventIds
|
|
945
949
|
.map((id) => byId.get(id))
|
|
@@ -958,9 +962,7 @@ function projectProviderMessages(envelope) {
|
|
|
958
962
|
* System and tool messages are untouched.
|
|
959
963
|
*/
|
|
960
964
|
function annotateMessageTimestamps(envelope, messages, nowMs = Date.now()) {
|
|
961
|
-
const eventIds = envelope
|
|
962
|
-
? envelope.projection.eventIds
|
|
963
|
-
: envelope.events.map((event) => event.id);
|
|
965
|
+
const eventIds = projectedSessionEventIds(envelope);
|
|
964
966
|
const byId = new Map(envelope.events.map((event) => [event.id, event]));
|
|
965
967
|
const events = eventIds
|
|
966
968
|
.map((id) => byId.get(id))
|
package/dist/mind/context.js
CHANGED
|
@@ -349,6 +349,7 @@ function loadSession(filePath) {
|
|
|
349
349
|
return {
|
|
350
350
|
messages: (0, session_events_1.sanitizeProviderMessages)((0, session_events_1.projectProviderMessages)(envelope)),
|
|
351
351
|
events: envelope.events,
|
|
352
|
+
projectionEventIds: [...(0, session_events_1.projectedSessionEventIds)(envelope)],
|
|
352
353
|
structuredOutputs: envelope.structuredOutputs ?? [],
|
|
353
354
|
lastUsage: envelope.lastUsage ?? undefined,
|
|
354
355
|
state: denormalizeContinuityState(envelope.state),
|
|
@@ -177,70 +177,134 @@ function extractOutwardSenseDeliveryText(messages) {
|
|
|
177
177
|
: assistantContentText(assistant.content);
|
|
178
178
|
}
|
|
179
179
|
function hasAcceptedOutwardSessionAck(events, assistantIndex, toolCallId, toolName) {
|
|
180
|
+
if (typeof toolCallId !== "string" || !toolCallId.trim())
|
|
181
|
+
return false;
|
|
180
182
|
const expectedAck = OUTWARD_DELIVERY_TOOL_ACKS.get(toolName);
|
|
181
183
|
for (let index = assistantIndex + 1; index < events.length; index++) {
|
|
182
184
|
const candidate = events[index];
|
|
183
185
|
if (candidate.role !== "tool")
|
|
184
186
|
return false;
|
|
185
|
-
if (candidate.toolCallId
|
|
186
|
-
|
|
187
|
+
if (candidate.toolCallId !== toolCallId && candidate.relations?.toolCallId !== toolCallId)
|
|
188
|
+
continue;
|
|
189
|
+
return candidate.toolCallId === toolCallId
|
|
190
|
+
&& candidate.relations?.toolCallId === toolCallId
|
|
191
|
+
&& candidate.provenance?.captureKind === "live"
|
|
192
|
+
&& Array.isArray(candidate.toolCalls)
|
|
193
|
+
&& candidate.toolCalls.length === 0
|
|
194
|
+
&& typeof candidate.content === "string"
|
|
195
|
+
&& candidate.content === expectedAck;
|
|
187
196
|
}
|
|
188
197
|
return false;
|
|
189
198
|
}
|
|
190
|
-
function newOutwardCoordinates(events, existingEventIds,
|
|
191
|
-
const boundaryIndex = events.findIndex((event) => event.id === afterEventId);
|
|
192
|
-
if (boundaryIndex < 0)
|
|
193
|
-
return [];
|
|
199
|
+
function newOutwardCoordinates(events, existingEventIds, boundaryIndex) {
|
|
194
200
|
return events.flatMap((event, eventIndex) => {
|
|
195
|
-
if (eventIndex <= boundaryIndex || existingEventIds.has(event.id) || event.role !== "assistant" || event.provenance?.captureKind
|
|
201
|
+
if (eventIndex <= boundaryIndex || existingEventIds.has(event.id) || event.role !== "assistant" || event.provenance?.captureKind !== "live" || !Array.isArray(event.toolCalls))
|
|
196
202
|
return [];
|
|
197
203
|
const outwardTools = event.toolCalls.flatMap((call) => {
|
|
198
|
-
|
|
204
|
+
const toolName = call?.function?.name;
|
|
205
|
+
if (toolName !== "speak" && toolName !== "settle")
|
|
199
206
|
return [];
|
|
200
|
-
if (!hasAcceptedOutwardSessionAck(events, eventIndex, call.id,
|
|
207
|
+
if (!hasAcceptedOutwardSessionAck(events, eventIndex, call.id, toolName))
|
|
201
208
|
return [];
|
|
202
|
-
const text = stripThinkBlocks(parseToolStringArg(call,
|
|
203
|
-
return text ? [{ kind:
|
|
209
|
+
const text = stripThinkBlocks(parseToolStringArg(call, toolName, toolName === "speak" ? "message" : "answer") ?? "");
|
|
210
|
+
return text ? [{ kind: toolName, eventId: event.id, eventIndex, text }] : [];
|
|
204
211
|
});
|
|
205
212
|
if (outwardTools.length > 0)
|
|
206
213
|
return outwardTools;
|
|
207
214
|
if (event.toolCalls.length > 0)
|
|
208
215
|
return [];
|
|
209
216
|
const text = stripThinkBlocks(typeof event.content === "string" ? event.content : "");
|
|
210
|
-
return text ? [{ kind: "text", eventId: event.id, text }] : [];
|
|
217
|
+
return text ? [{ kind: "text", eventId: event.id, eventIndex, text }] : [];
|
|
211
218
|
});
|
|
212
219
|
}
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
&& (!("tool_calls" in candidate) || !Array.isArray(candidate.tool_calls) || candidate.tool_calls.length === 0)
|
|
216
|
-
&& typeof candidate.content === "string"
|
|
217
|
-
&& candidate.content.trim().length > 0);
|
|
218
|
-
return message ? assistantContentText(message.content) : null;
|
|
219
|
-
}
|
|
220
|
-
function causalSessionEventIds(events, existingEventIds, attempts, afterEventId) {
|
|
221
|
-
if (!afterEventId)
|
|
220
|
+
function alignedDeliveryCoordinates(view, attempts, finalAttemptIndex, finalCoordinate) {
|
|
221
|
+
if (!view)
|
|
222
222
|
return attempts.flatMap((attempt) => attempt.delivered ? [null] : []);
|
|
223
|
-
const
|
|
223
|
+
const terminalCoordinate = finalCoordinate ?? (finalAttemptIndex === undefined ? view.terminal : undefined);
|
|
224
|
+
const reservedAttemptIndex = terminalCoordinate
|
|
225
|
+
? finalAttemptIndex !== undefined
|
|
226
|
+
? (attempts[finalAttemptIndex]?.delivered && attempts[finalAttemptIndex]?.kind === terminalCoordinate.kind && attempts[finalAttemptIndex]?.text === terminalCoordinate.text ? finalAttemptIndex : -1)
|
|
227
|
+
: attempts.findLastIndex((attempt) => attempt.delivered && attempt.kind === terminalCoordinate.kind && attempt.text === terminalCoordinate.text)
|
|
228
|
+
: -1;
|
|
229
|
+
const availableCoordinates = terminalCoordinate ? view.coordinates.filter((coordinate) => coordinate !== terminalCoordinate) : view.coordinates;
|
|
224
230
|
let nextCoordinate = 0;
|
|
225
|
-
return attempts.flatMap((attempt) => {
|
|
231
|
+
return attempts.flatMap((attempt, attemptIndex) => {
|
|
226
232
|
if (!attempt.delivered)
|
|
227
233
|
return [];
|
|
228
|
-
|
|
234
|
+
if (attemptIndex === finalAttemptIndex)
|
|
235
|
+
return finalCoordinate && finalCoordinate.kind === attempt.kind && finalCoordinate.text === attempt.text ? [finalCoordinate] : [null];
|
|
236
|
+
if (attemptIndex === reservedAttemptIndex && terminalCoordinate)
|
|
237
|
+
return [terminalCoordinate];
|
|
238
|
+
const coordinateIndex = availableCoordinates.findIndex((coordinate, index) => index >= nextCoordinate && coordinate.kind === attempt.kind && coordinate.text === attempt.text);
|
|
229
239
|
if (coordinateIndex < 0)
|
|
230
240
|
return [null];
|
|
231
241
|
nextCoordinate = coordinateIndex + 1;
|
|
232
|
-
return [
|
|
242
|
+
return [availableCoordinates[coordinateIndex]];
|
|
233
243
|
});
|
|
234
244
|
}
|
|
245
|
+
function causalSessionEventIds(view, attempts, finalAttemptIndex, finalCoordinate) {
|
|
246
|
+
return alignedDeliveryCoordinates(view, attempts, finalAttemptIndex, finalCoordinate).map((coordinate) => coordinate?.eventId ?? null);
|
|
247
|
+
}
|
|
235
248
|
function currentIngressEventId(events, existingEventIds, userMessage, precommittedIngress, ingressRelations) {
|
|
236
|
-
if (precommittedIngress)
|
|
237
|
-
return precommittedIngress.eventId;
|
|
238
249
|
const reference = ingressRelations?.references[0];
|
|
239
|
-
|
|
240
|
-
|
|
250
|
+
const carriesReference = (event, expected) => Array.isArray(event.relations?.references) && event.relations.references.includes(expected);
|
|
251
|
+
const matches = events.filter((event) => (event.role === "user"
|
|
241
252
|
&& event.content === userMessage
|
|
242
|
-
&& event.provenance?.captureKind
|
|
243
|
-
&& (
|
|
253
|
+
&& event.provenance?.captureKind === "live"
|
|
254
|
+
&& (precommittedIngress
|
|
255
|
+
? ((event.id === precommittedIngress.eventId && carriesReference(event, precommittedIngress.reference))
|
|
256
|
+
|| (!existingEventIds.has(event.id) && carriesReference(event, precommittedIngress.reference)))
|
|
257
|
+
: !existingEventIds.has(event.id) && (!reference || carriesReference(event, reference)))));
|
|
258
|
+
return matches.length === 1 ? matches[0].id : undefined;
|
|
259
|
+
}
|
|
260
|
+
function rawSessionEvents(value) {
|
|
261
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
262
|
+
return [];
|
|
263
|
+
const record = value;
|
|
264
|
+
return record.version === 2 && Array.isArray(record.events) ? record.events : [];
|
|
265
|
+
}
|
|
266
|
+
function exactProjectedIngressMessage(existing, messages, eventId) {
|
|
267
|
+
const eventsById = new Map(existing.events.map((event) => [event.id, event]));
|
|
268
|
+
if (eventsById.size !== existing.events.length)
|
|
269
|
+
return null;
|
|
270
|
+
const seenProjectionIds = new Set();
|
|
271
|
+
const projectedEvents = [];
|
|
272
|
+
for (const projectedId of existing.projectionEventIds) {
|
|
273
|
+
if (typeof projectedId !== "string" || !projectedId.trim() || seenProjectionIds.has(projectedId))
|
|
274
|
+
return null;
|
|
275
|
+
const event = eventsById.get(projectedId);
|
|
276
|
+
if (!event)
|
|
277
|
+
return null;
|
|
278
|
+
seenProjectionIds.add(projectedId);
|
|
279
|
+
projectedEvents.push(event);
|
|
280
|
+
}
|
|
281
|
+
if (existing.projectionEventIds.filter((projectedId) => projectedId === eventId).length !== 1)
|
|
282
|
+
return null;
|
|
283
|
+
const projectedUsers = projectedEvents.filter((event) => event.role === "user");
|
|
284
|
+
const providerUsers = messages.filter((message) => message.role === "user");
|
|
285
|
+
if (projectedUsers.at(-1)?.id !== eventId || projectedUsers.length !== providerUsers.length)
|
|
286
|
+
return null;
|
|
287
|
+
return providerUsers.at(-1);
|
|
288
|
+
}
|
|
289
|
+
function currentTurnEventView(events, existingEventIds, userMessage, precommittedIngress, ingressRelations) {
|
|
290
|
+
const eventIds = new Set();
|
|
291
|
+
let previousSequence = 0;
|
|
292
|
+
for (const event of events) {
|
|
293
|
+
if (!event || typeof event !== "object" || typeof event.id !== "string" || !event.id.trim() || eventIds.has(event.id) || !Number.isSafeInteger(event.sequence) || event.sequence <= previousSequence)
|
|
294
|
+
return null;
|
|
295
|
+
eventIds.add(event.id);
|
|
296
|
+
previousSequence = event.sequence;
|
|
297
|
+
}
|
|
298
|
+
const ingressEventId = currentIngressEventId(events, existingEventIds, userMessage, precommittedIngress, ingressRelations);
|
|
299
|
+
if (!ingressEventId)
|
|
300
|
+
return null;
|
|
301
|
+
const ingressIndex = events.findIndex((event) => event.id === ingressEventId);
|
|
302
|
+
if (events.some((event, eventIndex) => eventIndex > ingressIndex && existingEventIds.has(event.id)))
|
|
303
|
+
return null;
|
|
304
|
+
const coordinates = newOutwardCoordinates(events, existingEventIds, ingressIndex);
|
|
305
|
+
const newestAssistantIndex = events.findLastIndex((event, eventIndex) => eventIndex > ingressIndex && event.role === "assistant");
|
|
306
|
+
const terminal = coordinates.findLast((coordinate) => coordinate.eventIndex === newestAssistantIndex);
|
|
307
|
+
return { coordinates, ...(terminal ? { terminal } : {}) };
|
|
244
308
|
}
|
|
245
309
|
function getSenseSessionPath(agentName, friendId, channel, sessionKey, agentRootOverride) {
|
|
246
310
|
return path.join(agentRootOverride ?? (0, identity_1.getAgentRoot)(agentName), "state", "sessions", friendId, channel, `${(0, config_1.sanitizeKey)(sessionKey)}.json`);
|
|
@@ -309,11 +373,13 @@ async function runSenseTurn(options) {
|
|
|
309
373
|
return runWithLease(sessPath, async (sessionTurnLease) => {
|
|
310
374
|
const baseSessionRevision = (0, session_transaction_1.readSessionTransaction)(sessPath, sessionTurnLease).revision;
|
|
311
375
|
const existing = (0, context_1.loadSession)(sessPath);
|
|
376
|
+
const precommittedIngressEvent = options.precommittedIngress
|
|
377
|
+
? existing?.events?.find((candidate) => candidate.id === options.precommittedIngress.eventId)
|
|
378
|
+
: undefined;
|
|
312
379
|
if (options.precommittedIngress) {
|
|
313
|
-
const event = existing?.events?.find((candidate) => candidate.id === options.precommittedIngress.eventId);
|
|
314
380
|
const latestUserEvent = existing?.events?.filter((candidate) => candidate.role === "user").at(-1);
|
|
315
|
-
if (!
|
|
316
|
-
|| !
|
|
381
|
+
if (!precommittedIngressEvent || precommittedIngressEvent !== latestUserEvent || precommittedIngressEvent.role !== "user" || precommittedIngressEvent.content !== userMessage
|
|
382
|
+
|| !precommittedIngressEvent.relations.references.includes(options.precommittedIngress.reference)) {
|
|
317
383
|
throw new Error("shared turn precommitted ingress is missing, mismatched, or no longer current");
|
|
318
384
|
}
|
|
319
385
|
}
|
|
@@ -323,7 +389,16 @@ async function runSenseTurn(options) {
|
|
|
323
389
|
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
324
390
|
? existing.messages
|
|
325
391
|
: [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)(channel, {}, undefined)) }];
|
|
326
|
-
|
|
392
|
+
if (precommittedIngressEvent) {
|
|
393
|
+
const projectedIngress = exactProjectedIngressMessage(existing, sessionMessages, precommittedIngressEvent.id);
|
|
394
|
+
if (!projectedIngress || projectedIngress.role !== "user" || projectedIngress.content !== userMessage)
|
|
395
|
+
throw new Error("shared turn precommitted ingress is absent from the provider projection");
|
|
396
|
+
(0, session_events_1.stampIngressRelations)(projectedIngress, {
|
|
397
|
+
replyToEventId: precommittedIngressEvent.relations.replyToEventId,
|
|
398
|
+
threadRootEventId: precommittedIngressEvent.relations.threadRootEventId,
|
|
399
|
+
references: precommittedIngressEvent.relations.references,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
327
402
|
// Pending dir
|
|
328
403
|
const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
|
|
329
404
|
// Accumulate outward text through the same callback boundary used by chat
|
|
@@ -352,9 +427,10 @@ async function runSenseTurn(options) {
|
|
|
352
427
|
const text = stripThinkBlocks(pendingResponseText);
|
|
353
428
|
pendingResponseText = "";
|
|
354
429
|
if (!text)
|
|
355
|
-
return;
|
|
430
|
+
return undefined;
|
|
356
431
|
const delivery = { kind, text };
|
|
357
432
|
const attempt = { kind, text, delivered: false };
|
|
433
|
+
const attemptIndex = deliveryAttempts.length;
|
|
358
434
|
deliveryAttempts.push(attempt);
|
|
359
435
|
try {
|
|
360
436
|
await options.deliverySink?.onDelivery(delivery);
|
|
@@ -376,6 +452,7 @@ async function runSenseTurn(options) {
|
|
|
376
452
|
throw error;
|
|
377
453
|
commitResponseText(text);
|
|
378
454
|
}
|
|
455
|
+
return attemptIndex;
|
|
379
456
|
};
|
|
380
457
|
/* v8 ignore start — callback stubs are exercised through the pipeline integration */
|
|
381
458
|
const callbacks = {
|
|
@@ -393,7 +470,7 @@ async function runSenseTurn(options) {
|
|
|
393
470
|
},
|
|
394
471
|
onError: () => { },
|
|
395
472
|
onClearText: () => { pendingResponseText = ""; },
|
|
396
|
-
flushNow: () => deliverPending("speak", { throwOnError: true }),
|
|
473
|
+
flushNow: async () => { await deliverPending("speak", { throwOnError: true }); },
|
|
397
474
|
};
|
|
398
475
|
/* v8 ignore stop */
|
|
399
476
|
// Run the pipeline
|
|
@@ -469,27 +546,28 @@ async function runSenseTurn(options) {
|
|
|
469
546
|
};
|
|
470
547
|
}
|
|
471
548
|
const persistedEvents = persistPromise ? await persistPromise : [];
|
|
472
|
-
const
|
|
473
|
-
|
|
549
|
+
const terminalEvents = persistedEvents.length > 0
|
|
550
|
+
? persistedEvents
|
|
551
|
+
: rawSessionEvents((0, session_transaction_1.readSessionTransaction)(sessPath, sessionTurnLease).value);
|
|
552
|
+
const eventView = currentTurnEventView(terminalEvents, existingEventIds, userMessage, options.precommittedIngress, options.ingressRelations);
|
|
553
|
+
let finalDeliveryKind = terminalDeliveryKind;
|
|
474
554
|
const acceptedTerminalOutcome = turnResult.turnOutcome === "settled" || turnResult.turnOutcome === "blocked";
|
|
475
555
|
const failoverText = turnResult.turnOutcome === "errored" ? turnResult.failoverMessage?.trim() : undefined;
|
|
476
556
|
const expectsOutwardResponse = acceptedTerminalOutcome || turnResult.turnOutcome === "command" || Boolean(failoverText);
|
|
477
|
-
|
|
478
|
-
let recoveredTerminalEventId;
|
|
557
|
+
let finalCausalCoordinate;
|
|
479
558
|
if (acceptedTerminalOutcome) {
|
|
480
|
-
const completionText = turnResult.completion?.answer
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
recoveredTerminalEventId = causalSessionEventIds(persistedEvents, existingEventIds, [{ kind: "text", text: stripThinkBlocks(plainTerminalText), delivered: true }], ingressEventId)[0] ?? undefined;
|
|
559
|
+
const completionText = stripThinkBlocks(turnResult.completion?.answer ?? "");
|
|
560
|
+
let authoritativeText = completionText;
|
|
561
|
+
if (!authoritativeText && eventView?.terminal) {
|
|
562
|
+
authoritativeText = eventView.terminal.text;
|
|
563
|
+
finalDeliveryKind = eventView.terminal.kind;
|
|
564
|
+
}
|
|
565
|
+
if (eventView?.terminal?.kind === finalDeliveryKind && eventView.terminal.text === authoritativeText)
|
|
566
|
+
finalCausalCoordinate = eventView.terminal;
|
|
567
|
+
const terminalAlreadyDelivered = finalCausalCoordinate
|
|
568
|
+
? alignedDeliveryCoordinates(eventView, deliveryAttempts).includes(finalCausalCoordinate)
|
|
569
|
+
: false;
|
|
570
|
+
pendingResponseText = authoritativeText && !terminalAlreadyDelivered ? authoritativeText : "";
|
|
493
571
|
}
|
|
494
572
|
else if (turnResult.turnOutcome === "command") {
|
|
495
573
|
// Slash-command text is emitted directly by the pipeline and has no assistant event.
|
|
@@ -500,28 +578,20 @@ async function runSenseTurn(options) {
|
|
|
500
578
|
else {
|
|
501
579
|
pendingResponseText = "";
|
|
502
580
|
}
|
|
503
|
-
await deliverPending(finalDeliveryKind, { throwOnError: false });
|
|
581
|
+
const finalDeliveryAttemptIndex = await deliverPending(finalDeliveryKind, { throwOnError: false });
|
|
504
582
|
const ponderDeferred = false;
|
|
505
583
|
// Build response
|
|
506
584
|
let finalResponse;
|
|
507
|
-
|
|
585
|
+
const finalDeliveryAttempt = finalDeliveryAttemptIndex === undefined ? undefined : deliveryAttempts[finalDeliveryAttemptIndex];
|
|
586
|
+
const responseDeliveryFailure = finalDeliveryAttempt && !finalDeliveryAttempt.delivered ? deliveryFailures.at(-1) : undefined;
|
|
587
|
+
const responseCausalSessionEventId = finalDeliveryAttempt && !finalDeliveryAttempt.delivered ? finalCausalCoordinate?.eventId : undefined;
|
|
508
588
|
if (committedResponseText.length === 0) {
|
|
509
589
|
if (!expectsOutwardResponse) {
|
|
510
590
|
finalResponse = "";
|
|
511
591
|
}
|
|
512
592
|
else {
|
|
513
|
-
// The terminal turn had no committed text — check its session transcript for the delivered answer.
|
|
514
|
-
const postTurnSession = (0, context_1.loadSession)(sessPath);
|
|
515
593
|
const emptyFallback = options.emptyResponseFallback?.();
|
|
516
|
-
|
|
517
|
-
const recovered = extractOutwardSenseDeliveryText(postTurnSession.messages.slice(preTurnMessageCount));
|
|
518
|
-
finalResponse = recovered ?? emptyFallback ?? (hadReasoningChunk ? "" : "(agent responded but response was empty)");
|
|
519
|
-
if (recovered)
|
|
520
|
-
responseCausalSessionEventId = causalSessionEventIds(persistedEvents, existingEventIds, [{ kind: finalDeliveryKind, text: stripThinkBlocks(recovered), delivered: true }], ingressEventId)[0] ?? undefined;
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
finalResponse = emptyFallback ?? (hadReasoningChunk ? "" : "(agent responded but response was empty)");
|
|
524
|
-
}
|
|
594
|
+
finalResponse = emptyFallback ?? (hadReasoningChunk ? "" : "(agent responded but response was empty)");
|
|
525
595
|
}
|
|
526
596
|
}
|
|
527
597
|
else {
|
|
@@ -561,10 +631,11 @@ async function runSenseTurn(options) {
|
|
|
561
631
|
ponderDeferred,
|
|
562
632
|
deliveries,
|
|
563
633
|
deliveryFailures,
|
|
634
|
+
...(responseDeliveryFailure ? { responseDeliveryFailure } : {}),
|
|
564
635
|
providerInvocationCount,
|
|
565
636
|
toolInvocationCount,
|
|
566
637
|
sessionPath: sessPath,
|
|
567
|
-
...(deliveries.length > 0 ? { causalSessionEventIds: causalSessionEventIds(
|
|
638
|
+
...(deliveries.length > 0 ? { causalSessionEventIds: causalSessionEventIds(eventView, deliveryAttempts, finalDeliveryAttemptIndex, finalCausalCoordinate) } : {}),
|
|
568
639
|
...(responseCausalSessionEventId ? { responseCausalSessionEventId } : {}),
|
|
569
640
|
};
|
|
570
641
|
});
|
package/dist/senses/telegram.js
CHANGED
|
@@ -90,6 +90,16 @@ const pending_1 = require("../mind/pending");
|
|
|
90
90
|
const relationship_authorization_1 = require("../repertoire/relationship-authorization");
|
|
91
91
|
const telegram_admission_1 = require("./telegram-admission");
|
|
92
92
|
const telegram_effect_adapter_1 = require("./telegram-effect-adapter");
|
|
93
|
+
const EMPTY_SHARED_TURN_DIAGNOSTIC = "(agent responded but response was empty)";
|
|
94
|
+
function hasUserVisibleTurnResponse(response) {
|
|
95
|
+
const text = response.trim();
|
|
96
|
+
return text.length > 0 && text !== EMPTY_SHARED_TURN_DIAGNOSTIC;
|
|
97
|
+
}
|
|
98
|
+
function responseFallbackAfterDeliveries(result, successfulDeliveryCount) {
|
|
99
|
+
const failedTerminal = result.responseDeliveryFailure?.text ?? (result.responseCausalSessionEventId ? result.deliveryFailures.at(-1)?.text : undefined);
|
|
100
|
+
const response = failedTerminal ?? (successfulDeliveryCount === 0 && result.deliveryFailures.length === 0 ? result.response : "");
|
|
101
|
+
return hasUserVisibleTurnResponse(response) ? response : null;
|
|
102
|
+
}
|
|
93
103
|
function createFullVisibilityProgress() {
|
|
94
104
|
const progress = {};
|
|
95
105
|
return { progress, emptyResponseFallback: () => progress.fallback?.() };
|
|
@@ -1181,10 +1191,15 @@ function createTelegramSenseApp(options) {
|
|
|
1181
1191
|
emptyResponseFallback: fullVisibility.emptyResponseFallback,
|
|
1182
1192
|
deliverySink: { onDelivery: (delivery) => deliver(delivery.text, delivery.kind === "settle") },
|
|
1183
1193
|
});
|
|
1184
|
-
|
|
1185
|
-
|
|
1194
|
+
let responseFallbackArtifactId;
|
|
1195
|
+
const responseFallback = responseFallbackAfterDeliveries(result, effects.length);
|
|
1196
|
+
if (responseFallback) {
|
|
1197
|
+
await deliver(responseFallback, true);
|
|
1198
|
+
responseFallbackArtifactId = effects.at(-1)?.id;
|
|
1199
|
+
}
|
|
1186
1200
|
const causalEventIds = Object.fromEntries(effects.flatMap((artifact, index) => {
|
|
1187
|
-
const eventId = result.causalSessionEventIds?.[index]
|
|
1201
|
+
const eventId = result.causalSessionEventIds?.[index]
|
|
1202
|
+
?? (artifact.id === responseFallbackArtifactId ? result.responseCausalSessionEventId : undefined);
|
|
1188
1203
|
return eventId ? [[artifact.id, eventId]] : [];
|
|
1189
1204
|
}));
|
|
1190
1205
|
await recordAcceptedEffects(sessionPath, effects, undefined, causalEventIds);
|
|
@@ -1317,7 +1332,6 @@ function createTelegramSenseApp(options) {
|
|
|
1317
1332
|
meta: lifecycleMeta("senses.telegram_turn_start", { agentName: options.agentName, subject, ...acceptanceMeta, ...lifecycleCoordinates }, lifecycleStartedAt),
|
|
1318
1333
|
});
|
|
1319
1334
|
acceptanceAuditBarrier();
|
|
1320
|
-
let deliveryCount = 0;
|
|
1321
1335
|
const deliveredMessageIds = [];
|
|
1322
1336
|
const deliveredChunks = [];
|
|
1323
1337
|
let receiptStatus = "success";
|
|
@@ -1381,7 +1395,6 @@ function createTelegramSenseApp(options) {
|
|
|
1381
1395
|
}
|
|
1382
1396
|
else {
|
|
1383
1397
|
turnEffects.push(await deliverButlerEffect(delivery.text, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }, exactDownloadCreditQuestion));
|
|
1384
|
-
deliveryCount += 1;
|
|
1385
1398
|
}
|
|
1386
1399
|
},
|
|
1387
1400
|
},
|
|
@@ -1413,12 +1426,14 @@ function createTelegramSenseApp(options) {
|
|
|
1413
1426
|
throw new Error("Canonical Sanctuary query did not produce exactly one matching grounded settle");
|
|
1414
1427
|
const canonical = (0, sanctuary_grounding_1.renderSanctuaryGroundedResponse)(grounding.toolName, grounding.facts);
|
|
1415
1428
|
turnEffects.push(await deliverButlerEffect(canonical, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }));
|
|
1416
|
-
deliveryCount = 1;
|
|
1417
1429
|
}
|
|
1418
|
-
else
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
|
|
1430
|
+
else {
|
|
1431
|
+
const responseFallback = responseFallbackAfterDeliveries(result, turnEffects.length);
|
|
1432
|
+
if (responseFallback) {
|
|
1433
|
+
const artifact = await deliverButlerEffect(responseFallback, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }, exactDownloadCreditQuestion);
|
|
1434
|
+
turnEffects.push(artifact);
|
|
1435
|
+
responseFallbackArtifactId = artifact.id;
|
|
1436
|
+
}
|
|
1422
1437
|
}
|
|
1423
1438
|
if (result.sessionPath) {
|
|
1424
1439
|
const causalEventIds = Object.fromEntries((groundingIntentTool ? [] : turnEffects).flatMap((artifact, index) => {
|
|
@@ -1435,7 +1450,7 @@ function createTelegramSenseApp(options) {
|
|
|
1435
1450
|
component: "senses",
|
|
1436
1451
|
event: "senses.telegram_turn_end",
|
|
1437
1452
|
message: "Telegram authorized turn completed",
|
|
1438
|
-
meta: lifecycleMeta("senses.telegram_turn_end", { agentName: options.agentName, subject, deliveryCount:
|
|
1453
|
+
meta: lifecycleMeta("senses.telegram_turn_end", { agentName: options.agentName, subject, deliveryCount: turnEffects.length, ...acceptanceMeta, ...lifecycleCoordinates, ...(acceptanceMarker ? { outcome: "success", errorDigest: null } : {}) }, Math.max(Date.now(), lifecycleStartedAt + 1)),
|
|
1439
1454
|
});
|
|
1440
1455
|
}
|
|
1441
1456
|
catch (error) {
|
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.802",
|
|
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.802",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|