clearai-dsh 0.1.2 → 0.1.4
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 +68 -0
- package/README.md +28 -24
- package/README.zh-CN.md +28 -24
- package/bin/clearai.mjs +128 -12
- package/lib/client.js +95 -95
- package/lib/fold.js +151 -66
- package/lib/host.js +20 -18
- package/package.json +9 -3
- package/presets/clearai/agent.cordis.yml +107 -36
- package/presets/clearai/plugins/brain.js +2 -2
- package/presets/clearai/plugins/clearai-kernel.js +552 -284
- package/presets/clearai/plugins/commands.js +199 -0
- package/presets/clearai/plugins/ontology.js +3 -3
- package/presets/clearai/plugins/prompts.js +51 -25
|
@@ -59,7 +59,7 @@ const SELF_JUDGE_MAX_INDEX = 2
|
|
|
59
59
|
const MAX_PLAN_STEPS = 25
|
|
60
60
|
/**
|
|
61
61
|
* `_DEFAULT_BLOCKED_THRESHOLD`:连续未过闸达阈值 → 计划置 blocked,等人。
|
|
62
|
-
*
|
|
62
|
+
* 它**不按档取值**:它是证据质量闸,不是预算,
|
|
63
63
|
* 人在不在场都得先过闸。部署想要「人就在旁边,早点回来问」,就在配置面写小一点(现在是 2)。
|
|
64
64
|
*/
|
|
65
65
|
const DEFAULT_BLOCKED_THRESHOLD = 3
|
|
@@ -71,7 +71,7 @@ const MIN_BRIEF_CHARS = 280
|
|
|
71
71
|
*/
|
|
72
72
|
const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url))
|
|
73
73
|
/**
|
|
74
|
-
* 模板从哪来(
|
|
74
|
+
* 模板从哪来(打包纪律:**发行物里不许出现仓库路径**)。
|
|
75
75
|
*
|
|
76
76
|
* 原来这里有一条 fallback 指回 ClearAI 仓库里那份工作区模板(同一份模板的第二个位置)——
|
|
77
77
|
* 在 dev checkout 里很方便,**但它把仓库结构写进了发行物**:
|
|
@@ -306,7 +306,7 @@ function collectText(output) {
|
|
|
306
306
|
/**
|
|
307
307
|
* 子 run 的结局 —— **必须过这里**,别各自 `then(v => ok:true)`。
|
|
308
308
|
*
|
|
309
|
-
*
|
|
309
|
+
* 一个修过的 bug:中断一次跑动之后,状态里**所有子代理都显示"执行完成"**。
|
|
310
310
|
* 根因:DSH 里「中断 / 报错」**不是 reject**——`run.result` 照常 resolve,只是
|
|
311
311
|
* `stopReason` 变成 `aborted` / `error` / `interrupted`(`dsh-subagent` 的
|
|
312
312
|
* `settleSubagent` 就是这么落的)。只按 reject 判失败,就会把被打断的侦察记成"完成"、
|
|
@@ -390,7 +390,7 @@ const OUTPUT_SCHEMA = {
|
|
|
390
390
|
*
|
|
391
391
|
* 两份实现必须同格式,所以不是靠人记住,而是靠测试:
|
|
392
392
|
* `test/kernel.test.mjs` 拿同一批消息喂两个解析器,断言结论一致。
|
|
393
|
-
*
|
|
393
|
+
* 这份白名单不能是**抄在闭包里的私本**——
|
|
394
394
|
* 改标记要改两处、忘了就静默失效。现在它在模块层、被导出,漏一处测试就红。
|
|
395
395
|
*/
|
|
396
396
|
export const HUMAN_GATE_MARK = '[clearai·人门]'
|
|
@@ -398,7 +398,7 @@ export const HUMAN_GATE_MARK = '[clearai·人门]'
|
|
|
398
398
|
* 人门**动词白名单**(与宿主半 `fold.js` 的同名表逐字一致 —— 两边不 import 对方,
|
|
399
399
|
* 靠 `test/kernel.test.mjs` 的等价性用例钉住)。
|
|
400
400
|
*
|
|
401
|
-
*
|
|
401
|
+
* 两侧白名单一旦各自维护,摘动词时只改一侧 ⇒ 内核仍认、宿主不认 ⇒ **等价性用例立刻红** ✓。
|
|
402
402
|
* 这正是那条用例存在的意义:两份实现漂移不许静默。
|
|
403
403
|
*/
|
|
404
404
|
export const HUMAN_GATE_ACTIONS = ['adopt_branch', 'abandon_fork', 'promote_skill']
|
|
@@ -490,28 +490,17 @@ const TOOL_CATALOG = new Set(Object.values(MECHANISM_TOOLS).flat())
|
|
|
490
490
|
const AUTONOMY_VARIANTS = new Set(Object.values(SECTION_SLOTS).flatMap((variants) => Object.values(variants)))
|
|
491
491
|
|
|
492
492
|
/**
|
|
493
|
-
*
|
|
493
|
+
* 续跑轮数:一个**保险丝**,不是用户的档位(「预算档」这个概念已拆,奥卡姆)。
|
|
494
494
|
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
495
|
+
* 历史上这里是 `{attended: 6, unattended: 512}`,由面板上「多问我 / 自己跑」那个开关选。
|
|
496
|
+
* 从第一性原理看错了两次:
|
|
497
|
+
* · 「我要不要在场」是**运行时状态**(有没有门开着、有没有裁决在飞、有没有开着的步),
|
|
498
|
+
* 不是**配置项**——它现在由 `turnDemand` 从门状态算出来,不看档;
|
|
499
|
+
* · 那一档还顺手把「计划经人确认」变成系统自己签的 ✗。
|
|
498
500
|
*
|
|
499
|
-
* 2026-09-11 拆掉「预算档」这个概念(奥卡姆,用户拍板),三件事一起说清:
|
|
500
|
-
* · **执行它的本来就是原生**——`maxAutoTurns` → 宿主目标的 `maxGoalRounds`,到限由
|
|
501
|
-
* `dsh-goal-round-driver` 自己 `block(code='round-limit')`。我们只负责给数字,不造机制;
|
|
502
|
-
* · `tokenBudget`(8M / 32M)**删掉**:它从未被执行(上下文预算本来就是原生
|
|
503
|
-
* `dsh-token-meter` + `dsh-compaction-basic` 的活),留着的两个死数字只会让人以为
|
|
504
|
-
* 「这里还有个额度旋钮」——不可执行的旋钮不该出现在任何清单上;
|
|
505
|
-
* · `blockedThreshold`(连拦阈值)**移出**这个概念:它是观察准入的**质量闸**,不是预算。
|
|
506
|
-
* 现在是一个独立旋钮(默认 2),不再按档取值。
|
|
507
|
-
*/
|
|
508
|
-
/**
|
|
509
|
-
* 续跑窗口的**额度**(§34):一个保险丝,不是用户的档位。
|
|
510
|
-
*
|
|
511
|
-
* 原先这里是 `{attended: 6, unattended: 512}` —— 由面板上「多问我 / 自己跑」那个开关选。
|
|
512
|
-
* 从第一性原理看:「我要不要在场」是**运行时状态**(有没有门开着、有没有裁决在飞、有没有开着的步),
|
|
513
|
-
* 不是**配置项** ✗;而且那一档还顺手把「计划经人确认」变成系统自己签的 ✗(见 §34)。
|
|
514
501
|
* 现在只剩一个默认值:够长到能跑完一件真活,又短到不会无声烧掉一整夜;要更长由人显式表达。
|
|
502
|
+
* 执行它的是**原生**——`maxAutoTurns` → 宿主目标的 `maxGoalRounds`,到限由
|
|
503
|
+
* `dsh-goal-round-driver` 自己 `block(code='round-limit')`。我们只负责给数字,不造机制。
|
|
515
504
|
*/
|
|
516
505
|
const DEFAULT_MAX_AUTO_TURNS = 128
|
|
517
506
|
|
|
@@ -535,7 +524,7 @@ const CONTRIB_DEFAULTS = {
|
|
|
535
524
|
function resolveContributions(contributions, autonomy) {
|
|
536
525
|
const declared = contributions ?? {}
|
|
537
526
|
/**
|
|
538
|
-
*
|
|
527
|
+
* 贡献表**顶层**也走白名单:`budgets` 块删掉之后,旧写法
|
|
539
528
|
* (`contributions: { budgets: {...} }`)必须装配期就炸——否则它会**静默无效**,
|
|
540
529
|
* 而「配了没生效」正是这份移植最想消灭的一类错(与未知机制/未知工具同一条纪律)。
|
|
541
530
|
*/
|
|
@@ -569,7 +558,7 @@ function resolveContributions(contributions, autonomy) {
|
|
|
569
558
|
if (AUTONOMY_VARIANTS.has(entry)) throw new Error(`autonomy_section_must_use_slot:clearai-kernel:${entry}`)
|
|
570
559
|
}
|
|
571
560
|
/**
|
|
572
|
-
*
|
|
561
|
+
* **本体**:贡献表的第八项,也是唯一一项**状态面**(前七项都是行为面)。
|
|
573
562
|
* 三条装配期纪律(逐条对着 ClearAI 本体 P1 的定案):
|
|
574
563
|
* · **一个进程一份本体**:两份声明的合并语义未定义(两个插件各声明一个 hypothesis 算什么?),
|
|
575
564
|
* 所以第二份直接拒;换装 = 换发行清单那一行。
|
|
@@ -585,7 +574,7 @@ function resolveContributions(contributions, autonomy) {
|
|
|
585
574
|
if (problems.length > 0) throw new Error(`invalid_ontology:clearai-kernel:${problems[0]}`)
|
|
586
575
|
ontology = VERIFICATION_LOOP
|
|
587
576
|
}
|
|
588
|
-
//
|
|
577
|
+
// 「预算档」这个概念已拆。轮数与连拦阈值不再是贡献表里的一档,
|
|
589
578
|
// 而是两个普通配置键(maxAutoTurns / blockedThreshold,见 CONFIG_KEYS)——
|
|
590
579
|
// 它们跟「装哪些机制」不是一类事,混在一张清单里只会让人以为关掉机制就得关掉预算。
|
|
591
580
|
return { mechanisms, tools, sections, ontology }
|
|
@@ -619,13 +608,13 @@ export function apply(ctx, config = {}) {
|
|
|
619
608
|
const CFG = {
|
|
620
609
|
/**
|
|
621
610
|
* 连拦阈值:同一件事连续冲闸这么多次没过,计划置 blocked、停下等人。
|
|
622
|
-
*
|
|
611
|
+
* **它不是预算**:它管的是证据质量,
|
|
623
612
|
* 而且不再按档取值——人在不在场都得先过闸。
|
|
624
613
|
*/
|
|
625
614
|
blockedThreshold: config.blockedThreshold ?? DEFAULT_BLOCKED_THRESHOLD,
|
|
626
615
|
/**
|
|
627
|
-
*
|
|
628
|
-
*
|
|
616
|
+
* 续跑轮数上限:显式写了就用它,没写回落到 `DEFAULT_MAX_AUTO_TURNS`(128)。
|
|
617
|
+
* 这里只存「人写没写」,真正的回落发生在布防点(见 `armContinuation`)。
|
|
629
618
|
*/
|
|
630
619
|
maxAutoTurns: config.maxAutoTurns ?? null,
|
|
631
620
|
minBriefChars: config.minBriefChars ?? MIN_BRIEF_CHARS,
|
|
@@ -633,7 +622,7 @@ export function apply(ctx, config = {}) {
|
|
|
633
622
|
/**
|
|
634
623
|
* 收上来的结论「多久没在投影里落地就重收一次」(缺省 2000ms)。
|
|
635
624
|
* 为什么需要它:内存里的 `reported` 只是「我发布过」,不等于**事实已经到了账本上**
|
|
636
|
-
* (
|
|
625
|
+
* (失效模式:侦察结论发布后被丢掉,而条目已删、`reported` 已置位 ⇒ 永久丢)。
|
|
637
626
|
* 判据改成看**投影**:投影里还没落地就再发一次(同 id 的 `scout/settled`/`worldline/executed`
|
|
638
627
|
* 在 fold 里是幂等的,重复发布不会长出第二条事实)。
|
|
639
628
|
*/
|
|
@@ -656,7 +645,7 @@ export function apply(ctx, config = {}) {
|
|
|
656
645
|
// 部署里没有,`tools.restrict` 会**直接抛**(未知工具名)→ 侦察整条路 fail-closed。
|
|
657
646
|
auditToolFilter: config.auditToolFilter ?? ['read', 'glob', 'grep', 'read_image'],
|
|
658
647
|
/** 侦察的只读工具面:读文件、找文件、找内容、查公网——都不能写、不能执行。 */
|
|
659
|
-
// 侦察是**只读**角色,看图也是读:`read_image`
|
|
648
|
+
// 侦察是**只读**角色,看图也是读:`read_image` 必须在这张脸上。
|
|
660
649
|
scoutToolFilter: config.scoutToolFilter ?? ['read', 'glob', 'grep', 'read_image', 'web_search', 'web_fetch'],
|
|
661
650
|
/** 世界线是否物化成 git 分支 + worktree(工作区是 git 仓库时)。关掉就退化成声明的目录。 */
|
|
662
651
|
gitWorldlines: config.gitWorldlines !== false,
|
|
@@ -691,10 +680,12 @@ export function apply(ctx, config = {}) {
|
|
|
691
680
|
forkArbitration: config.forkArbitration !== false,
|
|
692
681
|
runtimeCard: config.runtimeCard !== false,
|
|
693
682
|
/**
|
|
694
|
-
* 人在场 /
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
683
|
+
* 人在场 / 人不在场。**现在只决定一件事**:澄清协议装哪一段(槽位 `clarification`)。
|
|
684
|
+
*
|
|
685
|
+
* 它不再决定续跑(那由 `turnDemand` 从门状态算)、不再决定预算
|
|
686
|
+
* (只有一个 `DEFAULT_MAX_AUTO_TURNS`)、也不再是用户可切换的运行档
|
|
687
|
+
* (`set_autonomy` 已摘除;这里写的是部署初值)。
|
|
688
|
+
* 它不决定人格、不决定工具面——`if mode === ...` 在内核里不该出现。
|
|
698
689
|
*/
|
|
699
690
|
autonomy: config.autonomy === 'unattended' ? 'unattended' : 'attended',
|
|
700
691
|
/** 贡献表。缺省 = 全开;要裁剪就从这里裁,而不是去改装配代码。 */
|
|
@@ -704,7 +695,7 @@ export function apply(ctx, config = {}) {
|
|
|
704
695
|
/** 贡献表:先校验(装配期炸),后登记(见文件末尾的装配段)。 */
|
|
705
696
|
const CONTRIB = resolveContributions(CFG.contributions, CFG.autonomy)
|
|
706
697
|
const SECTION_LIST = resolveSections(CONTRIB.sections, CFG.autonomy)
|
|
707
|
-
//
|
|
698
|
+
// 连拦阈值**不按档取**(见 CFG.blockedThreshold):它是质量闸,不是预算。
|
|
708
699
|
|
|
709
700
|
/** 宿主读面。缺了它整件事不成立——所以每个工具都显式报错,不静默降级。 */
|
|
710
701
|
const host = () => ctx.get('clearai')
|
|
@@ -727,7 +718,7 @@ export function apply(ctx, config = {}) {
|
|
|
727
718
|
/**
|
|
728
719
|
* 认一条假设:`id` 最稳,**原文**与**唯一前缀**(≥8 字)也认。
|
|
729
720
|
*
|
|
730
|
-
*
|
|
721
|
+
* 为什么放宽:模型会把假设**原文**整句填进 `tests.hypothesis`(真跑里连着三轮都是),
|
|
731
722
|
* 而当时只认 id,错误信息又不列出有效 id——它于是逐字猜哪里差了一个标点,白烧了三轮上下文。
|
|
732
723
|
* 「机制把模型逼进猜谜」是机制的问题,不是模型的问题。
|
|
733
724
|
*/
|
|
@@ -767,7 +758,7 @@ export function apply(ctx, config = {}) {
|
|
|
767
758
|
message: `${value.message ?? value.code ?? 'ok'}\n\n${preview.card}`,
|
|
768
759
|
}
|
|
769
760
|
// 输出**越界就裁掉并告警**:宿主会拿 output.schema 校验工具结果,多一个未声明的字段
|
|
770
|
-
// 会让整个工具调用失败(
|
|
761
|
+
// 会让整个工具调用失败(CreatePlan 曾因此全军覆没)。
|
|
771
762
|
// 裁掉会让模型少看到一个字段(可接受的降级),但绝不让一次成功的动作整个作废。
|
|
772
763
|
const declared = new Set(Object.keys(OUTPUT_SCHEMA.properties))
|
|
773
764
|
const trimmed = Object.keys(result).filter((key) => !declared.has(key))
|
|
@@ -916,6 +907,13 @@ export function apply(ctx, config = {}) {
|
|
|
916
907
|
- **只读**:你**不能写文件、不能执行命令**。若任务要求"产出文件/写入某路径",这不是你能做的——
|
|
917
908
|
把该文件**应有的内容直接写进你的最终答复文本**,由父任务据此落盘。
|
|
918
909
|
|
|
910
|
+
**产出纪律(结论是给父任务省上下文的,不是流水账):**
|
|
911
|
+
- 用 Markdown 写结论,**3000 字以内**;要的是判断与可复核的锚点(文件路径、行号、数字、原文摘录),
|
|
912
|
+
不是把你读过的过程复述一遍。
|
|
913
|
+
- 超了就**压缩**:先给结论与关键读数,再给「要细节去哪里看」的指针(哪个文件、哪一节)。
|
|
914
|
+
3000 字装不下的细节,本来就不该指望父任务一次读完。
|
|
915
|
+
- 你只读、不能落盘;父任务会把你的答复落成文件。所以宁可写短而准,也不要长而糊。
|
|
916
|
+
|
|
919
917
|
**怎么算查清了(侦察的方法,不只是禁令):**
|
|
920
918
|
- **落到第一手证据**:结论要落在你**亲眼读到**的东西上——文件里的原话、真实的目录清单、
|
|
921
919
|
日志里的退出码、代码里的那一行。"按常理应该是"不是证据;凭文件名猜内容也不是。
|
|
@@ -1052,6 +1050,7 @@ export function apply(ctx, config = {}) {
|
|
|
1052
1050
|
const persona = options.persona
|
|
1053
1051
|
const schema = options.outputSchema ?? undefined
|
|
1054
1052
|
const toolFilter = options.toolFilter ?? null
|
|
1053
|
+
const failures = []
|
|
1055
1054
|
const attempts = []
|
|
1056
1055
|
if (toolFilter !== null) attempts.push({ variant: { persona, outputSchema: schema, toolFilter }, capability: 'persona+outputSchema+toolFilter' })
|
|
1057
1056
|
attempts.push({ variant: { persona, outputSchema: schema }, capability: 'persona+outputSchema' })
|
|
@@ -1059,11 +1058,15 @@ export function apply(ctx, config = {}) {
|
|
|
1059
1058
|
attempts.push({ variant: { persona }, capability: 'persona' })
|
|
1060
1059
|
if (schema !== undefined) attempts.push({ variant: { outputSchema: schema }, capability: 'outputSchema' })
|
|
1061
1060
|
attempts.push({ variant: {}, capability: 'prompt-only' })
|
|
1062
|
-
const failures = []
|
|
1063
1061
|
for (const attempt of attempts) {
|
|
1064
1062
|
try {
|
|
1065
1063
|
const run = await subagents.start(CFG.auditProvider, { ...base, ...attempt.variant })
|
|
1066
|
-
|
|
1064
|
+
/**
|
|
1065
|
+
* `native: false` = 结论不会由运行时投递,得靠收集那一刻的返回值带上(降级路径)。
|
|
1066
|
+
* `degraded` 把**为什么降级**如实带出去(调用方落进账本):只说「能力不是 continuable」
|
|
1067
|
+
* 不解释原因,读账的人无法判断这是部署限制还是代码 bug。
|
|
1068
|
+
*/
|
|
1069
|
+
return { ok: true, run, capability: attempt.capability, native: false, degraded: failures[0] ?? null }
|
|
1067
1070
|
} catch (error) {
|
|
1068
1071
|
failures.push(`${attempt.capability}:${String(error?.message ?? error).slice(0, 100)}`)
|
|
1069
1072
|
}
|
|
@@ -1078,20 +1081,24 @@ export function apply(ctx, config = {}) {
|
|
|
1078
1081
|
/**
|
|
1079
1082
|
* 把一条已落定的执行者结果折成变更(仍在跑就返回 null)。
|
|
1080
1083
|
*
|
|
1081
|
-
*
|
|
1084
|
+
* **这里不管「报过没有」**:发布的时机与重试由 sweep 按**投影**判断
|
|
1082
1085
|
* (「报过」不等于「到账本了」)。`dispose` 也挪到「确认到账」之后——早释放会把子会话
|
|
1083
1086
|
* 从会话服务里摘掉,而它正是回收结论的最后一份凭据。
|
|
1084
1087
|
*/
|
|
1085
|
-
function collectExecutor(entry) {
|
|
1088
|
+
function collectExecutor(entry, recovered = false) {
|
|
1086
1089
|
if (entry.settled === null) return null
|
|
1087
1090
|
const ok = entry.settled.ok === true
|
|
1088
1091
|
return {
|
|
1089
1092
|
fork: entry.fork,
|
|
1090
1093
|
branch: entry.branch,
|
|
1091
1094
|
label: entry.label,
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
+
/**
|
|
1096
|
+
* `note` 写**出处或具体结局**,两件事都写清:
|
|
1097
|
+
* · 成功且是从它自己的会话日志回收的 ⇒ `recovered`(派它的那次进程可能已经不在了,
|
|
1098
|
+
* 读账的人必须看得出这条结论不是当场拿到的);
|
|
1099
|
+
* · 失败 ⇒ 具体结局(aborted / error / …),不写笼统的 failed——与侦察那条路同一个纪律。
|
|
1100
|
+
*/
|
|
1101
|
+
mutation: { t: 'worldline/executed', fork: entry.fork, branch: entry.branch, child: entry.child, ok, conclusion: clipConclusion(entry.settled.conclusion, entry.workspace ?? null), note: ok ? (recovered ? 'recovered' : null) : String(entry.settled.stopReason ?? 'failed') },
|
|
1095
1102
|
}
|
|
1096
1103
|
}
|
|
1097
1104
|
|
|
@@ -1109,7 +1116,7 @@ export function apply(ctx, config = {}) {
|
|
|
1109
1116
|
/**
|
|
1110
1117
|
* **只派遣,不等**:把执行者放出去、登记进表,立刻返回。
|
|
1111
1118
|
*
|
|
1112
|
-
*
|
|
1119
|
+
* 为什么必须分开:`ForkPlan` 若在这里
|
|
1113
1120
|
* `await Promise.all(四个执行者)`,而变更记录是**随工具结果**进日志的——
|
|
1114
1121
|
* 于是「长出四条世界线」这条事实要等四个执行者全部跑完才落账:
|
|
1115
1122
|
* · 树上十几分钟看不到分叉(用户看到的就是这个窗口);
|
|
@@ -1147,22 +1154,29 @@ export function apply(ctx, config = {}) {
|
|
|
1147
1154
|
const childId = String(dispatched.run.id)
|
|
1148
1155
|
const entry = { fork: fork.id, branch: branch.id, label: branch.label, workspace: branch.workspace, child: childId, settled: null, reported: false, run: dispatched.run }
|
|
1149
1156
|
executorRuns.set(childId, entry)
|
|
1150
|
-
mutations.push({ t: 'worldline/executing', fork: fork.id, branch: branch.id, child: childId, capability: dispatched.capability })
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1157
|
+
mutations.push({ t: 'worldline/executing', fork: fork.id, branch: branch.id, child: childId, capability: dispatched.capability, degraded_reason: dispatched.degraded ?? null })
|
|
1158
|
+
/**
|
|
1159
|
+
* 先把结果挂上,再等:等超时了也不要紧——条目留在表里,WorldlineStatus 之后来收。
|
|
1160
|
+
* 中断/报错是 resolve 带 stopReason(见 settleSubRun):不判它就会把「被打断的执行者」
|
|
1161
|
+
* 记成「交付成功」,而世界线的算术会拿半截读数去比较。
|
|
1162
|
+
*
|
|
1163
|
+
* **可续跑那一档没有 `result` promise**(与侦察同一个形状):结算改由运行时的结算通知
|
|
1164
|
+
* 或子会话日志给出,所以这里要会「没有」。
|
|
1165
|
+
*/
|
|
1166
|
+
if (dispatched.run?.result !== undefined) {
|
|
1167
|
+
entry.promise = dispatched.run.result.then(
|
|
1168
|
+
(value) => {
|
|
1169
|
+
const settled = settleSubRun(value)
|
|
1170
|
+
entry.settled = { ok: settled.ok, conclusion: settled.conclusion, stopReason: settled.stopReason }
|
|
1171
|
+
return entry.settled
|
|
1172
|
+
},
|
|
1173
|
+
(error) => {
|
|
1174
|
+
const settled = settleSubRun(undefined, error)
|
|
1175
|
+
entry.settled = { ok: false, conclusion: settled.conclusion, stopReason: settled.stopReason }
|
|
1176
|
+
return entry.settled
|
|
1177
|
+
},
|
|
1178
|
+
)
|
|
1179
|
+
}
|
|
1166
1180
|
return { ok: true, mutations, entry, child: childId }
|
|
1167
1181
|
}
|
|
1168
1182
|
|
|
@@ -1172,9 +1186,9 @@ export function apply(ctx, config = {}) {
|
|
|
1172
1186
|
* 两个调用者:pre-step 的 sweep(不问自答,结论自动回灌)与 `WorldlineStatus`(模型主动查)。
|
|
1173
1187
|
*/
|
|
1174
1188
|
/**
|
|
1175
|
-
* **从执行者自己的会话日志里回收结论**(
|
|
1189
|
+
* **从执行者自己的会话日志里回收结论**(结论若只挂在父进程内存的 promise 上,父进程一断就永久丢)。
|
|
1176
1190
|
*
|
|
1177
|
-
*
|
|
1191
|
+
* 典型形态:四条执行者**都正常跑完了**(各自子会话日志最后一条都是 `turn/end{completed}`),
|
|
1178
1192
|
* 可父会话退出时只收上来一条 —— 另外三条的结论还在内存那个 `.then()` 上,进程一没就没了。
|
|
1179
1193
|
* 而结论**并没有丢**:它就写在执行者自己的会话日志里(DSH 的会话日志就是账本)。
|
|
1180
1194
|
* 「什么都不删」在这里的具体含义是:**没丢的东西不该当成丢了**。
|
|
@@ -1229,7 +1243,13 @@ export function apply(ctx, config = {}) {
|
|
|
1229
1243
|
if (text !== '') conclusion = text
|
|
1230
1244
|
}
|
|
1231
1245
|
const reason = String(end.data?.reason?.kind ?? 'unknown')
|
|
1232
|
-
|
|
1246
|
+
const ok = reason === 'completed'
|
|
1247
|
+
/**
|
|
1248
|
+
* 异常结束的结论要**自带说明**,与一次性派遣那条路(`settleSubRun`)同一个形状:
|
|
1249
|
+
* 半截文本被当成「回灌过的结论」是这里最危险的假话。两条路形状一致,
|
|
1250
|
+
* 「这句话是从哪条路来的」就不再影响账本的可读性。
|
|
1251
|
+
*/
|
|
1252
|
+
return { ok, stopReason: reason, conclusion: ok ? conclusion : `子任务未正常结束(${reason}):${conclusion.slice(0, 1200)}` }
|
|
1233
1253
|
}
|
|
1234
1254
|
|
|
1235
1255
|
/**
|
|
@@ -1240,11 +1260,11 @@ export function apply(ctx, config = {}) {
|
|
|
1240
1260
|
* 执行者的产物还在它自己的工作副本里,模型可以照常 `AdvanceWorldline` 用那些产物交付。
|
|
1241
1261
|
*/
|
|
1242
1262
|
/**
|
|
1243
|
-
*
|
|
1263
|
+
* 收集器的**回合纪元**:纪元 = 当前回合号(`payload.turn`)。
|
|
1244
1264
|
*
|
|
1245
1265
|
* 为什么:投影**在回合内不前进**(本回合落的变更要等回合边界才折进去),所以
|
|
1246
1266
|
* 「投影里还没落地」在同一回合内**永远为真**;旧写法配的是「过了 2 秒就重发」,
|
|
1247
|
-
* 于是每个成功路径都重发一遍(
|
|
1267
|
+
* 于是每个成功路径都重发一遍(踩过的坑:1 次侦察派遣 ⇒ **31 条** `scout/settled`;
|
|
1248
1268
|
* 3 条世界线 ⇒ 14 条 `worldline/executed`),每次都还读一遍子会话日志,那一场跑了 8408s。
|
|
1249
1269
|
* 重发只该发生在**下一个回合**——那时投影才有机会说话。
|
|
1250
1270
|
*
|
|
@@ -1253,8 +1273,25 @@ export function apply(ctx, config = {}) {
|
|
|
1253
1273
|
* (第一版就是这么把 3/3 掉成 2/3 的,真跑验收抓住的)。它按**时间窗口**限流,不按回合。
|
|
1254
1274
|
*/
|
|
1255
1275
|
const collectEpochs = new Map()
|
|
1256
|
-
/**
|
|
1257
|
-
|
|
1276
|
+
/**
|
|
1277
|
+
* **本回合已经落过账的子 run**(会话 → 回合号 + id 集)。
|
|
1278
|
+
*
|
|
1279
|
+
* 投影在回合内不前进,同一件事会被每一拍重新「发现」一次;靠这张表保证**同一回合不重发**。
|
|
1280
|
+
* 跨回合仍不见落地才补发——那时回合号变了,自然放行。keyed by 会话+记录 id,
|
|
1281
|
+
* 所以重启之后同样成立(不依赖内存里的条目)。
|
|
1282
|
+
*/
|
|
1283
|
+
const publishedInEpoch = new Map()
|
|
1284
|
+
function alreadyPublished(sessionId, key) {
|
|
1285
|
+
const record = publishedInEpoch.get(String(sessionId))
|
|
1286
|
+
return record !== undefined && record.epoch === collectEpoch(sessionId) && record.keys.has(String(key))
|
|
1287
|
+
}
|
|
1288
|
+
function markPublished(sessionId, key) {
|
|
1289
|
+
const sessionKey = String(sessionId)
|
|
1290
|
+
const epoch = collectEpoch(sessionId)
|
|
1291
|
+
const record = publishedInEpoch.get(sessionKey)
|
|
1292
|
+
if (record === undefined || record.epoch !== epoch) publishedInEpoch.set(sessionKey, { epoch, keys: new Set([String(key)]) })
|
|
1293
|
+
else record.keys.add(String(key))
|
|
1294
|
+
}
|
|
1258
1295
|
function collectEpoch(sessionId) {
|
|
1259
1296
|
return collectEpochs.get(String(sessionId)) ?? -1
|
|
1260
1297
|
}
|
|
@@ -1262,7 +1299,7 @@ export function apply(ctx, config = {}) {
|
|
|
1262
1299
|
collectEpochs.set(String(sessionId), Number.isFinite(turn) ? Number(turn) : collectEpoch(sessionId) + 1)
|
|
1263
1300
|
}
|
|
1264
1301
|
|
|
1265
|
-
function sweepLostExecutors(state) {
|
|
1302
|
+
function sweepLostExecutors(state, sessionId) {
|
|
1266
1303
|
const mutations = []
|
|
1267
1304
|
let recovered = 0
|
|
1268
1305
|
let lost = 0
|
|
@@ -1279,7 +1316,7 @@ export function apply(ctx, config = {}) {
|
|
|
1279
1316
|
const child = execution.child === null || execution.child === undefined ? null : String(execution.child)
|
|
1280
1317
|
const entry = child === null ? undefined : executorRuns.get(child)
|
|
1281
1318
|
/**
|
|
1282
|
-
* **表里有条目但还没落定**时也要试着回收(
|
|
1319
|
+
* **表里有条目但还没落定**时也要试着回收(形态:四条执行者都跑完了,
|
|
1283
1320
|
* 表里那条 promise 却没落定,于是「有条目」把回收挡住了,那条世界线永久停在「执行者未归」)。
|
|
1284
1321
|
* 判据不看内存表,看**执行者自己的会话日志**:它写了 `turn/end`,结论就存在了。
|
|
1285
1322
|
* 回收成功就把条目标成已报,免得那份 promise 之后落定时又报一次(一条事实一份账)。
|
|
@@ -1288,14 +1325,21 @@ export function apply(ctx, config = {}) {
|
|
|
1288
1325
|
const found = child === null ? null : recoverFromChildSession(child)
|
|
1289
1326
|
if (found !== null) {
|
|
1290
1327
|
recovered += 1
|
|
1291
|
-
|
|
1328
|
+
/**
|
|
1329
|
+
* 回收成功要把条目也标成已落定:可续跑那一档没有 promise,条目会永远停在
|
|
1330
|
+
* `settled === null`,于是每一拍回收都成功、每一拍都重发同一条事实。
|
|
1331
|
+
* 重发本身由 `alreadyPublished` 兜住,这里只是让条目如实反映「已经拿到了」。
|
|
1332
|
+
*/
|
|
1333
|
+
if (entry !== undefined) entry.settled = { ok: found.ok, conclusion: found.conclusion, stopReason: found.stopReason }
|
|
1334
|
+
if (alreadyPublished(sessionId, `${fork.id}:${branch.id}`)) continue
|
|
1335
|
+
markPublished(sessionId, `${fork.id}:${branch.id}`)
|
|
1292
1336
|
mutations.push({
|
|
1293
1337
|
t: 'worldline/executed',
|
|
1294
1338
|
fork: fork.id,
|
|
1295
1339
|
branch: branch.id,
|
|
1296
1340
|
child,
|
|
1297
1341
|
ok: found.ok,
|
|
1298
|
-
conclusion:
|
|
1342
|
+
conclusion: clipConclusion(found.conclusion, branch.workspace ?? null),
|
|
1299
1343
|
note: found.ok ? 'recovered' : found.stopReason,
|
|
1300
1344
|
})
|
|
1301
1345
|
continue
|
|
@@ -1319,7 +1363,7 @@ export function apply(ctx, config = {}) {
|
|
|
1319
1363
|
}
|
|
1320
1364
|
|
|
1321
1365
|
/**
|
|
1322
|
-
*
|
|
1366
|
+
* **收侦察的结论**。
|
|
1323
1367
|
*
|
|
1324
1368
|
* 两级,和 `sweepLostExecutors` 同一套:
|
|
1325
1369
|
* ① 表里已落定的 → 落 `scout/settled`;正常结束且结论非空 → 再落一条 `observation/recorded`(资料面);
|
|
@@ -1330,79 +1374,157 @@ export function apply(ctx, config = {}) {
|
|
|
1330
1374
|
function sweepScouts(state, sessionId) {
|
|
1331
1375
|
const mutations = []
|
|
1332
1376
|
const lines = []
|
|
1333
|
-
|
|
1334
|
-
const
|
|
1335
|
-
|
|
1336
|
-
|
|
1377
|
+
/** 降级形态的送达(见 `noticeBlock`):有原生结算通知时不使用。 */
|
|
1378
|
+
const notices = []
|
|
1379
|
+
/** 派出去了、这一拍还没落定的条数:`AwaitWorldlines` 靠它决定要不要继续等。 */
|
|
1380
|
+
let pending = 0
|
|
1381
|
+
const publish = (scoutId, stepId, ok, conclusion, stopReason, meta = {}) => {
|
|
1382
|
+
const full = String(conclusion ?? '')
|
|
1383
|
+
/**
|
|
1384
|
+
* **全文落盘**。一条事实有三个当事人:下达侦察的模型、独立评估者、人。
|
|
1385
|
+
* 结论只活在折叠出的资料面里时,只有面板读得到;落进 `clear/` 之内,三者都能读。
|
|
1386
|
+
* 账本仍是真值源,这份文件是投影产物。
|
|
1387
|
+
*/
|
|
1388
|
+
const path = ok && full.trim() !== '' ? persistMaterial(sessionId, scoutId, meta, full) : null
|
|
1389
|
+
// 账本与观测**同一个上限、同一句截断标记**(超出时必须说清全文在哪)。
|
|
1390
|
+
const clipped = clipConclusion(full, path)
|
|
1391
|
+
mutations.push({ t: 'scout/settled', id: scoutId, step: stepId, conclusion: clipped, note: ok ? null : String(stopReason ?? 'failed'), path })
|
|
1392
|
+
if (ok && full.trim() !== '') {
|
|
1337
1393
|
mutations.push({
|
|
1338
1394
|
t: 'observation/recorded',
|
|
1339
1395
|
id: `m-${Math.random().toString(36).slice(2, 8)}`,
|
|
1340
1396
|
ref: `scout:${scoutId}`,
|
|
1341
1397
|
source: 'scout',
|
|
1342
1398
|
digest: null,
|
|
1343
|
-
bytes:
|
|
1344
|
-
note:
|
|
1399
|
+
bytes: full.length,
|
|
1400
|
+
note: clipped,
|
|
1401
|
+
path,
|
|
1345
1402
|
step: stepId ?? null,
|
|
1346
1403
|
})
|
|
1347
1404
|
}
|
|
1405
|
+
if (ok && full.trim() !== '') notices.push({ kind: 'scout', id: scoutId, trigger: meta.trigger ?? null, conclusion: full, path, native: meta.native === true })
|
|
1348
1406
|
}
|
|
1349
|
-
/**
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1407
|
+
/**
|
|
1408
|
+
* **唯一收集通道**:投影里**还没收口**的侦察——不管内存表里有没有它的条目。
|
|
1409
|
+
*
|
|
1410
|
+
* 两条来源,按优先级:
|
|
1411
|
+
* ① 本进程攥着的 handle 已经落定(只有**一次性派遣**才有 `result` promise);
|
|
1412
|
+
* ② **子会话自己的日志**(权威记录,重启之后也还在)。
|
|
1413
|
+
*
|
|
1414
|
+
* 运行时的结算通知**不在这条路上**:它是运行时给**模型**的送达(已经工作了),
|
|
1415
|
+
* 不是账本的依据。把一个面向模型的消息当成账本的承重结构,就是这一串 bug 的病根——
|
|
1416
|
+
* 通知没到/到得晚,账本就整条路失效;而执行者那条路之所以稳,正是因为它只读日志与目录。
|
|
1417
|
+
*/
|
|
1418
|
+
for (const scout of state?.scouts ?? []) {
|
|
1419
|
+
if (scout.conclusion !== null && scout.conclusion !== undefined) continue
|
|
1420
|
+
const child = scout.child === null || scout.child === undefined ? null : String(scout.child)
|
|
1421
|
+
if (child === null) continue
|
|
1422
|
+
const entry = scoutRuns.get(child)
|
|
1423
|
+
let settled = entry?.settled ?? null
|
|
1424
|
+
let recovered = false
|
|
1425
|
+
if (settled === null) {
|
|
1426
|
+
const found = recoverFromChildSession(child)
|
|
1427
|
+
if (found !== null) {
|
|
1428
|
+
settled = { ok: found.ok, conclusion: found.conclusion, stopReason: found.stopReason }
|
|
1429
|
+
recovered = true
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
// 读不到:还在跑(或这个形态读不了子会话)——留着,别冤枉它。
|
|
1433
|
+
if (settled === null) {
|
|
1434
|
+
if (entry !== undefined) pending += 1
|
|
1435
|
+
continue
|
|
1436
|
+
}
|
|
1437
|
+
// 同一回合里不重发(投影在回合内不前进);跨回合仍不见落地才补发。
|
|
1438
|
+
if (alreadyPublished(sessionId, scout.id)) continue
|
|
1439
|
+
markPublished(sessionId, scout.id)
|
|
1440
|
+
publish(scout.id, scout.step ?? null, settled.ok === true, settled.conclusion, settled.stopReason, {
|
|
1441
|
+
trigger: scout.trigger ?? null,
|
|
1442
|
+
digest: scout.digest ?? null,
|
|
1443
|
+
child,
|
|
1444
|
+
native: entry?.native === true,
|
|
1445
|
+
})
|
|
1446
|
+
lines.push(`侦察 · ${scout.trigger ?? scout.id}:${settled.ok === true ? (recovered ? '从会话日志回收' : '完成') : `未完成(${settled.stopReason ?? 'unknown'})`}`)
|
|
1447
|
+
// 结论已经拿到,handle 没用了:退休(可续跑那一档没有 dispose,子会话按原生语义留着)。
|
|
1448
|
+
if (entry !== undefined) {
|
|
1449
|
+
scoutRuns.delete(child)
|
|
1358
1450
|
try {
|
|
1359
1451
|
void entry.run?.dispose?.().catch?.(() => {})
|
|
1360
1452
|
} catch {
|
|
1361
1453
|
/* dispose 失败不影响结论 */
|
|
1362
1454
|
}
|
|
1363
|
-
continue
|
|
1364
1455
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
const
|
|
1387
|
-
if (
|
|
1456
|
+
}
|
|
1457
|
+
return { mutations, lines, pending, notices }
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* **失联的侦察**。判据与评估者那条**逐字同构**(`sweepLostAudits`):
|
|
1462
|
+
* · 只看投影里还没收口的;
|
|
1463
|
+
* · 本进程攥着的那几次派遣是活的,**不问目录**;
|
|
1464
|
+
* · 问原生子代理目录:还在跑的不动;
|
|
1465
|
+
* · 目录里不在跑、结论又收不回来 ⇒ 如实落「失联」终局(一句永久「未回灌」是等不到下文的承诺);
|
|
1466
|
+
* · 目录拿不到 ⇒ **不判**(判不出就不编)。
|
|
1467
|
+
* 为什么放回合边界而不是每次收集都判:目录是异步读面,而「这次没捞到」不等于「永远捞不到」。
|
|
1468
|
+
*/
|
|
1469
|
+
/**
|
|
1470
|
+
* 子会话日志里**有几条事件**。判不出回 `null`(读面不可用)——**不编**:
|
|
1471
|
+
* 「一片空白」与「读不到」是两回事,前者说明结论永远不会来,后者什么也说明不了。
|
|
1472
|
+
*/
|
|
1473
|
+
function childSessionEventCount(childId) {
|
|
1474
|
+
const sessions = ctx.get('sessions')
|
|
1475
|
+
if (sessions === undefined || typeof sessions.get !== 'function') return null
|
|
1476
|
+
try {
|
|
1477
|
+
const session = sessions.get(childId)
|
|
1478
|
+
if (session === null || session === undefined) return 0
|
|
1479
|
+
const events = typeof session.ownEvents === 'function' ? session.ownEvents() : []
|
|
1480
|
+
return Array.isArray(events) ? events.length : 0
|
|
1481
|
+
} catch {
|
|
1482
|
+
return null
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
async function sweepLostScouts(sessionId, state, justSettled = new Set()) {
|
|
1487
|
+
// `justSettled`:这一拍**刚刚**收上来的(还在同一批 `factMutations` 里,投影尚未前进)。
|
|
1488
|
+
// 不排除它们,就会把刚落定的结论再判成「失联」——自己和自己打架,还把那一条结论写没。
|
|
1489
|
+
const pending = (state?.scouts ?? []).filter(
|
|
1490
|
+
(scout) => (scout.conclusion === null || scout.conclusion === undefined) && typeof scout.child === 'string' && scout.child !== '' && !justSettled.has(scout.id),
|
|
1491
|
+
)
|
|
1492
|
+
if (pending.length === 0) return { mutations: [], lost: 0, lines: [] }
|
|
1493
|
+
const known = new Set([...scoutRuns.values()].map((entry) => String(entry.child ?? '')))
|
|
1494
|
+
const unresolved = pending.filter((scout) => !known.has(String(scout.child)))
|
|
1495
|
+
if (unresolved.length === 0) return { mutations: [], lost: 0, lines: [] }
|
|
1496
|
+
const subagents = ctx.get('subagents')
|
|
1497
|
+
if (subagents === undefined || typeof subagents.listChildren !== 'function') return { mutations: [], lost: 0, lines: [] }
|
|
1498
|
+
let children = []
|
|
1499
|
+
try {
|
|
1500
|
+
children = await subagents.listChildren(sessionId)
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
ctx.logger?.warn?.(`clearai: 列子代理失败,本次不判侦察失联 ${String(error?.message ?? error).slice(0, 120)}`)
|
|
1503
|
+
return { mutations: [], lost: 0, lines: [] }
|
|
1504
|
+
}
|
|
1505
|
+
const running = new Set((Array.isArray(children) ? children : []).filter((item) => item?.kind === 'child' && item.activity === 'running').map((item) => String(item.id)))
|
|
1506
|
+
const listed = new Set((Array.isArray(children) ? children : []).filter((item) => item?.kind === 'child').map((item) => String(item.id)))
|
|
1507
|
+
const mutations = []
|
|
1508
|
+
const lines = []
|
|
1509
|
+
for (const scout of unresolved) {
|
|
1510
|
+
const child = String(scout.child)
|
|
1511
|
+
if (running.has(child)) continue
|
|
1388
1512
|
/**
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1513
|
+
* 目录里**没有**这个子会话时,不能立刻判死:可能它刚派出、注册表还没认领。
|
|
1514
|
+
* 退回**它自己的日志**判——一片空白 ⇒ 结论永远不会来了,如实落失联;
|
|
1515
|
+
* 有事件却没有 `turn/end` ⇒ 还在跑,不动它。(目录里**有**它、但不在跑,
|
|
1516
|
+
* 那就是注册表也认它已经结束 ⇒ 直接判失联。)
|
|
1391
1517
|
*/
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
recoverAttempts.set(recoveryKey, Date.now())
|
|
1396
|
-
const found = recoverFromChildSession(child)
|
|
1397
|
-
if (found === null) continue
|
|
1398
|
-
publish(scout.id, scout.step ?? null, found.ok === true, found.conclusion, found.stopReason)
|
|
1399
|
-
lines.push(`侦察 · ${scout.trigger ?? scout.id}:从会话日志回收(${found.ok === true ? '完成' : found.stopReason})`)
|
|
1518
|
+
if (!listed.has(child) && childSessionEventCount(child) !== 0) continue
|
|
1519
|
+
mutations.push({ t: 'scout/settled', id: scout.id, step: scout.step ?? null, conclusion: '', note: '失联', path: null })
|
|
1520
|
+
lines.push(`${scout.trigger ?? scout.id}:侦察失联(子会话已不在跑,结论收不回来)`)
|
|
1400
1521
|
}
|
|
1401
|
-
return { mutations, lines }
|
|
1522
|
+
return { mutations, lost: mutations.length, lines }
|
|
1402
1523
|
}
|
|
1403
1524
|
|
|
1525
|
+
|
|
1404
1526
|
/**
|
|
1405
|
-
*
|
|
1527
|
+
* **失联的评估者**。
|
|
1406
1528
|
*
|
|
1407
1529
|
* `pendingAudits` 是**进程内**的:重启之后它空了,而投影里那条 `audit/dispatched` 还在
|
|
1408
1530
|
* (`verdict === null`)。后果有两条,后一条更狠:
|
|
@@ -1455,6 +1577,8 @@ export function apply(ctx, config = {}) {
|
|
|
1455
1577
|
function sweepWorldlineExecutors(state, sessionId) {
|
|
1456
1578
|
const mutations = []
|
|
1457
1579
|
const lines = []
|
|
1580
|
+
/** 本次刚收到的执行者报告(降级形态靠它送达:有原生通知时这一项不被使用)。 */
|
|
1581
|
+
const notices = []
|
|
1458
1582
|
/** 投影里这条世界线的执行者收到了结论吗(`execution.ok` 不再是 null)。 */
|
|
1459
1583
|
const landedIn = (forkId, branchId) => {
|
|
1460
1584
|
const fork = (state?.forks ?? []).find((item) => item.id === forkId)
|
|
@@ -1475,31 +1599,41 @@ export function apply(ctx, config = {}) {
|
|
|
1475
1599
|
}
|
|
1476
1600
|
continue
|
|
1477
1601
|
}
|
|
1478
|
-
if (entry.settled === null) {
|
|
1479
|
-
lines.push(`${entry.label}:仍在跑`)
|
|
1480
|
-
continue
|
|
1481
|
-
}
|
|
1482
1602
|
/**
|
|
1483
|
-
*
|
|
1484
|
-
* (
|
|
1603
|
+
* 结算只认两条来源:① 本进程攥着的 handle 已落定(一次性派遣);② 子会话自己的日志。
|
|
1604
|
+
* **结算通知不在这条路上**——它是给模型的送达,不是账本的依据(与侦察同一条纪律)。
|
|
1605
|
+
* 读不到就是还在跑:留着,别冤枉。
|
|
1485
1606
|
*/
|
|
1486
|
-
|
|
1487
|
-
if (entry.
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1607
|
+
let recoveredFromLog = false
|
|
1608
|
+
if (entry.settled === null) {
|
|
1609
|
+
const found = recoverFromChildSession(entry.child)
|
|
1610
|
+
if (found === null) {
|
|
1611
|
+
lines.push(`${entry.label}:仍在跑`)
|
|
1612
|
+
continue
|
|
1613
|
+
}
|
|
1614
|
+
entry.settled = { ok: found.ok, conclusion: found.conclusion, stopReason: found.stopReason }
|
|
1615
|
+
recoveredFromLog = true
|
|
1616
|
+
}
|
|
1617
|
+
// 同一回合里不重发(投影在回合内不前进);跨回合仍不见落地才补发。
|
|
1618
|
+
if (alreadyPublished(sessionId, `${entry.fork}:${entry.branch}`)) continue
|
|
1619
|
+
markPublished(sessionId, `${entry.fork}:${entry.branch}`)
|
|
1620
|
+
const collected = collectExecutor(entry, recoveredFromLog)
|
|
1492
1621
|
if (collected === null) continue
|
|
1493
1622
|
mutations.push(collected.mutation)
|
|
1494
|
-
lines.push(
|
|
1623
|
+
lines.push(
|
|
1624
|
+
`${entry.label}:${collected.mutation.ok !== true ? `未完成(${collected.mutation.note ?? 'unknown'})` : recoveredFromLog ? '从会话日志回收' : '完成'}`,
|
|
1625
|
+
)
|
|
1626
|
+
if (collected.mutation.ok === true && String(entry.settled.conclusion ?? '').trim() !== '') {
|
|
1627
|
+
notices.push({ kind: 'worldline', id: entry.branch, trigger: entry.label, conclusion: String(entry.settled.conclusion), path: entry.workspace ?? null, native: entry.native === true })
|
|
1628
|
+
}
|
|
1495
1629
|
}
|
|
1496
|
-
return { mutations, lines }
|
|
1630
|
+
return { mutations, lines, notices }
|
|
1497
1631
|
}
|
|
1498
1632
|
|
|
1499
1633
|
/**
|
|
1500
1634
|
* 一条侦察任务的**身份**:锚定的步 + 任务原文(空白归一)。
|
|
1501
1635
|
*
|
|
1502
|
-
*
|
|
1636
|
+
* 为什么要它:没有它,中途被打断之后再跑,所有侦察**从头重派**——
|
|
1503
1637
|
* 已经正常回灌过的那些也白跑一遍。任务原文相同 = 同一件事,认出来就能复用它的结论。
|
|
1504
1638
|
* 身份里带锚定的步:同一个问法落在不同的步上,是两件事。
|
|
1505
1639
|
*/
|
|
@@ -1565,9 +1699,9 @@ export function apply(ctx, config = {}) {
|
|
|
1565
1699
|
return { ok: false, note: `侦察派不出去(${dispatched.reason})`, mutations }
|
|
1566
1700
|
}
|
|
1567
1701
|
const childId = String(dispatched.run.id)
|
|
1568
|
-
mutations.push({ t: 'scout/dispatched', id: scoutId, step: step.id, plan: plan?.id ?? null, goal: step.goal ?? null, trigger, child: childId, capability: dispatched.capability, digest })
|
|
1702
|
+
mutations.push({ t: 'scout/dispatched', id: scoutId, step: step.id, plan: plan?.id ?? null, goal: step.goal ?? null, trigger, child: childId, capability: dispatched.capability, digest, degraded_reason: dispatched.degraded ?? null })
|
|
1569
1703
|
/**
|
|
1570
|
-
* **派出去就返回**(
|
|
1704
|
+
* **派出去就返回**(与「世界线执行者」同一个病,同一个修法)。
|
|
1571
1705
|
*
|
|
1572
1706
|
* 原来这里 `await Promise.race([结论, 240s 超时])`,而 `scout/dispatched` 是**随工具结果**
|
|
1573
1707
|
* 进日志的 —— 于是这 240 秒里跑动一断(用户打断、进程退出、模型换路),「派过侦察」这条事实
|
|
@@ -1579,7 +1713,12 @@ export function apply(ctx, config = {}) {
|
|
|
1579
1713
|
*/
|
|
1580
1714
|
const entry = { scoutId, stepId: step.id, planId: plan?.id ?? null, trigger, digest, child: childId, settled: null, reported: false, run: dispatched.run }
|
|
1581
1715
|
scoutRuns.set(childId, entry)
|
|
1582
|
-
|
|
1716
|
+
/**
|
|
1717
|
+
* 只有一次性派遣才有 `result` promise。可续跑那一档没有 ⇒ 结算改由
|
|
1718
|
+
* `sweepScouts()` 从**子会话自己的日志**里读(它本来就更权威,也是重启后唯一的路)。
|
|
1719
|
+
*/
|
|
1720
|
+
if (dispatched.run?.result !== undefined) {
|
|
1721
|
+
entry.promise = dispatched.run.result.then(
|
|
1583
1722
|
(value) => {
|
|
1584
1723
|
const settled = settleSubRun(value)
|
|
1585
1724
|
entry.settled = { ok: settled.ok, conclusion: settled.conclusion, stopReason: settled.stopReason }
|
|
@@ -1590,7 +1729,8 @@ export function apply(ctx, config = {}) {
|
|
|
1590
1729
|
entry.settled = { ok: false, conclusion: settled.conclusion, stopReason: settled.stopReason }
|
|
1591
1730
|
return entry.settled
|
|
1592
1731
|
},
|
|
1593
|
-
|
|
1732
|
+
)
|
|
1733
|
+
}
|
|
1594
1734
|
return { ok: true, pending: true, conclusion: '', note: null, digest, mutations, child: childId, scoutId }
|
|
1595
1735
|
}
|
|
1596
1736
|
|
|
@@ -1669,7 +1809,7 @@ export function apply(ctx, config = {}) {
|
|
|
1669
1809
|
}
|
|
1670
1810
|
|
|
1671
1811
|
/**
|
|
1672
|
-
*
|
|
1812
|
+
* 从「依据」里挑出一个**真实存在**的工作区文件。
|
|
1673
1813
|
*
|
|
1674
1814
|
* 依据是模型写的一句话,通常点着某个产物(「lab/roots.csv 的读数」)。这里把候选 token 逐个
|
|
1675
1815
|
* 落到盘上核一次:存在才算数。**这不是猜,是核** —— 面板上每个可点的东西都必须此刻真的在盘上。
|
|
@@ -1688,7 +1828,7 @@ export function apply(ctx, config = {}) {
|
|
|
1688
1828
|
}
|
|
1689
1829
|
|
|
1690
1830
|
/**
|
|
1691
|
-
*
|
|
1831
|
+
* 一条证据的**出处**:记账那一刻就解析成事实,界面只渲染、不猜。
|
|
1692
1832
|
*
|
|
1693
1833
|
* 四类(与面板上的四个入口一一对应):
|
|
1694
1834
|
* · `artifact` —— 产物文件(准入闸门 stat 过的,或依据里点名且此刻在盘上的)
|
|
@@ -1740,14 +1880,14 @@ export function apply(ctx, config = {}) {
|
|
|
1740
1880
|
}
|
|
1741
1881
|
|
|
1742
1882
|
/**
|
|
1743
|
-
*
|
|
1883
|
+
* 「该收口哪个分叉」的目标步。
|
|
1744
1884
|
*
|
|
1745
1885
|
* 洞长这样:`AbandonFork`/`ConvergeFork` 都在 `firstOpenStep` 上取步。步骤一旦被作废,
|
|
1746
1886
|
* 它就不再是「第一个未落定步」——于是挂在上面的未收口分叉**既不能收敛也不能放弃**,
|
|
1747
|
-
*
|
|
1887
|
+
* 工作副本永久留在盘上(形态:`lab/` 被声明成物证 → 准入拒 → 模型作废该步改道,
|
|
1748
1888
|
* 两条世界线就此失联)。
|
|
1749
1889
|
*
|
|
1750
|
-
* 收口的判据(
|
|
1890
|
+
* 收口的判据(「只落事实 + 放开可达性」):
|
|
1751
1891
|
* · 常规:目标步 = 第一个未落定步(不带 step_id 时的默认);
|
|
1752
1892
|
* · 例外:**已作废**的步,且它上面的分叉还没收口(没收敛、没放弃)——允许显式收口。
|
|
1753
1893
|
* **只允许放弃**,不允许把成果并进一个已经撤回的承诺(交付必须有归宿)。
|
|
@@ -1771,7 +1911,7 @@ export function apply(ctx, config = {}) {
|
|
|
1771
1911
|
}
|
|
1772
1912
|
|
|
1773
1913
|
/**
|
|
1774
|
-
*
|
|
1914
|
+
* **用到世界线的地方先收一次**结论。
|
|
1775
1915
|
*
|
|
1776
1916
|
* 原来只有 pre-step 的 sweep 收结论。可一次真跑里整条链(ForkPlan → 四条交付 → 收敛 → 交付
|
|
1777
1917
|
* → 收尾)可能**全在一个回合内**走完,中间根本没有回合边界——于是三条执行者跑完了、
|
|
@@ -1788,7 +1928,7 @@ export function apply(ctx, config = {}) {
|
|
|
1788
1928
|
* 而失败的返回(`fail(...)`)不带走 `mutations` —— 在入口处收,一旦这次调用失败,
|
|
1789
1929
|
* 结论就**永久丢了**(没人会再收它)。放在成功返回前收,失败时它们原封不动留到下一次。
|
|
1790
1930
|
*
|
|
1791
|
-
* 为什么两级一起收(
|
|
1931
|
+
* 为什么两级一起收(只收一级时,四条执行者只收上来一条):
|
|
1792
1932
|
* 内存表是**尽力而为**的——表里没有、或者扫的时候它还没落定的那些,答案在**执行者自己的
|
|
1793
1933
|
* 会话日志**里。所以每个「结果会用到世界线」的返回点都顺手做一次回收:
|
|
1794
1934
|
* 交付、收敛、收尾、以及两件观察工具。少收一条就是少一条结论叙事,而它是可回收的。
|
|
@@ -1796,12 +1936,49 @@ export function apply(ctx, config = {}) {
|
|
|
1796
1936
|
function collectExecutors(mutations, state, sessionId) {
|
|
1797
1937
|
const swept = sweepWorldlineExecutors(state, sessionId)
|
|
1798
1938
|
mutations.push(...swept.mutations)
|
|
1799
|
-
const late = sweepLostExecutors(state)
|
|
1939
|
+
const late = sweepLostExecutors(state, sessionId)
|
|
1800
1940
|
mutations.push(...late.mutations)
|
|
1801
|
-
// 侦察同一套:它也是「派出去就不等」的子 run
|
|
1941
|
+
// 侦察同一套:它也是「派出去就不等」的子 run,结论同样由这里收。
|
|
1802
1942
|
const scouts = sweepScouts(state, sessionId)
|
|
1803
1943
|
mutations.push(...scouts.mutations)
|
|
1804
|
-
return {
|
|
1944
|
+
return {
|
|
1945
|
+
lines: [...swept.lines, ...scouts.lines],
|
|
1946
|
+
recovered: late.recovered,
|
|
1947
|
+
lost: late.lost,
|
|
1948
|
+
scouts: scouts.mutations.length,
|
|
1949
|
+
scoutsPending: scouts.pending,
|
|
1950
|
+
scoutsLost: scouts.lost,
|
|
1951
|
+
// 本次刚收到的结论:有原生通知的那一档由运行时投递,这里只带「没有原生通知」的那些。
|
|
1952
|
+
notices: [...(swept.notices ?? []), ...(scouts.notices ?? [])],
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/** 结论正文进消息时的上限(账本、文件、消息三处同一个数;文件里是全文)。 */
|
|
1957
|
+
const CONCLUSION_CHARS = 4000
|
|
1958
|
+
|
|
1959
|
+
/** 正文进消息时的截断标记:不许静默截断——读者要知道自己拿到的是不是全文。 */
|
|
1960
|
+
function clipConclusion(text, path) {
|
|
1961
|
+
const full = String(text ?? '')
|
|
1962
|
+
if (full.length <= CONCLUSION_CHARS) return full
|
|
1963
|
+
return `${full.slice(0, CONCLUSION_CHARS)}\n…已截断(全文 ${full.length} 字${path === null || path === undefined ? '' : `,见 ${path}`})`
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/**
|
|
1967
|
+
* 「没有原生通知」那一档的送达:把**本次刚收到**的结论正文拼进这次工具返回。
|
|
1968
|
+
*
|
|
1969
|
+
* 有原生通知时**不拼**——运行时已经把它投给模型了,同一段话出现两遍是这个仓库
|
|
1970
|
+
* 一直反对的事。所以这里只挑 `native !== true` 的那些(即降级到一次性派遣的形态)。
|
|
1971
|
+
*/
|
|
1972
|
+
function noticeBlock(notices) {
|
|
1973
|
+
const pendingNotices = (notices ?? []).filter((item) => item.native !== true)
|
|
1974
|
+
if (pendingNotices.length === 0) return ''
|
|
1975
|
+
return pendingNotices
|
|
1976
|
+
.map((item) => {
|
|
1977
|
+
const who = item.kind === 'scout' ? '侦察' : '执行者'
|
|
1978
|
+
const head = `【${who}结论 · ${item.id}】${item.trigger === null || item.trigger === undefined ? '' : `${item.trigger}\n`}`
|
|
1979
|
+
return `${head}${clipConclusion(item.conclusion, item.path)}`
|
|
1980
|
+
})
|
|
1981
|
+
.join('\n\n')
|
|
1805
1982
|
}
|
|
1806
1983
|
|
|
1807
1984
|
/** 开一次调用的上下文:拿宿主读面、取状态、备一个变更列表。 */
|
|
@@ -1827,7 +2004,7 @@ export function apply(ctx, config = {}) {
|
|
|
1827
2004
|
const CONTINUATION_CODES = { stalled: 'clearai_loop_stalled', abandoned: 'clearai_loop_abandoned' }
|
|
1828
2005
|
|
|
1829
2006
|
/**
|
|
1830
|
-
*
|
|
2007
|
+
* 计划审阅的两个标签:它们是**机制**定义的措辞,不是模型的即兴表达。
|
|
1831
2008
|
* 原生 `plan-review` 意图只要求 `approve` 精确指向本问题自己的某个选项,
|
|
1832
2009
|
* 所以标签怎么写由我们定——但**必须**与 `intent.approve` 是同一个字面值。
|
|
1833
2010
|
*/
|
|
@@ -1835,7 +2012,7 @@ export function apply(ctx, config = {}) {
|
|
|
1835
2012
|
const PLAN_REVIEW_REVISE = '先改再交'
|
|
1836
2013
|
|
|
1837
2014
|
/**
|
|
1838
|
-
*
|
|
2015
|
+
* 内核**自己**对平台说过的关于续跑窗口的话,落进投影。
|
|
1839
2016
|
*
|
|
1840
2017
|
* 为什么不再记在进程内存里:宿主的 `paused` 相位分不清「人按的」与「策略按的」,
|
|
1841
2018
|
* 而这两者的处置正好相反——人按的绝不覆盖,自己按的要能恢复。记在内存里,
|
|
@@ -1894,12 +2071,12 @@ export function apply(ctx, config = {}) {
|
|
|
1894
2071
|
if (plan !== null && plan.blocked !== undefined) return 'stop'
|
|
1895
2072
|
// 计划在场但**授权记号未落账**(`PLAN_AWAITING_CONFIRM`):那是等人的一道门,
|
|
1896
2073
|
// 不是「还有活可干」——推它就是替人做决定(ClearAI 原话:「已经有人在推它了」)。
|
|
1897
|
-
//
|
|
2074
|
+
// 它与档无关:没有任何一档会替你签这个记号(见 CreatePlan 的确认门)。
|
|
1898
2075
|
if (plan !== null && plan.status === 'active' && !derived.planIsAuthorized(plan)) return 'hold'
|
|
1899
2076
|
/**
|
|
1900
2077
|
* 裁决还没回来(`audit/dispatched` 但未 `audit/settled`)= 机器等待态,与 fold 的派生同源。
|
|
1901
2078
|
* `auditsResolved` 是「这一拍刚刚判定这些裁决已经失联」:那些事实要到下一拍才折进投影,
|
|
1902
|
-
*
|
|
2079
|
+
* 所以这一拍必须显式放行,否则一条永远不会回来的裁决会把目标按死在挂起上。
|
|
1903
2080
|
*/
|
|
1904
2081
|
if (auditsResolved !== true && state.audits.some((audit) => audit.verdict === null)) return 'hold'
|
|
1905
2082
|
// 有一道门开着(收件箱非空)就不驱动:等人的事永远优先于往前跑。
|
|
@@ -1909,7 +2086,7 @@ export function apply(ctx, config = {}) {
|
|
|
1909
2086
|
// (SetGoal 之后目标一定是开着的——那一拍投影还没前进)。
|
|
1910
2087
|
const goalOpen = goalOpenOverride ?? (state.goal !== null && state.goal.status === 'open')
|
|
1911
2088
|
/**
|
|
1912
|
-
* 目标还开着、门都关着、也没有在飞的裁决 ⇒
|
|
2089
|
+
* 目标还开着、门都关着、也没有在飞的裁决 ⇒ **该继续**。
|
|
1913
2090
|
* 原先这里按档分叉(无人值守 drive / 人在场 hold)——那是让用户**预先声明**
|
|
1914
2091
|
* 「请多问我」✗,而"要不要人"这件事已经由**门**表达了:有事要拍板 ⇒ 门开着 ⇒ 上面就 hold 了。
|
|
1915
2092
|
*/
|
|
@@ -1917,18 +2094,18 @@ export function apply(ctx, config = {}) {
|
|
|
1917
2094
|
return 'hold'
|
|
1918
2095
|
}
|
|
1919
2096
|
|
|
1920
|
-
/**
|
|
2097
|
+
/** 令牌可用性:服务不在就如实说,而不是假装布防了。 */
|
|
1921
2098
|
function continuationService() {
|
|
1922
|
-
//
|
|
1923
|
-
// (
|
|
1924
|
-
//
|
|
2099
|
+
// **不按 autonomy 分叉**:要不要继续由 `turnDemand` 从门状态算出来
|
|
2100
|
+
// (有没有门开着、有没有裁决在飞、有没有开着的步),档位在这条路上不参与判断。
|
|
2101
|
+
// 额度也只有一个:`DEFAULT_MAX_AUTO_TURNS`(见布防点)。
|
|
1925
2102
|
const goals = ctx.get('goals')
|
|
1926
2103
|
if (goals === undefined || typeof goals.create !== 'function') return { goals: null, why: 'goals 服务不可用' }
|
|
1927
2104
|
return { goals, why: null }
|
|
1928
2105
|
}
|
|
1929
2106
|
|
|
1930
2107
|
/**
|
|
1931
|
-
*
|
|
2108
|
+
* 续跑窗口的**身份**:谁在跑 + 哪一档。
|
|
1932
2109
|
*
|
|
1933
2110
|
* 为什么不再把目标的主张与判据铺进去:宿主那句 objective 有两个消费者——平台面板(**给人看**)
|
|
1934
2111
|
* 与驱动器的续跑种子(**给模型看**)。把它写成「用户的目标」,面板上就出现第二个目标,
|
|
@@ -1940,14 +2117,14 @@ export function apply(ctx, config = {}) {
|
|
|
1940
2117
|
* 这条也是「目标修订不重置预算」的机制保证——否则反复修订目标就能刷出无限轮数。
|
|
1941
2118
|
*/
|
|
1942
2119
|
/**
|
|
1943
|
-
*
|
|
2120
|
+
* 窗口身份里那一档怎么写:**用人话**,与工具行那颗控制同一套词。
|
|
1944
2121
|
*
|
|
1945
2122
|
* 为什么不是「人在场 / 无人值守」:那两个词是**机制**的词汇,给模型和写文档的人用
|
|
1946
2123
|
* (运行态卡、提示词、AUDIT 里继续用它们)。而窗口身份是印在**人看的那块面板**上的
|
|
1947
|
-
*
|
|
2124
|
+
* ——同一屏上出现「人在场」与「多问我」两个名字指同一件事,人只会更糊涂。
|
|
1948
2125
|
* 两处的字面值由两侧的测试各钉一遍(内核这一份 + 客户端那一份),漂移会当场红。
|
|
1949
2126
|
*/
|
|
1950
|
-
/**
|
|
2127
|
+
/** 窗口身份里**不写档位**:「多问我 / 自己跑」不是用户的配置,是运行时状态。 */
|
|
1951
2128
|
/** 一句话摘要:压平空白、超长截断(平台上那句给人看的话用它;内核里没有客户端的 `brief`)。 */
|
|
1952
2129
|
function clip(text, max) {
|
|
1953
2130
|
const flat = String(text ?? '').replace(/\s+/g, ' ').trim()
|
|
@@ -1960,7 +2137,7 @@ export function apply(ctx, config = {}) {
|
|
|
1960
2137
|
return '继续把手上这一步做完'
|
|
1961
2138
|
}
|
|
1962
2139
|
/**
|
|
1963
|
-
*
|
|
2140
|
+
* 窗口上那句**给人看的话**。
|
|
1964
2141
|
*
|
|
1965
2142
|
* 原来这里是 `ClearAI 续跑窗口 · 目标 g-mtyirobr2y2l`:一个机制词加一串机器 id。
|
|
1966
2143
|
* 而这句是**印在平台面板上、给人看**的 —— 人该看到"在做什么",不是我们在内部怎么称呼它。
|
|
@@ -1990,7 +2167,7 @@ export function apply(ctx, config = {}) {
|
|
|
1990
2167
|
function armContinuation(agent, state, derived, options = {}) {
|
|
1991
2168
|
const { goals, why } = continuationService()
|
|
1992
2169
|
/**
|
|
1993
|
-
* 窗口不在时,把这句**事实**的后果也说清楚(
|
|
2170
|
+
* 窗口不在时,把这句**事实**的后果也说清楚(一次性形态根本没有窗口,
|
|
1994
2171
|
* 而提示词里「让出本轮等唤醒」那句在那种场合是不成立的)。
|
|
1995
2172
|
* 只说事实与含义,不劝:**回合结束之后没有人会叫醒你** —— 这一句就够了。
|
|
1996
2173
|
*/
|
|
@@ -2012,7 +2189,7 @@ export function apply(ctx, config = {}) {
|
|
|
2012
2189
|
const current = continuationView(agent)
|
|
2013
2190
|
if (current === null) {
|
|
2014
2191
|
/**
|
|
2015
|
-
*
|
|
2192
|
+
* **它不在了,而那不是我们干的**。我们自己每次清除都在同一次调用里
|
|
2016
2193
|
* 立刻建回一枚(见下面 freshWindow 与 blocked/complete 两支),所以「我们记过一枚
|
|
2017
2194
|
* 活着的窗口、此刻它不在」在证据上只可能是外部清的——平台的人在面板上按了清空。
|
|
2018
2195
|
* 那就**不重建**:人的动作即刻为真,我们只落一条账、并如实说明后果。
|
|
@@ -2041,18 +2218,18 @@ export function apply(ctx, config = {}) {
|
|
|
2041
2218
|
}
|
|
2042
2219
|
/**
|
|
2043
2220
|
* 窗口活着(active + armed),而且账也对得上 → **一个字都不说、一次写都不做**。
|
|
2044
|
-
*
|
|
2045
|
-
*
|
|
2221
|
+
* 文本随目标修订而改写的 `edit` 对齐分支已删:身份一变
|
|
2222
|
+
* 就是换一枚窗口,所以那条路整个不需要了:
|
|
2046
2223
|
* 目标修订不再动窗口 ⇒ 预算也不会被反复修订刷掉。
|
|
2047
2224
|
*/
|
|
2048
2225
|
/**
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
2226
|
+
* **额度变了就换窗口**:
|
|
2227
|
+
* 窗口的额度就是**授权**,额度变了就是换一份授权(额度只有一个默认值,
|
|
2228
|
+
* 但 `maxAutoTurns` 仍可由人显式配置)。旧实现靠 `edit` 去对齐轮数,
|
|
2229
|
+
* 不在这里换窗口,额度变化会把窗口锁在旧额度上。
|
|
2053
2230
|
*
|
|
2054
|
-
*
|
|
2055
|
-
* 是我们的 ⇒
|
|
2231
|
+
* 怎么分清「额度变了」与「人改写过了」:看**平台上的文本还是不是我们记下的那句**。
|
|
2232
|
+
* 是我们的 ⇒ 可以按额度换;不是我们的 ⇒ 以人为准,一个字都不动。
|
|
2056
2233
|
*/
|
|
2057
2234
|
const ours = before !== null && before.label !== null && before.label !== undefined && before.label === current.objective
|
|
2058
2235
|
// 额度变了 = 授权变了 ⇒ 换一枚新窗口(清 + 建,按宿主契约)。
|
|
@@ -2091,8 +2268,9 @@ export function apply(ctx, config = {}) {
|
|
|
2091
2268
|
}
|
|
2092
2269
|
|
|
2093
2270
|
/**
|
|
2094
|
-
*
|
|
2095
|
-
* 上限由调用方现算的 maxGoalRounds
|
|
2271
|
+
* 窗口的说明文字:别让人以为「没有窗口」——每个会话都有窗口,只是额度不同。
|
|
2272
|
+
* 上限由调用方现算的 maxGoalRounds 给:它是 `maxAutoTurns` 或默认值(128),
|
|
2273
|
+
* **不随任何运行档变化**(档位连面板入口都没有了)。
|
|
2096
2274
|
*/
|
|
2097
2275
|
function windowLabel(maxGoalRounds) {
|
|
2098
2276
|
return `${maxGoalRounds} 轮自动续跑,一条人类消息换一个新窗口`
|
|
@@ -2126,7 +2304,7 @@ export function apply(ctx, config = {}) {
|
|
|
2126
2304
|
: '计划层已经收尾而目标还开着,下一阶段由人给。人一开口就换新窗口。'
|
|
2127
2305
|
try {
|
|
2128
2306
|
continuationService().goals.pause(agent, { id: current.id, revision: current.revision })
|
|
2129
|
-
// 「这次暂停是我们按的」必须落账:宿主的 paused
|
|
2307
|
+
// 「这次暂停是我们按的」必须落账:宿主的 paused 相位分不清人按的与策略按的。
|
|
2130
2308
|
recordContinuation(mutations, state.continuation ?? null, { state: 'paused', goal: current.id, target: state.continuation?.target ?? null, why: reason })
|
|
2131
2309
|
return `\n(这一档暂时停着:${why})`
|
|
2132
2310
|
} catch (error) {
|
|
@@ -2136,7 +2314,7 @@ export function apply(ctx, config = {}) {
|
|
|
2136
2314
|
|
|
2137
2315
|
/**
|
|
2138
2316
|
* 让窗口服从策略。判定顺序就是 ClearAI 的语义顺序:
|
|
2139
|
-
* ⓪ 人清掉过续跑 → 撤回一直有效,直到人再开口(
|
|
2317
|
+
* ⓪ 人清掉过续跑 → 撤回一直有效,直到人再开口(人的动作不被静默撤销);
|
|
2140
2318
|
* ① **人开口 = 重新授权**:无条件换新窗口(`reset_goal_loop` 连 budget_exhausted 一起清);
|
|
2141
2319
|
* ② 窗口已经停着(blocked / complete / 人按的 paused)→ 如实说,不自动重开
|
|
2142
2320
|
* ——自动重开一个「额度用尽」的窗口,等于把预算机制废掉;
|
|
@@ -2150,7 +2328,7 @@ export function apply(ctx, config = {}) {
|
|
|
2150
2328
|
const planWork = plan !== null && plan.steps.some((step) => step.status === 'open')
|
|
2151
2329
|
const goalOpen = options.goalOpen ?? (state.goal !== null && state.goal.status === 'open')
|
|
2152
2330
|
const hasWork = goalOpen || planWork
|
|
2153
|
-
//
|
|
2331
|
+
// 档位已经是**部署预设的初值**,不随回合变化 ⇒ 这里没有"当档"要优先。
|
|
2154
2332
|
const demand = turnDemand(state, derived, options.goalOpen ?? goalOpen, options.auditsResolved === true)
|
|
2155
2333
|
const { goals, why } = continuationService()
|
|
2156
2334
|
const current = continuationView(agent)
|
|
@@ -2195,7 +2373,7 @@ export function apply(ctx, config = {}) {
|
|
|
2195
2373
|
if (plan !== null && plan.status === 'active' && !derived.planIsAuthorized(plan)) return 'plan_confirm'
|
|
2196
2374
|
if (derived.hasOpenGate) return 'gate'
|
|
2197
2375
|
/**
|
|
2198
|
-
*
|
|
2376
|
+
* 这里曾有个 `goal_boundary`(「人在场时,一个阶段收尾就停下等人」)——
|
|
2199
2377
|
* 那是**档位**的表达,档删了它也就不该存在。上面三条是**唯一**能让窗口停下的理由
|
|
2200
2378
|
* (都是"真的有人的事"),所以走到这里说明判据与我理解的不一致 ⇒ 如实报未知 + 告警,
|
|
2201
2379
|
* 而不是编一个好听的理由(理由那句话是要给模型读的,不许说假话)。
|
|
@@ -2228,22 +2406,23 @@ export function apply(ctx, config = {}) {
|
|
|
2228
2406
|
}
|
|
2229
2407
|
|
|
2230
2408
|
/**
|
|
2231
|
-
*
|
|
2409
|
+
* **「当档从输入读」这整套已删**(`autonomyFromMessages` / `turnAutonomy` / `autonomyForTurn`):
|
|
2232
2410
|
* 它服务的唯一动词是已摘掉的 `set_autonomy` ✗。档位现在只是**部署预设的初值**,
|
|
2233
2411
|
* 不随回合变化 ⇒ 没有"这一拍按哪一档跑"这个问题,也就没有瞬时参数要传。
|
|
2234
2412
|
*/
|
|
2235
2413
|
|
|
2236
2414
|
|
|
2237
2415
|
/**
|
|
2238
|
-
*
|
|
2416
|
+
* 请人**审阅计划**:走原生 `ctx.userQuestions.ask` 的 `plan-review` 意图。
|
|
2239
2417
|
*
|
|
2240
2418
|
* 为什么要借这一条:客户端为这个意图做了**专门的整屏审阅**(计划 markdown 由原生渲染),
|
|
2241
|
-
*
|
|
2242
|
-
*
|
|
2419
|
+
* 批准/继续改的标签由机制定义,答案**程序化回到调用方**——于是「计划要人审」从
|
|
2420
|
+
* **提示词劝告**(模型记得问)变成**机制动作**(系统自己问)。而授权记号仍然由我们落账:
|
|
2243
2421
|
* **借界面,不借账。**
|
|
2244
2422
|
*
|
|
2245
|
-
*
|
|
2246
|
-
* / unavailable(这个形态没有审阅通道)
|
|
2423
|
+
* 四种结局:approved(落 by='user')/ declined(带回人的反馈)/ cancelled(他改为先说话)
|
|
2424
|
+
* / unavailable(这个形态没有审阅通道)。后三种**不落授权记号**——记号是归属,不是闸门:
|
|
2425
|
+
* 未授权的唯一后果是自动续跑 hold;显式推进不被阻止,第一次交付会按事实补写 by='progress'。
|
|
2247
2426
|
*/
|
|
2248
2427
|
async function requestPlanReview(agent, planMarkdown, signal) {
|
|
2249
2428
|
const questions = ctx.get('userQuestions')
|
|
@@ -2281,7 +2460,7 @@ export function apply(ctx, config = {}) {
|
|
|
2281
2460
|
* 请人审阅**已经立起来的**计划,并按结果落授权记号。
|
|
2282
2461
|
*
|
|
2283
2462
|
* 为什么必须有这条路:`CreatePlan` 会请人审阅,但**改完不会再请** —— 而审阅卡上那句
|
|
2284
|
-
*
|
|
2463
|
+
* 「改完再呈一次」正是我们承诺的。死胡同的形态:人在审阅里选了「先改再交」,
|
|
2285
2464
|
* 模型照意见改了计划,然后**没有任何入口**能再呈一次 ⇒ 计划永远停在未授权,
|
|
2286
2465
|
* 而内核又如实拒绝开工。**打不开的门比没有门更糟**:它把机制变成死胡同。
|
|
2287
2466
|
*
|
|
@@ -2305,7 +2484,7 @@ export function apply(ctx, config = {}) {
|
|
|
2305
2484
|
}
|
|
2306
2485
|
if (review.outcome === 'declined') return { confirmed: false, note: `\n人又一次选择**先改再交**${review.note === '' ? '' : `,他的意见:${review.note}`}——仍未授权,按意见再改。` }
|
|
2307
2486
|
if (review.outcome === 'cancelled') return { confirmed: false, note: '\n人把审阅撤下、改为先说话:仍未授权,等他的下一步指令。' }
|
|
2308
|
-
return { confirmed: false, note: `\n(这份计划还没有得到人的授权:${review.note}
|
|
2487
|
+
return { confirmed: false, note: `\n(这份计划还没有得到人的授权:${review.note}。系统不会自动续跑它;你显式推进时,第一次交付会按事实记下归属。想再请人审,用 RequestPlanReview 重呈。)` }
|
|
2309
2488
|
}
|
|
2310
2489
|
|
|
2311
2490
|
/** 给人审阅的计划正文(markdown)。原生审阅界面渲染它,所以它得是人读得懂的一份计划。 */
|
|
@@ -2325,7 +2504,7 @@ export function apply(ctx, config = {}) {
|
|
|
2325
2504
|
const ontologyShelved = new Set()
|
|
2326
2505
|
|
|
2327
2506
|
/**
|
|
2328
|
-
*
|
|
2507
|
+
* **事实货架**:把已升格的事实汇成 `clear/knowledge/facts/INDEX.md`。
|
|
2329
2508
|
*
|
|
2330
2509
|
* 为什么必须有它:在它之前,事实只落进 `clear/knowledge/facts/<目标 id>.md`
|
|
2331
2510
|
* —— **没有任何读者**(模型不知道有哪些事实、更不知道文件名按目标 id 拼)。
|
|
@@ -2373,7 +2552,7 @@ export function apply(ctx, config = {}) {
|
|
|
2373
2552
|
}
|
|
2374
2553
|
|
|
2375
2554
|
/**
|
|
2376
|
-
*
|
|
2555
|
+
* **本体货架**:把已装的那份本体落成 `clear/ontology/<id>.md`。
|
|
2377
2556
|
*
|
|
2378
2557
|
* 为什么落成文件而不是只留在代码里:声明是**给模型读的**——它得知道这套系统认哪些对象、
|
|
2379
2558
|
* 哪些转移合法、每一级谁来判,才能在写判据与交付时对得上。与 ClearAI 那一侧的
|
|
@@ -2482,8 +2661,10 @@ export function apply(ctx, config = {}) {
|
|
|
2482
2661
|
if (typeof hypothesis?.claim !== 'string' || hypothesis.claim.trim() === '') return fail('hypothesis_claim_required', '每条假设要有一句话主张。')
|
|
2483
2662
|
if (typeof hypothesis?.refute_when !== 'string' || hypothesis.refute_when.trim() === '') return fail('hypothesis_refute_required', '每条假设必须写清「什么结果会推翻它」——没有推翻条件的假设无法被检验。')
|
|
2484
2663
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2664
|
+
// 假设数量下限:首次立目标就得带够候选——0 条一样拦(候选对比是检验的前提,
|
|
2665
|
+
// 只有一个猜想时「验证」容易退化成找证据支持自己)。修订不受此限。
|
|
2666
|
+
if (CFG.minHypotheses > 0 && state.goal === null && hypotheses.length < CFG.minHypotheses) {
|
|
2667
|
+
return fail('hypotheses_too_few', `至少登记 ${CFG.minHypotheses} 条候选假设(每条:一句话主张 + 一句推翻条件)。只有一个猜想,检验容易退化成找证据支持自己;候选对比才让「推翻」成为可能。`)
|
|
2487
2668
|
}
|
|
2488
2669
|
const isRevision = state.goal !== null && state.goal.status === 'open'
|
|
2489
2670
|
if (isRevision && (typeof args.reason !== 'string' || args.reason.trim() === '')) {
|
|
@@ -2524,7 +2705,7 @@ export function apply(ctx, config = {}) {
|
|
|
2524
2705
|
exec.signal,
|
|
2525
2706
|
)
|
|
2526
2707
|
mutations.push(...scout.mutations)
|
|
2527
|
-
// 侦察是「派出去就不等」的子 run
|
|
2708
|
+
// 侦察是「派出去就不等」的子 run:结论由 sweep 收,进资料面。
|
|
2528
2709
|
scoutNote =
|
|
2529
2710
|
scout.pending === true
|
|
2530
2711
|
? '\n立约前侦察已派出(只读),结论会作为观测回灌到资料面 —— 下一步卡片的「资料面」里能看到;要在这个回合里就等它,用 AwaitWorldlines。'
|
|
@@ -2579,12 +2760,12 @@ export function apply(ctx, config = {}) {
|
|
|
2579
2760
|
}
|
|
2580
2761
|
const plan = activePlanOf(state)
|
|
2581
2762
|
/**
|
|
2582
|
-
*
|
|
2763
|
+
* **目标结案前先把计划收尾**(严格,不是提示)。
|
|
2583
2764
|
*
|
|
2584
2765
|
* 为什么不让跳过:事实是在收尾那条路上沉淀的(`fact/promoted` 就在本工具里,
|
|
2585
2766
|
* 而它按 `derived.hypotheses` 逐条升格)—— 先结目标、留一份 active 的计划,
|
|
2586
2767
|
* 等于在账上留下一个开着的东西,还绕过了"沉淀"这道动作 ✗。
|
|
2587
|
-
*
|
|
2768
|
+
* 真跑里出现过这个组合:目标 achieved 而计划 active、`fact/promoted: 0`。
|
|
2588
2769
|
*
|
|
2589
2770
|
* 放弃(`abandoned`)走的是另一条路,**不**受此限:如实说清阻塞就收兵,别为难人 ✓。
|
|
2590
2771
|
*/
|
|
@@ -2614,7 +2795,7 @@ export function apply(ctx, config = {}) {
|
|
|
2614
2795
|
if (audit.verdict === 'pending') return fail('audit_pending', `目标评估者仍在跑:${audit.basis}。先观察当前事实,再谈重试。`)
|
|
2615
2796
|
if (audit.verdict !== 'support') {
|
|
2616
2797
|
/**
|
|
2617
|
-
*
|
|
2798
|
+
* 目标级裁决也要带得出出处:那条审计自己写了一张卡、也有它的评估者会话。
|
|
2618
2799
|
* 这一处原先 `refs: []` ⇒ 面板上这条证据一个可点的东西都没有 ✗。
|
|
2619
2800
|
*/
|
|
2620
2801
|
const goalOrigin = buildEvidenceOrigins({
|
|
@@ -2646,7 +2827,17 @@ export function apply(ctx, config = {}) {
|
|
|
2646
2827
|
{ mutations },
|
|
2647
2828
|
)
|
|
2648
2829
|
}
|
|
2649
|
-
|
|
2830
|
+
/**
|
|
2831
|
+
* **没被任何证据触及的假设**,结案时如实记一笔。
|
|
2832
|
+
*
|
|
2833
|
+
* 两种「没结论」要分得开:证据说「无法判定」= 现有信息不足以定论(已经在账上);
|
|
2834
|
+
* 三样全零 = **没人碰过它**。不强制证实/证伪——但「没看过」不能被写成「没问题」,
|
|
2835
|
+
* 所以这里把它记进结案那条变更里,卡片与面板都说得出来。
|
|
2836
|
+
*/
|
|
2837
|
+
const untouched = derived.hypotheses.filter(
|
|
2838
|
+
(hypothesis) => (hypothesis.supportedLevel === null || hypothesis.supportedLevel === undefined) && (hypothesis.refutations ?? 0) === 0 && (hypothesis.inconclusive ?? 0) === 0,
|
|
2839
|
+
)
|
|
2840
|
+
mutations.push({ t: 'goal/closed', id: goal.id, status: 'achieved', verdict: 'support', note: args.note ?? null, unjudged: untouched.map((hypothesis) => hypothesis.id) })
|
|
2650
2841
|
const continuationNote = stopContinuation(exec.agent, null, '目标达成', mutations)
|
|
2651
2842
|
const threshold = levelIndexOf(goal.promote_at_level)
|
|
2652
2843
|
const promoted = []
|
|
@@ -2679,12 +2870,52 @@ export function apply(ctx, config = {}) {
|
|
|
2679
2870
|
message:
|
|
2680
2871
|
`目标 ${goal.id} 已达成(独立评估者裁决:${audit.basis})。` +
|
|
2681
2872
|
(promoted.length > 0 ? `\n升格为事实:${promoted.join(' / ')}(写入 clear/knowledge/facts/${goal.id}.md)` : '\n没有达到升格门槛的假设。') +
|
|
2873
|
+
(untouched.length > 0
|
|
2874
|
+
? `\n结案时有 ${untouched.length} 条假设**没有被任何证据触及**:${untouched.map((hypothesis) => hypothesis.id).join(', ')}——未判的假设不是「没问题」,是「没看过」;它们留在账上,随时可以补一次验证。`
|
|
2875
|
+
: '') +
|
|
2682
2876
|
'\n被推翻与被改版的假设保留在日志里。' +
|
|
2683
2877
|
continuationNote,
|
|
2684
2878
|
})
|
|
2685
2879
|
},
|
|
2686
2880
|
})
|
|
2687
2881
|
|
|
2882
|
+
/**
|
|
2883
|
+
* 把一份侦察结论**全文**落成工作区文件(`clear/` 之内,账本仍是真值源,这是投影产物)。
|
|
2884
|
+
*
|
|
2885
|
+
* 为什么是文件而不是只留在账本里:一条事实有三个当事人——下达侦察的模型、独立评估者、
|
|
2886
|
+
* 人。只放在会话日志折叠出的资料面里,只有面板读得到;判据一旦写成「与侦察结论一致」,
|
|
2887
|
+
* 模型与评估者都无处可读,只能裁 inconclusive。落成文件之后三者读的是同一份。
|
|
2888
|
+
* 失败只 warn(照 `persistFact` 的做法):投递仍走消息,只是少了那份可读副本。
|
|
2889
|
+
*/
|
|
2890
|
+
function persistMaterial(sessionId, scoutId, meta, conclusion) {
|
|
2891
|
+
const file = join(sessionCwd(sessionId), 'clear', 'knowledge', 'materials', `${scoutId}.md`)
|
|
2892
|
+
try {
|
|
2893
|
+
writeTextFile(
|
|
2894
|
+
file,
|
|
2895
|
+
[
|
|
2896
|
+
`# 侦察结论 · ${scoutId}`,
|
|
2897
|
+
'',
|
|
2898
|
+
`- 触发:${String(meta.trigger ?? '(未记)')}`,
|
|
2899
|
+
`- 锚在哪一步:${String(meta.stepId ?? meta.step ?? '(未记)')}`,
|
|
2900
|
+
meta.child === undefined ? null : `- 子会话:${String(meta.child)}`,
|
|
2901
|
+
meta.digest === undefined ? null : `- 任务指纹:${String(meta.digest)}`,
|
|
2902
|
+
`- 收到时间:${new Date().toISOString()}`,
|
|
2903
|
+
'',
|
|
2904
|
+
'> 这份文件由系统按账本落盘(投影产物);真值源是会话日志里的 `scout/settled`。',
|
|
2905
|
+
'',
|
|
2906
|
+
String(conclusion),
|
|
2907
|
+
'',
|
|
2908
|
+
]
|
|
2909
|
+
.filter((line) => line !== null)
|
|
2910
|
+
.join('\n'),
|
|
2911
|
+
)
|
|
2912
|
+
return file
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
ctx.logger?.warn?.(`clearai kernel: 侦察结论落盘失败 ${String(error?.message ?? error)}`)
|
|
2915
|
+
return null
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2688
2919
|
function persistFact(sessionId, goal, hypothesis, factId) {
|
|
2689
2920
|
const file = join(sessionCwd(sessionId), 'clear', 'knowledge', 'facts', `${goal.id}.md`)
|
|
2690
2921
|
try {
|
|
@@ -2754,39 +2985,32 @@ export function apply(ctx, config = {}) {
|
|
|
2754
2985
|
const goal = state.goal !== null && state.goal.status === 'open' ? state.goal : null
|
|
2755
2986
|
const planId = uniqueId('p')
|
|
2756
2987
|
/**
|
|
2757
|
-
*
|
|
2758
|
-
*
|
|
2759
|
-
*
|
|
2760
|
-
*
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
*
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
2988
|
+
* 计划确认门:**永远请人审阅,没有任何一档自动确认**。
|
|
2989
|
+
*
|
|
2990
|
+
* 如果让系统替人签「计划经人确认」,这条证据就是**系统自己签的**,
|
|
2991
|
+
* 和 L4「人放行」是同一类病。门的意义就在"这一下是人按的":
|
|
2992
|
+
* 没有它,后面所有基于授权的推理都是空的。
|
|
2993
|
+
*
|
|
2994
|
+
* 走**原生审阅**:由**我们**在计划立起来的这一步请人审阅,计划正文交给原生界面渲染。
|
|
2995
|
+
* 批准 ⇒ 授权记号现在就落(`by:'user'`);其余三种结局 ⇒ 记号不落。这一条把
|
|
2996
|
+
* 「模型记得问才有一道门」换成了「机制自己问」——机制优于劝告。
|
|
2997
|
+
*
|
|
2998
|
+
* 注意记号**不是闸门**:未授权只让自动续跑 `hold`(见 turnDemand),`AdvancePlan`
|
|
2999
|
+
* 照常执行,并在同一条变更里补写 `by:'progress'`(行为即授权,见 AdvancePlan)。
|
|
2767
3000
|
*/
|
|
2768
|
-
const autoConfirmed = false
|
|
2769
3001
|
const brief = typeof args.brief === 'string' ? args.brief : ''
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
* 由**我们**在计划立起来的这一步请人审阅,计划正文交给原生界面渲染。
|
|
2773
|
-
* 批准 ⇒ 授权记号现在就落(by: 'user');其余三种结局 ⇒ 一个字都不落,如实停下等人。
|
|
2774
|
-
* 这一条把「模型记得问才有一道门」换成了「机制自己问」——P1(机制优于劝告)。
|
|
2775
|
-
*/
|
|
2776
|
-
let confirmed = autoConfirmed
|
|
3002
|
+
const review = await requestPlanReview(exec.agent, renderPlanForReview(planId, brief, goal, args.steps), exec.signal)
|
|
3003
|
+
let confirmed = false
|
|
2777
3004
|
let reviewNote = ''
|
|
2778
|
-
if (
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
} else {
|
|
2788
|
-
reviewNote = `\n(这份计划还没有得到人的授权:${review.note}。**不要开工**——如实停下等人,别自己去问一遍(系统已经问过了)。)`
|
|
2789
|
-
}
|
|
3005
|
+
if (review.outcome === 'approved') {
|
|
3006
|
+
confirmed = true
|
|
3007
|
+
reviewNote = '\n人在审阅里**批准**了这份计划(原生审阅卡),授权记号已落账——开始执行。'
|
|
3008
|
+
} else if (review.outcome === 'declined') {
|
|
3009
|
+
reviewNote = `\n人在审阅里选择**先改再交**${review.note === '' ? '' : `,他的意见:${review.note}`}——计划仍未授权,按他的意见改完再呈一次。`
|
|
3010
|
+
} else if (review.outcome === 'cancelled') {
|
|
3011
|
+
reviewNote = '\n人把审阅撤下、改为先说话:计划仍未授权,等他的下一步指令。'
|
|
3012
|
+
} else {
|
|
3013
|
+
reviewNote = `\n(这份计划还没有得到人的授权:${review.note}。系统不会自动续跑它;你显式推进时,第一次交付会按事实记下归属。想再请人审,用 RequestPlanReview 重呈。)`
|
|
2790
3014
|
}
|
|
2791
3015
|
mutations.push({
|
|
2792
3016
|
t: 'plan/created',
|
|
@@ -2795,7 +3019,7 @@ export function apply(ctx, config = {}) {
|
|
|
2795
3019
|
phase_id: goal?.id ?? null,
|
|
2796
3020
|
brief,
|
|
2797
3021
|
confirmed_at: confirmed ? new Date().toISOString() : null,
|
|
2798
|
-
confirmed_by: confirmed ?
|
|
3022
|
+
confirmed_by: confirmed ? 'user' : null,
|
|
2799
3023
|
steps: args.steps.map((step) => ({ id: step.id, do: step.do, artifacts: step.artifacts ?? [], done_criteria: step.done_criteria, tests: step.tests ?? null })),
|
|
2800
3024
|
})
|
|
2801
3025
|
return done({
|
|
@@ -2923,7 +3147,7 @@ export function apply(ctx, config = {}) {
|
|
|
2923
3147
|
if (typeof args.reason !== 'string' || args.reason.trim() === '') return fail('reason_required', '作废必须带原因。')
|
|
2924
3148
|
mutations.push({ t: 'plan/voided', plan: plan.id, step: step.id, reason: args.reason.trim() })
|
|
2925
3149
|
/**
|
|
2926
|
-
*
|
|
3150
|
+
* 作废**不动**分叉:作废是承诺层的权威动作,它不改变尝试层已经发生的事实
|
|
2927
3151
|
* ——那些世界线探索过、有的还出了读数。把它们改写成「已放弃」就是改写历史。
|
|
2928
3152
|
* 但也不能装作没看见:这一步上要是还挂着没收口的分叉,那是一条**事实**,
|
|
2929
3153
|
* 而且它有唯一一个出口(带 step_id 的 AbandonFork)。所以如实说,不说教。
|
|
@@ -2959,13 +3183,14 @@ export function apply(ctx, config = {}) {
|
|
|
2959
3183
|
if (unsettled.length > 0) {
|
|
2960
3184
|
return fail('plan_has_open_steps', `还有 ${unsettled.length} 步没落定:${unsettled.map((step) => step.id).join(', ')}。交付它们,或带因作废(VoidPlanStep)。`)
|
|
2961
3185
|
}
|
|
2962
|
-
|
|
3186
|
+
// 收尾也要把**结论正文**带给模型:这一拍收上来的东西,丢弃返回值就等于白收。
|
|
3187
|
+
const collected = collectExecutors(mutations, hostService.state(sessionId), sessionId)
|
|
2963
3188
|
mutations.push({ t: 'plan/closed', plan: plan.id, summary: args.summary ?? null })
|
|
2964
3189
|
persistArchive(sessionId, plan, args.summary ?? null)
|
|
2965
3190
|
return done({
|
|
2966
3191
|
ok: true,
|
|
2967
3192
|
code: 'plan_closed',
|
|
2968
|
-
message: `计划 ${plan.id} 已收束归档(clear/goals/plans/${plan.id}.md)。${state.goal === null || state.goal.status !== 'open' ? '' : `目标 ${state.goal.id} 仍未结案,继续开下一阶段。`}`,
|
|
3193
|
+
message: `计划 ${plan.id} 已收束归档(clear/goals/plans/${plan.id}.md)。${state.goal === null || state.goal.status !== 'open' ? '' : `目标 ${state.goal.id} 仍未结案,继续开下一阶段。`}${noticeBlock(collected.notices) === '' ? '' : `\n\n${noticeBlock(collected.notices)}`}`,
|
|
2969
3194
|
})
|
|
2970
3195
|
},
|
|
2971
3196
|
})
|
|
@@ -3124,7 +3349,7 @@ export function apply(ctx, config = {}) {
|
|
|
3124
3349
|
if (audit.verdict === 'refute') {
|
|
3125
3350
|
const scout = await runScout(sessionId, exec.agent, plan, step, audit.shortfalls.join('; '), `audit_shortfall:${audit.shortfalls[0] ?? '未指明'}`, exec.signal)
|
|
3126
3351
|
mutations.push(...scout.mutations)
|
|
3127
|
-
//
|
|
3352
|
+
// 派出去就不等:结论会在下一个回合边界回灌到资料面。
|
|
3128
3353
|
if (scout.pending === true) basis = `${basis}\n[已派出只读侦察补缺口 ${scout.scoutId}:结论会作为观测回灌到资料面,不在这次回执里]`
|
|
3129
3354
|
}
|
|
3130
3355
|
} else {
|
|
@@ -3140,12 +3365,12 @@ export function apply(ctx, config = {}) {
|
|
|
3140
3365
|
}
|
|
3141
3366
|
|
|
3142
3367
|
/**
|
|
3143
|
-
*
|
|
3368
|
+
* **人的放行先落账**(在别的门之前)。
|
|
3144
3369
|
*
|
|
3145
3370
|
* 放行是**人的动作**,它成立与否与这次交付后来过不过得了别的门无关:
|
|
3146
|
-
* · 发起者仍是 `tools/pre-execute` 瀑布(
|
|
3371
|
+
* · 发起者仍是 `tools/pre-execute` 瀑布(在工具跑起来之前就把这次交付摆给人看);
|
|
3147
3372
|
* · 但旧写法把 `human/released` 留在第 ⑦ 步(交付成功之后)——于是「人放行了,可这一交付
|
|
3148
|
-
*
|
|
3373
|
+
* 栽在来源分离/判据门上」时,那条事实随失败一起消失,**同一步重试又问人一遍**。
|
|
3149
3374
|
* 所以:一知道人放行过,就把这条事实落下来;失败时它跟着回去(见下面的 `refuse`)。
|
|
3150
3375
|
*/
|
|
3151
3376
|
const releaseTarget = levelIndex === 4 && CFG.l4RequiresHumanRelease ? l4Delivery(state, 'AdvancePlan', args) : null
|
|
@@ -3169,7 +3394,7 @@ export function apply(ctx, config = {}) {
|
|
|
3169
3394
|
}
|
|
3170
3395
|
|
|
3171
3396
|
/**
|
|
3172
|
-
* ⑥ L4
|
|
3397
|
+
* ⑥ L4 人放行:**只读**审批栈的权威记录,不推断。
|
|
3173
3398
|
* 发起者是 `tools/pre-execute` 瀑布;这里只认那条记录——`human/released`
|
|
3174
3399
|
* 已经在上面(别的门之前)落过账,所以这里不写第二遍。
|
|
3175
3400
|
*/
|
|
@@ -3183,7 +3408,7 @@ export function apply(ctx, config = {}) {
|
|
|
3183
3408
|
|
|
3184
3409
|
// ⑦ 写证据 + 推进(只增不改)
|
|
3185
3410
|
/**
|
|
3186
|
-
*
|
|
3411
|
+
* **出处在这一刻定下来**:四类入口(产物 / 评估卡 / 评估者子会话 / 人放行)
|
|
3187
3412
|
* 全部解析成事实写进账里,界面只渲染、不猜。
|
|
3188
3413
|
*/
|
|
3189
3414
|
const originInfo = buildEvidenceOrigins({
|
|
@@ -3252,7 +3477,7 @@ export function apply(ctx, config = {}) {
|
|
|
3252
3477
|
|
|
3253
3478
|
// ═══ git 世界线层:世界线 = 分支 + worktree ═══════════════════════════════
|
|
3254
3479
|
//
|
|
3255
|
-
//
|
|
3480
|
+
// 探针换来三条硬事实(见 tools/spike-git-worldlines.mjs):
|
|
3256
3481
|
// · worktree 隔离完美,落选后 branch ref 永久可读(P5 的落点);
|
|
3257
3482
|
// · **主线脏且与世界线改动重叠时 git 直接拒绝合并** → 先把用户手上的快照落成一条提交,再合并;
|
|
3258
3483
|
// · **落选世界线里还有未提交的活时 git 拒绝删 worktree** → 先提交到分支,再删工作副本。
|
|
@@ -3334,7 +3559,7 @@ export function apply(ctx, config = {}) {
|
|
|
3334
3559
|
/**
|
|
3335
3560
|
* 基线的提交信息可以**由调用方给**:账本是懒建的,如果第一次建立就发生在某一步交付那一刻,
|
|
3336
3561
|
* 那么这一笔既是基线、也是那一步的账 —— 信息必须写成那一步,否则那一步就"没有自己的提交"了
|
|
3337
|
-
* (
|
|
3562
|
+
* (失效模式:基线把第一步的内容吞掉,历史里那一步只剩一条匿名基线)。
|
|
3338
3563
|
*/
|
|
3339
3564
|
const committed = added.ok ? gitAt(context, [...GIT_IDENTITY, 'commit', '-qm', baselineMessage ?? 'clearai:旁路账本基线(工作区当时的全部内容)']) : added
|
|
3340
3565
|
if (!committed.ok && !/nothing to commit|无文件要提交|working tree clean/i.test(`${committed.out}${committed.err}`)) {
|
|
@@ -3353,7 +3578,7 @@ export function apply(ctx, config = {}) {
|
|
|
3353
3578
|
// ── 账本(D3/D4:账本 = git,与世界线共用一本) ─────────────────────────────
|
|
3354
3579
|
//
|
|
3355
3580
|
// ClearAI 的账本是**每次写入**都记一笔(带 turn/tool 归属),我们记在**交付点**:
|
|
3356
|
-
// 一次提交 =
|
|
3581
|
+
// 一次提交 = 一次「这一步交付时工作区长什么样」。这是刻意的偏离:
|
|
3357
3582
|
// 每次写入的归属在 DSH 里属于宿主的 fs 领域,而交付点归属是内核真正知道的事实。
|
|
3358
3583
|
// 两条性质照抄不变:**只前进**(恢复 = 新版本 + 新提交)与**留下来源**(提交信息写步 id)。
|
|
3359
3584
|
|
|
@@ -3413,11 +3638,11 @@ export function apply(ctx, config = {}) {
|
|
|
3413
3638
|
/**
|
|
3414
3639
|
* 世界线工作副本的容器:**工作区里**(`<workspace>/clear/worldlines/`)。
|
|
3415
3640
|
*
|
|
3416
|
-
*
|
|
3641
|
+
* 位置由**宿主沙箱的硬边界**决定:它只允许写「会话自己的 cwd」,
|
|
3417
3642
|
* 而原生子代理**继承父会话的 cwd**(`dsh-subagent` 的 `childSessionMeta` 写死
|
|
3418
3643
|
* `cwd: parentHeader.cwd`,`SubagentStartRequest` 没有 cwd 字段,沙箱也没有「额外可写根」的缝)。
|
|
3419
3644
|
* 于是原设计那个位置(数据区、jail 之外)会让每个执行者**写不进自己的交付物**:
|
|
3420
|
-
*
|
|
3645
|
+
* 沙箱报 `[sandbox: file access denied under workspace-write mode]`,只能申请提权——而人只会拒绝。
|
|
3421
3646
|
*
|
|
3422
3647
|
* 前提是:「**分支 run 的根就是它自己的 worktree**」,
|
|
3423
3648
|
* 所以它写自己是「jail 之内」。DSH 给不了子会话自己的根 ⇒ 位置必须让位于沙箱边界。
|
|
@@ -3425,7 +3650,7 @@ export function apply(ctx, config = {}) {
|
|
|
3425
3650
|
* 为什么放 `clear/` 而不是 `.clearai/`:这个产品对用户有一句明确承诺——「系统只在你的文件夹里
|
|
3426
3651
|
* 加 `clear/` 一个目录」。放 `clear/worldlines/` 完全在那句承诺之内,而且照样在沙箱可写区里。
|
|
3427
3652
|
*
|
|
3428
|
-
* 代价(
|
|
3653
|
+
* 代价(三条):① 要往账本仓库(工作区本身是 git 仓库时是它的)写
|
|
3429
3654
|
* `.git/info/exclude`,否则每次交付提交会把各条世界线的文件一起提进主线历史;
|
|
3430
3655
|
* ② 「只准写自己那条」从此由**我们的路径守卫**承担(沙箱只保证「在工作区内」);
|
|
3431
3656
|
* ③ 备份/同步会同时看到两份(与旧的放置方式同样的成本)。
|
|
@@ -3468,7 +3693,7 @@ export function apply(ctx, config = {}) {
|
|
|
3468
3693
|
}
|
|
3469
3694
|
|
|
3470
3695
|
/**
|
|
3471
|
-
*
|
|
3696
|
+
* **盘上残留的读数**:截断之后,上一轮的世界线工作副本与分支还躺在盘上,
|
|
3472
3697
|
* 而**新会话的投影是从零开始的**——树上看不到、卡片不提,连「它们存在」这件事都没人报。
|
|
3473
3698
|
*
|
|
3474
3699
|
* 「什么都不删」(P5)不该等于「什么都看不见」:残留是一条**事实**,该像目录、当档一样
|
|
@@ -3981,7 +4206,7 @@ export function apply(ctx, config = {}) {
|
|
|
3981
4206
|
if (typeof observation?.ref !== 'string' || observation.ref.trim() === '') continue
|
|
3982
4207
|
const ref = observation.ref.trim()
|
|
3983
4208
|
/**
|
|
3984
|
-
*
|
|
4209
|
+
* 相对路径按**这条世界线自己的工作副本**解析。
|
|
3985
4210
|
*
|
|
3986
4211
|
* 原来按主线 `cwd` 解析,于是「世界线的观测」永远落在主线路径上,再被下面那条
|
|
3987
4212
|
* 隔离检查拒掉——用户那次真跑里 `AdvanceWorldline` 连续两次报
|
|
@@ -4028,7 +4253,7 @@ export function apply(ctx, config = {}) {
|
|
|
4028
4253
|
|
|
4029
4254
|
const levelIndex = levelIndexOf(branch.level)
|
|
4030
4255
|
/**
|
|
4031
|
-
* **L4
|
|
4256
|
+
* **L4 的两道门,这条路也要走**。
|
|
4032
4257
|
*
|
|
4033
4258
|
* 等级是**事实的属性**,不是工具的属性:同一个 L4,走主线要人放行 + 只认外部来源,
|
|
4034
4259
|
* 走世界线两样都不要——那是门挂错了轴。这里与 `AdvancePlan` 用**同一个**判定
|
|
@@ -4102,7 +4327,7 @@ export function apply(ctx, config = {}) {
|
|
|
4102
4327
|
}
|
|
4103
4328
|
if (validity === null && reading !== null) validity = metricReading(reading) === null ? 'unusable' : 'usable'
|
|
4104
4329
|
/**
|
|
4105
|
-
*
|
|
4330
|
+
* 世界线证据的出处同样在**记账那一刻**定下来。世界线在**自己的工作副本**里作业,
|
|
4106
4331
|
* 所以 `cwd` 就是那份副本 —— 依据里点名的产物按副本核存在性。
|
|
4107
4332
|
*/
|
|
4108
4333
|
const originInfo = buildEvidenceOrigins({
|
|
@@ -4124,7 +4349,7 @@ export function apply(ctx, config = {}) {
|
|
|
4124
4349
|
level: branch.level,
|
|
4125
4350
|
evaluator,
|
|
4126
4351
|
basis,
|
|
4127
|
-
/** `refs`
|
|
4352
|
+
/** `refs` 一律是**路径**:只写材料 id ⇒ 世界线证据一条都开不了。 */
|
|
4128
4353
|
refs: originInfo.paths,
|
|
4129
4354
|
origins: originInfo.origins,
|
|
4130
4355
|
anchor: evaluator === 'independent' ? 'auditor' : 'artifact',
|
|
@@ -4147,7 +4372,8 @@ export function apply(ctx, config = {}) {
|
|
|
4147
4372
|
})
|
|
4148
4373
|
const parsed = metricReading(reading)
|
|
4149
4374
|
// 交付成功前收一次执行者结论:这条世界线的「执行者跑成没跑成」该跟它的读数一起落账。
|
|
4150
|
-
|
|
4375
|
+
// 返回值不再丢弃:本次收到的结论正文要跟着这次交付一起给模型。
|
|
4376
|
+
const collectedOnDeliver = collectExecutors(mutations, hostService.state(sessionId), sessionId)
|
|
4151
4377
|
return done({
|
|
4152
4378
|
ok: true,
|
|
4153
4379
|
code: 'worldline_delivered',
|
|
@@ -4155,7 +4381,8 @@ export function apply(ctx, config = {}) {
|
|
|
4155
4381
|
evaluator,
|
|
4156
4382
|
message:
|
|
4157
4383
|
`世界线「${branch.label}」已交付(裁决 ${verdict}${evaluator === 'independent' ? ' · 独立评估者读数' : ' · 自判'})。` +
|
|
4158
|
-
`读数:${reading ?? '未报'}${parsed === null ? '(读不出一个数 → 不参赛)' : ` → ${parsed}`};有效性:${validity ?? '未声明'}。\n胜负由 ConvergeFork
|
|
4384
|
+
`读数:${reading ?? '未报'}${parsed === null ? '(读不出一个数 → 不参赛)' : ` → ${parsed}`};有效性:${validity ?? '未声明'}。\n胜负由 ConvergeFork 的算术决定。` +
|
|
4385
|
+
(noticeBlock(collectedOnDeliver.notices) === '' ? '' : `\n\n${noticeBlock(collectedOnDeliver.notices)}`),
|
|
4159
4386
|
})
|
|
4160
4387
|
},
|
|
4161
4388
|
})
|
|
@@ -4246,7 +4473,7 @@ export function apply(ctx, config = {}) {
|
|
|
4246
4473
|
/**
|
|
4247
4474
|
* 采纳 = 合并,但**只在那个 git 对象今天真的还在的时候**。
|
|
4248
4475
|
*
|
|
4249
|
-
*
|
|
4476
|
+
* 形态:分叉落账时物化过,后来账本被清理,分支与工作副本一起没了
|
|
4250
4477
|
* ——于是 `fork.git_branch` 还写着名字,合并却必然失败,一份计划被一个**机制上不可能收敛**
|
|
4251
4478
|
* 的分叉卡住(agent 只能改走 AbandonFork)。
|
|
4252
4479
|
*
|
|
@@ -4321,7 +4548,7 @@ export function apply(ctx, config = {}) {
|
|
|
4321
4548
|
}
|
|
4322
4549
|
}
|
|
4323
4550
|
mutations.push({ t: 'fork/converged', fork: fork.id, winner: outcome.winner, margin: outcome.margin, tie: outcome.tie, metric: outcome.metric, direction: outcome.direction })
|
|
4324
|
-
collectExecutors(mutations, hostService.state(sessionId), sessionId)
|
|
4551
|
+
const collectedOnConverge = collectExecutors(mutations, hostService.state(sessionId), sessionId)
|
|
4325
4552
|
return done({
|
|
4326
4553
|
ok: true,
|
|
4327
4554
|
code: 'fork_converged',
|
|
@@ -4330,7 +4557,8 @@ export function apply(ctx, config = {}) {
|
|
|
4330
4557
|
`算术裁决:采纳「${winner?.label ?? outcome.winner}」(${outcome.metric} ${outcome.direction === 'min' ? '越小越好' : '越大越好'}),` +
|
|
4331
4558
|
`差额 ${outcome.margin}${outcome.tie ? ' · 并列(指标说一样好是买到的信息)' : ''}。\n` +
|
|
4332
4559
|
`读数:${outcome.readings.map((row) => `${row.label}=${row.value}`).join('、')}\n` +
|
|
4333
|
-
`落选的世界线标为未采纳,**留档不删**。${mergeNote}\n下一步:把采纳那条的产物落进步骤 ${step.id} 声明的位置,再用 AdvancePlan
|
|
4560
|
+
`落选的世界线标为未采纳,**留档不删**。${mergeNote}\n下一步:把采纳那条的产物落进步骤 ${step.id} 声明的位置,再用 AdvancePlan 交付这一步。` +
|
|
4561
|
+
(noticeBlock(collectedOnConverge.notices) === '' ? '' : `\n\n${noticeBlock(collectedOnConverge.notices)}`),
|
|
4334
4562
|
})
|
|
4335
4563
|
},
|
|
4336
4564
|
})
|
|
@@ -4366,11 +4594,11 @@ export function apply(ctx, config = {}) {
|
|
|
4366
4594
|
return done({
|
|
4367
4595
|
ok: true,
|
|
4368
4596
|
code: 'scout_reported',
|
|
4369
|
-
message: `这件事**上一次已经正常回灌过**(同一个任务原文,身份 ${scout.digest}),直接复用它的结论——没有再派一次。要真重跑,把任务原文改一个字。\n\n${
|
|
4597
|
+
message: `这件事**上一次已经正常回灌过**(同一个任务原文,身份 ${scout.digest}),直接复用它的结论——没有再派一次。要真重跑,把任务原文改一个字。\n\n${clipConclusion(scout.conclusion, null)}`,
|
|
4370
4598
|
})
|
|
4371
4599
|
}
|
|
4372
4600
|
/**
|
|
4373
|
-
*
|
|
4601
|
+
* **派出去就不等**:结论走事实通道,不挤在工具结果里——
|
|
4374
4602
|
* 工具结果是「这一动作做完了」的收据,不是「世界发生了什么」的记录。
|
|
4375
4603
|
*/
|
|
4376
4604
|
return done({
|
|
@@ -4456,6 +4684,7 @@ export function apply(ctx, config = {}) {
|
|
|
4456
4684
|
const lines = swept.lines.map((item) => `· ${item}`)
|
|
4457
4685
|
if (swept.recovered > 0) lines.push(`· ${swept.recovered} 条结论是从执行者自己的会话日志里回收的`)
|
|
4458
4686
|
if (swept.lost > 0) lines.push(`· ${swept.lost} 条失联(产物还在各自的工作副本里)`)
|
|
4687
|
+
if (noticeBlock(swept.notices) !== '') lines.push('', noticeBlock(swept.notices))
|
|
4459
4688
|
if (lines.length === 0) lines.push('· 没有在跑的世界线执行者')
|
|
4460
4689
|
return done({ ok: true, code: 'worldline_status', message: lines.join('\n') })
|
|
4461
4690
|
},
|
|
@@ -4464,7 +4693,7 @@ export function apply(ctx, config = {}) {
|
|
|
4464
4693
|
defineTool({
|
|
4465
4694
|
name: 'AwaitWorldlines',
|
|
4466
4695
|
description:
|
|
4467
|
-
'
|
|
4696
|
+
'**有界地等**派出去还没回来的子 run——世界线执行者**与侦察**都算(最长 300 秒),谁的结论回来了就顺手落账。它是「等」这件事的**唯一正当姿势**:换成 bash `sleep` 轮询会把一整个回合的时间烧在空等上,而且结论什么时候回来与你睡多久无关。等待期间不会阻塞别的会话;到点就返回,没回来的照样在跑。',
|
|
4468
4697
|
parameters: {
|
|
4469
4698
|
type: 'object',
|
|
4470
4699
|
properties: {
|
|
@@ -4484,16 +4713,31 @@ export function apply(ctx, config = {}) {
|
|
|
4484
4713
|
const started = Date.now()
|
|
4485
4714
|
let collected = 0
|
|
4486
4715
|
let lines = []
|
|
4716
|
+
/**
|
|
4717
|
+
* 等到的**结论正文**要攒着:摘要行每一拍都重建,正文必须跨拍累积,
|
|
4718
|
+
* 否则「等到的那一拍」过去之后,模型只剩一句「回灌 1 条结论」。
|
|
4719
|
+
*/
|
|
4720
|
+
const notices = []
|
|
4487
4721
|
const pending = () => {
|
|
4488
4722
|
const before = mutations.length
|
|
4489
4723
|
const swept = collectExecutors(mutations, hostService.state(sessionId), sessionId)
|
|
4490
|
-
|
|
4491
|
-
|
|
4724
|
+
/**
|
|
4725
|
+
* 「回灌几条」数的是**结论**,不是变更条数:一条侦察会落两条变更
|
|
4726
|
+
* (`scout/settled` + 观测),一条世界线落一条 `worldline/executed`。
|
|
4727
|
+
* 模型读这句话是判断「它回来了没有」——数变更会报出比事实大的数字
|
|
4728
|
+
* (一条侦察报成两条),那是这一层最不该有的假话。
|
|
4729
|
+
*/
|
|
4730
|
+
collected += mutations.slice(before).filter((mutation) => mutation.t === 'scout/settled' || mutation.t === 'worldline/executed').length
|
|
4492
4731
|
lines = swept.lines.map((item) => `· ${item}`)
|
|
4493
4732
|
if (swept.recovered > 0) lines.push(`· ${swept.recovered} 条结论是从执行者自己的会话日志里回收的`)
|
|
4494
4733
|
if (swept.lost > 0) lines.push(`· ${swept.lost} 条失联(产物还在各自的工作副本里)`)
|
|
4495
|
-
|
|
4496
|
-
|
|
4734
|
+
notices.push(...(swept.notices ?? []))
|
|
4735
|
+
/**
|
|
4736
|
+
* 「还在跑」= 世界线执行者(内存表里真有、还没落定的那些)**加上**侦察。
|
|
4737
|
+
* 少了侦察这一票,只有侦察在跑时这个循环第一拍就退出,
|
|
4738
|
+
* 而 SpawnScout 的返回原话恰恰让模型「用 AwaitWorldlines 在这个回合里等它」。
|
|
4739
|
+
*/
|
|
4740
|
+
return swept.lines.filter((line) => line.endsWith('仍在跑')).length + (swept.scoutsPending ?? 0)
|
|
4497
4741
|
}
|
|
4498
4742
|
/**
|
|
4499
4743
|
* 等:每 2 秒看一次表。**不 busy-wait**——每次都是一个真定时器,
|
|
@@ -4522,11 +4766,15 @@ export function apply(ctx, config = {}) {
|
|
|
4522
4766
|
const recovered = lines.filter((line) => line.includes('从执行者自己的会话日志里回收')).length
|
|
4523
4767
|
const lostLines = lines.filter((line) => line.includes('条失联'))
|
|
4524
4768
|
const head =
|
|
4525
|
-
`等了 ${waited}s(上限 ${wait}s):回灌 ${collected}
|
|
4769
|
+
`等了 ${waited}s(上限 ${wait}s):回灌 ${collected} 条结论` +
|
|
4526
4770
|
(waited >= wait && running > 0 ? `,**还有 ${running} 条仍在跑**(到点返回,不是失败:它们跑的是真活,可以再来等一次,或先干别的)` : '') +
|
|
4527
4771
|
(recovered > 0 ? `;其中 ${recovered} 条是从执行者自己的会话日志里回收的` : '') +
|
|
4528
4772
|
(lostLines.length > 0 ? `;${lostLines.join('、')}` : '')
|
|
4529
|
-
return done({
|
|
4773
|
+
return done({
|
|
4774
|
+
ok: true,
|
|
4775
|
+
code: 'worldlines_awaited',
|
|
4776
|
+
message: `${head}\n${(doneLines.length > 0 ? doneLines : lines).join('\n')}${noticeBlock(notices) === '' ? '' : `\n\n${noticeBlock(notices)}`}`,
|
|
4777
|
+
})
|
|
4530
4778
|
},
|
|
4531
4779
|
})
|
|
4532
4780
|
|
|
@@ -4687,7 +4935,7 @@ export function apply(ctx, config = {}) {
|
|
|
4687
4935
|
/**
|
|
4688
4936
|
* ═══ 项目章程(``PROJECT.md``):把「它还是不是一个空壳」变成一条可查的事实 ═══
|
|
4689
4937
|
*
|
|
4690
|
-
*
|
|
4938
|
+
* 为什么需要它:没有它,跑完几轮、交付了产物、记忆写了两份,
|
|
4691
4939
|
* `PROJECT.md` 还是铺工作区那一刻的模板——18 处占位原封不动,谁也没发现。
|
|
4692
4940
|
* 根因不是模型懒:章程的**读**侧已经是最好的(原生指令文件每步重投影),但**写**侧
|
|
4693
4941
|
* 只有一句提示词、没有任何时机,而且**没有任何地方让模型或人看见「它还是空壳」**。
|
|
@@ -4701,9 +4949,9 @@ export function apply(ctx, config = {}) {
|
|
|
4701
4949
|
/**
|
|
4702
4950
|
* 章程的读数——**只有文件系统事实**:在不在、多大、最后改动什么时候。
|
|
4703
4951
|
*
|
|
4704
|
-
*
|
|
4705
|
-
* 「要有计数吗」)
|
|
4706
|
-
* ·
|
|
4952
|
+
* 这里原来那份「占位 X/Y 条 + 当前阶段解析」已砍掉(
|
|
4953
|
+
* 「要有计数吗」)。三条理由:
|
|
4954
|
+
* · **脆**:它是对文本做格式解析——模型把章程第 5 节写成 `- **变更记录**(一行即…)`,
|
|
4707
4955
|
* 要求 `**标签**:` 的规则当场把条目数从 18 数成 17。改一个标点,「事实」就变了;
|
|
4708
4956
|
* · **可刷**:「占位 0/18」可以靠删占位、灌废话达到——奖励的是把数字清掉,不是把章程写实;
|
|
4709
4957
|
* · **第二本账**:章程每回合被**原生指令文件整份注入**上下文,模型手里就是原文。
|
|
@@ -4775,7 +5023,7 @@ export function apply(ctx, config = {}) {
|
|
|
4775
5023
|
// 正文文件 = 技能目录下的 `SKILL.md`(原生 filesystem provider 自己的约定:
|
|
4776
5024
|
// `path = join(dir,'SKILL.md')`、`resourceBase.path = 技能目录`)。
|
|
4777
5025
|
// 记忆那条是**虚拟条目**(根本没有正文文件),所以它是 null——面板据此不画链接,
|
|
4778
|
-
// 而不是让人点进去撞一个 404
|
|
5026
|
+
// 而不是让人点进去撞一个 404。
|
|
4779
5027
|
file: dir === null || String(skill?.source ?? '') === 'clearai-memory' ? null : join(dir, 'SKILL.md'),
|
|
4780
5028
|
// 面板读正文走的是**工作区内**的读面(路径守卫),所以「在不在工作区里」这条
|
|
4781
5029
|
// 事实由知道 cwd 的这一侧算好发下去,免得面板去猜。
|
|
@@ -4804,7 +5052,7 @@ export function apply(ctx, config = {}) {
|
|
|
4804
5052
|
return CFG.autonomy
|
|
4805
5053
|
}
|
|
4806
5054
|
/**
|
|
4807
|
-
*
|
|
5055
|
+
* **本体形状**下发:面板那一格页眉要从**声明**生成,不能在界面里手抄一份
|
|
4808
5056
|
* (抄一份就会漂移——而漂移的界面比没有界面更坏)。所以把形状当事实发下去,
|
|
4809
5057
|
* 与目录/当档同一条纪律:**变了才发**;它一个会话只发一次(本体在进程内是常量)。
|
|
4810
5058
|
*/
|
|
@@ -5127,7 +5375,7 @@ export function apply(ctx, config = {}) {
|
|
|
5127
5375
|
}
|
|
5128
5376
|
|
|
5129
5377
|
/**
|
|
5130
|
-
*
|
|
5378
|
+
* **人放行的权威记录**。
|
|
5131
5379
|
*
|
|
5132
5380
|
* 原生审批栈在**会话日志**里落了一对事件:`approval/asked{id,toolName,callId}` +
|
|
5133
5381
|
* `approval/decided{id,outcome}`。`allowed-once` 是唯一的放行,而审批策略只有 `ask`/`never`
|
|
@@ -5165,7 +5413,7 @@ export function apply(ctx, config = {}) {
|
|
|
5165
5413
|
}
|
|
5166
5414
|
|
|
5167
5415
|
/**
|
|
5168
|
-
* L4
|
|
5416
|
+
* L4 的那道门挂在**等级**上,不挂在工具名上。
|
|
5169
5417
|
*
|
|
5170
5418
|
* 同一个 `level: 'L4'`,主线按**步骤**等级、世界线按**分支**等级——两条交付路径必须是同一道门。
|
|
5171
5419
|
* 改之前:`l4RequiresHumanRelease` 与 `l4RejectSelfWritten` 只写在 `AdvancePlan` 里,
|
|
@@ -5255,7 +5503,7 @@ export function apply(ctx, config = {}) {
|
|
|
5255
5503
|
return { kind: 'deny', reason: `clear/evidence、clear/knowledge/facts、clear/goals 由系统所有,做的人不能写:${suspect}。事实与评估卡只能由系统落盘。` }
|
|
5256
5504
|
}
|
|
5257
5505
|
// L4 人放行:ClearAI 的设计目标未实现,这里用宿主的审批瀑布实现。
|
|
5258
|
-
//
|
|
5506
|
+
// 门挂在**等级**上:主线看步骤等级,世界线看分支等级——两条路同一道门。
|
|
5259
5507
|
{
|
|
5260
5508
|
const target = CFG.l4RequiresHumanRelease ? l4Delivery(state, exec.name, args) : null
|
|
5261
5509
|
if (target !== null && !target.released) {
|
|
@@ -5294,7 +5542,7 @@ export function apply(ctx, config = {}) {
|
|
|
5294
5542
|
* 技能目录是宿主原生那张**合并目录**的一部分,而原生 `tool-skill` 的 pre-step 会在我们前面
|
|
5295
5543
|
* 先取一次快照(`cwd + 作用域 + revision` 做键的缓存)。等轮到自己才铺,那次快照就把**空表**
|
|
5296
5544
|
* 缓存住了——后果不只是面板没内容:模型自己也一直看不到 `clear/skills` 里的 18 条模板技能,
|
|
5297
|
-
* 人引用 `/技能名`
|
|
5545
|
+
* 人引用 `/技能名` 也注入不出正文。
|
|
5298
5546
|
* pre-step 里仍然保留一次调用:铺之前就存在的会话、或事件没送到的情况,兜底。
|
|
5299
5547
|
*
|
|
5300
5548
|
* `cwdHint` 只信**调用方给的** cwd:`agent/created` 那一刻会话服务里可能还查不到这个会话,
|
|
@@ -5345,7 +5593,7 @@ export function apply(ctx, config = {}) {
|
|
|
5345
5593
|
const humanSpoke = entering.some((message) => message?.source?.kind === 'user')
|
|
5346
5594
|
const autoRound = entering.find((message) => message?.source?.kind === 'goal') ?? null
|
|
5347
5595
|
/**
|
|
5348
|
-
*
|
|
5596
|
+
* **本回合的当档从输入读**:人门消息先入 inbox、后落日志,而这一拍跑在它落账之前,
|
|
5349
5597
|
* 所以这里读投影必然是旧档——而这一轮恰恰是人的动作触发的那一轮。
|
|
5350
5598
|
*/
|
|
5351
5599
|
noteCollectTurn(sessionId, payload.turn)
|
|
@@ -5360,7 +5608,7 @@ export function apply(ctx, config = {}) {
|
|
|
5360
5608
|
/**
|
|
5361
5609
|
* 本体货架:每个会话铺一次;第一拍在卡里指一下它(只说一次——它是**常驻事实**,
|
|
5362
5610
|
* 每拍重复就是往上下文里灌水)。模型据此知道「这套系统认哪些对象、每级谁来判」,
|
|
5363
|
-
*
|
|
5611
|
+
* 于是写判据与交付时对得上本体。
|
|
5364
5612
|
*/
|
|
5365
5613
|
let ontologyNote = ''
|
|
5366
5614
|
if (CONTRIB.ontology !== null && CONTRIB.ontology !== undefined && !ontologyShelved.has(sessionId)) {
|
|
@@ -5410,7 +5658,7 @@ export function apply(ctx, config = {}) {
|
|
|
5410
5658
|
if (autonomyPayload !== null && brainNote === '') brainNote = `\n(运行档:${autonomyPayload.value === 'unattended' ? '无人值守' : '人在场'}。)`
|
|
5411
5659
|
|
|
5412
5660
|
/**
|
|
5413
|
-
*
|
|
5661
|
+
* **世界线结论的回灌**:`ForkPlan` 把执行者放出去就返回(事实先落账),
|
|
5414
5662
|
* 结论由这里收——每个回合扫一次已落定的执行者,把 `worldline/executed` 当作**事实**注入。
|
|
5415
5663
|
*
|
|
5416
5664
|
* 为什么走「插件消息的 section」而不是等模型来问:结论是**世界发生的事**,该像目录、
|
|
@@ -5431,11 +5679,16 @@ export function apply(ctx, config = {}) {
|
|
|
5431
5679
|
const done = swept.lines.filter((line) => !line.endsWith('仍在跑'))
|
|
5432
5680
|
if (done.length > 0) brainNote = `${brainNote}\n(世界线执行者已回灌:${done.join('、')}——结论已进投影,逐条用 AdvanceWorldline 交付读数。)`
|
|
5433
5681
|
}
|
|
5682
|
+
/**
|
|
5683
|
+
* 这一拍收上来的结论,**没有原生通知的那一档**要把正文直接放进注记:
|
|
5684
|
+
* 它是「没有工具调用却收到结论」的唯一路径,不带正文的话,这条结论就只剩计数了。
|
|
5685
|
+
*/
|
|
5686
|
+
if (noticeBlock(swept.notices) !== '') brainNote = `${brainNote}\n\n${noticeBlock(swept.notices)}`
|
|
5434
5687
|
} catch (error) {
|
|
5435
5688
|
ctx.logger?.warn?.(`clearai: 世界线结论回灌失败 ${String(error?.message ?? error).slice(0, 160)}`)
|
|
5436
5689
|
}
|
|
5437
5690
|
/**
|
|
5438
|
-
*
|
|
5691
|
+
* 失联的评估者:重启之后 `pendingAudits` 空了,而投影里那条裁决还停在「在跑」——
|
|
5439
5692
|
* 它会把目标按在 hold 上。这里问一次宿主的子代理目录,把「它已经不在跑」如实落成一条事实。
|
|
5440
5693
|
*/
|
|
5441
5694
|
let auditsResolved = false
|
|
@@ -5450,7 +5703,22 @@ export function apply(ctx, config = {}) {
|
|
|
5450
5703
|
ctx.logger?.warn?.(`clearai: 失联裁决盘点失败 ${String(error?.message ?? error).slice(0, 160)}`)
|
|
5451
5704
|
}
|
|
5452
5705
|
/**
|
|
5453
|
-
*
|
|
5706
|
+
* **失联的侦察**:同一拍、同一套判据(原生子代理目录 + 本进程攥着的派遣)。
|
|
5707
|
+
* 一句永久「未回灌」在卡片上就是一句等不到下文的承诺。
|
|
5708
|
+
*/
|
|
5709
|
+
try {
|
|
5710
|
+
// 本拍刚落定的侦察不算失联(它们的 `scout/settled` 还在这一批变更里,投影没前进)。
|
|
5711
|
+
const settledInBatch = new Set(factMutations.filter((mutation) => mutation.t === 'scout/settled').map((mutation) => mutation.id))
|
|
5712
|
+
const scouts = await sweepLostScouts(sessionId, hostService.state(sessionId), settledInBatch)
|
|
5713
|
+
if (scouts.mutations.length > 0) {
|
|
5714
|
+
factMutations.push(...scouts.mutations)
|
|
5715
|
+
brainNote = `${brainNote}\n(有 ${scouts.lost} 条侦察**失联**(子会话已不在跑):已如实记下「结论收不回来」——不必再等它,需要那份材料就重新派一次。)`
|
|
5716
|
+
}
|
|
5717
|
+
} catch (error) {
|
|
5718
|
+
ctx.logger?.warn?.(`clearai: 失联侦察盘点失败 ${String(error?.message ?? error).slice(0, 160)}`)
|
|
5719
|
+
}
|
|
5720
|
+
/**
|
|
5721
|
+
* **盘上残留**:投影从零开始,可盘上的上一轮还在。
|
|
5454
5722
|
* 每个会话扫一次(签名 = 工作目录 + 当前这盘的分叉 id 列表,变了才重扫),
|
|
5455
5723
|
* 有残留就如实报一条读数——不落状态、不新增变更类型。
|
|
5456
5724
|
*/
|
|
@@ -5458,7 +5726,7 @@ export function apply(ctx, config = {}) {
|
|
|
5458
5726
|
const cwd = sessionCwd(sessionId)
|
|
5459
5727
|
const state = hostService.state(sessionId)
|
|
5460
5728
|
/**
|
|
5461
|
-
*
|
|
5729
|
+
* **事实货架**:事实变了才重写、才在卡里提一句——**变了才发**,与目录同一条纪律。
|
|
5462
5730
|
* 事实很少变(升格一次),所以这句话在大多数回合里都不出现。
|
|
5463
5731
|
*/
|
|
5464
5732
|
const shelf = ensureFactsShelf(sessionId, state)
|
|
@@ -5489,7 +5757,7 @@ export function apply(ctx, config = {}) {
|
|
|
5489
5757
|
if (brainNote === '' && brainPayload === null && autonomyPayload === null && ontologyPayload === null && factMutations.length === 0) return decision
|
|
5490
5758
|
return { kind: 'enter', messages: [...decision.messages, pluginNotice(payload, brainNote, brainPayload, autonomyPayload, factMutations)] }
|
|
5491
5759
|
}
|
|
5492
|
-
//
|
|
5760
|
+
// 档位不再随回合变化(它是部署预设的初值)⇒ 卡片照投影渲染,没有"本回合的新档"要覆盖。
|
|
5493
5761
|
card = hostService.renderCard(sessionId)
|
|
5494
5762
|
const derived = hostService.derive(sessionId)
|
|
5495
5763
|
// 续跑窗口的接管:策略说 drive 就布防(人开口则换新窗口)、hold 就停、
|