diff --git a/parity-manifest.json b/parity-manifest.json index 5be1fb6..014654b 100644 --- a/parity-manifest.json +++ b/parity-manifest.json @@ -108,7 +108,8 @@ "PR #281 (payload_override on /fire): PORTED — included in PR #11 series via cueapi_fire_cue acceptance of payload_override + merge_strategy.", "PR #589 (expose payload on GET /v1/executions): PORTED — this PR (description-only, since the existing get_execution + list_executions tools are passthrough; the new 'payload' field surfaces automatically). cueapi_get_execution and cueapi_list_executions descriptions updated to call out the field.", "PR #590 (require_payload_override + required_payload_keys + cue.fire enforcement): PORTED — this PR. Added two new optional fields to cueapi_create_cue + cueapi_update_cue schemas + handler passthroughs. Updated cueapi_fire_cue description to surface the 3 new error codes (payload_override_required, missing_required_payload_keys, inconsistent_message_instruction). 7 new tests cover the new field passthrough and false/empty edge cases.", - "PR #619 (§17 BCC-light — `notify` field on POST /v1/messages): PORTED — this PR. New cueapi_send_message tool with full notify-field semantics (max-10 cap, omit-when-empty, server contract pinned: from + idempotency_key go via headers, not body). 8 new tests cover the 8 semantics rows from PR #619's pinning table. Minimum-viable port — broader messaging-primitive lifecycle (get/read/ack/inbox/sent + agent identity) tracked in endpoints_missing for follow-up." + "PR #619 (§17 BCC-light — `notify` field on POST /v1/messages): PORTED — this PR. New cueapi_send_message tool with full notify-field semantics (max-10 cap, omit-when-empty, server contract pinned: from + idempotency_key go via headers, not body). 8 new tests cover the 8 semantics rows from PR #619's pinning table. Minimum-viable port — broader messaging-primitive lifecycle (get/read/ack/inbox/sent + agent identity) tracked in endpoints_missing for follow-up.", + "PR #623 (per-message send_at scheduling on POST /v1/messages, §13): PORTED — this PR. Added optional `send_at` ISO-8601 field to cueapi_send_message schema + handler passthrough as body field (server contract: MessageCreate.send_at, app/schemas/message.py). 4 new tests pin omit-by-default, body-not-header transport, and combo-with-notify+idempotency_key behavior. Mirrors cue-fire send_at (PR #618) wording and parallels cueapi-python + cueapi-cli ports of the same private-monorepo PR #623." ], "notes": "First seeded 2026-05-04 by CC-cue-mac-app per CTO-PARITY-L2-MCP-ROUTE, mirroring the cueapi-python #24 + cueapi-cli #25 shape. Layer 2 of the 3-layer parity discipline (PR template + this manifest + Backlog rows). Schema may evolve based on what auditors actually need; the `endpoints_missing` and `tool_param_drift` sections are deliberately verbose because they ARE the audit checklist for catching MCP up to the hosted API." diff --git a/src/tools.ts b/src/tools.ts index f1be914..6dee70d 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -334,6 +334,12 @@ const sendMessageSchema = z.object({ .describe( "Server dedups same-key sends from the same sender; dedup-hit returns the original message_id and bcc_emitted=[] (no re-emit of notifications). Recommended for retry-safe sends." ), + send_at: z + .string() + .optional() + .describe( + "Optional ISO 8601 timestamp to schedule this message for future delivery (cueapi #623, §13). Omitted = send now (server default). When set in the future, the message sits in the recipient's inbox-query gate until `send_at <= now()`, then becomes fetchable; push-delivery dispatch is also gated. Past timestamps are treated as send-now (forgiving fallback). Same semantics as cue-fire `send_at` (PR #618)." + ), }); // ---------- tools ---------- @@ -684,6 +690,7 @@ export const tools: ToolDefinition[] = [ // format identical to pre-Surface-6 senders and avoids payload noise // on the common path. if (args.mode && args.mode !== "auto") body.delivery_mode = args.mode; + if (args.send_at) body.send_at = args.send_at; const extraHeaders: Record = { "X-Cueapi-From-Agent": args.from, }; diff --git a/tests/tools.test.ts b/tests/tools.test.ts index b4c8f95..e81dbd5 100644 --- a/tests/tools.test.ts +++ b/tests/tools.test.ts @@ -1319,6 +1319,84 @@ describe("cueapi_send_message — HTTP contract (PR #619 BCC-light)", () => { expect(description).toContain("effective_delivery_mode"); }); }); + + // cueapi #623 — per-message send_at scheduling + describe("send_at parameter (cueapi #623 — scheduled send)", () => { + it("send_at is omitted by default (server treats absent === send-now)", async () => { + // Wire-format must match pre-#623 senders when caller doesn't pass + // send_at. Server contract: NULL send_at === deliver immediately. + const tool = findTool("cueapi_send_message"); + const { client, calls } = stubClient(); + await tool.handler(client, { + to: "agt_bob", + from: "agt_alice", + subject: "x", + body: "y", + }); + expect(calls[0].body).not.toHaveProperty("send_at"); + }); + + it("send_at passes through to body verbatim as ISO string", async () => { + // Server takes send_at as a body field on POST /v1/messages + // (MessageCreate.send_at, app/schemas/message.py). Same shape as + // cue-fire send_at (PR #618). Pin the body field path. + const tool = findTool("cueapi_send_message"); + const { client, calls } = stubClient(); + const future = "2099-01-01T00:00:00Z"; + await tool.handler(client, { + to: "agt_bob", + from: "agt_alice", + subject: "x", + body: "y", + send_at: future, + }); + expect(calls[0].body).toMatchObject({ send_at: future }); + }); + + it("send_at is a body field, NOT a header (mirrors cue-fire transport)", async () => { + // Verify-server-transport-per-endpoint: same field name (send_at) + // could in principle be a header; here it's a body field. Pin it + // so a refactor doesn't accidentally promote it to a header and + // diverge from the server contract. + const tool = findTool("cueapi_send_message"); + const { client, calls } = stubClient(); + await tool.handler(client, { + to: "agt_bob", + from: "agt_alice", + subject: "x", + body: "y", + send_at: "2099-01-01T00:00:00Z", + }); + // Headers should only contain X-Cueapi-From-Agent (no send_at header). + const headers = calls[0].extraHeaders ?? {}; + expect(Object.keys(headers)).not.toContain("Send-At"); + expect(Object.keys(headers)).not.toContain("X-Cueapi-Send-At"); + expect(headers["X-Cueapi-From-Agent"]).toBe("agt_alice"); + }); + + it("send_at + notify + idempotency_key all flow correctly", async () => { + // Combo test: ensure adding send_at didn't displace any of the + // other optional fields. notify in body, idempotency_key in + // header, send_at in body, from in header. + const tool = findTool("cueapi_send_message"); + const { client, calls } = stubClient(); + await tool.handler(client, { + to: "agt_bob", + from: "agt_alice", + subject: "x", + body: "y", + send_at: "2099-01-01T00:00:00Z", + notify: ["agt_charlie"], + idempotency_key: "k-1", + }); + expect(calls[0].body).toMatchObject({ + send_at: "2099-01-01T00:00:00Z", + notify: ["agt_charlie"], + }); + expect(calls[0].extraHeaders?.["Idempotency-Key"]).toBe("k-1"); + expect(calls[0].extraHeaders?.["X-Cueapi-From-Agent"]).toBe("agt_alice"); + }); + }); }); describe("cueapi_bulk_delete_cues — schema + HTTP contract", () => {