@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 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
 
@@ -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[];
@@ -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 pendingClears;
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 clearPendingKey;
218
- private setPendingClear;
222
+ private channelMemoryPendingKey;
223
+ private deliverPendingChannelMemoryMutation;
224
+ private takePendingChannelMemoryMutation;
225
+ private deletePendingChannelMemoryMutation;
219
226
  private classifyChannelMemoryIntent;
220
227
  private channelMemoryErrorMessage;
221
228
  private channelMemoryUserErrorMessage;
@@ -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';
@@ -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
- pendingClears = new Map();
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.supportsProactiveTarget(target)) {
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
- async handleChannelMemoryIntent(envelope, intent, options = {}) {
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.setPendingClear(this.clearPendingKey(envelope));
1944
- await this.sendMessage(envelope.chatId, 'This clears channel memory for this chat. Say "确认清空记忆" or "confirm clear memory" to proceed.');
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.appendChannelMemory(this.channelMemoryTarget(envelope), intent.text);
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
- this.invalidateSessionContext(envelope);
1962
- await this.sendMessage(envelope.chatId, 'Channel memory updated.');
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 text;
2091
+ if (intent.kind === 'list' || intent.kind === 'inspect') {
2092
+ let entries;
1967
2093
  try {
1968
- text = (await channelMemory.readChannelMemory(this.channelMemoryTarget(envelope))).trim();
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
- await this.sendMessage(envelope.chatId, text === '' ? 'No channel memory saved.' : sanitizePromptText(text));
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 === '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;
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
- this.invalidateSessionContext(envelope);
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
- clearPendingKey(envelope) {
2014
- return `${this.name}:${envelope.chatId}:${envelope.threadId ?? ''}:${envelope.senderId ?? ''}`;
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
- setPendingClear(key) {
2191
+ async deliverPendingChannelMemoryMutation(envelope, mutation, message) {
2017
2192
  const now = Date.now();
2018
- for (const [pendingKey, expiresAt] of this.pendingClears) {
2019
- if (expiresAt < now) {
2020
- this.pendingClears.delete(pendingKey);
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.pendingClears.set(key, now + 60_000);
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
- async classifyChannelMemoryIntent(text) {
2026
- if (!this.memoryIntentClassifier) {
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
- if (classified.confidence < CHANNEL_MEMORY_CLASSIFIER_MIN_CONFIDENCE) {
2039
- return null;
2040
- }
2041
- if (classified.intent === 'remember') {
2042
- const memory = classified.memory?.trim();
2043
- return memory ? { kind: 'remember', text: memory } : null;
2044
- }
2045
- if (classified.intent === 'list') {
2046
- return { kind: 'list' };
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
- if (classified.intent === 'clear_all') {
2049
- return { kind: 'clear_request' };
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.text);
2735
+ memoryIntent = await this.classifyChannelMemoryIntent(envelope);
2424
2736
  }
2425
2737
  if (memoryIntent) {
2426
2738
  await this.handleChannelMemoryIntent(envelope, memoryIntent);