Skip to main content

Command Palette

Search for a command to run...

Corsac Phase 2: Closing the Runtime Gap

Updated
•10 min read•View as Markdown
Corsac Phase 2: Closing the Runtime Gap
B
Founder of Narcoleptic Fox. Marine veteran. Senior engineer learning AI/ML and writing about it. Building Mousing.ai, Reqforay,.ai, and foxstash.

First published in my LinkedIn newsletter. Corsac is still a work in progress; this describes it as it was then.

At the end of Corsac Phase 1, the project had crossed an important line.

It was no longer just an architecture document around Gust. It had a real CLI, a local runtime, run records, repair tooling, runtime configuration, a Lambda packaging path, and tests around the parts most likely to break.

But it still had a gap I didn't want to hand-wave away.

The Lambda path existed. The deploy and invoke commands existed. The fake-AWS tests passed. Docker could produce the Lambda package. The docs were written.

What I didn't have yet was the thing that actually matters for an AWS-facing runtime: proof that the canonical workflow could go through real Lambda and behave the same way.

Phase 2 was about closing that kind of gap.

Not by making Corsac bigger. Not by adding a hosted control plane. Not by starting a visual designer. Not by adding more product surface just because the roadmap had room.

Phase 2 made the narrow thing harder to fool.

The current wedge is still small:

  • CLI-first, Lambda-friendly for AWS-heavy teams
  • compiled Gust workflows with explicit effect versus action semantics
  • node-boundary durability (records progress around meaningful workflow boundaries instead of full event-sourced replay — see the post on effects and actions)
  • local run inspection
  • no hosted control plane, no visual designer, no shared remote run store

The question changed from "can Corsac run a compiled Gust workflow locally?" to "can this path hold up when it leaves the laptop, and can an operator understand what happened afterward?"

That's a more useful question.

The AWS smoke test finally ran

The biggest open Phase 1 confidence gap was real AWS validation.

Corsac had already implemented the Lambda path:

corsac package lambda docs/examples/workflows/order-notification.gu --use-docker
corsac deploy lambda docs/examples/workflows/order-notification.gu \
  --function-name <name> \
  --role <role-arn> \
  --region <region> \
  --lambda-env-from CORSAC_SLACK_TOKEN=CORSAC_SLACK_TOKEN \
  --yes
corsac invoke lambda <function-name> \
  --region <region> \
  --payload @docs/examples/payloads/success.json

That was implementation. It wasn't external validation.

So Phase 2 put the canonical order notification workflow through real Lambda in us-east-1.

The success fixture completed with final state NotificationSent.

The failure fixture returned the expected handled workflow shape: CorsacWorkflowError with failure_class: handler_error.

CloudWatch logs were verified for both requests. The success path showed the simulated Slack handler path and confirmed the credential was present without writing the secret into the notes or docs.

That closed the Phase 1 external validation gap.

But it's important to say exactly what closed and what didn't.

Corsac has now run the canonical workflow through real Lambda. That's real evidence. It's also not the same as automated AWS validation in CI. AWS smoke is still a manual release or design-partner gate, because account setup, IAM permissions, Docker, costs, and cleanup boundaries should be explicit every time.

Manual validation isn't CI.

It's still useful, as long as it's named honestly.

After validation, the temporary smoke resources were deleted: the Lambda function, the CloudWatch log group, and the dedicated execution role. The point was to prove the path, not leave stray infrastructure around because it was convenient.

The smoke test caught something real

The best thing about the AWS smoke test is that it didn't just produce a green checkmark.

It found a real bug.

The first deploy and invoke worked. So I let the runtime sit warm for a few minutes and tried again with the failure payload — the kind of "second invoke after the cold start is over" pattern any real workflow would hit in production.

Instead of returning the expected CorsacWorkflowError, the function came back as Runtime.ExitError.

That's the failure mode that turns into a 2am page somewhere. The function appears broken. The workflow logic is fine. The operator stares at CloudWatch trying to figure out which layer to blame, and the answer is in the runtime bootstrap, not the workflow.

The custom Lambda bootstrap had the wrong timeout shape for AWS's actual Runtime API behavior. The HTTP client timeout was reasonable for normal requests but wrong for the invocation/next endpoint. That endpoint is a long-poll. When the runtime is waiting for the next event, it's supposed to sit there. Timing out at the client layer turns a warm idle period into a runtime failure.

The fix was straightforward once the behavior was visible: remove the client-level timeout for the Runtime API long-poll and let Lambda's invocation lifecycle drive the wait.

There was a second AWS-specific cleanup in the same area. Existing-function redeploys needed to stop passing --architectures to update-function-configuration; that belongs on update-function-code.

Neither bug changes the product thesis. Both matter because they're the kind of edge that appears only when the integration is real.

The point of the smoke test wasn't the green checkmark. It was forcing the runtime through AWS's actual behavior.

Inspection became less awkward

A workflow runtime gets much less mysterious when every run leaves a record you can ask questions of.

Phase 1 already had local run records under .corsac/runs/. It had history, show-run, and repair-runs. That was enough to prove the local runtime wasn't a black box.

Phase 2 made the inspection surface more operator-shaped.

The first improvement was small but useful: corsac show-run now defaults to the most recent run when no execution ID is supplied. The common local question after a failed run isn't "let me copy that execution ID and paste it somewhere." It's "what just happened?"

The second improvement was machine-readable output. history and show-run now support stable JSON output, so inspection can feed scripts without fragile table parsing.

The third improvement was scope.

Once corsac invoke lambda started recording invocation results, local runs and Lambda records needed to be easy to separate. Phase 2 added explicit scopes: local, lambda, and all.

That means the operator can ask different questions:

corsac history --scope local
corsac history --scope lambda
corsac show-run --scope lambda

Lambda invocation recording now defaults on. If you invoke a Lambda through Corsac, the result is recorded into .corsac/runs with scope: lambda. If you don't want that, there's a --no-record opt-out.

That default matters. A runtime should leave breadcrumbs unless the operator explicitly asks it not to.

The first version could run workflows. The Phase 2 version is easier to ask what happened.

Artifacts need receipts

The other inspection gap wasn't about runs. It was about artifacts.

Before Phase 2, Corsac could build and package workflows. But explaining exactly what produced a given artifact required too much context outside the artifact directory.

That's a bad habit for deployment tooling.

If a zip is going to be deployed, it should be able to explain what produced it.

Phase 2 added metadata to the build and package outputs.

Local build artifacts now get an artifact.json that records the useful facts:

  • schema and artifact kind
  • workflow digest
  • target
  • build mode
  • selected config environment
  • Gust version
  • runtime wrapper hash and revision
  • cache key inputs
  • cache decision

Lambda packages now get lambda-package.json with package-specific facts:

  • package schema and kind
  • Lambda runtime
  • architecture
  • function and environment fields
  • source artifact cache fields

The CLI also prints metadata paths from build, package, and deploy output, so the operator doesn't have to know the artifact directory layout by memory.

This isn't glamorous work. It isn't a feature that demos well.

It's the kind of thing that makes a runtime easier to trust later, when a generated binary isn't just something you ran once from your own terminal.

Artifacts need receipts.

Gust 0.2.1 closed the language gate

The post on effects and actions is about the effect versus action boundary: why workflow runtimes need to distinguish replay-safe work from externally visible committed work.

Phase 2 turned that from a design rule into a release gate.

Corsac had requested annotation comments in Gust for the side-effect contract:

/// gust:effect -- replay-safe / idempotent
effect parse_order_json(body: String) -> OrderPayload

/// gust:action -- not replay-safe / externally visible
action post_slack(channel: String, text: String, credential_id: String) -> String

Gust 0.2.1 landed with the path Corsac needed.

Corsac then validated the canonical workflow against the real Gust 0.2.1 binary, refreshed the pinned generated Rust fixture, and reran the live compatibility gate.

That gate matters because default CI intentionally uses a checked-in generated fixture. That keeps the normal test suite stable while Gust and Corsac evolve together. But release validation still needs to prove that the current real compiler can build the canonical workflow.

That's what the live Gust gate does.

The fixture refresh also caught a small tooling detail worth documenting: gust build -o now targets an output directory, not a direct output file. The docs and fixture header were updated so the next compiler upgrade doesn't repeat that mistake.

The generated fixture changed in expected ways. It preserved the side-effect annotations in the generated trait comments, included codegen updates from Gust 0.2.1, and became the new pinned fixture for the acceptance path.

After that, Corsac's Phase 2 closeout was no longer waiting on upstream Gust work.

Gust 0.2.1 closed the loop between the language contract Corsac wanted and the runtime validation Corsac needed.

There's one boundary to keep explicit: this doesn't mean Corsac now has full parser-backed semantic proof. Corsac's Phase 2 linter is still a hardened source-level bridge. It masks comments and string literals before matching declarations, handlers, braces, and perform calls, and tests cover comments, strings, quoted braces, block-comment braces, and valid current syntax.

That's stronger than where Phase 1 started.

It's still not the final form. The long-term authority for side-effect semantics should live in Gust metadata or diagnostics. Corsac should consume that contract instead of growing a parallel parser forever.

What still isn't done

Phase 2 closed a lot of real gaps. It didn't make Corsac a finished workflow platform.

The remaining limits are still visible:

  • AWS smoke validation is manual, not default CI.
  • Execution history is local unless a Lambda invocation is recorded through the CLI; there is no shared remote run store.
  • There is no hosted control plane.
  • There is no visual workflow designer.
  • There is no multi-environment promotion workflow.
  • Parser-backed side-effect proof is still future Gust metadata or diagnostics work.
  • Full event-sourced replay is deferred.

Some of those are gaps. Some are intentional boundaries.

That distinction still matters.

No hosted control plane isn't an accident. It's a choice to keep the current wedge focused until the compiled runtime has enough pressure from real workflows to justify a bigger surface.

The same is true for AWS automation. A live AWS smoke test proved the path. Automating that in CI has different tradeoffs: account boundaries, IAM, cleanup, cost, and secrets. That's a real decision, not something to sneak in because a checklist wants another green square.

The current verdict

Corsac Phase 2 did what it needed to do.

It closed the real AWS smoke gap. It found and fixed a warm-runtime Lambda bug. It made run inspection more useful. It made Lambda invocation records visible by default. It gave artifacts metadata. It validated the canonical workflow against Gust 0.2.1. It refreshed the fixture. It cleaned up the temporary AWS resources when the smoke test was done.

That's a useful checkpoint.

The project is still intentionally small. But it's a more credible small thing now.

The next question is no longer "can Corsac run a compiled Gust workflow?"

It can.

The next question is whether the right move is to keep hardening the CLI/Lambda wedge or start a separate hosted/control-plane phase.

I don't want to answer that just because a roadmap wants a next box.

The next phase should be shaped by the friction that shows up while using Corsac for real workflows.