@thincoder/core 0.9.1 → 0.9.3
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.
- package/CHANGELOG.md +63 -0
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/advisor/loop.mjs +2 -2
- package/advisor/run.mjs +1 -1
- package/agent/completion.mjs +3 -1
- package/agent/family-tools.mjs +24 -11
- package/agent/helpers.mjs +11 -2
- package/agent/run-stages.mjs +27 -5
- package/agent/setup-reminders.mjs +67 -11
- package/agent/setup.mjs +6 -0
- package/agent/write-gate.mjs +5 -5
- package/agent-tools/advisor-async.mjs +4 -4
- package/agent-tools/advisor.mjs +3 -3
- package/agent-tools/async-discard.mjs +1 -1
- package/agent-tools/audit-block.mjs +106 -0
- package/agent-tools/batch-lifecycle.mjs +301 -0
- package/agent-tools/batch-segment.mjs +16 -263
- package/agent-tools/batch-skeleton.mjs +156 -0
- package/agent-tools/batch.mjs +410 -0
- package/agent-tools/context.mjs +174 -0
- package/agent-tools/eng.mjs +4 -0
- package/agent-tools/goal.mjs +7 -0
- package/agent-tools/parent-channel.mjs +18 -1
- package/agent-tools/plan.mjs +39 -5
- package/agent-tools/read-history.mjs +122 -24
- package/agent-tools/settings.mjs +4 -2
- package/agent-tools/subagent-async.mjs +3 -3
- package/agent-tools/subagent-spawn.mjs +29 -101
- package/agent-tools/task.mjs +11 -0
- package/agent-tools.mjs +9 -2
- package/agent.mjs +10 -4
- package/config.mjs +1 -1
- package/context.mjs +66 -121
- package/fts-text.mjs +41 -0
- package/generate-title.mjs +6 -6
- package/i18n.mjs +4 -4
- package/ledger-cmd.mjs +30 -7
- package/ledger-db.mjs +22 -2
- package/ledger-executors.mjs +103 -0
- package/ledger-surface.mjs +15 -8
- package/ledger.mjs +15 -4
- package/manifest.mjs +172 -60
- package/memory/core.mjs +4 -18
- package/memory/schema.mjs +4 -11
- package/package.json +22 -1
- package/prompts/advisor-design.md +1 -1
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +3 -3
- package/prompts/discipline-engineering.md +17 -2
- package/prompts/persona-eng-coder.md +1 -1
- package/prompts/persona-eng-designer.md +1 -1
- package/prompts/persona-engineering.md +9 -6
- package/session-gc.mjs +129 -74
- package/session-index-build.mjs +298 -0
- package/session-index-cmd.mjs +61 -0
- package/session-index-pass.mjs +95 -0
- package/session-index-query.mjs +102 -0
- package/session-index.mjs +285 -0
- package/session-lifecycle.mjs +18 -5
- package/session-slots-manifest.mjs +55 -3
- package/session-stale.mjs +247 -0
- package/token-window.mjs +188 -0
- package/tools/bash.mjs +4 -15
- package/tools/execute.mjs +5 -13
- package/tools/git-checkpoint.mjs +1 -1
- package/tools/git-ext.mjs +23 -20
- package/tools/git-run.mjs +141 -0
- package/tools/git.mjs +56 -45
- package/tools/index.mjs +3 -1
- package/tools/process-tree.mjs +20 -0
- package/tools/shared.mjs +8 -4
- package/traces/trace-cleanup.mjs +109 -0
- package/traces/trace-store.mjs +32 -36
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-index.mjs — 会话派生索引库:打开 / DDL / schema / 行写入原语 / 测试缝(SESSION.md §6.19)。
|
|
3
|
+
*
|
|
4
|
+
* 机制契约全文 = `docs/core/design/SESSION.md` §6.19(D-SE43–D-SE47;D2 单一权威源)。
|
|
5
|
+
* 本档 = 「DB 打开 / DDL / schema / 测试缝」面;查询与 FTS 检索 = `session-index-query.mjs`;
|
|
6
|
+
* 写面(枚举 / 水位增量 / 会话重建 / 延迟拍 / 清行)= `session-index-build.mjs` +
|
|
7
|
+
* `session-index-pass.mjs`(>300 软线按设计「跨档线拆分预案」二分);命令面 = `session-index-cmd.mjs`。
|
|
8
|
+
*
|
|
9
|
+
* **零权威**:主存(槽 JSON + `{槽}.d/` sidecar + manifest)= 真源;本库只读主存(整读 / 偏移
|
|
10
|
+
* `readSync`),**绝不写回** sessions 树;库丢 / 坏 = 重生成(D-SE46 打开即验 + 改名保留自愈)。
|
|
11
|
+
* **零第三方依赖**:仅 `node:` + 仓内相对 import(FTS5 同装配先例 = `memory/schema.mjs`)。
|
|
12
|
+
* 本档静态 import `node:sqlite` ⇒ **消费侧一律动态 import**(W8 契约②——`ledger-db.mjs` 同款:
|
|
13
|
+
* 静态 import 会把 node:sqlite 拉进 `read_history` 的装配期静态闭包)。
|
|
14
|
+
*/
|
|
15
|
+
import { DatabaseSync } from "node:sqlite"
|
|
16
|
+
import { existsSync, mkdirSync, renameSync, rmSync, statSync } from "node:fs"
|
|
17
|
+
import { dirname, join } from "node:path"
|
|
18
|
+
import { configDir } from "./config-io.mjs"
|
|
19
|
+
import { segmentCJK } from "./fts-text.mjs"
|
|
20
|
+
|
|
21
|
+
/** 库名(单库——与 `memory.db` 同区 `~/.thincoder/`;D-SE43)。 */
|
|
22
|
+
export const SESSION_INDEX_DB_NAME = "session-index.db"
|
|
23
|
+
/** schema 版本(`index_meta.v` 不符 ⇒ 重建——不做迁移;D-SE46)。 */
|
|
24
|
+
export const SESSION_INDEX_SCHEMA_VERSION = "1"
|
|
25
|
+
/** 多进程(CLI + VSC 同启)竞争:WAL + busy_timeout(`memory/schema.mjs` 同值)。 */
|
|
26
|
+
export const SESSION_INDEX_BUSY_TIMEOUT = 3000
|
|
27
|
+
/** 水位 `tail_hash` 窗口 = 末段尾 4KB(D-SE45——等长改写检出)。 */
|
|
28
|
+
export const TAIL_HASH_BYTES = 4096
|
|
29
|
+
/** `tool_calls` 输出上限(= 存储面同值——`slimForDisplay` 截断面即精度上限;D-SE47)。 */
|
|
30
|
+
export const ARG_MAX_CHARS = 300
|
|
31
|
+
/** 延迟拍预算(D-SE45 触发点②:单趟 ≤ 2000ms 且 ≤ 40 会话)。 */
|
|
32
|
+
export const INDEX_PASS_BUDGET_MS = 2000
|
|
33
|
+
export const INDEX_PASS_MAX_SESSIONS = 40
|
|
34
|
+
/** 查询窗口缺省 / 上限(与 read_history 参数面同值:缺省 50 / 上限 200)。 */
|
|
35
|
+
export const DEFAULT_QUERY_LIMIT = 50
|
|
36
|
+
export const MAX_QUERY_LIMIT = 200
|
|
37
|
+
|
|
38
|
+
// ── 测试缝(`_setLedgerDirForTest` 同款:测试不碰真实用户目录)─────────────────
|
|
39
|
+
let indexDirOverride = null
|
|
40
|
+
export function _setSessionIndexDirForTest(dir) { indexDirOverride = dir }
|
|
41
|
+
export function _resetSessionIndexDirForTest() { indexDirOverride = null }
|
|
42
|
+
export function sessionIndexDir() { return indexDirOverride ?? configDir }
|
|
43
|
+
export function sessionIndexDbPath() { return join(sessionIndexDir(), SESSION_INDEX_DB_NAME) }
|
|
44
|
+
|
|
45
|
+
/** DDL(四表 + FTS + 元表;`UNIQUE(sid, idx)` 兼作查询主索引——同列序不重复建;D-SE44)。 */
|
|
46
|
+
export const SESSION_INDEX_DDL = `
|
|
47
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
48
|
+
id INTEGER PRIMARY KEY,
|
|
49
|
+
cwd_key TEXT NOT NULL,
|
|
50
|
+
slot INTEGER NOT NULL,
|
|
51
|
+
file TEXT NOT NULL,
|
|
52
|
+
cwd TEXT,
|
|
53
|
+
title TEXT,
|
|
54
|
+
identity TEXT,
|
|
55
|
+
src TEXT NOT NULL,
|
|
56
|
+
updated_at INTEGER,
|
|
57
|
+
indexed_at INTEGER,
|
|
58
|
+
seg_n INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
seg_bytes INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
seg_mtime REAL NOT NULL DEFAULT 0,
|
|
61
|
+
tail_hash TEXT NOT NULL DEFAULT '',
|
|
62
|
+
UNIQUE(cwd_key, slot)
|
|
63
|
+
);
|
|
64
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
65
|
+
rowid INTEGER PRIMARY KEY,
|
|
66
|
+
sid INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
67
|
+
idx INTEGER NOT NULL,
|
|
68
|
+
seg INTEGER NOT NULL,
|
|
69
|
+
ts INTEGER,
|
|
70
|
+
role TEXT,
|
|
71
|
+
name TEXT,
|
|
72
|
+
tool_call_id TEXT,
|
|
73
|
+
content TEXT NOT NULL DEFAULT '',
|
|
74
|
+
UNIQUE(sid, idx)
|
|
75
|
+
);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS messages_ts ON messages(ts);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS messages_name ON messages(name);
|
|
78
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
79
|
+
id INTEGER PRIMARY KEY,
|
|
80
|
+
sid INTEGER NOT NULL,
|
|
81
|
+
msg_idx INTEGER NOT NULL,
|
|
82
|
+
ord INTEGER NOT NULL,
|
|
83
|
+
call_id TEXT,
|
|
84
|
+
name TEXT,
|
|
85
|
+
args TEXT,
|
|
86
|
+
UNIQUE(sid, msg_idx, ord)
|
|
87
|
+
);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS tool_calls_name ON tool_calls(name);
|
|
89
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(seg_content, seg_name, tokenize='unicode61');
|
|
90
|
+
CREATE TABLE IF NOT EXISTS index_meta (k TEXT PRIMARY KEY, v TEXT);
|
|
91
|
+
`
|
|
92
|
+
|
|
93
|
+
function openRaw(path) {
|
|
94
|
+
const db = new DatabaseSync(path)
|
|
95
|
+
try {
|
|
96
|
+
db.exec(`PRAGMA journal_mode = WAL`)
|
|
97
|
+
db.exec(`PRAGMA busy_timeout = ${SESSION_INDEX_BUSY_TIMEOUT}`)
|
|
98
|
+
db.exec(`PRAGMA foreign_keys = ON`)
|
|
99
|
+
db.exec(SESSION_INDEX_DDL)
|
|
100
|
+
const v = readMeta(db, "v")
|
|
101
|
+
if (v !== null && v !== SESSION_INDEX_SCHEMA_VERSION) throw new Error(`session index schema ${v} ≠ ${SESSION_INDEX_SCHEMA_VERSION}`)
|
|
102
|
+
if (v === null) {
|
|
103
|
+
writeMeta(db, "v", SESSION_INDEX_SCHEMA_VERSION)
|
|
104
|
+
writeMeta(db, "built_at", Date.now())
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
// 失败句柄必须关闭:Windows 上未关闭的句柄会让改名失败(现场保留 + 新建两步都落空)
|
|
108
|
+
try { db.close() } catch { /* ignore */ }
|
|
109
|
+
throw e
|
|
110
|
+
}
|
|
111
|
+
return db
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 打开即验 + 开箱自愈(D-SE46):缺失 ⇒ 建;打开 / DDL / 版本失败 ⇒ 现场改名
|
|
115
|
+
* `session-index.db.corrupt-<epochms>`(连同 `-wal` / `-shm`——否则残 WAL 会挂到新库上)+ 新建空库。
|
|
116
|
+
* 仍失败(磁盘满 / 目录不可写 / 无 node:sqlite)⇒ **null = 静默降级**(查询回落主存路径,主存零险)。 */
|
|
117
|
+
export function openSessionIndex({ path = sessionIndexDbPath() } = {}) {
|
|
118
|
+
try { mkdirSync(dirname(path), { recursive: true }) } catch { /* 已存在 / 并发建目录竞争 */ }
|
|
119
|
+
try { return openRaw(path) } catch { /* 缺失 / 坏库 / 版本不符 ⇒ 走自愈 */ }
|
|
120
|
+
const stamp = `${path}.corrupt-${Date.now()}`
|
|
121
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
122
|
+
try { if (existsSync(path + suffix)) renameSync(path + suffix, stamp + suffix) } catch { /* 现场保留尽力面 */ }
|
|
123
|
+
}
|
|
124
|
+
try { return openRaw(path) } catch { return null }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 元表读 / 写(`v` = schema 版本 · `built_at` = 首建 / 全量重建时刻 · `pass_at` = **最近一次
|
|
128
|
+
* 实际写入(变更)时刻**(无源变的 pass 零写、不推进)· 计数快照)。 */
|
|
129
|
+
export function readMeta(db, k) {
|
|
130
|
+
try {
|
|
131
|
+
const row = db.prepare("SELECT v FROM index_meta WHERE k = ?").get(k)
|
|
132
|
+
return row ? row.v : null
|
|
133
|
+
} catch { return null }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function writeMeta(db, k, v) {
|
|
137
|
+
db.prepare("INSERT INTO index_meta (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v").run(k, String(v))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── 行形态(写入面与读出面共用同一口径)──────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/** 消息文本(多模态数组取 text 部件拼接)。**与 `agent-tools/read-history.mjs` 的 JSON 面
|
|
143
|
+
* `messageText` 同规则**——两面各自持有(索引面不得静态引 read-history 的装配面,反之亦然),
|
|
144
|
+
* 等价性由过滤矩阵对拍用例覆盖。 */
|
|
145
|
+
export function messageText(m) {
|
|
146
|
+
if (typeof m?.content === "string") return m.content
|
|
147
|
+
if (Array.isArray(m?.content)) {
|
|
148
|
+
return m.content
|
|
149
|
+
.map((p) => (p && typeof p === "object" && p.type === "text" ? p.text ?? "" : ""))
|
|
150
|
+
.filter((t) => t.length > 0)
|
|
151
|
+
.join(" ")
|
|
152
|
+
}
|
|
153
|
+
return ""
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 一次工具声明 → `{ name, args, call_id }`(`{function:{…}}` 与扁平两形兼容;`args` 逐字取存储串)。 */
|
|
157
|
+
export function toolCallParts(tc) {
|
|
158
|
+
return {
|
|
159
|
+
call_id: tc?.id ?? null,
|
|
160
|
+
name: tc?.function?.name ?? tc?.name ?? "",
|
|
161
|
+
args: typeof tc?.function?.arguments === "string" ? tc.function.arguments
|
|
162
|
+
: typeof tc?.arguments === "string" ? tc.arguments : null,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 输出上限(= 存储面同值:≤300 原样,超限截 300 + `…`)。 */
|
|
167
|
+
export function capArguments(s) {
|
|
168
|
+
const t = String(s ?? "")
|
|
169
|
+
return t.length <= ARG_MAX_CHARS ? t : t.slice(0, ARG_MAX_CHARS) + "…"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── 删除面单点(FTS 同删义务——无触发器:四条删除路径全部经此函数,同事务内同步删;§6.19)──
|
|
173
|
+
|
|
174
|
+
/** 删行单点:`fromIdx = null` ⇒ 整会话(消息 + FTS + 工具声明 + 会话行);给定 `fromIdx` ⇒ 区间
|
|
175
|
+
* (`idx >= fromIdx` 的消息 / FTS 与 `msg_idx >= fromIdx` 的声明——段重写 / 重扫面)。 */
|
|
176
|
+
export function deleteSessionRows(db, sid, { fromIdx = null } = {}) {
|
|
177
|
+
const range = fromIdx === null ? "" : " AND idx >= ?"
|
|
178
|
+
const mArgs = fromIdx === null ? [sid] : [sid, fromIdx]
|
|
179
|
+
const tRange = fromIdx === null ? "" : " AND msg_idx >= ?"
|
|
180
|
+
db.prepare(`DELETE FROM messages_fts WHERE rowid IN (SELECT rowid FROM messages WHERE sid = ?${range})`).run(...mArgs)
|
|
181
|
+
db.prepare(`DELETE FROM tool_calls WHERE sid = ?${tRange}`).run(...mArgs)
|
|
182
|
+
db.prepare(`DELETE FROM messages WHERE sid = ?${range}`).run(...mArgs)
|
|
183
|
+
if (fromIdx === null) db.prepare(`DELETE FROM sessions WHERE id = ?`).run(sid)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 单会话清行(源消失面——单事务;幂等):删四表该会话行 + 推进 `pass_at`
|
|
187
|
+
* (查询侧据此走回落路径——既有 `session file not found` 文案逐字保留)。 */
|
|
188
|
+
export function dropSessionRows(db, sid, now = Date.now()) {
|
|
189
|
+
db.exec("BEGIN IMMEDIATE")
|
|
190
|
+
try {
|
|
191
|
+
deleteSessionRows(db, sid, { fromIdx: null })
|
|
192
|
+
writeMeta(db, "pass_at", now)
|
|
193
|
+
db.exec("COMMIT")
|
|
194
|
+
} catch (e) {
|
|
195
|
+
try { db.exec("ROLLBACK") } catch { /* ignore */ }
|
|
196
|
+
throw e
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** `--rebuild` 面:清四表(单事务)+ 刷新 `built_at`。 */
|
|
201
|
+
export function clearIndex(db) {
|
|
202
|
+
db.exec("BEGIN IMMEDIATE")
|
|
203
|
+
try {
|
|
204
|
+
db.exec("DELETE FROM messages_fts")
|
|
205
|
+
db.exec("DELETE FROM tool_calls")
|
|
206
|
+
db.exec("DELETE FROM messages")
|
|
207
|
+
db.exec("DELETE FROM sessions")
|
|
208
|
+
writeMeta(db, "built_at", Date.now())
|
|
209
|
+
db.exec("COMMIT")
|
|
210
|
+
} catch (e) {
|
|
211
|
+
try { db.exec("ROLLBACK") } catch { /* 回滚尽力面 */ }
|
|
212
|
+
throw e
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── 会话行 / 消息行 / 声明行写入 ─────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
export function findSessionByFile(db, file) {
|
|
219
|
+
try { return db.prepare("SELECT * FROM sessions WHERE file = ?").get(file) ?? null } catch { return null }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function findSessionByCwdSlot(db, cwdKey, slot) {
|
|
223
|
+
try { return db.prepare("SELECT * FROM sessions WHERE cwd_key = ? AND slot = ?").get(cwdKey, slot) ?? null } catch { return null }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 建会话行(返回 sid);同 `(cwd_key, slot)` 已占 ⇒ 先摘旧行(槽号回收 / 轮转 / 本行重建面——
|
|
227
|
+
* 新旧行不混,`UNIQUE(cwd_key, slot)` 零冲突)。 */
|
|
228
|
+
export function insertSession(db, row) {
|
|
229
|
+
const old = findSessionByCwdSlot(db, row.cwd_key, row.slot)
|
|
230
|
+
if (old) deleteSessionRows(db, old.id, { fromIdx: null })
|
|
231
|
+
const r = db.prepare(
|
|
232
|
+
`INSERT INTO sessions (cwd_key, slot, file, cwd, title, identity, src, updated_at, indexed_at, seg_n, seg_bytes, seg_mtime, tail_hash)
|
|
233
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
234
|
+
).run(row.cwd_key, row.slot, row.file, row.cwd ?? null, row.title ?? null, row.identity ?? null, row.src,
|
|
235
|
+
row.updated_at ?? null, row.indexed_at ?? null, row.seg_n ?? 0, row.seg_bytes ?? 0, row.seg_mtime ?? 0, row.tail_hash ?? "")
|
|
236
|
+
return Number(r.lastInsertRowid)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** 会话级元数据 / 水位推进(单语句——写面在同一事务内调用)。 */
|
|
240
|
+
export function updateSession(db, sid, patch) {
|
|
241
|
+
const cols = ["cwd", "title", "identity", "src", "updated_at", "indexed_at", "seg_n", "seg_bytes", "seg_mtime", "tail_hash"]
|
|
242
|
+
const set = cols.filter((c) => patch[c] !== undefined)
|
|
243
|
+
if (set.length === 0) return
|
|
244
|
+
db.prepare(`UPDATE sessions SET ${set.map((c) => `${c} = ?`).join(", ")} WHERE id = ?`).run(...set.map((c) => patch[c]), sid)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** 插消息行(连同 FTS 行——同事务)。`rows` = `{ idx, seg, ts, role, name, tool_call_id, content }`。 */
|
|
248
|
+
export function insertMessages(db, sid, rows) {
|
|
249
|
+
const msg = db.prepare(`INSERT INTO messages (sid, idx, seg, ts, role, name, tool_call_id, content) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
250
|
+
const fts = db.prepare(`INSERT INTO messages_fts (rowid, seg_content, seg_name) VALUES (?, ?, ?)`)
|
|
251
|
+
for (const r of rows) {
|
|
252
|
+
const info = msg.run(sid, r.idx, r.seg, r.ts ?? null, r.role ?? null, r.name ?? null, r.tool_call_id ?? null, r.content ?? "")
|
|
253
|
+
fts.run(Number(info.lastInsertRowid), segmentCJK(r.content ?? ""), segmentCJK(r.name ?? ""))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 插声明行(`ord` = 该消息内声明序;`args` = 存储串逐字)。 */
|
|
258
|
+
export function insertToolCalls(db, sid, rows) {
|
|
259
|
+
const stmt = db.prepare(`INSERT INTO tool_calls (sid, msg_idx, ord, call_id, name, args) VALUES (?, ?, ?, ?, ?, ?)`)
|
|
260
|
+
for (const r of rows) stmt.run(sid, r.msg_idx, r.ord, r.call_id ?? null, r.name ?? "", r.args ?? null)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** `--status` 读数(行数 / 取源分布 / 库字节 / 水位覆盖 / 上次变更时刻)。 */
|
|
264
|
+
export function indexSummary(db, { path = sessionIndexDbPath() } = {}) {
|
|
265
|
+
const one = (sql) => { try { return Number(Object.values(db.prepare(sql).get())[0] ?? 0) } catch { return 0 } }
|
|
266
|
+
let bytes = 0
|
|
267
|
+
try { bytes = statSync(path).size } catch { /* 库不在 / 不可读 ⇒ 0 */ }
|
|
268
|
+
return {
|
|
269
|
+
sessions: one("SELECT COUNT(*) FROM sessions"),
|
|
270
|
+
sidecar: one("SELECT COUNT(*) FROM sessions WHERE src = 'sidecar'"),
|
|
271
|
+
json: one("SELECT COUNT(*) FROM sessions WHERE src = 'json'"),
|
|
272
|
+
messages: one("SELECT COUNT(*) FROM messages"),
|
|
273
|
+
toolCalls: one("SELECT COUNT(*) FROM tool_calls"),
|
|
274
|
+
bytes,
|
|
275
|
+
builtAt: Number(readMeta(db, "built_at") ?? 0),
|
|
276
|
+
passAt: Number(readMeta(db, "pass_at") ?? 0),
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 库侧清理(测试收尾 / 现场重置):删库文件三件套(含 WAL / SHM)。 */
|
|
281
|
+
export function removeIndexFiles(path = sessionIndexDbPath()) {
|
|
282
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
283
|
+
try { rmSync(path + suffix, { force: true }) } catch { /* 尽力面 */ }
|
|
284
|
+
}
|
|
285
|
+
}
|
package/session-lifecycle.mjs
CHANGED
|
@@ -29,11 +29,12 @@ import { loadSlotFile } from "./session.mjs"
|
|
|
29
29
|
import { mergeAdjacentAssistantEchoes } from "./context.mjs"
|
|
30
30
|
import { scheduleSessionGC } from "./session-gc.mjs"
|
|
31
31
|
import { restoreEngTokens } from "./token-ttl.mjs"
|
|
32
|
+
import { clearPlanMode } from "./agent-tools/plan.mjs"
|
|
32
33
|
import { bindRecordStore, unbindRecordStore, RECORD_WINDOW_MESSAGES } from "./session-store.mjs"
|
|
33
34
|
|
|
34
35
|
/** 恢复决策包装(SESSION.md §6.12 启动钩子,2026-09-06):本端恢复入口触发一次残留 GC——
|
|
35
|
-
* scheduleSessionGC
|
|
36
|
-
*
|
|
36
|
+
* scheduleSessionGC 内部**启动窗外延迟拍**(`GC_PASS_DELAY_MS` = 3s)+ 每进程每前缀去重,
|
|
37
|
+
* **异步非阻塞**启动路径(N4;§6.17 D-SE39);**async**(F-MI7——调用面必须 await)。 */
|
|
37
38
|
export async function resumeSlot(cwd) {
|
|
38
39
|
scheduleSessionGC(cwd)
|
|
39
40
|
return slotsResumeSlot(cwd)
|
|
@@ -112,6 +113,10 @@ export function applySession(agent, data, opts = {}) {
|
|
|
112
113
|
agent.config.agent ??= {}
|
|
113
114
|
agent.config.agent.engineering = data.engineering === true
|
|
114
115
|
}
|
|
116
|
+
// ENG-PLAN-EXCLUSION(FR31 ③ / KD10——恢复点①·CLI `applySession`):工程模式 ⇒ planMode 恒
|
|
117
|
+
// false(内存位 + 未注入的 plan 提示语)——防「工程纪律 × plan 只读」半状态随槽恢复复活。
|
|
118
|
+
// **槽不就地回写**:内存值在下一次 `saveSession` 随 `planMode` 字段自然收正(`session.mjs:128`)。
|
|
119
|
+
if (agent.config?.agent?.engineering === true) clearPlanMode(agent)
|
|
115
120
|
if (data.advisor) {
|
|
116
121
|
agent.config.advisor = { ...data.advisor }
|
|
117
122
|
}
|
|
@@ -166,8 +171,11 @@ export function applySession(agent, data, opts = {}) {
|
|
|
166
171
|
* write an empty session, and mark it as the active slot.
|
|
167
172
|
* No limit on the number of sessions.
|
|
168
173
|
* **async**(F-MI7:入口一次异步束——探测不阻塞事件循环;调用面必须 await)。
|
|
174
|
+
* `opts.releaseStale`(F-CR1 · SESSION.md §6.5 / §6.16):**调用面 opt-in**——CLI `/new` 传
|
|
175
|
+
* (`cmd-new.mjs`),释放本进程残留认领(保留集 = {新槽});ACP 四点不传(多会话多认领属其
|
|
176
|
+
* 设计——F-CR3 零回归)。
|
|
169
177
|
*/
|
|
170
|
-
export async function newSession(cwd) {
|
|
178
|
+
export async function newSession(cwd, opts = {}) {
|
|
171
179
|
const m = loadManifest(cwd)
|
|
172
180
|
|
|
173
181
|
// 2026-09-01 advisor 🔵:先清理死主条目(与 ensureActive 分支2 语义一致)——否则
|
|
@@ -223,7 +231,9 @@ export async function newSession(cwd) {
|
|
|
223
231
|
slots: deadSlots.filter((s) => !m.slots[s]),
|
|
224
232
|
}
|
|
225
233
|
: null
|
|
226
|
-
|
|
234
|
+
// F-CR1 认领释放(SESSION.md §6.5 / §6.16 公式代入):保留集 = {新槽}——与既有死主
|
|
235
|
+
// deletions 合流(同一一次 saveManifest;落盘判据见核 `saveManifest`)。
|
|
236
|
+
saveManifest(cwd, m, deletions, opts.releaseStale ? { setActive: true, release: [slot] } : { setActive: true })
|
|
227
237
|
// 2026-09-05 §6.10 D-4:/new 落点写本端记录(显式切换跟随——T-M8)
|
|
228
238
|
writeEndMarker(cwd, slot)
|
|
229
239
|
return slot
|
|
@@ -280,7 +290,10 @@ export function switchToSlot(cwd, slot) {
|
|
|
280
290
|
m.slotSessions ??= {}
|
|
281
291
|
m.slotSessions[slot] = getSessionId()
|
|
282
292
|
}
|
|
283
|
-
|
|
293
|
+
// F-CR1 认领释放(SESSION.md §6.5 / §6.16 公式代入):保留集 = 未占 ⇒ {目标};**被占 ⇒ 空**
|
|
294
|
+
// (不认领目标 + 旧认领一并释放——下次保存经 `allocateFresh` fork,既有语义)。核受占 =
|
|
295
|
+
// 切换成立(D-SE33:指针 / 记录按 D-6 / D-4 落点——fork 面依赖指针翻至目标槽)。
|
|
296
|
+
saveManifest(cwd, m, null, { setActive: true, release: occ.occupied ? [] : [slot] })
|
|
284
297
|
// 2026-09-05 §6.10 D-4:/session N 跟随"最后查看的槽"(成功切换才写)
|
|
285
298
|
writeEndMarker(cwd, slot)
|
|
286
299
|
return data
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* `CORE-UNIFICATION.md` §2.8.1 表第 3 行随批执行外提;各函数语义**原样**迁移):
|
|
5
5
|
* 摘要面(`extractSlotMeta` / `slotDigest`)· 清单读写(`loadManifest` / `saveManifest`)·
|
|
6
6
|
* 属主面(`ownerPid` / `ownerPids` / `ownerStateOf` / `ownerArgs` / `cleanDeadOwners`)·
|
|
7
|
-
* 认领面(`ensureActive` / `allocateFresh` / `claimSlot` / `activeSlot
|
|
7
|
+
* 认领面(`ensureActive` / `allocateFresh` / `claimSlot` / `activeSlot` / **认领释放集
|
|
8
|
+
* `staleClaims`**——F-CR1 · SESSION-CLAIM 批:判据单源,`saveManifest` 落盘 + 各落点共用)。
|
|
8
9
|
*
|
|
9
10
|
* 静态环(与既有的 session.mjs ↔ session-slots.mjs 同形):本档引 `session-slots.mjs` 的
|
|
10
11
|
* 存储原语(`getSessionId` / `manifestPath` / `slotPath` / `writeSessionFile`),
|
|
@@ -69,6 +70,10 @@ export function saveManifest(cwd, m, deletions = null, opts = {}) {
|
|
|
69
70
|
// (ensureActive 分支、newSession、switchToSlot、deleteSlot 删到 active 时)传
|
|
70
71
|
// opts.setActive;其余调用方(saveSession/ACP 认领/死项清理)默认保留磁盘 fresh 的
|
|
71
72
|
// active,否则毫秒窗口内会把并发方刚翻的 active 回滚(F1 防漂移的反向变体)。
|
|
73
|
+
// F-CR1 认领释放(判据单源 · SESSION.md §6.2 / §6.16 落盘判据三条):`opts.release` = 保留集
|
|
74
|
+
// (落点槽 ∪ 本进程其余活绑定槽;被占落点 ⇒ 空集)。③ 内存认领表先移除残留条目(否则下方
|
|
75
|
+
// 条目级合并会把它从内存复活回写)——先于 try(fresh 不可读时亦须生效)。
|
|
76
|
+
if (opts.release) dropStaleClaims(m, opts.release)
|
|
72
77
|
try {
|
|
73
78
|
const fresh = JSON.parse(readFileSync(manifestPath(cwd), "utf8"))
|
|
74
79
|
if (fresh && typeof fresh === "object" && fresh.slots && typeof fresh.slots === "object") {
|
|
@@ -77,6 +82,14 @@ export function saveManifest(cwd, m, deletions = null, opts = {}) {
|
|
|
77
82
|
merged.slots = { ...fresh.slots, ...(m.slots ?? {}) }
|
|
78
83
|
merged.slotSessions = { ...(fresh.slotSessions ?? {}), ...(m.slotSessions ?? {}) }
|
|
79
84
|
if (m.sessionId) merged.sessionId = m.sessionId
|
|
85
|
+
// ① 释放集按**本次 fresh 快照**取值(不得以陈旧内存 manifest 构 deletions);② **值条件删除**
|
|
86
|
+
// (仅当 fresh 属主仍为本进程——窗口内他人的新认领不在集内、不被误删)⇒ 并入本次 deletions。
|
|
87
|
+
if (opts.release) {
|
|
88
|
+
const released = staleClaims(fresh, opts.release)
|
|
89
|
+
if (released.length > 0) {
|
|
90
|
+
deletions = { ...(deletions ?? {}), slotSessions: [...(deletions?.slotSessions ?? []), ...released] }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
80
93
|
if (deletions) {
|
|
81
94
|
for (const [section, keys] of Object.entries(deletions)) {
|
|
82
95
|
for (const k of keys) delete merged[section]?.[k]
|
|
@@ -95,6 +108,23 @@ export function saveManifest(cwd, m, deletions = null, opts = {}) {
|
|
|
95
108
|
writeSessionFile(manifestPath(cwd), m)
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
/** 本进程**残留认领集**(F-CR1 释放谓词 —— **判据单源** · SESSION.md §6.2 / §6.16):
|
|
112
|
+
* 「释放 A ⟺ `slotSessions[A]` 为本进程 ∧ `A ∉ 保留集`」。`keep` = 保留集 = 落点槽 ∪ 本进程
|
|
113
|
+
* 其余活绑定槽(单绑定落点 ⇒ `{落点槽}`;被占落点 ⇒ 空集)。返回槽号(**字符串键**形态)。 */
|
|
114
|
+
export function staleClaims(src, keep = []) {
|
|
115
|
+
const keepSet = new Set((keep ?? []).filter((s) => s != null).map(Number))
|
|
116
|
+
const mySessionId = getSessionId()
|
|
117
|
+
return Object.entries(src?.slotSessions ?? {})
|
|
118
|
+
.filter(([slot, owner]) => owner === mySessionId && !keepSet.has(Number(slot)))
|
|
119
|
+
.map(([slot]) => slot)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 从**内存**认领表移除本进程残留条目(判据③——与写盘同一时机;见 `saveManifest`)。 */
|
|
123
|
+
function dropStaleClaims(m, keep) {
|
|
124
|
+
if (!m?.slotSessions) return
|
|
125
|
+
for (const slot of staleClaims(m, keep)) delete m.slotSessions[slot]
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
/** 属主串 → pid(`"pid-ts-rand"`——`owner.split("-")[0]` 格式判据单源:session-slots ↔
|
|
99
129
|
* session-lifecycle 共用,防两处漂移)。不可解析 ⇒ NaN——判据层按 dead 处理
|
|
100
130
|
* (见 process-probe `ownerState`)。 */
|
|
@@ -248,12 +278,34 @@ export function allocateFresh(cwd, m, deadParam = null, bundle = null) {
|
|
|
248
278
|
* 已按"属主 空/死/本进程"判据校验可用性;幂等)。manifest active 保留为共享指针(D-6:
|
|
249
279
|
* 旧版端/ACP 恢复依据 + 无记录端一次性继承源 + 列表回退高亮——不再作本端恢复第一依据)。
|
|
250
280
|
* deadParam 在写入所有权之后求值(防 deletions 删掉本调用刚认领的槽——ensureActive
|
|
251
|
-
* deadParam 同型过滤)。
|
|
281
|
+
* deadParam 同型过滤)。
|
|
282
|
+
* F-CR1(2026-09-21 · SESSION-CLAIM 批):认领落点释放本进程残留认领(保留集 = {认领槽}——
|
|
283
|
+
* §6.2 公式代入);本函数 = 启动恢复 / 本端恢复落点(核 `resumeSlot` 与 VSC 端壳
|
|
284
|
+
* `resumeSlot` 共用),落盘判据(fresh 快照 / 值条件删除 / 内存移除)见 `saveManifest`。 */
|
|
252
285
|
export function claimSlot(cwd, slot, m = loadManifest(cwd), deadParam = null) {
|
|
253
286
|
m.slotSessions ??= {}
|
|
254
287
|
m.slotSessions[slot] = getSessionId()
|
|
255
288
|
m.active = slot // 认领即翻共享指针(setActive 写 m.active——不更新则落快照旧值)
|
|
256
|
-
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
289
|
+
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true, release: [slot] })
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 退出全释放(EXIT-CLAIM-RELEASE 批 · F-XR1 · SESSION.md §6.18 D-SE41):优雅退出前释放
|
|
293
|
+
* 本进程**全部**槽认领(保留集空——`staleClaims` 释放谓词单源);落盘判据 = `saveManifest`
|
|
294
|
+
* `opts.release` 三条自动继承(fresh 同次快照取释放集 / 值条件删除——他人窗口内新认领不误删 /
|
|
295
|
+
* 内存认领表同步移除)。**零触碰 marker 面**(F-XR2 路标保留——下次启动经 `usableSlot`
|
|
296
|
+
* 无属主短路零探测直达恢复);崩溃路径不经此(信号无 JS 钩子——F-XR3)。
|
|
297
|
+
* 早退面:磁盘无 manifest ⇒ `existsSync` 单条目早退**零写**(不造盘面);本进程无认领 ⇒
|
|
298
|
+
* 零写返回 false。失败容忍(F-XR1「退出恒达」):整体 try/catch **永不抛出**,返回 boolean
|
|
299
|
+
* (true = 有释放且落盘成功;false = 早退 / 失败——断言面);写失败盘面 = 认领残留 →
|
|
300
|
+
* 恢复走既有探测面(现状形态,数据零险)。 */
|
|
301
|
+
export function releaseClaimsAll(cwd) {
|
|
302
|
+
try {
|
|
303
|
+
if (!existsSync(manifestPath(cwd))) return false // 无 manifest ⇒ 零写(不造盘面)
|
|
304
|
+
const m = loadManifest(cwd)
|
|
305
|
+
if (staleClaims(m, []).length === 0) return false // 无本进程认领 ⇒ 零写(免白刷共享 sessionId)
|
|
306
|
+
saveManifest(cwd, m, null, { release: [] }) // 保留集空 = 全释放;不 setActive(active 共享指针不动)
|
|
307
|
+
return true
|
|
308
|
+
} catch { return false } // F-XR1:退出恒达(写失败 = 认领残留 → 恢复走探测面)
|
|
257
309
|
}
|
|
258
310
|
|
|
259
311
|
/** Return the active slot number for this process, claiming one if necessary */
|