@thincoder/core 0.9.4 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +14 -14
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +23 -13
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +55 -294
- package/agent/family-tools.mjs +11 -11
- package/agent/helpers.mjs +63 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +19 -15
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +90 -30
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +11 -11
- package/agent-tools/advisor-settle.mjs +17 -17
- package/agent-tools/advisor.mjs +38 -43
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +23 -20
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +65 -34
- package/agent-tools/batch-paths.mjs +186 -0
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +22 -119
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +58 -58
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +18 -13
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +3 -3
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +29 -273
- package/agent-tools/subagent-async.mjs +36 -31
- package/agent-tools/subagent-panel.mjs +38 -9
- package/agent-tools/subagent-run.mjs +44 -29
- package/agent-tools/subagent-scheduler.mjs +29 -28
- package/agent-tools/subagent-spawn.mjs +57 -25
- package/agent-tools/subagent.mjs +46 -66
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +20 -351
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +12 -7
- package/config-watch.mjs +83 -0
- package/config.mjs +17 -4
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/git/checkpoint.mjs +4 -8
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +35 -7
- package/ledger-migrate.mjs +299 -0
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +11 -5
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-specs.mjs +131 -66
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +263 -0
- package/peer-domains.mjs +100 -71
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +19 -7
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +1 -2
- package/prompts/persona-engineering.md +41 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +10 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-gc.mjs +67 -6
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +90 -18
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -236
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/question.md +1 -0
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +10 -11
- package/tools/edit-diff.mjs +33 -25
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +11 -12
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/glob-dialect.mjs +3 -3
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/search.mjs +2 -2
- package/tools/shared.mjs +9 -7
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
package/agent/setup.mjs
CHANGED
|
@@ -10,14 +10,18 @@ import { search as memorySearch, docSearch } from "../memory.mjs"
|
|
|
10
10
|
import { pushReal } from "../context.mjs"
|
|
11
11
|
import { toOpenAISchema } from "../tools/index.mjs"
|
|
12
12
|
import { loadSkills, formatSkillListing } from "../skills.mjs"
|
|
13
|
+
import { scopedRulesBlock } from "../rules.mjs"
|
|
13
14
|
import { assemblePrompt } from "../prompt-overlays.mjs"
|
|
14
15
|
import {
|
|
15
16
|
escapeXml, repairHistory, listWorkDir,
|
|
16
17
|
collectGitContext, loadProjectInstructions, OUTLINE_INJECT_PREFIX,
|
|
17
18
|
DEFAULT_MAX_TURNS, ensureAutoReminder,
|
|
18
19
|
} from "./helpers.mjs"
|
|
19
|
-
import { pushEnvStateReminder, pushPeerReminder } from "./setup-reminders.mjs"
|
|
20
|
+
import { pushEnvStateReminder, pushPeerReminder, pushInjections } from "./setup-reminders.mjs"
|
|
20
21
|
import { assembleFamilyTools } from "./family-tools.mjs"
|
|
22
|
+
import { normalizeOrigin } from "../memory/origin.mjs"
|
|
23
|
+
import { searchOrigins } from "../memory/code-search.mjs"
|
|
24
|
+
import { INDEX_ORIGIN_ROW_WARN } from "../memory/schema.mjs"
|
|
21
25
|
|
|
22
26
|
const DEFAULT_COMPACT_THRESHOLD = 100_000
|
|
23
27
|
const DOC_SEARCH_LIMIT = 5
|
|
@@ -41,6 +45,10 @@ const MEMORY_SEARCH_LIMIT = 3
|
|
|
41
45
|
*/
|
|
42
46
|
export async function prepareRun(agent, input, callbacks, {
|
|
43
47
|
depth = 0, signal, overrideTurns, resume, extraTools = null,
|
|
48
|
+
// P4-I(批次档 §2.5 A1 + 父侧 2026-09-29 裁定①):端壳装配缝——注入串 ∕ 工具装饰;
|
|
49
|
+
// 两项缺省(null ∕ undefined)⇒ 现行行为逐字零变(CLI ∕ desktop 不传即零变)。
|
|
50
|
+
// (A2 提示尾块载位已退役——B7 3a:核自持 `.cursor/rules` 尾块,见下方 depth-0 块。)
|
|
51
|
+
injections = null, toolDecorate = undefined,
|
|
44
52
|
} = {}) {
|
|
45
53
|
const maxTurns = overrideTurns ?? agent.config?.agent?.maxTurns ?? DEFAULT_MAX_TURNS
|
|
46
54
|
const threshold = agent.config?.agent?.compactThreshold ?? DEFAULT_COMPACT_THRESHOLD
|
|
@@ -91,7 +99,8 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
91
99
|
if (agent.memory && !agent.history.some((m) => typeof m.content === "string" && m.content.startsWith(OUTLINE_INJECT_PREFIX))) {
|
|
92
100
|
try {
|
|
93
101
|
const { buildSummary } = await import("../tools/repomap.mjs")
|
|
94
|
-
|
|
102
|
+
// §6.14 B4:origin 限定 + 文件数上界(越界 ⇒ 提示行 + 跳过大纲构建——见 repomap.mjs)
|
|
103
|
+
const summary = await buildSummary(agent.memory.db, agent.cwd, { origin: normalizeOrigin(agent.memory.codeOrigin) })
|
|
95
104
|
if (summary && !summary.startsWith("(no indexed")) {
|
|
96
105
|
agent.history.push({ role: "user", content: `${OUTLINE_INJECT_PREFIX}\n${summary}]`, transient: true })
|
|
97
106
|
}
|
|
@@ -103,8 +112,16 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
103
112
|
if (agent.memory && depth === 0) {
|
|
104
113
|
const docs = await docSearch(agent.memory, input, { limit: DOC_SEARCH_LIMIT })
|
|
105
114
|
if (docs.length > 0) {
|
|
106
|
-
|
|
107
|
-
const
|
|
115
|
+
// §6.14 B3 + §6.15:origin 集限定 + 有界计数(子查询 LIMIT = WARN+1 形——越界回「20000+」;跨 origin 全扫不再回升)
|
|
116
|
+
const origins = searchOrigins(agent.memory)
|
|
117
|
+
const bound = INDEX_ORIGIN_ROW_WARN + 1
|
|
118
|
+
const countFilter = origins.length === 1 ? " WHERE origin = ?" : origins.length > 1 ? ` WHERE origin IN (${origins.map(() => "?").join(", ")})` : ""
|
|
119
|
+
const countRow = agent.memory.db.prepare(
|
|
120
|
+
`SELECT COUNT(*) AS n FROM (SELECT 1 FROM doc_chunks${countFilter} LIMIT ${bound})`,
|
|
121
|
+
).get(...origins)
|
|
122
|
+
const count = Number(countRow?.n ?? 0)
|
|
123
|
+
const countLabel = count >= bound ? `${INDEX_ORIGIN_ROW_WARN}+` : String(count)
|
|
124
|
+
const more = count > docs.length ? ` (${countLabel} chunks indexed total — call doc_search if you need more)` : ""
|
|
108
125
|
agent.history.push({
|
|
109
126
|
role: "user",
|
|
110
127
|
content:
|
|
@@ -137,6 +154,9 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
137
154
|
pushEnvStateReminder(agent)
|
|
138
155
|
await pushPeerReminder(agent)
|
|
139
156
|
}
|
|
157
|
+
// A1(§2.5——端壳编辑器上下文注入,序位 = env/peer 之后、time 之前):端采集内容经
|
|
158
|
+
// `pushInjections` 单点落机器线(transient + 同文去重);缺省 null ⇒ 零行为。
|
|
159
|
+
pushInjections(agent.history, injections)
|
|
140
160
|
|
|
141
161
|
// Time grounding for EVERY agent depth AND every resume, pushed LAST (after the user
|
|
142
162
|
// input): transient on the HUMAN line — dropped on persist; on the MACHINE line — kept
|
|
@@ -165,6 +185,9 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
165
185
|
engineering: agent.config?.agent?.engineering === true,
|
|
166
186
|
consultModels: agent.config?.agent?.consultModels ?? [],
|
|
167
187
|
batchDoc: agent._batchDoc ?? null,
|
|
188
|
+
// 裁定①(P4-I · 父侧 2026-09-29):端壳装饰体透传(`assembleFamilyTools` 既有 `decorate`
|
|
189
|
+
// 参数面——缺省 undefined ⇒ 核默认形态逐字零变,CLI ∕ desktop 不传即零变)。
|
|
190
|
+
decorate: toolDecorate,
|
|
168
191
|
})
|
|
169
192
|
// 两段式:绑定值(基础集)→ 家族段 → caller 注入面(§2.3F 不相交义务在注入方)。
|
|
170
193
|
const tools = [...agent.tools, ...familyTools, ...(Array.isArray(extraTools) ? extraTools : [])]
|
|
@@ -232,6 +255,11 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
232
255
|
const skills = await loadSkills(agent.cwd)
|
|
233
256
|
const listing = formatSkillListing(skills)
|
|
234
257
|
if (listing) systemPrompt += `\n\n${listing}`
|
|
258
|
+
// B7 3a:`.cursor/rules` 常驻集尾块(原端壳 A2 载位退役——核自持;序位 = skills 清单之后)。
|
|
259
|
+
// 尾块 ∕ JIT 两缝同门 `depth === 0`(子代理不携——B1 §2.5-A2 在册态);同点缓存 `agent._rules`
|
|
260
|
+
// (JIT 判定只读该缓存——`loadScopedRules` 本 run 唯一一次目录读取)。
|
|
261
|
+
const scopedBlock = scopedRulesBlock(agent, agent.cwd)
|
|
262
|
+
if (scopedBlock) systemPrompt += scopedBlock
|
|
235
263
|
}
|
|
236
264
|
|
|
237
265
|
ensureAutoReminder(agent)
|
package/agent/spawn-child.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* spawn-child.mjs — 生成侧统一子代理管线(AGENT-LOOP.md §7.2
|
|
2
|
+
* spawn-child.mjs — 生成侧统一子代理管线(AGENT-LOOP-SUBAGENT.md §6.7.2)。
|
|
3
3
|
*
|
|
4
4
|
* 收编 subagent/escalate/consult 三份机械同构的重复实现:
|
|
5
5
|
* - allocRelay / emitRelayModel: relay 取号段 ∥ 出生声明(`[model]`)发射段——两段分离
|
|
@@ -20,8 +20,8 @@ import { specForModel } from "../config.mjs"
|
|
|
20
20
|
import { ContinueError } from "../agent.mjs"
|
|
21
21
|
import { relayPrefixOf } from "./relay-prefix.mjs"
|
|
22
22
|
import { appendCappedText } from "../text-budget.mjs"
|
|
23
|
-
// 第 27
|
|
24
|
-
// 生成侧枢纽再导出(TUI/ACP
|
|
23
|
+
// 第 27 批:relay 前缀文法单一权威模块(`src/agent/relay-prefix.mjs`)——
|
|
24
|
+
// 生成侧枢纽再导出(TUI/ACP 消费方可经此导入;消费方直连模块亦可)。
|
|
25
25
|
export { RELAY_PREFIX_RE, parseRelayPath, relayPrefixOf } from "./relay-prefix.mjs"
|
|
26
26
|
|
|
27
27
|
/** 事件 token 哨兵串(D1)——LLM 正常内容混淆概率极低;字段分隔用 RS (\x1e)。 */
|
|
@@ -35,7 +35,7 @@ const RS = "\x1e"
|
|
|
35
35
|
export { TURN_CAP_MARK, STOPPED_MARK } from "./child-marks.mjs"
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* D-E3 工程子代理内部 spawn 机械门(AGENT-LOOP-SUBAGENT.md §6.7.6 后备 +
|
|
39
39
|
* ENGINEERING-MODE.md §2.15 D):eng-coder(偏差审计)与 **eng-designer(自己勘察)**
|
|
40
40
|
* 同为受限通道——depth>0 且父角色 ∈ 集合时,内部 spawn 只允许 role='explore'、
|
|
41
41
|
* async 强制同步。
|
|
@@ -109,7 +109,7 @@ export function makeRelay(parent, label, emit, model) {
|
|
|
109
109
|
// Single source for the event grammar branch lists (consult P3, 2026-08-30):
|
|
110
110
|
// stripEventToken (display) and stripEventTokensForCapture (capture) shared them
|
|
111
111
|
// literally — extending the event set meant touching both regexes.
|
|
112
|
-
// "done" = §
|
|
112
|
+
// "done" = AGENT-LOOP-SUBAGENT.md §6.7.3 D-A3 async-child completion event (emitted by the parent's
|
|
113
113
|
// turn-end collection, not by children — listed so the grammar stays honest).
|
|
114
114
|
const EVENT_PHASE = "turn|approval|done"
|
|
115
115
|
const EVENT_TYPE = "llm|tool|approval|done"
|
|
@@ -139,7 +139,7 @@ export function stripEventTokensForCapture(text) {
|
|
|
139
139
|
* - onToolOutput 带**已加前缀**的 name 走父 onToolOutput(name 形如
|
|
140
140
|
* "coder#1/bash",消费端剥前缀路由进对应区块;chunk 对象/裸串原样透传)。
|
|
141
141
|
* 父回调缺省时不包装(headless 嵌入)。
|
|
142
|
-
* §
|
|
142
|
+
* docs/cli/design/TUI.md §6.8.2 R23 D-R23c1:包装产物在 onToken 上留 `_relayPrefix` 标记——同步收尾段据此
|
|
143
143
|
* 判定"ctx.callbacks 已是嵌套 wrapper"(本 spawn 处于更深一层——eng-coder 内
|
|
144
144
|
* explore)→ emitNestedChildEvent 补发射内层完成事件(T-R23c.2b 断言源)。
|
|
145
145
|
*/
|
|
@@ -163,7 +163,7 @@ export function wrapChildCallbacks(relayPrefix, parentCallbacks = {}) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
* §
|
|
166
|
+
* §6.8.2 R23 D-R23c1(生成侧补发射——评审 #1 🅰):sync spawn 同步收尾时若父回调已是
|
|
167
167
|
* 嵌套 wrapper(onToken 带 `_relayPrefix` 标记——即本 spawn 的父本身是子代理,如
|
|
168
168
|
* eng-coder 内 explore 审计)→ 发内层 done/stopped 事件(带完整嵌套前缀——wrapper
|
|
169
169
|
* 链自动补外层前缀)→ 主 TUI routeSubToken 路由到子块定格。非嵌套(depth-0 直连主
|
|
@@ -178,7 +178,7 @@ export function emitNestedChildEvent(ctx, relayPrefix, kind) {
|
|
|
178
178
|
return true
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
/** 捕获额度(TUI-OOM-ROOTCAUSE——AGENT-LOOP.md §
|
|
181
|
+
/** 捕获额度(TUI-OOM-ROOTCAUSE——AGENT-LOOP.md §6.15:滞后水位截断):
|
|
182
182
|
* `_capturedOutput` 是子代理流式文本的第二份全量拷贝(原无上限——勘察 C2);超 hard 即
|
|
183
183
|
* 裁至头 16K + 标记 + 尾 48K(摊还 O(1))——消费面读时已各自 slice(0, 2000/4000),头尾
|
|
184
184
|
* 保真覆盖;停止报告内联场景出现截断标记(可断言——D-SM1)。纯函数本体住
|
|
@@ -235,7 +235,7 @@ export async function runWithContinue(runner, child, input, callbacks, runOpts,
|
|
|
235
235
|
// Review #4 fix: strip sentinel/control chars from the capture — `output` feeds
|
|
236
236
|
// onDeclined's partial-output return, which lands in the PARENT LLM history where
|
|
237
237
|
// the display-layer sanitizeDisplay backstop does not apply.
|
|
238
|
-
// TUI-OOM-ROOTCAUSE
|
|
238
|
+
// TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §6.15):捕获滞后水位截断——超 CAPTURE_CAP_OPTS.hard 裁至
|
|
239
239
|
// 头 + 标记 + 尾(_capturedOutput 有界;续跑同闭包累积,语义一致)。
|
|
240
240
|
const capture = callbacks?.onToken
|
|
241
241
|
? (t) => { output = appendCappedText(output, stripEventTokensForCapture(String(t)), CAPTURE_CAP_OPTS); child._capturedOutput = output; callbacks.onToken(t) }
|
package/agent/suspension.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* agent/suspension.mjs —
|
|
2
|
+
* agent/suspension.mjs — 挂起 / 唤醒驱动(核内形态——AGENT-LOOP.md §2.3 #184 定案;机制面 = AGENT-LOOP-ASYNC-POOL.md §6.8)。
|
|
3
3
|
*
|
|
4
4
|
* 语义(两端同源,逐字承两端现行挂起驱动——CLI 侧 tui 面 / VSC 侧 extension 面,同语义移植):
|
|
5
5
|
* 回合尾后台池仍 live → 挂起会话——池项 settle → 入 pending;pending 非空 **或存在未 drain 的
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 载体注入(端差面 = 池 / pending / 标志挂谁):`ctx.carrier` = 字段集按核内异步面
|
|
11
11
|
* 现行口径的对象(`_asyncSubagents` · `_asyncAdvisors` · `_consultSessions` ·
|
|
12
|
-
* `_pendingAsyncResults` · `_suspended
|
|
12
|
+
* `_pendingAsyncResults` · `_suspended`;#9 后台 bash 任务池 `_bgTasks` 同列)
|
|
13
|
+
* ——CLI 形 = `agent` 对象;VSC 形 = depth-0
|
|
13
14
|
* `history` 数组(附加属性不污染会话文件)。机制对载体零预设(空字段一律 `?.` 读)。
|
|
14
15
|
*
|
|
15
16
|
* 核内零文案、零渲染、零端名分支(契约 5 / 10):状态行文本 / 提示行 / 消息族全在宿主侧,
|
|
@@ -18,7 +19,13 @@
|
|
|
18
19
|
* ctx 接口(注入面):
|
|
19
20
|
* - carrier 池 / pending / 标志载体(见上)
|
|
20
21
|
* - runTurn(text, opts) 回合执行器(digest = `("", { autoTurn: true })`;上行唤醒轮 =
|
|
21
|
-
* `("", { autoTurn: true, upstreamTurn: true })
|
|
22
|
+
* `("", { autoTurn: true, upstreamTurn: true })`;timer 轮 =
|
|
23
|
+
* `("", { autoTurn: true, timerTurn: true })`——旗标仅供域文本选择)
|
|
24
|
+
* - inputQueue **opt-in** 宿主既有输入队列数组(增补 C):驱动就地消费该数组——宿主
|
|
25
|
+
* 其余入队 ∕ 读取面与驱动同一数组(零同步面);缺省核内新建(既有宿主零变)
|
|
26
|
+
* - takeInput(queue) **opt-in** 取项缝(增补 C):批次策略(富条目 / 合并批 / 消费回执行)
|
|
27
|
+
* 归宿主;缺省 `shift`;返回 null/undefined ⇒ 本步零动作、落第 2 步
|
|
28
|
+
* (首动作不可消费的防御面——条目不消费留队)。可异步(宿主取项含动态 import)
|
|
22
29
|
* - abortSignal 会话中止信号(兜底监听 + abort 清场判据)
|
|
23
30
|
* - hooks.onCounts(counts) 计数变化通知({ running, queued, pending, done })
|
|
24
31
|
* - hooks.onDigest(phase, counts) 消化轮边界(start / end)
|
|
@@ -26,6 +33,11 @@
|
|
|
26
33
|
* - hooks.freezeAll() 退出冻结(兜底残项)
|
|
27
34
|
* - injectResidual(entry) 残余注入面(idle 退出清场;宿主装配端注入器——CLI =
|
|
28
35
|
* 逐族注入分发 / VSC = pending 单容器注入器)
|
|
36
|
+
* - timerFace **opt-in** timer 面(§6.30.10):`{ deadline(), deliver() }`——`deadline()`
|
|
37
|
+
* 每轮步骤 4 现算(number|null;缺省 / null ⇒ 零注册,等待仍三态);
|
|
38
|
+
* `deliver()` 到期兑现面(返回布尔 = 是否已交付;交付真 ⇒ timer 轮)。
|
|
39
|
+
* 缺省不传 ⇒ 既有行为逐字等价(现宿主零传)。
|
|
40
|
+
* - timer / clear 等待原语注入缝(缺省 `setTimeout` / `clearTimeout`——用例零真实等待)
|
|
29
41
|
*/
|
|
30
42
|
|
|
31
43
|
import { parkAsyncPending } from "../agent-tools/async-settle.mjs"
|
|
@@ -42,11 +54,13 @@ function consultRunningChildren(carrier) {
|
|
|
42
54
|
|
|
43
55
|
/** 后台池存活判据(D-S2/F5 口径——CLI 为准):running/queued 的池条目,或已 settle 未注入
|
|
44
56
|
* (pending 非空 = D-S3「未注入」),或运行中 consult children(会诊跨回合)。回合尾与每次
|
|
45
|
-
*
|
|
57
|
+
* 轮末都评估退出。**#9 三域并入**:`_bgTasks`(后台 bash 任务)在途亦为 live——settle 经
|
|
58
|
+
* pending 停靠驱动消化轮(`bash-async.mjs` 恒停靠)。 */
|
|
46
59
|
export function poolLive(carrier) {
|
|
47
60
|
const sub = carrier?._asyncSubagents
|
|
48
61
|
const adv = carrier?._asyncAdvisors
|
|
49
|
-
|
|
62
|
+
const bg = carrier?._bgTasks
|
|
63
|
+
return (sub && sub.size > 0) || (adv && adv.size > 0) || (bg && bg.size > 0)
|
|
50
64
|
|| (carrier?._pendingAsyncResults?.length ?? 0) > 0
|
|
51
65
|
|| consultRunningChildren(carrier) > 0
|
|
52
66
|
}
|
|
@@ -94,13 +108,17 @@ function consultLiveCount(carrier) {
|
|
|
94
108
|
|
|
95
109
|
/**
|
|
96
110
|
* 退出清场(单点——驱动 finally 与单测共用):
|
|
97
|
-
* - abort
|
|
111
|
+
* - abort:**只清已死**(§6.20 口径——增补 D:`discardAbortedPool` ∕ `discardAbortedAdvisors`
|
|
112
|
+
* 单点,与核 run-stages 回合尾中止同式;存活 ∕ 已 settle 条目留池——settled 报告沿后续
|
|
113
|
+
* 回合边界注入到达,不静默丢)+ pending 单容器清 + 会诊会话清理标记;
|
|
98
114
|
* - idle:残余直注入(极端竞态残项——结果零丢失;注入器由宿主注入,缺省 no-op 保残项)。
|
|
99
115
|
*/
|
|
100
116
|
export async function finishSuspension(carrier, { aborted = false, injectResidual = null } = {}) {
|
|
101
117
|
if (aborted) {
|
|
102
|
-
|
|
103
|
-
carrier
|
|
118
|
+
const { discardAbortedPool, discardAbortedAdvisors, discardAbortedBgTasks } = await import("../agent-tools/async-discard.mjs")
|
|
119
|
+
discardAbortedPool(carrier)
|
|
120
|
+
discardAbortedAdvisors(carrier)
|
|
121
|
+
discardAbortedBgTasks(carrier) // #9 收尾档②:后台 bash 任务逐条杀树 + 出池 + 墓碑(杀 ⟺ 控制器已中止)
|
|
104
122
|
carrier._pendingAsyncResults = []
|
|
105
123
|
if (carrier._consultSessions instanceof Map) {
|
|
106
124
|
const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
|
|
@@ -114,13 +132,19 @@ export async function finishSuspension(carrier, { aborted = false, injectResidua
|
|
|
114
132
|
}
|
|
115
133
|
}
|
|
116
134
|
|
|
117
|
-
/** 等待下一次 settle(池 waiter——核内异步面 settle
|
|
118
|
-
*
|
|
119
|
-
|
|
135
|
+
/** 等待下一次 settle(池 waiter——核内异步面 settle 尾部唤醒)/ 宿主唤醒(wake——用户输入)/
|
|
136
|
+
* **timer 到期**(opt-in——§6.30.10 等待第四态,与 settle / wake 同槽先到先得)。
|
|
137
|
+
* `deadline != null` ⇒ 一次性注册(`unref()` 尽力——不阻断宿主退出);缺省 / null ⇒ 三态逐字等价。
|
|
138
|
+
* cleanup 摘除全部注册(含未触发句柄——尽力清);到期兑现交循环支(本原语零交付语义)。 */
|
|
139
|
+
function waitForSettleOrWake(carrier, abortSignal, latch, { deadline = null, timer = setTimeout, clear = clearTimeout } = {}) {
|
|
120
140
|
return new Promise((resolve) => {
|
|
121
141
|
let finished = false
|
|
142
|
+
let handle = null
|
|
122
143
|
const cleanup = () => {
|
|
123
144
|
latch.wake = null
|
|
145
|
+
const h = handle
|
|
146
|
+
handle = null
|
|
147
|
+
if (h !== null) { try { clear(h) } catch { /* 已触发 / 不可清——尽力面 */ } }
|
|
124
148
|
const i = (carrier._asyncWaiters ?? []).indexOf(onSettle)
|
|
125
149
|
if (i >= 0) carrier._asyncWaiters.splice(i, 1)
|
|
126
150
|
abortSignal?.removeEventListener("abort", onAbort)
|
|
@@ -133,8 +157,14 @@ function waitForSettleOrWake(carrier, abortSignal, latch) {
|
|
|
133
157
|
}
|
|
134
158
|
const onSettle = () => finish("settle")
|
|
135
159
|
const onAbort = () => finish("aborted")
|
|
160
|
+
const onTimer = () => finish("timer")
|
|
136
161
|
;(carrier._asyncWaiters ??= []).push(onSettle)
|
|
137
162
|
latch.wake = () => finish("wake")
|
|
163
|
+
// opt-in deadline(§6.30.10 等待第四态):无在途 / 未传 ⇒ 零注册(等待仍三态)。宿主不变式 = 到期即出列 / 关即 null(否则「已到期 ∧ 交付假」会 0ms 反复重注册)。
|
|
164
|
+
if (deadline != null) {
|
|
165
|
+
handle = timer(onTimer, Math.max(0, deadline - Date.now()))
|
|
166
|
+
try { handle?.unref?.() } catch { /* unref 失败不阻断 */ }
|
|
167
|
+
}
|
|
138
168
|
if (abortSignal?.aborted) { onAbort(); return }
|
|
139
169
|
abortSignal?.addEventListener("abort", onAbort, { once: true })
|
|
140
170
|
})
|
|
@@ -143,17 +173,23 @@ function waitForSettleOrWake(carrier, abortSignal, latch) {
|
|
|
143
173
|
/**
|
|
144
174
|
* 启动挂起会话(同步返回句柄;宿主 `await handle.done` ⇒ `{ reason, residualInput }`)。
|
|
145
175
|
*
|
|
146
|
-
* 状态机行表(承 AGENT-LOOP.md §
|
|
176
|
+
* 状态机行表(承 AGENT-LOOP-ASYNC-POOL.md §6.8 行表):
|
|
147
177
|
* 1. 用户输入优先(D-S5):pendingInput 非空 → 以该消息开普通回合(`_suspended=false`);
|
|
148
178
|
* 2. pending 非空 或 存在未 drain 的 ask(`upstreamWaiting`)→ 合并消化轮 / 唤醒轮(auto-turn;
|
|
149
179
|
* 注入由宿主 runTurn 首行完成——单注入点);
|
|
150
180
|
* 3. 池空(无 running / queued / 未注入)→ 自然退出;
|
|
151
|
-
* 4. 等下一 settle / 宿主唤醒(handle.wake
|
|
181
|
+
* 4. 等下一 settle / 宿主唤醒(handle.wake)/ timer 到期(opt-in——兑现真 ⇒ timer 轮)。
|
|
152
182
|
* 每轮消化 / 用户回合后:hooks.reclaim(consumed) 逐条回收(不等池空)。
|
|
153
183
|
*/
|
|
154
184
|
export function startSuspension(ctx) {
|
|
155
|
-
const {
|
|
156
|
-
|
|
185
|
+
const {
|
|
186
|
+
carrier, runTurn, abortSignal = null, hooks = {}, injectResidual = null,
|
|
187
|
+
timerFace = null, timer = setTimeout, clear = clearTimeout,
|
|
188
|
+
inputQueue = null, takeInput = null,
|
|
189
|
+
} = ctx
|
|
190
|
+
// 输入队列(增补 C):宿主既有数组(VSC ∕ CLI 形——宿主其余读者与驱动同一数组)优先;
|
|
191
|
+
// 缺省核内新建(desktop 形——经 `pushInput` 入槽)。
|
|
192
|
+
const pendingInput = Array.isArray(inputQueue) ? inputQueue : []
|
|
157
193
|
const latch = { wake: null }
|
|
158
194
|
let finished = false
|
|
159
195
|
|
|
@@ -173,19 +209,23 @@ export function startSuspension(ctx) {
|
|
|
173
209
|
while (true) {
|
|
174
210
|
if (abortSignal?.aborted) { aborted = true; break }
|
|
175
211
|
sweepSettledToPending(carrier)
|
|
176
|
-
// 1.
|
|
212
|
+
// 1. 用户输入优先(D-S5):队列非空 → 取项缝(增补 C——缺省 `shift`;富条目 ∕ 合并批
|
|
213
|
+
// 由宿主 `takeInput` 实现)开普通回合(`_suspended=false`);首动作不可消费
|
|
214
|
+
// (`takeInput ⇒ null`)⇒ 本步零动作、条目不消费,落第 2 步。
|
|
177
215
|
if (pendingInput.length > 0) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
216
|
+
const item = takeInput ? await takeInput(pendingInput) : pendingInput.shift()
|
|
217
|
+
if (item != null) {
|
|
218
|
+
const afterRun = consumedByRun()
|
|
219
|
+
carrier._suspended = false // 用户回合 = 普通回合语义(settle 即冻结 + 回合尾直注入)
|
|
220
|
+
try {
|
|
221
|
+
await runTurn(item)
|
|
222
|
+
} finally {
|
|
223
|
+
carrier._suspended = true
|
|
224
|
+
}
|
|
225
|
+
hooks.reclaim?.(afterRun())
|
|
226
|
+
hooks.onCounts?.(backgroundCounts(carrier))
|
|
227
|
+
continue
|
|
185
228
|
}
|
|
186
|
-
hooks.reclaim?.(afterRun())
|
|
187
|
-
hooks.onCounts?.(backgroundCounts(carrier))
|
|
188
|
-
continue
|
|
189
229
|
}
|
|
190
230
|
// 2. pending 非空 或 未 drain 的 ask → 合并消化轮 / 上行唤醒轮(单注入点由 runTurn
|
|
191
231
|
// 首行完成:pending 经 run 起始注入器,ask 经回合头 `drainChildUpstream`)。
|
|
@@ -210,9 +250,28 @@ export function startSuspension(ctx) {
|
|
|
210
250
|
}
|
|
211
251
|
// 3. 池空 → 自然退出。
|
|
212
252
|
if (!poolLive(carrier)) break
|
|
213
|
-
// 4. 等下一 settle /
|
|
214
|
-
const why = await waitForSettleOrWake(carrier, abortSignal, latch
|
|
253
|
+
// 4. 等下一 settle / 宿主唤醒 / timer 到期(opt-in——§6.30.10 等待第四态;deadline 每轮现算)。
|
|
254
|
+
const why = await waitForSettleOrWake(carrier, abortSignal, latch, {
|
|
255
|
+
deadline: timerFace?.deadline?.() ?? null, timer, clear,
|
|
256
|
+
})
|
|
215
257
|
if (why === "aborted") { aborted = true; break }
|
|
258
|
+
// 4b. 窗内到期兑现(§6.30.10 循环兑现支):交付真 ⇒ timer 轮(轮后序同消化轮形 = reclaim +
|
|
259
|
+
// onCounts 两钩,不发 digest 边界);交付面契约 = 同步 · 严格布尔(`=== true`——非布尔返值
|
|
260
|
+
// 即静默零轮);池空窗退交宿主空闲闩(到期件已出列——零重复投递);交付假 ⇒ 循环重入。
|
|
261
|
+
if (why === "timer" && timerFace?.deliver?.() === true) {
|
|
262
|
+
const afterRun = consumedByRun()
|
|
263
|
+
try {
|
|
264
|
+
await runTurn("", { autoTurn: true, timerTurn: true })
|
|
265
|
+
} catch (e) {
|
|
266
|
+
// timer 轮自身的回合级中止不是会话停止——容纳并重入循环(timer 轮不发 digest 边界 ⇒
|
|
267
|
+
// 中止路径零边界、不补发轮后序钩子;池空 / pending 空自然退出——与消化支同判,§6.30.10)。
|
|
268
|
+
if (e?.name === "AbortError" && !abortSignal?.aborted) continue
|
|
269
|
+
throw e
|
|
270
|
+
}
|
|
271
|
+
hooks.reclaim?.(afterRun())
|
|
272
|
+
hooks.onCounts?.(backgroundCounts(carrier))
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
216
275
|
}
|
|
217
276
|
} finally {
|
|
218
277
|
if (!finished) {
|
|
@@ -231,8 +290,9 @@ export function startSuspension(ctx) {
|
|
|
231
290
|
})()
|
|
232
291
|
|
|
233
292
|
return {
|
|
234
|
-
/**
|
|
235
|
-
|
|
293
|
+
/** 挂起空闲期入槽(宿主 busy 期拒收——条数策略归宿主)。**保留原值**(增补 C:富条目
|
|
294
|
+
* ——VSC ∕ CLI 载体对象;消费面 = 宿主 `takeInput`;`String()` 化归宿主需要时自做)。 */
|
|
295
|
+
pushInput(msg) { pendingInput.push(msg) },
|
|
236
296
|
/** 宿主唤醒(用户输入落槽后 / 宿主 settle 回调路径——settle 由池 waiter 另一路唤醒)。 */
|
|
237
297
|
wake() { latch.wake?.() },
|
|
238
298
|
done,
|
package/agent/timers.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/timers.mjs — timer 到期件(载体①:在途 timer 的单一权威读面)+ 空闲唤醒机制(闩 ∕ 派发 ∕ 火策略)。
|
|
3
|
+
*
|
|
4
|
+
* 机制单源 = `docs/core/design/AGENT-LOOP-ASYNC-POOL.md` §6.30.2(载体定形 D-TW1):
|
|
5
|
+
* 读面三件纯函数住本档——最近到期时点 / 到期出列(幂等)/ 历史注入单点。消费面三处共用
|
|
6
|
+
* 同一读点(零第二份过滤语义):① 回合边界轮询(`agent/post-turn.mjs`——行为零变);
|
|
7
|
+
* ② 空闲闩 deadline;③ 挂起窗 deadline;显示面(状态行标记 / `/timers`)同读源。
|
|
8
|
+
*
|
|
9
|
+
* 载体仍在 agent 字段 `_pendingTimers`(条目形 `{ id, expiresAt, message }`)——**唯一写点** =
|
|
10
|
+
* timer 工具 `thincoder-core/agent-tools/timer.mjs`;跨 run 存活 = 规范语义(D-TW3,无 per-run 复位)。
|
|
11
|
+
*
|
|
12
|
+
* 空闲唤醒族(2026-09-28 流程批 R4 **上提核件** · 纯搬 + 转口 · 零语义改;上提源 = VSC
|
|
13
|
+
* `thincoder-vscode/src/extension/timer-watch.mjs:46` ∕ `:73` ∕ `:94`——as-of 2026-09-28 R4 读数;两端现址(B3 收编后,实读 2026-09-29)=
|
|
14
|
+
* VSC `:63`(闩 ∕ 装配)· `:29`(派发)· `:48`(火)∥ CLI `:41`(闩转口)· `:34`(派发转口)· `:57`(火)):`createTimerWatch` = 一次性最近截止闩
|
|
15
|
+
* (起 ∕ 停 ∕ 重臂 ∕ `unref`)· `deliverExpiredTimers` = 到期批派发(出列幂等 ⇒ 注入单点 ⇒ 落流缝)·
|
|
16
|
+
* `fireTimerWake` = 火策略(非空闲零动作 ∕ 零交付零开轮)。端面表壳留端(桌面键面 ∕ `ev:timer` 出词;
|
|
17
|
+
* VSC ∕ CLI 自持副本已随 B3 迁移轮退场(2026-09-29——双写窗口收口),本档 = 单源)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** 在途条数帽(§6.30.3 门三件②成本闸):超限 ⇒ timer 工具**显式拒**(不静默丢 / 不静默清;
|
|
21
|
+
* 形态先例 = 队满「拒 + 提示 + 保留」)。与 CLI 排队容量 `QUEUED_MAX_ITEMS`(8)同值同形。 */
|
|
22
|
+
export const TIMER_MAX_PENDING = 8
|
|
23
|
+
|
|
24
|
+
/** 最近到期时点(毫秒)或 `null`(空在途)——端闩 / 挂起窗 deadline / 显示面共用同源(每帧活读)。 */
|
|
25
|
+
export function pendingTimerDeadline(agent) {
|
|
26
|
+
const timers = agent?._pendingTimers ?? []
|
|
27
|
+
let min = null
|
|
28
|
+
for (const t of timers) {
|
|
29
|
+
const at = t?.expiresAt
|
|
30
|
+
if (typeof at !== "number" || !Number.isFinite(at)) continue
|
|
31
|
+
if (min === null || at < min) min = at
|
|
32
|
+
}
|
|
33
|
+
return min
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 到期出列(**幂等**):取走 `expiresAt <= now` 的条目并返回(在途表内移除)——二次调用零返
|
|
37
|
+
* (§6.30.3 成本闸②「到期即消费」——绝不重复投递)。未到期项原序保留。 */
|
|
38
|
+
export function takeExpiredTimers(agent, now = Date.now()) {
|
|
39
|
+
const timers = agent?._pendingTimers ?? []
|
|
40
|
+
const expired = timers.filter((t) => t.expiresAt <= now)
|
|
41
|
+
if (expired.length === 0) return []
|
|
42
|
+
agent._pendingTimers = timers.filter((t) => t.expiresAt > now)
|
|
43
|
+
return expired
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 历史注入单点(逐字形态 = 既有注入单源):逐条 `[System reminder: ⏰ timer — <message>]` 落历史,
|
|
47
|
+
* 返回注入的原文(显示面复用同一串——零第二份字面)。 */
|
|
48
|
+
export function injectTimerReminders(agent, entries) {
|
|
49
|
+
const lines = []
|
|
50
|
+
for (const t of entries ?? []) {
|
|
51
|
+
const line = `[System reminder: ⏰ timer — ${t.message}]`
|
|
52
|
+
agent.history.push({ role: "user", content: line })
|
|
53
|
+
lines.push(line)
|
|
54
|
+
}
|
|
55
|
+
return lines
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 开关判据(§6.30.3 门三件③ D-TW5 · 上提源 = VSC `timer-watch.mjs:33`——as-of 2026-09-28 R4 读数,B3 收编后该端副本退场)
|
|
59
|
+
* (现转口 = CLI `timer-watch.mjs:18`):默认开——只有显式 `false` 才关
|
|
60
|
+
* (键 `agent.timerWake`;与 CLI 同口径 `!== false`)。关 ⇒ 闩零注册 ⇒ 回仅回合边界投递(既有语义)。 */
|
|
61
|
+
export function timerWakeEnabled(agent) {
|
|
62
|
+
return agent?.config?.agent?.timerWake !== false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 到期批派发(**两路共用**:空闲闩 fire ∥ 挂起窗 `timerFace.deliver` · 上提源 = VSC `timer-watch.mjs:73`
|
|
66
|
+
* 核心体——as-of 2026-09-28 R4 读数;现址 = VSC `:29` · CLI `:34`):出列(幂等——绝不重复投递)→ 注入单点 → 逐条落流缝(`onLine`——端侧 `ev:timer` ∕ webview ∕
|
|
67
|
+
* TUI 行)。返回交付原文(核注入单源逐字;零到期 ∕ 无载体 ⇒ 空数组——在途不动 = 零静默丢)。
|
|
68
|
+
* `now` = 时点值(默认现刻;与 `takeExpiredTimers` 同形——调用方传钟、本档取时点)。 */
|
|
69
|
+
export function deliverExpiredTimers(agent, { now = Date.now(), onLine = null } = {}) {
|
|
70
|
+
const lines = injectTimerReminders(agent, takeExpiredTimers(agent, now))
|
|
71
|
+
for (const line of lines) onLine?.(line)
|
|
72
|
+
return lines
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 一次性最近截止闩(单槽 · 上提源 = VSC `timer-watch.mjs:46` 逐字——as-of 2026-09-28 R4 读数;现址 = VSC `:63`(闩 ∕ 装配)· CLI `:41`(闩转口)):`sync()` = 按当前在途最近到期时点
|
|
76
|
+
* (重)武装——无在途 ∕ 开关关 ∕ 无载体 ⇒ 撤旧零注册;`disarm()` = 撤闩清点。到点自撤 · `unref()`(尽力面)。
|
|
77
|
+
* `getAgent` / `onFire` 缺省宽容沿用源(未接线 = 惰性零注册);`timer` ∕ `clear` ∕ `now` = 注入缝(默认平台全局)。
|
|
78
|
+
* @returns {{ sync: Function, disarm: Function }}——`sync()` ⇒ 武装延迟(毫秒)或 `null`(未武装) */
|
|
79
|
+
export function createTimerWatch({ getAgent, onFire, timer = setTimeout, clear = clearTimeout, now = Date.now } = {}) {
|
|
80
|
+
let handle = null
|
|
81
|
+
const disarm = () => {
|
|
82
|
+
const h = handle
|
|
83
|
+
handle = null
|
|
84
|
+
if (h !== null) { try { clear(h) } catch { /* 已触发 / 不可清——尽力面 */ } }
|
|
85
|
+
}
|
|
86
|
+
const sync = () => {
|
|
87
|
+
const agent = getAgent?.()
|
|
88
|
+
const deadline = timerWakeEnabled(agent) ? pendingTimerDeadline(agent) : null
|
|
89
|
+
if (deadline == null) { disarm(); return null } // 无在途 / 开关关 / 无载体——撤旧,零注册
|
|
90
|
+
disarm() // 单槽:重复武装 = 撤旧立新(延迟按最近到期重算)
|
|
91
|
+
const delay = Math.max(0, deadline - now())
|
|
92
|
+
handle = timer(() => { handle = null; onFire?.() }, delay)
|
|
93
|
+
try { handle?.unref?.() } catch { /* unref 失败不阻断(一次性闩自然退出) */ }
|
|
94
|
+
return delay
|
|
95
|
+
}
|
|
96
|
+
return { sync, disarm }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 空闲唤醒火策略(上提源 = VSC `timer-watch.mjs:94`——as-of 2026-09-28 R4 读数;现址 = VSC `:48` · CLI `:57`;桌面同形):
|
|
100
|
+
* **非空闲零动作**——回合在飞 ∕ 挂起窗
|
|
101
|
+
* 由各自既有路径接管,在途表零触碰(链尾重同步再武装);空闲 ⇒ 派发;交付真 ⇒ 开 timer 轮(开轮面 =
|
|
102
|
+
* `openTurn` 缝,承担 `{ autoTurn: true, timerTurn: true }` 语义)。`deliver` 契约 = 同步 · 严格布尔
|
|
103
|
+
* (判定 `=== true`——同 §6.30.10 `timerFace.deliver`);缝缺 ⇒ 抛(fail-loud——未接线不得静默零开轮)。
|
|
104
|
+
* `busy` = 非空闲判据(宿主现算:在飞回合 ∕ 窗内 ⇒ true);返回是否开轮。 */
|
|
105
|
+
export async function fireTimerWake({ busy = false, deliver, openTurn } = {}) {
|
|
106
|
+
if (typeof deliver !== "function" || typeof openTurn !== "function") {
|
|
107
|
+
throw new Error("[timers] fireTimerWake requires deliver/openTurn seams (unwired seams must not pass silently)")
|
|
108
|
+
}
|
|
109
|
+
if (busy) return false
|
|
110
|
+
if (deliver() !== true) return false
|
|
111
|
+
await openTurn()
|
|
112
|
+
return true
|
|
113
|
+
}
|