From b40838f259ddf16b396991bdbf6b0971a7fdd361 Mon Sep 17 00:00:00 2001 From: zhi Date: Thu, 2 Apr 2026 05:48:00 +0000 Subject: [PATCH] Refine discussion closure messaging --- plans/CSM.md | 4 +- plans/TASKLIST.md | 16 ++--- plugin/core/discussion-messages.ts | 4 +- test/discussion-service.test.ts | 98 ++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 10 deletions(-) diff --git a/plans/CSM.md b/plans/CSM.md index aa7872b..748d467 100644 --- a/plans/CSM.md +++ b/plans/CSM.md @@ -255,10 +255,11 @@ After callback: [Discussion Idle] No agent responded in the latest discussion round. -If the discussion goal has been achieved, the initiator should now: +If the discussion goal has already been achieved, the initiator should now: 1. write the discussion summary to a file in the workspace 2. call discuss-callback with the summary file path +This reminder does not mean the discussion was automatically summarized or closed. If more discussion is still needed, continue the discussion in this channel. ``` @@ -284,6 +285,7 @@ If more discussion is still needed, continue the discussion in this channel. This discussion channel has been closed. It is now kept for archive/reference only. Further discussion in this channel is ignored. +If follow-up work is needed, continue it from the origin work channel instead. ``` 这部分实现明确采用已有“在指定 session 上临时覆盖为 no-reply 模型”的方式,而不是修改 Agent 的全局默认模型。 diff --git a/plans/TASKLIST.md b/plans/TASKLIST.md index ffa00e3..1618f57 100644 --- a/plans/TASKLIST.md +++ b/plans/TASKLIST.md @@ -152,9 +152,9 @@ - [x] 模板中明确 `discuss-callback(summaryPath)` 调用要求 #### A10.2 idle reminder -- [. ] 定稿 discussion idle 模板 -- [. ] 模板中提醒 initiator:写总结文件并 callback -- [. ] 避免提醒文案歧义或像自动总结器 +- [x] 定稿 discussion idle 模板 +- [x] 模板中提醒 initiator:写总结文件并 callback +- [x] 避免提醒文案歧义或像自动总结器 #### A10.3 origin callback message - [x] 定稿发回原工作 channel 的结果通知模板 @@ -163,8 +163,8 @@ - [x] 模板中明确“继续基于该总结文件推进原任务” #### A10.4 closed reply -- [. ] 定稿 closed channel 固定回复模板 -- [. ] 明确 channel 已关闭,仅做留档使用 +- [x] 定稿 closed channel 固定回复模板 +- [x] 明确 channel 已关闭,仅做留档使用 ### A11. `discuss-callback` 详细校验任务 - [x] 校验当前 channel 必须是 discussion channel @@ -212,9 +212,9 @@ - [x] 测试绝对路径越界失败 #### A13.5 回调链路测试 -- [. ] 测试 callback 成功后 moderator 在 origin channel 发出通知 -- [. ] 测试 origin channel 收到路径后能继续原工作流 -- [. ] 测试 discussion channel 后续只保留留档行为 +- [x] 测试 callback 成功后 moderator 在 origin channel 发出通知 +- [ ] 测试 origin channel 收到路径后能继续原工作流 +- [x] 测试 discussion channel 后续只保留留档行为 #### A13.6 文档交付 - [. ] 根据最终代码实现更新 `plans/CSM.md` diff --git a/plugin/core/discussion-messages.ts b/plugin/core/discussion-messages.ts index ea59c80..67a256e 100644 --- a/plugin/core/discussion-messages.ts +++ b/plugin/core/discussion-messages.ts @@ -31,10 +31,11 @@ export function buildDiscussionIdleReminderMessage(): string { "[Discussion Idle]", "", "No agent responded in the latest discussion round.", - "If the discussion goal has been achieved, the initiator should now:", + "If the discussion goal has already been achieved, the initiator should now:", "1. write the discussion summary to a file in the workspace", "2. call discuss-callback with the summary file path", "", + "This reminder does not mean the discussion was automatically summarized or closed.", "If more discussion is still needed, continue the discussion in this channel.", ].join("\n"); } @@ -46,6 +47,7 @@ export function buildDiscussionClosedMessage(): string { "This discussion channel has been closed.", "It is now kept for archive/reference only.", "Further discussion in this channel is ignored.", + "If follow-up work is needed, continue it from the origin work channel instead.", ].join("\n"); } diff --git a/test/discussion-service.test.ts b/test/discussion-service.test.ts index 35cc742..ee924fe 100644 --- a/test/discussion-service.test.ts +++ b/test/discussion-service.test.ts @@ -5,6 +5,7 @@ import os from 'node:os'; import path from 'node:path'; import { createDiscussionService } from '../plugin/core/discussion-service.ts'; +import { buildDiscussionClosedMessage, buildDiscussionOriginCallbackMessage } from '../plugin/core/discussion-messages.ts'; function makeLogger() { return { @@ -244,3 +245,100 @@ test('handleCallback rejects an absolute path outside the initiator workspace', /summaryPath must stay inside the initiator workspace/, ); }); + +test('handleCallback notifies the origin channel with the resolved summary path', async () => { + const workspace = makeWorkspace(); + const summaryRelPath = path.join('plans', 'discussion-summary.md'); + const summaryAbsPath = path.join(workspace, summaryRelPath); + fs.mkdirSync(path.dirname(summaryAbsPath), { recursive: true }); + fs.writeFileSync(summaryAbsPath, '# done\n'); + + const fetchCalls: Array<{ url: string; body: any }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => { + const body = init?.body ? JSON.parse(String(init.body)) : undefined; + fetchCalls.push({ url: String(url), body }); + return new Response(JSON.stringify({ id: `msg-${fetchCalls.length}` }), { status: 200 }); + }) as typeof fetch; + + try { + const service = createDiscussionService({ + api: makeApi() as any, + workspaceRoot: workspace, + moderatorBotToken: 'bot-token', + forceNoReplyForSession: () => {}, + }); + + await service.initDiscussion({ + discussionChannelId: 'discussion-origin-1', + originChannelId: 'origin-8', + initiatorAgentId: 'agent-theta', + initiatorSessionId: 'session-theta', + discussGuide: 'Notify the origin channel.', + }); + + await service.handleCallback({ + channelId: 'discussion-origin-1', + summaryPath: summaryRelPath, + callerAgentId: 'agent-theta', + callerSessionKey: 'session-theta', + }); + + assert.equal(fetchCalls.length, 2); + assert.equal(fetchCalls[1]?.url, 'https://discord.com/api/v10/channels/origin-8/messages'); + assert.equal( + fetchCalls[1]?.body?.content, + buildDiscussionOriginCallbackMessage(fs.realpathSync.native(summaryAbsPath), 'discussion-origin-1'), + ); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('maybeReplyClosedChannel sends the archive-only closed message for later channel activity', async () => { + const workspace = makeWorkspace(); + const summaryRelPath = 'summary.md'; + const summaryAbsPath = path.join(workspace, summaryRelPath); + fs.writeFileSync(summaryAbsPath, 'closed\n'); + + const fetchCalls: Array<{ url: string; body: any }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => { + const body = init?.body ? JSON.parse(String(init.body)) : undefined; + fetchCalls.push({ url: String(url), body }); + return new Response(JSON.stringify({ id: `msg-${fetchCalls.length}` }), { status: 200 }); + }) as typeof fetch; + + try { + const service = createDiscussionService({ + api: makeApi() as any, + workspaceRoot: workspace, + moderatorBotToken: 'bot-token', + moderatorUserId: 'moderator-user', + forceNoReplyForSession: () => {}, + }); + + await service.initDiscussion({ + discussionChannelId: 'discussion-closed-1', + originChannelId: 'origin-9', + initiatorAgentId: 'agent-iota', + initiatorSessionId: 'session-iota', + discussGuide: 'Close and archive.', + }); + + await service.handleCallback({ + channelId: 'discussion-closed-1', + summaryPath: summaryRelPath, + callerAgentId: 'agent-iota', + callerSessionKey: 'session-iota', + }); + + const handled = await service.maybeReplyClosedChannel('discussion-closed-1', 'human-user'); + assert.equal(handled, true); + assert.equal(fetchCalls.length, 3); + assert.equal(fetchCalls[2]?.url, 'https://discord.com/api/v10/channels/discussion-closed-1/messages'); + assert.equal(fetchCalls[2]?.body?.content, buildDiscussionClosedMessage()); + } finally { + globalThis.fetch = originalFetch; + } +});