@qwen-code/channel-base 0.19.10 → 0.19.11-nightly.20260717.f8e6e8931

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 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 channel worker, the worker connects back to the daemon through the SDK, and adapters receive a `DaemonChannelBridge`-backed `ChannelAgentBridge` facade.
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, one daemon is bound to one workspace. Every selected channel's `cwd` must resolve to that same workspace. The optional `shellCommand` method is exposed to adapters only when the daemon advertises the `session_shell_command` capability.
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
 
@@ -111,6 +111,7 @@ export declare abstract class ChannelBase {
111
111
  private lifecycleBase;
112
112
  supportsProactiveSend(): boolean;
113
113
  protected supportsProactiveTarget(target: SessionTarget): boolean;
114
+ protected supportsProactiveWebhookTarget(target: SessionTarget): boolean;
114
115
  protected pushProactive(target: SessionTarget, text: string): Promise<void>;
115
116
  private prependUnattendedSessionContext;
116
117
  private drainCollectBufferForCurrentPrompt;
@@ -212,6 +213,9 @@ export declare abstract class ChannelBase {
212
213
  private invalidateSessionContext;
213
214
  private dropQueuedTurnIfStale;
214
215
  private getChannelMemory;
216
+ private entriesForChannelMemoryIds;
217
+ private renderChannelMemoryCandidate;
218
+ private renderChannelMemoryCandidates;
215
219
  private handleChannelMemoryIntent;
216
220
  private shouldClassifyChannelMemoryIntent;
217
221
  private clearPendingKey;
@@ -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 = /(记住|记得|记一下|记忆|忘掉|忘记|清空|清除|删除|保存|remember|memory|forget)/iu;
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';
@@ -370,6 +372,9 @@ export class ChannelBase {
370
372
  supportsProactiveTarget(target) {
371
373
  return target.threadId === undefined;
372
374
  }
375
+ supportsProactiveWebhookTarget(target) {
376
+ return this.supportsProactiveTarget(target);
377
+ }
373
378
  async pushProactive(target, text) {
374
379
  if (target.threadId) {
375
380
  throw new Error('Channel does not support proactive loop messages for threaded targets.');
@@ -675,7 +680,7 @@ export class ChannelBase {
675
680
  throw new Error(`Unknown webhook source "${task.source}".`);
676
681
  }
677
682
  const target = resolveChannelWebhookTarget(this.name, this.config.webhooks, task.source, task.targetRef);
678
- if (!this.supportsProactiveTarget(target)) {
683
+ if (!this.supportsProactiveWebhookTarget(target)) {
679
684
  throw new Error('Channel does not support proactive webhook messages for this chat target.');
680
685
  }
681
686
  return target;
@@ -1407,30 +1412,6 @@ export class ChannelBase {
1407
1412
  this.registerCommand('approve', (envelope, args) => this.handlePermissionResponseCommand(envelope, args, 'approve'));
1408
1413
  this.registerCommand('approve-always', (envelope, args) => this.handlePermissionResponseCommand(envelope, args, 'approve-always'));
1409
1414
  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
1415
  // Read-only: report the current (possibly group-shared) session and workspace.
1435
1416
  // For a shared session, gate it to authorized senders like /clear — /who
1436
1417
  // leaks the workspace basename, so non-members shouldn't see it either.
@@ -1938,7 +1919,64 @@ export class ChannelBase {
1938
1919
  }
1939
1920
  return this.channelMemory;
1940
1921
  }
1941
- async handleChannelMemoryIntent(envelope, intent, options = {}) {
1922
+ entriesForChannelMemoryIds(entries, ids) {
1923
+ const selected = new Set(ids);
1924
+ return entries.filter((entry) => selected.has(entry.id));
1925
+ }
1926
+ renderChannelMemoryCandidate(entry) {
1927
+ const preview = truncateCodePoints(sanitizePromptText(entry.text)
1928
+ .replace(/[\r\n]+/gu, ' ')
1929
+ .trim(), CHANNEL_MEMORY_PREVIEW_CODE_POINT_LIMIT);
1930
+ return `${entry.id} ${preview}`;
1931
+ }
1932
+ renderChannelMemoryCandidates(entries) {
1933
+ return entries.map((entry) => this.renderChannelMemoryCandidate(entry));
1934
+ }
1935
+ async handleChannelMemoryIntent(envelope, intent) {
1936
+ if (intent.kind === 'no_match') {
1937
+ await this.sendMessage(envelope.chatId, 'No matching channel memory entry.');
1938
+ return;
1939
+ }
1940
+ if (intent.kind === 'ambiguous') {
1941
+ const channelMemory = await this.getChannelMemory(envelope);
1942
+ if (!channelMemory)
1943
+ return;
1944
+ let entries;
1945
+ try {
1946
+ entries = await channelMemory.listChannelMemoryEntries(this.channelMemoryTarget(envelope));
1947
+ }
1948
+ catch (error) {
1949
+ this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
1950
+ await this.sendMessage(envelope.chatId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
1951
+ return;
1952
+ }
1953
+ const selected = this.entriesForChannelMemoryIds(entries, intent.ids);
1954
+ await this.sendMessage(envelope.chatId, [
1955
+ 'Multiple channel memory entries match:',
1956
+ ...this.renderChannelMemoryCandidates(selected),
1957
+ ].join('\n'));
1958
+ return;
1959
+ }
1960
+ if (intent.kind === 'list_matches') {
1961
+ const channelMemory = await this.getChannelMemory(envelope);
1962
+ if (!channelMemory)
1963
+ return;
1964
+ let entries;
1965
+ try {
1966
+ entries = await channelMemory.listChannelMemoryEntries(this.channelMemoryTarget(envelope));
1967
+ }
1968
+ catch (error) {
1969
+ this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
1970
+ await this.sendMessage(envelope.chatId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
1971
+ return;
1972
+ }
1973
+ const selected = this.entriesForChannelMemoryIds(entries, intent.ids);
1974
+ await this.sendMessage(envelope.chatId, [
1975
+ 'Channel memory (page 1/1):',
1976
+ ...this.renderChannelMemoryCandidates(selected),
1977
+ ].join('\n'));
1978
+ return;
1979
+ }
1942
1980
  if (intent.kind === 'clear_request') {
1943
1981
  this.setPendingClear(this.clearPendingKey(envelope));
1944
1982
  await this.sendMessage(envelope.chatId, 'This clears channel memory for this chat. Say "确认清空记忆" or "confirm clear memory" to proceed.');
@@ -1949,8 +1987,9 @@ export class ChannelBase {
1949
1987
  return;
1950
1988
  }
1951
1989
  if (intent.kind === 'remember') {
1990
+ let result;
1952
1991
  try {
1953
- await channelMemory.appendChannelMemory(this.channelMemoryTarget(envelope), intent.text);
1992
+ result = await channelMemory.addChannelMemoryEntries(this.channelMemoryTarget(envelope), [intent.text], envelope.senderId);
1954
1993
  }
1955
1994
  catch (error) {
1956
1995
  const message = this.channelMemoryErrorMessage(error);
@@ -1958,14 +1997,27 @@ export class ChannelBase {
1958
1997
  await this.sendMessage(envelope.chatId, `Failed to save channel memory: ${this.channelMemoryUserErrorMessage()}`);
1959
1998
  return;
1960
1999
  }
1961
- this.invalidateSessionContext(envelope);
1962
- await this.sendMessage(envelope.chatId, 'Channel memory updated.');
2000
+ if (result.changed) {
2001
+ this.invalidateSessionContext(envelope);
2002
+ }
2003
+ if (result.added.length > 0) {
2004
+ const ids = result.added.map((entry) => entry.id);
2005
+ await this.sendMessage(envelope.chatId, ids.length === 1
2006
+ ? `Channel memory ${ids[0]} saved.`
2007
+ : `Channel memory saved: ${ids.join(', ')}.`);
2008
+ }
2009
+ else if (result.duplicateIds.length > 0) {
2010
+ await this.sendMessage(envelope.chatId, `Channel memory already contains ${result.duplicateIds.join(', ')}.`);
2011
+ }
2012
+ else {
2013
+ await this.sendMessage(envelope.chatId, 'Channel memory updated.');
2014
+ }
1963
2015
  return;
1964
2016
  }
1965
- if (intent.kind === 'list') {
1966
- let text;
2017
+ if (intent.kind === 'list' || intent.kind === 'inspect') {
2018
+ let entries;
1967
2019
  try {
1968
- text = (await channelMemory.readChannelMemory(this.channelMemoryTarget(envelope))).trim();
2020
+ entries = await channelMemory.listChannelMemoryEntries(this.channelMemoryTarget(envelope));
1969
2021
  }
1970
2022
  catch (error) {
1971
2023
  const message = this.channelMemoryErrorMessage(error);
@@ -1973,19 +2025,77 @@ export class ChannelBase {
1973
2025
  await this.sendMessage(envelope.chatId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
1974
2026
  return;
1975
2027
  }
1976
- await this.sendMessage(envelope.chatId, text === '' ? 'No channel memory saved.' : sanitizePromptText(text));
2028
+ if (intent.kind === 'inspect') {
2029
+ const entry = entries.find((candidate) => candidate.id === intent.id);
2030
+ await this.sendMessage(envelope.chatId, entry
2031
+ ? `Channel memory ${entry.id}:\n${sanitizePromptText(entry.text).trim()}`
2032
+ : `No channel memory entry ${intent.id}.`);
2033
+ return;
2034
+ }
2035
+ const totalPages = Math.max(1, Math.ceil(entries.length / CHANNEL_MEMORY_PAGE_SIZE));
2036
+ if (intent.page > totalPages) {
2037
+ await this.sendMessage(envelope.chatId, `Channel memory page ${intent.page} does not exist.`);
2038
+ return;
2039
+ }
2040
+ if (entries.length === 0) {
2041
+ await this.sendMessage(envelope.chatId, 'No channel memory saved.');
2042
+ return;
2043
+ }
2044
+ const pageStart = (intent.page - 1) * CHANNEL_MEMORY_PAGE_SIZE;
2045
+ const lines = entries
2046
+ .slice(pageStart, pageStart + CHANNEL_MEMORY_PAGE_SIZE)
2047
+ .map((entry) => this.renderChannelMemoryCandidate(entry));
2048
+ await this.sendMessage(envelope.chatId, [`Channel memory (page ${intent.page}/${totalPages}):`, ...lines].join('\n'));
1977
2049
  return;
1978
2050
  }
1979
- if (intent.kind === 'clear_confirm') {
1980
- if (!options.skipPendingClear) {
1981
- const pendingKey = this.clearPendingKey(envelope);
1982
- const expiresAt = this.pendingClears.get(pendingKey);
1983
- this.pendingClears.delete(pendingKey);
1984
- if (expiresAt === undefined || expiresAt < Date.now()) {
1985
- await this.sendMessage(envelope.chatId, 'No pending clear request. Say "清空记忆" first.');
1986
- return;
2051
+ if (intent.kind === 'update' ||
2052
+ intent.kind === 'remove' ||
2053
+ intent.kind === 'natural_update' ||
2054
+ intent.kind === 'natural_remove') {
2055
+ let changed;
2056
+ const isUpdate = intent.kind === 'update' || intent.kind === 'natural_update';
2057
+ const id = intent.id;
2058
+ try {
2059
+ if (isUpdate) {
2060
+ ({ changed } = await channelMemory.updateChannelMemoryEntry(this.channelMemoryTarget(envelope), intent.kind === 'natural_update'
2061
+ ? {
2062
+ id: intent.id,
2063
+ text: intent.text,
2064
+ expectedText: intent.expectedText,
2065
+ }
2066
+ : { id: intent.id, text: intent.text }));
2067
+ }
2068
+ else {
2069
+ ({ changed } = await channelMemory.removeChannelMemoryEntries(this.channelMemoryTarget(envelope), intent.kind === 'natural_remove'
2070
+ ? {
2071
+ ids: [intent.id],
2072
+ expectedTextById: { [intent.id]: intent.expectedText },
2073
+ }
2074
+ : { ids: [intent.id] }));
1987
2075
  }
1988
2076
  }
2077
+ catch (error) {
2078
+ const message = this.channelMemoryErrorMessage(error);
2079
+ this.logChannelMemoryError(isUpdate ? 'update' : 'remove', envelope, message);
2080
+ await this.sendMessage(envelope.chatId, `Failed to ${isUpdate ? 'update' : 'remove'} channel memory: ${this.channelMemoryUserErrorMessage()}`);
2081
+ return;
2082
+ }
2083
+ if (!changed) {
2084
+ await this.sendMessage(envelope.chatId, `No channel memory entry ${id}.`);
2085
+ return;
2086
+ }
2087
+ this.invalidateSessionContext(envelope);
2088
+ await this.sendMessage(envelope.chatId, `Channel memory ${id} ${isUpdate ? 'updated' : 'removed'}.`);
2089
+ return;
2090
+ }
2091
+ if (intent.kind === 'clear_confirm') {
2092
+ const pendingKey = this.clearPendingKey(envelope);
2093
+ const expiresAt = this.pendingClears.get(pendingKey);
2094
+ this.pendingClears.delete(pendingKey);
2095
+ if (expiresAt === undefined || expiresAt < Date.now()) {
2096
+ await this.sendMessage(envelope.chatId, 'No pending clear request. Say "清空记忆" first.');
2097
+ return;
2098
+ }
1989
2099
  let result;
1990
2100
  try {
1991
2101
  result = await channelMemory.clearChannelMemory(this.channelMemoryTarget(envelope));
@@ -1996,7 +2106,9 @@ export class ChannelBase {
1996
2106
  await this.sendMessage(envelope.chatId, `Failed to clear channel memory: ${this.channelMemoryUserErrorMessage()}`);
1997
2107
  return;
1998
2108
  }
1999
- this.invalidateSessionContext(envelope);
2109
+ if (result.changed) {
2110
+ this.invalidateSessionContext(envelope);
2111
+ }
2000
2112
  await this.sendMessage(envelope.chatId, result.changed ? 'Channel memory cleared.' : 'No channel memory saved.');
2001
2113
  return;
2002
2114
  }
@@ -2022,32 +2134,100 @@ export class ChannelBase {
2022
2134
  }
2023
2135
  this.pendingClears.set(key, now + 60_000);
2024
2136
  }
2025
- async classifyChannelMemoryIntent(text) {
2026
- if (!this.memoryIntentClassifier) {
2137
+ async classifyChannelMemoryIntent(envelope) {
2138
+ if (!this.memoryIntentClassifier || !this.channelMemory) {
2139
+ return null;
2140
+ }
2141
+ let entries;
2142
+ try {
2143
+ entries = await this.channelMemory.listChannelMemoryEntries(this.channelMemoryTarget(envelope));
2144
+ }
2145
+ catch (error) {
2146
+ this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
2027
2147
  return null;
2028
2148
  }
2029
2149
  let classified;
2030
2150
  try {
2031
2151
  classified =
2032
- await this.memoryIntentClassifier.classifyChannelMemoryIntent(text);
2152
+ await this.memoryIntentClassifier.classifyChannelMemoryIntent(envelope.text, entries);
2033
2153
  }
2034
2154
  catch (error) {
2035
2155
  process.stderr.write(`[${this.name}] channel memory intent classifier failed: ${sanitizeLogText(this.channelMemoryErrorMessage(error), 200)}\n`);
2036
2156
  return null;
2037
2157
  }
2038
- if (classified.confidence < CHANNEL_MEMORY_CLASSIFIER_MIN_CONFIDENCE) {
2158
+ const confidence = classified && typeof classified === 'object'
2159
+ ? classified.confidence
2160
+ : undefined;
2161
+ if (typeof confidence !== 'number' ||
2162
+ !Number.isFinite(confidence) ||
2163
+ confidence < 0 ||
2164
+ confidence > 1 ||
2165
+ confidence < CHANNEL_MEMORY_CLASSIFIER_MIN_CONFIDENCE) {
2039
2166
  return null;
2040
2167
  }
2041
- if (classified.intent === 'remember') {
2042
- const memory = classified.memory?.trim();
2168
+ const result = classified;
2169
+ const targetIds = result.targetIds;
2170
+ const entryById = new Map(entries.map((entry) => [entry.id, entry]));
2171
+ const resolvedEntries = targetIds === undefined
2172
+ ? undefined
2173
+ : Array.isArray(targetIds) &&
2174
+ targetIds.every((id) => typeof id === 'string' && entryById.has(id)) &&
2175
+ new Set(targetIds).size === targetIds.length
2176
+ ? this.entriesForChannelMemoryIds(entries, targetIds)
2177
+ : null;
2178
+ if (result.intent === 'remember') {
2179
+ const memory = typeof result.memory === 'string' ? result.memory.trim() : '';
2043
2180
  return memory ? { kind: 'remember', text: memory } : null;
2044
2181
  }
2045
- if (classified.intent === 'list') {
2046
- return { kind: 'list' };
2047
- }
2048
- if (classified.intent === 'clear_all') {
2182
+ if (result.intent === 'list') {
2183
+ if (resolvedEntries === undefined)
2184
+ return { kind: 'list', page: 1 };
2185
+ if (resolvedEntries === null)
2186
+ return null;
2187
+ return resolvedEntries.length === 0
2188
+ ? { kind: 'no_match' }
2189
+ : {
2190
+ kind: 'list_matches',
2191
+ ids: resolvedEntries.map((entry) => entry.id),
2192
+ };
2193
+ }
2194
+ if (result.intent === 'clear_all') {
2049
2195
  return { kind: 'clear_request' };
2050
2196
  }
2197
+ if (result.intent !== 'inspect' &&
2198
+ result.intent !== 'update' &&
2199
+ result.intent !== 'remove') {
2200
+ return null;
2201
+ }
2202
+ if (resolvedEntries === null || resolvedEntries === undefined)
2203
+ return null;
2204
+ if (resolvedEntries.length === 0)
2205
+ return { kind: 'no_match' };
2206
+ if (resolvedEntries.length > 1) {
2207
+ return {
2208
+ kind: 'ambiguous',
2209
+ ids: resolvedEntries.map((entry) => entry.id),
2210
+ };
2211
+ }
2212
+ const entry = resolvedEntries[0];
2213
+ if (result.intent === 'inspect')
2214
+ return { kind: 'inspect', id: entry.id };
2215
+ if (result.intent === 'remove') {
2216
+ return {
2217
+ kind: 'natural_remove',
2218
+ id: entry.id,
2219
+ expectedText: entry.text,
2220
+ };
2221
+ }
2222
+ const text = typeof result.memory === 'string' ? result.memory.trim() : '';
2223
+ if (text) {
2224
+ return {
2225
+ kind: 'natural_update',
2226
+ id: entry.id,
2227
+ text,
2228
+ expectedText: entry.text,
2229
+ };
2230
+ }
2051
2231
  return null;
2052
2232
  }
2053
2233
  channelMemoryErrorMessage(error) {
@@ -2420,7 +2600,7 @@ export class ChannelBase {
2420
2600
  let memoryIntent = parseChannelMemoryIntent(envelope.text);
2421
2601
  if (!memoryIntent &&
2422
2602
  this.shouldClassifyChannelMemoryIntent(envelope.text)) {
2423
- memoryIntent = await this.classifyChannelMemoryIntent(envelope.text);
2603
+ memoryIntent = await this.classifyChannelMemoryIntent(envelope);
2424
2604
  }
2425
2605
  if (memoryIntent) {
2426
2606
  await this.handleChannelMemoryIntent(envelope, memoryIntent);