Codemode script reuse: itx.capabilityHost.previousScriptHelper
Status summary#
Done, pending review. Five Codex-run eval rounds drove the design: rounds 1–2
failed and bought platform guards, round 3 passed (2 retries), round 4 passed
first-attempt after the alias redesign, and round 5 (typed value-form
parameters) reused first-attempt but failed the strict criterion when the
model chose to re-derive the algorithm while correcting stale message prose —
model stochasticity in the correction turn, not a mechanism failure; logged
below. Reviewer judgement wanted on: the prompt-ceiling raise (4250 → 4350)
and the done-row contract bump (0.6.0 → 0.7.0).
Problem#
Agents re-derive the same codemode script every time a request repeats with different inputs:
- user: "prime factorize 23409823948238439732889"
- agent: writes long complicated codemode prime factorization algorithm
- user: "prime factorize 5489334582393292300937"
- agent: writes the same long algorithm again, spending the same tokens
The script text of every past run is already durably journaled
(events.iterate.com/capability-host/script-run-requested on the scope
stream). The agent should be able to point at a past script, name which
literals were "the inputs", and call it again with new inputs:
async itx => {
const primeFactorize = await itx.previousScriptAsHelperFunction({
eventOffset: 1234,
parameters: [{ name: "number", content: "23409823948238439732889n" }],
})
return await primeFactorize(itx, { number: 5489334582393292300937n })
}Design (as built — redesigned after Misha's review)#
The original build manufactured a plain-function callable inside the dynamic-worker harness ("RPC can't return a function"). Misha flagged that as a red flag and relaxed the API to a handle with a method — which is the itx surface's native shape, so the whole harness workaround was deleted:
const h = await itx.capabilityHost.previousScriptHelper({
eventOffset: results[0].scriptOffset, // every results row carries its request-event offset
parameters: { n: 1234567890n }, // the value the old script used inline
})
return await h.run({ n: 987n }) // typed: { n: bigint } — "987n" is a gate errorIt lives on the capability host (next to runScript/getScriptResult/ setPreamble — where the script journal lives), works in every runtime (CLI, REPL, workers, codemode), and is typed via the generated api.
previousScriptHelper({ eventOffset, parameters }):
eventOffsetmust be the run'sscript-run-requestedevent offset — every retained results row hands it to scripts asresults[N].scriptOffset(Misha's follow-up; this deleted an earlier lenient three-offset resolution and its agent-lane idempotency scan). Any other event kind errors with the fix named.- Parameters are primitive values, not text (Misha's follow-up): the
platform renders each value's candidate literal spellings (strings across
quote styles when escape-free; bare number/bigint/boolean literals
boundary-matched so
42can't hit142) and requires exactly one occurrence in total, reported per-spelling in the error. The occurrence is swapped for a generated alias (__reuse_<name>, deterministically suffixed until free — also Misha's suggestion), so parameter names never collide with the script's own identifiers, andrun(vars)is typed from the parameters object through the generated api's generics. - Returns a
ReusableScripthandle;run(vars)renders an envelope — the new values as consts (serialized as JS literals, bigint included) bound to the aliases — and submits it as a journaled child script run viarunScript, so the reuse is a self-contained auditable record.
Supporting changes:
doneresults rows (contract 0.6.0 → 0.7.0): a void-returning success previously left noresultsrow, making the common send-message-and-finish script unaddressable. Every settled script now keeps an offset handle; void successes render as{ offset, executionId, done: true }.- Prompt teach: one bullet in the output-formatting section; prompt ceiling raised 4250 → 4350 with the raise argued in the constant's comment log (the sanctioned mechanism per that file).
- Regenerated itx api (
ReusableScriptinterface + the host method).
Checklist#
-
reparameterizeScriptpure function + unit tests inscript-reuse.ts; validation errors name counts so the agent can self-correct (observed live: it renamedn→targetNumberafter the collision error) - offset→script lookup + transform RPC now
previousScriptHelperonCapabilityHostRpcTarget, next togetScriptResult; returns theReusableScripthandle -
harness closuresuperseded by the redesign: plain RpcTarget handle; harness special-casing reverteditx.previousScriptAsHelperFunction - child-run execution path journaled via existing
runScript; no deadlock — the processor starts executions withrunInBackground, verified live (child at offset 1156 ran while the parent awaited it) -
prelude typing onsuperseded: typed via the generated api (generic method + verbatim return annotation); prelude hack revertedItx - regenerate itx api
pnpm generate:itx-api - prompt documentation agent-system-prompt.md + ceiling raise
-
donerows for void results contract 0.7.0; new preamble tests - structural e2e test
script-reuse.itx.e2e.test.ts, passing against local dev — no model turns -
evals/script-reuse/eval.mdwritten 10-digit-prime semiprimes force a real algorithm; criteria demand the reuse mechanism, not just answers -
scriptOffseton results rows + strict request-offset demand Misha's follow-up: contract 0.8.0; every retained row hands scripts the request-event offset, and previousScriptHelper accepts only that — the lenient settle/assistant resolution and agent-lane idempotency scan are deleted - multi-turn UI spec + video
specs/agent-script-reuse.spec.tson #2523's intercepted/* models: both turns' scripts execute for real, the spec opens each turn's codemode snippet, VIDEO_MODE recording embedded in the PR body - eval run via
pnpm eval script-reusepassed on round 3 (evals/runs.ignoreme/script-reuse/1787769275338/): reuse mechanism confirmed, 265 vs 2,408 chars, answers verified independently. Rounds 1–2 failed and each produced a product guard (failed-source rejection; statement/template content rejection) - Track reusable eval helpers
evals/script-reuse/run-turn.tsdrives each turn through full agent settlement;report.tsaudits every agent stream, turn boundary, message, token-usage event, script body, and settlement without depending on leftover ignored files
Assumptions made (user was AFK)#
- Exactly-once occurrence enforced per parameter, as specified; a literal hardcoded twice (e.g. in a sendMessage template) stays un-swapped — the live agent noticed via its delivery receipt and sent a correction message.
- The callable executes as a journaled child run rather than in-isolate
(
evalis unavailable in workers; observability seemed more valuable than journal thrift anyway). - Void-result
donerows change the preamble surface for everyone — judged worth it because reuse is otherwise undiscoverable for the most common script shape. Precisedone in the value-form redesign:typeof-based vars typing deferredrun(vars)is inferred from the parameters object.- The prompt ceiling raise (4250 → 4350) is flagged for explicit review.
Implementation log#
- Explored codemode internals + evals harness (two subagent reports).
- Built transform + envelope as pure functions; unit tests first failed on a bad substring assumption, fixed.
- e2e passed on the first live run (settle-offset door, child run, bigint round trip).
- Live probe 1 (easy numbers, small script): agent ignored the helper and
rewrote — exposed (a) void scripts leave no
resultsrow, (b) rewriting a short script is rational. Led todonerows + harder eval numbers. - Live probe 2 (2^31/2^32-adjacent primes): agent tried
eventOffset: <assistant offset>; resolution failed (agent-lane idempotency prefix) — fixed with the bounded forward scan. Model also passed a bigint as a string (teach now says123nnot"123n"), and then factored the too-recognizable number in its own head (!) — eval numbers changed to arbitrary 10-digit primes. - Live probe 3: full success — reuse via
results[N].offsetincluding self-correction of a rejected parameter name and of the reused script's hardcoded message prose. - Prompt ceiling: bullet compacted twice; final overage was exactly 1 char (17401/17400) before the last trim landed it at the raised ceiling.
- Codex eval round 1: FAILED — the model's first attempt hit the
nname collision,results[0]shifted to that failed attempt's error row, and the model pointed the next attempts at its own failed reuse script, nesting reuse-of-reuse until it rewrote the algorithm. Guard added: a failed run is rejected as a reuse source with the fix in the message. - Codex eval round 2: FAILED — splice-thinking: content was a whole
const n = 123n;line (child died withn is not defined) and a template-string interior. Guards added: statement-shaped and template-interior content rejected with the value-substitution recipe. - Codex eval round 3: SUCCESS — two failed attempts (caught by the new teaching errors), one small results inspection, then a 265-char reuse script; old-number prose corrected in a follow-up message per criteria.
- Codex eval rounds 6-8 (edits, post-review): round 6 FAILED under the new
strict criteria (reuse worked but no
edits— stale message sent, then corrected; the criteria no longer accept that), driving the stale-leftover guard. Round 7 FAILED on the ordering trap (a broad prose edit ran before parameterization and clobbered the value literal), driving edits-after- parameterize. Round 8 (1787816789588): SUCCESS — reuse via parameterize + edits in a 251-char script, no incorrect message ever emitted, no re-derivation. - Codex eval round 5 (typed value-form, run
1787771786047): FAILURE on the strict criterion — the 373-char reuse script and platform child both succeeded first-attempt, but the agent then wrote a fresh 2,097-char Pollard-rho implementation to correct the child's stale message label (rounds 3–4 wrote the short correction from the delivery receipt instead). Both equations correct. Takeaway: the mechanism is solid; the correction turn is model-stochastic. The Codex runner also contributed tracked helpers (run-turn.tssettle-waiting driver,report.tsstream audit).
Follow-up ideas (deliberately out of scope)#
-
Gate policy discovery (typed-spec work): the execution gate blocks only provable errors (syntax range + near-miss typos) — plain TS2322 assignability mismatches NEVER block, by design ("a script must never be stopped by what the checker doesn't know").
satisfiesfailures are TS1360 (syntax range) and DO block — which makes satisfies the reliable in-script type-assertion idiom. Also: tsgo doesn't report unused@ts-expect-error(TS2578). Worth deciding deliberately whether either should change. -
A
contains/search addressing mode so the agent can name a past script by content rather than offset. -
Stale-prose corrections: when a reused script's message carries the old input hardcoded in prose, models sometimes re-derive the answer instead of writing the short correction from the delivery receipt (eval round 5). A settlement-render nudge on reuse child runs could steer this.
Redesign log (post-review)#
Misha's feedback replaced the harness-manufactured callable with a plain RPC
handle (ReusableScript.run(vars)) on the capability host, and replaced
name-collision errors with generated aliases (no collision class at all —
it had been the first-attempt failure in every eval round). The harness and
typecheck-prelude special cases were reverted wholesale; the collision
follow-up idea above became moot.
Eval round 4 (new API): SUCCESS on the first attempt — zero failed reuse
attempts (round 3 needed two corrective rounds before the aliasing change).
Turn 2: one 239-char reuse script via results[0].offset, the journaled
child run, and the allowed prose-correction message. 562 chars of turn-2
agent-authored script vs 2,359 for turn 1
(evals/runs.ignoreme/script-reuse/1787770331931/).