@qwen-code/channel-base 0.21.1-preview.0 → 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/AcpBridge.d.ts +7 -3
- package/dist/AcpBridge.d.ts.map +1 -0
- package/dist/AcpBridge.js +35 -3
- package/dist/AcpBridge.js.map +1 -1
- package/dist/BlockStreamer.d.ts +1 -0
- package/dist/BlockStreamer.d.ts.map +1 -0
- package/dist/ChannelAgentBridge.d.ts +1 -0
- package/dist/ChannelAgentBridge.d.ts.map +1 -0
- package/dist/ChannelBase.d.ts +30 -4
- package/dist/ChannelBase.d.ts.map +1 -0
- package/dist/ChannelBase.js +347 -14
- package/dist/ChannelBase.js.map +1 -1
- package/dist/ChannelLoopScheduler.d.ts +1 -0
- package/dist/ChannelLoopScheduler.d.ts.map +1 -0
- package/dist/ChannelLoopStore.d.ts +1 -0
- package/dist/ChannelLoopStore.d.ts.map +1 -0
- package/dist/ChannelLoopTools.d.ts +1 -0
- package/dist/ChannelLoopTools.d.ts.map +1 -0
- package/dist/ChannelProactiveDeliveryError.d.ts +1 -0
- package/dist/ChannelProactiveDeliveryError.d.ts.map +1 -0
- package/dist/ChannelWebhookTask.d.ts +1 -0
- package/dist/ChannelWebhookTask.d.ts.map +1 -0
- package/dist/DaemonChannelBridge.d.ts +16 -1
- package/dist/DaemonChannelBridge.d.ts.map +1 -0
- package/dist/DaemonChannelBridge.js +75 -2
- package/dist/DaemonChannelBridge.js.map +1 -1
- package/dist/DmGate.d.ts +1 -0
- package/dist/DmGate.d.ts.map +1 -0
- package/dist/GroupGate.d.ts +1 -0
- package/dist/GroupGate.d.ts.map +1 -0
- package/dist/PairingStore.d.ts +4 -2
- package/dist/PairingStore.d.ts.map +1 -0
- package/dist/PairingStore.js +11 -2
- package/dist/PairingStore.js.map +1 -1
- package/dist/PollingChannelBase.d.ts +1 -0
- package/dist/PollingChannelBase.d.ts.map +1 -0
- package/dist/SenderGate.d.ts +1 -0
- package/dist/SenderGate.d.ts.map +1 -0
- package/dist/SessionRouter.d.ts +1 -0
- package/dist/SessionRouter.d.ts.map +1 -0
- package/dist/channel-memory-intent.d.ts +1 -0
- package/dist/channel-memory-intent.d.ts.map +1 -0
- package/dist/channel-memory-recall.d.ts +1 -0
- package/dist/channel-memory-recall.d.ts.map +1 -0
- package/dist/group-history-store.d.ts +1 -0
- package/dist/group-history-store.d.ts.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +1 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- 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,
|
|
@@ -1151,6 +1376,7 @@ export class ChannelBase {
|
|
|
1151
1376
|
this.router.removeSessionId(sessionId);
|
|
1152
1377
|
this.instructedSessions.delete(sessionId);
|
|
1153
1378
|
this.unattendedMemorySessions.delete(sessionId);
|
|
1379
|
+
this.discardRetiredSession(promptBridge, sessionId, `timed-out loop ${jobId}`);
|
|
1154
1380
|
process.stderr.write(`[${this.name}] retired timed out loop ${jobId} session ${sessionId} after cancel did not settle\n`);
|
|
1155
1381
|
}
|
|
1156
1382
|
}
|
|
@@ -1161,6 +1387,18 @@ export class ChannelBase {
|
|
|
1161
1387
|
clearTimeout(graceTimer);
|
|
1162
1388
|
}
|
|
1163
1389
|
}
|
|
1390
|
+
discardRetiredSession(promptBridge, sessionId, reason) {
|
|
1391
|
+
const safeSessionId = sanitizeLogText(sessionId, 64);
|
|
1392
|
+
const safeReason = sanitizeLogText(reason, 128);
|
|
1393
|
+
try {
|
|
1394
|
+
void promptBridge.discardSession?.(sessionId).catch((err) => {
|
|
1395
|
+
process.stderr.write(`[${this.name}] failed to discard ${safeReason} session ${safeSessionId}: ${this.lifecycleError(err)}\n`);
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
catch (err) {
|
|
1399
|
+
process.stderr.write(`[${this.name}] failed to discard ${safeReason} session ${safeSessionId}: ${this.lifecycleError(err)}\n`);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1164
1402
|
requestActivePromptCancellation(sessionId, reason = 'cancel_command') {
|
|
1165
1403
|
const active = this.activePrompts.get(sessionId);
|
|
1166
1404
|
if (!active) {
|
|
@@ -1199,10 +1437,18 @@ export class ChannelBase {
|
|
|
1199
1437
|
active.cancelled = true;
|
|
1200
1438
|
this.stopActiveStreaming(active, sessionId, reason);
|
|
1201
1439
|
this.dropCollectBuffer(sessionId);
|
|
1440
|
+
this.removePendingPermissionsForSession(sessionId, 'run_cancelled');
|
|
1202
1441
|
this.emitTaskCancellation(active, sessionId, reason);
|
|
1203
1442
|
return true;
|
|
1204
1443
|
});
|
|
1205
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
|
+
}
|
|
1206
1452
|
dropCollectBuffer(sessionId) {
|
|
1207
1453
|
const buffer = this.collectBuffers.get(sessionId);
|
|
1208
1454
|
if (!buffer)
|
|
@@ -1297,7 +1543,12 @@ export class ChannelBase {
|
|
|
1297
1543
|
* Override to implement progressive display (e.g., updating an AI card in-place).
|
|
1298
1544
|
* Default: no-op (chunks are collected internally and delivered via onResponseComplete).
|
|
1299
1545
|
*/
|
|
1300
|
-
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
|
+
}
|
|
1301
1552
|
/**
|
|
1302
1553
|
* Called when the agent starts a new response segment for the same prompt.
|
|
1303
1554
|
* Override to clear adapter-owned streaming buffers.
|
|
@@ -1309,12 +1560,33 @@ export class ChannelBase {
|
|
|
1309
1560
|
const threadId = active?.threadId ?? target?.threadId;
|
|
1310
1561
|
await this.sendThreadMessage(chatId, threadId, text);
|
|
1311
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
|
+
}
|
|
1312
1584
|
/**
|
|
1313
1585
|
* Called when the agent's full response is ready.
|
|
1314
1586
|
* Override to customize delivery (e.g., finalize an AI card).
|
|
1315
1587
|
* Default: sends the full response text.
|
|
1316
1588
|
*/
|
|
1317
|
-
async onResponseComplete(chatId, fullText, sessionId) {
|
|
1589
|
+
async onResponseComplete(chatId, fullText, sessionId, _segment) {
|
|
1318
1590
|
await this.sendResponseMessage(chatId, fullText, sessionId);
|
|
1319
1591
|
}
|
|
1320
1592
|
/**
|
|
@@ -1371,12 +1643,13 @@ export class ChannelBase {
|
|
|
1371
1643
|
}
|
|
1372
1644
|
return live;
|
|
1373
1645
|
}
|
|
1374
|
-
removePendingPermission(requestId) {
|
|
1646
|
+
removePendingPermission(requestId, reason = 'resolved_outside_presenter') {
|
|
1375
1647
|
const pending = this.pendingPermissions.get(requestId);
|
|
1376
1648
|
if (!pending) {
|
|
1377
1649
|
return;
|
|
1378
1650
|
}
|
|
1379
1651
|
this.pendingPermissions.delete(requestId);
|
|
1652
|
+
this.settleUserInput(pending, reason);
|
|
1380
1653
|
const chatKey = this.permissionChatKey(pending.target);
|
|
1381
1654
|
const requestIds = this.pendingPermissionsByChat.get(chatKey);
|
|
1382
1655
|
if (!requestIds) {
|
|
@@ -1390,17 +1663,48 @@ export class ChannelBase {
|
|
|
1390
1663
|
this.pendingPermissionsByChat.set(chatKey, remaining);
|
|
1391
1664
|
}
|
|
1392
1665
|
}
|
|
1393
|
-
removePendingPermissionsForSession(sessionId) {
|
|
1666
|
+
removePendingPermissionsForSession(sessionId, reason = 'cancelled') {
|
|
1394
1667
|
const requestIds = Array.from(this.pendingPermissions)
|
|
1395
1668
|
.filter(([, pending]) => pending.sessionId === sessionId)
|
|
1396
1669
|
.map(([requestId]) => requestId);
|
|
1397
1670
|
for (const requestId of requestIds) {
|
|
1398
|
-
this.removePendingPermission(requestId);
|
|
1671
|
+
this.removePendingPermission(requestId, reason);
|
|
1399
1672
|
}
|
|
1400
1673
|
}
|
|
1401
1674
|
clearPendingPermissions() {
|
|
1402
|
-
this.pendingPermissions.
|
|
1403
|
-
|
|
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';
|
|
1404
1708
|
}
|
|
1405
1709
|
pendingPermissionForEnvelope(envelope, args) {
|
|
1406
1710
|
const trimmed = args.trim();
|
|
@@ -1434,6 +1738,8 @@ export class ChannelBase {
|
|
|
1434
1738
|
canEnvelopeAnswerPendingPermission(envelope, pending) {
|
|
1435
1739
|
return (pending.target.chatId === envelope.chatId &&
|
|
1436
1740
|
pending.target.threadId === envelope.threadId &&
|
|
1741
|
+
(!pending.userInputPresented ||
|
|
1742
|
+
pending.target.senderId === envelope.senderId) &&
|
|
1437
1743
|
(this.isSharedSessionTarget(pending.target) ||
|
|
1438
1744
|
pending.target.senderId === envelope.senderId));
|
|
1439
1745
|
}
|
|
@@ -1528,6 +1834,10 @@ export class ChannelBase {
|
|
|
1528
1834
|
return true;
|
|
1529
1835
|
}
|
|
1530
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
|
+
}
|
|
1531
1841
|
const response = (() => {
|
|
1532
1842
|
if (decision === 'deny') {
|
|
1533
1843
|
return this.denialResponse(pending);
|
|
@@ -1547,7 +1857,9 @@ export class ChannelBase {
|
|
|
1547
1857
|
}
|
|
1548
1858
|
let accepted;
|
|
1549
1859
|
try {
|
|
1550
|
-
accepted =
|
|
1860
|
+
accepted = pending.userInputPresented
|
|
1861
|
+
? await this.respondToUserInput(pending, response)
|
|
1862
|
+
: await this.bridge.respondToPermission(pending.requestId, response);
|
|
1551
1863
|
}
|
|
1552
1864
|
catch (err) {
|
|
1553
1865
|
this.removePendingPermission(pending.requestId);
|
|
@@ -1584,7 +1896,7 @@ export class ChannelBase {
|
|
|
1584
1896
|
// already queued onto this session sees a stale generation and bails
|
|
1585
1897
|
// instead of running bridge.prompt() against the cleared session.
|
|
1586
1898
|
this.sessionGenerations.set(id, (this.sessionGenerations.get(id) ?? 0) + 1);
|
|
1587
|
-
this.removePendingPermissionsForSession(id);
|
|
1899
|
+
this.removePendingPermissionsForSession(id, 'run_cancelled');
|
|
1588
1900
|
// Cancel an in-flight turn (and drop its buffered follow-ups) before
|
|
1589
1901
|
// purging, so a running prompt can't deliver a stale response into —
|
|
1590
1902
|
// or resurrect via collect-drain — the just-cleared session.
|
|
@@ -1658,6 +1970,7 @@ export class ChannelBase {
|
|
|
1658
1970
|
// bumped value — reclaim it immediately.
|
|
1659
1971
|
this.sessionGenerations.delete(id);
|
|
1660
1972
|
}
|
|
1973
|
+
this.discardRetiredSession(this.bridge, id, 'cleared');
|
|
1661
1974
|
}
|
|
1662
1975
|
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Session cleared. The next message starts a fresh conversation.');
|
|
1663
1976
|
}
|
|
@@ -3314,6 +3627,7 @@ export class ChannelBase {
|
|
|
3314
3627
|
// turn at the channel level (cancelled is already set above), so
|
|
3315
3628
|
// the event reflects that intent, not the bridge RPC outcome.
|
|
3316
3629
|
this.emitTaskCancellation(active, sessionId, 'steer');
|
|
3630
|
+
this.removePendingPermissionsForSession(sessionId, 'run_cancelled');
|
|
3317
3631
|
}
|
|
3318
3632
|
// Diagnostic watchdog: if the predecessor turn is STILL the active prompt
|
|
3319
3633
|
// after the wind-down bound, this steered turn is wedged behind a hung
|
|
@@ -3442,6 +3756,11 @@ export class ChannelBase {
|
|
|
3442
3756
|
doneResolve = r;
|
|
3443
3757
|
});
|
|
3444
3758
|
const promptState = {
|
|
3759
|
+
runId: randomUUID(),
|
|
3760
|
+
owner: {
|
|
3761
|
+
kind: 'channel_user',
|
|
3762
|
+
id: envelope.senderId,
|
|
3763
|
+
},
|
|
3445
3764
|
cancelled: false,
|
|
3446
3765
|
done,
|
|
3447
3766
|
resolve: doneResolve,
|
|
@@ -3451,6 +3770,7 @@ export class ChannelBase {
|
|
|
3451
3770
|
messageId: envelope.messageId,
|
|
3452
3771
|
senderId: envelope.senderId,
|
|
3453
3772
|
senderName: envelope.senderName,
|
|
3773
|
+
metadata: envelope.metadata,
|
|
3454
3774
|
};
|
|
3455
3775
|
// This turn is now the single owner of the session's active-prompt slot.
|
|
3456
3776
|
// (Steer no longer hands a still-active session to a replacement; only
|
|
@@ -3485,12 +3805,13 @@ export class ChannelBase {
|
|
|
3485
3805
|
const releaseHeldChunks = () => {
|
|
3486
3806
|
for (const held of heldChunks.splice(0)) {
|
|
3487
3807
|
hasStreamedText = true;
|
|
3808
|
+
const segment = this.ensureOutputSegment(sessionId, promptState);
|
|
3488
3809
|
this.emitTaskLifecycle({
|
|
3489
3810
|
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
|
|
3490
3811
|
type: 'text_chunk',
|
|
3491
3812
|
chunk: held,
|
|
3492
3813
|
});
|
|
3493
|
-
this.onResponseChunk(envelope.chatId, held, sessionId);
|
|
3814
|
+
this.onResponseChunk(envelope.chatId, held, sessionId, segment);
|
|
3494
3815
|
streamer?.push(held);
|
|
3495
3816
|
}
|
|
3496
3817
|
};
|
|
@@ -3511,7 +3832,8 @@ export class ChannelBase {
|
|
|
3511
3832
|
}
|
|
3512
3833
|
heldChunks.length = 0;
|
|
3513
3834
|
hasStreamedText = false;
|
|
3514
|
-
this.
|
|
3835
|
+
const segment = this.closeOutputSegment(sessionId, promptState);
|
|
3836
|
+
void this.notifyOutputSegmentEnd(envelope.chatId, sessionId, segment, 'response_boundary');
|
|
3515
3837
|
streamer?.stop();
|
|
3516
3838
|
};
|
|
3517
3839
|
const promptBridge = this.bridge;
|
|
@@ -3536,7 +3858,11 @@ export class ChannelBase {
|
|
|
3536
3858
|
await streamer.flush();
|
|
3537
3859
|
}
|
|
3538
3860
|
else {
|
|
3539
|
-
|
|
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
|
+
}
|
|
3540
3866
|
}
|
|
3541
3867
|
}
|
|
3542
3868
|
// Once delivery started the turn's outcome is fixed — don't let a
|
|
@@ -3545,6 +3871,8 @@ export class ChannelBase {
|
|
|
3545
3871
|
await this.settleCancelRequested(promptState);
|
|
3546
3872
|
}
|
|
3547
3873
|
if (!promptState.cancelled && !promptState.cancellationEmitted) {
|
|
3874
|
+
const segment = this.closeOutputSegment(sessionId, promptState);
|
|
3875
|
+
void this.notifyOutputSegmentEnd(envelope.chatId, sessionId, segment, 'completed');
|
|
3548
3876
|
this.emitTaskLifecycle({
|
|
3549
3877
|
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
|
|
3550
3878
|
type: 'completed',
|
|
@@ -3560,6 +3888,8 @@ export class ChannelBase {
|
|
|
3560
3888
|
}
|
|
3561
3889
|
if (!promptState.cancelled) {
|
|
3562
3890
|
releaseHeldChunks();
|
|
3891
|
+
const segment = this.closeOutputSegment(sessionId, promptState);
|
|
3892
|
+
void this.notifyOutputSegmentEnd(envelope.chatId, sessionId, segment, 'failed');
|
|
3563
3893
|
this.emitTaskLifecycle({
|
|
3564
3894
|
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
|
|
3565
3895
|
type: 'failed',
|
|
@@ -3573,6 +3903,9 @@ export class ChannelBase {
|
|
|
3573
3903
|
const safeMessageId = sanitizeLogText(envelope.messageId ?? '', 64);
|
|
3574
3904
|
process.stderr.write(`[${channel}] turn ${safeMessageId} threw after cancellation for session ${safeSessionId}: ${this.lifecycleError(err)}\n`);
|
|
3575
3905
|
}
|
|
3906
|
+
if (promptState.cancelled) {
|
|
3907
|
+
return;
|
|
3908
|
+
}
|
|
3576
3909
|
throw err;
|
|
3577
3910
|
}
|
|
3578
3911
|
finally {
|