Timeline Code Capture
A timeline built with @nodetool-ai/sandbox-timeline stores the code that
builds it in document.source. This page defines what that code is and how
it is produced.
The rule
The stored code is a retained program, not the script the author ran. The retained program holds only what rebuilds the document:
- the scene callbacks, as source text
- the declarations those callbacks use, as source text, when they are pure
- the values those callbacks read from anything else, as literals
A research call, a generation, a fetch or a model call runs once, in the author’s run. Its result enters the retained program as a literal. A rebake runs the retained program with no capabilities, so it cannot repeat a side effect.
Capture
buildEntryModule (packages/agents/src/js-sandbox-worker/interpreter.ts)
passes guest code that imports @nodetool-ai/sandbox-timeline through
captureTimelineScenes (packages/agents/src/timeline-capture.ts). Every
guest run passes through this function, so every agent path gets the same
capture.
The transform finds each <receiver>.scene(name, seconds, fn, extra?) call
and appends one argument: the capture record. The record adds no line
breaks, so stack lines stay correct. It holds:
| Field | Content |
|---|---|
src |
The source text of fn, or the name of the function it refers to |
decls |
The retained declarations that fn needs, with source text and source order |
imports |
The pack imports that fn and the declarations use |
snap |
A thunk for each value to snapshot, called when the scene is built |
errors |
Diagnostics for values that cannot be kept |
Classification
The transform resolves each free variable of fn with eslint-scope, then
follows the free variables of each retained declaration in the same way.
| Variable | Result |
|---|---|
A standard global (Math, JSON, Number, …) |
Kept as is |
An import from a @nodetool-ai/sandbox-* pack |
Kept as an import |
The video(...) binding |
Printed from its recorded options |
A const or function whose initializer has no await, is not mutated, and uses only kept variables |
Kept as source text |
Any other variable: let, a loop variable, a parameter, a result of await |
Snapshot of its value when the scene is built |
nodetool, tools or another capability global |
Error |
| A snapshot value that is a function, a class instance or a cycle | Error |
A declaration that uses a snapshot is kept, but it is printed inside the scene’s own block, after the snapshot.
The printed program
v.save() prints the retained program from the capture records:
import { video, rad } from "@nodetool-ai/sandbox-timeline";
const v = video({"width":1920,"height":1080,"fps":30,"palette":{}});
const PAD = 120; // kept declaration
function title(s, text) { /* kept helper */ }
const __scene0 = (() => {
const headline = "Rain returns to the valley"; // snapshot
return v.scene("intro", 3, (s) => { title(s, headline); });
})();
v.series([__scene0, v.transition("fade", 0.5, {}), __scene1]);
v.__restore({ /* tracks, clips, markers and fields added after series */ });
return await v.save(nodetool.timelines, { name: "…", ops: [] });
Everything that v.adjust, v.audio, v.music, v.midi, v.beats and
v.document add after v.series is stored as data in v.__restore.
The embed gate
v.save() passes the printed program to nodetool.timelines.code.set with
require_match: true. The host runs the program in the hermetic bake. It
attaches the program only when the bake gives exactly the saved document.
In all other cases it writes nothing and returns a warning, which v.save()
returns in code.warnings. The usual causes are Math.random() or
Date.now() in a scene callback, and a scene clip that changes after
v.series.
el.react() needs an audio decode that the bake cannot do, so a timeline
that uses it is not embedded.
Not in this phase
- Keys and overrides. Clip ids come from call order in each scene. A
keyoption,s.each(items, {key}, fn)and per-instance overrides come next. With them, a hand edit becomes an override instead of a conflict. - Expression editing. Expressions are not stored as ASTs. A rebake runs the scene callback again, so a changed constant still moves every value that depends on it.