Skip to content

Migrate async chat generation (/api/chat/generate) onto runAgentWorkflow + native sandbox; remove OpenClaw run-sandbox-command #1813

Description

@sweetmantech

Tracking issue for converging the async / scheduled chat-generation path onto the same durable Vercel Workflow + native-sandbox architecture that interactive /api/chat already runs in prod, and retiring the OpenClaw run-sandbox-command Trigger.dev task. Scheduling stays in Trigger.dev; generation/agent work moves into api.

Why now: the scheduled weekly-report path is currently failing for every account. customer-prompt-taskPOST /api/chat/generate (legacy ToolLoopAgent) → the agent calls the prompt_sandbox MCP tool → tasks.trigger("run-sandbox-command") → OpenClaw bootstrap in the sandbox, which exits non-zero. The agent degrades gracefully and emails a partial report, so it looks successful but ships incomplete data. The OpenClaw layer is the failure surface; rather than patch soon-to-be-deprecated tasks code, we remove it by unifying onto the architecture we've already shipped.

Goal

Async chat generation runs on the same runAgentWorkflow durable workflow as interactive /api/chat — an AI SDK agent (streamText on @ai-sdk/gateway) with native in-sandbox tools (bash/read/write/edit/grep/glob) executing inside a Vercel Workflow. Concretely, when done:

  • POST /api/chat/generate provisions/attaches a session+sandbox and start()s runAgentWorkflow instead of running a synchronous ToolLoopAgent (api/lib/chat/handleChatGenerate.ts).
  • The prompt_sandbox MCP tool → processCreateSandboxtriggerPromptSandboxrun-sandbox-command bridge is gone — the agent has native sandbox tools in-workflow.
  • OpenClaw (install/onboard/gateway/skills/model-config) and the run-sandbox-command task are deleted from tasks.
  • Trigger.dev keeps only scheduling/light orchestration (customer-prompt-task + createSchedule/updateSchedule); no generation logic remains in tasks.

PRs (updated 2026-06-24)

PR Item Base State
database#36 add account_api_keys.expires_at (ephemeral-key TTL) main ✅ merged 2026-06-24 — see Done
api#700 enforce account_api_keys.expires_at in x-api-key auth (getApiKeyAccountId) test ✅ merged + promoted to main/prod 2026-06-24 — see Done
api#701 extract shared buildRunAgentInput() (behavior-preserving) test ✅ merged + promoted to main/prod 2026-06-24 — see Done
docs#249 rename → POST /api/chat/runs (202 { runId, chatId, sessionId }) + GET /api/chat/runs/{runId} status contract main ✅ merged 2026-06-24 — see Done
api#704 re-point /api/chat/runs → provision + mint + start(runAgentWorkflow) test ✅ merged + promoted to main/prod 2026-06-24 — see Done
api#705 retire OpenClaw prompt_sandboxrun-sandbox-command bridge test ✅ merged + promoted to main/prod 2026-06-24 — see Done
tasks#152 generateChat fire-and-forget (consume 202) main ✅ merged 2026-06-24 to main — see Done

Implementation PRs (2026-06-24): merge order docs#249 ✅ → api#704 → api#705 → tasks#152. docs#249 (the contract) is merged to main; the endpoint was renamed during review from /api/chat/generate to POST /api/chat/runs (REST: it creates a run), returns 202 { runId, chatId, sessionId } (so the result is retrievable, not fire-and-forget-only), and documents GET /api/chat/runs/{runId} (status). The status endpoint is implemented in api#704 and preview-verified (runningcompleted); its response was reconciled to { runId, status } via docs#250 (merged main, 83754b8) — getRun(runId) exposes only status and the caller already holds chatId/sessionId from the 202, so they're not echoed (full chatId/sessionId + per-run ownership would need a chats.last_run_id column, deferred). api#704/#705/tasks#152 were re-pointed to /api/chat/runs to match; api#704 is preview-verified end-to-end. Phase 2 (delete OpenClaw from tasks) remains not opened — gated on prod stability.

Scope correction (2026-06-24): api#700 was narrowed to expiry enforcement only. The mintEphemeralAccountKey helper + the insertApiKey expires_at writer change were removed from it (commit 9302dde) — neither had a caller in that PR, so they'd ship as untestable dead code (violating the "every PR is a testable slice" rule). The minting code now lands with the re-point item below, which is its only consumer; it is preserved verbatim there so nothing is lost.

✅ Functional migration COMPLETE (2026-06-24). All seven implementation PRs are merged and on prod: the re-point (api#704), the OpenClaw bridge retirement (api#705), and the tasks fire-and-forget cutover (tasks#152). The scheduled path now runs entirely on runAgentWorkflow with native sandbox tools + a per-run ephemeral key (mint→inject→authenticate→revoke, verified end-to-end). Nothing triggers run-sandbox-command anymore. Only Phase 2 remains — deleting the now-dormant OpenClaw code (install/onboard/gateway/skills/model-config + the run-sandbox-command task) from the tasks repo — intentionally gated on the unified path proving stable in prod first. Two non-blocking fast-follows surfaced in testing: (1) the sandbox agent has no email-sending capability yet; (2) RECOUP_ACCESS_TOKEN carries an API key but is named/documented as a JWT, so the recoup-api skill should use the x-api-key header.

Done

  • database#36 — add account_api_keys.expires_at (ephemeral-key TTL).
    ✅ Merged 2026-06-24 to main (merge commit fada52e; database is single-branch — no test/main split). First PR in the docs→database→api→tasks order.
    Adds a nullable expires_at timestamptz to public.account_api_keys via ADD COLUMN IF NOT EXISTS — additive, backward-compatible, no data migration. NULL = never expires, so all existing long-lived keys are unaffected; a column COMMENT documents the semantics and points back here. Unblocks api#700, which enforces the TTL in getApiKeyAccountId (rejecting expired keys) as defense-in-depth behind the per-run delete-on-completion.
    Verified: merged migration supabase/migrations/20260623000000_add_expires_at_to_account_api_keys.sql present on main at fada52e (19 additions, single ALTER + COMMENT, no down-migration needed). Applying the column to the live/target DB is the operational step that gates the queued re-point PR (above).

  • api#700 — enforce account_api_keys.expires_at in x-api-key auth.
    ✅ Merged 2026-06-24 to test (merge commit 03e0ef5), then promoted to main/prod via api#702 (testmain, merge commit 6b62b74, 2026-06-24); enforcement confirmed live on main at getApiKeyAccountId.ts:53. Scope: enforcement only — isApiKeyExpired (pure TTL check; NULL/unparseable = never expires) wired into getApiKeyAccountId, which now returns 401 for a key past its expires_at. Minting was deliberately removed (see scope-correction note above) and moves to the re-point item below.
    Verified end-to-end on the preview deployment (api-git-feat-ephemeral-account-api-keys-recoup.vercel.app, commit 9302dde) against GET /api/accounts/id, by creating one real recoup_sk_ key in account_api_keys (prod DB godremdqwajrwazhbrue, which the preview reads; expires_at column confirmed present) and varying only expires_at: NULL200, future (+1h) → 200, past (−1h) → 401 (deterministic), row deleted → 401. The future→past flip on the same key isolates isApiKeyExpired as the cause; long-lived/NULL-expiry keys are unaffected. Test results comment. (Note: the preview's PRIVY_PROJECT_SECRET differs from local, so the stored hash must be computed with the preview secret.)

  • api#701 — extract shared buildRunAgentInput() (behavior-preserving).
    ✅ Merged 2026-06-24 to test (merge commit 78bd71b), then promoted to main/prod via api#703 (testmain, merge commit 0c94809, 2026-06-24); lib/chat/buildRunAgentInput.ts confirmed present on main. Extracts the RunAgentWorkflowInput construction out of handleChatWorkflowStream into one builder (repo ids + recoup org id both derived from clone_url inside it — single source of truth) so the interactive and headless callers stay in lockstep. The interactive path (handleChatWorkflowStream.ts:124) is the live consumer; the headless re-point PR will be the second.
    Verified end-to-end on the preview (api-git-feat-build-run-agent-input-recoup.vercel.app, head 5ee07e07) by driving the real interactive flow with a Privy Bearer token: POST /api/sessions → 200; POST /api/sandbox → 200 (ready 5.6s); POST /api/chat200 text/event-stream, x-workflow-run-id: wrun_01KVWZNM82NA7XKNEWWHG8VPHJ, streamed assistant text PR701 OK (model followed the instruction exactly; anthropic/claude-haiku-4.5), 1 assistant message persisted. Proves the builder yields a RunAgentWorkflowInput the workflow accepts and the agent streams a real response — behavior-preserving. Test session/chat/message deleted afterward. Test results comment.

  • docs#249POST /api/chat/runs contract (the docs lead the api).
    ✅ Merged 2026-06-24 to main (merge commit 58f442a; docs is single-branch). First in the docs→api→tasks order.
    Renamed POST /api/chat/generatePOST /api/chat/runs (REST: it creates a run, not a generate verb) and changed the contract from a synchronous 200 completion to 202 { runId, chatId, sessionId } + a Location header — so a caller can retrieve the result (GET /api/chat/{chatId}/stream, or the persisted messages) instead of fire-and-forget-only. Added GET /api/chat/runs/{runId} (status snapshot — ChatRunStatusResponse; api implementation deferred to a follow-up). Trimmed the request body to prompt | messages, artistId, model (dropped legacy excludeTools / roomId / topic to match /api/chat). Bidirectional cross-links added across the four chat endpoints. /api/chat/generate removed (no alias).
    Verified: api-reference/chat/runs.mdx present on main at 58f442a; OpenAPI (research.json) carries /api/chat/runs + /api/chat/runs/{runId}. api#704/Telegram bot - missing errors #705/tasks#152 were re-pointed to /api/chat/runs to fulfill this contract.

  • api#704 — re-point POST /api/chat/runs onto runAgentWorkflow (the keystone).
    ✅ Merged 2026-06-24 to test (merge commit 2209b7de), then promoted to main/prod via api#706 (testmain, merge commit 6abaad68, 2026-06-24); app/api/chat/runs/route.ts, the {runId} status route, and the lib/chat/runs/ rename all confirmed on main. (Telegram bot - new conversations - hide test emails #706 needed a one-file conflict resolution in buildRunAgentInput.ts — took the test side with ephemeralKeyId, since main only lagged behind.) This is the consumer that re-adds the minting deferred from api#700.
    Provisions a headless session + active sandbox (shared createSessionWithInitialChat + connectSandbox + markSessionSandboxActive), mints a per-run ephemeral recoup_sk_ key → injects as recoupAccessToken (never the service key) → threads agentContext.ephemeralKeyIdstart(runAgentWorkflow) → returns 202 { runId, chatId, sessionId } + Location. The workflow's finally revokes the key on run end (deleteEphemeralKeyStep); the ~15m TTL is the backstop. Also implements GET /api/chat/runs/{runId} (status). Scope evolved in review: /generate/runs rename, dropped legacy excludeTools/roomId/topic, SRP/DRY extractions shared with the interactive /api/sessions+/api/sandbox handlers, and lib/chat/generate/lib/chat/runs/ rename.
    Verified end-to-end on the preview across the review iterations (latest 10a727ce): POST /api/chat/runs202; GET …/{runId}runningcompleted; 404 unknown run; 401 no-auth; the agent ran real native bash tool calls (e.g. echo rename-verified → stdout rename-verified) observable via the persisted chat by chatId; ephemeral key auto-revoked (0 left) each run; all test artifacts cleaned up. Observability test comment.

  • api#705 — retire the OpenClaw prompt_sandboxrun-sandbox-command bridge.
    ✅ Merged 2026-06-24 to test (merge commit 4c09cf06), then promoted to main/prod via api#707 (testmain, merge commit 83a269ea); registerPromptSandboxTool.ts confirmed gone from main.
    Removed the prompt_sandbox MCP tool + its registration, triggerPromptSandbox (the run-sandbox-command trigger), processCreateSandbox's prompt mode, and the POST /api/sandboxes prompt/runId path (now account_id-only). Folded in the prompt cleanup once the audit showed the legacy getGeneralAgent stack is live (Slack chat + inbound email), not dead: stripped the prompt_sandbox "Sandbox-First" section from SYSTEM_PROMPT and the (use prompt_sandbox for those) pointer from create_knowledge_base, with guard tests on both. Behavior note: Slack + email agents lose the OpenClaw sandbox tool — fine, since OpenClaw is the failing component being removed; those two still run on the legacy getGeneralAgent stack (not runAgentWorkflow), so that stack can't be fully deleted yet (out of scope here).
    Verified on the preview (65dd8d05): live MCP tools/list (39 tools) shows prompt_sandbox removed and no sandbox tools remain; create_knowledge_base's live description no longer mentions it; POST /api/sandboxes with a legacy {prompt} returns {sandboxes:[…]} with no runId (prompt ignored). Test results comment.

  • tasks#152generateChat fire-and-forget (the final cutover PR).
    ✅ Merged 2026-06-24 to main (merge commit 39b877da). generateChat now POSTs ${NEW_API_BASE_URL}/api/chat/runs, consumes the 202 { runId } (logs/returns it for observability only), and does not await generation — persistence + side effects run server-side in the durable workflow. customer-prompt-task returns right after the kickoff. With this merged, the scheduled path runs entirely on runAgentWorkflow and nothing triggers run-sandbox-command.
    Verified end-to-end through the live path (local tasks Trigger.dev dev → customer-prompt-task/api/chat/runs prod → durable run): task completed in 6.7s without awaiting, kickoff returned 202 { runId: wrun_01KVXY569… }. A diagnostic run confirmed the ephemeral-key architecture works: the sandbox agent's shell got $RECOUP_ACCESS_TOKEN = a recoup_sk_ key (len 53), authenticated to the Recoup API (/api/accounts/id200, correct accountId) via the x-api-key header, and the key was revoked on run end (0 ephemeral:* keys linger). Env-matching holds: the prod-minted token is valid against prod (200) and rejected by test-recoup-api (401). Test results comment.
    Known fast-follow (non-blocking): the new sandbox agent has no email-sending capability yet (the "Daily Hello World Email" test run completed but reported no email tool), and RECOUP_ACCESS_TOKEN carries an API key while its name/JSDoc imply a Privy JWT — align so the recoup-api skill uses x-api-key.

Current failure (context)

  • Failing Trigger.dev task run-sandbox-command, prod project Recoup-Chat (proj_pxwxehzmqaxylqhhkomn). Representative run finished 2026-06-22 13:02 UTC, exitCode: 1.
  • Thrown at tasks/src/sandboxes/runSetupSandboxSkill.ts:23 ("Failed to set up sandbox via OpenClaw"), reached via ensureSetupSandbox.ts:34runSandboxCommandTask.ts:68.
  • Underlying OpenClaw stderr: GatewayClientRequestError: ... All models failed (2): vercel-ai-gateway/anthropic/claude-opus-4.6: Unknown model .... The model id is valid in the live gateway catalog, so this is a runtime model/catalog-resolution failure in the OpenClaw gateway daemon (candidate causes: gateway daemon not receiving AI_GATEWAY_API_KEY; onboard-default model vs. allowlist mismatch). Root cause was not conclusively pinned — which is itself the argument for removing OpenClaw rather than patching it.

Open — immediate (this week, out-of-band)

  • Manually generate + deliver the current week's report for the affected scheduled-report customer. (no PR)
    ✅ Shipped 2026-06-22 — pulled all five /api/research/* endpoints live (HTTP 200, real data), composed the report in the task's structure, and emailed it from agent@recoupable.com (Resend accepted, message id 35ca1a58-38e6-4cbe-b829-5467f9e8ad63). Details in the closure comment. Out-of-band; not part of the migration's acceptance.

Open — design spikes (derisk before implementation)

  • Short-lived, account-scoped token for in-sandbox recoup-api calls.
    • Why: the new agent calls recoup-api via bash+curl using $RECOUP_ACCESS_TOKEN; the interactive path forwards a short-lived Privy JWT and deliberately does not forward the long-lived service key into model-driven bash (api/lib/chat/handleChatWorkflowStream.ts:148-152). The scheduled caller only holds the service x-api-key.
    • ✅ Resolved (2026-06-23): an ephemeral, account-scoped recoup_sk_ key (not a new JWT). Privy JWTs can't be issued server-side (@privy-io/node is verify-only), so we reuse the existing api-key primitive: add account_api_keys.expires_at, mint a ~15m-TTL single-account key per run, inject as $RECOUP_API_KEY, delete on run end; auth rejects expired keys. Reproduces the interactive trust profile (short-lived single-account bearer in the sandbox). PRs: database#36 (column), api#700 (expiry enforcement only — minting deferred to the re-point PR; see the scope-correction note above). Decision comment.
  • Headless session + active-sandbox provisioning.
    • Why: runAgentWorkflow requires a live session with an active sandbox before start() (api/lib/chat/handleChatWorkflowStream.ts:67 → 400 "Sandbox not initialized"); the legacy path lazily span one up via prompt_sandbox, so the scheduled path pre-provisions nothing.
    • ✅ Resolved (2026-06-23): reuse existing lib functions headlessly — ensurePersonalRepoinsertSession(buildSessionInsertRow) → insertChat, then connectSandbox + updateSession(sandbox.getState() / buildActiveLifecycleUpdate) until isSandboxActive. Scheduled caller has accountId (+ optional artistId/roomId) and mints the session+chat. Implemented in the re-point PR.
  • Tool-parity decision: research + email as first-class AI SDK tools vs. skill-driven curl.
    • Why: buildAgentTools (api/lib/agent/buildAgentTools.ts) ships native sandbox/file tools only — no send_email, no research, no Composio. The legacy generate path got those via MCP + Composio (api/lib/chat/setupToolsForRequest.ts:20-22). Moving the report onto the new agent loses email unless addressed.
    • ✅ Resolved (2026-06-23): (a) curl-via-recoup-api-skill — keeps the broad recoup-api surface (research + email) without enumerating native tools; depends on the ephemeral key above for $RECOUP_API_KEY.

Open — implementation (in merge order)

Documentation-driven; sequence docs → database → api → tasks, honoring hard deps. (Prereqs buildRunAgentInput extraction [api#701] + expires_at enforcement [api#700] are done + on prod — see Done.)

  • api: re-point POST /api/chat/runs to provision + start(runAgentWorkflow) and return 202 { runId, chatId, sessionId }. → PR api#704 ✅ merged to test (prod promotion pending) — see Done. Re-adds mintEphemeralAccountKey + insertApiKey writer; new validateGenerateRequest + provisionGenerateSession; mints key → injects as recoupAccessToken → threads agentContext.ephemeralKeyId; runAgentWorkflow's finally deletes the key on run end (deleteEphemeralKeyStep). Preview test: POST /api/chat/generate202 { runId: wrun_01KVX2RBB3JDZFA0131B3RT1QS } → assistant message PR704 OK persisted → ephemeral key revoked. Contract = docs#249.
    • Why safe to change the contract: the only caller, tasks/src/recoup/generateChat.ts, uses the response only for logging (textPreview/usage) — persistence + email happen server-side. (tasks side: tasks#152.)
    • Includes the ephemeral-key minting removed from api#700 (the missing piece). Re-add lib/keys/mintEphemeralAccountKey.ts + the insertApiKey expires_at writer param, then call them here as part of the headless provision step. Expiry enforcement (isApiKeyExpired in getApiKeyAccountId) already ships in api#700, so the key is rejected once its TTL passes.
      • Where to call: in the headless provision path (after insertSession/insertChat, before connectSandbox), mint a per-run key for the run's accountId; inject rawKey as the sandbox env var $RECOUP_API_KEY (the recoup-api skill reads it for curl); on run end (success or failure) deleteApiKey(keyId). The ~15m expires_at TTL is the defense-in-depth backstop if that delete is missed.
      • Exact helper removed from api#700, to re-add verbatim (api/lib/keys/mintEphemeralAccountKey.ts):
        import { generateApiKey } from "@/lib/keys/generateApiKey";
        import { hashApiKey } from "@/lib/keys/hashApiKey";
        import { insertApiKey } from "@/lib/supabase/account_api_keys/insertApiKey";
        import { PRIVY_PROJECT_SECRET } from "@/lib/const";
        
        /** Default lifetime for an ephemeral key: 15 minutes. */
        export const DEFAULT_EPHEMERAL_KEY_TTL_MS = 15 * 60 * 1000;
        
        export type EphemeralAccountKey = { rawKey: string; keyId: string };
        
        export async function mintEphemeralAccountKey(
          accountId: string,
          { ttlMs = DEFAULT_EPHEMERAL_KEY_TTL_MS, name = "ephemeral:chat-generate" }:
            { ttlMs?: number; name?: string } = {},
        ): Promise<EphemeralAccountKey> {
          const rawKey = generateApiKey("recoup_sk");
          const keyHash = hashApiKey(rawKey, PRIVY_PROJECT_SECRET);
          const expiresAt = new Date(Date.now() + ttlMs).toISOString();
          const { data, error } = await insertApiKey({
            name, account: accountId, key_hash: keyHash, expires_at: expiresAt,
          });
          if (error || !data) {
            throw new Error(`Failed to mint ephemeral api key: ${error?.message ?? "no row returned"}`);
          }
          return { rawKey, keyId: data.id };
        }
        The insertApiKey change is one optional field: destructure expires_at from the Insert type and pass it through to .insert({ ... expires_at }) (api/lib/supabase/account_api_keys/insertApiKey.ts).
    • Done when: a POST /api/chat/generate (x-api-key) starts a workflow run that persists assistant messages and performs the report's side effects; response is { runId }. The sandbox receives $RECOUP_API_KEY set to a freshly-minted, single-account recoup_sk_ key; that key is deleted on run end; and a key whose expires_at has passed returns 401 from getApiKeyAccountId (covered by api#700's tests).
  • api: retire the OpenClaw bridge — removed the prompt_sandbox MCP tool, processCreateSandbox's prompt mode, triggerPromptSandbox, and the POST /api/sandboxes prompt/runId path. → PR api#705 ✅ merged + promoted to main/prod 2026-06-24 — see Done. Correction to the earlier note: the SYSTEM_PROMPT/getGeneralAgent stack is not dead — it's live via Slack chat + the inbound email responder — so the prompt_sandbox prompt references weren't cosmetic; Telegram bot - missing errors #705 also cleaned SYSTEM_PROMPT + the create_knowledge_base description so live agents aren't told to call a removed tool.
    • Done when: nothing in api calls tasks.trigger("run-sandbox-command", ...); grep is clean.
  • tasks: make generateChat fire-and-forget — stop awaiting/saving the completion; customer-prompt-task only triggers the run. → PR tasks#152 ✅ merged 2026-06-24 to main (39b877da) — see Done. generateChat now POSTs /api/chat/runs, consumes the 202 { runId } (logs it, returns it), and does NOT await generation; customer-prompt-task returns after the kickoff.
    • Done when: customer-prompt-task returns after kicking off generation; no generation logic remains in the task.

Open — Phase 2 (LAST, only after the unified path is stable on prod)

  • Delete the OpenClaw layer from tasks. Remove run-sandbox-command registration + helpers: runSandboxCommandTask.ts, installOpenClaw.ts, onboardOpenClaw.ts, setupOpenClaw.ts, runOpenClawAgent.ts, runSetupSandboxSkill.ts, runSetupArtistSkill.ts, ensureSetupSandbox.ts, installSkill.ts, consts.OPENCLAW_DEFAULT_MODEL, and git/README/org-repo helpers used only by this task; drop related tests.
    • ⚠️ Correction (2026-06-23): installOpenClaw / onboardOpenClaw / setupOpenClaw / runOpenClawAgent / installSkill are shared with codingAgentTask, setupSandboxTask, and the pulse path — not exclusive to run-sandbox-command. This phase safely deletes only runSandboxCommandTask.ts + its exclusive helpers (runSetupSandboxSkill, runSetupArtistSkill, ensureSetupSandbox), OPENCLAW_DEFAULT_MODEL, and the api bridge triggerPromptSandbox. The shared OpenClaw helpers stay until those other tasks are migrated/removed separately.
    • Gate: no run-sandbox-command runs observed in prod for a stable window after the unified path ships.
    • Done when: run-sandbox-command + its exclusive helpers no longer appear in tasks (grep clean); tasks redeployed.

Architecture decisions

  • Async generation converges on runAgentWorkflow — the production interactive architecture — not a new bespoke workflow. Eliminates the dual (legacy ToolLoopAgent vs. workflow) architecture instead of maintaining both.
  • Scheduling stays in Trigger.dev; generation moves to api. customer-prompt-task + createSchedule/updateSchedule remain as light orchestration; no agent/generation code stays in tasks.
  • The service x-api-key is never forwarded into model-driven bash. The headless path mints a short-lived, account-scoped token (mirrors the interactive path's recoupAccessToken), preserving the exfiltration boundary documented at handleChatWorkflowStream.ts:148-152.
  • The prompt_sandboxrun-sandbox-command bridge is removed, not repointed. The unified agent gets native sandbox tools in-workflow, so no offloaded sandbox task is needed.

Accepted regressions / tradeoffs

  • Composio tools dropped for scheduled generation (Sheets/Drive/Docs/TikTok were in the legacy MCP+Composio set). Acceptable for reports; re-add as native tools if a scheduled task needs them.
  • /api/chat/generate returns { runId }, not the completion. Acceptable — the sole caller only logged the body. Any future synchronous consumer must poll the run instead.

Source references

  • New architecture: api/app/lib/workflows/runAgentWorkflow.ts, runAgentStep.ts; tools api/lib/agent/buildAgentTools.ts; entry api/lib/chat/handleChatWorkflowStream.ts.
  • Legacy path being migrated: api/lib/chat/handleChatGenerate.ts, api/lib/chat/setupToolsForRequest.ts, api/lib/agents/generalAgent/getGeneralAgent.ts.
  • OpenClaw bridge to remove: api/lib/mcp/tools/sandbox/registerPromptSandboxTool.ts, api/lib/sandbox/processCreateSandbox.ts, api/lib/trigger/triggerPromptSandbox.ts, tasks/src/tasks/runSandboxCommandTask.ts, tasks/src/sandboxes/* (OpenClaw helpers).
  • Scheduling (kept): tasks/src/tasks/customerPromptTask.ts, tasks/src/recoup/generateChat.ts, api/lib/trigger/createSchedule.ts.
  • Workflow infra (already present in api): next.config.ts (withWorkflow), workflow@4.2.4, existing api/app/workflows/*.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions