diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 93e152572..c84314810 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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//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 @ " 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.