fix: /compact 改走宿主命令通道(commands.execute),绕开隔离作用域

根因:compaction-basic 挂载在隔离作用域,插件 ctx 的 ctx.get('compaction')
解析不到(实测 undefined;agent.ctx 亦不可达)。但宿主命令运行时的按 agent
作用层已注册 /compact(command-compact,Web 端同款通道),
commands.find(agent,'compact') 可命中。

因此 /compact 改为 ctx.commands.execute(agent,'/compact',[],signal) 派发:
- 与 Web 端 /compact 完全同语义(busy/错误映射、command/run 生命周期日志)
- 结果文本按 command-compact 固定文案翻译为中文回复;无法识别时回退原文详情
- 忙会话提前提示(busySessions/phase 预检);异步执行不阻塞轮询;同会话去重
- 测试改 mock commands.find/execute(51 全绿)
This commit is contained in:
RRRosmontis
2026-09-05 17:34:37 +08:00
parent d712fb3494
commit 8600835436
6 changed files with 160 additions and 132 deletions
+58 -49
View File
@@ -14,11 +14,36 @@ import { formatLastTurn, loadLastTurn } from './history.js';
import { describeAgent, displayLabel, workspaceName } from './label.js'; import { describeAgent, displayLabel, workspaceName } from './label.js';
import { formatModel, loadSessionModels, selectSessionModel, } from './models.js'; import { formatModel, loadSessionModels, selectSessionModel, } from './models.js';
import { formatStatusText, } from './status.js'; import { formatStatusText, } from './status.js';
function compactErrorCode(err) { /**
const code = err?.code; * 把 command-compact 返回的英文结果翻译成 TG 中文回复。
if (code === 'busy' || code === 'cancelled' || code === 'changed' * 文案以宿主 @deepseek-ai/dsh-command-compact 的固定输出为准;无法识别时返回
|| code === 'summary' || code === 'commit' || code === 'persistence') { * undefined,由调用方回退为「压缩失败 + 原文详情」。
return code; */
function translateCompactResult(result) {
const kind = result?.kind;
const text = result?.text ?? '';
if (kind === 'success') {
const m = /Compacted (\d+) history items \(~(\d+) tokens\)/.exec(text);
if (m)
return MSG.COMPACT_DONE(Number(m[1]), Number(m[2]));
if (/No compactable history yet/.test(text))
return MSG.COMPACT_NOTHING;
return undefined;
}
if (kind === 'error') {
if (/active compaction|not idle/.test(text))
return MSG.COMPACT_BUSY;
if (/^Compaction cancelled/.test(text))
return MSG.COMPACT_CANCELLED;
if (/history selected for compaction changed/.test(text))
return MSG.COMPACT_CHANGED;
if (/could not produce a useful summary/.test(text))
return MSG.COMPACT_SUMMARY_FAILED;
if (/did not finish cleanly/.test(text))
return MSG.COMPACT_COMMIT_FAILED;
if (/could not be saved/.test(text))
return MSG.COMPACT_PERSIST_FAILED;
return undefined;
} }
return undefined; return undefined;
} }
@@ -758,17 +783,17 @@ export class TelegramBridge {
return undefined; return undefined;
} }
// ── /compact:手动压缩当前绑定会话(需会话空闲)── // ── /compact:手动压缩当前绑定会话(需会话空闲)──
//
// 压缩引擎(compaction-basic)挂载在隔离作用域,插件 ctx 拿不到;但宿主的
// command-compact 已把 /compact 注册进命令运行时的按 agent 作用层(Web 端
// 同款通道),因此这里复用它:语义、busy/错误映射、命令生命周期日志与 Web
// 端 /compact 完全一致,且无需接触引擎内部。
async requestCompact(chatId) { async requestCompact(chatId) {
const binding = this.bindings.get(String(chatId)); const binding = this.bindings.get(String(chatId));
if (!binding) { if (!binding) {
await this.client.sendMessage(chatId, MSG.NEED_BIND); await this.client.sendMessage(chatId, MSG.NEED_BIND);
return; return;
} }
const engine = this.serviceOf('compaction');
if (!engine?.compactNow) {
await this.client.sendMessage(chatId, MSG.COMPACT_UNAVAILABLE);
return;
}
if (this.compacting.has(binding.sessionId)) { if (this.compacting.has(binding.sessionId)) {
await this.client.sendMessage(chatId, MSG.COMPACT_INFLIGHT); await this.client.sendMessage(chatId, MSG.COMPACT_INFLIGHT);
return; return;
@@ -779,25 +804,24 @@ export class TelegramBridge {
await this.client.sendMessage(chatId, MSG.GONE); await this.client.sendMessage(chatId, MSG.GONE);
return; return;
} }
const controller = new AbortController(); const commands = this.serviceOf('commands');
let operation; if (!commands?.execute || typeof commands.find !== 'function' || !commands.find(agent, 'compact')) {
try { await this.client.sendMessage(chatId, MSG.COMPACT_UNAVAILABLE);
// compactNow 在会话忙时同步抛 busy(runMaintenance 同步判定)。 return;
operation = engine.compactNow(agent, controller.signal);
} }
catch (err) { // 会话正在跑任务时压缩会被拒(runMaintenance 忙);提前提示避免「已开始→失败」两步。
const code = compactErrorCode(err); const busy = this.busySessions.has(binding.sessionId)
if (code === 'busy') { || Boolean(agent.phase?.kind
&& agent.phase?.kind !== 'idle');
if (busy) {
await this.client.sendMessage(chatId, MSG.COMPACT_BUSY); await this.client.sendMessage(chatId, MSG.COMPACT_BUSY);
return; return;
} }
this.ctx.logger.warn(`dsh-telegram-channel: /compact rejected: ${this.redact(err)}`); const controller = new AbortController();
await this.client.sendMessage(chatId, MSG.COMPACT_FAILED(this.redact(err)));
return;
}
this.compacting.add(binding.sessionId); this.compacting.add(binding.sessionId);
this.compactAborts.set(binding.sessionId, controller); this.compactAborts.set(binding.sessionId, controller);
await this.client.sendMessage(chatId, MSG.COMPACT_STARTED); await this.client.sendMessage(chatId, MSG.COMPACT_STARTED);
const operation = commands.execute(agent, '/compact', [], controller.signal);
void this.runCompaction(chatId, binding.sessionId, operation, controller).catch((err) => { void this.runCompaction(chatId, binding.sessionId, operation, controller).catch((err) => {
this.ctx.logger.error(`dsh-telegram-channel: /compact runner crashed: ${this.redact(err)}`); this.ctx.logger.error(`dsh-telegram-channel: /compact runner crashed: ${this.redact(err)}`);
}); });
@@ -805,13 +829,20 @@ export class TelegramBridge {
/** 等待压缩落定并汇报结果(不阻塞轮询循环)。 */ /** 等待压缩落定并汇报结果(不阻塞轮询循环)。 */
async runCompaction(chatId, sessionId, operation, controller) { async runCompaction(chatId, sessionId, operation, controller) {
try { try {
const result = await operation; const execution = await operation;
if (result === null) { if (execution === undefined) {
await this.enqueueNotice(chatId, MSG.COMPACT_NOTHING); // find() 通过但 execute 未命中:按未注册处理。
await this.enqueueNotice(chatId, MSG.COMPACT_UNAVAILABLE);
return;
} }
else { const translated = translateCompactResult(execution.result);
await this.enqueueNotice(chatId, MSG.COMPACT_DONE(result.shadowedSeqs.length, result.shadowedTokenCount)); if (translated !== undefined) {
await this.enqueueNotice(chatId, translated);
return;
} }
// 无法识别的结果:回退为通用失败 + 宿主原文。
const raw = execution.result?.text;
await this.enqueueNotice(chatId, MSG.COMPACT_FAILED(raw));
} }
catch (err) { catch (err) {
if (controller.signal.aborted) { if (controller.signal.aborted) {
@@ -819,31 +850,9 @@ export class TelegramBridge {
this.ctx.logger.info('dsh-telegram-channel: compaction aborted by bridge stop'); this.ctx.logger.info('dsh-telegram-channel: compaction aborted by bridge stop');
return; return;
} }
const code = compactErrorCode(err);
switch (code) {
case 'cancelled':
await this.enqueueNotice(chatId, MSG.COMPACT_CANCELLED);
return;
case 'changed':
await this.enqueueNotice(chatId, MSG.COMPACT_CHANGED);
return;
case 'summary':
await this.enqueueNotice(chatId, MSG.COMPACT_SUMMARY_FAILED);
return;
case 'commit':
await this.enqueueNotice(chatId, MSG.COMPACT_COMMIT_FAILED);
return;
case 'persistence':
await this.enqueueNotice(chatId, MSG.COMPACT_PERSIST_FAILED);
return;
case 'busy':
await this.enqueueNotice(chatId, MSG.COMPACT_BUSY);
return;
default:
this.ctx.logger.warn(`dsh-telegram-channel: /compact failed: ${this.redact(err)}`); this.ctx.logger.warn(`dsh-telegram-channel: /compact failed: ${this.redact(err)}`);
await this.enqueueNotice(chatId, MSG.COMPACT_FAILED(this.redact(err))); await this.enqueueNotice(chatId, MSG.COMPACT_FAILED(this.redact(err)));
} }
}
finally { finally {
this.compacting.delete(sessionId); this.compacting.delete(sessionId);
this.compactAborts.delete(sessionId); this.compactAborts.delete(sessionId);
+1 -1
View File
@@ -15,7 +15,7 @@ export declare const MSG: {
readonly STATUS_BOUND: (label: string) => string; readonly STATUS_BOUND: (label: string) => string;
readonly STATUS_BOUND_COLD: (label: string) => string; readonly STATUS_BOUND_COLD: (label: string) => string;
readonly COMPACT_USAGE: "用法:/compact(无参数,压缩当前绑定会话的历史,缩短上下文)"; readonly COMPACT_USAGE: "用法:/compact(无参数,压缩当前绑定会话的历史,缩短上下文)";
readonly COMPACT_UNAVAILABLE: "当前宿主未启用压缩服务(compaction 未装配),无法执行 /compact。"; readonly COMPACT_UNAVAILABLE: "当前宿主不可用压缩:/compact 命令未注册(需宿主启用压缩插件 command-compact / compaction-basic 后重启 dsh web)。";
readonly COMPACT_STARTED: "已开始压缩会话历史…\n压缩会占用一个模型回合,期间新消息排队;完成或失败后会在这里报告结果。"; readonly COMPACT_STARTED: "已开始压缩会话历史…\n压缩会占用一个模型回合,期间新消息排队;完成或失败后会在这里报告结果。";
readonly COMPACT_BUSY: "当前会话有任务正在运行(或压缩锁被占用),无法压缩。请等本轮结束(或 /stop)后再试。"; readonly COMPACT_BUSY: "当前会话有任务正在运行(或压缩锁被占用),无法压缩。请等本轮结束(或 /stop)后再试。";
readonly COMPACT_INFLIGHT: "该会话已在进行压缩,请稍候(完成后会报告结果)。"; readonly COMPACT_INFLIGHT: "该会话已在进行压缩,请稍候(完成后会报告结果)。";
+1 -1
View File
@@ -39,7 +39,7 @@ export const MSG = {
return `当前绑定:${label}\n(会话当前未在内存中运行;发消息时会自动 resume。)`; return `当前绑定:${label}\n(会话当前未在内存中运行;发消息时会自动 resume。)`;
}, },
COMPACT_USAGE: '用法:/compact(无参数,压缩当前绑定会话的历史,缩短上下文)', COMPACT_USAGE: '用法:/compact(无参数,压缩当前绑定会话的历史,缩短上下文)',
COMPACT_UNAVAILABLE: '当前宿主未启用压缩服务(compaction 未装配),无法执行 /compact。', COMPACT_UNAVAILABLE: '当前宿主不可用压缩:/compact 命令未注册(需宿主启用压缩插件 command-compact / compaction-basic 后重启 dsh web)。',
COMPACT_STARTED: '已开始压缩会话历史…\n压缩会占用一个模型回合,期间新消息排队;完成或失败后会在这里报告结果。', COMPACT_STARTED: '已开始压缩会话历史…\n压缩会占用一个模型回合,期间新消息排队;完成或失败后会在这里报告结果。',
COMPACT_BUSY: '当前会话有任务正在运行(或压缩锁被占用),无法压缩。请等本轮结束(或 /stop)后再试。', COMPACT_BUSY: '当前会话有任务正在运行(或压缩锁被占用),无法压缩。请等本轮结束(或 /stop)后再试。',
COMPACT_INFLIGHT: '该会话已在进行压缩,请稍候(完成后会报告结果)。', COMPACT_INFLIGHT: '该会话已在进行压缩,请稍候(完成后会报告结果)。',
+69 -61
View File
@@ -126,27 +126,49 @@ interface ProjectionRegistryLike {
snapshot?: (session: unknown) => { asOfSeq: number; values: Record<string, unknown> } | undefined snapshot?: (session: unknown) => { asOfSeq: number; values: Record<string, unknown> } | undefined
} }
/** 手动压缩引擎(结构镜像 ctx.compaction.compactNow)。 */ /**
interface CompactionEngineLike { * 命令运行时(结构镜像 ctx.commands.execute)。
compactNow?: ( *
* /compact 不在插件可见作用域内直接提供 compaction 服务(compaction-basic 挂载在
* 隔离作用域,ctx.get 取不到),但宿主命令运行时可见,且 command-compact 已把
* /compact 注册进该运行时的按 agent 作用层 —— Web 端正是用 execute() 派发它的。
* TG 端复用同一条通道:语义、busy/错误映射、命令生命周期日志与 Web 完全一致。
*/
interface CommandsRuntimeLike {
find?: (agent: unknown, name: string) => unknown
execute?: (
agent: unknown, agent: unknown,
line: string,
images: readonly unknown[],
signal: AbortSignal, signal: AbortSignal,
sourceCommandId?: string, ) => Promise<{ commandId: string; result: { kind: string; text?: string } } | undefined>
) => Promise<{
shadowedSeqs: number[]
shadowedTokenCount: number
summarySeq: number
} | null>
} }
/** 手动压缩的预期失败类(ManualCompactionError.code 的运行时字符串)。 */ /** command-compact 的 CommandResult.kind 值。 */
type CompactFailureCode = 'busy' | 'cancelled' | 'changed' | 'summary' | 'commit' | 'persistence' type CompactCommandKind = 'success' | 'error'
function compactErrorCode(err: unknown): CompactFailureCode | undefined { /**
const code = (err as { code?: unknown } | undefined)?.code * 把 command-compact 返回的英文结果翻译成 TG 中文回复。
if (code === 'busy' || code === 'cancelled' || code === 'changed' * 文案以宿主 @deepseek-ai/dsh-command-compact 的固定输出为准;无法识别时返回
|| code === 'summary' || code === 'commit' || code === 'persistence') { * undefined,由调用方回退为「压缩失败 + 原文详情」。
return code */
function translateCompactResult(result: { kind: string; text?: string } | undefined): string | undefined {
const kind = result?.kind as CompactCommandKind | undefined
const text = result?.text ?? ''
if (kind === 'success') {
const m = /Compacted (\d+) history items \(~(\d+) tokens\)/.exec(text)
if (m) return MSG.COMPACT_DONE(Number(m[1]), Number(m[2]))
if (/No compactable history yet/.test(text)) return MSG.COMPACT_NOTHING
return undefined
}
if (kind === 'error') {
if (/active compaction|not idle/.test(text)) return MSG.COMPACT_BUSY
if (/^Compaction cancelled/.test(text)) return MSG.COMPACT_CANCELLED
if (/history selected for compaction changed/.test(text)) return MSG.COMPACT_CHANGED
if (/could not produce a useful summary/.test(text)) return MSG.COMPACT_SUMMARY_FAILED
if (/did not finish cleanly/.test(text)) return MSG.COMPACT_COMMIT_FAILED
if (/could not be saved/.test(text)) return MSG.COMPACT_PERSIST_FAILED
return undefined
} }
return undefined return undefined
} }
@@ -914,6 +936,11 @@ export class TelegramBridge {
} }
// ── /compact:手动压缩当前绑定会话(需会话空闲)── // ── /compact:手动压缩当前绑定会话(需会话空闲)──
//
// 压缩引擎(compaction-basic)挂载在隔离作用域,插件 ctx 拿不到;但宿主的
// command-compact 已把 /compact 注册进命令运行时的按 agent 作用层(Web 端
// 同款通道),因此这里复用它:语义、busy/错误映射、命令生命周期日志与 Web
// 端 /compact 完全一致,且无需接触引擎内部。
private async requestCompact(chatId: number): Promise<void> { private async requestCompact(chatId: number): Promise<void> {
const binding = this.bindings.get(String(chatId)) const binding = this.bindings.get(String(chatId))
@@ -921,11 +948,6 @@ export class TelegramBridge {
await this.client.sendMessage(chatId, MSG.NEED_BIND) await this.client.sendMessage(chatId, MSG.NEED_BIND)
return return
} }
const engine = this.serviceOf<CompactionEngineLike>('compaction')
if (!engine?.compactNow) {
await this.client.sendMessage(chatId, MSG.COMPACT_UNAVAILABLE)
return
}
if (this.compacting.has(binding.sessionId)) { if (this.compacting.has(binding.sessionId)) {
await this.client.sendMessage(chatId, MSG.COMPACT_INFLIGHT) await this.client.sendMessage(chatId, MSG.COMPACT_INFLIGHT)
return return
@@ -936,24 +958,24 @@ export class TelegramBridge {
await this.client.sendMessage(chatId, MSG.GONE) await this.client.sendMessage(chatId, MSG.GONE)
return return
} }
const controller = new AbortController() const commands = this.serviceOf<CommandsRuntimeLike>('commands')
let operation: Promise<{ shadowedSeqs: number[]; shadowedTokenCount: number; summarySeq: number } | null> if (!commands?.execute || typeof commands.find !== 'function' || !commands.find(agent, 'compact')) {
try { await this.client.sendMessage(chatId, MSG.COMPACT_UNAVAILABLE)
// compactNow 在会话忙时同步抛 busy(runMaintenance 同步判定)。 return
operation = engine.compactNow(agent as never, controller.signal) }
} catch (err) { // 会话正在跑任务时压缩会被拒(runMaintenance 忙);提前提示避免「已开始→失败」两步。
const code = compactErrorCode(err) const busy = this.busySessions.has(binding.sessionId)
if (code === 'busy') { || Boolean((agent as { phase?: { kind?: string } }).phase?.kind
&& (agent as { phase?: { kind?: string } }).phase?.kind !== 'idle')
if (busy) {
await this.client.sendMessage(chatId, MSG.COMPACT_BUSY) await this.client.sendMessage(chatId, MSG.COMPACT_BUSY)
return return
} }
this.ctx.logger.warn(`dsh-telegram-channel: /compact rejected: ${this.redact(err)}`) const controller = new AbortController()
await this.client.sendMessage(chatId, MSG.COMPACT_FAILED(this.redact(err)))
return
}
this.compacting.add(binding.sessionId) this.compacting.add(binding.sessionId)
this.compactAborts.set(binding.sessionId, controller) this.compactAborts.set(binding.sessionId, controller)
await this.client.sendMessage(chatId, MSG.COMPACT_STARTED) await this.client.sendMessage(chatId, MSG.COMPACT_STARTED)
const operation = commands.execute(agent as never, '/compact', [], controller.signal)
void this.runCompaction(chatId, binding.sessionId, operation, controller).catch((err) => { void this.runCompaction(chatId, binding.sessionId, operation, controller).catch((err) => {
this.ctx.logger.error(`dsh-telegram-channel: /compact runner crashed: ${this.redact(err)}`) this.ctx.logger.error(`dsh-telegram-channel: /compact runner crashed: ${this.redact(err)}`)
}) })
@@ -963,46 +985,32 @@ export class TelegramBridge {
private async runCompaction( private async runCompaction(
chatId: number, chatId: number,
sessionId: string, sessionId: string,
operation: Promise<{ shadowedSeqs: number[]; shadowedTokenCount: number; summarySeq: number } | null>, operation: Promise<{ commandId: string; result: { kind: string; text?: string } } | undefined>,
controller: AbortController, controller: AbortController,
): Promise<void> { ): Promise<void> {
try { try {
const result = await operation const execution = await operation
if (result === null) { if (execution === undefined) {
await this.enqueueNotice(chatId, MSG.COMPACT_NOTHING) // find() 通过但 execute 未命中:按未注册处理。
} else { await this.enqueueNotice(chatId, MSG.COMPACT_UNAVAILABLE)
await this.enqueueNotice(chatId, MSG.COMPACT_DONE(result.shadowedSeqs.length, result.shadowedTokenCount)) return
} }
const translated = translateCompactResult(execution.result)
if (translated !== undefined) {
await this.enqueueNotice(chatId, translated)
return
}
// 无法识别的结果:回退为通用失败 + 宿主原文。
const raw = execution.result?.text
await this.enqueueNotice(chatId, MSG.COMPACT_FAILED(raw))
} catch (err) { } catch (err) {
if (controller.signal.aborted) { if (controller.signal.aborted) {
// 桥停止/热重载导致的取消:静默,不打扰(本实例正在退出)。 // 桥停止/热重载导致的取消:静默,不打扰(本实例正在退出)。
this.ctx.logger.info('dsh-telegram-channel: compaction aborted by bridge stop') this.ctx.logger.info('dsh-telegram-channel: compaction aborted by bridge stop')
return return
} }
const code = compactErrorCode(err)
switch (code) {
case 'cancelled':
await this.enqueueNotice(chatId, MSG.COMPACT_CANCELLED)
return
case 'changed':
await this.enqueueNotice(chatId, MSG.COMPACT_CHANGED)
return
case 'summary':
await this.enqueueNotice(chatId, MSG.COMPACT_SUMMARY_FAILED)
return
case 'commit':
await this.enqueueNotice(chatId, MSG.COMPACT_COMMIT_FAILED)
return
case 'persistence':
await this.enqueueNotice(chatId, MSG.COMPACT_PERSIST_FAILED)
return
case 'busy':
await this.enqueueNotice(chatId, MSG.COMPACT_BUSY)
return
default:
this.ctx.logger.warn(`dsh-telegram-channel: /compact failed: ${this.redact(err)}`) this.ctx.logger.warn(`dsh-telegram-channel: /compact failed: ${this.redact(err)}`)
await this.enqueueNotice(chatId, MSG.COMPACT_FAILED(this.redact(err))) await this.enqueueNotice(chatId, MSG.COMPACT_FAILED(this.redact(err)))
}
} finally { } finally {
this.compacting.delete(sessionId) this.compacting.delete(sessionId)
this.compactAborts.delete(sessionId) this.compactAborts.delete(sessionId)
+1 -1
View File
@@ -39,7 +39,7 @@ export const MSG = {
return `当前绑定:${label}\n(会话当前未在内存中运行;发消息时会自动 resume。)` return `当前绑定:${label}\n(会话当前未在内存中运行;发消息时会自动 resume。)`
}, },
COMPACT_USAGE: '用法:/compact(无参数,压缩当前绑定会话的历史,缩短上下文)', COMPACT_USAGE: '用法:/compact(无参数,压缩当前绑定会话的历史,缩短上下文)',
COMPACT_UNAVAILABLE: '当前宿主未启用压缩服务(compaction 未装配),无法执行 /compact。', COMPACT_UNAVAILABLE: '当前宿主不可用压缩:/compact 命令未注册(需宿主启用压缩插件 command-compact / compaction-basic 后重启 dsh web)。',
COMPACT_STARTED: '已开始压缩会话历史…\n压缩会占用一个模型回合,期间新消息排队;完成或失败后会在这里报告结果。', COMPACT_STARTED: '已开始压缩会话历史…\n压缩会占用一个模型回合,期间新消息排队;完成或失败后会在这里报告结果。',
COMPACT_BUSY: '当前会话有任务正在运行(或压缩锁被占用),无法压缩。请等本轮结束(或 /stop)后再试。', COMPACT_BUSY: '当前会话有任务正在运行(或压缩锁被占用),无法压缩。请等本轮结束(或 /stop)后再试。',
COMPACT_INFLIGHT: '该会话已在进行压缩,请稍候(完成后会报告结果)。', COMPACT_INFLIGHT: '该会话已在进行压缩,请稍候(完成后会报告结果)。',
+22 -11
View File
@@ -912,11 +912,11 @@ test('/compact compacts bound session and reports result', async () => {
roots: () => [agent], roots: () => [agent],
get: (id: ReturnType<typeof SessionId>) => (String(id) === sessionId ? agent : undefined), get: (id: ReturnType<typeof SessionId>) => (String(id) === sessionId ? agent : undefined),
}, },
compaction: { commands: {
compactNow: async () => ({ find: () => true,
shadowedSeqs: [1, 2, 3, 4], execute: async () => ({
shadowedTokenCount: 12_345, commandId: 'cmd-1',
summarySeq: 9, result: { kind: 'success', text: 'Compacted 4 history items (~12345 tokens).' },
}), }),
}, },
on() { return () => {} }, on() { return () => {} },
@@ -962,10 +962,15 @@ test('/compact busy agent replies COMPACT_BUSY without starting', async () => {
roots: () => [agent], roots: () => [agent],
get: (id: ReturnType<typeof SessionId>) => (String(id) === sessionId ? agent : undefined), get: (id: ReturnType<typeof SessionId>) => (String(id) === sessionId ? agent : undefined),
}, },
compaction: { commands: {
compactNow: () => { find: () => true,
throw Object.assign(new Error('busy'), { code: 'busy' }) execute: async () => ({
commandId: 'cmd-busy',
result: {
kind: 'error',
text: 'Compaction is unavailable because this process has an active compaction, or the agent is not idle.',
}, },
}),
}, },
on() { return () => {} }, on() { return () => {} },
} }
@@ -987,11 +992,14 @@ test('/compact busy agent replies COMPACT_BUSY without starting', async () => {
}) })
sent.length = 0 // drop the BOUND message; keep only /compact replies sent.length = 0 // drop the BOUND message; keep only /compact replies
await bridge.processUpdate(messageUpdate(10, 1, '/compact', 2)) await bridge.processUpdate(messageUpdate(10, 1, '/compact', 2))
assert.equal(sent.length, 1) assert.ok(sent.some((m) => m.text.includes('已开始压缩')), 'ack sent')
assert.equal(sent[0]!.text, MSG.COMPACT_BUSY) for (let i = 0; i < 100 && !sent.some((m) => m.text.includes(MSG.COMPACT_BUSY)); i++) {
await new Promise((r) => setTimeout(r, 5))
}
assert.ok(sent.some((m) => m.text === MSG.COMPACT_BUSY), 'busy notice arrives')
}) })
test('/compact without compaction engine reports unavailable', async () => { test('/compact without registered /compact command reports unavailable', async () => {
const sent: SentMessage[] = [] const sent: SentMessage[] = []
const followups: UserMessage[] = [] const followups: UserMessage[] = []
const sessionId = 'live-noeng' const sessionId = 'live-noeng'
@@ -1003,6 +1011,9 @@ test('/compact without compaction engine reports unavailable', async () => {
roots: () => [agent], roots: () => [agent],
get: (id: ReturnType<typeof SessionId>) => (String(id) === sessionId ? agent : undefined), get: (id: ReturnType<typeof SessionId>) => (String(id) === sessionId ? agent : undefined),
}, },
commands: {
find: () => undefined, // command not registered on this host
},
on() { return () => {} }, on() { return () => {} },
} }
const bridge = new TelegramBridge(ctx as any, { const bridge = new TelegramBridge(ctx as any, {