Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # docs/event-producer-consumer.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/client/runtime/README.i18n.yaml # packages/client/ui-conversation/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
@@ -23,7 +23,7 @@ import { SessionQueryError, type SessionSearchCursor } from '@deepseek-ai/dsh-se
|
||||
import type { Workspace, WorkspaceRecord } from '@deepseek-ai/dsh-workspace'
|
||||
import {
|
||||
workspaceDomainState, workspaceRecord, WorkspaceId as brandWorkspaceId,
|
||||
WorkspaceMoveInvalidError, WorkspaceNameConflictError,
|
||||
WorkspaceMoveInvalidError, WorkspaceNameConflictError, WorkspaceUnknownSessionError,
|
||||
} from '@deepseek-ai/dsh-workspace'
|
||||
// Type-only: brings the `ctx.tools` Context merge into this program (viewFor reads presenters).
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
@@ -75,6 +75,9 @@ import { openNativePath } from './native-path-opener.ts'
|
||||
/** Page size when history is called without maxMessages. */
|
||||
const DEFAULT_MAX_MESSAGES = 50
|
||||
|
||||
/** Non-model settings namespaces intentionally served to the Web client. */
|
||||
const WEB_SETTINGS_NAMESPACES = ['permission'] as const
|
||||
|
||||
/** Provider work budget: at most 100 calls and 2,000 inspected hits. */
|
||||
const SESSION_SEARCH_PROVIDER_CALL_LIMIT = 100
|
||||
|
||||
@@ -1256,31 +1259,35 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The settings namespaces this proxy serves: exactly those a registered
|
||||
* configurable provider addresses. The settings seam itself is general —
|
||||
* any plugin may register a namespace for its own configuration — but the
|
||||
* Web configuration plane is scoped to model providers, and that boundary
|
||||
* has to be enforced here rather than assumed from the current plugin set.
|
||||
* Without it, every future `settings.register()` would silently become
|
||||
* remotely readable and writable configuration.
|
||||
*/
|
||||
function exposedNamespaces(): Set<string> {
|
||||
/** Settings namespaces whose changes can invalidate the model catalog. */
|
||||
function modelProviderNamespaces(): Set<string> {
|
||||
return new Set(ctx.llm.listConfigurableProviders().map(entry => entry.settingsNs))
|
||||
}
|
||||
|
||||
/** Refuse a namespace outside the model-provider boundary, naming why. */
|
||||
/**
|
||||
* The settings namespaces this proxy serves: configurable model providers
|
||||
* plus the small explicit Web preference allowlist. The settings seam
|
||||
* remains general; a future registration does not become remotely readable
|
||||
* or writable by default.
|
||||
*/
|
||||
function exposedNamespaces(): Set<string> {
|
||||
const exposed = modelProviderNamespaces()
|
||||
for (const ns of WEB_SETTINGS_NAMESPACES) exposed.add(ns)
|
||||
return exposed
|
||||
}
|
||||
|
||||
/** Refuse a namespace outside the explicit configuration-client boundary. */
|
||||
function notExposed(request: RpcRequest<unknown>, ns: string): RpcResponse<SettingsNamespaceView> {
|
||||
return err(request, {
|
||||
code: 'settings-not-exposed',
|
||||
message: `settings namespace "${ns}" is not exposed to configuration clients; only a namespace a registered model provider addresses is`,
|
||||
message: `settings namespace "${ns}" is not exposed to configuration clients`,
|
||||
details: { ns },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one settings write (merge or wholesale replace) and acknowledge with
|
||||
* the namespace's new redacted view. A namespace outside the model-provider
|
||||
* the namespace's new redacted view. A namespace outside the configuration
|
||||
* boundary is refused before the seam is touched; every seam refusal —
|
||||
* unknown or invalid namespace, read-only provider, schema validation,
|
||||
* storage — becomes one `settings-rejected` carrying the seam's own message.
|
||||
@@ -1821,7 +1828,10 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
|
||||
workspace: {
|
||||
list(request) {
|
||||
return Promise.resolve(ok(request, { items: ctx.workspace.list().map(workspaceView) }))
|
||||
return Promise.resolve(ok(request, {
|
||||
items: ctx.workspace.list().map(workspaceView),
|
||||
archivedSessionIds: [...ctx.workspace.archivedSessionIds],
|
||||
}))
|
||||
},
|
||||
|
||||
// Exactly one of path/name arrives (schema refine). Existing-folder
|
||||
@@ -1937,6 +1947,23 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
}
|
||||
return ok(request, { workspace: workspaceView(workspace) })
|
||||
},
|
||||
|
||||
async archiveSession(request) {
|
||||
const { sessionId } = request.payload
|
||||
try {
|
||||
await ctx.workspace.archiveSession(sessionId)
|
||||
} catch (error: unknown) {
|
||||
// Only the registry's unknown-session rejection is the business
|
||||
// code; storage/durability failures propagate as internal errors.
|
||||
if (!(error instanceof WorkspaceUnknownSessionError)) throw error
|
||||
return err(request, {
|
||||
code: 'session-not-found',
|
||||
message: error.message,
|
||||
details: { sessionId },
|
||||
})
|
||||
}
|
||||
return ok(request, { archivedSessionIds: [...ctx.workspace.archivedSessionIds] })
|
||||
},
|
||||
},
|
||||
|
||||
host: {
|
||||
@@ -2348,6 +2375,10 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
const committedWorkspaceIds = new Set(
|
||||
ctx.workspace.list().map(workspace => String(workspace.id)),
|
||||
)
|
||||
// Frame-dedup baseline, same posture as committedWorkspaceIds: the
|
||||
// stream opens against the current set; workspace.list re-baselines
|
||||
// reconnecting clients, so only later changes need frames.
|
||||
let archivedSessionIds = ctx.workspace.archivedSessionIds
|
||||
const disposers = [
|
||||
ctx.on('session/created', (session: Session) => {
|
||||
queue.push(frame({
|
||||
@@ -2384,6 +2415,14 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
committedWorkspaceIds.add(workspaceId)
|
||||
queue.push(frame({ type: 'host/workspace-changed', workspace: workspaceView(workspace) }))
|
||||
}
|
||||
if (state.archivedSessionIds.length !== archivedSessionIds.length
|
||||
|| state.archivedSessionIds.some((id, index) => id !== archivedSessionIds[index])) {
|
||||
archivedSessionIds = state.archivedSessionIds
|
||||
queue.push(frame({
|
||||
type: 'host/archived-sessions-changed',
|
||||
archivedSessionIds: [...state.archivedSessionIds],
|
||||
}))
|
||||
}
|
||||
return
|
||||
}
|
||||
if (change.table !== 'workspaces') return
|
||||
@@ -2411,11 +2450,12 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
// inherited to overridden leaves the resolved value equal, and a
|
||||
// configuration client still has to re-read (its held revision is
|
||||
// stale, and the field's meaning changed).
|
||||
queue.push(frame({ type: 'host/settings-changed', ns: String(ns) }))
|
||||
const name = String(ns)
|
||||
queue.push(frame({ type: 'host/settings-changed', ns: name }))
|
||||
// A provider's own settings carry its model catalog and endpoint,
|
||||
// so a change there invalidates the model list even when the route
|
||||
// set is untouched — `llm/adapters-updated` alone misses it.
|
||||
if (exposedNamespaces().has(String(ns))) queue.push(frame({ type: 'host/models-changed' }))
|
||||
if (modelProviderNamespaces().has(name)) queue.push(frame({ type: 'host/models-changed' }))
|
||||
}),
|
||||
ctx.on('credentials/updated', (ref) => {
|
||||
queue.push(frame({ type: 'host/credentials-changed', ref: String(ref) }))
|
||||
|
||||
Reference in New Issue
Block a user