fix(session): retain JSONL resources through provider-triggered teardown

This commit is contained in:
turtle1999 committed 2026-10-09 13:46:43 +08:00
1 parent 15c10e0e87
commit 4d9047be2d
94 files changed
+457 -146

No files matched your search

@@ -11,7 +11,7 @@ import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import ToolResultPruner from '@deepseek-ai/dsh-compaction-tool-result-pruner'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as SessionCheckpointPolicy from '@deepseek-ai/dsh-session-checkpoint-policy'
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic'
import type { BasicCompactionConfig } from '@deepseek-ai/dsh-compaction-basic'
@@ -11,7 +11,7 @@ import {
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import { createUserMessage, ToolCallId, createMessage } from '@deepseek-ai/dsh-llm'
import { SessionSeq, SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { logPath } from '../../../../../../packages/session/session-persistence-jsonl/src/format.ts'
import { describe, expect, it } from 'vitest'
@@ -17,7 +17,7 @@ import {
type SessionEvent,
type SessionHeader,
} from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import {
generationLogFilename,
generationLogPath,
@@ -16,7 +16,7 @@ import {
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionSeq, SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { describe, expect, it } from 'vitest'
const fixtureDir = fileURLToPath(new URL('./expected/subagent-diagnostic', import.meta.url))
@@ -16,7 +16,7 @@ import {
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import { createMessage, createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { SessionSeq, SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { describe, expect, it } from 'vitest'
const fixtureDir = fileURLToPath(new URL('./expected/subagent-inheritance', import.meta.url))
@@ -23,7 +23,7 @@ import {
type SessionEvent,
type SessionHeader,
} from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { logPath } from '../../../../../../packages/session/session-persistence-jsonl/src/format.ts'
import { renderAgentInstructions } from '@deepseek-ai/dsh-agent-instructions'
import { resolveConfig, workspaceBaselineIdentity } from '@deepseek-ai/dsh-agent-instructions/src/config.ts'
+1 -1
View File
@@ -78,7 +78,7 @@ import {
type SessionEvent,
type SessionHeader,
} from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
// Empty type imports carry the webServer/agents/sessionPersistence Context merges.
import type {} from '@deepseek-ai/dsh-host-webserver'
import type {} from '@deepseek-ai/dsh-agent'
@@ -9,7 +9,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { assertBuiltBenchmarkRuntime } from '../support/built-worker.ts'
import { PARENT_ID, response, resultText, syntheticHistory, TIME_ZERO, WORKLOAD } from './workload.ts'
@@ -5,7 +5,7 @@ import { Context } from '@deepseek-ai/cordis'
import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SessionQueryEngine from '@deepseek-ai/dsh-session-query'
import SubagentRuntime, { SUBAGENT_DESCRIPTOR_VERSION } from '@deepseek-ai/dsh-subagent'
@@ -9,7 +9,7 @@ import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SessionController from '@deepseek-ai/dsh-api-session-controller'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, type SessionHeader } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SessionProjectionCache, { projectionCacheDomainSpec } from '@deepseek-ai/dsh-session-projection-cache'
import SqliteSessionQueryEngine from '@deepseek-ai/dsh-session-query-sqlite'
@@ -12,7 +12,7 @@ import SessionController from '@deepseek-ai/dsh-api-session-controller'
import { currentSessionMessageProjections } from '@deepseek-ai/dsh-session-format-catalog/message-projections'
import { Session, SessionLogOffset } from '@deepseek-ai/dsh-session'
import type { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionProjectionCache, { checkpointRecord, projectionCacheDomainSpec } from '@deepseek-ai/dsh-session-projection-cache'
import type { CheckpointRecord } from '@deepseek-ai/dsh-session-projection-cache'
import SqliteSessionQueryEngine from '@deepseek-ai/dsh-session-query-sqlite'
@@ -6,7 +6,7 @@ import { join } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { Session, SessionId, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import {
runBuiltBenchmarkWorker,
type BuiltBenchmarkWorkerRun,
@@ -13,7 +13,7 @@ import SessionStore, {
SessionPreparation,
} from '@deepseek-ai/dsh-session'
import type { AgentHandle } from '@deepseek-ai/dsh-agent'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SessionQueryEngine from '@deepseek-ai/dsh-session-query'
import type {
@@ -0,0 +1,16 @@
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/jsonl-persistence-plugin/guide.md
/:
en: bfa70c835a3ee484
zh: 96d7498ef521de6c
/jsonl-persistence-uses-a-namespace-plugin-import:
en: 758b9bedc071d6c7
zh: 8bbde76978fa3791
/jsonl-persistence-uses-a-namespace-plugin-import/change:
en: ae72fd2d26600080
zh: 9a618b92724cfa1a
/jsonl-persistence-uses-a-namespace-plugin-import/migration:
en: a220187cd899cc5f
zh: 2afe0d9f99cc26ce
@@ -0,0 +1,18 @@
---
kind: upgrade-guide
description: "The JSONL persistence package replaces its default service-class export with named function-plugin exports."
---
# JSONL persistence uses a namespace plugin import
English | [中文](guide.zh.md)
## Change
`@deepseek-ai/dsh-session-persistence-jsonl` exports `name`, `Config`, and `apply` instead of a default service class. This affects code that imports or constructs that class. The plugin owns listeners and handles through the consumer unloads triggered by removing this provider; concurrent root disposal remains unordered. The `sessionPersistence` service API, YAML configuration, and stored files remain unchanged.
## Migration
1. Replace default imports with `import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'`. Continue mounting with `ctx.plugin(JsonlSessionPersistence, config)`.
2. Replace direct class construction with plugin mounting. Consumers declare `inject: ['sessionPersistence']` and use `ctx.sessionPersistence`; use the `SessionPersistence` type from `@deepseek-ai/dsh-session-persistence` for service annotations.
3. Await plugin startup and confirm a session can be created, flushed, and reopened. Register asynchronous consumer cleanup in the scope that injects `sessionPersistence` so backend unloading waits for it.
@@ -0,0 +1,18 @@
---
kind: upgrade-guide
description: "JSONL 持久化包将默认导出的服务类改为函数插件的命名导出。"
---
# JSONL 持久化改用 namespace 插件导入
[English](guide.md) | 中文
## 变更
`@deepseek-ai/dsh-session-persistence-jsonl` 导出 `name`、`Config` 和 `apply`,不再默认导出服务类。这会影响导入或构造该类的代码。插件会保留监听器和句柄,直到移除该提供者所触发的消费者卸载完成;根作用域的并发卸载仍不保证顺序。`sessionPersistence` 服务 API、YAML 配置和存储文件保持不变。
## 迁移
1. 将默认导入替换为 `import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'`,继续通过 `ctx.plugin(JsonlSessionPersistence, config)` 挂载。
2. 将直接构造类实例改为挂载插件。消费者声明 `inject: ['sessionPersistence']` 并使用 `ctx.sessionPersistence`;服务类型标注使用 `@deepseek-ai/dsh-session-persistence` 的 `SessionPersistence` 类型。
3. 等待插件启动完成,确认能够创建、flush 并重新打开会话。将消费者的异步清理注册到注入 `sessionPersistence` 的作用域中,以便后端卸载时等待它完成。
+1 -1
View File
@@ -23,7 +23,7 @@ import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, St
import { type GenerateOptions, LlmAdapter, ReasoningEffortId, type LlmResolvedModelInfo, type StreamChunk } from '@deepseek-ai/dsh-llm'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import * as AcpPlugin from '../src/index.ts'
import type { AcpConfig } from '../src/index.ts'
@@ -9,7 +9,7 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
import type { SessionHeader } from '@deepseek-ai/dsh-session'
import type { SessionPersistenceSnapshot } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SessionProjectionCache, {
checkpointRecord,
@@ -13,7 +13,7 @@ import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import type { SessionHandle } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentLoop, { CONFIGURED_AGENT_IDENTITIES_KEY } from '@deepseek-ai/dsh-agent-loop'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -8,7 +8,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
@@ -13,7 +13,7 @@ import ToolRuntime, { defineContentToolFixture, TOOL_RUNTIME_SCHEDULER } from '@
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import type { SessionHandle } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { SessionId } from '@deepseek-ai/dsh-session'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
@@ -12,7 +12,7 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -9,7 +9,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentService, { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -8,7 +8,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionLogOffset, SessionId, type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentService from '@deepseek-ai/dsh-subagent'
import { deliverSubagentPrompt, type HostPromptDeliverer } from '@deepseek-ai/dsh-subagent/internal'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
@@ -14,7 +14,7 @@ import { PtcRuntime } from '@deepseek-ai/dsh-ptc-runtime'
import Llm, { LlmAdapter, ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import Sessions, { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import Agents from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -8,7 +8,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import WorkingDirectory from '@deepseek-ai/dsh-working-directory'
import ClaudeCodeMods, { defineMod } from '../src/index.ts'
@@ -9,7 +9,7 @@ import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deep
import SessionController from '@deepseek-ai/dsh-api-session-controller'
import Translator from '@deepseek-ai/dsh-experimental-translator'
import { SESSION_FORMAT_VERSION, SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
import JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionQuery from '@deepseek-ai/dsh-session-query'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
@@ -7,7 +7,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -7,7 +7,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -9,7 +9,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm'
import { scopeOf } from '@deepseek-ai/dsh-scope'
import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionQueryEngine from '@deepseek-ai/dsh-session-query'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
@@ -10,7 +10,7 @@ import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import PtcWorkflowEngine from '@deepseek-ai/dsh-workflow-ptc'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -13,7 +13,7 @@ import SessionStore, {
} from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SqliteSessionQueryEngine from '@deepseek-ai/dsh-session-query-sqlite'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
@@ -1,7 +1,8 @@
/** Common paid requests and results survive Host restart without altering main conversation input. */
import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import { pluginRecordOf, SessionId, SessionSeq, type SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
import type { JsonlHandleStorage } from '../../../session/session-persistence-jsonl/src/storage.ts'
import { readFile } from 'node:fs/promises'
import { setImmediate } from 'node:timers/promises'
import { afterEach, describe, expect, it, vi } from 'vitest'
@@ -39,8 +40,7 @@ function historicalEvents(): SessionEvent[] {
describe('common paid translation persistence', () => {
it.each(['caller cancellation', 'provider deadline'] as const)(
'keeps a pending result ahead of an immediate retry after %s without billing again', async (interruption) => {
const b = await paidFixture(cleanups, { translator: { deepseekTimeoutMs: 60_000 } }), backend = b.ctx.sessionPersistence
if (!(backend instanceof JsonlPersistence)) throw new Error('Expected the real JSONL backend')
const b = await paidFixture(cleanups, { translator: { deepseekTimeoutMs: 60_000 } }), backend = b.ctx.sessionPersistence as SessionPersistence & Pick<JsonlHandleStorage, 'persistBatch'>
const entered = Promise.withResolvers<undefined>(), release = Promise.withResolvers<undefined>()
cleanups.push(async () => { release.resolve(undefined) })
const persist = backend.persistBatch.bind(backend)
@@ -3,7 +3,7 @@ import type { ModuleLoaderV2 } from '@deepseek-ai/cordis-plugin-loader'
import { boot } from '@deepseek-ai/dsh-app-boot'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as Official from '@deepseek-ai/dsh-llm-deepseek-api-key'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
@@ -3,7 +3,7 @@ import type { ModuleLoaderV2 } from '@deepseek-ai/cordis-plugin-loader'
import { boot } from '@deepseek-ai/dsh-app-boot'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type Session, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SqliteSessionQuery from '@deepseek-ai/dsh-session-query-sqlite'
import * as Official from '@deepseek-ai/dsh-llm-deepseek-api-key'
import * as Account from '@deepseek-ai/dsh-llm-deepseek-account'
@@ -5,7 +5,7 @@ import { boot } from '@deepseek-ai/dsh-app-boot'
import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type Session, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import type { SessionHandle } from '@deepseek-ai/dsh-session-persistence'
import JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { once } from 'node:events'
import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
@@ -7,7 +7,7 @@ import { Context } from '@deepseek-ai/cordis'
import Include from '@deepseek-ai/cordis-plugin-include'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
import MessageFeedbackService from '../src/index.ts'
import { appendMessageFixture } from './helpers.ts'
@@ -4,7 +4,7 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { RetryId } from '@deepseek-ai/dsh-llm-retry'
import type {} from '../src/index.ts'
@@ -9,7 +9,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import Storage from '@deepseek-ai/dsh-storage'
import * as StorageDomain from '@deepseek-ai/dsh-storage-domain'
import * as StorageJson from '@deepseek-ai/dsh-storage-json'
@@ -29,7 +29,7 @@ const [
{ default: AgentLoop },
{ mountAgentLoopTestDependencies },
{ default: SubagentRuntime },
{ default: JsonlSessionPersistence },
JsonlSessionPersistence,
{ HarnessSdkJsonRpcServer },
{ SessionId },
] = await Promise.all([
@@ -13,7 +13,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as jsonrpc from '../src/index.ts'
/**
+1 -1
View File
@@ -17,7 +17,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
import SubagentRuntime, { SubagentRunId, type SubagentStartRequest, type SubagentRun, type SubagentResult, type SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent'
import type { JsonRpcTransportPeer } from '@deepseek-ai/dsh-sdk-protocol'
+1 -1
View File
@@ -9,7 +9,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentRuntime, { NO_START_CAPABILITIES } from '@deepseek-ai/dsh-subagent'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { HarnessSdkJsonRpcServer } from '../src/server.ts'
@@ -10,7 +10,7 @@ import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import SessionStore, { SessionSeq, SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SqliteSessionQueryEngine, * as queryModule from '@deepseek-ai/dsh-session-query-sqlite'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
@@ -22,7 +22,7 @@ import type {
SessionPersistenceListOptions,
SessionPersistenceSnapshot,
} from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SqliteSessionQueryEngine, {
SESSION_QUERY_SQLITE_SCHEMA_VERSION,
} from '@deepseek-ai/dsh-session-query-sqlite'
@@ -9,7 +9,7 @@ import SessionStore, {
SessionId, TOOL_OUTCOME_UNKNOWN, interruptedTurnClosers,
type SessionEvent,
} from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url))
const childScript = fileURLToPath(new URL('./fixtures/crash-child.ts', import.meta.url))
@@ -4,7 +4,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { createUserMessage, ToolCallId, type GenerateOptions, LlmAdapter, type StreamChunk } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as checkpointPolicy from '../../src/index.ts'
function waitForCrash(): Promise<never> {
@@ -7,7 +7,7 @@ import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import MessageFeedback from '@deepseek-ai/dsh-message-feedback'
import { recordFeedback } from '@deepseek-ai/dsh-command-feedback'
import LlmRuntime, { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
@@ -27,8 +27,8 @@
en: e20cf32540d7c785
zh: 67e182529e9211ca
/deepseek-ai-dsh-session-persistence-jsonl/use-this-package/durability-and-crash-semantics:
en: 1eaa87736cac029b
zh: 26224e8889786d69
en: efd0891dd5f7e16a
zh: 6813052e2f368429
/deepseek-ai-dsh-session-persistence-jsonl/use-this-package/reading-the-logs:
en: 3e53fc90a486d373
zh: 00e6c6e65c56629e
@@ -63,8 +63,8 @@
en: 53ce4da7ce68d21c
zh: 10f40bc354100f77
/deepseek-ai-dsh-session-persistence-jsonl/known-limitations-and-deferred-work:
en: e6343e8ad3f6fb71
zh: fe632c9711e7921b
en: c46c91a811abc547
zh: 41fb942d4e61e544
/deepseek-ai-dsh-session-persistence-jsonl/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: 3bb0c20c93f16f98
@@ -77,6 +77,8 @@ A session is materialized lazily: `create(header)` writes nothing and returns th
The current-generation scanner applies the current codec owner’s structural admission checks before recoverable-tail handling. Retired required PTC tags and `request/header.header.system` refuse the file even after an earlier malformed row; recovery never truncates them as ordinary damaged tail data.
Unloading the backend waits for the consumer unloads it triggers through `sessionPersistence` injection before removing live-event listeners and draining and closing remaining handles. Consumers must register cleanup in that injecting scope; unrelated scopes and explicit Session or handle closure have their own lifetimes. Programmatic mounting uses the package namespace (`import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'`) with `ctx.plugin`; access storage through `ctx.sessionPersistence`.
### Reading the logs
Materialized `stat` and `list` snapshots include the format catalog's header classification as `formatStatus`, alongside the translated logical `header`. A read-only preparation leaves the selected historical version unchanged; the status becomes `current` after publication of the successor. Unmaterialized pending Sessions omit `formatStatus`.
@@ -163,6 +165,7 @@ These limits define when this backend is a poor fit or needs special operational
- **Compressed files are not directly line-readable** — use the backend to load them, or select `compression: 'none'` before writing a fresh root when external line readers are required.
- **Nothing deletes session files** — logs accumulate under `root` until removed externally; the seam has no deletion API.
- **One live writer per session** — the write-handle claim excludes a second writer inside the owning backend instance, and a kernel lock (non-blocking `flock(2)` on `session.lock`; on Windows a named kernel semaphore derived from that path, with no filesystem footprint) excludes every other instance and process; the lock is taken at write-open of an existing artifact and, for a created session, only right before its first materializing write, so an unmaterialized session leaves no filesystem footprint. A crashed holder's lock dies with its process, so its session is writable again immediately, while a live-but-wedged holder blocks writers until its process exits (on POSIX, removing the lock file forfeits that exclusion; release itself never removes it). Advisory `flock` is unreliable on some network filesystems (NFSv3), and the Windows semaphore name is per login session.
- **Concurrent root disposal is not ordered** — Cordis can remove already-disposing consumers from its registry before the provider can join their cleanup. Dependency ordering here covers consumer unloads triggered by removing this provider.
- **POSIX materialization requires hard-link support** — first append uses `link()` so same-id races fail instead of overwriting a committed log; Windows uses write-through rename without replacement.
- **POSIX writes require the matching prebuilt system addon** — [`node-addon-system`](../../../native/system/README.md) supplies asynchronous flock without consumer-side compilation. A missing addon rejects write ownership; Windows retains its semaphore implementation.
@@ -77,6 +77,8 @@ kind: "package-reference"
当前代际扫描器在处理可恢复尾部之前,执行当前编解码器所有者的结构准入检查。已退役的必需 PTC 标签与 `request/header.header.system` 即使出现在较早的畸形行之后也会导致文件被拒绝;恢复绝不将它们作为普通损坏尾部数据截断。
卸载后端时,会先等待经由 `sessionPersistence` 注入所触发的消费者卸载,再移除实时事件监听器、排空并关闭剩余句柄。消费者必须在该注入作用域中注册清理;无依赖关系的作用域,以及显式关闭的 Session 或句柄,各自遵循其生命周期。通过代码挂载时,使用包的 namespace(`import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'`)调用 `ctx.plugin`,并通过 `ctx.sessionPersistence` 访问存储。
### 读取日志
已实体化的 `stat` 与 `list` 快照在转换后的逻辑 `header` 之外,通过 `formatStatus` 保留格式 catalog 的头部分类。只读准备不会改变所选历史版本;发布后继之后,状态才变为 `current`。尚未实体化的待定 Session 省略 `formatStatus`。
@@ -163,6 +165,7 @@ JSONL 存储不修改实时请求前缀。只有重建历史、当前 envelope
- **压缩文件不能直接按行读取**——使用后端加载;或在写入新根前选择 `compression: 'none'`,供外部行读取方使用。
- **不删除会话文件**——日志在 `root` 下累积,直到外部移除;seam 无删除接口。
- **每会话一个活动写入方**——写句柄认领在所属后端实例内排除第二个写入方,内核锁(`session.lock` 上的非阻塞 `flock(2)`;Windows 上为由该路径派生的命名内核信号量,零文件系统足迹)排除其他所有实例与进程;锁在以写模式打开既有产物时立即获取,新建会话则仅在首次实体化写入之前获取,因此未实体化的会话不留任何文件系统足迹。崩溃持有者的锁随其进程消亡,会话立即可再写入,而活着但卡死的持有者会阻塞写入方直到其进程退出(POSIX 上删除锁文件即放弃该排他;释放本身从不删除它)。咨询式 `flock` 在部分网络文件系统(NFSv3)上不可靠,Windows 信号量名按登录会话隔离。
- **根作用域的并发卸载不保证顺序**——Cordis 可能在提供者等待消费者清理前,已将正在卸载的消费者移出注册表。此处的依赖顺序仅覆盖移除该提供者所触发的消费者卸载。
- **POSIX 实体化需要硬链接支持**——第一次 append 使用 `link()`,使同 id 竞态失败而不覆盖已提交日志;Windows 使用无替换 write-through rename。
- **POSIX 写入需要匹配的预编译系统 addon**——[`node-addon-system`](../../../native/system/README.zh.md) 提供异步 flock,无须在用户侧编译。addon 缺失时拒绝写入所有权;Windows 保留其信号量实现。
@@ -6,7 +6,7 @@
* @module @deepseek-ai/dsh-session-persistence-jsonl
*/
import { Context } from '@deepseek-ai/cordis'
import { Context, type Fiber } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import {
createSessionFormatCatalogWithChildren,
@@ -243,24 +243,18 @@ function waitWithAbort<T>(operation: Promise<T>, signal?: AbortSignal): Promise<
}
/**
* The JSONL persistence backend. Load as a plugin; it registers as
* `ctx.sessionPersistence`. Sessions materialize lazily: a created session is
* The JSONL backend registered as `ctx.sessionPersistence` by the plugin.
* Sessions materialize lazily: a created session is
* visible to this process immediately, reaches disk on its first append or
* flush, and never existed if the process crashes before that.
*/
class JsonlSessionPersistence extends SessionPersistence {
static Config: z<Config> = z.object({
root: z.string().required(),
compression: JsonlCompressionSchema,
})
/** Backend label for diagnostics and effects; shadows `Service.name` without changing the service key. */
override readonly name = 'session-persistence-jsonl'
private root: string
private compression: JsonlCompression
private rootEncodingCheck: Promise<void> | undefined
private readonly tracker = new JsonlBackendTracker(this.name)
private readonly generationFormat: Omit<JsonlGenerationFormatAdapter, 'createRestore'>
/**
* Bounded LRU of parsed, validated stored logs keyed by session id and
@@ -273,7 +267,7 @@ class JsonlSessionPersistence extends SessionPersistence {
/** One joinable decode/migration operation per selected historical Session file revision. */
private readonly migrationPreparations = new Map<SessionId, MigrationPreparation>()
constructor(ctx: Context, public config: Config) {
constructor(ctx: Context, public config: Config, private readonly tracker: JsonlBackendTracker) {
super(ctx)
/* v8 ignore next 5 -- generated catalog and Session source share one build-time version owner. */
if (sessionFormatCatalog.currentVersion !== SESSION_FORMAT_VERSION) {
@@ -294,7 +288,6 @@ class JsonlSessionPersistence extends SessionPersistence {
error instanceof SessionFormatUnsupportedMigrationError,
}
this.assertUsableRoot()
this.tracker.install(ctx)
}
/**
@@ -1726,10 +1719,36 @@ class JsonlSessionPersistence extends SessionPersistence {
/* v8 ignore stop */
}
/**
* One open channel onto a JSONL-stored session: the shared storage-handle
* scaffolding over this backend's file primitives. Reads re-scan the artifact
* under the stable-read loop.
*/
/** JSONL persistence plugin label. */
export const name = 'session-persistence-jsonl'
export default JsonlSessionPersistence
/** Loader schema for the backend's root and physical encoding. */
export const Config: z<Config> = z.object({
root: z.string().required(),
compression: JsonlCompressionSchema,
})
/**
* Mount persistence with live routing and handles retained through the
* consumer unloads triggered by removing this provider. Consumers must own
* cleanup in their injecting scope; concurrent root disposal is not ordered.
* @param ctx - the owning plugin context.
* @param config - the storage root and physical encoding.
* @returns resolves when the service is ready.
*/
export async function apply(ctx: Context, config: Config): Promise<void> {
let service!: Fiber
// An async generator awaits these thenable disposers, changing their registered identity.
ctx.effect(function* () {
const tracker = new JsonlBackendTracker(name)
yield tracker.install(ctx)
service = ctx.plugin({
name: 'session-persistence-jsonl-service',
apply(serviceCtx: Context) {
new JsonlSessionPersistence(serviceCtx, config, tracker)
},
})
yield service.dispose
})
await service.await()
}
@@ -528,40 +528,44 @@ export class JsonlBackendTracker {
* sessions' events by id; the teardown effect closes every open handle —
* close drains the routed buffer — and aggregates failures. This provider
* owns no separate storage connection, so closing handles is the complete
* teardown. Registrations are effects of the current fiber.
* @param ctx - the backend's context.
* teardown. The returned effect removes listeners before closing handles.
* @param ctx - the backend's resource-owning context.
* @returns the disposer to run after persistence consumers finish unloading.
*/
install(ctx: Context): void {
ctx.on('session/event', (session: Session, event) => {
this.writers.get(session.id)?.enqueueLive(event, (error) => {
ctx.logger.warn(`session-persistence: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`)
})
})
ctx.on('session/flush', (session: Session) => {
const writer = this.writers.get(session.id)
if (writer === null || writer === undefined) return undefined
return (async () => {
await writer.drainLive()
await writer.flush()
})()
})
ctx.on('session/disposed', (session: Session) => {
const writer = this.writers.get(session.id)
if (writer === null || writer === undefined) return
writer.close().catch((error: unknown) => {
ctx.logger.warn(`session-persistence: final drain for session "${session.id}" failed: ${String(error)}`)
})
})
ctx.effect(() => async () => {
const errors: unknown[] = []
for (const handle of [...this.openHandles]) {
try {
await handle.close()
} catch (error: unknown) {
errors.push(error)
install(ctx: Context): () => Promise<void> {
const { writers, openHandles, name } = this
return ctx.effect(function* () {
yield async () => {
const errors: unknown[] = []
for (const handle of [...openHandles]) {
try {
await handle.close()
} catch (error: unknown) {
errors.push(error)
}
}
if (errors.length > 0) throw new AggregateError(errors, `${name} dispose failed`)
}
if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`)
}, `${this.name} open handles`)
yield ctx.on('session/event', (session: Session, event) => {
writers.get(session.id)?.enqueueLive(event, (error) => {
ctx.logger.warn(`session-persistence: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`)
})
})
yield ctx.on('session/flush', (session: Session) => {
const writer = writers.get(session.id)
if (writer === null || writer === undefined) return undefined
return (async () => {
await writer.drainLive()
await writer.flush()
})()
})
yield ctx.on('session/disposed', (session: Session) => {
const writer = writers.get(session.id)
if (writer === null || writer === undefined) return
writer.close().catch((error: unknown) => {
ctx.logger.warn(`session-persistence: final drain for session "${session.id}" failed: ${String(error)}`)
})
})
}, `${this.name} live routing and open handles`)
}
}
@@ -18,7 +18,7 @@ describe.skipIf(!built)('built migration verifier (plain node)', () => {
import { Worker } from 'node:worker_threads'
import { Context } from '@deepseek-ai/cordis'
import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
const terminate = Worker.prototype.terminate
let terminations = 0
@@ -2,7 +2,7 @@
import { Context } from '@deepseek-ai/cordis'
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { spawnSync } from 'node:child_process'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
@@ -1,7 +1,7 @@
import { Context } from '@deepseek-ai/cordis'
import { SessionId, type SessionHeader } from '@deepseek-ai/dsh-session'
import { SessionFormatUnsupportedError, SessionPersistenceCorruptionError } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
@@ -4,7 +4,7 @@ import { Context } from '@deepseek-ai/cordis'
import { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionFormatJsonObject } from '@deepseek-ai/dsh-session-format'
import { SessionFormatUnsupportedError } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
@@ -1,6 +1,6 @@
import { Context } from '@deepseek-ai/cordis'
import { SESSION_FORMAT_VERSION, SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
@@ -7,7 +7,7 @@
import { Context } from '@deepseek-ai/cordis'
import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
const [root, sessionId] = process.argv.slice(2)
const ctx = new Context()
@@ -8,7 +8,7 @@ import { scheduler } from 'node:timers/promises'
import { SESSION_FORMAT_VERSION, SessionLogOffset, SessionSeq, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import {
assertNoRetiredHeaderFields, encodeSegment, eventLines, generationLogFilename, generationLogPath,
logPath, parseGenerationLogFilename, projectDir, projectKey, scanLog, sessionDir, SessionLogScanner,
@@ -18,7 +18,7 @@ import {
runPersistenceContract, meta, oneTurnLog, releasedV1OneTurnLog,
} from '../../session-persistence/tests/contract.ts'
import { runLiveWritePathContract } from '../../session-persistence/tests/live-write-contract.ts'
import { LIVE_WRITE_BATCH_MAX_DELAY_MS, type JsonlSessionHandle } from '../src/storage.ts'
import { LIVE_WRITE_BATCH_MAX_DELAY_MS, type JsonlSessionHandle, type JsonlHandleStorage } from '../src/storage.ts'
import { JsonlGenerationSourceChangedError } from '../src/generation.ts'
import SessionStore from '@deepseek-ai/dsh-session'
@@ -795,7 +795,7 @@ describe('JsonlSessionPersistence: immutable format generations', () => {
})
it('resolves absent, current, and historical current-generation paths', async () => {
const persistence = ctx.sessionPersistence as JsonlSessionPersistence
const persistence = ctx.sessionPersistence as SessionPersistence & JsonlHandleStorage
expect(await persistence.resolveCurrentLog(SessionId('missing-generation'))).toBeUndefined()
const current = meta('resolved-current', '/work')
@@ -1059,7 +1059,7 @@ describe('JsonlSessionPersistence: immutable format generations', () => {
const currentPath = rawLogPath(root, header.cwd, header.id)
await mkdir(dirname(sourcePath), { recursive: true })
await writeFile(sourcePath, `${JSON.stringify(releasedV0Header(header))}\n`)
const persistence = ctx.sessionPersistence as JsonlSessionPersistence
const persistence = ctx.sessionPersistence as SessionPersistence & JsonlHandleStorage
await expect(persistence.resolveCurrentLog(header.id, new AbortController().signal)).resolves.toBeUndefined()
expect(await readFile(sourcePath, 'utf8')).toBe(`${JSON.stringify(releasedV0Header(header))}\n`)
@@ -1885,7 +1885,7 @@ describe('JsonlSessionPersistence: durability and crash semantics', () => {
it('stat handles artifact removal and non-ENOENT failures after log discovery', async () => {
const m = meta('stored-revision-race')
await writeLog(ctx.sessionPersistence, m, oneTurnLog())
const persistence = ctx.sessionPersistence as JsonlSessionPersistence
const persistence = ctx.sessionPersistence
const internals = persistence as unknown as {
findLog(id: SessionId, signal?: AbortSignal): Promise<{
sourcePath: string
@@ -25,7 +25,7 @@ import {
SessionPersistenceNotFoundError,
} from '@deepseek-ai/dsh-session-persistence'
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '../src/index.ts'
import * as JsonlSessionPersistence from '../src/index.ts'
import { LEASE_FILENAME, SessionWriteLease } from '../src/lease.ts'
import type { JsonlSessionHandle } from '../src/storage.ts'
import { sessionDir } from '../src/format.ts'
@@ -16,7 +16,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
import { SessionAlreadyOwnedError } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
const SESSION = 'two-process-lease'
@@ -6,7 +6,7 @@ import { createSessionFormatCatalogWithChildren } from '@deepseek-ai/dsh-session
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { SessionFormatJsonObject } from '@deepseek-ai/dsh-session-format'
import { SessionFormatUnsupportedError, SessionPersistenceCorruptionError } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
@@ -10,7 +10,7 @@ import {
restoreReleasedV2Artifact, sessionFormatV1ToV2,
} from '@deepseek-ai/dsh-session-format-v1-to-v2'
import { SessionFormatUnsupportedError } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { appendFile, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
@@ -2,7 +2,7 @@
import { Context } from '@deepseek-ai/cordis'
import { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import { SessionPersistenceCorruptionError } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
@@ -1,7 +1,7 @@
import { Context } from '@deepseek-ai/cordis'
import { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import { SessionPersistenceCorruptionError } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
@@ -2,7 +2,7 @@
import { Context } from '@deepseek-ai/cordis'
import { SessionId } from '@deepseek-ai/dsh-session'
import { SessionFormatUnsupportedError, SessionPersistenceCorruptionError } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
@@ -0,0 +1,139 @@
/** JSONL teardown preserves events emitted by an asynchronously disposing injected consumer. */
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import * as SessionPlugin from '@deepseek-ai/dsh-session'
import { SessionId, type Session } from '@deepseek-ai/dsh-session'
import * as JsonlPlugin from '@deepseek-ai/dsh-session-persistence-jsonl'
import { SessionHandleClosedError } from '@deepseek-ai/dsh-session-persistence'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { scheduler } from 'node:timers/promises'
import { pathToFileURL } from 'node:url'
import { expect, it } from 'vitest'
import { logPath } from '../src/format.ts'
it('waits for injected consumer teardown, persists its final event, and releases listeners and leases', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-jsonl-teardown-'))
const ctx = new Context()
const entered = Promise.withResolvers<undefined>()
const release = Promise.withResolvers<undefined>()
const consumerFinished = Promise.withResolvers<undefined>()
let session: Session | undefined
let unload: Promise<void> | undefined
let unloadFinished = false
const consumer = {
name: 'async-persistence-consumer',
inject: ['sessionPersistence', 'sessions'],
async apply(consumerCtx: Context) {
// Keep the Session alive while its persistence consumer unloads.
session = ctx.sessions.create(SessionId('async-teardown'))
await consumerCtx.sessionPersistence.create(session.header)
const live = session
live.append('turn/start', { turn: 1 })
await consumerCtx.sessions.flush(live)
consumerCtx.effect(() => async () => {
entered.resolve(undefined)
try {
await release.promise
live.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
} finally {
consumerFinished.resolve(undefined)
}
})
},
}
try {
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
'- id: sessions',
" name: '@deepseek-ai/dsh-session'",
'- id: persistence',
" name: '@deepseek-ai/dsh-session-persistence-jsonl'",
' config:',
` root: ${JSON.stringify(root)}`,
' compression: none',
'- id: consumer',
' name: async-persistence-consumer',
'',
].join('\n'))
ctx.baseUrl = pathToFileURL(root).href + '/'
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
ctx.loader.internal = {
version: 'v2',
loadCache: new Map(),
async import(specifier: string) {
switch (specifier) {
case '@deepseek-ai/dsh-session': return SessionPlugin
case '@deepseek-ai/dsh-session-persistence-jsonl': return JsonlPlugin
case 'async-persistence-consumer': return consumer
default: throw new Error(`Unexpected plugin: ${specifier}`)
}
},
register(): never { throw new Error('Unexpected module hook registration') },
getOrCreateModuleJob(): never { throw new Error('Unexpected module job creation') },
resolveSync(): never { throw new Error('Unexpected synchronous module resolution') },
load(): never { throw new Error('Unexpected module load') },
}
await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
await ctx.loader.await()
const entries = [...ctx.loader.entries()]
const provider = entries.find(entry => entry.options.id === 'persistence')!
const consumingEntry = entries.find(entry => entry.options.id === 'consumer')!
await provider.fiber!.await()
await consumingEntry.fiber!.await()
expect(session).toBeDefined()
const live = session!
const oldReader = await ctx.sessionPersistence.open(live.id, 'read')
const path = logPath(root, live.header.cwd, live.id, 'none')
const before = await readFile(path, 'utf8')
expect(before).toContain('turn/start')
expect(before).not.toContain('turn/end')
const routedEvents = ['session/event', 'session/flush', 'session/disposed'] as const
for (const event of routedEvents) expect(ctx.events.dispatch('emit', [event])).toHaveLength(1)
unload = provider.fiber!.dispose().then(() => { unloadFinished = true })
await Promise.race([
entered.promise,
unload.then(() => { throw new Error('Provider unloaded before its injected consumer entered teardown') }),
])
await scheduler.yield()
expect(unloadFinished).toBe(false)
for (const event of routedEvents) expect(ctx.events.dispatch('emit', [event])).toHaveLength(1)
release.resolve(undefined)
await consumerFinished.promise
await unload
expect(ctx.get('sessionPersistence')).toBeUndefined()
for (const event of routedEvents) expect(ctx.events.dispatch('emit', [event])).toHaveLength(0)
await expect(oldReader.read()).rejects.toBeInstanceOf(SessionHandleClosedError)
const persisted = (await readFile(path, 'utf8')).trimEnd().split('\n').map((line): unknown => JSON.parse(line))
expect(persisted.slice(1)).toEqual(live.snapshotEvents())
expect(persisted.at(-1)).toMatchObject({ type: 'turn/end', seq: 1, data: { turn: 1, reason: { kind: 'completed' } } })
await consumingEntry.fiber!.dispose()
await provider.init()
await provider.fiber!.await()
const reopened = await ctx.sessionPersistence.open(live.id, 'write')
try {
expect((await reopened.read()).events).toEqual(live.snapshotEvents())
await reopened.flush()
} finally {
await reopened.close()
}
for (const event of routedEvents) expect(ctx.events.dispatch('emit', [event])).toHaveLength(1)
} finally {
release.resolve(undefined)
try {
try {
await unload
} finally {
await ctx.fiber.dispose()
}
} finally {
await rm(root, { recursive: true, force: true })
}
}
})
@@ -3,7 +3,7 @@
import { Context } from '@deepseek-ai/cordis'
import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionFormatEvent } from '@deepseek-ai/dsh-session-format'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
@@ -4,7 +4,7 @@ import { SESSION_FORMAT_VERSION, Session, SessionId, SessionLogOffset, SessionSe
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import { SessionFormatUnsupportedError } from '@deepseek-ai/dsh-session-persistence'
import type { SessionHandle } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
@@ -1,6 +1,6 @@
import { Context } from '@deepseek-ai/cordis'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { afterEach, describe, expect, it } from 'vitest'
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
@@ -8,7 +8,7 @@ import { performance } from 'node:perf_hooks'
import { SessionSeq, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import {
generationLogPath, logPath, scanLog, sessionDir, toHeaderLine, type JsonlCompression,
} from '../src/format.ts'
@@ -467,10 +467,8 @@ describe('JsonlSessionPersistence: default Zstandard encoding', () => {
const root = await freshRoot()
const ctx = new Context()
contexts.push(ctx)
let backend!: JsonlSessionPersistence
await ctx.plugin((inner: Context) => {
backend = new JsonlSessionPersistence(inner, { root })
})
await ctx.plugin((inner: Context) => JsonlSessionPersistence.apply(inner, { root }))
const backend = ctx.sessionPersistence
const header = meta('direct-default')
const path = logPath(root, header.cwd, header.id, 'zstd')
@@ -20,7 +20,7 @@ import { recordFeedback } from '@deepseek-ai/dsh-command-feedback'
import { createAssistantMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, Session, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
import MessageFeedbackService from '@deepseek-ai/dsh-message-feedback'
import JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import OpenTelemetrySessionBackend, { Config, DEFAULT_TELEMETRY_MODE, SessionTelemetryMode } from '../src/index.ts'
interface Capture {
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionTitleService, { foldSessionTitle } from '@deepseek-ai/dsh-session-title'
const CONFIG = {
@@ -5,7 +5,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'nod
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { PtcRuntime, type PtcRunRequest, type PtcRunResult, type PtcRunSpec } from '@deepseek-ai/dsh-ptc-runtime'
@@ -1,6 +1,6 @@
import { mountWorkingDirectoryFixture } from '../../subagent/tests/working-directory-fixture.ts'
import { join } from 'node:path'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -19,7 +19,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SandboxPolicyService, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { queueHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
@@ -10,7 +10,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { DeepSeekAdapter, resolveAdapterOptions } from '@deepseek-ai/dsh-llm-deepseek'
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { end, MODEL, server, sse, start } from '../../../llm/llm-deepseek/tests/helpers.ts'
import SubagentRuntime, { type SubagentRunEndInfo } from '../src/index.ts'
@@ -11,7 +11,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import * as SubagentControl from '@deepseek-ai/dsh-tool-subagent-control'
@@ -9,7 +9,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { generationLogPath } from '../../../session/session-persistence-jsonl/src/format.ts'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
@@ -5,7 +5,7 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { type ContinuableCreateSpec, type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import { onTestFinished } from 'vitest'
@@ -19,7 +19,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import AgentPresets from '@deepseek-ai/dsh-agent-preset-registry'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '../../../session/session-persistence-jsonl/src/index.ts'
import * as JsonlSessionPersistence from '../../../session/session-persistence-jsonl/src/index.ts'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as Spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -9,7 +9,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as Spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as Fork from '@deepseek-ai/dsh-subagent-fork-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -8,7 +8,7 @@ import { ToolCallId } from '@deepseek-ai/dsh-llm'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentCatalogEntry } from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
@@ -9,7 +9,7 @@ import { ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
@@ -11,7 +11,7 @@ import { assembleContextFor } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
@@ -7,7 +7,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'
import SessionProjections from '@deepseek-ai/dsh-session-projection'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import FileSystem from '@deepseek-ai/dsh-fs-local'
import Subprocess from '@deepseek-ai/dsh-subprocess-local'
import Sandbox from '@deepseek-ai/dsh-sandbox-local'
+12 -3
View File
@@ -334,11 +334,20 @@ describe('one-time V4 migration command', () => {
const backendUrl = pathToFileURL(join(repository, 'packages/session/session-persistence-jsonl/src/index.ts')).href
const generationUrl = pathToFileURL(join(repository, 'packages/session/session-persistence-jsonl/src/generation.ts')).href
await writeFile(entrypoint, `
const { default: Backend } = await import(${JSON.stringify(backendUrl)});
const Backend = await import(${JSON.stringify(backendUrl)});
const { Context } = await import(${JSON.stringify(pathToFileURL(join(repository, 'vendor/cordis/src/index.ts')).href)});
const { JsonlGenerationSourceChangedError } = await import(${JSON.stringify(generationUrl)});
const open = Backend.prototype.open;
const ctx = new Context();
let prototype;
try {
await ctx.plugin(Backend, { root: ${JSON.stringify(temporaryRoot())} });
prototype = Object.getPrototypeOf(ctx.sessionPersistence);
} finally {
await ctx.fiber.dispose();
}
const open = prototype.open;
let changed = false;
Backend.prototype.open = async function(id, access, options) {
prototype.open = async function(id, access, options) {
if (id === 'a-parent' && access === 'write' && !changed) {
changed = true;
throw new JsonlGenerationSourceChangedError(${JSON.stringify(child.path)});
+1 -1
View File
@@ -10,7 +10,7 @@ import { inspect, parseArgs } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
import { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import { sessionFormatCatalog } from '@deepseek-ai/dsh-session-format-catalog'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { encodeSegment, generationLogFilename, parseGenerationLogFilename, type JsonlCompression } from '../packages/session/session-persistence-jsonl/src/format.ts'
import { JsonlGenerationSourceChangedError } from '../packages/session/session-persistence-jsonl/src/generation.ts'
import { classifyMigrationFailure, type MigrationFailureDiagnostic } from './migration-failure-summary.ts'
@@ -0,0 +1,56 @@
/** Persist an injected consumer's final event during provider-only unload. */
import type { Context } from '@deepseek-ai/cordis'
import type { Session } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/cordis-plugin-loader'
import type {} from '@deepseek-ai/dsh-tool-todo'
import { scheduler } from 'node:timers/promises'
export const name = 'snapshot-persistence-provider-unload'
export const inject = ['loader', 'sessions']
/**
* Hold the completed turn's flush until persistence and its consumer unload.
* @param ctx - Shipped profile context with the Loader-owned persistence entry.
*/
export async function apply(ctx: Context): Promise<void> {
const entered = Promise.withResolvers<void>()
const release = Promise.withResolvers<void>()
let live: Session | undefined
let triggered = false
await ctx.plugin({
name: 'snapshot-persistence-consumer',
inject: ['sessionPersistence'],
apply(consumer: Context) {
consumer.effect(() => async () => {
if (live === undefined) return
entered.resolve()
await release.promise
live.append('todo/write', {
todos: [{ content: 'Persistence consumer cleanup completed', status: 'completed' }],
})
})
},
})
ctx.on('session/flush', async (session) => {
if (triggered || !session.snapshotEvents().some(event => event.type === 'turn/end')) return
const provider = [...ctx.loader.entries()].find(entry => entry.options.id === 'session-persistence-jsonl')
if (provider?.fiber === undefined) throw new Error('Snapshot persistence provider is missing')
triggered = true
// Finish the checkpoint before removing its writer; the nested callback is skipped.
await ctx.sessions.flush(session)
live = session
let finished = false
const unloading = provider.fiber.dispose().then(() => { finished = true })
try {
await Promise.race([
entered.promise,
unloading.then(() => { throw new Error('Persistence unloaded before its consumer cleanup') }),
])
await scheduler.yield()
if (finished) throw new Error('Persistence did not await its consumer cleanup')
} finally {
release.resolve()
await unloading
}
})
}
@@ -0,0 +1,3 @@
- insert:
- id: persistence-provider-unload
name: './provider-unload.ts'
@@ -0,0 +1,18 @@
{"type":"session","version":4,"id":"{{session:1}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
{"type":"approval/policy","data":{"policy":"never"}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with DONE after the persistence lifecycle check."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
{"type":"turn/start","data":{"turn":1}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with DONE after the persistence lifecycle check."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Reply with DONE after the","messageSeqs":[8],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":12,"outputTokens":3},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":3}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"todo/write","data":{"todos":[{"content":"Persistence consumer cleanup completed","status":"completed"}]}}
@@ -0,0 +1,7 @@
version: 1
scenario: persistence-provider-unload
profile: headless
composition: default
recording: authored
header:
class: default