dsh-vibe-math 2.0.21 → 2.0.22
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/LICENSE +21 -21
- package/README.md +5 -4
- package/RELEASE-NOTES-2.0.22.md +112 -0
- package/cordis.patch.yml +9 -9
- package/installer.js +304 -290
- package/package.json +10 -4
- package/vibe-math-v2/agent.cordis.yml +256 -212
- package/vibe-math-v2/preset.yml +2 -2
- package/vibe-math-v2/vibe-math-v2.js +243 -36
- package/vibe-math-v3/agent.cordis.yml +278 -226
- package/vibe-math-v3/preset.yml +2 -2
- package/vibe-math-v3/vibe-math-v3.js +2938 -2631
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +540 -540
- package/vibe-math-v4/agent.cordis.yml +300 -239
- package/vibe-math-v4/preset.yml +2 -2
- package/vibe-math-v4/vibe-math-v4.js +1275 -1111
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +647 -636
|
@@ -1,1111 +1,1275 @@
|
|
|
1
|
-
// Vibe Math V4 — persistent self-organizing collaborative research framework.
|
|
2
|
-
// FACILITATOR (message bus / meetings / per-resident artifact libraries /
|
|
3
|
-
// unanimous-consensus verification / context compaction proxy / resume / human
|
|
4
|
-
// intervention). It NEVER assigns tasks: residents message & meet and decide all
|
|
5
|
-
// task allocation among themselves. Consumes HOST subagents/agents/fs/tools/commands.
|
|
6
|
-
// NOTE: must declare `inject` for every service read as a ctx property (the Guard
|
|
7
|
-
// rejects undeclared dependencies), and must use the `timer` Service (ctx.timeout),
|
|
8
|
-
// not global setTimeout/clearTimeout, which do not exist in the plugin runtime.
|
|
9
|
-
export const inject = ['subagents', 'agents', 'fs', 'tools', 'commands', 'timer']
|
|
10
|
-
export function apply(ctx) {
|
|
11
|
-
const subagents = ctx.subagents
|
|
12
|
-
const agents = ctx.agents
|
|
13
|
-
const fs = ctx.fs
|
|
14
|
-
const tools = ctx.tools
|
|
15
|
-
const commands = ctx.commands
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
function
|
|
66
|
-
function
|
|
67
|
-
function
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
// resident
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
// verify
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// (
|
|
89
|
-
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
|
-
async function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
+'
|
|
239
|
-
+'
|
|
240
|
-
+'
|
|
241
|
-
+'
|
|
242
|
-
+'
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
+'
|
|
248
|
-
+'
|
|
249
|
-
+'
|
|
250
|
-
+'{
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
r.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// ----
|
|
405
|
-
async function
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
async function
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
if(!
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if(
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
function
|
|
609
|
-
if(
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if(
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
function
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
//
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
//
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if(
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
if(
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
// a
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
893
|
-
async function
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
//
|
|
921
|
-
//
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
if(
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
//
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if(
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
//
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
//
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
//
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
//
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
} }
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
return
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1
|
+
// Vibe Math V4 — persistent self-organizing collaborative research framework.
|
|
2
|
+
// FACILITATOR (message bus / meetings / per-resident artifact libraries /
|
|
3
|
+
// unanimous-consensus verification / context compaction proxy / resume / human
|
|
4
|
+
// intervention). It NEVER assigns tasks: residents message & meet and decide all
|
|
5
|
+
// task allocation among themselves. Consumes HOST subagents/agents/fs/tools/commands.
|
|
6
|
+
// NOTE: must declare `inject` for every service read as a ctx property (the Guard
|
|
7
|
+
// rejects undeclared dependencies), and must use the `timer` Service (ctx.timeout),
|
|
8
|
+
// not global setTimeout/clearTimeout, which do not exist in the plugin runtime.
|
|
9
|
+
export const inject = ['subagents', 'agents', 'fs', 'tools', 'commands', 'timer']
|
|
10
|
+
export function apply(ctx) {
|
|
11
|
+
const subagents = ctx.subagents
|
|
12
|
+
const agents = ctx.agents
|
|
13
|
+
const fs = ctx.fs
|
|
14
|
+
const tools = ctx.tools
|
|
15
|
+
const commands = ctx.commands
|
|
16
|
+
// Optional services are resolved LAZILY at call time, never snapshotted in apply().
|
|
17
|
+
// A `ctx.get()` snapshot taken here is order-sensitive: if the service has not been
|
|
18
|
+
// provided yet when this preset subtree mounts, the snapshot stays undefined for the
|
|
19
|
+
// whole session, so `runShell` would report 'no-subprocess' forever and `ensureDirs()`
|
|
20
|
+
// would silently stop creating the project tree (only masked by fs.writeText's
|
|
21
|
+
// automatic parent creation). Reading on demand removes that dependency on mount order.
|
|
22
|
+
const subprocessOf = () => { try { return ctx.get('subprocess') } catch(e){ return undefined } }
|
|
23
|
+
const sandboxPolicyOf = () => { try { return ctx.get('sandboxPolicy') } catch(e){ return undefined } }
|
|
24
|
+
const compactionOf = () => { try { return ctx.get('compaction') } catch(e){ return undefined } }
|
|
25
|
+
|
|
26
|
+
const sessions = new Map() // rootAgentId -> Session
|
|
27
|
+
const childOwner = new Map() // childId -> rootAgentId
|
|
28
|
+
const fileOwner = {} // process-level write lock
|
|
29
|
+
const processEpoch = String(Date.now()) + '-' + Math.random().toString(36).slice(2, 8)
|
|
30
|
+
|
|
31
|
+
function sessionIdOf(agent){ try { return (agent&&agent.id)?String(agent.id):undefined } catch(e){ return undefined } }
|
|
32
|
+
function rootOf(agent){ try { let cur=agent; const seen=new Set(); while(cur){ const id=cur.id; if(seen.has(id)) return cur; seen.add(id); const p=(cur.session&&cur.session.header)?cur.session.header.parentSession:undefined; if(p===undefined) return cur; const par=agents.get(p); if(!par) return cur; cur=par } } catch(e){} return agent }
|
|
33
|
+
function getSession(agent){ const root=rootOf(agent); const sid=sessionIdOf(root); if(sid===undefined) return undefined; let s=sessions.get(sid); if(!s){ s=makeSession(root,sid); sessions.set(sid,s) } return s }
|
|
34
|
+
|
|
35
|
+
function makeSession(rootAgent, sessionId) {
|
|
36
|
+
let currentProject = 'default'
|
|
37
|
+
const DEFAULT_PARAMS = {
|
|
38
|
+
residentCount: 4, compactThreshold: 66, compactAfterRounds: 8,
|
|
39
|
+
maxParallel: 3, activityTimeoutMs: 120000, verdictMaxRounds: 3,
|
|
40
|
+
meetingKeepEvery: 5, // 每积累 N 个新产物自动触发一次同步会议
|
|
41
|
+
stallAutoMeetingMs: 360000, // 团队空闲且无新产物的"停滞阈值":超过则自动召集同步会议(分级保活 B)
|
|
42
|
+
// model/provider inheritance: '' = the resident inherits the parent (main assistant)
|
|
43
|
+
// route (provider + model). Set them to override the resident's LLM backend/model.
|
|
44
|
+
provider: '', model: '', residentPersona: '',
|
|
45
|
+
// tool permissions: an allow/deny list of tool names applied via startContinuable's
|
|
46
|
+
// toolFilter (scoped tools.restrict() in the child). Empty = inherit all tools.
|
|
47
|
+
// CAUTION: only set one of these; an empty allow:[] would deny EVERY tool.
|
|
48
|
+
toolAllow: [], toolDeny: [],
|
|
49
|
+
}
|
|
50
|
+
let params = Object.assign({}, DEFAULT_PARAMS)
|
|
51
|
+
let running = false, autoDone = false, phase = 'idle'
|
|
52
|
+
let residents = new Map(), mailboxes = new Map(), taskboard = [], decisions = []
|
|
53
|
+
let meetings = [], reports = [], activityLog = []
|
|
54
|
+
let problemText = '', problemId = 'problem', runId = 'run-' + shortId()
|
|
55
|
+
let meetingState = null, verifyState = null, pendingVerify = [], pendingMeeting = null // pendingVerify: FIFO queue (several residents may independently propose different objects before any verify runs — a single slot silently DROPPED all but the last proposal)
|
|
56
|
+
let busy = new Set(), wakeKind = new Map(), currentResident = ''
|
|
57
|
+
let finalizeLock = null // 'meeting'|'verify' while a consensus finalize is running (reentry guard)
|
|
58
|
+
const verifiedRecently = new Map() // targetId -> timestamp when it was closed as Verified (dedup re-propose)
|
|
59
|
+
let lastActivityAt = now(), lastProgressAt = now(), artifactCount = 0, lastSyncMeetingAt = 0, persistedEpoch = '', heartbeatDisposer = null
|
|
60
|
+
const activityLogCap = 200
|
|
61
|
+
|
|
62
|
+
// ---- utils ----
|
|
63
|
+
function now(){ return Date.now() }
|
|
64
|
+
function uuid(){ const h='0123456789abcdef'; let s=''; for(let i=0;i<36;i++){ if(i===8||i===13||i===18||i===23) s+='-'; else s+=h[Math.floor(Math.random()*16)] } return s }
|
|
65
|
+
function shortId(){ const h='0123456789abcdef'; let s=''; for(let i=0;i<8;i++) s+=h[Math.floor(Math.random()*16)]; return s }
|
|
66
|
+
function clamp01(v){ const n=Number(v); if(!Number.isFinite(n)) return 0.5; return Math.max(0,Math.min(1,n)) }
|
|
67
|
+
function fmtTime(ts){ try { return new Date(ts||now()).toISOString().replace('T',' ').slice(0,19) } catch(e){ return String(ts||'') } }
|
|
68
|
+
function cl(x){ return clamp01(Number(x)) }
|
|
69
|
+
// contextPct is a PERCENT (0-100); never clamp to 0-1 or the compactThreshold
|
|
70
|
+
// comparison (e.g. 66) becomes `1.0 >= 66` and never fires.
|
|
71
|
+
function clPct(x){ const n=Number(x); if(!Number.isFinite(n)) return 0; return Math.max(0,Math.min(100,n)) }
|
|
72
|
+
function textBlock(t){ return { type:'text', text:String(t) } }
|
|
73
|
+
function blocksToText(b){ if(!b) return ''; let out=''; for(const x of b){ if(x&&x.type==='text'&&typeof x.text==='string') out+=x.text+'\n' } return out.trim() }
|
|
74
|
+
function logActivity(event,detail){ activityLog.push({at:now(),event,detail:String(detail||'')}); if(activityLog.length>activityLogCap) activityLog.shift() }
|
|
75
|
+
function logDecision(kind,detail){ decisions.push({at:now(),kind,detail:String(detail||'')}) }
|
|
76
|
+
// Record that the project made real progress (new artifact, meeting, verify, task, or a
|
|
77
|
+
// resident speaking to the group). The stall auto-sync meeting (B) fires only when this has
|
|
78
|
+
// NOT advanced for stallAutoMeetingMs, so a group that is genuinely producing keeps working
|
|
79
|
+
// and only a truly stalled group gets a coordination meeting to reboot itself.
|
|
80
|
+
function markProgress(){ lastProgressAt = now(); }
|
|
81
|
+
// How long a meeting/verify may run without collecting a new input/verdict before we treat it as
|
|
82
|
+
// deadlocked and abandon it. A meeting round's own signal window is activityTimeoutMs, so a
|
|
83
|
+
// resident should speak within that; 2× that without ANY new input/verdict means the meeting/verify
|
|
84
|
+
// is stuck and must not keep the whole group blocked.
|
|
85
|
+
// Positive duration with a safe fallback: a NEGATIVE/NaN activityTimeoutMs or stallAutoMeetingMs
|
|
86
|
+
// (misconfigured via vibe_v4_set) would otherwise make recoverStallMs negative → every meeting/
|
|
87
|
+
// verify watchdog fires INSTANTLY (abandoning all consensus) and A-fill's idle window would never
|
|
88
|
+
// elapse (waking everyone every pass). Guard every duration read with this.
|
|
89
|
+
function posMs(v,def){ const n=Number(v); return (Number.isFinite(n)&&n>0)?n:(def||120000) }
|
|
90
|
+
function recoverStallMs(){ return posMs(params.activityTimeoutMs,120000) * 2 }
|
|
91
|
+
function pickProvider(){ try { const n=subagents.list?subagents.list():[]; if(n.indexOf('spawn')!==-1) return 'spawn'; if(n.indexOf('fork')!==-1) return 'fork' } catch(e){} return 'spawn' }
|
|
92
|
+
// Per-resident model/provider inheritance: when params.provider / params.model are set,
|
|
93
|
+
// the resident uses that exact route; when left '' the resident inherits the parent's
|
|
94
|
+
// (main assistant) route — the documented DSH default (resolveChildAgentOptions merges
|
|
95
|
+
// requested over parent). No override is applied for empty values.
|
|
96
|
+
function residentAgentOptions(){ const ao={}; if(params.provider) ao.provider=params.provider; if(params.model) ao.model=params.model; return ao }
|
|
97
|
+
// Tool permission (scoped toolFilter). Only emit a filter when allow or deny has entries;
|
|
98
|
+
// an empty object is rejected by DSH ("must declare allow and/or deny").
|
|
99
|
+
function residentToolFilter(){
|
|
100
|
+
const allow=Array.isArray(params.toolAllow)?params.toolAllow.filter(x=>String(x).trim()):[]
|
|
101
|
+
const deny=Array.isArray(params.toolDeny)?params.toolDeny.filter(x=>String(x).trim()):[]
|
|
102
|
+
if(allow.length===0 && deny.length===0) return undefined
|
|
103
|
+
const f={}; if(allow.length) f.allow=allow; if(deny.length) f.deny=deny; return f
|
|
104
|
+
}
|
|
105
|
+
function makeSignal(ms){ return AbortSignal.timeout(posMs(ms,30000)) }
|
|
106
|
+
function workspaceRoot(){ try { if(rootAgent&&rootAgent.session&&rootAgent.session.header&&rootAgent.session.header.cwd) return rootAgent.session.header.cwd } catch(e){} const sp=sandboxPolicyOf(); if(sp&&sp.workspaceRoot) return sp.workspaceRoot; return '.' }
|
|
107
|
+
function vibeRoot(){ return (workspaceRoot()+'/VibeMath').replace(/\\/g,'/') }
|
|
108
|
+
function frameworkRoot(){ return vibeRoot()+'/Projects/'+currentProject }
|
|
109
|
+
function slugify(s){ const t=String(s==null?'':s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g,'-').replace(/^-+|-+$/g,''); return t||'project' }
|
|
110
|
+
// Object ids (verify targets, recorded cards) become FILE NAMES and DIRECTORY PATHS
|
|
111
|
+
// (Verified/命题/<id>.md, Shared/debates/<id>.md, Propos/<r>/<id>.md, source-card scans).
|
|
112
|
+
// A hostile/sloppy id containing path separators ('../../x') or Windows-forbidden chars would
|
|
113
|
+
// escape the project tree. Keep every harmless character (incl. Chinese) and replace only
|
|
114
|
+
// separators/control chars; strip leading/trailing dots/dashes so the name is never '.'/'..'.
|
|
115
|
+
function idSafe(s){
|
|
116
|
+
const t=String(s==null?'':s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g,'-').replace(/-{2,}/g,'-').replace(/^[.\-]+|[.\-]+$/g,'')
|
|
117
|
+
return t||'id'
|
|
118
|
+
}
|
|
119
|
+
let warnedNoPolicy = false
|
|
120
|
+
function warnNoPolicyOnce(){ if(!warnedNoPolicy){ warnedNoPolicy=true; console.error('vibe-math-v4: sandboxPolicy unavailable; writes go out with no explicit policy') } }
|
|
121
|
+
function getPolicy(){ const sp=sandboxPolicyOf(); if(!sp){ warnNoPolicyOnce(); return undefined } try { if(rootAgent&&rootAgent.session) return sp.resolve({session:rootAgent.session}) } catch(e){ warnNoPolicyOnce() } try { const p=sp.resolve({}); if(!warnedNoPolicy){ warnedNoPolicy=true; console.error('vibe-math-v4: falling back to sandboxPolicy.resolve({}) — the fence root is the host-configured workspace, not necessarily this session cwd') } return p } catch(e){ warnNoPolicyOnce() } return undefined }
|
|
122
|
+
function psQuote(p){ return "'"+String(p).replace(/'/g,"''")+"'" }
|
|
123
|
+
/** POSIX 单引号引用:把 ' 换成 '\'' 以安全嵌入任意路径。 */
|
|
124
|
+
function shQuote(p){ return "'"+String(p).replace(/'/g,"'\\''")+"'" }
|
|
125
|
+
/**
|
|
126
|
+
* 执行一段 shell 脚本。**按平台选择解释器**:此前硬编码 powershell,而预设用
|
|
127
|
+
* `disabled: !!js process.platform !== 'win32'` 在非 Windows 上关掉了 tool-pwsh 行——
|
|
128
|
+
* 插件会调用一个自己声明不提供的二进制,且返回值无人检查,表现为静默失效。
|
|
129
|
+
*/
|
|
130
|
+
function isWindows(){ return process.platform === 'win32' }
|
|
131
|
+
function mkdirCmd(paths){
|
|
132
|
+
if(isWindows()) return 'New-Item -Force -ItemType Directory -Path '+paths.map(psQuote).join(',')+' | Out-Null'
|
|
133
|
+
return 'mkdir -p '+paths.map(shQuote).join(' ')
|
|
134
|
+
}
|
|
135
|
+
async function runShell(script,cwd){
|
|
136
|
+
const subprocess=subprocessOf(); if(subprocess===undefined) return {ok:false,error:'no-subprocess'}
|
|
137
|
+
try {
|
|
138
|
+
const argv = isWindows()
|
|
139
|
+
? ['powershell','-NoProfile','-NonInteractive','-Command',script]
|
|
140
|
+
: ['/bin/sh','-c',script]
|
|
141
|
+
const h=subprocess.spawn({argv:argv,cwd:cwd||workspaceRoot(),stdio:{stdin:'ignore',stdout:'inherit',stderr:'inherit'},graceMs:20000})
|
|
142
|
+
const o=await h.done
|
|
143
|
+
return {ok:o.exitCode===0,exitCode:o.exitCode}
|
|
144
|
+
} catch(e){ return {ok:false,error:String((e&&e.message)||e)} }
|
|
145
|
+
}
|
|
146
|
+
async function fsTarget(rel){ return await fs.resolve(rel,{cwd:frameworkRoot()}) }
|
|
147
|
+
async function readText(rel){ try { const t=await fsTarget(rel); if(await fs.stat(t)===undefined) return undefined; return await fs.readText(t) } catch(e){ return undefined } }
|
|
148
|
+
async function writeText(rel,content){ try { const t=await fsTarget(rel); await fs.writeText(t,content,undefined,undefined,getPolicy()); return true } catch(e){ return false } }
|
|
149
|
+
// State files (taskboard/residents/session/mailboxes/decisions) are written by MANY concurrent
|
|
150
|
+
// flows (parallel resident turns + end handlers + tools). Two near-simultaneous writers of the
|
|
151
|
+
// SAME file each stringified their snapshot BEFORE their fs.writeText landed, so the writer with
|
|
152
|
+
// the OLDER snapshot could land LAST and silently erase the other's entry (e.g. two residents
|
|
153
|
+
// proposing tasks in the same tick → one task vanished from taskboard.json until the next save).
|
|
154
|
+
// Fix: serialize writes PER FILE, and defer JSON.stringify until the write actually runs (so the
|
|
155
|
+
// snapshot always reflects the newest in-memory state at execution time — late writers win with
|
|
156
|
+
// the FULL state, never with a stale subset).
|
|
157
|
+
const jsonQueues = new Map() // rel -> tail promise (per-session file write chain)
|
|
158
|
+
function writeJson(rel,obj){
|
|
159
|
+
const key='j:'+rel
|
|
160
|
+
const prev=jsonQueues.get(key)||Promise.resolve(true)
|
|
161
|
+
const run=prev.catch(()=>{}).then(async ()=>{ try { if(!(await assertWritable(rel))) return false; const t=await fsTarget(rel); await fs.writeText(t,JSON.stringify(obj,null,2),undefined,undefined,getPolicy()); return true } catch(e){ return false } })
|
|
162
|
+
jsonQueues.set(key,run.catch(()=>{}))
|
|
163
|
+
return run
|
|
164
|
+
}
|
|
165
|
+
async function readJson(rel){ const t=await readText(rel); if(t===undefined||t==='') return undefined; try { return JSON.parse(t) } catch(e){ noteSuspect(rel); return undefined } }
|
|
166
|
+
/**
|
|
167
|
+
* Corruption guard. `readJson` cannot tell "no file yet" from "file present but
|
|
168
|
+
* unparseable", yet loadAll() treats both as "no data" and the next saveAll()
|
|
169
|
+
* writes that emptiness back — so one externally damaged State/*.json silently
|
|
170
|
+
* reset the whole run (residents, taskboard, mailboxes). Any read that hits a
|
|
171
|
+
* present-but-unparseable JSON file records it; writeJson then REFUSES to write
|
|
172
|
+
* that path until the file is fixed or deleted. A missing file is still created
|
|
173
|
+
* normally, so first-run behaviour is unchanged.
|
|
174
|
+
*/
|
|
175
|
+
const suspectFiles = new Set()
|
|
176
|
+
const warnedSuspect = {}
|
|
177
|
+
function noteSuspect(rel){
|
|
178
|
+
suspectFiles.add(rel)
|
|
179
|
+
if(warnedSuspect[rel]) return
|
|
180
|
+
warnedSuspect[rel]=true
|
|
181
|
+
console.error('vibe-math-v4: '+rel+' exists but is not parseable JSON — REFUSING to overwrite it so a corrupted file cannot silently erase your run. Fix or delete the file, then retry.')
|
|
182
|
+
}
|
|
183
|
+
function assertWritableSync(rel){
|
|
184
|
+
if(suspectFiles.has(rel)){ console.error('vibe-math-v4: write to '+rel+' blocked (file is unparseable; see the earlier warning)'); return false }
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Nothing read this path yet, so inspect the file actually on disk before
|
|
189
|
+
* replacing it. This closes the case where a fresh process never ran loadAll()
|
|
190
|
+
* (configure/start) and would otherwise write an empty state over a corrupt one
|
|
191
|
+
* the user might still want to inspect or repair.
|
|
192
|
+
*/
|
|
193
|
+
async function assertWritable(rel){
|
|
194
|
+
if(!assertWritableSync(rel)) return false
|
|
195
|
+
try {
|
|
196
|
+
const raw=await readText(rel)
|
|
197
|
+
if(raw!==undefined && raw!==''){
|
|
198
|
+
try { JSON.parse(raw) } catch(e){ noteSuspect(rel); return false }
|
|
199
|
+
}
|
|
200
|
+
} catch(e){}
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
async function ensureDirs(){ const base=frameworkRoot(); const dirs=['Problems','Progress','Propos','Methods','Subproblems','Shared/meetings','Shared/debates','Verified/命题','Verified/问题','Reliable','Notes','State']; return await runShell(mkdirCmd([vibeRoot()+'/Projects'].concat(dirs.map(d=>base+'/'+d)))) }
|
|
204
|
+
async function readTextAbs(path){ try { const t=await fs.resolve(path); const s=await fs.stat(t); if(s===undefined) return undefined; return await fs.readText(t) } catch(e){ return undefined } }
|
|
205
|
+
async function writeTextAbs(path,content){ try { const t=await fs.resolve(path); await fs.writeText(t,content,undefined,undefined,getPolicy()); return true } catch(e){ return false } }
|
|
206
|
+
async function readCurrentProject(){ try { const t=await readTextAbs(vibeRoot()+'/.current'); if(t) return String(t).trim() } catch(e){} return currentProject }
|
|
207
|
+
async function writeCurrentProject(){ try { await writeTextAbs(vibeRoot()+'/.current', currentProject) } catch(e){} }
|
|
208
|
+
function tryJson(s){ try { return JSON.parse(s) } catch(e){ return undefined } }
|
|
209
|
+
function parseReply(text){
|
|
210
|
+
let obj; const fence=/```(?:json)?[ \t]*([\s\S]*?)```/gi; let m
|
|
211
|
+
while((m=fence.exec(text))!==null){ const o=tryJson(m[1].trim()); if(o&&typeof o==='object'&&!Array.isArray(o)) obj=o }
|
|
212
|
+
if(!obj){ const w=tryJson(text.trim()); if(w&&typeof w==='object'&&!Array.isArray(w)) obj=w }
|
|
213
|
+
return obj||{}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---- persistence ----
|
|
217
|
+
async function saveAll(){
|
|
218
|
+
await writeJson('State/residents.json', Object.fromEntries(residents))
|
|
219
|
+
await writeJson('State/mailboxes.json', Object.fromEntries(mailboxes))
|
|
220
|
+
await writeJson('State/taskboard.json', taskboard)
|
|
221
|
+
await writeJson('State/decisions.json', decisions)
|
|
222
|
+
await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,lastProgressAt,activityLog,processEpoch,artifactCount})
|
|
223
|
+
}
|
|
224
|
+
async function loadAll(){
|
|
225
|
+
const s=await readJson('State/session.json'); if(s){ running=!!s.running; autoDone=!!s.autoDone; phase=s.phase||'idle'; problemId=s.problemId||problemId; problemText=s.problemText||problemText; runId=s.runId||runId; meetings=s.meetings||[]; reports=s.reports||[]; lastActivityAt=s.lastActivityAt||now(); lastProgressAt=s.lastProgressAt||now(); activityLog=s.activityLog||activityLog; persistedEpoch=s.processEpoch||''; artifactCount=s.artifactCount||0 }
|
|
226
|
+
const rm=await readJson('State/residents.json'); if(rm&&typeof rm==='object') residents=new Map(Object.entries(rm))
|
|
227
|
+
const mb=await readJson('State/mailboxes.json'); if(mb&&typeof mb==='object') mailboxes=new Map(Object.entries(mb))
|
|
228
|
+
const tb=await readJson('State/taskboard.json'); if(Array.isArray(tb)) taskboard=tb
|
|
229
|
+
const dc=await readJson('State/decisions.json'); if(Array.isArray(dc)) decisions=dc
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---- resident prompts ----
|
|
233
|
+
function banner(){ const o=[]; for(const [id,r] of residents) o.push('- '+id+'「'+(r.direction||'(未定)')+'」'+r.status+'·轮'+r.rounds); return o.join('\n') }
|
|
234
|
+
async function inboxText(rId){ const mb=mailboxes.get(rId)||[]; if(mb.length===0) return ' (no new messages)\n'; return mb.map(m=>' ['+m.from+'] '+m.content).join('\n')+'\n' }
|
|
235
|
+
function residentLibraries(){
|
|
236
|
+
const base=frameworkRoot()
|
|
237
|
+
return '你的资料库根目录:'+base+'/\n'
|
|
238
|
+
+' Progress/<你>/progress.md —— 你的研究日志(叙述,可追加。主要内容是尝试过的各方法、路线、历程、进度,当前研究进展/进度、将来的计划与打算,及各路线、过程中遇到的障碍及其原因,对各路线、方法的看法、可行性评估,自己研究过程中的一些有价值看法、感想、猜想、理解。以及其它各种你认为有价值的值得记录的事物、经验、方法/想法、创新等都可进行记录)。\n'
|
|
239
|
+
+' Propos/<你>/<id>.md —— 你的命题/引理。格式:\n'
|
|
240
|
+
+' - ID: p-<id>; - 状态: 未定论; - 概率: <0-1>; - 价值程度: <0-1>; - 动机用途计划: <为何重要/打算怎么用>\n'
|
|
241
|
+
+' 然后 ## 陈述 <陈述>;## 证明尝试;## 证伪尝试。\n'
|
|
242
|
+
+' Methods/<你>/<id>.md —— 你的理论/方法/工具。格式:- ID: m-<id>; - 状态: 经验; - 可信断言: []; - 价值程度: <0-1>; - 动机用途计划: ...;然后 ## 核心内容;## 定义与记号;## 应用记录;## 改进历史。\n'
|
|
243
|
+
+' Subproblems/<你>/<id>.md —— 你的子问题。格式:- ID: s-<id>; - 状态: 求解中; - 价值程度: <0-1>; - 动机用途计划: ...;然后 ## 陈述;## 进度。\n'
|
|
244
|
+
}
|
|
245
|
+
function toolList(){
|
|
246
|
+
return 'vibe_v4_send_message {to, content} —— 给某常驻发消息(to=all 广播)。\n'
|
|
247
|
+
+'vibe_v4_meeting {agenda} —— 发起/参与会议(框架会把各常驻的实际 input 转给其他人,让大家看到并讨论/辩论)。\n'
|
|
248
|
+
+'vibe_v4_propose_task/claim_task/task_done/list_tasks —— 共享任务板(提议/认领/完成/查看;任务板是你们协调分工的载体)。\n'
|
|
249
|
+
+'vibe_v4_publish_progress/record_proposition/record_method/record_subproblem —— 便捷记录器(可选;推荐直接用 fs 写自己的文件)。\n'
|
|
250
|
+
+'vibe_v4_read_progress {id} —— 只读某常驻的进展。\n'
|
|
251
|
+
+'vibe_v4_list_residents / vibe_v4_list_tasks —— 查看团队组成 / 开放任务。\n'
|
|
252
|
+
+'vibe_v4_report_context {pct} —— 上报上下文占比(框架据此压缩你的上下文)。\n'
|
|
253
|
+
+'fs (read/write/list) —— 读取任意文件;写入你自己的文件(推荐直接用 fs 直接写自己的 md)。\n'
|
|
254
|
+
}
|
|
255
|
+
// A shared, complete context block so a resident always knows the situation: mission,
|
|
256
|
+
// work model, what it can do, which files it owns (+ formats), what others' files are,
|
|
257
|
+
// and that it may READ anyone and WRITE its own directly. level 'full' = initial brief.
|
|
258
|
+
function contextBrief(r, level){
|
|
259
|
+
const s=[]
|
|
260
|
+
s.push('## 背景 —— 你是常驻研究团队的一员')
|
|
261
|
+
s.push('You are resident researcher '+r.rId+'(常驻研究者 '+r.rId+';共 '+params.residentCount+' 位常驻),正在协作解决:')
|
|
262
|
+
s.push(problemText)
|
|
263
|
+
s.push('')
|
|
264
|
+
s.push('这像一个**真实的学术小组**:没有中央调度器、没有外部派活——你们自己通过 **互相发消息 + 开会讨论** 来决定一切:谁做什么、怎么分工、验证什么、何时停止。你的 Round 决定你这一轮做什么;团队的优先级与分工由大家的讨论涌现。')
|
|
265
|
+
s.push('')
|
|
266
|
+
s.push('### 工作模式(会发生什么)')
|
|
267
|
+
s.push('1. 每人有一份持久、全组可见的专属资料库(见下)。')
|
|
268
|
+
s.push('2. 你们自由发消息、开会;**会议会把每个人实际说的话(input)转给其他人**,让你看得到、能回复、能讨论、能辩论。')
|
|
269
|
+
s.push('3. 你独立研究,并**直接用 fs 写入你自己的文件**(按格式),供全组阅读。')
|
|
270
|
+
s.push('4. 任何"已确立"的东西须**全组一致**验证(全真或全假)才作数;否则只是带概率的工作估计。')
|
|
271
|
+
s.push('5. 只有**全组在会议上一致认为原问题已解决**,run 才停止。')
|
|
272
|
+
s.push('')
|
|
273
|
+
s.push('### 你负责的文件(你只写自己的;但可读任何人的)')
|
|
274
|
+
s.push(residentLibraries())
|
|
275
|
+
s.push('其他人把结论/进展写进他们的目录,你就能读到。**你应主动读别人的库**,对齐事实、彼此衔接、避免重复劳动。')
|
|
276
|
+
if(level!=='full'){ s.push('(格式见你最初的说明;直接用 fs 写自己的文件即可。)') }
|
|
277
|
+
s.push('')
|
|
278
|
+
s.push('### 可用工具')
|
|
279
|
+
s.push(toolList())
|
|
280
|
+
s.push('')
|
|
281
|
+
if(level==='full'){
|
|
282
|
+
s.push('### 可自主发明理论/工具(鼓励,但不强迫)')
|
|
283
|
+
s.push('请注意:你可以(但**不强迫**,完全视实际需要而定)尝试自主构建新的理论框架或工具——例如对某种系统做抽象化、一般化,抽离/推广出更一般的结构或理论框架;然后不断完善这个理论框架,在该框架下推得各种定理、性质、结论,以利于该框架下问题的解决。这就像为解决方程问题发明了群论、为分析需要建立了泛函分析框架——它比单纯解决当前问题更有学术价值,因为你直接得到了一类更普遍的方法/理论体系。')
|
|
284
|
+
s.push('若你发明了这样的理论/工具,请**阐明它对原问题的用处、价值**;后续可根据需要不断**完善、一般化、推广**它。把这类成果记入你的 Methods/<你>/ 库。')
|
|
285
|
+
s.push('')
|
|
286
|
+
}
|
|
287
|
+
s.push('### 规则')
|
|
288
|
+
s.push('- 只有 Verified/(或卡片标"已验证·真/假")算已确立;其余都是你的实验性工作,请区分"猜想/已知"。')
|
|
289
|
+
s.push('- 验证必须**全组一致**(全真或全假);你只信全票结果。未全票的对象留在库里带概率。')
|
|
290
|
+
s.push('- 你自己决定做什么,但**优先级/分工由团队讨论决定**,不是固定模式。若你认为问题已解决或接近解决,请**发起会议**让团队表决。')
|
|
291
|
+
s.push('- 退出时**只**输出一个 JSON 对象(放在 ```json 代码围栏内;围栏外不要有文字)。')
|
|
292
|
+
return s.join('\n')
|
|
293
|
+
}
|
|
294
|
+
// A SHORT core-rules recap, re-injected ONLY right after a compaction so the resident
|
|
295
|
+
// never loses the ground rules (they are told fully once at brainstorm, but a /compact
|
|
296
|
+
// could blank them).
|
|
297
|
+
function coreRulesBrief(){
|
|
298
|
+
const base=frameworkRoot()
|
|
299
|
+
return '[核心规则重申] 只有 Verified/(及标记"已验证·真/假")算已确立;验证须全组一致(全真或全假)才作数,否则留库附平均概率;你只写自己的库('+base+'/ 的 Progress/<你>/、Propos/<你>/、Methods/<你>/、Subproblems/<你>/),可只读任何人的库;任务分工由团队讨论决定;退出只输出一个 JSON 对象。'
|
|
300
|
+
}
|
|
301
|
+
function brainstormPrompt(r){
|
|
302
|
+
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
303
|
+
+contextBrief(r,'full')+'\n'
|
|
304
|
+
+(r.direction?('\n\n你被建议的初始方向(可自行调整/细化):\n'+r.direction+'\n'):'')
|
|
305
|
+
+'## 这是你的第一轮:独立头脑风暴\n'
|
|
306
|
+
+'独立地想清楚:你对这个问题的洞察 / 解决方向 / 关键子问题 / 可能的引理 / 粗略计划。你还未见到其他人,先独立产出。\n'
|
|
307
|
+
+'把有价值的产物**直接用 fs 写进你自己的文件**(按上面格式),并在 summary 里概述你的切入方向与初步结论(标注哪些是猜想、哪些凭你已确证)。\n'
|
|
308
|
+
+'Reply with ONLY a JSON object:\n'
|
|
309
|
+
+'{"summary":"<your insight / direction / rough plan, one tight paragraph>","solved":false}'
|
|
310
|
+
}
|
|
311
|
+
async function normalPrompt(r){
|
|
312
|
+
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
313
|
+
+'Resident researcher '+r.rId+' — 第 '+r.rounds+' 轮。一切由你和团队讨论决定。动手前先**读别人的库**对齐事实、避免重复;把新进展/结论**直接用 fs 写进你自己的文件**;想对团队说的话放 "input"(会转给其他常驻)。\n'
|
|
314
|
+
+'\n团队成员:\n'+banner()+'\n'
|
|
315
|
+
+'New items:\n'+ (await inboxText(r.rId)) +'\n'
|
|
316
|
+
+'Reply with ONLY a JSON object:\n'
|
|
317
|
+
+'{"summary":"<what you did / decided this round, 1-3 sentences>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<task id|null>","task_done":"<task id|null>","contextPct":40}'
|
|
318
|
+
}
|
|
319
|
+
function meetingPrompt(r, st){
|
|
320
|
+
const prior=Object.entries(st.inputs).filter(([k])=>k!==r.rId).map(([k,iv])=>' ['+k+'] '+String(iv.input||iv.summary||'')).join('\n')
|
|
321
|
+
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
322
|
+
+'Resident '+r.rId+' — 团队会议进行中。 A meeting is in progress (agenda: '+st.agenda+').'
|
|
323
|
+
+(st.type==='verify'?('\n团队正在验证对象:'+st.targetId+'('+st.targetType+',提出者 '+st.targetOwner+')。请先看他人意见,再给独立判断。'):'')
|
|
324
|
+
+'\n这是一场真实讨论:下面已有人发言(转给你),请先看,然后**加入讨论/补充/反驳/表决**。'
|
|
325
|
+
+(prior?('\n\n### 已有发言(他人 input,已转发给你)\n'+prior):'\n(目前还没人发言,你先说。)')
|
|
326
|
+
+'\n\n你可以:提议任务(propose_task)、认领开放任务(claim_task)、提议验证对象(propose_verify)、或对"原问题是否已解决"表决(voteSolved)。请把**你的实际发言**写进 "input"。'
|
|
327
|
+
+'\nReply with ONLY a JSON object:\n'
|
|
328
|
+
+'{"input":"<your real contribution to this discussion>","propose_task":"<task title or null>","task_desc":"...","claim_task":"<task id or null>","propose_verify":"<id or null>","voteSolved":true}'
|
|
329
|
+
}
|
|
330
|
+
function verifyPrompt(r, vs){
|
|
331
|
+
// In a DEBATE round, show the PREVIOUS round's opinions (kept in vs.history) so the resident can
|
|
332
|
+
// see others' stances and give a fresh independent judgement; in the first (independent) round no
|
|
333
|
+
// others' opinions exist yet. vs.verdicts only ever holds the CURRENT round's votes.
|
|
334
|
+
const src = (vs.stage==='debate' && vs.history && Object.keys(vs.history).length>0) ? vs.history : (vs.stage==='debate' ? vs.verdicts : {})
|
|
335
|
+
const others=Object.entries(src).map(([k,v])=>'- '+k+': 正确概率 '+String(v.prob!=null?Number(v.prob).toFixed(2):0.5)+' → '+v.reason).join('\n')
|
|
336
|
+
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
337
|
+
+'Resident '+r.rId+' — 团队验证。 The group is verifying object '+vs.targetId+'('+vs.targetType+',提出者 '+vs.targetOwner+')。\n'
|
|
338
|
+
+'请给出你对「该对象为真」的**正确概率 `verdict`**,仅一个 0–1 数值:**1 = 绝对为真,0 = 绝对为假,0.5 = 完全不确定,其余为介于其间的程度**(不要给 TRUE/FALSE,就给一个数值)。\n'
|
|
339
|
+
+'判定规则:仅当**全体常驻一致给 1(都认为是真)或一致给 0(都认为是假)**,才按「真/假」写入 Verified/;否则**只作为概率数值(一种程度)保留在库中**,附全组平均正确概率,不写成真/假。\n'
|
|
340
|
+
+'请给出你**诚实独立的判断**'
|
|
341
|
+
+(vs.stage==='debate'?',并参考他人意见:\n':'。\n')
|
|
342
|
+
+(vs.stage==='debate'&&others?('### 他人上一轮意见(已转发给你)\n'+others+'\n'):'')
|
|
343
|
+
+'\nReply with ONLY a JSON object:\n'
|
|
344
|
+
+'{"vote":{"verdict":0.9,"reason":"<your logic>"}}'
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---- resident lifecycle ----
|
|
348
|
+
let residentSeq = 0
|
|
349
|
+
function newResident(dir){ const rId='r-'+(++residentSeq); return {rId,childId:'',direction:dir||'',status:'brainstorm',rounds:0,roundsSinceCompact:0,lastActiveAt:now(),insight:'',contextPct:0,contextSeed:'',needCompact:false} }
|
|
350
|
+
async function spawnResident(r){
|
|
351
|
+
const ao=residentAgentOptions(); const tf=residentToolFilter()
|
|
352
|
+
const started=await subagents.startContinuable({provider:pickProvider(),label:r.rId,request:{prompt:[textBlock(brainstormPrompt(r))],parent:rootAgent,agentOptions:ao,...(tf?{toolFilter:tf}:{})},signal:makeSignal(params.activityTimeoutMs||60000)})
|
|
353
|
+
r.childId=started.childId; r.status='brainstorm'; r.lastActiveAt=now()
|
|
354
|
+
childOwner.set(started.childId,sessionId); busy.add(r.rId); wakeKind.set(r.rId,'normal'); currentResident=r.rId
|
|
355
|
+
residents.set(r.rId,r); await saveAll(); logActivity('spawn',r.rId+' ('+(r.direction||'brainstorm')+')')
|
|
356
|
+
}
|
|
357
|
+
async function wakeResident(r, promptText, kind){
|
|
358
|
+
if(!r || !r.childId) return false // a removed resident must never be woken (else r.childId would crash)
|
|
359
|
+
clearHeartbeat()
|
|
360
|
+
busy.add(r.rId); wakeKind.set(r.rId,kind||'normal'); currentResident=r.rId
|
|
361
|
+
r.lastActiveAt=now(); r.rounds+=1; r.roundsSinceCompact+=1
|
|
362
|
+
// Context compaction has TWO distinct needs. Confusing them is the bug that made
|
|
363
|
+
// '[核心规则重申]+[CONTEXT COMPACT]' repeat at the start of nearly every prompt:
|
|
364
|
+
// (a) r.needCompact (set by a REAL /compact) => the resident's rules may be blurred, so
|
|
365
|
+
// re-anchor the short core rules on the next wake of ANY kind, then CLEAR the flag.
|
|
366
|
+
// (Short recap only; no self-summary directive — the real compact already condensed.)
|
|
367
|
+
// (b) soft-compact trigger (contextPct>=threshold OR roundsSinceCompact>=afterRounds) =>
|
|
368
|
+
// the resident's context genuinely grew; ask it to self-summary. ONLY on a normal
|
|
369
|
+
// research round (kind==='normal'): a meeting/verify reply has no contextPct/compacted
|
|
370
|
+
// fields, so a directive injected there is never acknowledged and would repeat forever.
|
|
371
|
+
let prompt = promptText
|
|
372
|
+
const isNormal = (kind||'normal')==='normal'
|
|
373
|
+
const wantSoft = isNormal && (Number(r.contextPct)>=Number(params.compactThreshold) || Number(r.roundsSinceCompact)>=Number(params.compactAfterRounds))
|
|
374
|
+
const wantReanchor = r.needCompact
|
|
375
|
+
if(wantSoft){
|
|
376
|
+
prompt = coreRulesBrief() + '\n' +
|
|
377
|
+
'[CONTEXT COMPACT — your conversation is at/near the limit. Do NOT re-derive history.\n' +
|
|
378
|
+
'Condense your current working state into ONE tight self-summary (findings so far, active direction, key artifacts you recorded, next concrete steps, open questions), then answer this round in the normal JSON format as usual.\n' +
|
|
379
|
+
'Set "contextPct": 15 (your post-compact usage) and "compacted": true in the reply so the framework records the condensed seed.]\n\n' + promptText
|
|
380
|
+
r.needCompact = true
|
|
381
|
+
} else if(wantReanchor){
|
|
382
|
+
prompt = coreRulesBrief() + '\n' + prompt
|
|
383
|
+
r.needCompact = false
|
|
384
|
+
}
|
|
385
|
+
// The DSH continuable-wake API is subagents.sendMessage(sender, targetId, content, {signal}),
|
|
386
|
+
// NOT subagents.followup (which is only Agent.followup, and does NOT exist on the subagents
|
|
387
|
+
// service). Using a non-existent method threw TypeError and made EVERY wake fail silently →
|
|
388
|
+
// the group went idle forever. Prefer sendMessage; fall back to a legacy followup if a host
|
|
389
|
+
// still exposes it (older deployments), so this works across versions.
|
|
390
|
+
try {
|
|
391
|
+
if(typeof subagents.sendMessage==='function'){
|
|
392
|
+
await subagents.sendMessage(rootAgent, r.childId, [textBlock(prompt)], {signal: makeSignal(params.activityTimeoutMs||60000)})
|
|
393
|
+
} else if(typeof subagents.followup==='function'){
|
|
394
|
+
await subagents.followup(rootAgent, r.childId, [textBlock(prompt)], {source:{kind:'user'},signal:makeSignal(params.activityTimeoutMs||60000)})
|
|
395
|
+
} else {
|
|
396
|
+
throw new Error('no subagent continuation API (need sendMessage or followup)')
|
|
397
|
+
}
|
|
398
|
+
return true
|
|
399
|
+
}
|
|
400
|
+
catch(e){ console.error('vibe-v4 wake '+r.rId+' failed: '+String((e&&e.message)||e)); busy.delete(r.rId); return false }
|
|
401
|
+
}
|
|
402
|
+
function byChild(childId){ for(const [,r] of residents){ if(r.childId===childId) return r } return undefined }
|
|
403
|
+
|
|
404
|
+
// ---- artifact writers (resident-facing) ----
|
|
405
|
+
async function publishProgress(rId,content){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const rel='Progress/'+rId+'/progress.md'; const prev=(await readText(rel))||''; await writeText(rel, prev+'\n### '+fmtTime()+'|'+rId+'\n'+String(content||'')+'\n'); return {ok:true} }
|
|
406
|
+
async function recordProposition(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id?idSafe(o.id):('p-'+shortId()); const lines=['# 命题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: 命题','- 状态: 未定论','- 概率: '+cl(o.prob!=null?o.prob:0.5),'- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 证明尝试','','## 证伪尝试','']; await writeText('Propos/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 命题 '+id); bumpArtifacts(); return {ok:true,id,file:'Propos/'+rId+'/'+id+'.md'} }
|
|
407
|
+
async function recordMethod(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id?idSafe(o.id):('m-'+shortId()); const lines=['# 方法|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: '+(o.type||'方法'),'- 状态: 经验','- 可信断言: []','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'','## 核心内容',String(o.content||''),'','## 定义与记号',String(o.notation||''),'','## 应用记录','## 改进历史','']; await writeText('Methods/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 方法 '+id); bumpArtifacts(); return {ok:true,id,file:'Methods/'+rId+'/'+id+'.md'} }
|
|
408
|
+
async function recordSubproblem(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id?idSafe(o.id):('s-'+shortId()); const lines=['# 子问题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 状态: 求解中','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 进度','']; await writeText('Subproblems/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 子问题 '+id); bumpArtifacts(); return {ok:true,id,file:'Subproblems/'+rId+'/'+id+'.md'} }
|
|
409
|
+
// auto-sync meeting: every meetingKeepEvery artifact records, convene a general coordination meeting.
|
|
410
|
+
//
|
|
411
|
+
// ⚠ 已知设计缺口(有意保留,未修):计数口径**只有** record_* 三个便捷工具的调用。而提示词明确
|
|
412
|
+
// 告诉常驻:"vibe_v4_publish_progress/record_* 只是便捷记录器(可选;推荐直接用 fs 写自己的文件)",
|
|
413
|
+
// 所以一个完全按推荐方式(fs 直写)工作的团队不会让 artifactCount 增长,
|
|
414
|
+
// `artifactCount % meetingKeepEvery === 0` 永不成立 —— 文档承诺的"每积累 N 个新产物自动同步
|
|
415
|
+
// 一次"在推荐工作流下不可达(此时只有"停滞看门狗"那条时间触发路径会开会)。
|
|
416
|
+
// 试过把"完成的常驻轮次"也计入,但那会改变开会节奏,令 e2e-v4-fixes T4 与 selfdrive-v4 的
|
|
417
|
+
// 时序断言失败(两套测试都按当前节奏写死了预期)。这属于**设计参数取舍**,需要维护者决定:
|
|
418
|
+
// 要么改计数口径并同步调整测试预期,要么把"便捷记录器可选"的措辞改为"建议使用以便触发周期同步"。
|
|
419
|
+
function bumpArtifacts(){ artifactCount+=1; markProgress(); if(!meetingState && !verifyState && !pendingMeeting && Number(params.meetingKeepEvery)>0 && artifactCount % Number(params.meetingKeepEvery)===0){ startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{}) } }
|
|
420
|
+
function listResidents(){ return Array.from(residents.values()).map(r=>({id:r.rId,direction:r.direction,status:r.status,rounds:r.rounds,contextPct:r.contextPct,insight:r.insight?r.insight.slice(0,80):''})) }
|
|
421
|
+
// identify WHICH resident is calling a resident-facing tool: match the caller's
|
|
422
|
+
// subagent id to a resident's childId. Fall back to the last-woken resident when
|
|
423
|
+
// the caller is the host/assistant (or an unknown agent). This makes per-resident
|
|
424
|
+
// libraries correct under concurrency (e.g. all brainstorm residents in flight).
|
|
425
|
+
function residentOfAgent(agent){ try { const id=agent&&agent.id?String(agent.id):''; if(!id) return ''; for(const [,r] of residents){ if(r.childId===id) return r.rId } } catch(e){} return '' }
|
|
426
|
+
|
|
427
|
+
// ---- task board (residents propose / claim / complete; framework wakes the claimer) ----
|
|
428
|
+
async function writeTaskboard(){ const lines=['# 任务板','']; for(const t of taskboard){ lines.push('- ['+t.status+'] '+t.title+(t.claimer?('(认领:'+t.claimer+')'):'')+(t.proposer?('(提议:'+t.proposer+')'):'')+(t.description?(':'+t.description):'')) } await writeText('Shared/taskboard.md',lines.join('\n')) }
|
|
429
|
+
async function proposeTask(title,description,proposer){ const id='t-'+shortId(); taskboard.push({id,title:String(title),description:String(description||''),status:'open',proposer:proposer||'',claimer:'',source:''}); await saveTaskboard(); markProgress(); logActivity('task','proposed '+id+'「'+title+'」'); return {ok:true,id} }
|
|
430
|
+
async function claimTask(id,claimer){ const t=taskboard.find(x=>x.id===id); if(!t) return {ok:false,message:'no such task'}; if(t.status!=='open') return {ok:false,message:'task already '+t.status}; t.status='claimed'; t.claimer=claimer; await saveTaskboard(); markProgress(); logActivity('task',claimer+' claimed '+id);
|
|
431
|
+
// wake the claimer to work on it (framework moves the task, resident decides how).
|
|
432
|
+
// NOT while paused/stopped: a paused run must not start new work — the claim is recorded on the
|
|
433
|
+
// board and the resident (who claimed it) picks it up again after resume.
|
|
434
|
+
const r=residents.get(claimer); if(r && !busy.has(claimer) && running && !autoDone){ currentResident=claimer; await wakeResident(r, (await normalPrompt(r))+'\n\n[YOU CLAIMED TASK '+id+'] '+t.title+' — '+t.description,'normal'); await saveAll() }
|
|
435
|
+
return {ok:true} }
|
|
436
|
+
async function taskDone(id,claimer){ const t=taskboard.find(x=>x.id===id); if(!t) return {ok:false}; t.status='done'; t.doneBy=claimer; await saveTaskboard(); await writeTaskboard(); markProgress(); logActivity('task','done '+id); return {ok:true} }
|
|
437
|
+
async function saveTaskboard(){ await writeJson('State/taskboard.json',taskboard); await writeTaskboard() }
|
|
438
|
+
function listTasks(){ return taskboard.filter(t=>t.status!=='done') }
|
|
439
|
+
async function reportContext(rId,pct){ const r=residents.get(rId); if(r){ r.contextPct=clPct(pct); if(Number(pct)<30) r.needCompact=false; } return {ok:true} }
|
|
440
|
+
// Apply context/compact bookkeeping from a resident's reply, so the flag can clear even when
|
|
441
|
+
// the reply came through a meeting/verify branch (defensive) as well as the normal branch.
|
|
442
|
+
function postmark(r, parsed){
|
|
443
|
+
const cp=Number(parsed.contextPct); if(Number.isFinite(cp)) r.contextPct=clPct(cp) // tolerate numeric strings ("40")
|
|
444
|
+
if(parsed.compacted===true || (r.needCompact && parsed.summary)){
|
|
445
|
+
r.contextSeed=String(parsed.summary||r.contextSeed||'')
|
|
446
|
+
r.contextPct=Math.min(r.contextPct||15,25)
|
|
447
|
+
r.roundsSinceCompact=0
|
|
448
|
+
r.needCompact=false
|
|
449
|
+
logActivity('compact', r.rId+' consolidated context')
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---- messaging ----
|
|
454
|
+
async function postMessage(from,to,content){
|
|
455
|
+
const r=residents.get(to); if(!r) return {ok:false,message:'no such resident'}
|
|
456
|
+
if(!busy.has(to)){
|
|
457
|
+
currentResident=r.rId
|
|
458
|
+
await wakeResident(r, (await normalPrompt(r))+'\n\n[NEW MESSAGE from '+from+']\n'+content,'normal')
|
|
459
|
+
await saveAll(); markProgress(); logActivity('message',from+'→'+to); return {ok:true}
|
|
460
|
+
}
|
|
461
|
+
const mb=mailboxes.get(to)||[]; mb.push({from,at:now(),content}); mailboxes.set(to,mb); await saveAll(); logActivity('message',from+'→'+to+' (queued)'); return {ok:true}
|
|
462
|
+
}
|
|
463
|
+
async function broadcast(content, from){
|
|
464
|
+
let n=0
|
|
465
|
+
for(const [,r] of residents){ if(from && r.rId===from) continue; const res=await postMessage(from||'facilitator',r.rId,content); if(res&&res.ok) n++ }
|
|
466
|
+
logActivity('broadcast','to '+n+' resident(s)'); await saveAll(); return {ok:true,message:'broadcast to '+n+' resident(s)'}
|
|
467
|
+
}
|
|
468
|
+
// group conversation relay: when a resident "speaks" (input in its round), forward its
|
|
469
|
+
// words to every other resident's inbox so the whole group can see & react — a real group chat.
|
|
470
|
+
async function relayToGroup(from, content){
|
|
471
|
+
const text=String(content||'').trim()
|
|
472
|
+
if(!text) return
|
|
473
|
+
for(const [,r] of residents){
|
|
474
|
+
if(r.rId===from) continue
|
|
475
|
+
const mb=mailboxes.get(r.rId)||[]; mb.push({from,at:now(),content:'[群聊] '+text}); mailboxes.set(r.rId,mb)
|
|
476
|
+
}
|
|
477
|
+
logActivity('relay',from+' → 团队: '+text.slice(0,60)); markProgress(); await saveAll()
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ---- meeting ----
|
|
481
|
+
async function startMeeting(agenda,type,targetId){
|
|
482
|
+
if(meetingState) return {ok:false,message:'meeting already in progress'}
|
|
483
|
+
// Park-and-resume (never lose a coordination request, never create a zombie): while a
|
|
484
|
+
// verification holds the floor, while the group is still brainstorming (members are busy in
|
|
485
|
+
// their first rounds — a meeting started there could not be serviced and the old code let its
|
|
486
|
+
// stall watchdog silently ABANDON it minutes later), or while the run is paused, the meeting
|
|
487
|
+
// request is parked in pendingMeeting (FIRST request wins) and starts as soon as the floor is
|
|
488
|
+
// free. A never-started session (no residents to talk) and a concluded run (autoDone) refuse
|
|
489
|
+
// instead — convening there previously created a meeting nobody could ever be woken into.
|
|
490
|
+
if(!running || autoDone || phase==='brainstorm' || verifyState || pendingVerify.length>0){
|
|
491
|
+
if(autoDone || (!running && residents.size===0)) return {ok:false,message:'run is not active (use vibe_v4_start or vibe_v4_resume first)'}
|
|
492
|
+
if(!pendingMeeting) pendingMeeting = { agenda, type:type||'general', targetId:targetId||null }
|
|
493
|
+
return {ok:true,deferred:true,during: phase==='brainstorm'?'brainstorm':(!running?'paused':'verify')}
|
|
494
|
+
}
|
|
495
|
+
clearHeartbeat()
|
|
496
|
+
const ids=Array.from(residents.keys())
|
|
497
|
+
// Rotate the per-meeting speaking order so the SAME resident isn't always the "first speaker
|
|
498
|
+
// who sees no one else's contribution"; a real discussion lets each member lead sometimes.
|
|
499
|
+
const rot=Math.floor(Math.random()*Math.max(1,ids.length))
|
|
500
|
+
const order=ids.slice(rot).concat(ids.slice(0,rot))
|
|
501
|
+
meetingState={id:'mt-'+shortId(),agenda,type:type||'general',targetId:targetId||null,round:0,asked:[],inputs:{},transcript:[],order,at:now(),lastInputAt:now()}
|
|
502
|
+
markProgress();
|
|
503
|
+
logActivity('meeting','start: '+agenda); await saveAll(); await scheduleNext(); return {ok:true,id:meetingState.id}
|
|
504
|
+
}
|
|
505
|
+
async function continueMeetingRound(){
|
|
506
|
+
if(!meetingState) return
|
|
507
|
+
const st=meetingState
|
|
508
|
+
// STUCK watchdog: a meeting that has been active but collected NO new input for a long while
|
|
509
|
+
// is deadlocked (e.g. an in-flight/hung resident, a run of failed wakes). Abandoning it returns
|
|
510
|
+
// the group to normal self-organization (A heartbeat / B auto-sync can then re-drive) instead of
|
|
511
|
+
// permanently blocking the whole group behind a broken meeting.
|
|
512
|
+
if(now()-(st.lastInputAt||st.at||now())>=recoverStallMs()){
|
|
513
|
+
meetingState=null; wakeKind.clear(); logActivity('meeting','abandoned (stuck: no resident spoke)')
|
|
514
|
+
await saveAll(); await scheduleNext(); return
|
|
515
|
+
}
|
|
516
|
+
const ids=Array.from(residents.keys()); const allSpoke=ids.every(id=>st.inputs[id]!==undefined)
|
|
517
|
+
if(allSpoke){ await finalizeMeeting(); return }
|
|
518
|
+
// only wake IDLE un-spoken residents (rotated order); in-flight ones re-trigger this on end.
|
|
519
|
+
// NOTE: we deliberately do NOT flush mailboxes here — drafting an un-spoken resident into a normal
|
|
520
|
+
// mail round would delay the meeting and can starve the consensus past its watchdog if the mail
|
|
521
|
+
// backlog is large. Mail is delivered on scheduleNext passes when no consensus is in progress.
|
|
522
|
+
const order=st.order||ids
|
|
523
|
+
const id=order.find(x=>st.inputs[x]===undefined && !busy.has(x))
|
|
524
|
+
if(!id){ armHeartbeat(); return } // no idle un-spoken resident (a busy/hung one): re-check later
|
|
525
|
+
const r=residents.get(id)
|
|
526
|
+
const ok = await wakeResident(r, meetingPrompt(r,st), 'meeting'); await saveAll()
|
|
527
|
+
if(!ok) armHeartbeat() // a failed meeting wake must NOT silently hang the meeting
|
|
528
|
+
}
|
|
529
|
+
async function finalizeMeeting(){
|
|
530
|
+
if(finalizeLock) return // reentry guard: two onResidentEnd may both see allSpoke → only finalize once
|
|
531
|
+
finalizeLock='meeting'
|
|
532
|
+
let doSchedule=false
|
|
533
|
+
try {
|
|
534
|
+
const st=meetingState
|
|
535
|
+
const ids=Array.from(residents.keys()); const allSpoke=ids.length>0 && ids.every(id=>st.inputs[id]!==undefined)
|
|
536
|
+
const lines=['# 会议 '+st.id+'|'+fmtTime(),'','**议程**:'+st.agenda,'']
|
|
537
|
+
for(const [id,iv] of Object.entries(st.inputs)){ lines.push('### '+id); lines.push(iv.input||''); lines.push('') }
|
|
538
|
+
await writeText('Shared/meetings/'+st.id+'.md', lines.join('\n'))
|
|
539
|
+
// the full transcript lives on disk (Shared/meetings/<id>.md); the State array keeps a small
|
|
540
|
+
// index (id/agenda/at) so session.json does not carry a second copy of every transcript and
|
|
541
|
+
// rewrite it on EVERY saveAll during long runs (reports below are capped for the same reason).
|
|
542
|
+
meetings.push({id:st.id,agenda:st.agenda,at:now()}); if(meetings.length>200) meetings.shift()
|
|
543
|
+
logDecision('meeting',st.agenda)
|
|
544
|
+
// handle what the meeting produced: task proposals/claims, verify targets, stop vote
|
|
545
|
+
for(const [id,iv] of Object.entries(st.inputs)){
|
|
546
|
+
if(iv.propose_task) await proposeTask(iv.propose_task, iv.task_desc||'', id)
|
|
547
|
+
if(iv.claim_task) await claimTask(iv.claim_task, id)
|
|
548
|
+
if(iv.propose_verify) maybeQueueVerify(iv.propose_verify, id)
|
|
549
|
+
}
|
|
550
|
+
const votes=Object.values(st.inputs).map(x=>x.voteSolved).filter(v=>typeof v==='boolean')
|
|
551
|
+
const allSolved = allSpoke && votes.length>0 && votes.every(v=>v===true)
|
|
552
|
+
logActivity('meeting', 'concluded'+(allSolved?' → ALL agree solved':' (no unanimous solved vote)'))
|
|
553
|
+
if(allSolved){
|
|
554
|
+
running=false; autoDone=true; phase='done'; clearHeartbeat()
|
|
555
|
+
// symmetric with initAbort: the STOP path must also release the coordination state, else
|
|
556
|
+
// status keeps reporting a phantom in-progress meeting (and stale wake kinds) forever.
|
|
557
|
+
meetingState=null; wakeKind.clear(); pendingMeeting=null; verifyState=null; pendingVerify=[]
|
|
558
|
+
logActivity('stop','all residents agree: problem solved'); await saveAll(); return
|
|
559
|
+
}
|
|
560
|
+
meetingState=null; wakeKind.clear(); await saveAll()
|
|
561
|
+
doSchedule=true
|
|
562
|
+
} finally { finalizeLock=null } // release BEFORE scheduling so a chained verify/meeting is not swallowed
|
|
563
|
+
if(doSchedule) await scheduleNext()
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ---- verification (unanimous) ----
|
|
567
|
+
async function beginVerify(pv){
|
|
568
|
+
clearHeartbeat()
|
|
569
|
+
// Re-check dedup at ACTUAL start, not just at propose time: a resident may propose object X while
|
|
570
|
+
// X is already being verified (it does not know). That proposal sits in the pendingVerify queue;
|
|
571
|
+
// when the current X verify closes, beginVerify would run X end-to-end a SECOND time (test9:
|
|
572
|
+
// p-r3-04 was verified twice back-to-back). Drop it if X was closed within the dedup window.
|
|
573
|
+
// (pv was already popped from the FIFO queue by scheduleNext — nothing else to clear here.)
|
|
574
|
+
const tgt=pv&&pv.targetId?String(pv.targetId):''
|
|
575
|
+
if(tgt){
|
|
576
|
+
const last=verifiedRecently.get(tgt)
|
|
577
|
+
if(last!==undefined && (now()-last) < recoverStallMs()){
|
|
578
|
+
logActivity('verify',tgt+' queued verify dropped at start (just verified at '+fmtTime(last)+')')
|
|
579
|
+
await saveAll(); await scheduleNext(); return
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
verifyState={targetId:pv.targetId,targetType:pv.targetType,targetOwner:pv.proposer||'',stage:'independent',round:0,asked:[],verdicts:{},history:{},transcript:[],at:now(),lastVerdictAt:now()}
|
|
583
|
+
markProgress();
|
|
584
|
+
logActivity('verify','debate begin: '+pv.targetId+' ('+pv.targetType+')'); await saveAll(); await scheduleNext()
|
|
585
|
+
}
|
|
586
|
+
async function continueVerifyRound(){
|
|
587
|
+
if(!verifyState) return
|
|
588
|
+
const vs=verifyState
|
|
589
|
+
// STUCK watchdog: a verification that has been active but collected no new verdict for a long
|
|
590
|
+
// while is deadlocked (e.g. an in-flight/hung resident). Abandoning it keeps the object as an
|
|
591
|
+
// unverified probability (no unanimous consensus was reachable) and returns the group to normal
|
|
592
|
+
// scheduling instead of blocking the whole team behind a broken verification.
|
|
593
|
+
if(now()-(vs.lastVerdictAt||vs.at||now())>=recoverStallMs()){
|
|
594
|
+
verifyState=null; wakeKind.clear(); logActivity('verify',vs.targetId+' abandoned (stuck: no unanimous verdict reachable)')
|
|
595
|
+
await saveAll(); await scheduleNext(); return
|
|
596
|
+
}
|
|
597
|
+
const ids=Array.from(residents.keys()); const allVoted=ids.every(id=>vs.verdicts[id]!==undefined)
|
|
598
|
+
if(allVoted){ await finalizeVerify(); return }
|
|
599
|
+
// NOTE: we deliberately do NOT flush mailboxes here — drafting an un-voted resident into a normal
|
|
600
|
+
// mail round would delay its verdict and can starve the verify past its watchdog when the backlog
|
|
601
|
+
// is large. Mail is delivered on scheduleNext passes when no consensus is in progress.
|
|
602
|
+
const id=ids.find(x=>vs.verdicts[x]===undefined && !busy.has(x))
|
|
603
|
+
if(!id){ armHeartbeat(); return } // no idle un-voted resident (a busy/hung one): re-check later
|
|
604
|
+
const r=residents.get(id)
|
|
605
|
+
const ok = await wakeResident(r, verifyPrompt(r,vs), vs.stage==='independent'?'verif-ind':'verif-deb'); await saveAll()
|
|
606
|
+
if(!ok) armHeartbeat() // a failed verify wake must NOT silently hang the verification
|
|
607
|
+
}
|
|
608
|
+
async function finalizeVerify(){
|
|
609
|
+
if(finalizeLock) return // reentry guard (two onResidentEnd may both see allVoted)
|
|
610
|
+
finalizeLock='verify'
|
|
611
|
+
let doSchedule=false
|
|
612
|
+
try {
|
|
613
|
+
const vs=verifyState; const expected=Array.from(residents.keys()).length
|
|
614
|
+
const allVoted = expected>0 && Object.keys(vs.verdicts).length>=expected
|
|
615
|
+
const vals=Object.values(vs.verdicts)
|
|
616
|
+
// verdict is a PURE 0-1 probability; only ALL=1 (true) or ALL=0 (false) is a binary verdict.
|
|
617
|
+
const allTrue = allVoted && vals.every(x=>Number(x.prob)===1)
|
|
618
|
+
const allFalse = allVoted && vals.every(x=>Number(x.prob)===0)
|
|
619
|
+
if(allTrue||allFalse){ await closeVerify(vs,allTrue); doSchedule=true }
|
|
620
|
+
else if(vs.round+1<params.verdictMaxRounds){
|
|
621
|
+
// Move to a REAL debate round: snapshot the current votes into history (so the next round's
|
|
622
|
+
// prompt can show others' previous stances), then CLEAR verdicts so every resident is asked to
|
|
623
|
+
// give a fresh independent judgement after seeing the debate. Without the clear, allVoted stays
|
|
624
|
+
// true and the debate rounds burn through with NOBODY being re-asked (a silent no-op).
|
|
625
|
+
vs.history=Object.assign({}, vs.verdicts); vs.verdicts={}
|
|
626
|
+
vs.lastVerdictAt=now() // fresh deadlock window for the re-vote round
|
|
627
|
+
vs.stage='debate'; vs.round+=1; vs.asked=[]; logActivity('verify',vs.targetId+' round '+vs.round+' → debate (re-vote after seeing others)'); await saveAll(); doSchedule=true
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
const avg=vals.length? vals.reduce((a,x)=>a+(x.prob!=null?x.prob:0.5),0)/vals.length : 0.5
|
|
631
|
+
await writeDebateDoc(vs,false,avg); await rewriteSourceProb(vs.targetId, avg, vs.targetOwner); logActivity('verify',vs.targetId+' NOT unanimous → kept unverified (avg '+avg.toFixed(2)+')')
|
|
632
|
+
verifyState=null; wakeKind.clear(); await saveAll(); doSchedule=true
|
|
633
|
+
}
|
|
634
|
+
} finally { finalizeLock=null } // release BEFORE scheduling (chained verifies must not be swallowed)
|
|
635
|
+
if(doSchedule) await scheduleNext()
|
|
636
|
+
}
|
|
637
|
+
async function closeVerify(vs,isTrue){
|
|
638
|
+
await writeDebateDoc(vs,true,isTrue?1:0)
|
|
639
|
+
const target=vs.targetId
|
|
640
|
+
await writeVerifiedCard(vs,isTrue)
|
|
641
|
+
await rewriteSource(target,isTrue,vs.targetOwner)
|
|
642
|
+
verifiedRecently.set(target, now()) // dedup: block an immediate re-proposal of the same object
|
|
643
|
+
logActivity('verify',target+' → Verified ('+(isTrue?'真':'假')+') by unanimous consensus')
|
|
644
|
+
verifyState=null; wakeKind.clear(); await saveAll()
|
|
645
|
+
// scheduling is done by finalizeVerify AFTER it releases finalizeLock (so a chained verify is
|
|
646
|
+
// never swallowed by the still-held reentry lock)
|
|
647
|
+
}
|
|
648
|
+
// Queue a verify proposal UNLESS the same object was just verified (closed as 真/假). In parallel
|
|
649
|
+
// self-organization several residents may independently propose targets while a verify is already
|
|
650
|
+
// settling — sometimes the SAME object (test9: p-r3-04 was Verified twice back-to-back), sometimes
|
|
651
|
+
// DIFFERENT objects (e.g. a sync meeting where each member proposes its own target). pendingVerify
|
|
652
|
+
// is therefore a FIFO queue with per-target dedup: every distinct proposal is honored in order, and
|
|
653
|
+
// duplicates collapse to one entry. A resident who genuinely extends the object later can still
|
|
654
|
+
// re-propose after the dedup window (recoverStallMs) has passed.
|
|
655
|
+
function maybeQueueVerify(target, proposer){
|
|
656
|
+
const t=idSafe(target) // sanitize BEFORE it becomes file names / dedup keys / status output
|
|
657
|
+
if(!t || t==='id') return false
|
|
658
|
+
const last=verifiedRecently.get(t)
|
|
659
|
+
if(last!==undefined && (now()-last) < recoverStallMs()){
|
|
660
|
+
logActivity('verify',t+' re-propose ignored (just verified at '+fmtTime(last)+')')
|
|
661
|
+
return false
|
|
662
|
+
}
|
|
663
|
+
if(pendingVerify.some(p=>String(p.targetId)===t)) return true // already queued → keep ONE entry
|
|
664
|
+
pendingVerify.push({targetId:t,targetType:guessTargetType(t),proposer:proposer||'',at:now()})
|
|
665
|
+
return true
|
|
666
|
+
}
|
|
667
|
+
async function writeDebateDoc(vs,done,val){
|
|
668
|
+
const lines=['# 验证辩论|'+vs.targetId+'('+vs.targetType+')|'+fmtTime(),'',(done?('**结论**:'+(val===1?'全体一致为真':'全体一致为假')):('**未达成全体一致**,平均概率 '+val.toFixed(2))),'','## 各常驻意见']
|
|
669
|
+
for(const [k,v] of Object.entries(vs.verdicts)){ lines.push('### '+k+'|正确概率 '+(v.prob!=null?Number(v.prob).toFixed(2):'0.50')); lines.push(v.reason||''); lines.push('') }
|
|
670
|
+
await writeText('Shared/debates/'+vs.targetId+'.md', lines.join('\n'))
|
|
671
|
+
}
|
|
672
|
+
async function writeVerifiedCard(vs,isTrue){
|
|
673
|
+
const isSub=vs.targetType==='subproblem'
|
|
674
|
+
const dir= isSub?'问题':'命题'
|
|
675
|
+
const type= isSub?'问题': vs.targetType==='method'?'方法':'命题'
|
|
676
|
+
const text='# 已验证|'+vs.targetId+'\n- ID: '+vs.targetId+'\n- 类型: '+type+'\n- 结论: '+(isTrue?'真':'假')+'\n- 概率: '+(isTrue?1:0)+'\n- 来源: 全体常驻一致\n## 陈述\n参见来源卡。\n'
|
|
677
|
+
await writeText('Verified/'+dir+'/'+vs.targetId+'.md', text)
|
|
678
|
+
}
|
|
679
|
+
// Does `content` declare the target as its card ID? Accept both the exact `- ID: <id>` and the
|
|
680
|
+
// compact single-line form (`- ID: <id>; - 状态: ...`). Residents write cards by hand via fs with
|
|
681
|
+
// varying formats and (crucially) sometimes put a DIFFERENT file name than the declared ID (e.g.
|
|
682
|
+
// Propos/r-3/p-01.md declares "- ID: p-r3-01"). Matching only on the file name then silently loses
|
|
683
|
+
// the verified-status write-back, so we scan candidates' declared ID too.
|
|
684
|
+
function cardDeclaresId(content, target){
|
|
685
|
+
if(!content || !target) return false
|
|
686
|
+
const m=/-\s*ID:\s*([^;\n]+)/.exec(content)
|
|
687
|
+
return !!(m && String(m[1]).trim()===String(target).trim())
|
|
688
|
+
}
|
|
689
|
+
async function findSourceRel(target, owner){
|
|
690
|
+
// 1) exact file name in the owner's library (fast path), then every resident's library
|
|
691
|
+
const order = owner ? [owner, ...Array.from(residents.keys()).filter(k=>k!==owner)] : Array.from(residents.keys())
|
|
692
|
+
for(const rid of order){
|
|
693
|
+
for(const base of ['Propos','Methods','Subproblems']){
|
|
694
|
+
const cand=base+'/'+rid+'/'+target+'.md'
|
|
695
|
+
const t0=await readText(cand); if(t0!==undefined) return cand
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// 2) declared-ID scan: residents sometimes name the file differently from the declared card ID
|
|
699
|
+
// (e.g. p-01.md declares ID p-r3-01). Look inside every card of every library for the target ID.
|
|
700
|
+
try {
|
|
701
|
+
for(const rid of order){
|
|
702
|
+
for(const base of ['Propos','Methods','Subproblems']){
|
|
703
|
+
const dirT=await fs.resolve(base+'/'+rid, {cwd: frameworkRoot()})
|
|
704
|
+
if(await fs.stat(dirT)===undefined) continue
|
|
705
|
+
const entries=await fs.listDir(dirT)
|
|
706
|
+
for(const e of entries||[]){
|
|
707
|
+
if(!e || e.type!=='file' || !/\.md$/.test(String(e.name))) continue
|
|
708
|
+
const c=await readText(base+'/'+rid+'/'+e.name)
|
|
709
|
+
if(c!==undefined && cardDeclaresId(c,target)) return base+'/'+rid+'/'+e.name
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
} catch(e){ /* scanning is best-effort */ }
|
|
714
|
+
return null // NOT 'Propos/'+target+'.md': writing there would create a stray empty card
|
|
715
|
+
}
|
|
716
|
+
// Update the `- 状态:` / `- 概率:` fields of a source card. Residents hand-write cards in two
|
|
717
|
+
// shapes: one field per line, or one line with `; `-separated fields. Accept both by allowing the
|
|
718
|
+
// anchor anywhere on a line and consuming up to the next `;` when fields share the line.
|
|
719
|
+
function rewriteCardField(text, field, newValue){
|
|
720
|
+
if(!text) return text
|
|
721
|
+
const esc=field.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')
|
|
722
|
+
// one-per-line: `- 状态: ...\n` OR inline: `; - 状态: ...;` / `- 状态: ...; - 概率:`
|
|
723
|
+
const re=new RegExp('(^|\\n|;\\s*)-\\s*'+esc+':[^;\\n]*','gm')
|
|
724
|
+
const replaced=text.replace(re,'$1- '+field+': '+newValue)
|
|
725
|
+
return replaced===text ? text : replaced
|
|
726
|
+
}
|
|
727
|
+
// non-unanimous verification: keep the object in its library but write back the
|
|
728
|
+
// average probability (design §8: "留库附概率"), so the card reflects the consensus estimate.
|
|
729
|
+
async function rewriteSourceProb(target,prob,owner){
|
|
730
|
+
const rel=await findSourceRel(target,owner)
|
|
731
|
+
if(!rel){ logActivity('verify',target+' source card NOT found; avg prob '+Number(prob).toFixed(2)+' not written back'); return }
|
|
732
|
+
let text=(await readText(rel))||''
|
|
733
|
+
const next=rewriteCardField(text,'概率',Number(prob).toFixed(2))
|
|
734
|
+
await writeText(rel,next||text)
|
|
735
|
+
}
|
|
736
|
+
async function rewriteSource(target,isTrue,owner){
|
|
737
|
+
// find & update the source card status/prob; best effort across per-resident libs
|
|
738
|
+
const rel=await findSourceRel(target,owner)
|
|
739
|
+
if(!rel){ logActivity('verify',target+' source card NOT found; verified status not written back'); return }
|
|
740
|
+
let text=(await readText(rel))||''
|
|
741
|
+
let next=rewriteCardField(text,'状态',isTrue?'已验证·真':'已验证·假')
|
|
742
|
+
next=rewriteCardField(next,'概率',isTrue?'1':'0')
|
|
743
|
+
await writeText(rel,next||text)
|
|
744
|
+
}
|
|
745
|
+
function guessTargetType(id){ if(/^p-/.test(id)) return 'proposition'; if(/^m-/.test(id)) return 'method'; if(/^s-/.test(id)) return 'subproblem'; return 'proposition' }
|
|
746
|
+
|
|
747
|
+
// ---- heartbeat / liveness helpers (boundary-A: event-driven + gated heartbeat) ----
|
|
748
|
+
// A checkpoint wake is NOT "keep working forever": it nudges the least-recently-active
|
|
749
|
+
// resident, after an idle timeout, to CONTINUE the work itself (self-drive), and only to
|
|
750
|
+
// propose a meeting/verify/solved when it truly has nothing left. This keeps the group moving
|
|
751
|
+
// on its own (framework never assigns work) but adds convergence pressure instead of letting
|
|
752
|
+
// a stalled group sit idle forever.
|
|
753
|
+
function heartbeatPrompt(r){
|
|
754
|
+
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
755
|
+
+'Resident researcher '+r.rId+' — CHECKPOINT(团队空闲,请由你们继续自主推进)。当前项目尚未解决(除非你已确认)。团队在等待有人继续:请**继续解决这个问题**——读他人的库对齐、推进某个子问题/引理/方法、尝试一条路线;或向团队发消息(input)、提议任务(propose_task)让大家分工。若你确实认为问题已解决、或已彻底无路可走,才提议开会(propose_meeting)让团队表决/商量、或声明 solved=true。默认立场是:**请推进,而不是停在原地。**\n'
|
|
756
|
+
+'Reply with ONLY a JSON object:\n'
|
|
757
|
+
+'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<id|null>","contextPct":40}'
|
|
758
|
+
}
|
|
759
|
+
function clearHeartbeat(){ if(heartbeatDisposer!==null){ try{ heartbeatDisposer() }catch(e){} heartbeatDisposer=null } }
|
|
760
|
+
function armHeartbeat(){
|
|
761
|
+
clearHeartbeat()
|
|
762
|
+
const ms=posMs(params.activityTimeoutMs,120000)
|
|
763
|
+
if(typeof ctx.timeout!=='function') return
|
|
764
|
+
heartbeatDisposer=ctx.timeout(()=>{ heartbeatDisposer=null; scheduleNext().catch(()=>{}) }, ms)
|
|
765
|
+
}
|
|
766
|
+
// Real DSH /compact of a resident's OWN session via ctx.compaction (if the host provides it);
|
|
767
|
+
// falling back silently to the resident self-summary directive when the service is absent.
|
|
768
|
+
//
|
|
769
|
+
// WHY THE LIVE-AGENT CACHE EXISTS: `subagent/end` fires AFTER the child's Activation has
|
|
770
|
+
// been torn down. The host's teardown order is
|
|
771
|
+
// dsh-subagent/lib/index.js:1231 await activation.handle.dispose()
|
|
772
|
+
// dsh-agent/lib/index.js:508 this.store.delete(entry.id) <- child leaves the registry
|
|
773
|
+
// dsh-subagent/lib/index.js:1241 activation.observer.settle(...) <- ONLY NOW is subagent/end emitted
|
|
774
|
+
// so `agents.get(childId)` inside an end handler ALWAYS returns undefined. Looking the child
|
|
775
|
+
// up there made this entire path dead code. Instead we capture the live Agent when
|
|
776
|
+
// `subagent/start` fires (the child is still registered then) and hold it in a WeakRef so a
|
|
777
|
+
// resident that is never released cannot pin its Agent forever.
|
|
778
|
+
const liveAgents = new Map() // childId -> WeakRef<Agent>
|
|
779
|
+
function rememberAgent(childId, agent){ if(childId && agent){ try { liveAgents.set(childId, new WeakRef(agent)) } catch(e){ liveAgents.set(childId, { deref:()=>agent }) } } }
|
|
780
|
+
function forgetAgent(childId){ liveAgents.delete(childId) }
|
|
781
|
+
function liveAgentOf(childId){
|
|
782
|
+
const ref=liveAgents.get(childId)
|
|
783
|
+
if(ref){ const a=typeof ref.deref==='function'?ref.deref():undefined; if(a) return a }
|
|
784
|
+
// fallback: a host that keeps the child registered through the end notification
|
|
785
|
+
try { return agents.get(childId) } catch(e){ return undefined }
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Resolve the compaction service a CHILD AGENT should use — i.e. through that
|
|
789
|
+
* agent's own context, not this plugin's.
|
|
790
|
+
*
|
|
791
|
+
* Each preset group declares `isolate: { compaction: true, toolResultPruner: true }`
|
|
792
|
+
* (copied verbatim from DSH's own `standard` preset, whose comment states the realm's
|
|
793
|
+
* purpose: "What a preset chooses is whether its agent compacts at all, which is
|
|
794
|
+
* `compaction-basic` below"). A row sitting OUTSIDE that realm resolves the HOST ROOT
|
|
795
|
+
* instance instead, so this plugin's calls would ignore the preset's own compaction
|
|
796
|
+
* config, and the sub-agent's step-boundary compaction (which runs from inside the
|
|
797
|
+
* realm) would use a different instance than the framework's calls here.
|
|
798
|
+
*
|
|
799
|
+
* `Agent.ctx` is documented as "Agent-scoped context; its contributions are
|
|
800
|
+
* agent-local" (dsh-agent/lib/types/runtime-types.d.ts:148), so the child's own
|
|
801
|
+
* context resolves the preset plane the child actually lives on. This keeps the
|
|
802
|
+
* plugin row where it is (nothing else moves in or out of the realm) while making
|
|
803
|
+
* both compaction paths agree on one instance.
|
|
804
|
+
*/
|
|
805
|
+
function compactionForAgent(agent){
|
|
806
|
+
try { const c = agent && agent.ctx ? agent.ctx.get('compaction') : undefined; if(c && c.compactIfNeeded) return c } catch(e){}
|
|
807
|
+
// fallback: this plugin's own plane (host root for a row outside the realm)
|
|
808
|
+
return compactionOf()
|
|
809
|
+
}
|
|
810
|
+
async function realCompact(r){
|
|
811
|
+
if(!r || !r.childId) return
|
|
812
|
+
const agent = liveAgentOf(r.childId)
|
|
813
|
+
if(!agent || !agent.session) return
|
|
814
|
+
const compaction = compactionForAgent(agent)
|
|
815
|
+
if(compaction===undefined || !compaction.compactIfNeeded) return
|
|
816
|
+
try {
|
|
817
|
+
const signal = makeSignal(params.activityTimeoutMs||60000)
|
|
818
|
+
const result = await compaction.compactIfNeeded(agent, 'pressure', signal)
|
|
819
|
+
if(result && (result.shadowedSeqs||[]).length>0){
|
|
820
|
+
// the resident's real session was compacted → its context is now a summary.
|
|
821
|
+
// Flag needCompact so the NEXT wake re-anchors the core rules (they may have been blurred).
|
|
822
|
+
r.roundsSinceCompact=0; r.needCompact=true; r.contextPct=Math.min(r.contextPct||15,25)
|
|
823
|
+
logActivity('compact', r.rId+' real /compact (shadowed '+result.shadowedSeqs.length+' items, ~'+String(result.shadowedTokenCount||0)+' tokens)')
|
|
824
|
+
}
|
|
825
|
+
} catch(e){ /* real compaction unavailable/failed; the soft directive already covers it */ }
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ---- liveness / scheduling ----
|
|
829
|
+
async function scheduleNext(){
|
|
830
|
+
if(!running||autoDone){ clearHeartbeat(); return }
|
|
831
|
+
if(phase==='brainstorm'){ await maybeFinishBrainstorm(); return }
|
|
832
|
+
if(meetingState){ await continueMeetingRound(); return }
|
|
833
|
+
if(verifyState){ await continueVerifyRound(); return }
|
|
834
|
+
if(pendingVerify.length){ const pv=pendingVerify.shift(); await beginVerify(pv); return }
|
|
835
|
+
// A meeting requested while a verify held the floor is parked in pendingMeeting; once the
|
|
836
|
+
// verify queue has truly drained (no verifyState / pendingVerify), resume it before anything else.
|
|
837
|
+
if(pendingMeeting){ const pm=pendingMeeting; pendingMeeting=null; await startMeeting(pm.agenda, pm.type, pm.targetId); return }
|
|
838
|
+
// mailbox delivery
|
|
839
|
+
const delivered=await deliverNextMailbox(); if(delivered) return
|
|
840
|
+
// maxParallel: don't start a new wake when the in-flight cap is reached
|
|
841
|
+
const mp=Number(params.maxParallel)||0
|
|
842
|
+
if(mp>0 && busy.size>=mp){ armHeartbeat(); return }
|
|
843
|
+
// B) stall auto-sync meeting (分级保活 B): the group has been idle with NO progress for
|
|
844
|
+
// stallAutoMeetingMs → convene a sync meeting so the residents coordinate their next move
|
|
845
|
+
// (framework convenes & records; residents decide — never assigns work). Only when no
|
|
846
|
+
// meeting/verify/pending work is active AND no resident is currently working (so it never
|
|
847
|
+
// preempts an in-flight round).
|
|
848
|
+
if(phase==='active' && !meetingState && !verifyState && pendingVerify.length===0 && busy.size===0){
|
|
849
|
+
const stallMs=posMs(params.stallAutoMeetingMs, posMs(params.activityTimeoutMs,120000)*3)
|
|
850
|
+
if(now()-lastProgressAt>=stallMs){
|
|
851
|
+
await startMeeting('团队较长时间没有新进展。请你们自行讨论:当前问题是否已解决、开放难点是什么、谁负责哪部分、下一步如何推进,并自主决定是否继续。框架只负责转达与记录,不替你们决定。','general',null)
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// A) heartbeat / coordination: wake IDLE residents after an idle timeout to SELF-DRIVE (continue
|
|
856
|
+
// solving / message / propose task / meeting / verify). This is a CONCURRENCY FILL, not a
|
|
857
|
+
// single nudge: scheduleNext should wake up to `maxParallel` idle residents in one pass so the
|
|
858
|
+
// group can progress in parallel (design §A: "同一时刻可唤醒多个空闲常驻,受 maxParallel 上限").
|
|
859
|
+
// On a FAILED wake we re-arm the heartbeat so a single follow-up error NEVER permanently stops
|
|
860
|
+
// the group (a successful wake re-drives scheduleNext through its own onResidentEnd, which re-arms).
|
|
861
|
+
clearHeartbeat()
|
|
862
|
+
const atOs=posMs(params.activityTimeoutMs,120000)
|
|
863
|
+
// `mp` (maxParallel) is already declared above in this function scope.
|
|
864
|
+
// Collect idle (not busy) residents sorted by idle time, oldest-first (round-robin fairness).
|
|
865
|
+
const idleCandidates = Array.from(residents.values())
|
|
866
|
+
.filter(r=>!busy.has(r.rId))
|
|
867
|
+
.sort((a,b)=>(now()-b.lastActiveAt)-(now()-a.lastActiveAt))
|
|
868
|
+
// Fill the concurrency budget: keep waking the most-idle resident until either everyone idle is
|
|
869
|
+
// started OR the in-flight cap (maxParallel) is reached. This turns the previous "one at a time"
|
|
870
|
+
// serialization into genuine parallel progress.
|
|
871
|
+
let started=0
|
|
872
|
+
for(const r of idleCandidates){
|
|
873
|
+
const free = mp>0 ? (mp - busy.size) : Number.MAX_SAFE_INTEGER
|
|
874
|
+
if(free<=0) break // concurrency cap reached → stop filling
|
|
875
|
+
if((now()-r.lastActiveAt)<atOs) break // the remaining are all busy-or-not-idle-enough
|
|
876
|
+
let ok=false
|
|
877
|
+
try { ok = await wakeResident(r, await heartbeatPrompt(r), 'normal') } catch(e){ ok=false }
|
|
878
|
+
if(ok) started++
|
|
879
|
+
await saveAll()
|
|
880
|
+
if(!ok) continue // a failed wake must NOT stop the fill; try the next idle resident
|
|
881
|
+
}
|
|
882
|
+
if(started>0) { armHeartbeat(); return } // started some; re-drive comes via onResidentEnd, BUT also arm a
|
|
883
|
+
// safety-net heartbeat so a woken resident whose subagent/end NEVER arrives (a hung normal round) does not
|
|
884
|
+
// freeze the group. Below mp, the next scheduleNext will re-fill; if all woken residents are stuck busy the
|
|
885
|
+
// heartbeat just re-arms harmlessly. (Meeting/verify already have recoverStallMs watchdogs; normal A-fill did not.)
|
|
886
|
+
// everyone is busy or not idle-enough: arm a heartbeat to re-check later (no infinite spin)
|
|
887
|
+
armHeartbeat()
|
|
888
|
+
}
|
|
889
|
+
async function maybeFinishBrainstorm(){
|
|
890
|
+
const pending=[]; for(const [,r] of residents){ if(r.status==='brainstorm' && !r.insight) pending.push(r.rId) }
|
|
891
|
+
if(pending.length===0){ phase='active'; logActivity('phase','active — residents now self-organize'); await saveBrainstormSummary(); await saveAll(); await scheduleNext() }
|
|
892
|
+
}
|
|
893
|
+
async function saveBrainstormSummary(){
|
|
894
|
+
const lines=['# 头脑风暴','']; for(const [,r] of residents){ if(r.insight){ lines.push('## '+r.rId+'「'+(r.direction||'')+'」'); lines.push(r.insight); lines.push('') } }
|
|
895
|
+
await writeText('Shared/meetings/brainstorm.md', lines.join('\n'))
|
|
896
|
+
}
|
|
897
|
+
async function deliverNextMailbox(){
|
|
898
|
+
// Deliver queued messages to ALL currently-idle recipients in one pass (parallel), bounded by the
|
|
899
|
+
// same maxParallel concurrency cap, so a group chat (relayToGroup → many non-busy recipients) is
|
|
900
|
+
// not serialized one-message-at-a-time. Returns true if anything was delivered. A busy recipient
|
|
901
|
+
// keeps its message queued (avoid starving others).
|
|
902
|
+
let delivered=false
|
|
903
|
+
for(const [to,msgs] of mailboxes){
|
|
904
|
+
if(msgs.length===0) continue
|
|
905
|
+
const r=residents.get(to); if(!r){ mailboxes.delete(to); continue } // stale recipient → drop the entry
|
|
906
|
+
if(busy.has(to)) continue // recipient busy → leave the message queued for a later pass
|
|
907
|
+
const mp=Number(params.maxParallel)||0
|
|
908
|
+
if(mp>0 && busy.size>=mp) break // concurrency cap reached → stop delivering more now
|
|
909
|
+
const m=msgs.shift()
|
|
910
|
+
currentResident=to
|
|
911
|
+
const ok = await wakeResident(r, (await normalPrompt(r))+'\n\n[MESSAGE from '+m.from+']\n'+m.content,'normal')
|
|
912
|
+
await saveAll(); if(!ok) msgs.unshift(m); delivered=delivered||ok
|
|
913
|
+
}
|
|
914
|
+
return delivered
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ---- resident end handler ----
|
|
918
|
+
async function onResidentEnd(childId, info){
|
|
919
|
+
const r=byChild(childId); if(!r) return
|
|
920
|
+
// A turn that is NOT marked busy is a duplicate/stale end (the same subagent/end delivered twice,
|
|
921
|
+
// or an end for a turn already settled). Without this guard every side effect below — task
|
|
922
|
+
// proposal, group relay, verify queueing, meetings.push — would run a SECOND time (the
|
|
923
|
+
// duplicate-task/duplicate-stop class from test9 reappears whenever a host re-delivers an end).
|
|
924
|
+
// Every legitimate end corresponds to a busy turn: busy is added at spawn/wake and cleared only
|
|
925
|
+
// here, on wake failure, on removeMember, or on respawn (whose stale childIds no longer match).
|
|
926
|
+
if(!busy.delete(r.rId)) return
|
|
927
|
+
// Any resident turn that COMPLETED is real activity for the stall clock (B). Residents frequently
|
|
928
|
+
// write their libraries via direct fs (not the record* tools), so relying only on
|
|
929
|
+
// bumpArtifacts/markProgress would leave lastProgressAt stale and B would fire against an active
|
|
930
|
+
// team. We count only a clean 'completed' turn: an error/max-tokens/refusal did NOT meaningfully
|
|
931
|
+
// advance the work, so it must NOT mask a truly stalled group (B can then convene a recovery
|
|
932
|
+
// meeting). A completed turn also refreshes the meeting/verify deadlock clock through lastInputAt.
|
|
933
|
+
if(info && info.stopReason==='completed') markProgress()
|
|
934
|
+
realCompact(r).catch(()=>{}) // best-effort real DSH /compact of this resident while idle
|
|
935
|
+
const output=blocksToText(info&&info.lastAssistantMessage)
|
|
936
|
+
const parsed=parseReply(output)
|
|
937
|
+
postmark(r, parsed) // context/compact bookkeeping, regardless of wake kind (clears any leak)
|
|
938
|
+
const kind=wakeKind.get(r.rId)||'normal'
|
|
939
|
+
if(kind==='meeting' && meetingState){
|
|
940
|
+
meetingState.inputs[r.rId]={input:parsed.input||parsed.summary||'',voteSolved:typeof parsed.voteSolved==='boolean'?parsed.voteSolved:null,propose_verify:parsed.propose_verify||null,propose_task:parsed.propose_task||null,task_desc:parsed.task_desc||'',claim_task:parsed.claim_task||null}
|
|
941
|
+
meetingState.lastInputAt=now()
|
|
942
|
+
if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
|
|
943
|
+
await saveAll()
|
|
944
|
+
// PAUSE/stop: record the in-flight input/verdict but do NOT start any NEW consensus wake —
|
|
945
|
+
// a paused run must stay paused (resume() refreshes the consensus clocks and re-drives).
|
|
946
|
+
if(!running || autoDone) return
|
|
947
|
+
await continueMeetingRound(); return
|
|
948
|
+
}
|
|
949
|
+
if((kind==='verif-ind'||kind==='verif-deb') && verifyState){
|
|
950
|
+
const v=(parsed&&parsed.vote)||{}
|
|
951
|
+
// verdict = 0-1 probability the object is TRUE (1=绝对真, 0=绝对假, 0.5=不确定);
|
|
952
|
+
// also accept legacy 'TRUE'/'FALSE' strings AND quoted numeric strings ("0.9"), which LLMs
|
|
953
|
+
// occasionally emit — without this a confident "0.9" was silently misread as 0.5 (uncertainty).
|
|
954
|
+
let p
|
|
955
|
+
if(typeof v.verdict==='number'){ p=clamp01(v.verdict) }
|
|
956
|
+
else if(/^TRUE$/i.test(String(v.verdict))){ p=1 }
|
|
957
|
+
else if(/^FALSE$/i.test(String(v.verdict))){ p=0 }
|
|
958
|
+
else if(typeof v.verdict==='string' && v.verdict.trim()!=='' && Number.isFinite(Number(v.verdict))){ p=clamp01(Number(v.verdict)) }
|
|
959
|
+
else { p=clamp01(Number(v.confidence)) }
|
|
960
|
+
// verdict is a PURE 0-1 probability (a degree); no binary TRUE/FALSE classification.
|
|
961
|
+
verifyState.verdicts[r.rId]={prob:p,confidence:p,reason:String(v.reason||parsed.summary||'')}
|
|
962
|
+
verifyState.lastVerdictAt=now()
|
|
963
|
+
await saveAll()
|
|
964
|
+
if(!running || autoDone) return // pause: freeze (resume refreshes the clocks and re-drives)
|
|
965
|
+
await continueVerifyRound(); return
|
|
966
|
+
}
|
|
967
|
+
// normal turn
|
|
968
|
+
if(r.status==='brainstorm'){ r.insight=parsed.summary||output; r.status='active' }
|
|
969
|
+
if(typeof parsed.solved==='boolean'){ reports.push({rId:r.rId,solved:parsed.solved,summary:parsed.summary||'',at:now()}); if(reports.length>100) reports.shift() } // solved-signal ring (not surfaced in status/report)
|
|
970
|
+
if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
|
|
971
|
+
// group-conversation relay: the resident may choose to speak to the whole team (input) —
|
|
972
|
+
// forward it to the others so this is a real discussion group, not private monologues.
|
|
973
|
+
if(typeof parsed.input==='string' && parsed.input.trim()) await relayToGroup(r.rId, parsed.input.trim())
|
|
974
|
+
// task actions via reply (a resident may propose or claim a task in its round)
|
|
975
|
+
if(parsed.propose_task) await proposeTask(parsed.propose_task, parsed.task_desc||'', r.rId)
|
|
976
|
+
if(parsed.claim_task) await claimTask(parsed.claim_task, r.rId)
|
|
977
|
+
if(parsed.task_done) await taskDone(parsed.task_done, r.rId)
|
|
978
|
+
// a resident may self-trigger a meeting (resident-driven coordination, closest to the philosophy).
|
|
979
|
+
// If a verify is holding the floor the meeting is deferred (pendingMeeting) and we fall through
|
|
980
|
+
// so the pending verify (or mailbox/heartbeat) still advances rather than being stuck behind it.
|
|
981
|
+
if(parsed.propose_meeting && !meetingState){
|
|
982
|
+
const mr=await startMeeting(String(parsed.propose_meeting),'general',null); await saveAll()
|
|
983
|
+
if(mr && !mr.deferred) return
|
|
984
|
+
}
|
|
985
|
+
await saveAll(); await scheduleNext()
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// ---- controls ----
|
|
989
|
+
async function start({problem,residentCount,seedDirections}){
|
|
990
|
+
await loadSettings()
|
|
991
|
+
currentProject=await readCurrentProject(); if(!currentProject||currentProject==='default'){ currentProject='default'; }
|
|
992
|
+
await ensureDirs()
|
|
993
|
+
if(problem) problemText=String(problem)
|
|
994
|
+
if(!problemText) return {ok:false,message:'problem text required (pass problem, or use vibe_v4_configure first)'}
|
|
995
|
+
problemId=slugify(problemText.slice(0,40))||'problem'
|
|
996
|
+
if(residentCount) params.residentCount=Number(residentCount)||4
|
|
997
|
+
if(!(Number(params.residentCount)>=1)) params.residentCount=DEFAULT_PARAMS.residentCount // a 0/negative count (settings misconfig) would spawn nobody & idle forever
|
|
998
|
+
running=true; autoDone=false; phase='brainstorm'
|
|
999
|
+
await writeText('Problems/'+problemId+'.md','# 问题|'+problemId+'\n- ID: '+problemId+'\n- 类型: 问题\n- 状态: 求解中\n- 优先级: 1\n- 依赖: []\n\n## 陈述\n'+problemText+'\n')
|
|
1000
|
+
// A reused session may still have OLD residents in flight from a previous run (start is a FRESH
|
|
1001
|
+
// run that reuses the same r-1.. library paths). Interrupt them BEFORE resetting, otherwise their
|
|
1002
|
+
// still-running turns keep writing into the same per-resident files the new run is about to use.
|
|
1003
|
+
for(const [,or] of residents){ if(or.childId){ try{ subagents.interrupt(or.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } }
|
|
1004
|
+
residents=new Map(); mailboxes=new Map(); taskboard=[]; decisions=[]; meetings=[]; reports=[]; verifyState=null; meetingState=null; pendingVerify=[]; residentSeq=0; artifactCount=0; clearHeartbeat()
|
|
1005
|
+
busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; lastSyncMeetingAt=0; finalizeLock=null; verifiedRecently.clear() // fresh run must NOT inherit stale concurrency/coordination state (busy/wakeKind/currentResident/pendingMeeting) from a previous run on the same reused session
|
|
1006
|
+
lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
|
|
1007
|
+
const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
|
|
1008
|
+
for(let i=0;i<params.residentCount;i++){ const r=newResident(dirs[i]||''); await spawnResident(r) }
|
|
1009
|
+
await saveAll(); return {ok:true,message:'v4 started: '+params.residentCount+' resident(s) brainstorming',project:currentProject}
|
|
1010
|
+
}
|
|
1011
|
+
async function resume(){
|
|
1012
|
+
currentProject=await readCurrentProject(); await ensureDirs()
|
|
1013
|
+
// Resume is only meaningful for a stopped/paused/crashed run. If THIS process is already driving
|
|
1014
|
+
// a live run whose disk state belongs to it, loadAll below would overwrite the in-memory state
|
|
1015
|
+
// with a slightly stale snapshot (busy marks, wake round counters, mailbox contents from the last
|
|
1016
|
+
// saveAll) — a silent clobber for a useless "kick". No-op instead. A cross-process restart is
|
|
1017
|
+
// always allowed: its disk epoch differs, so the in-memory state is empty/stale anyway.
|
|
1018
|
+
const pre=await readJson('State/session.json')
|
|
1019
|
+
if(running && !autoDone && pre && pre.processEpoch===processEpoch) return {ok:true,message:'already running (no-op)'}
|
|
1020
|
+
await loadAll(); await loadSettings()
|
|
1021
|
+
// A run the group CONCLUDED (unanimous voteSolved → autoDone) must not be silently revived into
|
|
1022
|
+
// a zombie that keeps waking residents with no consensus that it should still run. The group
|
|
1023
|
+
// decided it is done; continuing means a NEW run (vibe_v4_start / vibe_v4_configure).
|
|
1024
|
+
if(autoDone) return {ok:false,message:'This run already concluded (all residents agreed solved). Start a fresh run with vibe_v4_start (vibe_v4_configure a new problem first if needed).'}
|
|
1025
|
+
// After loadAll the residentSeq counter is still whatever THIS process had (0 on a fresh process),
|
|
1026
|
+
// but persisted residents may already be r-1..r-N. Sync it to the max existing id so a later
|
|
1027
|
+
// addMember never collides with an existing resident (it would silently overwrite it).
|
|
1028
|
+
for(const key of residents.keys()){ const mm=/^r-(\d+)$/.exec(String(key)); if(mm) residentSeq=Math.max(residentSeq, Number(mm[1])) }
|
|
1029
|
+
lastActivityAt=now(); lastProgressAt=now() // pause must not count as stall time; a resumed run gets a fresh clock
|
|
1030
|
+
if(phase==='idle' && !running && residents.size===0) return {ok:false,message:'nothing to resume'}
|
|
1031
|
+
// If the persisted State came from a DIFFERENT process (crash/restart), the saved
|
|
1032
|
+
// childIds are stale; clear them so residents re-spawn (their libraries persist on
|
|
1033
|
+
// disk and re-seed the resumed run). Same-process pause→resume keeps continuable ids.
|
|
1034
|
+
const crossProcess = persistedEpoch !== processEpoch
|
|
1035
|
+
if(crossProcess){ for(const [,r] of residents){ r.childId=''; r.status='brainstorm'; r.roundsSinceCompact=0 } }
|
|
1036
|
+
// ANY re-spawn (cross-process OR a same-process abort that already cleared childIds) must get a FRESH
|
|
1037
|
+
// coordination/concurrency state and a brainstorm phase. Otherwise: re-spawned brainstorm residents run
|
|
1038
|
+
// under phase='active' (brainstorm summary never written), and a LATE subagent/end from an interrupted
|
|
1039
|
+
// OLD resident (same rId) deletes the NEW resident's busy mark → A-fill can wake it mid-brainstorm.
|
|
1040
|
+
const needRespawn = Array.from(residents.values()).some(r=>!r.childId)
|
|
1041
|
+
if(needRespawn){
|
|
1042
|
+
for(const [,r] of residents){ r.childId=''; r.status='brainstorm'; r.insight=''; r.roundsSinceCompact=0 }
|
|
1043
|
+
busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; pendingVerify=[]; verifyState=null; meetingState=null; finalizeLock=null; verifiedRecently.clear()
|
|
1044
|
+
}
|
|
1045
|
+
for(const [,r] of residents){ if(!r.childId){ await spawnResident(r) } }
|
|
1046
|
+
if(!running){ running=true; autoDone=false; if(phase==='idle') phase='active' }
|
|
1047
|
+
if(needRespawn && phase!=='brainstorm') phase='brainstorm' // let re-spawned residents re-bootstrap together
|
|
1048
|
+
// A pause froze an in-progress meeting/verify with its watchdog clock still running: refresh the
|
|
1049
|
+
// clocks so a resumed consensus gets a full fresh stall window instead of being abandoned the
|
|
1050
|
+
// instant it is serviced again (a short pause must never silently kill a real discussion).
|
|
1051
|
+
if(meetingState && meetingState.lastInputAt) meetingState.lastInputAt=now()
|
|
1052
|
+
if(verifyState && verifyState.lastVerdictAt) verifyState.lastVerdictAt=now()
|
|
1053
|
+
logActivity('resume','restarted'+(crossProcess?' (cross-process: re-spawned)':needRespawn?' (re-spawned)':'')); await saveAll(); await scheduleNext(); return {ok:true,message:'resumed',project:currentProject}
|
|
1054
|
+
}
|
|
1055
|
+
function status(){ return { ok:true, running, phase, autoDone, project:currentProject, residentCount:residents.size,
|
|
1056
|
+
residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
|
|
1057
|
+
meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null, pendingVerifyCount: pendingVerify.length,
|
|
1058
|
+
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
1059
|
+
params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
|
|
1060
|
+
function report(){ return { ok:true, running, phase, autoDone, project:currentProject, problem:problemText,
|
|
1061
|
+
residents:listResidents(), taskboard:taskboard.filter(t=>t.status!=='done'),
|
|
1062
|
+
meeting: meetingState?{id:meetingState.id, agenda:meetingState.agenda, spoke:Object.keys(meetingState.inputs).length+'/'+residents.size}:null,
|
|
1063
|
+
verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+residents.size}:null,
|
|
1064
|
+
pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
|
|
1065
|
+
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
1066
|
+
meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
|
|
1067
|
+
async function addMember(direction){
|
|
1068
|
+
// Adding a member starts a REAL resident turn (spawnResident → brainstorm) — refuse unless the
|
|
1069
|
+
// run is live: on a concluded (autoDone) or never-started/paused run the new member would work
|
|
1070
|
+
// with nobody to coordinate (zombie work on a project the group already declared done).
|
|
1071
|
+
if(!running || autoDone) return {ok:false,message:'no active run to join (start or resume first)'}
|
|
1072
|
+
const r=newResident(direction||''); await spawnResident(r)
|
|
1073
|
+
// Mid-meeting additions must join the meeting's speaking order; otherwise allSpoke (over CURRENT
|
|
1074
|
+
// residents) can never be true for the new member (not in the snapshot order) and the meeting is
|
|
1075
|
+
// only ever released by the stuck watchdog instead of finalizing with everyone's input.
|
|
1076
|
+
if(meetingState){ if(!Array.isArray(meetingState.order)) meetingState.order=Array.from(residents.keys()); if(!meetingState.order.includes(r.rId)) meetingState.order.push(r.rId) }
|
|
1077
|
+
// Mid-verify additions are automatically asked to vote (continueVerifyRound recomputes ids from
|
|
1078
|
+
// the live residents map), so no extra handling is needed there.
|
|
1079
|
+
return {ok:true,id:r.rId,direction:r.direction} }
|
|
1080
|
+
async function removeMember(id){ const r=residents.get(id); if(!r) return {ok:false}; if(r.childId){ try{ subagents.interrupt(r.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } residents.delete(id); busy.delete(id); mailboxes.delete(id); wakeKind.delete(id); if(currentResident===id) currentResident=''
|
|
1081
|
+
// Reconcile in-progress coordination so a removed member cannot hang consensus or crash a round:
|
|
1082
|
+
// drop its meeting speech / verify verdict and prune it from the meeting's speaking order so the
|
|
1083
|
+
// find() there never selects a ghost. Its QUEUED verify proposals are deliberately KEPT: a
|
|
1084
|
+
// proposal is a statement about an OBJECT the group can judge on its merits with its CURRENT
|
|
1085
|
+
// members (allVoted recomputes over the live residents), and dropping the queue entry would also
|
|
1086
|
+
// erase the intent of any OTHER member who independently proposed the same target (dedup keeps
|
|
1087
|
+
// only the first entry, which may belong to the removed member).
|
|
1088
|
+
if(meetingState){ delete meetingState.inputs[id]; meetingState.order=(meetingState.order||[]).filter(x=>x!==id) }
|
|
1089
|
+
if(verifyState){ delete verifyState.verdicts[id] }
|
|
1090
|
+
await saveAll()
|
|
1091
|
+
// Re-drive the scheduler right away. If the removed member was the ONLY turn in flight (e.g. the
|
|
1092
|
+
// last unspoken meeting speaker / the last unvoted voter, interrupted mid-turn), NO subagent/end
|
|
1093
|
+
// will ever arrive to trigger the next pass, and while a consensus is being serviced no heartbeat
|
|
1094
|
+
// is armed either — without this kick the meeting/verify would freeze forever behind members that
|
|
1095
|
+
// can already conclude. scheduleNext no-ops safely when the run is paused/stopped.
|
|
1096
|
+
await scheduleNext()
|
|
1097
|
+
return {ok:true} }
|
|
1098
|
+
// Normalize one parameter value to its intended type so a string from /v4 set or configure
|
|
1099
|
+
// becomes the right number/array. Keeps settings.json clean regardless of how it was set.
|
|
1100
|
+
function normalizeParam(k, v){
|
|
1101
|
+
const INT_KEYS=['residentCount','compactThreshold','compactAfterRounds','maxParallel','activityTimeoutMs','verdictMaxRounds','meetingKeepEvery','stallAutoMeetingMs']
|
|
1102
|
+
if(INT_KEYS.includes(k)){ const n=Number(v); if(!Number.isFinite(n)) return v; return Math.floor(n) }
|
|
1103
|
+
if(k==='toolAllow'||k==='toolDeny'){ if(Array.isArray(v)) return v.map(x=>String(x).trim()).filter(Boolean); if(typeof v==='string') return v.split(',').map(x=>x.trim()).filter(Boolean); return [] }
|
|
1104
|
+
return v
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* 并发闸门相关参数的下界。
|
|
1108
|
+
*
|
|
1109
|
+
* `maxParallel` 在调度里被当成"0 或负数 = 不限流"(见 scheduleNext 的 `mp>0` 守卫与
|
|
1110
|
+
* `free = mp>0 ? mp-busy.size : MAX_SAFE_INTEGER`),所以用户设成 0 会让闸门**完全失效**、
|
|
1111
|
+
* 一次唤醒全部常驻,与"同时唤醒的常驻上限"语义正好相反 —— 这里抬到最小合法值 1。
|
|
1112
|
+
* `residentCount` 同理(0 会一个常驻都不建)。
|
|
1113
|
+
*
|
|
1114
|
+
* 注意**不要**给 activityTimeoutMs / compactThreshold 之类加下界:文档明确建议测试时把
|
|
1115
|
+
* activityTimeoutMs 设成 40ms 这种小值,钳制它会破坏受支持的配置。
|
|
1116
|
+
*/
|
|
1117
|
+
const INT_MIN = { maxParallel: 1, residentCount: 1 }
|
|
1118
|
+
function clampInt(k, n){
|
|
1119
|
+
const min = INT_MIN[k]
|
|
1120
|
+
if(min===undefined) return n
|
|
1121
|
+
return n < min ? min : n
|
|
1122
|
+
}
|
|
1123
|
+
function setParams(upd){ for(const k of Object.keys(upd||{})){ if(k in params){ const nv=normalizeParam(k, upd[k]); params[k]= (typeof nv==='number') ? clampInt(k, nv) : nv } } saveSettings().catch(()=>{}); return {ok:true} }
|
|
1124
|
+
// ---- create / configure (no auto-start) + settings-file persistence ----
|
|
1125
|
+
async function loadSettings(){ const s=await readJson('State/settings.json'); if(s&&typeof s==='object'){ for(const k of Object.keys(s)){ if(k in params){ const nv=normalizeParam(k, s[k]); params[k]= (typeof nv==='number') ? clampInt(k, nv) : nv } } } }
|
|
1126
|
+
async function saveSettings(){ await writeJson('State/settings.json', params) }
|
|
1127
|
+
// Create/configure a project and set params/problem WITHOUT starting any resident.
|
|
1128
|
+
// The intended flow: vibe_v4_configure {project?, problem?, params?} → vibe_v4_start {}.
|
|
1129
|
+
async function configure(cfg){
|
|
1130
|
+
// configure is the PRE-START setup tool (project/problem/params). Switching the project while a
|
|
1131
|
+
// run is LIVE would split the run's state across two trees: residents' briefs & libraries point
|
|
1132
|
+
// at the OLD frameworkRoot while every subsequent saveAll/transcript/Verified card would go to the
|
|
1133
|
+
// NEW project. Params tuning mid-run belongs to vibe_v4_set.
|
|
1134
|
+
if(running && !autoDone) return {ok:false,message:'cannot configure while a run is running (pause or abort first; use vibe_v4_set to tune params)'}
|
|
1135
|
+
if(cfg && cfg.project && String(cfg.project).trim()) currentProject=String(cfg.project).trim()
|
|
1136
|
+
if(cfg && cfg.problem) problemText=String(cfg.problem)
|
|
1137
|
+
if(cfg && cfg.params && typeof cfg.params==='object') setParams(cfg.params)
|
|
1138
|
+
await writeCurrentProject(); await ensureDirs(); await saveSettings()
|
|
1139
|
+
// create the problem card so the project is complete BEFORE the run starts
|
|
1140
|
+
if(problemText){ const pid=slugify(problemText.slice(0,40))||'problem'; await writeText('Problems/'+pid+'.md','# 问题|'+pid+'\n- ID: '+pid+'\n- 类型: 问题\n- 状态: 求解中\n- 优先级: 1\n- 依赖: []\n\n## 陈述\n'+problemText+'\n') }
|
|
1141
|
+
await saveAll()
|
|
1142
|
+
return {ok:true,project:currentProject,problem:problemText?problemText.slice(0,60):'',params:Object.keys(params).map(k=>k+'='+params[k]).join(', ')}
|
|
1143
|
+
}
|
|
1144
|
+
async function initAbort(){ clearHeartbeat(); running=false; phase='idle'; autoDone=false; for(const [,r] of residents){ if(r.childId){ try{ subagents.interrupt(r.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } r.childId=''; r.lastActiveAt=0; r.roundsSinceCompact=0 }
|
|
1145
|
+
// Wipe the coordination state too: an aborted run must not report an in-flight meeting/verify,
|
|
1146
|
+
// a parked meeting, a verify queue, or busy residents (their childIds are gone, so no end event
|
|
1147
|
+
// can ever clear those marks). resume()/start() re-initialize anyway; this keeps status truthful
|
|
1148
|
+
// between abort and the next action.
|
|
1149
|
+
meetingState=null; verifyState=null; pendingMeeting=null; pendingVerify=[]; busy=new Set(); wakeKind=new Map(); currentResident=''; finalizeLock=null
|
|
1150
|
+
await saveAll(); return {ok:true,message:'aborted'} }
|
|
1151
|
+
function setPause(){ clearHeartbeat(); running=false; return {ok:true,message:'paused'} }
|
|
1152
|
+
|
|
1153
|
+
return {
|
|
1154
|
+
sessionId, running:()=>running, autoDone:()=>autoDone, phase:()=>phase,
|
|
1155
|
+
onResidentEnd, start, resume, status, report, addMember, removeMember, setParams,
|
|
1156
|
+
// live-Agent cache for the real /compact path (see `liveAgents`): the child is
|
|
1157
|
+
// captured at subagent/start and released once its end handler has run.
|
|
1158
|
+
rememberAgent, forgetAgent,
|
|
1159
|
+
setPause, initAbort, postMessage, startMeeting, saveAll, broadcast, configure, loadSettings,
|
|
1160
|
+
currentResident:()=>currentResident,
|
|
1161
|
+
// safety kick: drive one scheduler pass (used when an end handler errored, so an exceptional
|
|
1162
|
+
// turn can never leave the group with no end-event and no heartbeat to continue it)
|
|
1163
|
+
nudge:()=>scheduleNext().catch(()=>{}),
|
|
1164
|
+
residentIdOf:(agent)=>{ const m=residentOfAgent(agent); if(m) return m; const c=currentResident; return (c && residents.has(c)) ? c : '' },
|
|
1165
|
+
useResident:(id)=>{ currentResident=id },
|
|
1166
|
+
publishProgress, recordProposition, recordMethod, recordSubproblem, listResidents, reportContext,
|
|
1167
|
+
proposeTask, claimTask, taskDone, listTasks,
|
|
1168
|
+
readProgress: async (rid)=>({text:(await readText('Progress/'+rid+'/progress.md'))||''}),
|
|
1169
|
+
frameworkRoot:frameworkRoot, currentProject:()=>currentProject, problemText:()=>problemText,
|
|
1170
|
+
residentCount:()=>residents.size,
|
|
1171
|
+
busyCount:()=>busy.size,
|
|
1172
|
+
}
|
|
1173
|
+
} // end makeSession
|
|
1174
|
+
|
|
1175
|
+
// ================= apply-level registration (ONCE) =================
|
|
1176
|
+
function objParams(props, required){ return { type:'object', properties:props, additionalProperties:false, required:required||[] } }
|
|
1177
|
+
function registerTool(name, description, parameters, fn){
|
|
1178
|
+
// tools.register() returns a Cordis effect disposer. Both v2 and v3 keep the
|
|
1179
|
+
// registration inside ctx.effect() so it is wound back when the preset subtree
|
|
1180
|
+
// unloads; v4 used to drop the disposer, so a second mount of this preset in the
|
|
1181
|
+
// same process collided on the already-registered tool names and the entries
|
|
1182
|
+
// survived an unload. Route it through ctx.effect() like the other two.
|
|
1183
|
+
ctx.effect(() => tools.register({ name, description, parameters,
|
|
1184
|
+
output:{ schema:{ type:'string' }, render:(_a,v)=>[{type:'text',text:String(v)}] },
|
|
1185
|
+
execute: async (args, exec)=>{
|
|
1186
|
+
try { const s=getSession(exec&&exec.agent); if(!s) return JSON.stringify({ok:false,error:'no session'}); return JSON.stringify(await fn(s,args||{},exec&&exec.agent)) }
|
|
1187
|
+
catch(e){ return JSON.stringify({ok:false,error:String((e&&e.message)||e)}) }
|
|
1188
|
+
} }))
|
|
1189
|
+
}
|
|
1190
|
+
// host/assistant-facing
|
|
1191
|
+
registerTool('vibe_v4_configure','Create/configure a project: set project name, problem, and params WITHOUT starting a run. Use this FIRST, then vibe_v4_start to actually spawn residents.',objParams({project:{type:'string'},problem:{type:'string'},params:{type:'object'}}),(s,a)=>s.configure(a))
|
|
1192
|
+
registerTool('vibe_v4_start','Start V4: spawn N resident subagents (brainstorm then self-organize).',objParams({problem:{type:'string'},residentCount:{type:'integer'},seedDirections:{type:'array',items:{type:'string'}}}),(s,a)=>s.start(a))
|
|
1193
|
+
registerTool('vibe_v4_resume','Resume a persisted V4 run.',objParams({}),(s)=>s.resume())
|
|
1194
|
+
registerTool('vibe_v4_pause','Pause V4.',objParams({}),(s)=>s.setPause())
|
|
1195
|
+
registerTool('vibe_v4_abort','Abort V4 and interrupt residents.',objParams({}),(s)=>s.initAbort())
|
|
1196
|
+
registerTool('vibe_v4_status','Show V4 status.',objParams({}),(s)=>s.status())
|
|
1197
|
+
registerTool('vibe_v4_report','Return the V4 progress report.',objParams({}),(s)=>s.report())
|
|
1198
|
+
registerTool('vibe_v4_message','Inject a message to a resident (or all).',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a)=>{ const to=a.to||'all'; if(to==='all') return s.broadcast(a.content); return s.postMessage('facilitator',to,a.content) })
|
|
1199
|
+
registerTool('vibe_v4_meeting','Start a meeting (coordinate / allocate / propose verification).',objParams({agenda:{type:'string'}},['agenda']),(s,a)=>s.startMeeting(a.agenda))
|
|
1200
|
+
registerTool('vibe_v4_list_members','List residents.',objParams({}),(s)=>({ok:true,residents:s.listResidents()}))
|
|
1201
|
+
registerTool('vibe_v4_add_member','Add a resident.',objParams({direction:{type:'string'}}),(s,a)=>s.addMember(a.direction))
|
|
1202
|
+
registerTool('vibe_v4_remove_member','Close a resident.',objParams({id:{type:'string'}},['id']),(s,a)=>s.removeMember(a.id))
|
|
1203
|
+
// model/provider inheritance: set model/provider to override the residents' LLM route (''=inherit
|
|
1204
|
+
// the main assistant's route). toolAllow/toolDeny are per-resident tool permissions (scoped
|
|
1205
|
+
// restrict). residentPersona prepends a persona line to every resident prompt.
|
|
1206
|
+
registerTool('vibe_v4_set','Set V4 parameters. model/provider override resident LLM route (empty=inherit main); toolAllow/toolDeny restrict resident tools (arrays of tool names); residentPersona adds a persona line.',objParams({residentCount:{type:'integer'},compactAfterRounds:{type:'integer'},compactThreshold:{type:'integer'},meetingKeepEvery:{type:'integer'},maxParallel:{type:'integer'},activityTimeoutMs:{type:'integer'},verdictMaxRounds:{type:'integer'},stallAutoMeetingMs:{type:'integer'},provider:{type:'string'},model:{type:'string'},residentPersona:{type:'string'},toolAllow:{type:'array',items:{type:'string'}},toolDeny:{type:'array',items:{type:'string'}}}),(s,a)=>{ s.setParams(a); return {ok:true} })
|
|
1207
|
+
// resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
|
|
1208
|
+
// fall back to the last-woken resident when called by the host/assistant.
|
|
1209
|
+
registerTool('vibe_v4_send_message','(resident) Send a message to another resident (to=all broadcasts to the whole team).',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a,x)=>{ const from=s.residentIdOf(x); if(!from) return {ok:false,message:'no such resident'}; if(String(a.to)==='all') return s.broadcast(a.content, from); return s.postMessage(from,a.to,a.content) })
|
|
1210
|
+
registerTool('vibe_v4_publish_progress','(resident) Append to your own progress markdown.',objParams({content:{type:'string'}},['content']),(s,a,x)=>s.publishProgress(s.residentIdOf(x),a.content))
|
|
1211
|
+
registerTool('vibe_v4_record_proposition','(resident) Record a proposition to your library.',objParams({id:{type:'string'},title:{type:'string'},statement:{type:'string'},prob:{type:'number'},value:{type:'number'},motivation:{type:'string'}}),(s,a,x)=>s.recordProposition(s.residentIdOf(x),a))
|
|
1212
|
+
registerTool('vibe_v4_record_method','(resident) Record a method/theory to your library.',objParams({id:{type:'string'},title:{type:'string'},type:{type:'string'},content:{type:'string'},notation:{type:'string'},value:{type:'number'},motivation:{type:'string'}}),(s,a,x)=>s.recordMethod(s.residentIdOf(x),a))
|
|
1213
|
+
registerTool('vibe_v4_record_subproblem','(resident) Record a sub-problem to your library.',objParams({id:{type:'string'},title:{type:'string'},statement:{type:'string'},value:{type:'number'},motivation:{type:'string'}}),(s,a,x)=>s.recordSubproblem(s.residentIdOf(x),a))
|
|
1214
|
+
registerTool('vibe_v4_read_progress','(resident) Read another resident\'s progress (read-only).',objParams({id:{type:'string'}},['id']),async (s,a)=>{ const rp=await s.readProgress(a.id); return {ok:true,text:(rp&&rp.text)||''} })
|
|
1215
|
+
registerTool('vibe_v4_list_residents','(resident) List fellow residents.',objParams({}),(s)=>({ok:true,residents:s.listResidents()}))
|
|
1216
|
+
// task board (residents; board is the residents' own allocation mechanism)
|
|
1217
|
+
registerTool('vibe_v4_propose_task','(resident) Propose a task to the shared task board.',objParams({title:{type:'string'},description:{type:'string'}},['title']),(s,a,x)=>s.proposeTask(a.title,a.description,s.residentIdOf(x)))
|
|
1218
|
+
registerTool('vibe_v4_claim_task','(resident) Claim an open task from the board (framework then wakes you to work it).',objParams({id:{type:'string'}},['id']),(s,a,x)=>s.claimTask(a.id,s.residentIdOf(x)))
|
|
1219
|
+
registerTool('vibe_v4_task_done','(resident) Mark a claimed task done.',objParams({id:{type:'string'},claimer:{type:'string'}},['id']),(s,a,x)=>s.taskDone(a.id,a.claimer||s.residentIdOf(x)))
|
|
1220
|
+
registerTool('vibe_v4_list_tasks','(resident) List open tasks.',objParams({}),(s)=>({ok:true,tasks:s.listTasks()}))
|
|
1221
|
+
// context / compact (resident reports its context usage so the framework can /compact-equivalent)
|
|
1222
|
+
registerTool('vibe_v4_report_context','(resident) Report your context usage %; the framework compacts (self-summary) when it reaches compactThreshold.',objParams({pct:{type:'number'}},['pct']),(s,a,x)=>s.reportContext(s.residentIdOf(x),a.pct))
|
|
1223
|
+
registerTool('vibe_v4_claim_write','Reserved: shared-file write lock (framework-managed).',objParams({target:{type:'string'}},['target']),(s,a)=>({ok:true,key:a.target}))
|
|
1224
|
+
registerTool('vibe_v4_release_write','Reserved: shared-file write lock release.',objParams({target:{type:'string'}},['target']),(s,a)=>({ok:true,key:a.target}))
|
|
1225
|
+
|
|
1226
|
+
// Same lifecycle rule as registerTool: commands.register() returns a disposer, so the
|
|
1227
|
+
// registration belongs to this fiber and must be unwound with it.
|
|
1228
|
+
ctx.effect(() => commands.register({
|
|
1229
|
+
name:'v4', description:'control the Vibe Math V4 framework',
|
|
1230
|
+
input:{hint:'[configure|start|resume|pause|abort|status|report|meeting|members|add|remove|set]'},
|
|
1231
|
+
handler: async function(inv){
|
|
1232
|
+
const s=getSession(inv&&inv.agent); if(!s) return {kind:'success',text:JSON.stringify({ok:false,error:'no session'})}
|
|
1233
|
+
const line=String(inv&&inv.rawInput?inv.rawInput:'').trim(); const parts=line.split(/\s+/); const cmd=parts[0]||''; const rest=parts.slice(1)
|
|
1234
|
+
let r
|
|
1235
|
+
if(cmd==='configure') r=await s.configure({project:rest[0]||'', problem:parts.slice(2).join(' ')})
|
|
1236
|
+
else if(cmd==='start') r=await s.start({})
|
|
1237
|
+
else if(cmd==='resume') r=await s.resume()
|
|
1238
|
+
else if(cmd==='pause') r=s.setPause()
|
|
1239
|
+
else if(cmd==='abort') r=await s.initAbort()
|
|
1240
|
+
else if(cmd==='status') r=s.status()
|
|
1241
|
+
else if(cmd==='report') r=s.report()
|
|
1242
|
+
else if(cmd==='meeting') r=await s.startMeeting(rest.join(' '))
|
|
1243
|
+
else if(cmd==='members') r={ok:true,residents:s.listResidents()}
|
|
1244
|
+
else if(cmd==='add') r=await s.addMember(rest.join(' '))
|
|
1245
|
+
else if(cmd==='remove') r=await s.removeMember(rest[0]||'')
|
|
1246
|
+
else if(cmd==='set'){ const upd={}; for(const tok of rest){ const eq=tok.indexOf('='); if(eq>0){ const k=tok.slice(0,eq); const rv=tok.slice(eq+1); const n=Number(rv); upd[k]=Number.isFinite(n)?n:rv } } r=s.setParams(upd) }
|
|
1247
|
+
else r={ok:false,usage:'configure|start|resume|pause|abort|status|report|message|meeting|members|add|remove|set'}
|
|
1248
|
+
return {kind:'success',text:JSON.stringify(r,null,2)}
|
|
1249
|
+
},
|
|
1250
|
+
}))
|
|
1251
|
+
|
|
1252
|
+
// Capture the live child Agent while it is still registered. `subagent/end` is
|
|
1253
|
+
// emitted only AFTER the child's Activation teardown has removed it from the agent
|
|
1254
|
+
// registry (dsh-subagent:1231 dispose -> dsh-agent:508 store.delete ->
|
|
1255
|
+
// dsh-subagent:1241 settle/emit), so an end-time `agents.get(childId)` can never
|
|
1256
|
+
// resolve. See `liveAgents` in the session body.
|
|
1257
|
+
ctx.on('subagent/start', function(info){
|
|
1258
|
+
if(!info || !info.id) return
|
|
1259
|
+
const sid=childOwner.get(info.id); const s=sid!==undefined?sessions.get(sid):undefined
|
|
1260
|
+
if(!s) return
|
|
1261
|
+
let agent
|
|
1262
|
+
try { agent = agents.get(info.id) } catch(e){ agent = undefined }
|
|
1263
|
+
if(agent) s.rememberAgent(info.id, agent)
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1266
|
+
ctx.on('subagent/end', function(info){
|
|
1267
|
+
const sid=childOwner.get(info.id); const s=sid!==undefined?sessions.get(sid):undefined
|
|
1268
|
+
if(s) s.onResidentEnd(info.id, info)
|
|
1269
|
+
.catch(e=>{ console.error('vibe-v4 end: '+String((e&&e.stack)||e)); if(s.nudge) s.nudge() })
|
|
1270
|
+
// Release the captured live Agent only AFTER this end has been processed, so the
|
|
1271
|
+
// real /compact inside onResidentEnd still sees it. The WeakRef means a missed
|
|
1272
|
+
// release only delays collection rather than leaking the Agent.
|
|
1273
|
+
.finally(()=>{ if(s.forgetAgent) s.forgetAgent(info.id) })
|
|
1274
|
+
})
|
|
1275
|
+
}
|