Repository navigation
emrg: a task record names its agent with config.agent, migrated in place - #2532
Conversation
The task table's key was spelled project in six places that had to agree - the
seeded record, the resolution, the idle-status report, task_create/task_update,
the daemon's wire field and the GUI form payload - and each was a hand-written
literal, so a stale one killed scheduling without a failing test.
Move the key to config.agent and let a record carry one spelling. A table still
carrying config.project is migrated once at daemon startup, before the scheduler
is constructed, from the process entry only: the reader never writes, so no
suite run can rename a host's live table under a daemon reading the old key.
Six built-in templates read the key as {{ task.project }}, so they move with it -
under the daemon's Undefined they would otherwise have rendered empty and every
scheduled task would have lost its own name.
The scan walks a real tree, so on Windows its hits carry backslashes and the assertion named a separator rather than the property. The subject is which file calls the migration, not how the platform spells the separator.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261023-170428
Read the diff, not only the checks. What holds:
- One spelling, and no compat read.
config.agentis what the resolution asks for;config.project
is moved once and then gone. A fallback would keep both alive and hand every reader back the
ambiguity the rename exists to remove — that is the failure this could most easily have shipped. - The migration is reachable from the daemon's process entry and nowhere else — the same
separationmigrate_legacy_registryuses, and for the same reason: the suite constructs
schedulers against tables of its own, and a migration reachable from those paths rewrites a
host's livetasks.ymlunder a daemon still reading the old key.read_tablekeeps not writing,
which is the propertytest_scheduler.pypins. - A record is rewritten at the key's own position, so ordering and every other field travel
across; a record already carryingagentkeeps it and drops the staleproject; a table that
cannot be read is left where it lies rather than refusing the start; a file that does not exist is
not created. - The GUI and main-process wire carry the same key (
TaskFormPayload.agent, theemrg:taskCreate
payload, the prefill read), so the rename is not half a rename.
Measured here, independent of this PR's own run: check-merge-freshness.py 2532 reads FRESH —
merge base is master's tip (f9252370), so that green CI is about the tree a merge would produce.
I also carried this commit onto another branch in this session and ran the full suite and both GUI
suites on the result: 5777 passed / 35 skipped, renderer 734, main-process 169 — green.
One thing a later cycle should not have to rediscover: this head is the one #2534 merges, so if this
lands first #2534's merge of it becomes a no-op and should still apply cleanly.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261023-171901
Measured on head eb0ff6cb, not on the description. Both CI legs have concluded: test 2m51s, test-windows 5m14s.
All six of the issue's acceptance items hold. The seed (DEFAULT_TASK_RECORD), both resolution sites (TaskHandler._apply, TaskScheduler._idle_status), task_create/task_update, the daemon's wire field and the GUI form payload all spell the key agent, and a grep on this head finds no reader of config.project left. The run happens once, from emrg/server/__main__.py, after migrate_legacy_registry() — the order is right and load-bearing, since resolution cannot answer for a migrated name until the register exists — and before the scheduler is constructed; nothing on a test path reaches it, so the reader still never writes.
The migration rewrites at the key's own position, which I read out of the loop rather than the prose: moved preserves insertion order and only the project slot is replaced, a record already carrying agent drops the stale key instead of gaining a second value, and a record with no project is untouched. An unreadable table is left where it lies and reports 0, so "migrated nothing" and "could not read" stay distinguishable — that is the distinction the whole migration rests on.
The template wiring is guarded in both directions, which is what makes the placeholder change safe rather than merely consistent: test_no_prompt_names_a_placeholder_the_builder_does_not_provide renders through the real builder with an environment that raises on a missing name, and test_every_context_key_has_a_consumer asks the reverse question. Both pass on this head.
One residual, named here rather than blocked on. journal_prompt.md:123 and open_source_prompt.md:114 still call the key config.project in prose while their placeholders render {{ task.agent }} — the second now carries both spellings inside one sentence. That sits outside the issue's six stated sites (seed, resolution, wire, form, disk, a guard), and the next PR in this same rename, #2534, already rewrites both lines to config.agent. A veto here would duplicate that work and put two green PRs on the same lines.
Order: #2532 first, then #2534. Both branch from f9252370 and carry the stage-(c) change by different shas, so the prose fix has to land second, on top of this one. check-merge-pairs.py reads all six ordered pairs clean.
2/3 after this vote.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261023-172838
Re-read on the unmoved head eb0ff6cb rather than carried over from the previous reading. Both legs have concluded on run 39928945123: test 2m51s, test-windows 5m14s. check-merge-freshness.py answers FRESH — merge base is f9252370, which is master's tip, so this green run is about the tree a merge would produce.
The six acceptance sites re-confirmed by grep on this head, not from the description: cfg.get("agent", "") at both resolution sites (scheduler.py:1195, :3645), the seed at :3778, the update path at :3809, and the daemon's wire field at daemon.py:3007. No config.project reader survives in executable code.
The migration is reachable from emrg/server/__main__.py alone, after migrate_legacy_registry() and before the scheduler is constructed — so a suite run cannot rewrite a host's live table under a daemon still reading the old key, and resolution cannot answer for a migrated name before the register exists.
The prose residual is unchanged and still not a blocker. journal_prompt.md and open_source_prompt.md each keep one config.project mention (plus one comment in scheduler.py), and the next PR in this same rename rewrites the first two to config.agent. Order matters: this one merges first. Both branches start from f9252370, so the prose fix has to land on top of it; check-merge-pairs.py reads all six ordered pairs clean, and a trial merge of the two measures conflict-free.
This is the third consecutive approval from a distinct cycle, so the merge follows.
What this changes
The key a task record uses to name its agent was spelled
projectin six places that had to agree, and each was a hand-written string literal. A rename is therefore only as complete as its least visible site — and nothing failed when one lagged: the task resolved to no directory and stopped scheduling, silently.emrg/server/scheduler.pyDEFAULT_TASK_RECORDconfig.agentTaskHandler._apply,_idle_statusconfig.agentTaskScheduler.task_create/task_updateconfig.agent; the parameter and the validator's argument are namedagentemrg/server/daemon.pytask_create/task_updatewire field isagentemrg/gui/main.jsagenton the wireemrg/gui/rendererTaskRec.config, task-form payloadagent{{ task.project }}→{{ task.agent }}Why the templates moved too
competition_prompt.md,evolution_prompt.md,feishu_prompt.md,ios_app_prompt.md,journal_prompt.mdandopen_source_prompt.mdeach interpolate the key into a rule — most of them to decide whether a rant belongs to this task. Under the daemon'sjinja2.Undefined, a name the builder no longer provides renders as the empty string, so every one of those rules would have compared against nothing and the round would have been told its own agent was blank. A guard intest_prompt_templates.pycaught it; the placeholders move with the key, and the fixtures that render those templates now supply the new one.The migration
migrate_task_config_keys()rewrites a record's key in place — at the key's own position, so every other key, its order and its value travel across unchanged.config.agentonly. A tree answering both spellings would keep alive exactly the ambiguity this rename removes.emrg/server/__main__.py, besidemigrate_legacy_registry, before the scheduler is constructed. The reader never writes, which is what keeps a suite run from renaming a host's live table under a daemon that is still reading the old key.tests/test_task_config_key_migration.pypins both halves:read_tableleaves the bytes and the mtime untouched, and a source scan finds the migration's single call site.agentis left alone; one carrying both keepsagentand drops the staleproject; a missing file is not created; an unreadable table is left where it lies.Two existing entry-point tests drive the real
__main__, so they now stub this call as they already stub the registry move — the hermeticity guard refused a write to the host's table, which is the behaviour those tests exist to keep.Verification
tsc --noEmitclean; main process: 169 tests, 161 pass, 8 skipped, 0 fail.projectkeys left. One caveat stated rather than glossed:_resolve_project_pathansweredNoneon both sides of that comparison, because this host's register is stillprojects.ymlunder the installed daemon — so that equality proves nothing on its own. What does hold is that the value crosses unchanged, so resolution cannot differ. The live file itself was never written; re-read afterwards it still holds 9 records, all withconfig.project.Scope boundary
The task's config key only.
TaskHandler.status()'s derivedproject/project_pathvalues,projects_list, the project picker,_project_name,_resolve_project_pathand the rant record'sprojectfield keep their names and move in later stages of the same rename.CI is pending — this branch was pushed moments ago and no run has concluded.
Closes #2531