Merge remote-tracking branch 'origin/master' into worktree/web-file-session-references

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md
#	.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md
#	.agents/notes/implemented/feature/2026-07-21-cross-session-references.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-21-cross-session-references.md
#	.agents/notes/implemented/feature/2026-07-21-cross-session-references.zh.md
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml
#	apps/cli/config/web.cordis.yml
#	apps/cli/package.json
#	apps/web/tests/scaffold.ts
#	docs/capability-seams.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	packages/client/connection/src/client/api.ts
#	packages/client/connection/src/client/fixture.ts
#	packages/client/connection/src/client/index.ts
#	packages/client/runtime/src/client/contract/session.ts
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/runtime/tests/session.client.spec.ts
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-conversation/src/client/chat/ChatView.tsx
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
#	packages/client/ui-conversation/src/client/chat/chat-flow.ts
#	packages/client/ui-conversation/src/client/input/facade.ts
#	packages/client/ui-conversation/src/client/input/hub.ts
#	packages/client/ui-conversation/tests/apply-inject.client.spec.tsx
#	packages/client/ui-conversation/tests/chat-branch-tails.client.spec.tsx
#	packages/client/ui-conversation/tests/chat-view.client.spec.tsx
#	packages/client/ui-conversation/tests/input-bar.client.spec.tsx
#	packages/client/ui-conversation/tests/input-matrix.client.spec.tsx
#	packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx
#	packages/client/ui-conversation/tests/skeleton.client.spec.tsx
#	packages/client/ui-input-trigger/package.json
#	packages/client/ui-input-trigger/src/types.ts
#	packages/client/ui-jobs/README.i18n.yaml
#	packages/client/ui-slash/README.md
#	packages/client/ui-slash/README.zh.md
#	packages/client/ui-subagent/README.i18n.yaml
#	packages/client/ui-subagent/README.md
#	packages/client/ui-subagent/README.zh.md
#	packages/client/ui-subagent/package.json
#	packages/client/ui-subagent/src/client/index.ts
#	packages/client/ui-subagent/tests/browser-plugin.client.spec.ts
#	packages/client/ui-subagent/tsconfig.json
#	packages/context/session-reference/README.i18n.yaml
#	packages/context/session-reference/README.md
#	packages/context/session-reference/README.zh.md
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/session/README.i18n.yaml
#	packages/core/session/README.zh.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/README.md
#	packages/host/apiproxy/README.zh.md
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/api/index.ts
#	packages/host/apiproxy/src/api/rpc-map.ts
#	packages/host/apiproxy/src/api/rpc.schema.ts
#	packages/host/apiproxy/src/api/rpc.ts
#	packages/host/apiproxy/src/api/sessions.ts
#	packages/host/apiproxy/src/fetch/client.ts
#	packages/host/apiproxy/src/fetch/handler.ts
#	packages/host/apiproxy/src/index.ts
#	packages/host/apiproxy/tests/client-handler.spec.ts
#	packages/host/apiproxy/tsconfig.json
#	pnpm-lock.yaml
#	scripts/gen-cordis-catalog.ts
#	scripts/gen-doc-graphs.ts
#	scripts/verify-package-readme-model-experience.ts
#	tsconfig.base.json
#	tsconfig.client.json
#	vitest.config.ts
This commit is contained in:
Yichen Jiang
2026-08-14 16:18:40 +08:00
7484 changed files with 415329 additions and 280588 deletions
@@ -1,11 +1,11 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact'
import { CompactionId, compactCheckpointSource } from '@deepseek-ai/dsh-compaction'
import { createUserMessage, CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
import SessionQueryService from '@deepseek-ai/dsh-session-query'
import SessionReferenceService, {
import SessionQueryEngine from '@deepseek-ai/dsh-session-query'
import SessionReferenceResolver, {
decodeSessionReferenceUri,
encodeSessionReferenceUri,
formatSessionReferenceMention,
@@ -15,16 +15,16 @@ import SessionReferenceService, {
} from '@deepseek-ai/dsh-session-reference'
import { stringifyTagSafeJson } from '../src/serialization.ts'
class TestSessionQueryService extends SessionQueryService {
class TestSessionQueryEngine extends SessionQueryEngine {
override searchSessions(
..._args: Parameters<SessionQueryService['searchSessions']>
): ReturnType<SessionQueryService['searchSessions']> {
..._args: Parameters<SessionQueryEngine['searchSessions']>
): ReturnType<SessionQueryEngine['searchSessions']> {
return Promise.resolve({ items: [] })
}
override searchEvents(
...args: Parameters<SessionQueryService['searchEvents']>
): ReturnType<SessionQueryService['searchEvents']> {
...args: Parameters<SessionQueryEngine['searchEvents']>
): ReturnType<SessionQueryEngine['searchEvents']> {
return this.readSurface(args[0].sessionId).then(surface => ({
session: surface.session,
items: [],
@@ -35,8 +35,8 @@ class TestSessionQueryService extends SessionQueryService {
async function harness(config: Config = {}): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(TestSessionQueryService)
await ctx.plugin(SessionReferenceService, config)
await ctx.plugin(TestSessionQueryEngine)
await ctx.plugin(SessionReferenceResolver, config)
return ctx
}
@@ -48,6 +48,10 @@ function expectCode(code: SessionReferenceErrorCode): Error {
return expect.objectContaining({ code }) as Error
}
function checkpointSource(id: string) {
return compactCheckpointSource(CompactionId(id))
}
function appendConversation(session: Session): void {
const oldUser = session.append(
'user/message',
@@ -75,7 +79,8 @@ function appendConversation(session: Session): void {
session.append(
'user/message',
createUserMessage({
content: [{ type: 'text', text: '<compacted-summary>checkpoint</compacted-summary>' }], source: COMPACT_CHECKPOINT_SOURCE,
content: [{ type: 'text', text: '<compacted-summary>checkpoint</compacted-summary>' }],
source: checkpointSource('conversation'),
}),
{
surfaceOp: { op: 'replace', start: oldUser.seq, end: oldAssistant.seq },
@@ -247,19 +252,19 @@ describe('session reference discovery and preparation', () => {
source: { kind: 'fallback' },
})
await expect(ctx.sessionReferences.listCandidates(fakeAgent(target))).resolves.toEqual([
await expect(ctx.sessionReferenceResolver.listCandidates(fakeAgent(target))).resolves.toEqual([
{ sessionId: SessionId('same-later'), label: 'Latest title', cwd: '/same', createdAt: 25 },
{ sessionId: SessionId('same'), label: 'same', cwd: '/same', createdAt: 20 },
{ sessionId: SessionId('none'), label: 'none', createdAt: 30 },
{ sessionId: SessionId('other'), label: 'other', cwd: '/else', createdAt: 40 },
])
await expect(ctx.sessionReferences.listCandidates(fakeAgent(target), 'els', 1)).resolves.toEqual([
await expect(ctx.sessionReferenceResolver.listCandidates(fakeAgent(target), 'els', 1)).resolves.toEqual([
{ sessionId: SessionId('other'), label: 'other', cwd: '/else', createdAt: 40 },
])
await expect(ctx.sessionReferences.listCandidates(fakeAgent(target), 'LATEST', 1)).resolves.toEqual([
await expect(ctx.sessionReferenceResolver.listCandidates(fakeAgent(target), 'LATEST', 1)).resolves.toEqual([
{ sessionId: SessionId('same-later'), label: 'Latest title', cwd: '/same', createdAt: 25 },
])
await expect(ctx.sessionReferences.listCandidates(fakeAgent(target), '', 0))
await expect(ctx.sessionReferenceResolver.listCandidates(fakeAgent(target), '', 0))
.rejects.toThrow(expectCode('SESSION_REFERENCE_INVALID_REFERENCE'))
let releaseList: (() => void) | undefined
@@ -268,7 +273,7 @@ describe('session reference discovery and preparation', () => {
return []
})
const controller = new AbortController()
const pending = ctx.sessionReferences.listCandidates(fakeAgent(target), '', undefined, controller.signal)
const pending = ctx.sessionReferenceResolver.listCandidates(fakeAgent(target), '', undefined, controller.signal)
await vi.waitFor(() => { expect(releaseList).toBeTypeOf('function') })
const cancelledList = expect(pending).rejects.toThrow(expectCode('SESSION_REFERENCE_CANCELLED'))
controller.abort('autocomplete superseded')
@@ -289,7 +294,7 @@ describe('session reference discovery and preparation', () => {
reason: new Error('broken title log'),
}])
await expect(ctx.sessionReferences.listCandidates(fakeAgent(target), 'source')).resolves.toEqual([
await expect(ctx.sessionReferenceResolver.listCandidates(fakeAgent(target), 'source')).resolves.toEqual([
{ sessionId: source.id, label: source.id, createdAt: source.header.createdAt },
])
@@ -301,7 +306,7 @@ describe('session reference discovery and preparation', () => {
return []
})
const controller = new AbortController()
const pending = ctx.sessionReferences.listCandidates(fakeAgent(target), 'source', undefined, controller.signal)
const pending = ctx.sessionReferenceResolver.listCandidates(fakeAgent(target), 'source', undefined, controller.signal)
await vi.waitFor(() => { expect(releaseTitles).toBeTypeOf('function') })
expect(titleSignal).toBe(controller.signal)
const cancelledTitles = expect(pending).rejects.toThrow(expectCode('SESSION_REFERENCE_CANCELLED'))
@@ -318,7 +323,7 @@ describe('session reference discovery and preparation', () => {
const source = ctx.sessions.create(SessionId('source'), { meta: { cwd: '/source' } })
appendConversation(source)
const prepared = await ctx.sessionReferences.prepare(
const prepared = await ctx.sessionReferenceResolver.prepare(
fakeAgent(target),
[{ type: 'text', text: 'use @source' }],
[{ sessionId: source.id, label: 'source' }],
@@ -380,7 +385,7 @@ describe('session reference discovery and preparation', () => {
source: { kind: 'user' },
}), { surfaceOp: 'append' })
const prepared = await ctx.sessionReferences.prepare(
const prepared = await ctx.sessionReferenceResolver.prepare(
fakeAgent(target),
[{ type: 'text', text: 'inspect source' }],
[{ sessionId: source.id }],
@@ -406,7 +411,7 @@ describe('session reference discovery and preparation', () => {
{ surfaceOp: 'append' },
)
const prepared = await ctx.sessionReferences.prepare(
const prepared = await ctx.sessionReferenceResolver.prepare(
fakeAgent(target),
[{ type: 'text', text: 'use @source' }],
[{ sessionId: source.id }],
@@ -435,36 +440,36 @@ describe('session reference discovery and preparation', () => {
const agent = fakeAgent(target)
const content = [{ type: 'text' as const, text: 'go' }]
const withoutReferences = await ctx.sessionReferences.prepare(agent, content, [])
const withoutReferences = await ctx.sessionReferenceResolver.prepare(agent, content, [])
expect(withoutReferences).toEqual({ content })
expect(withoutReferences.content).not.toBe(content)
await expect(ctx.sessionReferences.prepare(agent, content, [
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [
{ sessionId: one.id, label: 'first' },
{ sessionId: one.id, label: 'ignored duplicate' },
{ sessionId: two.id },
])).resolves.toMatchObject({ additionalContext: { source: { references: [{ label: 'first' }, { label: 'two' }] } } })
await expect(ctx.sessionReferences.prepare(agent, content, [{ sessionId: target.id }]))
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [{ sessionId: target.id }]))
.rejects.toThrow(expectCode('SESSION_REFERENCE_SELF_REFERENCE'))
await expect(ctx.sessionReferences.prepare(agent, content, [null as never]))
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [null as never]))
.rejects.toThrow(expectCode('SESSION_REFERENCE_INVALID_REFERENCE'))
await expect(ctx.sessionReferences.prepare(agent, content, [1 as never]))
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [1 as never]))
.rejects.toThrow(expectCode('SESSION_REFERENCE_INVALID_REFERENCE'))
await expect(ctx.sessionReferences.prepare(agent, content, [{ sessionId: 1 } as never]))
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [{ sessionId: 1 } as never]))
.rejects.toThrow(expectCode('SESSION_REFERENCE_INVALID_REFERENCE'))
await expect(ctx.sessionReferences.prepare(agent, content, [
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [
{ sessionId: one.id }, { sessionId: two.id }, { sessionId: SessionId('three') },
])).rejects.toThrow(expectCode('SESSION_REFERENCE_TOO_MANY'))
await expect(ctx.sessionReferences.prepare(agent, content, [
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [
{ sessionId: one.id }, { sessionId: SessionId('missing') },
])).rejects.toThrow(expectCode('SESSION_REFERENCE_READ_FAILED'))
const readSurface = vi.spyOn(ctx.sessionQuery, 'readSurface')
readSurface.mockRejectedValueOnce('non-error read failure')
await expect(ctx.sessionReferences.prepare(agent, content, [{ sessionId: one.id }]))
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [{ sessionId: one.id }]))
.rejects.toThrow(/non-error read failure/)
readSurface.mockRejectedValueOnce('non-error signalled read failure')
await expect(ctx.sessionReferences.prepare(agent, content, [{ sessionId: one.id }], new AbortController().signal))
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [{ sessionId: one.id }], new AbortController().signal))
.rejects.toThrow(/non-error signalled read failure/)
const duringRead = new AbortController()
@@ -472,7 +477,7 @@ describe('session reference discovery and preparation', () => {
duringRead.abort('cancelled during read')
throw new Error('read interrupted')
})
await expect(ctx.sessionReferences.prepare(agent, content, [{ sessionId: one.id }], duringRead.signal))
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [{ sessionId: one.id }], duringRead.signal))
.rejects.toThrow(expectCode('SESSION_REFERENCE_CANCELLED'))
const snapshot = await ctx.sessionQuery.readSurface(one.id)
@@ -482,7 +487,7 @@ describe('session reference discovery and preparation', () => {
return snapshot
})
const hangingRead = new AbortController()
const pending = ctx.sessionReferences.prepare(agent, content, [{ sessionId: one.id }], hangingRead.signal)
const pending = ctx.sessionReferenceResolver.prepare(agent, content, [{ sessionId: one.id }], hangingRead.signal)
await vi.waitFor(() => { expect(releaseRead).toBeTypeOf('function') })
const cancelledRead = expect(pending).rejects.toThrow(expectCode('SESSION_REFERENCE_CANCELLED'))
hangingRead.abort('cancelled while storage remained pending')
@@ -493,7 +498,7 @@ describe('session reference discovery and preparation', () => {
const abort = new AbortController()
abort.abort('host cancelled')
await expect(ctx.sessionReferences.prepare(agent, content, [{ sessionId: one.id }], abort.signal))
await expect(ctx.sessionReferenceResolver.prepare(agent, content, [{ sessionId: one.id }], abort.signal))
.rejects.toThrow(expectCode('SESSION_REFERENCE_CANCELLED'))
})
@@ -519,7 +524,7 @@ describe('session reference discovery and preparation', () => {
{ surfaceOp: 'append' },
)
const prepared = await ctx.sessionReferences.prepare(fakeAgent(target), [{ type: 'text', text: 'go' }], [{ sessionId: source.id }])
const prepared = await ctx.sessionReferenceResolver.prepare(fakeAgent(target), [{ type: 'text', text: 'go' }], [{ sessionId: source.id }])
const context = prepared.additionalContext
if (context?.content[0]?.type !== 'text') throw new Error('expected text context')
const data = promptData(context.content[0].text) as unknown[]
@@ -539,7 +544,8 @@ describe('session reference discovery and preparation', () => {
source.append(
'user/message',
createUserMessage({
content: [{ type: 'text', text: `${id}-${'界'.repeat(400)}` }], source: COMPACT_CHECKPOINT_SOURCE,
content: [{ type: 'text', text: `${id}-${'界'.repeat(400)}` }],
source: checkpointSource(id),
}),
{ surfaceOp: 'append' },
)
@@ -553,7 +559,7 @@ describe('session reference discovery and preparation', () => {
return source
})
const prepared = await ctx.sessionReferences.prepare(
const prepared = await ctx.sessionReferenceResolver.prepare(
fakeAgent(target),
[{ type: 'text', text: 'go' }],
sources.map(source => ({ sessionId: source.id })),
@@ -571,7 +577,7 @@ describe('session reference discovery and preparation', () => {
const ctx = await harness({ maxReferenceBytes: 16 })
const target = ctx.sessions.create(SessionId('target'))
const source = ctx.sessions.create(SessionId('source'))
await expect(ctx.sessionReferences.prepare(fakeAgent(target), [{ type: 'text', text: 'go' }], [{ sessionId: source.id }]))
await expect(ctx.sessionReferenceResolver.prepare(fakeAgent(target), [{ type: 'text', text: 'go' }], [{ sessionId: source.id }]))
.rejects.toThrow(expectCode('SESSION_REFERENCE_BUDGET_EXCEEDED'))
})
@@ -588,7 +594,7 @@ describe('session reference discovery and preparation', () => {
}),
{ surfaceOp: 'append' },
)
const prepared = await ctx.sessionReferences.prepare(
const prepared = await ctx.sessionReferenceResolver.prepare(
fakeAgent(target),
[{ type: 'text', text: 'use @source' }],
[{ sessionId: source.id }],
@@ -621,7 +627,8 @@ describe('session reference discovery and preparation', () => {
source.append(
'user/message',
createUserMessage({
content: [{ type: 'text', text: 'later compact checkpoint' }], source: COMPACT_CHECKPOINT_SOURCE,
content: [{ type: 'text', text: 'later compact checkpoint' }],
source: checkpointSource('later-source-mutation'),
}),
{
surfaceOp: { op: 'replace', start: original.seq, end: later.seq },
@@ -641,19 +648,19 @@ describe('session reference discovery and preparation', () => {
it('rejects direct invalid configuration before service publication', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(TestSessionQueryService)
expect(() => new SessionReferenceService(ctx, { maxReferences: 0 }))
await ctx.plugin(TestSessionQueryEngine)
expect(() => new SessionReferenceResolver(ctx, { maxReferences: 0 }))
.toThrow(expectCode('SESSION_REFERENCE_INVALID_CONFIG'))
const oversizedCtx = new Context()
await oversizedCtx.plugin(SessionStore)
await oversizedCtx.plugin(TestSessionQueryService)
expect(() => new SessionReferenceService(oversizedCtx, { maxReferences: 4 }))
await oversizedCtx.plugin(TestSessionQueryEngine)
expect(() => new SessionReferenceResolver(oversizedCtx, { maxReferences: 4 }))
.toThrow(expectCode('SESSION_REFERENCE_INVALID_CONFIG'))
const defaultCtx = new Context()
await defaultCtx.plugin(SessionStore)
await defaultCtx.plugin(TestSessionQueryService)
expect(() => new SessionReferenceService(defaultCtx)).not.toThrow()
await defaultCtx.plugin(TestSessionQueryEngine)
expect(() => new SessionReferenceResolver(defaultCtx)).not.toThrow()
})
})