Merge remote-tracking branch 'origin/stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring
# Conflicts: # packages/client/connection/README.i18n.yaml # packages/client/connection/README.md # packages/client/connection/README.zh.md
This commit is contained in:
@@ -1,6 +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/session-persistence/session-checkpoint-policy/README.md
|
||||
# pnpm run verify-translation-pairing --write packages/session/session-checkpoint-policy/README.md
|
||||
README.md: 01ed3c694967b1a86245d01b9d9f7eecd1193348
|
||||
README.zh.md: e2c197c0cc1d942a045c1ea22c1fb257f6e4059d
|
||||
|
||||
@@ -161,7 +161,7 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
|
||||
}
|
||||
|
||||
// Each backend keeps the typed service surface beside its storage hooks;
|
||||
// extracting these trivial forwards would add an inheritance seam.
|
||||
// extracting these trivial forwards would add an inheritance layer.
|
||||
/* jscpd:ignore-start */
|
||||
// --- SessionPersistence service surface (delegated to the coordinator) ---
|
||||
|
||||
|
||||
@@ -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: 4961f62bf6854c343d35b8406c9d721590274534
|
||||
README.zh.md: 0507e0fa99bc553317c34e51f30e4daaceee93fd
|
||||
README.md: 745c25616e06c391a70b37471917aa9d2539c4dd
|
||||
README.zh.md: c86531bcaeca85fa565b537f1fc1beca5d6ac7b3
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
A SQLite durable session-persistence backend — a second `SessionPersistence` implementation ([session persistence](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)), built to validate that the abstract seam and the shared `runPersistenceContract` suite are genuinely backend-agnostic. It satisfies the SAME contract as `dsh-session-persistence-jsonl` (append-only, contiguous-seq, lazy materialization, interrupted-turn close on load), expressed over `node:sqlite` rows instead of file bytes.
|
||||
A SQLite durable session-persistence backend — a second `SessionPersistence` provider ([session persistence](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)), built to validate that the Service Definition and the shared `runPersistenceContract` suite are genuinely backend-agnostic. It satisfies the SAME contract as `dsh-session-persistence-jsonl` (append-only, contiguous-seq, lazy materialization, interrupted-turn close on load), expressed over `node:sqlite` rows instead of file bytes.
|
||||
|
||||
`locate(meta)` returns `undefined`: all sessions share one database, so there is no honest independent per-session transcript path.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
SQLite 持久会话存储后端:第二个 `SessionPersistence` 实现(见[会话持久化](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)),用于验证抽象 seam 和共享 `runPersistenceContract` 套件真正与后端无关。它满足与 `dsh-session-persistence-jsonl` 相同的约定(仅追加、连续 seq、延迟实体化、在 load 时关闭中断轮次),但用 `node:sqlite` 行而非文件字节表达。
|
||||
SQLite 持久会话存储后端:第二个 `SessionPersistence` 提供方(见[会话持久化](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)),用于验证 Service Definition 和共享 `runPersistenceContract` 套件真正与后端无关。它满足与 `dsh-session-persistence-jsonl` 相同的约定(仅追加、连续 seq、延迟实体化、在 load 时关闭中断轮次),但用 `node:sqlite` 行而非文件字节表达。
|
||||
|
||||
`locate(meta)` 返回 `undefined`:所有会话共享一个数据库,因此不存在真实、独立的逐会话 transcript(文本记录)路径。
|
||||
|
||||
@@ -22,7 +22,7 @@ SQLite 持久会话存储后端:第二个 `SessionPersistence` 实现(见[
|
||||
- **延迟实体化。**`create()` 只在内存记录意图,第一次 `append` 前不写行。从未 append 的会话没有 `sessions` 行,因此不在 `list()` 中(它精确报告有行的会话)。
|
||||
- **在 load 时关闭中断轮次。**`load()` 实现共享[崩溃恢复约定](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md):保留有效中断轮次,在一个事务中追加合成关闭事件,并只移除撕裂尾部行。已提交解析错误或序列缺口使会话无法加载。恢复会变更已存储行,因此下一次 append 从平衡日志和准确游标开始。
|
||||
- **非变更检查。**`inspect()` 返回不可变、平衡的逻辑视图,并可在内存中合成恢复 closer,但不会删除撕裂尾部行、追加恢复行或更改轻量修订。
|
||||
- **轻量修订。**`listSnapshots(signal?)` 组合不可变存储与数据库文件身份、每实体化 incarnation id,以及在每个变更事务中递增的每会话计数器。完整前缀读取在同一个读事务中捕获该修订及其事件行,`readStoredRevision()` 则只查询会话行来校验保留的准备结果。它在不解析事件行的情况下保持未变观察稳定,并区分独立存储和重建的同 id 日志。它在共享就绪和同步元数据查询前后检查取消;查询本身不可抢占。
|
||||
- **轻量修订。**`listSnapshots(signal?)` 组合不可变存储与数据库文件身份、每实体化 incarnation id,以及在每个变更事务中递增的每会话计数器。完整前缀读取在同一个读事务中捕获该 revision 及其事件行,`readStoredRevision()` 则只查询 session 行来校验保留的 preparation。它在不解析事件行的情况下保持未变观察稳定,并区分独立存储和重建的同 id 日志。它在共享就绪和同步元数据查询前后检查取消;查询本身不可抢占。
|
||||
|
||||
## 配置(schemastery)
|
||||
|
||||
@@ -37,7 +37,7 @@ interface Config {
|
||||
|
||||
## 写入路径
|
||||
|
||||
与 JSONL 后端一样,插件将每个冻结的 `session/event` 复制到每个活动会话各自的 controller。第一个待处理事件会开启配置的固定批处理窗口,后续事件会加入但不会重置截止时间。窗口到期后会启动一个事务;该次写入期间接纳的事件会形成另一个独立有界的后续批次。`session/flush` 会取消等待并排空当前与待处理批次。Controller 会持久化一次 fork 种子,并保留写入游标,使恢复操作绝不重新 append 已存储事件;它还会在 apply 时为活动会话设置初始状态,因为 HMR(热模块替换)不回放 `session/created`。dispose(资源释放)会在关闭数据库前排空每个保留的 controller。每个事件仍各占一行 SQLite 记录;批处理只把更多 INSERT 归入同一个事务和同一次修订递增。
|
||||
与 JSONL 后端一样,插件将每个冻结的 `session/event` 复制到每个活动会话各自的 controller。第一个待处理事件会开启配置的固定批处理窗口,后续事件会加入但不会重置截止时间。窗口到期后会启动一个事务;该次写入期间接纳的事件会形成另一个独立有界的后续批次。`session/flush` 会取消等待并排空当前与待处理批次。Controller 会持久化一次 fork 种子,并保留写入游标,使恢复操作绝不重新 append 已存储事件;它还会在 apply 时为活动会话设置初始状态,因为 HMR(热模块替换)不回放 `session/created`。dispose(资源释放)会在关闭数据库前排空每个保留的 controller。每个事件仍各占一行 SQLite 记录;批处理只把更多 INSERT 归入同一个事务和同一次修订版本递增。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -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/README.md
|
||||
README.md: 89c7cd5ebaff6f9ce9df9b60a50121dff4ddeeb5
|
||||
README.zh.md: 6b02f37343da07a80953ecb9d05e5903bd42d95c
|
||||
README.md: c64826db1e9c7339f43a64ad7c13f01a2a39638e
|
||||
README.zh.md: 651d920404300fa602f77cee63b0f31aec837919
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The abstract durable session-persistence seam (`ctx.sessionPersistence`). Defines WHAT a persistence backend does — durably store, reload, and list sessions — without saying HOW. Mirrors the `dsh-bash` capability-seam template ([capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract service here, a concrete implementation in a sibling package, consumers that inject the interface.
|
||||
The durable session-persistence Service Definition (`ctx.sessionPersistence`). Defines WHAT a persistence backend does — durably store, reload, and list sessions — without saying HOW. Mirrors the `dsh-bash` capability-seam template ([capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract service here, a Service provider in a sibling package, and Consumers that inject the service.
|
||||
|
||||
The persisted unit IS the existing `SessionEvent` (event-sourced model — the log is the single source of truth), so there is no parallel "persisted message" type. Metadata that is NOT replayable conversation state (format version, cwd, lineage, seed boundary, origin, delegation depth) travels separately as `SessionHeader`, owned by `dsh-session` and re-exported here.
|
||||
|
||||
@@ -35,13 +35,13 @@ Each `session/event` copies its event into the session controller. The first pen
|
||||
|
||||
Crash repair is cold-only. For a live id, `load(id)` snapshots the authoritative in-memory log, waits for that snapshot to become durable, and returns it only when balanced; an open live turn rejects instead of receiving synthetic interruption closers. For a cold id, inspection reads, validates, freezes, and constructs one unpublished Session; repeated inspection reuses that object graph only while its source revision remains current. `prepare(id)` performs the same check before repair, reserves the exact Session, commits any pending torn-tail/interrupted-turn repair, and returns it for publication. HMR adoption reads through `loadStored`, applies the coordinator's cwd check, and never closes the active turn.
|
||||
|
||||
Backend reads normalize the exact supported same-version shapes before current-shape validation. Pre-identity messages receive the deterministic id `legacy-message:<session-id>:<event-seq>`; a tool-result content replacement inherits its target's imported id. A pre-react-loop `turn/start` loses its obsolete trigger, a removed `steering/message` becomes the same identified `user/message`, and an older `turn/end` maps its terminal reason without inventing unavailable cancellation provenance. The coordinator uses the same normalized view for `load`, `inspect`, `readFrom`, ownerless-state claims, and HMR prefix adoption. Storage remains append-only: reads do not rewrite old records, and later appends use the current shape. These are narrow import exceptions from the [pre-identity message](../../../.agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.md) and [pre-react-loop session](../../../.agents/notes/implemented/bug-fix/2026-08-04-load-pre-react-loop-sessions.md) decisions, not a general v0 migration promise.
|
||||
Backend reads normalize the exact supported same-version shapes before current-shape validation. Pre-identity messages receive the deterministic id `legacy-message:<session-id>:<event-seq>`; a tool-result content replacement inherits its target's imported id. A pre-react-loop `turn/start` loses its obsolete trigger, a removed `steering/message` becomes the same identified `user/message`, and an older `turn/end` maps its terminal reason without inventing a caller that the old record did not name. The coordinator uses the same normalized view for `load`, `inspect`, `readFrom`, ownerless-state claims, and HMR prefix adoption. Storage remains append-only: reads do not rewrite old records, and later appends use the current shape. These are narrow import exceptions from the [pre-identity message](../../../.agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.md) and [pre-react-loop session](../../../.agents/notes/implemented/bug-fix/2026-08-04-load-pre-react-loop-sessions.md) decisions, not a general v0 migration promise.
|
||||
|
||||
When a live session emits `session/disposed`, the coordinator waits for its controller, serializes a final drain, then releases state owned by that exact `Session` object. Failed retirement leaves the controller in the live-session map, so backend teardown can retry it. Backend teardown stops event admission first, flushes every remaining controller, awaits per-id operations, and only then closes the storage handle.
|
||||
|
||||
The side-effect-free `locate`, lightweight `listSnapshots`, and per-id `readStoredRevision` queries remain backend-owned because they describe storage topology and revision identity rather than write orchestration. `listSnapshots(signal?)` passes the caller's exact signal into backend discovery so observers can cancel that work without detaching it.
|
||||
|
||||
The `PersistenceBackend<TornMarker>` hooks (the only seam between the coordinator and storage):
|
||||
The `PersistenceBackend<TornMarker>` hooks (the only contract between the coordinator and storage):
|
||||
|
||||
| Hook | Role |
|
||||
|---|---|
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
这是用于持久保存会话的抽象 seam(`ctx.sessionPersistence`)。它定义持久化后端做什么:持久存储、重新加载和列出会话,而不规定如何实现。它与 `dsh-bash` 能力 seam 模板一致(见[能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):本包提供抽象服务,同级包提供具体实现,消费方注入接口。
|
||||
这是用于持久保存会话的 Service Definition(`ctx.sessionPersistence`)。它定义持久化后端做什么:持久存储、重新加载和列出会话,而不规定如何实现。它与 `dsh-bash` 能力 seam 模板一致(见[能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):本包提供抽象服务,同级包提供 Service provider,Consumer 注入服务。
|
||||
|
||||
持久化单元就是现有 `SessionEvent`(事件溯源模型:日志是唯一真源),因此不存在另一套并行的「持久消息」类型。不属于可回放对话状态的元数据(格式版本、cwd、血缘、种子边界、origin、委托深度)作为 `SessionHeader` 单独传输,该类型归 `dsh-session` 所有,并在此重新导出。
|
||||
|
||||
@@ -35,13 +35,13 @@
|
||||
|
||||
崩溃修复只适用于冷状态。对于实时 id,`load(id)` 为权威内存日志制作快照,等待该快照持久,并只在平衡时返回;开放实时轮次会被拒绝,而不会收到合成中断 closer。对于冷 id,检查只读取、验证、冻结并构造一次未发布 Session;只有来源修订值仍然是当前值时,重复检查才会复用该对象图。`prepare(id)` 在修复前执行相同校验,预留该 Session 本身,提交任何待处理的撕裂尾部或中断轮次修复,并将其返回用于发布。HMR 接管通过 `loadStored` 读取,应用协调器 cwd 检查,并绝不关闭活动轮次。
|
||||
|
||||
后端读取会在当前形状验证前,规范化明确受支持的同版本形状。消息标识机制引入前的消息会获得确定性的 id `legacy-message:<session-id>:<event-seq>`;工具结果的内容替换会继承其目标导入后的 id。react-loop 引入前的 `turn/start` 会移除过时的 trigger,已移除的 steering(中途引导)事件 `steering/message` 会转换为同一条带标识的 `user/message`;旧版 `turn/end` 会在不虚构无法获得的取消来源的前提下映射终止原因。协调器对 `load`、`inspect`、`readFrom`、无所有者状态的认领和 HMR 前缀接管使用同一份规范化视图。存储仍然仅追加:读取不会重写旧记录,此后追加的事件使用当前形状。这些是[消息标识机制引入前的消息](../../../.agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.md)与 [react-loop 引入前会话](../../../.agents/notes/implemented/bug-fix/2026-08-04-load-pre-react-loop-sessions.md)决策所规定的范围受限的导入例外,并不构成通用的 v0 迁移承诺。
|
||||
后端读取会在当前形状验证前,规范化明确受支持的同版本形状。消息标识机制引入前的消息会获得确定性的 id `legacy-message:<session-id>:<event-seq>`;工具结果的内容替换会继承其目标导入后的 id。react-loop 引入前的 `turn/start` 会移除过时的 trigger,已移除的 steering(中途引导)事件 `steering/message` 会转换为同一条带标识的 `user/message`;旧版 `turn/end` 会在不虚构旧记录中未命名调用方的前提下映射终止原因。协调器对 `load`、`inspect`、`readFrom`、无所有者状态的认领和 HMR 前缀接管使用同一份规范化视图。存储仍然仅追加:读取不会重写旧记录,此后追加的事件使用当前形状。这些是[消息标识机制引入前的消息](../../../.agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.md)与 [react-loop 引入前会话](../../../.agents/notes/implemented/bug-fix/2026-08-04-load-pre-react-loop-sessions.md)决策所规定的范围受限的导入例外,并不构成通用的 v0 迁移承诺。
|
||||
|
||||
实时会话发出 `session/disposed` 时,协调器等待其 controller,串行化最终 drain,然后释放该精确 `Session` 对象拥有的状态。失败退役会将 controller 保留在实时会话 map 中,使后端拆卸可重试。后端拆卸先停止事件接纳,flush 每个剩余 controller,等待每 id 操作,最后才关闭存储句柄。
|
||||
|
||||
无副作用 `locate`、轻量 `listSnapshots` 和按 id 查询的 `readStoredRevision` 仍由后端负责,因为它们描述存储拓扑和修订身份,而非写入编排。`listSnapshots(signal?)` 将调用方传入的同一个信号传给后端发现流程,使观察者可在不脱离该工作的情况下取消。
|
||||
|
||||
`PersistenceBackend<TornMarker>` 钩子(协调器与存储之间的唯一 seam):
|
||||
`PersistenceBackend<TornMarker>` 钩子(协调器与存储之间的唯一约定):
|
||||
|
||||
| 钩子 | 职责 |
|
||||
|---|---|
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface StoredSuffix {
|
||||
}
|
||||
|
||||
/**
|
||||
* The storage seam between {@link PersistenceCoordinator} and a concrete
|
||||
* The storage contract between {@link PersistenceCoordinator} and a concrete
|
||||
* backend: the minimal set of durable primitives the orchestration calls. A
|
||||
* backend implements these (over files, rows, an object store, …); the
|
||||
* coordinator supplies everything else (buffering, serialization, cursors,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Durable session-persistence seam (`ctx.sessionPersistence`). Backends store
|
||||
* Durable session-persistence Service Definition (`ctx.sessionPersistence`). Backends store
|
||||
* {@link SessionEvent}s as the event-sourced log and carry non-replayable
|
||||
* {@link SessionHeader} metadata separately.
|
||||
* @module @deepseek-ai/dsh-session-persistence
|
||||
@@ -10,7 +10,7 @@ import { SessionPreparation } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionPersistenceRevision } from './revision.ts'
|
||||
|
||||
// Re-export the metadata vocabulary so consumers import it from the seam.
|
||||
// Re-export the metadata vocabulary so Consumers import it from the Service Definition.
|
||||
export type { SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
export { SessionPersistenceRevision } from './revision.ts'
|
||||
|
||||
|
||||
@@ -1279,7 +1279,7 @@ describe('PersistenceCoordinator observation cancellation', () => {
|
||||
}
|
||||
const suffix = await coordinator.readFrom(id, 3)
|
||||
expect(suffix.events).toEqual(log.slice(3))
|
||||
// The hook's undefined is the seam's not-found.
|
||||
// The hook's `undefined` is the backend contract's not-found result.
|
||||
await expect(coordinator.readFrom(SessionId('missing-seek'), 0)).rejects.toThrow('not found')
|
||||
|
||||
// A hook failure with no cancellation in play propagates as-is.
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/session/session-projection/README.md
|
||||
README.md: a42e88c262915fc5e53cd72205079cbc8029a8df
|
||||
README.zh.md: 01b23a7152a509efab6e169043a823d49d6d9fde
|
||||
README.md: 2615b253999c798172168ec9d0232eb965b07fbc
|
||||
README.zh.md: 0712e9c7a61fbcf43939791b3e7cd24af6777c3a
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Session-projection seam. It owns `ctx.sessionProjections`, the registry that drives every registered projection unit over committed session events and serves finished whole values to carriers, currently the api-proxy history tail page and `session/projection` push frame. A domain registers pure mathematics; the framework owns the drive. The [session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) records the design rationale.
|
||||
Session-projection Service Definition and drive registry. It owns `ctx.sessionProjections`, the registry that drives every registered projection unit over committed session events and serves finished whole values to carriers, currently the api-proxy history tail page and `session/projection` push frame. A domain registers pure mathematics; the framework owns the drive. The [session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) records the design rationale.
|
||||
|
||||
## Service: `SessionProjectionRegistry` (ctx key: `sessionProjections`)
|
||||
|
||||
@@ -25,11 +25,11 @@ Session-projection seam. It owns `ctx.sessionProjections`, the registry that dri
|
||||
- **Synchronous unit discipline.** `init`/`apply`/`view` MUST be synchronous; carriers read `snapshot()` in the same tick as their page slice, which is what makes `asOfSeq` one consistent cut. An accidentally-async `view` returns a Promise, which fails the boundary `schema.parse` loudly.
|
||||
- **State is plain JSON, `stateVersion` is its invalidation anchor.** The persisted projection cache stores `(sessionId, key, ver, seq, val)` rows; bump `stateVersion` whenever the state shape or the fold semantics change so stale rows are discarded instead of forward-applied into garbage.
|
||||
- **No wire vocabulary here.** The registry exposes only the change feed and the snapshot read face; carriers (api-proxy) mint their own frames (`session/projection`) and blocks from them.
|
||||
- **Optional seam.** Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected; carriers use `ctx.get('sessionProjections')` and omit their block/frames entirely when the registry is absent.
|
||||
- **Optional capability.** Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected; carriers use `ctx.get('sessionProjections')` and omit their block/frames entirely when the registry is absent.
|
||||
|
||||
## Role
|
||||
|
||||
This is the interface-plus-drive package of the capability-seam split: domain host plugins (e.g. `dsh-tool-todo`) contribute units, carriers (`dsh-host-apiproxy`) consume the snapshot and change feed, and neither knows the other.
|
||||
This package owns the Service Definition and drive roles of the capability seam: domain host plugins (e.g. `dsh-tool-todo`) contribute units, carriers (`dsh-host-apiproxy`) consume the snapshot and change feed, and neither knows the other.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
会话投影 seam。它拥有 `ctx.sessionProjections`:该注册表在已提交的会话事件上驱动每个已注册的投影单元,并向载体提供完整的最终值,目前包括 api-proxy 历史尾页和 `session/projection` 推送帧。领域注册的只是纯数学;驱动权归框架。[session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) 记录了设计理由。
|
||||
会话投影 Service Definition 与驱动注册表。它拥有 `ctx.sessionProjections`:该注册表在已提交的会话事件上驱动每个已注册的投影单元,并向载体提供完整的最终值,目前包括 api-proxy 历史尾页和 `session/projection` 推送帧。领域注册的只是纯数学;驱动权归框架。[session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md)记录了设计理由。
|
||||
|
||||
## 服务:`SessionProjectionRegistry`(ctx 键:`sessionProjections`)
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
- **单元的同步纪律。**`init`/`apply`/`view` 必须是同步的;载体在切出页面切片的同一 tick 内读取 `snapshot()`,`asOfSeq` 之所以是一个一致切面正系于此。误写成异步的 `view` 会返回 Promise,让边界的 `schema.parse` 当场大声失败。
|
||||
- **状态是纯 JSON,`stateVersion` 是其失效锚点。** 持久投影缓存(persisted projection cache)存储 `(sessionId, key, ver, seq, val)` 行;状态形状或折叠语义一旦变化就递增 `stateVersion`,使陈旧行被丢弃,而不是被正向 apply 成垃圾。
|
||||
- **本层没有协议词汇。** 注册表只暴露变更流与快照读取面;载体(api-proxy)据此自铸各自的帧(`session/projection`)与块。
|
||||
- **可选 seam。** 领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响;载体使用 `ctx.get('sessionProjections')`,注册表缺席时完全省略自己的块与帧。
|
||||
- **可选能力。** 领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响;载体使用 `ctx.get('sessionProjections')`,注册表缺席时完全省略自己的块与帧。
|
||||
|
||||
## 职责
|
||||
|
||||
这是能力 seam 拆分中「接口 + 驱动」的那个包:领域 host 插件(如 `dsh-tool-todo`)贡献单元,载体(`dsh-host-apiproxy`)消费快照与变更流,两侧互不相识。
|
||||
本包承担能力 seam 的 Service Definition 与驱动角色:领域 host 插件(如 `dsh-tool-todo`)贡献单元,载体(`dsh-host-apiproxy`)消费快照与变更流,两侧互不相识。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Session-projection seam: the merge-extensible `SessionProjectionMap` type
|
||||
* Service Definition and drive registry for the session-projection capability seam: the merge-extensible `SessionProjectionMap` type
|
||||
* table, the `ProjectionDefinition` state-driven computation unit contract,
|
||||
* and the `ctx.sessionProjections` registry that DRIVES every registered unit
|
||||
* forward eagerly over committed session events. Domain host plugins
|
||||
@@ -117,7 +117,7 @@ export interface ProjectionCheckpointRow {
|
||||
/** Checkpoint rows keyed by projection key (one session's persisted cache value). */
|
||||
export type ProjectionCheckpoint = Record<string, ProjectionCheckpointRow>
|
||||
|
||||
/** Type-erased unit view the drive machinery works with (the register seam already proved the typed contract). */
|
||||
/** Type-erased unit view the drive machinery works with (the registration contract already proved the typed form). */
|
||||
interface ErasedDefinition {
|
||||
key: string
|
||||
schema: { parse(value: unknown): unknown }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Pure-type outlet of the session-projection seam: the one projection type
|
||||
* Pure-type outlet of the session-projection Service Definition: the one projection type
|
||||
* table, importable from client aggregates without dragging the host-side
|
||||
* cordis Context merges of the package root (dsh-agent → dsh-session). Domain
|
||||
* packages may declare-merge through either the package root or this outlet —
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* OpenTelemetry backend for the DeepSeek Harness telemetry seam.
|
||||
* OpenTelemetry Service provider for the DeepSeek Harness telemetry capability.
|
||||
*
|
||||
* Composes the OTel JS SDK as-is — a `LoggerProvider` with a
|
||||
* `BatchLogRecordProcessor` and an OTLP/HTTP log exporter — and maps each
|
||||
* record handed over by the seam onto `logger.emit()`. Per the seam's
|
||||
* record handed over by the capture coordinator onto `logger.emit()`. Per the Service Definition's
|
||||
* boundary axiom, everything downstream of that call (batching, retry,
|
||||
* queueing, loss policy) is the SDK's documented behavior, configured
|
||||
* verbatim through the `exporter`/`processor` passthroughs. This package owns
|
||||
@@ -121,7 +121,7 @@ export const DEFAULT_SHUTDOWN_TIMEOUT_MILLIS = 3_000
|
||||
// protocol limit, not a deployment default.
|
||||
const MAX_TIMER_DELAY_MILLIS = 2_147_483_647
|
||||
|
||||
/** Severity mapping from the seam's three-level vocabulary to OTel severity numbers. */
|
||||
/** Severity mapping from the Service Definition's three-level vocabulary to OTel severity numbers. */
|
||||
const SEVERITY: Record<TelemetrySeverity, { severityNumber: SeverityNumber; severityText: string }> = {
|
||||
info: { severityNumber: SeverityNumber.INFO, severityText: 'INFO' },
|
||||
warn: { severityNumber: SeverityNumber.WARN, severityText: 'WARN' },
|
||||
@@ -250,7 +250,7 @@ export class TelemetryOtel extends Telemetry {
|
||||
this.directEmit(record)
|
||||
}
|
||||
|
||||
// The seam's optional flush() hint is deliberately NOT implemented. The
|
||||
// The Service Definition's optional flush() hint is deliberately NOT implemented. The
|
||||
// batch processor exports on its own cadence (`processor.scheduledDelayMillis`,
|
||||
// the SDK's documented knob), and this backend is the SDK pipeline's only
|
||||
// caller — forwarding the hint to `forceFlush()` was the sole source of
|
||||
|
||||
@@ -17,7 +17,7 @@ export const inject = ['invariants']
|
||||
/**
|
||||
* No runtime invariant: mode selection changes capture handoff, SDK setup, and
|
||||
* local diagnostics without mutating session or service state an independent
|
||||
* companion can compare. Export remains inside the SDK past the seam boundary.
|
||||
* companion can compare. Export remains inside the SDK past the backend boundary.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@ export const USER_ID_FILE_NAME = '.userid'
|
||||
|
||||
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
/** Ambient seams for locating and generating the id; every field has a default. */
|
||||
/** Ambient hooks for locating and generating the id; every field has a default. */
|
||||
export interface AnonymousUserIdOptions {
|
||||
/** Environment consulted for `DSH_HOME`; defaults to `process.env`. */
|
||||
env?: NodeJS.ProcessEnv
|
||||
/** UUID generator; defaults to `crypto.randomUUID` (test seam). */
|
||||
/** UUID generator; defaults to `crypto.randomUUID` (test hook). */
|
||||
randomUUID?: () => string
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ function readPersistedId(file: string): AnonymousUserId | undefined {
|
||||
* run; the next launch converges on the persisted one.) Persistence is
|
||||
* best-effort — a write failure (read-only home) still returns a usable id
|
||||
* for the current run so telemetry is never blocked.
|
||||
* @param options - home-location and UUID-generation seams.
|
||||
* @param options - Home-location and UUID-generation hooks.
|
||||
* @returns the stable per-harness-home anonymous user id.
|
||||
*/
|
||||
export function getOrCreateAnonymousUserId(options: AnonymousUserIdOptions = {}): AnonymousUserId {
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('getOrCreateAnonymousUserId', () => {
|
||||
const home = tempHome()
|
||||
const winner = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'
|
||||
const file = join(home, USER_ID_FILE_NAME)
|
||||
// The generator seam runs between the initial read (absent) and the wx
|
||||
// The generator hook runs between the initial read (absent) and the wx
|
||||
// write, so planting the winner here simulates the concurrent first launch.
|
||||
const id = getOrCreateAnonymousUserId({
|
||||
env: { DSH_HOME: home },
|
||||
|
||||
@@ -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-telemetry/README.md
|
||||
README.md: 67d95bcc62bbf6783f8dcd11f0236d8c926b557b
|
||||
README.zh.md: 80044ceb57c93f8406398a9239d83cbb6015e98c
|
||||
README.md: 506f0f5bcb03a54f09805ed0f866a396e3cf0334
|
||||
README.zh.md: b5e47c8832452ece281dcdcfac007d6082813583
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The telemetry seam: the capture side of session-event reporting, behind a backend contract any reporting SDK satisfies with zero bending. Capture can follow live session events or replay a canonical session-log prefix on demand. The boundary axiom that shapes everything here: **this package's aspect ends at `emit()`** — batching, retry, queueing, and loss policy belong to the backend's SDK and are neither specified nor wrapped. Rationale and rejected alternatives: [the revival Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md), [feedback-gated delivery](../../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md), and [buffer-free feedback replay](../../../.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.md).
|
||||
The telemetry Service Definition and capture coordinator sit behind a backend contract any reporting SDK satisfies with zero bending. Capture can follow live session events or replay a canonical session-log prefix on demand. The boundary axiom that shapes everything here: **this package's aspect ends at `emit()`** — batching, retry, queueing, and loss policy belong to the backend's SDK and are neither specified nor wrapped. Rationale and rejected alternatives: [the revival Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md), [feedback-gated delivery](../../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md), and [buffer-free feedback replay](../../../.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.md).
|
||||
|
||||
## The backend contract
|
||||
|
||||
@@ -14,7 +14,7 @@ In `live` mode the coordinator registers, all through the composing fiber's effe
|
||||
|
||||
## The redact waterfall
|
||||
|
||||
Every record passes the `telemetry/record` waterfall immediately after projection — the seam's scrubbing extension point. The seam ships NO rules of its own: the innermost `next()` passes the record through unchanged, so with no listener mounted records reach the backend exactly as captured, and exported data is precisely as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath, and a throwing listener withholds that one record fail-closed inside the coordinator's containment. Live capture runs the waterfall at append time; on-demand capture runs it while replaying the canonical log, using the rules mounted at that time. Redaction applies to the outbound copy only; the canonical session log is never rewritten.
|
||||
Every record passes the `telemetry/record` waterfall immediately after projection — the Service Definition's scrubbing extension point. This package ships NO rules of its own: the innermost `next()` passes the record through unchanged, so with no listener mounted records reach the backend exactly as captured, and exported data is precisely as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath, and a throwing listener withholds that one record fail-closed inside the coordinator's containment. Live capture runs the waterfall at append time; on-demand capture runs it while replaying the canonical log, using the rules mounted at that time. Redaction applies to the outbound copy only; the canonical session log is never rewritten.
|
||||
|
||||
## The handoff cursor
|
||||
|
||||
@@ -30,7 +30,7 @@ Only the first `assistant/chunk` of each `(turn, step)` ships; the rest are drop
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as the seam only observes the session stream and hands redacted copies to a reporting backend; it never contributes to a model request.
|
||||
None, as this package only observes the session stream and hands redacted copies to a reporting backend; it never contributes to a model request.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
遥测(telemetry)seam:会话事件上报的捕获侧,隔在一个后端约定之后,任何上报 SDK 都无需变形即可满足该约定。捕获侧可跟随实时会话事件,也可按需回放权威会话日志前缀。塑造本包一切设计的边界公理:**本包的职责止于 `emit()`**。批处理、重试、排队与丢失策略都属于后端自身的 SDK,本包既不为其立规,也不做包装。设计依据与被否决的替代方案见[复活 Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)、[反馈门控投递](../../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md)与[无缓冲反馈回放](../../../.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.md)。
|
||||
遥测(telemetry)Service Definition 与捕获协调器位于一个后端约定之后,任何上报 SDK 都无需变形即可满足该约定。捕获侧可跟随实时会话事件,也可按需回放权威会话日志前缀。塑造本包(package)一切设计的边界公理:**本包的职责止于 `emit()`**。批处理、重试、排队与丢失策略都属于后端自身的 SDK,本包既不为其立规,也不做包装。设计依据与被否决的替代方案见[复活 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)、[反馈门控投递](../../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md)与[无缓冲反馈回放](../../../.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.md)。
|
||||
|
||||
## 后端约定
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
## 脱敏 waterfall(瀑布式事件)
|
||||
|
||||
每条记录在投影后立即经过 `telemetry/record` waterfall,这是该 seam 的脱敏扩展点。seam 自身不带任何规则:最内层的 `next()` 原样透传记录,因此未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式拦下这一条记录。实时捕获在追加时运行 waterfall;按需捕获则在回放权威日志时使用当时挂载的规则运行 waterfall。脱敏只作用于外发副本;权威会话日志永不改写。
|
||||
每条记录在投影后立即经过 `telemetry/record` waterfall,这是 Service Definition 的脱敏扩展点。本包自身不带任何规则:最内层的 `next()` 原样透传记录,因此未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式拦下这一条记录。实时捕获在追加时运行 waterfall;按需捕获则在回放权威日志时使用当时挂载的规则运行 waterfall。脱敏只作用于外发副本;权威会话日志永不改写。
|
||||
|
||||
## handoff 游标
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
## 模型体验
|
||||
|
||||
无。该 seam 只观察会话流,并把脱敏后的副本交给上报后端;它绝不向模型请求贡献任何内容。
|
||||
无。本包只观察会话流,并把脱敏后的副本交给上报后端;它绝不向模型请求贡献任何内容。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Capture coordinator: the seam's upstream half. Live capture subscribes to
|
||||
* Capture coordinator for the telemetry capability. Live capture subscribes to
|
||||
* the session firehose plus the one live-bus relay (`agent/error`). Both
|
||||
* capture paths apply the fixed chunk projection, build logical records, and
|
||||
* run each through the
|
||||
@@ -159,7 +159,7 @@ export class TelemetryCoordinator {
|
||||
* at or below the start still feed the projection state (first-chunk
|
||||
* tracking) without being re-handed, so a resumed fiber drops mid-step
|
||||
* chunk continuations exactly like the fiber that saw the step begin. The
|
||||
* cost, accepted with the seam's at-most-once stance: a resume no longer
|
||||
* cost, accepted with the capture contract's at-most-once stance: a resume no longer
|
||||
* backfills records a previous process failed to deliver.
|
||||
* @param session - the live session to adopt; a second adoption is a no-op.
|
||||
*/
|
||||
@@ -204,7 +204,7 @@ export class TelemetryCoordinator {
|
||||
|
||||
/**
|
||||
* Run the `telemetry/record` waterfall at capture time. The innermost `next`
|
||||
* passes the record through unchanged — the seam ships no rules; exported
|
||||
* passes the record through unchanged — this package ships no rules; exported
|
||||
* data is as clean as the listeners a deployment mounts. Callers run inside
|
||||
* {@link contain}, so a throwing rule withholds the record instead of
|
||||
* reaching the loop (fail-closed). On-demand capture invokes this waterfall
|
||||
@@ -289,7 +289,7 @@ function severityOf(event: SessionEvent): TelemetrySeverity {
|
||||
case 'turn/end':
|
||||
return event.data.reason.kind === 'error' ? 'error' : 'info'
|
||||
default:
|
||||
// Merge-extensible fall-through (no assertNever): event types this seam
|
||||
// Merge-extensible fall-through (no assertNever): event types this coordinator
|
||||
// does not depend on — including plugin-merged ones it never heard of —
|
||||
// pass through as info; their owners' outcome semantics stay theirs.
|
||||
return 'info'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Telemetry seam for the DeepSeek Harness.
|
||||
* Telemetry Service Definition for the DeepSeek Harness.
|
||||
*
|
||||
* The seam owns the CAPTURE side of session-event reporting — which records
|
||||
* This package owns the CAPTURE side of session-event reporting — which records
|
||||
* exist (the chunk projection), what they carry (the logical record), when
|
||||
* they are captured (adoption, the per-append firehose, lifecycle
|
||||
* forwarding), live versus on-demand canonical-log capture, and the HMR
|
||||
@@ -24,7 +24,7 @@ declare module 'cordis' {
|
||||
interface Events {
|
||||
/**
|
||||
* Transform one outbound record before it reaches the backend. This
|
||||
* waterfall is the seam's redaction extension point. It ships NO rules
|
||||
* waterfall is the Service Definition's redaction extension point. It ships NO rules
|
||||
* of its own: the
|
||||
* innermost `next()` passes the record through unchanged, and with no
|
||||
* listener mounted records reach the backend as captured, so exported
|
||||
@@ -55,7 +55,7 @@ declare module 'cordis' {
|
||||
export type TelemetrySeverity = 'info' | 'warn' | 'error'
|
||||
|
||||
/**
|
||||
* One logical record handed to a backend — the seam's whole outbound
|
||||
* One logical record handed to a backend — the capture contract's whole outbound
|
||||
* vocabulary. Ledger records mirror session-log events one-to-one;
|
||||
* operational records (`channel: 'ops'`) carry the two signals with no log
|
||||
* home (`agent-error`, `shutdown`) and deliberately omit `event.seq`-style
|
||||
@@ -143,7 +143,7 @@ export abstract class Telemetry extends Service implements TelemetryBackend {
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link TelemetryBackend.emit} — the seam declaration is the contract's one home.
|
||||
* See {@link TelemetryBackend.emit} — that declaration is the contract's one home.
|
||||
* @param record - the logical record to report; owned by the backend after the call.
|
||||
*/
|
||||
abstract emit(record: TelemetryRecord): void
|
||||
|
||||
@@ -15,7 +15,7 @@ export const name = 'session-telemetry-invariant'
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: the seam's whole output is the backend handoff — a
|
||||
* No runtime invariant: the package's whole output is the backend handoff — a
|
||||
* synchronous `emit()` call outside every authoritative event stream — and its
|
||||
* capture side never appends session events, so no event/data relation exists
|
||||
* for an independent companion to observe.
|
||||
|
||||
@@ -1,6 +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/session-title/session-title-all-messages-llm/README.md
|
||||
# pnpm run verify-translation-pairing --write packages/session/session-title-all-messages-llm/README.md
|
||||
README.md: 25ec92432b5c2d18624f2f4d851552ee2b3bf5d0
|
||||
README.zh.md: 947aa614741b2b0bd50e70f1d273a625a5624cc6
|
||||
|
||||
@@ -1,6 +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/session-title/session-title-first-message-llm/README.md
|
||||
# pnpm run verify-translation-pairing --write packages/session/session-title-first-message-llm/README.md
|
||||
README.md: c24d4de1fa945d11b5a868cad88a94ed14ad3891
|
||||
README.zh.md: 1f864b16c473654d0a52716cddbe26fe087860dc
|
||||
|
||||
@@ -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-title-llm/README.md
|
||||
README.md: 342687b5aa0cd35a70abf8cc66b3fe80342bce0b
|
||||
README.zh.md: 10496a727baa8a515f831feb425bc3d66ad91b5f
|
||||
README.md: db03205cabf9f723ee9bf6ad122026d75cb92113
|
||||
README.zh.md: a214caae84aeee7a3a5c90f55e8a01ea957b823f
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Shared implementation policy for model-backed session-title providers. It resolves the auxiliary route, frames exact selected human messages as JSON, records the exact dispatchable request, applies a language-aware title instruction, enforces input and output budgets, composes timeout and caller cancellation, assembles the stream, and returns normalized text with exact source seqs and model provenance.
|
||||
Shared implementation policy for model-backed session-title providers. It resolves the auxiliary route, frames exact selected human messages as JSON, records the exact dispatchable request, applies a language-aware title instruction, enforces input and output budgets, composes timeout and caller cancellation, assembles the stream, and returns normalized text with exact source seqs plus the provider/model route used to generate it.
|
||||
|
||||
This package is a library, not a Cordis plugin. The provider plugins call `registerSessionTitleLlmProvider()` with their cadence and message selector; it validates shared config and delegates each revision to `generateSessionTitleWithLlm()`, so registration, route, prompt, cancellation, and validation behavior cannot drift between them.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
由模型支持的会话标题提供方的共享实现策略。它解析辅助路由,将精确选中的用户消息封装为 JSON,记录可分发的确切请求,应用语言感知的标题指令,强制执行输入和输出预算,组合超时与调用方取消,组装流,并返回带有确切来源 seq 和模型来源信息的规范化文本。
|
||||
由模型支持的会话标题提供方的共享实现策略。它解析辅助路由,将精确选中的用户消息封装为 JSON,记录可分发的确切请求,应用语言感知的标题指令,强制执行输入和输出预算,组合超时与调用方取消,组装流,并返回规范化文本,同时给出确切来源 seq 以及生成该文本时使用的提供方/模型路由。
|
||||
|
||||
此包是普通库,不是 Cordis 插件。提供方插件调用 `registerSessionTitleLlmProvider()`,传入各自节奏与消息选择器;该函数验证共享配置,并将每次修订委派给 `generateSessionTitleWithLlm()`,使各插件的注册、路由、提示词、取消与验证行为不会漂移。
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ export type SessionTitleLlmMessageSelector = (
|
||||
* Register one model-backed provider through the shared configuration and call policy.
|
||||
* @param ctx - context exposing the title and LLM services.
|
||||
* @param config - untrusted required deployment policy.
|
||||
* @param id - stable plugin identity recorded in title provenance.
|
||||
* @param id - stable plugin id recorded with generated titles.
|
||||
* @param automatic - provider-owned automatic generation cadence.
|
||||
* @param selectMessages - exact source-message selection for one revision.
|
||||
*/
|
||||
|
||||
@@ -16,7 +16,7 @@ export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this stateless helper validates and freezes each auxiliary request before
|
||||
* dispatch; deadline, stream, and provenance relationships are checked synchronously and by tests.
|
||||
* dispatch; deadline, stream, cited message seqs, and provider/model fields are checked synchronously and by tests.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
|
||||
@@ -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-title/README.md
|
||||
README.md: 13a2a1c298c3abb8c8987b9373edc46a071ce9db
|
||||
README.zh.md: 075e9e31fc7acbc76f0e3fd7a2642b4d879569d3
|
||||
README.md: e7004eb2e03757cb28b4b5756a876e1dee4b6229
|
||||
README.zh.md: 45c1aafdf214f63ae7a071d2558eaefec13ea70f
|
||||
|
||||
@@ -29,7 +29,7 @@ All limits are required; the library supplies no defaults.
|
||||
|
||||
## Provider contract
|
||||
|
||||
A provider supplies a branded stable id, automatic mode (`first-message` or `all-user-messages`), and `generate(request)`. The request carries the live session, all eligible messages through one fixed revision, the current logged main-request route when available, and cancellation. The result identifies a non-empty title, unique ordered source-message seqs from that request, and optional model provenance. The service normalizes and validates the result before it becomes durable.
|
||||
A provider supplies a branded stable id, automatic mode (`first-message` or `all-user-messages`), and `generate(request)`. The request carries the live session, all eligible messages through one fixed revision, the current logged main-request route when available, and cancellation. The result identifies a non-empty title, unique ordered source-message seqs from that request, and the optional provider/model route used to generate it. The service normalizes and validates the result before it becomes durable.
|
||||
|
||||
See the [session-title data structures](../../../docs/subsystems/session-title.md) and [implemented decision](../../../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md).
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Fork 出的会话会原样继承种子中的标题事件。首消息节奏不会
|
||||
|
||||
## 提供方约定
|
||||
|
||||
提供方会提供带品牌类型的稳定 id、自动模式(`first-message` 或 `all-user-messages`)和 `generate(request)`。请求携带活跃会话、截至一次固定修订的所有符合条件消息、可用时当前已记录的主请求路由,以及取消信号。结果包含非空标题、该请求中唯一且有序的来源消息 seq,以及可选的模型来源信息。服务会在结果持久保存前进行规范化和验证。
|
||||
提供方会提供带品牌类型的稳定 id、自动模式(`first-message` 或 `all-user-messages`)和 `generate(request)`。请求携带活跃会话、截至一次固定修订的所有符合条件消息、可用时当前已记录的主请求路由,以及取消信号。结果包含非空标题、该请求中唯一且有序的来源消息 seq,以及生成该标题时使用的可选提供方/模型路由。服务会在结果持久保存前进行规范化和验证。
|
||||
|
||||
参见[会话标题数据结构](../../../docs/subsystems/session-title.md)与[已实现决策](../../../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md)。
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Log-backed session title service, deterministic fallback, and provider seam.
|
||||
* Log-backed session title service, deterministic fallback, and provider contract.
|
||||
* @module @deepseek-ai/dsh-session-title
|
||||
*/
|
||||
|
||||
@@ -63,7 +63,7 @@ export interface SessionTitleEventData {
|
||||
readonly title: string
|
||||
/** Exact human `user/message` seqs used to derive this title; empty for an explicit user rename. */
|
||||
readonly messageSeqs: number[]
|
||||
/** Built-in fallback, registered-provider, or explicit-user provenance. */
|
||||
/** Whether the built-in fallback, a registered provider, or the user supplied the title. */
|
||||
readonly source: SessionTitleSource
|
||||
}
|
||||
|
||||
@@ -146,14 +146,14 @@ export interface SessionTitleProviderResult {
|
||||
|
||||
/** One optional asynchronous title implementation registered with the service. */
|
||||
export interface SessionTitleProvider {
|
||||
/** Stable provider identity recorded in title provenance. */
|
||||
/** Stable id of the provider recorded with the title. */
|
||||
readonly id: SessionTitleProviderId
|
||||
/** When new human prompts start automatic generation. */
|
||||
readonly automatic: SessionTitleAutomaticMode
|
||||
/**
|
||||
* Produce one title revision.
|
||||
* @param request - message snapshot, current route, session, and cancellation.
|
||||
* @returns proposed title plus exact input seqs and optional model provenance.
|
||||
* @returns proposed title plus exact input seqs and the optional provider/model route used to generate it.
|
||||
*/
|
||||
generate(request: SessionTitleProviderRequest): Promise<SessionTitleProviderResult>
|
||||
}
|
||||
@@ -615,12 +615,12 @@ export class SessionTitleService extends Service {
|
||||
let model: SessionTitleModelProvenance | undefined
|
||||
if (modelCandidate !== undefined) {
|
||||
if (modelCandidate === null || typeof modelCandidate !== 'object') {
|
||||
throw new Error('session-title provider model provenance requires non-empty provider and model')
|
||||
throw new Error('session-title provider result model must contain non-empty provider and model strings')
|
||||
}
|
||||
const record = modelCandidate as Record<string, unknown>
|
||||
if (typeof record.provider !== 'string' || record.provider.length === 0
|
||||
|| typeof record.model !== 'string' || record.model.length === 0) {
|
||||
throw new Error('session-title provider model provenance requires non-empty provider and model')
|
||||
throw new Error('session-title provider result model must contain non-empty provider and model strings')
|
||||
}
|
||||
model = { provider: record.provider, model: record.model }
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const name = 'session-title-invariant'
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* Durable title-provenance invariant: an automatic title always cites at
|
||||
* Durable title-source invariant: an automatic title always cites at
|
||||
* least one human `user/message` seq, and an explicit user rename cites none
|
||||
* — `messageSeqs` is empty iff `source.kind` is `user`. Provider revisions
|
||||
* are validated by the service before their append; this checks the durable
|
||||
@@ -32,7 +32,8 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant
|
||||
if (event.type !== 'session/title') return
|
||||
const { source, messageSeqs } = event.data
|
||||
if ((messageSeqs.length === 0) !== (source.kind === 'user')) {
|
||||
fail(`session/title event ${String(event.seq)} breaks provenance: source "${source.kind}" with ${String(messageSeqs.length)} cited message seq(s)`)
|
||||
const requirement = source.kind === 'user' ? 'cite no message seqs' : 'cite at least one message seq'
|
||||
fail(`session/title event ${String(event.seq)} with source "${source.kind}" must ${requirement}; got ${String(messageSeqs.length)}`)
|
||||
}
|
||||
}, { global: true })
|
||||
}, { inject: ['sessions'] })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Title-provenance invariant: messageSeqs is empty iff source.kind is 'user'
|
||||
// Title-source invariant: `messageSeqs` is empty iff `source.kind` is `user`.
|
||||
// — the durable relationship every appended session/title event must keep.
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
@@ -14,7 +14,7 @@ async function setup(): Promise<Context> {
|
||||
return ctx
|
||||
}
|
||||
|
||||
describe('session-title provenance invariant', () => {
|
||||
describe('session-title source invariant', () => {
|
||||
it('accepts cited automatic titles and citation-free user renames', async () => {
|
||||
const ctx = await setup()
|
||||
const session = ctx.sessions.create(SessionId('title-invariant-valid'))
|
||||
|
||||
@@ -436,12 +436,12 @@ describe('SessionTitleService provider validation and stale scheduling', () => {
|
||||
{ value: { title: 'valid', messageSeqs: [999] }, error: /unique, ordered seqs/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq, first.seq] }, error: /unique, ordered seqs/ },
|
||||
{ value: { title: 'valid', messageSeqs: [second.seq, first.seq] }, error: /unique, ordered seqs/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: null }, error: /model provenance/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: 'route' }, error: /model provenance/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: { provider: 1, model: 'm' } }, error: /model provenance/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: { provider: '', model: 'm' } }, error: /model provenance/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: { provider: 'p', model: 1 } }, error: /model provenance/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: { provider: 'p', model: '' } }, error: /model provenance/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: null }, error: /provider result model/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: 'route' }, error: /provider result model/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: { provider: 1, model: 'm' } }, error: /provider result model/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: { provider: '', model: 'm' } }, error: /provider result model/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: { provider: 'p', model: 1 } }, error: /provider result model/ },
|
||||
{ value: { title: 'valid', messageSeqs: [first.seq], model: { provider: 'p', model: '' } }, error: /provider result model/ },
|
||||
]
|
||||
for (const item of cases) {
|
||||
result = item.value
|
||||
|
||||
Reference in New Issue
Block a user