The provider's Linux decisions now live in LinuxScopeSelector, a state machine
over injected observations: the deep probe, the user-manager listener, the
manager confirmation, and the systemd-run binary identity. Each decision
carries its reason and probe trigger, the runtime logs it at debug level, and
table-driven scenarios cover every transition.
- A positive verdict probes again when the systemd-run found through PATH
changes device, inode, size, or modification time, so an in-place upgrade
past v254 cannot reuse options that no longer keep argv literal.
- The listener is read before the probe, so a manager that starts during a
failed probe counts as a new listener.
- The kernel socket table is decoded as UTF-8, matching runtime directories
outside ASCII.
- LinuxScopeNotStartedError drops its unused code field.
A cached positive verdict let a hung user manager that still listens fail every
later command, and a cached negative verdict ignored a manager that started
later until the provider restarted.
- A scope launch whose bootstrap never consumed its launch request now fails
with LinuxScopeNotStartedError, and the runtime clears the verdict it
launched under, so the next eligible spawn probes again and records the
fallback when that probe fails. A generation counter keeps launches from a
superseded probe from forcing another probe.
- After a negative verdict, the runtime records the /proc/net/unix listener on
$XDG_RUNTIME_DIR/systemd/private and probes again only when a different
listener, meaning a new manager instance, appears.
The real-socket test rejects on a listen error instead of hanging, the table
fixture comment names the accepted-connection row it models, and the remaining
linux-scope module mocks provide confirmLinuxManager. The README notes that
the socket-table read matches only listeners in the same network namespace and
costs time proportional to the table size.
systemd closes the private listening socket on shutdown without unlinking its
path, so the socket file outlives a manager stopped in place. The fast path now
reads /proc/net/unix, where the kernel lists a bound socket only while a
process holds it open, and accepts the manager only when a process listens on
$XDG_RUNTIME_DIR/systemd/private; otherwise the systemctl query decides. The
scope startup error names an unreachable manager as the cause, and the spawn
test again asserts that Windows launches never reach the fallback.
Without a per-spawn check, a user manager that stops after a positive probe,
such as after a logout without lingering, made every later scope launch fail.
Each eligible spawn now confirms the manager: its socket file at
$XDG_RUNTIME_DIR/systemd/private disappears when the manager stops, so its
presence answers with one stat; otherwise the restored systemctl --user show
query decides and the spawn falls back while it fails. A crashed or hung
manager that leaves its socket file still fails the scope launch with the
existing restart hint.
The probe always passes --collect, which systemd-run rejects before v236, so those hosts use the fallback regardless of the --expand-environment=no retry.
With the probe cached for the provider lifetime, a user manager that stops
after a positive probe makes every later scope launch fail. The startup error
now says to restart the provider to probe native containment again. The README
no longer lists older user-systemd among fallback hosts and notes the second
probe attempt for a systemd-run that rejects --expand-environment=no.
The README says the provider also keeps a failed or timed-out first probe and
names the user manager as reachable rather than live, since it is checked once.
The probe cache documents why no later spawn repeats the probe, and the spawn
test now starts two Windows commands to match its per-spawn Job recheck title.
systemd-run before v254 rejects --expand-environment=no, so the scope probe
always failed and those hosts used the weaker fallback. Its scope mode never
expands argv, so the probe retries without the option only when systemd-run
reports it as unrecognized, and scope launches reuse the probed options.
systemctl before v252 rejects --kill-whom. Use --kill-who, the older
spelling, which newer systemctl accepts as an unambiguous prefix.
Every Linux spawn() and spawnTerminal() ran a synchronous spawnSync probe
before launching the command: the full systemd-run scope probe until it first
succeeded (a failed verdict was never cached), then a systemctl manager recheck.
spawnSync blocks the host event loop until the probe exits, so hosts without a
usable user manager paid the full probe on every grep, glob, and shell command.
Keep the first verdict, positive or negative, for the provider lifetime and
drop the per-spawn manager recheck.
Use the package's own `SubprocessTerminalActivity['state']` instead of a derived
alias, label the samples as `state:revision` so a reader cannot mistake the
suffix for a count, name the summariser's bound as a group count, and state only
the two observables the prompt count supports.
Refs #5755
An activity poll that times out reports only "unknown" or "Matcher did not
succeed in time". The Linux standby has failed that way twice, at different
polls, and the log shows nothing about what `inspect()` read, which returns
`unknown` for several distinct conditions.
Every activity poll now records the state and revision it samples and counts the
prompts printed while it waits, so a timeout names both. Repeated samples
collapse and the printed list is bounded. Timeouts, intervals and expectations
are unchanged, and where the poll budget equals the test budget the test timer
can still preempt this message.
Refs #5755
Delete @deepseek-ai/dsh-invariants, every package's ./invariant companion
and its tests, the invariant gates and Vitest setup, the scoped-event
resolver generator, and the INVARIANT-coded listener rethrow paths.
Update docs, READMEs, Agent Notes, and add an upgrade guide.
Review follow-ups: the Electron payload smoke allocates the way the product
does on Windows, so the packaged tree resolves the console host assets it now
depends on; the README states that dependency and its loud failure as a known
limitation; the terminal handle takes the platform this method already
resolved; and the note links the earlier tail-grace note, records why that
grace cannot cover a malformed tail, and drops the claim that the smoke
exercises the operating system host.
The per-platform case reads the third spawn argument to assert the console-host
option is absent on POSIX; an untyped vi.fn() declares no parameters, so the
indexed call argument fails typecheck.
The README, the Ctrl-C comment, and the bare-kill comment still described
Windows' own console host as the one backing the PTY. Name the host the code
now selects, record the exit-event behaviour of both teardown paths measured on
the pool machine, state that a missing conpty asset fails loud, and move the
note to the bug-fix class next to the other Windows console fix. The terminal
case now asserts the option key is absent on POSIX rather than not true.
The console host Windows provides, on the images the master standby pool runs
(Server 2022, Windows 10 22H2), replays a prompt line that scrolled out of the
viewport as `\ndsh>\x1b[1C`: the trailing space becomes a cursor move, so the
persistent-pwsh readiness path never sees its `dsh> ` tail and every send waits
out the silence tier. The same case passes on the Blacksmith Windows image,
whose console host emits the tail verbatim.
Allocate Windows PTYs with `useConptyDll: true`, so the PTY is backed by the
OpenConsole the node-pty package ships. The option is set only for `win32`;
the terminal spec pins it per platform.
Probing the pool machine with the bundled console host rendered the `dsh> `
tail for all seven prompts of the failing case's command sequence, and
`pty.pid` still resolves to `pwsh.exe` and equals the shell's own `$PID`.
* feat(i18n): key translation pairing records by heading section
Replace the whole-file blob hashes in *.i18n.yaml with one entry per
heading section that still holds language-specific content, keyed by the
English heading-slug path. Blocks identical across the pair (generated
table rows, code blocks, shared regions) stay out of the hash, so edits to
different sections and identical regenerations merge with Git's default
text merge, including on GitHub.
Remove the dsh-translation-pairing merge driver, its resolver command and
installer wiring; gen-translation-brief recovers last-confirmed text from
commit history. Migrate all active records, archive the superseded Agent
Note, and record the replayed conflict reduction in the new one.
* test(i18n): refresh translation prompt snapshot for the terminology note
* feat(i18n): exclude only code blocks and generated regions from pairing records
Replace byte-equality masking with an explicit rule: a record hash skips
fenced code blocks and BEGIN/END GENERATED regions, the two kinds of
content the pairing gate already requires to be identical on both sides.
Move generated data into regions written into both pages:
gen-config-catalog renders both languages and the record with per-package
regions labelled by code identifiers; gen-module-graph keeps its table in a
region; gen-doc-graphs splices the event matrix regions into the Chinese
page. Add a generic region splice beside the region grammar.
- Agent Notes: cross-link the new decision with the native fatal recovery
and fail-loud release notes both ways, rewrite the older note's
console-retention and dialog-detail facts in place, and state the report's
actual bounds.
- crash-report: cut the rendered error section at 256 KiB and prune only
files matching the generated crash-<time>-<source>.log syntax.
- main: backendReady follows the backend state except during shutdown, so a
failure while tearing down a ready backend still reads as running.
- subprocess-local: the temporary-file-cleaner hint is appended only for
ENOENT; other codes rely on the appended error.
- client-modules: describeError is module-private; BootModuleRow.url's
JSDoc names its fallback role.
client-modules: a batch that already executed is never requested again,
even when its first importer was a row it did register — every executed
batch URL is remembered and a still-missing row goes straight to its
one-resource URL. The in-process module system test in test-support is
updated to the aggregated arrival message. describeError replaces three
inline non-Error ternaries and is covered.
subprocess-local: the spill path is published only after the exclusive
open succeeds, so a failed open (EEXIST on a planted entry included) can no
longer unlink a path this process did not create; a reporter that throws is
contained and written to stderr; logSpillFailure builds the owner reporter
for both the runtime and the SSH helper so the logging path is exercised
once instead of duplicated. Tests cover the runtime logger, the bare-caller
stderr default, EEXIST, and a throwing reporter.
desktop: a fatal failure during shutdown still writes its report; the
report phase follows the backend state so a failure during a restart is
'startup' again; writeReport is a required recovery operation and the
dialog tests await the write; the shell pid is labelled; the withheld
response-header list states why set-cookie is dropped.
app-boot, READMEs, Agent Note: "never resumes" is restated as control never
returning to the failed operation while the loop runs until release
settles; inspect output is bounded; incident inferences (who removed the
spill directory, which batch failure occurred) are written as inferences.
OutputCollector.spillAll ran openSync/writeSync inside the stream 'data'
listener with no containment. When the private per-process spill directory
had been removed by a temporary-file cleaner, the first overflowing chunk
threw ENOENT from Pipe.onStreamRead, became an uncaught exception in the
Desktop Host, and the Electron shell reported it as a fatal failure.
Spilling is best-effort by contract (spillPath is optional; a failed final
close already withholds it). Every filesystem failure while opening or
appending the spill file now discards the spill, reports once through the
owner's logger, and leaves in-memory collection untouched. Spill inputs move
into one SpillOptions object with the reporter, so tail-only collectors no
longer pass an empty directory; prepareManagedProcessBinding is the explicit
resolve step for the directory and reporter defaults, and the runtime and
SSH helper supply their plugin loggers.
Tests cover ENOENT on open (removed directory), ENOTDIR on open, and ENOSPC
on append after the file exists, asserting the tail stays byte-exact, the
spill file is withdrawn, and the reporter fires once.
Merge master 62b700bba7bb71e66d2d5a3b850ba8f4dd0fd6b4 into release/session-log-v4 at 4825bc01a999f564e79172a6af28e34fc757c540. Preserve this pinned integration checkpoint before any later upstream movement so the dependent session-format repairs have an immutable rebase base.
Keep release projection-backed child catalogs, independent Session references, exact fork behavior, V4 writer, and existing migration data. Adapt the incoming subagent sidebar chat to refreshProjections rather than the removed dedicated catalog API, preserve title projections in complete projection fixtures, and retain both sidebar actions and release lineage/status behavior. Remove the duplicate SessionProjectionMap import introduced by automatic merging.
Preserve both disjoint archive seals without changing any existing hash. Regenerate current Cordis and documentation catalogs from the combined source, retaining release session methods plus master slot factories. The frozen V0-V3 runtime paths are unchanged. Initial validation passed the full build, 93 focused Client tests, and all 1905 archived artifact seals; remaining integration and snapshot checks follow on this checkpoint.