Merge origin/master into worktree/remove-sdk-project-toolchain

# Conflicts:
#	THIRD_PARTY_NOTICES.md
This commit is contained in:
Tianyi Cui
2026-08-11 15:08:18 +08:00
181 changed files with 3919 additions and 1053 deletions

View File

@@ -18,7 +18,8 @@ import {
DEFAULT_PREPARED_SESSION_CACHE_SIZE, DEFAULT_WRITE_BATCH_MAX_DELAY_MS, MAX_WRITE_BATCH_DELAY_MS,
SessionPersistence, SessionPersistenceRevision, PersistenceCoordinator, SessionFormatUnsupportedError,
type PersistenceBackend, type SessionLocation, type SessionPersistenceSnapshot,
type SessionInspection, type SessionPersistenceRevision as PersistenceRevision, type StoredPrefix,
type SessionInspection, type SessionPersistenceRevision as PersistenceRevision, type SessionRawArtifact,
type StoredPrefix,
} from '@deepseek-ai/dsh-session-persistence'
import type { SessionEvent, SessionId, SessionHeader, SessionPreparation } from '@deepseek-ai/dsh-session'
import {
@@ -233,6 +234,73 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
}
}
/**
* Read a session's stored artifact text verbatim: the durable file bytes
* decoded from this backend's physical encoding (complete zstd frames
* concatenated, or UTF-8 plaintext). The content is the exact JSONL text the
* backend wrote — never a reconstruction from parsed events — so packed-
* chunk rows, key order, and line breaks survive byte-for-byte. A torn
* final frame is omitted, matching the committed-prefix semantics of every
* other read.
* @param id - the persisted session to read.
* @param signal - optional cancellation for the stat/read/decode work.
* @returns the raw artifact text plus the header parsed from its own first
* line, or `undefined` when the session has no stored artifact.
*/
override async readRaw(id: SessionId, signal?: AbortSignal): Promise<SessionRawArtifact | undefined> {
signal?.throwIfAborted()
await this.ensureRootEncoding()
signal?.throwIfAborted()
const path = await this.findLog(id, signal)
if (path === undefined) return undefined
const { buffer } = await this.readStableFile(path, signal)
let content: string
if (this.compression === 'zstd') {
const { frames } = scanZstdFrames(buffer)
if (frames.length === 0) return undefined
const decoder = createZstdFrameDecoder()
const plaintexts: Buffer[] = []
// The decoder yields views into a reused buffer; copy each frame's
// plaintext immediately so a later concat cannot read overwritten memory.
for (const plaintext of decoder.decode(buffer, frames)) {
signal?.throwIfAborted()
plaintexts.push(Buffer.from(plaintext))
}
content = Buffer.concat(plaintexts).toString('utf8')
} else {
content = buffer.toString('utf8')
}
const meta = parseHeaderMeta(content.split('\n', 1)[0] as string)
if (meta === undefined || meta.id !== id) {
throw new Error(`corrupt session log: invalid header line in "${path}"`)
}
// The logical artifact name is `session.jsonl` regardless of the physical
// encoding suffix (`.jsonl.zstd` marks compression only).
return { meta, filename: 'session.jsonl', content }
}
/**
* Read a file's bytes under a revision-stable loop: a writer appending
* between stat and readFile would yield a torn physical file, so retry
* while the stat revision changes.
* @param path - the artifact file to read.
* @param signal - optional cancellation for the stat/read work.
* @returns the stable bytes and the revision that matched both stats.
*/
private async readStableFile(
path: string,
signal?: AbortSignal,
): Promise<{ buffer: Buffer; revision: PersistenceRevision }> {
for (;;) {
signal?.throwIfAborted()
const before = fileRevision(await stat(path, { bigint: true }))
const buffer = await readFile(path, { signal })
signal?.throwIfAborted()
const after = fileRevision(await stat(path, { bigint: true }))
if (before === after) return { buffer, revision: after }
}
}
/**
* Read a stored prefix and convert torn-tail state to the opaque marker the
* coordinator can round-trip without knowing the physical encoding.
@@ -242,19 +310,7 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
expectedId?: SessionId,
signal?: AbortSignal,
): Promise<StoredPrefix<JsonlTornMarker>> {
let buffer: Buffer
let revision: PersistenceRevision
for (;;) {
signal?.throwIfAborted()
const before = fileRevision(await stat(path, { bigint: true }))
buffer = await readFile(path, { signal })
signal?.throwIfAborted()
const after = fileRevision(await stat(path, { bigint: true }))
if (before === after) {
revision = after
break
}
}
const { buffer, revision } = await this.readStableFile(path, signal)
let prefix: Omit<StoredPrefix<JsonlTornMarker>, 'revision'>
try {
if (this.compression === 'zstd') {

View File

@@ -287,6 +287,46 @@ describe('SessionPersistenceJsonl: durability and crash semantics', () => {
expect((await ctx.sessionPersistence.list()).map(h => h.id)).toContain(m.id)
})
it('readRaw returns the stored artifact text verbatim with its original filename', async () => {
const m = meta('raw-read', '/work')
await ctx.sessionPersistence.create(m)
await ctx.sessionPersistence.append(m.id, oneTurnLog())
const raw = await ctx.sessionPersistence.readRaw(m.id)
expect(raw).toBeDefined()
expect(raw!.filename).toBe('session.jsonl')
expect(raw!.meta.id).toBe(m.id)
// Byte-identical to the physical file — never a reconstruction.
expect(raw!.content).toBe(await readFile(rawLogPath(root, '/work', m.id), 'utf8'))
expect(raw!.content.split('\n')[0]).toBe(JSON.stringify(toHeaderLine(m)))
const scanned = scanLog(Buffer.from(raw!.content))
expect(scanned.events.map(event => event.type)).toEqual(oneTurnLog().map(event => event.type))
})
it('readRaw is undefined for an absent session', async () => {
const m = meta('raw-missing', '/work')
expect(await ctx.sessionPersistence.readRaw(m.id)).toBeUndefined()
})
it('readRaw rejects a corrupt header line instead of exporting it', async () => {
const m = meta('raw-corrupt', '/work')
await ctx.sessionPersistence.create(m)
await ctx.sessionPersistence.append(m.id, oneTurnLog())
await writeFile(rawLogPath(root, '/work', m.id), 'not a header line\n{"type":"turn/start","seq":0}\n')
await expect(ctx.sessionPersistence.readRaw(m.id)).rejects.toThrow(/corrupt session log/)
})
it('readRaw retries when the file revision changes during the read', async () => {
const m = meta('raw-revision-race', '/work')
await ctx.sessionPersistence.create(m)
await ctx.sessionPersistence.append(m.id, oneTurnLog())
statRace.path = rawLogPath(root, '/work', m.id)
const raw = await ctx.sessionPersistence.readRaw(m.id)
expect(raw).toBeDefined()
// Two stat calls per iteration; the mocked revision change forces a retry.
expect(statRace.reads).toBe(4)
})
it('keeps the same location on resume and gives a fork its own location', async () => {
const parent = meta('location-parent', '/work')
const parentLocation = ctx.sessionPersistence.locate(parent)

View File

@@ -356,6 +356,39 @@ describe('SessionPersistenceJsonl: default Zstandard encoding', () => {
expect((await ctx.sessionPersistence.load(header.id)).events).toEqual(oneTurnLog())
})
it('readRaw decodes the compressed artifact back to the original JSONL text', async () => {
const root = await freshRoot()
const ctx = await mount(root)
const header = meta('raw-read-zstd', '/work')
await ctx.sessionPersistence.create(header)
await ctx.sessionPersistence.append(header.id, oneTurnLog())
const raw = await ctx.sessionPersistence.readRaw(header.id)
expect(raw).toBeDefined()
// The logical name drops the physical encoding suffix.
expect(raw!.filename).toBe('session.jsonl')
expect(raw!.meta.id).toBe(header.id)
expect(raw!.content).toBe([
JSON.stringify(toHeaderLine(header)),
...oneTurnLog().map(e => JSON.stringify(e)),
'',
].join('\n'))
const scanned = scanLog(Buffer.from(raw!.content))
expect(scanned.events.map(event => event.type)).toEqual(oneTurnLog().map(event => event.type))
})
it('readRaw is undefined for a zstd artifact that carries no frame', async () => {
const root = await freshRoot()
const ctx = await mount(root)
const header = meta('raw-zero-frame', '/work')
await ctx.sessionPersistence.create(header)
await ctx.sessionPersistence.append(header.id, oneTurnLog())
// Overwrite the physical artifact with a short buffer: frame scanning
// answers zero frames before any magic check, so readRaw reports no artifact.
await writeFile(logPath(root, '/work', header.id, 'zstd'), Buffer.alloc(0))
expect(await ctx.sessionPersistence.readRaw(header.id)).toBeUndefined()
})
it('resolves the default when a programmatic wrapper bypasses Loader schema normalization', async () => {
const root = await freshRoot()
const ctx = new Context()

View File

@@ -30,6 +30,16 @@ export interface SessionInspection {
readonly events: readonly SessionEvent[]
}
/** A backend's own raw artifact text for one session, verbatim. */
export interface SessionRawArtifact {
/** The session header parsed from the artifact's own first line. */
readonly meta: SessionHeader
/** The artifact's base filename on disk, without any physical encoding suffix. */
readonly filename: string
/** The artifact's full text content, decoded from the backend's physical encoding. */
readonly content: string
}
// The backend-agnostic write-path orchestration first-party backends compose.
export {
DEFAULT_PREPARED_SESSION_CACHE_SIZE,
@@ -85,6 +95,26 @@ export abstract class SessionPersistence extends Service {
*/
abstract locate(meta: SessionHeader): SessionLocation | undefined
/**
* Read a session's backend-owned artifact text verbatim — the exact durable
* bytes the backend wrote (decoded from its physical encoding, e.g. a
* decompressed JSONL). The returned `content` is the raw text, not a
* reconstruction from parsed events, so it preserves backend-specific
* serialization (chunk packing, key order, line breaks). Backends without a
* per-session artifact (SQLite) inherit the `undefined` default.
* @param _id - the persisted session to read (unused by the default: no
* per-session artifact).
* @param signal - optional cancellation for backend read work.
* @returns the raw artifact plus its parsed header, or `undefined` when the
* session is absent or the backend owns no per-session artifact.
*/
readRaw(_id: SessionId, signal?: AbortSignal): Promise<SessionRawArtifact | undefined> {
if (signal?.aborted === true) {
return Promise.reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'))
}
return Promise.resolve(undefined)
}
/**
* Register a new session's metadata. A backend MAY defer the physical write
* until the first {@link append} (lazy materialization), in which case a

View File

@@ -246,6 +246,24 @@ runPersistenceContract('memory', async () => {
}
})
describe('the inherited readRaw default', () => {
it('answers undefined and honors an aborted signal', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(MemoryPersistence)
expect(await ctx.sessionPersistence.readRaw(SessionId('any-session'))).toBeUndefined()
await expect(
ctx.sessionPersistence.readRaw(SessionId('any-session'), AbortSignal.abort()),
).rejects.toThrow()
// A non-Error abort reason falls back to a wrapped Error rejection.
const controller = new AbortController()
controller.abort('boom')
await expect(
ctx.sessionPersistence.readRaw(SessionId('any-session'), controller.signal),
).rejects.toThrow('aborted')
})
})
// Each fixture shares one map across mounts. No `corruptTail` is supplied because map writes are
// atomic; the suite asserts that skip while JSONL and SQLite cover the repair branch.
runCoordinatorContract('memory', async (): Promise<CoordinatorFixture> => {

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-projection/README.md
README.md: 2615b253999c798172168ec9d0232eb965b07fbc
README.zh.md: 0712e9c7a61fbcf43939791b3e7cd24af6777c3a
README.md: 9018b133bb69ed4717fede14c9a2070a07c3fa62
README.zh.md: b91908117fd452855a82976515d13165803def43

View File

@@ -42,6 +42,7 @@ None; projections never assemble or send provider requests.
## Known Limitations and Deferred Work
- **Every tail page carries every registered key** — there is no per-key opt-out or lazy-key request shape yet; acceptable while values are UI-scale whole states (a todo list, a goal snapshot), revisit if a domain's value grows large.
- **The unit table is process-wide, so key presence is not a per-session capability signal** — a key registered by ANY agent preset appears in every session's snapshot, including sessions whose own composition mounts nothing that produces it. A client must read the VALUE (`plan.active`, an empty todo list) rather than treat an absent key as absence of the feature; a unit whose empty value is indistinguishable from a real one belongs on the host plane instead, which is why `dsh-token-meter` sits there.
- **Eager drive touches every unit per event** — cheap by construction (whole-value rule, same-reference gate), but a hot path would justify per-unit event-type prefilters, addable without contract change.
- **Registry cells live in memory only** — a restart rebuilds by folding the log on first touch; compositions that mount `dsh-session-projection-cache` seed that fold from persisted rows instead.
- **Synchronous unit discipline is only partially mechanical** — the boundary `schema.parse` rejects a Promise-returning `view`, but an `apply` that blocks or reads torn non-session state is a review concern; the invariant companion documents why no runtime check exists.

View File

@@ -42,6 +42,7 @@
## 已知限制与暂缓事项
- **每个尾页携带每个已注册的 key**——尚无逐 key 的 opt-out 或惰性 key 请求形状;在值都是 UI 量级的全量状态(一张 todo 清单、一份 goal 快照)时可以接受,若某领域的值变大再重议。
- **单元表是进程级的,因此 key 是否存在不能当作逐会话的能力信号**——只要**任何**一个 agent preset 注册了某个 key,它就出现在每个会话的快照里,包括自身组装完全不产出该值的会话。客户端必须读**值**(`plan.active`、空的 todo 列表),不能把 key 缺席当作功能缺席;如果某个单元的空值与真实值无法区分,它就该待在宿主平面——`dsh-token-meter` 正因如此留在那里。
- **主动驱动(eager drive)逐事件触达每个单元**——按构造开销很低(全量值规则、同引用闸门),但若出现热点路径,可加按单元的事件类型预过滤,约定不变。
- **注册表 cell 只活在内存里**——重启后首次触达时靠折叠日志重建;挂载了 `dsh-session-projection-cache` 的组合改由持久行播种该折叠。
- **单元同步纪律只有部分可机械把关**——边界 `schema.parse` 能拒绝返回 Promise 的 `view`,但阻塞的 `apply`、或读取撕裂的非会话状态的 `apply`,只能靠评审把关;invariant 配套记载了为何不存在运行时检查。