dsh-vibe-math 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT-CHECKLIST.md +145 -0
- package/README.md +29 -2
- package/RELEASE-NOTES-2.2.0.md +266 -0
- package/RELEASE-NOTES-2.2.1.md +43 -0
- package/audit-v5-integrity.mjs +343 -0
- package/audit-v5-sensitivity.mjs +306 -0
- package/e2e-v5-round2.test.mjs +521 -0
- package/package.json +90 -80
- package/prompt-corpus-v5/prompt-corpus-v5.json +1209 -0
- package/prompt-corpus-v5/prompt-corpus-v5.md +7520 -0
- package/prompt-v5-integrity.test.mjs +942 -0
- package/selfdrive-v5.mjs +470 -0
- package/vibe-math-v5/vibe-math-v5.js +377 -140
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +159 -3
|
@@ -13,6 +13,18 @@
|
|
|
13
13
|
// consensus tally, context compaction and resume. It NEVER assigns tasks — the
|
|
14
14
|
// academician does, as a member who is himself bound by the same m-vote rule.
|
|
15
15
|
//
|
|
16
|
+
// WHAT A MEMBER READS IS THE PRODUCT. Every prompt builder takes the member it addresses
|
|
17
|
+
// and derives the [状态] block, the roster, the quorum and the charter from THAT member —
|
|
18
|
+
// never from a "the last member we touched" global. A prompt naming the wrong identity is
|
|
19
|
+
// a fatal bug no tool-level assertion can see, so:
|
|
20
|
+
// · `briefBlock` FAILS LOUDLY when it is not told which member it describes;
|
|
21
|
+
// · `spawnMember` commits the member to the ACTIVE roster BEFORE building its brief;
|
|
22
|
+
// · the charter is frozen at hire (it says "你入职时的在册编制") and reused on resume;
|
|
23
|
+
// · a rebuilt session is framed as a rebuild, never as an induction;
|
|
24
|
+
// · framing names the TRUE sender and kind, and framework feedback has its own sender.
|
|
25
|
+
// `prompt-v5-integrity.test.mjs` asserts all of that against the real prompt text and
|
|
26
|
+
// writes the full corpus to `prompt-corpus-v5/` for human review (实现方案.md §14.5).
|
|
27
|
+
//
|
|
16
28
|
// Durable state lives in a HOST-ONLY session projection unit (key `vibeMathV5`):
|
|
17
29
|
// institute events are appended to the session log, never enter the model history
|
|
18
30
|
// (zero context cost), are checkpointed by DSH, and are replayed on restore — which
|
|
@@ -520,6 +532,14 @@ export function apply(ctx) {
|
|
|
520
532
|
return Math.max(1, Math.min(cap, voterCount()))
|
|
521
533
|
}
|
|
522
534
|
function byChild(childId) { return inst().members.find((m) => m.childId === childId) }
|
|
535
|
+
// The live academician's id, or '' when the office founded the institute with
|
|
536
|
+
// `academician: false`. Every piece of charter text that talks about "the leader"
|
|
537
|
+
// must go through this: a hard-coded 'acad' told members to report to a leader who
|
|
538
|
+
// does not exist, and described organizational duties nobody holds.
|
|
539
|
+
function academicianId() {
|
|
540
|
+
const a = activeMembers().find((m) => m.kind === 'academician')
|
|
541
|
+
return a ? a.id : ''
|
|
542
|
+
}
|
|
523
543
|
|
|
524
544
|
// ---- the institute charter (public regulations) -----------------------
|
|
525
545
|
// Written into every member's `persona` at hire time. `persona` is part of the
|
|
@@ -569,28 +589,47 @@ export function apply(ctx) {
|
|
|
569
589
|
' ③ 推荐**直接用 fs 写你自己的文件**;vibe_v5_record_* 只是便捷记录器,不是必需。',
|
|
570
590
|
].join('\n')
|
|
571
591
|
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
592
|
+
// Computed per hire (a FUNCTION, not a frozen const): the text names the live
|
|
593
|
+
// academician, and must describe a leaderless institute honestly when the office
|
|
594
|
+
// founded one with `academician: false`.
|
|
595
|
+
function orgCommon() {
|
|
596
|
+
const a = academicianId()
|
|
597
|
+
const L = [
|
|
598
|
+
' 本所是自组织的,但**不是没有组织**——现实中一个研究所也有所长/学术带头人统筹全局。',
|
|
599
|
+
]
|
|
600
|
+
if (a) {
|
|
601
|
+
L.push(
|
|
602
|
+
' 本所的领头人是**院士 ' + a + '**。它以**全所视角**组织与协调:',
|
|
603
|
+
' ① **统筹全局**:掌握各方向布局、谁在做什么、哪里是瓶颈、哪里有重复或空白;',
|
|
604
|
+
' ② **规划与分派**:把原问题拆成值得做的工作,作为**任务**分派给合适的成员(含临时工)。',
|
|
605
|
+
' 分派是它的职责,不是越权;',
|
|
606
|
+
' ③ **设定优先级**:多个方向并行时,它有责任指明"先做什么、什么可以缓、什么该放弃";',
|
|
607
|
+
' ④ **协调资源**:决定临时工往哪里调配;建议增聘/解聘常驻研究员;',
|
|
608
|
+
' ⑤ **主持会议**:由它召集正式会议、设定议程、维持讨论不跑偏,并把结论落实为任务;',
|
|
609
|
+
' ⑥ **督导进度**:定期检查各成员的 Progress/ 与会议发言,催办停滞的方向、纠正偏离、',
|
|
610
|
+
' 在成员之间牵线;',
|
|
611
|
+
' ⑦ **对外代表**:通过所办向外部汇报与提要求。',
|
|
612
|
+
'',
|
|
613
|
+
' 对**你**(非院士)的要求:',
|
|
614
|
+
' · **主动汇报**:把你这一轮的进展、发现、卡点写进你自己的 Progress/,并把关键结论在',
|
|
615
|
+
' 群聊里说出来——院士需要这些信息才能统筹;',
|
|
616
|
+
' · **接受分派,但不要盲从**:院士分派给你的任务,默认应当执行;如果你认为方向错了、',
|
|
617
|
+
' 信息过时、或你有更好的路线,**先说清理由再决定**——本所允许并鼓励有理据的反对。',
|
|
618
|
+
' 真正的原则是:组织由院士负责,但**判断属于每个人自己**;',
|
|
619
|
+
' · **有异议走会议**:若你与院士在方向上持续分歧,提议开会,让全所讨论;',
|
|
620
|
+
' · **不要重复劳动**:做之前先看任务板和别人的库;发现别人已在做同一件事,告诉院士。')
|
|
621
|
+
} else {
|
|
622
|
+
L.push(
|
|
623
|
+
' 本所当前**没有在册院士**(所办以无领头人方式建所):组织与协调由**全体有表决权者',
|
|
624
|
+
' 共同商议**,通过群聊、提议开会(vibe_v5_meeting)与任务板完成。请特别注意:',
|
|
625
|
+
' · 没有谁替你分派工作——**方向要你们自己讨论出来**,并把讨论结果落到任务板上;',
|
|
626
|
+
' · 提议开会需要有人附议/由所办确认(只有院士或所办能直接召开);',
|
|
627
|
+
' · **主动汇报**:把你的进展、发现、卡点写进你自己的 Progress/ 并说在群聊里,',
|
|
628
|
+
' 否则别人无从与你协作;',
|
|
629
|
+
' · **不要重复劳动**:做之前先看任务板和别人的库;发现重复,直接在群聊里指出。')
|
|
630
|
+
}
|
|
631
|
+
return L.join('\n')
|
|
632
|
+
}
|
|
594
633
|
|
|
595
634
|
const ACAD_ORG = [
|
|
596
635
|
' 【四、你的组织职责与边界(院士)】',
|
|
@@ -620,6 +659,10 @@ export function apply(ctx) {
|
|
|
620
659
|
function charterFor(member) {
|
|
621
660
|
const kind = member.kind
|
|
622
661
|
const m = quorumM()
|
|
662
|
+
// The leader's REAL id (or '' when the office founded a leaderless institute).
|
|
663
|
+
// Charter text must never name a leader who is not on staff: a member told to
|
|
664
|
+
// "report to the academician" when there is none has no one to report to.
|
|
665
|
+
const acadId = academicianId()
|
|
623
666
|
const L = []
|
|
624
667
|
// ── opening ──────────────────────────────────────────────────────────
|
|
625
668
|
if (kind === 'academician') {
|
|
@@ -639,19 +682,26 @@ export function apply(ctx) {
|
|
|
639
682
|
L.push('你的任务期至:' + (member.term || '雇主另行通知') + '。任务完成后请主动告知雇主。')
|
|
640
683
|
L.push('')
|
|
641
684
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
685
|
+
if (acadId || kind === 'academician') {
|
|
686
|
+
L.push('本所没有**外部**派活:做什么、往哪走,由所内自己决定。所内的组织与协调由**院士**牵头——')
|
|
687
|
+
L.push('它统筹全局、把工作拆解成分派下去、设定优先级、主持会议、督导进度;你则在自己的方向上')
|
|
688
|
+
L.push('深入钻研,把进展与判断汇报给它和全所。请记住这条分工:**组织由院士负责,但判断属于')
|
|
689
|
+
L.push('你自己**——它分派的是工作,不是结论。')
|
|
690
|
+
} else {
|
|
691
|
+
L.push('本所没有**外部**派活:做什么、往哪走,由所内自己决定。本所当前**没有在册院士**,')
|
|
692
|
+
L.push('组织与协调由**全体有表决权者共同商议**(所办代表外部);但请守住同一条分工:')
|
|
693
|
+
L.push('**组织归集体,判断属于你自己**——讨论决定的是工作,不是结论。')
|
|
694
|
+
}
|
|
646
695
|
L.push('')
|
|
647
696
|
L.push('────────────────────────────────────────')
|
|
648
697
|
// ── 一、roster ───────────────────────────────────────────────────────
|
|
649
698
|
L.push('【一、所内编制与你的同事】')
|
|
650
699
|
if (kind === 'temp') {
|
|
651
|
-
L.push(' · **院士
|
|
652
|
-
L.push(' 会议、督导进度,也可以直接分派任务给你。')
|
|
700
|
+
if (acadId) L.push(' · **院士 ' + acadId + '** —— 本所领头人,组织与协调中心。它统筹全所、分派任务、主持')
|
|
701
|
+
if (acadId) L.push(' 会议、督导进度,也可以直接分派任务给你。')
|
|
653
702
|
L.push(' · **常驻研究员** —— 本所有表决权者。你是临时雇入的协作人员。')
|
|
654
|
-
L.push(' · 你的雇主:' + (member.hiredBy || '?') + '
|
|
703
|
+
L.push(' · 你的雇主:' + (member.hiredBy || '?') + '。它给你派活' + (acadId ? ';院士也可以给你派活。' : '。'))
|
|
704
|
+
if (!acadId) L.push(' · 本所当前**没有在册院士**;组织与协调由全体有表决权者共同商议。')
|
|
655
705
|
} else if (kind === 'academician') {
|
|
656
706
|
L.push(' · **院士 ' + member.id + '(你)** —— 本所领头人,本所的**组织与协调中心**。你亲自参与')
|
|
657
707
|
L.push(' 研究,同时向全所负责:建立全所视图、拆解并分派工作、设定优先级、主持会议、')
|
|
@@ -663,23 +713,28 @@ export function apply(ctx) {
|
|
|
663
713
|
L.push(' 可写自己的成果库、可认领或被分派任务,但**没有表决权**。')
|
|
664
714
|
L.push(' · **所办(对外接口)** —— 不参与研究、不投票。代表本所与外部沟通并转达外部指令。')
|
|
665
715
|
} else {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
716
|
+
if (acadId) {
|
|
717
|
+
L.push(' · **院士 ' + acadId + '** —— 本所领头人,本所的**组织与协调中心**。它亲自参与研究,同时')
|
|
718
|
+
L.push(' 向全所负责:建立全所视图、把原问题拆解成工作并**分派**给合适的成员(含临时工)、')
|
|
719
|
+
L.push(' 设定优先级与路线取舍、召集并主持会议、督导进度与催办停滞、调配临时工。')
|
|
720
|
+
L.push(' 但它的一票与你**等重**,不能单方面定论。')
|
|
721
|
+
L.push(' · **常驻研究员(含你)** —— 有表决权。可自主雇佣/解雇自己的临时工。')
|
|
722
|
+
L.push(' 向院士汇报进展、接受其组织与分派。')
|
|
723
|
+
} else {
|
|
724
|
+
L.push(' · **常驻研究员(含你)** —— 有表决权。可自主雇佣/解雇自己的临时工。')
|
|
725
|
+
L.push(' 本所当前**没有在册院士**:方向由你们共同商议决定,不要等别人来派活。')
|
|
726
|
+
}
|
|
727
|
+
L.push(' · **临时工** —— 由某位研究员' + (acadId ? '或院士' : '') + '为特定任务临时雇入。可读、可想、可发言、')
|
|
673
728
|
L.push(' 可写自己的成果库、可认领或被分派任务,但**没有表决权**。')
|
|
674
729
|
L.push(' · **所办(对外接口)** —— 不参与研究、不投票。代表本所与外部沟通并转达外部指令。')
|
|
675
730
|
}
|
|
676
|
-
L.push('
|
|
731
|
+
L.push(' 你入职时的在册编制(这是一份**快照**,此后可能变化):')
|
|
677
732
|
L.push(rosterLine())
|
|
678
|
-
L.push('
|
|
733
|
+
L.push(' (权威的在册名单与法定票数 m 以每轮提示里的状态块为准;编制可能变化。)')
|
|
679
734
|
L.push('')
|
|
680
735
|
// ── 二、general rules ────────────────────────────────────────────────
|
|
681
736
|
L.push('【二、通用规章(全员必读)】')
|
|
682
|
-
L.push(' 1.
|
|
737
|
+
L.push(' 1. 本所一切任务安排由成员讨论' + (acadId ? '与院士组织' : '共同') + '决定;没有**外部**给你派活。')
|
|
683
738
|
L.push(' 2. 只有 Verified/ 目录下的结论(以及成果卡中标注"已验证·真/假"的条目)绝对可信。')
|
|
684
739
|
L.push(' 其余一切——他人的推测、你自己的未验结论、Progress/、Methods/ 里的未验证断言——')
|
|
685
740
|
L.push(' 都只是经验性参考,引用时必须注明"未验证"。')
|
|
@@ -688,8 +743,13 @@ export function apply(ctx) {
|
|
|
688
743
|
L.push(' 价值程度、动机用途计划、你自己对"该对象为真"的概率估计。')
|
|
689
744
|
L.push(' 5. 你随时可以在群聊里说话;要单独找人可以私信。需要集体决策就提议开会。')
|
|
690
745
|
L.push(' 6. 请主动读同事的库,对齐事实、避免重复劳动、发现冲突。')
|
|
691
|
-
|
|
692
|
-
|
|
746
|
+
if (acadId) {
|
|
747
|
+
L.push(' 7. **主动向院士汇报**:它需要你的进展、发现与卡点才能统筹全所;把关键结论在群聊里')
|
|
748
|
+
L.push(' 说出来,把细节留在你自己的 Progress/ 里。')
|
|
749
|
+
} else {
|
|
750
|
+
L.push(' 7. **主动在群聊里汇报**:本所没有院士替你统筹,你不说别人就无从与你协作;把关键')
|
|
751
|
+
L.push(' 结论说出来,把细节留在你自己的 Progress/ 里。')
|
|
752
|
+
}
|
|
693
753
|
L.push('')
|
|
694
754
|
// ── 三、libraries ───────────────────────────────────────────────────
|
|
695
755
|
L.push('【三、你的资料库、progress 与卡片格式】')
|
|
@@ -702,29 +762,39 @@ export function apply(ctx) {
|
|
|
702
762
|
if (kind === 'academician') {
|
|
703
763
|
L.push(ACAD_ORG)
|
|
704
764
|
} else {
|
|
705
|
-
L.push('
|
|
706
|
-
L.push(
|
|
707
|
-
if (kind === 'temp') {
|
|
765
|
+
L.push('【四、所内的组织与协调' + (acadId ? '(院士领头)' : '(无院士:集体商议)') + '】')
|
|
766
|
+
L.push(orgCommon())
|
|
767
|
+
if (kind === 'temp' && acadId) {
|
|
708
768
|
L.push(' · **院士也可以直接分派任务给你**(它统筹全所)。雇主与院士的分派都应执行;')
|
|
709
769
|
L.push(' 若你认为分派有误,先说清理由。')
|
|
770
|
+
} else if (kind === 'temp') {
|
|
771
|
+
L.push(' · 本所当前没有在册院士:你只需向**雇主**负责(它给你派活)。')
|
|
710
772
|
}
|
|
711
773
|
}
|
|
712
774
|
L.push('')
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
775
|
+
if (acadId || kind === 'academician') {
|
|
776
|
+
L.push(' 【重要】分派**不改变求真规则**:院士分派任务、设定优先级,但**不能**因此让任何结论')
|
|
777
|
+
L.push(' 变得"正确"。任何对象要进 Verified/,仍然必须满足 m 票布尔一致(见【五】)。院士自己')
|
|
778
|
+
L.push(' 的一票与别人**等重**。')
|
|
779
|
+
} else {
|
|
780
|
+
L.push(' 【重要】组织工作**不改变求真规则**:谁开任务、谁定优先级,都**不能**因此让任何结论')
|
|
781
|
+
L.push(' 变得"正确"。任何对象要进 Verified/,仍然必须满足 m 票布尔一致(见【五】)。任何人的')
|
|
782
|
+
L.push(' 一票都与别人**等重**。')
|
|
783
|
+
}
|
|
716
784
|
L.push('')
|
|
717
785
|
// ── 五、voting ──────────────────────────────────────────────────────
|
|
718
786
|
L.push('【五、表决与定论(求真门槛)】')
|
|
719
787
|
if (kind === 'temp') {
|
|
720
|
-
L.push(' ·
|
|
788
|
+
L.push(' · 本所结论由有表决权者(' + (acadId ? '院士与' : '') + '常驻研究员)按 m 票布尔一致决定。**你没有表决权**,')
|
|
721
789
|
L.push(' 但你的判断很重要——请把你的意见和理由清楚地告诉雇主或在群聊里说出来,供他们')
|
|
722
790
|
L.push(' 参考。若你认为某个结论该被验证,可以提议。')
|
|
723
791
|
} else {
|
|
724
792
|
L.push(' · 任何命题 / 论断 / 方法 / 子问题的结论,要进入 Verified/,必须满足:')
|
|
725
|
-
L.push(' (a) 至少有 m = ' + m + '
|
|
793
|
+
L.push(' (a) 至少有 m = ' + m + ' 名有表决权者(' + (acadId ? '院士 + ' : '') + '常驻研究员)投出**布尔概率值**;')
|
|
726
794
|
L.push(' (b) 这些票**全部**是 1(绝对为真)或**全部**是 0(绝对为假);')
|
|
727
795
|
L.push(' (c) 若同时出现 1 和 0(分歧),或投布尔票者不足 m 人 → 不能定论。')
|
|
796
|
+
L.push(' · m 随在册有表决权者人数变化(m = min(所办设定的上限, 人数));本规章里的 m 是')
|
|
797
|
+
L.push(' **你入职时的值**,请始终以每轮状态块里的 m 为准。')
|
|
728
798
|
L.push(' · 你的票是一个 [0,1] 的数值概率:1 = 你认为绝对为真;0 = 你认为绝对为假;')
|
|
729
799
|
L.push(' 介于 0 与 1 之间表示你不确定——这会被记为"弃权/存疑",**不计入**上述 m 票,')
|
|
730
800
|
L.push(' 但会连同你的理由一起进入辩论录,并参与"全组平均概率"的计算。')
|
|
@@ -756,7 +826,7 @@ export function apply(ctx) {
|
|
|
756
826
|
// ── 七、hire / fire ─────────────────────────────────────────────────
|
|
757
827
|
L.push('【七、雇佣与解雇】')
|
|
758
828
|
if (kind === 'temp') {
|
|
759
|
-
L.push(' ·
|
|
829
|
+
L.push(' · 你可以建议雇主雇佣或解雇他人,但雇佣/解雇的决定权在雇主' + (acadId ? '与院士' : '') + '。')
|
|
760
830
|
} else {
|
|
761
831
|
L.push(' · 你可以自主雇佣临时工:当你需要某个具体任务的帮助时,用 vibe_v5_hire 申请,')
|
|
762
832
|
L.push(' 说明用途与初始任务。框架会代为创建,成功后你会拿到它的代号,之后你可以直接')
|
|
@@ -764,10 +834,12 @@ export function apply(ctx) {
|
|
|
764
834
|
L.push(' · 你也可以自主解雇**你雇的**临时工:用 vibe_v5_fire 说明理由即可。解雇后它的')
|
|
765
835
|
L.push(' 当前工作会被停止,未完成任务会被收回,它将不再是本所成员,也不再收到任何消息。')
|
|
766
836
|
L.push(' 它的档案会留在所史里(代号永不复用)。')
|
|
767
|
-
L.push(' ·
|
|
837
|
+
L.push(' · 解雇别人雇的临时工,或增聘/解聘常驻研究员,只能向全所提议,由' + (acadId ? '院士/' : '') + '所办决定。')
|
|
768
838
|
L.push(' · 请节约用人:临时工是有成本的。任务完成、且你不再需要它时,请主动解雇。')
|
|
769
|
-
|
|
770
|
-
|
|
839
|
+
if (acadId) {
|
|
840
|
+
L.push(' · **院士统筹全所的用人**:它可以决定把临时工调配到哪个方向,也可以解雇任何临时工;')
|
|
841
|
+
L.push(' 若它把你的临时工调走了,请配合——全所效率优先于个人便利。')
|
|
842
|
+
}
|
|
771
843
|
}
|
|
772
844
|
L.push('')
|
|
773
845
|
// ── 八、task board ──────────────────────────────────────────────────
|
|
@@ -776,10 +848,17 @@ export function apply(ctx) {
|
|
|
776
848
|
L.push(' · 任务只有在它的**全部依赖都已完成**之后才能被认领。')
|
|
777
849
|
L.push(' · 认领即拥有;完成后标记完成,或释放回板上,或重新打开。')
|
|
778
850
|
L.push(' · 每次修改都基于版本号比较交换:拿着过期副本去改会被拒绝,所以改之前先读最新版。')
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
851
|
+
if (acadId) {
|
|
852
|
+
L.push(' · **院士可以直接分派任务**(vibe_v5_assign):它可以把任务指派给指定成员(含临时工),')
|
|
853
|
+
L.push(' 并说明理由与验收标准。被分派者默认应当执行,但有权先说明理由再决定。')
|
|
854
|
+
L.push(' · **优先级由院士牵头决定**:院士可以调整任务的优先级;你若认为安排有误,说出来。')
|
|
855
|
+
L.push(' · 除院士的分派之外,任务是**协调工具**而非派活指令:认领与否、做什么,主要靠你们自己。')
|
|
856
|
+
} else {
|
|
857
|
+
L.push(' · 任务是**协调工具**而非派活指令:本所没有院士,认领与否、做什么,靠你们自己协商')
|
|
858
|
+
L.push(' 决定;所办也可以直接分派任务(vibe_v5_assign)。被分派者默认应当执行,但有权先')
|
|
859
|
+
L.push(' 说明理由再决定。')
|
|
860
|
+
L.push(' · **优先级由集体协商决定**;所办可以协助调整。')
|
|
861
|
+
}
|
|
783
862
|
L.push('')
|
|
784
863
|
// ── 九、context ─────────────────────────────────────────────────────
|
|
785
864
|
L.push('【九、上下文与纪律】')
|
|
@@ -799,12 +878,26 @@ export function apply(ctx) {
|
|
|
799
878
|
|
|
800
879
|
// A minimal per-round status block: everything VOLATILE lives here rather than in
|
|
801
880
|
// the immutable persona (roster, current m, pending chat, this round's ask).
|
|
881
|
+
//
|
|
882
|
+
// `member` is the member this block DESCRIBES and MUST be the one the prompt is
|
|
883
|
+
// addressed to. It is a required parameter on purpose: this block used to read a
|
|
884
|
+
// mutable "currentMember" global, and because the founding path assigned that global
|
|
885
|
+
// only AFTER the subagent had already been started, every member's induction brief
|
|
886
|
+
// named the PREVIOUSLY founded member (the academician was told it was "?"). The
|
|
887
|
+
// model's whole self-model, its library path and its vote were therefore wrong.
|
|
802
888
|
function briefBlock(member) {
|
|
889
|
+
if (!member || typeof member.id !== 'string' || !member.id) {
|
|
890
|
+
throw v5err('V5_INTERNAL', 'briefBlock: a member is required (a status block must never be built for an unknown identity)')
|
|
891
|
+
}
|
|
803
892
|
const ms = activeMembers()
|
|
804
893
|
const b = []
|
|
805
894
|
b.push('[状态] 你是 ' + member.id + '(' + kindLabel(member.kind) + ')|轮次 ' + (rounds.get(member.id) || 0) +
|
|
806
895
|
'|法定票数 m=' + quorumM() + '|有表决权者 ' + voterCount() + ' 人')
|
|
807
|
-
b.push('[在册] ' + ms.map((x) => x.id
|
|
896
|
+
b.push('[在册] ' + (ms.length ? ms.map((x) => x.id).join('、') : '(无)'))
|
|
897
|
+
// Members that are on the books but NOT on the floor. Silently omitting them made a
|
|
898
|
+
// failed provision invisible to the whole institute.
|
|
899
|
+
const absent = inst().members.filter((m) => m.phase !== 'active' && m.phase !== 'dismissed')
|
|
900
|
+
if (absent.length) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))
|
|
808
901
|
const tasks = inst().tasks.filter((t) => t.status !== 'deleted')
|
|
809
902
|
const mine = tasks.filter((t) => t.ownerId === member.id && t.status === 'in_progress')
|
|
810
903
|
const ready = tasks.filter((t) => t.status === 'pending' && taskReady(t))
|
|
@@ -939,6 +1032,17 @@ export function apply(ctx) {
|
|
|
939
1032
|
scheduleNext().catch(() => {})
|
|
940
1033
|
return { ok: true, delivered: targets.length, to: targets.map((t) => t.id).join(',') }
|
|
941
1034
|
}
|
|
1035
|
+
// A framework NOTICE to one member. This must NOT be sent as the member itself:
|
|
1036
|
+
// `say()` refuses a self-addressed message (V5_SELF_MESSAGE), so the previous
|
|
1037
|
+
// `say(member.id, {to: member.id, …})` calls returned an error object that nobody
|
|
1038
|
+
// checked and the member never received the feedback ("claim failed", "verdict must
|
|
1039
|
+
// be a number"). The framework is a first-class sender with its own framing.
|
|
1040
|
+
async function notice(memberId, text) {
|
|
1041
|
+
if (!memberId) return { ok: false, code: 'V5_MEMBER_NOT_FOUND' }
|
|
1042
|
+
const m = memberById(memberId)
|
|
1043
|
+
if (!m || m.phase !== 'active') return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: 'active member "' + memberId + '" not found' }
|
|
1044
|
+
return await say('framework', { to: memberId, kind: 'notice', text: String(text) })
|
|
1045
|
+
}
|
|
942
1046
|
// Pending = durable messages addressed to this member and not yet acknowledged.
|
|
943
1047
|
// (Acknowledging is what removes them, so the queue is exactly "queued minus delivered".)
|
|
944
1048
|
function pendingFor(memberId) {
|
|
@@ -955,10 +1059,16 @@ export function apply(ctx) {
|
|
|
955
1059
|
function frameLine(m) {
|
|
956
1060
|
if (m.kind === 'chat') return '【研究所·群聊】' + m.from + ':' + m.text
|
|
957
1061
|
if (m.kind === 'dm') return '【研究所·私信 from ' + m.from + '】' + m.text
|
|
1062
|
+
if (m.kind === 'voters') return '【研究所·致全体表决者 from ' + m.from + '】' + m.text
|
|
958
1063
|
if (m.kind === 'office') return '【所办通知】' + m.text
|
|
959
1064
|
if (m.kind === 'meeting') return '【研究所·会议】' + m.text
|
|
960
1065
|
if (m.kind === 'verify') return '【研究所·表决】' + m.text
|
|
961
|
-
|
|
1066
|
+
// An assignment is framed by its TRUE origin: the office can assign too, and
|
|
1067
|
+
// labelling an office assignment "院士分派" told the assignee to answer to
|
|
1068
|
+
// someone who never asked.
|
|
1069
|
+
if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text
|
|
1070
|
+
if (m.kind === 'nudge') return '【督办 from ' + m.from + '】' + m.text
|
|
1071
|
+
if (m.kind === 'notice') return '【框架提示】' + m.text
|
|
962
1072
|
return '【研究所·' + m.kind + ' from ' + m.from + '】' + m.text
|
|
963
1073
|
}
|
|
964
1074
|
// Batch plain chat so a chatty institute cannot cause a wake storm; anything
|
|
@@ -1048,39 +1158,75 @@ export function apply(ctx) {
|
|
|
1048
1158
|
} catch (e) { /* ignore */ }
|
|
1049
1159
|
return 'spawn'
|
|
1050
1160
|
}
|
|
1051
|
-
|
|
1161
|
+
// Bring a member into being. ORDER IS LOAD-BEARING and is the fix for the
|
|
1162
|
+
// "every brief describes the wrong person" bug:
|
|
1163
|
+
// 1. commit the member as ACTIVE first, so that everything derived from
|
|
1164
|
+
// `activeMembers()` — the [状态]/[在册] block, the quorum m, the voter count and
|
|
1165
|
+
// the charter's induction roster — describes the institute WITH this member in
|
|
1166
|
+
// it. Committing after the spawn made a joiner's own brief omit itself and
|
|
1167
|
+
// report m/P from before it joined.
|
|
1168
|
+
// 2. mark it busy, so the scheduler cannot try to wake a half-born member.
|
|
1169
|
+
// 3. build the persona and the founding prompt (both pure, both identity-checked).
|
|
1170
|
+
// 4. create its directories and write the mirrors BEFORE its first turn, so the
|
|
1171
|
+
// member finds its own Progress/Propos/Methods/Subproblems already in place.
|
|
1172
|
+
// 5. only then start the child.
|
|
1173
|
+
// `mode` is 'founding' for a genuinely new member and 'resume' for one whose child
|
|
1174
|
+
// session is being rebuilt: the latter must NOT be told it "just joined the
|
|
1175
|
+
// institute" and must not be shown the induction blurb.
|
|
1176
|
+
async function spawnMember(member, initialTask, mode) {
|
|
1052
1177
|
const provider = member.provider || pickProvider()
|
|
1053
1178
|
const ao = memberAgentOptions()
|
|
1054
1179
|
const tf = memberToolFilter(member)
|
|
1055
|
-
const
|
|
1056
|
-
member.persona = persona
|
|
1057
|
-
const started = await subagents.startContinuable({
|
|
1058
|
-
provider,
|
|
1059
|
-
label: 'vibe5 ' + member.id + ' (' + kindLabel(member.kind) + ')',
|
|
1060
|
-
request: Object.assign({
|
|
1061
|
-
prompt: [textBlock(initialPrompt(member, initialTask))],
|
|
1062
|
-
parent: rootAgent,
|
|
1063
|
-
persona,
|
|
1064
|
-
}, Object.keys(ao).length ? { agentOptions: ao } : {}, tf ? { toolFilter: tf } : {}),
|
|
1065
|
-
signal: makeSignal(params.activityTimeoutMs),
|
|
1066
|
-
})
|
|
1067
|
-
member.childId = started.childId
|
|
1180
|
+
const kind = mode === 'resume' ? 'resume' : 'initial'
|
|
1068
1181
|
member.phase = 'active'
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
// Without this the child's first `subagent/end` has no token to match, so
|
|
1072
|
-
// onMemberEnd would ignore it: the founding round would never be processed and
|
|
1073
|
-
// the member would be re-woken with a heartbeat prompt instead of a brainstorm.
|
|
1074
|
-
inflight.set(started.childId, shortId())
|
|
1182
|
+
member.childId = ''
|
|
1183
|
+
await putMember(member)
|
|
1075
1184
|
busy.add(member.id)
|
|
1076
|
-
wakeKind.set(member.id,
|
|
1185
|
+
wakeKind.set(member.id, kind)
|
|
1077
1186
|
currentMember = member.id
|
|
1078
1187
|
lastActiveAt.set(member.id, now())
|
|
1079
1188
|
rounds.set(member.id, (rounds.get(member.id) || 0) + 1)
|
|
1080
1189
|
roundsSinceCompact.set(member.id, (roundsSinceCompact.get(member.id) || 0) + 1)
|
|
1190
|
+
// The charter is FROZEN at hire time (it is the durable "seal" record and it says
|
|
1191
|
+
// "你入职时的在册编制"). Rebuilding it on resume would silently rewrite that
|
|
1192
|
+
// hire-time snapshot into a resume-time one and make the sentence untrue.
|
|
1193
|
+
const persona = member.persona || memberPersona(member)
|
|
1194
|
+
member.persona = persona
|
|
1195
|
+
const prompt = initialPrompt(member, initialTask, mode)
|
|
1081
1196
|
await putMember(member)
|
|
1082
1197
|
await mkdirs()
|
|
1083
1198
|
await writeRosterMirror()
|
|
1199
|
+
let started
|
|
1200
|
+
try {
|
|
1201
|
+
started = await subagents.startContinuable({
|
|
1202
|
+
provider,
|
|
1203
|
+
label: 'vibe5 ' + member.id + ' (' + kindLabel(member.kind) + ')',
|
|
1204
|
+
request: Object.assign({
|
|
1205
|
+
prompt: [textBlock(prompt)],
|
|
1206
|
+
parent: rootAgent,
|
|
1207
|
+
persona,
|
|
1208
|
+
}, Object.keys(ao).length ? { agentOptions: ao } : {}, tf ? { toolFilter: tf } : {}),
|
|
1209
|
+
signal: makeSignal(params.activityTimeoutMs),
|
|
1210
|
+
})
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
// Roll the in-memory marks back so a failed provisioning leaves no phantom
|
|
1213
|
+
// "busy, round 1" member behind; the member record itself goes to `failed` and
|
|
1214
|
+
// the caller's catch reports it.
|
|
1215
|
+
busy.delete(member.id)
|
|
1216
|
+
wakeKind.delete(member.id)
|
|
1217
|
+
rounds.delete(member.id)
|
|
1218
|
+
roundsSinceCompact.delete(member.id)
|
|
1219
|
+
await putMember(Object.assign({}, memberById(member.id) || member, { phase: 'failed', error: String((e && e.message) || e) }))
|
|
1220
|
+
throw e
|
|
1221
|
+
}
|
|
1222
|
+
member.childId = started.childId
|
|
1223
|
+
childOwner.set(started.childId, sessionId)
|
|
1224
|
+
// Register the FOUNDING turn as in-flight, exactly like a normal wake does.
|
|
1225
|
+
// Without this the child's first `subagent/end` has no token to match, so
|
|
1226
|
+
// onMemberEnd would ignore it: the founding round would never be processed and
|
|
1227
|
+
// the member would be re-woken with a heartbeat prompt instead of a brainstorm.
|
|
1228
|
+
inflight.set(started.childId, shortId())
|
|
1229
|
+
await putMember(member)
|
|
1084
1230
|
return member
|
|
1085
1231
|
}
|
|
1086
1232
|
// Deliver one prompt to a member. MUST use `subagents.sendMessage` — the
|
|
@@ -1140,14 +1286,27 @@ export function apply(ctx) {
|
|
|
1140
1286
|
return false
|
|
1141
1287
|
}
|
|
1142
1288
|
}
|
|
1289
|
+
// WHICH member (or office) is calling a tool. The answer must be DERIVED, never
|
|
1290
|
+
// guessed: the previous fallback answered "whoever this session woke last" whenever
|
|
1291
|
+
// the caller was not a member child — so the OFFICE (the session root, i.e. the
|
|
1292
|
+
// human/host) was impersonated as a random member. Concretely, the office calling
|
|
1293
|
+
// vibe_v5_assign was resolved to a researcher and refused with V5_NOT_ACADEMICIAN,
|
|
1294
|
+
// and its assignments/nudges would have been signed by the wrong person.
|
|
1143
1295
|
function memberIdOfAgent(agent) {
|
|
1144
1296
|
const id = sessionIdOf(agent)
|
|
1145
|
-
if (id
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1297
|
+
if (id !== undefined) {
|
|
1298
|
+
const m = byChild(id)
|
|
1299
|
+
if (m) return m.id
|
|
1300
|
+
// Not one of our member children. If it is a session ROOT it is the office —
|
|
1301
|
+
// 'office' rather than '' so the framing records a real, non-member sender.
|
|
1302
|
+
try { if (rootOf(agent) === agent) return 'office' } catch (e) { /* fall through */ }
|
|
1303
|
+
// An unrelated child agent: report no member. Member-only writes refuse with
|
|
1304
|
+
// V5_MEMBER_NOT_FOUND (that guard is what makes guessing unnecessary), and the
|
|
1305
|
+
// office-capable tools treat '' as the office.
|
|
1306
|
+
return ''
|
|
1307
|
+
}
|
|
1308
|
+
// No session id at all (a synthetic exec context). Only here may we fall back to
|
|
1309
|
+
// the last-woken member, and only while it still genuinely exists.
|
|
1151
1310
|
const c = currentMember
|
|
1152
1311
|
return (c && memberById(c)) ? c : ''
|
|
1153
1312
|
}
|
|
@@ -1182,7 +1341,11 @@ export function apply(ctx) {
|
|
|
1182
1341
|
}
|
|
1183
1342
|
L.push(' "task_create": {"subject":"…","description":"…","blocked_by":["t-1"],"write_scopes":["Members/r-1/Propos"]},')
|
|
1184
1343
|
L.push(' "task_claim": "t-3",')
|
|
1344
|
+
L.push(' "task_done": "t-3",')
|
|
1185
1345
|
L.push(' "task_update": {"task_id":"t-3","expected_revision":2,"action":"complete|release|reopen|edit|set_dependencies|delete"},')
|
|
1346
|
+
L.push(' "input": "本轮会议/辩论的发言正文(会议轮用;也可直接用 say)",')
|
|
1347
|
+
L.push(' "reject_assign": {"task_id":"t-3","why":"你对这项分派的异议理由"} ← 有异议时填;理由会被广播给')
|
|
1348
|
+
L.push(' 全体表决者(任务仍会执行,但你的理由不会被埋掉),')
|
|
1186
1349
|
if (kind !== 'temp') {
|
|
1187
1350
|
L.push(' "hire": {"purpose":"…","initial_task":"…","direction":"…"} ← 雇佣一名临时工(说明用途与初始任务),')
|
|
1188
1351
|
L.push(' "fire": {"id":"t-2","reason":"…"} ← 解雇(雇主/院士;你只能解雇你雇的),')
|
|
@@ -1194,21 +1357,33 @@ export function apply(ctx) {
|
|
|
1194
1357
|
L.push('}')
|
|
1195
1358
|
return L.join('\n')
|
|
1196
1359
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1360
|
+
// Every prompt builder below passes the member it is addressing. There is
|
|
1361
|
+
// deliberately NO fallback to "the last member we happened to touch": guessing the
|
|
1362
|
+
// identity is what produced the wrong-identity briefs in the first place.
|
|
1363
|
+
function stateBlock(member) {
|
|
1364
|
+
return briefBlock(member)
|
|
1199
1365
|
}
|
|
1200
|
-
function initialPrompt(member, initialTask) {
|
|
1366
|
+
function initialPrompt(member, initialTask, mode) {
|
|
1201
1367
|
const L = []
|
|
1202
|
-
|
|
1368
|
+
const resume = mode === 'resume'
|
|
1369
|
+
L.push(resume
|
|
1370
|
+
? '【会话重建 —— ' + kindLabel(member.kind) + ' ' + member.id + '】'
|
|
1371
|
+
: '【入职首轮 —— ' + kindLabel(member.kind) + ' ' + member.id + '】')
|
|
1203
1372
|
L.push('')
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1373
|
+
if (resume) {
|
|
1374
|
+
L.push('你的常驻会话已被重建(进程重启或被所办停止后恢复),现在继续工作。')
|
|
1375
|
+
L.push('请**先读回你自己的 Progress/ 与成果库**,确认你在哪、做到哪一步、下一步做什么,')
|
|
1376
|
+
L.push('然后接着推进——不要从头再来,也不要重新做已经做过的事。')
|
|
1377
|
+
} else {
|
|
1378
|
+
L.push('你刚刚加入本所。请你先**独立**想清楚:面对这个问题,你打算从哪个方向切入?')
|
|
1379
|
+
L.push('给出你的初始见解、思路与可行的方向;如果已有具体想法,可以顺手记进你自己的 '
|
|
1380
|
+
+ 'Progress/ 与成果库。')
|
|
1381
|
+
}
|
|
1207
1382
|
L.push('')
|
|
1208
|
-
if (initialTask) { L.push('你的初始任务/用途:'); L.push(' ' + initialTask); L.push('') }
|
|
1209
|
-
if (member.direction) { L.push('给你的起点方向:' + member.direction); L.push('') }
|
|
1383
|
+
if (initialTask) { L.push(resume ? '恢复说明:' : '你的初始任务/用途:'); L.push(' ' + initialTask); L.push('') }
|
|
1384
|
+
if (member.direction && !resume) { L.push('给你的起点方向:' + member.direction); L.push('') }
|
|
1210
1385
|
L.push('------------')
|
|
1211
|
-
L.push(stateBlock())
|
|
1386
|
+
L.push(stateBlock(member))
|
|
1212
1387
|
L.push('------------')
|
|
1213
1388
|
L.push(replySpec(member.kind))
|
|
1214
1389
|
return L.join('\n')
|
|
@@ -1226,14 +1401,14 @@ export function apply(ctx) {
|
|
|
1226
1401
|
}
|
|
1227
1402
|
L.push('')
|
|
1228
1403
|
L.push('------------')
|
|
1229
|
-
L.push(stateBlock())
|
|
1404
|
+
L.push(stateBlock(member))
|
|
1230
1405
|
L.push('------------')
|
|
1231
1406
|
L.push(replySpec(member.kind))
|
|
1232
1407
|
return L.join('\n')
|
|
1233
1408
|
}
|
|
1234
1409
|
function checkpointPrompt(member) {
|
|
1235
1410
|
const L = []
|
|
1236
|
-
L.push('【心跳检查 —— ' + member.id + '】')
|
|
1411
|
+
L.push('【心跳检查 —— ' + kindLabel(member.kind) + ' ' + member.id + '】')
|
|
1237
1412
|
L.push('')
|
|
1238
1413
|
L.push('所内一段时间没有新进展了。请**继续推进**这个问题,而不是停在原地:')
|
|
1239
1414
|
L.push('读一读同事的库、推进你的子问题/引理/方法、尝试一条新路线;')
|
|
@@ -1241,14 +1416,14 @@ export function apply(ctx) {
|
|
|
1241
1416
|
L.push('如果你确实已无路可走或认为原问题接近解决,请说明你的判断与理由。')
|
|
1242
1417
|
L.push('')
|
|
1243
1418
|
L.push('------------')
|
|
1244
|
-
L.push(stateBlock())
|
|
1419
|
+
L.push(stateBlock(member))
|
|
1245
1420
|
L.push('------------')
|
|
1246
1421
|
L.push(replySpec(member.kind))
|
|
1247
1422
|
return L.join('\n')
|
|
1248
1423
|
}
|
|
1249
1424
|
function meetingPrompt(member, mn) {
|
|
1250
1425
|
const L = []
|
|
1251
|
-
L.push('【研究所会议 ' + mn.id + ' 进行中 —— ' + member.id + '】')
|
|
1426
|
+
L.push('【研究所会议 ' + mn.id + ' 进行中 —— ' + kindLabel(member.kind) + ' ' + member.id + '】')
|
|
1252
1427
|
L.push('')
|
|
1253
1428
|
L.push('议程:' + mn.agenda + '(类型:' + mn.kind + ')')
|
|
1254
1429
|
L.push('')
|
|
@@ -1262,18 +1437,19 @@ export function apply(ctx) {
|
|
|
1262
1437
|
L.push('')
|
|
1263
1438
|
}
|
|
1264
1439
|
L.push('请就议程发表你的意见。分工、优先级、下一步做什么、是否认为原问题已解决,都可以说。')
|
|
1440
|
+
L.push('(会议轮请把你的发言同时填进 JSON 的 "input" 字段,框架据此写会议纪要。)')
|
|
1265
1441
|
L.push('如果你认为原问题已解决,请填 "vote_solved": true —— 只有当**全体有表决权者**都')
|
|
1266
1442
|
L.push('一致认为是真时,本所才会停下来。')
|
|
1267
1443
|
L.push('')
|
|
1268
1444
|
L.push('------------')
|
|
1269
|
-
L.push(stateBlock())
|
|
1445
|
+
L.push(stateBlock(member))
|
|
1270
1446
|
L.push('------------')
|
|
1271
1447
|
L.push(replySpec(member.kind))
|
|
1272
1448
|
return L.join('\n')
|
|
1273
1449
|
}
|
|
1274
1450
|
function verifyPrompt(member, vs) {
|
|
1275
1451
|
const L = []
|
|
1276
|
-
L.push('【求真表决 —— ' + member.id + ' 就对象 ' + vs.target + ' 投票】')
|
|
1452
|
+
L.push('【求真表决 —— ' + kindLabel(member.kind) + ' ' + member.id + ' 就对象 ' + vs.target + ' 投票】')
|
|
1277
1453
|
L.push('')
|
|
1278
1454
|
L.push('本所正在对下列对象发起共识验证:')
|
|
1279
1455
|
L.push(' 对象:' + vs.target + '(类型:' + kindLabel2(vs.kind) + ')')
|
|
@@ -1297,7 +1473,7 @@ export function apply(ctx) {
|
|
|
1297
1473
|
L.push('本所宁可留下未定论,也不要一个骗人的结论。')
|
|
1298
1474
|
L.push('')
|
|
1299
1475
|
L.push('------------')
|
|
1300
|
-
L.push(stateBlock())
|
|
1476
|
+
L.push(stateBlock(member))
|
|
1301
1477
|
L.push('------------')
|
|
1302
1478
|
L.push('结束时请**只**输出一个 JSON 对象(```json 围栏内):')
|
|
1303
1479
|
L.push('{"verdict":{"target":"' + vs.target + '","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}')
|
|
@@ -1708,10 +1884,12 @@ export function apply(ctx) {
|
|
|
1708
1884
|
const withMeta = Object.assign({}, after, { assignedBy: isOffice(memberId) ? 'office' : memberId, why, acceptance })
|
|
1709
1885
|
await putTask(withMeta)
|
|
1710
1886
|
await writeTaskboardMirror()
|
|
1711
|
-
|
|
1887
|
+
const assignerIsOffice = isOffice(memberId)
|
|
1888
|
+
await say(assignerIsOffice ? 'office' : memberId, {
|
|
1712
1889
|
to, kind: 'assign',
|
|
1713
1890
|
text: '任务 ' + taskId + '「' + withMeta.subject + '」分派给你。理由:' + why + '|验收标准:' + acceptance +
|
|
1714
|
-
'
|
|
1891
|
+
'。默认应当执行;若你认为方向有误,请说明理由(会被广播给全所)。' +
|
|
1892
|
+
'若你有异议,请在 JSON 里填 reject_assign。',
|
|
1715
1893
|
})
|
|
1716
1894
|
await wakeIfIdle(target)
|
|
1717
1895
|
return { ok: true, task: taskView(withMeta) }
|
|
@@ -1773,8 +1951,12 @@ export function apply(ctx) {
|
|
|
1773
1951
|
lines.push('')
|
|
1774
1952
|
lines.push('| 代号 | 职位 | 状态 | 雇主 | 方向/用途 | 轮次 | 上下文% |')
|
|
1775
1953
|
lines.push('|---|---|---|---|---|---|---|')
|
|
1776
|
-
|
|
1777
|
-
|
|
1954
|
+
// Dismissed members belong ONLY in the 已除名 section below. Listing them here too
|
|
1955
|
+
// showed the same person twice and made the roster look like they were still on
|
|
1956
|
+
// staff.
|
|
1957
|
+
const onBooks = s.members.filter((m) => m.phase !== 'dismissed')
|
|
1958
|
+
if (!onBooks.length) lines.push('| (暂无成员) | | | | | | |')
|
|
1959
|
+
for (const m of onBooks) {
|
|
1778
1960
|
lines.push('| ' + m.id + ' | ' + kindLabel(m.kind) + ' | ' + m.phase + ' | ' + (m.hiredBy || '—') + ' | ' +
|
|
1779
1961
|
String(m.direction || '—').replace(/\|/g, '/').slice(0, 80) + ' | ' + (rounds.get(m.id) || 0) + ' | ' +
|
|
1780
1962
|
(contextPct.get(m.id) || 0) + ' |')
|
|
@@ -2149,7 +2331,7 @@ export function apply(ctx) {
|
|
|
2149
2331
|
if (member.kind === 'temp') {
|
|
2150
2332
|
// Temp workers have no vote — but their judgement still matters, so it is
|
|
2151
2333
|
// relayed to the group instead of being silently dropped.
|
|
2152
|
-
await say(memberId, { to: 'voters', kind: '
|
|
2334
|
+
await say(memberId, { to: 'voters', kind: 'voters', text: '(临时工 ' + memberId + ' 的参考意见,无表决权)对 ' + target + ':' + String(reason || '') })
|
|
2153
2335
|
return { ok: false, code: 'V5_NOT_VOTER', message: '临时工没有表决权;你的意见已转达给表决者' }
|
|
2154
2336
|
}
|
|
2155
2337
|
const vs = currentVerify()
|
|
@@ -2203,8 +2385,12 @@ export function apply(ctx) {
|
|
|
2203
2385
|
if (!office && !(academician && params.academicianLeads)) {
|
|
2204
2386
|
// Everyone else may only PROPOSE; the request is relayed to the academician
|
|
2205
2387
|
// and the office instead of silently doing nothing.
|
|
2206
|
-
|
|
2207
|
-
|
|
2388
|
+
//
|
|
2389
|
+
// The relay must carry the TRUE proposer. It used to be sent as `currentMember`
|
|
2390
|
+
// — "whoever this session last woke" — so a proposal by r-1 arrived signed by
|
|
2391
|
+
// r-2 and the voters replied to the wrong person.
|
|
2392
|
+
if (callerId) {
|
|
2393
|
+
await say(callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })
|
|
2208
2394
|
}
|
|
2209
2395
|
return { ok: true, proposed: true, message: '已向院士/所办提议开会(只有院士或所办可以直接召开)' }
|
|
2210
2396
|
}
|
|
@@ -2494,23 +2680,35 @@ export function apply(ctx) {
|
|
|
2494
2680
|
const target = memberById(to)
|
|
2495
2681
|
if (!target || target.phase !== 'active') return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: 'active member "' + to + '" not found' }
|
|
2496
2682
|
const why = String(args.why || '').trim()
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2683
|
+
// A nudge is SUPERVISION, not an assignment, and it must name its true origin:
|
|
2684
|
+
// an office nudge previously arrived labelled "院士督办" under the 【院士分派】
|
|
2685
|
+
// prefix, so the member was told the academician had spoken when it had not.
|
|
2686
|
+
const office = isOffice(callerId)
|
|
2687
|
+
await say(office ? 'office' : callerId, {
|
|
2688
|
+
to, kind: 'nudge',
|
|
2689
|
+
text: (office ? '所办督办' : '院士督办') + ':' + (why || '(未说明)') +
|
|
2690
|
+
(args.next_step ? '|建议的下一步:' + String(args.next_step) : ''),
|
|
2500
2691
|
})
|
|
2501
2692
|
await wakeIfIdle(target)
|
|
2502
2693
|
return { ok: true, nudged: to }
|
|
2503
2694
|
}
|
|
2504
2695
|
|
|
2505
2696
|
// ---- inbox-aware waking -------------------------------------------------
|
|
2506
|
-
|
|
2697
|
+
// The mailbox is drained BEFORE the round prompt is built, and the round prompt is
|
|
2698
|
+
// therefore passed as a THUNK. Order matters: `promptFor` used to receive an already
|
|
2699
|
+
// built prompt (which had already embedded the pending mail through
|
|
2700
|
+
// briefBlock's [新到的消息/通知]) and then prepended the same messages again, so a
|
|
2701
|
+
// member read every newly delivered message TWICE in one prompt — once in the
|
|
2702
|
+
// prepended inbox block and once inside its own [状态] block.
|
|
2703
|
+
async function promptFor(member, baseFn) {
|
|
2507
2704
|
const pending = pendingFor(member.id)
|
|
2508
2705
|
const inbox = pending.length ? composeInbox(pending) : ''
|
|
2509
2706
|
if (pending.length) await ackPending(pending)
|
|
2707
|
+
const base = typeof baseFn === 'function' ? baseFn() : baseFn
|
|
2510
2708
|
return (inbox ? inbox + '\n\n' : '') + base
|
|
2511
2709
|
}
|
|
2512
|
-
async function wakeWithInbox(member,
|
|
2513
|
-
return await wakeMember(member, await promptFor(member,
|
|
2710
|
+
async function wakeWithInbox(member, baseFn, kind) {
|
|
2711
|
+
return await wakeMember(member, await promptFor(member, baseFn), kind)
|
|
2514
2712
|
}
|
|
2515
2713
|
|
|
2516
2714
|
// ---- scheduling / graded keep-alive (ported and upgraded from v4 §25) ---
|
|
@@ -2567,15 +2765,23 @@ export function apply(ctx) {
|
|
|
2567
2765
|
return
|
|
2568
2766
|
}
|
|
2569
2767
|
const budget = Math.max(1, Math.floor(Number(params.maxParallel) || 3))
|
|
2768
|
+
const idleMs = posMs(params.activityTimeoutMs, 120000)
|
|
2570
2769
|
let filled = 0
|
|
2571
|
-
// (a) members with work they already own or were assigned
|
|
2770
|
+
// (a) members with work they already own or were assigned.
|
|
2771
|
+
// PACED by the same idle window the heartbeat uses. Without the gate this branch
|
|
2772
|
+
// re-woke a task owner the instant its turn ended — and because every reply drives
|
|
2773
|
+
// another scheduling pass, a single unfinished task turned into an unbounded
|
|
2774
|
+
// wake -> turn -> wake chain that no parameter could slow down and that no pause
|
|
2775
|
+
// could interrupt between turns. New traffic still gets through immediately: the
|
|
2776
|
+
// addressed-mail branch below is not paced.
|
|
2572
2777
|
const tasks = inst().tasks
|
|
2573
2778
|
for (const t of tasks) {
|
|
2574
2779
|
if (filled >= budget) break
|
|
2575
2780
|
if (t.status !== 'in_progress' || !t.ownerId) continue
|
|
2576
2781
|
const m = memberById(t.ownerId)
|
|
2577
2782
|
if (!m || m.phase !== 'active' || busy.has(m.id)) continue
|
|
2578
|
-
|
|
2783
|
+
if ((now() - (lastActiveAt.get(m.id) || 0)) < idleMs) continue
|
|
2784
|
+
const ok = await wakeWithInbox(m, () => normalPrompt(m), 'normal')
|
|
2579
2785
|
if (ok) filled += 1
|
|
2580
2786
|
else armHeartbeat()
|
|
2581
2787
|
}
|
|
@@ -2586,7 +2792,7 @@ export function apply(ctx) {
|
|
|
2586
2792
|
if (filled >= budget) break
|
|
2587
2793
|
const d = deliveryDecision(m.id)
|
|
2588
2794
|
if (!d.deliver || !d.urgent) continue
|
|
2589
|
-
const ok = await wakeWithInbox(m, normalPrompt(m), 'normal')
|
|
2795
|
+
const ok = await wakeWithInbox(m, () => normalPrompt(m), 'normal')
|
|
2590
2796
|
if (ok) filled += 1
|
|
2591
2797
|
else armHeartbeat()
|
|
2592
2798
|
}
|
|
@@ -2596,7 +2802,7 @@ export function apply(ctx) {
|
|
|
2596
2802
|
if (chatDue.length) {
|
|
2597
2803
|
for (const m of chatDue) {
|
|
2598
2804
|
if (filled >= budget) break
|
|
2599
|
-
const ok = await wakeWithInbox(m, normalPrompt(m), 'normal')
|
|
2805
|
+
const ok = await wakeWithInbox(m, () => normalPrompt(m), 'normal')
|
|
2600
2806
|
if (ok) filled += 1
|
|
2601
2807
|
}
|
|
2602
2808
|
if (filled) { armHeartbeat(); return }
|
|
@@ -2612,13 +2818,18 @@ export function apply(ctx) {
|
|
|
2612
2818
|
return
|
|
2613
2819
|
}
|
|
2614
2820
|
// (e) heartbeat: push the longest-idle member to make progress rather than just
|
|
2615
|
-
// asking "are we done" (v4's original heartbeat invited stagnation).
|
|
2616
|
-
|
|
2821
|
+
// asking "are we done" (v4's original heartbeat invited stagnation). A member that
|
|
2822
|
+
// owns in-progress work gets a WORK round (its task block in [状态] tells it what
|
|
2823
|
+
// it owes); an otherwise idle member gets the heartbeat that asks it to advance the
|
|
2824
|
+
// problem by itself.
|
|
2617
2825
|
if (busy.size < budget && idle.length) {
|
|
2618
2826
|
const candidates = idle.slice().sort((a, b) => (lastActiveAt.get(a.id) || 0) - (lastActiveAt.get(b.id) || 0))
|
|
2619
2827
|
const pick = candidates[0]
|
|
2620
2828
|
if (pick && (now() - (lastActiveAt.get(pick.id) || 0)) >= idleMs) {
|
|
2621
|
-
const
|
|
2829
|
+
const owns = inst().tasks.some((t) => t.ownerId === pick.id && t.status === 'in_progress')
|
|
2830
|
+
const ok = await wakeWithInbox(pick,
|
|
2831
|
+
() => (owns ? normalPrompt(pick) : checkpointPrompt(pick)),
|
|
2832
|
+
owns ? 'normal' : 'checkpoint')
|
|
2622
2833
|
// ALWAYS re-arm after a wake, even on success. A wake whose turn never ends
|
|
2623
2834
|
// (a host that drops the delivery, a child that vanished) would otherwise
|
|
2624
2835
|
// leave nothing to schedule the next pass and the institute would freeze
|
|
@@ -2682,7 +2893,10 @@ export function apply(ctx) {
|
|
|
2682
2893
|
if (typeof speech === 'string' && speech.trim()) await say(member.id, { to: 'all', text: speech, kind: 'chat' })
|
|
2683
2894
|
else if (speech && typeof speech === 'object' && speech.text) {
|
|
2684
2895
|
const to = String(speech.to || 'all')
|
|
2685
|
-
|
|
2896
|
+
// A "to: voters" broadcast is not a private message: framing it 私信 told the
|
|
2897
|
+
// voters they had been singled out when the whole voting body was addressed.
|
|
2898
|
+
const kind = to === 'all' ? 'chat' : to === 'voters' ? 'voters' : 'dm'
|
|
2899
|
+
await say(member.id, { to, text: String(speech.text), kind })
|
|
2686
2900
|
}
|
|
2687
2901
|
// (2) progress log
|
|
2688
2902
|
if (typeof p.progress === 'string' && p.progress.trim()) await publishProgress(member.id, p.progress)
|
|
@@ -2699,28 +2913,48 @@ export function apply(ctx) {
|
|
|
2699
2913
|
if (p.task_create && typeof p.task_create === 'object') await taskCreate(member.id, p.task_create)
|
|
2700
2914
|
if (p.task_claim) {
|
|
2701
2915
|
const t = inst().tasks.find((x) => x.id === String(p.task_claim))
|
|
2702
|
-
if (t)
|
|
2703
|
-
|
|
2916
|
+
if (t) {
|
|
2917
|
+
const r = await taskUpdate(member.id, { task_id: t.id, expected_revision: t.revision, action: 'claim' })
|
|
2918
|
+
// Report a REFUSED claim back to the claimer: silently doing nothing left the
|
|
2919
|
+
// member believing it owned a task it does not own (and the notice used to be
|
|
2920
|
+
// dropped as a self-message).
|
|
2921
|
+
if (r && r.ok === false) await notice(member.id, '认领 ' + t.id + ' 失败(' + (r.code || '') + '):' + (r.message || ''))
|
|
2922
|
+
} else await notice(member.id, '认领失败:没有任务 ' + String(p.task_claim))
|
|
2923
|
+
}
|
|
2924
|
+
if (p.task_update && typeof p.task_update === 'object') {
|
|
2925
|
+
const r = await taskUpdate(member.id, p.task_update)
|
|
2926
|
+
if (r && r.ok === false) await notice(member.id, 'task_update 未生效(' + (r.code || '') + '):' + (r.message || ''))
|
|
2704
2927
|
}
|
|
2705
|
-
if (p.task_update && typeof p.task_update === 'object') await taskUpdate(member.id, p.task_update)
|
|
2706
2928
|
if (p.task_done) {
|
|
2707
2929
|
const t = inst().tasks.find((x) => x.id === String(p.task_done))
|
|
2708
|
-
if (t)
|
|
2930
|
+
if (t) {
|
|
2931
|
+
const r = await taskUpdate(member.id, { task_id: t.id, expected_revision: t.revision, action: 'complete' })
|
|
2932
|
+
if (r && r.ok === false) await notice(member.id, '完成任务 ' + t.id + ' 失败(' + (r.code || '') + '):' + (r.message || ''))
|
|
2933
|
+
} else await notice(member.id, '标记完成失败:没有任务 ' + String(p.task_done))
|
|
2709
2934
|
}
|
|
2710
2935
|
// (5) verification
|
|
2711
2936
|
if (p.propose_verify) {
|
|
2712
2937
|
const pv = typeof p.propose_verify === 'string' ? { target: p.propose_verify } : p.propose_verify
|
|
2713
|
-
if (pv && pv.target)
|
|
2938
|
+
if (pv && pv.target) {
|
|
2939
|
+
const r = await maybeQueueVerify(pv.target, pv.kind, member.id, pv.reason)
|
|
2940
|
+
if (r && r.ok === false) await notice(member.id, '提议验证 ' + String(pv.target) + ' 未受理(' + (r.code || '') + '):' + (r.message || ''))
|
|
2941
|
+
}
|
|
2714
2942
|
}
|
|
2715
2943
|
if (p.verdict && typeof p.verdict === 'object') {
|
|
2716
2944
|
const n = normVerdictNumber(p.verdict.verdict)
|
|
2717
|
-
if (n === undefined) await
|
|
2718
|
-
else
|
|
2945
|
+
if (n === undefined) await notice(member.id, 'verdict 必须是 0-1 的数值;本轮的票未被记录。')
|
|
2946
|
+
else {
|
|
2947
|
+
const r = await castVerdict(member.id, String(p.verdict.target || ''), n, p.verdict.reason)
|
|
2948
|
+
if (r && r.ok === false) await notice(member.id, '本轮的票未被记录(' + (r.code || '') + '):' + (r.message || ''))
|
|
2949
|
+
}
|
|
2719
2950
|
}
|
|
2720
2951
|
// (6) meetings
|
|
2721
2952
|
if (p.propose_meeting) {
|
|
2722
2953
|
const pm = typeof p.propose_meeting === 'string' ? { agenda: p.propose_meeting } : p.propose_meeting
|
|
2723
|
-
if (pm && pm.agenda)
|
|
2954
|
+
if (pm && pm.agenda) {
|
|
2955
|
+
const r = await startMeeting(member.id, pm)
|
|
2956
|
+
if (r && r.ok === false) await notice(member.id, '提议开会未受理(' + (r.code || '') + '):' + (r.message || ''))
|
|
2957
|
+
}
|
|
2724
2958
|
}
|
|
2725
2959
|
if (p.convene_meeting && typeof p.convene_meeting === 'object' && isAcademician(member.id) && params.academicianLeads) {
|
|
2726
2960
|
await startMeeting(member.id, p.convene_meeting)
|
|
@@ -2736,9 +2970,9 @@ export function apply(ctx) {
|
|
|
2736
2970
|
if (p.reject_assign && typeof p.reject_assign === 'object' && params.memberMayRejectAssign) {
|
|
2737
2971
|
const ra = p.reject_assign
|
|
2738
2972
|
await say(member.id, {
|
|
2739
|
-
to: 'voters', kind: '
|
|
2973
|
+
to: 'voters', kind: 'voters',
|
|
2740
2974
|
text: '【反对分派】我对任务 ' + String(ra.task_id || '(未指明)') + ' 有异议:' + String(ra.why || '(未说明理由)') +
|
|
2741
|
-
'
|
|
2975
|
+
'。任务仍会执行,但请' + (academicianId() ? '院士与全所' : '全所') + '知悉我的理由。',
|
|
2742
2976
|
})
|
|
2743
2977
|
}
|
|
2744
2978
|
// (10) solve votes / personal judgement
|
|
@@ -2990,8 +3224,11 @@ export function apply(ctx) {
|
|
|
2990
3224
|
'从已知的相近结论出发,看能否推广或加强得到所需结果。',
|
|
2991
3225
|
]
|
|
2992
3226
|
const spawned = []
|
|
2993
|
-
// The academician is founded FIRST so
|
|
2994
|
-
// it can begin overseeing the founding round.
|
|
3227
|
+
// The academician is founded FIRST so it is on the roster for every later member's
|
|
3228
|
+
// induction brief, and so it can begin overseeing the founding round. Each member
|
|
3229
|
+
// is committed to the ACTIVE roster before its own brief is built (see
|
|
3230
|
+
// spawnMember), so every founding brief describes a roster that includes its
|
|
3231
|
+
// reader.
|
|
2995
3232
|
if (params.academician) {
|
|
2996
3233
|
const m = await newMember('academician', { direction: '统领全所:统筹全局、拆解并分派工作、设定优先级、督导进度。' })
|
|
2997
3234
|
try {
|
|
@@ -3059,8 +3296,7 @@ export function apply(ctx) {
|
|
|
3059
3296
|
if (m.childId) continue
|
|
3060
3297
|
try {
|
|
3061
3298
|
const seedText = (await readTextRel('Members/' + m.id + '/Progress/progress.md')) || ''
|
|
3062
|
-
await spawnMember(m, '
|
|
3063
|
-
+ (seedText ? seedText.slice(-4000) : '(你的 Progress/ 还是空的——请先把当前状态补写进去。)'))
|
|
3299
|
+
await spawnMember(m, seedText ? seedText.slice(-4000) : '(你的 Progress/ 还是空的——请先把当前状态补写进去。)', 'resume')
|
|
3064
3300
|
respawned += 1
|
|
3065
3301
|
} catch (e) {
|
|
3066
3302
|
await putMember(Object.assign({}, m, { phase: 'failed', error: String((e && e.message) || e) }))
|
|
@@ -3199,7 +3435,7 @@ export function apply(ctx) {
|
|
|
3199
3435
|
// structure, so members may only propose it; the office decides and executes.
|
|
3200
3436
|
async function addResearcher(callerId, direction) {
|
|
3201
3437
|
if (!isOffice(callerId)) {
|
|
3202
|
-
await say(callerId, { to: 'voters', kind: '
|
|
3438
|
+
await say(callerId, { to: 'voters', kind: 'voters', text: '提议增聘一名常驻研究员(方向:' + String(direction || '未指定') + ')' })
|
|
3203
3439
|
return { ok: true, proposed: true, message: '已向所办提议增聘常驻研究员(编制变更需所办批准)' }
|
|
3204
3440
|
}
|
|
3205
3441
|
if (!running || autoDone) return { ok: false, code: 'V5_INSTITUTE_STATE', message: 'the institute is not running' }
|
|
@@ -3315,7 +3551,8 @@ export function apply(ctx) {
|
|
|
3315
3551
|
registerTool('vibe_v5_say', '(member) Speak in the group chat (omit "to"), send a private message ("to":"r-2"), or address only the voters ("to":"voters").', objParams({ text: S, to: S }, ['text']), (s, a, x) => {
|
|
3316
3552
|
const from = s.memberIdOfAgent(x)
|
|
3317
3553
|
if (!from) return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: 'no calling member' }
|
|
3318
|
-
|
|
3554
|
+
const to = a.to || 'all'
|
|
3555
|
+
return s.say(from, { to, text: a.text, kind: to === 'voters' ? 'voters' : (a.to ? 'dm' : 'chat') })
|
|
3319
3556
|
})
|
|
3320
3557
|
registerTool('vibe_v5_wait', '(member) Wait for the next institute change (roster/task/mail/status) WITHOUT polling. Returns immediately with noProgress when nobody else is running or provisioning. timeout_ms: 10000-3600000 (default 30000).', objParams({ timeout_ms: I, reason: S }), async (s, a, x) => {
|
|
3321
3558
|
const me = s.memberIdOfAgent(x)
|