volute 0.54.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/dist/{activity-events-LQII6PZS.js → activity-events-YASF46HS.js} +3 -3
  2. package/dist/{ai-service-YW5SWCSB.js → ai-service-3UFNG6GI.js} +3 -5
  3. package/dist/{archive-FOHGXHPT.js → archive-L2OFBGDQ.js} +2 -2
  4. package/dist/{auth-KRDSM55Q.js → auth-N2F5ASAB.js} +4 -4
  5. package/dist/auto-upgrade-O4MQPYQF.js +46 -0
  6. package/dist/{avatar-image-52WGXLHV.js → avatar-image-QEKKUFDF.js} +3 -3
  7. package/dist/{backup-QRNX6RK7.js → backup-TPM7QQBQ.js} +13 -13
  8. package/dist/{channels-CKCY64TD.js → channels-JP3GYTPX.js} +18 -18
  9. package/dist/{chat-ZUGNL5GS.js → chat-7YCF4ZU5.js} +3 -3
  10. package/dist/{chunk-QUSMYGNQ.js → chunk-2674KIGI.js} +1 -0
  11. package/dist/{chunk-444RR3YF.js → chunk-2B4E6SL7.js} +14 -14
  12. package/dist/chunk-2GATCSZK.js +520 -0
  13. package/dist/{chunk-I7U5EEZC.js → chunk-2LFTJNVH.js} +2 -2
  14. package/dist/{chunk-L7BKTZ5U.js → chunk-2QLWAYIU.js} +2 -2
  15. package/dist/{chunk-F6D766VQ.js → chunk-45MCH4G4.js} +2 -2
  16. package/dist/{chunk-OGPVJ53N.js → chunk-4RZS7XCV.js} +5 -5
  17. package/dist/{chunk-HKGYZVWN.js → chunk-6LICEQGP.js} +38 -4
  18. package/dist/{chunk-LAUCO2HC.js → chunk-APDEUAIE.js} +1 -1
  19. package/dist/{chunk-5Y54AE3L.js → chunk-CLH357FI.js} +2 -2
  20. package/dist/{chunk-OAYY3WF3.js → chunk-CVPCRWWC.js} +1 -12
  21. package/dist/{chunk-EGV6R6UF.js → chunk-E2SN3ZOJ.js} +5 -40
  22. package/dist/{chunk-LGXVGIWN.js → chunk-ELQU2GBT.js} +193 -69
  23. package/dist/{chunk-OT5SXF7L.js → chunk-EPRLTYJR.js} +28 -14
  24. package/dist/{chunk-ATZF2RNJ.js → chunk-EYFBT2KP.js} +15 -15
  25. package/dist/{chunk-U54PJPTB.js → chunk-FYMIGUJ5.js} +2 -2
  26. package/dist/{chunk-7XKPG66D.js → chunk-G7A5LEVI.js} +5 -5
  27. package/dist/{chunk-RGHF3OK5.js → chunk-GAAATVTM.js} +17 -17
  28. package/dist/{chunk-7LT3YZUR.js → chunk-I3OJ342S.js} +2 -2
  29. package/dist/{chunk-3LHUXY3E.js → chunk-IABDYN4N.js} +4 -4
  30. package/dist/{chunk-VDUCLVZI.js → chunk-IOMBP2T5.js} +28 -26
  31. package/dist/{chunk-YSRYCS3N.js → chunk-IQROTVBG.js} +2 -2
  32. package/dist/{chunk-PPSN2MLC.js → chunk-IRNM4LSF.js} +9 -6
  33. package/dist/{chunk-ZNHL5KQJ.js → chunk-KN3UHV7O.js} +8 -3
  34. package/dist/{chunk-CV6BNGFK.js → chunk-KXM4K46T.js} +4 -4
  35. package/dist/{chunk-Z4Q6TQWL.js → chunk-LRRPKZBD.js} +2 -2
  36. package/dist/{chunk-LNNI3MS4.js → chunk-MNKPAAQF.js} +9 -2
  37. package/dist/{chunk-OIOW3FPT.js → chunk-NA2NJXY4.js} +1 -1
  38. package/dist/{chunk-GLM7TVWP.js → chunk-OHHS5PSI.js} +1 -1
  39. package/dist/{chunk-CKPAVQNK.js → chunk-OPTA3BJ4.js} +36 -24
  40. package/dist/{chunk-X67BDDZR.js → chunk-P3GXOT2X.js} +48 -2
  41. package/dist/{chunk-WHL6PKYP.js → chunk-PAO7JZNW.js} +52 -48
  42. package/dist/{chunk-TPX6E6PL.js → chunk-Q5M5QFKY.js} +3 -3
  43. package/dist/{chunk-OYF25B62.js → chunk-T7UMSTH2.js} +3 -3
  44. package/dist/{chunk-H6X6CKDN.js → chunk-TAVIVN2I.js} +2 -32
  45. package/dist/{chunk-56IT2ZXF.js → chunk-TGYKFUCF.js} +9 -15
  46. package/dist/{chunk-5U5GLZSD.js → chunk-TZ7OHGNL.js} +2 -2
  47. package/dist/{chunk-UFTDRTCP.js → chunk-UMQP5TC5.js} +2 -2
  48. package/dist/{chunk-LKUGUQUW.js → chunk-UNTDRIV2.js} +5 -5
  49. package/dist/{chunk-CLCHOXLR.js → chunk-V2M6PNLE.js} +5 -5
  50. package/dist/{chunk-ICXY54ZC.js → chunk-WTZD6LFW.js} +2 -2
  51. package/dist/{chunk-FLIEXG2T.js → chunk-XZP3FJ7Q.js} +1 -1
  52. package/dist/{chunk-HTRSSOHY.js → chunk-YU5EYQAE.js} +1 -1
  53. package/dist/cli.js +26 -23
  54. package/dist/{clock-X7V3C7NL.js → clock-ZT5YT6HA.js} +2 -2
  55. package/dist/{cloud-sync-52YNJRMR.js → cloud-sync-RTUXMYU2.js} +23 -23
  56. package/dist/{conversations-ZL6PHROD.js → conversations-ERMMPMOS.js} +4 -4
  57. package/dist/{create-G2RIKQYK.js → create-IUCFJXCE.js} +1 -1
  58. package/dist/{daemon-restart-QWHN5VMU.js → daemon-restart-N2YOISNJ.js} +11 -6
  59. package/dist/daemon.js +230 -279
  60. package/dist/{db-BONH4SUG.js → db-BQFYIONA.js} +1 -1
  61. package/dist/{db-I4EH5SLF.js → db-JH3NTERX.js} +17 -3
  62. package/dist/delivery-manager-5EGCFN4K.js +41 -0
  63. package/dist/{delivery-notices-RZQ6GONP.js → delivery-notices-LZKZXWTX.js} +6 -6
  64. package/dist/{delivery-router-QS4NLJR6.js → delivery-router-U6Q22ZYU.js} +3 -3
  65. package/dist/doctor-225FYBAQ.js +454 -0
  66. package/dist/{down-UN2EDX2Y.js → down-3BQSPUAZ.js} +4 -4
  67. package/dist/echo-text-Y2DFTD7L.js +40 -0
  68. package/dist/{env-SQFCWW52.js → env-4YX5YNWZ.js} +2 -2
  69. package/dist/{exec-Z34TTUO3.js → exec-Y5VMDAVA.js} +2 -2
  70. package/dist/{export-VBNWM7I4.js → export-ASRHBMRG.js} +2 -2
  71. package/dist/{extensions-QT65JGV6.js → extensions-YEPEWURN.js} +27 -23
  72. package/dist/{files-PCKBQHAY.js → files-FASUDSZQ.js} +3 -3
  73. package/dist/{import-DB2KULRO.js → import-O35Y7DSF.js} +6 -6
  74. package/dist/{isolation-UQPDO34R.js → isolation-ZOAUXYXA.js} +2 -2
  75. package/dist/{login-57MMYILL.js → login-54TZQQP4.js} +5 -5
  76. package/dist/{logout-YFZAJRPY.js → logout-4G46Z4RU.js} +1 -1
  77. package/dist/message-delivery-HBTMGWFU.js +46 -0
  78. package/dist/{mind-CT6ANXK4.js → mind-IKMX7K7G.js} +8 -24
  79. package/dist/{mind-activity-tracker-QF6DCX2X.js → mind-activity-tracker-ABAEIMA3.js} +4 -4
  80. package/dist/mind-service-LRVOQG2T.js +48 -0
  81. package/dist/{mind-sleep-24HV4RCA.js → mind-sleep-LZAFBDZD.js} +2 -2
  82. package/dist/{package-F6S5NHY2.js → package-X5O7OD2E.js} +12 -13
  83. package/dist/{prompts-JECJIFPQ.js → prompts-ZNOE3KN3.js} +2 -2
  84. package/dist/{registry-TRC2HJXR.js → registry-BTQXLDW6.js} +1 -1
  85. package/dist/{sandbox-NNWCTHRI.js → sandbox-OUFAGN45.js} +3 -3
  86. package/dist/scheduler-G6BCIVGP.js +27 -0
  87. package/dist/{seed-cmd-6IMTO3LG.js → seed-cmd-BGAW3WDY.js} +2 -2
  88. package/dist/{seed-create-VNLDYRFK.js → seed-create-SMCPEUFQ.js} +3 -3
  89. package/dist/{seed-readiness-H5TTPAQH.js → seed-readiness-RBANMSWW.js} +3 -3
  90. package/dist/{seed-sprout-PPGYGK4G.js → seed-sprout-CSHV36N3.js} +9 -9
  91. package/dist/{send-UXUQKO4N.js → send-D3ADUBR5.js} +6 -6
  92. package/dist/{service-T4P5W6RS.js → service-SWEEBOOY.js} +3 -24
  93. package/dist/{service-install-R4SNYUVO.js → service-install-RCC26KDV.js} +4 -4
  94. package/dist/{setup-YUAQMDVZ.js → setup-L5NZG2U3.js} +5 -5
  95. package/dist/{setup-WZSXHHRJ.js → setup-R7Q4HFJP.js} +1 -5
  96. package/dist/skills/volute-mind/SKILL.md +8 -0
  97. package/dist/{skills-GMQ5UIIW.js → skills-BCCU4OJW.js} +4 -4
  98. package/dist/sleep-manager-25P44HAO.js +42 -0
  99. package/dist/{spirit-Q5VGFWUI.js → spirit-YPG2MU6W.js} +7 -7
  100. package/dist/spirit-availability-ZQM5IH4C.js +43 -0
  101. package/dist/{src-H3UCOP6B.js → src-246GSX7A.js} +5 -2
  102. package/dist/src-LOUB3WPR.js +4189 -0
  103. package/dist/{status-LCKTTIWS.js → status-BYPCJU6H.js} +4 -4
  104. package/dist/{system-events-Q7IVZ5T4.js → system-events-CWC7CBKP.js} +6 -4
  105. package/dist/{systems-HL7SGSW7.js → systems-VDRUZXNF.js} +2 -2
  106. package/dist/{tailscale-KHNZFC7L.js → tailscale-4XY7B5A2.js} +2 -2
  107. package/dist/{template-hash-YKWMVYMM.js → template-hash-RJRMF6DT.js} +2 -2
  108. package/dist/{template-staleness-PY2CULH6.js → template-staleness-CL4V7BT7.js} +5 -5
  109. package/dist/{turn-tracker-HZDPMSCC.js → turn-tracker-LZP5X6GZ.js} +8 -8
  110. package/dist/{up-XZDSJVMU.js → up-3AAN4GYY.js} +4 -4
  111. package/dist/{update-ILCPVNDD.js → update-2MWXRE2S.js} +4 -4
  112. package/dist/{update-check-SZ7B5IFI.js → update-check-PJOZXZYJ.js} +2 -2
  113. package/dist/variant-cleanup-K7RFMSH7.js +24 -0
  114. package/dist/{version-notify-VNVFX6UP.js → version-notify-QIDHR6WO.js} +12 -12
  115. package/dist/web-assets/assets/{index-CwtxGwe_.js → index-Bfvls20e.js} +17 -17
  116. package/dist/web-assets/assets/index-hPn_fkz3.css +1 -0
  117. package/dist/web-assets/index.html +2 -2
  118. package/drizzle/0000_crazy_epoch.sql +232 -0
  119. package/drizzle/README.md +42 -0
  120. package/drizzle/meta/0000_snapshot.json +1420 -3
  121. package/drizzle/meta/_journal.json +2 -128
  122. package/package.json +12 -13
  123. package/packages/extensions/intentions/dist/ui/assets/index-Bl5KEcli.js +67 -0
  124. package/packages/extensions/intentions/dist/ui/index.html +1 -1
  125. package/packages/extensions/pages/dist/ui/assets/index-BKxVQkA9.css +1 -0
  126. package/packages/extensions/pages/dist/ui/assets/index-BMt6sStP.js +68 -0
  127. package/packages/extensions/pages/dist/ui/index.html +2 -2
  128. package/packages/extensions/pages/skills/pages/SKILL.md +123 -4
  129. package/templates/_base/.init/.config/prompts.json +0 -1
  130. package/templates/_base/.init/.local/hooks/pre-prompt/turn-context.ts +30 -0
  131. package/templates/_base/src/lib/codex-session-seed.ts +6 -55
  132. package/templates/_base/src/lib/pi-session-seed.ts +8 -57
  133. package/templates/_base/src/lib/seed-note.ts +4 -4
  134. package/templates/_base/src/lib/session-seed.ts +8 -53
  135. package/templates/_base/src/lib/startup.ts +0 -20
  136. package/templates/claude/package.json +2 -2
  137. package/templates/claude/src/agent.ts +34 -102
  138. package/templates/claude/src/lib/hooks/pre-compact.ts +6 -6
  139. package/templates/claude/src/server.ts +0 -1
  140. package/templates/codex/package.json +2 -2
  141. package/templates/codex/src/agent.ts +23 -89
  142. package/templates/pi/package.json +1 -1
  143. package/templates/pi/src/agent.ts +26 -93
  144. package/templates/pi/src/server.ts +0 -1
  145. package/dist/auto-upgrade-HEVZKPTF.js +0 -46
  146. package/dist/chunk-AAPNU7GA.js +0 -168
  147. package/dist/delivery-manager-7R2YIQ2W.js +0 -41
  148. package/dist/echo-text-73CYQRL7.js +0 -40
  149. package/dist/message-delivery-TRKT4T2P.js +0 -46
  150. package/dist/migrate-name-placeholder-C7NCNLWF.js +0 -37
  151. package/dist/migrate-thread-config-EQSEP2XG.js +0 -90
  152. package/dist/mind-service-7EFIJ3ZA.js +0 -48
  153. package/dist/scheduler-73Q4MQ7Y.js +0 -27
  154. package/dist/seed-DRKAP3XV.js +0 -11
  155. package/dist/sleep-manager-KHI65FEX.js +0 -42
  156. package/dist/spirit-availability-G5FMWKRH.js +0 -43
  157. package/dist/sprout-PMTRL7QN.js +0 -11
  158. package/dist/src-3FIW7DLH.js +0 -2376
  159. package/dist/src-LTTQVN2Z.js +0 -832
  160. package/dist/variant-HHDTW74J.js +0 -62
  161. package/dist/variant-cleanup-QHGGY7ND.js +0 -24
  162. package/dist/web-assets/assets/index-DhM1yYRN.css +0 -1
  163. package/drizzle/0000_baseline.sql +0 -152
  164. package/drizzle/0001_add_conversation_private.sql +0 -1
  165. package/drizzle/0002_turns.sql +0 -21
  166. package/drizzle/0003_turn_feed_links.sql +0 -11
  167. package/drizzle/0004_spirits.sql +0 -5
  168. package/drizzle/0005_meta_summaries.sql +0 -15
  169. package/drizzle/0006_channels.sql +0 -17
  170. package/drizzle/0007_drop_conversation_name_title.sql +0 -11
  171. package/drizzle/0008_performance_indexes.sql +0 -6
  172. package/drizzle/0009_mind_notices.sql +0 -14
  173. package/drizzle/0010_delivery_queue_redrive.sql +0 -10
  174. package/drizzle/0011_drop_turn_summary_event_id.sql +0 -3
  175. package/drizzle/0012_channel_gates.sql +0 -17
  176. package/drizzle/0013_variant_purpose.sql +0 -3
  177. package/drizzle/0014_system_events.sql +0 -26
  178. package/drizzle/0015_rename_session_to_thread.sql +0 -12
  179. package/drizzle/0016_brain_to_human.sql +0 -4
  180. package/drizzle/0017_event_history_rows.sql +0 -9
  181. package/drizzle/0018_api_tokens.sql +0 -20
  182. package/drizzle/meta/0001_snapshot.json +0 -7
  183. package/drizzle/meta/0002_snapshot.json +0 -7
  184. package/drizzle/meta/0003_snapshot.json +0 -7
  185. package/drizzle/meta/0004_snapshot.json +0 -7
  186. package/drizzle/meta/0005_snapshot.json +0 -7
  187. package/drizzle/meta/0006_snapshot.json +0 -7
  188. package/drizzle/meta/0007_snapshot.json +0 -7
  189. package/packages/extensions/intentions/dist/ui/assets/index-CQz4aceM.js +0 -67
  190. package/packages/extensions/notes/dist/ui/assets/index-BBs8pyel.js +0 -67
  191. package/packages/extensions/notes/dist/ui/assets/index-D3cdui42.css +0 -1
  192. package/packages/extensions/notes/dist/ui/index.html +0 -14
  193. package/packages/extensions/notes/skills/notes/SKILL.md +0 -90
  194. package/packages/extensions/pages/dist/ui/assets/index-B589e8Xe.js +0 -2
  195. package/packages/extensions/pages/dist/ui/assets/index-BuXnE-rb.css +0 -1
  196. package/dist/{env-7AZFBPWX.js → env-IBW2QI6K.js} +5 -5
  197. package/dist/{login-4NUS72Z5.js → login-RJJTCVR3.js} +3 -3
  198. package/dist/{mind-contacts-PEXCDRXA.js → mind-contacts-KV64UDJS.js} +6 -6
  199. package/dist/{mind-history-RZMMCHMX.js → mind-history-H45GM2M2.js} +6 -6
  200. package/dist/{register-WNCSOTAU.js → register-OX4HRSGC.js} +3 -3
  201. package/dist/{restart-B4GNMVF2.js → restart-AGEGNSYQ.js} +4 -4
  202. package/dist/{skill-QYTBUDNQ.js → skill-B5OJEQUA.js} +4 -4
  203. package/dist/{start-BDHJVOVX.js → start-MUMNFR52.js} +3 -3
  204. package/dist/{stop-45QYSGYS.js → stop-EKERNYTX.js} +4 -4
@@ -0,0 +1,520 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/extensions/pages/src/db.ts
4
+ function initDb(db) {
5
+ db.exec(`
6
+ CREATE TABLE IF NOT EXISTS published_pages (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ mind TEXT NOT NULL,
9
+ file TEXT NOT NULL,
10
+ published_at TEXT NOT NULL DEFAULT (datetime('now')),
11
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
12
+ author TEXT
13
+ );
14
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_pp_mind_file ON published_pages(mind, file);
15
+ CREATE INDEX IF NOT EXISTS idx_pp_updated_at ON published_pages(updated_at);
16
+ -- The ambient tier filters and orders on published_at (an artifact appears when
17
+ -- it came into existence, not when it was last edited), and does so on every
18
+ -- turn of every mind. updated_at was the only indexed date before this.
19
+ CREATE INDEX IF NOT EXISTS idx_pp_published_at ON published_pages(published_at);
20
+
21
+ -- Social layer, keyed on page identity (mind, file) rather than a row id, so a
22
+ -- thread survives the page being rewritten, republished, or deleted.
23
+ -- page_hash records the published_pages.hash the comment was written against; a
24
+ -- mismatch with the page's current hash is exactly what "this comment refers to
25
+ -- an older version" means. No content snapshotting, no separate versioning.
26
+ -- body_mind/body_file is the optional page pointer: the comment's body also
27
+ -- lives as a page in the responder's own space. Without it a comment is a
28
+ -- pebble and stays cheap; with it the same response also counts as the
29
+ -- responder's own work. One mechanism at two weights.
30
+ CREATE TABLE IF NOT EXISTS page_comments (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ mind TEXT NOT NULL,
33
+ file TEXT NOT NULL,
34
+ author_id INTEGER NOT NULL,
35
+ content TEXT NOT NULL,
36
+ page_hash TEXT,
37
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
38
+ );
39
+ CREATE INDEX IF NOT EXISTS idx_page_comments_page ON page_comments(mind, file);
40
+ -- The ambient tier asks "which pages have been spoken on since the horizon" on
41
+ -- every turn, to avoid grouping over every comment ever written.
42
+ CREATE INDEX IF NOT EXISTS idx_page_comments_created_at ON page_comments(created_at);
43
+
44
+ CREATE TABLE IF NOT EXISTS page_reactions (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ mind TEXT NOT NULL,
47
+ file TEXT NOT NULL,
48
+ user_id INTEGER NOT NULL,
49
+ emoji TEXT NOT NULL,
50
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
51
+ );
52
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_page_reactions_unique
53
+ ON page_reactions(mind, file, user_id, emoji);
54
+ CREATE INDEX IF NOT EXISTS idx_page_reactions_page ON page_reactions(mind, file);
55
+
56
+ -- Citations: an @name written in a page body. Deliberately NOT a notice \u2014
57
+ -- where a mention appears decides its tier. In a page body it is a citation:
58
+ -- ambient, highlighted, exactly the same cost as a link. In a comment it is a
59
+ -- hail and goes down the recordNotice path. If naming a mind obligated them
60
+ -- while linking their work did not, a small house would learn to cite by link
61
+ -- and never by name.
62
+ --
63
+ -- The mentioned column is the name as written, NOT a verified user. Extracting it
64
+ -- happens while reading files off disk (describePages), which is synchronous
65
+ -- and has no user lookup; resolution happens at query time instead, where
66
+ -- citationsOf() is always called with a real username. A row for an @token
67
+ -- that belongs to nobody is simply never matched, so the index costs a little
68
+ -- noise rather than an async dependency in the publish path.
69
+ CREATE TABLE IF NOT EXISTS page_citations (
70
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
71
+ mind TEXT NOT NULL,
72
+ file TEXT NOT NULL,
73
+ mentioned TEXT NOT NULL,
74
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
75
+ );
76
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_page_citations_unique
77
+ ON page_citations(mind, file, mentioned);
78
+ CREATE INDEX IF NOT EXISTS idx_page_citations_mentioned ON page_citations(mentioned);
79
+
80
+ -- Read signals: that someone opened this page, once per reader per page.
81
+ --
82
+ -- Presence, deliberately not a metric. The unique index is the whole design:
83
+ -- re-opening a page is not more presence, so a second visit writes nothing and
84
+ -- the row keeps its *first* time. That also means the table cannot answer "how
85
+ -- often does X come back", which is a question nobody here should be able to
86
+ -- ask about a reader.
87
+ --
88
+ -- The author of a page is never recorded reading it \u2014 they know they were
89
+ -- there, and counting it would make the number mean nothing.
90
+ --
91
+ -- created_at is a first-open time rather than a counter so the alibi
92
+ -- hypothesis in #807 stays checkable later: reads over time next to cross-mind
93
+ -- comments over time is the measurement, and it needs timestamps, not totals.
94
+ -- Two honest limits on that. It is a live table, not an audit log: rows go
95
+ -- when a page is hard-deleted (see retirePage), so the series under-counts
96
+ -- reads of pages that were later removed \u2014 plausibly the pages that landed
97
+ -- least. And a re-measure should be taken from this table directly rather
98
+ -- than assuming it is complete back to the beginning.
99
+ CREATE TABLE IF NOT EXISTS page_reads (
100
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
101
+ mind TEXT NOT NULL,
102
+ file TEXT NOT NULL,
103
+ reader_id INTEGER NOT NULL,
104
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
105
+ );
106
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_page_reads_unique
107
+ ON page_reads(mind, file, reader_id);
108
+ CREATE INDEX IF NOT EXISTS idx_page_reads_page ON page_reads(mind, file);
109
+
110
+ -- Links from a page to another mind's site. The second highlighting signal in
111
+ -- the ambient tier, alongside page_citations, and the same weight as one: a
112
+ -- page that links your work is highlighted, never promoted to a notice.
113
+ --
114
+ -- Same shape and same honesty as page_citations: target is the name as
115
+ -- written, not a verified user, because extraction happens while reading files
116
+ -- off disk and has no user lookup. Detection is the crude substring test from
117
+ -- commonsReport rather than a second, cleverer one \u2014 see links.ts.
118
+ CREATE TABLE IF NOT EXISTS page_links (
119
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
120
+ mind TEXT NOT NULL,
121
+ file TEXT NOT NULL,
122
+ target TEXT NOT NULL,
123
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
124
+ );
125
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_page_links_unique
126
+ ON page_links(mind, file, target);
127
+ CREATE INDEX IF NOT EXISTS idx_page_links_target ON page_links(target);
128
+
129
+ -- What the ambient tier has already put in front of a mind.
130
+ --
131
+ -- Read this next to ambient_state, because the pair is the whole of #807's
132
+ -- "watermark, never an unread set", and it is easy to mistake this table for
133
+ -- the thing that rule forbids.
134
+ --
135
+ -- The forbidden structure is a set of things a mind *has not* seen: it grows
136
+ -- when the house is productive, it is answerable to "how many are left", and
137
+ -- it turns into a backlog the moment anything renders it. This table is the
138
+ -- exact inverse \u2014 a record of what has *already* been shown, which only ever
139
+ -- suppresses. It cannot be counted into a debt because everything in it is
140
+ -- done. Nothing outside the selection filter ever reads it, and no query here
141
+ -- asks it what is missing.
142
+ --
143
+ -- The watermark alone would very nearly do this job, and briefly did: advance
144
+ -- it past everything considered, and an artifact is structurally unrepeatable.
145
+ -- It is kept as well because a timestamp comparison quietly stops guaranteeing
146
+ -- anything whenever a timestamp moves \u2014 republishing over a tombstone revives
147
+ -- published_at, and the notes migration writes historical ones. A guarantee
148
+ -- that survives only while nobody rewrites a date is not a guarantee.
149
+ --
150
+ -- Two timestamps, because the tiers ask different questions of this table:
151
+ --
152
+ -- created_at \u2014 the FIRST time this artifact was ever put in front of this
153
+ -- mind. The ambient *live* tier keys off mere existence of
154
+ -- the row: a page is announced as new exactly once, ever.
155
+ -- last_shown_at \u2014 the MOST RECENT time. The *archive* tier keys off this,
156
+ -- because it is allowed to come back around.
157
+ --
158
+ -- Once-ever is right for "whorl published a thing" and wrong for the archive.
159
+ -- Applied to the archive it produces a terminal state: a mind that has walked
160
+ -- the whole shelf never hears from that tier again \u2014 weeks away at this
161
+ -- corpus size, not years \u2014 and then a quiet house makes a quiet digest makes a
162
+ -- quiet house. That spiral is the exact thing the retrospective tier exists to
163
+ -- break, so it must not be the tier that starts it. Re-encountering a page you
164
+ -- read half a year ago is not repetition; it is what an archive is *for*.
165
+ --
166
+ -- The author column is denormalized so fairness weighting is one grouped read on the
167
+ -- turn path: "whose work has this mind met least" is the selection's central
168
+ -- question and must not cost a join per candidate.
169
+ CREATE TABLE IF NOT EXISTS ambient_shown (
170
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
171
+ viewer TEXT NOT NULL,
172
+ kind TEXT NOT NULL,
173
+ ref TEXT NOT NULL,
174
+ last_shown_at TEXT,
175
+ author TEXT NOT NULL,
176
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
177
+ );
178
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ambient_shown_unique
179
+ ON ambient_shown(viewer, kind, ref);
180
+ CREATE INDEX IF NOT EXISTS idx_ambient_shown_author ON ambient_shown(viewer, author);
181
+
182
+ -- Per-mind ambient state: how far back to look, and when we last spoke.
183
+ --
184
+ -- The watermark is a horizon, not a queue head. It advances to *now* whenever a
185
+ -- block is emitted, which means anything new that the block did not select is
186
+ -- dropped from the live tier rather than held for later. That is deliberate
187
+ -- and it is the design: a mind returning meets what is on the shelf now, not
188
+ -- an accumulation. Work that falls past the horizon is not lost \u2014 the
189
+ -- retrospective tier reaches into the archive, which is precisely where old
190
+ -- work belongs.
191
+ --
192
+ -- A row is created with watermark = now on a mind's first ask, so a mind
193
+ -- that has just arrived is not handed the entire history as though it were
194
+ -- news. Its introduction to the archive is the retrospective tier's job, at a
195
+ -- pace that tier chooses.
196
+ --
197
+ -- The last_block_at column is the rate limit. A burst of publishes produces one block,
198
+ -- not one per page.
199
+ CREATE TABLE IF NOT EXISTS ambient_state (
200
+ viewer TEXT PRIMARY KEY,
201
+ watermark TEXT NOT NULL,
202
+ last_block_at TEXT
203
+ );
204
+
205
+ -- Ledger for the one-way notes -> pages migration. Keyed on the source note id
206
+ -- so a re-run is a no-op rather than a second set of files and threads.
207
+ CREATE TABLE IF NOT EXISTS migrated_notes (
208
+ note_id INTEGER PRIMARY KEY,
209
+ mind TEXT NOT NULL,
210
+ file TEXT NOT NULL,
211
+ migrated_at TEXT NOT NULL DEFAULT (datetime('now'))
212
+ );
213
+
214
+ -- One-time homepage invitations. A row means the cue was delivered \u2014 written
215
+ -- only after a successful recordNotice, so a failed send retries on the next
216
+ -- mind start rather than being lost. Per-mind rather than a flag file (the way
217
+ -- the commons cue flags the spirit) because every mind gets one, not just the spirit.
218
+ CREATE TABLE IF NOT EXISTS homepage_cue_sent (
219
+ mind TEXT PRIMARY KEY,
220
+ sent_at TEXT NOT NULL DEFAULT (datetime('now'))
221
+ );
222
+ `);
223
+ addColumns(db, "published_pages", [
224
+ "author TEXT",
225
+ "hash TEXT",
226
+ "deleted_at TEXT",
227
+ // Per-page `comments: false` frontmatter. Default open — not everything wants
228
+ // to be an invitation, but the default is that it is.
229
+ "comments_closed INTEGER NOT NULL DEFAULT 0"
230
+ ]);
231
+ addColumns(db, "ambient_shown", ["last_shown_at TEXT"]);
232
+ addColumns(db, "page_comments", [
233
+ // "comment" (a response) or "publish" (the --shared message that explains a
234
+ // change). Both live in the thread; only a response is an invitation.
235
+ "kind TEXT NOT NULL DEFAULT 'comment'",
236
+ "body_mind TEXT",
237
+ "body_file TEXT"
238
+ ]);
239
+ }
240
+ function addColumns(db, table, columns) {
241
+ for (const column of columns) {
242
+ try {
243
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column}`);
244
+ } catch (err) {
245
+ if (!(err instanceof Error) || !err.message.includes("duplicate column")) {
246
+ throw err;
247
+ }
248
+ }
249
+ }
250
+ }
251
+ function isSiteHome(file) {
252
+ return file === "index.html" || file === "index.md";
253
+ }
254
+ function sortSiteFiles(files) {
255
+ return [...files].sort((a, b) => {
256
+ if (isSiteHome(a.file) !== isSiteHome(b.file)) return isSiteHome(a.file) ? -1 : 1;
257
+ return b.updated_at.localeCompare(a.updated_at);
258
+ });
259
+ }
260
+ function getPublishedPages(db, mind) {
261
+ return db.prepare(
262
+ `SELECT file, published_at, updated_at, author FROM published_pages
263
+ WHERE mind = ? AND deleted_at IS NULL ORDER BY file`
264
+ ).all(mind);
265
+ }
266
+ function getRecentPages(db, opts) {
267
+ const limit = opts?.limit ?? 10;
268
+ if (opts?.mind) {
269
+ return db.prepare(
270
+ `SELECT mind, file, updated_at, author FROM published_pages
271
+ WHERE mind = ? AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT ?`
272
+ ).all(opts.mind, limit);
273
+ }
274
+ return db.prepare(
275
+ `SELECT mind, file, updated_at, author FROM published_pages
276
+ WHERE deleted_at IS NULL ORDER BY updated_at DESC LIMIT ?`
277
+ ).all(limit);
278
+ }
279
+ function getAllSites(db) {
280
+ const rows = db.prepare(
281
+ `SELECT mind, file, updated_at, author FROM published_pages
282
+ WHERE mind != '_system' AND deleted_at IS NULL ORDER BY updated_at DESC, id DESC`
283
+ ).all();
284
+ const siteMap = /* @__PURE__ */ new Map();
285
+ for (const row of rows) {
286
+ let files = siteMap.get(row.mind);
287
+ if (!files) {
288
+ files = [];
289
+ siteMap.set(row.mind, files);
290
+ }
291
+ files.push({ file: row.file, updated_at: row.updated_at, author: row.author });
292
+ }
293
+ return Array.from(siteMap.entries()).map(([mind, files]) => ({
294
+ mind,
295
+ files: sortSiteFiles(files)
296
+ }));
297
+ }
298
+ function getSystemPages(db) {
299
+ const rows = db.prepare(
300
+ `SELECT mind, file, updated_at, author FROM published_pages
301
+ WHERE mind = '_system' AND deleted_at IS NULL ORDER BY updated_at DESC, id DESC`
302
+ ).all();
303
+ if (rows.length === 0) return null;
304
+ return {
305
+ mind: "_system",
306
+ files: sortSiteFiles(
307
+ rows.map((r) => ({ file: r.file, updated_at: r.updated_at, author: r.author }))
308
+ )
309
+ };
310
+ }
311
+ function getMindsWithSites(db) {
312
+ return db.prepare(
313
+ `SELECT DISTINCT mind FROM published_pages
314
+ WHERE mind != '_system' AND deleted_at IS NULL ORDER BY mind`
315
+ ).all().map((r) => r.mind);
316
+ }
317
+ function getPage(db, mind, file) {
318
+ return db.prepare(
319
+ `SELECT mind, file, hash, published_at, updated_at, author, deleted_at
320
+ FROM published_pages WHERE mind = ? AND file = ?`
321
+ ).get(mind, file) ?? null;
322
+ }
323
+ function getLivePageFiles(db, mind) {
324
+ return db.prepare(
325
+ "SELECT file FROM published_pages WHERE mind = ? AND deleted_at IS NULL ORDER BY file"
326
+ ).all(mind).map((r) => r.file);
327
+ }
328
+ function knownPageFiles(db, mind) {
329
+ const rows = db.prepare("SELECT file FROM published_pages WHERE mind = ?").all(mind);
330
+ return new Set(rows.map((r) => r.file));
331
+ }
332
+ function pagesWithThreads(db, mind) {
333
+ const rows = db.prepare(
334
+ `SELECT file FROM page_comments WHERE mind = ?
335
+ UNION SELECT file FROM page_reactions WHERE mind = ?`
336
+ ).all(mind, mind);
337
+ return new Set(rows.map((r) => r.file));
338
+ }
339
+ function retirePage(db, mind, file, hasThread) {
340
+ if (hasThread) {
341
+ db.prepare(
342
+ `UPDATE published_pages SET deleted_at = datetime('now')
343
+ WHERE mind = ? AND file = ? AND deleted_at IS NULL`
344
+ ).run(mind, file);
345
+ } else {
346
+ db.prepare("DELETE FROM published_pages WHERE mind = ? AND file = ?").run(mind, file);
347
+ db.prepare("DELETE FROM page_reads WHERE mind = ? AND file = ?").run(mind, file);
348
+ }
349
+ db.prepare("DELETE FROM page_citations WHERE mind = ? AND file = ?").run(mind, file);
350
+ db.prepare("DELETE FROM page_links WHERE mind = ? AND file = ?").run(mind, file);
351
+ }
352
+ function existingPages(db, mind) {
353
+ const rows = db.prepare("SELECT file, hash, deleted_at FROM published_pages WHERE mind = ?").all(mind);
354
+ return new Map(rows.map((r) => [r.file, r]));
355
+ }
356
+ function applyPageMeta(db, mind, page) {
357
+ if (page.commentsClosed !== void 0) {
358
+ db.prepare("UPDATE published_pages SET comments_closed = ? WHERE mind = ? AND file = ?").run(
359
+ page.commentsClosed ? 1 : 0,
360
+ mind,
361
+ page.file
362
+ );
363
+ }
364
+ if (page.links !== void 0) {
365
+ db.prepare("DELETE FROM page_links WHERE mind = ? AND file = ?").run(mind, page.file);
366
+ for (const target of page.links) {
367
+ if (target === mind) continue;
368
+ db.prepare("INSERT OR IGNORE INTO page_links (mind, file, target) VALUES (?, ?, ?)").run(
369
+ mind,
370
+ page.file,
371
+ target
372
+ );
373
+ }
374
+ }
375
+ if (page.mentions === void 0) return;
376
+ db.prepare("DELETE FROM page_citations WHERE mind = ? AND file = ?").run(mind, page.file);
377
+ for (const mentioned of page.mentions) {
378
+ if (mentioned === mind) continue;
379
+ db.prepare("INSERT OR IGNORE INTO page_citations (mind, file, mentioned) VALUES (?, ?, ?)").run(
380
+ mind,
381
+ page.file,
382
+ mentioned
383
+ );
384
+ }
385
+ }
386
+ function areCommentsClosed(db, mind, file) {
387
+ const row = db.prepare("SELECT comments_closed FROM published_pages WHERE mind = ? AND file = ?").get(mind, file);
388
+ return !!row?.comments_closed;
389
+ }
390
+ function citationsOf(db, mentioned) {
391
+ return db.prepare(
392
+ `SELECT c.mind, c.file, c.created_at FROM page_citations c
393
+ JOIN published_pages p ON p.mind = c.mind AND p.file = c.file
394
+ WHERE c.mentioned = ? AND p.deleted_at IS NULL
395
+ ORDER BY c.created_at DESC, c.id DESC`
396
+ ).all(mentioned);
397
+ }
398
+ function syncSystemPages(db, pages, author) {
399
+ const existing = existingPages(db, "_system");
400
+ const newSet = new Set(pages.map((p) => p.file));
401
+ const threaded = pagesWithThreads(db, "_system");
402
+ db.exec("BEGIN");
403
+ try {
404
+ for (const page of pages) {
405
+ const { file, hash } = page;
406
+ const prior = existing.get(file);
407
+ if (!prior) {
408
+ db.prepare(
409
+ "INSERT INTO published_pages (mind, file, hash, author) VALUES ('_system', ?, ?, ?)"
410
+ ).run(file, hash, author ?? null);
411
+ } else if (prior.deleted_at != null) {
412
+ db.prepare(
413
+ `UPDATE published_pages
414
+ SET deleted_at = NULL, updated_at = datetime('now'), hash = ?,
415
+ author = COALESCE(?, author)
416
+ WHERE mind = '_system' AND file = ?`
417
+ ).run(hash, author ?? null, file);
418
+ } else if (prior.hash !== hash) {
419
+ if (author) {
420
+ db.prepare(
421
+ "UPDATE published_pages SET updated_at = datetime('now'), hash = ?, author = ? WHERE mind = '_system' AND file = ?"
422
+ ).run(hash, author, file);
423
+ } else {
424
+ db.prepare(
425
+ "UPDATE published_pages SET updated_at = datetime('now'), hash = ? WHERE mind = '_system' AND file = ?"
426
+ ).run(hash, file);
427
+ }
428
+ }
429
+ applyPageMeta(db, "_system", page);
430
+ }
431
+ for (const [file, prior] of existing) {
432
+ if (!newSet.has(file) && prior.deleted_at == null) {
433
+ retirePage(db, "_system", file, threaded.has(file));
434
+ }
435
+ }
436
+ db.exec("COMMIT");
437
+ } catch (err) {
438
+ db.exec("ROLLBACK");
439
+ throw err;
440
+ }
441
+ }
442
+ function syncPublishedPages(db, mind, pages) {
443
+ const existing = existingPages(db, mind);
444
+ const newSet = new Set(pages.map((p) => p.file));
445
+ const threaded = pagesWithThreads(db, mind);
446
+ const added = [];
447
+ const updated = [];
448
+ const removed = [];
449
+ db.exec("BEGIN");
450
+ try {
451
+ for (const page of pages) {
452
+ const { file, hash } = page;
453
+ const prior = existing.get(file);
454
+ if (!prior) {
455
+ db.prepare("INSERT INTO published_pages (mind, file, hash) VALUES (?, ?, ?)").run(
456
+ mind,
457
+ file,
458
+ hash
459
+ );
460
+ added.push(file);
461
+ } else if (prior.deleted_at != null) {
462
+ db.prepare(
463
+ `UPDATE published_pages SET deleted_at = NULL, updated_at = datetime('now'), hash = ?
464
+ WHERE mind = ? AND file = ?`
465
+ ).run(hash, mind, file);
466
+ added.push(file);
467
+ } else if (prior.hash !== hash) {
468
+ db.prepare(
469
+ "UPDATE published_pages SET updated_at = datetime('now'), hash = ? WHERE mind = ? AND file = ?"
470
+ ).run(hash, mind, file);
471
+ updated.push(file);
472
+ }
473
+ applyPageMeta(db, mind, page);
474
+ }
475
+ for (const [file, prior] of existing) {
476
+ if (!newSet.has(file) && prior.deleted_at == null) {
477
+ retirePage(db, mind, file, threaded.has(file));
478
+ removed.push(file);
479
+ }
480
+ }
481
+ db.exec("COMMIT");
482
+ } catch (err) {
483
+ db.exec("ROLLBACK");
484
+ throw err;
485
+ }
486
+ return { added, removed, updated };
487
+ }
488
+ function upsertPageWithTimestamps(db, mind, file, hash, publishedAt, updatedAt, author) {
489
+ const prior = db.prepare("SELECT id FROM published_pages WHERE mind = ? AND file = ?").get(mind, file);
490
+ if (prior) {
491
+ db.prepare(
492
+ `UPDATE published_pages
493
+ SET hash = ?, published_at = ?, updated_at = ?, author = ?, deleted_at = NULL
494
+ WHERE id = ?`
495
+ ).run(hash, publishedAt, updatedAt, author ?? null, prior.id);
496
+ return;
497
+ }
498
+ db.prepare(
499
+ `INSERT INTO published_pages (mind, file, hash, published_at, updated_at, author)
500
+ VALUES (?, ?, ?, ?, ?, ?)`
501
+ ).run(mind, file, hash, publishedAt, updatedAt, author ?? null);
502
+ }
503
+
504
+ export {
505
+ initDb,
506
+ isSiteHome,
507
+ getPublishedPages,
508
+ getRecentPages,
509
+ getAllSites,
510
+ getSystemPages,
511
+ getMindsWithSites,
512
+ getPage,
513
+ getLivePageFiles,
514
+ knownPageFiles,
515
+ areCommentsClosed,
516
+ citationsOf,
517
+ syncSystemPages,
518
+ syncPublishedPages,
519
+ upsertPageWithTimestamps
520
+ };
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  setBridgeConfig
4
- } from "./chunk-L7BKTZ5U.js";
4
+ } from "./chunk-2QLWAYIU.js";
5
5
  import {
6
6
  readEnv,
7
7
  sharedEnvPath,
8
8
  writeEnv
9
- } from "./chunk-GLM7TVWP.js";
9
+ } from "./chunk-OHHS5PSI.js";
10
10
 
11
11
  // packages/daemon/src/lib/template/import-utils.ts
12
12
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  logger_default
4
- } from "./chunk-QUSMYGNQ.js";
4
+ } from "./chunk-2674KIGI.js";
5
5
  import {
6
6
  voluteSystemDir
7
- } from "./chunk-H6X6CKDN.js";
7
+ } from "./chunk-TAVIVN2I.js";
8
8
 
9
9
  // packages/daemon/src/lib/bridges/bridges.ts
10
10
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  logger_default
4
- } from "./chunk-QUSMYGNQ.js";
4
+ } from "./chunk-2674KIGI.js";
5
5
  import {
6
6
  stateDir
7
- } from "./chunk-H6X6CKDN.js";
7
+ } from "./chunk-TAVIVN2I.js";
8
8
 
9
9
  // packages/daemon/src/lib/chat/file-sharing.ts
10
10
  import { randomBytes } from "crypto";
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getMindManager
4
- } from "./chunk-WHL6PKYP.js";
4
+ } from "./chunk-PAO7JZNW.js";
5
5
  import {
6
6
  chownMindDir,
7
7
  gitExec
8
- } from "./chunk-OIOW3FPT.js";
8
+ } from "./chunk-NA2NJXY4.js";
9
9
  import {
10
10
  logger_default
11
- } from "./chunk-QUSMYGNQ.js";
11
+ } from "./chunk-2674KIGI.js";
12
12
  import {
13
13
  deleteMindDbFootprint,
14
14
  mindDir,
15
15
  readAllMinds,
16
16
  removeMind
17
- } from "./chunk-H6X6CKDN.js";
17
+ } from "./chunk-TAVIVN2I.js";
18
18
 
19
19
  // packages/daemon/src/lib/mind/variant-cleanup.ts
20
20
  import { existsSync, readdirSync, rmSync, statSync } from "fs";
@@ -29,7 +29,7 @@ async function cleanupVariant(variantName, baseName, projectRoot, variantPath, o
29
29
  }
30
30
  let branchName = opts?.branch;
31
31
  if (!branchName) {
32
- const { findMind } = await import("./registry-TRC2HJXR.js");
32
+ const { findMind } = await import("./registry-BTQXLDW6.js");
33
33
  const variantEntry = await findMind(variantName);
34
34
  branchName = variantEntry?.branch ?? variantName;
35
35
  }
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ parseDbTimestamp,
3
4
  publish
4
- } from "./chunk-LNNI3MS4.js";
5
+ } from "./chunk-MNKPAAQF.js";
5
6
  import {
6
7
  logger_default
7
- } from "./chunk-QUSMYGNQ.js";
8
+ } from "./chunk-2674KIGI.js";
8
9
  import {
9
10
  findMind,
10
11
  getBaseName,
11
12
  getDb
12
- } from "./chunk-H6X6CKDN.js";
13
+ } from "./chunk-TAVIVN2I.js";
13
14
  import {
14
15
  mindHistory,
15
16
  systemEvents,
@@ -257,7 +258,7 @@ async function deliverEvent(mind, input) {
257
258
  return { id: eventId, delivered: false };
258
259
  }
259
260
  const baseName = await getBaseName(mind);
260
- const { getSleepManagerIfReady } = await import("./sleep-manager-KHI65FEX.js");
261
+ const { getSleepManagerIfReady } = await import("./sleep-manager-25P44HAO.js");
261
262
  const sleepManager = getSleepManagerIfReady();
262
263
  const sleeping = !input.force && sleepManager?.isSleeping(baseName);
263
264
  const behavior = input.whileSleeping ?? "queue";
@@ -523,6 +524,38 @@ async function hasUndeliveredEvent(mind, reason) {
523
524
  ).limit(1).get();
524
525
  return row != null;
525
526
  }
527
+ var STRANDED_EVENT_TURN_THRESHOLD = 10;
528
+ async function findStrandedEventMinds(turnThreshold = STRANDED_EVENT_TURN_THRESHOLD) {
529
+ const db = await getDb();
530
+ const groups = await db.select({
531
+ mind: systemEvents.mind,
532
+ pending: sql`count(*)`,
533
+ oldestId: sql`min(${systemEvents.id})`
534
+ }).from(systemEvents).where(and(eq(systemEvents.delivery, "next-turn"), isNull(systemEvents.delivered_at))).groupBy(systemEvents.mind);
535
+ const stranded = [];
536
+ for (const group of groups) {
537
+ const oldest = await db.select({ created_at: systemEvents.created_at }).from(systemEvents).where(eq(systemEvents.id, group.oldestId)).get();
538
+ if (!oldest) continue;
539
+ const turnCount = await db.select({ n: sql`count(*)` }).from(turns).where(
540
+ and(
541
+ eq(turns.mind, group.mind),
542
+ eq(turns.status, "complete"),
543
+ gt(turns.created_at, oldest.created_at)
544
+ )
545
+ ).get();
546
+ const turnsSince = turnCount?.n ?? 0;
547
+ if (turnsSince < turnThreshold) continue;
548
+ stranded.push({
549
+ mind: group.mind,
550
+ pending: group.pending,
551
+ turnsSince,
552
+ ageHours: Math.round(
553
+ (Date.now() - parseDbTimestamp(oldest.created_at).getTime()) / (60 * 60 * 1e3)
554
+ )
555
+ });
556
+ }
557
+ return stranded;
558
+ }
526
559
  async function listEvents(mind, limit = 100, before) {
527
560
  const db = await getDb();
528
561
  const conditions = [eq(systemEvents.mind, mind)];
@@ -580,6 +613,7 @@ export {
580
613
  latestFailureEvent,
581
614
  latestEvent,
582
615
  hasUndeliveredEvent,
616
+ findStrandedEventMinds,
583
617
  listEvents,
584
618
  recordReflection,
585
619
  captureReflection
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  voluteSystemDir
4
- } from "./chunk-H6X6CKDN.js";
4
+ } from "./chunk-TAVIVN2I.js";
5
5
 
6
6
  // packages/daemon/src/lib/update-check.ts
7
7
  import { execFile as execFileCb, execFileSync } from "child_process";