Merge remote-tracking branch 'origin/master' into feat/web-session-log-export

# Conflicts:
#	docs/subsystems/persistence.i18n.yaml
#	docs/subsystems/persistence.md
#	docs/subsystems/persistence.zh.md
#	packages/client/ui-trajectory/README.i18n.yaml
#	packages/client/ui-trajectory/README.md
#	packages/client/ui-trajectory/README.zh.md
#	packages/client/ui-trajectory/package.json
#	packages/client/ui-trajectory/src/client/TrajectoryView.tsx
#	packages/client/ui-trajectory/src/client/index.ts
#	packages/client/ui-trajectory/tests/client-bundle.spec.ts
#	packages/client/ui-trajectory/tests/views.spec.tsx
#	packages/host/apiproxy/package.json
#	pnpm-lock.yaml
This commit is contained in:
_Kerman
2026-08-11 12:42:53 +08:00
2172 changed files with 30345 additions and 15794 deletions
@@ -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' }))
@@ -331,11 +354,11 @@ describe('agentPreset.select', () => {
})
it('records the switch in the log, and the list reads it back', async () => {
const { api, ctx } = await harness(['standard', 'core-web'])
const { api, ctx } = await harness(['standard', 'minimal'])
await api.sessions.create(request({ sessionId: SessionId('sel-log'), agentPreset: 'standard' }))
await api.agentPresets.select(
request({ sessionId: SessionId('sel-log'), agentPreset: 'core-web' }))
request({ sessionId: SessionId('sel-log'), agentPreset: 'minimal' }))
// The header is written once at creation, so the switch lives in the log —
// this is what a restart replays and what every projection resolves from.
@@ -343,11 +366,11 @@ describe('agentPreset.select', () => {
const session = ctx.sessions.get(SessionId('sel-log'))
if (session === undefined) throw new Error('unreachable')
expect(session.header.agentPreset).toBe('standard')
expect(resolveSessionPreset(session)).toBe('core-web')
expect(resolveSessionPreset(session)).toBe('minimal')
const listed = await api.sessions.list(request({}))
if (!listed.result.ok) throw new Error('unreachable')
expect(listed.result.value.items.find(item => item.sessionId === 'sel-log')?.agentPreset)
.toBe('core-web')
.toBe('minimal')
})
it('frames the committed switch so clients can drop that session\'s catalogs', async () => {
@@ -382,14 +405,14 @@ describe('agentPreset.select', () => {
})
it('serializes two concurrent selects on one session', async () => {
const { api, ctx } = await harness(['standard', 'core-web'])
const { api, ctx } = await harness(['standard', 'minimal'])
await api.sessions.create(request({ sessionId: SessionId('sel-race'), agentPreset: 'standard' }))
// Both pass the blank check; unserialized, the second unmount finds no
// record because the first already removed it, and two compositions end up
// in one agent layer. The client's busy flag is not enforcement.
const [first, second] = await Promise.all([
api.agentPresets.select(request({ sessionId: SessionId('sel-race'), agentPreset: 'core-web' })),
api.agentPresets.select(request({ sessionId: SessionId('sel-race'), agentPreset: 'minimal' })),
api.agentPresets.select(request({ sessionId: SessionId('sel-race'), agentPreset: 'standard' })),
])
@@ -612,7 +635,7 @@ describe('skills over the layered host registry', () => {
})
it('resolves a cold session to its recorded preset standing key', async () => {
const { api, ctx } = await harness(['standard', 'core-web'])
const { api, ctx } = await harness(['standard', 'minimal'])
const seen: unknown[] = []
ctx.provide('skills', {
list: (options: { scope?: unknown }) => {
@@ -620,12 +643,12 @@ describe('skills over the layered host registry', () => {
return Promise.resolve([])
},
} as never)
ctx.sessions.create(SessionId('h2'), { meta: { cwd: '/workspace/cold', agentPreset: 'core-web' } })
ctx.sessions.create(SessionId('h2'), { meta: { cwd: '/workspace/cold', agentPreset: 'minimal' } })
const response = await api.skills.list(request({ sessionId: SessionId('h2') }))
expect(response.result).toMatchObject({ ok: true, value: { skills: [] } })
expect(seen).toEqual([standingKeys.get('core-web')])
expect(seen).toEqual([standingKeys.get('minimal')])
})
it('serves the global view when the roster no longer supplies the recorded preset', async () => {
@@ -648,8 +671,8 @@ describe('skills over the layered host registry', () => {
describe('session.history presenter scope', () => {
it('asks the roster for the RECORDED preset\'s standing key on a cold read', async () => {
const { api } = await harness(['standard', 'core-web'])
await api.sessions.create(request({ sessionId: SessionId('p1'), agentPreset: 'core-web' }))
const { api } = await harness(['standard', 'minimal'])
await api.sessions.create(request({ sessionId: SessionId('p1'), agentPreset: 'minimal' }))
// Cold: creation registered a live agent in this harness, so simulate the
// cold path by asking for a session only persistence knows... the harness
// has no persistence, so read the live one and assert no roster query.
@@ -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' }
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -6,7 +6,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { unzipSync, strFromU8 } from 'fflate'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import type { SessionHeader, SessionId } from '@deepseek-ai/dsh-session'