mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(session): retain JSONL resources through provider-triggered teardown
This commit is contained in:
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'
|
||||
|
||||
@@ -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` 的作用域中,以便后端卸载时等待它完成。
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)});
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user