dsh-date-wrapper 0.1.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ # dsh-date-wrapper bundle patch:一行装配进 profile。
2
+ #
3
+ # host 半(src/index.js)把当前日期注册为一条动态运行上下文,
4
+ # 由平台并入自己那条「Current runtime context」快照消息,文本形如:
5
+ # Current date: 2026-09-08 Asia/Shanghai Tuesday
6
+ #
7
+ # 设计取舍:不加载 @deepseek-ai/dsh-time-context。Cordis 的 prepend 用 unshift,
8
+ # 子 fiber 异步加载后注册的监听器会跑到 wrapper 外面,verbose 文本既看不到也删不掉
9
+ # (详见 .agents/plans/dsh-date-wrapper/findings.md 的 D1)。
10
+ #
11
+ # config 随行下发;改时区只需改这里。
12
+ - insert:
13
+ - id: date-wrapper
14
+ name: dsh-date-wrapper
15
+ config:
16
+ timeZone: Asia/Shanghai
@@ -0,0 +1,387 @@
1
+ # DSH 会话、JSONL 与请求组装机制
2
+
3
+ > 本文档描述 DeepSeek Harness 的**会话事件日志 ⇄ JSONL 持久化**、**surface 派生**、**请求组装**与**运行上下文快照的取代机制**。
4
+ > 所有结论都带 `文件:行号` 依据,路径以本机安装为基准:
5
+ > `[DSH]` = `C:\nvm\v22.22.1\node_modules\@deepseek-ai\dsh\`,`[PKG]` = `[DSH]node_modules\@deepseek-ai\`。
6
+ > 实测数据来自一个真实会话(373 步 / 822 个 surface 事件 / 2.1 MB 压缩日志),见 §8。
7
+
8
+ ---
9
+
10
+ ## 0. 一页速览
11
+
12
+ ```
13
+ 用户输入 / steering
14
+ │
15
+ ▼
16
+ inbox(两个列表:next-step / next-turn)
17
+ │ claim(target, turn) → 本次请求的「claimed 批次」
18
+ ▼
19
+ agent/pre-step waterfall(13 个监听器,8 个会改批次)
20
+ │ 默认决策在最内层追加 runtime-context 快照
21
+ ▼
22
+ decision.messages ← 这一步要落盘的 user/message 们
23
+ │ session.append(..., { surfaceOp:'append' })
24
+ ▼
25
+ 会话事件日志(唯一真相,append-only)
26
+ │ │
27
+ │ surfaceOp: append │ surfaceOp: replace{start,end} ← 只遮蔽「表面」,不删日志
28
+ ▼ ▼
29
+ surface(模型可见消息序列)
30
+ │ deriveMessages()
31
+ ▼
32
+ buildRequest(turn, step, tools, system, deriveMessages(), signal)
33
+ │
34
+ ├── request/header ← 仅在 system/tools/config 变化时追加(含整份 system + 工具 schema)
35
+ ├── request/context ← 仅在 provider/model/contextWindow 变化时追加
36
+ ▼
37
+ deepFreeze(request) + markAgentLoopRequest(request)
38
+ │
39
+ ▼
40
+ llm/stream waterfall → adapter
41
+ │ (只允许包流;agent-loop invariant 断言 messages 必须等于 deriveMessages())
42
+ ▼
43
+ 模型
44
+ │
45
+ ▼
46
+ assistant/message + tool/call + tool/result → 再次 session.append
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 1. 会话事件日志:唯一真相
52
+
53
+ ### 1.1 事件形状
54
+
55
+ ```js
56
+ { type, seq, time, data, ...surfaceMetadata } // surfaceMetadata = { surfaceOp?, sourceEventSeqs? }
57
+ ```
58
+
59
+ `session.append()` 做三件事(`[PKG]dsh-session/lib/types/index.js:484-509`):
60
+
61
+ 1. `snapshotJsonValue(data)` —— 必须**无损 JSON 序列化**(不能有 `undefined`、函数、`Date`、循环、非有限数);
62
+ 2. `assertSupportedRequestHeader()` —— 拒绝已废弃的历史格式;
63
+ 3. `deepFreeze({ type, seq, time, data, ... })` —— **日志自己深冻结快照**,调用方不需要预冻结,也不会被后续别名修改。
64
+
65
+ > 日志是 durable source of truth:坏事件在 `append` 处就失败,而不是等到刷盘时(`:478-483` 的注释)。
66
+
67
+ ### 1.2 主要事件类型(本会话实测计数)
68
+
69
+ | 事件 | 说明 | 本会话 |
70
+ |---|---|---|
71
+ | `turn/start` · `turn/end` | 轮次边界 | 10 / 9 |
72
+ | `step/start` · `step/end` | 步骤边界(= 一次模型调用) | 231 / 230 |
73
+ | `user/message` | 用户输入 + 所有注入上下文 | 20 |
74
+ | `assistant/message` | 模型回复 | 231 |
75
+ | `assistant/chunk` / `text-chunks` / `reasoning-chunks` / `tool-call-chunks` | 流式增量(后三者是 `packChunks` 打包行) | 1756 / 164 / 1202 / 851 |
76
+ | `tool/call` · `tool/result` | 工具调用与结果 | 275 / 274 |
77
+ | `request/header` | 请求头(system + tools + config)变化 | 1 |
78
+ | `request/context` | provider/model/contextWindow 变化 | 1 |
79
+ | `agent/inbox/spliced` | 入队/出队审计 | 27 |
80
+
81
+ ### 1.3 落盘路径
82
+
83
+ `session.append` 后,持久化层把事件批量写成 JSONL:
84
+
85
+ - 目录:`<sessionRoot>/<project>/<session-id>/`(`[PKG]dsh-session-persistence-jsonl/lib/index.js:145-157`);
86
+ - 文件:`session.jsonl`(`compression: none`)或 `session.jsonl.zstd`(默认)。
87
+
88
+ ---
89
+
90
+ ## 2. 会话 ⇄ JSONL 的转换
91
+
92
+ ### 2.1 物理布局:header 帧 + N 个批次帧
93
+
94
+ ```
95
+ session.jsonl.zstd
96
+ ├── [zstd frame #1] 首行 header(JSON) ← encodeMaterialization
97
+ ├── [zstd frame #2] 首批事件行 ← encodeMaterialization
98
+ ├── [zstd frame #3] 一次 append 批次的事件行 ← encodeEventBatch
99
+ ├── ...
100
+ └── [zstd frame #N] ...
101
+ ```
102
+
103
+ - `encodeMaterialization()`(`:1170-1178`):header 行与首批事件**分成两个独立 zstd 帧**,刻意不合并帧边界;
104
+ - `encodeEventBatch()`(`:1179-1183`):每次持久化 append = 一批事件 → `eventLines() + "\n"` → 一个 zstd 帧;
105
+ - 所以一个长会话是**多帧 zstd 拼接**:本会话实测 **2731 个帧**(`0x28 0xB5 0x2F 0xFD` 魔数计数),普通解压只解出第一帧——需要按魔数切帧或走 DSH 自带的 `zstd-private-decoder`(`:336-383`)。
106
+
107
+ ### 2.2 header 行
108
+
109
+ ```js
110
+ { type: 'session', version, id, createdAt, cwd?, parentSession?, seedLength?, origin?, delegationDepth, agentPreset? }
111
+ ```
112
+ (`toHeaderLine()`,`:36-49`)—— 首行必须是合法 header,否则日志判为损坏(`:187-198`)。
113
+
114
+ ### 2.3 事件行与 `packChunks`
115
+
116
+ ```js
117
+ eventLines(events, packChunks) {
118
+ return (packChunks ? packChunkRuns(events) : events).map((r) => JSON.stringify(r)).join("\n")
119
+ }
120
+ ```
121
+ (`:160-172`)
122
+
123
+ - 默认 `packChunks: true`:连续的流式 delta 会被**打包成 `text-chunks` / `reasoning-chunks` / `tool-call-chunks` 存储行**,不再是一事件一行;
124
+ - 关闭时逐事件一行,与打包前的字节布局一致;读取端对布局不敏感(`scanLog` 总是先解码再读)。
125
+
126
+ ### 2.4 崩溃安全
127
+
128
+ - 写入后 `fsync`;部分写或同步失败时**回滚到之前的文件长度**再抛错,因为游标未变会重试该批次,残留半行会造成重复 `seq`(`:1195-1199` 起)。
129
+
130
+ ### 2.5 读回与恢复
131
+
132
+ - 解码:多帧 zstd → 逐行 JSON → 事件对象;header 行单独校验(`refuseForeignFormatVersion()` 拒绝未来格式);
133
+ - 形状校验发生在**导入边界**:`adoptSessionEvent` / `snapshotSessionEvent` → `assertMessageEventShape()`(`[PKG]dsh-session/lib/types/index.js:219-247`)。`user/message` 要求 `id` 非空串、`role` 匹配、`source.kind` 非空串、`content` 是数组;**不检查** `source.form` / `sections`;
134
+ - 历史消息缺 `id` 时会被补铸确定性 id `legacy-message:${sessionId}:${seq}`(`[PKG]dsh-session-persistence/lib/index.js:665-678`、`:518-521`);
135
+ - 由于日志可完整重建,**恢复 = 重放**,不需要额外快照文件。
136
+
137
+ ---
138
+
139
+ ## 3. Surface:从日志派生「模型可见的消息」
140
+
141
+ 日志里并非每条事件都进对话。`surfaceOp` 决定可见性(`[PKG]dsh-session/lib/types/surface.js`):
142
+
143
+ | op | 含义 |
144
+ |---|---|
145
+ | 缺失 | 不是 surface 事件(如 `step/start`、`tool/call`),不参与消息派生 |
146
+ | `'append'` | 追加到可见序列 |
147
+ | `{ op: 'replace', start, end, sourceEventSeqs? }` | **遮蔽** `[start, end]` 区间的既有可见事件;事件本身仍留在日志里 |
148
+
149
+ - surface 事件类型是固定的那几种(`user/message`、`assistant/message`、`tool/result` 等,`:12`);
150
+ - `deriveMessages()` 增量遍历 surface 节点,产出模型可见的消息数组,并在 `replaceGeneration` 变化时重建缓存(`[PKG]dsh-session/lib/types/index.js` 的 `deriveMessages()`);
151
+ - **`replace` 的真实使用者**:压缩检查点 —— `session.append('user/message', checkpoint, { surfaceOp: { op:'replace', start, end }, sourceEventSeqs:[…] })`(`[PKG]dsh-compaction-basic/lib/index.js:606-611`)。它把被折叠的一段历史从**表面**遮掉,日志里一条不删。
152
+
153
+ > 这就是「日志 ≠ 对话」的分界线:日志只追加,对话靠 `surfaceOp` 重新投影。
154
+
155
+ ---
156
+
157
+ ## 4. 请求组装(`buildRequest`)
158
+
159
+ 每次 step 前,`[PKG]dsh-agent-loop/lib/index.js:693-762` 组装三块:
160
+
161
+ | 块 | 来源 | 变化时记录 |
162
+ |---|---|---|
163
+ | `system` | `renderPrompt(assembly)` 把 sections 用 `\n\n` 拼起来(`[PKG]dsh-system-prompt/lib/index.js:65-67`) | `request/header.system` |
164
+ | `tools` | 各 provider 提供的 schema,按 scope 收集 + `orderTools()` 排序(`[PKG]dsh-tools:2595`、`[PKG]dsh-system-prompt:280`) | `request/header.tools` |
165
+ | `messages` | `session.deriveMessages()`(即 §3 的 surface 派生) | 不单独记录(就是日志本身) |
166
+
167
+ ### 4.1 `request/header`:只在变化时写
168
+
169
+ ```js
170
+ const header = canonicalHeader({ config, adapterDefaults?, system?, tools? })
171
+ if (!requestHeaderLogged) append('request/header', { header, reason: 'initial' | 'resume' })
172
+ else if (!headerEquals(baseline, header)) append('request/header', { header, reason: 'change' })
173
+ ```
174
+ (`:725-741`)
175
+
176
+ `headerEquals` 比较 `config`、`adapterDefaults`、**`system` 全串**、**`tools` 全量 schema**(`:548` 起)。所以:
177
+
178
+ - 系统提示词或工具表**不变**时,一条 header 都不写(本会话 373 步只有 **1 条** header:`system` 16853 字符 + **56** 个工具 schema);
179
+ - 一旦变化,追加的是**整份** header —— 这是「改系统提示词代价高」的直接原因。
180
+
181
+ ### 4.2 请求对象被冻结并打标
182
+
183
+ ```js
184
+ request: markAgentLoopRequest(deepFreeze({ ...header.config, messages, system?, tools?, sessionId, signal }))
185
+ ```
186
+ (`:751-761`)—— `deepFreeze` 后不可改;`markAgentLoopRequest` 把对象登记进 WeakSet(`[PKG]dsh-llm/lib/index.js:87-90`),供下游按身份识别。
187
+
188
+ ### 4.3 `llm/stream` 只能包流,不能改请求
189
+
190
+ ```js
191
+ streamWithRegistration(options, prepared) {
192
+ return this.ctx.waterfall(this, 'llm/stream', options, () => this.adapterStream(options, prepared))
193
+ }
194
+ ```
195
+ (`[PKG]dsh-llm/lib/index.js:1636-1641`)
196
+
197
+ `next` 是零参闭包、`options` 已冻结 → 监听器只能包装返回的流(in-box 的 `llm-invariant` 就是 `validateStream(next())`)。
198
+
199
+ ### 4.4 「请求必须等于日志派生」是硬契约
200
+
201
+ `[PKG]dsh-agent-loop/lib/invariant.js:15-33` 在 `llm/stream` 上断言:
202
+
203
+ ```js
204
+ if (!isAgentLoopRequest(options)) return next()
205
+ if (!Object.isFrozen(options)) fail('a loop-built request must be frozen')
206
+ const expected = session.deriveMessages()
207
+ if (JSON.stringify(options.messages) !== JSON.stringify(expected))
208
+ fail('llm request … diverges from the dispatch-time durable derivation (log-reconstruction desync)')
209
+ if (!(options.model === header.config.model && options.system === header.system && …)) fail(…)
210
+ ```
211
+
212
+ → **任何「只发给模型、不落日志」的消息注入都会被判为违约**。想影响模型看到的 messages,只能通过日志(`agent/pre-step` 追加消息);想影响 system,只能通过 `request/header`(即 `systemPrompt.section`)。
213
+
214
+ ---
215
+
216
+ ## 5. pre-step 批次:`decision.messages`
217
+
218
+ ### 5.1 组装顺序
219
+
220
+ ```js
221
+ const claimed = this.inbox.claim(target, position.turn)
222
+ const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
223
+ const context = this.runtimeContext.project(joinContextSections(sections), sections)
224
+ const decision = await this.dispatch.waterfall('agent/pre-step',
225
+ { messages: claimed, ...position, signal },
226
+ () => Promise.resolve({ kind: 'enter', messages: context === undefined ? claimed : [...claimed, context] }))
227
+ ```
228
+ (`[PKG]dsh-agent-loop/lib/index.js:492-514`)
229
+
230
+ ### 5.2 `claim` 规则
231
+
232
+ ```js
233
+ claim(target, turn) {
234
+ const claimed = this.mutate('next-step', 0, this.nextStep.length, [], false) // next-step 全部
235
+ if (target === 'next-turn') claimed.push(...this.mutate('next-turn', 0, 1, [], false)) // 至多 1 条
236
+ …
237
+ }
238
+ ```
239
+ (`[PKG]dsh-agent/lib/types/inbox.js:50-57`)
240
+
241
+ - `next-step`:`steer()` / `inject()` 的目标,**没有条数上限**(连续插队会累积);
242
+ - `next-turn`:`followup()` 的目标,每次 claim **最多取 1 条**;
243
+ - `turn()` 里 `target` 首步是 `'next-turn'`,之后变 `'next-step'`(`:529`、`:572`)。
244
+
245
+ ### 5.3 谁在动批次
246
+
247
+ 本版本 13 个 `agent/pre-step` 监听器,其中 8 个会改批次:`time-context`(append)、`tmux-context`(prepend)、`session-reference`(改写+插入)、`tool-skill`(append/replace/remove)、`agent-instructions`(插在最后一条 claimed 之后)、`tool-cordis`(append)、`plan-mode`(append)、以及运行上下文默认追加。
248
+
249
+ ### 5.4 实测:大多数步骤批次是空的
250
+
251
+ | 每步 `user/message` 条数 | 步数 |
252
+ |---|---|
253
+ | 0 | 349 |
254
+ | 1 | 20 |
255
+ | 2 | 2 |
256
+ | 3 | 1 |
257
+ | 6 | 1 |
258
+
259
+ 工具调用后的续跑没有新输入 → 批次为空 → `for (const m of decision.messages) session.append(...)`(`:554`)空转,但 `step/start` 照常开、模型继续。
260
+
261
+ ---
262
+
263
+ ## 6. 运行上下文快照与「取代」机制
264
+
265
+ ### 6.1 它是「动态事实」的合并通道
266
+
267
+ 四个贡献者(`systemPrompt.context({ name, order, text })`,`[PKG]dsh-system-prompt/lib/index.js:196-199`):
268
+
269
+ | 来源 | name | order |
270
+ |---|---|---|
271
+ | `dsh-sandbox-policy` | `sandbox:policy` | 110 |
272
+ | `dsh-user-approval` | `approval:policy` | 115 |
273
+ | `dsh-date-wrapper` | `date-wrapper:date` | 116 |
274
+ | `dsh-subagent` | `subagent:delegation` | 120 |
275
+
276
+ 每次 assemble 重新求值(`text` 可以是函数,`:271`),按 order 排序后用 `\n\n` 拼成一条快照文本。
277
+
278
+ ### 6.2 只在文本变化时才产生消息
279
+
280
+ ```js
281
+ project(current, sections) {
282
+ if (this.retained === void 0 && current.length === 0) return
283
+ const snapshot = current.length === 0 ? CLEARED : current
284
+ if (this.retained?.text === snapshot) return // ← 文本没变:不产生任何消息
285
+ return createUserMessage({ content: [{ type:'text', text: snapshot }], source: … })
286
+ }
287
+ ```
288
+ (`[PKG]dsh-agent-loop/lib/index.js:57-75`)
289
+
290
+ ### 6.3 取代不是改写,是「追加 + 声明」
291
+
292
+ 快照正文第一句固定为:
293
+
294
+ ```
295
+ Current runtime context. This snapshot supersedes earlier runtime-context snapshots.
296
+ ```
297
+ (`[PKG]dsh-system-prompt/lib/index.js:84-88`)
298
+
299
+ - 新快照以 `surfaceOp: 'append'` **追加**,旧快照**留在日志里**;
300
+ - 模型按「最新优先」读;`RuntimeContextProjection` 在快照被 `replace` 遮蔽时把 `retained` 清空(`[PKG]dsh-agent-loop:54`),以便下次变化重新发布;
301
+ - 本会话三条快照的演进(全部 `append`,零 `replace`):
302
+
303
+ ```
304
+ seq=8 466 chars 无日期 ← 初始
305
+ seq=76791 390 chars 无日期 ← 沙箱/审批策略变化触发重发
306
+ seq=248462 438 chars 含日期 ← 插件生效
307
+ ```
308
+
309
+ ### 6.4 与「系统提示词 section」的区别
310
+
311
+ | | `context()`(快照) | `section()`(系统提示词) |
312
+ |---|---|---|
313
+ | 落点 | 一条 `user/message` | 请求的 `system` 字符串 |
314
+ | 日志 | 文本变化时追加一条快照消息 | 变化时在 `request/header` 里重记**整份** system |
315
+ | 前缀缓存 | 尾部追加,前缀不变 | **头部被重写 → 整个前缀失效** |
316
+ | 被 preset 顶掉 | `includeRuntimeContext: false` → `contexts: []` | `complete: true` 的 section 会独占 `sections` |
317
+
318
+ (抑制逻辑:`[PKG]dsh-system-prompt/lib/index.js:243`、`:276`、`:284-289`;官方 `minimal` 与本地 `simple-reply` 两个 preset 同时设了这两项,因此**任何**提示词/上下文注入在那类 preset 下都失效。)
319
+
320
+ ---
321
+
322
+ ## 7. 体积与缓存
323
+
324
+ ### 7.1 三种变更的代价
325
+
326
+ | 变更 | 请求序列怎么变 | 前缀缓存 |
327
+ |---|---|---|
328
+ | 追加消息(用户输入、注入上下文、快照) | 末尾多一条 | **不受影响**,只有新增后缀是 miss |
329
+ | `surfaceOp: replace` 遮蔽一段 | 可见序列变短 | 遮蔽点之后需要重算 |
330
+ | 改 `system` 或 `tools` | 头部被重写 | **整个前缀失效**(这是 Anthropic 系 `defer_loading` 想解决的问题) |
331
+
332
+ ### 7.2 本会话实测
333
+
334
+ | 项 | 数值 |
335
+ |---|---|
336
+ | 日志 | 822 个 surface 事件,**全部 `append`,0 个 `replace`**;压缩后 2.1 MB,2731 个 zstd 帧 |
337
+ | `request/header` | 1 条:`system` 16853 字符 + 56 个工具 schema |
338
+ | 运行上下文快照 | 3 条,390–466 字符(JSONL 1055–1211 字节/条) |
339
+ | 真实用户消息 | 22 条 |
340
+ | 最大单条注入 | `skill-catalog` 56527 字节(19887 字符) |
341
+ | `subagent-settled` | 33683 字节/条 × 2 |
342
+ | 本插件贡献 | 46 字符 ≈ 12 token(`CHARS_PER_TOKEN=4`,`[PKG]dsh-token-meter/lib/index.js:15`),且仅在日期变化时随快照带上 |
343
+
344
+ ---
345
+
346
+ ## 8. 本插件在其中的位置
347
+
348
+ `dsh-date-wrapper` 只做一件事:往「动态运行上下文」通道里加一行。
349
+
350
+ ```js
351
+ ctx.inject(['systemPrompt'], (scope) => {
352
+ scope.systemPrompt.context({
353
+ name: 'date-wrapper:date', order: 116,
354
+ text: () => renderDate(Date.now(), formatter, zone), // 每次 assemble 重新求值
355
+ })
356
+ })
357
+ ```
358
+
359
+ - 不产生额外 `user/message`:日期并入平台**本来就会发**的那条快照;
360
+ - 同一天内 0 条额外事件(`project()` 文本去重);
361
+ - 跨天时追加一条新快照,旧的那条留在历史里,靠 supersedes 声明让最新生效;
362
+ - 前缀缓存不受影响(追加而非改写)。
363
+
364
+ ---
365
+
366
+ ## 9. 代码索引
367
+
368
+ | 主题 | 位置 |
369
+ |---|---|
370
+ | 事件 append 与深冻结 | `[PKG]dsh-session/lib/types/index.js:484-509` |
371
+ | surface 判定与 op 词表 | `[PKG]dsh-session/lib/types/surface.js:32-56,124-145,255-290` |
372
+ | 消息形状校验(导入边界) | `[PKG]dsh-session/lib/types/index.js:219-247` |
373
+ | JSONL 路径 / header / 批次编码 | `[PKG]dsh-session-persistence-jsonl/lib/index.js:145-198,160-172,1170-1199` |
374
+ | 多帧 zstd 解码 | 同上 `:336-383`、`:456-468` |
375
+ | legacy 消息 id 补铸 | `[PKG]dsh-session-persistence/lib/index.js:518-521,665-678` |
376
+ | pre-step 与默认决策 | `[PKG]dsh-agent-loop/lib/index.js:492-514` |
377
+ | inbox claim | `[PKG]dsh-agent/lib/types/inbox.js:50-57` |
378
+ | 请求组装 / header 比较 | `[PKG]dsh-agent-loop/lib/index.js:693-762,529-548` |
379
+ | 请求冻结与打标 | `[PKG]dsh-agent-loop/lib/index.js:751-761`、`[PKG]dsh-llm/lib/index.js:87-90` |
380
+ | `llm/stream` waterfall | `[PKG]dsh-llm/lib/index.js:1636-1641` |
381
+ | log-reconstruction 断言 | `[PKG]dsh-agent-loop/lib/invariant.js:15-33` |
382
+ | prompt 组装 / sections / contexts / variables | `[PKG]dsh-system-prompt/lib/index.js:65-102,186-230,240-289` |
383
+ | 快照去重与 supersedes | `[PKG]dsh-agent-loop/lib/index.js:26-75`、`[PKG]dsh-system-prompt/lib/index.js:84-88` |
384
+ | 工具 schema 提供者 | `[PKG]dsh-tools/lib/index.js:2595`、`[PKG]dsh-system-prompt/lib/index.js:280` |
385
+ | 技能目录的 digest + 批内替换 | `[PKG]dsh-tool-skill/lib/index.js:181-214,247,309-336` |
386
+ | 压缩检查点用 replace 遮蔽 | `[PKG]dsh-compaction-basic/lib/index.js:606-611` |
387
+ | token 估算启发式 | `[PKG]dsh-token-meter/lib/index.js:15` |
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "dsh-date-wrapper",
3
+ "version": "0.1.1-beta.1",
4
+ "type": "module",
5
+ "description": "Minimal date line for the DeepSeek Harness runtime-context snapshot: 'Current date: 2026-09-08 Asia/Shanghai Tuesday' (46 chars, ~12 tokens) - a cordis host plugin, no dsh source changes, no PR required",
6
+ "keywords": [
7
+ "deepseek-harness",
8
+ "dsh",
9
+ "plugin",
10
+ "time",
11
+ "date",
12
+ "context",
13
+ "timezone"
14
+ ],
15
+ "license": "MIT",
16
+ "main": "src/index.js",
17
+ "exports": {
18
+ ".": "./src/index.js",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "src/",
23
+ "docs/",
24
+ "cordis.patch.yml",
25
+ "README.md",
26
+ "README.zh.md",
27
+ "README.ja.md",
28
+ "README.ko.md",
29
+ "INSTALL.md",
30
+ "INSTALL.zh.md",
31
+ "INSTALL.ja.md",
32
+ "INSTALL.ko.md",
33
+ "CHANGELOG.md",
34
+ "CHANGELOG.ja.md",
35
+ "CHANGELOG.ko.md",
36
+ "HANDOVER.md",
37
+ "LICENSE"
38
+ ],
39
+ "dsh": {
40
+ "bundle": {
41
+ "patch": "./cordis.patch.yml"
42
+ }
43
+ },
44
+ "scripts": {
45
+ "test": "node --test \"tests/*.test.mjs\"",
46
+ "tdd": "node --test --watch \"tests/*.test.mjs\"",
47
+ "lint": "eslint .",
48
+ "lint:fix": "eslint . --fix",
49
+ "verify": "npm run lint && npm run test"
50
+ },
51
+ "devDependencies": {
52
+ "@eslint/js": "^10.0.1",
53
+ "eslint": "^10.10.0"
54
+ },
55
+ "engines": {
56
+ "node": ">=20",
57
+ "dsh": ">=0.1.0-rc.7 <0.2.0-0"
58
+ }
59
+ }
package/src/format.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * dsh-date-wrapper — 时区投影与日期文本(纯函数,零依赖,可单测)。
3
+ *
4
+ * 输出格式:`Current date: 2026-09-08 Asia/Shanghai Tuesday`
5
+ * —— 标签 + ISO 日期 + IANA 时区名 + 英文星期,无时分秒。
6
+ * 标签与快照里其它条目的风格一致(`Current DSH file policy: …`、`Approval policy: …`)。
7
+ *
8
+ * 设计要点:
9
+ * - 日期与星期都从**同一份投影结果**推出:先用 `formatToParts` 得到该时区的
10
+ * Y/M/D,再由 `Date.UTC(y, m-1, d).getUTCDay()` 求星期。这样星期不可能与
11
+ * 日期错位(跨时区边界时,北京 2026-09-08 00:30 必须是 Tuesday,而 UTC
12
+ * 同一时刻还是 2026-09-07 Monday)。
13
+ * - 不依赖 locale 数据:星期用固定英文表,日期手工拼串(`en-CA` 恰好也给
14
+ * `YYYY-MM-DD`,但那是巧合而非契约)。
15
+ * - 只产出**文本**:注入点是 `systemPrompt.context()`(动态运行上下文),由平台
16
+ * 把它并入「Current runtime context」快照消息 —— 不产生额外的 user/message,
17
+ * 也不改写系统提示词(见 README)。
18
+ * - 文本 provider 必须 fail-soft:prompt 组装期抛错会让**每一次请求**都失败,
19
+ * 所以无法渲染时返回空串(平台会过滤掉空文本,见 dsh-system-prompt:102)。
20
+ */
21
+
22
+ /** 星期名(索引同 `getUTCDay()`:0=Sunday)。固定英文表,不依赖 locale。 */
23
+ const WEEKDAY_NAMES = Object.freeze([
24
+ 'Sunday',
25
+ 'Monday',
26
+ 'Tuesday',
27
+ 'Wednesday',
28
+ 'Thursday',
29
+ 'Friday',
30
+ 'Saturday',
31
+ ])
32
+
33
+ /**
34
+ * 文本标签。与快照里其它条目的风格一致(`Current DSH file policy: …`)。
35
+ * 想换成 `Date:` / `Date refer:` 只改这一处。
36
+ */
37
+ export const TEXT_LABEL = 'Current date: '
38
+
39
+ /**
40
+ * 解析并校验时区,产出一个可复用的格式化器。
41
+ *
42
+ * @param {string|undefined} timeZone IANA 时区名;`undefined` 表示用进程时区。
43
+ * @returns {{ formatter: Intl.DateTimeFormat, zone: string }} 格式化器与规范化时区名。
44
+ * @throws {Error} 时区非法或无法解析时抛错(fail-fast,绝不静默降级成 UTC)。
45
+ */
46
+ export function resolveZone(timeZone) {
47
+ let formatter
48
+ try {
49
+ formatter = new Intl.DateTimeFormat('en-US', {
50
+ ...(timeZone === undefined ? {} : { timeZone }),
51
+ year: 'numeric',
52
+ month: '2-digit',
53
+ day: '2-digit',
54
+ })
55
+ } catch (error) {
56
+ const reason =
57
+ timeZone === undefined
58
+ ? '系统时区无法解析'
59
+ : `非法 IANA timeZone ${JSON.stringify(timeZone)}`
60
+ throw new Error(`date-wrapper: ${reason}`, { cause: error })
61
+ }
62
+ return { formatter, zone: formatter.resolvedOptions().timeZone }
63
+ }
64
+
65
+ /**
66
+ * 渲染一行日期文本。
67
+ *
68
+ * @param {number} now 时间戳(毫秒)。
69
+ * @param {Intl.DateTimeFormat} formatter 由 {@link resolveZone} 产出。
70
+ * @param {string} zone 规范化 IANA 时区名,原样出现在文本里。
71
+ * @returns {string} 形如 `Current date: 2026-09-08 Asia/Shanghai Tuesday`。
72
+ */
73
+ export function renderDate(now, formatter, zone) {
74
+ const parts = Object.fromEntries(
75
+ formatter.formatToParts(new Date(now)).map((part) => [part.type, part.value]),
76
+ )
77
+ const date = `${parts.year}-${parts.month}-${parts.day}`
78
+ const weekday = WEEKDAY_NAMES[new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay()]
79
+ return `${TEXT_LABEL}${date} ${zone} ${weekday}`
80
+ }
81
+
82
+ /**
83
+ * 构造运行上下文条目的文本 provider。
84
+ *
85
+ * 平台每次组装 prompt 都会调用它(`dsh-system-prompt:271`),所以日期始终是
86
+ * 当次请求的当下日期。渲染失败时返回空串而不是抛出 —— prompt 组装期抛错会
87
+ * 让每一次请求都失败,而日期缺失只是可接受的降级;空文本会被平台过滤掉
88
+ * (`dsh-system-prompt:102`)。
89
+ *
90
+ * @param {{ formatter: Intl.DateTimeFormat, zone: string }} deps 渲染依赖。
91
+ * @returns {() => string} 每次调用重新取当前时间的文本 provider。
92
+ */
93
+ export function createDateContextText({ formatter, zone }) {
94
+ return () => {
95
+ try {
96
+ return renderDate(Date.now(), formatter, zone)
97
+ } catch {
98
+ return ''
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 校验并规范化插件配置。
105
+ *
106
+ * @param {unknown} config 来自 `cordis.patch.yml` 的行配置。
107
+ * @returns {{ timeZone: string|undefined }} 去空白后的配置。
108
+ * @throws {Error} `timeZone` 类型或取值非法时抛错。
109
+ */
110
+ export function validateConfig(config) {
111
+ const raw = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
112
+ const { timeZone } = raw
113
+ if (timeZone !== undefined && (typeof timeZone !== 'string' || timeZone.trim() === '')) {
114
+ throw new Error(
115
+ `date-wrapper: config.timeZone must be a non-empty IANA zone string, got ${JSON.stringify(timeZone)}`,
116
+ )
117
+ }
118
+ return { timeZone: timeZone === undefined ? undefined : timeZone.trim() }
119
+ }
package/src/index.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * dsh-date-wrapper — host 半。
3
+ *
4
+ * 把当前日期注册为一条**动态运行上下文**(`systemPrompt.context`)。平台在每次
5
+ * 组装 prompt 时渲染它,并把它并入自己那条「Current runtime context」快照消息,
6
+ * 文本形如 `2026-09-08 Asia/Shanghai Tuesday`:
7
+ *
8
+ * - 不产生额外的 `user/message`(对比:往 `agent/pre-step` 塞消息会每轮落一条);
9
+ * - 平台对快照**按文本去重**(`RuntimeContextProjection.project()` 只在文本变化时
10
+ * 追加新快照),所以日期不变时一条事件都不多,跨天时最多多一条;
11
+ * - 快照是**追加**而非原地改写,请求序列只增长,因此不破坏前缀缓存。
12
+ *
13
+ * 生命周期:`ctx.inject(['systemPrompt'], …)` 建立子 fiber,注册随插件 fiber
14
+ * 一起回收。**不导出插件级 `inject`** —— 那会让 fiber 卡在 PENDING,服务缺失时
15
+ * 触发启动审计失败;`ctx.inject` 的等待语义相同但不拖垮 boot(先例:
16
+ * `@deepseek-ai/dsh-user-approval`)。
17
+ */
18
+ import { createDateContextText, resolveZone, validateConfig } from './format.js'
19
+
20
+ /** Cordis 插件名。 */
21
+ export const name = 'date-wrapper'
22
+
23
+ /** 运行上下文条目的名字(同名重复注册会抛错)。 */
24
+ export const CONTEXT_NAME = 'date-wrapper:date'
25
+
26
+ /**
27
+ * 排序位。上下文按 order 升序渲染(`dsh-system-prompt:276`);
28
+ * 已占用:110 sandbox:policy、115 approval:policy、120 subagent:delegation。
29
+ */
30
+ export const CONTEXT_ORDER = 116
31
+
32
+ /**
33
+ * 注册日期运行上下文。
34
+ *
35
+ * @param {object} ctx Cordis 上下文。
36
+ * @param {{ timeZone?: string }} [config] patch 行下发的配置。
37
+ * @throws {Error} `timeZone` 非法时抛错(fail-fast,绝不静默降级成 UTC)。
38
+ */
39
+ export function apply(ctx, config) {
40
+ const { timeZone } = validateConfig(config)
41
+ const { formatter, zone } = resolveZone(timeZone)
42
+ ctx.inject(['systemPrompt'], (scope) => {
43
+ scope.systemPrompt.context({
44
+ name: CONTEXT_NAME,
45
+ order: CONTEXT_ORDER,
46
+ text: createDateContextText({ formatter, zone }),
47
+ })
48
+ })
49
+ }