@bolloon/bolloon-agent 0.4.33 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +493 -35
- package/dist/agents/external-events.js +35 -6
- package/dist/agents/gateway-group.js +100 -12
- package/dist/agents/goal-criteria.js +10 -3
- package/dist/agents/goal-flywheel/block-executor.js +346 -0
- package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
- package/dist/agents/goal-flywheel/goal-change.js +754 -0
- package/dist/agents/goal-flywheel/index.js +26 -0
- package/dist/agents/goal-flywheel/memory-layers.js +227 -0
- package/dist/agents/goal-flywheel/run-closure.js +809 -0
- package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
- package/dist/agents/goal-flywheel/types.js +329 -0
- package/dist/agents/goal-flywheel/wiring/change.js +288 -0
- package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
- package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
- package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
- package/dist/agents/goal-flywheel/wiring/index.js +26 -0
- package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
- package/dist/agents/goal-flywheel/work-contract.js +478 -0
- package/dist/agents/goal-flywheel/work-monitor.js +488 -0
- package/dist/agents/goal-flywheel-wiring.js +1870 -0
- package/dist/agents/goal-state-reducer.js +406 -0
- package/dist/agents/goal-store.js +35 -1
- package/dist/agents/mobile-flywheel-view.js +144 -0
- package/dist/agents/mobile-task-actions-verify.js +74 -0
- package/dist/agents/mobile-task-actions.js +178 -0
- package/dist/agents/mobile-task-views.js +199 -0
- package/dist/agents/network-pulse.js +304 -18
- package/dist/agents/pi-sdk.js +75 -20
- package/dist/agents/run-store.js +18 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-runner.js +9 -3
- package/dist/agents/task-group.js +8 -51
- package/dist/agents/task-public-text.js +89 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/tasks.js +350 -2
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/setup-wizard.js +71 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +12 -2
- package/dist/electron-build/agents/contacts/chain.js +26 -6
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +492 -34
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/external-events.js +34 -5
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-criteria.js +10 -3
- package/dist/electron-build/agents/goal-criteria.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
- package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
- package/dist/electron-build/agents/goal-state-reducer.js +414 -0
- package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
- package/dist/electron-build/agents/goal-store.js +35 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/network-pulse.js +307 -19
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +74 -19
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +20 -5
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/skill-readiness.js +52 -9
- package/dist/electron-build/agents/skill-readiness.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +8 -2
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/orbitdb/cid-database.js +83 -17
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/utils/dual-source.js +471 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +331 -57
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +16 -1
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-info.js +82 -2
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +103 -12
- package/dist/ios/chain-explorer.js +617 -0
- package/dist/ios/explorer.html +163 -0
- package/dist/ios/index.html +96 -0
- package/dist/ios/mobile-agent.js +2 -2
- package/dist/ios/mobile-contacts.js +329 -0
- package/dist/ios/mobile-core.js +1379 -226
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +706 -7
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +762 -0
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/utils/dual-source.js +426 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +329 -57
- package/dist/utils/update-state.js +16 -1
- package/dist/utils/version-info.js +75 -1
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +1197 -513
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +531 -1
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/package.json +3 -1
|
@@ -103,7 +103,34 @@ function writeEvents(list, h) {
|
|
|
103
103
|
/* 统计失败绝不影响主路径 */
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
export function tallySignatureAudit(h, opts = {}) {
|
|
107
|
+
const now = opts.now ?? Date.now();
|
|
108
|
+
const windowMs = opts.windowMs ?? PULSE_LIMITS.windowMs;
|
|
109
|
+
const file = path.join(home(h), '.bolloon', 'wallet-signatures.jsonl');
|
|
110
|
+
if (!fs.existsSync(file))
|
|
111
|
+
return { available: false, count: 0, window_ms: windowMs };
|
|
112
|
+
let count = 0;
|
|
113
|
+
try {
|
|
114
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
115
|
+
if (!line.trim())
|
|
116
|
+
continue;
|
|
117
|
+
let row = null;
|
|
118
|
+
try {
|
|
119
|
+
row = JSON.parse(line);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
continue;
|
|
123
|
+
} // 坏行跳过, 不让一行坏数据毁掉整个计数
|
|
124
|
+
const at = Number(row?.at);
|
|
125
|
+
if (Number.isFinite(at) && at >= now - windowMs)
|
|
126
|
+
count += 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return { available: false, count: 0, window_ms: windowMs };
|
|
131
|
+
}
|
|
132
|
+
return { available: true, count, window_ms: windowMs };
|
|
133
|
+
}
|
|
107
134
|
/** 节点摘要: sha256(值) 前 16 位。不可逆, 不暴露原值。 */
|
|
108
135
|
export function nodeDigest(value) {
|
|
109
136
|
return crypto.createHash('sha256').update(`bolloon-pulse|${String(value)}`).digest('hex').slice(0, 16);
|
|
@@ -728,6 +755,8 @@ export function buildChainIdScope(rows) {
|
|
|
728
755
|
* (页面不许出现「0 个任务」与「N 行任务」并存而**不解释**)
|
|
729
756
|
* ⑥ 0x 长 hex 越界 (2026-09-23): 只许出现在 tx_hash/contract/explorer_* 四个白名单键下 ——
|
|
730
757
|
* EOA 地址跑到别的键 (或白名单键形状不对) 就拒绝导出
|
|
758
|
+
* ⑧ ★ **同一概念不许并排两个数** (2026-09-24, 见 `totalsScopeIssues`): 顶部计数与表格行数/
|
|
759
|
+
* 同源计数矛盾, 或者某个数没有来源却不标「未接入」→ 拒绝导出 (这道门在 UI 侧还有一份镜像)
|
|
731
760
|
*/
|
|
732
761
|
export function snapshotConsistencyIssues(snap) {
|
|
733
762
|
const issues = [];
|
|
@@ -756,8 +785,137 @@ export function snapshotConsistencyIssues(snap) {
|
|
|
756
785
|
}
|
|
757
786
|
issues.push(...auditPublicHexLeaks(snap)); // ⑥ 0x 长 hex 越界 → 拒绝导出 (不静默放行)
|
|
758
787
|
issues.push(...openTasksIssues(snap)); // ⑦ 待接单任务的字段白名单与「只含未认领」不变式
|
|
788
|
+
issues.push(...totalsScopeIssues(snap)); // ⑧ ★ 同一概念不得并排两个数 (顶部 vs 表格)
|
|
759
789
|
return issues;
|
|
760
790
|
}
|
|
791
|
+
/** 顶部逐字段口径的键 (与 `TotalsFieldKey` 逐字一致; 少一个 = 那个数没有来源说明) */
|
|
792
|
+
export const TOTALS_FIELD_KEYS = [
|
|
793
|
+
'nodes', 'agents', 'active_agents', 'seen_last_24h',
|
|
794
|
+
'tasks', 'tasks_completed', 'tasks_verified', 'tasks_settled', 'signatures',
|
|
795
|
+
];
|
|
796
|
+
const TOTALS_FIELD_SOURCES = ['pulse-events', 'chain-index', 'signature-audit', 'none'];
|
|
797
|
+
const TOTALS_FIELD_WINDOWS = ['window-24h', 'full', 'unknown'];
|
|
798
|
+
/** 「未接入」类措辞 (无源时必须出现, 否则等于没说清为什么没有数) */
|
|
799
|
+
const UNAVAILABLE_WORDS = /未接入|not connected|无可用源|no available source/;
|
|
800
|
+
/**
|
|
801
|
+
* ★ 新不变量门 (2026-09-24 leo:「数量怎么对不上」): **同一概念不许并排两个数**。
|
|
802
|
+
*
|
|
803
|
+
* 检查的是「页面会把它们放在同一屏里」的那几个数 —— 它们的口径必须各自挂得住:
|
|
804
|
+
* ① 逐字段口径 `totals_scope.fields` 九个键必须齐 (source/window/short/label 双语都在) ——
|
|
805
|
+
* 缺了就只能靠 notes 辩解, 而那正是当初「顶部 0 个任务 vs 表格 25 行」能上线的路径;
|
|
806
|
+
* ② 值 `null` ⇔ 口径标 `none` + 明说「未接入」; **有源却不给数** 或 **无源却拿 0 冒充** 都算矛盾;
|
|
807
|
+
* ③ 同一概念对账 (表里有 5 个任务而顶部写 0 任务 → 判红):
|
|
808
|
+
* · 链上索引可用时, 顶部 tasks/tasks_completed/tasks_settled **必须逐字等于** activity_totals
|
|
809
|
+
* 的同名字段 (同源即恒等, 任何解释都救不了);
|
|
810
|
+
* · 表格行数/同源计数非零而顶部为 0 或缺值 → 必须有口径说明 (note 里写清) 才放行;
|
|
811
|
+
* · 顶部说有 N 个任务而表格一行都没有 (反向矛盾) → 同样判红。
|
|
812
|
+
* `status === 'unavailable'` 的整份快照不发布任何计数 (页面整块显示「快照读不到」), 故只查形状。
|
|
813
|
+
*/
|
|
814
|
+
export function totalsScopeIssues(snap) {
|
|
815
|
+
const out = [];
|
|
816
|
+
const t = snap?.totals || {};
|
|
817
|
+
const ts = snap?.totals_scope;
|
|
818
|
+
const at = snap?.activity_totals || {};
|
|
819
|
+
const rows = Array.isArray(snap?.confirmed_activity) ? snap.confirmed_activity : [];
|
|
820
|
+
const unavailableSnap = String(snap?.status || '') === 'unavailable';
|
|
821
|
+
if (!ts || typeof ts !== 'object')
|
|
822
|
+
return ['totals_scope 缺失 (顶部计数的口径必须随快照一起给)'];
|
|
823
|
+
if (!ts.fields || typeof ts.fields !== 'object') {
|
|
824
|
+
return ['totals_scope.fields 缺失 (顶部每个数都要能就地说明来自哪 —— 不许只靠 notes 辩解)'];
|
|
825
|
+
}
|
|
826
|
+
// ① 形状: 九个键齐 + 每个键的 source/window/short/label 都合法
|
|
827
|
+
for (const k of TOTALS_FIELD_KEYS) {
|
|
828
|
+
const f = ts.fields[k];
|
|
829
|
+
if (!f || typeof f !== 'object') {
|
|
830
|
+
out.push(`totals_scope.fields.${k} 缺失`);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (!TOTALS_FIELD_SOURCES.includes(f.source))
|
|
834
|
+
out.push(`totals_scope.fields.${k}.source=${JSON.stringify(f.source)} 不在白名单`);
|
|
835
|
+
if (!TOTALS_FIELD_WINDOWS.includes(f.window))
|
|
836
|
+
out.push(`totals_scope.fields.${k}.window=${JSON.stringify(f.window)} 不在白名单`);
|
|
837
|
+
if (!f.label?.zh || !f.label?.en)
|
|
838
|
+
out.push(`totals_scope.fields.${k}.label 必须双语都给`);
|
|
839
|
+
if (!f.short?.zh || !f.short?.en)
|
|
840
|
+
out.push(`totals_scope.fields.${k}.short 必须双语都给 (页面要就地贴在数字旁)`);
|
|
841
|
+
}
|
|
842
|
+
if (out.length)
|
|
843
|
+
return out; // 形状都不对就不往下算数
|
|
844
|
+
if (!unavailableSnap) {
|
|
845
|
+
// ② 值 ⇔ 口径 (null = 未接入; 有源必须有数; 无源不许给 0)
|
|
846
|
+
for (const k of TOTALS_FIELD_KEYS) {
|
|
847
|
+
const f = ts.fields[k];
|
|
848
|
+
const v = t[k];
|
|
849
|
+
const isNull = v === null || v === undefined;
|
|
850
|
+
if (isNull) {
|
|
851
|
+
if (f.source !== 'none') {
|
|
852
|
+
out.push(`totals.${k} 没有值 (未接入) 却标了源 ${f.source} —— 有源就必须给真值, 没源就标 none`);
|
|
853
|
+
}
|
|
854
|
+
if (!UNAVAILABLE_WORDS.test(`${f.short?.zh}${f.short?.en}${f.label?.zh}${f.label?.en}`)) {
|
|
855
|
+
out.push(`totals.${k} 没有值却没有一句「未接入」说明 (读者会把空白读成 0)`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v < 0) {
|
|
860
|
+
out.push(`totals.${k}=${JSON.stringify(v)} 不是非负有限数`);
|
|
861
|
+
}
|
|
862
|
+
else if (f.source === 'none') {
|
|
863
|
+
out.push(`totals.${k}=${v} 却标「无源」 —— 不许拿裸 0 冒充「没有接入」(那会被读成"没发生过")`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
// ③ 同一概念对账 (顶部计数 vs 表格行数/同源计数)
|
|
868
|
+
const notes = Array.isArray(snap.notes) ? snap.notes.join(' ') : '';
|
|
869
|
+
const pairs = [
|
|
870
|
+
['tasks', t.tasks, at.tasks],
|
|
871
|
+
['tasks_completed', t.tasks_completed, at.tasks_completed],
|
|
872
|
+
['tasks_settled', t.tasks_settled, at.tasks_settled],
|
|
873
|
+
];
|
|
874
|
+
const tableRows = Number(at.rows) || rows.length;
|
|
875
|
+
const sameSourceChain = String(at.source) === 'chain-index';
|
|
876
|
+
// ⑨ 声明与能力不符: 某字段自称「链上索引」口径, 而这一份快照的链上索引根本不可用 ——
|
|
877
|
+
// 那就是替一个不存在的源背书 (页面会写「链上索引·全量」, 而索引这一份没读出来)。
|
|
878
|
+
// 注: 本块必须放在 pairs / sameSourceChain 之后 (先声明后使用, 否则 TDZ 崩)。
|
|
879
|
+
if (!sameSourceChain) {
|
|
880
|
+
for (const [k] of pairs) {
|
|
881
|
+
const f = ts.fields[k];
|
|
882
|
+
if (f.source === 'chain-index') {
|
|
883
|
+
out.push(`totals_scope.fields.${k}.source='chain-index' 而这一份快照的链上索引不可用 ` +
|
|
884
|
+
`(activity_totals.source=${JSON.stringify(at.source)}) —— 声明与能力不符`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
for (const [k, top, src] of pairs) {
|
|
889
|
+
const n = Number(src) || 0;
|
|
890
|
+
const topN = (top === null || top === undefined) ? null : Number(top);
|
|
891
|
+
const topScope = ts.fields[k];
|
|
892
|
+
if (sameSourceChain && topScope.source === 'chain-index') {
|
|
893
|
+
// 同源: 必须逐字相等, 不需要任何"解释"
|
|
894
|
+
if (topN !== n) {
|
|
895
|
+
out.push(`同一概念两个数: 顶部 totals.${k}=${topN === null ? '未接入' : topN} ≠ 链上索引同源计数 activity_totals.${k}=${n}` +
|
|
896
|
+
` (表格 ${tableRows} 行) —— 同源即恒等, 页面并排出现两个数就是矛盾`);
|
|
897
|
+
}
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (n > 0 && (topN === null || topN === 0)) {
|
|
901
|
+
const explained = notes.includes('口径') && notes.includes(String(tableRows)) && notes.includes('脉冲事件');
|
|
902
|
+
if (!explained) {
|
|
903
|
+
out.push(`顶部 totals.${k}=${topN === null ? '未接入' : 0} 与表格里的 ${n} 个同类 (${tableRows} 行) 矛盾, 且没有任何口径说明`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (topN !== null && n === 0 && tableRows === 0 && topN > 0) {
|
|
907
|
+
out.push(`顶部 totals.${k}=${topN} 而表格一行都没有 (${tableRows} 行) —— 反向矛盾: 数不出行却报了数`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
// ④ 明说"数字不一样"时必须有解释 (老规矩保留; 不许只靠一句 label)
|
|
912
|
+
if (ts.differs_from_activity === true) {
|
|
913
|
+
const notes = Array.isArray(snap.notes) ? snap.notes.join(' ') : '';
|
|
914
|
+
if (!notes.includes('口径'))
|
|
915
|
+
out.push('totals_scope.differs_from_activity=true 但 notes 里没有任何口径说明');
|
|
916
|
+
}
|
|
917
|
+
return out;
|
|
918
|
+
}
|
|
761
919
|
/** open_tasks 行的字段白名单 (与 `OpenTaskRow` 逐字一致; 多一个键 = 可能把正文/身份带出去了) */
|
|
762
920
|
export const OPEN_TASK_KEYS = ['capability', 'budget', 'currency', 'network', 'deadline', 'claimed', 'announcementId'];
|
|
763
921
|
/**
|
|
@@ -798,26 +956,85 @@ export function openTasksIssues(snap) {
|
|
|
798
956
|
});
|
|
799
957
|
return out;
|
|
800
958
|
}
|
|
959
|
+
// ── 顶部计数的逐字段口径 (2026-09-24) ────────────────────────────────────────
|
|
960
|
+
// 页面把 8~9 个数字并排放在**一行**里, 一行一个总口径是解释不了它们的 —— 每个数必须自带来源。
|
|
961
|
+
const WINDOW_HOURS = Math.round(PULSE_LIMITS.windowMs / 3600000);
|
|
962
|
+
/** 「本节点 N h 脉冲事件」口径 (节点/智能体类计数, 以及链上索引不可用时的降级口径) */
|
|
963
|
+
function pulseField(what) {
|
|
964
|
+
return {
|
|
965
|
+
source: 'pulse-events',
|
|
966
|
+
window: 'window-24h',
|
|
967
|
+
short: { zh: `${WINDOW_HOURS}h 脉冲`, en: `${WINDOW_HOURS}h pulse` },
|
|
968
|
+
label: {
|
|
969
|
+
zh: `本节点 ${WINDOW_HOURS}h 观察窗口内收到的脉冲事件 · ${what.zh}`,
|
|
970
|
+
en: `pulse events received by this node within the ${WINDOW_HOURS}h window · ${what.en}`,
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/** 「链上索引 · 全量」口径 (与下方活动表**同源**: 同源即恒等, 页面不许并排两个数) */
|
|
975
|
+
function chainField(what) {
|
|
976
|
+
return {
|
|
977
|
+
source: 'chain-index',
|
|
978
|
+
window: 'full',
|
|
979
|
+
short: { zh: '链上索引·全量', en: 'chain index · whole' },
|
|
980
|
+
label: {
|
|
981
|
+
zh: `链上索引全量 (与下方活动表同源) · ${what.zh}`,
|
|
982
|
+
en: `chain index, whole (same source as the activity table below) · ${what.en}`,
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
/** 「本机签名审计账」口径 (真源: `<home>/.bolloon/wallet-signatures.jsonl` 窗口内条数) */
|
|
987
|
+
function signatureAuditField() {
|
|
988
|
+
return {
|
|
989
|
+
source: 'signature-audit',
|
|
990
|
+
window: 'window-24h',
|
|
991
|
+
short: { zh: `${WINDOW_HOURS}h 签名审计`, en: `${WINDOW_HOURS}h signature audit` },
|
|
992
|
+
label: {
|
|
993
|
+
zh: `本机签名审计账 (wallet-signatures.jsonl) ${WINDOW_HOURS}h 内条数 —— 只数条数, 不含签名内容`,
|
|
994
|
+
en: `rows in this node's signature audit log (wallet-signatures.jsonl) within ${WINDOW_HOURS}h — count only, no content`,
|
|
995
|
+
},
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
/** 「没有可用源」= 未接入 (值必须是 null; 页面必须写「未接入」, 不许拿 0 冒充「没发生过」) */
|
|
999
|
+
function noneField(why) {
|
|
1000
|
+
return {
|
|
1001
|
+
source: 'none',
|
|
1002
|
+
window: 'unknown',
|
|
1003
|
+
short: { zh: '未接入', en: 'not connected' },
|
|
1004
|
+
label: { zh: `没有可用源 → 报「未接入」而不是 0 · ${why.zh}`, en: `no available source → reported as "not connected", not 0 · ${why.en}` },
|
|
1005
|
+
unavailable: true,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
801
1008
|
export function computeSnapshot(events, opts) {
|
|
802
1009
|
const now = opts.now;
|
|
803
1010
|
const fresh_until = now + PULSE_LIMITS.snapshotTtlMs;
|
|
804
1011
|
if (opts.unavailable) {
|
|
805
1012
|
const emptyRows = [];
|
|
806
1013
|
const gates = normalizeActivityGates(DEFAULT_CONFIRMATIONS);
|
|
1014
|
+
// 观察层不可用 = **没有数** (不是 0): 计数一律 null/0 占位由页面整块隐藏, 口径逐字段标「未接入」。
|
|
1015
|
+
const downField = noneField({ zh: '观察层不可用', en: 'observation layer unavailable' });
|
|
807
1016
|
return {
|
|
808
1017
|
status: 'unavailable',
|
|
809
1018
|
generated_at: now,
|
|
810
1019
|
fresh_until,
|
|
811
1020
|
scope: 'observed',
|
|
812
1021
|
scope_label: { zh: '当前节点观察到', en: 'Observed by this node' },
|
|
813
|
-
totals: {
|
|
1022
|
+
totals: {
|
|
1023
|
+
nodes: 0, agents: 0, active_agents: 0, seen_last_24h: 0,
|
|
1024
|
+
tasks: null, tasks_completed: null, tasks_verified: null, signatures: null, tasks_settled: null,
|
|
1025
|
+
},
|
|
814
1026
|
totals_scope: {
|
|
815
1027
|
source: 'pulse-events', window_ms: PULSE_LIMITS.windowMs,
|
|
816
1028
|
label: {
|
|
817
|
-
zh:
|
|
818
|
-
en: `
|
|
1029
|
+
zh: `本节点 ${WINDOW_HOURS}h 观察窗口的脉冲事件层不可用 → 没有数 (不是 0)`,
|
|
1030
|
+
en: `This node's ${WINDOW_HOURS}h pulse layer is unavailable → no counts (not zero)`,
|
|
819
1031
|
},
|
|
820
1032
|
differs_from_activity: false,
|
|
1033
|
+
fields: {
|
|
1034
|
+
nodes: downField, agents: downField, active_agents: downField, seen_last_24h: downField,
|
|
1035
|
+
tasks: downField, tasks_completed: downField, tasks_verified: downField,
|
|
1036
|
+
tasks_settled: downField, signatures: downField,
|
|
1037
|
+
},
|
|
821
1038
|
},
|
|
822
1039
|
capabilities: [],
|
|
823
1040
|
recent_activity: [],
|
|
@@ -859,10 +1076,13 @@ export function computeSnapshot(events, opts) {
|
|
|
859
1076
|
capabilities.push({ key: 'other', count: otherCount.size });
|
|
860
1077
|
capabilities.sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1));
|
|
861
1078
|
}
|
|
862
|
-
//
|
|
1079
|
+
// 经济计数 (**脉冲口径**) —— 注意: 顶部「任务/已完成/已结算」的**权威源是链上索引**
|
|
1080
|
+
// (见下面 chainAuthoritative 那一段), 脉冲口径只在索引不可用时才顶上去。
|
|
1081
|
+
// 按**不同任务**去重 (同一任务重复事件不虚增), 且只给聚合数不给内容
|
|
863
1082
|
const tasks = new Set();
|
|
864
1083
|
const tasksCompleted = new Set();
|
|
865
1084
|
const tasksVerified = new Set();
|
|
1085
|
+
const tasksSettledPulse = new Set();
|
|
866
1086
|
for (const e of window) {
|
|
867
1087
|
const t = e.taskProof;
|
|
868
1088
|
if (!t)
|
|
@@ -871,6 +1091,8 @@ export function computeSnapshot(events, opts) {
|
|
|
871
1091
|
tasks.add(t);
|
|
872
1092
|
if (e.type === 'task_completed' || e.type === 'trade_settled')
|
|
873
1093
|
tasksCompleted.add(t);
|
|
1094
|
+
if (e.type === 'trade_settled')
|
|
1095
|
+
tasksSettledPulse.add(t);
|
|
874
1096
|
if (e.type === 'trade_verified')
|
|
875
1097
|
tasksVerified.add(t);
|
|
876
1098
|
}
|
|
@@ -895,15 +1117,62 @@ export function computeSnapshot(events, opts) {
|
|
|
895
1117
|
// 同源计数 + 链归属: **输入就是上表那批行**, 所以不可能与行数/链 id 打架
|
|
896
1118
|
const activity_totals = summarizeActivityRows(activity.rows, { source: activity.source, gates: activity.gates });
|
|
897
1119
|
const chain_id_scope = buildChainIdScope(activity.rows);
|
|
1120
|
+
// ★ 2026-09-24 (leo: 「数量怎么对不上, 尤其是后面的任务和钱包」):
|
|
1121
|
+
// 顶部任务类计数的**权威源改成链上索引** —— 它与下面那张表**同源**, 同源即恒等,
|
|
1122
|
+
// 从根上消灭「顶部 0 个任务 / 表格 15 行任务」这种并排矛盾 (不再靠 notes 辩解)。
|
|
1123
|
+
// 链上索引不可用 (降级 pulse-events) 时才退回旧的脉冲口径, 并在 fields 里如实标出降级。
|
|
1124
|
+
const chainAuthoritative = activity.source === 'chain-index';
|
|
1125
|
+
const totalsTasks = chainAuthoritative ? activity_totals.tasks : tasks.size;
|
|
1126
|
+
const totalsCompleted = chainAuthoritative ? activity_totals.tasks_completed : tasksCompleted.size;
|
|
1127
|
+
const totalsSettled = chainAuthoritative ? activity_totals.tasks_settled : tasksSettledPulse.size;
|
|
1128
|
+
// 「验真」在链上索引里**没有**对应事件 → 链上口径下这个数没有源 (null + 未接入), 不拿结算数冒充已验证
|
|
1129
|
+
const totalsVerified = chainAuthoritative ? null : tasksVerified.size;
|
|
1130
|
+
// 钱包签名: 真源 = 本机签名审计账 (窗口内条数, 只计数); 没有审计账 → 退回脉冲事件上报数;
|
|
1131
|
+
// 两个都没有 → null (未接入)。**绝不**在没有源的时候报 0 (那是「没发生过」, 是另一句话)。
|
|
1132
|
+
const sigTally = opts.signatureAudit ?? null;
|
|
1133
|
+
const auditWired = !!sigTally && sigTally.available === true;
|
|
1134
|
+
const totalsSignatures = auditWired
|
|
1135
|
+
? Number(sigTally.count)
|
|
1136
|
+
: (signatureKeys.size > 0 ? signatureKeys.size : null);
|
|
1137
|
+
const fields = {
|
|
1138
|
+
nodes: pulseField({ zh: '不同节点数', en: 'distinct nodes' }),
|
|
1139
|
+
agents: pulseField({ zh: '不同 Agent 数', en: 'distinct agents' }),
|
|
1140
|
+
active_agents: pulseField({ zh: '活跃 Agent 数', en: 'active agents' }),
|
|
1141
|
+
seen_last_24h: pulseField({ zh: '24h 内出现过的不同 Agent', en: 'distinct agents seen within 24h' }),
|
|
1142
|
+
tasks: chainAuthoritative
|
|
1143
|
+
? chainField({ zh: '有链上活动的不同任务', en: 'distinct tasks with on-chain activity' })
|
|
1144
|
+
: pulseField({ zh: '观察到发起的任务', en: 'tasks observed as created' }),
|
|
1145
|
+
tasks_completed: chainAuthoritative
|
|
1146
|
+
? chainField({ zh: '交付完成 (ProofSubmittedV2) 的不同任务', en: 'distinct tasks with ProofSubmittedV2' })
|
|
1147
|
+
: pulseField({ zh: '交付完成的任务', en: 'tasks observed as completed' }),
|
|
1148
|
+
tasks_settled: chainAuthoritative
|
|
1149
|
+
? chainField({ zh: '托管结算 (Released/Refunded/Expired/Disputed) 的不同任务', en: 'distinct tasks with an on-chain escrow settlement' })
|
|
1150
|
+
: pulseField({ zh: '链上口径结算的任务', en: 'tasks with on-chain settlement' }),
|
|
1151
|
+
tasks_verified: chainAuthoritative
|
|
1152
|
+
? noneField({ zh: '链上索引没有「验真」事件', en: 'the chain index has no verification event' })
|
|
1153
|
+
: pulseField({ zh: '真验真 (trade_verified) 的任务', en: 'tasks truly verified (trade_verified)' }),
|
|
1154
|
+
signatures: auditWired
|
|
1155
|
+
? signatureAuditField()
|
|
1156
|
+
: (signatureKeys.size > 0
|
|
1157
|
+
? pulseField({ zh: '上报过的钱包签名 (按来源+时刻去重)', en: 'reported wallet signatures (deduped by source+time)' })
|
|
1158
|
+
: noneField({ zh: '本节点既没有签名审计账, 也没有签名脉冲事件', en: 'this node has neither a signature audit log nor signature pulse events' })),
|
|
1159
|
+
};
|
|
898
1160
|
const totals_scope = {
|
|
899
|
-
source: 'pulse-events',
|
|
1161
|
+
source: chainAuthoritative ? 'mixed' : 'pulse-events',
|
|
900
1162
|
window_ms: PULSE_LIMITS.windowMs,
|
|
901
1163
|
label: {
|
|
902
|
-
zh:
|
|
903
|
-
|
|
1164
|
+
zh: chainAuthoritative
|
|
1165
|
+
? `节点/智能体 = 本节点 ${WINDOW_HOURS}h 脉冲事件; 任务/已完成/已结算 = 链上索引全量 (与下表同源); 钱包签名 = 本机签名审计`
|
|
1166
|
+
: `只统计本节点 ${WINDOW_HOURS}h 观察窗口内收到的脉冲事件 (本节点自己上报的)`,
|
|
1167
|
+
en: chainAuthoritative
|
|
1168
|
+
? `nodes/agents = this node's ${WINDOW_HOURS}h pulse events; tasks/completed/settled = chain index, whole (same source as the table below); wallet signatures = this node's signature audit log`
|
|
1169
|
+
: `Only pulse events received by this node within the ${WINDOW_HOURS}h observation window (reported by this node itself)`,
|
|
904
1170
|
},
|
|
905
1171
|
differs_from_activity: activity_totals.rows > 0 &&
|
|
906
|
-
(
|
|
1172
|
+
(totalsTasks !== activity_totals.tasks ||
|
|
1173
|
+
totalsCompleted !== activity_totals.tasks_completed ||
|
|
1174
|
+
totalsSettled !== activity_totals.tasks_settled),
|
|
1175
|
+
fields,
|
|
907
1176
|
};
|
|
908
1177
|
if (activity.source === 'chain-index') {
|
|
909
1178
|
notes.push(`confirmed_activity 来自链上索引 (chain-index): ${activity_totals.rows} 行 · ${activity_totals.tasks} 个不同任务 · ` +
|
|
@@ -916,10 +1185,21 @@ export function computeSnapshot(events, opts) {
|
|
|
916
1185
|
}
|
|
917
1186
|
// 两套计数口径不同时**必须**解释 —— 页面不许出现「0 个任务」与「N 行任务」并存而不解释
|
|
918
1187
|
if (totals_scope.differs_from_activity) {
|
|
919
|
-
notes.push(`口径不同, 不是数据丢失:
|
|
920
|
-
|
|
921
|
-
`
|
|
922
|
-
|
|
1188
|
+
notes.push(`口径不同, 不是数据丢失: 链上索引不可用 → 顶部任务类计数**降级为脉冲口径** (本快照 tasks=${tasks.size} · ` +
|
|
1189
|
+
`tasks_completed=${tasksCompleted.size} · tasks_verified=${tasksVerified.size} · tasks_settled=${tasksSettledPulse.size} · ` +
|
|
1190
|
+
`signatures=${signatureKeys.size}); 上表 ${activity_totals.rows} 行来自脉冲事件 (同源计数见 activity_totals, ` +
|
|
1191
|
+
`不是链上索引全量) —— 每个数的来源见 totals_scope.fields`);
|
|
1192
|
+
}
|
|
1193
|
+
if (chainAuthoritative) {
|
|
1194
|
+
notes.push(`顶部「任务/已完成/已结算」= 链上索引的**同源计数** (= activity_totals, 与下表同源, 同源即恒等): ` +
|
|
1195
|
+
`tasks=${totalsTasks} · tasks_completed=${totalsCompleted} · tasks_settled=${totalsSettled}; ` +
|
|
1196
|
+
`「节点/智能体」= 本节点 ${WINDOW_HOURS}h 脉冲事件; 「已验证」在链上索引里没有对应事件 → ` +
|
|
1197
|
+
`该口径无对应事件源, 不下发该字段 (不是 0); 「钱包签名」= ${auditWired
|
|
1198
|
+
? `本机签名审计账 ${totalsSignatures} 条 (${WINDOW_HOURS}h 窗口, 只计数)`
|
|
1199
|
+
: (totalsSignatures === null
|
|
1200
|
+
? '无可用源 (本节点既没有签名审计账, 也没有签名脉冲事件), 不下发该字段'
|
|
1201
|
+
: `脉冲事件上报 ${totalsSignatures} 条`)} — ` +
|
|
1202
|
+
`每个数的来源见 totals_scope.fields`);
|
|
923
1203
|
}
|
|
924
1204
|
if (activity_totals.rows > 0)
|
|
925
1205
|
notes.push(chain_id_scope.note.zh);
|
|
@@ -936,10 +1216,12 @@ export function computeSnapshot(events, opts) {
|
|
|
936
1216
|
agents: agents.size,
|
|
937
1217
|
active_agents: active.size,
|
|
938
1218
|
seen_last_24h: agents.size, // 24h 窗口内的不同 Agent
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1219
|
+
// ★ 任务类计数 = 链上索引同源值 (与下表恒等); 索引不可用才退回脉冲口径 (见 fields.tasks)
|
|
1220
|
+
tasks: totalsTasks,
|
|
1221
|
+
tasks_completed: totalsCompleted,
|
|
1222
|
+
tasks_verified: totalsVerified, // 链上口径下 = null (未接入), 不冒充 0
|
|
1223
|
+
signatures: totalsSignatures, // 真源 = 本机签名审计账; 无源 = null (未接入)
|
|
1224
|
+
tasks_settled: totalsSettled, // 新增字段一律排最后 (老 8 个字段顺序逐字不变)
|
|
943
1225
|
},
|
|
944
1226
|
totals_scope,
|
|
945
1227
|
capabilities,
|
|
@@ -966,6 +1248,7 @@ export async function getNetworkPulse(opts = {}) {
|
|
|
966
1248
|
const shapeOk = Array.isArray(cached?.confirmed_activity)
|
|
967
1249
|
&& typeof cached?.confirmed_activity_source === 'string'
|
|
968
1250
|
&& !!cached?.totals_scope
|
|
1251
|
+
&& !!cached?.totals_scope?.fields // 老缓存没有逐字段口径 → 过期形状, 重算
|
|
969
1252
|
&& !!cached?.activity_totals
|
|
970
1253
|
&& !!cached?.chain_id_scope
|
|
971
1254
|
&& Array.isArray(cached?.open_tasks); // 老缓存没有待接单任务 → 过期形状, 重算
|
|
@@ -983,8 +1266,11 @@ export async function getNetworkPulse(opts = {}) {
|
|
|
983
1266
|
}
|
|
984
1267
|
// 活动行优先取 P5 链上索引 (真链上事实); 索引不可用 → 退回脉冲事件, 并在快照里标出来源
|
|
985
1268
|
const confirmedActivity = await resolveConfirmedActivity({ home: opts.home, events, now });
|
|
1269
|
+
// 钱包签名的**真源**: 本机签名审计账 (窗口内条数)。没有账 → 注入 available:false → 快照标「未接入」
|
|
1270
|
+
// (不拿 0 冒充「没发生过」: 本机明明签过而脉冲事件丢了的那次真事故就是这么来的)
|
|
1271
|
+
const signatureAudit = tallySignatureAudit(opts.home, { now, windowMs: PULSE_LIMITS.windowMs });
|
|
986
1272
|
// 待接单任务: 只读本机公告板目录 (未认领且未过期), 投影成白名单 7 字段 —— 与脉冲事件层无关
|
|
987
|
-
const snap = computeSnapshot(events, { now, confirmedActivity, openTasks: readOpenTasks(opts.home, now) });
|
|
1273
|
+
const snap = computeSnapshot(events, { now, confirmedActivity, openTasks: readOpenTasks(opts.home, now), signatureAudit });
|
|
988
1274
|
try {
|
|
989
1275
|
fs.mkdirSync(pulseDir(opts.home), { recursive: true });
|
|
990
1276
|
fs.writeFileSync(snapshotFile(opts.home), JSON.stringify(snap), 'utf8');
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -29,7 +29,7 @@ import { DeepThinkingEngine, AgentCoordinator } from '@bolloon/constraint-runtim
|
|
|
29
29
|
import { WorkflowPivotLoop, createDefaultPivotConfig } from './workflow-pivot-loop.js';
|
|
30
30
|
import { initDocumentReceiver } from './p2p-document-tools.js';
|
|
31
31
|
import { startRun, recordStep, finishRun, readRun, budgetVerdict, recordDegradation, recordHarnessEvent, recordRecovery, setRunStatus, prepareResume, markRunRunning, buildResumeInstruction, argsDigestOf, repeatedFailureCount, classifyError as classifyRunError } from './run-store.js';
|
|
32
|
-
import { createGoal,
|
|
32
|
+
import { createGoal, attachRun, findActiveGoal } from './goal-store.js';
|
|
33
33
|
import { PiAgentHarness } from './pi-harness.js';
|
|
34
34
|
import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
|
|
35
35
|
import { SkillRegistry } from '@bolloon/constraint-runtime';
|
|
@@ -757,6 +757,30 @@ export class PiAgentSession {
|
|
|
757
757
|
status: 'needs_human',
|
|
758
758
|
error: 'LLM 不可用: provider 未初始化或无 apiKey (fallback 不是执行结果, 需要配置或人工处理)',
|
|
759
759
|
});
|
|
760
|
+
// ★ M0: "工具/权限不可用"也是一条终止路径 → 同样进收尾漏斗 (规则 ④)。
|
|
761
|
+
const wiring = await import('./goal-flywheel-wiring.js');
|
|
762
|
+
const closed = await wiring.closeRunOnce({
|
|
763
|
+
goalId: boundGoalId,
|
|
764
|
+
runId: rec.runId,
|
|
765
|
+
caller: 'runner',
|
|
766
|
+
now: new Date().toISOString(),
|
|
767
|
+
finalReview: `LLM 不可用 → fallback 收尾 (不是执行结果): ${response}`,
|
|
768
|
+
});
|
|
769
|
+
if (!wiring.isRefusal(closed) && closed.outcome) {
|
|
770
|
+
const { reduceGoalState } = await import('./goal-state-reducer.js');
|
|
771
|
+
await reduceGoalState({
|
|
772
|
+
goalId: boundGoalId,
|
|
773
|
+
intent: 'closure_outcome',
|
|
774
|
+
now: new Date().toISOString(),
|
|
775
|
+
by: 'runner',
|
|
776
|
+
outcome: {
|
|
777
|
+
goalStatus: wiring.goalStatusFromDecision(closed.outcome.decision),
|
|
778
|
+
continuation: wiring.toGoalStoreContinuation(closed.outcome.continuation),
|
|
779
|
+
reason: closed.outcome.decision.reason,
|
|
780
|
+
},
|
|
781
|
+
runId: rec.runId,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
760
784
|
console.log(`[PiAgent] LLM 不可用 → 本次仍落 Run ${rec.runId} (needs_human), 不伪装成正常执行`);
|
|
761
785
|
}
|
|
762
786
|
catch (err) {
|
|
@@ -2341,30 +2365,61 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2341
2365
|
await recordDegradation({ kind: 'core', op: 'pi-sdk.finishRun', runId: this.currentRunId, message }).catch(() => { });
|
|
2342
2366
|
onStream?.({ type: 'error', content: `⚠️ ${message}`, tool: 'harness' });
|
|
2343
2367
|
}
|
|
2344
|
-
// 2026-09-
|
|
2368
|
+
// ★ 2026-09-25 (M0 接线冻结, 规则 ③ + ④): Run 结束**必过收尾漏斗**。
|
|
2369
|
+
//
|
|
2370
|
+
// 旧写法在这里另起了一套 Goal 侧收尾 (读证据 → `evaluateGoalCompletion` →
|
|
2371
|
+
// `completeGoalIfEligible` / `setUnresolved`): 它**不收尾** —— 不写 Memory、不生成 Skill 候选、
|
|
2372
|
+
// 不写权威 continuation、不留决策记录。于是同一条 Run 在 Supervisor 那里还会再收一次 (或反过来
|
|
2373
|
+
// 这条路径根本不收), 全仓就有了两套"这一轮跑完意味着什么"。那一套已删除。
|
|
2374
|
+
//
|
|
2375
|
+
// 现在: `closeRunOnce` (幂等) → 9 步收尾 → 产物落盘; 再由 Goal reducer 把"下一步是什么"
|
|
2376
|
+
// 落进 Goal (没有 Supervisor 宿主时 Runner 也要留下 continuation, 否则下一次还是从零开始)。
|
|
2345
2377
|
if (this.currentGoalId) {
|
|
2346
2378
|
try {
|
|
2347
|
-
const
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2379
|
+
const wiring = await import('./goal-flywheel-wiring.js');
|
|
2380
|
+
const { reduceGoalState } = await import('./goal-state-reducer.js');
|
|
2381
|
+
const nowIso = new Date().toISOString();
|
|
2382
|
+
const closed = await wiring.closeRunOnce({
|
|
2383
|
+
goalId: this.currentGoalId,
|
|
2384
|
+
runId: this.currentRunId,
|
|
2385
|
+
caller: 'runner',
|
|
2386
|
+
now: nowIso,
|
|
2387
|
+
finalReview: finalResponse ? String(finalResponse).slice(0, 2000) : '',
|
|
2388
|
+
});
|
|
2389
|
+
if (wiring.isRefusal(closed)) {
|
|
2390
|
+
onStream?.({ type: 'status', content: `⚠️ 收尾被拒 (不当作收过): ${closed.reason}`, tool: 'harness' });
|
|
2391
|
+
}
|
|
2392
|
+
else if (closed.outcome) {
|
|
2393
|
+
const view = closed.outcome;
|
|
2394
|
+
const applied = await reduceGoalState({
|
|
2395
|
+
goalId: this.currentGoalId,
|
|
2396
|
+
intent: 'closure_outcome',
|
|
2397
|
+
now: nowIso,
|
|
2398
|
+
by: 'runner',
|
|
2399
|
+
outcome: {
|
|
2400
|
+
goalStatus: wiring.goalStatusFromDecision(view.decision),
|
|
2401
|
+
continuation: wiring.toGoalStoreContinuation(view.continuation),
|
|
2402
|
+
reason: view.decision.reason,
|
|
2403
|
+
},
|
|
2404
|
+
runId: view.runId,
|
|
2405
|
+
});
|
|
2406
|
+
const line = view.decision.decision === 'complete'
|
|
2407
|
+
? `🎯 目标已达成 (仍要过完成门): ${view.decision.reason}`
|
|
2408
|
+
: `🎯 目标仍在进行 (未判完成): ${view.decision.reason}`;
|
|
2409
|
+
onStream?.({
|
|
2410
|
+
type: 'status',
|
|
2411
|
+
content: `${line}${applied.gateRejected ? ` [完成门拒绝: ${applied.gateRejected}]` : ''} `
|
|
2412
|
+
+ `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.currentGoalId})`,
|
|
2413
|
+
tool: 'harness',
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
else {
|
|
2417
|
+
// 幂等: 别处 (或上一次) 已经收过尾 —— 事实读不回来就如实说, 不重收也不假装
|
|
2418
|
+
onStream?.({ type: 'status', content: `🎯 ${closed.reason}`, tool: 'harness' });
|
|
2364
2419
|
}
|
|
2365
2420
|
}
|
|
2366
2421
|
catch (err) {
|
|
2367
|
-
await recordDegradation({ kind: 'observational', op: 'pi-sdk.
|
|
2422
|
+
await recordDegradation({ kind: 'observational', op: 'pi-sdk.runClosure', runId: this.currentRunId, message: String(err?.message || err).slice(0, 160) }).catch(() => { });
|
|
2368
2423
|
}
|
|
2369
2424
|
}
|
|
2370
2425
|
this.currentRunId = '';
|
package/dist/agents/run-store.js
CHANGED
|
@@ -24,14 +24,14 @@ export const RUN_TRANSITIONS = {
|
|
|
24
24
|
queued: ['running', 'aborted', 'interrupted'],
|
|
25
25
|
running: ['recovering', 'paused', 'awaiting_external', 'done', 'failed', 'aborted', 'interrupted', 'stalled', 'needs_human'],
|
|
26
26
|
recovering: ['running', 'failed', 'aborted', 'needs_human', 'interrupted', 'stalled'],
|
|
27
|
-
paused: ['running', 'aborted', 'interrupted'],
|
|
28
|
-
awaiting_external: ['running', 'failed', 'aborted', 'interrupted', 'stalled'],
|
|
27
|
+
paused: ['running', 'aborted', 'interrupted', 'recovering'],
|
|
28
|
+
awaiting_external: ['running', 'failed', 'aborted', 'interrupted', 'stalled', 'recovering'],
|
|
29
29
|
done: [],
|
|
30
30
|
failed: [],
|
|
31
31
|
aborted: [],
|
|
32
32
|
interrupted: ['recovering', 'aborted'], // 允许"从 checkpoint 恢复"
|
|
33
33
|
stalled: ['recovering', 'aborted', 'needs_human'],
|
|
34
|
-
needs_human: ['running', 'aborted'],
|
|
34
|
+
needs_human: ['running', 'aborted', 'recovering'],
|
|
35
35
|
};
|
|
36
36
|
export function canTransition(from, to) {
|
|
37
37
|
if (from === to)
|
|
@@ -572,6 +572,13 @@ function pidAlive(pid) {
|
|
|
572
572
|
return e?.code === 'EPERM';
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
|
+
let onRunTerminal = null;
|
|
576
|
+
export function setOnRunTerminal(hook) {
|
|
577
|
+
onRunTerminal = hook;
|
|
578
|
+
}
|
|
579
|
+
export function getOnRunTerminal() {
|
|
580
|
+
return onRunTerminal;
|
|
581
|
+
}
|
|
575
582
|
/**
|
|
576
583
|
* 孤儿对账: 启动时把 pid 已死的 running 记录改判 interrupted。
|
|
577
584
|
* 不做这一层的话, 重载后 UI/CLI 会显示"还在跑"的幽灵运行 —— 那是最典型的假状态。
|
|
@@ -588,11 +595,14 @@ export async function reconcileOrphans() {
|
|
|
588
595
|
continue;
|
|
589
596
|
}
|
|
590
597
|
try {
|
|
591
|
-
await finishRun(r.runId, {
|
|
598
|
+
const closed = await finishRun(r.runId, {
|
|
592
599
|
status: 'interrupted',
|
|
593
600
|
error: `进程 ${r.pid} 已不在 (刷新/重载/崩溃); 运行到此中断`,
|
|
594
601
|
});
|
|
595
602
|
interrupted.push(r.runId);
|
|
603
|
+
// 终止路径进唯一责任链 (拿不到 Goal 就不收, 不假装收过)
|
|
604
|
+
if (onRunTerminal)
|
|
605
|
+
await Promise.resolve(onRunTerminal(closed || r)).catch(() => null);
|
|
596
606
|
}
|
|
597
607
|
catch (err) {
|
|
598
608
|
failed.push(r.runId);
|
|
@@ -615,11 +625,14 @@ export async function superviseRuns(now = Date.now()) {
|
|
|
615
625
|
continue; // 交给 reconcileOrphans
|
|
616
626
|
if (now - Date.parse(r.updatedAt) > cfg.staleMs) {
|
|
617
627
|
try {
|
|
618
|
-
await finishRun(r.runId, {
|
|
628
|
+
const closed = await finishRun(r.runId, {
|
|
619
629
|
status: 'stalled',
|
|
620
630
|
error: `超过 ${Math.round(cfg.staleMs / 1000)}s 没有新进展 (可能是工具卡住或模型长时间无响应)`,
|
|
621
631
|
});
|
|
622
632
|
stalled.push(r.runId);
|
|
633
|
+
// 同上: 失速也是一条终止路径
|
|
634
|
+
if (onRunTerminal)
|
|
635
|
+
await Promise.resolve(onRunTerminal(closed || r)).catch(() => null);
|
|
623
636
|
}
|
|
624
637
|
catch (err) {
|
|
625
638
|
failed.push(r.runId);
|