chore(dsh): base the LoopX plugin on the released 0.1.5 client surface - #4624
huangruiteng wants to merge 3 commits into
Conversation
The plugin still built its host and client halves against `0.1.1-rc.2` while
the managed Turn host already ships on the released `0.1.5` line, so the two
dsh surfaces had drifted apart. That is not a preference: the 0.1.5 line does
not publish `@deepseek-ai/dsh-client-runtime`, which is where this package read
the `slots` service seat and its retirement is what the manifest previously
ordered in `dsh.client.inject`.
Move the development, host, and client surfaces to `0.1.5-rc.2` and admit only
`>=0.1.5-rc.1` peers:
- `dsh.client.inject` and the client Context augmentation now name
`@deepseek-ai/dsh-client-ui-renderer`, the 0.1.5 owner of `ctx.slots` and
`slots/changed`, instead of the retired runtime package.
- `Session.events` became `Session.snapshotEvents()`, and `Inbox.hasPending`
became the `nextTurn`/`nextStep` queues, so the activation fold, the candidate
cursor reads, and the driver readiness gate follow the installed shape.
- `Connection.rpc.handle` is back to two arguments; its third
`{ authority: 'loopback' }` option no longer exists because 0.1.5 registers an
already-authenticated channel.
- The observer keeps treating token-level `assistant/chunk` as un-consumed, now
as an explicitly retired type that only older durable logs replay, so a
historical log cannot turn every token into an `unsupported` row.
The lockfile is regenerated rather than patched: the previous one still resolved
part of the `@deepseek-ai/*` graph at `0.1.1-rc.2`, which mixed generations in
one process and broke the host before this change.
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
…ntracts Every layer of plugin validation still described the 0.1.1 wire, so the packed install, the boot graph, and the real-runtime phase would have kept passing on a generation the package no longer targets. Follow the installed contracts instead of loosening the assertions: - Fixtures own their session log behind `snapshotEvents()` and drop the retired `inbox.hasPending`, and typed `SessionSeq` admission replaces the bare sequence numbers. - The synthetic boot manifests carry the initial-load batch 0.1.5 requires for every entry, and the client-discovery probes use the Loader `internal` resolver, the located-manifest return shape, and the opaque initial row revision the generation now produces. - The real host is addressed as `<namespace>/<method>` with one `args` payload field, which is how 0.1.5 exposes `session/create` and `skills/list`. - The packed carrier probe registers the standalone authenticated `/loopx` channel and constructs the service with the three-argument 0.1.5 shape, so the phase again exercises the host-half state machine rather than failing on an undefined browser-auth face. The shared `/api/loopx.goalbar` route stays covered by the real-profile phase. - The clean-container smoke pins and asserts `0.1.5-rc.2`. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
The RFC still described the plugin as building on `0.1.1-rc.2` with the upgrade tracked as an open pin item, and the plugin README still claimed a `loopback-only /loopx` channel registered through a Connection `authority` option that 0.1.5 no longer has. State what now ships: the plugin builds on the released `0.1.5-rc.2` line, the npm peer range admits only `>=0.1.5-rc.1`, and the three upstream moves that forced a new release line instead of a patch are named so a future reader does not try to serve both generations from one `dsh.client.inject` list. The GoalBar carrier is described as Connection's own authenticated registry — the shared `/api/loopx.goalbar` route where a Fetch face exists and the standalone `/loopx` channel otherwise — with no second credential owned by this package. The observer prose keeps its subject: token-level `assistant/chunk` is not consumed, and is now named as a retired type that only older logs replay. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
9fde075 to
ef7efd6
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Reviewed exact head: ef7efd65714503a7033354d9effcfc0c3b477d85 (PR #4624, base main).
Review policy revision: 6. Review evidence was produced and checked with
loopx pr-review --check-result /tmp/review_result_4624.json --packet /tmp/pr-packet-4624.json → ok: true.
动机
This is not a preference change; it is a tracked pin item. The repository's own RFC
(docs/architecture/rfcs/harness-selection-dsh-pi-v0.md) records that the dsh-side package built
its development and client surfaces against 0.1.1-rc.2 while the released 0.1.5 line no longer
publishes @deepseek-ai/dsh-client-runtime — the package this manifest ordered in
dsh.client.inject and read the slots service seat from.
The old behavior was a deterministic failure for every install into a current profile, and it was
silent in the worst way: the package looked installed while its slots provider was never ordered
ahead of it, and the stale lockfile additionally resolved part of the @deepseek-ai graph at
0.1.1-rc.2, mixing two generations in one process so the host failed to boot outright
(Module '@deepseek-ai/dsh-llm' does not provide an export named 'assertNever').
After the change, the real profile boots DSH 0.1.5-rc.2, composes the boot graph, serves and
materializes the client bundle, and Start/Pause work over both carriers. The affected operator is
anyone installing the dsh-side package, i.e. the managed/steward host surface and its GoalBar
users.
Delivery verdict is a justified increment, not goal closure: the remaining gap is the merge plus
the matching npm release asset and marketplace/update-feed entry, which is the maintainer release
step and does not belong in this diff. The boundary is independently reviewable, testable by the
existing capability-owned canary profile, and reversible by reverting the branch.
改动思路
The entry point is the package manifest (dsh.client.inject, peerDependencies) together with its
host and client entry modules. The authoritative input is the installed @deepseek-ai package set
of the profile; the decision owner for row ordering and authentication stays the DSH host
(ClientModuleRegistry and Connection), so the package states edges and reads state rather than
owning either.
Reuse was the deciding factor rather than a local shim. In 0.1.5 the slots seat and the
slots/changed event are declared by @deepseek-ai/dsh-client-ui-renderer, so the manifest and the
client Context augmentation now name that owner. @deepseek-ai/dsh-cordis-client-runner was
considered and rejected: it is the browser half of dynamic dual-half Cordis packages, which this
package is not, so naming it would have added a false ordering edge rather than a reused owner.
No compatibility layer, no second slots provider, and no new state were introduced.
Positive path, in order: dsh plugin --profile web add <tarball> → the registry scans
dsh.client → compose() orders the ui-renderer row ahead of the plugin → window.__DSH_BOOT__
carries both entries and their initial-load batch → the shell materializes lib/client.js →
apply() registers the GoalBar slot and locale → the host registers the GoalBar carrier on the
generation's authenticated channel.
Negative path: a malformed GoalBar envelope carrying a reflected field is answered with the fixed
typed bad-request carrier and no echoed payload, and a request with a non-loopback Origin is
rejected by Connection's own Host/Origin fence. Connection owns the fence; the GoalBar service owns
the typed carrier; neither retries.
具体改动
22 files, +6608/-7139. The line count is dominated by pnpm-lock.yaml at +6338/-6982, which is
mechanical re-resolution: the previous lock still resolved part of the graph at 0.1.1-rc.2, which
is exactly what broke the host boot. Hand-written behavior is five source files, four typed
fixtures, three smokes, two version assertions, and docs.
关键代码讲解
packages/dsh-loopx-plugin/package.json — dsh.client.inject. Names the client packages whose
browser bundles must precede this row. Before it named @deepseek-ai/dsh-client-runtime; after it
names @deepseek-ai/dsh-client-ui-renderer. The invariant is that every named package must both
exist in the line and own a ./client export; a dangling name silently contributes no edge, which
is why the artifact smoke now reads the inject list back from the installed registry instead of
restating it.
packages/dsh-loopx-plugin/src/driver.ts — inboxHasPending(). Replaces the retired
inbox.hasPending flag at six call sites with one helper over the two pending queues. The critical
branch is that readiness must stay false while any queued input exists, so a competing human message
still suppresses automatic continuation; the driver fixtures that exercise "human input arrives" and
"terminal Agent error" pin that behavior.
packages/dsh-loopx-plugin/src/goalbar/events.ts — latestGoalBarCandidateSeq(). Now reads one
session.snapshotEvents() snapshot per call instead of indexing the retired events property. Only
sequence-bearing step/end or turn/end events advance the cursor; a missing sequence returns
null, so a wrong cursor would surface as a missed runtime update rather than a wrong mutation.
packages/dsh-loopx-plugin/src/observer.ts — RETIRED_SESSION_EVENT_TYPES. assistant/chunk
left the typed event union in the new line, so it is no longer a case in a typed switch. It is
named in an explicit set checked before the switch, which keeps the semantic (token rows stay
unconsumed) while preventing a replayed older log from turning every token into an unsupported
row. Genuinely unknown types still fall through to the unsupported branch.
对主干的风险
The strongest regression scenario is an operator on a 0.1.1 host installing the new package and
silently never getting the ordering edge, or an old durable log replaying assistant/chunk rows.
The peer range fails such a host at install time instead of mis-composing the graph, and the retired
set keeps replayed rows out of the consumed-kind ledger. Blast radius is limited to this package's
own loader rows and its observer ledger; no other LoopX authority or persisted state is touched.
Rollback is branch revert: the package stays installable on the older line until the release that
raises the peer range ships.
The unit suite cannot see host-side composition, which is the honest limit of this evidence: tests
can be green while the real host refuses the composed graph, because the boot row and inject list
are produced by DSH, not by the package. That is why the load-bearing evidence is the real-profile
phase, not the unit suite. The packed carrier probe keeps a permissive auth face, so its fence is
asserted by the real-profile phase rather than by itself.
Validation at the reviewed head:
| case | command | result |
|---|---|---|
| changed invariant, positive | python3 examples/canary/dsh-loopx-plugin-validation.py --phase runtime |
passed; self-reports real-profile: DSH 0.1.5-rc.2 |
| material negative / failure | same phase plus --phase package (bad-request carrier, loopback fence, registry-derived inject equality) |
passed |
| repository required checks | --phase quality, --phase package, targeted pytest for the touched docs and provider contract |
passed (typecheck, 187 tests, peer-range smoke; packed artifact and profile lifecycle; 33 pytest checks) |
| clean-container smoke | pnpm smoke:docker |
unverified — no Docker daemon in this environment; the pinned version and its assertion were updated together |
Behavior-change disclosure: the peer range narrows from the 0.1.0-rc.7/0.1.1 union to
>=0.1.5-rc.1. That is machine-enforced by the package manager and disclosed in the PR body, the
package README, and the RFC paragraph that previously recorded the pending pin. No opt-in feature
is added, so disabled-path parity is simply "absent package behaves as before"; the profile smoke
installs and removes the artifact and asserts the profile loses every row on removal.
authority_semantics: the carrier is registered through Connection's authenticated registry, the
package adds no second credential, and the GoalBar names stay narrower than the implementation
(carrier plus contribution, not a new agent lifecycle).
Typed-state rule: retired event types are matched by explicit membership, not by substring
denylist; the residual risk is that a future streaming type would fall through to unsupported
until added, which is visible rather than silent. Error and obligation text remains domain-neutral.
语义与 CI 对齐
semantic_alignment is aligned with candidate decision reuse_existing: the package adopts the
current owner of each moved contract (dsh-client-ui-renderer for the slots seat, dsh-session for
the snapshot reader, dsh-client-connection for the authenticated channel registry) instead of
redefining any of them locally. Six observable surfaces were compared row by row between the
baseline 3ca868193 and the reviewed head; the session-event and pending-input rows are equivalent,
and the peer range, inject owner, channel arity, and retired event type are intentional deltas,
each disclosed. Per the configured review policy for this Goal, CI was not consulted or polled;
repository-native local validation at the reviewed head is the evidence source, and no required
local check failed or was skipped.
我的整体评价
Approving. This is a proportional, well-scoped dependency-line migration: the smallest viable fix
really is "bump the line and follow the four moved APIs", and the mechanism cost is one helper
function and one named set. The three upstream moves make a dual-generation shim impossible — one
dsh.client.inject list cannot order boot rows for two generations — so the peer-range narrowing
is the contract rather than an incidental break. No blocking finding, and no non-blocking finding
worth a follow-up comment.
Residual risk, stated plainly: the container layer is unverified here, and the pre-0.1.5
standalone-channel fallback is now a graceful-degradation path rather than part of the supported
matrix, so it should be deleted in a follow-up once the peer range has been on 0.1.5+ for a
release.
Compare against the baseline honestly: the plugin suite was 187/187 on 0.1.1-rc.2 and is 187/187
at the reviewed head, and the runtime phase only passed before because it ran against a 0.1.1
host. Re-review is needed if the head changes; a rebase restarts this evidence pass.
English verdict: APPROVE - the dsh-side LoopX package now builds on the released 0.1.5 line with peers that admit only >=0.1.5-rc.1, follows each moved host/client contract instead of shimming it, and is validated by all three capability-owned canary phases at exact head ef7efd6, with the clean-container smoke the only unverified layer.
|
Superseded by #4649 after the implementation was reworked against current main. The replacement retains the 0.1.5 upgrade, uses upstream host connection types, removes automatic legacy transport fallback, and verifies the packed client plus real DSH runtime. The explicitly exported legacy registration remains deprecated for existing callers. The replacement has passed typecheck, 187 tests, package/profile/runtime validation and standard premerge. Docker clean-install and owner-mounted browser promotion remain release gates; no npm version has been published. Closing this branch to keep one active implementation. |
Superseded by #4649 after the implementation was reworked against current main. The replacement retains the 0.1.5 upgrade, uses upstream host connection types, removes automatic legacy transport fallback, and verifies the packed client plus real DSH runtime. The explicitly exported legacy registration remains deprecated for existing callers.
The replacement has passed typecheck, 187 tests, package/profile/runtime validation and standard premerge. Docker clean-install and owner-mounted browser promotion remain release gates; no npm version has been published. Closing this branch to keep one active implementation.