@qwen-code/channel-base 0.21.0 → 0.21.1-preview.0
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.js +2 -2
- package/dist/AcpBridge.js.map +1 -1
- package/dist/ChannelBase.d.ts +13 -5
- package/dist/ChannelBase.js +99 -80
- package/dist/ChannelBase.js.map +1 -1
- package/dist/PollingChannelBase.d.ts +22 -0
- package/dist/PollingChannelBase.js +107 -0
- package/dist/PollingChannelBase.js.map +1 -0
- package/dist/SenderGate.d.ts +1 -0
- package/dist/SenderGate.js +3 -0
- package/dist/SenderGate.js.map +1 -1
- package/dist/SessionRouter.js +4 -0
- package/dist/SessionRouter.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +11 -1
- 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
|
};
|
|
@@ -1294,8 +1303,11 @@ export class ChannelBase {
|
|
|
1294
1303
|
* Override to clear adapter-owned streaming buffers.
|
|
1295
1304
|
*/
|
|
1296
1305
|
onResponseBoundary(_chatId, _sessionId) { }
|
|
1297
|
-
async sendResponseMessage(chatId, text,
|
|
1298
|
-
|
|
1306
|
+
async sendResponseMessage(chatId, text, sessionId) {
|
|
1307
|
+
const active = this.activePrompts.get(sessionId);
|
|
1308
|
+
const target = this.router.getTarget(sessionId);
|
|
1309
|
+
const threadId = active?.threadId ?? target?.threadId;
|
|
1310
|
+
await this.sendThreadMessage(chatId, threadId, text);
|
|
1299
1311
|
}
|
|
1300
1312
|
/**
|
|
1301
1313
|
* Called when the agent's full response is ready.
|
|
@@ -1320,23 +1332,23 @@ export class ChannelBase {
|
|
|
1320
1332
|
// to authorized senders like /clear (auth gate only — no confirm step). A
|
|
1321
1333
|
// non-shared (1:1) session is always authorized, so behavior is unchanged.
|
|
1322
1334
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1323
|
-
await this.
|
|
1335
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can cancel requests in this shared session.');
|
|
1324
1336
|
return true;
|
|
1325
1337
|
}
|
|
1326
1338
|
const activeSessionId = this.findActiveSessionId(envelope);
|
|
1327
1339
|
if (!activeSessionId) {
|
|
1328
|
-
await this.
|
|
1340
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No request is currently running.');
|
|
1329
1341
|
return true;
|
|
1330
1342
|
}
|
|
1331
1343
|
const active = this.activePrompts.get(activeSessionId);
|
|
1332
1344
|
if (!active) {
|
|
1333
|
-
await this.
|
|
1345
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No request is currently running.');
|
|
1334
1346
|
return true;
|
|
1335
1347
|
}
|
|
1336
1348
|
// Single cancel state machine: adapter stop buttons and /cancel share
|
|
1337
1349
|
// requestActivePromptCancellation so the two paths cannot drift.
|
|
1338
1350
|
const cancelSucceeded = await this.requestActivePromptCancellation(activeSessionId, 'cancel_command');
|
|
1339
|
-
await this.
|
|
1351
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, cancelSucceeded
|
|
1340
1352
|
? 'Cancelled current request.'
|
|
1341
1353
|
: 'Failed to cancel current request.');
|
|
1342
1354
|
return true;
|
|
@@ -1487,7 +1499,7 @@ export class ChannelBase {
|
|
|
1487
1499
|
}
|
|
1488
1500
|
async handlePermissionResponseCommand(envelope, args, decision) {
|
|
1489
1501
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1490
|
-
await this.
|
|
1502
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can answer permission requests in this shared session.');
|
|
1491
1503
|
return true;
|
|
1492
1504
|
}
|
|
1493
1505
|
const lookup = this.pendingPermissionForEnvelope(envelope, args);
|
|
@@ -1502,17 +1514,17 @@ export class ChannelBase {
|
|
|
1502
1514
|
return `- ${sanitizeQuotedText(id, 128)}${title}`;
|
|
1503
1515
|
})
|
|
1504
1516
|
.join('\n');
|
|
1505
|
-
await this.
|
|
1517
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Multiple permission requests are pending for this chat. Reply with /${decision} <request-id>.\n${requestList}`);
|
|
1506
1518
|
return true;
|
|
1507
1519
|
}
|
|
1508
1520
|
if (lookup.kind === 'none') {
|
|
1509
|
-
await this.
|
|
1521
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, lookup.explicit
|
|
1510
1522
|
? 'No pending permission request with that id for this chat.'
|
|
1511
1523
|
: 'No pending permission request for this chat.');
|
|
1512
1524
|
return true;
|
|
1513
1525
|
}
|
|
1514
1526
|
if (!this.bridge.respondToPermission) {
|
|
1515
|
-
await this.
|
|
1527
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Permission relay is not available for this session.');
|
|
1516
1528
|
return true;
|
|
1517
1529
|
}
|
|
1518
1530
|
const { pending } = lookup;
|
|
@@ -1528,7 +1540,7 @@ export class ChannelBase {
|
|
|
1528
1540
|
: undefined;
|
|
1529
1541
|
})();
|
|
1530
1542
|
if (!response) {
|
|
1531
|
-
await this.
|
|
1543
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, decision === 'approve-always'
|
|
1532
1544
|
? 'This permission request has no always-allow option.'
|
|
1533
1545
|
: 'This permission request has no approvable option.');
|
|
1534
1546
|
return true;
|
|
@@ -1540,11 +1552,11 @@ export class ChannelBase {
|
|
|
1540
1552
|
catch (err) {
|
|
1541
1553
|
this.removePendingPermission(pending.requestId);
|
|
1542
1554
|
process.stderr.write(`[${this.name}] permission response failed for request ${sanitizeLogText(pending.requestId, 128)}: ${this.lifecycleError(err)}\n`);
|
|
1543
|
-
await this.
|
|
1555
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Failed to answer the permission request.');
|
|
1544
1556
|
return true;
|
|
1545
1557
|
}
|
|
1546
1558
|
this.removePendingPermission(pending.requestId);
|
|
1547
|
-
await this.
|
|
1559
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, accepted
|
|
1548
1560
|
? decision === 'approve'
|
|
1549
1561
|
? 'Permission approved.'
|
|
1550
1562
|
: decision === 'approve-always'
|
|
@@ -1647,10 +1659,10 @@ export class ChannelBase {
|
|
|
1647
1659
|
this.sessionGenerations.delete(id);
|
|
1648
1660
|
}
|
|
1649
1661
|
}
|
|
1650
|
-
await this.
|
|
1662
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Session cleared. The next message starts a fresh conversation.');
|
|
1651
1663
|
}
|
|
1652
1664
|
else {
|
|
1653
|
-
await this.
|
|
1665
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No active session to clear.');
|
|
1654
1666
|
}
|
|
1655
1667
|
};
|
|
1656
1668
|
// For a shared session, clearing it affects everyone who shares it: restrict
|
|
@@ -1659,11 +1671,11 @@ export class ChannelBase {
|
|
|
1659
1671
|
// directly — there /clear only touches the caller's own session.
|
|
1660
1672
|
const clearHandler = async (envelope, args) => {
|
|
1661
1673
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1662
|
-
await this.
|
|
1674
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can clear this shared session.');
|
|
1663
1675
|
return true;
|
|
1664
1676
|
}
|
|
1665
1677
|
if (this.isSharedSession(envelope) && args.toLowerCase() !== 'confirm') {
|
|
1666
|
-
await this.
|
|
1678
|
+
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
1679
|
return true;
|
|
1668
1680
|
}
|
|
1669
1681
|
await doClear(envelope);
|
|
@@ -1680,7 +1692,7 @@ export class ChannelBase {
|
|
|
1680
1692
|
// leaks the workspace basename, so non-members shouldn't see it either.
|
|
1681
1693
|
this.registerCommand('who', async (envelope) => {
|
|
1682
1694
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1683
|
-
await this.
|
|
1695
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can view this shared session.');
|
|
1684
1696
|
return true;
|
|
1685
1697
|
}
|
|
1686
1698
|
const active = this.router.hasSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
|
|
@@ -1697,7 +1709,7 @@ export class ChannelBase {
|
|
|
1697
1709
|
: envelope.isGroup
|
|
1698
1710
|
? ' (private to you)'
|
|
1699
1711
|
: '';
|
|
1700
|
-
await this.
|
|
1712
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, [
|
|
1701
1713
|
`Channel: ${this.name}`,
|
|
1702
1714
|
// Identity/memory lines only for channels that opted in — keep
|
|
1703
1715
|
// unconfigured channels' output unchanged.
|
|
@@ -1758,14 +1770,14 @@ export class ChannelBase {
|
|
|
1758
1770
|
}
|
|
1759
1771
|
}
|
|
1760
1772
|
lines.push('', 'Send any text to chat with the agent.');
|
|
1761
|
-
await this.
|
|
1773
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, lines.join('\n'));
|
|
1762
1774
|
return true;
|
|
1763
1775
|
});
|
|
1764
1776
|
this.registerCommand('status', async (envelope) => {
|
|
1765
1777
|
// For a shared session, gate it to authorized senders like /who — /status
|
|
1766
1778
|
// reports session & access state, so non-members shouldn't read it either.
|
|
1767
1779
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1768
|
-
await this.
|
|
1780
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can view this shared session.');
|
|
1769
1781
|
return true;
|
|
1770
1782
|
}
|
|
1771
1783
|
const hasSession = this.router.hasSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
|
|
@@ -1781,18 +1793,18 @@ export class ChannelBase {
|
|
|
1781
1793
|
]
|
|
1782
1794
|
: []),
|
|
1783
1795
|
];
|
|
1784
|
-
await this.
|
|
1796
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, lines.join('\n'));
|
|
1785
1797
|
return true;
|
|
1786
1798
|
});
|
|
1787
1799
|
this.registerCommand('loop', async (envelope, args) => this.handleLoopCommand(envelope, args));
|
|
1788
1800
|
}
|
|
1789
1801
|
async handleLoopCommand(envelope, args) {
|
|
1790
1802
|
if (!this.loopController) {
|
|
1791
|
-
await this.
|
|
1803
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Loops are not available.');
|
|
1792
1804
|
return true;
|
|
1793
1805
|
}
|
|
1794
1806
|
if (!this.isAuthorizedForSharedSession(envelope)) {
|
|
1795
|
-
await this.
|
|
1807
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Only authorized members can use loops in this shared session.');
|
|
1796
1808
|
return true;
|
|
1797
1809
|
}
|
|
1798
1810
|
const [subcommand = '', ...rest] = args.trim().split(/\s+/u);
|
|
@@ -1806,7 +1818,7 @@ export class ChannelBase {
|
|
|
1806
1818
|
case 'cancel':
|
|
1807
1819
|
return this.handleLoopCancel(envelope, rest[0]);
|
|
1808
1820
|
default:
|
|
1809
|
-
await this.
|
|
1821
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Usage: /loop add "<cron>" <prompt> | /loop list | /loop inspect <id> | /loop cancel <id>');
|
|
1810
1822
|
return true;
|
|
1811
1823
|
}
|
|
1812
1824
|
}
|
|
@@ -1814,33 +1826,33 @@ export class ChannelBase {
|
|
|
1814
1826
|
if (!this.loopController)
|
|
1815
1827
|
return true;
|
|
1816
1828
|
if (!this.supportsProactiveSend()) {
|
|
1817
|
-
await this.
|
|
1829
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'This channel does not support proactive loop messages.');
|
|
1818
1830
|
return true;
|
|
1819
1831
|
}
|
|
1820
1832
|
if (this.config.sessionScope === 'single') {
|
|
1821
|
-
await this.
|
|
1833
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Loops are not supported when sessionScope is single.');
|
|
1822
1834
|
return true;
|
|
1823
1835
|
}
|
|
1824
1836
|
const parsed = parseLoopAddArgs(args);
|
|
1825
1837
|
if (!parsed) {
|
|
1826
|
-
await this.
|
|
1838
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Usage: /loop add "<cron>" <prompt>');
|
|
1827
1839
|
return true;
|
|
1828
1840
|
}
|
|
1829
1841
|
try {
|
|
1830
1842
|
this.loopController.validateCron(parsed.cron);
|
|
1831
1843
|
}
|
|
1832
1844
|
catch (err) {
|
|
1833
|
-
await this.
|
|
1845
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Invalid cron expression: ${err instanceof Error ? err.message : String(err)}`);
|
|
1834
1846
|
return true;
|
|
1835
1847
|
}
|
|
1836
1848
|
const target = this.loopTargetFromEnvelope(envelope);
|
|
1837
1849
|
if (!this.supportsProactiveTarget(target)) {
|
|
1838
|
-
await this.
|
|
1850
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'This channel does not support proactive loop messages for this chat target.');
|
|
1839
1851
|
return true;
|
|
1840
1852
|
}
|
|
1841
1853
|
const prompt = sanitizePromptText(parsed.prompt.trim());
|
|
1842
1854
|
if (Array.from(prompt).length > MAX_LOOP_PROMPT_CHARS) {
|
|
1843
|
-
await this.
|
|
1855
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Loop prompt is too long; keep it under ${MAX_LOOP_PROMPT_CHARS} characters.`);
|
|
1844
1856
|
return true;
|
|
1845
1857
|
}
|
|
1846
1858
|
const input = {
|
|
@@ -1865,10 +1877,10 @@ export class ChannelBase {
|
|
|
1865
1877
|
}
|
|
1866
1878
|
}
|
|
1867
1879
|
if (!job) {
|
|
1868
|
-
await this.
|
|
1880
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Too many loops for this chat. Cancel an existing loop before adding another.`);
|
|
1869
1881
|
return true;
|
|
1870
1882
|
}
|
|
1871
|
-
await this.
|
|
1883
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Loop ${job.id}: ${job.cron}`);
|
|
1872
1884
|
return true;
|
|
1873
1885
|
}
|
|
1874
1886
|
async createLoopFromTool(sessionId, input) {
|
|
@@ -1975,23 +1987,23 @@ export class ChannelBase {
|
|
|
1975
1987
|
return true;
|
|
1976
1988
|
const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
|
|
1977
1989
|
if (jobs.length === 0) {
|
|
1978
|
-
await this.
|
|
1990
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No loops.');
|
|
1979
1991
|
return true;
|
|
1980
1992
|
}
|
|
1981
|
-
await this.
|
|
1993
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, jobs.map((job) => this.formatLoopListLine(job)).join('\n'));
|
|
1982
1994
|
return true;
|
|
1983
1995
|
}
|
|
1984
1996
|
async handleLoopInspect(envelope, id) {
|
|
1985
1997
|
if (!this.loopController)
|
|
1986
1998
|
return true;
|
|
1987
1999
|
if (!id) {
|
|
1988
|
-
await this.
|
|
2000
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Usage: /loop inspect <id>');
|
|
1989
2001
|
return true;
|
|
1990
2002
|
}
|
|
1991
2003
|
const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
|
|
1992
2004
|
const job = jobs.find((candidate) => candidate.id === id);
|
|
1993
2005
|
if (!job) {
|
|
1994
|
-
await this.
|
|
2006
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `No loop ${id}.`);
|
|
1995
2007
|
return true;
|
|
1996
2008
|
}
|
|
1997
2009
|
const lines = [
|
|
@@ -2013,7 +2025,7 @@ export class ChannelBase {
|
|
|
2013
2025
|
lines.push(`Last result: ${job.lastResultPreview}`);
|
|
2014
2026
|
}
|
|
2015
2027
|
lines.push(`Prompt: ${job.prompt}`);
|
|
2016
|
-
await this.
|
|
2028
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, lines.join('\n'));
|
|
2017
2029
|
return true;
|
|
2018
2030
|
}
|
|
2019
2031
|
formatLoopListLine(job) {
|
|
@@ -2046,17 +2058,17 @@ export class ChannelBase {
|
|
|
2046
2058
|
if (!this.loopController)
|
|
2047
2059
|
return true;
|
|
2048
2060
|
if (!id) {
|
|
2049
|
-
await this.
|
|
2061
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Usage: /loop cancel <id>');
|
|
2050
2062
|
return true;
|
|
2051
2063
|
}
|
|
2052
2064
|
const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
|
|
2053
2065
|
const match = jobs.find((job) => job.id === id);
|
|
2054
2066
|
if (!match) {
|
|
2055
|
-
await this.
|
|
2067
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `No loop ${id}.`);
|
|
2056
2068
|
return true;
|
|
2057
2069
|
}
|
|
2058
2070
|
const disabled = await this.loopController.disable(id);
|
|
2059
|
-
await this.
|
|
2071
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, disabled ? `Cancelled loop ${id}.` : `Failed to cancel loop ${id}.`);
|
|
2060
2072
|
return true;
|
|
2061
2073
|
}
|
|
2062
2074
|
loopTargetFromEnvelope(envelope) {
|
|
@@ -2191,7 +2203,7 @@ export class ChannelBase {
|
|
|
2191
2203
|
}
|
|
2192
2204
|
async getChannelMemory(envelope) {
|
|
2193
2205
|
if (!this.channelMemory) {
|
|
2194
|
-
await this.
|
|
2206
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Channel memory is not configured for this channel.');
|
|
2195
2207
|
return undefined;
|
|
2196
2208
|
}
|
|
2197
2209
|
return this.channelMemory;
|
|
@@ -2211,7 +2223,7 @@ export class ChannelBase {
|
|
|
2211
2223
|
}
|
|
2212
2224
|
async handleChannelMemoryIntent(envelope, intent, options = {}) {
|
|
2213
2225
|
if (intent.kind === 'no_match') {
|
|
2214
|
-
await this.
|
|
2226
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No matching channel memory entry.');
|
|
2215
2227
|
return;
|
|
2216
2228
|
}
|
|
2217
2229
|
if (intent.kind === 'ambiguous') {
|
|
@@ -2224,11 +2236,11 @@ export class ChannelBase {
|
|
|
2224
2236
|
}
|
|
2225
2237
|
catch (error) {
|
|
2226
2238
|
this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
|
|
2227
|
-
await this.
|
|
2239
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2228
2240
|
return;
|
|
2229
2241
|
}
|
|
2230
2242
|
const selected = this.entriesForChannelMemoryIds(entries, intent.ids);
|
|
2231
|
-
await this.
|
|
2243
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, [
|
|
2232
2244
|
'Multiple channel memory entries match:',
|
|
2233
2245
|
...this.renderChannelMemoryCandidates(selected),
|
|
2234
2246
|
].join('\n'));
|
|
@@ -2244,11 +2256,11 @@ export class ChannelBase {
|
|
|
2244
2256
|
}
|
|
2245
2257
|
catch (error) {
|
|
2246
2258
|
this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
|
|
2247
|
-
await this.
|
|
2259
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2248
2260
|
return;
|
|
2249
2261
|
}
|
|
2250
2262
|
const selected = this.entriesForChannelMemoryIds(entries, intent.ids);
|
|
2251
|
-
await this.
|
|
2263
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, [
|
|
2252
2264
|
'Channel memory (page 1/1):',
|
|
2253
2265
|
...this.renderChannelMemoryCandidates(selected),
|
|
2254
2266
|
].join('\n'));
|
|
@@ -2263,7 +2275,7 @@ export class ChannelBase {
|
|
|
2263
2275
|
? this.takePendingChannelMemoryMutation(envelope, 'update')
|
|
2264
2276
|
: this.takePendingChannelMemoryMutation(envelope, 'remove');
|
|
2265
2277
|
if (!pending) {
|
|
2266
|
-
await this.
|
|
2278
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, intent.kind === 'update_confirm'
|
|
2267
2279
|
? 'No pending channel memory update. Start a new update request first.'
|
|
2268
2280
|
: 'No pending channel memory removal. Start a new removal request first.');
|
|
2269
2281
|
return;
|
|
@@ -2291,17 +2303,17 @@ export class ChannelBase {
|
|
|
2291
2303
|
catch (error) {
|
|
2292
2304
|
const message = this.channelMemoryErrorMessage(error);
|
|
2293
2305
|
this.logChannelMemoryError(isUpdate ? 'update' : 'remove', envelope, message);
|
|
2294
|
-
await this.
|
|
2306
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, message === 'Channel memory entry changed'
|
|
2295
2307
|
? 'That channel memory entry changed since it was selected. View channel memory and start the operation again.'
|
|
2296
2308
|
: `Failed to ${isUpdate ? 'update' : 'remove'} channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2297
2309
|
return;
|
|
2298
2310
|
}
|
|
2299
2311
|
if (!changed) {
|
|
2300
|
-
await this.
|
|
2312
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `No channel memory entry ${pending.id}.`);
|
|
2301
2313
|
return;
|
|
2302
2314
|
}
|
|
2303
2315
|
this.invalidateUnattendedMemory(envelope);
|
|
2304
|
-
await this.
|
|
2316
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Channel memory ${pending.id} ${isUpdate ? 'updated' : 'removed'}.`);
|
|
2305
2317
|
return;
|
|
2306
2318
|
}
|
|
2307
2319
|
if (intent.kind === 'natural_update') {
|
|
@@ -2342,7 +2354,7 @@ export class ChannelBase {
|
|
|
2342
2354
|
catch (error) {
|
|
2343
2355
|
const message = this.channelMemoryErrorMessage(error);
|
|
2344
2356
|
this.logChannelMemoryError('save', envelope, message);
|
|
2345
|
-
await this.
|
|
2357
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to save channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2346
2358
|
return;
|
|
2347
2359
|
}
|
|
2348
2360
|
if (result.changed) {
|
|
@@ -2357,17 +2369,17 @@ export class ChannelBase {
|
|
|
2357
2369
|
}
|
|
2358
2370
|
if (result.added.length > 0) {
|
|
2359
2371
|
const ids = result.added.map((entry) => entry.id);
|
|
2360
|
-
await this.
|
|
2372
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, result.duplicateIds.length > 0
|
|
2361
2373
|
? `Channel memory saved: ${ids.join(', ')}. Skipped duplicates: ${result.duplicateIds.join(', ')}.`
|
|
2362
2374
|
: ids.length === 1
|
|
2363
2375
|
? `Channel memory ${ids[0]} saved.`
|
|
2364
2376
|
: `Channel memory saved: ${ids.join(', ')}.`);
|
|
2365
2377
|
}
|
|
2366
2378
|
else if (result.duplicateIds.length > 0) {
|
|
2367
|
-
await this.
|
|
2379
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Channel memory already contains ${result.duplicateIds.join(', ')}.`);
|
|
2368
2380
|
}
|
|
2369
2381
|
else {
|
|
2370
|
-
await this.
|
|
2382
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Channel memory updated.');
|
|
2371
2383
|
}
|
|
2372
2384
|
return;
|
|
2373
2385
|
}
|
|
@@ -2379,30 +2391,30 @@ export class ChannelBase {
|
|
|
2379
2391
|
catch (error) {
|
|
2380
2392
|
const message = this.channelMemoryErrorMessage(error);
|
|
2381
2393
|
this.logChannelMemoryError('read', envelope, message);
|
|
2382
|
-
await this.
|
|
2394
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2383
2395
|
return;
|
|
2384
2396
|
}
|
|
2385
2397
|
if (intent.kind === 'inspect') {
|
|
2386
2398
|
const entry = entries.find((candidate) => candidate.id === intent.id);
|
|
2387
|
-
await this.
|
|
2399
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, entry
|
|
2388
2400
|
? `Channel memory ${entry.id}:\n${sanitizePromptText(entry.text).trim()}`
|
|
2389
2401
|
: `No channel memory entry ${intent.id}.`);
|
|
2390
2402
|
return;
|
|
2391
2403
|
}
|
|
2392
2404
|
const totalPages = Math.max(1, Math.ceil(entries.length / CHANNEL_MEMORY_PAGE_SIZE));
|
|
2393
2405
|
if (intent.page > totalPages) {
|
|
2394
|
-
await this.
|
|
2406
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Channel memory page ${intent.page} does not exist.`);
|
|
2395
2407
|
return;
|
|
2396
2408
|
}
|
|
2397
2409
|
if (entries.length === 0) {
|
|
2398
|
-
await this.
|
|
2410
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No channel memory saved.');
|
|
2399
2411
|
return;
|
|
2400
2412
|
}
|
|
2401
2413
|
const pageStart = (intent.page - 1) * CHANNEL_MEMORY_PAGE_SIZE;
|
|
2402
2414
|
const lines = entries
|
|
2403
2415
|
.slice(pageStart, pageStart + CHANNEL_MEMORY_PAGE_SIZE)
|
|
2404
2416
|
.map((entry) => this.renderChannelMemoryCandidate(entry));
|
|
2405
|
-
await this.
|
|
2417
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, [`Channel memory (page ${intent.page}/${totalPages}):`, ...lines].join('\n'));
|
|
2406
2418
|
return;
|
|
2407
2419
|
}
|
|
2408
2420
|
if (intent.kind === 'update' || intent.kind === 'remove') {
|
|
@@ -2420,21 +2432,21 @@ export class ChannelBase {
|
|
|
2420
2432
|
catch (error) {
|
|
2421
2433
|
const message = this.channelMemoryErrorMessage(error);
|
|
2422
2434
|
this.logChannelMemoryError(isUpdate ? 'update' : 'remove', envelope, message);
|
|
2423
|
-
await this.
|
|
2435
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to ${isUpdate ? 'update' : 'remove'} channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2424
2436
|
return;
|
|
2425
2437
|
}
|
|
2426
2438
|
if (!changed) {
|
|
2427
|
-
await this.
|
|
2439
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `No channel memory entry ${id}.`);
|
|
2428
2440
|
return;
|
|
2429
2441
|
}
|
|
2430
2442
|
this.invalidateUnattendedMemory(envelope);
|
|
2431
|
-
await this.
|
|
2443
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Channel memory ${id} ${isUpdate ? 'updated' : 'removed'}.`);
|
|
2432
2444
|
return;
|
|
2433
2445
|
}
|
|
2434
2446
|
if (intent.kind === 'clear_confirm') {
|
|
2435
2447
|
const pending = this.takePendingChannelMemoryMutation(envelope, 'clear');
|
|
2436
2448
|
if (!pending) {
|
|
2437
|
-
await this.
|
|
2449
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'No pending clear request. Say "清空记忆" first.');
|
|
2438
2450
|
return;
|
|
2439
2451
|
}
|
|
2440
2452
|
let result;
|
|
@@ -2444,13 +2456,13 @@ export class ChannelBase {
|
|
|
2444
2456
|
catch (error) {
|
|
2445
2457
|
const message = this.channelMemoryErrorMessage(error);
|
|
2446
2458
|
this.logChannelMemoryError('clear', envelope, message);
|
|
2447
|
-
await this.
|
|
2459
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Failed to clear channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2448
2460
|
return;
|
|
2449
2461
|
}
|
|
2450
2462
|
if (result.changed) {
|
|
2451
2463
|
this.invalidateUnattendedMemory(envelope);
|
|
2452
2464
|
}
|
|
2453
|
-
await this.
|
|
2465
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, result.changed ? 'Channel memory cleared.' : 'No channel memory saved.');
|
|
2454
2466
|
return;
|
|
2455
2467
|
}
|
|
2456
2468
|
const unhandled = intent;
|
|
@@ -2482,7 +2494,7 @@ export class ChannelBase {
|
|
|
2482
2494
|
const key = this.channelMemoryPendingKey(envelope);
|
|
2483
2495
|
this.pendingChannelMemoryMutationDeliveries.set(key, mutation);
|
|
2484
2496
|
try {
|
|
2485
|
-
await this.
|
|
2497
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, message);
|
|
2486
2498
|
}
|
|
2487
2499
|
catch (error) {
|
|
2488
2500
|
if (this.pendingChannelMemoryMutationDeliveries.get(key) === mutation) {
|
|
@@ -2655,15 +2667,18 @@ export class ChannelBase {
|
|
|
2655
2667
|
* Whether the resolved session is SHARED across senders. `single` collapses
|
|
2656
2668
|
* the whole channel to one `__single__` session for EVERY sender — group OR
|
|
2657
2669
|
* 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
|
-
*
|
|
2670
|
+
* shared only in a group (a DM maps to the lone caller's own chat).
|
|
2671
|
+
* `chat_thread` is always shared: it scopes by chat+thread, and a thread
|
|
2672
|
+
* (issue/PR discussion) can carry multiple participants even outside a
|
|
2673
|
+
* group. `user` is per-sender, never shared. Drives both the
|
|
2674
|
+
* destructive-/clear confirm gate and the host-shell (`!`) gate.
|
|
2661
2675
|
*/
|
|
2662
2676
|
isSharedSession(envelope) {
|
|
2663
2677
|
return this.isSharedSessionTarget(envelope);
|
|
2664
2678
|
}
|
|
2665
2679
|
isSharedSessionTarget(target) {
|
|
2666
2680
|
return (this.config.sessionScope === 'single' ||
|
|
2681
|
+
this.config.sessionScope === 'chat_thread' ||
|
|
2667
2682
|
(target.isGroup === true && this.config.sessionScope === 'thread'));
|
|
2668
2683
|
}
|
|
2669
2684
|
/**
|
|
@@ -2962,7 +2977,7 @@ export class ChannelBase {
|
|
|
2962
2977
|
if (!result.allowed) {
|
|
2963
2978
|
if (result.pairingCode !== undefined) {
|
|
2964
2979
|
this.logPreflightRejected('sender_pairing_required');
|
|
2965
|
-
return this.onPairingRequired(envelope.chatId, result.pairingCode)
|
|
2980
|
+
return this.onPairingRequired(envelope.chatId, result.pairingCode, envelope.threadId)
|
|
2966
2981
|
.then(() => false)
|
|
2967
2982
|
.catch((err) => {
|
|
2968
2983
|
process.stderr.write(`[Channel:${this.name}] pairing notification failed: ${sanitizeLogText(err instanceof Error ? err.message : String(err), 200)}\n`);
|
|
@@ -3103,13 +3118,13 @@ export class ChannelBase {
|
|
|
3103
3118
|
process.stderr.write(`[${this.name}] blocked ! shell command from ${who} (sender ${envelope.senderId}) in chat ${sanitizeLogText(envelope.chatId, 64)}\n`);
|
|
3104
3119
|
}
|
|
3105
3120
|
if (envelope.isGroup) {
|
|
3106
|
-
await this.
|
|
3121
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Shell commands (`!`) are disabled in group chats.');
|
|
3107
3122
|
return;
|
|
3108
3123
|
}
|
|
3109
3124
|
// A single-scope DM collapses every DM to one channel-wide session, so it
|
|
3110
3125
|
// is multi-operator too despite not being a group.
|
|
3111
3126
|
if (this.isSharedSession(envelope)) {
|
|
3112
|
-
await this.
|
|
3127
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, 'Shell commands (`!`) are disabled in shared sessions.');
|
|
3113
3128
|
return;
|
|
3114
3129
|
}
|
|
3115
3130
|
}
|
|
@@ -3131,10 +3146,10 @@ export class ChannelBase {
|
|
|
3131
3146
|
const exitLine = result.exitCode !== null && result.exitCode !== 0
|
|
3132
3147
|
? `\nExit code: ${result.exitCode}`
|
|
3133
3148
|
: '';
|
|
3134
|
-
await this.
|
|
3149
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `$ ${cmd}\n${output}${exitLine}`);
|
|
3135
3150
|
}
|
|
3136
3151
|
catch (error) {
|
|
3137
|
-
await this.
|
|
3152
|
+
await this.sendThreadMessage(envelope.chatId, envelope.threadId, `Shell command failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3138
3153
|
}
|
|
3139
3154
|
return;
|
|
3140
3155
|
}
|
|
@@ -3212,6 +3227,9 @@ export class ChannelBase {
|
|
|
3212
3227
|
promptText = promptText + '\n\n' + filePaths.join('\n');
|
|
3213
3228
|
}
|
|
3214
3229
|
}
|
|
3230
|
+
if (envelope.metadata) {
|
|
3231
|
+
promptText = promptText + '\n\n' + sanitizePromptText(envelope.metadata);
|
|
3232
|
+
}
|
|
3215
3233
|
// Resolve dispatch mode: per-group override → channel config → default
|
|
3216
3234
|
const groupCfg = envelope.isGroup
|
|
3217
3235
|
? this.config.groups[envelope.chatId] || this.config.groups['*']
|
|
@@ -3628,6 +3646,7 @@ export class ChannelBase {
|
|
|
3628
3646
|
// Clear attachments/references — already resolved in original text
|
|
3629
3647
|
referencedText: undefined,
|
|
3630
3648
|
attachments: undefined,
|
|
3649
|
+
metadata: undefined,
|
|
3631
3650
|
imageBase64: undefined,
|
|
3632
3651
|
imageMimeType: undefined,
|
|
3633
3652
|
};
|
|
@@ -3643,12 +3662,12 @@ export class ChannelBase {
|
|
|
3643
3662
|
this.sessionQueues.set(sessionId, current.catch(() => { }));
|
|
3644
3663
|
await current;
|
|
3645
3664
|
}
|
|
3646
|
-
async onPairingRequired(chatId, code) {
|
|
3665
|
+
async onPairingRequired(chatId, code, threadId) {
|
|
3647
3666
|
if (code) {
|
|
3648
|
-
await this.
|
|
3667
|
+
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
3668
|
}
|
|
3650
3669
|
else {
|
|
3651
|
-
await this.
|
|
3670
|
+
await this.sendThreadMessage(chatId, threadId, 'Too many pending pairing requests. Please try again later.');
|
|
3652
3671
|
}
|
|
3653
3672
|
}
|
|
3654
3673
|
}
|