1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
|
# newOS4 changelog
Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers.
<!-- generated:start -->
## v4.6.155 — 2026-11-07
_Touches modules (10), CHANGELOG.md (1), main.html (1) · fix 1_
- **Second review round — identity for every stock node, load dedup, sweep race, a11y** `0acee7d`
- Four models (kimi-k2.7-code, deepseek-v4-pro, minimax-m3, glm-5.3) reviewed the shipped change set. Review log with every accepted and rejected finding: .hermes/plans/2026-11-07_163420-newos4-six-ui-modes.review-log.md
- identity guard now covers every snapshot node, not just #terminal-container: terminal.js also
- binds #output, #command-input, #input-area and #status-bar, so a cloned one leaves the terminal writing into a detached node while the page looks perfect
- _restoreStock anchors on the next stock node already home, then on the recorded trailing
- sibling; a raw child index was measured wrong (terminal,input,status-bar,quick-actions)
- _loading map: a double-clicked card or a repeated 'ui easy' loads the mode once
- _sweepPending returns while that id is in flight instead of declaring failure at 6 s and
- overwriting the stored preference
- … run `git show <sha>` for the rest
## v4.6.153 — 2026-11-07
_Touches CHANGELOG.md (1), main.html (1), modules (1) · project 1_
- **V4.6.153 — cache-bust the review fixes** `13271e5`
## v4.6.152 — 2026-11-07
_Touches modules (18), CHANGELOG.md (2), README.md (1), help.html (1) · docs 2, fix 2, release 1_
- **Six interface modes (easy -> cockpit) over one terminal** `5dafaa4`
- Symptom: newOS4 had exactly one layout. A first-time user had no way in that did not require learning commands, and an operator had no surface showing live runtime state; the only way to see heap/VFS/tool-call telemetry was to type cat /sys/...
- Root cause: no UI layer existed. main.html hard-codes one flex column, and terminal.js binds the stock nodes by id at construction, so an alternative layout had to either duplicate the output stream or reparent the real nodes.
- Fix:
- modules/core/module-uishell.js (new): mode registry, #ui-root, stock-node
- snapshot/restore, identity check that reverts to classic when a mode removes or clones #terminal-container, shell-docked #ui-foot (the AI stop button exists in every mode), ui command, gated first-run picker, ?ui= honoured only if the mode actually registers, lazy mode loading via the same loader ext modules use.
- … run `git show <sha>` for the rest
- **V4.6.152 changelog + change-history row for the six modes** `38e75c5`
- **Mode layout constraints, capture() arg split, suppress classic welcome modal** `61ba8b5`
- #ui-root is a flex column (it was display:block, so every mode's flex:1 was inert and the
- terminal collapsed to 0px while the composer sat at the bottom of a 4600px flow)
- overflow:clip on #ui-root and body: overflow:hidden is still a scroll container, and the
- terminal's scrollIntoView() on new output scrolled the mode's own chrome off-screen
- terminal min-height 100px: in the tabbed/panel modes its grid parent is not a flex parent
- api.capture() split arguments itself; term._parseArgs does not exist, so every panel showed
- 'unavailable: term._parseArgs is not a function'
- the classic Let's Get Started modal is suppressed in non-classic modes: it covered easy's cards
- **Rebuild changelog for v4.6.152** `5d9e037`
- **Four-model review — lazy-load race, restore order, a11y, key handling** `fcd58a4`
- Reviewed the shipped change set with kimi-k2.7-code, deepseek-v4-pro, glm-5.3 and minimax-m3. Findings below are the ones that survived verification against file:line; the rest are in .hermes/plans/2026-11-07_163420-newos4-six-ui-modes.review-log.md
- Accepted and fixed:
- apply() had no generation token: a lazy mode load that finished after the user switched to
- another mode force-applied the abandoned one (`apply(want,{force:true})` in the .then). Every apply() now bumps `_loadSeq` and the callback acts only while its token is newest. Same token also removes a double mount: registerMode() already applies the mode as it registers, so the callback was mounting every lazy mode twice (teardown+cleanup twice).
- _restoreStock() used the recorded `nextSibling` as insert anchor. Every real mode moves
- … run `git show <sha>` for the rest
## v4.6.151 — 2026-11-07
_Touches modules (5), main.html (1), help.html (1) · project 1, fix 1_
- **V4.6.151 — wget that actually saves, web_fetch that actually pages** `f84a82d`
- S1 (reported by agent-os4-maint-01, wrongly refuted by the chat auto-reply): wget existed but was unusable and dishonest.
- Cause 1: _cmdWget used the page's own fetch(url), so every cross-origin URL
- died on CORS ("Failed to fetch"). Measured in the served v4.6.150 page: fetch(cgit README) threw, /api/fetch returned 200/1954 bytes.
- Cause 2: _vfsWriteBytes ignored writeFile()'s boolean, so the command printed
- a green tick when the target directory did not exist. Measured: fs.writeFile('nosuchdir/x.txt') === false.
- Cause 3: bytes were round-tripped through String.fromCharCode, mangling UTF-8.
- Fix: fetch through /api/fetch (raw source), mkdir -p, writeFile + read-back
- verification, error instead of a tick on failure. Register curl with -s/-o parsing delegating to the same writer so the two cannot drift.
- … run `git show <sha>` for the rest
- **Document curl in help command table (auto)** `f8e4c51`
- Plan: .hermes/plans/2026-11-07_061825-newos4-curl-help-row.md (T1) Root cause: curl shipped in v4.6.151 (cryptogzip.js:19 getHelpInfo, :612 commands['curl']) but help.html's command table was never updated, so check_help_coverage() reported 365/366.
- Change: one <tr> row added after the wget row in help.html's command table. No JS/PHP touched; cryptogzip.js, modules-index.json and atlas unchanged as specified in T2.
- Verification: grep -c '<code>curl ' help.html -> 1 wget row at 1086, curl row at 1087 -> adjacent grep -c "_isBase64" modules/ext/cryptogzip.js -> 0 (unchanged) git diff --stat -> help.html | 1 + python3 scripts/newos4-maintain.py -> PASS: help coverage 366/366, maintenance result: OK, needs OK: none HTTP help.html -> 200
## v4.6.150 — 2026-11-06
_Touches modules (7), main.html (1), CHANGELOG.md (1) · project 2_
- **Commit the module work that has been live but uncommitted** `6bea062`
- Operator commit-approve, 2026-11-06. These changes have been served since 2026-11-02/03 while git HEAD still stamped v4.6.146, so the mirror published a tree four builds behind the running instance and the release tag named the wrong build.
- … run `git show <sha>` for the rest
- **Regenerate for v4.6.150** `10e2ac2`
## v4.6.146 — 2026-11-06
_Touches docs (11), CHANGELOG.md (4), modules (3), README.md (3) · docs 10, fix 4, release 3_
- …and 5 earlier commit(s) in this build
- **Drop hardcoded file/commit counts from the usage page** `d2bb158`
- 310 files / 182 commits were already wrong at publish time (184). A count that changes on every publish is a rot vector; the page now says what the tree is and lets the reader run the commands.
- **No byte count either — it grows with every publish** `a24ec6b`
- **Usage page fits a 390 px screen** `373f6ca`
- Two measured overflows, both structural: a grid item defaults to min-width:auto so one long command stretched the column to 521 px, and the troubleshooting table had no fixed layout so a column of long code strings pushed the page 75 px wide. minmax(0,1fr) + min-width:0 + table-layout:fixed + overflow-wrap:anywhere. verify-mobile: 4/4 viewports OK.
- **Last stale count on the usage page (181 commits)** `bc4d180`
- **Automating with the pi agent (recipes verified against pi 0.80.3)** `83cb1c7`
- Four recipes on the usage page: one-shot @file analysis, --mode json + jq for scripts, a read-only --tools allowlist for tree inspection, and a cron-able digest. Every flag shown was exercised on this host first; the retired-default model gotcha (HTTP 410 'was retired') is documented because it wasted a run. Adds the untrusted-input warning: pi auto-loads AGENTS.md/skills from the working directory, so a public clone must run with --no-approve and no write tools.
- **Install pi + point it at Ollama; fix dead /about/ link** `d99962d`
- Install is npm (node>=22.19). Ollama wiring is two files under ~/.pi/agent: models.json (baseUrl http://localhost:11434/v1, api openai-completions, model list) and settings.json (defaultProvider/defaultModel). Documents the trap that a pulled model must also be declared — undeclared gives 'Model not found', which reads like a connection failure. Both paths verified on this host: declared coder:latest -> 'LOCAL OK', undeclared llama3.2:latest -> error.
- The two /about/ links pointed at the host root, where nothing exists; the repo page is /newos4/about/.
- **Reconcile pi version (verified on 0.80.3, npm ships 1.5.0)** `5fdeb6d`
- **Contrib push surface with automated moderation** `e90dc38`
- Users push to push.newos.webx11.duckdns.org with a per-user token; the read-only host is untouched. http.receivepack is unset so git only allows receive-pack when REMOTE_USER is present, which only that vhost sets. A pre-receive hook (installed by the publisher every run) allows refs/heads/contrib/* only and refuses banned paths, credential-shaped blobs, >200 files or >2MB, logging user+ref+decision.
- moderate-contrib.py triages every 10 min: deletes hard violations, stamps the rest ready-docs/review/stale into a queue rendered at /admin/git-contrib.html. A human still decides what merges.
- Verified end to end: contrib push accepted, main refused, credential file refused, accepted ref visible on the read-only host; guard extended (push host 401, hook present+executable, receivepack unset) and proven RED by removing the hook.
- **Namespace contrib refs per account (contrib/<user>/<topic>)** `4acb065`
- Done before anyone has a token or a branch, because renaming later breaks every contributor. The hook compares the middle path segment to REMOTE_USER, so a token can only write under its own namespace; deletion and cleanup become a prefix operation instead of a guess.
- Verified 4/4: own namespace accepted, neighbour's namespace refused naming the authenticated user, missing owner segment refused with the correct form, main refused. Test branch and token removed afterwards; queue back to 0.
- **Self-service git push tokens from the dashboard** `5b55af9`
- GET/POST /api/auth/gitpush-token (+ /revoke) on auth-api: session-scoped, CSRF-gated, 3 mints per account per hour, bcrypt into the htpasswd, plaintext returned once and never stored, audit at /var/lib/webx11/git-push-audit.jsonl.
- No sudo or reload needed: nginx re-opens auth_basic_user_file per request, so a new line is live immediately — proved by a 500 whose error line was per request.
- Dashboard card 'Git push token' shows none/active, mints and reveals the remote once, revokes. Verified in a real browser: badge none -> mint -> active, remote shape checked, then revoked; htpasswd back to 0 bytes and the test session removed.
- **Remove published dashboard fixtures from the space** `0fe3af9`
- Symptom: an outside review of the public mirror reported trade-history.json and wallet-snapshot.json at repo root as personal financial records, contradicting README's promise that personal data is absent by design.
- Cause: they are fixtures, not records (trade-history.json:60 "Sample data for dashboard rendering", wallet-snapshot.json:13 source=manual-baseline, address 0xA12b…3d4 synthetic), committed by an auto-commit in July and left at the space root. Nothing in the tree ever referenced either path (grepped the whole web root, 0 hits), and both answered HTTP 200 unauthenticated at /spaces/newos4/<file>.json — so a reader could only take them at face value.
- Fix: deleted both files, and admin/scripts/publish-newos4.py now lists them in EXCLUDE_GLOBS, which strips them from the mirror's rewritten history too (a tip deletion alone would leave them in every earlier commit).
- … run `git show <sha>` for the rest
- **Render the commit body, not just the subject** `c759c50`
- Symptom: the mirrored CHANGELOG.md (and the page) showed one-line commit summaries while README promises "every shipped fix, symptom -> cause -> fix -> verified". A reviewer measured the gap from the public mirror and could only conclude the claim overpromised.
- Cause: scripts/build-changelog.py:59 formatted commits with %s only; %b was never read, so the verification prose every fix commit carries (5b55af9 is a typical one) was discarded before rendering. The MD writer then replaced every tag with a backtick, so nothing richer could have survived anyway.
- … run `git show <sha>` for the rest
- **Keep the newest commits of a build; wrap long detail URLs** `9751798`
- Symptom: after bodies were rendered, this fix's own commit was missing from its own entry, and a long URL inside an expanded detail was clipped at the right edge on a 390 px screen.
- Cause: items took group[:MAX_ITEMS] from a CHRONOLOGICAL list, so a 17-commit build showed its first 14 and dropped the newest three (the count line said "…and 3 more", hiding which three). And the injected detail CSS set no overflow-wrap, so a 47-character URL pushed the paragraph to 320 px of unbreakable text — the document stayed at 0 overflow only because the li clipped it.
- … run `git show <sha>` for the rest
- **Regenerate after the render and ordering fixes** `2feefc0`
## v4.6.145 — 2026-11-02
_Touches modules (2), main.html (1) · fix 1_
- **Stop offering a Telegram widget where it cannot work** `532e2ff`
- Two hosts cannot complete the widget flow and both are in use: an Android WebView / in-app browser (the operator's phone UA ends "; wv)") and a desktop browser that blocks the popup or hands the link to the Telegram app. The widget needs a popup that can postMessage back into the opener; in those hosts the link lands in a browser tab or in Chrome with no opener, onTelegramAuth never fires, and the login card looks dead — "it opens my default browser" (reported twice: phone, then Max's laptop).
- modules/wx11-tg-webview.js (new) WX11TG.isWebView() -> "; wv)", Telegram in-app, Meta in-app UAs WX11TG.dropWidgets()-> removes already-rendered oauth.telegram.org iframes/scripts
- … run `git show <sha>` for the rest
## v4.6.144 — 2026-11-02
_Touches modules (3), main.html (1) · fix 1_
- **One session writer for every login path; expire dead credentials** `2421eb4`
- Third occurrence of the same class of bug, and the first fix is why: commit e95b7be (2026-11-01, "multi-session .newsession v3") touched exactly ONE hunk of auth-file-api.py — handle_login. handle_device_poll, handle_telegram_login and handle_register kept overwriting the whole .newsession with a single-session v2 payload, so a phone sign-in logged the desktop out and the loser held a session id no store knew: permanent `401 Invalid session or API key` (newOS4 asked for a login it could never complete). Measured: agando's store held 1 entry (sha8 45cb7819) while the phone sent 9ac607f1 for 3 h; 26 device-polls in 2 s from one page minted 26 cookies and only the last write survived, which also drained the shared nginx `webx11` zone (11x 503 on /admin/api/*).
- api/webx11_auth.py
- add_session() is now THE writer: v3 `sessions[]` (cap 5) under a per-user
- … run `git show <sha>` for the rest
## v4.6.143 — 2026-11-02
_Touches modules (6), main.html (1) · fix 1_
- **Read_file pages were silently truncated; delegation no longer invents "completed"** `b380ec8`
- Two defects found by re-deriving a report that claimed both were still open.
- read_file paging (module-aiassist.js)
- A page larger than the result clip was clipped AFTER read_file had computed
- next_offset. On the last page next_offset is null, so the model was told truncated:false while its tail had been removed: measured 11 890 of 19 999 chars delivered, returned:19999, no pointer to the rest - 8 109 chars lost with no way to ask for them.
- the clip now sets truncated=true and next_offset=offset+keep when it removes
- the tail of a page that had no continuation (and never splits a surrogate pair)
- read_file caps a page at the result budget (min(pageCap, toolResultChars-200)),
- … run `git show <sha>` for the rest
## v4.6.141 — 2026-11-02
_Touches modules (7), main.html (1) · fix 1_
- **Memory-graph loads stop rewriting user data; unknown sizes say unknown** `8eaf114`
- Three follow-ups from the merge review, each with the assertion that pins it.
- module-memory-graph.js
- _normalizeEdges no longer mutates stored weights. It capped legacy 'failed' edges
- from 0.8 to 0.2 in memory, and the next debounced save made that permanent - an irreversible migration of the user's graph with no schema version. The cap moved to _effectiveWeight(), which ranking (showStats "Strongest relationships"), the session view and prune() now use; the stored record is left exactly as written.
- config.json is stamped version: 2 so a future format change can migrate deliberately.
- ext/webdav.js
- the two fallback parsers emit size: null instead of 0. They cannot see the size
- column, and 0 rendered as "0 B" - the same "every file looks empty" symptom as the parseInt bug fixed in e639c2d, one regex miss away.
- … run `git show <sha>` for the rest
## v4.6.140 — 2026-11-02
_Touches modules (6), main.html (1) · fix 1_
- **WebDAV listing sizes + pull, and five MemoryGraph defects** `e639c2d`
- Closes STEER-20261102-205416-facd and STEER-20261102-204031-f82a.
- WebDAV (modules/ext/webdav.js):
- listings showed "21 B" for a 21 KB file: nginx emits humanised sizes
- (autoindex_exact_size off, vhost :412) and _parseAutoindex did parseInt("21K"). _parseAutoindexSize() handles K/M/G, decimals, lowercase and "-"; PROPFIND's exact getcontentlength path is untouched.
- webdav-pull into a missing local directory printed "Pulled: ... (N bytes)" and
- wrote nothing: it ignored fs.writeFile's false return (virtual-fs.js:561). It now creates the parent chain, checks the write, and only reports success after a confirmed write. The byte count uses TextEncoder, not .length (a Cyrillic file under-reported ~2x).
- _formatSize gained the GB branch the parser already accepted (3 GB printed
- "3072.0 MB") and no longer prints "NaN MB" for a missing size.
- … run `git show <sha>` for the rest
## v4.6.139 — 2026-11-02
_Touches modules (4), main.html (1) · fix 1_
- **Delegated agent runs no longer serialise; a waiter can't steal a lock** `7491c34`
- Closes the second half of STEER-20261102-201045-ab08.
- chat() gated the main-turn lock on nothing, so orchestrate-parallel's fan-out
- (each sub-task calling chat() on the same instance with its own agentTag) ran strictly one at a time. Measured 2x300ms delegated runs: 617ms before, 306ms now; the two 200ms user turns still take 400ms (main path stays serialised).
- the 10s lock timeout called this._chatLock = false and printed "force
- released", which cleared a lock the waiter did not own - that released the main turn's lock while it was still running. A waiter now throws and the owner is the only writer.
- delegated runs no longer clear the user's cancel flag on entry (an agent start
- used to reset this._cancelled, disarming a Stop the user had pressed).
- tool-log entries carry the run they belong to and _recentToolLog(tag, n) is
- … run `git show <sha>` for the rest
## v4.6.138 — 2026-11-02
_Touches modules (4), main.html (1) · fix 1, review 1_
- **Chat() threw on every tool-using turn and never returned its answer** `8a9333b`
- Regression from 32608bc, caught by the .chat() caller sweep before it aged:
- `recent` was referenced in chat()'s finally block but never declared, so any
- turn with toolTurns > 0 threw ReferenceError there and chat() REJECTED. delegateTask's catch turned that into "agent failed"; the 'completed' fallback was only ever reachable on tool-free turns.
- chat() still had no return at all, so a tool-free turn resolved undefined and
- module-orchestrator.js:1250 fell through to `|| 'completed'`.
- Both fixed: `const recent` is now built from _toolCallLog, and chat() returns { content: finalAnswer, toolTurns } before the archive block (a return at the end of the method would let _archiveSession swallow the answer).
- … run `git show <sha>` for the rest
- **Guard chat()'s return and tool-turn survival** `af28b5b`
- The regression in 32608bc shipped because nothing exercised chat() itself. test-chat-return.js runs 7 assertions against the real module: a tool-using turn must not throw, must return {content, toolTurns}, the orchestrator projection must keep the answer, a tool-free turn must return too, the lock must be released after a failed turn, and a second turn must return its own answer.
- Green 7/7 on 8a9333b; against 32608bc it exits 1 (ReferenceError: recent). Auto-discovered by newos4-maintain (23/23 node tests).
## v4.6.137 — 2026-11-02
_Touches modules (6), help.html (1), main.html (1), modules-index.json (1) · release 1_
- **Configurable tool budgets, per-model defaults, real tool telemetry** `32608bc`
- Closes STEER-20261102-201234-7200 (read_file capped at 6000 while the schema promised 32000).
- Budgets (module-aiassist.js + new modules/core/model-profiles.js):
- one resolver, precedence: call arg -> loop profile -> model profile ->
- ai-config -> code default. Keys: read-page-chars, read-result-chars, tool-context-budget, fetch-chars.
- model classes tiny/small/mid/large keyed by id then by size token; `mid`
- reproduces the old numbers so nothing moves for a mid model.
- _clipToolResult now clips the `content` field, never the serialised JSON
- envelope: clipping the envelope deleted truncated/next_offset, so a 16k page lost its continuation pointer (the reported symptom).
- … run `git show <sha>` for the rest
## v4.6.136 — 2026-11-02
_Touches scripts (4), modules (3), main.html (1) · release 4_
- **No model-class gate; free allowance becomes $0.10/day** `a7cbc19`
- Operator policy 2026-11-01: every subscription may call any model, and the free tier resets daily.
- Model gate removed (api/ai-gateway.py) `plan == 'free' and model_weight > 1 -> 403 "Advanced models require Pro"` ran before any money logic, so a free-plan account holding $1086 of purchased credit was refused and told to upgrade. A model tier is not something we sell: the allowance and the USD balance bound spend. A caller who cannot pay now gets the honest 429 (with period + needed amount). model_weight survives only for track_usage() analytics.
- Free allowance: $0.50/month -> $0.10/day ~$3/month retail = $2.14/month of provider cost at 1.4x markup, ~6x the previous giveaway and still bounded per member (the 50-slot invite cap bounds the total; at full occupancy ~$107/mo or $2.14/user at 100% utilisation).
- The window is policy, not code:
- … run `git show <sha>` for the rest
- **R5 stale-artefact rule + dashboard queues a decision once** `89910fd`
- Feedback queue triage 2026-11-02: 23 reports, 22 decided, 1 open -- and the open one (RPT-20261029-115241, "[Terminal] Error in ShellModule.test: {\"silent\":true}") was already fixed in v4.6.135 (4472c81, modules/core/terminal.js:1966) yet could never be cleared by automation.
- Root cause: identifiers() keeps only generic class names (Terminal, ShellModule), both of which necessarily stay in the code, so R2 (every identifier absent) cannot fire; R4 needs a stack and this report has none. A stack-less "Class.method: {json}" console.error whose emitter gets *guarded* -- rather than deleted -- therefore sits "awaiting decision" forever.
- newos4-feedback-autoreview.py: new R5 stale artefact -- auto report, no stack,
- … run `git show <sha>` for the rest
- **Ingest report-like chat messages into the feedback queue** `b7fef93`
- The chat plane (api/chat/post.php -> data/chat/*.json) was read by nothing in the feedback pipeline: 22 report-like messages over 11 days produced 0 queue items. This adds scripts/newos4-chat-ingest.py - a read-only scanner by default, --apply writes RPT-* files in the report.php schema with diagnostics.auto=false so the auto-reviewer leaves them for a human, deduped on (thread, msg_id).
- First apply filed 5 items (2 x 2026-10-28, 3 x 2026-11-02); snapshot went reports_total 23 -> 28, awaiting_decision 0 -> 5. Second run is silent.
- **Chat plane on the dashboard + honest triage ranking** `9f0b042`
- Tasks 3-8 of the chat-plane plan:
- cron newos4-chat-ingest (0cc4bb4d2f82, */10, script-only, silent when idle)
- collect.py: plan_key() normalises the two ledger spellings on read (5 rows
- carried a plan name without .md and were silently unmatched); per-item bulk_pass for the 2026-10-26 pass; counts.chat_total
- feedback.html: 5th tile "from chat"; card shows who closed an item and when,
- with a bulk marker
- triage: UNFILEABLE class -> top_real now ranks actionable errors only
- (first row went from an 18 203 fetch failure to the 989 toLowerCase runtime error); network/abort signatures move to policy_noise
- … run `git show <sha>` for the rest
## v4.6.135 — 2026-10-30
_Touches modules (6), data (6), course (2), help.html (1) · release 1, docs 1, project 1_
- **Register `test` / `[` so chains decide locally instead of asking a model (v4.6.135)** `4472c81`
- `[ -f "/qt.txt" ] && echo exists` — the probe that closed the v4.6.132 batch — was not a shell test at all: both commands were unregistered, so the line fell through to the AI fallback and a model was asked whether a file exists. `_evalTest()` has implemented `-f -d -e -z -n ! = !=` for the if/while parser since the script runner was written; only the command was missing.
- `test` and `[` registered in ShellModule. Success resolves silently; false rejects with a
- `silent` error so `&&`/`||` skip it — POSIX prints nothing on exit 1.
- `[` requires the closing bracket (`[ : missing ']'`), and that message is NOT silent.
- `test` already names the on-demand regression-suite runner ("test auto" / "test list" in
- … run `git show <sha>` for the rest
- **Regenerate after the metering and shell commits (73 entries)** `4fca1c9`
- **Untrack cron/API runtime state, keep it on disk** `8555cd4`
- These files are rewritten in place every few minutes by cron jobs and the APIs (visits.json, agent-items.json, subscribers.json, newOS4 debug/report/ maintenance state, changelog feed, course atlas...). Tracking them made `git status` useless and put runtime noise into every commit. The daily backup (admin/scripts/backup-spaces.sh) already owns this state; nothing is deleted from disk here — files stay in place, only removed from the index.
- No source files touched: api/*.py, admin/scripts/*, modules/, *.html pages staged nowhere; the two ambiguous data files (data/income-tracker.json, admin/dead-jobs.html) are deliberately left tracked.
## v4.6.132 — 2026-10-29
_Touches modules (4), CHANGELOG.md (1), data (1), main.html (1) · fix 1_
- **Shell tokenises command lines quote-aware everywhere (v4.6.132)** `3b1773c`
- v4.6.131 taught the pipe stages to forward their raw text, but the shell's own wrapper still split each stage with `cmdText.split(/\s+/)` — so `grep "alpha beta" /qt.txt | wc -l` handed grep the two args `"alpha` and `beta"` and matched nothing. Measured live: the wrapper (ShellModule.init() replaces terminal.executePipeChain) is the implementation that runs, so the core fix alone is decorative.
- All six command-line split sites in modules/ext/shell.js now go through one helper, `_tokenize()`, which delegates to `security._parseArgs` — the parser the bare-command path already uses — so the two cannot drift again:
- … run `git show <sha>` for the rest
## v4.6.131 — 2026-10-29
_Touches modules (8), scripts (3), help.html (1), main.html (1) · fix 1_
- **Pipe stage quoting, maintain ordering, help cost row, plan status (v4.6.131)** `2c6c218`
- Four items closed in one batch; three ran as parallel subagents on disjoint files, the bundle was rebuilt once by the parent (rebuild-bundles.py serialises the working tree, which carries other sessions' work — see 161372f).
- … run `git show <sha>` for the rest
## v4.6.130 — 2026-10-29
_Touches modules (5), main.html (1) · fix 1_
- **Js keeps its string literals — dispatcher forwards the raw command line (v4.6.130)** `161372f`
- _parseArgs() treats quote characters as delimiters and drops them, so every string literal in a js expression died before eval: `js "abc".toUpperCase()` reached the module as `abc.toUpperCase()` → ReferenceError: abc is not defined. Same for `js import('fs')` → `import(fs)`. Found while verifying the v4.6.128 console fix (plan 2026-10-29_112000-newos4-parseargs-strips-js-string-literals.md).
- terminal.js: checkAndRun() records the untokenised line (`_currentRaw`), _dispatch() forwards it,
- dispatchCommand(cmd, args, rawText) passes it to the module. Security classification still runs on the tokenised form, and pipes/AI/script-runner calls pass nothing, so they behave as before.
- console.js: `js` evaluates _expr(args, rawText) — the raw remainder. `node -e` deliberately does
- … run `git show <sha>` for the rest
## v4.6.128 — 2026-10-29
_Touches modules (2), help.html (1), main.html (1) · fix 1_
- **Js console handles promise rejections + Node-builtin hint (v4.6.128)** `75c5113`
- evalJS() evaluated the expression bare and printed JSON.stringify(result), so a rejected thenable (e.g. `js import('fs')`) became `{}` on screen while the rejection escaped to window.onunhandledrejection and was auto-filed as a runtime bug report (RPT-20261028-172211-9c7f2adb, cluster 480x). evalJSFile() already had .then/.catch — the asymmetry was the defect.
- modules/ext/console.js: thenable branch in evalJS() awaits and catches; new
- _printError() is the single error sink for all four catch paths and appends a hint when a bare specifier fails to resolve ("'fs' is a Node built-in; the browser has no Node — use the VFS (ls/cat/run, terminal.fs) instead").
- help.html: js / js-run rows say "browser only — no Node built-ins".
- module-loader.js + main.html: v4.6.128.
- … run `git show <sha>` for the rest
## v4.6.119 — 2026-10-27
_Touches scripts (6), modules (4), CHANGELOG.md (3), guide.html (2) · release 3, fix 2_
- **`if test …` was always false and `for` items never globbed** `e575ee8`
- Found by running the shipped runner the way a user would, not by re-reading it.
- _evalTest() stripped [ ] but not the `test` keyword, so `if test -f /work/alpha.txt` evaluated
- the word "test" as an operator, returned false, and every such branch took the else arm — measured live: "alpha missing" for a file that exists. Both spellings work now (`test -f x`, `[ -f x ]`), plus `-d`, `-e`, `-z`, `-n`, `!` and the = / != comparisons.
- `for f in /work/*.txt` ran ONCE with $f set to the literal pattern, because only command
- arguments went through _expandGlobs. Loop items are expanded per iteration now, so the body runs once per matched file with the real path.
- _expandGlobs() falls back to readDir() when a fs has no ls(), so the helper works against either
- shape instead of silently returning the literal pattern.
- Verification, live on v4.6.119:
- … run `git show <sha>` for the rest
- **Generate the changelog from git and keep it current in maintenance** `22bb505`
- The page at spaces/agando/downloads/changelog.html was hand-built from VFS notes by the in-page agent: 11 entries, huge honest holes ("v4.6.59 - v4.6.100 unknown"), and it had stopped at v4.6.101 while the app was running v4.6.119. A docs page that lags the release reads as current, which is worse than no page.
- scripts/build-changelog.py derives every entry from git: it walks commits touching
- spaces/newos4, stamps each with the LATEST_VER that was live at that commit, and groups them into builds. Kinds map onto the page's own vocabulary (feat->release, fix->fix, test->review, docs->docs, chore/style/refactor->project), and a build's chip is its dominant kind. Commits whose prefix carries `backticks` render as <code>, escaped first.
- The page is its own template: everything visible comes from the #changelog-data JSON
- … run `git show <sha>` for the rest
- **Link the changelog in and deep-link each board fix to its build** `b26bfb8`
- The generated changelog was an orphan: no page linked to it, and nothing linked from it, so "generated and maintained" still meant "nobody lands here".
- guide.html + help.html footers and the board header now link the changelog (and the board, from
- the app side). All three verified live: HTTP 200, 1 link each.
- Every closed board card gains "fixed in <sha> · v4.6.XXX" where the version is a link to
- changelog.html#v4-6-XXX. The anchor rule is the changelog renderer's own (non-alphanumerics -> "-", lowercased), not a second guess at it.
- The page now actually jumps to a hash. Previously the list is built by JS after load, so the
- browser's own anchor scroll ran against an empty list and every deep link landed at the top. jumpToHash() runs after render and on hashchange.
- A build can legitimately be missing: v4.6.83 fixed MAINT-002 but never reached a commit (the
- … run `git show <sha>` for the rest
- **Tag builds, feed the changelog to machines, fail maintenance when it ages** `782b3db`
- Tasks 3, 4, 5, 6, 7 of the changelog plan.
- 67 build tags (v4.6.10 … v4.6.119) now exist, so a build is a ref instead of something that has
- to be re-inferred: `git log v4.6.118..v4.6.119` finally means something.
- The generator prefers tags, but via ONE `git for-each-ref` pass rather than a `git describe` per
- commit. That matters: describe-per-commit made the build 2.4-2.7 s against a 1.0-1.45 s baseline — i.e. tags made it slower, which the plan's gate said to treat as "tags bought nothing". The right fix was a bulk lookup, not dropping the tags: now 0.80-1.04 s (median 0.84 s) with output byte-identical to the untagged baseline (68 entries, same version order).
- changelog.json + feed.xml (Atom) are generated from the same page dict: 36 KB / 21 KB, 68/68
- entries, both HTTP 200, item markup stripped so a reader gets text not HTML.
- … run `git show <sha>` for the rest
- **Apply the GLM-5.3 review to the changelog pipeline** `f3b2664`
- Verdict was APPROVE WITH CHANGES: 21 findings, 17 fixed, 3 refuted with the command that killed them, 1 kept with the audit that justifies it. The reviewer's best find was a design bug of mine, not a typo.
- The age guard was unfixable by --fix and fired on a quiet week. check_changelog() regenerated only
- on a version mismatch, then returned from the version-match branch before the age check could act, so a page older than the threshold stayed red forever with the false diagnosis "the daily job is not running". Now --fix always regenerates (0.84 s) and age is read from a new `checked` stamp separate from `generated` (content-change time), which also stops a daily 65 KB rewrite of an unchanged page. Proved: seeded 641 h -> FAIL + "run maintain --fix to refresh" -> --fix -> PASS.
- Atomic writes everywhere (temp + os.replace): nginx serves these files off disk, so an in-place
- … run `git show <sha>` for the rest
## v4.6.118 — 2026-10-27
_Touches modules (5), main.html (1) · fix 1_
- **Send num_ctx to Ollama, and make one auto-connect failure read as one line** `3bba2c2`
- Two things I had flagged as "worth your attention" and one leftover nit.
- num_ctx was never sent. Ollama's server-side default context is 2k-4k depending on the build, so
- a system prompt + 20 messages + up to 24k chars of tool results could be truncated server-side, which is exactly the "it forgot what it just read" class of report. Every Ollama request now carries options.num_ctx (default 16384, overridable via webx11_ollama_numctx, floor 512); the OpenAI-compatible branch is untouched because num_ctx is not one of its parameters.
- Automatic connect prints one outcome instead of one line per endpoint tried (7 -> 3 on a dead
- endpoint). Interactive `ollamaconnect` keeps the per-endpoint detail.
- write_file's failure message no longer echoes a trailing slash.
- Verification:
- test-ollama-numctx.js (7 asserts): generate and chat payloads carry num_ctx=16384, a stored
- … run `git show <sha>` for the rest
## v4.6.116 — 2026-10-27
_Touches modules (7), main.html (1) · fix 1, review 1_
- **Piped grep searched the working directory instead of its input** `8b9c826`
- The stdin work in 5cf5010 fixed wc/head/tail but left grep alone, and grep's no-operand branch sweeps the cwd. Measured on v4.6.115: `cat /gdir/in.txt | grep zzz` answered `other.txt:1: zzz in a cwd file` — a file that was never piped, and `cat in.txt | grep bbb` reported the match twice (`in.txt` from the cwd sweep plus `__pipebuf__` from the injected file).
- module-fs.js: when terminal._pipeInput is set and no file operand was given, grep searches the
- staged text and prints `<line>: <text>` (stdin has no filename, as in GNU). The cwd sweep stays for a bare interactive `grep x`.
- shell.js: _isPipeAware now covers only sort/cut/uniq/expand. grep/head/tail/wc read _pipeInput
- themselves, so injecting the __pipebuf__ file produced duplicate hits and named stdin results.
- Verification:
- test-shell-stdin.js 20 asserts (+4 for grep: no cwd match, non-match reports none, match prints
- … run `git show <sha>` for the rest
- **Prove the mixed-batch loop ceiling stops an agent at 9 identical calls** `b09fea8`
- MAINT-006 was the last open board item and it needed a live run, not another code argument. The fix landed in 5058d3b (the hard stop was only reachable when EVERY call in a batch was blocked, so one repeated call + one novel call per turn ran to maxTurns).
- Live probe on v4.6.116 with the deployed class and a deterministic model stub (repeat read_file
- + a fresh list_files each turn): the loop stopped at turn 9 with "⛔ main: no progress after 9 identical calls — ending loop with partial result" and returned {finalAnswer, toolTurns} with a 400-char partial answer.
- test-agent-loop-ceiling.js (9 asserts, real module via its constructor): A mixed batch stops before
- maxTurns / B all-blocked batch still stops / C a productive loop that never repeats is NOT stopped.
- RED on 52d188a (pre-fix): 8 passed, 1 FAILED — the mixed batch ran to "AI reached the 12/12 tool
- … run `git show <sha>` for the rest
## v4.6.115 — 2026-10-27
_Touches modules (7), main.html (1) · release 1_
- **The script runner runs scripts — control flow, variables, globs, redirects, stdin** `5cf5010`
- MAINT-021 and MAINT-017, executed from .hermes/plans/2026-10-27_145500-newos4-script-runner.md.
- shell.js: new block parser (_parseBlocks) and executor (_runInstructions/_runSimple/_runCommand/
- _dispatchOne). `for`/`while`/`if` bodies now run, including one-line `if [ -f x ]; then y; fi`. Variables expand through _expandVars ($VAR, ${VAR}, $?, single quotes literal), globs expand through _expandGlobs (unquoted args only, per-command), and `>`/`>>` capture the command's output into the VFS instead of being passed to the command as text.
- shell.js: a script never answers an unknown command by calling the AI any more. It prints
- `script:<path>:<line>: <cmd>: command not found` and sets $? — the AI fallback stays for typing.
- shell.js: unclosed blocks and bad `for` headers surface as `Script <path> failed: …` (the parse
- used to sit outside the try, so a bad script crashed the runner).
- … run `git show <sha>` for the rest
## v4.6.110 — 2026-10-27
_Touches modules (4), main.html (1) · fix 1_
- **The loop guard reads the config it documents** `5425e66`
- MAINT-004. The guard read warnAt/blockAt/hardStopAt while one config path wrote `threshold`, and `enabled` was never consulted anywhere — writing {"enabled": false} into webx11_ai_config changed nothing at all. One resolver, _loopPolicy(), now normalises both spellings, guarantees warnAt <= blockAt < hardStopAt, and both the block and the warning are gated on enabled.
- Live on v4.6.110 (_loopPolicy called in the page): defaults {warn 3, block 6, hardstop 9}; {threshold: 4} -> {warn 2, block 4, hardstop 7}; {enabled: false, threshold: 3} -> enabled false. test-loop-policy.js: 17 asserts, RED on HEAD ("_loopPolicy is not a function"), suite 16/16.
## v4.6.109 — 2026-10-27
_Touches modules (6), main.html (1) · fix 1_
- **Agent registration is absolute-path and honest about what it stored** `5058d3b`
- Three of the four agent-cluster items, each with a test that is red on HEAD.
- MAINT-008 (module-agent-registry.js + module-orchestrator.js): registerAgent took args[0] as the
- agent id and only args[1] as the capability list, while both orchestrator callers passed the command word first — so the boot log read "Agent 'register' registered with 1 capabilities" and four capabilities were stored as one. The command word is now stripped, args[1..] are joined and split on commas (both "a,b" and "a b c" work), and the callers pass [id, ...caps].
- MAINT-007 (registry + aiassist): mkdir/rm used relative paths, so a registration made from /docs
- created /docs/agents/<id> and an unregister from any other cwd removed nothing — the leaked directories the report suspected. Every path in the registry (and the four /_sessions writes in module-aiassist.js) is absolute now.
- … run `git show <sha>` for the rest
## v4.6.108 — 2026-10-27
_Touches modules (5), main.html (1) · fix 1_
- **Help names the owner of an undocumented command; loop lessons leave /skills** `243a766`
- Two board items plus the context-window close-out.
- MAINT-019 (module-system.js): `help <cmd>` answered "No help available for X" for a command
- that is registered but has no help line, which reads as if the command does not exist. It now says "X — registered by <Module>, which publishes no help line for it. Try: help". A command that genuinely does not exist still gets the old message.
- MAINT-020 (module-aiassist.js): auto-generated loop lessons were written into /skills with
- their own index entries, so `skill list` filled up with system noise. Lessons now live in /lessons with /lessons/index.json; the reader still falls back to legacy loop-lesson-* entries in /skills for one release, and the one-time migration deletes them from the skills index.
- … run `git show <sha>` for the rest
## v4.6.107 — 2026-10-27
_Touches modules (8), main.html (1) · fix 1_
- **Bound one turn's tool context, honest skill-create, quarantine a corrupt memory graph** `d4d80f0`
- The three items the last review and probes left open, each with a test that is red on HEAD.
- MAINT-023 (module-aiassist.js): one agentic turn grew without limit — every role:'tool' body
- was pushed verbatim and only this.context was trimmed BETWEEN turns (measured 6 -> 112 705 tokens across 8 model calls against maxContextTokens 12 000). Observations are now clipped to maxToolResultChars (6 000) with a note saying how much was withheld and to re-read with offset/limit, and _boundToolContext() elides the oldest tool bodies once the running total passes toolContextBudgetChars (24 000), always keeping the newest 4 verbatim. The ceiling is turn-independent: the same ~26 k chars at 8 turns and at 20.
- MAINT-024 (modules/ext/skill-pack-core.js): the command printed "✅ Skill created" with
- … run `git show <sha>` for the rest
## v4.6.106 — 2026-10-27
_Touches modules (7), main.html (1) · fix 1_
- **Name the real Ollama failure, bound auto-connect, fix the review findings** `1f07ef9`
- Second pass over v4.6.105, driven by an independent review that reproduced each item.
- ollama-manager.js: chat() now reports the captured cause ("Ollama error: Failed to fetch",
- "…: HTTP 500") instead of the generic "No provider responded"; _lastChatError is reset per call so an old reason is never replayed. chatWithTools carries the reason in its error string.
- ollama-manager.js: _ensureConnected() bounds connect() with a deadline (connectTimeoutMs,
- default 10 s). connect() uses a bare fetch(), so a blackholed endpoint previously left the shared in-flight promise pending forever and hung every later message — the old code failed fast. The attempt is now try/finally, so a throwing terminal.log() can no longer poison _connecting and suppress the retry on the next message.
- ollama-manager.js: an automatic connect no longer silently rewrites the user's saved model;
- … run `git show <sha>` for the rest
## v4.6.105 — 2026-10-27
_Touches modules (6), main.html (1) · fix 1_
- **Connect to Ollama on first use, honest write_file, list_files sees /sys** `d6c5cb9`
- Three defects, each re-probed first and covered by a test that is red on HEAD.
- ollama-manager.js: nothing connects at boot; only the Ollama tab button called connect(),
- so a user's first message died with "Ollama not connected. Use ollamaconnect first." even when the endpoint was healthy. New _ensureConnected() runs from chat(), chatStream() and chatWithTools(): one attempt, shared by concurrent callers, a "Connecting…" hint, and only then the error — which now says the endpoint is unreachable instead of telling the user to run a command they already expected to work.
- module-aiassist.js write_file tool: fs.writeFile()'s false return was dropped and the tool
- answered "File written: …" anyway, sending the model on to a read that found nothing (MAINT-012). It now returns "Write FAILED: … — parent directory missing or target is a directory; nothing was written".
- … run `git show <sha>` for the rest
## v4.6.104 — 2026-10-27
_Touches modules (5), main.html (1) · fix 1_
- **Never overwrite a directory node; ls honours a path after its flags** `ec9d523`
- MAINT-010 and MAINT-016, the two high items the review left open. Both were re-probed before the change and the new test is red on HEAD (5 passed, 7 FAILED).
- virtual-fs.js rename()/copy(): they assigned dst.dir.content[dst.name] with no destination
- type check, so `mv file /docs` replaced the /docs node and every child vanished while the command printed "Moved: …". Refuse when the destination is a real directory node (the virtual /sys entry is exempt). rename() also erased the file on `mv a a`, because assigning and then deleting the same key removes it; that path is now an explicit no-op.
- module-fs.js mv/cp: a refusal used to surface as "No such file or directory", which sent
- the operator looking for a missing source. The destination is now inspected first and the message says "is a directory — refusing to overwrite it".
- … run `git show <sha>` for the rest
## v4.6.103 — 2026-10-27
_Touches modules (6), data (2), scripts (2), main.html (1) · fix 1, project 1_
- **Rollback keeps scaffolding, checkpoints cannot collide, tests gate the build** `07544cd`
- Review of the 08:30 implementer run (v4.6.102, checkpoint name fix). The shipped fix was correct; these are the defects it introduced or left behind, each measured first.
- virtual-fs.js rollback(): the snapshot strips _snapshots/_sessions/.trash for bloat. Restoring
- it verbatim deleted every checkpoint, every agent session and the trash, made rollback irreversible and unrepeatable. Re-attach the live copies after the root swap.
- checkpoint(): names are millisecond-resolution, so tight calls overwrote each other
- (measured 6 calls -> 3 snapshots). Dedupe with a -N suffix, refuse names containing "/" (written but unlistable), and prune by snapshot createdAt instead of filename sort, which let a custom name ("aaa-oldest") evict newer auto-named checkpoints.
- module-predictor.js: legacy pre-v2 points have no `usable`, so a 0.079 s aborted run
- … run `git show <sha>` for the rest
- **Point the predictor closure at the shipped commit** `a5f1b92`
- The code commit was amended (39b61ad -> 07544cd) after the first stamp, so re-stamped the steering entry against the commit that actually exists.
## v4.6.102 — 2026-10-27
_Touches modules (4), main.html (1) · fix 2_
- **Print the checkpoint name actually saved (auto)** `c768584`
- Task 2 + ship of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md.
- module-system.js `checkpoint` read the clock a second time for the printed name, so the name in "✅ Checkpoint created: <name>" differed from the name on disk by >=1ms whenever no explicit name was given. It now prints the value returned by fs.checkpoint(). try/catch and error strings unchanged.
- Bundles rebuilt (rebuild-bundles.py); LATEST_VER in module-loader.js and all three ?v= tags in main.html (core-bundle, module-loader, critical-ext-bundle) bumped v4.6.101 -> v4.6.102.
- … run `git show <sha>` for the rest
- **Regression test for checkpoint name round-trip (auto)** `2be415c`
- Task 3 of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md.
- New modules/test/test-vfs-checkpoint.js loads the real VirtualFileSystem and SystemModule with guarded localStorage/indexedDB/window stubs and asserts: A1-A4 checkpoint('') returns the resolved name; two calls differ; the file exists under the returned name B1-B2 checkpoint-list shows the returned name; rollback(<that name>) is true C1-C4 the `checkpoint` command prints exactly the name it created (and an explicit name verbatim)
- RED before the fix (v4.6.101): FAIL C2 printed name exists on disk — printed=2026-10-27T07:31:21.575Z listed=["2026-10-27T07:31:21.574Z"] test-vfs-checkpoint: 2 passed, 8 FAILED
- GREEN after: $ node modules/test/test-vfs-checkpoint.js test-vfs-checkpoint: 10 passed, 0 failed
## v4.6.101 — 2026-10-27
_Touches modules (5), main.html (1) · release 1, fix 1_
- **Cut/copy in Files, paste into WebDAV (Issue D2)** `dca2c3b`
- The two panes kept separate clipboards (filemgr._clipboard vs webdav._selected), so "cut here, paste there" did not exist — the operator's last phone-test item.
- filemgr.js _openOverlay() publishes a read-only view of its clipboard on the terminal object: webx11.fileClipboard() / fileClipboardClear() / fileClipboardSet(paths, mode). webdav.js the bulk bar gains "📥 Paste N" (hidden when the clipboard is empty). Handler: for each path readFile -> _webdavRequest('PUT', dest + '/' + name, {body}); only 200/201/204 counts. For mode "cut" the local file is deleted AFTER the server accepted the bytes, never before. A failed PUT keeps the local copy, prints "📥 Paste failed: <name> — HTTP n", and leaves that path in the clipboard so "📥 Paste 1" is a retry. Successes leave it. Summary "📥 Pasted N item(s) to <path>" (+ "— M failed"), then the pane reopens there.
- … run `git show <sha>` for the rest
- **Checkpoint() returns resolved name (auto)** `f7249ff`
- Task 1 of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md.
- virtual-fs.js checkpoint(name) generated an ISO name when none was given but returned bare `true`, so callers could not know which name the snapshot was written under. It now returns the resolved name (string). Prune (max 3) and saveToStorage() behaviour unchanged.
- Verification: $ node --check modules/core/virtual-fs.js SYNTAX OK $ node modules/test/test-vfs-checkpoint.js test-vfs-checkpoint: 9 passed, 1 FAILED (remaining FAIL was the module-system display path, fixed in the next commit)
## v4.6.99 — 2026-10-26
_Touches modules (4), main.html (1) · fix 1_
- **Four phone-test defects — no preview close, single-file share, dead-end fullscreen, silent WebDAV move** `2023728`
- All four came from the operator's phone pass on v4.6.95 and each collapsed to a line of code.
- … run `git show <sha>` for the rest
## v4.6.95 — 2026-10-26
_Touches modules (4), main.html (1) · release 1_
- **Mobile download/share UX — deferred blob revoke, multi-select, Web Share, 44px targets** `6ef379f`
- Executes .hermes/plans/2026-10-24_051600-newos4-mobile-download-share.md (approved via the feedback dashboard; marker autonomy: safe). Front-end only, v4.6.94 -> v4.6.95.
- … run `git show <sha>` for the rest
## v4.6.94 — 2026-10-26
_Touches scripts (7), modules (3), help.html (1), main.html (1) · release 5, docs 1_
- **Review dashboard, public guide, terminal commands (v4.6.94)** `de37884`
- Dashboard (/admin/feedback.html, admin-gated)
- User-feedback tab over the live :8908 API: list, filter, search, publish reply,
- archive/reopen. Reply is what the reporter sees in /stear -> Your reports & replies.
- Agent-items tab: 3 plans parked at autonomy:review + 19 defect reports, one click each.
- Snapshot age line + stale warning (>30 min), and the last apply outcome with failures.
- Existing /admin/ tab relabelled "Client error log" (it renders feedback_log.json, not
- the feedback store) and now links to the review dashboard.
- Decision path
- … run `git show <sha>` for the rest
- **Open the Agent tab on the queue, guide out of empty states, cleaner directives** `db25ef0`
- **Preview a plan or report in a modal before deciding** `85133ab`
- The cards only carried a title, a path and buttons — approving blind was the only option. The page cannot read .hermes/plans/*.md itself (600 darkbot, php-fpm is www-data), so the collector — already running as darkbot every 10 minutes — now embeds what a decision needs:
- plans -> excerpt (first 60 lines, 2.4 KB cap), task headings, status/implementation lines reports -> body, page url, newOS4 version, user agent, first 3 error strings
- Modal: click a card title (or, on a report, read it in place) -> title, meta line with path/size/modified/task count/decision, the text in a scrollable monospace block, and the same Approve / Reject / Noise / Resolve actions. Esc, backdrop and the X close it; on a phone it is full-screen. Cards and modal share one actButtons() helper instead of duplicating markup.
- … run `git show <sha>` for the rest
- **Reconstruct _docs/decisions-2026-10-23.md — two feedback reports referenced a file that did not exist** `1d8f658`
- **Filter network-class auto reports at ingest — count, don't file** `a976c35`
- The debug supervisor files an auto report for any failed fetch, so a flaky phone connection or a retired endpoint became a review card. One cluster held 14 407 + 1 031 occurrences and produced 3 of the 19 cards in the queue; 10 of 21 items reviewed on 2026-10-26 were noise for this reason.
- api/debug/report.php: an auto report whose *every* error is network-class (fetch failed / failed to fetch / network error / load failed / aborted / signal is aborted / ERR_INTERNET_*) now increments data/debug/network_failures.json (per day, per source path, top 40 kept) and returns 202 {filtered:"network"}. No RPT file, no card.
- … run `git show <sha>` for the rest
- **Autonomous first-pass reviewer — decide what is provable, leave the rest** `0446064`
- The operator asked: "when I don't do anything, an agent can do it for me and I can just follow up or steer it." This is that agent, deterministic (no LLM): same input, same decision, and every reason is a grep or a commit hash that can be checked.
- spaces/newos4/scripts/newos4-feedback-autoreview.py — rules, first match wins: R1 duplicate same fingerprint already decided -> resolve R2 already-fixed identifiers absent from modules/**/*.js AND `git log -S<token>` proves a commit removed them -> resolve R3 noise fetch/abort/probe patterns -> reject R4 still-present auto report + stack + identifiers still in code -> approve (max 2/day) R5 plan / R6 user report -> never decided
- … run `git show <sha>` for the rest
## v4.6.91 — 2026-10-23
_Touches modules (3), help.html (1), main.html (1) · release 1_
- **Context window warns at 80% and says what trimming dropped (v4.6.91)** `227ee77`
- P0-5 asked what to do about a 422k-token session against a 400k threshold. There is no 400k threshold in this tree — the budgets are maxContextTokens (12000) and maxContext*2 messages — so rather than chase a number that does not exist, this applies the decision that is defensible either way: warn before the cliff, never lose history without saying so.
- 80% of the token budget prints one warning per crossing (no per-turn nagging)
- the message-count trim unshifts a marker naming how many messages left the window and quoting
- the earliest one, instead of splicing them away silently
- 'max-context-tokens (default: 2000)' now reports the real code default, 12000
- … run `git show <sha>` for the rest
## v4.6.90 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · fix 1_
- **The virtual /sys entry leaked into listings, trees and counts (v4.6.88-90)** `dc0114a`
- One phantom entry, three symptoms:
- ls() injected 'sys' whenever it was called without a path, so 'cd _agents; ls' advertised a
- sys directory that does not exist, and list_files('_agents/sys') answered 'Directory not found' — the reported false negative. Injection is now gated on dir === root.
- tree() pushed the same virtual name into the walk and dereferenced it, throwing mid-output on
- every root listing. Guarded.
- _gatherFileInfo() walked into /sys and counted a synthetic subtree: 146 files / 114 dirs for a
- 44-file / 15-dir store. Virtual entries are skipped.
- Verified: root still lists sys; _agents and skills do not; tree('') completes; world model total (44) equals an independent walk of fs.root (44), agree: true.
## v4.6.87 — 2026-10-23
_Touches modules (2), help.html (1), main.html (1) · fix 1_
- **Audit names broken refs instead of counting documentation (v4.6.86-87)** `f57108f`
- The reference checker ran /skill-view \S+/ over every skill's prose, so 'skill-view <name>' and a quoted example in skills-system-guide were counted as missing skills: 'Broken refs: 2' with no names, both phantom. Placeholders and quoted names are now ignored (trailing punctuation stripped first), and the survivors are listed in the audit line and in the suggestion.
- Verified: clean tree -> Broken refs: 0. A probe skill referencing 'ghost-skill' -> Broken refs: 1 (probe-broken-ref → ghost-skill), while '<name>.' and '"module-management"' in the same file are ignored.
## v4.6.85 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · fix 1_
- **The audit stops inventing '999d unused' (v4.6.85)** `497cd96`
- getStaleSkills() returned daysUnused=999 for a skill that had useCount 0 and no lastUsed — a placeholder rendered by the audit as '999d unused', indistinguishable from a measurement. It told the operator to archive procedural memory that had simply never been instrumented.
- Now: daysUnused null + neverUsed true when there is no lastUsed; real day count otherwise; the audit prints '(never used)'.
- Verified: probe skill with no telemetry -> {daysUnused: null, neverUsed: true}; one last used 2026-08-01 -> 83; no 999 anywhere in getStaleSkills() or the audit output.
## v4.6.84 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · fix 1_
- **World_model_query default path parses again (v4.6.83-84)** `a05f696`
- _modelDigest() returned JSON.stringify(...) cut at 4000 chars plus '…(digest truncated)'. The tool then did JSON.parse() on it, so the default detail threw 'Unexpected end of JSON input' every single call; detail:'full' worked only because it returned the object untouched.
- The digest now returns an OBJECT (_modelSnapshot) and shrinks by reducing list lengths, then by dropping whole sections with an explicit 'omitted' list. The string form is generated at the edge and is always valid JSON.
- Verified: default call -> no error, 13-key snapshot, facts answered; caps 400/1200/4000 all under cap and parseable; the residual floor at cap 200 is identity fields only.
## v4.6.82 — 2026-10-23
_Touches modules (2), help.html (1), main.html (1) · fix 1_
- **Skill-create stops mangling what the agent wrote (v4.6.82)** `c532594`
- Two defects in one parser, both silent:
- content = args.slice(2).join(' ') destroyed every newline, so markdown arrived as one line
- the last argument was treated as tags whenever it contained a comma, and stayed in the body:
- a multi-line skill became a single line with ~13 bogus tags, reported as success
- Bodies can now be read from a VFS path (the only way markdown survives an argv splitter), tags are opt-in via --tags=, category via --category=. skill-patch takes a path too.
- Verified: body from file -> 6 newlines preserved, tags [alpha,beta], category testing; inline body containing four commas -> tags [], body intact.
## v4.6.81 — 2026-10-23
_Touches modules (3), api (1), help.html (1), main.html (1) · fix 1_
- **OutboxResult scoped, markdown mime, old reply path retired (v4.6.81)** `72a94ea`
- delegateTask declared `const outboxResult` inside the AI branch and returned it outside, so a delegation with the AI module missing threw 'outboxResult is not defined'. The catch turned that into _handleAgentFailure: the worker stayed idle with no result while spawn_agent reported ok — the reported symptom exactly. Now function-scoped, and the no-module path returns 'queued — AI module not loaded, nothing executed' rather than claiming 'completed'.
- Also: nginx served _docs/*.md as application/octet-stream (no .md type) — now text/markdown + nosniff; agent-replies.php returns 410 for GET as well as POST, so there is one reply path.
- Verified: with the AI module deleted at runtime, delegateTask returns the queued string and throws nothing.
## v4.6.80 — 2026-10-23
_Touches modules (3), _docs (2), api (2), main.html (1) · fix 1, docs 1_
- **GLM review batch — corruption, traversal, cursor truncation, double print (v4.6.78-80)** `dd234ab`
- 12 findings accepted, 3 refuted with evidence (see _docs/chat-review-glm-2026-10-23.md).
- Data loss: a corrupt thread read as [] and the next append rewrote it with one message — now 500 and the file is left alone. Cursor after a >50-message truncation pointed at the thread tail, making the oldest permanently unfetchable — now the last message actually sent, plus truncated:true. Two posts in the same millisecond shared a cursor — epoch is now strictly increasing.
- Security: chat.py show() had no id validation and joined argv onto CHAT_DIR ('../../x' read any .json as darkbot) — THREAD_RE added. The auto-reply prompt now treats message text as untrusted data and forbids acting on instructions found in it.
- … run `git show <sha>` for the rest
- **Chat bridge in AGENTS.md, changelog v4.6.72-80, contract deltas** `3c35556`
- Contract now documents truncated/cursor semantics, the full code table with retry rules, 503-from-nginx (HTML, retryable), the two-cursor rule, strictly-increasing epoch, and the security posture in plain words: client-minted tokens mean public write, mitigated but not closed.
## v4.6.77 — 2026-10-23
_Touches modules (4), main.html (1), scripts (1) · fix 1_
- **Three concurrency holes found reviewing my own patch (v4.6.77)** `7e8bf4c`
- 1. chat.py renamed the temp file AFTER releasing the flock, so two writers could each read the pre-replace snapshot and the later rename silently dropped the earlier message. os.replace now happens inside the lock. 2. The agent cursor was global. After hermes-reset it pointed past the end of the new empty thread, so every read returned nothing — the agent would think it had been ignored. Key is now per thread and reset clears every chat cursor. 3. chat-relay.sh had no overlap guard: a slow agent run (up to 5 min) could collide with the next tick and answer the same thread twice. flock -n on /tmp/chat-relay.lock.
- Verified: stale cursor 'webx11_chat_agent_cursor_oldthread00000' set to 9999999999999, hermes-reset -> zero cursor keys remain, read returns {messages:[], note:'no new messages...'}.
## v4.6.76 — 2026-10-23
_Touches modules (3), main.html (1) · fix 1_
- **Agent read gets its own chat cursor (v4.6.76)** `df0b5e4`
- Two readers shared one cursor. The 30 s UI poller printed each reply and advanced webx11_chat_state.cursor, so the agent's chat_hermes read then asked for 'since=<that>' and got an empty list — the answer appeared on the terminal and never reached the model. That is the reported symptom exactly.
- chat_hermes read now uses webx11_chat_agent_cursor, starts from 0 on first use so the whole thread is visible, and never touches the display cursor. Empty reads say why ('still awaiting Hermes' vs 'no new messages since your last read').
- Proven: UI poll consumed the reply (printed to terminal), then chat_hermes read still returned both the agent's question and Hermes's answer.
## v4.6.75 — 2026-10-23
_Touches modules (2), skills (2), main.html (1) · fix 1_
- **Skill points at the served contract URL, not a VFS path (v4.6.75)** `83b399d`
- The OS4 agent reported the skill cites /_docs/chat-contract.md, which does not exist in its VFS — correct: the file lives on the host and is served over HTTP. Browser VFS and disk are two stores, so a path-only pointer is a dead end. Now names the URL.
## v4.6.74 — 2026-10-23
_Touches skills (3), modules (2), main.html (1) · docs 1_
- **Seed a chatting-with-hermes skill (v4.6.74)** `020f519`
- A command that exists but is undocumented might as well not exist: the agent had no way to learn that `hermes` exists, that `chat` is taken, that replies are asynchronous, or that the cursor is milliseconds. Skill covers the commands, the chat_hermes tool, the failure table (401/413/429/503/507), the rules that keep it useful, and how to verify a send.
- Seeded from skill-pack-core._buildSkills() so every fresh session gets it; mirrored to skills/chatting-with-hermes.{md,json} + index.json (19 entries).
## v4.6.73 — 2026-10-23
_Touches modules (4), _docs (2), help.html (1), main.html (1) · release 1, docs 1_
- **Hermes command, auto-poll, chat_hermes tool, token reset (v4.6.73)** `cfbc835`
- Phase C as repo edits (browser edits in _mods/ do not survive a reload).
- `hermes <msg>`: sends through the chat endpoint and always prints an outcome —
- '📨 Sending…' then either '✅ sent id=… epoch=…' or a coded error with a hint (429 retry later, 503 host busy, 401 run hermes-reset). `feedback` stays as the alias.
- NOT named `chat`: ChatStudioModule already registers `chat` and dispatch walks modules
- in order, so a second `chat` was silently unreachable. Found by test, not by reading.
- `hermes-poll` / `feedback-inbox`, `hermes-reset` (token rotation).
- Poller: 30 s, paused while the tab is hidden, polls on visibilitychange, first poll
- adopts the cursor and prints nothing, cursor persisted in webx11_chat_state.
- … run `git show <sha>` for the rest
- **Update brief for the agent, v4.6.57 to v4.6.73** `6163429`
- Seven behaviour changes it has to act on, not just seven code changes: the command is `hermes` not `chat` (ChatStudio owns that name), replies arrive untyped within 30s, the cursor is epoch milliseconds, only 429 is retryable, the old reply endpoint is 410, multi-path grep/head/tail, and on-demand agents are not a fault.
## v4.6.70 — 2026-10-23
_Touches modules (4), api (4), help.html (1), main.html (1) · release 2_
- **On-demand worker labels and a server-canonical predictor store (v4.6.70)** `e1d1167`
- Two decisions recorded by the operator on 2026-10-23, implemented.
- 6 — spawn-on-demand is the design, so stop reporting it as a defect:
- OrchestratorModule counts delegations inside delegateTask()
- status prints "Mode: on-demand — N/M worker(s) available, X delegation(s) this
- session" and drops the "spawning is not dispatching" warning
- per-agent lines read "available, no delegations yet (spawned ...)" / "N
- delegation(s), last ..."
- 10 — per-user, server file canonical, browser VFS a TTL'd read-through cache:
- … run `git show <sha>` for the rest
- **Token-scoped chat endpoints, contract, host CLI, old POST retired (v4.6.71 phase B)** `fde3ce3`
- api/chat/{lib,post,poll}.php: thread = sha256(token)[0:16]; flock read-modify-write;
- tmp+rename; ms epoch cursor; per-thread + per-IP windows with retry_after_ms; 401/413/507/405 carry a machine code, only rate_limited is retryable
- _docs/chat-contract.md: frozen shapes, incl. why the cursor is ms and not ts
- scripts/chat.py: list/show/reply, UTC, locked, atomic replace
- api/debug/agent-replies.php POST -> 410 Gone with pointers (one write path)
- Measured: 401 no-token, 201+id, poll 1 msg, poll>since 1 new, data/chat 403, 4001 chars 413 (codepoints), 429 with Retry-After 576, round trip via chat.py.
## v4.6.67 — 2026-10-23
_Touches modules (3), help.html (1), main.html (1) · fix 1_
- **Grep, head and tail take every path argument (v4.6.67)** `8ab3972`
- Packet 1's multi-path ask was only honoured by wc in v4.6.58 — grep read only args[ai+1] and head/tail only positional[0], so `grep x a b` searched `a`, printed one file's hits, and exited 0. Same silent drop, three more commands.
- grep: loops every path; multiple files print `file:line: text`, `-c` prints one
- count per file, missing files are reported per file and excluded from the summary count; single-file output unchanged
- head: every path, `==> file <==` header when more than one; `head f 40` still works
- tail: every path with headers; a numeric second positional is still the legacy
- `tail <file> <count>`; `-f` warns and follows only the first file instead of printing all of them and claiming otherwise
- … run `git show <sha>` for the rest
## v4.6.65 — 2026-10-23
_Touches skills (3), modules (2), main.html (1) · docs 1_
- **Merge the two shell-composition skills into one seeded skill (v4.6.65)** `bfa1ffa`
- Packet 9. `terminal-commands-silently-failing` and `command-composition-rules` described the same bug family from two angles, and since v4.6.58 both were describing behaviour that is now refused outright.
- new seeded skill `command-composition-rules` in skill-pack-core's _buildSkills():
- the rule, why the refusal exists (with the 42-42 / 34k-a measurement), what works (one command per call, wc multi-path, interactive-only pipes, quoted operators), how to verify a plausible-looking result, and the pitfalls
- its History section names both retired skills so a reader arriving from either
- search finds the surviving one
- mirrored onto disk as skills/command-composition-rules.md + .json and added to
- skills/index.json (18 entries) so the repo copy matches what sessions receive
- … run `git show <sha>` for the rest
## v4.6.64 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · fix 1_
- **An invalid grep pattern returns a message, not an exception (v4.6.64)** `b45f13f`
- Packet 8. `new RegExp(pattern)` sat unguarded in the terminal grep handler and in VFS find()/grep(), so a bad pattern threw out of the command handler into the error ledger -- the same ledger packet 2 had to stop trusting.
- module-fs.js grep: compile in try/catch, print
- "grep: invalid pattern: <pattern> (<engine message>)" as a warning and return
- virtual-fs.js find()/grep(): same guard, return [] and stash the message on
- _lastPatternError instead of throwing into the module error wrapper
- Verified live on v4.6.64 with the packet's own repro: `grep "/\bug_id\" file` prints the message, the error counter stays 0 (was: one ledger entry per call), a valid pattern still matches, and fs.grep('[') returns [] instead of throwing.
## v4.6.63 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · release 1_
- **Agent status says never-scheduled vs waiting; `guides` command (v4.6.63)** `1615b27`
- Packet 6 and 7.
- 6 — four agents showed `sleeping` and nothing distinguished "waiting for work" from "never handed a task". Added tasksAssigned/lastTaskAt to the agent record (counted where a task is actually assigned), and the status line now reads idle — NEVER SCHEDULED (0 tasks since 2026-10-23 16:22) idle — waiting for work (3 task(s), last 2026-10-23 16:22) parked — NEVER SCHEDULED ... plus "Dispatch: 0/3 agent(s) have ever been given a task" and a warning that spawning is not dispatching.
- 7 — /guides/ was reachable by nobody. New `guides` command lists it newest-first with sizes and the preview/cat hints; help.html gained a Guides nav entry and section with links to the published guides.
- … run `git show <sha>` for the rest
## v4.6.62 — 2026-10-23
_Touches modules (3), help.html (1), main.html (1) · fix 1_
- **Tokenised body-aware skill search for load_skills (v4.6.62)** `3194d1c`
- Packet 4. The matcher tested the whole query as one substring against name+description+tags: word order decided the outcome ("telemetry dashboard" = 0 while "telemetry" = 1), a sentence could never match, and skill bodies were never searched at all.
- _skillTokens(): lowercase, split on non-word chars, drop <3-char words and a
- stopword list; a query made only of stopwords keeps its literal form
- scoring: name 3, description 2, tags 2, body 1 + up to 1.0 for repeated hits,
- multiplied by token coverage (0.5 + 0.5 * matched/total), +3 phrase bonus so word order can never zero a real hit
- results carry score and matchedTokens ("2/3"); top 3 by score, name as tiebreak
- zero results now name the tokens searched and the closest skill names instead of
- "No matching skills found"
- … run `git show <sha>` for the rest
## v4.6.61 — 2026-10-23
_Touches modules (6), help.html (1), main.html (1) · fix 1_
- **One error counter for errors/dashboard/world-model (v4.6.61)** `36450fa`
- Packet 2 (three surfaces, three answers). Measured first: `errors` scanned /logs/current.log for [ERROR] lines, but that file only ever receives explicit terminal.log('ERROR', ...) calls -- exactly one call site in the whole app -- so it read 0. The dashboard added DebugSupervisor._errorDB plus failed tool calls (18), and the world model read the debug DB alone (11).
- terminal.countErrors() / countLogErrors(): the single counter, exposed on the one
- object all three surfaces already hold
- `errors` prints the canonical total with its breakdown, the entry list (hash,
- count, severity, message), and reports the log scan separately as a mirror
- dashboard health.errors and health.errorBreakdown derive from it
- world model telemetry.recentErrors derives from it, plus errorsBySource
- terminal.log('ERROR', ...) now also records into DebugSupervisor, so the log is a
- … run `git show <sha>` for the rest
## v4.6.58 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · fix 1_
- **Refuse shell composition, wc takes multiple paths (v4.6.58)** `24d189d`
- Packet 1 and 3 (silent wrong answers from a shell-less terminal).
- detectComposition() in the security gate refuses ; && || | before dispatch.
- `;` used to fail loudly, but `&&`/`||` folded the right-hand side into the left command's argv (wc -l f && grep -c x f returned "42 42"), and a `|` arriving from the AI run_command tool bypassed splitPipe entirely and degraded the pattern to empty -- which matches every line. Operators inside quotes are untouched.
- wc accepts multiple paths: one line per file plus a total, per-file errors for
- missing paths. `wc -c a b` used to print only `a` and exit 0.
- help.html: "There is no shell" callout at the top of the command reference,
- plus the v4.6.57/58 changelog entry.
- … run `git show <sha>` for the rest
## v4.6.57 — 2026-10-23
_Touches modules (6), main.html (1) · fix 1_
- **Predictor arithmetic, deployed-source viewer, ls text output (v4.6.57)** `bece3bc`
- Acting on the "Request to the newOS4 team" packet (spaces/agando/guides/, re-verified by its author on 4.6.56). Three verified defects fixed:
- Packet 5 (predictor arithmetic) - module-predictor.js
- the persisted field named mape held a SUM while predictor-status divided by the
- paired count: renamed to errorSum, older payloads read through
- errorPct is stored raw; the 500% clamp is now render-only, so a 475x miss stays
- visible instead of being flattened to the ceiling
- predictions are floored at 1 s and marked floored; a floored prediction is
- consumed but never scored as 100% error (a 0 s prediction is not a prediction)
- … run `git show <sha>` for the rest
## v4.6.56 — 2026-10-23
_Touches modules (5), downloads (1), help.html (1), main.html (1) · fix 1_
- **Measure predictor accuracy over paired predictions (v4.6.56)** `e3157bb`
- The accuracy readout was structurally 0%: module-predictor.js:187 incremented the denominator on every observed run while the numerator only moved when a stored prediction existed, and predictions were stored only by the manual `predict` command (the AI tool predict_task computed one and discarded it). Replaying the AI loop's own call path with 6 tasks printed "Predictions made: 0, Accuracy: 0/6".
- pending predictions with a 15m TTL, consumed by threaded id -> task hash ->
- cli-only similarity >= 0.5; unpaired runs never enter the denominator
- duration scored within 30% or 3s (integer predictions vs fractional observations)
- errorPct capped at 500% so one miss cannot poison the mean error
- schema v2: v1 correct/total counters dropped on load, observed runs and the
- prediction counter kept; task text stored as an FNV-1a hash only
- tuning (confidence-threshold, min-data-points, pair-ttl) persists
- … run `git show <sha>` for the rest
## v4.6.55 — 2026-10-23
_Touches modules (6), api (5), data (3), scripts (3) · fix 1, release 1_
- **Empty-pipeline crash, stacks in reports, frame in digest + dashboard (v4.6.55)** `2855c63`
- Root cause: splitPipe() splits on every '|', so `help |`, `| wc -l`, `a || b` produced a stage with no argv and executePipeChain() called parts[0].toLowerCase() on undefined — 989 occurrences, first filed 2026-10-20, unfixable from the report because bug reports dropped the stack that named executePipeChain:3955. Same unguarded read existed in _classify().
- terminal.js: _classify() returns an inert classification for empty argv; executePipeChain()
- parses each stage once, filters stages with no argv (so the last real stage stays final), warns only when the whole pipeline is empty. Last-stage test now compares against the filtered length.
- module-system.js: `bug` diagnostics include the stack (10 frames, 1200 chars); auto-filed reports
- carry it too — they are the majority of the inbox and were previously message-only.
- … run `git show <sha>` for the rest
- **Fix ledger + norm clustering — the loop now knows what a fix was worth** `eec18ba`
- Lifetime counts ranked an 8-day-old outage above today's live crash, and nothing asserted that a shipped fix actually held: a regression of a fixed bug looked exactly like a new bug.
- api/debug/ingest.php: norm_key() stores `norm` (host/port/scheme/`?v=`/`//` stripped) — the client
- hash covers the raw URL, so one outage became two signatures (`/ollama//api/tags` 18 203x and `/ollama/api/tags` 16 540x). PHP + Python implementations verified byte-identical on 5 real messages.
- scripts/newos4-ledger.py: data/maintenance/fixes.json maps norm -> {commit, fixed_at, note}, and
- verify() returns REGRESSED / WATCHING / CLOSED / UNSEEN. "Seen after the fix" alone is not a regression (a fix shipped 10d ago with one sighting 9d ago has been silent since) — regressed means seen after the fix AND recent; otherwise the silence wins.
- … run `git show <sha>` for the rest
## v4.6.54 — 2026-10-23
_Touches modules (3), data (2), downloads (2), main.html (1) · fix 1, project 1_
- **Define VirtualFS.emptyTrash() — empty-trash threw since 2026-10-18** `563a9be`
- Report RPT-20261018-164646-89352963 ("this.fs.emptyTrash is not a function") was filed by the auto-filer and never fixed: terminal.js:1777 called a method VirtualFS never defined, so 'empty-trash --confirm' threw instead of emptying. Added emptyTrash() returning true only when it removed something.
- Verified live on v4.6.54: put -> rm -> empty-trash --confirm => 'Trash emptied.'; second run => 'Trash is already empty.'; .trash entries 0.
- **Archive the delta-sync probe report (test artifact)** `235ab7c`
## v4.6.53 — 2026-10-23
_Touches data (9), api (7), modules (7), scripts (5) · release 3, fix 2, project 1_
- **Idempotent ingest, admin-gated reads, honest counts (v4.6.52-53)** `d86d508`
- Root cause: _syncToBackend() re-sent the whole error DB with cumulative counts every 300s, and ingest.php did count += client count per batch. 33 real signatures had accumulated 116,696 "occurrences" (one self-referential row alone: 46,133).
- Backend
- ingest.php: per-(session,hash) synced map, adds only the delta, resets prev when the
- client count shrank (cleared localStorage), caps delta at 5000, bounds synced at 500 hashes and sessions at 200, normalises timestamps to +00:00, 30-day retention that also purges dropped hashes from every synced map, returns `accepted` (entries) next to `ingested` (occurrences).
- stats.php/dashboard.php: total_errors = occurrences, unique_errors = signatures,
- last-ingest card. The two numbers were inconsistent by ~3,500x.
- health.php: reports self_referential count and status=degraded instead of ok.
- … run `git show <sha>` for the rest
- **Maintenance agent channel — triage engine, two-way replies, cron** `8a2abfb`
- scripts/newos4-triage.py: deterministic collector (records vs occurrences, real vs
- noise vs self-referential, new reports since last run, checker WARN/FAIL). Untrusted strings are sanitised and prefixed DATA| before they can reach an agent prompt; state + output written under flock with mode 644.
- scripts/newos4-reply.py: appends one reply to data/maintenance/replies.json under
- flock. Hermes writes here instead of POSTing with an admin cookie, so an expired session can never look like a delivered reply.
- api/debug/agent-replies.php: GET/POST (admin-gated by nginx) half of the thread.
- api/debug/report.php: accepts thread + kind=maintenance so in-app feedback is
- distinguishable from operator bug reports.
- terminal: `feedback <message>` and `feedback-inbox` commands; help.html rows added
- … run `git show <sha>` for the rest
- **Unmeasured skills are untracked, world-model writes verified, skill audit** `6a30129`
- External review packet (os4-external-review-v4.6.51.html) triaged against the code: 2 findings confirmed and fixed, 4 could not be reproduced and were recorded as such.
- module-memory-graph.js: getStaleSkills() treated a missing lastUsed as 999 days
- unused, so skill-audit recommended archiving every skill (22 of 22). Skills with no usage telemetry are now collected by getUntrackedSkills() and never counted stale. Live: "Stale: 0" + "Untracked: 22 (no usage telemetry — NOT stale)".
- module-world-model.js: _saveToVFS() ignored fsWriteJSON()'s return value and promoted
- whatever a truthy read returned. Now write -> read-back -> byte compare -> promote, with every return checked. Regression test modules/test/test-world-model-save.js (red on the old code: promoted=true with errs=[]; green now).
- scripts/newos4-skill-audit.py: 5 mechanical probes (version x3, read_file paging +
- … run `git show <sha>` for the rest
- **Drop the one-off delta-sync probe row** `1a3dc45`
- **Dashboard triage + thread panels, mobile layout, gate error.php** `0f5fdbd`
- Triage card reads admin/docs/newos4-triage-latest.json defensively: missing/unreadable
- renders as a warning pill, never as zeros (a zero here reads as "nothing is wrong"). Freshness pill goes stale past 26h so a dead collector cannot look healthy.
- Maintenance thread merges in-app `feedback` reports (kind=maintenance) with agent
- replies from data/maintenance/replies.json into one chronological view.
- Mobile: <=640px single column, smaller type, .table-scroll wrappers, Type column
- hidden, max-width clamp. verify-mobile.py: 360/390px, light+dark -> ALL VIEWPORTS OK, page width == viewport (no horizontal overflow).
- "Last ingest" now uses max(ingested_at) instead of end($errors): after the prune the
- array is not chronological and the header contradicted the triage card by 3 days.
- … run `git show <sha>` for the rest
- **Operator steering queue for the maintenance jobs** `c117065`
- newos4-steer.py add|list|done|clear appends directives under flock with unique ids; both cron jobs read it first and mark entries consumed. Verified: two adds with distinct ids, done only affects the named entry.
## v4.6.51 — 2026-10-22
_Touches modules (4), downloads (1), main.html (1), course (1) · fix 1, docs 1_
- **Dashboard shows the real world model and stops dying after 10 s (v4.6.51)** `c54ca24`
- Two defects, both verified live before/after:
- 1. The World Model panel always said "Not synthesized yet". The page had a renderer for data.worldModel and _collectDashboardData() never set it -- the model was right there in WorldModelModule._model. Now collected and mapped to the renderer's exact keys (model has modules.loaded/skills.count/memory.nodes, the page reads modules/skills/memoryNodes), plus changesSinceLastSnapshot and the recent-error sample, with an honest "not synthesized" fallback.
- 2. The page discarded any payload older than 10 s and its own 5 s auto-refresh then showed "Cannot connect to newOS4". Stale payloads are now rendered with their age ("Data as of 1:20:14 AM - 3m ago"), labelled amber past 5 min.
- … run `git show <sha>` for the rest
- **Clear the help.html command backlog, and make it detectable** `8fc4e01`
- help.html had not been touched since 2026-08-02. Auditing the page against the code found 74 commands the runtime registers but the docs never mentioned, and 4 rows documenting commands that no longer exist.
- Documented (by section): terminal 11 (dashboard --watch/--stop, usage, ps, top, df, free, history, run, sysmsg, view_module), a2a 6, AI 14 (webxai/webx/wx, context*/smart-ask, commit-*, ai-model/ai-ignore), vaultmind 4, debug 2, filesystem 2, sessions 1, memory-graph 3, modules 4, neural-vfs 1, agents 3, web 1, voice 3, config 3, extras 8 (kanban, three, course, gzip, wget, gui-*), vault 2 (encrypt/decrypt), skills 5, webdav 1, plus an alias row for h/s/ll.
- Removed ghost rows: commands, check, sandbox-kill, webdav-explorer -- none are registered anywhere in modules/; the sandbox prose no longer promises sandbox-kill.
- … run `git show <sha>` for the rest
## v4.6.50 — 2026-10-22
_Touches modules (5), main.html (1) · release 1_
- **World-model-ask answers with the LLM, grounded in the model (v4.6.50)** `6ddaadd`
- `world-model-ask` was a 10-keyword lookup: any real question returned "Unknown query. Try: modules, skills, ...". The documented example ("what changed since yesterday") compared nothing — it restated the current snapshot.
- _modelDigest(): whitelisted snapshot (measured 942-965 chars on fixtures,
- ~3.3 KB live) + recent error sample + changesSinceLastSnapshot
- _buildAskPrompt(): "answer only from this JSON, never invent numbers, name the
- command that would produce missing data"
- ask(): connects Ollama on demand, answers via chat(), falls back to the facts
- path with a visible warning; --facts keeps the instant lookup
- _diffSincePrev(): keeps one previous snapshot in /system/world-model.prev.json
- … run `git show <sha>` for the rest
## v4.6.49 — 2026-10-22
_Touches modules (4), main.html (1) · fix 1_
- **Coreutils flags + loop-guard arg key, both found by the agent's live run (v4.6.49)** `7f3508b`
- Agent session surfaced two real defects:
- 1. The A4 prompt told it to use grep -n / sed -n 'N,Mp' / wc -l / head -N — none of those flags parsed: grep treated '-n' as the pattern (6/6 variants failed), wc read '-l' as a filename, head looked for a file named '-2', sed had no range parser. Fixed the tools, not just the text: grep now accepts -n/-i/-c/-v/-l (line numbers were already printed), wc accepts -l/-w/-c, head accepts -N and -n N, sed accepts Np and N,Mp, tail no longer prints a blank line for a trailing newline. Prompt re-ranked to the working forms.
- … run `git show <sha>` for the rest
## v4.6.47 — 2026-10-21
_Touches modules (3), main.html (1) · fix 1_
- **GLM review follow-ups for AI read/context (v4.6.47)** `3fed8df`
- loop guard: a mutation unblocks the replay but no longer erases the repeat counter
- (identical mutating calls could loop forever)
- tool traces are per agent tag, flushed in a catch as well as on success, ring-buffered
- (first 2 + last 10) so verification reads survive
- read_file notes are metadata only (path/range/next_offset); no file bodies, no secrets,
- and web_search/web_fetch output is excluded from durable context (injection surface)
- trace is role:'system' (was 'assistant' — the model re-read it as its own answer)
- surrogate-safe pagination, 500 floor / 16000 cap, prompt limits aligned
- … run `git show <sha>` for the rest
## v4.6.46 — 2026-10-21
_Touches modules (3), main.html (1) · release 1_
- **AI paged file reads, durable tool traces, 12k context budget, read/tool/verify prompt rules (v4.6.46)** `44d023d`
- read_file: offset/limit + JSON {total_chars,truncated,next_offset} (was a hard 8000-char cut with no resume)
- tool results now leave a per-turn trace that is persisted into session context and survives compression
- maxContextTokens 2000 -> 12000; compression keeps 8 exchanges and 400 chars/msg
- system prompt: page big files, prefer grep -n / sed -n / head, re-read + verify after every edit
- verified in the live browser: 30k file pages 0->8000->16000, prompt section present, v4.6.46 served
## v4.6.45 — 2026-10-21
_Touches modules (7), main.html (1) · fix 1_
- **Download link uses the user's space; WebDAV share copies verified file links; preview fullscreen+share; files->WebDAV upload button (v4.6.45)** `8e7b3c5`
- download: URL built from _resolveSpaceName() (config url = logged-in user), not the page space
- WebDAV browser 🔗: shares selected FILES only, URL-encoded, HEAD-verified (200) before claiming success;
- folders offer a tar fallback instead of a guaranteed-403 link
- file manager preview: ⛶ fullscreen + 🔗 copy-link toolbar (was none)
- file manager toolbar: ☁ WebDAV publishes selected/current file via webdav-push
- modules/test/test-download-url.js: 4 PASS
- rebuild bundles, bump v4.6.44 -> v4.6.45 (loader + 3 script tags)
## v4.6.44 — 2026-10-18
_Touches modules (3), main.html (1), scripts (1) · fix 1_
- **Stop agent accumulation on reload — stable role ids + boot reconciliation** `9034ff3`
- spawnAgent() accepts an idHint and is idempotent; boot team uses agent-<role>
- _reanimateFromVFS() adopts newest dir per default role, fs.rm()s duplicates/orphans
- _bootAgents() awaits async VFS hydration before reconciling, then spawns only gaps
- scripts/test-agents-reload.js: 5-boot regression + hydration-race test
- v4.6.44 bundle rebuild + cache-bust
## v4.6.40 — 2026-10-18
_Touches modules (7), api (1), main.html (1) · project 1_
- **Review fixes (bug diagnostics, trash tree, dir paste, stale capture, preview)** `05257d7`
## v4.6.38 — 2026-10-18
_Touches modules (3), main.html (1) · project 1_
- **Auto-file fatal errors with 3/day quota (A5)** `dd73eee`
## v4.6.37 — 2026-10-18
_Touches modules (4), api (3), downloads (1), main.html (1) · project 2_
- **Bug-report pipeline (report.php, bug command, HTML generator, telemetry-loop fix)** `ea96355`
- **Regenerate course atlas (adds bug command)** `923bdbd`
## v4.6.36 — 2026-10-18
_Touches modules (2), main.html (1) · project 1_
- **Fix CourseModule print-capture swallowing all command output** `3686850`
## v4.6.35 — 2026-10-18
_Touches modules (3), main.html (1) · project 1_
- **File manager multi-select + safe paste/rename/delete (Part B)** `7823821`
## v4.6.34 — 2026-10-18
_Touches modules (3), main.html (1) · project 1_
- **Running tool ledger in the prompt (D4)** `2f84d27`
## v4.6.33 — 2026-10-18
_Touches modules (3), main.html (1) · project 1_
- **Progress-based turn budget (D3)** `b777a5a`
## v4.6.31 — 2026-10-18
_Touches modules (3), main.html (1) · project 1_
- **Tool-result memo cache with epoch invalidation (D2)** `fe6d198`
## v4.6.30 — 2026-10-18
_Touches modules (4), main.html (1) · project 1_
- **Per-agent session isolation (D1)** `1c02f4c`
## v4.6.29 — 2026-10-18
_Touches modules (4), main.html (1) · project 1_
- **Progress-aware loop guard + agent attribution** `29de5fe`
## v4.6.28 — 2026-10-18
_Touches course (6), course.html (2), scripts (2), main.html (1) · project 4_
- **Cache-bust course.js after review fixes** `2c2461f`
- **Exclude auto-open from lesson_open telemetry, add session counts, filter synthetic sids** `9457e6c`
- **Weekly telemetry report script + Monday 09:00 cron** `3bf3694`
- **Fix P0 content errors + completion/mobile/keyboard UX (review A)** `486b10b`
## v4.6.27 — 2026-10-18
_Touches course (20), scripts (8), modules (5), api (2) · project 3_
- **Interactive AI course (terminal module, portal, atlas, telemetry, daily maintenance+cron) + free store card** `1bc6369`
- **First daily lesson generated, atlas 345 commands, modules-index refreshed** `7221eeb`
- **Post-exec review fixes (telemetry detail sanitize, skill-sync retry, atlas check)** `ccbf471`
## v4.6.26 — 2026-10-17
_Touches data (3), modules (2), downloads (2), scripts (2) · project 2_
- **2026-07-29_20:45 changes** `e55f2fe`
- **Floating-ui-designer v3 — repair 12 escape-mangled JS sites, rebuild 344-cmd registry (124->344), quoting/import/preset UX fixes; add Node harness + registry generator; floating-ui skill JSON de-piped** `bb41664`
## v4.6.25 — 2026-10-08
_Touches data (3), modules (2), api (1), main.html (1) · project 1_
- **2026-07-29_20:45 changes** `eb7462a`
## v4.6.18 — 2026-10-02
_Touches modules (2), main.html (1), scripts (1) · project 1_
- **Nginx dav trailing-slash semantics** `89c5beb`
- Discovered by dav-health-check.sh E2E (10/10 pass now):
- nginx dav module REQUIRES trailing slash on MKCOL (no-slash → 409)
- and on directory DELETE (no-slash → 409/404); existing dir + slash MKCOL → 405 (the real 'already exists' signal)
- _normalizeRemotePath now PRESERVES trailing slashes (collection ops)
- _ensureDir MKCOLs with trailing slash
- _deleteFile retries with slash on 409/404 (collection delete)
- health-check expectations corrected (405 = existing, slash required)
## v4.6.16 — 2026-10-02
_Touches modules (2), main.html (1), scripts (1) · project 1_
- **NewOS4 webdav path fix + WebX11 resilience package (2026-10-02)** `d3e5af7`
- Root cause fixes (mindtech incident):
- webdav.js: _normalizeRemotePath strips full URLs/space prefixes, applied
- to all request entry points + _absoluteUrl for MOVE/COPY Destinations
- _ensureDir: __list__ existence fast-path (kills MKCOL 409 storms)
- dashboard-files.js: updateFilesJson parses __list__ autoindex (no more
- '#' entries), ensureSpaceExists only re-inits on genuine 404
- 20-test regression suite: scripts/test-webdav-paths.mjs
- Resilience:
- … run `git show <sha>` for the rest
## v4.6.10 — 2026-10-01
_Touches modules (55), skills (36), downloads (32), data (11) · project 11, fix 4, release 2_
- …and 4 earlier commit(s) in this build
- **Add kanban to packages.json, update cache version to 2026-07-29** `c1b96f7`
- **2026-07-29_20:45 changes** `79f682c`
- **Tail -n/-N flags + live follow mode (-f) with test harness** `1692f20`
- **Exact-delta assertions for tail -f follow/stop semantics** `5f17863`
- **Sed command (s///, /pat/d, Nd, -i, -n, pipe input) + help entries** `d8c63a9`
- **Ctrl+C/Esc stops tail -f follow; security whitelist gains sed** `69f1fc5`
- **Rebuilt bundles with tail -f and sed** `ec02916`
- **Typo suggestion bar (one-tap fix), consistent unknown-cmd messages, AI loop guard** `2cf0d3d`
- **Race-safe AI loop guard (depth counter), cover _capture* entry points, structured JSON tool error** `8f2fb31`
- **Parse per-call captured output for unknown-cmd error (race-free under Promise.all)** `212c05d`
- **Regex captures full command name + suggestion in unknown-cmd tool error** `00a6f24`
- **Reentrant print-capture stack, AI tail -f block, read-only sed in safeRead, security test harness (21 checks)** `4e57424`
- **Sequential safe-tool execution — parallel fan-out corrupted print-capture attribution + shadow diff pairing** `0c5316e`
- **2026-07-29_20:45 changes** `b1fb657`
## before v4.6.10 — 2026-07-02
_Touches modules (317), skills (161), backups (49), downloads (40) · project 47_
- …and 33 earlier commit(s) in this build
- **2026-06-15_06:00 changes** `1e964c9`
- **2026-06-15_18:00 changes** `a0afeba`
- **2026-06-16_06:00 changes** `bd9db0e`
- **2026-06-17_06:01 changes** `75e5278`
- **2026-06-17_18:01 changes** `9f82275`
- **2026-06-18_06:00 changes** `b348bd5`
- **2026-06-18_18:00 changes** `7f5129f`
- **2026-06-19_18:01 changes** `c83f38a`
- **2026-06-22_18:00 changes** `536c2ad`
- **2026-06-23_06:00 changes** `e50d590`
- **2026-06-23_18:00 changes** `bdab7cb`
- **2026-06-24_06:01 changes** `c2c71ba`
- **2026-06-25_06:00 changes** `e683a33`
- **2026-07-02_06:00 changes** `ae177bd`
<!-- generated:end -->
|