A same-thread refresh kept the last resolved live controls until the session and history reads finished. When the fresh project lookup finds the project archived or the archive state unknown, withdraw the unguarded thread controls and remount the request panel fenced before those reads.
Jason's owner report 2026-11-06: no way to archive or delete projects.
Archive is the reversible half; delete stays out of scope because project
and run records are write-once evidence.
- project.archive / project.unarchive: human-only (project.create's gate
minus the tester carve-out; tester, user and agent refused). State is the
latest row of a new append-only project_archives journal (3s->3t, additive;
the rows are the audit trail), so projects itself never changes and nothing
is deleted. Repeats refuse project-already-archived / project-not-archived.
- project.list hides archived projects until {includeArchived:true}, which
flags each with archived:true; a user still lists granted projects only.
- While archived, thread.create, thread.send, room.open and room.send refuse
project-archived; every existing record stays readable and unarchive
restores everything.
- Console: new public/project-archive.js (+css) and project-archive-http.mjs
routes (chatHTTP's write fence); chat.js gains two one-line hooks,
index.html one script and one link tag; styling only through the shell's
.project-archive-control / .project-archived-filter hooks.
- CLI: mosaic project archive|unarchive <name-or-id> and list --archived.
- stack-release COUNTED/LISTED gain project_archives (schema order); schema
step-count pins and migration fixtures updated for 3t.
bus 1601/1601 (Node 24; Node 26 has the pre-existing project-file socket
stall), webui 582/582, plugins 132/132, cli 1191/1194 (two pre-existing
load flakes of the row253 C4 and land_work families; both pass isolated and
the base commit's full suite fails four of the same family).
The room context event read project.repo without a guard, so a room
outside the loaded project list threw and failed the room load.
Gate: full WebUI with the editor build required, 633/0 on the second
run. The first run failed I1 inbox in decide-browser.test.mjs once;
three isolated runs each on this tree and on 5a346825b passed 9/0.
Enable CLI and WebUI channels in the Case 1 starter. Explain channel
refusals, preserve in-memory drafts, and distinguish failed reads from
successful empty data. Keep room refusal state through failed refreshes
and restore session controls after catalog recovery.
Integrates reviewed source commits 5558fe020 and a1ba9cd56. Independent
review R2 approved both recovery fixes; append-only records preserve
controller and implementation evidence.
The bus refuses a stale mode revision as request-mode-revision-conflict, so the Console route maps that
code to 409 instead of request-mode-stale and request-mode-unchanged, which no bus emits. The mode copy and the
request-mode-changed wait label describe Task 4's project-level change. The draft route relays Task 4's
{ candidate, prd, requirements } reply when its candidate names the request, and maps prd-candidate-required to 409.
Review r1 of lifecycle Task 3.
C1: the four owner controls (answer, attach, mode, cancel) inserted and routed a
human-to-PM message without the messaging policy check message.send makes. The
shared writer now reads the current human-to-PM edge inside the control's
transaction, before it inserts or routes anything, and refuses with
message-sender-refused. The refusal rolls back the journal row, the human.input
event, the message and a withdrawn selection decision. The cancel withdrawal
checks the same edge. Regression tests close the edge after the question and
cover all four controls, then reopen it and prove the same call lands once.
I1: serve.mjs read sso.publicOrigin for a request URL when the Console ran
without --sso (or in preview mode), which ended the process on an unauthenticated
GET /api/requests/r1. A request route now answers through requestHTTP with no
transports and no SSO fields: unavailable on a read, read-only on a write, 405
for the wrong method. Listener tests cover GET and POST without SSO and in
preview mode, named and loopback, and the process keeps serving.
M1: the browser test waiter allowed about 3 s and reload() could run out its loop
without failing. Waits are now deadline-bound at 15 s and name the expression,
and reload() waits for the new document's own ready flag and the mounted chat,
and throws if it never gets there. The no-repost and stored-answer assertions of
the lost-answer case are unchanged.
Migration 3p-3q is unchanged.
The owner handles a new request from the Console: pick the mode (new
autonomous, new guided or an existing project), answer the project
manager's questions and confirm an attachment, with no copy-to-terminal
step.
Bus: 3p->3q append-only migration widens the request_events kinds. The PM
asks one question with request.question. The owner-only
request.answer.console, request.attach.console, request.mode.console and
request.cancel.console join the two console reads in CONSOLE_VERBS. An
answer is an attributed human.input linked to its question and request,
recorded once per op and never a PRD approval. An attach pick makes the PM
propose that project as a confirmation decision; nothing is mapped on the
pick. A mode change takes the expected revisions and leaves the bound plan
as evidence. Cancel keeps a created project.
Console: request-http.mjs serves GET /api/requests/:request, /projects and
/draft and POST /answer, /attach, /mode and /cancel with the signed-in
owner, same-origin and bounded-body checks of the decision routes.
requests.js draws the panel under the owner's chat message, answers the
mode decision through the existing confirmation endpoints, and after a lost
answer response reads the request first and resends the same op only on the
owner's click.
Chromium also targets the document when a handler-bearing element is attached to the live page and removed before the CSP event is sent, so the readHTML filter alone (directive, source file, line, target document) could not tell that from the inert parse. The page now also records, with a MutationObserver on the live document, every element that enters the page carrying an on* attribute and every on* attribute set on an element in the page, and h.quiet() asserts the record is empty. The live-handler test gains a case for a handler attached and removed in the same turn, and checks per case that the observer saw the handler enter the page (not for the element never attached). Test-only; no product change.
Chromium 156 (host upgrade 2026-11-03) checks script-src-attr when an
inline handler attribute is created, in a document with no window as well
(WPT PR 62762; the follow-up PR 64290 that skips windowless documents is
not in 156). prosemirror-view's readHTML parses pasted HTML into the body
of a new createHTMLDocument, so the paste test at :154 got one report per
pasted handler (img onerror, a onclick, a onmouseover, svg onload), each
at editor.js:8212 with the document as target. No pasted handler reaches
the live document; Chromium 153, 151 and Brave 154 report nothing there.
The page record now sets aside only a script-src-attr report whose event
names editor.js at that readHTML line and whose target is the document.
The line is found from the bundle's text, and the lines that make the
parse target a new createHTMLDocument body are pinned; the test file
refuses to load if they change. h.quiet(), inert(), injected, errors and
rejections are unchanged. No product or CSP change.
New test: after a hostile paste, three live handlers each still fail
h.quiet() with one script-src-attr report: an onclick set on an editor
paragraph and clicked, an img onerror parsed into a page-owned detached
element, and an onclick prosemirror-view sets through its attributes prop
(a report naming editor.js at another line).
The usage queue read drains the proxy. A batch that is then not written (the
client or the bus refuses its shape, an ambiguous key owner, a failed write) is
lost, and the Console showed proxy totals with no sign of it. The reader now
counts these per enabled business since the bus started, the proxy view returns
the count as droppedBatches, and the Console proxy section says so above 0.
A read that failed before the proxy answered or that it refused drained nothing
and is not counted. Telemetry only; no quota reads it. The log line stays
proxy-usage-read-failed.
leasePort({closed:true}) ignored its option: it probed an ephemeral port and
closed it, so a parallel test file could bind it and answer a request the
test expected to fail (preview.test.mjs:209 saw 200 instead of 502).
leaseClosedPort holds a bound, non-listening socket in a perl child until the
test's after hook. Connects still get ECONNREFUSED, bind() from another
process gets EADDRINUSE, and the kernel's ephemeral allocation skips the port.
Lane mode keeps its band and per-port lock, and releases the socket before
the lock. leasePort now refuses a closed option, so a stale call cannot get
the unheld port again.
Unanswerable callers (webui preview, serve, service) move to leaseClosedPort(t).
Callers that listen on the port themselves (stack-release rehearse, control-board
serve, tester-cli) keep leasePort and drop the ignored option.
Fixture helpers that commit to a repository the product later inspects now pass
maintenance.auto=false and gc.auto=0, as the product's own git calls already do,
so a detached git maintenance run no longer writes pack temp files mid-walk.
inspectGit now refuses a git metadata entry that vanishes between readdir and
lstat (or between listing a directory and reading it) with project-repository,
'git metadata changed during inspection', instead of leaking a raw ENOENT.
It still refuses rather than skipping or retrying. A missing .git keeps its
existing error.
A deterministic test removes the named entry from inside a replaced readdirSync,
for both a file and a directory.
Ruling 1: model records keep a bounded previous list (64 ids, oldest dropped)
of ids that left by rotation, grace expiry or re-issue; a fire removes the
record. A tokens.keys leaf matches and counts every id in the lineage that
holds it. Lineage ids are unique host-wide. An orphaned leaf limits nothing,
logs one value-free line when it becomes orphaned, and the Console usage
view lists it.
Ruling 2: an agent with no model key under a key quota refuses
key-quota-no-model-key, passed through the broker, launcher and stop reason.
Ruling 3: covered by the lineage; no mayLaunch change.
Ruling 4: the usage module receives a frozen { readUsageQueue } only.
Ruling 5: every telemetry fixture wait races a 5 s real-timer deadline.
Joins row 333 R2 head 0ef269da9 (base 09a847eda: R1 503b91e56, overlay
2627321f6, docs c012b2e0a and ddb3e961f, R2 fixtures 12eba4b59 and
0ef269da9) onto refactor with row 332 landed at 5df0e9d6f. Run with the
union attributes for BUILD-LOG.md and docs/SESSIONS.md. Two files
conflicted; no other path did.
packages/business/src/launch.mjs, import block: 332 changed the errors
import to `{ BusinessError, refuse }` for the proxy route refusals; 333
added `isDailyModelQuota` from plugins/src/model-quota.mjs for the daily
model leaves under limits.tokens.keys. Kept both: the model-quota import
and the `{ BusinessError, refuse }` import. The rest of launch.mjs merged
without conflict (332's checkModels route handling and checkAccount
credential "proxy"; 333's checkTokenLimits keys block).
packages/plugins/README.md, end of file: 332 appended "Per-session proxy
context (P3)" and 333 appended "Proxy usage telemetry (row333 / P5)" at
the same spot. Kept both sections, P3 first, one blank line between.
The resolved files match the earlier 332 R1 join resolution
(join-resolution.patch) line for line apart from hunk offsets.