You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #1813 (closed — the async/scheduled chat-generation migration onto runAgentWorkflow, functionally complete and fully on prod as of 2026-06-24). The cutover works end-to-end, but it left two deferred items that this issue tracks: (1) the new sandbox agent has no way to send email, so scheduled report tasks complete without delivering anything; and (2) the now-dormant OpenClaw code still sits in the tasks repo (Phase 2 cleanup, originally gated on prod stability — that gate is now met).
Goal
After #1813, scheduled chat-generation runs entirely on POST /api/chat/runs → runAgentWorkflow with native in-sandbox tools (bash/read/write/edit/grep/glob) + a per-run ephemeral key. Two things remain before the scheduled-report use case is whole and the legacy code is gone:
The sandbox agent can send email via a real Recoup API endpoint (it can't today), so report tasks actually deliver.
The OpenClaw bootstrap + run-sandbox-command Trigger.dev task are deleted from tasks (nothing calls them post-cutover).
Done = a scheduled task whose prompt is "email X to Y" results in a delivered email, observable end-to-end; and grep for OpenClaw / run-sandbox-command in tasks/src returns nothing but intentional history.
correct the skill to the shipped api (chat_id, recipient note, auth wording) + self-contained inline auth so the sandbox agent actually authenticates (preview test showed it reached for the unset $RECOUP_API_KEY)
toand subject optional in sendEmailBodySchema — resolve account_emails when to omitted; resolveEmailSubject() defaults subject from the body (else Message from Recoup)
test
✅ merged 2026-06-25 to test (18d20f7c) + promoted to main/prod (#714) — see Done
agent-reliability fix: force bash+curl for send-email (forbid web_fetch), lock payload keys, to as array, + collapse the dual x-api-key/Bearer auth to one inline Bearer header (net deletion)
main
✅ merged 2026-06-26 — measured 2/5 → 4/5, web_fetch 3/5 → 0, zero misroutes (see Done)
observability: Admin Telegram notification on every email sent (notifyEmailSent, best-effort) — stacked on api#717
test
🔄 open — 11 emails tests green
Follow-up bundle (the last 5 rows): two items, each merged docs → api. Item A (to + subject optional): ✅ done on prod — docs#252 + api#710 (#714). Item B (delete /api/notifications): ✅ done on prod — docs#253 + api#711 (promoted via api#715) + the CLI re-point cli#24 (published @recoupable/cli@0.1.15) + publish-workflow fix cli#25. The CLI was published before the prod promotion, so no caller broke.
Merge sequencing / status: Item 1's contract → endpoint chain is shipped to prod: docs#251 ✅ → api#708 ✅ (#709) → skills#57 ✅ + skills#59 ✅ (skill content now correct). Item 2 (tasks#153) ✅ merged. The skill install fix api#712 is now ✅ on prod (#713) too — so endpoint + skill content + skill install are all live. The remaining blocker is agent reliability: across three preview runs the agent loaded the skill and reached /api/emails but fumbled the call (web_fetch instead of bash+curl, to as a string, cross-env key). The endpoint+auth chain is proven (two emails delivered via the test endpoint). See the top Open — next up item.
What shipped (context) — the ephemeral-key foundation works
The per-run ephemeral key is verified working end-to-end, so Item 1 builds on solid ground (the gap is purely "no email tool", not auth):
The sandbox agent's shell receives the minted key as $RECOUP_ACCESS_TOKEN (a recoup_sk_… key, len 53), and it authenticates to the Recoup API: GET /api/accounts/id → 200, resolving to the correct accountId.
The key is revoked on run end (0 ephemeral:* keys linger for the account afterward).
Env-matching holds: a prod-minted token is valid against prod (200) and rejected by test-recoup-api.vercel.app (401) — same PRIVY_PROJECT_SECRET + DB required.
docs#251 — POST /api/emails send-email contract (the docs lead the api).
✅ Merged 2026-06-25 to main (merge commit 4321426; docs is single-branch). First in the docs → api → skills order — this is the contract api#708 fulfills and skills#57 documents.
Documents POST /api/emails in api-reference/openapi/accounts.json (SendEmailRequest + EmailResponse), a Send Email MDX page, and the Emails nav group. Scope evolved in review: (1) the request field was renamed room_id → chat_id to match the new session/chat architecture (the footer already links to chat.recoupable.com/chat/{id}), with a hyperlink to Create Chat (POST /api/chats) so devs know where to mint one; (2) to/cc are documented as restricted to the account's own email unless a payment method is on file, linking to the credits top-up session (POST /api/credits/sessions); (3) description trimmed (KISS — dropped the Resend/third-party mention); (4) to gets minItems: 1; (5) EmailResponse{ success, message, id } marked required.
Verified: accounts.json on main at 4321426 carries POST /api/emails with chat_id (not room_id), both recipient-restriction notes, and the required EmailResponse fields; JSON validates. CodeRabbit's create.mdxopenapi-frontmatter suggestion was skipped — the repo convention is "/api-reference/openapi/<spec>.json METHOD /path" (≈26 pages), which the page already follows; the bare form would break the spec ref (reason posted).
api#708 — POST /api/emails endpoint, fulfilling the docs#251 contract.
✅ Merged 2026-06-25 to test (merge commit 4ecca444), then promoted to main/prod via api#709 (test→main, merge commit f56e35d7); test re-synced with main afterward (27f05322).
Adds app/api/emails (reuses processAndSendEmail → the shared send_email MCP domain fn, untouched). Contract parity with docs#251: public field chat_id (handler maps it to the internal room_id footer lookup, so the rooms-table plumbing is unchanged), and the recipient restriction — without a payment method on file, to/cc are limited to the account's own email (403), lifted by a card on file (assertRecipientsAllowed + accountHasPaymentMethod, the latter shared with ensureSongstatsPaymentMethod). Review hardening: the recipient check + auth live in validateSendEmailBody (SRP); and the server now accepts a recoup_sk_ key over Authorization: Bearer (getAuthenticatedAccountId → shared getAccountIdByApiKey), so buildRecoupExecEnv carries a single RECOUP_ACCESS_TOKEN (no client-side x-api-key routing).
Verified end-to-end on the preview (a1a7270c, the approved commit): /api/emails → 401 (no auth) / 400 (minItems) / 200 (own email + chat_id, delivered) / 200 (shared@recoupable.com, card-on-file lifts the restriction); and a minted recoup_sk_ key returns 200 over both x-api-key and Authorization: Bearer (the new path, was 401), email send over Bearer 200, test key deleted after. 427 unit tests green; tsc 0 new errors. Results. Blocks nothing on the api side, but the sandbox path needs skills#57 (below): buildRecoupExecEnv no longer sets RECOUP_API_KEY, so the skill must use Authorization: Bearer $RECOUP_ACCESS_TOKEN.
skills#57 — document POST /api/emails in recoup-platform-api-access.
✅ Merged 2026-06-25 to main (recoupable/skills is single-branch). Adds the Send-email section + curl to the recoup-platform-api-access skill so the agent has the /api/emails playbook. Caveat — merged against the pre-merge contract; corrected by skills#59 (next). This skill only reaches the sandbox once api#712 fixes the install refs (below).
skills#59 — correct the skill to the shipped api + self-contained inline auth.
✅ Merged 2026-06-25 to main (merge commit 93396094; single-branch).
Brings recoup-platform-api-access to parity with the shipped /api/emails: room_id → chat_id, the no-card recipient restriction, and the auth note (the key authenticates over x-api-keyorAuthorization: Bearer). Plus the delivery-critical part, proven necessary by the 2026-06-25 api#712 preview test: a self-contained inline-auth curl. In that test the agent bypassed the top-of-skill ${AUTH[@]} helper and hand-rolled Bearer $RECOUP_API_KEY (unset in the sandbox → auth failed, no email) because each agent bash call is a fresh shell, so an AUTH set in a prior step is gone. Tech322/artist setting #59 makes the email curl resolve auth inline in one command (x-api-key when RECOUP_API_KEY set, else Bearer $RECOUP_ACCESS_TOKEN), working in both sandbox + local contexts with no cross-step state and no duplicated env var. So Tech322/artist setting #59 is not just doc accuracy — it's part of the delivery fix.
Re-test pending: needs api#712 on the deployment so the corrected skill actually installs into the sandbox.
api#712 — fix the renamed global-skill refs so the sandbox actually installs the platform skills.
✅ Merged 2026-06-25 to test (merge commit 6d9e74e8), then promoted to main/prod via api#713. defaultGlobalSkillRefs.ts installed {skillName:"recoup-api"} + {skillName:"artist-workspace"} — both renamed/split in recoupable/skills, so npx skills add --skill recoup-api threw and no platform skills landed in the sandbox (broke all platform-skill loading, not just email). Now installs the real slugs (recoup-platform-api-access + recoup-platform-build-workspace + recoup-roster-{add,list,manage}-artist); recoupApiSkillPrompt.ts updated to the new names + send-email triggers. 569 tests green; tsc 0 new errors.
Verified on the preview (results): the agent's behavior flipped from "I don't have a tool to send emails" (zero tool calls) → loads the skill and calls POST /api/emails. The skill-install fix is confirmed working. (Email delivery itself was then blocked first by a preview↔prod key-secret mismatch, then by agent-reliability fumbles — see Open — next up; the endpoint+auth chain itself is proven, with two emails delivered via the test endpoint over both headers.)
tasks#153 — retire the dead run-sandbox-command task (OpenClaw Phase 2 cleanup).
✅ Merged 2026-06-25 to main (merge commit 52b2a772; tasks is single-branch).
Deletes src/tasks/runSandboxCommandTask.ts + the code used exclusively by it (src/schemas/sandboxSchema.ts, src/sandboxes/appendNoCommitInstruction.ts, src/sandboxes/git/syncOrgRepos.ts, + their tests) and a stale getSandboxEnv JSDoc. The broader OpenClaw subsystem stays — it's still live via the coding-agent product (the issue's earlier "dormant" guess was wrong).
Verified: 355 tests pass; grep for the retired symbols in tasks/src is clean. Post-merge orphan audit (2026-06-25): reverse-dep-checked every helper the deleted task imported — none orphaned; each still roots in a live registered task (provisionSandbox → setup-sandbox; coding-agent; update-pr). No further dead code introduced by the deletion.
docs#252 — make to and subject optional on POST /api/emails (the contract for follow-up Item A).
✅ Merged 2026-06-25 to main (merge commit 3a025e4b; docs is single-branch).
Drops both to and subject from SendEmailRequest.required (it now has no required fields) and documents the server-side defaults: to → the authenticated account's own email; subject → the body's first heading/line, falling back to Message from Recoup. Resend itself still needs a non-empty subject (CreateEmailBaseOptions.subject: string), so the default is resolved server-side before the Resend call. CreateNotificationRequest untouched.
The api impl is api#710 (next Done item).
api#710 — make to and subject optional on POST /api/emails (the impl for follow-up Item A).
✅ Merged 2026-06-25 to test (merge commit 18d20f7c), then promoted to main/prod via api#714. sendEmailBodySchema now marks both to and subject optional. to omitted → resolves the account's own address(es) via selectAccountEmails (400 if none on file); the recipient restriction runs on the resolved recipients (own email always allowed without a card). subject omitted → resolveEmailSubject() returns the provided subject, else the body's first heading/line (text preferred, then HTML via firstMeaningfulLine/stripHtml), else Message from Recoup (120-char cap). The shared processAndSendEmail / MCP send_email / /api/notifications paths keep their own required-subject schemas — only /api/emails relaxes it.
Verified on the preview (31d885d5, post-SRP-refactor): POST /api/emails → 200 with to+subject (regression), 200 with to omitted (delivered to the account email), 200 with subject omitted (# Pulse Report body), 200 with {} (to self, Message from Recoup), 400 on a non-array to — 4 emails delivered, Resend ids recorded. SRP: firstMeaningfulLine + stripHtml extracted to their own files (review). 159 unit tests green; tsc 0 new errors. Results. Unblocks cli#24 — the recoup emails re-point to /api/emails (self-send, no to) now has its dependency live on prod.
docs#253 — remove POST /api/notifications from the docs (the contract for follow-up Item B).
✅ Merged 2026-06-25 to main (merge commit b6c20e9d; docs is single-branch). The docs-first contract for deleting the superseded endpoint.
Removes the /api/notifications path, the CreateNotificationRequest / NotificationResponse schemas, the Notifications nav group, and the reference page; re-points the cli.mdx section to /api/emails and renames it ### emails (recoup emails, per review). 128 deletions; JSON valid. Next for Item B:api#711 deletes the route/handlers, but cli#24 must land before/with it so recoup emails keeps working.
Done — agent reliability (the scheduled-email goal works end-to-end)
The whole pipeline is on prod (endpoint, skill content, skill install) and the agent now reliably sends the email. Path proven 2026-06-25 (real run delivered to sweetmantech@gmail.com); reliability hardened + measured 2026-06-26.
Make the sandbox agent reliably send the email — skills#60 ✅.
✅ Merged 2026-06-26 to recoupable/skillsmain. The agent kept fumbling the call on claude-haiku-4.5 (used web_fetch instead of bash+curl, to as a string, invented recipients/email keys — which delivered only because optional-to silently defaults to self). skills#60 (a) forces bash+curl and forbids web_fetch, (b) locks the valid keys + mandates to as a JSON array + tells it to confirm the recipient in the response, and (c) collapsed the obsolete dual x-api-key/Bearer auth into one inline Bearer header (${RECOUP_API_KEY:-$RECOUP_ACCESS_TOKEN}) — a net deletion (the server accepts a recoup_sk_ key over Bearer since api#708), removing the ${AUTH[@]} array + the cross-shell gotcha that had spawned a second, conflicting auth recipe. Measured (N=5 per arm, prod, → a non-self recipient shared@recoupable.com so the optional-to default can't mask a misroute): correct-send 2/5 → 4/5; web_fetch3/5 → 0/5; payload misroutes 0; to-as-string now self-corrects to an array. Ship bar (≥4/5 + zero misroutes): met. Residual (optional, ~1/5): the model sometimes hardcodes Bearer $RECOUP_API_KEY (unset in the sandbox) instead of copying the ${RECOUP_API_KEY:-$RECOUP_ACCESS_TOKEN} fallback → 401, then gives up. Not a misroute. Next lever if we want >4/5: make the auth snippet more copy-exact, or use a stronger model than haiku-4.5 for the headless send. Tracked as a small follow-up, not a blocker.
Done — original send-email endpoint (superseded by the items above)
Why: the migrated runAgentWorkflow agent has native sandbox tools but no email capability — the old getGeneralAgent path used the MCP send_email tool, which the new agent lacks. Confirmed by a diagnostic run of the "Daily Hello World Email" scheduled task: the run completed but the agent replied "I don't have an email-sending tool available in my current toolset" and made zero tool calls → no email delivered. This blocks the core scheduled-report use case (reports email their output).
Fix:
Add a REST endpoint in api (e.g. POST /api/emails) that sends via the existing Resend integration, account-scoped from the caller's auth (validateAuthContext), so the agent can curl it with x-api-key: $RECOUP_ACCESS_TOKEN.
Document it in the recoup-api skill (recoupable/skills) so the agent knows the endpoint + payload and reaches for it.
Use the x-api-key header in the skill's curl examples, not Authorization: Bearer.$RECOUP_ACCESS_TOKEN carries a recoup_sk_ API key, but its env name + buildRecoupExecEnv JSDoc (api/lib/agent/tools/buildRecoupExecEnv.ts) imply a Privy JWT, and REST endpoints reject the key over Bearer (JWT path → 401) while accepting it via x-api-key (200). Either fix the docs/naming or have the skill always use x-api-key.
Done when: a scheduled task with a "send an email to …" prompt results in a delivered email, and the run's persisted chat shows the agent calling the new endpoint and getting a 2xx. (Bonus: the recoup-api skill no longer 401s by reaching for Bearer.)
PRs (merge docs → api → skills):
docs#251 — ✅ merged 2026-06-25 (the contract; see Done).
Charge credits for POST /api/emails. → api#717 (open)
Price decided 2026-06-25:1 credit ($0.01) per email. Resend's per-email cost is ≤ $0.0004 (cheapest paid tier: Pro $20 / 50,000; every tier well under $0.01), so by the "round up to $0.01 if below, else at-cost no-markup" rule → 1 credit. EMAIL_CREDIT_COST = 1.
Impl (matches the deep-research handler):ensureCreditsOrShortCircuit gates (402 + no send if the account can't cover it; auto-recharges via a card; does not deduct) → send → on success recordCreditDeduction({ creditsToDeduct: 1, source: "api" }) (atomic credits_usage debit + usage_events insert). Failed send (502) is not charged. Only /api/emails charges — the shared processAndSendEmail/MCP path is unchanged.
Original analysis below (still accurate):
Why: verified 2026-06-25 — the endpoint charges nothing. No credit/billing/x402/usage references anywhere in the /api/emails path (app/api/emails/route.ts, sendEmailHandler.ts, validateSendEmailBody.ts, processAndSendEmail.ts). So sends — including agent/scheduled sends — are currently free, unmetered surface area.
Fix: wire the existing credit primitive into the send path — deductCredits({ accountId, creditsToDeduct }) (api/lib/credits/deductCredits.ts, the same helper the research handlers use), after auth/validation and before/around processAndSendEmail. Pick a per-email credit cost, decide pre- vs post-send deduction and the insufficient-credits behavior (e.g. 402), and record it via recordCreditDeduction. Keep the MCP send_email path consistent if it should also charge.
Done when: a successful POST /api/emails deducts the chosen credit amount from the account; an account with insufficient credits is rejected (no send); the deduction is recorded in credits_usage / usage events.
Open — observability
Post an Admin Telegram notification on every email sent. → api#718 (open, stacked on api#717) Decided 2026-06-25 — so the team can review the quality and frequency of emails going out (esp. agent/scheduled sends).
Impl: new notifyEmailSent() posts account + to/cc + subject + Resend id + timestamp to the same TELEGRAM_CHAT_ID channel as other alerts (via the existing sendMessage), best-effort (try/catch, never blocks the send), wired into sendEmailHandler after a successful send.
Why: sends are currently invisible — no signal when /api/emails fires. We want a per-send ping in the Admin Telegram channel to spot bad/spammy/over-frequent sends.
Fix: on a successful send, call the existing primitive sendMessage(text, opts) (api/lib/telegram/sendMessage.ts → TELEGRAM_CHAT_ID via TELEGRAM_BOT_TOKEN; already used by the credit-spend digest). Post-send, best-effort / fire-and-forget (never fail or block the email if Telegram errors — wrap in try/catch like sendErrorNotification). Include: account id, to/cc, subject, Resend id, and a timestamp. Wire it in sendEmailHandler after processAndSendEmail succeeds; decide whether the shared processAndSendEmail (MCP send_email + any other caller) should notify too, or keep it /api/emails-only. Confirm the Admin channel id (reuse TELEGRAM_CHAT_ID or add a dedicated admin chat id).
Done when: each successful /api/emails send posts one message to the Admin Telegram channel with account + recipients + subject + Resend id; a Telegram failure does not affect the email response.
Done — cleanup (Item B: POST /api/notifications removed)
Delete the placeholder POST /api/notifications endpoint (docs + api + cli). ✅ Done on prod 2026-06-25 — docs#253 + api#711 (promoted via api#715) + cli#24 (published @recoupable/cli@0.1.15) + cli#25 (publish-workflow fix). The CLI was published before the prod promotion, so no caller 404'd. Superseded by POST /api/emails, which was built as its replacement — /api/notifications was the earlier placeholder (it emails the account's own address; /api/emails is the general send-email endpoint).
Why: two endpoints now cover the same ground; /api/emails is the contract we're standardizing on. Keeping /api/notifications is dead surface area.
Fix: remove the route + its handler/validator in api (app/api/notifications/, lib/notifications/), and the page + OpenAPI entry in docs. Keep processAndSendEmail — it's still the shared domain fn for the send_email MCP tool. Re-point anything that called /api/notifications to /api/emails.
🔻 Cross-repo caller found (2026-06-25): this is 3 PRs, not 2 — the clinotifications command (cli/src/commands/notifications.ts, registered in cli/src/bin.ts) is a live caller of POST /api/notifications, and docs/cli.mdx links its reference page. So api#711 must not merge before cli#24 or recoup notifications breaks. The cli command re-points to /api/emails with no to (self-send), so it also depends on api#710 (to-optional) being live.
PRs (docs → api, cli before/with the api deletion):
docs#253 — removes the /api/notifications path + CreateNotificationRequest/NotificationResponse schemas + the Notifications nav group + the reference page (128 deletions, JSON valid); re-points the cli.mdx link to /api/emails.
api#711 — deletes the route + lib/notifications/* (handler, validator, both tests; 598 deletions); keeps processAndSendEmail and fixes its stale JSDoc (/api/notifications → /api/emails). grep clean; lib/emails suite 126 green.
cli#24 — renames the command notifications → emails (no alias; git mv to src/commands/emails.ts, bin.ts updated), re-points to POST /api/emails, --chat-id (sends chat_id), no --to (self-send), and --subject now optional (matches api#710). 75 tests green; tsup build OK; recoup emails --help verified. ✅ Promotion gate CLEARED (2026-06-25):@recoupable/cli@0.1.15 is published and calls /api/emails (verified: 0 /api/notifications refs in the published dist/bin.cjs). The release needed a publish-workflow fix (cli#25) — the original run failed first on a stale NPM_TOKEN, then on a non-fast-forward bump push. So api#711 is now safe to promote to prod (api#715).
Done when:grep -r "api/notifications" api docs is clean (bar intentional history); the docs nav no longer lists it; processAndSendEmail + the MCP send_email tool still pass their suites; recoup notifications still delivers (via /api/emails).
Sequencing
Independent — but Item 1 (send-email) leads: it unblocks the user-facing scheduled-report value, while Item 2 is pure dead-code removal that can land whenever. Do Item 1 first; Item 2 any time after. The /api/notifications deletion is a tidy-up that should land before close (it depends on /api/emails being live).
Key files: api/lib/skills/defaultGlobalSkillRefs.ts (the renamed-skill-ref bug — global skills installed into every sandbox), api/lib/skills/installGlobalSkills.ts (npx skills add … --skill <name>, throws on unknown name), api/lib/chat/recoupApiSkillPrompt.ts (the nudge that tells the agent which skill to load), api/lib/agent/tools/buildRecoupExecEnv.ts (token injection), tasks/src/tasks/* (OpenClaw, mostly retired by feat: upload pfp to ipfs. #153).
Follow-up to #1813 (closed — the async/scheduled chat-generation migration onto
runAgentWorkflow, functionally complete and fully on prod as of 2026-06-24). The cutover works end-to-end, but it left two deferred items that this issue tracks: (1) the new sandbox agent has no way to send email, so scheduled report tasks complete without delivering anything; and (2) the now-dormant OpenClaw code still sits in thetasksrepo (Phase 2 cleanup, originally gated on prod stability — that gate is now met).Goal
After #1813, scheduled chat-generation runs entirely on
POST /api/chat/runs→runAgentWorkflowwith native in-sandbox tools (bash/read/write/edit/grep/glob) + a per-run ephemeral key. Two things remain before the scheduled-report use case is whole and the legacy code is gone:run-sandbox-commandTrigger.dev task are deleted fromtasks(nothing calls them post-cutover).Done = a scheduled task whose prompt is "email X to Y" results in a delivered email, observable end-to-end; and
grepforOpenClaw/run-sandbox-commandintasks/srcreturns nothing but intentional history.PRs (updated 2026-06-25)
POST /api/emailssend-email contract (OpenAPI +SendEmailRequest/EmailResponse+ nav)mainPOST /api/emailsendpoint (reusesprocessAndSendEmail) +chat_idrename + recipient restriction + server-siderecoup_sk_-over-Bearer authtestmain/prod (#709) — see DonePOST /api/emailsinrecoup-platform-api-accessmainrun-sandbox-commandtask + its exclusive code (OpenClaw Phase 2 cleanup)mainrecoup-platform-api-access(the real reason scheduled email doesn't deliver)testtest(6d9e74e8) + promoted tomain/prod (#713) — see Donechat_id, recipient note, auth wording) + self-contained inline auth so the sandbox agent actually authenticates (preview test showed it reached for the unset$RECOUP_API_KEY)maintoandsubjectoptional onPOST /api/emails(to→ account's own email;subject→ first body line, elseMessage from Recoup)maintoandsubjectoptional insendEmailBodySchema— resolveaccount_emailswhentoomitted;resolveEmailSubject()defaults subject from the body (elseMessage from Recoup)testtest(18d20f7c) + promoted tomain/prod (#714) — see DonePOST /api/notifications(path +CreateNotificationRequest/NotificationResponse+ nav + page)main/api/notificationsroute +lib/notifications/*(keepprocessAndSendEmail)testtest(d9815368) + promoted tomain/prod (#715)notificationscommand →emails(no alias) + re-point toPOST /api/emails(self-send);--subjectoptional;--chat-id→chat_idmain@recoupable/cli@0.1.15(calls/api/emails, 0/api/notificationsrefs)ref: main,workflow_dispatch) — fixed the non-fast-forward release failuremainbash+curlfor send-email (forbidweb_fetch), lock payload keys,toas array, + collapse the dualx-api-key/Bearer auth to one inline Bearer header (net deletion)mainweb_fetch3/5 → 0, zero misroutes (see Done)POST /api/emailssend (gate→send→deduct;recordCreditDeductionsourceapi)testnotifyEmailSent, best-effort) — stacked on api#717testWhat shipped (context) — the ephemeral-key foundation works
The per-run ephemeral key is verified working end-to-end, so Item 1 builds on solid ground (the gap is purely "no email tool", not auth):
$RECOUP_ACCESS_TOKEN(arecoup_sk_…key, len 53), and it authenticates to the Recoup API:GET /api/accounts/id→ 200, resolving to the correctaccountId.ephemeral:*keys linger for the account afterward).200) and rejected bytest-recoup-api.vercel.app(401) — samePRIVY_PROJECT_SECRET+ DB required.Done
docs#251 —
POST /api/emailssend-email contract (the docs lead the api).✅ Merged 2026-06-25 to
main(merge commit4321426;docsis single-branch). First in the docs → api → skills order — this is the contract api#708 fulfills and skills#57 documents.Documents
POST /api/emailsinapi-reference/openapi/accounts.json(SendEmailRequest+EmailResponse), aSend EmailMDX page, and theEmailsnav group. Scope evolved in review: (1) the request field was renamedroom_id→chat_idto match the new session/chat architecture (the footer already links tochat.recoupable.com/chat/{id}), with a hyperlink to Create Chat (POST /api/chats) so devs know where to mint one; (2)to/ccare documented as restricted to the account's own email unless a payment method is on file, linking to the credits top-up session (POST /api/credits/sessions); (3) description trimmed (KISS — dropped the Resend/third-party mention); (4)togetsminItems: 1; (5)EmailResponse{ success, message, id }marked required.Verified:
accounts.jsononmainat4321426carriesPOST /api/emailswithchat_id(notroom_id), both recipient-restriction notes, and the requiredEmailResponsefields; JSON validates. CodeRabbit'screate.mdxopenapi-frontmatter suggestion was skipped — the repo convention is"/api-reference/openapi/<spec>.json METHOD /path"(≈26 pages), which the page already follows; the bare form would break the spec ref (reason posted).api#708 —
POST /api/emailsendpoint, fulfilling the docs#251 contract.✅ Merged 2026-06-25 to
test(merge commit4ecca444), then promoted tomain/prod via api#709 (test→main, merge commitf56e35d7);testre-synced withmainafterward (27f05322).Adds
app/api/emails(reusesprocessAndSendEmail→ the sharedsend_emailMCP domain fn, untouched). Contract parity with docs#251: public fieldchat_id(handler maps it to the internalroom_idfooter lookup, so therooms-table plumbing is unchanged), and the recipient restriction — without a payment method on file,to/ccare limited to the account's own email (403), lifted by a card on file (assertRecipientsAllowed+accountHasPaymentMethod, the latter shared withensureSongstatsPaymentMethod). Review hardening: the recipient check + auth live invalidateSendEmailBody(SRP); and the server now accepts arecoup_sk_key overAuthorization: Bearer(getAuthenticatedAccountId→ sharedgetAccountIdByApiKey), sobuildRecoupExecEnvcarries a singleRECOUP_ACCESS_TOKEN(no client-side x-api-key routing).Verified end-to-end on the preview (
a1a7270c, the approved commit):/api/emails→ 401 (no auth) / 400 (minItems) / 200 (own email +chat_id, delivered) / 200 (shared@recoupable.com, card-on-file lifts the restriction); and a mintedrecoup_sk_key returns 200 over bothx-api-keyandAuthorization: Bearer(the new path, was 401), email send over Bearer 200, test key deleted after. 427 unit tests green; tsc 0 new errors. Results.Blocks nothing on the api side, but the sandbox path needs skills#57 (below):
buildRecoupExecEnvno longer setsRECOUP_API_KEY, so the skill must useAuthorization: Bearer $RECOUP_ACCESS_TOKEN.skills#57 — document
POST /api/emailsinrecoup-platform-api-access.✅ Merged 2026-06-25 to
main(recoupable/skillsis single-branch). Adds the Send-email section + curl to therecoup-platform-api-accessskill so the agent has the/api/emailsplaybook.Caveat — merged against the pre-merge contract; corrected by skills#59 (next). This skill only reaches the sandbox once api#712 fixes the install refs (below).
skills#59 — correct the skill to the shipped api + self-contained inline auth.
✅ Merged 2026-06-25 to
main(merge commit93396094; single-branch).Brings
recoup-platform-api-accessto parity with the shipped/api/emails:room_id→chat_id, the no-card recipient restriction, and the auth note (the key authenticates overx-api-keyorAuthorization: Bearer). Plus the delivery-critical part, proven necessary by the 2026-06-25 api#712 preview test: a self-contained inline-auth curl. In that test the agent bypassed the top-of-skill${AUTH[@]}helper and hand-rolledBearer $RECOUP_API_KEY(unset in the sandbox → auth failed, no email) because each agent bash call is a fresh shell, so anAUTHset in a prior step is gone. Tech322/artist setting #59 makes the email curl resolve auth inline in one command (x-api-keywhenRECOUP_API_KEYset, elseBearer $RECOUP_ACCESS_TOKEN), working in both sandbox + local contexts with no cross-step state and no duplicated env var. So Tech322/artist setting #59 is not just doc accuracy — it's part of the delivery fix.Re-test pending: needs api#712 on the deployment so the corrected skill actually installs into the sandbox.
api#712 — fix the renamed global-skill refs so the sandbox actually installs the platform skills.
✅ Merged 2026-06-25 to
test(merge commit6d9e74e8), then promoted tomain/prod via api#713.defaultGlobalSkillRefs.tsinstalled{skillName:"recoup-api"}+{skillName:"artist-workspace"}— both renamed/split inrecoupable/skills, sonpx skills add --skill recoup-apithrew and no platform skills landed in the sandbox (broke all platform-skill loading, not just email). Now installs the real slugs (recoup-platform-api-access+recoup-platform-build-workspace+recoup-roster-{add,list,manage}-artist);recoupApiSkillPrompt.tsupdated to the new names + send-email triggers. 569 tests green; tsc 0 new errors.Verified on the preview (results): the agent's behavior flipped from "I don't have a tool to send emails" (zero tool calls) → loads the skill and calls
POST /api/emails. The skill-install fix is confirmed working. (Email delivery itself was then blocked first by a preview↔prod key-secret mismatch, then by agent-reliability fumbles — see Open — next up; the endpoint+auth chain itself is proven, with two emails delivered via the test endpoint over both headers.)tasks#153 — retire the dead
run-sandbox-commandtask (OpenClaw Phase 2 cleanup).✅ Merged 2026-06-25 to
main(merge commit52b2a772;tasksis single-branch).Deletes
src/tasks/runSandboxCommandTask.ts+ the code used exclusively by it (src/schemas/sandboxSchema.ts,src/sandboxes/appendNoCommitInstruction.ts,src/sandboxes/git/syncOrgRepos.ts, + their tests) and a stalegetSandboxEnvJSDoc. The broader OpenClaw subsystem stays — it's still live via the coding-agent product (the issue's earlier "dormant" guess was wrong).Verified: 355 tests pass;
grepfor the retired symbols intasks/srcis clean. Post-merge orphan audit (2026-06-25): reverse-dep-checked every helper the deleted task imported — none orphaned; each still roots in a live registered task (provisionSandbox→setup-sandbox;coding-agent;update-pr). No further dead code introduced by the deletion.docs#252 — make
toandsubjectoptional onPOST /api/emails(the contract for follow-up Item A).✅ Merged 2026-06-25 to
main(merge commit3a025e4b;docsis single-branch).Drops both
toandsubjectfromSendEmailRequest.required(it now has no required fields) and documents the server-side defaults:to→ the authenticated account's own email;subject→ the body's first heading/line, falling back toMessage from Recoup. Resend itself still needs a non-empty subject (CreateEmailBaseOptions.subject: string), so the default is resolved server-side before the Resend call.CreateNotificationRequestuntouched.The api impl is api#710 (next Done item).
api#710 — make
toandsubjectoptional onPOST /api/emails(the impl for follow-up Item A).✅ Merged 2026-06-25 to
test(merge commit18d20f7c), then promoted tomain/prod via api#714.sendEmailBodySchemanow marks bothtoandsubjectoptional.toomitted → resolves the account's own address(es) viaselectAccountEmails(400 if none on file); the recipient restriction runs on the resolved recipients (own email always allowed without a card).subjectomitted →resolveEmailSubject()returns the provided subject, else the body's first heading/line (text preferred, then HTML viafirstMeaningfulLine/stripHtml), elseMessage from Recoup(120-char cap). The sharedprocessAndSendEmail/ MCPsend_email//api/notificationspaths keep their own required-subject schemas — only/api/emailsrelaxes it.Verified on the preview (
31d885d5, post-SRP-refactor):POST /api/emails→ 200 withto+subject(regression), 200 withtoomitted (delivered to the account email), 200 withsubjectomitted (# Pulse Reportbody), 200 with{}(to self,Message from Recoup), 400 on a non-arrayto— 4 emails delivered, Resend ids recorded. SRP:firstMeaningfulLine+stripHtmlextracted to their own files (review). 159 unit tests green; tsc 0 new errors. Results.Unblocks cli#24 — the
recoup emailsre-point to/api/emails(self-send, noto) now has its dependency live on prod.docs#253 — remove
POST /api/notificationsfrom the docs (the contract for follow-up Item B).✅ Merged 2026-06-25 to
main(merge commitb6c20e9d;docsis single-branch). The docs-first contract for deleting the superseded endpoint.Removes the
/api/notificationspath, theCreateNotificationRequest/NotificationResponseschemas, the Notifications nav group, and the reference page; re-points thecli.mdxsection to/api/emailsand renames it### emails(recoup emails, per review). 128 deletions; JSON valid.Next for Item B: api#711 deletes the route/handlers, but cli#24 must land before/with it so
recoup emailskeeps working.Done — agent reliability (the scheduled-email goal works end-to-end)
The whole pipeline is on prod (endpoint, skill content, skill install) and the agent now reliably sends the email. Path proven 2026-06-25 (real run delivered to
sweetmantech@gmail.com); reliability hardened + measured 2026-06-26.✅ Merged 2026-06-26 to
recoupable/skillsmain. The agent kept fumbling the call onclaude-haiku-4.5(usedweb_fetchinstead ofbash+curl,toas a string, inventedrecipients/emailkeys — which delivered only because optional-tosilently defaults to self). skills#60 (a) forcesbash+curland forbidsweb_fetch, (b) locks the valid keys + mandatestoas a JSON array + tells it to confirm the recipient in the response, and (c) collapsed the obsolete dualx-api-key/Bearer auth into one inline Bearer header (${RECOUP_API_KEY:-$RECOUP_ACCESS_TOKEN}) — a net deletion (the server accepts arecoup_sk_key over Bearer since api#708), removing the${AUTH[@]}array + the cross-shell gotcha that had spawned a second, conflicting auth recipe.Measured (N=5 per arm, prod, → a non-self recipient
shared@recoupable.comso the optional-todefault can't mask a misroute): correct-send 2/5 → 4/5;web_fetch3/5 → 0/5; payload misroutes 0;to-as-string now self-corrects to an array. Ship bar (≥4/5 + zero misroutes): met.Residual (optional, ~1/5): the model sometimes hardcodes
Bearer $RECOUP_API_KEY(unset in the sandbox) instead of copying the${RECOUP_API_KEY:-$RECOUP_ACCESS_TOKEN}fallback → 401, then gives up. Not a misroute. Next lever if we want >4/5: make the auth snippet more copy-exact, or use a stronger model thanhaiku-4.5for the headless send. Tracked as a small follow-up, not a blocker.Done — original send-email endpoint (superseded by the items above)
runAgentWorkflowagent has native sandbox tools but no email capability — the oldgetGeneralAgentpath used the MCPsend_emailtool, which the new agent lacks. Confirmed by a diagnostic run of the "Daily Hello World Email" scheduled task: the run completed but the agent replied "I don't have an email-sending tool available in my current toolset" and made zero tool calls → no email delivered. This blocks the core scheduled-report use case (reports email their output).api(e.g.POST /api/emails) that sends via the existing Resend integration, account-scoped from the caller's auth (validateAuthContext), so the agent cancurlit withx-api-key: $RECOUP_ACCESS_TOKEN.recoup-apiskill (recoupable/skills) so the agent knows the endpoint + payload and reaches for it.x-api-keyheader in the skill's curl examples, notAuthorization: Bearer.$RECOUP_ACCESS_TOKENcarries arecoup_sk_API key, but its env name +buildRecoupExecEnvJSDoc (api/lib/agent/tools/buildRecoupExecEnv.ts) imply a Privy JWT, and REST endpoints reject the key over Bearer (JWT path → 401) while accepting it viax-api-key(200). Either fix the docs/naming or have the skill always usex-api-key.POST /api/emails+ the self-contained inline auth; see Done). The skill install fix (renamed refs → api#712) is now ✅ on prod (Sweetmantech/myc 2202 apicronrecoupdailystats daily cron at 9am et once daily #713); the remaining blocker is agent reliability — see the top Open — next up item.Open — billing
POST /api/emails. → api#717 (open)EMAIL_CREDIT_COST = 1.ensureCreditsOrShortCircuitgates (402 + no send if the account can't cover it; auto-recharges via a card; does not deduct) → send → on successrecordCreditDeduction({ creditsToDeduct: 1, source: "api" })(atomiccredits_usagedebit +usage_eventsinsert). Failed send (502) is not charged. Only/api/emailscharges — the sharedprocessAndSendEmail/MCP path is unchanged.x402/usagereferences anywhere in the/api/emailspath (app/api/emails/route.ts,sendEmailHandler.ts,validateSendEmailBody.ts,processAndSendEmail.ts). So sends — including agent/scheduled sends — are currently free, unmetered surface area.deductCredits({ accountId, creditsToDeduct })(api/lib/credits/deductCredits.ts, the same helper theresearchhandlers use), after auth/validation and before/aroundprocessAndSendEmail. Pick a per-email credit cost, decide pre- vs post-send deduction and the insufficient-credits behavior (e.g. 402), and record it viarecordCreditDeduction. Keep the MCPsend_emailpath consistent if it should also charge.POST /api/emailsdeducts the chosen credit amount from the account; an account with insufficient credits is rejected (no send); the deduction is recorded incredits_usage/ usage events.Open — observability
notifyEmailSent()posts account +to/cc+ subject + Resend id + timestamp to the sameTELEGRAM_CHAT_IDchannel as other alerts (via the existingsendMessage), best-effort (try/catch, never blocks the send), wired intosendEmailHandlerafter a successful send./api/emailsfires. We want a per-send ping in the Admin Telegram channel to spot bad/spammy/over-frequent sends.sendMessage(text, opts)(api/lib/telegram/sendMessage.ts→TELEGRAM_CHAT_IDviaTELEGRAM_BOT_TOKEN; already used by the credit-spend digest). Post-send, best-effort / fire-and-forget (never fail or block the email if Telegram errors — wrap in try/catch likesendErrorNotification). Include: account id,to/cc, subject, Resend id, and a timestamp. Wire it insendEmailHandlerafterprocessAndSendEmailsucceeds; decide whether the sharedprocessAndSendEmail(MCPsend_email+ any other caller) should notify too, or keep it/api/emails-only. Confirm the Admin channel id (reuseTELEGRAM_CHAT_IDor add a dedicated admin chat id)./api/emailssend posts one message to the Admin Telegram channel with account + recipients + subject + Resend id; a Telegram failure does not affect the email response.Done — cleanup (Item B:
POST /api/notificationsremoved)POST /api/notificationsendpoint (docs + api + cli). ✅ Done on prod 2026-06-25 — docs#253 + api#711 (promoted via api#715) + cli#24 (published@recoupable/cli@0.1.15) + cli#25 (publish-workflow fix). The CLI was published before the prod promotion, so no caller 404'd. Superseded byPOST /api/emails, which was built as its replacement —/api/notificationswas the earlier placeholder (it emails the account's own address;/api/emailsis the general send-email endpoint)./api/emailsis the contract we're standardizing on. Keeping/api/notificationsis dead surface area.api(app/api/notifications/,lib/notifications/), and the page + OpenAPI entry indocs. KeepprocessAndSendEmail— it's still the shared domain fn for thesend_emailMCP tool. Re-point anything that called/api/notificationsto/api/emails.clinotificationscommand (cli/src/commands/notifications.ts, registered incli/src/bin.ts) is a live caller ofPOST /api/notifications, anddocs/cli.mdxlinks its reference page. So api#711 must not merge before cli#24 orrecoup notificationsbreaks. The cli command re-points to/api/emailswith noto(self-send), so it also depends on api#710 (to-optional) being live./api/notificationspath +CreateNotificationRequest/NotificationResponseschemas + the Notifications nav group + the reference page (128 deletions, JSON valid); re-points thecli.mdxlink to/api/emails.lib/notifications/*(handler, validator, both tests; 598 deletions); keepsprocessAndSendEmailand fixes its stale JSDoc (/api/notifications→/api/emails). grep clean;lib/emailssuite 126 green.notifications→emails(no alias;git mvtosrc/commands/emails.ts,bin.tsupdated), re-points toPOST /api/emails,--chat-id(sendschat_id), no--to(self-send), and--subjectnow optional (matches api#710). 75 tests green; tsup build OK;recoup emails --helpverified. ✅ Promotion gate CLEARED (2026-06-25):@recoupable/cli@0.1.15is published and calls/api/emails(verified: 0/api/notificationsrefs in the publisheddist/bin.cjs). The release needed a publish-workflow fix (cli#25) — the original run failed first on a staleNPM_TOKEN, then on a non-fast-forward bump push. So api#711 is now safe to promote to prod (api#715).grep -r "api/notifications" api docsis clean (bar intentional history); the docs nav no longer lists it;processAndSendEmail+ the MCPsend_emailtool still pass their suites;recoup notificationsstill delivers (via/api/emails).Sequencing
Independent — but Item 1 (send-email) leads: it unblocks the user-facing scheduled-report value, while Item 2 is pure dead-code removal that can land whenever. Do Item 1 first; Item 2 any time after. The
/api/notificationsdeletion is a tidy-up that should land before close (it depends on/api/emailsbeing live).Source references
api/lib/skills/defaultGlobalSkillRefs.ts(the renamed-skill-ref bug — global skills installed into every sandbox),api/lib/skills/installGlobalSkills.ts(npx skills add … --skill <name>, throws on unknown name),api/lib/chat/recoupApiSkillPrompt.ts(the nudge that tells the agent which skill to load),api/lib/agent/tools/buildRecoupExecEnv.ts(token injection),tasks/src/tasks/*(OpenClaw, mostly retired by feat: upload pfp to ipfs. #153).