mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
feat(session): reduce persistence storage size (#3048)
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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 .agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.md
|
||||
2026-08-25-persistence-latency-and-page-size.md: 27eb58cc551f01c48361a3af3224eb8b12592a00
|
||||
2026-08-25-persistence-latency-and-page-size.zh.md: 24ab1835cc313cd617d665a0c52a399d505069ea
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# Agent Note: Persistence compression latency and SQLite page size
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-25-persistence-latency-and-page-size.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The physical persistence optimizations need to reduce retained storage without moving disproportionate work into full writes, reads, or session forks. The original 105-session corpus showed that JSONL level-19 compression made full writes and forks more than twice as slow. The earlier SQLite page-size experiment predated shared-dictionary row compression and showed negligible savings, so it did not establish the best page size for the current row distribution.
|
||||
|
||||
The decision needs evidence from more varied sessions, including long event streams and payloads outside the original corpus. The expanded corpus contains 501 real sessions, 16,153,332 logical events, and 2,002,145,570 bytes of serialized event data.
|
||||
|
||||
## Decision
|
||||
|
||||
### Storage encoding stays physical and independently decodable
|
||||
|
||||
JSONL stores strictly increasing `sourceEventSeqs` as mixed scalar values and inclusive ranges; other orders remain verbatim. SQLite stores the same arrays as tagged zigzag-delta or `(start, count)` varints, choosing the smaller encoding. Both readers restore the original `number[]` before exposing an event.
|
||||
|
||||
SQLite uses an internal integer `sessions.id` and keeps the public session id once in `sessions.session_key`, so event rows and their primary key do not repeat a text identifier. Each `events.data` value remains independently decodable: the writer tries level-3 Zstandard with the packaged 64 KiB raw-content dictionary and retains SQLite text when compression is not smaller. The dictionary bytes are part of schema 19 and a test pins their SHA-256 digest; replacing them requires another schema-version bump.
|
||||
|
||||
### JSONL uses the standard Zstandard level
|
||||
|
||||
The JSONL writer keeps one checksummed Zstandard frame per durable append batch but uses the compressor's standard level. Lossless `sourceEventSeqs` range encoding remains active. Frames stay independently decodable for suffix reads and torn-tail recovery; only the expensive level-19 search is removed.
|
||||
|
||||
### New SQLite databases use 64 KiB pages
|
||||
|
||||
The SQLite provider sets `page_size=65536` before initializing a pristine schema-19 database. An established schema-19 database retains its current page size because SQLite ignores the pragma after allocation.
|
||||
|
||||
The page size is part of schema 19's fixed physical layout and is applied through the package's closed SQL resources like the other fixed SQLite pragmas.
|
||||
|
||||
### Expanded benchmark
|
||||
|
||||
Each candidate was rebuilt five times from the same 501-session corpus with 512-event append batches. Their order rotates between rounds so every candidate occupies each run position once. Each build runs three complete and suffix-read sweeps. For each displayed metric, the highest and lowest build are discarded and the remaining three values are averaged. Complete and suffix read times cover one sweep over all sessions, and fork time covers all 501 sessions.
|
||||
|
||||
| Backend | Stored size | Full write | Full read | Suffix read | Fork |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| JSONL `master` | 172.43 MB | 200.902 s | 8.033 s | 24.479 s | 72.670 s |
|
||||
| JSONL with provenance ranges | 148.15 MB (-14.1%) | 197.281 s (-1.8%) | 7.799 s (-2.9%) | 24.582 s (+0.4%) | 72.308 s (-0.5%) |
|
||||
| JSONL with provenance ranges and level 19 | 130.22 MB (-24.5%) | 329.442 s (+64.0%) | 7.764 s (-3.3%) | 24.454 s (-0.1%) | 166.177 s (+128.7%) |
|
||||
| SQLite `master` (schema 17) | 438.31 MB | 69.632 s | 8.211 s | 0.546 s | 64.290 s |
|
||||
| SQLite with all physical optimizations and 64 KiB pages | 233.18 MB (-46.8%) | 87.656 s (+25.9%) | 9.155 s (+11.5%) | 0.575 s (+5.3%) | 79.417 s (+23.5%) |
|
||||
|
||||
Relative to standard-level frames with provenance ranges, level 19 saves another 12.1% of the JSONL bytes but increases full-write time by 67.0% and fork time by 129.8%. Its complete and suffix reads change by -0.4% and -0.5%. The extra search therefore benefits retained size without improving the latency-sensitive operations enough to offset its repeated encoding cost.
|
||||
|
||||
An otherwise identical SQLite build isolates the page-size effect: 4 KiB pages use 256.97 MB and 64 KiB pages use 233.18 MB (-9.26%). The `events` table's unused page bytes fall from 30.25 MB to 6.95 MB, while the index changes from 5.92 MB to 6.03 MB. In the paired run, full write, full read, and suffix read change by -0.5%, -0.4%, and -3.8%; fork changes by -14.8%. The space gain therefore comes from better large-row page utilization rather than a smaller index or omitted data, without a measured latency regression.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep JSONL level 19.** Rejected. On the expanded corpus it saves another 12.1% relative to default-level frames but increases full-write time by 67.0% and fork time by 129.8%, while complete and suffix reads differ by less than 1%. Default-level frames plus provenance ranges retain a 14.1% size reduction relative to master without a material latency regression.
|
||||
|
||||
**Compress one whole JSONL log as a single frame.** Rejected. It improves cross-batch compression but makes suffix reads decompress from the start and removes batch-local torn-tail recovery.
|
||||
|
||||
**Keep 4 KiB SQLite pages.** Rejected for pristine databases. The current compressed-row distribution retains 9.26% more bytes because large compressed records leave more unusable space across 4 KiB B-tree pages. Existing databases keep their page size to avoid a historical rewrite.
|
||||
|
||||
**Remove ROWID from `events`.** Rejected. The composite primary key becomes the table B-tree key and repeats through internal pages; the 105-session comparison produced a larger database than ordinary ROWID tables.
|
||||
|
||||
**Deduplicate event content.** Rejected. Message restatements and tool arguments can be reconstructed only under assumptions that compaction, retries, and pruning may invalidate. Physical compression preserves every event without adding reconstruction semantics.
|
||||
|
||||
**Use per-session SQLite files or DuckDB.** Rejected for the hot store. Per-session files lose cross-session queries, while DuckDB's OLAP write model fits cold batch analysis rather than durable append batches and low-latency suffix reads.
|
||||
|
||||
## Consequences
|
||||
|
||||
JSONL keeps the low-cost provenance optimization without the level-19 write and fork penalty. SQLite exchanges approximately 5–26% more time across the measured operations for a 46.8% retained-size reduction; its full write remains materially faster than JSONL, and its suffix read remains much faster. Its complete read and fork are slightly slower than default-level JSONL on this expanded corpus.
|
||||
|
||||
New SQLite databases use 64 KiB WAL frames and cache pages. Small databases may reserve more bytes for sparsely populated schema and metadata pages, while the measured multi-session workload gains substantially better `events` page utilization. Schema 19 rejects every other schema version rather than migrating it.
|
||||
|
||||
## Related
|
||||
|
||||
- [sqlite-physical-chunk-row-compression](2026-08-18-sqlite-physical-chunk-row-compression.md) — owns the packed row model; its earlier page-size conclusion applies to the pre-dictionary layout.
|
||||
- [zstandard-jsonl-session-logs](2026-07-19-zstandard-jsonl-session-logs.md) — owns the checksummed frame-per-batch container and the standard compressor-level policy restored here.
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# Agent Note: 持久化压缩延迟与 SQLite page size
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-25-persistence-latency-and-page-size.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
物理持久化优化需要减少保留存储,同时不能把不成比例的工作转移到完整写入、读取或会话 fork。原有的 105 会话语料显示,JSONL level-19 压缩会让完整写入与 fork 耗时增加一倍以上。此前的 SQLite page-size 实验早于共享字典行压缩,所得空间收益可以忽略,因此无法确定当前行分布的最佳 page size。
|
||||
|
||||
该决策需要来自更多样会话的证据,包括长事件流与原语料之外的 payload。扩展后的语料包含 501 个真实会话、16,153,332 个逻辑事件与 2,002,145,570 字节序列化事件数据。
|
||||
|
||||
## 决策
|
||||
|
||||
### 存储编码保持为物理层行为并可独立解码
|
||||
|
||||
JSONL 把严格递增的 `sourceEventSeqs` 存为标量值与闭区间的混合数组,其他顺序保持原样。SQLite 把同一数组存为带 tag 的 zigzag-delta 或 `(start, count)` varint,并选择更小的编码。两个读取方都会在暴露事件前还原原始 `number[]`。
|
||||
|
||||
SQLite 使用内部整数 `sessions.id`,并只在 `sessions.session_key` 中保留一次公开会话 id,使事件行及其主键不再重复文本标识。每个 `events.data` 值仍可独立解码:写入方尝试用打包的 64 KiB raw-content 字典执行 level-3 Zstandard 压缩,结果不更小时保留 SQLite 文本。字典字节属于 schema 19,测试固定其 SHA-256 摘要;替换字典需要再次提升 schema 版本。
|
||||
|
||||
### JSONL 使用 Zstandard 标准级别
|
||||
|
||||
JSONL 写入方继续为每个持久 append 批次写入一个带 checksum 的 Zstandard frame,但使用压缩器的标准级别。无损 `sourceEventSeqs` 区间编码继续生效。各 frame 仍可独立解码,以支持后缀读取与撕裂尾部恢复;只移除昂贵的 level-19 搜索。
|
||||
|
||||
### 新建 SQLite 数据库使用 64 KiB page
|
||||
|
||||
SQLite 提供方在初始化全新 schema-19 数据库前设置 `page_size=65536`。SQLite 在 page 已分配后会忽略该 pragma,因此已有 schema-19 数据库保留其当前 page size。
|
||||
|
||||
Page size 属于 schema 19 的固定物理布局,并与其他固定 SQLite pragma 一样通过包内封闭的 SQL 资源应用。
|
||||
|
||||
### 扩展基准
|
||||
|
||||
每个候选方案都从同一份 501 会话语料独立重建五次,每个 append 批次包含 512 个事件。各轮轮换执行顺序,使每个候选方案在每个运行位置各出现一次。每次重建执行三轮完整读取与后缀读取。下表中的每项指标都去掉最高与最低的一次重建,再平均其余三次。完整读取与后缀读取耗时覆盖对全部会话的一轮扫描,fork 耗时覆盖全部 501 个会话。
|
||||
|
||||
| 后端 | 存储大小 | 完整写入 | 完整读取 | 后缀读取 | Fork |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| JSONL `master` | 172.43 MB | 200.902 s | 8.033 s | 24.479 s | 72.670 s |
|
||||
| JSONL + 来源区间 | 148.15 MB (-14.1%) | 197.281 s (-1.8%) | 7.799 s (-2.9%) | 24.582 s (+0.4%) | 72.308 s (-0.5%) |
|
||||
| JSONL + 来源区间 + level 19 | 130.22 MB (-24.5%) | 329.442 s (+64.0%) | 7.764 s (-3.3%) | 24.454 s (-0.1%) | 166.177 s (+128.7%) |
|
||||
| SQLite `master`(schema 17) | 438.31 MB | 69.632 s | 8.211 s | 0.546 s | 64.290 s |
|
||||
| SQLite + 全部物理优化 + 64 KiB page | 233.18 MB (-46.8%) | 87.656 s (+25.9%) | 9.155 s (+11.5%) | 0.575 s (+5.3%) | 79.417 s (+23.5%) |
|
||||
|
||||
相对使用来源区间的标准级别 frame,level 19 可再减少 12.1% 的 JSONL 字节,但会让完整写入增加 67.0%、fork 增加 129.8%;完整读取与后缀读取分别变化 -0.4% 与 -0.5%。因此,更深入的搜索只改善保留体积,无法通过延迟敏感操作的收益抵消反复付出的编码成本。
|
||||
|
||||
其余条件相同的 SQLite 重建可单独观察 page-size 影响:4 KiB page 使用 256.97 MB,64 KiB page 使用 233.18 MB(-9.26%)。`events` 表的 page 内未使用字节从 30.25 MB 降至 6.95 MB,索引则从 5.92 MB 变为 6.03 MB。在该成对运行中,完整写入、完整读取与后缀读取分别变化 -0.5%、-0.4% 与 -3.8%,fork 变化 -14.8%。因此,空间收益来自更高的大记录 page 利用率,而不是索引缩小或数据省略,并且没有测得延迟退化。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留 JSONL level 19。** 不予采用。在扩展语料上,它相对默认级别 frame 可再减少 12.1%,却让完整写入增加 67.0%、fork 增加 129.8%,而完整读取与后缀读取的差异都不足 1%。默认级别 frame 配合来源区间后,相对 master 仍能缩小 14.1%,且没有实质性延迟退化。
|
||||
|
||||
**把整份 JSONL 日志压成单个 frame。** 不予采用。该方案可改善跨批次压缩,但后缀读取必须从头解压,也会失去按批次恢复撕裂尾部的能力。
|
||||
|
||||
**新建 SQLite 数据库继续使用 4 KiB page。** 不予采用。当前压缩行分布会在 4 KiB B-tree page 之间留下更多不可用空间,使保留字节增加 9.26%。已有数据库保留其 page size,避免改写历史数据。
|
||||
|
||||
**从 `events` 移除 ROWID。** 不予采用。复合主键会成为表 B-tree 键并在内部 page 中重复;105 会话对比所得数据库大于使用普通 ROWID 的表。
|
||||
|
||||
**对事件内容去重。** 不予采用。消息复述与工具参数只能在依赖重建假设时删除,而 compaction、重试和修剪可能让这些假设失效。物理压缩保留每个事件,不增加重建语义。
|
||||
|
||||
**使用逐会话 SQLite 文件或 DuckDB。** 不用于热存储。逐会话文件会失去跨会话查询,DuckDB 的 OLAP 写入模型则更适合冷批量分析,而不是持久 append 批次与低延迟后缀读取。
|
||||
|
||||
## 后果
|
||||
|
||||
JSONL 保留低成本来源优化,同时避开 level-19 的写入与 fork 代价。SQLite 以实测各项操作约 5–26% 的额外耗时换取 46.8% 的保留体积缩减;其完整写入仍明显快于 JSONL,后缀读取也仍快得多。在这份扩展语料上,完整读取与 fork 略慢于默认级别 JSONL。
|
||||
|
||||
新建 SQLite 数据库使用 64 KiB WAL frame 与 cache page。小型数据库可能为稀疏的 schema 与元数据 page 预留更多字节,而实测的多会话工作负载显著改善了 `events` page 利用率。Schema 19 会拒绝其他所有 schema 版本,而不是迁移它们。
|
||||
|
||||
## 相关资料
|
||||
|
||||
- [sqlite-physical-chunk-row-compression](2026-08-18-sqlite-physical-chunk-row-compression.zh.md) — 定义打包行模型;其此前的 page-size 结论适用于共享字典之前的布局。
|
||||
- [zstandard-jsonl-session-logs](2026-07-19-zstandard-jsonl-session-logs.zh.md) — 定义带 checksum 的按批次 frame 容器,以及本笔记恢复的标准压缩级别策略。
|
||||
@@ -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 docs/config-catalog.md
|
||||
config-catalog.md: e771c16e7b0407b55806bae37caf689a9cae530d
|
||||
config-catalog.zh.md: 8f0d597b3bbbf339dad56470ee547ae51bb6494b
|
||||
config-catalog.md: f91a64d73af53cc9c57d9d2e795f8f54bcf9eacf
|
||||
config-catalog.zh.md: 8b1ab084668ef8e1568801855f9aebc19e76a5e3
|
||||
|
||||
@@ -1307,7 +1307,7 @@ export interface ReplayModelConfig {
|
||||
|
||||
Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
|
||||
|
||||
Source: [`packages/test-support/llm-replay/src/index.ts:914`](../packages/test-support/llm-replay/src/index.ts)
|
||||
Source: [`packages/test-support/llm-replay/src/index.ts:918`](../packages/test-support/llm-replay/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-retry"></a>
|
||||
|
||||
|
||||
@@ -1309,7 +1309,7 @@ export interface ReplayModelConfig {
|
||||
|
||||
依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
|
||||
|
||||
来源:[`packages/test-support/llm-replay/src/index.ts:914`](../packages/test-support/llm-replay/src/index.ts)
|
||||
来源:[`packages/test-support/llm-replay/src/index.ts:918`](../packages/test-support/llm-replay/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-retry"></a>
|
||||
|
||||
|
||||
@@ -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 docs/subsystems/persistence.md
|
||||
persistence.md: 402836cb727fb99d92cea5e2a0d242b010aad2c9
|
||||
persistence.zh.md: 424fc928d5a8ef18b403ea31e5a5d26d3fd3fdc7
|
||||
persistence.md: f73b9ab01c232c4d4fec5aa51e5250c60b9337da
|
||||
persistence.zh.md: 061c29f6b54c41137e3c764e9a7804f411f63f17
|
||||
|
||||
@@ -233,7 +233,7 @@ interface SessionPersistenceSnapshot {
|
||||
All implement the same abstract `SessionPersistence` (locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots over `SessionEvent`, with optional cancellation on observation methods) and pass the shared `runPersistenceContract` suite:
|
||||
|
||||
- **[dsh-session-persistence-jsonl](../../packages/session/session-persistence-jsonl)** — an append-only logical JSONL log per session, stored as checksummed concatenated Zstandard frames by default or raw lines by configuration, with crash-safe atomic writes, interrupted-turn recovery, and a read/replay path.
|
||||
- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)** — an opt-in `node:sqlite` backend using schema 18 to store exact same-block delta runs in bounded physical `text-chunks`, `reasoning-chunks`, and `tool-call-chunks` rows. It reconstructs the complete logical event stream before returning it, packs only newly durable batches, and rejects older schemas rather than migrating them.
|
||||
- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)** — an opt-in `node:sqlite` backend using schema 19 to store exact same-block delta runs in bounded physical `text-chunks`, `reasoning-chunks`, and `tool-call-chunks` rows. It reconstructs the complete logical event stream before returning it, packs only newly durable batches, and rejects older schemas rather than migrating them.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ interface SessionPersistenceSnapshot {
|
||||
两者都实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 上执行 locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots,观察方法可选支持取消),并通过共享的 `runPersistenceContract` 套件:
|
||||
|
||||
- **[dsh-session-persistence-jsonl](../../packages/session/session-persistence-jsonl)**——逐会话仅追加的逻辑 JSONL 日志,默认存储为带 checksum 的连续 Zstandard frame,也可配置为原始行;支持崩溃安全的原子写入、被中断轮次的恢复以及读取/回放路径。
|
||||
- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)**:一个可选启用的 `node:sqlite` 后端,使用 schema 18 把同一分片块中字段完全匹配的 delta 连续段存为有界物理 `text-chunks`、`reasoning-chunks` 与 `tool-call-chunks` 行。它在返回前重建完整逻辑事件流,只打包新增的持久批次,并拒绝旧 schema,而不是执行迁移。
|
||||
- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)**:一个可选启用的 `node:sqlite` 后端,使用 schema 19 把同一分片块中字段完全匹配的 delta 连续段存为有界物理 `text-chunks`、`reasoning-chunks` 与 `tool-call-chunks` 行。它在返回前重建完整逻辑事件流,只打包新增的持久批次,并拒绝旧 schema,而不是执行迁移。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -1152,4 +1152,5 @@ export class SessionStore extends Service {
|
||||
|
||||
}
|
||||
|
||||
export { decodeSeqRanges, encodeSeqRanges } from './seq-ranges.ts'
|
||||
export default SessionStore
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/** Lossless range encoding for JSONL `sourceEventSeqs` arrays. */
|
||||
|
||||
/** A stored source sequence or inclusive consecutive range. */
|
||||
export type EncodedSeq = number | [number, number]
|
||||
|
||||
function isStrictlyIncreasing(values: readonly number[]): boolean {
|
||||
return values.every((value, index) => index === 0 || value > (values[index - 1] as number))
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace profitable consecutive runs with inclusive pairs.
|
||||
* @param values - validated in-memory source sequences.
|
||||
* @returns a lossless JSON storage form.
|
||||
*/
|
||||
export function encodeSeqRanges(values: readonly number[]): EncodedSeq[] {
|
||||
if (!isStrictlyIncreasing(values)) return [...values]
|
||||
const encoded: EncodedSeq[] = []
|
||||
for (let start = 0; start < values.length;) {
|
||||
let end = start
|
||||
while (end + 1 < values.length && values[end + 1] === (values[end] as number) + 1) end += 1
|
||||
if (end - start >= 2) encoded.push([values[start] as number, values[end] as number])
|
||||
else for (let index = start; index <= end; index += 1) encoded.push(values[index] as number)
|
||||
start = end + 1
|
||||
}
|
||||
return encoded
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a JSON storage-form source sequence array.
|
||||
* @param value - parsed storage value.
|
||||
* @param maxEntries - largest list permitted by the owning event.
|
||||
* @returns the in-memory source sequences.
|
||||
*/
|
||||
export function decodeSeqRanges(value: unknown, maxEntries = Number.MAX_SAFE_INTEGER): number[] {
|
||||
if (!Array.isArray(value)) throw new TypeError('sourceEventSeqs must be an array')
|
||||
const decoded: number[] = []
|
||||
let hasRange = false
|
||||
for (const entry of value) {
|
||||
if (typeof entry === 'number') {
|
||||
assertSeq(entry)
|
||||
if (decoded.length >= maxEntries) throw new TypeError('sourceEventSeqs exceeds its event sequence')
|
||||
decoded.push(entry)
|
||||
continue
|
||||
}
|
||||
if (!Array.isArray(entry) || entry.length !== 2) {
|
||||
throw new TypeError('sourceEventSeqs range entries must be [start, end] pairs')
|
||||
}
|
||||
const start: unknown = entry[0]
|
||||
const end: unknown = entry[1]
|
||||
assertSeq(start)
|
||||
assertSeq(end)
|
||||
if (end < start) throw new TypeError('sourceEventSeqs ranges require start <= end')
|
||||
const length = end - start + 1
|
||||
if (length > maxEntries - decoded.length) {
|
||||
throw new TypeError('sourceEventSeqs range exceeds its event sequence')
|
||||
}
|
||||
for (let seq = start; seq <= end; seq += 1) decoded.push(seq)
|
||||
hasRange = true
|
||||
}
|
||||
if (hasRange && !isStrictlyIncreasing(decoded)) {
|
||||
throw new TypeError('sourceEventSeqs ranges must be strictly increasing')
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
function assertSeq(value: unknown): asserts value is number {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
||||
throw new TypeError('sourceEventSeqs must contain non-negative safe integers')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { decodeSeqRanges, encodeSeqRanges } from '@deepseek-ai/dsh-session'
|
||||
|
||||
describe('sourceEventSeqs ranges', () => {
|
||||
it.each([
|
||||
[],
|
||||
[5],
|
||||
[10, 11, 12, 13, 14],
|
||||
[16, 17, 100, 200, 201, 202, 203],
|
||||
[3, 2],
|
||||
[Number.MAX_SAFE_INTEGER - 1, 0, Number.MAX_SAFE_INTEGER - 2],
|
||||
].map(values => [values]))('round-trips %j', (values) => {
|
||||
expect(decodeSeqRanges(encodeSeqRanges(values))).toEqual(values)
|
||||
})
|
||||
|
||||
it('encodes only profitable increasing runs', () => {
|
||||
expect(encodeSeqRanges([1, 3, 4, 5, 7])).toEqual([1, [3, 5], 7])
|
||||
expect(encodeSeqRanges([1, 3, 4, 7])).toEqual([1, 3, 4, 7])
|
||||
expect(encodeSeqRanges([3, 2])).toEqual([3, 2])
|
||||
})
|
||||
|
||||
it('does not impose a persistence-only provenance length limit', () => {
|
||||
const values = Array.from({ length: 1_000_001 }, (_, index) => index)
|
||||
expect(encodeSeqRanges(values)).toEqual([[0, 1_000_000]])
|
||||
})
|
||||
|
||||
it('rejects malformed or impossible expansions', () => {
|
||||
expect(() => decodeSeqRanges('nope')).toThrow(/must be an array/)
|
||||
expect(() => decodeSeqRanges([-1])).toThrow(/non-negative safe integers/)
|
||||
expect(() => decodeSeqRanges([[1]])).toThrow(/\[start, end\] pairs/)
|
||||
expect(() => decodeSeqRanges([[4, 2]])).toThrow(/start <= end/)
|
||||
expect(() => decodeSeqRanges([[2, 5], [4, 7]])).toThrow(/strictly increasing/)
|
||||
expect(() => decodeSeqRanges([0], 0)).toThrow(/exceeds its event sequence/)
|
||||
expect(() => decodeSeqRanges([[0, 10]], 10)).toThrow(/exceeds its event sequence/)
|
||||
})
|
||||
})
|
||||
@@ -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-persistence-jsonl/README.md
|
||||
README.md: cd0e9fefaed6260f890efd07160dbc264b915c9a
|
||||
README.zh.md: 8be8571bc7424621d4ecb940a8e651c8b5f73e6e
|
||||
README.md: 69fb2901d783c327878cd37570aec730a3ca0841
|
||||
README.zh.md: 188982028374d4ca11a5a658acabce5fe5df9930
|
||||
|
||||
@@ -54,7 +54,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
|
||||
|
||||
### On-disk layout
|
||||
|
||||
Each session gets a session-owned directory under a readable project directory; the first logical line of the log is the immutable `SessionHeader`, followed by one storage record per logical event (or one packed chunk row per eligible run):
|
||||
Each session gets a session-owned directory under a readable project directory; the first logical line of the log is the immutable `SessionHeader`, followed by one storage record per logical event (or one packed chunk row per eligible run). Storage records use the lossless provenance representation described below:
|
||||
|
||||
```text
|
||||
<root>/
|
||||
@@ -90,7 +90,7 @@ The backend is a thin storage layer over the shared [PersistenceCoordinator](../
|
||||
|
||||
### Physical encoding
|
||||
|
||||
The default artifact is a standard concatenation of independent [Zstandard frames](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md): one checksummed frame containing only the header line, then one checksummed frame per durable append batch, using Node's built-in Zstandard API at its default compression level (no level knob). Listing reads and validates only the header frame. A root belongs to one encoding: startup discovery and targeted lookup reject the opposite suffix, and there is no format or compression migration, mixed-root fallback, or dual write. When `packChunks` is enabled, an eligible run of ≥3 consecutive same-block `assistant/chunk` delta events becomes one packed row (`text-chunks`/`reasoning-chunks`/`tool-call-chunks`) whose `seq0`/`time0` and per-member `dt` gaps reconstruct every member exactly; the lossless codec lives in `dsh-session` and reading is layout-blind, so packed, unpacked, and mixed files load identically.
|
||||
The default artifact is a standard concatenation of independent [Zstandard frames](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md): one checksummed frame containing only the header line, then one checksummed frame per durable append batch, using Node's built-in Zstandard API at its default compression level (no level knob). `sourceEventSeqs` uses a lossless storage representation: consecutive runs of at least three sequence numbers become `[start, end]` pairs, any other list stays verbatim, and reading expands the exact in-memory array. Listing reads and validates only the header frame. `compression: 'none'` keeps the same storage-form logical lines without frame compression. A root belongs to one encoding: startup discovery and targeted lookup reject the opposite suffix, and there is no format or compression migration, mixed-root fallback, or dual write. When `packChunks` is enabled, an eligible run of ≥3 consecutive same-block `assistant/chunk` delta events becomes one packed row (`text-chunks`/`reasoning-chunks`/`tool-call-chunks`) whose `seq0`/`time0` and per-member `dt` gaps reconstruct every member exactly; the lossless codec lives in `dsh-session` and reading is layout-blind, so packed, unpacked, and mixed files load identically.
|
||||
|
||||
### Source map
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ kind: "package-reference"
|
||||
|
||||
### 磁盘布局
|
||||
|
||||
每个会话在可读项目目录下获得一个会话自有目录;日志第一个逻辑行是不可变 `SessionHeader`,之后每个逻辑事件一条存储记录(或每个符合条件的连续段一条打包分片行):
|
||||
每个会话在可读项目目录下获得一个会话自有目录;日志第一个逻辑行是不可变 `SessionHeader`,之后每个逻辑事件一条存储记录(或每个符合条件的连续段一条打包分片行)。存储记录使用下文所述的无损来源序列表示:
|
||||
|
||||
```text
|
||||
<root>/
|
||||
@@ -90,7 +90,7 @@ kind: "package-reference"
|
||||
|
||||
### 物理编码
|
||||
|
||||
默认产物是独立 [Zstandard 帧](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md) 的标准拼接:一个仅包含 header 行的带校验和帧,后跟每个持久 append 批次一个带校验和帧,使用 Node 内置 Zstandard API 的默认压缩级别(无级别开关)。列表只读取并验证 header 帧。一个根只属于一种编码:启动发现与定向查找会拒绝相反后缀,且不提供格式或压缩迁移、混合根回退或双写。启用 `packChunks` 时,符合条件的 ≥3 个连续同 block `assistant/chunk` delta 事件连续段会变成一行打包行(`text-chunks`/`reasoning-chunks`/`tool-call-chunks`),其 `seq0`/`time0` 与各成员的 `dt` 间隔精确重建每个成员;无损 codec 位于 `dsh-session`,读取与布局无关,因此打包、非打包与混合文件加载结果一致。
|
||||
默认产物是独立 [Zstandard 帧](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md) 的标准拼接:一个仅包含 header 行的带校验和帧,后跟每个持久 append 批次一个带校验和帧,使用 Node 内置 Zstandard API 的默认压缩级别(无级别开关)。`sourceEventSeqs` 使用无损存储形式:至少包含三个序列号的连续段会变成 `[start, end]` 区间对,其他列表原样保留;读取时会展开回精确的内存数组。列表只读取并验证 header 帧。`compression: 'none'` 保留相同的存储形式逻辑行,但不使用帧压缩。一个根只属于一种编码:启动发现与定向查找会拒绝相反后缀,且不提供格式或压缩迁移、混合根回退或双写。启用 `packChunks` 时,符合条件的 ≥3 个连续同 block `assistant/chunk` delta 事件连续段会变成一行打包行(`text-chunks`/`reasoning-chunks`/`tool-call-chunks`),其 `seq0`/`time0` 与各成员的 `dt` 间隔精确重建每个成员;无损 codec 位于 `dsh-session`,读取与布局无关,因此打包、非打包与混合文件加载结果一致。
|
||||
|
||||
### 源码地图
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
*/
|
||||
|
||||
import { join } from 'node:path'
|
||||
import { decodeStorageRecord, packChunkRuns, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
decodeSeqRanges, decodeStorageRecord, encodeSeqRanges, packChunkRuns, SESSION_FORMAT_VERSION,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionHeader, SessionId, StorageRecord } from '@deepseek-ai/dsh-session'
|
||||
import { SessionFormatUnsupportedError, sessionFormatVersionRefusal } from '@deepseek-ai/dsh-session-persistence'
|
||||
|
||||
@@ -211,16 +213,47 @@ export function logPath(
|
||||
* Serialize an event batch as JSONL lines (no trailing newline). With
|
||||
* `packChunks` on, delta-chunk runs pack into `text-chunks` /
|
||||
* `reasoning-chunks` / `tool-call-chunks` storage rows; off writes one event
|
||||
* per line, byte-identical to the pre-packing layout. Reading is layout-blind
|
||||
* either way ({@link scanLog} always decodes rows), so the switch changes only
|
||||
* newly written bytes.
|
||||
* per line. Both modes range-encode provenance at the storage boundary.
|
||||
* Reading is layout-blind either way ({@link scanLog} always decodes rows),
|
||||
* so the switch changes only newly written bytes.
|
||||
* @param events - the batch to serialize, in log order.
|
||||
* @param packChunks - whether to pack delta runs into storage rows.
|
||||
* @returns the batch's JSONL text; the writer adds the final newline.
|
||||
*/
|
||||
export function eventLines(events: readonly SessionEvent[], packChunks: boolean): string {
|
||||
const records: readonly StorageRecord[] = packChunks ? packChunkRuns(events) : events
|
||||
return records.map(record => JSON.stringify(record)).join('\n')
|
||||
return records.map(record => JSON.stringify(encodeProvenanceForStorage(record))).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Losslessly shrink a record's `sourceEventSeqs` for the log: consecutive
|
||||
* runs of at least three seqs become `[start, end]` pairs, and any other list
|
||||
* stays verbatim.
|
||||
* @param record - one stored record (event or packed row).
|
||||
* @returns the record with its provenance in storage form (widened from the
|
||||
* in-memory `number[]`; {@link expandProvenanceFromStorage} restores it).
|
||||
*/
|
||||
function encodeProvenanceForStorage(record: StorageRecord): unknown {
|
||||
if (!('sourceEventSeqs' in record)) return record
|
||||
return { ...record, sourceEventSeqs: encodeSeqRanges(record.sourceEventSeqs) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a parsed line's storage-form provenance back to `number[]`.
|
||||
* @param parsed - the JSON-parsed value of one stored line.
|
||||
* @returns the value with provenance expanded.
|
||||
* @throws when the record or its storage-form provenance is malformed.
|
||||
*/
|
||||
function expandProvenanceFromStorage(parsed: unknown): unknown {
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
throw new TypeError('stored session records must be objects')
|
||||
}
|
||||
const record = parsed as { seq?: unknown; sourceEventSeqs?: unknown }
|
||||
if (record.sourceEventSeqs === undefined) return parsed
|
||||
if (!Number.isSafeInteger(record.seq) || (record.seq as number) < 0) {
|
||||
throw new TypeError('stored session event seq must be a non-negative safe integer')
|
||||
}
|
||||
return { ...record, sourceEventSeqs: decodeSeqRanges(record.sourceEventSeqs, record.seq as number) }
|
||||
}
|
||||
|
||||
interface SessionLogScan {
|
||||
@@ -348,7 +381,7 @@ export class SessionLogScanner {
|
||||
this.eventLine += 1
|
||||
let decoded: SessionEvent[]
|
||||
try {
|
||||
decoded = decodeStorageRecord(JSON.parse(line.toString('utf8')))
|
||||
decoded = decodeStorageRecord(expandProvenanceFromStorage(JSON.parse(line.toString('utf8'))))
|
||||
} catch {
|
||||
this.issue ??= new Error(`corrupt session log: unparsable committed event at line ${this.eventLine}`)
|
||||
return
|
||||
|
||||
@@ -992,13 +992,20 @@ describe('JsonlSessionPersistence: scanLog unit', () => {
|
||||
expect(() => scanLog(Buffer.from(log))).toThrow(/seq gap in committed region/)
|
||||
})
|
||||
|
||||
it('rejects a corrupt line BEFORE a later committed turn/end (committed data damaged)', () => {
|
||||
const log = [
|
||||
JSON.stringify({ type: 'session', version: 0, id: 'c', createdAt: 1, delegationDepth: 0 }),
|
||||
'{not json', // corrupt, sits in the committed region (a turn/end follows)
|
||||
JSON.stringify({ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } }),
|
||||
].join('\n') + '\n'
|
||||
expect(() => scanLog(Buffer.from(log))).toThrow(/unparsable committed event/)
|
||||
it('rejects malformed records before a later committed turn/end', () => {
|
||||
const corruptRecords = [
|
||||
'{not json',
|
||||
'null',
|
||||
JSON.stringify({ type: 'assistant/message', sourceEventSeqs: [0], data: {} }),
|
||||
]
|
||||
for (const record of corruptRecords) {
|
||||
const log = [
|
||||
JSON.stringify({ type: 'session', version: 0, id: 'c', createdAt: 1, delegationDepth: 0 }),
|
||||
record,
|
||||
JSON.stringify({ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } }),
|
||||
].join('\n') + '\n'
|
||||
expect(() => scanLog(Buffer.from(log))).toThrow(/unparsable committed event/)
|
||||
}
|
||||
})
|
||||
|
||||
it('a header-only log (no event lines at all) preserves nothing — committedBytes is the header', () => {
|
||||
@@ -1177,9 +1184,20 @@ describe('JsonlSessionPersistence: default packed chunk rows', () => {
|
||||
expect(scanned.committedBytes).toBe(Buffer.byteLength(headerAndTurn, 'utf8'))
|
||||
})
|
||||
|
||||
it('eventLines(packChunks: false) is byte-identical to the pre-packing layout', () => {
|
||||
it('eventLines(packChunks: false) keeps one event per line and round-trips provenance', () => {
|
||||
const log = chunkRunLog()
|
||||
expect(eventLines(log, false)).toBe(log.map(e => JSON.stringify(e)).join('\n'))
|
||||
const text = eventLines(log, false)
|
||||
const lines = text.split('\n')
|
||||
expect(lines).toHaveLength(log.length)
|
||||
for (const line of lines) {
|
||||
expect((JSON.parse(line) as { type: string }).type).not.toMatch(/-chunks$/)
|
||||
}
|
||||
// the message's consecutive provenance is stored as an inclusive range
|
||||
const messageLine = lines.map(l => JSON.parse(l) as { type: string; sourceEventSeqs?: unknown })
|
||||
.find(r => r.type === 'assistant/message')
|
||||
expect(messageLine?.sourceEventSeqs).toEqual([[2, 6]])
|
||||
const header = JSON.stringify(toHeaderLine(meta('packed', '/work'))) + '\n'
|
||||
expect(scanLog(Buffer.from(header + text + '\n')).events).toEqual(log)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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-persistence-sqlite/README.md
|
||||
README.md: 5f8d4cebf1a189cd768673359686ab2774550285
|
||||
README.zh.md: 12900cc8783430b8716612cdda55d2622cc7f7aa
|
||||
README.md: fc8e8eb7032eda475fceb065b80315421020a63f
|
||||
README.zh.md: 88858ea07ff6bee54d29b8af947e610dc3879901
|
||||
|
||||
@@ -33,9 +33,7 @@ Choose this backend when a local deployment benefits from one queryable database
|
||||
|
||||
### Disk footprint and performance
|
||||
|
||||
The packed layout trades disk space for speed and structure. The available benchmark measures schema 17, the packed predecessor with the same chunk codec but the former row discriminator; schema 18 has not been remeasured. On its corpus — 105 sessions, about 2.5 million events — the SQLite database used 75 MB against 31 MB for the default compressed JSONL logs: roughly 2.5× the on-disk size.
|
||||
|
||||
The same measurements show writes finishing about 3× faster, 50-event suffix reads about 40× faster, full-session reads comparable or slightly faster, and about 2.5 million physical rows shrinking to roughly 66 thousand. Expect 2–3× the compressed JSONL footprint depending on session content; the full numbers and method live in the [SQLite physical chunk-row decision](../../../.agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.md).
|
||||
The packed layout exchanges some SQLite-local latency for a smaller queryable database. On the 501-session comparison corpus, the schema-19 layout used 233.18 MB against the SQLite comparison baseline's 438.31 MB and compressed JSONL's 148.15 MB. Full writes were about 2.3× faster than JSONL and suffix reads remained much faster; complete reads and forks were slightly slower than JSONL. The [persistence latency and page-size decision](../../../.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.md) owns the method, complete metrics, and accepted trade-offs.
|
||||
|
||||
The disk cost buys a structured, queryable view of session history: external tooling can analyze `sessions` and `events` with SQL, decoding physical rows the way this provider does — the groundwork for features such as built-in full-text search.
|
||||
|
||||
@@ -77,7 +75,7 @@ await ctx.sessionPersistence.append(id, events)
|
||||
|
||||
### Startup and safe operation
|
||||
|
||||
A fresh database initializes directly at schema version 18. Databases with any other version, a foreign application identity, an unversioned non-pristine schema, or unexpected schema objects are rejected before any data is exposed or changed — this pre-release provider ships no migration. Every statement and fixed pragma comes from packaged `.sql` resources in `resources/sql/`, and runtime values are bound as SQLite parameters, so package code never assembles query text.
|
||||
A fresh database initializes directly at schema version 19 with 64 KiB pages. Existing files are never retuned: databases with any other version, a foreign application identity, an unversioned non-pristine schema, or unexpected schema objects are rejected before any data is exposed or changed. This pre-release provider ships no migration. Every statement and fixed pragma comes from packaged `.sql` resources in `resources/sql/`, and runtime values are bound as SQLite parameters, so package code never assembles query text.
|
||||
|
||||
Each connection disables SQLite trusted schemas and memory-mapped I/O, verifies the requested journal mode, and pins `synchronous=FULL` so a resolved append remains durable across an OS crash or power loss. On POSIX, the database parent directory and file must belong to the current user, the parent must not be group/world-writable, and the file must grant no group or world permissions; Windows additionally rejects symbolic links and non-regular files, while ACL restriction stays the deployment's job. Path and ownership failures reject plugin initialization; Node's SQLite driver loads lazily on the first persistence operation. Ordinary `create` stays lazy until the first append, while `ensureMaterialized` writes a session metadata row with no event rows.
|
||||
|
||||
@@ -96,11 +94,11 @@ This section explains the design decisions behind the provider and points at the
|
||||
The provider is built on one separation and three commitments:
|
||||
|
||||
- **Logical contract, physical format.** Callers always read and write ordinary `SessionEvent[]`; how rows are packed, stored, and compressed is private to this package.
|
||||
- **The schema owns the format.** Schema 18 is a frozen physical contract: a database at another version, with a foreign identity, or with unexpected schema objects is rejected, never migrated. Changing the physical rules requires a new schema.
|
||||
- **The schema owns the format.** Schema 19 is a frozen physical contract: a database at another version, with a foreign identity, or with unexpected schema objects is rejected, never migrated. Changing the schema, row codec, page size, or dictionary bytes requires a new schema version.
|
||||
- **Durability is the default.** Appends run in immediate transactions with `synchronous=FULL`, and a resolved `append()` means the batch is durable. Normal appends are insert-only: earlier event rows are never rewritten.
|
||||
- **Efficiency within strict bounds.** Packing and compression keep the database small, but every limit is a hard format bound — at most 1,024 events and 1 MiB of payload per packed row.
|
||||
|
||||
The decision history — alternatives considered, measurements, and consequences — lives in the [SQLite physical chunk-row decision](../../../.agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.md).
|
||||
The packed-row foundation lives in the [SQLite physical chunk-row decision](../../../.agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.md); the current compression, key, and page-size choices live in the [persistence latency and page-size decision](../../../.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.md).
|
||||
|
||||
### Source map
|
||||
|
||||
@@ -110,7 +108,7 @@ The decision history — alternatives considered, measurements, and consequences
|
||||
| [`src/store.ts`](src/store.ts) | Storage primitives: transactional append, reads, repair, path and ownership validation |
|
||||
| [`src/schema.ts`](src/schema.ts) | Schema ownership: version gate, connection hardening, row decoding |
|
||||
| [`src/codec.ts`](src/codec.ts) | Packing: which `assistant/chunk` runs become packed rows, size bounds |
|
||||
| [`src/compression.ts`](src/compression.ts) | Physical encoding: compression threshold, sequence lists, row scan and decode |
|
||||
| [`src/compression.ts`](src/compression.ts) | Physical encoding: dictionary compression, sequence lists, row scan and decode |
|
||||
| [`src/sql.ts`](src/sql.ts) + [`resources/sql/`](resources/sql/) | Every SQL statement as a packaged, closed-name resource |
|
||||
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; packing is observable only by database round-trip) |
|
||||
|
||||
@@ -124,7 +122,7 @@ A fresh database contains three strict tables, defined in [`resources/sql/schema
|
||||
| `sessions` | One row per session: header fields plus a monotonic revision |
|
||||
| `events` | Physical event rows: one logical event, or one packed run |
|
||||
|
||||
The exact columns live in [`resources/sql/schema.sql`](resources/sql/schema.sql). `events.data` holds text or a blob: small payloads stay text, larger ones are stored compressed when that is smaller. `events.is_packed` is `0` for a scalar logical event and `1` for a packed chunk run, so a scalar event whose type matches a physical chunk tag remains unambiguous. Packed rows reuse the `seq` of their first logical event, so under the composite `(session_id, seq)` primary key physical order is logical order.
|
||||
The exact columns live in [`resources/sql/schema.sql`](resources/sql/schema.sql). `sessions.id` is an internal integer key while `sessions.session_key` retains the public session id. `events.data` holds text or an independently decodable Zstandard blob; compression uses the schema-owned shared dictionary only when the result is smaller. `events.source_event_seqs` uses tagged delta or run encoding. `events.is_packed` is `0` for a scalar logical event and `1` for a packed chunk run, so a scalar event whose type matches a physical chunk tag remains unambiguous. Packed rows reuse the `seq` of their first logical event, so under the composite `(session_id, seq)` primary key physical order is logical order.
|
||||
|
||||
### Write path
|
||||
|
||||
@@ -147,6 +145,7 @@ Read these pages when the package-level contract is not enough. They move from t
|
||||
- [Session package map](../README.md) — adjacent persistence, projection, title, and telemetry packages.
|
||||
- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-persistence-sqlite) — every accepted config field and its source declaration.
|
||||
- [SQLite physical chunk-row decision](../../../.agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.md) — rationale, alternatives, and measurements behind the packed layout.
|
||||
- [Persistence latency and page-size decision](../../../.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.md) — the 501-session benchmark and schema-19 storage trade-offs.
|
||||
|
||||
-----
|
||||
|
||||
@@ -174,9 +173,9 @@ Physical packing does not mutate request prefixes. Provider cache reuse depends
|
||||
|
||||
These limits define when the provider is a poor fit or needs special operational care. They are current package constraints, not a general SQLite comparison or a task backlog.
|
||||
|
||||
- **Pre-release design with no migration** — schema 18 is an interim SQLite-only design; the deferred unified multi-backend relational design with configurable schemas exists as a working external prototype in [morlay/session-persistence-rdb](https://github.com/morlay/session-persistence-rdb) (Drizzle-based, SQLite and PostgreSQL), and neither schema stability nor migration support is guaranteed.
|
||||
- **Pre-release design with no migration** — schema 19 is an interim SQLite-only design; neither schema stability nor migration support is guaranteed.
|
||||
- **Packing depends on batch boundaries** — a compatible run split by the write-behind window or an explicit flush stays split across physical rows; this avoids rewriting prior rows at the cost of a timing-dependent packing ratio.
|
||||
- **Synchronous SQLite and compression** — Node's SQLite driver and Zstandard calls block the JavaScript thread; the 4 KiB compression threshold bounds per-frame work for small records.
|
||||
- **Synchronous SQLite and compression** — Node's SQLite driver and Zstandard calls block the JavaScript thread.
|
||||
- **Busy waits block the event loop** — SQLite waits inside synchronous calls; a competing writer can stall the thread for up to the configured `busyTimeoutMs`.
|
||||
- **External SQL readers must decode physical rows** — a packed `events.type` (`text-chunks`, `reasoning-chunks`, `tool-call-chunks`) is not a logical event type; supported consumers read through this provider.
|
||||
- **No deletion or historical compaction** — normal appends are insert-only and nothing removes old rows.
|
||||
@@ -187,37 +186,6 @@ These limits define when the provider is a poor fit or needs special operational
|
||||
<details>
|
||||
<summary>Working context for maintainers — click to expand</summary>
|
||||
|
||||
This Dev Note is working context for maintainers: measured artifacts, open design questions, and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes, and conclusions migrate there once they stabilize.
|
||||
|
||||
#### Benchmark artifact
|
||||
|
||||
The numbers below are the frozen schema-17 benchmark. Schema 18 changes the row discriminator and has not been remeasured; the [SQLite physical chunk-row decision](../../../.agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.md) is the authoritative record, and this table is an annotated digest.
|
||||
|
||||
| Metric | **JSONL (zstd)** | **SQLite (legacy)** | **SQLite (new)** |
|
||||
|---|---|---|---|
|
||||
| On-disk size | **30.65 MB** | 709.57 MB | 75.01 MB |
|
||||
| Write time, 105 sessions | 28.21 s | 10.64 s | **8.58 s** |
|
||||
| Complete-session read p50 / p95 | 4.49 / 23.36 ms | 9.02 / 69.16 ms | **3.95 / 21.58 ms** |
|
||||
| 50-event tail read p50 / p95 | 10.58 / 80.90 ms | **0.189 / 0.293 ms** | 0.253 / 0.378 ms |
|
||||
| Event rows | 2,507,860 (logical) | 2,507,860 | **65,810** |
|
||||
| Fork of all sessions | 14.48 s | 19.30 s | **13.10 s** |
|
||||
|
||||
The corpus was 105 sessions with 2,507,860 logical events appended in 512-event durable batches, so the ratios depend on session content, stream density, and batch boundaries. `SQLite (legacy)` is the scalar layout — one physical row per logical event, no packing — whose 709.57 MB footprint motivated the packed rows. In the measured schema-17 layout, SQLite uses ≈2.5× the JSONL disk space but writes ≈3.3× faster, reads complete sessions faster at both percentiles, and reads 50-event tails ≈40× faster; against the scalar layout it is ≈89% smaller, faster to write, and shrinks 2,507,860 rows to 65,810, while scalar tail reads remain marginally faster (0.189 vs 0.253 ms p50). Re-run or extend this benchmark whenever the write path or the schema changes.
|
||||
|
||||
#### Future: multi-backend RDB persistence (Drizzle)
|
||||
|
||||
A unified multi-backend relational design stays deferred. A Drizzle-backed rework would need to resolve: schema ownership — the per-version freeze and exact-object validation exist so any composition can read any same-version database, so a customizable schema must remain versioned and validated the same way; backend hardening — `synchronous=FULL`, busy timeout, and ownership checks are SQLite-specific, and Postgres or MySQL backends need their own durability and permission story; and codec portability — the packed-row codec is shaped around SQLite columns, so either a shared codec across dialects or per-backend codecs fixed by schema version must keep the logical contract identical.
|
||||
|
||||
#### Future: persistence-to-persistence transfer and version migration
|
||||
|
||||
The README documents a manual `load` → `create`/`append` transfer, but the seam has no import/export API, and SQLite rejects other schema versions outright. Automating transfer needs: an export format that preserves header lineage (`seedLength`, `parentSession`, `agentPreset`) and revision semantics; an upgrader chain for format and schema versions, the deferred chain from the [fail-closed event-vocabulary note](../../../.agents/notes/implemented/simplification/2026-08-25-fail-closed-session-event-vocabulary.md); and a source-side guarantee that the log is readable and balanced before export — `load` already commits cold repair.
|
||||
|
||||
#### Future: in-database full-text search and indexing
|
||||
|
||||
The sibling [session-query-sqlite](../../session-query/session-query-sqlite/README.md) package already maintains a dedicated SQLite FTS5 search index over session content in a separate derived-index database. Putting FTS inside the persistence database would duplicate that surface; open questions are where the index belongs, how to keep it transactional with append, and whether packed rows should be expanded into index documents or the index should read the logical stream. The persistence schema currently indexes only `(session_id, seq)`; further indexes (for example on `sessions.created_at` for cold-cutoff scans) are easy but add write cost.
|
||||
|
||||
#### Future: cold-data offloading to cascaded database files
|
||||
|
||||
The provider has no deletion or background compaction: everything stays in one database forever. One direction is offloading cold sessions (for example, older than 30 days) into separate archive database files arranged as a cascade, with more aggressive compression — a higher Zstandard level is cheap for cold data. That needs: a routing rule that knows which file holds which session, cross-file fan-out for `list`/`readFrom`/`load`, consistent revisions and store identity across the cascade, and a decision on whether offloading replaces the no-deletion limit or supplements it.
|
||||
The 501-session corpus contains private session data and is not committed. Its aggregate method, complete results, and rejected candidates are recorded in the [persistence latency and page-size decision](../../../.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.md); the packaged dictionary's hash-pinned resource is the schema-19 source of truth.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -33,9 +33,7 @@ kind: "package-reference"
|
||||
|
||||
### 磁盘占用与性能
|
||||
|
||||
打包布局以磁盘空间换取速度与结构。现有基准测量 schema 17,该打包前身使用相同的分片 codec,但行判别值不同;schema 18 尚未重新测量。在该语料上——105 个会话、约 250 万个事件——SQLite 数据库占用 75 MB,而默认压缩 JSONL 日志为 31 MB:磁盘占用约为后者的 2.5 倍。
|
||||
|
||||
同一组测量显示,写入快约 3 倍,50 个事件的后缀读取快约 40 倍,完整会话读取相当或略快,约 250 万个物理行缩减到约 6.6 万个。按会话内容不同,磁盘占用约为压缩 JSONL 的 2–3 倍;完整数据与方法见 [SQLite 物理分片行决策](../../../.agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.zh.md)。
|
||||
打包布局以部分 SQLite 本地延迟换取更小的可查询数据库。在 501 会话对比语料上,schema-19 布局占用 233.18 MB,SQLite 对比基线占用 438.31 MB,压缩 JSONL 占用 148.15 MB。全量写入约比 JSONL 快 2.3 倍,后缀读取也仍快得多;完整读取与 fork 则略慢于 JSONL。方法、完整指标与取舍由[持久化延迟与 page size 决策](../../../.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.zh.md)记录。
|
||||
|
||||
磁盘成本换来的是结构化、可查询的会话历史视图:外部工具可以用 SQL 分析 `sessions` 与 `events`,按本提供方的方式解码物理行——这是内置全文搜索等功能的天然基础。
|
||||
|
||||
@@ -77,7 +75,7 @@ await ctx.sessionPersistence.append(id, events)
|
||||
|
||||
### 启动与安全运行
|
||||
|
||||
全新数据库直接初始化为 schema 版本 18。任何其他版本、外来应用标识、无版本的非全新 schema 或意外 schema 对象,都会在任何数据暴露或变更之前被拒绝——本预发布提供方不提供迁移。每条语句和固定 pragma 都来自 `resources/sql/` 下打包的 `.sql` 资源,运行时的值以 SQLite 参数绑定,包代码从不拼装查询文本。
|
||||
全新数据库直接初始化为 schema 版本 19,并使用 64 KiB page。已有文件不会被重新调参:任何其他版本、外来应用标识、无版本的非全新 schema 或意外 schema 对象,都会在任何数据暴露或变更之前被拒绝。本预发布提供方不提供迁移。每条语句和固定 pragma 都来自 `resources/sql/` 下打包的 `.sql` 资源,运行时的值以 SQLite 参数绑定,包代码从不拼装查询文本。
|
||||
|
||||
每个连接都会禁用 SQLite trusted schema 与内存映射 I/O、验证所请求的 journal mode,并固定 `synchronous=FULL`,保证成功返回的追加在操作系统崩溃或断电后依然持久。在 POSIX 上,数据库父目录和文件必须属于当前用户,父目录不得允许组或其他用户写入,文件也不得授予任何组或其他用户权限;Windows 还会拒绝符号链接和非普通文件,ACL 限制则由部署方负责。路径与所有权失败会拒绝插件初始化;Node 的 SQLite 驱动在首次持久化操作时才延迟加载。普通 `create` 会保持惰性直到首次 append,而 `ensureMaterialized` 会写入一条没有事件行的会话元数据记录。
|
||||
|
||||
@@ -96,11 +94,11 @@ await ctx.sessionPersistence.append(id, events)
|
||||
本提供方建立在一个分离与三项承诺之上:
|
||||
|
||||
- **逻辑约定,物理格式。** 调用方始终读写普通的 `SessionEvent[]`;行如何打包、存储与压缩是本包私有的存储行为。
|
||||
- **schema 拥有格式。** Schema 18 是冻结的物理约定:任何其他版本、外来标识或意外 schema 对象的数据库都会被拒绝,绝不迁移。改变物理规则需要新的 schema。
|
||||
- **schema 拥有格式。** Schema 19 是冻结的物理约定:任何其他版本、外来标识或意外 schema 对象的数据库都会被拒绝,绝不迁移。改变 schema、行 codec、page size 或字典字节都需要新的 schema 版本。
|
||||
- **持久性是默认值。** 追加在立即事务中以 `synchronous=FULL` 提交,成功返回的 `append()` 意味着该批次已持久。普通追加仅插入:更早的事件行永远不会被重写。
|
||||
- **在严格边界内追求效率。** 打包与压缩让数据库保持小巧,但每个上限都是硬性格式边界——每个打包行至多表示 1,024 个事件、1 MiB 载荷。
|
||||
|
||||
决策历史——备选方案、测量数据与后果——记录在 [SQLite 物理分片行决策](../../../.agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.zh.md) 中。
|
||||
打包行基础由 [SQLite 物理分片行决策](../../../.agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.zh.md)记录;当前压缩、键和 page-size 选择由[持久化延迟与 page size 决策](../../../.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.zh.md)记录。
|
||||
|
||||
### 源码地图
|
||||
|
||||
@@ -110,7 +108,7 @@ await ctx.sessionPersistence.append(id, events)
|
||||
| [`src/store.ts`](src/store.ts) | 存储原语:事务追加、读取、修复、路径与所有权验证 |
|
||||
| [`src/schema.ts`](src/schema.ts) | schema 归属:版本门禁、连接加固、行解码 |
|
||||
| [`src/codec.ts`](src/codec.ts) | 打包:哪些 `assistant/chunk` 连续段成为打包行、大小上限 |
|
||||
| [`src/compression.ts`](src/compression.ts) | 物理编码:压缩阈值、序列列表、行扫描与解码 |
|
||||
| [`src/compression.ts`](src/compression.ts) | 物理编码:字典压缩、序列列表、行扫描与解码 |
|
||||
| [`src/sql.ts`](src/sql.ts) + [`resources/sql/`](resources/sql/) | 所有 SQL 语句均为打包的闭名资源 |
|
||||
| [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;打包只能通过数据库往返观察) |
|
||||
|
||||
@@ -124,7 +122,7 @@ await ctx.sessionPersistence.append(id, events)
|
||||
| `sessions` | 每个会话一行:头部字段加单调递增的 revision |
|
||||
| `events` | 物理事件行:一个逻辑事件,或一个打包连续段 |
|
||||
|
||||
确切的列定义见 [`resources/sql/schema.sql`](resources/sql/schema.sql)。`events.data` 列存放文本或 blob:小载荷保持为文本,较大的载荷在压缩后更小时以压缩形式存储。标量逻辑事件的 `events.is_packed` 为 `0`,打包分片连续段的该值为 `1`,因此类型与物理分片标签同名的标量事件仍然明确。打包行沿用其首个逻辑事件的 `seq`,因此在复合主键 `(session_id, seq)` 下,物理顺序就是逻辑顺序。
|
||||
确切的列定义见 [`resources/sql/schema.sql`](resources/sql/schema.sql)。`sessions.id` 是内部整数键,`sessions.session_key` 保留公开会话 id。`events.data` 存放文本或可独立解码的 Zstandard blob;仅在结果更小时才使用 schema 自有的共享字典压缩。`events.source_event_seqs` 使用带 tag 的 delta 或 run 编码。标量逻辑事件的 `events.is_packed` 为 `0`,打包分片连续段的该值为 `1`,因此类型与物理分片标签同名的标量事件仍然明确。打包行沿用其首个逻辑事件的 `seq`,因此在复合主键 `(session_id, seq)` 下,物理顺序就是逻辑顺序。
|
||||
|
||||
### 写入路径
|
||||
|
||||
@@ -147,6 +145,7 @@ await ctx.sessionPersistence.append(id, events)
|
||||
- [会话包映射](../README.zh.md)——相邻的持久化、投影、标题与遥测包。
|
||||
- [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-persistence-sqlite)——每个受支持配置字段及其源声明。
|
||||
- [SQLite 物理分片行决策](../../../.agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.zh.md)——打包布局背后的理由、备选方案与测量。
|
||||
- [持久化延迟与 page size 决策](../../../.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.zh.md)——501 会话基准与 schema-19 存储取舍。
|
||||
|
||||
-----
|
||||
|
||||
@@ -174,9 +173,9 @@ await ctx.sessionPersistence.append(id, events)
|
||||
|
||||
这些限制说明本提供方何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用 SQLite 对比或任务积压。
|
||||
|
||||
- **预发布设计,无迁移**——schema 18 是临时的 SQLite 专用设计;被推迟的统一多后端、可配置 schema 关系型设计已有可运行的外部原型 [morlay/session-persistence-rdb](https://github.com/morlay/session-persistence-rdb)(基于 Drizzle,支持 SQLite 与 PostgreSQL),预发布期间不保证 schema 稳定性或迁移支持。
|
||||
- **预发布设计,无迁移**——schema 19 是临时的 SQLite 专用设计;不保证 schema 稳定性或迁移支持。
|
||||
- **打包依赖批次边界**——被写后窗口或显式 flush 拆开的兼容连续段仍分属不同物理行;这避免了重写先前行,代价是打包比例依赖时序。
|
||||
- **同步 SQLite 与压缩**——Node 的 SQLite 驱动与 Zstandard 调用会阻塞 JavaScript 线程;4 KiB 压缩阈值限制了小记录的单帧工作量。
|
||||
- **同步 SQLite 与压缩**——Node 的 SQLite 驱动与 Zstandard 调用会阻塞 JavaScript 线程。
|
||||
- **忙等待阻塞事件循环**——SQLite 在同步调用内部等待;竞争写入方最长可让线程停顿配置的 `busyTimeoutMs`。
|
||||
- **外部 SQL 读取方必须解码物理行**——打包的 `events.type`(`text-chunks`、`reasoning-chunks`、`tool-call-chunks`)不是逻辑事件类型;受支持的消费方通过本提供方读取。
|
||||
- **没有删除或历史压缩**——普通追加仅插入,没有任何机制移除旧行。
|
||||
@@ -187,37 +186,6 @@ await ctx.sessionPersistence.append(id, events)
|
||||
<details>
|
||||
<summary>维护者的工作上下文——点击展开</summary>
|
||||
|
||||
本开发备注是维护者的工作上下文:测量产物、开放设计问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码和相关 Agent Note 为准,结论一旦稳定就迁移到对应归属。
|
||||
|
||||
#### 基准产物
|
||||
|
||||
以下数字是冻结的 schema 17 基准。Schema 18 改变了行判别值,尚未重新测量;[SQLite 物理分片行决策](../../../.agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.zh.md) 是权威记录,本表只是带注的摘要。
|
||||
|
||||
| 指标 | **JSONL(zstd)** | **SQLite(legacy)** | **SQLite(new)** |
|
||||
|---|---|---|---|
|
||||
| 磁盘占用 | **30.65 MB** | 709.57 MB | 75.01 MB |
|
||||
| 105 个会话的写入时间 | 28.21 s | 10.64 s | **8.58 s** |
|
||||
| 完整会话读取 p50 / p95 | 4.49 / 23.36 ms | 9.02 / 69.16 ms | **3.95 / 21.58 ms** |
|
||||
| 50 个事件尾部读取 p50 / p95 | 10.58 / 80.90 ms | **0.189 / 0.293 ms** | 0.253 / 0.378 ms |
|
||||
| 事件行数 | 2,507,860(逻辑) | 2,507,860 | **65,810** |
|
||||
| 全部会话 fork | 14.48 s | 19.30 s | **13.10 s** |
|
||||
|
||||
语料为 105 个会话、2,507,860 个逻辑事件,按 512 个事件一批追加,因此具体比例取决于会话内容、流密度与批次边界。`SQLite(legacy)` 是标量布局——每个逻辑事件一行、不打包——其 709.57 MB 的占用正是打包行的动机。在已测量的 schema 17 布局中,SQLite 磁盘占用约为 JSONL 的 2.5 倍,但写入快约 3.3 倍,完整会话读取在两个分位上都更快,50 个事件尾部读取快约 40 倍;相对标量布局,它缩小约 89%、写入更快,并把 2,507,860 行缩减到 65,810 行,只有标量尾部读取仍略快(0.189 对 0.253 ms p50)。写入路径或 schema 变化时,请重跑或扩展该基准。
|
||||
|
||||
#### 未来:多后端 RDB 持久化(Drizzle)
|
||||
|
||||
统一的多后端关系型设计仍被推迟。若以 Drizzle 实现,需要解决:schema 归属——逐版本冻结与精确对象校验的意义在于任何组合都能读取同版本数据库,可定制 schema 必须同样带版本并接受同样校验;后端加固——`synchronous=FULL`、busy timeout 与所有权检查都是 SQLite 专属,Postgres 或 MySQL 后端需要各自的持久性与权限方案;codec 可移植性——打包 codec 围绕 SQLite 列设计,无论跨方言共享 codec 还是按 schema 版本固定每后端 codec,都必须保持逻辑约定完全一致。
|
||||
|
||||
#### 未来:持久化到持久化的迁移与版本升级
|
||||
|
||||
README 记录了手动的 `load` → `create`/`append` 迁移,但 seam 没有导入/导出 API,SQLite 也直接拒绝其他 schema 版本。自动化迁移需要:能保留头部血缘(`seedLength`、`parentSession`、`agentPreset`)与 revision 语义的导出格式;格式与 schema 版本的升级链,即[事件词汇表显式拒绝笔记](../../../.agents/notes/implemented/simplification/2026-08-25-fail-closed-session-event-vocabulary.zh.md)中推迟的升级链;以及导出前源日志可读且平衡的保证——`load` 已先提交冷修复。
|
||||
|
||||
#### 未来:库内全文搜索与索引改进
|
||||
|
||||
兄弟包 [session-query-sqlite](../../session-query/session-query-sqlite/README.zh.md) 已经在独立派生索引数据库中维护一份会话内容的 SQLite FTS5 搜索索引。把 FTS 放进持久化数据库会重复该表面;开放问题包括索引归属、如何与追加保持事务一致,以及打包行应展开成索引文档还是索引直接读取逻辑流。持久化 schema 目前只索引 `(session_id, seq)`;额外索引(例如 `sessions.created_at` 用于冷数据截止扫描)实现简单,但会增加写入成本。
|
||||
|
||||
#### 未来:冷数据卸载到级联数据库文件
|
||||
|
||||
本提供方没有删除或后台压缩:所有数据永远留在同一个数据库里。一个方向是把冷会话(例如超过 30 天)卸载到按级联组织的独立归档数据库文件,并对冷文件使用更激进的压缩——对冷数据而言,更高的 Zstandard 级别代价很低。这需要:知道哪个文件存放哪个会话的路由规则、`list`/`readFrom`/`load` 的跨文件扇出、级联间一致的 revision 与存储标识,以及卸载是取代还是补充「无删除」限制的决定。
|
||||
501 会话语料包含私有会话数据,因此不提交到仓库。汇总方法、完整结果与未采用候选记录在[持久化延迟与 page size 决策](../../../.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.zh.md)中;schema 19 以打包资源及测试固定的字典摘要为准。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"resources/zstd-dictionary.bin",
|
||||
"resources/sql/**/*.sql",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
PRAGMA page_size = 65536;
|
||||
@@ -4,7 +4,8 @@ CREATE TABLE persistence_state (
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY,
|
||||
session_key TEXT NOT NULL UNIQUE,
|
||||
version INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
cwd TEXT,
|
||||
@@ -18,7 +19,7 @@ CREATE TABLE sessions (
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE events (
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
session_id INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
seq INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
SELECT id
|
||||
FROM sessions
|
||||
WHERE session_key = ?;
|
||||
@@ -1,4 +1,4 @@
|
||||
SELECT id, version, created_at, cwd, parent_session, seed_length, origin,
|
||||
SELECT session_key AS id, version, created_at, cwd, parent_session, seed_length, origin,
|
||||
delegation_depth, agent_preset, incarnation, revision
|
||||
FROM sessions
|
||||
WHERE id = ?;
|
||||
WHERE session_key = ?;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
SELECT id, version, created_at, cwd, parent_session, seed_length, origin,
|
||||
SELECT session_key AS id, version, created_at, cwd, parent_session, seed_length, origin,
|
||||
delegation_depth, agent_preset, incarnation, revision
|
||||
FROM sessions;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
PRAGMA user_version = 18;
|
||||
@@ -0,0 +1 @@
|
||||
PRAGMA user_version = 19;
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
UPDATE sessions
|
||||
SET revision = revision + 1
|
||||
WHERE id = ?;
|
||||
WHERE session_key = ?;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
INSERT INTO sessions
|
||||
(id, version, created_at, cwd, parent_session, seed_length, origin,
|
||||
(session_key, version, created_at, cwd, parent_session, seed_length, origin,
|
||||
delegation_depth, agent_preset, incarnation, revision)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
ON CONFLICT(session_key) DO UPDATE SET
|
||||
version = excluded.version,
|
||||
created_at = excluded.created_at,
|
||||
cwd = excluded.cwd,
|
||||
@@ -10,4 +10,5 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
seed_length = excluded.seed_length,
|
||||
origin = excluded.origin,
|
||||
delegation_depth = excluded.delegation_depth,
|
||||
agent_preset = excluded.agent_preset;
|
||||
agent_preset = excluded.agent_preset
|
||||
RETURNING id;
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
{"turn":1}
|
||||
{"policy":"never"}
|
||||
{"turn":1,"step":2}
|
||||
{"turn":1,"step":4}
|
||||
{"turn":1,"step":6}
|
||||
{"mode":"read-only"}
|
||||
{"mode":"workspace-write"}
|
||||
{"mode":"danger-full-access"}
|
||||
{"compactionId":"{{id:1}}","turn":1}
|
||||
{"policy":"never","source":"delegation"}
|
||||
{"turn":1,"reason":{"kind":"max-tokens"}}
|
||||
{"runId":"{{workflow:1}}","name":"snapshot-flow"}
|
||||
{"mode":"danger-full-access","source":"delegation"}
|
||||
{"retryId":"{{retry:1}}","turn":1,"step":1,"retry":1}
|
||||
{"runId":"{{workflow:1}}","name":"advanced-acp-snapshot"}
|
||||
{"todos":[{"content":"keep going","status":"in_progress"}]}
|
||||
{"runId":"{{workflow:1}}","name":"advanced-headless-snapshot"}
|
||||
{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}
|
||||
{"turn":1,"step":2,"index":1,"dt":[0,0],"texts":["B","OTH","_OK"]}
|
||||
{"todos":[{"content":"watch the kettle boil","status":"in_progress"}]}
|
||||
{"turn":1,"point":"Stop","dialect":"codex","handlerId":"codex:Stop:2"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"B"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}
|
||||
{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}
|
||||
{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}
|
||||
{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"al"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ETA"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ONG"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"DONE"}}
|
||||
{"turn":1,"step":5,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}
|
||||
{"turn":1,"step":5,"chunk":{"type":"text-delta","index":0,"text":"DONE."}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}
|
||||
{"turn":1,"step":3,"index":1,"dt":[0,0,1],"texts":["PAR","ENT","_D","ONE"]}
|
||||
{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"max-tokens"}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}
|
||||
{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}
|
||||
{"version":3,"mode":"one-shot","provider":"spawn","label":"Start depth one"}
|
||||
{"version":3,"mode":"one-shot","provider":"spawn","label":"Truncated child"}
|
||||
{"title":"Do NOT use the read","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"ROOT_DONE"}}
|
||||
{"title":"Use the bash tool to","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"Recovered."}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-start","index":1,"blockType":"text"}}
|
||||
{"turn":1,"step":5,"callId":"pty-list","name":"terminal_list","arguments":"{}"}
|
||||
{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}
|
||||
{"version":3,"mode":"one-shot","provider":"spawn","label":"Check direct child"}
|
||||
{"title":"Call the bash tool to","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Use the read tool (NOT","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Run true once with bash","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Use the write tool (NOT","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"point":"Stop","dialect":"claude-code","handlerId":"claude-code:Stop:2"}
|
||||
{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":128000}
|
||||
{"title":"You are one fresh worker","messageSeqs":[8],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DEPTH_REJECTED"}}
|
||||
{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}
|
||||
{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}
|
||||
{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}
|
||||
{"title":"Exercise the six PTY tools","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Reply with the single word","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"What is my favorite color?","messageSeqs":[9],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"PARENT_COMPLETED"}}
|
||||
{"title":"Call the run_code tool (NOT","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Use the subagent tool TWICE","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":2,"index":1,"dt":[0,0,0,0,0],"texts":["G","LOB","_S","AM","PL","ED"]}
|
||||
{"title":"A file named greeting.txt in","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Perform these exact steps in","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Use read_image on red.png in","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":0,"outputTokens":0}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}
|
||||
{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}
|
||||
{"title":"This prompt first receives an","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Use the subagent tool exactly","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":8}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":24,"outputTokens":6}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":4}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}
|
||||
{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}
|
||||
{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}
|
||||
{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}
|
||||
{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}
|
||||
{"title":"Run this advanced flow exactly","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Use the web_fetch tool exactly","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":30,"outputTokens":12}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"CHILD_EXIT_PRESERVED"}}
|
||||
{"turn":1,"step":6,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_HEADLESS_OK"}}
|
||||
{"title":"This prompt triggers a recorded","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Attempt one subagent call beyond","messageSeqs":[8],"source":{"kind":"fallback"}}
|
||||
{"title":"Observe the ACP diagnostic twice","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Using ONE run_code program: call","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"CORDIS_INSPECT_JSDOC_OK"}}
|
||||
{"title":"Call ask_user_question once to ask","messageSeqs":[8],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"text-delta","index":0,"text":"RUNNER_FAILURES_SURFACED"}}
|
||||
{"title":"Inspect the configured child model,","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The skill is loaded."}}
|
||||
{"title":"Delegate one foreground subagent. Its","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":1,"callId":"call_read_a","name":"read","arguments":"{\"file_path\":\"a.txt\"}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"BETA"}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"teal"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WIDE"}}}
|
||||
{"turn":1,"step":2,"index":1,"dt":[0,0,0,0,0,0],"texts":["CODE","_","ONE","+","CODE","_T","WO"]}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}
|
||||
{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}
|
||||
{"title":"Establish a durable compaction premise","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ALPHA"}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"FIRST"}}}
|
||||
{"title":"Delegate through two child generations.","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Load the editing-cordis-compositions ski","messageSeqs":[7],"source":{"kind":"fallback"}}
|
||||
{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Load the requested skill."}}
|
||||
{"turn":1,"step":4,"chunk":{"type":"text-delta","index":0,"text":"PARENT_OBSERVED_ACP_DIAGNOSTIC"}}
|
||||
{"turn":1,"point":"PostToolUse","dialect":"codex","handlerId":"codex:PostToolUse:1","matcher":"bash"}
|
||||
{"turn":1,"step":1,"callId":"fs-edit-read","name":"read","arguments":"{\"file_path\":\"config.txt\"}"}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Done."}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"partial one"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}}
|
||||
{"turn":1,"step":4,"callId":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"GLOB_SAMPLED"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WORKFLOW_DONE"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_ONE_DONE"}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}
|
||||
{"turn":1,"reason":{"kind":"error","error":{"message":"simulated provider error (HTTP 401)","code":"AUTH"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_COMPLETED"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CODE_ONE+CODE_TWO"}}}
|
||||
{"turn":1,"point":"PreToolUse","dialect":"claude-code","handlerId":"claude-code:PreToolUse:1","matcher":"bash"}
|
||||
{"turn":1,"step":3,"callId":"fs-delete-read-after","name":"read","arguments":"{\"file_path\":\"deleted.txt\"}"}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_EXIT_PRESERVED"}}}
|
||||
{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_HEADLESS_OK"}}}
|
||||
{"turn":1,"point":"PostToolUse","dialect":"claude-code","handlerId":"claude-code:PostToolUse:2","matcher":"bash"}
|
||||
{"turn":1,"step":4,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}
|
||||
{"turn":1,"step":1,"callId":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}
|
||||
{"turn":1,"point":"Stop","handlerId":"codex:Stop:2","decision":"pass","exitCode":0,"durationMs":2.7725000000000364}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RALPH SNAPSHOT COMPLETE"}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RUNNER_FAILURES_SURFACED"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The skill is loaded."}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CONFIGURED_EFFORT_REJECTED"}}}
|
||||
{"turn":1,"point":"Stop","handlerId":"claude-code:Stop:2","decision":"pass","exitCode":0,"durationMs":2.744416000000001}
|
||||
{"turn":1,"step":1,"callId":"read-image-reencode-call","name":"read_image","arguments":"{\"file_path\":\"gradient.png\"}"}
|
||||
{"turn":1,"step":3,"callId":"call_acp_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}
|
||||
{"turn":1,"step":2,"callId":"missing-runner-output","name":"job_output","arguments":"{\"job_id\":\"bash-1\",\"wait\":true}"}
|
||||
{"turn":1,"step":1,"callId":"call_00_hHPZCcivsIkXAGS9jTGy8417","name":"read","arguments":"{\"file_path\": \"greeting.txt\"}"}
|
||||
{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-list","name":"terminal_list","argumentsDelta":"{}"}}
|
||||
{"turn":1,"step":2,"callId":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}}}
|
||||
{"turn":1,"point":"PreToolUse","handlerId":"claude-code:PreToolUse:1","decision":"ask","exitCode":0,"durationMs":4.231499999999869}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1255,"outputTokens":99,"cacheReadTokens":0,"reasoningTokens":22}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2877,"outputTokens":90,"cacheReadTokens":0,"reasoningTokens":18}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2878,"outputTokens":89,"cacheReadTokens":0,"reasoningTokens":23}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2880,"outputTokens":83,"cacheReadTokens":0,"reasoningTokens":17}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2891,"outputTokens":41,"cacheReadTokens":0,"reasoningTokens":38}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2892,"outputTokens":22,"cacheReadTokens":0,"reasoningTokens":19}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3256,"outputTokens":94,"cacheReadTokens":0,"reasoningTokens":26}}}
|
||||
{"turn":1,"step":2,"callId":"fs-overwrite-write","name":"write","arguments":"{\"file_path\":\"data.txt\",\"content\":\"replaced\"}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":17,"outputTokens":23,"cacheReadTokens":3072,"reasoningTokens":18}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2907,"outputTokens":142,"cacheReadTokens":0,"reasoningTokens":67}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3132,"outputTokens":115,"cacheReadTokens":0,"reasoningTokens":36}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3263,"outputTokens":103,"cacheReadTokens":0,"reasoningTokens":35}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":48,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":18}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":5405,"outputTokens":103,"cacheReadTokens":0,"reasoningTokens":44}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":59,"outputTokens":89,"cacheReadTokens":3328,"reasoningTokens":21}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":36,"outputTokens":28,"cacheReadTokens":3456,"reasoningTokens":14}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10400,"outputTokens":130,"cacheReadTokens":0,"reasoningTokens":34}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":117,"outputTokens":50,"cacheReadTokens":6272,"reasoningTokens":42}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":133,"outputTokens":96,"cacheReadTokens":2944,"reasoningTokens":23}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":167,"outputTokens":52,"cacheReadTokens":2816,"reasoningTokens":21}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":170,"outputTokens":28,"cacheReadTokens":2816,"reasoningTokens":25}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":188,"outputTokens":41,"cacheReadTokens":2816,"reasoningTokens":27}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":188,"outputTokens":51,"cacheReadTokens":2816,"reasoningTokens":30}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":214,"outputTokens":20,"cacheReadTokens":2816,"reasoningTokens":17}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":292,"outputTokens":30,"cacheReadTokens":2816,"reasoningTokens":27}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":50,"outputTokens":35,"cacheReadTokens":10496,"reasoningTokens":31}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":115,"outputTokens":35,"cacheReadTokens":3072,"reasoningTokens":30}}}
|
||||
{"runId":"{{workflow:1}}","seq":1,"label":"Reply with exactly the word WF_CHILD_OK and not…","phase":"Run","childId":"{{session:2}}"}
|
||||
{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":281,"outputTokens":119,"cacheReadTokens":3200,"reasoningTokens":40}}}
|
||||
{"turn":1,"point":"UserPromptSubmit","handlerId":"codex:UserPromptSubmit:1","decision":"pass","exitCode":0,"durationMs":4.196374999999989}
|
||||
{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}
|
||||
{"turn":1,"step":2,"callId":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}
|
||||
{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"series"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_read_a","name":"read","argumentsDelta":"{\"file_path\":\"a.txt\"}"}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The final tool result verbatim:\n\n```\nHELLO\n```"}}}
|
||||
{"turn":1,"point":"UserPromptSubmit","handlerId":"claude-code:UserPromptSubmit:1","decision":"pass","exitCode":0,"durationMs":4.07300000000032}
|
||||
{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-list","name":"terminal_list","arguments":"{}"}}}
|
||||
{"turn":1,"step":1,"callId":"partial-landlock-call","name":"bash","arguments":"{\"command\":\"false\",\"description\":\"Exit with status one\"}"}
|
||||
{"content":[{"type":"text","text":"This prompt triggers a recorded provider error."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"turn":1,"step":3,"callId":"bounded-task-kill","name":"job_kill","arguments":"{\"job_id\":\"bash-1\",\"reason\":\"free the bounded task slot\"}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"fs-edit-read","name":"read","argumentsDelta":"{\"file_path\":\"config.txt\"}"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"fs-bounded-read","name":"read","argumentsDelta":"{\"file_path\":\"data.txt\"}"}}
|
||||
{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-kill","name":"terminal_close","argumentsDelta":"{\"sessionId\":\"pty-1\"}"}}
|
||||
{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:8}}"}
|
||||
{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:10}}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"fs-overwrite-read","name":"read","argumentsDelta":"{\"file_path\":\"data.txt\"}"}}
|
||||
{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:14}}"}
|
||||
{"turn":1,"step":1,"callId":"call_00_APMUCJJm9lrTSlVbg6dB0185","name":"write","arguments":"{\"file_path\": \"notes.txt\", \"content\": \"hello world\"}"}
|
||||
{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:17}}"}
|
||||
{"turn":1,"step":1,"callId":"call_00_6k0oGSliVHxGSgqBmMEO4311","name":"bash","arguments":"{\"command\": \"echo HELLO\", \"description\": \"Echo HELLO\"}"}
|
||||
{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_session_query_spill","name":"session_event_read","argumentsDelta":"{\"seq\":10}"}}
|
||||
{"turn":1,"step":3,"callId":"call_4","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"watch the kettle boil\", \"status\": \"in_progress\"}]}"}
|
||||
{"turn":1,"step":2,"callId":"fs-edit-replace","name":"edit","arguments":"{\"file_path\":\"config.txt\",\"old_string\":\"DEBUG\",\"new_string\":\"RELEASE\"}"}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The second attempt succeeded. The final result is \"HELLO\"."}}}
|
||||
{"rootCallId":"code-image-call","parentCallId":"code-image-call","subCallId":"code-image-call:code:2","name":"read_image","arguments":{"file_path":"red.png"}}
|
||||
{"turn":1,"step":1,"callId":"call_00_JliP571Bh0QQ8QExbSPk0080","name":"bash","arguments":"{\"command\": \"echo HELLO\", \"description\": \"Run echo HELLO\"}"}
|
||||
{"turn":1,"step":1,"callId":"call_00_tv0SMeLXaTuyuVrOxnV97085","name":"bash","arguments":"{\"command\": \"echo HELLO\", \"description\": \"Run echo HELLO\"}"}
|
||||
{"turn":1,"step":2,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," tool"," result"," I"," received"," is",":\n\n","```\n","HE","LL","O","\n","```"]}
|
||||
{"turn":1,"step":1,"callId":"missing-runner-foreground","name":"bash","arguments":"{\"command\":\"true\",\"description\":\"Exercise missing sandbox runner\"}"}
|
||||
{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-undefine","name":"cordis_undefine","argumentsDelta":"{\"pluginId\":\"snap-1\"}"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_read_a","name":"read","arguments":"{\"file_path\":\"a.txt\"}"}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_workspace_read","name":"read","argumentsDelta":"{\"file_path\":\"nested/task.txt\"}"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-spawn","name":"terminal_open","argumentsDelta":"{\"type\":\"shell\",\"name\":\"main\"}"}}
|
||||
{"turn":1,"step":1,"callId":"call_00_1rmSWHhVchVg7PDTmegT0421","name":"bash","arguments":"{\"command\": \"echo HELLO\", \"description\": \"Echo HELLO to stdout\"}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with the single word \"FIRST\" and stop."}}}
|
||||
{"turn":1,"step":2,"callId":"call_00_tDV4j1p5eAeHTtQhXOfn6856","name":"bash","arguments":"{\"command\": \"echo HELLO\", \"description\": \"Echo HELLO to stdout\"}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"fs-edit-read","name":"read","arguments":"{\"file_path\":\"config.txt\"}"}}}
|
||||
{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}}}
|
||||
{"turn":1,"step":1,"callId":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}
|
||||
{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"call_acp_output","name":"job_output","argumentsDelta":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"fs-overwrite-read","name":"read","arguments":"{\"file_path\":\"data.txt\"}"}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly \"WF_CHILD_OK\" and nothing else."}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"missing-runner-output","name":"job_output","argumentsDelta":"{\"job_id\":\"bash-1\",\"wait\":true}"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run a specific bash command and then reply with DONE."}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"call_claude_output","name":"job_output","argumentsDelta":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}
|
||||
{"rootCallId":"call_workspace_read","parentCallId":"call_workspace_read","subCallId":"call_workspace_read:code:1","name":"read","arguments":{"file_path":"nested/task.txt"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"BETA\" and nothing else."}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The tool output was rejected by codex policy. Let me quote what I got back."}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"ALPHA\" and nothing else."}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The replacement was successful. I'll reply with just \"DONE\" as instructed."}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_session_query_spill","name":"session_event_read","arguments":"{\"seq\":10}"}}}
|
||||
{"turn":1,"step":3,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," second"," attempt"," succeeded","."," The"," final"," result"," is"," \"","HE","LL","O","\"."]}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run a simple bash command and report the result verbatim."}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"fs-delete-read-after","name":"read","arguments":"{\"file_path\":\"deleted.txt\"}"}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"fs-delete-read-before","name":"read","arguments":"{\"file_path\":\"deleted.txt\"}"}}}
|
||||
{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\":\"snap-1\"}"}}}
|
||||
{"turn":1,"step":1,"callId":"call_00_x0zlnXl5JOxLrAYL9y7P0119","name":"edit","arguments":"{\"file_path\": \"settings.txt\", \"old_string\": \"blue\", \"new_string\": \"green\"}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"read-image-dimension","name":"read_image","arguments":"{\"file_path\":\"wide.png\"}"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The first call was rejected by policy. The user said to retry once. Let me retry."}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"fs-overwrite-write","name":"write","argumentsDelta":"{\"file_path\":\"data.txt\",\"content\":\"replaced\"}"}}
|
||||
{"content":[{"type":"text","text":"Load the editing-cordis-compositions skill with the skill tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"content":[{"type":"text","text":"Also reply with the single word SECOND, then stop."}],"source":{"kind":"plugin","plugin":"hooks-claude-code"},"role":"user","id":"{{message:4}}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user's favorite color is teal, as stated in the context provided by the plugin."}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-signal","name":"terminal_signal","argumentsDelta":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"editing-cordis-compositions\"}"}}}
|
||||
{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"}}
|
||||
{"content":[{"type":"text","text":"The user has previously stated their favorite color is teal."}],"source":{"kind":"plugin","plugin":"hooks-codex"},"role":"user","id":"{{message:3}}"}
|
||||
{"content":[{"type":"text","text":"Note: command output has been verified against the audit log."}],"source":{"kind":"plugin","plugin":"hooks-codex"},"role":"user","id":"{{message:5}}"}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"First subagent returned \"ALPHA\". Now I'll call the second subagent to return \"BETA\"."}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run `echo HELLO` using the bash tool and report the result verbatim."}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_acp_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_workspace_delimiter_read","name":"read","argumentsDelta":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"missing-runner-output","name":"job_output","arguments":"{\"job_id\":\"bash-1\",\"wait\":true}"}}}
|
||||
{"turn":1,"step":1,"callId":"call_00_e0MSVSocL0o4UWjOdG4c2072","name":"pwsh","arguments":"{\"command\": \"[Console]::Out.Write('PWSH_OK')\", \"description\": \"Write PWSH_OK to console\"}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_session_root","name":"write","argumentsDelta":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}}
|
||||
{"content":[{"type":"text","text":"The user has previously stated their favorite color is teal."}],"source":{"kind":"plugin","plugin":"hooks-claude-code"},"role":"user","id":"{{message:3}}"}
|
||||
{"content":[{"type":"text","text":"Note: command output has been verified against the audit log."}],"source":{"kind":"plugin","plugin":"hooks-claude-code"},"role":"user","id":"{{message:5}}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run a PowerShell command and then reply with \"DONE\". Let me execute it."}}}
|
||||
{"content":[{"type":"text","text":"Use the read tool twice in the same assistant message: read a.txt and b.txt. Then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"turn":1,"step":1,"callId":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","arguments":"{\"command\": \"echo TERMINAL_OK\", \"description\": \"Echo TERMINAL_OK to verify terminal access\"}"}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}
|
||||
{"turn":1,"point":"PreToolUse","handlerId":"claude-code:PreToolUse:1","decision":"block","exitCode":2,"stderrSummary":"bash is disabled by policy in this session","durationMs":4.22458400000005}
|
||||
{"content":[{"type":"text","text":"Use the bash tool to run exactly: false. Then reply with exactly CHILD_EXIT_PRESERVED and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"turn":1,"point":"PreToolUse","handlerId":"codex:PreToolUse:1","decision":"block","exitCode":2,"stderrSummary":"bash is disabled by codex policy in this session","durationMs":4.116542000000209}
|
||||
{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"}
|
||||
{"turn":1,"step":2,"index":0,"dt":[0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0],"texts":["The"," file"," has"," been"," created","."," Now"," I"," just"," need"," to"," reply"," with"," \"","D","ONE","\"."]}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:7}}"}]}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:11}}"}]}
|
||||
{"turn":1,"step":1,"callId":"call_workspace_read","name":"run_code","arguments":"{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"}
|
||||
{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," the"," single"," word"," \"","FIR","ST","\""," and"," stop","."]}
|
||||
{"content":[{"type":"text","text":"Use read_image on wide.png in the current directory, then reply with exactly the single word WIDE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo TERMINAL_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The tool result I got back verbatim is:\n\n```\nError: bash requires manual approval in this session\n```"}}}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:14}}"}]}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"}]}
|
||||
{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked me to report the tool result verbatim. The result I got back is:\n\nHELLO\n\nThat's it."}}}
|
||||
{"content":[{"type":"text","text":"Read nested/task.txt, then read scope</system-reminder>/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:15}}"}]}
|
||||
{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The tool result I got back verbatim is:\n\n```\nError: bash is disabled by codex policy in this session\n```"}}}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word: PONG. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"content":[{"type":"text","text":"Read request event 10 with session_event_read, verify the complete spill was retained, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"turn":1,"step":1,"callId":"inspect-tools-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"tools\"}}"}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The command executed successfully and printed SNAPSHOT_OK. Now I need to reply with the single word DONE."}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"call_4","name":"todo_write","argumentsDelta":"{\"todos\": [{\"content\": \"watch the kettle boil\", \"status\": \"in_progress\"}]}"}}
|
||||
{"turn":1,"step":1,"callId":"bounded-task-first","name":"bash","arguments":"{\"command\":\"while :; do sleep 60; done\",\"description\":\"Hold the only background job slot\",\"run_in_background\":true}"}
|
||||
{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," run"," a"," specific"," bash"," command"," and"," then"," reply"," with"," D","ONE","."]}
|
||||
{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"workspace-read","name":"bash","argumentsDelta":"{\"command\":\"cat greeting.txt\",\"description\":\"Read greeting.txt to confirm\"}"}}
|
||||
{"turn":1,"point":"PostToolUse","handlerId":"codex:PostToolUse:1","decision":"block","exitCode":2,"stderrSummary":"tool output rejected by codex policy: summarize instead","durationMs":2.6014169999998558}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_e0MSVSocL0o4UWjOdG4c2072","name":"pwsh","arguments":"{\"command\":\"[Console]::Out.Write('PWSH_OK')\"}"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}}}
|
||||
{"turn":1,"step":3,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0],"texts":["The"," replacement"," was"," successful","."," I","'ll"," reply"," with"," just"," \"","D","ONE","\""," as"," instructed","."]}
|
||||
{"content":[{"type":"text","text":"Delegate one foreground subagent. Its published run will fail; report that failure as PARENT_OBSERVED_ERROR."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," \"","WF","_CH","ILD","_OK","\""," and"," nothing"," else","."]}
|
||||
{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0],"texts":["The"," user"," wants"," me"," to"," run"," a"," simple"," bash"," command"," and"," report"," the"," result"," verb","atim","."]}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The bash tool returned an error saying it requires manual approval in this session. I'll report this verbatim."}}}
|
||||
{"turn":1,"step":2,"index":0,"dt":[0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," the"," single"," word"," \"","SEC","OND","\""," and"," then"," stop","."]}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"partial-landlock-call","name":"bash","arguments":"{\"command\":\"false\",\"description\":\"Exit with status one\"}"}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The read tool returned lines 5 through 8 as expected. Now I need to reply with exactly the single word \"DONE\"."}}}
|
||||
{"turn":1,"step":2,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0],"texts":["The"," tool"," output"," was"," rejected"," by"," code","x"," policy","."," Let"," me"," quote"," what"," I"," got"," back","."]}
|
||||
{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0],"texts":["The"," user"," wants"," me"," to"," run"," a"," simple"," bash"," command"," and"," then"," reply"," with"," \"","D","ONE","\"."]}
|
||||
{"content":[{"type":"text","text":"Earlier context was compacted for this snapshot."}],"source":{"kind":"plugin","plugin":"compact","compactionId":"workspace-context-fixture"},"role":"user","id":"{{message:5}}"}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with the single word \"PARENT_DONE\" and stop."}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Both subagents have returned: first with \"ALPHA\", second with \"BETA\". Now I should reply with \"PARENT_DONE\"."}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to call glob exactly once with pattern * and path tree, then reply with exactly \"GLOB_SAMPLED\"."}}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_child_2","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"keep going\", \"status\": \"completed\"}]}"}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_compaction_marker","name":"bash","argumentsDelta":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}}
|
||||
{"turn":1,"step":2,"callId":"call_acp_background","name":"subagent_acp","arguments":"{\"description\":\"Observe ACP background failure\",\"prompt\":\"Return the scripted ACP failure.\",\"run_in_background\":true}"}
|
||||
{"content":[{"type":"text","text":"Use the lsp tool exactly once to find the definition at subject.ts line 1 character 7, then reply with exactly DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_child_1","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"keep going\", \"status\": \"in_progress\"}]}"}}}
|
||||
{"turn":1,"step":1,"index":0,"dt":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","AL","P","HA","\""," and"," nothing"," else","."]}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The output is \"BOTH_OK\" (with a trailing newline, but that's fine). The user asked me to reply with that output only."}}}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the bash tool to print a large deterministic output, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"turn":1,"step":2,"callId":"fs-bounded-write","name":"write","arguments":"{\"file_path\":\"data.txt\",\"content\":\"The replacement line is deliberately longer than the configured sixty-four byte diff-basis bound.\"}"}
|
||||
{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Also reply with the single word SECOND, then stop."}],"source":{"kind":"plugin","plugin":"hooks-codex"},"role":"user","id":"{{message:4}}"}]}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_1","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"watch the kettle boil\", \"status\": \"in_progress\"}]}"}}}
|
||||
{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_4","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"watch the kettle boil\", \"status\": \"in_progress\"}]}"}}}
|
||||
{"turn":1,"step":2,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"workspace-read","name":"bash","arguments":"{\"command\":\"cat greeting.txt\",\"description\":\"Read greeting.txt to confirm\"}"}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_HbCMzTslWBZTSphWN0z97382","name":"bash","arguments":"{\"command\": \"echo HELLO\", \"description\": \"Run echo HELLO\"}"}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_Q6wHtakaip2QNfIXaVJY5458","name":"bash","arguments":"{\"command\": \"echo HELLO\", \"description\": \"Run echo HELLO\"}"}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"workspace-append","name":"bash","argumentsDelta":"{\"command\":\"printf 'WORLD\\n' >> greeting.txt\",\"description\":\"Append WORLD to greeting.txt\"}"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"missing-runner-foreground","name":"bash","arguments":"{\"command\":\"true\",\"description\":\"Exercise missing sandbox runner\"}"}}}
|
||||
{"turn":1,"step":1,"callId":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\":false}"}
|
||||
{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","P","ONG","\""," and"," not"," use"," any"," tools","."]}
|
||||
{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","P","ONG","\""," and"," not"," use"," any"," tools","."]}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"},"role":"user","id":"{{message:11}}"}]}
|
||||
{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Also reply with the single word SECOND, then stop."}],"source":{"kind":"plugin","plugin":"hooks-claude-code"},"role":"user","id":"{{message:4}}"}]}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to read the file greeting.txt using the read tool (not bash), then reply with exactly the single word \"DONE\"."}}}
|
||||
{"turn":1,"step":1,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0],"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","args":["","{","\"","pattern","\"",": ","\"","*","\"",", ","\"","path","\"",": ","\"","tree","\"","}"]}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to create a file named notes.txt with the content \"hello world\" using the write tool, then reply with \"DONE\"."}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_VAByyMjsct4c7P6k1ysX9256","name":"bash","arguments":"{\"command\": \"echo HELLO\", \"description\": \"Echo HELLO to stdout\"}"}}}
|
||||
{"turn":1,"step":4,"callId":"call_codex_foreground","name":"subagent_codex","arguments":"{\"description\":\"Observe Codex foreground diagnostic\",\"prompt\":\"Return the Codex diagnostic failure.\",\"run_in_background\":false}"}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"What is my favorite color? Reply with just the color and stop. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"turn":1,"step":2,"callId":"call_claude_background","name":"subagent_codex","arguments":"{\"description\":\"Observe Claude background diagnostic\",\"prompt\":\"Return the Claude diagnostic failure.\",\"run_in_background\":true}"}
|
||||
{"turn":1,"step":2,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"texts":["The"," tool"," result"," was",":\n\n","```\n","HE","LL","O","\n","```\n\n","It"," completed"," successfully"," with"," exit"," code"," ","0","."]}
|
||||
{"turn":1,"step":1,"callId":"call_claude_foreground","name":"subagent_codex","arguments":"{\"description\":\"Observe Claude foreground diagnostic\",\"prompt\":\"Return the Claude diagnostic failure.\",\"run_in_background\":false}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to call the run_code tool with a TypeScript program that runs `echo BOTH_OK` via `tools.bash` and returns its output."}}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_lsp_definition","name":"lsp","arguments":"{\"operation\":\"goToDefinition\",\"file_path\":\"subject.ts\",\"line\":1,\"character\":7}"}}}
|
||||
{"turn":1,"step":2,"callId":"call_00_FudNKuJ0fchSptGy3Scw1411","name":"subagent","arguments":"{\"description\": \"Return BETA only\", \"prompt\": \"Reply with exactly the word BETA and nothing else.\", \"run_in_background\": false}"}
|
||||
{"turn":1,"step":1,"callId":"call_00_7zDCCjdsQgrk5LR2bAEQ1010","name":"subagent","arguments":"{\"description\": \"Return ALPHA only\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\", \"run_in_background\": false}"}
|
||||
{"rootCallId":"call_00_Era4M5eh79bvNOIey5q90401","parentCallId":"call_00_Era4M5eh79bvNOIey5q90401","subCallId":"call_00_Era4M5eh79bvNOIey5q90401:code:1","name":"bash","arguments":{"command":"echo BOTH_OK","description":"Print BOTH_OK"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run the bash tool with the command \"echo HELLO\". If it's rejected, retry once. Then quote the final result verbatim."}}}
|
||||
{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Note: command output has been verified against the audit log."}],"source":{"kind":"plugin","plugin":"hooks-claude-code"},"role":"user","id":"{{message:5}}"}]}
|
||||
{"content":[{"type":"text","text":"Use the web_fetch tool exactly once to fetch http://public.test:43117/menu.html, then reply with exactly DONE. Do not describe the content."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The tool returned:\n\n> Error: bash is disabled by policy in this session\n\nI cannot run the command because the bash tool is disabled by policy."}}}
|
||||
{"content":[{"type":"text","text":"Call the bash tool exactly once to run: echo HELLO. Whatever tool result comes back, quote it verbatim and stop without calling another tool."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"workspace-append","name":"bash","arguments":"{\"command\":\"printf 'WORLD\\n' >> greeting.txt\",\"description\":\"Append WORLD to greeting.txt\"}"}}}
|
||||
{"turn":1,"step":2,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"texts":["The"," bash"," tool"," is"," disabled"," by"," policy","."," I"," need"," to"," report"," this"," error"," verb","atim"," back"," to"," the"," user","."]}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"}]}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Exercise the six PTY tools in order, including one missing-session signal error, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_workspace_read","name":"run_code","argumentsDelta":"{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_GVknJu2tksKkP4lALCwh0926","name":"edit","arguments":"{\"file_path\": \"settings.txt\", \"old_string\": \"blue\", \"new_string\": \"green\"}"}}}
|
||||
{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":1}}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use read_image on wide.png in the current directory, then reply with exactly the single word WIDE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo TERMINAL_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":2}}
|
||||
{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"bounded-task-side-effect-check","name":"bash","argumentsDelta":"{\"command\":\"test ! -e second-task-ran.txt\",\"description\":\"Verify the rejected producer did not run\"}"}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_depth_three_rejected","name":"subagent","argumentsDelta":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\",\"run_in_background\":false}"}}
|
||||
{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Write the todo list 'watch the kettle boil' five times in a row without changing it, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"turn":1,"step":2,"callId":"bounded-task-second","name":"bash","arguments":"{\"command\":\"printf SHOULD_NOT_RUN > second-task-ran.txt; while :; do sleep 60; done\",\"description\":\"Attempt a second background job\",\"run_in_background\":true}"}
|
||||
{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"ROOT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2}}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Read request event 10 with session_event_read, verify the complete spill was retained, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"inspect-tools-api","name":"cordis_inspect_query","argumentsDelta":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"tools\"}}"}}
|
||||
{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," run"," `","echo"," HE","LL","O","`"," using"," the"," bash"," tool"," and"," report"," the"," result"," verb","atim","."]}
|
||||
{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"UNAVAILABLE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bounded-task-first","name":"bash","argumentsDelta":"{\"command\":\"while :; do sleep 60; done\",\"description\":\"Hold the only background job slot\",\"run_in_background\":true}"}}
|
||||
{"turn":1,"step":2,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," command"," ran"," successfully"," and"," output"," \"","TER","MIN","AL","_OK","\"."," I"," should"," now"," reply"," with"," just"," \"","D","ONE","\"."]}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use read_image to look at red.png in the current directory, then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"turn":1,"step":1,"index":0,"dt":[0,0,0,1,0,0,0,0,0,17,0,0,0,0,0,0,0,1,290,0,1],"texts":["The"," user"," wants"," me"," to"," run"," a"," PowerShell"," command"," and"," then"," reply"," with"," \"","D","ONE","\"."," Let"," me"," execute"," it","."]}
|
||||
{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"workspace-append"},"content":[{"type":"tool-result","toolCallId":"workspace-append","content":[{"type":"text","text":"(no output)"}],"isError":false}],"role":"user","id":"{{message:4}}"}}
|
||||
{"content":[{"type":"text","text":"Delegate one question check. Ask the child to call ask_user_question once about the CUDA fallback and return any unresolved question in its final result."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_root_child"},"content":[{"type":"tool-result","toolCallId":"call_root_child","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"isError":false}],"role":"user","id":"{{message:4}}"}}
|
||||
{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3}}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Delegate one foreground subagent. Its published run will fail; report that failure as PARENT_OBSERVED_ERROR."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the edit tool to replace \"blue\" with \"green\" in settings.txt without reading the file first, and then reply with just \"DONE\"."}}}
|
||||
{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:14}}"},"usage":{"inputTokens":3,"outputTokens":3}}
|
||||
{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"code-image-call"},"content":[{"type":"tool-result","toolCallId":"code-image-call","content":[{"type":"text","text":"{{cwd}}/red.png"}],"isError":false}],"role":"user","id":"{{message:4}}"}}
|
||||
{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_REJECTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:15}}"},"usage":{"inputTokens":10,"outputTokens":2}}
|
||||
{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_COMPLETED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2}}
|
||||
{"content":[{"type":"text","text":"Call the bash tool to run exactly: echo HELLO. If the first tool result is rejected, retry that command once. Quote the final tool result verbatim and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}
|
||||
{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:17}}"},"usage":{"inputTokens":3,"outputTokens":3}}
|
||||
{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parallel_alpha_1"},"content":[{"type":"tool-result","toolCallId":"call_parallel_alpha_1","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"{{message:4}}"}}
|
||||
{"turn":1,"step":1,"callId":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\",\"run_in_background\":false}"}
|
||||
{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_EXIT_PRESERVED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":1,"outputTokens":1}}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the pwsh tool to run exactly: [Console]::Out.Write('PWSH_OK'). Then reply with the single word DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"COMPACTION RECOVERED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":20,"outputTokens":4}}
|
||||
{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_compaction_marker"},"content":[{"type":"tool-result","toolCallId":"call_compaction_marker","content":[{"type":"text","text":"alpha\n"}],"isError":false}],"role":"user","id":"{{message:4}}"}}
|
||||
{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_ERROR"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_read","name":"run_code","arguments":"{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"}}}
|
||||
{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"CORDIS_INSPECT_JSDOC_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3}}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_acp_foreground","name":"subagent_acp","argumentsDelta":"{\"description\":\"Observe ACP foreground failure\",\"prompt\":\"Return the scripted ACP failure.\",\"run_in_background\":false}"}}
|
||||
{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"RUNNER_FAILURES_SURFACED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":1,"outputTokens":1}}
|
||||
{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_depth_one_child"},"content":[{"type":"tool-result","toolCallId":"call_depth_one_child","content":[{"type":"text","text":"DEPTH_REJECTED"}],"isError":false}],"role":"user","id":"{{message:9}}"}}
|
||||
{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bounded-task-side-effect-check","name":"bash","arguments":"{\"command\":\"test ! -e second-task-ran.txt\",\"description\":\"Verify the rejected producer did not run\"}"}}}
|
||||
{"turn":1,"step":1,"index":1,"dt":[0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_APMUCJJm9lrTSlVbg6dB0185","name":"write","args":["","{","\"","file","_path","\"",": ","\"","notes",".txt","\"",", ","\"","content","\"",": ","\"","hello"," world","\"","}"]}
|
||||
{"turn":1,"step":2,"index":0,"dt":[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," bash"," tool"," returned"," an"," error"," saying"," it"," requires"," manual"," approval"," in"," this"," session","."," I","'ll"," report"," this"," verb","atim","."]}
|
||||
{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_DIAGNOSTICS"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"{{message:17}}"},"usage":{"inputTokens":10,"outputTokens":2}}
|
||||
{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"fs-bounded-write","name":"write","argumentsDelta":"{\"file_path\":\"data.txt\",\"content\":\"The replacement line is deliberately longer than the configured sixty-four byte diff-basis bound.\"}"}}
|
||||
{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{message:8}}"}}
|
||||
{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Delegate through two child generations. The depth-two child must attempt one more subagent call and report the rejection."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}
|
||||
{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_child_question","name":"ask_user_question","argumentsDelta":"{\"questions\":[{\"id\":\"cuda-fallback\",\"header\":\"Deployment\",\"question\":\"Should deployment use the CUDA fallback?\"}]}"}}
|
||||
{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_ACP_DIAGNOSTIC"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":2}}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Schema-18 physical chunk-row codec. This package owns the durable tags,
|
||||
* Schema-19 physical chunk-row codec. This package owns the durable tags,
|
||||
* validation, and row-size limits independently from other persistence formats.
|
||||
* @module @deepseek-ai/dsh-session-persistence-sqlite/codec
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/* jscpd:ignore-start -- schema 18 deliberately owns a frozen physical codec;
|
||||
/* jscpd:ignore-start -- schema 19 deliberately owns a frozen physical codec;
|
||||
* importing or sharing the JSONL codec would let that format mutate this database interpreter. */
|
||||
type DeltaKind = 'text-delta' | 'reasoning-delta' | 'tool-call-delta'
|
||||
type DeltaEvent = SessionEvent<'assistant/chunk'>
|
||||
@@ -29,13 +29,13 @@ interface ToolCallRunData extends RunDataBase {
|
||||
readonly args: string[]
|
||||
}
|
||||
|
||||
/** One schema-18 packed physical record. */
|
||||
/** One schema-19 packed physical record. */
|
||||
export type ChunkRow =
|
||||
| { readonly type: 'text-chunks'; readonly seq0: number; readonly time0: number; readonly data: TextRunData }
|
||||
| { readonly type: 'reasoning-chunks'; readonly seq0: number; readonly time0: number; readonly data: TextRunData }
|
||||
| { readonly type: 'tool-call-chunks'; readonly seq0: number; readonly time0: number; readonly data: ToolCallRunData }
|
||||
|
||||
/** One scalar event or schema-18 packed physical record. */
|
||||
/** One scalar event or schema-19 packed physical record. */
|
||||
export type StorageRecord = SessionEvent | ChunkRow
|
||||
|
||||
/** Minimum eligible members in a packed physical record. */
|
||||
@@ -174,7 +174,7 @@ function emitBoundedRun(out: StorageRecord[], kind: DeltaKind, completeRun: read
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack eligible logical chunk runs into bounded schema-18 records.
|
||||
* Pack eligible logical chunk runs into bounded schema-19 records.
|
||||
* @param events - logical events in sequence order.
|
||||
* @returns scalar and packed physical records in equivalent order.
|
||||
*/
|
||||
@@ -308,7 +308,7 @@ function expandRow(row: ChunkRow): SessionEvent[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode one scalar or packed schema-18 record.
|
||||
* Decode one scalar or packed schema-19 record.
|
||||
* @param value - parsed physical-record value.
|
||||
* @returns the represented logical events.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* @module @deepseek-ai/dsh-session-persistence-sqlite/compression
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { TextDecoder } from 'node:util'
|
||||
import { constants, zstdCompressSync, zstdDecompressSync } from 'node:zlib'
|
||||
import type { SessionEvent, SurfaceEventType } from '@deepseek-ai/dsh-session'
|
||||
@@ -27,13 +28,24 @@ export interface BoundRecord {
|
||||
readonly isPacked: 0 | 1
|
||||
}
|
||||
|
||||
/** Small values stay as SQLite text to avoid per-frame CPU and byte overhead. */
|
||||
export const ZSTD_DATA_THRESHOLD_BYTES = 4_096
|
||||
|
||||
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER)
|
||||
const MAX_ZIGZAG_INTEGER = MAX_SAFE_INTEGER * 2n
|
||||
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true })
|
||||
const ZSTD_COMPRESSION_LEVEL = 3
|
||||
const DELTA_TAG = 0
|
||||
const RUN_TAG = 1
|
||||
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER)
|
||||
const MAX_ZIGZAG_INTEGER = MAX_SAFE_INTEGER * 2n
|
||||
/**
|
||||
* Schema-19 raw-content zstd dictionary for independently decodable data rows.
|
||||
* Its exact bytes are part of the physical format; changing the resource
|
||||
* requires a schema-version bump.
|
||||
*/
|
||||
const ZSTD_DICTIONARY = readFileSync(new URL('../resources/zstd-dictionary.bin', import.meta.url))
|
||||
|
||||
/** Compress options shared by every data-column frame. */
|
||||
const DATA_ZSTD_OPTIONS = {
|
||||
dictionary: ZSTD_DICTIONARY,
|
||||
params: { [constants.ZSTD_c_compressionLevel]: ZSTD_COMPRESSION_LEVEL },
|
||||
} as const
|
||||
const CHUNK_TAGS = ['text-chunks', 'reasoning-chunks', 'tool-call-chunks'] as const
|
||||
type ChunkTag = typeof CHUNK_TAGS[number]
|
||||
|
||||
@@ -96,39 +108,59 @@ export function bindRecord(record: StorageRecord): BoundRecord {
|
||||
|
||||
function encodeData(serialized: string): string | Uint8Array {
|
||||
const bytes = Buffer.from(serialized)
|
||||
if (bytes.length < ZSTD_DATA_THRESHOLD_BYTES) return serialized
|
||||
const compressed = zstdCompressSync(bytes, {
|
||||
params: { [constants.ZSTD_c_compressionLevel]: ZSTD_COMPRESSION_LEVEL },
|
||||
})
|
||||
const compressed = zstdCompressSync(bytes, DATA_ZSTD_OPTIONS)
|
||||
return compressed.length < bytes.length ? compressed : serialized
|
||||
}
|
||||
|
||||
function decodeData(value: string | Uint8Array, maxOutputLength?: number): string {
|
||||
if (typeof value === 'string') return value
|
||||
const decoded = maxOutputLength === undefined
|
||||
? zstdDecompressSync(value)
|
||||
: zstdDecompressSync(value, { maxOutputLength })
|
||||
? zstdDecompressSync(value, { dictionary: ZSTD_DICTIONARY })
|
||||
: zstdDecompressSync(value, { dictionary: ZSTD_DICTIONARY, maxOutputLength })
|
||||
return UTF8_DECODER.decode(decoded)
|
||||
}
|
||||
|
||||
function encodeSourceEventSeqs(values: readonly number[]): Uint8Array {
|
||||
const bytes: number[] = []
|
||||
if (values.length === 0) return new Uint8Array()
|
||||
const deltas = [DELTA_TAG]
|
||||
let previous = 0n
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const sourceSeq = values[index] as number
|
||||
if (!Number.isSafeInteger(sourceSeq) || sourceSeq < 0) {
|
||||
const value = values[index] as number
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new TypeError('sourceEventSeqs must contain non-negative safe integers')
|
||||
}
|
||||
const value = BigInt(sourceSeq)
|
||||
const current = BigInt(value)
|
||||
const encoded = index === 0
|
||||
? value
|
||||
: value >= previous
|
||||
? (value - previous) * 2n
|
||||
: ((previous - value) * 2n) - 1n
|
||||
appendVarint(bytes, encoded)
|
||||
previous = value
|
||||
? current
|
||||
: current >= previous
|
||||
? (current - previous) * 2n
|
||||
: ((previous - current) * 2n) - 1n
|
||||
appendVarint(deltas, encoded)
|
||||
previous = current
|
||||
}
|
||||
return Buffer.from(bytes)
|
||||
if (!isStrictlyIncreasing(values)) return Uint8Array.from(deltas)
|
||||
|
||||
const runs = [RUN_TAG]
|
||||
let start = values[0] as number
|
||||
let end = start
|
||||
for (let index = 1; index < values.length; index += 1) {
|
||||
const value = values[index] as number
|
||||
if (value === end + 1) {
|
||||
end = value
|
||||
continue
|
||||
}
|
||||
appendVarint(runs, BigInt(start))
|
||||
appendVarint(runs, BigInt(end - start + 1))
|
||||
start = value
|
||||
end = start
|
||||
}
|
||||
appendVarint(runs, BigInt(start))
|
||||
appendVarint(runs, BigInt(end - start + 1))
|
||||
return Uint8Array.from(runs.length < deltas.length ? runs : deltas)
|
||||
}
|
||||
|
||||
function isStrictlyIncreasing(values: readonly number[]): boolean {
|
||||
return values.every((value, index) => index === 0 || value > (values[index - 1] as number))
|
||||
}
|
||||
|
||||
function appendVarint(bytes: number[], value: bigint): void {
|
||||
@@ -140,10 +172,21 @@ function appendVarint(bytes: number[], value: bigint): void {
|
||||
bytes.push(Number(remaining))
|
||||
}
|
||||
|
||||
function decodeSourceEventSeqs(bytes: Uint8Array): number[] {
|
||||
function decodeSourceEventSeqs(bytes: Uint8Array, maxEntries: number): number[] {
|
||||
if (bytes.length === 0) return []
|
||||
if (bytes.length === 1) {
|
||||
throw new Error('malformed source_event_seqs storage value: truncated tagged payload')
|
||||
}
|
||||
switch (bytes[0]) {
|
||||
case DELTA_TAG: return decodeDeltaVarints(bytes, 1)
|
||||
case RUN_TAG: return decodeRunVarints(bytes, 1, maxEntries)
|
||||
default: throw new Error('malformed source_event_seqs storage value: unknown encoding tag')
|
||||
}
|
||||
}
|
||||
|
||||
function decodeDeltaVarints(bytes: Uint8Array, offset: number): number[] {
|
||||
const values: number[] = []
|
||||
let previous = 0n
|
||||
let offset = 0
|
||||
while (offset < bytes.length) {
|
||||
const first = values.length === 0
|
||||
const decoded = readVarint(bytes, offset, first ? MAX_SAFE_INTEGER : MAX_ZIGZAG_INTEGER)
|
||||
@@ -163,6 +206,30 @@ function decodeSourceEventSeqs(bytes: Uint8Array): number[] {
|
||||
return values
|
||||
}
|
||||
|
||||
function decodeRunVarints(bytes: Uint8Array, offset: number, maxEntries: number): number[] {
|
||||
const values: number[] = []
|
||||
let previousEnd = -1
|
||||
while (offset < bytes.length) {
|
||||
const start = readVarint(bytes, offset, MAX_SAFE_INTEGER)
|
||||
const count = readVarint(bytes, start.offset, MAX_SAFE_INTEGER)
|
||||
offset = count.offset
|
||||
const first = Number(start.value)
|
||||
const length = Number(count.value)
|
||||
if (length < 1) {
|
||||
throw new Error('malformed source_event_seqs storage value: run count must be positive')
|
||||
}
|
||||
if (first <= previousEnd || !Number.isSafeInteger(first + length - 1)) {
|
||||
throw new Error('malformed source_event_seqs storage value: runs must ascend within safe integers')
|
||||
}
|
||||
if (length > maxEntries - values.length) {
|
||||
throw new Error('malformed source_event_seqs storage value: run exceeds its event sequence')
|
||||
}
|
||||
for (let index = 0; index < length; index += 1) values.push(first + index)
|
||||
previousEnd = first + length - 1
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
function readVarint(
|
||||
bytes: Uint8Array,
|
||||
offset: number,
|
||||
@@ -199,7 +266,7 @@ function decodeScalarRow(row: EventRow): SessionEvent {
|
||||
const surfaceFields = {
|
||||
...row.source_event_seqs === null
|
||||
? {}
|
||||
: { sourceEventSeqs: decodeSourceEventSeqs(row.source_event_seqs) },
|
||||
: { sourceEventSeqs: decodeSourceEventSeqs(row.source_event_seqs, row.seq) },
|
||||
...row.surface_op === null
|
||||
? {}
|
||||
: { surfaceOp: JSON.parse(row.surface_op) as SessionEvent<SurfaceEventType>['surfaceOp'] },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Opt-in SQLite persistence provider. Logical sessions remain unchanged;
|
||||
* the physical backend packs eligible chunk runs into schema-18 rows.
|
||||
* the physical backend packs eligible chunk runs into schema-19 rows.
|
||||
* @module @deepseek-ai/dsh-session-persistence-sqlite
|
||||
*/
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { sql } from './sql.ts'
|
||||
|
||||
/** Current physical-record schema with packed and compressed event rows. */
|
||||
export const SCHEMA_VERSION = 18
|
||||
export const SCHEMA_VERSION = 19
|
||||
/** Application id reserved for DeepSeek Harness SQLite session databases. */
|
||||
export const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 0x44534850
|
||||
|
||||
@@ -109,6 +109,7 @@ function configureDatabase(
|
||||
db: DatabaseSync,
|
||||
path: string,
|
||||
): void {
|
||||
db.exec(sql('page-size'))
|
||||
db.exec(sql('foreign-keys-on'))
|
||||
let began = false
|
||||
try {
|
||||
@@ -206,7 +207,7 @@ function initializeDatabase(db: DatabaseSync): void {
|
||||
db.exec(sql('schema'))
|
||||
db.prepare(sql('insert-persistence-state')).run(randomUUID())
|
||||
db.exec(sql('set-application-id'))
|
||||
db.exec(sql('set-user-version-18'))
|
||||
db.exec(sql('set-user-version-19'))
|
||||
}
|
||||
|
||||
let canonicalSchema: readonly SchemaObjectRow[] | undefined
|
||||
|
||||
@@ -19,6 +19,7 @@ const SQL_RESOURCES = [
|
||||
'journal-mode-truncate',
|
||||
'journal-mode-wal',
|
||||
'mmap-off',
|
||||
'page-size',
|
||||
'rollback',
|
||||
'schema',
|
||||
'select-application-id',
|
||||
@@ -28,6 +29,7 @@ const SQL_RESOURCES = [
|
||||
'select-packed-predecessors',
|
||||
'select-schema-objects',
|
||||
'select-session',
|
||||
'select-session-key',
|
||||
'select-sessions',
|
||||
'select-store-id',
|
||||
'select-synchronous',
|
||||
@@ -36,7 +38,7 @@ const SQL_RESOURCES = [
|
||||
'select-user-object-count',
|
||||
'select-user-version',
|
||||
'set-application-id',
|
||||
'set-user-version-18',
|
||||
'set-user-version-19',
|
||||
'synchronous-full',
|
||||
'trusted-schema-off',
|
||||
'update-session-revision',
|
||||
|
||||
@@ -136,7 +136,7 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
const snapshot = this.readTransaction(() => {
|
||||
const row = this.rowFor(id)
|
||||
if (row === undefined) return undefined
|
||||
const eventRows = this.db.prepare(sql('select-events')).all(id).map(decodeEventRow)
|
||||
const eventRows = this.db.prepare(sql('select-events')).all(this.sessionKey(id)).map(decodeEventRow)
|
||||
return { row, eventRows }
|
||||
})
|
||||
signal?.throwIfAborted()
|
||||
@@ -162,7 +162,7 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
const snapshot = this.readTransaction(() => {
|
||||
const row = this.rowFor(id)
|
||||
if (row === undefined) return undefined
|
||||
return { row, ...this.physicalSpanFrom(id, fromSeq) }
|
||||
return { row, ...this.physicalSpanFrom(this.sessionKey(id), fromSeq) }
|
||||
})
|
||||
signal?.throwIfAborted()
|
||||
if (snapshot === undefined) return undefined
|
||||
@@ -180,17 +180,17 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
this.db.exec(sql('begin-immediate'))
|
||||
try {
|
||||
validateSchemaForMutation(this.databaseConstructor, this.db, this.databasePath)
|
||||
const tailRows = this.tailRows(meta.id)
|
||||
const sessionKey = isMaterialized ? this.sessionKey(meta.id) : this.writeRow(meta)
|
||||
const tailRows = this.tailRows(sessionKey)
|
||||
const currentLast = this.logicalLastEvent(meta.id, tailRows)
|
||||
const expected = currentLast === undefined ? 0 : currentLast.seq + 1
|
||||
const first = events[0] as SessionEvent
|
||||
if (first.seq !== expected) {
|
||||
throw new Error(`session ${meta.id} append starts at seq ${first.seq}, stored next seq is ${expected}`)
|
||||
}
|
||||
if (!isMaterialized) this.writeRow(meta)
|
||||
|
||||
const insert = this.insertStatement()
|
||||
for (const record of packChunkRuns(events)) this.insertRecord(insert, meta.id, bindRecord(record))
|
||||
for (const record of packChunkRuns(events)) this.insertRecord(insert, sessionKey, bindRecord(record))
|
||||
this.incrementRevision(meta.id)
|
||||
this.db.exec(sql('commit'))
|
||||
} catch (error: unknown) {
|
||||
@@ -223,14 +223,15 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
validateSchemaForMutation(this.databaseConstructor, this.db, this.databasePath)
|
||||
const row = this.rowFor(meta.id)
|
||||
if (row === undefined) throw new Error(`session ${meta.id} metadata row is missing`)
|
||||
const currentRows = this.db.prepare(sql('select-events')).all(meta.id).map(decodeEventRow)
|
||||
const sessionKey = this.sessionKey(meta.id)
|
||||
const currentRows = this.db.prepare(sql('select-events')).all(sessionKey).map(decodeEventRow)
|
||||
const current = scanRows(currentRows)
|
||||
if (tornMarker !== undefined) {
|
||||
if (current.tornFrom !== tornMarker) {
|
||||
throw new Error(`session ${meta.id} repair is stale: physical tail no longer starts at seq ${tornMarker}`)
|
||||
}
|
||||
this.db.prepare(sql('delete-events-from'))
|
||||
.run(meta.id, tornMarker)
|
||||
.run(sessionKey, tornMarker)
|
||||
} else if (current.tornFrom !== undefined) {
|
||||
throw new Error(`session ${meta.id} repair omitted current torn tail at seq ${current.tornFrom}`)
|
||||
}
|
||||
@@ -242,7 +243,7 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
throw new Error(`session ${meta.id} repair is stale: closer starts at seq ${closers[0]?.seq}, stored next seq is ${expected}`)
|
||||
}
|
||||
const insert = this.insertStatement()
|
||||
for (const closer of closers) this.insertRecord(insert, meta.id, bindRecord(closer))
|
||||
for (const closer of closers) this.insertRecord(insert, sessionKey, bindRecord(closer))
|
||||
}
|
||||
this.incrementRevision(meta.id)
|
||||
this.db.exec(sql('commit'))
|
||||
@@ -289,6 +290,12 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
return value === undefined ? undefined : decodeSessionRow(value)
|
||||
}
|
||||
|
||||
private sessionKey(id: SessionId): number {
|
||||
const row = this.db.prepare(sql('select-session-key')).get(id) as { id: number } | undefined
|
||||
if (row === undefined) throw new Error(`session ${id} metadata row is missing`)
|
||||
return row.id
|
||||
}
|
||||
|
||||
private async observe(signal: AbortSignal | undefined): Promise<void> {
|
||||
signal?.throwIfAborted()
|
||||
await this.open()
|
||||
@@ -327,20 +334,20 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
if (Number(updated.changes) !== 1) throw new Error(`session ${id} metadata row is missing`)
|
||||
}
|
||||
|
||||
private tailRows(id: SessionId): EventRow[] {
|
||||
const tail = this.db.prepare(sql('select-tail-events')).all(id, 2).map(decodeEventRow).reverse()
|
||||
private tailRows(sessionKey: number): EventRow[] {
|
||||
const tail = this.db.prepare(sql('select-tail-events')).all(sessionKey, 2).map(decodeEventRow).reverse()
|
||||
if (tail.length === 0) return []
|
||||
return this.physicalSpanFrom(id, (tail[0] as EventRow).seq).eventRows
|
||||
return this.physicalSpanFrom(sessionKey, (tail[0] as EventRow).seq).eventRows
|
||||
}
|
||||
|
||||
/** Select the bounded physical span that may represent `fromSeq`. */
|
||||
private physicalSpanFrom(
|
||||
id: SessionId,
|
||||
sessionKey: number,
|
||||
fromSeq: number,
|
||||
): { readonly base: number; readonly eventRows: EventRow[] } {
|
||||
const packedFloor = Math.max(0, fromSeq - MAX_PACKED_ROW_MEMBERS + 1)
|
||||
const packedPredecessors = this.db.prepare(sql('select-packed-predecessors'))
|
||||
.all(id, packedFloor, fromSeq)
|
||||
.all(sessionKey, packedFloor, fromSeq)
|
||||
.map(decodeEventRow)
|
||||
let base = fromSeq
|
||||
for (const predecessor of packedPredecessors) {
|
||||
@@ -352,7 +359,7 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
base = Math.min(base, predecessor.seq)
|
||||
}
|
||||
}
|
||||
const eventRows = this.db.prepare(sql('select-events-from')).all(id, base).map(decodeEventRow)
|
||||
const eventRows = this.db.prepare(sql('select-events-from')).all(sessionKey, base).map(decodeEventRow)
|
||||
return { base, eventRows }
|
||||
}
|
||||
|
||||
@@ -367,9 +374,9 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
return this.db.prepare(sql('insert-event'))
|
||||
}
|
||||
|
||||
private insertRecord(insert: StatementSync, id: SessionId, record: BoundRecord): void {
|
||||
private insertRecord(insert: StatementSync, sessionKey: number, record: BoundRecord): void {
|
||||
insert.run(
|
||||
id,
|
||||
sessionKey,
|
||||
record.seq,
|
||||
record.type,
|
||||
record.time,
|
||||
@@ -380,8 +387,8 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
)
|
||||
}
|
||||
|
||||
private writeRow(meta: SessionHeader): void {
|
||||
this.db.prepare(sql('upsert-session')).run(
|
||||
private writeRow(meta: SessionHeader): number {
|
||||
const inserted = this.db.prepare(sql('upsert-session')).get(
|
||||
meta.id,
|
||||
meta.version,
|
||||
meta.createdAt,
|
||||
@@ -392,7 +399,8 @@ export class SqliteStore implements PersistenceBackend<number> {
|
||||
meta.delegationDepth ?? null,
|
||||
meta.agentPreset ?? null,
|
||||
randomUUID(),
|
||||
)
|
||||
) as { id: number }
|
||||
return inserted.id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@ vi.mock('node:zlib', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
import { bindRecord, ZSTD_DATA_THRESHOLD_BYTES } from '../src/compression.ts'
|
||||
import { bindRecord } from '../src/compression.ts'
|
||||
|
||||
describe('SQLite compression fallback', () => {
|
||||
it('keeps large data as text when its Zstandard frame is not smaller', () => {
|
||||
it('keeps data as text when its Zstandard frame is not smaller', () => {
|
||||
const event = {
|
||||
type: 'assistant/message',
|
||||
seq: 0,
|
||||
time: 1,
|
||||
data: { text: 'x'.repeat(ZSTD_DATA_THRESHOLD_BYTES) },
|
||||
data: { text: 'x' },
|
||||
} as unknown as SessionEvent
|
||||
|
||||
expect(typeof bindRecord(event).data).toBe('string')
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { zstdCompressSync } from 'node:zlib'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
@@ -13,7 +15,6 @@ import {
|
||||
bindRecord,
|
||||
decodeRow,
|
||||
scanRows,
|
||||
ZSTD_DATA_THRESHOLD_BYTES,
|
||||
} from '../src/compression.ts'
|
||||
import type { EventRow } from '../src/schema.ts'
|
||||
|
||||
@@ -48,6 +49,12 @@ function row(record: StorageRecord): EventRow {
|
||||
}
|
||||
|
||||
describe('SQLite compression', () => {
|
||||
it('pins the schema-19 dictionary bytes', () => {
|
||||
const dictionary = readFileSync(new URL('../resources/zstd-dictionary.bin', import.meta.url))
|
||||
expect(createHash('sha256').update(dictionary).digest('hex'))
|
||||
.toBe('dad18fa0247a8fdd886a62d8552eabd36cbd50c25af172873080d2f0ae770d17')
|
||||
})
|
||||
|
||||
it('stores a 100-member run in one row and restores every logical event', () => {
|
||||
const events = Array.from({ length: 100 }, (_, index) => chunk(index))
|
||||
const records = packChunkRuns(events)
|
||||
@@ -160,7 +167,7 @@ describe('SQLite compression', () => {
|
||||
expect(() => decodeStorageRecord(record)).toThrow(/malformed .* storage row/)
|
||||
})
|
||||
|
||||
it('decodes the schema-18 row vocabulary without another package codec', () => {
|
||||
it('decodes the schema-19 row vocabulary without another package codec', () => {
|
||||
const fixture: EventRow = {
|
||||
seq: 7,
|
||||
type: 'text-chunks',
|
||||
@@ -211,19 +218,34 @@ describe('SQLite compression', () => {
|
||||
},
|
||||
)
|
||||
|
||||
it('compresses large data and delta-encodes complete provenance arrays', () => {
|
||||
it('compresses small repetitive data with the shared dictionary', () => {
|
||||
const event = {
|
||||
type: 'tool/result',
|
||||
seq: 1,
|
||||
time: 2,
|
||||
data: { turn: 1, step: 1, message: { content: [{ type: 'text', text: 'hello world '.repeat(40) }] } },
|
||||
sourceEventSeqs: [0],
|
||||
surfaceOp: 'append',
|
||||
} as unknown as SessionEvent
|
||||
const bound = bindRecord(event)
|
||||
expect(bound.data).toBeInstanceOf(Uint8Array)
|
||||
expect(decodeRow(row(event))).toEqual([event])
|
||||
})
|
||||
|
||||
it('compresses large data and run-encodes consecutive provenance arrays', () => {
|
||||
const sources = Array.from({ length: 2_000 }, (_, index) => index + 10)
|
||||
const event = {
|
||||
type: 'assistant/message',
|
||||
seq: sources.at(-1)! + 1,
|
||||
time: 1,
|
||||
data: { text: 'x'.repeat(ZSTD_DATA_THRESHOLD_BYTES * 2) },
|
||||
data: { text: 'x'.repeat(8_192) },
|
||||
sourceEventSeqs: sources,
|
||||
surfaceOp: 'append',
|
||||
} as unknown as SessionEvent
|
||||
const bound = bindRecord(event)
|
||||
expect(bound.data).toBeInstanceOf(Uint8Array)
|
||||
expect(bound.sourceEventSeqs).toBeInstanceOf(Uint8Array)
|
||||
expect(bound.sourceEventSeqs?.[0]).toBe(1)
|
||||
expect(bound.sourceEventSeqs?.byteLength).toBeLessThan(Buffer.byteLength(JSON.stringify(sources)))
|
||||
expect(decodeRow(row(event))).toEqual([event])
|
||||
|
||||
@@ -234,6 +256,7 @@ describe('SQLite compression', () => {
|
||||
it('round-trips empty, descending, and maximum-safe provenance deltas', () => {
|
||||
for (const sources of [
|
||||
[],
|
||||
[1, 3, 4, 5, 10],
|
||||
[Number.MAX_SAFE_INTEGER - 1, 0, Number.MAX_SAFE_INTEGER - 2],
|
||||
]) {
|
||||
const event = {
|
||||
@@ -248,6 +271,19 @@ describe('SQLite compression', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not impose a persistence-only provenance length limit', () => {
|
||||
const sources = Array.from({ length: 1_000_001 }, (_, index) => index)
|
||||
const event = {
|
||||
type: 'assistant/message',
|
||||
seq: sources.length,
|
||||
time: 1,
|
||||
data: {},
|
||||
sourceEventSeqs: sources,
|
||||
surfaceOp: 'append',
|
||||
} as unknown as SessionEvent
|
||||
expect(bindRecord(event).sourceEventSeqs?.[0]).toBe(1)
|
||||
})
|
||||
|
||||
it.each([-1, 0.5])('rejects invalid provenance sequence %s before encoding', (sourceSeq) => {
|
||||
const event = {
|
||||
type: 'assistant/message',
|
||||
@@ -263,20 +299,36 @@ describe('SQLite compression', () => {
|
||||
it('rejects malformed compressed and delta-encoded values', () => {
|
||||
const scalar = row({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } })
|
||||
expect(() => decodeRow({ ...scalar, data: Buffer.from('not zstd') })).toThrow()
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x80]) }))
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x00]) }))
|
||||
.toThrow(/truncated tagged payload/)
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x01]) }))
|
||||
.toThrow(/truncated tagged payload/)
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x02, 0x00]) }))
|
||||
.toThrow(/unknown encoding tag/)
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x00, 0x80]) }))
|
||||
.toThrow(/truncated varint/)
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x80, 0x00]) }))
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x00, 0x80, 0x00]) }))
|
||||
.toThrow(/non-canonical varint/)
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x00, 0x01]) }))
|
||||
// tag 0, first value 0, then a negative delta (zigzag 0x01) from 0
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x00, 0x00, 0x01]) }))
|
||||
.toThrow(/decoded seq is out of range/)
|
||||
// tag 0, first value MAX_SAFE_INTEGER, then a positive delta overflowing it
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x02,
|
||||
0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x02,
|
||||
]) })).toThrow(/decoded seq is out of range/)
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([
|
||||
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x10,
|
||||
0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x10,
|
||||
]) })).toThrow(/varint is out of range/)
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.alloc(9, 0x80) }))
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.concat([
|
||||
Buffer.from([0x00]), Buffer.alloc(9, 0x80),
|
||||
]) }))
|
||||
.toThrow(/varint is out of range/)
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x01, 0x00, 0x01]) }))
|
||||
.toThrow(/run exceeds its event sequence/)
|
||||
expect(() => decodeRow({ ...scalar, source_event_seqs: Buffer.from([0x01, 0x00, 0x00]) }))
|
||||
.toThrow(/run count must be positive/)
|
||||
expect(() => decodeRow({ ...scalar, seq: 2, source_event_seqs: Buffer.from([0x01, 0x00, 0x01, 0x00, 0x01]) }))
|
||||
.toThrow(/runs must ascend/)
|
||||
})
|
||||
|
||||
it('rejects an oversized packed data column before JSON decoding', () => {
|
||||
|
||||
+1
-1
@@ -11,4 +11,4 @@ CREATE TABLE events (
|
||||
INSERT INTO persistence_state (singleton, store_id)
|
||||
VALUES (1, '00000000-0000-4000-8000-000000000000');
|
||||
PRAGMA application_id = 1146308688;
|
||||
PRAGMA user_version = 18;
|
||||
PRAGMA user_version = 19;
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
DELETE FROM events
|
||||
WHERE session_id = ?;
|
||||
WHERE session_id = (SELECT id FROM sessions WHERE session_key = ?)
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
INSERT INTO events (session_id, seq, type, time, data, is_packed)
|
||||
VALUES (?, ?, ?, ?, ?, ?);
|
||||
VALUES ((SELECT id FROM sessions WHERE session_key = ?), ?, ?, ?, ?, ?);
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
SELECT rowid, seq, type, time, data, source_event_seqs, surface_op, is_packed
|
||||
FROM events
|
||||
WHERE session_id = ?
|
||||
WHERE session_id = (SELECT id FROM sessions WHERE session_key = ?)
|
||||
ORDER BY seq;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
SELECT seq, type, data
|
||||
FROM events
|
||||
WHERE session_id = ?
|
||||
WHERE session_id = (SELECT id FROM sessions WHERE session_key = ?)
|
||||
ORDER BY seq DESC
|
||||
LIMIT 1;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
PRAGMA page_size;
|
||||
@@ -0,0 +1 @@
|
||||
PRAGMA page_size = 4096;
|
||||
+1
@@ -0,0 +1 @@
|
||||
PRAGMA user_version = 19;
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
UPDATE sessions
|
||||
SET origin = 'external', delegation_depth = -1, seed_length = -1
|
||||
WHERE id = ?;
|
||||
WHERE session_key = ?;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
VACUUM;
|
||||
@@ -300,6 +300,7 @@ describe('SessionPersistenceSqlite physical packing', () => {
|
||||
|
||||
const db = new DatabaseSync(path)
|
||||
expect(db.prepare(testSql('select-user-version')).get()).toEqual({ user_version: SCHEMA_VERSION })
|
||||
expect(db.prepare(testSql('select-page-size')).get()).toEqual({ page_size: 65_536 })
|
||||
expect(db.prepare(testSql('count-events')).get()).toEqual({ count: 7 })
|
||||
expect(db.prepare(testSql('count-packed-events')).get())
|
||||
.toEqual({ count: 1 })
|
||||
@@ -379,6 +380,22 @@ describe('SessionPersistenceSqlite physical packing', () => {
|
||||
.rejects.toThrow(/schema version 17.*incompatible/)
|
||||
})
|
||||
|
||||
it('keeps the page size of an established schema 19 database', async () => {
|
||||
const path = await freshDbPath('dsh-sqlite-page-size-')
|
||||
const seed = await openDatabase(DatabaseSync, path, 'delete', DEFAULT_BUSY_TIMEOUT_MS)
|
||||
seed.close()
|
||||
|
||||
const resize = new DatabaseSync(path)
|
||||
resize.exec(testSql('set-page-size-4096'))
|
||||
resize.exec(testSql('vacuum'))
|
||||
expect(resize.prepare(testSql('select-page-size')).get()).toEqual({ page_size: 4_096 })
|
||||
resize.close()
|
||||
|
||||
const reopened = await openDatabase(DatabaseSync, path, 'wal', DEFAULT_BUSY_TIMEOUT_MS)
|
||||
expect(reopened.prepare(testSql('select-page-size')).get()).toEqual({ page_size: 4_096 })
|
||||
reopened.close()
|
||||
})
|
||||
|
||||
it('rejects a stale physical append without replacing the winning tail', async () => {
|
||||
const path = await freshDbPath('dsh-sqlite-stale-')
|
||||
const first = new SqliteStore({ path, journalMode: 'wal', busyTimeoutMs: DEFAULT_BUSY_TIMEOUT_MS })
|
||||
@@ -392,6 +409,21 @@ describe('SessionPersistenceSqlite physical packing', () => {
|
||||
await second.close()
|
||||
})
|
||||
|
||||
it('rolls back lazy integer-key materialization after a rejected append', async () => {
|
||||
const store = new SqliteStore({
|
||||
path: await freshDbPath('dsh-sqlite-key-rollback-'),
|
||||
journalMode: 'wal',
|
||||
busyTimeoutMs: DEFAULT_BUSY_TIMEOUT_MS,
|
||||
})
|
||||
const header = meta(SessionId('key-rollback'))
|
||||
|
||||
await expect(store.appendBatch(header, [chunk(1)], false)).rejects.toThrow(/stored next seq is 0/)
|
||||
await expect(store.appendBatch(header, [chunk(0)], true)).rejects.toThrow(/metadata row is missing/)
|
||||
await expect(store.appendBatch(header, [chunk(0)], false)).resolves.toBeUndefined()
|
||||
expect((await store.loadStored(header.id))?.events).toEqual([chunk(0)])
|
||||
await store.close()
|
||||
})
|
||||
|
||||
it('rejects a stale repair without deleting a newer winning tail', async () => {
|
||||
const path = await freshDbPath('dsh-sqlite-stale-repair-')
|
||||
const stale = new SqliteStore({ path, journalMode: 'wal', busyTimeoutMs: DEFAULT_BUSY_TIMEOUT_MS })
|
||||
@@ -537,7 +569,7 @@ describe('SessionPersistenceSqlite schema ownership', () => {
|
||||
|
||||
const foreignPath = await freshDbPath('dsh-sqlite-foreign-')
|
||||
const foreign = new DatabaseSync(foreignPath)
|
||||
foreign.exec(testSql('set-user-version-18'))
|
||||
foreign.exec(testSql('set-user-version-19'))
|
||||
foreign.exec(testSql('set-application-id-12345'))
|
||||
foreign.close()
|
||||
await expect(openDatabase(DatabaseSync, foreignPath, 'wal', DEFAULT_BUSY_TIMEOUT_MS)).rejects.toThrow(/has application id 12345/)
|
||||
|
||||
@@ -16,15 +16,19 @@ export type TestSqlName =
|
||||
| 'measure-write-traffic'
|
||||
| 'replace-events-with-nonstrict-table'
|
||||
| 'select-last-event'
|
||||
| 'select-page-size'
|
||||
| 'select-event-rowids'
|
||||
| 'select-event-rows'
|
||||
| 'select-user-version'
|
||||
| 'set-application-id-12345'
|
||||
| 'set-page-size-4096'
|
||||
| 'set-user-version-15'
|
||||
| 'set-user-version-16'
|
||||
| 'set-user-version-17'
|
||||
| 'set-user-version-18'
|
||||
| 'set-user-version-19'
|
||||
| 'update-invalid-session-metadata'
|
||||
| 'vacuum'
|
||||
|
||||
/** Load one fixed test SQL resource. */
|
||||
export function testSql(name: TestSqlName): string {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { delimiter as pathDelimiter } from 'node:path'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-compaction'
|
||||
import type {} from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
|
||||
import { decodeStorageRecord, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { decodeSeqRanges, decodeStorageRecord, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type {
|
||||
ContentBlock,
|
||||
GenerateOptions,
|
||||
@@ -170,9 +170,10 @@ export interface SessionScript {
|
||||
/**
|
||||
* Parse a session `.jsonl` buffer into its event list. Line 0 is the session
|
||||
* header (a `{type:'session',…}` record), every subsequent non-empty line is a
|
||||
* {@link SessionEvent} or a packed chunk row (expanded back into its events, so
|
||||
* a fixture recorded with `packChunks` on derives the same script). The header
|
||||
* is skipped; malformed lines fail loud.
|
||||
* {@link SessionEvent} or a packed chunk row. Packed rows expand back into
|
||||
* events, and JSONL storage-form provenance ranges expand back into
|
||||
* `number[]`, so physical fixture encodings derive the same script. The
|
||||
* header is skipped; malformed lines fail loud.
|
||||
* @param text - the raw `.jsonl` file contents.
|
||||
* @returns every event after the header, in log order.
|
||||
*/
|
||||
@@ -206,6 +207,9 @@ export function parseSessionLog(text: string): SessionEvent[] {
|
||||
if (!Object.hasOwn(record, timeKey)) record[timeKey] = 0
|
||||
let decoded: SessionEvent[]
|
||||
try {
|
||||
if (Object.hasOwn(record, 'sourceEventSeqs')) {
|
||||
record.sourceEventSeqs = decodeSeqRanges(record.sourceEventSeqs)
|
||||
}
|
||||
decoded = decodeStorageRecord(record)
|
||||
} catch (error) {
|
||||
/* v8 ignore next -- decodeStorageRecord only throws Error instances; the String arm satisfies unknown narrowing. */
|
||||
|
||||
@@ -103,6 +103,28 @@ describe('parseSessionLog', () => {
|
||||
expect(parseSessionLog(`${header}\n\n${JSON.stringify(ev)}\n\n`)).toEqual([ev])
|
||||
})
|
||||
|
||||
it('expands range-encoded source provenance', () => {
|
||||
const header = JSON.stringify({ type: 'session', version: 0, id: 's1', createdAt: 0 })
|
||||
const event = {
|
||||
...chunkEvent(4, 1, 1, TEXT_CHUNKS[0] as StreamChunk),
|
||||
sourceEventSeqs: [[1, 3], 5],
|
||||
}
|
||||
expect(parseSessionLog(`${header}\n${JSON.stringify(event)}\n`)).toEqual([{
|
||||
...event,
|
||||
sourceEventSeqs: [1, 2, 3, 5],
|
||||
}])
|
||||
})
|
||||
|
||||
it('reports malformed range provenance with its source line', () => {
|
||||
const header = JSON.stringify({ type: 'session', version: 0, id: 's1', createdAt: 0 })
|
||||
const event = {
|
||||
...chunkEvent(4, 1, 1, TEXT_CHUNKS[0] as StreamChunk),
|
||||
sourceEventSeqs: [[3, 1]],
|
||||
}
|
||||
expect(() => parseSessionLog(`${header}\n${JSON.stringify(event)}\n`))
|
||||
.toThrow('session snapshot line 2: sourceEventSeqs ranges require start <= end')
|
||||
})
|
||||
|
||||
it('rejects non-object body rows with their source line', () => {
|
||||
const header = JSON.stringify({ type: 'session', version: 0, id: 's1', createdAt: 0 })
|
||||
expect(() => parseSessionLog(`${header}\nnull\n`))
|
||||
|
||||
@@ -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/test-support/session-snapshot/README.md
|
||||
README.md: 979dcc06e8a2249e89a2dc7c63503c21ffb237f7
|
||||
README.zh.md: 02d04a6145e51b916baefd76c6cd611f709962ae
|
||||
README.md: 9f1907af8b9e50f587bcc05b93d8e3730c8f272c
|
||||
README.zh.md: 2d4290578a655df91eb60f63177c97fc5d82da6c
|
||||
|
||||
@@ -102,7 +102,7 @@ This section explains the design of the kit; the observable behavior is fully co
|
||||
|
||||
### Design
|
||||
|
||||
The shared core owns manifests, workspace setup/comparison, typed identity mapping, normalizers, and fixture invariants. The ACP adapter adds four composable layers: launcher, scenario harness, normalizers, and suite factory. `launchAcpTestAgent` boots a source profile under tsx or a built `lib` profile under plain Node, connects the SDK client over a raw-byte stdout tee, collects session updates and stderr, fails closed on unhandled permission requests, and owns shutdown. `runScenario` drives ACP JSON-RPC stdio and harvests every persisted raw JSONL session log. The pure normalizers replace cwd paths and typed identities with stable tokens, zero times, and scrub request-header bulk. `defineAcpSnapshotSuite` registers comparisons, fixture write-back, and the live uniformity guard.
|
||||
The shared core owns manifests, workspace setup/comparison, typed identity mapping, normalizers, and fixture invariants. The ACP adapter adds four composable layers: launcher, scenario harness, normalizers, and suite factory. `launchAcpTestAgent` boots a source profile under tsx or a built `lib` profile under plain Node, connects the SDK client over a raw-byte stdout tee, collects session updates and stderr, fails closed on unhandled permission requests, and owns shutdown. `runScenario` drives ACP JSON-RPC stdio and harvests every persisted raw JSONL session log. The pure normalizers replace cwd paths and typed identities with stable tokens, zero times, expand physical provenance ranges, and scrub request-header bulk. `defineAcpSnapshotSuite` registers comparisons, fixture write-back, and the live uniformity guard.
|
||||
|
||||
### Source map
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ defineAcpSnapshotSuite({
|
||||
|
||||
### 设计
|
||||
|
||||
共享核心拥有 manifest、workspace 设置/比较、类型化身份映射、规范化器与 fixture 不变式。ACP 适配器增加四个可组合层:启动器、场景 harness、规范化器与套件工厂。`launchAcpTestAgent` 在 tsx 下启动源码 profile,或在普通 Node 下启动已构建 `lib` profile,通过原始字节 stdout tee 连接 SDK 客户端,收集会话更新与 stderr,默认拒绝未处理的权限请求,并负责关闭。`runScenario` 驱动 ACP JSON-RPC stdio,并收集每个持久化原始 JSONL 会话日志。纯规范化器把 cwd 路径与类型化身份变为稳定 token,将时间归零,并擦除请求 header bulk。`defineAcpSnapshotSuite` 注册比较、fixture 回写与实时一致性保护。
|
||||
共享核心拥有 manifest、workspace 设置/比较、类型化身份映射、规范化器与 fixture 不变式。ACP 适配器增加四个可组合层:启动器、场景 harness、规范化器与套件工厂。`launchAcpTestAgent` 在 tsx 下启动源码 profile,或在普通 Node 下启动已构建 `lib` profile,通过原始字节 stdout tee 连接 SDK 客户端,收集会话更新与 stderr,默认拒绝未处理的权限请求,并负责关闭。`runScenario` 驱动 ACP JSON-RPC stdio,并收集每个持久化原始 JSONL 会话日志。纯规范化器把 cwd 路径与类型化身份变为稳定 token,将时间归零、展开物理来源区间,并擦除请求 header bulk。`defineAcpSnapshotSuite` 注册比较、fixture 回写与实时一致性保护。
|
||||
|
||||
### 源码地图
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
decodeSeqRanges,
|
||||
decodeStorageRecord,
|
||||
packChunkRuns,
|
||||
type SessionEvent,
|
||||
@@ -367,6 +368,9 @@ export function normalizeSessionLog(
|
||||
if ('createdAt' in data) data.createdAt = 0
|
||||
if ('updatedAt' in data) data.updatedAt = 0
|
||||
}
|
||||
if (Object.hasOwn(record, 'sourceEventSeqs')) {
|
||||
record.sourceEventSeqs = decodeSeqRanges(record.sourceEventSeqs)
|
||||
}
|
||||
return scrubValue(record, ctx, cwdPathMode, identityMode) as Record<string, unknown>
|
||||
})
|
||||
return records.map(r => JSON.stringify(r)).join('\n') + '\n'
|
||||
|
||||
@@ -547,6 +547,19 @@ describe('normalizeSessionSnapshot', () => {
|
||||
].join('\n')])
|
||||
})
|
||||
|
||||
it('projects persisted provenance ranges back to logical seq arrays', () => {
|
||||
const raw = [
|
||||
JSON.stringify({ type: 'session', version: 0 }),
|
||||
JSON.stringify({
|
||||
type: 'assistant/message',
|
||||
sourceEventSeqs: [[1, 3], 5],
|
||||
surfaceOp: 'append',
|
||||
data: { turn: 1, step: 1 },
|
||||
}),
|
||||
].join('\n') + '\n'
|
||||
expect(normalizeSessionSnapshot(raw, ctx)).toContain('"sourceEventSeqs":[1,2,3,5]')
|
||||
})
|
||||
|
||||
it('rejects headerless input', () => {
|
||||
expect(() => normalizeSessionSnapshot('{"type":"turn/start"}\n', ctx))
|
||||
.toThrow('session snapshot must start with a session header')
|
||||
|
||||
@@ -160,8 +160,12 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
// sandbox-local resolves it through the package's ./runner export. tsdown
|
||||
// also shares its generated FFI code through a hashed runtime chunk.
|
||||
'@deepseek-ai/dsh-sandbox-windows-acl': ['lib/runner.js', 'lib/types-*.js'],
|
||||
// SQLite loads every statement from immutable package resources at runtime.
|
||||
'@deepseek-ai/dsh-session-persistence-sqlite': ['resources/sql/**/*.sql'],
|
||||
// SQLite loads its compression dictionary and every statement from immutable
|
||||
// package resources at runtime.
|
||||
'@deepseek-ai/dsh-session-persistence-sqlite': [
|
||||
'resources/zstd-dictionary.bin',
|
||||
'resources/sql/**/*.sql',
|
||||
],
|
||||
'@deepseek-ai/dsh-skill-badge': ['assets'],
|
||||
// tsdown shares the repository/pack code between the lib entry and the bin
|
||||
// through a hashed chunk. The committed bin.js is the link target pnpm can
|
||||
|
||||
@@ -76,13 +76,25 @@ describe('canonicalSessionFixture', () => {
|
||||
})
|
||||
|
||||
describe('isPhysicalSessionFixture', () => {
|
||||
it('excludes only persisted logs under the WebWorker example root', () => {
|
||||
it('recognizes fixtures that preserve physical persistence encoding', () => {
|
||||
expect(isPhysicalSessionFixture(
|
||||
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/main/session.jsonl',
|
||||
)).toBe(true)
|
||||
expect(isPhysicalSessionFixture(
|
||||
'scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl',
|
||||
)).toBe(true)
|
||||
expect(isPhysicalSessionFixture(
|
||||
'scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl',
|
||||
)).toBe(true)
|
||||
expect(isPhysicalSessionFixture(
|
||||
'scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl',
|
||||
)).toBe(true)
|
||||
expect(isPhysicalSessionFixture(
|
||||
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/README.jsonl',
|
||||
)).toBe(false)
|
||||
expect(isPhysicalSessionFixture(
|
||||
'scripts/snapshots/python-sdk-single-exe/advanced/requests.jsonl',
|
||||
)).toBe(false)
|
||||
expect(isPhysicalSessionFixture('apps/web/tests/snapshots/example/session.jsonl')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,9 +8,13 @@ import { packChunkRuns, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
|
||||
|
||||
/** Physical persistence artifacts validated by the WebWorker runtime fixture spec. */
|
||||
const PHYSICAL_SESSION_FIXTURE_ROOT =
|
||||
const WEBWORKER_PHYSICAL_SESSION_FIXTURE_ROOT =
|
||||
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/'
|
||||
|
||||
/** Installed-runtime snapshots that preserve the JSONL writer's physical encoding. */
|
||||
const PYTHON_RUNTIME_PHYSICAL_SESSION_FIXTURE_ROOT =
|
||||
'scripts/snapshots/python-sdk-single-exe/'
|
||||
|
||||
/** One repository session fixture and its canonical projected representation. */
|
||||
export interface SessionFixtureLayout {
|
||||
/** Repository-relative path with `/` separators. */
|
||||
@@ -22,13 +26,17 @@ export interface SessionFixtureLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a repository JSONL is a production-layout persistence artifact rather
|
||||
* than an envelope-free replay snapshot owned by this script.
|
||||
* Whether a repository JSONL preserves physical persistence encoding rather
|
||||
* than the logical event projection owned by this script.
|
||||
* @param path - Repository-relative path with `/` separators.
|
||||
* @returns True only for Session logs under the WebWorker VFS example root.
|
||||
* @returns True for physical WebWorker and installed-runtime session logs.
|
||||
*/
|
||||
export function isPhysicalSessionFixture(path: string): boolean {
|
||||
return path.startsWith(PHYSICAL_SESSION_FIXTURE_ROOT) && path.endsWith('/session.jsonl')
|
||||
if (path.startsWith(WEBWORKER_PHYSICAL_SESSION_FIXTURE_ROOT)) {
|
||||
return path.endsWith('/session.jsonl')
|
||||
}
|
||||
return path.startsWith(PYTHON_RUNTIME_PHYSICAL_SESSION_FIXTURE_ROOT)
|
||||
&& /\/session(?:\.\d+)?\.jsonl$/.test(path)
|
||||
}
|
||||
|
||||
function isSessionHeader(value: unknown): boolean {
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[14,18]],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[14,18]],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[13,17]],"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
@@ -28,7 +28,7 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[24,28]],"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[30],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
@@ -40,7 +40,7 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[36,37,38,39,40],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[36,40]],"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}
|
||||
{"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}}
|
||||
{"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}}
|
||||
@@ -53,7 +53,7 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[49,53]],"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":4}}
|
||||
@@ -64,7 +64,7 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[60,61,62,63,64],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[60,64]],"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}
|
||||
{"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}}
|
||||
{"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}}
|
||||
@@ -79,7 +79,7 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[75,76,77,78,79],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[75,79]],"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":6,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[81],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":6}}
|
||||
@@ -91,6 +91,6 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[87,88,89,90,91],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[87,91]],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":7}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PROCESS_ONE_OK"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_ONE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_ONE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[13,17]],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PROCESS_TWO_OK"}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_TWO_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_TWO_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[[13,17]],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
Reference in New Issue
Block a user