Context
Found via a real calque spawn-run --bucket ... --ami ami-09b4d1958a23cbe9e --instance g6.2xlarge modal/forecasts_app.py --i-understand-this-spends-money run against real AWS (AI-Almanac's forecasts_app.py). The classified callable, run_season_forecast_bundle, has this real body:
def run_season_forecast_bundle(job_id, model_id, config, season_params):
return _season_bundle_impl(job_id, model_id, config, season_params)
_season_bundle_impl is a plain module-level sibling function (not itself an @app.function). The real run acquired an instance, ran host-mode (no docker needed for this callable), and failed immediately:
LEAK[unhandled_case] item 0 failed in payload: name '_season_bundle_impl' is not defined
Root cause
cmd/calque/run.go's collectLocalExtras (transitive-closure resolution of sibling functions/constants/imports/classes a picked unit's body bare-references, calque#92/#139/#146/#147) is called by dryRunWarm and manifestBodyForUnit — the paths run --dry-run/real/fleetrun all go through. internal/exec/spawnshard.go's ResolveSpawnCallables/BuildSpawnManifests and cmd/calque/spawnrun.go's runSpawnShard never call it at all (confirmed by grep — zero references to Extras/collectLocalExtras anywhere in spawnshard.go or spawnrun.go). SpawnCallable.MethodBody is shipped completely verbatim with no sibling resolution whatsoever.
This is exactly the class of gap calque#92/#139/#146/#147 already closed for every OTHER real-run path — spawn-run was simply never updated to use the same mechanism when those were built.
Why this matters
This isn't a narrow edge case — a .spawn()'d callable delegating to a private helper function (keeping the @app.function-decorated wrapper thin) is a completely ordinary Python pattern, and it's exactly what broke here. Any real .spawn()-shaped script with this common structure will hit the identical NameError today.
Ask
runSpawnShard/spawnManifestBody (cmd/calque/spawnrun.go) should resolve and ship sibling functions/constants/imports/classes the SAME way manifestBodyForUnit already does for real/fleetrun — likely via the same collectLocalExtras call, given SpawnCallable's Key (the callable name) and the parsed ir.App. This needs SpawnCallable/ResolveSpawnCallables to either carry enough info for collectLocalExtras to run against it (it takes a warmUnit, calque's own internal shape — may need adapting or a parallel resolution path), or for spawnrun.go to call collectLocalExtras directly per-callable using the already-available ir.App.
Real repro: testdata/scripts/spawn_fanout.py (the existing calque#88 fixture) doesn't currently exercise this — its worker_a/worker_b bodies are self-contained. Worth adding a sibling-function-referencing spawn callable to that fixture (or a new one) to regression-test whichever fix lands, mirroring how testdata/scripts/plain_function.py/free_refs_*.py already cover this for the non-spawn paths.
Context
Found via a real
calque spawn-run --bucket ... --ami ami-09b4d1958a23cbe9e --instance g6.2xlarge modal/forecasts_app.py --i-understand-this-spends-moneyrun against real AWS (AI-Almanac'sforecasts_app.py). The classified callable,run_season_forecast_bundle, has this real body:_season_bundle_implis a plain module-level sibling function (not itself an@app.function). The real run acquired an instance, ran host-mode (no docker needed for this callable), and failed immediately:Root cause
cmd/calque/run.go'scollectLocalExtras(transitive-closure resolution of sibling functions/constants/imports/classes a picked unit's body bare-references, calque#92/#139/#146/#147) is called bydryRunWarmandmanifestBodyForUnit— the pathsrun --dry-run/real/fleetrunall go through.internal/exec/spawnshard.go'sResolveSpawnCallables/BuildSpawnManifestsandcmd/calque/spawnrun.go'srunSpawnShardnever call it at all (confirmed by grep — zero references toExtras/collectLocalExtrasanywhere inspawnshard.goorspawnrun.go).SpawnCallable.MethodBodyis shipped completely verbatim with no sibling resolution whatsoever.This is exactly the class of gap calque#92/#139/#146/#147 already closed for every OTHER real-run path —
spawn-runwas simply never updated to use the same mechanism when those were built.Why this matters
This isn't a narrow edge case — a
.spawn()'d callable delegating to a private helper function (keeping the@app.function-decorated wrapper thin) is a completely ordinary Python pattern, and it's exactly what broke here. Any real.spawn()-shaped script with this common structure will hit the identicalNameErrortoday.Ask
runSpawnShard/spawnManifestBody(cmd/calque/spawnrun.go) should resolve and ship sibling functions/constants/imports/classes the SAME waymanifestBodyForUnitalready does forreal/fleetrun— likely via the samecollectLocalExtrascall, givenSpawnCallable'sKey(the callable name) and the parsedir.App. This needsSpawnCallable/ResolveSpawnCallablesto either carry enough info forcollectLocalExtrasto run against it (it takes awarmUnit, calque's own internal shape — may need adapting or a parallel resolution path), or forspawnrun.goto callcollectLocalExtrasdirectly per-callable using the already-availableir.App.Real repro:
testdata/scripts/spawn_fanout.py(the existing calque#88 fixture) doesn't currently exercise this — itsworker_a/worker_bbodies are self-contained. Worth adding a sibling-function-referencing spawn callable to that fixture (or a new one) to regression-test whichever fix lands, mirroring howtestdata/scripts/plain_function.py/free_refs_*.pyalready cover this for the non-spawn paths.