Merge origin/master into worktree/web-plugin-config

Resolves the vendored-package rescope (cordis -> @deepseek-ai/cordis,
schemastery -> @deepseek-ai/schemastery) against this branch's settings
namespaces: the bash capability namespace, its two executors' section
installs, and the new plugin-config client package all move to the scoped
specifiers.
This commit is contained in:
Yichen Jiang
2026-08-10 23:13:11 +08:00
1738 changed files with 11013 additions and 8201 deletions

View File

@@ -62,13 +62,13 @@
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/dsh-user-interaction": "workspace:^",
"@deepseek-ai/dsh-workspace": "workspace:^",
"schemastery": "^3.18.0",
"@deepseek-ai/schemastery": "^3.18.0",
"zod": "^4.4.3"
},
"peerDependencies": {
"@deepseek-ai/dsh-agent-presets": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-agent-presets": "workspace:^",
@@ -77,6 +77,6 @@
"@deepseek-ai/dsh-storage-domain": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
}
}

View File

@@ -6,7 +6,7 @@
import { randomUUID } from 'node:crypto'
import { mkdir, stat } from 'node:fs/promises'
import { dirname } from 'node:path'
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { installModelSelection } from '@deepseek-ai/dsh-agent'
import type { Agent, ModelSelection, ModelSelectionRef, AgentOptions, AgentStatus } from '@deepseek-ai/dsh-agent'
import { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-default-model'
@@ -33,6 +33,7 @@ import {
PresetNotWritableError, resolveSessionPreset,
SETTINGS_NAMESPACE as AGENT_PRESET_SETTINGS_NAMESPACE, UnknownPresetError,
} from '@deepseek-ai/dsh-agent-presets'
import type { PresetBearingSession } from '@deepseek-ai/dsh-agent-presets'
import type {} from '@deepseek-ai/dsh-tools'
import type {
ApiProxy, ConfigurableProviderView, CredentialView, GoalRef, HistoryEntry, HostFrame,
@@ -1048,7 +1049,9 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
* common paths — reconnecting, resuming, retrying a create — are unaffected.
* @param sessionId - the identity being adopted.
* @param requested - the preset the request named, if any.
* @param existing - the preset the session was created under, if any.
* @param existing - the preset the session RUNS, if any; both callers resolve
* it from the log, which differs from the creation header once a blank
* session has switched.
* @throws when both are present and differ.
*/
function assertPresetUnchanged(
@@ -1361,17 +1364,26 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
* The registry view scope a transcript's presenters resolve in.
*
* A live agent is that scope itself (its chain passes through its preset's
* standing layer). A cold session names its preset on the header, and the
* standing layer). A cold session resolves its preset from the LOG, and the
* preset's STANDING key serves without resuming anything — ensuring the
* mount composes plugins but starts no agent, session, or turn. No roster,
* no recorded preset, or a preset the roster no longer supplies all fall
* back to the global layer: the transcript still serves, with the generic
* cards a viewless entry renders.
*
* Reading the header alone would render a session that switched while blank
* through the composition it was CREATED with. Every tool only the newer
* preset registers resolves to no presenter there, and the transcript
* silently degrades to generic cards for exactly the calls its history is
* made of.
* @param sessionId - the transcript being read.
* @param header - that session's header (attached or inspected).
* @param session - that session's header and log (attached or inspected).
* @returns the scope to pass to presenter lookups, or undefined for global.
*/
async function presenterScopeFor(sessionId: SessionId, header: SessionHeader): Promise<ScopeKey | undefined> {
async function presenterScopeFor(
sessionId: SessionId,
session: PresetBearingSession,
): Promise<ScopeKey | undefined> {
const live = ctx.get('agents')?.get(sessionId)
if (live !== undefined) return live
const presets = ctx.get('agentPresets')
@@ -1381,7 +1393,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
// through the DEFAULT preset's standing layer: that is the composition
// an unnamed session composes today, and presenters are pure display,
// so the worst a mismatch produces is the generic card it had anyway.
return await presets.standingKeyFor(header.agentPreset)
return await presets.standingKeyFor(resolveSessionPreset(session))
} catch {
// Swallows only the unknown/unusable-preset rejection from the roster:
// a deleted or broken preset must degrade this read, never fail it.
@@ -1474,7 +1486,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
// Beside the cwd check for the same reason, and after the await so it
// covers every path that yields a live agent — freshly created, adopted
// live, resumed from disk, or recovered by the concurrent-creation catch.
assertPresetUnchanged(sessionId, presetId, agent.session.header.agentPreset)
assertPresetUnchanged(sessionId, presetId, resolveSessionPreset(agent.session))
if (agent.session.header.cwd !== cwd) {
throw new SessionCwdConflict(sessionId, cwd, agent.session.header.cwd)
}
@@ -1990,12 +2002,16 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
})
}
}
// Echo the RESOLVED composition so a client can label the session it
// just created without waiting for the next list refresh — the create
// is the commit point that knows it (a caller that named none gets
// the default the header recorded).
// Echo the composition the session RUNS so a client can label it
// without waiting for the next list refresh — the create is the commit
// point that knows it (a caller that named none gets the default).
// Resolved from the log for the same reason `sessionListFields()` is:
// this handler also adopts an already-live session, and one that
// switched while blank runs a preset its header no longer names, so
// echoing the header would contradict both the adoption this call just
// allowed and the row `session.list` serves for the same session.
const created = ctx.agents.get(sessionId)
const createdPreset = created?.session.header.agentPreset
const createdPreset = created === undefined ? undefined : resolveSessionPreset(created.session)
return ok(request, { sessionId, ...createdPreset === undefined ? {} : { agentPreset: createdPreset } })
},
@@ -2014,7 +2030,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
details: {},
})
}
const page = historyPage(ctx, state.events, beforeSeq, maxMessages, await presenterScopeFor(sessionId, state.header))
const page = historyPage(ctx, state.events, beforeSeq, maxMessages, await presenterScopeFor(sessionId, state))
return ok(request, {
events: page.events,
hasMore: page.hasMore,
@@ -2993,7 +3009,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
// The scope presenters resolve in — the live agent, else the recorded
// preset's standing key, else the global layer — so a cold session's
// '/' popup lists the catalog its composition actually serves.
const scope = await presenterScopeFor(sessionId, session.header)
const scope = await presenterScopeFor(sessionId, session)
try {
const skills = (await skillRegistry.list({ cwd, scope })).filter(isUserInvocable)
return ok(request, {

View File

@@ -12,8 +12,8 @@
* service; sessions that have already logged a selection remain unchanged.
*/
import { Context, Service } from 'cordis'
import z from 'schemastery'
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-agent-default-model'
import type { ApiProxy } from './api/index.ts'
import { createApiProxy } from './api-proxy.ts'
@@ -26,7 +26,7 @@ export type { IApiClient } from './fetch/client.ts'
export { createApiProxy } from './api-proxy.ts'
export type { ApiProxyDefaults } from './api-proxy.ts'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
/** The host-side ApiProxy implementation (the transport-agnostic gateway face). */
apiProxy: ApiProxy

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-host-apiproxy'

View File

@@ -8,7 +8,7 @@
import { mkdtempSync, realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type AgentFactory } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
@@ -186,6 +186,29 @@ describe('session.create with an agent preset', () => {
})
})
it('adopts a live session under the preset it SWITCHED to', async () => {
const { api, ctx } = await harness(['standard', 'minimal'])
await api.sessions.create(request({ sessionId: SessionId('s4b'), agentPreset: 'standard' }))
// Exactly what `agentPreset.select` leaves behind on a blank session: the
// header keeps the creation fact, the log states what the agent runs.
ctx.sessions.get(SessionId('s4b'))?.append('agent-preset/selected', { agentPreset: 'minimal' })
const adopted = await api.sessions.create(request({ sessionId: SessionId('s4b'), agentPreset: 'minimal' }))
const stale = await api.sessions.create(request({ sessionId: SessionId('s4b'), agentPreset: 'standard' }))
// Comparing against the header would invert both answers: the preset the
// session actually runs would be refused, and the one it left would pass.
expect(adopted.result.ok).toBe(true)
// The echo has to name the same preset the adoption just accepted, or the
// client labels the session with one it has already left — and disagrees
// with the row `session.list` serves for it.
if (!adopted.result.ok) throw new Error('unreachable')
expect(adopted.result.value).toMatchObject({ agentPreset: 'minimal' })
expect(stale.result.ok).toBe(false)
if (stale.result.ok) throw new Error('unreachable')
expect(stale.result.error.details).toMatchObject({ existingPreset: 'minimal' })
})
it('adopts a live session unchanged when the caller names no preset', async () => {
const { api } = await harness(['standard', 'minimal'])
await api.sessions.create(request({ sessionId: SessionId('s5'), agentPreset: 'minimal' }))
@@ -660,6 +683,27 @@ describe('session.history presenter scope', () => {
expect(standingKeyRequests).toEqual([])
})
it('resolves a switched session from the LOG, not its creation header', async () => {
// The header is a creation fact; a switch while blank is a logged event,
// and every turn after it ran under the newer composition. Reading the
// header would render that history through the older preset's layer,
// where the tools it is made of have no presenter at all.
const meta = { id: SessionId('p4'), createdAt: 1, cwd: '/tmp/p4', agentPreset: 'standard' }
const { api } = await harness(['standard', 'minimal'], {
list: () => Promise.resolve([meta]),
inspect: () => Promise.resolve({
meta,
events: [{ type: 'agent-preset/selected', seq: 1, time: 0, data: { agentPreset: 'minimal' } }],
}),
})
standingKeyRequests.length = 0
const response = await api.sessions.history(request({ sessionId: SessionId('p4') }))
expect(response.result.ok).toBe(true)
expect(standingKeyRequests).toEqual(['minimal'])
})
it('serves a COLD transcript whose standing mount is no longer usable', async () => {
// A genuinely cold session: persistence knows it, no live agent exists.
const meta = { id: SessionId('p3'), createdAt: 1, cwd: '/tmp/p3', agentPreset: 'standard' }

View File

@@ -7,7 +7,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'

View File

@@ -8,7 +8,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'

View File

@@ -8,7 +8,7 @@ import { mkdtempSync, writeFileSync, utimesSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import SessionStore from '@deepseek-ai/dsh-session'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import { TypeRTLookupFailure } from '@deepseek-ai/dsh-type-meta'

View File

@@ -10,7 +10,7 @@ import { MessageId, freezeMessage } from '@deepseek-ai/dsh-llm'
*/
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'

View File

@@ -6,8 +6,8 @@
*/
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import z from 'schemastery'
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'

View File

@@ -1,7 +1,7 @@
/** Session-fork boundaries, lineage, and inherited model routing. */
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent'
import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'

View File

@@ -6,7 +6,7 @@
*/
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import LlmService, { LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'

View File

@@ -8,7 +8,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { z } from 'zod'
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'

View File

@@ -8,7 +8,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import SessionStore from '@deepseek-ai/dsh-session'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent'

View File

@@ -5,7 +5,7 @@
*/
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { stat } from 'node:fs/promises'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import { SubagentError } from '@deepseek-ai/dsh-subagent'
import { RpcId } from '../src/api/rpc.ts'

View File

@@ -8,7 +8,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'

View File

@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, mkdtempSync, realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentFactory } from '@deepseek-ai/dsh-agent'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'