@qwen-code/channel-base 0.19.11-nightly.20260718.767a32484 → 0.19.11

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. 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.
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.
76
76
 
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.
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.
78
78
 
79
79
  ## Architecture
80
80
 
@@ -62,13 +62,6 @@ 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;
72
65
  }
73
66
  export interface ChannelAgentBridge {
74
67
  readonly availableCommands: AvailableCommand[];
@@ -70,8 +70,7 @@ 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 pendingChannelMemoryMutations;
74
- private pendingChannelMemoryMutationDeliveries;
73
+ private pendingClears;
75
74
  /** Per-session active prompt tracking for dispatch modes. */
76
75
  private activePrompts;
77
76
  /** Per-session message buffer for collect mode. */
@@ -219,10 +218,8 @@ export declare abstract class ChannelBase {
219
218
  private renderChannelMemoryCandidates;
220
219
  private handleChannelMemoryIntent;
221
220
  private shouldClassifyChannelMemoryIntent;
222
- private channelMemoryPendingKey;
223
- private deliverPendingChannelMemoryMutation;
224
- private takePendingChannelMemoryMutation;
225
- private deletePendingChannelMemoryMutation;
221
+ private clearPendingKey;
222
+ private setPendingClear;
226
223
  private classifyChannelMemoryIntent;
227
224
  private channelMemoryErrorMessage;
228
225
  private channelMemoryUserErrorMessage;
@@ -110,8 +110,7 @@ export class ChannelBase {
110
110
  * so a cleared session can't be resurrected by an already-queued prompt.
111
111
  */
112
112
  sessionGenerations = new Map();
113
- pendingChannelMemoryMutations = new Map();
114
- pendingChannelMemoryMutationDeliveries = new Map();
113
+ pendingClears = new Map();
115
114
  /** Per-session active prompt tracking for dispatch modes. */
116
115
  activePrompts = new Map();
117
116
  /** Per-session message buffer for collect mode. */
@@ -1979,79 +1978,8 @@ export class ChannelBase {
1979
1978
  return;
1980
1979
  }
1981
1980
  if (intent.kind === 'clear_request') {
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'));
1981
+ this.setPendingClear(this.clearPendingKey(envelope));
1982
+ await this.sendMessage(envelope.chatId, 'This clears channel memory for this chat. Say "确认清空记忆" or "confirm clear memory" to proceed.');
2055
1983
  return;
2056
1984
  }
2057
1985
  const channelMemory = await this.getChannelMemory(envelope);
@@ -2061,7 +1989,7 @@ export class ChannelBase {
2061
1989
  if (intent.kind === 'remember') {
2062
1990
  let result;
2063
1991
  try {
2064
- result = await channelMemory.addChannelMemoryEntries(this.channelMemoryTarget(envelope), intent.texts, envelope.senderId);
1992
+ result = await channelMemory.addChannelMemoryEntries(this.channelMemoryTarget(envelope), [intent.text], envelope.senderId);
2065
1993
  }
2066
1994
  catch (error) {
2067
1995
  const message = this.channelMemoryErrorMessage(error);
@@ -2074,11 +2002,9 @@ export class ChannelBase {
2074
2002
  }
2075
2003
  if (result.added.length > 0) {
2076
2004
  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(', ')}.`);
2005
+ await this.sendMessage(envelope.chatId, ids.length === 1
2006
+ ? `Channel memory ${ids[0]} saved.`
2007
+ : `Channel memory saved: ${ids.join(', ')}.`);
2082
2008
  }
2083
2009
  else if (result.duplicateIds.length > 0) {
2084
2010
  await this.sendMessage(envelope.chatId, `Channel memory already contains ${result.duplicateIds.join(', ')}.`);
@@ -2122,16 +2048,30 @@ export class ChannelBase {
2122
2048
  await this.sendMessage(envelope.chatId, [`Channel memory (page ${intent.page}/${totalPages}):`, ...lines].join('\n'));
2123
2049
  return;
2124
2050
  }
2125
- if (intent.kind === 'update' || intent.kind === 'remove') {
2051
+ if (intent.kind === 'update' ||
2052
+ intent.kind === 'remove' ||
2053
+ intent.kind === 'natural_update' ||
2054
+ intent.kind === 'natural_remove') {
2126
2055
  let changed;
2127
- const isUpdate = intent.kind === 'update';
2056
+ const isUpdate = intent.kind === 'update' || intent.kind === 'natural_update';
2128
2057
  const id = intent.id;
2129
2058
  try {
2130
2059
  if (isUpdate) {
2131
- ({ changed } = await channelMemory.updateChannelMemoryEntry(this.channelMemoryTarget(envelope), { id: intent.id, text: intent.text }));
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 }));
2132
2067
  }
2133
2068
  else {
2134
- ({ changed } = await channelMemory.removeChannelMemoryEntries(this.channelMemoryTarget(envelope), { ids: [intent.id] }));
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] }));
2135
2075
  }
2136
2076
  }
2137
2077
  catch (error) {
@@ -2149,8 +2089,10 @@ export class ChannelBase {
2149
2089
  return;
2150
2090
  }
2151
2091
  if (intent.kind === 'clear_confirm') {
2152
- const pending = this.takePendingChannelMemoryMutation(envelope, 'clear');
2153
- if (!pending) {
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()) {
2154
2096
  await this.sendMessage(envelope.chatId, 'No pending clear request. Say "清空记忆" first.');
2155
2097
  return;
2156
2098
  }
@@ -2180,62 +2122,17 @@ export class ChannelBase {
2180
2122
  !normalized.startsWith('/') &&
2181
2123
  CHANNEL_MEMORY_CLASSIFIER_TRIGGER_RE.test(normalized));
2182
2124
  }
2183
- channelMemoryPendingKey(envelope) {
2184
- return JSON.stringify([
2185
- this.name,
2186
- envelope.chatId,
2187
- envelope.threadId ?? null,
2188
- envelope.senderId ?? null,
2189
- ]);
2125
+ clearPendingKey(envelope) {
2126
+ return `${this.name}:${envelope.chatId}:${envelope.threadId ?? ''}:${envelope.senderId ?? ''}`;
2190
2127
  }
2191
- async deliverPendingChannelMemoryMutation(envelope, mutation, message) {
2128
+ setPendingClear(key) {
2192
2129
  const now = Date.now();
2193
- for (const [pendingKey, pending] of this.pendingChannelMemoryMutations) {
2194
- if (pending.expiresAt < now) {
2195
- this.pendingChannelMemoryMutations.delete(pendingKey);
2130
+ for (const [pendingKey, expiresAt] of this.pendingClears) {
2131
+ if (expiresAt < now) {
2132
+ this.pendingClears.delete(pendingKey);
2196
2133
  }
2197
2134
  }
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);
2207
- }
2208
- throw error;
2209
- }
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
- });
2218
- }
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);
2135
+ this.pendingClears.set(key, now + 60_000);
2239
2136
  }
2240
2137
  async classifyChannelMemoryIntent(envelope) {
2241
2138
  if (!this.memoryIntentClassifier || !this.channelMemory) {
@@ -2258,106 +2155,80 @@ export class ChannelBase {
2258
2155
  process.stderr.write(`[${this.name}] channel memory intent classifier failed: ${sanitizeLogText(this.channelMemoryErrorMessage(error), 200)}\n`);
2259
2156
  return null;
2260
2157
  }
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) {
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) {
2166
+ return null;
2167
+ }
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() : '';
2180
+ return memory ? { kind: 'remember', text: memory } : null;
2181
+ }
2182
+ if (result.intent === 'list') {
2183
+ if (resolvedEntries === undefined)
2309
2184
  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) {
2185
+ if (resolvedEntries === null)
2317
2186
  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',
2187
+ return resolvedEntries.length === 0
2188
+ ? { kind: 'no_match' }
2189
+ : {
2190
+ kind: 'list_matches',
2333
2191
  ids: resolvedEntries.map((entry) => entry.id),
2334
2192
  };
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;
2356
2193
  }
2357
- catch (error) {
2358
- process.stderr.write(`[${this.name}] channel memory intent validation failed: ${sanitizeLogText(this.channelMemoryErrorMessage(error), 200)}\n`);
2194
+ if (result.intent === 'clear_all') {
2195
+ return { kind: 'clear_request' };
2196
+ }
2197
+ if (result.intent !== 'inspect' &&
2198
+ result.intent !== 'update' &&
2199
+ result.intent !== 'remove') {
2359
2200
  return null;
2360
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
+ }
2231
+ return null;
2361
2232
  }
2362
2233
  channelMemoryErrorMessage(error) {
2363
2234
  return error instanceof Error ? error.message : String(error);
@@ -2727,9 +2598,6 @@ export class ChannelBase {
2727
2598
  throw new Error('processInbound called without a successful preflightInbound check.');
2728
2599
  }
2729
2600
  let memoryIntent = parseChannelMemoryIntent(envelope.text);
2730
- if (memoryIntent?.kind === 'update' || memoryIntent?.kind === 'remove') {
2731
- this.deletePendingChannelMemoryMutation(envelope);
2732
- }
2733
2601
  if (!memoryIntent &&
2734
2602
  this.shouldClassifyChannelMemoryIntent(envelope.text)) {
2735
2603
  memoryIntent = await this.classifyChannelMemoryIntent(envelope);