Production checklist — running Penelope past the demo stage.
Penelope is good enough for: small services, durable workflows, agent runtimes, side-projects with snapshot-based state. It is not a substitute for a tier-1 language runtime at scale — there is no GC tuning, no JIT bailout machinery, no contiguous-arena allocator. The thesis is correct and the implementation is clean, but you should expect to operate it the way you'd operate a careful in-house tool.
The .penz format is a versioned JSON blob (gzip-compressed by default since 5.D). It includes the program path, the program SHA-256, the paused IP, and the full VM state. Treat snapshots as durable values: store them in an object store, rotate them on a TTL, audit them when you redeploy.
| Concern | Recommendation |
|---|---|
| Long-lived snapshots | Retain in S3 / GCS with a TTL matching your workflow SLA. |
| Schema migration | If you rev the source, the hash check rejects old snapshots by default. Use --force only when you've audited the IP map (see live editing). |
| Sensitive data | Snapshots include all live bindings. Encrypt at rest. Audit which workflows could capture PII into the state. |
| Size | gzip default: ~5–20× smaller than JSON. Inspect with pen inspect <snap.penz>. |
The VM records every external effect (print, net_fetch, now, random_int, read_file, write_file, wait_until, wait_for) into the snapshot's effect log. On resume, those effects are not re-performed: the recorded value is reused. This is what makes resume safe.
The implications:
now() calls. A workflow that sleeps for hours and then calls now() gets the recorded earlier timestamp on replay. Use --time N to override.--no-replay opts out of the entire effect log — every effect re-fires. Useful for forking a snapshot into a "what if" alternate timeline.The VM has a tracer hook (src/tracer.ts). Pass an implementer of the Tracer interface to run(prog, state, profile, tracer), and you'll get structured events: fn_call, fn_return, effect, pause, resume, error. The shape mirrors OpenTelemetry's span events closely enough that a 50-line adapter ports it.
import { run, freshState } from 'penelope-lang';
import { JsonLinesTracer } from 'penelope-lang/dist/tracer';
run(prog, freshState(), undefined, new JsonLinesTracer(process.stderr));
Built-in tracers:
MemoryTracer — buffer events in memory; for tests.JsonLinesTracer — write one JSON object per line to any writable stream.CompositeTracer — fan out to multiple sinks.The coordinator in src/dist/coordinator.ts is single-node. That is fine for workloads up to ~thousands of in-flight jobs per process. For HA, run the coordinator behind a process supervisor (systemd, supervisord, k8s Deployment) and back it with a FileStore pointing at a persistent volume — when the coordinator restarts, jobs survive.
What is not built in: consensus / leader election. If you need multi-coordinator HA, you're looking at a different project (Raft + state replication). For most use cases, "one coordinator with a persistent store and quick restart" is fine.
| Vector | Mitigation |
|---|---|
| Untrusted source code | Don't run untrusted .pen files. The VM has no sandbox — effects (read_file, net_fetch) hit the host directly. |
| Coordinator API | Unauthenticated. Run behind a private network or wrap with an auth proxy. |
| Snapshot tampering | The program-hash check catches accidental drift but is not a cryptographic signature. Verify your own integrity (HMAC, signed envelope) if snapshots cross trust boundaries. |
pen exec --jit). ~2.4× on tight loops.-O2 optimizer (default) handles constant folding, dead-code elimination, peephole IC insertion. -O0 disables it; useful for debugger sessions.pen profile <file.pen> — shows per-opcode hit counts and time.Snapshot format is v3 (since Phase 3). Pre-v3 snapshots are not migratable (the VM model changed). When the format revs again, this page will document the migration path. Until then, treat .penz files as bound to the language version that produced them.