Skip to content

Let trusted hosts stop executions and join owned cleanup - #1008

Merged
taras merged 9 commits into
mainfrom
capability/execution-stop-971
Oct 28, 2026
Merged

taras merged 9 commits into
mainfrom
capability/execution-stop-971

Conversation

@taras

@taras taras commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Why

A trusted host needs to stop one running document, retain its partial output and wait until its resources are released. This supplies the execution capability consumed by the separate REPL layout/Stops PR #1004.

What changes

Before: ending the host scope leaves the document unfinished; the host has no explicit operation that retains a cancelled root ending and joins that invocation.

After: executeInstalled() returns a bound HostExecution.stop(). Stop joins owned cleanup and returns an Effection Result. Emitted text remains available, a normal terminal already being written keeps its outcome, and persistence or cleanup failures retain their established identity and precedence. Repeated Stop adds no output or second ending. Ordinary scope halt keeps its unfinished-history behavior.

The prerequisite also includes a bounded generic ACP cleanup repair: published attachment identity checks creation time alongside device and inode, preserving replacements with a reused inode and a different creation time. The session-ownership test compares exact directory membership independently of enumeration order. These corrections support the existing CLI attachment lifecycle; they add no layout behavior.

How it works

owned HostExecution.stop()
  -> canonical runner/root control
  -> retained terminal and invocation cleanup
  -> joined Result

The durable root uses the existing status-only cancelled Close and writer. Host authority stays bound to the owned execution. The ACP repair strengthens the existing identity predicate without changing acquisition or joined teardown ordering.

Review guide

Review HostExecution.stop in packages/core/src/execute.ts, the root control in packages/durable-streams/run.ts, and EP29–EP31 alongside the durable-root regressions. The final CI correction is confined to sidekick-acp/posix.ts and session-ownership-cli.test.ts.

What must stay true

  • Stop waits for held cleanup; partial output survives closure and repeated Stop.
  • Failed persistence retains its exact fatal identity without a compensating terminal.
  • Ordinary halt, replay and subtree alignment retain their behavior.
  • ACP exit joins owned resources and preserves replacement files; the ownership command creates no provider or document effects.

How to verify it

The unchanged host-Stop/durable tests passed at b60e58d: 5 passed, 89 steps, 0 failed. EP30 observes nonempty output before Stop and compares the channel's final value; EP31 checks refused-append identity and joined cleanup. Their blobs remain unchanged.

At b639fab:

  • deno task test packages/cli/tests/repl-sidekick-acp.test.ts packages/cli/tests/session-ownership-cli.test.ts: 5 passed, 26 steps, 0 failed.
  • bun test packages/cli/tests/repl-sidekick-acp.test.ts packages/cli/tests/session-ownership-cli.test.ts: 26 pass, 0 fail.
  • deno task test packages/cli/tests/repl-sidekick-lifecycle.test.ts packages/cli/tests/repl-sidekick-recovery.test.ts: 19 passed, 68 steps, 0 failed.

Changed-path typecheck, lint and formatting receipts are clean. The standalone build passed at 0ab6863; that historical build does not prove this head's changed CLI build input. Current-head remote build/required CI and the first-green Fable delivery review remain pending.

Scope

Included: trusted-host Stop, canonical durable-root control, focused tests and generic documentation, plus the bounded generic ACP cleanup/test prerequisite.

The dependent #1004 retains REPL entry admission/recovery policy, session Stop dispatch, layout and UI test reorganization. Its unfinished work is not claimed complete here. Follow/shared queue presentation remains deferred. No dependency or journal schema is added.

Risks and limitations

Creation-time comparison strengthens the prior device/inode check; it is not a universal uniqueness guarantee on filesystems lacking precise creation timestamps. Local passing receipts do not claim to reproduce Linux inode reuse or establish remote success.

Scope confirmation

  • Every changed file supports the capability or its bounded generic ACP prerequisite.
  • Layout and UI reorganization remain in the dependent PR.
  • Test and build claims name their actual tested revisions.
  • No journal migration or unrelated generated changes are included.

Current-head standalone build: deno task build passed at b639fab612b69a608603d777b28621266d2b4eed before this normal push. Required remote checks and the independent first-green review remain delivery gates.

taras added 2 commits October 28, 2026 10:28
The generic half, on its own: an execution a host can stop, the durable root
control it is written through, and the documentation for both.

`HostExecution.stop()` answers for the stop and the join, never for the
document. It reaches a document still running, unwinds it, and joins every
teardown the execution owns before it answers — a stop that returned while an
owned cleanup was still running would be reporting an ending that had not
happened. What the journal keeps is the terminal representation the protocol
already has: one cancelled `Close` for the root, a status and nothing else. No
answer, outcome or successful payload is invented for work the document never
finished, and asking again writes no second ending.

A persistence failure raised while that ending is being written stays the run's
own failure by identity: the stop answers with it, the owned teardown is still
attempted and joined, and nothing compensating is appended in its place. A
backing failure is still never a workflow terminal.

`EP30` and `EP31` prove both at the seam itself — `executeInstalled` and
nothing else, with no session, entry or command in them. The existing EP29
terminal-winner, fatal-versus-ordinary and durable root cases are unchanged
beside them.

This carries no command, session or presentation code. What consumes it is a
separate delivery with this one as its ancestor.
The witness claimed partial output and had none. Its document was a single
blocking component, and `execution.output` was never read — so everything about
the stop was proved except the thing a reader of the comment would have taken
for granted.

The document prints a prefix now, before it reaches anything that blocks, and
the row drains that channel from the moment the execution exists. It waits for
both facts — the prefix seen and the component blocked — so the stop happens at
a known point rather than a timed one. Afterwards the channel's close value is
compared to exactly what was read: a stop that threw the partial text away, or
closed the channel empty, passes every other assertion here and fails that one.
Asking again adds neither a byte of output nor a second ending.

The held cleanup gate, the non-success completion, the single status-only
cancelled root Close and EP31's refused-append identity, cause, joined cleanup
and absent compensating terminal are all unchanged. EP31 reads its error
through the Result's own inferred type instead of asserting one.

No production code is touched by this.
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

PR #1008: Let trusted hosts stop executions and join owned cleanup

10 files, +2718 / -69

Scope

🔴 PR has 2787 lines changed. Split into focused PRs.

🟡 2787 lines changed. PRs under 400 receive more thorough review.

Structural

Oxlint structural signals:

  • no-unused-vars ×9: packages/durable-streams/run.ts, packages/core/src/execute.ts
  • no-redundant-type-constituents ×9: packages/cli/src/sidekick-acp/posix.ts
  • no-empty-function ×6: packages/cli/src/sidekick-acp/posix.ts, packages/core/src/execute.ts
  • no-unnecessary-type-assertion ×5: packages/core/src/execute.ts
  • no-unnecessary-type-arguments ×2: packages/cli/src/sidekick-acp/posix.ts

Slop

  • packages/cli/src/sidekick-acp/posix.ts:343 (removed)
  • packages/cli/src/sidekick-acp/posix.ts:437 (removed)
  • packages/cli/src/sidekick-acp/posix.ts:441 — // refresh: it is what a refreshed reading has to still be.
  • packages/cli/src/sidekick-acp/posix.ts:504 — // below still happens before the directory is removed.
  • packages/cli/src/sidekick-acp/posix.ts:511 — // could not identify is not one it may remove.
  • packages/cli/src/sidekick-acp/posix.ts:606 — // afterwards.
  • packages/core/src/execute.ts:2962 — // its consumers ever see.
  • packages/durable-streams/run.ts:243 — // longer do is accept a new claim on an owner that has gone.
  • packages/durable-streams/run.ts:270 — // which failure ended it.
  • packages/durable-streams/run.ts:317 — // The same ask, answered the same way.
  • packages/durable-streams/run.ts:374 — // backend that may already hold it.
  • packages/durable-streams/run.ts:386 — // gets the ordinary error close every other failure gets.
  • packages/durable-streams/run.ts:415 — // root is its ending, and this failure does not acknowledge one around it.

Static Analysis

Oxlint: 64 diagnostics across 3 files (13 rules)
Density: 0.024 violations/added-line

no-shadow (13): packages/cli/src/sidekick-acp/posix.ts, packages/core/src/execute.ts
no-unused-vars (9): packages/durable-streams/run.ts, packages/core/src/execute.ts
no-redundant-type-constituents (9): packages/cli/src/sidekick-acp/posix.ts
no-unsafe-type-assertion (7): packages/durable-streams/run.ts, packages/core/src/execute.ts
no-empty-function (6): packages/cli/src/sidekick-acp/posix.ts, packages/core/src/execute.ts
unbound-method (6): packages/core/src/execute.ts
no-unnecessary-type-assertion (5): packages/core/src/execute.ts
consistent-return (2): packages/core/src/execute.ts
no-unnecessary-type-arguments (2): packages/cli/src/sidekick-acp/posix.ts
no-duplicate-type-constituents (2): packages/core/src/execute.ts
no-array-sort (1): packages/core/src/execute.ts
consistent-function-scoping (1): packages/core/src/execute.ts
no-floating-promises (1): packages/cli/src/sidekick-acp/posix.ts

Correctness

FILE: packages/durable-streams/run.ts, PATTERN: dead-construct, CONCERN: Unused variable directoryOwned declared but never referenced, QUESTION: Is directoryOwned intended to be used in cleanup logic or a leftover from prior implementation?

FILE: packages/core/src/execute.ts, PATTERN: dead-construct, CONCERN: Unused variable directoryOwned declared but never referenced, QUESTION: Is directoryOwned intended to be used in cleanup logic or a leftover from prior implementation?

FILE: packages/cli/src/sidekick-acp/posix.ts, PATTERN: signal-cluster, CONCERN: Multiple Oxlint violations (no-shadow, no-unused-vars, no-redundant-type-constituents) in same file, QUESTION: Are these violations due to unreviewed generated code or incomplete refactoring?

FILE: packages/core/src/execute.ts, PATTERN: signal-cluster, CONCERN: Multiple Oxlint violations (no-unused-vars, no-empty-function, no-unsafe-type-assertion) in same file, QUESTION: Are these violations due to unreviewed generated code or incomplete refactoring?

@taras
taras marked this pull request as ready for review October 28, 2026 15:26
The two failures CI found on this head, and nothing else.

**An owner's exit removed a replacement it had never published.** `same()`
identified a published attachment file by device and inode. An inode number is
reused, and ext4 on the runner did reuse one: the file this owner wrote was
unlinked, a replacement was created in its place and was handed the same
number, and the exit took it away. `ENOENT` on the read that followed, in both
the Deno and the Bun job.

The correction is the one the dependent layout branch already carries, taken
whole and unchanged: the moment a file came into existence is part of its
identity. Measured here — a creation time is reported, an untouched file keeps
it, and a file created after another was unlinked does not share it. Strictly
stricter, so it can only ever leave a name alone and never remove one this
owner did not publish. This host does not reuse inodes eagerly and so cannot
reproduce the failure itself; the preserved run logs are that evidence.

**A directory was read as a list.** `readdir` of `.acpx` was compared to a
literal pair in one order, and a runner handed the two names back the other way
round. It is sorted now, exactly as the assertion above it already was: what
this row is about is which names are there, and a directory promises nothing
about the order it enumerates them in. The membership is unchanged and nothing
is skipped, slept on or excluded.
@taras taras changed the title ✨ Let trusted hosts stop the execution they own Let trusted hosts stop executions and join owned cleanup Oct 28, 2026

@taras taras left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fable review — correctness, performance, minimalism

Verdict: no blocking defect found in the Stop capability. Three actionable findings are posted as line comments (one platform-conditional cleanup concern in the generic ACP ownership change, two minimalism/doc items). Reviewed: the complete published diff 267cc55f..b639fab6 (9 files, 3 commits), architecture.md + AGENTS.md + the Effection v4 guidance, and the supplied CI/receipt evidence.

Correctness (what I checked and how)

  • Ordering of a stop. stopDocument asks the root first, then halts the runner task and joins it, then closes output with exactly the text that cleared the scanner, then settles once. The refused branch (requestInterruption() === false) halts nothing and waits on the completion, so an in-flight Close(ok) is never cancelled — EP29 covers it and my harness confirmed it on 90 samples (one ok close, stop Ok, completion Ok).
  • Claim exclusivity in durableRun. open → claimed → appending → settled/ended is only moved from inside the run; the ordinary return/catch paths refuse to acknowledge a terminal once claimed; the cancellation finalizer appends the one cancelled Close and re-checks the sticky durability failure and replay alignment first. RootInterrupted is bypassed by object identity, not class, and the test proves a carried interruption still gets an err close elsewhere.
  • Reachability of the "workflow returned after the claim" path from the host. HostExecution.stop() claims and calls runner.halt() in one synchronous frame. Effection 4.1.0's reducer replaces a queued resume with the unwind (routine.resume(Ok()) overwrites resumeWith with unwinding = true), so from the host that path is unreachable; the completion for an accepted stop is always HostExecutionError("this document execution was stopped") or the halt's own failure. The durable-streams test reaches it only by claiming without halting.
  • Failure identity. stopInvocation ranks durability, then Files, then the stop's own failure, then teardown, each by identity, and EP29/EP31 assert refused === failed. Consistent with reconcile.
  • One edge worth knowing, not a defect: a Stop that lands while a replayed completed run is still restoring its output (before durableRun would have handed out a control) halts the restoration and the handle completes as stopped while the journal keeps its ok terminal. Nothing is written twice; the handle and the journal just disagree in that window. If that matters to the REPL owner, stopDocument could wait on the completion when the journal already holds a root close.
  • Focused tests at head, locally under Deno 2.9.7: execution-protocol, durable-run, session-ownership-cli — ok, 6 passed (92 steps), 0 failed. First required green: run 40721115143.

Performance (measured, current head b639fab; base 267cc55 for the comparable workloads)

Boundary: application operation boundary (performance.now() around yield* execution.stop() inside one Effection run()), canonical executeInstalled + InMemoryStream, inline one-paragraph document suspended in a component with an ensure() cleanup gate. Warmup 5, 30 measured samples per workload per runtime, Apple M1 Max, Deno 2.9.7 / Node 26.5.1 / Bun 1.3.14. No UI, terminal or paint boundary is involved in this PR, so no FPS/frame/input figures exist here (that obligation stays with PR #1004).

workload (ms, p50 / p95) Deno Node Bun
W1 stop of a running document, cleanup released at once — ask→answer 0.585 / 0.779 0.784 / 1.157 0.699 / 0.886
W1h same with cleanup deliberately held; control cost after subtracting the measured hold 0.593 / 0.688 0.683 / 0.838 0.609 / 0.817
W2 eight concurrent asks on one running document — all answered 0.789 / 0.897 0.866 / 1.010 0.816 / 0.973
W2 repeated ask after settlement (each) 0.002 / 0.004 0.002 / 0.005 0.002 / 0.008
W3 ask during an in-flight Close(ok) — journal release→answer 0.301 / 0.350 0.288 / 0.365 0.304 / 2.338
W4 durableRun + control + halt (no document) 0.122 / 0.148 0.135 / 0.160 0.081 / 0.096
B1 scope-cancel of the same document (not a Stop equivalent: no close, output never closed), head / base 0.365 / 0.409 0.446 / 0.539 0.393 / 0.456
B2 ordinary run to completion, head / base (p50) 3.48 / 3.71 4.06 / 5.06 4.80 / 5.00

Every sample held its invariants (one cancelled root close carrying only status, output closed with exactly the observed partial text, cleanup joined before the answer, all repeated asks Ok). The added cost of a Stop over a bare scope cancel is ~0.2–0.35 ms at p50, of which ~0.1 ms is the cancelled-close append itself (W4 vs the no-control halt). Ordinary completion shows no regression on any runtime; the only measurable delta is the isolated durable-only halt on Deno (0.025 vs 0.008 ms p50 — two extra ensure() registrations), invisible inside any document run.

CPU profiles (Node and Bun --cpu-prof; Deno via the V8 inspector since it has no --cpu-prof): hot self-time is Ajv schema codegen for component props and Effection scope construction, i.e. document setup, plus the tsx loader under Node (harness artefact). durable-streams/run.ts self-time is 3 ms of 3.1 s sampled on Node; inclusive time of stopDocument+haltRunner+stopInvocation is tens of microseconds per stop. Memory/allocation was not measured and nothing is claimed about it. The ownership change in posix.ts was probed rather than profiled (see the line comment).

Minimalism

The capability is small for what it guarantees. Two items would make it smaller or truer without changing behaviour (line comments on run.ts and execute.ts). The architecture and durable-integration paragraphs are accurate to the code.

Not covered here

Linux inode reuse was not reproduced locally (the preserved CI failure remains that evidence). No UI path was exercised. No merge or CI action was taken.

*/
function same(a: Stats, b: Stats): boolean {
return a.dev === b.dev && a.ino === b.ino;
return a.dev === b.dev && a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 1 — correctness (platform-conditional, PLAUSIBLE, not reproduced on macOS). The creation-time identity is captured before this owner's own metadata operations, so on a filesystem/runtime pair that substitutes ctime for birthtime the owner's exit leaves its own files behind.

Where: stageIdentity is read at line 416, then link(stage, path) at 419 and the stage unlink at 422; directoryIdentity is read before chmod(directory, 0o700) at 384. Each of link, unlink and chmod updates the inode's ctime. Node documents that birthtime "may instead hold either the ctime or 1970-01-01T00:00Z" where creation time is unavailable (libuv's non-statx fallback on Linux is the ctime case — e.g. a seccomp profile that denies statx). On such a host the exit comparison in same() is false for the descriptor, the stage file and the private directory, so removeOwned/rmdir leave them: every publish leaks a stale descriptor and a .sidekick-<uuid> file, and a later readDescriptor reads the stale descriptor. The direction is fail-safe (nothing foreign is ever removed), which is why I rate it a cleanup regression rather than a safety defect.

Evidence: measured on this host (APFS, Node 26 and Bun 1.3): across write → link → unlink → chmod on one inode, birthtimeMs stayed identical while ctimeMs changed at every step — i.e. the ordering only matters when the substitute is ctime, and CI's ext4 under statx reports a real btime, so green does not exercise it. The comment's claim that "both readings carry the same substitute" holds for the epoch-0 substitute only.

Recommendation: take the retained identity after the last metadata operation this owner performs — after link, re-read lstat(path), check dev/ino equal the pre-link reading before adopting it, and use that reading for both the stage removal and descriptorIdentity; read directoryIdentity after the chmod. That keeps the stricter predicate and makes it hold under either substitute.

Comment thread packages/durable-streams/run.ts Outdated
}

/**
* One run's own authority to end its root as cancelled.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 2 — minimalism/doc (CONFIRMED). This JSDoc block ("One run's own authority to end its root as cancelled…", lines 85–92) is orphaned: a second JSDoc block follows it immediately and attaches to class RootInterrupted, so this one documents nothing, while export interface DurableRootControl (line 116) — the thing it describes — has no doc comment.

The RootInterrupted comment also says it is "Never seen outside this module's own frames", but the return path throws it to the caller by design (line 369) and the test "a request accepted while open ends the run, however the workflow returns" asserts the caller catches exactly that object.

Recommendation: move this block onto DurableRootControl, and reword the RootInterrupted sentence to say it leaves durableRun as the caller's error with no terminal written around it. No behaviour change.

* within a kind. What comes back is the object the classifier found, never a
* rebuilt one, because the engine's fences match on identity.
*/
function fromEach<T>(

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 3 — minimalism (CONFIRMED, behaviour-preserving). fromEach is a third spelling of the kind-then-occurrence ranking that reconcile (line 4055) and reconcileExpansion (line 3970) already write as ?? chains over durabilityFailure / filesFatalFailure, all three returning the classifier's object by identity.

Recommendation: either write the two lookups in stopInvocation as the same ?? chains the neighbours use and drop fromEach, or keep the helper and use it in the two existing sites so there is one spelling of the rule. Result is identical either way (first non-undefined classification in argument order, identity preserved), and the EP29/EP31 identity assertions keep it honest.

taras added 4 commits October 28, 2026 12:46
An attachment captured dev, ino and birthtime when each object came into
being, and then kept changing those objects: the directory was chmod'd and
had a socket created inside it, the staged descriptor was written, linked and
unlinked. Every one of those moves ctime, so where birthtimeMs stands in for
a ctime the kernel does not expose, a reading taken at creation no longer
describes the file by the time cleanup judges it, and an owned file is left
behind.

Re-read each identity once this owner has finished changing the object it
names, and judge the directory before its socket child is taken out of it,
since removing a child changes the directory the question is about. A second
reading answers for the same inode or it is somebody else's file; sameNode
says so rather than publishing theirs. The first reading stays in place until
the second replaces it, so a cancellation in between still joins and adopts.

Also: give DurableRootControl back the doc that had drifted off it, and say
plainly that RootInterrupted does reach the caller on the return path. And
spell the failure precedence one way — reconcile and reconcileExpansion now
use the same fromEach the stop path already had.
Reading an identity again is only worth doing if the reading is checked, and
these were checked in the wrong place. The stage comparison took its own fresh
Stats for both sides, so it could not see a replacement at all. The published
one stored the reading first and threw second — and the finalizer joins that
acquisition, so the identity it had already stored was a foreign file's, and
cleanup unlinked a file this owner never published. Cancellation made it worse,
not better: the callback still runs, and nothing after it does.

So the check moves inside the retained callback, which is where authority is
actually installed, and it compares against an identity captured before the
refresh that the refresh never writes over. A reading of another device and
inode is refused there; the identity this owner already held stays, and the
replacement stays somebody else's file.

The directory question moves too. Closing the listener unlinks its socket —
Node's documented behaviour for a path it bound — and taking a child out of a
directory changes the directory, so asking afterwards asks about a directory
this teardown has already modified. It is asked once, before any of that, and
every listener and socket join still happens before the directory is removed.

Four cases hold a refresh's read in flight: a stage name replaced under it, a
published name replaced under it, a cancellation delivered while it is
pending, and the directory judged while its socket is still in it. The first
three remove the replacement without the fix; the fourth reads the directory
after its socket is gone.
Asking which directory this is before anything else can change it was right.
Raising the answer's failure from there was not: a read that fails is not a
listener that closed, a peer that was let go or a name this owner knows it
published, and all three were still this teardown's to finish. The attachment
and its socket were left behind with the error on its way out.

So the failure is kept rather than raised, and the stages that never depended
on that question run. Ownership stays unconfirmed, which keeps `rmdir` off the
table — a directory this owner could not identify is not one it may remove,
and that is the direction to fail in. The error this teardown ends with is the
one it was handed, by identity. When finishing also fails, both travel: the
reading that failed and the removal that failed, neither standing in for the
other.

Two cases fail the directory's teardown read with EIO through the reader seam
already there. Without this, the first leaves the descriptor and the socket on
disk, and the second reports only the read. With it, the published name and
the socket are gone, the accepted connection and the listener are joined, the
unidentified directory is still there to be looked at, and the original error
is observable either way.

Also: the two message assertions added last time read the error by narrowing
now, not by casting it.
A descriptor that will not unlink says nothing about the staged name beside
it, and the one try/catch around the whole teardown treated it as if it did:
the first stage to fail ended the rest. Cancel a publication at the staged
refresh — both names exist then, one inode under two of them — make the
parent unwritable, and only the published name was ever attempted. The staged
one stayed on disk and its failure was never reported, so what came back said
less than what happened.

Each stage is attempted on its own now and keeps what it fails with. The order
is the order they were met: the reading of the directory first when that is
what failed, then each name as it was tried. One failure still leaves as
itself, because a caller's fence may be looking for that exact object; more
than one travel together.

The early ownership decision, the joins, validate-before-adopt and the
fail-safe unconfirmed directory are where they were. Nothing is removed that
this owner did not publish, and nothing is removed because removing it would
tidy up the leftovers.

The new case holds the staged refresh, takes write permission off the parent
and cancels there. Before this it reported two failures and left the staged
file; now it reports three, the reading and both names, and both are still on
disk to be seen.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 8 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/sidekick-acp/posix.ts
Comment thread packages/cli/src/sidekick-acp/posix.ts
Comment thread packages/cli/src/sidekick-acp/posix.ts
Comment thread packages/cli/src/sidekick-acp/posix.ts
// inside the runner task, because the stop below is a *second* frame that has
// to read and finish the same things: the text that actually cleared the live
// scanner, the one settlement this completion ever takes, and the one close
// its consumers ever see.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// its consumers ever see.

// the run finishes writing, publishes its own result and output, and this
// waits for it. Halting now would cancel an append over a backend that
// may already hold it, and a cancelled ending written around it would be
// this run recording two.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// this run recording two.

// unwinds after everything the workflow owns. Inert unless this root
// accepted a claim while it was still open: an ordinary halt reaches here
// unclaimed and appends nothing, which is what every caller without a
// control has always seen.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// control has always seen.

control({
requestInterruption(): boolean {
if (phaseNow() === "claimed") {
// The same ask, answered the same way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The same ask, answered the same way.

@taras
taras enabled auto-merge (squash) October 28, 2026 18:47
taras added 2 commits October 28, 2026 15:30
Two of these cases replaced the attachment by unlinking it and writing a new
file at the same name. On this host that is a different file and the cases
pass. On the CI host it is not: the published name is the inode's last link by
then, Linux frees the inode at the unlink and hands its number straight to the
next file created, and the stranger arrives wearing the identity of the file it
replaced. One case waited out its five minutes because nothing refused; the
other watched the owner's cleanup delete the stranger it had adopted.

So the stranger is written under its own name while ours still holds the one
under test. Two files existing at the same moment cannot share an inode — no
allocator can do that — and the cases now say so out loud before replacing
anything. Then one rename moves it over, which never leaves the name empty.

The refusal, the absence of publication, the preserved replacement with its own
inode and bytes, the cancellation and the joined cleanup are all asserted
exactly as before. The published case also stops suspending after a publication
it does not expect, so a future regression says what is wrong instead of taking
three hundred seconds to say nothing.

This host cannot reproduce the CI failure — it never reuses an inode, measured
under all three runtimes — so a green run here does not establish the Linux
one. What it does establish is that the replacement no longer depends on which
host it runs on.
A device and inode read off a name is a reading of history. Once the last
link to a file goes, that inode number is handed to whoever creates the next
one — so the name this publication had just written could come back wearing
its number, pass the check, and become the authority that later unlinked
somebody else's file. Timestamps cannot settle it: one runtime reports them
to the second, and a host that has no creation time to report substitutes one
that moves.

Lifetime settles it. The publication now opens the file it creates and keeps
that handle for as long as it is making claims about it. While the handle is
open the original cannot be freed, so its number cannot be given away, and
"is this name still that file" has an answer rather than an inference. The
numbers come from the handle, as bigints, because rounding an inode number to
a double would put a second ambiguity where this removes the first. A name's
own reading answers that one question and never becomes the answer.

The handle is taken even when the caller is cancelled between opening and
owning, identified from itself rather than from a pathname when cancellation
got in first, and let go of last — after every name decision, as one more
stage whose failure joins the others in its turn rather than replacing them.

Sockets, directories and unpinned names keep the predicate they had. The five
cases describe a recycled number only where one could be recycled: the live
original is checked, not assumed, and a closed one is what the old lifetime
hands them.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 11 redundant comments. Inline suggestions to remove them below.

let directoryAcquisition: Promise<string> | undefined;
let directoryIdentity: Stats | undefined;
// The reading the directory was created with. Never written over by a
// refresh: it is what a refreshed reading has to still be.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// refresh: it is what a refreshed reading has to still be.

// taking a child out of a directory changes the directory. Asked
// after that, the question is about a directory this teardown has
// already modified; asked here, it is still a fair one. Every join
// below still happens before the directory is removed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// below still happens before the directory is removed.

// published, and every one of those is still this cleanup's to
// finish. So the failure waits below, where it is what this teardown
// ends with — and `ours` stays false, because a directory this owner
// could not identify is not one it may remove.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// could not identify is not one it may remove.

// letting go of it is the final thing released rather than the first.
// A close that fails joins the others in its turn; nothing is
// removed on an unpinned reading afterwards, because there is no
// afterwards.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// afterwards.

// inside the runner task, because the stop below is a *second* frame that has
// to read and finish the same things: the text that actually cleared the live
// scanner, the one settlement this completion ever takes, and the one close
// its consumers ever see.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// its consumers ever see.

// retained history the definition did not align with is divergence rather
// than a stopped run: neither is papered over with a cancelled close, and
// neither is swallowed — whoever asked this root to stop has to learn
// which failure ended it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// which failure ended it.

control({
requestInterruption(): boolean {
if (phaseNow() === "claimed") {
// The same ask, answered the same way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The same ask, answered the same way.

}
// The phase moves first, so a cancellation asked for from here on joins
// this ending rather than cancelling the write and compensating over a
// backend that may already hold it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// backend that may already hold it.

// settles it, and a terminal written around that would be this run
// recording two endings. By identity, and only this run's own: an
// interruption some other root minted is an ordinary failure here, and it
// gets the ordinary error close every other failure gets.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// gets the ordinary error close every other failure gets.

};

// The same claim rule as the ordinary return: a Stop that holds this open
// root is its ending, and this failure does not acknowledge one around it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// root is its ending, and this failure does not acknowledge one around it.

@taras
taras merged commit 70e3315 into main Oct 28, 2026
44 checks passed
@taras
taras deleted the capability/execution-stop-971 branch October 28, 2026 20:31
taras added a commit that referenced this pull request Oct 28, 2026
The three posted line comments on PR #1008 are capability-owned; the layout
work here is unchanged by them.
taras added a commit that referenced this pull request Oct 28, 2026
Pinned at 70e3315, the squash merge of #1008.
This branch already carries that reviewed content, so the integration changes
no file: it records the accepted base, and the tree is the one it had.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant