P4-fix3b: name the row a late conflicting bind leaves behind

The sentence said no project row survives a conversion refusal on either
path. That is true at both binding checkpoints, and it is not true for the
microseconds between the second read and the bind: the request has already
created its row there, and no registry primitive removes one.

The clause is now conditioned on the checkpoints, the restored lane and the
silent 409 are stated, and the residual is named: the empty row stays, the
registry has no removal primitive, retiring it would reserve the id (and the
display name the in-task path derives it from) forever, and the owner can
delete the row like any other project. Neighbouring lines are byte-identical.
This commit is contained in:
Ouroboros 2026-09-12 11:37:47 +03:00
parent 8f2950942a
commit 927ee5fb07

View file

@ -1741,7 +1741,7 @@ Pooled workers retain their slot until root post-task synthesis settles, for API
Ouroboros remains one identity across Main, project rooms, and Background Consciousness. A project is a focused working room, not an isolated sub-mind: unified dialogue memory remains available to the one agent, while an executing project task preferentially receives its own thread, journal, workpad, and project knowledge. `project_facts.py` routes project facts to `projects/<id>/knowledge`; subagents inherit the root's resolved project id and never derive a new one; there is no per-project identity or scratchpad. An id minted from a DISPLAY name collapses dash runs and carries a short deterministic suffix when the name held characters the slug could not keep, so two different non-Latin names can no longer share one project; the normalizer itself is unchanged, so existing ids are never re-slugged and stay reachable by their explicit id.
The projects registry owns immutable project identity, canonical chat id, optional working directory, lifecycle/tombstone state, routing generation, and activity revision. Admission persists the resolved project id in the task itself. `project_lease.py` serializes assignment of pooled roots by Project while allowing their own subagent trees; it is not a physical-folder lock and does not withhold tools from ordinary conversation. Binding/history files support routing and presentation, not the lease. Delete closes routing, cancels/quiesces the tree, and tombstones only after settlement, preserving everything for recovery. The durable binding is the SINGLE truth about a task's project: the in-task scope guard and `project_facts.resolve_project_id` read it FIRST, ahead of `task["project_id"]` and a worker's in-memory `ctx.project_id`, which are copies a mid-run conversion never reaches (a guard reading only the copy is how a task already bound to one project minted a second, empty one). `ensure_project_scope` can create or bind the current root to one project mid-execution: it persists the durable registry binding first, then marks the live queue/lease surface under the queue lock so the lease recognizes the running task as a lane occupant; it is idempotent for the same project, and a task BOUND elsewhere is renamed rather than re-scoped - its scope call carries the requested display name to the project it already belongs to and creates nothing. A project-SCOPED but unbound run keeps the older refusal, since there is no durable project to rename, and a child still cannot escape the inherited scope. An unreadable bindings store is disclosed once and read as "no binding" on every path, hot and guarded alike: the refusal is reserved for the measured case, a readable binding to another project. That refusal precedes every side effect on BOTH conversion paths - no project row, lease mark, broadcast or announcement survives it - the UI conversion answers 409 naming the bound project by id and display name in one human sentence. It re-reads that authority after the naming step and before its first side effect, since naming can await a model call while the task binds itself, and a durable bind refused after the mark restores the lane to the value it held, so no lane keeps a project the binding does not name. The lease mark is fill-only by default; the single exception is a conversion that already holds the binding it is about to write, which moves the lane onto that binding.
The projects registry owns immutable project identity, canonical chat id, optional working directory, lifecycle/tombstone state, routing generation, and activity revision. Admission persists the resolved project id in the task itself. `project_lease.py` serializes assignment of pooled roots by Project while allowing their own subagent trees; it is not a physical-folder lock and does not withhold tools from ordinary conversation. Binding/history files support routing and presentation, not the lease. Delete closes routing, cancels/quiesces the tree, and tombstones only after settlement, preserving everything for recovery. The durable binding is the SINGLE truth about a task's project: the in-task scope guard and `project_facts.resolve_project_id` read it FIRST, ahead of `task["project_id"]` and a worker's in-memory `ctx.project_id`, which are copies a mid-run conversion never reaches (a guard reading only the copy is how a task already bound to one project minted a second, empty one). `ensure_project_scope` can create or bind the current root to one project mid-execution: it persists the durable registry binding first, then marks the live queue/lease surface under the queue lock so the lease recognizes the running task as a lane occupant; it is idempotent for the same project, and a task BOUND elsewhere is renamed rather than re-scoped - its scope call carries the requested display name to the project it already belongs to and creates nothing. A project-SCOPED but unbound run keeps the older refusal, since there is no durable project to rename, and a child still cannot escape the inherited scope. An unreadable bindings store is disclosed once and read as "no binding" on every path, hot and guarded alike: the refusal is reserved for the measured case, a readable binding to another project. That refusal precedes every side effect on BOTH conversion paths whenever the binding is readable at one of those checkpoints - no project row, lease mark, broadcast or announcement survives it - the UI conversion answers 409 naming the bound project by id and display name in one human sentence. It re-reads that authority after the naming step and before its first side effect, since naming can await a model call while the task binds itself, and a durable bind refused after the mark restores the lane to the value it held, broadcasts nothing and answers the same 409, so no lane keeps a project the binding does not name. One residual survives that last case, a conflicting bind landing in the microseconds between the re-read and the bind: the empty project row the request had already created stays behind, because the registry deliberately has no primitive that removes a row and its delete lifecycle tombstones the id permanently instead, which would cost the owner that id (and, in-task, the display name it derives from) forever. The row holds no task and no binding, and the owner deletes it like any other project. The lease mark is fill-only by default; the single exception is a conversion that already holds the binding it is about to write, which moves the lane onto that binding.
Project `journal.jsonl` records curated milestones and `workpad.md` retains active working context; focused context includes the workpad in full and recent journal rows with a visible pointer to older entries. On root completion, only high-signal blockers, questions, and interface contracts are mirrored once from the ephemeral task-tree ledger into the durable journal, and a finished root whose effective working tree is not the registered `working_dir` writes one typed "work lives at <path> @ <sha>" journal row from facts the task record already holds. A project digest gives consciousness a concise completion signal without pretending to be the raw project memory.