diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index 6395c3af34..7db0750027 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -652,9 +652,9 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy { // Snapshot at request time, deliver after the transit delay (mirrors a real host under latency). const page = pageOf(log, request.payload.beforeSeq, request.payload.maxMessages ?? 50) // Tail page carries the projections block (host parallel: one consistent - // cut over the registered units, asOfSeq = window tail seq); an empty - // log has no cut to stamp, so the block stays absent. - const projections = request.payload.beforeSeq === undefined && log.length > 0 + // cut over the registered units; asOfSeq = window tail seq, -1 on an + // empty log — the host's session.seq-1 convention). + const projections = request.payload.beforeSeq === undefined ? { asOfSeq: log.length - 1, values: projectionValuesOf(log) } : undefined const doomed = failNextHistory diff --git a/packages/client/connection/tests/fixture.spec.ts b/packages/client/connection/tests/fixture.spec.ts index 11558b58f7..8eff350cbf 100644 --- a/packages/client/connection/tests/fixture.spec.ts +++ b/packages/client/connection/tests/fixture.spec.ts @@ -65,10 +65,11 @@ describe('createFixtureApi', () => { const clamped = await api.sessions.history(req({ sessionId: sid('fx-alpha'), beforeSeq: -5, maxMessages: 10 })) if (!clamped.result.ok) throw new Error('clamped failed') expect(clamped.result.value.events).toEqual([]) - // Unknown session: empty page, not an error (history of a bare id). + // Unknown session: empty page, not an error (history of a bare id). The + // tail block still rides it — empty-log cut at -1, the host convention. const empty = await api.sessions.history(req({ sessionId: sid('no-such'), maxMessages: 10 })) if (!empty.result.ok) throw new Error('empty failed') - expect(empty.result.value).toEqual({ events: [], hasMore: false }) + expect(empty.result.value).toEqual({ events: [], hasMore: false, projections: { asOfSeq: -1, values: {} } }) }) it('emits the todo/write snapshot at the real tool boundary: between tool/call and tool/result, timestamps monotonic', async () => { diff --git a/packages/client/runtime/package.json b/packages/client/runtime/package.json index ff994dad72..a1f3cc9cd2 100644 --- a/packages/client/runtime/package.json +++ b/packages/client/runtime/package.json @@ -37,6 +37,7 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/dsh-session-title": "workspace:^", "immer": "^10.1.1", "react": "^18.2.0", "zustand": "~4.4.7" diff --git a/packages/client/runtime/src/client/sessions/manager.ts b/packages/client/runtime/src/client/sessions/manager.ts index 43b4ada94d..66a0d00dd9 100644 --- a/packages/client/runtime/src/client/sessions/manager.ts +++ b/packages/client/runtime/src/client/sessions/manager.ts @@ -9,6 +9,10 @@ import { transportError } from '@deepseek-ai/dsh-host-apiproxy/api' import { mergeOrderedBaseline } from '../ordered-baseline.ts' import type { SessionListEntry, TitledSessionSummary } from './lineage.ts' import { flattenLineage } from './lineage.ts' +// Type-only merge edge: the title domain's client-namespace outlet declares +// the 'title' projection key this manager projects into list rows (and any +// useProjection('title') consumer reads). Zero value imports by construction. +import type {} from '@deepseek-ai/dsh-session-title/client' import { Notifier } from './notifier.ts' import { ProjectionValueStore } from './projection-store.ts' import { Session } from './session.ts' diff --git a/packages/client/runtime/tsconfig.json b/packages/client/runtime/tsconfig.json index afb8b76cb3..eea6a26f03 100644 --- a/packages/client/runtime/tsconfig.json +++ b/packages/client/runtime/tsconfig.json @@ -26,6 +26,9 @@ { "path": "../../session-projection/session-projection" }, + { + "path": "../../session-title/session-title" + }, { "path": "../../llm/llm" }, diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index 86d63b171c..0b184dcbdf 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -50,6 +50,7 @@ "devDependencies": { "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/dsh-tool-todo": "workspace:^", "@deepseek-ai/dsh-client-ui-layout": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-slash": "workspace:^", diff --git a/packages/client/ui-conversation/src/client/skeleton/TodoPanel.tsx b/packages/client/ui-conversation/src/client/skeleton/TodoPanel.tsx index a148b28ca0..b7ef46271a 100644 --- a/packages/client/ui-conversation/src/client/skeleton/TodoPanel.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/TodoPanel.tsx @@ -8,18 +8,11 @@ import { useId, useState } from 'react' import type { Context } from 'cordis' import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' -import type { TodoItem } from '@deepseek-ai/dsh-client-runtime/client' - -// Client-side view of the todos projection key. The authoritative merge lives -// with the domain host unit (tool-todo), whose program never overlaps the -// client's, so this consumer restates the identical member through the same -// pure-type outlet (any program holding both merges rejects drift). -declare module '@deepseek-ai/dsh-session-projection/types' { - interface SessionProjectionMap { - /** The agent's current whole todo list (latest `todo/write` snapshot), or `null` before the first write. */ - todos: TodoItem[] | null - } -} +// The domain's client-namespace pure-type outlet: one import edge delivers +// the `todos` projection-key merge (single source, no consumer-side restated +// declare) and the payload type. Type-only by construction — the outlet is +// free of host value imports, so no host Context merge enters this program. +import type { TodoItem } from '@deepseek-ai/dsh-tool-todo/client' import { IconChevronDownOutline14, IconChevronUpOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' import css from './TodoPanel.module.css' diff --git a/packages/client/ui-conversation/tsconfig.json b/packages/client/ui-conversation/tsconfig.json index 3363771deb..32ba48e8fe 100644 --- a/packages/client/ui-conversation/tsconfig.json +++ b/packages/client/ui-conversation/tsconfig.json @@ -26,6 +26,9 @@ { "path": "../../session-projection/session-projection" }, + { + "path": "../../todo/tool-todo" + }, { "path": "../ui-slash" }, diff --git a/packages/session-title/session-title/src/client/types.ts b/packages/session-title/session-title/src/client.ts similarity index 100% rename from packages/session-title/session-title/src/client/types.ts rename to packages/session-title/session-title/src/client.ts diff --git a/packages/todo/tool-todo/src/client/types.ts b/packages/todo/tool-todo/src/client.ts similarity index 100% rename from packages/todo/tool-todo/src/client/types.ts rename to packages/todo/tool-todo/src/client.ts