@sleep2agi/commhub-server 0.9.0-preview.6 → 0.9.0-preview.60

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.
Files changed (140) hide show
  1. package/README.md +125 -7
  2. package/bin/commhub.ts +10 -2
  3. package/package.json +5 -5
  4. package/src/ack-create-request-transport.test.ts +159 -0
  5. package/src/ack-create-request.test.ts +331 -0
  6. package/src/admin-networks-http.test.ts +88 -0
  7. package/src/alias-filter.test.ts +72 -0
  8. package/src/alias-filter.ts +32 -0
  9. package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
  10. package/src/api-host-supervisors-fallback.test.ts +271 -0
  11. package/src/api-nodes-shape.test.ts +26 -10
  12. package/src/auth-validate.test.ts +5 -1
  13. package/src/auth.ts +93 -14
  14. package/src/avatar-validate.test.ts +163 -0
  15. package/src/avatar-validate.ts +103 -0
  16. package/src/blocked-exit-on-reply.test.ts +122 -0
  17. package/src/boot-side-effect.test.ts +124 -0
  18. package/src/config-apply-sec1.test.ts +107 -0
  19. package/src/config-apply-validate.test.ts +92 -0
  20. package/src/config-apply-validate.ts +81 -3
  21. package/src/create-node-blocked-gate.test.ts +356 -0
  22. package/src/create-node-tool-schema.test.ts +39 -0
  23. package/src/create-node-validate.test.ts +89 -1
  24. package/src/create-node-validate.ts +55 -7
  25. package/src/create-node.ts +51 -1
  26. package/src/dashboard-slash-routing-http.test.ts +107 -0
  27. package/src/db-adapter-guard.test.ts +85 -0
  28. package/src/db-adapter.ts +64 -23
  29. package/src/db-timestamp.test.ts +38 -0
  30. package/src/db-timestamp.ts +15 -0
  31. package/src/db.ts +710 -33
  32. package/src/delivered-without-push.test.ts +140 -0
  33. package/src/desktop-message-seam.test.ts +150 -0
  34. package/src/desktop-message.test.ts +368 -0
  35. package/src/ext-token-shared.test.ts +60 -0
  36. package/src/ext-validation-regression.test.ts +131 -0
  37. package/src/external-schedule-edits-http.test.ts +320 -0
  38. package/src/external-schedule-edits-timestamps.test.ts +64 -0
  39. package/src/external-schedule-edits.ts +327 -0
  40. package/src/file-download-authz-dev-open.test.ts +135 -0
  41. package/src/file-download-authz.test.ts +403 -0
  42. package/src/file-download-date-bucket.test.ts +436 -0
  43. package/src/file-network-scope-dev-open.test.ts +97 -0
  44. package/src/file-network-scope.test.ts +731 -0
  45. package/src/health-redaction.test.ts +641 -0
  46. package/src/http-gzip.test.ts +73 -0
  47. package/src/http-gzip.ts +60 -0
  48. package/src/hub-timestamp.test.ts +38 -0
  49. package/src/hub-timestamp.ts +27 -0
  50. package/src/inbox-count-sse.test.ts +310 -0
  51. package/src/inbox-count.ts +29 -0
  52. package/src/index.ts +15 -2227
  53. package/src/lifecycle-guard.ts +50 -0
  54. package/src/list-host-supervisors.test.ts +314 -0
  55. package/src/mcp-write-network-resolution.test.ts +429 -0
  56. package/src/messages-ack-by-agent-http.test.ts +142 -0
  57. package/src/network-name-http.test.ts +75 -0
  58. package/src/network-scope.ts +101 -0
  59. package/src/node-attrs-validate.ts +88 -0
  60. package/src/node-attrs.test.ts +217 -0
  61. package/src/node-create-request-status-http.test.ts +92 -0
  62. package/src/node-files-transport.test.ts +344 -0
  63. package/src/node-id-alias.test.ts +188 -0
  64. package/src/node-lifecycle-controllable.test.ts +64 -0
  65. package/src/node-lifecycle-controllable.ts +51 -0
  66. package/src/node-request-retention.test.ts +281 -0
  67. package/src/node-request-retention.ts +104 -0
  68. package/src/node-skills-transport.test.ts +209 -0
  69. package/src/observer-avatar-http.test.ts +428 -0
  70. package/src/observer-push.test.ts +252 -0
  71. package/src/package-version-consistency.test.ts +25 -0
  72. package/src/peer-reply-atomic.test.ts +455 -0
  73. package/src/peer-reply-capability.test.ts +70 -0
  74. package/src/push.ts +163 -7
  75. package/src/redact-tokens.test.ts +65 -0
  76. package/src/redact-tokens.ts +40 -0
  77. package/src/reply-target-derivation.test.ts +96 -0
  78. package/src/report-status-host-schema.test.ts +211 -0
  79. package/src/report-status-resume-id-handover.test.ts +150 -0
  80. package/src/require-explicit-test-db.ts +9 -0
  81. package/src/resolve-port.test.ts +57 -0
  82. package/src/resolve-port.ts +44 -0
  83. package/src/response-charset.test.ts +130 -0
  84. package/src/rest-explicit-columns-http.test.ts +216 -0
  85. package/src/rest-identity.test.ts +129 -0
  86. package/src/rest-identity.ts +100 -0
  87. package/src/rest-projections.ts +67 -0
  88. package/src/rest-write-network-resolution.test.ts +133 -0
  89. package/src/retention.ts +10 -1
  90. package/src/rules-file-duplicate-alias.test.ts +234 -0
  91. package/src/rules-file-session-target.test.ts +207 -0
  92. package/src/rules-file-transport.test.ts +237 -0
  93. package/src/runtime-label.test.ts +22 -0
  94. package/src/runtime-label.ts +21 -0
  95. package/src/scheduled-run-terminal.test.ts +268 -0
  96. package/src/scheduled-tasks-http.test.ts +483 -0
  97. package/src/scheduled-tasks.ts +549 -0
  98. package/src/send-reply-agent-warning.test.ts +202 -0
  99. package/src/send-reply-alias-optional.test.ts +131 -0
  100. package/src/send-reply-attachments.test.ts +458 -0
  101. package/src/serve-error.test.ts +38 -0
  102. package/src/serve-error.ts +27 -0
  103. package/src/server.ts +3842 -0
  104. package/src/shared/external-schedule-contract.ts +60 -0
  105. package/src/side-thread-command-transport.test.ts +302 -0
  106. package/src/side-thread-command-transport.ts +318 -0
  107. package/src/side-thread-http-integration.test.ts +233 -0
  108. package/src/side-thread-http.ts +504 -0
  109. package/src/side-thread-production.test.ts +72 -0
  110. package/src/side-thread-production.ts +118 -0
  111. package/src/side-thread.test.ts +1067 -0
  112. package/src/side-thread.ts +2495 -0
  113. package/src/skillhub-http.test.ts +92 -0
  114. package/src/start-node.test.ts +109 -0
  115. package/src/stop-delete-node.test.ts +952 -0
  116. package/src/task-auth-origin-http.test.ts +91 -0
  117. package/src/task-auth-origin.test.ts +24 -0
  118. package/src/task-auth-origin.ts +7 -0
  119. package/src/task-consumption.test.ts +482 -0
  120. package/src/task-diagnostic-http.test.ts +149 -0
  121. package/src/task-diagnostic.test.ts +115 -0
  122. package/src/task-diagnostic.ts +118 -0
  123. package/src/task-idempotency-mcp.test.ts +142 -0
  124. package/src/task-idempotency.test.ts +60 -0
  125. package/src/task-idempotency.ts +75 -0
  126. package/src/task-lifecycle-watcher.test.ts +204 -0
  127. package/src/task-lifecycle-watcher.ts +49 -0
  128. package/src/task-network-resolution-http.test.ts +141 -0
  129. package/src/tasks-pagination-http.test.ts +141 -0
  130. package/src/tools-list-json-schema.test.ts +66 -0
  131. package/src/tools.ts +3231 -204
  132. package/src/update-provider.test.ts +65 -0
  133. package/src/upload-early-reject-guard.test.ts +155 -0
  134. package/src/uploads-http.test.ts +97 -15
  135. package/src/uploads.test.ts +66 -0
  136. package/src/uploads.ts +130 -5
  137. package/src/user-inbox-by-agent-http.test.ts +161 -0
  138. package/src/user-inbox-netid-notnull.test.ts +122 -0
  139. package/src/user-inbox-read-http.test.ts +223 -0
  140. package/src/user-inbox-schema.test.ts +54 -0
@@ -0,0 +1,202 @@
1
+ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { db } from "./db.js";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { registerTools } from "./tools.js";
5
+
6
+ // #498 compatibility warning remains load-bearing during the mixed-version
7
+ // rollout. Capability-negotiated agents use send_peer_reply; old send_reply
8
+ // callers targeting an agent get explicit migration guidance while Dashboard
9
+ // targets stay quiet.
10
+
11
+ const NET_ID = "net_498_warn";
12
+ const USER_ID = "u_498_warn";
13
+ const NODE_ID = "node_498_warn";
14
+ const AGENT_ALIAS = "peer-agent-498";
15
+ const DISPATCHER_NODE_ID = "node_498_dispatcher";
16
+ const DISPATCHER_ALIAS = "dispatcher";
17
+ const AGENT_TOK_ID = "tok_498_warn";
18
+
19
+ interface ToolHandler {
20
+ (args: any, extra?: any): Promise<{ content: Array<{ type: "text"; text: string }> }>;
21
+ }
22
+ interface Reply {
23
+ ok?: boolean;
24
+ message_id?: string;
25
+ session_status?: string;
26
+ warning?: string;
27
+ error?: string;
28
+ [k: string]: unknown;
29
+ }
30
+
31
+ function cleanup() {
32
+ try { db.run("DELETE FROM tasks WHERE network_id = ?1", [NET_ID]); } catch {}
33
+ try { db.run("DELETE FROM inbox WHERE network_id = ?1", [NET_ID]); } catch {}
34
+ try { db.run("DELETE FROM task_events WHERE network_id = ?1", [NET_ID]); } catch {}
35
+ try { db.run("DELETE FROM sessions WHERE network_id = ?1", [NET_ID]); } catch {}
36
+ try { db.run("DELETE FROM nodes WHERE network_id = ?1", [NET_ID]); } catch {}
37
+ try { db.run("DELETE FROM api_tokens WHERE network_id = ?1", [NET_ID]); } catch {}
38
+ try { db.run("DELETE FROM network_members WHERE network_id = ?1", [NET_ID]); } catch {}
39
+ try { db.run("DELETE FROM networks WHERE network_id = ?1", [NET_ID]); } catch {}
40
+ try { db.run("DELETE FROM users WHERE user_id = ?1", [USER_ID]); } catch {}
41
+ }
42
+
43
+ beforeEach(cleanup);
44
+ afterAll(cleanup);
45
+
46
+ function seed() {
47
+ db.run(
48
+ `INSERT INTO users (user_id, username, password_hash, role, created_at)
49
+ VALUES (?1, ?2, 'x', 'user', datetime('now'))`,
50
+ [USER_ID, USER_ID],
51
+ );
52
+ db.run(
53
+ `INSERT INTO nodes (node_id, node_name, alias, network_id, hostname, created_at, updated_at, lifecycle_state)
54
+ VALUES (?1, ?2, ?3, ?4, ?5, datetime('now'), datetime('now'), 'active')`,
55
+ [DISPATCHER_NODE_ID, DISPATCHER_ALIAS, DISPATCHER_ALIAS, NET_ID, `host-${DISPATCHER_ALIAS}`],
56
+ );
57
+ db.run(
58
+ `INSERT INTO networks (network_id, network_name, owner_id, created_at)
59
+ VALUES (?1, ?2, ?3, datetime('now'))`,
60
+ [NET_ID, NET_ID, USER_ID],
61
+ );
62
+ db.run(
63
+ `INSERT INTO sessions (resume_id, alias, status, node_id, network_id, updated_at, last_seen_at)
64
+ VALUES (?1, ?2, 'idle', ?3, ?4, datetime('now'), datetime('now'))`,
65
+ [`res_${DISPATCHER_ALIAS}`, DISPATCHER_ALIAS, DISPATCHER_NODE_ID, NET_ID],
66
+ );
67
+ db.run(
68
+ `INSERT INTO network_members (user_id, network_id, role, joined_at)
69
+ VALUES (?1, ?2, 'owner', datetime('now'))`,
70
+ [USER_ID, NET_ID],
71
+ );
72
+ db.run(
73
+ `INSERT INTO nodes (node_id, node_name, alias, network_id, hostname, created_at, updated_at, lifecycle_state)
74
+ VALUES (?1, ?2, ?3, ?4, ?5, datetime('now'), datetime('now'), 'active')`,
75
+ [NODE_ID, AGENT_ALIAS, AGENT_ALIAS, NET_ID, `host-${AGENT_ALIAS}`],
76
+ );
77
+ db.run(
78
+ `INSERT INTO sessions (resume_id, alias, status, node_id, network_id, updated_at, last_seen_at)
79
+ VALUES (?1, ?2, 'idle', ?3, ?4, datetime('now'), datetime('now'))`,
80
+ [`res_${AGENT_ALIAS}`, AGENT_ALIAS, NODE_ID, NET_ID],
81
+ );
82
+ db.run(
83
+ `INSERT INTO api_tokens (token_id, user_id, network_id, scope, name, token_hash, expires_at, revoked_at)
84
+ VALUES (?1, ?2, ?3, 'network', ?4, ?5, NULL, NULL)`,
85
+ [AGENT_TOK_ID, USER_ID, NET_ID, `node:${AGENT_ALIAS}`, `hash_${AGENT_TOK_ID}`],
86
+ );
87
+ }
88
+
89
+ function makeTask(from: string): string {
90
+ const id = "t_" + Math.random().toString(36).slice(2, 14);
91
+ db.run(
92
+ `INSERT INTO tasks (task_id, from_name, to_name, to_node_id, priority, status, content, requires_response, created_at, network_id)
93
+ VALUES (?1, ?2, ?3, ?4, 'normal', 'delivered', 'test', 'reply', datetime('now'), ?5)`,
94
+ [id, from, AGENT_ALIAS, NODE_ID, NET_ID],
95
+ );
96
+ return id;
97
+ }
98
+
99
+ function buildHandlers(): Record<string, ToolHandler> {
100
+ const server = new McpServer({ name: "test", version: "0" }) as any;
101
+ const tools: Record<string, ToolHandler> = {};
102
+ const origTool = server.tool.bind(server);
103
+ server.tool = (name: string, _desc: string, _schema: any, handler: ToolHandler) => {
104
+ tools[name] = handler;
105
+ return origTool(name, _desc, _schema, handler);
106
+ };
107
+ const origRegisterTool = server.registerTool?.bind(server);
108
+ if (origRegisterTool) {
109
+ server.registerTool = (name: string, _cfg: any, handler: ToolHandler) => {
110
+ tools[name] = handler;
111
+ return origRegisterTool(name, _cfg, handler);
112
+ };
113
+ }
114
+ // Bind as a network token whose caller alias is the sender.
115
+ registerTools(
116
+ server,
117
+ undefined,
118
+ /* enforceNetworkId */ NET_ID,
119
+ /* enforceUserId */ USER_ID,
120
+ /* callerAlias */ AGENT_ALIAS,
121
+ /* callerTokenIsNetwork */ true,
122
+ /* callerTokenId */ AGENT_TOK_ID,
123
+ );
124
+ return tools;
125
+ }
126
+
127
+ async function call(handler: ToolHandler, args: any): Promise<Reply> {
128
+ const r = await handler(args);
129
+ return JSON.parse(r.content[0].text) as Reply;
130
+ }
131
+
132
+ describe("send_reply legacy/Dashboard compatibility (#698)", () => {
133
+ test("legacy call still closes the original and enqueues one no-response result", async () => {
134
+ seed();
135
+ const tools = buildHandlers();
136
+ const sendReply = tools["send_reply"];
137
+ expect(sendReply).toBeDefined();
138
+
139
+ const taskId = makeTask(DISPATCHER_ALIAS);
140
+ const reply = await call(sendReply, {
141
+ alias: DISPATCHER_ALIAS,
142
+ text: "peer reply (atomic)",
143
+ in_reply_to: taskId,
144
+ status: "replied",
145
+ from_session: AGENT_ALIAS,
146
+ });
147
+
148
+ expect(reply.ok).toBe(true);
149
+ expect(reply.warning).toContain("commhub_send_peer_reply");
150
+ expect(reply.warning).toContain("terminalized the original task");
151
+ expect(reply.warning).not.toContain("commhub_send_task");
152
+ expect(reply.warning).toContain("mixed-version");
153
+ expect(db.get<{ status: string; result: string }>(
154
+ "SELECT status, result FROM tasks WHERE task_id = ?1", taskId,
155
+ )).toEqual(expect.objectContaining({ status: "replied", result: "peer reply (atomic)" }));
156
+ expect(db.all<{ type: string; requires_response: string; session_name: string }>(
157
+ "SELECT type, requires_response, session_name FROM inbox WHERE in_reply_to = ?1", taskId,
158
+ )).toEqual([{ type: "reply", requires_response: "none", session_name: DISPATCHER_ALIAS }]);
159
+ expect(db.get<{ n: number }>(
160
+ "SELECT COUNT(*) AS n FROM tasks WHERE parent_task_id = ?1", taskId,
161
+ )?.n).toBe(0);
162
+ });
163
+
164
+ test("target alias is `hub` (Dashboard path) → NO warning field", async () => {
165
+ seed();
166
+ const tools = buildHandlers();
167
+ const sendReply = tools["send_reply"];
168
+
169
+ const taskId = makeTask("hub");
170
+ const reply = await call(sendReply, {
171
+ alias: "hub",
172
+ text: "dashboard reply",
173
+ in_reply_to: taskId,
174
+ status: "replied",
175
+ from_session: AGENT_ALIAS,
176
+ });
177
+
178
+ expect(reply.ok).toBe(true);
179
+ // Silence-affirms the Dashboard path. Every reply carrying a
180
+ // warning would train the LLM to ignore it, so absence here is
181
+ // load-bearing.
182
+ expect(reply.warning).toBeUndefined();
183
+ });
184
+
185
+ test("target alias is `api` (Dashboard path) → NO warning field", async () => {
186
+ seed();
187
+ const tools = buildHandlers();
188
+ const sendReply = tools["send_reply"];
189
+
190
+ const taskId = makeTask("api");
191
+ const reply = await call(sendReply, {
192
+ alias: "api",
193
+ text: "dashboard reply via api",
194
+ in_reply_to: taskId,
195
+ status: "replied",
196
+ from_session: AGENT_ALIAS,
197
+ });
198
+
199
+ expect(reply.ok).toBe(true);
200
+ expect(reply.warning).toBeUndefined();
201
+ });
202
+ });
@@ -0,0 +1,131 @@
1
+ // #1085 — send_reply must not require `alias` when `in_reply_to` is set.
2
+ //
3
+ // Symptom (deployed SDK-runtime nodes): `mcp__commhub__send_reply(task_id,
4
+ // text)` → `-32602 Invalid input: expected string, received undefined at
5
+ // alias`. That is the HUB's Zod schema rejecting a required `alias` the
6
+ // client never sent — the request dies before the handler runs, so the
7
+ // original task is never terminalized and its inbox row stays unacked
8
+ // (pending inflation).
9
+ //
10
+ // Fix (two parts, both in tools.ts):
11
+ // 1. `replyToolSchema.alias` → optional (stops the MCP-layer Zod -32602).
12
+ // 2. `handleReply` derives the target from the in_reply_to task's
13
+ // `from_name` when alias is omitted — the target was always knowable
14
+ // from the task the hub already looks up, so the param was redundant.
15
+ //
16
+ // This suite drives the registered handler directly (MCP Zod layer is
17
+ // exercised separately by the framework), so it pins part (2): omit alias
18
+ // → derive → reply lands on the original sender; omit both → clean error.
19
+ //
20
+ // 改坏报红 gate: revert the derive block (require alias again) and
21
+ // "alias omitted → derives to original sender" flips to a resolve failure.
22
+
23
+ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
24
+ import { db } from "./db.js";
25
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26
+ import { registerTools } from "./tools.js";
27
+
28
+ const NET_ID = "net_1085";
29
+ const USER_ID = "u_1085";
30
+ const NODE_ID = "node_1085";
31
+ const AGENT_ALIAS = "worker-1085";
32
+ const ORIGIN_ALIAS = "boss-1085"; // the task's from_name — the derive target
33
+ const AGENT_TOK_ID = "tok_1085";
34
+
35
+ interface ToolHandler {
36
+ (args: any, extra?: any): Promise<{ content: Array<{ type: "text"; text: string }> }>;
37
+ }
38
+ interface Reply { ok?: boolean; message_id?: string; error?: string; message?: string; [k: string]: unknown; }
39
+
40
+ function cleanup() {
41
+ for (const t of ["tasks", "inbox", "task_events", "sessions", "nodes", "api_tokens", "network_members", "networks"]) {
42
+ try { db.run(`DELETE FROM ${t} WHERE network_id = ?1`, [NET_ID]); } catch {}
43
+ }
44
+ try { db.run("DELETE FROM users WHERE user_id = ?1", [USER_ID]); } catch {}
45
+ }
46
+ beforeEach(cleanup);
47
+ afterAll(cleanup);
48
+
49
+ function seed(): { taskId: string } {
50
+ db.run(`INSERT INTO users (user_id, username, password_hash, role, created_at) VALUES (?1, ?2, 'x', 'user', datetime('now'))`, [USER_ID, USER_ID]);
51
+ db.run(`INSERT INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?2, ?3, datetime('now'))`, [NET_ID, NET_ID, USER_ID]);
52
+ db.run(`INSERT INTO network_members (user_id, network_id, role, joined_at) VALUES (?1, ?2, 'owner', datetime('now'))`, [USER_ID, NET_ID]);
53
+ db.run(`INSERT INTO nodes (node_id, node_name, alias, network_id, hostname, created_at, updated_at, lifecycle_state) VALUES (?1, ?2, ?3, ?4, ?5, datetime('now'), datetime('now'), 'active')`, [NODE_ID, AGENT_ALIAS, AGENT_ALIAS, NET_ID, `host-${AGENT_ALIAS}`]);
54
+ db.run(`INSERT INTO sessions (resume_id, alias, status, node_id, network_id, updated_at, last_seen_at) VALUES (?1, ?2, 'idle', ?3, ?4, datetime('now'), datetime('now'))`, [`res_${AGENT_ALIAS}`, AGENT_ALIAS, NODE_ID, NET_ID]);
55
+ // The reply TARGET session (the task originator). Its alias is what the
56
+ // handler must derive from the task's from_name.
57
+ db.run(`INSERT INTO sessions (resume_id, alias, status, network_id, updated_at, last_seen_at) VALUES (?1, ?2, 'idle', ?3, datetime('now'), datetime('now'))`, [`res_${ORIGIN_ALIAS}`, ORIGIN_ALIAS, NET_ID]);
58
+ // A task from ORIGIN_ALIAS → the worker. Replying to it should land on
59
+ // ORIGIN_ALIAS even when the caller omits alias.
60
+ const taskId = `task_1085_${Date.now()}${Math.floor(Math.random() * 1000)}`;
61
+ db.run(`INSERT INTO tasks (task_id, from_name, to_name, status, content, requires_response, created_at, delivered_at, expires_at, network_id, priority) VALUES (?1, ?2, ?3, 'delivered', 'do the thing', 'reply', datetime('now'), datetime('now'), datetime('now', '+1 hour'), ?4, 'normal')`, [taskId, ORIGIN_ALIAS, AGENT_ALIAS, NET_ID]);
62
+ return { taskId };
63
+ }
64
+
65
+ async function getHandler(): Promise<ToolHandler> {
66
+ const server = new McpServer({ name: "test", version: "0" });
67
+ const tools: Record<string, ToolHandler> = {};
68
+ const originalTool = (server as any).tool.bind(server);
69
+ (server as any).tool = (name: string, ...rest: any[]) => {
70
+ const handler = rest[rest.length - 1];
71
+ if (typeof handler === "function") tools[name] = handler;
72
+ return originalTool(name, ...rest);
73
+ };
74
+ registerTools(server, {
75
+ enforceNetworkId: NET_ID,
76
+ enforceUserId: USER_ID,
77
+ callerAlias: AGENT_ALIAS,
78
+ callerTokenIsNetwork: true,
79
+ callerTokenId: AGENT_TOK_ID,
80
+ } as any);
81
+ const handler = tools.send_reply;
82
+ if (!handler) throw new Error("send_reply handler not registered");
83
+ return handler;
84
+ }
85
+
86
+ async function call(h: ToolHandler, args: any): Promise<Reply> {
87
+ return JSON.parse((await h(args)).content[0].text) as Reply;
88
+ }
89
+
90
+ describe("#1085 send_reply — alias optional, derived from in_reply_to", () => {
91
+ test("alias OMITTED + in_reply_to set → derives target from task from_name, reply lands on originator", async () => {
92
+ const { taskId } = seed();
93
+ const h = await getHandler();
94
+ const r = await call(h, { in_reply_to: taskId, text: "done", status: "replied", from_session: AGENT_ALIAS });
95
+ expect(r.ok).toBe(true);
96
+ // The reply inbox row must be addressed to the derived originator.
97
+ const row = db.get<{ session_name: string; in_reply_to: string }>(
98
+ "SELECT session_name, in_reply_to FROM inbox WHERE id = ?1", [r.message_id],
99
+ );
100
+ expect(row?.session_name).toBe(ORIGIN_ALIAS);
101
+ expect(row?.in_reply_to).toBe(taskId);
102
+ // And the original task is terminalized (no longer pending).
103
+ const task = db.get<{ status: string }>("SELECT status FROM tasks WHERE task_id = ?1", [taskId]);
104
+ expect(task?.status).toBe("replied");
105
+ });
106
+
107
+ test("alias EXPLICIT still works (unchanged path)", async () => {
108
+ const { taskId } = seed();
109
+ const h = await getHandler();
110
+ const r = await call(h, { alias: ORIGIN_ALIAS, in_reply_to: taskId, text: "done", from_session: AGENT_ALIAS });
111
+ expect(r.ok).toBe(true);
112
+ const row = db.get<{ session_name: string }>("SELECT session_name FROM inbox WHERE id = ?1", [r.message_id]);
113
+ expect(row?.session_name).toBe(ORIGIN_ALIAS);
114
+ });
115
+
116
+ test("alias OMITTED + no in_reply_to → clean error, not a crash", async () => {
117
+ seed();
118
+ const h = await getHandler();
119
+ const r = await call(h, { text: "orphan reply", from_session: AGENT_ALIAS });
120
+ expect(r.ok).toBe(false);
121
+ expect(r.error).toBe("reply_target_required");
122
+ });
123
+
124
+ test("alias OMITTED + in_reply_to points at nonexistent task → resolve error", async () => {
125
+ seed();
126
+ const h = await getHandler();
127
+ const r = await call(h, { in_reply_to: "task_does_not_exist_1085", text: "x", from_session: AGENT_ALIAS });
128
+ expect(r.ok).toBe(false);
129
+ expect(r.error).toBe("reply_target_unresolved");
130
+ });
131
+ });