@sleep2agi/commhub-server 0.9.0-preview.14 → 0.9.0-preview.140

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 (341) hide show
  1. package/README.md +130 -10
  2. package/bin/commhub.ts +99 -11
  3. package/package.json +7 -7
  4. package/src/ack-create-request-transport.test.ts +182 -0
  5. package/src/ack-create-request.test.ts +50 -7
  6. package/src/admin-delete-network-http.test.ts +228 -0
  7. package/src/admin-networks-http.test.ts +88 -0
  8. package/src/adoption-candidates-http.test.ts +137 -0
  9. package/src/adoption-candidates.ts +158 -0
  10. package/src/agent-access-batch.test.ts +210 -0
  11. package/src/agent-access.ts +487 -0
  12. package/src/agent-acl-groups-http.test.ts +247 -0
  13. package/src/agent-acl-http.test.ts +659 -0
  14. package/src/agent-acl-slice3-http.test.ts +267 -0
  15. package/src/agent-department-http.test.ts +148 -0
  16. package/src/agent-groups-perf.test.ts +223 -0
  17. package/src/agent-notice.ts +48 -0
  18. package/src/agent-team-whoami-http.test.ts +98 -0
  19. package/src/agent-teams-http.test.ts +323 -0
  20. package/src/agent-teams.ts +244 -0
  21. package/src/alias-filter.test.ts +72 -0
  22. package/src/alias-filter.ts +42 -0
  23. package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
  24. package/src/api-host-supervisors-fallback.test.ts +271 -0
  25. package/src/api-host-supervisors-runtime-readiness.test.ts +195 -0
  26. package/src/api-nodes-shape.test.ts +26 -10
  27. package/src/auth-password-change-cli-tokens-http.test.ts +423 -0
  28. package/src/auth-sessions-http.test.ts +250 -0
  29. package/src/auth-validate.test.ts +5 -1
  30. package/src/auth-weak-password-login.test.ts +176 -0
  31. package/src/auth.ts +604 -51
  32. package/src/avatar-validate.test.ts +163 -0
  33. package/src/avatar-validate.ts +103 -0
  34. package/src/binding-generation-http.test.ts +121 -0
  35. package/src/blocked-exit-on-reply.test.ts +122 -0
  36. package/src/boot-side-effect.test.ts +124 -0
  37. package/src/chain-attempt-dedup.test.ts +447 -0
  38. package/src/clone-node.test.ts +248 -0
  39. package/src/clone-node.ts +289 -0
  40. package/src/codex-config-flags.ts +77 -0
  41. package/src/codex-fork-contract.ts +23 -0
  42. package/src/codex-fork-migration.test.ts +38 -0
  43. package/src/commhub-bin-args.test.ts +85 -0
  44. package/src/config-apply-sec1.test.ts +107 -0
  45. package/src/config-apply-validate.test.ts +134 -1
  46. package/src/config-apply-validate.ts +111 -5
  47. package/src/create-node-blocked-gate.test.ts +356 -0
  48. package/src/create-node-network-id.test.ts +79 -0
  49. package/src/create-node-tool-schema.test.ts +49 -0
  50. package/src/create-node-unicode-name.test.ts +120 -0
  51. package/src/create-node-validate.test.ts +199 -7
  52. package/src/create-node-validate.ts +232 -15
  53. package/src/create-node-workdir.test.ts +289 -0
  54. package/src/create-node.ts +88 -23
  55. package/src/daemon-provider-snapshots.test.ts +421 -0
  56. package/src/daemon-provider-snapshots.ts +327 -0
  57. package/src/daemon-token-owner-http.test.ts +236 -0
  58. package/src/dashboard-slash-routing-http.test.ts +109 -0
  59. package/src/db-adapter-guard.test.ts +148 -0
  60. package/src/db-adapter-translate.test.ts +128 -0
  61. package/src/db-adapter.ts +323 -87
  62. package/src/db-timestamp.test.ts +38 -0
  63. package/src/db-timestamp.ts +15 -0
  64. package/src/db.ts +1111 -42
  65. package/src/delivered-without-push.test.ts +140 -0
  66. package/src/department-groups-http.test.ts +209 -0
  67. package/src/department-groups-sync-http.test.ts +328 -0
  68. package/src/department-groups.ts +434 -0
  69. package/src/department-heads-http.test.ts +330 -0
  70. package/src/department-heads.ts +226 -0
  71. package/src/departments-http.test.ts +185 -0
  72. package/src/departments.ts +276 -0
  73. package/src/desktop-message-seam.test.ts +150 -0
  74. package/src/desktop-message.test.ts +368 -0
  75. package/src/dm-file-scope-http.test.ts +178 -0
  76. package/src/env-transport-gate-http.test.ts +146 -0
  77. package/src/env-transport-gate.ts +74 -0
  78. package/src/env-transport-policy.ts +21 -0
  79. package/src/ext-token-shared.test.ts +60 -0
  80. package/src/ext-validation-regression.test.ts +131 -0
  81. package/src/external-schedule-edits-http.test.ts +321 -0
  82. package/src/external-schedule-edits-timestamps.test.ts +64 -0
  83. package/src/external-schedule-edits.ts +327 -0
  84. package/src/file-download-authz-dev-open.test.ts +135 -0
  85. package/src/file-download-authz.test.ts +404 -0
  86. package/src/file-download-date-bucket.test.ts +436 -0
  87. package/src/file-network-scope-dev-open.test.ts +97 -0
  88. package/src/file-network-scope.test.ts +732 -0
  89. package/src/group-membership-events-http.test.ts +264 -0
  90. package/src/group-messages-http.test.ts +482 -0
  91. package/src/group-messages.ts +297 -0
  92. package/src/health-redaction.test.ts +642 -0
  93. package/src/host-supervisor-identity-http.test.ts +152 -0
  94. package/src/host-supervisor-identity.ts +92 -0
  95. package/src/http-gzip.test.ts +94 -0
  96. package/src/http-gzip.ts +112 -0
  97. package/src/hub-catalog-read-http.test.ts +187 -0
  98. package/src/hub-provider-config.test.ts +139 -0
  99. package/src/hub-provider-config.ts +105 -0
  100. package/src/hub-timestamp.test.ts +38 -0
  101. package/src/hub-timestamp.ts +27 -0
  102. package/src/human-dm.ts +215 -0
  103. package/src/human-inbox.ts +42 -0
  104. package/src/inbox-caller.ts +49 -0
  105. package/src/inbox-count-sse.test.ts +310 -0
  106. package/src/inbox-count.ts +29 -0
  107. package/src/index.ts +15 -2362
  108. package/src/lifecycle-guard.ts +1 -1
  109. package/src/list-host-supervisors.test.ts +5 -4
  110. package/src/mcp-context-cost-http.test.ts +123 -0
  111. package/src/mcp-projects-events-http.test.ts +162 -0
  112. package/src/mcp-route-label-http.test.ts +120 -0
  113. package/src/mcp-write-network-resolution.test.ts +432 -0
  114. package/src/member-audit-network-id-http.test.ts +93 -0
  115. package/src/member-presence-http.test.ts +210 -0
  116. package/src/messages-ack-by-agent-http.test.ts +142 -0
  117. package/src/migrate-sqlite-to-pg.test.ts +47 -0
  118. package/src/migrate-sqlite-to-pg.ts +263 -0
  119. package/src/model-auth-notify-http.test.ts +181 -0
  120. package/src/model-auth-notify.ts +116 -0
  121. package/src/network-name-http.test.ts +75 -0
  122. package/src/network-scope.ts +276 -0
  123. package/src/network-token-membership-http.test.ts +139 -0
  124. package/src/node-alias-control.ts +12 -0
  125. package/src/node-attrs-validate.ts +88 -0
  126. package/src/node-attrs.test.ts +217 -0
  127. package/src/node-caller-identity-http.test.ts +1083 -0
  128. package/src/node-create-request-status-http.test.ts +105 -0
  129. package/src/node-daemon-bindings.test.ts +132 -0
  130. package/src/node-daemon-bindings.ts +135 -0
  131. package/src/node-files-transport.test.ts +344 -0
  132. package/src/node-health-codex-login.test.ts +120 -0
  133. package/src/node-health-dispatch-http.test.ts +279 -0
  134. package/src/node-health-guard.ts +105 -0
  135. package/src/node-health-passthrough.test.ts +115 -0
  136. package/src/node-health-store.ts +101 -0
  137. package/src/node-id-alias.test.ts +190 -0
  138. package/src/node-identity-conflict-http.test.ts +269 -0
  139. package/src/node-identity-conflict.ts +142 -0
  140. package/src/node-identity-mismatch.test.ts +74 -0
  141. package/src/node-lifecycle-controllable.test.ts +64 -0
  142. package/src/node-lifecycle-controllable.ts +53 -0
  143. package/src/node-lifecycle-read-http.test.ts +245 -0
  144. package/src/node-lifecycle-read.ts +121 -0
  145. package/src/node-logs-transport.test.ts +263 -0
  146. package/src/node-permissions-http.test.ts +276 -0
  147. package/src/node-permissions.ts +285 -0
  148. package/src/node-request-retention.test.ts +281 -0
  149. package/src/node-request-retention.ts +120 -0
  150. package/src/node-secret-rest-http.test.ts +190 -0
  151. package/src/node-skills-transport.test.ts +209 -0
  152. package/src/node-token-identity.ts +35 -0
  153. package/src/node-token-ownership.ts +51 -0
  154. package/src/node-token-privilege-http.test.ts +158 -0
  155. package/src/observer-avatar-http.test.ts +442 -0
  156. package/src/observer-push.test.ts +252 -0
  157. package/src/opencode-v2-catalog.test.ts +268 -0
  158. package/src/opencode-v2-catalog.ts +352 -0
  159. package/src/package-version-consistency.test.ts +25 -0
  160. package/src/password-network-tokens.ts +25 -0
  161. package/src/peer-reply-atomic.test.ts +455 -0
  162. package/src/peer-reply-capability.test.ts +70 -0
  163. package/src/pg-worker.ts +87 -0
  164. package/src/project-dir-warn-clear-http.test.ts +132 -0
  165. package/src/project-viewer-can-http.test.ts +114 -0
  166. package/src/push.ts +429 -10
  167. package/src/redact-tokens.test.ts +65 -0
  168. package/src/redact-tokens.ts +40 -0
  169. package/src/rename.ts +38 -10
  170. package/src/reply-target-derivation.test.ts +96 -0
  171. package/src/report-status-handover-capabilities-http.test.ts +124 -0
  172. package/src/report-status-host-schema.test.ts +235 -0
  173. package/src/report-status-resume-id-handover.test.ts +184 -0
  174. package/src/report-status-secret-keys.test.ts +140 -0
  175. package/src/require-explicit-test-db.ts +9 -0
  176. package/src/requirement-due-reminders-http.test.ts +620 -0
  177. package/src/requirement-due-reminders.ts +416 -0
  178. package/src/requirement-events-http.test.ts +268 -0
  179. package/src/requirement-events.test.ts +45 -0
  180. package/src/requirement-events.ts +144 -0
  181. package/src/requirement-notify.ts +165 -0
  182. package/src/requirement-participant-notify-http.test.ts +309 -0
  183. package/src/requirement-tag-ops-http.test.ts +266 -0
  184. package/src/requirement-tags.ts +89 -0
  185. package/src/requirements-abandoned-http.test.ts +236 -0
  186. package/src/requirements-comment-http.test.ts +167 -0
  187. package/src/requirements-completed-at-migration.test.ts +70 -0
  188. package/src/requirements-errors-mcp-http.test.ts +164 -0
  189. package/src/requirements-errors.ts +143 -0
  190. package/src/requirements-etag-http.test.ts +91 -0
  191. package/src/requirements-http.test.ts +709 -0
  192. package/src/requirements-last-event-http.test.ts +349 -0
  193. package/src/requirements-list-mcp-slim-http.test.ts +155 -0
  194. package/src/requirements-list-slim-http.test.ts +319 -0
  195. package/src/requirements-mcp.test.ts +163 -0
  196. package/src/requirements-migrate.ts +259 -0
  197. package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
  198. package/src/requirements-people-http.test.ts +114 -0
  199. package/src/requirements-people-mcp-http.test.ts +225 -0
  200. package/src/requirements-people.ts +131 -0
  201. package/src/requirements-priority-lowest.test.ts +114 -0
  202. package/src/requirements-search-http.test.ts +182 -0
  203. package/src/requirements-search.test.ts +32 -0
  204. package/src/requirements-search.ts +145 -0
  205. package/src/requirements-seq-http.test.ts +193 -0
  206. package/src/requirements-seq-migration.test.ts +63 -0
  207. package/src/requirements-start-date.test.ts +125 -0
  208. package/src/requirements-stats-http.test.ts +357 -0
  209. package/src/requirements-stats.ts +195 -0
  210. package/src/requirements-status-participants-http.test.ts +222 -0
  211. package/src/requirements-upgrade-e2e.test.ts +126 -0
  212. package/src/requirements-upgrade-migration.test.ts +98 -0
  213. package/src/requirements.ts +1806 -0
  214. package/src/resolve-port.test.ts +57 -0
  215. package/src/resolve-port.ts +44 -0
  216. package/src/response-charset.test.ts +130 -0
  217. package/src/rest-explicit-columns-http.test.ts +222 -0
  218. package/src/rest-identity.test.ts +130 -0
  219. package/src/rest-identity.ts +141 -0
  220. package/src/rest-projections.ts +70 -0
  221. package/src/rest-write-network-resolution.test.ts +133 -0
  222. package/src/restricted-files.ts +76 -0
  223. package/src/retention.ts +15 -3
  224. package/src/route-callers-http.test.ts +120 -0
  225. package/src/route-timing.test.ts +211 -0
  226. package/src/route-timing.ts +258 -0
  227. package/src/rules-file-duplicate-alias.test.ts +234 -0
  228. package/src/rules-file-session-target.test.ts +207 -0
  229. package/src/rules-file-transport.test.ts +237 -0
  230. package/src/runtime-label.test.ts +22 -0
  231. package/src/runtime-label.ts +21 -0
  232. package/src/runtime-readiness.ts +90 -0
  233. package/src/schedule-agent-mcp-http.test.ts +381 -0
  234. package/src/schedule-agent.ts +192 -0
  235. package/src/scheduled-backoff-http.test.ts +193 -0
  236. package/src/scheduled-backoff.ts +64 -0
  237. package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
  238. package/src/scheduled-failures-http.test.ts +233 -0
  239. package/src/scheduled-failures.ts +179 -0
  240. package/src/scheduled-offline.test.ts +131 -0
  241. package/src/scheduled-offline.ts +66 -0
  242. package/src/scheduled-outcome-guidance-http.test.ts +139 -0
  243. package/src/scheduled-outcome-guidance.ts +9 -0
  244. package/src/scheduled-reply-unread-http.test.ts +212 -0
  245. package/src/scheduled-requirement-wake-http.test.ts +264 -0
  246. package/src/scheduled-requirement-wake.ts +125 -0
  247. package/src/scheduled-run-terminal.test.ts +268 -0
  248. package/src/scheduled-skip-blocking-task.test.ts +120 -0
  249. package/src/scheduled-stuck-http.test.ts +312 -0
  250. package/src/scheduled-stuck.ts +186 -0
  251. package/src/scheduled-tasks-http.test.ts +638 -0
  252. package/src/scheduled-tasks.ts +819 -0
  253. package/src/scheduled-work-outcome-http.test.ts +154 -0
  254. package/src/scheduled-work-outcome.ts +50 -0
  255. package/src/scoped-secrets.ts +139 -0
  256. package/src/send-dedup-request-id.test.ts +229 -0
  257. package/src/send-reply-agent-warning.test.ts +202 -0
  258. package/src/send-reply-alias-optional.test.ts +131 -0
  259. package/src/send-reply-attachments.test.ts +458 -0
  260. package/src/send_dedup.test.ts +54 -0
  261. package/src/send_dedup.ts +52 -6
  262. package/src/serve-error.test.ts +38 -0
  263. package/src/serve-error.ts +27 -0
  264. package/src/server.ts +5398 -0
  265. package/src/session-task-on-dispatch.ts +14 -0
  266. package/src/session-token-ownership-http.test.ts +122 -0
  267. package/src/shared/codex-inventory.ts +41 -0
  268. package/src/shared/daemon-provider-login.ts +31 -0
  269. package/src/shared/daemon-provider-snapshot.ts +52 -0
  270. package/src/shared/external-schedule-contract.ts +60 -0
  271. package/src/shared/node-name.test.ts +62 -0
  272. package/src/shared/node-name.ts +161 -0
  273. package/src/shared/provider-application.ts +29 -0
  274. package/src/shared/provider-collection.ts +14 -0
  275. package/src/shared/provider-initialization.ts +8 -0
  276. package/src/shared/reserved-env-drift.test.ts +10 -0
  277. package/src/shared/scoped-secret-contract.ts +5 -0
  278. package/src/shared/task-content-limit-drift.test.ts +28 -0
  279. package/src/shared/task-content-limit.ts +25 -0
  280. package/src/side-thread-command-transport.test.ts +312 -0
  281. package/src/side-thread-command-transport.ts +318 -0
  282. package/src/side-thread-http-integration.test.ts +233 -0
  283. package/src/side-thread-http.ts +504 -0
  284. package/src/side-thread-production.test.ts +72 -0
  285. package/src/side-thread-production.ts +118 -0
  286. package/src/side-thread.test.ts +1067 -0
  287. package/src/side-thread.ts +2499 -0
  288. package/src/skillhub-http.test.ts +108 -0
  289. package/src/sqlite-close-checkpoint.test.ts +78 -0
  290. package/src/start-node.test.ts +264 -0
  291. package/src/status-alias-filter-http.test.ts +106 -0
  292. package/src/status-alias-resolver-http.test.ts +163 -0
  293. package/src/status-alias-resolver.ts +19 -0
  294. package/src/status-node-id-filter-http.test.ts +161 -0
  295. package/src/status-read-cache-http.test.ts +229 -0
  296. package/src/status-read-cache.ts +122 -0
  297. package/src/stop-delete-node.test.ts +509 -2
  298. package/src/task-access-http.test.ts +402 -0
  299. package/src/task-access.ts +420 -0
  300. package/src/task-activity.ts +60 -0
  301. package/src/task-auth-origin-http.test.ts +93 -0
  302. package/src/task-auth-origin.test.ts +24 -0
  303. package/src/task-auth-origin.ts +7 -0
  304. package/src/task-consumption.test.ts +482 -0
  305. package/src/task-diagnostic-http.test.ts +149 -0
  306. package/src/task-diagnostic.test.ts +115 -0
  307. package/src/task-diagnostic.ts +118 -0
  308. package/src/task-expiry-notice-http.test.ts +287 -0
  309. package/src/task-expiry-notice.ts +315 -0
  310. package/src/task-idempotency-mcp.test.ts +142 -0
  311. package/src/task-idempotency.test.ts +60 -0
  312. package/src/task-idempotency.ts +75 -0
  313. package/src/task-late-replies.ts +54 -0
  314. package/src/task-late-reply-http.test.ts +152 -0
  315. package/src/task-lifecycle-watcher.test.ts +204 -0
  316. package/src/task-lifecycle-watcher.ts +49 -0
  317. package/src/task-network-resolution-http.test.ts +140 -0
  318. package/src/task-orphan-http.test.ts +287 -0
  319. package/src/task-orphan.ts +119 -0
  320. package/src/task-queue-ahead-http.test.ts +309 -0
  321. package/src/task-queue-ahead-scale.test.ts +236 -0
  322. package/src/task-queue-ahead.ts +186 -0
  323. package/src/task-rejection-log-http.test.ts +160 -0
  324. package/src/task-rejection-log.ts +49 -0
  325. package/src/task-sender-http.test.ts +143 -0
  326. package/src/task-stale-open-http.test.ts +290 -0
  327. package/src/task-stale-open.ts +103 -0
  328. package/src/tasks-pagination-http.test.ts +141 -0
  329. package/src/tool-audience-http.test.ts +220 -0
  330. package/src/tool-audience.ts +118 -0
  331. package/src/tools-list-json-schema.test.ts +67 -0
  332. package/src/tools.ts +4008 -355
  333. package/src/upload-early-reject-guard.test.ts +155 -0
  334. package/src/uploads-http.test.ts +97 -15
  335. package/src/uploads.ts +103 -6
  336. package/src/user-avatar-http.test.ts +74 -0
  337. package/src/user-avatar-migration.test.ts +25 -0
  338. package/src/user-inbox-by-agent-http.test.ts +161 -0
  339. package/src/user-inbox-netid-notnull.test.ts +122 -0
  340. package/src/user-inbox-read-http.test.ts +223 -0
  341. package/src/user-inbox-schema.test.ts +54 -0
@@ -0,0 +1,447 @@
1
+ // Node-TMAI#4 — auto-chain: attempt identity, terminal-parent safety,
2
+ // dispatcher participation.
3
+ //
4
+ // This file is the trimmed re-implementation of PR #1940 on current main. The
5
+ // original candidate carried four coupled behavioural changes; two of them
6
+ // contradict decisions this repository made AFTER that candidate was written,
7
+ // so they are deliberately NOT implemented here:
8
+ //
9
+ // * C — "an `acked` task must expire on its deadline". #519 (task-stale-open.ts)
10
+ // owns this convergence and made it **activity-based and default-off** on
11
+ // purpose: a Hub-wide TTL sweep of acked rows would close production history
12
+ // the moment it ships. `patrolExpiredTasks` therefore keeps
13
+ // `status IN ('created','delivered')`, and task-stale-open-http.test.ts pins
14
+ // that an old acked row stays open while #519 is disabled.
15
+ //
16
+ // * D(parent row) — "a child result must never complete its parent". #690
17
+ // (scheduled-run-terminal.test.ts) is built on the opposite contract: a
18
+ // scheduled/dispatched parent whose executor answers through a child must
19
+ // reach a terminal state and carry its scheduler-run mirror with it.
20
+ //
21
+ // * A — "never infer parent_task_id". #668 restored the inference on purpose
22
+ // (`send_task` would otherwise lose the chain whenever a node forgets the id)
23
+ // and narrowed it to `delivered` only.
24
+ //
25
+ // What IS implemented here, because it is a defect on current main and conflicts
26
+ // with nothing:
27
+ //
28
+ // T) A TERMINAL parent used to be rewritten: its `result` was appended and its
29
+ // `completed_at` moved forward long after it finished (observed: 22 minutes
30
+ // later). The child result is still preserved as an audit row plus a notice
31
+ // to the parent's author, but the parent row now stays byte-identical.
32
+ // O) An OPEN parent is still completed by the chain — that is the #690
33
+ // contract, pinned below so a future edit cannot quietly drop it.
34
+ // R) One ATTEMPT of a child result is recorded against a parent at most once:
35
+ // a replayed reply no longer fans out into a second audit row and a second
36
+ // notice.
37
+ // N) A fresh attempt of the same task — a legal retry, identified by its inbox
38
+ // transport row — still gets through, even when both attempts complete in
39
+ // the same second (`completed_at` has second granularity).
40
+ // E) The de-dup key binds the ORIGIN of the result, not the walk position, so
41
+ // a descendant notice cannot consume an ancestor's own completion slot.
42
+ // P) A same-network caller may not adopt an unrelated third party's task as
43
+ // its parent (`parent_not_participant`).
44
+
45
+ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
46
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
47
+ import { db, chainReplyToParent, uuidv4 } from "./db.js";
48
+ import { registerTools } from "./tools.js";
49
+
50
+ const NET = "net_cpt_dedup";
51
+ const USER = "u_cpt_dedup";
52
+ const PEER = "cpt-peer"; // the session dispatching work outward
53
+ const TARGET = "cpt-target"; // the session receiving the dispatch
54
+ const BOSS = "cpt-boss"; // the originator who reads the answers
55
+ const THIRD = "cpt-third"; // a third party with no involvement
56
+
57
+ type Handler = (args: any, extra?: any) => Promise<{ content: Array<{ type: "text"; text: string }> }>;
58
+
59
+ function cleanup(): void {
60
+ for (const table of ["task_events", "inbox", "scheduled_task_runs", "scheduled_tasks", "tasks", "sessions", "nodes", "network_members", "networks"]) {
61
+ try { db.run(`DELETE FROM ${table} WHERE network_id = ?1`, [NET]); } catch {}
62
+ }
63
+ try { db.run("DELETE FROM users WHERE user_id = ?1", [USER]); } catch {}
64
+ }
65
+
66
+ function seed(): void {
67
+ db.run("INSERT INTO users (user_id, username, password_hash, role, created_at) VALUES (?1, ?2, 'x', 'user', datetime('now'))", [USER, USER]);
68
+ db.run("INSERT INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?1, ?2, datetime('now'))", [NET, USER]);
69
+ db.run("INSERT INTO network_members (user_id, network_id, role, joined_at) VALUES (?1, ?2, 'owner', datetime('now'))", [USER, NET]);
70
+ for (const alias of [PEER, TARGET, BOSS, THIRD]) {
71
+ db.run(
72
+ `INSERT INTO nodes (node_id, node_name, alias, network_id, lifecycle_state)
73
+ VALUES (?1, ?2, ?2, ?3, 'active')`,
74
+ [`n_${alias}`, alias, NET],
75
+ );
76
+ db.run(
77
+ `INSERT INTO sessions (resume_id, alias, status, node_id, network_id, updated_at, last_seen_at)
78
+ VALUES (?1, ?2, 'idle', ?3, ?4, datetime('now'), datetime('now'))`,
79
+ [`r_${alias}`, alias, `n_${alias}`, NET],
80
+ );
81
+ }
82
+ }
83
+
84
+ function handlers(): Record<string, Handler> {
85
+ const server = new McpServer({ name: "test-cpt-dedup", version: "0" }) as any;
86
+ const out: Record<string, Handler> = {};
87
+ const original = server.tool.bind(server);
88
+ server.tool = (name: string, ...args: any[]) => {
89
+ const h = args.at(-1);
90
+ if (typeof h === "function") out[name] = h;
91
+ return original(name, ...args);
92
+ };
93
+ registerTools(server, undefined, NET, USER, PEER, false, null);
94
+ return out;
95
+ }
96
+
97
+ async function call(handler: Handler, args: any): Promise<any> {
98
+ const r = await handler(args);
99
+ return JSON.parse(r.content[0].text);
100
+ }
101
+
102
+ function insertTask(o: {
103
+ task_id?: string;
104
+ from_name: string;
105
+ to_name: string;
106
+ status: string;
107
+ content: string;
108
+ parent_task_id?: string | null;
109
+ result?: string | null;
110
+ completed_at?: string | null;
111
+ expires_at?: string | null;
112
+ }): string {
113
+ const id = o.task_id ?? uuidv4();
114
+ db.run(
115
+ `INSERT INTO tasks
116
+ (task_id, from_name, to_name, priority, status, content, requires_response,
117
+ created_at, network_id, parent_task_id, result, completed_at, expires_at)
118
+ VALUES (?1, ?2, ?3, 'normal', ?4, ?5, 'reply', datetime('now'), ?6, ?7, ?8, ?9, ?10)`,
119
+ [id, o.from_name, o.to_name, o.status, o.content, NET, o.parent_task_id ?? null, o.result ?? null, o.completed_at ?? null, o.expires_at ?? null],
120
+ );
121
+ return id;
122
+ }
123
+
124
+ /** A dispatch is identified by its inbox transport row — the same identity the
125
+ * chain uses as its attempt key. Tests that want a "fresh attempt" insert one. */
126
+ function insertTransport(taskId: string, to = TARGET, body = "dispatch"): string {
127
+ const id = `tr_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
128
+ db.run(
129
+ `INSERT INTO inbox (id, session_name, type, priority, content, from_session, task_id, requires_response, network_id)
130
+ VALUES (?1, ?2, 'task', 'normal', ?3, ?4, ?5, 'reply', ?6)`,
131
+ [id, to, body, PEER, taskId, NET],
132
+ );
133
+ return id;
134
+ }
135
+
136
+ const taskById = (id: string) =>
137
+ db.get<{ status: string; result: string | null; completed_at: string | null; parent_task_id: string | null }>(
138
+ "SELECT status, result, completed_at, parent_task_id FROM tasks WHERE task_id = ?1", id);
139
+
140
+ /** Audit rows this path appends to a parent (either shape). */
141
+ const chainEvents = (parentId: string) =>
142
+ db.get<{ c: number }>(
143
+ "SELECT COUNT(*) AS c FROM task_events WHERE task_id = ?1 AND actor IN ('auto-chain','auto-chain-append')", parentId)?.c ?? 0;
144
+
145
+ const childNotices = (childText: string) =>
146
+ db.get<{ c: number }>(
147
+ "SELECT COUNT(*) AS c FROM inbox WHERE content LIKE ?1", `%${childText}%`)?.c ?? 0;
148
+
149
+ beforeEach(() => { cleanup(); seed(); });
150
+ afterAll(cleanup);
151
+
152
+ // ─────────────────────────────────────────────────────────────────────
153
+ // T — a terminal parent is never rewritten
154
+ // ─────────────────────────────────────────────────────────────────────
155
+
156
+ describe("T — a terminal parent stays byte-identical", () => {
157
+ const TERMINAL = ["replied", "failed", "cancelled", "expired"];
158
+
159
+ for (const state of TERMINAL) {
160
+ test(`parent status=${state}: status/result/completed_at unchanged; child result recorded`, () => {
161
+ const ORIGINAL = `ORIGINAL-${state}-ANSWER`;
162
+ const STAMP = "2026-01-01 00:00:00";
163
+ const parent = insertTask({
164
+ from_name: BOSS, to_name: PEER, status: state, content: `cpt-t parent in ${state}`,
165
+ result: ORIGINAL, completed_at: STAMP,
166
+ });
167
+ const child = insertTask({
168
+ from_name: PEER, to_name: TARGET, status: "replied",
169
+ content: `cpt-t child for ${state}`, parent_task_id: parent,
170
+ });
171
+ insertTransport(child);
172
+
173
+ const res = chainReplyToParent(child, `CHILD-ANSWER-FOR-${state}`, "replied", 5, NET);
174
+
175
+ const after = taskById(parent);
176
+ expect(after?.status).toBe(state);
177
+ expect(after?.result).toBe(ORIGINAL);
178
+ expect(after?.completed_at).toBe(STAMP);
179
+
180
+ // Preserved as an audit row plus a notice — never as the parent's answer.
181
+ expect(res.chained).toBe(true);
182
+ expect(chainEvents(parent)).toBe(1);
183
+ expect(childNotices(`CHILD-ANSWER-FOR-${state}`)).toBe(1);
184
+ });
185
+ }
186
+
187
+ test("a terminal parent with a NULL result does not gain one", () => {
188
+ const parent = insertTask({
189
+ from_name: BOSS, to_name: PEER, status: "cancelled", content: "cpt-t cancelled parent",
190
+ result: null, completed_at: "2026-02-02 02:02:02",
191
+ });
192
+ const child = insertTask({
193
+ from_name: PEER, to_name: TARGET, status: "replied",
194
+ content: "cpt-t child of cancelled parent", parent_task_id: parent,
195
+ });
196
+ insertTransport(child);
197
+
198
+ chainReplyToParent(child, "LATE-CHILD-ANSWER", "replied", 5, NET);
199
+
200
+ const after = taskById(parent);
201
+ expect(after?.status).toBe("cancelled");
202
+ expect(after?.result).toBeNull();
203
+ expect(after?.completed_at).toBe("2026-02-02 02:02:02");
204
+ });
205
+ });
206
+
207
+ // ─────────────────────────────────────────────────────────────────────
208
+ // O — an OPEN parent is still completed by the chain (#690 contract)
209
+ // ─────────────────────────────────────────────────────────────────────
210
+
211
+ describe("O — an open parent is still completed by the chain (#690)", () => {
212
+ for (const state of ["created", "delivered", "acked", "running"]) {
213
+ test(`parent status=${state}: reaches replied with the child result`, () => {
214
+ const parent = insertTask({
215
+ from_name: BOSS, to_name: PEER, status: state, content: `cpt-o parent in ${state}`,
216
+ });
217
+ const child = insertTask({
218
+ from_name: PEER, to_name: TARGET, status: "replied",
219
+ content: `cpt-o child for ${state}`, parent_task_id: parent,
220
+ });
221
+ insertTransport(child);
222
+
223
+ chainReplyToParent(child, `OPEN-CHILD-${state}`, "replied", 5, NET);
224
+
225
+ const after = taskById(parent);
226
+ expect(after?.status).toBe("replied");
227
+ expect(after?.result).toContain(`OPEN-CHILD-${state}`);
228
+ expect(after?.completed_at).toBeTruthy();
229
+ expect(chainEvents(parent)).toBe(1);
230
+ expect(childNotices(`OPEN-CHILD-${state}`)).toBe(1);
231
+ });
232
+ }
233
+ });
234
+
235
+ // ─────────────────────────────────────────────────────────────────────
236
+ // R — one attempt is recorded at most once
237
+ // ─────────────────────────────────────────────────────────────────────
238
+
239
+ describe("R — a replayed delivery does not fan out twice", () => {
240
+ test("same attempt replayed: one audit row, one notice, second call not chained", () => {
241
+ const parent = insertTask({
242
+ from_name: BOSS, to_name: PEER, status: "delivered", content: "cpt-r replay parent",
243
+ });
244
+ const child = insertTask({
245
+ from_name: PEER, to_name: TARGET, status: "replied",
246
+ content: "cpt-r replay child", parent_task_id: parent,
247
+ });
248
+ insertTransport(child);
249
+
250
+ const first = chainReplyToParent(child, "REPLAY-ANSWER", "replied", 5, NET);
251
+ const second = chainReplyToParent(child, "REPLAY-ANSWER", "replied", 5, NET);
252
+
253
+ expect(first.chained).toBe(true);
254
+ expect(second.chained).toBe(false);
255
+ expect(chainEvents(parent)).toBe(1);
256
+ expect(childNotices("REPLAY-ANSWER")).toBe(1);
257
+ });
258
+
259
+ test("a terminal parent dedups its audit row too", () => {
260
+ const parent = insertTask({
261
+ from_name: BOSS, to_name: PEER, status: "replied", content: "cpt-r terminal parent",
262
+ result: "ALREADY-ANSWERED", completed_at: "2026-03-03 03:03:03",
263
+ });
264
+ const child = insertTask({
265
+ from_name: PEER, to_name: TARGET, status: "replied",
266
+ content: "cpt-r terminal child", parent_task_id: parent,
267
+ });
268
+ insertTransport(child);
269
+
270
+ expect(chainReplyToParent(child, "TERMINAL-REPLAY", "replied", 5, NET).chained).toBe(true);
271
+ expect(chainReplyToParent(child, "TERMINAL-REPLAY", "replied", 5, NET).chained).toBe(false);
272
+ expect(chainEvents(parent)).toBe(1);
273
+ expect(childNotices("TERMINAL-REPLAY")).toBe(1);
274
+ });
275
+ });
276
+
277
+ // ─────────────────────────────────────────────────────────────────────
278
+ // N — a NEW attempt is not swallowed, even in the same second
279
+ // ─────────────────────────────────────────────────────────────────────
280
+
281
+ describe("N — a legal second attempt keeps its own notice", () => {
282
+ test("retry queues a new transport row → the second result still records", () => {
283
+ const parent = insertTask({
284
+ from_name: BOSS, to_name: PEER, status: "delivered", content: "cpt-n retry parent",
285
+ });
286
+ const child = insertTask({
287
+ from_name: PEER, to_name: TARGET, status: "replied",
288
+ content: "cpt-n retry child", parent_task_id: parent,
289
+ });
290
+ insertTransport(child);
291
+
292
+ expect(chainReplyToParent(child, "ATTEMPT-ONE", "replied", 5, NET).chained).toBe(true);
293
+
294
+ // retry_task: reset the task, then queue a fresh transport row (same
295
+ // transaction, new row). Both attempts carry the same second-granularity
296
+ // delivery stamp — the transport row is what tells them apart.
297
+ db.run(
298
+ "UPDATE tasks SET status = 'replied', result = NULL, completed_at = '2026-04-04 04:04:04' WHERE task_id = ?1",
299
+ [child],
300
+ );
301
+ insertTransport(child, TARGET, "retry dispatch");
302
+ db.run(
303
+ "UPDATE tasks SET status = 'replied', result = NULL, completed_at = '2026-04-04 04:04:04' WHERE task_id = ?1",
304
+ [child],
305
+ );
306
+
307
+ expect(chainReplyToParent(child, "ATTEMPT-TWO", "replied", 5, NET).chained).toBe(true);
308
+ expect(childNotices("ATTEMPT-ONE")).toBe(1);
309
+ expect(childNotices("ATTEMPT-TWO")).toBe(1);
310
+ expect(chainEvents(parent)).toBe(2);
311
+ });
312
+ });
313
+
314
+ // ─────────────────────────────────────────────────────────────────────
315
+ // E — the key binds the ORIGIN, not the walk position
316
+ // ─────────────────────────────────────────────────────────────────────
317
+
318
+ describe("E — a descendant notice does not consume the executor's own completion", () => {
319
+ test("grandparent ← parent ← leaf: leaf notice then parent completion both record", () => {
320
+ const grandparent = insertTask({
321
+ from_name: BOSS, to_name: PEER, status: "delivered", content: "cpt-e grandparent",
322
+ });
323
+ const parent = insertTask({
324
+ from_name: PEER, to_name: THIRD, status: "running",
325
+ content: "cpt-e parent", parent_task_id: grandparent,
326
+ });
327
+ const leaf = insertTask({
328
+ from_name: THIRD, to_name: TARGET, status: "replied",
329
+ content: "cpt-e leaf", parent_task_id: parent,
330
+ });
331
+ insertTransport(leaf);
332
+ insertTransport(parent, THIRD, "parent dispatch");
333
+
334
+ // (1) the leaf answers the parent; the notice also walks up to the
335
+ // grandparent.
336
+ const early = chainReplyToParent(leaf, "LEAF-EARLY", "replied", 5, NET);
337
+ expect(early.chained).toBe(true);
338
+ // Two hops carry the same reply: one notice to the parent's author and
339
+ // one to the grandparent's author.
340
+ expect(childNotices("LEAF-EARLY")).toBe(2);
341
+ const afterLeaf = chainEvents(grandparent);
342
+
343
+ // (2) the parent — a real executor — now finishes for real.
344
+ const result = chainReplyToParent(parent, "PARENT-FINAL", "replied", 5, NET);
345
+ expect(result.chained).toBe(true);
346
+ expect(childNotices("PARENT-FINAL")).toBe(1);
347
+
348
+ // The parent's own completion is a different event from the descendant
349
+ // notice that already reached the grandparent.
350
+ expect(chainEvents(grandparent)).toBe(afterLeaf + 1);
351
+ });
352
+
353
+ test("two different leaves under one parent each keep their own notice", () => {
354
+ const parent = insertTask({
355
+ from_name: BOSS, to_name: PEER, status: "delivered", content: "cpt-e shared parent",
356
+ });
357
+ const firstLeaf = insertTask({
358
+ from_name: PEER, to_name: TARGET, status: "replied",
359
+ content: "cpt-e leaf one", parent_task_id: parent,
360
+ });
361
+ const secondLeaf = insertTask({
362
+ from_name: PEER, to_name: THIRD, status: "replied",
363
+ content: "cpt-e leaf two", parent_task_id: parent,
364
+ });
365
+ insertTransport(firstLeaf);
366
+ insertTransport(secondLeaf, THIRD, "second leaf dispatch");
367
+
368
+ expect(chainReplyToParent(firstLeaf, "LEAF-ONE", "replied", 5, NET).chained).toBe(true);
369
+ expect(chainReplyToParent(secondLeaf, "LEAF-TWO", "replied", 5, NET).chained).toBe(true);
370
+
371
+ expect(childNotices("LEAF-ONE")).toBe(1);
372
+ expect(childNotices("LEAF-TWO")).toBe(1);
373
+ expect(chainEvents(parent)).toBe(2);
374
+ });
375
+ });
376
+
377
+ // ─────────────────────────────────────────────────────────────────────
378
+ // P — an explicit parent requires a participation relation
379
+ // ─────────────────────────────────────────────────────────────────────
380
+
381
+ describe("P — explicit parent requires a participation relation", () => {
382
+ test("a third party's task cannot be adopted as parent", async () => {
383
+ const stranger = insertTask({
384
+ from_name: BOSS, to_name: THIRD, status: "delivered",
385
+ content: "cpt-p a task between other parties",
386
+ });
387
+
388
+ const reply = await call(handlers().send_task, {
389
+ alias: TARGET,
390
+ task: "cpt-p attempt to adopt a stranger parent",
391
+ priority: "normal",
392
+ from_session: PEER,
393
+ network_id: NET,
394
+ parent_task_id: stranger,
395
+ });
396
+
397
+ expect(reply.ok).toBe(false);
398
+ expect(reply.error).toBe("parent_not_participant");
399
+ const spawned = db.get<{ c: number }>(
400
+ "SELECT COUNT(*) AS c FROM tasks WHERE content = ?1",
401
+ "cpt-p attempt to adopt a stranger parent",
402
+ );
403
+ expect(spawned?.c).toBe(0);
404
+ expect(taskById(stranger)?.status).toBe("delivered");
405
+ });
406
+
407
+ test("the parent's own originator may chain a follow-up onto it", async () => {
408
+ const parent = insertTask({
409
+ from_name: PEER, to_name: THIRD, status: "delivered",
410
+ content: "cpt-p parent dispatched by the peer",
411
+ });
412
+
413
+ const reply = await call(handlers().send_task, {
414
+ alias: TARGET,
415
+ task: "cpt-p originator follow-up",
416
+ priority: "normal",
417
+ from_session: PEER,
418
+ network_id: NET,
419
+ parent_task_id: parent,
420
+ });
421
+ expect(reply.ok).toBe(true);
422
+ const child = db.get<{ parent_task_id: string | null }>(
423
+ "SELECT parent_task_id FROM tasks WHERE content = ?1", "cpt-p originator follow-up");
424
+ expect(child?.parent_task_id).toBe(parent);
425
+ });
426
+
427
+ test("a cross-network parent is still refused with its own error code", async () => {
428
+ const foreign = uuidv4();
429
+ db.run(
430
+ `INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, network_id)
431
+ VALUES (?1, ?2, ?3, 'normal', 'delivered', 'cpt-p foreign parent', 'reply', datetime('now'), ?4)`,
432
+ [foreign, BOSS, THIRD, "net_other_cpt"],
433
+ );
434
+
435
+ const reply = await call(handlers().send_task, {
436
+ alias: TARGET,
437
+ task: "cpt-p cross-network parent attempt",
438
+ priority: "normal",
439
+ from_session: PEER,
440
+ network_id: NET,
441
+ parent_task_id: foreign,
442
+ });
443
+ expect(reply.ok).toBe(false);
444
+ expect(reply.error).toBe("cross_network_parent");
445
+ db.run("DELETE FROM tasks WHERE task_id = ?1", [foreign]);
446
+ });
447
+ });