Control effects
Source:
docs/runtime/control-effects.md
Status: current
Control effects
Section titled “Control effects”Parents: ../canon/grain.md. Compact fold lives here. Authorization is not Control: approval.md. Session/Job ontology: session-job.md. Living ADRs: ../decisions/calls-propose-verdicts-dispose.md, ../decisions/budget-is-data-not-policy.md, ../decisions/scheme-at-structure-cadence.md.
PEW is work; verdict dispose is the pure/impure cut; control effects are named traps with different return laws; policy chooses named restarts; program bodies are always flat — time, join, and re-invocation belong to the interpreter. Compact is a turn-queue fold, not a Control trap.
1. Flat execution — interpreter-owned wait
Section titled “1. Flat execution — interpreter-owned wait”LAW. Userland and kernel programs never await. A run starts, computes, and returns data. It does not block on humans, I/O, PEW seals, or sibling work.
Waiting, parallelism, and re-invocation are interpreter responsibilities (daemon / claim-owner host / PEW substrate) — not Scheme (or ts-builtin) control-flow syntax.
| Who | Does | Does not |
|---|---|---|
| Program body | Return Done(series), Control(condition, meta), Abort(reason), parallel work descriptors, optional join/after lambdas (as values) |
await, park Promise, stack suspend, thread sleep |
| Interpreter | Schedule PEW; fan-out map/foreach; join seals; apply verdicts; hold open control obligations; re-invoke procedures when results land |
Push async syntax into the program language |
| PEW / surfaces | Durable “not done yet” + progressive face | Live inside a program stack frame |
Author vocabulary is return values, not pause:
run(env) → Done(series) — host disposes Expectations, KV, inbox, log-ops Control(condition, meta) — control obligation; this run is finished Abort(reason) — fail closed; this run is finishedThere is no Pause as a program-facing return. Host-internal “lane stopped” / fuel stop is SCHED_STOP bookkeeping after a control condition or host law — still not an await inside a body.
Re-invocation is a new flat evaluation after the host changed the world or delivered a settlement (restart name + payload). It is not classical resume k v into a frozen stack.
(map (lambda (x) …) items) returns a list of work descriptors. The interpreter mints N obligations and runs them concurrently. map / foreach do not mean sequential await-in-a-loop.
| Forbidden | Why |
|---|---|
await / Promise-style suspend in program bodies |
Hides wait inside the language |
harness/pause as userland API |
Suggests the body is still running while blocked |
Saga yield need-* as OS spine |
Same class of mistake at the extension layer |
| One frozen stack continuation across claim death | PEW one-execution + re-invoke a new run instead |
Claim-owner host modules (TypeScript actors — e.g. OpenAI-compat) may use process-local async to talk to HTTP. That is interpreter/host, not program language. Process-local parks that outlive the pure tick must be dual to durable PEW surfaces.
2. Layered grounding
Section titled “2. Layered grounding”| Layer | Grounding | Owns |
|---|---|---|
| OS process model | PEW (durable Expectations + openWork) | Work identity, claim, cancel tree, seals, progress |
| Extension language | map → reduce → verdict / Control return → host dispose | Programs propose; host applies |
| Execution law | Flat runs; interpreter owns wait/join/re-invoke | Time and parallelism |
| Control ABI | Four trap classes | How a flat run’s Control return is interpreted |
| Control situations | Conditions + named restarts | What is wrong; which recoveries exist |
Rejected as primary: whole OS as algebraic effects; one big interaction tree; userland around-advice; unified saga yield need-*; “everything re-enters the same reduction” (only FAULT_RESTART re-invokes the same event); async/await in program bodies.
Permission / UAC is not a Control return and not a PEW/Proposal. Sole home: approval.md.
3. Four trap classes
Section titled “3. Four trap classes”Every control effect must state which class it is. If it cannot, it is not a control effect yet.
| Class | OS analogue | Interpreter law after Control return | Typical |
|---|---|---|---|
| FAULT_RESTART | Page fault | Fix world → new flat run, same event | defined-action compact handlers that still return need-compact |
| SYSCALL_BLOCK | Blocking call | Hold obligation → new flat run on a settlement event | need-credential, need-attach, need-elicit |
| SIGNAL_CANCEL | Signal to process group | Drain/cancel openWork; do not re-run the interrupted step | need-interrupt-settle |
| SCHED_STOP | Scheduler stop | No auto re-invoke; manual wake only | need-fuel-pause |
Not control: minting PEW (normal Done series); inbox post/consume; KV setup; PEW progress yields; session compact (the fold below); session authorization.
Session compact does not return Control(need-compact). Session authorization does not return Control(need-approval). Those two seats are host pipelines, not trap returns.
4. Conditions and named restarts
Section titled “4. Conditions and named restarts”Chrome groupings (Continue / Abort / stop) are not the kernel algebra. Named restarts are. Establishers of restarts = claim owner / host resource site. Handlers (policy) choose among established restarts only.
| Condition | Situation | Named restarts | Class |
|---|---|---|---|
| need-compact | Defined-action program: processable log too full | compact-then-reenter, abort-turn |
FAULT_RESTART |
| need-credential | Secret ref unresolved | resolved (host credential seat only), abort |
SYSCALL_BLOCK |
| need-attach | Inference source not ready | attached, abort |
SYSCALL_BLOCK |
| need-elicit | Dangerous adopt / structured confirm | confirm, reject, answer |
SYSCALL_BLOCK |
| need-interrupt-settle | Open work must quiet before new admit | barrier complete only | SIGNAL_CANCEL |
| need-fuel-pause | Re-entry / lane depth overflow | manual-resume only |
SCHED_STOP |
need-quota is not a fifth trap class — Abort via policy, default restart abort only.
Settlement delivers a restart name + payload into the env of the next flat run, not into a suspended stack frame.
5. Compact is a turn-queue fold
Section titled “5. Compact is a turn-queue fold”Session compact is a CompactJob on the turn queue plus a config-addressed Scheme program. There is no Control entity on that loop, and no executor for the job — the turn loop runs it when the chunk is shifted, after tools have settled. Occupancy that fires compact is config.json compactTriggers. Occupancy that pauses a live completion so the fold can finish is pauseForCompaction (not shipped).
Host gates — not the Scheme file. Empty eat and compact-on-compact (no new work since the last fold) reject; the strategy never sees them. Keep-all (preserve floors leave no fold range and a nonempty keep) skips auto compact, including hard overflow. Compact failure is an execution failure: the job stores a human notice (couldn't compact — try again later) and is not a cutoff. No second emergency algorithm. The notice is not injected into the next model prompt. Strategies are the happy path only.
CompactJob on queue → compactRejectReason (empty | already-compacted) → complete, not a cutoff → sliceCompactZones(eat, keepFloors) → { fold, squeeze, keep } → fold empty + keep nonempty → keep-all, complete, not a cutoff → applyCompact(program, fold, full-eat) → card text | failure notice → job.card = card; job is the cutoffThe job is not “start here.” A successful job means everything strictly before it is projected: the fold zone is the card string, the squeeze zone is stored tool lines (regeneratable tools omit call and result together), and the keep zone stays verbatim. Cells after the job stay full. Floors and focus are read from the ContextNeed before the job, else compactKeepTurns. reason is stamped at enqueue.
A document that already has a folded TurnCompactChunk still projects representation the old way. New folds do not create that chunk.
The program is a reducer. config.json compact names the file (default @inhuman.tools/harness-server/compact.scm). Owning the fold is repointing, never editing a seeded copy. Dist body:
(define (compact compact-transcript full-transcript) (let ((xs (transcript/cells compact-transcript))) (if (null? xs) xs (list (car xs)))))(define forced-compact compact)Keep-first stub: the card is the first fold cell. Slash /compact (reason: "user") calls forced-compact when defined, else compact. Auto / overflow always call compact. Transcripts are arguments. The program returns the card; the host stores that text on the job. Empty or non-shrinking cards fail closed (empty-card / did-not-shrink).
CompactJob is a Job, not PEW, and nothing registers an executor for it. Fields: reason (overflow | need | user), card, notice, failed, keepTurns (the dial reading at the fold). Auto enqueue is hard overflow after a completion (keep-all vetoes it). User slash mints a compact turn whose queue is the job, reason: "user". A legacy TurnCompactChunk remains a WorkContainer with representation for old documents.
applyCompact loads the strategy with Arrival execState and calls the named function. Programs stay flat; the host owns the walk. Same hatch as remainder: a local path is the operator’s; the dist token updates with the package.
Must not: dual-write live PEW into log and KV; delete PEW archaeology by default; store full history in KV; treat compact as a Control trap; run compact on the hot paint path.
6. Dispose, not await
Section titled “6. Dispose, not await”Mappers (programs) are pure: (event, fold-views, KV-snapshot) → proposals. Reducers consolidate. The host disposes a Done series (mint PEW, apply KV, inbox, log-ops). Introducing a mapper is SAFE; introducing a reducer is DANGEROUS and audited. Law: ../decisions/calls-propose-verdicts-dispose.md.
Budget is data, not a kernel kill-switch: ../decisions/budget-is-data-not-policy.md. Scheme does not run on the hot paint path: ../decisions/scheme-at-structure-cadence.md.
7. Structural risks
Section titled “7. Structural risks”- Uniform “re-enter same event” for all trap classes
- Conflating FAULT_RESTART re-invoke with SYSCALL settlement event
- Process-local host async without durable PEW dual + abandonment law
- Partial series dispose without residual obligation for the unapplied tail
- Fuel stop only process-local → storm after claim-orphan unless re-derived
- Userland await/advice/sagas that invert audit and authority
- Session compact reintroduced as
Control(need-compact) - Session authorization reintroduced as
Control(need-approval)or a Permission PEW/Proposal - Author-facing pause/await under a new name
See also
Section titled “See also”| Doc | Role |
|---|---|
../canon/grain.md |
Causal spine; compact is a queue chunk, not this file’s ABI |
approval.md |
LICENSE WHEN; not a trap |
session-job.md |
SessionTurn queue; Job vs PEW |
../packages/harness-server/src/daemon/session/compact.ts |
host gates, compactDecision |
../packages/harness-server/src/daemon/session/compact-env.ts |
applyCompact |
../packages/harness-server/compact.scm |
Dist fold |
../packages/harness-sdk/src/models/session/jobs/CompactJob.ts |
cutoff job |
../packages/harness-sdk/src/models/session/turn/compact-chunk.ts |
legacy TurnCompactChunk |