feat(init): speak the Vercel Workflows protocol#1160
Conversation
The init server migrated from the Mastra workflow engine to Vercel
Workflows, which exposes a different HTTP contract (/api/wizard,
/api/run, /api/resume with deterministic hook tokens) instead of the
Mastra client's create-run/start-async/resume-async.
Add a workflow-client shim (src/lib/init/workflow-client.ts) that
presents the same createRun/startAsync/resumeAsync/runById surface the
wizard runner already uses, mapping the server's { status, seq, request }
shape onto WorkflowRunResult and polling GET /api/run between suspends.
Resume targets the hook token wizard:<runId>:<seq> reconstructed from the
runId + seq (never received from the server).
Replace MastraClient in wizard-runner with the shim; drop the now-unused
@mastra/client-js dependency. Update the runner tests to mock
createWorkflowClient instead of MastraClient.prototype.getWorkflow.
Tests: init suite green (418 pass).
|
Codecov Results 📊❌ Patch coverage is 17.14%. Project has 5198 uncovered lines. Files with missing lines (2)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
- Coverage 81.48% 81.31% -0.17%
==========================================
Files 397 398 +1
Lines 27712 27814 +102
Branches 18013 18060 +47
==========================================
+ Hits 22579 22616 +37
- Misses 5133 5198 +65
- Partials 1860 1867 +7Generated by Codecov Action |
runById creates its own isolated seqRef, so after tryRecoverCurrentRunState recovers via workflow.runById(), the run's seqRef stays stale. The next resumeAsync then builds a token targeting the wrong suspend hook. Fix: carry _seq on WorkflowRunResult, expose updateSeq() on WorkflowRun, and call syncSeqFromRecovery() in both recovery paths of resumeWithRecovery.
|
good catch — |
| * state through a separate `seqRef`. Without this, `resumeAsync` would build a | ||
| * stale token after recovery. | ||
| */ | ||
| _seq?: number; |
There was a problem hiding this comment.
_seq: -1 sentinel overwrites a valid seqRef after runById recovery when server omits seq
In toWorkflowRunResult, the suspended branch emits _seq: seqRef.current unconditionally. During runById recovery, pollUntilSettled is invoked with a fresh seqRef = { current: -1 }. If the server returns a suspended state with a request but no seq field (seq?: number is optional in RunStatePayload), the guard if (typeof state.seq === 'number') is skipped, so seqRef.current stays -1 and _seq: -1 is emitted. syncSeqFromRecovery in wizard-runner.ts only checks typeof recovered._seq === 'number' — which -1 satisfies — and calls run.updateSeq(-1), overwriting the run's previously valid seq. The next resumeAsync then builds an invalid token wizard:<runId>:-1. Fix by guarding the emission (_seq: seqRef.current >= 0 ? seqRef.current : undefined) or the update (if (typeof recovered._seq === 'number' && recovered._seq >= 0)).
Evidence
runById(workflow-client.ts) creates a freshseqRef = { current: -1 }and callspollUntilSettled(runId, seqRef).- In
toWorkflowRunResult, the suspended branch only setsseqRef.current = state.seqwhentypeof state.seq === 'number', but emits_seq: seqRef.currentunconditionally — so a missingseqleaves_seq: -1. RunStatePayload.seqis typedseq?: number, so an absentseqon a suspended response is within contract.syncSeqFromRecovery(wizard-runner.ts line 734) accepts anynumberincluding-1and callsrun.updateSeq(-1), replacing a valid seq.resumeAsyncthen buildswizard:${runId}:${seqRef.current}=wizard:<runId>:-1, an invalid resume token.
Also found at 2 additional locations
src/lib/init/wizard-runner.ts:730-737src/lib/init/workflow-client.ts:106-110
Identified by Warden find-bugs · GZZ-3R9
Surface what the wizard is doing and why it fails under the existing --verbose / --log-level=debug flags (no new flag needed). The workflow-client shim now logs, via a `wizard-client` tag: - the target base URL at construction (reveals wrong/stale server), - each POST/GET request, response status, and error body, - run id, resume token, and poll settle transitions. Also improve the readiness health check to log the URL + non-ok status (previously only network errors were logged), so a 404 from a misconfigured MASTRA_API_URL is visible. Tests: init suite green (418 pass).
When the init server errors (e.g. a crashed dev server returning a Bun HTML error overlay), the workflow client was surfacing the entire raw body — hundreds of KB of markup — in the CLI error. Extract the meaningful bit instead: the `error` field from JSON envelopes, or the first "<Something>Error: message" line from an HTML page, capped at 500 chars. Falls back to a short hint for opaque HTML. Tests: init suite green (418 pass).
Updates the init CLI to talk to the migrated init server, which moved from the Mastra workflow engine to Vercel Workflows. That migration (getsentry/cli-init-api#183) replaced the Mastra client contract (
/api/workflows/sentry-wizard/{create-run,start-async,resume-async}) with a new one:POST /api/wizard,GET /api/run,POST /api/resume. Pointing the CLI at the new server currently 404s because those endpoints don't match.Changes
src/lib/init/workflow-client.ts— a shim presenting the samecreateRun/startAsync/resumeAsync/runByIdsurface the wizard runner already uses, while speaking the new protocol underneath. It maps the server's{ status, seq, request }into the existingWorkflowRunResultshape and pollsGET /api/runbetween suspends.wizard:<runId>:<seq>, reconstructed from the runId (returned by/api/wizard) and theseqreported by/api/run— the token is never received from the server, so no capability leaks.MastraClientinwizard-runner.tswith the shim; drop the now-unused@mastra/client-jsdependency.wizard-runner.test.tsto mockcreateWorkflowClientinstead ofMastraClient.prototype.getWorkflow.Testing
vitest run test/lib/init— 418 passing. Typecheck + biome clean on changed files.Notes / follow-ups
DEFAULT_MASTRA_API_URLstill points at the old Cloudflare worker; it should be updated to the new Vercel URL once the server is deployed. Local testing works today viaMASTRA_API_URL=http://localhost:4111.