@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.
- package/README.md +130 -10
- package/bin/commhub.ts +99 -11
- package/package.json +7 -7
- package/src/ack-create-request-transport.test.ts +182 -0
- package/src/ack-create-request.test.ts +50 -7
- package/src/admin-delete-network-http.test.ts +228 -0
- package/src/admin-networks-http.test.ts +88 -0
- package/src/adoption-candidates-http.test.ts +137 -0
- package/src/adoption-candidates.ts +158 -0
- package/src/agent-access-batch.test.ts +210 -0
- package/src/agent-access.ts +487 -0
- package/src/agent-acl-groups-http.test.ts +247 -0
- package/src/agent-acl-http.test.ts +659 -0
- package/src/agent-acl-slice3-http.test.ts +267 -0
- package/src/agent-department-http.test.ts +148 -0
- package/src/agent-groups-perf.test.ts +223 -0
- package/src/agent-notice.ts +48 -0
- package/src/agent-team-whoami-http.test.ts +98 -0
- package/src/agent-teams-http.test.ts +323 -0
- package/src/agent-teams.ts +244 -0
- package/src/alias-filter.test.ts +72 -0
- package/src/alias-filter.ts +42 -0
- package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
- package/src/api-host-supervisors-fallback.test.ts +271 -0
- package/src/api-host-supervisors-runtime-readiness.test.ts +195 -0
- package/src/api-nodes-shape.test.ts +26 -10
- package/src/auth-password-change-cli-tokens-http.test.ts +423 -0
- package/src/auth-sessions-http.test.ts +250 -0
- package/src/auth-validate.test.ts +5 -1
- package/src/auth-weak-password-login.test.ts +176 -0
- package/src/auth.ts +604 -51
- package/src/avatar-validate.test.ts +163 -0
- package/src/avatar-validate.ts +103 -0
- package/src/binding-generation-http.test.ts +121 -0
- package/src/blocked-exit-on-reply.test.ts +122 -0
- package/src/boot-side-effect.test.ts +124 -0
- package/src/chain-attempt-dedup.test.ts +447 -0
- package/src/clone-node.test.ts +248 -0
- package/src/clone-node.ts +289 -0
- package/src/codex-config-flags.ts +77 -0
- package/src/codex-fork-contract.ts +23 -0
- package/src/codex-fork-migration.test.ts +38 -0
- package/src/commhub-bin-args.test.ts +85 -0
- package/src/config-apply-sec1.test.ts +107 -0
- package/src/config-apply-validate.test.ts +134 -1
- package/src/config-apply-validate.ts +111 -5
- package/src/create-node-blocked-gate.test.ts +356 -0
- package/src/create-node-network-id.test.ts +79 -0
- package/src/create-node-tool-schema.test.ts +49 -0
- package/src/create-node-unicode-name.test.ts +120 -0
- package/src/create-node-validate.test.ts +199 -7
- package/src/create-node-validate.ts +232 -15
- package/src/create-node-workdir.test.ts +289 -0
- package/src/create-node.ts +88 -23
- package/src/daemon-provider-snapshots.test.ts +421 -0
- package/src/daemon-provider-snapshots.ts +327 -0
- package/src/daemon-token-owner-http.test.ts +236 -0
- package/src/dashboard-slash-routing-http.test.ts +109 -0
- package/src/db-adapter-guard.test.ts +148 -0
- package/src/db-adapter-translate.test.ts +128 -0
- package/src/db-adapter.ts +323 -87
- package/src/db-timestamp.test.ts +38 -0
- package/src/db-timestamp.ts +15 -0
- package/src/db.ts +1111 -42
- package/src/delivered-without-push.test.ts +140 -0
- package/src/department-groups-http.test.ts +209 -0
- package/src/department-groups-sync-http.test.ts +328 -0
- package/src/department-groups.ts +434 -0
- package/src/department-heads-http.test.ts +330 -0
- package/src/department-heads.ts +226 -0
- package/src/departments-http.test.ts +185 -0
- package/src/departments.ts +276 -0
- package/src/desktop-message-seam.test.ts +150 -0
- package/src/desktop-message.test.ts +368 -0
- package/src/dm-file-scope-http.test.ts +178 -0
- package/src/env-transport-gate-http.test.ts +146 -0
- package/src/env-transport-gate.ts +74 -0
- package/src/env-transport-policy.ts +21 -0
- package/src/ext-token-shared.test.ts +60 -0
- package/src/ext-validation-regression.test.ts +131 -0
- package/src/external-schedule-edits-http.test.ts +321 -0
- package/src/external-schedule-edits-timestamps.test.ts +64 -0
- package/src/external-schedule-edits.ts +327 -0
- package/src/file-download-authz-dev-open.test.ts +135 -0
- package/src/file-download-authz.test.ts +404 -0
- package/src/file-download-date-bucket.test.ts +436 -0
- package/src/file-network-scope-dev-open.test.ts +97 -0
- package/src/file-network-scope.test.ts +732 -0
- package/src/group-membership-events-http.test.ts +264 -0
- package/src/group-messages-http.test.ts +482 -0
- package/src/group-messages.ts +297 -0
- package/src/health-redaction.test.ts +642 -0
- package/src/host-supervisor-identity-http.test.ts +152 -0
- package/src/host-supervisor-identity.ts +92 -0
- package/src/http-gzip.test.ts +94 -0
- package/src/http-gzip.ts +112 -0
- package/src/hub-catalog-read-http.test.ts +187 -0
- package/src/hub-provider-config.test.ts +139 -0
- package/src/hub-provider-config.ts +105 -0
- package/src/hub-timestamp.test.ts +38 -0
- package/src/hub-timestamp.ts +27 -0
- package/src/human-dm.ts +215 -0
- package/src/human-inbox.ts +42 -0
- package/src/inbox-caller.ts +49 -0
- package/src/inbox-count-sse.test.ts +310 -0
- package/src/inbox-count.ts +29 -0
- package/src/index.ts +15 -2362
- package/src/lifecycle-guard.ts +1 -1
- package/src/list-host-supervisors.test.ts +5 -4
- package/src/mcp-context-cost-http.test.ts +123 -0
- package/src/mcp-projects-events-http.test.ts +162 -0
- package/src/mcp-route-label-http.test.ts +120 -0
- package/src/mcp-write-network-resolution.test.ts +432 -0
- package/src/member-audit-network-id-http.test.ts +93 -0
- package/src/member-presence-http.test.ts +210 -0
- package/src/messages-ack-by-agent-http.test.ts +142 -0
- package/src/migrate-sqlite-to-pg.test.ts +47 -0
- package/src/migrate-sqlite-to-pg.ts +263 -0
- package/src/model-auth-notify-http.test.ts +181 -0
- package/src/model-auth-notify.ts +116 -0
- package/src/network-name-http.test.ts +75 -0
- package/src/network-scope.ts +276 -0
- package/src/network-token-membership-http.test.ts +139 -0
- package/src/node-alias-control.ts +12 -0
- package/src/node-attrs-validate.ts +88 -0
- package/src/node-attrs.test.ts +217 -0
- package/src/node-caller-identity-http.test.ts +1083 -0
- package/src/node-create-request-status-http.test.ts +105 -0
- package/src/node-daemon-bindings.test.ts +132 -0
- package/src/node-daemon-bindings.ts +135 -0
- package/src/node-files-transport.test.ts +344 -0
- package/src/node-health-codex-login.test.ts +120 -0
- package/src/node-health-dispatch-http.test.ts +279 -0
- package/src/node-health-guard.ts +105 -0
- package/src/node-health-passthrough.test.ts +115 -0
- package/src/node-health-store.ts +101 -0
- package/src/node-id-alias.test.ts +190 -0
- package/src/node-identity-conflict-http.test.ts +269 -0
- package/src/node-identity-conflict.ts +142 -0
- package/src/node-identity-mismatch.test.ts +74 -0
- package/src/node-lifecycle-controllable.test.ts +64 -0
- package/src/node-lifecycle-controllable.ts +53 -0
- package/src/node-lifecycle-read-http.test.ts +245 -0
- package/src/node-lifecycle-read.ts +121 -0
- package/src/node-logs-transport.test.ts +263 -0
- package/src/node-permissions-http.test.ts +276 -0
- package/src/node-permissions.ts +285 -0
- package/src/node-request-retention.test.ts +281 -0
- package/src/node-request-retention.ts +120 -0
- package/src/node-secret-rest-http.test.ts +190 -0
- package/src/node-skills-transport.test.ts +209 -0
- package/src/node-token-identity.ts +35 -0
- package/src/node-token-ownership.ts +51 -0
- package/src/node-token-privilege-http.test.ts +158 -0
- package/src/observer-avatar-http.test.ts +442 -0
- package/src/observer-push.test.ts +252 -0
- package/src/opencode-v2-catalog.test.ts +268 -0
- package/src/opencode-v2-catalog.ts +352 -0
- package/src/package-version-consistency.test.ts +25 -0
- package/src/password-network-tokens.ts +25 -0
- package/src/peer-reply-atomic.test.ts +455 -0
- package/src/peer-reply-capability.test.ts +70 -0
- package/src/pg-worker.ts +87 -0
- package/src/project-dir-warn-clear-http.test.ts +132 -0
- package/src/project-viewer-can-http.test.ts +114 -0
- package/src/push.ts +429 -10
- package/src/redact-tokens.test.ts +65 -0
- package/src/redact-tokens.ts +40 -0
- package/src/rename.ts +38 -10
- package/src/reply-target-derivation.test.ts +96 -0
- package/src/report-status-handover-capabilities-http.test.ts +124 -0
- package/src/report-status-host-schema.test.ts +235 -0
- package/src/report-status-resume-id-handover.test.ts +184 -0
- package/src/report-status-secret-keys.test.ts +140 -0
- package/src/require-explicit-test-db.ts +9 -0
- package/src/requirement-due-reminders-http.test.ts +620 -0
- package/src/requirement-due-reminders.ts +416 -0
- package/src/requirement-events-http.test.ts +268 -0
- package/src/requirement-events.test.ts +45 -0
- package/src/requirement-events.ts +144 -0
- package/src/requirement-notify.ts +165 -0
- package/src/requirement-participant-notify-http.test.ts +309 -0
- package/src/requirement-tag-ops-http.test.ts +266 -0
- package/src/requirement-tags.ts +89 -0
- package/src/requirements-abandoned-http.test.ts +236 -0
- package/src/requirements-comment-http.test.ts +167 -0
- package/src/requirements-completed-at-migration.test.ts +70 -0
- package/src/requirements-errors-mcp-http.test.ts +164 -0
- package/src/requirements-errors.ts +143 -0
- package/src/requirements-etag-http.test.ts +91 -0
- package/src/requirements-http.test.ts +709 -0
- package/src/requirements-last-event-http.test.ts +349 -0
- package/src/requirements-list-mcp-slim-http.test.ts +155 -0
- package/src/requirements-list-slim-http.test.ts +319 -0
- package/src/requirements-mcp.test.ts +163 -0
- package/src/requirements-migrate.ts +259 -0
- package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
- package/src/requirements-people-http.test.ts +114 -0
- package/src/requirements-people-mcp-http.test.ts +225 -0
- package/src/requirements-people.ts +131 -0
- package/src/requirements-priority-lowest.test.ts +114 -0
- package/src/requirements-search-http.test.ts +182 -0
- package/src/requirements-search.test.ts +32 -0
- package/src/requirements-search.ts +145 -0
- package/src/requirements-seq-http.test.ts +193 -0
- package/src/requirements-seq-migration.test.ts +63 -0
- package/src/requirements-start-date.test.ts +125 -0
- package/src/requirements-stats-http.test.ts +357 -0
- package/src/requirements-stats.ts +195 -0
- package/src/requirements-status-participants-http.test.ts +222 -0
- package/src/requirements-upgrade-e2e.test.ts +126 -0
- package/src/requirements-upgrade-migration.test.ts +98 -0
- package/src/requirements.ts +1806 -0
- package/src/resolve-port.test.ts +57 -0
- package/src/resolve-port.ts +44 -0
- package/src/response-charset.test.ts +130 -0
- package/src/rest-explicit-columns-http.test.ts +222 -0
- package/src/rest-identity.test.ts +130 -0
- package/src/rest-identity.ts +141 -0
- package/src/rest-projections.ts +70 -0
- package/src/rest-write-network-resolution.test.ts +133 -0
- package/src/restricted-files.ts +76 -0
- package/src/retention.ts +15 -3
- package/src/route-callers-http.test.ts +120 -0
- package/src/route-timing.test.ts +211 -0
- package/src/route-timing.ts +258 -0
- package/src/rules-file-duplicate-alias.test.ts +234 -0
- package/src/rules-file-session-target.test.ts +207 -0
- package/src/rules-file-transport.test.ts +237 -0
- package/src/runtime-label.test.ts +22 -0
- package/src/runtime-label.ts +21 -0
- package/src/runtime-readiness.ts +90 -0
- package/src/schedule-agent-mcp-http.test.ts +381 -0
- package/src/schedule-agent.ts +192 -0
- package/src/scheduled-backoff-http.test.ts +193 -0
- package/src/scheduled-backoff.ts +64 -0
- package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
- package/src/scheduled-failures-http.test.ts +233 -0
- package/src/scheduled-failures.ts +179 -0
- package/src/scheduled-offline.test.ts +131 -0
- package/src/scheduled-offline.ts +66 -0
- package/src/scheduled-outcome-guidance-http.test.ts +139 -0
- package/src/scheduled-outcome-guidance.ts +9 -0
- package/src/scheduled-reply-unread-http.test.ts +212 -0
- package/src/scheduled-requirement-wake-http.test.ts +264 -0
- package/src/scheduled-requirement-wake.ts +125 -0
- package/src/scheduled-run-terminal.test.ts +268 -0
- package/src/scheduled-skip-blocking-task.test.ts +120 -0
- package/src/scheduled-stuck-http.test.ts +312 -0
- package/src/scheduled-stuck.ts +186 -0
- package/src/scheduled-tasks-http.test.ts +638 -0
- package/src/scheduled-tasks.ts +819 -0
- package/src/scheduled-work-outcome-http.test.ts +154 -0
- package/src/scheduled-work-outcome.ts +50 -0
- package/src/scoped-secrets.ts +139 -0
- package/src/send-dedup-request-id.test.ts +229 -0
- package/src/send-reply-agent-warning.test.ts +202 -0
- package/src/send-reply-alias-optional.test.ts +131 -0
- package/src/send-reply-attachments.test.ts +458 -0
- package/src/send_dedup.test.ts +54 -0
- package/src/send_dedup.ts +52 -6
- package/src/serve-error.test.ts +38 -0
- package/src/serve-error.ts +27 -0
- package/src/server.ts +5398 -0
- package/src/session-task-on-dispatch.ts +14 -0
- package/src/session-token-ownership-http.test.ts +122 -0
- package/src/shared/codex-inventory.ts +41 -0
- package/src/shared/daemon-provider-login.ts +31 -0
- package/src/shared/daemon-provider-snapshot.ts +52 -0
- package/src/shared/external-schedule-contract.ts +60 -0
- package/src/shared/node-name.test.ts +62 -0
- package/src/shared/node-name.ts +161 -0
- package/src/shared/provider-application.ts +29 -0
- package/src/shared/provider-collection.ts +14 -0
- package/src/shared/provider-initialization.ts +8 -0
- package/src/shared/reserved-env-drift.test.ts +10 -0
- package/src/shared/scoped-secret-contract.ts +5 -0
- package/src/shared/task-content-limit-drift.test.ts +28 -0
- package/src/shared/task-content-limit.ts +25 -0
- package/src/side-thread-command-transport.test.ts +312 -0
- package/src/side-thread-command-transport.ts +318 -0
- package/src/side-thread-http-integration.test.ts +233 -0
- package/src/side-thread-http.ts +504 -0
- package/src/side-thread-production.test.ts +72 -0
- package/src/side-thread-production.ts +118 -0
- package/src/side-thread.test.ts +1067 -0
- package/src/side-thread.ts +2499 -0
- package/src/skillhub-http.test.ts +108 -0
- package/src/sqlite-close-checkpoint.test.ts +78 -0
- package/src/start-node.test.ts +264 -0
- package/src/status-alias-filter-http.test.ts +106 -0
- package/src/status-alias-resolver-http.test.ts +163 -0
- package/src/status-alias-resolver.ts +19 -0
- package/src/status-node-id-filter-http.test.ts +161 -0
- package/src/status-read-cache-http.test.ts +229 -0
- package/src/status-read-cache.ts +122 -0
- package/src/stop-delete-node.test.ts +509 -2
- package/src/task-access-http.test.ts +402 -0
- package/src/task-access.ts +420 -0
- package/src/task-activity.ts +60 -0
- package/src/task-auth-origin-http.test.ts +93 -0
- package/src/task-auth-origin.test.ts +24 -0
- package/src/task-auth-origin.ts +7 -0
- package/src/task-consumption.test.ts +482 -0
- package/src/task-diagnostic-http.test.ts +149 -0
- package/src/task-diagnostic.test.ts +115 -0
- package/src/task-diagnostic.ts +118 -0
- package/src/task-expiry-notice-http.test.ts +287 -0
- package/src/task-expiry-notice.ts +315 -0
- package/src/task-idempotency-mcp.test.ts +142 -0
- package/src/task-idempotency.test.ts +60 -0
- package/src/task-idempotency.ts +75 -0
- package/src/task-late-replies.ts +54 -0
- package/src/task-late-reply-http.test.ts +152 -0
- package/src/task-lifecycle-watcher.test.ts +204 -0
- package/src/task-lifecycle-watcher.ts +49 -0
- package/src/task-network-resolution-http.test.ts +140 -0
- package/src/task-orphan-http.test.ts +287 -0
- package/src/task-orphan.ts +119 -0
- package/src/task-queue-ahead-http.test.ts +309 -0
- package/src/task-queue-ahead-scale.test.ts +236 -0
- package/src/task-queue-ahead.ts +186 -0
- package/src/task-rejection-log-http.test.ts +160 -0
- package/src/task-rejection-log.ts +49 -0
- package/src/task-sender-http.test.ts +143 -0
- package/src/task-stale-open-http.test.ts +290 -0
- package/src/task-stale-open.ts +103 -0
- package/src/tasks-pagination-http.test.ts +141 -0
- package/src/tool-audience-http.test.ts +220 -0
- package/src/tool-audience.ts +118 -0
- package/src/tools-list-json-schema.test.ts +67 -0
- package/src/tools.ts +4008 -355
- package/src/upload-early-reject-guard.test.ts +155 -0
- package/src/uploads-http.test.ts +97 -15
- package/src/uploads.ts +103 -6
- package/src/user-avatar-http.test.ts +74 -0
- package/src/user-avatar-migration.test.ts +25 -0
- package/src/user-inbox-by-agent-http.test.ts +161 -0
- package/src/user-inbox-netid-notnull.test.ts +122 -0
- package/src/user-inbox-read-http.test.ts +223 -0
- 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
|
+
});
|