Codemode script preamble ("prior results" for agent scripts)
Status summary#
Implemented and green locally (typecheck, lint, knip, format, full test suite). Main pieces all landed: contract events + retained-results state, the two-variant assembler, typecheck/execution injection with attribution rules, host verbs (setPreamble/removePreamble/getPreamble/getScriptResult), agent rendering + prompt teach, ~30 new tests. Remaining: CI + review; possible follow-up to teach the Slack/Telegram prompts about results (left out to respect their budgets).
Motivation#
Codemode scripts are exactly one async (itx) => {...} expression. When a script returns data, the model can only reuse it by copy-pasting JSON into its next script (token waste, transcription errors) or re-fetching it. Oversized results are worse: they spill to script-results/ workspace files and the next script must JSON.parse(await itx.workspace.readFile(...)) by hand.
Fix: a preamble — TypeScript injected above the script at both typecheck and execution. Two sources:
- Platform-derived
resultsarray — prior script results, addressable and typed. - User/agent-defined entries via events — constants, helpers, anything:
const TECH_CHANNEL_ID = "c1234";
function myHelper(input: string) { /* ... */ }Decisions (grill session, 2026-08-06)#
-
Capability host owns preamble state. New events
capability-host/preamble-set {key, code}(keyed upsert) /capability-host/preamble-removed {key}on the capability-host contract (v0.5.0), reduced into host state; entries ordered by first-set offset. Every script door in the scope — agent output, slash commands, scheduler, publicrunScript— gets the preamble at typecheck + execution. (user-approved) -
Prior results are auto-provided by the platform, reusing the existing small-vs-spilled split — no new truncation algorithm. Small results embed as JSON literals (their literal type comes for free); large ones contribute their already-inferred type (
inferJsonType) plus an async loader. (user-approved) -
One
resultsarray, DERIVED — never stored as preamble code. The host reducesscript-run-settledinto a retained tail (last 20) and the assembler stitches the array fresh per run. No per-result events, no O(n²) array rewrites — the settlement event is the only durable storage.results[0]is newest;.data(never.result),.errorfor failures,as constfor literal types, and large rows getget data(): never { throw }steering to a typedload(itx). (user-specified shape) -
Writer surface: methods on the capability host —
await itx.capabilityHost.setPreamble({ key, code })/removePreamble({ key })/getPreamble().setPreamblecompiles the assembled preamble at set time and rejects only problems the candidate introduces (a stale entry never vetoes an unrelated set). -
Execution mechanics. Typecheck module injects the preamble between the
Itxalias and the script const — the emitted-JS path carries it for free. No-emit fallback wrapsconst fn = await (async () => { <preambleJs>; return (<code>); })()so preamble names cannot collide with harness symbols. -
Typecheck attribution. Any error on preamble lines downgrades the run gate to
unchecked(a preamble syntax error cascades misparses, so even script-line diagnostics are untrustworthy then). The advisory door labels preamble errorspreamble:N. -
Model visibility. Settlement render names the binding (
results[0].dataorawait results[0].load(itx));preamble-set/removedtranscribe as non-triggering developer context; the system prompt's fresh-scripts bullet teachesresults+setPreamble(prompt ceiling 4200 → 4250, documented in agent-prompt-budgets.test.ts). -
itx.docs.typecheckparity — the advisory checker includes the scope's preamble viagetPreamble().
Checklist#
- Contract:
preamble-set/preamble-removedevents + preamble entries + retained-settlements state (v0.5.0) — capability-host-processor-contract.ts, rows pinned tocapability-host-preamble.tstypes - Assembly: state → preamble text, shared by gate/execution/docs —
assemblePreamblerenders ts + js variants;retainedScriptResultclassifies settlements (16KB inline cap, 3KB inferred-type budget, 2KB error cap);__proto__JSON falls back to JSON.parse - Typecheck: preamble input, module-scope injection, line mapping, downgrade-to-unchecked — virtual-project.ts
assembleScriptProject/checkItxScriptForExecution/checkItxScript+ newcheckPreamble;formatProblemslabelspreamble:N - Execution:
scriptWorkerReftakespreambleJs; async-IIFE wrap on the no-emit fallback — script-execution-entrypoint.ts - Host: reduce (upsert/remove/retained tail), verbs with set-time gate diffing with/without candidate,
getScriptResultpoint read by settle idempotency key, preamble threaded from head state — capability-host-processor-implementation.ts, durable object, deps - RPC surface + regenerated
itx-api.generated.ts— CapabilityHostRpcTarget setPreamble/removePreamble/getPreamble/getScriptResult; docs.typecheck parity - Agent processor: render names the binding (small vs large); preamble transcription (non-triggering); prompt teach — agent-processor-implementation.ts, agent-defaults.ts
- Tests — capability-host-preamble.test.ts (assembler + verbs, 11), virtual-project.test.ts (+6 real-compiler: end-to-end results typing incl. the
nevertrap, unchecked downgrade, preamble:N attribution, checkPreamble), capability-host-processor.test.ts (+3: derive+inject, gate threading, retention cap), agent-processor.test.ts (+2 & extended render assertions), script-execution-entrypoint.test.ts (+2 fallback wrap) -
pnpm typecheck && pnpm lint && pnpm knip && pnpm format && pnpm test— all green locally
Implementation notes#
Post-review round (2026-08-06)#
-
Misha: ts/js dual render → single marked source (
tsOnly()spans,toTs/toJsderivations); getScriptResult-over-workspace rationale documented in the PR thread; line-number attribution confirmed (prelude offset absorbs the preamble). -
Preview-5 field test: the model followed the spill render's fenced
JSON.parse(readFile)recipe instead of the loader footnote — the recipe IS the prompt, so spill renders now lead with theresults[0]recipe and the workspace path is a parenthetical. -
Bugbot (all three confirmed real): syntax-cascade preamble errors could brick a scope (fixed: set-time gate counts any script.ts error; run gate re-checks the bare script before blocking); set-gate line-shift false rejects (fixed: position-stripped diff); spill recipe named
.loadfor rows that only have.data(fixed: recipes key on the host's compact-JSON threshold). -
The
resultsarray is assembled at the at-head pass from the same head state as capabilities, so a result settled in the same delivery as the next request is visible to it. -
Slack/Telegram system prompts were deliberately NOT updated (their own budgets); their scripts still GET the preamble — the scope is the stream — they just aren't taught it. Possible follow-up.
-
getScriptResultreads the settlement event back bycapability-host/script-run-settled@<executionId>, so loaders work for any execution that ever settled in the scope, retained or not, and for every script door (no workspace coupling). -
Set-time gate compares problems with vs without the candidate (string-set diff) — a scope already broken (stale entry, rotted inferred type) doesn't block new sets, mirroring the run gate's never-block-on-scope-code stance.