Consolidate supervision and remember models per role

Bundle Intercom/VCC with the internal supervisor. Add default alignment questions and conversational plan review. Cover model recovery, cancellation and packed single-package operation.
This commit is contained in:
wassname2 committed 2026-09-08 08:45:26 +08:00
1 parent 8e44738773
commit d5729ac106
36 files changed
+8119 -305

No files matched your search

+411
View File
@@ -0,0 +1,411 @@
{"type":"session","version":3,"id":"fixture","timestamp":"2026-01-01T00:00:00.000Z","cwd":"/tmp/fixture"}
{"type":"message","id":"e0000","parentId":null,"timestamp":"2026-01-01T00:00:00.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 0, this text is on every branch"}]}}
{"type":"message","id":"e0001","parentId":"e0000","timestamp":"2026-01-01T00:00:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 0, this text is on every branch"}]}}
{"type":"message","id":"e0002","parentId":"e0001","timestamp":"2026-01-01T00:00:02.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 1, this text is on every branch"}]}}
{"type":"message","id":"e0003","parentId":"e0002","timestamp":"2026-01-01T00:00:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 1, this text is on every branch"}]}}
{"type":"message","id":"e0004","parentId":"e0003","timestamp":"2026-01-01T00:00:04.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 2, this text is on every branch"}]}}
{"type":"message","id":"e0005","parentId":"e0004","timestamp":"2026-01-01T00:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 2, this text is on every branch"}]}}
{"type":"message","id":"e0006","parentId":"e0005","timestamp":"2026-01-01T00:00:06.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 3, this text is on every branch"}]}}
{"type":"message","id":"e0007","parentId":"e0006","timestamp":"2026-01-01T00:00:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 3, this text is on every branch"}]}}
{"type":"message","id":"e0008","parentId":"e0007","timestamp":"2026-01-01T00:00:08.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 4, this text is on every branch"}]}}
{"type":"message","id":"e0009","parentId":"e0008","timestamp":"2026-01-01T00:00:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 4, this text is on every branch"}]}}
{"type":"message","id":"e0010","parentId":"e0009","timestamp":"2026-01-01T00:00:10.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 5, this text is on every branch"}]}}
{"type":"message","id":"e0011","parentId":"e0010","timestamp":"2026-01-01T00:00:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 5, this text is on every branch"}]}}
{"type":"message","id":"e0012","parentId":"e0011","timestamp":"2026-01-01T00:01:40.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 0, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0013","parentId":"e0012","timestamp":"2026-01-01T00:01:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 0, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0014","parentId":"e0013","timestamp":"2026-01-01T00:01:42.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 1, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0015","parentId":"e0014","timestamp":"2026-01-01T00:01:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 1, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0016","parentId":"e0015","timestamp":"2026-01-01T00:01:44.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 2, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0017","parentId":"e0016","timestamp":"2026-01-01T00:01:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 2, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0018","parentId":"e0017","timestamp":"2026-01-01T00:01:46.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 3, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0019","parentId":"e0018","timestamp":"2026-01-01T00:01:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 3, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0020","parentId":"e0019","timestamp":"2026-01-01T00:01:48.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 4, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0021","parentId":"e0020","timestamp":"2026-01-01T00:01:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 4, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0022","parentId":"e0021","timestamp":"2026-01-01T00:01:50.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 5, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0023","parentId":"e0022","timestamp":"2026-01-01T00:01:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 5, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0024","parentId":"e0023","timestamp":"2026-01-01T00:01:52.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 6, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0025","parentId":"e0024","timestamp":"2026-01-01T00:01:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 6, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0026","parentId":"e0025","timestamp":"2026-01-01T00:01:54.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 7, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0027","parentId":"e0026","timestamp":"2026-01-01T00:01:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 7, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0028","parentId":"e0027","timestamp":"2026-01-01T00:01:56.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 8, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0029","parentId":"e0028","timestamp":"2026-01-01T00:01:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 8, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0030","parentId":"e0029","timestamp":"2026-01-01T00:01:58.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 9, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0031","parentId":"e0030","timestamp":"2026-01-01T00:01:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 9, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0032","parentId":"e0031","timestamp":"2026-01-01T00:02:00.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 10, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0033","parentId":"e0032","timestamp":"2026-01-01T00:02:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 10, rewound away and must not appear in a view"}]}}
{"type":"message","id":"e0034","parentId":"e0011","timestamp":"2026-01-01T00:05:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 0, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0035","parentId":"e0034","timestamp":"2026-01-01T00:05:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 0, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0036","parentId":"e0035","timestamp":"2026-01-01T00:05:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 1, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0037","parentId":"e0036","timestamp":"2026-01-01T00:05:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 1, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0038","parentId":"e0037","timestamp":"2026-01-01T00:05:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 2, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0039","parentId":"e0038","timestamp":"2026-01-01T00:05:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 2, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0040","parentId":"e0039","timestamp":"2026-01-01T00:05:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 3, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0041","parentId":"e0040","timestamp":"2026-01-01T00:05:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 3, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0042","parentId":"e0041","timestamp":"2026-01-01T00:05:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 4, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0043","parentId":"e0042","timestamp":"2026-01-01T00:05:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 4, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0044","parentId":"e0043","timestamp":"2026-01-01T00:05:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 5, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0045","parentId":"e0044","timestamp":"2026-01-01T00:05:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 5, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0046","parentId":"e0045","timestamp":"2026-01-01T00:05:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 6, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0047","parentId":"e0046","timestamp":"2026-01-01T00:05:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 6, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0048","parentId":"e0047","timestamp":"2026-01-01T00:05:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 7, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0049","parentId":"e0048","timestamp":"2026-01-01T00:05:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 7, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0050","parentId":"e0049","timestamp":"2026-01-01T00:05:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 8, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0051","parentId":"e0050","timestamp":"2026-01-01T00:05:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 8, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0052","parentId":"e0051","timestamp":"2026-01-01T00:05:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 9, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0053","parentId":"e0052","timestamp":"2026-01-01T00:05:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 9, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0054","parentId":"e0053","timestamp":"2026-01-01T00:05:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 10, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0055","parentId":"e0054","timestamp":"2026-01-01T00:05:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 10, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0056","parentId":"e0055","timestamp":"2026-01-01T00:05:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 11, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0057","parentId":"e0056","timestamp":"2026-01-01T00:05:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 11, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0058","parentId":"e0057","timestamp":"2026-01-01T00:05:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 12, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0059","parentId":"e0058","timestamp":"2026-01-01T00:05:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 12, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0060","parentId":"e0059","timestamp":"2026-01-01T00:05:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 13, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0061","parentId":"e0060","timestamp":"2026-01-01T00:05:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 13, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0062","parentId":"e0061","timestamp":"2026-01-01T00:05:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 14, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0063","parentId":"e0062","timestamp":"2026-01-01T00:05:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 14, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0064","parentId":"e0063","timestamp":"2026-01-01T00:05:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 15, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0065","parentId":"e0064","timestamp":"2026-01-01T00:05:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 15, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0066","parentId":"e0065","timestamp":"2026-01-01T00:05:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 16, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0067","parentId":"e0066","timestamp":"2026-01-01T00:05:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 16, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0068","parentId":"e0067","timestamp":"2026-01-01T00:05:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 17, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0069","parentId":"e0068","timestamp":"2026-01-01T00:05:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 17, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0070","parentId":"e0069","timestamp":"2026-01-01T00:05:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 18, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0071","parentId":"e0070","timestamp":"2026-01-01T00:05:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 18, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0072","parentId":"e0071","timestamp":"2026-01-01T00:05:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 19, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0073","parentId":"e0072","timestamp":"2026-01-01T00:05:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 19, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0074","parentId":"e0073","timestamp":"2026-01-01T00:05:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 20, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0075","parentId":"e0074","timestamp":"2026-01-01T00:05:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 20, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0076","parentId":"e0075","timestamp":"2026-01-01T00:05:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 21, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0077","parentId":"e0076","timestamp":"2026-01-01T00:05:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 21, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0078","parentId":"e0077","timestamp":"2026-01-01T00:05:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 22, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0079","parentId":"e0078","timestamp":"2026-01-01T00:05:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 22, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0080","parentId":"e0079","timestamp":"2026-01-01T00:05:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 23, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0081","parentId":"e0080","timestamp":"2026-01-01T00:05:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 23, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0082","parentId":"e0081","timestamp":"2026-01-01T00:05:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 24, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0083","parentId":"e0082","timestamp":"2026-01-01T00:05:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 24, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0084","parentId":"e0083","timestamp":"2026-01-01T00:05:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 25, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0085","parentId":"e0084","timestamp":"2026-01-01T00:05:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 25, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0086","parentId":"e0085","timestamp":"2026-01-01T00:05:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 26, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0087","parentId":"e0086","timestamp":"2026-01-01T00:05:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 26, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0088","parentId":"e0087","timestamp":"2026-01-01T00:05:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 27, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0089","parentId":"e0088","timestamp":"2026-01-01T00:05:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 27, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0090","parentId":"e0089","timestamp":"2026-01-01T00:05:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 28, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0091","parentId":"e0090","timestamp":"2026-01-01T00:05:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 28, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0092","parentId":"e0091","timestamp":"2026-01-01T00:05:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 29, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0093","parentId":"e0092","timestamp":"2026-01-01T00:05:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 29, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0094","parentId":"e0093","timestamp":"2026-01-01T00:06:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 30, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0095","parentId":"e0094","timestamp":"2026-01-01T00:06:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 30, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0096","parentId":"e0095","timestamp":"2026-01-01T00:06:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 31, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0097","parentId":"e0096","timestamp":"2026-01-01T00:06:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 31, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0098","parentId":"e0097","timestamp":"2026-01-01T00:06:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 32, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0099","parentId":"e0098","timestamp":"2026-01-01T00:06:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 32, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0100","parentId":"e0099","timestamp":"2026-01-01T00:06:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 33, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0101","parentId":"e0100","timestamp":"2026-01-01T00:06:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 33, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0102","parentId":"e0101","timestamp":"2026-01-01T00:06:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 34, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0103","parentId":"e0102","timestamp":"2026-01-01T00:06:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 34, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0104","parentId":"e0103","timestamp":"2026-01-01T00:06:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 35, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0105","parentId":"e0104","timestamp":"2026-01-01T00:06:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 35, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0106","parentId":"e0105","timestamp":"2026-01-01T00:06:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 36, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0107","parentId":"e0106","timestamp":"2026-01-01T00:06:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 36, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0108","parentId":"e0107","timestamp":"2026-01-01T00:06:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 37, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0109","parentId":"e0108","timestamp":"2026-01-01T00:06:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 37, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0110","parentId":"e0109","timestamp":"2026-01-01T00:06:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 38, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0111","parentId":"e0110","timestamp":"2026-01-01T00:06:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 38, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0112","parentId":"e0111","timestamp":"2026-01-01T00:06:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 39, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0113","parentId":"e0112","timestamp":"2026-01-01T00:06:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 39, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0114","parentId":"e0113","timestamp":"2026-01-01T00:06:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 40, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0115","parentId":"e0114","timestamp":"2026-01-01T00:06:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 40, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0116","parentId":"e0115","timestamp":"2026-01-01T00:06:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 41, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0117","parentId":"e0116","timestamp":"2026-01-01T00:06:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 41, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0118","parentId":"e0117","timestamp":"2026-01-01T00:06:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 42, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0119","parentId":"e0118","timestamp":"2026-01-01T00:06:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 42, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0120","parentId":"e0119","timestamp":"2026-01-01T00:06:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 43, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0121","parentId":"e0120","timestamp":"2026-01-01T00:06:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 43, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0122","parentId":"e0121","timestamp":"2026-01-01T00:06:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 44, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0123","parentId":"e0122","timestamp":"2026-01-01T00:06:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 44, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0124","parentId":"e0123","timestamp":"2026-01-01T00:06:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 45, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0125","parentId":"e0124","timestamp":"2026-01-01T00:06:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 45, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0126","parentId":"e0125","timestamp":"2026-01-01T00:06:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 46, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0127","parentId":"e0126","timestamp":"2026-01-01T00:06:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 46, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0128","parentId":"e0127","timestamp":"2026-01-01T00:06:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 47, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0129","parentId":"e0128","timestamp":"2026-01-01T00:06:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 47, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0130","parentId":"e0129","timestamp":"2026-01-01T00:06:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 48, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0131","parentId":"e0130","timestamp":"2026-01-01T00:06:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 48, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0132","parentId":"e0131","timestamp":"2026-01-01T00:06:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 49, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0133","parentId":"e0132","timestamp":"2026-01-01T00:06:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 49, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0134","parentId":"e0133","timestamp":"2026-01-01T00:06:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 50, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0135","parentId":"e0134","timestamp":"2026-01-01T00:06:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 50, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0136","parentId":"e0135","timestamp":"2026-01-01T00:06:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 51, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0137","parentId":"e0136","timestamp":"2026-01-01T00:06:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 51, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0138","parentId":"e0137","timestamp":"2026-01-01T00:06:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 52, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0139","parentId":"e0138","timestamp":"2026-01-01T00:06:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 52, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0140","parentId":"e0139","timestamp":"2026-01-01T00:06:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 53, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0141","parentId":"e0140","timestamp":"2026-01-01T00:06:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 53, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0142","parentId":"e0141","timestamp":"2026-01-01T00:06:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 54, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0143","parentId":"e0142","timestamp":"2026-01-01T00:06:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 54, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0144","parentId":"e0143","timestamp":"2026-01-01T00:06:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 55, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0145","parentId":"e0144","timestamp":"2026-01-01T00:06:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 55, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0146","parentId":"e0145","timestamp":"2026-01-01T00:06:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 56, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0147","parentId":"e0146","timestamp":"2026-01-01T00:06:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 56, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0148","parentId":"e0147","timestamp":"2026-01-01T00:06:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 57, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0149","parentId":"e0148","timestamp":"2026-01-01T00:06:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 57, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0150","parentId":"e0149","timestamp":"2026-01-01T00:06:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 58, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0151","parentId":"e0150","timestamp":"2026-01-01T00:06:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 58, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0152","parentId":"e0151","timestamp":"2026-01-01T00:06:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 59, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0153","parentId":"e0152","timestamp":"2026-01-01T00:06:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 59, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0154","parentId":"e0153","timestamp":"2026-01-01T00:07:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 60, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0155","parentId":"e0154","timestamp":"2026-01-01T00:07:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 60, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0156","parentId":"e0155","timestamp":"2026-01-01T00:07:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 61, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0157","parentId":"e0156","timestamp":"2026-01-01T00:07:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 61, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0158","parentId":"e0157","timestamp":"2026-01-01T00:07:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 62, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0159","parentId":"e0158","timestamp":"2026-01-01T00:07:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 62, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0160","parentId":"e0159","timestamp":"2026-01-01T00:07:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 63, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0161","parentId":"e0160","timestamp":"2026-01-01T00:07:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 63, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0162","parentId":"e0161","timestamp":"2026-01-01T00:07:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 64, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0163","parentId":"e0162","timestamp":"2026-01-01T00:07:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 64, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0164","parentId":"e0163","timestamp":"2026-01-01T00:07:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 65, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0165","parentId":"e0164","timestamp":"2026-01-01T00:07:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 65, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0166","parentId":"e0165","timestamp":"2026-01-01T00:07:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 66, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0167","parentId":"e0166","timestamp":"2026-01-01T00:07:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 66, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0168","parentId":"e0167","timestamp":"2026-01-01T00:07:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 67, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0169","parentId":"e0168","timestamp":"2026-01-01T00:07:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 67, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0170","parentId":"e0169","timestamp":"2026-01-01T00:07:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 68, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0171","parentId":"e0170","timestamp":"2026-01-01T00:07:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 68, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0172","parentId":"e0171","timestamp":"2026-01-01T00:07:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 69, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0173","parentId":"e0172","timestamp":"2026-01-01T00:07:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 69, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0174","parentId":"e0173","timestamp":"2026-01-01T00:07:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 70, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0175","parentId":"e0174","timestamp":"2026-01-01T00:07:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 70, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0176","parentId":"e0175","timestamp":"2026-01-01T00:07:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 71, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0177","parentId":"e0176","timestamp":"2026-01-01T00:07:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 71, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0178","parentId":"e0177","timestamp":"2026-01-01T00:07:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 72, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0179","parentId":"e0178","timestamp":"2026-01-01T00:07:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 72, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0180","parentId":"e0179","timestamp":"2026-01-01T00:07:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 73, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0181","parentId":"e0180","timestamp":"2026-01-01T00:07:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 73, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0182","parentId":"e0181","timestamp":"2026-01-01T00:07:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 74, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0183","parentId":"e0182","timestamp":"2026-01-01T00:07:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 74, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0184","parentId":"e0183","timestamp":"2026-01-01T00:07:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 75, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0185","parentId":"e0184","timestamp":"2026-01-01T00:07:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 75, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0186","parentId":"e0185","timestamp":"2026-01-01T00:07:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 76, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0187","parentId":"e0186","timestamp":"2026-01-01T00:07:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 76, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0188","parentId":"e0187","timestamp":"2026-01-01T00:07:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 77, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0189","parentId":"e0188","timestamp":"2026-01-01T00:07:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 77, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0190","parentId":"e0189","timestamp":"2026-01-01T00:07:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 78, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0191","parentId":"e0190","timestamp":"2026-01-01T00:07:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 78, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0192","parentId":"e0191","timestamp":"2026-01-01T00:07:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 79, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0193","parentId":"e0192","timestamp":"2026-01-01T00:07:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 79, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0194","parentId":"e0193","timestamp":"2026-01-01T00:07:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 80, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0195","parentId":"e0194","timestamp":"2026-01-01T00:07:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 80, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0196","parentId":"e0195","timestamp":"2026-01-01T00:07:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 81, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0197","parentId":"e0196","timestamp":"2026-01-01T00:07:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 81, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0198","parentId":"e0197","timestamp":"2026-01-01T00:07:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 82, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0199","parentId":"e0198","timestamp":"2026-01-01T00:07:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 82, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0200","parentId":"e0199","timestamp":"2026-01-01T00:07:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 83, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0201","parentId":"e0200","timestamp":"2026-01-01T00:07:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 83, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0202","parentId":"e0201","timestamp":"2026-01-01T00:07:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 84, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0203","parentId":"e0202","timestamp":"2026-01-01T00:07:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 84, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0204","parentId":"e0203","timestamp":"2026-01-01T00:07:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 85, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0205","parentId":"e0204","timestamp":"2026-01-01T00:07:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 85, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0206","parentId":"e0205","timestamp":"2026-01-01T00:07:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 86, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0207","parentId":"e0206","timestamp":"2026-01-01T00:07:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 86, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0208","parentId":"e0207","timestamp":"2026-01-01T00:07:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 87, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0209","parentId":"e0208","timestamp":"2026-01-01T00:07:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 87, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0210","parentId":"e0209","timestamp":"2026-01-01T00:07:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 88, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0211","parentId":"e0210","timestamp":"2026-01-01T00:07:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 88, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0212","parentId":"e0211","timestamp":"2026-01-01T00:07:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 89, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0213","parentId":"e0212","timestamp":"2026-01-01T00:07:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 89, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0214","parentId":"e0213","timestamp":"2026-01-01T00:08:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 90, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0215","parentId":"e0214","timestamp":"2026-01-01T00:08:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 90, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0216","parentId":"e0215","timestamp":"2026-01-01T00:08:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 91, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0217","parentId":"e0216","timestamp":"2026-01-01T00:08:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 91, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0218","parentId":"e0217","timestamp":"2026-01-01T00:08:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 92, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0219","parentId":"e0218","timestamp":"2026-01-01T00:08:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 92, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0220","parentId":"e0219","timestamp":"2026-01-01T00:08:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 93, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0221","parentId":"e0220","timestamp":"2026-01-01T00:08:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 93, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0222","parentId":"e0221","timestamp":"2026-01-01T00:08:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 94, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0223","parentId":"e0222","timestamp":"2026-01-01T00:08:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 94, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0224","parentId":"e0223","timestamp":"2026-01-01T00:08:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 95, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0225","parentId":"e0224","timestamp":"2026-01-01T00:08:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 95, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0226","parentId":"e0225","timestamp":"2026-01-01T00:08:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 96, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0227","parentId":"e0226","timestamp":"2026-01-01T00:08:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 96, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0228","parentId":"e0227","timestamp":"2026-01-01T00:08:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 97, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0229","parentId":"e0228","timestamp":"2026-01-01T00:08:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 97, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0230","parentId":"e0229","timestamp":"2026-01-01T00:08:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 98, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0231","parentId":"e0230","timestamp":"2026-01-01T00:08:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 98, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0232","parentId":"e0231","timestamp":"2026-01-01T00:08:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 99, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0233","parentId":"e0232","timestamp":"2026-01-01T00:08:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 99, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0234","parentId":"e0233","timestamp":"2026-01-01T00:08:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 100, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0235","parentId":"e0234","timestamp":"2026-01-01T00:08:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 100, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0236","parentId":"e0235","timestamp":"2026-01-01T00:08:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 101, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0237","parentId":"e0236","timestamp":"2026-01-01T00:08:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 101, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0238","parentId":"e0237","timestamp":"2026-01-01T00:08:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 102, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0239","parentId":"e0238","timestamp":"2026-01-01T00:08:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 102, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0240","parentId":"e0239","timestamp":"2026-01-01T00:08:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 103, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0241","parentId":"e0240","timestamp":"2026-01-01T00:08:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 103, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0242","parentId":"e0241","timestamp":"2026-01-01T00:08:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 104, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0243","parentId":"e0242","timestamp":"2026-01-01T00:08:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 104, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0244","parentId":"e0243","timestamp":"2026-01-01T00:08:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 105, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0245","parentId":"e0244","timestamp":"2026-01-01T00:08:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 105, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0246","parentId":"e0245","timestamp":"2026-01-01T00:08:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 106, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0247","parentId":"e0246","timestamp":"2026-01-01T00:08:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 106, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0248","parentId":"e0247","timestamp":"2026-01-01T00:08:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 107, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0249","parentId":"e0248","timestamp":"2026-01-01T00:08:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 107, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0250","parentId":"e0249","timestamp":"2026-01-01T00:08:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 108, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0251","parentId":"e0250","timestamp":"2026-01-01T00:08:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 108, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0252","parentId":"e0251","timestamp":"2026-01-01T00:08:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 109, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0253","parentId":"e0252","timestamp":"2026-01-01T00:08:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 109, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0254","parentId":"e0253","timestamp":"2026-01-01T00:08:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 110, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0255","parentId":"e0254","timestamp":"2026-01-01T00:08:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 110, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0256","parentId":"e0255","timestamp":"2026-01-01T00:08:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 111, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0257","parentId":"e0256","timestamp":"2026-01-01T00:08:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 111, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0258","parentId":"e0257","timestamp":"2026-01-01T00:08:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 112, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0259","parentId":"e0258","timestamp":"2026-01-01T00:08:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 112, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0260","parentId":"e0259","timestamp":"2026-01-01T00:08:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 113, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0261","parentId":"e0260","timestamp":"2026-01-01T00:08:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 113, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0262","parentId":"e0261","timestamp":"2026-01-01T00:08:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 114, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0263","parentId":"e0262","timestamp":"2026-01-01T00:08:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 114, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0264","parentId":"e0263","timestamp":"2026-01-01T00:08:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 115, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0265","parentId":"e0264","timestamp":"2026-01-01T00:08:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 115, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0266","parentId":"e0265","timestamp":"2026-01-01T00:08:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 116, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0267","parentId":"e0266","timestamp":"2026-01-01T00:08:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 116, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0268","parentId":"e0267","timestamp":"2026-01-01T00:08:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 117, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0269","parentId":"e0268","timestamp":"2026-01-01T00:08:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 117, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0270","parentId":"e0269","timestamp":"2026-01-01T00:08:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 118, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0271","parentId":"e0270","timestamp":"2026-01-01T00:08:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 118, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0272","parentId":"e0271","timestamp":"2026-01-01T00:08:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 119, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0273","parentId":"e0272","timestamp":"2026-01-01T00:08:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 119, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0274","parentId":"e0273","timestamp":"2026-01-01T00:09:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 120, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0275","parentId":"e0274","timestamp":"2026-01-01T00:09:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 120, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0276","parentId":"e0275","timestamp":"2026-01-01T00:09:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 121, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0277","parentId":"e0276","timestamp":"2026-01-01T00:09:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 121, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0278","parentId":"e0277","timestamp":"2026-01-01T00:09:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 122, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0279","parentId":"e0278","timestamp":"2026-01-01T00:09:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 122, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0280","parentId":"e0279","timestamp":"2026-01-01T00:09:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 123, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0281","parentId":"e0280","timestamp":"2026-01-01T00:09:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 123, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0282","parentId":"e0281","timestamp":"2026-01-01T00:09:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 124, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0283","parentId":"e0282","timestamp":"2026-01-01T00:09:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 124, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0284","parentId":"e0283","timestamp":"2026-01-01T00:09:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 125, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0285","parentId":"e0284","timestamp":"2026-01-01T00:09:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 125, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0286","parentId":"e0285","timestamp":"2026-01-01T00:09:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 126, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0287","parentId":"e0286","timestamp":"2026-01-01T00:09:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 126, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0288","parentId":"e0287","timestamp":"2026-01-01T00:09:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 127, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0289","parentId":"e0288","timestamp":"2026-01-01T00:09:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 127, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0290","parentId":"e0289","timestamp":"2026-01-01T00:09:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 128, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0291","parentId":"e0290","timestamp":"2026-01-01T00:09:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 128, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0292","parentId":"e0291","timestamp":"2026-01-01T00:09:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 129, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0293","parentId":"e0292","timestamp":"2026-01-01T00:09:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 129, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0294","parentId":"e0293","timestamp":"2026-01-01T00:09:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 130, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0295","parentId":"e0294","timestamp":"2026-01-01T00:09:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 130, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0296","parentId":"e0295","timestamp":"2026-01-01T00:09:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 131, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0297","parentId":"e0296","timestamp":"2026-01-01T00:09:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 131, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0298","parentId":"e0297","timestamp":"2026-01-01T00:09:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 132, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0299","parentId":"e0298","timestamp":"2026-01-01T00:09:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 132, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0300","parentId":"e0299","timestamp":"2026-01-01T00:09:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 133, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0301","parentId":"e0300","timestamp":"2026-01-01T00:09:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 133, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0302","parentId":"e0301","timestamp":"2026-01-01T00:09:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 134, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0303","parentId":"e0302","timestamp":"2026-01-01T00:09:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 134, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0304","parentId":"e0303","timestamp":"2026-01-01T00:09:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 135, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0305","parentId":"e0304","timestamp":"2026-01-01T00:09:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 135, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0306","parentId":"e0305","timestamp":"2026-01-01T00:09:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 136, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0307","parentId":"e0306","timestamp":"2026-01-01T00:09:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 136, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0308","parentId":"e0307","timestamp":"2026-01-01T00:09:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 137, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0309","parentId":"e0308","timestamp":"2026-01-01T00:09:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 137, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0310","parentId":"e0309","timestamp":"2026-01-01T00:09:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 138, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0311","parentId":"e0310","timestamp":"2026-01-01T00:09:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 138, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0312","parentId":"e0311","timestamp":"2026-01-01T00:09:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 139, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0313","parentId":"e0312","timestamp":"2026-01-01T00:09:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 139, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0314","parentId":"e0313","timestamp":"2026-01-01T00:09:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 140, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0315","parentId":"e0314","timestamp":"2026-01-01T00:09:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 140, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0316","parentId":"e0315","timestamp":"2026-01-01T00:09:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 141, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0317","parentId":"e0316","timestamp":"2026-01-01T00:09:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 141, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0318","parentId":"e0317","timestamp":"2026-01-01T00:09:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 142, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0319","parentId":"e0318","timestamp":"2026-01-01T00:09:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 142, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0320","parentId":"e0319","timestamp":"2026-01-01T00:09:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 143, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0321","parentId":"e0320","timestamp":"2026-01-01T00:09:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 143, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0322","parentId":"e0321","timestamp":"2026-01-01T00:09:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 144, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0323","parentId":"e0322","timestamp":"2026-01-01T00:09:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 144, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0324","parentId":"e0323","timestamp":"2026-01-01T00:09:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 145, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0325","parentId":"e0324","timestamp":"2026-01-01T00:09:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 145, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0326","parentId":"e0325","timestamp":"2026-01-01T00:09:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 146, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0327","parentId":"e0326","timestamp":"2026-01-01T00:09:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 146, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0328","parentId":"e0327","timestamp":"2026-01-01T00:09:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 147, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0329","parentId":"e0328","timestamp":"2026-01-01T00:09:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 147, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0330","parentId":"e0329","timestamp":"2026-01-01T00:09:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 148, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0331","parentId":"e0330","timestamp":"2026-01-01T00:09:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 148, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0332","parentId":"e0331","timestamp":"2026-01-01T00:09:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 149, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0333","parentId":"e0332","timestamp":"2026-01-01T00:09:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 149, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0334","parentId":"e0333","timestamp":"2026-01-01T00:10:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 150, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0335","parentId":"e0334","timestamp":"2026-01-01T00:10:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 150, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0336","parentId":"e0335","timestamp":"2026-01-01T00:10:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 151, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0337","parentId":"e0336","timestamp":"2026-01-01T00:10:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 151, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0338","parentId":"e0337","timestamp":"2026-01-01T00:10:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 152, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0339","parentId":"e0338","timestamp":"2026-01-01T00:10:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 152, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0340","parentId":"e0339","timestamp":"2026-01-01T00:10:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 153, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0341","parentId":"e0340","timestamp":"2026-01-01T00:10:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 153, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0342","parentId":"e0341","timestamp":"2026-01-01T00:10:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 154, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0343","parentId":"e0342","timestamp":"2026-01-01T00:10:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 154, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0344","parentId":"e0343","timestamp":"2026-01-01T00:10:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 155, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0345","parentId":"e0344","timestamp":"2026-01-01T00:10:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 155, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0346","parentId":"e0345","timestamp":"2026-01-01T00:10:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 156, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0347","parentId":"e0346","timestamp":"2026-01-01T00:10:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 156, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0348","parentId":"e0347","timestamp":"2026-01-01T00:10:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 157, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0349","parentId":"e0348","timestamp":"2026-01-01T00:10:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 157, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0350","parentId":"e0349","timestamp":"2026-01-01T00:10:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 158, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0351","parentId":"e0350","timestamp":"2026-01-01T00:10:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 158, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0352","parentId":"e0351","timestamp":"2026-01-01T00:10:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 159, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0353","parentId":"e0352","timestamp":"2026-01-01T00:10:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 159, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0354","parentId":"e0353","timestamp":"2026-01-01T00:10:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 160, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0355","parentId":"e0354","timestamp":"2026-01-01T00:10:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 160, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0356","parentId":"e0355","timestamp":"2026-01-01T00:10:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 161, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0357","parentId":"e0356","timestamp":"2026-01-01T00:10:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 161, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0358","parentId":"e0357","timestamp":"2026-01-01T00:10:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 162, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0359","parentId":"e0358","timestamp":"2026-01-01T00:10:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 162, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0360","parentId":"e0359","timestamp":"2026-01-01T00:10:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 163, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0361","parentId":"e0360","timestamp":"2026-01-01T00:10:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 163, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0362","parentId":"e0361","timestamp":"2026-01-01T00:10:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 164, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0363","parentId":"e0362","timestamp":"2026-01-01T00:10:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 164, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0364","parentId":"e0363","timestamp":"2026-01-01T00:10:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 165, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0365","parentId":"e0364","timestamp":"2026-01-01T00:10:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 165, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0366","parentId":"e0365","timestamp":"2026-01-01T00:10:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 166, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0367","parentId":"e0366","timestamp":"2026-01-01T00:10:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 166, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0368","parentId":"e0367","timestamp":"2026-01-01T00:10:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 167, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0369","parentId":"e0368","timestamp":"2026-01-01T00:10:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 167, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0370","parentId":"e0369","timestamp":"2026-01-01T00:10:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 168, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0371","parentId":"e0370","timestamp":"2026-01-01T00:10:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 168, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0372","parentId":"e0371","timestamp":"2026-01-01T00:10:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 169, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0373","parentId":"e0372","timestamp":"2026-01-01T00:10:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 169, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0374","parentId":"e0373","timestamp":"2026-01-01T00:10:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 170, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0375","parentId":"e0374","timestamp":"2026-01-01T00:10:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 170, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0376","parentId":"e0375","timestamp":"2026-01-01T00:10:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 171, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0377","parentId":"e0376","timestamp":"2026-01-01T00:10:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 171, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0378","parentId":"e0377","timestamp":"2026-01-01T00:10:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 172, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0379","parentId":"e0378","timestamp":"2026-01-01T00:10:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 172, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0380","parentId":"e0379","timestamp":"2026-01-01T00:10:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 173, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0381","parentId":"e0380","timestamp":"2026-01-01T00:10:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 173, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0382","parentId":"e0381","timestamp":"2026-01-01T00:10:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 174, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0383","parentId":"e0382","timestamp":"2026-01-01T00:10:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 174, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0384","parentId":"e0383","timestamp":"2026-01-01T00:10:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 175, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0385","parentId":"e0384","timestamp":"2026-01-01T00:10:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 175, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0386","parentId":"e0385","timestamp":"2026-01-01T00:10:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 176, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0387","parentId":"e0386","timestamp":"2026-01-01T00:10:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 176, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0388","parentId":"e0387","timestamp":"2026-01-01T00:10:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 177, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0389","parentId":"e0388","timestamp":"2026-01-01T00:10:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 177, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0390","parentId":"e0389","timestamp":"2026-01-01T00:10:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 178, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0391","parentId":"e0390","timestamp":"2026-01-01T00:10:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 178, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0392","parentId":"e0391","timestamp":"2026-01-01T00:10:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 179, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0393","parentId":"e0392","timestamp":"2026-01-01T00:10:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 179, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0394","parentId":"e0393","timestamp":"2026-01-01T00:11:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 180, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0395","parentId":"e0394","timestamp":"2026-01-01T00:11:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 180, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0396","parentId":"e0395","timestamp":"2026-01-01T00:11:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 181, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0397","parentId":"e0396","timestamp":"2026-01-01T00:11:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 181, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0398","parentId":"e0397","timestamp":"2026-01-01T00:11:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 182, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0399","parentId":"e0398","timestamp":"2026-01-01T00:11:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 182, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0400","parentId":"e0399","timestamp":"2026-01-01T00:11:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 183, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0401","parentId":"e0400","timestamp":"2026-01-01T00:11:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 183, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0402","parentId":"e0401","timestamp":"2026-01-01T00:11:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 184, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0403","parentId":"e0402","timestamp":"2026-01-01T00:11:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 184, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0404","parentId":"e0403","timestamp":"2026-01-01T00:11:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 185, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0405","parentId":"e0404","timestamp":"2026-01-01T00:11:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 185, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0406","parentId":"e0405","timestamp":"2026-01-01T00:11:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 186, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0407","parentId":"e0406","timestamp":"2026-01-01T00:11:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 186, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0408","parentId":"e0407","timestamp":"2026-01-01T00:11:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 187, this is the branch the supervisor should read"}]}}
{"type":"message","id":"e0409","parentId":"e0408","timestamp":"2026-01-01T00:11:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 187, this is the branch the supervisor should read"}]}}
+10
View File
@@ -0,0 +1,10 @@
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { type ModelRole, RoleModels } from "../../src/role-models.js";
const [directory, role, provider, id] = process.argv.slice(2);
let selected: (event: any, ctx: any) => void;
const pi = { on(_name: string, handler: any) { selected = handler; }, setModel: async () => true };
const ctx = { model: { provider, id }, modelRegistry: { find: (provider: string, id: string) => ({ provider, id }) }, ui: { notify(message: string) { throw new Error(message); } } };
const models = new RoleModels(pi as unknown as ExtensionAPI, directory);
await models.enter(role as ModelRole, ctx as unknown as ExtensionContext);
selected!({ source: "cycle", model: { provider, id } }, ctx);
+166 -38
View File
@@ -5,6 +5,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { describe, expect, it, vi } from "vitest";
import piGoalsExtension from "../src/index.js";
vi.mock("../src/internal/supervisor/index.js", () => ({ default: () => {} }));
function setup(
selectChoices: Array<string | undefined>,
editorChoices: Array<string | undefined> = [],
@@ -31,6 +33,8 @@ function setup(
};
const ctx = {
cwd,
model: { provider: "offline", id: "test" },
modelRegistry: { find: (provider: string, id: string) => ({ provider, id }) },
hasUI: true,
isIdle: () => true,
sessionManager: { getSessionId: () => "session-a", getSessionFile: () => join(cwd, "session-a.jsonl"), getEntries: () => entries, getBranch: () => entries },
@@ -38,7 +42,7 @@ function setup(
theme: { fg: (_kind: string, text: string) => text },
setStatus: () => {},
setWidget: () => {},
notify: () => {},
notify: vi.fn(),
select: async () => {
events.push("select");
return selectChoices.shift();
@@ -49,9 +53,13 @@ function setup(
},
},
};
let active = ["read", "write", "edit", "bash", "CompleteGoal", "RequestPlanReview"];
const pi = {
getActiveTools: () => active,
setActiveTools: (names: string[]) => { active = names; },
setModel: vi.fn(async (model: { provider: string; id: string }) => { const previousModel = ctx.model; ctx.model = model; await hooks.get("model_select")?.({ model, previousModel, source: "set" }, ctx); return true; }),
registerCommand: (name: string, command: any) => commands.set(name, command),
on: (name: string, handler: any) => hooks.set(name, handler),
on: (name: string, handler: any) => { const previous = hooks.get(name); hooks.set(name, async (...args: any[]) => { const prior = await previous?.(...args); return await handler(...args) ?? prior; }); },
appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }),
registerTool: (tool: any) => tools.set(tool.name, tool),
events: bus,
@@ -62,7 +70,12 @@ function setup(
sendUserMessage: (message: string) => messages.push({ content: message }),
};
piGoalsExtension(pi as unknown as ExtensionAPI);
return { bus, commands, ctx, cwd, entries, events, hooks, messages, tools };
return { pi, bus, commands, ctx, cwd, entries, events, hooks, messages, tools };
}
async function settleDraft(flow: ReturnType<typeof setup>) {
await flow.tools.get("RequestPlanReview").execute("", {}, undefined, undefined, flow.ctx);
await flow.hooks.get("agent_settled")({}, flow.ctx);
}
describe("/goals draft flow", () => {
@@ -120,14 +133,14 @@ describe("/goals draft flow", () => {
await flow.hooks.get("session_start")({}, flow.ctx);
await flow.commands.get("goals").handler("objective", flow.ctx);
writeFileSync(join(flow.cwd, ".pi/plan/session-a-v1.md"), "# Plan\n\n1. [ ] goal: make the file\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", stewardEnabled: true });
expect(flow.messages.some(message => message.content.startsWith("Work the goals"))).toBe(false);
} finally { vi.unstubAllEnvs(); rmSync(flow.cwd, { recursive: true, force: true }); }
});
it("preserves prior drafts, displays the plan before Refine, and records editor notes", async () => {
const flow = setup(["Refine"], ["Keep two columns.\nDo not add a filter."]);
it("preserves prior drafts, displays the plan before Discuss, and records chat answers", async () => {
const flow = setup(["Discuss"]);
try {
const legacy = join(flow.cwd, ".pi/plan/session-a.md");
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
@@ -141,9 +154,10 @@ describe("/goals draft flow", () => {
writeFileSync(v1, plan);
await flow.hooks.get("input")({ text: "The result must preserve column order.", source: "interactive" }, flow.ctx);
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.events).toEqual(["display", "select", "editor"]);
expect(flow.messages.at(-1)?.content).toContain("Revise the plan at");
await settleDraft(flow);
expect(flow.events).toEqual(["display", "select"]);
expect(flow.messages.at(-1)?.content).toContain("normal chat");
await flow.hooks.get("input")({ text: "Keep two columns.\nDo not add a filter.", source: "interactive" }, flow.ctx);
expect(flow.messages.find((message) => message.display)?.content).toContain("goal: preserve this");
const interviewedPlan = readFileSync(v1, "utf-8");
expect(interviewedPlan).toContain("> The result must preserve column order.");
@@ -188,27 +202,29 @@ describe("/goals draft flow", () => {
}
});
it("waits for Refine notes before starting a revision turn", async () => {
let submitNotes: (notes: string) => void;
const flow = setup(["Refine"], [], () => new Promise((resolve) => {
submitNotes = resolve;
}));
it("Discuss allows multiple chat turns, survives reload, and reopens review for an unchanged draft", async () => {
const flow = setup(["Discuss", "Ready"]);
try {
await flow.commands.get("goals").handler("steward off", flow.ctx);
await flow.commands.get("goals").handler("objective", flow.ctx);
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: make this specific\n");
const review = flow.hooks.get("agent_settled")({}, flow.ctx);
await new Promise((resolve) => setImmediate(resolve));
expect(flow.events).toEqual(["display", "select", "editor"]);
expect(flow.messages.filter((message) => !message.display)).toHaveLength(1);
submitNotes!("Name the output artifact.");
await review;
expect(flow.messages.at(-1)?.content).toContain("Revise the plan at");
} finally {
rmSync(flow.cwd, { recursive: true, force: true });
}
const path = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(path, "# Plan\n\n1. [ ] goal: make this specific\n");
await settleDraft(flow);
expect(flow.events).toEqual(["display", "select"]);
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", reviewRequested: false });
await flow.hooks.get("agent_settled")({}, flow.ctx); // question turn, not another menu
await flow.hooks.get("input")({ text: "Only the existing output; no new UI.", source: "interactive" }, flow.ctx);
await flow.hooks.get("agent_settled")({}, flow.ctx);
await flow.hooks.get("session_start")({ reason: "reload" }, flow.ctx);
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.events).toEqual(["display", "select"]);
const unchanged = readFileSync(path, "utf8");
await settleDraft(flow);
expect(readFileSync(path, "utf8")).toBe(unchanged);
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.events).toEqual(["display", "select", "display", "select"]);
expect(flow.messages.filter(m => m.content.startsWith("Work the goals"))).toHaveLength(1);
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
});
it("starts work only when the human chooses Ready", async () => {
@@ -219,7 +235,7 @@ describe("/goals draft flow", () => {
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: work on this\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
expect(flow.events).toEqual(["display", "select"]);
expect(flow.messages.filter((message) => !message.display)).toHaveLength(2);
@@ -239,7 +255,7 @@ describe("/goals draft flow", () => {
await flow.commands.get("goals").handler("objective", flow.ctx);
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: make the file\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
await flow.commands.get("goals").handler("steward on", flow.ctx);
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", stewardEnabled: false, supervisor: null });
} finally {
@@ -256,7 +272,7 @@ describe("/goals draft flow", () => {
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, original);
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
expect(flow.events).toEqual(["display", "select", "editor", "display", "select"]);
expect(() => readFileSync(planPath, "utf-8")).toThrow();
@@ -273,7 +289,7 @@ describe("/goals draft flow", () => {
await flow.commands.get("goals").handler("objective", flow.ctx);
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n\n## Log\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
await flow.hooks.get("turn_end")({}, flow.ctx);
for (let turn = 0; turn < 3; turn++) await flow.hooks.get("turn_end")({}, flow.ctx);
@@ -302,18 +318,18 @@ describe("/goals draft flow", () => {
await flow.commands.get("goals").handler("objective", flow.ctx);
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
await flow.commands.get("goals").handler("auto 1", flow.ctx);
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
await vi.advanceTimersByTimeAsync(0);
const autoMessages = () => flow.messages.filter((message) => message.content.includes("Auto-continue is enabled"));
expect(autoMessages()).toHaveLength(1);
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
await vi.advanceTimersByTimeAsync(60_000);
expect(autoMessages()).toHaveLength(2);
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
await vi.advanceTimersByTimeAsync(60_000);
expect(autoMessages()).toHaveLength(2);
} finally {
@@ -330,11 +346,11 @@ describe("/goals draft flow", () => {
await flow.commands.get("goals").handler("objective", flow.ctx);
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
await flow.commands.get("goals").handler("auto 1", flow.ctx);
await flow.hooks.get("agent_start")({}, flow.ctx);
await flow.hooks.get("tool_call")({ toolName: "process", input: { action: "start" } }, flow.ctx);
await flow.hooks.get("agent_settled")({}, flow.ctx);
await settleDraft(flow);
await vi.advanceTimersByTimeAsync(0);
const autoMessages = () => flow.messages.filter((message) => message.content.includes("Auto-continue is enabled"));
expect(autoMessages()).toHaveLength(0);
@@ -380,3 +396,115 @@ describe("/goals draft flow", () => {
}
});
});
describe("role models across goal lifecycle", () => {
const choice = (role: string) => ({ provider: `${role}-provider`, id: `${role}-model` });
const select = async (flow: ReturnType<typeof setup>, role: string) => {
flow.ctx.model = choice(role);
await flow.hooks.get("model_select")({ source: "cycle", model: flow.ctx.model }, flow.ctx);
};
const draft = (flow: ReturnType<typeof setup>, version = 1) => writeFileSync(join(flow.cwd, `.pi/plan/session-a-v${version}.md`), "# Plan\n\n1. [ ] goal: produce the artifact\n");
it("restores each role at Ready, fresh-instance reload/resume and the next plan without changing judge override", async () => {
const flow = setup(["Ready"]); const fresh = setup([]);
try {
await flow.commands.get("goals").handler("steward off", flow.ctx);
await flow.commands.get("goals").handler("first", flow.ctx); await select(flow, "planning"); draft(flow);
await settleDraft(flow); await select(flow, "worker");
await flow.commands.get("goals").handler("judge isolated/judge", flow.ctx);
fresh.entries.push(...structuredClone(flow.entries));
await fresh.hooks.get("session_start")({ reason: "resume" }, fresh.ctx);
expect(fresh.ctx.model).toEqual(choice("worker"));
await fresh.commands.get("goals").handler("second", fresh.ctx);
expect(fresh.ctx.model).toEqual(choice("planning"));
expect(fresh.entries.at(-1)?.data).toMatchObject({ phase: "planning", judgeModel: "isolated/judge" });
await fresh.hooks.get("session_start")({ reason: "reload" }, fresh.ctx);
expect(fresh.ctx.model).toEqual(choice("planning"));
} finally { for (const f of [flow, fresh]) { await f.hooks.get("session_shutdown")({}, f.ctx); rmSync(f.cwd, { recursive: true, force: true }); } }
});
it("failed Ready and explicit cancellation never save a worker choice over planning", async () => {
const flow = setup(["Ready", "Cancel"]); vi.stubEnv("HERDR_ENV", "");
try {
await flow.commands.get("goals").handler("objective", flow.ctx); await select(flow, "planning"); draft(flow);
await settleDraft(flow); expect(flow.ctx.model).toEqual(choice("planning"));
expect(flow.messages.some(m => m.content.startsWith("Work the goals"))).toBe(false);
await settleDraft(flow);
await select(flow, "outside-plan");
await flow.commands.get("goals").handler("next plan", flow.ctx);
expect(flow.ctx.model).toEqual(choice("planning"));
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
});
it("unavailable worker model blocks Ready until an explicit selection, without changing its remembered provider", async () => {
const flow = setup(["Ready", "Ready"]);
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals"); mkdirSync(dir);
writeFileSync(join(dir, "worker-model.json"), JSON.stringify(choice("worker")));
try {
await flow.commands.get("goals").handler("steward off", flow.ctx);
await flow.commands.get("goals").handler("objective", flow.ctx); draft(flow);
flow.pi.setModel.mockResolvedValueOnce(false);
await settleDraft(flow);
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning" });
expect(flow.messages.some(m => m.content.startsWith("Work the goals"))).toBe(false);
expect(flow.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("has no authentication"), "error");
expect(JSON.parse(readFileSync(join(dir, "worker-model.json"), "utf8"))).toEqual(choice("worker"));
await select(flow, "replacement-worker"); await settleDraft(flow);
expect(flow.ctx.model).toEqual(choice("replacement-worker"));
expect(flow.messages.filter(m => m.content.startsWith("Work the goals"))).toHaveLength(1);
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
});
it("supervisor resume uses its own preference and never restores inherited worker automation/tools", async () => {
const flow = setup([]);
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals"); mkdirSync(dir);
writeFileSync(join(dir, "supervisor-model.json"), JSON.stringify(choice("supervisor")));
try {
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, autoIntervalMs: 1, stewardEnabled: false } });
flow.entries.push({ type: "custom", customType: "pi-goals-supervisor", data: { binding: { id: "binding" }, workerId: "worker" } });
await flow.hooks.get("session_start")({ reason: "resume" }, flow.ctx);
expect(flow.ctx.model).toEqual(choice("supervisor"));
expect(flow.pi.getActiveTools()).not.toContain("CompleteGoal");
expect(flow.pi.getActiveTools()).not.toContain("RequestPlanReview");
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.messages).toEqual([]);
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
});
it("waives alignment only for the current plan and Escape returns to chat without deleting the draft", async () => {
const flow = setup([undefined]);
try {
await flow.commands.get("goals").handler("first; no questions", flow.ctx);
expect(flow.entries.at(-1)?.data).toMatchObject({ questionsWaived: true });
expect(flow.messages.at(-1)?.content).toContain("THIS plan only");
await flow.commands.get("goals").handler("second", flow.ctx);
expect(flow.entries.at(-1)?.data).toMatchObject({ questionsWaived: false, reviewRequested: false });
expect(flow.messages.at(-1)?.content).toContain("previous plan does NOT apply");
draft(flow, 2);
await flow.hooks.get("agent_settled")({}, flow.ctx); expect(flow.events).toEqual([]);
await settleDraft(flow);
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", reviewRequested: false });
expect(readFileSync(join(flow.cwd, ".pi/plan/session-a-v2.md"), "utf8")).toContain("produce the artifact");
expect(flow.events).toEqual(["display", "select"]);
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
});
});
it("cancels an ordinary Ready handoff suspended in setModel without losing planning preference", async () => {
const flow = setup(["Ready"]);
try {
await flow.commands.get("goals").handler("steward off", flow.ctx);
await flow.commands.get("goals").handler("objective", flow.ctx);
writeFileSync(join(flow.cwd, ".pi/plan/session-a-v1.md"), "# Plan\n\n1. [ ] goal: produce output\n");
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
writeFileSync(join(dir, "worker-model.json"), JSON.stringify({ provider: "worker", id: "work" }));
let finish!: () => void;
flow.pi.setModel.mockImplementationOnce(async model => { await new Promise<void>(resolve => { finish = resolve; }); flow.ctx.model = model; return true; });
const starting = settleDraft(flow);
await vi.waitFor(() => expect(finish).toBeTypeOf("function"));
await flow.commands.get("goals").handler("clear", flow.ctx);
finish(); await starting;
expect(flow.ctx.model).toEqual({ provider: "offline", id: "test" });
expect(flow.messages.some(m => m.content.startsWith("Work the goals"))).toBe(false);
expect(JSON.parse(readFileSync(join(dir, "worker-model.json"), "utf8"))).toEqual({ provider: "worker", id: "work" });
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
});
File diff suppressed because it is too large. Load diff
+328
View File
@@ -0,0 +1,328 @@
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { backgroundState } from "../../src/internal/supervisor/background.js";
import extension from "../../src/internal/supervisor/index.js";
import { PLAN_API_EVENT, type PlanBinding, planHash } from "../../src/internal/supervisor/plan-api.js";
const tick = () => new Promise(resolve => setImmediate(resolve));
function pairHarness() {
const cwd = mkdtempSync(join(tmpdir(), "supervise-plan-"));
const peers: any[] = [];
const wire: any[] = [];
const planPath = join(cwd, "plan.md");
writeFileSync(planPath, '# Plan\n\nUser voice: render the literal "[x]"\n\n1. [ ] goal: first\n2. [ ] goal: second\n');
const binding: PlanBinding = { id: "binding", planPath, workerSession: join(cwd, "worker.jsonl"), workerPane: "w1:p1", supervisorSession: join(cwd, "supervisor.jsonl"), supervisorPane: "w1:p2", everyTurns: 50, intervalMs: 3_600_000, compactTokens: 100_000 };
function make(id: string, entries: any[] = [], sessionFile = join(cwd, `${id}.jsonl`)) {
const hooks = new Map<string, any[]>(); const listeners = new Map<string, Set<any>>(); const tools = new Map<string, any>();
const messages: any[] = []; const contexts: any[] = []; const commands = new Map<string, any>();
let active: string[] = ["read", "grep", "bash", "edit", "write"];
const peer: any = { id, messages, contexts, entries, tools, commands, modelReady: true, activeProcesses: 0, activeSubagents: 0, idle: true, compactions: 0, tokens: 50_000, aborts: 0 };
const bus = {
on(name: string, handler: any) { const set = listeners.get(name) ?? new Set(); set.add(handler); listeners.set(name, set); return () => set.delete(handler); },
emit(name: string, value: any) {
if (name === "intercom:extension-register") {
peer.receive = value.onEvent;
value.onReady({ snapshot: () => ({ connected: true, supported: true }), listSessions: async () => peers.map(p => ({ id: p.id, pid: p === peer ? process.pid : process.pid + 1, cwd, model: "test" })), publish(payload: any) {
wire.push({ from: peer.id, ...payload });
for (const p of peers) queueMicrotask(() => p.receive?.({ type: "message", fromSessionId: peer.id, payload }));
} }); return true;
}
if (name === "processes:request:list") value.reply(Array.from({ length: peer.activeProcesses }, () => ({ status: "running" })));
if (name === "subagents:rpc:v1:request") bus.emit(`subagents:rpc:v1:reply:${value.requestId}`, { requestId: value.requestId, success: true, data: { fleet: { version: 1, totalActive: peer.activeSubagents } } });
for (const handler of listeners.get(name) ?? []) handler(value);
},
};
const pi: any = { events: bus, on(name: string, fn: any) { hooks.set(name, [...(hooks.get(name) ?? []), fn]); }, registerTool(tool: any) { tools.set(tool.name, tool); active.push(tool.name); }, registerCommand(name: string, command: any) { commands.set(name, command); }, appendEntry(customType: string, data: any) { entries.push({ type: "custom", customType, data }); }, getActiveTools: () => active, setActiveTools: (names: string[]) => { active = names; }, getAllTools: () => [{ name: "subagent" }], sendMessage: (message: any) => contexts.push(message), sendUserMessage: (text: string, options: any) => messages.push({ text, options }) };
const ctx: any = { cwd, hasUI: true, model: { contextWindow: 200_000 }, isIdle: () => peer.idle, abort: () => { peer.aborts++; }, getContextUsage: () => ({ tokens: peer.tokens }), compact({ onComplete }: any) { peer.compactions++; peer.tokens = 10_000; onComplete({}); }, ui: { notify() {}, setStatus() {}, theme: { fg: (_: any, text: string) => text } }, sessionManager: { getEntries: () => entries, getBranch: () => entries, getSessionFile: () => sessionFile } };
peer.pi = pi; peer.ctx = ctx;
peer.hook = async (name: string, event = {}) => { for (const fn of hooks.get(name) ?? []) await fn(event, ctx); };
peer.request = (method: string, params: any = {}) => new Promise<any>((resolve, reject) => bus.emit(PLAN_API_EVENT, { version: 1, method, ...params, resolve, reject }));
peers.push(peer); extension(pi, () => peer.modelReady); return peer;
}
const worker = make("worker"); const supervisor = make("supervisor");
return { worker, supervisor, binding, wire, planPath, async restart(peer: any, id: string) { await peer.hook("session_shutdown"); peers.splice(peers.indexOf(peer), 1); const replacement = make(id, structuredClone(peer.entries), peer.ctx.sessionManager.getSessionFile()); await replacement.hook("session_start"); await tick(); return replacement; }, async start() { await worker.hook("session_start"); await supervisor.hook("session_start"); await worker.request("prepare", { binding }); const attached = worker.request("attached", { bindingId: binding.id }); void attached.catch(() => {}); await supervisor.request("bootstrap", { binding, workerId: "worker" }); await attached; }, async close() { for (const peer of peers) await peer.hook("session_shutdown"); rmSync(cwd, { recursive: true, force: true }); } };
}
test("plan bootstrap compacts only the supervisor and pairing alone never starts a worker or a review", async () => {
const h = pairHarness(); try {
await h.start(); assert.equal(h.worker.compactions, 0); assert.equal(h.supervisor.compactions, 1);
assert.equal(h.worker.messages.length, 0); assert.equal(h.supervisor.messages.length, 0);
assert.ok(h.supervisor.contexts.some((m: any) => m.content.includes('literal "[x]"')));
await h.worker.request("activate", { bindingId: h.binding.id }); await tick();
assert.equal(h.supervisor.messages.length, 1); assert.match(h.supervisor.messages[0].text, /still going/);
assert.equal(h.worker.messages.length, 0);
} finally { await h.close(); }
});
test("goal decisions are correlated, preserve the pair across two goals, and cannot call overall done", async () => {
const h = pairHarness(); try {
await h.start(); const hash = planHash(readFileSync(h.planPath, "utf8"));
for (const goal of ["first", "second"]) {
const result = h.worker.request("review", { bindingId: h.binding.id, goal, planHash: hash }); await tick();
const req = h.wire.findLast((w: any) => w.t === "goal_review");
await assert.rejects(h.supervisor.tools.get("done").execute("", { reason: "done" }, undefined, undefined, h.supervisor.ctx), /Open plan goals/);
await assert.rejects(h.supervisor.tools.get("review_goal").execute("", { requestId: "stale", decision: "approve", reason: "ok" }), /No matching/);
await h.supervisor.tools.get("review_goal").execute("", { requestId: req.requestId, decision: "approve", reason: "Faithful to the plan" });
assert.equal((await result).goal, goal); assert.equal((await h.worker.request("status")).connected, true);
}
assert.equal(h.wire.filter((w: any) => w.t === "done").length, 0);
} finally { await h.close(); }
});
test("abort and stop cancel pending requests; late decisions cannot approve a replacement", async () => {
const h = pairHarness(); try {
await h.start(); const abort = new AbortController();
const pending = h.worker.request("review", { bindingId: h.binding.id, goal: "first", planHash: planHash(readFileSync(h.planPath, "utf8")), signal: abort.signal });
const rejected = assert.rejects(pending, /cancelled/); await tick(); abort.abort(); await rejected; await tick();
assert.equal(h.supervisor.aborts, 1);
await h.worker.request("stop", { bindingId: h.binding.id }); await tick(); assert.equal((await h.worker.request("status")).connected, false);
} finally { await h.close(); }
});
test("50 actual model turns trigger one view, independent of the number of messages", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.request("activate", { bindingId: h.binding.id }); await tick(); h.worker.idle = false;
const count = h.wire.filter((w: any) => w.t === "view").length;
for (let n = 0; n < 49; n++) await h.worker.hook("turn_end");
assert.equal(h.wire.filter((w: any) => w.t === "view").length, count);
await h.worker.hook("turn_end"); await tick();
assert.equal(h.wire.filter((w: any) => w.t === "view").length, count + 1);
} finally { await h.close(); }
});
test("unknown background providers are not proof of quiescence", async () => {
const result = await backgroundState({ events: { emit() {} }, getAllTools: () => [] });
assert.equal(result.quiet, false); assert.match(result.description, /unknown/);
});
test("stale plan content invalidates a pending goal review", async () => {
const h = pairHarness(); try {
await h.start(); const abort = new AbortController();
const pending = h.worker.request("review", { bindingId: h.binding.id, goal: "first", planHash: planHash(readFileSync(h.planPath, "utf8")), signal: abort.signal });
const rejected = assert.rejects(pending, /cancelled/); await tick();
const req = h.wire.findLast((w: any) => w.t === "goal_review"); writeFileSync(h.planPath, "Human changed the requirement");
await assert.rejects(h.supervisor.tools.get("review_goal").execute("", { requestId: req.requestId, decision: "approve", reason: "ok" }), /Plan changed/);
abort.abort(); await rejected;
} finally { await h.close(); }
});
test("small forks skip compaction, but real compaction failure prevents pairing", async () => {
const small = pairHarness(); try {
small.supervisor.tokens = 10_000; await small.start(); assert.equal(small.supervisor.compactions, 0);
assert.ok(small.supervisor.contexts.some((m: any) => m.content.includes('literal "[x]"')));
} finally { await small.close(); }
const failed = pairHarness(); try {
failed.supervisor.ctx.compact = ({ onError }: any) => onError(new Error("Compaction provider failed"));
await assert.rejects(failed.start(), /Compaction provider failed/);
assert.equal(failed.wire.filter((w: any) => w.t === "pair").length, 0);
} finally { await failed.close(); }
});
test("the hour timer and a coincident turn checkpoint produce a single view", async (t) => {
t.mock.timers.enable({ apis: ["Date", "setInterval", "setTimeout"] });
const h = pairHarness(); try {
await h.start(); await h.worker.request("activate", { bindingId: h.binding.id }); await tick(); h.worker.idle = false;
const count = h.wire.filter((w: any) => w.t === "view").length;
for (let n = 0; n < 49; n++) await h.worker.hook("turn_end");
t.mock.timers.tick(3_600_000); await h.worker.hook("turn_end"); await tick();
assert.equal(h.wire.filter((w: any) => w.t === "view").length, count + 1);
} finally { await h.close(); t.mock.timers.reset(); }
});
test("settled checks wait for tracked processes and subagents to finish", async (t) => {
t.mock.timers.enable({ apis: ["Date", "setInterval", "setTimeout"] });
const h = pairHarness(); try {
await h.start(); await h.worker.request("activate", { bindingId: h.binding.id }); await tick();
t.mock.timers.tick(2_000); h.worker.activeProcesses = 1; h.worker.activeSubagents = 1;
const count = h.wire.filter((w: any) => w.t === "view").length;
await h.worker.hook("agent_settled"); await tick();
assert.equal(h.wire.filter((w: any) => w.t === "view").length, count);
h.worker.activeProcesses = 0; h.worker.pi.events.emit("processes:ended", {}); await tick();
assert.equal(h.wire.filter((w: any) => w.t === "view").length, count);
h.worker.activeSubagents = 0; h.worker.pi.events.emit("subagent:async-complete", {}); await tick();
assert.equal(h.wire.filter((w: any) => w.t === "view").length, count + 1);
} finally { await h.close(); t.mock.timers.reset(); }
});
test("bootstrap stop cannot resurrect a supervisor after compaction completes", async () => {
const h = pairHarness(); let finish!: () => void;
h.supervisor.ctx.compact = ({ onComplete }: any) => { finish = onComplete; };
try {
const starting = h.start(); const rejected = assert.rejects(starting, /cancelled/); await tick();
await h.worker.request("stop", { bindingId: h.binding.id }); await tick(); finish(); await rejected;
assert.equal(h.wire.filter((w: any) => w.t === "pair").length, 0);
const restarted = await h.restart(h.supervisor, "stopped-bootstrap-reloaded");
await assert.rejects(restarted.request("bootstrap", { binding: h.binding, workerId: "worker" }), /was stopped/);
assert.equal(restarted.compactions, 0);
assert.equal(h.wire.filter((w: any) => w.t === "pair").length, 0);
assert.equal((await restarted.request("status")).binding.stopped, true);
} finally { await h.close(); }
});
test("a restarted worker reconnects by exact saved session identity without a new supervisor", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.request("activate", { bindingId: h.binding.id }); await tick();
const reloaded = await h.restart(h.worker, "worker-after-restart");
assert.equal((await reloaded.request("status")).connected, true);
assert.equal(h.wire.filter((w: any) => w.t === "pair").length, 1);
assert.equal(h.supervisor.compactions, 1);
const pending = reloaded.request("review", { bindingId: h.binding.id, goal: "first", planHash: planHash(readFileSync(h.planPath, "utf8")) }); await tick();
const req = h.wire.findLast((w: any) => w.t === "goal_review");
await h.supervisor.tools.get("review_goal").execute("", { requestId: req.requestId, decision: "approve", reason: "Same plan" });
assert.equal((await pending).decision, "approve");
} finally { await h.close(); }
});
test("unknown initial context must compact instead of taking the known-small shortcut", async () => {
const h = pairHarness(); try {
h.supervisor.tokens = null;
await h.start();
assert.equal(h.supervisor.compactions, 1);
} finally { await h.close(); }
});
test("null post-compaction usage cannot raise the next configured 100k checkpoint", async () => {
const h = pairHarness(); try {
h.supervisor.tokens = 150_000;
h.supervisor.ctx.compact = ({ onComplete }: any) => {
h.supervisor.compactions++;
h.supervisor.tokens = null; // Native Pi has no assistant usage yet after compaction.
onComplete({ estimatedTokensAfter: 10_000 });
};
await h.start();
await h.worker.request("activate", { bindingId: h.binding.id }); await tick();
assert.equal(h.supervisor.compactions, 1);
h.supervisor.tokens = 100_001;
await h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", view: "New work since compaction", stopped: false } });
await tick();
assert.equal(h.supervisor.compactions, 2);
} finally { await h.close(); }
});
test("stopping a routine view during compaction invalidates its suspended continuation", async () => {
const h = pairHarness(); let finish!: () => void;
try {
await h.start(); await h.worker.request("activate", { bindingId: h.binding.id }); await tick();
h.supervisor.tokens = 150_000;
h.supervisor.ctx.compact = ({ onComplete }: any) => { finish = onComplete; };
h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", view: "Must not restart the stopped supervisor", stopped: false } });
await tick(); assert.equal(typeof finish, "function");
await h.worker.request("stop", { bindingId: h.binding.id }); await tick();
const messages = h.supervisor.messages.length;
const contexts = h.supervisor.contexts.length;
const entries = h.supervisor.entries.length;
finish(); await tick();
assert.equal(h.supervisor.messages.length, messages);
assert.equal(h.supervisor.contexts.length, contexts);
assert.equal(h.supervisor.entries.length, entries);
assert.equal((await h.supervisor.request("status")).binding.stopped, true);
} finally { await h.close(); }
});
test("restart of a provisional bootstrap resumes compaction and pairing in the same saved session", async () => {
const h = pairHarness(); let finish!: () => void;
try {
await h.worker.hook("session_start"); await h.supervisor.hook("session_start");
await h.worker.request("prepare", { binding: h.binding });
const attached = h.worker.request("attached", { bindingId: h.binding.id });
void attached.catch(() => {});
h.supervisor.ctx.compact = ({ onComplete }: any) => { finish = onComplete; };
const interrupted = h.supervisor.request("bootstrap", { binding: h.binding, workerId: "worker" });
const rejected = assert.rejects(interrupted, /cancelled/); await tick();
assert.equal(h.supervisor.entries.at(-1).data.role, "supervisor");
assert.notEqual(h.supervisor.entries.at(-1).data.planInitialized, true);
const restarted = await h.restart(h.supervisor, "supervisor-restarted");
finish(); await rejected;
assert.equal(h.wire.filter((w: any) => w.t === "look" || w.t === "plan_hello").length, 0);
await restarted.request("bootstrap", { binding: h.binding, workerId: "worker" });
const ready = await attached;
assert.equal(ready.supervisorSession, h.binding.supervisorSession);
assert.equal(restarted.ctx.sessionManager.getSessionFile(), h.binding.supervisorSession);
assert.equal(restarted.compactions, 1);
assert.equal(restarted.entries.at(-1).data.planInitialized, true);
assert.equal((await h.worker.request("status")).connected, true);
assert.equal(h.wire.filter((w: any) => w.t === "pair").length, 1);
// An acknowledged reload reuses the pairing rather than bootstrapping once again.
const acknowledged = await h.restart(restarted, "supervisor-acknowledged-reloaded");
await acknowledged.request("bootstrap", { binding: h.binding, workerId: "worker" });
assert.equal(acknowledged.compactions, 0);
assert.equal(h.wire.filter((w: any) => w.t === "pair").length, 1);
} finally { await h.close(); }
});
for (const stop of ["command", "done"]) test(`${stop} preserves a stopped supervisor across reload`, async () => {
const h = pairHarness(); try {
await h.start();
if (stop === "command") await h.supervisor.commands.get("supervise").handler("stop", h.supervisor.ctx);
else {
writeFileSync(h.planPath, "# Plan\n\n1. [x] goal: first\n2. [x] goal: second\n");
await h.worker.request("activate", { bindingId: h.binding.id }); await tick();
await h.supervisor.tools.get("done").execute("", { reason: "All goals accepted" }, undefined, undefined, h.supervisor.ctx);
}
await tick(); const pairCount = h.wire.filter((w: any) => w.t === "pair").length;
const restarted = await h.restart(h.supervisor, `${stop}-reloaded`);
await assert.rejects(restarted.request("bootstrap", { binding: h.binding, workerId: "worker" }), /was stopped/);
assert.equal(restarted.compactions, 0);
assert.equal(h.wire.filter((w: any) => w.t === "pair").length, pairCount);
assert.equal((await restarted.request("status")).binding.stopped, true);
} finally { await h.close(); }
});
test("same-binding replay retains activation when the supervisor lost its acknowledgement", async () => {
const h = pairHarness(); try {
await h.worker.hook("session_start"); await h.supervisor.hook("session_start");
await h.worker.request("prepare", { binding: h.binding });
const attached = h.worker.request("attached", { bindingId: h.binding.id });
// Simulate loss of the supervisor before it receives/persists the acknowledgement.
// The worker still receives pair and can finish Ready independently of that process.
h.supervisor.receive = () => {};
void h.supervisor.request("bootstrap", { binding: h.binding, workerId: "worker" }).catch(() => {});
await attached;
await h.worker.request("activate", { bindingId: h.binding.id }); await tick();
assert.equal((await h.worker.request("status")).binding.active, true);
assert.notEqual(h.supervisor.entries.at(-1).data.planInitialized, true);
assert.notEqual(h.supervisor.entries.at(-1).data.plan.active, true);
const restarted = await h.restart(h.supervisor, "supervisor-after-lost-ack");
await restarted.request("bootstrap", { binding: h.binding, workerId: "worker" });
assert.equal(restarted.ctx.sessionManager.getSessionFile(), h.binding.supervisorSession);
assert.equal((await h.worker.request("status")).binding.active, true);
assert.equal(h.wire.findLast((w: any) => w.t === "paired").plan.active, true);
assert.equal((await restarted.request("status")).binding.active, true);
assert.equal(restarted.entries.at(-1).data.planInitialized, true);
const views = h.wire.filter((w: any) => w.t === "view").length;
const messages = restarted.messages.length;
h.worker.idle = false;
for (let n = 0; n < 50; n++) await h.worker.hook("turn_end");
await tick();
assert.equal(h.wire.filter((w: any) => w.t === "view").length, views + 1);
assert.equal(restarted.messages.length, messages + 1);
assert.equal(h.wire.filter((w: any) => w.t === "plan_activate").length, 1);
assert.equal(h.worker.messages.length, 0); // No new Ready/work handoff was necessary.
} finally { await h.close(); }
});
test("model-unavailable stop validates binding, cancels pending reviews and ignores old directives", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.request("activate", { bindingId: h.binding.id }); await tick();
const abort = new AbortController();
const pending = h.worker.request("review", { bindingId: h.binding.id, goal: "first", planHash: planHash(readFileSync(h.planPath, "utf8")), signal: abort.signal });
const cancelled = assert.rejects(pending, /cancelled/); await tick();
h.worker.modelReady = false; abort.abort(); await cancelled; await tick();
await assert.rejects(h.worker.request("stop", { bindingId: "wrong-binding" }), /No matching plan pairing/);
await h.worker.request("stop", { bindingId: h.binding.id }); await tick();
assert.equal(h.worker.entries.at(-1).data.role, "none");
assert.equal(h.supervisor.entries.at(-1).data.role, "none");
const before = h.worker.messages.length;
h.worker.modelReady = true;
h.worker.receive({ type: "message", fromSessionId: "supervisor", payload: { t: "directive", to: "worker", text: "stale instruction" } });
await tick(); assert.equal(h.worker.messages.length, before);
} finally { await h.close(); }
});
@@ -0,0 +1,43 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { copyFileSync, mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { childPiProcesses } from "../../src/internal/supervisor/subagents.js";
/**
* Runs the real ps command against a real child process, because the parsing is the part most
* likely to be quietly wrong: a column order or a comm name that differs by platform reads as
* "no subagents", which is the answer that lets the supervisor declare a busy worker finished.
*/
function spawnChildNamedPi() {
const dir = mkdtempSync(join(tmpdir(), "supervisor-test-"));
const fake = join(dir, "pi");
copyFileSync("/usr/bin/sleep", fake); // ps reports comm from the executable name
return spawn(fake, ["30"], { stdio: "ignore" });
}
test("a child process named pi is found by ps, and stops being found when it exits", async (t) => {
if (process.platform !== "linux" && process.platform !== "darwin") return t.skip("ps only");
assert.deepEqual(await childPiProcesses(), [], "no subagents before one is started");
const child = spawnChildNamedPi();
await new Promise((r) => setTimeout(r, 300));
assert.deepEqual(await childPiProcesses(), [child.pid], "the running child must be reported");
child.kill();
await new Promise((r) => child.once("exit", r));
assert.deepEqual(await childPiProcesses(), [], "and not reported once it exits");
});
test("the check is a snapshot, so it cannot hold up the worker's settle", async (t) => {
if (process.platform !== "linux" && process.platform !== "darwin") return t.skip("ps only");
const child = spawnChildNamedPi();
await new Promise((r) => setTimeout(r, 300));
const started = Date.now();
assert.deepEqual(await childPiProcesses(), [child.pid]);
assert.ok(Date.now() - started < 1000, "it returns while the child is still running, it does not wait");
child.kill();
});
+314
View File
@@ -0,0 +1,314 @@
import assert from "node:assert/strict";
import test from "node:test";
import { age, buildView, type Entry, goalPreview, MAX_VIEW_BYTES, outstandingWork, progressKey, sinceLastTurn, turnsSince } from "../../src/internal/supervisor/view.js";
function assistant(text: string, calls: Array<{ name: string; args: Record<string, unknown> }> = []): Entry {
return {
type: "message",
message: {
role: "assistant",
content: [
{ type: "text", text },
...calls.map((c) => ({ type: "toolCall", name: c.name, arguments: c.args })),
],
},
};
}
function toolResult(toolName: string, text: string, isError = false): Entry {
return { type: "message", message: { role: "toolResult", toolName, isError, content: [{ type: "text", text }] } };
}
test("a one-line goal stays whole while a multi-line goal has a locator", () => {
assert.equal(goalPreview("fix the parser"), "fix the parser");
assert.equal(goalPreview("Build the causal evaluation.\nThe full rubric follows."), "Build the causal evaluation. [...]");
const view = buildView({
goal: "Build the causal evaluation.\nThe full rubric follows.",
status: "idle",
entries: [assistant("working")],
});
assert.match(view, /<goal>\nBuild the causal evaluation\. \[\.\.\.\]\n<\/goal>/);
assert.doesNotMatch(view, /The full rubric follows/);
});
test("a view carries only the turns the supervisor has not been sent", () => {
// The supervisor is a real session and keeps every view it read, so re-sending the whole
// transcript each time is a second copy of what it already has. It grows with every review.
const entries = [assistant("the first thing I did"), assistant("the second thing I did")];
const first = buildView({ goal: "g", status: "idle", entries });
assert.match(first, /the first thing I did/);
assert.equal(turnsSince(entries), 2);
entries.push(assistant("the third thing I did"));
const next = buildView({ goal: "g", status: "idle", entries, since: 2 });
assert.match(next, /the third thing I did/);
assert.doesNotMatch(next, /the first thing I did/, "already sent, so it must not go again");
assert.match(next, /# New turns since your last look \(1 of 3\)/);
});
test("the last two reasoning blocks stay in the narrative, and older ones drop out", () => {
// Block shape read out of a real session jsonl. pi-vcc's normalize keeps only text and toolCall,
// so reasoning reaches nobody, although you see it on screen. It belongs beside the tool call it
// produced, not in a section of its own, because that is the order you read a session in.
const thinker = (thinking: string, said: string): Entry => ({
type: "message",
message: { role: "assistant", content: [{ type: "thinking", thinking }, { type: "text", text: said }] },
});
const entries = [
thinker("TOO OLD TO SEND", "first try"),
thinker("the bash tool is dead, LET ME TEST READ", "second try"),
thinker("read works, SO I WILL DELEGATE INSTEAD", "third try"),
];
const view = buildView({ goal: "g", status: "working", entries });
assert.match(view, /LET ME TEST READ[\s\S]*SO I WILL DELEGATE INSTEAD/, "in order, oldest first");
assert.match(view, /LET ME TEST READ[\s\S]*second try/, "each thought sits with the turn it produced");
assert.doesNotMatch(view, /TOO OLD TO SEND/, "two blocks, or the view becomes a second transcript");
assert.match(view, /\(thinking\)/, "marked, so the supervisor knows it is reasoning and not speech");
});
test("a compaction restarts the view, so no turn falls into the gap", () => {
// getBranch keeps the entries a compaction replaced, so the mark now points past the end. Read
// the wrong slice here and the supervisor silently reads a piece of the old history.
const entries: Entry[] = [
assistant("old work"),
{ type: "compaction", summary: "SUMMARY OF THE OLD WORK" },
assistant("work after the compaction"),
];
const view = buildView({ goal: "g", status: "idle", entries, since: 9 });
assert.match(view, /The worker compacted, so this view restarts/);
assert.match(view, /SUMMARY OF THE OLD WORK/);
assert.match(view, /work after the compaction/);
assert.equal(turnsSince(entries), 1, "the mark restarts from the compaction, not from the session");
});
test("pi-vcc reports the files the worker wrote, and separates them from the ones it read", () => {
const view = buildView({
goal: "g",
status: "idle",
entries: [
assistant("editing", [{ name: "edit", args: { path: "src/a.ts" } }]),
assistant("reading", [{ name: "read", args: { path: "src/never.ts" } }]),
assistant("writing", [{ name: "write", args: { file_path: "docs/b.md" } }]),
],
});
assert.match(view, /Modified:.*src\/a\.ts/);
assert.match(view, /Read:.*src\/never\.ts/);
});
test("progressKey is unchanged when a review produced no new file or commit", () => {
const worked = [assistant("editing", [{ name: "edit", args: { path: "src/a.ts" } }])];
const talked = [...worked, assistant("I will look into that shortly.")];
const wroteMore = [...worked, assistant("editing", [{ name: "write", args: { path: "src/b.ts" } }])];
assert.equal(progressKey(talked), progressKey(worked), "talking is not progress");
assert.notEqual(progressKey(wroteMore), progressKey(worked), "a new file is progress");
});
test("progressKey still sees a new file past pi-vcc's ten path display cap", () => {
// The rendered section stops at ten paths and says "(+N more)", so reading it there froze this
// key on exactly the long runs it exists for.
const many = (n: number) =>
Array.from({ length: n }, (_, i) => assistant("editing", [{ name: "edit", args: { path: `src/f${i}.ts` } }]));
assert.notEqual(progressKey(many(13)), progressKey(many(12)), "the 13th file must count as progress");
});
test("a commit counts as progress, even when no file was written since", () => {
// A worker whose whole turn was committing looked stagnant while the view line claims to count
// commits.
const ran = (cmd: string, out: string): Entry[] => [
assistant("committing", [{ name: "bash", args: { command: cmd } }]),
toolResult("bash", out),
];
const before = ran("git status", "nothing to commit");
const after = [...before, ...ran(`git commit -m "fix the parser"`, "[main abc1234] fix the parser")];
assert.notEqual(progressKey(after), progressKey(before));
});
test("outstandingWork finds tool calls that never got a result", () => {
const answered: Entry = {
type: "message",
message: { role: "toolResult", toolName: "read", toolCallId: "call-1", content: [{ type: "text", text: "ok" }] },
};
const entries: Entry[] = [
{ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "call-1", name: "read" }] } },
answered,
{ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "call-2", name: "subagent" }] } },
];
assert.deepEqual(outstandingWork(entries), ["subagent"]);
assert.deepEqual(outstandingWork([entries[0], answered]), []);
});
test("buildView reports a tool call with no result, so done can be refused", () => {
const busy = buildView({
goal: "g",
status: "idle",
entries: [{ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "x", name: "subagent", arguments: {} }] } }],
});
assert.match(busy, /^tool calls with no result: subagent$/m);
const quiet = buildView({ goal: "g", status: "idle", entries: [assistant("all done")] });
assert.match(quiet, /^tool calls with no result: none$/m);
});
test("the view says how many reviews in a row changed nothing, and says nothing at zero", () => {
const entries = [assistant("hi")];
assert.match(buildView({ goal: "g", status: "idle", entries, stale: 3 }), /no new file or commit for 3 reviews/);
assert.doesNotMatch(buildView({ goal: "g", status: "idle", entries }), /reviews in a row/);
});
test("the view merges the worker's compaction summary with the turns after it", () => {
// pi-vcc's compile() takes the old summary as previousSummary, so nothing between the summary
// and the newest turn falls in the gap between them.
const view = buildView({
goal: "g",
status: "idle",
entries: [
{ type: "compaction", summary: "[Session Goal]\n- build the dataset" },
assistant("carrying on", [{ name: "write", args: { path: "after.md" } }]),
],
});
assert.match(view, /build the dataset/, "the summary from before the compaction survives");
assert.match(view, /after\.md/, "so does the work done after it");
});
test("a turn the compaction summary already covers is not sent twice", () => {
// getBranch keeps the entries a compaction replaced, so passing all of them alongside the
// summary spends the byte budget on two copies of the same work.
const view = buildView({
goal: "g",
status: "idle",
entries: [
assistant("COVERED BY THE SUMMARY", [{ name: "edit", args: { path: "old.ts" } }]),
{ type: "compaction", summary: "[Session Goal]\n- build the dataset" },
assistant("after the compaction"),
],
});
assert.match(view, /build the dataset/);
assert.match(view, /after the compaction/);
assert.doesNotMatch(view, /COVERED BY THE SUMMARY/);
});
test("pi-vcc's sections and its transcript land on the right sides of the split", () => {
// compile() emits `sections + "\n\n---\n\n" + transcript`, and drops either part when it is
// empty. Splitting that wrong put the sections under "Recent turns", where the byte cut eats
// them from the top.
const view = buildView({
goal: "g",
status: "idle",
entries: [{ type: "message", message: { role: "user", content: "make the results table" } }],
});
const [above, below] = view.split("# Turns so far");
assert.match(above, /\[Session Goal\]/, "the sections belong above");
assert.match(below, /make the results table/, "the transcript belongs below");
assert.doesNotMatch(below, /\[Session Goal\]/);
});
test("the view does not tell the supervisor to use vcc_recall, a tool it does not have", () => {
const view = buildView({ goal: "g", status: "idle", entries: [assistant("hi")] });
assert.doesNotMatch(view, /vcc_recall/);
});
test("supervisor directives are not sent back as worker evidence", () => {
const directive: Entry = {
type: "message",
message: { role: "user", content: "[supervisor] Read exactly src/a.ts and quote it." },
};
const first = buildView({
goal: "g",
status: "working",
entries: [directive, assistant("I read src/a.ts", [{ name: "read", args: { path: "src/a.ts" } }])],
});
assert.doesNotMatch(first, /Read exactly src\/a\.ts/);
assert.match(first, /I read src\/a\.ts/);
const next = buildView({
goal: "g",
status: "working",
entries: [directive, assistant("I read src/a.ts", [{ name: "read", args: { path: "src/a.ts" } }]), directive],
since: 1,
});
assert.match(next, /# New turns since your last look \(0 of 1\)/);
assert.doesNotMatch(next, /Read exactly src\/a\.ts/);
});
test("bookkeeping tool calls are kept out of the transcript", () => {
const view = buildView({
goal: "g",
status: "idle",
entries: [
assistant("planning", [{ name: "TodoWrite", args: {} }]),
toolResult("TodoWrite", "todo list updated with 5 items"),
assistant("real work", [{ name: "edit", args: { path: "src/a.ts" } }]),
],
});
assert.doesNotMatch(view, /todo list updated/);
assert.match(view, /src\/a\.ts/);
});
test("buildView reports the goal, status, and files without historical failures", () => {
const view = buildView({
goal: "make the table",
status: "idle",
entries: [assistant("done", [{ name: "write", args: { path: "results.md" } }]), toolResult("bash", "exit code 2")],
});
assert.match(view, /<goal>\nmake the table\n<\/goal>/);
assert.match(view, /status: idle/);
assert.match(view, /turns: 2/);
assert.match(view, /results\.md/);
assert.doesNotMatch(view, /# Problems|bash exit 2/);
});
test("how long the worker has been quiet, measured from its own last entry", () => {
// The number wassname asked for after the 2h27m silence: "there have been no turns for this
// long, is it stuck". Measured from the worker's last message, not from the supervisor's last
// look, so it reads the same whether the worker sits at the prompt or hangs inside one command.
const now = Date.parse("2026-08-14T04:29:00Z");
const at = (iso: string): Entry => ({ ...assistant("working"), timestamp: iso });
const stopped = [at("2026-08-14T01:00:00Z"), at("2026-08-14T02:02:18Z")];
assert.equal(age(sinceLastTurn(stopped, now)), "2h27m", "the real overnight gap, from session 019ffa73");
const supervisorMessage: Entry = {
type: "message",
timestamp: "2026-08-14T04:28:59Z",
message: { role: "user", content: "[supervisor] Do not reset the worker clock." },
};
assert.equal(age(sinceLastTurn([...stopped, supervisorMessage], now)), "2h27m", "a supervisor directive is not worker progress");
// The last non-supervisor entry wins even when an older one follows it in some other order, and a branch with
// no timestamps at all reports 0 rather than throwing.
assert.equal(age(sinceLastTurn([at("2026-08-14T04:28:30Z")], now)), "30s");
assert.equal(age(sinceLastTurn([at("2026-08-14T03:44:00Z")], now)), "45m");
assert.equal(sinceLastTurn([assistant("no stamp")], now), 0);
});
test("buildView keeps the newest turns when it has to cut for the channel limit", () => {
const long = Array.from({ length: 400 }, (_, i) => assistant(`turn ${i} ${"x".repeat(400)}`));
const view = buildView({ goal: "g", status: "idle", entries: long });
assert.ok(Buffer.byteLength(view, "utf-8") <= MAX_VIEW_BYTES, `view was ${Buffer.byteLength(view)} bytes`);
assert.match(view, /turn 399/, "the newest turn must survive the cut");
assert.doesNotMatch(view, /turn 0 /, "the oldest must be the one dropped");
});
test("pi's own branch logic drops the abandoned fork, on a session file", async () => {
// Synthetic fixture copied from the upstream make-fixture.ts output; no real transcript.
// An earlier version of this test asserted on the rendered view, and passed even when fed every
// abandoned entry, because byte trimming removed them anyway.
const { parseSessionEntries, buildContextEntries } = await import("@earendil-works/pi-coding-agent");
const { readFileSync } = await import("node:fs");
const fixture = new URL("../fixtures/forked-session.jsonl", import.meta.url).pathname;
const all = parseSessionEntries(readFileSync(fixture, "utf-8")).filter((e: any) => e.type !== "session");
const byId = new Map(all.map((e: any) => [e.id, e]));
const leaf = all[all.length - 1] as any;
const branch = buildContextEntries(all as any, leaf.id, byId as any);
const asText = (entries: any[]) => JSON.stringify(entries);
assert.match(asText(all), /ABANDONED/, "the fixture must contain an abandoned branch");
assert.doesNotMatch(asText(branch), /ABANDONED/, "the live branch must not contain the abandoned fork");
assert.equal(all.length - branch.length, 22);
});
test("a long goal cannot push the view past the broker limit", () => {
const view = buildView({ goal: "x".repeat(60000), status: "idle", entries: [assistant("hi")] });
assert.ok(Buffer.byteLength(view, "utf-8") <= MAX_VIEW_BYTES, `view was ${Buffer.byteLength(view)} bytes`);
});
+13 -2
View File
@@ -1,17 +1,28 @@
import { describe, expect, it } from "vitest";
import { judgeSystem, planDrafting, planningState, reminder, resync } from "../src/prompts.js";
import { alignmentPolicy, judgeSystem, planDrafting, planningState, reminder, resync, waivesAlignment } from "../src/prompts.js";
describe("planning prompt", () => {
it("requires fact finding or a focused question before a goal", () => {
expect(planDrafting).toContain("Use read-only repository tools or web search when either can\nresolve a fact.");
expect(planDrafting).toContain("ask the human to confirm your interpretation");
expect(planDrafting).toContain("approve an editorial or other preference choice");
expect(planDrafting).toContain("answer materially reduces uncertainty\nwhile discovering the right plan");
expect(planDrafting).toContain("ask at least THREE distinct task-specific alignment");
expect(planDrafting).toContain("questions in ONE chat round");
expect(planDrafting).toContain("Wait for the human's answers and use them");
expect(planDrafting).toContain("self-contained: state the relevant\ncontext, use the human's language and ASD-STE100");
expect(planDrafting).toContain("placeholder goal such as \"work out the thing\"");
expect(planDrafting).toContain("object, observable result, settled scope, and required approval");
});
it("waives questions only for an explicit current-objective instruction", () => {
for (const objective of ["fix it, no questions", "skip questions and implement", "do not ask me any questions", "no q's", "skip q's", "fix it; no q’s please"]) {
expect(waivesAlignment(objective)).toBe(true);
expect(alignmentPolicy(waivesAlignment(objective))).toContain("THIS plan only");
}
for (const objective of ["next objective", "fix the exporter; do not skip questions", "don't skip questions", "add a 'skip questions' button", 'add a "no questions" mode', 'document "first; skip questions; then build"']) expect(waivesAlignment(objective), objective).toBe(false);
expect(alignmentPolicy(false)).toContain("previous plan does NOT apply");
});
it("restores the same rule after compaction", () => {
expect(planningState(".pi/plan/test.md")).toContain("web search\nwhen either can resolve a fact.");
expect(planningState(".pi/plan/test.md")).toContain("choice that needs their approval");
+95
View File
@@ -0,0 +1,95 @@
import { execFile } from "node:child_process";
import { readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { describe, expect, it, vi } from "vitest";
import { type ModelChoice, type ModelRole, RoleModels } from "../src/role-models.js";
function harness(directory: string, current: ModelChoice = { provider: "initial", id: "model" }) {
let selected: any;
const ctx = { model: current, modelRegistry: { find: vi.fn((provider: string, id: string) => ({ provider, id })) }, ui: { notify: vi.fn() } };
const pi = {
on(_name: string, handler: any) { selected = handler; },
setModel: vi.fn(async (model: ModelChoice) => { const previousModel = ctx.model; ctx.model = model; if (previousModel.provider !== model.provider || previousModel.id !== model.id) selected({ source: "set", previousModel, model }, ctx); return true; }),
};
const models = new RoleModels(pi as unknown as ExtensionAPI, directory);
return {
models, ctx, pi,
enter: (role: ModelRole) => models.enter(role, ctx as unknown as ExtensionContext),
select: (source: "set" | "cycle" | "restore", model: ModelChoice) => { ctx.model = model; selected({ source, model }, ctx); },
};
}
const stored = (dir: string, role: string) => JSON.parse(readFileSync(join(dir, `${role}-model.json`), "utf8"));
describe("role-models storage and public model_select", () => {
it("inherits current on first use, remembers set/cycle independently, and ignores automatic set and restore", async () => {
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
const h = harness(dir);
for (const role of ["planning", "worker", "supervisor"] as const) {
const inherited = { ...h.ctx.model };
expect(await h.enter(role)).toBe(true);
expect(stored(dir, role)).toEqual(inherited);
h.select(role === "planning" ? "set" : "cycle", { provider: role, id: `${role}-choice` });
}
for (const role of ["planning", "worker", "supervisor"] as const) {
expect(await h.enter(role)).toBe(true);
expect(h.ctx.model).toEqual({ provider: role, id: `${role}-choice` });
h.select("restore", { provider: "session", id: "old" });
expect(stored(dir, role)).toEqual({ provider: role, id: `${role}-choice` });
}
h.models.leave(); h.select("cycle", { provider: "outside", id: "unrelated" });
expect(stored(dir, "supervisor").provider).toBe("supervisor");
});
it("restores after a fresh instance/process and avoids cross-role lost updates", async () => {
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
const h = harness(dir);
await h.enter("planning"); h.select("cycle", { provider: "planner", id: "persisted" });
await Promise.all(["worker", "supervisor"].map(role => promisify(execFile)(process.execPath, ["--import", "tsx", resolve("test/fixtures/role-model-process.ts"), dir, role, `${role}-provider`, "persisted"])));
const fresh = harness(dir, { provider: "unrelated", id: "start" });
for (const role of ["planning", "worker", "supervisor"] as const) {
await fresh.enter(role);
expect(fresh.ctx.model).toEqual({ provider: role === "planning" ? "planner" : `${role}-provider`, id: "persisted" });
expect(Object.keys(stored(dir, role))).toEqual(["provider", "id"]);
}
});
it.each(["unavailable", "unauthenticated"])("visibly pauses a %s remembered model without replacing it", async failure => {
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
const h = harness(dir); await h.enter("worker");
h.select("set", { provider: "chosen", id: "keep" });
const fresh = harness(dir, { provider: "other-provider", id: "fallback" });
if (failure === "unavailable") fresh.ctx.modelRegistry.find.mockReturnValue(undefined as any);
else fresh.pi.setModel.mockResolvedValue(false);
expect(await fresh.enter("worker")).toBe(false);
expect(fresh.models.ready).toBe(false);
expect(fresh.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Saved choice unchanged"), "error");
expect(fresh.ctx.model.provider).toBe("other-provider");
expect(stored(dir, "worker")).toEqual({ provider: "chosen", id: "keep" });
fresh.select("restore", { provider: "fallback", id: "automatic" });
expect(stored(dir, "worker").provider).toBe("chosen");
fresh.select("cycle", { provider: "explicit", id: "replacement" });
expect(fresh.models.ready).toBe(true);
expect(stored(dir, "worker").provider).toBe("explicit");
});
});
it("explicit use-current recovers the paused role when same-model selection emits no event", async () => {
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
const h = harness(dir); await h.enter("worker"); h.select("set", { provider: "missing", id: "worker-choice" });
const fresh = harness(dir, { provider: "current", id: "working-model" });
fresh.ctx.modelRegistry.find.mockReturnValue(undefined as any);
expect(await fresh.enter("worker")).toBe(false);
await fresh.pi.setModel(fresh.ctx.model); // Real Pi suppresses this model_select.
expect(fresh.models.ready).toBe(false);
expect(stored(dir, "worker").provider).toBe("missing");
fresh.pi.setModel.mockResolvedValueOnce(false);
expect(await fresh.models.useCurrent(fresh.ctx as unknown as ExtensionContext)).toBe(false);
expect(stored(dir, "worker").provider).toBe("missing");
expect(await fresh.models.useCurrent(fresh.ctx as unknown as ExtensionContext)).toBe(true);
expect(fresh.models.activeRole).toBe("worker");
expect(fresh.models.ready).toBe(true);
expect(stored(dir, "worker")).toEqual({ provider: "current", id: "working-model" });
});
+50 -13
View File
@@ -1,5 +1,5 @@
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
@@ -49,13 +49,23 @@ function streamResponse(response: import("node:http").ServerResponse, delta: obj
}
describe("RPC review flow", () => {
it("opens Refine's editor before it starts the revision turn", async () => {
it.each([false, true])("alignment and Discuss before one Ready handoff; real-Pi same-current recovery=%s", async (recoverCurrent) => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-rpc-"));
const modelDir = join(cwd, ".agent", "pi-goals"); mkdirSync(modelDir, { recursive: true });
if (recoverCurrent) writeFileSync(join(modelDir, "worker-model.json"), JSON.stringify({ provider: "missing", id: "unavailable-worker" }));
let requestCount = 0;
let planPath = "";
const server = createServer((_request, response) => {
requestCount++;
if (requestCount === 1) {
if (requestCount === 1 || requestCount === 4) {
streamResponse(response, { content: "1. Should the output be a text file? 2. Keep the existing CLI only? 3. Does a saved PASS receipt prove success?" }, "stop");
return;
}
if (requestCount === 3 || requestCount === 6) {
streamResponse(response, { tool_calls: [{ index: 0, id: `review-${requestCount}`, type: "function", function: { name: "RequestPlanReview", arguments: "{}" } }] }, "tool_calls");
return;
}
if (requestCount === 2) {
streamResponse(response, {
tool_calls: [{
index: 0,
@@ -97,16 +107,43 @@ describe("RPC review flow", () => {
const sessionId = (state.data as { sessionId: string }).sessionId;
planPath = join(cwd, ".pi", "plan", `${sessionId}-v1.md`);
client.send({ type: "prompt", id: "goals", message: "/goals work out the thing" });
const review = await client.waitFor((message) => message.type === "extension_ui_request" && message.method === "select");
client.send({ type: "extension_ui_response", id: review.id, value: "Refine" });
const editor = await client.waitFor((message) => message.type === "extension_ui_request" && message.method === "editor");
expect(requestCount).toBe(2);
const revisionStart = client.messages.length;
client.send({ type: "extension_ui_response", id: editor.id, value: "Name the produced file." });
await client.waitFor((message) => message.type === "agent_end", revisionStart);
expect(requestCount).toBe(3);
client.send({ type: "prompt", id: "off", message: "/goals steward off" });
await client.waitFor(message => message.type === "response" && message.id === "off");
client.send({ type: "prompt", id: "goals", message: "/goals name the output file" });
await client.waitFor(message => message.type === "agent_end");
expect(requestCount).toBe(1);
expect(client.messages.some(message => message.method === "select")).toBe(false);
client.send({ type: "prompt", id: "answers", message: "Text file, existing CLI only, and a saved PASS receipt." });
const review = await client.waitFor(message => message.type === "extension_ui_request" && message.method === "select");
expect(review.options).toEqual(["Ready", "Discuss", "Edit", "Cancel"]);
const discussionAt = client.messages.length;
client.send({ type: "extension_ui_response", id: review.id, value: "Discuss" });
await client.waitFor(message => message.type === "agent_end", discussionAt);
expect(requestCount).toBe(4);
expect(client.messages.slice(discussionAt).some(message => message.method === "editor" || message.method === "select")).toBe(false);
const answerAt = client.messages.length;
client.send({ type: "prompt", id: "discuss-answer", message: "Use output.txt, no UI changes." });
await client.waitFor(message => message.type === "agent_end", answerAt);
expect(client.messages.slice(answerAt).some(message => message.method === "select")).toBe(false);
client.send({ type: "prompt", id: "finish-discussion", message: "Yes, that is enough; the draft is still right." });
const reviewedAgain = await client.waitFor(message => message.type === "extension_ui_request" && message.method === "select", discussionAt);
const readyAt = client.messages.length;
client.send({ type: "extension_ui_response", id: reviewedAgain.id, value: "Ready" });
if (recoverCurrent) {
await client.waitFor(message => message.type === "extension_ui_request" && message.method === "notify" && JSON.stringify(message).includes("worker model paused"), readyAt);
client.send({ type: "set_model", id: "same-current", provider: "offline", modelId: "test" });
expect((await client.waitFor(message => message.type === "response" && message.id === "same-current")).success).toBe(true);
expect(JSON.parse(readFileSync(join(modelDir, "worker-model.json"), "utf8"))).toEqual({ provider: "missing", id: "unavailable-worker" });
expect(client.messages.slice(readyAt).some(message => message.method === "select")).toBe(false);
client.send({ type: "prompt", id: "use-current", message: "/goals model current" });
const recoveryMenu = await client.waitFor(message => message.type === "extension_ui_request" && message.method === "select", readyAt);
expect(JSON.parse(readFileSync(join(modelDir, "worker-model.json"), "utf8"))).toEqual({ provider: "offline", id: "test" });
expect(JSON.parse(readFileSync(join(modelDir, "planning-model.json"), "utf8"))).toEqual({ provider: "offline", id: "test" });
client.send({ type: "extension_ui_response", id: recoveryMenu.id, value: "Ready" });
}
await client.waitFor(message => message.type === "agent_end", readyAt);
expect(client.messages.filter(message => message.type === "message_start" && JSON.stringify(message).includes("Work the goals"))).toHaveLength(1);
expect(requestCount).toBe(7);
} finally {
pi.kill();
server.close();
+37 -10
View File
@@ -1,29 +1,45 @@
import { spawn } from "node:child_process";
import { execFileSync, spawn } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
const source = process.env.PI_GOALS_SUPERVISOR_SOURCE;
const intercom = source ? resolve(dirname(source), "../node_modules/pi-intercom/index.ts") : "";
const stream = (res: import("node:http").ServerResponse, delta: object, finish = "stop") => {
res.writeHead(200, { "content-type": "text/event-stream" });
for (const data of [{ choices: [{ index: 0, delta, finish_reason: null }] }, { choices: [{ index: 0, delta: {}, finish_reason: finish }] }]) res.write(`data: ${JSON.stringify(data)}\n\n`);
res.end("data: [DONE]\n\n");
};
describe.skipIf(!source || !existsSync(intercom))("two real Pi sessions with the actual Intercom broker (Herdr mocked)", () => {
describe("two real Pi sessions with the actual Intercom broker (Herdr mocked)", () => {
it("forks, pairs, reviews a goal and invokes a fresh offline evidence judge", async () => {
const cwd = mkdtempSync(join(tmpdir(), "goals-supervisor-rpc-"));
// Pack real production dependencies, then run outside every checkout with no install/symlink.
const packed = JSON.parse(execFileSync("npm", ["pack", "--json", "--ignore-scripts", "--cache", "/tmp/pi-goals-npm-cache", "--pack-destination", cwd], { cwd: resolve("."), encoding: "utf8" }))[0];
execFileSync("tar", ["-xzf", join(cwd, packed.filename), "-C", cwd]);
const packageRoot = join(cwd, "package");
const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
expect(manifest.bundledDependencies).toEqual(["pi-intercom", "@sting8k/pi-vcc"]);
expect(existsSync(join(packageRoot, "node_modules/pi-intercom/broker/broker.ts"))).toBe(true);
expect(existsSync(join(packageRoot, "src/internal/supervisor/index.ts"))).toBe(true);
expect(existsSync(join(packageRoot, "node_modules/@sting8k/pi-vcc/src/core/summarize.ts"))).toBe(true);
expect(packed.files.some((file: { path: string }) => /node_modules\/(?:@earendil-works|typebox)\//.test(file.path))).toBe(false);
expect(existsSync(join(packageRoot, "THIRD_PARTY_NOTICES.md"))).toBe(true);
const host = join(cwd, "herdr-test-host.ts");
writeFileSync(host, readFileSync(resolve("test/fixtures/herdr-test-host.ts"), "utf8").replace('"../../src/index.js"', JSON.stringify(join(packageRoot, "src/index.ts"))));
// Only the worker's Herdr exec is wrapped. Supervisor loads the untouched package manifest.
const resources = manifest.pi.extensions.flatMap((path: string) => ["-e", path === "./src/index.ts" ? host : join(packageRoot, path)]);
let planPath = ""; let reviewCalls = 0; let judgeCalls = 0;
const supervisorModels: string[] = []; const judgeModels: string[] = []; const workerModels: string[] = [];
const server = createServer((req, res) => {
let raw = ""; req.on("data", chunk => { raw += chunk; }); req.on("end", () => {
const body = JSON.parse(raw); const messages = body.messages; const last = messages.at(-1); const text = typeof last.content === "string" ? last.content : JSON.stringify(last.content);
const names = (body.tools ?? []).map((tool: any) => tool.function.name);
const call = (name: string, args: object) => stream(res, { tool_calls: [{ index: 0, id: `call-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } }] }, "tool_calls");
if (last.role === "tool" && messages.at(-2)?.tool_calls?.some((c: any) => c.function.name === "write")) return call("RequestPlanReview", {});
if (last.role === "tool") return stream(res, { content: "Check complete." });
if (names.includes("review_goal")) {
supervisorModels.push(body.model);
const request = text.match(/Goal sign-off request ([^ .]+)\./);
if (request) { reviewCalls++; return call("review_goal", { requestId: request[1], decision: "approve", reason: "This goal remains faithful to the plan." }); }
return call("let_it_run", { reason: "Ready selected; worker starting" });
@@ -32,17 +48,19 @@ describe.skipIf(!source || !existsSync(intercom))("two real Pi sessions with the
if (names.includes("CompleteGoal")) {
if (text.includes("sign off first")) return call("CompleteGoal", { goal: "first" });
if (text.includes("We're in plan mode.") || text.includes("[PLANNING MODE]")) return call("write", { path: planPath, content: "# Plan\n\n## User-visible result\n\nTwo text files.\n\n## Goals\n\n1. [ ] goal: first\n - evidence: evidence.txt says PASS\n2. [ ] goal: second\n\n## Log\n" });
return stream(res, { content: "Worker is ready." });
workerModels.push(body.model); return stream(res, { content: "Worker is ready." });
}
judgeCalls++; stream(res, { content: "## checks:\n- evidence.txt: `PASS`; the saved receipt passed\n\nVERDICT: accept\nmissing:" });
judgeCalls++; judgeModels.push(body.model); stream(res, { content: "## checks:\n- evidence.txt: `PASS`; the saved receipt passed\n\nVERDICT: accept\nmissing:" });
});
});
await new Promise<void>(done => server.listen(0, "127.0.0.1", done));
const address = server.address(); if (!address || typeof address === "string") throw new Error("Offline HTTP server did not start");
const agentDir = join(cwd, ".agent"); mkdirSync(agentDir);
writeFileSync(join(agentDir, "models.json"), JSON.stringify({ providers: { offline: { baseUrl: `http://127.0.0.1:${address.port}`, apiKey: "test", api: "openai-completions", models: [{ id: "test", name: "Offline", reasoning: false, input: ["text"], contextWindow: 200_000, maxTokens: 1000, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } }] } } }));
writeFileSync(join(agentDir, "models.json"), JSON.stringify({ providers: { offline: { baseUrl: `http://127.0.0.1:${address.port}`, apiKey: "test", api: "openai-completions", models: ["test", "planning", "worker", "supervisor", "judge"].map(id => ({ id, name: `Offline ${id}`, reasoning: false, input: ["text"], contextWindow: 200_000, maxTokens: 1000, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } })) } } }));
const modelDir = join(agentDir, "pi-goals"); mkdirSync(modelDir);
for (const role of ["worker", "supervisor"]) writeFileSync(join(modelDir, `${role}-model.json`), JSON.stringify({ provider: "offline", id: role }));
writeFileSync(join(cwd, "evidence.txt"), "PASS\n");
const child = spawn(resolve("node_modules/.bin/pi"), ["--mode", "rpc", "--no-extensions", "--model", "offline/test", "-e", source!, "-e", intercom, "-e", resolve("test/fixtures/herdr-test-host.ts")], {
const child = spawn(resolve("node_modules/.bin/pi"), ["--mode", "rpc", "--no-extensions", "--model", "offline/test", ...resources], {
cwd, env: { ...process.env, PI_CODING_AGENT_DIR: agentDir, PI_INTERCOM_SCOPE_ID: `test-${Date.now()}`, HERDR_ENV: "1", HERDR_PANE_ID: "test-worker-pane", PI_SUPERVISOR_DEBUG: "1" }, stdio: ["pipe", "pipe", "pipe"],
});
const events: any[] = []; let buffer = ""; let stderr = "";
@@ -64,15 +82,24 @@ describe.skipIf(!source || !existsSync(intercom))("two real Pi sessions with the
expect(JSON.stringify(diagnostic), "Actual Intercom must connect before testing goals").toContain("Connected: Yes");
await wait(e => e.type === "agent_end", diagnosticAt);
send({ type: "prompt", id: "plan", message: "/goals plan create the outputs" });
const menu = await wait(e => e.type === "extension_ui_request" && e.method === "select"); const readyAt = events.length;
const menu = await wait(e => e.type === "extension_ui_request" && e.method === "select");
send({ type: "set_model", id: "planning-model", provider: "offline", modelId: "planning" });
expect((await wait(e => e.type === "response" && e.id === "planning-model")).success).toBe(true);
const readyAt = events.length;
send({ type: "extension_ui_response", id: menu.id, value: "Ready" });
await wait(e => e.type === "message_start" && JSON.stringify(e.message).includes("Work the goals"), readyAt);
await wait(e => e.type === "agent_end", readyAt);
send({ type: "prompt", id: "judge-model", message: "/goals judge offline/judge" });
await wait(e => e.type === "response" && e.id === "judge-model");
const signoffAt = events.length;
send({ type: "prompt", id: "signoff", message: "sign off first" });
await wait(e => e.type === "tool_execution_end" && e.toolName === "CompleteGoal", signoffAt);
expect(readFileSync(planPath, "utf8")).toContain("[x] goal: first"); expect(reviewCalls).toBe(1); expect(judgeCalls).toBe(1);
expect(readFileSync(planPath, "utf8")).toContain("[ ] goal: second");
expect(workerModels).toEqual(["worker"]);
expect(supervisorModels.length).toBeGreaterThan(0); expect(supervisorModels.every(model => model === "supervisor")).toBe(true);
expect(judgeModels).toEqual(["judge"]);
for (const role of ["planning", "worker", "supervisor"]) expect(JSON.parse(readFileSync(join(modelDir, `${role}-model.json`), "utf8"))).toEqual({ provider: "offline", id: role });
} finally { send({ type: "abort" }); child.kill(); server.close(); await new Promise(done => child.once("close", done)); rmSync(cwd, { recursive: true, force: true }); }
}, 55_000);
});
+8
View File
@@ -0,0 +1,8 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, vi } from "vitest";
let agentDir: string;
beforeEach(() => { agentDir = mkdtempSync(join(tmpdir(), "goals-model-prefs-")); vi.stubEnv("PI_CODING_AGENT_DIR", agentDir); });
afterEach(() => { rmSync(agentDir, { recursive: true, force: true }); vi.unstubAllEnvs(); });
+88 -13
View File
@@ -1,5 +1,5 @@
import { EventEmitter } from "node:events";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { SessionManager } from "@earendil-works/pi-coding-agent";
@@ -18,28 +18,28 @@ vi.mock("node:child_process", async (original) => {
} };
});
const supervisorSource = process.env.PI_GOALS_SUPERVISOR_SOURCE;
const tick = () => new Promise(resolve => setImmediate(resolve));
afterEach(() => { vi.unstubAllEnvs(); judge.calls = []; });
describe.skipIf(!supervisorSource)("actual goals and supervisor package hooks (Herdr and judge mocked)", () => {
it.each(["completion", "replacement during activation", "steward off during activation"])("Ready forks once and preserves lifecycle ownership: %s", async (scenario) => {
const supervise = (await import(/* @vite-ignore */ supervisorSource!)).default;
describe("actual goals and supervisor package hooks (Herdr and judge mocked)", () => {
it.each(["completion", "replacement during activation", "steward off during activation", "missing worker model", "unauthenticated worker model", "clear while model unavailable", "off while model unavailable", "clear during recovery restore", "replacement during recovery restore"])("Ready forks once and preserves lifecycle ownership: %s", async (scenario) => {
const cwd = mkdtempSync(join(tmpdir(), "goals-supervisor-integration-"));
const peers: any[] = [];
const wires: any[] = [];
const herdrCalls: string[][] = [];
const modelDir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals"); mkdirSync(modelDir);
for (const role of ["worker", "supervisor"]) writeFileSync(join(modelDir, `${role}-model.json`), JSON.stringify({ provider: "offline", id: role }));
vi.stubEnv("HERDR_ENV", "1"); vi.stubEnv("HERDR_PANE_ID", "w1:p1");
function make(id: string, manager: SessionManager) {
const hooks = new Map<string, any[]>(); const commands = new Map<string, any>(); const tools = new Map<string, any>(); const listeners = new Map<string, Set<any>>();
const messages: string[] = []; const contexts: any[] = []; let active = ["read", "grep", "bash", "write", "edit"];
const peer: any = { id, manager, messages, contexts, commands, tools, compactions: 0, aborts: 0 };
const peer: any = { id, manager, messages, contexts, commands, tools, compactions: 0, compactionModels: [], modelChanges: [], aborts: 0 };
const bus = {
on(name: string, fn: any) { const list = listeners.get(name) ?? new Set(); list.add(fn); listeners.set(name, list); return () => list.delete(fn); },
emit(name: string, payload: any) {
if (name === "intercom:extension-register") {
peer.receive = payload.onEvent;
payload.onReady({ snapshot: () => ({ connected: true, supported: true }), listSessions: async () => peers.map(p => ({ id: p.id, pid: p === peer ? process.pid : process.pid + 1, cwd, model: "offline/test" })), publish(wire: any) { wires.push(wire); for (const p of peers) queueMicrotask(() => p.receive({ type: "message", fromSessionId: peer.id, payload: wire })); } });
payload.onReady({ snapshot: () => ({ connected: true, supported: true }), listSessions: async () => peers.map(p => ({ id: p.id, pid: p === peer ? process.pid : process.pid + 1, cwd, model: "offline/test" })), publish(wire: any) { if (wire.t === "plan_activate") expect(peer.ctx.model.id).not.toBe("planning"); wires.push(wire); for (const p of peers) queueMicrotask(() => p.receive({ type: "message", fromSessionId: peer.id, payload: wire })); } });
return true;
}
if (name === "processes:request:list") payload.reply([]);
@@ -52,11 +52,21 @@ describe.skipIf(!supervisorSource)("actual goals and supervisor package hooks (H
},
};
const pi: any = {
setModel: async (model: any) => {
if (peer.noAuth && model.id === "worker") return false;
if (peer.deferWorkerRestore && model.id === "worker") {
peer.deferWorkerRestore = false;
await new Promise<void>(resolve => { peer.finishRestore = resolve; });
}
const previousModel = ctx.model; ctx.model = model; peer.modelChanges.push(model.id);
manager.appendModelChange(model.provider, model.id);
await peer.hook("model_select", { source: "set", model, previousModel }); return true;
},
events: bus, on(name: string, fn: any) { hooks.set(name, [...(hooks.get(name) ?? []), fn]); },
registerCommand(name: string, command: any) { commands.set(name, command); }, registerTool(tool: any) { tools.set(tool.name, tool); active.push(tool.name); },
appendEntry: (name: string, data: any) => manager.appendCustomEntry(name, data),
getActiveTools: () => active, setActiveTools: (names: string[]) => { active = names; },
getCommands: () => [{ name: "supervise", sourceInfo: { path: supervisorSource } }],
getCommands: () => [{ name: "supervise", sourceInfo: { path: "internal-supervisor" } }],
getAllTools: () => [{ name: "subagent" }, { name: "intercom", sourceInfo: { path: "intercom-test-only" } }],
sendUserMessage: (text: string) => messages.push(text), sendMessage: (message: any) => contexts.push(message),
exec: async (command: string, args: string[]) => {
@@ -70,12 +80,13 @@ describe.skipIf(!supervisorSource)("actual goals and supervisor package hooks (H
return { code: 0, stdout: "{}", stderr: "" };
},
};
const ctx: any = { cwd, hasUI: true, isIdle: () => true, model: { provider: "offline", id: "test", contextWindow: 200_000 }, sessionManager: manager,
getContextUsage: () => ({ tokens: 50_000 }), compact({ onComplete }: any) { peer.compactions++; onComplete({}); }, abort() { peer.aborts++; },
const ctx: any = { cwd, hasUI: true, isIdle: () => true, model: { provider: "offline", id: (manager.getBranch().findLast((entry: any) => entry.type === "model_change") as any)?.modelId ?? "test", contextWindow: 200_000 }, sessionManager: manager,
modelRegistry: { find: (provider: string, id: string) => peer.missing && id === "worker" ? undefined : ({ provider, id, contextWindow: 200_000 }) },
getContextUsage: () => ({ tokens: 50_000 }), compact({ onComplete }: any) { peer.compactions++; peer.compactionModels.push(ctx.model.id); onComplete({}); }, abort() { peer.aborts++; },
ui: { theme: { fg: (_: string, text: string) => text }, setWidget() {}, setStatus() {}, notify: vi.fn(), select: async () => "Ready" },
};
peer.pi = pi; peer.ctx = ctx; peer.hook = async (name: string, event = {}) => { for (const fn of hooks.get(name) ?? []) await fn(event, ctx); };
peers.push(peer); supervise(pi); goals(pi); return peer;
peers.push(peer); goals(pi); return peer;
}
const manager = SessionManager.create(cwd, join(cwd, "sessions"));
manager.appendMessage({ role: "user", content: "Keep the literal [x] and produce two files", timestamp: Date.now() });
@@ -84,9 +95,15 @@ describe.skipIf(!supervisorSource)("actual goals and supervisor package hooks (H
try {
await worker.hook("session_start");
await worker.commands.get("goals").handler("plan judge the vendor options", worker.ctx);
worker.ctx.model = { provider: "offline", id: "planning", contextWindow: 200_000 };
manager.appendModelChange("offline", "planning");
await worker.hook("model_select", { source: "cycle", model: worker.ctx.model });
const path = join(cwd, ".pi", "plan", `${manager.getSessionId()}-v1.md`);
writeFileSync(path, '# Plan\n\nUser voice: render "[x]" literally\n\n1. [ ] goal: first\n2. [ ] goal: second\n\n## Log\n');
worker.delayBackground = scenario !== "completion";
worker.delayBackground = scenario.includes("during activation");
worker.missing = scenario === "missing worker model" || scenario.includes("during recovery restore");
worker.noAuth = scenario === "unauthenticated worker model";
await worker.tools.get("RequestPlanReview").execute("", {}, undefined, undefined, worker.ctx);
const starting = worker.hook("agent_settled");
if (worker.delayBackground) {
await vi.waitFor(() => expect(worker.finishBackground).toBeTypeOf("function"));
@@ -95,6 +112,8 @@ describe.skipIf(!supervisorSource)("actual goals and supervisor package hooks (H
await worker.commands.get("goals").handler(stopping ? "steward off" : "plan a replacement that has not received Ready", worker.ctx);
worker.finishBackground(); await starting; await tick();
expect(worker.messages.filter((m: string) => m.startsWith("Work the goals"))).toHaveLength(0);
expect(worker.ctx.model.id).toBe("planning");
expect(worker.modelChanges).toContain("worker");
const saved = manager.getBranch().findLast((entry: any) => entry.customType === "pi-goals-state") as any;
expect(saved.data).toMatchObject({ phase: "planning", planVersion: stopping ? 1 : 2, supervisor: null });
if (stopping) {
@@ -110,11 +129,65 @@ describe.skipIf(!supervisorSource)("actual goals and supervisor package hooks (H
}
await starting; await tick();
const supervisor = peers[1];
if (worker.missing || worker.noAuth) {
const saved = manager.getBranch().findLast((entry: any) => entry.customType === "pi-goals-state") as any;
expect(saved.data).toMatchObject({ phase: "planning", modelRecovery: "worker" });
expect(wires.some(w => w.t === "plan_activate" || w.t === "view")).toBe(false);
expect(supervisor.messages).toEqual([]);
expect(worker.messages.some((m: string) => m.startsWith("Work the goals"))).toBe(false);
expect(worker.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("worker model paused"), "error");
await worker.hook("session_start", { reason: "reload" });
expect(worker.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("/goals model current"), "error");
const before = worker.messages.length;
worker.receive({ type: "message", fromSessionId: supervisor.id, payload: { t: "directive", to: worker.id, text: "Do not revive an inactive worker" } });
await tick(); expect(worker.messages).toHaveLength(before);
worker.missing = false; worker.noAuth = false;
if (scenario.includes("during recovery restore")) {
worker.deferWorkerRestore = true;
const startupCount = herdrCalls.length;
const recovering = worker.hook("agent_settled");
await vi.waitFor(() => expect(worker.finishRestore).toBeTypeOf("function"));
const clearing = scenario.startsWith("clear");
await worker.commands.get("goals").handler(clearing ? "clear" : "plan replacement has no Ready", worker.ctx);
worker.finishRestore(); await recovering; await tick();
expect(herdrCalls).toHaveLength(startupCount);
expect(wires.some(w => w.t === "plan_activate")).toBe(false);
expect(worker.messages.some((m: string) => m.startsWith("Work the goals"))).toBe(false);
expect(manager.getBranch().findLast((entry: any) => entry.customType === "pi-goals-state").data).toMatchObject({ phase: clearing ? null : "planning", planVersion: clearing ? null : 2, supervisor: null, modelRecovery: null });
expect(worker.ctx.model.id).toBe("planning");
return;
}
await worker.pi.setModel({ provider: "offline", id: "recovered-worker", contextWindow: 200_000 });
await vi.waitFor(() => expect(worker.messages.filter((m: string) => m.startsWith("Work the goals"))).toHaveLength(1));
expect(JSON.parse(readFileSync(join(modelDir, "worker-model.json"), "utf8"))).toEqual({ provider: "offline", id: "recovered-worker" });
expect(JSON.parse(readFileSync(join(modelDir, "planning-model.json"), "utf8"))).toEqual({ provider: "offline", id: "planning" });
expect(wires.filter(w => w.t === "plan_activate")).toHaveLength(1);
expect(herdrCalls.filter(args => args[1] === "start")).toHaveLength(1);
return;
}
if (scenario.includes("while model unavailable")) {
worker.noAuth = true;
await worker.hook("session_start", { reason: "reload" });
await worker.commands.get("goals").handler(scenario.startsWith("clear") ? "clear" : "steward off", worker.ctx);
await tick();
for (const peer of peers) expect(peer.manager.getBranch().findLast((e: any) => e.customType === "supervise-state").data.role).toBe("none");
const before = worker.messages.length;
worker.receive({ type: "message", fromSessionId: supervisor.id, payload: { t: "directive", to: worker.id, text: "An old directive must not revive work" } });
await tick(); expect(worker.messages).toHaveLength(before);
expect(supervisor.aborts).toBeGreaterThan(0);
return;
}
expect(supervisor).toBeDefined(); expect(supervisor.manager.getSessionFile()).not.toBe(manager.getSessionFile());
expect(supervisor.manager.getBranch().some((e: any) => e.customType === SUPERVISOR_ROLE)).toBe(true);
expect(supervisor.compactionModels).toEqual(["supervisor"]);
expect(supervisor.manager.getBranch().some((entry: any) => entry.type === "model_change" && entry.modelId === "worker")).toBe(false);
expect(supervisor.manager.getBranch().some((entry: any) => entry.type === "model_change" && entry.modelId === "planning")).toBe(true);
expect(worker.ctx.model.id).toBe("worker");
expect(JSON.parse(readFileSync(join(modelDir, "planning-model.json"), "utf8"))).toEqual({ provider: "offline", id: "planning" });
expect(supervisor.compactions).toBe(1); expect(worker.compactions).toBe(0);
expect(worker.messages.filter((m: string) => m.startsWith("Work the goals"))).toHaveLength(1);
expect(supervisor.pi.getActiveTools()).not.toContain("CompleteGoal");
await worker.commands.get("goals").handler("judge offline/judge", worker.ctx);
for (const goal of ["first", "second"]) {
const completion = worker.tools.get("CompleteGoal").execute("", { goal }, undefined, undefined, worker.ctx);
await tick(); const request = wires.findLast((w: any) => w.t === "goal_review");
@@ -124,7 +197,9 @@ describe.skipIf(!supervisorSource)("actual goals and supervisor package hooks (H
expect(completed.isError, JSON.stringify(completed)).toBe(false);
expect(readFileSync(path, "utf8")).toContain(`[x] goal: ${goal}`);
}
expect(judge.calls).toHaveLength(2); expect(judge.calls.every(args => args.includes("--no-extensions"))).toBe(true);
expect(judge.calls).toHaveLength(2); expect(judge.calls.every(args => args.includes("--no-extensions") && args.includes("offline/judge"))).toBe(true);
expect(worker.ctx.model.id).toBe("worker");
expect(supervisor.ctx.model.id).toBe("supervisor");
expect(wires.some(w => w.t === "done")).toBe(false);
await worker.commands.get("goals").handler("supervisor", worker.ctx);
expect(herdrCalls.at(-1)).toEqual(["agent", "focus", "w1:p2"]);
+1 -1
View File
@@ -7,7 +7,7 @@ afterEach(() => vi.unstubAllEnvs());
describe("supervisor adapter", () => {
it("fails visibly when the supervisor API is absent", async () => {
const pi = { events: { emit() {} } } as unknown as ExtensionAPI;
await expect(supervisorRequest(pi, "status")).rejects.toThrow("Load the plan-aware");
await expect(supervisorRequest(pi, "status")).rejects.toThrow("Load the pi-goals package directory");
});
it("does not operate on a live Herdr session from outside Herdr", async () => {
vi.stubEnv("HERDR_ENV", "");