@qwen-code/channel-base 0.19.10 → 0.19.11-nightly.20260718.767a32484
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 +2 -2
- package/dist/ChannelAgentBridge.d.ts +7 -0
- package/dist/ChannelBase.d.ts +10 -3
- package/dist/ChannelBase.js +381 -69
- package/dist/ChannelBase.js.map +1 -1
- package/dist/DaemonChannelBridge.d.ts +5 -7
- package/dist/DaemonChannelBridge.js +15 -0
- package/dist/DaemonChannelBridge.js.map +1 -1
- package/dist/SessionRouter.js +4 -4
- package/dist/SessionRouter.js.map +1 -1
- package/dist/channel-memory-intent.d.ts +16 -1
- package/dist/channel-memory-intent.js +72 -2
- package/dist/channel-memory-intent.js.map +1 -1
- package/dist/types.d.ts +58 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,9 +72,9 @@ Migration note for existing TypeScript plugins: if your adapter constructor or f
|
|
|
72
72
|
Channel adapters can run in two host modes:
|
|
73
73
|
|
|
74
74
|
- `qwen channel start [name]` is the standalone service. It uses `AcpBridge` over a `qwen-code --acp` child process and remains the default channel command.
|
|
75
|
-
- `qwen serve --channel <name>` and `qwen serve --channel all` are experimental daemon-managed modes. `qwen serve` starts one out-of-process
|
|
75
|
+
- `qwen serve --channel <name>` and `qwen serve --channel all` are experimental daemon-managed modes. Named channels are grouped by owning workspace and `qwen serve` starts one out-of-process worker per owning runtime. Each worker connects back to the daemon through the SDK, and adapters receive a `DaemonChannelBridge`-backed `ChannelAgentBridge` facade. `--channel all` stays primary-only.
|
|
76
76
|
|
|
77
|
-
In daemon-managed mode,
|
|
77
|
+
In daemon-managed mode, every named channel's `cwd` must resolve to exactly one registered, trusted workspace. Its worker receives that runtime's cwd and environment overlay; an ambiguous or untrusted selection fails instead of using primary. The optional `shellCommand` method is exposed to adapters only when the daemon advertises the `session_shell_command` capability.
|
|
78
78
|
|
|
79
79
|
## Architecture
|
|
80
80
|
|
|
@@ -62,6 +62,13 @@ export interface BridgeSessionInfo {
|
|
|
62
62
|
}
|
|
63
63
|
export interface ChannelAgentBridgeSessionOptions {
|
|
64
64
|
approvalMode?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Channel instance name (e.g. `feishu-main`) stamped as the daemon `sourceId`
|
|
67
|
+
* on **new** sessions — creation-time attribution paired with
|
|
68
|
+
* `sourceType: 'channel'`. Ignored by `loadSession`: loading an existing
|
|
69
|
+
* session never re-stamps its creation attribution.
|
|
70
|
+
*/
|
|
71
|
+
sourceId?: string;
|
|
65
72
|
}
|
|
66
73
|
export interface ChannelAgentBridge {
|
|
67
74
|
readonly availableCommands: AvailableCommand[];
|
package/dist/ChannelBase.d.ts
CHANGED
|
@@ -70,7 +70,8 @@ export declare abstract class ChannelBase {
|
|
|
70
70
|
* so a cleared session can't be resurrected by an already-queued prompt.
|
|
71
71
|
*/
|
|
72
72
|
private sessionGenerations;
|
|
73
|
-
private
|
|
73
|
+
private pendingChannelMemoryMutations;
|
|
74
|
+
private pendingChannelMemoryMutationDeliveries;
|
|
74
75
|
/** Per-session active prompt tracking for dispatch modes. */
|
|
75
76
|
private activePrompts;
|
|
76
77
|
/** Per-session message buffer for collect mode. */
|
|
@@ -111,6 +112,7 @@ export declare abstract class ChannelBase {
|
|
|
111
112
|
private lifecycleBase;
|
|
112
113
|
supportsProactiveSend(): boolean;
|
|
113
114
|
protected supportsProactiveTarget(target: SessionTarget): boolean;
|
|
115
|
+
protected supportsProactiveWebhookTarget(target: SessionTarget): boolean;
|
|
114
116
|
protected pushProactive(target: SessionTarget, text: string): Promise<void>;
|
|
115
117
|
private prependUnattendedSessionContext;
|
|
116
118
|
private drainCollectBufferForCurrentPrompt;
|
|
@@ -212,10 +214,15 @@ export declare abstract class ChannelBase {
|
|
|
212
214
|
private invalidateSessionContext;
|
|
213
215
|
private dropQueuedTurnIfStale;
|
|
214
216
|
private getChannelMemory;
|
|
217
|
+
private entriesForChannelMemoryIds;
|
|
218
|
+
private renderChannelMemoryCandidate;
|
|
219
|
+
private renderChannelMemoryCandidates;
|
|
215
220
|
private handleChannelMemoryIntent;
|
|
216
221
|
private shouldClassifyChannelMemoryIntent;
|
|
217
|
-
private
|
|
218
|
-
private
|
|
222
|
+
private channelMemoryPendingKey;
|
|
223
|
+
private deliverPendingChannelMemoryMutation;
|
|
224
|
+
private takePendingChannelMemoryMutation;
|
|
225
|
+
private deletePendingChannelMemoryMutation;
|
|
219
226
|
private classifyChannelMemoryIntent;
|
|
220
227
|
private channelMemoryErrorMessage;
|
|
221
228
|
private channelMemoryUserErrorMessage;
|
package/dist/ChannelBase.js
CHANGED
|
@@ -26,8 +26,10 @@ const GROUP_HISTORY_ENTRY_TEXT_LIMIT = 1000;
|
|
|
26
26
|
const GROUP_HISTORY_ENTRY_METADATA_LIMIT = 256;
|
|
27
27
|
const LOOP_CANCEL_GRACE_MS = 5000;
|
|
28
28
|
const CHANNEL_MEMORY_PROMPT_CODE_POINT_LIMIT = 12_000;
|
|
29
|
+
const CHANNEL_MEMORY_PAGE_SIZE = 20;
|
|
30
|
+
const CHANNEL_MEMORY_PREVIEW_CODE_POINT_LIMIT = 160;
|
|
29
31
|
const CHANNEL_MEMORY_CLASSIFIER_MIN_CONFIDENCE = 0.7;
|
|
30
|
-
const CHANNEL_MEMORY_CLASSIFIER_TRIGGER_RE = /(
|
|
32
|
+
const CHANNEL_MEMORY_CLASSIFIER_TRIGGER_RE = /(?:记住|记得|记一下|记忆|忘掉|忘记|清空|清除|删除|删掉|改成|更新|刚才那条|保存|(?:只|仅)(?:看|列出)[\p{Script=Han}\s]{0,12}(?:偏好|习惯)|\b(?:remember|memory|forget|delete|remove|update|change)\b)/iu;
|
|
31
33
|
/** Sentinel message for the loop-prompt timeout rejection; matched by identity below. */
|
|
32
34
|
const LOOP_TIMED_OUT_MESSAGE = 'loop timed out';
|
|
33
35
|
const DEBUG_PAYLOAD_ENV = 'QWEN_CHANNEL_DEBUG_PAYLOAD';
|
|
@@ -108,7 +110,8 @@ export class ChannelBase {
|
|
|
108
110
|
* so a cleared session can't be resurrected by an already-queued prompt.
|
|
109
111
|
*/
|
|
110
112
|
sessionGenerations = new Map();
|
|
111
|
-
|
|
113
|
+
pendingChannelMemoryMutations = new Map();
|
|
114
|
+
pendingChannelMemoryMutationDeliveries = new Map();
|
|
112
115
|
/** Per-session active prompt tracking for dispatch modes. */
|
|
113
116
|
activePrompts = new Map();
|
|
114
117
|
/** Per-session message buffer for collect mode. */
|
|
@@ -370,6 +373,9 @@ export class ChannelBase {
|
|
|
370
373
|
supportsProactiveTarget(target) {
|
|
371
374
|
return target.threadId === undefined;
|
|
372
375
|
}
|
|
376
|
+
supportsProactiveWebhookTarget(target) {
|
|
377
|
+
return this.supportsProactiveTarget(target);
|
|
378
|
+
}
|
|
373
379
|
async pushProactive(target, text) {
|
|
374
380
|
if (target.threadId) {
|
|
375
381
|
throw new Error('Channel does not support proactive loop messages for threaded targets.');
|
|
@@ -675,7 +681,7 @@ export class ChannelBase {
|
|
|
675
681
|
throw new Error(`Unknown webhook source "${task.source}".`);
|
|
676
682
|
}
|
|
677
683
|
const target = resolveChannelWebhookTarget(this.name, this.config.webhooks, task.source, task.targetRef);
|
|
678
|
-
if (!this.
|
|
684
|
+
if (!this.supportsProactiveWebhookTarget(target)) {
|
|
679
685
|
throw new Error('Channel does not support proactive webhook messages for this chat target.');
|
|
680
686
|
}
|
|
681
687
|
return target;
|
|
@@ -1407,30 +1413,6 @@ export class ChannelBase {
|
|
|
1407
1413
|
this.registerCommand('approve', (envelope, args) => this.handlePermissionResponseCommand(envelope, args, 'approve'));
|
|
1408
1414
|
this.registerCommand('approve-always', (envelope, args) => this.handlePermissionResponseCommand(envelope, args, 'approve-always'));
|
|
1409
1415
|
this.registerCommand('deny', (envelope, args) => this.handlePermissionResponseCommand(envelope, args, 'deny'));
|
|
1410
|
-
this.registerCommand('remember-channel', async (envelope, args) => {
|
|
1411
|
-
const text = args.trim();
|
|
1412
|
-
if (text === '') {
|
|
1413
|
-
await this.sendMessage(envelope.chatId, 'Usage: /remember-channel <text>');
|
|
1414
|
-
return true;
|
|
1415
|
-
}
|
|
1416
|
-
await this.handleChannelMemoryIntent(envelope, {
|
|
1417
|
-
kind: 'remember',
|
|
1418
|
-
text,
|
|
1419
|
-
});
|
|
1420
|
-
return true;
|
|
1421
|
-
});
|
|
1422
|
-
this.registerCommand('channel-memory', async (envelope) => {
|
|
1423
|
-
await this.handleChannelMemoryIntent(envelope, { kind: 'list' });
|
|
1424
|
-
return true;
|
|
1425
|
-
});
|
|
1426
|
-
this.registerCommand('forget-channel', async (envelope, args) => {
|
|
1427
|
-
if (args.toLowerCase() !== 'confirm') {
|
|
1428
|
-
await this.sendMessage(envelope.chatId, 'This clears channel memory for this chat. Re-send with "confirm" (e.g. /forget-channel confirm) to proceed.');
|
|
1429
|
-
return true;
|
|
1430
|
-
}
|
|
1431
|
-
await this.handleChannelMemoryIntent(envelope, { kind: 'clear_confirm' }, { skipPendingClear: true });
|
|
1432
|
-
return true;
|
|
1433
|
-
});
|
|
1434
1416
|
// Read-only: report the current (possibly group-shared) session and workspace.
|
|
1435
1417
|
// For a shared session, gate it to authorized senders like /clear — /who
|
|
1436
1418
|
// leaks the workspace basename, so non-members shouldn't see it either.
|
|
@@ -1938,10 +1920,138 @@ export class ChannelBase {
|
|
|
1938
1920
|
}
|
|
1939
1921
|
return this.channelMemory;
|
|
1940
1922
|
}
|
|
1941
|
-
|
|
1923
|
+
entriesForChannelMemoryIds(entries, ids) {
|
|
1924
|
+
const selected = new Set(ids);
|
|
1925
|
+
return entries.filter((entry) => selected.has(entry.id));
|
|
1926
|
+
}
|
|
1927
|
+
renderChannelMemoryCandidate(entry) {
|
|
1928
|
+
const preview = truncateCodePoints(sanitizePromptText(entry.text)
|
|
1929
|
+
.replace(/[\r\n]+/gu, ' ')
|
|
1930
|
+
.trim(), CHANNEL_MEMORY_PREVIEW_CODE_POINT_LIMIT);
|
|
1931
|
+
return `${entry.id} ${preview}`;
|
|
1932
|
+
}
|
|
1933
|
+
renderChannelMemoryCandidates(entries) {
|
|
1934
|
+
return entries.map((entry) => this.renderChannelMemoryCandidate(entry));
|
|
1935
|
+
}
|
|
1936
|
+
async handleChannelMemoryIntent(envelope, intent) {
|
|
1937
|
+
if (intent.kind === 'no_match') {
|
|
1938
|
+
await this.sendMessage(envelope.chatId, 'No matching channel memory entry.');
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (intent.kind === 'ambiguous') {
|
|
1942
|
+
const channelMemory = await this.getChannelMemory(envelope);
|
|
1943
|
+
if (!channelMemory)
|
|
1944
|
+
return;
|
|
1945
|
+
let entries;
|
|
1946
|
+
try {
|
|
1947
|
+
entries = await channelMemory.listChannelMemoryEntries(this.channelMemoryTarget(envelope));
|
|
1948
|
+
}
|
|
1949
|
+
catch (error) {
|
|
1950
|
+
this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
|
|
1951
|
+
await this.sendMessage(envelope.chatId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
const selected = this.entriesForChannelMemoryIds(entries, intent.ids);
|
|
1955
|
+
await this.sendMessage(envelope.chatId, [
|
|
1956
|
+
'Multiple channel memory entries match:',
|
|
1957
|
+
...this.renderChannelMemoryCandidates(selected),
|
|
1958
|
+
].join('\n'));
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
if (intent.kind === 'list_matches') {
|
|
1962
|
+
const channelMemory = await this.getChannelMemory(envelope);
|
|
1963
|
+
if (!channelMemory)
|
|
1964
|
+
return;
|
|
1965
|
+
let entries;
|
|
1966
|
+
try {
|
|
1967
|
+
entries = await channelMemory.listChannelMemoryEntries(this.channelMemoryTarget(envelope));
|
|
1968
|
+
}
|
|
1969
|
+
catch (error) {
|
|
1970
|
+
this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
|
|
1971
|
+
await this.sendMessage(envelope.chatId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
const selected = this.entriesForChannelMemoryIds(entries, intent.ids);
|
|
1975
|
+
await this.sendMessage(envelope.chatId, [
|
|
1976
|
+
'Channel memory (page 1/1):',
|
|
1977
|
+
...this.renderChannelMemoryCandidates(selected),
|
|
1978
|
+
].join('\n'));
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1942
1981
|
if (intent.kind === 'clear_request') {
|
|
1943
|
-
this.
|
|
1944
|
-
|
|
1982
|
+
await this.deliverPendingChannelMemoryMutation(envelope, { kind: 'clear' }, 'This clears channel memory for this chat. Say "确认清空记忆" or "confirm clear memory" to proceed.');
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
if (intent.kind === 'update_confirm' || intent.kind === 'remove_confirm') {
|
|
1986
|
+
const pending = intent.kind === 'update_confirm'
|
|
1987
|
+
? this.takePendingChannelMemoryMutation(envelope, 'update')
|
|
1988
|
+
: this.takePendingChannelMemoryMutation(envelope, 'remove');
|
|
1989
|
+
if (!pending) {
|
|
1990
|
+
await this.sendMessage(envelope.chatId, intent.kind === 'update_confirm'
|
|
1991
|
+
? 'No pending channel memory update. Start a new update request first.'
|
|
1992
|
+
: 'No pending channel memory removal. Start a new removal request first.');
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const channelMemory = await this.getChannelMemory(envelope);
|
|
1996
|
+
if (!channelMemory)
|
|
1997
|
+
return;
|
|
1998
|
+
const isUpdate = pending.kind === 'update';
|
|
1999
|
+
let changed;
|
|
2000
|
+
try {
|
|
2001
|
+
if (isUpdate) {
|
|
2002
|
+
({ changed } = await channelMemory.updateChannelMemoryEntry(this.channelMemoryTarget(envelope), {
|
|
2003
|
+
id: pending.id,
|
|
2004
|
+
text: pending.proposedText,
|
|
2005
|
+
expectedText: pending.expectedText,
|
|
2006
|
+
}));
|
|
2007
|
+
}
|
|
2008
|
+
else {
|
|
2009
|
+
({ changed } = await channelMemory.removeChannelMemoryEntries(this.channelMemoryTarget(envelope), {
|
|
2010
|
+
ids: [pending.id],
|
|
2011
|
+
expectedTextById: { [pending.id]: pending.expectedText },
|
|
2012
|
+
}));
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
catch (error) {
|
|
2016
|
+
const message = this.channelMemoryErrorMessage(error);
|
|
2017
|
+
this.logChannelMemoryError(isUpdate ? 'update' : 'remove', envelope, message);
|
|
2018
|
+
await this.sendMessage(envelope.chatId, message === 'Channel memory entry changed'
|
|
2019
|
+
? 'That channel memory entry changed since it was selected. View channel memory and start the operation again.'
|
|
2020
|
+
: `Failed to ${isUpdate ? 'update' : 'remove'} channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
if (!changed) {
|
|
2024
|
+
await this.sendMessage(envelope.chatId, `No channel memory entry ${pending.id}.`);
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
this.invalidateSessionContext(envelope);
|
|
2028
|
+
await this.sendMessage(envelope.chatId, `Channel memory ${pending.id} ${isUpdate ? 'updated' : 'removed'}.`);
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
if (intent.kind === 'natural_update') {
|
|
2032
|
+
await this.deliverPendingChannelMemoryMutation(envelope, {
|
|
2033
|
+
kind: 'update',
|
|
2034
|
+
id: intent.id,
|
|
2035
|
+
expectedText: intent.expectedText,
|
|
2036
|
+
proposedText: intent.text,
|
|
2037
|
+
}, [
|
|
2038
|
+
`Update channel memory ${intent.id}?`,
|
|
2039
|
+
`Before: ${sanitizePromptText(intent.expectedText).trim()}`,
|
|
2040
|
+
`After: ${sanitizePromptText(intent.text).trim()}`,
|
|
2041
|
+
'Say "确认更新记忆" or "confirm memory update" within 60 seconds.',
|
|
2042
|
+
].join('\n'));
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
if (intent.kind === 'natural_remove') {
|
|
2046
|
+
await this.deliverPendingChannelMemoryMutation(envelope, {
|
|
2047
|
+
kind: 'remove',
|
|
2048
|
+
id: intent.id,
|
|
2049
|
+
expectedText: intent.expectedText,
|
|
2050
|
+
}, [
|
|
2051
|
+
`Remove channel memory ${intent.id}?`,
|
|
2052
|
+
sanitizePromptText(intent.expectedText).trim(),
|
|
2053
|
+
'Say "确认删除记忆" or "confirm memory removal" within 60 seconds.',
|
|
2054
|
+
].join('\n'));
|
|
1945
2055
|
return;
|
|
1946
2056
|
}
|
|
1947
2057
|
const channelMemory = await this.getChannelMemory(envelope);
|
|
@@ -1949,8 +2059,9 @@ export class ChannelBase {
|
|
|
1949
2059
|
return;
|
|
1950
2060
|
}
|
|
1951
2061
|
if (intent.kind === 'remember') {
|
|
2062
|
+
let result;
|
|
1952
2063
|
try {
|
|
1953
|
-
await channelMemory.
|
|
2064
|
+
result = await channelMemory.addChannelMemoryEntries(this.channelMemoryTarget(envelope), intent.texts, envelope.senderId);
|
|
1954
2065
|
}
|
|
1955
2066
|
catch (error) {
|
|
1956
2067
|
const message = this.channelMemoryErrorMessage(error);
|
|
@@ -1958,14 +2069,29 @@ export class ChannelBase {
|
|
|
1958
2069
|
await this.sendMessage(envelope.chatId, `Failed to save channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
1959
2070
|
return;
|
|
1960
2071
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
2072
|
+
if (result.changed) {
|
|
2073
|
+
this.invalidateSessionContext(envelope);
|
|
2074
|
+
}
|
|
2075
|
+
if (result.added.length > 0) {
|
|
2076
|
+
const ids = result.added.map((entry) => entry.id);
|
|
2077
|
+
await this.sendMessage(envelope.chatId, result.duplicateIds.length > 0
|
|
2078
|
+
? `Channel memory saved: ${ids.join(', ')}. Skipped duplicates: ${result.duplicateIds.join(', ')}.`
|
|
2079
|
+
: ids.length === 1
|
|
2080
|
+
? `Channel memory ${ids[0]} saved.`
|
|
2081
|
+
: `Channel memory saved: ${ids.join(', ')}.`);
|
|
2082
|
+
}
|
|
2083
|
+
else if (result.duplicateIds.length > 0) {
|
|
2084
|
+
await this.sendMessage(envelope.chatId, `Channel memory already contains ${result.duplicateIds.join(', ')}.`);
|
|
2085
|
+
}
|
|
2086
|
+
else {
|
|
2087
|
+
await this.sendMessage(envelope.chatId, 'Channel memory updated.');
|
|
2088
|
+
}
|
|
1963
2089
|
return;
|
|
1964
2090
|
}
|
|
1965
|
-
if (intent.kind === 'list') {
|
|
1966
|
-
let
|
|
2091
|
+
if (intent.kind === 'list' || intent.kind === 'inspect') {
|
|
2092
|
+
let entries;
|
|
1967
2093
|
try {
|
|
1968
|
-
|
|
2094
|
+
entries = await channelMemory.listChannelMemoryEntries(this.channelMemoryTarget(envelope));
|
|
1969
2095
|
}
|
|
1970
2096
|
catch (error) {
|
|
1971
2097
|
const message = this.channelMemoryErrorMessage(error);
|
|
@@ -1973,19 +2099,61 @@ export class ChannelBase {
|
|
|
1973
2099
|
await this.sendMessage(envelope.chatId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
1974
2100
|
return;
|
|
1975
2101
|
}
|
|
1976
|
-
|
|
2102
|
+
if (intent.kind === 'inspect') {
|
|
2103
|
+
const entry = entries.find((candidate) => candidate.id === intent.id);
|
|
2104
|
+
await this.sendMessage(envelope.chatId, entry
|
|
2105
|
+
? `Channel memory ${entry.id}:\n${sanitizePromptText(entry.text).trim()}`
|
|
2106
|
+
: `No channel memory entry ${intent.id}.`);
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const totalPages = Math.max(1, Math.ceil(entries.length / CHANNEL_MEMORY_PAGE_SIZE));
|
|
2110
|
+
if (intent.page > totalPages) {
|
|
2111
|
+
await this.sendMessage(envelope.chatId, `Channel memory page ${intent.page} does not exist.`);
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
if (entries.length === 0) {
|
|
2115
|
+
await this.sendMessage(envelope.chatId, 'No channel memory saved.');
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
const pageStart = (intent.page - 1) * CHANNEL_MEMORY_PAGE_SIZE;
|
|
2119
|
+
const lines = entries
|
|
2120
|
+
.slice(pageStart, pageStart + CHANNEL_MEMORY_PAGE_SIZE)
|
|
2121
|
+
.map((entry) => this.renderChannelMemoryCandidate(entry));
|
|
2122
|
+
await this.sendMessage(envelope.chatId, [`Channel memory (page ${intent.page}/${totalPages}):`, ...lines].join('\n'));
|
|
1977
2123
|
return;
|
|
1978
2124
|
}
|
|
1979
|
-
if (intent.kind === '
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
if (
|
|
1985
|
-
await this.
|
|
1986
|
-
|
|
2125
|
+
if (intent.kind === 'update' || intent.kind === 'remove') {
|
|
2126
|
+
let changed;
|
|
2127
|
+
const isUpdate = intent.kind === 'update';
|
|
2128
|
+
const id = intent.id;
|
|
2129
|
+
try {
|
|
2130
|
+
if (isUpdate) {
|
|
2131
|
+
({ changed } = await channelMemory.updateChannelMemoryEntry(this.channelMemoryTarget(envelope), { id: intent.id, text: intent.text }));
|
|
2132
|
+
}
|
|
2133
|
+
else {
|
|
2134
|
+
({ changed } = await channelMemory.removeChannelMemoryEntries(this.channelMemoryTarget(envelope), { ids: [intent.id] }));
|
|
1987
2135
|
}
|
|
1988
2136
|
}
|
|
2137
|
+
catch (error) {
|
|
2138
|
+
const message = this.channelMemoryErrorMessage(error);
|
|
2139
|
+
this.logChannelMemoryError(isUpdate ? 'update' : 'remove', envelope, message);
|
|
2140
|
+
await this.sendMessage(envelope.chatId, `Failed to ${isUpdate ? 'update' : 'remove'} channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
if (!changed) {
|
|
2144
|
+
await this.sendMessage(envelope.chatId, `No channel memory entry ${id}.`);
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
this.invalidateSessionContext(envelope);
|
|
2148
|
+
await this.sendMessage(envelope.chatId, `Channel memory ${id} ${isUpdate ? 'updated' : 'removed'}.`);
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
if (intent.kind === 'clear_confirm') {
|
|
2152
|
+
const pending = this.takePendingChannelMemoryMutation(envelope, 'clear');
|
|
2153
|
+
if (!pending) {
|
|
2154
|
+
await this.sendMessage(envelope.chatId, 'No pending clear request. Say "清空记忆" first.');
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
1989
2157
|
let result;
|
|
1990
2158
|
try {
|
|
1991
2159
|
result = await channelMemory.clearChannelMemory(this.channelMemoryTarget(envelope));
|
|
@@ -1996,7 +2164,9 @@ export class ChannelBase {
|
|
|
1996
2164
|
await this.sendMessage(envelope.chatId, `Failed to clear channel memory: ${this.channelMemoryUserErrorMessage()}`);
|
|
1997
2165
|
return;
|
|
1998
2166
|
}
|
|
1999
|
-
|
|
2167
|
+
if (result.changed) {
|
|
2168
|
+
this.invalidateSessionContext(envelope);
|
|
2169
|
+
}
|
|
2000
2170
|
await this.sendMessage(envelope.chatId, result.changed ? 'Channel memory cleared.' : 'No channel memory saved.');
|
|
2001
2171
|
return;
|
|
2002
2172
|
}
|
|
@@ -2010,45 +2180,184 @@ export class ChannelBase {
|
|
|
2010
2180
|
!normalized.startsWith('/') &&
|
|
2011
2181
|
CHANNEL_MEMORY_CLASSIFIER_TRIGGER_RE.test(normalized));
|
|
2012
2182
|
}
|
|
2013
|
-
|
|
2014
|
-
return
|
|
2183
|
+
channelMemoryPendingKey(envelope) {
|
|
2184
|
+
return JSON.stringify([
|
|
2185
|
+
this.name,
|
|
2186
|
+
envelope.chatId,
|
|
2187
|
+
envelope.threadId ?? null,
|
|
2188
|
+
envelope.senderId ?? null,
|
|
2189
|
+
]);
|
|
2015
2190
|
}
|
|
2016
|
-
|
|
2191
|
+
async deliverPendingChannelMemoryMutation(envelope, mutation, message) {
|
|
2017
2192
|
const now = Date.now();
|
|
2018
|
-
for (const [pendingKey,
|
|
2019
|
-
if (expiresAt < now) {
|
|
2020
|
-
this.
|
|
2193
|
+
for (const [pendingKey, pending] of this.pendingChannelMemoryMutations) {
|
|
2194
|
+
if (pending.expiresAt < now) {
|
|
2195
|
+
this.pendingChannelMemoryMutations.delete(pendingKey);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
this.deletePendingChannelMemoryMutation(envelope);
|
|
2199
|
+
const key = this.channelMemoryPendingKey(envelope);
|
|
2200
|
+
this.pendingChannelMemoryMutationDeliveries.set(key, mutation);
|
|
2201
|
+
try {
|
|
2202
|
+
await this.sendMessage(envelope.chatId, message);
|
|
2203
|
+
}
|
|
2204
|
+
catch (error) {
|
|
2205
|
+
if (this.pendingChannelMemoryMutationDeliveries.get(key) === mutation) {
|
|
2206
|
+
this.pendingChannelMemoryMutationDeliveries.delete(key);
|
|
2021
2207
|
}
|
|
2208
|
+
throw error;
|
|
2022
2209
|
}
|
|
2023
|
-
this.
|
|
2210
|
+
if (this.pendingChannelMemoryMutationDeliveries.get(key) !== mutation) {
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
this.pendingChannelMemoryMutationDeliveries.delete(key);
|
|
2214
|
+
this.pendingChannelMemoryMutations.set(key, {
|
|
2215
|
+
...mutation,
|
|
2216
|
+
expiresAt: Date.now() + 60_000,
|
|
2217
|
+
});
|
|
2024
2218
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2219
|
+
takePendingChannelMemoryMutation(envelope, kind) {
|
|
2220
|
+
const key = this.channelMemoryPendingKey(envelope);
|
|
2221
|
+
const pending = this.pendingChannelMemoryMutations.get(key);
|
|
2222
|
+
if (!pending) {
|
|
2223
|
+
return undefined;
|
|
2224
|
+
}
|
|
2225
|
+
if (pending.expiresAt < Date.now()) {
|
|
2226
|
+
this.pendingChannelMemoryMutations.delete(key);
|
|
2227
|
+
return undefined;
|
|
2228
|
+
}
|
|
2229
|
+
if (pending.kind !== kind) {
|
|
2230
|
+
return undefined;
|
|
2231
|
+
}
|
|
2232
|
+
this.pendingChannelMemoryMutations.delete(key);
|
|
2233
|
+
return pending;
|
|
2234
|
+
}
|
|
2235
|
+
deletePendingChannelMemoryMutation(envelope) {
|
|
2236
|
+
const key = this.channelMemoryPendingKey(envelope);
|
|
2237
|
+
this.pendingChannelMemoryMutations.delete(key);
|
|
2238
|
+
this.pendingChannelMemoryMutationDeliveries.delete(key);
|
|
2239
|
+
}
|
|
2240
|
+
async classifyChannelMemoryIntent(envelope) {
|
|
2241
|
+
if (!this.memoryIntentClassifier || !this.channelMemory) {
|
|
2242
|
+
return null;
|
|
2243
|
+
}
|
|
2244
|
+
let entries;
|
|
2245
|
+
try {
|
|
2246
|
+
entries = await this.channelMemory.listChannelMemoryEntries(this.channelMemoryTarget(envelope));
|
|
2247
|
+
}
|
|
2248
|
+
catch (error) {
|
|
2249
|
+
this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
|
|
2027
2250
|
return null;
|
|
2028
2251
|
}
|
|
2029
2252
|
let classified;
|
|
2030
2253
|
try {
|
|
2031
2254
|
classified =
|
|
2032
|
-
await this.memoryIntentClassifier.classifyChannelMemoryIntent(text);
|
|
2255
|
+
await this.memoryIntentClassifier.classifyChannelMemoryIntent(envelope.text, entries);
|
|
2033
2256
|
}
|
|
2034
2257
|
catch (error) {
|
|
2035
2258
|
process.stderr.write(`[${this.name}] channel memory intent classifier failed: ${sanitizeLogText(this.channelMemoryErrorMessage(error), 200)}\n`);
|
|
2036
2259
|
return null;
|
|
2037
2260
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
const
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2261
|
+
try {
|
|
2262
|
+
if (typeof classified !== 'object' || classified === null)
|
|
2263
|
+
return null;
|
|
2264
|
+
const result = classified;
|
|
2265
|
+
const confidence = result.confidence;
|
|
2266
|
+
if (typeof confidence !== 'number' ||
|
|
2267
|
+
!Number.isFinite(confidence) ||
|
|
2268
|
+
confidence < 0 ||
|
|
2269
|
+
confidence > 1 ||
|
|
2270
|
+
confidence < CHANNEL_MEMORY_CLASSIFIER_MIN_CONFIDENCE) {
|
|
2271
|
+
return null;
|
|
2272
|
+
}
|
|
2273
|
+
const intent = result.intent;
|
|
2274
|
+
if (intent === 'remember') {
|
|
2275
|
+
const hasMemory = Object.prototype.hasOwnProperty.call(result, 'memory');
|
|
2276
|
+
const hasMemories = Object.prototype.hasOwnProperty.call(result, 'memories');
|
|
2277
|
+
if (hasMemory === hasMemories)
|
|
2278
|
+
return null;
|
|
2279
|
+
let texts = [];
|
|
2280
|
+
if (hasMemory) {
|
|
2281
|
+
const memory = result.memory;
|
|
2282
|
+
texts = typeof memory === 'string' ? [memory.trim()] : [];
|
|
2283
|
+
}
|
|
2284
|
+
else {
|
|
2285
|
+
const memories = result.memories;
|
|
2286
|
+
if (Array.isArray(memories)) {
|
|
2287
|
+
const snapshot = Array.from(memories);
|
|
2288
|
+
if (snapshot.every((memory) => typeof memory === 'string')) {
|
|
2289
|
+
texts = snapshot.map((memory) => memory.trim());
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return texts.length >= 1 &&
|
|
2294
|
+
texts.length <= 10 &&
|
|
2295
|
+
texts.every((text) => text.length > 0)
|
|
2296
|
+
? { kind: 'remember', texts }
|
|
2297
|
+
: null;
|
|
2298
|
+
}
|
|
2299
|
+
if (intent === 'clear_all')
|
|
2300
|
+
return { kind: 'clear_request' };
|
|
2301
|
+
if (intent !== 'list' &&
|
|
2302
|
+
intent !== 'inspect' &&
|
|
2303
|
+
intent !== 'update' &&
|
|
2304
|
+
intent !== 'remove') {
|
|
2305
|
+
return null;
|
|
2306
|
+
}
|
|
2307
|
+
const targetIds = result.targetIds;
|
|
2308
|
+
if (intent === 'list' && targetIds === undefined) {
|
|
2309
|
+
return { kind: 'list', page: 1 };
|
|
2310
|
+
}
|
|
2311
|
+
if (!Array.isArray(targetIds))
|
|
2312
|
+
return null;
|
|
2313
|
+
const targetIdSnapshot = Array.from(targetIds);
|
|
2314
|
+
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
2315
|
+
if (!targetIdSnapshot.every((id) => typeof id === 'string' && entryById.has(id)) ||
|
|
2316
|
+
new Set(targetIdSnapshot).size !== targetIdSnapshot.length) {
|
|
2317
|
+
return null;
|
|
2318
|
+
}
|
|
2319
|
+
const resolvedEntries = this.entriesForChannelMemoryIds(entries, targetIdSnapshot);
|
|
2320
|
+
if (intent === 'list') {
|
|
2321
|
+
return resolvedEntries.length === 0
|
|
2322
|
+
? { kind: 'no_match' }
|
|
2323
|
+
: {
|
|
2324
|
+
kind: 'list_matches',
|
|
2325
|
+
ids: resolvedEntries.map((entry) => entry.id),
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
if (resolvedEntries.length === 0)
|
|
2329
|
+
return { kind: 'no_match' };
|
|
2330
|
+
if (resolvedEntries.length > 1) {
|
|
2331
|
+
return {
|
|
2332
|
+
kind: 'ambiguous',
|
|
2333
|
+
ids: resolvedEntries.map((entry) => entry.id),
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
const entry = resolvedEntries[0];
|
|
2337
|
+
if (intent === 'inspect')
|
|
2338
|
+
return { kind: 'inspect', id: entry.id };
|
|
2339
|
+
if (intent === 'remove') {
|
|
2340
|
+
return {
|
|
2341
|
+
kind: 'natural_remove',
|
|
2342
|
+
id: entry.id,
|
|
2343
|
+
expectedText: entry.text,
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
const memory = result.memory;
|
|
2347
|
+
const text = typeof memory === 'string' ? memory.trim() : '';
|
|
2348
|
+
return text
|
|
2349
|
+
? {
|
|
2350
|
+
kind: 'natural_update',
|
|
2351
|
+
id: entry.id,
|
|
2352
|
+
text,
|
|
2353
|
+
expectedText: entry.text,
|
|
2354
|
+
}
|
|
2355
|
+
: null;
|
|
2047
2356
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2357
|
+
catch (error) {
|
|
2358
|
+
process.stderr.write(`[${this.name}] channel memory intent validation failed: ${sanitizeLogText(this.channelMemoryErrorMessage(error), 200)}\n`);
|
|
2359
|
+
return null;
|
|
2050
2360
|
}
|
|
2051
|
-
return null;
|
|
2052
2361
|
}
|
|
2053
2362
|
channelMemoryErrorMessage(error) {
|
|
2054
2363
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -2418,9 +2727,12 @@ export class ChannelBase {
|
|
|
2418
2727
|
throw new Error('processInbound called without a successful preflightInbound check.');
|
|
2419
2728
|
}
|
|
2420
2729
|
let memoryIntent = parseChannelMemoryIntent(envelope.text);
|
|
2730
|
+
if (memoryIntent?.kind === 'update' || memoryIntent?.kind === 'remove') {
|
|
2731
|
+
this.deletePendingChannelMemoryMutation(envelope);
|
|
2732
|
+
}
|
|
2421
2733
|
if (!memoryIntent &&
|
|
2422
2734
|
this.shouldClassifyChannelMemoryIntent(envelope.text)) {
|
|
2423
|
-
memoryIntent = await this.classifyChannelMemoryIntent(envelope
|
|
2735
|
+
memoryIntent = await this.classifyChannelMemoryIntent(envelope);
|
|
2424
2736
|
}
|
|
2425
2737
|
if (memoryIntent) {
|
|
2426
2738
|
await this.handleChannelMemoryIntent(envelope, memoryIntent);
|