compat: narrow the support matrix to dsh 0.1.2-rc.1+
The install/runtime compatibility floor moves from 0.1.1-rc2+ and 0.1.2-alpha2+ to 0.1.2-rc.1+. The 0.1.1 line and the 0.1.2-alpha previews leave the support matrix, so every pre-0.1.2 fallback they existed for is removed outright: - client history: drop the legacy `connection.api.sessions.history` face, the `result`-nested envelope, and the `events` rows field — the gateway `remote.session.page` face and its ClientResult envelope are the only served shapes. - client image loader: drop the pre-0.1.2 `conversation.resolveImage` face; `uiConversation.imageUrl` only. - client node join: drop the pre-0.1.2 `useSession.nodes` seat; `useChat`'s ChatSnapshot `legacy.nodes` only (UseSessionLike removed). - tests/baselines.ts: single baseline `v0.1.2-rc.1`; HostSeam dropped (the registry generation hard-codes header-passing init and seq-0 eager seeding, now mirrored with the exact `init(header, inheritedEventCount)` arity); ClientSeam slimmed to the consumed probes. - deps: devDeps re-pinned to the 0.1.2-rc.1 generation (types compile against the real rc.1 surfaces); cordis ^4.0.2 and schemastery ^3.18.2 per the rc.1 peer requirements; dsh-scope/dsh-invariants declared as dev deps; peer floors >=0.1.2-rc.1; dshReleases table trimmed. - release workflow fetches only the dsh-v0.1.2-rc.1 tag; docs updated (AGENTS.md, README, docs/compatibility.md). - verified against the rc.1 sources: the tool-attribution map (names and packages, incl. the documented `subagent_fork` alias), MCP naming, the page/imageUrl/legacy.nodes faces, and the registry contract.
This commit is contained in:
1 parent
d5d0fbda85
commit
33068ee1c9
33 files changed
+735
-1288
No files matched your search
@@ -52,9 +52,6 @@ jobs:
|
||||
git init -q
|
||||
git remote add origin https://github.com/deepseek-ai/deepseek-harness.git
|
||||
git fetch --depth 1 -q origin \
|
||||
refs/tags/dsh-v0.1.1-rc.2:refs/tags/dsh-v0.1.1-rc.2 \
|
||||
refs/tags/dsh-v0.1.2-alpha.2:refs/tags/dsh-v0.1.2-alpha.2 \
|
||||
refs/tags/dsh-v0.1.2-alpha.5:refs/tags/dsh-v0.1.2-alpha.5 \
|
||||
refs/tags/dsh-v0.1.2-rc.1:refs/tags/dsh-v0.1.2-rc.1
|
||||
|
||||
- name: Test
|
||||
|
||||
@@ -58,7 +58,7 @@ The plugin lives off data it does not own: the durable session log (event shapes
|
||||
- Consider updating the dependencies to the latest version if possible, as the deepseek-harness is evolving rapidly.
|
||||
|
||||
## Compatibility - Important!
|
||||
- MUST be able to install and work correctly on `@deepseek-ai/dsh` all of **0.1.1-rc2+** and **0.1.2-alpha2+** — no regressions in runtime dependencies, message parsing, or any user-visible behavior. Older lines (0.1.0-rc.x, 0.1.2-alpha.1) are out of the support matrix.
|
||||
- MUST be able to install and work correctly on `@deepseek-ai/dsh` **0.1.2-rc1+** — no regressions in runtime dependencies, message parsing, or any user-visible behavior. Older lines (0.1.0-rc.x, 0.1.1-rc.x, 0.1.2-alpha.x) are out of the support matrix.
|
||||
- Check carefully in depth for the compatibility of the plugin with all supported dsh version, investigate and dive deep into details of dsh source code and its dependencies (run pnpm install in dsh source code folder).
|
||||
- Low-level logic (e.g. token counting) should track the implementation of the newest supported dsh version.
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ Pick **Live (next request)** or any retained step, and browse what that request
|
||||
|
||||

|
||||
|
||||
- **Image payloads render as cards** — thumbnails with name, dimensions, stored size, and the official DeepSeek image-token estimate (dsh 0.1.1+ multimodal pipeline, e.g. `read_image` results and image attachments):
|
||||
- **Image payloads render as cards** — thumbnails with name, dimensions, stored size, and the official DeepSeek image-token estimate (the dsh multimodal pipeline, e.g. `read_image` results and image attachments):
|
||||
|
||||

|
||||
|
||||
@@ -140,7 +140,7 @@ In **Settings → Plugins → Plugin configuration**, the **Context** card holds
|
||||
## Good to know
|
||||
|
||||
- **Estimates vs actuals** — category figures use dsh's own fixed-density heuristic (the same one as its built-in token meter); the pinned trend details and Token/Timing rings show provider-reported actuals next to them.
|
||||
- **Compatibility** — works on `@deepseek-ai/dsh` **0.1.1-rc2+** and **0.1.2-alpha2+**. The per-release matrix and how it is verified: [docs/compatibility.md](docs/compatibility.md).
|
||||
- **Compatibility** — works on `@deepseek-ai/dsh` **0.1.2-rc1+**. The per-release matrix and how it is verified: [docs/compatibility.md](docs/compatibility.md).
|
||||
- **I18n** — UI in English and 简体中文.
|
||||
|
||||
## Like it?
|
||||
|
||||
@@ -2,19 +2,16 @@
|
||||
|
||||
dsh-context declares per-release compatibility with `@deepseek-ai/dsh` in its package manifest (`dsh.compatibility.dshReleases`). This page records what is actually verified for each declared release, and how.
|
||||
|
||||
Last verified: **2026-09-03** (plugin `dsh-context@0.41.2` source tree).
|
||||
Last verified: **2026-09-04** (plugin `dsh-context@0.41.3` source tree).
|
||||
|
||||
## Supported dsh releases
|
||||
|
||||
| dsh release | npm channel | Declared | Automated seam matrix | Disposable-profile install / uninstall |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `0.1.1-rc.2` | `latest` (stable) | compatible | ✅ baseline `v0.1.1-rc.2` | ✅ install OK → 1 composed row → uninstall OK → 0 rows |
|
||||
| `0.1.2-alpha.2` | — | compatible | ✅ baseline `v0.1.2-alpha.2` | not run (covered by the seam matrix at its tag) |
|
||||
| `0.1.2-alpha.3` | — | compatible | same generation, bracketed by the alpha.2 / alpha.5 baselines | ✅ install OK → 1 composed row → uninstall OK → 0 rows |
|
||||
| `0.1.2-alpha.4` | — | compatible | same generation, bracketed by the alpha.2 / alpha.5 baselines | ✅ install OK → 1 composed row → uninstall OK → 0 rows |
|
||||
| `0.1.2-alpha.5` | `alpha` (preview) | compatible | ✅ baseline `v0.1.2-alpha.5` | ✅ install OK → 1 composed row → uninstall OK → 0 rows |
|
||||
| `0.1.2-rc.1` | `next` | compatible | ✅ baseline `v0.1.2-rc.1` | ✅ install OK → 1 composed row → uninstall OK → 0 rows |
|
||||
|
||||
Releases older than `0.1.2-rc.1` — the `0.1.1` line and the `0.1.2-alpha.*` previews — were supported and verified through `dsh-context@0.41.x` and are no longer in the support matrix.
|
||||
|
||||
## What each check means
|
||||
|
||||
- **Automated seam matrix** — part of this repository's `pnpm test` (the `compat` vitest project). For every baseline tag it stages the harness's REAL sources at that tag, boots the plugin's built host entry into that tag's actual `SessionProjectionRegistry` on the cordis release the line vendors, and probes the tag's client seams (slots, finalized-nodes seat, image loader, history face/envelope, markdown chrome, platform module table, durable-event vocabulary, settings namespace). Definitions live in `tests/baselines.ts`; the release workflow fetches the pinned baseline tags before testing.
|
||||
|
||||
+13
-16
@@ -62,11 +62,6 @@
|
||||
},
|
||||
"compatibility": {
|
||||
"dshReleases": {
|
||||
"0.1.1-rc.2": "compatible",
|
||||
"0.1.2-alpha.2": "compatible",
|
||||
"0.1.2-alpha.3": "compatible",
|
||||
"0.1.2-alpha.4": "compatible",
|
||||
"0.1.2-alpha.5": "compatible",
|
||||
"0.1.2-rc.1": "compatible"
|
||||
}
|
||||
}
|
||||
@@ -85,11 +80,11 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.1-rc.2",
|
||||
"@deepseek-ai/dsh-session": ">=0.1.1-rc.2",
|
||||
"@deepseek-ai/dsh-settings": ">=0.1.1-rc.2",
|
||||
"@deepseek-ai/schemastery": "^3.18.1",
|
||||
"@deepseek-ai/cordis": "^4.0.2",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-session": ">=0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-settings": ">=0.1.2-rc.1",
|
||||
"@deepseek-ai/schemastery": "^3.18.2",
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -101,12 +96,14 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.2",
|
||||
"@deepseek-ai/dsh-session": "0.1.2-alpha.2",
|
||||
"@deepseek-ai/dsh-session-projection": "0.1.2-alpha.2",
|
||||
"@deepseek-ai/dsh-settings": "0.1.2-alpha.2",
|
||||
"@deepseek-ai/schemastery": "^3.18.1",
|
||||
"@deepseek-ai/cordis": "^4.0.2",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-scope": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-settings": "0.1.2-rc.1",
|
||||
"@deepseek-ai/schemastery": "^3.18.2",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^18.3.31",
|
||||
|
||||
Generated
+183
-384
@@ -13,23 +13,29 @@ importers:
|
||||
version: 4.4.3
|
||||
devDependencies:
|
||||
'@deepseek-ai/cordis':
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2
|
||||
'@deepseek-ai/dsh-client-ui-primitives':
|
||||
specifier: 0.1.2-alpha.2
|
||||
version: 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)
|
||||
specifier: 0.1.2-rc.1
|
||||
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/dsh-invariants':
|
||||
specifier: 0.1.2-rc.1
|
||||
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/dsh-scope':
|
||||
specifier: 0.1.2-rc.1
|
||||
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: 0.1.2-alpha.2
|
||||
version: 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))
|
||||
specifier: 0.1.2-rc.1
|
||||
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
|
||||
'@deepseek-ai/dsh-session-projection':
|
||||
specifier: 0.1.2-alpha.2
|
||||
version: 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-session@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))
|
||||
specifier: 0.1.2-rc.1
|
||||
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))
|
||||
'@deepseek-ai/dsh-settings':
|
||||
specifier: 0.1.2-alpha.2
|
||||
version: 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-session@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/schemastery@3.18.1)
|
||||
specifier: 0.1.2-rc.1
|
||||
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.2)
|
||||
'@deepseek-ai/schemastery':
|
||||
specifier: ^3.18.1
|
||||
version: 3.18.1
|
||||
specifier: ^3.18.2
|
||||
version: 3.18.2
|
||||
'@stylistic/eslint-plugin':
|
||||
specifier: ^5.10.0
|
||||
version: 5.10.0(eslint@10.8.1)
|
||||
@@ -150,99 +156,87 @@ packages:
|
||||
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@deepseek-ai/cordis@4.0.1':
|
||||
resolution: {integrity: sha512-YBdskTU2Po1kru3GgcUWUbkTsPMA9LkSQDAY8rBkFJeajdgcQad3QPJZE26JyK99Xb6HaASvoXg2DSUTeN/0Nw==}
|
||||
'@deepseek-ai/cordis@4.0.2':
|
||||
resolution: {integrity: sha512-asOnXP1TzFSFQlHb1iegDZp0z/8WD1c7YNrwJR/Tx2bzNuMXfcekE/I67Iv6SQXeLB4csxqCngzQKANP7gdw0g==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis-plugin-include': ^1.0.6
|
||||
'@deepseek-ai/cordis-plugin-loader': ^1.0.2
|
||||
'@deepseek-ai/cordis-plugin-include': ^1.0.7
|
||||
'@deepseek-ai/cordis-plugin-loader': ^1.0.3
|
||||
peerDependenciesMeta:
|
||||
'@deepseek-ai/cordis-plugin-include':
|
||||
optional: true
|
||||
'@deepseek-ai/cordis-plugin-loader':
|
||||
optional: true
|
||||
|
||||
'@deepseek-ai/cosmokit@1.8.2':
|
||||
resolution: {integrity: sha512-muBOKtSrUKU5m/xpq8ZXWL6hQ/jgd4PhU2PqH97bcxIiLEJfNwZOGQEx4t/aS/GgxRAR+ra9pMHPMtTHU4sqqA==}
|
||||
|
||||
'@deepseek-ai/cosmokit@1.8.3':
|
||||
resolution: {integrity: sha512-qBo+ronVM6Eu2WNVJXi8JcMiqZ19T9BRIpV+5qJUFPXjGH/Z0QKcQMC/IZJ7L394YTOtJgcovbk9qP0w2GsBXQ==}
|
||||
|
||||
'@deepseek-ai/dsh-brand@0.1.2-alpha.2':
|
||||
resolution: {integrity: sha512-sWFbShCe8LNLuqD3gdQeneUGxFtZ/rywe6Tmi2eteBDubecHnf04XurtF38Epno42bnNTMYWuKwt1cihcbHL3A==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-invariants': ^0.1.2-alpha.2
|
||||
|
||||
'@deepseek-ai/dsh-client-ui-primitives@0.1.2-alpha.2':
|
||||
resolution: {integrity: sha512-OKOfYgM4KxACMJEprT0ThIDTJetWpYp5qNqULwJmc/4CY+sTQmFQ2tJjOXLwNpkCrryVJF2YZet/6j1JNp2h5w==}
|
||||
'@deepseek-ai/dsh-brand@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-y2RHIag3kzLdY8kTBuSUSTsKzWPscJrysNObXTiNM7stXLpa3jXpshsB1caqGzNx50vYcmHeN1gFVECgx3NZHg==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
|
||||
'@deepseek-ai/dsh-invariants@0.1.0-rc.6':
|
||||
resolution: {integrity: sha512-WfEfOi99a4cpOugRAHTBSTnesLieu3ist1q9PXDXFBHX++K1rAl9+sB7YrdnbB8LH0UOY532gS9xJUYU6w0SLw==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.1
|
||||
|
||||
'@deepseek-ai/dsh-llm@0.1.2-alpha.2':
|
||||
resolution: {integrity: sha512-ip6yMxwHugxQm4VCbwX/FDnlTeeBM9VBkIn0+74ityQy7Z3yKREJ1Ov8Z04l4G3duRzeGRsQ4ztOFZ01oNfKIw==}
|
||||
'@deepseek-ai/dsh-client-ui-primitives@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-4gZS+tXAHbSxAjP91+lENpxKb49XVKcO80m7ATNNgD6lJHOwbuWVGv9IwEnmQ5ebZvI8Yrp8C1J+jDe1pB9jXw==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
|
||||
'@deepseek-ai/dsh-scope@0.1.0-rc.6':
|
||||
resolution: {integrity: sha512-UlDLV4syLoJinNg9imhXrSAHrdaTa5Ff8gg46rzjFJGPUOhAk3DZff0hryT5OhrBi0A5Tj92qVpg2pRVvxnUzQ==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.1
|
||||
'@deepseek-ai/dsh-invariants': ^0.1.0-rc.6
|
||||
|
||||
'@deepseek-ai/dsh-session-projection@0.1.2-alpha.2':
|
||||
resolution: {integrity: sha512-E5NYRAOQwiYYu56WSHTlnI2MkyqznA4ePlOdOQvpF97a7ZG2BkRfflwvQ5NHIPv8FABemoSCGMiI3/0GSfqvBg==}
|
||||
'@deepseek-ai/dsh-invariants@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-zSjuFJYcEB94kPwn7SicqAV1dfVWZkjt1I8luDboxtjfp5CN6ziwJhLv3KaB+Dh9tgkyGY4mvo886W/HhE9L9g==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-invariants': ^0.1.2-alpha.2
|
||||
'@deepseek-ai/dsh-session': ^0.1.2-alpha.2
|
||||
|
||||
'@deepseek-ai/dsh-session@0.1.2-alpha.2':
|
||||
resolution: {integrity: sha512-RfikXscYTDXDr7CD7C/8oGJZaH8Egclj7pmXRtd90QcB5L8RIQ7069xrHZjds8OjNrFo69qQwNK3gYLUVZy9PA==}
|
||||
'@deepseek-ai/dsh-llm@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-7VYsha5AXsVLnsAwYJffWXz9bwUbElw8i5N8tlTSdai9Bupk3sMbsotzPf8ZbsuGAxQYErahMwQwgGEu4qZO6g==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-scope': ^0.1.2-alpha.2
|
||||
|
||||
'@deepseek-ai/dsh-settings@0.1.2-alpha.2':
|
||||
resolution: {integrity: sha512-rWPH/LfDU9SCFaonqfxCMjnD4gkO31DlQVHL9LIy7wH/FVqnBJ6v13YN70Fpof2KKPyL2YhcwOOPQbusxlzSDw==}
|
||||
'@deepseek-ai/dsh-scope@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-GHwbzWrGbmE0ZGzAZsXBudpyCM9ENCpXxlKODIaPFbIWhRePh6nH2jW47TAOqCIsPPedWC2p3w8Tx5WAa3I/2A==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-brand': ^0.1.2-alpha.2
|
||||
'@deepseek-ai/dsh-invariants': ^0.1.2-alpha.2
|
||||
'@deepseek-ai/dsh-session': ^0.1.2-alpha.2
|
||||
'@deepseek-ai/dsh-invariants': ^0.1.2-rc.1
|
||||
|
||||
'@deepseek-ai/dsh-session-projection@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-qVWlIzPm4u9vzQ5G/QtE6jeSJ3b3b42hWVJj664BurEZlHbAfdqkhYc1QsoBU+5k5kUEJFHh07z8HxuXA9kTnQ==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-session': ^0.1.2-rc.1
|
||||
|
||||
'@deepseek-ai/dsh-session@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-jRGNPTbQcvIx1F2MVmmkoiHLgpC3Btqo1wkl/3JDLlOWRVWKOuaC+5fQiMrFPOYfno6YiX/c2UvmA0td8qB92w==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-scope': ^0.1.2-rc.1
|
||||
|
||||
'@deepseek-ai/dsh-settings@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-wMmJ2w5S6I7hgmgPppuAVemltEcTmAa8gvii7fk4T2KGVCuBYgig8xPwQ8Lp2ukLZSzzE6ZVCQxhdYZyJX/USA==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-brand': ^0.1.2-rc.1
|
||||
'@deepseek-ai/dsh-invariants': ^0.1.2-rc.1
|
||||
'@deepseek-ai/dsh-session': ^0.1.2-rc.1
|
||||
'@deepseek-ai/schemastery': ^3.18.2
|
||||
|
||||
'@deepseek-ai/dsh-timeout@0.1.2-alpha.2':
|
||||
resolution: {integrity: sha512-8q5cd55aMoOvrPaqSws/3xiyzHhs1bfjdtAs4YHWimQgMd+yMrDnlu8i+zFOkWoSc0A2wPkXcCYR8xogl4gerA==}
|
||||
'@deepseek-ai/dsh-timeout@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-llz11yxWeJB8suKCa6hRLjNBWVaPn3Fbd0XoBS0bkZLhPFzO1mDorx/XMca2GhI7XF9A86CURQcDt+aGjjvqJA==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-invariants': ^0.1.2-alpha.2
|
||||
|
||||
'@deepseek-ai/dsh-typert-protocol@0.1.2-alpha.2':
|
||||
resolution: {integrity: sha512-U3j/usWHRllaJMdTr4GuVkkUciHWyq01mvMtWeGT+RY2hb+ysTxeuLEiV0Lh5EyVScgFdDLbe/GtTZ2OkGsGwQ==}
|
||||
'@deepseek-ai/dsh-typert-protocol@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-vBWD0NpriPd96ltbWaAw+iKeWFIEjiAcCWYtmqEcV1Ms+FaV02usjjm7LCmhSQhMZpBSBc7EZUX0spGlpjI1bA==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-invariants': ^0.1.2-alpha.2
|
||||
|
||||
'@deepseek-ai/dsh-util-crypto@0.1.2-alpha.2':
|
||||
resolution: {integrity: sha512-03WVzlgLErJRRGMqaMdx5ufEoN3Scpdsnd0f8vXMhcbeEzKZ61lbI8z/m6ZttM/LhpzPALuFtDc5u+N5LFP8Fg==}
|
||||
'@deepseek-ai/dsh-util-crypto@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-gE8FznQ1hkknw9QMrHNSlm3UEyRYuj3MLVvlJ6NyH1kmOsdjnbrVbSs+Pn72k2oIV4Fiyu3Umg0sLznUEcsiXw==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-invariants': ^0.1.2-alpha.2
|
||||
|
||||
'@deepseek-ai/dsh-util-values@0.1.2-alpha.2':
|
||||
resolution: {integrity: sha512-ZyZHTqGQ/8S5ZYflTkuiZGPxhkuHa9Uy5G+teEmnmnkhiN/UVPGQvI//CXRNmxpTKjfx+qcVVmr5x6bohplsQg==}
|
||||
'@deepseek-ai/dsh-util-values@0.1.2-rc.1':
|
||||
resolution: {integrity: sha512-FQdgoK+KYVR0pBQb9fTsajvGb3gzUxEzJQEYnFOt+Qdwc4nkxJOPwJJdIvXuqvZaQ33DaOoqpnpcr2p5HpLu4g==}
|
||||
peerDependencies:
|
||||
'@deepseek-ai/cordis': ^4.0.2
|
||||
'@deepseek-ai/dsh-invariants': ^0.1.2-alpha.2
|
||||
|
||||
'@deepseek-ai/schemastery@3.18.1':
|
||||
resolution: {integrity: sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg==}
|
||||
|
||||
'@deepseek-ai/schemastery@3.18.2':
|
||||
resolution: {integrity: sha512-njDtZsznjYxok7KLLlHOPyuv2efdWVbSflAHgztSfbMsg+CVraEoRe2DjOCgClYv3ZCSm7WXoaUkbB/+RY7tWQ==}
|
||||
@@ -316,11 +310,8 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@oxc-project/types@0.146.0':
|
||||
resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==}
|
||||
|
||||
'@oxc-project/types@0.147.0':
|
||||
resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==}
|
||||
'@oxc-project/types@0.148.0':
|
||||
resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==}
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@7.0.2001':
|
||||
resolution: {integrity: sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==}
|
||||
@@ -477,194 +468,98 @@ packages:
|
||||
'@quansync/fs@1.0.0':
|
||||
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
|
||||
|
||||
'@rolldown/binding-android-arm-eabi@1.2.5':
|
||||
resolution: {integrity: sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==}
|
||||
'@rolldown/binding-android-arm-eabi@1.2.7':
|
||||
resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-android-arm-eabi@1.2.6':
|
||||
resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-android-arm64@1.2.5':
|
||||
resolution: {integrity: sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==}
|
||||
'@rolldown/binding-android-arm64@1.2.7':
|
||||
resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-android-arm64@1.2.6':
|
||||
resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.2.5':
|
||||
resolution: {integrity: sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==}
|
||||
'@rolldown/binding-darwin-arm64@1.2.7':
|
||||
resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.2.6':
|
||||
resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.2.5':
|
||||
resolution: {integrity: sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==}
|
||||
'@rolldown/binding-darwin-x64@1.2.7':
|
||||
resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.2.6':
|
||||
resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.2.5':
|
||||
resolution: {integrity: sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==}
|
||||
'@rolldown/binding-freebsd-x64@1.2.7':
|
||||
resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.2.6':
|
||||
resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.5':
|
||||
resolution: {integrity: sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==}
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.7':
|
||||
resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.6':
|
||||
resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.2.5':
|
||||
resolution: {integrity: sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==}
|
||||
'@rolldown/binding-linux-arm64-gnu@1.2.7':
|
||||
resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.2.6':
|
||||
resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.2.5':
|
||||
resolution: {integrity: sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==}
|
||||
'@rolldown/binding-linux-arm64-musl@1.2.7':
|
||||
resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.2.6':
|
||||
resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.2.5':
|
||||
resolution: {integrity: sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==}
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.2.7':
|
||||
resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.2.6':
|
||||
resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.2.5':
|
||||
resolution: {integrity: sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==}
|
||||
'@rolldown/binding-linux-s390x-gnu@1.2.7':
|
||||
resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.2.6':
|
||||
resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.2.5':
|
||||
resolution: {integrity: sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==}
|
||||
'@rolldown/binding-linux-x64-gnu@1.2.7':
|
||||
resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.2.6':
|
||||
resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.2.5':
|
||||
resolution: {integrity: sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==}
|
||||
'@rolldown/binding-linux-x64-musl@1.2.7':
|
||||
resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.2.6':
|
||||
resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.2.5':
|
||||
resolution: {integrity: sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==}
|
||||
'@rolldown/binding-openharmony-arm64@1.2.7':
|
||||
resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.2.6':
|
||||
resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.2.5':
|
||||
resolution: {integrity: sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==}
|
||||
'@rolldown/binding-win32-arm64-msvc@1.2.7':
|
||||
resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.2.6':
|
||||
resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.2.5':
|
||||
resolution: {integrity: sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.2.6':
|
||||
resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==}
|
||||
'@rolldown/binding-win32-x64-msvc@1.2.7':
|
||||
resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1734,10 +1629,6 @@ packages:
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@4.0.5:
|
||||
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
picomatch@4.0.7:
|
||||
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1817,13 +1708,8 @@ packages:
|
||||
vue-tsc:
|
||||
optional: true
|
||||
|
||||
rolldown@1.2.5:
|
||||
resolution: {integrity: sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
rolldown@1.2.6:
|
||||
resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==}
|
||||
rolldown@1.2.7:
|
||||
resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
@@ -2222,23 +2108,20 @@ snapshots:
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0': {}
|
||||
|
||||
'@deepseek-ai/cordis@4.0.1':
|
||||
'@deepseek-ai/cordis@4.0.2':
|
||||
dependencies:
|
||||
'@deepseek-ai/cosmokit': 1.8.2
|
||||
'@deepseek-ai/cosmokit': 1.8.3
|
||||
'@standard-schema/spec': 1.1.0
|
||||
|
||||
'@deepseek-ai/cosmokit@1.8.2': {}
|
||||
|
||||
'@deepseek-ai/cosmokit@1.8.3': {}
|
||||
|
||||
'@deepseek-ai/dsh-brand@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))':
|
||||
'@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
|
||||
'@deepseek-ai/dsh-client-ui-primitives@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)':
|
||||
'@deepseek-ai/dsh-client-ui-primitives@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
'@shikijs/langs': 4.4.3
|
||||
'@types/mdast': 4.0.4
|
||||
anser: 2.3.5
|
||||
@@ -2262,79 +2145,65 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)':
|
||||
'@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/schemastery': 3.18.1
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
'@deepseek-ai/schemastery': 3.18.2
|
||||
|
||||
'@deepseek-ai/dsh-llm@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))':
|
||||
'@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/dsh-brand': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
'@deepseek-ai/dsh-timeout': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
'@deepseek-ai/dsh-typert-protocol': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
'@deepseek-ai/dsh-util-crypto': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
'@deepseek-ai/dsh-util-values': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
'@deepseek-ai/dsh-brand': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/dsh-timeout': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/dsh-typert-protocol': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/dsh-util-crypto': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/dsh-util-values': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/schemastery': 3.18.2
|
||||
zod: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- '@deepseek-ai/dsh-invariants'
|
||||
|
||||
'@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))':
|
||||
'@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
'@deepseek-ai/dsh-invariants': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
|
||||
'@deepseek-ai/dsh-session-projection@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-session@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))':
|
||||
'@deepseek-ai/dsh-session-projection@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)
|
||||
'@deepseek-ai/dsh-session': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
'@deepseek-ai/dsh-session': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
|
||||
zod: 4.4.3
|
||||
|
||||
'@deepseek-ai/dsh-session@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))':
|
||||
'@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/dsh-brand': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
'@deepseek-ai/dsh-llm': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
'@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
'@deepseek-ai/dsh-util-values': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
transitivePeerDependencies:
|
||||
- '@deepseek-ai/dsh-invariants'
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
'@deepseek-ai/dsh-brand': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/dsh-llm': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/dsh-scope': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
|
||||
'@deepseek-ai/dsh-util-values': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
|
||||
'@deepseek-ai/dsh-settings@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-session@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/schemastery@3.18.1)':
|
||||
'@deepseek-ai/dsh-settings@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.2)':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/dsh-brand': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
'@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)
|
||||
'@deepseek-ai/dsh-session': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))
|
||||
'@deepseek-ai/dsh-util-values': 0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))
|
||||
'@deepseek-ai/schemastery': 3.18.1
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
'@deepseek-ai/dsh-brand': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/dsh-invariants': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/dsh-session': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
|
||||
'@deepseek-ai/dsh-util-values': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
|
||||
'@deepseek-ai/schemastery': 3.18.2
|
||||
|
||||
'@deepseek-ai/dsh-timeout@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))':
|
||||
'@deepseek-ai/dsh-timeout@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
|
||||
'@deepseek-ai/dsh-typert-protocol@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))':
|
||||
'@deepseek-ai/dsh-typert-protocol@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
|
||||
'@deepseek-ai/dsh-util-crypto@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))':
|
||||
'@deepseek-ai/dsh-util-crypto@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
|
||||
'@deepseek-ai/dsh-util-values@0.1.2-alpha.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))':
|
||||
'@deepseek-ai/dsh-util-values@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
|
||||
dependencies:
|
||||
'@deepseek-ai/cordis': 4.0.1
|
||||
'@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)
|
||||
|
||||
'@deepseek-ai/schemastery@3.18.1':
|
||||
dependencies:
|
||||
'@deepseek-ai/cosmokit': 1.8.2
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@deepseek-ai/cordis': 4.0.2
|
||||
|
||||
'@deepseek-ai/schemastery@3.18.2':
|
||||
dependencies:
|
||||
@@ -2398,9 +2267,7 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@oxc-project/types@0.146.0': {}
|
||||
|
||||
'@oxc-project/types@0.147.0': {}
|
||||
'@oxc-project/types@0.148.0': {}
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@7.0.2001':
|
||||
optional: true
|
||||
@@ -2481,94 +2348,49 @@ snapshots:
|
||||
dependencies:
|
||||
quansync: 1.0.0
|
||||
|
||||
'@rolldown/binding-android-arm-eabi@1.2.5':
|
||||
'@rolldown/binding-android-arm-eabi@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-android-arm-eabi@1.2.6':
|
||||
'@rolldown/binding-android-arm64@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-android-arm64@1.2.5':
|
||||
'@rolldown/binding-darwin-arm64@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-android-arm64@1.2.6':
|
||||
'@rolldown/binding-darwin-x64@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.2.5':
|
||||
'@rolldown/binding-freebsd-x64@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.2.6':
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.2.5':
|
||||
'@rolldown/binding-linux-arm64-gnu@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.2.6':
|
||||
'@rolldown/binding-linux-arm64-musl@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.2.5':
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.2.6':
|
||||
'@rolldown/binding-linux-s390x-gnu@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.5':
|
||||
'@rolldown/binding-linux-x64-gnu@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.6':
|
||||
'@rolldown/binding-linux-x64-musl@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.2.5':
|
||||
'@rolldown/binding-openharmony-arm64@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.2.6':
|
||||
'@rolldown/binding-win32-arm64-msvc@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.2.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.2.6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.2.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.2.6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.2.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.2.6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.2.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.2.6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.2.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.2.6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.2.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.2.6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.2.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.2.6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.2.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.2.6':
|
||||
'@rolldown/binding-win32-x64-msvc@1.2.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.1': {}
|
||||
@@ -2623,7 +2445,7 @@ snapshots:
|
||||
eslint-visitor-keys: 4.2.1
|
||||
espree: 10.4.0
|
||||
estraverse: 5.3.0
|
||||
picomatch: 4.0.5
|
||||
picomatch: 4.0.7
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
dependencies:
|
||||
@@ -3078,9 +2900,9 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.5):
|
||||
fdir@6.5.0(picomatch@4.0.7):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.5
|
||||
picomatch: 4.0.7
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
dependencies:
|
||||
@@ -3714,8 +3536,6 @@ snapshots:
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.5: {}
|
||||
|
||||
picomatch@4.0.7: {}
|
||||
|
||||
playwright-core@1.62.1: {}
|
||||
@@ -3767,12 +3587,12 @@ snapshots:
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
rolldown-plugin-dts@0.27.14(rolldown@1.2.5)(typescript@7.0.2):
|
||||
rolldown-plugin-dts@0.27.14(rolldown@1.2.7)(typescript@7.0.2):
|
||||
dependencies:
|
||||
dts-resolver: 3.0.0
|
||||
get-tsconfig: 5.0.0-beta.5
|
||||
obug: 2.1.4
|
||||
rolldown: 1.2.5
|
||||
rolldown: 1.2.7
|
||||
yuku-ast: 0.8.7
|
||||
yuku-codegen: 0.8.7
|
||||
yuku-parser: 0.8.7
|
||||
@@ -3781,47 +3601,26 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- oxc-resolver
|
||||
|
||||
rolldown@1.2.5:
|
||||
rolldown@1.2.7:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.146.0
|
||||
'@oxc-project/types': 0.148.0
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.5
|
||||
'@rolldown/binding-android-arm64': 1.2.5
|
||||
'@rolldown/binding-darwin-arm64': 1.2.5
|
||||
'@rolldown/binding-darwin-x64': 1.2.5
|
||||
'@rolldown/binding-freebsd-x64': 1.2.5
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.5
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.5
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.5
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.5
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.5
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.5
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.5
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.5
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.5
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.5
|
||||
|
||||
rolldown@1.2.6:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.147.0
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.6
|
||||
'@rolldown/binding-android-arm64': 1.2.6
|
||||
'@rolldown/binding-darwin-arm64': 1.2.6
|
||||
'@rolldown/binding-darwin-x64': 1.2.6
|
||||
'@rolldown/binding-freebsd-x64': 1.2.6
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.6
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.6
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.6
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.6
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.6
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.6
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.6
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.6
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.6
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.6
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.7
|
||||
'@rolldown/binding-android-arm64': 1.2.7
|
||||
'@rolldown/binding-darwin-arm64': 1.2.7
|
||||
'@rolldown/binding-darwin-x64': 1.2.7
|
||||
'@rolldown/binding-freebsd-x64': 1.2.7
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.7
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.7
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.7
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.7
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.7
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.7
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.7
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.7
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.7
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.7
|
||||
|
||||
saxes@6.0.0:
|
||||
dependencies:
|
||||
@@ -3883,8 +3682,8 @@ snapshots:
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.5)
|
||||
picomatch: 4.0.5
|
||||
fdir: 6.5.0(picomatch@4.0.7)
|
||||
picomatch: 4.0.7
|
||||
|
||||
tinyrainbow@3.1.1: {}
|
||||
|
||||
@@ -3919,9 +3718,9 @@ snapshots:
|
||||
hookable: 6.1.1
|
||||
import-without-cache: 0.4.0
|
||||
obug: 2.1.4
|
||||
picomatch: 4.0.5
|
||||
rolldown: 1.2.5
|
||||
rolldown-plugin-dts: 0.27.14(rolldown@1.2.5)(typescript@7.0.2)
|
||||
picomatch: 4.0.7
|
||||
rolldown: 1.2.7
|
||||
rolldown-plugin-dts: 0.27.14(rolldown@1.2.7)(typescript@7.0.2)
|
||||
tinyexec: 1.3.0
|
||||
tinyglobby: 0.2.17
|
||||
tree-kill: 1.2.2
|
||||
@@ -4022,7 +3821,7 @@ snapshots:
|
||||
lightningcss: 1.33.0
|
||||
picomatch: 4.0.7
|
||||
postcss: 8.5.26
|
||||
rolldown: 1.2.6
|
||||
rolldown: 1.2.7
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 26.2.0
|
||||
@@ -4043,7 +3842,7 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.4
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.5
|
||||
picomatch: 4.0.7
|
||||
std-env: 4.2.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.3.0
|
||||
|
||||
+12
-10
@@ -8,15 +8,17 @@ allowBuilds:
|
||||
esbuild: true
|
||||
|
||||
minimumReleaseAgeExclude:
|
||||
- '@deepseek-ai/dsh-client-ui-primitives@0.1.1-rc.2 || 0.1.2-alpha.2'
|
||||
- '@deepseek-ai/dsh-session@0.1.1-rc.2 || 0.1.2-alpha.2'
|
||||
- '@deepseek-ai/dsh-session-projection@0.1.1-rc.2 || 0.1.2-alpha.2'
|
||||
- '@deepseek-ai/dsh-settings@0.1.1-rc.2 || 0.1.2-alpha.2'
|
||||
- '@deepseek-ai/dsh-client-ui-primitives@0.1.2-rc.1'
|
||||
- '@deepseek-ai/dsh-invariants@0.1.2-rc.1'
|
||||
- '@deepseek-ai/dsh-scope@0.1.2-rc.1'
|
||||
- '@deepseek-ai/dsh-session@0.1.2-rc.1'
|
||||
- '@deepseek-ai/dsh-session-projection@0.1.2-rc.1'
|
||||
- '@deepseek-ai/dsh-settings@0.1.2-rc.1'
|
||||
- '@deepseek-ai/cosmokit@1.8.3'
|
||||
- '@deepseek-ai/dsh-brand@0.1.2-alpha.2'
|
||||
- '@deepseek-ai/dsh-llm@0.1.2-alpha.2'
|
||||
- '@deepseek-ai/dsh-timeout@0.1.2-alpha.2'
|
||||
- '@deepseek-ai/dsh-typert-protocol@0.1.2-alpha.2'
|
||||
- '@deepseek-ai/dsh-util-crypto@0.1.2-alpha.2'
|
||||
- '@deepseek-ai/dsh-util-values@0.1.2-alpha.2'
|
||||
- '@deepseek-ai/dsh-brand@0.1.2-rc.1'
|
||||
- '@deepseek-ai/dsh-llm@0.1.2-rc.1'
|
||||
- '@deepseek-ai/dsh-timeout@0.1.2-rc.1'
|
||||
- '@deepseek-ai/dsh-typert-protocol@0.1.2-rc.1'
|
||||
- '@deepseek-ai/dsh-util-crypto@0.1.2-rc.1'
|
||||
- '@deepseek-ai/dsh-util-values@0.1.2-rc.1'
|
||||
- '@deepseek-ai/schemastery@3.18.2'
|
||||
@@ -18,11 +18,10 @@ const COMMAND = 'context'
|
||||
const LINE = '/' + COMMAND
|
||||
|
||||
export function registerContextCommand(ctx: ClientCtx, kit: ViewKit): void {
|
||||
// Wait for the SERVICE, not for module arrival order: dsh 0.1.2 composes
|
||||
// Wait for the SERVICE, not for module arrival order: dsh composes
|
||||
// the client from finer modules, so `inputTriggers` may not be provided
|
||||
// yet when this plugin applies (the 0.1.1 monolith made a ctx.get at
|
||||
// apply time reliable). A harness without the service never fires the
|
||||
// callback — the tab keeps working and only the command is absent.
|
||||
// yet when this plugin applies. A harness without the service never fires
|
||||
// the callback — the tab keeps working and only the command is absent.
|
||||
ctx.inject(['inputTriggers'], (ictx) => {
|
||||
const inputTriggers = (ictx as ClientCtx).get('inputTriggers') as InputTriggersFace | undefined
|
||||
if (inputTriggers === undefined || typeof inputTriggers.registerSource !== 'function') return
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
* The assistant-message action that jumps to the Context tab at this reply's
|
||||
* turn. Registered on the harness `conversation.chat.assistant-actions` seat
|
||||
* (the icon row beside copy/branch), it receives the finalized reply's durable
|
||||
* message id, resolves the matching assistant node's seq off whichever node
|
||||
* seat the running harness serves (`useChat` on 0.1.2+, the session snapshot
|
||||
* before it), records it in the viewFocus relay, and activates the Context
|
||||
* message id, resolves the matching assistant node's seq off the `useChat`
|
||||
* node seat, records it in the viewFocus relay, and activates the Context
|
||||
* tab — where the jump pins the reply's TURN (see contextView's leg 2). An
|
||||
* unresolvable seq still switches tabs, just without a pin; a message id that
|
||||
* is not a plain string renders nothing at all.
|
||||
@@ -12,7 +11,7 @@
|
||||
|
||||
import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type * as ReactNS from 'react'
|
||||
import type { ConversationNodeLike, UseChatLike, UseSessionLike } from '../services'
|
||||
import type { ConversationNodeLike, UseChatLike } from '../services'
|
||||
import { conversationNodesOf } from '../services'
|
||||
import { React } from '../react'
|
||||
import { activateContextTab, requestContextFocus } from '../viewFocus'
|
||||
@@ -23,7 +22,6 @@ export interface ContextJumpProps {
|
||||
messageId?: unknown
|
||||
sessionId?: unknown
|
||||
useChat?: UseChatLike
|
||||
useSession?: UseSessionLike
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,12 +59,12 @@ export function makeContextModal(ctx: ClientCtx, kit: ViewKit): (props: ContextM
|
||||
// Same targeted content fetch the Context tab wires (one seq-anchored history read per expanded row), plus the on-demand header
|
||||
// epoch content read for the browser's system/tools sections.
|
||||
const fetchContent = React.useMemo(
|
||||
() => (sessionId !== '' ? makeContentFetcher(ctx, sessionId) : undefined),
|
||||
[ctx, sessionId],
|
||||
() => (sessionId !== '' ? makeContentFetcher(sessionId) : undefined),
|
||||
[sessionId],
|
||||
)
|
||||
const fetchHeader = React.useMemo(
|
||||
() => (sessionId !== '' ? makeHeaderFetcher(ctx, sessionId) : undefined),
|
||||
[ctx, sessionId],
|
||||
() => (sessionId !== '' ? makeHeaderFetcher(sessionId) : undefined),
|
||||
[sessionId],
|
||||
)
|
||||
|
||||
const close = React.useCallback(() => {
|
||||
@@ -72,7 +72,7 @@ export function makeContextModal(ctx: ClientCtx, kit: ViewKit): (props: ContextM
|
||||
modalStoreOf(sessionId).set(false)
|
||||
// Consume the `/context` token now (it stayed in the composer while the modal was open) via the scoped input event — a stale guard
|
||||
// (the user typed meanwhile) fails soft inside the shell and leaves the draft untouched. The sessions service is read at CLOSE time:
|
||||
// capturing it at apply would race the finer 0.1.2 module composition (`ctx.get` is the inject-free reflect read — undefined, never a
|
||||
// capturing it at apply would race the finer module composition (`ctx.get` is the inject-free reflect read — undefined, never a
|
||||
// throw, when the service is not composed).
|
||||
const guard = takePendingConsume(sessionId)
|
||||
const sessions = ctx.get('sessions') as SessionsFace | undefined
|
||||
|
||||
@@ -118,9 +118,8 @@ export function makeContextView(
|
||||
const [nodeFocus, setNodeFocus] = React.useState<{ step: number | 'live'; seq: number; cat: SurfaceNode['cat'] } | null>(null)
|
||||
const clearNodeFocus = React.useCallback(() => { setNodeFocus(null) }, [])
|
||||
|
||||
// Session-authorized durable-image loader for the browser's attachment cards, resolved through the harness conversation service —
|
||||
// whichever face the running harness serves (`resolveImage` pre-0.1.2, `imageUrl` since); absent service/session degrades the cards
|
||||
// to metadata-only, never an error.
|
||||
// Session-authorized durable-image loader for the browser's attachment cards, resolved through the harness `uiConversation` service
|
||||
// (`imageUrl`); absent service/session degrades the cards to metadata-only, never an error.
|
||||
const loadImage = React.useMemo(
|
||||
() => imageLoaderOf(ctx, typeof sessionId === 'string' ? sessionId : undefined),
|
||||
[ctx, sessionId],
|
||||
@@ -129,14 +128,14 @@ export function makeContextView(
|
||||
// Targeted full-content fetch for browser nodes outside the conversation window (one seq-anchored history read per expanded row);
|
||||
// absent face/session degrades to the static hint — never an error.
|
||||
const fetchContent = React.useMemo(
|
||||
() => (typeof sessionId === 'string' && sessionId !== '' ? makeContentFetcher(ctx, sessionId) : undefined),
|
||||
[ctx, sessionId],
|
||||
() => (typeof sessionId === 'string' && sessionId !== '' ? makeContentFetcher(sessionId) : undefined),
|
||||
[sessionId],
|
||||
)
|
||||
// On-demand header epoch content (system prompt text, tool schemas) for the Context browser — one seq-anchored history read per
|
||||
// opened epoch; the `contextHeaders` projection carries metadata only. Same degradation shape as fetchContent.
|
||||
const fetchHeader = React.useMemo(
|
||||
() => (typeof sessionId === 'string' && sessionId !== '' ? makeHeaderFetcher(ctx, sessionId) : undefined),
|
||||
[ctx, sessionId],
|
||||
() => (typeof sessionId === 'string' && sessionId !== '' ? makeHeaderFetcher(sessionId) : undefined),
|
||||
[sessionId],
|
||||
)
|
||||
|
||||
const rootRef = React.useRef<HTMLDivElement | null>(null)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* dsh's multimodal pipeline stores only a normalized reference per image (never inline bytes); the block carries everything the card needs
|
||||
* except the display URL, which comes from the harness conversation service's `resolveImage` (the same loader the chat history rides on),
|
||||
* except the display URL, which comes from the harness `uiConversation` service's `imageUrl` (the same loader the chat history rides on),
|
||||
* handed down as `load`; absent loader or failed load degrades to the metadata row alone — the card never throws.
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface ImageKit {
|
||||
|
||||
/**
|
||||
* Narrow an unknown content block to a durable image ref: accepts both raw message blocks (`{ type: 'image', attachment }`) and the
|
||||
* snapshot's assistant blocks (`{ kind: 'image', attachment }`); everything else null. Lenient on the optional facts — resolveImage reads
|
||||
* snapshot's assistant blocks (`{ kind: 'image', attachment }`); everything else null. Lenient on the optional facts — imageUrl reads
|
||||
* only attachmentId.
|
||||
*/
|
||||
export function imageRefOf(block: unknown): ImageRefLike | null {
|
||||
|
||||
@@ -12,9 +12,9 @@ import type { ViewKit } from '../viewkit'
|
||||
export type RichMode = 'raw' | 'md'
|
||||
|
||||
/**
|
||||
* The Markdown chrome dsh 0.1.2-alpha serves as a required `labels` prop
|
||||
* (renamed from 0.1.1's optional `codeLabels`). Typed locally so the plugin
|
||||
* typechecks against either face; 0.1.1-era renderers ignore the extra key.
|
||||
* The Markdown chrome the harness primitives serve as a required `labels`
|
||||
* prop. Typed locally so the plugin typechecks without a primitives
|
||||
* dependency.
|
||||
*/
|
||||
interface MarkdownChrome {
|
||||
code: { copyLabel: string; copiedLabel: string }
|
||||
|
||||
+75
-124
@@ -4,39 +4,37 @@
|
||||
* conversation window, ONE seq-anchored history read returns the page
|
||||
* containing that event: the host cuts pages on whole append-origin message
|
||||
* boundaries, so the newest group on the page covers `seq` whenever the
|
||||
* durable log still holds it. The read rides whichever face the running
|
||||
* harness serves — the 0.1.2+ gateway remotes (`remote.session.page`, with
|
||||
* the inclusive cut `throughSeq` pinned to the target seq) or the pre-0.1.2
|
||||
* api client verb (`sessions.history({ beforeSeq })`) — and the raw events
|
||||
* map into the same conversation-node shapes the window join delivers (a
|
||||
* thin display subset of dsh's own fold). Fetched nodes cache per session —
|
||||
* history is immutable, so a seq never needs fetching twice.
|
||||
* durable log still holds it. The read rides the harness gateway remotes
|
||||
* (`remote.session.page`, with the inclusive cut `throughSeq` pinned to the
|
||||
* target seq) and the raw events map into the same conversation-node shapes
|
||||
* the window join delivers (a thin display subset of dsh's own fold).
|
||||
* Fetched nodes cache per session — history is immutable, so a seq never
|
||||
* needs fetching twice.
|
||||
*
|
||||
* The remote face is resolved through the DECLARED inject
|
||||
* (`watchHistoryFaces`): this plugin's module inject lists only slots/
|
||||
* locale, and a top-level `remote.session` would stall the whole plugin on
|
||||
* pre-0.1.2 hosts that never mount the namespace — while NONDECLARED reads
|
||||
* of the traced service proxy throw "cannot get property … without inject"
|
||||
* and can take a view down. The injection callback runs under a fiber that
|
||||
* declares both `remote` and `remote.session` (the dsh idiom), so the
|
||||
* property path resolves; the guarded reflect reads beneath it degrade to
|
||||
* the legacy face or to no face instead of ever throwing.
|
||||
* locale, and NONDECLARED reads of the traced service proxy throw "cannot
|
||||
* get property … without inject" and can take a view down. The injection
|
||||
* callback runs under a fiber that declares both `remote` and
|
||||
* `remote.session` (the dsh idiom), so the property path resolves there and
|
||||
* nowhere else; the resolved face is re-proved and a hostile read leaves
|
||||
* the slot unset instead of ever throwing.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ClientCtx, ContentFetcher, ConversationNodeLike, HeaderFetcher,
|
||||
HistoryEntryLike, SessionPageFace, SessionsHistoryFace,
|
||||
HistoryEntryLike, SessionPageFace,
|
||||
} from './services'
|
||||
import type { HeaderEpochContent } from '../shared/types'
|
||||
|
||||
/** Narrow one served row to a validated durable event envelope, or null. */
|
||||
function eventOf(entry: unknown): { type: string; seq: number; data: Record<string, unknown> } | null {
|
||||
if (entry === null || typeof entry !== 'object') return null
|
||||
// Both eras wrap the envelope in an `event` field (0.1.1 history entries
|
||||
// `{event, view?}`, 0.1.2 records `{type:'event'|'chunks', event}`); a bare
|
||||
// envelope passes too, so shape drift across versions degrades instead of
|
||||
// breaking the page. Packed chunk-row records flow through as unknown types
|
||||
// and project to nothing downstream — only final events matter here.
|
||||
// History records wrap the envelope in an `event` field
|
||||
// (`{type:'event'|'chunks', event}`); a bare envelope passes too, so shape
|
||||
// drift degrades instead of breaking the page. Packed chunk-row records
|
||||
// flow through as unknown types and project to nothing downstream — only
|
||||
// final events matter here.
|
||||
const inner = (entry as HistoryEntryLike).event ?? entry
|
||||
if (typeof inner !== 'object') return null
|
||||
const e = inner as { type?: unknown; seq?: unknown; data?: unknown }
|
||||
@@ -163,51 +161,21 @@ export function pageNodesOf(entries: readonly unknown[]): Map<number, Conversati
|
||||
return nodes
|
||||
}
|
||||
|
||||
/** Read a service off the client context without letting an absent/foreign
|
||||
* cordis service (a throwing `ctx.get`) escape: returns undefined instead. */
|
||||
function serviceOf(ctx: ClientCtx, name: string): unknown {
|
||||
/** The `page` verb of a history face, re-proved and bound to its owner.
|
||||
* Hostile objects (any accessor backed by host state can throw) degrade to
|
||||
* undefined instead of escaping the read. */
|
||||
function readPageOf(face: unknown): SessionPageFace['page'] | undefined {
|
||||
if (face === null || typeof face !== 'object') return undefined
|
||||
try {
|
||||
return ctx.get(name)
|
||||
const fn = (face as Record<string, unknown>).page
|
||||
return typeof fn === 'function' ? (fn as SessionPageFace['page']).bind(face) : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a (possibly traced/proxied) service value down a plain-property path,
|
||||
* degrading at the FIRST throw. cordis's undeclared-service proxies are one
|
||||
* hostile object class — any accessor backed by host state can throw too —
|
||||
* and the no-white-screen contract needs the whole chain guarded, not just
|
||||
* the `ctx.get` call.
|
||||
*/
|
||||
function readKeysOf(value: unknown, ...keys: string[]): unknown {
|
||||
let current = value
|
||||
for (const key of keys) {
|
||||
if (current === null || typeof current !== 'object') return undefined
|
||||
try {
|
||||
current = (current as Record<string, unknown>)[key]
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
/** The `page` verb of a history face, re-proved and bound to its owner. */
|
||||
function readPageOf(face: unknown): SessionPageFace['page'] | undefined {
|
||||
const fn = readKeysOf(face, 'page')
|
||||
return typeof fn === 'function' ? (fn as SessionPageFace['page']).bind(face) : undefined
|
||||
}
|
||||
|
||||
/** The `history` verb of the legacy api client face, re-proved and bound. */
|
||||
function readHistoryOf(face: unknown): SessionsHistoryFace['history'] | undefined {
|
||||
const sessions = readKeysOf(face, 'api', 'sessions')
|
||||
const fn = readKeysOf(sessions, 'history')
|
||||
return typeof fn === 'function' ? (fn as SessionsHistoryFace['history']).bind(sessions) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* The 0.1.2+ gateway history page verb, resolved through the DECLARED inject
|
||||
* The gateway history page verb, resolved through the DECLARED inject
|
||||
* (see {@link watchHistoryFaces}) and bound up front: a method extracted
|
||||
* unbound loses `this`, and the traced `remote` proxy that hands it out
|
||||
* requires the inject to resolve at all.
|
||||
@@ -215,39 +183,35 @@ function readHistoryOf(face: unknown): SessionsHistoryFace['history'] | undefine
|
||||
let declaredPage: SessionPageFace['page'] | undefined
|
||||
|
||||
/**
|
||||
* Register the plugin's 0.1.2+ history faces with the harness through the
|
||||
* DECLARED inject — both `remote` AND `remote.session` (the ui-chat idiom)
|
||||
* must be in one fiber's requirement list, because the traced `remote`
|
||||
* proxy resolves `.session` through the context and each name needs the
|
||||
* other's declaration. Pre-0.1.2 hosts never provide either name, so the
|
||||
* callback simply never fires and the legacy `connection` face carries the
|
||||
* reads. The callback re-runs on every unload/remount, so it owns the
|
||||
* slot's lifetime. The face itself is re-proven (a never-fired or hostile
|
||||
* invocation leaves the slot unset — nothing here can throw).
|
||||
* Register the plugin's history face with the harness through the DECLARED
|
||||
* inject — both `remote` AND `remote.session` (the ui-chat idiom) must be in
|
||||
* one fiber's requirement list, because the traced `remote` proxy resolves
|
||||
* `.session` through the context and each name needs the other's
|
||||
* declaration; an undeclared read of the traced proxy throws instead of
|
||||
* resolving. The callback re-runs on every unload/remount, so it owns the
|
||||
* slot's lifetime. The face itself is re-proven: a never-fired invocation
|
||||
* or a hostile property read leaves the slot unset — nothing here can throw.
|
||||
*/
|
||||
export function watchHistoryFaces(ctx: ClientCtx): void {
|
||||
ctx.inject(['remote', 'remote.session'], (c) => {
|
||||
const session = (c as ClientCtx & { remote?: { session?: SessionPageFace } }).remote?.session
|
||||
const page = session !== undefined ? readPageOf(session) : undefined
|
||||
declaredPage = page
|
||||
try {
|
||||
const session = (c as ClientCtx & { remote?: { session?: SessionPageFace } }).remote?.session
|
||||
declaredPage = session !== undefined ? readPageOf(session) : undefined
|
||||
} catch {
|
||||
declaredPage = undefined
|
||||
}
|
||||
return () => {
|
||||
declaredPage = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** The rows array of a history/page response, under every served envelope. */
|
||||
/** The rows array of a successful history page, under the served envelope. */
|
||||
function rowsOf(response: unknown): readonly unknown[] {
|
||||
// Envelope unwrapping, both served shapes: the 0.1.2 remote resolves to
|
||||
// the ClientResult itself ({ok, value}); the 0.1.1 api client nests it
|
||||
// under `result` ({result: {ok, value}}). A bare {records}/{events}
|
||||
// payload passes too. Anything else rejects so the caller can offer a
|
||||
// retry instead of claiming absence.
|
||||
// Envelope unwrapping: the remote resolves to the ClientResult itself
|
||||
// ({ok, value}). A bare {records} payload passes too. Anything else
|
||||
// rejects so the caller can offer a retry instead of claiming absence.
|
||||
let payload: unknown = response
|
||||
if (payload !== null && typeof payload === 'object') {
|
||||
const nested = (payload as { result?: unknown }).result
|
||||
if (nested !== null && typeof nested === 'object') payload = nested
|
||||
}
|
||||
if (payload !== null && typeof payload === 'object' && 'ok' in payload) {
|
||||
const r = payload as { ok?: unknown; value?: unknown }
|
||||
if (r.ok !== true || r.value === null || typeof r.value !== 'object') {
|
||||
@@ -256,58 +220,45 @@ function rowsOf(response: unknown): readonly unknown[] {
|
||||
payload = r.value
|
||||
}
|
||||
if (payload === null || typeof payload !== 'object') throw new Error('history rpc failed')
|
||||
const rows = (payload as { records?: unknown; events?: unknown }).records
|
||||
?? (payload as { events?: unknown }).events
|
||||
const rows = (payload as { records?: unknown }).records
|
||||
if (!Array.isArray(rows)) throw new Error('history rpc failed')
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* The session's history readers over whichever face the running harness
|
||||
* serves — the 0.1.2+ gateway remotes (`remote.session.page`) first, the
|
||||
* pre-0.1.2 api client verb (`connection.api.sessions.history`) beneath it.
|
||||
* Both cut message-aligned pages, so the returned read covers `seq` whenever
|
||||
* the durable log still holds it: the newer face pins the inclusive cut to
|
||||
* the seq itself, the legacy reader uses the exclusive bound one past it —
|
||||
* and because a non-declared read of the traced remote proxy throws
|
||||
* ("cannot get property … without inject"), the page face prefers the
|
||||
* injection-resolved slot and only then tries the reflect read. Every
|
||||
* service property access degrades at its own guard instead of taking the
|
||||
* view down. Undefined when no face exists (older hosts) — callers keep
|
||||
* their static degradation.
|
||||
* The session's history reader over the gateway remotes (`remote.session.page`),
|
||||
* from the declared-inject slot. The page cuts message-aligned pages, so the
|
||||
* returned read covers `seq` whenever the durable log still holds it: the
|
||||
* inclusive cut is pinned to the seq itself, the exclusive bound one past
|
||||
* it. Undefined when the slot holds no face (the inject never fired or
|
||||
* served no usable page verb) — callers keep their static degradation.
|
||||
*/
|
||||
function pageReadersOf(ctx: ClientCtx, sessionId: string): ((seq: number) => Promise<unknown>) | undefined {
|
||||
// The declared-inject face wins; the reflect read beneath it is the
|
||||
// bounded second chance on hosts where the inject callback never fired.
|
||||
const page = declaredPage ?? readPageOf(serviceOf(ctx, 'remote.session'))
|
||||
const history = readHistoryOf(serviceOf(ctx, 'connection'))
|
||||
if (page === undefined && history === undefined) return undefined
|
||||
function pageReaderOf(sessionId: string): ((seq: number) => Promise<unknown>) | undefined {
|
||||
// The face resolved at build time: absent face = the caller's static
|
||||
// degradation, a resolved face stays bound for the fetcher's lifetime.
|
||||
const page = declaredPage
|
||||
if (page === undefined) return undefined
|
||||
return (seq) => {
|
||||
if (page !== undefined) {
|
||||
// The inclusive log cut pinned to the target seq, the exclusive bound
|
||||
// one past it — the newer face's exact-pinning of the legacy page.
|
||||
return page({
|
||||
address: { kind: 'session' as const, sessionId },
|
||||
throughSeq: seq,
|
||||
beforeSeq: seq + 1,
|
||||
}, new AbortController().signal)
|
||||
}
|
||||
return (history as (request: { sessionId: string; beforeSeq: number }) => Promise<unknown>)({ sessionId, beforeSeq: seq + 1 })
|
||||
// The inclusive log cut pinned to the target seq, the exclusive bound
|
||||
// one past it.
|
||||
return page({
|
||||
address: { kind: 'session' as const, sessionId },
|
||||
throughSeq: seq,
|
||||
beforeSeq: seq + 1,
|
||||
}, new AbortController().signal)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the browser's per-session fetcher over whichever history face the
|
||||
* running harness serves — the 0.1.2+ gateway remotes (`remote.session.page`)
|
||||
* first, the pre-0.1.2 api client verb (`connection.api.sessions.history`)
|
||||
* beneath it. Both cut message-aligned pages, so `beforeSeq: seq + 1` (with
|
||||
* the inclusive cut pinned to `seq` on the newer face) covers the seq whenever
|
||||
* the durable log still holds it. Undefined when no face exists (older
|
||||
* hosts) — the caller keeps its static preview-plus-hint degradation. Found
|
||||
* nodes cache in the closure: one mount re-reading a row never re-fetches.
|
||||
* Build the browser's per-session fetcher over the gateway history face
|
||||
* (`remote.session.page`): message-aligned pages whose inclusive cut pinned
|
||||
* to `seq` (exclusive bound one past it) cover the seq whenever the durable
|
||||
* log still holds it. Undefined when no face was resolved at build time —
|
||||
* the caller keeps its static preview-plus-hint degradation. Found nodes
|
||||
* cache in the closure: one mount re-reading a row never re-fetches.
|
||||
*/
|
||||
export function makeContentFetcher(ctx: ClientCtx, sessionId: string): ContentFetcher | undefined {
|
||||
const read = pageReadersOf(ctx, sessionId)
|
||||
export function makeContentFetcher(sessionId: string): ContentFetcher | undefined {
|
||||
const read = pageReaderOf(sessionId)
|
||||
if (read === undefined) return undefined
|
||||
// Fetched nodes cache in the closure: history is immutable, so one mount
|
||||
// re-reading a row never re-fetches.
|
||||
@@ -361,11 +312,11 @@ function headerContentOf(data: Record<string, unknown>): HeaderEpochContent | nu
|
||||
* mapped client-side into the renderable content. Epochs cache per session
|
||||
* (history is immutable), and OLDER epochs sharing the page cache for free —
|
||||
* stepping back through epochs walks the same pages. Undefined when no
|
||||
* history face exists (older hosts) — the browser keeps a metadata-only
|
||||
* degradation instead.
|
||||
* history face exists — the browser keeps a metadata-only degradation
|
||||
* instead.
|
||||
*/
|
||||
export function makeHeaderFetcher(ctx: ClientCtx, sessionId: string): HeaderFetcher | undefined {
|
||||
const read = pageReadersOf(ctx, sessionId)
|
||||
export function makeHeaderFetcher(sessionId: string): HeaderFetcher | undefined {
|
||||
const read = pageReaderOf(sessionId)
|
||||
if (read === undefined) return undefined
|
||||
const cache = new Map<number, HeaderEpochContent>()
|
||||
return async (seq) => {
|
||||
|
||||
+4
-4
@@ -68,12 +68,12 @@ function apply(ctx: ClientCtx): void {
|
||||
const t = ctx.locale.bind(NS)
|
||||
|
||||
const kit = makeViewKit(t)
|
||||
// History faces of the 0.1.2+ gateway remotes, resolved through the
|
||||
// History face of the harness gateway remotes, resolved through the
|
||||
// DECLARED inject — a non-declared read of the traced `remote.session`
|
||||
// proxy throws ("cannot get property … without inject") and would take
|
||||
// the browser down with the view. Injection waits for the service and
|
||||
// never fires on hosts that lack the namespace (the pre-0.1.2 legacy
|
||||
// `connection` face carries those reads instead).
|
||||
// the browser down with the view. Injection waits for the service; a
|
||||
// harness that never composes the namespace never fires the callback and
|
||||
// the targeted fetches simply stay absent.
|
||||
watchHistoryFaces(ctx)
|
||||
const settings = createContextSettings()
|
||||
const ContextView = makeContextView(ctx, kit, settings)
|
||||
|
||||
+34
-89
@@ -3,7 +3,7 @@
|
||||
* from the harness web half.
|
||||
*
|
||||
* The plugin bundles its own code but relies on the reader to deliver the
|
||||
* framework standard kit to slot components (`sessionId`, `useSession`,
|
||||
* framework standard kit to slot components (`sessionId`, `useChat`,
|
||||
* `useProjection`, `t` …); only the small faces below are referenced across
|
||||
* modules. These are TYPE-ONLY: the runtime services come from the user's
|
||||
* harness. This plugin no longer calls any RPC — data arrives as pushed
|
||||
@@ -95,25 +95,19 @@ export type ImageLoader = (attachment: ImageRefLike) => Promise<string>
|
||||
|
||||
/**
|
||||
* The harness conversation client service, minimally typed for image
|
||||
* resolution — the same call the chat view's own message images ride on.
|
||||
* Renamed across dsh versions: `ctx.conversation.resolveImage` before
|
||||
* 0.1.2, `ctx.uiConversation.imageUrl` since. Whichever face is present
|
||||
* serves the loader; absence degrades the cards to metadata-only.
|
||||
* resolution — the same call the chat view's own message images ride on
|
||||
* (`ctx.uiConversation.imageUrl`).
|
||||
*/
|
||||
export interface ConversationFace {
|
||||
resolveImage?(sessionId: string, attachment: ImageRefLike): Promise<string>
|
||||
}
|
||||
|
||||
export interface UiConversationFace {
|
||||
imageUrl?(sessionId: string, attachment: ImageRefLike): Promise<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* A session-authorized durable-image loader over whichever conversation
|
||||
* face the running harness provides (`resolveImage` pre-0.1.2, `imageUrl`
|
||||
* since), or undefined when neither service is composed — the caller
|
||||
* degrades to metadata-only cards. Hostile snapshots and throwing service
|
||||
* reads are caught: this helper can never take a render down.
|
||||
* A session-authorized durable-image loader over the harness conversation
|
||||
* face (`uiConversation.imageUrl`), or undefined when the service is not
|
||||
* composed — the caller degrades to metadata-only cards. Hostile snapshots
|
||||
* and throwing service reads are caught: this helper can never take a
|
||||
* render down.
|
||||
*/
|
||||
export function imageLoaderOf(
|
||||
ctx: ClientCtx,
|
||||
@@ -121,73 +115,42 @@ export function imageLoaderOf(
|
||||
): ImageLoader | undefined {
|
||||
if (typeof sessionId !== 'string' || sessionId === '') return undefined
|
||||
try {
|
||||
const legacy = ctx.get('conversation') as ConversationFace | undefined
|
||||
if (legacy !== undefined && typeof legacy.resolveImage === 'function') {
|
||||
const resolveImage = legacy.resolveImage.bind(legacy)
|
||||
return attachment => resolveImage(sessionId, attachment)
|
||||
}
|
||||
const modern = ctx.get('uiConversation') as UiConversationFace | undefined
|
||||
if (modern !== undefined && typeof modern.imageUrl === 'function') {
|
||||
const imageUrl = modern.imageUrl.bind(modern)
|
||||
const conversation = ctx.get('uiConversation') as UiConversationFace | undefined
|
||||
if (conversation !== undefined && typeof conversation.imageUrl === 'function') {
|
||||
const imageUrl = conversation.imageUrl.bind(conversation)
|
||||
return attachment => imageUrl(sessionId, attachment)
|
||||
}
|
||||
} catch { /* absent or hostile service — metadata-only cards */ }
|
||||
return undefined
|
||||
}
|
||||
|
||||
export type UseSessionLike = <T>(
|
||||
selector: (snapshot: {
|
||||
nodes?: readonly ConversationNodeLike[]
|
||||
}) => T,
|
||||
) => T
|
||||
|
||||
/**
|
||||
* The `useChat` standard seat (dsh 0.1.2+: the finalized chat nodes moved
|
||||
* from the session snapshot to a per-view `ChatSnapshot` whose `legacy`
|
||||
* slice keeps the plain `ConversationNode[]`). Minimally typed: the selector
|
||||
* receives the harness snapshot (untrusted — re-proved outside), and the
|
||||
* slice it returns must be reference-stable so the framework's
|
||||
* selector-hook equality can gate re-renders.
|
||||
* The `useChat` standard seat (the finalized chat nodes live on a per-view
|
||||
* `ChatSnapshot` whose `legacy` slice keeps the plain `ConversationNode[]`).
|
||||
* Minimally typed: the selector receives the harness snapshot (untrusted —
|
||||
* re-proved outside), and the slice it returns must be reference-stable so
|
||||
* the framework's selector-hook equality can gate re-renders.
|
||||
*/
|
||||
export type UseChatLike = <T>(selector: (snapshot: unknown) => T) => T
|
||||
|
||||
/**
|
||||
* The conversation-window nodes this plugin joins on, from whichever seat
|
||||
* the running harness provides — `useChat` (`ChatSnapshot.legacy.nodes`) on
|
||||
* dsh 0.1.2+, the session snapshot's own `nodes` before that. Returns
|
||||
* undefined when neither seat delivers a real array (absent seat, older or
|
||||
* foreign harness, hostile snapshot) — callers render without the join,
|
||||
* never an error.
|
||||
*
|
||||
* Hook-order contract: the seats are real React hooks, so BOTH are invoked
|
||||
* on every call whenever present (the chat seat first, the session seat
|
||||
* second) and only the RESULT is picked conditionally — a stable call order
|
||||
* across renders. Selectors return stable slice references so the
|
||||
* framework's snapshot equality can gate re-renders.
|
||||
* The conversation-window nodes this plugin joins on, from the `useChat`
|
||||
* seat (`ChatSnapshot.legacy.nodes`). Returns undefined when the seat does
|
||||
* not deliver a real array (absent seat, foreign harness, hostile snapshot)
|
||||
* — callers render without the join, never an error.
|
||||
*/
|
||||
export function conversationNodesOf(props: {
|
||||
useChat?: UseChatLike
|
||||
useSession?: UseSessionLike
|
||||
}): readonly ConversationNodeLike[] | undefined {
|
||||
const useChat: unknown = props.useChat
|
||||
let chatNodes: unknown
|
||||
if (typeof useChat === 'function') {
|
||||
try {
|
||||
// `s.legacy` is a stable object; the array is read outside the selector.
|
||||
const slice = (useChat as UseChatLike)((s: unknown) =>
|
||||
s !== null && typeof s === 'object' ? (s as { legacy?: unknown }).legacy : undefined)
|
||||
chatNodes = slice !== null && typeof slice === 'object' ? (slice as { nodes?: unknown }).nodes : undefined
|
||||
} catch { /* hostile seat — the session snapshot still answers */ }
|
||||
}
|
||||
const useSession: unknown = props.useSession
|
||||
let sessionNodes: unknown
|
||||
if (typeof useSession === 'function') {
|
||||
try {
|
||||
sessionNodes = (useSession as UseSessionLike)(s => s.nodes)
|
||||
} catch { /* hostile seat — the join degrades to nothing */ }
|
||||
}
|
||||
if (Array.isArray(chatNodes)) return chatNodes as readonly ConversationNodeLike[]
|
||||
if (Array.isArray(sessionNodes)) return sessionNodes as readonly ConversationNodeLike[]
|
||||
if (typeof useChat !== 'function') return undefined
|
||||
try {
|
||||
// `s.legacy` is a stable object; the array is read outside the selector.
|
||||
const slice = (useChat as UseChatLike)((s: unknown) =>
|
||||
s !== null && typeof s === 'object' ? (s as { legacy?: unknown }).legacy : undefined)
|
||||
const nodes = slice !== null && typeof slice === 'object' ? (slice as { nodes?: unknown }).nodes : undefined
|
||||
return Array.isArray(nodes) ? nodes as readonly ConversationNodeLike[] : undefined
|
||||
} catch { /* hostile seat — the join degrades to nothing */ }
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -200,8 +163,7 @@ export function conversationNodesOf(props: {
|
||||
export interface SessionStandardProps {
|
||||
sessionId?: string
|
||||
useProjection?: (key: string) => unknown
|
||||
useSession?: UseSessionLike
|
||||
/** The chat-view snapshot seat (dsh 0.1.2+; see {@link UseChatLike}). */
|
||||
/** The chat-view snapshot seat (see {@link UseChatLike}). */
|
||||
useChat?: UseChatLike
|
||||
}
|
||||
|
||||
@@ -523,31 +485,15 @@ export interface HistoryEntryLike {
|
||||
}
|
||||
|
||||
/**
|
||||
* The sessions domain of the shared api client (`connection.api.sessions`),
|
||||
* narrowed to the one verb the targeted content fetch rides on: a
|
||||
* seq-anchored history page whose boundaries align to whole append-origin
|
||||
* messages, so `beforeSeq: seq + 1` always covers that seq when the log
|
||||
* still holds it. This is the dsh <= 0.1.1 face — the RPC was rewritten in
|
||||
* 0.1.2 into `remote.session.page` (see {@link SessionPageFace}). Every
|
||||
* response field is re-proven at runtime.
|
||||
*/
|
||||
export interface SessionsHistoryFace {
|
||||
history(request: { sessionId: string; beforeSeq: number }): Promise<{
|
||||
result?: { ok?: unknown; value?: { events?: unknown; records?: unknown } | null } | null
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* The seq-anchored history page verb of the dsh 0.1.2+ gateway remotes: the
|
||||
* The seq-anchored history page verb of the harness gateway remotes: the
|
||||
* session namespace mounts as a traced cordis service literally named
|
||||
* `remote.session`. The plugin resolves it through the DECLARED inject
|
||||
* (`watchHistoryFaces` in historyPage.ts — a non-declared read of the
|
||||
* traced proxy throws), so reads are undefined on harnesses that never
|
||||
* mount it (the 0.1.1 remotes carry no session namespace) rather than
|
||||
* crashing. `throughSeq` is the inclusive log cut (a seq that must exist in
|
||||
* the log), `beforeSeq` the exclusive upper bound, and the response wraps
|
||||
* the rows in a `ClientResult`-style envelope. Rows are
|
||||
* `SessionHistoryRecord`s — `{type:'event', event}` entries plus packed
|
||||
* mount it rather than crashing. `throughSeq` is the inclusive log cut (a
|
||||
* seq that must exist in the log), `beforeSeq` the exclusive upper bound,
|
||||
* and the response wraps the rows in a `ClientResult`-style envelope. Rows
|
||||
* are `SessionHistoryRecord`s — `{type:'event', event}` entries plus packed
|
||||
* `{type:'chunks', …}` runs the mapper skips (every event the fold needs —
|
||||
* user/assistant messages, tool calls/results, compaction summaries — is
|
||||
* always served verbatim; only streaming deltas pack).
|
||||
@@ -564,7 +510,6 @@ export interface SessionPageFace {
|
||||
/** The connection service face, as far as this plugin consumes it. */
|
||||
export interface ConnectionFace {
|
||||
api?: {
|
||||
sessions?: SessionsHistoryFace
|
||||
host?: { openPath?(request: { path: string }): Promise<unknown> }
|
||||
}
|
||||
/** Observable host description (dsh's HostDescriptionSource); `canOpenPath` gates the open affordance. */
|
||||
|
||||
+16
-19
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Session-projection unit contract compatibility layer.
|
||||
*
|
||||
* Supported harness baselines (see AGENTS.md "Compatibility"): dsh
|
||||
* 0.1.1-rc.2+ and 0.1.2-alpha.2+. On both, the session-projection registry
|
||||
* drives the SAME unit contract (introduced in dsh 0.1.1-rc.1, replacing the
|
||||
* pre-0.1.1 `{ schema, view }` shape this plugin no longer emits):
|
||||
* Supported harness baseline (see AGENTS.md "Compatibility"): dsh
|
||||
* 0.1.2-rc.1+. On it, the session-projection registry drives the unit
|
||||
* contract (introduced in dsh 0.1.1-rc.1, replacing the pre-0.1.1
|
||||
* `{ schema, view }` shape this plugin no longer emits):
|
||||
*
|
||||
* `{ key, stateSchema, init, apply, wire?, stateVersion }`
|
||||
*
|
||||
@@ -15,33 +15,30 @@
|
||||
* A unit WITHOUT `wire` is host-only — its value is never delivered to
|
||||
* clients, and the Context tab would sit on its loading screen forever.
|
||||
*
|
||||
* dsh 0.1.2-alpha.1 widened `init` to `init(header: SessionHeader)` — the
|
||||
* registry now hands the session's immutable header to a fresh fold. A
|
||||
* zero-argument `init` satisfies that contract as-is (extra arguments are
|
||||
* simply not observed), so one definition installs unchanged on both
|
||||
* baselines: the 0.1.1-rc.2 registry calls `init()` and the 0.1.2-alpha.2
|
||||
* registry calls `init(header)`.
|
||||
* The registry hands the session's immutable header to a fresh fold:
|
||||
* `init(header: SessionHeader)`. A zero-argument `init` satisfies that
|
||||
* contract as-is (the extra argument is simply not observed).
|
||||
*
|
||||
* The installed devDependency types pin the newest published surface
|
||||
* (0.1.2-alpha.2), whose `wire?` is optional on both supported baselines;
|
||||
* this plugin's units are always client-visible, and the registry's
|
||||
* wired-register overload demands `wire` PRESENT — which the dts's optional
|
||||
* `wire?` fails. The contract is therefore mirrored here as {@link
|
||||
* ProjectionDefinition} to keep both halves compile-checked.
|
||||
* (0.1.2-rc.1), whose `wire?` is optional; this plugin's units are always
|
||||
* client-visible, and the registry's wired-register overload demands `wire`
|
||||
* PRESENT — which the dts's optional `wire?` fails. The contract is
|
||||
* therefore mirrored here as {@link ProjectionDefinition} to keep both
|
||||
* halves compile-checked.
|
||||
*/
|
||||
|
||||
import type { z } from 'zod'
|
||||
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** The session-projection unit contract served by dsh 0.1.1-rc.2+ and 0.1.2-alpha.2+ (local mirror). */
|
||||
/** The session-projection unit contract served by dsh 0.1.2-rc.1+ (local mirror). */
|
||||
export interface ProjectionDefinition<K extends keyof SessionProjectionMap, S> {
|
||||
key: K
|
||||
stateSchema: z.ZodType<S>
|
||||
/**
|
||||
* Fresh fold state for the empty log. dsh 0.1.2-alpha.1 widened the
|
||||
* contract to `init(header: SessionHeader)`; a zero-argument init
|
||||
* satisfies every supported registry (extra arguments go unobserved).
|
||||
* Fresh fold state for the empty log. The registry passes the session's
|
||||
* immutable header; a zero-argument init satisfies the contract (the
|
||||
* extra argument goes unobserved).
|
||||
*/
|
||||
init(): S
|
||||
/**
|
||||
|
||||
@@ -44,11 +44,9 @@ export const SettingsSchema: z<PluginSettings> = z.object({
|
||||
/** Serve the namespace while a settings provider is composed; inert otherwise. */
|
||||
export function installSettings(ctx: Context): void {
|
||||
ctx.inject(['settings'], (sctx) => {
|
||||
// dsh 0.1.2-alpha.2 removed the `settingsNamespace()` brand helper — its own
|
||||
// packages now register the raw string. The literal matches the namespace
|
||||
// pattern every version enforces somewhere (inside `register` on 0.1.2-alpha.2,
|
||||
// inside the removed helper before that), and the branded cast only satisfies
|
||||
// the pre-0.1.2 type face.
|
||||
// The settings packages register the raw namespace string (the
|
||||
// `settingsNamespace()` brand helper is long gone); the branded cast
|
||||
// only satisfies the dsh-settings type face.
|
||||
sctx.settings.register(SETTINGS_NAMESPACE as SettingsNamespace, SettingsSchema)
|
||||
})
|
||||
}
|
||||
@@ -135,7 +135,7 @@ const contextTimelineSchema = z.object({
|
||||
}).strict() as unknown as z.ZodType<ContextTimeline>
|
||||
|
||||
/**
|
||||
* The persisted fold-state schema (the dsh 0.1.1-rc.1+ `stateSchema`
|
||||
* The persisted fold-state schema (the registry's `stateSchema`
|
||||
* contract). Validates the plain-JSON `TimelineState` before a checkpoint
|
||||
* row seeds a fold — the same shape guarantee the projection cache's
|
||||
* plain-JSON precondition already enforces at write time.
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
* 2. MCP tools: `dsh-mcp-client` names every proxied tool
|
||||
* `mcp__<server>__<rawName>`, so the server is recoverable from the name.
|
||||
* 3. Shipped first-party tools: a PINNED name → package map derived from the
|
||||
* deepseek-harness tool-schema catalog (packages/core/tools + the shipped
|
||||
* `tool-*` packages at dsh 0.1.1-rc.2; names are stable across the plugin's
|
||||
* supported 0.1.1-rc2+ / 0.1.2-alpha2+ range). Third-party tools stay
|
||||
* deepseek-harness official tool-schema catalog (the shipped tool packages
|
||||
* at the supported dsh baseline, tests/baselines.ts; names verified stable
|
||||
* across the plugin's supported range). Third-party tools stay
|
||||
* unattributed rather than guessed.
|
||||
*
|
||||
* Pure string functions: fully unit-testable, no harness imports.
|
||||
@@ -52,10 +52,11 @@ export function pinnedSourceOf(name: string): string | undefined {
|
||||
|
||||
/**
|
||||
* Pinned first-party tool → plugin package map (see header comment). One entry
|
||||
* per model-facing name of the shipped tool packages at dsh 0.1.1-rc.2, from
|
||||
* the official tool-schema catalog; names are stable across the supported
|
||||
* harness range. Packages that mount under distinct names per composition
|
||||
* (bash/pwsh persistent variants, subagent fork) map to their primary package.
|
||||
* per model-facing name of the shipped tool packages in the official
|
||||
* tool-schema catalog of the supported dsh baseline; names are verified
|
||||
* stable across the supported harness range. Packages that mount under
|
||||
* distinct names per composition (bash/pwsh persistent variants, the
|
||||
* `subagent_fork` fixed-route alias) map to their primary package.
|
||||
*/
|
||||
export const FIRST_PARTY_SOURCES: Readonly<Record<string, string>> = Object.freeze({
|
||||
read: '@deepseek-ai/dsh-tool-fs',
|
||||
|
||||
+20
-107
@@ -15,138 +15,51 @@
|
||||
* refactor — not just "it broke somewhere".
|
||||
*/
|
||||
|
||||
/** Every dsh line this plugin must install and work on (see AGENTS.md "Compatibility"). */
|
||||
export const SUPPORTED_BASELINES = ['v0.1.1-rc.2', 'v0.1.2-alpha.2', 'v0.1.2-alpha.5', 'v0.1.2-rc.1'] as const
|
||||
/** The supported dsh tags, in lockstep with the BASELINES entries below. */
|
||||
export type BaselineId = 'v0.1.2-rc.1'
|
||||
|
||||
export type BaselineId = (typeof SUPPORTED_BASELINES)[number]
|
||||
|
||||
/** The session-projection unit contract (src/host/compat.ts mirrors it). */
|
||||
export interface HostSeam {
|
||||
/**
|
||||
* Whether the registry hands the session's immutable header to a fresh
|
||||
* fold: `init(header)` since dsh 0.1.2-alpha.1, `init()` before. The
|
||||
* plugin's zero-argument `init` satisfies both — the matrix proves it.
|
||||
*/
|
||||
initHeader: boolean
|
||||
/**
|
||||
* Whether `session/created` eagerly seeds cells for fresh (seq-0)
|
||||
* sessions (dsh 0.1.2-alpha.1+); before that, cells build lazily on
|
||||
* first touch. Observationally identical for contributors, but the
|
||||
* driver reproduces both timings.
|
||||
*/
|
||||
seedsOnCreate: boolean
|
||||
}
|
||||
|
||||
/** The harness web half's client faces, as far as this plugin consumes them. */
|
||||
/** The harness web half's client faces, as far as the compat probes consume them. */
|
||||
export interface ClientSeam {
|
||||
/**
|
||||
* Where the finalized conversation nodes come from: the `useChat`
|
||||
* ChatSnapshot seat (`legacy.nodes`) since dsh 0.1.2, or the session
|
||||
* snapshot's own `nodes` before.
|
||||
*/
|
||||
chatNodesSeat: 'useChat' | 'useSession'
|
||||
/** The durable-image loader service/method, renamed across 0.1.2. */
|
||||
imageFace: { service: 'conversation' | 'uiConversation'; method: 'resolveImage' | 'imageUrl' }
|
||||
/** The seq-anchored history face the browser's targeted fetch rides. */
|
||||
historyFace: 'api.sessions.history' | 'remote.session.page'
|
||||
/**
|
||||
* The served response envelope: the 0.1.1 api client nests the result
|
||||
* (`{result:{ok,value}}`), the 0.1.2 remote resolves the ClientResult
|
||||
* itself (`{ok,value}`).
|
||||
*/
|
||||
historyEnvelope: 'resultNested' | 'clientResult'
|
||||
/** The rows field on a successful history page value. */
|
||||
historyRowsField: 'events' | 'records'
|
||||
/**
|
||||
* MarkdownText's chrome prop: optional `codeLabels` on 0.1.1 (extra props
|
||||
* are ignored), REQUIRED `labels` since 0.1.2-alpha — missing it would
|
||||
* throw inside the primitive.
|
||||
*/
|
||||
markdownChrome: 'codeLabels' | 'labels'
|
||||
/** The durable-image loader method the browser cards ride. */
|
||||
imageFaceMethod: string
|
||||
/** MarkdownText's chrome prop the plugin must hand every markdown render. */
|
||||
markdownChrome: string
|
||||
/** The platform module table the shell seeds (client-bundle requires must resolve). */
|
||||
platformModules: readonly string[]
|
||||
}
|
||||
|
||||
export interface Baseline {
|
||||
id: BaselineId
|
||||
/** The dsh git tag in deepseek-ai/deepseek-harness (local checkout or CI fetch). */
|
||||
id: BaselineId
|
||||
tag: string
|
||||
/** The vendored @deepseek-ai/cordis release that harness line ships. */
|
||||
cordis: string
|
||||
/**
|
||||
* The @deepseek-ai/dsh-session release that line vendors. The staged
|
||||
* session-projection sources import runtime values from it (SessionLogOffset /
|
||||
* SessionSeq, non-type imports since 0.1.2-alpha.3), so the compat driver must
|
||||
* resolve the specifier to the tag's own generation.
|
||||
* SessionSeq), so the compat driver must resolve the specifier to the tag's
|
||||
* own generation.
|
||||
*/
|
||||
session: string
|
||||
host: HostSeam
|
||||
client: ClientSeam
|
||||
}
|
||||
|
||||
/** The client faces shared by the whole 0.1.2 generation (verified identical alpha.2 through rc.1). */
|
||||
const CLIENT_0_1_2: ClientSeam = {
|
||||
chatNodesSeat: 'useChat',
|
||||
imageFace: { service: 'uiConversation', method: 'imageUrl' },
|
||||
historyFace: 'remote.session.page',
|
||||
historyEnvelope: 'clientResult',
|
||||
historyRowsField: 'records',
|
||||
markdownChrome: 'labels',
|
||||
platformModules: [
|
||||
'react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', '@deepseek-ai/cordis',
|
||||
'@deepseek-ai/dsh-client-store',
|
||||
'@deepseek-ai/dsh-client-ui-slots',
|
||||
'@deepseek-ai/dsh-client-ui-primitives',
|
||||
],
|
||||
}
|
||||
|
||||
export const BASELINES: readonly Baseline[] = [
|
||||
{
|
||||
id: 'v0.1.1-rc.2',
|
||||
tag: 'dsh-v0.1.1-rc.2',
|
||||
cordis: '4.0.1',
|
||||
session: '0.1.1-rc.2',
|
||||
host: { initHeader: false, seedsOnCreate: false },
|
||||
client: {
|
||||
chatNodesSeat: 'useSession',
|
||||
imageFace: { service: 'conversation', method: 'resolveImage' },
|
||||
historyFace: 'api.sessions.history',
|
||||
historyEnvelope: 'resultNested',
|
||||
historyRowsField: 'events',
|
||||
markdownChrome: 'codeLabels',
|
||||
platformModules: [
|
||||
'react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', '@deepseek-ai/cordis',
|
||||
'@deepseek-ai/dsh-client-ui-slots',
|
||||
'@deepseek-ai/dsh-client-ui-primitives',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'v0.1.2-alpha.2',
|
||||
tag: 'dsh-v0.1.2-alpha.2',
|
||||
cordis: '4.0.2',
|
||||
session: '0.1.2-alpha.2',
|
||||
host: { initHeader: true, seedsOnCreate: true },
|
||||
client: CLIENT_0_1_2,
|
||||
},
|
||||
{
|
||||
// The preview window's head — the DSH STORE latest-three compatibility
|
||||
// window currently ends here (alpha.3/alpha.4 sit between the probed
|
||||
// alpha.2 and alpha.5 tags with no seam drift).
|
||||
id: 'v0.1.2-alpha.5',
|
||||
tag: 'dsh-v0.1.2-alpha.5',
|
||||
cordis: '4.0.2',
|
||||
session: '0.1.2-alpha.5',
|
||||
host: { initHeader: true, seedsOnCreate: true },
|
||||
client: CLIENT_0_1_2,
|
||||
},
|
||||
{
|
||||
// The `next`-channel release candidate — the newest declared dsh release.
|
||||
id: 'v0.1.2-rc.1',
|
||||
tag: 'dsh-v0.1.2-rc.1',
|
||||
cordis: '4.0.2',
|
||||
session: '0.1.2-rc.1',
|
||||
host: { initHeader: true, seedsOnCreate: true },
|
||||
client: CLIENT_0_1_2,
|
||||
client: {
|
||||
imageFaceMethod: 'imageUrl',
|
||||
markdownChrome: 'labels',
|
||||
platformModules: [
|
||||
'react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', '@deepseek-ai/cordis',
|
||||
'@deepseek-ai/dsh-client-store',
|
||||
'@deepseek-ai/dsh-client-ui-slots',
|
||||
'@deepseek-ai/dsh-client-ui-primitives',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -3,11 +3,10 @@
|
||||
// always-on suite can drive the plugin's client half through each baseline's
|
||||
// seams. The real-code complement runs in the `compat` vitest project.
|
||||
|
||||
import type { Baseline } from '../../baselines'
|
||||
import { TestClientCtx } from '../helpers/harness'
|
||||
import type { ConversationNodeLike, UseChatLike, UseSessionLike } from '../../../src/client/services'
|
||||
import type { ConversationNodeLike, UseChatLike } from '../../../src/client/services'
|
||||
|
||||
/** A finalized conversation-node window (the seats' currency, both baselines). */
|
||||
/** A finalized conversation-node window (the seats' currency). */
|
||||
export function convNodes(): ConversationNodeLike[] {
|
||||
return [
|
||||
{ kind: 'user', seq: 4, content: [{ type: 'text', text: 'window node hello' }] },
|
||||
@@ -15,12 +14,7 @@ export function convNodes(): ConversationNodeLike[] {
|
||||
]
|
||||
}
|
||||
|
||||
/** A `useSession`-shaped hook over the given snapshot (the pre-0.1.2 seat). */
|
||||
export function sessionSeat(nodes: readonly ConversationNodeLike[] | undefined): UseSessionLike {
|
||||
return selector => selector({ nodes })
|
||||
}
|
||||
|
||||
/** A `useChat`-shaped hook over a ChatSnapshot (`legacy.nodes`, dsh 0.1.2+). */
|
||||
/** A `useChat`-shaped hook over a ChatSnapshot (`legacy.nodes`). */
|
||||
export function chatSeat(nodes: readonly ConversationNodeLike[] | undefined): UseChatLike {
|
||||
return selector => selector({ legacy: { nodes } })
|
||||
}
|
||||
@@ -34,58 +28,39 @@ function historyEvents(): unknown[] {
|
||||
time: 1,
|
||||
data: { content: [{ type: 'text', text: 'history node hello' }] },
|
||||
},
|
||||
// The 0.1.2 storage packs streaming deltas into chunk rows — unmappable noise here.
|
||||
// The storage packs streaming deltas into chunk rows — unmappable noise here.
|
||||
{ type: 'text-chunks', seq0: 5, time0: 2, members: [] },
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Arm a test ctx with ONLY the baseline's service faces, the way that
|
||||
* generation composes them: the image loader service, the history face, and
|
||||
* nothing from the other baseline. The returned props hooks pick the seats.
|
||||
* Arm a test ctx with the harness service faces the way the generation
|
||||
* composes them: the gateway remotes (the traced `remote` facade carrying
|
||||
* the session page, plus the direct `remote.session` service whose
|
||||
* undeclared reads throw) and the durable-image loader service. Arm the
|
||||
* plugin's declared inject (`watchHistoryFaces`) to resolve the page face.
|
||||
*/
|
||||
export function baselineCtx(baseline: Baseline): { ctx: TestClientCtx; calls: { history: unknown[]; page: unknown[]; image: { sessionId: string; attachment: unknown }[] } } {
|
||||
export function baselineCtx(): { ctx: TestClientCtx; calls: { page: unknown[]; image: { sessionId: string; attachment: unknown }[] } } {
|
||||
const ctx = new TestClientCtx()
|
||||
const calls: { history: unknown[]; page: unknown[]; image: { sessionId: string; attachment: unknown }[] } = { history: [], page: [], image: [] }
|
||||
const calls: { page: unknown[]; image: { sessionId: string; attachment: unknown }[] } = { page: [], image: [] }
|
||||
const events = historyEvents()
|
||||
|
||||
if (baseline.client.historyFace === 'api.sessions.history') {
|
||||
ctx.setService('connection', {
|
||||
api: {
|
||||
sessions: {
|
||||
history(request: unknown): Promise<unknown> {
|
||||
calls.history.push(request)
|
||||
// The 0.1.1 api client nests the result envelope.
|
||||
return Promise.resolve({ result: { ok: true, value: { events } } })
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// The 0.1.2 gateway remotes mount as a traced cordis service literally
|
||||
// named `remote.session`; the remote resolves the ClientResult itself.
|
||||
ctx.setService('remote.session', {
|
||||
page(request: unknown): Promise<unknown> {
|
||||
calls.page.push(request)
|
||||
return Promise.resolve({ ok: true, value: { records: events.map(event => ({ type: 'event', event })) } })
|
||||
},
|
||||
})
|
||||
// The remote resolves the ClientResult itself; rows are
|
||||
// `{type:'event', event}` records.
|
||||
const page = (request: unknown): Promise<unknown> => {
|
||||
calls.page.push(request)
|
||||
return Promise.resolve({ ok: true, value: { records: events.map(event => ({ type: 'event', event })) } })
|
||||
}
|
||||
ctx.setService('remote', { session: { page } })
|
||||
ctx.setService('remote.session', {
|
||||
get page() { throw new Error('cannot get property "remote.session" without inject') },
|
||||
})
|
||||
|
||||
if (baseline.client.imageFace.service === 'conversation') {
|
||||
ctx.setService('conversation', {
|
||||
resolveImage(sessionId: string, attachment: unknown): Promise<string> {
|
||||
calls.image.push({ sessionId, attachment })
|
||||
return Promise.resolve('blob:legacy-image')
|
||||
},
|
||||
})
|
||||
} else {
|
||||
ctx.setService('uiConversation', {
|
||||
imageUrl(sessionId: string, attachment: unknown): Promise<string> {
|
||||
calls.image.push({ sessionId, attachment })
|
||||
return Promise.resolve('blob:modern-image')
|
||||
},
|
||||
})
|
||||
}
|
||||
ctx.setService('uiConversation', {
|
||||
imageUrl(sessionId: string, attachment: unknown): Promise<string> {
|
||||
calls.image.push({ sessionId, attachment })
|
||||
return Promise.resolve('blob:modern-image')
|
||||
},
|
||||
})
|
||||
return { ctx, calls }
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
// The always-on compatibility matrix — CLIENT side. For every supported dsh
|
||||
// baseline (tests/baselines.ts) the plugin's client half is driven through
|
||||
// THAT generation's faces: the finalized-nodes seat (useChat on 0.1.2+, the
|
||||
// session snapshot before), the durable-image loader service (renamed across
|
||||
// 0.1.2), the seq-anchored history face and its response envelope (rewritten
|
||||
// in 0.1.2 into the remote.session gateway), and the MarkdownText chrome prop
|
||||
// (required `labels` since 0.1.2-alpha). These are the seams whose drift
|
||||
// produced the recurring client-side incidents (issues #8, #12, #26).
|
||||
// THAT generation's faces: the finalized-nodes seat (`useChat`'s
|
||||
// ChatSnapshot), the durable-image loader service, the seq-anchored history
|
||||
// face and its response envelope (the remote.session gateway), and the
|
||||
// MarkdownText chrome prop (required `labels`). These are the seams whose
|
||||
// drift produced the recurring client-side incidents (issues #8, #12, #26).
|
||||
//
|
||||
// The real-code complement — the ACTUAL dsh sources per tag — runs in the
|
||||
// `compat` vitest project (tests/compat/matrix.spec.ts).
|
||||
@@ -15,7 +14,7 @@ import { describe, test, vi } from 'vitest'
|
||||
import { BASELINES } from '../../baselines'
|
||||
import { conversationNodesOf, imageLoaderOf } from '../../../src/client/services'
|
||||
import type { SessionStandardProps } from '../../../src/client/services'
|
||||
import { makeContentFetcher } from '../../../src/client/historyPage'
|
||||
import { makeContentFetcher, watchHistoryFaces } from '../../../src/client/historyPage'
|
||||
import { makeRichText } from '../../../src/client/components/richText'
|
||||
import { makeContextView } from '../../../src/client/components/contextView'
|
||||
import { createContextSettings } from '../../../src/client/settings'
|
||||
@@ -24,13 +23,13 @@ import type { ContextTimeline } from '../../../src/shared/types'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { asClientCtx, TestClientCtx } from '../helpers/harness'
|
||||
import { click, makeKit, mount, queryAll, text } from '../helpers/kit'
|
||||
import { baselineCtx, chatSeat, convNodes, sessionSeat } from './baselineFaces'
|
||||
import { baselineCtx, chatSeat, convNodes } from './baselineFaces'
|
||||
|
||||
const kit = makeKit()
|
||||
|
||||
// The chrome-prop capture: the plugin must hand EVERY markdown render a
|
||||
// well-formed `labels` object (the 0.1.2-alpha+ primitives REQUIRE it; the
|
||||
// 0.1.1 renderer ignores the extra key). Intercept the primitive.
|
||||
// well-formed `labels` object (the primitives REQUIRE it). Intercept the
|
||||
// primitive.
|
||||
const captured = vi.hoisted(() => ({ markdownProps: undefined as Record<string, unknown> | undefined }))
|
||||
vi.mock('@deepseek-ai/dsh-client-ui-primitives', async () => {
|
||||
const React = await import('react')
|
||||
@@ -61,72 +60,64 @@ function timeline(): ContextTimeline {
|
||||
|
||||
for (const baseline of BASELINES) {
|
||||
describe(`client faces — ${baseline.id}`, () => {
|
||||
test('the finalized-nodes seat of this generation serves the conversation window join', () => {
|
||||
test('the finalized-nodes seat serves the conversation window join', () => {
|
||||
const nodes = convNodes()
|
||||
const fromSeat = baseline.client.chatNodesSeat === 'useChat'
|
||||
? conversationNodesOf({ useChat: chatSeat(nodes) })
|
||||
: conversationNodesOf({ useSession: sessionSeat(nodes) })
|
||||
assert.deepEqual(fromSeat, nodes)
|
||||
// The other generation's seat absent: no join, no error.
|
||||
const absent = baseline.client.chatNodesSeat === 'useChat'
|
||||
? conversationNodesOf({ useSession: sessionSeat(undefined) })
|
||||
: conversationNodesOf({ useChat: chatSeat(undefined) })
|
||||
assert.equal(absent, undefined)
|
||||
assert.deepEqual(conversationNodesOf({ useChat: chatSeat(nodes) }), nodes)
|
||||
// A seat without the legacy slice: no join, no error.
|
||||
assert.equal(conversationNodesOf({ useChat: chatSeat(undefined) }), undefined)
|
||||
})
|
||||
|
||||
test('the durable-image loader rides the service of this generation', async () => {
|
||||
const { ctx, calls } = baselineCtx(baseline)
|
||||
test('the durable-image loader rides the uiConversation service', async () => {
|
||||
const { ctx, calls } = baselineCtx()
|
||||
const loader = imageLoaderOf(asClientCtx(ctx), 's-face')
|
||||
assert.ok(loader !== undefined, 'the baseline image face serves a loader')
|
||||
const url = await loader({ attachmentId: 'a1' })
|
||||
assert.equal(url, baseline.client.imageFace.service === 'conversation' ? 'blob:legacy-image' : 'blob:modern-image')
|
||||
assert.equal(url, 'blob:modern-image')
|
||||
assert.equal(calls.image.length, 1)
|
||||
assert.equal(calls.image[0]?.sessionId, 's-face')
|
||||
})
|
||||
|
||||
test('the targeted content fetch rides the history face and envelope of this generation', async () => {
|
||||
const { ctx, calls } = baselineCtx(baseline)
|
||||
const fetcher = makeContentFetcher(asClientCtx(ctx), 's-face')
|
||||
test('the targeted content fetch rides the gateway history face and envelope', async () => {
|
||||
const { ctx, calls } = baselineCtx()
|
||||
// Arm the declared inject the way the client apply does; the slot is
|
||||
// unloaded afterwards so no face stales into the next test.
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
const fetcher = makeContentFetcher('s-face')
|
||||
assert.ok(fetcher !== undefined, 'the baseline history face serves a fetcher')
|
||||
const node = await fetcher(4)
|
||||
assert.ok(node !== null)
|
||||
assert.equal(node.kind, 'user')
|
||||
assert.equal(node.seq, 4)
|
||||
assert.deepEqual(node.content, [{ type: 'text', text: 'history node hello' }])
|
||||
// The per-generation request shape.
|
||||
if (baseline.client.historyFace === 'api.sessions.history') {
|
||||
assert.deepEqual(calls.history, [{ sessionId: 's-face', beforeSeq: 5 }])
|
||||
} else {
|
||||
assert.deepEqual(calls.page, [{ address: { kind: 'session', sessionId: 's-face' }, throughSeq: 4, beforeSeq: 5 }])
|
||||
}
|
||||
// The inclusive cut pinned to the seq, the exclusive bound one past it.
|
||||
assert.deepEqual(calls.page, [{ address: { kind: 'session', sessionId: 's-face' }, throughSeq: 4, beforeSeq: 5 }])
|
||||
// History is immutable: a re-read never re-fetches.
|
||||
await fetcher(4)
|
||||
assert.equal(calls.history.length + calls.page.length, 1)
|
||||
assert.equal(calls.page.length, 1)
|
||||
ctx.dispose()
|
||||
assert.equal(makeContentFetcher('s-face'), undefined, 'the unloaded slot leaves no face behind')
|
||||
})
|
||||
|
||||
test('a failed history rpc rejects (retryable), never resolving to a fake absence', async () => {
|
||||
const ctx = baselineCtx(baseline).ctx
|
||||
if (baseline.client.historyFace === 'api.sessions.history') {
|
||||
ctx.setService('connection', { api: { sessions: { history: () => Promise.resolve({ result: { ok: false } }) } } })
|
||||
} else {
|
||||
ctx.setService('remote.session', { page: () => Promise.resolve({ ok: false }) })
|
||||
}
|
||||
const fetcher = makeContentFetcher(asClientCtx(ctx), 's-face')
|
||||
const { ctx } = baselineCtx()
|
||||
// The inject resolves the page off the `remote` facade — fail it there.
|
||||
ctx.setService('remote', { session: { page: () => Promise.resolve({ ok: false }) } })
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
const fetcher = makeContentFetcher('s-face')
|
||||
assert.ok(fetcher !== undefined)
|
||||
await assert.rejects(fetcher(4))
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
test('the Context tab renders through the seats of this generation without tripping the error boundary', async () => {
|
||||
const { ctx } = baselineCtx(baseline)
|
||||
const { ctx } = baselineCtx()
|
||||
const settings = createContextSettings()
|
||||
const View = makeContextView(asClientCtx(ctx), kit, settings)
|
||||
const nodes = convNodes()
|
||||
const props: SessionStandardProps = {
|
||||
sessionId: 's-face',
|
||||
useProjection: (key: string) => (key === 'contextTimeline' ? timeline() : undefined),
|
||||
...(baseline.client.chatNodesSeat === 'useChat'
|
||||
? { useChat: chatSeat(nodes) }
|
||||
: { useSession: sessionSeat(nodes) }),
|
||||
useChat: chatSeat(nodes),
|
||||
}
|
||||
const m = await mount(h(View, props))
|
||||
const rendered = text(m.container)
|
||||
@@ -139,42 +130,43 @@ for (const baseline of BASELINES) {
|
||||
})
|
||||
|
||||
test('a throwing history-service proxy degrades the tab, never crashes it', async () => {
|
||||
// Issue #42: an undeclared-service proxy can throw on the property READ
|
||||
// itself ("cannot get property "remote.session" without inject") — the
|
||||
// no-white-screen guarantee must degrade it to the static hint, not let
|
||||
// it escape the view and take the whole web client down.
|
||||
// Issue #42: a traced proxy can throw on the property READ itself
|
||||
// ("cannot get property "remote.session" without inject") — arming the
|
||||
// declared inject with such a hostile facade must leave the slot unset
|
||||
// (nothing escapes the callback) and the tab renders with its static
|
||||
// hint instead of crashing.
|
||||
const ctx = new TestClientCtx()
|
||||
if (baseline.client.historyFace === 'remote.session.page') {
|
||||
ctx.setService('remote.session', {
|
||||
get page() { throw new Error('cannot get property "remote.session" without inject') },
|
||||
})
|
||||
} else {
|
||||
ctx.setService('connection', { get api() { throw new Error('hostile connection') } })
|
||||
}
|
||||
ctx.setService('remote', {
|
||||
get session() { throw new Error('cannot get property "remote.session" without inject') },
|
||||
})
|
||||
ctx.setService('remote.session', {
|
||||
get page() { throw new Error('cannot get property "remote.session" without inject') },
|
||||
})
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
assert.equal(makeContentFetcher('s-face'), undefined, 'the hostile face left the slot unset')
|
||||
const settings = createContextSettings()
|
||||
const View = makeContextView(asClientCtx(ctx), kit, settings)
|
||||
const nodes = convNodes()
|
||||
const props: SessionStandardProps = {
|
||||
sessionId: 's-face',
|
||||
useProjection: (key: string) => (key === 'contextTimeline' ? timeline() : undefined),
|
||||
...(baseline.client.chatNodesSeat === 'useChat'
|
||||
? { useChat: chatSeat(nodes) }
|
||||
: { useSession: sessionSeat(nodes) }),
|
||||
useChat: chatSeat(nodes),
|
||||
}
|
||||
const m = await mount(h(View, props))
|
||||
assert.ok(text(m.container).includes(DICT_EN['overview.title']), 'the tab still rendered around a hostile face')
|
||||
await m.unmount()
|
||||
ctx.dispose()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('client faces — markdown chrome prop (required since 0.1.2-alpha)', () => {
|
||||
describe('client faces — markdown chrome prop (required)', () => {
|
||||
test('every markdown render carries a well-formed `labels` chrome', async () => {
|
||||
const { RichText } = makeRichText(kit)
|
||||
const m = await mount(h(RichText, { text: '# hello', mode: 'md' }))
|
||||
assert.ok(captured.markdownProps !== undefined, 'MarkdownText received props')
|
||||
const labels = captured.markdownProps.labels as { code: { copyLabel: string; copiedLabel: string }; footnotes: string } | undefined
|
||||
assert.ok(labels !== undefined, 'labels chrome present (required by the 0.1.2-alpha primitives)')
|
||||
assert.ok(labels !== undefined, 'labels chrome present (required by the primitives)')
|
||||
assert.equal(typeof labels.code.copyLabel, 'string')
|
||||
assert.equal(typeof labels.code.copiedLabel, 'string')
|
||||
assert.equal(typeof labels.footnotes, 'string')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { makeContextJumpButton } from '../../../src/client/components/contextJump'
|
||||
import type { ConversationNodeLike, UseSessionLike } from '../../../src/client/services'
|
||||
import type { ConversationNodeLike, UseChatLike } from '../../../src/client/services'
|
||||
import { takeContextFocus } from '../../../src/client/viewFocus'
|
||||
import { DICT_EN } from '../../../src/client/i18n'
|
||||
import { h } from '../../../src/client/react'
|
||||
@@ -17,9 +17,9 @@ const Jump = makeContextJumpButton(kit)
|
||||
|
||||
const SESSION = 'sv-jump'
|
||||
|
||||
/** The chat node seat: the session snapshot arm carrying assistant nodes with message ids (hostile shapes forced past the seat type). */
|
||||
function sessionWith(nodes: unknown): { useSession?: UseSessionLike } {
|
||||
return { useSession: (sel => sel({ nodes: nodes as readonly ConversationNodeLike[] })) as UseSessionLike }
|
||||
/** The chat node seat: the ChatSnapshot arm carrying assistant nodes with message ids (hostile shapes forced past the seat type). */
|
||||
function chatWith(nodes: unknown): { useChat?: UseChatLike } {
|
||||
return { useChat: (sel => sel({ legacy: { nodes: nodes as readonly ConversationNodeLike[] } })) as UseChatLike }
|
||||
}
|
||||
|
||||
const reply = { kind: 'assistant', seq: 4, messageId: 'msg-4' }
|
||||
@@ -69,7 +69,7 @@ describe('ContextJump — click flow', () => {
|
||||
bar.append(chat, context)
|
||||
document.body.appendChild(bar)
|
||||
try {
|
||||
const m = await mount(h(Jump, { messageId: 'msg-4', sessionId: SESSION, ...sessionWith([reply, toolNode]) }))
|
||||
const m = await mount(h(Jump, { messageId: 'msg-4', sessionId: SESSION, ...chatWith([reply, toolNode]) }))
|
||||
await click(query(m.container, 'button.lc-jump'))
|
||||
assert.equal(takeContextFocus(SESSION), 4, 'the clicked reply’s request seq rides the relay')
|
||||
assert.equal(contextClicks, 1, 'the Context tab button was clicked')
|
||||
@@ -81,13 +81,13 @@ describe('ContextJump — click flow', () => {
|
||||
})
|
||||
|
||||
test('the seq resolution prefers the matching assistant node and drops non-finite seqs', async () => {
|
||||
const m = await mount(h(Jump, { messageId: 'msg-tool', sessionId: SESSION, ...sessionWith([reply, toolNode]) }))
|
||||
const m = await mount(h(Jump, { messageId: 'msg-tool', sessionId: SESSION, ...chatWith([reply, toolNode]) }))
|
||||
await click(query(m.container, 'button.lc-jump'))
|
||||
assert.equal(takeContextFocus(SESSION), null, 'a tool node never mints a focus')
|
||||
await m.unmount()
|
||||
|
||||
const broken = { kind: 'assistant', seq: Number.NaN, messageId: 'msg-x' }
|
||||
const m2 = await mount(h(Jump, { messageId: 'msg-x', sessionId: SESSION, ...sessionWith([broken]) }))
|
||||
const m2 = await mount(h(Jump, { messageId: 'msg-x', sessionId: SESSION, ...chatWith([broken]) }))
|
||||
await click(query(m2.container, 'button.lc-jump'))
|
||||
assert.equal(takeContextFocus(SESSION), null, 'a non-finite seq never mints a focus')
|
||||
await m2.unmount()
|
||||
@@ -95,7 +95,7 @@ describe('ContextJump — click flow', () => {
|
||||
|
||||
test('a hostile node that throws on property access is skipped, not fatal', async () => {
|
||||
const hostile = { get kind(): string { throw new Error('boom') } }
|
||||
const m = await mount(h(Jump, { messageId: 'msg-4', sessionId: SESSION, ...sessionWith([hostile, reply]) }))
|
||||
const m = await mount(h(Jump, { messageId: 'msg-4', sessionId: SESSION, ...chatWith([hostile, reply]) }))
|
||||
await click(query(m.container, 'button.lc-jump'))
|
||||
assert.equal(takeContextFocus(SESSION), 4, 'the real reply after the hostile element still resolves')
|
||||
await m.unmount()
|
||||
@@ -104,7 +104,7 @@ describe('ContextJump — click flow', () => {
|
||||
get kind(): string { return 'assistant' },
|
||||
get messageId(): string { throw new Error('boom') },
|
||||
}
|
||||
const m2 = await mount(h(Jump, { messageId: 'msg-9', sessionId: SESSION, ...sessionWith([hostileMatch]) }))
|
||||
const m2 = await mount(h(Jump, { messageId: 'msg-9', sessionId: SESSION, ...chatWith([hostileMatch]) }))
|
||||
await click(query(m2.container, 'button.lc-jump'))
|
||||
assert.equal(takeContextFocus(SESSION), null, 'a hostile match candidate degrades to no focus')
|
||||
await m2.unmount()
|
||||
@@ -120,13 +120,13 @@ describe('ContextJump — click flow', () => {
|
||||
bar.appendChild(context)
|
||||
document.body.appendChild(bar)
|
||||
try {
|
||||
const m = await mount(h(Jump, { messageId: 'msg-none', sessionId: SESSION, ...sessionWith([reply]) }))
|
||||
const m = await mount(h(Jump, { messageId: 'msg-none', sessionId: SESSION, ...chatWith([reply]) }))
|
||||
await click(query(m.container, 'button.lc-jump'))
|
||||
assert.equal(takeContextFocus(SESSION), null)
|
||||
assert.equal(contextClicks, 1, 'the tab still opens, just without a pinned step')
|
||||
await m.unmount()
|
||||
|
||||
const m2 = await mount(h(Jump, { messageId: 'msg-4', sessionId: 7, ...sessionWith([reply]) }))
|
||||
const m2 = await mount(h(Jump, { messageId: 'msg-4', sessionId: 7, ...chatWith([reply]) }))
|
||||
await click(query(m2.container, 'button.lc-jump'))
|
||||
assert.equal(contextClicks, 2)
|
||||
await m2.unmount()
|
||||
|
||||
@@ -11,10 +11,11 @@ import { createRoot } from 'react-dom/client'
|
||||
import { afterEach, describe, test, vi } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeContextView } from '../../../src/client/components/contextView'
|
||||
import { watchHistoryFaces } from '../../../src/client/historyPage'
|
||||
import { requestContextFocus, takeContextFocus } from '../../../src/client/viewFocus'
|
||||
import { createContextSettings } from '../../../src/client/settings'
|
||||
import type { SettingsScopeLike } from '../../../src/client/settings'
|
||||
import type { UseSessionLike } from '../../../src/client/services'
|
||||
import type { UseChatLike } from '../../../src/client/services'
|
||||
import type { ContextTimeline } from '../../../src/shared/types'
|
||||
import { DICT_EN } from '../../../src/client/i18n'
|
||||
import { TestClientCtx, TestLocale, asClientCtx } from '../helpers/harness'
|
||||
@@ -189,7 +190,9 @@ async function mountRich(sessionId: string) {
|
||||
useProjection: projectionsFor(richTimeline(), {
|
||||
contextHeaders: { headers: [{ seq: 1, time: T0, system: 'SYS', tools: [{ name: 'bash', tokens: 12, description: 'run' }] }] },
|
||||
}),
|
||||
useSession: (sel => sel({ nodes: [] })) as UseSessionLike,
|
||||
useChat: (sel =>
|
||||
sel({
|
||||
legacy: { nodes: [] } })) as UseChatLike,
|
||||
}))
|
||||
return m
|
||||
}
|
||||
@@ -420,7 +423,9 @@ describe('ContextView — file activity card', () => {
|
||||
const m = await mount(h(View, {
|
||||
sessionId,
|
||||
useProjection: projectionsFor(fileTimeline()),
|
||||
useSession: (sel => sel({ nodes: fileConv })) as UseSessionLike,
|
||||
useChat: (sel =>
|
||||
sel({
|
||||
legacy: { nodes: fileConv } })) as UseChatLike,
|
||||
}))
|
||||
const card = queryAll(m.container, '.lc-card').find(c => text(c).includes(DICT_EN['files.title']))
|
||||
assert.ok(card !== undefined)
|
||||
@@ -512,7 +517,9 @@ describe('ContextView — file activity card', () => {
|
||||
const m = await mount(h(View, {
|
||||
sessionId: 'sv-ptc-locate',
|
||||
useProjection: projectionsFor(ptcTimeline),
|
||||
useSession: (sel => sel({ nodes: ptcConv })) as UseSessionLike,
|
||||
useChat: (sel =>
|
||||
sel({
|
||||
legacy: { nodes: ptcConv } })) as UseChatLike,
|
||||
}))
|
||||
const card = queryAll(m.container, '.lc-card').find(c => text(c).includes(DICT_EN['files.title']))
|
||||
assert.ok(card !== undefined)
|
||||
@@ -546,7 +553,9 @@ describe('ContextView — file activity card', () => {
|
||||
const m = await mount(h(View, {
|
||||
sessionId: 'sv-files-open',
|
||||
useProjection: projectionsFor(fileTimeline()),
|
||||
useSession: (sel => sel({ nodes: conv })) as UseSessionLike,
|
||||
useChat: (sel =>
|
||||
sel({
|
||||
legacy: { nodes: conv } })) as UseChatLike,
|
||||
}))
|
||||
const card = queryAll(m.container, '.lc-card').find(c => text(c).includes(DICT_EN['files.title']))
|
||||
assert.ok(card !== undefined)
|
||||
@@ -572,53 +581,59 @@ describe('ContextView — file activity card', () => {
|
||||
})
|
||||
|
||||
describe('ContextView — targeted content fetch and image loading', () => {
|
||||
test('opening an un-joined node reads one seq-anchored history page through the connection api', async () => {
|
||||
const calls: { sessionId: string; beforeSeq: number }[] = []
|
||||
test('opening an un-joined node reads one seq-anchored history page through the gateway remote', async () => {
|
||||
const calls: { sessionId: string; throughSeq: number; beforeSeq: number }[] = []
|
||||
const ctx = new TestClientCtx({
|
||||
services: {
|
||||
connection: {
|
||||
api: {
|
||||
sessions: {
|
||||
history: (request: { sessionId: string; beforeSeq: number }) => {
|
||||
calls.push(request)
|
||||
return Promise.resolve({
|
||||
result: {
|
||||
ok: true,
|
||||
value: {
|
||||
events: [
|
||||
{ event: { type: 'user/message', seq: request.beforeSeq - 1, data: { content: [{ type: 'text', text: 'OLD FULL BODY' }] } } },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
remote: {
|
||||
session: {
|
||||
page: (request: { address: { sessionId: string }; throughSeq: number; beforeSeq: number }) => {
|
||||
calls.push({ sessionId: request.address.sessionId, throughSeq: request.throughSeq, beforeSeq: request.beforeSeq })
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
value: {
|
||||
records: [
|
||||
{ type: 'event', event: { type: 'user/message', seq: request.throughSeq, data: { content: [{ type: 'text', text: 'OLD FULL BODY' }] } } },
|
||||
],
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// The direct `remote.session` service is hostile, as on the real host:
|
||||
// the face resolves only through the declared inject.
|
||||
ctx.setService('remote.session', { get page() { throw new Error('cannot get property "remote.session" without inject') } })
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
const View = makeView(ctx)
|
||||
const m = await mount(h(View, {
|
||||
sessionId: 'sv-page',
|
||||
useProjection: projectionsFor(timeline({ nodes: [{ seq: 1, cat: 'user', tokens: 5, text: 'old msg' }] })),
|
||||
useSession: (sel => sel({ nodes: [] })) as UseSessionLike,
|
||||
useChat: (sel =>
|
||||
sel({
|
||||
legacy: { nodes: [] } })) as UseChatLike,
|
||||
}))
|
||||
const catRow = queryAll(m.container, '.lc-br-cat-row').find(r => text(r).includes(DICT_EN['cat.user']))
|
||||
assert.ok(catRow !== undefined)
|
||||
// The lone node auto-expands with the category, which triggers the fetch.
|
||||
await click(catRow)
|
||||
await flush()
|
||||
assert.deepEqual(calls, [{ sessionId: 'sv-page', beforeSeq: 2 }], 'one read anchored just past the seq')
|
||||
assert.deepEqual(calls, [{ sessionId: 'sv-page', throughSeq: 1, beforeSeq: 2 }], 'one read anchored just past the seq')
|
||||
assert.ok(text(m.container).includes('OLD FULL BODY'), 'mapped page content renders')
|
||||
await m.unmount()
|
||||
// Unload the declared slot so no face stales into the next test.
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
test('without a connection face an uncached node shows the static note', async () => {
|
||||
test('without a history face an uncached node shows the static note', async () => {
|
||||
const View = makeView(new TestClientCtx())
|
||||
const m = await mount(h(View, {
|
||||
sessionId: 'sv-nopage',
|
||||
useProjection: projectionsFor(timeline({ nodes: [{ seq: 1, cat: 'user', tokens: 5, text: 'old msg' }] })),
|
||||
useSession: (sel => sel({ nodes: [] })) as UseSessionLike,
|
||||
useChat: (sel =>
|
||||
sel({
|
||||
legacy: { nodes: [] } })) as UseChatLike,
|
||||
}))
|
||||
const catRow = queryAll(m.container, '.lc-br-cat-row').find(r => text(r).includes(DICT_EN['cat.user']))
|
||||
assert.ok(catRow !== undefined)
|
||||
@@ -631,8 +646,8 @@ describe('ContextView — targeted content fetch and image loading', () => {
|
||||
const resolved: [string, unknown][] = []
|
||||
const ctx = new TestClientCtx({
|
||||
services: {
|
||||
conversation: {
|
||||
resolveImage: (sessionId: string, attachment: unknown) => {
|
||||
uiConversation: {
|
||||
imageUrl: (sessionId: string, attachment: unknown) => {
|
||||
resolved.push([sessionId, attachment])
|
||||
return Promise.resolve('blob:pic')
|
||||
},
|
||||
@@ -646,14 +661,14 @@ describe('ContextView — targeted content fetch and image loading', () => {
|
||||
images: 1,
|
||||
nodes: [{ seq: 1, cat: 'user', tokens: 400, text: 'see this', imgs: 1 }],
|
||||
})),
|
||||
useSession: (sel =>
|
||||
useChat: (sel =>
|
||||
sel({
|
||||
nodes: [{
|
||||
legacy: { nodes: [{
|
||||
kind: 'user',
|
||||
seq: 1,
|
||||
content: [{ type: 'image', attachment: { attachmentId: 'att-1', name: 'pic.png', bytes: 2048, width: 640, height: 480 } }],
|
||||
}],
|
||||
})) as UseSessionLike,
|
||||
}] }
|
||||
})) as UseChatLike,
|
||||
}))
|
||||
const catRow = queryAll(m.container, '.lc-br-cat-row').find(r => text(r).includes(DICT_EN['cat.user']))
|
||||
assert.ok(catRow !== undefined)
|
||||
@@ -666,8 +681,8 @@ describe('ContextView — targeted content fetch and image loading', () => {
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('a conversation service without resolveImage degrades quietly', async () => {
|
||||
const ctx = new TestClientCtx({ services: { conversation: {} } })
|
||||
test('a conversation service without imageUrl degrades quietly', async () => {
|
||||
const ctx = new TestClientCtx({ services: { uiConversation: {} } })
|
||||
const View = makeView(ctx)
|
||||
const m = await mount(h(View, {
|
||||
sessionId: 'sv-noimg',
|
||||
@@ -772,11 +787,13 @@ describe('ContextView — locale and settings', () => {
|
||||
{ seq: 9, cat: 'tool', tokens: 30, tool: 'read', text: 'z', time: T0 + 8000 },
|
||||
],
|
||||
})),
|
||||
useSession: (sel => sel({ nodes: [
|
||||
useChat: (sel =>
|
||||
sel({
|
||||
legacy: { nodes: [
|
||||
{ kind: 'tool', seq: 7, call: { name: 'read', argsRaw: JSON.stringify({ file_path: '/z.ts' }) } },
|
||||
{ kind: 'tool', seq: 8, call: { name: 'read', argsRaw: JSON.stringify({ file_path: '/a.ts' }) } },
|
||||
{ kind: 'tool', seq: 9, call: { name: 'read', argsRaw: JSON.stringify({ file_path: '/z.ts' }) } },
|
||||
] })) as UseSessionLike,
|
||||
] } })) as UseChatLike,
|
||||
}))
|
||||
assert.ok(buttonByText(m.container, DICT_EN['gran.turn']).className.includes('lc-gran-on'))
|
||||
assert.ok(buttonByText(m.container, DICT_EN['gran.delta']).className.includes('lc-gran-on'))
|
||||
|
||||
+137
-230
@@ -160,138 +160,96 @@ describe('pageNodesOf — the durable-page mapper', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/** Arm the DECLARED inject with one `remote` facade (the way the harness
|
||||
* serves it) and prove the fetchers read through that path only. The direct
|
||||
* `remote.session` service is hostile on purpose — an undeclared read of the
|
||||
* traced proxy throws on the real host, so the declared path must be the one
|
||||
* that carries the read. Dispose the returned ctx to unload the slot. */
|
||||
function armHistoryFaces(remote: unknown): TestClientCtx {
|
||||
const ctx = new TestClientCtx()
|
||||
ctx.setService('remote', remote)
|
||||
ctx.setService('remote.session', { get page() { throw new Error('cannot get property "remote.session" without inject') } })
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
return ctx
|
||||
}
|
||||
|
||||
/** One raw durable-log event, as a page record carries it. */
|
||||
const ev = (type: string, seq: number, data: unknown) => ({ type, seq, time: seq, data })
|
||||
|
||||
/** The gateway session-page face: records of `{type:'event', event}` rows. */
|
||||
const pageFace = (events: unknown[], calls: unknown[] = [], envelope: (records: unknown[]) => unknown = (r) => ({ ok: true, value: { records: r } })) => ({
|
||||
page: (request: unknown) => {
|
||||
calls.push(request)
|
||||
return Promise.resolve(envelope(events.map(event => ({ type: 'event', event }))))
|
||||
},
|
||||
})
|
||||
|
||||
describe('makeContentFetcher — the per-session targeted read', () => {
|
||||
/** A minimal cordis-like ctx over one service map. */
|
||||
function ctxWith(services: Record<string, unknown>): Parameters<typeof makeContentFetcher>[0] {
|
||||
return { get: (key: string) => services[key] } as Parameters<typeof makeContentFetcher>[0]
|
||||
}
|
||||
|
||||
const apiFace = (events: unknown[], calls: { beforeSeq: number }[] = []) => ({
|
||||
sessions: {
|
||||
history: (request: { sessionId: string; beforeSeq: number }) => {
|
||||
calls.push({ beforeSeq: request.beforeSeq })
|
||||
return Promise.resolve({ result: { ok: true, value: { events } } })
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
test('undefined without the connection face or the history verb (older hosts)', () => {
|
||||
assert.equal(makeContentFetcher(ctxWith({}), 's'), undefined)
|
||||
assert.equal(makeContentFetcher(ctxWith({ connection: {} }), 's'), undefined)
|
||||
assert.equal(makeContentFetcher(ctxWith({ connection: { api: { sessions: {} } } }), 's'), undefined)
|
||||
test('undefined while the declared slot holds no face', () => {
|
||||
assert.equal(makeContentFetcher('s'), undefined)
|
||||
})
|
||||
|
||||
test('fetches the page anchored past the seq, maps it, and caches the hit', async () => {
|
||||
const calls: { beforeSeq: number }[] = []
|
||||
const fetch = makeContentFetcher(ctxWith({
|
||||
connection: { api: apiFace([
|
||||
row('user/message', 7, { content: [{ type: 'text', text: 'CACHED BODY' }] }),
|
||||
], calls) },
|
||||
}), 'sess-1')!
|
||||
const calls: unknown[] = []
|
||||
const ctx = armHistoryFaces({ session: pageFace([
|
||||
ev('user/message', 7, { content: [{ type: 'text', text: 'CACHED BODY' }] }),
|
||||
], calls) })
|
||||
const fetch = makeContentFetcher('sess-1')!
|
||||
assert.deepEqual(calls, [])
|
||||
const node = await fetch(7)
|
||||
assert.deepEqual(calls, [{ beforeSeq: 8 }])
|
||||
assert.deepEqual(calls, [{ address: { kind: 'session', sessionId: 'sess-1' }, throughSeq: 7, beforeSeq: 8 }])
|
||||
assert.equal((node?.content as { text: string }[])[0]?.text, 'CACHED BODY')
|
||||
// Second read hits the closure cache — no second RPC.
|
||||
const again = await fetch(7)
|
||||
assert.equal(again, node)
|
||||
assert.deepEqual(calls.length, 1)
|
||||
assert.equal(calls.length, 1)
|
||||
// Unload clears the slot: nothing stales across plugin reloads.
|
||||
ctx.dispose()
|
||||
assert.equal(makeContentFetcher('sess-1'), undefined, 'the unloaded slot leaves no face behind')
|
||||
})
|
||||
|
||||
test('a page without the seq resolves null; an empty events page too', async () => {
|
||||
const fetch = makeContentFetcher(ctxWith({ connection: { api: apiFace([row('user/message', 9, {})]) } }), 's')!
|
||||
test('a page without the seq resolves null; an empty records page too', async () => {
|
||||
const calls: unknown[] = []
|
||||
const ctx = armHistoryFaces({ session: pageFace([ev('user/message', 9, {})], calls) })
|
||||
const fetch = makeContentFetcher('s')!
|
||||
assert.equal(await fetch(500), null)
|
||||
const empty = makeContentFetcher(ctxWith({
|
||||
connection: { api: { sessions: { history: async () => ({ result: { ok: true, value: { events: [] } } }) } } },
|
||||
}), 's')!
|
||||
assert.equal(await empty(1), null)
|
||||
ctx.dispose()
|
||||
const empty = armHistoryFaces({ session: pageFace([]) })
|
||||
assert.equal(await makeContentFetcher('s')!(1), null)
|
||||
empty.dispose()
|
||||
})
|
||||
|
||||
test('a payload with no rows array at all rejects (never claims absence)', async () => {
|
||||
const garbage = makeContentFetcher(ctxWith({
|
||||
connection: { api: { sessions: { history: async () => ({ result: { ok: true, value: {} } }) } } },
|
||||
}), 's')!
|
||||
await assert.rejects(garbage(1))
|
||||
})
|
||||
|
||||
test('a throwing service read degrades to no fetcher', () => {
|
||||
const hostile = (throwing: string): Parameters<typeof makeContentFetcher>[0] => ({
|
||||
get: (key: string) => {
|
||||
if (key === throwing) throw new Error('service absent')
|
||||
return undefined
|
||||
},
|
||||
}) as Parameters<typeof makeContentFetcher>[0]
|
||||
assert.equal(makeContentFetcher(hostile('remote.session'), 's'), undefined)
|
||||
assert.equal(makeContentFetcher(hostile('connection'), 's'), undefined)
|
||||
})
|
||||
|
||||
// The issue-42 failure: the injected-service PROXY throws on the `.page`
|
||||
// read itself ("cannot get property "remote.session" without inject") —
|
||||
// the guard around `ctx.get` alone cannot catch it. A throwing getter must
|
||||
// degrade to the legacy face (or to no face), never out of the fetcher.
|
||||
test('a throwing service-property read degrades to the legacy face', async () => {
|
||||
const calls: { beforeSeq: number }[] = []
|
||||
const hostilePage = {
|
||||
get page() {
|
||||
throw new Error('cannot get property "remote.session" without inject')
|
||||
},
|
||||
}
|
||||
const fetch = makeContentFetcher(ctxWith({
|
||||
'remote.session': hostilePage,
|
||||
connection: { api: apiFace([row('user/message', 7, { content: [{ type: 'text', text: 'LEGACY BODY' }] })], calls) },
|
||||
}), 's')!
|
||||
const node = await fetch(7)
|
||||
assert.deepEqual(calls, [{ beforeSeq: 8 }], 'the throwing page face falls back to the legacy verb')
|
||||
assert.equal((node?.content as { text: string }[])[0]?.text, 'LEGACY BODY')
|
||||
// No legacy face beneath it: the fetcher itself degrades, no throw.
|
||||
assert.equal(makeContentFetcher(ctxWith({ 'remote.session': hostilePage }), 's'), undefined)
|
||||
})
|
||||
|
||||
test('a throwing connection-property chain degrades to no fetcher', () => {
|
||||
const hostileApi = {
|
||||
get api() {
|
||||
throw new Error('cannot get property "api" without inject')
|
||||
},
|
||||
}
|
||||
assert.equal(makeContentFetcher(ctxWith({ connection: hostileApi }), 's'), undefined)
|
||||
assert.equal(makeContentFetcher(ctxWith({ connection: { api: { get sessions() { throw new Error('hostile sessions') } } } }), 's'), undefined)
|
||||
})
|
||||
|
||||
test('the declared remote.session inject carries the page read and unloads cleanly', async () => {
|
||||
// The harness mirrors the traced `remote` service: factory→`.session`
|
||||
// is the facade property path the DECLARED fiber resolves. The direct
|
||||
// `remote.session` service value is hostile on purpose — the injection
|
||||
// path must be the one that carries the read.
|
||||
const calls: unknown[] = []
|
||||
const declared = {
|
||||
session: {
|
||||
page(request: unknown, _signal: AbortSignal): Promise<unknown> {
|
||||
calls.push(request)
|
||||
return Promise.resolve({ ok: true, value: { records: [] } })
|
||||
},
|
||||
},
|
||||
}
|
||||
const ctx = new TestClientCtx()
|
||||
ctx.setService('remote', declared)
|
||||
ctx.setService('remote.session', { get page() { throw new Error('cannot get property "remote.session" without inject') } })
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
const fetch = makeContentFetcher(asClientCtx(ctx), 'sess-1')!
|
||||
const node = await fetch(7)
|
||||
assert.deepEqual(calls, [{ address: { kind: 'session', sessionId: 'sess-1' }, throughSeq: 7, beforeSeq: 8 }])
|
||||
assert.equal(node, null, 'the declared page face mapped the (empty) page')
|
||||
// Unload clears the slot: the hostile reflect read (and the absent
|
||||
// legacy face) then degrade to no fetcher — nothing stales across
|
||||
// plugin reloads.
|
||||
const ctx = armHistoryFaces({ session: pageFace([], [], () => ({ ok: true, value: {} })) })
|
||||
await assert.rejects(makeContentFetcher('s')!(1))
|
||||
ctx.dispose()
|
||||
assert.equal(makeContentFetcher(asClientCtx(ctx), 's'), undefined, 'the unloaded slot leaves no face behind')
|
||||
})
|
||||
|
||||
test('a hostile or shapeless declared face leaves the slot unset (issue #42)', () => {
|
||||
// The traced proxy can throw on the property READ itself, or serve a
|
||||
// null / primitive / verb-less / hostile-verb face — the declared
|
||||
// callback contains all of it, the slot stays unset, and the fetcher
|
||||
// degrades instead of taking the view down.
|
||||
const remotes = [
|
||||
{ get session() { throw new Error('traced read failed') } },
|
||||
{ session: null },
|
||||
{ session: 'x' },
|
||||
{ session: { get page() { throw new Error('hostile page read') } } },
|
||||
{ session: { page: 42 } },
|
||||
]
|
||||
for (const remote of remotes) {
|
||||
const ctx = armHistoryFaces(remote)
|
||||
assert.equal(makeContentFetcher('s'), undefined)
|
||||
ctx.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
test('the declared inject never fires without both remote services', () => {
|
||||
const ctx = new TestClientCtx()
|
||||
// Only `remote`: the injected fiber requires BOTH names, so nothing
|
||||
// fires and the legacy verb carries the reads (the pre-0.1.2 line).
|
||||
// Only `remote`: the injected fiber requires BOTH names, so nothing fires.
|
||||
ctx.setService('remote', { session: {} })
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
assert.equal(makeContentFetcher('s'), undefined)
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
@@ -302,190 +260,139 @@ describe('makeContentFetcher — the per-session targeted read', () => {
|
||||
ctx.setService('remote', {})
|
||||
ctx.setService('remote.session', {})
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
assert.equal(makeContentFetcher(asClientCtx(ctx), 's'), undefined)
|
||||
assert.equal(makeContentFetcher('s'), undefined)
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
test('the 0.1.2 page face wins over the legacy history verb and maps its records', async () => {
|
||||
test('the page face maps its records', async () => {
|
||||
const calls: unknown[] = []
|
||||
// The real 0.1.2 shape: the remote resolves to the ClientResult itself.
|
||||
const pageFace = (records: unknown[], envelope: (records: unknown[]) => unknown = (r) => ({ ok: true, value: { records: r } })) => ({
|
||||
page: (request: unknown) => {
|
||||
calls.push(request)
|
||||
return Promise.resolve(envelope(records))
|
||||
},
|
||||
})
|
||||
// The real shape: the remote resolves to the ClientResult itself.
|
||||
const records = [
|
||||
{ type: 'event', event: { type: 'user/message', seq: 7, time: 7, data: { content: [{ type: 'text', text: 'PAGED BODY' }] } } },
|
||||
{ type: 'event', event: ev('user/message', 7, { content: [{ type: 'text', text: 'PAGED BODY' }] }) },
|
||||
// A packed chunk-row record projects to nothing — only final events matter.
|
||||
{ type: 'chunks', event: { type: 'chunkrow/text-chunks', seq: 8, time: 8, data: { turn: 1, step: 0, index: 0, dt: [1], texts: ['delta'] } } },
|
||||
]
|
||||
const historyCalls: unknown[] = []
|
||||
const fetch = makeContentFetcher(ctxWith({
|
||||
'remote.session': pageFace(records),
|
||||
connection: { api: { sessions: { history: () => { historyCalls.push(1); return Promise.resolve({}) } } } },
|
||||
}), 'sess-9')!
|
||||
const node = await fetch(7)
|
||||
const ctx = armHistoryFaces({ session: pageFace([], calls, () => ({ ok: true, value: { records } })) })
|
||||
const node = await makeContentFetcher('sess-9')!(7)
|
||||
assert.deepEqual(calls, [{
|
||||
address: { kind: 'session', sessionId: 'sess-9' },
|
||||
throughSeq: 7,
|
||||
beforeSeq: 8,
|
||||
}])
|
||||
assert.equal((node?.content as { text: string }[])[0]?.text, 'PAGED BODY')
|
||||
assert.equal(historyCalls.length, 0, 'the legacy verb stays untouched')
|
||||
ctx.dispose()
|
||||
// A bare (non-enveloped) SessionPage passes too.
|
||||
const bare = makeContentFetcher(ctxWith({ 'remote.session': pageFace(records, (r) => ({ records: r })) }), 's')!
|
||||
assert.equal((await bare(7))?.kind, 'user')
|
||||
const bare = armHistoryFaces({ session: pageFace([], [], () => ({ records })) })
|
||||
assert.equal((await makeContentFetcher('s')!(7))?.kind, 'user')
|
||||
bare.dispose()
|
||||
})
|
||||
|
||||
test('page-face failures reject so the caller can offer a retry', async () => {
|
||||
const bad = makeContentFetcher(ctxWith({
|
||||
'remote.session': { page: async () => ({ result: { ok: false, value: undefined } }) },
|
||||
}), 's')!
|
||||
await assert.rejects(bad(1))
|
||||
const badTop = makeContentFetcher(ctxWith({
|
||||
'remote.session': { page: async () => ({ ok: false, error: { message: 'gone' } }) },
|
||||
}), 's')!
|
||||
await assert.rejects(badTop(1))
|
||||
const garbage = makeContentFetcher(ctxWith({
|
||||
'remote.session': { page: async () => ({ records: 'nope' }) },
|
||||
}), 's')!
|
||||
await assert.rejects(garbage(1))
|
||||
const broken = makeContentFetcher(ctxWith({
|
||||
'remote.session': { page: async () => { throw new Error('transport down') } } ,
|
||||
}), 's')!
|
||||
await assert.rejects(broken(1), /transport down/)
|
||||
const badTop = armHistoryFaces({ session: { page: async () => ({ ok: false, error: { message: 'gone' } }) } })
|
||||
await assert.rejects(makeContentFetcher('s')!(1))
|
||||
badTop.dispose()
|
||||
const garbage = armHistoryFaces({ session: { page: async () => ({ records: 'nope' }) } })
|
||||
await assert.rejects(makeContentFetcher('s')!(1))
|
||||
garbage.dispose()
|
||||
const broken = armHistoryFaces({ session: { page: async () => { throw new Error('transport down') } } })
|
||||
await assert.rejects(makeContentFetcher('s')!(1), /transport down/)
|
||||
broken.dispose()
|
||||
})
|
||||
|
||||
test('malformed rpc envelopes reject so the caller can offer a retry', async () => {
|
||||
const fetch = makeContentFetcher(ctxWith({
|
||||
connection: { api: { sessions: { history: async () => ({ result: { ok: false, value: undefined } }) } } },
|
||||
}), 's')!
|
||||
await assert.rejects(fetch(1))
|
||||
const nullResponse = makeContentFetcher(ctxWith({
|
||||
connection: { api: { sessions: { history: async () => null } } },
|
||||
}), 's')!
|
||||
await assert.rejects(nullResponse(1))
|
||||
const noResult = makeContentFetcher(ctxWith({
|
||||
connection: { api: { sessions: { history: async () => ({}) } } },
|
||||
}), 's')!
|
||||
await assert.rejects(noResult(1))
|
||||
const scalarValue = makeContentFetcher(ctxWith({
|
||||
connection: { api: { sessions: { history: async () => ({ result: { ok: true, value: 'nope' } }) } } },
|
||||
}), 's')!
|
||||
await assert.rejects(scalarValue(1))
|
||||
const broken = makeContentFetcher(ctxWith({
|
||||
connection: { api: { sessions: { history: async () => { throw new Error('transport down') } } } },
|
||||
}), 's')!
|
||||
await assert.rejects(broken(1), /transport down/)
|
||||
const nullResponse = armHistoryFaces({ session: { page: async () => null } })
|
||||
await assert.rejects(makeContentFetcher('s')!(1))
|
||||
nullResponse.dispose()
|
||||
const noResult = armHistoryFaces({ session: { page: async () => ({}) } })
|
||||
await assert.rejects(makeContentFetcher('s')!(1))
|
||||
noResult.dispose()
|
||||
const scalarValue = armHistoryFaces({ session: { page: async () => ({ ok: true, value: 'nope' }) } })
|
||||
await assert.rejects(makeContentFetcher('s')!(1))
|
||||
scalarValue.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeHeaderFetcher — the lazy epoch content read', () => {
|
||||
function ctxWith(services: Record<string, unknown>): Parameters<typeof makeHeaderFetcher>[0] {
|
||||
return { get: (key: string) => services[key] } as Parameters<typeof makeHeaderFetcher>[0]
|
||||
}
|
||||
|
||||
const apiFace = (events: unknown[], calls: { beforeSeq: number }[] = []) => ({
|
||||
sessions: {
|
||||
history: (request: { sessionId: string; beforeSeq: number }) => {
|
||||
calls.push({ beforeSeq: request.beforeSeq })
|
||||
return Promise.resolve({ result: { ok: true, value: { events } } })
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
test('undefined without the history faces (older hosts)', () => {
|
||||
assert.equal(makeHeaderFetcher(ctxWith({}), 's'), undefined)
|
||||
assert.equal(makeHeaderFetcher(ctxWith({ connection: {} }), 's'), undefined)
|
||||
assert.equal(makeHeaderFetcher(ctxWith({ connection: { api: { sessions: {} } } }), 's'), undefined)
|
||||
test('undefined while the declared slot holds no face', () => {
|
||||
assert.equal(makeHeaderFetcher('s'), undefined)
|
||||
})
|
||||
|
||||
test('fetches the epoch page, maps the raw header, and caches per seq', async () => {
|
||||
const calls: { beforeSeq: number }[] = []
|
||||
const calls: unknown[] = []
|
||||
const headerEvent = {
|
||||
event: {
|
||||
type: 'request/header', seq: 5, time: 5,
|
||||
data: {
|
||||
header: {
|
||||
system: 'You are an agent.',
|
||||
tools: [
|
||||
{ name: 'bash', description: 'run a command', parameters: { type: 'object' } },
|
||||
{ name: 'mcp__gh__issue', description: '', plugin: 'mcp:github' },
|
||||
null, // hostile entry degrades to an unnamed row
|
||||
42,
|
||||
],
|
||||
},
|
||||
type: 'request/header', seq: 5, time: 5,
|
||||
data: {
|
||||
header: {
|
||||
system: 'You are an agent.',
|
||||
tools: [
|
||||
{ name: 'bash', description: 'run a command', parameters: { type: 'object' } },
|
||||
{ name: 'mcp__gh__issue', description: '', plugin: 'mcp:github' },
|
||||
null, // hostile entry degrades to an unnamed row
|
||||
42,
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
const fetch = makeHeaderFetcher(ctxWith({ connection: { api: apiFace([headerEvent], calls) } }), 'sess')!
|
||||
const ctx = armHistoryFaces({ session: pageFace([headerEvent], calls) })
|
||||
const fetch = makeHeaderFetcher('sess')!
|
||||
const content = await fetch(5)
|
||||
assert.deepEqual(calls, [{ beforeSeq: 6 }])
|
||||
assert.deepEqual(calls, [{ address: { kind: 'session', sessionId: 'sess' }, throughSeq: 5, beforeSeq: 6 }])
|
||||
assert.equal(content?.system, 'You are an agent.')
|
||||
assert.equal(content?.tools.length, 4)
|
||||
assert.equal(content?.tools[0]?.name, 'bash')
|
||||
assert.equal(content?.tools[0]?.description, 'run a command')
|
||||
assert.deepEqual(content?.tools[0]?.schema, headerEvent.event.data.header.tools[0])
|
||||
assert.deepEqual(content?.tools[0]?.schema, headerEvent.data.header.tools[0])
|
||||
assert.equal(content?.tools[1]?.description, undefined, 'empty description omitted')
|
||||
assert.equal(content?.tools[1]?.schema, headerEvent.event.data.header.tools[1])
|
||||
assert.equal(content?.tools[1]?.schema, headerEvent.data.header.tools[1])
|
||||
assert.equal(content?.tools[2]?.name, '?')
|
||||
assert.equal(content?.tools[3]?.name, '?')
|
||||
// Cached: the second read of the epoch costs no RPC.
|
||||
assert.equal(await fetch(5), content)
|
||||
assert.equal(calls.length, 1)
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
test('an epoch with no system text and an absent tools list maps to empty tools', async () => {
|
||||
const fetch = makeHeaderFetcher(ctxWith({
|
||||
connection: { api: apiFace([row('request/header', 2, { header: {} })]) },
|
||||
}), 's')!
|
||||
const content = await fetch(2)
|
||||
const ctx = armHistoryFaces({ session: pageFace([ev('request/header', 2, { header: {} })]) })
|
||||
const content = await makeHeaderFetcher('s')!(2)
|
||||
assert.deepEqual(content, { tools: [] })
|
||||
assert.equal('system' in (content ?? {}), false)
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
test('a page holding OLDER epochs caches them for free alongside the picked one', async () => {
|
||||
const older = row('request/header', 1, { header: { system: 'OLD', tools: [] } })
|
||||
const picked = row('request/header', 5, { header: { system: 'NEW', tools: [] } })
|
||||
const calls: { beforeSeq: number }[] = []
|
||||
const fetch = makeHeaderFetcher(ctxWith({ connection: { api: apiFace([older, picked], calls) } }), 's')!
|
||||
const older = ev('request/header', 1, { header: { system: 'OLD', tools: [] } })
|
||||
const picked = ev('request/header', 5, { header: { system: 'NEW', tools: [] } })
|
||||
const calls: unknown[] = []
|
||||
const ctx = armHistoryFaces({ session: pageFace([older, picked], calls) })
|
||||
const fetch = makeHeaderFetcher('s')!
|
||||
const c5 = await fetch(5)
|
||||
assert.equal(c5?.system, 'NEW')
|
||||
assert.deepEqual(calls, [{ beforeSeq: 6 }])
|
||||
assert.deepEqual(calls, [{ address: { kind: 'session', sessionId: 's' }, throughSeq: 5, beforeSeq: 6 }])
|
||||
// The older epoch resolves from the cache — no second page read.
|
||||
const c1 = await fetch(1)
|
||||
assert.equal(c1?.system, 'OLD')
|
||||
assert.deepEqual(calls, [{ beforeSeq: 6 }])
|
||||
assert.equal(calls.length, 1)
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
test('a page without the epoch resolves null; a non-header-only page too', async () => {
|
||||
const fetch = makeHeaderFetcher(ctxWith({
|
||||
connection: { api: apiFace([row('user/message', 9, {}), row('request/header', 3, {})]) },
|
||||
}), 's')!
|
||||
const ctx = armHistoryFaces({ session: pageFace([ev('user/message', 9, {}), ev('request/header', 3, {})]) })
|
||||
const fetch = makeHeaderFetcher('s')!
|
||||
assert.equal(await fetch(500), null, 'the seq is not on the page')
|
||||
assert.equal(await fetch(3), null, 'a header envelope without a header object maps to nothing')
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
test('malformed rpc envelopes reject so the caller can offer a retry', async () => {
|
||||
const rejector = makeHeaderFetcher(ctxWith({
|
||||
connection: { api: { sessions: { history: async () => ({ result: { ok: false, value: undefined } }) } } },
|
||||
}), 's')!
|
||||
await assert.rejects(rejector(1))
|
||||
const rowsGarbage = makeHeaderFetcher(ctxWith({
|
||||
'remote.session': { page: async () => ({ ok: true, value: { records: 'nope' } }) },
|
||||
}), 's')!
|
||||
await assert.rejects(rowsGarbage(1))
|
||||
const broken = makeHeaderFetcher(ctxWith({
|
||||
'remote.session': { page: async () => { throw new Error('transport down') } },
|
||||
}), 's')!
|
||||
await assert.rejects(broken(1), /transport down/)
|
||||
})
|
||||
|
||||
test('a throwing service read degrades to no fetcher', () => {
|
||||
const hostile = {
|
||||
get: () => { throw new Error('service absent') },
|
||||
} as unknown as Parameters<typeof makeHeaderFetcher>[0]
|
||||
assert.equal(makeHeaderFetcher(hostile, 's'), undefined)
|
||||
const rejector = armHistoryFaces({ session: { page: async () => ({ ok: false, value: undefined }) } })
|
||||
await assert.rejects(makeHeaderFetcher('s')!(1))
|
||||
rejector.dispose()
|
||||
const rowsGarbage = armHistoryFaces({ session: { page: async () => ({ ok: true, value: { records: 'nope' } }) } })
|
||||
await assert.rejects(makeHeaderFetcher('s')!(1))
|
||||
rowsGarbage.dispose()
|
||||
const broken = armHistoryFaces({ session: { page: async () => { throw new Error('transport down') } } })
|
||||
await assert.rejects(makeHeaderFetcher('s')!(1), /transport down/)
|
||||
broken.dispose()
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,7 @@
|
||||
// Projection narrowing (src/client/services.ts): the no-white-screen wire
|
||||
// guards — numOf, timelineOf fast/slow paths, contextPressureOf,
|
||||
// contextBreakdownOf, tokenUsageOf, timingOf, headersOf — plus the dual-face helpers
|
||||
// that keep the plugin alive across the dsh 0.1.1/0.1.2 service seams
|
||||
// (conversationNodesOf, imageLoaderOf).
|
||||
// contextBreakdownOf, tokenUsageOf, timingOf, headersOf — plus the seat
|
||||
// helpers over the harness service faces (conversationNodesOf, imageLoaderOf).
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -26,58 +25,39 @@ import type { TimingTotals } from '../../src/shared/types'
|
||||
/** The chat-view snapshot face: the selector sees the whole snapshot. */
|
||||
const useChatOf = (snapshot: unknown) => (<T>(sel: (s: unknown) => T) => sel(snapshot))
|
||||
|
||||
describe('conversationNodesOf — the dual-seat window join', () => {
|
||||
describe('conversationNodesOf — the chat-seat window join', () => {
|
||||
const nodes: ConversationNodeLike[] = [{ kind: 'user', seq: 1 }, { kind: 'assistant', seq: 2 }]
|
||||
|
||||
test('prefers the 0.1.2+ chat seat (ChatSnapshot.legacy.nodes)', () => {
|
||||
const out = conversationNodesOf({ useChat: useChatOf({ legacy: { nodes } }), useSession: (sel) => sel({ nodes: [] }) })
|
||||
assert.equal(out, nodes)
|
||||
})
|
||||
|
||||
test('falls back to the pre-0.1.2 session-snapshot nodes', () => {
|
||||
const out = conversationNodesOf({ useSession: (sel) => sel({ nodes }) })
|
||||
test('serves the chat seat (ChatSnapshot.legacy.nodes)', () => {
|
||||
const out = conversationNodesOf({ useChat: useChatOf({ legacy: { nodes } }) })
|
||||
assert.equal(out, nodes)
|
||||
})
|
||||
|
||||
test('absent seats, non-array payloads, and hostile seats degrade to undefined', () => {
|
||||
assert.equal(conversationNodesOf({}), undefined)
|
||||
assert.equal(conversationNodesOf({ useChat: useChatOf({}) }), undefined, 'chat snapshot without legacy')
|
||||
assert.equal(conversationNodesOf({ useSession: (sel) => sel({}) }), undefined, 'session snapshot without nodes')
|
||||
assert.equal(
|
||||
conversationNodesOf({ useChat: useChatOf(null), useSession: (sel) => sel(null as unknown as { nodes?: readonly ConversationNodeLike[] }) }),
|
||||
undefined,
|
||||
'null snapshots',
|
||||
)
|
||||
const hostile = () => { throw new Error('boom') }
|
||||
assert.equal(conversationNodesOf({ useChat: hostile, useSession: hostile }), undefined)
|
||||
// A hostile chat seat still lets the session seat answer — both are probed.
|
||||
const out = conversationNodesOf({ useChat: hostile, useSession: (sel) => sel({ nodes }) })
|
||||
assert.equal(out, nodes)
|
||||
assert.equal(conversationNodesOf({ useChat: useChatOf(null) }), undefined, 'null snapshot')
|
||||
assert.equal(conversationNodesOf({ useChat: () => { throw new Error('boom') } }), undefined, 'hostile seat')
|
||||
})
|
||||
})
|
||||
|
||||
describe('imageLoaderOf — the dual-conversation-face loader', () => {
|
||||
describe('imageLoaderOf — the uiConversation loader', () => {
|
||||
const ctxWith = (services: Record<string, unknown>): ClientCtx => ({ get: (name: string) => services[name] }) as unknown as ClientCtx
|
||||
const attachment = { attachmentId: 'a1' }
|
||||
|
||||
test('prefers the legacy resolveImage face (dsh <= 0.1.1)', async () => {
|
||||
test('uses the uiConversation.imageUrl face', async () => {
|
||||
const calls: string[] = []
|
||||
const load = imageLoaderOf(ctxWith({ conversation: { resolveImage: (sid: string, att: { attachmentId: string }) => {
|
||||
const load = imageLoaderOf(ctxWith({ uiConversation: { imageUrl: (sid: string, att: { attachmentId: string }) => {
|
||||
calls.push(sid + ':' + att.attachmentId)
|
||||
return Promise.resolve('blob:legacy')
|
||||
return Promise.resolve('blob:modern')
|
||||
} } }), 'sv')
|
||||
assert.equal(await load!(attachment), 'blob:legacy')
|
||||
assert.deepEqual(calls, ['sv:a1'])
|
||||
})
|
||||
|
||||
test('uses the 0.1.2+ uiConversation.imageUrl face when the legacy one is absent', async () => {
|
||||
const load = imageLoaderOf(ctxWith({ uiConversation: { imageUrl: () => Promise.resolve('blob:modern') } }), 'sv')
|
||||
assert.equal(await load!(attachment), 'blob:modern')
|
||||
assert.deepEqual(calls, ['sv:a1'])
|
||||
})
|
||||
|
||||
test('absent faces, bad session ids, and hostile service reads degrade to undefined', () => {
|
||||
assert.equal(imageLoaderOf(ctxWith({}), 'sv'), undefined)
|
||||
assert.equal(imageLoaderOf(ctxWith({ conversation: {} }), 'sv'), undefined)
|
||||
assert.equal(imageLoaderOf(ctxWith({ uiConversation: {} }), 'sv'), undefined)
|
||||
assert.equal(imageLoaderOf(ctxWith({}), undefined), undefined)
|
||||
assert.equal(imageLoaderOf(ctxWith({}), ''), undefined)
|
||||
const ctxThrows = { get: (): never => { throw new Error('service absent') } } as unknown as ClientCtx
|
||||
|
||||
@@ -29,7 +29,7 @@ if (reasons.length > 0) {
|
||||
// The always-runnable part (no checkout needed): every specifier the built
|
||||
// bundle requires at runtime must be seeded by EACH baseline's platform
|
||||
// module table — a require the shell cannot answer is a guaranteed boot
|
||||
// crash on that generation (0.1.1's table, for one, lacks dsh-client-store).
|
||||
// crash on that generation.
|
||||
describe.skipIf(staging.artifactsMissing())('compat matrix — bundle requires vs baseline module tables', () => {
|
||||
test.each(BASELINES)('$id: the platform module table answers every bundle require', (baseline) => {
|
||||
assert.deepEqual(
|
||||
@@ -102,25 +102,16 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base
|
||||
)
|
||||
})
|
||||
|
||||
test('client: finalized-nodes seat (useChat on 0.1.2+, session snapshot before)', () => {
|
||||
const ok = baseline.client.chatNodesSeat === 'useChat'
|
||||
? staging.dshHasString(baseline.tag, 'useChat', 'packages/client/ui-chat/src/client/contract/slots.ts')
|
||||
: staging.dshHasString(baseline.tag, 'nodes: legacy.nodes', 'packages/client/runtime/src/client/sessions/session.ts')
|
||||
assert.equal(ok, true)
|
||||
test('client: finalized-nodes seat (useChat ChatSnapshot)', () => {
|
||||
assert.equal(staging.dshHasString(baseline.tag, 'useChat', 'packages/client/ui-chat/src/client/contract/slots.ts'), true)
|
||||
})
|
||||
|
||||
test('client: durable-image loader service', () => {
|
||||
assert.equal(staging.dshHasString(baseline.tag, baseline.client.imageFace.method, 'packages/client/*/src/**'), true)
|
||||
assert.equal(staging.dshHasString(baseline.tag, baseline.client.imageFaceMethod, 'packages/client/*/src/**'), true)
|
||||
})
|
||||
|
||||
test('client: the history face of this generation', () => {
|
||||
if (baseline.client.historyFace === 'remote.session.page') {
|
||||
assert.equal(staging.dshHasString(baseline.tag, "'remote.session'", 'packages/api/*/src/**'), true)
|
||||
} else {
|
||||
assert.equal(staging.dshHasString(baseline.tag, 'api.sessions.history', 'packages/client/connection/src/**'), true)
|
||||
// The plugin must fall back: no gateway session namespace on this line.
|
||||
assert.equal(staging.dshHasString(baseline.tag, "'remote.session'", 'packages/api/*/src/**'), false)
|
||||
}
|
||||
test('client: the gateway history face of this generation', () => {
|
||||
assert.equal(staging.dshHasString(baseline.tag, "'remote.session'", 'packages/api/*/src/**'), true)
|
||||
})
|
||||
|
||||
test('client: MarkdownText chrome prop', () => {
|
||||
|
||||
@@ -243,13 +243,7 @@ export function runDriver(baseline: Baseline): DriverReport {
|
||||
ensureHostDeps(baseline)
|
||||
stageFile(baseline, 'packages/session/session-projection/src/index.ts', join('registry', 'index.ts'))
|
||||
stageFile(baseline, 'packages/session/session-projection/src/types.ts', join('registry', 'types.ts'))
|
||||
// The lossless-JSON probe the projection cache's write path uses (moved
|
||||
// between packages across the baselines; self-contained either way).
|
||||
if (baseline.id === 'v0.1.1-rc.2') {
|
||||
stageFile(baseline, 'packages/core/session/src/json.ts', join('dsh', 'json-values.ts'))
|
||||
} else {
|
||||
stageFile(baseline, 'packages/util/values/src/index.ts', join('dsh', 'json-values.ts'))
|
||||
}
|
||||
stageFile(baseline, 'packages/util/values/src/index.ts', join('dsh', 'json-values.ts'))
|
||||
const driver = join(STAGE, baseline.id, 'driver.mjs')
|
||||
writeFileSync(driver, driverSource(`file://${join(REPO, 'lib', 'index.js')}`))
|
||||
const run = spawnSync(process.execPath, [driver], { cwd: STAGE, encoding: 'utf8', timeout: 60_000 })
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// state that fails the projection cache's plain-JSON write gate (issues
|
||||
// #5-#7, #27-#30 — session creation/titles broke host-wide), a stale-row
|
||||
// restore that must seed through stateSchema, and an init that must tolerate
|
||||
// the 0.1.2 header argument.
|
||||
// the registry's header argument.
|
||||
//
|
||||
// The real-code complement — the ACTUAL dsh registry sources per tag — runs
|
||||
// in the `compat` vitest project (tests/compat/matrix.spec.ts).
|
||||
@@ -68,7 +68,7 @@ function bootSession(baselineIndex: number, log = fullLog()): { driver: Registry
|
||||
const driver = new RegistryDriver(baseline)
|
||||
driver.register(createContextTimelineDefinition({}))
|
||||
driver.register(createContextHeadersDefinition())
|
||||
const session: SessionLike = { seq: 0, header: { id: 's-matrix', cwd: '/tmp' }, events: [] }
|
||||
const session: SessionLike = { seq: 0, header: { id: 's-matrix', cwd: '/tmp' }, inheritedEventCount: 0, events: [] }
|
||||
driver.sessionCreated(session)
|
||||
for (const event of log) {
|
||||
session.events[event.seq] = event
|
||||
@@ -92,19 +92,19 @@ for (const [index, baseline] of BASELINES.entries()) {
|
||||
assert.equal(headers.headers[0].tools[0].name, 'bash')
|
||||
})
|
||||
|
||||
test('init tolerates the baseline call shape: with the header arg when the registry passes one, without before', () => {
|
||||
test('init tolerates the registry call shape: the header and inherited-count arguments go unobserved', () => {
|
||||
const timelineDef = createContextTimelineDefinition({})
|
||||
const headersDef = createContextHeadersDefinition()
|
||||
const probe = { id: 's-init', cwd: '/tmp' }
|
||||
for (const def of [timelineDef, headersDef]) {
|
||||
const bare = (def.init as () => unknown)()
|
||||
const headed = (def.init as (header: unknown) => unknown)(probe)
|
||||
assert.deepEqual(bare, headed, 'the zero-argument init ignores the 0.1.2 header argument')
|
||||
const headed = (def.init as (header: unknown, inheritedEventCount: number) => unknown)(probe, 7)
|
||||
assert.deepEqual(bare, headed, 'the zero-argument init ignores the registry arguments')
|
||||
}
|
||||
// And through the driver, whose init arity follows the baseline.
|
||||
// And through the driver, whose init arity follows the registry's.
|
||||
const driver = new RegistryDriver(baseline)
|
||||
driver.register(createContextTimelineDefinition({}))
|
||||
const session: SessionLike = { seq: 0, header: probe, events: [] }
|
||||
const session: SessionLike = { seq: 0, header: probe, inheritedEventCount: 7, events: [] }
|
||||
driver.sessionCreated(session)
|
||||
assert.equal(driver.snapshot(session).values.contextTimeline !== undefined, true)
|
||||
})
|
||||
@@ -263,10 +263,10 @@ for (const [index, baseline] of BASELINES.entries()) {
|
||||
}
|
||||
|
||||
describe('registry contract — settings namespace seam', () => {
|
||||
// Both baselines enforce `/^[a-z][a-z0-9-]*$/` on registered namespaces —
|
||||
// inside the removed `settingsNamespace()` helper before 0.1.2-alpha.2,
|
||||
// inside `settings.register` since. The plugin registers the raw literal
|
||||
// (branded cast), so the literal must pass the pattern on every baseline.
|
||||
// The settings module enforces `/^[a-z][a-z0-9-]*$/` on registered
|
||||
// namespaces (inside `settings.register` on the supported baselines). The
|
||||
// plugin registers the raw literal (branded cast), so the literal must
|
||||
// pass the pattern.
|
||||
const NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*$/
|
||||
const SETTINGS_NAMESPACE = 'dsh-context'
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
// source at each baseline tag so the always-on suite pins the plugin's units
|
||||
// against every supported registry generation:
|
||||
//
|
||||
// dsh-v0.1.1-rc.2 packages/session/session-projection/src/index.ts
|
||||
// dsh-v0.1.2-alpha.2 same file (delta: init(header) + session/created seeding)
|
||||
// dsh-v0.1.2-rc.1 packages/session/session-projection/src/index.ts
|
||||
//
|
||||
// The real-code matrix (scripts/compat-matrix.mjs) runs the ACTUAL registry
|
||||
// The real-code matrix (tests/compat/matrix.spec.ts) runs the ACTUAL registry
|
||||
// sources per tag; this driver exists so `pnpm test` alone still detects a
|
||||
// definition that drifts from any supported generation's rules (a missing
|
||||
// wire block, a non-plain-JSON state, an init that cannot take the header…).
|
||||
@@ -17,7 +16,7 @@ import type { Baseline } from '../../baselines'
|
||||
interface ErasedDefinition {
|
||||
key: string
|
||||
stateSchema: { parse(value: unknown): unknown }
|
||||
init(header: unknown): unknown
|
||||
init(header: unknown, inheritedEventCount: number): unknown
|
||||
apply(state: unknown, event: unknown): unknown
|
||||
wire: { viewSchema: { parse(value: unknown): unknown }; view(state: unknown): unknown } | undefined
|
||||
stateVersion: number
|
||||
@@ -41,6 +40,8 @@ interface UnitCell {
|
||||
export interface SessionLike {
|
||||
seq: number
|
||||
header: unknown
|
||||
/** Inherited events from pre-identity log compaction (registry hands it to init). */
|
||||
inheritedEventCount: number
|
||||
events: unknown[]
|
||||
}
|
||||
|
||||
@@ -60,8 +61,8 @@ export interface CheckpointRow {
|
||||
export type Checkpoint = Record<string, CheckpointRow>
|
||||
|
||||
/**
|
||||
* The registry's register-time rules (both baselines): a non-negative safe
|
||||
* integer `stateVersion`, and a shared key must agree on it.
|
||||
* The registry's register-time rules: a non-negative safe integer
|
||||
* `stateVersion`, and a shared key must agree on the version.
|
||||
*/
|
||||
export class RegistryViolationError extends Error {}
|
||||
|
||||
@@ -79,11 +80,6 @@ export class RegistryDriver {
|
||||
if (!Number.isSafeInteger(definition.stateVersion) || definition.stateVersion < 0) {
|
||||
throw new RegistryViolationError(`stateVersion must be a non-negative integer, got ${String(definition.stateVersion)}`)
|
||||
}
|
||||
if (definition.wire !== undefined
|
||||
&& (typeof definition.wire.view !== 'function'
|
||||
|| typeof definition.wire.viewSchema?.parse !== 'function')) {
|
||||
throw new RegistryViolationError('a wire block needs viewSchema.parse + view')
|
||||
}
|
||||
const rawWire = definition.wire
|
||||
const wire = rawWire === undefined ? undefined : {
|
||||
viewSchema: rawWire.viewSchema,
|
||||
@@ -92,9 +88,9 @@ export class RegistryDriver {
|
||||
const def: ErasedDefinition = {
|
||||
key: definition.key,
|
||||
stateSchema: definition.stateSchema,
|
||||
// dsh 0.1.2-alpha.1+ calls init(header); before that, init() — the
|
||||
// registry's arity is the BASELINE's, not the definition's.
|
||||
init: header => this.baseline.host.initHeader ? definition.init(header) : definition.init(),
|
||||
// The registry calls init(header, inheritedEventCount); the plugin's
|
||||
// zero-argument init observes neither argument.
|
||||
init: (header, inheritedEventCount) => definition.init(header, inheritedEventCount),
|
||||
apply: (state, event) => definition.apply(state, event),
|
||||
wire,
|
||||
stateVersion: definition.stateVersion,
|
||||
@@ -116,13 +112,12 @@ export class RegistryDriver {
|
||||
this.listeners.add(listener)
|
||||
}
|
||||
|
||||
/** The `session/created` eager seeding (dsh 0.1.2-alpha.1+ only). */
|
||||
/** The `session/created` eager seeding (seq-0 sessions only). */
|
||||
sessionCreated(session: SessionLike): void {
|
||||
if (!this.baseline.host.seedsOnCreate) return
|
||||
if (session.seq !== 0) return
|
||||
for (const { def, cells } of this.registrations.values()) {
|
||||
if (cells.has(session)) continue
|
||||
cells.set(session, { state: def.init(session.header), observedSeq: -1 })
|
||||
cells.set(session, { state: def.init(session.header, session.inheritedEventCount), observedSeq: -1 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +127,7 @@ export class RegistryDriver {
|
||||
let cell = cells.get(session)
|
||||
if (cell !== undefined && cell.observedSeq >= event.seq) continue
|
||||
if (cell === undefined) {
|
||||
cell = { state: def.init(session.header), observedSeq: -1 }
|
||||
cell = { state: def.init(session.header, session.inheritedEventCount), observedSeq: -1 }
|
||||
cells.set(session, cell)
|
||||
}
|
||||
const previous = cell.state
|
||||
@@ -151,7 +146,7 @@ export class RegistryDriver {
|
||||
if (def.wire === undefined) continue
|
||||
let cell = cells.get(session)
|
||||
if (cell === undefined) {
|
||||
cell = { state: def.init(session.header), observedSeq: -1 }
|
||||
cell = { state: def.init(session.header, session.inheritedEventCount), observedSeq: -1 }
|
||||
cells.set(session, cell)
|
||||
}
|
||||
this.advanceCell(def, cell, session.events, session.seq - 1)
|
||||
@@ -166,7 +161,7 @@ export class RegistryDriver {
|
||||
for (const { def, cells } of this.registrations.values()) {
|
||||
let cell = cells.get(session)
|
||||
if (cell === undefined) {
|
||||
cell = { state: def.init(session.header), observedSeq: -1 }
|
||||
cell = { state: def.init(session.header, session.inheritedEventCount), observedSeq: -1 }
|
||||
cells.set(session, cell)
|
||||
}
|
||||
this.advanceCell(def, cell, session.events, session.seq - 1)
|
||||
@@ -215,8 +210,12 @@ export class RegistryDriver {
|
||||
return floor === undefined ? undefined : Math.max(floor - 1, 0)
|
||||
}
|
||||
|
||||
/** Cold read: seed usable rows through stateSchema.parse, refold the rest. */
|
||||
restore(checkpoint: Checkpoint, events: { seq: number }[], baseSeq: number, header: unknown): ProjectionSnapshot {
|
||||
/**
|
||||
* Cold read: seed usable rows through stateSchema.parse, refold the rest.
|
||||
* `inheritedEventCount` is the fork-inherited prefix length the registry
|
||||
* hands to unit initialization (0 for a plain session).
|
||||
*/
|
||||
restore(checkpoint: Checkpoint, events: { seq: number }[], baseSeq: number, header: unknown, inheritedEventCount = 0): ProjectionSnapshot {
|
||||
const endSeq = events.at(-1)?.seq ?? baseSeq - 1
|
||||
const values: Record<string, unknown> = {}
|
||||
for (const { def } of this.registrations.values()) {
|
||||
@@ -228,7 +227,7 @@ export class RegistryDriver {
|
||||
if (!usable && baseSeq > 0) {
|
||||
throw new RegistryViolationError(`${def.key} cannot restore from seq ${baseSeq}: re-read from seq 0`)
|
||||
}
|
||||
let state = usable ? def.stateSchema.parse(row.val) : def.init(header)
|
||||
let state = usable ? def.stateSchema.parse(row.val) : def.init(header, inheritedEventCount)
|
||||
const from = usable ? row.seq : baseSeq - 1
|
||||
for (let index = from - baseSeq + 1; index < events.length; index++) {
|
||||
state = def.apply(state, events[index])
|
||||
|
||||
@@ -50,8 +50,8 @@ export function headersDef(): HeadersDefLike {
|
||||
|
||||
/**
|
||||
* Lossless-JSON probe and detach, inlined with the dsh `snapshotJsonValue`
|
||||
* semantics (the export left `@deepseek-ai/dsh-session` in 0.1.2-alpha.2, and
|
||||
* the test fixtures must track no single dsh face). Returns undefined when the
|
||||
* semantics (the export lives in `@deepseek-ai/dsh-util-values`, and the
|
||||
* test fixtures must track no single dsh face). Returns undefined when the
|
||||
* value is not losslessly JSON-serializable: an undefined/function/symbol
|
||||
* member, a non-finite number, a non-plain object, or a cycle. Shared with the
|
||||
* compat matrix's registry driver (tests/host/compat/registryDriver.ts).
|
||||
|
||||
@@ -34,10 +34,9 @@ class MemorySettings extends SettingsProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// The raw string is what every supported dsh version's `register` accepts at
|
||||
// runtime; the branded cast satisfies the pre-0.1.2 type face (see
|
||||
// src/host/settings.ts — the `settingsNamespace()` helper was removed in
|
||||
// 0.1.2-alpha.2).
|
||||
// The raw string is what the dsh settings `register` accepts at runtime; the
|
||||
// branded cast satisfies the SettingsNamespace type face (see
|
||||
// src/host/settings.ts).
|
||||
const ns = SETTINGS_NAMESPACE as SettingsNamespace
|
||||
|
||||
/** Poll until the inject callback inside installSettings has registered the namespace. */
|
||||
|
||||
Reference in new issue
Block a user