@thincoder/core 0.10.4 → 0.10.6

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.
Files changed (153) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +1 -1
  3. package/advisor/compaction.mjs +11 -6
  4. package/advisor/run.mjs +4 -4
  5. package/agent/assemble.mjs +1 -1
  6. package/agent/chat-call.mjs +1 -1
  7. package/agent/child-marks.mjs +2 -2
  8. package/agent/digest-account.mjs +5 -4
  9. package/agent/dispatch-gates.mjs +8 -0
  10. package/agent/dispatch.mjs +39 -18
  11. package/agent/family-tools.mjs +22 -11
  12. package/agent/helpers.mjs +4 -3
  13. package/agent/light-round.mjs +66 -0
  14. package/agent/post-turn.mjs +8 -4
  15. package/agent/run-stages.mjs +5 -3
  16. package/agent/run-start.mjs +3 -2
  17. package/agent/setup-reminders.mjs +1 -1
  18. package/agent/setup.mjs +18 -3
  19. package/agent/subagent-event.mjs +1 -1
  20. package/agent/suspension.mjs +8 -5
  21. package/agent/timers.mjs +56 -5
  22. package/agent/turn-loop.mjs +5 -3
  23. package/agent/write-gate.mjs +7 -7
  24. package/agent-tools/advisor-async.mjs +2 -2
  25. package/agent-tools/advisor-settle.mjs +2 -2
  26. package/agent-tools/advisor.mjs +1 -1
  27. package/agent-tools/async-discard.mjs +34 -0
  28. package/agent-tools/async-settle.mjs +3 -2
  29. package/agent-tools/audit-block.mjs +1 -1
  30. package/agent-tools/bash-async.mjs +1 -1
  31. package/agent-tools/batch-lifecycle.mjs +35 -14
  32. package/agent-tools/batch-paths.mjs +4 -4
  33. package/agent-tools/batch-skeleton.mjs +29 -11
  34. package/agent-tools/batch.mjs +28 -15
  35. package/agent-tools/browser-async.mjs +198 -0
  36. package/agent-tools/checkpoint.mjs +1 -1
  37. package/agent-tools/design-slots.mjs +232 -0
  38. package/agent-tools/digest-budget.mjs +1 -1
  39. package/agent-tools/manifest.mjs +193 -0
  40. package/agent-tools/parent-channel.mjs +7 -4
  41. package/agent-tools/review-facts.mjs +13 -13
  42. package/agent-tools/settings.mjs +1 -1
  43. package/agent-tools/skill.mjs +1 -1
  44. package/agent-tools/subagent-async.mjs +21 -12
  45. package/agent-tools/subagent-scheduler.mjs +8 -5
  46. package/agent-tools/subagent-spawn.mjs +29 -55
  47. package/agent-tools/subagent.mjs +21 -8
  48. package/agent-tools/timer.mjs +186 -12
  49. package/agent-tools/zero-write-watch.mjs +2 -2
  50. package/agent-tools.mjs +2 -1
  51. package/agent.mjs +1 -1
  52. package/attachments.mjs +1 -1
  53. package/browser/actions.mjs +163 -0
  54. package/browser/cdp.mjs +161 -0
  55. package/browser/clipboard.mjs +142 -0
  56. package/browser/input-actions.mjs +197 -0
  57. package/browser/input.mjs +256 -0
  58. package/browser/launch.mjs +143 -0
  59. package/browser/queue.mjs +171 -0
  60. package/browser/session.mjs +494 -0
  61. package/browser/snapshot.mjs +270 -0
  62. package/config-io.mjs +18 -13
  63. package/config-migrate.mjs +13 -29
  64. package/config-presets.mjs +33 -29
  65. package/config.mjs +68 -25
  66. package/context-degrade.mjs +1 -1
  67. package/context-echo.mjs +1 -1
  68. package/context-push.mjs +2 -2
  69. package/context.mjs +1 -1
  70. package/conventions.mjs +2 -2
  71. package/declaration.mjs +18 -2
  72. package/file-links.mjs +1 -1
  73. package/file-refs.mjs +1 -1
  74. package/history-window.mjs +1 -1
  75. package/i18n.mjs +2 -0
  76. package/ledger-cmd.mjs +62 -16
  77. package/ledger-db.mjs +6 -0
  78. package/ledger-surface.mjs +49 -5
  79. package/ledger-tools.mjs +16 -8
  80. package/ledger.mjs +9 -16
  81. package/log.mjs +1 -1
  82. package/manifest-discovery.mjs +1 -1
  83. package/manifest-schema.mjs +1 -1
  84. package/mcp/transport-stdio.mjs +1 -1
  85. package/mcp.mjs +1 -1
  86. package/memory/code-index.mjs +1 -1
  87. package/memory/code-search.mjs +1 -1
  88. package/memory/code-sync.mjs +1 -1
  89. package/memory/docs.mjs +6 -2
  90. package/memory/file-list.mjs +1 -1
  91. package/memory/file-walk.mjs +17 -5
  92. package/memory/maintain.mjs +410 -0
  93. package/memory/memory-tool.mjs +21 -6
  94. package/memory/schema.mjs +19 -7
  95. package/memory/sweep.mjs +8 -5
  96. package/memory/sync-tail.mjs +16 -10
  97. package/model-ref.mjs +16 -13
  98. package/model-specs.mjs +21 -9
  99. package/package.json +2 -1
  100. package/peer-instances.mjs +1 -1
  101. package/permission.mjs +33 -0
  102. package/process-probe.mjs +2 -2
  103. package/prompts/advisor-design.md +4 -2
  104. package/prompts/advisor-round1.md +1 -1
  105. package/prompts/advisor-round2.md +1 -1
  106. package/prompts/advisor-round3.md +1 -1
  107. package/prompts/common.md +36 -2
  108. package/prompts/consult-base.md +1 -1
  109. package/prompts/discipline-engineering.md +32 -15
  110. package/prompts/discipline-normal.md +46 -2
  111. package/prompts/persona-eng-designer.md +6 -6
  112. package/prompts/persona-engineering.md +14 -6
  113. package/prompts/persona-normal.md +2 -0
  114. package/prompts/subagent-base.md +3 -0
  115. package/provider/core.mjs +6 -0
  116. package/provider/errors.mjs +5 -3
  117. package/provider/google.mjs +9 -8
  118. package/provider/list-models.mjs +3 -2
  119. package/provider/sse.mjs +3 -0
  120. package/provider-flows.mjs +14 -15
  121. package/proxy-chunked.mjs +102 -0
  122. package/proxy-target.mjs +54 -0
  123. package/proxy-transport.mjs +298 -0
  124. package/proxy.mjs +17 -248
  125. package/session-gc.mjs +2 -2
  126. package/session-lifecycle.mjs +18 -14
  127. package/session-slot-claims.mjs +2 -2
  128. package/session-slots-manifest.mjs +1 -1
  129. package/session-slots.mjs +3 -3
  130. package/team-ledger.mjs +282 -0
  131. package/team.mjs +223 -0
  132. package/tool-docs/bash.md +1 -0
  133. package/tool-docs/batch.md +1 -1
  134. package/tool-docs/browser.md +48 -0
  135. package/tool-docs/ledger.md +3 -3
  136. package/tool-docs/manifest.md +16 -0
  137. package/tool-docs/memory.md +1 -0
  138. package/tool-docs/notify_parent.md +5 -0
  139. package/tool-docs/process.md +4 -4
  140. package/tool-docs/question.md +1 -0
  141. package/tool-docs/subagent.md +4 -3
  142. package/tool-docs/team_ledger.md +9 -0
  143. package/tool-docs/timer.md +6 -0
  144. package/tool-docs/wait_for.md +1 -0
  145. package/tools/browser.mjs +161 -0
  146. package/tools/git.mjs +1 -1
  147. package/tools/index.mjs +3 -0
  148. package/tools/ops.mjs +26 -10
  149. package/tools/repomap.mjs +1 -1
  150. package/tools/write-path.mjs +2 -2
  151. package/traces/trace-cleanup.mjs +1 -1
  152. package/traces/trace-store.mjs +1 -1
  153. package/vision-reader.mjs +10 -16
package/config.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * config.mjs — configuration loading and saving
3
- * Model-merge schema v2 (2026-09-10 MODEL-SELECTION): providers[] carry ONE default model
4
- * per channel (`providers[].model` — the new-install seed / empty-slot fallback); the
5
- * candidates list field models[] is gone — the available-model list is fetched from the
6
- * provider at runtime (`GET /models`, PROVIDER.md §16). config.defaultModel (top level,
3
+ * Model-merge schema v2 (2026-09-10 MODEL-SELECTION; 2026-10-09 清除批:渠道单值模型退场):
4
+ * providers[] carry NO channel default model (`providers[].model` retired — 渠道不是模型选择
5
+ * 单位); the candidates list field models[] is gone — the available-model list is fetched from
6
+ * the provider at runtime (`GET /models`, PROVIDER.md §16). config.defaultModel (top level,
7
7
  * "provider:model" composite) is the new-session starting point; activeProvider/activeModel
8
8
  * are gone from config (session slots keep their own double fields). Legacy fields migrate
9
9
  * on load (write-back failure never blocks startup).
@@ -35,7 +35,7 @@ export const DEFAULTS = {
35
35
  agent: {
36
36
  maxTurns: 200,
37
37
  subagentTurns: 100,
38
- subagentModel: null, // default subagent model: "provider:model" | provider name | model name (parent provider); null = inherit parent provider
38
+ subagentModel: null, // default subagent model: "provider:model" | model name (parent provider); null = inherit parent provider
39
39
  subagentModels: {}, // per-type override: { explore, plan, coder, "eng-coder" } — priority: subagent tool model arg > this[role] > subagentModel > parent provider
40
40
  goalTurns: 200,
41
41
  compactThreshold: 100000,
@@ -69,6 +69,8 @@ export const DEFAULTS = {
69
69
  dbPath: join(configDir, "memory.db"),
70
70
  projectDir: ".thincoder/memory",
71
71
  team: null,
72
+ // 索引自维护(§6.14 面⑤ · 台账 #1096):维护型备份保留枚数(轮转保最新 N——引擎钳 ≥ 1)
73
+ maintain: { backupKeep: 2 },
72
74
  },
73
75
  shell: null, // bash tool shell executable (e.g. "C:\\Program Files\\Git\\bin\\bash.exe" or "pwsh"); null = system default (cmd on Windows, /bin/sh elsewhere)
74
76
  embedding: {
@@ -82,11 +84,14 @@ export const DEFAULTS = {
82
84
  // Structured search via Tavily when a key is set — empty apiKey → Bing RSS/HTML fallback (zero-config).
83
85
  apiKey: "", // Tavily key (tvly-...) — optional
84
86
  },
87
+ // 浏览器工具(BROWSER-TOOL.md §3.2):非空 ⇒ navigate 目标 host ∥ click/evaluate/type 的当前页 host 须命中
88
+ //(全等(大小写不敏感)∥ `*.` 前缀 = 子域通配)——收束面非沙箱;缺省空 = 不设限。
89
+ browser: { allowDomains: [] },
85
90
  traces: {
86
91
  enabled: false, // §18.6 D-TR6 修订(2026-09-05 用户裁定——发布隐私:"不希望用户那边也采集"):轨迹存档默认 OFF——新用户零采集;本地调试分析可显式开(~/.thincoder/config.json traces.enabled:true)
87
92
  retentionHours: 24, // D-TR10:轨迹文件保留小时数——CLI 启动时删除超过该时长的文件(默认 24h)
88
93
  },
89
- // 诊断开关(用户面 ① 配置键——CONFIG.md §6.2 名录):`heapWatch` 默认开(设 false 关闭)——自愈臂
94
+ // 诊断开关(用户面 ① 配置键——CONFIG.md §6.2 名录):`heapWatch` 默认开(设 false 关闭)——自愈分支
90
95
  // 遥测;`heapSnapshot` 默认关——采集 opt-in(堆转储敏感度 ≥ traces ⇒ 对齐发布隐私先例 D-TR6;
91
96
  // 2026-09-30 采集收网批;桌面端运行期热读 ∥ CLI 本进程下次启动)
92
97
  diagnostics: { heapWatch: true, heapSnapshot: false },
@@ -99,11 +104,34 @@ import { specForModel, providerSpec, specMatch, assistantToolCallMessage } from
99
104
  export { specForModel, providerSpec, specMatch, assistantToolCallMessage }
100
105
 
101
106
 
102
- // Window utilization threshold: compacts at 60% context, reserving 40% headroom
103
- // for injected context (directory tree, git context, outline, project instructions,
104
- // memory/doc search results) which can consume 30-50K tokens each turn.
107
+ // Window utilization threshold: compacts at 60% of the USABLE window (window − output
108
+ // reservation — providers require input + max_tokens ≤ window; 2026-10-06 user ruling:
109
+ // the reservation folds into the budget so compaction fires first, never a provider
110
+ // rejection), the remaining 40% stays as headroom for injected context (directory tree,
111
+ // git context, outline, project instructions, memory/doc search results — 30-50K tokens
112
+ // per turn).
105
113
  const COMPACT_RATIO = 0.6
106
114
 
115
+ /** Output reservation the request counts against the window: explicit `provider.maxTokens`
116
+ * (the value actually sent — `provider/core.mjs`) when set, else the spec `maxOutput`, else 0. */
117
+ export function outputReserve(provider, spec) {
118
+ const explicit = provider?.maxTokens
119
+ if (Number.isFinite(explicit) && explicit > 0) return explicit
120
+ return spec?.maxOutput ?? 0
121
+ }
122
+
123
+ /** Usable input window = window − output reservation. Degenerate guard: a reservation
124
+ * ≥ the window is non-physical (no input could ever fit — e.g. the doubao spec
125
+ * `maxOutput` 524_288 > `context` 256_000 ∥ the test harness's 1K window vs the default
126
+ * spec); collapsing the budget to 0 would be instant `context_limit` death — the very
127
+ * failure the reservation fold exists to prevent. Fall back to half the window.
128
+ * (2026-10-06——收口核验捕获:advisor-loop-split B5a 红。) */
129
+ export function usableWindow(context, reserve) {
130
+ const ctx = context ?? 0
131
+ const usable = ctx - reserve
132
+ return usable > 0 ? usable : Math.floor(ctx / 2)
133
+ }
134
+
107
135
  /** Derive compaction threshold; explicit is the value explicitly set in config file (takes priority), otherwise auto-computed from model.
108
136
  * Second param accepts EITHER a model name string (pure spec lookup — legacy caller:
109
137
  * first-run wizard) OR a provider object (providerSpec — the providers[].context
@@ -112,7 +140,8 @@ export function resolveCompactThreshold(explicit, modelOrProvider) {
112
140
  if (explicit != null) return { value: explicit, auto: false }
113
141
  const provider = typeof modelOrProvider === "string" ? { model: modelOrProvider } : (modelOrProvider ?? {})
114
142
  const spec = providerSpec(provider)
115
- const value = Math.floor(spec.context * COMPACT_RATIO)
143
+ const usable = usableWindow(spec.context, outputReserve(provider, spec))
144
+ const value = Math.floor(usable * COMPACT_RATIO)
116
145
  return { value, auto: true }
117
146
  }
118
147
 
@@ -191,7 +220,7 @@ function warnConsultModelsFiltered(dropped, path) {
191
220
  /** #861(AGENT-LOOP-SUBAGENT.md §6.7.1):subagent 模型覆盖加载期清洗——单源纯函数(核 loadConfig ∥ VSC 端壳 raw 读点同消费);非法形态不 throw(`subagentModel` 非法 ⇒ null、`subagentModels` 非对象 ⇒ {}、值非法 ⇒ 剔除该键)。 */
192
221
  export function sanitizeSubagentModel(v) {
193
222
  const ok = typeof v === "string" && v.trim()
194
- const dropped = ok || v == null ? [] : [`agent.subagentModel must be a non-empty string ("provider:model" | provider | model) — got ${Array.isArray(v) ? "array" : typeof v} — ignored (inherits the parent provider)`]
223
+ const dropped = ok || v == null ? [] : [`agent.subagentModel must be a non-empty string ("provider:model" | model) — got ${Array.isArray(v) ? "array" : typeof v} — ignored (inherits the parent provider)`]
195
224
  return { value: ok ? v : null, dropped }
196
225
  }
197
226
  export function sanitizeSubagentModels(v) {
@@ -200,7 +229,7 @@ export function sanitizeSubagentModels(v) {
200
229
  const value = {}, dropped = []
201
230
  for (const [role, val] of Object.entries(v)) {
202
231
  if (typeof val === "string" && val.trim()) { value[role] = val; continue }
203
- dropped.push(`agent.subagentModels.${role} must be a non-empty string ("provider:model" | provider | model) — got ${Array.isArray(val) ? "array" : typeof val} — dropped`)
232
+ dropped.push(`agent.subagentModels.${role} must be a non-empty string ("provider:model" | model) — got ${Array.isArray(val) ? "array" : typeof val} — dropped`)
204
233
  }
205
234
  return { value, dropped }
206
235
  }
@@ -210,12 +239,12 @@ function warnSubagentModelsFiltered(dropped, path) {
210
239
  if (warnedSubagentModels || dropped.length === 0) return
211
240
  warnedSubagentModels = true
212
241
  console.warn(`[config] agent.subagentModel/subagentModels: ${dropped.length} invalid entr${dropped.length === 1 ? "y ignored" : "ies ignored"} (filtered — startup continues; no crash):\n` +
213
- `${dropped.map((d) => ` - ${d}`).join("\n")}\n Fix: clean it in ${path} — "provider:model" | provider | model.`)
242
+ `${dropped.map((d) => ` - ${d}`).join("\n")}\n Fix: clean it in ${path} — "provider:model" | model.`)
214
243
  }
215
244
 
216
245
  /**
217
246
  * Find provider by name in providers[].
218
- * Throws if name is non-empty but not found — a typo in activeProvider silently falling to the first provider would use the wrong key on the wrong endpoint.
247
+ * Throws if name is non-empty but not found — a typo in the caller-supplied name (e.g. `defaultModel`) silently falling to the first provider would use the wrong key on the wrong endpoint.
219
248
  * Returns the first provider when name is empty.
220
249
  */
221
250
  export function findProvider(providers, name) {
@@ -223,24 +252,35 @@ export function findProvider(providers, name) {
223
252
  const found = providers.find((p) => p.name === name)
224
253
  if (found) return found
225
254
  const available = providers.map((p) => p.name).join(", ") || "(empty)"
226
- throw new Error(`activeProvider "${name}" not in providers list (available: ${available}); check for a typo in: ${configPath}`)
255
+ throw new Error(`provider "${name}" not in providers list (available: ${available}); check for a typo in: ${configPath}`)
227
256
  }
228
- return providers[0] ?? { name: "default", baseURL: "", model: "" }
257
+ return providers[0] ?? { name: "default", baseURL: "" }
229
258
  }
230
259
 
231
- /** Normalize proxy config to { uri, web, model } or undefined (uri/url both accepted; invalid types dropped) */
260
+ /** Normalize proxy config to { uri, web } or undefined (uri/url both accepted; invalid types dropped) */
232
261
  export function normalizeProxy(proxy) {
233
- if (typeof proxy === "string") return proxy ? { uri: proxy, web: true, model: false } : undefined
262
+ if (typeof proxy === "string") return proxy ? { uri: proxy, web: true } : undefined
234
263
  if (!proxy || typeof proxy !== "object" || Array.isArray(proxy)) return undefined
235
264
  const uri = proxy.uri || proxy.url || ""
236
265
  if (typeof uri !== "string" || !uri) return undefined
237
- return { uri, web: proxy.web !== false, model: proxy.model === true }
266
+ return { uri, web: proxy.web !== false }
267
+ }
268
+
269
+ /** 团队登录段加载归一(B1 批——`docs/core/design/TEAM.md` §2.1 ∥ CONFIG.md §6.4):非对象 ⇒ null(软失败
270
+ * 不阻启动——沿 proxy 段先例);键级 = trim 非空字符串(非串 ∥ 空 ⇒ null);`member` 浅形 `{ username, name }`。
271
+ * **不入 `DEFAULTS`**(settings 工具面零涉——agent 不可写;`team.token` 经读面到达 = 敏感段词表遮罩)。 */
272
+ export function normalizeTeamSection(value) {
273
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null
274
+ const str = (v) => (typeof v === "string" && v.trim() !== "" ? v.trim() : null)
275
+ const raw = value.member
276
+ const member = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? { username: str(raw.username), name: str(raw.name) } : null
277
+ return { server: str(value.server), member, label: str(value.label), token: str(value.token) }
238
278
  }
239
279
 
240
280
  /**
241
281
  * Load configuration.
242
282
  * No env-var overrides — config.json is the single source of truth
243
- * (API keys, baseURL, model, activeProvider all come from the file).
283
+ * (API keys, baseURL, defaultModel all come from the file).
244
284
  */
245
285
  /** Keep only { header: "string value" } pairs from a provider's headers field — anything
246
286
  * else (null, arrays, nested objects) is dropped so it can never reach a fetch call.
@@ -289,6 +329,7 @@ export function loadConfig() {
289
329
  agent: { ...DEFAULTS.agent, ...config.agent },
290
330
  memory: { ...DEFAULTS.memory, ...config.memory },
291
331
  embedding: { ...DEFAULTS.embedding, ...config.embedding },
332
+ browser: { ...DEFAULTS.browser, ...config.browser },
292
333
  traces: { ...DEFAULTS.traces, ...config.traces },
293
334
  diagnostics: { ...DEFAULTS.diagnostics },
294
335
  }
@@ -306,10 +347,9 @@ export function loadConfig() {
306
347
  const _diagRaw = config.diagnostics && typeof config.diagnostics === "object" && !Array.isArray(config.diagnostics) ? config.diagnostics : {}
307
348
  for (const key of ["heapWatch", "heapSnapshot"]) if (typeof _diagRaw[key] === "boolean") merged.diagnostics[key] = _diagRaw[key]
308
349
 
309
- // providers[].model 内存归一(v2 M3:非空字符串保留;非字符串/空串归一删除)——渠道默认模型
310
- // 单值;无默认模型合法(模型选择经 /models 拉取候选——准入判据见 M8/M9)。
350
+ // providers[].model 一律删除(2026-10-09 清除批——渠道单值模型退场:渠道不携模型,不落任何
351
+ // 回退;模型身份唯二 = 顶层 defaultModel 复合串 ∥ 会话槽选定——PROVIDER.md §6.16 M3)。
311
352
  for (const p of merged.providers) {
312
- if (typeof p.model === "string" && p.model.trim()) continue
313
353
  if (p.model !== undefined) delete p.model
314
354
  }
315
355
 
@@ -351,10 +391,13 @@ export function loadConfig() {
351
391
  if (p.baseURL) p.baseURL = p.baseURL.replace(/\/+$/, "")
352
392
  }
353
393
 
354
- // Normalize proxy: string → { uri, web:true, model:false }; object 补默认值;非法类型丢弃。
394
+ // Normalize proxy: string → { uri, web:true }; object 补默认值;非法类型丢弃。
355
395
  // 保证 agent.config.proxy 永远是规范形态或 undefined
356
396
  merged.proxy = normalizeProxy(merged.proxy)
357
397
 
398
+ // 团队登录段(B1 批——TEAM.md §2.1):加载归一(形不符 ⇒ null——软失败不阻启动);不入 DEFAULTS
399
+ merged.team = normalizeTeamSection(merged.team)
400
+
358
401
  // Runtime provider = 统一解析(#841——resolveProviderPlan 单源:回退序 + 三态;PROVIDER.md §6.22)。
359
402
  // 不可运行(渠表空 ∥ 全表无 key)⇒ {} + providerInvalidReason(D-S1 处置不 throw——make-agent 打
360
403
  // _providerInvalid 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用
@@ -387,7 +430,7 @@ export function loadConfig() {
387
430
  /**
388
431
  * MCP.md §5 D-3 (2026-09-01): re-read config.json and replace ONLY the agent's mcp section
389
432
  * — the agent 代配 closed loop (agent edits config.json with its edit tool, /mcp picks it
390
- * up). Never touches other config sections (providers/activeProvider stay as loaded).
433
+ * up). Never touches other config sections (providers/defaultModel stay as loaded).
391
434
  *
392
435
  * Malformed disk config → memory state kept, { ok:false, error } returned (the /mcp menu
393
436
  * shows "⚠ disk config unreadable"). Never throws.
@@ -2,7 +2,7 @@
2
2
  * context-degrade.mjs — 确定性降级面(无 LLM 的有损收缩 · 自 `context.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786)。
3
3
  * 内容 = `pruneStub` ∥ `pruneStaleToolOutputs`(F-CC3 陈旧工具输出清理)∥ `OVERSIZE_CONTENT_LIMIT` ∥
4
4
  * `shrinkOversized`(splitHistory 无中间段时的单条超限收缩)——迁出块逐字。
5
- * 原档 `context.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改);
5
+ * 原档 `context.mjs` 经 `export { … } from` 转口保名(消费面 / 单元测试档 import 面零改);
6
6
  * 跨档被引的原私有面(`shrinkOversized`)补 `export` 关键字。
7
7
  */
8
8
  import { collectStaleToolOutputs } from "./token-window.mjs"
package/context-echo.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  * context-echo.mjs — 回声安全面(D-CC18 ∥ D-CC19 恢复面归并 · 自 `context.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786)。
3
3
  * 内容 = `COMPACTION_PLACEHOLDER` ∥ 前缀器族(`prefixContent` / `withPlaceholderPrefix`)∥ 回声对归并
4
4
  * (`isAssistantEchoPair` / `contentTextOf` / `absorbEchoContent` / `mergeAdjacentAssistantEchoes`)——迁出块逐字。
5
- * 原档 `context.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改);
5
+ * 原档 `context.mjs` 经 `export { … } from` 转口保名(消费面 / 单元测试档 import 面零改);
6
6
  * 跨档被引的原私有面(`COMPACTION_PLACEHOLDER` ∥ `withPlaceholderPrefix`)补 `export` 关键字。
7
7
  */
8
8
 
package/context-push.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  * context-push.mjs — 消息双线写入单点(写缝面 · 自 `context.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786)。
3
3
  * `pushReal` = 真实消息写缝(机器线 `agent.history` + 人读线 `agent._fullHistory` 同源双写);
4
4
  * `pushRecord` = 人读线留档记录单点(机器线零触)。迁出块逐字;原档 `context.mjs` 经
5
- * `export { … } from` 转口保名(消费面 / 批内件 import 面零改)。
5
+ * `export { … } from` 转口保名(消费面 / 单元测试档 import 面零改)。
6
6
  */
7
7
 
8
8
  /**
@@ -40,7 +40,7 @@ export function pushReal(agent, msg) {
40
40
  * pushRecord — 人读线留档记录单点(消化生命周期面 · #726;形 ∥ 写缝 ∥ 判据单源 = SESSION.md §6.26)。
41
41
  * `pushReal` 双胞(直复用——零算法副本):`ts` 打点 ∥ 记录存储追加(`_recordStore?.append`——§6.14)∥
42
42
  * 尾窗驱逐三面同源;**机器线零触** = `history` 以一次性弃数组承接 ⇒ 记录不入 `agent.history` /
43
- * `contextHistory`(不喂模型)。未绑定(`_recordStore` 缺)⇒ 人读线追加照常 ∥ 存储腿空转(零抛——模式 F
43
+ * `contextHistory`(不喂模型)。未绑定(`_recordStore` 缺)⇒ 人读线追加照常 ∥ 存储分支空转(零抛——模式 F
44
44
  * 零回归);载体缺位(无活跃会话)⇒ 零动作(零抛——日志归端侧)。
45
45
  */
46
46
  export function pushRecord(agent, record) {
package/context.mjs CHANGED
@@ -250,7 +250,7 @@ export function compressFallback(agent) {
250
250
 
251
251
  // ─── 迁出面(import 面保持:TUI / verify-compress / VSC 对拍经本档取——先例 = 下方 explore-distill 再导出)──
252
252
  export { estimateTokens }
253
- // ─── 拆分转口(2026-10-01 拆分批 · #755 ∥ #786):写缝 ∥ 回声 ∥ 降级三面外提——消费面 / 批内件 import 面零改 ───
253
+ // ─── 拆分转口(2026-10-01 拆分批 · #755 ∥ #786):写缝 ∥ 回声 ∥ 降级三面外提——消费面 / 单元测试档 import 面零改 ───
254
254
  export { pushReal, pushRecord } from "./context-push.mjs"
255
255
  export { isAssistantEchoPair, mergeAdjacentAssistantEchoes } from "./context-echo.mjs"
256
256
  export { pruneStaleToolOutputs } from "./context-degrade.mjs"
package/conventions.mjs CHANGED
@@ -187,6 +187,6 @@ export function isStatePath(p, conv) {
187
187
 
188
188
  // ─────────────────────────────────────────────────────────────────────────────
189
189
  // 迁出面(2026-10-01 拆分批 · #755 ∥ #786):声明装载面外提 `declaration.mjs`——本档经
190
- // 转口保名(消费面 / 批内件 import 面零改;`DEFAULT_DECLARATION` 另供本档分类裁判缺省取用)。
190
+ // 转口保名(消费面 / 单元测试档 import 面零改;`DEFAULT_DECLARATION` 另供本档分类裁判缺省取用)。
191
191
  // ─────────────────────────────────────────────────────────────────────────────
192
- export { loadProjectDeclaration, DEFAULT_DECLARATION, isExcludedRelPath, declaredPublicRoots, clearDeclarationCache } from "./declaration.mjs"
192
+ export { loadProjectDeclaration, declarationForTarget, DEFAULT_DECLARATION, isExcludedRelPath, declaredPublicRoots, clearDeclarationCache } from "./declaration.mjs"
package/declaration.mjs CHANGED
@@ -3,11 +3,11 @@
3
3
  * · 自 `conventions.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786。
4
4
  * 投影单源 = `manifest.mjs`(ONE reader / ONE root resolution——`readManifest` + `manifestFilePath`;
5
5
  * name / shape / defaults = schema 面 `docs/core/design/MANIFEST.md` §2.2)。迁出块逐字;
6
- * 原档 `conventions.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改)。
6
+ * 原档 `conventions.mjs` 经 `export { … } from` 转口保名(消费面 / 单元测试档 import 面零改)。
7
7
  */
8
8
  import { existsSync, statSync } from "node:fs"
9
9
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"
10
- import { DEFAULT_MANIFEST, manifestFilePath, readManifest } from "./manifest.mjs"
10
+ import { DEFAULT_MANIFEST, manifestFilePath, owningProject, readManifest } from "./manifest.mjs"
11
11
  import { logEvent } from "./log.mjs"
12
12
 
13
13
  /** Retired declaration carrier (project-root relative). Its EXISTENCE is checked, its
@@ -216,3 +216,19 @@ export function loadProjectDeclaration(cwd) {
216
216
  _cache.set(file, decl)
217
217
  return decl
218
218
  }
219
+
220
+ /**
221
+ * 辖域单点(F11 · 2026-10-08 · #1104 · PORTABILITY §3.2 行 8 / §3.9 / D20):目标**绝对路径**
222
+ * ⇒ 沿祖先链取**最近**带档目录(`owningProject` 归属形——nearest wins ∕ 纯 fs)⇒ 该项目的声明
223
+ * 对象(`loadProjectDeclaration` 本体零改——按档路径缓存);祖先链无档 ⇒ `null` = **出辖**
224
+ * (不在任何 manifest 树内——父侧门不判不拦)。
225
+ * **发现梯(`discoverProjects`)不参与**——无候选 ∥ 歧义均出辖(不在无档面凭空落脚)。
226
+ * 相对形由消费面按会话 cwd 先解析(判据 = 目标所属项目,非会话 cwd)。
227
+ * @param {string} targetAbs 目标绝对路径(目录 / 文件)
228
+ * @returns {Readonly<object>|null} 所属项目的声明对象 / null = 出辖
229
+ */
230
+ export function declarationForTarget(targetAbs) {
231
+ const root = owningProject(targetAbs)
232
+ if (!root) return null
233
+ return loadProjectDeclaration(root)
234
+ }
package/file-links.mjs CHANGED
@@ -22,7 +22,7 @@ export const MAX_LINKS = 50
22
22
  /**
23
23
  * 工具结果文本 ⇒ 验存文件链接(去重 + 封顶)。`cwd` = 项目根(相对 token 的解析基);缺 ∕ 空 ⇒ 相对 token
24
24
  * 一律不检(禁假链接);绝对 token 与 cwd 无关,照检。`probe` = 盘面探针注入缝(`{ existsSync(p), statSync(p) }`——
25
- * `node:fs` 同名面;桌面端侧注入)。零文本(`""` ∕ 非串)先于探针校验早退(零判据面 —— 探针不检)。
25
+ * `node:fs` 同名面;桌面端侧注入)。零文本(`""` ∕ 非串)先于探针校验早退(零判据 —— 探针不检)。
26
26
  */
27
27
  export function extractFileLinks(cwd, text, probe) {
28
28
  if (typeof text !== "string" || text === "") return []
package/file-refs.mjs CHANGED
@@ -26,7 +26,7 @@ function probeOf(probe) {
26
26
  }
27
27
 
28
28
  /** Scan text for @path references and replace with inline file content.
29
- * 零文本(非串 ∥ 空串)先于探针校验早退(零判据面——探针不检,同序沿核 `file-links.mjs:28`)。 */
29
+ * 零文本(非串 ∥ 空串)先于探针校验早退(零判据——探针不检,同序沿核 `file-links.mjs:28`)。 */
30
30
  export function injectAtRefs(text, cwd, probe) {
31
31
  if (typeof text !== "string" || text === "") return text
32
32
  const io = probeOf(probe)
@@ -112,7 +112,7 @@ function pairToolEntries(history) {
112
112
  * `opts.records === true`(opt-in —— 留档批 · #719)⇒ `digest` ∕ `subagent` 两型
113
113
  * 记录原样入窗(`{ ...record, idx }`,字段零改名)并占条目位(游标按条目推进);
114
114
  * 记录不参与 turn ∕ 可见性谓词(`visible` 面语义不动)。**默认关** ⇒ 输出与改前
115
- * 逐字等价(负控腿);缺 `opts` ∕ 非 `true` ⇒ 同默认径。
115
+ * 逐字等价(负控分支);缺 `opts` ∕ 非 `true` ⇒ 同默认径。
116
116
  */
117
117
  export function historyWindow(history, before, pageSize = HISTORY_PAGE_SIZE, opts = {}) {
118
118
  const total = Array.isArray(history) ? history.length : 0
package/i18n.mjs CHANGED
@@ -70,6 +70,8 @@ export const CORE_MESSAGES = Object.freeze({
70
70
  "status.stopped": { en: "[stopped]", zh: "[已停止]" },
71
71
  "status.thinking": { en: "Thinking", zh: "思考中" },
72
72
  "status.turn": { en: "turn ${n}/${m}", zh: "轮次 ${n}/${m}" },
73
+ // ── 团队登录态(登录面补全批 · 2026-10-10 · 台账 #1231——TEAM.md §2.6:已失效段 ∥ 面板失效行;图形端经核字典投影直取)──
74
+ "status.team.invalid": { en: "Session expired — log in again", zh: "已失效——重新登录" },
73
75
  "tool.running": { en: "running…", zh: "执行中…" },
74
76
  "tool.done": { en: "done", zh: "完成" },
75
77
  // ── 台账(ledger)· F-LX3 变体键库首跑提示(批 2026-10-05-ledger-variant-db-notice · 台账 #935 · LEDGER.md §12.2 逐字;消费面 = CLI TUI 启动行)
package/ledger-cmd.mjs CHANGED
@@ -11,13 +11,26 @@
11
11
  * 消费侧静态 import 会破 W8 契约②)。
12
12
  */
13
13
  import { resolve } from "node:path"
14
- import { ALLOWED_MIGRATIONS, nowIso, openLedger, PENDING_STATUSES } from "./ledger-db.mjs"
14
+ import { ALLOWED_MIGRATIONS, nowIso, openLedger, PENDING_STATUSES, AGING_DAYS } from "./ledger-db.mjs"
15
15
  import { resolveProjectRoot } from "./manifest.mjs"
16
16
  import { resolveDeclaredRef } from "./declaration.mjs"
17
17
 
18
- /** 查询(只读,全角色):SELECT 行集(status/kind/board 过滤,缺省 = 全部行;id 升序)。库不在 = 空账。
19
- * 只读开库(readOnly 句柄 + 零 DDL/ALTER——read-data-interface 批 FR2③);库档非台账库 ⇒ 空账。 */
20
- export function ledgerQuery({ cwd, status = null, kind = null, board = null } = {}) {
18
+ /** 逐行老化判真(LEDGER.md §13.4 · 2026-10-07 批 ledger-tool):未决四态(`PENDING_STATUSES`)∧
19
+ * 行龄 > `AGING_DAYS` 天;行龄源 = `updated_at ?? created_at`(§5 同源);时间戳缺 / 不可解析
20
+ * ⇒ `false`(零假阳降级——与 `buildScan` 行龄口径同源)。 */
21
+ function isRowAged(row, now) {
22
+ if (!PENDING_STATUSES.includes(row.status)) return false
23
+ const t = row.updated_at ?? row.created_at
24
+ if (t == null) return false
25
+ const ms = Date.parse(String(t))
26
+ return Number.isFinite(ms) && (now - ms) / 86400000 > AGING_DAYS
27
+ }
28
+
29
+ /** 查询(只读,全角色):SELECT 行集(status / kind / board / trigger 等值过滤并取,缺省 = 全部行;id 升序)。
30
+ * 库不在 = 空账。只读开库(readOnly 句柄 + 零 DDL/ALTER——read-data-interface 批 FR2③);库档非台账库 ⇒ 空账。
31
+ * 逐行携计算字段 `aged: boolean`(§13.4——不入库、不入导出白名单 DATA_COLUMNS);`trigger` 过滤
32
+ * 仅三枚举等值(`NULL` 不过滤——批面未列,§13.2);`now` = 注入缝(确定性用例)。 */
33
+ export function ledgerQuery({ cwd, status = null, kind = null, board = null, trigger = null, now = Date.now() } = {}) {
21
34
  const db = openLedger(cwd, { readOnly: true })
22
35
  if (!db) return []
23
36
  try {
@@ -25,7 +38,9 @@ export function ledgerQuery({ cwd, status = null, kind = null, board = null } =
25
38
  if (status) { where.push("status = ?"); args.push(status) }
26
39
  if (kind) { where.push("kind = ?"); args.push(kind) }
27
40
  if (board) { where.push("board = ?"); args.push(board) }
28
- return db.prepare(`SELECT * FROM items${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`).all(...args)
41
+ if (trigger) { where.push("trigger = ?"); args.push(trigger) }
42
+ const rows = db.prepare(`SELECT * FROM items${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`).all(...args)
43
+ return rows.map((r) => ({ ...r, aged: isRowAged(r, now) }))
29
44
  } finally { db.close() }
30
45
  }
31
46
 
@@ -75,9 +90,11 @@ export function ledgerAdd({ cwd, row }) {
75
90
  }
76
91
 
77
92
  /** executor 目标值计算(设计档 docs/core/design/LEDGER.md §3.1——判位在 ledgerUpdate 体内:迁移表判 → 本语义 → 写门 → UPDATE)。
78
- * 优先级 = patch 显式 > 自动语义(进在途 = executorSessionId / 出在途 = NULL)> 行现值兜底 > NULL。 */
93
+ * 优先级 = patch 显式 > 自动语义(进边 = executorSessionId / 出边 = NULL)> 行现值兜底 > NULL。
94
+ * 进边集(2026-10-07 批 ledger-tool 扩面——§13.3 / KD-LT1):`待讨论 → 待设计`(批点火边)∥
95
+ * `待设计 → 在途`(实施入边)——两枚同式。 */
79
96
  function resolveExecutorTarget(row, to, patch, executorSessionId) {
80
- if (row.status === "待设计" && to === "在途") {
97
+ if ((row.status === "待讨论" && to === "待设计") || (row.status === "待设计" && to === "在途")) {
81
98
  return patch.executor ?? executorSessionId ?? row.executor ?? null
82
99
  }
83
100
  if (row.status === "在途" && (to === "待核销" || to === "已废弃")) {
@@ -86,9 +103,29 @@ function resolveExecutorTarget(row, to, patch, executorSessionId) {
86
103
  return patch.executor ?? row.executor ?? null
87
104
  }
88
105
 
106
+ /** `evidence` 值计算(设计档 LEDGER.md §13.9——update / close 两函数同源一处落):追加缺省(旧值非空 ⇒
107
+ * `旧 + "|" + 新`——「|」两侧零空格直接相接;旧空(`null` ∥ 空串 ∥ 全空白)⇒ 直写,零分隔符残留);
108
+ * `evidenceReplace === true` 与值同传 ⇒ 整段替换(旧文零保留);`evidence` 缺 / `null` ≡ 略去(零变)。
109
+ * 拒面①(旗独传——无新文可算)判于本阶段(close 侧先于追认门);核内旗判只认 `=== true`(类型严判在
110
+ * 工具层)。两值逐字落行(不裁 / 不判内容)。 */
111
+ function evidenceValue(fnName, oldValue, evidence, evidenceReplace) {
112
+ if (evidenceReplace === true && evidence == null) throw new Error(`${fnName}:evidenceReplace 须与 evidence 同传(缺新文)`)
113
+ if (evidence == null) return oldValue
114
+ const old = oldValue == null || String(oldValue).trim() === "" ? "" : String(oldValue)
115
+ return evidenceReplace === true || old === "" ? evidence : `${old}|${evidence}`
116
+ }
117
+
118
+ /** 拒面②(新值全空白——追加 ∥ 覆盖两径同判)判点(§13.9 判序):update 随值计算同拍 ∥ close 落
119
+ * 追认门之后;`evidence` 未传(`null` / 略去)⇒ 不判。 */
120
+ function assertEvidenceValue(fnName, evidence) {
121
+ if (evidence != null && String(evidence).trim() === "") throw new Error(`${fnName}:evidence 为空(非空字符串)`)
122
+ }
123
+
89
124
  /** 更新(写命令,仅主 agent):UPDATE——状态迁移前判允许迁移表(不在表内 → 拒,行不变)。
90
125
  * executor 目标值随同一 UPDATE 落列(LEDGER.md §3.1);executorSessionId = 调用会话(函数参数注入——K-LX1
91
- * 纯函数可测;工具层动态 import getSessionId() 供值,零新静态边)。 */
126
+ * 纯函数可测;工具层动态 import getSessionId() 供值,零新静态边)。
127
+ * `evidence` 传值 = 追加缺省、`patch.evidenceReplace` = 布尔覆盖旗(§13.9——值计算 / 两拒面 = `evidenceValue`
128
+ * / `assertEvidenceValue`;判位 = 本语义段,随 executor 目标值同拍)。 */
92
129
  export function ledgerUpdate({ cwd, id, patch, executorSessionId }) {
93
130
  const db = openLedger(cwd, { create: true })
94
131
  try {
@@ -98,20 +135,27 @@ export function ledgerUpdate({ cwd, id, patch, executorSessionId }) {
98
135
  if (to !== row.status && !(ALLOWED_MIGRATIONS[row.status] ?? []).includes(to)) {
99
136
  throw new Error(`ledgerUpdate:迁移 ${row.status} → ${to} 不在允许迁移表`)
100
137
  }
101
- const nextTaskBook = patch.task_book ?? row.task_book
102
- assertTaskBookGate(cwd, "ledgerUpdate", to, nextTaskBook) // 判序:迁移表判 → 本门 → UPDATE
138
+ // 判序(§13.9):迁移表判 → 本语义(executor 目标值 + evidence 值计算——含两拒面)→ 写门 → UPDATE
103
139
  const nextExecutor = resolveExecutorTarget(row, to, patch, executorSessionId)
140
+ const nextEvidence = evidenceValue("ledgerUpdate", row.evidence, patch.evidence, patch.evidenceReplace)
141
+ assertEvidenceValue("ledgerUpdate", patch.evidence)
142
+ const nextTaskBook = patch.task_book ?? row.task_book
143
+ assertTaskBookGate(cwd, "ledgerUpdate", to, nextTaskBook) // → 写门 → UPDATE
104
144
  const now = nowIso()
105
145
  db.prepare(`UPDATE items SET status = ?, title = ?, board = ?, req_doc = ?, task_book = ?, evidence = ?, trigger = ?, executor = ?, updated_at = ? WHERE id = ?`)
106
- .run(to, patch.title ?? row.title, patch.board ?? row.board, patch.req_doc ?? row.req_doc, nextTaskBook, patch.evidence ?? row.evidence, patch.trigger ?? row.trigger, nextExecutor, now, id)
146
+ .run(to, patch.title ?? row.title, patch.board ?? row.board, patch.req_doc ?? row.req_doc, nextTaskBook, nextEvidence, patch.trigger ?? row.trigger, nextExecutor, now, id)
107
147
  return { id }
108
148
  } finally { db.close() }
109
149
  }
110
150
 
111
151
  /** 收口(写命令,仅主 agent):核销两源(勾销:待核销 → 已核销;追认核销:待讨论 / 待设计 → 已核销,
112
152
  * 行 `evidence` 非空,缺 / 全空白 ⇒ 拒)/ 撤回(任意态 → 已废弃),事务包裹;归档 = 软删除。
113
- * 判序(同函数体同层):目标集判 → 行取 → 源态判 → `evidence` 门 → UPDATE(LEDGER.md §3 收口两源)。 */
114
- export function ledgerClose({ cwd, id, status }) {
153
+ * 可选 `evidence` 参:传值 = 追加缺省(§13.9——旧空直写 / 旧非空拼 `旧|新`)、`evidenceReplace: true`
154
+ * 同传 = 整段替换;勾销 / 追认 / 撤回三径同携(写值落行,内容不判);两拒面(旗独传 @值计算 ∥
155
+ * 新值空白 @追认门后)见 `evidenceValue` / `assertEvidenceValue`。
156
+ * 判序(同函数体同层,§13.9):目标集判 → 行取 → 源态判 → 值计算 → 追认门(原样——判**结果值**缺 /
157
+ * 全空白 ⇒ 拒,文案逐字不变)→ 拒面②(新值空白)→ UPDATE(LEDGER.md §3 收口两源)。 */
158
+ export function ledgerClose({ cwd, id, status, evidence, evidenceReplace }) {
115
159
  if (status !== "已核销" && status !== "已废弃") throw new Error(`ledgerClose:目标态 ${status} ∉ {已核销, 已废弃}`)
116
160
  const db = openLedger(cwd, { create: true })
117
161
  try {
@@ -119,12 +163,14 @@ export function ledgerClose({ cwd, id, status }) {
119
163
  try {
120
164
  const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
121
165
  if (!row) throw new Error(`ledgerClose:行 ${id} 不存在`)
122
- // 判序:源态判(核销仅三源——在途不可跳)→ 追认口 `evidence` 门(行字段非空,缺 / 全空白 ⇒ 拒)
166
+ // 判序(§13.9):源态判(核销仅三源——在途不可跳)→ 值计算(拒面① 旗独传判于此)→ 追认口 `evidence` 门
123
167
  if (status === "已核销" && !["待讨论", "待设计", "待核销"].includes(row.status)) throw new Error(`ledgerClose:核销仅限 待讨论 / 待设计 / 待核销(现态 ${row.status})`)
124
- if (status === "已核销" && ["待讨论", "待设计"].includes(row.status) && (row.evidence == null || String(row.evidence).trim() === "")) throw new Error(`ledgerClose:追认核销须带 evidence(现态 ${row.status})`)
168
+ const nextEvidence = evidenceValue("ledgerClose", row.evidence, evidence, evidenceReplace)
169
+ if (status === "已核销" && ["待讨论", "待设计"].includes(row.status) && (nextEvidence == null || String(nextEvidence).trim() === "")) throw new Error(`ledgerClose:追认核销须带 evidence(现态 ${row.status})`)
170
+ assertEvidenceValue("ledgerClose", evidence) // 判序:拒面②(新值空白)落追认门之后 → UPDATE
125
171
  const now = nowIso()
126
172
  // 撤回(任意态 → 已废弃——含在途直撤)同步 executor = NULL(LEDGER.md §3.1 ④);核销两源(勾销 / 追认)executor 零触碰(非在途出边)
127
- db.prepare("UPDATE items SET status = ?, closed_at = ?, updated_at = ?, executor = ? WHERE id = ?").run(status, now, now, status === "已废弃" ? null : row.executor, id)
173
+ db.prepare("UPDATE items SET status = ?, closed_at = ?, updated_at = ?, executor = ?, evidence = ? WHERE id = ?").run(status, now, now, status === "已废弃" ? null : row.executor, nextEvidence, id)
128
174
  db.exec("COMMIT")
129
175
  return { id, status }
130
176
  } catch (e) { db.exec("ROLLBACK"); throw e }
package/ledger-db.mjs CHANGED
@@ -55,6 +55,12 @@ export function ledgerDbPath(cwd) {
55
55
  /** 未决四态(「活条目」口径 = 计数单源 WHERE 集;已核销 / 已废弃 = 归档态)。 */
56
56
  export const PENDING_STATUSES = ["待讨论", "待设计", "在途", "待核销"]
57
57
 
58
+ /** 老化阈值(天——口径 = 需求档;消费面 = `buildScan`(展示,`days` 可覆盖)+ `ledgerQuery` 逐行
59
+ * `aged`(查询面,`now` 可注入))。2026-10-07 批 ledger-tool:自 `ledger.mjs` 迁入(枚举常量档单源
60
+ * ——`ledgerQuery` 计算面直引本档,消 `ledger-cmd ↔ ledger.mjs` 新环;`ledger.mjs` 同名 re-export
61
+ * 保公共面零变——LEDGER.md §13.4)。 */
62
+ export const AGING_DAYS = 30
63
+
58
64
  /** 台账库结构版本(N2 标记**单源**——`PRAGMA user_version` 落标:写面开库 `v < 本值 ⇒ 置`;
59
65
  * 读面如实回读(旧库未标 = 0);结构变更(新列 / 新表)同步升号——2026-10-03 read-data-interface 批)。 */
60
66
  export const LEDGER_SCHEMA_VERSION = 1
@@ -3,8 +3,13 @@
3
3
  *
4
4
  * 面:族扫描 → 判活批量解析 → 状态位(`state.ledger`——L1 常驻标记,范围归约 `scopeMarkerOf`
5
5
  * 单源 §7.2,渲染端消费);只读台账,本面零写。
6
+ *
7
+ * **写后即时**(2026-10-13 缺陷修复——用户原话「写了以后就要立即更新」):台账库目录
8
+ * `fs.watch` ⇒ 去抖立即重扫(同进程 ∥ 跨进程写皆覆盖);`REFRESH_MS` 周期降为兜底。
6
9
  */
10
+ import { watch } from "node:fs"
7
11
  import { buildScan, discoverFamily, REFRESH_MS, resolveExecutorStates, scopeMarkerOf } from "./ledger.mjs"
12
+ import { ledgerDirPath } from "./ledger-db.mjs"
8
13
 
9
14
  /** 单次扫描(直驱面——timer 包装见 `startLedgerSurface`)。**async**(F-LX1:await 判活解析——
10
15
  * LEDGER.md §7.3.1 ④ 读面不阻塞事件循环)。 */
@@ -23,18 +28,50 @@ export async function runLedgerScan({ state, agent = null, anchor = null, render
23
28
  render()
24
29
  }
25
30
 
26
- /** 挂载:首扫(`setImmediate`——不抢首帧)+ 周期(`REFRESH_MS`;`state.processing` 期间跳过本轮)。
31
+ /** 挂载:首扫(`setImmediate`——不抢首帧)+ 周期兜底(`REFRESH_MS`)
32
+ * + **写后即时**(台账库目录变更 ⇒ 去抖 400ms 重扫——同进程 ∥ 跨进程写皆覆盖)。
27
33
  * `tick` 异步化(F-LX1——runLedgerScan async):await 且 catch 不崩(N1)。重叠防衛:
28
- * 判活探测秒级可达,上一轮未落定时跳过本轮(防探束堆积)。 */
34
+ * 判活探测秒级可达,上一轮未落定时跳过本轮(防探束堆积)。
35
+ * `state.processing` 跳过仅限**定时轮**(不打断回合帧);库变更轮放行(写后立即可见 ——
36
+ * 用户 2026-10-13「写了以后就要立即更新」)。
37
+ * 评审 #5 修复(2026-10-13):① 变更轮遇在途扫描 ⇒ 记待办、落定补跑(不丢);② watcher 缺席
38
+ * (目录未建)⇒ 2s 短重试补挂(闭「首写才建」窗口)。 */
29
39
  export function startLedgerSurface(ctx = {}) {
30
40
  let disposed = false
31
41
  let timer = null
32
42
  let inflight = false
33
- const tick = async () => {
34
- if (disposed || inflight) return
35
- if (ctx.state?.processing) return // 扫描不打断回合帧
43
+ let watcher = null
44
+ let schedTimer = null
45
+ let armTimer = null
46
+ let pendingForce = false
47
+ const tick = async (force = false) => {
48
+ if (disposed) return
49
+ if (inflight) { if (force) pendingForce = true; return } // 在途扫描:变更轮记待办(落定补跑——不丢)
50
+ if (!force && ctx.state?.processing) return // 定时轮不打断回合帧
36
51
  inflight = true
37
52
  try { await runLedgerScan(ctx) } catch { /* 可见面尽力——不崩(N1) */ } finally { inflight = false }
53
+ armWatcher() // 目录可能首写才建——每轮后补挂一次(零成本:已挂则直接返)
54
+ if (pendingForce && !disposed) { pendingForce = false; void tick(true) } // 补跑被在途挤掉的变更轮
55
+ }
56
+ const onFsEvent = (_evt, name) => {
57
+ if (name && !/\.db($|-(wal|shm|journal)$)/.test(String(name))) return
58
+ if (schedTimer) clearTimeout(schedTimer)
59
+ schedTimer = setTimeout(() => { schedTimer = null; tick(true) }, 400)
60
+ schedTimer.unref?.()
61
+ }
62
+ function armWatcher() {
63
+ if (disposed || watcher) return
64
+ try {
65
+ watcher = watch(ledgerDirPath(), { persistent: false }, onFsEvent)
66
+ watcher.unref?.()
67
+ if (armTimer) { clearTimeout(armTimer); armTimer = null }
68
+ } catch {
69
+ // 目录未建(首写才建)——2s 短重试补挂(评审 #5 发现②:闭「首写不被观测」窗口)
70
+ if (!armTimer) {
71
+ armTimer = setTimeout(() => { armTimer = null; armWatcher() }, 2000)
72
+ armTimer.unref?.()
73
+ }
74
+ }
38
75
  }
39
76
  setImmediate(() => {
40
77
  if (disposed) return
@@ -42,11 +79,18 @@ export function startLedgerSurface(ctx = {}) {
42
79
  timer = setInterval(() => tick(), REFRESH_MS)
43
80
  timer.unref?.()
44
81
  })
82
+ armWatcher()
45
83
  return {
46
84
  dispose() {
47
85
  disposed = true
48
86
  if (timer) clearInterval(timer)
87
+ if (schedTimer) clearTimeout(schedTimer)
88
+ if (armTimer) clearTimeout(armTimer)
89
+ if (watcher) watcher.close()
49
90
  timer = null
91
+ schedTimer = null
92
+ armTimer = null
93
+ watcher = null
50
94
  },
51
95
  }
52
96
  }