@bolloon/bolloon-agent 0.6.0 → 0.6.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/dist/agents/execution-entry.js +10 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +5 -1
- package/dist/agents/pi-sdk-tools.js +18 -8
- package/dist/agents/pi-sdk.js +472 -1263
- package/dist/agents/runner-resolver.js +2 -1
- package/dist/agents/tool-capability/index.js +51 -0
- package/dist/agents/workflow-pivot-loop.js +31 -10
- package/dist/agents/write-verify.js +60 -0
- package/dist/bollharness-integration/integration.js +8 -0
- package/dist/bollharness-integration/skill-adapter.js +32 -3
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +5 -1
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +21 -11
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +473 -1262
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/runner-resolver.js +2 -1
- package/dist/electron-build/agents/runner-resolver.js.map +1 -1
- package/dist/electron-build/agents/tool-capability/index.js +90 -0
- package/dist/electron-build/agents/tool-capability/index.js.map +1 -0
- package/dist/electron-build/agents/workflow-pivot-loop.js +31 -10
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/agents/write-verify.js +99 -0
- package/dist/electron-build/agents/write-verify.js.map +1 -0
- package/dist/electron-build/bollharness-integration/integration.js +8 -0
- package/dist/electron-build/bollharness-integration/integration.js.map +1 -1
- package/dist/electron-build/bollharness-integration/skill-adapter.js +32 -3
- package/dist/electron-build/bollharness-integration/skill-adapter.js.map +1 -1
- package/dist/electron-build/kernel/channel-actor.js +25 -0
- package/dist/electron-build/kernel/channel-actor.js.map +1 -1
- package/dist/electron-build/kernel/control.js +102 -0
- package/dist/electron-build/kernel/control.js.map +1 -0
- package/dist/electron-build/kernel/model-runtime.js +508 -0
- package/dist/electron-build/kernel/model-runtime.js.map +1 -0
- package/dist/electron-build/kernel/run-lifecycle.js +146 -0
- package/dist/electron-build/kernel/run-lifecycle.js.map +1 -0
- package/dist/electron-build/kernel/transport.js +119 -0
- package/dist/electron-build/kernel/transport.js.map +1 -0
- package/dist/electron-build/llm/native-adapter.js +122 -0
- package/dist/electron-build/llm/native-adapter.js.map +1 -0
- package/dist/index.js +19 -0
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/client.js +13 -7
- package/dist/ios/mobile-core.js +7762 -6289
- package/dist/ios/mobile.js +64 -0
- package/dist/ios/routes-mobile-tasks.js +2 -1
- package/dist/ios/routes-tasks.js +16 -1
- package/dist/ios/routes-x402-info.js +28 -1
- package/dist/ios/server-storage.js +26 -5
- package/dist/ios/server.js +840 -862
- package/dist/ios/sw.js +1 -1
- package/dist/ios/ui/message-renderer.js +473 -29
- package/dist/ios/ui/step-timeline.js +13 -12
- package/dist/kernel/channel-actor.js +24 -0
- package/dist/kernel/gate-scan.js +318 -4
- package/dist/kernel/model-runtime.js +22 -0
- package/dist/kernel/plan-channel-actor.js +12 -5
- package/dist/kernel/plan-communication.js +163 -0
- package/dist/kernel/plan-constraint.js +2 -15
- package/dist/kernel/plan-deletion.js +1 -1
- package/dist/kernel/plan-harness.js +78 -0
- package/dist/kernel/plan.js +1 -5
- package/dist/kernel/roster.js +13 -11
- package/dist/kernel/run-lifecycle.js +138 -0
- package/dist/kernel/transport.js +111 -0
- package/dist/llm/native-adapter.js +117 -0
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +9 -5
- package/dist/web/routes-tasks.js +12 -1
- package/dist/web/server.js +802 -866
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
|
@@ -215,6 +215,30 @@ export function peekActor(actorKey) {
|
|
|
215
215
|
export function actorCount() {
|
|
216
216
|
return actors.size;
|
|
217
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* K8 收尾: **按 channel 中止** —— 给 UI 的终止按钮用 (`POST /api/chat/abort`)。
|
|
220
|
+
*
|
|
221
|
+
* 为什么是"扫描"而不是"按 channel 分桶": 注册表的键是**会话身份** (channel 前缀分桶曾被全量回归打红 ——
|
|
222
|
+
* 同 channel 下不同会话会互相看见 history)。终止只是**只读查找 + 动作**, 不改归属粒度 ⇒ 扫 `state.channelId` 即可。
|
|
223
|
+
*
|
|
224
|
+
* 返回**真的被中止了几个** —— 调用方据此回 `aborted: true/false`;
|
|
225
|
+
* 不许把"没找到 / 没在跑"说成"已中止" (那是谎报)。
|
|
226
|
+
*/
|
|
227
|
+
export function abortActorsOfChannel(channelId, reason = 'aborted') {
|
|
228
|
+
const cid = String(channelId || '');
|
|
229
|
+
if (!cid)
|
|
230
|
+
return 0;
|
|
231
|
+
let aborted = 0;
|
|
232
|
+
for (const actor of actors.values()) {
|
|
233
|
+
if (actor.state.channelId !== cid)
|
|
234
|
+
continue;
|
|
235
|
+
if (!actor.state.cancellation)
|
|
236
|
+
continue; // 没在跑 ⇒ 不算中止 (不强造一个 controller)
|
|
237
|
+
actor.abort(reason);
|
|
238
|
+
aborted += 1;
|
|
239
|
+
}
|
|
240
|
+
return aborted;
|
|
241
|
+
}
|
|
218
242
|
/** 清空注册表 —— **仅测试用** (避免测试之间互相污染) */
|
|
219
243
|
export function resetActors() {
|
|
220
244
|
actors.clear();
|
package/dist/kernel/gate-scan.js
CHANGED
|
@@ -493,8 +493,10 @@ export function scanRunIdSeed(files, opts) {
|
|
|
493
493
|
if ((code.match(/this\.runCtx = this\.seedRunContext\(\)/g) || []).length > 0) {
|
|
494
494
|
out.push({ rule: 'runid-seed-on-reset', file: 'agents/pi-sdk.ts', line: 1, what: '复位点带了播种 (清空不许携带身份)' });
|
|
495
495
|
}
|
|
496
|
-
|
|
497
|
-
|
|
496
|
+
// K4-B (2026-10-02): 阈值 5 → 3 —— 原先是**两条 loop** (老 `runReActLoop` 内另有 2 处 `createRunContext()`);
|
|
497
|
+
// 老 loop 删除后只剩 pivot 这条路的复位点。不变量不变: **每条退出路径都要复位** (用完即清)。
|
|
498
|
+
if (resets < 3)
|
|
499
|
+
out.push({ rule: 'runid-reset-missing', file: 'agents/pi-sdk.ts', line: 1, what: `复位点只剩 ${resets} 处 (期望 ≥3, K4-B 后只剩一条 loop)` });
|
|
498
500
|
return out;
|
|
499
501
|
}
|
|
500
502
|
/**
|
|
@@ -698,7 +700,9 @@ export function countEntryExecutionPoints(code, opts = {}) {
|
|
|
698
700
|
for (const m of methods) {
|
|
699
701
|
// `[!?]?` 允许非空断言 / 可选调用: `session.runExecution!(req)` 也是执行点
|
|
700
702
|
// (实测: 不带这一段, 改成请求式的站点会从计数里消失 ⇒ 该文件 total 从 11 掉到 10)
|
|
701
|
-
|
|
703
|
+
// 2026-10-02 (K8): 也认**助手形态** `requireRunExecution(agent)(req)` —— 它是"响亮检查 + 调入口"的等价执行点。
|
|
704
|
+
// 不认它 ⇒ 迁移后的站点从 total 里消失, 而 wired(deliverThroughActor 数) 不变 ⇒ 破坏 `wired ≤ total` 不变量 (门当场拒了)。
|
|
705
|
+
const rx = new RegExp(`\\.\\s*${m}\\s*[!?]?\\s*\\(|require${m[0].toUpperCase()}${m.slice(1)}\\s*\\(`, 'g');
|
|
702
706
|
if (!rx.test(l))
|
|
703
707
|
continue;
|
|
704
708
|
const recv = new RegExp(`([A-Za-z_$][\\w$]*|this)\\s*\\.\\s*${m}\\s*[!?]?\\s*\\(`).exec(l);
|
|
@@ -813,7 +817,8 @@ export function scanExecutionRequest(entrySources, piCode, req) {
|
|
|
813
817
|
const l = raw.replace(/\/\/.*$/, '');
|
|
814
818
|
if (/^\s*\*/.test(l))
|
|
815
819
|
continue;
|
|
816
|
-
|
|
820
|
+
// 2026-10-02 (K8): 也认助手形态 `requireRunExecution(agent)(...)` —— 它是"响亮检查 + 调入口"的等价形状
|
|
821
|
+
converted += (l.match(/runExecution[!?]?\(|requireRunExecution\s*\(/g) ?? []).length;
|
|
817
822
|
}
|
|
818
823
|
}
|
|
819
824
|
if (converted !== req.converted)
|
|
@@ -1034,3 +1039,312 @@ export function scanModelRuntimeFile(code, opts) {
|
|
|
1034
1039
|
f(`capabilitiesDone=${opts.progress.capabilitiesDone} ≠ 清单里 done 的条数 ${done}`);
|
|
1035
1040
|
return out;
|
|
1036
1041
|
}
|
|
1042
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1043
|
+
// K7: Harness 唯一系统调用门 (台账 + 执行点普查)
|
|
1044
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1045
|
+
/** 剥掉块注释 (保留换行, 不破坏行号) 与行注释 —— 判据吃源码文本前必须先剥注释 (K2 教训) */
|
|
1046
|
+
export function stripJsComments(src) {
|
|
1047
|
+
return src
|
|
1048
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
|
|
1049
|
+
.split('\n')
|
|
1050
|
+
.map((l) => l.replace(/\/\/.*$/, ''))
|
|
1051
|
+
.join('\n');
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* 执行点计数 (K7 的唯一口径): 剥注释后数 `\.execute\s*\(` 与 `executeTool` 的**匹配次数**。
|
|
1055
|
+
* 口径必须是**纯函数 + 可重算**, 否则台账与盘上事实会对不上。
|
|
1056
|
+
*/
|
|
1057
|
+
export function countHarnessExecSites(src) {
|
|
1058
|
+
const s = stripJsComments(src);
|
|
1059
|
+
return (s.match(/\.execute\s*\(/g) || []).length + (s.match(/executeTool/g) || []).length;
|
|
1060
|
+
}
|
|
1061
|
+
/** 阶段顺序即契约 (与设计页 §7 第 164 行逐字一致) */
|
|
1062
|
+
export const HARNESS_STAGE_ORDER = [
|
|
1063
|
+
'discover', 'permission', 'policy', 'budget', 'idempotency', 'execute', 'verify', 'evidence', 'event',
|
|
1064
|
+
];
|
|
1065
|
+
const EXEC_KINDS = ['main', 'skill', 'mcp', 'registry', 'bypass', 'homonym', 'port-callback', 'decl', 'import', 'ledger-string'];
|
|
1066
|
+
/**
|
|
1067
|
+
* 再核对整本 K7 台账:
|
|
1068
|
+
* ① 9 阶段顺序/唯一/gate 指向的文件真存在 ② 9 覆盖面 canonical 真存在
|
|
1069
|
+
* ③ 执行点**逐文件重算**并与台账一致, 合计 == progress.execSitesTotal
|
|
1070
|
+
* ④ 旁路候选逐条有替代路径且目标文件存在; 旁路数 == 台账 progress.bypasses == kinds 含 bypass 的条目所声明的数量
|
|
1071
|
+
* ⑤ 拿不到事实 (文件读不出来) ⇒ 拒跑, 不许跳过
|
|
1072
|
+
*/
|
|
1073
|
+
export function scanHarnessLedger(ledger, opts) {
|
|
1074
|
+
const out = [];
|
|
1075
|
+
const f = (what) => out.push({ rule: 'harness-ledger', file: 'kernel/plan-harness.ts', line: 1, what });
|
|
1076
|
+
// ① 阶段
|
|
1077
|
+
if (ledger.stages.length !== 9)
|
|
1078
|
+
f(`阶段数 ${ledger.stages.length} ≠ 9`);
|
|
1079
|
+
const names = ledger.stages.map((s) => s.stage);
|
|
1080
|
+
if (new Set(names).size !== names.length)
|
|
1081
|
+
f('阶段名有重复');
|
|
1082
|
+
if (names.join(',') !== HARNESS_STAGE_ORDER.join(','))
|
|
1083
|
+
f(`阶段顺序不等于契约顺序: ${names.join(' → ')}`);
|
|
1084
|
+
for (const s of ledger.stages) {
|
|
1085
|
+
if (!s.gate?.trim())
|
|
1086
|
+
f(`阶段 ${s.stage} 没写 gate (谁承担它)`);
|
|
1087
|
+
if ((s.why ?? '').trim().length < 6)
|
|
1088
|
+
f(`阶段 ${s.stage} 的 why 太短 (要能说清为什么)`);
|
|
1089
|
+
for (const m of s.gate.match(/[\w./-]+\.ts/g) || []) {
|
|
1090
|
+
if (opts.readFile(m) === null)
|
|
1091
|
+
f(`阶段 ${s.stage} 的 gate 指向的文件不存在: ${m}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
// ② 覆盖面
|
|
1095
|
+
if (ledger.surfaces.length !== 9)
|
|
1096
|
+
f(`覆盖面数 ${ledger.surfaces.length} ≠ 9`);
|
|
1097
|
+
const surf = ledger.surfaces.map((s) => s.surface);
|
|
1098
|
+
if (new Set(surf).size !== surf.length)
|
|
1099
|
+
f('覆盖面名有重复');
|
|
1100
|
+
for (const s of ledger.surfaces) {
|
|
1101
|
+
if (opts.readFile(s.canonical) === null)
|
|
1102
|
+
f(`覆盖面 ${s.surface} 的 canonical 文件不存在: ${s.canonical}`);
|
|
1103
|
+
}
|
|
1104
|
+
// ③ 执行点逐文件重算
|
|
1105
|
+
let total = 0;
|
|
1106
|
+
for (const site of ledger.execSites) {
|
|
1107
|
+
const code = opts.readFile(site.file);
|
|
1108
|
+
if (code === null) {
|
|
1109
|
+
f(`执行点普查里的文件读不出来 ⇒ 拒跑: ${site.file}`);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
const real = countHarnessExecSites(code);
|
|
1113
|
+
if (real !== site.count)
|
|
1114
|
+
f(`${site.file} 执行点 台账=${site.count} 盘上=${real} (增=新旁路未登记 · 减=改了盘没改账)`);
|
|
1115
|
+
for (const k of site.kinds)
|
|
1116
|
+
if (!EXEC_KINDS.includes(k))
|
|
1117
|
+
f(`${site.file} 未知 kind: ${k}`);
|
|
1118
|
+
total += site.count;
|
|
1119
|
+
}
|
|
1120
|
+
if (total !== ledger.progress.execSitesTotal)
|
|
1121
|
+
f(`普查合计 ${total} ≠ progress.execSitesTotal ${ledger.progress.execSitesTotal}`);
|
|
1122
|
+
// ④ 旁路候选
|
|
1123
|
+
// 2026-10-02: 核"**开着**的旁路数"而不是"候选项个数" —— 收敛掉的旁路要留在台账里当记录,
|
|
1124
|
+
// 否则收敛历史随进度条一起消失 (而 progress.bypasses 是棘轮基线: 只许减)。
|
|
1125
|
+
const openBypasses = ledger.bypasses.filter((b) => b.status !== 'converged').length;
|
|
1126
|
+
if (openBypasses !== ledger.progress.bypasses)
|
|
1127
|
+
f(`开着的旁路 ${openBypasses} ≠ progress.bypasses ${ledger.progress.bypasses}`);
|
|
1128
|
+
const bypassKinds = ledger.execSites.filter((s) => s.kinds.includes('bypass')).length;
|
|
1129
|
+
if (bypassKinds === 0 && ledger.bypasses.length > 0)
|
|
1130
|
+
f('登记了旁路候选, 但普查里没有任何条目标 bypass');
|
|
1131
|
+
for (const b of ledger.bypasses) {
|
|
1132
|
+
// 2026-10-02: target 只许是**文件路径**, 指位置靠 `symbol` —— 行号会随任何编辑漂移,
|
|
1133
|
+
// 而旧 clause 要求 `文件:行` 却只核"文件存在", 等于**把一个会过期的东西写进台账而没人核它**。
|
|
1134
|
+
// 现在改核 `symbol` 是否**真出现在该文件里** (防漂, 且比行号更强)。
|
|
1135
|
+
if (!/\.tsx?$/.test(b.target))
|
|
1136
|
+
f(`旁路 target 要写成文件路径 (行号会漂, 不许 file:line): ${b.target}`);
|
|
1137
|
+
if (opts.readFile(b.target) === null)
|
|
1138
|
+
f(`旁路目标文件不存在: ${b.target}`);
|
|
1139
|
+
if (!b.symbol?.trim())
|
|
1140
|
+
f(`旁路 ${b.target} 没写 symbol (指位置要有可在文件里被找到的记号)`);
|
|
1141
|
+
else if (!(opts.readFile(b.target) ?? '').includes(b.symbol))
|
|
1142
|
+
f(`旁路 symbol 在该文件里找不到 ⇒ 台账已过期: ${b.symbol}`);
|
|
1143
|
+
if (b.status === 'converged' && !b.evidence?.trim())
|
|
1144
|
+
f(`旁路 ${b.target} 标了 converged 却没写证据`);
|
|
1145
|
+
if (!b.replacesWith?.trim())
|
|
1146
|
+
f(`旁路 ${b.target} 没写替代路径 (五条件第 ① 条)`);
|
|
1147
|
+
if ((b.why ?? '').trim().length < 6)
|
|
1148
|
+
f(`旁路 ${b.target} 的 why 太短`);
|
|
1149
|
+
}
|
|
1150
|
+
// ⑤ 台账自报与盘上一致: 说"台账已落"就必须真存在
|
|
1151
|
+
if (ledger.progress.stage === 'ledger-landed' && !opts.planFileExists)
|
|
1152
|
+
f('progress 说台账已落, 但 plan-harness.ts 不存在');
|
|
1153
|
+
if (ledger.progress.stage === 'not-started' && opts.planFileExists)
|
|
1154
|
+
f('progress 说未开始, 但台账文件已存在 (半搬状态)');
|
|
1155
|
+
return out;
|
|
1156
|
+
}
|
|
1157
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
1158
|
+
// K8: Communication Runtime 收口 —— 判据 (口径唯一: 剥注释后数 `.promptStream(`/`.prompt(`)
|
|
1159
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
1160
|
+
export const K8_SITE_KINDS = ['direct-prompt', 'via-actor'];
|
|
1161
|
+
/** transport→agent 直连的**唯一口径**: 剥注释后数 `.promptStream(` / `.prompt(` */
|
|
1162
|
+
export function countTransportAgentSites(src) {
|
|
1163
|
+
return (stripJsComments(src).match(/\.promptStream\s*\(|\.prompt\s*\(/g) || []).length;
|
|
1164
|
+
}
|
|
1165
|
+
export function scanCommunicationLedger(ledger, opts) {
|
|
1166
|
+
const out = [];
|
|
1167
|
+
const f = (what) => out.push({ rule: 'k8-communication', file: 'kernel/plan-communication.ts', line: 1, what });
|
|
1168
|
+
// 行号的**自然写法**都要抓: `(668,`/`(668)`/`:668`/`第 668 行`
|
|
1169
|
+
const LINE_REF = /\(\s*\d{2,5}\s*[,)]|:\d{2,5}\b|第\s*\d{2,5}\s*行/;
|
|
1170
|
+
if (ledger.faces.length !== 10)
|
|
1171
|
+
f(`事件面数 ${ledger.faces.length} ≠ 10`);
|
|
1172
|
+
if (new Set(ledger.faces).size !== ledger.faces.length)
|
|
1173
|
+
f('事件面有重复');
|
|
1174
|
+
let total = 0;
|
|
1175
|
+
for (const s of ledger.sites) {
|
|
1176
|
+
const code = opts.readFile(s.file);
|
|
1177
|
+
if (code === null) {
|
|
1178
|
+
f(`直连普查里的文件读不出来 ⇒ 拒跑: ${s.file}`);
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
const real = countTransportAgentSites(code);
|
|
1182
|
+
if (real !== s.count)
|
|
1183
|
+
f(`${s.file} 直连 台账=${s.count} 盘上=${real} (增=新直连未登记 · 减=改了盘没改账)`);
|
|
1184
|
+
if (!K8_SITE_KINDS.includes(s.kind))
|
|
1185
|
+
f(`${s.file} 未知 kind: ${s.kind}`);
|
|
1186
|
+
if (!s.faces?.length)
|
|
1187
|
+
f(`${s.file} 没写属于哪个事件面`);
|
|
1188
|
+
if (s.why && LINE_REF.test(s.why))
|
|
1189
|
+
f(`${s.file} why 里写了行号 (行号会漂 ⇒ 按符号写)`);
|
|
1190
|
+
total += s.count;
|
|
1191
|
+
}
|
|
1192
|
+
if (total !== ledger.progress.directSites)
|
|
1193
|
+
f(`直连合计 ${total} ≠ progress.directSites ${ledger.progress.directSites}`);
|
|
1194
|
+
for (const p of ledger.perChannel) {
|
|
1195
|
+
if (opts.readFile(p.file) === null)
|
|
1196
|
+
f(`各通道自带状态的文件不存在: ${p.file}`);
|
|
1197
|
+
if (p.why && LINE_REF.test(p.why))
|
|
1198
|
+
f(`${p.file} why 里写了行号`);
|
|
1199
|
+
}
|
|
1200
|
+
if (ledger.perChannel.length !== ledger.progress.perChannelStateFiles) {
|
|
1201
|
+
f(`各通道自带状态文件数 ${ledger.perChannel.length} ≠ progress.perChannelStateFiles ${ledger.progress.perChannelStateFiles}`);
|
|
1202
|
+
}
|
|
1203
|
+
return out;
|
|
1204
|
+
}
|
|
1205
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
1206
|
+
// K8 第二步: "各通道自带状态" 台账 —— 判据 (按**符号**核, 不按行号)
|
|
1207
|
+
//
|
|
1208
|
+
// 为什么按符号: 行号会随任何编辑漂 (K7 真发生过: 台账写 4144, 插 35 行后变 4176 而没人发现)。
|
|
1209
|
+
// 为什么每个符号都要真: "看起来该有" 不算 —— 台账里写一个盘上没有的符号 = 台账撒谎。
|
|
1210
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
1211
|
+
export const K8_STATE_SCOPE_NAMES = ['k8-target', 'ui-state', 'domain-pending', 'infra-retry'];
|
|
1212
|
+
/** 数"通道自带状态"的符号数 (总 / 其中属 K8 收口对象的) */
|
|
1213
|
+
export function countChannelStateSymbols(ledger) {
|
|
1214
|
+
let total = 0, target = 0;
|
|
1215
|
+
for (const s of ledger.sites) {
|
|
1216
|
+
for (const sym of s.symbols ?? []) {
|
|
1217
|
+
total += 1;
|
|
1218
|
+
if (sym.scope === 'k8-target')
|
|
1219
|
+
target += 1;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return { total, target };
|
|
1223
|
+
}
|
|
1224
|
+
export function scanChannelStateLedger(ledger, opts) {
|
|
1225
|
+
const out = [];
|
|
1226
|
+
const f = (what) => out.push({ rule: 'k8-channel-state', file: 'kernel/plan-communication.ts', line: 1, what });
|
|
1227
|
+
// 0) 台账文本里不许出现行号 (抓自然写法)
|
|
1228
|
+
const LINE_REF = /\(\s*\d{2,5}\s*[,)]|:\d{2,5}\b|第\s*\d{2,5}\s*行/;
|
|
1229
|
+
for (const s of ledger.sites) {
|
|
1230
|
+
const text = `${s.file} ${s.kind} ${s.why ?? ''} ${(s.symbols ?? []).map((x) => `${x.name} ${x.note ?? ''}`).join(' ')}`;
|
|
1231
|
+
if (LINE_REF.test(text))
|
|
1232
|
+
f(`通道状态台账写了行号 ⇒ 改按符号记: ${s.file}`);
|
|
1233
|
+
}
|
|
1234
|
+
// 1) 逐符号核"真在该文件里" + 逐符号定性必须齐全
|
|
1235
|
+
let total = 0, target = 0;
|
|
1236
|
+
for (const s of ledger.sites) {
|
|
1237
|
+
const src = opts.readFile(s.file);
|
|
1238
|
+
if (src === null) {
|
|
1239
|
+
f(`各通道状态台账里的文件读不出来 ⇒ 拒跑: ${s.file}`);
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
const code = stripJsComments(src);
|
|
1243
|
+
const syms = s.symbols ?? [];
|
|
1244
|
+
if (syms.length === 0 && !/纯存储|无自带|none/i.test(s.why ?? '')) {
|
|
1245
|
+
f(`${s.file} 符号表为空却没说清"无自带状态" (空表要显式声明, 不许静默留白)`);
|
|
1246
|
+
}
|
|
1247
|
+
for (const sym of syms) {
|
|
1248
|
+
total += 1;
|
|
1249
|
+
if (sym.scope === 'k8-target')
|
|
1250
|
+
target += 1;
|
|
1251
|
+
if (!K8_STATE_SCOPE_NAMES.includes(sym.scope)) {
|
|
1252
|
+
f(`${s.file} 的符号 ${sym.name} scope 非法: ${sym.scope} (只能是 ${K8_STATE_SCOPE_NAMES.join(' / ')})`);
|
|
1253
|
+
}
|
|
1254
|
+
// 范围外符号**必须**写明为什么不算 —— 不许拿"范围外"当静默豁免
|
|
1255
|
+
if (sym.scope !== 'k8-target' && (sym.note ?? '').trim().length < 6) {
|
|
1256
|
+
f(`${s.file} 的 ${sym.name} 标了 scope=${sym.scope} 却没写明理由 ⇒ 不许静默豁免`);
|
|
1257
|
+
}
|
|
1258
|
+
const re = new RegExp(`(^|[^\\w$])${sym.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\w$]|$)`);
|
|
1259
|
+
if (!re.test(code))
|
|
1260
|
+
f(`${s.file} 的通道状态符号盘上找不到: ${sym.name} (台账=有 · 盘上=无)`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
// 2) 自洽 + 棘轮
|
|
1264
|
+
if (ledger.sites.length !== ledger.progress.perChannelStateFiles) {
|
|
1265
|
+
f(`各通道状态文件数 台账=${ledger.sites.length} progress=${ledger.progress.perChannelStateFiles}`);
|
|
1266
|
+
}
|
|
1267
|
+
const totalBudget = ledger.progress.perChannelStateSymbols;
|
|
1268
|
+
if (typeof totalBudget === 'number' && total !== totalBudget) {
|
|
1269
|
+
f(`通道状态符号总数 台账=${total} progress=${totalBudget} (增了要登记, 减了要同步)`);
|
|
1270
|
+
}
|
|
1271
|
+
const targetBudget = ledger.progress.k8TargetSymbols;
|
|
1272
|
+
if (typeof targetBudget === 'number' && target > targetBudget) {
|
|
1273
|
+
f(`K8 收口对象符号数超棘轮: 盘上=${target} > 预算=${targetBudget} (新增通道自带 outbound/重试/恢复必须走 router/mailbox)`);
|
|
1274
|
+
}
|
|
1275
|
+
return out;
|
|
1276
|
+
}
|
|
1277
|
+
export function scanDidFixConsolidation(spec) {
|
|
1278
|
+
const out = [];
|
|
1279
|
+
const f = (what) => out.push({ rule: 'k8-consolidation', file: 'web/server.ts', line: 1, what });
|
|
1280
|
+
const code = stripJsComments(spec.src);
|
|
1281
|
+
// 用字符串拼接造正则, 避开模板字面量里的多层转义 (踩过: 写出过非法 TS)
|
|
1282
|
+
const wordy = (sym) => new RegExp('(^|[^\\w$])' + sym + '([^\\w$]|$)');
|
|
1283
|
+
if (wordy(spec.removedFlag).test(code)) {
|
|
1284
|
+
f(spec.removedFlag + ' 仍在代码里 (通道自己的全局单飞) ⇒ 收口没真生效');
|
|
1285
|
+
}
|
|
1286
|
+
if (!code.includes(spec.mustUse + '(')) {
|
|
1287
|
+
f('没看到 ' + spec.mustUse + '( ⇒ 待办执行没走内核邮箱');
|
|
1288
|
+
}
|
|
1289
|
+
if (!wordy(spec.nextTargetSymbol).test(code)) {
|
|
1290
|
+
f(spec.nextTargetSymbol + ' 不见了 ⇒ 台账必须同步 (它是**下一目标**或已收口, 两种都要显式登记)');
|
|
1291
|
+
}
|
|
1292
|
+
return out;
|
|
1293
|
+
}
|
|
1294
|
+
/** 剥注释后数某符号出现次数 (迁移工作面的唯一口径) */
|
|
1295
|
+
export function countSymbolOccurrences(src, symbol) {
|
|
1296
|
+
return (stripJsComments(src).match(new RegExp('(^|[^\\w$])' + symbol + '([^\\w$]|$)', 'g')) || []).length;
|
|
1297
|
+
}
|
|
1298
|
+
export function scanRunStateConsolidation(ledger, progress, opts) {
|
|
1299
|
+
const out = [];
|
|
1300
|
+
const f = (what) => out.push({ rule: 'k8-runstate', file: 'kernel/plan-communication.ts', line: 1, what });
|
|
1301
|
+
const src = opts.readFile(ledger.file);
|
|
1302
|
+
if (src === null) {
|
|
1303
|
+
f(`读不出源码 ⇒ 拒跑: ${ledger.file}`);
|
|
1304
|
+
return out;
|
|
1305
|
+
}
|
|
1306
|
+
// ① 用法数: 台账 == 盘上 (迁移后同步下调)
|
|
1307
|
+
const real = countSymbolOccurrences(src, ledger.symbol);
|
|
1308
|
+
if (real !== ledger.sites)
|
|
1309
|
+
f(`${ledger.symbol} 用法数 台账=${ledger.sites} 盘上=${real} (增=新用法未登记 · 减=改了盘没改账)`);
|
|
1310
|
+
// 棘轮: 不许比基线更多
|
|
1311
|
+
const base = progress.runStateSites;
|
|
1312
|
+
if (typeof base === 'number' && real > base)
|
|
1313
|
+
f(`${ledger.symbol} 用法数超棘轮: 盘上=${real} > 基线=${base}`);
|
|
1314
|
+
// ② 字段必须真在接口里
|
|
1315
|
+
for (const fld of ledger.fields) {
|
|
1316
|
+
const re = new RegExp('(^|[^\\w$])' + fld.name + '\\??\\s*:', 'm');
|
|
1317
|
+
if (!re.test(src))
|
|
1318
|
+
f(`字段 ${fld.name} 在 ${ledger.file} 的接口里找不到`);
|
|
1319
|
+
if (!['k8-target', 'observational', 'domain-collab'].includes(fld.role))
|
|
1320
|
+
f(`字段 ${fld.name} role 非法: ${fld.role}`);
|
|
1321
|
+
if (fld.role === 'k8-target' && !(fld.replacedBy ?? '').trim())
|
|
1322
|
+
f(`字段 ${fld.name} 是收口对象却没写替代机制`);
|
|
1323
|
+
if (fld.role !== 'k8-target' && (fld.note ?? '').trim().length < 6)
|
|
1324
|
+
f(`字段 ${fld.name} 标了 ${fld.role} 却没写明理由 ⇒ 不许静默豁免`);
|
|
1325
|
+
}
|
|
1326
|
+
// K8 正刀 (2026-10-02): 收口对象 **3 → 1** —— `queue` 已**删除** (每条消息自己进内核邮箱, 通道不再持队列),
|
|
1327
|
+
// `running` 已**降为观测** (串行权威归邮箱); 只剩 `abortController` (abort 语义要单独定, 不随队列顺手合并)。
|
|
1328
|
+
// 棘轮: 只许减; 要加回来必须是一次显式动作 + 说明为什么。
|
|
1329
|
+
if (ledger.fields.filter((x) => x.role === 'k8-target').length !== 0) {
|
|
1330
|
+
f(`收口字段数应为 0 (K8 收尾 2026-10-02: 最后一个 abortController 已归内核 actor —— beginCancellation/abort; queue 早前已删 · running 已降观测), 实际 ${ledger.fields.filter((x) => x.role === 'k8-target').length}`);
|
|
1331
|
+
}
|
|
1332
|
+
// 反回归: 已收口的 `queue` 不许悄悄溜回接口 (删于 2026-10-02 正刀)
|
|
1333
|
+
if (/(^|[^\w$])queue\s*:\s*\w+\[\]/.test(src))
|
|
1334
|
+
f('已收口的字段 queue 又出现在接口里 (通道不该再持队列)');
|
|
1335
|
+
// ③ 计划必须有 (别只登记不动手)
|
|
1336
|
+
if ((ledger.plan ?? '').trim().length < 10)
|
|
1337
|
+
f('缺迁移计划 (台账不能只登记)');
|
|
1338
|
+
// ④ 收口**前置**必须有, 且要点到机制 —— 否则下一个人会裸删字段把串行性搞坏
|
|
1339
|
+
const pre = (ledger.prerequisite ?? '').trim();
|
|
1340
|
+
if (pre.length < 20)
|
|
1341
|
+
f('缺收口总前置 (哪些字段不能裸删 / 为什么)');
|
|
1342
|
+
else if (!pre.includes('getChannelQueue'))
|
|
1343
|
+
f('收口总前置必须点到机制 (`getChannelQueue`) ⇒ 否则只是空话');
|
|
1344
|
+
for (const fld of ledger.fields) {
|
|
1345
|
+
if (typeof fld.prerequisite === 'string' && fld.prerequisite.trim().length < 10) {
|
|
1346
|
+
f(`字段 ${fld.name} 的 prerequisite 太短 (要写清先满足什么)`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return out;
|
|
1350
|
+
}
|
|
@@ -14,6 +14,28 @@
|
|
|
14
14
|
* 通用判据 (K2 实证): **本文件出现后, 旧写口的调用点数不许增加** —— 由 `scanModelRuntimeFile` 机械核对。
|
|
15
15
|
*/
|
|
16
16
|
/** 429 识别 (端口可以把限流表达成返回值 status=429, 或抛一个带 status 的错) */
|
|
17
|
+
/**
|
|
18
|
+
* K10 ②: 把"有效选择"投影成内核的 `ModelSnapshot` —— **只读投影 + 校验**。
|
|
19
|
+
* 缺 provider/model ⇒ **返回 null** (调用方自己决定降级): 不许编一个假的填进去。
|
|
20
|
+
* `acquire()` 自己也会拒, 但这里先拒能省掉一次"猜配置"的机会 (而且这个函数是纯的, 好判)。
|
|
21
|
+
*/
|
|
22
|
+
export function snapshotFromSelection(sel) {
|
|
23
|
+
const provider = String(sel?.provider ?? '').trim();
|
|
24
|
+
const model = String(sel?.model ?? '').trim();
|
|
25
|
+
if (!provider || !model)
|
|
26
|
+
return null;
|
|
27
|
+
const out = { provider, model };
|
|
28
|
+
const baseUrl = String(sel?.baseUrl ?? '').trim();
|
|
29
|
+
if (baseUrl)
|
|
30
|
+
out.baseUrl = baseUrl;
|
|
31
|
+
if (typeof sel?.timeoutMs === 'number')
|
|
32
|
+
out.timeoutMs = sel.timeoutMs;
|
|
33
|
+
if (sel?.capabilities)
|
|
34
|
+
out.capabilities = sel.capabilities;
|
|
35
|
+
if (sel?.fallbackProviders)
|
|
36
|
+
out.fallbackProviders = sel.fallbackProviders;
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
17
39
|
export function isRateLimited(x) {
|
|
18
40
|
if (!x || typeof x !== 'object')
|
|
19
41
|
return false;
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
* (用户消息 / 第二条路径 / 重新生成), `web/server.ts` 里共 8 处入口执行点 ⇒ `entrySites { total: 8, wired: 3 }`,
|
|
54
54
|
* 两个数字都由判据 `scanEntryDelivery` **从盘上重算**, 自报无效。
|
|
55
55
|
* 四个**粗粒度**入口 (web / CLI / P2P / Supervisor) 的 `entriesWired` 仍 **0/4** —— 一条入口要全部执行点接完才算。
|
|
56
|
+
* · 2026-10-02 (K8 第二步/第三步): server.ts 11 → **8** —— 3 处 `promptStream(...)` 改走 `runExecution` (统一入口),
|
|
57
|
+
* 它们不再是"通道直呼 agent"的执行点 ⇒ 逐文件表跟着重建 (仍全部 wired)。
|
|
56
58
|
* · 2026-10-02 **步骤④ 完成**: web (server.ts 11/11 + routes-tasks 1/1) · CLI (index.ts) · P2P 入站 · Supervisor/子Agent
|
|
57
59
|
* (runner-resolver.ts 1/1) ⇒ `entriesWired **4/4**`。
|
|
58
60
|
* **关键发现 (口径漏了一整类入口)**: P2P 入站调的是 `a.summarizeDocument(...)` / `a.improveDocument(...)` ——
|
|
@@ -216,9 +218,11 @@ export const K5_FIELD_DELETION = {
|
|
|
216
218
|
*/
|
|
217
219
|
export const K5_EXECUTION_REQUEST = {
|
|
218
220
|
methodAdded: true,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
// 2026-10-02 (K8 第二步): 1 → 3 —— routes-tasks.ts / runner-resolver.ts 两处改走 `runExecution`
|
|
222
|
+
// (K5 这张台账记的正是"请求式投递的收敛进度", K8 的迁移会推进它 ⇒ 跨台账必须同步)
|
|
223
|
+
converted: 12, // 2026-10-02: 6 → 10 → 13 → **12** (K8 正刀删掉 `runMessageFromQueue` 简化版路径 ⇒ 随它消失 1 处)
|
|
224
|
+
wiredTotal: 23, // 2026-10-02 正刀: 24 → **23** (删掉的 `runMessageFromQueue` 自己也有一处请求式投递点)
|
|
225
|
+
remaining: 11, // 派生: wiredTotal − converted = 23 − 12 = 11 (与盘上一致)
|
|
222
226
|
};
|
|
223
227
|
/** 入口 → 文件分组 (判据做**双向**校验: 说完成 ⇒ 其文件必须全接完; 说没完成 ⇒ 必须真有文件没接完) */
|
|
224
228
|
export const K5_ENTRY_GROUPS = [
|
|
@@ -265,8 +269,11 @@ export const K5_PROGRESS = {
|
|
|
265
269
|
entrySites: [
|
|
266
270
|
// `excludeReceivers` 是**已核实的非执行点 receiver** (台账数据, 冻结; 改动会出现在 diff 里):
|
|
267
271
|
// `this` = CLI readline 提示 (`this.prompt('> ')`); `s` = index.ts 里的 UI 打印助手 (`s.prompt('📩 …')`)
|
|
268
|
-
|
|
269
|
-
|
|
272
|
+
// 2026-10-02 (K8 第二/三步): 3 处 `promptStream(...)` 改走唯一入口 `runExecution`。
|
|
273
|
+
// 口径说明: `runExecution` 也在方法名单里, 且**助手形态** `requireRunExecution(...)` 也被认作执行点
|
|
274
|
+
// ⇒ 迁移前后 total/wired 都不变 (11/11); 真正在变的是 K8 台账的"通道直呼 prompt"直连数 (10 → 7)。
|
|
275
|
+
{ file: 'web/server.ts', total: 10, wired: 10, excludeReceivers: ['this'] }, // web 用户/中继/任务/心跳 (2026-10-02 正刀: 11 → 10, 简化版路径删除)
|
|
276
|
+
{ file: 'web/routes-tasks.ts', total: 1, wired: 1, excludeReceivers: ['this'] }, // web 任务路由 (K8 后是请求式点; total 不降)
|
|
270
277
|
{ file: 'index.ts', total: 11, wired: 11, excludeReceivers: ['this', 's'] }, // CLI 主入口 + P2P 入站 (含文档摘要/改写)
|
|
271
278
|
{ file: 'cli/interface.ts', total: 0, wired: 0, excludeReceivers: ['this'] }, // readline ⇒ 无执行点
|
|
272
279
|
{ file: 'agents/runner-resolver.ts', total: 1, wired: 1, excludeReceivers: ['this'] }, // 子 Agent / Supervisor 面
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K8 台账 (2026-10-02 建) —— **Communication Runtime 收口**
|
|
3
|
+
*
|
|
4
|
+
* 目标形态: `transport → router → channel mailbox`
|
|
5
|
+
* **绝不**: `transport → agent.promptStream()` (通道各自直呼 Agent = 通信层长出 N 条执行路径,
|
|
6
|
+
* 每条的流式/错误/重试/恢复语义都不一样, 没人能一眼看清"一条消息到底怎么被处理")。
|
|
7
|
+
*
|
|
8
|
+
* 本文件是 K8 的**第一步 (台账)**; 判据在 `gate-scan.ts` 的 `countTransportAgentSites` +
|
|
9
|
+
* `scanCommunicationLedger` 里重算比对 —— **台账与盘上事实必须逐字一致**。
|
|
10
|
+
*/
|
|
11
|
+
/** K8 要统一的 8 个事件面 (设计页 §7 原文口径) */
|
|
12
|
+
export const K8_EVENT_FACES = [
|
|
13
|
+
'web-message', // Web 普通消息
|
|
14
|
+
'web-stream', // Web 流式回复
|
|
15
|
+
'web-regen', // Web 重生成
|
|
16
|
+
'cli', // CLI 通道
|
|
17
|
+
'p2p', // P2P 对等消息
|
|
18
|
+
'mobile', // 手机端
|
|
19
|
+
'contacts-reply', // 联系人自动回复
|
|
20
|
+
'external-wake', // 外部唤醒
|
|
21
|
+
'cron', // 定时任务
|
|
22
|
+
'supervisor', // Supervisor 事件
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* 迁移记录 (每题一处, 都要有"为什么这次是零差量/有差量"的判断):
|
|
26
|
+
* · 2026-10-02 第二步: `routes-tasks.ts` / `runner-resolver.ts` 改走 `runExecution`
|
|
27
|
+
* —— routes-tasks 那处**有差量** (隐式绑定 → 显式 `channelId`), 记为待真跑核验。
|
|
28
|
+
* · 2026-10-02 第三步: `web/server.ts` **10 → 7** —— 三处 `promptStream(p, cb, undefined, channelId)`
|
|
29
|
+
* 改走 `runExecution({ input, onStream, channelId })`: 参数与派发**完全一致** ⇒ **按构造零差量** (不新增任何绑定)。
|
|
30
|
+
*/
|
|
31
|
+
export const K8_TRANSPORT_AGENT_SITES = [
|
|
32
|
+
// K8 第五步: 10 → 7 → 3 → 0 (★ server.ts 已零直连)
|
|
33
|
+
{ file: 'src/web/server.ts', count: 0, kind: 'via-actor', faces: ['web-message', 'web-stream', 'web-regen', 'cron', 'supervisor', 'external-wake'], status: 'open', why: 'web 通道 (消息/流式/重生成/cron/Supervisor/外部唤醒)' },
|
|
34
|
+
{ file: 'src/web/routes-tasks.ts', count: 0, kind: 'via-actor', status: 'migrated', evidence: '改经 runExecution({input}) —— 行为等价 (applyExecutionRequest 只覆盖显式给出的字段)', faces: ['contacts-reply'], why: 'task 路由' },
|
|
35
|
+
{ file: 'src/agents/runner-resolver.ts', count: 0, kind: 'via-actor', status: 'migrated', evidence: 'routes-tasks 那处由隐式绑定改为**显式**传 channelId —— **2026-10-02 真跑核验通过** (src/test/k8-routes-tasks-execution.test.ts: 两半 id 一致的自检不变量 + agent 无 prompt 也跑通 + 真变异判红)', faces: ['external-wake'], why: '独立宿主 runner' },
|
|
36
|
+
{ file: 'src/cli-entry.ts', count: 0, kind: 'direct-prompt', status: 'open', faces: ['cli'], why: 'CLI 通道' },
|
|
37
|
+
{ file: 'src/web/mobile-core.ts', count: 0, kind: 'direct-prompt', status: 'open', faces: ['mobile'], why: '手机端通道' }
|
|
38
|
+
];
|
|
39
|
+
/** 各通道自带的出站/重试/恢复状态 (K8 验收: "无各通道自己的重试/任务恢复/outbound 状态")
|
|
40
|
+
*
|
|
41
|
+
* 纪律: **按符号记, 不按行号** —— 行号会漂 (K7 已发生过一次), 门的核法 = "该符号真在该文件里"。
|
|
42
|
+
* 口径 (2026-10-02 逐符号定性, 修正过一次): 每个符号必须带 **scope** —— 因为"文件里出现的状态"
|
|
43
|
+
* 不等于"通道自己的 outbound/重试/恢复"。把 UI 展示状态当收口对象是**伪收口**。
|
|
44
|
+
*/
|
|
45
|
+
export const K8_STATE_SCOPES = ['k8-target', 'ui-state', 'domain-pending', 'infra-retry'];
|
|
46
|
+
const T = (name) => ({ name, scope: 'k8-target' });
|
|
47
|
+
export const K8_PER_CHANNEL_STATE = [
|
|
48
|
+
{ file: 'src/network/p2p-outbox.ts', kind: 'outbox',
|
|
49
|
+
symbols: ['outbox', 'outboxFile', 'outboxStats', 'totalQueued', 'sendOrQueue', 'queueOnly', 'flushAllOutboxes'].map(T),
|
|
50
|
+
why: 'P2P 出站队列 (通道自己的 outbound 状态 + 自己的 flush 时机)' },
|
|
51
|
+
{ file: 'src/web/delivery-ledger.ts', kind: 'delivery-ledger',
|
|
52
|
+
symbols: ['DeliveryLedger', 'DeliveryRecord', 'DeliveryState', 'LedgerStats'].map(T),
|
|
53
|
+
why: '投递台账 (通道自己的投递状态机)' },
|
|
54
|
+
{ file: 'src/web/server-v3-p2p.ts', kind: 'retry',
|
|
55
|
+
symbols: ['v3PendingHistoryGets', 'getV3PendingHistoryGets'].map(T),
|
|
56
|
+
why: 'P2P 侧挂起的 history 取回 (通道自己的待办 + 恢复路径)' },
|
|
57
|
+
{ file: 'src/web/server.ts', kind: 'retry',
|
|
58
|
+
symbols: [
|
|
59
|
+
T('didFixQueue'),
|
|
60
|
+
T('deliveryLedger'), // SSE ping 用的 per-client 连败计数 ⇒ 真出站状态
|
|
61
|
+
{ name: 'didFixTimer', scope: 'k8-target', note: '2s 节流定时器 —— 保留为**调度策略** (执行已交内核邮箱); 它本身仍是通道自己的调度状态' },
|
|
62
|
+
{ name: 'channelRunState', scope: 'k8-target', note: '**下一个(大)目标**: 模块级 `Map<channelId, {running, queue, abortController}>` (24 处用法) —— 通道自己的消息队列 + 串行 flag + abort, 与内核邮箱**功能重复**' },
|
|
63
|
+
{ name: 'messageQueue', scope: 'ui-state', note: '`(global as any)` 上的 **Web UI 通知列表** (pending/已读) ⇒ 展示状态, 不是 outbound/重试/恢复' },
|
|
64
|
+
{ name: 'pendingFriendRequests', scope: 'domain-pending', note: '**好友申请待办** (业务审批数据, 落盘恢复) ⇒ 业务状态, 不是投递状态' },
|
|
65
|
+
{ name: 'PENDING_FRIEND_REQ_FILE', scope: 'domain-pending', note: '同上: 好友申请待办的落盘文件' },
|
|
66
|
+
{ name: 'maxAttempts', scope: 'infra-retry', note: 'HTTP `listen` 的 **EADDRINUSE 重试上限** ⇒ 进程启动基础设施, 不是通道出站' },
|
|
67
|
+
],
|
|
68
|
+
why: 'web 通道内联的真状态: 待修复 channelId 队列(执行已交内核邮箱) + SSE 投递连败计数 + 通道自己的消息队列 channelRunState; 三个符号经定性**不属** K8 收口范围, 逐条写明原因' },
|
|
69
|
+
{ file: 'src/cli-entry.ts', kind: 'resume',
|
|
70
|
+
symbols: [T('resumeId')],
|
|
71
|
+
why: 'CLI 侧会话恢复' },
|
|
72
|
+
{ file: 'src/network/peer-fs.ts', kind: 'outbox',
|
|
73
|
+
symbols: ['OutboxEntry', 'enqueueOutbox', 'readOutbox', 'countOutbox', 'clearOutbox', 'getPeerOutboxPath'].map(T),
|
|
74
|
+
why: '文件传输侧出站 (落盘 outbox, 与 p2p-outbox 又一套)' },
|
|
75
|
+
{ file: 'src/agents/contacts/store.ts', kind: 'none', symbols: [],
|
|
76
|
+
why: '纯存储: **无**自带出站/重试/恢复状态 (显式记 none, 不是"忘了填")' },
|
|
77
|
+
{ file: 'src/agents/contacts/providers.ts', kind: 'retry',
|
|
78
|
+
symbols: ['DeliverResult', 'OTP_MAX_ATTEMPTS', 'smtpDeliver'].map(T),
|
|
79
|
+
why: '联系人通道自己的投递结果与重试上限' },
|
|
80
|
+
{ file: 'src/agents/p2p-chat-tools.ts', kind: 'outbox',
|
|
81
|
+
symbols: ['outboxPath', 'processPendingInbox'].map(T),
|
|
82
|
+
why: 'P2P 聊天工具侧出站 + 待办处理' },
|
|
83
|
+
{ file: 'src/agents/task/task-runner.ts', kind: 'resume',
|
|
84
|
+
symbols: ['ResumeResult', 'resumeTask', 'redelivered', 'refetchDeliveredContent'].map(T),
|
|
85
|
+
why: '任务运行器自带恢复 (含 x402 内容重取/重投递语义)' },
|
|
86
|
+
];
|
|
87
|
+
export const K8_ACCEPTANCE = [
|
|
88
|
+
'通信层**无直接 Agent 调用** (一律 transport → router → channel mailbox)',
|
|
89
|
+
'**无各通道自己的** 重试 / 任务恢复 / outbound 状态 (统一到 router + mailbox)',
|
|
90
|
+
'8 个事件面全部经同一条路由 (Web/CLI/P2P/手机/联系人回复/外部唤醒/cron/Supervisor)',
|
|
91
|
+
'收口后**直连数只许减** (棘轮), 且每减一处都要有"改走 router"的可回滚提交点',
|
|
92
|
+
'失败语义统一: 通道不再各自决定重试/放弃 (避免"有的通道静默丢、有的无限重试")',
|
|
93
|
+
];
|
|
94
|
+
export const K8_PROGRESS = {
|
|
95
|
+
stage: 'ledger-landed',
|
|
96
|
+
directSites: 0, // 12 → 10 (K8 第二步) → 7 (第三步: server.ts 3 处零差量迁移) (2026-10-02: routes-tasks / runner-resolver 两处已走唯一入口; 棘轮只许减)
|
|
97
|
+
perChannelStateFiles: 10,
|
|
98
|
+
// ⚠️ 2026-10-02 **补登**: 上一版漏了两个真状态符号 (`channelRunState` / `didFixTimer`) ⇒ 35 → 37。
|
|
99
|
+
// 这是**纠正漏登**不是范围扩张 (棘轮拦的是"通道自己状态变多", 不是"把已存在的登记上来")。
|
|
100
|
+
// 同一次提交里 `didFixRunning` (通道自己的全局单飞) 被**真删掉** —— 收口本身是减项, 只是它先前没被登记。
|
|
101
|
+
perChannelStateSymbols: 37, // 10 文件 / 37 个状态符号 (含 4 个范围外)
|
|
102
|
+
k8TargetSymbols: 33, // 其中 **33** 个属 K8 收口对象 (棘轮: 只许减)
|
|
103
|
+
runStateSites: 20, // 下一大目标的迁移工作面 (棘轮: 只许减)
|
|
104
|
+
};
|
|
105
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
106
|
+
// K8 下一个大目标: `web/server.ts` 的 `channelRunState` (通道自己的消息队列 + 串行 flag + abort)
|
|
107
|
+
//
|
|
108
|
+
// 为什么它是"最大的一刀": 它与内核邮箱**功能重复** —— 通道自己存队列、自己管 running、自己管 abort。
|
|
109
|
+
// 为什么先落台账不直接改: 21 处用法 (剥注释后, 2026-10-02 量) + 三种语义混在一个对象里
|
|
110
|
+
// (调度 / 观测 / 协作续看), 直接改必然把观测与业务语义一起搅进去。
|
|
111
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
112
|
+
export const K8_RUNSTATE_ROLES = ['k8-target', 'observational', 'domain-collab'];
|
|
113
|
+
/**
|
|
114
|
+
* `channelRunState` 收口的**总前置** (2026-10-02 量出, 必须写在代码旁边而不是只写在提交里)。
|
|
115
|
+
*
|
|
116
|
+
* 为什么: `running` 同时是**主路径内联跑**与**排队路径**的串行权威。内核邮箱只认识"投给它的任务" ——
|
|
117
|
+
* 若只把排队项投进邮箱而主路径仍内联跑, 邮箱会在内联跑还没结束时**立刻起跑**排队项
|
|
118
|
+
* ⇒ 同一 channel **两条 run 并行** ⇒ 串行性被破坏 (K5 的"同通道串行"承诺失效)。
|
|
119
|
+
* 所以删 `queue`/`running` 的**前置**是: 主路径内联跑自己也进邮箱 (`await getChannelQueue(id).submit(...)`)。
|
|
120
|
+
*
|
|
121
|
+
* **2026-10-02 自纠: 下面这段早些时候写的"实测缺陷"是错的, 按盘上事实改正 (留着错的说法比没有更危险)。**
|
|
122
|
+
* 旧说法 = "主路径 check-then-set 夹在多个 `await` 之间 ⇒ 并发 `/message` 会都通过检查 ⇒ 同通道双开"。
|
|
123
|
+
* **盘上事实不成立**: `if (runState.running)` (server.ts 4839) 与 `running = true` (4853) 之间只有
|
|
124
|
+
* `queue.push` / `broadcastQueueUpdate` / `console.log` / `return` —— **没有 await**; 排空路径那对
|
|
125
|
+
* (`if (runState.running) return;` / `running = true`, 5657/5658) 更是**相邻两行**
|
|
126
|
+
* ⇒ 两处在 Node 单线程模型下都**是原子的**。
|
|
127
|
+
*
|
|
128
|
+
* 真正存在的洞是**同族的另一条** (`handoff 不是授权`): `finishChannelRun` 先**同步**置 `running = false`,
|
|
129
|
+
* 再把下一条**异步**交给邮箱 (`getChannelQueue(channelId).submit`) —— 而该邮箱**与 didFix 等任务共用**
|
|
130
|
+
* (`server.ts` 5790 用同一个 `getChannelQueue(id)`) ⇒ 邮箱里排着前序任务时 handoff 会延后, 这期间新到的
|
|
131
|
+
* `/message` 看到 `running === false` 就**内联起跑**; 等那条排队消息终于被跑到时, 它的防重入守卫
|
|
132
|
+
* `if (runState.running) return;` 直接返回 ⇒ **那条排队消息被静默丢掉** (不是双开, 是丢消息 —— 更糟),
|
|
133
|
+
* 且顺序也被打乱 (后到的先跑)。修法仍是同一个前置: **每条消息 (含主路径) 都投邮箱**, `running`/`queue` 降为观测。
|
|
134
|
+
*/
|
|
135
|
+
export const K8_RUNSTATE_PREREQUISITE = '主路径内联跑必须先进内核邮箱 (`await getChannelQueue(channelId).submit(...)`) 才能删 `queue`/`running`: ' +
|
|
136
|
+
'否则邮箱会与内联跑并行起跑排队项, 同通道双开 (K5 串行承诺失效)。' +
|
|
137
|
+
'**已核实的现网缺陷 (2026-10-02 自纠后)**: handoff 不是授权 —— `finishChannelRun` 同步置 running=false 后异步投邮箱, ' +
|
|
138
|
+
'邮箱与 didFix 共用 ⇒ 前序任务排队时 handoff 延后, 新 `/message` 见 running=false 就内联跑; ' +
|
|
139
|
+
'那条排队消息随后撞上 `if (runState.running) return;` ⇒ **被静默丢掉** (并打乱 FIFO)。' +
|
|
140
|
+
'(旧说法"check-then-set 夹 await ⇒ 并发双开"**不成立**, 已按盘上事实改正。)';
|
|
141
|
+
export const K8_CHANNEL_RUNSTATE = {
|
|
142
|
+
file: 'src/web/server.ts',
|
|
143
|
+
symbol: 'channelRunState',
|
|
144
|
+
sites: 20, // 2026-10-02 收尾后真实值 20 (原 21; 中止位归 actor 时删掉 1 处用法) · 剥注释口径
|
|
145
|
+
fields: [
|
|
146
|
+
{ name: 'running', role: 'observational',
|
|
147
|
+
note: '**2026-10-02 正刀后已降为观测口径**: 串行权威归内核邮箱 (`getChannelQueue(channelId).submit`), `running` 只剩"这一轮是否在飞"的展示/判定用途 (UI 的 `queue_update` · `remoteFollowup` 的 `!matchedRs.running` · `/api/loop/inspect` 文案), 不再参与任何"能不能起跑"的判断' },
|
|
148
|
+
// `queue` 字段 **已删除** (2026-10-02 正刀): 每条消息自己 `getChannelQueue(channelId).submit(...)`,
|
|
149
|
+
// 通道不再持有队列 (随之删掉 `PendingMessage` 接口与 `runMessageFromQueue` 那条简化路径)。
|
|
150
|
+
// `abortController` 字段 **已删除** (2026-10-02 收尾): 中止位归**内核 actor** ——
|
|
151
|
+
// `ChannelActor.beginCancellation()` 每次执行开一个新 controller (与邮箱串行一致) ·
|
|
152
|
+
// `ChannelActor.abort(reason)` 中断它; UI 的终止按钮 (`POST /api/chat/abort`) 走
|
|
153
|
+
// `abortActorsOfChannel(channelId, 'user abort')` (按 channel 扫描注册表, 返回**真的中止了几个**)。
|
|
154
|
+
// ⇒ 通道状态里**不再留第二份**中止位 (该条原先挂在 `role: 'k8-target'` 下, 现已收口)。
|
|
155
|
+
{ name: 'lastSteps', role: 'observational', note: '供 `/api/loop/inspect` 的步骤累积 ⇒ 观测数据, 不是调度状态' },
|
|
156
|
+
{ name: 'lastSummary', role: 'observational', note: '同上: 最近一轮摘要 (检查接口用)' },
|
|
157
|
+
{ name: 'lastFinalReply', role: 'observational', note: '同上: 最近一次最终回复 (检查接口用)' },
|
|
158
|
+
{ name: 'lastTokens', role: 'observational', note: '同上: token 计数 (用量展示)' },
|
|
159
|
+
{ name: 'remoteFollowup', role: 'domain-collab', note: '远端协作续看 (`rounds`/`maxRounds`/`remoteChannelId`) ⇒ **业务协作语义**, 不是通道调度' },
|
|
160
|
+
],
|
|
161
|
+
plan: '按语义分三批, 每批独立提交: ① 队列+单飞 (`queue`/`running`) → 内核邮箱; ② `abort` → `ExecutionRequest.signal` (先定语义); ③ `last*` 观测数据搬出通道对象 (或显式标注"非调度")。',
|
|
162
|
+
prerequisite: K8_RUNSTATE_PREREQUISITE,
|
|
163
|
+
};
|