@qwen-code/channel-base 0.21.0 → 0.21.1
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/dist/AcpBridge.d.ts +7 -3
- package/dist/AcpBridge.d.ts.map +1 -0
- package/dist/AcpBridge.js +37 -5
- 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 +15 -5
- package/dist/ChannelBase.d.ts.map +1 -0
- package/dist/ChannelBase.js +113 -80
- 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 +1 -0
- package/dist/PairingStore.d.ts.map +1 -0
- package/dist/PollingChannelBase.d.ts +23 -0
- package/dist/PollingChannelBase.d.ts.map +1 -0
- package/dist/PollingChannelBase.js +107 -0
- package/dist/PollingChannelBase.js.map +1 -0
- package/dist/SenderGate.d.ts +2 -0
- package/dist/SenderGate.d.ts.map +1 -0
- package/dist/SenderGate.js +3 -0
- package/dist/SenderGate.js.map +1 -1
- package/dist/SessionRouter.d.ts +1 -0
- package/dist/SessionRouter.d.ts.map +1 -0
- package/dist/SessionRouter.js +4 -0
- package/dist/SessionRouter.js.map +1 -1
- 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 +4 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- 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 +12 -1
- 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
|
@@ -226,7 +226,7 @@ export class ChannelBase {
|
|
|
226
226
|
await this.pushProactive(target, text);
|
|
227
227
|
}
|
|
228
228
|
else {
|
|
229
|
-
await this.
|
|
229
|
+
await this.sendThreadMessage(target.chatId, target.threadId, text);
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
catch (err) {
|
|
@@ -307,6 +307,14 @@ export class ChannelBase {
|
|
|
307
307
|
this.attachBridgeEvents(bridge);
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Thread-targeted delivery. Polling adapters override this to post comments
|
|
312
|
+
* on a specific issue/PR. The default falls through to sendMessage(chatId,
|
|
313
|
+
* text), ignoring threadId — existing IM adapters are behaviorally unchanged.
|
|
314
|
+
*/
|
|
315
|
+
async sendThreadMessage(chatId, _threadId, text) {
|
|
316
|
+
await this.sendMessage(chatId, text);
|
|
317
|
+
}
|
|
310
318
|
/**
|
|
311
319
|
* Adapter hook for task lifecycle events — the canonical way to track task
|
|
312
320
|
* state (onPromptStart/onPromptEnd are retained for back-compat). The prompt
|
|
@@ -444,7 +452,7 @@ export class ChannelBase {
|
|
|
444
452
|
if (target.threadId) {
|
|
445
453
|
throw new Error('Channel does not support proactive loop messages for threaded targets.');
|
|
446
454
|
}
|
|
447
|
-
await this.
|
|
455
|
+
await this.sendThreadMessage(target.chatId, target.threadId, text);
|
|
448
456
|
}
|
|
449
457
|
async pushProactiveDelivery(target, text) {
|
|
450
458
|
try {
|
|
@@ -640,6 +648,7 @@ export class ChannelBase {
|
|
|
640
648
|
alreadyPrefixed: true,
|
|
641
649
|
referencedText: undefined,
|
|
642
650
|
attachments: undefined,
|
|
651
|
+
metadata: undefined,
|
|
643
652
|
imageBase64: undefined,
|
|
644
653
|
imageMimeType: undefined,
|
|
645
654
|
};
|
|
@@ -1142,6 +1151,7 @@ export class ChannelBase {
|
|
|
1142
1151
|
this.router.removeSessionId(sessionId);
|
|
1143
1152
|
this.instructedSessions.delete(sessionId);
|
|
1144
1153
|
this.unattendedMemorySessions.delete(sessionId);
|
|
1154
|
+
this.discardRetiredSession(promptBridge, sessionId, `timed-out loop ${jobId}`);
|
|
1145
1155
|
process.stderr.write(`[${this.name}] retired timed out loop ${jobId} session ${sessionId} after cancel did not settle\n`);
|
|
1146
1156
|
}
|
|
1147
1157
|
}
|
|
@@ -1152,6 +1162,18 @@ export class ChannelBase {
|
|
|
1152
1162
|
clearTimeout(graceTimer);
|
|
1153
1163
|
}
|
|
1154
1164
|
}
|
|
1165
|
+
discardRetiredSession(promptBridge, sessionId, reason) {
|
|
1166
|
+
const safeSessionId = sanitizeLogText(sessionId, 64);
|
|
1167
|
+
const safeReason = sanitizeLogText(reason, 128);
|
|
1168
|
+
try {
|
|
1169
|
+
void promptBridge.discardSession?.(sessionId).catch((err) => {
|
|
1170
|
+
process.stderr.write(`[${this.name}] failed to discard ${safeReason} session ${safeSessionId}: ${this.lifecycleError(err)}\n`);
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
catch (err) {
|
|
1174
|
+
process.stderr.write(`[${this.name}] failed to discard ${safeReason} session ${safeSessionId}: ${this.lifecycleError(err)}\n`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1155
1177
|
requestActivePromptCancellation(sessionId, reason = 'cancel_command') {
|
|
1156
1178
|
const active = this.activePrompts.get(sessionId);
|
|
1157
1179
|
if (!active) {
|
|
@@ -1294,8 +1316,11 @@ export class ChannelBase {
|
|
|
1294
1316
|
* Override to clear adapter-owned streaming buffers.
|
|
1295
1317
|
*/
|
|
1296
1318
|
onResponseBoundary(_chatId, _sessionId) { }
|
|
1297
|
-
async sendResponseMessage(chatId, text,
|
|
1298
|
-
|
|
1319
|
+
async sendResponseMessage(chatId, text, sessionId) {
|
|
1320
|
+
const active = this.activePrompts.get(sessionId);
|
|
1321
|
+
const target = this.router.getTarget(sessionId);
|
|
1322
|
+
const threadId = active?.threadId ?? target?.threadId;
|
|
1323
|
+
await this.sendThreadMessage(chatId, threadId, text);
|
|
1299
1324
|
}
|
|
1300
1325
|
/**
|
|
1301
1326
|
* Called when the agent's full response is ready.
|
|
@@ -1320,23 +1345,23 @@ export class ChannelBase {
|
|
|
1320
1345
|
// to authorized senders like /clear (auth gate only — no confirm step). A
|
|
1321
1346
|
// non-shared (1:1) session is always authorized, so behavior is unchanged.
|
|
1322
1347
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1323
|
-
await this.
|
|
1348
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can cancel requests in this shared session.');
|
|
1324
1349
|
return true;
|
|
1325
1350
|
}
|
|
1326
1351
|
const activeSessionId = this.findActiveSessionId(envelope);
|
|
1327
1352
|
if (!activeSessionId) {
|
|
1328
|
-
await this.
|
|
1353
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No request is currently running.');
|
|
1329
1354
|
return true;
|
|
1330
1355
|
}
|
|
1331
1356
|
const active = this.activePrompts.get(activeSessionId);
|
|
1332
1357
|
if (!active) {
|
|
1333
|
-
await this.
|
|
1358
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No request is currently running.');
|
|
1334
1359
|
return true;
|
|
1335
1360
|
}
|
|
1336
1361
|
// Single cancel state machine: adapter stop buttons and /cancel share
|
|
1337
1362
|
// requestActivePromptCancellation so the two paths cannot drift.
|
|
1338
1363
|
const cancelSucceeded = await this.requestActivePromptCancellation(activeSessionId, 'cancel_command');
|
|
1339
|
-
await this.
|
|
1364
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, cancelSucceeded
|
|
1340
1365
|
? 'Cancelled current request.'
|
|
1341
1366
|
: 'Failed to cancel current request.');
|
|
1342
1367
|
return true;
|
|
@@ -1487,7 +1512,7 @@ export class ChannelBase {
|
|
|
1487
1512
|
}
|
|
1488
1513
|
async handlePermissionResponseCommand(envelope, args, decision) {
|
|
1489
1514
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1490
|
-
await this.
|
|
1515
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can answer permission requests in this shared session.');
|
|
1491
1516
|
return true;
|
|
1492
1517
|
}
|
|
1493
1518
|
const lookup = this.pendingPermissionForEnvelope(envelope, args);
|
|
@@ -1502,17 +1527,17 @@ export class ChannelBase {
|
|
|
1502
1527
|
return `- ${sanitizeQuotedText(id, 128)}${title}`;
|
|
1503
1528
|
})
|
|
1504
1529
|
.join('\n');
|
|
1505
|
-
await this.
|
|
1530
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Multiple permission requests are pending for this chat. Reply with /${decision} <request-id>.\n${requestList}`);
|
|
1506
1531
|
return true;
|
|
1507
1532
|
}
|
|
1508
1533
|
if (lookup.kind === 'none') {
|
|
1509
|
-
await this.
|
|
1534
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, lookup.explicit
|
|
1510
1535
|
? 'No pending permission request with that id for this chat.'
|
|
1511
1536
|
: 'No pending permission request for this chat.');
|
|
1512
1537
|
return true;
|
|
1513
1538
|
}
|
|
1514
1539
|
if (!this.bridge.respondToPermission) {
|
|
1515
|
-
await this.
|
|
1540
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Permission relay is not available for this session.');
|
|
1516
1541
|
return true;
|
|
1517
1542
|
}
|
|
1518
1543
|
const { pending } = lookup;
|
|
@@ -1528,7 +1553,7 @@ export class ChannelBase {
|
|
|
1528
1553
|
: undefined;
|
|
1529
1554
|
})();
|
|
1530
1555
|
if (!response) {
|
|
1531
|
-
await this.
|
|
1556
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, decision === 'approve-always'
|
|
1532
1557
|
? 'This permission request has no always-allow option.'
|
|
1533
1558
|
: 'This permission request has no approvable option.');
|
|
1534
1559
|
return true;
|
|
@@ -1540,11 +1565,11 @@ export class ChannelBase {
|
|
|
1540
1565
|
catch (err) {
|
|
1541
1566
|
this.removePendingPermission(pending.requestId);
|
|
1542
1567
|
process.stderr.write(`[${this.name}] permission response failed for request ${sanitizeLogText(pending.requestId, 128)}: ${this.lifecycleError(err)}\n`);
|
|
1543
|
-
await this.
|
|
1568
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Failed to answer the permission request.');
|
|
1544
1569
|
return true;
|
|
1545
1570
|
}
|
|
1546
1571
|
this.removePendingPermission(pending.requestId);
|
|
1547
|
-
await this.
|
|
1572
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, accepted
|
|
1548
1573
|
? decision === 'approve'
|
|
1549
1574
|
? 'Permission approved.'
|
|
1550
1575
|
: decision === 'approve-always'
|
|
@@ -1646,11 +1671,12 @@ export class ChannelBase {
|
|
|
1646
1671
|
// bumped value — reclaim it immediately.
|
|
1647
1672
|
this.sessionGenerations.delete(id);
|
|
1648
1673
|
}
|
|
1674
|
+
this.discardRetiredSession(this.bridge, id, 'cleared');
|
|
1649
1675
|
}
|
|
1650
|
-
await this.
|
|
1676
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Session cleared. The next message starts a fresh conversation.');
|
|
1651
1677
|
}
|
|
1652
1678
|
else {
|
|
1653
|
-
await this.
|
|
1679
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No active session to clear.');
|
|
1654
1680
|
}
|
|
1655
1681
|
};
|
|
1656
1682
|
// For a shared session, clearing it affects everyone who shares it: restrict
|
|
@@ -1659,11 +1685,11 @@ export class ChannelBase {
|
|
|
1659
1685
|
// directly — there /clear only touches the caller's own session.
|
|
1660
1686
|
const clearHandler = async (envelope, args) => {
|
|
1661
1687
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1662
|
-
await this.
|
|
1688
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can clear this shared session.');
|
|
1663
1689
|
return true;
|
|
1664
1690
|
}
|
|
1665
1691
|
if (this.isSharedSession(envelope) && args.toLowerCase() !== 'confirm') {
|
|
1666
|
-
await this.
|
|
1692
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'This clears the shared session for everyone who shares it. Re-send with "confirm" (e.g. /clear confirm) to proceed.');
|
|
1667
1693
|
return true;
|
|
1668
1694
|
}
|
|
1669
1695
|
await doClear(envelope);
|
|
@@ -1680,7 +1706,7 @@ export class ChannelBase {
|
|
|
1680
1706
|
// leaks the workspace basename, so non-members shouldn't see it either.
|
|
1681
1707
|
this.registerCommand('who', async (envelope) => {
|
|
1682
1708
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1683
|
-
await this.
|
|
1709
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can view this shared session.');
|
|
1684
1710
|
return true;
|
|
1685
1711
|
}
|
|
1686
1712
|
const active = this.router.hasSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
|
|
@@ -1697,7 +1723,7 @@ export class ChannelBase {
|
|
|
1697
1723
|
: envelope.isGroup
|
|
1698
1724
|
? ' (private to you)'
|
|
1699
1725
|
: '';
|
|
1700
|
-
await this.
|
|
1726
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, [
|
|
1701
1727
|
`Channel: ${this.name}`,
|
|
1702
1728
|
// Identity/memory lines only for channels that opted in — keep
|
|
1703
1729
|
// unconfigured channels' output unchanged.
|
|
@@ -1758,14 +1784,14 @@ export class ChannelBase {
|
|
|
1758
1784
|
}
|
|
1759
1785
|
}
|
|
1760
1786
|
lines.push('', 'Send any text to chat with the agent.');
|
|
1761
|
-
await this.
|
|
1787
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, lines.join('\n'));
|
|
1762
1788
|
return true;
|
|
1763
1789
|
});
|
|
1764
1790
|
this.registerCommand('status', async (envelope) => {
|
|
1765
1791
|
// For a shared session, gate it to authorized senders like /who — /status
|
|
1766
1792
|
// reports session & access state, so non-members shouldn't read it either.
|
|
1767
1793
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1768
|
-
await this.
|
|
1794
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can view this shared session.');
|
|
1769
1795
|
return true;
|
|
1770
1796
|
}
|
|
1771
1797
|
const hasSession = this.router.hasSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
|
|
@@ -1781,18 +1807,18 @@ export class ChannelBase {
|
|
|
1781
1807
|
]
|
|
1782
1808
|
: []),
|
|
1783
1809
|
];
|
|
1784
|
-
await this.
|
|
1810
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, lines.join('\n'));
|
|
1785
1811
|
return true;
|
|
1786
1812
|
});
|
|
1787
1813
|
this.registerCommand('loop', async (envelope, args) => this.handleLoopCommand(envelope, args));
|
|
1788
1814
|
}
|
|
1789
1815
|
async handleLoopCommand(envelope, args) {
|
|
1790
1816
|
if (!this.loopController) {
|
|
1791
|
-
await this.
|
|
1817
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Loops are not available.');
|
|
1792
1818
|
return true;
|
|
1793
1819
|
}
|
|
1794
1820
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1795
|
-
await this.
|
|
1821
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can use loops in this shared session.');
|
|
1796
1822
|
return true;
|
|
1797
1823
|
}
|
|
1798
1824
|
const [subcommand = '', ...rest] = args.trim().split(/\s+/u);
|
|
@@ -1806,7 +1832,7 @@ export class ChannelBase {
|
|
|
1806
1832
|
case 'cancel':
|
|
1807
1833
|
return this.handleLoopCancel(envelope, rest[0]);
|
|
1808
1834
|
default:
|
|
1809
|
-
await this.
|
|
1835
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Usage: /loop add "<cron>" <prompt> | /loop list | /loop inspect <id> | /loop cancel <id>');
|
|
1810
1836
|
return true;
|
|
1811
1837
|
}
|
|
1812
1838
|
}
|
|
@@ -1814,33 +1840,33 @@ export class ChannelBase {
|
|
|
1814
1840
|
if (!this.loopController)
|
|
1815
1841
|
return true;
|
|
1816
1842
|
if (!this.supportsProactiveSend()) {
|
|
1817
|
-
await this.
|
|
1843
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'This channel does not support proactive loop messages.');
|
|
1818
1844
|
return true;
|
|
1819
1845
|
}
|
|
1820
1846
|
if (this.config.sessionScope === 'single') {
|
|
1821
|
-
await this.
|
|
1847
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Loops are not supported when sessionScope is single.');
|
|
1822
1848
|
return true;
|
|
1823
1849
|
}
|
|
1824
1850
|
const parsed = parseLoopAddArgs(args);
|
|
1825
1851
|
if (!parsed) {
|
|
1826
|
-
await this.
|
|
1852
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Usage: /loop add "<cron>" <prompt>');
|
|
1827
1853
|
return true;
|
|
1828
1854
|
}
|
|
1829
1855
|
try {
|
|
1830
1856
|
this.loopController.validateCron(parsed.cron);
|
|
1831
1857
|
}
|
|
1832
1858
|
catch (err) {
|
|
1833
|
-
await this.
|
|
1859
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Invalid cron expression: ${err instanceof Error ? err.message : String(err)}`);
|
|
1834
1860
|
return true;
|
|
1835
1861
|
}
|
|
1836
1862
|
const target = this.loopTargetFromEnvelope(envelope);
|
|
1837
1863
|
if (!this.supportsProactiveTarget(target)) {
|
|
1838
|
-
await this.
|
|
1864
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'This channel does not support proactive loop messages for this chat target.');
|
|
1839
1865
|
return true;
|
|
1840
1866
|
}
|
|
1841
1867
|
const prompt = sanitizePromptText(parsed.prompt.trim());
|
|
1842
1868
|
if (Array.from(prompt).length > MAX_LOOP_PROMPT_CHARS) {
|
|
1843
|
-
await this.
|
|
1869
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Loop prompt is too long; keep it under ${MAX_LOOP_PROMPT_CHARS} characters.`);
|
|
1844
1870
|
return true;
|
|
1845
1871
|
}
|
|
1846
1872
|
const input = {
|
|
@@ -1865,10 +1891,10 @@ export class ChannelBase {
|
|
|
1865
1891
|
}
|
|
1866
1892
|
}
|
|
1867
1893
|
if (!job) {
|
|
1868
|
-
await this.
|
|
1894
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Too many loops for this chat. Cancel an existing loop before adding another.`);
|
|
1869
1895
|
return true;
|
|
1870
1896
|
}
|
|
1871
|
-
await this.
|
|
1897
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Loop ${job.id}: ${job.cron}`);
|
|
1872
1898
|
return true;
|
|
1873
1899
|
}
|
|
1874
1900
|
async createLoopFromTool(sessionId, input) {
|
|
@@ -1975,23 +2001,23 @@ export class ChannelBase {
|
|
|
1975
2001
|
return true;
|
|
1976
2002
|
const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
|
|
1977
2003
|
if (jobs.length === 0) {
|
|
1978
|
-
await this.
|
|
2004
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No loops.');
|
|
1979
2005
|
return true;
|
|
1980
2006
|
}
|
|
1981
|
-
await this.
|
|
2007
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, jobs.map((job) => this.formatLoopListLine(job)).join('\n'));
|
|
1982
2008
|
return true;
|
|
1983
2009
|
}
|
|
1984
2010
|
async handleLoopInspect(envelope, id) {
|
|
1985
2011
|
if (!this.loopController)
|
|
1986
2012
|
return true;
|
|
1987
2013
|
if (!id) {
|
|
1988
|
-
await this.
|
|
2014
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Usage: /loop inspect <id>');
|
|
1989
2015
|
return true;
|
|
1990
2016
|
}
|
|
1991
2017
|
const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
|
|
1992
2018
|
const job = jobs.find((candidate) => candidate.id === id);
|
|
1993
2019
|
if (!job) {
|
|
1994
|
-
await this.
|
|
2020
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `No loop ${id}.`);
|
|
1995
2021
|
return true;
|
|
1996
2022
|
}
|
|
1997
2023
|
const lines = [
|
|
@@ -2013,7 +2039,7 @@ export class ChannelBase {
|
|
|
2013
2039
|
lines.push(`Last result: ${job.lastResultPreview}`);
|
|
2014
2040
|
}
|
|
2015
2041
|
lines.push(`Prompt: ${job.prompt}`);
|
|
2016
|
-
await this.
|
|
2042
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, lines.join('\n'));
|
|
2017
2043
|
return true;
|
|
2018
2044
|
}
|
|
2019
2045
|
formatLoopListLine(job) {
|
|
@@ -2046,17 +2072,17 @@ export class ChannelBase {
|
|
|
2046
2072
|
if (!this.loopController)
|
|
2047
2073
|
return true;
|
|
2048
2074
|
if (!id) {
|
|
2049
|
-
await this.
|
|
2075
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Usage: /loop cancel <id>');
|
|
2050
2076
|
return true;
|
|
2051
2077
|
}
|
|
2052
2078
|
const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
|
|
2053
2079
|
const match = jobs.find((job) => job.id === id);
|
|
2054
2080
|
if (!match) {
|
|
2055
|
-
await this.
|
|
2081
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `No loop ${id}.`);
|
|
2056
2082
|
return true;
|
|
2057
2083
|
}
|
|
2058
2084
|
const disabled = await this.loopController.disable(id);
|
|
2059
|
-
await this.
|
|
2085
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, disabled ? `Cancelled loop ${id}.` : `Failed to cancel loop ${id}.`);
|
|
2060
2086
|
return true;
|
|
2061
2087
|
}
|
|
2062
2088
|
loopTargetFromEnvelope(envelope) {
|
|
@@ -2191,7 +2217,7 @@ export class ChannelBase {
|
|
|
2191
2217
|
}
|
|
2192
2218
|
async getChannelMemory(envelope) {
|
|
2193
2219
|
if (!this.channelMemory) {
|
|
2194
|
-
await this.
|
|
2220
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Channel memory is not configured for this channel.');
|
|
2195
2221
|
return undefined;
|
|
2196
2222
|
}
|
|
2197
2223
|
return this.channelMemory;
|
|
@@ -2211,7 +2237,7 @@ export class ChannelBase {
|
|
|
2211
2237
|
}
|
|
2212
2238
|
async handleChannelMemoryIntent(envelope, intent, options = {}) {
|
|
2213
2239
|
if (intent.kind === 'no_match') {
|
|
2214
|
-
await this.
|
|
2240
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No matching channel memory entry.');
|
|
2215
2241
|
return;
|
|
2216
2242
|
}
|
|
2217
2243
|
if (intent.kind === 'ambiguous') {
|
|
@@ -2224,11 +2250,11 @@ export class ChannelBase {
|
|
|
2224
2250
|
}
|
|
2225
2251
|
catch (error) {
|
|
2226
2252
|
this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
|
|
2227
|
-
await this.
|
|
2253
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2228
2254
|
return;
|
|
2229
2255
|
}
|
|
2230
2256
|
const selected = this.entriesForChannelMemoryIds(entries, intent.ids);
|
|
2231
|
-
await this.
|
|
2257
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, [
|
|
2232
2258
|
'Multiple channel memory entries match:',
|
|
2233
2259
|
...this.renderChannelMemoryCandidates(selected),
|
|
2234
2260
|
].join('\n'));
|
|
@@ -2244,11 +2270,11 @@ export class ChannelBase {
|
|
|
2244
2270
|
}
|
|
2245
2271
|
catch (error) {
|
|
2246
2272
|
this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
|
|
2247
|
-
await this.
|
|
2273
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2248
2274
|
return;
|
|
2249
2275
|
}
|
|
2250
2276
|
const selected = this.entriesForChannelMemoryIds(entries, intent.ids);
|
|
2251
|
-
await this.
|
|
2277
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, [
|
|
2252
2278
|
'Channel memory (page 1/1):',
|
|
2253
2279
|
...this.renderChannelMemoryCandidates(selected),
|
|
2254
2280
|
].join('\n'));
|
|
@@ -2263,7 +2289,7 @@ export class ChannelBase {
|
|
|
2263
2289
|
? this.takePendingChannelMemoryMutation(envelope, 'update')
|
|
2264
2290
|
: this.takePendingChannelMemoryMutation(envelope, 'remove');
|
|
2265
2291
|
if (!pending) {
|
|
2266
|
-
await this.
|
|
2292
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, intent.kind === 'update_confirm'
|
|
2267
2293
|
? 'No pending channel memory update. Start a new update request first.'
|
|
2268
2294
|
: 'No pending channel memory removal. Start a new removal request first.');
|
|
2269
2295
|
return;
|
|
@@ -2291,17 +2317,17 @@ export class ChannelBase {
|
|
|
2291
2317
|
catch (error) {
|
|
2292
2318
|
const message = this.channelMemoryErrorMessage(error);
|
|
2293
2319
|
this.logChannelMemoryError(isUpdate ? 'update' : 'remove', envelope, message);
|
|
2294
|
-
await this.
|
|
2320
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, message === 'Channel memory entry changed'
|
|
2295
2321
|
? 'That channel memory entry changed since it was selected. View channel memory and start the operation again.'
|
|
2296
2322
|
: `Failed to ${isUpdate ? 'update' : 'remove'} channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2297
2323
|
return;
|
|
2298
2324
|
}
|
|
2299
2325
|
if (!changed) {
|
|
2300
|
-
await this.
|
|
2326
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `No channel memory entry ${pending.id}.`);
|
|
2301
2327
|
return;
|
|
2302
2328
|
}
|
|
2303
2329
|
this.invalidateUnattendedMemory(envelope);
|
|
2304
|
-
await this.
|
|
2330
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Channel memory ${pending.id} ${isUpdate ? 'updated' : 'removed'}.`);
|
|
2305
2331
|
return;
|
|
2306
2332
|
}
|
|
2307
2333
|
if (intent.kind === 'natural_update') {
|
|
@@ -2342,7 +2368,7 @@ export class ChannelBase {
|
|
|
2342
2368
|
catch (error) {
|
|
2343
2369
|
const message = this.channelMemoryErrorMessage(error);
|
|
2344
2370
|
this.logChannelMemoryError('save', envelope, message);
|
|
2345
|
-
await this.
|
|
2371
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to save channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2346
2372
|
return;
|
|
2347
2373
|
}
|
|
2348
2374
|
if (result.changed) {
|
|
@@ -2357,17 +2383,17 @@ export class ChannelBase {
|
|
|
2357
2383
|
}
|
|
2358
2384
|
if (result.added.length > 0) {
|
|
2359
2385
|
const ids = result.added.map((entry) => entry.id);
|
|
2360
|
-
await this.
|
|
2386
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, result.duplicateIds.length > 0
|
|
2361
2387
|
? `Channel memory saved: ${ids.join(', ')}. Skipped duplicates: ${result.duplicateIds.join(', ')}.`
|
|
2362
2388
|
: ids.length === 1
|
|
2363
2389
|
? `Channel memory ${ids[0]} saved.`
|
|
2364
2390
|
: `Channel memory saved: ${ids.join(', ')}.`);
|
|
2365
2391
|
}
|
|
2366
2392
|
else if (result.duplicateIds.length > 0) {
|
|
2367
|
-
await this.
|
|
2393
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Channel memory already contains ${result.duplicateIds.join(', ')}.`);
|
|
2368
2394
|
}
|
|
2369
2395
|
else {
|
|
2370
|
-
await this.
|
|
2396
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Channel memory updated.');
|
|
2371
2397
|
}
|
|
2372
2398
|
return;
|
|
2373
2399
|
}
|
|
@@ -2379,30 +2405,30 @@ export class ChannelBase {
|
|
|
2379
2405
|
catch (error) {
|
|
2380
2406
|
const message = this.channelMemoryErrorMessage(error);
|
|
2381
2407
|
this.logChannelMemoryError('read', envelope, message);
|
|
2382
|
-
await this.
|
|
2408
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2383
2409
|
return;
|
|
2384
2410
|
}
|
|
2385
2411
|
if (intent.kind === 'inspect') {
|
|
2386
2412
|
const entry = entries.find((candidate) => candidate.id === intent.id);
|
|
2387
|
-
await this.
|
|
2413
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, entry
|
|
2388
2414
|
? `Channel memory ${entry.id}:\n${sanitizePromptText(entry.text).trim()}`
|
|
2389
2415
|
: `No channel memory entry ${intent.id}.`);
|
|
2390
2416
|
return;
|
|
2391
2417
|
}
|
|
2392
2418
|
const totalPages = Math.max(1, Math.ceil(entries.length / CHANNEL_MEMORY_PAGE_SIZE));
|
|
2393
2419
|
if (intent.page > totalPages) {
|
|
2394
|
-
await this.
|
|
2420
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Channel memory page ${intent.page} does not exist.`);
|
|
2395
2421
|
return;
|
|
2396
2422
|
}
|
|
2397
2423
|
if (entries.length === 0) {
|
|
2398
|
-
await this.
|
|
2424
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No channel memory saved.');
|
|
2399
2425
|
return;
|
|
2400
2426
|
}
|
|
2401
2427
|
const pageStart = (intent.page - 1) * CHANNEL_MEMORY_PAGE_SIZE;
|
|
2402
2428
|
const lines = entries
|
|
2403
2429
|
.slice(pageStart, pageStart + CHANNEL_MEMORY_PAGE_SIZE)
|
|
2404
2430
|
.map((entry) => this.renderChannelMemoryCandidate(entry));
|
|
2405
|
-
await this.
|
|
2431
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, [`Channel memory (page ${intent.page}/${totalPages}):`, ...lines].join('\n'));
|
|
2406
2432
|
return;
|
|
2407
2433
|
}
|
|
2408
2434
|
if (intent.kind === 'update' || intent.kind === 'remove') {
|
|
@@ -2420,21 +2446,21 @@ export class ChannelBase {
|
|
|
2420
2446
|
catch (error) {
|
|
2421
2447
|
const message = this.channelMemoryErrorMessage(error);
|
|
2422
2448
|
this.logChannelMemoryError(isUpdate ? 'update' : 'remove', envelope, message);
|
|
2423
|
-
await this.
|
|
2449
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to ${isUpdate ? 'update' : 'remove'} channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2424
2450
|
return;
|
|
2425
2451
|
}
|
|
2426
2452
|
if (!changed) {
|
|
2427
|
-
await this.
|
|
2453
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `No channel memory entry ${id}.`);
|
|
2428
2454
|
return;
|
|
2429
2455
|
}
|
|
2430
2456
|
this.invalidateUnattendedMemory(envelope);
|
|
2431
|
-
await this.
|
|
2457
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Channel memory ${id} ${isUpdate ? 'updated' : 'removed'}.`);
|
|
2432
2458
|
return;
|
|
2433
2459
|
}
|
|
2434
2460
|
if (intent.kind === 'clear_confirm') {
|
|
2435
2461
|
const pending = this.takePendingChannelMemoryMutation(envelope, 'clear');
|
|
2436
2462
|
if (!pending) {
|
|
2437
|
-
await this.
|
|
2463
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No pending clear request. Say "清空记忆" first.');
|
|
2438
2464
|
return;
|
|
2439
2465
|
}
|
|
2440
2466
|
let result;
|
|
@@ -2444,13 +2470,13 @@ export class ChannelBase {
|
|
|
2444
2470
|
catch (error) {
|
|
2445
2471
|
const message = this.channelMemoryErrorMessage(error);
|
|
2446
2472
|
this.logChannelMemoryError('clear', envelope, message);
|
|
2447
|
-
await this.
|
|
2473
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to clear channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2448
2474
|
return;
|
|
2449
2475
|
}
|
|
2450
2476
|
if (result.changed) {
|
|
2451
2477
|
this.invalidateUnattendedMemory(envelope);
|
|
2452
2478
|
}
|
|
2453
|
-
await this.
|
|
2479
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, result.changed ? 'Channel memory cleared.' : 'No channel memory saved.');
|
|
2454
2480
|
return;
|
|
2455
2481
|
}
|
|
2456
2482
|
const unhandled = intent;
|
|
@@ -2482,7 +2508,7 @@ export class ChannelBase {
|
|
|
2482
2508
|
const key = this.channelMemoryPendingKey(envelope);
|
|
2483
2509
|
this.pendingChannelMemoryMutationDeliveries.set(key, mutation);
|
|
2484
2510
|
try {
|
|
2485
|
-
await this.
|
|
2511
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, message);
|
|
2486
2512
|
}
|
|
2487
2513
|
catch (error) {
|
|
2488
2514
|
if (this.pendingChannelMemoryMutationDeliveries.get(key) === mutation) {
|
|
@@ -2655,15 +2681,18 @@ export class ChannelBase {
|
|
|
2655
2681
|
* Whether the resolved session is SHARED across senders. `single` collapses
|
|
2656
2682
|
* the whole channel to one `__single__` session for EVERY sender — group OR
|
|
2657
2683
|
* DM — so it is ALWAYS shared (even a DM maps to `__single__`). `thread` is
|
|
2658
|
-
* shared only in a group (a DM maps to the lone caller's own chat).
|
|
2659
|
-
*
|
|
2660
|
-
*
|
|
2684
|
+
* shared only in a group (a DM maps to the lone caller's own chat).
|
|
2685
|
+
* `chat_thread` is always shared: it scopes by chat+thread, and a thread
|
|
2686
|
+
* (issue/PR discussion) can carry multiple participants even outside a
|
|
2687
|
+
* group. `user` is per-sender, never shared. Drives both the
|
|
2688
|
+
* destructive-/clear confirm gate and the host-shell (`!`) gate.
|
|
2661
2689
|
*/
|
|
2662
2690
|
isSharedSession(envelope) {
|
|
2663
2691
|
return this.isSharedSessionTarget(envelope);
|
|
2664
2692
|
}
|
|
2665
2693
|
isSharedSessionTarget(target) {
|
|
2666
2694
|
return (this.config.sessionScope === 'single' ||
|
|
2695
|
+
this.config.sessionScope === 'chat_thread' ||
|
|
2667
2696
|
(target.isGroup === true && this.config.sessionScope === 'thread'));
|
|
2668
2697
|
}
|
|
2669
2698
|
/**
|
|
@@ -2962,7 +2991,7 @@ export class ChannelBase {
|
|
|
2962
2991
|
if (!result.allowed) {
|
|
2963
2992
|
if (result.pairingCode !== undefined) {
|
|
2964
2993
|
this.logPreflightRejected('sender_pairing_required');
|
|
2965
|
-
return this.onPairingRequired(envelope.chatId, result.pairingCode)
|
|
2994
|
+
return this.onPairingRequired(envelope.chatId, result.pairingCode, envelope.threadId)
|
|
2966
2995
|
.then(() => false)
|
|
2967
2996
|
.catch((err) => {
|
|
2968
2997
|
process.stderr.write(`[Channel:${this.name}] pairing notification failed: ${sanitizeLogText(err instanceof Error ? err.message : String(err), 200)}\n`);
|
|
@@ -3103,13 +3132,13 @@ export class ChannelBase {
|
|
|
3103
3132
|
process.stderr.write(`[${this.name}] blocked ! shell command from ${who} (sender ${envelope.senderId}) in chat ${sanitizeLogText(envelope.chatId, 64)}\n`);
|
|
3104
3133
|
}
|
|
3105
3134
|
if (envelope.isGroup) {
|
|
3106
|
-
await this.
|
|
3135
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Shell commands (`!`) are disabled in group chats.');
|
|
3107
3136
|
return;
|
|
3108
3137
|
}
|
|
3109
3138
|
// A single-scope DM collapses every DM to one channel-wide session, so it
|
|
3110
3139
|
// is multi-operator too despite not being a group.
|
|
3111
3140
|
if (this.isSharedSession(envelope)) {
|
|
3112
|
-
await this.
|
|
3141
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Shell commands (`!`) are disabled in shared sessions.');
|
|
3113
3142
|
return;
|
|
3114
3143
|
}
|
|
3115
3144
|
}
|
|
@@ -3131,10 +3160,10 @@ export class ChannelBase {
|
|
|
3131
3160
|
const exitLine = result.exitCode !== null && result.exitCode !== 0
|
|
3132
3161
|
? `\nExit code: ${result.exitCode}`
|
|
3133
3162
|
: '';
|
|
3134
|
-
await this.
|
|
3163
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `$ ${cmd}\n${output}${exitLine}`);
|
|
3135
3164
|
}
|
|
3136
3165
|
catch (error) {
|
|
3137
|
-
await this.
|
|
3166
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Shell command failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3138
3167
|
}
|
|
3139
3168
|
return;
|
|
3140
3169
|
}
|
|
@@ -3212,6 +3241,9 @@ export class ChannelBase {
|
|
|
3212
3241
|
promptText = promptText + '\n\n' + filePaths.join('\n');
|
|
3213
3242
|
}
|
|
3214
3243
|
}
|
|
3244
|
+
if (envelope.metadata) {
|
|
3245
|
+
promptText = promptText + '\n\n' + sanitizePromptText(envelope.metadata);
|
|
3246
|
+
}
|
|
3215
3247
|
// Resolve dispatch mode: per-group override → channel config → default
|
|
3216
3248
|
const groupCfg = envelope.isGroup
|
|
3217
3249
|
? this.config.groups[envelope.chatId] || this.config.groups['*']
|
|
@@ -3628,6 +3660,7 @@ export class ChannelBase {
|
|
|
3628
3660
|
// Clear attachments/references — already resolved in original text
|
|
3629
3661
|
referencedText: undefined,
|
|
3630
3662
|
attachments: undefined,
|
|
3663
|
+
metadata: undefined,
|
|
3631
3664
|
imageBase64: undefined,
|
|
3632
3665
|
imageMimeType: undefined,
|
|
3633
3666
|
};
|
|
@@ -3643,12 +3676,12 @@ export class ChannelBase {
|
|
|
3643
3676
|
this.sessionQueues.set(sessionId, current.catch(() => { }));
|
|
3644
3677
|
await current;
|
|
3645
3678
|
}
|
|
3646
|
-
async onPairingRequired(chatId, code) {
|
|
3679
|
+
async onPairingRequired(chatId, code, threadId) {
|
|
3647
3680
|
if (code) {
|
|
3648
|
-
await this.
|
|
3681
|
+
await this.sendThreadMessage(chatId, threadId, `Your pairing code is: ${code}\n\nAsk the bot operator to approve you with:\n qwen channel pairing approve ${this.name} ${code}`);
|
|
3649
3682
|
}
|
|
3650
3683
|
else {
|
|
3651
|
-
await this.
|
|
3684
|
+
await this.sendThreadMessage(chatId, threadId, 'Too many pending pairing requests. Please try again later.');
|
|
3652
3685
|
}
|
|
3653
3686
|
}
|
|
3654
3687
|
}
|