@qwen-code/channel-base 0.21.1 → 0.21.2
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/README.md +3 -1
- package/dist/ChannelBase.d.ts +28 -4
- package/dist/ChannelBase.d.ts.map +1 -1
- package/dist/ChannelBase.js +333 -14
- package/dist/ChannelBase.js.map +1 -1
- package/dist/PairingStore.d.ts +3 -2
- package/dist/PairingStore.d.ts.map +1 -1
- package/dist/PairingStore.js +11 -2
- package/dist/PairingStore.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/ChannelBase.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename, join } from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { BlockStreamer } from './BlockStreamer.js';
|
|
3
4
|
import { ChannelProactiveDeliveryError, isChannelProactiveDeliveryError, } from './ChannelProactiveDeliveryError.js';
|
|
4
5
|
import { GroupGate } from './GroupGate.js';
|
|
@@ -62,6 +63,9 @@ const SENSITIVE_PAYLOAD_KEY_PATTERN = new RegExp([
|
|
|
62
63
|
'senderNick',
|
|
63
64
|
'senderName',
|
|
64
65
|
].join('|'), 'i');
|
|
66
|
+
function isRecord(value) {
|
|
67
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
68
|
+
}
|
|
65
69
|
/**
|
|
66
70
|
* Character class (sans the enclosing `[]`) for a slash-command token: alphanumerics
|
|
67
71
|
* plus `_`, `:` and `-`, so hyphenated and namespaced agent commands (e.g.
|
|
@@ -212,6 +216,7 @@ export class ChannelBase {
|
|
|
212
216
|
sessionId: event.sessionId,
|
|
213
217
|
target,
|
|
214
218
|
request: event.request,
|
|
219
|
+
settlementListeners: new Set(),
|
|
215
220
|
};
|
|
216
221
|
this.pendingPermissions.set(event.requestId, pending);
|
|
217
222
|
const chatKey = this.permissionChatKey(target);
|
|
@@ -219,6 +224,10 @@ export class ChannelBase {
|
|
|
219
224
|
requestIds.push(event.requestId);
|
|
220
225
|
this.pendingPermissionsByChat.set(chatKey, requestIds);
|
|
221
226
|
try {
|
|
227
|
+
const presentation = this.tryPresentUserInput(pending);
|
|
228
|
+
if (presentation && (await presentation)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
222
231
|
const text = this.formatPermissionRequest(pending);
|
|
223
232
|
if (target.threadId !== undefined &&
|
|
224
233
|
this.supportsProactiveSend() &&
|
|
@@ -230,7 +239,7 @@ export class ChannelBase {
|
|
|
230
239
|
}
|
|
231
240
|
}
|
|
232
241
|
catch (err) {
|
|
233
|
-
this.removePendingPermission(event.requestId);
|
|
242
|
+
this.removePendingPermission(event.requestId, 'cancelled');
|
|
234
243
|
try {
|
|
235
244
|
await this.bridge.respondToPermission?.(event.requestId, {
|
|
236
245
|
outcome: { outcome: 'cancelled' },
|
|
@@ -242,6 +251,154 @@ export class ChannelBase {
|
|
|
242
251
|
throw err;
|
|
243
252
|
}
|
|
244
253
|
}
|
|
254
|
+
tryPresentUserInput(pending) {
|
|
255
|
+
const active = this.activePrompts.get(pending.sessionId);
|
|
256
|
+
const questions = this.normalizeUserQuestions(pending);
|
|
257
|
+
const submitOptionId = this.approvalOptionId(pending);
|
|
258
|
+
if (!active ||
|
|
259
|
+
active.loopPrompt ||
|
|
260
|
+
!active.owner ||
|
|
261
|
+
!questions ||
|
|
262
|
+
!submitOptionId) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
const precedingSegment = this.closeOutputSegment(pending.sessionId, active, pending.target);
|
|
266
|
+
let respondInvoked = false;
|
|
267
|
+
const context = {
|
|
268
|
+
requestId: pending.requestId,
|
|
269
|
+
sessionId: pending.sessionId,
|
|
270
|
+
runId: active.runId,
|
|
271
|
+
owner: active.owner,
|
|
272
|
+
target: pending.target,
|
|
273
|
+
...(precedingSegment
|
|
274
|
+
? { precedingSegmentId: precedingSegment.segmentId }
|
|
275
|
+
: {}),
|
|
276
|
+
questions,
|
|
277
|
+
submitOptionId,
|
|
278
|
+
onSettled: (listener) => {
|
|
279
|
+
if (pending.settled) {
|
|
280
|
+
listener(pending.settled);
|
|
281
|
+
return () => { };
|
|
282
|
+
}
|
|
283
|
+
pending.settlementListeners.add(listener);
|
|
284
|
+
return () => {
|
|
285
|
+
pending.settlementListeners.delete(listener);
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
respond: (response) => {
|
|
289
|
+
respondInvoked = true;
|
|
290
|
+
return this.respondToUserInput(pending, response);
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
pending.userInputPresented = true;
|
|
294
|
+
return (async () => {
|
|
295
|
+
try {
|
|
296
|
+
if (precedingSegment) {
|
|
297
|
+
await this.notifyOutputSegmentEnd(pending.target.chatId, pending.sessionId, precedingSegment, 'input_requested');
|
|
298
|
+
}
|
|
299
|
+
const result = await this.presentUserInputRequest(context);
|
|
300
|
+
if (this.pendingPermissions.get(pending.requestId) !== pending) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
if (result.kind === 'presented' ||
|
|
304
|
+
(result.kind === 'handled' && respondInvoked)) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
pending.userInputPresented = false;
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
process.stderr.write(`[${this.name}] user input presentation failed for request ${sanitizeLogText(pending.requestId, 128)}: ${this.lifecycleError(err)}\n`);
|
|
312
|
+
if (this.pendingPermissions.get(pending.requestId) !== pending) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
pending.userInputPresented = false;
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
}
|
|
320
|
+
normalizeUserQuestions(pending) {
|
|
321
|
+
const toolCall = pending.request.toolCall;
|
|
322
|
+
const meta = isRecord(toolCall['_meta']) ? toolCall['_meta'] : undefined;
|
|
323
|
+
const canonical = meta?.['qwenInteractionKind'] === 'user_question';
|
|
324
|
+
const identifiedLegacy = meta?.['toolName'] === 'ask_user_question' ||
|
|
325
|
+
toolCall['kind'] === 'ask_user_question';
|
|
326
|
+
const rawInput = isRecord(toolCall['rawInput'])
|
|
327
|
+
? toolCall['rawInput']
|
|
328
|
+
: undefined;
|
|
329
|
+
const rawQuestions = canonical
|
|
330
|
+
? meta?.['qwenQuestions']
|
|
331
|
+
: identifiedLegacy
|
|
332
|
+
? rawInput?.['questions']
|
|
333
|
+
: undefined;
|
|
334
|
+
if (!Array.isArray(rawQuestions) ||
|
|
335
|
+
rawQuestions.length < 1 ||
|
|
336
|
+
rawQuestions.length > 4) {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
const questions = [];
|
|
340
|
+
for (const [index, rawQuestion] of rawQuestions.entries()) {
|
|
341
|
+
if (!isRecord(rawQuestion)) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
const header = rawQuestion['header'];
|
|
345
|
+
const question = rawQuestion['question'];
|
|
346
|
+
const rawOptions = rawQuestion['options'];
|
|
347
|
+
const multiSelect = rawQuestion['multiSelect'];
|
|
348
|
+
if (typeof header !== 'string' ||
|
|
349
|
+
header.trim().length === 0 ||
|
|
350
|
+
typeof question !== 'string' ||
|
|
351
|
+
question.trim().length === 0 ||
|
|
352
|
+
!Array.isArray(rawOptions) ||
|
|
353
|
+
rawOptions.length < 2 ||
|
|
354
|
+
rawOptions.length > 4 ||
|
|
355
|
+
(multiSelect !== undefined && typeof multiSelect !== 'boolean')) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
const options = [];
|
|
359
|
+
for (const rawOption of rawOptions) {
|
|
360
|
+
if (!isRecord(rawOption) ||
|
|
361
|
+
typeof rawOption['label'] !== 'string' ||
|
|
362
|
+
rawOption['label'].trim().length === 0 ||
|
|
363
|
+
typeof rawOption['description'] !== 'string') {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
options.push({
|
|
367
|
+
label: rawOption['label'],
|
|
368
|
+
description: rawOption['description'],
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
questions.push({
|
|
372
|
+
answerKey: String(index),
|
|
373
|
+
header,
|
|
374
|
+
question,
|
|
375
|
+
options,
|
|
376
|
+
multiSelect: multiSelect ?? false,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return questions;
|
|
380
|
+
}
|
|
381
|
+
async respondToUserInput(pending, response) {
|
|
382
|
+
if (pending.responsePromise) {
|
|
383
|
+
return pending.responsePromise;
|
|
384
|
+
}
|
|
385
|
+
if (this.pendingPermissions.get(pending.requestId) !== pending ||
|
|
386
|
+
!this.bridge.respondToPermission) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
pending.responsePromise = Promise.resolve()
|
|
390
|
+
.then(() => this.bridge.respondToPermission(pending.requestId, response))
|
|
391
|
+
.then((accepted) => {
|
|
392
|
+
this.removePendingPermission(pending.requestId, accepted
|
|
393
|
+
? this.userInputSettlementReason(pending, response.outcome)
|
|
394
|
+
: 'cancelled');
|
|
395
|
+
return accepted;
|
|
396
|
+
}, (error) => {
|
|
397
|
+
this.removePendingPermission(pending.requestId, 'cancelled');
|
|
398
|
+
throw error;
|
|
399
|
+
});
|
|
400
|
+
return pending.responsePromise;
|
|
401
|
+
}
|
|
245
402
|
permissionTargetForEvent(event) {
|
|
246
403
|
const routeTarget = this.router.getTarget(event.sessionId);
|
|
247
404
|
if (!routeTarget || routeTarget.channelName !== this.name) {
|
|
@@ -268,7 +425,11 @@ export class ChannelBase {
|
|
|
268
425
|
return target;
|
|
269
426
|
}
|
|
270
427
|
dispatchPermissionResolved(event) {
|
|
271
|
-
this.
|
|
428
|
+
const pending = this.pendingPermissions.get(event.requestId);
|
|
429
|
+
if (!pending) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
this.removePendingPermission(event.requestId, this.userInputSettlementReason(pending, event.outcome));
|
|
272
433
|
}
|
|
273
434
|
constructor(name, config, bridge, options) {
|
|
274
435
|
this.name = name;
|
|
@@ -322,6 +483,9 @@ export class ChannelBase {
|
|
|
322
483
|
* logged, nothing more.
|
|
323
484
|
*/
|
|
324
485
|
onTaskLifecycle(_event) { }
|
|
486
|
+
async presentUserInputRequest(_context) {
|
|
487
|
+
return { kind: 'unsupported' };
|
|
488
|
+
}
|
|
325
489
|
emitTaskLifecycle(event) {
|
|
326
490
|
try {
|
|
327
491
|
const result = this.onTaskLifecycle(event);
|
|
@@ -351,6 +515,8 @@ export class ChannelBase {
|
|
|
351
515
|
return;
|
|
352
516
|
}
|
|
353
517
|
active.cancellationEmitted = true;
|
|
518
|
+
const segment = this.closeOutputSegment(sessionId, active);
|
|
519
|
+
void this.notifyOutputSegmentEnd(active.chatId, sessionId, segment, 'cancelled');
|
|
354
520
|
this.emitTaskLifecycle({
|
|
355
521
|
...this.lifecycleBase(active.chatId, sessionId, active.messageId),
|
|
356
522
|
type: 'cancelled',
|
|
@@ -427,15 +593,72 @@ export class ChannelBase {
|
|
|
427
593
|
return Boolean(this.config.identity || this.config.memoryScope);
|
|
428
594
|
}
|
|
429
595
|
lifecycleBase(chatId, sessionId, messageId) {
|
|
596
|
+
const active = this.activePrompts.get(sessionId);
|
|
430
597
|
return {
|
|
431
598
|
channelName: this.name,
|
|
432
599
|
chatId,
|
|
433
600
|
sessionId,
|
|
434
601
|
...(messageId ? { messageId } : {}),
|
|
602
|
+
...(active?.runId ? { runId: active.runId } : {}),
|
|
603
|
+
...(active?.owner ? { owner: active.owner } : {}),
|
|
435
604
|
identity: this.identity,
|
|
436
605
|
memoryScope: this.memoryScope,
|
|
437
606
|
};
|
|
438
607
|
}
|
|
608
|
+
outputSegmentContext(sessionId, active, segmentId, target) {
|
|
609
|
+
const resolvedTarget = target ??
|
|
610
|
+
(active.senderId
|
|
611
|
+
? {
|
|
612
|
+
channelName: this.name,
|
|
613
|
+
chatId: active.chatId,
|
|
614
|
+
senderId: active.senderId,
|
|
615
|
+
...(active.threadId ? { threadId: active.threadId } : {}),
|
|
616
|
+
...(active.isGroup !== undefined
|
|
617
|
+
? { isGroup: active.isGroup }
|
|
618
|
+
: {}),
|
|
619
|
+
}
|
|
620
|
+
: undefined);
|
|
621
|
+
if (!active.owner ||
|
|
622
|
+
!resolvedTarget ||
|
|
623
|
+
resolvedTarget.channelName !== this.name) {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
channelName: this.name,
|
|
628
|
+
sessionId,
|
|
629
|
+
runId: active.runId,
|
|
630
|
+
segmentId,
|
|
631
|
+
owner: active.owner,
|
|
632
|
+
target: resolvedTarget,
|
|
633
|
+
...(active.messageId ? { messageId: active.messageId } : {}),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
ensureOutputSegment(sessionId, active) {
|
|
637
|
+
if (!active.owner)
|
|
638
|
+
return undefined;
|
|
639
|
+
const segmentId = active.activeSegmentId ?? randomUUID();
|
|
640
|
+
const context = this.outputSegmentContext(sessionId, active, segmentId);
|
|
641
|
+
if (context)
|
|
642
|
+
active.activeSegmentId = segmentId;
|
|
643
|
+
return context;
|
|
644
|
+
}
|
|
645
|
+
closeOutputSegment(sessionId, active, target) {
|
|
646
|
+
const segmentId = active.activeSegmentId;
|
|
647
|
+
if (!segmentId)
|
|
648
|
+
return undefined;
|
|
649
|
+
active.activeSegmentId = undefined;
|
|
650
|
+
return this.outputSegmentContext(sessionId, active, segmentId, target);
|
|
651
|
+
}
|
|
652
|
+
async notifyOutputSegmentEnd(chatId, sessionId, segment, reason) {
|
|
653
|
+
if (!segment)
|
|
654
|
+
return;
|
|
655
|
+
try {
|
|
656
|
+
await this.onOutputSegmentEnd(chatId, sessionId, segment, reason);
|
|
657
|
+
}
|
|
658
|
+
catch (err) {
|
|
659
|
+
process.stderr.write(`[${this.name}] output segment boundary failed for session ${sanitizeLogText(sessionId, 64)}: ${this.lifecycleError(err)}\n`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
439
662
|
supportsProactiveSend() {
|
|
440
663
|
return false;
|
|
441
664
|
}
|
|
@@ -751,6 +974,7 @@ export class ChannelBase {
|
|
|
751
974
|
doneResolve = resolve;
|
|
752
975
|
});
|
|
753
976
|
const promptState = {
|
|
977
|
+
runId: randomUUID(),
|
|
754
978
|
cancelled: false,
|
|
755
979
|
done,
|
|
756
980
|
resolve: doneResolve,
|
|
@@ -989,6 +1213,7 @@ export class ChannelBase {
|
|
|
989
1213
|
doneResolve = resolve;
|
|
990
1214
|
});
|
|
991
1215
|
const promptState = {
|
|
1216
|
+
runId: randomUUID(),
|
|
992
1217
|
cancelled: false,
|
|
993
1218
|
done,
|
|
994
1219
|
resolve: doneResolve,
|
|
@@ -1212,10 +1437,18 @@ export class ChannelBase {
|
|
|
1212
1437
|
active.cancelled = true;
|
|
1213
1438
|
this.stopActiveStreaming(active, sessionId, reason);
|
|
1214
1439
|
this.dropCollectBuffer(sessionId);
|
|
1440
|
+
this.removePendingPermissionsForSession(sessionId, 'run_cancelled');
|
|
1215
1441
|
this.emitTaskCancellation(active, sessionId, reason);
|
|
1216
1442
|
return true;
|
|
1217
1443
|
});
|
|
1218
1444
|
}
|
|
1445
|
+
requestPromptRunCancellation(sessionId, runId, reason = 'cancel_command') {
|
|
1446
|
+
const active = this.activePrompts.get(sessionId);
|
|
1447
|
+
if (!active || active.runId !== runId) {
|
|
1448
|
+
return Promise.resolve(false);
|
|
1449
|
+
}
|
|
1450
|
+
return this.requestActivePromptCancellation(sessionId, reason);
|
|
1451
|
+
}
|
|
1219
1452
|
dropCollectBuffer(sessionId) {
|
|
1220
1453
|
const buffer = this.collectBuffers.get(sessionId);
|
|
1221
1454
|
if (!buffer)
|
|
@@ -1310,7 +1543,12 @@ export class ChannelBase {
|
|
|
1310
1543
|
* Override to implement progressive display (e.g., updating an AI card in-place).
|
|
1311
1544
|
* Default: no-op (chunks are collected internally and delivered via onResponseComplete).
|
|
1312
1545
|
*/
|
|
1313
|
-
onResponseChunk(_chatId, _chunk, _sessionId) { }
|
|
1546
|
+
onResponseChunk(_chatId, _chunk, _sessionId, _segment) { }
|
|
1547
|
+
onOutputSegmentEnd(chatId, sessionId, _segment, reason) {
|
|
1548
|
+
if (reason === 'response_boundary') {
|
|
1549
|
+
return this.onResponseBoundary(chatId, sessionId);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1314
1552
|
/**
|
|
1315
1553
|
* Called when the agent starts a new response segment for the same prompt.
|
|
1316
1554
|
* Override to clear adapter-owned streaming buffers.
|
|
@@ -1322,12 +1560,33 @@ export class ChannelBase {
|
|
|
1322
1560
|
const threadId = active?.threadId ?? target?.threadId;
|
|
1323
1561
|
await this.sendThreadMessage(chatId, threadId, text);
|
|
1324
1562
|
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Adapter hook for delivery-only metadata. The response delivery path can read
|
|
1565
|
+
* the active prompt while it exists; adapters must not retain its raw content.
|
|
1566
|
+
*/
|
|
1567
|
+
getResponseMessageId(sessionId) {
|
|
1568
|
+
return this.activePrompts.get(sessionId)?.messageId;
|
|
1569
|
+
}
|
|
1570
|
+
getResponseSenderId(sessionId) {
|
|
1571
|
+
return this.activePrompts.get(sessionId)?.senderId;
|
|
1572
|
+
}
|
|
1573
|
+
getResponseMetadata(sessionId) {
|
|
1574
|
+
return this.activePrompts.get(sessionId)?.metadata;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Returns the active prompt's response thread while it remains available for
|
|
1578
|
+
* adapter delivery. Falls back to the session target after prompt cleanup.
|
|
1579
|
+
*/
|
|
1580
|
+
getResponseThreadId(sessionId) {
|
|
1581
|
+
return (this.activePrompts.get(sessionId)?.threadId ??
|
|
1582
|
+
this.router.getTarget(sessionId)?.threadId);
|
|
1583
|
+
}
|
|
1325
1584
|
/**
|
|
1326
1585
|
* Called when the agent's full response is ready.
|
|
1327
1586
|
* Override to customize delivery (e.g., finalize an AI card).
|
|
1328
1587
|
* Default: sends the full response text.
|
|
1329
1588
|
*/
|
|
1330
|
-
async onResponseComplete(chatId, fullText, sessionId) {
|
|
1589
|
+
async onResponseComplete(chatId, fullText, sessionId, _segment) {
|
|
1331
1590
|
await this.sendResponseMessage(chatId, fullText, sessionId);
|
|
1332
1591
|
}
|
|
1333
1592
|
/**
|
|
@@ -1384,12 +1643,13 @@ export class ChannelBase {
|
|
|
1384
1643
|
}
|
|
1385
1644
|
return live;
|
|
1386
1645
|
}
|
|
1387
|
-
removePendingPermission(requestId) {
|
|
1646
|
+
removePendingPermission(requestId, reason = 'resolved_outside_presenter') {
|
|
1388
1647
|
const pending = this.pendingPermissions.get(requestId);
|
|
1389
1648
|
if (!pending) {
|
|
1390
1649
|
return;
|
|
1391
1650
|
}
|
|
1392
1651
|
this.pendingPermissions.delete(requestId);
|
|
1652
|
+
this.settleUserInput(pending, reason);
|
|
1393
1653
|
const chatKey = this.permissionChatKey(pending.target);
|
|
1394
1654
|
const requestIds = this.pendingPermissionsByChat.get(chatKey);
|
|
1395
1655
|
if (!requestIds) {
|
|
@@ -1403,17 +1663,48 @@ export class ChannelBase {
|
|
|
1403
1663
|
this.pendingPermissionsByChat.set(chatKey, remaining);
|
|
1404
1664
|
}
|
|
1405
1665
|
}
|
|
1406
|
-
removePendingPermissionsForSession(sessionId) {
|
|
1666
|
+
removePendingPermissionsForSession(sessionId, reason = 'cancelled') {
|
|
1407
1667
|
const requestIds = Array.from(this.pendingPermissions)
|
|
1408
1668
|
.filter(([, pending]) => pending.sessionId === sessionId)
|
|
1409
1669
|
.map(([requestId]) => requestId);
|
|
1410
1670
|
for (const requestId of requestIds) {
|
|
1411
|
-
this.removePendingPermission(requestId);
|
|
1671
|
+
this.removePendingPermission(requestId, reason);
|
|
1412
1672
|
}
|
|
1413
1673
|
}
|
|
1414
1674
|
clearPendingPermissions() {
|
|
1415
|
-
this.pendingPermissions.
|
|
1416
|
-
|
|
1675
|
+
for (const requestId of Array.from(this.pendingPermissions.keys())) {
|
|
1676
|
+
this.removePendingPermission(requestId, 'cancelled');
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
settleUserInput(pending, reason) {
|
|
1680
|
+
if (pending.settled) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
pending.settled = reason;
|
|
1684
|
+
const listeners = Array.from(pending.settlementListeners);
|
|
1685
|
+
pending.settlementListeners.clear();
|
|
1686
|
+
for (const listener of listeners) {
|
|
1687
|
+
try {
|
|
1688
|
+
listener(reason);
|
|
1689
|
+
}
|
|
1690
|
+
catch (err) {
|
|
1691
|
+
process.stderr.write(`[${this.name}] user input settlement listener failed for request ${sanitizeLogText(pending.requestId, 128)}: ${this.lifecycleError(err)}\n`);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
userInputSettlementReason(pending, outcome) {
|
|
1696
|
+
if (outcome?.outcome === 'cancelled') {
|
|
1697
|
+
return 'cancelled';
|
|
1698
|
+
}
|
|
1699
|
+
if (outcome?.outcome === 'selected') {
|
|
1700
|
+
const selected = pending.request.options.find((option) => option.optionId === outcome.optionId);
|
|
1701
|
+
if (selected?.kind === 'reject_once' ||
|
|
1702
|
+
(selected?.optionId === 'cancel' &&
|
|
1703
|
+
selected.kind === undefined)) {
|
|
1704
|
+
return 'cancelled';
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
return 'resolved_outside_presenter';
|
|
1417
1708
|
}
|
|
1418
1709
|
pendingPermissionForEnvelope(envelope, args) {
|
|
1419
1710
|
const trimmed = args.trim();
|
|
@@ -1447,6 +1738,8 @@ export class ChannelBase {
|
|
|
1447
1738
|
canEnvelopeAnswerPendingPermission(envelope, pending) {
|
|
1448
1739
|
return (pending.target.chatId === envelope.chatId &&
|
|
1449
1740
|
pending.target.threadId === envelope.threadId &&
|
|
1741
|
+
(!pending.userInputPresented ||
|
|
1742
|
+
pending.target.senderId === envelope.senderId) &&
|
|
1450
1743
|
(this.isSharedSessionTarget(pending.target) ||
|
|
1451
1744
|
pending.target.senderId === envelope.senderId));
|
|
1452
1745
|
}
|
|
@@ -1541,6 +1834,10 @@ export class ChannelBase {
|
|
|
1541
1834
|
return true;
|
|
1542
1835
|
}
|
|
1543
1836
|
const { pending } = lookup;
|
|
1837
|
+
if (pending.userInputPresented && decision !== 'deny') {
|
|
1838
|
+
await this.sendMessage(envelope.chatId, 'Submit this question through its interactive card, or use /deny [request-id] to cancel it.');
|
|
1839
|
+
return true;
|
|
1840
|
+
}
|
|
1544
1841
|
const response = (() => {
|
|
1545
1842
|
if (decision === 'deny') {
|
|
1546
1843
|
return this.denialResponse(pending);
|
|
@@ -1560,7 +1857,9 @@ export class ChannelBase {
|
|
|
1560
1857
|
}
|
|
1561
1858
|
let accepted;
|
|
1562
1859
|
try {
|
|
1563
|
-
accepted =
|
|
1860
|
+
accepted = pending.userInputPresented
|
|
1861
|
+
? await this.respondToUserInput(pending, response)
|
|
1862
|
+
: await this.bridge.respondToPermission(pending.requestId, response);
|
|
1564
1863
|
}
|
|
1565
1864
|
catch (err) {
|
|
1566
1865
|
this.removePendingPermission(pending.requestId);
|
|
@@ -1597,7 +1896,7 @@ export class ChannelBase {
|
|
|
1597
1896
|
// already queued onto this session sees a stale generation and bails
|
|
1598
1897
|
// instead of running bridge.prompt() against the cleared session.
|
|
1599
1898
|
this.sessionGenerations.set(id, (this.sessionGenerations.get(id) ?? 0) + 1);
|
|
1600
|
-
this.removePendingPermissionsForSession(id);
|
|
1899
|
+
this.removePendingPermissionsForSession(id, 'run_cancelled');
|
|
1601
1900
|
// Cancel an in-flight turn (and drop its buffered follow-ups) before
|
|
1602
1901
|
// purging, so a running prompt can't deliver a stale response into —
|
|
1603
1902
|
// or resurrect via collect-drain — the just-cleared session.
|
|
@@ -3328,6 +3627,7 @@ export class ChannelBase {
|
|
|
3328
3627
|
// turn at the channel level (cancelled is already set above), so
|
|
3329
3628
|
// the event reflects that intent, not the bridge RPC outcome.
|
|
3330
3629
|
this.emitTaskCancellation(active, sessionId, 'steer');
|
|
3630
|
+
this.removePendingPermissionsForSession(sessionId, 'run_cancelled');
|
|
3331
3631
|
}
|
|
3332
3632
|
// Diagnostic watchdog: if the predecessor turn is STILL the active prompt
|
|
3333
3633
|
// after the wind-down bound, this steered turn is wedged behind a hung
|
|
@@ -3456,6 +3756,11 @@ export class ChannelBase {
|
|
|
3456
3756
|
doneResolve = r;
|
|
3457
3757
|
});
|
|
3458
3758
|
const promptState = {
|
|
3759
|
+
runId: randomUUID(),
|
|
3760
|
+
owner: {
|
|
3761
|
+
kind: 'channel_user',
|
|
3762
|
+
id: envelope.senderId,
|
|
3763
|
+
},
|
|
3459
3764
|
cancelled: false,
|
|
3460
3765
|
done,
|
|
3461
3766
|
resolve: doneResolve,
|
|
@@ -3465,6 +3770,7 @@ export class ChannelBase {
|
|
|
3465
3770
|
messageId: envelope.messageId,
|
|
3466
3771
|
senderId: envelope.senderId,
|
|
3467
3772
|
senderName: envelope.senderName,
|
|
3773
|
+
metadata: envelope.metadata,
|
|
3468
3774
|
};
|
|
3469
3775
|
// This turn is now the single owner of the session's active-prompt slot.
|
|
3470
3776
|
// (Steer no longer hands a still-active session to a replacement; only
|
|
@@ -3499,12 +3805,13 @@ export class ChannelBase {
|
|
|
3499
3805
|
const releaseHeldChunks = () => {
|
|
3500
3806
|
for (const held of heldChunks.splice(0)) {
|
|
3501
3807
|
hasStreamedText = true;
|
|
3808
|
+
const segment = this.ensureOutputSegment(sessionId, promptState);
|
|
3502
3809
|
this.emitTaskLifecycle({
|
|
3503
3810
|
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
|
|
3504
3811
|
type: 'text_chunk',
|
|
3505
3812
|
chunk: held,
|
|
3506
3813
|
});
|
|
3507
|
-
this.onResponseChunk(envelope.chatId, held, sessionId);
|
|
3814
|
+
this.onResponseChunk(envelope.chatId, held, sessionId, segment);
|
|
3508
3815
|
streamer?.push(held);
|
|
3509
3816
|
}
|
|
3510
3817
|
};
|
|
@@ -3525,7 +3832,8 @@ export class ChannelBase {
|
|
|
3525
3832
|
}
|
|
3526
3833
|
heldChunks.length = 0;
|
|
3527
3834
|
hasStreamedText = false;
|
|
3528
|
-
this.
|
|
3835
|
+
const segment = this.closeOutputSegment(sessionId, promptState);
|
|
3836
|
+
void this.notifyOutputSegmentEnd(envelope.chatId, sessionId, segment, 'response_boundary');
|
|
3529
3837
|
streamer?.stop();
|
|
3530
3838
|
};
|
|
3531
3839
|
const promptBridge = this.bridge;
|
|
@@ -3550,7 +3858,11 @@ export class ChannelBase {
|
|
|
3550
3858
|
await streamer.flush();
|
|
3551
3859
|
}
|
|
3552
3860
|
else {
|
|
3553
|
-
|
|
3861
|
+
const segment = this.ensureOutputSegment(sessionId, promptState);
|
|
3862
|
+
await this.onResponseComplete(envelope.chatId, response, sessionId, segment);
|
|
3863
|
+
if (segment && promptState.activeSegmentId === segment.segmentId) {
|
|
3864
|
+
promptState.activeSegmentId = undefined;
|
|
3865
|
+
}
|
|
3554
3866
|
}
|
|
3555
3867
|
}
|
|
3556
3868
|
// Once delivery started the turn's outcome is fixed — don't let a
|
|
@@ -3559,6 +3871,8 @@ export class ChannelBase {
|
|
|
3559
3871
|
await this.settleCancelRequested(promptState);
|
|
3560
3872
|
}
|
|
3561
3873
|
if (!promptState.cancelled && !promptState.cancellationEmitted) {
|
|
3874
|
+
const segment = this.closeOutputSegment(sessionId, promptState);
|
|
3875
|
+
void this.notifyOutputSegmentEnd(envelope.chatId, sessionId, segment, 'completed');
|
|
3562
3876
|
this.emitTaskLifecycle({
|
|
3563
3877
|
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
|
|
3564
3878
|
type: 'completed',
|
|
@@ -3574,6 +3888,8 @@ export class ChannelBase {
|
|
|
3574
3888
|
}
|
|
3575
3889
|
if (!promptState.cancelled) {
|
|
3576
3890
|
releaseHeldChunks();
|
|
3891
|
+
const segment = this.closeOutputSegment(sessionId, promptState);
|
|
3892
|
+
void this.notifyOutputSegmentEnd(envelope.chatId, sessionId, segment, 'failed');
|
|
3577
3893
|
this.emitTaskLifecycle({
|
|
3578
3894
|
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
|
|
3579
3895
|
type: 'failed',
|
|
@@ -3587,6 +3903,9 @@ export class ChannelBase {
|
|
|
3587
3903
|
const safeMessageId = sanitizeLogText(envelope.messageId ?? '', 64);
|
|
3588
3904
|
process.stderr.write(`[${channel}] turn ${safeMessageId} threw after cancellation for session ${safeSessionId}: ${this.lifecycleError(err)}\n`);
|
|
3589
3905
|
}
|
|
3906
|
+
if (promptState.cancelled) {
|
|
3907
|
+
return;
|
|
3908
|
+
}
|
|
3590
3909
|
throw err;
|
|
3591
3910
|
}
|
|
3592
3911
|
finally {
|