CedarCopilot wants to merge 1 commit into staging from fix/task-dedup-gate-chat
Live on prod, 1 of 2 behaviors degraded
Strict CloudWatch fallback found 27 prod failure log lines for Failed to create execution for task (Task already claimed or no longer eligible) in the last 24h while checking createtasktool, nologgingrunid, duplicateawarenessgate, approval-flag-guard.test.
Libra has verdicts on 1 of 2 tracked behaviors on prod; 1 is still being checked. Libra checks hourly for 3 days after each deploy.
On a Slack DM, the chat agent repeatedly failed to create a task, reporting a "duplicate-check gate ... isn't letting the new task through even on retry." The deal (Soren Transport) already had 3 open tasks.
Root cause: the duplicate-awareness gate (§3.0) in createTaskTool is guarded by if (runId && …). It's designed to fire at most once and let the agent through on retry , but that bypass only works if the retry "breadcrumb" gets logged. Every chat surface (in-app chat + the Slack/iMessage/SMS/WhatsApp bots) runs with runId = 'noLoggingRunId', a sentinel that:
logToolCall short-circuits on (tool-calls.ts:98), so it disables all tool-call logging.With logging off, the gate's alreadyLooked query (a prior list-tasks row, or its own duplicateAwarenessGate: true log) can never find anything. So the gate re-fires on every attempt , neither retrying nor calling list-tasks first can clear it. Task creation is permanently impossible on chat for any deal that already has an open task. This affects in-app chat too, not just Slack.
The gate's own comment claimed "interactive chat is unaffected" , that assumption was that chat has no runId, but chat actually passes the truthy sentinel.
Exclude the sentinel from the guard:
if (runId && runId !== 'noLoggingRunId' && status !== 'recommended') {
This is the same runId !== 'noLoggingRunId' idiom already used across sibling tools (updateTaskTool, the calendar tools, writeDocumentTool). Now:
list-tasks/breadcrumb retry mechanism actually works (each event execution logs its calls and handles one conversation).Also rewrote the stale comment to describe the true invariant.
Added a regression case to createTaskTool.duplicate-gate.test.ts: with runId: 'noLoggingRunId' and an open task present, the task is created and the gate does not fire.
pnpm --filter @zero/server run types → clean on both changed filespnpm --filter @zero/server exec vitest run …/createTaskTool.duplicate-gate.test.ts → 8/8 passpnpm deps:check → no violationsThe 'noLoggingRunId' literal is now compared in ~8 files; sibling-draft-guard.ts already localizes it as a const. A shared exported constant would prevent a future typo'd literal from re-opening this class of bug.
🤖 Generated with Claude Code
The PR exempts the non-logging run-id sentinel from the task duplicate-awareness gate and adds a regression test for that context. However, the alternate Agent SDK in-app chat path supplies a generated UUID, so it remains subject to the gate.
noLoggingRunId.The PR should not merge until the Agent SDK in-app chat path is also made consistent with the intended chat exemption.
Sentinel-based chat calls now bypass the duplicate gate, but SDK-backed in-app chat propagates a generated UUID and therefore still returns the duplicate-awareness response when open tasks exist.
Files Needing Attention: apps/server/src/mastra/tools/task/createTaskTool.ts and its duplicate-gate regression tests
| Filename | Overview |
|---|---|
| apps/server/src/mastra/tools/task/createTaskTool.ts | Exempts the non-logging sentinel from the duplicate gate, bu |
Libra has not measured any production surfaces for this change yet.