refactor(session): separate attachment carrier policy from V4 migration

The event-type allowlist and opaque-payload attachment policy change live
Session authorization/export behavior independently of a format upgrade.
They belong in a focused PR against master, where supported native V3 nested
tool-result content must also be handled deliberately.

Restore the session-controller and archive implementation, tests and README
pairs to this layer's parent. Remove the test-only session-format-catalog
dependency and its lock entry with the migration-specific attachment tests.
The original changes are captured from immutable commit 85737364be9 for the
standalone work; migration identifier conversion and reader checks are intact.

This extraction keeps the integration stack's existing attachment behavior
until the separate policy change is reviewed. It makes no Session-format,
schema or version change.
This commit is contained in:
Tianyi Cui committed 2026-09-19 15:43:10 +08:00
1 parent 11ef33b336
commit d5302e33f8
12 files changed
+48 -236

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/api/session-controller/README.md
README.md: de20b2bacf0b563404b66f716c3ffdca01cb8058
README.zh.md: cc94e573b0bc5d52323d06d010c12457749b1914
README.md: 228ed88ed20b9f9581496468e47079afd1542e4f
README.zh.md: edebf8bdda249cf0ea6797db7dd77fcfa973074a
@@ -54,8 +54,6 @@ A resume blocked by an existing write handle returns `session/writer-held` with
Queue edits replace pending content with non-empty text only.
Attachment authorization inspects only declared content fields of recognized Session events and completed assistant stream blocks. Unknown event payloads, unrelated fields, and extension data cannot authorize attachment reads.
<a id="client-references"></a>
## Client references
@@ -54,8 +54,6 @@ Fork 复制 `atSeq` 所选的精确事件前缀,包含切点事件,允许在
队列编辑仅允许用非空文本替换待处理内容。
附件授权只检查已知 Session 事件声明的内容字段及已完成的 assistant 流块。未知事件载荷、无关字段和扩展数据不能授权读取附件。
<a id="client-references"></a>
## Client 引用
@@ -141,7 +141,6 @@
"@deepseek-ai/dsh-remote-mock": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-format-catalog": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
+18 -39
View File
@@ -620,16 +620,19 @@ function imageBlockIn(
if (!Array.isArray(content)) return undefined
for (const value of content) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) continue
const block = value as { readonly type?: unknown; readonly attachment?: unknown }
const block = value as { readonly type?: unknown; readonly attachment?: unknown; readonly content?: unknown }
if (block.type === 'image' && typeof block.attachment === 'object' && block.attachment !== null) {
const ref = block.attachment as ImageAttachmentRef
if (match(ref)) return ref
}
if (block.type === 'tool-result') {
const nested = imageBlockIn(block.content, match)
if (nested !== undefined) return nested
}
}
return undefined
}
/** Read only first-party declared content fields; unknown event payloads stay opaque. */
function imageInEvent(
event: SessionEvent,
match: (ref: ImageAttachmentRef) => boolean,
@@ -637,46 +640,22 @@ function imageInEvent(
const data = event.data as {
readonly content?: unknown
readonly message?: { readonly content?: unknown }
readonly inserted: readonly { readonly content?: unknown }[]
readonly messages: readonly { readonly content?: unknown }[]
readonly summary?: unknown
readonly rawOutput?: unknown
readonly inserted?: readonly { readonly content?: unknown }[]
}
// First-party event payloads can be present without their producer plugin mounted.
const type: string = event.type
switch (type) {
case 'user/message':
case 'tool/ptc-dispatch':
return imageBlockIn(data.content, match)
case 'system/message':
case 'developer/message':
case 'tool/result':
case 'team/message/queued':
return imageBlockIn(data.message?.content, match)
case 'agent/inbox/spliced':
case 'session/title-llm-request': {
const messages = type === 'agent/inbox/spliced' ? data.inserted : data.messages
for (const message of messages) {
const found = imageBlockIn(message.content, match)
if (found !== undefined) return found
}
return undefined
}
case 'compaction/summary':
return imageBlockIn(data.summary, match) ?? imageBlockIn(data.rawOutput, match)
case 'assistant/message': {
const found = imageBlockIn(data.message?.content, match)
if (found !== undefined) return found
break
}
case 'assistant/attempt': break
default: return undefined
}
const assistant = event as SessionEvent<'assistant/message' | 'assistant/attempt'>
for (const chunk of assistantStreamChunks(assistant.data.stream, 'block-end')) {
const found = imageBlockIn([chunk.block], match)
const direct = imageBlockIn(data.content, match)
if (direct !== undefined) return direct
const message = imageBlockIn(data.message?.content, match)
if (message !== undefined) return message
for (const inserted of data.inserted ?? []) {
const found = imageBlockIn(inserted.content, match)
if (found !== undefined) return found
}
if (event.type === 'assistant/message' || event.type === 'assistant/attempt') {
for (const chunk of assistantStreamChunks(event.data.stream, 'block-end')) {
const found = imageBlockIn([chunk.block], match)
if (found !== undefined) return found
}
}
return undefined
}
@@ -10,7 +10,6 @@ import SessionStore, {
SESSION_FORMAT_VERSION, Session, SessionId, SessionLogOffset, SessionSeq,
} from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader, UserMessage } from '@deepseek-ai/dsh-session'
import { createSessionFormatCatalogWithChildren, historicalSessionFormatCatalog } from '@deepseek-ai/dsh-session-format-catalog'
import { snapshotSubagentDescriptor, SUBAGENT_DESCRIPTOR_VERSION } from '@deepseek-ai/dsh-subagent'
import { subagentIdentityProjectionDefinition } from '@deepseek-ai/dsh-subagent/src/projection.ts'
import { describe, expect, it, vi } from 'vitest'
@@ -317,8 +316,7 @@ function imageRef(id: string): ImageAttachmentRef {
}
function event(type: string, seq: SessionSeq, data: unknown): SessionEvent {
const surface = ['user/message', 'system/message', 'developer/message', 'tool/result'].includes(type)
return { type, seq, time: seq + 1, data, ...surface ? { surfaceOp: 'append' } : {} } as SessionEvent
return { type, seq, time: seq + 1, data } as SessionEvent
}
async function persistedController(
@@ -350,90 +348,16 @@ async function persistedController(
}
describe('Session attachment authorization', () => {
it.each([
['system/message', 'message'], ['developer/message', 'message'], ['tool/result', 'message'],
['team/message/queued', 'message'], ['tool/ptc-dispatch', 'content'],
['session/title-llm-request', 'messages'], ['compaction/summary', 'summary'], ['compaction/summary', 'rawOutput'],
] as const)('reads the declared %s %s content without rewriting it', async (type, field) => {
const ref = imageRef(`${type}-${field}`)
const content = [{ type: 'image', attachment: ref }]
const source = type === 'system/message' ? { kind: 'system-prompt' }
: type === 'tool/result' ? { kind: 'tool', callId: 'declared-call' } : { kind: 'test' }
const data = field === 'message' ? { message: {
id: 'declared-message', role: type.split('/')[0], content, source,
...type === 'tool/result' ? { toolCallId: 'declared-call' } : {},
} }
: field === 'messages' ? { messages: [{ content: [] }, { content }] } : { [field]: content }
const events = [event(type, SessionSeq(0), data)]
const saved = JSON.stringify(events)
const readImage = vi.fn((image: ImageAttachmentRef) => Promise.resolve({ ref: image, data: Uint8Array.of(1) }))
const fixture = await persistedController(events, readImage)
try {
await expect(fixture.controller.attachment({ sessionId: fixture.sessionId, attachmentId: ref.attachmentId }))
.resolves.toEqual({ attachment: ref, data: 'AQ==' })
expect(readImage).toHaveBeenCalledExactlyOnceWith(ref)
expect(JSON.stringify(events)).toBe(saved)
} finally {
await fixture.ctx.fiber.dispose()
}
})
it('ignores unrelated fields of a known message event and its content blocks', async () => {
const ref = imageRef('not-a-content-occurrence')
const content = [{ type: 'image', attachment: ref }]
const stored = event('user/message', SessionSeq(0), {
id: 'metadata', role: 'user', source: { kind: 'user' },
content: [{ type: 'text', text: 'no image', content }, { type: 'plugin:vendor', data: { content }, content }],
message: { content }, inserted: [{ content }],
})
const readImage = vi.fn((image: ImageAttachmentRef) => Promise.resolve({ ref: image, data: Uint8Array.of(1) }))
const fixture = await persistedController([stored], readImage)
try {
await expect(fixture.controller.attachment({ sessionId: fixture.sessionId, attachmentId: ref.attachmentId }))
.rejects.toMatchObject({ code: 'session/attachment-invalid' })
expect(readImage).not.toHaveBeenCalled()
} finally {
await fixture.ctx.fiber.dispose()
}
})
it('denies attachment access from a migrated unknown ignorable event', async () => {
const ref = imageRef('opaque-event-image')
const content = [{ type: 'message', message: {
role: 'tool', toolCallId: 'opaque-call', content: [{ type: 'image', attachment: ref }],
} }]
const header = { type: 'session', version: 3, id: 'cold-attachment', createdAt: 1, isSeeded: false, delegationDepth: 0 }
const original = { type: 'external/image-record', seq: 0, time: 1, ignorable: true, data: {
content, message: { content }, inserted: [{ content }],
stream: [{ type: 'chunk', time: 1, chunk: { type: 'block-end', index: 0, block: content[0] } }],
} }
const historical = historicalSessionFormatCatalog.createRestore(header, { recovery: 'strict', validation: 'current' })
historical.decodeRow(original)
expect(historical.finish().header.version).toBe(3)
const reader = createSessionFormatCatalogWithChildren([]).createRestore(header, { recovery: 'strict', validation: 'current' })
reader.decodeRow(original)
const converted = reader.finish()
expect(converted.events[0]).toMatchObject({ type: 'plugin:external/image-record', data: original.data })
const readImage = vi.fn((image: ImageAttachmentRef) => Promise.resolve({ ref: image, data: Uint8Array.of(1) }))
const fixture = await persistedController(converted.events as SessionEvent[], readImage)
try {
await expect(fixture.controller.attachment({ sessionId: fixture.sessionId, attachmentId: ref.attachmentId }))
.rejects.toMatchObject({ code: 'session/attachment-invalid', details: { reason: 'ATTACHMENT_NOT_REFERENCED' } })
expect(readImage).not.toHaveBeenCalled()
} finally {
await fixture.ctx.fiber.dispose()
}
})
it('finds references in direct, message, inserted, and streamed content', async () => {
it('finds references in direct, message, inserted, nested, and streamed content', async () => {
const nested = imageRef('nested')
const message = imageRef('message')
const inserted = imageRef('inserted')
const streamed = imageRef('streamed')
const events: SessionEvent[] = [
{ ...event('user/message', SessionSeq(0), {
id: 'direct', role: 'user', source: { kind: 'user' },
content: [null, [], { type: 'text', text: 'none' }, { type: 'image', attachment: nested }],
{ ...event('fixture/direct', SessionSeq(0), {
content: [null, [], { type: 'tool-result', content: [{ type: 'text', text: 'none' }] }, {
type: 'tool-result', content: [{ type: 'image', attachment: nested }],
}],
}), ignorable: true as const },
{
type: 'assistant/message', seq: SessionSeq(1), time: 2, surfaceOp: 'append',
@@ -527,7 +451,7 @@ describe('Session attachment authorization', () => {
]) {
const ref = imageRef(`failure-${thrown.name}`)
const fixture = await persistedController(
[event('user/message', SessionSeq(0), { id: 'failure', role: 'user', source: { kind: 'user' }, content: [{ type: 'image', attachment: ref }] })],
[event('fixture/content', SessionSeq(0), { content: [{ type: 'image', attachment: ref }] })],
() => Promise.reject(thrown),
)
await expectFailure(fixture.controller.attachment({
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session-query/session-log-export/README.md
README.md: 7fa004df954260de16a6a151e3110e6adfbc0f09
README.zh.md: 147ee93b3929473d4e78024ff0b4ccbd040a1220
README.md: 79b8d572a75f57aa13a0dfaa823a04ad1e818f87
README.zh.md: d6229927130e7200cd21df0da64d0ce9ec80ab2f
@@ -59,8 +59,6 @@ The Web bundle mounts the package with Connection, `dsh-commands`, `dsh-client-u
The dialog reports three phases: preparing, download started, or failed. Closing the dialog does not cancel an in-flight download, and the dialog does not reopen when that operation later settles. One session admits one active download at a time; repeated gestures share that operation. The export includes the live session's newest events: the host endpoint flushes a live root session before reading, so a slash-triggered ZIP includes the `command/run` and `command/done` pair that started the download; cold persisted sessions need no flush. Each logical log uses the current generation's canonical filename inside the archive (`session.jsonl` for v0, otherwise `session.vN.jsonl`), including beneath each sub-session directory. Images use `media/<attachmentId>.<ext>`, and generic files use `files/<digest-prefix>/<digest>/<name>`. Generic-file bytes are read and compressed as bounded chunks, so exporting a large upload does not buffer it in full.
Attachment collection reads only declared content fields of recognized Session events and completed assistant stream blocks. Unknown event payloads, unrelated fields, and extension data remain opaque; the exported log retains them unchanged.
### Failures
The dialog shows a preparation error when the preflight fails before ZIP streaming starts — for example an unreachable or misconfigured host endpoint. A descendant or attachment read failure after the browser accepts the GET is reported by the browser download manager, not by the dialog.
@@ -59,8 +59,6 @@ Web bundle 将本包与 Connection、`dsh-commands`、`dsh-client-ui-commands`
弹窗报告三个阶段:准备中、开始下载或失败。关闭弹窗不会取消正在进行的下载,该操作随后结束时弹窗也不会重新打开。每个会话同时只允许一项下载,重复操作共用该任务。导出包含实时会话的最新事件:Host 端点在读取前会 flush 活动的根会话,因此斜杠命令触发的 ZIP 会包含启动下载的 `command/run` 与 `command/done` 事件对;非活动的持久化会话不需要 flush。每份逻辑日志在归档中使用当前 generation 的规范文件名(v0 为 `session.jsonl`,其他版本为 `session.vN.jsonl`),每个子会话目录下也遵循同一规则。图片使用 `media/<attachmentId>.<ext>`,通用文件使用 `files/<digest-prefix>/<digest>/<name>`。通用文件以有界分块读取并压缩,因此导出大型上传文件时不会把它完整缓冲进内存。
附件收集只读取已知 Session 事件声明的内容字段及已完成的 assistant 流块。未知事件载荷、无关字段和扩展数据保持不透明,导出的日志原样保留它们。
### 失败
当 ZIP 流式传输开始前的预检失败时——例如 Host 端点不可达或配置错误——弹窗显示准备阶段错误。浏览器接受 GET 后发生的子会话或附件读取失败由浏览器下载管理器报告,不通过弹窗报告。
@@ -196,8 +196,9 @@ function fileEntryPath(ref: FileAttachmentRef): string {
}
/**
* Collect attachment references from one flat content array; all other block fields remain opaque.
* @param content - a declared event content array.
* Collect every attachment reference inside one content array, descending into
* nested tool results the way the live attachment route does.
* @param content - an event content array (or nested tool-result content).
* @param images - image dedupe map keyed by attachment id.
* @param files - file dedupe map keyed by attachment id and stored name.
*/
@@ -212,7 +213,7 @@ function collectAttachmentRefs(
while (pending.length > 0) {
const value = pending.pop()
if (typeof value !== 'object' || value === null || Array.isArray(value)) continue
const block = value as { type?: unknown; attachment?: unknown }
const block = value as { type?: unknown; attachment?: unknown; content?: unknown }
if (block.type === 'image' && typeof block.attachment === 'object' && block.attachment !== null) {
const ref = block.attachment as ImageAttachmentRef
images.set(String(ref.attachmentId), ref)
@@ -221,12 +222,16 @@ function collectAttachmentRefs(
const ref = block.attachment as FileAttachmentRef
files.set(`${String(ref.attachmentId)}\u0000${ref.name}`, ref)
}
if (Array.isArray(block.content)) {
for (const item of block.content) pending.push(item)
}
}
}
/**
* Collect references only from declared first-party content fields and completed
* Assistant blocks. Unknown events and unrelated payload fields remain opaque.
* Collect every attachment reference one session event carries, across the same
* carriers the live attachment route scans (direct content, message content,
* inserted messages, and completed blocks in embedded Assistant streams).
* @param event - one parsed JSONL event object.
* @param images - image dedupe map keyed by attachment id.
* @param files - file dedupe map keyed by attachment id and stored name.
@@ -236,40 +241,18 @@ function collectEventAttachmentRefs(
images: Map<string, ImageAttachmentRef>,
files: Map<string, FileAttachmentRef>,
): void {
if (typeof event !== 'object' || event === null || Array.isArray(event)) return
const row = event as { type?: unknown; data?: unknown }
const data = row.data
const data = (event as { data?: unknown }).data
if (typeof data !== 'object' || data === null) return
const carrier = data as {
content?: unknown
message?: { content?: unknown }
inserted?: Array<{ content?: unknown }>
messages?: Array<{ content?: unknown }>
summary?: unknown
rawOutput?: unknown
stream?: Array<{ type?: unknown; chunk?: { type?: unknown; block?: unknown } }>
}
switch (row.type) {
case 'user/message': case 'tool/ptc-dispatch':
collectAttachmentRefs(carrier.content, images, files)
return
case 'system/message': case 'developer/message': case 'tool/result': case 'team/message/queued':
collectAttachmentRefs(carrier.message?.content, images, files)
return
case 'agent/inbox/spliced': case 'session/title-llm-request': {
const messages = row.type === 'agent/inbox/spliced' ? carrier.inserted : carrier.messages
for (const message of messages ?? []) collectAttachmentRefs(message.content, images, files)
return
}
case 'compaction/summary':
collectAttachmentRefs(carrier.summary, images, files)
collectAttachmentRefs(carrier.rawOutput, images, files)
return
case 'assistant/message':
collectAttachmentRefs(carrier.message?.content, images, files)
break
case 'assistant/attempt': break
default: return
collectAttachmentRefs(carrier.content, images, files)
if (carrier.message !== undefined) collectAttachmentRefs(carrier.message.content, images, files)
if (carrier.inserted !== undefined) {
for (const message of carrier.inserted) collectAttachmentRefs(message.content, images, files)
}
if (carrier.stream !== undefined) {
for (const record of carrier.stream) {
@@ -503,15 +503,6 @@ describe('session.export download endpoint', () => {
expect(strFromU8(files[exportLogName] as Uint8Array)).toBe('')
})
it('preserves non-event JSON and absent content carriers without discovering attachment lookalikes', async () => {
const content = [{ type: 'image', attachment: { attachmentId: 'not-an-occurrence', mediaType: 'image/png' } }]
const text = [null, [], false, { type: 'agent/inbox/spliced', data: { content } },
{ type: 'session/title-llm-request', data: { content } }].map(value => JSON.stringify(value)).join('\n')
const files = await directZipFiles(text)
expect(Object.keys(files)).toEqual([exportLogName])
expect(strFromU8(files[exportLogName] as Uint8Array)).toBe(text)
})
it('waits for response pull capacity before reading the next archive entry', async () => {
const filler = {
type: 'user/message', seq: SessionSeq(2), time: 1000,
@@ -806,63 +797,10 @@ describe('session.export download endpoint', () => {
await expect(response.arrayBuffer()).rejects.toThrow('file bytes missing')
})
it.each([
['system/message', 'message'], ['developer/message', 'message'], ['team/message/queued', 'message'],
['tool/ptc-dispatch', 'content'], ['session/title-llm-request', 'messages'],
['compaction/summary', 'summary'], ['compaction/summary', 'rawOutput'],
] as const)('exports attachments from the declared %s %s content', async (type, field) => {
const content = [{ type: 'image', attachment: { attachmentId: 'declared-image', mediaType: 'image/png', bytes: 4, width: 2, height: 2 } }]
const data = field === 'message' ? { message: { content } }
: field === 'messages' ? { messages: [{ content: [] }, { content }] } : { [field]: content }
const stored = log('session-root', undefined, [{ type, seq: SessionSeq(1), time: 1000, data } as unknown as SessionEvent])
const api = await buildApi({ 'session-root': stored })
const response = await toFetchHandler(api).fetch(new Request('http://host/api/session.export?sessionId=session-root'))
const files = unzipSync(await responseBytes(response))
expect(Object.keys(files).sort()).toEqual(['media/declared-image.png', exportLogName].sort())
expect(new TextDecoder().decode(files[exportLogName])).toBe(logText(stored))
})
it('keeps unrelated event fields and content-block extension fields out of attachment collection', async () => {
const content = [{ type: 'image', attachment: { attachmentId: 'not-an-occurrence', mediaType: 'image/png', bytes: 4, width: 2, height: 2 } }]
const event = { type: 'user/message', seq: SessionSeq(1), time: 1000, data: {
content: [{ type: 'text', text: 'no image', content }, { type: 'plugin:vendor', data: { content }, content }],
message: { content }, inserted: [{ content }],
stream: [{ type: 'chunk', chunk: { type: 'block-end', block: content[0] } }],
} } as unknown as SessionEvent
const stored = log('session-root', undefined, [event])
const readImage = vi.fn((ref: ImageAttachmentRef) => Promise.resolve(storedImage(String(ref.attachmentId), ref.mediaType)))
const api = await buildApi({ 'session-root': stored }, [], { attachments: readImage })
const response = await toFetchHandler(api).fetch(new Request('http://host/api/session.export?sessionId=session-root'))
const files = unzipSync(await responseBytes(response))
expect(Object.keys(files)).toEqual([exportLogName])
expect(new TextDecoder().decode(files[exportLogName])).toBe(logText(stored))
expect(readImage).not.toHaveBeenCalled()
})
it('exports an unknown ignorable event without reading attachments from its opaque payload', async () => {
const image = { type: 'image', attachment: { attachmentId: 'opaque-image', mediaType: 'image/png', bytes: 4, width: 2, height: 2 } }
const file = { type: 'file', attachment: { attachmentId: `sha256:${'e'.repeat(64)}`, name: 'opaque.txt', bytes: 5 } }
const content = [{ type: 'message', message: { role: 'tool', toolCallId: 'opaque', content: [image, file] } }]
const opaque = { type: 'plugin:external/image-record', seq: SessionSeq(1), time: 1000, ignorable: true, data: {
content, message: { content }, inserted: [{ content }],
stream: [{ type: 'chunk', time: 1000, chunk: { type: 'block-end', index: 0, block: content[0] } }],
} } as unknown as SessionEvent
const stored = log('session-root', undefined, [opaque])
const readImage = vi.fn((ref: ImageAttachmentRef) => Promise.resolve(storedImage(String(ref.attachmentId), ref.mediaType)))
const readFileStream = vi.fn(async function* (_ref: FileAttachmentRef) { yield new Uint8Array([1]) })
const api = await buildApi({ 'session-root': stored }, [], { attachments: readImage, readFileStream })
const response = await toFetchHandler(api).fetch(new Request('http://host/api/session.export?sessionId=session-root'))
const files = unzipSync(await responseBytes(response))
expect(Object.keys(files)).toEqual([exportLogName])
expect(new TextDecoder().decode(files[exportLogName])).toBe(logText(stored))
expect(readImage).not.toHaveBeenCalled()
expect(readFileStream).not.toHaveBeenCalled()
})
it('collects media referenced from flat tool results', async () => {
it('collects media referenced from nested tool results', async () => {
const nested = {
type: 'tool/result', seq: SessionSeq(2), time: 2000,
data: { message: { role: 'tool', toolCallId: 'outer', content: [{ type: 'image', attachment: { attachmentId: 'nested-1', mediaType: 'image/webp', bytes: 4, width: 2, height: 2 } }] } },
type: 'assistant/message', seq: SessionSeq(2), time: 2000,
data: { content: [{ type: 'tool-result', content: [{ type: 'image', attachment: { attachmentId: 'nested-1', mediaType: 'image/webp', bytes: 4, width: 2, height: 2 } }] }] },
} as unknown as SessionEvent
const api = await buildApi({ 'session-root': log('session-root', undefined, [nested]) })
const response = await toFetchHandler(api).fetch(
@@ -880,7 +818,7 @@ describe('session.export download endpoint', () => {
data: { message: { role: 'assistant', content: ['noise', block('wrapped-1', 'image/jpeg')] } },
} as unknown as SessionEvent
const inserted = {
type: 'agent/inbox/spliced', seq: SessionSeq(3), time: 3000,
type: 'context/inserted', seq: SessionSeq(3), time: 3000,
data: { inserted: [{ content: [block('inserted-1', 'image/gif')] }] },
} as unknown as SessionEvent
const attempt = {
-3
View File
@@ -1142,9 +1142,6 @@ importers:
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
'@deepseek-ai/dsh-session-format-catalog':
specifier: workspace:^
version: link:../../session/session-format-catalog
'@deepseek-ai/dsh-session-persistence':
specifier: workspace:^
version: link:../../session/session-persistence