Commit Graph
11509 Commits
Author SHA1 Message Date
Huanqi Cao 8ff75622c6 feat(bundle): default Windows hosts to the pwsh shell stack
win32 hosts booting a shipped profile now get pwsh-local as the ctx.bash
executor and tool-pwsh as the shell tool through the base bundle's new
windows.cordis.patch.yml platform layer, injected by the launcher between
the bundle layers and the user layers on win32. bash-sandbox, tool-bash,
permission, and ui-permission are disabled there: the POSIX-only executor
cannot run on Windows, and dsh-permission requires a confining executor.
Overriding the default is a composition decision through the user's
cordis.patch.yml; there is no environment override channel.

apps/cli and dsh-base re-declare dsh-pwsh-local/dsh-tool-pwsh so the
profile module fallback links them for cold starts (the profiles rework
had dropped them from the CLI closure).

Promotes the windows-pwsh-default Agent Note from proposed to implemented
and documents the platform layer in the base bundle README.
2026-08-07 00:37:33 +08:00
Yichen Jiang 8d06b2d576 feat(agent-presets): make the default preset a user setting
`config.default` becomes the composition base of an `agent-presets` settings
namespace, so the user document layers over the deployment's engineering
default and a person can change which preset new sessions get without a
restart.

The value is read per resolution rather than snapshotted: a hot-reloaded
document takes effect on the next session created, and every running session
stays on the preset it was composed from — which is the same rule the
session-header guard enforces from the other side.

`resolve()` read `config.default` directly, which would have made the whole
setting inert; it now goes through `defaultId` like every other caller.

The write-protection test is rewritten against a temp profile root. It was
passing vacuously: the un-overridden Loader REWRITES the composition it read —
stamping `disabled: true` onto the self-disposing row — so the committed
fixture had been mutated by the very run that proved the bug, and every later
run compared against the damaged file and passed. Building the preset in a
temp directory makes the assertion immune to its own failure mode, and it now
fails with a visible `+ disabled: true` when the override is removed.

Review follow-ups on this layer. The exported schema is
`AgentPresetSettingsSchema`, symmetric with the `AgentPresetSettings`
interface it resolves and self-describing at an import site. The `session.create`
JSDoc promised "the deployment's default preset" for an omitted `agentPreset`,
which this layer makes false — it now names the effective default. The
constructor records why it does not use `installSettingsSection`: that helper
re-judges what a consumer DERIVED across attach and detach, and nothing here is
derived. The provider-unload test disposes the fiber `ctx.plugin()` handed back
instead of reaching into `ctx.reflect.store`, and the write-protection wait says
why slack is the right shape for an absence assertion.

The real composition covers the layering too. `apps/cli` boots the shipped
`cordis.yml`, stores `agent-presets.default`, and asserts an unnamed session
composes from it — the package suite proves the layering against a hand-built
context, this proves the roster and the settings provider are wired to each
other. That test also pins the settings row at a temp file: it defaulted to
`$DSH_HOME/settings.yaml`, so a developer's own stored default decided the
outcome of a file whose whole point is that only the shipped root does.

The Agent Note records the per-resolution read and its correspondence with the
session header, and the vacuous-test finding above.
2026-08-07 00:36:54 +08:00
Yichen Jiang 5ed79887fb fix(web): correct the preset-layer contracts review found stale
None of these change behavior; each said something that was not true.

`SessionCwdConflict`'s doc block had been left stranded above the
`AgentPresetConflict` inserted under it, so one class carried a comment
about the other and the second carried none.

The roster comment named a `.system` directory that does not exist; the
shipped root is `config/agent-presets/`, and `system` is the trust its
entries carry.

The real-composition test attributed the disabled `api-gateway` row to
"side effects outside this process" alongside the port and the exporter.
It is disabled for a different reason — the api-proxy cannot mount in
this layer at all — and hiding that behind the same phrase would leave a
later layer unable to tell whether the line can come out.

One test claimed to refuse an adoption while asserting only that the
header records the preset; it now says what it checks.

`PERSONA_SECTION`/`PERSONA_ORDER` existed twice, once in the registry
that declares the slot and once restated in the row that replaces it —
a drift that would land a preset's persona beside the deployment's
instead of shadowing it. The registry exports them now.

The preset conflict message read "already runs agent preset undefined"
for a session that records none, which is the shape a deployment with no
roster produces; it names that case instead, with the regression that
reaches it through the gateway.

Finally, `PresetTree.write()` drops the `loader/config-update` the
inherited method emits — recorded on the override, since a future
edit-while-running flow needs its own persistence path.
2026-08-07 00:35:30 +08:00
Yichen Jiang c58cc23d45 fix(web): address a session's own services from the host
A preset publishes its services behind `isolate` realms, which is what
makes them per session — and what makes them invisible to every host
context. The api-proxy kept reading the root realm, so requests that are
ABOUT a session but arrive from outside it answered for a singleton that
no longer exists: `goal.pause`/`clear` and `skill.list` returned "this
deployment does not mount @deepseek-ai/dsh-goal / dsh-skill" for sessions
whose composition mounts exactly that. Verified against a running host
before and after.

`agentPresets.serviceFor(agent, name)` addresses the instance instead,
reading the same subtree-ownership relation `leakedServices` already
uses, inverted. It is read addressing for a caller holding the agent: a
host row that `inject`s a service cannot use it, because injection
resolves before any session exists — which is why `tools` and
`subagents` stay host-plane and this is not a way around that.

Tool presenters had the same shape and the same cure: `viewFor` looked
definitions up without a scope while the global layer is empty by
design, so every card degraded to the generic renderer. It now takes the
owning agent.

Cold resume through `agentFor()` mounted no preset at all, so every
generic entry point — prompt, models, commands — rebuilt a restarted
session on host tools and the deployment persona. It composes the
recorded preset now, as the other resume path already did.
2026-08-07 00:35:30 +08:00
Yichen Jiang fedb8a2702 fix(session-projection): count registrants sharing one projection key
One unit definition already serves every session — its cells are keyed by
`Session` — but registrants became per-session when agent presets started
mounting tool packages per agent. N sessions on one preset register the
same key N times.

The first registration won and owned the only disposer, so ending one
session stripped `goal`, `todos`, `plan`, `tokenUsage` and
`contextPressure` from every other live session's snapshot. Measured
against the shipped `standard` preset: two concurrent sessions each had
eight projection keys, and disposing the first left the second with three
— the ones host rows register.

Count the registrants instead and remove the key when the last one goes.
A differing `stateVersion` still refuses to share: it is the one
incompatibility a runtime comparison can name, since everything else
about a definition is functions.
2026-08-07 00:35:30 +08:00
Yichen Jiang 523d95a9cf docs(agent-presets): record that a preset file is never written back
The Loader writes a tree back to its source whenever it decides the
config changed, and a row disposing its own fiber is enough to decide
that. The mounted subtree overrides `write()` as a no-op for that
reason — a fact that lived only in a PR description, so nothing in the
repo said why the override exists or what removing it would cost.
2026-08-07 00:35:30 +08:00
Yichen Jiang 25b6381c84 fix(web): compose a forked session, and give the shell realm its provider
Two consequences of moving the agent plane behind presets, both invisible
until the host plane stopped carrying model-facing rows.

`sessions.fork` built its child with a bare `installTarget` and a `meta`
without `agentPreset`. That was harmless while every tool sat in the host
plane — the child inherited them for free. It now comes up with an EMPTY
tool set. The child composes the parent's preset instead, for the same
reason a resumed session keeps its own: the seeded history was produced
under those tools.

`bashEnv` lives in its own `dsh-bash-env` row rather than inside
`tool-bash`, so a preset that isolates the realm must compose the provider
beside its consumer; the host row is disabled here like every other
model-facing one. Nothing outside the agent plane injects `bashEnv`, so it
stays per-session.
2026-08-07 00:35:30 +08:00
Yichen Jiang 3d68185480 feat(web): move the agent plane behind per-session presets
The Web overlay disables base's 32 agent-plane rows and mounts the preset
roster instead, so each session composes its own tools and prompt rather than
sharing one process-wide set. The TUI keeps base unchanged: it is single-session
and composing its agent process-wide is correct there.

`roots` is patched in by AppCLIEntry, like `distIndex`: the shipped presets sit
beside the composition that names them and the user's live under the Harness
home, neither of which a config author chooses.

A session's preset is fixed at creation. Naming a different one for an existing
identity is `agent-preset-conflict` rather than a switch, because that
session's history was produced under the first preset's tools. The guard sits
after `await creation`, beside the cwd check, so it covers every path that
yields a live agent — freshly created, adopted live, resumed, or recovered by
the concurrent-creation catch. A request naming no preset adopts the session as
it is, keeping reconnect and retry ordinary.

Two bugs the real-composition test caught, both invisible to unit tests:

`PresetTree` now refuses to write. The Loader persists a tree whose plugin
self-disposed, and tearing an agent down disposes its whole subtree — inherited,
that rewrote the shipped composition, truncating a 241-line preset to `[]` the
first time a session ended.

`dsh-tool-skill` compared against a lookup of its own name in the global layer,
so it threw inside any preset: `register()` files into the calling context's
scope. It now compares against the definition it registered, which is what the
identity check meant all along.

The `standard` catalog is asserted exactly, not spot-checked: a row that
registers into the wrong layer mounts cleanly and simply contributes nothing, so
an omission is this design's quietest failure. It matches the shipped TUI
catalog plus `glob`/`grep`, the pair that composition documents as
ripgrep-dependent.

Re-records `cordis-inspect-jsdoc`, whose rendered `SessionHeader` gains the
`agentPreset` field. `fs-glob-sampling` fails identically on pristine master
and is untouched here.

The browser e2e scaffold gains the roster fact AppCLIEntry supplies. `roots` is
resolved and patched in by the CLI entry, like `distIndex` on the webserver row,
and this lane boots the shipped tree without that entry — so it has to supply
the same fact or the roster resolves nothing and every session in the lane
composes an agent with no tools, no persona, and no token meter. Only the
shipped root: a developer's own `~/.dsh/.agent-presets` must not decide a golden. The
`cordis:group` builtin comes with it, exactly as `boot()` registers it, because
a preset resolving package names from its own directory cannot reach
`@cordisjs/plugin-group` by name.

The lane stays red through this layer and the next four for the reason stated
above — the api-proxy injects `subagents`, `workspace`, and `tools`, so
`api-gateway` cannot activate and the browser has no `/api` at all. It goes
green again in the layer that returns those registries to the host plane; this
change is what makes that layer's fix sufficient rather than partial.
2026-08-07 00:35:30 +08:00
Yichen Jiang 91b55b9245 feat(web): compose a web session's agent from a named preset
`session.create` takes an optional `agentPreset`, and the host resolves it,
mounts it during pre-publication setup, and records the resolved id on the
session header so a later resume rebuilds the same agent.

Resolution happens BEFORE the session exists, not inside setup: the session
boundary snapshots `meta` before asynchronous setup begins, so an id discovered
during setup could never reach the header. Mounting still happens in setup,
where a failure rolls the whole creation back rather than publishing a session
whose capabilities are half-installed.

Resume ignores whatever the request names and rebuilds from the stored id. A
resumed session's history was produced under that composition; restoring a
different one would replay tool calls the model can no longer make.

`dsh-agent-presets` now throws `UnknownPresetError` / `PresetMountError` so
the host can tell a bad request from a broken preset — they become
`agent-preset-not-found` and `agent-preset-invalid`.

Ships the two built-in compositions (`standard`, `core-web`) and the persona
row that lets them differ in identity. Nothing mounts them yet: no roster is
configured, so `composeAgent` finds no service and every session keeps the host
composition. Wiring the roster and moving base's agent-plane rows behind it is
the next commit, so the switch happens atomically with a real-composition test.
2026-08-07 00:35:30 +08:00
Yichen Jiang 739042a804 feat(persona): make the agent persona a composable row
`dsh-system-prompt` owns the deployment persona as its own config and registers
that section unconditionally, so a process has exactly one. An agent preset
cannot mount the prompt registry itself, which means that without a row of its
own a preset could change an agent's tools but never its identity — and a
roster of presets that all sound the same is not worth having.

The row is scope-only by construction: mounted outside an agent scope it
collides with the registry's own `deployment:persona` registration and fails
loud. That is the correct shape rather than a gap, because the unscoped slot
already has an owner and this row exists to shadow it for one agent.

Two behaviours are pinned by test because both read the other way at a glance:
an empty persona still occupies the slot (shadowing the deployment persona away
entirely, then disappearing at render), and `assemble()` keeps section text
uninterpolated — `renderPrompt()` is the stage that resolves `{{…}}`.
2026-08-07 00:35:30 +08:00
Yichen Jiang e27d38efd6 feat(app-boot): register cordis:group beside cordis:include
A composition that shares one `isolate` realm across rows needs a
`cordis:group` row, and a preset living outside this workspace — the
authored ones under the Harness home — cannot resolve
`@cordisjs/plugin-group` by name: Node's upward `node_modules` walk
never reaches the harness from there. Registering it as a loader builtin
beside `cordis:include` loads both through the ambient module pipeline
instead.

Record it in the preset Agent Note, which leans on the realm vocabulary
without saying where the group row comes from, and drop the preset
README's limitation claiming this builtin is unavailable — it described
the state this change ends.

The test's assertion had a vacuous escape: `provide` mints the root
symbol unconditionally, so the `rootKey === undefined` disjunct could
never hold and the comment claiming the root realm never learned the
name was wrong. Pin both halves — the symbol exists, nothing is stored
under it — and clean up the global the fixture writes.
2026-08-07 00:34:19 +08:00
Yichen Jiang 065257addb fix(agent-presets): bound the mount registry on a host that never reads it
Records are pruned by observation rather than by a disposal hook, for the
reason the module already states: three different owners can tear a
subtree down, and a cleared `uid` is what they share. That leaves the
pruning to whoever reads — and the only production reader is the
invariant companion, whose package is a development composition a
shipped host never loads.

So a live host pruned nothing: every session ever composed left a record
retaining its whole disposed subtree, since the fiber holds its config
and that config is the key its EntryTree is stored under.

Prune on the mount path too. Every session takes it, which bounds the
set at one generation of dead records instead of one per session.
2026-08-07 00:33:40 +08:00
imccyu c98a754ccb fix: address review round four
- the cached-identity rung gains a finality gate: the identity value
  carries its descriptor seq and a cached row is served only when that
  seq lands in the child's own suffix, so a fork seed's replayed
  ancestor identity can never outrank the authoritative refold
  (stateVersion bumped for the state-shape change)
- the cold preparation validates the inspected header against the
  enumerated candidate's lifecycle witness; a republished id degrades to
  that child's corrupt diagnostic instead of leaking the new owner's log
- the new projection registration proves HMR disposal; companion notes
  qualify the superseded decision text and record the deliberate
  error-face asymmetry
2026-08-07 00:05:43 +08:00
imccyu b9886d1615 refactor(apiproxy): read subagent history like session history
subagents.history drops its session-query read: a live child serves its
in-memory events with the registry watermark snapshot, a cold child goes
through inspectServable with a detached fold, and the retired
SESSION_QUERY_* arms disappear while every wire face stays put. The
design note records the shared read source.
2026-08-07 00:05:43 +08:00
imccyu ba791c0ac3 fix: ci 2026-08-07 00:05:43 +08:00
imccyu 397d70d545 fix: address review round three
- the subagent projection's empty state becomes a serializable null
  sentinel (undefined fields vanish in JSON push frames, leaving stale
  identities in clients); consumers treat null and a missing key alike,
  and cached sentinels defer to the authoritative refold
- the tool catalog generator mounts the projection registry for
  list_agents; the shared unavailable-error copy goes catalog-neutral
- the design note's verification section records the new
  subagent-diagnostic snapshot honestly, the 2026-07-22 note's test
  inventory is rewritten to the current mechanism, and the session-store
  error code is documented
2026-08-07 00:05:43 +08:00
imccyu a25d4331d7 feat(subagent): opportunistic projection-cache rung for cold listings
Cold children consult the optional session-projection-cache checkpoint
before paying a preparation recompute: the identity is immutable once
appended, so a cached value is definitive regardless of its watermark.
The cache stays a read-only accelerator (absent service or any rung-two
fault falls through silently; verdicts stay with the authoritative
refold), and the note plus core-data-structures pages describe the
three-rung ladder.
2026-08-07 00:05:42 +08:00
imccyu 6ff4fc0ed0 fix: address review round two
- listChildren contains per-child projection faults on both ladder rungs
  (any registered unit's fold/schema rejection maps to that child's corrupt
  diagnostic) and pins the whole-enumeration listing-failure rethrow
- the base bundle mounts session-projection (web-app's own insert retired
  to avoid the double mount); stale composition comment updated
- the shared projections-unavailable wire face is pinned across
  list/history/prompt; retired session-query arms removed from the catalog
  paths
- the design note records the unknown-parent semantics shift and the
  fold-fault isolation rule
2026-08-07 00:05:42 +08:00
imccyu efd78f44f4 fix: refresh graph docs and retire a dead spec reference
Mounting session-projection in the example compositions staled the
generated composition and module graphs; the 2026-07-22 note now
describes the retired optional-session-query spec without a live path.
2026-08-07 00:05:42 +08:00
imccyu ac1dffb809 test(session-query): pin the persisted-corruption wrapping branch
The retired subagent list path was the only caller exercising
inspectPersisted's corruption arm; cover it directly.
2026-08-07 00:05:42 +08:00
imccyu 96e7c0496a fix: regenerate cordis catalog and allowlist the diagnostic fixture
The subagent API surface change staled the committed catalog artifacts;
the snapshot fixture agent is referenced only from its cordis.snapshot.yml,
so knip learns it as an entry like its siblings.
2026-08-07 00:05:41 +08:00
imccyu 0b0b9e4707 fix: address PR #1802 review round
- listChildren reads the session store via strict ctx.get (property proxy
  is caller-scoped), orders candidates branchlessly, narrows the cold-read
  return type, and pins the cost model and store/registry composition gaps
  with tests; per-file coverage restored
- acp-agent and headless-agent compositions mount session-projection; a
  keyless snapshot pins the descriptor-less diagnostic row
- api-proxy cold spec pins header-origin ownership and the legacy
  descriptor-only opt-out
- design note ships as implemented with its English pairing; companion
  notes and core-data-structures pages synced
2026-08-07 00:05:41 +08:00
imccyu a328fd34d5 feat: subagent list use preparation + projection 2026-08-07 00:05:41 +08:00
Yichen Jiang 6b75bb0425 fix(web): say the API key format is wrong rather than naming the characters 2026-08-06 23:15:08 +08:00
Tianyi Cui 5c7d8e8124 Merge latest master into PWA metadata 2026-08-06 23:09:18 +08:00
fz 949d406086 Merge pull request #1574 from deepseek-harness/codex/fix-resume-baseline-dedup
fix(workspace-context): deduplicate baseline on resume
2026-08-06 23:02:40 +08:00
Tianyi Cui 8f2168303b feat(web): add install metadata 2026-08-06 23:00:01 +08:00
fz bb9e921667 Merge remote-tracking branch 'origin/master' into codex/fix-resume-baseline-dedup 2026-08-06 22:50:31 +08:00
fz d67116eb8c Merge pull request #1353 from deepseek-harness/codex/fix-compact-agents-reinjection
fix(workspace-context): restore AGENTS.md after compaction
2026-08-06 22:49:32 +08:00
Yichen Jiang a89c26b611 test(web): pin the API key field refusal end to end 2026-08-06 22:42:56 +08:00
fz 16beccf423 Merge branch 'codex/fix-compact-agents-reinjection' into codex/fix-resume-baseline-dedup 2026-08-06 22:42:35 +08:00
fz c882c8be8b Merge remote-tracking branch 'origin/master' into codex/fix-compact-agents-reinjection
# Conflicts:
#	docs/architecture.i18n.yaml
2026-08-06 22:41:10 +08:00
Yichen Jiang fe31cd7609 Merge remote-tracking branch 'origin/master' into claude/unified-environment-credentials-c8841a
# Conflicts:
#	apps/cli/src/profile-boot.ts
#	apps/cli/src/web.ts
2026-08-06 22:34:46 +08:00
Yichen Jiang cf9eade39d feat(web): refuse an unusable API key on the field that holds it 2026-08-06 22:30:43 +08:00
Yichen Jiang 665b5697bb docs(llm-pi-ai): document the invalid-credential refusal 2026-08-06 22:13:02 +08:00
Yichen Jiang 45d78c9272 fix(llm-pi-ai): refuse an unusable API key before the header is built 2026-08-06 22:12:50 +08:00
Yichen Jiang fe8a88c96a Merge pull request #1803 from deepseek-harness/worktree/align-core-web-rl-prompt
fix(web): align core profile with RL prompt
2026-08-06 21:57:28 +08:00
pku-xht 5359408d11 Merge pull request #1600 from deepseek-harness/codex/product-providers-pr2-claude-code
Add the Claude Code subagent provider
2026-08-06 21:48:52 +08:00
imccyu e6fd547cd9 Merge pull request #1812 from deepseek-harness/fix/onboarding-step-owned-chrome
fix(ui): move the onboarding takeover chrome into the step
2026-08-06 21:44:41 +08:00
Yichen Jiang 48fd9b70ea docs: drop the notes for changes master now owns
The profile-json entry and the personal composition layer were both
settled on master by its profile restructure — the first removed with
`app-cli-entry.ts`, the second deliberately restored as
`$DSH_HOME/cordis.patch.yml`. Neither is this branch's change any more,
so the notes claiming them go, and the prose they edited returns to
master's.
2026-08-06 21:43:40 +08:00
_Kerman e4256a1684 feat(web): steer the whole queue with an empty-draft Cmd/Ctrl+Enter 2026-08-06 21:40:50 +08:00
Yichen Jiang b1660ab8a4 docs(llm-deepseek): document the invalid-credential refusal 2026-08-06 21:40:06 +08:00
Yichen Jiang 5514dd2bd6 fix(llm-deepseek): refuse an API key no header can carry 2026-08-06 21:39:57 +08:00
ZiyaZhang 7aedc02ae3 docs: regenerate module graph for ui-deliverables 2026-08-06 06:39:08 -07:00
Tianyi Cui 4b1528bcec Merge retargeted parent into Claude Code subagent provider 2026-08-06 21:38:47 +08:00
Yichen Jiang 5e139ac64e Merge remote-tracking branch 'origin/master' into claude/unified-environment-credentials-c8841a
# Conflicts:
#	.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md
#	.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md
#	apps/cli/config/base.cordis.yml
#	apps/cli/package.json
#	apps/cli/reference/README.i18n.yaml
#	apps/cli/reference/README.md
#	apps/cli/reference/README.zh.md
#	apps/cli/src/app-cli-entry.ts
#	apps/cli/src/args.ts
#	apps/cli/src/bin.ts
#	apps/cli/src/config.ts
#	apps/cli/src/dump-config.ts
#	apps/cli/src/headless.ts
#	apps/cli/src/web.ts
#	apps/cli/tests/args.spec.ts
#	apps/cli/tests/built-bin.e2e.ts
#	apps/cli/tests/headless-shutdown.e2e.ts
#	apps/cli/tsconfig.json
#	docs/user/guide/config.i18n.yaml
#	docs/user/guide/config.md
#	docs/user/guide/config.zh.md
#	examples/mcp-memory/README.i18n.yaml
#	examples/mcp-memory/README.md
#	examples/mcp-memory/README.zh.md
#	packages/bundle/web-app/cordis.patch.yml
#	packages/cordis/repository-plugin/README.i18n.yaml
#	packages/cordis/repository-plugin/README.md
#	packages/cordis/repository-plugin/README.zh.md
#	packages/credentials/credentials-local/README.i18n.yaml
#	packages/credentials/credentials-local/README.md
#	packages/credentials/credentials-local/README.zh.md
#	packages/ui/app-boot/README.i18n.yaml
#	packages/ui/app-boot/README.md
#	packages/ui/app-boot/README.zh.md
#	packages/ui/app-boot/src/index.ts
#	packages/ui/app-boot/tests/config-reload.spec.ts
#	packages/ui/app-boot/tests/user-patches.spec.ts
#	pnpm-lock.yaml
2026-08-06 21:34:45 +08:00
Tianyi Cui 0748ebee39 Merge latest master into Codex subagent provider 2026-08-06 21:26:31 +08:00
ZiyaZhang 4767285694 Merge remote-tracking branch 'upstream/master' into feat/web-workspace-file-links
# Conflicts:
#	apps/cli/package.json
#	pnpm-lock.yaml
2026-08-06 06:24:53 -07:00
ZiyaZhang f00a44fd44 refactor(web): move the produced-files row into its own plugin package
ui-conversation now owns only the conversation.chat.turnTail hole; the row,
its derivation, and its copy live in @deepseek-ai/dsh-client-ui-deliverables,
composed in or out by one cordis.yml line.
2026-08-06 06:21:19 -07:00
imccyu 760138ac2c fix: dep 2026-08-06 21:14:59 +08:00