Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion parity-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
7 changes: 7 additions & 0 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----------
Expand Down Expand Up @@ -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<string, string> = {
"X-Cueapi-From-Agent": args.from,
};
Expand Down
78 changes: 78 additions & 0 deletions tests/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down