Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
| Comment: | Questions read every post and ticket change the ocx-message tag was ever put on, whatever its value, cancelled or not, so editing a tag after a confirmation changes neither who may confirm the question nor what closed it; the docs say what the reader trusts of a run's started
Intent: the maintainer, 2026-10-21: "Whoever sent the run (you, usually) may also confirm its questions." (HISTORY.md decision 93), and the standing rule decision 94 quotes, "please don't go down any path with a possible race condition". An adversarial review of 2ea82d7e36 (scratchpad review-qnr.md) found that a confirmation changes after it was made: "any user holding `i` can reopen a question the run's sender closed", since said_tagged_asked read only artifacts whose ocx-message tag still stood with one of the kinds it wanted. It reproduced this on a copy of the server: run-zz cancelled the tag on the run R1 and pushed one artifact, and the question read open, with stone's confirmation counted as a plain response; giving R1's started the tag value "stop R1" did the same. Yet a tag "only chooses what is read" (docs/protocol-v1-fossil.md, section 2), and src/ocx/fossil/message_tags.cr says the tag "only says where to read: what an artifact holds is read from it". The same review found three more things. The docs claimed more than the reader checks: "src/ocx/commands/questions.cr:287 accepts a started from whatever login the run names as its node. A sender can name itself, so no real node has to start the run." src/ocx/fossil/forum.cr still said "a sync meanwhile sends it", though the maintainer, 2026-10-21, wrote: "agents are mistakenly trying to use `sync`, when they should just use a regular `fossil push`." (decision 92). And SCENARIOS.md, docs/parity-v1.md and USAGE.md still showed the question command without --run, which the patterns now write (decision 94). Expression: src/ocx/fossil/message_tags.cr: MESSAGE_TAGS_EVER is every row of the tag, whatever its value, a cancelled one included: Fossil's tag cancel keeps the artifact's row and sets its tagtype to 0. MESSAGE_TAGS, the live rows that the rounds and status read, is now MESSAGE_TAGS_EVER with "AND x.tagtype>0"; its SQL is unchanged. said_tagged_asked is still one query: b.rid IN (SELECT x.rid FROM MESSAGE_TAGS_EVER), with no filter on the tag's value or state. The one message each post or ticket change holds (Messages.find) says what it is, as run_of, Question.in and Response.in already read it. A stop or an ended is passed over, and a check-in or a wiki page carrying the tag holds no message, so it is not read. The comment says why no filter is kept: a filter would let anyone holding i hide a question's run, its started or its confirmation. src/ocx/commands/questions.cr: ReadAsked's description now says what it reads. RunStarted's comment says what is trusted: the started, as the word of the login the run names as its node that the run passed every rule. What the run's sender could do when they sent it, and whether that login is a node at all, are not checked. What the reader checks is unchanged. docs/protocol-v1-fossil.md section 2 says what questions reads and why an edit of a tag changes nothing there. It also says that a round and status keep the limit given there: they read by the tags as they stand, so a tag cancelled or changed leaves its message one whose tag is missing or wrong, found only as the end of that paragraph says. docs/protocol-v1-questions.md section 2 now says what the reader checks of a run and its started and what it trusts, the login the run names as its node. It says plainly that one who names their own login as a run's node and writes its started may confirm a question whose sender names that run, while the patterns' --run $$run$$ names only the run whose process asks. The table row on runs taking no password step says a node refuses that step in a run from a user holding R, which this reader does not check. The row on the started names the login the run names as its node. The paragraph on moderation says a run's sender holding neither admin nor setup may have their confirmation held for a moderator. docs/protocol-v1-tools.md (questions), docs/interfaces.md (the ocx-message tag row), docs/design/modules.json and the modules.md generated from it, and HISTORY.md decision 94 say the same. src/ocx/fossil/forum.cr: "a sync meanwhile sends it to be read where it arrives" becomes "and fossil push, or a clone's pull, meanwhile carries it". SCENARIOS.md steps 1 and 2 and docs/parity-v1.md now show the question line with --run $$run$$ and --run RUN, and USAGE.md's synopsis with [--run RUN] inside --for. Timed on copies with a binary built from this tree, median of five (scratchpad qtt/page-times.txt and questions-times.txt). Copy A is scratchpad adv10/r0.fossil: 252 questions, 532 rows of the tag. Copy B is copy A plus an ended for each of its 35 posted runs: 567 rows. On copy A, the list page took 0.687 to 0.710 s for stone, other, ec2-user and run-zz, against 0.686 to 0.710 s with 2ea82d7e36's binary. On copy B it took 0.697 to 0.721 s, against 0.687 to 0.709 s. The questions tool took 0.388 to 0.389 s on copy A and 0.393 to 0.395 s on copy B, against 0.385 to 0.388 s. On a copy of this workspace's clone of the server, which holds 8 rows of the tag, the page took 0.066 to 0.067 s and questions 0.033 to 0.034 s. On copies of the review's edited repositories, rev12/e.fossil (R1's tag cancelled) and rev12/d.fossil (S1 tagged "stop R1"), 2ea82d7e36's binary read Q1 as open with confirmers [run-zz]. This tree's binary reads it closed: post refuses a response, saying "the question is closed already: stone, who sent the run that asked it, confirmed it answered" (scratchpad qtt/repro.txt). Review: Gemma (gemma4:31b) read the change five times, the fifth after the spec below was folded into an existing example. Held: none. Misread, each a Clarity defect changed until the next reading read it right. First, that the query should keep its filter on the tag's value, and that an ended whose text held a question might be taken for one. Each artifact holds one message, the first OCX block, and Question.in and Response.in read only one of their own kind. The comment, which had listed the kinds a reader of the questions needs as if the query chose them, now says the query keeps no filter on value or state and why, and that the message, never the tag, says what each is; the second reading did not raise it. The fix the first reading proposed would also have left the started tagged "stop RUN" hidden. Second, that said_where joins MESSAGE_TAGS and so drops a cancelled row again. said_where filters only on the condition it is given, and its tagged only reports the tag. The comment now says so, and the third reading did not raise it. Third, that a cancelled confirmation "remains hidden because its tag is cancelled". MESSAGE_TAGS_EVER's comment now says Fossil's tag cancel keeps the row, setting its tagtype to 0, so the artifact is still selected, and the fourth reading did not raise it. Wrong: the third and fourth readings' finding that a check-in carrying the tag is selected by the subquery but matched by no post or ticket change, at a cost, the fourth added, as cancelled tags grow. A check-in holds no message (docs/protocol-v1-fossil.md, section 2), so not reading one is the design, which the comment says. The restriction they proposed repeats what said_where's join on forumpost and ticketchng already does, and would not parse, as MESSAGE_TAGS_EVER already ends in its WHERE. The timings above stay under 0.4 s for questions and 0.73 s for the page. Also wrong: the fifth reading's finding that said_where fails on a condition that is a subquery: the condition has the form, b.rid IN (SELECT x.rid FROM ...), it always had. And its finding that thousands of tagged artifacts that are no question would be read: a tag is anyone's to add with any value, so the old filter never bounded what is read, as anyone could tag ten thousand posts "question"; each run now adds one more row, its ended, beside its run and its started, which the old query read already, and 35 endeds cost 7 ms above. Its fix named a column, b.kind, that blob has not. It waited, set aside in a stash, for gate-headroom's check-in, 2a256e2adc, which runs the specs in three parts side by side and brought the gate from 180 to 191 s down to about 92 s, under its 180 s limit. Demonstrates: spec/ocx/questions_spec.cr, "lets whoever sent the run a question names as asking it confirm it answered, ... whatever they, or an edit of a tag, may do since; no one else". Once its questions are closed, it records every question's confirmers and what closed it. Merge was closed by the fixture user, who sent the run mine; rename and again by boss, who sent the run theirs; ship by other, its sender. Then it makes the review's two reproductions and a third: the tag on mine cancelled, the started of theirs that counts tagged "stop THEIRS", and the tag on other's confirmation of ship cancelled. Every question reads the same. Against 2ea82d7e36's query it failed at that assertion: merge, rename and again lost their run's sender and read open, and ship read open (scratchpad qtt/before-folded.txt). This was first a separate example with three fresh runs, which failed the same way (scratchpad qtt/before.txt). It was folded into this one, whose runs and confirmations it reuses, after two gates run with it failed only on the 180 s pace limit, at 188 s and 181 s (scratchpad qtt/gate-1-pace.txt and gate-2-pace.txt), its spec run taking 2:23 and 2:34. spec/ocx/message_tags_spec.cr, "is on a question and a response, ... and questions reads every post it was ever put on, whatever its value, cancelled or not": a question whose tag was cancelled and a started tagged as a stop are read, with the run and the response, each with its tag as it stands. It failed against the old query too (scratchpad qtt/before-both.txt). spec/scenarios/question_spec.cr, "runs the steps, a wiki page's among them": the scenario's question is asked with run, as the patterns' --run $$run$$ asks it, and its block names the run. spec/ocx/create_spec.cr, "names a run stored but not indexed, ...": the error ends "and fossil push, or a clone's pull, meanwhile carries it". crystal run ci.cr green: 448 examples, 0 failures. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVpmCsBwVF3d4zhMHHDAxs |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | agent-tools |
| Files: | files | file ages | folders |
| SHA3-256: |
9118789d1ad8392ac6ffc3871188efdb |
| User & Date: | opus-ocx2 2026-10-21 10:55:32.661 |
|
2026-10-21
| ||
| 12:07 |
A run whose program exits without reading its run gets its ended, and the watcher answers nil only for a lock another holds
Intent: protocol v1's promise that a run that started gets an ended, exited with its code, budget, stopped or lost (docs/protocol-v1.md, sections 4 to 7 and 10), never silence. An adversarial review of 9118789d1a (scratchpad review-headroom.md, finding 1) saw the gate fail once on a clean tree: spec/ocx/program_spec.cr:341, an unconfined echo run, hit watch(...).not_nil! "Nil assertion failed", and an empty /tmp/*ocx-run directory was left, which only Watcher.prepared makes. Watcher.watch ended in a rescue of IO::Error around everything it did, meant for the two lock takings, so an IO::Error after the run started was answered nil: no ended after the started, and the scratch directory left. The review forced the same path: an unconfined run given a 200 KB objective, whose program exits without reading it. watch returned nil, the directory was left, and no ended followed the started; with a 10-byte objective it returned 3 reports. The cause: the run's first line went to Process.new as an IO::Memory, which Crystal copies into a pipe in a fiber of its own, and Process#wait raises what that copy met, here EPIPE, before it gives the status. A session each of whose deliveries starts a process was given its lines the same way and was lost the same way. A session's relay and a terminal write their own pipe or pseudo-terminal in a fiber that rescues IO::Error: the same program and objective, as a session and at a terminal, ended exited 3 before this change. Which IO::Error failed the gate that once is not known: a short line given to a program that exits at once did not raise in 500 tries on a quiet host, with Process.new or with this change (scratchpad wl/race-probe-short.txt). With this change, whether the program read its line before it ended changes nothing. Expression: src/ocx/sandbox.cr: Sandbox.start, the one call that starts a run's process under precept I7 (Single Process Site), now takes its input by type. An IO::FileDescriptor the caller holds, a terminal's or a relay's, is passed on as before. A String, the text given, is written through a pipe by a fiber of its own, which then closes it and rescues IO::Error: a process that ends without reading all it was given is no error, and its status says how it ended. This process's copy of the read end is closed once the process has started, as Process.new closes its own pipes' ends, so the fiber's write fails at once when the process ends. Nothing gives Process.new an IO::Memory as input any more, so Process#wait never raises a copy's error. src/ocx/launch.cr: a task's empty input is "". src/ocx/watcher.cr: launched passes relay.try(&.input) || input.to_s, a descriptor or a String, which the compiler sends to the overload for each; its input stays an IO::Memory, whose lines a terminal types. watch no longer rescues IO::Error: it returns nil only when Node.take_run or Node.take answers that another holds the lock, as before. Any other failure raises, and the node's check answers lost a started run left with no ended (docs/protocol-v1-fossil.md, section 5; failure case 2). Before, such an error was answered nil, which wrote no ended either. prepared removes its scratch directory, the files made for the run's sandbox, in an ensure, whatever ends it: a refusal, a decision not to start, the run's end, or an error such as a write that fails. That replaces its three removals, and it now runs after the ended is written. The comments on watch and watched say that each lock is let go as its File.open block closes its file, an error too. src/ocx/care.cr: Node.take_run and Node.take answer whether they took the lock, false while another holds it, and took? rescues only the non-blocking try of flock, so no other error is read as a lock another holds. docs/protocol-v1-failures.md gains case 30, the program never reads its run. docs/crystal.md says that Process#wait raises what the copy of an input met. src/ocx/watcher.cr is 400 lines, within precept C1 (File Boundedness). Review: Gemma (gemma4:31b) read the change seven times. Held: none. Misread, each a Clarity defect, changed until a later reading read it right: first, that closing the read end right after the start lets the child see an end or an error before it reads. The child holds its own copy from its fork on; the comment now says so, and that Process.new closes its own ends the same way, and the second reading did not raise it. Second, that an error escaping watch leaves the run with no ended for ever. watch's comment said nothing of other failures. It now says the node's check answers lost a started run left with no ended, and the third to fifth readings did not raise it. Reworded as "left to the node's check", the sixth raised it again; reworded to say what the check does, the seventh did not. Third and fourth, that the run's lock, then the user's, stays held when decided raises. Each is opened by File.open with a block, which closes the file and so lets the lock go. The comments on watch and watched now say so, and the next reading read each right. Sixth, that the scratch directory leaks when watched fails before prepared. Only prepared makes it, and its ensure covers it from there; the ensure's comment now names the scratch made above, and the seventh reading did not raise it. Wrong: the first reading's finding that the pipe's writer leaks if the fiber never reaches its ensure. The fiber's ensure closes it whenever the fiber runs. The fifth reading's runtime TypeError for a relay whose input is no descriptor: Relay#input is declared IO::FileDescriptor, and Crystal picks an overload when it compiles. An input of any other type does not compile, as this change's first build showed: "expected argument #2 to 'OCX::Sandbox.start' to be IO::FileDescriptor or IO::Memory, not IO". The sixth reading's claim that watch runs inside Node.locked in the node's check loop: the hook's watch mode calls it in a process of its own, holding no lock of the node's. The seventh reading's NameError when the scratch is not yet assigned: scratch is assigned as prepared starts, before any begin. Crystal types a body's local as nilable in its ensure, which `if scratch` handles, and an unknown name does not compile. Demonstrates: spec/ocx/watcher_spec.cr, "ends a run whose program exits without reading its run, alone, as a session, at a terminal, or each of whose deliveries starts a process, as it ended (case 30), leaving no scratch directory, whatever ends the run". The run is unconfined, with a 200 000-byte objective, sh -c 'exit 3' and a wall of 1s. On a node that runs only sandboxed processes it is refused. On one that allows unconfined runs it ends exited 3 alone, as a session and at a terminal. As a session each of whose deliveries starts a process, given no delivery, it ends budget, past its wall. A run whose started's index fails (StandInFossil failing test-crosslink) raises Fossil::Failed from watch. After each, no scratch directory of this process is left. Against the old code it failed at the first exited run, watch answering nil (scratchpad wl/before-1.txt). Run first, the each-delivery session failed the same way (wl/before-each.txt). Run first, the failed write left its scratch directory (wl/before-failed-write.txt). spec/ocx/watcher_spec.cr, "ends a running session's program on TERM: stopped for a stop that counts, lost with none; a second watcher leaves the first's lock as it is", and "keeps a second run of the same user waiting while the first is in care, and hands it on after (case 26)": with the run's lock held by another watcher, and the user's by another holder, watch still answers nil. spec/ocx/care_spec.cr, "takes a run's lock only once a try of it under way is over, so the try never makes the taking fail", now reads take's answer rather than rescuing its error. spec/ocx/sandbox_spec.cr, "runs no task unconfined": a program not installed is refused through the String overload too. spec/ocx/program_spec.cr:341 is unchanged: the echo run's ended no longer depends on whether echo exits before its line is written. crystal run ci.cr green: 449 examples, 0 failures. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVpmCsBwVF3d4zhMHHDAxs check-in: 35b2b0febb user: opus-ocx2 tags: agent-tools | |
| 10:55 |
Questions read every post and ticket change the ocx-message tag was ever put on, whatever its value, cancelled or not, so editing a tag after a confirmation changes neither who may confirm the question nor what closed it; the docs say what the reader trusts of a run's started
Intent: the maintainer, 2026-10-21: "Whoever sent the run (you, usually) may also confirm its questions." (HISTORY.md decision 93), and the standing rule decision 94 quotes, "please don't go down any path with a possible race condition". An adversarial review of 2ea82d7e36 (scratchpad review-qnr.md) found that a confirmation changes after it was made: "any user holding `i` can reopen a question the run's sender closed", since said_tagged_asked read only artifacts whose ocx-message tag still stood with one of the kinds it wanted. It reproduced this on a copy of the server: run-zz cancelled the tag on the run R1 and pushed one artifact, and the question read open, with stone's confirmation counted as a plain response; giving R1's started the tag value "stop R1" did the same. Yet a tag "only chooses what is read" (docs/protocol-v1-fossil.md, section 2), and src/ocx/fossil/message_tags.cr says the tag "only says where to read: what an artifact holds is read from it". The same review found three more things. The docs claimed more than the reader checks: "src/ocx/commands/questions.cr:287 accepts a started from whatever login the run names as its node. A sender can name itself, so no real node has to start the run." src/ocx/fossil/forum.cr still said "a sync meanwhile sends it", though the maintainer, 2026-10-21, wrote: "agents are mistakenly trying to use `sync`, when they should just use a regular `fossil push`." (decision 92). And SCENARIOS.md, docs/parity-v1.md and USAGE.md still showed the question command without --run, which the patterns now write (decision 94). Expression: src/ocx/fossil/message_tags.cr: MESSAGE_TAGS_EVER is every row of the tag, whatever its value, a cancelled one included: Fossil's tag cancel keeps the artifact's row and sets its tagtype to 0. MESSAGE_TAGS, the live rows that the rounds and status read, is now MESSAGE_TAGS_EVER with "AND x.tagtype>0"; its SQL is unchanged. said_tagged_asked is still one query: b.rid IN (SELECT x.rid FROM MESSAGE_TAGS_EVER), with no filter on the tag's value or state. The one message each post or ticket change holds (Messages.find) says what it is, as run_of, Question.in and Response.in already read it. A stop or an ended is passed over, and a check-in or a wiki page carrying the tag holds no message, so it is not read. The comment says why no filter is kept: a filter would let anyone holding i hide a question's run, its started or its confirmation. src/ocx/commands/questions.cr: ReadAsked's description now says what it reads. RunStarted's comment says what is trusted: the started, as the word of the login the run names as its node that the run passed every rule. What the run's sender could do when they sent it, and whether that login is a node at all, are not checked. What the reader checks is unchanged. docs/protocol-v1-fossil.md section 2 says what questions reads and why an edit of a tag changes nothing there. It also says that a round and status keep the limit given there: they read by the tags as they stand, so a tag cancelled or changed leaves its message one whose tag is missing or wrong, found only as the end of that paragraph says. docs/protocol-v1-questions.md section 2 now says what the reader checks of a run and its started and what it trusts, the login the run names as its node. It says plainly that one who names their own login as a run's node and writes its started may confirm a question whose sender names that run, while the patterns' --run $$run$$ names only the run whose process asks. The table row on runs taking no password step says a node refuses that step in a run from a user holding R, which this reader does not check. The row on the started names the login the run names as its node. The paragraph on moderation says a run's sender holding neither admin nor setup may have their confirmation held for a moderator. docs/protocol-v1-tools.md (questions), docs/interfaces.md (the ocx-message tag row), docs/design/modules.json and the modules.md generated from it, and HISTORY.md decision 94 say the same. src/ocx/fossil/forum.cr: "a sync meanwhile sends it to be read where it arrives" becomes "and fossil push, or a clone's pull, meanwhile carries it". SCENARIOS.md steps 1 and 2 and docs/parity-v1.md now show the question line with --run $$run$$ and --run RUN, and USAGE.md's synopsis with [--run RUN] inside --for. Timed on copies with a binary built from this tree, median of five (scratchpad qtt/page-times.txt and questions-times.txt). Copy A is scratchpad adv10/r0.fossil: 252 questions, 532 rows of the tag. Copy B is copy A plus an ended for each of its 35 posted runs: 567 rows. On copy A, the list page took 0.687 to 0.710 s for stone, other, ec2-user and run-zz, against 0.686 to 0.710 s with 2ea82d7e36's binary. On copy B it took 0.697 to 0.721 s, against 0.687 to 0.709 s. The questions tool took 0.388 to 0.389 s on copy A and 0.393 to 0.395 s on copy B, against 0.385 to 0.388 s. On a copy of this workspace's clone of the server, which holds 8 rows of the tag, the page took 0.066 to 0.067 s and questions 0.033 to 0.034 s. On copies of the review's edited repositories, rev12/e.fossil (R1's tag cancelled) and rev12/d.fossil (S1 tagged "stop R1"), 2ea82d7e36's binary read Q1 as open with confirmers [run-zz]. This tree's binary reads it closed: post refuses a response, saying "the question is closed already: stone, who sent the run that asked it, confirmed it answered" (scratchpad qtt/repro.txt). Review: Gemma (gemma4:31b) read the change five times, the fifth after the spec below was folded into an existing example. Held: none. Misread, each a Clarity defect changed until the next reading read it right. First, that the query should keep its filter on the tag's value, and that an ended whose text held a question might be taken for one. Each artifact holds one message, the first OCX block, and Question.in and Response.in read only one of their own kind. The comment, which had listed the kinds a reader of the questions needs as if the query chose them, now says the query keeps no filter on value or state and why, and that the message, never the tag, says what each is; the second reading did not raise it. The fix the first reading proposed would also have left the started tagged "stop RUN" hidden. Second, that said_where joins MESSAGE_TAGS and so drops a cancelled row again. said_where filters only on the condition it is given, and its tagged only reports the tag. The comment now says so, and the third reading did not raise it. Third, that a cancelled confirmation "remains hidden because its tag is cancelled". MESSAGE_TAGS_EVER's comment now says Fossil's tag cancel keeps the row, setting its tagtype to 0, so the artifact is still selected, and the fourth reading did not raise it. Wrong: the third and fourth readings' finding that a check-in carrying the tag is selected by the subquery but matched by no post or ticket change, at a cost, the fourth added, as cancelled tags grow. A check-in holds no message (docs/protocol-v1-fossil.md, section 2), so not reading one is the design, which the comment says. The restriction they proposed repeats what said_where's join on forumpost and ticketchng already does, and would not parse, as MESSAGE_TAGS_EVER already ends in its WHERE. The timings above stay under 0.4 s for questions and 0.73 s for the page. Also wrong: the fifth reading's finding that said_where fails on a condition that is a subquery: the condition has the form, b.rid IN (SELECT x.rid FROM ...), it always had. And its finding that thousands of tagged artifacts that are no question would be read: a tag is anyone's to add with any value, so the old filter never bounded what is read, as anyone could tag ten thousand posts "question"; each run now adds one more row, its ended, beside its run and its started, which the old query read already, and 35 endeds cost 7 ms above. Its fix named a column, b.kind, that blob has not. It waited, set aside in a stash, for gate-headroom's check-in, 2a256e2adc, which runs the specs in three parts side by side and brought the gate from 180 to 191 s down to about 92 s, under its 180 s limit. Demonstrates: spec/ocx/questions_spec.cr, "lets whoever sent the run a question names as asking it confirm it answered, ... whatever they, or an edit of a tag, may do since; no one else". Once its questions are closed, it records every question's confirmers and what closed it. Merge was closed by the fixture user, who sent the run mine; rename and again by boss, who sent the run theirs; ship by other, its sender. Then it makes the review's two reproductions and a third: the tag on mine cancelled, the started of theirs that counts tagged "stop THEIRS", and the tag on other's confirmation of ship cancelled. Every question reads the same. Against 2ea82d7e36's query it failed at that assertion: merge, rename and again lost their run's sender and read open, and ship read open (scratchpad qtt/before-folded.txt). This was first a separate example with three fresh runs, which failed the same way (scratchpad qtt/before.txt). It was folded into this one, whose runs and confirmations it reuses, after two gates run with it failed only on the 180 s pace limit, at 188 s and 181 s (scratchpad qtt/gate-1-pace.txt and gate-2-pace.txt), its spec run taking 2:23 and 2:34. spec/ocx/message_tags_spec.cr, "is on a question and a response, ... and questions reads every post it was ever put on, whatever its value, cancelled or not": a question whose tag was cancelled and a started tagged as a stop are read, with the run and the response, each with its tag as it stands. It failed against the old query too (scratchpad qtt/before-both.txt). spec/scenarios/question_spec.cr, "runs the steps, a wiki page's among them": the scenario's question is asked with run, as the patterns' --run $$run$$ asks it, and its block names the run. spec/ocx/create_spec.cr, "names a run stored but not indexed, ...": the error ends "and fossil push, or a clone's pull, meanwhile carries it". crystal run ci.cr green: 448 examples, 0 failures. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVpmCsBwVF3d4zhMHHDAxs check-in: 9118789d1a user: opus-ocx2 tags: agent-tools | |
| 10:51 |
The plain specs and the coverage run each build their spec program once and run it as three parts side by side, every example in one part and a failure in any failing the gate, so the gate takes about 90 s of its 180 s
Intent: the pace precept (docs/precepts/precepts.json; neither PLAN.md nor HISTORY.md states it): "the whole CI run finishes under its stated time", since "a suite too slow to run is not run, and its feedback arrives after the next change"; its stated time is docs/precepts/pace.json's 180 s, which stays. The maintainer chose the gate's shape (479d0c60f9): "Run the plain specs and the coverage run side by side (recommended): the same checks, and the gate takes about 100 s. It means two niced compiles at once on this 8-CPU host." The gate had come to take 175 to 191 s, so a check-in failed on pace about half the time with no slowdown of its own: at 2ea82d7e36, on a quiet host, it failed at 191 s ("FAIL: CI took 191 s, over its limit of 180 s") and passed at 180 s, wall clock. Both of its longest steps ran their examples one after another: the plain specs took 2:20 to 2:24 with about one of the host's eight CPUs busy (one run's examples took 144 s by its JUnit output, and 138 s of CPU), and the coverage run, spec/ocx's 329 examples in the instrumented copy, took 149 s alone. Measured before choosing: the plain specs' program, built once and run as three parts side by side, took 47 s where it took 2:24 whole (two of its examples failed there, the two stages sharing one compiler cache, below), so the specs are split and src is unchanged. Expression: spec/support/parts.cr (new): Parts.files lists the spec files as `crystal spec` does (a file, or each *_spec.cr under a directory, in the order the file system lists them); Parts.run runs a spec program as COUNT = 3 processes at once, each given its part by the spec runner's own SPEC_SPLIT, "PART%3": the examples whose place, counted in the order the files define them before any tag filters them, leaves PART over when divided by 3, so each example is in one part alone; each part has a compiler cache of its own (CRYSTAL_CACHE_DIR), a directory removed once all parts have ended, since `crystal run ci.cr` builds to one path in its cache and deletes it after, and two stages of spec/ocx/release_commands_spec.cr at once in one cache failed with "you've found a bug in the Crystal compiler"; a part that cannot start fails, saying why. Parts.total adds up the parts' summaries; Parts.wrong fails the gate with "specs failed" when any part failed, and otherwise unless the parts ran as many examples as a dry run (--dry-run, 52 ms) of the same program and options lists. ci.cr: the plain specs are built once with `crystal build` from every spec file, as `crystal spec` builds them, then listed by a dry run and run as the parts; it prints each part's output under "-- part N of 3" and then "-- the 3 parts" with the total, such as "448 examples, 0 failures, 0 errors, 0 pending, of 448 listed"; its failures join the gate's, and the result line reads as before. spec/coverage/run.cr: the instrumented copy's specs are built once in the copy's kept cache and run as the parts; each part writes the statements it ran to a file named by its process number (spec/coverage/instrument.cr's writer now takes a directory), the run takes their union, aborts unless every part wrote its file, and exits failed when any part failed, as it did when its one run failed; the ratchet check is unchanged. docs/crystal.md: `crystal run NAME.cr` builds to crystal-run-NAME.tmp in its cache, and the spec runner's SPEC_SPLIT. Nothing reads an environment variable, as precept C6 (Explicit Arguments) requires: SPEC_SPLIT and CRYSTAL_CACHE_DIR are given to the child, as the coverage run already gave its compiler CRYSTAL_CACHE_DIR. What each saved, on this host, wall clock: the coverage run alone 149 s to 67 s (parts of 38, 40 and 43 s); the plain specs' examples 2:20 to 51 to 56 s for each part, side by side with the coverage run's; their build, 24 s with its cache warm, now comes before the run. The whole gate: 193 s and 180 s before (2ea82d7e36, the second green), 109 s (the new build's cache cold) and 92 s after, measured twice on a quiet host (1-minute load under 1 at each start). Review: Gemma (gemma4:31b) read the change three times. Held: none. Misread, each a Clarity defect changed until the next reading read it right: the first and second readings, that no part's cache directory is made, so the compiler could not use it; the compiler makes its own (Dir.mkdir_p in its CacheDir), and every release stage compiled in its part's cache in both gates, but Parts.run now makes the directories before the parts start, and the third reading did not raise it. The first reading, that a coverage part whose examples failed, having written its file, could leave the run passing: the run exits failed when any part failed, which comments at the check and at the exit now say; the second reading read that exit, and the third did not raise it. Wrong: the first reading's finding that the result line's "448 examples, 0 failures" is cut short: it is the form that line had, which staging and the check-in comments read. The second reading's finding that the exit comes after the ratchet is read and leaves a failure to be scrolled for: it comes before, and ci.cr lists the coverage run's failed examples under "specs failed in the coverage run"; and that two parts' process numbers, which name their files, can be the same: the three run at once, each alive until it has written its file, so no two share a number, and its fix, the part's number in an environment variable the spec program reads, would break precept C6 (Explicit Arguments). The third reading's finding that a part's file may not be written yet when the run reads the directory: the part writes and closes it before it exits, and the run reads it after every part has exited. Its finding that a part with no summary makes the total undercount: counting it as zeros, its fix, adds the same, the total is printed beside the count listed, and the gate fails. And its finding that a dry run with every example filtered out lists none and fails the gate: such a run prints "0 examples, 0 failures, 0 errors, 0 pending" (tried), which counts 0. Demonstrates: spec/guards/pace_spec.cr ("has ci.cr and the coverage run run the specs as parts"; "gives each part its own split of the examples and its own compiler cache, removed once all have ended, and adds up what they ran", with a stand-in program writing in its cache and saying what it was given; "refutes a part that failed, one that ended with no summary or did not start, and examples run other than once each"), spec/ocx/release_commands_spec.cr (two of its stages, which failed in two parts sharing one cache, pass in parts with caches of their own), and the gate's own output: each part's examples and "448 examples, 0 failures, 0 errors, 0 pending, of 448 listed", 445 before plus the three above; the coverage run's 110, 110 and 109 examples, 329 as before, and "coverage ratchet holds: 0 statements missed in 0 files". crystal run ci.cr green: 448 examples, 0 failures. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVpmCsBwVF3d4zhMHHDAxs check-in: 2a256e2adc user: opus-ocx2 tags: agent-tools | |