Takes the action-card fixes: kinds reconcile before every action, only live
roots get cards, the tool refuses while messages are queued, and bodies no
longer receive ready. The plugin card drops ready, its queued-card test goes
with the suppressed phase, and the shared-row limitation follows the base's
new limitations.
- An enable that a plugin reload cuts off records unconfirmed; the card
offers a retry and Not now, and a retry that finds the plugin on records
enabled because this card's change landed.
- A rejected profile change records failed with operation-error and keeps
the retry instead of failing the click; overridden and cancelled are gone
because enabling a bundle only reports applied, restart-required or failed.
- A refused or lost catalog lookup keeps Not now and offers Try again, with
separate copy for each; a settled card keeps its outcome after its plugin
leaves the catalog. Only the clicked button shows Working, the details
label is interpolated, and the reason and description stay its
accessible description.
- The Host entry exports its inject list, so the configuration catalog lists
its services; the skill keeps its rules without the examples.
- With both experiment bundles on, the shared action-card rows cannot be
switched or configured on the Plugins page; the READMEs say so and a
Loader test pins it while the bundles still switch.
- The unused intent fixture is removed and the evaluation set expects a
create_personalized_page card for the counter.
Update the Web permission-policy snapshot, the config catalog line
reference and README text for the new switch notice; pin both switch
directions in the unit test.
Co-authored-by: Tianyi Cui <[email protected]>
- Kinds can reconcile a card before every user action, including Not now;
the page kind confirms uncertain runs against the installed profile, so an
installed page keeps Undo instead of being dismissed.
- Only a live root's direct skill load activates propose_action; calls from
subagents or from code are refused.
- The tool refuses while queuedMessageLimit messages wait, so the model is
never told a hidden card was shown; the suppressed phase is gone and the
projection moves to state version 2.
- record() refuses actions no kind registered and the derived replaced phase.
- Not now dismisses an unstarted card whose kind is no longer registered.
- The unused ready prop and dead locale keys are removed, and the READMEs
state the card and row rules as implemented.
Plugin recommendations now proposes through the shared propose_action tool
instead of a dsh-action answer block. The Host registers the
enable_official_plugin kind with the action card service: the
official-plugins skill, details holding the plugin name, the Enable verb
with global access, and the enabled, already-enabled and failed phases,
whose data carries the application outcome and the management error code.
The package drops its own projection, Remote act, status notice and
persisted record; it keeps the describe Remote for card text. The bundle
inserts the shared card rows beside its own, as Personalized pages does.
The browser half registers a card body in the shared frame that keeps the
catalog text cache, the reserved loading lines, the single details target
and the Enable and Not now footer. The skill's submission section now asks
for one propose_action call.
Personalized pages registers the create_personalized_page card kind with
its create, keep, undo, cancel and restore handlers and drops its own
projection, Remote and status notice. The skill now ends with a
propose_action call. The Client package contributes only the page card
body. The persistence record moves from the page status source to the
shared action-cards source and decision record.
action-cards owns the propose_action tool, the actionCards projection of
proposals and decisions, the act Remote and the card status notice; card
kinds register an action name, skill, details schema, verbs and handler.
The tool is admitted per Agent only after a registered kind's skill loads.
client-ui-action-cards draws the card frame below the completed answer,
lets kinds register a body per action and adds the propose_action
process row.
* refactor(llm-deepseek): share the /models listing machinery
Move the account route's `/models` request and decoding, its
credential-scoped listing cache, and listed-catalog resolution with the
version gate into the shared DeepSeek Messages library, so a second
route can follow the same listing. Names drop the account prefix, the
cache takes each access's endpoint settings with the call, and it gains
`invalidate()` for a forced reload. `isBuiltInCatalog()` recognizes the
catalog an uncustomized `models` setting resolves to.
The account route keeps its behavior; only its imports and the
override diagnostic wording ("model listing") change.
* feat(llm-deepseek-api-key): follow the official /models listing
While `models` equals the built-in catalog and the Messages origin is
https://api.deepseek.com, `deepseek-official` serves the official
`GET /models` listing, authenticated with the API key in `x-api-key`.
Loading, refresh, fallback, merging, and the client-version gate match
the account route: the first access waits up to `modelsTimeoutMs`, a
listing older than `modelsMaxAgeMs` refreshes in the background on the
next access, a failure keeps the last good listing or the built-in
catalog, and listed models this build cannot serve are refused before
network I/O.
A customized `models` list replaces the catalog and requests nothing.
Switching between a customized catalog and the listing discards the
cached listing, so Restore defaults reloads it on the next access.
Suites that boot the route on the official endpoint now answer the
listing themselves instead of reaching the network.
* docs: describe the official route model listing
Document when `deepseek-official` follows the official `/models`
listing, its timing fields, customization precedence, and the forced
reload after Restore defaults in the API-key README, the shared
transport README, and the provider guide. Record the decision in a new
Agent Note, keep the account note's statement of where the shared
listing code lives current, and regenerate the configuration catalog.
* test(llm-deepseek-api-key): pin the once-per-listing hidden-entry diagnostic
* docs: regenerate module and event graphs for the shared listing
* fix(llm-deepseek-api-key): report credential failures on the model request
On the official endpoint, `connect` waited for the listing view, whose
credential lookup rethrew a malformed key or a credential-store failure
before the agent loop recorded the prompt. A failed lookup now resolves
the connection from the current listing view, so `resolveAuth` reports
the failure on the model request and the turn keeps the prompt and the
failed attempt. A caller abort still rejects.
* docs(llm-deepseek-api-key): name which hidden listing entries are logged
* test: keep unit suites off the official model listing
Suites that boot the DeepSeek routes with a dummy or inherited key now
request `GET https://api.deepseek.com/models` on the official endpoint.
A unit-lane setup file rejects that request at once, so such a suite
serves the built-in catalog instead of reaching production or waiting
out the listing deadline on a network that never answers; suites that
exercise the listing answer `fetch` themselves. Other requests pass
through unchanged.
* feat(llm-deepseek): read tool_update from the model listing
Both DeepSeek routes now take a listed model's Messages tool update mode
from `api_capabilities.anthropic_messages.tool_update`, alongside the
system prompt mode. `in-history` and `addition-only` replace the
bundled mode; any other value reads as `leading-only` and removes it.
An absent or `null` mode keeps the bundled one, so a listing that does
not declare the field leaves `deepseek-flash` on `addition-only`.
`system_prompt_update: null` likewise keeps the bundled mode now; it
previously read as `leading-only`, which a serializer writing empty
values as null would have turned into a cache-unfriendly downgrade.
* fix(llm-deepseek): sync the built-in context window with the official listing
The official `/models` listing declares a 1,048,576-token context window
for `deepseek-flash` and `deepseek-v4-pro`; the built-in catalog said
1,000,000, a decimal-versus-binary mismatch. Both built-in entries now
carry the listed value, so offline starts and listing failures size the
context the same way the listing does. The Models page shows the exact
count for these rows, since 1,048,576 has no decimal K/M spelling.
`defaultContextWindow`, the fallback for model ids the catalog does not
describe, stays 1,000,000.
* test(llm-deepseek-api-key): reject the stubbed listing request with an Error
* test(cli): keep the built SDK smoke off the official model listing
The SDK profile's initialize now resolves the deepseek-official catalog,
which requests GET https://api.deepseek.com/models on the official
endpoint. With the smoke's fixture key that request answers 401 and the
route warns on stderr, so the test's empty-stderr assertion failed in the
e2e and snapshots lanes. Point the run at a loopback mock and assert it
received no model call.
* test(llm-deepseek): compose the malformed-key turn without the native executor
release/dsh-0.2.1 predates @deepseek-ai/dsh-tool-executor-native; its tool
runtime executes tools itself. Compose the official-route turn the way
account-routing.spec.ts does on this branch.
* fix(llm-deepseek-api-key): gate nothing when the credential lookup fails
The listing cache switches scope only after it reads a credential, so a
failed lookup left `connect` serving the previous credential's listing.
After a key whose listing refuses a model was replaced by a malformed one,
`admit` refused that model with CLIENT_UPGRADE_REQUIRED before the turn
recorded the prompt, instead of `resolveAuth` reporting the credential
error on the request. Serve the built-in catalog without listing gates
when the lookup fails.
* test(llm-deepseek-api-key): pin the once-per-listing diagnostic across request preparation
The hidden-entry diagnostic test repeated the same discovery assertion to
prove a second pass reports nothing new, which read as a duplicate. Assert
the single warning after the request preparations instead; they resolve
the same listing, so dropping the per-listing deduplication still fails
the test.
* docs(llm-deepseek-api-key): note that a list matching an earlier built-in catalog counts as customized
Customization is judged against the running version's built-in catalog,
so a saved `models` list equal to an earlier version's catalog stops
following the listing once the built-in catalog changes, until Restore
defaults. Record the consequence in the Agent Note and the README's known
limitations.
* fix(ui): separate navigation pins from selection and show menu icons
* fix(ui): simplify More menus and fold conversation views on resize
* test(ui): fix navigation CI regressions and refresh catalogs
* refactor(ui): render sidebar panel icons directly
* fix(ui): keep Chat visible and stop persisting navigation pins
* refactor(ui): drop unreachable pin validation from in-memory stores
The DeepSeek search and Search/Fetch API providers now report readiness
from credential presence alone, on any model route: the account token
first on the deepseek-account route, then a literal apiKey or a
configured apiKeyEnv reference. A conversation on another provider's
model keeps web_search and web_fetch while a DeepSeek key is configured,
as on master, so the route refusal WEB_PROVIDER_ROUTE_DISABLED and its
upgrade guide are removed.
The pi-ai-conversation-updates and deepseek-messages-system-prompt
snapshots replay their master recordings again, the headless keyless
smoke points its search key reference at an unset variable, and the
readiness Agent Note records the rejected route and endpoint rules.
A user who gives DeepSeek search a key of its own has chosen what that key
pays for, so the route check now applies only to the shared
DEEPSEEK_API_KEY. A literal apiKey, or an apiKeyEnv other than
DEEPSEEK_API_KEY, opens every model route for Messages search and for the
Search and Fetch APIs; a configured endpoint does not, since the search
still sends the shared key. The Web search settings page saves its key
under the section's apiKeyEnv, DEEPSEEK_API_KEY by default, so a key saved
there keeps serving only the DeepSeek routes.
The route refusal now names the settings that give search a key of its
own, and the READMEs, the readiness Agent Note, and the upgrade guide
describe the exception.
Review found two faults in the endpoint and configuration checks. The
providers took the official chat route's endpoint from the first active
dsh-llm-deepseek-api-key Loader row, so a composition that isolates several
instances answered for an unrelated one: a correct route lost search, or a
gateway's key reached DeepSeek. And a key saved on the Web search settings
page lands under the default DEEPSEEK_API_KEY reference, so the inference
that search had its own configuration never saw it.
Both DeepSeek web packages now serve only the deepseek-account and
deepseek-official routes, whatever key they are configured with, and no
longer compare the chat endpoint with their own. ready() checks the route
and then credential presence as before; the API-key resolver repeats the
route check and refuses with WEB_PROVIDER_ROUTE_DISABLED. The packages no
longer read Loader rows or depend on dsh-llm-deepseek-api-key, so the
lockfile, the module graph, and the keyless smoke expectations that the
endpoint check had changed return to master's.
An upgrade guide covers conversations on other providers' models, which
lose DeepSeek web search and, with the Search and Fetch APIs selected,
web_fetch.
* feat(webserver): add resource routes and in-process readResource
A webserver route or the fallback seat may now answer through a resource,
a function from a Fetch Request to a Response, besides the existing
node:http handler. register accepts either route form and registerFallback
accepts a handler or { resource }; existing handler callers are unchanged.
HTTP GET and HEAD requests reach a resource; webServer.readResource(request,
peer) follows the same match order and reaches only resources, so handler
routes stay unreachable to in-process readers.
client-modules registers /plugins as the fetchBundle resource, and
frontend-static claims the fallback seat with a resource that renders the
index for a supplied Peer, or runs Connection's existing authorizeIndex for
an HTTP request and renders for the browser Peer.
* fix(webserver): flush resource headers and keep one drain listener per response
Resource responses send their status and headers before the first body
chunk, register one drain/close listener pair per response so the gzip
middleware's zlib stream does not accumulate listeners across backpressure
waits, and readResource drops the body of a HEAD read as HTTP does. HTTP
requests and readResource share one responder lookup.
* chore(scripts): prune the retired client-modules unknown-cast baseline entry
* feat(frontend-static): let browsers revalidate dist files except the index
Every dist file other than the rendered index now carries
cache-control: no-cache and a weak ETag from a SHA-1 of its bytes; a
request whose If-None-Match equals that ETag receives an empty 304. The
index keeps sending no cache headers. In-process reads through
webServer.readResource revalidate the same way.
* fix(webserver): stop reading Host for resource URLs and copy no held body
A resource request's URL is the request target resolved against the same
placeholder origin the route match uses, so a missing or unparsable Host no
longer turns a dist or /plugins request into 400. The webserver ends a
resource response itself and treats any earlier close as an abort, which
also covers the Web Worker tunnel marking an aborted response ended first.
client-modules bundle bodies and frontend-static file bodies are the held
bytes themselves instead of copies, and frontend-static no longer exports
its internal serveStatic helper.
* docs(webserver): document resource route types and the handler-or-resource fallback
Paste WebResource and WebResourceRoute beside WebRoute on the web-server
subsystem page and link the catalog signatures to it, replace the remaining
'fallback handler' wording now that the seat takes a handler or a resource,
and note why the drain listener removal is a no-op under gzip.
The Web search switch selects the DeepSeek Search and Fetch APIs together,
and both reuse the DeepSeek chat key by default. Neither provider answered
readiness, so a step without a usable key was offered web_search and
web_fetch whose every call failed, and a conversation on another provider's
model or behind a gateway sent the chat key to DeepSeek.
dsh-web gives WebFetchProvider an optional ready(llmProvider) and adds
ctx.web.fetchReady(), which asks only the provider fetch() selects, as
searchReady() does for search. tool-web registers web_fetch with a step
activation condition on fetchReady(), so a step that cannot fetch leaves the
tool out of declarations, guidance, and PTC bindings and refuses a call to
it as UNKNOWN_TOOL; without web_fetch the search guidance uses its
search-only text. The shipped HTTP fetch provider has no ready() and stays
admitted.
web-deepseek-api applies the API-key rules of Messages search to both APIs:
without a literal apiKey, an apiKeyEnv other than DEEPSEEK_API_KEY, or a
baseURL other than the default, the chat key goes only to the DeepSeek
routes and only to an API base on the official chat route's origin. The
refusal sits in the key's only resolver, and both providers' ready()
answers presence in the order an operation resolves credentials. The
transport repeats these checks so the package still does not depend on
dsh-web-search-deepseek.
* ci: run native Windows PR jobs in a separate workflow
GitHub re-runs failed jobs only after a whole run finishes, so slow Windows jobs blocked re-running failed Linux jobs. CI Windows now owns the native Windows jobs and the Windows x64 Python runtime target, with its own required 'windows checks passed' verdict.
Refs #6893
* ci: run Python PR jobs in a separate workflow
Python rarely fails while the Windows lanes run long, so the Python SDK suite and the Linux/Windows x64 runtime matrix move into CI Python. The three pull-request workflows can now be re-run without waiting for each other, and branch protection gains "python checks passed" beside the existing verdicts.
Refs #6893
* docs: record the three PR verdicts in the failover runbook
* test: pin the shared environment across the PR workflows
* test: read the python SDK lane from CI Python
* test(sdk): parse session JSONL frames in the keyless smoke
Node 24.19 returned only the first frame of a concatenated zstd log, so the single JSON.parse saw just the header; 24.22 returns every frame and the JSONL body broke the parse. Read frames explicitly and select the main session by its header so both versions agree.
Profile templates and the default, optional, on-demand, retired, and
installation-owned bundle lists move to the new data-only package
@deepseek-ai/dsh-official-bundles, so editing a bundle list no longer
touches app-boot.
Master's step-scoped conditional activation resolves each tool's condition
before a model step assembles, and both the declarations and dispatch read
that admitted set. Prompt assembly no longer carries tool declarations, so
tool-web's system-prompt/assemble listener could not remove web_search any
more, and removing a declaration there never stopped a repeated call from
running.
tool-web now registers web_search with a when condition that asks
ctx.web.searchReady() about the route of the step's model selection, and its
guidance section renders from the step's admitted set. A step that does not
admit the tool leaves it out of declarations, guidance, and PTC program
bindings, and refuses a call to it as UNKNOWN_TOOL without reaching a
provider. Tests that execute web_search directly now run inside a resolved
step, as the executor requires for conditional registrations.
Docs, the readiness Agent Note, and the event producer/consumer table follow
the new mechanism; the tools README gains an explicit conditional-tools
anchor for cross-locale links.
The fenced-block parser moves to the zero-dependency package
@deepseek-ai/dsh-util-action-block, which the client bundle inlines;
Personalized pages and Plugin recommendations both read answers through
it and no longer import or edit each other. Personalized pages yields:
a page block in an answer or turn that also carries another action's
block is logged but never offered, and a later other-action block
retracts this turn's undecided page card. Plugin recommendations never
yields and keeps one plugin card per turn of its own.
Replaces list_official_plugins, suggest_plugin and the routing prompt
section with the official-plugins skill, whose load appends the live
catalog, and an enable_official_plugin block at the end of the answer.
The card reads catalog text from the Host, Enable goes through the plugin
manager, and each outcome reaches the model as a status notice on the
next request. Adds the queuedMessageLimit option, en/zh/ja card copy and
a re-recorded persistence change and Web replay.