@lqc123qwe/car-runtime 1.14.0 → 1.15.0

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.
@@ -1,4 +1,4 @@
1
- export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' | 'usage' | 'compact' | 'routing';
1
+ export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' | 'usage' | 'compact' | 'routing' | 'memory';
2
2
  export type Actor = 'user' | 'model' | 'plugin' | 'runtime';
3
3
  export interface SessionEvent {
4
4
  seq: number;
@@ -15,6 +15,12 @@ declare const sha: (s: string) => string;
15
15
  export interface ModelSnapshot {
16
16
  /** 快照对应日志前缀边界(事件数),断言时逐字节比对前缀投影 */
17
17
  atSeq: number;
18
+ /**
19
+ * 1.15-S2(K-STEER-1 / D-47):快照时宿主声明的活跃 turn——提供时快照投影 = 边界内
20
+ * 「活跃 turn 起点之后落链的他 turn user 事件被过滤」口径(与请求投影同构,N1 逐字节
21
+ * 自洽);缺席 = 全量前缀投影(1.14.1 形态,行为零变化)。
22
+ */
23
+ activeTurnId?: string;
18
24
  request: unknown[];
19
25
  }
20
26
  export declare class SessionLog {
@@ -30,8 +36,10 @@ export declare class SessionLog {
30
36
  attachSink(sink: (line: string) => void): void;
31
37
  /** 唯一写入路径(append-only;无 update/delete API) */
32
38
  append(actor: Actor, kind: EventKind, turnId: string, payload: unknown, meta?: Record<string, unknown>): SessionEvent;
33
- /** 模型请求发起时调用(agent-loop 集成点):登记「请求时快照」 */
34
- snapshotModelRequest(): ModelSnapshot;
39
+ /** 模型请求发起时调用(agent-loop 集成点):登记「请求时快照」;opts.activeTurnId 见 ModelSnapshot 注记 */
40
+ snapshotModelRequest(opts?: {
41
+ activeTurnId?: string;
42
+ }): ModelSnapshot;
35
43
  /** 逐链校验;返回 null=完整,否则断链事件序号(审计导出前置,断链即中止) */
36
44
  verifyChain(): number | null;
37
45
  /**
@@ -45,12 +53,20 @@ export declare class SessionLog {
45
53
  * 其 compactedUpTo 前缀折叠为单条 system 摘要(事件原文不改写,仅投影层折叠);compact 之前的
46
54
  * 请求快照(atSeq ≤ compact.seq)不含 compact 事件 → 投影仍为全量形态(N1 不变量自洽——
47
55
  * 「请求时发出 = 日志前缀投影」逐快照成立);无 compact 事件的日志投影与 1.13 逐字节一致。
56
+ * 1.15-S2(K-STEER-1 / D-47)投影隔离:opts.activeTurnId 提供时,跳过「turn 进行中落链的
57
+ * 他 turn user 事件」(特征 = kind='user' 且 seq > 活跃 turn 首事件 seq 且 turnId ≠ 活跃
58
+ * turnId——引导排队事件的链上形态)——**引导 ≠ 注入当前 turn 上下文**;不采用 turn 开始
59
+ * 快照(快照会误杀 turn 内落的 toolResult——agent loop 依赖实时投影),过滤口径精确匹配
60
+ * 「turn 外增量不进、turn 内增量照进」;无引导事件时过滤零命中,投影与 1.14.1 逐字节一致
61
+ * (兼容线)。缺省(无 opts)= 1.14.1 行为零变化。
48
62
  */
49
- deriveMessages(upTo?: number): Array<{
63
+ deriveMessages(upTo?: number, opts?: {
64
+ activeTurnId?: string;
65
+ }): Array<{
50
66
  role: string;
51
67
  [k: string]: unknown;
52
68
  }>;
53
- /** N1 不变量断言(CI 门禁雏形):全部快照必须与当前日志前缀投影逐字节一致 */
69
+ /** N1 不变量断言(CI 门禁雏形):全部快照必须与当前日志前缀投影逐字节一致(快照口径含 1.15-S2 activeTurnId 过滤) */
54
70
  assertModelVisibleLogged(): {
55
71
  ok: boolean;
56
72
  failedAt?: number;
@@ -41,9 +41,15 @@ export class SessionLog {
41
41
  this.#tail = full.hash;
42
42
  return full;
43
43
  }
44
- /** 模型请求发起时调用(agent-loop 集成点):登记「请求时快照」 */
45
- snapshotModelRequest() {
46
- const snap = { atSeq: this.events.length, request: structuredClone(this.deriveMessages(this.events.length)) };
44
+ /** 模型请求发起时调用(agent-loop 集成点):登记「请求时快照」;opts.activeTurnId 见 ModelSnapshot 注记 */
45
+ snapshotModelRequest(opts) {
46
+ const filter = opts?.activeTurnId ? { activeTurnId: opts.activeTurnId } : undefined;
47
+ const snap = {
48
+ atSeq: this.events.length,
49
+ // 1.15-S2:口径必须随快照落盘——assert 重算时以同口径投影逐字节比对(漏存 = 口径丢失 → N1 假红)
50
+ ...(filter ? { activeTurnId: filter.activeTurnId } : {}),
51
+ request: structuredClone(this.deriveMessages(this.events.length, filter)),
52
+ };
47
53
  this.#snapshots.push(snap);
48
54
  return snap;
49
55
  }
@@ -68,8 +74,14 @@ export class SessionLog {
68
74
  * 其 compactedUpTo 前缀折叠为单条 system 摘要(事件原文不改写,仅投影层折叠);compact 之前的
69
75
  * 请求快照(atSeq ≤ compact.seq)不含 compact 事件 → 投影仍为全量形态(N1 不变量自洽——
70
76
  * 「请求时发出 = 日志前缀投影」逐快照成立);无 compact 事件的日志投影与 1.13 逐字节一致。
77
+ * 1.15-S2(K-STEER-1 / D-47)投影隔离:opts.activeTurnId 提供时,跳过「turn 进行中落链的
78
+ * 他 turn user 事件」(特征 = kind='user' 且 seq > 活跃 turn 首事件 seq 且 turnId ≠ 活跃
79
+ * turnId——引导排队事件的链上形态)——**引导 ≠ 注入当前 turn 上下文**;不采用 turn 开始
80
+ * 快照(快照会误杀 turn 内落的 toolResult——agent loop 依赖实时投影),过滤口径精确匹配
81
+ * 「turn 外增量不进、turn 内增量照进」;无引导事件时过滤零命中,投影与 1.14.1 逐字节一致
82
+ * (兼容线)。缺省(无 opts)= 1.14.1 行为零变化。
71
83
  */
72
- deriveMessages(upTo) {
84
+ deriveMessages(upTo, opts) {
73
85
  const scoped = upTo !== undefined ? this.events.slice(0, upTo) : this.events;
74
86
  let compactEvent;
75
87
  for (const e of scoped) {
@@ -84,7 +96,21 @@ export class SessionLog {
84
96
  }
85
97
  compactedUpTo = cp.compactedUpTo; // 折叠边界 = payload 声明的前缀边界(非 compact 事件自身 seq)
86
98
  }
87
- const projected = compactedUpTo !== undefined ? scoped.filter(e => e.seq >= compactedUpTo) : scoped;
99
+ // 1.15-S2 投影隔离(D-47):活跃 turn 首事件 seq 之后落链的他 turn user 事件 → 跳过
100
+ let steerSkip;
101
+ if (opts?.activeTurnId) {
102
+ let turnStartSeq = -1;
103
+ for (const e of scoped) {
104
+ if (e.turnId === opts.activeTurnId) {
105
+ turnStartSeq = e.seq;
106
+ break;
107
+ }
108
+ }
109
+ if (turnStartSeq >= 0) {
110
+ steerSkip = (e) => e.kind === 'user' && e.turnId !== opts.activeTurnId && e.seq > turnStartSeq;
111
+ } // 活跃 turn 无链上事件(异常形态)→ 不过滤(fail-safe 退化为 1.14.1 全量投影)
112
+ }
113
+ const projected = compactedUpTo !== undefined ? scoped.filter(e => e.seq >= compactedUpTo && !(steerSkip?.(e) ?? false)) : scoped.filter(e => !(steerSkip?.(e) ?? false));
88
114
  const lastSeqById = new Map();
89
115
  for (const e of projected) {
90
116
  if (e.kind !== 'toolResult')
@@ -114,10 +140,11 @@ export class SessionLog {
114
140
  }
115
141
  return out;
116
142
  }
117
- /** N1 不变量断言(CI 门禁雏形):全部快照必须与当前日志前缀投影逐字节一致 */
143
+ /** N1 不变量断言(CI 门禁雏形):全部快照必须与当前日志前缀投影逐字节一致(快照口径含 1.15-S2 activeTurnId 过滤) */
118
144
  assertModelVisibleLogged() {
119
145
  for (const s of this.#snapshots) {
120
- if (JSON.stringify(s.request) !== JSON.stringify(this.deriveMessages(s.atSeq))) {
146
+ const filter = s.activeTurnId ? { activeTurnId: s.activeTurnId } : undefined;
147
+ if (JSON.stringify(s.request) !== JSON.stringify(this.deriveMessages(s.atSeq, filter))) {
121
148
  return { ok: false, failedAt: s.atSeq };
122
149
  }
123
150
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 1.15-S2 · 引导消息排队扫描(K-STEER-1 / D-V02-9 / D-47 ②)
3
+ *
4
+ * 口径(红线先立后码):
5
+ * - **消费契约**:内核出排队扫描查询面(未消费 user 事件按 seq 序枚举),FIFO 续跑调度
6
+ * 在宿主装配层(turn 收口 → 扫描 → 逐条以事件自身 turnId 调 runTurn)——内核不改
7
+ * runTurn 调度语义(D-40 同款宿主装配层纪律);
8
+ * - **pending 判定**:kind='user' 且该 turnId 无 turnEnd 事件(未闭合)。当前执行中 turn
9
+ * 也未闭合但正在消费——由 opts.excludeTurnId 排除(宿主传活跃 turnId);已消费(turnEnd
10
+ * 已落)/ 已被 recover 补记 interrupted(turnEnd 已落,R-8 定性:消息已发未跑,重启后
11
+ * 不重复消费)均不算 pending;
12
+ * - **逐条成 turn 不合并**:每条 pending 条目 = 一条 user 事件(含 turnId 与发出时刻 ts/
13
+ * seq)——宿主逐条消费;同 turnId 多条 user 事件逐条报出(宿主守规形态 = 每条引导消息
14
+ * append 时分配独立 turnId);
15
+ * - **发出时刻落链**:引导 append 本身零包装(SessionLog.append 任意时刻可调——执行中
16
+ * append 无需内核加「允许」逻辑,D-47 ④);**投影隔离红线**(引导不进当前 turn 投影)
17
+ * 在 log.ts deriveMessages activeTurnId 口径落地,非本文件职责;
18
+ * - **model_visible=0**:pending user 事件在其所属 turn 被消费前不进任何模型请求
19
+ * (消费后以正常 user 事件身份进该 turn 投影——Model-visible means logged 由构造保证)。
20
+ */
21
+ import type { SessionLog } from './log.ts';
22
+ export interface PendingSteerTurn {
23
+ /** 引导 user 事件 seq(发出时刻链上位置——FIFO 序) */
24
+ seq: number;
25
+ /** 引导 user 事件 ts(发出时刻时间戳——链序校验口径) */
26
+ ts: number;
27
+ /** 引导事件 turnId(发出时分配的新 turnId——消费时宿主以同 turnId 调 runTurn) */
28
+ turnId: string;
29
+ /** 引导消息正文(user 事件 payload 原样) */
30
+ text: unknown;
31
+ }
32
+ /** 排队扫描:未闭合 user 事件按 seq 序枚举(未消费引导队列——宿主 FIFO 续跑消费面) */
33
+ export declare function pendingSteerTurns(log: SessionLog, opts?: {
34
+ excludeTurnId?: string;
35
+ }): PendingSteerTurn[];
@@ -0,0 +1,19 @@
1
+ /** 排队扫描:未闭合 user 事件按 seq 序枚举(未消费引导队列——宿主 FIFO 续跑消费面) */
2
+ export function pendingSteerTurns(log, opts = {}) {
3
+ const closed = new Set();
4
+ for (const e of log.events) {
5
+ if (e.kind === 'turnEnd')
6
+ closed.add(e.turnId);
7
+ }
8
+ const out = [];
9
+ for (const e of log.events) {
10
+ if (e.kind !== 'user')
11
+ continue;
12
+ if (closed.has(e.turnId))
13
+ continue; // 已消费 / 已 recover 补记 interrupted——不算 pending
14
+ if (opts.excludeTurnId && e.turnId === opts.excludeTurnId)
15
+ continue; // 当前执行中 turn
16
+ out.push({ seq: e.seq, ts: e.ts, turnId: e.turnId, text: e.payload });
17
+ }
18
+ return out;
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lqc123qwe/car-runtime",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "Composable Agent Runtime (CAR) - microkernel plugin system with sandboxed PTC, append-only hashed session log, and MCP host bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/loop/stop.ts CHANGED
@@ -175,7 +175,9 @@ export async function runTurn(opts: {
175
175
  return { reason: 'aborted', steps, endReasonTrail: trail }
176
176
  }
177
177
  steps++
178
- log.snapshotModelRequest() // agent-loop 集成点:请求时快照(N1)
178
+ // 1.15-S2(K-STEER-1 / D-47):快照带活跃 turn 口径——与请求投影(chatStep 内
179
+ // activeTurnId 过滤)同构,N1 逐快照自洽;无引导事件时与 1.14.1 逐字节一致
180
+ log.snapshotModelRequest({ activeTurnId: turnId }) // agent-loop 集成点:请求时快照(N1)
179
181
  const step = await model()
180
182
 
181
183
  if (step.stopReason === 'error') {
@@ -0,0 +1,288 @@
1
+ /**
2
+ * 1.15-S1 · 白盒记忆面(K-MEM-1 / D-46)——存储 + CRUD 落链 + 注入装载
3
+ *
4
+ * 口径(红线先立后码):
5
+ * - **审计链同源(差异化核心,D-V02-2)**:记忆 write/edit/delete/rollback 全部落会话
6
+ * JSONL 审计事件(EventKind='memory',actor='user',turnId='memory'——会话级能力面,
7
+ * 同 'compaction' 先例),哈希链保护——记忆篡改在链上可见;**链即版本库**:rollback
8
+ * 的恢复数据源 = 链上历史事件的 payload(before/after 全文),不依赖文件系统历史;
9
+ * - **append-only 红线**:CRUD 只走 SessionLog.append(SessionLog 无 update/delete API);
10
+ * delete 与 rollback 均为补偿事件追加,已落链事件零改写;node:test 断言操作前后
11
+ * verifyChain() 均 null 且旧事件逐字节不变;
12
+ * - **存储形态(D-46 ①)**:`<workspace>/.car/memory/` 逐条 JSON 文件(记忆 id 为文件名,
13
+ * 内含正文 + 元数据),与 `.car/sessions/` 每会话一文件同构——白盒可见性最强、CRUD
14
+ * 单条原子性天然;写入 = 临时文件 + rename 原子替换(半文件不落地);工作区分片 =
15
+ * 不同 dir 的 store 实例天然隔离;id 即文件名——形态校验拒绝路径穿越(fail-visible);
16
+ * - **落链作用域(D-46 ②)**:CRUD 事件落调用方传入的会话链(宿主指定当前活跃会话
17
+ * 承载);log 参数必填——无会话上下文调用显式拒绝(CAR-E-MEM-NOCHAIN,fail-visible,
18
+ * 不留旁路写入通道);
19
+ * - **落链与文件动作顺序(§3.2.M7.5 同源)**:先落链(sink 成功后才入内存)后写文件——
20
+ * 审计链为权威;文件动作失败 CAR-E-MEM-IO 显式抛出(调用方知情,fail-visible);
21
+ * - **注入装载(D-46 ③④)**:loadMemoryForInjection 出确定性拼装文本(updatedAt 降序,
22
+ * 全量装入——禁静默选择性注入),字符近似预算门控(行为门控非计量出数,D-41 不适用);
23
+ * 超预算 CAR-E-MEM-BUDGET fail-visible 不静默截断;注入落链由宿主装配层执行(turn
24
+ * 开始前 append system ctx 行 = D-C3 预留位,Model-visible means logged 由构造保证);
25
+ * - **零凭据路径**:本模块不读 env、不触 CredentialService(spec 行为断言锁定);
26
+ * - **零新依赖**:node:fs / node:crypto 内置件;v0.2 收窄 = 手动白盒(D-V02-4)——
27
+ * 自动抽取/Dream/向量检索/全文匹配过滤均不做(登记可溯)。
28
+ */
29
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
30
+ import { join } from 'node:path'
31
+ import { randomBytes } from 'node:crypto'
32
+ import type { SessionLog } from '../session/log.ts'
33
+
34
+ export type MemoryOp = 'write' | 'edit' | 'delete' | 'rollback'
35
+
36
+ export interface MemoryEntry {
37
+ id: string
38
+ text: string
39
+ tags?: string[]
40
+ createdAt: number
41
+ updatedAt: number
42
+ /** 创建来源会话(write 时调用方提供;可溯性辅助,非鉴权字段) */
43
+ sourceSession?: string
44
+ }
45
+
46
+ /** 链上 memory 事件 payload 统一形态(kind='memory',payload.type 区分操作) */
47
+ export interface MemoryEventPayload {
48
+ type: MemoryOp
49
+ id: string
50
+ /** 操作后状态正文:write 的初始正文 / edit 的新正文 / rollback 的恢复正文;delete 与回退到删除态时缺席 */
51
+ text?: string
52
+ /** 操作前状态正文:edit 的被替换正文 / delete 的被删正文 / rollback 的被回退正文(链即版本库的恢复数据源) */
53
+ before?: string
54
+ tags?: string[]
55
+ /** rollback 溯源:被回退到的状态所对应的事件 seq */
56
+ fromSeq?: number
57
+ }
58
+
59
+ /** id 即文件名——禁路径穿越:字母数字开头,仅 [A-Za-z0-9._-],且不接纳以 '.' 开头的隐藏态(write 派生 id 恒字母开头) */
60
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
61
+
62
+ function assertValidId(id: string): void {
63
+ if (typeof id !== 'string' || !ID_RE.test(id) || id.includes('..')) {
64
+ throw new Error(`CAR-E-MEM-ID: 非法记忆 id "${String(id).slice(0, 64)}"(允许:字母/数字开头,[A-Za-z0-9._-],≤128——id 即文件名,路径穿越 fail-visible 拒绝)`)
65
+ }
66
+ }
67
+
68
+ export class MemoryStore {
69
+ readonly dir: string
70
+
71
+ constructor(dir: string) {
72
+ this.dir = dir
73
+ mkdirSync(dir, { recursive: true })
74
+ }
75
+
76
+ #file(id: string): string {
77
+ assertValidId(id)
78
+ return join(this.dir, `${id}.json`)
79
+ }
80
+
81
+ /** 列出全部记忆(updatedAt 降序——最近维护优先);坏文件显式拒绝(带病不静默,loadSessionLog 同纪律) */
82
+ list(): MemoryEntry[] {
83
+ const out: MemoryEntry[] = []
84
+ for (const name of readdirSync(this.dir)) {
85
+ if (!name.endsWith('.json')) continue
86
+ const file = join(this.dir, name)
87
+ let entry: MemoryEntry
88
+ try {
89
+ entry = JSON.parse(readFileSync(file, 'utf8')) as MemoryEntry
90
+ } catch (e) {
91
+ throw new Error(`CAR-E-MEM-FORMAT: 记忆文件 ${name} 非法 JSON——拒绝带病装载(${(e as Error).message})`)
92
+ }
93
+ if (typeof entry?.id !== 'string' || typeof entry?.text !== 'string') {
94
+ throw new Error(`CAR-E-MEM-FORMAT: 记忆文件 ${name} 缺 id/text 字段——拒绝带病装载`)
95
+ }
96
+ out.push(entry)
97
+ }
98
+ return out.sort((a, b) => b.updatedAt - a.updatedAt)
99
+ }
100
+
101
+ get(id: string): MemoryEntry | undefined {
102
+ const file = this.#file(id)
103
+ if (!existsSync(file)) return undefined
104
+ const entry = JSON.parse(readFileSync(file, 'utf8')) as MemoryEntry
105
+ if (typeof entry?.id !== 'string' || typeof entry?.text !== 'string') {
106
+ throw new Error(`CAR-E-MEM-FORMAT: 记忆文件 ${id}.json 缺 id/text 字段——拒绝带病装载`)
107
+ }
108
+ return entry
109
+ }
110
+
111
+ /** 原子写(临时文件 + rename——半文件不落地) */
112
+ #writeFile(entry: MemoryEntry): void {
113
+ const file = this.#file(entry.id)
114
+ const tmp = `${file}.tmp`
115
+ writeFileSync(tmp, JSON.stringify(entry, null, 2) + '\n', 'utf8')
116
+ renameSync(tmp, file)
117
+ }
118
+
119
+ /**
120
+ * 创建记忆:落 memory/write 事件(payload 携带完整正文)→ 原子写文件。
121
+ * id 缺省派生 `mem-<ts36>-<rand>`;对已存在 id 冲突显式拒绝(改用 edit)。
122
+ */
123
+ write(log: SessionLog, input: { id?: string; text: string; tags?: string[]; sourceSession?: string }): MemoryEntry {
124
+ if (!log || typeof log.append !== 'function') {
125
+ throw new Error('CAR-E-MEM-NOCHAIN: 记忆 CRUD 必须传入会话链(D-46 ②:落链作用域 = 宿主指定活跃会话;无会话上下文显式拒绝,不留旁路写入通道)')
126
+ }
127
+ if (typeof input?.text !== 'string' || !input.text.trim().length) {
128
+ throw new Error('CAR-E-MEM-EMPTY: 记忆正文为空(fail-visible——空正文条目无白盒价值,禁静默落盘)')
129
+ }
130
+ const id = input.id ?? `mem-${Date.now().toString(36)}-${randomBytes(3).toString('hex')}`
131
+ assertValidId(id)
132
+ if (this.get(id)) {
133
+ throw new Error(`CAR-E-MEM-EXISTS: 记忆 "${id}" 已存在(write 冲突 fail-visible——修改请走 edit)`)
134
+ }
135
+ const now = Date.now()
136
+ const entry: MemoryEntry = { id, text: input.text, createdAt: now, updatedAt: now }
137
+ if (input.tags?.length) entry.tags = [...input.tags]
138
+ if (input.sourceSession) entry.sourceSession = input.sourceSession
139
+ log.append('user', 'memory', 'memory', { type: 'write', id, text: entry.text, ...(entry.tags ? { tags: entry.tags } : {}) } satisfies MemoryEventPayload)
140
+ try {
141
+ this.#writeFile(entry)
142
+ } catch (e) {
143
+ throw new Error(`CAR-E-MEM-IO: 记忆文件写入失败 "${id}"(事件已落链——盘面动作失败显式抛出,§3.2.M7.5 落链先行动作顺序;${(e as Error).message})`)
144
+ }
145
+ return entry
146
+ }
147
+
148
+ /** 修改记忆:落 memory/edit 事件(payload before/after 全文——rollback 的恢复数据源)→ 原子重写 */
149
+ edit(log: SessionLog, id: string, text: string, tags?: string[]): MemoryEntry {
150
+ if (!log || typeof log.append !== 'function') {
151
+ throw new Error('CAR-E-MEM-NOCHAIN: 记忆 CRUD 必须传入会话链(D-46 ②;无会话上下文显式拒绝)')
152
+ }
153
+ const prev = this.#mustGet(id)
154
+ if (typeof text !== 'string' || !text.trim().length) {
155
+ throw new Error(`CAR-E-MEM-EMPTY: 记忆 "${id}" 新正文为空(fail-visible——清空请走 delete)`)
156
+ }
157
+ // tags 语义:undefined = 不变(payload 不带键,重放取前态);数组 = 整体替换(含空数组 =
158
+ // 清空,payload 恒带键——重放 [] 与清空同构,禁「带键/缺键」二义)
159
+ const next: MemoryEntry = { ...prev, text, updatedAt: Date.now() }
160
+ if (tags !== undefined) {
161
+ const { tags: _drop, ...rest } = next as MemoryEntry & { tags?: string[] }
162
+ void _drop
163
+ const cleaned = tags.length ? { ...(rest as MemoryEntry), tags: [...tags] } : rest
164
+ log.append('user', 'memory', 'memory', { type: 'edit', id, before: prev.text, text, tags } satisfies MemoryEventPayload)
165
+ try { this.#writeFile(cleaned as MemoryEntry) } catch (e) {
166
+ throw new Error(`CAR-E-MEM-IO: 记忆文件写入失败 "${id}"(事件已落链;${(e as Error).message})`)
167
+ }
168
+ return cleaned as MemoryEntry
169
+ }
170
+ log.append('user', 'memory', 'memory', { type: 'edit', id, before: prev.text, text } satisfies MemoryEventPayload)
171
+ try { this.#writeFile(next) } catch (e) {
172
+ throw new Error(`CAR-E-MEM-IO: 记忆文件写入失败 "${id}"(事件已落链;${(e as Error).message})`)
173
+ }
174
+ return next
175
+ }
176
+
177
+ /** 删除记忆:落 memory/delete 事件(payload 携带被删正文——rollback 可恢复)→ 删文件 */
178
+ remove(log: SessionLog, id: string): void {
179
+ if (!log || typeof log.append !== 'function') {
180
+ throw new Error('CAR-E-MEM-NOCHAIN: 记忆 CRUD 必须传入会话链(D-46 ②;无会话上下文显式拒绝)')
181
+ }
182
+ const prev = this.#mustGet(id)
183
+ log.append('user', 'memory', 'memory', { type: 'delete', id, before: prev.text } satisfies MemoryEventPayload)
184
+ try {
185
+ rmSync(this.#file(id))
186
+ } catch (e) {
187
+ throw new Error(`CAR-E-MEM-IO: 记忆文件删除失败 "${id}"(事件已落链;${(e as Error).message})`)
188
+ }
189
+ }
190
+
191
+ /**
192
+ * 回滚记忆:沿事件链单步时间回退(undo 栈语义——连续回退逐版本回走:v3→v2→v1)。
193
+ * 恢复数据源 = 链上版本栈重放(链即版本库,不依赖文件系统历史)。落 memory/rollback
194
+ * 事件(before/after 全文 + fromSeq 溯源)→ 文件动作。返回 null = 回退到删除态。
195
+ * 语义:edit 后回退 → 恢复 before;delete 后回退 → undelete(恢复被删正文);
196
+ * 仅剩 write 初始版 / 链上无历史 → CAR-E-MEM-NOREV。
197
+ */
198
+ rollback(log: SessionLog, id: string): MemoryEntry | null {
199
+ if (!log || typeof log.append !== 'function') {
200
+ throw new Error('CAR-E-MEM-NOCHAIN: 记忆 CRUD 必须传入会话链(D-46 ②;无会话上下文显式拒绝)')
201
+ }
202
+ const states = this.#rebuildStates(log, id)
203
+ if (states.length < 2) {
204
+ throw new Error(states.length
205
+ ? `CAR-E-MEM-NOREV: 记忆 "${id}" 已处初始版本(write seq=${states[0]!.seq},无更早状态可回退——fail-visible)`
206
+ : `CAR-E-MEM-NOREV: 记忆 "${id}" 无链上历史(rollback 数据源 = 审计链——链外写入的条目无版本可回退,fail-visible)`)
207
+ }
208
+ const cur = states[states.length - 1]!
209
+ const target = states[states.length - 2]! // 单步回退目标(present 或 absent)
210
+ log.append('user', 'memory', 'memory', {
211
+ type: 'rollback',
212
+ id,
213
+ ...(cur.present ? { before: cur.text! } : {}),
214
+ ...(target.present ? { text: target.text!, ...(target.tags ? { tags: target.tags } : {}) } : {}),
215
+ fromSeq: cur.seq,
216
+ } satisfies MemoryEventPayload)
217
+ try {
218
+ if (target.present) {
219
+ const entry: MemoryEntry = { id, text: target.text!, createdAt: target.createdAt!, updatedAt: Date.now() }
220
+ if (target.tags?.length) entry.tags = [...target.tags]
221
+ if (target.sourceSession) entry.sourceSession = target.sourceSession
222
+ this.#writeFile(entry)
223
+ return entry
224
+ }
225
+ rmSync(this.#file(id))
226
+ return null // 回退到删除态:条目移除(调用方以 get 校验)
227
+ } catch (e) {
228
+ throw new Error(`CAR-E-MEM-IO: 记忆文件动作失败 "${id}"(事件已落链;${(e as Error).message})`)
229
+ }
230
+ }
231
+
232
+ #mustGet(id: string): MemoryEntry {
233
+ const prev = this.get(id)
234
+ if (!prev) {
235
+ throw new Error(`CAR-E-MEM-NOENT: 记忆 "${id}" 不存在(fail-visible——先 write 再操作)`)
236
+ }
237
+ return prev
238
+ }
239
+
240
+ /**
241
+ * 链上版本栈重放:按 seq 序重放该 id 的全部 memory 事件。write/edit → push present;
242
+ * delete → push absent;**rollback → pop(单步时间回退的重放同构——调用时回退一步,
243
+ * 重放时同样回退一步,pop 后栈顶即当前有效状态;连续 rollback 逐版本回走)**。
244
+ * 栈底恒为 write 初始版;每个状态记录来源事件 seq(fromSeq 溯源)+ 进入时的正文/元数据。
245
+ */
246
+ #rebuildStates(log: SessionLog, id: string): Array<{ seq: number; present: boolean; text?: string; tags?: string[]; createdAt?: number; sourceSession?: string }> {
247
+ assertValidId(id)
248
+ const stack: Array<{ seq: number; present: boolean; text?: string; tags?: string[]; createdAt?: number; sourceSession?: string }> = []
249
+ for (const e of log.events) {
250
+ if (e.kind !== 'memory') continue
251
+ const p = (e.payload ?? {}) as MemoryEventPayload
252
+ if (p.id !== id) continue
253
+ const prevTop = stack[stack.length - 1]
254
+ if (p.type === 'write') stack.push({ seq: e.seq, present: true, text: p.text, tags: p.tags, createdAt: e.ts, sourceSession: prevTop?.sourceSession })
255
+ else if (p.type === 'edit') stack.push({ seq: e.seq, present: true, text: p.text, tags: p.tags, createdAt: prevTop?.createdAt ?? e.ts, sourceSession: prevTop?.sourceSession })
256
+ else if (p.type === 'delete') stack.push({ seq: e.seq, present: false })
257
+ else if (p.type === 'rollback') stack.pop() // 单步回退重放同构(栈空 pop 为坏链防御——正常链形不发生)
258
+ }
259
+ return stack
260
+ }
261
+ }
262
+
263
+ export interface MemoryInjectionResult {
264
+ count: number
265
+ totalChars: number
266
+ /** 确定性拼装文本(updatedAt 降序全量)——宿主落 system ctx 行的原文(D-C3 预留位) */
267
+ rendered: string
268
+ }
269
+
270
+ /**
271
+ * 注入装载:全量装入(updatedAt 降序,禁静默选择性注入——选择性 = 不可解释),
272
+ * 字符近似预算门控(行为门控非计量出数;超预算 fail-visible 不静默截断——
273
+ * 提示宿主调大预算或删减记忆)。空记忆 = 空串 + count 0(正常态非错误,宿主跳过注入行)。
274
+ */
275
+ export function loadMemoryForInjection(store: MemoryStore, opts: { budgetChars: number }): MemoryInjectionResult {
276
+ const budget = opts?.budgetChars
277
+ if (typeof budget !== 'number' || !Number.isFinite(budget) || budget <= 0) {
278
+ throw new Error(`CAR-E-MEM-BUDGET: 注入预算非法(${String(budget)})——须为正数(字符近似门控口径)`)
279
+ }
280
+ const entries = store.list()
281
+ if (!entries.length) return { count: 0, totalChars: 0, rendered: '' }
282
+ const lines = entries.map(e => `- [${e.id}] ${e.text.replace(/\r?\n/g, ' ')}`)
283
+ const rendered = `[CAR 记忆注入] 共 ${entries.length} 条:\n${lines.join('\n')}`
284
+ if (rendered.length > budget) {
285
+ throw new Error(`CAR-E-MEM-BUDGET: 记忆注入超预算(${rendered.length} > ${budget} 字符)——fail-visible 禁静默截断(调大预算或删减记忆)`)
286
+ }
287
+ return { count: entries.length, totalChars: rendered.length, rendered }
288
+ }
@@ -37,6 +37,12 @@ export interface ChatStepOptions {
37
37
  adapterId?: string
38
38
  /** 1.4-S4(D-19):turn 取消信号——chunk 间隙与流结束后(finish 检查前)检查,命中抛 TurnAborted */
39
39
  signal?: { aborted: boolean }
40
+ /**
41
+ * 1.14.1(K-MP-1 消费面 / D-39):思考参数透传——chatStep 是宿主 model 回调的唯一路径,
42
+ * 本形参补齐后 reasoning 才经 chatStep 可达(off / 缺席 = 不下发该字段,字节面零痕迹);
43
+ * 模型元数据思考档位判断在宿主侧(models:meta),内核防线 = 未知档位 fail-visible。
44
+ */
45
+ reasoning?: { effort: 'off' | 'low' | 'medium' | 'high' }
40
46
  }
41
47
 
42
48
  /** ModelStep 加法扩展:脱敏计数留痕(S15 secretsRedacted 同名口径) */
@@ -57,7 +63,10 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
57
63
  // 1.5-BUG-1:此前仅透传 m.content,toolCall 属性被丢弃——工具调用历史在后续请求
58
64
  // 静默消失(真实 provider 对无 tool_calls 前驱的 tool 消息直接 400);anthropic E2E
59
65
  // 生产调用点核查(1.5-GO-5)暴露,openai-compat 同样受益。
60
- const messages = log.deriveMessages().map(m => ({
66
+ // 1.15-S2(K-STEER-1 / D-47):投影带活跃 turn 口径——turn 进行中落链的他 turn user
67
+ // 事件(引导排队)不进当前 turn 请求(引导 ≠ 注入当前 turn 上下文红线);无引导事件
68
+ // 时与 1.14.1 投影逐字节一致(兼容线)。
69
+ const messages = log.deriveMessages(undefined, { activeTurnId: turnId }).map(m => ({
61
70
  role: m.role as 'system' | 'user' | 'assistant' | 'toolResult',
62
71
  content: (m as unknown as { toolCall?: unknown }).toolCall !== undefined
63
72
  ? { toolCall: (m as unknown as { toolCall: unknown }).toolCall }
@@ -78,6 +87,8 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
78
87
  tools,
79
88
  ...(maxTokens != null ? { maxTokens } : {}),
80
89
  ...(adapterId ? { adapterId } : {}),
90
+ // 1.14.1(K-MP-1 消费面):reasoning 透传(off/缺席 = 不下发)
91
+ ...(opts.reasoning ? { reasoning: opts.reasoning } : {}),
81
92
  ...(opts.signal ? { signal: opts.signal } : {}),
82
93
  ...(opts.log.sessionId ? { metadata: { sessionId: opts.log.sessionId, turnId, traceId: `${opts.log.sessionId}:${turnId}` } } : {}),
83
94
  })
package/src/seam.ts CHANGED
@@ -22,6 +22,19 @@
22
22
  * mountPlugin 组合呈现的 SKIPPED 标注)+ ReloadManager 出面(热重载 epoch 击穿 + invalidate 语义随本面)+
23
23
  * verifyPluginFile 出面(签名校验挂载点——宿主自有装载路径可复用同一签名门;minisign 轨唯一实装,sigstore
24
24
  * 主轨维持 verifier 骨架期预留——D-43 裁决:登记 strict 期限不引入 sigstore 依赖,engines 硬不兼容见评估报告)。
25
+ * 加法登记:1.14.1 —— B2 前置补出面(1.14 消费面完成):K-MP-2 压缩行为 API(shouldCompact/applyCompaction
26
+ * + CompactionConfig——自动挂载点在宿主装配层〔runTurn 之间〕,立即压缩 API 独立可用,append-only 红线随本面)
27
+ * + K-POOL-1 池装配行为 API(assembleModelPool/probePoolMember + PoolMember/RoutingDecision 类型——
28
+ * 桌面模型池设置 UI 与 doctor 池健康行消费面;routing/decide 落链由宿主接线 onDecide)。
29
+ * 加法登记:1.15.0 —— K-MEM-1 白盒记忆面(T-10 / D-46):MemoryStore 出面(`.car/memory/` 逐条 JSON 存储 +
30
+ * CRUD 落链行为 API——memory/write·edit·delete·rollback 审计事件随会话链哈希链保护,rollback = 链上版本栈
31
+ * 单步回退〔链即版本库〕;log 参数必填 = 无会话上下文显式拒绝)+ loadMemoryForInjection(确定性拼装 + 字符
32
+ * 近似预算门控 fail-visible;注入落链 = 宿主装配层 system ctx 行〔D-C3 预留位〕)+ 5 类型(MemoryEntry/
33
+ * MemoryOp/MemoryEventPayload/MemoryInjectionResult + MemoryStore 随类)+ K-STEER-1 排队扫描(T-17 / D-47):
34
+ * pendingSteerTurns + PendingSteerTurn(未消费引导队列按 seq 序枚举,excludeTurnId 排除执行中 turn;FIFO 消费
35
+ * 调度留宿主装配层;投影隔离〔引导不进当前 turn 投影〕在 deriveMessages/chatStep 内建,随既有符号语义可达
36
+ * 零新符号)。负面清单(不出面):存储引擎内部件(文件读写器/预算计算器/版本栈重放器——#私有方法)、
37
+ * 记忆注入落链执行器(宿主 system ctx 行职责)、消费调度器(宿主 FIFO 续跑职责)、cli 引导接线(非验收必需)。
25
38
  */
26
39
  export { SessionLog, loadSessionLog, deriveSessionId, type SessionEvent, type EventKind, type Actor } from './session/log.ts'
27
40
  export { SessionFileStore } from './session/store.ts'
@@ -50,3 +63,9 @@ export { exportForensicsBundle, verifyBundle, type ForensicsBundle, type ExportM
50
63
  // —— 1.14.0(K-S4-1 / D-43):PluginManager 装载报告面 + 签名校验挂载点 ——
51
64
  export { loadPlugins, ReloadManager, type LoadReportVO, type LoadPluginsResult, type LoadPluginsOptions, type LoadStage, type StageStatus, type PeerConflictChain } from './load/report.ts'
52
65
  export { verifyPluginFile, type SignatureGateOptions, type SigGateResult } from './load/sigGate.ts'
66
+ // —— 1.14.1(B2 前置补出面):K-MP-2 压缩行为 API + K-POOL-1 池装配行为 API(1.14 消费面完成)——
67
+ export { shouldCompact, applyCompaction, type CompactionConfig } from './session/compact.ts'
68
+ export { assembleModelPool, probePoolMember, type PoolMember, type PoolMemberHealth, type RoutingDecision, type RoutingAttempt } from './load/pool.ts'
69
+ // —— 1.15.0(K-MEM-1 / D-46):白盒记忆面 +(K-STEER-1 / D-47):引导消息排队扫描 ——
70
+ export { MemoryStore, loadMemoryForInjection, type MemoryEntry, type MemoryOp, type MemoryEventPayload, type MemoryInjectionResult } from './memory/store.ts'
71
+ export { pendingSteerTurns, type PendingSteerTurn } from './session/steer.ts'