@sleep2agi/commhub-server 0.9.0-preview.49 → 0.9.0-preview.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.9.0-preview.49",
3
+ "version": "0.9.0-preview.50",
4
4
  "description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and MCP tools (17 collaboration-core + node/provider ops tools; authoritative list: docs-site/docs/api/mcp-tools.md).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -175,8 +175,10 @@ describe("#507 — send_reply attachments (positive)", () => {
175
175
 
176
176
  // Verify persistence — the echo must come from what actually landed on disk.
177
177
  const taskMeta = readTaskMeta(taskId);
178
- expect(taskMeta?.attachments?.length).toBe(2);
179
- expect(taskMeta.attachments.map((a: any) => a.file_id).sort()).toEqual([a1.file_id, a2.file_id].sort());
178
+ // #1823 —— 任务行上回复附件在 reply_attachments;attachments 键留给提问者自己带的附件
179
+ expect(taskMeta?.attachments).toBeUndefined();
180
+ expect(taskMeta?.reply_attachments?.length).toBe(2);
181
+ expect(taskMeta.reply_attachments.map((a: any) => a.file_id).sort()).toEqual([a1.file_id, a2.file_id].sort());
180
182
 
181
183
  const inboxMeta = readInboxMeta(reply.message_id!);
182
184
  expect(inboxMeta?.attachments?.length).toBe(2);
@@ -196,8 +198,10 @@ describe("#507 — send_reply attachments (positive)", () => {
196
198
  expect(reply.ok).toBe(true);
197
199
  expect(reply.attachments_saved!.length).toBe(1);
198
200
  const persisted = readTaskMeta(taskId);
199
- expect(persisted?.attachments?.[0]?.file_id).toBe(a1.file_id);
200
- expect(persisted?.other_field).toBe("preserved"); // meta merge preserves siblings
201
+ expect(persisted?.reply_attachments?.[0]?.file_id).toBe(a1.file_id);
202
+ expect(persisted?.attachments).toBeUndefined();
203
+ // #1823 —— 回复 meta 的兄弟字段属于回复(inbox 行),不写进提问者的任务行
204
+ expect(readInboxMeta(reply.message_id)?.other_field).toBe("preserved");
201
205
  });
202
206
 
203
207
  test("P3: top-level attachments WIN over meta.attachments when both supplied (parity with REST /api/task L2101)", async () => {
@@ -217,9 +221,26 @@ describe("#507 — send_reply attachments (positive)", () => {
217
221
  expect(reply.attachments_saved!.length).toBe(1);
218
222
  expect(reply.attachments_saved![0].file_id).toBe(topLevel.file_id);
219
223
  const persisted = readTaskMeta(taskId);
220
- expect(persisted?.attachments?.length).toBe(1);
221
- expect(persisted?.attachments?.[0]?.file_id).toBe(topLevel.file_id);
222
- expect(persisted?.other).toBe("kept");
224
+ expect(persisted?.reply_attachments?.length).toBe(1);
225
+ expect(persisted?.reply_attachments?.[0]?.file_id).toBe(topLevel.file_id);
226
+ expect(readInboxMeta(reply.message_id)?.other).toBe("kept");
227
+ });
228
+
229
+ // #1823 —— 提问者随任务带的附件必须留在 attachments;回复附件另放 reply_attachments。
230
+ // (Vincent 2026-09-06:回复的图被画进提问者气泡;以前整体 COALESCE 替换还会把提问者的附件覆盖掉)
231
+ test("P6 (#1823): asker's attachments survive a reply with attachments; reply goes to reply_attachments", async () => {
232
+ const { taskId } = seed();
233
+ const asked = att("11111111111111111111111111111111", { name: "asked.png" });
234
+ db.run("UPDATE tasks SET meta_json = ?1 WHERE task_id = ?2", [JSON.stringify({ attachments: [asked], origin: "dashboard" }), taskId]);
235
+ const handler = await getSendReplyHandler();
236
+ const answer = att("22222222222222222222222222222222", { name: "answer.md" });
237
+ const reply = await callReply(handler, { alias: AGENT_ALIAS, text: "here", in_reply_to: taskId, status: "replied" as const, attachments: [answer] });
238
+ expect(reply.ok).toBe(true);
239
+ const persisted = readTaskMeta(taskId);
240
+ expect(persisted?.attachments?.map((a: any) => a.file_id)).toEqual([asked.file_id]);
241
+ expect(persisted?.origin).toBe("dashboard");
242
+ expect(persisted?.reply_attachments?.map((a: any) => a.file_id)).toEqual([answer.file_id]);
243
+ expect(readInboxMeta(reply.message_id)?.attachments?.[0]?.file_id).toBe(answer.file_id);
223
244
  });
224
245
  });
225
246
 
package/src/tools.ts CHANGED
@@ -1981,7 +1981,21 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
1981
1981
  // against clobbering pre-existing meta_json when this reply
1982
1982
  // brings no attachments (metaJson === null → keep the existing
1983
1983
  // task meta_json unchanged). Reverse-(e) invariant.
1984
- const updateParams: any[] = [replyStatus, text, metaJson, in_reply_to];
1984
+ // #1823 —— 回复的附件写到 tasks.meta_json.reply_attachments,**不碰** meta_json.attachments:
1985
+ // 那个键是提问者随任务带的附件,客户端把它画在提问者的气泡里。以前这里用回复的 metaJson
1986
+ // 整体 COALESCE 替换,于是 agent 回的图出现在提问者气泡下面、提问者自己带的附件被覆盖
1987
+ // (Vincent 2026-09-06 实测「好像没成功发送给我」)。inbox 行(收件方视角)照旧写 metaJson。
1988
+ const replyTaskMetaJson = (() => {
1989
+ if (!attachmentsResult.attachments.length) return null;
1990
+ const rowParams: any[] = [in_reply_to];
1991
+ let rowSql = "SELECT meta_json FROM tasks WHERE task_id = ?1";
1992
+ rowSql = addScope(rowSql, rowParams, effectiveNetId);
1993
+ const existing = db.get<{ meta_json: string | null }>(rowSql, ...rowParams)?.meta_json ?? null;
1994
+ let base: Record<string, unknown> = {};
1995
+ if (existing) { try { const parsed = JSON.parse(existing); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) base = parsed; } catch {} }
1996
+ return JSON.stringify({ ...base, reply_attachments: attachmentsResult.attachments });
1997
+ })();
1998
+ const updateParams: any[] = [replyStatus, text, replyTaskMetaJson, in_reply_to];
1985
1999
  let updateSql = `UPDATE tasks
1986
2000
  SET status = ?1,
1987
2001
  result = ?2,