Review findings (claude-opus-5 slot): `test_chat_history_paging_browser.py` still looked for the
routing receipt's Open Project button inside the note it moved out of — the assertion now names
the shared `.msg-routing-actions` row and pins its place between the note and the timestamp;
the new static check in `test_projects_v6640.py` reads `chat_activity.js` with an explicit UTF-8
encoding (the file carries a non-cp1252 glyph, and the Windows PR job has no UTF-8 mode).
Advisory: the lifecycle and answerable state lists now have one JS home in
`question_presentation.js`.
Review finding (Claude Fable 5.1 slot, critical): `_task_activity_facts` projected only
quiz_id/state/asked_at/wait_for_answer from the owner_quiz block, so the `required_question`
pointer of the 3-second activity census arrived with empty question/options, and the browser
merged those blanks over the complete history row — the Main pointer flipped between the
question and the placeholder, and a settled pointer lost its option label.
- The census projection keeps question, options, answered_index, comment and wait_ended_at
(the task result is already read and memoized; no new I/O).
- `project_question_pointer` emits question/options only when known, like the answer fields.
- The browser's pointer merge drops empty question/options/project_name from any re-delivery,
so no producer can blank a painted row (pinned in chat_decision.test.js).
- `applyQuizStateFrame` observes the live frame once; the pointer repaints from the merged
observation (advisory A1).
- The live pointer frame in `message_bus.send_quiz` is built with constant keys plus explicit
optional assignments (`test_chat_outbound_matches_message_bus_sends` forbids `**` expansion);
generated inventories regenerated after the `contracts.py` comment paydown.
Roast findings (codex gpt-6-astra) folded in:
- The pointer row carries the question, option labels, recorded answer and the
wait facts from history, the live delivery and the activity census
(`project_question_pointer`, `owner_wait_projection`; both contract mirrors),
so Main paints it from the row alone. The IntersectionObserver hydration, the
settled-source cache and the Retry state are gone: freshness is the ordinary
history reconciliation plus the `quiz_state` frame, and task detail is read
only to open the original form.
- Lifecycle observation precedence: once a live frame closed a wait, an older
history row or a detail read begun before it cannot reopen «Waiting»; an
unavailable row keeps what is known; a settled question never reopens. The
production timeout frame (`wait_for_answer:false`, no `wait_ended_at`) is the
shape the tests use.
- Parity: history attaches the task's wait record to the Project room's quiz rows
(a wait the owner resumed by ordinary input leaves no frame behind), the quiz
card reads those facts, and the parity fixture now pins the rows Python emits
against what the browser reads from them.
- Wording leads with one word — «Waiting for your answer» / «Unanswered · …» /
«You answered» / «Replaced by a newer question» / «Status unavailable» — with
three action labels; status and source lines use meta ink (DESIGN: the owner
reads them to act). The wait-ended line names the default path the task took
and that silence was not consent; a late answer's toast says where it went.
- Previews bound the option and the comment separately and never cut for less
than the marker costs.
- Module map row for `question_presentation.js`; DEVELOPMENT 11 points at the
ARCHITECTURE data flow instead of restating it; a stale comment in
`owner_quiz.py`; `contracts.py` pays its 1600-line gate down by compacting
five comment blocks.
The Main-chat pointer for a required Project question read «Question answered in
<Project>» with its `View question` button jammed against the timestamp, and the
lifecycle words did not tell the owner whether a question was waiting for him.
- One shared action-row composition, `ui_helpers.createSystemMessageActions`, now
owns space above and below the buttons, wrapping and focus-ring clearance for
question pointers, Project lifecycle rows and routing receipts; a button never
sits in a nowrap text line again.
- One lifecycle vocabulary in the pure `web/modules/question_presentation.js`,
shared by the pointer and the quiz-card header, with the Python fallback in
`project_dialogue.project_question_pointer` pinned by a shared parity fixture.
- The pointer shows the question first, then the status, the recorded option and
comment (never a locally invented answer), and the Project as its source.
- The answer POST settles the card only on a valid recorded confirmation
(`ok`, `state`, a valid index or a non-empty comment); a malformed 2xx never
substitutes the local draft (the independent audit's fabricated-answer defect).
Docs replace the touched descriptions in DESIGN §5, ARCHITECTURE 03 and
DEVELOPMENT 11. Version carriers are untouched: a contributor PR into `ouroboros`
leaves the release version to integration.
A card row replayed from history now merges through the historical timeline
seam with its history id and position, so it sorts by its source position and
leaves the card when its page is released, like every other replayed timeline
item. The custody split derives the row id from the task's canonical answer
identity when the answer reaches the seam unregistered, and keeps the joined
host notice when no task can key it. The late acceptance row's head states what
the host holds (no settled verdict) and names a reviewer whose outcome is still
unknown. A placed row raises the unread badge like a standalone row did.
DESIGN §4 and ARCHITECTURE 03 name the rows that stay ordinary by design.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The earlier reflow to hold the 1500-line band dropped two facts: a failed call
still counts in the evidence total, and the completed-event normalization takes
its label, phase and terminal truth from the canonical projector. Both are back,
paid for by the cost-checkpoint note and the child branch, which say the same in
fewer lines.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The folded row's documented phases were invisible: only `error` had a colour,
so a turn mid-burst and a turn that lost a call looked like a settled one. The
row keeps its counts-only label and says the phase in ink, with the same
colour-only shape the error rule uses.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
A child's own progress frames carry the same typed `narration` fact a root's
do, but the subagent branch promoted every one of them, so a checkpoint or
fallback note inside a child's turn took over the child card's collapsed
activity line. Its non-terminal frames now follow the fact (absent stays
legacy narration), while the host's lifecycle, result and error frames keep
leading, and the collapsed line takes a child's activity only from a frame
that speaks in the turn's own voice. Two comment blocks around the branch say
the same in fewer lines, so the module stays at its 1500-line band bound.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The folded evidence row never reaches the generic timeline upsert — the owning
frame is skipped there and `upsertToolFoldRow` passes `inPlaceByKey` itself —
so the `tools|` entry in the prefix list decided nothing.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
A review slot's run registered under the reviewed task's id was treated as
that task's delegation: the loop-exit audit listed it as open delegated
execution (Done with warnings plus a standalone System bubble) and the
periodic orphan sweep cancelled the live reviewer once the task's durable
result read completed (issue #1006).
Custody kinds: `RunCustody.review_owned` (the durable `source` under the
review substrate, consolidated from the private recovery predicate) is now
read by the orphan/kill reconcile (a review-owned row is cancelled only
behind an owner cancellation, otherwise left live with a typed reason), the
terminal custody audit, the execution-evidence counters, the nanny hold, the
crash-recovery candidate lists and the pending-invocation recovery (a review
invocation is retained, never re-posted). SETTLED rows carry `source` and
`category`. Physical custody keeps seeing every run. The consumer matrix in
tests/test_custody_owner_kinds.py is the surface a new reader joins.
Card rows: a host fact about a task is a row of that task's card. The
producer stamps `card_row` (timeline | reviews) and `card_row_id` on the chat
row (persisted by log_chat, replayed by history, mirrored in ChatOutbound).
The custody audit becomes its own typed row (`custody_notice`, its own owed
delivery id; the outbox rebuild no longer appends a host line into the
assistant answer). The late acceptance settlement stamps a host-composed
`late_settlement` note on the panel projection and its row is placed in the
card's Reviews group, which prints the note verbatim. The browser attaches a
stamped row to the task's card record as one timeline item, live and on
replay, and falls back to the standalone System row only when no card record
exists. Single-body transports keep the joined host notice text.
Docs: ARCHITECTURE 01/03/06/11, DESIGN §4/§5, DEVELOPMENT 06/11 state both
rules; generated inventories regenerated.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The supervisor states a turn's totals more than once — the metrics event,
the terminal, a replayed summary — and each fact carries its own subset.
Replacing the whole snapshot let a later partial fact (a bare `tool_calls`)
erase the routing count that classified an addressing-only turn as a
receipt, and erase a known error count with it. Each field now keeps its
last known value until a fact actually states it, and `tool_call_counts`
counts as stated only when it is a non-empty object, so an empty map keeps
the live names instead of emptying the row behind Expand.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
A turn that reads five files drew five rows, so the narration around them
scrolled out of the block. Routine execution is now ONE stationary row per
block: it counts the calls, names the tools behind Expand, and keeps the place
and the timestamp of the first frame it counted. Failures and timeouts keep
their own row where they happened and still count in the total.
The accumulator is a per-invocation state map on the record rather than a pair
of counters, so duplicate, reordered and concurrent frames all settle on the
same reading: a status never regresses, an error is counted once, and a late
start cannot reopen a finished call. One builder produces the row for both the
live frames and the host's metrics, and the meta counts follow the same
reading, so the header cannot disagree with the row while a turn runs.
The host's totals replace the derived ones only where the host stated them. An
absent field stays absent instead of reading as zero, so a terminal that
carries `tool_calls` alone can no longer turn a block that only addressed work
into content. `noteToolMetrics` drops its per-call-row guard and always upserts
the same key; the keyed upsert makes a second row impossible.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Owner decision 16.09 (Q1 = A): host-authored notes stay visible timeline rows
but never become the card title or the collapsed activity line. Only the model's
narration does.
The progress projection reads the typed `narration` fact instead of promoting
every progress frame: a host note keeps its row, its phase and its markdown, and
loses only `promote`/`human`, which are exactly what feed the title and the
collapsed line. A frame without the key is a legacy frame and is promoted as
before, so older workers, supervisor-authored notes and stored rows are
unaffected. The progress reconstruction forwards the key from the history row,
so the replay of a turn behaves like the live turn did.
A turn whose progress was host notes only therefore keeps its coined name (or
the running placeholder) and shows an empty activity line.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
A progress frame carries the model's own round narration and the host's notes
about the turn (checkpoints, fallback, plan, acceptance, nudge, transport,
density) under one type, so the only way a reader could tell them apart was to
match the note's wording, which BIBLE P5 forbids.
The worker now stamps the fact instead. `_emit_progress` takes `narration` and
writes `progress_meta.narration` on EVERY frame it emits, so absence means "an
older worker or a row written before the fact existed" rather than "a host
note". `_emit_round_progress` is the single producer that passes True, for both
of its emissions; the loop-level notifier and the whole ToolContext ABI
(`emit_progress_fn`, one positional argument) keep the default.
The key needs no transport work: the delivery seam already spreads progress_meta
onto the top level of the live frame and of the stored progress row. It joins
the ChatOutbound contract in both mirrors and the progress-meta whitelist, so a
reload replays the same voice the live frame carried. `cancelable`'s comment is
reflowed, without changing a word, to keep contracts.py at its 1600-line cap.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The full battery on 3ac880fd9 failed one test: the JS effort list is a
hand-maintained mirror of config.EFFORT_SCALE and its guard reads the
`{ value: '<tier>' }` entries inside the EFFORT_OPTIONS literal; the folded
pair table hid them. The literal is back, two entries per line, so
settings_ui.js stays under the 1000-line band.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Owner decision (16.09, 1=A): an empty OUROBOROS_EFFORT_CONSCIOUSNESS slot now
means the Task / Chat effort, the same way an empty model slot means Main — a
wake-up is an ordinary Main turn and shares its request shape. A set value is
honored; an invalid one is treated as empty. The settings default becomes empty,
the Consciousness segmented field gains a "Same as Task / Chat" option, and the
configuration doc row says so.
Semantic merge fix for PR #970: its static UI contract pinned blockHasWork on
the shouldAlwaysShowTaskCard helper that this branch retired with the
bg-consciousness card kind; the fixture now pins the retired form (the
quick-test failure on 505f6f4ae).
The effort option table folds onto two lines so settings_ui.js stays under the
1000-line ratchet band.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Conflicts: supervisor/log_addressing.py and supervisor/worker_chat_lane.py (the turn
event queue carries both the initiator label and the lazy-naming callback),
web/modules/chat.js and model_wait.js (the target's chrome-follows-the-work model
wins; no lane placeholder survives), tests/test_log_forwarding.py (both blocks),
docs/architecture/03 (their chrome paragraph plus the consciousness wording),
docs/DOMAIN_MAP.md and the v7next inventories (regenerated). The merged get_tools
sits at the 300-line ratchet cap (one entry joined onto one line).
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The full host UI smoke lane on the merged head (workflow_dispatch; never run on
pull requests) showed three failures caused by #970. One was a real regression:
the viewport-preservation anchor picks a boundary inside a live card that
crosses the top of the viewport, and a wait-only block now carries no title
placeholder and no conversion button, so nothing inside it was anchorable and
the capture fell back to the card's own top — a wait update that shrank the
block moved the reader's messages by 40 px. Such a card now anchors on the first
message that follows it, which is what the reader is looking at (unit pin in
render_batch.test.js; the model-wait browser test passes again).
The other two were pins of text the sprint changed on purpose: the `Reason:`
label in front of a cause sentence and the answer excerpt in the Main
completion row. Both tests now pin the shipped shape. The remaining 34 lane
failures predate #970 and are filed as their own issue.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
- Observe does without skill_exec and run_ci_tests (an enabled skill's script; a branch
push plus a workflow dispatch) — the two remaining built-in execution verbs.
- A refused wake launch debounces the next event like a skip does, so the backoff it
armed is never undone on the next supervisor pass.
- The direct lane names the card's chat in the closed-bound frame; reconciling a replayed
row into an existing card projects the closed bound too (a missed timeout frame).
- Docs: PERSISTENCE drops the retired observation fold; architecture/06 names the two
round-4 gates the mode cap binds at; DESIGN describes the question pointer's states.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Conflicts: ouroboros/tools/control_routing.py (both kept: the presence note inside
its branch, the consciousness origin stamp after it), docs/architecture/03, 06, 12
(their new sentences kept, the consciousness wording re-applied), and the generated
v7next inventories (regenerated).
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
- A deep review a consciousness tree starts keeps the tree's ledger category, so the
rolling allowance discovers its root and counts its spend (its rows said
deep_self_review and the tree was invisible to the allowance).
- A late quiz answer relayed by a transport skill is forwarded with that skill as its
source (answer_decision takes an additive `source`; the web card stays "web").
- A bounded owner wait that times out stops saying "waiting": the quiz block drops
wait_for_answer (wait_ended_at for audit), the live card gets an additive
quiz_state wait_for_answer=false, and the card stays open and answerable.
- Wake message: the newest unanswered cards first, bounded to four, then settled tasks
newest first; one placeholder pass (a title containing "{daily_usd}" is a fact);
"at least" before the spend when unmetered rows sit in the window.
- Only a consciousness refusal pauses an evolution campaign; any other refusal retries
on the next pass without recording a cycle.
- A root consciousness started reads the runtime mode it actually runs in (the per-task
light cap) in its Runtime block; the wake itself keeps Main's block byte-identical.
- A wake whose thread cannot start unregisters itself; /bg start after a long off
period never announces a wake in the past; the nanny's "chose not to delegate"
nudge respects a withheld delegate_start.
- docs/architecture/06 names the gates the mode cap binds at; a stale docstring
mention of the retired observation inbox is gone.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
- A stop remembers who placed it and the key never outlives the stop: an agent's
toggle_evolution(False) on top of the owner's stop keeps the owner's; /evolve off and
panic drop any agent source; every clear drops the key. Only an agent-placed stop is
undoable by the agent (В12).
- request_deep_self_review starts a root: Observe does without it, and the request
carries the caller's consciousness origin to the ONE admission door and the ledger.
- The globalized view of a project task keeps the consciousness origin, so a campaign a
Full tree promotes stays inside the consciousness limits.
- An evolution cycle the admission door refuses pauses the campaign once with an owner
line (like the budget-reserve breaker) instead of minting a transaction and bumping
the cycle on every supervisor pass.
- The allowance is read before the queue lock (the ledger flock never stalls queue
readers); a refused wake launch keeps its event reason; a silent gate refusal inside
admission is typed as the gate; the wake message lists cards first and settled tasks
newest first.
- UI: the allowance line never wears a $0.00 receipt for an absent number and prints
"≥" for an unmetered window and "ledger integrity degraded" when flagged.
- docs/architecture/06: a wake is an ordinary direct turn (no background owner, locked
memory, pause/stop event of its own; the interactive transport class applies).
- Size-ratchet manifest regenerated.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Three conflicts resolved by keeping both sides: the chrome tests and the
refused-promote test in web/tests/chat_activity_block.test.js; the admission-notice
sentence beside the cause-table verdict sentence and the refused-addressing sentence
beside the work-keyed chrome sentence in docs/architecture/03; inventories regenerated.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
- The alarm no longer re-arms on the owner's own direct turn ending (B13) nor on a
project digest of a tree consciousness started; notify keeps the boot floor after
a restart (max(last wake, boot) + floor).
- A wake carries what is left of its allowance as the tree's GRACEFUL ceiling
(`root_cost_ceiling_usd`, now honored for the root itself by the in-task stop)
instead of narrowing the ledger fence: one Main attempt reserves ~$8 up front, and
the narrowed fence refused every wake of a nearly spent day before its first call,
posting a budget error into Main at each heartbeat (stand: 15 such wakes). A
remainder at or below the planning margin is skipped as allowance_exhausted.
- A wake the lane could not admit backs off like a failed wake; a closed budget door
is retried quietly at the interval, a transient door at the floor.
- The wake message lists unanswered cards of any task, the previous wake's own
included (an expired_terminal card still takes a late answer).
- Only an owner's stop is sticky against toggle_evolution: a stop the agent placed
remembers its source (`evolution_stop_source`) and stays undoable by the agent.
- The status snapshot carries `unknown_unmetered`/`integrity_degraded`; the allowance
line says "at least $X" and "ledger integrity degraded" when they apply.
- Docs: a Presence cycle a wake starts is outside the allowance; benchmark profiles
drop the retired OUROBOROS_BG_MAX_ROUNDS key.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
A wake-up is an ordinary direct turn with its own task id, its own durable
result and its own card, so the pseudo task id "bg-consciousness" no longer
names anything live. Every special case built around it goes:
* chat_activity.js loses isBackgroundTaskId/shouldAlwaysShowTaskCard (the
block appears under the same predicate as any other turn) and the
"Background thinking" meta hardcode; the P1 origin label is the only one
left. task_control_menu.js drops the id from REUSABLE_TASK_IDS, so a wake
offers Stop and is scanned like any other unconfirmed card.
* model_wait.js loses syncBackground, the background owner, the paused phase
and the "this wakeup cycle" wording; a wake's model wait is a direct turn's
model wait. The consciousness role label stays (the model slot is honored).
* logs.js and log_events.js file and label a wake by `initiator` only; the
end-of-cycle `consciousness_state` projector and the "thought"/"background"
vocabulary are gone, as is the dead `source === 'consciousness'` own-loop
branch.
* gateway/history.py no longer stamps the loop's last progress row "done" and
no longer appends its live model-wait row. The retired loop wrote no
task_result, so its legacy rows replay as any other row whose result is
gone: the client's durable task-detail read settles the card as "Outcome
unavailable", never as Done. The dead get_background_model_wait reader and
the `background` parameter go with it.
The remaining stale mentions (a compacted context section, a context-health
error type, docstrings and comments) are corrected in place.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
A consciousness wake-up is now an ordinary Main direct turn nobody typed
(owner decisions В13/В15): the same system prompt, memory, tools and loop as
an owner's message, started through handle_wake_direct with the wake's
origin/authority envelope and a rendered user message. ouroboros/consciousness.py
is rewritten as the alarm clock only (1533 -> 289 lines): tick(now) runs from
the supervisor loop beside publish_direct_roots — disabled -> live wake ->
live owner turn -> not due -> rolling-24h allowance (allowance_unknown is a
disclosed skip, never a silent block) -> owner chat bound -> launch; the next
wake is last finish + the model's set_next_wakeup interval (or
WAKE_DEFAULT_SEC), clamped into the owner's bounds, doubling on a runner
failure; notify(reason) pulls the next wake to max(now, last wake + min)
(arithmetic debounce) from a finished root task (any outcome, never a
consciousness-origin one), a project digest and the orphan-heal sweep; an
owner message no longer wakes it. Liveness is the DirectActivityRegistry
alone; /bg stop and toggle_consciousness(stop) arm a graceful stop of a live
wake off-thread; the legacy observation inbox is moved once to archive/
without being read. The health WARNING/CRITICAL trigger is not implemented
(no producer exists) and says so in the docstring.
prompts/CONSCIOUSNESS.md is now the wake's USER message template, rendered by
the new ouroboros/consciousness_wake.py (events since the last wake from
task_results, open owner cards and owner-message count, truncated with an
explicit "(+N more; see recent_tasks)" line; wake_task_metadata builds the
envelope). set_next_wakeup becomes an ordinary registry tool in
tools/control.py.
Gone with the loop: BACKGROUND_DELEGATION_ROLE and its consumers
(owner_delivery deferred frames, the _escalate refusal), the bg model-wait
twins and get_background_model_wait plumbing, BG_CONTEXT_*/BG_OBSERVATIONS
budget constants and the inbox hot-store tripwire, the CONSCIOUSNESS.md
whitelist drift check, the startup inbox fold, the consciousness pause/resume
around owner turns and both "Message from my human" injections. The
OUROBOROS_BG_WAKEUP_MIN/MAX keys leave the restart-required set (read at each
alarm decision); docs/architecture/07 and the Settings note say so.
server._describe_bg_consciousness_state projects honest statuses (disabled /
stopped / thinking / sleeping until HH:MM / waiting_for_first_conversation /
allowance_exhausted / allowance_unknown / wake_rejected / wake_failed);
Activity and Evolution render the new snapshot; api_types documents it.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
A question card no longer dies with its author (owner decision В17a=A, which
explicitly retired 30=A). Expiry stays structural — the task-done seam still
flips an unanswered card to expired_terminal — but the answer ingress now
accepts an answer on that expired block, records it with
answered_after_terminal for audit, and keeps first-answer-wins exactly as it
was.
Because a finished task has no mailbox anyone will drain, the recorded answer
is delivered into the card's own chat as the owner's OWN message through the
named ingress accept_local_message: its canonical chat row is the acceptance
receipt and its client_message_id (quiz_late_answer:<task>:<quiz>) is the
idempotency key, so a retry after a failed delivery re-enters the same
delivery instead of enqueueing twice. The message text is the existing full
_quiz_answer_frame, untrimmed; provenance rides its own task_metadata field
so the real transport's client_surface is never substituted; and the owner
gets the same WS echo bubble their own typing produces.
The 2xx now says answered_after_terminal and forwarded, with a reason_code
when the card's chat is synthetic A2A traffic or the hidden partition — a
destination with no owner turn to start. 409 is left for a card that was
already answered, so the ingress stops fabricating an expiry on a bare
refusal. record_asked stores the card's chat_id (and a waiting asker's
max_wait_minutes) so the delivery addresses where the card was shown, with
address_task_event as the fallback for older blocks.
On the web the card stays answerable after its task finishes: the option
buttons and the free-answer field survive expired_terminal (only answered or
superseded turn the card into a record), the status reads "Task finished — you
can still answer", the project pointer reads "Answer still possible" and keeps
refreshing, and a bodyless 409 no longer invents an expiry the card obeys.
Telegram reports the same two outcomes honestly. Without the web half the
feature would have been reachable from Telegram alone.
Disclosed properties: in a project room with exactly one live steerable root
task the ordinary routing hands the late answer to THAT task's mailbox; and a
card evicted by the 16-block cap answers 404, because it is genuinely no
longer known.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The wake head now counts the slots that answered before the release (their
verdicts live in the collected panel) so its "N of M" is on the same basis as
the quorum; the roster records answered slot IDS rather than a count, so a
re-released wave that replays a settled slot never counts it twice. The
accepted-on-earlier-revision sentence says the answer "changed" (a keep with a
new review subject moves the paid identity without rewriting the bytes). Two
record-only "valid quorum" remnants reworded; a test docstring moved above its
import; a redundant sleep dropped.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Background Consciousness redesign, P1 (label carrier). A self-initiated Main
turn — a consciousness wake-up, and later the roots it starts — is marked by
ONE fact, `metadata.initiator = "consciousness"`, and that fact now survives
every hop an owner-visible message takes, so the block's meta line and the
final bubble are labelled live and after a reload alike:
- Producers: `subagent_messages.initiator_meta()` reads the label from a task
record or its metadata; `agent._subagent_progress_meta` merges it into every
progress/heartbeat frame, the terminal `send_message` frame carries it next
to the child lineage, and `TurnEventQueue` stamps it (optional `initiator`)
on the turn's own tool/log events beside `_is_direct_chat`.
- Persistence: `log_chat` copies it onto the chat.jsonl row like
`task_terminal_status`; the authored `task_summary` row and the persisted
task result carry it too.
- Replay: `history._PROGRESS_META_FIELDS`, `_copy_task_summary_metadata`, the
terminal-truth overlay (from the stored result metadata) and the chat-row
copy return it on each row (history.py stays at the 1600-line gate:
the four additions are folded into existing lines).
- Envelope: additive `ChatOutbound.initiator` (+ the JSDoc mirror), pinned by
the contract parity test; one row in the frozen-contracts chapter.
- Web: `CARD_META_KEYS` carries it into the live projections
(`summarizeChatLiveEvent`/`taskTerminalSummary` stamp it), the block meta
line shows "Consciousness", the assistant bubble is signed
"Ouroboros · Consciousness" through the existing sender line, Logs files the
rows under Consciousness (`categorizeLogEvent`) and labels the task group;
the legacy `bg-consciousness` branches are untouched (P5 removes them).
Tests: tests/test_consciousness_initiator_label.py (frames, chat row, summary
row, result metadata, history replay), web/tests/consciousness_label.test.js
(sender line, categories, projections, live meta line/bubble, tool-only wake,
owner turn unlabelled, reload), the Logs group label in
web/tests/dashboard_read_states.test.js.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
- `route_to_project` keeps its typed abstention code (`target_unspecified` /
`invalid_project_id` / `target_not_found`) as the receipt `reason` and carries
the model's own words as `detail`; the three codes and `project_unavailable`
join the host cause table, so an empty-options abstention reads
"Not started: no destination was chosen" instead of a raw code.
- `_record_routing_receipt` (owner routing) reads the same host table: the
`project_unavailable` refusal carries `reason` and `cause` on the durable row,
the live ack and the fallback broadcast.
- A refusal receipt with neither options nor a cause sentence (rows written
before the sentence existed) reads "Not routed", never "Choose a target".
- An untitled host-issued act is named by its request's first words, cut at a
word boundary with an ellipsis.
- Docs narrowed to what the code produces: `detail` on the producers that hold a
fact beyond the code; `(reason: detail)` on the promote/route identifiers only;
`cause` on the frame and replay, `cause` beside `reason` only on the 409 body.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Owner decision D4=A (2026-09-16): reviewer verdicts are for Ouroboros, and a
review that only ever reached Main by Main not moving on was worthless to it.
Four changes, one new leaf (ouroboros/acceptance_settlement.py):
1. The released wave wakes Main at its own quorum (min_successful_slots) and
again when the last slot settles; the mailbox row carries each reviewer's
own parsed verdict and note instead of "reply keep to collect". The
settlement thread still never aggregates (review_custody roster gains
`verdicts` / `quorum_announced`; two module-private helpers).
2. A final answer delivered while the panel this turn released is still
running neither buys a second panel nor is refused one
(`_deliver_under_running_panel`, inserted after `_prior_acceptance_run`).
Waiting is the default; under advisory enforcement Main may answer
`"pending_review":"finish"` on its delivery control (classified beside
`acceptance_subject`, recorded on every control answer, offered in the
prompt only where the host can honour it — blocking always waits, Cyber Pro
keeps its own rule). A panel that settled PASS on the earlier revision
accepts the task on the reviewers' word (`previous_revision_accepted`,
owner fork 1=B) and the row says so; a FAIL or DEGRADED on the earlier
revision is not a verdict on the rewrite and hands the delivery to the
ordinary path (a new panel while the cap allows, otherwise the typed
refusal), so a rejected old draft never paints the new one red.
3. A panel that settles after its task ended is collected at $0, republished
on the task's review projection and announced once in the task's room
(`acceptance_late_settlement`, deduped by delivery id; owner fork 2=A).
4. The DEGRADED progress line no longer claims a quorum failure.
Renderers: an accepted decision whose reason has a table sentence now states
it (both twins); one phrase; the completeness gate scans the new leaf; one
parity case. Tests N1-N6 pin the incident shape (4525349b) end to end, the
quorum wake, the late supplement and its idempotence, the advisory finish,
blocking/Cyber Pro rows, and the untouched finished card. Docs: architecture
01/06, development 06, DESIGN, inventories.
Sizes: loop_acceptance_review.py 1586 -> 1572, review_custody.py 1505 -> 1553,
loop_delivery.py 1300 -> 1317 (band), log_events.js 1492 -> 1496,
outcomes.py untouched.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
A supervisor refusal of a chat-issued promote used to reach the owner as a
standalone Ouroboros bubble (WORKSPACE_UNUSABLE ...), reach the model without
its cause, and label the owner's message "Choose a target" with no options.
- Every workspace refusal returns `detail` (cause + repair) composed by
`workspace_admission.workspace_repair_hint` from the typed source of the
refused folder; `_persist_promote_rejection` is the single durable writer.
`_fail_promoted_task_loudly` and `_explicit_workspace_remedy` are gone.
- Placement follows who can narrate: a tool-issued act gets its receipt, the
failed-call error row and `detail`; a host-issued act (skill card, Swarm,
picker click, stamped `host_initiated`) gets ONE typed System row
(`task_not_started` / `task_start_unconfirmed`) in the chat the owner wrote
in, from the one publication boundary `_handle_promote_chat_to_task` wraps
around every promote outcome. The skill-repair untyped bubble is removed;
steer cancel-pending notices obey the same owner-labelled rule.
- The host owns the owner-facing sentence: `project_dialogue.routing_refusal_cause`
(action + status + reason, e.g. "Not started: the working folder can't be
used") rides the annotation, the live `message_annotation` frame, history
replay, `MessageAnnotationOutbound`/`DecisionResponse` and the picker's 409
body; the browser renders `cause` verbatim and keeps no client table.
- Admission-notice rows stay in the chat they were sent to on replay
(`room_membership` ignores the never-started task's project binding); the
"Project · Started" row is announced only after the task is really queued.
- `workspace_root` naming the Ouroboros repository itself maps to the existing
`workspace="none"` sentinel at the promote tool with a disclosure; subfolders,
the data drive and every other caller keep the typed refusal.
- Docs (DESIGN, architecture 01/03/04/05/06/12, DEVELOPMENT naming rule),
generated inventories, python and web tests.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Settings scaffolding for the Background Consciousness redesign: the three keys that
will govern a wake-up, and honest new bounds for the two that already exist. No
runtime behaviour changes yet — consciousness.py still reads the wake-up bounds at
__init__ and adopts these readers in a later phase.
- OUROBOROS_CONSCIOUSNESS_AUTONOMY (observe|act|full, default act): what a wake may do.
- OUROBOROS_CONSCIOUSNESS_DAILY_USD (20.0): rolling-24h spend ceiling for consciousness,
its wakes plus the tasks they start; 0 means it may not spend.
- OUROBOROS_CONSCIOUSNESS_MAX_TASKS (2): concurrently running consciousness-started
tasks; 0 means it never starts tasks.
- OUROBOROS_BG_WAKEUP_MIN 30 -> 900 and OUROBOROS_BG_WAKEUP_MAX 7200 -> 14400, same
semantics: the lower/upper bound of the interval the model picks via set_next_wakeup.
WAKE_DEFAULT_SEC = 3300 is the SSOT interval used when it picks none — 55 minutes,
just under the default 1h prompt-cache TTL so the shared prefix stays warm.
- OUROBOROS_BG_MAX_ROUNDS is retired: a wake is an ordinary Main turn, bounded by
OUROBOROS_MAX_ROUNDS and the per-task cost cap, so a consciousness-specific round cap
has no reader. It leaves the restart-required set with the same edit, so a stored
ghost drops at settings load and the boot notice names it.
Readers live in runtime_limits.py with the other clamped getters and are re-exported
through config.py. The three new keys are deliberately NOT restart-required: they are
read at decision time, so a save applies without a restart.
Settings UI: the Background Cognition section gains an autonomy segmented control, a
float Daily Allowance field (collected as text, like the evolution budget reserve, so
20.5 is not truncated to 20) and a Max Concurrent Tasks field; the wake-up inputs are
relabelled as the bounds of the model-chosen interval and the max-rounds input is gone.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Four review lanes (grok, astra, opus-5, astra scope) on 73ef007f2; the accepted
findings, applied together:
- The general empty-title rule lost the cascade to the collapsed-title clamp
and its reserved line (equal specificity, later position), so an attention-
only block still reserved a blank title row. A collapsed-geometry :empty
rule now wins; verified by computed style in Chromium and WebKit at 1440
and 390.
- panel_reason() restated the raw tier identifier ("tier=best_effort") right
after the capsule header had said it in words; the tier is now said in words
everywhere and the header asks for the reason alone (the test pins the whole
header identifier-free).
- The subagent detail body still prefixed the owner sentence with "Reason:".
- model_wait.js still chose a wait title from the lane fact; a block without
work carries no placeholder, the always-shown kind keeps "Background thinking".
- The five execution phrases carried from the old prefixed table rendered as
lowercase fragments; capitalized in both twins, the fixture and the pins.
- The acceptance wake's re-arm reset the candidate's one malformed-control
repair on every wake; the re-offer keeps a spent repair spent.
- Docs: the depth sentence left the terminal summary row (architecture 01/06),
the wake re-offer is stated exactly (development 06), the activity-block test
docstring no longer keys chrome on the lane fact; the recursive querySelector
patch is restored after the file's tests.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Roast round 1: four cause sentences claimed more than the record proves —
"too few reviewers could be read" is false when the reviewers were read and
the cycle cap refused the re-authored answer; "failed before any reviewer
answered", "after it was reviewed" and "the review was skipped" likewise.
They now state the cause neutrally. The X › X dedupe compares the two names
exactly (a prefix rule folded "Art" into "Arthur"). The JS test that parsed
the Python table with a regex was a second SSOT parser and is gone; the
shared parity fixture is the pin. The long-work sentence says "one message
saying what I will check and why"; the promote description names the queue
slot, admission and reviews an independent task gets; send_user_message says
"card". Outcome honesty is shorter than the original (prompts/SYSTEM.md
24391 -> 24289 bytes). The configuration chapter no longer says direct turns
are not named.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Roast round 1 (three lanes): the content predicate counted a bare non-Done
ending as content, so a greeting that failed with no tool call would have
been offered "Turn into project" — outside the owner's rule that a turn
without tools gets no card with actions. blockHasWork now names the work a
block stands on (an always-shown kind, a review group, a child, a non-receipt
row, a tool error); presence keeps the non-Done term. The data-chrome
attribute and its three CSS rules are gone: every attention-only state already
shows its own chip (Waiting…, Cancelling…, Failed), so the only thing a block
without work lacks is a title placeholder and the conversion control — one
unconditional rule hides the empty title.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Owner-facing task rows carried the machine underneath them: a
"Reason:"/"Acceptance:" label in front of a raw code, the stored reviewer
rationale copied into Main, a 240-char cut of the model's own answer, and a
"role=…; parent=…; root=…; project=…" preamble plus a tool name in the Project
thread. The row now states one sentence from a single shared cause table, and
every raw code, stored rationale, template excerpt and ledger identifier stays
where it belongs: the typed row fields, the card, task_results and Logs.
TASK_CAUSE_PHRASES is the one table, keyed on the code alone, in
ouroboros/project_dialogue.py with a byte-identical twin in
web/modules/log_events.js; outcome_phase_parity.json carries a case per
sentence and pins both languages, a second test compares the tables directly,
and the completeness gate now also scans the acceptance vocabulary. A code
with no sentence still stays raw.
Row shapes: Main is "<label> · <word>" plus the sentence and the invitation
into the Project, keeping only the labelled salvage excerpt; the Started row is
one line; the Project-thread row names the outcome and the lineage in words and
drops the id soup, the depth audit and get_task_result. A task whose name is
the project name no longer renders "Launch › Launch". SYSTEM.md's outcome
honesty, the improvement-note header and two plan-review disclosures say the
three endings in words instead of the reviewers' JSON identifiers.
prompts/SYSTEM.md: 24290 -> 24391 bytes.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
PR #942 keyed the chat block's chrome on the host's lane fact: a direct
conversation turn rendered as a compact activity block with no Done chip,
no title and no "Turn into project", even when it was an 18-tool research.
The owner withdrew that on 16.09: real work is a task card, a greeting with
no tool call is nothing.
One predicate, blockHasContent, now names what the block stands on — an
always-shown kind, a review group, a child card, a non-receipt row, a tool
error, a non-Done outcome — and selects the chrome: a content block is the
task card whatever lane produced it (title, chip, conversion in Main unless
its origin is bound); a block that exists only for open attention keeps the
compact form under data-chrome="compact". blockVisible reuses the same
predicate, so presence and chrome cannot drift. The lane fact stays on the
record for the header pill only (a direct turn keeps the census verdict
beside its block) and no longer blanks the title or gates the button.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Preserve the reviewed feature while moving its documentation into the new reference-book chapters and regenerating their inventory source hash.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Persist an optional working folder in existing profile state and expose it through the owner tool and Skills card. Carry the admitted folder through turns, promotion and follow-ups while retaining canonical shared memory and preserving legacy profiles without a folder.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Owner 5=A asked for a task-authored message to be visible where it lands:
the receiving task's timeline. The event reached events.jsonl and the Logs
projection, but the chat reducer rendered it invisible and the supervisor
never pushed it live. The row now carries a bounded preview, is pushed to
the live log stream, and renders as a visible compact row in the receiver's
block.
A direct turn that only called tools offered no Stop until the model wrote
a narration row, because the host-attested marker rode progress frames only.
The turn's own queue proxy now stamps the marker on its tool frames (the
same by-value rule that carries the lane fact), and the client grants Stop
from any stamped log frame; a turn that neither narrates nor calls tools
keeps no block to hang a Stop on. The lane key is declared in ChatOutbound.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
A live tool or progress frame from a direct conversation turn minted its
block as a managed card until the activity census listed the turn: in that
window (the first tool call landing before the 3-second census read) the
block wore a Task title and a "Turn into project" control the V1 design
removed for direct turns. The turn-scoped event queue already stamps the
turn's chat onto its own task-scoped events by value, because the turn's
registry entry dies before the supervisor drains them; the lane fact now
rides the same rule (a stamped task_done keeps its own value), the progress
delivery carries it into the live frame, and the client settles the lane
from any stamped frame instead of waiting for task_done or the census.
docs/DESIGN.md: a Presence turn is a direct turn and follows the block
rules; only Background consciousness is an always-shown kind.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
WP-G2 named tests/test_s3_task_control_browser.py::test_s3_chat_card_dropdown_hurry_and_soft_stop
(timeout on `[data-cancel-run]`) as a candidate regression. It is not: the test
passes at its last-touch commit 02e76e741 (2026-08-30) and fails identically on
the sprint base c306b40d8 and on the candidate. Its premise (a replayed
progress row with the host-attested marker offers Stop with no live source) was
retired by 54dfdc727 (2026-09-09, "Activity unconfirmed": a replayed card
offers Stop only once the census or its durable record vouches for the root),
and the fixture's server never lists `live-root`. The same replay with a census
that lists the root shows Stop on the managed card and on the direct block
alike (web/tests/chat_activity_block.test.js pins both), so V1's "Stop stays
reachable while a turn runs" holds on the candidate; the test-vs-09.09 fork is
reported to the owner, not decided here.
What the candidate did get wrong is a split derivation: blockVisible kept a
block for `cancelableTaskIds.has(id)` while the control read the marker
through cancelRunEligibility with the unconfirmed/unavailable facts, so a block
could stand on a Stop it hid. `stopEligible` is now the one reading both use.
The click path's completion-won branch revokes through the existing owner
(revokeManagedTaskCancelAuthority) instead of a second copy, and the dead
`record.cancelable` writes (never read) are gone; chat.js lands at 192,200
bytes, under WP-G's 192,234.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Owner decision 11.09 (2A): a turn that only addressed work (promote_chat_to_task,
route_to_project, steer_task) draws no block; the typed annotation on the owner's
message is its receipt. WP-G made every successful tool call a content row and
deleted the client tool-name list (correct, P5), but replaced the deleted rule
with nothing, so "turn this into a project" showed a direct-turn block with one
promote row beside the annotation and the managed root's own card: exactly the
redundancy 11.09 removed (tests/test_chat_addressing_browser.py, promote_only).
The host states the fact once. tools/control_events.py owns the routing-verb
table (ROUTING_VERBS): the typed action on task_done reads its event side, the
live tool-call frames read its tool side (`routing_action` on tool_call_started
and every tool_call_finished producer), and task_tool_metrics counts the calls
through it (`routing_tool_calls`, carried by the task_metrics event, the
authored summary row and the history replay). The client marks such a row
`receipt` (chatView, the timeline item, patched back to content by a failure
frame) and blockVisible's content term skips receipt rows; the replay summary of
a turn whose recorded calls were all addressing calls, without error, is a
receipt row too. A recorded tool error is content on its own
(`record.toolErrors`), which makes the V1 replayToolErrors term explicit. No
client list of tool names decides presence; the row still renders inside a block
that exists for other reasons. DESIGN.md, the web-UI chapter and the
anti-pattern chapter ("an open default behind a closed exception list") state
the rule in present tense and name the host stamps.
Size: loop_tool_execution.py stays at 1500 lines by reusing `_tc_args` for the
started-frame arguments instead of a second best-effort parse; chat.js
192,234 -> 192,349 bytes, paid back in the Stop commit.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>