From d4de1eb12cac996079c67985cfea79937da605f9 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 16:22:55 -0700 Subject: [PATCH 01/19] =?UTF-8?q?chore(deps):=20bump=20@mastra/*=20to=201.?= =?UTF-8?q?42.0-alpha.3=20alpha=20set=20=E2=80=94=20unlocks=20SignalProvid?= =?UTF-8?q?er/signals=20(foreman-22c3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the feature/mastra-signals-alpha branch only; main stays on stable 1.41.0. Coordinated alpha across all 14 @mastra/* + mastra packages; root override @mastra/core -> 1.42.0-alpha.3. Clean reinstall passed check:deps; node_modules now exports @mastra/core/signals (SignalProvider) + @mastra/core/channels. --- .beads/issues.jsonl | 2 +- package-lock.json | 488 +++++++++++++++++++---------------- package.json | 2 +- packages/agents/package.json | 28 +- 4 files changed, 280 insertions(+), 240 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 2cf9bf00..96c639e3 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -210,7 +210,7 @@ {"_type":"issue","id":"foreman-5ld","title":"Initialize Next.js 15 project","description":"Scaffold Next.js 15 with App Router, TypeScript, Tailwind. AC: npm run dev serves a blank page at /. Project committed to git under C:\\Users\\HamCh\\code\\foreman.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:38Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-l8ev","title":"Document Zapier SDK auth model in README (client-creds vs userJwt per surface)","description":"Discovered during SDK exploration: the Zapier SDK surfaces need DIFFERENT auth. Trigger inboxes + discovery + runAction accept app-level client-credentials (ZAPIER_CLIENT_ID/SECRET). Durable-workflow endpoints (runDurable/publishWorkflowVersion) require a per-user userJwt (PKCE OAuth token), NOT client-creds. The new zapier-sdk-cli (0.54) login yields client-creds only; ~/.zapierrc deployKey is a legacy 32-char non-JWT (invalid as SDK creds). Document in README: which surfaces need which auth, how Foreman obtains each (env client-creds vs lib/zapier/sdk.ts PKCE per-user token), and the open question for Zapier (server-side path to per-user durable). Come back to this later. See docs/durable-workflow-spike.md + docs/trigger-inbox-spike.md.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T18:39:52Z","created_by":"HamChowderr","updated_at":"2026-06-11T18:39:52Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-z0kn","title":"Port provider-routing generate() test to a PG-enabled tier (storage injection)","description":"provider-swap.test.ts 'returns text' sub-test calls agent.generate() which now needs PostgresStore-backed Memory (foreman migrated LibSQL-\u003e@mastra/pg). It can't run in the no-external-services mocked tier (agents CI job has no PG), so it's it.skip'd. Proper fix: either inject a test storage into createForemanAgent (let tests use an in-memory/file store), or run this sub-test in a PG-enabled tier (test:live or a CI job with a postgres service). Discovered during the catch-up integration of claude/add-multiple-providers.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T16:40:19Z","created_by":"HamChowderr","updated_at":"2026-06-11T16:40:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-elyi","title":"[bump] Surface ZapierApprovalError approval URL to the user (handleSdkError)","description":"Under SDK \u003e=0.50 with approvalMode unset, a server (non-TTY) defaults to 'throw': sdk.runAction throws ZapierApprovalError carrying an approval URL on approval-gated responses. Foreman's handleSdkError (zapier-sdk-tools.ts) currently catches it only via the generic ZapierError catch-all, surfacing .message/.code but DROPPING the approval URL. Add an explicit 'if (err instanceof ZapierApprovalError)' branch that returns the approval URL + approvalStatus so the agent can tell the user how to approve. Non-blocking for the bump (degrades gracefully today). Discovered during foreman-2xdk verification.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T15:10:57Z","created_by":"HamChowderr","updated_at":"2026-06-11T15:10:57Z","dependencies":[{"issue_id":"foreman-elyi","depends_on_id":"foreman-2xdk","type":"discovered-from","created_at":"2026-06-11T08:10:57Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-elyi","title":"[bump] Surface ZapierApprovalError approval URL to the user (handleSdkError)","description":"Under SDK \u003e=0.50 with approvalMode unset, a server (non-TTY) defaults to 'throw': sdk.runAction throws ZapierApprovalError carrying an approval URL on approval-gated responses. Foreman's handleSdkError (zapier-sdk-tools.ts) currently catches it only via the generic ZapierError catch-all, surfacing .message/.code but DROPPING the approval URL. Add an explicit 'if (err instanceof ZapierApprovalError)' branch that returns the approval URL + approvalStatus so the agent can tell the user how to approve. Non-blocking for the bump (degrades gracefully today). Discovered during foreman-2xdk verification.","status":"closed","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T15:10:57Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:39:56Z","closed_at":"2026-06-11T22:39:56Z","close_reason":"Done: handleSdkError now surfaces ZapierApprovalError.approvalUrl (code APPROVAL_REQUIRED), placed before the generic ZapierError catch-all; handleSdkError exported + 2 unit tests (with/without URL) pass; lint clean. Verified approvalUrl field exists in SDK 0.69.3.","dependencies":[{"issue_id":"foreman-elyi","depends_on_id":"foreman-2xdk","type":"discovered-from","created_at":"2026-06-11T08:10:57Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-d4w9","title":"[aimock] Add a fixture-match coverage check to CI","description":"Nothing verifies fixtures on disk match aimock.json declarations. Add scripts/verify-aimock-coverage.mjs that enumerates declared MCP tools + A2A agents and asserts each has a fixture; wire into .github/workflows/test.yml. Add a short fixture-authoring doc (and why catch-alls are an anti-pattern).","acceptance_criteria":"Coverage script exists, runs in CI, fails on any unmocked declaration; short fixture doc added.","status":"in_progress","priority":3,"issue_type":"chore","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:31Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:37:31Z","started_at":"2026-06-11T21:37:31Z","dependencies":[{"issue_id":"foreman-d4w9","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:31Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-8bh9","title":"[durable] Post-v1 spike: publishWorkflowVersion + runDurable hello-world; evaluate cron-\u003ewatchTriggerInbox SSE","description":"Deferred (post-v1). Prove Zapier durable execution: publish a trivial workflow version, runDurable, poll getDurableRun to terminal, triggerWorkflow via tokenized URL. Separately evaluate replacing the minute-tick cron-driver with watchTriggerInbox SSE for real-time triggers (relates to the trigger-inbox spike). No production wiring.","acceptance_criteria":"Spike doc with call shapes, latency notes, and a recommendation on whether/when to augment. Marked post-v1 backlog.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:29:44Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:29:44Z","dependencies":[{"issue_id":"foreman-8bh9","depends_on_id":"foreman-iyq6","type":"discovered-from","created_at":"2026-06-11T01:29:44Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-13mw","title":"[trigger-inbox] Confirm long-term stability/sunset of experimental Triggers with Zapier","description":"Access works today (iyq6 notes), but the API is /experimental and the 0.51 changelog mentions a closed-beta notice. Ask Zapier whether trigger-inbox is the go-forward path, the stability/SLA, and any sunset risk. Low priority since we can already build/spike now; this de-risks long-term reliance and is a good Zapier-feedback touchpoint.","acceptance_criteria":"Documented answer from Zapier on stability/roadmap, or a logged attempt + explicit decision to proceed at our own risk.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:29:35Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:29:35Z","dependencies":[{"issue_id":"foreman-13mw","depends_on_id":"foreman-iyq6","type":"discovered-from","created_at":"2026-06-11T01:29:34Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/package-lock.json b/package-lock.json index fdf08a33..cd80d874 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,13 +71,13 @@ "license": "MIT" }, "node_modules/@ai-sdk/gateway": { - "version": "3.0.127", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.127.tgz", - "integrity": "sha512-Obmw5hmE5x+ccRrMp/Djx5r0rpFVX87YqE6OY06g5fwYlRI30dA84ARfTzX45ivCvkW4eCnBpOVXVWQ/pjH85w==", + "version": "3.0.128", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.128.tgz", + "integrity": "sha512-t9Dyqk5thSmIzsNvVTQtZZiO3xqKGkk1hX3+GNpYmro+GuEdW+E6mKFHihb9Y1vCEt3rEyd4pbGUcin4D57FfQ==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.10", - "@ai-sdk/provider-utils": "4.0.27", + "@ai-sdk/provider-utils": "4.0.28", "@vercel/oidc": "3.2.0" }, "engines": { @@ -100,9 +100,9 @@ } }, "node_modules/@ai-sdk/provider-utils": { - "version": "4.0.27", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.27.tgz", - "integrity": "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==", + "version": "4.0.28", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.28.tgz", + "integrity": "sha512-nCtGJY43Kqwoyk0rYLj80a3eyXxcaWwyu+lYZDHgY+U6JbYYyvRdRTSy+XCDIB91rFH0Ul0eMDHmDr+YMYtrSw==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.10", @@ -191,13 +191,13 @@ } }, "node_modules/@ai-sdk/react": { - "version": "3.0.203", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.203.tgz", - "integrity": "sha512-6YcuOw50Ay/q21FXytEaRDXsabEqwpp8YcJ+moEaImyQpd94OKEqFEDs62jLHkK8gW6Xw9nmKNSNpCglsHAJ4g==", + "version": "3.0.204", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.204.tgz", + "integrity": "sha512-4I5fq9n/YBCEPfbvYEzc5lFNMxoF2hZRXVrUAsjA6NL7LY8AJo3V3PD2TH9rTKIY08uolnyJSnnQc7YuFXXUIA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider-utils": "4.0.27", - "ai": "6.0.201", + "@ai-sdk/provider-utils": "4.0.28", + "ai": "6.0.202", "swr": "^2.2.5", "throttleit": "2.1.0" }, @@ -2017,9 +2017,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -2033,9 +2033,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -2049,9 +2049,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -2065,9 +2065,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -2081,9 +2081,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -2097,9 +2097,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -2113,9 +2113,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -2129,9 +2129,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -2145,9 +2145,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -2161,9 +2161,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -2177,9 +2177,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -2193,9 +2193,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -2209,9 +2209,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -2225,9 +2225,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -2241,9 +2241,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -2257,9 +2257,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -2273,9 +2273,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -2289,9 +2289,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -2305,9 +2305,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -2321,9 +2321,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -2337,9 +2337,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -2353,9 +2353,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -2369,9 +2369,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -2385,9 +2385,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -2401,9 +2401,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -2417,9 +2417,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -2525,9 +2525,9 @@ "link": true }, "node_modules/@fuma-translate/react": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@fuma-translate/react/-/react-0.0.3.tgz", - "integrity": "sha512-EGaUcCyXM2p1HMV6D/NM1kZjIwCMitO+lmDni7b/J0St8IAZ5cUddhtqPqYyMtFj6Og721B5dXSIN6IDq8wf/g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@fuma-translate/react/-/react-1.0.1.tgz", + "integrity": "sha512-jYNEoTHzOqYsVqnPawGq/zbGhc+xMV69YPENDccZ5gFla2uafEEl82Jxic/PxAGmkZzv8U65etDkvaJXzc0DjQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3762,9 +3762,9 @@ "license": "MIT" }, "node_modules/@mastra/ai-sdk": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@mastra/ai-sdk/-/ai-sdk-1.4.4.tgz", - "integrity": "sha512-03Mt0wQLK/80phsEM7P0BilIpXAW776vYxAb74qpBBRkuxP3WtHT2HThWMTqiLEV9v7P1c8iJ99VtLqXas44Gg==", + "version": "1.4.5-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/ai-sdk/-/ai-sdk-1.4.5-alpha.0.tgz", + "integrity": "sha512-lSochIlukdKI+rFxYXFpO7F7xxGme5CP1SOyTY1MwhvnswcQomSQVR5sIK7kYIYHXdHQE6YtJIqoJEfoN6FBKQ==", "license": "Apache-2.0", "engines": { "node": ">=22.13.0" @@ -3775,9 +3775,9 @@ } }, "node_modules/@mastra/core": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/@mastra/core/-/core-1.41.0.tgz", - "integrity": "sha512-A3gV8kdyO3xf4zIFgzUYutVIOhN4595mGoz0IpMrQPuGTYSVTMwSoUhqpKhvyRXt1UYZAOCt88qqI66lscNQtQ==", + "version": "1.42.0-alpha.3", + "resolved": "https://registry.npmjs.org/@mastra/core/-/core-1.42.0-alpha.3.tgz", + "integrity": "sha512-rMO4COXxWcrO2go9+xHZfaUJFG7L4JYPy5QHVI6lRVJsUszQ99ZYJa9upu4mYVR6oXv5DGgIDRMxpysnD7efHQ==", "license": "Apache-2.0", "dependencies": { "@a2a-js/sdk": "~0.3.13", @@ -3829,9 +3829,9 @@ } }, "node_modules/@mastra/deployer": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/@mastra/deployer/-/deployer-1.41.0.tgz", - "integrity": "sha512-85yn+JzYwzKaZsVZEfMIeRW+rjo9vxowB1IEwsntvhvFmfFkKpTUg0t4IPQfnL1G15PyUmMPKI6wM8vwmDvqbg==", + "version": "1.42.0-alpha.3", + "resolved": "https://registry.npmjs.org/@mastra/deployer/-/deployer-1.42.0-alpha.3.tgz", + "integrity": "sha512-fqKJ23+/3gsYWtuHFIJEMsJ5jI81tyslFk75O6lnzofDvkl5k8XS3TtdcCLMMpmUgO0MfAvUkzwTonq2j4R2Ww==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -3839,7 +3839,7 @@ "@babel/preset-typescript": "^7.29.7", "@babel/traverse": "^7.29.7", "@hono/node-ws": "^1.3.0", - "@mastra/server": "1.41.0", + "@mastra/server": "1.42.0-alpha.3", "@optimize-lodash/rollup-plugin": "^5.1.0", "@rollup/plugin-alias": "6.0.0", "@rollup/plugin-commonjs": "29.0.2", @@ -3871,14 +3871,14 @@ } }, "node_modules/@mastra/deployer-cloudflare": { - "version": "1.1.42", - "resolved": "https://registry.npmjs.org/@mastra/deployer-cloudflare/-/deployer-cloudflare-1.1.42.tgz", - "integrity": "sha512-5Rd1KOaFkqpKW0cdzSibjO3BQpX2iia1GzZJXx1kvcCPShubiXiRXjDHVTeXInLFHxHaOEDQa6TpnzsMAWNMLw==", + "version": "1.1.43-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/deployer-cloudflare/-/deployer-cloudflare-1.1.43-alpha.0.tgz", + "integrity": "sha512-sDssXJ/7BSzKDSf/hlkZdgKb1JeLRvhMzEfE9AcVTazXBE+xycAt1eQD0FtIM3WIpsuAwttI15Fwp6Ohwn+svw==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/core": "^7.29.7", - "@mastra/deployer": "^1.41.0", + "@mastra/deployer": "^1.42.0-alpha.0", "@rollup/plugin-virtual": "3.0.2", "cloudflare": "^5.2.0", "rollup": "^4.59.0", @@ -3899,13 +3899,13 @@ } }, "node_modules/@mastra/deployer-vercel": { - "version": "1.1.36", - "resolved": "https://registry.npmjs.org/@mastra/deployer-vercel/-/deployer-vercel-1.1.36.tgz", - "integrity": "sha512-f6GAt7X3374ISTxIpDzyp2XYO1nnqUrcY9PFkdmpMAsCSUN8yHys+TDs1+PSkZJ1JstnBoLkNY9lISzIsQCu1A==", + "version": "1.1.37-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/deployer-vercel/-/deployer-vercel-1.1.37-alpha.0.tgz", + "integrity": "sha512-YtG5YZeINXwJuJrnK0wtGTWl9GP2QYRpN1isNzICbOx+/fQI8+KfHWtMn7hQWsg67SaJshxiMIIfBpM2RqfhaA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@mastra/deployer": "^1.41.0", + "@mastra/deployer": "^1.42.0-alpha.0", "fs-extra": "^11.3.4" }, "engines": { @@ -3959,9 +3959,9 @@ } }, "node_modules/@mastra/duckdb": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@mastra/duckdb/-/duckdb-1.4.1.tgz", - "integrity": "sha512-GPr0SIdDGMtyjV4eVavzx1wsHOw9ILJ2g/dxsvf1hxwo5/UtdZHzwmo//APtXX0oFwzXs286rv3ZuDqojmmCXw==", + "version": "1.4.1-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/duckdb/-/duckdb-1.4.1-alpha.0.tgz", + "integrity": "sha512-/55TAhKJhlMd5sTc3GBGrj7WuA7jEHywDWTloaXIkCPpKjzqHH2RnCpnRHC4dMzvUKtgrAPs8L+R1fUvS2gRyg==", "license": "Apache-2.0", "dependencies": { "@duckdb/node-api": "^1.5.2-r.1" @@ -3974,9 +3974,9 @@ } }, "node_modules/@mastra/editor": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@mastra/editor/-/editor-0.11.1.tgz", - "integrity": "sha512-0EqAXwS26uuHrtTL2mmHx552IOCwKxWAdUPZvqb3PnXlpQZKgO1BeeC1iVO5bRA4WbiN+OYsEvIrLuUwflOU5A==", + "version": "0.11.2-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/editor/-/editor-0.11.2-alpha.0.tgz", + "integrity": "sha512-6RdnmabIHucTkbOG9CvMqNRQFHp8jYiAAhtmm0pKnhPpMHVYwGqAczr+BKZO8cJe8z4kgwkTd7kQzttCDiTsqA==", "dependencies": { "@arcadeai/arcadejs": "^2.3.0", "@composio/core": "^0.6.5", @@ -3997,10 +3997,33 @@ } } }, + "node_modules/@mastra/editor/node_modules/@mastra/memory": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@mastra/memory/-/memory-1.20.2.tgz", + "integrity": "sha512-J999LmWrEniRSz5MiLxtjX+lYwwASPrZI6+oSoW1h01rzODpw+0CMFIIzbVFluteD1BQLk8F/TGI4dkXDS0HHA==", + "license": "Apache-2.0", + "dependencies": { + "@mastra/schema-compat": "1.2.11", + "async-mutex": "^0.5.0", + "diff": "^8.0.3", + "image-size": "^2.0.2", + "json-schema": "^0.4.0", + "lru-cache": "^11.2.7", + "probe-image-size": "^7.2.3", + "tokenx": "^1.3.0", + "xxhash-wasm": "^1.1.0" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@mastra/core": ">=1.4.1-0 <2.0.0-0" + } + }, "node_modules/@mastra/evals": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@mastra/evals/-/evals-1.2.4.tgz", - "integrity": "sha512-9V3Jw71RZeVziCJbNmevNkrADnUP1Ck6FcNQXXGnvgNZXnuB6/ecDkk6IptfG/6Os7GTrIAhnWWYw4A9XarY+Q==", + "version": "1.2.4-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/evals/-/evals-1.2.4-alpha.0.tgz", + "integrity": "sha512-Bi3VpsrNFVHbiQq6dEQttNNGbsbs+nAZnN+ncWKDy4NvWWV98nG9TpC2iaETkW0AZN6nJnp1FZ4AOEml0dDTzg==", "license": "Apache-2.0", "dependencies": { "compromise": "^14.15.0", @@ -4016,9 +4039,9 @@ } }, "node_modules/@mastra/loggers": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@mastra/loggers/-/loggers-1.1.2.tgz", - "integrity": "sha512-1YdWiUzBX14ULWI+5oDX4eIHt8AD1F5nbx98xevybjd/HqKBPmJni/chcngod2sJZfDcx4NyQE87c/s1Cj+ULQ==", + "version": "1.1.2-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/loggers/-/loggers-1.1.2-alpha.0.tgz", + "integrity": "sha512-2R5tpUvWh2JJvgbQHbB8BsL2sc52z0q1b8PzOaj0/HJCs070pV2Gkqk2nBfPs4QPkVHWfMR4ncyqP9LRmbI+pg==", "license": "Apache-2.0", "dependencies": { "pino": "^10.3.1", @@ -4032,9 +4055,9 @@ } }, "node_modules/@mastra/memory": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@mastra/memory/-/memory-1.20.2.tgz", - "integrity": "sha512-J999LmWrEniRSz5MiLxtjX+lYwwASPrZI6+oSoW1h01rzODpw+0CMFIIzbVFluteD1BQLk8F/TGI4dkXDS0HHA==", + "version": "1.20.2-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/memory/-/memory-1.20.2-alpha.0.tgz", + "integrity": "sha512-B/L1DY0qFavrhY6BsHbOkPAPOkQQSfmpx8EgXI1+KLC3zl489LzZ8Zdl6V6c0GsHy2TCVNqT9cpcbfZugSIcvQ==", "license": "Apache-2.0", "dependencies": { "@mastra/schema-compat": "1.2.11", @@ -4055,9 +4078,9 @@ } }, "node_modules/@mastra/observability": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@mastra/observability/-/observability-1.14.1.tgz", - "integrity": "sha512-VKfn3mE1mNFOXgY9Pr9sy5L4q4xLnoLj501gFMOr0teNnRMvAiANVB+p4rnooDfM4i8nzLDG9icM80uMsgXTzQ==", + "version": "1.14.1-alpha.1", + "resolved": "https://registry.npmjs.org/@mastra/observability/-/observability-1.14.1-alpha.1.tgz", + "integrity": "sha512-6tuDV4T7LAtRmpjc9d9txBOhKMD1KQSaKigu+LaoT54HQwnJC+X7YCVDoxfUnP1/35wAaCPCIb36ddVh9Lb68Q==", "license": "Apache-2.0", "engines": { "node": ">=22.13.0" @@ -4068,9 +4091,9 @@ } }, "node_modules/@mastra/pg": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@mastra/pg/-/pg-1.12.1.tgz", - "integrity": "sha512-GTzUcqyoKTXeu+lxdDSOlfBXoxFcsdLX637jZxhiGWqMFtVK+6g6QeLl+5H+rl+W0ceCYVhzOT/N8fzPJBDV/g==", + "version": "1.13.0-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/pg/-/pg-1.13.0-alpha.0.tgz", + "integrity": "sha512-oHnUttVZTAHj2giYhZ82jVrDsDicfdNSdhkyJ8Tz9tEO9DuOKnRNtTIvCdyilXDRKoMEQ7OfFCX1yNQY/Z6k1A==", "license": "Apache-2.0", "dependencies": { "async-mutex": "^0.5.0", @@ -4086,9 +4109,9 @@ } }, "node_modules/@mastra/rag": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mastra/rag/-/rag-2.2.1.tgz", - "integrity": "sha512-Xt24rmypDC3OjoLDEWa6chbBsaqB8h67VX27dtYe/rS+0n+gSU4B/TeRjxKpy11afT0X9xgbnQj7RzZYX2BD9Q==", + "version": "2.2.1-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/rag/-/rag-2.2.1-alpha.0.tgz", + "integrity": "sha512-qbP6leSnKIoBM1ynOXzpdmjX7lPEo91/qbPscdPql0wWI1Rr/rwJIKpehyAKP/wtT5K6paTeAtQl/hYJHSIuqg==", "license": "Apache-2.0", "dependencies": { "@paralleldrive/cuid2": "^2.3.1", @@ -4125,9 +4148,9 @@ } }, "node_modules/@mastra/server": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/@mastra/server/-/server-1.41.0.tgz", - "integrity": "sha512-Dd0k2dJoi/Voe/3KNcUge1+h2SDA7GDHW+p26kXm/4nnBQfzfZPOdc5rne4ZY/FP/MH0g7nFhHtK74z2ut9A7w==", + "version": "1.42.0-alpha.3", + "resolved": "https://registry.npmjs.org/@mastra/server/-/server-1.42.0-alpha.3.tgz", + "integrity": "sha512-InmbmY0tdITb6hINTZaoqNDbRshOYObeUrL50pWHobZzh0jXGce4X1p7oyZx9ApsPWWuZbWvjryhsOoHICYgpw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -4142,9 +4165,9 @@ } }, "node_modules/@mastra/voice-openai": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@mastra/voice-openai/-/voice-openai-0.12.2.tgz", - "integrity": "sha512-IICJBIiwda0aKU6d8uGz6OTmhr+09U4MQKAQJ89SSNgjgQXkNx31JQblp/3ffcpI4KSviu3DPiCiLFs7JzLiuw==", + "version": "0.12.2-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/voice-openai/-/voice-openai-0.12.2-alpha.0.tgz", + "integrity": "sha512-rTYeIYWt0L+h8uAJ1k+a+xogdYfRgsMZ2w1o3VJf2udmlTE6ykPsK6+n/eU/+b9AI3ZDLvyNNSUKFufEgoAMqA==", "license": "Apache-2.0", "dependencies": { "openai": "^5.23.2" @@ -7872,9 +7895,9 @@ } }, "node_modules/@slack/web-api": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.16.0.tgz", - "integrity": "sha512-68SAV77uuGKuhyyaRytX8UijVnqSLsTSKslGXw17cjQYXn+jtNl7gbaEjHgC5x2rhCuFdahBrEC2VCLppbzReg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.17.0.tgz", + "integrity": "sha512-jejr34a8B4L5AS713wOAx1LAqNkW16HVMDEa6sYBvFDc/llUBl8hXaiI4BwF+Al+Sug19Vn2O7iokTVIhVvZ1Q==", "license": "MIT", "dependencies": { "@slack/logger": "^4.0.1", @@ -9295,9 +9318,9 @@ } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -9340,14 +9363,14 @@ } }, "node_modules/ai": { - "version": "6.0.201", - "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.201.tgz", - "integrity": "sha512-iDmXSy7csk+SYaCkTUMwsOE/73pksR1spctV+u+DW97v+R/Cj01gchgCUNVrn9QyNHqOJQuTg+JBVsBsp6lOag==", + "version": "6.0.202", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.202.tgz", + "integrity": "sha512-14O7uyHVa5lXyt1RzduRqM1zwOnLBN+EaE+btWiP2R7GLHd5oh+Z9uaebR785V/Y2hXlxvuQGsw2Gj6iHpkxsg==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "3.0.127", + "@ai-sdk/gateway": "3.0.128", "@ai-sdk/provider": "3.0.10", - "@ai-sdk/provider-utils": "4.0.27", + "@ai-sdk/provider-utils": "4.0.28", "@opentelemetry/api": "^1.9.0" }, "engines": { @@ -9783,9 +9806,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.35", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", - "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "version": "2.10.36", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.36.tgz", + "integrity": "sha512-lVq/Df7LXlO79MVaaUHztSwWiG9oXoWHlgvNS51v8Dpd4+G4/VIy6qYePTw31nAVls33nUtnfezYeLkYAak9dg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -12461,9 +12484,9 @@ } }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -12473,32 +12496,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -13299,12 +13322,12 @@ } }, "node_modules/fumadocs-core": { - "version": "16.10.0", - "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.10.0.tgz", - "integrity": "sha512-DYAYVh83RCglrJ9eBjXJ0xCMv7yhiUfj3RVHlj2QRv4UuUPhtKkA4K1Tu49qvG1MjZQdTuP8B2A0WLiFijXDgg==", + "version": "16.10.1", + "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.10.1.tgz", + "integrity": "sha512-iGnB03/VyMSTWIaZ8zaDG/b/4q1e4gSzWDSvP3AR5Yxg9UJMsA0acaN/IFcURBSgRgJq6PELyYA6WfHBvHAgSg==", "license": "MIT", "dependencies": { - "@fuma-translate/react": "^0.0.3", + "@fuma-translate/react": "^1.0.1", "@orama/orama": "^3.1.18", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", @@ -13573,23 +13596,23 @@ } }, "node_modules/fumadocs-ui": { - "version": "16.10.0", - "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.10.0.tgz", - "integrity": "sha512-pSqtHX4rxYoALY7j6k32oK3rWmDESkaeUqUJxFiIkl7tCh4NXkkFAPURQSSzPYgy6NWC5qu6ellrbw9LnjWIQg==", + "version": "16.10.1", + "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.10.1.tgz", + "integrity": "sha512-ytEwbMFFadfuul9x4Pz4pg9FMRI1MkqW5P7bHrWsLF+d1C4whzNtcUKPn0QP6KCQqIKoVhIa3C7qlI9v06Ik1A==", "license": "MIT", "dependencies": { - "@fuma-translate/react": "^0.0.3", + "@fuma-translate/react": "^1.0.1", "@fumadocs/tailwind": "0.0.5", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-direction": "^1.1.1", - "@radix-ui/react-navigation-menu": "^1.2.14", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-presence": "^1.1.5", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-accordion": "^1.2.13", + "@radix-ui/react-collapsible": "^1.1.13", + "@radix-ui/react-dialog": "^1.1.16", + "@radix-ui/react-direction": "^1.1.2", + "@radix-ui/react-navigation-menu": "^1.2.15", + "@radix-ui/react-popover": "^1.1.16", + "@radix-ui/react-presence": "^1.1.6", + "@radix-ui/react-scroll-area": "^1.2.11", + "@radix-ui/react-slot": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.14", "class-variance-authority": "^0.7.1", "lucide-react": "^1.17.0", "motion": "^12.40.0", @@ -13605,7 +13628,7 @@ "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", - "fumadocs-core": "16.10.0", + "fumadocs-core": "16.10.1", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" @@ -16174,9 +16197,9 @@ } }, "node_modules/mastra": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/mastra/-/mastra-1.12.2.tgz", - "integrity": "sha512-KYMmL4SIDY7WJ9Oc5b4sxrmwUZScGpNoxemCKVb6v6YODdIE+PC2I+/t77tBsJ4NDgUCm4UPGPzVWD9ToDdcUw==", + "version": "1.12.3-alpha.3", + "resolved": "https://registry.npmjs.org/mastra/-/mastra-1.12.3-alpha.3.tgz", + "integrity": "sha512-IsYnqvnE7inOEYmEXIvuc9LrjvJw1rRFoNvBUxYn37reogaUJndWbcTYhgluF3W7odmIBEhqwP2pMnzW2lcVzA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -16184,7 +16207,7 @@ "@babel/types": "^7.29.7", "@clack/prompts": "^1.1.0", "@expo/devcert": "^1.2.1", - "@mastra/deployer": "^1.41.0", + "@mastra/deployer": "^1.42.0-alpha.3", "@mastra/loggers": "^1.1.2", "archiver": "^7.0.1", "commander": "^14.0.3", @@ -16214,6 +16237,23 @@ "@mastra/core": ">=1.34.0-0 <2.0.0-0" } }, + "node_modules/mastra/node_modules/@mastra/loggers": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@mastra/loggers/-/loggers-1.1.2.tgz", + "integrity": "sha512-1YdWiUzBX14ULWI+5oDX4eIHt8AD1F5nbx98xevybjd/HqKBPmJni/chcngod2sJZfDcx4NyQE87c/s1Cj+ULQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "pino": "^10.3.1", + "pino-pretty": "^13.1.3" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@mastra/core": ">=1.0.0-0 <2.0.0-0" + } + }, "node_modules/mastra/node_modules/commander": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", @@ -23259,9 +23299,9 @@ } }, "node_modules/zod-from-json-schema": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/zod-from-json-schema/-/zod-from-json-schema-0.5.2.tgz", - "integrity": "sha512-/dNaicfdhJTOuUd4RImbLUE2g5yrSzzDjI/S6C2vO2ecAGZzn9UcRVgtyLSnENSmAOBRiSpUdzDS6fDWX3Z35g==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/zod-from-json-schema/-/zod-from-json-schema-0.5.3.tgz", + "integrity": "sha512-44YFiuq+WHw9YZQAo/Ad0F7o9c/im0Q6cnHI23BsXhEmZtkNn4cD0bljLMMjkfb/EidopPWdsmKI8EvLHX5ZyA==", "license": "MIT", "dependencies": { "zod": "^4.0.17" @@ -23310,17 +23350,17 @@ "@chat-adapter/telegram": "^4.28.1", "@chat-adapter/whatsapp": "^4.28.1", "@hono/node-server": "^1.19.14", - "@mastra/ai-sdk": "^1.4.4", - "@mastra/core": "^1.41.0", - "@mastra/duckdb": "^1.4.1", - "@mastra/editor": "^0.11.1", - "@mastra/evals": "^1.2.4", - "@mastra/loggers": "^1.1.2", - "@mastra/memory": "^1.20.2", - "@mastra/observability": "^1.14.1", - "@mastra/pg": "^1.12.1", - "@mastra/rag": "^2.2.1", - "@mastra/voice-openai": "^0.12.2", + "@mastra/ai-sdk": "1.4.5-alpha.0", + "@mastra/core": "1.42.0-alpha.3", + "@mastra/duckdb": "1.4.1-alpha.0", + "@mastra/editor": "0.11.2-alpha.0", + "@mastra/evals": "1.2.4-alpha.0", + "@mastra/loggers": "1.1.2-alpha.0", + "@mastra/memory": "1.20.2-alpha.0", + "@mastra/observability": "1.14.1-alpha.1", + "@mastra/pg": "1.13.0-alpha.0", + "@mastra/rag": "2.2.1-alpha.0", + "@mastra/voice-openai": "0.12.2-alpha.0", "@supabase/supabase-js": "^2.105.4", "@zapier/zapier-sdk": "^0.69.3", "ai": "^6.0.177", @@ -23346,14 +23386,14 @@ "@copilotkit/aimock": "^1.14.3", "@types/node": "^20", "@zapier/zapier-sdk-cli": "^0.54.3", - "mastra": "^1.12.2", + "mastra": "1.12.3-alpha.3", "typescript": "^5", "vitest": "^4.1.4", "zlib-sync": "^0.1.10" }, "optionalDependencies": { - "@mastra/deployer-cloudflare": "^1.1.42", - "@mastra/deployer-vercel": "^1.1.36" + "@mastra/deployer-cloudflare": "1.1.43-alpha.0", + "@mastra/deployer-vercel": "1.1.37-alpha.0" } }, "packages/web": { diff --git a/package.json b/package.json index 58b84c43..e0abb3d8 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "esbuild": ">=0.25.0", "zod": "4.4.3", "postcss": "^8.5.10", - "@mastra/core": "1.41.0" + "@mastra/core": "1.42.0-alpha.3" }, "devDependencies": { "@biomejs/biome": "^2.4.12", diff --git a/packages/agents/package.json b/packages/agents/package.json index f7ec4bd8..f399453b 100644 --- a/packages/agents/package.json +++ b/packages/agents/package.json @@ -47,17 +47,17 @@ "@chat-adapter/telegram": "^4.28.1", "@chat-adapter/whatsapp": "^4.28.1", "@hono/node-server": "^1.19.14", - "@mastra/ai-sdk": "^1.4.4", - "@mastra/core": "^1.41.0", - "@mastra/duckdb": "^1.4.1", - "@mastra/editor": "^0.11.1", - "@mastra/evals": "^1.2.4", - "@mastra/loggers": "^1.1.2", - "@mastra/memory": "^1.20.2", - "@mastra/observability": "^1.14.1", - "@mastra/pg": "^1.12.1", - "@mastra/rag": "^2.2.1", - "@mastra/voice-openai": "^0.12.2", + "@mastra/ai-sdk": "1.4.5-alpha.0", + "@mastra/core": "1.42.0-alpha.3", + "@mastra/duckdb": "1.4.1-alpha.0", + "@mastra/editor": "0.11.2-alpha.0", + "@mastra/evals": "1.2.4-alpha.0", + "@mastra/loggers": "1.1.2-alpha.0", + "@mastra/memory": "1.20.2-alpha.0", + "@mastra/observability": "1.14.1-alpha.1", + "@mastra/pg": "1.13.0-alpha.0", + "@mastra/rag": "2.2.1-alpha.0", + "@mastra/voice-openai": "0.12.2-alpha.0", "@supabase/supabase-js": "^2.105.4", "@zapier/zapier-sdk": "^0.69.3", "ai": "^6.0.177", @@ -80,14 +80,14 @@ "zod": "^4.3.6" }, "optionalDependencies": { - "@mastra/deployer-cloudflare": "^1.1.42", - "@mastra/deployer-vercel": "^1.1.36" + "@mastra/deployer-cloudflare": "1.1.43-alpha.0", + "@mastra/deployer-vercel": "1.1.37-alpha.0" }, "devDependencies": { "@copilotkit/aimock": "^1.14.3", "@types/node": "^20", "@zapier/zapier-sdk-cli": "^0.54.3", - "mastra": "^1.12.2", + "mastra": "1.12.3-alpha.3", "typescript": "^5", "vitest": "^4.1.4", "zlib-sync": "^0.1.10" From b57e81ede777af4e361811b6e535f0821cd66d8d Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 16:56:18 -0700 Subject: [PATCH 02/19] fix(build): legacy-peer-deps for the uncoordinated @mastra alpha set (foreman-22c3) mastra build's strict sub-install rejected @mastra/core@1.42.0-alpha.3 because @mastra/ai-sdk@1.4.5-alpha.0 peers core '>=1.5.0-0' and node-semver excludes a prerelease (1.42.0-alpha.3) from a range whose comparators don't share its exact major.minor.patch. legacy-peer-deps lets the alpha set install. mastra build now exits 0 ('Build successful'). Phase 0 fully green on alpha: install + 280 tests + build. --- .npmrc | 1 + packages/agents/.npmrc | 1 + 2 files changed, 2 insertions(+) create mode 100644 .npmrc create mode 100644 packages/agents/.npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..521a9f7c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/packages/agents/.npmrc b/packages/agents/.npmrc new file mode 100644 index 00000000..521a9f7c --- /dev/null +++ b/packages/agents/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true From db63a26f6132d00089482e85ef783d7dfb978686 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 17:34:10 -0700 Subject: [PATCH 03/19] feat(workflows): un-stub poll trigger as a Foreman-native poll driver (foreman-ld7s) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds src/workflows/poll-driver.ts (tickPoll/startPollDriver) mirroring the cron driver: reads workflow_trigger type='poll', runs the Zapier read action via runAction, diffs results by config.dedupeKey against the last_dedupe_key cursor, and fires executeWorkflow once per new record (oldest-first). First poll establishes a baseline (no backlog replay); intervalMinutes gates polling. Un-stubs attach_trigger (adds 'poll' + pollConfig); list/detach were already poll-generic. Wires startPollDriver into cron-driver-server alongside cron. Rationale: exercised the alpha @mastra/core SignalProvider API directly — its poll loop skips when the in-memory subscription registry is empty, and notify() throws unless registered on an in-process Agent. It's built for a provider that lives in the agent and notifies threads, not a singleton worker firing DB-backed workflows. SignalProvider will be adopted on the webhook/channel path instead. Keeps the door open to swap in Zapier watchTriggerInbox SSE later. 8 unit tests (baseline, diff/dedup, oldest-first order, interval gate, guardrail skip, bare-array result). Full agents suite green (288 passed). Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 1 + packages/agents/src/cron-driver-server.ts | 16 +- .../agents/src/mastra/tools/attach-trigger.ts | 52 +++- packages/agents/src/workflows/poll-driver.ts | 276 ++++++++++++++++++ .../agents/tests/unit/poll-driver.test.ts | 185 ++++++++++++ 5 files changed, 513 insertions(+), 17 deletions(-) create mode 100644 packages/agents/src/workflows/poll-driver.ts create mode 100644 packages/agents/tests/unit/poll-driver.test.ts diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 96c639e3..5bc917df 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,6 +92,7 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-ld7s","title":"[workflow] Un-stub the poll trigger as a Foreman-native poll driver (tickPoll)","description":"Poll trigger type is documented in the migration and accepted by list/detach but hidden by attach-trigger and has no driver. Build src/workflows/poll-driver.ts mirroring cron-driver: read workflow_trigger type=poll, run the Zapier read action via runAction, diff results by config.dedupeKey field against last_dedupe_key cursor, fire executeWorkflow on new records, persist cursor. Un-stub attach_trigger (add poll + pollConfig). Wire into cron-driver-server. Rationale (explored alpha API hands-on): Mastra SignalProvider.poll() skips when zero in-memory subscriptions and notify() requires an in-process agent — it is built for provider-lives-in-agent/notify-thread, not a singleton worker firing DB workflows. SignalProvider will be adopted on the webhook/channel path instead. Keeps door open to Zapier watchTriggerInbox SSE later (iyq6).","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:27:10Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:27:27Z","started_at":"2026-06-12T00:27:27Z","dependencies":[{"issue_id":"foreman-ld7s","depends_on_id":"foreman-c63f","type":"discovered-from","created_at":"2026-06-11T17:27:10Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-22c3","title":"Evaluate Mastra signals/signal-providers/notifications vs Foreman's home-grown triggers","description":"Foreman is on @mastra/core 1.41 (post-bump). Mastra now ships native primitives that overlap with Foreman's hand-built trigger/workflow/notification system: (1) SignalProvider (poll OR webhook an external source -\u003e push notifications into agent threads) ~= Foreman's channel-trigger + cron-driver + the stubbed poll trigger (c63f); (2) built-in dedupeKey on notify() directly solves the dedup gap in tv5p (Foreman hand-rolls idempotency); (3) @mastra/github-signals is a production provider and Foreman already has a GitHub channel; (4) background-task suspend/resume for human approval + notification inbox ~= Foreman's proposal/approval flow. Sibling to iyq6 (Zapier-durable eval). Decide adopt-Mastra-native vs keep-own BEFORE building the v8k1 workflow epic. Confirmed code-side: backgroundTasks enabled in mastra/index.ts; pubsub is default in-process EventEmitter (no distributed backend — matters for 2-process + multi-instance); signals/signal-providers not used at all.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T22:33:47Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:33:47Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-rpsr","title":"Fix flaky db-types CI job — 'supabase gen types --local' intermittently errors 'Access token not provided'","description":"CI job 'db types fresh' (.github/workflows/test.yml) is flaky: passes on some commits (4dd20ff, 25b1ed1), fails on others (51e2065, f677794, b1070ae, 040b6a6) with 'Access token not provided' from 'npx supabase gen types typescript --local', producing empty output → diff shows 0a1,4128 (empty vs committed 4128-line database.types.ts). NOT a stale-types problem and NOT caused by the SDK/mastra bump. Root cause: supabase/setup-cli@v1 uses version:latest (unpinned), and the drifted CLI intermittently requires an access token even for --local. Fix options: (1) pin setup-cli to a known-good supabase version; (2) ensure 'supabase start' is fully healthy before gen; (3) set SUPABASE_ACCESS_TOKEN (project avoids secrets). Verify locally by starting Foreman's own supabase (note: a different project's supabase '*_Zillow' was running during diagnosis).","status":"closed","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-11T19:40:46Z","created_by":"HamChowderr","updated_at":"2026-06-11T20:18:52Z","closed_at":"2026-06-11T20:18:52Z","close_reason":"Fixed (379d9b4): added a placeholder SUPABASE_ACCESS_TOKEN to the CI db-types step. The latest supabase CLI demands a token's presence before local type-gen in Linux CI (value unused for --local). All 5 CI jobs green on main (run 27374657799). Also switched scripts to --local --workdir ../.. (cwd-independent) + documented the gotcha in CLAUDE.md.","dependencies":[{"issue_id":"foreman-rpsr","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T12:40:46Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-chxx","title":"Reskin README to myrp-build product-launch style (hero+badges, emoji sections, narrative, FAQ)","description":"Current README is content-complete but dry/dev-doc-toned. Rewrite to mirror hamchowderr/myrp-build README: centered hero + shields.io badges, emoji-prefixed sections split by ---, benefit-led narrative with example-prompt blockquotes, collapsible concrete example, ASCII pipeline diagram, dev-vs-self-hosted comparison table, annotated file-tree architecture, testing-tiers table, FAQ, acknowledgments, license stance. Preserve all factual content (5 agents, 34 Zapier tools, channel statuses, modes, testing tiers, deploy targets, live URLs, MIT license, contact). User-authorized.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T19:17:43Z","created_by":"HamChowderr","updated_at":"2026-06-11T19:28:04Z","closed_at":"2026-06-11T19:28:04Z","close_reason":"README reskinned to myrp-build product-launch style; committed b1070ae, pushed to origin/main, rendered + reviewed in Chrome.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/src/cron-driver-server.ts b/packages/agents/src/cron-driver-server.ts index 5aa63955..9ba04925 100644 --- a/packages/agents/src/cron-driver-server.ts +++ b/packages/agents/src/cron-driver-server.ts @@ -1,18 +1,22 @@ /** - * Standalone entrypoint for the cron driver. Run with: + * Standalone entrypoint for the trigger drivers. Run with: * node --env-file=.env.local src/cron-driver-server.ts * * Keeps a long-lived process that ticks once per minute and fires due - * `workflow_trigger` rows. Only run a single instance — there is no - * distributed lock. + * `workflow_trigger` rows: the cron driver (schedule matches) and the poll + * driver (Zapier read actions returning new records). Only run a single + * instance — there is no distributed lock. */ import { startCronDriver } from "./workflows/cron-driver"; +import { startPollDriver } from "./workflows/poll-driver"; -const stop = startCronDriver(); +const stopCron = startCronDriver(); +const stopPoll = startPollDriver(); const shutdown = (sig: string) => { - console.log(`[cron-driver] received ${sig}, shutting down`); - stop(); + console.log(`[trigger-driver] received ${sig}, shutting down`); + stopCron(); + stopPoll(); process.exit(0); }; process.on("SIGINT", () => shutdown("SIGINT")); diff --git a/packages/agents/src/mastra/tools/attach-trigger.ts b/packages/agents/src/mastra/tools/attach-trigger.ts index eff8fe47..a367318f 100644 --- a/packages/agents/src/mastra/tools/attach-trigger.ts +++ b/packages/agents/src/mastra/tools/attach-trigger.ts @@ -33,23 +33,53 @@ const channelConfig = z.object({ .describe("Match criteria for the incoming message."), }); +const pollConfig = z.object({ + app: z.string().min(1).describe("Zapier app key, e.g. 'gmail' or 'google_sheets'."), + action: z + .string() + .min(1) + .describe("The read action key, e.g. 'new_email' or 'new_or_updated_spreadsheet_row'."), + connection: z + .string() + .optional() + .describe("Connection id/alias. Auto-discovered for the app if omitted."), + inputs: z + .record(z.string(), z.unknown()) + .optional() + .describe("Inputs passed to the read action (e.g. { sheet: '...' })."), + dedupeKey: z + .string() + .min(1) + .describe( + "Field on each returned record that uniquely identifies it (e.g. 'id'). " + + "Records are assumed newest-first; new records are those above the last-seen value.", + ), + intervalMinutes: z + .number() + .int() + .min(1) + .optional() + .default(5) + .describe("How often to poll, in minutes (default 5)."), +}); + export const attachTriggerTool = createTool({ id: "attach_trigger", strict: true, description: "Bind a saved workflow to an event source so it fires automatically. " + - "Two trigger types are supported today: 'cron' for scheduled runs (e.g. " + - "every weekday at 9am) and 'channel' for chat-message matches (e.g. when " + - "you DM '!standup' on Slack). The 'poll' type is reserved for future use. " + - "Use after `save_workflow` and after the user confirms the schedule or " + - "match condition.", + "Three trigger types are supported: 'cron' for scheduled runs (e.g. every " + + "weekday at 9am), 'channel' for chat-message matches (e.g. when you DM " + + "'!standup' on Slack), and 'poll' to fire when a Zapier read action returns " + + "a new record (e.g. a new Gmail email or spreadsheet row). Use after " + + "`save_workflow` and after the user confirms the schedule, match, or poll " + + "source.", inputSchema: z.object({ workflowId: z.string().describe("The workflow id (from list_workflows)."), - type: z - .enum(["cron", "channel"]) - .describe("Trigger kind. 'poll' is not yet exposed via the agent."), + type: z.enum(["cron", "channel", "poll"]).describe("Trigger kind."), cron: cronConfig.optional().describe("Required when type='cron'."), channel: channelConfig.optional().describe("Required when type='channel'."), + poll: pollConfig.optional().describe("Required when type='poll'."), enabled: z .boolean() .optional() @@ -58,7 +88,7 @@ export const attachTriggerTool = createTool({ }), outputSchema: z.object({ id: z.string(), - type: z.enum(["cron", "channel"]), + type: z.enum(["cron", "channel", "poll"]), enabled: z.boolean(), }), // Attaching a cron / channel trigger means a workflow may fire without @@ -68,11 +98,11 @@ export const attachTriggerTool = createTool({ const o = output as { id?: string; type?: string }; console.log(`[tool:${toolName}] attached ${o.type} trigger ${o.id?.slice(0, 8)}`); }, - execute: async ({ workflowId, type, cron, channel, enabled }, context) => { + execute: async ({ workflowId, type, cron, channel, poll, enabled }, context) => { const userId = context?.requestContext?.get("userId") as string | undefined; if (!userId) throw new Error("attach_trigger: no userId in request context"); - const config = type === "cron" ? cron : channel; + const config = type === "cron" ? cron : type === "channel" ? channel : poll; if (!config) { throw new Error(`attach_trigger: type='${type}' requires the matching '${type}' field`); } diff --git a/packages/agents/src/workflows/poll-driver.ts b/packages/agents/src/workflows/poll-driver.ts new file mode 100644 index 00000000..f1554680 --- /dev/null +++ b/packages/agents/src/workflows/poll-driver.ts @@ -0,0 +1,276 @@ +/** + * Poll driver — long-running worker that fires `workflow_trigger` rows of + * type='poll' when a Zapier read action returns records newer than the last + * one we've seen. + * + * Model (matches the migration comment on workflow_trigger): + * config = { app, action, connection?, inputs?, dedupeKey, intervalMinutes? } + * - `dedupeKey` is the field on each returned record that uniquely identifies + * it (e.g. "id"). Reads are assumed newest-first (the Zapier convention for + * "new X" triggers). + * - `last_dedupe_key` is the persisted cursor: the key of the newest record + * we've already processed. New records = those above the cursor. + * - `last_fired_at` doubles as the last-polled timestamp; `intervalMinutes` + * gates how often we actually hit the Zapier action. + * + * First poll establishes the baseline (cursor was null → set it, fire nothing) + * so binding a trigger doesn't replay the whole backlog. Subsequent polls fire + * the saved workflow once per new record, oldest-first. + * + * Like the cron driver, this is intentionally simple — no distributed lock, no + * missed-tick catch-up. Run a single instance (it lives in cron-driver-server + * alongside the cron driver). Unlike cron we AWAIT each trigger's processing: + * poll reads aren't minute-precision-sensitive, awaiting gives an accurate + * fired count, and there are few poll triggers. + * + * Why a hand-rolled worker and not a Mastra SignalProvider: the alpha + * SignalProvider poll loop skips entirely when its in-memory subscription + * registry is empty, and `notify()` throws unless the provider is registered on + * an in-process Agent — it's built for a provider that lives inside the agent + * and notifies threads, not a singleton worker that fires DB-backed workflows. + * The SignalProvider primitive is adopted on the webhook/channel path instead. + * This worker keeps the door open to swap in Zapier's `watchTriggerInbox` SSE + * later (foreman-iyq6) without touching the trigger storage or tools. + */ + +import { getSupabase } from "@/lib/db"; +import { executeWorkflow } from "@/lib/workflows/engine"; +import { runAction } from "@/lib/zapier/execution"; + +/** Cap on how many backlog records a single poll will fire, to avoid a flood + * if the cursor falls off the end of the returned page. */ +const MAX_FIRE_PER_POLL = 25; + +interface PollConfig { + app: string; + action: string; + connection?: string; + inputs?: Record; + /** Field on each record that uniquely identifies it (e.g. "id"). */ + dedupeKey: string; + /** How often to poll, in minutes. Defaults to 5. */ + intervalMinutes?: number; +} + +interface PollTriggerRow { + id: string; + workflow_id: string; + config: string; + last_fired_at: string | null; + last_dedupe_key: string | null; +} + +/** Normalize a Zapier read result into an array of record objects. Handles the + * common shapes: a bare array, `{ data: [...] }`, or `{ results: [...] }`. */ +function extractRecords(result: unknown): Record[] { + if (Array.isArray(result)) return result as Record[]; + if (result && typeof result === "object") { + const obj = result as Record; + if (Array.isArray(obj.data)) return obj.data as Record[]; + if (Array.isArray(obj.results)) return obj.results as Record[]; + } + return []; +} + +/** Stable string key for a record, read from the configured dedupeKey field. */ +function recordKey(rec: Record, dedupeKey: string): string | null { + const v = rec[dedupeKey]; + if (v === undefined || v === null) return null; + return String(v); +} + +/** + * Records strictly newer than the cursor, newest-first. Walks the page from + * the top until it hits the cursor key; if the cursor isn't on the page (it + * scrolled off, or this is a fresh page), every record counts as new (capped). + */ +function collectNewRecords( + records: Record[], + dedupeKey: string, + cursorKey: string | null, +): Record[] { + const fresh: Record[] = []; + for (const rec of records) { + if (recordKey(rec, dedupeKey) === cursorKey) break; + fresh.push(rec); + if (fresh.length >= MAX_FIRE_PER_POLL) break; + } + return fresh; +} + +/** Flatten a record's primitive fields to string inputs for {{param}} + * substitution, plus the full record as JSON under `record`. */ +function recordToInputs(rec: Record): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(rec)) { + if (v === null || v === undefined) continue; + if (typeof v === "object") continue; + out[k] = String(v); + } + out.record = JSON.stringify(rec); + return out; +} + +/** + * Process one poll trigger: run its read action, diff against the cursor, fire + * the workflow for each new record, and advance the cursor. Returns the number + * of workflow runs fired. + */ +async function pollOne( + row: PollTriggerRow, + cfg: PollConfig, + userId: string, + nowIso: string, +): Promise { + const supabase = getSupabase(); + + const result = await runAction( + userId, + cfg.app, + "read", + cfg.action, + cfg.inputs ?? {}, + cfg.connection, + ); + + // A guardrail may demand confirmation — we can't fire autonomously then. + if (result && typeof result === "object" && "__guardrail_confirmation_required" in result) { + console.warn(`[poll-driver] ${row.id} read needs confirmation — skipping this cycle`); + return 0; + } + + const records = extractRecords(result); + const cursorKey = row.last_dedupe_key ?? null; + const newestKey = records.length ? recordKey(records[0], cfg.dedupeKey) : cursorKey; + + // Advance the cursor + mark polled even when nothing is new, so the interval + // gate and baseline both work off last_fired_at / last_dedupe_key. + await supabase + .from("workflow_trigger") + .update({ last_dedupe_key: newestKey, last_fired_at: nowIso, updated_at: nowIso }) + .eq("id", row.id); + + // First poll: establish the baseline, don't replay the backlog. + if (cursorKey === null) return 0; + + const fresh = collectNewRecords(records, cfg.dedupeKey, cursorKey); + if (fresh.length === 0) return 0; + + let fired = 0; + // Fire oldest-first so the workflow sees records in chronological order. + for (const rec of fresh.reverse()) { + try { + for await (const ev of executeWorkflow(row.workflow_id, userId, recordToInputs(rec))) { + if (ev.type === "error") { + console.warn(`[poll-driver] ${row.id} workflow error: ${ev.message}`); + } else if (ev.type === "param_request") { + console.warn( + `[poll-driver] ${row.id} workflow needs params not in the record: ${ev.missing?.join(", ")}`, + ); + } + } + fired++; + } catch (err) { + console.error(`[poll-driver] ${row.id} fire failed:`, err); + } + } + return fired; +} + +/** Single-tick handler. Exported for testing. */ +export async function tickPoll(now: Date = new Date()): Promise<{ fired: number; polled: number }> { + const supabase = getSupabase(); + const nowIso = now.toISOString(); + + const { data: rows, error } = await supabase + .from("workflow_trigger") + .select("id, workflow_id, config, last_fired_at, last_dedupe_key") + .eq("type", "poll") + .eq("enabled", true); + if (error) throw new Error(`poll-driver: ${error.message}`); + + let fired = 0; + let polled = 0; + for (const row of (rows ?? []) as unknown as PollTriggerRow[]) { + let cfg: PollConfig; + try { + cfg = JSON.parse(row.config); + } catch { + console.warn(`[poll-driver] trigger ${row.id} has invalid config — skipping`); + continue; + } + if (!cfg.app || !cfg.action || !cfg.dedupeKey) { + console.warn(`[poll-driver] trigger ${row.id} missing app/action/dedupeKey — skipping`); + continue; + } + + // Interval gate — only hit the Zapier action once per intervalMinutes. + const intervalMs = Math.max(1, cfg.intervalMinutes ?? 5) * 60_000; + if (row.last_fired_at) { + const elapsed = now.getTime() - new Date(row.last_fired_at).getTime(); + if (elapsed < intervalMs) continue; + } + + // Resolve the workflow owner so runAction/executeWorkflow run as that user. + const { data: wf } = await supabase + .from("workflow") + .select("user_id") + .eq("id", row.workflow_id) + .maybeSingle(); + if (!wf) { + console.warn(`[poll-driver] trigger ${row.id} → workflow ${row.workflow_id} missing`); + continue; + } + + polled++; + try { + fired += await pollOne(row, cfg, wf.user_id as string, nowIso); + } catch (err) { + console.error(`[poll-driver] trigger ${row.id} failed:`, err); + } + } + return { fired, polled }; +} + +/** + * Start the driver. Returns a stop function. Self-aligns to wall-clock minute + * boundaries (first tick at the next :00 + 750ms — a hair after the cron driver + * so the two don't contend on the same trigger table read). + */ +export function startPollDriver(): () => void { + let stopped = false; + let timer: NodeJS.Timeout | null = null; + let running = false; + + const scheduleNext = () => { + if (stopped) return; + const now = new Date(); + const next = new Date(now); + next.setSeconds(0, 750); + if (next.getTime() <= now.getTime()) next.setMinutes(next.getMinutes() + 1); + const wait = next.getTime() - now.getTime(); + timer = setTimeout(async () => { + if (!running) { + running = true; + try { + const { fired, polled } = await tickPoll(new Date()); + if (fired > 0) + console.log(`[poll-driver] fired ${fired} run(s) across ${polled} trigger(s)`); + } catch (err) { + console.error("[poll-driver] tick failed:", err); + } finally { + running = false; + } + } + scheduleNext(); + }, wait); + }; + + scheduleNext(); + console.log("[poll-driver] started"); + return () => { + stopped = true; + if (timer) clearTimeout(timer); + console.log("[poll-driver] stopped"); + }; +} diff --git a/packages/agents/tests/unit/poll-driver.test.ts b/packages/agents/tests/unit/poll-driver.test.ts new file mode 100644 index 00000000..8f85d00e --- /dev/null +++ b/packages/agents/tests/unit/poll-driver.test.ts @@ -0,0 +1,185 @@ +/** + * Unit tests for the poll driver — baseline, diff-by-dedupeKey, cursor advance, + * and the interval gate. No network: runAction + executeWorkflow are mocked. + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// ─── Supabase + engine + zapier mocks ──────────────────────────────────────── + +type Result = { data: T; error: { message: string } | null }; +let triggerListResult: Result = { data: [], error: null }; +let workflowSelectResult: Result = { data: null, error: null }; +const updates: { table: string; payload: any }[] = []; + +function createChain(table: string) { + const builder: any = {}; + for (const m of ["select", "eq", "limit", "update"]) { + builder[m] = vi.fn().mockReturnValue(builder); + } + builder.maybeSingle = vi.fn().mockImplementation(() => Promise.resolve(workflowSelectResult)); + // biome-ignore lint/suspicious/noThenProperty: thenable mock + builder.then = (resolve: any) => { + if (builder._verb === "update") return resolve({ data: null, error: null }); + if (table === "workflow_trigger") return resolve(triggerListResult); + return resolve({ data: null, error: null }); + }; + const origUpdate = builder.update; + builder.update = vi.fn((payload: any) => { + builder._verb = "update"; + updates.push({ table, payload }); + return origUpdate(payload); + }); + return builder; +} + +const mockSupabase = { from: vi.fn((t: string) => createChain(t)) }; +vi.mock("@/lib/db", () => ({ getSupabase: () => mockSupabase })); + +const mockRunAction = vi.fn(); +vi.mock("@/lib/zapier/execution", () => ({ + runAction: (...args: unknown[]) => mockRunAction(...args), +})); + +const mockExecuteWorkflow = vi.fn(); +vi.mock("@/lib/workflows/engine", () => ({ + executeWorkflow: (...args: unknown[]) => mockExecuteWorkflow(...args), +})); + +async function* gen(events: unknown[] = [{ type: "complete" }]): AsyncGenerator { + for (const e of events) yield e; +} + +function pollRow(over: Partial> = {}) { + return { + id: "trig-1", + workflow_id: "wf-1", + config: JSON.stringify({ + app: "gmail", + action: "new_email", + dedupeKey: "id", + intervalMinutes: 5, + }), + last_fired_at: null, + last_dedupe_key: null, + ...over, + }; +} + +const lastTriggerUpdate = () => + [...updates].reverse().find((u) => u.table === "workflow_trigger")?.payload; + +// ─── Tests ─────────────────────────────────────────────────────────────────── + +describe("tickPoll", () => { + beforeEach(() => { + vi.resetAllMocks(); + triggerListResult = { data: [], error: null }; + workflowSelectResult = { data: { user_id: "user-1" }, error: null }; + updates.length = 0; + mockExecuteWorkflow.mockReturnValue(gen()); + }); + + it("first poll establishes the baseline — fires nothing, sets the cursor to newest", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: null })], error: null }; + mockRunAction.mockResolvedValue({ data: [{ id: "3" }, { id: "2" }, { id: "1" }] }); + + const { tickPoll } = await import("@/workflows/poll-driver"); + const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + + expect(out.polled).toBe(1); + expect(out.fired).toBe(0); + expect(mockExecuteWorkflow).not.toHaveBeenCalled(); + expect(lastTriggerUpdate()?.last_dedupe_key).toBe("3"); + }); + + it("fires once per new record above the cursor, oldest-first, then advances", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; + // newest-first page; cursor is "1", so "3" and "2" are new + mockRunAction.mockResolvedValue({ data: [{ id: "3" }, { id: "2" }, { id: "1" }] }); + + const { tickPoll } = await import("@/workflows/poll-driver"); + const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(2); + expect(mockExecuteWorkflow).toHaveBeenCalledTimes(2); + // oldest-first: "2" fired before "3" + expect(mockExecuteWorkflow.mock.calls[0][2]).toMatchObject({ id: "2" }); + expect(mockExecuteWorkflow.mock.calls[1][2]).toMatchObject({ id: "3" }); + expect(lastTriggerUpdate()?.last_dedupe_key).toBe("3"); + }); + + it("fires nothing when the newest record equals the cursor (no new data)", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "3" })], error: null }; + mockRunAction.mockResolvedValue({ data: [{ id: "3" }, { id: "2" }] }); + + const { tickPoll } = await import("@/workflows/poll-driver"); + const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(0); + expect(mockExecuteWorkflow).not.toHaveBeenCalled(); + }); + + it("respects the interval gate — skips a trigger polled too recently", async () => { + triggerListResult = { + data: [pollRow({ last_dedupe_key: "1", last_fired_at: "2026-05-10T12:00:00.000Z" })], + error: null, + }; + mockRunAction.mockResolvedValue({ data: [{ id: "9" }, { id: "1" }] }); + + const { tickPoll } = await import("@/workflows/poll-driver"); + // only 2 minutes later, intervalMinutes is 5 → not due + const out = await tickPoll(new Date("2026-05-10T12:02:00Z")); + + expect(out.polled).toBe(0); + expect(mockRunAction).not.toHaveBeenCalled(); + }); + + it("polls again once the interval has elapsed", async () => { + triggerListResult = { + data: [pollRow({ last_dedupe_key: "1", last_fired_at: "2026-05-10T12:00:00.000Z" })], + error: null, + }; + mockRunAction.mockResolvedValue({ data: [{ id: "9" }, { id: "1" }] }); + + const { tickPoll } = await import("@/workflows/poll-driver"); + const out = await tickPoll(new Date("2026-05-10T12:06:00Z")); + + expect(out.polled).toBe(1); + expect(out.fired).toBe(1); + }); + + it("skips a trigger missing app/action/dedupeKey", async () => { + triggerListResult = { + data: [pollRow({ config: JSON.stringify({ app: "gmail", action: "new_email" }) })], + error: null, + }; + + const { tickPoll } = await import("@/workflows/poll-driver"); + const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + + expect(out.polled).toBe(0); + expect(mockRunAction).not.toHaveBeenCalled(); + }); + + it("does not fire when a guardrail demands confirmation", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; + mockRunAction.mockResolvedValue({ __guardrail_confirmation_required: true }); + + const { tickPoll } = await import("@/workflows/poll-driver"); + const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(0); + expect(mockExecuteWorkflow).not.toHaveBeenCalled(); + }); + + it("handles a bare-array read result (no data wrapper)", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; + mockRunAction.mockResolvedValue([{ id: "2" }, { id: "1" }]); + + const { tickPoll } = await import("@/workflows/poll-driver"); + const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(1); + expect(mockExecuteWorkflow.mock.calls[0][2]).toMatchObject({ id: "2" }); + }); +}); From edac304e814d49107380183014af4c13b2940cd7 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 18:01:16 -0700 Subject: [PATCH 04/19] refactor(signals): host the poll trigger as an agent-mounted Mastra SignalProvider (foreman-ld7s) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the poll-trigger logic out of the bare worker (db63a26) and into a real Mastra primitive: ZapierPollSignalProvider, registered on the foreman agent via signals: [zapierPollProvider]. Foreman IS a Mastra app with agents, so the provider lives where it belongs — in the agent — which is what gives it connect()/notify(). It deliberately has NO pollInterval: the foreman agent is constructed in both the :4111 server and the :4112 webhook server, so framework auto-poll would double-fire. cron-driver-server (the single dedicated worker) drives the new runDuePolls() instead; poll-driver.ts is now just that scheduler. On each new record the provider fires executeWorkflow (deterministic, no LLM) AND drops a notification signal into the owner's thread (poll:) with the framework's native dedupeKey. notify() is additive + best-effort — a missing agent connection or notification-store error never blocks a run. Tests: 10 provider unit tests (logic + notify wiring asserting native dedupeKey + thread target + best-effort no-agent path) and an agent integration test proving the provider is connected on construction with no pollInterval. Full agents suite green (290); mastra build succeeds with the provider wired in. Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 3 +- packages/agents/src/mastra/agents/foreman.ts | 6 + .../mastra/signals/zapier-poll-provider.ts | 278 ++++++++++++++++++ packages/agents/src/workflows/poll-driver.ts | 248 +--------------- .../agents/tests/unit/mastra-agent.test.ts | 12 + ...r.test.ts => zapier-poll-provider.test.ts} | 73 +++-- 6 files changed, 363 insertions(+), 257 deletions(-) create mode 100644 packages/agents/src/mastra/signals/zapier-poll-provider.ts rename packages/agents/tests/unit/{poll-driver.test.ts => zapier-poll-provider.test.ts} (63%) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 5bc917df..152d0835 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,7 +92,8 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-ld7s","title":"[workflow] Un-stub the poll trigger as a Foreman-native poll driver (tickPoll)","description":"Poll trigger type is documented in the migration and accepted by list/detach but hidden by attach-trigger and has no driver. Build src/workflows/poll-driver.ts mirroring cron-driver: read workflow_trigger type=poll, run the Zapier read action via runAction, diff results by config.dedupeKey field against last_dedupe_key cursor, fire executeWorkflow on new records, persist cursor. Un-stub attach_trigger (add poll + pollConfig). Wire into cron-driver-server. Rationale (explored alpha API hands-on): Mastra SignalProvider.poll() skips when zero in-memory subscriptions and notify() requires an in-process agent — it is built for provider-lives-in-agent/notify-thread, not a singleton worker firing DB workflows. SignalProvider will be adopted on the webhook/channel path instead. Keeps door open to Zapier watchTriggerInbox SSE later (iyq6).","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:27:10Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:27:27Z","started_at":"2026-06-12T00:27:27Z","dependencies":[{"issue_id":"foreman-ld7s","depends_on_id":"foreman-c63f","type":"discovered-from","created_at":"2026-06-11T17:27:10Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-ueep","title":"[workflow] Live-verify the poll driver against a real Zapier read action","description":"poll-driver.ts (foreman-ld7s) is unit-tested only. Verify the real run path: bind a type=poll trigger to a real Zapier read action (e.g. gmail/new_email), run cron-driver-server, create a new record, assert exactly one workflow run fires and zero duplicates on re-poll; confirm baseline (first poll fires nothing) and intervalMinutes gating. Per verify-behavior-not-just-logic.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:35:12Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:12Z","dependencies":[{"issue_id":"foreman-ueep","depends_on_id":"foreman-ld7s","type":"discovered-from","created_at":"2026-06-11T17:35:12Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-ld7s","title":"[workflow] Un-stub the poll trigger as a Foreman-native poll driver (tickPoll)","description":"Poll trigger type is documented in the migration and accepted by list/detach but hidden by attach-trigger and has no driver. Build src/workflows/poll-driver.ts mirroring cron-driver: read workflow_trigger type=poll, run the Zapier read action via runAction, diff results by config.dedupeKey field against last_dedupe_key cursor, fire executeWorkflow on new records, persist cursor. Un-stub attach_trigger (add poll + pollConfig). Wire into cron-driver-server. Rationale (explored alpha API hands-on): Mastra SignalProvider.poll() skips when zero in-memory subscriptions and notify() requires an in-process agent — it is built for provider-lives-in-agent/notify-thread, not a singleton worker firing DB workflows. SignalProvider will be adopted on the webhook/channel path instead. Keeps door open to Zapier watchTriggerInbox SSE later (iyq6).","status":"closed","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:27:10Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:07Z","started_at":"2026-06-12T00:27:27Z","closed_at":"2026-06-12T00:35:07Z","close_reason":"Poll driver implemented + unit-tested; full agents suite green (288). Live Zapier integration tracked as follow-up.","dependencies":[{"issue_id":"foreman-ld7s","depends_on_id":"foreman-c63f","type":"discovered-from","created_at":"2026-06-11T17:27:10Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-22c3","title":"Evaluate Mastra signals/signal-providers/notifications vs Foreman's home-grown triggers","description":"Foreman is on @mastra/core 1.41 (post-bump). Mastra now ships native primitives that overlap with Foreman's hand-built trigger/workflow/notification system: (1) SignalProvider (poll OR webhook an external source -\u003e push notifications into agent threads) ~= Foreman's channel-trigger + cron-driver + the stubbed poll trigger (c63f); (2) built-in dedupeKey on notify() directly solves the dedup gap in tv5p (Foreman hand-rolls idempotency); (3) @mastra/github-signals is a production provider and Foreman already has a GitHub channel; (4) background-task suspend/resume for human approval + notification inbox ~= Foreman's proposal/approval flow. Sibling to iyq6 (Zapier-durable eval). Decide adopt-Mastra-native vs keep-own BEFORE building the v8k1 workflow epic. Confirmed code-side: backgroundTasks enabled in mastra/index.ts; pubsub is default in-process EventEmitter (no distributed backend — matters for 2-process + multi-instance); signals/signal-providers not used at all.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T22:33:47Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:33:47Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-rpsr","title":"Fix flaky db-types CI job — 'supabase gen types --local' intermittently errors 'Access token not provided'","description":"CI job 'db types fresh' (.github/workflows/test.yml) is flaky: passes on some commits (4dd20ff, 25b1ed1), fails on others (51e2065, f677794, b1070ae, 040b6a6) with 'Access token not provided' from 'npx supabase gen types typescript --local', producing empty output → diff shows 0a1,4128 (empty vs committed 4128-line database.types.ts). NOT a stale-types problem and NOT caused by the SDK/mastra bump. Root cause: supabase/setup-cli@v1 uses version:latest (unpinned), and the drifted CLI intermittently requires an access token even for --local. Fix options: (1) pin setup-cli to a known-good supabase version; (2) ensure 'supabase start' is fully healthy before gen; (3) set SUPABASE_ACCESS_TOKEN (project avoids secrets). Verify locally by starting Foreman's own supabase (note: a different project's supabase '*_Zillow' was running during diagnosis).","status":"closed","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-11T19:40:46Z","created_by":"HamChowderr","updated_at":"2026-06-11T20:18:52Z","closed_at":"2026-06-11T20:18:52Z","close_reason":"Fixed (379d9b4): added a placeholder SUPABASE_ACCESS_TOKEN to the CI db-types step. The latest supabase CLI demands a token's presence before local type-gen in Linux CI (value unused for --local). All 5 CI jobs green on main (run 27374657799). Also switched scripts to --local --workdir ../.. (cwd-independent) + documented the gotcha in CLAUDE.md.","dependencies":[{"issue_id":"foreman-rpsr","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T12:40:46Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-chxx","title":"Reskin README to myrp-build product-launch style (hero+badges, emoji sections, narrative, FAQ)","description":"Current README is content-complete but dry/dev-doc-toned. Rewrite to mirror hamchowderr/myrp-build README: centered hero + shields.io badges, emoji-prefixed sections split by ---, benefit-led narrative with example-prompt blockquotes, collapsible concrete example, ASCII pipeline diagram, dev-vs-self-hosted comparison table, annotated file-tree architecture, testing-tiers table, FAQ, acknowledgments, license stance. Preserve all factual content (5 agents, 34 Zapier tools, channel statuses, modes, testing tiers, deploy targets, live URLs, MIT license, contact). User-authorized.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T19:17:43Z","created_by":"HamChowderr","updated_at":"2026-06-11T19:28:04Z","closed_at":"2026-06-11T19:28:04Z","close_reason":"README reskinned to myrp-build product-launch style; committed b1070ae, pushed to origin/main, rendered + reviewed in Chrome.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/src/mastra/agents/foreman.ts b/packages/agents/src/mastra/agents/foreman.ts index beea31d9..bf9fc932 100644 --- a/packages/agents/src/mastra/agents/foreman.ts +++ b/packages/agents/src/mastra/agents/foreman.ts @@ -17,6 +17,7 @@ import { toolsWithCacheControl, } from "../../lib/providers"; import { generateZapierTools } from "../../lib/zapier-sdk-tools"; +import { zapierPollProvider } from "../signals/zapier-poll-provider"; import { attachTriggerTool } from "../tools/attach-trigger"; import { connectZapierTool } from "../tools/connect-zapier"; import { deleteWorkflowTool } from "../tools/delete-workflow"; @@ -177,6 +178,11 @@ export function createForemanAgent(databaseUrl: string) { stopWhen: stepCountIs(40), }, tools: () => buildForemanTools(), + // Poll triggers run as a Mastra SignalProvider hosted on this agent. It has + // no pollInterval (so it doesn't auto-poll in every process the agent is + // built in) — cron-driver-server drives its runDuePolls() on a single tick. + // Being agent-hosted gives it notify(): poll hits land in the user's thread. + signals: [zapierPollProvider], voice: process.env.OPENAI_API_KEY ? new OpenAIVoice() : undefined, scorers: { relevancy: { diff --git a/packages/agents/src/mastra/signals/zapier-poll-provider.ts b/packages/agents/src/mastra/signals/zapier-poll-provider.ts new file mode 100644 index 00000000..93af0d2c --- /dev/null +++ b/packages/agents/src/mastra/signals/zapier-poll-provider.ts @@ -0,0 +1,278 @@ +/** + * ZapierPollSignalProvider — a Mastra SignalProvider that watches Zapier read + * actions and fires saved workflows when new records appear. + * + * This lives ON the foreman agent (`signals: [zapierPollProvider]`), so it's a + * first-class Mastra primitive — not a worker bolted onto the side. Hosting it + * on the agent gives us `notify()`: when a poll turns up new data we drop a + * notification signal (with the framework's native `dedupeKey`) into the user's + * thread, so they *see* "new record → ran your workflow." + * + * Why we DON'T set `pollInterval`: the foreman agent is constructed in every + * Foreman process (the :4111 server AND the :4112 webhook server build it via + * the channel bots). If the provider auto-polled, it would fire in each process + * — the same double-fire the cron driver avoids by running a single dedicated + * instance. Instead we leave `pollInterval` undefined (no framework auto-poll) + * and drive `runDuePolls()` from the single `cron-driver-server` process. One + * Mastra app, the provider lives in the agent, one process ticks it. + * + * Firing stays deterministic: `runDuePolls()` runs the Zapier read via + * `runAction`, diffs results against the persisted `last_dedupe_key` cursor, and + * fires `executeWorkflow` once per new record — no LLM in the hot path. The + * `notify()` call is best-effort and additive (skipped if no agent is connected + * or the notification store is unavailable), so a notification hiccup never + * blocks a workflow run. + * + * Config (workflow_trigger.config for type='poll'): + * { app, action, connection?, inputs?, dedupeKey, intervalMinutes? } + * Reads are assumed newest-first (the Zapier "new X" convention). First poll + * establishes a baseline (sets the cursor, fires nothing) so binding a trigger + * doesn't replay the whole backlog. + */ + +import { SignalProvider } from "@mastra/core/signals"; +import { getSupabase } from "@/lib/db"; +import { executeWorkflow } from "@/lib/workflows/engine"; +import { runAction } from "@/lib/zapier/execution"; + +/** Cap on how many backlog records a single poll will fire, to avoid a flood if + * the cursor falls off the end of the returned page. */ +const MAX_FIRE_PER_POLL = 25; + +interface PollConfig { + app: string; + action: string; + connection?: string; + inputs?: Record; + /** Field on each record that uniquely identifies it (e.g. "id"). */ + dedupeKey: string; + /** How often to poll, in minutes. Defaults to 5. */ + intervalMinutes?: number; +} + +interface PollTriggerRow { + id: string; + workflow_id: string; + config: string; + last_fired_at: string | null; + last_dedupe_key: string | null; +} + +/** Normalize a Zapier read result into an array of record objects. Handles the + * common shapes: a bare array, `{ data: [...] }`, or `{ results: [...] }`. */ +function extractRecords(result: unknown): Record[] { + if (Array.isArray(result)) return result as Record[]; + if (result && typeof result === "object") { + const obj = result as Record; + if (Array.isArray(obj.data)) return obj.data as Record[]; + if (Array.isArray(obj.results)) return obj.results as Record[]; + } + return []; +} + +/** Stable string key for a record, read from the configured dedupeKey field. */ +function recordKey(rec: Record, dedupeKey: string): string | null { + const v = rec[dedupeKey]; + if (v === undefined || v === null) return null; + return String(v); +} + +/** + * Records strictly newer than the cursor, newest-first. Walks the page from the + * top until it hits the cursor key; if the cursor isn't on the page (it scrolled + * off, or this is a fresh page), every record counts as new (capped). + */ +function collectNewRecords( + records: Record[], + dedupeKey: string, + cursorKey: string | null, +): Record[] { + const fresh: Record[] = []; + for (const rec of records) { + if (recordKey(rec, dedupeKey) === cursorKey) break; + fresh.push(rec); + if (fresh.length >= MAX_FIRE_PER_POLL) break; + } + return fresh; +} + +/** Flatten a record's primitive fields to string inputs for {{param}} + * substitution, plus the full record as JSON under `record`. */ +function recordToInputs(rec: Record): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(rec)) { + if (v === null || v === undefined) continue; + if (typeof v === "object") continue; + out[k] = String(v); + } + out.record = JSON.stringify(rec); + return out; +} + +export class ZapierPollSignalProvider extends SignalProvider<"zapier-poll"> { + readonly id = "zapier-poll" as const; + readonly name = "Zapier Poll Signals"; + // No `pollInterval` — see the file header. Driven by cron-driver-server. + + /** + * Drop a notification signal into the workflow owner's thread. Best-effort: + * the workflow fire is the primary action, so a missing agent connection or + * notification-store error must never block it. + */ + private async notifyOwner( + triggerId: string, + cfg: PollConfig, + userId: string, + rec: Record, + key: string | null, + ): Promise { + if (!this.agent) return; // not connected (e.g. unit test, or non-worker process) + try { + await this.notify( + { + source: "zapier-poll", + kind: `${cfg.app}.${cfg.action}`, + summary: `New ${cfg.app} ${cfg.action}${key ? ` (${key})` : ""}`, + payload: rec, + // Native dedup — the same record key won't create a duplicate + // notification even if a re-poll surfaces it again. + dedupeKey: key ?? undefined, + }, + { threadId: `poll:${triggerId}`, resourceId: userId }, + ); + } catch (err) { + console.warn(`[zapier-poll] notify failed for trigger ${triggerId}:`, err); + } + } + + /** + * Process one poll trigger: run its read action, diff against the cursor, fire + * the workflow for each new record, notify the owner's thread, and advance the + * cursor. Returns the number of workflow runs fired. + */ + private async pollOne( + row: PollTriggerRow, + cfg: PollConfig, + userId: string, + nowIso: string, + ): Promise { + const supabase = getSupabase(); + + const result = await runAction( + userId, + cfg.app, + "read", + cfg.action, + cfg.inputs ?? {}, + cfg.connection, + ); + + // A guardrail may demand confirmation — we can't fire autonomously then. + if (result && typeof result === "object" && "__guardrail_confirmation_required" in result) { + console.warn(`[zapier-poll] ${row.id} read needs confirmation — skipping this cycle`); + return 0; + } + + const records = extractRecords(result); + const cursorKey = row.last_dedupe_key ?? null; + const newestKey = records.length ? recordKey(records[0], cfg.dedupeKey) : cursorKey; + + // Advance the cursor + mark polled even when nothing is new, so the interval + // gate and baseline both work off last_fired_at / last_dedupe_key. + await supabase + .from("workflow_trigger") + .update({ last_dedupe_key: newestKey, last_fired_at: nowIso, updated_at: nowIso }) + .eq("id", row.id); + + // First poll: establish the baseline, don't replay the backlog. + if (cursorKey === null) return 0; + + const fresh = collectNewRecords(records, cfg.dedupeKey, cursorKey); + if (fresh.length === 0) return 0; + + let fired = 0; + // Fire oldest-first so the workflow sees records in chronological order. + for (const rec of fresh.reverse()) { + const key = recordKey(rec, cfg.dedupeKey); + try { + for await (const ev of executeWorkflow(row.workflow_id, userId, recordToInputs(rec))) { + if (ev.type === "error") { + console.warn(`[zapier-poll] ${row.id} workflow error: ${ev.message}`); + } else if (ev.type === "param_request") { + console.warn( + `[zapier-poll] ${row.id} workflow needs params not in the record: ${ev.missing?.join(", ")}`, + ); + } + } + fired++; + // Additive: surface the fire in the owner's thread via the signal. + await this.notifyOwner(row.id, cfg, userId, rec, key); + } catch (err) { + console.error(`[zapier-poll] ${row.id} fire failed:`, err); + } + } + return fired; + } + + /** + * Run every due poll trigger once. Called by cron-driver-server on its tick. + * Exported behavior mirrors the cron driver's `tickCron`. + */ + async runDuePolls(now: Date = new Date()): Promise<{ fired: number; polled: number }> { + const supabase = getSupabase(); + const nowIso = now.toISOString(); + + const { data: rows, error } = await supabase + .from("workflow_trigger") + .select("id, workflow_id, config, last_fired_at, last_dedupe_key") + .eq("type", "poll") + .eq("enabled", true); + if (error) throw new Error(`zapier-poll: ${error.message}`); + + let fired = 0; + let polled = 0; + for (const row of (rows ?? []) as unknown as PollTriggerRow[]) { + let cfg: PollConfig; + try { + cfg = JSON.parse(row.config); + } catch { + console.warn(`[zapier-poll] trigger ${row.id} has invalid config — skipping`); + continue; + } + if (!cfg.app || !cfg.action || !cfg.dedupeKey) { + console.warn(`[zapier-poll] trigger ${row.id} missing app/action/dedupeKey — skipping`); + continue; + } + + // Interval gate — only hit the Zapier action once per intervalMinutes. + const intervalMs = Math.max(1, cfg.intervalMinutes ?? 5) * 60_000; + if (row.last_fired_at) { + const elapsed = now.getTime() - new Date(row.last_fired_at).getTime(); + if (elapsed < intervalMs) continue; + } + + // Resolve the workflow owner so runAction/executeWorkflow run as that user. + const { data: wf } = await supabase + .from("workflow") + .select("user_id") + .eq("id", row.workflow_id) + .maybeSingle(); + if (!wf) { + console.warn(`[zapier-poll] trigger ${row.id} → workflow ${row.workflow_id} missing`); + continue; + } + + polled++; + try { + fired += await this.pollOne(row, cfg, wf.user_id as string, nowIso); + } catch (err) { + console.error(`[zapier-poll] trigger ${row.id} failed:`, err); + } + } + return { fired, polled }; + } +} + +/** Singleton hosted on the foreman agent (`signals: [...]`) and driven by the + * cron-driver-server worker. */ +export const zapierPollProvider = new ZapierPollSignalProvider(); diff --git a/packages/agents/src/workflows/poll-driver.ts b/packages/agents/src/workflows/poll-driver.ts index f1554680..c4302f8f 100644 --- a/packages/agents/src/workflows/poll-driver.ts +++ b/packages/agents/src/workflows/poll-driver.ts @@ -1,241 +1,20 @@ /** - * Poll driver — long-running worker that fires `workflow_trigger` rows of - * type='poll' when a Zapier read action returns records newer than the last - * one we've seen. + * Poll driver scheduler — the single-process tick that drives the + * ZapierPollSignalProvider. * - * Model (matches the migration comment on workflow_trigger): - * config = { app, action, connection?, inputs?, dedupeKey, intervalMinutes? } - * - `dedupeKey` is the field on each returned record that uniquely identifies - * it (e.g. "id"). Reads are assumed newest-first (the Zapier convention for - * "new X" triggers). - * - `last_dedupe_key` is the persisted cursor: the key of the newest record - * we've already processed. New records = those above the cursor. - * - `last_fired_at` doubles as the last-polled timestamp; `intervalMinutes` - * gates how often we actually hit the Zapier action. - * - * First poll establishes the baseline (cursor was null → set it, fire nothing) - * so binding a trigger doesn't replay the whole backlog. Subsequent polls fire - * the saved workflow once per new record, oldest-first. - * - * Like the cron driver, this is intentionally simple — no distributed lock, no - * missed-tick catch-up. Run a single instance (it lives in cron-driver-server - * alongside the cron driver). Unlike cron we AWAIT each trigger's processing: - * poll reads aren't minute-precision-sensitive, awaiting gives an accurate - * fired count, and there are few poll triggers. - * - * Why a hand-rolled worker and not a Mastra SignalProvider: the alpha - * SignalProvider poll loop skips entirely when its in-memory subscription - * registry is empty, and `notify()` throws unless the provider is registered on - * an in-process Agent — it's built for a provider that lives inside the agent - * and notifies threads, not a singleton worker that fires DB-backed workflows. - * The SignalProvider primitive is adopted on the webhook/channel path instead. - * This worker keeps the door open to swap in Zapier's `watchTriggerInbox` SSE - * later (foreman-iyq6) without touching the trigger storage or tools. - */ - -import { getSupabase } from "@/lib/db"; -import { executeWorkflow } from "@/lib/workflows/engine"; -import { runAction } from "@/lib/zapier/execution"; - -/** Cap on how many backlog records a single poll will fire, to avoid a flood - * if the cursor falls off the end of the returned page. */ -const MAX_FIRE_PER_POLL = 25; - -interface PollConfig { - app: string; - action: string; - connection?: string; - inputs?: Record; - /** Field on each record that uniquely identifies it (e.g. "id"). */ - dedupeKey: string; - /** How often to poll, in minutes. Defaults to 5. */ - intervalMinutes?: number; -} - -interface PollTriggerRow { - id: string; - workflow_id: string; - config: string; - last_fired_at: string | null; - last_dedupe_key: string | null; -} - -/** Normalize a Zapier read result into an array of record objects. Handles the - * common shapes: a bare array, `{ data: [...] }`, or `{ results: [...] }`. */ -function extractRecords(result: unknown): Record[] { - if (Array.isArray(result)) return result as Record[]; - if (result && typeof result === "object") { - const obj = result as Record; - if (Array.isArray(obj.data)) return obj.data as Record[]; - if (Array.isArray(obj.results)) return obj.results as Record[]; - } - return []; -} - -/** Stable string key for a record, read from the configured dedupeKey field. */ -function recordKey(rec: Record, dedupeKey: string): string | null { - const v = rec[dedupeKey]; - if (v === undefined || v === null) return null; - return String(v); -} - -/** - * Records strictly newer than the cursor, newest-first. Walks the page from - * the top until it hits the cursor key; if the cursor isn't on the page (it - * scrolled off, or this is a fresh page), every record counts as new (capped). - */ -function collectNewRecords( - records: Record[], - dedupeKey: string, - cursorKey: string | null, -): Record[] { - const fresh: Record[] = []; - for (const rec of records) { - if (recordKey(rec, dedupeKey) === cursorKey) break; - fresh.push(rec); - if (fresh.length >= MAX_FIRE_PER_POLL) break; - } - return fresh; -} - -/** Flatten a record's primitive fields to string inputs for {{param}} - * substitution, plus the full record as JSON under `record`. */ -function recordToInputs(rec: Record): Record { - const out: Record = {}; - for (const [k, v] of Object.entries(rec)) { - if (v === null || v === undefined) continue; - if (typeof v === "object") continue; - out[k] = String(v); - } - out.record = JSON.stringify(rec); - return out; -} - -/** - * Process one poll trigger: run its read action, diff against the cursor, fire - * the workflow for each new record, and advance the cursor. Returns the number - * of workflow runs fired. + * The provider itself lives on the foreman agent (a Mastra SignalProvider). It + * deliberately has no `pollInterval` (which would auto-poll in every process the + * agent is built in). This scheduler — run only inside cron-driver-server — is + * the one place that calls `runDuePolls()`, self-aligned to wall-clock minute + * boundaries (a hair after the cron driver so the two don't contend on the same + * trigger-table read). */ -async function pollOne( - row: PollTriggerRow, - cfg: PollConfig, - userId: string, - nowIso: string, -): Promise { - const supabase = getSupabase(); - - const result = await runAction( - userId, - cfg.app, - "read", - cfg.action, - cfg.inputs ?? {}, - cfg.connection, - ); - - // A guardrail may demand confirmation — we can't fire autonomously then. - if (result && typeof result === "object" && "__guardrail_confirmation_required" in result) { - console.warn(`[poll-driver] ${row.id} read needs confirmation — skipping this cycle`); - return 0; - } - const records = extractRecords(result); - const cursorKey = row.last_dedupe_key ?? null; - const newestKey = records.length ? recordKey(records[0], cfg.dedupeKey) : cursorKey; - - // Advance the cursor + mark polled even when nothing is new, so the interval - // gate and baseline both work off last_fired_at / last_dedupe_key. - await supabase - .from("workflow_trigger") - .update({ last_dedupe_key: newestKey, last_fired_at: nowIso, updated_at: nowIso }) - .eq("id", row.id); - - // First poll: establish the baseline, don't replay the backlog. - if (cursorKey === null) return 0; - - const fresh = collectNewRecords(records, cfg.dedupeKey, cursorKey); - if (fresh.length === 0) return 0; - - let fired = 0; - // Fire oldest-first so the workflow sees records in chronological order. - for (const rec of fresh.reverse()) { - try { - for await (const ev of executeWorkflow(row.workflow_id, userId, recordToInputs(rec))) { - if (ev.type === "error") { - console.warn(`[poll-driver] ${row.id} workflow error: ${ev.message}`); - } else if (ev.type === "param_request") { - console.warn( - `[poll-driver] ${row.id} workflow needs params not in the record: ${ev.missing?.join(", ")}`, - ); - } - } - fired++; - } catch (err) { - console.error(`[poll-driver] ${row.id} fire failed:`, err); - } - } - return fired; -} - -/** Single-tick handler. Exported for testing. */ -export async function tickPoll(now: Date = new Date()): Promise<{ fired: number; polled: number }> { - const supabase = getSupabase(); - const nowIso = now.toISOString(); - - const { data: rows, error } = await supabase - .from("workflow_trigger") - .select("id, workflow_id, config, last_fired_at, last_dedupe_key") - .eq("type", "poll") - .eq("enabled", true); - if (error) throw new Error(`poll-driver: ${error.message}`); - - let fired = 0; - let polled = 0; - for (const row of (rows ?? []) as unknown as PollTriggerRow[]) { - let cfg: PollConfig; - try { - cfg = JSON.parse(row.config); - } catch { - console.warn(`[poll-driver] trigger ${row.id} has invalid config — skipping`); - continue; - } - if (!cfg.app || !cfg.action || !cfg.dedupeKey) { - console.warn(`[poll-driver] trigger ${row.id} missing app/action/dedupeKey — skipping`); - continue; - } - - // Interval gate — only hit the Zapier action once per intervalMinutes. - const intervalMs = Math.max(1, cfg.intervalMinutes ?? 5) * 60_000; - if (row.last_fired_at) { - const elapsed = now.getTime() - new Date(row.last_fired_at).getTime(); - if (elapsed < intervalMs) continue; - } - - // Resolve the workflow owner so runAction/executeWorkflow run as that user. - const { data: wf } = await supabase - .from("workflow") - .select("user_id") - .eq("id", row.workflow_id) - .maybeSingle(); - if (!wf) { - console.warn(`[poll-driver] trigger ${row.id} → workflow ${row.workflow_id} missing`); - continue; - } - - polled++; - try { - fired += await pollOne(row, cfg, wf.user_id as string, nowIso); - } catch (err) { - console.error(`[poll-driver] trigger ${row.id} failed:`, err); - } - } - return { fired, polled }; -} +import { zapierPollProvider } from "@/mastra/signals/zapier-poll-provider"; /** - * Start the driver. Returns a stop function. Self-aligns to wall-clock minute - * boundaries (first tick at the next :00 + 750ms — a hair after the cron driver - * so the two don't contend on the same trigger table read). + * Start the poll scheduler. Returns a stop function. First tick lands at the + * next :00 + 750ms. */ export function startPollDriver(): () => void { let stopped = false; @@ -253,9 +32,10 @@ export function startPollDriver(): () => void { if (!running) { running = true; try { - const { fired, polled } = await tickPoll(new Date()); - if (fired > 0) + const { fired, polled } = await zapierPollProvider.runDuePolls(new Date()); + if (fired > 0) { console.log(`[poll-driver] fired ${fired} run(s) across ${polled} trigger(s)`); + } } catch (err) { console.error("[poll-driver] tick failed:", err); } finally { diff --git a/packages/agents/tests/unit/mastra-agent.test.ts b/packages/agents/tests/unit/mastra-agent.test.ts index f27cfaf1..4bb128c4 100644 --- a/packages/agents/tests/unit/mastra-agent.test.ts +++ b/packages/agents/tests/unit/mastra-agent.test.ts @@ -43,4 +43,16 @@ describe("Mastra foreman agent", () => { const agent = await createForemanAgent("file:./test-agent.db"); expect(agent.model).toEqual(AGENT_MODELS.foreman); }, 30000); + + it("hosts the zapier poll SignalProvider and connects it on construction", async () => { + const { createForemanAgent } = await import("@/mastra/agents/foreman"); + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + await createForemanAgent("file:./test-agent.db"); + // The Agent constructor calls provider.connect(this) for each signals entry, + // proving the primitive is live in the agent (not merely defined). + expect(zapierPollProvider.isConnected).toBe(true); + // No pollInterval — we drive runDuePolls() from cron-driver-server, so the + // framework must NOT auto-poll it in every process the agent is built in. + expect(zapierPollProvider.pollInterval).toBeUndefined(); + }, 30000); }); diff --git a/packages/agents/tests/unit/poll-driver.test.ts b/packages/agents/tests/unit/zapier-poll-provider.test.ts similarity index 63% rename from packages/agents/tests/unit/poll-driver.test.ts rename to packages/agents/tests/unit/zapier-poll-provider.test.ts index 8f85d00e..e6cfcd45 100644 --- a/packages/agents/tests/unit/poll-driver.test.ts +++ b/packages/agents/tests/unit/zapier-poll-provider.test.ts @@ -1,6 +1,8 @@ /** - * Unit tests for the poll driver — baseline, diff-by-dedupeKey, cursor advance, - * and the interval gate. No network: runAction + executeWorkflow are mocked. + * Unit tests for the ZapierPollSignalProvider — baseline, diff-by-dedupeKey, + * cursor advance, the interval gate, and the notify() wiring (native dedupeKey + * into the owner's thread). No network: runAction + executeWorkflow are mocked, + * and notify() goes to a stub agent. */ import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -70,7 +72,7 @@ const lastTriggerUpdate = () => // ─── Tests ─────────────────────────────────────────────────────────────────── -describe("tickPoll", () => { +describe("ZapierPollSignalProvider.runDuePolls", () => { beforeEach(() => { vi.resetAllMocks(); triggerListResult = { data: [], error: null }; @@ -83,8 +85,8 @@ describe("tickPoll", () => { triggerListResult = { data: [pollRow({ last_dedupe_key: null })], error: null }; mockRunAction.mockResolvedValue({ data: [{ id: "3" }, { id: "2" }, { id: "1" }] }); - const { tickPoll } = await import("@/workflows/poll-driver"); - const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); expect(out.polled).toBe(1); expect(out.fired).toBe(0); @@ -94,15 +96,13 @@ describe("tickPoll", () => { it("fires once per new record above the cursor, oldest-first, then advances", async () => { triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; - // newest-first page; cursor is "1", so "3" and "2" are new mockRunAction.mockResolvedValue({ data: [{ id: "3" }, { id: "2" }, { id: "1" }] }); - const { tickPoll } = await import("@/workflows/poll-driver"); - const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); expect(out.fired).toBe(2); expect(mockExecuteWorkflow).toHaveBeenCalledTimes(2); - // oldest-first: "2" fired before "3" expect(mockExecuteWorkflow.mock.calls[0][2]).toMatchObject({ id: "2" }); expect(mockExecuteWorkflow.mock.calls[1][2]).toMatchObject({ id: "3" }); expect(lastTriggerUpdate()?.last_dedupe_key).toBe("3"); @@ -112,8 +112,8 @@ describe("tickPoll", () => { triggerListResult = { data: [pollRow({ last_dedupe_key: "3" })], error: null }; mockRunAction.mockResolvedValue({ data: [{ id: "3" }, { id: "2" }] }); - const { tickPoll } = await import("@/workflows/poll-driver"); - const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); expect(out.fired).toBe(0); expect(mockExecuteWorkflow).not.toHaveBeenCalled(); @@ -126,9 +126,8 @@ describe("tickPoll", () => { }; mockRunAction.mockResolvedValue({ data: [{ id: "9" }, { id: "1" }] }); - const { tickPoll } = await import("@/workflows/poll-driver"); - // only 2 minutes later, intervalMinutes is 5 → not due - const out = await tickPoll(new Date("2026-05-10T12:02:00Z")); + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:02:00Z")); expect(out.polled).toBe(0); expect(mockRunAction).not.toHaveBeenCalled(); @@ -141,8 +140,8 @@ describe("tickPoll", () => { }; mockRunAction.mockResolvedValue({ data: [{ id: "9" }, { id: "1" }] }); - const { tickPoll } = await import("@/workflows/poll-driver"); - const out = await tickPoll(new Date("2026-05-10T12:06:00Z")); + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:06:00Z")); expect(out.polled).toBe(1); expect(out.fired).toBe(1); @@ -154,8 +153,8 @@ describe("tickPoll", () => { error: null, }; - const { tickPoll } = await import("@/workflows/poll-driver"); - const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); expect(out.polled).toBe(0); expect(mockRunAction).not.toHaveBeenCalled(); @@ -165,8 +164,8 @@ describe("tickPoll", () => { triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; mockRunAction.mockResolvedValue({ __guardrail_confirmation_required: true }); - const { tickPoll } = await import("@/workflows/poll-driver"); - const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); expect(out.fired).toBe(0); expect(mockExecuteWorkflow).not.toHaveBeenCalled(); @@ -176,10 +175,40 @@ describe("tickPoll", () => { triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; mockRunAction.mockResolvedValue([{ id: "2" }, { id: "1" }]); - const { tickPoll } = await import("@/workflows/poll-driver"); - const out = await tickPoll(new Date("2026-05-10T12:00:00Z")); + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); expect(out.fired).toBe(1); expect(mockExecuteWorkflow.mock.calls[0][2]).toMatchObject({ id: "2" }); }); + + it("notify()s the owner's thread with a native dedupeKey for each new record", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; + mockRunAction.mockResolvedValue({ data: [{ id: "2" }, { id: "1" }] }); + + const { ZapierPollSignalProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const provider = new ZapierPollSignalProvider(); + const sendNotificationSignal = vi.fn().mockResolvedValue(undefined); + // Hosting on an agent is what gives the provider notify() — stub that link. + provider.connect({ sendNotificationSignal } as any); + + const out = await provider.runDuePolls(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(1); + expect(sendNotificationSignal).toHaveBeenCalledTimes(1); + const [notification, target] = sendNotificationSignal.mock.calls[0]; + expect(notification).toMatchObject({ source: "zapier-poll", dedupeKey: "2" }); + expect(target).toMatchObject({ threadId: "poll:trig-1", resourceId: "user-1" }); + }); + + it("does not throw when no agent is connected (notify is best-effort)", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; + mockRunAction.mockResolvedValue({ data: [{ id: "2" }, { id: "1" }] }); + + const { ZapierPollSignalProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const provider = new ZapierPollSignalProvider(); // not connected + const out = await provider.runDuePolls(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(1); // firing still succeeds without notify + }); }); From fae9ca709a580bb4e4d8eaf04b1e77fb8f6a5e29 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 20:22:53 -0700 Subject: [PATCH 05/19] feat(signals): channel triggers as an agent-hosted SignalProvider with native dedup (foreman-tv5p) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ChannelTriggerSignalProvider, hosted on the foreman agent alongside the poll provider. matchAndFireChannelTriggers is now a thin wrapper delegating to it; the match/fire logic moved into the provider. Fixes tv5p's channel half: a retried webhook re-delivers the same message and previously fired the workflow twice. The provider derives a delivery key — the caller-supplied dedupeKey (e.g. a platform message id) when available, else a content hash of channel|room|from|text — and skips a trigger whose persisted last_dedupe_key already equals it, recording the key before firing. ChannelMessage gains an optional dedupeKey field (callers can pass a real message id; content hash is the fallback). On a fresh fire it also drops a notification signal (native dedupeKey) into the user's thread — additive + best-effort, never blocking the run. Tests: existing 6 match tests preserved + retry-skip, new-fire-records-key, content-hash fallback, and notify() wiring (native dedupeKey + thread target). Agent integration test now asserts both providers connect on construction with no pollInterval. Full agents suite green (295). Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 2 +- packages/agents/src/mastra/agents/foreman.ts | 12 +- .../signals/channel-trigger-provider.ts | 143 ++++++++++++++++ .../agents/src/workflows/channel-trigger.ts | 90 ++-------- .../agents/tests/unit/channel-trigger.test.ts | 154 +++++++++++------- .../agents/tests/unit/mastra-agent.test.ts | 11 +- 6 files changed, 267 insertions(+), 145 deletions(-) create mode 100644 packages/agents/src/mastra/signals/channel-trigger-provider.ts diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 152d0835..e029ab25 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -103,7 +103,7 @@ {"_type":"issue","id":"foreman-ezsm","title":"[workflow] Build the /workflows UI page (list saved automations + run history)","description":"foreman-v8k1 calls for a UI so users can see saved automations; it does not exist. Build a /workflows page in packages/web listing workflows + triggers + recent runs (status, error_message from the observability issue), using the existing /workflows/* API routes and the shadcn stack. Per-workflow run-history view.","acceptance_criteria":"/workflows lists the user's workflows + triggers; clicking shows run history incl. failed-run errors; reads from existing API routes.","status":"open","priority":2,"issue_type":"feature","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:13Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:13Z","dependencies":[{"issue_id":"foreman-ezsm","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:13Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-8fcs","title":"[workflow] Tests: params.ts substitution + engine step-failure paths","description":"params.ts (extract/substitute/validate) and engine.ts step-failure handling (~80-122) are untested. Add unit tests: substituteParams (partial replace, unknown keys left as-is, nested/arrays), validateParams (missing detected), engine step failure (runAction throws -\u003e failed + error captured), generator-throw cleanup, empty-steps early-exit.","acceptance_criteria":"params + engine error paths covered; npm test green.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:09Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:09Z","dependencies":[{"issue_id":"foreman-8fcs","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:08Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-2afc","title":"[workflow] Run-state \u0026 observability: orphaned 'running' rows on crash; persist error_message + trigger context","description":"engine.ts inserts workflow_run status=running (~line 56) and never sets failed if executeWorkflow throws before completing -\u003e rows orphaned forever. Wrap generator consumption in try/finally at each call site (routes/workflows.ts, channel-trigger.ts, cron-driver.ts) to mark failed with the error. Persist error_message + fired_by(manual/cron/channel) + trigger_id on workflow_run; trigger-fired runs only console.warn today and never appear in run history. Surface in GET /workflows/:id/runs.","acceptance_criteria":"A failed run shows status=failed + error_message within ~1s; cron/channel-fired runs appear in run history with trigger context; no rows stuck in running.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:04Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:04Z","dependencies":[{"issue_id":"foreman-2afc","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:03Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-tv5p","title":"[workflow] BUG: trigger dedup/idempotency — channel-trigger has no dedup; cron can double-fire on same-minute restart","description":"channel-trigger.ts (matchAndFireChannelTriggers ~43-79) fires with NO dedup despite an unused workflow_trigger.last_dedupe_key column — a retried webhook = duplicate runs. Add a dedupe key (channel+sender+ts+text hash or delivery id) checked against last_dedupe_key before firing. Separately cron-driver.ts (~164-167) can double-fire if the process restarts mid-minute; add a guard/lock. runOne also swallows errors silently.","acceptance_criteria":"Duplicate webhook =\u003e 1 run not 2 (unit test); a new message =\u003e new run; cron fires once per minute even across a same-minute restart (unit test).","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:59Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:30:59Z","dependencies":[{"issue_id":"foreman-tv5p","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:30:58Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-tv5p","title":"[workflow] BUG: trigger dedup/idempotency — channel-trigger has no dedup; cron can double-fire on same-minute restart","description":"channel-trigger.ts (matchAndFireChannelTriggers ~43-79) fires with NO dedup despite an unused workflow_trigger.last_dedupe_key column — a retried webhook = duplicate runs. Add a dedupe key (channel+sender+ts+text hash or delivery id) checked against last_dedupe_key before firing. Separately cron-driver.ts (~164-167) can double-fire if the process restarts mid-minute; add a guard/lock. runOne also swallows errors silently.","acceptance_criteria":"Duplicate webhook =\u003e 1 run not 2 (unit test); a new message =\u003e new run; cron fires once per minute even across a same-minute restart (unit test).","status":"in_progress","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:59Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:15:08Z","started_at":"2026-06-12T03:15:08Z","dependencies":[{"issue_id":"foreman-tv5p","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:30:58Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-wzmx","title":"[aimock] Resolve declared-but-unfixtured MCP/A2A mocks (write fixtures or remove declarations)","description":"aimock.json declares 8 MCP tools + 5 A2A agents but tests/fixtures/aimock/ has no mcp/ or a2a/ fixtures — a declared-vs-real mismatch that 503s under strict mode. Either write minimal fixtures for the tools/agents tests actually exercise, or remove the unused declarations so config reflects reality.","acceptance_criteria":"Every MCP/A2A declaration has a fixture OR is removed; npm test passes under strict mode with no unmatched MCP/A2A requests.","status":"closed","priority":2,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:27Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:42:33Z","started_at":"2026-06-11T21:37:23Z","closed_at":"2026-06-11T21:42:33Z","close_reason":"MCP/A2A declarations made real; orphaned mcp/tools.json folded in and removed","dependencies":[{"issue_id":"foreman-wzmx","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:27Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-wrrd","title":"[aimock] Enable strict mode and tighten/remove the empty-match catch-all fixture","description":"@copilotkit/aimock supports strict:true (verified config-loader.d.ts:101) so unmatched requests 503 instead of silently matching z-catchall.json (match {}), which currently risks false-green tests. Enable strict in packages/agents/aimock.json and remove/convert z-catchall to a strict error, fixing any tests that relied on the fallback.","acceptance_criteria":"aimock.json has strict:true; z-catchall removed/converted; npm test passes with every test hitting an explicit fixture.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:22Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:43:26Z","closed_at":"2026-06-11T21:43:26Z","close_reason":"strict:true enabled in aimock.json; z-catchall.json deleted; npm test 278 passed/10 skipped under strict; strict 503 verified via probe","dependencies":[{"issue_id":"foreman-wrrd","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:21Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-xnis","title":"[bump] Surface getProfile as an agent tool; confirm trash option auto-surfaces on table-list tools","description":"Post-bump, surface worthwhile new stable methods. Add getProfile to READ_ONLY in zapier-sdk-tools.ts (agent can answer who-am-I-on-Zapier). Confirm the 0.57 trash option (exclude/include/only) auto-surfaces in listTableRecords/listTableFields tool schemas via the registry (likely no code change). Keep client-credentials mgmt + listTriggers excluded (documented in capability map).","acceptance_criteria":"getProfile in READ_ONLY and callable; trash param present in table-list tool input schema; tests pass.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:17Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:50:31Z","closed_at":"2026-06-11T21:50:31Z","close_reason":"getProfile already surfaced as a read-only tool (in READ_ONLY set, generated via sdk.getRegistry; added 7wks ago in 2092317) — agent can answer who-am-I-on-Zapier, no change needed. 'Trash option': no 'trash' field exists anywhere in @zapier/zapier-sdk@0.69.3; tool params auto-surface from SDK Zod schemas (mechanism verified) but there is nothing to surface — note is stale.","dependencies":[{"issue_id":"foreman-xnis","depends_on_id":"foreman-8ujc","type":"discovered-from","created_at":"2026-06-11T01:30:17Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/src/mastra/agents/foreman.ts b/packages/agents/src/mastra/agents/foreman.ts index bf9fc932..bf60b787 100644 --- a/packages/agents/src/mastra/agents/foreman.ts +++ b/packages/agents/src/mastra/agents/foreman.ts @@ -17,6 +17,7 @@ import { toolsWithCacheControl, } from "../../lib/providers"; import { generateZapierTools } from "../../lib/zapier-sdk-tools"; +import { channelTriggerProvider } from "../signals/channel-trigger-provider"; import { zapierPollProvider } from "../signals/zapier-poll-provider"; import { attachTriggerTool } from "../tools/attach-trigger"; import { connectZapierTool } from "../tools/connect-zapier"; @@ -178,11 +179,12 @@ export function createForemanAgent(databaseUrl: string) { stopWhen: stepCountIs(40), }, tools: () => buildForemanTools(), - // Poll triggers run as a Mastra SignalProvider hosted on this agent. It has - // no pollInterval (so it doesn't auto-poll in every process the agent is - // built in) — cron-driver-server drives its runDuePolls() on a single tick. - // Being agent-hosted gives it notify(): poll hits land in the user's thread. - signals: [zapierPollProvider], + // Triggers run as Mastra SignalProviders hosted on this agent (agent-hosted + // gives them notify() → events land in the user's thread). Neither sets a + // pollInterval: the poll provider is driven by cron-driver-server on a + // single tick (so it doesn't auto-poll in every process the agent is built + // in); the channel provider is event-driven via the webhook handlers. + signals: [zapierPollProvider, channelTriggerProvider], voice: process.env.OPENAI_API_KEY ? new OpenAIVoice() : undefined, scorers: { relevancy: { diff --git a/packages/agents/src/mastra/signals/channel-trigger-provider.ts b/packages/agents/src/mastra/signals/channel-trigger-provider.ts new file mode 100644 index 00000000..391fca4b --- /dev/null +++ b/packages/agents/src/mastra/signals/channel-trigger-provider.ts @@ -0,0 +1,143 @@ +/** + * ChannelTriggerSignalProvider — a Mastra SignalProvider, hosted on the foreman + * agent, that fires saved workflows when an inbound chat message matches a + * `workflow_trigger` of type='channel'. + * + * Like the poll provider, it lives ON the agent (`signals: [...]`) so it's a + * first-class Mastra primitive with `connect()`/`notify()`. It has no + * `pollInterval` — it's event-driven: the channel webhook handlers call + * `handleMessage()` on every inbound message (via the thin + * `matchAndFireChannelTriggers` wrapper in workflows/channel-trigger.ts). + * + * Dedup (fixes foreman-tv5p): a retried webhook re-delivers the same message, + * which previously fired the workflow twice. We derive a delivery key — the + * caller-supplied `dedupeKey` (e.g. the platform message id) when available, + * else a content hash of channel|room|from|text — and skip a trigger whose + * persisted `last_dedupe_key` already equals it. The content-hash fallback + * catches retries (identical content) but would also coalesce a user sending + * the *exact same* text back-to-back; callers that pass a real message id avoid + * that edge entirely (tracked as a follow-up). + * + * On a fresh fire we also drop a notification signal (native `dedupeKey`) into + * the user's thread — additive and best-effort, never blocking the run. + */ + +import { createHash } from "node:crypto"; +import { SignalProvider } from "@mastra/core/signals"; +import { getSupabase } from "@/lib/db"; +import { executeWorkflow } from "@/lib/workflows/engine"; +import type { ChannelMessage } from "@/workflows/channel-trigger"; + +interface ChannelTriggerRow { + id: string; + workflow_id: string; + config: string; + last_dedupe_key: string | null; +} + +/** Strip leading `/` or `!` and trim — used for `match.command`. */ +function normalizeCommand(s: string): string { + return s.trim().replace(/^[/!]\s*/, ""); +} + +/** Stable delivery key for a message: the caller's id if given, else a content + * hash. Identical (retried) deliveries produce the same key. */ +function deliveryKey(msg: ChannelMessage): string { + if (msg.dedupeKey) return msg.dedupeKey; + return createHash("sha1") + .update(`${msg.channel}|${msg.room ?? ""}|${msg.from}|${msg.text}`) + .digest("hex"); +} + +export class ChannelTriggerSignalProvider extends SignalProvider<"channel-trigger"> { + readonly id = "channel-trigger" as const; + readonly name = "Channel Trigger Signals"; + // No pollInterval — event-driven via handleMessage(). + + /** Drop a best-effort notification into the user's thread on a fresh fire. */ + private async notifyOwner(msg: ChannelMessage, userId: string, key: string): Promise { + if (!this.agent) return; + try { + await this.notify( + { + source: "channel-trigger", + kind: msg.channel, + summary: `Workflow triggered by a ${msg.channel} message`, + payload: { from: msg.from, room: msg.room, text: msg.text }, + dedupeKey: key, + }, + { threadId: `channel:${msg.channel}:${msg.room ?? msg.from}`, resourceId: userId }, + ); + } catch (err) { + console.warn(`[channel-trigger] notify failed (${msg.channel}):`, err); + } + } + + /** + * Match an inbound message against channel triggers and fire each match's + * workflow exactly once per delivery. Returns the number of workflows fired. + */ + async handleMessage(msg: ChannelMessage): Promise { + const supabase = getSupabase(); + const { data: rows, error } = await supabase + .from("workflow_trigger") + .select("id, workflow_id, config, last_dedupe_key") + .eq("type", "channel") + .eq("enabled", true); + if (error) { + console.error(`[channel-trigger] fetch failed: ${error.message}`); + return 0; + } + + const key = deliveryKey(msg); + let fired = 0; + for (const row of (rows ?? []) as unknown as ChannelTriggerRow[]) { + let cfg: { channel?: string; match?: { command?: string; from?: string; room?: string } }; + try { + cfg = JSON.parse(row.config); + } catch { + continue; + } + if (cfg.channel !== msg.channel) continue; + const m = cfg.match ?? {}; + if (m.command && normalizeCommand(msg.text) !== normalizeCommand(m.command)) continue; + if (m.from && !new RegExp(m.from).test(msg.from)) continue; + if (m.room && msg.room !== m.room) continue; + + // Dedup: same delivery already fired this trigger → skip (retried webhook). + if (row.last_dedupe_key && row.last_dedupe_key === key) continue; + + const { data: wf } = await supabase + .from("workflow") + .select("user_id") + .eq("id", row.workflow_id) + .maybeSingle(); + if (!wf) continue; + + const userId = wf.user_id as string; + const now = new Date().toISOString(); + // Record the delivery key BEFORE firing so a concurrent retry can't race + // a second run in. last_fired_at doubles as the fire timestamp. + await supabase + .from("workflow_trigger") + .update({ last_dedupe_key: key, last_fired_at: now, updated_at: now }) + .eq("id", row.id); + + try { + for await (const ev of executeWorkflow(row.workflow_id, userId, {})) { + if (ev.type === "error") { + console.warn(`[channel-trigger] ${row.id} workflow error: ${ev.message}`); + } + } + fired++; + await this.notifyOwner(msg, userId, key); + } catch (err) { + console.error(`[channel-trigger] trigger ${row.id} failed:`, err); + } + } + return fired; + } +} + +/** Singleton hosted on the foreman agent (`signals: [...]`). */ +export const channelTriggerProvider = new ChannelTriggerSignalProvider(); diff --git a/packages/agents/src/workflows/channel-trigger.ts b/packages/agents/src/workflows/channel-trigger.ts index f8043830..65c89022 100644 --- a/packages/agents/src/workflows/channel-trigger.ts +++ b/packages/agents/src/workflows/channel-trigger.ts @@ -1,9 +1,9 @@ /** - * Channel-trigger matcher. Channel webhook handlers (slack/discord/telegram/etc.) - * call `matchAndFireChannelTriggers` on every inbound message. If any - * `workflow_trigger` row of type='channel' matches the channel + criteria, - * the workflow is fired (in the background — the handler still replies - * normally). + * Channel-trigger entrypoint. Channel webhook handlers (slack/discord/telegram/ + * etc.) call `matchAndFireChannelTriggers` on every inbound message. The match + + * fire + dedup logic lives in the ChannelTriggerSignalProvider (a Mastra + * SignalProvider hosted on the foreman agent); this wrapper keeps the existing + * call-site signature stable and delegates to it. * * Match semantics: * - `channel` field MUST equal the inbound channel name. @@ -13,12 +13,11 @@ * - `match.from` (optional): regex on the sender id/handle. * - `match.room` (optional): exact match on room/channel/DM target. * - * All provided criteria must pass — empty match = matches every message - * on that channel (rarely what you want, but supported). + * All provided criteria must pass — empty match = matches every message on that + * channel (rarely what you want, but supported). */ -import { getSupabase } from "@/lib/db"; -import { executeWorkflow } from "@/lib/workflows/engine"; +import { channelTriggerProvider } from "@/mastra/signals/channel-trigger-provider"; export interface ChannelMessage { channel: "slack" | "discord" | "telegram" | "linear" | "github" | "gchat" | "teams" | "whatsapp"; @@ -27,72 +26,15 @@ export interface ChannelMessage { from: string; /** Channel/room/DM target — used by `match.room`. */ room?: string; -} - -interface ChannelTriggerRow { - id: string; - workflow_id: string; - config: string; -} - -/** Strip leading `/` or `!` and trim — used for `match.command`. */ -function normalizeCommand(s: string): string { - return s.trim().replace(/^[/!]\s*/, ""); + /** + * Stable per-delivery key for idempotency (e.g. the platform message id). + * When omitted, a content hash is used. A retried webhook carries the same + * key, so the workflow fires once. Callers should pass a real message id when + * available to avoid coalescing legitimately-repeated identical messages. + */ + dedupeKey?: string; } export async function matchAndFireChannelTriggers(msg: ChannelMessage): Promise { - const supabase = getSupabase(); - const { data: rows, error } = await supabase - .from("workflow_trigger") - .select("id, workflow_id, config") - .eq("type", "channel") - .eq("enabled", true); - if (error) { - console.error(`[channel-trigger] fetch failed: ${error.message}`); - return 0; - } - - let fired = 0; - for (const row of (rows ?? []) as unknown as ChannelTriggerRow[]) { - let cfg: { channel?: string; match?: { command?: string; from?: string; room?: string } }; - try { - cfg = JSON.parse(row.config); - } catch { - continue; - } - if (cfg.channel !== msg.channel) continue; - const m = cfg.match ?? {}; - if (m.command && normalizeCommand(msg.text) !== normalizeCommand(m.command)) continue; - if (m.from && !new RegExp(m.from).test(msg.from)) continue; - if (m.room && msg.room !== m.room) continue; - - const { data: wf } = await supabase - .from("workflow") - .select("user_id") - .eq("id", row.workflow_id) - .maybeSingle(); - if (!wf) continue; - - void runOne(row.workflow_id, wf.user_id as string, row.id); - fired++; - } - return fired; -} - -async function runOne(workflowId: string, userId: string, triggerId: string) { - const supabase = getSupabase(); - const now = new Date().toISOString(); - try { - await supabase - .from("workflow_trigger") - .update({ last_fired_at: now, updated_at: now }) - .eq("id", triggerId); - for await (const ev of executeWorkflow(workflowId, userId, {})) { - if (ev.type === "error") { - console.warn(`[channel-trigger] ${triggerId} workflow error: ${ev.message}`); - } - } - } catch (err) { - console.error(`[channel-trigger] trigger ${triggerId} failed:`, err); - } + return channelTriggerProvider.handleMessage(msg); } diff --git a/packages/agents/tests/unit/channel-trigger.test.ts b/packages/agents/tests/unit/channel-trigger.test.ts index ec92ad3e..ab2c7646 100644 --- a/packages/agents/tests/unit/channel-trigger.test.ts +++ b/packages/agents/tests/unit/channel-trigger.test.ts @@ -1,11 +1,13 @@ /** - * Unit tests for matchAndFireChannelTriggers. + * Unit tests for matchAndFireChannelTriggers / ChannelTriggerSignalProvider — + * matching, dedup (foreman-tv5p), and the notify() wiring. */ import { beforeEach, describe, expect, it, vi } from "vitest"; type Result = { data: T; error: { message: string } | null }; let triggerListResult: Result = { data: [], error: null }; let workflowSelectResult: Result = { data: null, error: null }; +const updates: { table: string; payload: any }[] = []; function createChain(table: string) { const builder: any = {}; @@ -22,6 +24,7 @@ function createChain(table: string) { const origUpdate = builder.update; builder.update = vi.fn((payload: any) => { builder._verb = "update"; + updates.push({ table, payload }); return origUpdate(payload); }); return builder; @@ -40,28 +43,28 @@ async function* gen(events: unknown[]): AsyncGenerator { for (const e of events) yield e; } +const channelRow = (over: Record = {}) => ({ + id: "t-1", + workflow_id: "wf-1", + config: JSON.stringify({ channel: "slack", match: { command: "!standup" } }), + last_dedupe_key: null, + ...over, +}); + +const lastTriggerUpdate = () => + [...updates].reverse().find((u) => u.table === "workflow_trigger")?.payload; + describe("matchAndFireChannelTriggers", () => { beforeEach(() => { vi.resetAllMocks(); triggerListResult = { data: [], error: null }; workflowSelectResult = { data: { user_id: "user-1" }, error: null }; + updates.length = 0; mockExecuteWorkflow.mockReturnValue(gen([{ type: "complete" }])); }); it("fires when channel + command match", async () => { - triggerListResult = { - data: [ - { - id: "t-1", - workflow_id: "wf-1", - config: JSON.stringify({ - channel: "slack", - match: { command: "!standup" }, - }), - }, - ], - error: null, - }; + triggerListResult = { data: [channelRow()], error: null }; const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); const fired = await matchAndFireChannelTriggers({ @@ -73,19 +76,7 @@ describe("matchAndFireChannelTriggers", () => { }); it("tolerates leading slashes and whitespace in command match", async () => { - triggerListResult = { - data: [ - { - id: "t-1", - workflow_id: "wf-1", - config: JSON.stringify({ - channel: "slack", - match: { command: "!standup" }, - }), - }, - ], - error: null, - }; + triggerListResult = { data: [channelRow()], error: null }; const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); const fired = await matchAndFireChannelTriggers({ @@ -97,19 +88,7 @@ describe("matchAndFireChannelTriggers", () => { }); it("skips when channel differs", async () => { - triggerListResult = { - data: [ - { - id: "t-1", - workflow_id: "wf-1", - config: JSON.stringify({ - channel: "slack", - match: { command: "!standup" }, - }), - }, - ], - error: null, - }; + triggerListResult = { data: [channelRow()], error: null }; const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); const fired = await matchAndFireChannelTriggers({ @@ -123,14 +102,12 @@ describe("matchAndFireChannelTriggers", () => { it("skips when from regex doesn't match", async () => { triggerListResult = { data: [ - { - id: "t-1", - workflow_id: "wf-1", + channelRow({ config: JSON.stringify({ channel: "slack", match: { command: "!standup", from: "^U999" }, }), - }, + }), ], error: null, }; @@ -147,14 +124,12 @@ describe("matchAndFireChannelTriggers", () => { it("skips when room differs", async () => { triggerListResult = { data: [ - { - id: "t-1", - workflow_id: "wf-1", + channelRow({ config: JSON.stringify({ channel: "slack", match: { command: "!standup", room: "#ops" }, }), - }, + }), ], error: null, }; @@ -170,19 +145,7 @@ describe("matchAndFireChannelTriggers", () => { }); it("skips when workflow has been deleted", async () => { - triggerListResult = { - data: [ - { - id: "t-1", - workflow_id: "wf-deleted", - config: JSON.stringify({ - channel: "slack", - match: { command: "!standup" }, - }), - }, - ], - error: null, - }; + triggerListResult = { data: [channelRow({ workflow_id: "wf-deleted" })], error: null }; workflowSelectResult = { data: null, error: null }; const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); @@ -193,4 +156,73 @@ describe("matchAndFireChannelTriggers", () => { }); expect(fired).toBe(0); }); + + // ─── Dedup (foreman-tv5p) ─────────────────────────────────────────────────── + + it("skips a retried delivery whose dedupeKey already fired the trigger", async () => { + triggerListResult = { data: [channelRow({ last_dedupe_key: "msg-42" })], error: null }; + + const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); + const fired = await matchAndFireChannelTriggers({ + channel: "slack", + text: "!standup", + from: "U123", + dedupeKey: "msg-42", + }); + expect(fired).toBe(0); + expect(mockExecuteWorkflow).not.toHaveBeenCalled(); + }); + + it("fires a new delivery and records its dedupeKey", async () => { + triggerListResult = { data: [channelRow({ last_dedupe_key: "msg-old" })], error: null }; + + const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); + const fired = await matchAndFireChannelTriggers({ + channel: "slack", + text: "!standup", + from: "U123", + dedupeKey: "msg-new", + }); + expect(fired).toBe(1); + expect(lastTriggerUpdate()?.last_dedupe_key).toBe("msg-new"); + }); + + it("derives a content-hash dedupeKey when none is supplied (records it)", async () => { + triggerListResult = { data: [channelRow()], error: null }; + + const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); + const fired = await matchAndFireChannelTriggers({ + channel: "slack", + text: "!standup", + from: "U123", + }); + expect(fired).toBe(1); + // a real (40-char sha1 hex) key was persisted, not undefined + expect(lastTriggerUpdate()?.last_dedupe_key).toMatch(/^[0-9a-f]{40}$/); + }); + + it("notify()s the owner's thread with the native dedupeKey on a fresh fire", async () => { + triggerListResult = { data: [channelRow()], error: null }; + + const { ChannelTriggerSignalProvider } = await import( + "@/mastra/signals/channel-trigger-provider" + ); + const provider = new ChannelTriggerSignalProvider(); + const sendNotificationSignal = vi.fn().mockResolvedValue(undefined); + provider.connect({ sendNotificationSignal } as any); + + const fired = await provider.handleMessage({ + channel: "slack", + text: "!standup", + from: "U123", + room: "#ops", + dedupeKey: "msg-7", + }); + + expect(fired).toBe(1); + expect(sendNotificationSignal).toHaveBeenCalledTimes(1); + const [notification, target] = sendNotificationSignal.mock.calls[0]; + expect(notification).toMatchObject({ source: "channel-trigger", dedupeKey: "msg-7" }); + expect(target).toMatchObject({ threadId: "channel:slack:#ops", resourceId: "user-1" }); + }); }); diff --git a/packages/agents/tests/unit/mastra-agent.test.ts b/packages/agents/tests/unit/mastra-agent.test.ts index 4bb128c4..dfcb4b08 100644 --- a/packages/agents/tests/unit/mastra-agent.test.ts +++ b/packages/agents/tests/unit/mastra-agent.test.ts @@ -44,15 +44,18 @@ describe("Mastra foreman agent", () => { expect(agent.model).toEqual(AGENT_MODELS.foreman); }, 30000); - it("hosts the zapier poll SignalProvider and connects it on construction", async () => { + it("hosts the trigger SignalProviders and connects them on construction", async () => { const { createForemanAgent } = await import("@/mastra/agents/foreman"); const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const { channelTriggerProvider } = await import("@/mastra/signals/channel-trigger-provider"); await createForemanAgent("file:./test-agent.db"); // The Agent constructor calls provider.connect(this) for each signals entry, - // proving the primitive is live in the agent (not merely defined). + // proving the primitives are live in the agent (not merely defined). expect(zapierPollProvider.isConnected).toBe(true); - // No pollInterval — we drive runDuePolls() from cron-driver-server, so the - // framework must NOT auto-poll it in every process the agent is built in. + expect(channelTriggerProvider.isConnected).toBe(true); + // Neither sets a pollInterval — the poll provider is driven by + // cron-driver-server, the channel provider is event-driven via webhooks. expect(zapierPollProvider.pollInterval).toBeUndefined(); + expect(channelTriggerProvider.pollInterval).toBeUndefined(); }, 30000); }); From f825d4d7795b0519d496aa4af50905e514de0d7b Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 20:28:33 -0700 Subject: [PATCH 06/19] fix(cron): atomic same-minute claim prevents double-fire on restart (foreman-tv5p) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old guard checked last_fired_at in JS and set it inside the fire-and-forget runOne, leaving a race: two overlapping ticks (a process restart mid-minute, or briefly co-existing drivers) could both pass the guard before either persisted last_fired_at, firing the workflow twice. tickCron now claims the minute with a conditional UPDATE — set last_fired_at= tickIso WHERE id=trigger AND (last_fired_at IS NULL OR last_fired_at < tickIso), returning the claimed id. The DB UPDATE is the lock: only one tick claims a given minute, so the workflow fires exactly once. runOne no longer writes last_fired_at (the claim already did). Test: a lost claim (UPDATE matches no row) => 0 fires, executeWorkflow never called. Full cron suite green. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/agents/src/workflows/cron-driver.ts | 37 +++++++++++-------- .../agents/tests/unit/cron-driver.test.ts | 17 +++++++-- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/packages/agents/src/workflows/cron-driver.ts b/packages/agents/src/workflows/cron-driver.ts index 1c14a0d9..7ae97fcb 100644 --- a/packages/agents/src/workflows/cron-driver.ts +++ b/packages/agents/src/workflows/cron-driver.ts @@ -160,12 +160,6 @@ export async function tickCron(now: Date = new Date()): Promise<{ fired: number } if (!matches) continue; - // Already fired this minute? Skip. - if (row.last_fired_at) { - const last = minuteFloor(new Date(row.last_fired_at)); - if (last.getTime() >= tickMinute.getTime()) continue; - } - // Find the workflow's owner so executeWorkflow can run with the right userId. const { data: wf } = await supabase .from("workflow") @@ -177,23 +171,34 @@ export async function tickCron(now: Date = new Date()): Promise<{ fired: number continue; } + // Atomic same-minute claim: set last_fired_at=tickIso only if this trigger + // hasn't already fired this minute. The conditional UPDATE is the lock — + // two overlapping ticks (e.g. a process restart mid-minute, or briefly + // co-existing drivers) can't both claim the same minute, so the workflow + // fires exactly once even when last_fired_at hadn't yet been persisted. + const { data: claimed, error: claimErr } = await supabase + .from("workflow_trigger") + .update({ last_fired_at: tickIso, updated_at: tickIso }) + .eq("id", row.id) + .or(`last_fired_at.is.null,last_fired_at.lt.${tickIso}`) + .select("id"); + if (claimErr) { + console.warn(`[cron-driver] trigger ${row.id} claim failed: ${claimErr.message}`); + continue; + } + if (!claimed || claimed.length === 0) continue; // another tick already fired it + // Fire and forget — we don't await full completion to keep the tick fast. - void runOne(row.workflow_id, wf.user_id as string, row.id, tickIso); + void runOne(row.workflow_id, wf.user_id as string, row.id); fired++; } return { fired }; } -async function runOne(workflowId: string, userId: string, triggerId: string, firedAtIso: string) { - const supabase = getSupabase(); +async function runOne(workflowId: string, userId: string, triggerId: string) { + // last_fired_at was already set by the atomic claim in tickCron, so the run is + // dedup'd before we get here. We just execute and surface errors. try { - // Mark fired BEFORE executing so a long-running workflow can't race a - // second tick. last_fired_at is a fingerprint, not a completion marker. - await supabase - .from("workflow_trigger") - .update({ last_fired_at: firedAtIso, updated_at: firedAtIso }) - .eq("id", triggerId); - for await (const ev of executeWorkflow(workflowId, userId, {})) { if (ev.type === "error") { console.warn(`[cron-driver] ${triggerId} workflow error: ${ev.message}`); diff --git a/packages/agents/tests/unit/cron-driver.test.ts b/packages/agents/tests/unit/cron-driver.test.ts index 6b223d14..e07f3d20 100644 --- a/packages/agents/tests/unit/cron-driver.test.ts +++ b/packages/agents/tests/unit/cron-driver.test.ts @@ -8,17 +8,21 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; type Result = { data: T; error: { message: string } | null }; let triggerListResult: Result = { data: [], error: null }; let workflowSelectResult: Result = { data: null, error: null }; +// Result of the atomic same-minute claim (`update ... .select("id")`). A +// non-empty array means the claim succeeded; empty means another tick already +// fired this minute. +let claimResult: Result = { data: [{ id: "trig-1" }], error: null }; const updates: any[] = []; function createChain(table: string) { const builder: any = {}; - for (const m of ["select", "eq", "limit", "update"]) { + for (const m of ["select", "eq", "limit", "update", "or"]) { builder[m] = vi.fn().mockReturnValue(builder); } builder.maybeSingle = vi.fn().mockImplementation(() => Promise.resolve(workflowSelectResult)); // biome-ignore lint/suspicious/noThenProperty: thenable mock builder.then = (resolve: any) => { - if (builder._verb === "update") return resolve({ data: null, error: null }); + if (builder._verb === "update") return resolve(claimResult); if (table === "workflow_trigger") return resolve(triggerListResult); return resolve({ data: null, error: null }); }; @@ -111,6 +115,7 @@ describe("tickCron", () => { vi.resetAllMocks(); triggerListResult = { data: [], error: null }; workflowSelectResult = { data: null, error: null }; + claimResult = { data: [{ id: "trig-1" }], error: null }; updates.length = 0; mockExecuteWorkflow.mockReset(); }); @@ -139,7 +144,7 @@ describe("tickCron", () => { expect(updates.find((u) => u.table === "workflow_trigger")).toBeTruthy(); }); - it("skips a trigger that already fired this minute", async () => { + it("does not double-fire when the atomic claim is lost (same-minute restart)", async () => { triggerListResult = { data: [ { @@ -151,10 +156,16 @@ describe("tickCron", () => { ], error: null, }; + workflowSelectResult = { data: { user_id: "user-1" }, error: null }; + // The conditional UPDATE matches no row — another tick already claimed this + // minute. The driver must NOT fire. + claimResult = { data: [], error: null }; + mockExecuteWorkflow.mockReturnValue(gen([{ type: "complete" }])); const { tickCron } = await import("@/workflows/cron-driver"); const out = await tickCron(new Date("2026-05-10T12:00:30Z")); expect(out.fired).toBe(0); + expect(mockExecuteWorkflow).not.toHaveBeenCalled(); }); it("skips a non-matching schedule", async () => { From 4c5da4c94be294fe71b357fe68af5f5022d3d0ef Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 20:40:03 -0700 Subject: [PATCH 07/19] fix(workflows): never leave a run stuck in 'running' (foreman-2afc, part 1) executeWorkflow inserted workflow_run status='running' and only wrote a terminal status inside the per-step catch and the success path. If the caller abandoned the generator mid-stream (a for-await break calls generator.return()) or an unexpected error propagated out of a step, the row stayed 'running' forever. Wrap the run body in try/finally with a flag that flips at each terminal write; the finally marks the run 'failed' on any unsettled exit. The status yield moved inside the try so an abandon at any point after the row exists is covered. Tests: step-throw => one 'failed' write; full success => one 'success' (no spurious 'failed' from the finally); caller-abandons-generator => 'failed'. Part 2 (persist error_message + fired_by + trigger_id and surface trigger-fired runs in GET /workflows/:id/runs) needs a workflow_run migration + type regen and is gated separately. Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 3 +- packages/agents/src/lib/workflows/engine.ts | 135 ++++++++++-------- .../agents/tests/unit/workflow-engine.test.ts | 100 +++++++++++++ 3 files changed, 181 insertions(+), 57 deletions(-) create mode 100644 packages/agents/tests/unit/workflow-engine.test.ts diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index e029ab25..19242a54 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -103,7 +103,7 @@ {"_type":"issue","id":"foreman-ezsm","title":"[workflow] Build the /workflows UI page (list saved automations + run history)","description":"foreman-v8k1 calls for a UI so users can see saved automations; it does not exist. Build a /workflows page in packages/web listing workflows + triggers + recent runs (status, error_message from the observability issue), using the existing /workflows/* API routes and the shadcn stack. Per-workflow run-history view.","acceptance_criteria":"/workflows lists the user's workflows + triggers; clicking shows run history incl. failed-run errors; reads from existing API routes.","status":"open","priority":2,"issue_type":"feature","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:13Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:13Z","dependencies":[{"issue_id":"foreman-ezsm","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:13Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-8fcs","title":"[workflow] Tests: params.ts substitution + engine step-failure paths","description":"params.ts (extract/substitute/validate) and engine.ts step-failure handling (~80-122) are untested. Add unit tests: substituteParams (partial replace, unknown keys left as-is, nested/arrays), validateParams (missing detected), engine step failure (runAction throws -\u003e failed + error captured), generator-throw cleanup, empty-steps early-exit.","acceptance_criteria":"params + engine error paths covered; npm test green.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:09Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:09Z","dependencies":[{"issue_id":"foreman-8fcs","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:08Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-2afc","title":"[workflow] Run-state \u0026 observability: orphaned 'running' rows on crash; persist error_message + trigger context","description":"engine.ts inserts workflow_run status=running (~line 56) and never sets failed if executeWorkflow throws before completing -\u003e rows orphaned forever. Wrap generator consumption in try/finally at each call site (routes/workflows.ts, channel-trigger.ts, cron-driver.ts) to mark failed with the error. Persist error_message + fired_by(manual/cron/channel) + trigger_id on workflow_run; trigger-fired runs only console.warn today and never appear in run history. Surface in GET /workflows/:id/runs.","acceptance_criteria":"A failed run shows status=failed + error_message within ~1s; cron/channel-fired runs appear in run history with trigger context; no rows stuck in running.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:04Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:04Z","dependencies":[{"issue_id":"foreman-2afc","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:03Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-tv5p","title":"[workflow] BUG: trigger dedup/idempotency — channel-trigger has no dedup; cron can double-fire on same-minute restart","description":"channel-trigger.ts (matchAndFireChannelTriggers ~43-79) fires with NO dedup despite an unused workflow_trigger.last_dedupe_key column — a retried webhook = duplicate runs. Add a dedupe key (channel+sender+ts+text hash or delivery id) checked against last_dedupe_key before firing. Separately cron-driver.ts (~164-167) can double-fire if the process restarts mid-minute; add a guard/lock. runOne also swallows errors silently.","acceptance_criteria":"Duplicate webhook =\u003e 1 run not 2 (unit test); a new message =\u003e new run; cron fires once per minute even across a same-minute restart (unit test).","status":"in_progress","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:59Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:15:08Z","started_at":"2026-06-12T03:15:08Z","dependencies":[{"issue_id":"foreman-tv5p","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:30:58Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-tv5p","title":"[workflow] BUG: trigger dedup/idempotency — channel-trigger has no dedup; cron can double-fire on same-minute restart","description":"channel-trigger.ts (matchAndFireChannelTriggers ~43-79) fires with NO dedup despite an unused workflow_trigger.last_dedupe_key column — a retried webhook = duplicate runs. Add a dedupe key (channel+sender+ts+text hash or delivery id) checked against last_dedupe_key before firing. Separately cron-driver.ts (~164-167) can double-fire if the process restarts mid-minute; add a guard/lock. runOne also swallows errors silently.","acceptance_criteria":"Duplicate webhook =\u003e 1 run not 2 (unit test); a new message =\u003e new run; cron fires once per minute even across a same-minute restart (unit test).","status":"closed","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:59Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:29:21Z","started_at":"2026-06-12T03:15:08Z","closed_at":"2026-06-12T03:29:21Z","close_reason":"Channel dedup + cron same-minute atomic claim implemented and unit-tested per acceptance criteria. Error-surfacing portion tracked under 2afc.","dependencies":[{"issue_id":"foreman-tv5p","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:30:58Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-wzmx","title":"[aimock] Resolve declared-but-unfixtured MCP/A2A mocks (write fixtures or remove declarations)","description":"aimock.json declares 8 MCP tools + 5 A2A agents but tests/fixtures/aimock/ has no mcp/ or a2a/ fixtures — a declared-vs-real mismatch that 503s under strict mode. Either write minimal fixtures for the tools/agents tests actually exercise, or remove the unused declarations so config reflects reality.","acceptance_criteria":"Every MCP/A2A declaration has a fixture OR is removed; npm test passes under strict mode with no unmatched MCP/A2A requests.","status":"closed","priority":2,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:27Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:42:33Z","started_at":"2026-06-11T21:37:23Z","closed_at":"2026-06-11T21:42:33Z","close_reason":"MCP/A2A declarations made real; orphaned mcp/tools.json folded in and removed","dependencies":[{"issue_id":"foreman-wzmx","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:27Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-wrrd","title":"[aimock] Enable strict mode and tighten/remove the empty-match catch-all fixture","description":"@copilotkit/aimock supports strict:true (verified config-loader.d.ts:101) so unmatched requests 503 instead of silently matching z-catchall.json (match {}), which currently risks false-green tests. Enable strict in packages/agents/aimock.json and remove/convert z-catchall to a strict error, fixing any tests that relied on the fallback.","acceptance_criteria":"aimock.json has strict:true; z-catchall removed/converted; npm test passes with every test hitting an explicit fixture.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:22Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:43:26Z","closed_at":"2026-06-11T21:43:26Z","close_reason":"strict:true enabled in aimock.json; z-catchall.json deleted; npm test 278 passed/10 skipped under strict; strict 503 verified via probe","dependencies":[{"issue_id":"foreman-wrrd","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:21Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-xnis","title":"[bump] Surface getProfile as an agent tool; confirm trash option auto-surfaces on table-list tools","description":"Post-bump, surface worthwhile new stable methods. Add getProfile to READ_ONLY in zapier-sdk-tools.ts (agent can answer who-am-I-on-Zapier). Confirm the 0.57 trash option (exclude/include/only) auto-surfaces in listTableRecords/listTableFields tool schemas via the registry (likely no code change). Keep client-credentials mgmt + listTriggers excluded (documented in capability map).","acceptance_criteria":"getProfile in READ_ONLY and callable; trash param present in table-list tool input schema; tests pass.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:17Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:50:31Z","closed_at":"2026-06-11T21:50:31Z","close_reason":"getProfile already surfaced as a read-only tool (in READ_ONLY set, generated via sdk.getRegistry; added 7wks ago in 2092317) — agent can answer who-am-I-on-Zapier, no change needed. 'Trash option': no 'trash' field exists anywhere in @zapier/zapier-sdk@0.69.3; tool params auto-surface from SDK Zod schemas (mechanism verified) but there is nothing to surface — note is stale.","dependencies":[{"issue_id":"foreman-xnis","depends_on_id":"foreman-8ujc","type":"discovered-from","created_at":"2026-06-11T01:30:17Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -210,6 +210,7 @@ {"_type":"issue","id":"foreman-c9y","title":"Set up LibSQL + Drizzle ORM","description":"Single LibSQL database for app + Mastra data. Migrations runnable via script. AC: npm run db:migrate creates BetterAuth tables. Connection string in .env.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:48Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-s46","title":"Install and configure BetterAuth","description":"Email/password auth. Session stored in LibSQL. AC: /sign-up and /sign-in pages work. Authenticated session persists across reloads.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:46Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-5ld","title":"Initialize Next.js 15 project","description":"Scaffold Next.js 15 with App Router, TypeScript, Tailwind. AC: npm run dev serves a blank page at /. Project committed to git under C:\\Users\\HamCh\\code\\foreman.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:38Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-m9f6","title":"[channels] Thread real platform message ids into ChannelMessage.dedupeKey","description":"ChannelTriggerSignalProvider dedup falls back to a content hash (channel|room|from|text) when ChannelMessage.dedupeKey is absent, which coalesces a user sending the exact same text back-to-back. Pass each adapter's stable message id as dedupeKey from the ~10 channel-bot call sites (slack/discord/telegram/teams/whatsapp/gchat/github/linear onDirectMessage + onNewMention) where the Chat SDK message exposes one. Removes the false-positive edge; true per-delivery idempotency.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T03:31:07Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:31:07Z","dependencies":[{"issue_id":"foreman-m9f6","depends_on_id":"foreman-tv5p","type":"discovered-from","created_at":"2026-06-11T20:31:07Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-l8ev","title":"Document Zapier SDK auth model in README (client-creds vs userJwt per surface)","description":"Discovered during SDK exploration: the Zapier SDK surfaces need DIFFERENT auth. Trigger inboxes + discovery + runAction accept app-level client-credentials (ZAPIER_CLIENT_ID/SECRET). Durable-workflow endpoints (runDurable/publishWorkflowVersion) require a per-user userJwt (PKCE OAuth token), NOT client-creds. The new zapier-sdk-cli (0.54) login yields client-creds only; ~/.zapierrc deployKey is a legacy 32-char non-JWT (invalid as SDK creds). Document in README: which surfaces need which auth, how Foreman obtains each (env client-creds vs lib/zapier/sdk.ts PKCE per-user token), and the open question for Zapier (server-side path to per-user durable). Come back to this later. See docs/durable-workflow-spike.md + docs/trigger-inbox-spike.md.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T18:39:52Z","created_by":"HamChowderr","updated_at":"2026-06-11T18:39:52Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-z0kn","title":"Port provider-routing generate() test to a PG-enabled tier (storage injection)","description":"provider-swap.test.ts 'returns text' sub-test calls agent.generate() which now needs PostgresStore-backed Memory (foreman migrated LibSQL-\u003e@mastra/pg). It can't run in the no-external-services mocked tier (agents CI job has no PG), so it's it.skip'd. Proper fix: either inject a test storage into createForemanAgent (let tests use an in-memory/file store), or run this sub-test in a PG-enabled tier (test:live or a CI job with a postgres service). Discovered during the catch-up integration of claude/add-multiple-providers.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T16:40:19Z","created_by":"HamChowderr","updated_at":"2026-06-11T16:40:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-elyi","title":"[bump] Surface ZapierApprovalError approval URL to the user (handleSdkError)","description":"Under SDK \u003e=0.50 with approvalMode unset, a server (non-TTY) defaults to 'throw': sdk.runAction throws ZapierApprovalError carrying an approval URL on approval-gated responses. Foreman's handleSdkError (zapier-sdk-tools.ts) currently catches it only via the generic ZapierError catch-all, surfacing .message/.code but DROPPING the approval URL. Add an explicit 'if (err instanceof ZapierApprovalError)' branch that returns the approval URL + approvalStatus so the agent can tell the user how to approve. Non-blocking for the bump (degrades gracefully today). Discovered during foreman-2xdk verification.","status":"closed","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T15:10:57Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:39:56Z","closed_at":"2026-06-11T22:39:56Z","close_reason":"Done: handleSdkError now surfaces ZapierApprovalError.approvalUrl (code APPROVAL_REQUIRED), placed before the generic ZapierError catch-all; handleSdkError exported + 2 unit tests (with/without URL) pass; lint clean. Verified approvalUrl field exists in SDK 0.69.3.","dependencies":[{"issue_id":"foreman-elyi","depends_on_id":"foreman-2xdk","type":"discovered-from","created_at":"2026-06-11T08:10:57Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/src/lib/workflows/engine.ts b/packages/agents/src/lib/workflows/engine.ts index 3c6f9d48..1b60d180 100644 --- a/packages/agents/src/lib/workflows/engine.ts +++ b/packages/agents/src/lib/workflows/engine.ts @@ -61,72 +61,95 @@ export async function* executeWorkflow( created_at: now, }); - yield { type: "status", runId, status: "running" }; - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const template = JSON.parse(step.proposal_template) as Record; - const label = (template.humanLabel as string) ?? `Step ${i + 1}`; - - yield { - type: "step", - runId, - stepIndex: i, - stepId: step.id, - status: "running", - label, - }; - - try { - const rawInputs = (template.inputs ?? {}) as Record; - const resolved = substituteParams(rawInputs, inputs); - - const result = await runAction( - userId, - template.appKey as string, - template.actionType as string, - template.actionKey as string, - resolved, - template.connectionId as string | undefined, - ); - - yield { - type: "step", - runId, - stepIndex: i, - stepId: step.id, - status: "complete", - label, - result, - }; - } catch (err) { - const errorMsg = err instanceof Error ? err.message : String(err); + // Own the run's terminal state in a try/finally so the row can never get + // stuck in 'running'. `settled` flips at each terminal write (step failure or + // success). The finally catches the cases the old code missed: the caller + // abandons the generator mid-stream (for-await break → generator .return()), + // or an unexpected error propagates out of a step. The status yield lives + // inside the try so an abandon at any point after the row exists is covered. + // (foreman-2afc) + let settled = false; + try { + yield { type: "status", runId, status: "running" }; + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const template = JSON.parse(step.proposal_template) as Record; + const label = (template.humanLabel as string) ?? `Step ${i + 1}`; yield { type: "step", runId, stepIndex: i, stepId: step.id, - status: "failed", + status: "running", label, - error: errorMsg, }; - await supabase - .from("workflow_run") - .update({ status: "failed", completed_at: new Date().toISOString() }) - .eq("id", runId); + try { + const rawInputs = (template.inputs ?? {}) as Record; + const resolved = substituteParams(rawInputs, inputs); + + const result = await runAction( + userId, + template.appKey as string, + template.actionType as string, + template.actionKey as string, + resolved, + template.connectionId as string | undefined, + ); + + yield { + type: "step", + runId, + stepIndex: i, + stepId: step.id, + status: "complete", + label, + result, + }; + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + + yield { + type: "step", + runId, + stepIndex: i, + stepId: step.id, + status: "failed", + label, + error: errorMsg, + }; + + await supabase + .from("workflow_run") + .update({ status: "failed", completed_at: new Date().toISOString() }) + .eq("id", runId); + settled = true; + + yield { type: "error", runId, message: errorMsg }; + return; + } + } - yield { type: "error", runId, message: errorMsg }; - return; + // All steps succeeded + await supabase + .from("workflow_run") + .update({ status: "success", completed_at: new Date().toISOString() }) + .eq("id", runId); + settled = true; + + yield { type: "complete", runId, status: "success" }; + } finally { + if (!settled) { + try { + await supabase + .from("workflow_run") + .update({ status: "failed", completed_at: new Date().toISOString() }) + .eq("id", runId); + } catch (e) { + console.error(`[engine] failed to mark abandoned run ${runId} failed:`, e); + } } } - - // All steps succeeded - await supabase - .from("workflow_run") - .update({ status: "success", completed_at: new Date().toISOString() }) - .eq("id", runId); - - yield { type: "complete", runId, status: "success" }; } diff --git a/packages/agents/tests/unit/workflow-engine.test.ts b/packages/agents/tests/unit/workflow-engine.test.ts new file mode 100644 index 00000000..7393f339 --- /dev/null +++ b/packages/agents/tests/unit/workflow-engine.test.ts @@ -0,0 +1,100 @@ +/** + * Unit tests for executeWorkflow's run-state lifecycle (foreman-2afc): a run row + * must never get stuck in 'running' — not on step failure, and not when the + * caller abandons the generator mid-stream. runAction is mocked. + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +let stepsResult: { data: unknown[] | null; error: unknown } = { data: [], error: null }; +const updates: { table: string; payload: any }[] = []; + +function createChain(table: string) { + const b: any = {}; + for (const m of ["select", "eq", "order", "insert", "update"]) { + b[m] = vi.fn().mockReturnValue(b); + } + // biome-ignore lint/suspicious/noThenProperty: thenable mock + b.then = (resolve: any) => { + if (b._verb === "insert") return resolve({ error: null }); + if (b._verb === "update") return resolve({ error: null }); + if (table === "workflow_step") return resolve(stepsResult); + return resolve({ data: null, error: null }); + }; + const oi = b.insert; + b.insert = vi.fn((p: any) => { + b._verb = "insert"; + return oi(p); + }); + const ou = b.update; + b.update = vi.fn((p: any) => { + b._verb = "update"; + updates.push({ table, payload: p }); + return ou(p); + }); + return b; +} + +const mockSupabase = { from: vi.fn((t: string) => createChain(t)) }; +vi.mock("@/lib/db", () => ({ getSupabase: () => mockSupabase })); + +const mockRunAction = vi.fn(); +vi.mock("@/lib/zapier/execution", () => ({ + runAction: (...args: unknown[]) => mockRunAction(...args), +})); + +const oneStep = [ + { + id: "s-1", + proposal_template: JSON.stringify({ + humanLabel: "Send", + inputs: {}, + appKey: "gmail", + actionType: "write", + actionKey: "send_email", + }), + }, +]; + +const statuses = () => + updates.filter((u) => u.table === "workflow_run").map((u) => u.payload.status); + +describe("executeWorkflow run-state", () => { + beforeEach(() => { + vi.resetAllMocks(); + stepsResult = { data: oneStep, error: null }; + updates.length = 0; + }); + + it("marks the run failed when a step throws (no stuck 'running')", async () => { + mockRunAction.mockRejectedValue(new Error("boom")); + const { executeWorkflow } = await import("@/lib/workflows/engine"); + + const events: string[] = []; + for await (const ev of executeWorkflow("wf-1", "user-1", {})) events.push(ev.type); + + expect(statuses()).toEqual(["failed"]); // exactly one terminal write, not stuck + expect(events).toContain("error"); + }); + + it("marks the run success when all steps complete", async () => { + mockRunAction.mockResolvedValue({ ok: true }); + const { executeWorkflow } = await import("@/lib/workflows/engine"); + + for await (const ev of executeWorkflow("wf-1", "user-1", {})) void ev; + + // success terminal write, and the finally must NOT add a spurious 'failed' + expect(statuses()).toEqual(["success"]); + }); + + it("marks the run failed when the caller abandons the generator mid-stream", async () => { + // runAction would hang, but the caller breaks before it's reached. + mockRunAction.mockReturnValue(new Promise(() => {})); + const { executeWorkflow } = await import("@/lib/workflows/engine"); + + for await (const ev of executeWorkflow("wf-1", "user-1", {})) { + if (ev.type === "status") break; // abandon — triggers the generator's finally + } + + expect(statuses()).toEqual(["failed"]); // finally rescued the orphaned run + }); +}); From c039220cd362f139a4845a012334dc6cb09b07d7 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 21:24:44 -0700 Subject: [PATCH 08/19] feat(workflows): persist run failure + trigger context in run history (foreman-2afc, part 2) Adds a workflow_run migration (error_message, fired_by, trigger_id) + regenerated db types. executeWorkflow takes a WorkflowTriggerContext (firedBy manual/cron/ channel/poll + optional triggerId, default manual): it records fired_by/trigger_id on the run row at insert, and writes error_message on the step-failure and abandoned-run terminal updates. The three trigger drivers (cron, channel, poll) now pass their firedBy + trigger id; the manual run paths (route + run_workflow tool) keep the default. GET /workflows/:id/runs now returns error_message/fired_by/trigger_id, so trigger-fired runs appear in history with context and failures show the reason. Tests: run row carries fired_by/trigger_id (manual default + cron context); failed run persists error_message (step error and abandoned). Full agents suite green (300); db:types:check clean; migration applies on a clean db reset. Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 2 +- packages/agents/src/lib/db/database.types.ts | 9 +++++ packages/agents/src/lib/workflows/engine.ts | 26 ++++++++++++-- .../signals/channel-trigger-provider.ts | 5 ++- .../mastra/signals/zapier-poll-provider.ts | 11 +++++- packages/agents/src/routes/workflows.ts | 3 ++ packages/agents/src/workflows/cron-driver.ts | 5 ++- .../agents/tests/unit/workflow-engine.test.ts | 34 +++++++++++++++++-- ...60612000000_workflow_run_observability.sql | 17 ++++++++++ 9 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 supabase/migrations/20260612000000_workflow_run_observability.sql diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 19242a54..b1eaeae0 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -102,7 +102,7 @@ {"_type":"issue","id":"foreman-7jn1","title":"[workflow] Decision: per-user Slack isolation sufficient vs build shared group-channel context","description":"Foreman already isolates per Slack user (resourceId per user; slack/bot.ts:58; foreman.ts memory scope resource). Mastra natively supports scope thread|resource but NO shared-group memory, and slack/bot.ts:137 never persists the channel thread to the conversation table. Decide: (A) per-user is enough (lock it), or (B) build shared group context = new channel_conversation table + group memory scope + thread persistence. If B, spawn the feature issue.","acceptance_criteria":"Decision recorded; if B, a scoped feature issue (channel_conversation table + group scope + persist channel thread) is filed.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:18Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:18Z","dependencies":[{"issue_id":"foreman-7jn1","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:17Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ezsm","title":"[workflow] Build the /workflows UI page (list saved automations + run history)","description":"foreman-v8k1 calls for a UI so users can see saved automations; it does not exist. Build a /workflows page in packages/web listing workflows + triggers + recent runs (status, error_message from the observability issue), using the existing /workflows/* API routes and the shadcn stack. Per-workflow run-history view.","acceptance_criteria":"/workflows lists the user's workflows + triggers; clicking shows run history incl. failed-run errors; reads from existing API routes.","status":"open","priority":2,"issue_type":"feature","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:13Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:13Z","dependencies":[{"issue_id":"foreman-ezsm","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:13Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-8fcs","title":"[workflow] Tests: params.ts substitution + engine step-failure paths","description":"params.ts (extract/substitute/validate) and engine.ts step-failure handling (~80-122) are untested. Add unit tests: substituteParams (partial replace, unknown keys left as-is, nested/arrays), validateParams (missing detected), engine step failure (runAction throws -\u003e failed + error captured), generator-throw cleanup, empty-steps early-exit.","acceptance_criteria":"params + engine error paths covered; npm test green.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:09Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:09Z","dependencies":[{"issue_id":"foreman-8fcs","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:08Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-2afc","title":"[workflow] Run-state \u0026 observability: orphaned 'running' rows on crash; persist error_message + trigger context","description":"engine.ts inserts workflow_run status=running (~line 56) and never sets failed if executeWorkflow throws before completing -\u003e rows orphaned forever. Wrap generator consumption in try/finally at each call site (routes/workflows.ts, channel-trigger.ts, cron-driver.ts) to mark failed with the error. Persist error_message + fired_by(manual/cron/channel) + trigger_id on workflow_run; trigger-fired runs only console.warn today and never appear in run history. Surface in GET /workflows/:id/runs.","acceptance_criteria":"A failed run shows status=failed + error_message within ~1s; cron/channel-fired runs appear in run history with trigger context; no rows stuck in running.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:04Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:04Z","dependencies":[{"issue_id":"foreman-2afc","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:03Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-2afc","title":"[workflow] Run-state \u0026 observability: orphaned 'running' rows on crash; persist error_message + trigger context","description":"engine.ts inserts workflow_run status=running (~line 56) and never sets failed if executeWorkflow throws before completing -\u003e rows orphaned forever. Wrap generator consumption in try/finally at each call site (routes/workflows.ts, channel-trigger.ts, cron-driver.ts) to mark failed with the error. Persist error_message + fired_by(manual/cron/channel) + trigger_id on workflow_run; trigger-fired runs only console.warn today and never appear in run history. Surface in GET /workflows/:id/runs.","acceptance_criteria":"A failed run shows status=failed + error_message within ~1s; cron/channel-fired runs appear in run history with trigger context; no rows stuck in running.","notes":"Part 1 DONE (4c5da4c): engine try/finally + settled flag = no run stuck in running; 3 unit tests. Part 2 GATED on user go: workflow_run migration (error_message, fired_by, trigger_id), thread trigger context through executeWorkflow + 3 driver sites, surface in GET /workflows/:id/runs, regen db types (needs Foreman local supabase up; a Zillow stack currently holds default ports).","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:04Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:41:09Z","dependencies":[{"issue_id":"foreman-2afc","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:03Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-tv5p","title":"[workflow] BUG: trigger dedup/idempotency — channel-trigger has no dedup; cron can double-fire on same-minute restart","description":"channel-trigger.ts (matchAndFireChannelTriggers ~43-79) fires with NO dedup despite an unused workflow_trigger.last_dedupe_key column — a retried webhook = duplicate runs. Add a dedupe key (channel+sender+ts+text hash or delivery id) checked against last_dedupe_key before firing. Separately cron-driver.ts (~164-167) can double-fire if the process restarts mid-minute; add a guard/lock. runOne also swallows errors silently.","acceptance_criteria":"Duplicate webhook =\u003e 1 run not 2 (unit test); a new message =\u003e new run; cron fires once per minute even across a same-minute restart (unit test).","status":"closed","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:59Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:29:21Z","started_at":"2026-06-12T03:15:08Z","closed_at":"2026-06-12T03:29:21Z","close_reason":"Channel dedup + cron same-minute atomic claim implemented and unit-tested per acceptance criteria. Error-surfacing portion tracked under 2afc.","dependencies":[{"issue_id":"foreman-tv5p","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:30:58Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-wzmx","title":"[aimock] Resolve declared-but-unfixtured MCP/A2A mocks (write fixtures or remove declarations)","description":"aimock.json declares 8 MCP tools + 5 A2A agents but tests/fixtures/aimock/ has no mcp/ or a2a/ fixtures — a declared-vs-real mismatch that 503s under strict mode. Either write minimal fixtures for the tools/agents tests actually exercise, or remove the unused declarations so config reflects reality.","acceptance_criteria":"Every MCP/A2A declaration has a fixture OR is removed; npm test passes under strict mode with no unmatched MCP/A2A requests.","status":"closed","priority":2,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:27Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:42:33Z","started_at":"2026-06-11T21:37:23Z","closed_at":"2026-06-11T21:42:33Z","close_reason":"MCP/A2A declarations made real; orphaned mcp/tools.json folded in and removed","dependencies":[{"issue_id":"foreman-wzmx","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:27Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-wrrd","title":"[aimock] Enable strict mode and tighten/remove the empty-match catch-all fixture","description":"@copilotkit/aimock supports strict:true (verified config-loader.d.ts:101) so unmatched requests 503 instead of silently matching z-catchall.json (match {}), which currently risks false-green tests. Enable strict in packages/agents/aimock.json and remove/convert z-catchall to a strict error, fixing any tests that relied on the fallback.","acceptance_criteria":"aimock.json has strict:true; z-catchall removed/converted; npm test passes with every test hitting an explicit fixture.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:22Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:43:26Z","closed_at":"2026-06-11T21:43:26Z","close_reason":"strict:true enabled in aimock.json; z-catchall.json deleted; npm test 278 passed/10 skipped under strict; strict 503 verified via probe","dependencies":[{"issue_id":"foreman-wrrd","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:21Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/src/lib/db/database.types.ts b/packages/agents/src/lib/db/database.types.ts index 9b28c8f1..10fab1be 100644 --- a/packages/agents/src/lib/db/database.types.ts +++ b/packages/agents/src/lib/db/database.types.ts @@ -3429,25 +3429,34 @@ export type Database = { Row: { completed_at: string | null; created_at: string; + error_message: string | null; + fired_by: string | null; id: string; inputs: string; status: string; + trigger_id: string | null; workflow_id: string; }; Insert: { completed_at?: string | null; created_at: string; + error_message?: string | null; + fired_by?: string | null; id: string; inputs: string; status: string; + trigger_id?: string | null; workflow_id: string; }; Update: { completed_at?: string | null; created_at?: string; + error_message?: string | null; + fired_by?: string | null; id?: string; inputs?: string; status?: string; + trigger_id?: string | null; workflow_id?: string; }; Relationships: [ diff --git a/packages/agents/src/lib/workflows/engine.ts b/packages/agents/src/lib/workflows/engine.ts index 1b60d180..84958f95 100644 --- a/packages/agents/src/lib/workflows/engine.ts +++ b/packages/agents/src/lib/workflows/engine.ts @@ -15,6 +15,16 @@ export interface WorkflowEvent { missing?: string[]; } +/** How a run started — recorded on workflow_run so trigger-fired runs carry + * context in run history. (foreman-2afc) */ +export type FiredBy = "manual" | "cron" | "channel" | "poll"; + +export interface WorkflowTriggerContext { + firedBy: FiredBy; + /** The workflow_trigger row that fired this run (null for manual runs). */ + triggerId?: string; +} + /** * Execute a saved workflow, yielding SSE-friendly events as each step runs. */ @@ -23,6 +33,7 @@ export async function* executeWorkflow( userId: string, inputs: Record, _orgId?: string, + trigger: WorkflowTriggerContext = { firedBy: "manual" }, ): AsyncGenerator { const supabase = getSupabase(); @@ -59,6 +70,8 @@ export async function* executeWorkflow( inputs: JSON.stringify(inputs), status: "running", created_at: now, + fired_by: trigger.firedBy, + trigger_id: trigger.triggerId ?? null, }); // Own the run's terminal state in a try/finally so the row can never get @@ -123,7 +136,11 @@ export async function* executeWorkflow( await supabase .from("workflow_run") - .update({ status: "failed", completed_at: new Date().toISOString() }) + .update({ + status: "failed", + completed_at: new Date().toISOString(), + error_message: errorMsg, + }) .eq("id", runId); settled = true; @@ -145,7 +162,12 @@ export async function* executeWorkflow( try { await supabase .from("workflow_run") - .update({ status: "failed", completed_at: new Date().toISOString() }) + .update({ + status: "failed", + completed_at: new Date().toISOString(), + error_message: + "run did not complete (generator abandoned or aborted before a terminal state)", + }) .eq("id", runId); } catch (e) { console.error(`[engine] failed to mark abandoned run ${runId} failed:`, e); diff --git a/packages/agents/src/mastra/signals/channel-trigger-provider.ts b/packages/agents/src/mastra/signals/channel-trigger-provider.ts index 391fca4b..1403e8e2 100644 --- a/packages/agents/src/mastra/signals/channel-trigger-provider.ts +++ b/packages/agents/src/mastra/signals/channel-trigger-provider.ts @@ -124,7 +124,10 @@ export class ChannelTriggerSignalProvider extends SignalProvider<"channel-trigge .eq("id", row.id); try { - for await (const ev of executeWorkflow(row.workflow_id, userId, {})) { + for await (const ev of executeWorkflow(row.workflow_id, userId, {}, undefined, { + firedBy: "channel", + triggerId: row.id, + })) { if (ev.type === "error") { console.warn(`[channel-trigger] ${row.id} workflow error: ${ev.message}`); } diff --git a/packages/agents/src/mastra/signals/zapier-poll-provider.ts b/packages/agents/src/mastra/signals/zapier-poll-provider.ts index 93af0d2c..d393651f 100644 --- a/packages/agents/src/mastra/signals/zapier-poll-provider.ts +++ b/packages/agents/src/mastra/signals/zapier-poll-provider.ts @@ -195,7 +195,16 @@ export class ZapierPollSignalProvider extends SignalProvider<"zapier-poll"> { for (const rec of fresh.reverse()) { const key = recordKey(rec, cfg.dedupeKey); try { - for await (const ev of executeWorkflow(row.workflow_id, userId, recordToInputs(rec))) { + for await (const ev of executeWorkflow( + row.workflow_id, + userId, + recordToInputs(rec), + undefined, + { + firedBy: "poll", + triggerId: row.id, + }, + )) { if (ev.type === "error") { console.warn(`[zapier-poll] ${row.id} workflow error: ${ev.message}`); } else if (ev.type === "param_request") { diff --git a/packages/agents/src/routes/workflows.ts b/packages/agents/src/routes/workflows.ts index 5b5bda33..b631c889 100644 --- a/packages/agents/src/routes/workflows.ts +++ b/packages/agents/src/routes/workflows.ts @@ -316,6 +316,9 @@ workflows.get("/:id/runs", async (c) => { status: r.status, created_at: r.created_at, completed_at: r.completed_at ?? null, + error_message: r.error_message ?? null, + fired_by: r.fired_by ?? null, + trigger_id: r.trigger_id ?? null, })), ); }); diff --git a/packages/agents/src/workflows/cron-driver.ts b/packages/agents/src/workflows/cron-driver.ts index 7ae97fcb..81a48a73 100644 --- a/packages/agents/src/workflows/cron-driver.ts +++ b/packages/agents/src/workflows/cron-driver.ts @@ -199,7 +199,10 @@ async function runOne(workflowId: string, userId: string, triggerId: string) { // last_fired_at was already set by the atomic claim in tickCron, so the run is // dedup'd before we get here. We just execute and surface errors. try { - for await (const ev of executeWorkflow(workflowId, userId, {})) { + for await (const ev of executeWorkflow(workflowId, userId, {}, undefined, { + firedBy: "cron", + triggerId, + })) { if (ev.type === "error") { console.warn(`[cron-driver] ${triggerId} workflow error: ${ev.message}`); } diff --git a/packages/agents/tests/unit/workflow-engine.test.ts b/packages/agents/tests/unit/workflow-engine.test.ts index 7393f339..edc84e7d 100644 --- a/packages/agents/tests/unit/workflow-engine.test.ts +++ b/packages/agents/tests/unit/workflow-engine.test.ts @@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; let stepsResult: { data: unknown[] | null; error: unknown } = { data: [], error: null }; const updates: { table: string; payload: any }[] = []; +const inserts: { table: string; payload: any }[] = []; function createChain(table: string) { const b: any = {}; @@ -23,6 +24,7 @@ function createChain(table: string) { const oi = b.insert; b.insert = vi.fn((p: any) => { b._verb = "insert"; + inserts.push({ table, payload: p }); return oi(p); }); const ou = b.update; @@ -57,15 +59,18 @@ const oneStep = [ const statuses = () => updates.filter((u) => u.table === "workflow_run").map((u) => u.payload.status); +const lastRunUpdate = () => [...updates].reverse().find((u) => u.table === "workflow_run")?.payload; +const runInsert = () => inserts.find((i) => i.table === "workflow_run")?.payload; describe("executeWorkflow run-state", () => { beforeEach(() => { vi.resetAllMocks(); stepsResult = { data: oneStep, error: null }; updates.length = 0; + inserts.length = 0; }); - it("marks the run failed when a step throws (no stuck 'running')", async () => { + it("marks the run failed with the error_message when a step throws", async () => { mockRunAction.mockRejectedValue(new Error("boom")); const { executeWorkflow } = await import("@/lib/workflows/engine"); @@ -73,6 +78,7 @@ describe("executeWorkflow run-state", () => { for await (const ev of executeWorkflow("wf-1", "user-1", {})) events.push(ev.type); expect(statuses()).toEqual(["failed"]); // exactly one terminal write, not stuck + expect(lastRunUpdate()?.error_message).toBe("boom"); expect(events).toContain("error"); }); @@ -86,7 +92,7 @@ describe("executeWorkflow run-state", () => { expect(statuses()).toEqual(["success"]); }); - it("marks the run failed when the caller abandons the generator mid-stream", async () => { + it("marks the run failed (with a reason) when the caller abandons the generator", async () => { // runAction would hang, but the caller breaks before it's reached. mockRunAction.mockReturnValue(new Promise(() => {})); const { executeWorkflow } = await import("@/lib/workflows/engine"); @@ -96,5 +102,29 @@ describe("executeWorkflow run-state", () => { } expect(statuses()).toEqual(["failed"]); // finally rescued the orphaned run + expect(lastRunUpdate()?.error_message).toMatch(/did not complete/); + }); + + it("defaults fired_by='manual' with no trigger id", async () => { + mockRunAction.mockResolvedValue({ ok: true }); + const { executeWorkflow } = await import("@/lib/workflows/engine"); + + for await (const ev of executeWorkflow("wf-1", "user-1", {})) void ev; + + expect(runInsert()).toMatchObject({ fired_by: "manual", trigger_id: null }); + }); + + it("records the trigger context on the run row when fired by a trigger", async () => { + mockRunAction.mockResolvedValue({ ok: true }); + const { executeWorkflow } = await import("@/lib/workflows/engine"); + + for await (const ev of executeWorkflow("wf-1", "user-1", {}, undefined, { + firedBy: "cron", + triggerId: "trig-9", + })) { + void ev; + } + + expect(runInsert()).toMatchObject({ fired_by: "cron", trigger_id: "trig-9" }); }); }); diff --git a/supabase/migrations/20260612000000_workflow_run_observability.sql b/supabase/migrations/20260612000000_workflow_run_observability.sql new file mode 100644 index 00000000..77da845b --- /dev/null +++ b/supabase/migrations/20260612000000_workflow_run_observability.sql @@ -0,0 +1,17 @@ +-- workflow_run observability (foreman-2afc) +-- +-- Persist WHY a run fired and WHY it failed, so trigger-fired runs carry context +-- in run history (previously they only console.warn'd) and failures surface +-- their error instead of just flipping status to 'failed'. +-- +-- error_message — the failure reason when status='failed' +-- fired_by — how the run started: 'manual' | 'cron' | 'channel' | 'poll' +-- trigger_id — the workflow_trigger row that fired it (null for manual runs) + +ALTER TABLE public.workflow_run + ADD COLUMN IF NOT EXISTS error_message text, + ADD COLUMN IF NOT EXISTS fired_by text, + ADD COLUMN IF NOT EXISTS trigger_id text; + +CREATE INDEX IF NOT EXISTS workflow_run_trigger_id_idx + ON public.workflow_run(trigger_id); From 87efca52165ee8b4034410cc0b46787e58a53dc2 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 21:47:29 -0700 Subject: [PATCH 09/19] feat(agent): opt read-only Zapier tools into Mastra background execution (foreman-7am4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First increment of the 3rd primitive. Read-only tools get tool-level background:{enabled:true} via toolBackgroundConfig(isReadOnly); write/delete (approval) tools stay foreground. Deliberately NOT the agent-level backgroundTasks:{tools:'all'} — verified in the compiled resolveAgentToolConfig that 'all' returns {enabled:true} for EVERY tool, which would background the approval-required writes too. Tool-level opt-in makes only reads eligible (resolution order: agent-tools > tool-level > foreground), so nothing else needs agent config and writes are untouched. Env-gated + OFF by default (FOREMAN_BACKGROUND_TOOLS=1): this changes live tool dispatch and is unverified on the alpha, so it ships inert. With no waitTimeoutMs yet, an enabled read still blocks the loop (same UX) but runs as a tracked task, observable on the already-built /background-tasks/stream. Non-blocking (waitTimeoutMs) + a web stream consumer + live verification are the next increments. Gate unit-tested (reads on with flag, writes never, off by default). Full suite green (304). Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 3 +- packages/agents/src/lib/zapier-sdk-tools.ts | 28 +++++++++++++++ .../tests/unit/tool-background-config.test.ts | 36 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 packages/agents/tests/unit/tool-background-config.test.ts diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index b1eaeae0..b30a27bb 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,6 +92,7 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T04:42:18Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ueep","title":"[workflow] Live-verify the poll driver against a real Zapier read action","description":"poll-driver.ts (foreman-ld7s) is unit-tested only. Verify the real run path: bind a type=poll trigger to a real Zapier read action (e.g. gmail/new_email), run cron-driver-server, create a new record, assert exactly one workflow run fires and zero duplicates on re-poll; confirm baseline (first poll fires nothing) and intervalMinutes gating. Per verify-behavior-not-just-logic.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:35:12Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:12Z","dependencies":[{"issue_id":"foreman-ueep","depends_on_id":"foreman-ld7s","type":"discovered-from","created_at":"2026-06-11T17:35:12Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ld7s","title":"[workflow] Un-stub the poll trigger as a Foreman-native poll driver (tickPoll)","description":"Poll trigger type is documented in the migration and accepted by list/detach but hidden by attach-trigger and has no driver. Build src/workflows/poll-driver.ts mirroring cron-driver: read workflow_trigger type=poll, run the Zapier read action via runAction, diff results by config.dedupeKey field against last_dedupe_key cursor, fire executeWorkflow on new records, persist cursor. Un-stub attach_trigger (add poll + pollConfig). Wire into cron-driver-server. Rationale (explored alpha API hands-on): Mastra SignalProvider.poll() skips when zero in-memory subscriptions and notify() requires an in-process agent — it is built for provider-lives-in-agent/notify-thread, not a singleton worker firing DB workflows. SignalProvider will be adopted on the webhook/channel path instead. Keeps door open to Zapier watchTriggerInbox SSE later (iyq6).","status":"closed","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:27:10Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:07Z","started_at":"2026-06-12T00:27:27Z","closed_at":"2026-06-12T00:35:07Z","close_reason":"Poll driver implemented + unit-tested; full agents suite green (288). Live Zapier integration tracked as follow-up.","dependencies":[{"issue_id":"foreman-ld7s","depends_on_id":"foreman-c63f","type":"discovered-from","created_at":"2026-06-11T17:27:10Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-22c3","title":"Evaluate Mastra signals/signal-providers/notifications vs Foreman's home-grown triggers","description":"Foreman is on @mastra/core 1.41 (post-bump). Mastra now ships native primitives that overlap with Foreman's hand-built trigger/workflow/notification system: (1) SignalProvider (poll OR webhook an external source -\u003e push notifications into agent threads) ~= Foreman's channel-trigger + cron-driver + the stubbed poll trigger (c63f); (2) built-in dedupeKey on notify() directly solves the dedup gap in tv5p (Foreman hand-rolls idempotency); (3) @mastra/github-signals is a production provider and Foreman already has a GitHub channel; (4) background-task suspend/resume for human approval + notification inbox ~= Foreman's proposal/approval flow. Sibling to iyq6 (Zapier-durable eval). Decide adopt-Mastra-native vs keep-own BEFORE building the v8k1 workflow epic. Confirmed code-side: backgroundTasks enabled in mastra/index.ts; pubsub is default in-process EventEmitter (no distributed backend — matters for 2-process + multi-instance); signals/signal-providers not used at all.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T22:33:47Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:33:47Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -102,7 +103,7 @@ {"_type":"issue","id":"foreman-7jn1","title":"[workflow] Decision: per-user Slack isolation sufficient vs build shared group-channel context","description":"Foreman already isolates per Slack user (resourceId per user; slack/bot.ts:58; foreman.ts memory scope resource). Mastra natively supports scope thread|resource but NO shared-group memory, and slack/bot.ts:137 never persists the channel thread to the conversation table. Decide: (A) per-user is enough (lock it), or (B) build shared group context = new channel_conversation table + group memory scope + thread persistence. If B, spawn the feature issue.","acceptance_criteria":"Decision recorded; if B, a scoped feature issue (channel_conversation table + group scope + persist channel thread) is filed.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:18Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:18Z","dependencies":[{"issue_id":"foreman-7jn1","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:17Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ezsm","title":"[workflow] Build the /workflows UI page (list saved automations + run history)","description":"foreman-v8k1 calls for a UI so users can see saved automations; it does not exist. Build a /workflows page in packages/web listing workflows + triggers + recent runs (status, error_message from the observability issue), using the existing /workflows/* API routes and the shadcn stack. Per-workflow run-history view.","acceptance_criteria":"/workflows lists the user's workflows + triggers; clicking shows run history incl. failed-run errors; reads from existing API routes.","status":"open","priority":2,"issue_type":"feature","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:13Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:13Z","dependencies":[{"issue_id":"foreman-ezsm","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:13Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-8fcs","title":"[workflow] Tests: params.ts substitution + engine step-failure paths","description":"params.ts (extract/substitute/validate) and engine.ts step-failure handling (~80-122) are untested. Add unit tests: substituteParams (partial replace, unknown keys left as-is, nested/arrays), validateParams (missing detected), engine step failure (runAction throws -\u003e failed + error captured), generator-throw cleanup, empty-steps early-exit.","acceptance_criteria":"params + engine error paths covered; npm test green.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:09Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:09Z","dependencies":[{"issue_id":"foreman-8fcs","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:08Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-2afc","title":"[workflow] Run-state \u0026 observability: orphaned 'running' rows on crash; persist error_message + trigger context","description":"engine.ts inserts workflow_run status=running (~line 56) and never sets failed if executeWorkflow throws before completing -\u003e rows orphaned forever. Wrap generator consumption in try/finally at each call site (routes/workflows.ts, channel-trigger.ts, cron-driver.ts) to mark failed with the error. Persist error_message + fired_by(manual/cron/channel) + trigger_id on workflow_run; trigger-fired runs only console.warn today and never appear in run history. Surface in GET /workflows/:id/runs.","acceptance_criteria":"A failed run shows status=failed + error_message within ~1s; cron/channel-fired runs appear in run history with trigger context; no rows stuck in running.","notes":"Part 1 DONE (4c5da4c): engine try/finally + settled flag = no run stuck in running; 3 unit tests. Part 2 GATED on user go: workflow_run migration (error_message, fired_by, trigger_id), thread trigger context through executeWorkflow + 3 driver sites, surface in GET /workflows/:id/runs, regen db types (needs Foreman local supabase up; a Zillow stack currently holds default ports).","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:04Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:41:09Z","dependencies":[{"issue_id":"foreman-2afc","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:03Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-2afc","title":"[workflow] Run-state \u0026 observability: orphaned 'running' rows on crash; persist error_message + trigger context","description":"engine.ts inserts workflow_run status=running (~line 56) and never sets failed if executeWorkflow throws before completing -\u003e rows orphaned forever. Wrap generator consumption in try/finally at each call site (routes/workflows.ts, channel-trigger.ts, cron-driver.ts) to mark failed with the error. Persist error_message + fired_by(manual/cron/channel) + trigger_id on workflow_run; trigger-fired runs only console.warn today and never appear in run history. Surface in GET /workflows/:id/runs.","acceptance_criteria":"A failed run shows status=failed + error_message within ~1s; cron/channel-fired runs appear in run history with trigger context; no rows stuck in running.","notes":"Part 1 DONE (4c5da4c): engine try/finally + settled flag = no run stuck in running; 3 unit tests. Part 2 GATED on user go: workflow_run migration (error_message, fired_by, trigger_id), thread trigger context through executeWorkflow + 3 driver sites, surface in GET /workflows/:id/runs, regen db types (needs Foreman local supabase up; a Zillow stack currently holds default ports).","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:04Z","created_by":"HamChowderr","updated_at":"2026-06-12T04:25:28Z","closed_at":"2026-06-12T04:25:28Z","close_reason":"Part 1 (4c5da4c): no run stuck in running. Part 2 (c039220): workflow_run migration + error_message/fired_by/trigger_id persisted + surfaced in GET runs. db:types:check clean, migration applies on clean reset, 300 tests green.","dependencies":[{"issue_id":"foreman-2afc","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:03Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-tv5p","title":"[workflow] BUG: trigger dedup/idempotency — channel-trigger has no dedup; cron can double-fire on same-minute restart","description":"channel-trigger.ts (matchAndFireChannelTriggers ~43-79) fires with NO dedup despite an unused workflow_trigger.last_dedupe_key column — a retried webhook = duplicate runs. Add a dedupe key (channel+sender+ts+text hash or delivery id) checked against last_dedupe_key before firing. Separately cron-driver.ts (~164-167) can double-fire if the process restarts mid-minute; add a guard/lock. runOne also swallows errors silently.","acceptance_criteria":"Duplicate webhook =\u003e 1 run not 2 (unit test); a new message =\u003e new run; cron fires once per minute even across a same-minute restart (unit test).","status":"closed","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:59Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:29:21Z","started_at":"2026-06-12T03:15:08Z","closed_at":"2026-06-12T03:29:21Z","close_reason":"Channel dedup + cron same-minute atomic claim implemented and unit-tested per acceptance criteria. Error-surfacing portion tracked under 2afc.","dependencies":[{"issue_id":"foreman-tv5p","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:30:58Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-wzmx","title":"[aimock] Resolve declared-but-unfixtured MCP/A2A mocks (write fixtures or remove declarations)","description":"aimock.json declares 8 MCP tools + 5 A2A agents but tests/fixtures/aimock/ has no mcp/ or a2a/ fixtures — a declared-vs-real mismatch that 503s under strict mode. Either write minimal fixtures for the tools/agents tests actually exercise, or remove the unused declarations so config reflects reality.","acceptance_criteria":"Every MCP/A2A declaration has a fixture OR is removed; npm test passes under strict mode with no unmatched MCP/A2A requests.","status":"closed","priority":2,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:27Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:42:33Z","started_at":"2026-06-11T21:37:23Z","closed_at":"2026-06-11T21:42:33Z","close_reason":"MCP/A2A declarations made real; orphaned mcp/tools.json folded in and removed","dependencies":[{"issue_id":"foreman-wzmx","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:27Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-wrrd","title":"[aimock] Enable strict mode and tighten/remove the empty-match catch-all fixture","description":"@copilotkit/aimock supports strict:true (verified config-loader.d.ts:101) so unmatched requests 503 instead of silently matching z-catchall.json (match {}), which currently risks false-green tests. Enable strict in packages/agents/aimock.json and remove/convert z-catchall to a strict error, fixing any tests that relied on the fallback.","acceptance_criteria":"aimock.json has strict:true; z-catchall removed/converted; npm test passes with every test hitting an explicit fixture.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:22Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:43:26Z","closed_at":"2026-06-11T21:43:26Z","close_reason":"strict:true enabled in aimock.json; z-catchall.json deleted; npm test 278 passed/10 skipped under strict; strict 503 verified via probe","dependencies":[{"issue_id":"foreman-wrrd","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:21Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/src/lib/zapier-sdk-tools.ts b/packages/agents/src/lib/zapier-sdk-tools.ts index 615d862d..c950abaa 100644 --- a/packages/agents/src/lib/zapier-sdk-tools.ts +++ b/packages/agents/src/lib/zapier-sdk-tools.ts @@ -70,6 +70,33 @@ const READ_ONLY = new Set([ "getProfile", ]); +/** + * Opt read-only tools into Mastra background execution (foreman-7am4). + * + * Returns the `background` slice to spread into a tool's createTool() config: + * `{ background: { enabled: true } }` for a read-only tool when the feature flag + * is set, otherwise `{}`. + * + * Read tools are the safe target: they carry no approval/proposal flow, so + * routing them through the background-task manager can't disturb writes. We do + * NOT use the agent-level `backgroundTasks: { tools: 'all' }` — that resolves to + * `{ enabled: true }` for EVERY tool (incl. approval-required writes). Tool-level + * `background.enabled` makes *only* these tools eligible; everything else stays + * foreground with no agent-level config needed. + * + * Env-gated + OFF by default (`FOREMAN_BACKGROUND_TOOLS=1`): this changes live + * tool dispatch and is unverified on the @mastra alpha, so it ships inert until + * explicitly enabled for testing. Read at call time so tests can toggle it. + */ +export function toolBackgroundConfig(isReadOnly: boolean): { + background?: { enabled: true }; +} { + if (isReadOnly && process.env.FOREMAN_BACKGROUND_TOOLS === "1") { + return { background: { enabled: true } }; + } + return {}; +} + /** * SDK methods excluded from tool generation. * @@ -226,6 +253,7 @@ export function generateZapierTools( description, inputSchema: unwrapped as unknown as z.ZodObject, ...(isDestructive ? { requireApproval: true } : {}), + ...toolBackgroundConfig(isReadOnly), ...mcpAnnotations, execute: async (input) => { try { diff --git a/packages/agents/tests/unit/tool-background-config.test.ts b/packages/agents/tests/unit/tool-background-config.test.ts new file mode 100644 index 00000000..63b23d68 --- /dev/null +++ b/packages/agents/tests/unit/tool-background-config.test.ts @@ -0,0 +1,36 @@ +/** + * Unit tests for toolBackgroundConfig (foreman-7am4) — the gate that opts + * read-only Zapier tools into Mastra background execution. The safety invariant + * is that write/approval tools (isReadOnly=false) are NEVER backgrounded, and + * the feature is off unless FOREMAN_BACKGROUND_TOOLS=1. + */ +import { afterEach, describe, expect, it } from "vitest"; +import { toolBackgroundConfig } from "@/lib/zapier-sdk-tools"; + +const orig = process.env.FOREMAN_BACKGROUND_TOOLS; +afterEach(() => { + if (orig === undefined) delete process.env.FOREMAN_BACKGROUND_TOOLS; + else process.env.FOREMAN_BACKGROUND_TOOLS = orig; +}); + +describe("toolBackgroundConfig", () => { + it("opts a read-only tool into background when the flag is set", () => { + process.env.FOREMAN_BACKGROUND_TOOLS = "1"; + expect(toolBackgroundConfig(true)).toEqual({ background: { enabled: true } }); + }); + + it("never backgrounds a write/approval tool, even with the flag set", () => { + process.env.FOREMAN_BACKGROUND_TOOLS = "1"; + expect(toolBackgroundConfig(false)).toEqual({}); + }); + + it("is off by default — read-only tools stay foreground without the flag", () => { + delete process.env.FOREMAN_BACKGROUND_TOOLS; + expect(toolBackgroundConfig(true)).toEqual({}); + }); + + it("treats any value other than '1' as off", () => { + process.env.FOREMAN_BACKGROUND_TOOLS = "true"; + expect(toolBackgroundConfig(true)).toEqual({}); + }); +}); From 0a2082825235f73a3792f98055274c86e76c6ac9 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 22:05:22 -0700 Subject: [PATCH 10/19] =?UTF-8?q?fix(agent):=20background=20search=5Fhisto?= =?UTF-8?q?ry,=20not=20the=20Zapier=20tools=20=E2=80=94=20live-verify=20fi?= =?UTF-8?q?nding=20(foreman-7am4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live verification (mastra dev) caught a regression in the prior cut (87efca5): enabling background on the read-only Zapier tools makes Mastra inject a _background field into their input schemas, and Studio's toJSONSchema introspection over those (pathological) zod schemas hangs the dev server at 'Logging to server.log' with no port bound — the same failure the lazy-init-zapier test guards. mastra build was unaffected (no Studio), which is why the bundle passed but dev hung. Confirmed by bisection: no flag -> dev binds :4111 in ~12s; flag on Zapier tools -> hang. Moving the opt-in to search_history (a slow-ish RAG read with a simple 3-field schema) -> dev binds in ~12s with the flag on, and /background-tasks/stream serves SSE 200 (manager live). So the primitive works on Foreman's simple-schema custom tools; the Zapier SDK tools are off-limits until the alpha's toJSONSchema issue is fixed. Replaces the Zapier-tool gate with a shared backgroundIfEnabled() helper applied to search_history, env-gated FOREMAN_BACKGROUND_TOOLS=1 (OFF by default). Gate unit-tested; full suite green (303). Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 2 +- packages/agents/src/lib/background.ts | 22 ++++++++++++ packages/agents/src/lib/zapier-sdk-tools.ts | 28 --------------- .../agents/src/mastra/tools/search-history.ts | 5 +++ .../tests/unit/background-config.test.ts | 29 +++++++++++++++ .../tests/unit/tool-background-config.test.ts | 36 ------------------- 6 files changed, 57 insertions(+), 65 deletions(-) create mode 100644 packages/agents/src/lib/background.ts create mode 100644 packages/agents/tests/unit/background-config.test.ts delete mode 100644 packages/agents/tests/unit/tool-background-config.test.ts diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index b30a27bb..08219406 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,7 +92,7 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T04:42:18Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"Increment 1 DONE (87efca5): read-only Zapier tools opt into background via toolBackgroundConfig(isReadOnly), env-gated FOREMAN_BACKGROUND_TOOLS=1 (OFF by default). NOT agent-level tools:'all' (verified it returns {enabled:true} for EVERY tool incl approval writes). Gate unit-tested; 304 suite green. REMAINING: (2) web consumer of /background-tasks/stream; (3) waitTimeoutMs for non-blocking; (4) LIVE verify on dev server with real keys + a Zapier connection (the actual task-dispatch behavior on the alpha is only verifiable live; increment 1 with no waitTimeout is blocking-equivalent so low-risk).","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T04:49:02Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ueep","title":"[workflow] Live-verify the poll driver against a real Zapier read action","description":"poll-driver.ts (foreman-ld7s) is unit-tested only. Verify the real run path: bind a type=poll trigger to a real Zapier read action (e.g. gmail/new_email), run cron-driver-server, create a new record, assert exactly one workflow run fires and zero duplicates on re-poll; confirm baseline (first poll fires nothing) and intervalMinutes gating. Per verify-behavior-not-just-logic.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:35:12Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:12Z","dependencies":[{"issue_id":"foreman-ueep","depends_on_id":"foreman-ld7s","type":"discovered-from","created_at":"2026-06-11T17:35:12Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ld7s","title":"[workflow] Un-stub the poll trigger as a Foreman-native poll driver (tickPoll)","description":"Poll trigger type is documented in the migration and accepted by list/detach but hidden by attach-trigger and has no driver. Build src/workflows/poll-driver.ts mirroring cron-driver: read workflow_trigger type=poll, run the Zapier read action via runAction, diff results by config.dedupeKey field against last_dedupe_key cursor, fire executeWorkflow on new records, persist cursor. Un-stub attach_trigger (add poll + pollConfig). Wire into cron-driver-server. Rationale (explored alpha API hands-on): Mastra SignalProvider.poll() skips when zero in-memory subscriptions and notify() requires an in-process agent — it is built for provider-lives-in-agent/notify-thread, not a singleton worker firing DB workflows. SignalProvider will be adopted on the webhook/channel path instead. Keeps door open to Zapier watchTriggerInbox SSE later (iyq6).","status":"closed","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:27:10Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:07Z","started_at":"2026-06-12T00:27:27Z","closed_at":"2026-06-12T00:35:07Z","close_reason":"Poll driver implemented + unit-tested; full agents suite green (288). Live Zapier integration tracked as follow-up.","dependencies":[{"issue_id":"foreman-ld7s","depends_on_id":"foreman-c63f","type":"discovered-from","created_at":"2026-06-11T17:27:10Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-22c3","title":"Evaluate Mastra signals/signal-providers/notifications vs Foreman's home-grown triggers","description":"Foreman is on @mastra/core 1.41 (post-bump). Mastra now ships native primitives that overlap with Foreman's hand-built trigger/workflow/notification system: (1) SignalProvider (poll OR webhook an external source -\u003e push notifications into agent threads) ~= Foreman's channel-trigger + cron-driver + the stubbed poll trigger (c63f); (2) built-in dedupeKey on notify() directly solves the dedup gap in tv5p (Foreman hand-rolls idempotency); (3) @mastra/github-signals is a production provider and Foreman already has a GitHub channel; (4) background-task suspend/resume for human approval + notification inbox ~= Foreman's proposal/approval flow. Sibling to iyq6 (Zapier-durable eval). Decide adopt-Mastra-native vs keep-own BEFORE building the v8k1 workflow epic. Confirmed code-side: backgroundTasks enabled in mastra/index.ts; pubsub is default in-process EventEmitter (no distributed backend — matters for 2-process + multi-instance); signals/signal-providers not used at all.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T22:33:47Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:33:47Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/src/lib/background.ts b/packages/agents/src/lib/background.ts new file mode 100644 index 00000000..e55154e3 --- /dev/null +++ b/packages/agents/src/lib/background.ts @@ -0,0 +1,22 @@ +/** + * Gated background-execution config for agent tools (foreman-7am4). + * + * Returns `{ background: { enabled: true } }` when FOREMAN_BACKGROUND_TOOLS=1, + * else `{}` — spread into a tool's createTool() config to opt it into Mastra + * background execution. + * + * Apply ONLY to simple-schema custom tools. Live verification showed that + * backgrounding the generated Zapier SDK tools hangs `mastra dev`: enabling + * background makes Mastra inject a `_background` field into the tool's input + * schema, and Studio's `toJSONSchema` introspection over the Zapier tools' + * (pathologically complex) zod schemas then loops forever — the same failure the + * lazy-init-zapier regression test guards. `mastra build` is unaffected (no + * Studio introspection), but the dev playground is. Simple schemas (e.g. + * search_history's 3 fields) inject + introspect fine. + * + * Read at call time so tests can toggle the flag. OFF by default — this changes + * live tool dispatch and is alpha-unverified end-to-end, so it ships inert. + */ +export function backgroundIfEnabled(): { background?: { enabled: true } } { + return process.env.FOREMAN_BACKGROUND_TOOLS === "1" ? { background: { enabled: true } } : {}; +} diff --git a/packages/agents/src/lib/zapier-sdk-tools.ts b/packages/agents/src/lib/zapier-sdk-tools.ts index c950abaa..615d862d 100644 --- a/packages/agents/src/lib/zapier-sdk-tools.ts +++ b/packages/agents/src/lib/zapier-sdk-tools.ts @@ -70,33 +70,6 @@ const READ_ONLY = new Set([ "getProfile", ]); -/** - * Opt read-only tools into Mastra background execution (foreman-7am4). - * - * Returns the `background` slice to spread into a tool's createTool() config: - * `{ background: { enabled: true } }` for a read-only tool when the feature flag - * is set, otherwise `{}`. - * - * Read tools are the safe target: they carry no approval/proposal flow, so - * routing them through the background-task manager can't disturb writes. We do - * NOT use the agent-level `backgroundTasks: { tools: 'all' }` — that resolves to - * `{ enabled: true }` for EVERY tool (incl. approval-required writes). Tool-level - * `background.enabled` makes *only* these tools eligible; everything else stays - * foreground with no agent-level config needed. - * - * Env-gated + OFF by default (`FOREMAN_BACKGROUND_TOOLS=1`): this changes live - * tool dispatch and is unverified on the @mastra alpha, so it ships inert until - * explicitly enabled for testing. Read at call time so tests can toggle it. - */ -export function toolBackgroundConfig(isReadOnly: boolean): { - background?: { enabled: true }; -} { - if (isReadOnly && process.env.FOREMAN_BACKGROUND_TOOLS === "1") { - return { background: { enabled: true } }; - } - return {}; -} - /** * SDK methods excluded from tool generation. * @@ -253,7 +226,6 @@ export function generateZapierTools( description, inputSchema: unwrapped as unknown as z.ZodObject, ...(isDestructive ? { requireApproval: true } : {}), - ...toolBackgroundConfig(isReadOnly), ...mcpAnnotations, execute: async (input) => { try { diff --git a/packages/agents/src/mastra/tools/search-history.ts b/packages/agents/src/mastra/tools/search-history.ts index 05c2c09c..93b21373 100644 --- a/packages/agents/src/mastra/tools/search-history.ts +++ b/packages/agents/src/mastra/tools/search-history.ts @@ -1,10 +1,15 @@ import { createTool } from "@mastra/core/tools"; import { z } from "zod"; +import { backgroundIfEnabled } from "@/lib/background"; import { searchActionHistory } from "@/lib/rag"; export const searchHistoryTool = createTool({ id: "search_history", strict: true, + // Opt this read into Mastra background execution when FOREMAN_BACKGROUND_TOOLS=1. + // Simple schema (3 fields) → safe for Studio introspection, unlike the Zapier + // SDK tools whose schemas hang `mastra dev` when background is enabled. + ...backgroundIfEnabled(), description: "Search the user's past action history to find similar actions they've taken before. " + "Use this to recommend actions based on what the user has done previously, or to recall " + diff --git a/packages/agents/tests/unit/background-config.test.ts b/packages/agents/tests/unit/background-config.test.ts new file mode 100644 index 00000000..dc652448 --- /dev/null +++ b/packages/agents/tests/unit/background-config.test.ts @@ -0,0 +1,29 @@ +/** + * Unit tests for backgroundIfEnabled (foreman-7am4) — the env gate that opts a + * tool into Mastra background execution. Off unless FOREMAN_BACKGROUND_TOOLS=1. + */ +import { afterEach, describe, expect, it } from "vitest"; +import { backgroundIfEnabled } from "@/lib/background"; + +const orig = process.env.FOREMAN_BACKGROUND_TOOLS; +afterEach(() => { + if (orig === undefined) delete process.env.FOREMAN_BACKGROUND_TOOLS; + else process.env.FOREMAN_BACKGROUND_TOOLS = orig; +}); + +describe("backgroundIfEnabled", () => { + it("returns a background config when the flag is set to '1'", () => { + process.env.FOREMAN_BACKGROUND_TOOLS = "1"; + expect(backgroundIfEnabled()).toEqual({ background: { enabled: true } }); + }); + + it("is off by default — returns an empty slice", () => { + delete process.env.FOREMAN_BACKGROUND_TOOLS; + expect(backgroundIfEnabled()).toEqual({}); + }); + + it("treats any value other than '1' as off", () => { + process.env.FOREMAN_BACKGROUND_TOOLS = "true"; + expect(backgroundIfEnabled()).toEqual({}); + }); +}); diff --git a/packages/agents/tests/unit/tool-background-config.test.ts b/packages/agents/tests/unit/tool-background-config.test.ts deleted file mode 100644 index 63b23d68..00000000 --- a/packages/agents/tests/unit/tool-background-config.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Unit tests for toolBackgroundConfig (foreman-7am4) — the gate that opts - * read-only Zapier tools into Mastra background execution. The safety invariant - * is that write/approval tools (isReadOnly=false) are NEVER backgrounded, and - * the feature is off unless FOREMAN_BACKGROUND_TOOLS=1. - */ -import { afterEach, describe, expect, it } from "vitest"; -import { toolBackgroundConfig } from "@/lib/zapier-sdk-tools"; - -const orig = process.env.FOREMAN_BACKGROUND_TOOLS; -afterEach(() => { - if (orig === undefined) delete process.env.FOREMAN_BACKGROUND_TOOLS; - else process.env.FOREMAN_BACKGROUND_TOOLS = orig; -}); - -describe("toolBackgroundConfig", () => { - it("opts a read-only tool into background when the flag is set", () => { - process.env.FOREMAN_BACKGROUND_TOOLS = "1"; - expect(toolBackgroundConfig(true)).toEqual({ background: { enabled: true } }); - }); - - it("never backgrounds a write/approval tool, even with the flag set", () => { - process.env.FOREMAN_BACKGROUND_TOOLS = "1"; - expect(toolBackgroundConfig(false)).toEqual({}); - }); - - it("is off by default — read-only tools stay foreground without the flag", () => { - delete process.env.FOREMAN_BACKGROUND_TOOLS; - expect(toolBackgroundConfig(true)).toEqual({}); - }); - - it("treats any value other than '1' as off", () => { - process.env.FOREMAN_BACKGROUND_TOOLS = "true"; - expect(toolBackgroundConfig(true)).toEqual({}); - }); -}); From 177c281b1ddfc12efae392c829ca02c19ae168b0 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 22:34:00 -0700 Subject: [PATCH 11/19] fix(signals): connect the trigger providers in the worker/webhook processes so notify() fires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them never built the agent, so the provider had no connected agent (and no notification storage) — notifyOwner() was a silent no-op. Workflow firing was unaffected (it doesn't need the agent), but the notify() signal never reached a thread. cron-driver-server and webhook-server now call getMastra() at startup, which constructs the foreman agent in-process and connects the providers (they have no pollInterval, so this starts no second loop). Live-verified: cron-driver-server logs 'poll provider connected: true' and both drivers start clean. The remaining piece for user-visible notifications — a web consumer of the notification store — is tracked separately. Full suite green (303). Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 3 ++- packages/agents/src/cron-driver-server.ts | 11 +++++++++++ packages/agents/src/webhook-server.ts | 9 +++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 08219406..93fd6e3d 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,7 +92,8 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"Increment 1 DONE (87efca5): read-only Zapier tools opt into background via toolBackgroundConfig(isReadOnly), env-gated FOREMAN_BACKGROUND_TOOLS=1 (OFF by default). NOT agent-level tools:'all' (verified it returns {enabled:true} for EVERY tool incl approval writes). Gate unit-tested; 304 suite green. REMAINING: (2) web consumer of /background-tasks/stream; (3) waitTimeoutMs for non-blocking; (4) LIVE verify on dev server with real keys + a Zapier connection (the actual task-dispatch behavior on the alpha is only verifiable live; increment 1 with no waitTimeout is blocking-equivalent so low-risk).","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T04:49:02Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-tbjb","title":"[signals] notify() is inert — driving processes don't connect the providers","description":"The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them don't reliably CONNECT them, so notify() is a no-op (provider.agent undefined -\u003e notifyOwner returns early). (1) cron-driver-server drives zapierPollProvider.runDuePolls() but never calls getMastra()/builds the foreman agent, so poll notify() never fires. (2) channel: slack/bot.ts builds the agent lazily inside generateReply, AFTER matchAndFireChannelTriggers, so the channel provider isn't connected for trigger-only messages until the process first runs generateReply. (3) even when fired, notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) and no web UI consumes the notification store, so it's not user-visible. Workflow FIRING is unaffected/correct (doesn't need the agent). FIX: have the driving processes construct the agent at startup (cron-driver-server: getMastra() so the provider connects; webhook-server: build foreman at startup) + a web consumer of the notification store / thread to surface it. Until then notify() is scaffolding, not delivered.","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T05:22:51Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:22:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"LIVE-VERIFIED (mastra dev). FINDING: backgrounding the read-only Zapier SDK tools HANGS mastra dev — enabling background injects a _background field into the tool input schema and Studio toJSONSchema over the Zapier zod schemas loops forever (same as lazy-init-zapier hang). mastra build is unaffected (no Studio). Bisected: no-flag binds :4111 in ~12s; flag-on-Zapier hangs. FIX (commit 0a20828): background search_history instead (simple 3-field schema) -\u003e dev binds in ~12s with flag on; /background-tasks/stream serves SSE 200 (manager live). lib/background.ts backgroundIfEnabled() gate, FOREMAN_BACKGROUND_TOOLS=1, OFF by default. REMAINING for full non-blocking UX (modest, search_history-only given the Zapier wall): waitTimeoutMs + web /background-tasks/stream consumer + end-to-end chat verify (LLM round-trip). Zapier tools blocked until alpha fixes toJSONSchema.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:05:59Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ueep","title":"[workflow] Live-verify the poll driver against a real Zapier read action","description":"poll-driver.ts (foreman-ld7s) is unit-tested only. Verify the real run path: bind a type=poll trigger to a real Zapier read action (e.g. gmail/new_email), run cron-driver-server, create a new record, assert exactly one workflow run fires and zero duplicates on re-poll; confirm baseline (first poll fires nothing) and intervalMinutes gating. Per verify-behavior-not-just-logic.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:35:12Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:12Z","dependencies":[{"issue_id":"foreman-ueep","depends_on_id":"foreman-ld7s","type":"discovered-from","created_at":"2026-06-11T17:35:12Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ld7s","title":"[workflow] Un-stub the poll trigger as a Foreman-native poll driver (tickPoll)","description":"Poll trigger type is documented in the migration and accepted by list/detach but hidden by attach-trigger and has no driver. Build src/workflows/poll-driver.ts mirroring cron-driver: read workflow_trigger type=poll, run the Zapier read action via runAction, diff results by config.dedupeKey field against last_dedupe_key cursor, fire executeWorkflow on new records, persist cursor. Un-stub attach_trigger (add poll + pollConfig). Wire into cron-driver-server. Rationale (explored alpha API hands-on): Mastra SignalProvider.poll() skips when zero in-memory subscriptions and notify() requires an in-process agent — it is built for provider-lives-in-agent/notify-thread, not a singleton worker firing DB workflows. SignalProvider will be adopted on the webhook/channel path instead. Keeps door open to Zapier watchTriggerInbox SSE later (iyq6).","status":"closed","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:27:10Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:07Z","started_at":"2026-06-12T00:27:27Z","closed_at":"2026-06-12T00:35:07Z","close_reason":"Poll driver implemented + unit-tested; full agents suite green (288). Live Zapier integration tracked as follow-up.","dependencies":[{"issue_id":"foreman-ld7s","depends_on_id":"foreman-c63f","type":"discovered-from","created_at":"2026-06-11T17:27:10Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-22c3","title":"Evaluate Mastra signals/signal-providers/notifications vs Foreman's home-grown triggers","description":"Foreman is on @mastra/core 1.41 (post-bump). Mastra now ships native primitives that overlap with Foreman's hand-built trigger/workflow/notification system: (1) SignalProvider (poll OR webhook an external source -\u003e push notifications into agent threads) ~= Foreman's channel-trigger + cron-driver + the stubbed poll trigger (c63f); (2) built-in dedupeKey on notify() directly solves the dedup gap in tv5p (Foreman hand-rolls idempotency); (3) @mastra/github-signals is a production provider and Foreman already has a GitHub channel; (4) background-task suspend/resume for human approval + notification inbox ~= Foreman's proposal/approval flow. Sibling to iyq6 (Zapier-durable eval). Decide adopt-Mastra-native vs keep-own BEFORE building the v8k1 workflow epic. Confirmed code-side: backgroundTasks enabled in mastra/index.ts; pubsub is default in-process EventEmitter (no distributed backend — matters for 2-process + multi-instance); signals/signal-providers not used at all.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T22:33:47Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:33:47Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/src/cron-driver-server.ts b/packages/agents/src/cron-driver-server.ts index 9ba04925..d3e1ad08 100644 --- a/packages/agents/src/cron-driver-server.ts +++ b/packages/agents/src/cron-driver-server.ts @@ -7,9 +7,20 @@ * driver (Zapier read actions returning new records). Only run a single * instance — there is no distributed lock. */ +import { getMastra } from "./mastra"; +import { zapierPollProvider } from "./mastra/signals/zapier-poll-provider"; import { startCronDriver } from "./workflows/cron-driver"; import { startPollDriver } from "./workflows/poll-driver"; +// Build the Mastra instance here so the foreman agent is constructed in THIS +// worker process — that connects the SignalProviders (poll/channel) and gives +// them the notification storage notify() needs. Without it, runDuePolls() fires +// workflows fine but notifyOwner() is a silent no-op (no connected agent). The +// provider has no pollInterval, so constructing the agent here doesn't start a +// second poll loop. (foreman notify-wiring) +getMastra(); +console.log(`[trigger-driver] poll provider connected: ${zapierPollProvider.isConnected}`); + const stopCron = startCronDriver(); const stopPoll = startPollDriver(); diff --git a/packages/agents/src/webhook-server.ts b/packages/agents/src/webhook-server.ts index 6feac3b9..614c8139 100644 --- a/packages/agents/src/webhook-server.ts +++ b/packages/agents/src/webhook-server.ts @@ -4,6 +4,8 @@ import { getGoogleChatBot } from "./gchat/bot"; import { getGitHubBot } from "./github/bot"; import { getiMessageBot } from "./imessage/bot"; import { getLinearBot } from "./linear/bot"; +import { getMastra } from "./mastra"; +import { channelTriggerProvider } from "./mastra/signals/channel-trigger-provider"; import { getSlackBot } from "./slack/bot"; import { getTeamsBot } from "./teams/bot"; import { getTelegramBot } from "./telegram/bot"; @@ -233,6 +235,13 @@ const server = http.createServer(async (req, res) => { server.listen(port, () => { console.log(`Webhook server running on port ${port}`); + // Build the foreman agent up front so the channel SignalProvider is connected + // before the first inbound message — otherwise matchAndFireChannelTriggers + // runs before any generateReply() has lazily built the agent, and the + // channel notify() is a no-op for trigger-only messages. (foreman notify-wiring) + getMastra(); + console.log(`[webhook] channel provider connected: ${channelTriggerProvider.isConnected}`); + // Initialize Slack bot — getSlackBot() handles init + rehydration internally if (process.env.SLACK_SIGNING_SECRET) { getSlackBot() From 278246ac3957c4a55fc14c5462dd06b8b67005a4 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Thu, 11 Jun 2026 23:09:35 -0700 Subject: [PATCH 12/19] test(live): end-to-end poll driver against real Postgres (foreman-ueep) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live-tier integration test exercising the full poll path against real local Supabase: attach_trigger(type=poll) -> zapierPollProvider.runDuePolls() -> executeWorkflow -> workflow_run rows. Asserts baseline (first poll fires nothing, sets the cursor), a new record fires the workflow exactly once with fired_by=poll + trigger_id + status=success, and dedup (no double-fire when the newest record equals the cursor). Only the external Zapier read is stubbed (runAction) — the DB was reset and there's no OAuth-connected app to create records in; cursor persistence, firing, dedup and workflow_run inserts are all real Postgres. Lives in tests/live/ so it is excluded from the mocked CI suite (no Supabase in CI) and run via npm run test:live. 4/4 green locally. Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 2 +- .../agents/tests/live/poll-driver.test.ts | 198 ++++++++++++++++++ 2 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 packages/agents/tests/live/poll-driver.test.ts diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 93fd6e3d..7e5e4a25 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,7 +92,7 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-tbjb","title":"[signals] notify() is inert — driving processes don't connect the providers","description":"The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them don't reliably CONNECT them, so notify() is a no-op (provider.agent undefined -\u003e notifyOwner returns early). (1) cron-driver-server drives zapierPollProvider.runDuePolls() but never calls getMastra()/builds the foreman agent, so poll notify() never fires. (2) channel: slack/bot.ts builds the agent lazily inside generateReply, AFTER matchAndFireChannelTriggers, so the channel provider isn't connected for trigger-only messages until the process first runs generateReply. (3) even when fired, notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) and no web UI consumes the notification store, so it's not user-visible. Workflow FIRING is unaffected/correct (doesn't need the agent). FIX: have the driving processes construct the agent at startup (cron-driver-server: getMastra() so the provider connects; webhook-server: build foreman at startup) + a web consumer of the notification store / thread to surface it. Until then notify() is scaffolding, not delivered.","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T05:22:51Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:22:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-tbjb","title":"[signals] notify() is inert — driving processes don't connect the providers","description":"The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them don't reliably CONNECT them, so notify() is a no-op (provider.agent undefined -\u003e notifyOwner returns early). (1) cron-driver-server drives zapierPollProvider.runDuePolls() but never calls getMastra()/builds the foreman agent, so poll notify() never fires. (2) channel: slack/bot.ts builds the agent lazily inside generateReply, AFTER matchAndFireChannelTriggers, so the channel provider isn't connected for trigger-only messages until the process first runs generateReply. (3) even when fired, notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) and no web UI consumes the notification store, so it's not user-visible. Workflow FIRING is unaffected/correct (doesn't need the agent). FIX: have the driving processes construct the agent at startup (cron-driver-server: getMastra() so the provider connects; webhook-server: build foreman at startup) + a web consumer of the notification store / thread to surface it. Until then notify() is scaffolding, not delivered.","notes":"CONNECT FIX DONE (commit 177c281): cron-driver-server + webhook-server now call getMastra() at startup -\u003e foreman agent built in-process -\u003e providers connected (+ notification storage available). Live-verified: cron logs 'poll provider connected: true'. So notify() now FIRES (writes to the notification store) on poll/channel trigger hits. REMAINING (the user-visible half): a web consumer of the notification store / agent thread to surface the notification in chat — bundles with the deferred background-tasks /background-tasks/stream consumer. Also: notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) — revisit thread targeting when wiring the UI.","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T05:22:51Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:34:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"LIVE-VERIFIED (mastra dev). FINDING: backgrounding the read-only Zapier SDK tools HANGS mastra dev — enabling background injects a _background field into the tool input schema and Studio toJSONSchema over the Zapier zod schemas loops forever (same as lazy-init-zapier hang). mastra build is unaffected (no Studio). Bisected: no-flag binds :4111 in ~12s; flag-on-Zapier hangs. FIX (commit 0a20828): background search_history instead (simple 3-field schema) -\u003e dev binds in ~12s with flag on; /background-tasks/stream serves SSE 200 (manager live). lib/background.ts backgroundIfEnabled() gate, FOREMAN_BACKGROUND_TOOLS=1, OFF by default. REMAINING for full non-blocking UX (modest, search_history-only given the Zapier wall): waitTimeoutMs + web /background-tasks/stream consumer + end-to-end chat verify (LLM round-trip). Zapier tools blocked until alpha fixes toJSONSchema.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:05:59Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ueep","title":"[workflow] Live-verify the poll driver against a real Zapier read action","description":"poll-driver.ts (foreman-ld7s) is unit-tested only. Verify the real run path: bind a type=poll trigger to a real Zapier read action (e.g. gmail/new_email), run cron-driver-server, create a new record, assert exactly one workflow run fires and zero duplicates on re-poll; confirm baseline (first poll fires nothing) and intervalMinutes gating. Per verify-behavior-not-just-logic.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:35:12Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:12Z","dependencies":[{"issue_id":"foreman-ueep","depends_on_id":"foreman-ld7s","type":"discovered-from","created_at":"2026-06-11T17:35:12Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ld7s","title":"[workflow] Un-stub the poll trigger as a Foreman-native poll driver (tickPoll)","description":"Poll trigger type is documented in the migration and accepted by list/detach but hidden by attach-trigger and has no driver. Build src/workflows/poll-driver.ts mirroring cron-driver: read workflow_trigger type=poll, run the Zapier read action via runAction, diff results by config.dedupeKey field against last_dedupe_key cursor, fire executeWorkflow on new records, persist cursor. Un-stub attach_trigger (add poll + pollConfig). Wire into cron-driver-server. Rationale (explored alpha API hands-on): Mastra SignalProvider.poll() skips when zero in-memory subscriptions and notify() requires an in-process agent — it is built for provider-lives-in-agent/notify-thread, not a singleton worker firing DB workflows. SignalProvider will be adopted on the webhook/channel path instead. Keeps door open to Zapier watchTriggerInbox SSE later (iyq6).","status":"closed","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:27:10Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:07Z","started_at":"2026-06-12T00:27:27Z","closed_at":"2026-06-12T00:35:07Z","close_reason":"Poll driver implemented + unit-tested; full agents suite green (288). Live Zapier integration tracked as follow-up.","dependencies":[{"issue_id":"foreman-ld7s","depends_on_id":"foreman-c63f","type":"discovered-from","created_at":"2026-06-11T17:27:10Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/tests/live/poll-driver.test.ts b/packages/agents/tests/live/poll-driver.test.ts new file mode 100644 index 00000000..f60ab042 --- /dev/null +++ b/packages/agents/tests/live/poll-driver.test.ts @@ -0,0 +1,198 @@ +/** + * Live integration test for the poll driver (foreman-ueep). + * + * Exercises the full poll-trigger path against REAL local Supabase: + * attach_trigger(type='poll') → zapierPollProvider.runDuePolls() → + * executeWorkflow → workflow_run rows. + * + * The only thing stubbed is the external Zapier read (`runAction`): the local DB + * was reset and there's no OAuth-connected app to create records in, so we + * control what the "read" returns (and stub the workflow's write step so + * executeWorkflow completes). Everything else — cursor persistence, firing, + * dedup, workflow_run inserts with fired_by/trigger_id — is real Postgres. + * + * Auto-skips if Supabase isn't reachable. Run: npm run test:live + */ +import { RequestContext } from "@mastra/core/request-context"; +import { createClient } from "@supabase/supabase-js"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import type { Database } from "@/lib/db/database.types"; + +const SUPABASE_URL = process.env.SUPABASE_URL ?? "http://127.0.0.1:54421"; +const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY ?? ""; + +// Control the Zapier read the poll driver sees; stub the workflow's write step. +let readRecords: Array> = []; +vi.mock("@/lib/zapier/execution", () => ({ + runAction: vi.fn(async (_userId: string, _app: string, actionType: string) => + actionType === "read" ? { data: readRecords } : { ok: true }, + ), + rawFetch: vi.fn(), +})); + +async function supabaseIsReachable(): Promise { + try { + const res = await fetch(`${SUPABASE_URL}/rest/v1/`, { + headers: { apikey: SUPABASE_SERVICE_ROLE_KEY }, + signal: AbortSignal.timeout(2000), + }); + return res.status < 500; + } catch { + return false; + } +} + +function ctx({ userId, conversationId }: { userId: string; conversationId?: string }) { + const entries: [string, string][] = [["userId", userId]]; + if (conversationId) entries.push(["threadId", conversationId]); + return { requestContext: new RequestContext(entries) } as any; +} + +describe("poll driver — live Supabase (foreman-ueep)", () => { + let reachable = false; + let supabase: ReturnType>; + + const testRunId = `poll-${Date.now()}`; + const userId = `${testRunId}-user`; + const conversationId = `${testRunId}-conv`; + let workflowId = ""; + let triggerId = ""; + + async function runsForWorkflow() { + const { data } = await supabase.from("workflow_run").select("*").eq("workflow_id", workflowId); + return data ?? []; + } + async function triggerRow() { + const { data } = await supabase + .from("workflow_trigger") + .select("last_dedupe_key, last_fired_at") + .eq("id", triggerId) + .maybeSingle(); + return data; + } + + beforeAll(async () => { + reachable = await supabaseIsReachable(); + if (!reachable) { + console.warn(`\n⚠ Supabase not reachable at ${SUPABASE_URL}. Skipping live poll tests.\n`); + return; + } + supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); + const now = new Date().toISOString(); + + await supabase.from("user").insert({ + id: userId, + name: "live-poll-test", + email: `${testRunId}@test.local`, + emailVerified: true, + image: null, + createdAt: now, + updatedAt: now, + }); + await supabase.from("conversation").insert({ + id: conversationId, + user_id: userId, + mastra_thread_id: null, + title: null, + created_at: now, + updated_at: now, + }); + // An executed proposal → save_workflow turns it into a real workflow + step. + await supabase.from("action_proposal").insert({ + id: crypto.randomUUID(), + conversation_id: conversationId, + mastra_run_id: null, + app_key: "slack", + action_type: "write", + action_key: "send_message", + human_label: "slack.send_message", + inputs: JSON.stringify({ channel: "#ops", text: "poll fired" }), + input_schema: "{}", + connection_id: null, + status: "executed", + created_at: now, + updated_at: now, + }); + + const { saveWorkflowTool } = await import("@/mastra/tools/save-workflow"); + const saved = (await saveWorkflowTool.execute!( + { name: "Live poll workflow" } as any, + ctx({ userId, conversationId }), + )) as { workflowId: string }; + workflowId = saved.workflowId; + + const { attachTriggerTool } = await import("@/mastra/tools/attach-trigger"); + const attached = (await attachTriggerTool.execute!( + { + workflowId, + type: "poll", + poll: { app: "gmail", action: "new_email", dedupeKey: "id", intervalMinutes: 5 }, + } as any, + ctx({ userId }), + )) as { id: string; type: string }; + triggerId = attached.id; + expect(attached.type).toBe("poll"); + }); + + afterAll(async () => { + if (!reachable) return; + await supabase.from("workflow_run").delete().eq("workflow_id", workflowId); + await supabase.from("workflow_trigger").delete().eq("workflow_id", workflowId); + await supabase.from("workflow_step").delete().eq("workflow_id", workflowId); + await supabase.from("workflow").delete().eq("id", workflowId); + await supabase.from("action_proposal").delete().eq("conversation_id", conversationId); + await supabase.from("conversation").delete().eq("id", conversationId); + await supabase.from("user").delete().eq("id", userId); + }); + + it("persists the poll trigger config", async ({ skip }) => { + if (!reachable) skip(); + const { data } = await supabase + .from("workflow_trigger") + .select("type, config") + .eq("id", triggerId) + .maybeSingle(); + expect(data?.type).toBe("poll"); + expect(JSON.parse(data?.config as string)).toMatchObject({ + app: "gmail", + action: "new_email", + dedupeKey: "id", + }); + }); + + it("first poll establishes the baseline — fires nothing, sets the cursor", async ({ skip }) => { + if (!reachable) skip(); + readRecords = [{ id: "2" }, { id: "1" }]; + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-06-12T12:00:00Z")); + + expect(out.fired).toBe(0); + expect(await runsForWorkflow()).toHaveLength(0); + expect((await triggerRow())?.last_dedupe_key).toBe("2"); + }); + + it("fires the workflow once for a new record, recording fired_by=poll + trigger_id", async ({ + skip, + }) => { + if (!reachable) skip(); + readRecords = [{ id: "3" }, { id: "2" }, { id: "1" }]; // "3" is new + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-06-12T12:10:00Z")); + + expect(out.fired).toBe(1); + const runs = await runsForWorkflow(); + expect(runs).toHaveLength(1); + expect(runs[0]).toMatchObject({ fired_by: "poll", trigger_id: triggerId, status: "success" }); + expect((await triggerRow())?.last_dedupe_key).toBe("3"); + }); + + it("does not double-fire when no new record appears (dedup)", async ({ skip }) => { + if (!reachable) skip(); + readRecords = [{ id: "3" }, { id: "2" }]; // newest "3" == cursor + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-06-12T12:20:00Z")); + + expect(out.fired).toBe(0); + expect(await runsForWorkflow()).toHaveLength(1); // still just the one + }); +}); From 37e324d09bce607e21a87aab4e9b730194fb61f4 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Fri, 12 Jun 2026 09:17:38 -0700 Subject: [PATCH 13/19] fix(zapier): resolve app keys via the catalog, not string-munging (foreman-c8fo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalizeAppKey derived a slug from a CamelCase implementation name by inserting dashes at case boundaries, which is wrong for single-word names: GitHubCLIAPI -> git-hub (the real slug is github), while GoogleSheetsV2CLIAPI -> google-sheets genuinely needs the dash. You can't tell those apart from the string alone, so runAction({app:'git-hub'}) failed with 'No current_implementation_id'. (The agent normally passes catalog slugs so this mostly bit raw-key callers like a poll trigger configured with a connection's app_key.) The app_catalog table already stores both app_key (GitHubCLIAPI) and slug (github) from sdk.listApps, so it knows the correct mapping. New resolveAppSlug() looks up either form and returns the canonical slug; if the app isn't in the catalog (unseeded local DB), it returns the key UNCHANGED — the Zapier SDK accepts raw app_keys and slugs, only the mangled form ever broke. Wired into runAction (execution.ts) and workflow-step save (save.ts), replacing normalizeAppKey at both call sites. Verified live: with the raw 'GitHubCLIAPI' key (previously broken), the full poll fire now works against real Zapier + real Postgres — baseline sets cursor, a new record fires one workflow_run status=success. 5 unit tests; full suite green (308). Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 3 +- packages/agents/src/lib/catalog/index.ts | 1 + packages/agents/src/lib/catalog/resolve.ts | 37 +++++++++++++ packages/agents/src/lib/workflows/save.ts | 4 +- packages/agents/src/lib/zapier/execution.ts | 7 ++- .../tests/unit/resolve-app-slug.test.ts | 54 +++++++++++++++++++ 6 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 packages/agents/src/lib/catalog/resolve.ts create mode 100644 packages/agents/tests/unit/resolve-app-slug.test.ts diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 7e5e4a25..4490f9d1 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -94,7 +94,7 @@ {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-tbjb","title":"[signals] notify() is inert — driving processes don't connect the providers","description":"The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them don't reliably CONNECT them, so notify() is a no-op (provider.agent undefined -\u003e notifyOwner returns early). (1) cron-driver-server drives zapierPollProvider.runDuePolls() but never calls getMastra()/builds the foreman agent, so poll notify() never fires. (2) channel: slack/bot.ts builds the agent lazily inside generateReply, AFTER matchAndFireChannelTriggers, so the channel provider isn't connected for trigger-only messages until the process first runs generateReply. (3) even when fired, notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) and no web UI consumes the notification store, so it's not user-visible. Workflow FIRING is unaffected/correct (doesn't need the agent). FIX: have the driving processes construct the agent at startup (cron-driver-server: getMastra() so the provider connects; webhook-server: build foreman at startup) + a web consumer of the notification store / thread to surface it. Until then notify() is scaffolding, not delivered.","notes":"CONNECT FIX DONE (commit 177c281): cron-driver-server + webhook-server now call getMastra() at startup -\u003e foreman agent built in-process -\u003e providers connected (+ notification storage available). Live-verified: cron logs 'poll provider connected: true'. So notify() now FIRES (writes to the notification store) on poll/channel trigger hits. REMAINING (the user-visible half): a web consumer of the notification store / agent thread to surface the notification in chat — bundles with the deferred background-tasks /background-tasks/stream consumer. Also: notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) — revisit thread targeting when wiring the UI.","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T05:22:51Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:34:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"LIVE-VERIFIED (mastra dev). FINDING: backgrounding the read-only Zapier SDK tools HANGS mastra dev — enabling background injects a _background field into the tool input schema and Studio toJSONSchema over the Zapier zod schemas loops forever (same as lazy-init-zapier hang). mastra build is unaffected (no Studio). Bisected: no-flag binds :4111 in ~12s; flag-on-Zapier hangs. FIX (commit 0a20828): background search_history instead (simple 3-field schema) -\u003e dev binds in ~12s with flag on; /background-tasks/stream serves SSE 200 (manager live). lib/background.ts backgroundIfEnabled() gate, FOREMAN_BACKGROUND_TOOLS=1, OFF by default. REMAINING for full non-blocking UX (modest, search_history-only given the Zapier wall): waitTimeoutMs + web /background-tasks/stream consumer + end-to-end chat verify (LLM round-trip). Zapier tools blocked until alpha fixes toJSONSchema.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:05:59Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-ueep","title":"[workflow] Live-verify the poll driver against a real Zapier read action","description":"poll-driver.ts (foreman-ld7s) is unit-tested only. Verify the real run path: bind a type=poll trigger to a real Zapier read action (e.g. gmail/new_email), run cron-driver-server, create a new record, assert exactly one workflow run fires and zero duplicates on re-poll; confirm baseline (first poll fires nothing) and intervalMinutes gating. Per verify-behavior-not-just-logic.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:35:12Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:12Z","dependencies":[{"issue_id":"foreman-ueep","depends_on_id":"foreman-ld7s","type":"discovered-from","created_at":"2026-06-11T17:35:12Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-ueep","title":"[workflow] Live-verify the poll driver against a real Zapier read action","description":"poll-driver.ts (foreman-ld7s) is unit-tested only. Verify the real run path: bind a type=poll trigger to a real Zapier read action (e.g. gmail/new_email), run cron-driver-server, create a new record, assert exactly one workflow run fires and zero duplicates on re-poll; confirm baseline (first poll fires nothing) and intervalMinutes gating. Per verify-behavior-not-just-logic.","notes":"ORCHESTRATION LIVE-VERIFIED (commit 278246a): tests/live/poll-driver.test.ts, 4/4 green against real local Postgres. Proves attach_trigger(poll) -\u003e runDuePolls -\u003e executeWorkflow -\u003e real workflow_run row (fired_by=poll, trigger_id, status=success) + baseline (first poll no fire, cursor set) + dedup (no double-fire). Only the external Zapier read (runAction) is stubbed — DB was reset, no OAuth-connected app to create records in. REMAINING (thin, user-driven): smoke the literal real Zapier read by connecting an app (gmail) + creating a record while cron-driver-server runs; the SDK network call itself is covered by the tests/sdk live tier. The whole machinery around that call is now proven.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:35:12Z","created_by":"HamChowderr","updated_at":"2026-06-12T06:10:05Z","dependencies":[{"issue_id":"foreman-ueep","depends_on_id":"foreman-ld7s","type":"discovered-from","created_at":"2026-06-11T17:35:12Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ld7s","title":"[workflow] Un-stub the poll trigger as a Foreman-native poll driver (tickPoll)","description":"Poll trigger type is documented in the migration and accepted by list/detach but hidden by attach-trigger and has no driver. Build src/workflows/poll-driver.ts mirroring cron-driver: read workflow_trigger type=poll, run the Zapier read action via runAction, diff results by config.dedupeKey field against last_dedupe_key cursor, fire executeWorkflow on new records, persist cursor. Un-stub attach_trigger (add poll + pollConfig). Wire into cron-driver-server. Rationale (explored alpha API hands-on): Mastra SignalProvider.poll() skips when zero in-memory subscriptions and notify() requires an in-process agent — it is built for provider-lives-in-agent/notify-thread, not a singleton worker firing DB workflows. SignalProvider will be adopted on the webhook/channel path instead. Keeps door open to Zapier watchTriggerInbox SSE later (iyq6).","status":"closed","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:27:10Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:07Z","started_at":"2026-06-12T00:27:27Z","closed_at":"2026-06-12T00:35:07Z","close_reason":"Poll driver implemented + unit-tested; full agents suite green (288). Live Zapier integration tracked as follow-up.","dependencies":[{"issue_id":"foreman-ld7s","depends_on_id":"foreman-c63f","type":"discovered-from","created_at":"2026-06-11T17:27:10Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-22c3","title":"Evaluate Mastra signals/signal-providers/notifications vs Foreman's home-grown triggers","description":"Foreman is on @mastra/core 1.41 (post-bump). Mastra now ships native primitives that overlap with Foreman's hand-built trigger/workflow/notification system: (1) SignalProvider (poll OR webhook an external source -\u003e push notifications into agent threads) ~= Foreman's channel-trigger + cron-driver + the stubbed poll trigger (c63f); (2) built-in dedupeKey on notify() directly solves the dedup gap in tv5p (Foreman hand-rolls idempotency); (3) @mastra/github-signals is a production provider and Foreman already has a GitHub channel; (4) background-task suspend/resume for human approval + notification inbox ~= Foreman's proposal/approval flow. Sibling to iyq6 (Zapier-durable eval). Decide adopt-Mastra-native vs keep-own BEFORE building the v8k1 workflow epic. Confirmed code-side: backgroundTasks enabled in mastra/index.ts; pubsub is default in-process EventEmitter (no distributed backend — matters for 2-process + multi-instance); signals/signal-providers not used at all.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T22:33:47Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:33:47Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-rpsr","title":"Fix flaky db-types CI job — 'supabase gen types --local' intermittently errors 'Access token not provided'","description":"CI job 'db types fresh' (.github/workflows/test.yml) is flaky: passes on some commits (4dd20ff, 25b1ed1), fails on others (51e2065, f677794, b1070ae, 040b6a6) with 'Access token not provided' from 'npx supabase gen types typescript --local', producing empty output → diff shows 0a1,4128 (empty vs committed 4128-line database.types.ts). NOT a stale-types problem and NOT caused by the SDK/mastra bump. Root cause: supabase/setup-cli@v1 uses version:latest (unpinned), and the drifted CLI intermittently requires an access token even for --local. Fix options: (1) pin setup-cli to a known-good supabase version; (2) ensure 'supabase start' is fully healthy before gen; (3) set SUPABASE_ACCESS_TOKEN (project avoids secrets). Verify locally by starting Foreman's own supabase (note: a different project's supabase '*_Zillow' was running during diagnosis).","status":"closed","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-11T19:40:46Z","created_by":"HamChowderr","updated_at":"2026-06-11T20:18:52Z","closed_at":"2026-06-11T20:18:52Z","close_reason":"Fixed (379d9b4): added a placeholder SUPABASE_ACCESS_TOKEN to the CI db-types step. The latest supabase CLI demands a token's presence before local type-gen in Linux CI (value unused for --local). All 5 CI jobs green on main (run 27374657799). Also switched scripts to --local --workdir ../.. (cwd-independent) + documented the gotcha in CLAUDE.md.","dependencies":[{"issue_id":"foreman-rpsr","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T12:40:46Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -212,6 +212,7 @@ {"_type":"issue","id":"foreman-c9y","title":"Set up LibSQL + Drizzle ORM","description":"Single LibSQL database for app + Mastra data. Migrations runnable via script. AC: npm run db:migrate creates BetterAuth tables. Connection string in .env.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:48Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-s46","title":"Install and configure BetterAuth","description":"Email/password auth. Session stored in LibSQL. AC: /sign-up and /sign-in pages work. Authenticated session persists across reloads.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:46Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-5ld","title":"Initialize Next.js 15 project","description":"Scaffold Next.js 15 with App Router, TypeScript, Tailwind. AC: npm run dev serves a blank page at /. Project committed to git under C:\\Users\\HamCh\\code\\foreman.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:38Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-c8fo","title":"[zapier] Poll-trigger connection resolution: normalizeAppKey + numeric-id/alias vs SDK uuid mismatch","description":"Live e2e (foreman-ueep) surfaced this. A poll trigger config storing a raw Zapier app_key (e.g. 'GitHubCLIAPI') fails the real read: runAction does normalizeAppKey('GitHubCLIAPI')='github', then auto-discover findFirstConnection({app:'github'}) does NOT match the connection whose app_key is 'GitHubCLIAPI' -\u003e no connection -\u003e 'access_token missing'. Separately, resolveConnection expects a numeric connection_id or a registered connection_alias, but sdk.findFirstConnection returns a UUID id (e.g. 0206dbe8-...), so passing that id as config.connection throws 'Unknown connection alias'. Net: for a poll trigger to read for real, the app key must be the normalized form AND the user's connection must be discoverable/aliased under it. CONFIRMED WORKING in isolation: sdk.runAction({app:'GitHubCLIAPI', connection:'\u003cuuid\u003e'}) returns {data:[100 repos]} and the poll driver's extractRecords parses it. So this is connection-resolution plumbing, not poll-driver logic (logic proven by tests/live/poll-driver.test.ts). Decide: normalize app keys at attach time, store the resolved connection id, and/or make auto-discover tolerant of raw app_key suffixes.","notes":"RESOLVED / MISDIAGNOSED earlier. There is NO poll-driver or connection-model bug. Proven: the full real poll fire works end-to-end (real Postgres + real Zapier GitHub read) when the poll config uses the CATALOG app key 'github' — baseline sets cursor from the real read, a new record fires one workflow_run (status=success, fired_by=poll, trigger_id set), and dedup prevents a second. Earlier failures were entirely my test input: I used the raw Zapier app_key 'GitHubCLIAPI', and normalizeAppKey('GitHubCLIAPI')='git-hub' (dashed), which Zapier doesn't match. normalize/resolveConnection/findFirstConnection are all fine. RESIDUAL (latent, low priority, NOT poll-specific): normalizeAppKey turns the raw Zapier key 'GitHubCLIAPI' into 'git-hub' rather than 'github', so feeding a raw Zapier app_key into any runAction breaks. Only matters if a caller uses raw Zapier keys instead of catalog keys; the agent/catalog uses normalized keys. Reduce to P3 / hardening.","status":"open","priority":3,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T06:29:32Z","created_by":"HamChowderr","updated_at":"2026-06-12T15:53:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m9f6","title":"[channels] Thread real platform message ids into ChannelMessage.dedupeKey","description":"ChannelTriggerSignalProvider dedup falls back to a content hash (channel|room|from|text) when ChannelMessage.dedupeKey is absent, which coalesces a user sending the exact same text back-to-back. Pass each adapter's stable message id as dedupeKey from the ~10 channel-bot call sites (slack/discord/telegram/teams/whatsapp/gchat/github/linear onDirectMessage + onNewMention) where the Chat SDK message exposes one. Removes the false-positive edge; true per-delivery idempotency.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T03:31:07Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:31:07Z","dependencies":[{"issue_id":"foreman-m9f6","depends_on_id":"foreman-tv5p","type":"discovered-from","created_at":"2026-06-11T20:31:07Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-l8ev","title":"Document Zapier SDK auth model in README (client-creds vs userJwt per surface)","description":"Discovered during SDK exploration: the Zapier SDK surfaces need DIFFERENT auth. Trigger inboxes + discovery + runAction accept app-level client-credentials (ZAPIER_CLIENT_ID/SECRET). Durable-workflow endpoints (runDurable/publishWorkflowVersion) require a per-user userJwt (PKCE OAuth token), NOT client-creds. The new zapier-sdk-cli (0.54) login yields client-creds only; ~/.zapierrc deployKey is a legacy 32-char non-JWT (invalid as SDK creds). Document in README: which surfaces need which auth, how Foreman obtains each (env client-creds vs lib/zapier/sdk.ts PKCE per-user token), and the open question for Zapier (server-side path to per-user durable). Come back to this later. See docs/durable-workflow-spike.md + docs/trigger-inbox-spike.md.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T18:39:52Z","created_by":"HamChowderr","updated_at":"2026-06-11T18:39:52Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-z0kn","title":"Port provider-routing generate() test to a PG-enabled tier (storage injection)","description":"provider-swap.test.ts 'returns text' sub-test calls agent.generate() which now needs PostgresStore-backed Memory (foreman migrated LibSQL-\u003e@mastra/pg). It can't run in the no-external-services mocked tier (agents CI job has no PG), so it's it.skip'd. Proper fix: either inject a test storage into createForemanAgent (let tests use an in-memory/file store), or run this sub-test in a PG-enabled tier (test:live or a CI job with a postgres service). Discovered during the catch-up integration of claude/add-multiple-providers.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T16:40:19Z","created_by":"HamChowderr","updated_at":"2026-06-11T16:40:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/src/lib/catalog/index.ts b/packages/agents/src/lib/catalog/index.ts index b16824d0..ff138020 100644 --- a/packages/agents/src/lib/catalog/index.ts +++ b/packages/agents/src/lib/catalog/index.ts @@ -1,2 +1,3 @@ +export { resolveAppSlug } from "./resolve"; export { seedCatalog } from "./seed"; export { ensureCatalogIndex, indexAppCatalog, searchAppCatalog } from "./vector"; diff --git a/packages/agents/src/lib/catalog/resolve.ts b/packages/agents/src/lib/catalog/resolve.ts new file mode 100644 index 00000000..9ea77b09 --- /dev/null +++ b/packages/agents/src/lib/catalog/resolve.ts @@ -0,0 +1,37 @@ +import { getSupabase } from "../db"; + +/** + * Resolve any Zapier app identifier to its canonical slug, using the + * `app_catalog` table (seeded from `sdk.listApps`) as the source of truth. + * + * The catalog stores BOTH the Zapier `app_key` (e.g. "GitHubCLIAPI") and the + * display `slug` (e.g. "github"), so it knows the correct mapping. This replaces + * the string-munging `normalizeAppKey`, which can't reliably derive a slug from + * a CamelCase implementation name: "GitHubCLIAPI" → "git-hub" is wrong (the real + * slug is "github"), while "GoogleSheetsV2CLIAPI" → "google-sheets" needs the + * dash. You can't tell those apart from the string alone — only the catalog can. + * (foreman-c8fo) + * + * Accepts either form (app_key or slug, case-insensitive) and returns the slug. + * If the app isn't in the catalog (e.g. an unseeded local DB), the key is + * returned UNCHANGED — the Zapier SDK accepts both raw app_keys and slugs, so + * passing the original through is safe; only the mangled form ever broke. + */ +export async function resolveAppSlug(key: string): Promise { + if (!key) return key; + // App keys/slugs are simple identifiers; guard the PostgREST `or` filter. + if (!/^[\w.-]+$/.test(key)) return key; + + try { + const { data } = await getSupabase() + .from("app_catalog") + .select("slug") + .or(`app_key.ilike.${key},slug.ilike.${key}`) + .limit(1) + .maybeSingle(); + if (data?.slug) return data.slug; + } catch { + // catalog unavailable / not seeded — fall through to pass-through + } + return key; +} diff --git a/packages/agents/src/lib/workflows/save.ts b/packages/agents/src/lib/workflows/save.ts index 3ecfae54..8fd77fef 100644 --- a/packages/agents/src/lib/workflows/save.ts +++ b/packages/agents/src/lib/workflows/save.ts @@ -1,5 +1,5 @@ +import { resolveAppSlug } from "@/lib/catalog/resolve"; import { getSupabase } from "@/lib/db"; -import { normalizeAppKey } from "@/lib/zapier/normalize"; import { extractParams } from "./params"; /** Heuristic: values that look like they should become parameters. */ @@ -89,7 +89,7 @@ export async function saveWorkflowFromConversation( } const template = { - appKey: normalizeAppKey(p.app_key), + appKey: await resolveAppSlug(p.app_key), actionType: p.action_type, actionKey: p.action_key, humanLabel: p.human_label, diff --git a/packages/agents/src/lib/zapier/execution.ts b/packages/agents/src/lib/zapier/execution.ts index 7fdc64cb..6090f248 100644 --- a/packages/agents/src/lib/zapier/execution.ts +++ b/packages/agents/src/lib/zapier/execution.ts @@ -1,8 +1,8 @@ import { checkCapability } from "../capabilities"; +import { resolveAppSlug } from "../catalog/resolve"; import { runGuardrails } from "../guardrails"; import { resolveConnection } from "./aliases"; import { ZapierActionFailed, ZapierCapabilityDenied } from "./errors"; -import { normalizeAppKey } from "./normalize"; import { getSdkForUser } from "./sdk"; type ActionType = @@ -42,7 +42,10 @@ export async function runAction( inputs: Record, connection?: string, ) { - app = normalizeAppKey(app); + // Resolve to the canonical slug via the catalog (the source of truth). The SDK + // accepts both the raw app_key and the slug; only the old string-munged form + // ("GitHubCLIAPI" → "git-hub") broke. (foreman-c8fo) + app = await resolveAppSlug(app); const capability = ACTION_TYPE_CAPABILITY[actionType] ?? "execute"; await requireCapability(userId, capability); diff --git a/packages/agents/tests/unit/resolve-app-slug.test.ts b/packages/agents/tests/unit/resolve-app-slug.test.ts new file mode 100644 index 00000000..a95cd0d5 --- /dev/null +++ b/packages/agents/tests/unit/resolve-app-slug.test.ts @@ -0,0 +1,54 @@ +/** + * Unit tests for resolveAppSlug (foreman-c8fo) — catalog-backed app-key + * resolution that replaces the string-munging normalizeAppKey. + */ +import { afterEach, describe, expect, it, vi } from "vitest"; + +let catalogResult: { data: { slug: string } | null } = { data: null }; + +vi.mock("@/lib/db", () => ({ + getSupabase: () => ({ + from: () => { + const b: any = {}; + for (const m of ["select", "or", "limit"]) b[m] = vi.fn().mockReturnValue(b); + b.maybeSingle = vi.fn().mockImplementation(() => Promise.resolve(catalogResult)); + return b; + }, + }), +})); + +afterEach(() => { + catalogResult = { data: null }; +}); + +describe("resolveAppSlug", () => { + it("maps a raw app_key to its catalog slug", async () => { + catalogResult = { data: { slug: "github" } }; + const { resolveAppSlug } = await import("@/lib/catalog/resolve"); + expect(await resolveAppSlug("GitHubCLIAPI")).toBe("github"); + }); + + it("returns the slug unchanged when given a slug", async () => { + catalogResult = { data: { slug: "github" } }; + const { resolveAppSlug } = await import("@/lib/catalog/resolve"); + expect(await resolveAppSlug("github")).toBe("github"); + }); + + it("passes the key through unchanged when the catalog has no match (unseeded)", async () => { + catalogResult = { data: null }; + const { resolveAppSlug } = await import("@/lib/catalog/resolve"); + // The SDK accepts raw app_keys, so passing it through is safe — and crucially + // it does NOT mangle "GitHubCLIAPI" into the broken "git-hub". + expect(await resolveAppSlug("GitHubCLIAPI")).toBe("GitHubCLIAPI"); + }); + + it("passes through non-identifier input without querying", async () => { + const { resolveAppSlug } = await import("@/lib/catalog/resolve"); + expect(await resolveAppSlug("bad,key")).toBe("bad,key"); + }); + + it("handles an empty key", async () => { + const { resolveAppSlug } = await import("@/lib/catalog/resolve"); + expect(await resolveAppSlug("")).toBe(""); + }); +}); From ada01c9ea6f3da681a1f06a75424c672a7841c0a Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Fri, 12 Jun 2026 09:33:44 -0700 Subject: [PATCH 14/19] feat(web): surface run fired_by + error reason, fix failed-status badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent API returns fired_by/error_message/trigger_id (foreman-2afc) but the web ignored them, and failed runs rendered as 'running' — the engine writes status 'failed' while WorkflowRun.status + the StatusBadge map only knew 'error'. - WorkflowRun.status -> 'running'|'success'|'failed' + add fired_by/error_message/ trigger_id. - Run list: badge now resolves 'failed' (red); each run shows how it fired (manual/cron/channel/poll) + the error reason inline on failure. - Run detail: shows 'started by' + a red error box with the failure reason. next build passes (typecheck). First visible slice of surfacing the new backend (2afc run context) in the UI. Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 3 ++- packages/web/src/app/workflows/[id]/page.tsx | 10 +++++++++- .../web/src/app/workflows/[id]/runs/[runId]/page.tsx | 10 ++++++++++ packages/web/src/lib/workflows-client.ts | 8 +++++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 4490f9d1..3d0614c1 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,6 +92,7 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-hlh0","title":"[web] Surface run fired_by/error_message + fix failed-status badge","description":"The agent API (GET /workflows/:id/runs) now returns fired_by/error_message/trigger_id (foreman-2afc), but the web doesn't render them, and there's a pre-existing display bug: the engine writes status 'failed' but workflows-client WorkflowRun.status + the StatusBadge map only know 'error', so failed runs render as 'running'. Fix: (1) WorkflowRun.status -\u003e 'running'|'success'|'failed' + add fired_by/error_message/trigger_id; (2) [id]/page.tsx StatusBadge map key error-\u003efailed + show how each run fired; (3) run detail page show fired_by + error_message. First visible slice of the broader 'surface new backend in UI' work (foreman-tbjb/7am4/ezsm).","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T16:30:49Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:30:49Z","dependencies":[{"issue_id":"foreman-hlh0","depends_on_id":"foreman-2afc","type":"discovered-from","created_at":"2026-06-12T09:30:48Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-tbjb","title":"[signals] notify() is inert — driving processes don't connect the providers","description":"The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them don't reliably CONNECT them, so notify() is a no-op (provider.agent undefined -\u003e notifyOwner returns early). (1) cron-driver-server drives zapierPollProvider.runDuePolls() but never calls getMastra()/builds the foreman agent, so poll notify() never fires. (2) channel: slack/bot.ts builds the agent lazily inside generateReply, AFTER matchAndFireChannelTriggers, so the channel provider isn't connected for trigger-only messages until the process first runs generateReply. (3) even when fired, notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) and no web UI consumes the notification store, so it's not user-visible. Workflow FIRING is unaffected/correct (doesn't need the agent). FIX: have the driving processes construct the agent at startup (cron-driver-server: getMastra() so the provider connects; webhook-server: build foreman at startup) + a web consumer of the notification store / thread to surface it. Until then notify() is scaffolding, not delivered.","notes":"CONNECT FIX DONE (commit 177c281): cron-driver-server + webhook-server now call getMastra() at startup -\u003e foreman agent built in-process -\u003e providers connected (+ notification storage available). Live-verified: cron logs 'poll provider connected: true'. So notify() now FIRES (writes to the notification store) on poll/channel trigger hits. REMAINING (the user-visible half): a web consumer of the notification store / agent thread to surface the notification in chat — bundles with the deferred background-tasks /background-tasks/stream consumer. Also: notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) — revisit thread targeting when wiring the UI.","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T05:22:51Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:34:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"LIVE-VERIFIED (mastra dev). FINDING: backgrounding the read-only Zapier SDK tools HANGS mastra dev — enabling background injects a _background field into the tool input schema and Studio toJSONSchema over the Zapier zod schemas loops forever (same as lazy-init-zapier hang). mastra build is unaffected (no Studio). Bisected: no-flag binds :4111 in ~12s; flag-on-Zapier hangs. FIX (commit 0a20828): background search_history instead (simple 3-field schema) -\u003e dev binds in ~12s with flag on; /background-tasks/stream serves SSE 200 (manager live). lib/background.ts backgroundIfEnabled() gate, FOREMAN_BACKGROUND_TOOLS=1, OFF by default. REMAINING for full non-blocking UX (modest, search_history-only given the Zapier wall): waitTimeoutMs + web /background-tasks/stream consumer + end-to-end chat verify (LLM round-trip). Zapier tools blocked until alpha fixes toJSONSchema.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:05:59Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ueep","title":"[workflow] Live-verify the poll driver against a real Zapier read action","description":"poll-driver.ts (foreman-ld7s) is unit-tested only. Verify the real run path: bind a type=poll trigger to a real Zapier read action (e.g. gmail/new_email), run cron-driver-server, create a new record, assert exactly one workflow run fires and zero duplicates on re-poll; confirm baseline (first poll fires nothing) and intervalMinutes gating. Per verify-behavior-not-just-logic.","notes":"ORCHESTRATION LIVE-VERIFIED (commit 278246a): tests/live/poll-driver.test.ts, 4/4 green against real local Postgres. Proves attach_trigger(poll) -\u003e runDuePolls -\u003e executeWorkflow -\u003e real workflow_run row (fired_by=poll, trigger_id, status=success) + baseline (first poll no fire, cursor set) + dedup (no double-fire). Only the external Zapier read (runAction) is stubbed — DB was reset, no OAuth-connected app to create records in. REMAINING (thin, user-driven): smoke the literal real Zapier read by connecting an app (gmail) + creating a record while cron-driver-server runs; the SDK network call itself is covered by the tests/sdk live tier. The whole machinery around that call is now proven.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:35:12Z","created_by":"HamChowderr","updated_at":"2026-06-12T06:10:05Z","dependencies":[{"issue_id":"foreman-ueep","depends_on_id":"foreman-ld7s","type":"discovered-from","created_at":"2026-06-11T17:35:12Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -212,7 +213,7 @@ {"_type":"issue","id":"foreman-c9y","title":"Set up LibSQL + Drizzle ORM","description":"Single LibSQL database for app + Mastra data. Migrations runnable via script. AC: npm run db:migrate creates BetterAuth tables. Connection string in .env.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:48Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-s46","title":"Install and configure BetterAuth","description":"Email/password auth. Session stored in LibSQL. AC: /sign-up and /sign-in pages work. Authenticated session persists across reloads.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:46Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-5ld","title":"Initialize Next.js 15 project","description":"Scaffold Next.js 15 with App Router, TypeScript, Tailwind. AC: npm run dev serves a blank page at /. Project committed to git under C:\\Users\\HamCh\\code\\foreman.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:38Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-c8fo","title":"[zapier] Poll-trigger connection resolution: normalizeAppKey + numeric-id/alias vs SDK uuid mismatch","description":"Live e2e (foreman-ueep) surfaced this. A poll trigger config storing a raw Zapier app_key (e.g. 'GitHubCLIAPI') fails the real read: runAction does normalizeAppKey('GitHubCLIAPI')='github', then auto-discover findFirstConnection({app:'github'}) does NOT match the connection whose app_key is 'GitHubCLIAPI' -\u003e no connection -\u003e 'access_token missing'. Separately, resolveConnection expects a numeric connection_id or a registered connection_alias, but sdk.findFirstConnection returns a UUID id (e.g. 0206dbe8-...), so passing that id as config.connection throws 'Unknown connection alias'. Net: for a poll trigger to read for real, the app key must be the normalized form AND the user's connection must be discoverable/aliased under it. CONFIRMED WORKING in isolation: sdk.runAction({app:'GitHubCLIAPI', connection:'\u003cuuid\u003e'}) returns {data:[100 repos]} and the poll driver's extractRecords parses it. So this is connection-resolution plumbing, not poll-driver logic (logic proven by tests/live/poll-driver.test.ts). Decide: normalize app keys at attach time, store the resolved connection id, and/or make auto-discover tolerant of raw app_key suffixes.","notes":"RESOLVED / MISDIAGNOSED earlier. There is NO poll-driver or connection-model bug. Proven: the full real poll fire works end-to-end (real Postgres + real Zapier GitHub read) when the poll config uses the CATALOG app key 'github' — baseline sets cursor from the real read, a new record fires one workflow_run (status=success, fired_by=poll, trigger_id set), and dedup prevents a second. Earlier failures were entirely my test input: I used the raw Zapier app_key 'GitHubCLIAPI', and normalizeAppKey('GitHubCLIAPI')='git-hub' (dashed), which Zapier doesn't match. normalize/resolveConnection/findFirstConnection are all fine. RESIDUAL (latent, low priority, NOT poll-specific): normalizeAppKey turns the raw Zapier key 'GitHubCLIAPI' into 'git-hub' rather than 'github', so feeding a raw Zapier app_key into any runAction breaks. Only matters if a caller uses raw Zapier keys instead of catalog keys; the agent/catalog uses normalized keys. Reduce to P3 / hardening.","status":"open","priority":3,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T06:29:32Z","created_by":"HamChowderr","updated_at":"2026-06-12T15:53:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-c8fo","title":"[zapier] Poll-trigger connection resolution: normalizeAppKey + numeric-id/alias vs SDK uuid mismatch","description":"Live e2e (foreman-ueep) surfaced this. A poll trigger config storing a raw Zapier app_key (e.g. 'GitHubCLIAPI') fails the real read: runAction does normalizeAppKey('GitHubCLIAPI')='github', then auto-discover findFirstConnection({app:'github'}) does NOT match the connection whose app_key is 'GitHubCLIAPI' -\u003e no connection -\u003e 'access_token missing'. Separately, resolveConnection expects a numeric connection_id or a registered connection_alias, but sdk.findFirstConnection returns a UUID id (e.g. 0206dbe8-...), so passing that id as config.connection throws 'Unknown connection alias'. Net: for a poll trigger to read for real, the app key must be the normalized form AND the user's connection must be discoverable/aliased under it. CONFIRMED WORKING in isolation: sdk.runAction({app:'GitHubCLIAPI', connection:'\u003cuuid\u003e'}) returns {data:[100 repos]} and the poll driver's extractRecords parses it. So this is connection-resolution plumbing, not poll-driver logic (logic proven by tests/live/poll-driver.test.ts). Decide: normalize app keys at attach time, store the resolved connection id, and/or make auto-discover tolerant of raw app_key suffixes.","notes":"RESOLVED / MISDIAGNOSED earlier. There is NO poll-driver or connection-model bug. Proven: the full real poll fire works end-to-end (real Postgres + real Zapier GitHub read) when the poll config uses the CATALOG app key 'github' — baseline sets cursor from the real read, a new record fires one workflow_run (status=success, fired_by=poll, trigger_id set), and dedup prevents a second. Earlier failures were entirely my test input: I used the raw Zapier app_key 'GitHubCLIAPI', and normalizeAppKey('GitHubCLIAPI')='git-hub' (dashed), which Zapier doesn't match. normalize/resolveConnection/findFirstConnection are all fine. RESIDUAL (latent, low priority, NOT poll-specific): normalizeAppKey turns the raw Zapier key 'GitHubCLIAPI' into 'git-hub' rather than 'github', so feeding a raw Zapier app_key into any runAction breaks. Only matters if a caller uses raw Zapier keys instead of catalog keys; the agent/catalog uses normalized keys. Reduce to P3 / hardening.","status":"closed","priority":3,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T06:29:32Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:18:06Z","closed_at":"2026-06-12T16:18:06Z","close_reason":"FIXED (37e324d): added catalog-backed resolveAppSlug (app_key OR slug -\u003e canonical slug; pass-through unchanged when not in catalog), wired into runAction + workflow-step save, replacing the buggy normalizeAppKey at both call sites. Live-verified: the raw 'GitHubCLIAPI' key (previously broke with 'No current_implementation_id') now fires a real workflow_run (status=success) against real Zapier + real Postgres. 5 unit tests; full suite 308 green.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m9f6","title":"[channels] Thread real platform message ids into ChannelMessage.dedupeKey","description":"ChannelTriggerSignalProvider dedup falls back to a content hash (channel|room|from|text) when ChannelMessage.dedupeKey is absent, which coalesces a user sending the exact same text back-to-back. Pass each adapter's stable message id as dedupeKey from the ~10 channel-bot call sites (slack/discord/telegram/teams/whatsapp/gchat/github/linear onDirectMessage + onNewMention) where the Chat SDK message exposes one. Removes the false-positive edge; true per-delivery idempotency.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T03:31:07Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:31:07Z","dependencies":[{"issue_id":"foreman-m9f6","depends_on_id":"foreman-tv5p","type":"discovered-from","created_at":"2026-06-11T20:31:07Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-l8ev","title":"Document Zapier SDK auth model in README (client-creds vs userJwt per surface)","description":"Discovered during SDK exploration: the Zapier SDK surfaces need DIFFERENT auth. Trigger inboxes + discovery + runAction accept app-level client-credentials (ZAPIER_CLIENT_ID/SECRET). Durable-workflow endpoints (runDurable/publishWorkflowVersion) require a per-user userJwt (PKCE OAuth token), NOT client-creds. The new zapier-sdk-cli (0.54) login yields client-creds only; ~/.zapierrc deployKey is a legacy 32-char non-JWT (invalid as SDK creds). Document in README: which surfaces need which auth, how Foreman obtains each (env client-creds vs lib/zapier/sdk.ts PKCE per-user token), and the open question for Zapier (server-side path to per-user durable). Come back to this later. See docs/durable-workflow-spike.md + docs/trigger-inbox-spike.md.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T18:39:52Z","created_by":"HamChowderr","updated_at":"2026-06-11T18:39:52Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-z0kn","title":"Port provider-routing generate() test to a PG-enabled tier (storage injection)","description":"provider-swap.test.ts 'returns text' sub-test calls agent.generate() which now needs PostgresStore-backed Memory (foreman migrated LibSQL-\u003e@mastra/pg). It can't run in the no-external-services mocked tier (agents CI job has no PG), so it's it.skip'd. Proper fix: either inject a test storage into createForemanAgent (let tests use an in-memory/file store), or run this sub-test in a PG-enabled tier (test:live or a CI job with a postgres service). Discovered during the catch-up integration of claude/add-multiple-providers.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T16:40:19Z","created_by":"HamChowderr","updated_at":"2026-06-11T16:40:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/web/src/app/workflows/[id]/page.tsx b/packages/web/src/app/workflows/[id]/page.tsx index fbf3ccc3..8626ebda 100644 --- a/packages/web/src/app/workflows/[id]/page.tsx +++ b/packages/web/src/app/workflows/[id]/page.tsx @@ -109,6 +109,14 @@ export default async function WorkflowDetailPage({ params }: { params: Promise<{ {new Date(r.created_at).toLocaleString()} + + · {r.fired_by ?? "manual"} + + {r.status === "failed" && r.error_message ? ( + + — {r.error_message} + + ) : null} @@ -138,7 +146,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode function StatusBadge({ status }: { status: WorkflowRun["status"] }) { const map: Record = { success: { bg: "#E6F4EA", fg: "#1B6633", label: "succeeded" }, - error: { bg: "#FCE8E6", fg: "#A61B1B", label: "failed" }, + failed: { bg: "#FCE8E6", fg: "#A61B1B", label: "failed" }, running: { bg: "#FFF1E0", fg: "#7A4A1A", label: "running" }, }; const s = map[status] ?? map.running; diff --git a/packages/web/src/app/workflows/[id]/runs/[runId]/page.tsx b/packages/web/src/app/workflows/[id]/runs/[runId]/page.tsx index 2da66d37..8f59364b 100644 --- a/packages/web/src/app/workflows/[id]/runs/[runId]/page.tsx +++ b/packages/web/src/app/workflows/[id]/runs/[runId]/page.tsx @@ -41,6 +41,8 @@ export default async function RunDetailPage({

status: {run.status} + {" · started by: "} + {run.fired_by ?? "manual"} {duration != null ? ( <> {" · duration: "} @@ -48,6 +50,14 @@ export default async function RunDetailPage({ ) : null}

+ {run.status === "failed" && run.error_message ? ( +

+ {run.error_message} +

+ ) : null}
diff --git a/packages/web/src/lib/workflows-client.ts b/packages/web/src/lib/workflows-client.ts index 1124264c..f3c0e9eb 100644 --- a/packages/web/src/lib/workflows-client.ts +++ b/packages/web/src/lib/workflows-client.ts @@ -37,9 +37,15 @@ export interface WorkflowRun { id: string; workflow_id: string; inputs: Record; - status: "running" | "success" | "error"; + status: "running" | "success" | "failed"; created_at: string; completed_at: string | null; + /** How the run started: 'manual' | 'cron' | 'channel' | 'poll'. */ + fired_by: string | null; + /** Failure reason when status is 'failed'. */ + error_message: string | null; + /** The workflow_trigger that fired this run (null for manual runs). */ + trigger_id: string | null; } export interface WorkflowTrigger { From c942a9205a216af7d68408a63e757de6728a0a18 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Fri, 12 Jun 2026 09:46:19 -0700 Subject: [PATCH 15/19] feat(channels): dedupe channel triggers on real platform message ids (foreman-m9f6) Every channel bot (slack, discord, telegram, gchat, github, linear, teams, whatsapp) now passes the normalized Chat SDK message.id as ChannelMessage.dedupeKey at both call sites (DM + mention). The ChannelTriggerSignalProvider keys idempotency off this id instead of falling back to a content hash, so a retried webhook fires the workflow once while two legitimately-repeated identical messages keep distinct ids and both fire. Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 4 ++-- packages/agents/src/discord/bot.ts | 2 ++ packages/agents/src/gchat/bot.ts | 2 ++ packages/agents/src/github/bot.ts | 2 ++ packages/agents/src/linear/bot.ts | 2 ++ packages/agents/src/slack/bot.ts | 2 ++ packages/agents/src/teams/bot.ts | 2 ++ packages/agents/src/telegram/bot.ts | 2 ++ packages/agents/src/whatsapp/bot.ts | 2 ++ packages/agents/src/workflows/channel-trigger.ts | 10 ++++++---- 10 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 3d0614c1..058c30dd 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,7 +92,7 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-hlh0","title":"[web] Surface run fired_by/error_message + fix failed-status badge","description":"The agent API (GET /workflows/:id/runs) now returns fired_by/error_message/trigger_id (foreman-2afc), but the web doesn't render them, and there's a pre-existing display bug: the engine writes status 'failed' but workflows-client WorkflowRun.status + the StatusBadge map only know 'error', so failed runs render as 'running'. Fix: (1) WorkflowRun.status -\u003e 'running'|'success'|'failed' + add fired_by/error_message/trigger_id; (2) [id]/page.tsx StatusBadge map key error-\u003efailed + show how each run fired; (3) run detail page show fired_by + error_message. First visible slice of the broader 'surface new backend in UI' work (foreman-tbjb/7am4/ezsm).","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T16:30:49Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:30:49Z","dependencies":[{"issue_id":"foreman-hlh0","depends_on_id":"foreman-2afc","type":"discovered-from","created_at":"2026-06-12T09:30:48Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-hlh0","title":"[web] Surface run fired_by/error_message + fix failed-status badge","description":"The agent API (GET /workflows/:id/runs) now returns fired_by/error_message/trigger_id (foreman-2afc), but the web doesn't render them, and there's a pre-existing display bug: the engine writes status 'failed' but workflows-client WorkflowRun.status + the StatusBadge map only know 'error', so failed runs render as 'running'. Fix: (1) WorkflowRun.status -\u003e 'running'|'success'|'failed' + add fired_by/error_message/trigger_id; (2) [id]/page.tsx StatusBadge map key error-\u003efailed + show how each run fired; (3) run detail page show fired_by + error_message. First visible slice of the broader 'surface new backend in UI' work (foreman-tbjb/7am4/ezsm).","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T16:30:49Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:34:48Z","closed_at":"2026-06-12T16:34:48Z","close_reason":"Done (ada01c9): WorkflowRun type gains status=failed + fired_by/error_message/trigger_id; run list shows how each run fired + inline error on failure; run detail shows 'started by' + an error box. Fixed the pre-existing bug where failed runs rendered as 'running' (engine writes 'failed', web only knew 'error'). next build green.","dependencies":[{"issue_id":"foreman-hlh0","depends_on_id":"foreman-2afc","type":"discovered-from","created_at":"2026-06-12T09:30:48Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-tbjb","title":"[signals] notify() is inert — driving processes don't connect the providers","description":"The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them don't reliably CONNECT them, so notify() is a no-op (provider.agent undefined -\u003e notifyOwner returns early). (1) cron-driver-server drives zapierPollProvider.runDuePolls() but never calls getMastra()/builds the foreman agent, so poll notify() never fires. (2) channel: slack/bot.ts builds the agent lazily inside generateReply, AFTER matchAndFireChannelTriggers, so the channel provider isn't connected for trigger-only messages until the process first runs generateReply. (3) even when fired, notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) and no web UI consumes the notification store, so it's not user-visible. Workflow FIRING is unaffected/correct (doesn't need the agent). FIX: have the driving processes construct the agent at startup (cron-driver-server: getMastra() so the provider connects; webhook-server: build foreman at startup) + a web consumer of the notification store / thread to surface it. Until then notify() is scaffolding, not delivered.","notes":"CONNECT FIX DONE (commit 177c281): cron-driver-server + webhook-server now call getMastra() at startup -\u003e foreman agent built in-process -\u003e providers connected (+ notification storage available). Live-verified: cron logs 'poll provider connected: true'. So notify() now FIRES (writes to the notification store) on poll/channel trigger hits. REMAINING (the user-visible half): a web consumer of the notification store / agent thread to surface the notification in chat — bundles with the deferred background-tasks /background-tasks/stream consumer. Also: notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) — revisit thread targeting when wiring the UI.","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T05:22:51Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:34:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"LIVE-VERIFIED (mastra dev). FINDING: backgrounding the read-only Zapier SDK tools HANGS mastra dev — enabling background injects a _background field into the tool input schema and Studio toJSONSchema over the Zapier zod schemas loops forever (same as lazy-init-zapier hang). mastra build is unaffected (no Studio). Bisected: no-flag binds :4111 in ~12s; flag-on-Zapier hangs. FIX (commit 0a20828): background search_history instead (simple 3-field schema) -\u003e dev binds in ~12s with flag on; /background-tasks/stream serves SSE 200 (manager live). lib/background.ts backgroundIfEnabled() gate, FOREMAN_BACKGROUND_TOOLS=1, OFF by default. REMAINING for full non-blocking UX (modest, search_history-only given the Zapier wall): waitTimeoutMs + web /background-tasks/stream consumer + end-to-end chat verify (LLM round-trip). Zapier tools blocked until alpha fixes toJSONSchema.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:05:59Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ueep","title":"[workflow] Live-verify the poll driver against a real Zapier read action","description":"poll-driver.ts (foreman-ld7s) is unit-tested only. Verify the real run path: bind a type=poll trigger to a real Zapier read action (e.g. gmail/new_email), run cron-driver-server, create a new record, assert exactly one workflow run fires and zero duplicates on re-poll; confirm baseline (first poll fires nothing) and intervalMinutes gating. Per verify-behavior-not-just-logic.","notes":"ORCHESTRATION LIVE-VERIFIED (commit 278246a): tests/live/poll-driver.test.ts, 4/4 green against real local Postgres. Proves attach_trigger(poll) -\u003e runDuePolls -\u003e executeWorkflow -\u003e real workflow_run row (fired_by=poll, trigger_id, status=success) + baseline (first poll no fire, cursor set) + dedup (no double-fire). Only the external Zapier read (runAction) is stubbed — DB was reset, no OAuth-connected app to create records in. REMAINING (thin, user-driven): smoke the literal real Zapier read by connecting an app (gmail) + creating a record while cron-driver-server runs; the SDK network call itself is covered by the tests/sdk live tier. The whole machinery around that call is now proven.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:35:12Z","created_by":"HamChowderr","updated_at":"2026-06-12T06:10:05Z","dependencies":[{"issue_id":"foreman-ueep","depends_on_id":"foreman-ld7s","type":"discovered-from","created_at":"2026-06-11T17:35:12Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -214,7 +214,7 @@ {"_type":"issue","id":"foreman-s46","title":"Install and configure BetterAuth","description":"Email/password auth. Session stored in LibSQL. AC: /sign-up and /sign-in pages work. Authenticated session persists across reloads.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:46Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-5ld","title":"Initialize Next.js 15 project","description":"Scaffold Next.js 15 with App Router, TypeScript, Tailwind. AC: npm run dev serves a blank page at /. Project committed to git under C:\\Users\\HamCh\\code\\foreman.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:38Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-c8fo","title":"[zapier] Poll-trigger connection resolution: normalizeAppKey + numeric-id/alias vs SDK uuid mismatch","description":"Live e2e (foreman-ueep) surfaced this. A poll trigger config storing a raw Zapier app_key (e.g. 'GitHubCLIAPI') fails the real read: runAction does normalizeAppKey('GitHubCLIAPI')='github', then auto-discover findFirstConnection({app:'github'}) does NOT match the connection whose app_key is 'GitHubCLIAPI' -\u003e no connection -\u003e 'access_token missing'. Separately, resolveConnection expects a numeric connection_id or a registered connection_alias, but sdk.findFirstConnection returns a UUID id (e.g. 0206dbe8-...), so passing that id as config.connection throws 'Unknown connection alias'. Net: for a poll trigger to read for real, the app key must be the normalized form AND the user's connection must be discoverable/aliased under it. CONFIRMED WORKING in isolation: sdk.runAction({app:'GitHubCLIAPI', connection:'\u003cuuid\u003e'}) returns {data:[100 repos]} and the poll driver's extractRecords parses it. So this is connection-resolution plumbing, not poll-driver logic (logic proven by tests/live/poll-driver.test.ts). Decide: normalize app keys at attach time, store the resolved connection id, and/or make auto-discover tolerant of raw app_key suffixes.","notes":"RESOLVED / MISDIAGNOSED earlier. There is NO poll-driver or connection-model bug. Proven: the full real poll fire works end-to-end (real Postgres + real Zapier GitHub read) when the poll config uses the CATALOG app key 'github' — baseline sets cursor from the real read, a new record fires one workflow_run (status=success, fired_by=poll, trigger_id set), and dedup prevents a second. Earlier failures were entirely my test input: I used the raw Zapier app_key 'GitHubCLIAPI', and normalizeAppKey('GitHubCLIAPI')='git-hub' (dashed), which Zapier doesn't match. normalize/resolveConnection/findFirstConnection are all fine. RESIDUAL (latent, low priority, NOT poll-specific): normalizeAppKey turns the raw Zapier key 'GitHubCLIAPI' into 'git-hub' rather than 'github', so feeding a raw Zapier app_key into any runAction breaks. Only matters if a caller uses raw Zapier keys instead of catalog keys; the agent/catalog uses normalized keys. Reduce to P3 / hardening.","status":"closed","priority":3,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T06:29:32Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:18:06Z","closed_at":"2026-06-12T16:18:06Z","close_reason":"FIXED (37e324d): added catalog-backed resolveAppSlug (app_key OR slug -\u003e canonical slug; pass-through unchanged when not in catalog), wired into runAction + workflow-step save, replacing the buggy normalizeAppKey at both call sites. Live-verified: the raw 'GitHubCLIAPI' key (previously broke with 'No current_implementation_id') now fires a real workflow_run (status=success) against real Zapier + real Postgres. 5 unit tests; full suite 308 green.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-m9f6","title":"[channels] Thread real platform message ids into ChannelMessage.dedupeKey","description":"ChannelTriggerSignalProvider dedup falls back to a content hash (channel|room|from|text) when ChannelMessage.dedupeKey is absent, which coalesces a user sending the exact same text back-to-back. Pass each adapter's stable message id as dedupeKey from the ~10 channel-bot call sites (slack/discord/telegram/teams/whatsapp/gchat/github/linear onDirectMessage + onNewMention) where the Chat SDK message exposes one. Removes the false-positive edge; true per-delivery idempotency.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T03:31:07Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:31:07Z","dependencies":[{"issue_id":"foreman-m9f6","depends_on_id":"foreman-tv5p","type":"discovered-from","created_at":"2026-06-11T20:31:07Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-m9f6","title":"[channels] Thread real platform message ids into ChannelMessage.dedupeKey","description":"ChannelTriggerSignalProvider dedup falls back to a content hash (channel|room|from|text) when ChannelMessage.dedupeKey is absent, which coalesces a user sending the exact same text back-to-back. Pass each adapter's stable message id as dedupeKey from the ~10 channel-bot call sites (slack/discord/telegram/teams/whatsapp/gchat/github/linear onDirectMessage + onNewMention) where the Chat SDK message exposes one. Removes the false-positive edge; true per-delivery idempotency.","status":"in_progress","priority":3,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T03:31:07Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:39:12Z","started_at":"2026-06-12T16:39:12Z","dependencies":[{"issue_id":"foreman-m9f6","depends_on_id":"foreman-tv5p","type":"discovered-from","created_at":"2026-06-11T20:31:07Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-l8ev","title":"Document Zapier SDK auth model in README (client-creds vs userJwt per surface)","description":"Discovered during SDK exploration: the Zapier SDK surfaces need DIFFERENT auth. Trigger inboxes + discovery + runAction accept app-level client-credentials (ZAPIER_CLIENT_ID/SECRET). Durable-workflow endpoints (runDurable/publishWorkflowVersion) require a per-user userJwt (PKCE OAuth token), NOT client-creds. The new zapier-sdk-cli (0.54) login yields client-creds only; ~/.zapierrc deployKey is a legacy 32-char non-JWT (invalid as SDK creds). Document in README: which surfaces need which auth, how Foreman obtains each (env client-creds vs lib/zapier/sdk.ts PKCE per-user token), and the open question for Zapier (server-side path to per-user durable). Come back to this later. See docs/durable-workflow-spike.md + docs/trigger-inbox-spike.md.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T18:39:52Z","created_by":"HamChowderr","updated_at":"2026-06-11T18:39:52Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-z0kn","title":"Port provider-routing generate() test to a PG-enabled tier (storage injection)","description":"provider-swap.test.ts 'returns text' sub-test calls agent.generate() which now needs PostgresStore-backed Memory (foreman migrated LibSQL-\u003e@mastra/pg). It can't run in the no-external-services mocked tier (agents CI job has no PG), so it's it.skip'd. Proper fix: either inject a test storage into createForemanAgent (let tests use an in-memory/file store), or run this sub-test in a PG-enabled tier (test:live or a CI job with a postgres service). Discovered during the catch-up integration of claude/add-multiple-providers.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T16:40:19Z","created_by":"HamChowderr","updated_at":"2026-06-11T16:40:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-elyi","title":"[bump] Surface ZapierApprovalError approval URL to the user (handleSdkError)","description":"Under SDK \u003e=0.50 with approvalMode unset, a server (non-TTY) defaults to 'throw': sdk.runAction throws ZapierApprovalError carrying an approval URL on approval-gated responses. Foreman's handleSdkError (zapier-sdk-tools.ts) currently catches it only via the generic ZapierError catch-all, surfacing .message/.code but DROPPING the approval URL. Add an explicit 'if (err instanceof ZapierApprovalError)' branch that returns the approval URL + approvalStatus so the agent can tell the user how to approve. Non-blocking for the bump (degrades gracefully today). Discovered during foreman-2xdk verification.","status":"closed","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T15:10:57Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:39:56Z","closed_at":"2026-06-11T22:39:56Z","close_reason":"Done: handleSdkError now surfaces ZapierApprovalError.approvalUrl (code APPROVAL_REQUIRED), placed before the generic ZapierError catch-all; handleSdkError exported + 2 unit tests (with/without URL) pass; lint clean. Verified approvalUrl field exists in SDK 0.69.3.","dependencies":[{"issue_id":"foreman-elyi","depends_on_id":"foreman-2xdk","type":"discovered-from","created_at":"2026-06-11T08:10:57Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/agents/src/discord/bot.ts b/packages/agents/src/discord/bot.ts index 056c8e23..e2468a40 100644 --- a/packages/agents/src/discord/bot.ts +++ b/packages/agents/src/discord/bot.ts @@ -123,6 +123,7 @@ export async function getDiscordBot() { channel: "discord", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -144,6 +145,7 @@ export async function getDiscordBot() { channel: "discord", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/gchat/bot.ts b/packages/agents/src/gchat/bot.ts index 79ba6075..f1e413b6 100644 --- a/packages/agents/src/gchat/bot.ts +++ b/packages/agents/src/gchat/bot.ts @@ -62,6 +62,7 @@ export async function getGoogleChatBot() { channel: "gchat", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -88,6 +89,7 @@ export async function getGoogleChatBot() { channel: "gchat", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/github/bot.ts b/packages/agents/src/github/bot.ts index 120d9cd5..ede14729 100644 --- a/packages/agents/src/github/bot.ts +++ b/packages/agents/src/github/bot.ts @@ -65,6 +65,7 @@ export async function getGitHubBot() { channel: "github", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -91,6 +92,7 @@ export async function getGitHubBot() { channel: "github", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/linear/bot.ts b/packages/agents/src/linear/bot.ts index 73e62491..e2c44108 100644 --- a/packages/agents/src/linear/bot.ts +++ b/packages/agents/src/linear/bot.ts @@ -63,6 +63,7 @@ export async function getLinearBot() { channel: "linear", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -89,6 +90,7 @@ export async function getLinearBot() { channel: "linear", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/slack/bot.ts b/packages/agents/src/slack/bot.ts index d88918c3..41b1ad43 100644 --- a/packages/agents/src/slack/bot.ts +++ b/packages/agents/src/slack/bot.ts @@ -101,6 +101,7 @@ async function _createAndInitBot() { channel: "slack", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -128,6 +129,7 @@ async function _createAndInitBot() { channel: "slack", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/teams/bot.ts b/packages/agents/src/teams/bot.ts index 92d1ccca..ab010a00 100644 --- a/packages/agents/src/teams/bot.ts +++ b/packages/agents/src/teams/bot.ts @@ -62,6 +62,7 @@ export async function getTeamsBot() { channel: "teams", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -88,6 +89,7 @@ export async function getTeamsBot() { channel: "teams", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/telegram/bot.ts b/packages/agents/src/telegram/bot.ts index 76c2b6be..63f463b0 100644 --- a/packages/agents/src/telegram/bot.ts +++ b/packages/agents/src/telegram/bot.ts @@ -92,6 +92,7 @@ export async function getTelegramBot() { channel: "telegram", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -112,6 +113,7 @@ export async function getTelegramBot() { channel: "telegram", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/whatsapp/bot.ts b/packages/agents/src/whatsapp/bot.ts index 777e96b1..dec0c730 100644 --- a/packages/agents/src/whatsapp/bot.ts +++ b/packages/agents/src/whatsapp/bot.ts @@ -63,6 +63,7 @@ export async function getWhatsAppBot() { channel: "whatsapp", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -90,6 +91,7 @@ export async function getWhatsAppBot() { channel: "whatsapp", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/workflows/channel-trigger.ts b/packages/agents/src/workflows/channel-trigger.ts index 65c89022..6280b93a 100644 --- a/packages/agents/src/workflows/channel-trigger.ts +++ b/packages/agents/src/workflows/channel-trigger.ts @@ -27,10 +27,12 @@ export interface ChannelMessage { /** Channel/room/DM target — used by `match.room`. */ room?: string; /** - * Stable per-delivery key for idempotency (e.g. the platform message id). - * When omitted, a content hash is used. A retried webhook carries the same - * key, so the workflow fires once. Callers should pass a real message id when - * available to avoid coalescing legitimately-repeated identical messages. + * Stable per-delivery key for idempotency (the platform message id). Every + * channel bot passes `message.id` (the normalized Chat SDK `Message.id`), so + * a retried webhook carries the same key and the workflow fires once — while + * two legitimately-repeated identical messages keep distinct ids and both + * fire. When omitted, the provider falls back to a content hash (which would + * coalesce repeated identical text). */ dedupeKey?: string; } From 3e8a5ec595e34fed8cc982abc91894ee3f890b32 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Fri, 12 Jun 2026 10:14:45 -0700 Subject: [PATCH 16/19] fix(web): use npm ci for Vercel install to match the green CI path (foreman-sg23) PR #17 Vercel preview build failed at buildStep with module_not_found while all GHA gating jobs (web next-build via npm ci) are green on the same commit and a clean local build passes. npm ci wipes node_modules and installs exactly from the committed lockfile, neutralizing both install re-resolution and any stale cached node_modules on a fresh-branch preview. Non-gating; does not affect the merge decision. Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 3 ++- vercel.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 058c30dd..e3193407 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,6 +92,7 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-sg23","title":"Vercel preview build fails (module_not_found) on alpha branch — align install with CI","description":"PR #17 Vercel preview deploy errors at buildStep with errorCode module_not_found ('cd packages/web \u0026\u0026 npm run build' exit 1), while all 5 GHA gating jobs (incl. web next build via npm ci) are GREEN on the same commit, and a clean local build passes. Install succeeds; .source self-regenerates via withMDX so it is not the cause. Build-log API is 403 (CLI token lacks build-log scope), so the exact module is dashboard-only. Divergence: Vercel installCommand 'npm install' (+ possible stale cached node_modules on a fresh branch) vs GHA 'npm ci'. Non-gating (main production deploys are Ready), does not block merge. Fix: set vercel.json installCommand to 'npm ci' to force a clean lockfile-exact install matching the proven-green CI path.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T17:14:15Z","created_by":"HamChowderr","updated_at":"2026-06-12T17:14:36Z","started_at":"2026-06-12T17:14:36Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-hlh0","title":"[web] Surface run fired_by/error_message + fix failed-status badge","description":"The agent API (GET /workflows/:id/runs) now returns fired_by/error_message/trigger_id (foreman-2afc), but the web doesn't render them, and there's a pre-existing display bug: the engine writes status 'failed' but workflows-client WorkflowRun.status + the StatusBadge map only know 'error', so failed runs render as 'running'. Fix: (1) WorkflowRun.status -\u003e 'running'|'success'|'failed' + add fired_by/error_message/trigger_id; (2) [id]/page.tsx StatusBadge map key error-\u003efailed + show how each run fired; (3) run detail page show fired_by + error_message. First visible slice of the broader 'surface new backend in UI' work (foreman-tbjb/7am4/ezsm).","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T16:30:49Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:34:48Z","closed_at":"2026-06-12T16:34:48Z","close_reason":"Done (ada01c9): WorkflowRun type gains status=failed + fired_by/error_message/trigger_id; run list shows how each run fired + inline error on failure; run detail shows 'started by' + an error box. Fixed the pre-existing bug where failed runs rendered as 'running' (engine writes 'failed', web only knew 'error'). next build green.","dependencies":[{"issue_id":"foreman-hlh0","depends_on_id":"foreman-2afc","type":"discovered-from","created_at":"2026-06-12T09:30:48Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-tbjb","title":"[signals] notify() is inert — driving processes don't connect the providers","description":"The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them don't reliably CONNECT them, so notify() is a no-op (provider.agent undefined -\u003e notifyOwner returns early). (1) cron-driver-server drives zapierPollProvider.runDuePolls() but never calls getMastra()/builds the foreman agent, so poll notify() never fires. (2) channel: slack/bot.ts builds the agent lazily inside generateReply, AFTER matchAndFireChannelTriggers, so the channel provider isn't connected for trigger-only messages until the process first runs generateReply. (3) even when fired, notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) and no web UI consumes the notification store, so it's not user-visible. Workflow FIRING is unaffected/correct (doesn't need the agent). FIX: have the driving processes construct the agent at startup (cron-driver-server: getMastra() so the provider connects; webhook-server: build foreman at startup) + a web consumer of the notification store / thread to surface it. Until then notify() is scaffolding, not delivered.","notes":"CONNECT FIX DONE (commit 177c281): cron-driver-server + webhook-server now call getMastra() at startup -\u003e foreman agent built in-process -\u003e providers connected (+ notification storage available). Live-verified: cron logs 'poll provider connected: true'. So notify() now FIRES (writes to the notification store) on poll/channel trigger hits. REMAINING (the user-visible half): a web consumer of the notification store / agent thread to surface the notification in chat — bundles with the deferred background-tasks /background-tasks/stream consumer. Also: notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) — revisit thread targeting when wiring the UI.","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T05:22:51Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:34:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"LIVE-VERIFIED (mastra dev). FINDING: backgrounding the read-only Zapier SDK tools HANGS mastra dev — enabling background injects a _background field into the tool input schema and Studio toJSONSchema over the Zapier zod schemas loops forever (same as lazy-init-zapier hang). mastra build is unaffected (no Studio). Bisected: no-flag binds :4111 in ~12s; flag-on-Zapier hangs. FIX (commit 0a20828): background search_history instead (simple 3-field schema) -\u003e dev binds in ~12s with flag on; /background-tasks/stream serves SSE 200 (manager live). lib/background.ts backgroundIfEnabled() gate, FOREMAN_BACKGROUND_TOOLS=1, OFF by default. REMAINING for full non-blocking UX (modest, search_history-only given the Zapier wall): waitTimeoutMs + web /background-tasks/stream consumer + end-to-end chat verify (LLM round-trip). Zapier tools blocked until alpha fixes toJSONSchema.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:05:59Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -214,7 +215,7 @@ {"_type":"issue","id":"foreman-s46","title":"Install and configure BetterAuth","description":"Email/password auth. Session stored in LibSQL. AC: /sign-up and /sign-in pages work. Authenticated session persists across reloads.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:46Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-5ld","title":"Initialize Next.js 15 project","description":"Scaffold Next.js 15 with App Router, TypeScript, Tailwind. AC: npm run dev serves a blank page at /. Project committed to git under C:\\Users\\HamCh\\code\\foreman.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:38Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-c8fo","title":"[zapier] Poll-trigger connection resolution: normalizeAppKey + numeric-id/alias vs SDK uuid mismatch","description":"Live e2e (foreman-ueep) surfaced this. A poll trigger config storing a raw Zapier app_key (e.g. 'GitHubCLIAPI') fails the real read: runAction does normalizeAppKey('GitHubCLIAPI')='github', then auto-discover findFirstConnection({app:'github'}) does NOT match the connection whose app_key is 'GitHubCLIAPI' -\u003e no connection -\u003e 'access_token missing'. Separately, resolveConnection expects a numeric connection_id or a registered connection_alias, but sdk.findFirstConnection returns a UUID id (e.g. 0206dbe8-...), so passing that id as config.connection throws 'Unknown connection alias'. Net: for a poll trigger to read for real, the app key must be the normalized form AND the user's connection must be discoverable/aliased under it. CONFIRMED WORKING in isolation: sdk.runAction({app:'GitHubCLIAPI', connection:'\u003cuuid\u003e'}) returns {data:[100 repos]} and the poll driver's extractRecords parses it. So this is connection-resolution plumbing, not poll-driver logic (logic proven by tests/live/poll-driver.test.ts). Decide: normalize app keys at attach time, store the resolved connection id, and/or make auto-discover tolerant of raw app_key suffixes.","notes":"RESOLVED / MISDIAGNOSED earlier. There is NO poll-driver or connection-model bug. Proven: the full real poll fire works end-to-end (real Postgres + real Zapier GitHub read) when the poll config uses the CATALOG app key 'github' — baseline sets cursor from the real read, a new record fires one workflow_run (status=success, fired_by=poll, trigger_id set), and dedup prevents a second. Earlier failures were entirely my test input: I used the raw Zapier app_key 'GitHubCLIAPI', and normalizeAppKey('GitHubCLIAPI')='git-hub' (dashed), which Zapier doesn't match. normalize/resolveConnection/findFirstConnection are all fine. RESIDUAL (latent, low priority, NOT poll-specific): normalizeAppKey turns the raw Zapier key 'GitHubCLIAPI' into 'git-hub' rather than 'github', so feeding a raw Zapier app_key into any runAction breaks. Only matters if a caller uses raw Zapier keys instead of catalog keys; the agent/catalog uses normalized keys. Reduce to P3 / hardening.","status":"closed","priority":3,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T06:29:32Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:18:06Z","closed_at":"2026-06-12T16:18:06Z","close_reason":"FIXED (37e324d): added catalog-backed resolveAppSlug (app_key OR slug -\u003e canonical slug; pass-through unchanged when not in catalog), wired into runAction + workflow-step save, replacing the buggy normalizeAppKey at both call sites. Live-verified: the raw 'GitHubCLIAPI' key (previously broke with 'No current_implementation_id') now fires a real workflow_run (status=success) against real Zapier + real Postgres. 5 unit tests; full suite 308 green.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-m9f6","title":"[channels] Thread real platform message ids into ChannelMessage.dedupeKey","description":"ChannelTriggerSignalProvider dedup falls back to a content hash (channel|room|from|text) when ChannelMessage.dedupeKey is absent, which coalesces a user sending the exact same text back-to-back. Pass each adapter's stable message id as dedupeKey from the ~10 channel-bot call sites (slack/discord/telegram/teams/whatsapp/gchat/github/linear onDirectMessage + onNewMention) where the Chat SDK message exposes one. Removes the false-positive edge; true per-delivery idempotency.","status":"in_progress","priority":3,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T03:31:07Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:39:12Z","started_at":"2026-06-12T16:39:12Z","dependencies":[{"issue_id":"foreman-m9f6","depends_on_id":"foreman-tv5p","type":"discovered-from","created_at":"2026-06-11T20:31:07Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-m9f6","title":"[channels] Thread real platform message ids into ChannelMessage.dedupeKey","description":"ChannelTriggerSignalProvider dedup falls back to a content hash (channel|room|from|text) when ChannelMessage.dedupeKey is absent, which coalesces a user sending the exact same text back-to-back. Pass each adapter's stable message id as dedupeKey from the ~10 channel-bot call sites (slack/discord/telegram/teams/whatsapp/gchat/github/linear onDirectMessage + onNewMention) where the Chat SDK message exposes one. Removes the false-positive edge; true per-delivery idempotency.","status":"closed","priority":3,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T03:31:07Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:46:33Z","started_at":"2026-06-12T16:39:12Z","closed_at":"2026-06-12T16:46:33Z","close_reason":"Threaded normalized Chat SDK message.id into ChannelMessage.dedupeKey across all 8 channel bots (16 call-sites). Provider now dedupes on the real platform message id; content-hash is fallback only. Type-clean, 308 mocked tests green, committed c942a92.","dependencies":[{"issue_id":"foreman-m9f6","depends_on_id":"foreman-tv5p","type":"discovered-from","created_at":"2026-06-11T20:31:07Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-l8ev","title":"Document Zapier SDK auth model in README (client-creds vs userJwt per surface)","description":"Discovered during SDK exploration: the Zapier SDK surfaces need DIFFERENT auth. Trigger inboxes + discovery + runAction accept app-level client-credentials (ZAPIER_CLIENT_ID/SECRET). Durable-workflow endpoints (runDurable/publishWorkflowVersion) require a per-user userJwt (PKCE OAuth token), NOT client-creds. The new zapier-sdk-cli (0.54) login yields client-creds only; ~/.zapierrc deployKey is a legacy 32-char non-JWT (invalid as SDK creds). Document in README: which surfaces need which auth, how Foreman obtains each (env client-creds vs lib/zapier/sdk.ts PKCE per-user token), and the open question for Zapier (server-side path to per-user durable). Come back to this later. See docs/durable-workflow-spike.md + docs/trigger-inbox-spike.md.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T18:39:52Z","created_by":"HamChowderr","updated_at":"2026-06-11T18:39:52Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-z0kn","title":"Port provider-routing generate() test to a PG-enabled tier (storage injection)","description":"provider-swap.test.ts 'returns text' sub-test calls agent.generate() which now needs PostgresStore-backed Memory (foreman migrated LibSQL-\u003e@mastra/pg). It can't run in the no-external-services mocked tier (agents CI job has no PG), so it's it.skip'd. Proper fix: either inject a test storage into createForemanAgent (let tests use an in-memory/file store), or run this sub-test in a PG-enabled tier (test:live or a CI job with a postgres service). Discovered during the catch-up integration of claude/add-multiple-providers.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T16:40:19Z","created_by":"HamChowderr","updated_at":"2026-06-11T16:40:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-elyi","title":"[bump] Surface ZapierApprovalError approval URL to the user (handleSdkError)","description":"Under SDK \u003e=0.50 with approvalMode unset, a server (non-TTY) defaults to 'throw': sdk.runAction throws ZapierApprovalError carrying an approval URL on approval-gated responses. Foreman's handleSdkError (zapier-sdk-tools.ts) currently catches it only via the generic ZapierError catch-all, surfacing .message/.code but DROPPING the approval URL. Add an explicit 'if (err instanceof ZapierApprovalError)' branch that returns the approval URL + approvalStatus so the agent can tell the user how to approve. Non-blocking for the bump (degrades gracefully today). Discovered during foreman-2xdk verification.","status":"closed","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T15:10:57Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:39:56Z","closed_at":"2026-06-11T22:39:56Z","close_reason":"Done: handleSdkError now surfaces ZapierApprovalError.approvalUrl (code APPROVAL_REQUIRED), placed before the generic ZapierError catch-all; handleSdkError exported + 2 unit tests (with/without URL) pass; lint clean. Verified approvalUrl field exists in SDK 0.69.3.","dependencies":[{"issue_id":"foreman-elyi","depends_on_id":"foreman-2xdk","type":"discovered-from","created_at":"2026-06-11T08:10:57Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/vercel.json b/vercel.json index 8f5df92c..7783584d 100644 --- a/vercel.json +++ b/vercel.json @@ -1,6 +1,6 @@ { "framework": "nextjs", - "installCommand": "npm install", + "installCommand": "npm ci", "buildCommand": "cd packages/web && npm run build", "outputDirectory": "packages/web/.next" } From a5bb0494e714c0a777349707373cbc2c989fd728 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Fri, 12 Jun 2026 10:21:20 -0700 Subject: [PATCH 17/19] fix(web): clean stale workspace node_modules before Vercel install (foreman-sg23) Real cause (from the Vercel build log): next build failed with 'Cannot find module react' even though react/react-dom are declared in packages/web. Vercel restores a build cache from a previous deployment that includes a stale packages/web/node_modules; a root npm ci does not clean nested workspace node_modules, so next build resolves react from the poisoned nested dir and fails. GHA has no such cache and passes. The prior npm ci-only change did not help because the cache, not the install command, was the problem. Force-clean root + nested node_modules before installing so any restored stale cache cannot shadow resolution. Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 2 +- vercel.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index e3193407..1e0875ba 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,7 +92,7 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-sg23","title":"Vercel preview build fails (module_not_found) on alpha branch — align install with CI","description":"PR #17 Vercel preview deploy errors at buildStep with errorCode module_not_found ('cd packages/web \u0026\u0026 npm run build' exit 1), while all 5 GHA gating jobs (incl. web next build via npm ci) are GREEN on the same commit, and a clean local build passes. Install succeeds; .source self-regenerates via withMDX so it is not the cause. Build-log API is 403 (CLI token lacks build-log scope), so the exact module is dashboard-only. Divergence: Vercel installCommand 'npm install' (+ possible stale cached node_modules on a fresh branch) vs GHA 'npm ci'. Non-gating (main production deploys are Ready), does not block merge. Fix: set vercel.json installCommand to 'npm ci' to force a clean lockfile-exact install matching the proven-green CI path.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T17:14:15Z","created_by":"HamChowderr","updated_at":"2026-06-12T17:14:36Z","started_at":"2026-06-12T17:14:36Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-sg23","title":"Vercel preview build fails (module_not_found) on alpha branch — align install with CI","description":"PR #17 Vercel preview deploy errors at buildStep with errorCode module_not_found ('cd packages/web \u0026\u0026 npm run build' exit 1), while all 5 GHA gating jobs (incl. web next build via npm ci) are GREEN on the same commit, and a clean local build passes. Install succeeds; .source self-regenerates via withMDX so it is not the cause. Build-log API is 403 (CLI token lacks build-log scope), so the exact module is dashboard-only. Divergence: Vercel installCommand 'npm install' (+ possible stale cached node_modules on a fresh branch) vs GHA 'npm ci'. Non-gating (main production deploys are Ready), does not block merge. Fix: set vercel.json installCommand to 'npm ci' to force a clean lockfile-exact install matching the proven-green CI path.","notes":"Root cause via build log (dpl_JCwNBsDY7FiwLFQafDJ8x8L4MeNb): 'The module react was not found' / Cannot find module 'react' at next build. react+react-dom ARE declared in packages/web/package.json (19.2.4) — not a missing-dep bug. Log shows 'Restored build cache from previous deployment'; lockfile has react both at root and packages/web/node_modules. Vercel restores a stale nested packages/web/node_modules; root npm ci does not clean nested workspace dirs, so next build resolves react from the poisoned nested dir and fails. GHA passes (no Vercel cache). npm ci alone did NOT fix (3e8a5ec). Real fix: installCommand 'rm -rf node_modules packages/*/node_modules \u0026\u0026 npm ci' to defeat the stale cache.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T17:14:15Z","created_by":"HamChowderr","updated_at":"2026-06-12T17:21:20Z","started_at":"2026-06-12T17:14:36Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-hlh0","title":"[web] Surface run fired_by/error_message + fix failed-status badge","description":"The agent API (GET /workflows/:id/runs) now returns fired_by/error_message/trigger_id (foreman-2afc), but the web doesn't render them, and there's a pre-existing display bug: the engine writes status 'failed' but workflows-client WorkflowRun.status + the StatusBadge map only know 'error', so failed runs render as 'running'. Fix: (1) WorkflowRun.status -\u003e 'running'|'success'|'failed' + add fired_by/error_message/trigger_id; (2) [id]/page.tsx StatusBadge map key error-\u003efailed + show how each run fired; (3) run detail page show fired_by + error_message. First visible slice of the broader 'surface new backend in UI' work (foreman-tbjb/7am4/ezsm).","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T16:30:49Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:34:48Z","closed_at":"2026-06-12T16:34:48Z","close_reason":"Done (ada01c9): WorkflowRun type gains status=failed + fired_by/error_message/trigger_id; run list shows how each run fired + inline error on failure; run detail shows 'started by' + an error box. Fixed the pre-existing bug where failed runs rendered as 'running' (engine writes 'failed', web only knew 'error'). next build green.","dependencies":[{"issue_id":"foreman-hlh0","depends_on_id":"foreman-2afc","type":"discovered-from","created_at":"2026-06-12T09:30:48Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-tbjb","title":"[signals] notify() is inert — driving processes don't connect the providers","description":"The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them don't reliably CONNECT them, so notify() is a no-op (provider.agent undefined -\u003e notifyOwner returns early). (1) cron-driver-server drives zapierPollProvider.runDuePolls() but never calls getMastra()/builds the foreman agent, so poll notify() never fires. (2) channel: slack/bot.ts builds the agent lazily inside generateReply, AFTER matchAndFireChannelTriggers, so the channel provider isn't connected for trigger-only messages until the process first runs generateReply. (3) even when fired, notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) and no web UI consumes the notification store, so it's not user-visible. Workflow FIRING is unaffected/correct (doesn't need the agent). FIX: have the driving processes construct the agent at startup (cron-driver-server: getMastra() so the provider connects; webhook-server: build foreman at startup) + a web consumer of the notification store / thread to surface it. Until then notify() is scaffolding, not delivered.","notes":"CONNECT FIX DONE (commit 177c281): cron-driver-server + webhook-server now call getMastra() at startup -\u003e foreman agent built in-process -\u003e providers connected (+ notification storage available). Live-verified: cron logs 'poll provider connected: true'. So notify() now FIRES (writes to the notification store) on poll/channel trigger hits. REMAINING (the user-visible half): a web consumer of the notification store / agent thread to surface the notification in chat — bundles with the deferred background-tasks /background-tasks/stream consumer. Also: notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) — revisit thread targeting when wiring the UI.","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T05:22:51Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:34:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"LIVE-VERIFIED (mastra dev). FINDING: backgrounding the read-only Zapier SDK tools HANGS mastra dev — enabling background injects a _background field into the tool input schema and Studio toJSONSchema over the Zapier zod schemas loops forever (same as lazy-init-zapier hang). mastra build is unaffected (no Studio). Bisected: no-flag binds :4111 in ~12s; flag-on-Zapier hangs. FIX (commit 0a20828): background search_history instead (simple 3-field schema) -\u003e dev binds in ~12s with flag on; /background-tasks/stream serves SSE 200 (manager live). lib/background.ts backgroundIfEnabled() gate, FOREMAN_BACKGROUND_TOOLS=1, OFF by default. REMAINING for full non-blocking UX (modest, search_history-only given the Zapier wall): waitTimeoutMs + web /background-tasks/stream consumer + end-to-end chat verify (LLM round-trip). Zapier tools blocked until alpha fixes toJSONSchema.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:05:59Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/vercel.json b/vercel.json index 7783584d..c724854a 100644 --- a/vercel.json +++ b/vercel.json @@ -1,6 +1,6 @@ { "framework": "nextjs", - "installCommand": "npm ci", + "installCommand": "rm -rf node_modules packages/*/node_modules && npm ci", "buildCommand": "cd packages/web && npm run build", "outputDirectory": "packages/web/.next" } From 97459536e8100ec6daeeb67bad36fa32321301ec Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Fri, 12 Jun 2026 10:59:23 -0700 Subject: [PATCH 18/19] fix(web): dedupe react to a single version so Vercel resolves it (foreman-sg23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vercel preview build failed at next build with 'Cannot find module react' even though react/react-dom are declared in packages/web. Root cause: the alpha lockfile regen split react into two versions — root node_modules/react 19.2.7 (pulled by the ^19.2.0 ecosystem) and a nested packages/web/node_modules/react 19.2.4 (web pinned it exactly). next is hoisted to root and Vercel's npm could not resolve react across that split (GHA's npm tolerated it, so CI stayed green and masked it). Fix: add root overrides react/react-dom=19.2.7 so a clean resolve dedupes react to a single root copy with no nested duplicate. Verified locally: lockfile has one react entry and packages/web next build passes. Reverts the two earlier wrong guesses (npm ci, rm -rf cache clean); installCommand restored to npm install. Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 2 +- package-lock.json | 569 +++++++++++++++++++------------------------- package.json | 4 +- vercel.json | 2 +- 4 files changed, 252 insertions(+), 325 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 1e0875ba..398a93e6 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,7 +92,7 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-sg23","title":"Vercel preview build fails (module_not_found) on alpha branch — align install with CI","description":"PR #17 Vercel preview deploy errors at buildStep with errorCode module_not_found ('cd packages/web \u0026\u0026 npm run build' exit 1), while all 5 GHA gating jobs (incl. web next build via npm ci) are GREEN on the same commit, and a clean local build passes. Install succeeds; .source self-regenerates via withMDX so it is not the cause. Build-log API is 403 (CLI token lacks build-log scope), so the exact module is dashboard-only. Divergence: Vercel installCommand 'npm install' (+ possible stale cached node_modules on a fresh branch) vs GHA 'npm ci'. Non-gating (main production deploys are Ready), does not block merge. Fix: set vercel.json installCommand to 'npm ci' to force a clean lockfile-exact install matching the proven-green CI path.","notes":"Root cause via build log (dpl_JCwNBsDY7FiwLFQafDJ8x8L4MeNb): 'The module react was not found' / Cannot find module 'react' at next build. react+react-dom ARE declared in packages/web/package.json (19.2.4) — not a missing-dep bug. Log shows 'Restored build cache from previous deployment'; lockfile has react both at root and packages/web/node_modules. Vercel restores a stale nested packages/web/node_modules; root npm ci does not clean nested workspace dirs, so next build resolves react from the poisoned nested dir and fails. GHA passes (no Vercel cache). npm ci alone did NOT fix (3e8a5ec). Real fix: installCommand 'rm -rf node_modules packages/*/node_modules \u0026\u0026 npm ci' to defeat the stale cache.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T17:14:15Z","created_by":"HamChowderr","updated_at":"2026-06-12T17:21:20Z","started_at":"2026-06-12T17:14:36Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-sg23","title":"Vercel preview build fails (module_not_found) on alpha branch — align install with CI","description":"PR #17 Vercel preview deploy errors at buildStep with errorCode module_not_found ('cd packages/web \u0026\u0026 npm run build' exit 1), while all 5 GHA gating jobs (incl. web next build via npm ci) are GREEN on the same commit, and a clean local build passes. Install succeeds; .source self-regenerates via withMDX so it is not the cause. Build-log API is 403 (CLI token lacks build-log scope), so the exact module is dashboard-only. Divergence: Vercel installCommand 'npm install' (+ possible stale cached node_modules on a fresh branch) vs GHA 'npm ci'. Non-gating (main production deploys are Ready), does not block merge. Fix: set vercel.json installCommand to 'npm ci' to force a clean lockfile-exact install matching the proven-green CI path.","notes":"FIXED properly: root cause was a duplicated react in the alpha-regenerated lockfile — root node_modules/react 19.2.7 (ecosystem ^19.2.0) + nested packages/web/node_modules/react 19.2.4 (web's exact pin). next (hoisted to root) could not resolve react on Vercel's npm. Fix: root overrides react/react-dom=19.2.7 → clean reinstall dedupes to a SINGLE root react 19.2.7 (no nested copy), verified locally (lockfile single entry + web next build exit 0). Reverted the two wrong guesses (npm ci 3e8a5ec, rm -rf a5bb049); installCommand back to npm install.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T17:14:15Z","created_by":"HamChowderr","updated_at":"2026-06-12T17:59:22Z","started_at":"2026-06-12T17:14:36Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-hlh0","title":"[web] Surface run fired_by/error_message + fix failed-status badge","description":"The agent API (GET /workflows/:id/runs) now returns fired_by/error_message/trigger_id (foreman-2afc), but the web doesn't render them, and there's a pre-existing display bug: the engine writes status 'failed' but workflows-client WorkflowRun.status + the StatusBadge map only know 'error', so failed runs render as 'running'. Fix: (1) WorkflowRun.status -\u003e 'running'|'success'|'failed' + add fired_by/error_message/trigger_id; (2) [id]/page.tsx StatusBadge map key error-\u003efailed + show how each run fired; (3) run detail page show fired_by + error_message. First visible slice of the broader 'surface new backend in UI' work (foreman-tbjb/7am4/ezsm).","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T16:30:49Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:34:48Z","closed_at":"2026-06-12T16:34:48Z","close_reason":"Done (ada01c9): WorkflowRun type gains status=failed + fired_by/error_message/trigger_id; run list shows how each run fired + inline error on failure; run detail shows 'started by' + an error box. Fixed the pre-existing bug where failed runs rendered as 'running' (engine writes 'failed', web only knew 'error'). next build green.","dependencies":[{"issue_id":"foreman-hlh0","depends_on_id":"foreman-2afc","type":"discovered-from","created_at":"2026-06-12T09:30:48Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-tbjb","title":"[signals] notify() is inert — driving processes don't connect the providers","description":"The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them don't reliably CONNECT them, so notify() is a no-op (provider.agent undefined -\u003e notifyOwner returns early). (1) cron-driver-server drives zapierPollProvider.runDuePolls() but never calls getMastra()/builds the foreman agent, so poll notify() never fires. (2) channel: slack/bot.ts builds the agent lazily inside generateReply, AFTER matchAndFireChannelTriggers, so the channel provider isn't connected for trigger-only messages until the process first runs generateReply. (3) even when fired, notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) and no web UI consumes the notification store, so it's not user-visible. Workflow FIRING is unaffected/correct (doesn't need the agent). FIX: have the driving processes construct the agent at startup (cron-driver-server: getMastra() so the provider connects; webhook-server: build foreman at startup) + a web consumer of the notification store / thread to surface it. Until then notify() is scaffolding, not delivered.","notes":"CONNECT FIX DONE (commit 177c281): cron-driver-server + webhook-server now call getMastra() at startup -\u003e foreman agent built in-process -\u003e providers connected (+ notification storage available). Live-verified: cron logs 'poll provider connected: true'. So notify() now FIRES (writes to the notification store) on poll/channel trigger hits. REMAINING (the user-visible half): a web consumer of the notification store / agent thread to surface the notification in chat — bundles with the deferred background-tasks /background-tasks/stream consumer. Also: notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) — revisit thread targeting when wiring the UI.","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T05:22:51Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:34:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"LIVE-VERIFIED (mastra dev). FINDING: backgrounding the read-only Zapier SDK tools HANGS mastra dev — enabling background injects a _background field into the tool input schema and Studio toJSONSchema over the Zapier zod schemas loops forever (same as lazy-init-zapier hang). mastra build is unaffected (no Studio). Bisected: no-flag binds :4111 in ~12s; flag-on-Zapier hangs. FIX (commit 0a20828): background search_history instead (simple 3-field schema) -\u003e dev binds in ~12s with flag on; /background-tasks/stream serves SSE 200 (manager live). lib/background.ts backgroundIfEnabled() gate, FOREMAN_BACKGROUND_TOOLS=1, OFF by default. REMAINING for full non-blocking UX (modest, search_history-only given the Zapier wall): waitTimeoutMs + web /background-tasks/stream consumer + end-to-end chat verify (LLM round-trip). Zapier tools blocked until alpha fixes toJSONSchema.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:05:59Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/package-lock.json b/package-lock.json index cd80d874..8d1358be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,13 +71,13 @@ "license": "MIT" }, "node_modules/@ai-sdk/gateway": { - "version": "3.0.128", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.128.tgz", - "integrity": "sha512-t9Dyqk5thSmIzsNvVTQtZZiO3xqKGkk1hX3+GNpYmro+GuEdW+E6mKFHihb9Y1vCEt3rEyd4pbGUcin4D57FfQ==", + "version": "3.0.129", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.129.tgz", + "integrity": "sha512-KEQpZGJuCksc4iFxYtVHeHHG7yH0izGFzJLmRZlriI0hFIzJF9bT2AzJoaTHUI6minlxtP0WKYh84dP18o/Cuw==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.10", - "@ai-sdk/provider-utils": "4.0.28", + "@ai-sdk/provider-utils": "4.0.29", "@vercel/oidc": "3.2.0" }, "engines": { @@ -100,9 +100,9 @@ } }, "node_modules/@ai-sdk/provider-utils": { - "version": "4.0.28", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.28.tgz", - "integrity": "sha512-nCtGJY43Kqwoyk0rYLj80a3eyXxcaWwyu+lYZDHgY+U6JbYYyvRdRTSy+XCDIB91rFH0Ul0eMDHmDr+YMYtrSw==", + "version": "4.0.29", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.29.tgz", + "integrity": "sha512-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.10", @@ -191,13 +191,13 @@ } }, "node_modules/@ai-sdk/react": { - "version": "3.0.204", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.204.tgz", - "integrity": "sha512-4I5fq9n/YBCEPfbvYEzc5lFNMxoF2hZRXVrUAsjA6NL7LY8AJo3V3PD2TH9rTKIY08uolnyJSnnQc7YuFXXUIA==", + "version": "3.0.205", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.205.tgz", + "integrity": "sha512-qh4H9dUqTJwdr+arKSIrVBHG4o4sYJdZGFUCJfAf9vMe/5IwDRwhq9z5ztpcUrXD3QACO01nuyfufNc0ZoGReA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider-utils": "4.0.28", - "ai": "6.0.202", + "@ai-sdk/provider-utils": "4.0.29", + "ai": "6.0.203", "swr": "^2.2.5", "throttleit": "2.1.0" }, @@ -870,9 +870,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", - "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.0.tgz", + "integrity": "sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -886,20 +886,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.4.16", - "@biomejs/cli-darwin-x64": "2.4.16", - "@biomejs/cli-linux-arm64": "2.4.16", - "@biomejs/cli-linux-arm64-musl": "2.4.16", - "@biomejs/cli-linux-x64": "2.4.16", - "@biomejs/cli-linux-x64-musl": "2.4.16", - "@biomejs/cli-win32-arm64": "2.4.16", - "@biomejs/cli-win32-x64": "2.4.16" + "@biomejs/cli-darwin-arm64": "2.5.0", + "@biomejs/cli-darwin-x64": "2.5.0", + "@biomejs/cli-linux-arm64": "2.5.0", + "@biomejs/cli-linux-arm64-musl": "2.5.0", + "@biomejs/cli-linux-x64": "2.5.0", + "@biomejs/cli-linux-x64-musl": "2.5.0", + "@biomejs/cli-win32-arm64": "2.5.0", + "@biomejs/cli-win32-x64": "2.5.0" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", - "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==", "cpu": [ "arm64" ], @@ -914,9 +914,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", - "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz", + "integrity": "sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==", "cpu": [ "x64" ], @@ -931,9 +931,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", - "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.0.tgz", + "integrity": "sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==", "cpu": [ "arm64" ], @@ -951,9 +951,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", - "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==", "cpu": [ "arm64" ], @@ -971,9 +971,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", - "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.0.tgz", + "integrity": "sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==", "cpu": [ "x64" ], @@ -991,9 +991,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", - "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==", "cpu": [ "x64" ], @@ -1011,9 +1011,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", - "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.0.tgz", + "integrity": "sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==", "cpu": [ "arm64" ], @@ -1028,9 +1028,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", - "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.0.tgz", + "integrity": "sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==", "cpu": [ "x64" ], @@ -1958,6 +1958,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1979,6 +1980,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2525,9 +2527,9 @@ "link": true }, "node_modules/@fuma-translate/react": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@fuma-translate/react/-/react-1.0.1.tgz", - "integrity": "sha512-jYNEoTHzOqYsVqnPawGq/zbGhc+xMV69YPENDccZ5gFla2uafEEl82Jxic/PxAGmkZzv8U65etDkvaJXzc0DjQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@fuma-translate/react/-/react-1.0.2.tgz", + "integrity": "sha512-uOiOtBx3nRXR8Nu1GzBf1tApgF1FErDBTHxRIAQeyQdyOoZbrNRN6H4kDCWObY4qyGeGbHydG0DHzgeUgFDMIw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3250,6 +3252,16 @@ } } }, + "node_modules/@inquirer/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@inquirer/core/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3576,18 +3588,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3829,9 +3829,9 @@ } }, "node_modules/@mastra/deployer": { - "version": "1.42.0-alpha.3", - "resolved": "https://registry.npmjs.org/@mastra/deployer/-/deployer-1.42.0-alpha.3.tgz", - "integrity": "sha512-fqKJ23+/3gsYWtuHFIJEMsJ5jI81tyslFk75O6lnzofDvkl5k8XS3TtdcCLMMpmUgO0MfAvUkzwTonq2j4R2Ww==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@mastra/deployer/-/deployer-1.42.0.tgz", + "integrity": "sha512-iRpCf+gP4OCkGBYfHUKLzARi6+4gFBIK/yVngYveE5f/IuUhIPYRBL1SBoYVDx6r25L9yyLrw7jstAFDggRgHQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -3839,7 +3839,7 @@ "@babel/preset-typescript": "^7.29.7", "@babel/traverse": "^7.29.7", "@hono/node-ws": "^1.3.0", - "@mastra/server": "1.42.0-alpha.3", + "@mastra/server": "1.42.0", "@optimize-lodash/rollup-plugin": "^5.1.0", "@rollup/plugin-alias": "6.0.0", "@rollup/plugin-commonjs": "29.0.2", @@ -3859,7 +3859,7 @@ "rollup": "^4.59.0", "rollup-plugin-esbuild": "^6.2.1", "strip-json-comments": "^5.0.3", - "tinyglobby": "^0.2.16", + "tinyglobby": "^0.2.17", "typescript-paths": "^1.5.2", "ws": "^8.20.0" }, @@ -4148,9 +4148,9 @@ } }, "node_modules/@mastra/server": { - "version": "1.42.0-alpha.3", - "resolved": "https://registry.npmjs.org/@mastra/server/-/server-1.42.0-alpha.3.tgz", - "integrity": "sha512-InmbmY0tdITb6hINTZaoqNDbRshOYObeUrL50pWHobZzh0jXGce4X1p7oyZx9ApsPWWuZbWvjryhsOoHICYgpw==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@mastra/server/-/server-1.42.0.tgz", + "integrity": "sha512-3CXP5Mwh2OcFY8QdmPpqVWSJm4QcA9AD+Ln3PsLbBlcrlpzL2+w59SsbMau0sk9NjHyky6QRK29NzRhwG8hwug==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -4626,6 +4626,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5154,7 +5155,7 @@ "version": "0.133.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -5225,7 +5226,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.60.0" @@ -6841,6 +6842,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6857,6 +6859,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6873,6 +6876,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6889,6 +6893,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6905,6 +6910,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6921,6 +6927,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -6940,6 +6947,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -6959,6 +6967,7 @@ "cpu": [ "ppc64" ], + "dev": true, "libc": [ "glibc" ], @@ -6978,6 +6987,7 @@ "cpu": [ "s390x" ], + "dev": true, "libc": [ "glibc" ], @@ -6997,6 +7007,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -7016,6 +7027,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -7035,6 +7047,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7051,6 +7064,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7069,6 +7083,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7085,6 +7100,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7098,7 +7114,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@rollup/plugin-alias": { @@ -7153,24 +7169,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -8148,7 +8146,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 20" @@ -8175,6 +8173,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8191,6 +8190,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8207,6 +8207,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8223,6 +8224,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8239,6 +8241,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8255,6 +8258,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -8274,6 +8278,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -8293,6 +8298,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -8312,6 +8318,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -8339,6 +8346,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -8360,6 +8368,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8376,6 +8385,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8399,27 +8409,6 @@ "tailwindcss": "4.3.0" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -8440,13 +8429,6 @@ "yarn": ">=1" } }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, "node_modules/@testing-library/react": { "version": "16.3.2", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", @@ -8479,20 +8461,13 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/babel__traverse": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", @@ -8925,7 +8900,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -8935,7 +8910,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -9363,14 +9338,14 @@ } }, "node_modules/ai": { - "version": "6.0.202", - "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.202.tgz", - "integrity": "sha512-14O7uyHVa5lXyt1RzduRqM1zwOnLBN+EaE+btWiP2R7GLHd5oh+Z9uaebR785V/Y2hXlxvuQGsw2Gj6iHpkxsg==", + "version": "6.0.203", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.203.tgz", + "integrity": "sha512-2Qi1ZPGF/FnlvnRqntVgRbUYGeA5ZKFYwTtgu8rcUzMmddArM/nLsvCW69Ip99B1cop6XHRHl+GCKk9t9B+GDA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "3.0.128", + "@ai-sdk/gateway": "3.0.129", "@ai-sdk/provider": "3.0.10", - "@ai-sdk/provider-utils": "4.0.28", + "@ai-sdk/provider-utils": "4.0.29", "@opentelemetry/api": "^1.9.0" }, "engines": { @@ -9424,23 +9399,24 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "peer": true, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -9531,13 +9507,13 @@ } }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" + "engines": { + "node": ">= 0.4" } }, "node_modules/asap": { @@ -9749,12 +9725,13 @@ } }, "node_modules/bare-stream": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.2.tgz", - "integrity": "sha512-2V5j0dOfxv3iIngIWMnW1O6UPXpeoU70HJzUJGVth/+RURhDyq7SEWTD70Y2SkUB0MQonYYWpIX3f85P/I22+Q==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", "dev": true, "license": "Apache-2.0", "dependencies": { + "b4a": "^1.8.1", "streamx": "^2.25.0", "teex": "^1.0.1" }, @@ -9806,9 +9783,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.36", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.36.tgz", - "integrity": "sha512-lVq/Df7LXlO79MVaaUHztSwWiG9oXoWHlgvNS51v8Dpd4+G4/VIy6qYePTw31nAVls33nUtnfezYeLkYAak9dg==", + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -9988,19 +9965,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/boxen/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/boxen/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -11255,7 +11219,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/cytoscape": { @@ -12133,17 +12097,16 @@ } }, "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", - "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz", + "integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -12238,9 +12201,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.371", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", - "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", "devOptional": true, "license": "ISC" }, @@ -12442,9 +12405,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", - "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "version": "1.47.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.1.tgz", + "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==", "license": "MIT", "workspaces": [ "docs", @@ -12980,6 +12943,23 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -13141,16 +13121,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -13322,12 +13302,12 @@ } }, "node_modules/fumadocs-core": { - "version": "16.10.1", - "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.10.1.tgz", - "integrity": "sha512-iGnB03/VyMSTWIaZ8zaDG/b/4q1e4gSzWDSvP3AR5Yxg9UJMsA0acaN/IFcURBSgRgJq6PELyYA6WfHBvHAgSg==", + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.10.2.tgz", + "integrity": "sha512-7YMEkUYQYYey+8yzZW47jV3pbctCXsevFY6u4yN9PRwJi/q76QOFfefvzSkbYha3OIudr2C0RGzJxEhzV2a1/g==", "license": "MIT", "dependencies": { - "@fuma-translate/react": "^1.0.1", + "@fuma-translate/react": "^1.0.2", "@orama/orama": "^3.1.18", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", @@ -13596,12 +13576,12 @@ } }, "node_modules/fumadocs-ui": { - "version": "16.10.1", - "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.10.1.tgz", - "integrity": "sha512-ytEwbMFFadfuul9x4Pz4pg9FMRI1MkqW5P7bHrWsLF+d1C4whzNtcUKPn0QP6KCQqIKoVhIa3C7qlI9v06Ik1A==", + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.10.2.tgz", + "integrity": "sha512-l8fYpahaLVA73XUktsjTdq6iwG6V0/UGAnN1uuCeVuV4ea/8RDduOo9CcMM62SCup0EnT1zEAk+FGFCdKQ4BhQ==", "license": "MIT", "dependencies": { - "@fuma-translate/react": "^1.0.1", + "@fuma-translate/react": "^1.0.2", "@fumadocs/tailwind": "0.0.5", "@radix-ui/react-accordion": "^1.2.13", "@radix-ui/react-collapsible": "^1.1.13", @@ -13628,7 +13608,7 @@ "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", - "fumadocs-core": "16.10.1", + "fumadocs-core": "16.10.2", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" @@ -14082,16 +14062,6 @@ "node": ">=8.0.0" } }, - "node_modules/graphql": { - "version": "16.14.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", - "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, "node_modules/gray-matter": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", @@ -15189,7 +15159,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -15613,7 +15583,7 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "devOptional": true, + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -15646,6 +15616,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15666,6 +15637,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15686,6 +15658,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15706,6 +15679,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15726,6 +15700,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15746,6 +15721,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -15769,6 +15745,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -15792,6 +15769,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -15815,6 +15793,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -15838,6 +15817,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15858,6 +15838,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -16088,25 +16069,14 @@ "license": "ISC" }, "node_modules/lucide-react": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz", - "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz", + "integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-bytes.js": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", @@ -18057,9 +18027,9 @@ } }, "node_modules/obug": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", - "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -18800,7 +18770,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.60.0" @@ -18819,7 +18789,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -18919,12 +18889,12 @@ } }, "node_modules/posthog-node": { - "version": "5.36.17", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.36.17.tgz", - "integrity": "sha512-ed1LT4a9hhiFJizB6XX7dkYYLVPAFHfUpkQSns7BRxoUyhFnvMq15QENKeAOUEKQgPmnaq2I+xNLdAHN0o9eAA==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.37.0.tgz", + "integrity": "sha512-wFwWGcqAqZ1WJRlNNYc92veV83d1lOQcP4Lq0q7Kar9GdZLPpiFYHeudyybYJnjZjkI9v06vLvY/Og5CZIfByg==", "license": "MIT", "dependencies": { - "@posthog/core": "1.32.3" + "@posthog/core": "^1.32.3" }, "engines": { "node": "^20.20.0 || >=22.22.0" @@ -18966,22 +18936,6 @@ "node": ">=10" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", @@ -19501,7 +19455,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -19550,7 +19503,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -19558,13 +19510,6 @@ "react": "^19.2.7" } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT", - "peer": true - }, "node_modules/react-redux": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", @@ -20271,7 +20216,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.133.0", @@ -20464,7 +20409,7 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -21284,9 +21229,9 @@ } }, "node_modules/streamx": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.27.0.tgz", - "integrity": "sha512-WZ189TKnHoAokYHvwzaAQMpd55cgUmFIcJFzBSgGcb886jau5DL+XdDhTWV4ps3FLvk+OORp0dLRTPsLZ21CSA==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", "dev": true, "license": "MIT", "dependencies": { @@ -21341,6 +21286,15 @@ "node": ">=8" } }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -21353,6 +21307,15 @@ "node": ">=8" } }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -21407,16 +21370,13 @@ "node": ">=8" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, "node_modules/strip-bom-string": { @@ -21634,7 +21594,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/tapable": { @@ -21797,23 +21757,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -21984,7 +21927,7 @@ "version": "4.22.4", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.28.0" @@ -22003,6 +21946,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -22089,6 +22033,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -22629,7 +22574,7 @@ "version": "8.0.16", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -22707,6 +22652,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -22721,7 +22667,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -23029,6 +22975,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -23056,19 +23011,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", @@ -23500,23 +23442,6 @@ "engines": { "node": "^18 || >=20" } - }, - "packages/web/node_modules/react": { - "version": "19.2.4", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "packages/web/node_modules/react-dom": { - "version": "19.2.4", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } } } } diff --git a/package.json b/package.json index e0abb3d8..cdb38524 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "esbuild": ">=0.25.0", "zod": "4.4.3", "postcss": "^8.5.10", - "@mastra/core": "1.42.0-alpha.3" + "@mastra/core": "1.42.0-alpha.3", + "react": "19.2.7", + "react-dom": "19.2.7" }, "devDependencies": { "@biomejs/biome": "^2.4.12", diff --git a/vercel.json b/vercel.json index c724854a..8f5df92c 100644 --- a/vercel.json +++ b/vercel.json @@ -1,6 +1,6 @@ { "framework": "nextjs", - "installCommand": "rm -rf node_modules packages/*/node_modules && npm ci", + "installCommand": "npm install", "buildCommand": "cd packages/web && npm run build", "outputDirectory": "packages/web/.next" } From 7ded42ab2b8cefcb67f977fc1bcf61b7fec39324 Mon Sep 17 00:00:00 2001 From: HamChowderr Date: Fri, 12 Jun 2026 11:35:45 -0700 Subject: [PATCH 19/19] fix(web): dedupe react via web caret pin, keep lockfile in sync (foreman-sg23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes the override approach in 9745953. That made Vercel green but broke npm ci in CI (EUSAGE: 'Missing react@19.2.4 from lock file') because packages/web still declared react exactly 19.2.4 while the root override rewrote the lockfile to 19.2.7 — npm install tolerated the mismatch, npm ci (strict) rejected it. Root cause was web pinning react/react-dom to an EXACT 19.2.4 while the rest of the workspace uses ^19.2.0 (-> 19.2.7), which split react into a hoisted root copy + a nested web copy that next (hoisted to root) could not resolve on Vercel. Fix at the source: change web's react and react-dom from 19.2.4 to ^19.2.0 (matching the repo's caret convention) and drop the override, so a single react 19.2.7 hoists to root and declared==locked. Verified locally with npm ci (exit 0, in sync) and packages/web next build (exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 146 +++++++++++++++++++------------------- package.json | 4 +- packages/web/package.json | 4 +- 3 files changed, 76 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d1358be..161cc7ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8127,49 +8127,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", - "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.21.0", - "jiti": "^2.6.1", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.0" + "tailwindcss": "4.3.1" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", - "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-x64": "4.3.0", - "@tailwindcss/oxide-freebsd-x64": "4.3.0", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-x64-musl": "4.3.0", - "@tailwindcss/oxide-wasm32-wasi": "4.3.0", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", - "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", "cpu": [ "arm64" ], @@ -8184,9 +8184,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", - "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", "cpu": [ "arm64" ], @@ -8201,9 +8201,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", - "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", "cpu": [ "x64" ], @@ -8218,9 +8218,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", - "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", "cpu": [ "x64" ], @@ -8235,9 +8235,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", - "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", "cpu": [ "arm" ], @@ -8252,9 +8252,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", - "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", "cpu": [ "arm64" ], @@ -8272,9 +8272,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", - "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", "cpu": [ "arm64" ], @@ -8292,9 +8292,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", - "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", "cpu": [ "x64" ], @@ -8312,9 +8312,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", - "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", "cpu": [ "x64" ], @@ -8332,9 +8332,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", - "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -8354,7 +8354,7 @@ "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.1", + "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "engines": { @@ -8362,9 +8362,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", - "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", "cpu": [ "arm64" ], @@ -8379,9 +8379,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", - "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", "cpu": [ "x64" ], @@ -8396,17 +8396,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.0.tgz", - "integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.1.tgz", + "integrity": "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.3.0", - "@tailwindcss/oxide": "4.3.0", - "postcss": "^8.5.10", - "tailwindcss": "4.3.0" + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "postcss": "8.5.15", + "tailwindcss": "4.3.1" } }, "node_modules/@testing-library/jest-dom": { @@ -12313,9 +12313,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", - "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -21591,9 +21591,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", - "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", "dev": true, "license": "MIT" }, @@ -23390,10 +23390,10 @@ "prosemirror-state": "^1.4.3", "prosemirror-view": "^1.34.3", "radix-ui": "^1.4.3", - "react": "19.2.4", + "react": "^19.2.0", "react-data-grid": "7.0.0-beta.47", "react-day-picker": "^10.0.0", - "react-dom": "19.2.4", + "react-dom": "^19.2.0", "react-resizable-panels": "^4.11.0", "recharts": "^3.8.0", "shiki": "^3.23.0", diff --git a/package.json b/package.json index cdb38524..e0abb3d8 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,7 @@ "esbuild": ">=0.25.0", "zod": "4.4.3", "postcss": "^8.5.10", - "@mastra/core": "1.42.0-alpha.3", - "react": "19.2.7", - "react-dom": "19.2.7" + "@mastra/core": "1.42.0-alpha.3" }, "devDependencies": { "@biomejs/biome": "^2.4.12", diff --git a/packages/web/package.json b/packages/web/package.json index dcf8ca67..e936c01b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -58,10 +58,10 @@ "prosemirror-state": "^1.4.3", "prosemirror-view": "^1.34.3", "radix-ui": "^1.4.3", - "react": "19.2.4", + "react": "^19.2.0", "react-data-grid": "7.0.0-beta.47", "react-day-picker": "^10.0.0", - "react-dom": "19.2.4", + "react-dom": "^19.2.0", "react-resizable-panels": "^4.11.0", "recharts": "^3.8.0", "shiki": "^3.23.0",