Merge pull request #3292 from deepseek-harness/perf/3270-linear-stream-queues

perf(api): make stream queue draining linear
This commit is contained in:
Dudu-0223
2026-08-29 21:47:00 +08:00
committed by GitHub
47 changed files with 783 additions and 41 deletions
@@ -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/bug-fix/2026-08-28-linear-stream-queue-drain.md
2026-08-28-linear-stream-queue-drain.md: 3ec9ff3ae0f4df1265bc38dd86e44c126ea7e3e9
2026-08-28-linear-stream-queue-drain.zh.md: c71b1da07408a8c502a60c84a38d8f009721d7bc
@@ -0,0 +1,56 @@
# Agent Note: Linear drain for long-lived stream queues
Status: implemented
English | [中文](2026-08-28-linear-stream-queue-drain.zh.md)
## Problem
Long-lived stream queues can accumulate thousands of frames while their consumers are busy. Removing each frame with `Array.prototype.shift()` moves the remaining array range on the observed V8 path, so draining `N` queued frames performs quadratic reference movement and delays unrelated work on the same event loop. [Issue #3270](https://github.com/deepseek-harness/deepseek-harness/issues/3270) records the production sample that identified `ArrayShift`, `MoveRange`, and `memmove` as the dominant stack.
The affected streams have different wake-up, failure, cancellation, and disposal behavior. Their shared requirement is storage that preserves FIFO order without making those lifecycle decisions.
## Decision
`@deepseek-ai/dsh-deque` owns one zero-dependency circular array for Host and browser consumers. `pushBack()`, `pushFront()`, and `popFront()` change indices instead of moving the live range. A removal clears its slot immediately. The backing array doubles when full and halves when a non-empty deque reaches one quarter of capacity, so growth and compaction copy work remains amortized constant time and vacant storage stays bounded over interleaved queue use.
The package has no singleton state, symbols, or class identity shared between consumers. Each consumer constructs and confines its own deque, so duplicate npm copies preserve runtime behavior and the published dependency policy treats `Deque` as a safe Host export. The Client bundle purity rule also treats the package as an inline-safe library. The Gateway browser artifact carries its deque implementation without introducing a module-table entry or a Cordis service.
The Host Remote event source, each connected Client Remote event queue, the browser Remote stream inbox, each Session history follower, each Session control stream, and each Workspace follower store frames in this deque. Their owning classes retain all wake-up, failure, cancellation, buffered-drain, and disposal behavior. Session history uses front insertion to place constructor-seed events before live events received during its opening observation.
Queue capacity, frame coalescing, overload rejection, and global agent admission remain consumer or application policy. The deque does not infer any of them from storage pressure.
## Verification
The deque unit suite covers FIFO order, front insertion, array-boundary wrapping, geometric growth, quarter-full compaction after interleaved enqueue and dequeue, clearing, reuse, and `undefined` entries. Focused coverage reports 100% statements, branches, functions, and lines for `packages/util/deque/src/index.ts`.
The API Remote, Gateway, Session control/history, and Workspace follow suites exercise the migrated lifecycle behavior. They retain their package-owned ordering, failure, cancellation, and disposal assertions.
The command `pnpm exec tsx packages/util/deque/benchmarks/drain.ts` ran on Node v26.0.0, arm64 macOS 26.4. Five samples per size produced these median deque drain times; enqueue time is outside the measurement:
| Entries | Median drain | Nanoseconds per entry |
|---:|---:|---:|
| 250,000 | 1.705 ms | 6.818 ns |
| 500,000 | 2.541 ms | 5.082 ns |
| 1,000,000 | 4.668 ms | 4.668 ns |
| 2,000,000 | 9.656 ms | 4.828 ns |
The checked-in benchmark makes the measurement reproducible, but CI does not enforce a wall-clock threshold. Deterministic unit coverage owns the algorithm and compaction paths; the benchmark demonstrates approximately linear drain work on the recorded runtime.
## Alternatives considered
**Array head removal.** Keeping `shift()` preserves the smallest source diff but repeats the production failure mode and provides no amortized constant-time guarantee.
**A monotonic head cursor with occasional slicing.** This can provide amortized constant-time FIFO removal, but Session history also needs front insertion before concurrently buffered entries. A circular deque provides both operations through one storage rule without a special history prefix buffer.
**A linked deque.** Linked nodes make every end operation constant time and release removed nodes immediately, but each frame also allocates a node and pointer fields. The circular array keeps contiguous storage and amortizes the less frequent copies.
**An external deque dependency.** The required API is small, and the retention rule includes immediate slot clearing plus a specific shrink condition that the regression suite must exercise. A local zero-dependency utility keeps that storage lifecycle inspectable in both compiler faces; an external collection would still require the same integration and retention verification.
## Consequences
Draining a backlog performs linear deque work instead of quadratic array-range movement. Removed frame references become collectible before backing-storage compaction, and a stream that remains active does not retain every historical slot.
The repository owns a small generic collection implementation and its compatibility surface. Changes to its indexing, growth, or shrink rules require focused ordering and compaction coverage because every migrated stream shares the result.
Unbounded producers can still exhaust memory or delay consumers through the volume of legitimate per-frame work. Capacity and admission policy remain separate decisions rather than hidden behavior in a generic collection.
@@ -0,0 +1,56 @@
# Agent Note: 长期流队列的线性排空
Status: implemented
[English](2026-08-28-linear-stream-queue-drain.md) | 中文
## 问题
当消费方忙碌时,长期存在的流队列可能积累数千个帧。在观测到的 V8 路径上,使用 `Array.prototype.shift()` 移除每个帧会移动剩余数组区间,因此排空 `N` 个排队帧会执行二次方级别的引用移动,并延迟同一事件循环上的无关工作。[Issue #3270](https://github.com/deepseek-harness/deepseek-harness/issues/3270) 记录了把 `ArrayShift``MoveRange``memmove` 识别为主要堆栈的生产采样。
受影响的流具有不同的唤醒、失败、取消和 disposal 行为。它们的共同要求是保持 FIFO 顺序、同时不替它们作出这些生命周期决策的存储。
## 决策
`@deepseek-ai/dsh-deque` 为 Host 和浏览器消费方拥有一个零依赖环形数组。`pushBack()``pushFront()``popFront()` 改变索引,而不移动存活区间。移除会立即清空对应槽位。后备数组在满载时翻倍,在非空双端队列达到四分之一容量时减半,因此扩容和压缩的复制工作保持摊销常数时间,且交错队列使用期间的空闲存储保持有界。
该包没有消费方之间共享的 singleton 状态、符号或类身份。每个消费方都会构造并独占自己的双端队列,因此 npm 中存在重复包副本不会改变运行时行为,发布依赖策略也会把 `Deque` 视为安全的 Host 导出。Client bundle purity 规则同样把该包视为可内联库。Gateway 浏览器产物携带其双端队列实现,而不引入 module-table 条目或 Cordis 服务。
Host Remote 事件源、每个已连接 Client 的 Remote 事件队列、浏览器 Remote 流 inbox、每个会话历史 follower、每个会话控制流和每个 Workspace follower 都在此双端队列中存储帧。它们的所属类保留全部唤醒、失败、取消、缓冲排空和 disposal 行为。会话历史使用前插,把构造器种子事件放在打开观察期间收到的 live 事件之前。
队列容量、帧合并、过载拒绝和全局 agent admission 仍是消费方或应用策略。双端队列不会根据存储压力推断其中任何策略。
## 验证
双端队列单元测试覆盖 FIFO 顺序、前插、数组边界环绕、几何扩容、交错入队和出队后的四分之一满压缩、清空、复用与 `undefined` 条目。聚焦覆盖率报告显示 `packages/util/deque/src/index.ts` 的语句、分支、函数和行均为 100%。
API Remote、Gateway、会话控制/历史和 Workspace follow 测试覆盖迁移后的生命周期行为。它们保留所属包对顺序、失败、取消和 disposal 的断言。
命令 `pnpm exec tsx packages/util/deque/benchmarks/drain.ts` 在 Node v26.0.0、arm64 macOS 26.4 上运行。每个规模采样五次,得到以下双端队列排空时间中位数;测量不包含入队时间:
| 条目数 | 排空中位数 | 每条目纳秒数 |
|---:|---:|---:|
| 250,000 | 1.705 ms | 6.818 ns |
| 500,000 | 2.541 ms | 5.082 ns |
| 1,000,000 | 4.668 ms | 4.668 ns |
| 2,000,000 | 9.656 ms | 4.828 ns |
检入的 benchmark 使该测量可复现,但 CI 不强制墙钟时间阈值。确定性单元覆盖率负责算法和压缩路径;benchmark 在所记录运行时上证明排空工作近似线性。
## 考虑过的替代方案
**数组头部移除。** 保留 `shift()` 能得到最小源码差异,但会重复生产故障模式,也不提供摊销常数时间保证。
**单调头游标配合偶尔切片。** 这可以提供摊销常数时间的 FIFO 移除,但会话历史还需要在并发缓冲条目之前执行前插。环形双端队列通过一项存储规则同时提供两种操作,不需要特殊的历史前缀缓冲区。
**链式双端队列。** 链式节点让每个端点操作都保持常数时间,并立即释放已移除节点,但每个帧还会分配一个节点和指针字段。环形数组保持连续存储,并摊销频率较低的复制。
**外部双端队列依赖。** 所需 API 很小,保留规则包括立即清空槽位以及回归测试必须覆盖的特定缩容条件。本地零依赖工具让两个编译 face 都能检查该存储生命周期;外部集合仍需相同的集成和保留验证。
## 后果
排空 backlog 会执行线性双端队列工作,而不是二次方级别的数组区间移动。已移除帧的引用在后备存储压缩前即可回收,持续活动的流也不会保留每个历史槽位。
仓库拥有一项小型通用集合实现及其兼容性接口。对其索引、扩容或缩容规则的修改需要聚焦的顺序和压缩覆盖,因为每个已迁移流都会共享结果。
无界生产者仍可能通过合法逐帧工作的数量耗尽内存或延迟消费方。容量和 admission 策略仍是独立决策,而不是通用集合中的隐藏行为。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: 9e2b671ee054af797e9a919920fdd799e8c50e61
config-catalog.zh.md: df386630eeddefaccd9d1630da95a7116492173c
config-catalog.md: d968108ae652a10007a9c00595a955cefeb37f3b
config-catalog.zh.md: 16f33ee3934c82a94c07fa7c406665465b1decbf
+2 -1
View File
@@ -290,7 +290,7 @@ export interface Config {
}
```
Source: [`packages/api/gateway/src/index.ts:118`](../packages/api/gateway/src/index.ts)
Source: [`packages/api/gateway/src/index.ts:119`](../packages/api/gateway/src/index.ts)
<a id="deepseek-aidsh-api-session-controller"></a>
@@ -3524,6 +3524,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-client-web` ([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
- `@deepseek-ai/dsh-cmdline` ([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-code-runtime-python` ([`packages/code-runtime/code-runtime-python/src/index.ts`](../packages/code-runtime/code-runtime-python/src/index.ts))
- `@deepseek-ai/dsh-deque` ([`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts))
- `@deepseek-ai/dsh-experimental-agent-team-profile` ([`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts))
- `@deepseek-ai/dsh-experimental-agent-team-web-profile` ([`packages/experimental/agent-team-web-profile/src/index.ts`](../packages/experimental/agent-team-web-profile/src/index.ts))
- `@deepseek-ai/dsh-experimental-webworker-packer` ([`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts))
+2 -1
View File
@@ -292,7 +292,7 @@ export interface Config {
}
```
来源:[`packages/api/gateway/src/index.ts:118`](../packages/api/gateway/src/index.ts)
来源:[`packages/api/gateway/src/index.ts:119`](../packages/api/gateway/src/index.ts)
<a id="deepseek-aidsh-api-session-controller"></a>
@@ -3525,6 +3525,7 @@ export interface Config {
- `@deepseek-ai/dsh-client-web`[`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts)
- `@deepseek-ai/dsh-cmdline`[`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts)
- `@deepseek-ai/dsh-code-runtime-python`[`packages/code-runtime/code-runtime-python/src/index.ts`](../packages/code-runtime/code-runtime-python/src/index.ts)
- `@deepseek-ai/dsh-deque`[`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts)
- `@deepseek-ai/dsh-experimental-agent-team-profile`[`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts)
- `@deepseek-ai/dsh-experimental-agent-team-web-profile`[`packages/experimental/agent-team-web-profile/src/index.ts`](../packages/experimental/agent-team-web-profile/src/index.ts)
- `@deepseek-ai/dsh-experimental-webworker-packer`[`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts)
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/module-graph.md
module-graph.md: e5225db8618a3d056257bf96e57e3ad99fb73134
module-graph.zh.md: 9a5f72ef3ddcc8c3823dd59812869611dec77169
module-graph.md: 0b5b428d0ec8eaaf4914b178f3e1af9b4665f4f6
module-graph.zh.md: 9aa8c181a13dadde72302f4876969a89cc28eda7
+3
View File
@@ -10,6 +10,7 @@ flowchart TD
subgraph group_util["packages/util"]
pkg_atomic_write["atomic-write"]
pkg_brand["brand"]
pkg_deque["deque"]
pkg_home_paths["home-paths"]
pkg_launch_environment["launch-environment"]
pkg_native_command["native-command"]
@@ -360,6 +361,7 @@ flowchart TD
end
pkg_atomic_write --> pkg_invariants
pkg_brand --> pkg_invariants
pkg_deque --> pkg_invariants
pkg_home_paths --> pkg_invariants
pkg_launch_environment --> pkg_invariants
pkg_native_command --> pkg_invariants
@@ -1386,6 +1388,7 @@ flowchart TD
| [`typert-registry`](../packages/typert/registry) | `typert` | — |
| [`atomic-write`](../packages/util/atomic-write) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`brand`](../packages/util/brand) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`deque`](../packages/util/deque) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`home-paths`](../packages/util/home-paths) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`launch-environment`](../packages/util/launch-environment) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`native-command`](../packages/util/native-command) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
+3
View File
@@ -12,6 +12,7 @@ flowchart TD
subgraph group_util["packages/util"]
pkg_atomic_write["atomic-write"]
pkg_brand["brand"]
pkg_deque["deque"]
pkg_home_paths["home-paths"]
pkg_launch_environment["launch-environment"]
pkg_native_command["native-command"]
@@ -362,6 +363,7 @@ flowchart TD
end
pkg_atomic_write --> pkg_invariants
pkg_brand --> pkg_invariants
pkg_deque --> pkg_invariants
pkg_home_paths --> pkg_invariants
pkg_launch_environment --> pkg_invariants
pkg_native_command --> pkg_invariants
@@ -1388,6 +1390,7 @@ flowchart TD
| [`typert-registry`](../packages/typert/registry) | `typert` | — |
| [`atomic-write`](../packages/util/atomic-write) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`brand`](../packages/util/brand) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`deque`](../packages/util/deque) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`home-paths`](../packages/util/home-paths) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`launch-environment`](../packages/util/launch-environment) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`native-command`](../packages/util/native-command) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
+1
View File
@@ -56,6 +56,7 @@
],
"license": "MIT",
"dependencies": {
"@deepseek-ai/dsh-deque": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/schemastery": "workspace:^",
"ws": "^8.21.0"
@@ -7,6 +7,7 @@ import {
type RemoteStreamClientMessage,
type RemoteStreamServerMessage,
} from '../stream-protocol.ts'
import { Deque } from '@deepseek-ai/dsh-deque'
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
const INTERNAL_BASE = 'http://dsh.internal'
@@ -272,13 +273,13 @@ export class RemoteStreamMuxClient {
}
class StreamInbox {
private readonly frames: RemoteStreamServerMessage[] = []
private readonly frames = new Deque<RemoteStreamServerMessage>()
private wake: (() => void) | undefined
private failure: Error | undefined
push(frame: RemoteStreamServerMessage): void {
if (this.failure !== undefined) return
this.frames.push(frame)
this.frames.pushBack(frame)
this.wake?.()
this.wake = undefined
}
@@ -286,17 +287,17 @@ class StreamInbox {
fail(error: unknown): void {
if (this.failure !== undefined) return
this.failure = error instanceof Error ? error : new Error(String(error), { cause: error })
this.frames.length = 0
this.frames.clear()
this.wake?.()
this.wake = undefined
}
async next(): Promise<RemoteStreamServerMessage> {
while (this.frames.length === 0) {
while (this.frames.size === 0) {
if (this.failure !== undefined) throw this.failure
await new Promise<void>((resolve) => { this.wake = resolve })
}
return this.frames.shift() as RemoteStreamServerMessage
return this.frames.popFront() as RemoteStreamServerMessage
}
}
+4 -3
View File
@@ -8,6 +8,7 @@
import { randomUUID } from 'node:crypto'
import { Context, Service, symbols } from '@deepseek-ai/cordis'
import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection'
import { Deque } from '@deepseek-ai/dsh-deque'
import type { WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import z from '@deepseek-ai/schemastery'
@@ -882,13 +883,13 @@ type RemoteEventWireFrame =
/** Pull-driven queue owned by one connected Client event generation. */
class RemoteEventQueue {
private readonly frames: RemoteEventWireFrame[] = []
private readonly frames = new Deque<RemoteEventWireFrame>()
private waiter: (() => void) | undefined
private closed = false
push(frame: RemoteEventWireFrame): void {
if (this.closed) return
this.frames.push(frame)
this.frames.pushBack(frame)
this.waiter?.()
}
@@ -903,7 +904,7 @@ class RemoteEventQueue {
signal.addEventListener('abort', abort, { once: true })
try {
while (true) {
while (this.frames.length > 0) yield this.frames.shift() as RemoteEventWireFrame
while (this.frames.size > 0) yield this.frames.popFront() as RemoteEventWireFrame
if (this.closed || signal.aborted) return
await new Promise<void>((resolve) => { this.waiter = resolve })
this.waiter = undefined
@@ -25,6 +25,9 @@
{
"path": "../../typert/protocol"
},
{
"path": "../../util/deque"
},
{
"path": "../../util/crypto"
}
+3
View File
@@ -35,6 +35,9 @@
{
"path": "../../typert/protocol"
},
{
"path": "../../util/deque"
},
{
"path": "../../util/timeout"
}
+1
View File
@@ -56,6 +56,7 @@
],
"dependencies": {
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
"@deepseek-ai/dsh-deque": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^"
},
"peerDependencies": {
+6 -5
View File
@@ -8,6 +8,7 @@ import type {
TypertRemoteEventOutcome,
TypertRemoteEventSource,
} from '@deepseek-ai/dsh-api-gateway'
import { Deque } from '@deepseek-ai/dsh-deque'
import { carrierKeyOf } from '@deepseek-ai/dsh-scope'
import { isJsonValue } from '@deepseek-ai/dsh-session'
import type { JsonValue } from '@deepseek-ai/dsh-session'
@@ -79,13 +80,13 @@ function remoteEventSource(ctx: Context): TypertRemoteEventSource {
/** One pull-driven queue bridging synchronous Cordis listeners to an AsyncIterable. */
class RemoteEventQueue {
private readonly buffer: TypertRemoteEventDispatch[] = []
private readonly buffer = new Deque<TypertRemoteEventDispatch>()
private waiter: (() => void) | undefined
private done = false
push(frame: TypertRemoteEventDispatch): boolean {
if (this.done) return false
this.buffer.push(frame)
this.buffer.pushBack(frame)
this.waiter?.()
return true
}
@@ -93,8 +94,8 @@ class RemoteEventQueue {
private end(reason: unknown): void {
if (this.done) return
this.done = true
const buffered = this.buffer.splice(0)
for (const dispatch of buffered) {
while (this.buffer.size > 0) {
const dispatch = this.buffer.popFront() as TypertRemoteEventDispatch
if ('context' in dispatch) dispatch.reject(reason)
}
this.waiter?.()
@@ -106,7 +107,7 @@ class RemoteEventQueue {
try {
while (true) {
if (this.done || signal.aborted) return
while (this.buffer.length > 0) yield this.buffer.shift() as TypertRemoteEventDispatch
while (this.buffer.size > 0) yield this.buffer.popFront() as TypertRemoteEventDispatch
await new Promise<void>((resolve) => { this.waiter = resolve })
this.waiter = undefined
}
+3
View File
@@ -42,6 +42,9 @@
{
"path": "../../core/scope"
},
{
"path": "../../util/deque"
},
{
"path": "../../interaction/user-approval"
},
@@ -73,6 +73,7 @@
],
"license": "MIT",
"dependencies": {
"@deepseek-ai/dsh-deque": "workspace:^",
"@deepseek-ai/schemastery": "workspace:^",
"zod": "^4.4.3"
},
@@ -2,6 +2,7 @@
import type { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { Deque } from '@deepseek-ai/dsh-deque'
import type { JobSnapshot } from '@deepseek-ai/dsh-jobs'
import type {
JsonValue, Session, SessionEvent, SessionEventMap, SessionId, UserMessage,
@@ -129,13 +130,13 @@ export class SessionControlController {
}
class ControlQueue {
private readonly buffer: SessionControlFrame[] = []
private readonly buffer = new Deque<SessionControlFrame>()
private wake: (() => void) | undefined
private done = false
push(frame: SessionControlFrame): void {
if (this.done) return
this.buffer.push(frame)
this.buffer.pushBack(frame)
const wake = this.wake
this.wake = undefined
wake?.()
@@ -154,14 +155,14 @@ class ControlQueue {
signal.addEventListener('abort', onAbort, { once: true })
try {
while (!this.done && !signal.aborted) {
const frame = this.buffer.shift()
const frame = this.buffer.popFront()
if (frame !== undefined) {
yield frame
continue
}
await new Promise<void>((resolve) => { this.wake = resolve })
}
while (this.buffer.length > 0 && !signal.aborted) yield this.buffer.shift() as SessionControlFrame
while (this.buffer.size > 0 && !signal.aborted) yield this.buffer.popFront() as SessionControlFrame
} finally {
signal.removeEventListener('abort', onAbort)
this.end()
@@ -1,6 +1,7 @@
/** Cold Session history pagination and live-event source. */
import type { Context } from '@deepseek-ai/cordis'
import { Deque } from '@deepseek-ai/dsh-deque'
import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session'
import { isChunkRow, packChunkRuns, type ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
@@ -88,7 +89,7 @@ export class SessionHistoryController {
validateFollowRequest(request)
const { address } = request
const target = addressId(address)
const buffered: SessionEvent[] = []
const buffered = new Deque<SessionEvent>()
let snapshotCursor: number | undefined
let wake: (() => void) | undefined
const notify = (): void => {
@@ -104,7 +105,7 @@ export class SessionHistoryController {
this.closeFollowers.add(close)
const disposeEvent = this.ctx.on('session/event', (session, event) => {
if (session.id !== target) return
buffered.push(event)
buffered.pushBack(event)
notify()
}, { global: true })
const disposeCreated = this.ctx.on('session/created', (session) => {
@@ -115,7 +116,9 @@ export class SessionHistoryController {
const suffix = session.events.slice(snapshotCursor === undefined
? session.firstLiveSeq
: snapshotCursor + 1)
buffered.unshift(...suffix)
for (let index = suffix.length - 1; index >= 0; index -= 1) {
buffered.pushFront(suffix[index] as SessionEvent)
}
notify()
}, { global: true })
const onAbort = (): void => { notify() }
@@ -148,7 +151,7 @@ export class SessionHistoryController {
}
let nextSeq = cursor + 1
while (!follower.closed && !signal.aborted) {
const item = buffered.shift()
const item = buffered.popFront()
if (item === undefined) {
await new Promise<void>((resolve) => { wake = resolve })
continue
@@ -32,6 +32,7 @@
{ "path": "../../interaction/permission-presets" },
{ "path": "../../jobs/jobs" },
{ "path": "../../llm/llm" },
{ "path": "../../util/deque" },
{ "path": "../../util/native-command" },
{ "path": "../../preset/agent-presets" },
{ "path": "../../runtime-diagnostics/invariants" },
@@ -70,6 +70,7 @@
],
"license": "MIT",
"dependencies": {
"@deepseek-ai/dsh-deque": "workspace:^",
"zod": "^4.4.3"
},
"peerDependencies": {
@@ -1,6 +1,7 @@
/** Reconnect-safe Workspace baseline and increment producer. */
import type { Context } from '@deepseek-ai/cordis'
import { Deque } from '@deepseek-ai/dsh-deque'
import type { DomainChanged } from '@deepseek-ai/dsh-storage-domain'
import type { Workspace, WorkspaceRecord } from '@deepseek-ai/dsh-workspace'
import {
@@ -139,14 +140,14 @@ function sameStrings(left: readonly string[], right: readonly string[]): boolean
}
class WorkspaceFollower {
private readonly frames: WorkspaceFollowFrame[] = []
private readonly frames = new Deque<WorkspaceFollowFrame>()
private waiting: (() => void) | undefined
private closed = false
push(frame: WorkspaceFollowFrame): void {
/* v8 ignore next -- closed followers are removed before later publication can reach them. */
if (this.closed) return
this.frames.push(frame)
this.frames.pushBack(frame)
this.waiting?.()
}
@@ -158,7 +159,7 @@ class WorkspaceFollower {
async *read(signal: AbortSignal): AsyncIterable<WorkspaceFollowFrame> {
while (!this.closed && !signal.aborted) {
const frame = this.frames.shift()
const frame = this.frames.popFront()
if (frame !== undefined) {
yield frame
continue
@@ -178,7 +179,7 @@ class WorkspaceFollower {
this.waiting = finish
signal.addEventListener('abort', finish, { once: true })
/* v8 ignore next -- native signals and the private queue cannot change during this synchronous setup. */
if (signal.aborted || this.closed || this.frames.length > 0) finish()
if (signal.aborted || this.closed || this.frames.size > 0) finish()
})
}
}
@@ -20,6 +20,7 @@
{ "path": "../../runtime-diagnostics/invariants" },
{ "path": "../../storage/storage-domain" },
{ "path": "../../typert/protocol" },
{ "path": "../../util/deque" },
{ "path": "../../workspace/workspace" }
]
}
+1 -1
View File
@@ -58,7 +58,7 @@ function styleInjectionModule(
* Everything else under @deepseek-ai/* is either a module-table entry
* (external) or a leak the purity gate rejects.
*/
export const INLINE_SAFE = /^(?:@deepseek-ai\/dsh-(?:file-reference|session|llm|tools|brand|typert-protocol|util-crypto|util-workspace-path)(?:\/|$)|@deepseek-ai\/dsh-token-meter\/client$)/
export const INLINE_SAFE = /^(?:@deepseek-ai\/dsh-(?:file-reference|session|llm|tools|brand|deque|typert-protocol|util-crypto|util-workspace-path)(?:\/|$)|@deepseek-ai\/dsh-token-meter\/client$)/
/**
* Vendored framework libraries: rescoped into @deepseek-ai, so the gate below
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/util/README.md
README.md: dc7e2912ecc77ffb9710a45bf4c127b80b825ab6
README.zh.md: 46878a4d0001e7daf42078074c2e82a5bcaf461c
README.md: 2101454852046ee025aec11f9cd10e794257e21a
README.zh.md: 2eaf26ee27d09cf34bc3d7f8f836594b8f3c762b
+3 -2
View File
@@ -1,5 +1,5 @@
---
description: "Package map for the zero-dependency utility family: atomic file writes, branded ids, harness home paths, the launch environment, native commands, output retention, time zones, and timeouts."
description: "Package map for the zero-dependency utility family: atomic file writes, branded ids, deques, harness home paths, the launch environment, native commands, output retention, time zones, and timeouts."
kind: "package-group"
---
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
The `util/` group gives capability packages shared mechanical primitives instead of duplicate implementations. It covers atomic writes, branded ids, UUIDs, Harness-home paths, launch environments, native commands, output retention, time-zone canonicalization, and timeout handling. Every package here is a library: it registers no service or event, and the consuming capability retains the business semantics.
The `util/` group gives capability packages shared mechanical primitives instead of duplicate implementations. It covers atomic writes, branded ids, deques, UUIDs, Harness-home paths, launch environments, native commands, output retention, time-zone canonicalization, and timeout handling. Every package here is a library: it registers no service or event, and the consuming capability retains the business semantics.
## Table of Contents
@@ -28,6 +28,7 @@ Each package provides one primitive; open a package page for how to use it.
|---|---|
| [`brand/`](brand/README.md) | Compile-time-only nominal brands for ids that cross package boundaries |
| [`crypto/`](crypto/README.md) | Mints RFC 9562 v4 UUIDs from the cross-runtime `crypto.getRandomValues` primitive |
| [`deque/`](deque/README.md) | Provides amortized constant-time queue operations with bounded vacant storage |
| [`home-paths/`](home-paths/README.md) | Resolves the single Harness home and joins shared user-data paths |
| [`launch-environment/`](launch-environment/README.md) | Frozen launch environment that remembers which layer supplied each value |
| [`atomic-write/`](atomic-write/README.md) | Atomic file replacement and cross-process writer locking |
+3 -2
View File
@@ -1,5 +1,5 @@
---
description: "零依赖工具家族的包映射:原子文件写入、品牌化 id、harness 主目录路径、启动环境、原生命令、输出保留、时区与超时。"
description: "零依赖工具家族的包映射:原子文件写入、品牌化 id、双端队列、harness 主目录路径、启动环境、原生命令、输出保留、时区与超时。"
kind: "package-group"
---
@@ -9,7 +9,7 @@ kind: "package-group"
## 概述
`util/` 组为能力包提供共享的机制原语,避免重复实现。它涵盖原子写入、品牌化 id、UUID、Harness home 路径、启动环境、原生命令、输出保留、时区规范化和超时处理。这里的每个包都是库:它不注册服务或事件,业务语义仍由消费它的能力负责。
`util/` 组为能力包提供共享的机制原语,避免重复实现。它涵盖原子写入、品牌化 id、双端队列、UUID、Harness home 路径、启动环境、原生命令、输出保留、时区规范化和超时处理。这里的每个包都是库:它不注册服务或事件,业务语义仍由消费它的能力负责。
## 目录
@@ -28,6 +28,7 @@ kind: "package-group"
|---|---|
| [`brand/`](brand/README.zh.md) | 为跨越包边界的 id 提供仅编译期的名义品牌 |
| [`crypto/`](crypto/README.zh.md) | 基于跨运行时 `crypto.getRandomValues` 原语生成 RFC 9562 v4 UUID |
| [`deque/`](deque/README.zh.md) | 提供摊销常数时间的队列操作和有界空闲存储 |
| [`home-paths/`](home-paths/README.zh.md) | 解析统一的 Harness 主目录并拼接共享的用户数据路径 |
| [`launch-environment/`](launch-environment/README.zh.md) | 冻结的启动环境,记住每个值来自哪一层 |
| [`atomic-write/`](atomic-write/README.zh.md) | 原子文件替换与跨进程写锁 |
+6
View File
@@ -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 packages/util/deque/README.md
README.md: e029ecf574731bb2860dbc56383b93a9282dddfe
README.zh.md: 308f367e91c7345088eea0f5b2bccc0ad3539af7
+104
View File
@@ -0,0 +1,104 @@
---
description: "Circular deque for Host and browser packages that need amortized constant-time queue operations, immediate release of removed entries, and bounded vacant storage."
kind: "package-library"
---
# @deepseek-ai/dsh-deque
English | [中文](README.zh.md)
## Summary
`dsh-deque` lets Host and browser packages drain long-lived in-process queues without moving every remaining entry after each removal. Callers append or prepend entries and remove them from the front with amortized constant-time operations. The deque owns entry order and backing-storage release; each consumer still owns wake-up, failure, cancellation, capacity, and overload behavior.
## Table of Contents
- [Use this package](#use-this-package)
- [Understand the implementation](#understand-the-implementation)
- [Further Exploration](#further-exploration)
- [Model Experience](#model-experience)
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
- [Dev Note](#dev-note)
-----
<a id="use-this-package"></a>
## Use this package
### When to use it
Use `Deque<T>` when entries can accumulate across asynchronous work and the consumer needs FIFO removal, optional front insertion, or explicit queue clearing. Finite local worklists can stay as arrays when their maximum size makes head removal cost irrelevant.
### Entry point
Import the deque, append entries at the tail, and check `size` before removing an entry whose type may include `undefined`:
```ts
import { Deque } from '@deepseek-ai/dsh-deque'
const frames = new Deque<string>()
frames.pushBack('first')
frames.pushFront('before-first')
while (frames.size > 0) {
console.log(frames.popFront())
}
```
The methods do not impose a queue limit or translate consumer failures. See [`src/index.ts`](src/index.ts) for the exact TypeScript contract.
-----
<a id="understand-the-implementation"></a>
## Understand the implementation
<details>
<summary>Implementation internals — click to expand</summary>
The deque stores entries in a circular array. Removing an entry clears that slot immediately, while geometric growth and quarter-full shrinking keep copying work amortized constant time and prevent a head cursor from retaining indefinitely growing vacant storage.
### Source map
| File | Role |
|---|---|
| [`src/index.ts`](src/index.ts) | Circular deque operations and backing-storage lifecycle |
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; ordering and storage lifecycle are exercised by unit tests) |
| [`tests/deque.spec.ts`](tests/deque.spec.ts) | FIFO, front insertion, wrapping, growth, compaction, clearing, and reuse coverage |
| [`benchmarks/drain.ts`](benchmarks/drain.ts) | Reproducible backlog-drain timing across increasing queue sizes |
</details>
-----
<a id="further-exploration"></a>
## Further Exploration
- [Utility package map](../README.md) — the other zero-dependency primitives shared across package groups.
- [Linear stream queue decision](../../../.agents/notes/implemented/bug-fix/2026-08-28-linear-stream-queue-drain.md) — why production streams use this deque instead of array head removal.
-----
<a id="model-experience"></a>
## Model Experience
None, as this in-process collection registers nothing model-facing.
#### KV Cache effect
Nothing here enters a model request, so provider cache reuse is unaffected.
## Known Limitations and Deferred Work
<a id="known-limitations-and-deferred-work"></a>
- **No capacity policy** — the deque does not bound, coalesce, or reject entries; each consumer must define overload behavior appropriate to its stream.
<a id="dev-note"></a>
### Dev Note
<details>
<summary>Working context for maintainers — click to expand</summary>
None.
</details>
+104
View File
@@ -0,0 +1,104 @@
---
description: "供 Host 和浏览器包使用的环形双端队列,提供摊销常数时间的队列操作、已移除条目的即时释放和有界空闲存储。"
kind: "package-library"
---
# @deepseek-ai/dsh-deque
[English](README.md) | 中文
## 概述
`dsh-deque` 让 Host 和浏览器包可以排空长期存在的进程内队列,而无需在每次移除后移动所有剩余条目。调用方可以追加或前插条目,并以摊销常数时间从前端移除。双端队列负责条目顺序和后备存储释放;唤醒、失败、取消、容量和过载行为仍由各消费方负责。
## 目录
- [使用本包](#use-this-package)
- [理解实现](#understand-the-implementation)
- [进一步探索](#further-exploration)
- [模型体验](#model-experience)
- [已知限制与延期工作](#known-limitations-and-deferred-work)
- [开发备注](#dev-note)
-----
<a id="use-this-package"></a>
## 使用本包
### 何时使用
当条目可能在异步工作期间持续积累,且消费方需要 FIFO 移除、可选前插或显式清空队列时,使用 `Deque<T>`。如果有限本地工作列表的最大规模使头部移除成本无关紧要,它可以继续使用数组。
### 入口
导入双端队列,在尾部追加条目;当条目类型可能包含 `undefined` 时,在移除前检查 `size`
```ts
import { Deque } from '@deepseek-ai/dsh-deque'
const frames = new Deque<string>()
frames.pushBack('first')
frames.pushFront('before-first')
while (frames.size > 0) {
console.log(frames.popFront())
}
```
这些方法不施加队列限制,也不转换消费方失败。准确的 TypeScript 约定见 [`src/index.ts`](src/index.ts)。
-----
<a id="understand-the-implementation"></a>
## 理解实现
<details>
<summary>实现细节——点击展开</summary>
双端队列把条目存入环形数组。移除条目会立即清空对应槽位;按几何级数扩容并在四分之一满时缩容,使复制工作保持摊销常数时间,并防止头游标保留持续增长的空闲存储。
### 源码地图
| 文件 | 职责 |
|---|---|
| [`src/index.ts`](src/index.ts) | 环形双端队列操作与后备存储生命周期 |
| [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;顺序和存储生命周期由单元测试覆盖) |
| [`tests/deque.spec.ts`](tests/deque.spec.ts) | FIFO、前插、环绕、扩容、压缩、清空和复用覆盖 |
| [`benchmarks/drain.ts`](benchmarks/drain.ts) | 随队列规模增长的可复现 backlog 排空计时 |
</details>
-----
<a id="further-exploration"></a>
## 进一步探索
- [工具包映射](../README.zh.md)——跨包组共享的其他零依赖原语。
- [线性流队列决策](../../../.agents/notes/implemented/bug-fix/2026-08-28-linear-stream-queue-drain.zh.md)——生产流为何使用本双端队列而非数组头部移除。
-----
<a id="model-experience"></a>
## 模型体验
无,因为这个进程内集合不注册任何面向模型的内容。
#### KV 缓存影响
这里的内容不会进入模型请求,因此不影响提供方缓存复用。
## 已知限制与延期工作
<a id="known-limitations-and-deferred-work"></a>
- **没有容量策略**——双端队列不会限制、合并或拒绝条目;每个消费方必须定义适合其流的过载行为。
<a id="dev-note"></a>
### 开发备注
<details>
<summary>维护者的工作上下文——点击展开</summary>
无。
</details>
+36
View File
@@ -0,0 +1,36 @@
import { performance } from 'node:perf_hooks'
import { Deque } from '../src/index.ts'
const sizes = [250_000, 500_000, 1_000_000, 2_000_000]
const samples = 5
function drain(size: number): { readonly milliseconds: number; readonly checksum: number } {
const deque = new Deque<number>()
for (let value = 0; value < size; value += 1) deque.pushBack(value)
const started = performance.now()
let checksum = 0
while (deque.size > 0) checksum += deque.popFront() as number
return { milliseconds: performance.now() - started, checksum }
}
function median(values: readonly number[]): number {
const ordered = values.toSorted((left, right) => left - right)
return ordered[Math.floor(ordered.length / 2)] as number
}
drain(sizes[0] as number)
for (const size of sizes) {
const expected = size * (size - 1) / 2
const durations: number[] = []
for (let sample = 0; sample < samples; sample += 1) {
const result = drain(size)
if (result.checksum !== expected) throw new Error(`invalid checksum for ${String(size)} entries`)
durations.push(result.milliseconds)
}
const milliseconds = median(durations)
console.log(JSON.stringify({
size,
medianMilliseconds: Number(milliseconds.toFixed(3)),
nanosecondsPerEntry: Number((milliseconds * 1_000_000 / size).toFixed(3)),
}))
}
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@deepseek-ai/dsh-deque",
"description": "Zero-dependency circular deque with amortized constant-time end operations and bounded vacant storage",
"version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/util/deque"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}
+95
View File
@@ -0,0 +1,95 @@
/**
* Zero-dependency circular deque for queues that retain entries across asynchronous work.
* @module @deepseek-ai/dsh-deque
*/
const MIN_CAPACITY = 16
/**
* A circular deque with amortized constant-time insertion and removal.
* Removed entries are cleared immediately, and sparse storage shrinks after
* the live entry count reaches one quarter of its capacity.
*/
export class Deque<T> {
private buffer = new Array<T | undefined>(MIN_CAPACITY)
private head = 0
private count = 0
/** Number of entries available to remove. */
get size(): number {
return this.count
}
/**
* Append one entry after the current tail.
* @param value - entry to append.
*/
pushBack(value: T): void {
this.ensureCapacity()
const tail = this.head + this.count
this.buffer[tail < this.buffer.length ? tail : tail - this.buffer.length] = value
this.count += 1
}
/**
* Insert one entry before the current head.
* @param value - entry to prepend.
*/
pushFront(value: T): void {
this.ensureCapacity()
this.head = this.head === 0 ? this.buffer.length - 1 : this.head - 1
this.buffer[this.head] = value
this.count += 1
}
/**
* Remove the current head entry and clear its retained reference.
* Callers whose element type includes `undefined` use {@link size} to
* distinguish an empty deque from an `undefined` entry.
* @returns the removed entry, or `undefined` when the deque is empty.
*/
popFront(): T | undefined {
if (this.count === 0) return undefined
const value = this.buffer[this.head] as T
this.buffer[this.head] = undefined
this.head += 1
if (this.head === this.buffer.length) this.head = 0
this.count -= 1
this.compact()
return value
}
/** Drop every entry and release the current backing storage. */
clear(): void {
this.buffer = new Array<T | undefined>(MIN_CAPACITY)
this.head = 0
this.count = 0
}
private ensureCapacity(): void {
if (this.count < this.buffer.length) return
this.resize(this.buffer.length * 2)
}
private compact(): void {
if (this.count === 0) {
this.head = 0
return
}
if (this.buffer.length > MIN_CAPACITY && this.count <= this.buffer.length / 4) {
this.resize(Math.max(MIN_CAPACITY, this.buffer.length / 2))
}
}
private resize(capacity: number): void {
const next = new Array<T | undefined>(capacity)
let source = this.head
for (let index = 0; index < this.count; index += 1) {
next[index] = this.buffer[source]
source += 1
if (source === this.buffer.length) source = 0
}
this.buffer = next
this.head = 0
}
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-deque`.
* @module @deepseek-ai/dsh-deque/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-deque'
/** Cordis companion plugin name. */
export const name = 'deque-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: this pure utility owns no event stream or mutable data outside each deque;
* its ordering and storage lifecycle are exercised by unit tests.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
+94
View File
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest'
import { Deque } from '@deepseek-ai/dsh-deque'
function backingStorage<T>(deque: Deque<T>): readonly (T | undefined)[] {
// Storage retention is the behavior under test and has no public query API.
return (deque as unknown as { readonly buffer: readonly (T | undefined)[] }).buffer
}
describe('Deque', () => {
it('removes tail-appended entries in FIFO order', () => {
const deque = new Deque<number>()
expect(deque.size).toBe(0)
expect(deque.popFront()).toBeUndefined()
deque.pushBack(1)
deque.pushBack(2)
expect(deque.size).toBe(2)
expect(deque.popFront()).toBe(1)
expect(deque.popFront()).toBe(2)
expect(deque.size).toBe(0)
})
it('prepends entries before the existing head', () => {
const deque = new Deque<number>()
deque.pushBack(3)
deque.pushFront(2)
deque.pushFront(1)
expect([deque.popFront(), deque.popFront(), deque.popFront()]).toEqual([1, 2, 3])
})
it('appends through the array boundary without growing', () => {
const deque = new Deque<number>()
for (let value = 0; value < 8; value += 1) deque.pushBack(value)
for (let value = 0; value < 6; value += 1) expect(deque.popFront()).toBe(value)
for (let value = 8; value <= 16; value += 1) deque.pushBack(value)
for (const value of [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) {
expect(deque.popFront()).toBe(value)
}
})
it('preserves order across wrapping, growth, and sparse compaction', () => {
const deque = new Deque<number>()
for (let value = 0; value < 32; value += 1) deque.pushBack(value)
for (let value = 0; value < 24; value += 1) expect(deque.popFront()).toBe(value)
expect(backingStorage(deque)).toHaveLength(16)
for (let value = 32; value < 128; value += 1) deque.pushBack(value)
for (let value = 24; value < 128; value += 1) expect(deque.popFront()).toBe(value)
expect(deque.size).toBe(0)
expect(backingStorage(deque)).toHaveLength(16)
})
it('releases a removed reference before sparse compaction', () => {
const deque = new Deque<object>()
const removed = {}
deque.pushBack(removed)
deque.pushBack({})
expect(deque.popFront()).toBe(removed)
expect(backingStorage(deque)).not.toContain(removed)
expect(backingStorage(deque)).toHaveLength(16)
})
it('drops retained storage and remains reusable after clear', () => {
const deque = new Deque<object>()
const retained = {}
deque.pushBack(retained)
for (let index = 1; index < 64; index += 1) deque.pushBack({ index })
const grownStorage = backingStorage(deque)
deque.clear()
expect(deque.size).toBe(0)
expect(deque.popFront()).toBeUndefined()
expect(backingStorage(deque)).not.toBe(grownStorage)
expect(backingStorage(deque)).not.toContain(retained)
expect(backingStorage(deque)).toHaveLength(16)
const value = {}
deque.pushBack(value)
expect(deque.popFront()).toBe(value)
})
it('uses size to distinguish an undefined entry from an empty deque', () => {
const deque = new Deque<undefined>()
deque.pushBack(undefined)
expect(deque.size).toBe(1)
deque.popFront()
expect(deque.size).toBe(0)
})
})
@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as DequeInvariant from '../src/invariant.ts'
describe('deque invariant companion', () => {
it('registers its explained empty runtime invariant', async () => {
const ctx = new Context()
await ctx.plugin(InvariantRegistry)
const fiber = await ctx.plugin(DequeInvariant)
expect(() => {
ctx.invariants.register('@deepseek-ai/dsh-deque', () => {})
}).toThrow(/already registered/)
await fiber.dispose()
await ctx.fiber.dispose()
})
})
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../runtime-diagnostics/invariants"
}
]
}
+21
View File
@@ -624,6 +624,9 @@ importers:
packages/api/gateway:
dependencies:
'@deepseek-ai/dsh-deque':
specifier: workspace:^
version: link:../../util/deque
'@deepseek-ai/dsh-timeout':
specifier: workspace:^
version: link:../../util/timeout
@@ -670,6 +673,9 @@ importers:
'@deepseek-ai/dsh-api-session-controller':
specifier: workspace:^
version: link:../session-controller
'@deepseek-ai/dsh-deque':
specifier: workspace:^
version: link:../../util/deque
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
@@ -743,6 +749,9 @@ importers:
packages/api/session-controller:
dependencies:
'@deepseek-ai/dsh-deque':
specifier: workspace:^
version: link:../../util/deque
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
@@ -886,6 +895,9 @@ importers:
packages/api/workspace-controller:
dependencies:
'@deepseek-ai/dsh-deque':
specifier: workspace:^
version: link:../../util/deque
zod:
specifier: ^4.4.3
version: 4.4.3
@@ -9690,6 +9702,15 @@ importers:
specifier: workspace:^
version: link:../../runtime-diagnostics/invariants
packages/util/deque:
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../runtime-diagnostics/invariants
packages/util/home-paths:
devDependencies:
'@deepseek-ai/cordis':
+20 -2
View File
@@ -1,4 +1,4 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
@@ -24,6 +24,24 @@ function writeJson(root: string, path: string, value: unknown): void {
writeFileSync(absolute, `${JSON.stringify(value, null, 2)}\n`)
}
function processCanExecute(pid: number): boolean {
try {
process.kill(pid, 0)
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ESRCH') return false
throw error
}
if (process.platform !== 'linux') return true
try {
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8')
const state = stat.slice(stat.lastIndexOf(')') + 2).split(/\s+/, 1)[0]
return !/^[ZXx]$/.test(state ?? '')
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
throw error
}
}
describe('npm resolution benchmark', () => {
it('parses repeat, timeout, threshold, and ref options', () => {
expect(parseBenchmarkOptions([])).toEqual({ runs: 1, timeoutMs: 300_000 })
@@ -168,7 +186,7 @@ describe('npm resolution benchmark', () => {
expect(result.timedOut).toBe(true)
expect(result.signal).toBe('SIGKILL')
expect(() => { process.kill(reportedPid, 0) }).toThrow()
await expect.poll(() => processCanExecute(reportedPid), { timeout: 5_000 }).toBe(false)
} finally {
if (descendantPid !== undefined && Number.isSafeInteger(descendantPid)) {
try {
+2 -1
View File
@@ -91,9 +91,10 @@ describe('client bundle purity gate', () => {
expect(() => resolveId('@deepseek-ai/dsh-client-web-react/store')).toThrow(/purity/)
})
it('lets inline-safe wire layers inline', () => {
it('lets inline-safe libraries inline', () => {
expect(resolveId('@deepseek-ai/dsh-session/surface')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-brand')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-deque')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-token-meter/client')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-token-meter')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-token-meter/client/internal')).toThrow(/purity/)
+1
View File
@@ -78,6 +78,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
'packages/util/atomic-write': 'Zero-dependency filesystem write utility.',
'packages/util/brand': 'Type-only branding primitive erased at compile time.',
'packages/util/crypto': 'Zero-dependency identifier minting utility.',
'packages/util/deque': 'Zero-dependency circular deque utility.',
'packages/util/home-paths': 'Zero-dependency harness-home path resolver.',
'packages/util/launch-environment': 'Zero-dependency environment resolver.',
'packages/util/native-command': 'Host-side subprocess runner utility.',
+1
View File
@@ -35,6 +35,7 @@ const CONFIGURATION_ONLY_DEV_DEPENDENCIES = {
const SAFE_HOST_DEPENDENCY_EXPORTS = {
'@deepseek-ai/dsh-api-session-controller/remote-events': ['SESSION_CONTROLLER_REMOTE_EVENTS'],
'@deepseek-ai/dsh-credentials': ['credentialKey'],
'@deepseek-ai/dsh-deque': ['Deque'],
'@deepseek-ai/dsh-llm': ['MessageId', 'callConfigEquals', 'deepFreeze', 'freezeMessage'],
'@deepseek-ai/dsh-llm/brand': ['ToolCallId'],
'@deepseek-ai/dsh-session': ['isJsonValue'],
@@ -109,6 +109,7 @@ describe('package dependency scope', () => {
'@deepseek-ai/dsh-client-ui-theme': ['@deepseek-ai/dsh-api-remotes'],
'@deepseek-ai/dsh-client-ui-tool': ['@deepseek-ai/dsh-api-remotes'],
})
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-deque']).toEqual(['Deque'])
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/schemastery']).toEqual(['default'])
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-session/types']).toEqual([
'SessionId',
@@ -55,6 +55,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/code-runtime/code-runtime-python': { kind: 'indirect', reason: 'The CPython subprocess backend delegates model rendering to PTC mode in dsh-tools.' },
'packages/client/ui-agent-preset': { kind: 'indirect', reason: 'Browser-side settings row; the preset it selects owns every model-facing effect.' },
'packages/util/crypto': { kind: 'indirect', reason: 'Pure identifier minting; the ids consumers mint with it never enter prompts as semantic content.' },
'packages/util/deque': { kind: 'none', reason: 'In-process collection primitive; registers nothing model-facing.' },
'packages/util/time': { kind: 'indirect', reason: 'Pure zone validation; the consumer that records a canonical zone owns the model-visible line derived from it.' },
'packages/core/agent-default-model': { kind: 'indirect', reason: 'The service supplies a ModelSelection; request assembly and adapters own the model-visible request.' },
'packages/llm/deepseek-llm-api-extensions': { kind: 'indirect', reason: 'The registry contributes model-hidden provider fields; dsh-llm-deepseek owns their wire placement.' },
+2
View File
@@ -307,6 +307,8 @@
"@deepseek-ai/dsh-credentials-local/invariant": ["./packages/credentials/credentials-local/src/invariant.ts"],
"@deepseek-ai/dsh-deepseek-llm-api-extensions": ["./packages/llm/deepseek-llm-api-extensions/src"],
"@deepseek-ai/dsh-deepseek-llm-api-extensions/invariant": ["./packages/llm/deepseek-llm-api-extensions/src/invariant.ts"],
"@deepseek-ai/dsh-deque": ["./packages/util/deque/src"],
"@deepseek-ai/dsh-deque/invariant": ["./packages/util/deque/src/invariant.ts"],
"@deepseek-ai/dsh-e2b": ["./packages/e2b/e2b/src"],
"@deepseek-ai/dsh-e2b/invariant": ["./packages/e2b/e2b/src/invariant.ts"],
"@deepseek-ai/dsh-file-reference": ["./packages/context/file-reference/src"],
+1
View File
@@ -133,6 +133,7 @@
{ "path": "./packages/util/time" },
{ "path": "./packages/util/timeout" },
{ "path": "./packages/util/crypto" },
{ "path": "./packages/util/deque" },
{ "path": "./packages/util/workspace-path" },
{ "path": "./packages/util/output-retention" },
{ "path": "./packages/util/atomic-write" },