@earendil-works/chord 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1299) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +115 -90
  3. package/dist/context/index.d.ts +1 -1
  4. package/dist/context/index.d.ts.map +1 -1
  5. package/dist/context/index.js +1 -1
  6. package/dist/context/index.js.map +1 -1
  7. package/dist/contracts/accepts.d.ts +81 -0
  8. package/dist/contracts/accepts.d.ts.map +1 -0
  9. package/dist/contracts/accepts.js +123 -0
  10. package/dist/contracts/accepts.js.map +1 -0
  11. package/dist/contracts/contract.d.ts +121 -0
  12. package/dist/contracts/contract.d.ts.map +1 -0
  13. package/dist/contracts/contract.js +50 -0
  14. package/dist/contracts/contract.js.map +1 -0
  15. package/dist/contracts/declare.d.ts +107 -0
  16. package/dist/contracts/declare.d.ts.map +1 -0
  17. package/dist/contracts/declare.js +136 -0
  18. package/dist/contracts/declare.js.map +1 -0
  19. package/dist/contracts/evolve.d.ts +92 -0
  20. package/dist/contracts/evolve.d.ts.map +1 -0
  21. package/dist/contracts/evolve.js +179 -0
  22. package/dist/contracts/evolve.js.map +1 -0
  23. package/dist/contracts/fingerprint.d.ts +16 -0
  24. package/dist/contracts/fingerprint.d.ts.map +1 -0
  25. package/dist/contracts/fingerprint.js +50 -0
  26. package/dist/contracts/fingerprint.js.map +1 -0
  27. package/dist/contracts/history.d.ts +71 -0
  28. package/dist/contracts/history.d.ts.map +1 -0
  29. package/dist/contracts/history.js +3 -0
  30. package/dist/contracts/history.js.map +1 -0
  31. package/dist/contracts/host-kind.d.ts +40 -0
  32. package/dist/contracts/host-kind.d.ts.map +1 -0
  33. package/dist/contracts/host-kind.js +37 -0
  34. package/dist/contracts/host-kind.js.map +1 -0
  35. package/dist/contracts/index.d.ts +7 -0
  36. package/dist/contracts/index.d.ts.map +1 -0
  37. package/dist/contracts/index.js +6 -0
  38. package/dist/contracts/index.js.map +1 -0
  39. package/dist/contracts/internal.d.ts +10 -0
  40. package/dist/contracts/internal.d.ts.map +1 -0
  41. package/dist/contracts/internal.js +9 -0
  42. package/dist/contracts/internal.js.map +1 -0
  43. package/dist/contracts/names.d.ts +3 -0
  44. package/dist/contracts/names.d.ts.map +1 -0
  45. package/dist/contracts/names.js +13 -0
  46. package/dist/contracts/names.js.map +1 -0
  47. package/dist/contracts/params.d.ts +28 -0
  48. package/dist/contracts/params.d.ts.map +1 -0
  49. package/dist/contracts/params.js +50 -0
  50. package/dist/contracts/params.js.map +1 -0
  51. package/dist/contracts/schema.d.ts +102 -0
  52. package/dist/contracts/schema.d.ts.map +1 -0
  53. package/dist/contracts/schema.js +103 -0
  54. package/dist/contracts/schema.js.map +1 -0
  55. package/dist/contracts/snapshot.d.ts +16 -0
  56. package/dist/contracts/snapshot.d.ts.map +1 -0
  57. package/dist/contracts/snapshot.js +167 -0
  58. package/dist/contracts/snapshot.js.map +1 -0
  59. package/dist/data/args.d.ts +50 -0
  60. package/dist/data/args.d.ts.map +1 -0
  61. package/dist/data/args.js +83 -0
  62. package/dist/data/args.js.map +1 -0
  63. package/dist/data/errors.d.ts +80 -0
  64. package/dist/data/errors.d.ts.map +1 -0
  65. package/dist/data/errors.js +116 -0
  66. package/dist/data/errors.js.map +1 -0
  67. package/dist/data/failure.d.ts +26 -0
  68. package/dist/data/failure.d.ts.map +1 -0
  69. package/dist/data/failure.js +77 -0
  70. package/dist/data/failure.js.map +1 -0
  71. package/dist/data/index.d.ts +4 -0
  72. package/dist/data/index.d.ts.map +1 -0
  73. package/dist/data/index.js +5 -0
  74. package/dist/data/index.js.map +1 -0
  75. package/dist/data/internal.d.ts +7 -0
  76. package/dist/data/internal.d.ts.map +1 -0
  77. package/dist/data/internal.js +8 -0
  78. package/dist/data/internal.js.map +1 -0
  79. package/dist/data/params.d.ts +20 -0
  80. package/dist/data/params.d.ts.map +1 -0
  81. package/dist/data/params.js +31 -0
  82. package/dist/data/params.js.map +1 -0
  83. package/dist/data/principal.d.ts +22 -0
  84. package/dist/data/principal.d.ts.map +1 -0
  85. package/dist/data/principal.js +49 -0
  86. package/dist/data/principal.js.map +1 -0
  87. package/dist/data/validator.d.ts +12 -0
  88. package/dist/data/validator.d.ts.map +1 -0
  89. package/dist/data/validator.js +35 -0
  90. package/dist/data/validator.js.map +1 -0
  91. package/dist/delta/apply-immutable-trusted.js +3 -4
  92. package/dist/delta/apply-immutable-trusted.js.map +1 -1
  93. package/dist/delta/diff.d.ts.map +1 -1
  94. package/dist/delta/diff.js +5 -3
  95. package/dist/delta/diff.js.map +1 -1
  96. package/dist/delta/index.d.ts +9 -1
  97. package/dist/delta/index.d.ts.map +1 -1
  98. package/dist/delta/index.js +15 -11
  99. package/dist/delta/index.js.map +1 -1
  100. package/dist/delta/pending.d.ts +36 -0
  101. package/dist/delta/pending.d.ts.map +1 -0
  102. package/dist/delta/pending.js +143 -0
  103. package/dist/delta/pending.js.map +1 -0
  104. package/dist/delta/tracker.d.ts +12 -0
  105. package/dist/delta/tracker.d.ts.map +1 -1
  106. package/dist/delta/tracker.js +109 -39
  107. package/dist/delta/tracker.js.map +1 -1
  108. package/dist/documents/address-source.d.ts +20 -0
  109. package/dist/documents/address-source.d.ts.map +1 -0
  110. package/dist/documents/address-source.js +44 -0
  111. package/dist/documents/address-source.js.map +1 -0
  112. package/dist/documents/definition.d.ts +84 -0
  113. package/dist/documents/definition.d.ts.map +1 -0
  114. package/dist/documents/definition.js +74 -0
  115. package/dist/documents/definition.js.map +1 -0
  116. package/dist/documents/documents.d.ts +126 -0
  117. package/dist/documents/documents.d.ts.map +1 -0
  118. package/dist/documents/documents.js +248 -0
  119. package/dist/documents/documents.js.map +1 -0
  120. package/dist/documents/fence.d.ts +9 -0
  121. package/dist/documents/fence.d.ts.map +1 -0
  122. package/dist/documents/fence.js +15 -0
  123. package/dist/documents/fence.js.map +1 -0
  124. package/dist/documents/handles.d.ts +76 -0
  125. package/dist/documents/handles.d.ts.map +1 -0
  126. package/dist/documents/handles.js +66 -0
  127. package/dist/documents/handles.js.map +1 -0
  128. package/dist/documents/index.d.ts +4 -0
  129. package/dist/documents/index.d.ts.map +1 -0
  130. package/dist/documents/index.js +3 -0
  131. package/dist/documents/index.js.map +1 -0
  132. package/dist/documents/internal.d.ts +9 -0
  133. package/dist/documents/internal.d.ts.map +1 -0
  134. package/dist/documents/internal.js +9 -0
  135. package/dist/documents/internal.js.map +1 -0
  136. package/dist/documents/keyed-slot.d.ts +103 -0
  137. package/dist/documents/keyed-slot.d.ts.map +1 -0
  138. package/dist/documents/keyed-slot.js +395 -0
  139. package/dist/documents/keyed-slot.js.map +1 -0
  140. package/dist/documents/line.d.ts +7 -0
  141. package/dist/documents/line.d.ts.map +1 -0
  142. package/dist/documents/line.js +14 -0
  143. package/dist/documents/line.js.map +1 -0
  144. package/dist/documents/persistence.d.ts +104 -0
  145. package/dist/documents/persistence.d.ts.map +1 -0
  146. package/dist/documents/persistence.js +129 -0
  147. package/dist/documents/persistence.js.map +1 -0
  148. package/dist/documents/residency.d.ts +31 -0
  149. package/dist/documents/residency.d.ts.map +1 -0
  150. package/dist/documents/residency.js +93 -0
  151. package/dist/documents/residency.js.map +1 -0
  152. package/dist/documents/slot.d.ts +109 -0
  153. package/dist/documents/slot.d.ts.map +1 -0
  154. package/dist/documents/slot.js +181 -0
  155. package/dist/documents/slot.js.map +1 -0
  156. package/dist/documents/transaction.d.ts +143 -0
  157. package/dist/documents/transaction.d.ts.map +1 -0
  158. package/dist/documents/transaction.js +442 -0
  159. package/dist/documents/transaction.js.map +1 -0
  160. package/dist/host/activity.d.ts +31 -0
  161. package/dist/host/activity.d.ts.map +1 -0
  162. package/dist/host/activity.js +40 -0
  163. package/dist/host/activity.js.map +1 -0
  164. package/dist/host/adapted.d.ts +49 -0
  165. package/dist/host/adapted.d.ts.map +1 -0
  166. package/dist/host/adapted.js +214 -0
  167. package/dist/host/adapted.js.map +1 -0
  168. package/dist/host/attempt.d.ts +308 -0
  169. package/dist/host/attempt.d.ts.map +1 -0
  170. package/dist/host/attempt.js +1199 -0
  171. package/dist/host/attempt.js.map +1 -0
  172. package/dist/host/backoff.d.ts +29 -0
  173. package/dist/host/backoff.d.ts.map +1 -0
  174. package/dist/host/backoff.js +69 -0
  175. package/dist/host/backoff.js.map +1 -0
  176. package/dist/host/ctx.d.ts +94 -0
  177. package/dist/host/ctx.d.ts.map +1 -0
  178. package/dist/host/ctx.js +200 -0
  179. package/dist/host/ctx.js.map +1 -0
  180. package/dist/host/deadline.d.ts +12 -0
  181. package/dist/host/deadline.d.ts.map +1 -0
  182. package/dist/host/deadline.js +40 -0
  183. package/dist/host/deadline.js.map +1 -0
  184. package/dist/host/dependencies.d.ts +65 -0
  185. package/dist/host/dependencies.d.ts.map +1 -0
  186. package/dist/host/dependencies.js +115 -0
  187. package/dist/host/dependencies.js.map +1 -0
  188. package/dist/host/destination.d.ts +48 -0
  189. package/dist/host/destination.d.ts.map +1 -0
  190. package/dist/host/destination.js +101 -0
  191. package/dist/host/destination.js.map +1 -0
  192. package/dist/host/facet.d.ts +84 -0
  193. package/dist/host/facet.d.ts.map +1 -0
  194. package/dist/host/facet.js +55 -0
  195. package/dist/host/facet.js.map +1 -0
  196. package/dist/host/follow.d.ts +52 -0
  197. package/dist/host/follow.d.ts.map +1 -0
  198. package/dist/host/follow.js +207 -0
  199. package/dist/host/follow.js.map +1 -0
  200. package/dist/host/host.d.ts +571 -0
  201. package/dist/host/host.d.ts.map +1 -0
  202. package/dist/host/host.js +1318 -0
  203. package/dist/host/host.js.map +1 -0
  204. package/dist/host/index.d.ts +17 -0
  205. package/dist/host/index.d.ts.map +1 -0
  206. package/dist/host/index.js +6 -0
  207. package/dist/host/index.js.map +1 -0
  208. package/dist/host/internal.d.ts +11 -0
  209. package/dist/host/internal.d.ts.map +1 -0
  210. package/dist/host/internal.js +9 -0
  211. package/dist/host/internal.js.map +1 -0
  212. package/dist/host/members.d.ts +50 -0
  213. package/dist/host/members.d.ts.map +1 -0
  214. package/dist/host/members.js +107 -0
  215. package/dist/host/members.js.map +1 -0
  216. package/dist/host/native.d.ts +64 -0
  217. package/dist/host/native.d.ts.map +1 -0
  218. package/dist/host/native.js +126 -0
  219. package/dist/host/native.js.map +1 -0
  220. package/dist/host/object.d.ts +331 -0
  221. package/dist/host/object.d.ts.map +1 -0
  222. package/dist/host/object.js +1296 -0
  223. package/dist/host/object.js.map +1 -0
  224. package/dist/host/old-work.d.ts +34 -0
  225. package/dist/host/old-work.d.ts.map +1 -0
  226. package/dist/host/old-work.js +52 -0
  227. package/dist/host/old-work.js.map +1 -0
  228. package/dist/host/parked.d.ts +23 -0
  229. package/dist/host/parked.d.ts.map +1 -0
  230. package/dist/host/parked.js +79 -0
  231. package/dist/host/parked.js.map +1 -0
  232. package/dist/host/presence.d.ts +34 -0
  233. package/dist/host/presence.d.ts.map +1 -0
  234. package/dist/host/presence.js +117 -0
  235. package/dist/host/presence.js.map +1 -0
  236. package/dist/host/readiness.d.ts +27 -0
  237. package/dist/host/readiness.d.ts.map +1 -0
  238. package/dist/host/readiness.js +134 -0
  239. package/dist/host/readiness.js.map +1 -0
  240. package/dist/host/ref-state.d.ts +53 -0
  241. package/dist/host/ref-state.d.ts.map +1 -0
  242. package/dist/host/ref-state.js +83 -0
  243. package/dist/host/ref-state.js.map +1 -0
  244. package/dist/host/reference-keys.d.ts +36 -0
  245. package/dist/host/reference-keys.d.ts.map +1 -0
  246. package/dist/host/reference-keys.js +168 -0
  247. package/dist/host/reference-keys.js.map +1 -0
  248. package/dist/host/reference-source.d.ts +65 -0
  249. package/dist/host/reference-source.d.ts.map +1 -0
  250. package/dist/host/reference-source.js +285 -0
  251. package/dist/host/reference-source.js.map +1 -0
  252. package/dist/host/reference.d.ts +321 -0
  253. package/dist/host/reference.d.ts.map +1 -0
  254. package/dist/host/reference.js +797 -0
  255. package/dist/host/reference.js.map +1 -0
  256. package/dist/host/reload-cycle.d.ts +16 -0
  257. package/dist/host/reload-cycle.d.ts.map +1 -0
  258. package/dist/host/reload-cycle.js +81 -0
  259. package/dist/host/reload-cycle.js.map +1 -0
  260. package/dist/host/retry-on.d.ts +28 -0
  261. package/dist/host/retry-on.d.ts.map +1 -0
  262. package/dist/host/retry-on.js +52 -0
  263. package/dist/host/retry-on.js.map +1 -0
  264. package/dist/host/revocation.d.ts +16 -0
  265. package/dist/host/revocation.d.ts.map +1 -0
  266. package/dist/host/revocation.js +27 -0
  267. package/dist/host/revocation.js.map +1 -0
  268. package/dist/host/root.d.ts +37 -0
  269. package/dist/host/root.d.ts.map +1 -0
  270. package/dist/host/root.js +68 -0
  271. package/dist/host/root.js.map +1 -0
  272. package/dist/host/route.d.ts +64 -0
  273. package/dist/host/route.d.ts.map +1 -0
  274. package/dist/host/route.js +227 -0
  275. package/dist/host/route.js.map +1 -0
  276. package/dist/host/routines.d.ts +78 -0
  277. package/dist/host/routines.d.ts.map +1 -0
  278. package/dist/host/routines.js +398 -0
  279. package/dist/host/routines.js.map +1 -0
  280. package/dist/host/sleep.d.ts +9 -0
  281. package/dist/host/sleep.d.ts.map +1 -0
  282. package/dist/host/sleep.js +40 -0
  283. package/dist/host/sleep.js.map +1 -0
  284. package/dist/host/staging.d.ts +94 -0
  285. package/dist/host/staging.d.ts.map +1 -0
  286. package/dist/host/staging.js +304 -0
  287. package/dist/host/staging.js.map +1 -0
  288. package/dist/host/unbound-clock.d.ts +23 -0
  289. package/dist/host/unbound-clock.d.ts.map +1 -0
  290. package/dist/host/unbound-clock.js +58 -0
  291. package/dist/host/unbound-clock.js.map +1 -0
  292. package/dist/host/unbound.d.ts +37 -0
  293. package/dist/host/unbound.d.ts.map +1 -0
  294. package/dist/host/unbound.js +88 -0
  295. package/dist/host/unbound.js.map +1 -0
  296. package/dist/host/uuid.d.ts +3 -0
  297. package/dist/host/uuid.d.ts.map +1 -0
  298. package/dist/host/uuid.js +38 -0
  299. package/dist/host/uuid.js.map +1 -0
  300. package/dist/index.d.ts +29 -18
  301. package/dist/index.d.ts.map +1 -1
  302. package/dist/index.js +20 -9
  303. package/dist/index.js.map +1 -1
  304. package/dist/jobs/definition.d.ts +96 -0
  305. package/dist/jobs/definition.d.ts.map +1 -0
  306. package/dist/jobs/definition.js +83 -0
  307. package/dist/jobs/definition.js.map +1 -0
  308. package/dist/jobs/index.d.ts +5 -0
  309. package/dist/jobs/index.d.ts.map +1 -0
  310. package/dist/jobs/index.js +3 -0
  311. package/dist/jobs/index.js.map +1 -0
  312. package/dist/jobs/internal.d.ts +3 -0
  313. package/dist/jobs/internal.d.ts.map +1 -0
  314. package/dist/jobs/internal.js +4 -0
  315. package/dist/jobs/internal.js.map +1 -0
  316. package/dist/jobs/job.d.ts +149 -0
  317. package/dist/jobs/job.d.ts.map +1 -0
  318. package/dist/jobs/job.js +387 -0
  319. package/dist/jobs/job.js.map +1 -0
  320. package/dist/jobs/persisted.d.ts +38 -0
  321. package/dist/jobs/persisted.d.ts.map +1 -0
  322. package/dist/jobs/persisted.js +95 -0
  323. package/dist/jobs/persisted.js.map +1 -0
  324. package/dist/jobs/records.d.ts +231 -0
  325. package/dist/jobs/records.d.ts.map +1 -0
  326. package/dist/jobs/records.js +871 -0
  327. package/dist/jobs/records.js.map +1 -0
  328. package/dist/jobs/run-states.d.ts +69 -0
  329. package/dist/jobs/run-states.d.ts.map +1 -0
  330. package/dist/jobs/run-states.js +228 -0
  331. package/dist/jobs/run-states.js.map +1 -0
  332. package/dist/jobs/run.d.ts +117 -0
  333. package/dist/jobs/run.d.ts.map +1 -0
  334. package/dist/jobs/run.js +204 -0
  335. package/dist/jobs/run.js.map +1 -0
  336. package/dist/jobs/running.d.ts +44 -0
  337. package/dist/jobs/running.d.ts.map +1 -0
  338. package/dist/jobs/running.js +117 -0
  339. package/dist/jobs/running.js.map +1 -0
  340. package/dist/jobs/stored.d.ts +63 -0
  341. package/dist/jobs/stored.d.ts.map +1 -0
  342. package/dist/jobs/stored.js +55 -0
  343. package/dist/jobs/stored.js.map +1 -0
  344. package/dist/link/binary.d.ts +4 -0
  345. package/dist/link/binary.d.ts.map +1 -0
  346. package/dist/link/binary.js +544 -0
  347. package/dist/link/binary.js.map +1 -0
  348. package/dist/link/channel.d.ts +68 -0
  349. package/dist/link/channel.d.ts.map +1 -0
  350. package/dist/link/channel.js +215 -0
  351. package/dist/link/channel.js.map +1 -0
  352. package/dist/link/errors.d.ts +7 -0
  353. package/dist/link/errors.d.ts.map +1 -0
  354. package/dist/link/errors.js +21 -0
  355. package/dist/link/errors.js.map +1 -0
  356. package/dist/link/index.d.ts +8 -0
  357. package/dist/link/index.d.ts.map +1 -0
  358. package/dist/link/index.js +8 -0
  359. package/dist/link/index.js.map +1 -0
  360. package/dist/link/internal.d.ts +9 -0
  361. package/dist/link/internal.d.ts.map +1 -0
  362. package/dist/link/internal.js +10 -0
  363. package/dist/link/internal.js.map +1 -0
  364. package/dist/link/ipc.d.ts +22 -0
  365. package/dist/link/ipc.d.ts.map +1 -0
  366. package/dist/link/ipc.js +64 -0
  367. package/dist/link/ipc.js.map +1 -0
  368. package/dist/link/link.d.ts +34 -0
  369. package/dist/link/link.d.ts.map +1 -0
  370. package/dist/link/link.js +87 -0
  371. package/dist/link/link.js.map +1 -0
  372. package/dist/link/message.d.ts +262 -0
  373. package/dist/link/message.d.ts.map +1 -0
  374. package/dist/link/message.js +156 -0
  375. package/dist/link/message.js.map +1 -0
  376. package/dist/link/part.d.ts +80 -0
  377. package/dist/link/part.d.ts.map +1 -0
  378. package/dist/link/part.js +165 -0
  379. package/dist/link/part.js.map +1 -0
  380. package/dist/link/text.d.ts +6 -0
  381. package/dist/link/text.d.ts.map +1 -0
  382. package/dist/link/text.js +355 -0
  383. package/dist/link/text.js.map +1 -0
  384. package/dist/link/values.d.ts +40 -0
  385. package/dist/link/values.d.ts.map +1 -0
  386. package/dist/link/values.js +121 -0
  387. package/dist/link/values.js.map +1 -0
  388. package/dist/link/websocket.d.ts +26 -0
  389. package/dist/link/websocket.d.ts.map +1 -0
  390. package/dist/link/websocket.js +91 -0
  391. package/dist/link/websocket.js.map +1 -0
  392. package/dist/node/app/app.d.ts +269 -0
  393. package/dist/node/app/app.d.ts.map +1 -0
  394. package/dist/node/app/app.js +513 -0
  395. package/dist/node/app/app.js.map +1 -0
  396. package/dist/node/app/archive.d.ts +37 -0
  397. package/dist/node/app/archive.d.ts.map +1 -0
  398. package/dist/node/app/archive.js +204 -0
  399. package/dist/node/app/archive.js.map +1 -0
  400. package/dist/node/app/blobs.d.ts +23 -0
  401. package/dist/node/app/blobs.d.ts.map +1 -0
  402. package/dist/node/app/blobs.js +64 -0
  403. package/dist/node/app/blobs.js.map +1 -0
  404. package/dist/node/app/data.d.ts +35 -0
  405. package/dist/node/app/data.d.ts.map +1 -0
  406. package/dist/node/app/data.js +44 -0
  407. package/dist/node/app/data.js.map +1 -0
  408. package/dist/node/app/ephemeral-sessions.d.ts +20 -0
  409. package/dist/node/app/ephemeral-sessions.d.ts.map +1 -0
  410. package/dist/node/app/ephemeral-sessions.js +69 -0
  411. package/dist/node/app/ephemeral-sessions.js.map +1 -0
  412. package/dist/node/app/ephemeral.d.ts +11 -0
  413. package/dist/node/app/ephemeral.d.ts.map +1 -0
  414. package/dist/node/app/ephemeral.js +34 -0
  415. package/dist/node/app/ephemeral.js.map +1 -0
  416. package/dist/node/app/files-copy.d.ts +13 -0
  417. package/dist/node/app/files-copy.d.ts.map +1 -0
  418. package/dist/node/app/files-copy.js +86 -0
  419. package/dist/node/app/files-copy.js.map +1 -0
  420. package/dist/node/app/files-entry.d.ts +4 -0
  421. package/dist/node/app/files-entry.d.ts.map +1 -0
  422. package/dist/node/app/files-entry.js +7 -0
  423. package/dist/node/app/files-entry.js.map +1 -0
  424. package/dist/node/app/files-index.d.ts +180 -0
  425. package/dist/node/app/files-index.d.ts.map +1 -0
  426. package/dist/node/app/files-index.js +535 -0
  427. package/dist/node/app/files-index.js.map +1 -0
  428. package/dist/node/app/files.d.ts +363 -0
  429. package/dist/node/app/files.d.ts.map +1 -0
  430. package/dist/node/app/files.js +368 -0
  431. package/dist/node/app/files.js.map +1 -0
  432. package/dist/node/app/folder-views.d.ts +21 -0
  433. package/dist/node/app/folder-views.d.ts.map +1 -0
  434. package/dist/node/app/folder-views.js +55 -0
  435. package/dist/node/app/folder-views.js.map +1 -0
  436. package/dist/node/app/index.d.ts +12 -0
  437. package/dist/node/app/index.d.ts.map +1 -0
  438. package/dist/node/app/index.js +15 -0
  439. package/dist/node/app/index.js.map +1 -0
  440. package/dist/node/app/info.d.ts +66 -0
  441. package/dist/node/app/info.d.ts.map +1 -0
  442. package/dist/node/app/info.js +37 -0
  443. package/dist/node/app/info.js.map +1 -0
  444. package/dist/node/app/invite.d.ts +5 -0
  445. package/dist/node/app/invite.d.ts.map +1 -0
  446. package/dist/node/app/invite.js +64 -0
  447. package/dist/node/app/invite.js.map +1 -0
  448. package/dist/node/app/layout.d.ts +36 -0
  449. package/dist/node/app/layout.d.ts.map +1 -0
  450. package/dist/node/app/layout.js +87 -0
  451. package/dist/node/app/layout.js.map +1 -0
  452. package/dist/node/app/owner.d.ts +31 -0
  453. package/dist/node/app/owner.d.ts.map +1 -0
  454. package/dist/node/app/owner.js +104 -0
  455. package/dist/node/app/owner.js.map +1 -0
  456. package/dist/node/app/pairing.d.ts +55 -0
  457. package/dist/node/app/pairing.d.ts.map +1 -0
  458. package/dist/node/app/pairing.js +183 -0
  459. package/dist/node/app/pairing.js.map +1 -0
  460. package/dist/node/app/secrets.d.ts +16 -0
  461. package/dist/node/app/secrets.d.ts.map +1 -0
  462. package/dist/node/app/secrets.js +72 -0
  463. package/dist/node/app/secrets.js.map +1 -0
  464. package/dist/node/app/sessions.d.ts +72 -0
  465. package/dist/node/app/sessions.d.ts.map +1 -0
  466. package/dist/node/app/sessions.js +222 -0
  467. package/dist/node/app/sessions.js.map +1 -0
  468. package/dist/node/app/tar.d.ts +24 -0
  469. package/dist/node/app/tar.d.ts.map +1 -0
  470. package/dist/node/app/tar.js +167 -0
  471. package/dist/node/app/tar.js.map +1 -0
  472. package/dist/node/app/uploads.d.ts +37 -0
  473. package/dist/node/app/uploads.d.ts.map +1 -0
  474. package/dist/node/app/uploads.js +157 -0
  475. package/dist/node/app/uploads.js.map +1 -0
  476. package/dist/node/app/users.d.ts +37 -0
  477. package/dist/node/app/users.d.ts.map +1 -0
  478. package/dist/node/app/users.js +62 -0
  479. package/dist/node/app/users.js.map +1 -0
  480. package/dist/node/app/worker.d.ts +18 -0
  481. package/dist/node/app/worker.d.ts.map +1 -0
  482. package/dist/node/app/worker.js +84 -0
  483. package/dist/node/app/worker.js.map +1 -0
  484. package/dist/node/compression.d.ts +7 -0
  485. package/dist/node/compression.d.ts.map +1 -0
  486. package/dist/node/compression.js +25 -0
  487. package/dist/node/compression.js.map +1 -0
  488. package/dist/node/http/admission.d.ts +63 -0
  489. package/dist/node/http/admission.d.ts.map +1 -0
  490. package/dist/node/http/admission.js +134 -0
  491. package/dist/node/http/admission.js.map +1 -0
  492. package/dist/node/http/authenticate.d.ts +21 -0
  493. package/dist/node/http/authenticate.d.ts.map +1 -0
  494. package/dist/node/http/authenticate.js +34 -0
  495. package/dist/node/http/authenticate.js.map +1 -0
  496. package/dist/node/http/cookies.d.ts +32 -0
  497. package/dist/node/http/cookies.d.ts.map +1 -0
  498. package/dist/node/http/cookies.js +70 -0
  499. package/dist/node/http/cookies.js.map +1 -0
  500. package/dist/node/http/file.d.ts +23 -0
  501. package/dist/node/http/file.d.ts.map +1 -0
  502. package/dist/node/http/file.js +94 -0
  503. package/dist/node/http/file.js.map +1 -0
  504. package/dist/node/http/headers.d.ts +37 -0
  505. package/dist/node/http/headers.d.ts.map +1 -0
  506. package/dist/node/http/headers.js +136 -0
  507. package/dist/node/http/headers.js.map +1 -0
  508. package/dist/node/http/http.d.ts +111 -0
  509. package/dist/node/http/http.d.ts.map +1 -0
  510. package/dist/node/http/http.js +524 -0
  511. package/dist/node/http/http.js.map +1 -0
  512. package/dist/node/http/incoming.d.ts +19 -0
  513. package/dist/node/http/incoming.d.ts.map +1 -0
  514. package/dist/node/http/incoming.js +31 -0
  515. package/dist/node/http/incoming.js.map +1 -0
  516. package/dist/node/http/index.d.ts +11 -0
  517. package/dist/node/http/index.d.ts.map +1 -0
  518. package/dist/node/http/index.js +12 -0
  519. package/dist/node/http/index.js.map +1 -0
  520. package/dist/node/http/node.d.ts +78 -0
  521. package/dist/node/http/node.d.ts.map +1 -0
  522. package/dist/node/http/node.js +291 -0
  523. package/dist/node/http/node.js.map +1 -0
  524. package/dist/node/http/request.d.ts +60 -0
  525. package/dist/node/http/request.d.ts.map +1 -0
  526. package/dist/node/http/request.js +118 -0
  527. package/dist/node/http/request.js.map +1 -0
  528. package/dist/node/http/response.d.ts +63 -0
  529. package/dist/node/http/response.d.ts.map +1 -0
  530. package/dist/node/http/response.js +202 -0
  531. package/dist/node/http/response.js.map +1 -0
  532. package/dist/node/http/router.d.ts +132 -0
  533. package/dist/node/http/router.d.ts.map +1 -0
  534. package/dist/node/http/router.js +267 -0
  535. package/dist/node/http/router.js.map +1 -0
  536. package/dist/node/http/upgrades.d.ts +61 -0
  537. package/dist/node/http/upgrades.d.ts.map +1 -0
  538. package/dist/node/http/upgrades.js +192 -0
  539. package/dist/node/http/upgrades.js.map +1 -0
  540. package/dist/node/plugins/base-inputs.d.ts +14 -0
  541. package/dist/node/plugins/base-inputs.d.ts.map +1 -0
  542. package/dist/node/plugins/base-inputs.js +33 -0
  543. package/dist/node/plugins/base-inputs.js.map +1 -0
  544. package/dist/node/plugins/build.d.ts +77 -0
  545. package/dist/node/plugins/build.d.ts.map +1 -0
  546. package/dist/node/plugins/build.js +576 -0
  547. package/dist/node/plugins/build.js.map +1 -0
  548. package/dist/node/plugins/css-modules.d.ts +13 -0
  549. package/dist/node/plugins/css-modules.d.ts.map +1 -0
  550. package/dist/node/plugins/css-modules.js +58 -0
  551. package/dist/node/plugins/css-modules.js.map +1 -0
  552. package/dist/node/plugins/discover.d.ts +67 -0
  553. package/dist/node/plugins/discover.d.ts.map +1 -0
  554. package/dist/node/plugins/discover.js +369 -0
  555. package/dist/node/plugins/discover.js.map +1 -0
  556. package/dist/node/plugins/evaluate.d.ts +33 -0
  557. package/dist/node/plugins/evaluate.d.ts.map +1 -0
  558. package/dist/node/plugins/evaluate.js +121 -0
  559. package/dist/node/plugins/evaluate.js.map +1 -0
  560. package/dist/node/plugins/index.d.ts +11 -0
  561. package/dist/node/plugins/index.d.ts.map +1 -0
  562. package/dist/node/plugins/index.js +13 -0
  563. package/dist/node/plugins/index.js.map +1 -0
  564. package/dist/node/plugins/install.d.ts +38 -0
  565. package/dist/node/plugins/install.d.ts.map +1 -0
  566. package/dist/node/plugins/install.js +213 -0
  567. package/dist/node/plugins/install.js.map +1 -0
  568. package/dist/node/plugins/plugins.d.ts +162 -0
  569. package/dist/node/plugins/plugins.d.ts.map +1 -0
  570. package/dist/node/plugins/plugins.js +244 -0
  571. package/dist/node/plugins/plugins.js.map +1 -0
  572. package/dist/node/plugins/runtime.d.ts +57 -0
  573. package/dist/node/plugins/runtime.d.ts.map +1 -0
  574. package/dist/node/plugins/runtime.js +92 -0
  575. package/dist/node/plugins/runtime.js.map +1 -0
  576. package/dist/node/plugins/stack.d.ts +16 -0
  577. package/dist/node/plugins/stack.d.ts.map +1 -0
  578. package/dist/node/plugins/stack.js +61 -0
  579. package/dist/node/plugins/stack.js.map +1 -0
  580. package/dist/node/plugins/testing.d.ts +58 -0
  581. package/dist/node/plugins/testing.d.ts.map +1 -0
  582. package/dist/node/plugins/testing.js +210 -0
  583. package/dist/node/plugins/testing.js.map +1 -0
  584. package/dist/node/plugins/watcher.d.ts +69 -0
  585. package/dist/node/plugins/watcher.d.ts.map +1 -0
  586. package/dist/node/plugins/watcher.js +622 -0
  587. package/dist/node/plugins/watcher.js.map +1 -0
  588. package/dist/node/surfaces/bundle.d.ts +31 -0
  589. package/dist/node/surfaces/bundle.d.ts.map +1 -0
  590. package/dist/node/surfaces/bundle.js +118 -0
  591. package/dist/node/surfaces/bundle.js.map +1 -0
  592. package/dist/node/surfaces/current.d.ts +17 -0
  593. package/dist/node/surfaces/current.d.ts.map +1 -0
  594. package/dist/node/surfaces/current.js +54 -0
  595. package/dist/node/surfaces/current.js.map +1 -0
  596. package/dist/node/surfaces/index.d.ts +6 -0
  597. package/dist/node/surfaces/index.d.ts.map +1 -0
  598. package/dist/node/surfaces/index.js +6 -0
  599. package/dist/node/surfaces/index.js.map +1 -0
  600. package/dist/node/surfaces/published.d.ts +27 -0
  601. package/dist/node/surfaces/published.d.ts.map +1 -0
  602. package/dist/node/surfaces/published.js +142 -0
  603. package/dist/node/surfaces/published.js.map +1 -0
  604. package/dist/node/surfaces/served.d.ts +15 -0
  605. package/dist/node/surfaces/served.d.ts.map +1 -0
  606. package/dist/node/surfaces/served.js +42 -0
  607. package/dist/node/surfaces/served.js.map +1 -0
  608. package/dist/node/surfaces/surface-build.d.ts +53 -0
  609. package/dist/node/surfaces/surface-build.d.ts.map +1 -0
  610. package/dist/node/surfaces/surface-build.js +105 -0
  611. package/dist/node/surfaces/surface-build.js.map +1 -0
  612. package/dist/node/surfaces/tui.d.ts +40 -0
  613. package/dist/node/surfaces/tui.d.ts.map +1 -0
  614. package/dist/node/surfaces/tui.js +55 -0
  615. package/dist/node/surfaces/tui.js.map +1 -0
  616. package/dist/node/surfaces/web.d.ts +56 -0
  617. package/dist/node/surfaces/web.d.ts.map +1 -0
  618. package/dist/node/surfaces/web.js +108 -0
  619. package/dist/node/surfaces/web.js.map +1 -0
  620. package/dist/node/tui/actions.d.ts +49 -0
  621. package/dist/node/tui/actions.d.ts.map +1 -0
  622. package/dist/node/tui/actions.js +195 -0
  623. package/dist/node/tui/actions.js.map +1 -0
  624. package/dist/node/tui/credentials.d.ts +17 -0
  625. package/dist/node/tui/credentials.d.ts.map +1 -0
  626. package/dist/node/tui/credentials.js +54 -0
  627. package/dist/node/tui/credentials.js.map +1 -0
  628. package/dist/node/tui/filter.d.ts +5 -0
  629. package/dist/node/tui/filter.d.ts.map +1 -0
  630. package/dist/node/tui/filter.js +79 -0
  631. package/dist/node/tui/filter.js.map +1 -0
  632. package/dist/node/tui/index.d.ts +6 -0
  633. package/dist/node/tui/index.d.ts.map +1 -0
  634. package/dist/node/tui/index.js +6 -0
  635. package/dist/node/tui/index.js.map +1 -0
  636. package/dist/node/tui/launcher.d.ts +40 -0
  637. package/dist/node/tui/launcher.d.ts.map +1 -0
  638. package/dist/node/tui/launcher.js +439 -0
  639. package/dist/node/tui/launcher.js.map +1 -0
  640. package/dist/node/tui/pipe.d.ts +18 -0
  641. package/dist/node/tui/pipe.d.ts.map +1 -0
  642. package/dist/node/tui/pipe.js +134 -0
  643. package/dist/node/tui/pipe.js.map +1 -0
  644. package/dist/node/tui/sandbox.d.ts +54 -0
  645. package/dist/node/tui/sandbox.d.ts.map +1 -0
  646. package/dist/node/tui/sandbox.js +431 -0
  647. package/dist/node/tui/sandbox.js.map +1 -0
  648. package/dist/node/workers/base.d.ts +30 -0
  649. package/dist/node/workers/base.d.ts.map +1 -0
  650. package/dist/node/workers/base.js +255 -0
  651. package/dist/node/workers/base.js.map +1 -0
  652. package/dist/node/workers/child.d.ts +4 -0
  653. package/dist/node/workers/child.d.ts.map +1 -0
  654. package/dist/node/workers/child.js +6 -0
  655. package/dist/node/workers/child.js.map +1 -0
  656. package/dist/node/workers/control.d.ts +83 -0
  657. package/dist/node/workers/control.d.ts.map +1 -0
  658. package/dist/node/workers/control.js +77 -0
  659. package/dist/node/workers/control.js.map +1 -0
  660. package/dist/node/workers/index.d.ts +4 -0
  661. package/dist/node/workers/index.d.ts.map +1 -0
  662. package/dist/node/workers/index.js +7 -0
  663. package/dist/node/workers/index.js.map +1 -0
  664. package/dist/node/workers/lock.d.ts +30 -0
  665. package/dist/node/workers/lock.d.ts.map +1 -0
  666. package/dist/node/workers/lock.js +180 -0
  667. package/dist/node/workers/lock.js.map +1 -0
  668. package/dist/node/workers/main.d.ts +46 -0
  669. package/dist/node/workers/main.d.ts.map +1 -0
  670. package/dist/node/workers/main.js +230 -0
  671. package/dist/node/workers/main.js.map +1 -0
  672. package/dist/node/workers/process.d.ts +42 -0
  673. package/dist/node/workers/process.d.ts.map +1 -0
  674. package/dist/node/workers/process.js +125 -0
  675. package/dist/node/workers/process.js.map +1 -0
  676. package/dist/node/workers/workers.d.ts +122 -0
  677. package/dist/node/workers/workers.d.ts.map +1 -0
  678. package/dist/node/workers/workers.js +677 -0
  679. package/dist/node/workers/workers.js.map +1 -0
  680. package/dist/output.d.ts +1 -1
  681. package/dist/output.d.ts.map +1 -1
  682. package/dist/output.js +4 -2
  683. package/dist/output.js.map +1 -1
  684. package/dist/react/hooks.d.ts +43 -0
  685. package/dist/react/hooks.d.ts.map +1 -0
  686. package/dist/react/hooks.js +82 -0
  687. package/dist/react/hooks.js.map +1 -0
  688. package/dist/react/index.d.ts +3 -0
  689. package/dist/react/index.d.ts.map +1 -0
  690. package/dist/react/index.js +4 -0
  691. package/dist/react/index.js.map +1 -0
  692. package/dist/react/stores.d.ts +9 -0
  693. package/dist/react/stores.d.ts.map +1 -0
  694. package/dist/react/stores.js +37 -0
  695. package/dist/react/stores.js.map +1 -0
  696. package/dist/registries/contribution.d.ts +50 -0
  697. package/dist/registries/contribution.d.ts.map +1 -0
  698. package/dist/registries/contribution.js +129 -0
  699. package/dist/registries/contribution.js.map +1 -0
  700. package/dist/registries/entries.d.ts +18 -0
  701. package/dist/registries/entries.d.ts.map +1 -0
  702. package/dist/registries/entries.js +42 -0
  703. package/dist/registries/entries.js.map +1 -0
  704. package/dist/registries/index.d.ts +3 -0
  705. package/dist/registries/index.d.ts.map +1 -0
  706. package/dist/registries/index.js +5 -0
  707. package/dist/registries/index.js.map +1 -0
  708. package/dist/registries/internal.d.ts +4 -0
  709. package/dist/registries/internal.d.ts.map +1 -0
  710. package/dist/registries/internal.js +5 -0
  711. package/dist/registries/internal.js.map +1 -0
  712. package/dist/registries/record.d.ts +115 -0
  713. package/dist/registries/record.d.ts.map +1 -0
  714. package/dist/registries/record.js +283 -0
  715. package/dist/registries/record.js.map +1 -0
  716. package/dist/registries/registry.d.ts +74 -0
  717. package/dist/registries/registry.d.ts.map +1 -0
  718. package/dist/registries/registry.js +14 -0
  719. package/dist/registries/registry.js.map +1 -0
  720. package/dist/registries/value.d.ts +48 -0
  721. package/dist/registries/value.d.ts.map +1 -0
  722. package/dist/registries/value.js +224 -0
  723. package/dist/registries/value.js.map +1 -0
  724. package/dist/remote/calls.d.ts +25 -0
  725. package/dist/remote/calls.d.ts.map +1 -0
  726. package/dist/remote/calls.js +62 -0
  727. package/dist/remote/calls.js.map +1 -0
  728. package/dist/remote/consumer.d.ts +43 -0
  729. package/dist/remote/consumer.d.ts.map +1 -0
  730. package/dist/remote/consumer.js +350 -0
  731. package/dist/remote/consumer.js.map +1 -0
  732. package/dist/remote/hold.d.ts +141 -0
  733. package/dist/remote/hold.d.ts.map +1 -0
  734. package/dist/remote/hold.js +579 -0
  735. package/dist/remote/hold.js.map +1 -0
  736. package/dist/remote/host-link.d.ts +77 -0
  737. package/dist/remote/host-link.d.ts.map +1 -0
  738. package/dist/remote/host-link.js +184 -0
  739. package/dist/remote/host-link.js.map +1 -0
  740. package/dist/remote/internal.d.ts +6 -0
  741. package/dist/remote/internal.d.ts.map +1 -0
  742. package/dist/remote/internal.js +6 -0
  743. package/dist/remote/internal.js.map +1 -0
  744. package/dist/remote/path.d.ts +46 -0
  745. package/dist/remote/path.d.ts.map +1 -0
  746. package/dist/remote/path.js +73 -0
  747. package/dist/remote/path.js.map +1 -0
  748. package/dist/remote/provider.d.ts +89 -0
  749. package/dist/remote/provider.d.ts.map +1 -0
  750. package/dist/remote/provider.js +597 -0
  751. package/dist/remote/provider.js.map +1 -0
  752. package/dist/remote/relay.d.ts +69 -0
  753. package/dist/remote/relay.d.ts.map +1 -0
  754. package/dist/remote/relay.js +219 -0
  755. package/dist/remote/relay.js.map +1 -0
  756. package/dist/remote/remote-object.d.ts +138 -0
  757. package/dist/remote/remote-object.d.ts.map +1 -0
  758. package/dist/remote/remote-object.js +492 -0
  759. package/dist/remote/remote-object.js.map +1 -0
  760. package/dist/remote/replica.d.ts +59 -0
  761. package/dist/remote/replica.d.ts.map +1 -0
  762. package/dist/remote/replica.js +125 -0
  763. package/dist/remote/replica.js.map +1 -0
  764. package/dist/sqlite/cloudflare.d.ts +2 -0
  765. package/dist/sqlite/cloudflare.d.ts.map +1 -1
  766. package/dist/sqlite/cloudflare.js +10 -0
  767. package/dist/sqlite/cloudflare.js.map +1 -1
  768. package/dist/sqlite/database.d.ts +5 -2
  769. package/dist/sqlite/database.d.ts.map +1 -1
  770. package/dist/sqlite/database.js.map +1 -1
  771. package/dist/sqlite/node.d.ts +13 -2
  772. package/dist/sqlite/node.d.ts.map +1 -1
  773. package/dist/sqlite/node.js +31 -8
  774. package/dist/sqlite/node.js.map +1 -1
  775. package/dist/state/batch.d.ts +64 -0
  776. package/dist/state/batch.d.ts.map +1 -0
  777. package/dist/state/batch.js +164 -0
  778. package/dist/state/batch.js.map +1 -0
  779. package/dist/state/derive.d.ts +52 -0
  780. package/dist/state/derive.d.ts.map +1 -0
  781. package/dist/state/derive.js +182 -0
  782. package/dist/state/derive.js.map +1 -0
  783. package/dist/state/index.d.ts +6 -0
  784. package/dist/state/index.d.ts.map +1 -0
  785. package/dist/state/index.js +4 -0
  786. package/dist/state/index.js.map +1 -0
  787. package/dist/state/internal.d.ts +6 -0
  788. package/dist/state/internal.d.ts.map +1 -0
  789. package/dist/state/internal.js +8 -0
  790. package/dist/state/internal.js.map +1 -0
  791. package/dist/state/native-source.d.ts +106 -0
  792. package/dist/state/native-source.d.ts.map +1 -0
  793. package/dist/state/native-source.js +240 -0
  794. package/dist/state/native-source.js.map +1 -0
  795. package/dist/state/watchable.d.ts +114 -0
  796. package/dist/state/watchable.d.ts.map +1 -0
  797. package/dist/state/watchable.js +156 -0
  798. package/dist/state/watchable.js.map +1 -0
  799. package/dist/state/writable.d.ts +32 -0
  800. package/dist/state/writable.d.ts.map +1 -0
  801. package/dist/state/writable.js +49 -0
  802. package/dist/state/writable.js.map +1 -0
  803. package/dist/store/adapter.d.ts +67 -0
  804. package/dist/store/adapter.d.ts.map +1 -0
  805. package/dist/store/adapter.js +118 -0
  806. package/dist/store/adapter.js.map +1 -0
  807. package/dist/store/allocator.d.ts +17 -0
  808. package/dist/store/allocator.d.ts.map +1 -0
  809. package/dist/store/allocator.js +30 -0
  810. package/dist/store/allocator.js.map +1 -0
  811. package/dist/store/applier.d.ts +8 -0
  812. package/dist/store/applier.d.ts.map +1 -0
  813. package/dist/store/applier.js +375 -0
  814. package/dist/store/applier.js.map +1 -0
  815. package/dist/store/apply-batch.d.ts +12 -0
  816. package/dist/store/apply-batch.d.ts.map +1 -0
  817. package/dist/store/apply-batch.js +83 -0
  818. package/dist/store/apply-batch.js.map +1 -0
  819. package/dist/store/archive.d.ts +32 -0
  820. package/dist/store/archive.d.ts.map +1 -0
  821. package/dist/store/archive.js +130 -0
  822. package/dist/store/archive.js.map +1 -0
  823. package/dist/store/backend.d.ts +10 -0
  824. package/dist/store/backend.d.ts.map +1 -0
  825. package/dist/store/backend.js +15 -0
  826. package/dist/store/backend.js.map +1 -0
  827. package/dist/store/catalog.d.ts +24 -0
  828. package/dist/store/catalog.d.ts.map +1 -0
  829. package/dist/store/catalog.js +17 -0
  830. package/dist/store/catalog.js.map +1 -0
  831. package/dist/store/claims.d.ts +23 -0
  832. package/dist/store/claims.d.ts.map +1 -0
  833. package/dist/store/claims.js +65 -0
  834. package/dist/store/claims.js.map +1 -0
  835. package/dist/store/columns.d.ts +77 -0
  836. package/dist/store/columns.d.ts.map +1 -0
  837. package/dist/store/columns.js +77 -0
  838. package/dist/store/columns.js.map +1 -0
  839. package/dist/store/engine.d.ts +176 -0
  840. package/dist/store/engine.d.ts.map +1 -0
  841. package/dist/store/engine.js +283 -0
  842. package/dist/store/engine.js.map +1 -0
  843. package/dist/store/evaluate.d.ts +17 -0
  844. package/dist/store/evaluate.d.ts.map +1 -0
  845. package/dist/store/evaluate.js +128 -0
  846. package/dist/store/evaluate.js.map +1 -0
  847. package/dist/store/export.d.ts +17 -0
  848. package/dist/store/export.d.ts.map +1 -0
  849. package/dist/store/export.js +200 -0
  850. package/dist/store/export.js.map +1 -0
  851. package/dist/store/expression.d.ts +150 -0
  852. package/dist/store/expression.d.ts.map +1 -0
  853. package/dist/store/expression.js +268 -0
  854. package/dist/store/expression.js.map +1 -0
  855. package/dist/store/import.d.ts +10 -0
  856. package/dist/store/import.d.ts.map +1 -0
  857. package/dist/store/import.js +276 -0
  858. package/dist/store/import.js.map +1 -0
  859. package/dist/store/index.d.ts +16 -0
  860. package/dist/store/index.d.ts.map +1 -0
  861. package/dist/store/index.js +13 -0
  862. package/dist/store/index.js.map +1 -0
  863. package/dist/store/json-column.d.ts +33 -0
  864. package/dist/store/json-column.d.ts.map +1 -0
  865. package/dist/store/json-column.js +62 -0
  866. package/dist/store/json-column.js.map +1 -0
  867. package/dist/store/jsonl.d.ts +9 -0
  868. package/dist/store/jsonl.d.ts.map +1 -0
  869. package/dist/store/jsonl.js +61 -0
  870. package/dist/store/jsonl.js.map +1 -0
  871. package/dist/store/live-query.d.ts +108 -0
  872. package/dist/store/live-query.d.ts.map +1 -0
  873. package/dist/store/live-query.js +254 -0
  874. package/dist/store/live-query.js.map +1 -0
  875. package/dist/store/outbox.d.ts +26 -0
  876. package/dist/store/outbox.d.ts.map +1 -0
  877. package/dist/store/outbox.js +80 -0
  878. package/dist/store/outbox.js.map +1 -0
  879. package/dist/store/query.d.ts +39 -0
  880. package/dist/store/query.d.ts.map +1 -0
  881. package/dist/store/query.js +97 -0
  882. package/dist/store/query.js.map +1 -0
  883. package/dist/store/refusal.d.ts +15 -0
  884. package/dist/store/refusal.d.ts.map +1 -0
  885. package/dist/store/refusal.js +36 -0
  886. package/dist/store/refusal.js.map +1 -0
  887. package/dist/store/rows.d.ts +80 -0
  888. package/dist/store/rows.d.ts.map +1 -0
  889. package/dist/store/rows.js +183 -0
  890. package/dist/store/rows.js.map +1 -0
  891. package/dist/store/schema.d.ts +25 -0
  892. package/dist/store/schema.d.ts.map +1 -0
  893. package/dist/store/schema.js +115 -0
  894. package/dist/store/schema.js.map +1 -0
  895. package/dist/store/sql.d.ts +73 -0
  896. package/dist/store/sql.d.ts.map +1 -0
  897. package/dist/store/sql.js +234 -0
  898. package/dist/store/sql.js.map +1 -0
  899. package/dist/store/step.d.ts +50 -0
  900. package/dist/store/step.d.ts.map +1 -0
  901. package/dist/store/step.js +52 -0
  902. package/dist/store/step.js.map +1 -0
  903. package/dist/store/store.d.ts +171 -0
  904. package/dist/store/store.d.ts.map +1 -0
  905. package/dist/store/store.js +818 -0
  906. package/dist/store/store.js.map +1 -0
  907. package/dist/store/table-changes.d.ts +29 -0
  908. package/dist/store/table-changes.d.ts.map +1 -0
  909. package/dist/store/table-changes.js +102 -0
  910. package/dist/store/table-changes.js.map +1 -0
  911. package/dist/store/table-evolve.d.ts +74 -0
  912. package/dist/store/table-evolve.d.ts.map +1 -0
  913. package/dist/store/table-evolve.js +201 -0
  914. package/dist/store/table-evolve.js.map +1 -0
  915. package/dist/store/table-handle.d.ts +73 -0
  916. package/dist/store/table-handle.d.ts.map +1 -0
  917. package/dist/store/table-handle.js +101 -0
  918. package/dist/store/table-handle.js.map +1 -0
  919. package/dist/store/table-history.d.ts +75 -0
  920. package/dist/store/table-history.d.ts.map +1 -0
  921. package/dist/store/table-history.js +42 -0
  922. package/dist/store/table-history.js.map +1 -0
  923. package/dist/store/table-migration.d.ts +16 -0
  924. package/dist/store/table-migration.d.ts.map +1 -0
  925. package/dist/store/table-migration.js +202 -0
  926. package/dist/store/table-migration.js.map +1 -0
  927. package/dist/store/table-writes.d.ts +125 -0
  928. package/dist/store/table-writes.d.ts.map +1 -0
  929. package/dist/store/table-writes.js +170 -0
  930. package/dist/store/table-writes.js.map +1 -0
  931. package/dist/store/table.d.ts +80 -0
  932. package/dist/store/table.d.ts.map +1 -0
  933. package/dist/store/table.js +103 -0
  934. package/dist/store/table.js.map +1 -0
  935. package/dist/store/text.d.ts +14 -0
  936. package/dist/store/text.d.ts.map +1 -0
  937. package/dist/store/text.js +51 -0
  938. package/dist/store/text.js.map +1 -0
  939. package/dist/store/writes.d.ts +5 -0
  940. package/dist/store/writes.d.ts.map +1 -0
  941. package/dist/store/writes.js +4 -0
  942. package/dist/store/writes.js.map +1 -0
  943. package/dist/surfaces/active-language.d.ts +20 -0
  944. package/dist/surfaces/active-language.d.ts.map +1 -0
  945. package/dist/surfaces/active-language.js +78 -0
  946. package/dist/surfaces/active-language.js.map +1 -0
  947. package/dist/surfaces/active-theme.d.ts +16 -0
  948. package/dist/surfaces/active-theme.d.ts.map +1 -0
  949. package/dist/surfaces/active-theme.js +47 -0
  950. package/dist/surfaces/active-theme.js.map +1 -0
  951. package/dist/surfaces/client.d.ts +96 -0
  952. package/dist/surfaces/client.d.ts.map +1 -0
  953. package/dist/surfaces/client.js +264 -0
  954. package/dist/surfaces/client.js.map +1 -0
  955. package/dist/surfaces/connection.d.ts +49 -0
  956. package/dist/surfaces/connection.d.ts.map +1 -0
  957. package/dist/surfaces/connection.js +142 -0
  958. package/dist/surfaces/connection.js.map +1 -0
  959. package/dist/surfaces/format.d.ts +5 -0
  960. package/dist/surfaces/format.d.ts.map +1 -0
  961. package/dist/surfaces/format.js +37 -0
  962. package/dist/surfaces/format.js.map +1 -0
  963. package/dist/surfaces/index.d.ts +15 -0
  964. package/dist/surfaces/index.d.ts.map +1 -0
  965. package/dist/surfaces/index.js +18 -0
  966. package/dist/surfaces/index.js.map +1 -0
  967. package/dist/surfaces/language.d.ts +53 -0
  968. package/dist/surfaces/language.d.ts.map +1 -0
  969. package/dist/surfaces/language.js +74 -0
  970. package/dist/surfaces/language.js.map +1 -0
  971. package/dist/surfaces/loaded.d.ts +42 -0
  972. package/dist/surfaces/loaded.d.ts.map +1 -0
  973. package/dist/surfaces/loaded.js +116 -0
  974. package/dist/surfaces/loaded.js.map +1 -0
  975. package/dist/surfaces/locale-choice.d.ts +25 -0
  976. package/dist/surfaces/locale-choice.d.ts.map +1 -0
  977. package/dist/surfaces/locale-choice.js +14 -0
  978. package/dist/surfaces/locale-choice.js.map +1 -0
  979. package/dist/surfaces/messages.d.ts +67 -0
  980. package/dist/surfaces/messages.d.ts.map +1 -0
  981. package/dist/surfaces/messages.js +112 -0
  982. package/dist/surfaces/messages.js.map +1 -0
  983. package/dist/surfaces/page-state.d.ts +14 -0
  984. package/dist/surfaces/page-state.d.ts.map +1 -0
  985. package/dist/surfaces/page-state.js +43 -0
  986. package/dist/surfaces/page-state.js.map +1 -0
  987. package/dist/surfaces/preference.d.ts +32 -0
  988. package/dist/surfaces/preference.d.ts.map +1 -0
  989. package/dist/surfaces/preference.js +72 -0
  990. package/dist/surfaces/preference.js.map +1 -0
  991. package/dist/surfaces/registered.d.ts +33 -0
  992. package/dist/surfaces/registered.d.ts.map +1 -0
  993. package/dist/surfaces/registered.js +51 -0
  994. package/dist/surfaces/registered.js.map +1 -0
  995. package/dist/surfaces/scripts.d.ts +84 -0
  996. package/dist/surfaces/scripts.d.ts.map +1 -0
  997. package/dist/surfaces/scripts.js +28 -0
  998. package/dist/surfaces/scripts.js.map +1 -0
  999. package/dist/surfaces/theme-choice.d.ts +31 -0
  1000. package/dist/surfaces/theme-choice.d.ts.map +1 -0
  1001. package/dist/surfaces/theme-choice.js +18 -0
  1002. package/dist/surfaces/theme-choice.js.map +1 -0
  1003. package/dist/surfaces/themes.d.ts +43 -0
  1004. package/dist/surfaces/themes.d.ts.map +1 -0
  1005. package/dist/surfaces/themes.js +110 -0
  1006. package/dist/surfaces/themes.js.map +1 -0
  1007. package/dist/surfaces/translations.d.ts +29 -0
  1008. package/dist/surfaces/translations.d.ts.map +1 -0
  1009. package/dist/surfaces/translations.js +57 -0
  1010. package/dist/surfaces/translations.js.map +1 -0
  1011. package/dist/sync/batch.d.ts +18 -0
  1012. package/dist/sync/batch.d.ts.map +1 -0
  1013. package/dist/sync/batch.js +125 -0
  1014. package/dist/sync/batch.js.map +1 -0
  1015. package/dist/sync/client.d.ts +166 -0
  1016. package/dist/sync/client.d.ts.map +1 -0
  1017. package/dist/sync/client.js +438 -0
  1018. package/dist/sync/client.js.map +1 -0
  1019. package/dist/sync/files.d.ts +46 -0
  1020. package/dist/sync/files.d.ts.map +1 -0
  1021. package/dist/sync/files.js +13 -0
  1022. package/dist/sync/files.js.map +1 -0
  1023. package/dist/sync/filesystem.d.ts +37 -0
  1024. package/dist/sync/filesystem.d.ts.map +1 -0
  1025. package/dist/sync/filesystem.js +185 -0
  1026. package/dist/sync/filesystem.js.map +1 -0
  1027. package/dist/sync/index.d.ts +8 -0
  1028. package/dist/sync/index.d.ts.map +1 -0
  1029. package/dist/sync/index.js +9 -0
  1030. package/dist/sync/index.js.map +1 -0
  1031. package/dist/sync/lines.d.ts +18 -0
  1032. package/dist/sync/lines.d.ts.map +1 -0
  1033. package/dist/sync/lines.js +46 -0
  1034. package/dist/sync/lines.js.map +1 -0
  1035. package/dist/sync/remote.d.ts +130 -0
  1036. package/dist/sync/remote.d.ts.map +1 -0
  1037. package/dist/sync/remote.js +46 -0
  1038. package/dist/sync/remote.js.map +1 -0
  1039. package/dist/sync/state.d.ts +17 -0
  1040. package/dist/sync/state.d.ts.map +1 -0
  1041. package/dist/sync/state.js +40 -0
  1042. package/dist/sync/state.js.map +1 -0
  1043. package/dist/telemetry-ingest/batch.d.ts +70 -0
  1044. package/dist/telemetry-ingest/batch.d.ts.map +1 -0
  1045. package/dist/telemetry-ingest/batch.js +110 -0
  1046. package/dist/telemetry-ingest/batch.js.map +1 -0
  1047. package/dist/telemetry-ingest/facet.d.ts +21 -0
  1048. package/dist/telemetry-ingest/facet.d.ts.map +1 -0
  1049. package/dist/telemetry-ingest/facet.js +96 -0
  1050. package/dist/telemetry-ingest/facet.js.map +1 -0
  1051. package/dist/telemetry-ingest/index.d.ts +5 -0
  1052. package/dist/telemetry-ingest/index.d.ts.map +1 -0
  1053. package/dist/telemetry-ingest/index.js +8 -0
  1054. package/dist/telemetry-ingest/index.js.map +1 -0
  1055. package/dist/telemetry-ingest/recorder.d.ts +50 -0
  1056. package/dist/telemetry-ingest/recorder.d.ts.map +1 -0
  1057. package/dist/telemetry-ingest/recorder.js +298 -0
  1058. package/dist/telemetry-ingest/recorder.js.map +1 -0
  1059. package/dist/telemetry-ingest/sender.d.ts +14 -0
  1060. package/dist/telemetry-ingest/sender.d.ts.map +1 -0
  1061. package/dist/telemetry-ingest/sender.js +64 -0
  1062. package/dist/telemetry-ingest/sender.js.map +1 -0
  1063. package/dist/telemetry-sqlite/index.d.ts +13 -4
  1064. package/dist/telemetry-sqlite/index.d.ts.map +1 -1
  1065. package/dist/telemetry-sqlite/index.js +81 -241
  1066. package/dist/telemetry-sqlite/index.js.map +1 -1
  1067. package/dist/telemetry-sqlite/node.d.ts +13 -0
  1068. package/dist/telemetry-sqlite/node.d.ts.map +1 -0
  1069. package/dist/telemetry-sqlite/node.js +27 -0
  1070. package/dist/telemetry-sqlite/node.js.map +1 -0
  1071. package/dist/telemetry-sqlite/schema.d.ts +9 -0
  1072. package/dist/telemetry-sqlite/schema.d.ts.map +1 -0
  1073. package/dist/telemetry-sqlite/schema.js +61 -0
  1074. package/dist/telemetry-sqlite/schema.js.map +1 -0
  1075. package/dist/telemetry-sqlite/store.d.ts +3 -0
  1076. package/dist/telemetry-sqlite/store.d.ts.map +1 -1
  1077. package/dist/telemetry-sqlite/store.js +24 -13
  1078. package/dist/telemetry-sqlite/store.js.map +1 -1
  1079. package/dist/telemetry.d.ts +937 -0
  1080. package/dist/telemetry.d.ts.map +1 -0
  1081. package/dist/telemetry.js +470 -0
  1082. package/dist/telemetry.js.map +1 -0
  1083. package/dist/tui/bridge.d.ts +112 -0
  1084. package/dist/tui/bridge.d.ts.map +1 -0
  1085. package/dist/tui/bridge.js +9 -0
  1086. package/dist/tui/bridge.js.map +1 -0
  1087. package/dist/tui/channel.d.ts +44 -0
  1088. package/dist/tui/channel.d.ts.map +1 -0
  1089. package/dist/tui/channel.js +150 -0
  1090. package/dist/tui/channel.js.map +1 -0
  1091. package/dist/tui/frames.d.ts +5 -0
  1092. package/dist/tui/frames.d.ts.map +1 -0
  1093. package/dist/tui/frames.js +52 -0
  1094. package/dist/tui/frames.js.map +1 -0
  1095. package/dist/tui/index.d.ts +7 -0
  1096. package/dist/tui/index.d.ts.map +1 -0
  1097. package/dist/tui/index.js +8 -0
  1098. package/dist/tui/index.js.map +1 -0
  1099. package/dist/tui/runtime.d.ts +46 -0
  1100. package/dist/tui/runtime.d.ts.map +1 -0
  1101. package/dist/tui/runtime.js +101 -0
  1102. package/dist/tui/runtime.js.map +1 -0
  1103. package/dist/tui/scope.d.ts +46 -0
  1104. package/dist/tui/scope.d.ts.map +1 -0
  1105. package/dist/tui/scope.js +141 -0
  1106. package/dist/tui/scope.js.map +1 -0
  1107. package/dist/tui/terminal.d.ts +15 -0
  1108. package/dist/tui/terminal.d.ts.map +1 -0
  1109. package/dist/tui/terminal.js +37 -0
  1110. package/dist/tui/terminal.js.map +1 -0
  1111. package/dist/tui/theme.d.ts +17 -0
  1112. package/dist/tui/theme.d.ts.map +1 -0
  1113. package/dist/tui/theme.js +38 -0
  1114. package/dist/tui/theme.js.map +1 -0
  1115. package/dist/watch.d.ts +194 -0
  1116. package/dist/watch.d.ts.map +1 -0
  1117. package/dist/watch.js +383 -0
  1118. package/dist/watch.js.map +1 -0
  1119. package/dist/web/environment.d.ts +4 -0
  1120. package/dist/web/environment.d.ts.map +1 -0
  1121. package/dist/web/environment.js +37 -0
  1122. package/dist/web/environment.js.map +1 -0
  1123. package/dist/web/index.d.ts +5 -0
  1124. package/dist/web/index.d.ts.map +1 -0
  1125. package/dist/web/index.js +7 -0
  1126. package/dist/web/index.js.map +1 -0
  1127. package/dist/web/language.d.ts +7 -0
  1128. package/dist/web/language.d.ts.map +1 -0
  1129. package/dist/web/language.js +21 -0
  1130. package/dist/web/language.js.map +1 -0
  1131. package/dist/web/page.d.ts +29 -0
  1132. package/dist/web/page.d.ts.map +1 -0
  1133. package/dist/web/page.js +92 -0
  1134. package/dist/web/page.js.map +1 -0
  1135. package/dist/web/scripts.d.ts +23 -0
  1136. package/dist/web/scripts.d.ts.map +1 -0
  1137. package/dist/web/scripts.js +50 -0
  1138. package/dist/web/scripts.js.map +1 -0
  1139. package/dist/web/styles.d.ts +17 -0
  1140. package/dist/web/styles.d.ts.map +1 -0
  1141. package/dist/web/styles.js +218 -0
  1142. package/dist/web/styles.js.map +1 -0
  1143. package/dist/web/themes.d.ts +6 -0
  1144. package/dist/web/themes.d.ts.map +1 -0
  1145. package/dist/web/themes.js +31 -0
  1146. package/dist/web/themes.js.map +1 -0
  1147. package/docs/benchmarks.md +1036 -0
  1148. package/docs/decisions.md +3436 -0
  1149. package/docs/dependencies/book.md +1671 -0
  1150. package/docs/dependencies/index.html +2641 -0
  1151. package/docs/dependencies/model.md +330 -0
  1152. package/docs/design.md +3065 -0
  1153. package/docs/durable-observers/book.md +432 -0
  1154. package/docs/durable-observers/index.html +517 -0
  1155. package/docs/flow-control/book.md +333 -0
  1156. package/docs/flow-control/fold/index.html +265 -0
  1157. package/docs/flow-control/index.html +548 -0
  1158. package/docs/flow-control/window/index.html +228 -0
  1159. package/docs/guide-plugins-hosts/book.md +957 -0
  1160. package/docs/guide-plugins-hosts/index.html +952 -0
  1161. package/docs/http-workers/book.md +378 -0
  1162. package/docs/http-workers/index.html +515 -0
  1163. package/docs/i18n/book.md +217 -0
  1164. package/docs/i18n/index.html +344 -0
  1165. package/docs/layer-1.md +311 -0
  1166. package/docs/layer-2.md +216 -0
  1167. package/docs/p3-features/README.md +6 -0
  1168. package/docs/p3-features/book.md +478 -0
  1169. package/docs/plugin-authoring.md +276 -0
  1170. package/docs/plugin-model/book.md +186 -0
  1171. package/docs/plugin-model/index.html +310 -0
  1172. package/docs/plugin-primer/book.md +273 -0
  1173. package/docs/plugin-primer/index.html +430 -0
  1174. package/docs/slice-1.md +236 -0
  1175. package/docs/slice-10.md +288 -0
  1176. package/docs/slice-11.md +114 -0
  1177. package/docs/slice-12.md +132 -0
  1178. package/docs/slice-13.md +121 -0
  1179. package/docs/slice-2.md +174 -0
  1180. package/docs/slice-3a.md +296 -0
  1181. package/docs/slice-3b.md +197 -0
  1182. package/docs/slice-4.md +146 -0
  1183. package/docs/slice-4a.md +418 -0
  1184. package/docs/slice-4b.md +208 -0
  1185. package/docs/slice-5.md +281 -0
  1186. package/docs/slice-6a.md +676 -0
  1187. package/docs/slice-6b.md +332 -0
  1188. package/docs/slice-7a.md +130 -0
  1189. package/docs/slice-7b/book.md +608 -0
  1190. package/docs/slice-7b/index.html +842 -0
  1191. package/docs/slice-7b.md +255 -0
  1192. package/docs/slice-7c.md +165 -0
  1193. package/docs/slice-8.md +194 -0
  1194. package/docs/slice-9.md +110 -0
  1195. package/docs/slices.md +241 -0
  1196. package/docs/store/book.md +670 -0
  1197. package/docs/store/index.html +861 -0
  1198. package/docs/store/walkthrough.md +193 -0
  1199. package/docs/theming/book.md +223 -0
  1200. package/docs/theming/index.html +371 -0
  1201. package/docs/tutorial/book.md +2169 -0
  1202. package/docs/tutorial/index.html +2112 -0
  1203. package/docs/tutorial/screens/click-after.png +0 -0
  1204. package/docs/tutorial/screens/click-before.png +0 -0
  1205. package/docs/tutorial/screens/edited.png +0 -0
  1206. package/docs/tutorial/screens/french.png +0 -0
  1207. package/docs/tutorial/screens/harbor.png +0 -0
  1208. package/docs/tutorial/screens/page.png +0 -0
  1209. package/docs/tutorial/screens/traces-ada.png +0 -0
  1210. package/docs/tutorial/screens/traces.png +0 -0
  1211. package/package.json +114 -20
  1212. package/src/delta/README.md +69 -0
  1213. package/dist/cell.d.ts +0 -122
  1214. package/dist/cell.d.ts.map +0 -1
  1215. package/dist/cell.js +0 -356
  1216. package/dist/cell.js.map +0 -1
  1217. package/dist/cluster.d.ts +0 -27
  1218. package/dist/cluster.d.ts.map +0 -1
  1219. package/dist/cluster.js +0 -230
  1220. package/dist/cluster.js.map +0 -1
  1221. package/dist/connection.d.ts +0 -270
  1222. package/dist/connection.d.ts.map +0 -1
  1223. package/dist/connection.js +0 -1216
  1224. package/dist/connection.js.map +0 -1
  1225. package/dist/contract.d.ts +0 -208
  1226. package/dist/contract.d.ts.map +0 -1
  1227. package/dist/contract.js +0 -246
  1228. package/dist/contract.js.map +0 -1
  1229. package/dist/declare.d.ts +0 -33
  1230. package/dist/declare.d.ts.map +0 -1
  1231. package/dist/declare.js +0 -2
  1232. package/dist/declare.js.map +0 -1
  1233. package/dist/errors.d.ts +0 -28
  1234. package/dist/errors.d.ts.map +0 -1
  1235. package/dist/errors.js +0 -59
  1236. package/dist/errors.js.map +0 -1
  1237. package/dist/host.d.ts +0 -348
  1238. package/dist/host.d.ts.map +0 -1
  1239. package/dist/host.js +0 -1000
  1240. package/dist/host.js.map +0 -1
  1241. package/dist/link.d.ts +0 -107
  1242. package/dist/link.d.ts.map +0 -1
  1243. package/dist/link.js +0 -355
  1244. package/dist/link.js.map +0 -1
  1245. package/dist/loader.d.ts +0 -4
  1246. package/dist/loader.d.ts.map +0 -1
  1247. package/dist/loader.js +0 -73
  1248. package/dist/loader.js.map +0 -1
  1249. package/dist/manager.d.ts +0 -115
  1250. package/dist/manager.d.ts.map +0 -1
  1251. package/dist/manager.js +0 -345
  1252. package/dist/manager.js.map +0 -1
  1253. package/dist/node.d.ts +0 -4
  1254. package/dist/node.d.ts.map +0 -1
  1255. package/dist/node.js +0 -4
  1256. package/dist/node.js.map +0 -1
  1257. package/dist/object.d.ts +0 -438
  1258. package/dist/object.d.ts.map +0 -1
  1259. package/dist/object.js +0 -1571
  1260. package/dist/object.js.map +0 -1
  1261. package/dist/publish.d.ts +0 -5
  1262. package/dist/publish.d.ts.map +0 -1
  1263. package/dist/publish.js +0 -40
  1264. package/dist/publish.js.map +0 -1
  1265. package/dist/registry.d.ts +0 -25
  1266. package/dist/registry.d.ts.map +0 -1
  1267. package/dist/registry.js +0 -6
  1268. package/dist/registry.js.map +0 -1
  1269. package/dist/reload.d.ts +0 -126
  1270. package/dist/reload.d.ts.map +0 -1
  1271. package/dist/reload.js +0 -653
  1272. package/dist/reload.js.map +0 -1
  1273. package/dist/remote.d.ts +0 -165
  1274. package/dist/remote.d.ts.map +0 -1
  1275. package/dist/remote.js +0 -612
  1276. package/dist/remote.js.map +0 -1
  1277. package/dist/schema.d.ts +0 -21
  1278. package/dist/schema.d.ts.map +0 -1
  1279. package/dist/schema.js +0 -228
  1280. package/dist/schema.js.map +0 -1
  1281. package/dist/state.d.ts +0 -92
  1282. package/dist/state.d.ts.map +0 -1
  1283. package/dist/state.js +0 -287
  1284. package/dist/state.js.map +0 -1
  1285. package/dist/types.d.ts +0 -88
  1286. package/dist/types.d.ts.map +0 -1
  1287. package/dist/types.js +0 -2
  1288. package/dist/types.js.map +0 -1
  1289. package/dist/websocket.d.ts +0 -15
  1290. package/dist/websocket.d.ts.map +0 -1
  1291. package/dist/websocket.js +0 -16
  1292. package/dist/websocket.js.map +0 -1
  1293. package/dist/wire.d.ts +0 -216
  1294. package/dist/wire.d.ts.map +0 -1
  1295. package/dist/wire.js +0 -165
  1296. package/dist/wire.js.map +0 -1
  1297. package/docs/guide.md +0 -814
  1298. package/docs/spec.md +0 -288
  1299. package/docs/tutorial.md +0 -931
@@ -0,0 +1,3436 @@
1
+ # Redesign decisions (input for target design v2)
2
+
3
+ Decisions made with the owner after the plugin review round on `target-design.md` (6559589bd), in the order they were
4
+ made. Later entries supersede earlier ones; superseded entries are marked inline and point to their replacement. The target design v2 is `packages/chord/docs/design.md`, written from this log; new decisions land here
5
+ first and win until the design is updated.
6
+
7
+ ## Plan
8
+
9
+ - **p3 next to p2, fully isolated.** The new design is built as a new app (`apps/p3`) with its own data directory. p2
10
+ keeps running unchanged with its data. Plugins are ported to p3 one at a time.
11
+ - **No legacy data migration.** p3 starts empty: no import of p2's `catalog.sqlite`, `shares.sqlite` or subagents table.
12
+ - **Chord v2 lives in `packages/chord/src/next`** (export `@earendil-works/chord/next`), developed on `private` only
13
+ (owner override of the "pi first" rule for this directory) and upstreamed to pi once stable; then `next/` replaces
14
+ today's Chord and p2 is deleted. `next/` imports `../context`, `../delta`, `../json`, `../output` directly, so
15
+ Durable's `Context`, `Draft` and `JsonValue` are the same modules and types in v2 (no copies, no type conflicts).
16
+ (Replaces "Chord v2 lives inside p3". Now `src` and the package's root: "Chord v2 becomes the package's root".)
17
+ - **Build a playground before p3:** a small system with p3's shape (server, worker processes over IPC, clients over
18
+ WebSockets) and toy plugins, one per concept; it proves the concepts and reload and is the e2e test ground.
19
+ Location `apps/chord-playground` (private). Client: a web page first (a CLI client possibly later). Order: kernel
20
+ (hosts, links, objects, references, principals), then plugin loading and reload with the dev loop, then the web shell
21
+ with a private preview, then each feature built live as a plugin. Plugin changes reload; Chord changes restart the
22
+ hosts (fine for the owner).
23
+ - **Chord v2 is written fresh** in `packages/chord/src/next`, copying what fits from today's Chord (link, wire, cell,
24
+ delta and the like). It needs the same test coverage as today's Chord (tests under `packages/chord/test/next`).
25
+ - **Playground web stack:** React, built with esbuild (loader D) instead of Vite, if feasible.
26
+ - **Reload control in the playground's web shell:** when sources changed, a control pops up listing the changed
27
+ plugins (reload them) or base code (restart all hosts); the owner triggers it manually. The agent can trigger the
28
+ same actions (command or tool). Never automatic on save.
29
+ - **Preview:** `https://p3.mariozechner.at` on slayer (wildcard DNS already resolves), set up like
30
+ `~/p2.mariozechner.at` (Caddy container on `caddy-network`, unix-socket bridge, `run.sh` in tmux) with `basic_auth`;
31
+ the password is chosen by the owner and never stored in the repo. `p2.mariozechner.at` stays the live p2.
32
+
33
+ ## Natives
34
+
35
+ - **Natives only in objects.** An object opens a native in its synchronous factory with
36
+ `self.open(async ({ signal }) => value, close)`; Chord opens it during activation (the object goes live after all
37
+ opens) and closes it when the object ends, in reverse order. The value never changes during the object's life: no
38
+ `self.resource`, `version`, `replace`, `deps`, reopen, stale handles or "read `.value` at use".
39
+ - **The factory principle:** synchronous, declares only (references, documents, natives, derives, routines, jobs,
40
+ contributions); Chord performs activation; the factory receives handles usable in everything it registers; handles
41
+ are valid for the object's whole life.
42
+ - **No natives across plugins:** other plugins reach a native only through the owning object's methods and state, or
43
+ through the owner's registry entries ("Registries and HTTP"). No callbacks as method arguments, also not on
44
+ `local` types ("Object kinds").
45
+ - **The facet is a pure object.** (**superseded in part** by "Facet documents are removed" and "Facets only declare" (2026-10-26): no documents, no routines) It may hold documents, references, routines and contributions, and may not open
46
+ natives (`self.open` does not exist on the facet). Served objects of a facet may read and write the facet's documents;
47
+ a served object's transaction may span its own documents and its facet's documents (lock order: own line, then the
48
+ facet's). The facet may not reach into its served objects' documents.
49
+
50
+ ## API style
51
+
52
+ - **Tagged unions, always,** instead of a status string plus fields that only mean something in some states ("field
53
+ soup"). Each state carries exactly its own fields, and consumers narrow on `kind`. Example: `ref.state` is
54
+ `{ kind: "failed"; cause; retryAt } | { kind: "live" } | ...`, not `status` plus `error` plus `retryAt`.
55
+
56
+ - **Context last (owner, 2026-10-21):** parameters are the required arguments, then the context (`ctx` or a Durable
57
+ `Context`), then optional arguments, as Durable already does (`Storage.scanEntries(query, limit, cursor, context)`,
58
+ `transaction(change, context)`). Examples: `self.tx(fn, ctx)`, `handle.change(fn, ctx)` and keyed
59
+ `handle.change(key, fn, ctx)`, `handle.retire(ctx)` and keyed `handle.retire(key, ctx)`, `chord(fn, context)`,
60
+ `job.start(args, ctx, { key })`. Applied to design.md, the guide and the walkthrough; the frozen `step-*.md` and
61
+ `kernel.md` document the frozen code and keep its signatures. pi's `traceIn(context, options, fn)` (implemented in
62
+ `chord/context`) becomes `traceIn(options, fn, context)` in the same pi change as S1 (owner, 2026-10-21).
63
+
64
+ ## Hosts and placement
65
+
66
+ - **Placement is a function** of the object's typed input, not data naming an input field (`hostFamily(prefix,
67
+ key)` looked a key up in the input untyped). A function is type-checked and composable. The caller runs it and
68
+ sends the result; the provider runs it too and refuses an object that is not its own. Placement is then no longer
69
+ part of the contract fingerprint; a changed rule surfaces as that refusal.
70
+ - **Incarnations are minted by Chord** (uuidv7, the code copied from pi-ai into Chord: no dependency), never passed
71
+ in; the link handshake carries it, so the other side sees a restart as a new incarnation of the same identity.
72
+
73
+ - **The low-level host API** (no topology layer; Radius and DO show entry points vary, so the kernel has no
74
+ "listen"):
75
+ - host kinds are typed: `hostKind("server")`, `hostKind("session", Type.Object({ id: Type.String() }))`; an address
76
+ is `Session({ id: "S1" })`; placement functions return addresses; no host-name strings in app code;
77
+ - `createHost<P>({ address?, principal? })`: a host with an address is addressable and acts as `principal` on its
78
+ own host; no options is a client (not addressable, no own principal); the incarnation is minted inside;
79
+ - (**superseded in part** by "Host values are removed" (Review round 1): start data is a core object) `host.provide(Key, value)` before `load`; factories read host values with `self.value(Key)`;
80
+ - `host.load(facets)` takes imported facets (loaders are adapters);
81
+ - `host.connect(link, { principal })`: the link to the parent (at most one), `principal` = who the parent is;
82
+ today's duplicate `upstream` option is gone;
83
+ - `host.accept(link, { principal })`: a link from a client; every entry point (loopback/tunnel WebSocket server,
84
+ Radius channels, a DO edge, memory) is app or adapter code that authenticates and calls `accept`;
85
+ - `host.route(Kind, { authorize(principal, address), resolve(address, { signal }) => { link, principal } })`:
86
+ how to reach hosts of a kind; `authorize` runs before anything starts; Chord calls `resolve` once per address
87
+ while its link lives and again after a drop; the returned principal is stamped on that link (defined once, in
88
+ the parent; the app sends it to the child in its own start message, with any init data);
89
+ - starting processes, init data and keep-alive are app code (helpers later if apps repeat them).
90
+ Close to today's shape (`createHost`, `connect`, `accept`, `route`, `load`); gone: string host names and prefix
91
+ parsing, app-managed generations, `upstream`, `opaque` runtime objects, the loader inside the kernel.
92
+
93
+ ## Quick decisions on the design's open points (design.md section 26)
94
+
95
+ - **Call-scoped natives (#4):** no `ctx.open`; a native that lives for one call is opened and closed in the method with
96
+ `try/finally` (documented pattern). Revisit only if it shows up often.
97
+ - **Documents across plugins (#6):** no plugin reads or writes another plugin's documents (Chord or Durable). Data
98
+ crosses plugins only through state and methods. ~~Within a plugin, the facet's documents are readable and writable
99
+ by the objects that facet serves (decided earlier).~~ (**superseded** by "Facet documents are removed") Data core enforces lives in a format core owns and is handed to
100
+ core through its API when the data is created (e.g. `createConversation` with `init`), never written into core's
101
+ documents directly.
102
+ - **`overlap` for routines (#8):** no `overlap: "latest"` option; a handler compares with the current value itself.
103
+ - **Async activation guard (#3):** none; authority checks live in the per-principal view's methods (and in
104
+ `route.authorize` before a host starts).
105
+ - **`close` with a reason (#7):** `self.open(open, (value, reason) => …)` with `reason: { kind: "reload" } | { kind:
106
+ "stop" } | { kind: "end" }`, decided by Chord (its plugin is in the reload set and the object is native / the host is
107
+ shutting down / the last reference ended or its type was removed). Owners decide what to do with it. Chord aborts
108
+ only the object's own calls and jobs; work elsewhere that uses the native (e.g. Durable tool executions using a code
109
+ mode sandbox) is the owner's to drain or end in `close` ("old code keeps running" applies to natives too).
110
+ - **Code mode in p3 drains its sandbox in `close`** (**superseded in part** by R16 (`close` detaches the sandbox and disposes of it in the background)) (not moved into Durable): the plugin keeps owning the
111
+ `pi-codemode` sandbox; on `reload` (and `stop`) `close` seals it (no new scripts), running scripts finish or hit
112
+ their deadline, then its VMs and worker threads end (`closeWhenIdle` in `pi-codemode`, or a counter in the plugin).
113
+ New scripts start on the new sandbox through the registry entry. `RunningCalls`, the removal message and
114
+ `abortTask` on reload go away; after a worker stop Durable replays as today.
115
+ - **Invariants across owners (#5):** the owner of the dependent data enforces the rule by reconciling from the other
116
+ owner's state with an idempotent routine; no cross-owner transactions, no "notify the other owner" calls that can
117
+ be lost. Example: ports' forward store watches the catalog and closes forwards of archived sessions (also after a
118
+ crash, from the current value); sessions does not know ports exists.
119
+
120
+ ## Wire (#15)
121
+
122
+ - ~~**Compression:** large JSON envelopes are deflated on WebSocket links (relayed and mobile links benefit); byte
123
+ segments stay raw and uncompressed; IPC never compresses.~~ **Superseded** by R1 and "Compression at the origin".
124
+ - **Frame size limit per link adapter** (**superseded in part** by Q2 (limits per part from the header; `maxPayload` only as the hard cap)) (e.g. 16 MiB on client WebSockets, none on IPC): a frame over the limit fails
125
+ that call or member with `too_large`, the link stays up. Large uploads go through owner chunking anyway.
126
+ - **One batch per commit per link:** a commit that touches several holds on one link travels as one `state` frame;
127
+ the consumer applies every member update of it before listeners run. Remote consumers never see a torn commit,
128
+ matching local subscribers, who are notified once after every document of a commit is adopted.
129
+ - **Adapted bindings get diffs:** a consumer bound through `accepts` (an older contract version) is served like a
130
+ derive of the provider's state through the adapter: Chord keeps the last adapted value per binding, applies the
131
+ adapter to the new value and sends the diff (`diffRevisions`), never the whole adapted value per commit.
132
+
133
+ ## Gaps closed after the walkthrough rework
134
+
135
+ - **Clients** serve only `local` types (not addressable); local calls have no principal; what a client calls on the
136
+ server is stamped with the client's authenticated principal.
137
+ - **Placement refusal** is `invalid/placement`; during a rollout it is handled like a mismatch (provider on another
138
+ revision: park; same revision: fail).
139
+ - **HTTP route signature:** `(req, ctx) => Promise<HttpResponse>`; `ctx.principal` is the request's principal (from
140
+ the request resolvers), so `ctx.get(Notes, …)` gives that principal's view.
141
+ - **Start barrier** (**superseded in part** by R17 (the harness starts after the barrier, which waits until every plugin loaded or failed)) counts facets and `lifetime: "host"` objects; an object that must contribute at start (the model
142
+ runtime) is `lifetime: "host"`.
143
+ - **`persist` only in `local` objects and `lifetime: "host"` objects** (**superseded** by P3: only in input-less `local`
144
+ objects) (identities without a principal); `storeKey`
145
+ is dropped (two principals' objects writing one document would be two lines on one document). Enforced at the type
146
+ level (`self.state` in a remote factory takes only non-persisted definitions) and as a declaration error at runtime
147
+ ("persist needs a stable identity: put the documents into a local core").
148
+ - **Call deadlines:** a call ends through the caller's signal; there is no per-call deadline of its own. Parked calls
149
+ (link drop, loading, rollout) share one default deadline (60 s, like `readyTimeout`), then fail `unavailable`.
150
+ - ~~Lingering through `ref.release({ linger: true })`~~ **superseded** by D8 (no consumer-side linger). Was: a closing view releases with `ref.release({ linger: true })`; the hold stays, marked lingering, until
151
+ its timeout. A link drop is still a departure (presence details stay open).
152
+ - **Contributor order** (**superseded in part** by "Contributor order is sorted by plugin name (never load order)" in the design, section 17) is load order (alphabetical by plugin name), deterministic on every host and across reloads,
153
+ and carries no meaning: wraps in one phase must not depend on each other; meaningful order comes only from phases.
154
+ - **Revocation without field patterns:** the app defines a typed revocation type and a function
155
+ `createHost<P, R>({ …, revokedBy: (p: P) => R[] })` (the same app code on every host); `host.revoke(r)` ends every
156
+ object and closes every link whose principal `p` has `r` in `revokedBy(p)`, and propagates `r` (a small JSON value)
157
+ to the children, which apply their own `revokedBy`. No `PrincipalPattern`, no local-only predicates, no re-stamping:
158
+ a changed grant (mode `write` → `read`) is a revocation of the old one; the client reconnects and authenticates
159
+ anew, and its references re-hold.
160
+ - **Per-session native owners:** (**superseded in part** by "Host values are removed": the session is read from the core object `SessionInfo`) the facet cannot `self.get` a type it serves, so a worker-wide native owner is a
161
+ `lifetime: "host"` object without input that reads its session from a host value (`self.value(SessionInit)`).
162
+
163
+ ## Documents step
164
+
165
+ - **Plugin identity:** every plugin has a `package.json` whose `name` is its globally unique, stable plugin name (like
166
+ an npm package name); that name identifies its facets (~~persisted facet documents,~~ reload, diagnostics; **superseded in part** by "Facet documents are removed"). Loader D
167
+ reads it; static composition passes the same name. Never a directory name, never load order.
168
+ - **Document roots are `JsonObject`,** as in Durable (`defineDoc<T extends JsonObject>`); lists live in a field. Documents
169
+ hold JSON only.
170
+ - **In-memory documents end with their object** (last reference released, linger expired); they survive reloads and
171
+ native restarts of a live object. What must outlive the object is persisted or lives in a `lifetime: "host"` object.
172
+ - **Chord ships its own document storages, no adapter over Durable's storage:** (**superseded** by "The store"
173
+ (2026-10-21): Chord's persisted documents live in the per-session store, next to Durable's adapter and plugin tables) in-memory and SQLite (JSONL was cut later)
174
+ implementations of `DocumentStorage`, inspired by Durable's storage implementations (atomic commits, base plus
175
+ deltas, incarnations) but with Chord's own schema: ordered keys per owner and definition, a tag index. Durable's
176
+ storage is shaped for Durable (scans by id, no tags); Chord documents and Durable transcripts never commit together,
177
+ so they need not share a storage. Node-specific storages live outside the portable core; a Durable Object SQLite
178
+ storage can follow. (Replaces the earlier "adapter over Durable's node SQLite storage" for the playground.)
179
+ - **Durable's SQLite driver layer moves into Chord** (`@earendil-works/chord/sqlite`, `/sqlite/node`,
180
+ `/sqlite/cloudflare`): the `SqliteDatabase`/`SqliteExecutor` interfaces, the `node:sqlite` and Durable Object
181
+ drivers, the migration runner and schema-prefix helpers. Durable imports them from Chord (it already depends on
182
+ Chord, so no cycle) and re-exports them under its old paths so no Durable user breaks. Chord's SQLite document
183
+ storage uses the same drivers (`next/` imports `../sqlite` like `../output`). Done in pi first, then merged.
184
+
185
+ ## Derives and natives step
186
+
187
+ - **`self.open` returns a handle** whose `.value` is the native (throws before activation); no forwarding proxy
188
+ (private fields and identity break through proxies). The value never changes during the object's life.
189
+ - **A partly failed activation** closes the natives already open (reason `end`) and opens all of them again on the
190
+ next attempt.
191
+ - **A broken part fails the whole object** (owner): a derive that throws, or a document whose hydration or `migrate`
192
+ fails, makes the object `failed`; an incomplete object could break invariants that rely on atomic commits across its
193
+ documents. (Replaces "member-local" errors for documents.)
194
+ - **`previous` of derives** is tested through failed/retry until the reload step exists.
195
+ - **`self.retryOn` may appear anywhere in the factory:** the factory specifies, the host orders and executes (watches
196
+ are installed before natives open).
197
+ - **Failure causes are tagged:** `{ kind: "transient" }` (an `open` throws, a reference is unreachable) gets the
198
+ automatic backoff; `{ kind: "broken" }` (factory, derive, document hydration or `migrate` throws; or an `open` that
199
+ throws `broken(…)` because retrying is pointless, e.g. invalid config) gets no automatic retries, only reload,
200
+ `ref.retry()` or `retryOn`. Chord decides by where it failed; only an `open` can choose `broken` itself.
201
+ - **Activation waits for its dependencies:** an object whose references are not live yet stays `loading` and goes live
202
+ as soon as they are (bounded by `readyTimeout`, then it fails `transient` with the dependency as cause); ~~a `broken`
203
+ dependency fails it at once. No retained references while failed.~~ **Superseded** by R18 (dependency failures are
204
+ `transient`, references are kept as an automatic retry trigger).
205
+ - **Timeouts** are host defaults with per-thing overrides: `closeTimeout` 30 s (per native), call drain on teardown
206
+ = `closeTimeout` (per type), `readyTimeout` 60 s (per `get`), park deadline 60 s (per type), routine handover 10 s
207
+ (per routine), job `finish` retry backoff 1 s to 60 s (per job), rollout install 60 s, linger per type; route
208
+ `resolve` is bounded by the app's signal.
209
+ - **Inbox path:** the notes plugin computes its own inbox path from generic start data (the data directory; host
210
+ values were removed later, start data is the core object `SessionInfo`).
211
+ Natives open sequentially in declaration order and close in reverse; a throwing `close` does not stop the others
212
+ (`host.close` rejects with all errors, background cleanup reports to `onError`). Type names `Native<T>`,
213
+ `NativeCloseReason`.
214
+ - **`retryOn` sources** must live independently of the attempt (facet documents, registry values; host values were
215
+ removed later;
216
+ readables the host provides); a reference's member is not allowed (type error). Dependency waiting covers references.
217
+ - ~~**A facet that fails transiently in activation** retries automatically~~ (**superseded** by "Facets only declare": a facet cannot fail after `load`) with backoff, like an object; it does not wait
218
+ for a reload.
219
+ - **Park deadline per consumer:** `get(..., { parkTimeout })` overrides the host default for that reference; the
220
+ provider's per-type override applies to its own local bindings.
221
+ - **Playground:** the inbox watcher only opens and closes for now (the import comes with routines); a note-count derive
222
+ and the reader's redaction as a derive are visible to clients.
223
+
224
+ ## Telemetry
225
+
226
+ - **Context:** (**superseded in part** by "Telemetry types and review cleanup": `Context.telemetry` is always a
227
+ `TelemetrySpan` and `withTelemetry` takes spans) Chord's `Context` gets `telemetry: TelemetryContext` as a fixed member, like `abortSignal` (never
228
+ undefined; `NOOP_TELEMETRY_CONTEXT` by default), with `withTelemetry(telemetry, context)` and
229
+ `traceIn(context, options, fn)` (child span, `fn` gets the derived context). `value()` and keys stay, forever, without
230
+ deprecation. Durable gets telemetry everywhere through the `Context` it already passes; principal and other app
231
+ values stay out of Durable's context (Durable work runs in the scheduler's contexts and resumes without a caller;
232
+ origin travels as data). In Chord v2, `Ctx` extends `Context`: `ctx.telemetry` is the current span, `ctx.trace(name,
233
+ attributes, fn)` gives `fn` a child `ctx` (principal and `get` kept), so calls inside are parented correctly.
234
+ - **One adapter per host:** (**superseded in part** by the adapter is a `TelemetryContext` with readonly `metrics`/`propagation` members, held only by hosts ("Telemetry types and review cleanup")) `createHost({ telemetry })`, the adapter implements `TelemetryContext` and optionally
235
+ `TelemetryPropagation` and `TelemetryMetrics` (no split). Default NOOP.
236
+ - **pi-telemetry additions** (pi first): `TelemetryCarrier = Record<string, string>` (opaque, W3C-compatible);
237
+ `TelemetryPropagation { inject(context) → carrier | undefined; extract(carrier) → TelemetryContext }` (absent,
238
+ invalid or unknown carriers give the adapter's root, never throw; carriers are best effort: after an adapter change
239
+ only links are lost); span links (`SpanOptions.links`, required `TelemetrySpan.addLink(link)`, a link targets a
240
+ context); `NOOP_TELEMETRY_PROPAGATION`; the in-memory adapter implements propagation, links and metrics (carrier
241
+ snapshots, no forced W3C fields); conformance fixture with optional `propagation`. No `isRecording` for now (it would
242
+ break adapters, as would closure attributes).
243
+ - **Metrics** (in the same pi change): `TelemetryMetrics { counter, upDownCounter, histogram }` with `add`/`record`
244
+ (synchronous, cheap, never throw; histogram `record` takes an optional context for exemplars), `InstrumentOptions`
245
+ (description, unit), histogram boundaries, `NOOP_TELEMETRY_METRICS`, typed through `metrics` in telemetry schemas,
246
+ low-cardinality attributes only.
247
+ - **Across links:** (**superseded in part** by R5 (the carrier is a bounded field of the header schema)) `call` and `hold` frames carry the caller's carrier; the provider extracts it and runs the method
248
+ under it; the server adds a `chord.relay` span. Cancellation already travels as `cancel` frames.
249
+ - **Shared work** (an activation awaited by several getters, a worker start, the per-object line) is its own root span
250
+ with a link to its first trigger; every waiting span links to it. Work without a running trigger (routine runs, job
251
+ runs, eviction, backoff retries, background closes) is a root span, linked to its trigger where one exists. Chord
252
+ sets all of these; authors may add links through `ctx.telemetry.addLink`.
253
+ - **Tiers:** spans for calls, methods, relays, holds, activations, opens, closes, loads, revocations, resolves, routine
254
+ runs, job runs and finishes; events on the enclosing span for commits inside a method (`chord.commit` with docs, ops,
255
+ duration), retries and parking; metrics for background high-frequency work (state frames, bytes on WebSocket links,
256
+ commit durations, in-flight calls, active holds, open links, park durations, backpressure waits). Verbose per-commit
257
+ or per-frame spans are an adapter decision (filter by name); Chord, Durable and pi-ai only report. Sampling,
258
+ buffering and limits belong to the adapter.
259
+ - **No measuring for telemetry:** only sizes the code already has are reported (encoded WebSocket frames, `byteLength`
260
+ of bytes, lengths a storage computed while writing); nothing is serialized just to count it.
261
+ - **A Chord telemetry schema now** (typed span, event and metric names; used by tests and debugging).
262
+ - **Debugging:** (**superseded in part** by the SQLite telemetry store and `TelemetryStore` ("Telemetry store")) a JSONL trace adapter (bounded rotating file, batched asynchronous writes, drops when full, never
263
+ blocks) and a text renderer of one trace now; a web trace viewer in the playground (span tree, durations, hosts,
264
+ links, attributes, events; also usable standalone on a JSONL file) with the web shell.
265
+ - **Durable** (pi): (**superseded in part** by "Durable and AI spans" and "Telemetry implementation decisions" (phase spans under the stored carrier; every input submission starts its own trace)) `HarnessOptions.telemetry`; an optional `trace` carrier field on submission and task records (the
266
+ submitting call's or the parent task's carrier, rewritten with the record as other immutable fields are; JSON records,
267
+ no migration, no format bump, old records simply have no link); task runs are child spans while their trigger runs in
268
+ the same process, otherwise root spans linked to the stored carrier; Durable spans get their own schema. Persisted
269
+ Chord jobs store their start carrier the same way.
270
+ - **Own schema, no OTel GenAI (owner, 2026-10-18):** pi's AI and Durable telemetry uses its own typed schema
271
+ (`pi.ai.*`, `pi.durable.*`); the OTel GenAI conventions (development status, renames, streaming still `TODO`) are not
272
+ followed. An OTel adapter may translate pi's names to `gen_ai.*` at export. Model stream details are span events, never
273
+ per delta. The pi part of telemetry (propagation, links, metrics, typed metric schemas, `chord/context` telemetry
274
+ member, `HarnessOptions.telemetry` type) is approved as diffed in `/home/badlogic/pi-telemetry`.
275
+ - **Durable and AI spans (owner, 2026-10-18), partly open:** (**superseded in part** by pi-ai's option is `telemetry` (a span), not `telemetryContext` ("Telemetry types and review cleanup")) every span is short and in one process. Spans:
276
+ `pi.durable.submit` (admission), `pi.durable.phase` (one invocation of any task's phase; fixed name, `task.name`,
277
+ `phase`, `kind`, `attempt`, `resumed`, outcome as attributes), `pi.ai.request` (one model request). Events:
278
+ `pi.ai.first_chunk`, `pi.ai.block` (per `*_end`: type, index, chars, tool; never per delta), `pi.durable.entry`,
279
+ `pi.durable.submission`, `pi.durable.replay` (a codemode call answered from its journal). No content ever; entry ids
280
+ point to the transcript. Submissions and tasks store their creator's carrier; phase spans are children of it. Every
281
+ input submission starts its own trace, linked to its trigger (bounded traces; subagent report chains do not nest).
282
+ No hook spans for now. Metrics: `pi.ai.request.duration`, `pi.ai.first_chunk.duration`, `pi.ai.tokens`, `pi.ai.cost`,
283
+ `pi.durable.phase.duration`, `pi.durable.tasks.active` (ids never on metrics). `pi.ai.request` is emitted by pi-ai
284
+ in one wrapper around `lazyStream` in `models.ts` (both `stream` and `streamSimple`; reads the event stream, changes
285
+ nothing; deferred results get `pi.ai.poll` the same way); Durable passes `telemetryContext`. Background tasks
286
+ (e.g. compaction) are children of the creating phase like every task. A task picked up by a new process sets
287
+ `resumed=true` and stays under its stored carrier (same trace).
288
+ - **Telemetry implementation decisions (owner, 2026-10-18):** `TelemetryContext` carries optional `metrics` and
289
+ `propagation` members (every span from an adapter carries its capabilities), so one type is used everywhere
290
+ (`HarnessOptions.telemetry`, `createHost({ telemetry })`, `Context.telemetry`, pi-ai's option, renamed `telemetry`).
291
+ Entry and submission events only after a successful commit. Several queued inputs started as one run: the run takes
292
+ the first input's carrier and stores the others' as immutable creation-time links on its first phase span; a steer's
293
+ submit span links to the running run's carrier. A phase closed or cancelled without a committed state ends
294
+ `aborted`. Submit spans only on the public submit path; internal admissions inside another commit show as events.
295
+ ToolTask sets `tool.name`/`tool.call_id` on its own span. Write submissions are children of the caller; only input
296
+ submissions start traces. Polls count tokens and cost only for final results. Import budgets raised (pi-ai models
297
+ 18, Durable root 71).
298
+ - **Errors in telemetry (owner, 2026-10-18):** a failed span carries `status: "error"` with a fixed category name and the
299
+ error message truncated to 500 characters (`provider_error` for a model request, `task_faulted` for a faulted phase).
300
+ Cancelling is not an error (`stop=aborted`/`outcome=aborted` keep status ok). Durable's `onReport` stays unchanged;
301
+ every report is additionally a `pi.durable.report` event on the current span (its own short root span outside any
302
+ span) plus a `pi.durable.reports { type }` counter. (Superseded in part below: no root span for reports.)
303
+ - **Telemetry types and review cleanup (owner, 2026-10-18):** `Context.telemetry` is always a `TelemetrySpan` (default:
304
+ the NOOP span); the adapter (`TelemetryContext` with readonly `metrics`/`propagation`) is held only by hosts
305
+ (`createHost`, `HarnessOptions`) to start roots, never put into a Context; pi-ai's option is `telemetry?:
306
+ TelemetrySpan`. Code that only has a Context calls the generic `addEvent` (no typed-events helper, no casts, no
307
+ duck typing). A report is an event on the current span plus the counter and `onReport`; outside any span only the
308
+ latter two. `TaskOptions.trace?: TelemetryCarrier | "root"` (absent: inherit). No protection against broken adapters.
309
+ Telemetry never walks or reads data just to measure (checkpoint event compares the phase only). Instrument caches are
310
+ `WeakMap`s keyed by the adapter's metrics object.
311
+ - **Telemetry store (owner, 2026-10-18):** a SQLite adapter in Chord (`@earendil-works/chord/telemetry-sqlite`, reusing
312
+ `chord/sqlite`, Node and Cloudflare): settled spans one row each (events and links inline), metrics aggregated per
313
+ series per 10 s, a 1 s batched flush that also deletes rows older than the retention (7 days), a bounded queue of
314
+ settled spans (dropped and counted), no retries. A shared read interface `TelemetryStore` in pi-telemetry, modeled on
315
+ Jaeger's span reader: `findTraces({ name, attributes, errors, start window, limit, cursor })` returns complete traces
316
+ (newest trace start first, paged by an opaque cursor), `getTrace(id)`, `findMetrics({ name, window, attributes })`.
317
+ Spans carry global ids and a trace id (a root's own id), links carry trace and span id. Both the SQLite and the
318
+ in-memory adapter implement it; one renderer `renderTrace(spans)` (subpath `pi-telemetry/render`) prints links and
319
+ never follows them. Live spans, incoming-link search and duration search are left out. The store's metric read method is
320
+ `findMetrics` (`metrics` is the recording capability). Adapters trust typed inputs (no validation of malformed
321
+ caller values, no try/catch around recording); only carriers from another process are checked (keys present,
322
+ strings), else the root.
323
+
324
+ ## Review round 1 (Astra, Fable): kernel fixes and decisions
325
+
326
+ Source: `p3-reviews/v2-review-summary.md` (K = kernel fixes, D = decisions).
327
+
328
+ - **Variants (D11):** a member name has the same kind (state or method) in every variant that has it, and a method has
329
+ the same signature (arguments and result) in every variant; state schemas may differ per variant (redaction).
330
+ Enforced at the type level in `remote(...)` and at runtime for contracts arriving as data. K7 becomes that check.
331
+ - **Registry composition is a runtime state, not a staging check (D1; reverses "compose runs in staging").**
332
+ Contributions can be dynamic (only known once a native is open, or changing at runtime), so no conflict check can be
333
+ complete in staging. `compose(table)` is pure and runs whenever the table changes (after a commit, when a contributor
334
+ goes live or ends, when a dynamic contribution changes; contributions may be readables). The composed value is a
335
+ plain value for readers: `compose(table, { report })` always delivers a value, builds the fallback in (e.g. the
336
+ first by plugin name wins) and reports conflicts through `report` (they become owner notices and telemetry events);
337
+ readers never handle conflicts. Only if `compose` throws is the registry in an error state, and readers get an error
338
+ when they use it (like a failed reference). Staging rollback stays for code errors only (load errors, throwing factories). A contribution
339
+ is in the table exactly while its contributor is live (a facet while its plugin is loaded; an object from `live`
340
+ until it ends, fails or restarts). The HTTP router follows the same rule: a duplicate route is a conflict state, not
341
+ a rollback.
342
+ - **Code mode reload vs. a pinned Durable phase (D2):** (**superseded in part** by R16 (the old sandbox is detached and disposed of in the background, not drained in `close`)) the code mode tool entry looks up the current sandbox when
343
+ `execute()` starts (not the one it was created with), so a call pinned by a phase before a reload (e.g. waiting in
344
+ an approval hook) starts on the new sandbox; the old sandbox is still sealed and drained in `close` (running scripts
345
+ finish or hit their deadline, then it ends).
346
+ - **Natives that follow a relocation (D3):** documented pattern, no new mechanism: the native is a stable manager
347
+ (`self.open` once); a routine on the session location switches its internal resource (e.g. the watched directory)
348
+ and bumps a generation, so late events from the old location are dropped. Used for watchers, prompt resources and
349
+ OS probes in p3.
350
+ - **Jobs keep their owner alive (D4):** (**superseded in part** by R11 (revocation ends the objects whatever their jobs)) a job that is running, finishing or blocked in `finish` counts as a reference
351
+ to its object; the object ends only once all its jobs are settled. Starting a job and releasing the reference (an
352
+ HTTP route returning the job id, a tab closing) no longer aborts it. Keep-alive already counts running jobs.
353
+ - **Reloading native objects (D5):** stays as decided: native objects of a reloaded plugin restart. In practice this
354
+ costs little: the Durable harness is core (base code, restarts only with the host), terminal PTYs live in the
355
+ separate terminal host process (the server's native is only the connection), model runtimes, sandboxes and the
356
+ search index rebuild in milliseconds; only working on envs (running remote commands are killed) or radius (relayed
357
+ connections drop) hurts, and only on reloads of exactly that plugin. Keeping natives by name was rejected: natives
358
+ have no line, no transactions and no schema, so old and new code would interleave unchecked.
359
+ - **Old handles stay valid until their calls settle:** when a pure object is swapped in place, a call that started on
360
+ the old code (and its transaction) keeps working with the old handles until it finishes; only new calls get the new
361
+ handles. Matches "running calls finish on old code".
362
+ - **Core vs. plugin is decided by where the code lives:** `src/` (base code, imported statically by the start code
363
+ under a fixed name such as `@p3/core`, not switchable, a change restarts the host) or a plugin directory (found by
364
+ listing, loaded through loader D, named by its `package.json`, switchable, a change reloads it). Core is what the app
365
+ always needs and everything builds on (harness, catalog, access, session location); optional features are plugins
366
+ (code mode stays a plugin).
367
+ - **Host values are removed** (no `hostValue`, `provide`, `self.value`, no host-value versions). Start data (session
368
+ id, init, data directory) is a `local` core object with `lifetime: "host"` and constant state, served by the core
369
+ facet from the start message; platform bindings the host owns (a Durable Object's `state`/`env`) are core objects
370
+ whose `self.open` returns the binding and whose `close` does nothing. Plugins read start data through a reference,
371
+ in openers, derives, routines and methods (after activation); nothing reads it synchronously in a factory. A
372
+ long-lived native from the host is a `lifetime: "host"` object in a core facet.
373
+ - **`accepts` adapters map values (D6):** (**superseded in part** by R6 (provider-owned old schemas plus mappings)) per member, `state: { member: (newValue) => oldValue }` (pure; Chord wraps it
374
+ as a derive and diffs per binding) and `methods: { name: (oldArgs) => newArgs }` (plus an optional result mapping
375
+ back); members an old consumer does not know do not appear. Authors never build readables for adapters.
376
+ - **Revocation and work in cores (D7):** no revocation hook. A core that runs work on behalf of a principal stores the
377
+ grant (or device) as data with the work and reconciles from the grant owner's state (the sharing core's live grants):
378
+ a routine aborts jobs, withdraws questions and drops queued input whose grant no longer exists. The same pattern as
379
+ invariants across owners; it also holds after a crash.
380
+ - **Lingering and keep-alive (D8, decides open #1):** (**superseded in part** by R26 (the counts are one readable, `host.activity`)) the consumer-side linger is removed (`release({ linger })`, the
381
+ lingering mark on holds, the consumer linger cache): a closed tab closes its link, so it could never survive the
382
+ case it was made for. What remains is provider-side `serve(…, { linger })` (an object stays alive for a while after
383
+ its last reference, whoever held it; this is what keeps a just-closed conversation warm). Presence is the live
384
+ references (`host.references`). Worker keep-alive works as in p2 (the worker counts, the app's supervisor decides):
385
+ core code on the worker reports viewers (from `host.references`), active calls and running jobs (Chord's counters)
386
+ and Durable tasks (the core `Harness`), plus provider-side lingering objects; the server's supervisor stops the
387
+ worker when all are zero for the idle time (p2: 5 minutes, checked every second). Chord provides the counts, no idle
388
+ logic.
389
+ - **Validation cost (D9):** (**superseded in part** by R9 (no dev walks: one behavior everywhere) and R7 (arguments validated for every caller, inputs normalized)) document values are not walked against their schema in production. Instead each
390
+ document record stores the fingerprint of the schema it was written with (computed once per definition from the
391
+ structural schema, ignoring descriptions and titles; written at creation and with the new base after a `migrate`).
392
+ On access Chord compares the token's fingerprint with the record's: same version but a different fingerprint fails
393
+ the object `broken` ("schema changed without a version bump"). Always on, O(1). Every structural schema change needs
394
+ a version bump (an additive change gets a trivial `migrate`). Derives and readables are not validated in
395
+ production; in dev, values may be walked as a bug finder (documents at commit, state at publication). Inputs from
396
+ peers (method arguments, hold inputs, with the provider's schemas) are always validated.
397
+ - **Storage failure (D10):** Chord never ends the process. A storage error ends the store and closes the host, and
398
+ `onError` receives a tagged error (`{ kind: "storage_failed", … }`); the app's start code must handle it (log and
399
+ exit with its own code for a supervisor, or reopen). The design states that every app has to; the playground shows
400
+ how (`process.exit` with a distinct code). Fixes the server zombie (a closed host with a live listener).
401
+ - **Reading without starting a worker (D14):** (**superseded in part** by R20 (Chord stamps the caller's incarnation as `ctx.incarnation`)) plain pub/sub in an ordinary plugin, no Chord mechanism: the plugin's
402
+ worker facet records the summary locally and publishes it to the plugin's server object (a method call, from a
403
+ routine on the source); clients read the server object, which never starts a worker. The server object keeps the
404
+ latest value per session (a document if it must survive a server restart) and ignores reports from an older
405
+ worker incarnation. Session list, status, children, unread, artifact lists follow this.
406
+ - **The K list is confirmed** with these changes:
407
+ - K9: (**superseded in part** by R14 (`checkpointWhen` is required on every definition)) like Durable: storages do not validate deltas against content (the store already holds and validated the
408
+ authoritative value); a commit writes the delta; replay only when loading; checkpoints stay optional per
409
+ document, no default.
410
+ - K16: (**superseded in part** by "Flow control: an ack-clocked window" (call window N, not credits; no byte window)) no count limits that refuse; call credits as backpressure (throttle only, never an error); the byte window
411
+ applies to every frame including first snapshots; holds unlimited (protected by authorization and input
412
+ validation).
413
+ - K17: dropped (an IPC peer producing deep JSON shows up at once in development).
414
+ - K21: (**superseded in part** by "Only `tx.draft`" (`tx.get` is gone) and R12 (drafts are revoked; reads after the commit)) values are immutable by convention (Chord Delta's rule, as in Durable), documented for `tx.get`,
415
+ `handle.value`, `handle.initial()`; no copy, no freeze; the `keys()` index is fixed; the write on a read-only
416
+ transaction after a version bump is documented.
417
+
418
+ ## The state path: minimal compute and memory (after the cost review)
419
+
420
+ Source: `p3-reviews/v2-cost-model.md` (before/after per hop, measured by Fable and Astra). Replaces the per-change
421
+ validation and copies the v2 kernel added around Delta.
422
+
423
+ - **The model, end to end, as Delta and `replicatedState` (which Durable already uses):** (**superseded in part** by R1 and Q4 (parts encoded once at the origin per hold; relays rewrite headers; one binary format)) values are immutable
424
+ revisions with structural sharing; commits produce exact ops; subscribers on the same host get the same value and
425
+ ops by reference; derives publish through `diffRevisions` (identity short-circuit; bytes are atomic leaves;
426
+ equality never by `canonicalJson`); each frame is encoded once per link and sized from its encoded bytes; relays
427
+ read only the frame header and forward the body opaquely; receivers check a frame once by its header (`JSON.parse`
428
+ already yields strict JSON; bytes recognized in O(1)); consumers apply with `applyImmutable`. No validation of
429
+ state, no clone of snapshots or ops, no repeated walks.
430
+ - **A v2 readable is `replicatedState`:** today's `chord/src/state.ts` semantics (immutable value, ordered ops,
431
+ gapless attach with snapshot and cursor) is the one readable implementation, imported by `next/` like `../delta`,
432
+ so Durable's views and document watches are state members without conversion.
433
+ - **The store is a port of Durable's store design** (`durable/src/session/transaction.ts`, `session.ts`): one Delta
434
+ tracker per document, prepare and adopt by pointer swap, observers get the commit's ops, storages write the delta
435
+ (or a base when `checkpointWhen` says so) and check only metadata; replay only on load.
436
+ - **Only `tx.draft`:** it returns a draft of the current document, or a draft over `initial()` if the document is
437
+ absent; an unchanged draft (empty ops) commits nothing, so an absent document is never stored by being read.
438
+ `tx.get` is removed.
439
+ - **`derive` stays as it is,** documented with its footgun (a derive that builds new objects for unchanged data costs
440
+ O(value) per commit and per view) and a cookbook of efficient derives for p2's categories: select a field, look up
441
+ by key, small aggregates (just compute); filter/map a list (an identity cache per source element); redaction
442
+ (preferably separate documents per visibility, otherwise the identity cache); joins (identity cache plus the
443
+ looked-up source's revision); a window over a large list (reuse unchanged parts, as p2's chat window). No `project`,
444
+ no view algebra, no `memo` API.
445
+ - **Diffing and derived values come from Delta and today's Chord, not new code:** Delta's `diffRevisions` (tested,
446
+ identity short-circuit) gets bytes as atomic leaves (compared by identity, replaced when different), and v2 calls
447
+ it directly; v2's `diff.ts` is removed. Derives reuse today's derived cells (`chord/src/cell.ts`) on top of the
448
+ readable. Delta gets a small, pure, tested helper `mapShared(f)` (an identity cache per source element: a new object
449
+ per changed element, the previous result for unchanged ones), used by derives that build a new object per source
450
+ element; the cookbook documents it alongside the patterns.
451
+ - **Same-host calls on `remote` types pass values by reference** (**superseded in part** by R9 (no freezing anywhere, no development mode)) (immutable by convention, as `local`); the dev freeze
452
+ catches mutation. (Replaces "validate and copy arguments and results also on the same host".)
453
+ - **Arguments and hold inputs are validated at the provider by the caller's trust:** (**superseded in part** by R7 (arguments validated for every caller; inputs always normalized)) whenever the originating
454
+ principal is not a host principal (guests, owners, any client, also when relayed over trusted IPC), once, with the
455
+ provider's schema; calls from server and worker code are not validated in production. Results are not validated in
456
+ production. Provider schemas are compiled lazily at their first use and cached in a `WeakMap` keyed by the schema
457
+ object; old tokens and their compiled schemas are collected by the GC after a reload.
458
+ - **Contract descriptions travel once per (link, fingerprint):** (**superseded in part** by R6 (no descriptions on the wire; strict fingerprints)) a hold sends the fingerprint; the provider asks for
459
+ the description once and caches the parsed token per link; the cache dies with the link.
460
+ - **Cost guards:** (**superseded in part** by R9 (the counters are ordinary telemetry metrics, always on; no test mode) and T7 (one encode per hold part at its origin, none at relays)) counting tests in the normal test run (in test mode Chord counts full-value walks, deep copies,
461
+ encodes, schema validations and materializations; per hop a test asserts the counts, e.g. one appended note to a
462
+ 500 KB document with 10 viewers: no walks, no copies, one encode per link, no validations) and a `vitest bench`
463
+ suite outside CI (500 KB and 1 MB values, 0/1/10 viewers, memory/SQLite, 1 MiB bytes) with results recorded
464
+ in `packages/chord/docs/benchmarks.md`.
465
+ - **Naming:** the pattern of a `local` object holding shared data behind per-principal views is the **shared store**
466
+ (not "core"); base code and plugins use exactly the same primitives.
467
+
468
+ ## Cuts and closed deferred points (review round 1)
469
+
470
+ - **Cut:** the cross-relay reset coordination (`#collectReset`, `#resyncs`, `#resetRequests`): on an overflow a relay
471
+ releases the hold and re-holds, and the provider sends a fresh snapshot. The JSONL document storage (Chord ships
472
+ memory and SQLite). `MemoryDocumentStorage.prepareCommit`/`snapshot` (unused after K9).
473
+ - **Kept:** (**superseded in part** by R9 (no dev freeze)) the dev freeze (the safety net for passing values by reference). The Questions service stays a documented
474
+ pattern, not built. **Deferred:** the HTTP and channel components, middleware phases and the Durable Object profile,
475
+ until the kernel fixes and a first real proof (harness, one tool with an approval pause, a code mode reload) are
476
+ done; the playground's web shell serves its pages with plain app code until then.
477
+ - **Presence (#10):** a link drop is a departure (accurate and simple); smoothing (a phone switching networks) is the
478
+ UI's job, e.g. a debounced viewer count. Counts per person or origin are derived by the app from
479
+ `host.references` (principal, origin). Workers do not stop on a flicker because of provider-side linger (D8).
480
+ - **Optional dependencies (#2):** a rule, no mechanism: an optional dependency is a registry owned by the dependent
481
+ (ports contributes its forwards to `radius.forwards`; without ports the table is empty). No `optional` on `get`.
482
+ - **Rollout fixes (#12), built with the rollout step:** (a) a host checks its own install status before waiting on a
483
+ provider (a rejected own install fails at once with the reason); (b) installs are serialized per host; (c) a
484
+ revision also carries the code-set digest (the hash over the facet bundles' content hashes that loader D computes
485
+ anyway; no module analysis), and hosts compare the digest on reconnect, so a server restart with unchanged code
486
+ triggers no reinstall.
487
+ - **UI reload vs. leaving (#13):** "leaving" is never derived from an unmount, only from explicit navigation or
488
+ presence (a dropped reference); a component unmounted by a reload has not left.
489
+
490
+ ## Process (after review round 2)
491
+
492
+ - **Layer by layer, as pi-durable was built:** the existing `next/` code (about 10k lines, kernel steps 1–6) is frozen as
493
+ a reference and not built on; Chord v2 is rebuilt bottom-up in small layers (data and contracts; readable and derive;
494
+ documents and store; objects; references and `ctx`; protocol and links; relay, routes, revocation; routines and
495
+ jobs; registries; reload and rollout; telemetry). Per layer: a one-page spec with data flow, cost per hop, a line
496
+ budget, footguns and what is reused, approved by the owner; a minimal build with counting and GC tests; a cleanup
497
+ loop with fresh reviewers and the owner until he is happy; only then the next layer. Target: 6–7k lines without
498
+ tests. The handoff for continuing after a compaction is `apps/p2/docs/p3-handoff.md`.
499
+
500
+ ## Review round 2: decided items
501
+
502
+ - **R1, the message protocol and relays:** (**superseded in part** by Q4 (one binary message format on every link), R6 (no `describe` kind) and compression at the origin) a message is a header plus opaque parts, one part per hold. A part's payload
503
+ is encoded once by its origin with that hold's own Delta encoder (path interning per hold, end to end between
504
+ provider and consumer), packed with its byte segments, compressed at the origin above a threshold (`codec`). WebSocket:
505
+ `u32 header length | header JSON | part bytes`; IPC: structured clone of `{ header, parts: Uint8Array[] }`; memory:
506
+ the object. Kinds: `hold`, `release`, `call` (1 part: arguments), `cancel` (consumer to provider); `bound` (1 part:
507
+ snapshot), `state` (n parts: one per hold the commit touched on this link), `result` (1 part), `gone`, `describe`
508
+ (provider to consumer). Relays parse and rewrite only headers: map hold and call ids per link, stamp the principal on
509
+ `hold` and `call` (a principal field from a client link is always overwritten; only a trusted parent link may carry
510
+ one), group parts per destination link (each link gets only its own holds' parts); payloads are never parsed. One
511
+ `state` message per commit per link. Encoding happens when sending: while a link has credits a commit is encoded and
512
+ sent at once; under backpressure its ops are collected per hold and encoded into one part when credits return (still
513
+ atomic for the consumer). Flow control counts the parts' sizes from the header. A re-hold sends a fresh snapshot and
514
+ restarts that hold's encoder and decoder. Two holds of the same principal (two tabs) encode the same ops separately
515
+ (O(change) each). Replaces "relays forward bodies opaquely" from "The state path".
516
+
517
+ - **R2, frame size limits (owner, 2026-10-19):** (**superseded in part** by Q2 (limits per part; `maxPayload` only as the hard cap)) configurable per link kind: the compressed (wire) size through the
518
+ WebSocket server's `maxPayload`, the decoded size through zlib's `maxOutputLength` in the one decode function,
519
+ wherever decoding happens (relays do not decode). Defaults for client WebSockets 16 MiB compressed, 64 MiB decoded;
520
+ none on IPC. An oversized message fails only its own call (`too_large`); the link stays up.
521
+ - **R4 and R5, protocol header schema (owner, 2026-10-19):** (**superseded in part** by Q1 (unknown ids are ignored; only a header failing the schema closes the link)) the message header is a TypeBox schema, a tagged union
522
+ over `kind` (`hold`, `release`, `call`, `cancel`, `bound`, `state`, `result`, `gone`); protocol types derive from
523
+ it, and headers from untrusted links are checked with the schema compiled once (trusted links such as IPC are not
524
+ checked, as for arguments). No kind allow-list: every message resolves against the receiving link's own tables; a
525
+ header that fails the schema, or an unknown kind, hold or call id, is a protocol error that closes that link. The
526
+ trace carrier is an optional bounded field (key count, string lengths) of `call` and `hold` in that schema; relays
527
+ read it from the header. Payloads stay opaque.
528
+ - **R7, inputs and arguments (owner, 2026-10-19):** (**superseded in part** by "Params instead of inputs": flat required
529
+ params, no normalization) object inputs are always normalized (defaults, unknown fields
530
+ removed, a canonical identity key), whoever the caller is, so equal inputs give one identity; inputs stay arbitrary
531
+ (nested objects, arrays, records allowed). (**Superseded in part** by Q12: closed schemas, unknown fields rejected.)
532
+ Method arguments are always stripped to the provider's schema and
533
+ validated with it (compiled once), whoever the caller is: trusted principals are not type-checked code (plugins are
534
+ bundled by esbuild without type checks, agents write JavaScript plugins). Bytes are leaves: `instanceof Uint8Array`
535
+ plus an optional length bound, never walked. Results and state are not validated (they come from the provider).
536
+ Replaces "arguments validated only for callers that are not host principals".
537
+ - **R8, migration output (owner, 2026-10-19; `initial()` part **refined** by "Simplification pass after 4a" D3: once per definition):** the result of `migrate` and of `initial()` is validated against the new
538
+ schema once, when produced (per document per version change, on load, off the commit path); a failure refuses the
539
+ access, keeps the old data and fails the object. Commits are still never validated.
540
+ - **R9, no development mode (owner, 2026-10-19):** one behavior everywhere; the host's `dev` option is removed. Values
541
+ are immutable by convention and never frozen (deep freezing was too costly in Delta), and state is never validated
542
+ (schema changes are caught by fingerprints, migration results by R8). The cost-guard counters (walks, copies,
543
+ encodes, validations, materializations) are ordinary telemetry metrics, always on; tests assert them through the
544
+ in-memory adapter. Replaces "values immutable by convention, with a dev freeze".
545
+ - **R10, revocation window (owner, 2026-10-19):** accepted. Work started on behalf of a revoked principal stops when
546
+ the reconciling routine runs; a step started in between still runs. No extra checks before tool calls or approvals;
547
+ there is always some window.
548
+ - **R11, jobs and revocation (owner, 2026-10-19):** "jobs keep their owner alive" stays for the normal end (the last
549
+ reference released: a closed tab does not abort a running job). Revocation takes precedence: revoking a principal ends
550
+ its objects at once, whatever references or jobs remain; their jobs get the abort signal and the normal
551
+ `closeTimeout`. A job runs as its object's principal.
552
+ - **R12, returning data from transactions (owner, 2026-10-19):** option B, as in Durable: drafts are revoked when the
553
+ transaction settles; transactions write and return at most plain values; data is read after the commit from the
554
+ document (`handle.value`, `handle.get`). No `tx.read`.
555
+ - **R13, existence inside transactions (owner, 2026-10-19):** as in Durable, no existence check: `tx.retire` of an
556
+ absent document is a no-op, `tx.draft(…, { tags })` creates a missing document with its tags, existence is read
557
+ outside the transaction. The R12 and R13 patterns are documented in `design.md` (section 9).
558
+ - **R14, checkpoints (owner, 2026-10-19; **refined** by "Slice plan and slice-1 review decisions": required only for persisted documents):** no default, but `checkpointWhen` is required on every document
559
+ definition, in Chord and in Durable (a pi breaking change), so forgetting it is a type error. Durable's task-scoped
560
+ documents (nested call, codemode run and journal) return false (they retire with their task); the codemode store
561
+ checkpoints after 64 deltas; p2's small settings documents checkpoint on every write, its growing ones after 64.
562
+ - **R15, old calls and migration (owner, 2026-10-19):** as in Durable: running calls keep their old handles until they
563
+ settle, except for documents whose version rose; old code touching a document migrated past its version throws, the
564
+ document stays intact, the call fails once.
565
+ - **R16, native restarts (owner, 2026-10-19):** no Chord change: a restart closes the old native, then opens the new.
566
+ Code mode's `close` detaches the old sandbox, returns at once and disposes of it when its scripts finish or after
567
+ the default `closeTimeout` (30 s, no override); its tool entry finds the current sandbox at `execute()`. Documented
568
+ as two patterns for natives: drain in the background, and swap through a registry contribution.
569
+ - **R17, harness start (owner, 2026-10-19):** (**superseded in part** by "The harness is an ordinary object") the harness is not part of the start barrier; it starts after it (the
570
+ barrier waits until every plugin loaded or failed, so the registries hold everything that will be there). No list of
571
+ required contributions and no paused state: a missing tool gives its call an error result; a missing model makes the
572
+ generation report "model unavailable" and wait until a model appears; failed plugins are reported with their error.
573
+ - **Note for p3, moving a session between instances (owner, 2026-10-19):** copying a session's database to an instance
574
+ with other plugins already works as Durable does: missing tools, sections and hooks are skipped; tasks without their
575
+ definition stay blocked (`missing_task`) until installed or aborted; documents of missing plugins stay untouched;
576
+ stored selections are never rewritten. The UI must show blocked tasks with their reason and offer to abort them (a
577
+ turn waiting on such a task hangs until aborted).
578
+ - **R18, dependency recovery (owner, 2026-10-19):** a dependency that fails (or does not become live in time) fails
579
+ the dependent `transient` with the dependency's cause inside, never `broken`. The dependent keeps its references
580
+ while failed; their state is an automatic retry trigger, so the dependent is retried the moment the dependency goes
581
+ live (backoff cancelled; it stays as a safety net). Invisible to factory authors. After `live`, a dependency leaving
582
+ `live` leaves the dependent live; its calls using the dependency get its error or park while it loads.
583
+ - **R19, flow control and large snapshots (owner, 2026-10-19):** (**superseded** by "Flow control: an ack-clocked window") snapshots are always admitted, even when larger
584
+ than the window (they consume it); overflow is decided on changes queued after them; after an overflow a re-hold
585
+ waits until the credits cover the last snapshot's size, with a minimum interval.
586
+ - **R20, incarnation fence (owner, 2026-10-19):** Chord stamps the calling host's incarnation (from the link handshake)
587
+ on calls arriving over a link: `ctx.incarnation` next to `ctx.principal`, not part of the principal or its key; a
588
+ server object ignores reports from an older worker incarnation by comparing it. Nothing is sent as data.
589
+ - **Testing the core (owner, 2026-10-19):** every mechanism decided in review round 2 gets exhaustive, individual tests
590
+ with its edge cases; every layer spec lists them (design section 28).
591
+ - **R21, who contributes to registries (owner, 2026-10-19):** only facets and `lifetime: "host"` objects, enforced by
592
+ the type of the factory's `self` (only theirs has `contribute`). Reading registries stays open to every object.
593
+ - **R23 (owner, 2026-10-19):** `constant(value)`, a readable that never changes, used for start data.
594
+ - **R22 (owner, 2026-10-19):** a host-lifetime object without input gets session-scoped objects with `ctx.get` in a
595
+ routine or method (documented pattern); a `get` with an input computed after activation only if needed.
596
+ - **R24 (owner, 2026-10-19):** the container-width cost of structural sharing is documented (one array slot copy and
597
+ one element visit per element per change); a benchmark of long arrays sets the limit; large collections are keyed
598
+ documents or bounded windows. No new mechanism.
599
+ - **R25 (owner, 2026-10-19):** rejected are registries created at runtime and entries resolved through mutable
600
+ indirection; a contributor may change its contribution at any time (a readable contribution recomposes on change),
601
+ and readers may look up the current value at use.
602
+ - **Changing contributions, `self.writable` (owner, 2026-10-19):** a contribution is fixed for its contributor's life
603
+ or a readable; Chord subscribes to readable contributions and reruns `compose` on every change; use sites follow the
604
+ composed readable or read its current value. `self.writable(initial)` is a host-local readable of any in-process
605
+ value (identity-compared, no JSON, no wire) with `set`, for contributions such as MCP tool lists. Tools, sections,
606
+ hooks, wraps and tasks reach the harness as one Durable `Extension` per plugin; skills and other lists likewise.
607
+ - **Removing a contribution (owner, 2026-10-19):** no remove call; an entry leaves when its contributor ends or a reload
608
+ stops contributing; a live contributor with nothing to offer sets its readable to the registry's empty value (no
609
+ `undefined` entries).
610
+ - **R26, drift (2026-10-19):** fixed in the text (the description frame is gone with R6; §9 no longer says values read
611
+ in a transaction are shared). Keep-alive counters: one readable `host.activity` (`{ references, calls, jobs, lingering }`; owner).
612
+ - **Q1, unknown ids (owner, 2026-10-19; review round 3):** unknown hold and call ids are ignored on both sides (races
613
+ such as `result` crossing `cancel` or `call` crossing `gone`); a consumer receiving `gone(h)` rejects its own pending
614
+ calls on `h`. Only a header that fails the schema closes the link. Replaces "an unknown hold or call id closes the
615
+ link" (R4).
616
+ - **Q2, size limits per part (owner, 2026-10-19):** limits per part from the header sizes, before decoding; an oversized
617
+ part fails only its hold or call, other parts apply; an oversized snapshot fails its hold without automatic re-hold;
618
+ the admitting link's decoded limit is stamped on relayed work and enforced where it is decoded; `maxPayload` is only
619
+ the transport's hard cap (closes the connection). Replaces R2's per-message wording.
620
+ - **Q3, re-hold after overflow (owner, 2026-10-19):** (**superseded** by "Flow control: an ack-clocked window") re-hold once the previous snapshot's debt has drained (credits at
621
+ the full window), with the minimum interval; replaces "once the credits cover the last snapshot's size" (R19),
622
+ which stalls for snapshots larger than the window.
623
+ - **Q4, one message format and window (owner, 2026-10-19):** (**superseded in part** by "Wire protocol" (codecs, not the JSON header framing) and "Flow control: an ack-clocked window" (no byte window)) every link carries the same binary message
624
+ (`u32 header length | header JSON | part bytes`, one `Uint8Array`): WebSocket as a binary frame, IPC through
625
+ `serialization: "advanced"`, memory delivered asynchronously. Encoding, framing, checks, ids, flow control and limits
626
+ are one shared protocol layer; adapters only move bytes; one byte window everywhere from `byteLength`. Replaces the
627
+ IPC frame-count window and R1's per-adapter formats ("IPC: structured clone of { header, parts }; memory: the
628
+ object"). Tests on memory links run the production path.
629
+ - **Q5, dependency cycles (owner, 2026-10-19):** rejected before activation waits: on one host by following the
630
+ waiting edges of activating objects; across hosts through the activation chain of uuidv7 instance ids carried in the
631
+ header of holds made during an activation (bounded; ignored from untrusted links). A cycle fails the activation
632
+ `broken` naming it; no references are kept; checked again after reloads.
633
+ - **Q6, Chord from Durable tools (owner, 2026-10-19):** (**superseded in part** by "Context last": `chord(fn, context)`) p3's core puts the host into the harness's base context under
634
+ a typed key; `chord(context, fn)` opens a call scope (host principal, the call's abort and span, references released
635
+ when `fn` settles) in tools, hooks and sections; no wrapping, no p3 `defineTool`. Needs good docs: the pattern,
636
+ lifetime and principal, and the Questions service as the example (tools reach users only through owner state;
637
+ clients are not addressable). Replaces "hooks open a call scope with `host.root`" (plugins never hold the host).
638
+ - **Q7, code mode on reload (owner, 2026-10-19):** R16 stands: the detached old sandbox is disposed when its scripts
639
+ finish or after 30 s at most; a reload during a longer script aborts it and that turn fails. After a reload at most
640
+ 30 s of old code keeps running.
641
+ - **Q8, registry ownership and versions (owner, 2026-10-19):** one authoritative declaration per registry, from the
642
+ defining facet (`compose`, version, `accepts`); other tokens only identify it; a reload replaces it atomically.
643
+ `accepts` stays (third-party plugins are an explicit goal of p3), owned by the defining facet, pure, following the
644
+ owner-adapter pattern of documents, object types and jobs; a contribution without a matching version or adapter is
645
+ left out and reported, not a load error; contributions are not schema-checked. Every use site of a contribution
646
+ catches throws and rejections, reports naming the contributor, and continues. Replaces "newer ones are refused at
647
+ load".
648
+ - **Q9, job dedupe and retention (owner, 2026-10-19):** dedupe covers unsettled runs only; after `finish` commits,
649
+ Chord drops the run's record and key (a persisted record in the same commit) and releases waiters; dedupe after
650
+ completion is the owner's, through the result in its own document. Chord keeps nothing beyond a run's lifetime.
651
+ - **Q10, contract evolution (owner, 2026-10-19/20):** forward evolution in code, no build step: a base written in
652
+ full with an explicit version (`remote()`/`local()`; `local` keeps its own kind with flat `members`, C1 rejected),
653
+ `evolve(prev, e => …)` always continues the history with structured edits (`state`, `addState`, `removeState`,
654
+ `args`, `result`, `method(name, args, result)`, `addMethod`, `removeMethod`); every version is materialized and
655
+ fingerprinted at declaration. Input or variant-set changes start a new history (a new declaration with an explicit
656
+ version; no older accepts). Mappings only with the provider, in the pure `checkAccepts(contract, accepts)` (no
657
+ `serve` in this layer): required at type level and at runtime for every member of the old version that changed or
658
+ was removed; removed state from the whole current variant state or `"refuse"`. Fingerprint: `hash64` over a
659
+ schema-aware data-only normal form (`NORMAL_FORM_VERSION`; union and intersection order kept; `required`/`enum`
660
+ sorted; TypeBox keyword whitelist; bytes as a `{ type: "bytes" }` data marker); history private, deep-copied and
661
+ frozen (**superseded in part** by "L1 rewrite review": nothing is frozen). Schema helpers `str num int bool nul lit oneOf union obj arr map opt bytes unknown` + `type Static`.
662
+ Implemented and reviewed (Fable, Astra: thumbs up) in `packages/chord/src/contracts/` (`7d2bb8ea8`).
663
+ - **Q11, absent singletons (owner, 2026-10-21):** a singleton always has a value: `handle.value` is the written value
664
+ or `initial()`, never `undefined` (an exposed singleton is typed `T`); `undefined` only for absent keyed documents
665
+ (`handle.get(key)`); singleton existence has no API. A deliberate difference from Durable's `snapshot()`.
666
+ - **Facets typed by the app's principal (owner, 2026-10-21):** base code exports `defineFacet` bound to its `P`
667
+ (`bindFacet<P>()`, working name) next to the principal union; every facet imports it from there; `host.load`
668
+ accepts only facets typed for the host's `P`. No runtime check.
669
+ - **`placement`, not `host` (owner, 2026-10-21):** (**superseded in part** by "Params instead of inputs") the contract field naming where an object lives is `placement`
670
+ (a typed function of the input); `input` stays (the object's key among objects of its type, part of its identity).
671
+ - **Q-B2, in-memory documents across input changes (owner, 2026-10-21):** not carried over. An input change of a
672
+ type ends its live objects on reload; their in-memory documents, flows and jobs are lost; documented ("in-memory
673
+ documents survive a reload only while the object identity is unchanged; persist what must survive").
674
+ - **The store (owner, 2026-10-21; backends **superseded in part** by W14: no IndexedDB; direction accepted, details under review in `p3-reviews/store-proposal.md`):**
675
+ P1 recorded: a session is self-contained (its store plus its directory); export from the session alone, import
676
+ into another instance, sync to Radius and continue elsewhere; the server only mirrors (catalog) and keeps instance
677
+ data. One transactional table store per session, layered backend (SQLite, memory, IndexedDB) / store (tables, a
678
+ small query DSL, controlled writes, commit queue, seq, outbox, export/replay) / adapters: Durable's `Storage` as a
679
+ new `StoreStorage` adapter (Durable's existing storages stay unchanged), Chord's persisted documents, and plugin
680
+ tables (`self.table`), each table owned by one object like a document. Writes are only `put`/`delete`
681
+ (`deleteWhere` resolved to keys), recorded as row operations `{ table, key, v, row }` in the same backend
682
+ transaction as the rows, so a replica or an import needs no plugin code; indexes are local (**superseded** by "Index
683
+ definitions are recorded"); migrations run only
684
+ where the owning code runs and ship as ordinary puts. Lines stay per Chord object and per Durable Session; the
685
+ store has one short commit queue per store that assigns the seq; Durable's seq is the store's seq (Durable sees
686
+ gaps; it only compares seqs). Rejected on the way: derived-only plugin data, opaque plugin directories, export
687
+ hooks, plugin record logs, per-store outboxes with a global position counter, a SQLite-changeset or SQL-statement
688
+ store, raw SQL reads, an IndexedDB-only API, Chord documents inside Durable's `Storage`. Open: physical layout
689
+ (generic rows or real tables), DSL scope, migration timing, `next_id`, backend details, the ordering review
690
+ ("seq 40 after seq 50").
691
+ - **S1, adoption inside the commit step (owner, 2026-10-21; store review):** every owner's in-memory adoption runs
692
+ synchronously inside the store's commit step, after the backend commit and before the next commit starts, so memory
693
+ always shows a complete prefix of the log. Durable's `Storage.commit(writes, adopt, context)` gains a required `adopt`
694
+ callback (pi breaking change): every storage calls it synchronously once the commit is durable and before its next
695
+ commit; the Session catches its own adoption error and fails; a rejection after adoption fails the Session without
696
+ `tx.discard()`; `writeAtClose` passes a no-op; `StoreStorage` forwards `adopt` into the store's step; Chord objects
697
+ adopt (pointer swap) the same way. Listeners stay where they are: after adoption, in each owner's continuation
698
+ (unchanged), so their order across owners may differ from the seq order while every listener reads a complete
699
+ prefix. Checked against Durable's code (memory, JSONL, SQLite, GuardedStorage, writeAtClose); the pi change and its
700
+ test run come with the store work.
701
+ - **S2, references to later commits (owner, 2026-10-21; store review):** no mechanism and no new rule. Ids are minted
702
+ inside the transaction that creates them (Durable `tx.appendEntry` → `mintId`) and reach other code only after that
703
+ commit, so a reference to a later commit only arises when code leaks data out of an uncommitted transaction, which
704
+ is already wrong today (the transaction may still fail). The guide states it once: ids and data leave a transaction
705
+ only after it commits; reference what is committed.
706
+ - **S3, commit completion (owner, 2026-10-21; store review; IndexedDB parts **superseded** by W14):** the commit step finishes only on the backend's terminal
707
+ success (SQLite: `COMMIT` returned; IndexedDB: the transaction's `complete` event); only then adoption (S1) and the
708
+ next commit run. The memory backend stages a commit and swaps it in at the end. An ambiguous outcome (an error during
709
+ `COMMIT`) stops the store (`storage_failed`, D10), as Durable's `GuardedStorage` does. After a restart a local head
710
+ below the uploaded head is normal: the driver pulls its own uploaded batches back before writing. SQLite keeps
711
+ `synchronous=NORMAL` (no extra cost: a crash loses nothing, a power loss the last commits, recovered from Radius);
712
+ IndexedDB uses `durability: "strict"` where supported. Condition (owner): SQLite must not get slower.
713
+ - **S4, consistent reads (owner, 2026-10-21; store review):** an export pauses commits (holds the commit queue) while
714
+ it reads, so the archive is the state at one head; streaming from a snapshot comes only if exports get large. Reads
715
+ that need several queries (a document from its base and revisions) run in one backend read transaction, as
716
+ Durable's SQLite storage does. Ordinary reads in object code (separate awaits) are not snapshot reads; the guide
717
+ says so; no global snapshot guarantee.
718
+ - **S5, replica apply (owner, 2026-10-21; store review; a sync-layer rule):** a replica applies a whole batch in one
719
+ backend transaction together with its applied head. Uploads may run in parallel (owner); the driver resubmits a
720
+ batch until Radius acknowledges it and trims a batch from its outbox only after its acknowledgement. Every batch
721
+ carries `from` and `to`; a replica applies a batch only when its `from` equals the replica's applied head, so it
722
+ never applies past a gap: a later batch waits until the missing one has arrived.
723
+ - **S6, compaction (owner, 2026-10-21; store review):** (**superseded** by W9: no compaction at first) covered by S5: a compacted batch is applied as a whole, so
724
+ its intermediate states are never visible; takeover happens at batch boundaries. Nothing to add.
725
+ - **S7, losing the driver role (owner, 2026-10-21; store review):** no rollback (tools ran, files changed, natives and
726
+ derived data followed the tail). When an upload is refused for an old epoch, the local copy becomes a separate
727
+ session with a new id and keeps running; commits in flight land in it; the old id's sync stops there; the new session
728
+ may sync on its own from a full snapshot; the two never sync again. The catalog shows both (the split-off copy marked
729
+ with its origin and seq). No `not_driver` error. To work out with sync: changing the id (store metadata, session
730
+ directory, catalog), links from other sessions keep the original id.
731
+ - **S9, S12, S14, S17 (owner, 2026-10-21; store review):** S9: the store persists its head with every commit; the
732
+ prototype's `Sequencer` and safe cut go away (prototype bug, nothing to decide). S12: the query DSL starts without
733
+ `join`, `groupBy` and `select`; plugins aggregate in JS over indexed scans; added when a real case needs them. S14:
734
+ tables and documents of an object are written in one transaction API, `self.tx(fn, ctx)` (no separate
735
+ `table.transaction`). S17: bulk operations are designed now, together with migrations, after the remaining S items
736
+ (full-row puts make a 10k-row rewrite about 40 MB in the outbox; revision pruning is one record per row).
737
+ - **S8, work that finishes out of order (owner, 2026-10-21; store review):** an app concern, no store or job mechanism.
738
+ §14 states that the handler calls of one routine never overlap; the guide shows two patterns: chain through the
739
+ routine (await the run's outcome, skip values that are no longer current) or drop stale results in `finish` by
740
+ comparing the source position inside the destination's transaction.
741
+ - **S10, ids (owner, 2026-10-21; store review):** no persisted reservations. Durable's `mintId` contract is narrowed to
742
+ "greater than every id minted since this storage was opened, and every id stored, also across reopen" (an id from a
743
+ transaction that never committed was never seen outside it, S2; Durable's transactions are serialized on the
744
+ Session line). In the store each owner's id counter starts at open above that owner's highest stored id (one index
745
+ lookup per table; a replica taking over gets it the same way). Durable's existing storages already behave so; the
746
+ comment changes in the same pi change as S1.
747
+ - **S11, copying documents into a fork (owner, 2026-10-21; store review):** `StoreStorage` reconstructs the source
748
+ documents of a `document.copy` before entering the commit step (reads of committed history outside the step) and
749
+ enters it with plain creates carrying the base; Durable is unchanged. Safe because the source is a past state and
750
+ only Durable's Session line writes Durable's tables. The outbox only ever carries the resulting rows. Forks of forks
751
+ and selecting an entry in the parent work as today (rewindable documents keep their history).
752
+ - **S13, one documents engine (owner, 2026-10-21; store review):** the store contains one documents engine used by
753
+ Durable's `StoreStorage` and directly by Chord (Chord keeps no Durable dependency). Address `(namespace, owner, kind,
754
+ key?)` with an opaque `owner` string chosen by the adapter (Durable: `session`, `conversation:7`, `task:31`; Chord:
755
+ host/object type); records per incarnation with `history: "latest" | "full"`, `createdAt`, `retiredAt`, opaque
756
+ `meta`; revisions (base with version and optional fingerprint, deltas). Reads outside the step: `find(address, at,
757
+ context)`, `load(id, at, context)` (reconstructs base plus deltas once; no cache), `list(query, limit, cursor,
758
+ context)` ordered by id or key. Writes inside the step: `create(record, base)`, `change(id, content)`, `retire(id)`,
759
+ with the engine's checks (one live per address, create before change, delta on a base of the same version, nothing
760
+ after retire); `latest` prunes revisions on a new base and on retire. Callers decide base or delta
761
+ (`checkpointWhen`) and keep their in-memory trackers as today (Durable's Session caches, pins and evicts unchanged).
762
+ Fork copies stay in `StoreStorage` (S11). Chord's frozen storage layer and its conformance suite are not ported.
763
+ - **S15, Durable's existing storages (owner, 2026-10-21; store review):** kept, as decided; no plan to remove them.
764
+ Revisit whenever it becomes worth it.
765
+ - **S16, layout and text order (owner, 2026-10-21; store review, Astra's design; **superseded in part** by W8 (every text
766
+ column), W14 (no IndexedDB) and "Text is plain UTF-8, in code point order" (the whole text order and encoding)):** real tables per logical table
767
+ (definitions and changes recorded in the outbox, the store renders DDL); keys and indexed columns of type text or
768
+ integer, the rest of a row in a JSON column (**superseded in part** by "Flat columns"). Text order is JavaScript's (UTF-16 code units) on every backend: memory
769
+ and IndexedDB use plain strings natively; SQLite stores key and indexed text columns as `BLOB`s of UTF-16 code
770
+ units, high byte first (`Buffer.from(s, "utf16le").swap16()`), so its bytewise comparison equals JS order; text that
771
+ is not indexed stays `TEXT`. Lossless (lone surrogates, NUL round-trip), no comparator, no normalization ("é" and
772
+ "e\u0301" are two keys). The SQLite backend encodes writes and query bounds and decodes results; outbox records,
773
+ exports and cursors keep logical strings. Prefix ranges come from one store helper (increment the last code unit
774
+ below `FFFF`). Every index ends with the primary key; cursors are opaque `{ dir, last }` with logical values and are
775
+ refused by a scan in the other direction; a row whose indexed field is missing or null is not in that index. Plugin
776
+ rule: text compares exactly like JavaScript strings; page with the store's cursors. Verified on Node 24.4: lossless
777
+ round trip, SQLite BLOB order equals JS sort, ~0.4 µs per encode plus decode. Cost: indexed text shows as hex in
778
+ `sqlite3` and takes two bytes per code unit. Rejected: generic JSON rows, store-maintained index entries, code point
779
+ order with `toWellFormed()` (Fable), WTF-8, text equality-only.
780
+ - **S17, bulk operations and migrations (owner, 2026-10-21; `p3-reviews/store-bulk-migrations.md`; JSON-part wording
781
+ **superseded in part** by "Flat columns"):** writes are `put`
782
+ (a whole row), `update(table, key, set)` (some columns of one row), `delete`, `updateWhere` and `deleteWhere` (many
783
+ rows, one record). Bulk operations are recorded as operations with the driver's affected-row `count`; every replica
784
+ runs the same operation (one SQL statement, or a loop in the memory backend) without plugin code; a different count
785
+ stops applying that store and restores it from a snapshot. `set` expressions touch real columns only: `value`,
786
+ `col`, `add`, `sub`, `max`, `min`, `concat`, `replacePrefix`, `coalesce`, `when`; no JSON-field operands (added
787
+ when a real case needs them); no subqueries, joins, aggregates or insert-from-query (read first, then `inArray` or
788
+ puts); no case folding, `substr` on unindexed text or floats; one conformance suite for both backends. Migrations:
789
+ shape changes of the JSON part migrate on access (each row stores its version `v`; `migrate(row, from)` on read; the
790
+ next write stores the new shape; old code refuses newer rows); changes to key or indexed columns run eagerly when
791
+ the owning object starts on the driver, before it goes live: recorded table changes (`alter` add/rename/drop, a
792
+ rebuild for key or type changes), then either an `update` step (one `updateWhere`) or a `rows` step (scan in key
793
+ order, `tx.update` per row, chunks of N rows per commit; table version `migrating` → `ready`; a crash resumes the
794
+ scan). Indexes are local and created after `ready` (**superseded in part** by "Index definitions are recorded": the
795
+ index changes after `ready` are recorded).
796
+ - **S18, group commit (owner, 2026-11-04):** the commits queued behind a running commit step form one group, which the
797
+ next turn of the commit queue admits at its entry and writes in one backend transaction: each commit keeps its own
798
+ step, seq and outbox commit, the metadata is written once, and after `COMMIT` memory follows commit by commit, each
799
+ adopted when memory stands at it, in queue order, before the next turn (S1 as for lone steps). Fewer transactions are
800
+ always good, and a remote SQLite backend would pay a round trip per statement. A series, an export or a batch apply
801
+ queued after an open group ends it (queue order stays); an empty commit never joins one. A refusal in a group (one
802
+ commit's own writes throw, D4) rolls the group back whole and its admitted commits run again one by one, not admitted
803
+ again, so only the refusing commit is refused; their live queries run again too (`chord.query.runs` counts both
804
+ attempts, the statements only the stored one). The ids the rolled-back attempt took stay unused (D3: the allocator
805
+ is shared with Durable's `mintId`, never rewound; so within a group's worth of ids of `Number.MAX_SAFE_INTEGER` a
806
+ refused group can exhaust the id space a little earlier, accepted). A group that would pass the end of the seq range
807
+ runs its commits one by one, so the ones that fit are stored. A backend failure while the store closes (a commit's,
808
+ a read's, a maintenance write's) still refuses the admitted commits after it. No savepoints (two statements per
809
+ commit, and Durable Objects refuse them) and no check/write split of Chord's writes (their counted conditional writes
810
+ are the checks).
811
+ - **Blobs and derived data (owner, 2026-10-21):** blobs (artifact versions, uploads, sent files) are app layer: files in
812
+ the session directory named by the hash of their content, never changed, written (tmp, fsync, rename) before the row
813
+ or document that names them; any process may write one. Export copies `blobs/` with the store snapshot; import
814
+ copies them back and checks each hash. Sync: before uploading a batch, the sync layer uploads every blob file present
815
+ at that moment that has not been uploaded yet (no reference tracking; a replica never sees a hash without its file).
816
+ No blob is deleted before its session is deleted (an unreferenced blob only costs disk). Derived data (full-text
817
+ indexes, caches, a plugin's search database) is never exported or synced and is rebuilt where needed, typically by a
818
+ catch-up routine comparing it with the store.
819
+ - **Index definitions are recorded (owner, 2026-10-21):** index contents are never recorded (every backend maintains
820
+ them from the rows), but their definitions are: when the owning object starts on the driver, the store compares the
821
+ declared indexes with the existing ones and creates or drops the difference, recorded as `{ op: "index", table, name,
822
+ columns }` and `{ op: "dropIndex", table, name }` (after a migration's `ready`); every replica applies them. Every
823
+ copy of a store, an export and the file seen by outside tools is then identical and self-describing. Indexes never
824
+ affect results (queries order by index columns plus the primary key; rows without the field are excluded by rule).
825
+ Cost: replicas without the plugin maintain indexes they never use; adding an index builds it during apply.
826
+ - **P3, where persisted data lives (owner, 2026-10-21):** persisted documents and tables only in input-less `local`
827
+ objects (one per host and type); their address is (host, object type, name, key), with no input, so an input change
828
+ can never orphan persisted data. Per-entity data (per conversation, per session) is keyed documents or table rows in
829
+ such a store with an app-chosen key (data, not identity); objects with an input keep only in-memory state and read
830
+ persisted data from the input-less store by key. Not adopted (proposals of 2026-10-21): `previous`/`rehome`, the
831
+ split question, identity hashes as storage addresses. A persisted store's type and definition names stay stable;
832
+ renaming one, or changing how keys are formed, is an explicit one-off migration in the store. p2 fits: its four
833
+ stores with an input persist no Chord state. Enforced at the type level (`self.state(…, persist)` and `self.table`
834
+ only in input-less local factories) and as a declaration error at runtime.
835
+ - **Flat columns (owner, 2026-10-21):** `defineTable` takes `columns`, a flat record: each column is a scalar (`str`,
836
+ `int`, `bool`, `num`) or `json(...)`; no nested `obj`/`arr` outside `json(...)` (refused by the types and at
837
+ declaration). Every column is a real SQLite column; scalars can be filtered and ordered; keys and indexed columns are
838
+ `str` or `int` (S16); a `json(...)` column is one JSON `TEXT` column, opaque to queries, typed when read. Supersedes in
839
+ part S16 ("the rest of a row in a JSON column") and S17 ("shape changes of the JSON part"): adding, renaming or
840
+ dropping a column is a recorded `alter`; shape changes inside `json(...)` columns migrate on access.
841
+ - **P2, in-memory documents are instance state (owner, 2026-10-21):** an in-memory document is attached to the live
842
+ object: no storage, no address, no seq, never in the outbox. Same machinery as persisted documents (drafts, `self.tx`,
843
+ atomic commits, ops for subscribers and the wire); its commit is prepare and adopt. A transaction mixing both kinds
844
+ adopts the in-memory part inside the store's commit step (S1). It lives from activation to the end of the object:
845
+ survives plugin reloads (pure objects swapped in place) and native restarts of the live object, ends with the object
846
+ (last reference, linger, an input change on reload, Q-B2). Deletes the memory `DocumentStorage` for in-memory
847
+ documents and identity hashes as their keys.
848
+ - **Query runner (owner, 2026-10-21):** a query is immutable data built with `query(Table).where(…).orderBy(…)`;
849
+ `limit` is an argument, not part of the query; the same `where` serves `updateWhere`/`deleteWhere`. The table handle
850
+ from `self.table(Def)` runs it: `page(q, limit, ctx, { after })` returns `{ rows, next? }` (an opaque cursor with
851
+ direction and last key as logical values, bound to the query's order, refused with another query), `rows(q, ctx)`
852
+ iterates all pages, `get(key, ctx)`, `count(q, ctx)`. Outside a transaction it reads committed state, one query at a
853
+ time (S4). Reads inside `self.tx` (owner, 2026-10-21): `tx.page(table, q, limit, { after })` and `tx.rows(table, q)`
854
+ return committed rows, stable for the whole transaction (only the owning object writes its tables, P3, and its
855
+ transactions run one at a time on its line), without the transaction's own writes; `tx.get(table, key)` overlays
856
+ the transaction's own pending writes (put, update, delete) on the committed row (**superseded in part** by W10: after
857
+ a bulk operation on the table, `tx.get` throws `read_after_bulk`).
858
+ Documented rule: queries inside a transaction see the state before it; own writes become visible after the commit.
859
+ - **P4, plugins' own databases and files (owner, 2026-10-21):** state written by code lives in a store: the session's
860
+ store for session data, the server's store for instance data (p2's `catalog.sqlite` becomes a rebuildable mirror table,
861
+ `shares.sqlite` server tables, `subagents.sqlite` part of the session's metadata, the code-written JSON files such as
862
+ `models-store.json`, `forwards.json`, `owner-revoked.json`, `radius.json`, `device.json`, `prefs.json`, `envs.json`,
863
+ `voice.json` server documents or tables); a plugin's own SQLite file holds only derived data (`search.sqlite`); blobs
864
+ are files named by hash; files a person edits by hand (`settings.json`, `models.json`, `auth.json`) stay files
865
+ (open point #21). Logs are not data.
866
+ - **Params instead of inputs (owner, 2026-10-21):** an object type's `input` is renamed `params`: the fields that select
867
+ an instance among objects of its type (like route params), part of its identity. Params are a flat record of
868
+ required fields, each `str()`, `int()`, `bool()` or a union of string literals (`oneOf("chat", "agent")`); no
869
+ defaults, no optional, nested or unknown fields (a `get` with a missing or extra field fails `invalid/params`); the
870
+ identity key is the field values in declared order (no canonical JSON walk, no normalization). p2's 78 object types
871
+ fit (52 empty, the rest one or two required scalars). Supersedes in part R7 ("inputs stay arbitrary", "inputs are
872
+ always normalized") and the placement entry ("`input` stays"); Q12 (stripping) now concerns method arguments only.
873
+ "Input-less" in P3 and Q-A reads "with empty params".
874
+ - **Q-A, lifetime from kind and params (owner, 2026-10-21; both reviewers up):** no `lifetime` option and no lazy flag.
875
+ A `local` type with empty params lives as long as the host (started eagerly at host start, never ended by reference
876
+ count; it may hold persisted data, P3, and contribute, R21); every other `local` type and every `remote` type lives
877
+ while referenced (`linger` only for those). No remote object has host lifetime (its identity includes the
878
+ principal; long-lived parts go into a local shared store). A lazy flag was rejected for now: a lazy store would
879
+ contribute and run routines only after first access and escape the start barrier; p2's 11 such stores open only
880
+ cheap natives. Every "`lifetime: \"host\"` object" in earlier entries reads "`local` object with empty params".
881
+ - **The harness is an ordinary object (owner, 2026-10-21; Astra's C17):** no harness exception in the start barrier.
882
+ The harness is a host-lifetime object (`local`, empty params) that opens Durable during the barrier with scheduling
883
+ paused (Durable opens paused until `resume()`); base code calls `harness.resume()` after `await host.load(...)` and
884
+ before the worker connects to its parent (**superseded in part** by W6: connect first), so no request arrives earlier and every registry is filled. Chord knows
885
+ nothing about Durable's start.
886
+ - **Q12, closed argument schemas (owner, 2026-10-21; review round 3):** every object schema in a contract is closed
887
+ (`obj()` sets `additionalProperties: false`); an unknown argument field fails the call with `invalid/args` instead of
888
+ being stripped; the provider validates the caller's object in place, never copies or mutates it; arguments have no
889
+ defaults (optional fields are `opt()`, handlers write `args.limit ?? 50`). Supersedes in part R7 ("arguments are
890
+ always stripped") and the earlier "undeclared argument fields are stripped" (step 2); `checkArgs` is one compiled
891
+ validation.
892
+ - **Q15, `local` arguments are validated (owner, 2026-10-21; review round 3):** one rule for every method call: the
893
+ provider validates arguments against its closed schema (Q12) for `local` types too (callers are often other plugins,
894
+ bundled without type checks; `local` stores hold persisted data, P3). Never copied. Supersedes the `local` column
895
+ "neither validated nor copied" (design §5) and the `local` part of "Object kinds".
896
+ - **Q13, origin incarnation on relayed calls (owner, 2026-10-21; review round 3):** relays stamp the origin host's
897
+ incarnation into the `call`/`hold` header, taken from the link the call came in on and overwriting any value a client
898
+ sent (like the principal), so `ctx.incarnation` always names the host where the call started. One header field.
899
+ Refines R20.
900
+ - **Q14, exposable state (owner, 2026-10-21; review round 3):** a state member of a contract may be backed only by a
901
+ JSON readable: a document handle (persisted or in memory), a derive, `constant(...)` or a state readable
902
+ (`replicatedState`, e.g. Durable's views). `self.writable` (host-local, any in-process value, for registry
903
+ contributions) is refused as a state member: by type, and when the object is served (a kind tag check, no value
904
+ walk; the object fails `broken` naming the member). Values are still not validated (R9).
905
+ - **`reset` instead of release and re-hold (owner, 2026-10-21):** (**superseded in part** by "Flow control: an ack-clocked window": consumers never fall behind a window; only a relay sends `reset { hold }` upstream, for a downstream that fell behind; no debt or minimum interval) when a hold falls behind (the link's window
906
+ overflows), the consumer or relay sends `reset(hold)` on the existing hold, never a release: the reference count is
907
+ unchanged, so a slow connection can never end an object. The provider answers with `bound` (a fresh snapshot) and
908
+ restarts that hold's encoder; the consumer drops everything for the hold that arrived before the new `bound`
909
+ (per-direction order). Q3's rules stay (one snapshot in flight per hold, after the previous debt drained, minimum
910
+ interval). A link drop remains a departure. Supersedes "releases the hold and re-holds" (R19, design §3).
911
+ - **Routines get the changes since their last run (owner, 2026-10-21):** (**superseded** by "Watchable and one watch API": per-commit frames) `self.routines.on(readable, (value, changes,
912
+ ctx) => …)` runs one call at a time with the latest value; `changes` is `{ kind: "ops", ops }` (every op since the
913
+ previous run, in order, the commits' op arrays kept by reference, never re-diffed) or `{ kind: "reset" }` (the first
914
+ run, or more than N ops piled up). Same rule as Durable's watch overflow and the link's `reset`, applied per run;
915
+ routines that only want the value ignore `changes`.
916
+ - **Watchable and one watch API (owner, 2026-10-21; **superseded in part** by W1 (flush after the step), W2b (routines
917
+ start with the current value), W4 (two kinds) and "One pending queue for slow consumers of state" (the 100-frame
918
+ overflow)):** v2 has no `replicatedState`. Everything observable is a
919
+ `Watchable<T>` = `{ value; watch(ctx): Promise<WatchHandle<T>> }` with `T` limited to JSON plus bytes at the type
920
+ level: document handles (singletons; keyed per key), derives, reference members, `constant(v)`, and Durable's
921
+ conversation view, task graph and documents. Durable's `WatchHandle` and `WatchEnd` move into Chord as the one
922
+ observation API and Durable imports them: `watch()` atomically takes the current value and buffers later commits;
923
+ `start(listener)` installs the one listener; each commit is delivered as `(value, ops, ctx)`, in order, async
924
+ listeners awaited; after 100 pending frames the backlog becomes one root replacement `[["r", newest]]`; a retired
925
+ document ends with `[["r", null]]`; `stop()` is idempotent, drops pending frames, never aborts or awaits a running
926
+ call, and returns the end; `closed` resolves to the same. `WatchEnd` reasons are generic: `stopped`, `cancelled`,
927
+ `retired`, `ended` (source closed: object ended, reference released, Session closed), `listener_error { error }`,
928
+ `failed { error }` (source failed); Durable adopts them (was `session_closed`, `session_failed`). `routines.on` is a
929
+ watch owned by the object (per-commit frames; Chord awaits a running call on reload), replacing "Routines get the
930
+ changes since their last run". Durable's state getters (`documentState`, `viewState`, `taskGraph()`) are removed
931
+ (unused by p2). Derives and the wire encoder use an internal synchronous hook inside the commit step so a derive's
932
+ change travels in the same wire batch as its source (no torn state); this needs exhaustive tests. Memory: each watch
933
+ holds at most 100 frames by reference; every watch ends (object end, reload, ctx, source end); GC tests count
934
+ retained revisions.
935
+ - **W4, two watchable kinds (owner, 2026-10-21; both reviewers agree; review round 4; JSON-plus-bytes **superseded in part** by "L2 spec decisions": watchables are JSON only):** Delta's `Op` is typed over
936
+ JSON, so natives never ride in ops. `Watchable<T extends Json>` (JSON plus bytes): `value` + `watch(ctx)` →
937
+ `WatchHandle<T>`, listener `(value, ops, ctx)`, overflow → one `[["r", newest]]` frame; documents, derives, reference
938
+ members, `constant`, Durable's views; the only kind that can be a contract state member or go on the wire.
939
+ `NativeWatchable<T>` (any in-process value): `value` + `watch(ctx)` → `NativeWatchHandle<T>`, listener `(value,
940
+ ctx)`, a change is a new value by identity (`!==`; mutating an existing native value notifies nobody), overflow
941
+ collapses to the newest value, never ops; `self.writable(initial)` (with `set`) and composed registry values. Both
942
+ share one lifecycle implementation (`watch`, `start`, `stop`, `closed`, `WatchEnd`) and carry explicit runtime kind
943
+ tags; a `NativeWatchable` is rejected as contract state even when its current value happens to be JSON. Derives may
944
+ read either kind (mixed sources) and always return JSON plus bytes: a native change marks the derive dirty without
945
+ ops; the derive recomputes and its result is diffed as usual. Native changes use the same synchronous dirty-marking
946
+ hook (W1) with the same reentrancy flag; `writable.set` never goes through the commit queue and has no seq;
947
+ registries recompose in the owner's flush, never inside another owner's commit step. Supersedes the JSON limit in
948
+ "Watchable and one watch API" and the earlier `Writable` interface.
949
+ - **W1, adopt and mark in the step, one synchronous flush after it (owner, 2026-10-21; review round 4):** the commit
950
+ step writes, adopts (pointer swap of every changed value: the owner's documents; Durable's views and documents
951
+ touched by the commit, dispatched by conversation/document id) and marks (dependent derives dirty; ops appended by
952
+ reference to each touched hold's batch and each watch's queue), O(1) per dependent. Right after the step, before any
953
+ other code runs and without any await, the owner's flush recomputes the dirty derives that someone holds or watches,
954
+ in dependency order (derives are synchronous pure functions), encodes one part per touched hold (a hold covers all
955
+ members of the held object's variant) into one state message per link, and queues watch callbacks. A derive that
956
+ throws fails only its member (`broken`); the commit and its sources' parts still go out. A dirty derive nobody
957
+ flushes recomputes lazily on its next read. During adopt and flush, `self.tx`, `watch` and `release` throw
958
+ `reentrant`; a derive cycle throws once. The same adopt/mark/flush boundary runs without a store seq for in-memory
959
+ commits (P2), `writable.set`, registry changes and inbound wire batches. Durable calls the adopt synchronously
960
+ inside its commit step (no microtask). Async work belongs in routines writing in-memory documents. Supersedes
961
+ "derives and the wire encoder run inside the commit step" ("Watchable and one watch API"). Exhaustive tests: a
962
+ throwing derive, a chain A→B→C where B throws, two owners alternating with a derive over both, reentrancy, a watch
963
+ started after 150 frames, stop during a running call, a retire with a derive over it, a reload during a routine call,
964
+ GC counts of retained revisions.
965
+ - **W2, W3, watch fixes (owner, 2026-10-21; review round 4):** W2a: retirement is an explicit source event (last frame
966
+ `[["r", null]]`, then the end `retired`), never inferred from `value === null`; in Chord and in Durable alike (a pi
967
+ change; Durable's `CommittedWatch` stops treating `null` as retirement). W2b: only `routines.on` delivers the
968
+ acquisition value: it calls its handler once with `watch.value` (ops `[["r", value]]`), then starts the watch and
969
+ drains the buffered commits; a direct `watch()`/`start()` user keeps Durable's pattern (use `watch.value` first, then
970
+ `start`), unchanged. W3: termination clears every reference a watch holds (listener, detach/report/replace closures,
971
+ cancellation hooks); a running call keeps only what it needs; GC tests cover stopped but still referenced handles.
972
+ - **W6, worker start order (owner, 2026-10-21; review round 4):** connect to the parent first, then `load`, then
973
+ `harness.resume()`. A link carries nothing until a hold or call; a host-lifetime store that references a server type
974
+ in its factory no longer waits 60 s for a missing link; requests arriving during `load` park like any call to a
975
+ loading object; no turn starts before every plugin loaded.
976
+ - **W7, store addresses carry no host instance (owner, 2026-10-21; review round 4):** automatic: Chord builds the owner
977
+ string of persisted documents and tables from the host kind and object type (`session/notes.store`), never from a host
978
+ address (no session id), so a split (S7) or an import under a new id keeps every document reachable; each session has
979
+ its own store, so the session is the store, not part of each address. Durable's owner strings (`session`,
980
+ `conversation:7`, `task:31`) already qualify. Plugin code never writes addresses; the guide says keys need no session
981
+ id.
982
+ - ~~**W8, every text column as UTF-16 BLOB (owner, 2026-10-21; review round 4):**~~ (**superseded** by "Text is plain
983
+ UTF-8, in code point order") every `str()` column, indexed or not,
984
+ is stored as a UTF-16 `BLOB` (high byte first), so text has one order everywhere (JavaScript's), adding an index never
985
+ changes results, and lone surrogates and NUL survive; `json(...)` columns stay JSON `TEXT`. The SQL generator casts
986
+ `concat`/`replacePrefix` results back to `BLOB` and uses only even byte offsets (whole code units); conformance tests
987
+ cover emoji prefixes, NUL and lone surrogates. Cost: twice the bytes for ASCII text, hex in `sqlite3`, ~0.4 µs per
988
+ encode. Supersedes S16's "only key and indexed text". Revisit note (owner): Armin is not happy with the UTF-16
989
+ encoding; no better option is known yet.
990
+ - **W9, no compaction at first (owner, 2026-10-21; review round 4):** batches carry the ordered operations exactly as
991
+ committed; a replica applies them in order, as a whole, in one transaction (S5). "Keep the last write per row" is
992
+ dropped (it loses rows with `update` and bulk operations). A later compaction must merge `update`s into the preceding
993
+ `put` and treat bulk operations as barriers. Supersedes S6's compaction note and the book's compaction text.
994
+ - **W10, no reads after a bulk operation (owner, 2026-10-21; review round 4):** once a transaction has an
995
+ `updateWhere` or `deleteWhere` on a table, `tx.get` on that table throws `read_after_bulk` (no JS evaluator of `where`
996
+ and `set` on any backend); other tables and reads before the bulk operation are unaffected. Like Durable's
997
+ `ReadAfterWrite` (spec §4: no table read after the first table write), but narrower. The overlay for `put`,
998
+ `update` and `delete` stays (owner): helpers doing read-modify-write compose within one transaction. Durable keeps its
999
+ stricter rule (point reads could get the same overlay, scans could not; not needed today).
1000
+ - **W11, two version levels for migrations (owner, 2026-10-21; review round 4):** each `json(...)` column has its own
1001
+ version and a value-only migration (`json(Schema, { version, migrate: (old, from) => new })`), stored with its value;
1002
+ it migrates on access, and a write of that column stores the new shape and its version (writes of other columns never
1003
+ touch it). The row's `v` means only "eager steps applied up to v": only an eager step and a full-row `put` set it;
1004
+ `update`, `updateWhere` and `deleteWhere` never do. An eager `rows` step applies its row function to rows with
1005
+ `v < target` in key order with a cursor, writes the changed columns plus `v`, one commit per chunk; after a crash it
1006
+ scans from the first key again, skipping rows with `v >= target` (one pass, no temporary index, no progress record).
1007
+ No ordinary writes happen between chunks (the owner is not live until `ready`); old code refuses a newer row `v` or a
1008
+ newer JSON column version independently; replicas apply the shipped writes and alters without code; a new index is
1009
+ created after `ready`. Key changes are a recorded `rebuild`. Supersedes in part S17 ("each row stores its version `v`;
1010
+ `migrate(row, from)` on read") and "Flat columns" (shape changes inside `json(...)`).
1011
+ - **W12, W13 (owner, 2026-10-21; review round 4):** an in-memory commit (P2) takes the owner's next publication
1012
+ number, never a store seq, never enters the commit queue, and runs adopt/mark/flush (W1) when it happens. Durable's id
1013
+ counter at open is the maximum over all five of its tables (S10). One message per link per commit comes from the W1
1014
+ flush, also when one Durable commit touches several holds on a link. `count(q)` stays in the query runner although
1015
+ S12 starts without aggregates.
1016
+ - **W14, cuts (owner, 2026-10-21; review round 4):** (a) no IndexedDB backend: the browser runs SQLite in WebAssembly
1017
+ (official build 3.54.1: `sqlite3.wasm` 879 KB, ~406 KB gzipped), persisting into IndexedDB (a storage adapter such as
1018
+ wa-sqlite's) or the browser's private file system; serverless targets use SQLite where it exists (Durable Objects,
1019
+ Turso, D1) and a Postgres backend (a second SQL generator for the query language) when a real target needs it; a
1020
+ backend needs atomic transactions, ordered indexes, bytewise binary keys and one writer per session. FoundationDB was
1021
+ considered (Armin): the right model (ordered key-value with transactions) but the wrong shape (an operated cluster,
1022
+ no embedding, 5 s / 10 MB transaction limits, no self-contained session files); at most a later backend for a hosted
1023
+ multi-tenant service. (b) The `replicatedState` family (`replicatedState()`, `ReplicatedStateSource`,
1024
+ `AttachedReplicatedState`, the microtask scheduling in Durable's source attachment) is removed as public API in Chord
1025
+ and Durable. (c) `stopped` and `cancelled` stay two end reasons. (d) Derive `previous` is removed (`mapShared` and
1026
+ lazy recompute cover it). (e) The per-row `rows` migration step is the explicit exception (done by W11).
1027
+ - **No document tags (owner, 2026-10-21; Fable's consistency check, Astra earlier):** `defineTag`, the `tags` option of
1028
+ `tx.draft` and `retireByTag` are removed. They existed only to delete a session's data; with one store per session,
1029
+ deleting the session deletes its store and directory, and the documents engine (S13) has no tag index. Instance data
1030
+ on the server that refers to a session is a table with a `session` column, cleaned up by its owner (`deleteWhere`,
1031
+ reconciling from the catalog, "invariants across owners").
1032
+ - **Principals stay unversioned (owner, 2026-10-22; L1 part 4):** the app's `P` is a plain base-code type with no
1033
+ schema or version: Chord never persists principals, base code changes every host at once, links stamp principals.
1034
+ Objects persist a projection with its own schema, never the principal itself (p2: chat's `Author { kind, id, name }`
1035
+ from `authorOf(principal, ownerName)`, stored in every input entry); the guide states the rule.
1036
+ - **Direct mappings stay; params checked by TypeBox (owner, 2026-10-22; L1 build):** `accepts` maps each accepted old
1037
+ version directly to the current one (no chains in Chord; one argument mapping per call and one state projection per
1038
+ commit per old version); when a new version arrives, the author updates every still-accepted version's mappings, and
1039
+ the guide shows composing step functions (`(old) => v2to3(v1to2(old))`). Params are checked with a compiled TypeBox
1040
+ schema (a closed object), so constraints such as `str({ pattern })` or `int({ minimum: 0 })` are allowed and
1041
+ enforced; the type level and the declaration check allow only scalar helpers (`str`, `int`, `bool`, `oneOf`) without
1042
+ nesting. Refines L1 part 2 ("field by field, no TypeBox").
1043
+ - **`bindProvider` (owner, 2026-10-22; L1 build):** `serve` (L4) calls `bindProvider(Type, accepts)` once per load and
1044
+ reload: it runs `checkAccepts` once and keeps the validated mappings (frozen); per call `checkArgs(provider, method,
1045
+ raw, boundVersion)` checks with the bound version's schema and maps directly to the current version. Condition
1046
+ (owner): no leak. The binding is held only by the served type's registration and by calls in flight; no global
1047
+ strong cache keyed by contracts or bindings (compiled schemas only in `WeakMap`s keyed by schema objects); a GC test
1048
+ shows a binding, its mappings and compiled schemas are collected once dropped (reload) and the last call settled.
1049
+ - **L1 review round 1 decisions (owner, 2026-10-22):** same-host arguments are not checked for plain data (no
1050
+ prototype, getter or `unknown()`-subtree checks): the documented convention is that callers pass plain data (JSON
1051
+ plus bytes), immutable by convention. Params are never copied or frozen: the object keeps the caller's params
1052
+ object, immutable by convention (no freezing anywhere). A method without arguments takes `null`: dispatch passes
1053
+ `null`, the README says so. Cuts (owner): the error reason type is a closed union of the decided reasons (later layers
1054
+ add theirs; plugin errors never use Chord reasons, shape decided in L4); `failure.ts` and its activation helpers stay;
1055
+ the `copyJson` wrapper goes; `Provider` keeps only its runtime brand; `revocationKeys` stays (**superseded** by "Simplification pass after 4a": cut until 4b).
1056
+ - **L1 review round 2 decisions (owner, 2026-10-22):** error reasons are part of the link protocol: adding a reason
1057
+ bumps Chord's protocol version (hosts with different versions refuse each other, an old tab reloads), so a receiver
1058
+ keeps rejecting unknown reasons; a test pins the reason list with the protocol version. Failed checks take TypeBox's
1059
+ public error list (the internal first-error wrapper is removed). The provider's dispatch table is private (a
1060
+ provider-keyed `WeakMap`). Accepted versions' argument validators compile on first use. The closed-object argument
1061
+ check runs once per declared or edited method. Owner rule: no runtime checks of values TypeScript already guarantees
1062
+ (AGENTS.md); declaration checks of decided rules the types cannot express stay.
1063
+ - **Cost counters (owner, 2026-10-22):** the function that does the work counts it through its context (validation,
1064
+ compilation, canonical JSON, a future counted `copyJson` with an optional context added in pi); call sites never
1065
+ count work done elsewhere. Counters stay plain totals without site labels: mainly a test guard (a test isolates one
1066
+ operation), plus a rough production number; attribution, if ever needed, comes from an event on the context's span.
1067
+ The error rebuild from the wire does not copy (fresh decoder output) and checks only the envelope.
1068
+ - **Code style and L1 cuts (owner, 2026-10-22):** all Chord v2 code follows the section "Chord v2 Code Style" of the
1069
+ root AGENTS.md (one concept per file, top-down order, doc comments, no flag parameters, no typed-value checks, weak
1070
+ caches with GC tests), so every agent has it in context. Cuts: the params descriptor pre-check is removed (params and
1071
+ arguments follow one rule: the compiled closed schema; wire params are parsed JSON, same-host callers pass plain data
1072
+ by convention); `bindProvider` keeps the app's accepts table as is (immutable by convention), no copy or freeze.
1073
+ - **L1 rewrite questions (owner, 2026-10-22):** `checkArgs` returns a tagged value, `{ kind: "method"; args; result? }`
1074
+ (args in the current shape, `result` maps back for an old-version caller) or `{ kind: "call"; args; call }` (the
1075
+ checked old args and the provider's replacement, invoked by L4); no second lookup function. Failed-check paths are
1076
+ TypeBox's instance path unchanged; keys containing `/` or `~` make it ambiguous (a diagnostic; not worth recovering).
1077
+ The JSON Schema `oneOf` keyword is rejected (the `oneOf()` helper emits `anyOf`); local accepts tables and
1078
+ `Implementation` are flat.
1079
+ - **L1 rewrite review (owner, 2026-10-22):** nothing in L1 is frozen: contract handles, schema snapshots, params
1080
+ schemas and providers are Chord-owned data, immutable by convention like everything else; the snapshot copy stays
1081
+ (it keeps validation in sync with the fingerprint when an app changes its schema objects). Supersedes "frozen" in Q10
1082
+ ("history private, deep-copied and frozen") and the frozen metadata in the round-1 and round-2 entries. Size accepted
1083
+ (about 1,900 lines, 1,450 without blanks and comments; about 420 type-only). Failed checks say "unknown field" and
1084
+ "missing" for those two cases, TypeBox's message otherwise. A malformed error envelope from a peer throws a plain
1085
+ `Error`. `Principal` is a type alias (apps declare principals as type aliases, not interfaces).
1086
+ - **One watch-handle implementation, upstream first (owner, 2026-10-22; L2 spec):** Durable's `CommittedWatch` moves
1087
+ into Chord's shared `src/watch.ts` (pi change, upstream first), generalized to serve native watchables too
1088
+ (`NativeWatchHandle`, listener `(value, context)`, overflow to the newest value); Durable uses it and its
1089
+ `observation.ts` goes away; Chord v2's L2 builds on it. Publications, derives, sinks, `constant` and `writable` stay in
1090
+ `next/` (v2 only). Durable's synchronous view-change hook (so a derive over a Durable view is marked in the commit
1091
+ step) is a later pi change, with L3d.
1092
+ - **L2 spec decisions (owner, 2026-10-22):** watchable state is JSON only (no bytes in documents, derives, reference
1093
+ members or any watchable; bytes only in method arguments and results and chunked transfers; Delta stays JSON-only;
1094
+ supersedes W4's "JSON plus bytes" for watchables). `mapShared` lives in v2's `watch/` until v2 goes upstream. C10 is
1095
+ rejected (`constant` and `writable` stay two kinds, Q14). One publication object per host, passed to its sources;
1096
+ no module-level singleton. No CBOR: the wire stays JSON plus raw byte segments (R1, Q4), revisited only if L6
1097
+ benchmarks show encoding as a bottleneck.
1098
+ - **Contexts are required (owner, 2026-10-22):** as in Durable (356 required context parameters, none defaulted;
1099
+ optional only on error-reporting paths), every Chord v2 function that takes a context requires it: no
1100
+ `= BACKGROUND_CONTEXT` defaults, no `context?`. A getter that cannot take one (a lazy derive read) uses its owner's
1101
+ context (the publication's). Applied to L1 and L2; written into the AGENTS.md style section. L2 review: the
1102
+ publication resets its phase and queues in a `finally`, so a throwing sink cannot leave it stuck; L2's size (about
1103
+ 680 lines) is accepted.
1104
+ - **Carry-over ledger (owner, 2026-10-22):** obligations deferred to later layers are recorded in
1105
+ `packages/chord/docs/next/carry-over.md` (later folded into `slices.md`), under the layer that must do them, by whoever defers them, in the same
1106
+ change; a layer's spec includes its section; done entries are deleted. The AGENTS.md style section states the rule.
1107
+ - **Vertical slices instead of layers (owner, 2026-10-22):** building strictly bottom-up made L2 grow generic machinery
1108
+ for callers that did not exist yet (nodes, sinks, deliveries, "publications"), hard to follow; its code was deleted,
1109
+ not kept. From now on Chord v2 is built in vertical slices: each slice is the thinnest end-to-end path through
1110
+ every layer it needs (slice 1: one host, one local object with `self.state`, `self.tx`, `self.derive`, a checked
1111
+ method call and a watch), so every API has a real caller. Same process per slice: spec by this session, owner
1112
+ approval, an Opus 5.5 build, review, owner review, commit. L1 stays (stateless checks, committed with required
1113
+ contexts and an `index.ts`/`internal.ts` split, `ff4f4456d`). Kept from L2: Chord's shared watch handle
1114
+ (`src/watch.ts`) and the decisions (JSON-only state, one batch per host, no CBOR, C10 rejected). Plain names from
1115
+ the design's vocabulary or common usage (a signals library's `batch`), never invented internal jargon in APIs.
1116
+ Plan: `packages/chord/docs/slices.md`.
1117
+ - **`Implementation` maps state members to watchables (owner, 2026-10-22; slice 1):** L1's `Implementation<C>` types a
1118
+ state member as `Watchable<Static<schema>>` (the document handle, derive or reference member the factory returns),
1119
+ not a getter `() => value`: a getter cannot tell Chord when a value changed (design §11).
1120
+ - **Slice plan and slice-1 review decisions (owner, 2026-10-22):** slice order 1 local objects, 2 failures and
1121
+ natives, 3 two hosts, 4 topology, 5 routines and jobs, 6 persistence, 7 reload and rollout, 8 registries and the
1122
+ harness, 9 telemetry (routines and jobs before the store and reload). A feature map in `slices.md` assigns every
1123
+ design feature to a slice; implementers and reviewers check the slice spec, the map and the design (AGENTS.md).
1124
+ Slice process includes reviews by Astra and Fable. An object that ends rejects new calls, aborts its running calls'
1125
+ contexts and makes its `self.tx` throw `released` (a running method can change nothing after the end); no drain
1126
+ without natives (the drain, design §3, is decided with natives and C13 in slice 2). Keyed in-memory documents,
1127
+ `mapShared` and `defineFacet<P>` stay in slice 1 (base infrastructure), each with a real use in its example.
1128
+ `checkpointWhen` is required only for persisted documents (refines R14: in-memory documents are never written, so
1129
+ checkpointing cannot apply). `retryOn` waits for the first independently living source (registries, slice 8);
1130
+ slice 2 has `ref.retry()` and dependency recovery.
1131
+ - **Slice 1 approved (owner, 2026-10-22):** cycle rejection on one host (Q5) is in slice 1; watches belong to the
1132
+ reference they came through and end when it is released; at `get` the consumer's contract version and fingerprint
1133
+ must equal the provider's (`contract_mismatch` otherwise); `then`, `release`, `state` and `retry` are reserved member
1134
+ names, refused at declaration; cut C14 rejected (the `handle.change`/`handle.retire` shorthands are built); cut C16 (**superseded**: accepted on 2026-10-26, "Facet documents are removed")
1135
+ rejected (facet documents are built: served objects may draft them in their own transaction).
1136
+ - **Callers when an object ends (owner, 2026-10-22; slice-1 review):** as design §3 says, callers are rejected at
1137
+ once: when an object ends, every caller of a running call gets `released` immediately; the handler keeps running in
1138
+ the background under an aborted context and cannot commit (`self.tx` throws `released`). A call whose caller context
1139
+ is already aborted is rejected before its handler runs. Each call runs under `withAbortSignal(objectEndingSignal,
1140
+ callerContext)` (one controller per object; a caller's cancellation aborts only its own call). The frozen v2 kernel,
1141
+ its tests, the playground built on it and the kernel step docs were removed (`4a54fc38b`); `Watchable` went upstream
1142
+ into `src/watch.ts` (`a2fd25f56`). Facets are typed by exactly the host's principal type (invariant).
1143
+ - **No derive names, no `null` for no-argument methods (owner, 2026-10-22; slice-1 review):** `self.derive(sources,
1144
+ compute)` takes no name: the name existed for `previous` (removed, W14); on reload exposed derives are matched by their
1145
+ member name, internal ones are rebuilt (slice 7 adds matching only if a real need appears). A method without
1146
+ arguments omits them in the API: references call `ref.bump(ctx)` and implementations are `bump: async (ctx) => …`;
1147
+ the shape is fixed per method by the contract; `null` remains the internal and wire form. Refines the L1 round-1
1148
+ decision "no-argument methods take `null`".
1149
+ - **Slice 2 approved (owner, 2026-10-22):** natives with per-native readiness, ordered opens and reverse closes, close
1150
+ reasons and a close context aborting at `closeTimeout`; failure causes by where they failed; every attempt bounded
1151
+ by `readyTimeout`; backoff per identity (a map holding no object); only R18 failures keep references; gap-free
1152
+ dependency handover on retries; the start barrier waits for each host-lifetime object's first outcome; `host.get`:
1153
+ broken rejects at once, transient waits up to the getter's own deadline; the drain waits for handlers' own promises
1154
+ before natives close. Cuts: C5 rejected (no natives in facets); C6: `broken()` kept, `retryOn` in slice 8; C7:
1155
+ `linger` kept, built in slice 3; C13: two host timeouts (`closeTimeout`, `readyTimeout`), the drain uses
1156
+ `closeTimeout`. `ref.state`, `ref.retry()` and `host.hold` move to slice 3; the plugin error shape is decided in
1157
+ slice 3. Openers take a `Context` (not `({ signal })`).
1158
+ - **Slice 3a approved (owner, 2026-10-22):** slice 3 is split into 3a (remote objects over one link) and 3b (surviving
1159
+ drops: re-holds, parking, `ref.state`, `host.hold`, `ref.retry()`, `linger`, `host.root`, `state: false`). Guiding
1160
+ rule for every choice: simple, fast, correct. A remote factory returns `{ variant, members }`; `ref.variant` is the
1161
+ discriminant (C15 accepted: no `inVariant`/`expectVariant`; `variant` reserved). Calls are checked against the
1162
+ binding's variant. A hold's state is one object with one Delta encoder; failed members travel in the header. A
1163
+ release rejects the calls made through that reference, locally and over links (refines slices 1-2). `$bytes` is the
1164
+ one reserved key in values crossing a link (byte leaves travel as raw segments referenced by `{"$bytes": i}`).
1165
+ Plugin errors: `PluginError(code, message, details?)`, `internal` for anything else, never a stack. Headers are
1166
+ checked on every link until IPC (slice 4). Hot-path slices add light benchmarks (timing, memory, wire size) recorded
1167
+ in `benchmarks.md`.
1168
+ - **Wire protocol (owner, 2026-10-22; slice 3a review):** the protocol works on a typed `Message` (one TypeBox
1169
+ schema, `MESSAGE`, also checked after every decode); codecs are pluggable per link: `binaryCodec` (kind byte, varints,
1170
+ raw ids, JSON values; about 23 B for a streamed token instead of 98) and `textCodec` (Delta-style JSON tuples, no field
1171
+ names), compared by benchmarks for wire size, compute and memory. Member, method and variant names travel as indexes
1172
+ into the hold's bound version; a reload that drops that version ends its holds first (slice 7). Checks by layer: the
1173
+ codec's structural decode, the `MESSAGE` envelope, the bound version (ids, indexes, order), then the owners
1174
+ (`checkParams`, `checkArgs`, Delta's decoder). The memory link delivers with `queueMicrotask` (no Node APIs). A
1175
+ 2,000-token reply drops from about 196 KiB to about 46 KiB. Supersedes R1/Q4's JSON header framing.
1176
+ - **Slice 3b approved (owner, 2026-10-22):** references survive link drops (`loading`, values readable, calls queued
1177
+ with their own deadline and sent in order, sent calls fail), one owner of remote references across links re-holding
1178
+ on every `connect`, a re-hold may bind another variant; `ref.state` (`loading`/`live`/`failed`/`released`) is a
1179
+ `NativeWatchable` (interface added to `src/watch.ts` upstream first); `host.hold`; remote retries through two new
1180
+ messages `retry { hold }` and `loading { hold }`; parked calls wait at most `readyTimeout` (C13 keeps two host
1181
+ timeouts); `linger` for live objects; C8 (`state: false`) cut; `host.root` moves to slice 8.
1182
+ - **Flow control: an ack-clocked window (owner, 2026-10-23):** supersedes Q4's byte window, R19/Q3 (snapshot admission
1183
+ and debt), C12's credit accounting and K16's call credits. Measured by Astra and Fable (Node 24, `ws` 8.22, separate
1184
+ processes, proxy at 64 KiB/s): a `bufferedAmount` cap plus latest-state coalescing still delivered state 9.4 s old
1185
+ (kernel and proxy buffers hide megabytes); application acks with 1-4 messages in flight plus coalescing delivered
1186
+ 0.0 s old state, heap +0.4 MB, 98% fewer messages; no control buffered 7 MB (+18.5 MB heap; a client that stops
1187
+ reading: 23.4 MiB, RSS +121 MB in 15 s). IPC `send()` returns false from about 64 KB yet 623k sends cost +1,035 MB;
1188
+ its callback acknowledges the write, not the handler. Rules:
1189
+ - At most K=2 unacked `bound`/`state` messages per link; the consumer sends a small `ack` after applying each one.
1190
+ - Ops queued meanwhile are merged per hold; a snapshot replaces them only when the merged ops exceed the member's
1191
+ value size (2,000 appends: 46,024 B one by one, 26,021 B merged, 8,035 B as a snapshot; but a member with 500 KB
1192
+ of history would cost 508,035 B as a snapshot).
1193
+ - Calls: a call window N (default 256) announced in `hello`; the caller parks extra calls in 3b's parked-call queue,
1194
+ FIFO, refusing nothing (K16 kept). Exceeding N is a protocol error, enforced by the provider. 100k calls of 10 ms:
1195
+ a caller-side cap of 64 took 17.1 s, +4 MB, another client's p95 0.66 ms; unlimited ran 43,824 at once (+56.5 MB,
1196
+ p95 129 ms); a provider semaphore queued 96,736 on the provider.
1197
+ - Holds are unlimited by count; size limits stay a separate check. Dropped: the byte window and credit messages,
1198
+ snapshot admission and debt, C12's credit accounting, an adapter `backlog()` API, `reset` from the consumer.
1199
+ - **Relays ack per hop (option b):** the server acks upstream on receipt. When a downstream tab falls behind, the
1200
+ server drops that tab's queued parts and sends `reset { hold }` upstream; the provider answers with a fresh
1201
+ snapshot of that hold (a second `bound`). One slow tab never coarsens the other tabs of the same worker link
1202
+ (option a, end-to-end acks, rejected for that). About 40 more lines than (a).
1203
+ - Open for the slice-4 spec: Astra's circuit breaker (a hard transport-buffer cap plus a stall or heartbeat
1204
+ deadline that closes the link).
1205
+ Explainer: `packages/chord/docs/flow-control/` (Radius artifact 01m5n82vsbe5rtn4nm6wzaq0mp).
1206
+ - **One pending queue for slow consumers of state (owner, 2026-10-23):** refines the snapshot fallback of "Flow control:
1207
+ an ack-clocked window" and supersedes "Watchable and one watch API"'s "after 100 pending frames the backlog becomes
1208
+ one root replacement" for state watches. Measured by Astra and Fable (`/tmp/fc2-*.md`, `/tmp/fc3-*.md`; real p2 entries plus synthetic stress
1209
+ cases): size-based rules fail ("snapshot bytes + sent op bytes" is no bound: an interned path lets a value grow
1210
+ faster than its ops; it drifts 1,254x under churn; exact size needs per-node bookkeeping), a time trigger punishes a
1211
+ stalled stream into a large value, and a count of unfolded commits resends whole histories. Folding alone came
1212
+ within about 10% of an exact oracle. Rules:
1213
+ - Pending ops stay raw (never encoded per commit while waiting); `r` drops everything before it.
1214
+ - **Leaf index:** a map from path key to the last pending leaf op (`a`, `s` of a primitive); a new leaf op on a
1215
+ path in the map merges into that op in place (`a`+`a` joined, `s` replaces `s` or `a`, `a` after a string `s`
1216
+ appends into it). Any other op (`p`, `m`, `d`, `t`, `s` of an object or array) clears the map. Correct because
1217
+ leaf ops on different paths commute, except an ancestor primitive `s`, after which the descendant cannot be
1218
+ written until a structural write clears the map (fuzzed: about 1.8M tracker and `diffRevisions` ops, arrays with
1219
+ splices and reorders, adversarial cases; a broken index without the clear on `p` fails). Cost about 0.2-0.4 us and
1220
+ 60-80 B per pending leaf path. Ops come only from Chord's tracker and `diffRevisions`: no defences against
1221
+ hand-written ops (no path-key normalization). No folding of list appends (1% gain, quadratic copies) or trims.
1222
+ - **Cap: 10,000 pending ops after folding;** past it only "replace with the current value" stays pending (nothing
1223
+ more accumulates). Worst case about 2.2-2.7 MB per member per slow consumer. No size or text caps, no time trigger.
1224
+ - Used by link hold members while the K=2 window is full (encoded once when an ack frees a slot) and by state
1225
+ watches (`Watch` over `(value, ops)`): a slow listener gets one call per batch with the folded ops and the newest
1226
+ value. Measured, listener taking 1 s: 438 calls and 3.39 MB of ops become 171 and 371 KB. The watch change is in
1227
+ Chord's shared `src/watch.ts` (pi first); tests and `examples/04-watches` change with the contract.
1228
+ - Unchanged: Durable's event stream (`watchEvents` batches are events: they keep their own queue until removed),
1229
+ `NativeWatch` (newest value), relays (drop + `reset { hold }`: parts are opaque).
1230
+ Animation: `packages/chord/docs/flow-control/fold/` (Radius 01m5nc9s24e9kscppen2fxgwye).
1231
+ - **Slice 4 split and first decisions (owner, 2026-10-23):** slice 4 is split into 4a (links and relays: IPC and
1232
+ WebSocket links, trusted links, parts as opaque bytes, flow control, host kinds and placement, routes, relayed holds
1233
+ and calls) and 4b (revocation, cycles across hosts, `host.references`, `host.activity`, size limits, compression);
1234
+ draft with structural changes and races: `packages/chord/docs/slice-4.md`. Decided: trusted links are an
1235
+ explicit option (`trusted: true` on `accept` and in `resolve`'s result; default untrusted; stamping only from the
1236
+ parent link). ~~Resets and queue overflow travel as `[["r", value]]` ops in an ordinary `state` message~~ (**superseded** by
1237
+ "Slice 4a review decisions": resets are a list-`bound`; overflow stays an `r` op): a relay's
1238
+ `reset { holds }` (a list) is answered with one message carrying every reset hold's members as `r` ops (snapshots
1239
+ taken in one step, applied in one batch), entries flagged `reset: true` in the header for relays; a second `bound`
1240
+ only for slice 7's restarted objects. ~~Relayed calls are sent round-robin per origin while the worker's call window
1241
+ is full (each origin keeps its order).~~ (**superseded** by "Relay flow control per origin": a relay forwards calls at
1242
+ once; each origin has its own N at the provider). The Durable conversation view's size on re-hold is a known cost until pi
1243
+ #11327 lands.
1244
+ - **Relay flow control per origin (owner, 2026-10-24):** supersedes per-hop acks, `reset { holds }`, relay recovery
1245
+ and id renumbering ("Flow control: an ack-clocked window" relay part, "Slice 4a review decisions" reset part). The
1246
+ provider keeps the window (K=2), its pending queues and its call queue per **origin**; the relay never drops, folds or
1247
+ resets. The relay assigns each downstream link an origin number (never reused on an upstream link; 0 = its own
1248
+ work); upstream messages keep their ids and carry `origin` (read only on the parent link); the provider keys holds,
1249
+ calls, windows and acks by (origin, id); replies carry the origin. The relay keeps per downstream hold its upstream
1250
+ link and its original `hold` message (routing calls and releases; re-holds after a worker restart), and per downstream
1251
+ link a FIFO of the upstream link of each message in flight (each in-order `ack` goes to the right worker). A new
1252
+ message "origin closed" releases an origin's holds and calls when its link closes. A tab served by W workers may have
1253
+ 2W messages in flight. Measured motivation: a slow tab through the relay needed ~24 s and 3 resets to catch up after a
1254
+ 1 s stall (provider folding: 0.02-0.38 s). After the rework and the review fixes: a simplification pass with a budget
1255
+ of 1,200 lines of net growth for 4a (owner).
1256
+ - **Docs (owner, 2026-10-24):** `design.md` becomes the one current statement of all rules, updated in the same commit
1257
+ as each decision; `mechanisms.md` folds into it, `carry-over.md` into `slices.md`; the decision log stays complete as
1258
+ the history and the reasons; slice specs stay as history after their slice. Nothing is dropped in the move.
1259
+ - **Slice 4b: `host.activity` is a watchable (owner, 2026-10-24):** a JSON `Watchable` of `{ references, calls, jobs,
1260
+ lingering }`, published at most once per microtask turn with changes (design §3's "one readable").
1261
+ - **Docs consolidation (owner, 2026-10-24):** applied as decided in "Docs": `design.md` now states every current rule
1262
+ (`mechanisms.md` folded in section by section, its 17 stale points fixed, then deleted); `carry-over.md` is the
1263
+ section "Carry-over" of `slices.md` (AGENTS.md updated). Settled by what slices 1-4a built, as approved: resets do
1264
+ not exist (per-origin flow control); trusted links skip only the `MESSAGE` check; a parked call waits its
1265
+ reference's `readyTimeout` (no per-`get` `parkTimeout`); the object backoff is per identity; `host.get(Type, params,
1266
+ context, options)` takes a context and `host.hold` returns at once and never rejects; a native's `close` gets
1267
+ `(value, reason, context)`; a local object's handlers act as the host's principal. Still open, listed in design
1268
+ §26 (#24-#29): the timeouts of later mechanisms, `host.root`'s signature, how a migration commit reaches consumers,
1269
+ compression on browser links, the circuit breaker, cuts C3/C4/C11.
1270
+ - **Simplification pass after 4a (owner, 2026-10-24):** two cutters (Opus 5.5, GPT-6.1 Sol) read all of v2 (`src/next`)
1271
+ without a line target (a target is not meaningful); the merged small cuts, two bug fixes (relays strip a client's
1272
+ principal fields on every path; a closed link delivers no queued message) and D1-D4 are applied by a fresh builder.
1273
+ D1: the unreachable "binding back to pending" branch of a served hold is cut from 4a; slice 7 builds it with reload
1274
+ (carry-over). D2: L1's unused `revocationKeys` is cut; slice 4b writes revocation keys with their first user. D3:
1275
+ `initial()` is checked once per definition at its first value (refines R8's "per document": `initial()` takes no
1276
+ input, so a wrong shape shows at the first call); a failing keyed `initial()` fails only its transaction, a
1277
+ singleton's fails the activation. D4: object instance ids (Q5) come with slice 4b, their first user; one-host cycles
1278
+ follow the waiting objects without ids.
1279
+ - **Slice 5 spec drafted (owner, 2026-10-24):** straight from the discussion to `slice-5.md` (the walkthrough
1280
+ `routines-and-jobs.md` deleted, owner); design §14 and §15 state routines and jobs as decided; jobs in memory in
1281
+ slice 5, persistence in 6, reload rules in 7 (carry-over); jobs as objects (#9) rejected. `tx.start` returns the
1282
+ run's id (R12), `job.outcome(id, ctx)` waits by id; `unavailable/busy` is a new error reason.
1283
+ On the draft: routines start right after the object went live (same turn); `abort(id)` drops a blocked run (no
1284
+ `finish`, reported); `running` carries every run's args (views filter). Process: spec reviews, then the build.
1285
+ After the spec reviews (Astra, Fable, Opus, Sol): `tx.start` returns a `Run` handle (its outcome rejects if the
1286
+ transaction is discarded) instead of an id; `ctx.state.change(fn)` takes no context (changes while stopping after an
1287
+ abort still commit); running and finishing runs are a count, not references, and an object without holders ends
1288
+ (after `linger`) when its last run settles, dropping blocked runs; `finish` may chain the same job (its own run is
1289
+ excluded from the concurrency check); a `run` throwing after an abort ends `aborted`; `job_failed` reported;
1290
+ ~~routines also in facets~~ (**superseded** by "Facets only declare"); `persist: { resume: "rerun" | "abandon" }` is the final shape.
1291
+ After the build reviews: the builder's ten points accepted (every object end reports dropped blocked runs; their
1292
+ waiters get the end's error; `accepts` needs both mappings per older version; job args without bytes; routine labels
1293
+ per kind; `start` before live is `not_ready`; a run cancels a linger; design fixes; rejected handles keep their stack
1294
+ until read, documented); native `on` sources also get only the newest value after a busy call; the finishing run is
1295
+ excluded from dedupe too (`finish` may start the same job with the same key); the retirement frame in `on` is a
1296
+ slice-6 carry-over.
1297
+ - **Routines and jobs open points (owner, 2026-10-24; walkthrough `routines-and-jobs.md` and its reviews):** at the
1298
+ `concurrency` limit `start` rejects; the routine handover is bounded by `closeTimeout`; a persisted job's state
1299
+ document gets the deferred persistence option (as in Durable), and authors throttle frequent state changes themselves
1300
+ (Chord does not coalesce them: every change is a commit and a message). Jobs keep `accepts` (owner: everything
1301
+ versioned has adapters; the walkthrough's "no job adapters" was never decided): a run started on an older version
1302
+ finishes on its old code, its args and result are mapped to the current version for `finish` and for `running`, its
1303
+ state document migrates through its own `migrate`; without an adapter the run is `blocked` and reported until a
1304
+ reload brings one. So a version change aborts nothing and discards nothing.
1305
+ Crash handling for persisted jobs (owner, with all four reviewers): a restart limit as Durable's (five crash
1306
+ interruptions, counted and persisted, clean closes not counted; then parked as `blocked`, cleared by `abort` or a
1307
+ reload); a `finish` that keeps throwing is retried with backoff and then `blocked` after five consecutive failures,
1308
+ its outcome, state and key kept, retried on a reload; how a persisted run resumes after a crash (rerun, replay-safe,
1309
+ or abandoned with an aborted outcome, wording aligned with Durable) is required, with no default.
1310
+ - **Routines for slice 5 (owner, 2026-10-24; C4 decided):** two kinds, declared in the factory, no names and no
1311
+ `routines` namespace: `self.on(source, fn)` (first call with the current value, W2b; then one call per change, calls
1312
+ never overlap, changes meanwhile folded; a running call is not aborted by a change; a throwing call is reported and
1313
+ called again with the current value after a backoff of 1 s doubling to 60 s, a change during the backoff calls at
1314
+ once; the source ending ends the routine) and `self.onLive(fn)` (called once when the object goes live; returning
1315
+ ends it until the next reload or activation; throwing is reported and restarts it after the same backoff, reset
1316
+ after a run of a minute; an abort is no error). Both start when the object is live and stop when it ends (`ctx`
1317
+ aborted, then the drain); on reload all old routine calls of the object finish (bounded) before the new routines
1318
+ start: a handover per object, which needs no names. Routines are idempotent reconcilers; Chord keeps no state for
1319
+ them; what must survive is a document. Helpers `sleep(ms, ctx)` and `untilAborted(ctx)`; routines and handlers wait
1320
+ only through calls that take `ctx`. Errors: `HostError` `routine_failed { object, routine, error }` (`routine` =
1321
+ kind and position). Routines do not count in `host.activity`. Dropped: `every` (an `onLive` with `sleep`; a due
1322
+ time that must survive is a document), `custom` and `run(fn)` (an `onLive` with `try`/`finally`), names, the due
1323
+ time across reloads. Kept separate from jobs (different lifecycle, reload behaviour and output). Open: the
1324
+ handover's bound (`closeTimeout`, whose default may drop from 30 s to 10 s, or its own).
1325
+ - **Slice 6 first decisions (owner, 2026-10-26):** (1) **Durable's internal commit observers run inside `adopt`**
1326
+ (pi change, upstream first, built and reviewed before the slice): the Session's `observeCommits` listeners (view
1327
+ mounts, task graph, scheduler, submissions) move from `#publish`, after `Storage.commit` returned, into the `adopt`
1328
+ callback, so every Durable view changes inside the storage's commit step; with `StoreStorage` Durable's `adopt` runs
1329
+ inside the host's batch, and Chord follows Durable's views as sources marked in the step (W1). Reason: today a
1330
+ derive over two Durable views (conversation view and task graph), fed by two watches, computes between their frames
1331
+ of one Durable commit ("tool call issued, no task running"). `adopt` stays throw-free (a listener's throw is kept and
1332
+ fails the Session after `Storage.commit` returned, as today); `subscribeCommits` and telemetry events stay in
1333
+ `#publish`; nothing changes for users of Durable's own storages. (2) **Persisted singletons load sequentially** right
1334
+ after the factory, all singletons of an object in one read transaction (S4), before its references are bound; a
1335
+ storage error is `storage_failed`, a fingerprint mismatch, a throwing `migrate` or an invalid migration result is
1336
+ `broken`. (3) **Durable's ids in the store:** `StoreStorage` keeps a `record_ids` table (Durable's one namespace
1337
+ across its five tables); the counter at open is `coalesce(max(id), 1) + 1` over it (1 is the root conversation). (4)
1338
+ **Few statements per commit:** the store keeps seq and head in memory (read at open, the head persisted with each
1339
+ commit, S9) and folds the documents engine's checks into the writes (a unique `record_ids` key, a partial unique
1340
+ index on the live address, `UPDATE … WHERE id = ? AND version = ?`), so a delta commit is about three statements
1341
+ (revision, version, outbox); `StoreStorage` maps constraint errors to the messages Durable's conformance suite
1342
+ expects. A benchmark compares `SqliteStorage` and `StoreStorage` on the same workload. Still open: the split of slice
1343
+ 6, and retirement in watches (point 3 of the discussion: no `null` frame, Chord documents that retire).
1344
+ **Refined (owner, 2026-10-26), after four reviews of the first build:** the first build moved only Durable's internal
1345
+ observers into `adopt` and deferred host-waking work (`afterPublish`); the reviews found host-visible regressions in
1346
+ the gap between `adopt` and the end of `Storage.commit` (watch listeners before `subscribeCommits`, `waitForIdle`
1347
+ early, a late subscriber missing a commit, the active-task metric drifting after a rejection). Under review now,
1348
+ nothing built: "publication at adoption" (the whole publication, host `subscribeCommits` listeners and telemetry
1349
+ events included, inside `adopt`; no `afterPublish`; `adopt` runs no task code, so a marked run's `abort()` is
1350
+ queued). **No Durable special cases in Chord's store** (owner): the store never knows or waits for Durable's
1351
+ continuation.
1352
+ **Decided (owner, 2026-10-26), after four checks of the proposal (`/tmp/observers-proposal-*.md`, explainer
1353
+ `packages/chord/docs/durable-observers/`):** publication at adoption in Opus's variant. Inside `adopt`, in
1354
+ HEAD's order: the trackers, the telemetry events, the queueing of one microtask that dispatches the host
1355
+ `subscribeCommits` listeners (the listener list read inside that microtask), the internal observers. Microtasks run
1356
+ in order, so host listeners still run before every wait continuation, abort and `onWake` of the commit (before
1357
+ every watch delivery too, as first stated: **superseded**, see "Superseded (owner, 2026-10-26)" below), but after the storage's synchronous step, so never inside Chord's batch (Chord calls from them work; Chord
1358
+ needs only its step and flush to be synchronous). `afterPublish` is dropped. `adopt` runs no task code (a newly
1359
+ marked run's `abort()` is queued) and is fully throw-free (listeners, reports, metrics and telemetry contained; an
1360
+ internal observer's error fails the Session after `Storage.commit` returned). Accepted changes from HEAD: abort
1361
+ listeners fire after host listeners; a commit the storage rejects after `adopt` is published (it is durable; a
1362
+ replica stays equal to storage; spec invariant 2 changes) and its waits resolve before the Session fails; with a
1363
+ storage that works after `adopt` (JSONL) the publication runs before `Storage.commit` resolves; a throwing telemetry
1364
+ call is reported as `host_error` with callback `"telemetry"` and no longer rejects a durable commit. Nothing in pi or
1365
+ p2 depends on the old orders (checked: p2's three `subscribeCommits` users).
1366
+ **After the reviews of the second build (owner, 2026-10-26):** one delivery rule for every watch: a watch delivers a
1367
+ frame in a microtask queued after the frame arrived, also when its listener was busy (Chord's shared `src/watch.ts`:
1368
+ after a call the drain reschedules instead of taking the next frame in place). Without it a busy watch's
1369
+ continuation, queued before `adopt`, delivered a commit before its host listeners (Astra, Opus). Cost measured:
1370
+ about 170 ns per back-to-back delivery with a promise, less with a plain `queueMicrotask`; only busy watches pay it.
1371
+ Also: a host listener added between `adopt` and the dispatch gets the commit only when a listener existed at
1372
+ `adopt` (documented, accepted); a rejection after `adopt` keeps the storage error as the failure and still reports an
1373
+ observer's error.
1374
+ **Superseded (owner, 2026-10-26), after the third reviews:** the delivery rule did not hold (Astra, Opus: a delivery
1375
+ already queued folds frames that arrive later, so a watch can still deliver a commit before its host listeners, also
1376
+ in a shared commit step chained through promises); a complete fix (a late delivery queues itself again) would stall
1377
+ watchers behind a source that changes in every microtask. Decided instead (option b): **no ordering promise between
1378
+ host `subscribeCommits` listeners and watch listeners** (independent observers; nothing in pi or p2 depends on their
1379
+ order); waits and aborts still follow the host listeners by construction; the hop in `src/watch.ts` is reverted.
1380
+ - **Durable's synchronous observers (owner, 2026-10-26):** `observeConversation(id, observer, ctx)`,
1381
+ `observeTaskGraph(observer, ctx)` and `observeDoc(token, …, observer, ctx)` become Durable's public primitive: the
1382
+ observer's `advance(change, ctx)` (the listener bag `{ value, ops, retired }`) runs synchronously inside `adopt`,
1383
+ `end(failure)` when the Session closes or fails; attaching returns the current value and `detach`, taken atomically
1384
+ on the line. `watchXXX` stays, built on it (a `Watch` is such an observer), for app code; `observeXXX` is documented
1385
+ for adapters (record only: no awaits, no Session calls, no throws, quick). Chord's `durableView` (slice 6b) is a
1386
+ source over it, marked inside Chord's batch in the store and grouped into one batch per Durable commit on the next
1387
+ microtask outside it.
1388
+ **After the reviews of its build (owner, 2026-10-26: fix everything to HEAD's level of correctness, performance and
1389
+ memory; the owner delegated the details):** `onWake` is no longer ordered after host listeners (same cause as
1390
+ watches); `Watch.endAfterDelivery()` exists (a task document's watch delivers its retirement, then ends `ended`),
1391
+ detaching only when the watch really ends and never while a listener call runs; the `observeXXX` context covers the
1392
+ attachment only; observers get `end(undefined)` when their task document's task ends; the change object a listener
1393
+ gets carries no context (the context is a separate field of the watch); a watch keeps only its address, never a
1394
+ loaded document; `observeXXX` rejects like `watchXXX`; Durable's file-watcher type in `/env` is renamed
1395
+ `FileChange` so `WatchChange` names only the listener bag.
1396
+ - **One retirement mechanism for Chord and Durable, watches follow the address (owner, 2026-10-26; option A):**
1397
+ supersedes W2a's last frame `[["r", null]]` and the end reason `retired`, and Q11's silent reset as the only meaning
1398
+ of retiring a singleton. A document lives at an address (Chord: definition and key; Durable: kind, scope and key);
1399
+ every document, singleton or keyed, Chord or Durable, is watchable; a retirement is signalled to the watch listener
1400
+ as one last call with the last good value, no ops and a `retired` flag (no `null` anywhere); the watch goes on, and a
1401
+ later write delivers the new incarnation as `[["r", value]]` on the same watch. A watch ends when its owner ends
1402
+ (`ended`): a Chord document's object, a Durable task document's task (the retirement call, then `ended`, in the same
1403
+ commit), the Session; and by `stop`, cancellation, failure or a throwing listener. Durable's watches change from
1404
+ incarnation to address (breaking; nothing in pi or p2 watches a document that retires). Leaks: none new for task
1405
+ documents (they end with their task and invocation); a forgotten watch of a session or conversation document that
1406
+ is retired for good stays until its taker's context, `stop` or the Session ends it (GC tests). Still to decide: the
1407
+ shape of the flag, the value an absent address shows to members, derives and `.value`, and the per-key source in a
1408
+ factory.
1409
+ **Details decided (owner, 2026-10-26):** (1) a watch listener (and an `on` handler) takes one object before the
1410
+ context, `(change, ctx)`: `{ value, ops, retired }` for JSON watches, `{ value }` for native ones (a breaking change
1411
+ of the shared `WatchHandle`/`NativeWatchHandle` and of `self.on`, accepted). (2) A retirement call carries the
1412
+ address's value after the retirement: `initial()` for a Chord document (as Q11 reads an absent singleton), so
1413
+ `watch.value` equals the source's value for every reader; for Durable, which has no value for an absent document,
1414
+ the last value with no ops. (4) Watching an absent address gives `undefined` (`notes.watch(key, ctx)` as Durable's
1415
+ `watchDoc`). The wire carries a `retired` list per hold entry in `state`, like `failed`. (3) **A per-key source in a
1416
+ factory:** `handle.at(key)` declares a singleton per key: loaded at activation with the object's singletons, it reads
1417
+ `initial()` while the key is absent, and is a state member, a derive source and an `on` source; its watchers learn
1418
+ of a retirement through the flag. Telling "absent" from "empty" is `await handle.get(key)` (`undefined`) or the
1419
+ flag. Not adopted: the key as an activation dependency (fails when absent, ends `gone` on retirement).
1420
+ - **Text is plain UTF-8, in code point order (owner, 2026-10-26; reviews `/tmp/text-review-{astra,fable,opus,sol}.md`):**
1421
+ supersedes S16's JavaScript order and W8's UTF-16 BLOB. `str()` columns are plain UTF-8 `TEXT` (`STRICT` tables;
1422
+ strings bound and read as they are), and text compares by Unicode code point on every backend (SQLite `BINARY`,
1423
+ Durable Objects, sqlite-wasm; Postgres `COLLATE "C"`, MySQL `utf8mb4_0900_bin` later). Written values and every
1424
+ query operand (`where` values, `inArray`, prefixes, cursors, `set` operands) of a `str()` column go through
1425
+ `toWellFormed()`: a lone surrogate becomes U+FFFD (what `TextEncoder` and Node's SQLite binding do), documented; a
1426
+ value containing NUL fails `invalid/args` (Node 24.4.1 truncates text at NUL on read; Postgres refuses it; content
1427
+ goes into `json(...)` columns, which escape both). A leading U+FEFF stays allowed: sqlite-wasm's reader drops it, so
1428
+ its backend reads values starting with `EF BB BF` as bytes and decodes them keeping it. The store exports
1429
+ `compareText(a, b)`; the memory backend uses it for ordering, ranges, `max`/`min` and `when`; a prefix's upper bound is
1430
+ the next code point after its last (portable); `substr` offsets count code points; a SQLite file must use the UTF-8
1431
+ encoding (checked at open). The guide states the rule: store order differs from JS `sort()` only for U+E000-U+FFFF
1432
+ against characters above U+FFFF; sort store text with `compareText`, page with the store's cursors, use `prefix()`,
1433
+ cut strings with `[...s].slice()` or `toWellFormed()`. Durable's strings in `StoreStorage` (`requestId`, kinds) keep
1434
+ Durable's JSON encoding in indexed columns, so Durable stays lossless. Rejected: CESU-8 in `TEXT` (a read path of
1435
+ special cases on best-effort SQLite behaviour; Postgres refuses it), CESU-8 in a BLOB, keeping W8.
1436
+ - **Slice 6 is split (owner, 2026-10-26):** 6a, the store with Chord's persisted documents and tables (backends,
1437
+ tables, query runner, writes, commit queue, outbox, documents engine, index definitions, migrations, export and
1438
+ import, `self.table`, persisted `self.state`, P3, the host's store option, `storage_failed`, W12, keyed watches and
1439
+ retirement); 6b, what builds on it (persisted jobs, `StoreStorage` with Durable's conformance suite, the Chord source
1440
+ over Durable's views). The API points (the host option's name, how a Chord transaction enters the commit step, the
1441
+ table API in `self.tx`, the Durable view source, the commit benchmark) are proposed in the 6a and 6b specs.
1442
+ - **The host's store and how it is opened (owner, 2026-10-26; slice 6a point 1):** the host option is `store`
1443
+ (`createHost({ …, store })`), not `storage`: it is the store itself, and in 6b Durable's `Storage` is `StoreStorage`
1444
+ over the same store. One entry point, `openStore(backend, ctx)`, with `sqliteBackend(db)` over any `SqliteDatabase`
1445
+ (Node through `chord/sqlite/node`, Durable Objects, sqlite-wasm later) and `memoryBackend()` for tests; the backend
1446
+ interface stays internal; `importStore(backend, archive, ctx)` takes the same values. `openStore` runs the store's
1447
+ own migrations, checks the file is UTF-8, and reads the head, the definitions and the id counters once. The app owns
1448
+ the store's lifetime: the host never closes it (in 6b the host and the Durable Session share it); the app calls
1449
+ `store.close()` after `host.close(ctx)`; on `storage_failed` the store stops, the host closes, and the app closes or
1450
+ reopens. No `openNodeStore(path)` shortcut.
1451
+
1452
+ ```ts
1453
+ const store = await openStore(sqliteBackend(await openNodeSqliteDatabase(join(dir, "session.sqlite"))), ctx);
1454
+ const worker = createHost<P>({ address: Session({ id }), principal, store, onError });
1455
+ …
1456
+ await worker.close(ctx);
1457
+ await store.close();
1458
+ ```
1459
+ - **A Chord transaction enters the commit step as Durable's does (owner, 2026-10-26; slice 6a point 2: "if this is
1460
+ Durable's proven flow we should use it here too"; checked against Durable's `#runCommit`):** the object's line ~~(and
1461
+ the facet's)~~ (facet documents removed the same day) is held from the callback until the step returned; an abort or error before the step discards
1462
+ everything; then every draft is prepared, `checkpointWhen` picks base or delta per persisted document, and
1463
+ `await store.commit(writes, adopt, ctx)` runs without the caller's abort signal (once admitted, a commit runs to its
1464
+ end). `adopt(seq)` is one `host.batch.commit(...)` adopting the persisted documents, the in-memory documents and the
1465
+ staged changes together (Chord's addition: Durable has no in-memory documents; adopting them apart would show half
1466
+ a transaction, or publish an in-memory change of a commit SQLite then refused). An error before `COMMIT` returned
1467
+ rejects the transaction with nothing adopted; an error during or after `COMMIT`, or a failed `adopt`, is
1468
+ `storage_failed` (the commit stays published if it was adopted). A transaction with only in-memory writes never
1469
+ enters the queue and commits synchronously as before (W12): no extra await or promise.
1470
+
1471
+ ```
1472
+ line -> fn -> (abort/error: discard) -> prepare, checkpointWhen
1473
+ -> await store.commit(writes, adopt) // queue; BEGIN; revision, version, rows, outbox, head; COMMIT
1474
+ adopt(seq) = host.batch.commit(() => { persisted; in-memory; staged })
1475
+ -> release line(s) -> settle staged -> drafts revoked
1476
+ ```
1477
+ - **Facet documents are removed; persisted data stays in objects (owner, 2026-10-26; slice 6a point 5; reviews
1478
+ `/tmp/facet-persist-review-{astra,fable,opus,sol}.md`):** cut C16 accepted (supersedes its rejection in "Slice 1
1479
+ approved"): a facet has no documents (`FacetSelf.state` goes), so no facet line, no lock order across two lines, no
1480
+ `Documents.facet` and no `Transaction.#facetLine`; a served object's transaction covers only its own object. Shared
1481
+ plugin state lives in a `local` store object with empty params, reached through its methods (its own failure domain,
1482
+ R18 recovers its dependents). Point 5 (persisted facet data) is moot: persisted documents and tables stay only in
1483
+ `local` objects with empty params (P3 unchanged). Why: the four reviewers found no stated reason in the history (the
1484
+ frozen kernel let facets persist; P3 was written for objects only), but a facet cannot fail at runtime (slice 2: a
1485
+ facet setup or `initial()` that throws rejects `load`, closing the host; C5 was rejected because a plugin's always-on
1486
+ parts would share one failure domain), and persisted data fails at runtime (fingerprint mismatch, throwing `migrate`,
1487
+ a newer version after a rollback, no store on the host): one plugin's stale document would close the worker, or need
1488
+ a second failable lifecycle for facets. In-memory facet documents had no real use either: none of p2's 46 `self.state`
1489
+ calls is in a facet; their only ability, one transaction over an object's and its facet's documents, is a
1490
+ cross-owner transaction, which the design rejects elsewhere (§9 "Rules across owners"). Other findings kept for a
1491
+ later facet lifecycle: reads of shared tables need the writer's line; an owner string `facet:<plugin>` collides with
1492
+ type names. Built in slice 6a (carry-over).
1493
+ - **The table API in `self.tx` (owner, 2026-10-26; slice 6a point 3):** (a) the first argument is the table handle
1494
+ from `self.table(Def)`, never the definition (the handle belongs to one object, so `tx` refuses another object's
1495
+ table, as it refuses its documents); (b) keys are objects with exactly the key columns, `{ path, entry }`, not tuples
1496
+ (two swapped `str` columns of a tuple are silent at the type level; other or missing fields fail `invalid/args`, a
1497
+ trust boundary); (c) reads are async (`await tx.get(t, key)`, `tx.page(t, q, limit, { after? })`, `tx.rows(t, q)`),
1498
+ writes synchronous and buffered like a draft's ops (`tx.put(t, row)`, `tx.update(t, key, set)`, `tx.delete(t, key)`,
1499
+ `tx.updateWhere(t, where, set)`, `tx.deleteWhere(t, where)`): they check the row and stage it, no I/O, no line, so a
1500
+ bad row throws at its own call; the buffer goes into the commit step's backend transaction or is dropped on failure,
1501
+ like aborted drafts (writing at once would hold SQLite's one write transaction for the whole callback). As Durable
1502
+ (`setTask`, `settleSubmission` synchronous, reads async) and Chord's `tx.start`. (d) No write shorthands on the table
1503
+ handle (`edits.put(row, ctx)`) until real code repeats the one-liner. Outside a transaction the handle reads
1504
+ committed state: `edits.get(key, ctx)`, `edits.page(q, limit, ctx, { after? })`, `edits.rows(q, ctx)`,
1505
+ `edits.count(q, ctx)`. `tx.get` exists for tables only (documents keep `tx.draft`).
1506
+
1507
+ ```ts
1508
+ self.tx(async (tx) => {
1509
+ const old = await tx.get(edits, { id });
1510
+ tx.put(edits, { ...row, at: now });
1511
+ tx.updateWhere(edits, (e) => prefix(e.path, from), { path: replacePrefix(from, to) });
1512
+ }, ctx);
1513
+ ```
1514
+ - **Chunks of `rows` migrations (owner, 2026-10-26; slice 6a point 6):** 100 rows per commit, a fixed constant.
1515
+ Measured on slayer (`node:sqlite`, WAL, 20,000 rows of about 3 KB, a primary-key `UPDATE` plus an outbox `INSERT`
1516
+ per row): the total is flat from 100 to 1000 rows per chunk (about 15 µs per row); the step's median is 0.94 ms at
1517
+ 100, 2.4 ms at 250, 9 ms at 500, 14.5 ms at 1000, 32 ms at 2000, and every other owner's commit waits for it. The
1518
+ store benchmark measures it again with the real store. Can change later (owner), e.g. a table definition naming its
1519
+ own chunk size; not built until a case needs it.
1520
+ - **`lastSeq` instead of "head" (owner, 2026-10-26):** the seq of a store's last commit is called `lastSeq`, never
1521
+ "head" (Durable's "head" is a conversation's head: head markers, `findLatestHeadMarker`). Every "head" of the store
1522
+ in earlier entries (S3, S4, S5, S9, "Slice 6 first decisions" (4), "The host's store and how it is opened", the
1523
+ commit-step trace) reads `lastSeq`; a replica's "applied head" is its `lastSeq`. Renamed in design.md, the store
1524
+ book and the slice 6a spec.
1525
+ - **Export archives are operations, the encoding is pluggable (owner, 2026-10-26; slice 6a point 7):**
1526
+ `store.export(ctx)` yields `AsyncIterable<ArchiveRecord>`: the state after its `lastSeq`, written in the outbox's
1527
+ operations as one synthetic batch from 0 to `lastSeq` (a manifest `{ kind, format: 1, lastSeq, schema }`; `table`,
1528
+ `index`, `put` per row; `document.create` per live record with its base, plus its later `document.change` records
1529
+ for `history: "full"` ones (Durable, 6b); an `end` record with counts). Retired documents, blobs, the outbox and sync
1530
+ state are not in it. `importStore(backend, records, ctx)` refuses a non-empty backend, an unknown format, records out
1531
+ of order and a missing or wrong `end`, and applies everything in one backend transaction with the replica's applier
1532
+ (import, bootstrap and batch apply are one code path). Export holds the commit queue until the iteration ends,
1533
+ throws, is closed or `ctx` aborts (a slow consumer stalls commits; the guide: export to a local file, then upload;
1534
+ snapshot reads on a second connection only if exports get large, S4). The encoding is pluggable, as the link's
1535
+ codecs: the store yields and takes records only and ships plain JSONL (`encodeJsonl`, `decodeJsonl`) with no size
1536
+ optimizations; gzip or a Delta-style dictionary encoder come later as formats without touching the applier (owner:
1537
+ "then it doesn't matter that the first thing we do is plain JSONL"). Sync is not export: it ships outbox batches as
1538
+ commits happen (S5); a full archive is needed only where no history can be replayed (a session moved as a file, a
1539
+ split-off session, a replica restored after divergence). Open for the sync layer: a session that never syncs keeps
1540
+ every outbox record (trimmed only after Radius acknowledges it), so its outbox grows without bound.
1541
+ - **State and job names are restricted too (2026-10-27, slice 6a fix round, under the owner's delegation):**
1542
+ `defineState` and `defineJob` names follow the pattern of object type and table names, `[a-z0-9@/._~-]+`, lowercase,
1543
+ checked at declaration: they become the documents engine's `kind` and keys of job records, where NUL or lone
1544
+ surrogates would be truncated or replaced by SQLite.
1545
+ - **Slice 6a build reviews (2026-10-27, under the owner's delegation to the reviewers' majority):** two rounds of four
1546
+ build reviews (`/tmp/s6a-build-review{,2}-{astra,fable,opus,sol}.md`), merged into two fix lists
1547
+ (`/tmp/s6a-fix{1,2}-brief.md`, 28 and 16 items) by majority, a point raised by one reviewer taking that reviewer's
1548
+ recommendation. Rules they settle beyond fixes of the spec: (1) **members across attempts** (**superseded in part** by "References are the only indirection": the reference, not a member source on the object, follows the object across instances): a local object's state
1549
+ member is followed by its references and by other objects' derives through one member source per object and member
1550
+ name; when a new attempt goes live every member re-binds by name (one diff frame from the last value, consumers'
1551
+ derives recompute, a member the new variant lacks fails `invalid/member`), as a remote reference re-holds and gets a
1552
+ fresh snapshot (Opus B1); supersedes slice-6a §1.2's "derive members' watches end with their attempt". (2) While an
1553
+ object that was live is failed or activating again, same-host reads and new watches return the last values and get
1554
+ no frames (was `not_ready` while activating again). (3) A live failure freezes the members only after the commits
1555
+ admitted before the seal adopted and published once; a retry's factory and hydration wait for that, so no old commit
1556
+ lands in a new attempt. (4) Re-exposed and forwarded members freeze at the re-exposing object; the provider's source
1557
+ stays untouched. (5) A transaction cut at the seal while queued for the step rejects `released` at once; line jobs
1558
+ queued before the seal are refused. (6) The seq, document id and revision counters stop at
1559
+ `Number.MAX_SAFE_INTEGER` (`storage_failed`). (7) `evolveTable` checks each edit when it is made; import checks
1560
+ `meta` with the text rules; an export never started rejects `released` after `store.close()`. (8) The migration
1561
+ yield between chunks: `setImmediate` where the runtime has it, otherwise a `MessageChannel` message, never
1562
+ `setTimeout(0)` (clamped to 1 ms in Node; a message costs about 0.27 ms per yield there: a 10,000-row migration takes
1563
+ 70 ms against 100 ms and 191 ms); supersedes round 1's `setTimeout(…, 0)`. (9) Round 1: keyed keys follow the
1564
+ `str()` rules (the empty key valid); same-host reads of a failed live object return the last value; stuck handlers'
1565
+ counts, references and presence released at teardown; refusing a new job version on a live object kept for 6a
1566
+ (slice 7 carry-over); in-memory keyed slots migrate on access. Measured against HEAD (`benchmarks.md`): calls 1-3%
1567
+ faster (19 promises per call against 20, guarded), watches through a reference at HEAD's level, a document member's
1568
+ read through a reference 0.25 ns slower, host setup about 3% slower.
1569
+ - **Derives heal on a re-bind (owner, 2026-10-27: "a all the way, self healing is good"; Opus's F2 in the third
1570
+ build review):** a consumer's derive that failed on a provider member's earlier error recomputes when that member
1571
+ re-binds to the provider's new attempt (and every derive downstream of it), so it recovers once its source works
1572
+ again; watches over it get the recovered value as a fresh frame. Before, a failed derive ignored later changes (as at
1573
+ HEAD) and recovered only when its own object restarted.
1574
+ - **Failures are watch calls; derives heal on every change (owner, 2026-10-27: "B", "beautiful, let's do it"):**
1575
+ extends "Derives heal on a re-bind". (1) A failed derive recomputes on every later change of its sources (and on a
1576
+ re-bind), so it heals as soon as it gets a value it can compute. (2) A watch stays open through a recoverable failure:
1577
+ the listener gets one tagged call `{ kind: "failed", error }`, and the next good value as `{ kind: "value", value,
1578
+ ops: [["r", value]], retired }`; the call is a tagged union (`WatchCall<T>`: `value | failed`; native watches
1579
+ `NativeWatchCall<T>`: `{ kind: "value", value } | { kind: "failed", error }`), so the compiler makes every listener
1580
+ handle the failure (an optional `error` field would let `({ value }) => render(value)` render the old value).
1581
+ Supersedes the listener bags `({ value, ops, retired }, ctx)` and `({ value }, ctx)` of "One retirement mechanism"
1582
+ (1). (3) Applies to derives, replica members a hold lists as failed, a member a new variant lacks (it comes back with
1583
+ a later attempt), registry values whose `compose` throws (slice 8). (4) A watch ends only for good: its owner ended,
1584
+ `stop()` or a cancelled context, a throwing listener, `storage_failed`; Durable's watches fail only for such causes
1585
+ and only take the new shape. (5) `self.on` handlers are not called for failures (a reconciler acting on an error call
1586
+ would act on the old value); the routine waits for the next good value; the failure is reported where it happens:
1587
+ a derive's owner reports `derive_failed { object, member?, error }` once per failure episode (the first failure after
1588
+ a value or after creation; a heal ends it), a new `HostError` kind (2026-10-27, slice 6a fix round 4: there was no
1589
+ report for a throwing derive besides a routine's `routine_failed` on its ended watch). One failure call per
1590
+ episode (a value turning into a failure), not one per source change while failed; a hold keeps only a member's
1591
+ newest pending failure. Watching a failed source (2026-10-27, slice 6a fix round 5, under the rule "the routine
1592
+ waits for the next good value"): `watch(ctx)` opens at the last good value and delivers one `failed` call first;
1593
+ a source that never had a good value rejects `watch()` with its error; `self.on` on a failed source waits for the
1594
+ first good value instead of ending.
1595
+ (6) `ref.state` stays a separate watchable of reachability (`loading | live | failed | released`; `ended` since
1596
+ "One vocabulary, groups 2 and 3"), same words: a
1597
+ value's watch says whether it can be computed, `ref.state` whether its object is reachable (values stay readable
1598
+ while loading). No new leaks: an open watch on a failed source holds what it held before and ends with its owner,
1599
+ reference or context (GC test). Built: the shared `src/watch.ts` upstream in pi first (Durable's listeners adapt),
1600
+ then slice 6a (points 1-3 except registries; registries in slice 8).
1601
+ - **One vocabulary, group 1: the shared watch (owner, 2026-10-27: "yes"; vocabulary audit `/tmp/vocab-audit.md`,
1602
+ A1, A3, A8):** built in the same upstream change as the failure calls. (a) `WatchEnd` is tagged by `kind`, not
1603
+ `reason` (`end.kind === "failed"`). (b) A watch whose context is aborted ends `aborted`, not `cancelled` (one word
1604
+ for a requested end, as `AbortSignal`, the `aborted` code and `JobError.aborted`). (c) Durable's synchronous
1605
+ observers take the watch's shapes: `advance(call, ctx)` with a `WatchCall`, `end(end: WatchEnd)` (Durable never
1606
+ sends a failure call: its failures are final). (d) A member a new variant lacks behaves the same locally and over a
1607
+ link: a failure call, the watch stays open (today local `failed`, remote `ended`). (e) Durable's `FileChange` is
1608
+ tagged: `{ kind: "paths", paths } | { kind: "overflow" } | { kind: "failed", error }`. The audit also found that
1609
+ "Slice 6a small items" (1) is wrong: Durable's `Storage.close(context)` takes a context (decided with group 2).
1610
+ - **One vocabulary, groups 2 and 3 (owner, 2026-10-27: "lgtm"; Durable's stored formats stay the same):**
1611
+ Chord v2 (built in slice 6a): (1) `ref.state` `{ kind: "ended", error }` replaces `released`; the `released` code
1612
+ gets reasons `ended` (object ended), `closed` (host, store, native), `settled` (`ctx.get` after its call),
1613
+ `version` (an old handle), `discarded` (a transaction cut at the seal); a holder's own release stays `released`
1614
+ without a reason. (2) Internal results are `done | failed { error }`; the attempt's `Outcome` is `Activation`,
1615
+ the jobs' `Ended` is `Settled`. (3) `DocumentSlot.remove` is `retire`; `StoredChange` is `create | change |
1616
+ retire`. (4) The wire's `cancel` message is `abort`; a native's close reason `stop` is `close`; ~~`job.abort(id, ctx)`
1617
+ returns `"marked" | "terminal"` (Durable's words)~~ (**superseded**, owner 2026-10-27: Durable's `abortTask` stores a
1618
+ mark and joins the run, Chord's 6a abort only fires a signal in memory; `job.abort(id)` stays in 6a, and becomes async
1619
+ and stored with persisted jobs in 6b, its result chosen there). (5) Homonyms: the store's `Staged` is `StagedWrite`, its `Phase`
1620
+ `WriteOrder`, the host's `KeyedMember` `KeyedBinding`, `KeyedMembers.hold` `pin`, `Presence.origin` `incarnation`.
1621
+ (6) Context last: `runState.change(fn, ctx)`, `host.route(Kind, options, ctx)`, `store.close(ctx)` (as Durable's
1622
+ `Storage.close(context)`; supersedes "Slice 6a small items" (1)'s "no context"). (7) `failed` and `gone` carry the
1623
+ wire error format (`WireError`). (8) `HostError`: `failure` where the value is a `FailureCause`, `error` otherwise;
1624
+ positions `{ kind: "on" | "onLive" | "native", position }`. (9) Store types as Durable's: `history: "latest" |
1625
+ "rewindable"`, pages `{ items, next }`, a singleton's address has no key (`""` only in the column). (10) The
1626
+ listener parameter is `call`; holds `served | relayed`; `put.v` is `version`. Durable's public API (pi upstream,
1627
+ with group 1): (11) `onReport(report)` with `report.kind` and `report.error`; `session_failed` is `storage_failed`.
1628
+ (12) `subscribeCommits` is `observeCommits` with the observer shape (`{ value, detach }`); a file watcher stops with
1629
+ `stop(ctx)`. (13) `CompactionFailure.reason` and `CompactionStatus.reason` are `trigger` (`pi.live`'s definition
1630
+ bumps its version with a `migrate`). (14) ~~One word for a definition that cannot take the stored state:
1631
+ `incompatible`~~ (**superseded**, owner 2026-10-27: `task_too_old` is stored in orphaned outcomes and settlements, and the
1632
+ report's `incompatible_task` covered two cases): the `task_outdated` report uses the scheduler's own words,
1633
+ `missing_task | task_too_old | migration_failed` (newer without `migrate`: `migration_failed`); Chord's jobs name
1634
+ their blocked reasons in slice 7.
1635
+ (15) `TaskInspection` uses `TaskState`'s words (`ready` is `pending`). (16) `DocumentCommitChange` is tagged
1636
+ (`{ kind: "retire" }` instead of `value: null`). Not done: Durable's stored tags (`status`, `type`; group 4), Durable
1637
+ throwing Chord's codes (6b), one retirement value ("One retirement mechanism" (2) stands).
1638
+ Added (owner, 2026-10-27, "lgtm"): Durable's `SessionEnd` and the `ReportFrom` argument of `runtime.report()` are
1639
+ tagged by `kind` too (neither is stored).
1640
+ - **Chord v2 and its docs live in pi upstream (owner, 2026-10-27: "we can put chord v2 in upstream", "the docs can
1641
+ all be public, and in upstream"):** `packages/chord/src/next`, `src/next-node`, `test/next`, `test/next-node` and
1642
+ `docs/next` moved to pi `main` (`93029a0f1`), not exported until v2 has an entry point (experimental, noted in the
1643
+ changelog); the decision log moved from `apps/p2/docs/redesign-decisions.md` to `packages/chord/docs/decisions.md`;
1644
+ the root `AGENTS.md` carries the "Chord v2 Code Style" section. From now on every slice and every design doc change
1645
+ goes to pi first (worktree of `upstream/main`, check, tests, push, CI, merge into `private`); only `apps/` stays
1646
+ private.
1647
+ - **Slice 6b D1: `StoreStorage` and `durableView` live in Durable (owner, 2026-10-27: "they are only used if durable
1648
+ is used"):** in Durable's own `src` (an ordinary pi module, now that Chord v2 is upstream), importing
1649
+ `@earendil-works/chord/next` (Chord's `./next` export comes with them; now the root export). No package cycle: Durable already depends on
1650
+ Chord, Chord imports nothing of Durable (S13). Durable's conformance suite runs in Durable's tests and CI.
1651
+ - **Slice 6b D2: Durable's records are ordinary declared tables of the store (owner, 2026-10-27: "a, that's the whole
1652
+ purpose of this slice"):** `StoreStorage` declares conversations, entries, tasks, submissions and `record_ids` as
1653
+ `defineTable`s, writes them with `put` (Durable replaces complete records), and reads them with the store's queries;
1654
+ the outbox, export, import, the applier and later `evolveTable` versions handle them as every table. The partial
1655
+ head index becomes a table `entry_heads` (one row per entry with a head; build, 2026-10-27, faster than an index on
1656
+ `(conversation_id, has_head, id)`); indexed strings that must stay lossless keep
1657
+ Durable's JSON encoding.
1658
+ - **Slice 6b D3: one id counter per store (owner, 2026-10-27: "a"):** the store's counter (`next_document_id`, renamed
1659
+ `next_id`; the archive manifest's `nextDocumentId` becomes `nextId`) numbers every id in the store: Durable's
1660
+ `mintId` (conversations, entries, tasks, submissions, documents) takes from it in memory, explicit ids in Durable's
1661
+ writes raise it (an entry committed with id 100: the next mint is 101), and Chord's commit step takes its documents'
1662
+ ids from it; it is persisted as a high-water mark with every commit. `record_ids` stays for Durable's type check of
1663
+ an id ("already belongs to entry"); supersedes "Slice 6 first decisions" (3)'s `coalesce(max(id), 1) + 1` and 6a's
1664
+ "document ids monotonic with seq". Durable's own ids still rise in its creation order (its Session line serializes
1665
+ its commits and mints inside them), which its scans, transcripts and windows rely on; Chord's ids are gaps between
1666
+ them, as the five record types already leave. A minted id never committed may come again after a reopen (S10).
1667
+ - **Slice 6b D4: the store has Durable's Storage semantics (owner, 2026-10-27: "agreed"; supersedes in part "Store
1668
+ errors: Durable's rule" and slice-6a rule 10's "an expression result outside it fails the step, which is
1669
+ `storage_failed`"):** Durable has two levels: its `Storage` rolls a failed commit back and stays usable (the
1670
+ conformance suite tests it), its Session fails on any commit error (`GuardedStorage`). "Store errors: Durable's
1671
+ rule" had put the Session's level into the store. Now: a step that fails before `COMMIT` rolls back and rejects that
1672
+ commit only (the step's code runs in the transaction callback, so an error there is known to come before `COMMIT`);
1673
+ the store stops only when the outcome is unknown (`COMMIT` or `ROLLBACK` failed), when `adopt` threw, or on a read's
1674
+ backend error. Owners react as Durable's Session: Durable's Session fails (unchanged); a Chord object whose commit the
1675
+ step refused fails live (`object_failed`, `broken`: memory and disk disagree); an `int` `CHECK` failure (a plugin's
1676
+ value out of range) fails only that transaction (`invalid/args`). `StoreStorage` runs Durable's commit checks in the
1677
+ step, where `SqliteStorage` runs them: no special case. Built in 6b (the store's step and object failure).
1678
+ - **Slice 6b D5: persisted run records are documents of their object; one harness per store (owner, 2026-10-27:
1679
+ "sure"):** each unsettled run's record is a keyed persisted document of its object in the engine's namespace
1680
+ `chord.jobs` (owner the object's owner string, kind the job name, key the run id); Chord sets that namespace, a plugin
1681
+ never does, so nothing collides; the state document stays an ordinary document of the run; `finish` deletes the
1682
+ record and retires the state (Q9), so only unsettled runs are stored and in memory; activation loads them through the
1683
+ engine's key listing and a load per run (no new engine read). Durable's tables are named under the type name
1684
+ `pi.durable` (`"session:pi.durable:entries"`; type names are unique per app). One `StoreStorage` per store: a second
1685
+ one on the same store is refused (`invalid/args`), as one host per store (rule 7). Rejected on the way: a table per
1686
+ job, a store-wide runs table, `$`-marked internal names (owner: "if our abstractions can't cleanly support this
1687
+ without $ magic, it's a bad abstraction").
1688
+ - **Slice 6b D6: persisted jobs restart as Durable's tasks (owner, 2026-10-27: "yes, same rule for chord"):** at
1689
+ activation a run recorded `running` gets `restarts + 1` (stored) and resumes per its `resume`; a clean close writes
1690
+ its stopped runs back so the next activation counts nothing (as Durable's `writeAtClose`); a `ctx.state.change`
1691
+ resets the count; at five in a row the run ends `failed { kind: "restart_limit", restarts }` (a new `JobError`),
1692
+ `finish` runs, `job_interrupted` is reported. Supersedes design §15's `blocked { restart_limit }` (no parking, as
1693
+ Durable since #11139).
1694
+ - **Slice 6b D7: `job.abort(id, ctx): Promise<"marked" | "terminal">` (owner, 2026-10-27: "yes"):** Durable's words
1695
+ and behaviour (`abortTask`): a running run's abort is stored and its signal fired, resolving `"marked"` once `run`
1696
+ settled; a finishing run is only marked; a blocked run is dropped (`"marked"`, as Durable's orphan); no unsettled run
1697
+ with that id resolves `"terminal"` (Chord keeps no settled runs, Q9). Supersedes "job.abort stays until 6b".
1698
+ - **Slice 6b D8: deferred persistence of a run's state, per change (owner, 2026-10-27: "agree"):** `ctx.state.change(fn,
1699
+ ctx, { persistence: { kind: "deferred", withinMs? } })`, Durable's `CommitPersistence` shape (default 1,000 ms):
1700
+ adopted and published at once, stored with the next immediate change, before the outcome is recorded, at close, or
1701
+ after `withinMs`; a crash loses what is not stored. Only run state documents for now.
1702
+ - **Slice 6b D9: `self.follow(attach)` follows Durable's views (owner, 2026-10-27: "sounds good"):** a generic Chord
1703
+ primitive, no `durableView` module in Durable: `attach(observer, ctx)` returns `Promise<{ value, detach }>`
1704
+ (Durable's `Observation`; its observers have Chord's watch shapes since pi `5f312dac4`). Chord makes the observer,
1705
+ attaches during activation (early calls held, `value` the start value), detaches at the attempt's end; calls inside a
1706
+ batch mark the source in the step, calls outside one are grouped into one batch on the next microtask; a rejected or
1707
+ `undefined` attach fails the activation as a failed dependency; Durable's `failed` end ends the watches `failed`.
1708
+ Returns a `Watchable` (state member, derive source, `on` source). Supersedes the carry-over's `durableView` and D1's
1709
+ `durableView` part.
1710
+ - **Slice 8 is "Registries and `host.root`"; the harness object is p3's (owner, 2026-10-27: "we want to get chord done
1711
+ this week. p3 needs design and is next week. just need all chord primitives in"):** Chord builds the primitives the
1712
+ harness needs (registries, `self.writable`, `retryOn`, a `RegistryReader` for Durable, `host.root`, `chord(fn,
1713
+ context)`); the harness object (`Harness.open` over `StoreStorage`, `resume()` after the start barrier, the models,
1714
+ environments and extensions registries) is p3's base code, shown in Chord only as an example or test. Plan: Chord v2
1715
+ (6b, 7, 8, 9) this week, p3's design next week.
1716
+ - **Slice 6b R1: retirement per record by `history` (owner, 2026-10-27: "sgtm"; spec review, all four reviewers
1717
+ found that Durable keeps retired records):** the engine's `history` gets three values, set by the adapter at
1718
+ creation and applied at retirement: `none` (Chord's documents and job records: older revisions dropped at a new base,
1719
+ row and revisions deleted at retirement, 6a's behaviour, until now called `latest`), `latest` (Durable's session, task
1720
+ and latest conversation documents: older revisions dropped, the row kept with `retired_at` at retirement, so reads at
1721
+ a point find it), `rewindable` (everything kept). `retireDocument` reads the record's `history`; no flag, archives
1722
+ and replicas carry it. Rejected: keeping every retired record (one rule, but a dead row per finished job run, two
1723
+ with its state, and per retired keyed document, that Chord never reads).
1724
+ - **Slice 6b R2: no host kind in store names (owner, 2026-10-27: "what use is host in table names anyways? seems
1725
+ stupid. no host"; supersedes the host-kind part of W7, "Restricted names" and "Slice 6a's three code questions" (3)):**
1726
+ tables are `"<object type>:<table>"` (`"notes.store:edits"`, `"pi.durable:entries"`), document owners
1727
+ `"<object type>"`; a store belongs to one host (rule 7), so the host kind told nothing, and `StoreStorage` and stores
1728
+ used before or without a host get the same names. W7's rule stays: no host instance in a name. Changed in 6a's code
1729
+ in 6b; the applier checks two parts.
1730
+ - **Slice 6b: owner names and claims, no `pi.durable` (owner, 2026-10-27: "seems reasonable"; supersedes D2's and D5's
1731
+ `pi.durable` type name and D5's "one `StoreStorage` per store" rule):** every table and document in the store belongs
1732
+ to an owner name, claimed once per store. A `local` object with empty params (P3) claims its type when it first
1733
+ declares persisted data (`self.table`, persisted `self.state`, persisted `self.job`); `StoreStorage.open(store, owner,
1734
+ ctx)` claims `owner`; a second claim of a name is refused (`invalid/args`): a second harness, or a plugin type that
1735
+ meets the harness's name. `StoreStorage` declares its own tables (`entries`, `tasks`, …) through the store, the path
1736
+ `self.table` takes, named `"<owner>:<table>"`, and writes its documents under `owner` in the engine's namespaces
1737
+ (Durable's scopes; corrected 2026-10-27, it was "`durable`"). In p3 the harness object passes its own type (`StoreStorage.open(store, Harness.name, ctx)`; base code gives
1738
+ the store to the core facet); Durable's conformance suite passes a test name. The owner name plays the role of
1739
+ `SqliteStorage`'s `prefix`, checked by the store. Objects never see the store: their API writes to the host's store
1740
+ under their type; a host without a store fails an object declaring persisted data (`broken`, 6a). Rejected: an owner
1741
+ handle (`self.storage`), an indirection that only added protection against trusted code.
1742
+ - **Slice 6b R3: a seal is a clean stop for persisted runs (owner, 2026-10-27: "a"):** a persisted run cut off by
1743
+ its attempt's seal while the object continues (a live failure, then a retry) is written back stopped, and the next
1744
+ live attempt resumes it per its `resume` (rerun or abandon) without counting a restart, as after a clean close (D6).
1745
+ In-memory runs keep 6a's rule (`failed { aborted, reason: "restart" }`, finished on the next live attempt).
1746
+ - **Slice 6b R4: a follow source's end (owner, 2026-10-27: "yes"):** Durable ends an observer `failed` (its Session
1747
+ failed: the object that opened Durable fails `transient`, its retry reopens its natives, Durable included, and attaches
1748
+ again, as Durable's own recovery is to reopen from Storage) or `ended` (the observed owner ended normally, a task
1749
+ document's task: final, the member keeps its last value, its watches end `ended`).
1750
+ - **Slice 6b: claims per table and per document namespace (owner, 2026-10-27: "agree"; refines "owner names and
1751
+ claims"; Fable's second review N2):** what can collide is claimed once per store: a table by its SQL name, documents by
1752
+ `(owner, namespace)`; so the object that opens Durable can also declare its own persisted data under the same owner
1753
+ name, and two harnesses can share a store under two owner names (owner: "seems like they could"); a second
1754
+ `StoreStorage` on the same owner is refused by its tables. "One harness per store" is no rule, only how p3 uses it.
1755
+ - **Slice 8 D1: a registry's token is shared, its provider serves it (owner, 2026-10-27: "b", "yes"):** the token
1756
+ `defineRegistry<C, V>(name, { version })` carries identity only (bundled by every contributor, as a contract); the
1757
+ defining plugin's facet provides it with `host.serve(R, { compose, accepts? })`, as it serves object types. One rule
1758
+ for versioned things: a shared definition (object types, registries) keeps its adapters with the provider; a
1759
+ definition of one owner (documents, tables, `json(...)` columns, jobs) keeps them in itself. Refines Q8's "one
1760
+ authoritative declaration" (the serve is it) and design §17's example.
1761
+ - **Slice 8: registry failures (owner, 2026-10-27: "ok", "agree"):** contributions never fail a registry: a
1762
+ watchable contribution that fails is left out of the table and reported (`contribution_failed`), back once healed;
1763
+ a throwing `compose` (the owner's bug) is reported (`registry_failed`), its watches get a `failed` call, and `.value`
1764
+ keeps the last good composition (before any, it throws). Derives keep `.value` throwing while failed: state must match
1765
+ its sources, an extension point must stay available.
1766
+ - **Slice 8 D2 and D3 (owner, 2026-10-27: "yes"):** D2: a `retryOn` source retries a failed object only on a `value`
1767
+ call; a `failed` call does nothing. D3: `host.root<T>(fn, context, { principal? })` gives `fn` a handler's call
1768
+ context (the host's principal or the given one, this host's incarnation, `context`'s telemetry and signal linked
1769
+ until `fn` settles, `ctx.get`); at settle or abort the context is sealed (`ctx.get` rejects `released`, its references
1770
+ released, their calls reject), so an uncooperative `fn` fails at its next use of Chord; before `load` its gets park;
1771
+ after `host.close` it rejects `released`. `chord(fn, context)` is p3's helper. Closes design §26 #25.
1772
+ - **Slice 8 S1: a Session close fails Durable's observers (owner, 2026-10-27: "a"; spec review, all four reviewers):**
1773
+ Durable ends an observer `failed` (error "session closed") when its Session closes, and `ended` only when the
1774
+ observed owner ended (a task document's task); refines R4 (Durable change, upstream). So a follower of a harness
1775
+ reached through a registry fails `transient` when that harness closes, and its retry follows the new one. A
1776
+ `retryOn` wake that arrives while the object activates is kept and retries it at once if that attempt fails.
1777
+ - **Slice 8 S2: registry readers of an older version (owner, 2026-10-27: "a"):** as contract state, the provider maps
1778
+ back: `host.serve(R, { compose, accepts: { [old]: { contribution?: (old) => current, value?: (current) => old } } })`;
1779
+ a reader bundling an older token gets the composed value mapped to its version (computed once per composition and
1780
+ version, only while such a reader exists); without a `value` mapping, or for a reader newer than the provider, its
1781
+ value is failed `contract_mismatch` (reported); such a reader fails `broken`, not `transient` (nothing heals it before
1782
+ a reload, which retries broken objects; build review 2026-10-27).
1783
+ - **Slice 8 S3: `self.registry` is a dependency, as `self.get` (owner, 2026-10-27: "isn't self.registry meant to wait
1784
+ like self.get?"; my spec had it as a plain read, a mistake against design §6, §7 and §17):** activation waits until
1785
+ the registry has a value (served and composed once; an empty table composes too, at the serve); after `load`'s
1786
+ setups a registry without a value cannot get one before a reload, so a reader then fails at once `transient`
1787
+ (`unavailable/undeclared`) and is retried when the registry gets its first value (R18), as `self.get` of a type not
1788
+ served here fails at once (refined 2026-10-27 after the second spec review: no `readyTimeout` wait at start); once live, a later failure of the registry changes nothing (last good value).
1789
+ - **Slice 7b's open points, as recommended (owner, 2026-10-29: "yes"; brief `slice-7b/`):** (1) the API is
1790
+ `host.reload(facets, ctx)`, by plugin name (added, changed; a plugin missing from the list is removed), resolving with
1791
+ a report of what each object did, rejecting with the code error and nothing changed; `load` stays the first call. (2)
1792
+ A `writable` across a pure swap goes back to its initial value (the new factory makes a new one; no positional
1793
+ matching). (3) The rollout across hosts is the app's: it calls `host.reload` on each host in order (server, workers,
1794
+ clients); Chord guarantees per host (staging, commit, `installing`, rollback). (4) No install watchdog: `reload`
1795
+ takes a context and the app passes its deadline. (5) The loader lives in Chord (`chord/next/loader`, slice 7c). (6)
1796
+ Chord ships the app kit as optional subpaths: `loader` and `dev` (7c), `http` and `workers` (slice 10), `web` and
1797
+ `tui` (slice 11); telemetry (slice 9) last.
1798
+ - **Slice 7b's build decisions (owner, 2026-10-29, answering the builder; `slice-7b.md` §13):** a loaded plugin passed
1799
+ as the same facet object (`===`) is unchanged and untouched, any other facet value for a loaded name is changed;
1800
+ activating (not parked) and backoff-waiting objects of a reloaded plugin are retried; the report's `parked` covers
1801
+ swaps and retries (their first dependency check), restarts are reported at once; a swapped-out attempt is sealed
1802
+ once its calls settled (bounded by `closeTimeout`) and the swap ended, its job invocations finishing on their old
1803
+ code; a stuck old routine call is reported `drain_timeout` and fenced by the old routine calls' shared token; a swap
1804
+ to another variant sends holds a fresh `bound`; a native restart ends in-memory runs `aborted { reload }` while
1805
+ persisted ones follow 6b R3 (stopped, resumed per `resume`, no restart counted); staging is synchronous
1806
+ (`installing` tested from a staged factory); a removed object's member sources and pinned keys are handed to the next
1807
+ object of its identity (fix round 3: one hand-over per identity for removal, version refusal and an object ending
1808
+ while requests wait, `slice-7b.md` §13 item 11); keyed members' versions are built in 7b (L1's keyed edits and an accepts `keyed` section,
1809
+ decision "Keyed state members, local contracts only"). The builder's own choice, accepted by the owner (2026-10-29):
1810
+ adapted state members are derives per object and version, not per binding. (**Superseded in part** by "References
1811
+ are the only indirection": the hand-over of member sources and the per-version sources that outlive an object go.)
1812
+ - **References are the only indirection (owner, 2026-10-29: "go"; after seven fix rounds of 7b found one edge case after
1813
+ another in sources handed between objects):** supersedes in part 6a's "members across attempts" (one member source
1814
+ per object and member name) and slice 7b decisions 8 and 11 (member sources and pinned keys frozen and handed to the
1815
+ next object of the identity; per-version outward sources owned by (identity, version); `host/handover.ts`). The
1816
+ problem: a consumer's watch or derive over `ref.count` was attached to the referenced object's own document or derive
1817
+ (the reference was bypassed), so every change of what the reference points at needed a stable node somewhere else
1818
+ (`MemberSource` on the object, then sources that outlive the object and move to the next one). The rule: (1) a
1819
+ consumer's watches and derives over a referenced object's member attach through the reference's member, which passes
1820
+ the subscription straight through to the object's own document or derive (no extra hop on commits). (2) When what the
1821
+ reference points at changes (a new object instance after a factory re-run: reload, retry, restart; a new object
1822
+ after the type was removed and served again; another variant), the reference moves its subscriptions to the new one
1823
+ and sends one frame, a diff from the last value. (3) While it points at nothing, its members keep their last values
1824
+ (a pointer, no copy): while loading (a restart, a retry, a swap whose new instance is not live yet) its watches get
1825
+ no call; while failed (a live failure, its version refused, its type removed, a failed dependency) one `failed` call
1826
+ with the cause's error, and they stay open (replaces 6a's "no frames while failed"); a reference that never had a
1827
+ value reads `not_ready` while loading and its failure's error while failed, and its watches wait for its first values
1828
+ (a member the new variant lacks fails `invalid/member`, as before). Keyed watches and pins re-pin on each new instance the same way (the
1829
+ cross-attempt key sources of 6a go; a key keeps its last value until the new instance loaded it; a dependent's
1830
+ factory pin is still loaded before the dependent goes live). Once hydrated, a reference has last values for every state member of its bound variant, read or not: when it
1831
+ detaches it takes them at its version from the outgoing instance (through that instance's mapping for an older
1832
+ version), keeping no node of it. After a `failed` call, the next good value arrives as `[["r", value]]` even when
1833
+ unchanged; a watch opened while failed starts at the last value and gets the failure once. No source, mapping,
1834
+ document handle or pin of an instance outlives it through a reference: a detached reference keeps only plain last
1835
+ values and its failure; releasing it drops them and its bindings; stopping a keyed watch drops its pin. (4) The same rule over a link: a replica keeps its last values
1836
+ readable while its hold is failed. (5) An older consumer (`accepts`) attaches to the object instance's shared mapping
1837
+ for its version: per member one derive `(current) => old`, made on first use, dropped when no reference of that
1838
+ version is left, gone with the instance (one map and one diff per commit per version, whatever the number of old
1839
+ references); a version the instance does not accept gives no member (unbound, last values kept). A reference knows
1840
+ its version from the contract token it was made with. Nothing outlives its object instance; nothing is handed over.
1841
+ Kept: requests never end on a code change, healing on rebind, `accepts` for arguments and results. A simplification
1842
+ audit of slices 6a to 7b follows 7b's commit (owner: "how much other such bullshit was added in the last slices").
1843
+ Refined after the gap checks (Astra, Sol, Opus) and the build (owner, 2026-10-29: "i accept the recommendations"):
1844
+ a detaching reference follows the old instance until it stops changing (its seal after admitted commits, a restart
1845
+ after its drain), then takes its last values for every member of its bound variant, read or not, never from an
1846
+ instance it never followed; after a `failed` call the next good value is `[["r", value]]`; a watch on a reference that
1847
+ never hydrated resolves at its first binding (rejects on release, revocation, close, abort); release drops last
1848
+ values and bindings; registry values and job state are not affected. **Derives over a failed reference member compute
1849
+ on its last value** (G2 (A), as 6a): a contribution built on it stays in its table; only the reference member's own
1850
+ watches get the `failed` call, and `ref.state` shows the failure. **Activation churn +9.6% accepted** (CPU 22, 104.7-107.9
1851
+ → 114.7-118.3 µs, allocation equal): an object whose derive reads another object's member makes one pass-through
1852
+ node per reference member per instance; calls, commits, links and memory are within the bar. Two references to one
1853
+ key hold two pins (counted on the key's one source; the document is loaded and kept once). **Binary append commit-to-frame
1854
+ median +10% accepted** (owner, 2026-10-29; CPU 10, 0.031 → 0.034-0.035 ms): equal with the young generation pinned, fewer
1855
+ scavenges over the whole benchmark (V8's young-generation sizing, slice 7b fix round 3).
1856
+ - **Per plugin at startup and reload (owner, 2026-10-30: "yes", "1. lgtm 2. yes"; analyses
1857
+ `/tmp/per-plugin-{opus,fable}.md`):** supersedes "Rollback is all-or-nothing per host" (2026-10-2x, 7b §2, design §21)
1858
+ and 7c's Q2 ("a facet throwing while evaluated fails its host's whole step"). Why all or nothing was chosen: per plugin
1859
+ was decided first and p2's plugin reviewers found counter-traces (a dropped argument field written into a transcript; a
1860
+ copied Durable document written by two versions; a provider's failed factory leaving the harness hanging; mixed copied
1861
+ code and registries); v2 has since closed them (closed argument schemas for every caller, Q12, Q15; version binding
1862
+ with waiting, 7a; ownership of documents; registry versions with `accepts`, Q8; copied cross-plugin files limited to
1863
+ contracts without dependencies, 7c). Rules: (1) **at startup** a plugin whose build, evaluation or setup fails is left
1864
+ out; `load` resolves with a report `{ failed: [{ plugin, error }] }` and never closes the host for it (the core facet
1865
+ included: base code decides whether that is fatal); its consumers wait `failed { waiting }` as for any missing plugin.
1866
+ (2) **At reload** a plugin whose new code fails (build, evaluation, setup, a factory in staging, a declaration error,
1867
+ `checkAccepts`) keeps its old code (its loaded facet, unchanged), and every other plugin of the reload applies; the
1868
+ report lists it (`failed: [{ plugin, error }]`, as `load`'s; an added plugin that fails is left out); staging retries without it. (3) **The closure rule** (Opus): after
1869
+ staging, a changed plugin whose recorded requests would not bind because the plugin they need was kept old or left
1870
+ out is kept old too, so a consumer edited together with a broken provider keeps working on its old code. Its "missing"
1871
+ branch (owner, 2026-10-30: "keep", against narrowing it to names served before): a request nobody serves keeps its
1872
+ plugin old whenever any plugin of the reload failed, since a failed new setup may be what would have served it. (4) What no
1873
+ rule detects: a consumer relying on new behaviour of a kept provider whose contract (version, fingerprint) did not
1874
+ change: the same risk as a rollout across hosts, documented. (5) `rollout` keeps a plugin dirty while it was kept old
1875
+ on any host. Built with slice 7c (the kernel's `load` and `reload` change with it).
1876
+ - **Slice 7c's points (owner, 2026-10-30; `slice-7c.md` §5):** (1) esbuild is an optional peer dependency of Chord,
1877
+ imported only by the plugins component. (2) **No dev/prod split** (owner: "chord apps are supposed to not have a
1878
+ prod/dev split. they are supposed to be self modifying applications"): watching, rebuilding and rolling out are core
1879
+ machinery; one component `chord/plugins` (discovery, build, evaluation, `pluginFile`, the watcher, `rollout`) in
1880
+ `src/node/plugins/`, exported with v2's entry point; it turns V8's compilation cache off when created; reloads
1881
+ stay explicit (the watcher marks, never applies). Supersedes the separate `loader` and `dev` subpaths of "Slice 7b's
1882
+ open points" (5), (6). (3) `installDependencies(plugin, ctx)` as coding-agent installs package dependencies (`npm
1883
+ install --omit=dev --legacy-peer-deps --ignore-scripts --no-audit --no-fund` in the plugin's real directory, the
1884
+ command overridable). (4) Cross-plugin imports: any file of a discovered plugin, copied, documented as contracts and
1885
+ side-effect-free modules only, listed by the build, no static check yet. (5) `pluginFile` answered per facet by the
1886
+ `require` evaluation passes.
1887
+ - **Slice 7c build: an abort after the commit names what it applied (owner, 2026-10-30: "yes"; fix round 1, item
1888
+ 11, Astra):** `host.reload` still rejects `aborted` when its context aborts after the commit (the commit stands, only
1889
+ the wait ends), and the error carries `details.committed`, a `CommittedReload { added, changed, removed, failed }`
1890
+ (plugin names; `failed`: kept on their old code). Why: the plugins component keeps per host the facet each plugin
1891
+ runs and adopts it when the reload resolves; on this rejection it would keep the old facet and pass it at the next
1892
+ reload, swapping the committed plugin back silently. Rejected: resolving with the report after the commit despite
1893
+ the abort (it would change the rule that an abort after the commit rejects). Examples for 7c (owner): full coverage,
1894
+ each example a folder with the host code and real plugin directories (35-39).
1895
+ - **Slice 10's points (owner, 2026-10-30; `slice-10.md`):** (D1, "a"; `httpServer` **superseded** by `createHttp`, decisions "The HTTP core is a fetch handler" and "Settings are arguments") the HTTP component defines the registry `Http`
1896
+ (`"chord.http"`) and serves it through a facet of its own (`http.facet`, plugin name `chord.http`) that the app loads
1897
+ with its plugins; its `compose` builds the router; the server reads it with `host.registry(Http, ctx)` per request.
1898
+ (D2, "a") Wraps are built in slice 10 as designed (decision "HTTP routes and wraps", design §23): a contribution's
1899
+ `wraps` run around routes in the component's phases (`observe`, `headers`, `transform`, outermost first), within a
1900
+ phase in contributor order, by default around their own plugin's routes (`scope: "own"`; `"all"` explicit; `route`
1901
+ narrows to one, a missing one a reported conflict). The server's `headers` option stays for every response, also
1902
+ those no route makes (a 404, a refused request).
1903
+ (D3, "a"; **superseded** by "The HTTP server is a native object": the `Authenticators` registry) Authentication is one app function, `authenticate(request, ctx) => P | undefined`, called once per request
1904
+ and per upgrade; no Chord registry of resolvers (p3 composes its own from sharing, devices and the relay, with its
1905
+ own precedence rules).
1906
+ (D4, "k": a) Request bodies: `request.json(limit?)` (parsed JSON, 1 MiB by default, malformed: 400) and
1907
+ `request.body(limit?)` (an async iterable of byte chunks, for uploads); nothing is read until a route asks; past its
1908
+ limit the read stops and the request is answered 413; a disconnect aborts `ctx` and ends the iteration.
1909
+ (D5, "a") Responses are descriptions: `json`, `html` (a full document; replaces the design's `page { title, body }`),
1910
+ `bytes` (`mime`, `size`, `read(offset, length, ctx)`, `download?`, `ranges?`; 206/416, `HEAD`, safe
1911
+ `Content-Disposition`, 64 KiB reads with backpressure, stopped on disconnect), `redirect`, `error`; `headers` on every
1912
+ kind (cookies, a preview's own CSP); `fileResponse(path, options)` over a file; a throwing route is a 500 without its
1913
+ message.
1914
+ (D6, "a") Plugin WebSockets are `sockets` in the `Http` contribution: `{ "/terminal": (socket, request, ctx) => … }`;
1915
+ the component checks the origin and runs `authenticate` (no principal: refused, 403, before plugin code), upgrades
1916
+ and hands the plugin a `ws` WebSocket for its own protocol; patterns as routes; wraps do not apply; the links path is
1917
+ the component's (`host.accept`). The design's channel component (acknowledged byte channels) waits for a second user
1918
+ and would be built on top of it.
1919
+ (D7, "a") `ws` is an optional peer dependency of Chord (pinned exactly), imported only by `node/http`, as
1920
+ esbuild for plugins.
1921
+ (D8, "a") Route patterns: exact segments, `:name` (one non-empty segment, percent-decoded, `params.name`) and a
1922
+ trailing `/*` (the rest, possibly empty, `params["*"]`); the most specific match wins, segment by segment from the
1923
+ left (exact before `:name` before `*`); one pattern from two contributions is a reported conflict, the first by
1924
+ plugin name wins; `HEAD` falls back to the `GET` route.
1925
+ (D9, "a") Worker keep-alive: the child reports `host.activity` and the app's `busy` counts (a JSON watchable of
1926
+ numbers, e.g. Durable's active tasks) whenever they change; the server stops a worker whose counts were all zero for
1927
+ `idleMs` (default 5 min, checked every second), never while it starts; a stop sends `shutdown` (the host closes),
1928
+ the process tree killed after 5 s.
1929
+ (D10, "a"; **superseded** by R2 below: no automatic restarts, no route backoff) A failed worker start uses the kernel's route backoff (1 s doubling to 60 s while holds exist,
1930
+ `resolve_failed` reported); no sticky failure; `workers.state` shows the error and when it is tried again.
1931
+ (D10b, "b") `host.route(Kind, options, ctx)` returns a `RouteHandle` with `retry(address)`: it ends that address's
1932
+ backoff, so `resolve` runs now if the address has holders (a Retry button for a worker that failed to start, wired
1933
+ by the app to the handle; the workers component has no `retry` of its own).
1934
+ (D11, "a") Workers never build: the server's plugins component discovers and builds every plugin; a worker's start
1935
+ message carries its kind's current scripts (`Built`, plain data) and a reload ships new ones (D12); the child's
1936
+ `plugins` only evaluates (no esbuild, no watcher). So plugins are defined where the server can read them, one set for
1937
+ every host (owner: "no cwd specific plugins, which might be alright; that way workers can live anywhere"); project
1938
+ plugins (design §26 #14) would be discovered and built by the server too. A worker on another machine needs the
1939
+ plugin directories there only for `chord.external` packages and `pluginFile`.
1940
+ (D12, "a") A rollout step returns one report per host, `{ hosts: [{ host, report }] }` (7c's `RolloutStep` changes;
1941
+ the in-process server step returns a list of one); the workers step reloads every running worker in parallel; a
1942
+ worker that closed during it is listed with its error and does not stop the rollout; a plugin's dirty mark clears
1943
+ only when it applied on every host of every step.
1944
+ Spec review (Fable, Opus): (R1, "a") `RouteHandle.suspend(address)`: while suspended the route does not resolve the
1945
+ address, its holders stay `loading`; `retry(address)` ends the suspension; `workers.stop` suspends, then stops (an
1946
+ archived session stays stopped while a tab still holds it).
1947
+ (R2, owner: "take it"; **supersedes D10 and the kernel's route backoff**) **Workers start on demand and never
1948
+ restart by themselves** (owner: "if it crashes, chances are it had a good reason to crash, and restarting it won't
1949
+ fix that"; D10 as recommended was a restart loop whenever a consumer holds). A worker starts for a hold of an address
1950
+ that has not failed (the first, or one after an idle stop or `stop`). A crash or a failed start marks the address
1951
+ failed with its error until `workers.retry(address)` (`RouteHandle.retry`) or a rollout that reaches workers (new
1952
+ code may fix it, as a reload retries failed objects); its holders and new holds see `failed` with that error, nothing
1953
+ forks. Kernel: a route has no backoff timer; it resolves only for a hold, on `retry`, or after its link dropped while
1954
+ held, and a rejected `resolve` fails the route's holders with its error (no endless `loading`); a network route that
1955
+ wants to reconnect keeps that policy in its own `resolve`. The workers component owns its route
1956
+ (`createWorkers(host, Kind, options)` calls `host.route` and keeps the handle) and answers a resolve of a failed
1957
+ address with the stored failure at once. A start finding the lock still held by a draining old worker waits for it in
1958
+ the child (bounded by the start timeout). R1's `suspend` and D10b's `retry` stay.
1959
+ Other timed retries (owner, 2026-10-30: "keep them as is for now"): objects' transient failures (1 s to 60 s while
1960
+ held), `self.on` handlers that throw (1 s to 60 s while live) and `self.onLive` restarts (the same, reset after a
1961
+ minute's run) stay; they heal real transient failures (a network down for a while) and cost at most one try a minute
1962
+ each; revisit if one shows up as a loop in practice.
1963
+ (R3, "a"; refines D9) The child reports only the flip: `{ idle: true }` when every count (`host.activity` and `busy`)
1964
+ becomes zero, `{ idle: false }` when one becomes nonzero; the server's idle clock starts at `true` (a status message
1965
+ costs about 6 µs on each side, a local call 2.9 µs: no report per call).
1966
+ (R4; **superseded in part** by "allowed hosts only through contributions" and "The HTTP component's security baseline": no `allowedHosts` option, provenance by the socket; "a if configurable at runtime so allowed domains can be added and removed by a plugin") Before `authenticate`, the
1967
+ component refuses (403) a `Host` that is neither loopback nor allowed, and an upgrade (`/chord`, a plugin socket)
1968
+ whose `Origin` is not the request's own origin; `request.origin` comes only from an accepted `Host` (`http://` on
1969
+ loopback, `https://` for an allowed host); `headers(request, principal)` gets the principal. Allowed hosts are the
1970
+ server option `allowedHosts` plus every `hosts` list in `Http` contributions: a plugin adds and removes its domains
1971
+ at runtime by contributing a writable (`self.writable`, design §17), the router recomposed on each change (a tunnel or
1972
+ relay plugin names its public host).
1973
+ **The app kit is malleable by plugins** (owner, 2026-10-30: "all of this should be malleable via plugins"; "yes"):
1974
+ in `http`, `workers` (and slice 11's components) every option that is not bootstrap is either a contribution to
1975
+ `Http` (routes, wraps, sockets, hosts; writables where it changes at runtime) or a function the component calls per
1976
+ use (`authenticate`, `headers`, a worker's `start`, `idleMs`), which the app backs with its own registries; nothing is
1977
+ copied out of a contribution or an option at startup. Bootstrap stays base code, changed by a restart: the listen
1978
+ address and port, the links path, the worker entry module with its `execArgv` and `env`, the host kind workers serve.
1979
+ (R5, "agree") `workers.state` per address key: `{ status: "starting" | "running" | "stopping", pid, startedAt }` or
1980
+ `{ status: "failed", error: { message, code?, reason? }, at, stderrTail? }`; no entry when stopped or never started
1981
+ (a clean stop removes it); `failed` stays until Retry or a rollout; plain JSON (a core object may serve it).
1982
+ (R6, "b, common enough") `bytes` may omit `size`: chunked transfer encoding, no ranges (no `Accept-Ranges`), `read`
1983
+ called with growing offsets until it returns an empty chunk (a zip built on the fly, a growing log, an export paged
1984
+ from a worker).
1985
+ Spec review round 2 (Fable, Opus, Astra, Sol): (Q1, recommended) holders of a failed worker get `failed { waiting }`
1986
+ with the stored error as cause and stay on the address's path: a later link of the address (Retry, a rollout) binds
1987
+ them without a re-hold; a `get` waits its `readyTimeout` meanwhile.
1988
+ (Q2, "a") A rollout that reaches workers clears the failures from before it, then calls `RouteHandle.retry` for the
1989
+ cleared addresses that have holders (they start with the new code and are listed in the step); an unheld one starts
1990
+ at its next hold; a worker that fails during this rollout stays failed (no loop: one start per held failed address per
1991
+ rollout a person triggers).
1992
+ (Q3, "a"; refines R1) A `stop`'s suspension lasts while the address has holders; they see `failed { waiting }`
1993
+ ("suspended"); with the last holder the record goes, and a later hold starts the worker on demand (as after an idle
1994
+ stop); whether an archived session may be opened is the app's (`authorize`, its views). Memory stays bounded.
1995
+ (Q4, "a") The start timer (60 s) covers fork, lock and connect (`started`) only; after `started` a slow `load` is
1996
+ objects showing `loading`; `ready` sets `state` to `running` and lets idle stops begin; a `load` that never ends
1997
+ shows `starting` until `stop` (no kill, no retry).
1998
+ (Q5, "a"; refines R3) A worker's idleness is `references`, `jobs`, `lingering` and the app's `busy`, not `calls`: a
1999
+ remote call runs on a hold (counted), local calls come from routines (not counted, §14), jobs, Durable tasks (`busy`)
2000
+ and handlers under a hold; no message per local call, no timer. The first report goes with `ready` (`ready { idle }`),
2001
+ so a worker idle from its start is stopped too.
2002
+ (Q6, owner: "if sockets are a contribution, why keep them when the contribution is removed?": design §17's rule, an
2003
+ entry lives as long as its contributor and consumers keep nothing beyond a use) A plugin socket is closed (1012,
2004
+ "service restart") when the contribution that handed it out leaves the table (a reload changing or removing it); a
2005
+ `/chord` link or socket whose `Host` is no longer allowed closes (1008) when the allowed hosts change.
2006
+ - **Slice 11's points (owner, 2026-10-30; `slice-11.md`):** the surfaces are generic plumbing; the app keeps
2007
+ everything visible (owner: "this one needs to be generic, so apps can do whatever they want, css, tui components,
2008
+ themes"). (P1, "a") `chord/web` (its server side now `/surfaces`) ships serving the surface (the base bundle, the
2009
+ plugins' web scripts, stylesheets and assets, the page through the app's template, all as `Http` contributions), the
2010
+ browser's client host with its connection loop, and loading and applying the plugins' web facets; not HTML, CSS,
2011
+ components, themes, navigation, slots or layout.
2012
+ (P2, "script tag") A plugin's web script is served as a file that registers itself
2013
+ (`chordFacet("<plugin>", "<digest>", function (require, module, exports) { … })`) and is loaded with `<script src>`
2014
+ under a `'self'` CSP: no `eval`, no `new Function`.
2015
+ (Kinds, owner: "ya"; changes 7c's `createPlugins` options, built in slice 10) One definition per facet kind:
2016
+ `kinds: { server: { platform: "node", hostPackages: [...names] }, web: { platform: "browser", hostPackages: [...] } }`
2017
+ replaces `kinds` plus `browserKinds` and the component-wide `hostPackages` map. The platform decides the build (Node:
2018
+ builtins and `chord.external` external, no CSS or `?url` assets; browser: CSS and assets collected, builtins and
2019
+ `chord.external` refused); the names keep those imports external. The module objects are given where scripts are
2020
+ evaluated: the server's own at `plugins.load(host, { kind, core, modules })`, a worker's in `workerMain`'s
2021
+ `hostPackages`, a page's in `startPage`.
2022
+ - **Slice 10 build: plugin sockets get a heartbeat (owner, 2026-10-30: "does it make sense? then yes"):** the HTTP
2023
+ component pings every plugin socket with WebSocket ping frames after 15 s of silence and terminates one silent for
2024
+ 30 s (the link heartbeat's numbers); browsers and `ws` answer pings by themselves, so plugin code is unchanged. A dead
2025
+ peer (a phone that vanished) no longer keeps a plugin's resources (a terminal view) until TCP gives up.
2026
+ - **Origins only from relays (owner, 2026-10-31: "why would child links carry origin? the server keeps track of origin
2027
+ per connection and relays that to workers"; slice 10 security audit, Opus K1/K2, a hole since slice 4a):** `origin`
2028
+ is read only on parent and route links (where a relay's messages arrive); on a child link (a tab, a TUI) a message
2029
+ carrying `origin` is a protocol error that closes the link; the server numbers each child connection itself, as it
2030
+ does. Every link drops a per-origin record once it has no holds, no calls and nothing in flight (origins are never
2031
+ reused). Was: honoured on every link, so a tab could invent origins, each with its own call window (5,120 concurrent
2032
+ calls from one guest socket) and a record kept until the socket closed (about 1 KiB each). Supersedes design §3's
2033
+ "`origin` is honoured on every link". Fixed in slice 10's fix round.
2034
+ - **Slice 10 fix round, worker errors in public (owner, 2026-10-31: "a"; refines R5):** two levels. Public (`workers.state`
2035
+ and the holders' `failed { waiting }`): a `fatal` as the child's `redact` made it; a start error or a crash as a fixed
2036
+ message with a code (`"worker failed to start"`, `"worker exited (SIGSEGV)"`); no stderr in `state`. Private: the full
2037
+ error and the stderr tail go only to the server's `onError` (`worker_failed { address, error, stderrTail }`), the app's
2038
+ logs to redact. Was: `state.stderrTail` and the raw start error, which leaked secrets to guests (security audit, Astra,
2039
+ Opus H3).
2040
+ - **Slice 10 fix round, allowed hosts only through contributions (owner, 2026-10-31: "a, and we close everything that's
2041
+ been removed"; refines R4):** the server option `allowedHosts` goes; the app contributes its hosts like any plugin
2042
+ (fixed ones from its core facet, changing ones from a host-lifetime object's writable). Whatever is removed is closed
2043
+ at once: when a host leaves the allowed set, or a plugin's contribution (its sockets) leaves the table, every link and
2044
+ socket that came through it is terminated immediately (no graceful close the peer could ignore, Astra A1), its scope
2045
+ sealed; an upgrade still authenticating is checked again against the current set and router before it is handed out
2046
+ (Astra A2).
2047
+ - **The HTTP component's security baseline (owner, 2026-10-31: "this mechanism should come out of the box in http/app kit;
2048
+ basically encode all the best practices so users never have to think about it again"; "yes"; p2's rules from
2049
+ `hosts/principals.ts`, `hosts/server.ts`, `loopback-cookie.ts`, devices, sharing):** on by default in
2050
+ `chord/http`. (1) Provenance: local only when the socket's local address is loopback, `Host` is a loopback
2051
+ form with the port, and no `Forwarded`/`X-Forwarded-*` header; otherwise only an allowed host (https); else 403;
2052
+ duplicate `Host` and absolute-form targets 403; `request.provenance` `{ kind: "local" } | { kind: "remote", host }`.
2053
+ (2) Foreign requests (Fetch Metadata: `Sec-Fetch-Site` `cross-site`/`same-site`, or a frame another site embeds)
2054
+ never reach `authenticate`: they run anonymous, except a top-level navigation (`GET`, `navigate`) to a route marked
2055
+ `page: true`. (3) A foreign `Origin` is 403 on every request and upgrade; unsafe methods from a browser need `Origin`
2056
+ equal to the app's; a route may opt out (`crossSite: true`, a webhook), then it always runs anonymous. (4) Default
2057
+ headers on every response, the component's own included: `Cache-Control: no-store`, `nosniff`,
2058
+ `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, a CSP with a per-server script nonce (`default-src 'none'`,
2059
+ scripts `'self'` and the nonce, `connect-src` self and its WebSocket, `frame-ancestors 'none'` or the contributed
2060
+ `framers`); a route widens its own CSP through its response headers. (5) `cookie(name, value, options)`: `HttpOnly`,
2061
+ `Path=/`, `SameSite=Strict` (or `Lax`), `Secure` on a remote origin, signed with a given secret. (6) `bytes` and
2062
+ `fileResponse` refuse foreign requests, are `attachment` unless the route says inline, and inline ones get a
2063
+ `sandbox` CSP. The app decides only who someone is, which routes are pages, its extra CSP sources and its framers.
2064
+ Replaces the CSRF question of the security audit; extends R4.
2065
+ Relays (owner: "sure"): a contribution may declare `relays: [{ name, verify(request) => { origin, framer } |
2066
+ undefined }]`; admission calls them for a request that would be refused for forwarded headers on loopback (p2's
2067
+ Radius relay); a verified one gets `provenance: { kind: "relayed", relay, origin }` and its framer allowed for it,
2068
+ an unverified one stays refused; `authenticate` decides its principal.
2069
+ - **The HTTP server is a native object (owner, 2026-10-31: "still unconvinced the server can't be a native object; it's a
2070
+ singleton, unlikely to change"; supersedes D3 and the `httpServer` function of D1):** `chord.http`'s facet serves the
2071
+ registry `Http` and a host-lifetime object whose native is the listener; its configuration (`HttpConfig`: port,
2072
+ links path) and `Authenticators` are registries the app (or the app kit) contributes to. The facet is passed as the
2073
+ same object at every reload, so the listener never closes before the process ends. The port opens right after
2074
+ `load`'s publication (a few hundred milliseconds later than listening before `load`; a reconnecting tab retries).
2075
+ - **The app kit is opinionated (owner, 2026-10-31: "the app kit should be super duper opinionated and make for small
2076
+ app code"; "i suppose"):** after slice 10's fix round and before slice 11, a design pass on `chord/app`: one
2077
+ entry (`startApp`, `startWorker`), conventions for layout, hosts, principals, authentication, storage, plugins, web
2078
+ and TUI, decided point by point; the slice 10 components are its inside. Inputs from the owner: user management is
2079
+ part of it ("user management will also need to be a thing we support"), and app shapes without workers (server and
2080
+ clients only: "a chat app doesn't need workers").
2081
+ - **Plugins import each other by name (owner, 2026-10-31: "i like it"; changes 7c's cross-plugin rule; **superseded** by "Contracts come from where the plugin's code came from", 2026-11-01):**
2082
+ `import { Notes } from "@app/notes/contract"` resolves to the discovered plugin named `@app/notes`, its file
2083
+ `contract.ts`, wherever its directory is (any root), copied into the importer's script as before (contracts and
2084
+ side-effect-free files only; they may import only host packages and files of their own plugin); a missing plugin is
2085
+ a build error naming it; relative paths into another plugin's directory are refused. Owner's notes for the app kit
2086
+ pass: third-party plugins need the imported plugins' sources or types for type checking (publishing each plugin to
2087
+ npm works but is unpleasant); plugins should be testable on their own, with their dependencies mocked.
2088
+ - **The HTTP core is a `fetch` handler; Chord's own Node server is the easy path (owner, 2026-10-31: "could http just plug
2089
+ into hono/express?"; "we should still provide the easy path using just node's shit as well, for small apps or apps
2090
+ where we don't care"):** the component builds one Web-standard handler, `(request: Request) => Promise<Response>`,
2091
+ from `Http` (admission, the security baseline, router, wraps, descriptions), plus per-runtime upgrade adapters for
2092
+ `/chord` and plugin sockets. The default is Chord's own listener, the native host-lifetime object of `chord.http`
2093
+ over Node's `http` (decision "The HTTP server is a native object"); an app with its own server mounts the handler
2094
+ (`hono.all("/*", (c) => http.fetch(c.req.raw))`, an Express adapter) and passes upgrades to the adapter. The same
2095
+ routes then run on other runtimes later (Bun, Durable Objects, design §26 #16). Built in slice 10's fix round
2096
+ (reworks `server.ts`, `request.ts`, `response.ts`; router, admission rules and descriptions stay).
2097
+ - **Slice 10 fix round, three small points (owner, 2026-10-31: "yes on fileResponse etc"):** (1) `fileResponse(path, {
2098
+ root? })`: with a `root`, a file whose real path lies outside it (a symlink out) is refused 403; without one the path
2099
+ is trusted (a plugin's own file). (2) The worker lock on macOS and the BSDs fails closed on a stale lock file, with an
2100
+ error naming it (no takeover; Linux and Android use abstract sockets and are unaffected). (3) A route's overhead:
2101
+ Opus's two trims, measured again after the `fetch` rework, and the numbers brought to the owner.
2102
+ - **Stale lock files are cleared by the starter (owner, 2026-11-03: "do both"; refines point (2) of "Slice 10 fix
2103
+ round, three small points"):** on macOS and the BSDs a worker killed by a signal (the grace's SIGKILL, the start
2104
+ timer, a dev supervisor) runs no exit hooks and leaves its lock's socket file; every later start of that address then
2105
+ failed closed on it, also `retry`, until someone removed the file by hand. Takers still fail closed (two takers
2106
+ clearing could race); the workers component, the one process that starts an address's workers, removes a socket file
2107
+ nobody accepts on (`clearStaleLock`) before each start's message and after a child's exit by a signal, both in the
2108
+ address's queue, so never two clears of one name at once. A live holder (an older worker still exiting) accepts and
2109
+ is kept. One connect per start on macOS and the BSDs, nothing on Linux, Android and Windows. A taker also treats
2110
+ `ENOTSOCK` (a non-socket file at the path on macOS) as stale; before it retried until its context ended.
2111
+ - **A pause of the process is no silence (owner, 2026-11-03; refines "Slice 4a spec decisions"):** after a laptop
2112
+ slept, the host's heartbeat timer fires before any I/O is read and found every link silent past `close`: the server
2113
+ closed its worker links (and the worker its parent link) though both peers were fine, and p2's session workers
2114
+ stayed failed ("worker exited (code 0)") until Retry. A tick at least one interval late is time this process did
2115
+ not run (a sleep, a blocked event loop); that time does not count as silence: each link's silence is what it was
2116
+ before the pause, at least `ping`, so an open link pings at once and a dead peer (a TCP connection the sleep
2117
+ dropped) closes `close - ping` later (15 s by default). One subtraction per link on a late tick, nothing otherwise.
2118
+ - **A lost worker link is recovered from (owner, 2026-11-03: "should we not auto reconnect?"; refines R2 of "Slice
2119
+ 10's points"):** R2 counted a link lost while its child lives (the heartbeat, a protocol error, the child closing its
2120
+ own link) as a crash. A lost link of a `running` worker is now a stop instead: the child is stopped (grace 5 s, then
2121
+ killed), `worker_link_lost { address, error }` is reported, the address does not fail, and its held route resolves
2122
+ again (the kernel resolves once after a held link drops): a new worker starts after the exit. No restart loop: a
2123
+ second lost link within a minute of the address's last recovery fails it as before, with the public error `worker
2124
+ lost its link` (`worker_exited` and `worker_failed` as for a crash). A link lost before `ready` is still a failed
2125
+ start. Crashes, `fatal`s and failed starts stay failed until `retry` or a rollout (R2 unchanged for them).
2126
+ - **Slice 10: a route's overhead accepted; no header accessor (owner, 2026-10-31: "1 yes, 2 no"):** after the `fetch`
2127
+ rework and the Node fast path a route costs +20-21 µs over a plain Node handler on the standard bench, +12-13 µs warmed
2128
+ (before the rework +4.5 µs): the request's scope (2.1 µs), URL parsing, admission and routing, the baseline's headers,
2129
+ the scope's seal, and about 5 µs of Node's microtask machinery an async path triggers. Accepted against the 10% bar.
2130
+ No `request.header(name)` accessor: an authenticator reading headers builds the Web `Request` (about 3.5 µs more).
2131
+ - **A CSP nonce per response (owner, 2026-10-31: "csp ok"; Fable's recheck):** the HTTP component makes a fresh script
2132
+ nonce for every response (`request.nonce`), not one per server: a nonce read from one page cannot be reused by an
2133
+ injected script elsewhere.
2134
+ - **Settings are arguments, extensions are registries (owner, 2026-10-31: "making that config a registry is dumb; it's a
2135
+ general problem for all host-lifetime natives that need initialization at host startup"; "k"; supersedes the
2136
+ `HttpConfig` registry of "The HTTP server is a native object"):** what the app decides once at startup (a port, frame
2137
+ limits, a timeout, a data directory, which implementation to use) is an argument to the component's facet, which base
2138
+ code constructs; the facet and its host-lifetime objects close over it; changing it is a restart; no plugin can
2139
+ touch it. What plugins add while the app runs (routes, authenticators, tools, panels, more indexers) is a registry.
2140
+ HTTP: `createHttp(host, { listen, links, closeTimeout, onError }, ctx)`; `Http` and `Authenticators` stay registries.
2141
+ The same for the harness (its store and defaults as arguments; models, environments, extensions as registries) and
2142
+ the app kit's components (a session index takes its indexer as an argument, and may also follow a registry of
2143
+ indexers if plugins should supply them).
2144
+ - **API versions in the import path (owner, 2026-10-31: "this sounds about right"; "must be declared by the plugin in
2145
+ code, we do not want to dig around package.json files anymore"):** the app's base code is internal except its API
2146
+ modules, which it exposes as host packages with a version in the path (`@acme/app/v3`; Chord's
2147
+ `@earendil-works/chord/v2`); the app lists the versions it still provides (an older one as the same module, or a
2148
+ thin layer); an import of an unlisted version is a build error naming the available ones. No `package.json` field
2149
+ and no npm ranges for this. Contracts and registry tokens inside an API keep their own versions. The rest of the
2150
+ plugin model is a proposal (`docs/plugin-model/`).
2151
+ - **A schema package beneath pi-ai and Chord (owner, 2026-10-31: "i want the schema package"; "if we keep it a
2152
+ dependency in pi-ai, we must be able to rip it out"; TypeBox is "horrible to treeshake"):** a small package with no
2153
+ dependencies (working name `@earendil-works/schema`) under both pi-ai and Chord (siblings: neither depends on the
2154
+ other): helpers returning plain JSON Schema, a structural `JsonSchema` type, `Static` from the helpers' literal
2155
+ types, and a validator for general JSON Schema (tool schemas from MCP servers and extensions use `$ref`, `anyOf`,
2156
+ `pattern`, ...), without `eval` or `new Function` (CSP-safe). Chord v2 drops TypeBox; pi-ai's core never imports
2157
+ it (TypeBox schemas still pass as JSON Schema; TypeBox-typed extras only in an optional subpath, so ripping it out
2158
+ is deleting that subpath). Not omptype (its validator compiles with `new Function`; its 10.7k lines are mostly
2159
+ ArkType's API). Open: which validator (measure `@cfworker/json-schema` and TypeBox against the official
2160
+ JSON-Schema-Test-Suite and Chord's argument checks first), and moving `Type` out of pi-ai's main export.
2161
+ **Built** (owner, 2026-10-31: "clone pi add the package, done"; pi `fab2c53`): `packages/schema`,
2162
+ `@earendil-works/pi-schema`: `compile(schema, { draft, formats, schemas })` with `check` (narrowing) and `errors`
2163
+ (JSON Pointer paths), Chord-style builders typed with `Static`, formats as the opt-in `./formats` entry, leaf and regex
2164
+ checks shared through weak caches (GC-tested); its own closure-compiled validator, chosen after measuring
2165
+ (`/tmp/schema-measure.md`, Radius 01m69x3dz2fsg9k1r33vde7hv6): 2,222 of 2,223 core suite tests (TypeBox 2,200,
2166
+ cfworker 2,167, omptype 1,522), Chord checks at 0.95-1.3x TypeBox's compiled speed and the same with `new Function`
2167
+ blocked, 7.9 MB for 3,200 distinct schemas (cfworker 7.6, TypeBox 54.7), 8.5 KB gzip. Still open: switching Chord v2
2168
+ and pi-ai to it (`Type` out of pi-ai's main export), formats on by default in pi-ai.
2169
+ - **The schema switch (owner, 2026-10-31):** TypeBox leaves Chord (v1 too: "remove typebox wherever we can"),
2170
+ pi-ai, pi-agent-core and pi-durable; extensions must not break ("absolutely 100%"), proven by a guard over our own
2171
+ extensions, built-in tools and generated arguments (verdicts, coerced arguments, provider payloads and inferred
2172
+ parameter types identical; owner: "just use ours", no npm corpus). **Coercion lives in pi-schema** as an opt-in,
2173
+ the superset of TypeBox's `Value.Convert` rules and pi-ai's own (TypeBox's rule first where they conflict):
2174
+ `compile(schema, { coerce: true })` adds `coerce(value)` (a copy, never mutating) and `parse(value)` (`{ ok, value }
2175
+ | { ok, errors }`); pi-ai uses it for every tool, whatever built the schema (so plain-JSON MCP tools gain the
2176
+ TypeBox rules). **Error texts are pi-schema's own** (no mapping to TypeBox's wording). Extensions keep `Type`,
2177
+ `Static`, `TSchema` from the coding agent's bundled TypeBox (the pi-ai they see is compat plus those); an extension
2178
+ author type-checking against the published pi-ai no longer finds them there (accepted: "fine"). **`format` is an
2179
+ annotation in Chord** ("no format check": no format module in Chord's or Durable's entries; pi-ai still checks
2180
+ formats on tool arguments). **Chord's envelope check costs +10 to +100 ns per message** from untrusted links
2181
+ (pi-schema reads fields by name from a list; no eval): accepted ("negligible"), no hand-written check.
2182
+ - **Contracts come from where the plugin's code came from (owner, 2026-11-01: "ok"; plugin model point 1, primer
2183
+ `docs/plugin-primer/`; supersedes "Plugins import each other by name"):** a plugin carries the contract it was
2184
+ written against, never the running app's newest file. The problem: by name, a third-party plugin's next build copied
2185
+ the provider's newest `contract.ts` (version 2), so its version-1 code claimed version 2, `accepts` was never used
2186
+ and it computed wrong results silently. Rule: (A) plugins that change together, the app's own in its repository or a
2187
+ user's in one root, import each other by relative path (`../notes/contract.ts`), copied into the importer's script
2188
+ and listed in `copied`; a copied file may import only host packages and files of its own plugin; a relative path
2189
+ leaving its root (into base code or another root) is a build error pointing to the API or an npm dependency. (B)
2190
+ Plugins that do not change together take contracts as an npm dependency at a fixed version, bundled at the installed
2191
+ version (decision "A plugin's npm dependencies are bundled"); the provider serves it through `accepts` or the
2192
+ consumer waits "contract mismatch". Which package carries the app's contracts is point 5. The by-name lookup and the
2193
+ `tsconfig` path aliases go; built in the plugin slice (examples 35-38, fixtures, design §21, the plugins README, the
2194
+ guide's chapter "Plugins Using Plugins").
2195
+ - **Plugins import only the app's versioned API; Chord is re-exported in it (owner, 2026-11-01: "makes sense";
2196
+ plugin model point 2):** a plugin never imports Chord directly: Chord is not a host package plugins may name, so a
2197
+ direct import is a build error pointing to the app's API. The app's API module per version (`@acme/app/v3`)
2198
+ re-exports the Chord surface the app supports (contract and schema builders, `defineFacet`, `defineRegistry`, the
2199
+ types plugins see) next to its own small part (host kinds, the principal type, `defineFacet` bound to it, core
2200
+ contracts, registry tokens). One version number, the app's, describes everything a plugin gets from its host. The
2201
+ process runs one Chord: an older API version still listed is a layer over today's Chord; when Chord breaks its
2202
+ API it keeps its previous API as a compatibility module for a while, which the app's older version re-exports;
2203
+ once that is dropped the app drops the version and old plugins fail at build naming the available versions.
2204
+ Contracts are data and bind through `accepts` whatever Chord runs. One facet imports from one API version only
2205
+ (checked at build). Only base code imports Chord directly. Bundling is slice 10's host packages: the version is part
2206
+ of the name (`kinds.hostPackages` lists `@acme/app/v3`, `@acme/app/v2`), each process provides its own modules.
2207
+ - **The plugin model, points 3, 4, 5 and 8, imports and builds (owner, 2026-11-01, by voice: "yeah that checks out"; "lock the decisions on the
2208
+ points which was discussed"; primer `docs/plugin-primer/`):**
2209
+ (3) **Split by host kind, for the app and for plugins alike.** The app's API per version has a shared module
2210
+ (`@acme/app/v3`: Chord's re-exported surface, host kinds, the principal type, `defineFacet` bound to it, contracts
2211
+ and registries that cross hosts) and per-kind modules (`/v3/web`, `/v3/tui`, `/v3/server`, `/v3/worker` where
2212
+ needed: UI components, registries and `local` contracts that live on that kind only). A plugin's contract files
2213
+ mirror it: `contract.ts` (shared) and `contract.server.ts`, `contract.worker.ts`, `contract.web.tsx`,
2214
+ `contract.tui.ts` as needed. A contract file mentions native things (React components, functions) only through
2215
+ `import type`; at runtime it imports only the shared API and its plugin's data-only files (a hard rule). The build
2216
+ checks every import by the file it resolves to: a per-kind file (`.web.` etc.) or per-kind API module only for that
2217
+ kind, and a file importing a package the kind does not provide (React in a worker) refused. Shared UI between
2218
+ plugins: a plain component through an imported file (inlined into the importer's script, frozen at its build), or
2219
+ a registry when one live instance is needed (React contexts, a feature others embed).
2220
+ (4) **Host kinds on registry tokens** are covered by placement: a token used on one kind lives in that kind's file,
2221
+ so other kinds cannot import it; an explicit `hosts` marker only if a shared token must be limited later.
2222
+ Registries version by hand: readers and writers work across versions through the token's version and the defining
2223
+ plugin's `accepts`; no fingerprint for native values (decided earlier, unchanged).
2224
+ (5) **Third parties get the app's contracts through its versioned API:** each built-in plugin keeps its contract
2225
+ files; the API re-exports the ones the app promises (`export { Notes } from "../plugins/notes/contract.ts"`), so a
2226
+ plugin built against `/v2` keeps v2's contracts; the others stay private. No per-area subpackages.
2227
+ (Imports) **Plainly, with no resolution magic:** within one repository (a root, or a repository of several
2228
+ plugins), relative paths to the files (`../notes/contract.web.tsx`); across repositories, the package name with its
2229
+ exports (`@bob/notes`, `@bob/notes/web`), pinned in `dependencies` by an npm version or a git ref
2230
+ (`github:bob/notes#<commit>`; npm's git dependencies, no registry publishing needed; a plugin in a subfolder of a
2231
+ multi-plugin repository is cloned and linked by the plugins component, which npm cannot do). Rejected: one
2232
+ specifier resolving to a sibling or an npm package depending on `dependencies` ("kinda magic"). Plugins are
2233
+ installed from git by cloning a branch, tag or commit into a plugins root (as the coding agent's git packages).
2234
+ (8, base code) **Base code shrinks to the API, bootstrap and a start-data facet:** the vocabulary (the versioned API modules,
2235
+ host packages, never reloaded), bootstrap (creating hosts, opening the store, components such as `createHttp` and
2236
+ `createWorkers`, which take settings and process values and return facets, their natives opened by Chord, passed as
2237
+ the same object at every reload) and one small facet serving process values (start data, the opened store) as core
2238
+ objects. Every implementation, the harness and the providers of core registries included, is a plugin in the app's
2239
+ own root, loaded, reloaded and failing per plugin like any other; the app may mark its root required, so a core
2240
+ plugin failing at startup is fatal. Built-in plugins follow the same rules as any plugin.
2241
+ (Build) **No build hooks from plugins:** the server builds every plugin with one esbuild call from TypeScript source
2242
+ (no `dist` needed; installs run with scripts off); what the build handles (TS, TSX, JS, JSON, CSS, assets and WASM
2243
+ by URL) is the plugin's toolbox, generated files are committed; a CSS preprocessor or similar, if ever needed, is
2244
+ enabled by the app per host kind, never by a plugin (a stranger's web plugin must not run code on the server).
2245
+ To do: a skill and documentation for agents describing a plugin's layout, its contract files, imports and how the
2246
+ host builds it. Open from the primer: (6) `testPlugin` with stub facets, (7) single-file plugins.
2247
+ - **Single-file plugins (owner, 2026-11-01: "agreed"; plugin model point 7):** a root may hold plain files named
2248
+ `<name>.<kind>.ts(x)` (`hello.server.ts`, `hello.web.tsx`): files with the same first name are one plugin named
2249
+ after it, one facet per file. The name must be a valid unscoped name; a collision with a directory plugin of the
2250
+ same root is reported like any duplicate. No `package.json`, so no npm dependencies; it imports only the app's API
2251
+ (host packages), no other files and no contract files for others; it may serve objects, define contracts for
2252
+ itself inline and contribute to registries. When it needs more, it becomes a directory with the same name, which a
2253
+ reload treats as the same plugin. The watcher's `set` mark covers files appearing and disappearing at a root's top.
2254
+ - **Testing a plugin alone (owner, 2026-11-01: "let's do that"; plugin model point 6):** `testPlugin(dir, { kind,
2255
+ modules, stubs, start?, principal? }, ctx)` builds the plugin as production does and loads it on an in-memory host
2256
+ of that kind with an in-memory store. A plugin depends on four things, each answered without a mocking framework:
2257
+ the app's API and Chord are the real modules (definitions only); other plugins' and core objects are reached
2258
+ through contracts, so the test passes stubs, ordinary facets serving those contracts with fake data; registries
2259
+ the plugin contributes to are served by the helper, which collects the contributions for the test to read (a
2260
+ registry it reads gets fake entries from a stub); process values come as fake start data. The result is a live host:
2261
+ the test gets the plugin's objects, calls them, watches their state, and acts as any principal. A contract no stub
2262
+ serves leaves its requests waiting `not_served`; the helper lists those names, so the test says what to stub.
2263
+ Several hosts at once (a server plugin calling a worker) is a later, larger helper over memory links; the app kit
2264
+ may ship stubs for its core contracts.
2265
+ - **The app kit, point 1: its shape (owner, 2026-11-01, by voice: "yeah"; app kit design pass):** the app's own
2266
+ layout is fixed: `src/` with the API versions (`src/api/v3/`, per host kind as decided for the plugin model) and the
2267
+ built-in plugins root (`plugins/`). The data directory is deployment configuration, outside the app: a `startApp`
2268
+ option set by the app's own start script (no environment variables: owner, "no environment variables"), default a
2269
+ dot directory named after the app in the home directory; it holds the store, the user's plugins root, logs and the
2270
+ workers' data. The server is fixed (one per app, clients connect to it). Workers are a list of worker kinds the app
2271
+ defines, each a host kind with the app's own params (sharding is the app's: a project, a tenant, a document; the kit
2272
+ knows no sessions) and how to start it (entry, start data per address, principal, idle time: the workers
2273
+ component's options); zero kinds is a server with clients only (a chat app).
2274
+ - **The app kit, point 2: principals and users (owner, 2026-11-01, by voice: "yeah I guess. just make it nice and
2275
+ concise and easy to use and flexible"; "too opinionated might be a problem"):** three things kept apart. (1) The
2276
+ principal is tiny (a kind and an id: it travels on every call and is part of objects' identity) and the app defines
2277
+ its type; the kit requires only the variants it needs itself (the server and its worker kinds). (2) User data
2278
+ (avatar, bio, availability) is ordinary app data in objects keyed by the user's id, never in the principal. (3)
2279
+ Authentication turns a connection into a principal. The kit ships optional plugins: a users plugin storing accounts
2280
+ with a profile schema the app supplies (a Slack-like profile or just a name), and authenticator plugins (device
2281
+ pairing, invite links, an ephemeral one for an IRC-like "pick a name and you're in": a fresh id with the name, no
2282
+ stored account, gone with the connection); an app picks, replaces or writes its own. Rejected: a fixed principal type
2283
+ with roles.
2284
+ - **The app kit, point 3: authentication (owner, 2026-11-01, by voice: "yeah, I guess something like that"):** (a)
2285
+ no owner by loopback alone (any local process or OS user could claim it): the start script prints a link with a
2286
+ one-time token (as Jupyter), whose visit sets a signed `HttpOnly` owner cookie for that browser; drive-by requests
2287
+ from pages in that browser stay blocked by the security baseline (foreign requests, also other localhost ports,
2288
+ never authenticated; Host and Origin checks). (b) Deny by default, in the HTTP component itself (a change to slice
2289
+ 10's routes and sockets): a request without a principal is refused before plugin code unless its route or socket is
2290
+ marked public (a landing page, a webhook, the ephemeral name login, which hands out a principal). Authenticators stay
2291
+ the `Authenticators` registry, filled by the authenticator plugins the app chose; a session helper issues and revokes
2292
+ sessions so authenticators do not each reinvent cookies and logout.
2293
+ (c) Command-line clients (owner: "that sounds good"; the TUI client too) connect over the same WebSocket with a
2294
+ token in a header (no Origin, no Fetch Metadata: not a browser request; the token decides): on the same machine an
2295
+ owner token file in the data directory readable only by the OS user running the app; from another machine a device
2296
+ pairing flow (the tool shows a code and a link, the owner approves in a browser, the tool stores the token), the
2297
+ token a session like any other, listed and revocable.
2298
+ - **The app kit, point 4: storage (owner, 2026-11-01, by voice: "yeah, I suppose that sounds right"):** in the data
2299
+ directory the server has one store; each worker has its own directory under its worker kind, named by its address,
2300
+ with its store in it; the kit opens and closes them. Plugins never use paths of the data directory: their state is
2301
+ in their objects' documents and tables; a plugin that needs raw files gets a subdirectory of its own, owned by one of
2302
+ its objects.
2303
+ - **The app kit, point 5: plugins, files and export (owner, 2026-11-01: "okay", "yeah"):** two plugin roots: the
2304
+ app's built-in `plugins/` (required: a plugin failing at startup stops the app with its error) and the user's in the
2305
+ data directory (failures reported). Installs from git or npm into the user's root. One "apply changes" call builds the
2306
+ dirty plugins and rolls them out to the server, then every running worker; the app exposes it as a button, a
2307
+ command or an agent tool; changes are never applied by themselves. **Files:** the kit ships a files plugin, one owner
2308
+ per address (`self.get(Files, { scope })`: `put`, chunked transfers for large files (design §23), a watchable
2309
+ index, caps), its files in that worker's directory; apps build on it (p3's artifacts: versions, previews, sharing;
2310
+ design §26 #17). Raw directories only where a real path is needed (a git checkout, a library writing its own
2311
+ database), each owned by one object, under the worker's directory. **Export and import per address:** the store's
2312
+ archive plus the worker's directory as one archive; importing it under another address copies or moves it.
2313
+ Refines point 4 (storage): plugins use the files plugin instead of raw subdirectories of their own.
2314
+ - **Slice 11, P3: the base bundle and plugin scripts (owner, 2026-11-01, by voice: "yep, that sounds good"):** the
2315
+ server builds one base bundle per client kind from an entry in the app's source (the app's web start code, its API
2316
+ with the kind's module, Chord, React), content-hashed and cached by browsers forever; a change of base code reloads
2317
+ the page (as a restart of the server). Plugin scripts: at page load the server serves the current set of the kind's
2318
+ plugins as one combined file named by a hash of the set (one request, cacheable); while a page is open, a rollout's
2319
+ changed plugins are fetched one script each. A fresh page makes two requests, an update downloads only what changed.
2320
+ Rejected: a prebuilt base bundle shipped with the app (could disagree with the plugins' build).
2321
+ - **Slice 11, P4: new plugin scripts apply in place (owner, 2026-11-01: "yep, agree"):** a page's host reloads the
2322
+ changed plugins per plugin (`host.reload`, as on the server): objects swap or restart, the UI re-renders from its
2323
+ registries, keeping what is on screen (scroll, a half-typed message). A change of base code still reloads the whole
2324
+ page. Rejected: reloading the page for every plugin change (p2's behaviour, which lost the screen on every agent
2325
+ edit).
2326
+ - **Slice 11, P5: pages learn of new scripts through Chord (owner, 2026-11-01: "agree"):** the web component serves a
2327
+ small object whose state is the current set of the kind's plugin scripts (names, digests, URLs); every page holds
2328
+ it; a rollout publishes the new set without waiting for pages; each page gets one frame and fetches what changed;
2329
+ after a reconnect the hold's snapshot brings the current set, so a page catches up on anything it missed. Rejected:
2330
+ polling a manifest (p2: every 5 s while visible).
2331
+ - **Slice 11, P6: the connection loop (owner, 2026-11-01: "agree"):** p2's `keepConnected`: after a drop, reconnect
2332
+ with a growing pause (250 ms to 5 s); try at once when the tab becomes visible, the browser comes back online or the
2333
+ network changes; replace a socket open but silent; stop when the server reports an outdated protocol (the page
2334
+ reloads) or the app's `canRetry` says no (a guest's share ended). References keep their last values and wait while
2335
+ down, so the UI shows a banner from the connection state. A network reconnect, not a crash restart.
2336
+ - **Slice 11, P7: React bindings (owner, 2026-11-01: "we definitely want something like that"; p2's `useObject`,
2337
+ `useReadable`, `useRegistry` in `runtime/web.tsx`):** an optional subpath (`chord/react`): `HostProvider`,
2338
+ `useHold(Type, params)`, `useWatch(watchable)`, `useRegistry(R)`, `useRefState(ref)`; releasing holds on unmount,
2339
+ and a component unmounted by a reload never counts as leaving (design §21). No components, no styling; non-React apps
2340
+ never load it.
2341
+ - **Slice 11, P8: the terminal client always runs sandboxed (**superseded in part** by "The terminal's sandbox is a
2342
+ child process", 2026-11-02: no longer a worker thread) (owner, 2026-11-01: "the terminal should always use the
2343
+ sandbox approach"):** `chord/tui` is a Node client host that connects with a token (decision "The app kit, point
2344
+ 3", (c)), downloads the base bundle and the TUI plugin scripts from the server and runs the app's TUI entry, always
2345
+ inside p2's sandbox: a permission-dropped worker thread with a bridge terminal, the trusted launcher outside it
2346
+ holding the credentials, the terminal and local preferences. No unsandboxed mode, also for the owner's own server.
2347
+ - **Slice 11, P9: a terminal component scope, library-agnostic (owner, 2026-11-01: "yeah, sure"; p2's `TuiScope`
2348
+ in `src/runtime/tui.ts`):** the counterpart of the React bindings: a scope tied to a component's life with `hold`,
2349
+ `watch` and `registry`, releasing everything on dispose, calling one `changed` callback the app wires to its redraw
2350
+ (one line with pi-tui; any library works). p2's retry with backoff for gone references is not carried over (v2
2351
+ references wait and rebind by themselves). No pi-tui dependency.
2352
+ - **`chord.plugins` is a package's manifest of plugins, as pi's (owner, 2026-11-01: "yes"; slice 12 review, Opus H1;
2353
+ refines "The plugin model ... imports and builds" (Imports) and `slice-12.md` §2, §5):** a package (npm, git, local)
2354
+ declares the plugins it provides in its `package.json`, `"chord": { "plugins": ["./notes", "./search"] }` (globs and
2355
+ exclusions as pi's `pi.extensions`); installing it picks up exactly those folders. Without the field discovery is
2356
+ conventional: the package is one plugin when its top has facet files, else its subfolders with a `package.json`.
2357
+ Depending on a plugin inside a multi-plugin repository is an ordinary dependency on the whole repository (npm version
2358
+ or `github:bob/plugins#<commit>`, installed by plain npm), imported by path into it (`@bob/plugins/notes/contract.ts`)
2359
+ or through its `exports`. Removed: the dependency form of `chord.plugins` (`"name": "github:...#ref:subfolder"`) and
2360
+ its own clone-and-link installer (it could delete outside `node_modules`).
2361
+ - **Looks and themes (owner, 2026-11-02: "i suppose", then "do all the things"; proposal `docs/theming/`):**
2362
+ shared components come from the app's per-kind API modules (one instance in the base bundle); looks are design
2363
+ tokens, CSS custom properties on `:root` on the web, which the app's components and plugins' CSS read; plugins scope
2364
+ their CSS with CSS modules (`*.module.css`); the terminal gets the app's components and a theme lookup by role. A
2365
+ theme is data in one registry, `Themes`, shipped by the kit in its shared module with the code that writes the active
2366
+ theme as CSS variables (web) and the lookup (TUI): a base set of roles the kit defines (accent, bg, fg, muted, error,
2367
+ warning, success), apps may add roles; a user picks one dark and one light theme, the system preference chooses;
2368
+ `web` extras may name fonts and assets of the plugin under the page's CSP. Plugins contribute themes; they cannot
2369
+ change the app's base CSS or components. The open questions' recommendations taken.
2370
+ - **p3 meeting, 2026-11-02 (Mario and Armin; Armin's summary, relayed by the owner):** session sync: the session
2371
+ repository talks to the server process to enumerate sessions; the session index in the server is updated by the
2372
+ worker from events and sent to the server by RPC. Sync to R2 goes through Radius: Radius keys each session by id and
2373
+ keeps its last sequence number, tells pi which sessions it has with their last sequence numbers (optimization: time
2374
+ of last sync), and pi sends the delta commit log from that sequence number; R2 stores the SQLite file and the commit
2375
+ log (SQLite DO to R2); sessions have an owner id. The index is a Chord table replicated like sessions; its entry is
2376
+ updated only on a session metadata change, a session idle-state change and a subagent idle-state change. p3: each
2377
+ host (server, worker, tui, web) has a kernel loading the plugins with contribution points via registries; p2's look
2378
+ and feel made great; new session storage; hands (machine daemons) good, secure and connecting proactively.
2379
+ - **App kit naming and export (owner, 2026-11-02: "startApp > startServer?", "am i exporting an address? i'm exporting
2380
+ the store a host created", "export should stop nothing before the export, it slots into the commit queue"; refines
2381
+ slice 13):** `startApp` is `startServer` (the pair `startServer`/`startWorker`; an app always has one server).
2382
+ `exportAddress`/`importAddress` are `exportData(address, path)`/`importData(path, address)`: the data a host created
2383
+ (its store and files directory). Export stops nothing: a running worker exports its own store (`store.export()` holds
2384
+ the commit queue while it reads, so the archive is one consistent point) and its finished files to the given path,
2385
+ asked through a kit object on the worker; with no worker running, the server takes the worker's process lock (no
2386
+ worker can start meanwhile) and reads the store itself. Import still takes the lock and refuses an address whose
2387
+ directory exists (nothing is overwritten; replacing a session is delete, then import; a copy is an import under a new
2388
+ address); registering the imported host in the app's index is the app's.
2389
+ - **p3 notes, 2026-11-02 (owner):** subagents may start with the main agent's context: `context: "inherit"` forks the
2390
+ parent conversation (Durable's `conversation.fork`) at the last entry before the assistant message with the spawning
2391
+ call; model, tools and location may differ; the tool's description names the cost (prompt cache only with the same
2392
+ model). pi's `/tree` (in-session branches) is wanted for p3: proposed as forks inside the session's store (edit an
2393
+ earlier message to fork, a branch list to switch; a full navigator and branch summaries later).
2394
+ - **Sync applies batches with the host stopped (owner, 2026-11-02: "sure"; sync example `60-session-sync`):**
2395
+ `applyBatch` refuses while a host holds claims on the store; the sync layer stops the session's worker, applies,
2396
+ restarts it. Usually nothing runs: Radius receiving uploads runs no host for a session it does not drive, and taking
2397
+ a session back pulls before its worker starts. A live view of a replica (watching a session continue elsewhere) is a
2398
+ later refinement: fewer pulls while watched, or a read-only view without the worker.
2399
+ - **The terminal's sandbox is a child process (owner, 2026-11-02: "yes"; refines "Slice 11, P8"; slice 11 recheck,
2400
+ Opus E1, E2):** the hardened worker thread was escaped twice in one review round, because downloaded code shares
2401
+ one JavaScript realm with Node's internals: E1 replaced `Set.prototype.has`, which the `require` allowlist used, and
2402
+ `require("tty")` wrote raw to the terminal; E2 patched `EventEmitter.prototype.emit` and caught Node's real
2403
+ `process` when Node emitted a warning on it (environment, `kill`, every builtin). Each fix closes one road and the
2404
+ next reachable internal is another. So the sandbox is a child process of the launcher's Node binary: stdin, stdout
2405
+ and stderr are pipes to the launcher (no descriptor is the terminal), its environment empty (only
2406
+ `LD_LIBRARY_PATH` copied when the launcher has one: needed to run the same binary, set by the user), its directory
2407
+ a private empty one, `--permission` with nothing granted (no file system, network, child processes, workers,
2408
+ addons, inspector, WASI), `--no-experimental-sqlite` (SQLite opens files past the permission model, Node gist
2409
+ https://gist.github.com/badlogic/a83cd80d0669dbcf77e029ac7b25ac96). Messages are length-prefixed binary frames on
2410
+ the pipes, not Node's IPC. The JavaScript layer stays as a second barrier (evaluation only through the bootstrap,
2411
+ the builtin allowlist, a frozen substitute `process`); the V8 code-generation toggle goes (the bridge evaluates
2412
+ strings anyway). Known gaps, accepted until Node closes them: the child can signal the user's processes
2413
+ (`process.kill`) and allocate memory outside its heap. Cost: one more Node process per terminal session (measured
2414
+ by the build against the worker). Back to a worker only if Node ships per-worker permissions.
2415
+ - **Messages and languages in the app kit (owner, 2026-11-03: "i18n proposal is a go"; proposal `docs/i18n/`):**
2416
+ text people read is translated per viewer, never per server. (1) **Messages in code, per plugin:**
2417
+ `defineMessages(namespace, { en: {...}, de: {...} })` in the app's shared API returns a typed `t`; the first catalog
2418
+ defines the keys and argument types, every other catalog must match them (a missing key or a wrong argument is a
2419
+ type error); values may be functions (plurals, parameters in each language's order); dates and numbers through
2420
+ `Intl` helpers (`formatTime`, `formatNumber`) in the viewer's locale. Named keys (`newNote`), the English catalog the
2421
+ source of truth. A plugin keeps its messages in one shared file (`messages.ts`) every facet that renders imports.
2422
+ (2) **Languages are contributions:** a `Translations` registry (`{ namespace, locale, messages, override? }`) in the
2423
+ shared API; `MessagesOf<typeof t>` types a translation; a missing key falls back to the locale's base (`pt-BR` to
2424
+ `pt`), then to the first catalog, key by key, reported once; two contributions for one namespace and locale: the first
2425
+ by plugin name wins, the other reported; a plugin's own catalogs are its built-in contributions, `override: true`
2426
+ replaces one. Contributions are per host, so a translation plugin keeps its catalogs in a shared file and contributes
2427
+ them from each facet that renders (web, tui; server and worker facets only when they render final text, an email or
2428
+ a push notification). Catalogs never cross links (they hold functions). (3) **The locale is a user preference**
2429
+ (kept per principal like the theme choice), defaulting to the browser's `navigator.languages` or the system locale
2430
+ in the terminal, matched against the locales the catalogs offer; switching re-renders live (`useMessages(t)` on the
2431
+ web, the scope's redraw in the terminal), nothing reloads; the kit sets `lang` and `dir` on the page's root
2432
+ (right-to-left through logical CSS properties, the app's components). (4) **Servers and workers send keys, not
2433
+ text:** `t.message(key, ...args)` builds typed data `{ namespace, key, params }` (a `Message<"notes">`) for object
2434
+ state, notices, tool summaries and errors shown to people (a code plus parameters); each client renders it with
2435
+ `t.render` in its own language (checked at runtime across a link: an unknown key shows the key). Never translated:
2436
+ the model's prompts and answers, log lines, error codes. No translator file format now; an export/import tool to JSON
2437
+ when someone needs it. Built after slice 11 (carry-over "After slice 11"). **Refined in the build (2026-11-03;
2438
+ asked by the build, accepted by the main session):** (1) a `Translations` contribution is a list
2439
+ (`readonly Translation[]`): a contributor contributes once per registry, and the proposal's French plugin
2440
+ contributes `notes/fr` and `app/fr` from one facet. (2) A plugin's own catalogs are its built-in contributions
2441
+ literally: each facet that renders contributes them (`self.contribute(Translations, notes.translations)`), because
2442
+ `defineMessages` runs at module evaluation, tied to no host, and a host must know every locale its catalogs offer
2443
+ (the device default, a settings list); conflicts and `override` as decided (`override: true` wins over a catalog
2444
+ without it), and `t` still renders its own catalogs if a facet forgot the contribution. (3) `t` is per host:
2445
+ `useMessages(t)` returns `{ t, locale }` bound to the `HostProvider`'s host and its language, `scope.messages(t)`
2446
+ likewise (a change redraws); the plain `t(key, ...args)` imported from `messages.ts` renders in the language of the
2447
+ render under way (a synchronous render scope: nested `t` calls, `formatTime` in a message function), outside one in
2448
+ the first catalog (servers, tests, logs); no module-level current locale, since one process may run many hosts and a
2449
+ server renders per recipient. Also from the build: the typed message is `MessageData<N, K>` (the link protocol owns
2450
+ `Message`), `t.message` takes only keys whose arguments are JSON scalars, and the theme choice and the locale choice
2451
+ share one per-principal preference store (`src/surfaces/preference.ts`).
2452
+ - **Chord v2 becomes the package's root (owner, 2026-11-03: "src/old", "subpaths", "yes", "yes", "no, chord is
2453
+ wip"; for merging pi-private into pi this week; trial `/home/badlogic/chord-root-move/report.md`):** `src/next` is
2454
+ the package's `src` (its main export), the Node parts `src/next-node` are `src/node` with short subpaths (`/http`,
2455
+ `/workers`, `/plugins`, `/plugins/runtime`, `/app`, and slice 11's `/web`, `/surfaces`, `/react`, `/tui`,
2456
+ `/tui/launcher`; `/surfaces` was `/next/web/server`, renamed in the run because it exports the terminal surface's
2457
+ server side too), the shared foundations Durable and env use get their own subpaths (`/json`, `/watch`, next to
2458
+ `/context`, `/delta`, `/sqlite`), `test/next` and `docs/next` move up (`test/`, `docs/`). Chord v1, which only p2
2459
+ still runs on, moves to `src/old` (exported as `/old`, `/old/node`; its tests `test/old`, its docs `docs/old`), not
2460
+ into p2 (its vitest tests and about 140 imports would need porting); it is deleted in the change that moves p2 to
2461
+ v2. No version bump: Chord is work in progress. Done by a re-runnable script (move, import and link rewrites,
2462
+ generated exports and aliases) on the base that includes slice 11; p2's imports updated in private.
2463
+ - **p2 moves to Chord v2, then Chord v1 is deleted (owner, 2026-11-03: yes to the plan's eleven points; plan
2464
+ `/tmp/p2-port/plan.md`):** (1) way (a): p2's kernel, workers and plugin loading move to v2 (`createHost`,
2465
+ `createWorkers`, `createPlugins`), p2 keeps its own data directory, auth, devices and sharing, so no data migrates
2466
+ (Chord v1 stored nothing for p2; every file in its data directory is p2's or Durable's); (2) Radius Cloud Brain stays
2467
+ on its last v1 bundle and moves separately; (3) p2's web UI is p2 code again (brought back from bundled-ui) and is
2468
+ ported with p2; (4) `createPlugins` replaces p2's own loader (which leaked a module copy per reload); (5) p2 keeps its
2469
+ own SQLite files (catalog, shares, search, subagents) as natives (the rule against plugin databases is p3's); (6) p2's
2470
+ terminal uses Chord's child-process sandbox, exported upstream first; (7) `createHttp` for p2 later, optional; (8)
2471
+ Chord v1 (`src/old`) is deleted right after the switch; (9) p2's v1 plugin tutorial is retired for Chord's guide;
2472
+ (10) the phone gets it once the switch is green on slayer (owner, later the same day: "no hours on slayer first";
2473
+ was: after 1-2 days on slayer), about 10 minutes down, rollback by checkout; (11) a small shim
2474
+ reloads pages left open across the deploy, removed with v1. Server, workers, the terminal bundle and the web bundle
2475
+ switch in one deploy (v1 and v2 links do not talk); prep and cleanup steps deploy on their own. **Done
2476
+ 2026-11-03:** the switch is private `16329187c` (on the phone since 12:38, data unchanged); Chord v1 deleted upstream
2477
+ `ed4f48840` (owner: "delete old chord, clean up anything related to old chord"), p2's shim, v1 tutorial and v1
2478
+ fixture generator in private `7dadff697`.
2479
+ - **Mistakes that wait are reported at once (owner, 2026-11-03: "yes"; p2 port footguns 1, 2, 9,
2480
+ `/home/badlogic/p2-port/chord-footguns.md`):** requests keep waiting for providers (slice 7a), but a mistake no
2481
+ longer looks like slowness. (a) `get`, `hold` and `registry` on a host that has not loaded throw at once ("call
2482
+ `host.load` first"; a client host with nothing to load calls `load([])`). (b) When a load or reload publishes, a
2483
+ registry with contributions and no provider is reported through `onError` at once, naming the registry and its
2484
+ contributors. (c) A request waiting unbound (`not_served`, `waiting`) is reported once through `onError` after 5 s,
2485
+ naming what it waits for; it keeps waiting until its `readyTimeout` as before. (d) An invalid plugin name is a
2486
+ programming error: `load` and `reload` throw instead of listing it in the report's `failed`. (e) (owner, 2026-11-03:
2487
+ "add report"; p2 port footgun 10) Work a seal cancels before it ran (a document change or transaction waiting on its
2488
+ object's line, started with a root's, handler's or attempt's context that ended first) is also reported through
2489
+ `onError`, naming the object and the scope, besides rejecting: an empty `catch` cannot hide a change that never
2490
+ happened. The rule itself stays: what is started with a context ends with it; await it.
2491
+ - **No limit on calls in progress per link; references in a method act as the caller (owner, 2026-11-03: "i guess
2492
+ it's fine without, guess we document it"; p2 port footguns 7, 11):** v1's `maxCalls` per link has no counterpart and
2493
+ gets none (the call window N bounds calls per origin; part limits bound sizes); documented. `ctx.get` in a method
2494
+ acts as the method's caller (decision of slice 4a, unchanged): a server object acting as itself towards a worker
2495
+ takes the reference with `self.get` in its own object or a server root; documented with p2's `Search.rebuildIndex`
2496
+ example in the guide and the porting notes.
2497
+ - **Tutorial gaps (owner, 2026-11-03: "4. export", "3. ok", "1. b"; the tutorial's findings):** (4) Chord's root
2498
+ exports `invalid` and `forbidden`; an app's API re-exports them, so a plugin throws a decided `ChordError`. (3) An app's
2499
+ API is a real package (`"name": "@acme/app"`, `"exports": { "./v1": ..., "./v1/web": ... }`), so Node,
2500
+ TypeScript and plugin builds resolve it alike and tests import plugins' contract files; the tutorial and
2501
+ `plugin-authoring.md` show it, no Chord change. (1) **Refines "Slice 11, P4":** a component of a reloaded web plugin
2502
+ is a new function and remounts (its `useState` starts over); state that must survive a plugin update (a half-typed
2503
+ message, a scroll position) is kept by the page host: `usePageState(key, initial)` in `chord/react` (and the same
2504
+ per-key store for `TuiScope`), untouched by plugin reloads, gone with the page. Rejected: React Fast Refresh (a Babel
2505
+ or SWC transform in every web build, fragile when hooks change). Open: a live view of table rows (the tutorial's
2506
+ and 59-chat's `self.open` read at activation, which makes every reload a restart): to explain first.
2507
+ - **Live queries over an object's own tables (owner, 2026-11-03: "if self.query is easy enough, and we document it as
2508
+ 'there may be dragons' then i guess it's ok"; resolves the tutorial's gap 2):** `self.query(table, q, limit)` in a
2509
+ factory returns a `Watchable` of the query's rows, usable as a state member and a derive source. Chord runs the
2510
+ query before the object goes live (like loading its documents: no `self.open`, so a reload swaps the object), and
2511
+ again in the publication step of every commit of the object that touched that table, diffing the new rows against
2512
+ the previous ones (ops to watchers and holds, as for any member). Only the object's own tables (only it writes them,
2513
+ so its commits are the exact invalidation); the query is fixed for the attempt (params decide it); a limit is
2514
+ required (the third argument, as `page` takes it: the query language keeps the limit an argument of the runner;
2515
+ refused `invalid/args` at declaration; build, 2026-11-03). Cost: one query per touching commit, bounded by the
2516
+ limit; documented with a warning ("there may be dragons": not for tables written hundreds of times a second).
2517
+ Incremental maintenance of the result (an ordered, limited query with row-level changes kept in memory, re-running
2518
+ only when a removal empties the window or a bulk write hides which rows changed) is carry-over for when a case
2519
+ needs it. Rejected: a window document written beside the table (rows stored twice; app code mirroring every update
2520
+ and delete).
2521
+ - **Swaps: dependents first; calls during a swap never see `not_ready` (owner, 2026-11-04: "if swaps can be invisible
2522
+ to dependents that's cool, but I fear correctness bugs"; analysis `/tmp/swap-analysis.md`):** the problem: a reload
2523
+ swapping a provider and its pure dependent (61's `todo.store` and `todo.todos(groceries)`) parked the dependent
2524
+ every time. `#objects` keeps objects in the order they went live, providers before dependents, and fates started in
2525
+ that order: the store's swap showed `loading`, the view's new attempt bound it, parked, withdrew its contributions
2526
+ (a registry gap) and unbound its holds over links (`loading` on the client). Worse, a parked swap resolved the gate:
2527
+ a call over a link whose hold was still bound (the old instance draining) got `not_ready` while the client showed
2528
+ `live`. Decided: (1) the gate (`ChordObject.call`) holds a call while any attempt activates (a swap, parked or not,
2529
+ a restart after its drain) until the object is live, failed or ended, bounded by `readyTimeout`
2530
+ (`unavailable/park_timeout`, as a call parked on a reference); a parked attempt showing `failed { waiting }`
2531
+ rejects it with that cause. When a parked swap's hold then leaves the old instance it tells the consumer `loading`
2532
+ and answers its calls in flight `unavailable` (the rule for any object leaving `live`): an error consistent with
2533
+ what the client sees. (2) A reload starts the removals first, then the other fates dependents first (the reverse
2534
+ of `#objects`); the report keeps the objects' order. A dependent's new attempt binds its provider while that is
2535
+ still live, passes its check and reads the old instance until the new one is live (one frame): the path a
2536
+ provider-only reload already takes, so no new state. A pure dependent still parks only when its new code needs the
2537
+ provider's new version (the provider's `served` changes with its own fate, so the request waits unbound, then
2538
+ parks on the swap; it never reads that version off the old instance), when its provider went live after it (a
2539
+ provider that failed live and came back), or when it needs something that is not live (a new or missing
2540
+ dependency); so a call waiting at the gate when a swap parks is rare. Rejected: same-host references showing
2541
+ `live` during a swap by changing `ChordObject.state` (holds, keyed pins, versions and `variant` read it as "a live
2542
+ instance exists"; a per-binding view is possible later, the blip is cosmetic). Carry-over: calls surviving a
2543
+ `loading` unbind (slices.md, "Later: reload").
2544
+ - **Telemetry from browser pages (owner, 2026-11-04: "I just need this implemented"; "it couldn't really fuck the
2545
+ server up"; tutorial gap):** a page records spans in memory (the click and the calls it makes, carriers on its calls
2546
+ as any client) and sends finished spans in batches every few seconds and when hidden, over its existing Chord link,
2547
+ to a telemetry object on the server (`ingest(batch)`): sign-in, principals, part limits and flow control apply as for
2548
+ any call; an app may admit anonymous pages. The server treats them as untrusted, best effort: batch size and rate
2549
+ capped per connection, every field checked and capped (names, attributes, events, ids printable ASCII), spans marked
2550
+ as from that browser (`chord.host` `browser`, ids prefixed per sender by the store), never overwriting or posing as
2551
+ server spans; the store writes finished spans it receives (a new ingest path). The viewer clamps a browser span
2552
+ against its server children (clock skew). General Chord: the page recorder in the web surface, the ingest facet, the
2553
+ store's ingest; the app kit serves the object when given a telemetry store and `startPage` records when the app asks.
2554
+ - **Who a span was for (owner, 2026-11-04: "that hook could also allow us to resolve the username and put that in the
2555
+ telemetry if we're fine with that"; refines "IDs are attributes of spans" for principals):** method and call spans
2556
+ record the principal's attributes, by default its principal key (`chord.principal`), never other app data. A hook in
2557
+ the host's telemetry options maps a principal to the attributes to record (none, the key, or more, e.g. a user name
2558
+ the app resolves from its own data). Chord calls the hook for each span it records with a principal; the hook must be
2559
+ synchronous and cheap, and caching is the hook's own (owner: "the caching should be done by the hook"). The hook is
2560
+ a Chord host option (`createHost({ telemetry, principalAttributes })`); the app kit passes it through `startServer`
2561
+ and `startWorker`. Browser spans are stamped with the principal of the
2562
+ link their batch came over. The viewer stays the owner's and filters by principal.
2563
+ - **The server is the one place telemetry is stored (owner, 2026-11-04: "who stores all the telemetry? There must be a
2564
+ single place ... the client and the worker would just talk to that object on the server"; refines "Telemetry from
2565
+ browser pages" and "Telemetry store"):** the server owns the telemetry store and is its only writer. Browsers and
2566
+ workers record in memory and send finished spans in batches over the links they already have to the server's
2567
+ telemetry object (`ingest(batch)`), which checks, stamps (`chord.host`, the link's principal for browsers) and writes
2568
+ them. Browser batches get the strict checks and caps; a worker's (the server's own child, a trusted link) are
2569
+ written as they are, stamped with the worker's host. No worker opens the store file; nothing passes its path to
2570
+ workers. The store stays its own SQLite file in the data directory, not the server's Chord store (high volume, 7-day
2571
+ retention: it would compete with app data in the commit queue and travel in exports and sync).
2572
+ - **Calls in flight survive a `loading` unbind (owner, 2026-11-04: "dunno, ask reviewers"; both reviewers, Opus
2573
+ `/tmp/calls-survive-opus.md` and `/tmp/calls-survive-2.md`: build it; refines slice 7b's wire rule "A `failed` on a
2574
+ bound hold unbinds it"):** a hold unbinding with `loading` (a restart, a parked swap, a live failure retrying) keeps
2575
+ its calls in flight instead of rejecting them `unavailable`; `failed` and removal still reject. Each kept call gets
2576
+ exactly one answer from what it already waits on: the instance it runs on (its result within `closeTimeout`, else
2577
+ `released` at the seal) or the gate (the new code's result, `park_timeout` after `readyTimeout`, the parked cause, or
2578
+ the object's end). Not rare: every restart of an object with natives hit it (a client still `live` during the drain
2579
+ sends a call that waited at the gate). Calls sent before the client learns of the `loading` and arriving after the
2580
+ unbind wait at the gate too (the unbound hold keeps its old method names, as the swap-in-place rebind does), never
2581
+ `not_ready`. Consumers and relays need no change (they keep sent calls across `loading`; a method the new variant
2582
+ lacks fails `invalid/member`, a dropped version `contract_mismatch`). Cost: kept calls hold call-window slots up to
2583
+ `readyTimeout` while the object is parked.
2584
+ - **The rollout order stays; no `loading` grace (owner, 2026-11-04: "either order has the problem, just in the
2585
+ opposite host", "keep"; "no loading grace"; part of the restart-only discussion, `/tmp/restart-study.md`):** (1) The
2586
+ kit keeps server, then workers, then clients. A contract change between hosts made in one rollout leaves a gap
2587
+ whatever the order: a consumer on an earlier step whose provider on a later step moves to a new version waits
2588
+ `failed { waiting }` (with its last values) until the provider's step commits (server first: server consumers of
2589
+ worker types; workers first: worker consumers of server types). `accepts` cannot close it within one rollout (the
2590
+ old provider never serves the new version). Documented with the recipe that avoids it: two applies, the provider
2591
+ first (serving the new version and accepting the old one), then its consumers. The reload suite's two `it.fails`
2592
+ targets for it become documented-behaviour tests. Rejected: the kit ordering steps by the direction of the changed
2593
+ contracts (works only when every change points one way). (2) No grace on consumers' `loading` (a reference with
2594
+ values reporting `loading` only after ~300 ms): rejected from the restart-only proposal.
2595
+ - **Reload restarts every affected object; old work finishes on old code (owner, 2026-11-05: "if you think the new
2596
+ model is good, just ... fix p2"; the final design round: `/tmp/reload-final-brief.md`, three reviewers, model LS3
2597
+ `/tmp/reload-final-step3-restart-study.md`, accepted by the attacking reviewer `/tmp/reload-final-step3-reload-review-2.md`;
2598
+ supersedes the pure swap of slice 7b §5, "Swaps: dependents first", parked swaps and the swap handover of routines
2599
+ and jobs):** the problem: a restart makes new calls wait until the old attempt drained, so any wait of old work on
2600
+ something that needs the new attempt closed a cycle that only `closeTimeout` broke (a provider's running call
2601
+ reaching its dependent through `ctx.get` while both restart; re-entry; mutual calls; the same across hosts). The
2602
+ rules: (1) a reload restarts every live object of a changed plugin (no swap in place); documents and tables stay,
2603
+ F-a, F-b, F-c of the restart study apply. (2) **Old work** (the calls running at the commit and every call they cause,
2604
+ found through the call chain in `ctx` on one host and an `old` flag on `call` over trusted links, never honoured from
2605
+ untrusted ones) runs on the target's open old attempt; a getter of old work for a version the new code dropped binds
2606
+ the open old attempt or fails at once. (3) The old attempt seals once its own calls and the old work routed to it
2607
+ drained and its successor's requests (as staging recorded them, bound after every fate of the reload started) are
2608
+ live or failed for a reason other than another host's version skew, bounded by the reload drain bound
2609
+ `reloadDrainTimeout` (default 5 s, at most `closeTimeout`); only then the successor's factory runs, so no new
2610
+ declaration meets old work. (4) **New work** waits at the gate and runs on new code. (5) A same-host consumer whose
2611
+ version the open old attempt binds sees the object live meanwhile (its new code may read the provider's old state
2612
+ for up to the drain). (6) A wait of old work that its own open old attempt blocks through parked dependencies or an
2613
+ opener's gate waits fails at once, `unavailable/reload_cycle`, naming the chain. What remains (waits that leave
2614
+ Chord: roots opened without the method's `ctx`, Durable tasks, an app's locks) ends at the reload drain bound. The
2615
+ bound cuts any method still running then (`released`): long work belongs in jobs (in-memory runs end `aborted
2616
+ { reload }`, persisted ones resume per `resume`). A restart whose successor finds a failed request is reported
2617
+ `parked { cause }`. Built as one change with the restart-only removal of the swap.
2618
+ - **A call's answer follows its state (owner, 2026-11-05: "seems doable without any big downsides"; Petr's report
2619
+ that p2's UI is not optimistic):** before a host sends a `result` (or forwards one as a relay), it sends the page's
2620
+ pending `state` for that origin, past the window if the window is full (bounded by the calls in flight). So when a
2621
+ call resolves on the page, every change the answering host (and a relaying host) committed before answering is
2622
+ applied there. Pages show optimistic values with React's `useOptimistic` (a helper for `TuiScope`), dropped when the
2623
+ call settles; Chord changes no replicated state. Not covered: changes committed later (a method that returns before
2624
+ its effect) or on another host that does not relay the answer. The guide's rule: a method that should feel instant
2625
+ awaits its own changes and keeps them on the host that answers.
2626
+ - **App authoring rules confirmed by the p2 audit (owner, 2026-11-05):** state that lives outside Chord (a native's
2627
+ store, a Durable document) is shown through `self.follow` of its synchronous observer, never copied into a Chord
2628
+ document by a routine; long work is a job, never a method or a root a method awaits; a root opened inside a method
2629
+ takes the method's `ctx` (without its cancellation); a session's worker owns its data and pushes what the server's
2630
+ index shows (title, branches, working state) to the server inside the method that changed it, never pulling from
2631
+ the server. p2 is rewritten by these rules (audit `/tmp/p2-optimistic-audit.md`).
2632
+ - **Files v2 (owner, 2026-11-05; p2's per-host stores brief):** the kit's `Files` is replaced (`chord.app.files`
2633
+ version 2; the version 1 index, one document holding the whole map, is not migrated: nothing outside tests and
2634
+ examples used it). One object per host, bytes in the host's `files/blobs/`, index in four tables of the host's
2635
+ store (`files`: one row per version; `folders`; `blobs` with a reference count; `paths`, high-water marks) plus two persisted documents
2636
+ (`summary` per space, `usage`). Spaces and paths, folders as index entries, moves in the index only, immutable
2637
+ blobs with versions, `copy` sharing a blob, chunked writes as before, idempotency keys, fixed indexed metadata and
2638
+ free `meta`, a summary per space, `copyFiles` and `filesFacet` exported. Decided in the build: (1) a version's id
2639
+ is the id plugins keep (`FileInfo.id`); paths and version numbers are found through the index. (2) A version
2640
+ number is never used twice for a path (owner's review, 2026-11-05: transcripts and links name an artifact by name
2641
+ and version, so a reused number would show an old card new bytes): a `paths` table keeps each path's highest version
2642
+ ever, through removals of versions, the path and its space; a new version is one past it. A move carries its
2643
+ versions' numbers: the path left keeps its mark, the path moved to takes the higher of the two. (3) Changes of one `Files` object take turns (an in-memory queue
2644
+ per attempt): a version number, a key's uniqueness per space and a blob's count are decided by reads before the
2645
+ one transaction, never by a unique index (`key` is `""` when absent, and index columns cannot be `opt`). (4) A
2646
+ folder listing shows the folder's folders first, then its files' newest versions, each by path; its cursor names
2647
+ the part it continues. (5) Live listings are a second local type, `FilesFolder({ space, folder })`, following
2648
+ (`self.follow`) what `Files` hands it: `self.query` is fixed per attempt and only for the declaring object, and
2649
+ folders are dynamic. `Files` reads every watched folder of a changed space once, inside the changing method (one
2650
+ read per watched folder and change). (6) The summary keeps the newest 20 versions by `created` (ties by id),
2651
+ changed in the same commit as the change (the newest after a removal read before the commit, excluding what it
2652
+ removes). (7) The total cap counts the blobs on disk (the `usage` document), so a copy costs nothing. (8) Blob
2653
+ files a crash left without their commit are swept when the object goes live (one index read per blob file, under
2654
+ the change queue). (9) `copyFiles` imports the part of the source store's export that is `Files`' own into a new
2655
+ store and hard-links each blob (a copy where links are refused) while the export holds the source's commit queue,
2656
+ so no removal can delete a blob the copied index names; it refuses an existing target. (10) `meta: null` is stored
2657
+ as absent; space names are `[a-z0-9][a-z0-9._-]{0,63}`, paths at most 1,024 characters without empty, `.` or `..`
2658
+ segments. (11) `cancel(upload)` drops an upload a writer gave up (a server route whose client went away), so it
2659
+ holds neither a slot of the cap nor bytes for the hour until it expires. (12) `@earendil-works/chord/app/files`
2660
+ exports `Files`, `FilesFolder`, `filesFacet`, `copyFiles` and the kit's private directories and stores without the
2661
+ rest of the kit, for an app's worker process that must load neither esbuild nor the server's components (p2's). (13) A
2662
+ write takes an optional `created` for imports (owner, 2026-11-05, after p2's migration dry run: every migrated file
2663
+ had the migration's time, so newest-first orders meant nothing): the version keeps the time it was made elsewhere;
2664
+ the summary's newest versions are kept in `created` order, not write order.
2665
+ - **Workers start from the base code the server booted with (owner, 2026-11-04: "let's do this for armin"; Armin's
2666
+ incident report, Radius artifact 01m6m3xek8fveayqgstayg8w92):** the problem: a worker loads its entry module and the
2667
+ base code it imports from disk when it is forked, while the server keeps what it loaded at boot. An edit of the
2668
+ app's own source while it runs (an agent half way through a change) makes every later fork run other base code than
2669
+ the server: a crash while loading modules fails the start before `started` (and R2 keeps the address failed after
2670
+ the edit is finished), or the two sides run skewed code silently (`PROTOCOL_VERSION` stays 1 until p3). Design §21
2671
+ says base code applies only through a restart of the server; nothing enforced it for workers. Rule: the workers
2672
+ component builds its worker entry once when it is created, as the surfaces build their base bundles at start (one
2673
+ esbuild bundle for Node), and forks every worker of its kind from that snapshot; a change of base code reaches
2674
+ workers only through a server restart, which the kit's `base` mark flags. Plugins are unaffected (workers get the
2675
+ server's built scripts). What the bundle keeps external (Node builtins, packages that cannot be bundled, such as
2676
+ native addons) is settled by the build. p2 adopts it through its own `createWorkers`.
2677
+ - **The kit's owner is a principal the app chooses (owner, 2026-11-06: "ok"; p3's principals are `user`, `guest`,
2678
+ `worker`, `server`, with no `owner` kind):** the kit hardwired the owner as `{ kind: "owner" }` (the owner link,
2679
+ the owner token file and device pairing issue sessions for it; pairing's confirm and approve, invites and a
2680
+ pairing's starter check `kind === "owner"`; `AppOptions` required the variant in the app's principal type). Rule:
2681
+ `startServer`'s `principal` option takes `owner: P` beside `server` and `worker`; every place above issues sessions
2682
+ for that principal and recognizes it by canonical JSON (compared once per principal object, as the principal key);
2683
+ the type requirement and the kit's `OWNER`/`OwnerPrincipal` exports go; `principal.worker` is required only with
2684
+ worker kinds (a type error otherwise; added in the build for p3's first step, which has none). The owner is strict
2685
+ JSON and never the server's principal (refused at start); the kit serves it as start data (`AppInfo.owner`), so
2686
+ invites read it there. Changing `owner` between starts replaces the owner token file and revokes its old session;
2687
+ other sessions issued for the old value stay what they were (the app revokes them if it wants). The kit stays
2688
+ (owner: keep it and change it where it does not fit, rather than wiring p3 from the components: the kit keeps p3's
2689
+ base code, which only a restart changes, small).
2690
+ - **A reloaded UI part shows its loading state (owner, 2026-11-06: "not worth it imo. a short loading state is
2691
+ absolutely fine. this just adds more bugs and footguns"; p3 step 0a, rejected proposal `apps/p3/docs/step-0a/`
2692
+ version 2, reviews `/tmp/p3-step0a-astra.md`, `/tmp/p3-step0a-sol.md`):** after a web or terminal plugin's reload
2693
+ its components remount and make new references, which show `loading` (no values) for one round trip until bound;
2694
+ the rest of the page, and every reference of a server or worker reload, stay live (design §21). Rejected: a
2695
+ per-host table sharing references between UI components (`useHold`, `useRegistry`, `TuiScope`), released when no
2696
+ component and no call uses them. It covered only a same-update replacement of facet-contributed components; it
2697
+ needed the host address in its key, a lease state machine for ended references, counting of parked and queued
2698
+ calls, a borrowed reference type without `release`, and changed presence counts (one entry for several components;
2699
+ a never-settling call keeping it). The three bug fixes of step 0a stay (stylesheets loaded before the plugin reloads,
2700
+ base bundles built with the server's resolution conditions, the restart mark by content).
2701
+ - **A restarting page object's old UI may show unstyled (owner, 2026-11-06: "A"; step 0a):** a page applies a plugin's
2702
+ new stylesheet when its new code is adopted and retires the old one a drawn frame after the reload. UI a plugin's
2703
+ facet contributes is replaced in that update and always styled. UI contributed by an object in the page stays on its
2704
+ old version until the restarted object's successor is live (design §21); classes the edit renamed or removed are
2705
+ unstyled until then. Accepted and documented; apps contribute UI from facets and let objects supply data. Rejected
2706
+ for now: a kernel signal "this plugin's restarts on this host finished" to retire old sheets later.
2707
+ - **Documents and table rows can belong to a host (owner, 2026-11-09: "it looks like we have a plan"; p3 session data,
2708
+ backups and exports):** data needed while a host is not running lives on another host (p3: facts about a session the
2709
+ server needs while its worker is off) but must travel with that host on export, import and delete. (1) `defineState`'s
2710
+ `keyed` takes `true` (string keys, as today) or a **host kind**: keys are then addresses of that kind
2711
+ (`bindings.change(Session({ id }), fn, ctx)`, typed `KeyOf<K>`), stored with the address key in `key` and in a new
2712
+ indexed `host` column of `chord_documents` (NULL for string keys, so a string can never pose as an address); store
2713
+ schema migration to version 3. (2) Tables get a column type `host(Kind)` (at most one per table): the value is an
2714
+ address (written and read as `Address`, stored as its key, indexed automatically); the table catalog records it. (3)
2715
+ Two store operations: export the records and rows belonging to one address (a filtered archive in the existing format:
2716
+ manifest, `document.create`/`document.revision` parts of records with that `host`, per table with a host column its
2717
+ definition and `put` parts of its rows, `end`), and import such an archive into a store that already has data (fresh
2718
+ ids and seqs; an existing record or row for the same address refuses unless the import is an explicit replace; a copy
2719
+ under a new address rewrites only the host columns and keys). Delete retires those records and rows. (4) The kit's
2720
+ per-address export, import and delete (`exportData`, `importData`, archive) include the server's records and rows
2721
+ belonging to that address, next to the host's own store. Migration of imported content happens when the owning
2722
+ object loads it, as for every document. Plugins must not put addresses inside JSON values they expect to be rewritten.
2723
+ To be specified, reviewed and built (p3 status 2026-11-09).
2724
+ - **A plugin's npm dependencies are bundled; external only by opt-in (owner, 2026-10-29: "explicit opt in + (a)"):**
2725
+ a plugin's `dependencies` are installed into its own `node_modules` (`npm install --ignore-scripts`) and bundled into
2726
+ each facet that imports them (host packages, its `peerDependencies`, stay external and are passed in). On Node a
2727
+ plugin may keep named packages external with an explicit list in its `package.json` (`"chord": { "external": [...] }`):
2728
+ server and worker facets then load them from the plugin's `node_modules` (`createRequire`, as pi 1.0 loads extension
2729
+ dependencies), native addons included; they stay in Node's module cache until the process ends, so a new version
2730
+ takes effect after a restart. A browser or TUI facet importing an external package fails to build. Slice 7b
2731
+ (the loader).
2732
+ - **The dependency model is decided (owner, 2026-10-29: "just build this"):** `packages/chord/docs/dependencies/model.md`
2733
+ (explainer `book.md`, `index.html`) as decided, with its remaining four points taken as recommended: (1) the barrier
2734
+ is (a), `load` resolves at the first outcomes of this host's host-lifetime objects (local blockers still in their
2735
+ first attempt are waited for; waits on other hosts or failed dependencies end the first pass); (3) `waiting` is a new
2736
+ `FailureCause` kind; (4) a pure swap whose new factory needs a missing dependency parks the object, no overlap of old
2737
+ and new code; (5) a registry's last good value is kept only while every contribution it was built from is still in
2738
+ the table. Supersedes R18's dependency deadline and backoff, S2's `broken` and S3's backoff, §7's throw for a missing
2739
+ local type and getters' rejection, §21's `gone` on removal and its rollout `updating` deadline, the start barrier's
2740
+ admission gate (holds wait only for the publication). Built as slice 7a (`slice-7a.md`); design.md is folded with
2741
+ the build.
2742
+ - **Cycles across hosts: no probe, a diagnostic (owner, 2026-10-29: "yes"; dependency model, decision 6):** cycles stay
2743
+ found as today: on one host by the walk over waiting edges at `self.get` (exact), across hosts by the activation chain
2744
+ in holds (objects that start one after another). The remaining gap, a ring across hosts whose objects start at the
2745
+ same moment, is not detected: under the dependency model, which has no dependency deadline, such a ring stays parked.
2746
+ It is made visible: an attempt parked longer than its `readyTimeout` on a remote attempt that is itself only
2747
+ `loading` is reported once per parking episode as `HostError` `parked_long { object, waitingFor, since }`; nothing
2748
+ fails or retries. A cycle is a plugin bug (owner: "if no plugin bug, we don't need it"); the guide's rules for plugin
2749
+ authors and agents: (1) a factory declares only what the object needs, never a type that needs it back; (2)
2750
+ back-references and occasional lookups go into methods and routines (`ctx.get`); (3) data flows up through state and
2751
+ registries, not mutual `self.get`; (4) host-lifetime objects on the server never declare worker types in their
2752
+ factory. Rejected: the probe (edge-chasing searches with a check along the recorded trail, `/tmp/model/review*`,
2753
+ seven review rounds; complete only with a re-search timer or a full search lifecycle, the most intricate code in
2754
+ Chord for a plugin bug) and the slice 4b timeout narrowed (it polls chains across hosts again, misses a ring of
2755
+ waiting attempts, and retries in lockstep without finding the ring). Supersedes slice 4b review decision (2)'s
2756
+ timeout once the dependency model is built (slice 7); folded into design §6 with it.
2757
+ - **Getters wait through `waiting` (owner, 2026-10-29: "(b) with a default seems fine; UI stays responsive anyways
2758
+ because it usually holds instead of get"; dependency model, decision 2):** `host.get`, `ctx.get` and `host.registry`
2759
+ wait while the reference is `loading`, `failed { transient }` or `failed { waiting }`, bounded by their
2760
+ `readyTimeout` (default 60 s; callers choose their own), and reject at the deadline with the last cause; only
2761
+ `failed { broken }` rejects at once. Calls stay as they are: on a `loading` reference they park, bounded; on a
2762
+ `failed` one (any cause) they reject at once. A UI holds (`host.hold`) and shows `ref.state`; code that needs the
2763
+ object now gets it. Reviewers (`/tmp/model/stw-*.md`): Fable (b, calls included), Opus (a, plus a `whenLive`
2764
+ helper), Sol and Astra (c: callers park until their own context aborts, with caller deadlines and caps on pending
2765
+ calls; (b)'s default deadline is that cap). Sol's p2 note: cleanup in a `finally` after an abort needs its own
2766
+ bounded context, not the aborted one.
2767
+ - **Only hosts with an address serve remote types (owner, 2026-10-29: "b"):** a host serving a `remote` type needs an
2768
+ address and a principal; `load` refuses a facet that serves one on a host without an address (until now a host with
2769
+ a principal but no address served remote types and placed every one of them "here"). So "here" is decided by the
2770
+ address alone, as the host address of the type. Holds carry their `address` always (required on the wire).
2771
+ - **`hostAddress` instead of `placement` (owner, 2026-10-29: "yes, rename"):** the field of a `remote` contract that
2772
+ computes where its object lives is `hostAddress: (params) => Address` (it returns the address of the host that serves
2773
+ the object, the vocabulary of `createHost({ address })`, routes and holds); supersedes the name of "`placement`, not
2774
+ `host`" (2026-10-21). The error reason `invalid/placement` is `invalid/host_address`; the internal module that decides
2775
+ where a request goes (`host/placement.ts`, `Placement`) is `host/destination.ts`, `Destination`. Earlier entries' and
2776
+ slice specs' "placement" reads "host address"; they keep their wording as history.
2777
+ - **Every remote type declares its placement (owner, 2026-10-29: "yes, let's remove it"):** `placement` is required in
2778
+ `remote()` (a type error without it, and a declaration error at runtime); a remote type without placement, a leftover
2779
+ of slice 3a (remote objects over one link, before host kinds in 4a), is gone, and with it the fallback "here if served
2780
+ here, otherwise the parent" and its re-placement at the first parent link. Every remote request is placed by its
2781
+ address alone (dependency model, `packages/chord/docs/dependencies/`). Tests and examples get `placement: () =>
2782
+ Server()` or their own host kind.
2783
+ - **No client providers for now (owner, 2026-10-28: "yeah, no client providers for now"; reviews
2784
+ `/tmp/barrier-{astra,fable,opus,sol}.md`):** clients stay unaddressable and serve only `local` types (design §3).
2785
+ Client-side capabilities (a phone's camera, clipboard, a page's WebMCP tools) go through an owner's state, as the
2786
+ Questions service: a tool asks an owner store (`request`, `wait`), clients watching its open requests carry them out
2787
+ and answer with a method call (the answer is method arguments, checked against the owner's schema; bytes as `bytes()`
2788
+ leaves, chunked if large); a client may advertise the tools it can run to such a store, tied to its presence. Why:
2789
+ a client as a provider would feed untrusted results, state and ops into server and worker objects (consumers never
2790
+ validate them), needs client addresses, routing down through relays and a principal for clients; the owner pattern
2791
+ needs nothing new in Chord and survives link drops and several devices by the owner's rule.
2792
+ - **Design audit of slices 1-6a (owner, 2026-10-27; `/tmp/design-audit-{opus,sol}.md`):** the built code follows the
2793
+ design except for five bugs, fixed after 6b's commit with the auditors' reproductions as tests: on a failed
2794
+ activation references are released after the natives closed (§6, §13); a dependency going live while the dependent
2795
+ activates retries it (R18, the wake remembered); a call on a `local` object runs with the host's incarnation (§4); a
2796
+ retirement of an unloaded document checks its stored version and fingerprint (§9, R15); `ref.retry()` holds a refused
2797
+ hold again with a new id. Two choices: (6, "a") a link drop while a remote dependency is still loading leaves the
2798
+ dependent `loading` until `readyTimeout` (§6; supersedes slice-3b race 18's transient failure); (7, "a") `host.hold`
2799
+ and `host.get` check params at once, also before `load`; "fails once `load` resolved" stays only for a local type
2800
+ not served here (§7). Stale design text updated: `mapShared` per array, job runs keeping a failed object alive only
2801
+ while an attempt is live or activating, `ctx.state.change(fn, ctx)`, revocation closes child links, routes take a
2802
+ context, §22's `revoke`/`chain`/`limit`, native watches collapsing only past 100 pending values.
2803
+ - **Slice 6b: numbers accepted (owner, 2026-10-27: "yeah the reopen times are ok"; "i accept the 12%": resuming a
2804
+ conversation watch 20 entries back costs about 12% more CPU on `StoreStorage`, once per reconnect, about 0.1 ms per
2805
+ mount, from the extra promise per read in the fork scan):** reopening a store with
2806
+ `StoreStorage` costs about 0.15 ms more than `SqliteStorage` once per open (0.28 to 0.43 ms: six table declarations,
2807
+ the allocator read, compiling the reads); the one-entry commit's mean is raised by one outlier among 20 samples while
2808
+ its p75 is equal or lower. Neither is a hot path or a steady regression. Accepted by the redesign session in fix
2809
+ round 1 and shown to the owner (not his quote): `StoreStorage.close` waits for its commits only (its reads go straight
2810
+ to the store, which stays open).
2811
+ - **The performance and memory bar, and converging reviews (owner, 2026-10-27: "yes"):** supersedes "as good as
2812
+ HEAD in everything" for Chord v2 slices and pi changes. CPU: each hot path (calls, changes, watches, links) within
2813
+ about 5% of HEAD, measured in bursts with alternating processes pinned to one CPU; worse is fixed or goes to the
2814
+ owner with numbers. Memory: allocation per operation and memory retained after GC no worse than HEAD beyond noise,
2815
+ with GC probes for everything that must be collected (ended objects, released references, closed hosts, watches on
2816
+ failed sources). No super bad regressions; small differences within noise are not chased. Reviews converge: a fix
2817
+ round adds no new decisions or features; the same reviewers (restored, with their context) re-review their own
2818
+ findings; when only small items are left, the slice is committed and they are carried over.
2819
+ - **Size is not a goal by itself (owner, 2026-10-27):** no cut pass after slice 6a: "if the user-facing API is great
2820
+ and the underlying implementation fast and correct, we're ok". v2 (`src/next`) is about 13.2k lines without blank lines
2821
+ and comments after 6a's build (about 8.5k before), against the earlier target of 6-7k for all of Chord v2, which this
2822
+ supersedes; the bar stays a great API, HEAD-level speed and memory, and correctness.
2823
+ - **Second spec review: the majority (owner, 2026-10-27: "pick the majority vote for everything, get building"):** the
2824
+ second review of slice 6a (`/tmp/s6a-review2-{astra,fable,opus,sol}.md`) is decided by the reviewers' majority; a
2825
+ point raised by one reviewer takes that reviewer's recommendation; ties take the simpler single mechanism. Folded
2826
+ into slice-6a.md (section 9 and section 15, 27 rules) and design.md. The main points: (1) **migrations by
2827
+ `evolveTable`** (all four): a base `defineTable` and one `evolveTable(prev, edits)` per version, each materialized and
2828
+ fingerprinted (`TABLE_FORM_VERSION`), edits `rename add drop index dropIndex json` and at most one data step (`rows` or
2829
+ `update`), the editor's types ordering them, per version an (A) commit (DDL, `migrating`) and a (Z) commit (last
2830
+ chunk, later edits, `ready`), recorded as `alter`/`index`/`dropIndex`/`table` writes; fresh tables created at the
2831
+ newest version; shared with contracts only `hash64`, the normal form per column, `Increment<N>` and the editor-return
2832
+ check (supersedes the derived adds and drops of "The migration API: the reviewers' consensus"). (2) The store's
2833
+ admission check through `store.commit(writes, { admit, adopt }, ctx)`. (3) An admitted commit's staged job changes are
2834
+ adopted into the object (supersedes "The seal fence"'s "staged job changes of the sealed attempt abandoned"); a run cut
2835
+ off by its attempt's seal ends `aborted { reason: "restart" }`. (4) A transaction at the seal is discarded; pending
2836
+ async `tx.*` operations tracked as in Durable. (5) Key columns immutable in `set`. (6) Writes serialized before
2837
+ `BEGIN`; canonical `set` vocabulary with an explicit null for clears. (7) `int` columns kept in the safe range by a SQL
2838
+ `CHECK` (an out-of-range result in the step is fatal, accepted), the JS evaluator checking the same. (8) The outbox
2839
+ floor (`read(afterSeq)` valid from the floor to `lastSeq`). (9) The manifest carries `nextDocumentId`. (10) A bounded
2840
+ LRU for `chord/sqlite/node`'s prepared statements (pi change). (11) Slots keep `valueVersion` and `storedVersion` (as
2841
+ Durable's `LoadedDocument`). (12) An admitted commit of a failing object adopts and publishes once (Sol, Fable). (13)
2842
+ Retiring a `latest` record deletes its row. (14) One host per store. The rest: section 15 of the spec. Carry-over to
2843
+ slice 7 recorded in slices.md.
2844
+ - **Slice 6a small items (owner, 2026-10-27: "all yes"; from the four spec reviews):** (1) `store.close()` (no
2845
+ context, as Durable's `Storage.close()`) waits for a running step, rejects queued commits, running exports and reads
2846
+ `released`, closes the backend; idempotent, also after `storage_failed`. (2) The store tells its host of
2847
+ `storage_failed` through an internal listener, once; the host reports once and closes itself. (3) An omitted outbox
2848
+ option keeps the persisted setting; switching needs an explicit `outbox: true` or `false` (supersedes in part "The
2849
+ outbox is optional": "opening with the other setting is that explicit switch"). (4) `store.outbox` is `undefined`
2850
+ without an outbox (typed); `read(afterSeq)` before the first stored seq and `trim` beyond `lastSeq` reject; `trim` is
2851
+ idempotent. (5) **No hand-written memory backend:** tests run on SQLite `:memory:` through `chord/sqlite/node`; one
2852
+ semantics (`NULL`, text order, the `set` evaluator, ordering), no conformance suite across two backends; every real
2853
+ target is SQLite (supersedes the memory backend of "The store", S3 and "The host's store and how it is opened").
2854
+ (6) Export takes the queue at its first `next()`, an abort listener releases it while the consumer is suspended (a
2855
+ later `next()` rejects `aborted`); it reads page by page with plain reads, not one long read transaction. (7) Parts in
2856
+ order: manifest; per table its `table`, `put`s, `index`es; the `document.create`s; `end`; parts after `end` refused;
2857
+ `importStore(backend, parts, ctx, { outbox? })`, the outbox starting after `lastSeq`. (8) A live object failing is
2858
+ reported as `object_failed { object, cause }`. (9) `keys(ctx)` is in code point order, in memory too (`compareText`).
2859
+ (10) A migrated base is written back by the next commit that changes the document (supersedes design §9's "a
2860
+ read-only transaction right after a version bump writes the migrated base"); until then a load re-migrates (once per
2861
+ object). (11) Document ids are assigned in the step, monotonic with seq, given to `adopt`. (12) A run waiting for a
2862
+ live attempt keeps its object alive only while an attempt is live or activating. (13) Chord's own loads take no read
2863
+ transaction (the object holds its line, so plain reads are consistent); S4's read transaction stays for reads outside
2864
+ a line (Durable's, 6b); supersedes "Slice 6 first decisions" (2)'s "in one read transaction". (14) `put` is an upsert
2865
+ (the whole row replaced); `update`/`delete` of an absent key change nothing and are recorded with their count (0).
2866
+ (15) At write `int` is a safe integer, `num` finite, `bool` stored 0/1; one compiled closed check per table. (16) The
2867
+ column helpers are L1's (`str int bool num opt`), a restricted subset. (17) Default order by key ascending; the key is
2868
+ appended to every `orderBy` as the tie-break; one direction per query in 6a. (18) Cursors are opaque strings bound to
2869
+ table, order columns and direction; decoding them is a trust boundary (`invalid/args`). (19) Generated SQL has a
2870
+ bounded number of texts (the facade caches every prepared statement): `limit` and cursor values bound, `inArray` as
2871
+ one JSON parameter through `json_each`. (20) Cut to 6b: the engine's reads at an older seq (`find`/`load` with `at`)
2872
+ and exporting `history: "full"` (the format defines the fields now).
2873
+ - **Cold keyed loads on the line; Chord aligns with Durable (owner, 2026-10-27; slice 6a spec review blocker 9):**
2874
+ a cold read of a keyed document (`get`, `watch`, an `at` subscription) is a job on the object's line, as Durable's
2875
+ `snapshot` loads on the Session line: it waits for the line, loads, installs and releases it; cached reads take no
2876
+ line; `tx.draft` loads inside its transaction, which holds the line; one load in flight per key, shared, each
2877
+ caller's `ctx` ending only its own wait; a load installs its tracker only while the key is still absent (`adopt`
2878
+ always wins). Why: a load outside the line could install a value from before a transaction that committed meanwhile,
2879
+ and the next delta would be computed on it. **General rule (owner): Chord and Durable align as much as possible**
2880
+ (the same rules and mechanisms where they solve the same problem: commits, lines, loads, errors, watches).
2881
+ - **The migration API: the reviewers' consensus (owner, 2026-10-27; slice 6a spec review blocker 8):** the owner
2882
+ delegates the shape to the spec reviewers ("whatever the reviewers all like and find consistent and easy to implement
2883
+ correctly"); only a disagreement among them comes back. Proposal in the spec (Opus's shape, Sol's protocol): adds and
2884
+ drops derived from the declared versus the recorded columns (a new column `opt(...)` or with a `default`; a dropped
2885
+ column loses its indexes first); `migrations: { N: { rename?, rows? | update? } }` with renames explicit; a version
2886
+ bump without an entry is a schema-only change; columns or key differing at the same version fail `broken`; key and
2887
+ column-type changes (`rebuild`) cut from 6a; `rows` synchronous and pure, chunks of 100, writing changed columns plus
2888
+ `$v` through an internal write form (`tx.update` never writes `$v`); `update` one `updateWhere` stamping `$v`; one
2889
+ version at a time (a DDL commit recording `version N, migrating`, the data step, `ready`), indexes diffed after all
2890
+ versions; resume continues `migrating` skipping rows with `$v >= N`; a `rows` function gets the stored row with
2891
+ `json(...)` columns migrated on access.
2892
+ - **Nullable columns and the `set` form (owner, 2026-10-27; slice 6a spec review blocker 6 and the DSL):** columns
2893
+ are non-null by default; a nullable column is L1's `opt(...)` (`note: opt(str())`): in a row an optional field,
2894
+ absent when `NULL`, never an explicit `null`. A `put` carries every non-`opt` column (a missing or unknown field
2895
+ fails `invalid/args`). Queries follow SQL's rules for `NULL` (a comparison with `NULL` never matches, `ne`/`not`
2896
+ included; ask with `isNull` or `or(isNull(...), ne(...))`), on every backend. Keys, index columns and `orderBy`
2897
+ columns must be non-null (types and `defineTable`), so cursors are total. `alter` adding a column: `opt(...)` or a
2898
+ declared `default`. The `set` of `update`/`updateWhere` is a plain object of values (`{ note: "fix" }`) or a callback
2899
+ getting typed column references (`(e) => ({ hits: add(e.hits, 1) })`), like `where`; `col()` and `value()` are gone;
2900
+ `undefined` clears an `opt` column. The semantics the reviewers listed (simultaneous evaluation of a multi-column
2901
+ `set`, integer arithmetic and overflow, `replacePrefix` without the prefix, empty and U+10FFFF prefixes, `inArray([])`
2902
+ and long lists bound as one JSON parameter, `when`'s condition, default order and key tie-breaks, one evaluator for
2903
+ `where` and `set`) are specified in the 6a spec; no separate DSL review (owner: the spec review is enough).
2904
+ - **Restricted names instead of assigned table names (owner, 2026-10-27; slice 6a spec review blocker 5):** names
2905
+ that become SQL names are restricted at declaration (and checked on import) instead of being mapped. Host kinds,
2906
+ object types (every contract, not only stores) and table names use npm package-name characters `[a-z0-9@/._~-]`,
2907
+ lowercase only (SQLite compares identifiers case-insensitively), so plugin-prefixed type names (`@pi/file-history.store`)
2908
+ stay valid; the separator is `:`, which they cannot contain, so the rendering `"session:@pi/file-history.store:edits"`
2909
+ is injective (`a/b` + `c` and `a` + `b/c` differ) and readable in `sqlite3`. Index names `[a-z0-9_]+`, rendered
2910
+ `<table>:<index>`. Column names `[A-Za-z_][A-Za-z0-9_]*` (camelCase allowed), two columns equal ignoring ASCII case
2911
+ refused at `defineTable`, no `$` (the hidden `$v` and `<column>$v` cannot collide). Import is a trust boundary: every
2912
+ name in an archive is checked against these rules (no `"` can occur: quoting is always safe), every row against its
2913
+ table's recorded columns, the parts against one compiled `ArchivePart` schema; a failure refuses the whole import.
2914
+ Rejected: physical names assigned by the store (`t1`, `t2` with a mapping table: unreadable, owner), escaping logical
2915
+ names into physical ones.
2916
+ - **`runCounted` in `chord/sqlite` (owner, 2026-10-27; slice 6a spec review blocker 4):** `SqliteExecutor` gains
2917
+ `runCounted(sql, ...params): Promise<number>`, running one statement like `run` and resolving with the rows it
2918
+ inserted, updated or deleted (SQLite's `changes()`: index writes and trigger rows do not count); `run` stays
2919
+ `Promise<void>`. The store uses `runCounted` only where a count matters (bulk writes' `count`, the engine's
2920
+ conditional `UPDATE`, single `update`/`delete`). Node takes it from `StatementSync.run()` (free); the Durable Object
2921
+ driver reads `SELECT changes()` after the statement, because the cursor's `rowsWritten` is Cloudflare's billing
2922
+ count and includes index writes (verified in workerd 2026-10-12: an insert with two indexes 3, a 10-row bulk update
2923
+ 20, `changes()` 1 and 10). Measured in workerd: `SELECT changes()` costs about 1.5 µs per statement and reads or
2924
+ writes no billed rows, so making every `run` count would nearly double a small write on Durable Objects for callers
2925
+ that need no count (Durable's `SqliteStorage`): hence a separate method (option 2, owner). Name `runCounted`
2926
+ (owner). pi `8025c414d` (upstream first), with a real Durable Object test in Durable's workerd suite.
2927
+ - **Store errors: Durable's rule (owner, 2026-10-26; slice 6a spec review blocker 3; **superseded in part** by "Slice 6b
2928
+ D4: the store has Durable's Storage semantics": a step failing before `COMMIT` rejects only its commit):** any backend error in a commit
2929
+ step is `storage_failed` (the store stops, every later commit and read rejects `unavailable/storage_failed`, a new
2930
+ reason in the protocol list; the host closes; `onError` once), as Durable's `GuardedStorage`: the SQLite facade cannot
2931
+ tell a failed statement from a failed `COMMIT`, and what reaches the step is a real storage problem or memory and disk
2932
+ disagreeing. Plugin mistakes never reach the step: they are refused when the write is staged (`invalid/args`). A
2933
+ commit refused by the seal fence is rejected `released` before `BEGIN` and is no error of the backend. A backend
2934
+ error on a read is `storage_failed` unless the read's `ctx` was aborted (`aborted`); invalid arguments are checked
2935
+ before the backend (`invalid/args`). An `adopt` that throws is `storage_failed` (owners never throw from it).
2936
+ Accepted: integer overflow past 2^63 in a bulk `add` is fatal (a `STRICT` integer column refuses SQLite's REAL).
2937
+ Supersedes the 6a spec's "an error before `COMMIT` returned rejects that commit".
2938
+ - **A live object can fail (owner, 2026-10-26; slice 6a spec review blocker 7):** a runtime failure of the owner (a
2939
+ `migrate` that fails on access, a stored version newer than the code) seals its live attempt (the seal fence) and
2940
+ makes the object `failed` with a `broken` cause: calls on it reject at once with the cause, its state members keep
2941
+ their last values and get no frames, its natives close, its documents stay on the object (the failed data intact in
2942
+ the store), `onError` gets one report (the kind is settled in the 6a spec; `object_failed { object, cause }`
2943
+ proposed); recovery only by a reload or `ref.retry()` (a new attempt; only the failed load is redone). Nothing
2944
+ cascades: dependents that are live stay live (§6), only their calls into it fail.
2945
+ - **A native restart drains first (owner, 2026-10-26; general-model discussion):** when a reload restarts an object
2946
+ with natives, new calls park, the running calls are awaited (the drain, bounded by `closeTimeout`), then the natives
2947
+ close (reason `reload`), the new factory runs and the natives reopen; only a call that outlives the drain is cut off
2948
+ by the seal fence. Supersedes §21's "running work ends `aborted`" for calls (running job runs: slice 7 decides with
2949
+ the job ownership change). The general-model discussion is closed with this entry (owner: "model discussion done").
2950
+ - **Keyed state members, `local` contracts only (owner, 2026-10-26; reviews
2951
+ `/tmp/keyed-members-review-{astra,fable,opus,sol}.md`, all four for A):** the gap: across objects live state travels
2952
+ only as state members, one value per object, so a view with params (`NoteView { session, id }`) could not follow one
2953
+ keyed document of a persisted store (P3 keeps persisted data in `local` objects with empty params). A contract member
2954
+ may be `keyed(schema)`, a third member kind (part of the fingerprint), only in `local` contracts (`remote()` refuses
2955
+ it): a provider returns a keyed document handle for it (in memory or persisted) or re-exposes another reference's
2956
+ keyed member; a consumer on the same host takes `ref.member.at(key)` in its factory (only there: a counted pin on the
2957
+ provider's per-key source while the consumer lives, loaded before the consumer goes live and never evicted while
2958
+ pinned) and `ref.member.watch(key, ctx)` / `get(key, ctx)` anywhere (`watch` on an absent key gives `undefined`). An
2959
+ absent key reads `initial()`; a retirement arrives as the `retired` flag; a key whose `migrate` fails breaks the store
2960
+ (6a's keyed rule). Nothing on the wire changes: tabs and other hosts hold the view, an ordinary remote object with
2961
+ singleton members. A derive over a retired source carries the `retired` flag on (a reader's redacted member learns of
2962
+ the deletion). Versions of keyed members are mapped per key in slice 7 (`"refuse"` only for removed ones). Rejected:
2963
+ B (persisted `local` objects with params: one object per entity, nothing lists or retires across them). Kept for a
2964
+ real case: keyed members across links (Fable's design: keyed entries inside a hold's `bound`/`state`, a `keys { hold,
2965
+ add, drop }` message, re-holds carrying their keys; or Sol's selector holds), as a later slice. Option C (a singleton
2966
+ map plus a lookup derive, p2's `DraftStore`, `Status`) stays for small collections (§12 cookbook). Built in slice 6a
2967
+ (about 300-350 lines), which already builds the per-key source behind `handle.at(key)`.
2968
+ - **The seal fence (owner, 2026-10-26; the fence review `/tmp/fence-review-{astra,fable,opus,sol}.md`):** leftovers of
2969
+ an ended or replaced attempt (handlers past the drain, queued commits, routine calls, watch deliveries) are cut off
2970
+ by one token per attempt, and one per job run invocation for its state changes and outcome. (1) Sealed handles:
2971
+ after the seal, new work through the attempt's handles is refused at use with `released` (`self.tx`, `ctx.get`,
2972
+ which checks the abort signal first, a job start, a routine start); the attempt's references stay usable for its
2973
+ own natives' closes, then are released. (2) Commits are checked at two points: when they enter the store's commit
2974
+ step (sealed: rejected `released`, nothing written; not a storage failure) and at adopt (admitted while current,
2975
+ sealed during the SQL: durable, adopted into the object's documents, staged job changes of the sealed attempt
2976
+ abandoned); in-memory commits are checked inside the batch. (3) At the seal, a transaction of the attempt that has
2977
+ not entered the step yet is aborted and the object's line released; a step already started finishes before the
2978
+ attempt's teardown completes. (4) Cleanup still runs after the seal (releasing what stuck calls hold, `ctx.get`
2979
+ references, presence entries, `host.activity` counts), so a stuck handler cannot keep a worker busy forever. The
2980
+ existing start guards stay (an old activation finishing after the object ended, `Job.#invoke`, a `ctx.get` resolving
2981
+ after its call settled). Unit: the attempt (every way an attempt ends already goes through `Attempt.seal`); a pure
2982
+ swap is not a replacement (the same attempt continues), so a stuck old routine call past the handover is fenced per
2983
+ routine invocation (slice 7). Natives' effects outside Chord are the plugin's; their `close` is honoured, and their
2984
+ writes to Chord state go through transactions and are fenced. Cost: one boolean read per commit at one (in memory) or
2985
+ two (persisted) points. Replaces slice 6a's per-path rules ("`adopt` of a dropped handle is a no-op", race 4, staged
2986
+ job runs adopted after `Job.end()`); `Documents.seal`/`assertWritable` become the token's check.
2987
+ - **Job runs belong to the object; singletons stay resident (owner, 2026-10-26; the four fence reviewers'
2988
+ follow-ups):** (1) A job's records, keys, active count, `running`, run state documents and pending actions belong to
2989
+ the object (per job name), with its documents; the attempt keeps the job's implementation (`run`, `finish`) and the
2990
+ running invocations (its drain, its seal). A run's state changes and its outcome are fenced by the run's own
2991
+ invocation, not the attempt; `finish` runs on the object's current live attempt (a finishing run waits for one); a
2992
+ run whose job the new code no longer declares stays `blocked`. Why: with runs per attempt a native restart breaks key
2993
+ dedupe (a run can start twice), `concurrency` (undercounts), `running` (loses its runs), the state documents (a dead
2994
+ attempt's cleanup would clear them) and §15's rule that the current code finishes a run aborted by a restart. Open
2995
+ for the spec: whether a run waiting for a live attempt keeps a `broken` object alive (proposal: it counts only while
2996
+ an attempt is live or activating). (2) Persisted singletons and `at(key)` documents stay in memory while their object
2997
+ lives; only keyed persisted documents are evicted. Why: `handle.value` is a synchronous read (methods, derives,
2998
+ snapshots for new holds), so the value must be in memory when read; an evicted singleton would need an async read,
2999
+ as keyed documents have. Memory is bounded by the number of declared singletons, one value each; data that grows
3000
+ belongs in keyed documents or tables.
3001
+ - **Documents and the line belong to the object, not the attempt (owner, 2026-10-26; general-model discussion after
3002
+ the slice 6a spec reviews and the fence review `/tmp/fence-review-{astra,fable,opus,sol}.md`):** an object (one
3003
+ identity) owns its documents (in memory and persisted, their trackers) and its line for its whole life, across
3004
+ retries, failures and native restarts; an attempt (one try at activating the object: factory, references, natives,
3005
+ members, derives, routines) keeps only what belongs to one try: its natives, routines, derives and its seal token.
3006
+ A new attempt's factory gets handles to the same documents (`self.state(def)` by name). Persisted documents load
3007
+ once per object, and again only after a load failed (a `migrate` that needs fixed code). In-memory state survives a
3008
+ failure and retry too; ending the object clears it. Why: per-attempt documents let a late commit of a dead attempt
3009
+ leave the next one on a stale base (the review's race), contradict §9 ("in-memory documents survive native restarts
3010
+ of a live object"), and were an accident of slice 2 (each activation a fresh instance, documents created inside it).
3011
+ Built with slice 6a (carry-over), which needs it; replaces slice 7's carry-over "run records and state documents
3012
+ belong to the object across attempts (documents belong to an attempt today)" for documents. Decided the same
3013
+ day: the seal token's rule ("The seal fence") and job runs ("Job runs belong to the object").
3014
+ - **Writes carry their versions; archives are lossless (owner, 2026-10-26; spec review blocker 1, all four reviewers):**
3015
+ every `put` write carries the row's `$v` and each `json(...)` column's version explicitly, in the outbox and in
3016
+ archives alike (`{ kind: "put", table, row, v: 3, columns: { meta: 1 } }`); the applier never infers a version from
3017
+ the table definition (an archive's `table` write has the current version while cells may be stored at older ones; an
3018
+ inferred stamp would label an old shape current, and an export runs no plugin code, so it cannot migrate). A `table`
3019
+ write carries the table's `state` (`migrating` | `ready`), so an export between migration chunks imports as
3020
+ `migrating` and resumes. A `document.create` carries the record's stored version and fingerprint; for
3021
+ `history: "latest"` its base is the current value (base plus deltas, as a load gives it); for `history: "full"`
3022
+ (Durable, 6b) the archive carries every revision with its seq, retired `full` records included (format 1 defines the
3023
+ fields now, 6b builds them); retired `latest` records stay out. Supersedes in part "Export archives are operations"
3024
+ ("a `document.create` per live document record (with its base)", "Retired documents … are not in it").
3025
+ - **Slice 6a's three code questions (owner, 2026-10-26):** (1) keyed reads take a context: `handle.get(key, ctx)` and
3026
+ `handle.keys(ctx)` for every keyed handle, in memory too (one signature; persisted, they read the store; "Contexts
3027
+ are required"). (2) The activation attempt's `readyTimeout` deadline does not run while an eager migration step runs
3028
+ (a `rows` step over 10M rows takes longer than 60 s; the scan resumes after a crash anyway); getters still time out
3029
+ by their own `readyTimeout`. (3) A host without an address uses `client` as its host kind in store addresses (owner
3030
+ strings, table names), so a browser tab with sqlite-wasm can have a store later.
3031
+ - **The commit-step benchmark (owner, 2026-10-26; S3's condition "SQLite must not get slower"):** in 6a
3032
+ `test/bench/store.ts` measures the store without outbox against Durable's `SqliteStorage` per commit on (a) a
3033
+ commit of one document delta, (b) a commit with one table `put`, (c) 1,000 commits back to back, in bursts (median
3034
+ and p95 over several runs) with memory after GC; each must be no slower. The outbox's extra cost per commit is
3035
+ reported separately. A failed gate goes to the owner with numbers and options before the build continues. 6b repeats
3036
+ the gate with `StoreStorage` on Durable's own storage benchmark.
3037
+ - **The outbox is optional, internal and versioned (owner, 2026-10-26):** supersedes in part "The store" and S1/S5 where
3038
+ every commit writes the outbox. (1) A store has an outbox only when opened with one, `openStore(backend, ctx,
3039
+ { outbox: true })`; the setting is persisted in the store's metadata. Without it the commit step writes data and
3040
+ `lastSeq` only (tests, web tabs, a server's instance store, a session before sync). Turning it on records "outbox from
3041
+ seq N+1" and the sync layer starts from an archive at N; turning it off drops its commits (a later start needs a
3042
+ snapshot again). Opening with the other setting is an explicit switch with these rules, never a silent gap. (2) The
3043
+ outbox is a table in the store's own backend, written in the commit step's backend transaction: SQLite commits data
3044
+ and outbox atomically even with `synchronous=NORMAL`, for one `INSERT`. An external outbox would work only as Durable's
3045
+ JSONL storage commits across files (outbox entry first, SQLite `COMMIT` as the commit point, recovery cutting entries
3046
+ above `lastSeq`), only for a local, truncatable outbox (never Radius: an uploaded entry cannot be taken back), and only
3047
+ with an `fsync` of the outbox before every `COMMIT` (otherwise a power loss keeps the commit and loses its entry, which
3048
+ no pull-back recovers); can be retrofitted later if needed (owner). (3) No pluggable outbox encoding: the store keeps
3049
+ commits as it likes and exposes them typed, `store.outbox.read(afterSeq, limit, ctx)` -> `{ seq, writes }[]` and
3050
+ `store.outbox.trim(throughSeq, ctx)`; the sync layer encodes for the wire. (4) Versioned on two levels: the outbox
3051
+ table's layout is part of the store's schema (the store's own migrations at `openStore`); the write vocabulary has a
3052
+ `format` number, the archive manifest's `format`, stored per outbox commit (`outbox(seq, format, writes)`);
3053
+ `outbox.read` hands out the current format (older commits through the store's pure step functions), a newer format
3054
+ than the code is refused with the data intact, and batches and archives carry it. (5) The benchmark gate (S3) is
3055
+ measured without the outbox; its cost per commit is reported separately. Replaces the sync-layer carry-over "outbox
3056
+ growth without sync".
3057
+ - **Writes, commits, batches, archives (owner, 2026-10-26):** the names of what the outbox, sync and export carry. A
3058
+ **write** (`StoreWrite`, tagged by `kind`, Chord's rule for tagged unions) is one change of the store: `put`,
3059
+ `update`, `delete`, `updateWhere`, `deleteWhere` (with `count`), `table`, `alter`, `rebuild`, `index`, `dropIndex`,
3060
+ `document.create`, `document.change`, `document.retire` (as Durable's `StorageWrite`, "one record or document
3061
+ mutation in an atomic storage commit"; S17's "five writes"). A **commit** is the writes of one seq as the outbox keeps
3062
+ them (`{ seq, writes }`); a **batch** the commits `from..to` that sync uploads and a replica applies; an **archive** a
3063
+ manifest, the writes from 0 to `lastSeq` and an `end` (its parts are `ArchivePart`). Not "record" (document records,
3064
+ run records, Durable's records), "op" (Delta ops inside document changes), "entry" (Durable's conversation entries)
3065
+ or "change" (`WatchChange`, Delta's `Change`). Earlier entries' "outbox record" reads "the outbox's commit" and
3066
+ "operation" reads "write"; the book's `op:` tags read `kind:`. `StoreStorage` (6b) turns Durable's `StorageWrite`s
3067
+ into `StoreWrite`s.
3068
+ - **The `json(...)` column helper (owner, 2026-10-26; slice 6a point 8):** `json(schema, { version, migrate? })`.
3069
+ The schema is required (reads are typed from it; a migration's result is checked against it once, R8; written values
3070
+ are not walked, R9); `version` is required, as for `defineState`; `migrate(old, from)` is optional and pure: without
3071
+ it, or when it throws or returns an invalid value, reading an older value is refused, the data stays intact and the
3072
+ owning object fails `broken` (R8, R15). New: each `json(...)` column's version and schema fingerprint are recorded in
3073
+ the table definition (the `table` write); at activation, the same version with another fingerprint fails the object
3074
+ `broken` (D9; one comparison per column per activation, none per row). A read does not write back: an old row pays
3075
+ the migration and the check on every read until its column is written; an eager `rows` step rewrites a table at once.
3076
+ - **Facets only declare (owner, 2026-10-26: "remove facet routines, everything done through objects seems sensible"):**
3077
+ facet routines (`self.on`, `self.onLive` in `FacetSelf`, added in slice 5 because §16 listed routines) are removed
3078
+ with the documents. A facet's setup declares only: `host.serve`, `self.contribute` (slice 8), `host.references`;
3079
+ no natives, documents, routines or jobs, so a facet has no runtime behaviour and cannot fail after `load`. Without
3080
+ documents a facet routine owned nothing it could write and acted only through other objects' methods; standing
3081
+ behaviour belongs to the object whose state it reconciles. p2 already works so: all 63 facets are
3082
+ `defineFacet((host) => …)` with no `self`; its routines and loops live in host-lifetime objects. Built in slice 6a
3083
+ (carry-over): the routines in `FacetSelf`, their start in `host.ts#setUp`, their drain at close, tests, the facet
3084
+ part of example 18.
3085
+ - **Relays do not check answers (owner, 2026-10-24; second 4b review):** supersedes the third fix-round choice. A
3086
+ relay forwards answers from upstream (`bound`, `state`, results) unchecked and unparsed; the end that parses a part
3087
+ checks it against its own link's limits. Reason: the relay's check had to release the hold upstream and hold it
3088
+ again with the same id on `retry`, and an old `state` or `gone` still in flight then looked like the new hold's
3089
+ (reproduced by three reviewers). The relay still checks everything arriving from downstream. A reference failed
3090
+ `too_large/part` leaves its path (a reconnect does not hold it again) until `ref.retry()`; this lives only in the
3091
+ consumer's memory.
3092
+ - **Slice 4b fix round (owner, 2026-10-24):** the builder's five choices accepted and folded into design.md: call
3093
+ arguments sent up a parent link are never compressed (a host cannot tell whether its parent sees it as a child or a
3094
+ route); `revoke` goes on route links before revoked child links close (worker objects end `forbidden/revoked`, not
3095
+ `released`); ~~a relayed answer over the route link's wire limit fails its call, or releases its hold upstream and
3096
+ sends `loading` and a broken `failed` downstream (`retry` re-holds with the same id)~~ (**superseded** by "Relays do not check answers"); compressed hold params are a
3097
+ protocol error on every link; the asynchronous inflate runs one part at a time (its queue is bounded by the parent's
3098
+ or route peer's window; a misbehaving one of the app's own hosts is accepted). No extra performance regression tests
3099
+ (cost guards, GC tests and manual benchmarks suffice). A second, non-pedantic review (correctness and performance)
3100
+ before the commit.
3101
+ - **Slice 4b review decisions (owner, 2026-10-24; reviews `/tmp/s4b-review-{astra,fable,opus,sol}.md`):** (1) a
3102
+ dependency failing with `invalid/cycle` makes its dependent broken (exception to R18). (2) Cycles that enter an
3103
+ already activating object (simultaneous starts) are a documented limitation: they time out transient and retry; no
3104
+ probe message. (3) Child links refuse compressed parts (protocol error): clients never send them, and a small part
3105
+ could inflate to 64 MiB on the server; asynchronous inflate then happens only on a host receiving from its parent.
3106
+ (4) The text codec tags every part, so no JSON value is reserved for compression. (5) A departure discovered while a
3107
+ batch is sending lands in the next batch (one commit stays one message per link). (6) Presence `origin` is one rule:
3108
+ the caller's incarnation, also for `ctx.get`. (7) Revocation's error is `forbidden` with a new reason `revoked` (a UI
3109
+ can say "access revoked"; the protocol table changes, version stays 1 until p3 ships). (8) Compression details: a
3110
+ tagged adapter result (too large or corrupt), compression also on IPC, `./next/node` export later, hold params never
3111
+ compressed. (9) `host.references` keeps a type's list from its first ask (entries come and go with references).
3112
+ (10) Instance ids on restart are slice 7's (carry-over). Revocation propagates over route links; the decoded limit is
3113
+ stamped on holds only (design §4, §22 aligned).
3114
+ - **Slice 4b to building (owner, 2026-10-24):** `slice-4b.md` revised for flow control per origin goes to a builder
3115
+ directly; fresh reviewers (Astra, Fable, Opus, Sol) review the build; the owner then decides the open points the
3116
+ spec marks as proposals (a cycle error makes the dependent broken; compression as built; `host.references` lazily
3117
+ per type). Compression is built as option (b) (owner leaning, decided after the build): hosts with a synchronous
3118
+ compressor compress parts above 16 KiB at their origin; browsers send uncompressed and inflate asynchronously with
3119
+ `DecompressionStream`, holding later messages of the link in order (bounded by the window and N; acks after apply).
3120
+ - **Slice 4a approved (owner, 2026-10-24):** `packages/chord/docs/slice-4a.md` as revised after the reviews. Build
3121
+ order: the pending queue in Chord's shared code first (pi, upstream), then the slice by a fresh Opus builder (`s4`).
3122
+ - **Slice 4a review decisions (owner, 2026-10-23; Astra's and Fable's spec reviews, `/tmp/s4a-review-*.md`):**
3123
+ supersedes in part "Slice 4 split and first decisions" (resets as `r` ops). A `hold` carries the address the caller
3124
+ computed; relays route by address and never need the contract or parse params. A `hold` carries `principal?` and
3125
+ `incarnation?`, honoured only on the parent link (calls inherit their hold's, so calls carry none; refines R1/Q13
3126
+ "hold and call"); a reference made on a host sends its maker's principal over route links (`ctx.get` in a guest's
3127
+ handler acts as the guest); upward, a server ignores a child's fields (a worker acts as itself; documented
3128
+ boundary). Resets deliver full bindings: `bound` carries a list of holds (`bound { holds: [...] }`, one batch), a
3129
+ `bound` on a bound hold replaces its replicas (another variant allowed; moved from slice 7 to 4a); overflow stays an
3130
+ `r` op. A provider answers `reset` through its pending path (never exceeding K). A relay recovers per (downstream
3131
+ link, upstream link): once something from U cannot reach D, everything from U to D is dropped, and the next free slot
3132
+ requests one reset of every hold D has on U; an answer that finds no slot is requested again. Heartbeat: one
3133
+ `lastReceived` timestamp per link and one host timer; the 30 s silence covers the handshake. Transports: a connecting
3134
+ WebSocket buffers; IPC `send() === false` is backpressure. A hold from the parent is never routed back to it.
3135
+ - **Slice 4a spec decisions (owner, 2026-10-23; `packages/chord/docs/slice-4a.md`):** a heartbeat on every link
3136
+ kind, in the protocol layer (`ping`/`pong`; a side that received nothing for 15 s pings, a link silent for 30 s
3137
+ closes; host options with these defaults); supersedes design §3's "heartbeats are the adapter's policy". The text
3138
+ codec may carry relayed links (one parse per part per hop, accepted for a debugging codec). `resolve(address,
3139
+ context)` takes a Context. Upstream reconnects at a relay are `loading` downstream, then the new `bound` (3b's
3140
+ re-hold path, another variant allowed); flow-control resets stay `r` ops.
3141
+ - **Compression at the origin (owner, 2026-10-19):** a JSON part above a threshold (e.g. 16 KiB) is compressed once
3142
+ at its origin, whatever the link; byte segments stay raw; relays forward it compressed; IPC carries compressed parts
3143
+ too. No per-link compression.
3144
+ - **R6 and contract compatibility (owner, 2026-10-18), strict, like documents:** a hold carries only the consumer's
3145
+ contract version and fingerprint, never a description. The provider binds when they equal its current version and
3146
+ fingerprint, or an accepted old version's fingerprint, computed from the provider's own old schemas. Every contract
3147
+ change, additive ones included, bumps `version`. (**Superseded in part** by Q10: forward evolution in code and
3148
+ `checkAccepts`; old versions come from the materialized history, not from copies in `accepts`.) `accepts[v]` carries the provider's own copy of version v's member
3149
+ schemas plus value mappings: old arguments are validated against the provider's old schema, then mapped (state:
3150
+ new to old; methods: old arguments to new, optional result mapping back); an additive change needs the old schemas
3151
+ and no mappings. Anything else is `contract_mismatch` (or `updating` during a rollout). Consequences: no
3152
+ descriptions on the wire (the `describe` message kind of R1 and the per-link description cache go away), R3 is moot,
3153
+ the coverage check across fingerprints of one version is gone.
3154
+
3155
+ ## Simplification audit (2026-10-30)
3156
+
3157
+ The owner asked, after slice 7b's seven fix rounds, how many other mechanisms serve a rare case at a high cost. Audits
3158
+ by Opus and Sol (`/tmp/audit-{opus,sol}.md`) and this session; each item decided by the owner with its code and case.
3159
+
3160
+ - **A1, `installing` is cut (owner, 2026-10-30: "cut"):** model §1's state for a hold or request that does not fit the
3161
+ running code while a reload stages: staging is synchronous (`stagePlugins`, `recordFactory`), so no message or caller
3162
+ can arrive while it runs (the only test reached it from a factory calling back into the host). The `Serving` kind, the
3163
+ host's map, `Hold.start`'s branch and the test go. During the app's bundling before `host.reload` the old code
3164
+ answers a mismatch `failed { waiting }` until the commit wakes the request (model §5's accepted gap). Supersedes
3165
+ model §1's `installing` row and design §21's installing sentences.
3166
+
3167
+ - **A2, static registry cycle edges are cut (owner, 2026-10-30: "yeah cut"):** the edges a contribution of another
3168
+ registry's value recorded (`RegistryEdge`, `#reads` counts, `assertNoCycle`, `#reaches`, an attempt's pending edges,
3169
+ staging's edges) go; the runtime guard stays (a registry read while it composes throws `invalid/cycle`; the
3170
+ contribution that read it is left out and reported `contribution_failed`). A ring of registries is caught at its first
3171
+ composition instead of at `self.contribute`: the contributing object goes live, the rest of the registry works.
3172
+ Supersedes slice 8's "a cycle of registries reading each other refused when the second one serves or contributes".
3173
+
3174
+ - **A3, routine calls keep their ops (owner, 2026-10-30: "keep"):** `self.on` handlers keep `{ value, ops, retired }`
3175
+ with the routine's own folding (`PendingOps`, the basis after a throw).
3176
+
3177
+ - **A4, cross-host activation chains stay (owner, 2026-10-30: "yep keep"):** they catch the common, sequential form of a
3178
+ cross-host dependency ring (one request whose activation creates the other side) with an immediate `invalid/cycle`
3179
+ naming the path; `parked_long` stays the slow net for the rare simultaneous race.
3180
+
3181
+ - **A5, retry propagation by a hop budget (owner, 2026-10-30: "b if correct"):** `ref.retry()` still passes on from a
3182
+ parked object to its failed dependencies (model §3.7), but its dedupe is simpler: on one host one retry's walk keeps a
3183
+ visited set (a diamond retries a shared dependency once); across links the `retry` message carries a hop budget (an
3184
+ integer, 16 from the first host, one less per host, dropped at 0) instead of the operation id (uuidv7), the objects'
3185
+ last-8 remembered ids and the `through` list. Bounded effects, both harmless: a diamond across hosts may send a shared
3186
+ dependency two retries (the second meets it activating and is ignored, or starts one more attempt if the first
3187
+ already failed); a waiting ring across hosts (the undetected simultaneous cycle) passes the retry round until the budget
3188
+ runs out, starting no attempt (parked objects only pass it on). Instance ids stay (activation chains, A4).
3189
+
3190
+ - **A6, staging factories against a recording `self` stay (owner, 2026-10-30: "keep"):** a throwing new factory keeps
3191
+ rejecting the whole reload (the host stays on its old code), and each object's fate is decided from its recorded
3192
+ declarations before the commit.
3193
+
3194
+ - **A7, registry last-good with provenance stays (owner, 2026-10-30: "a").**
3195
+
3196
+ - **A8, deferred job-state persistence keeps its held ops (owner, 2026-10-30: "keep").**
3197
+
3198
+ - **A9, a pure swap sends holds a fresh `bound` (owner, 2026-10-30: "simplify"):** a hold over a link whose object
3199
+ swaps in place gets a fresh `bound` (one snapshot of its members), the path a restart, a variant change and a version
3200
+ change already take, instead of a re-subscription with `[["r", value]]` per changed member (`Hold.#rebound`, its
3201
+ constant-to-source and failed-member cases, `currentOf`). A tab's watchers may get one frame for an unchanged member;
3202
+ nothing is lost or stale. Supersedes design §21/§22's per-member `r` rule for holds after a swap.
3203
+
3204
+ - **A10, one rule after a live failure (owner, 2026-10-30: "simplify"):** every member of a failed live instance keeps
3205
+ being followed (references and holds) until the commits it admitted published (`documents.settled()`), then all stop:
3206
+ the restarts' rule. The "own" against "foreign" split (`#follows === "own"`, `Attempt.owns` and its mapping-derive
3207
+ case, `Hold.#stopForeign`) goes; a re-exposed member of another object may show one more change in that window.
3208
+ Refines 6a's build-review rule "members following another object's sources freeze at once".
3209
+
3210
+ - **A11, `parked_long` widened (owner, 2026-10-30: "widen"):** reported once per parking episode when an attempt has been
3211
+ parked longer than its `readyTimeout`, whatever its blocker (the first in declaration order named, with its state);
3212
+ the remote-loading condition, `heldOnLink` and the clock restart at a link go. Also covers a cross-host ring one of
3213
+ whose members waits on a failed dependency. Supersedes decision 6's narrow condition.
3214
+ - **A12, the routine fence token folds into the seal (owner, 2026-10-30: "fold"):** one seal token per attempt; after a
3215
+ swap the old routine calls get `closeTimeout` to finish, then the whole old attempt is sealed (its method calls had
3216
+ the same bound). `RoutineContext.fence` and the token choice per transaction go. Supersedes slice 7b decision 4's
3217
+ separate token.
3218
+ - **A13, the table version fence stays (owner, 2026-10-30: "keep"):** an old call stuck past a restart's drain never
3219
+ issues SQL against a migrated table (which would stop the store and close the host).
3220
+ - **A14, key pins keep their hand-over between instances (owner, 2026-10-30: "keep").**
3221
+
3222
+ - **A15, keyed members across objects stay, with their versioning (owner, 2026-10-30: "keep essential"):** they fill
3223
+ the gap P3 creates (a view with params following one entry of a persisted store live); the alternatives (a singleton
3224
+ map, a method) are weaker.
3225
+
3226
+ Summary: cut A1 (`installing`), A2 (static registry cycle edges); simplified A5 (retry hop budget), A9 (a fresh `bound`
3227
+ after a swap), A10 (one rule after a live failure), A11 (`parked_long` widened), A12 (one seal token); kept A3, A4, A6,
3228
+ A7, A8, A13, A14, A15.
3229
+
3230
+ ## Portability boundaries in the kernel
3231
+
3232
+ From `research/chord-on-durable-objects.md`, adopted for the Chord v2 kernel because they cost little and help tests:
3233
+
3234
+ - **A host is a logical identity plus an incarnation**, not an OS process.
3235
+ - **The loader is a host adapter:** the kernel also accepts facets imported ahead of time (static composition, as on a
3236
+ Durable Object or in tests); loader D is one adapter.
3237
+ - **Values are JSON plus bytes, defined independently of the transport;** (**superseded in part** by Q4 (IPC carries the same binary message, V8-serialized bytes)) IPC structured clone and WebSocket binary
3238
+ frames are carriers, not the definition.
3239
+ - **Durable plugin state:** whatever must survive a host teardown (a DO eviction, a crash, a restart) is a persisted
3240
+ document written in transactions (`DocumentStorage`). Everything else is rebuilt on activation: in-memory documents,
3241
+ natives, derives (`previous` is a cache), routines, presence and references (re-held by clients); job runs end
3242
+ `lost` unless their records persist.
3243
+
3244
+ ## Object kinds
3245
+
3246
+ - **`local` stays** next to `remote` (owner, after the plugin scouts judged "one kind only"; all four would have
3247
+ accepted it with conditions).
3248
+ - **`local` means:** (**superseded in part** by R7 (validation for every caller; `local`'s exception is Q15, open) and R9 (no dev freeze)) same host, no validation, no copy, no principals; plain data only; **no `opaque`, no callbacks**
3249
+ as arguments. Behaviour goes through registries, multi-step work through jobs, input through flows (or Questions),
3250
+ runtime services through host values (**superseded:** host values were removed; runtime services are core objects,
3251
+ "Hosts and placement" and "Review round 1"). Without a copy caller and object share values: neither may mutate arguments
3252
+ or results (the playground freezes them in dev mode to check).
3253
+ - **Kernel semantics decided while building step 2:**
3254
+ - `local` factories are `(input, self)`; a local method's `ctx` has no principal; references a local object makes
3255
+ run as the host principal; attribution is passed as data.
3256
+ - ~~Calls on `remote` types validate and copy arguments and results also on the same host.~~ **Superseded** by "The
3257
+ state path": same-host calls pass values by reference; arguments are validated only for callers that are not
3258
+ host principals, at the provider.
3259
+ - (**superseded in part** by R9 (no dev freeze)) `Uint8Array` leaves are never copied in process (shared, immutable by convention); the dev freeze of `local`
3260
+ values freezes objects and arrays only. Transports carry bytes raw (IPC structured clone, WebSocket binary
3261
+ segments), never base64.
3262
+ - (**Superseded** by Q12: unknown fields are rejected.) Undeclared argument fields are stripped as in today's Chord: the consumer strips to its schema, the provider to
3263
+ its own, then validates; a handler receives exactly its declared shape (additive rule; no mass assignment).
3264
+ - A failed reference stays alive: `ref.state` is a tagged value (`loading`, `live`, `failed` with `cause` and
3265
+ `retryAt`, `released`); while held, Chord retries per the per-address backoff; `ref.retry()` tries at once; calls
3266
+ on a failed reference reject at once. Calls park on link drops, loading or restarting objects and rollout
3267
+ `updating`.
3268
+ - **Pattern: shared core, per-principal views** (confirmed; renamed "shared store" in "The state path"). Identity stays (type, input, principal key): the
3269
+ factory receives the principal and builds its variant and principal-specific state (e.g. a read-only guest's
3270
+ filtered view). Heavy or shared parts (a Durable watch, a prompt watcher, a sandbox, a store) live once in a
3271
+ `local` core object (no principals: one per (type, input) on the host; no copy); per-principal remote objects
3272
+ `self.get` it and derive their view from it. Same for stores that serve different views per principal.
3273
+ - From the scouts' review of "one kind only", still valid for `remote`: same-host state as immutable snapshot plus
3274
+ deltas (never a full clone per change); bytes transferred, not cloned; real schemas instead of `Type.Unsafe`; double
3275
+ copies through facade layers are fixed by merging owners.
3276
+
3277
+ ## Reload
3278
+
3279
+ - **Per host:** load (bundle; a load error rejects) → staging (run the facet setup and every live object's new factory
3280
+ with a recording `self`, declarations only, nothing activated; a throw rejects) → commit (atomic) → after.
3281
+ - ~~**Rollback is all-or-nothing per host:**~~ (**superseded** by "Per plugin at startup and reload", 2026-10-30) any load or staging error leaves the host entirely on its previous code.
3282
+ A host is completely at revision R or R−1, so status per host suffices.
3283
+ - **Pure objects** (**superseded in part** by R15 (old code touching a document migrated past its version throws)) (no `self.open`, old or new) are swapped in place: new methods and derives; documents, references
3284
+ and jobs stay; routines restart serialized; new references become live; then the object's gate opens. Running calls
3285
+ finish with their old code.
3286
+ - **Native objects of the plugins in the reload restart:** running work in them is aborted (outcome `aborted`),
3287
+ natives close, the factory runs, natives reopen. ~~No per-module digest: if a native is expensive to restart, the
3288
+ author splits it into its own plugin.~~ **Superseded** by R16 (restart patterns: drain in the background, swap
3289
+ through a registry). An open that fails makes that object `failed` with backoff; the rest of the
3290
+ host runs on the new code.
3291
+ - **Remote consumers** of a restarting object see what they see for any object that briefly goes away: `loading`,
3292
+ values readable, calls park, re-hold; calls running on the old instance end with an error.
3293
+ - Rollback covers code errors (load, setup, factories); activation errors (opens, new references, migrations) stay per
3294
+ object; a bug in a method fails that call.
3295
+
3296
+ ## API additions
3297
+
3298
+ - `self.derive(name, sources, (values, previous) => …)`: `previous` is the derive's last value, kept across factory
3299
+ re-runs by name.
3300
+ - `job.start(args, ctx, opts)` returns a run with `outcome(ctx)`: the caller can await the outcome; the caller's abort
3301
+ ends only its wait, never the run.
3302
+ - Jobs stay as designed (intent / run / finish). Jobs as objects with an "until done" lifetime are an option for later.
3303
+ - **Bytes are a schema type** (**superseded in part** by Q4 (IPC carries the same binary message)) (`Uint8Array`), transported natively: structured clone on IPC (`fork` with
3304
+ `serialization: "advanced"`), binary frames on WebSockets. No base64 in JSON. (Today an IPC payload is JSON, deflated
3305
+ and base64-encoded; a 1 MiB chunk of compressed data grows by a third or more.)
3306
+ - **Large transfers are chunked by the owner, not by Chord:** the owning object offers `begin` / `append({ upload,
3307
+ offset, bytes })` / `finish` and `read({ id, offset, length })`, chunks of about 1 MiB. Each side holds one chunk;
3308
+ the awaited `append` is the backpressure; after a link drop the writer asks for the stored offset and resumes; an
3309
+ unfinished upload is removed after a timeout by the owner. A small helper wraps the loop. Chord gets no stream type
3310
+ (it would need its own rules for link drops, cancellation, relays and restarts); reconsider only if many owners
3311
+ repeat the protocol.
3312
+
3313
+ ## Registries and HTTP
3314
+
3315
+ - **Registries work like Durable's** (option c): (**superseded in part** by R21 (only facets and `lifetime: "host"` objects contribute) and "Changing contributions, `self.writable`") each contributor (a facet or an object, by its identity) contributes
3316
+ at most one value per registry; the table is an immutable, ordered list of `(contributor, value)`; Chord has no
3317
+ generic keys and no `conflict` flag. The owner composes the table (in the registry's `compose`, below) and decides
3318
+ conflicts and order (**superseded:** composition runs at runtime and reports conflicts, D1 in "Review round 1"). Chord ships helpers for the common compositions
3319
+ (`keyed(...)` with a conflict rule, `ordered(...)`). Several contributors (model providers, environment kinds) are
3320
+ the owner's composition, not a special case; selection (which contributions apply where) stays possible.
3321
+ - ~~The composition runs in staging and a conflict rolls the host back.~~ **Superseded** by D1 ("Review round 1"):
3322
+ `compose(table, { report })` runs at runtime, always delivers a value and reports conflicts.
3323
+
3324
+ - **Registries may carry natives.** An entry lives exactly as long as the object or facet that registered it; the
3325
+ table changes when it goes away. Consumers look up at each use or follow the table and keep nothing from an entry
3326
+ beyond a use (a pinned Durable phase holding an old table is fine). Natives reach other plugins only this way: no
3327
+ `opaque` members, no natives in method results. What a native's old value does after its owner restarts is the
3328
+ owner's `close` (a model runtime keeps working until collected; a closed SSH connection fails its in-flight `exec`).
3329
+ - **HTTP is an optional Chord component** (**superseded in part** by "Slice 10's points": `html` instead of `page`, request bodies through `json`/`body`, authentication one app hook, plugin sockets instead of a channel component until a second user) (`@earendil-works/chord/http`): a route gets a plain request summary
3330
+ (method, path params, query, principal, selected headers; parsed and capped JSON bodies; uploads handed over in
3331
+ chunks) and returns a response description (`json`, `page`, `bytes` with `size`, `mime` and `read(offset, length)`,
3332
+ `redirect`, `error`). The component does ranges (206/416), HEAD, download headers, streaming, abort on disconnect and
3333
+ the CSP nonce once, instead of every plugin. Request resolvers (who sent a request) are a hook taking the summary.
3334
+ A **channel component** provides WebSocket byte channels (`send`, `onData`, `close`) for terminals.
3335
+ - **HTTP routes and wraps (middleware):** a contribution holds `routes` (pattern to route) and `wraps`. Routes are
3336
+ composed with `keyed` (two contributors with one pattern: a reported conflict, D1). Wraps run in **phases the owner defines** (e.g.
3337
+ `observe`, `headers`, `transform`; outermost first), within a phase in contributor order; no numeric priorities
3338
+ (plugins have no single registration order, unlike Express or Hono; Fastify's lifecycle phases are the model). A wrap
3339
+ applies by default only to its own contributor's routes (`scope: "own"`, Fastify's encapsulation); `scope: "all"`
3340
+ is explicit; `route` narrows it to one route, and a `route` that does not exist is a reported conflict (D1). Wraps
3341
+ may add a security boundary on top of the owner, never instead of the owner's own checks. A request uses the router
3342
+ as it was when the request started.
3343
+
3344
+ ## Durable harness inputs
3345
+
3346
+ - **`HarnessOptions.models` and `settings` accept a function** (`() => Models`, `() => HarnessSettings`), like
3347
+ `env`; a plain object is still accepted. Internally both are stored as functions; generation, hooks and tools get
3348
+ the function's current return value. Replaces p2's `Proxy` around the registry entry. Done in pi main first, then
3349
+ merged into `private`.
3350
+ - Models and environments reach the harness through registry entries (functions) registered by the objects that own
3351
+ their natives; the `Harness` object opens Durable itself (no host value) and looks the entries up at each use.
3352
+
3353
+ ## Input into running work
3354
+
3355
+ - **Questions (`questions.md`) is one possible pattern, not the required one.** Another: the owner of a flow keeps it
3356
+ as state keyed by a uuidv7. Example login: `start({ provider })` returns an id; the flow's live state holds the
3357
+ current step (`{ id, kind, ... }`), notices and status; `answer({ id, step, value })` resolves the waiting step (a
3358
+ late or second answer to a step that is gone gets "no longer active"); `cancel`. The client watches the state, so no
3359
+ `next()` is needed. pi-ai's promise-based `prompt` callback stays inside the owner (the job keeps the resolver);
3360
+ nothing crosses an object boundary. Open: the flow as a keyed document of `Login`, or an object `LoginFlow {id}` that
3361
+ lives until done ("jobs as objects").
3362
+ - **Imperative consumers get a client helper over the same flow state,** (**superseded in part** by design section 20 (flow state is tagged by `kind`, not `status`)) not a second server protocol. Tests,
3363
+ scripts, a CLI or an agent tool loop over the steps; the web UI watches the state; both see the same steps:
3364
+
3365
+ ```ts
3366
+ async function* steps(flow, ctx) { // generic: any flow with { status, step?: { id, ... } }
3367
+ let last = 0;
3368
+ for (;;) {
3369
+ const s = await until(flow.state, (v) => v.status !== "running" || (v.step && v.step.id > last), ctx);
3370
+ if (s.status !== "running") return s.status; // done | failed | cancelled
3371
+ last = s.step.id;
3372
+ yield s.step;
3373
+ }
3374
+ }
3375
+
3376
+ const { id } = await login.start({ provider }, ctx);
3377
+ for await (const step of steps(await ctx.get(LoginFlow, { id }), ctx))
3378
+ await login.answer({ id, step: step.id, value: answerFor(step) }, ctx);
3379
+ ```
3380
+
3381
+ Why not a long-held server `step()` call: a link drop fails it and the consumer cannot tell whether it missed a
3382
+ step; the helper reads state that re-holds by itself, and `last` keeps steps from being lost or repeated. The
3383
+ same shape fits login, tool approval and a question from the agent.
3384
+
3385
+ ## Ownership principle
3386
+
3387
+ - **Every thing has exactly one owning object: the smallest object it belongs to.** The owner holds its data
3388
+ (documents), enforces its rules (in its methods) and offers the operations others need. Nobody reaches past the
3389
+ owner into its data, writes its files or documents, or attaches rules to it from outside.
3390
+ - Ownership nests: the session owns the set of its conversations and its location (creation, relocation); each
3391
+ conversation owns its content and rules (read-only, follows the session cwd) and enforces them in its own methods.
3392
+ Rules set at creation are written in the creating commit (Durable `createConversation` with `init`).
3393
+ - Methods return data, never objects; an object is handed out as its address, and the receiver gets it with
3394
+ `ctx.get(Type, address)` (identity, lifecycle and principal stay with Chord).
3395
+ - Registries are extension points that add behaviour (tools, routes, renderers, commands, send routers, input gates);
3396
+ they never guard owned data.
3397
+ - Core owners are always present; a plugin owner may be missing (optional dependencies are registries owned by the
3398
+ dependent, "Cuts and closed deferred points").
3399
+ - Risk to avoid: a core god object. Each core owner owns only what belongs to it.
3400
+
3401
+ ## Porting guidelines
3402
+
3403
+ - **What core enforces on data belongs in a format core owns,** written by the plugin when it creates the data; a
3404
+ plugin's private document is read only by that plugin. Example: subagents/agent mark one-shot and cwd-following
3405
+ child conversations in a core per-conversation policy document (`readOnly`, `followsSessionCwd`) in the creating
3406
+ commit; chat and relocation read it directly; `ReadOnlyConversations` and `SessionCwdFollowers` go away. (This was
3407
+ plugin design, not a missing mechanism: today the rule is lost when subagents is not loaded.)
3408
+
3409
+ ## Still valid from `target-design.md`
3410
+
3411
+ Documents and the store (async `tx.draft`, keys, tags, eviction, Durable's transaction rule), job sandwich, routines
3412
+ (`self.routines.on/every/loop/custom`), registries (versioned, immutable tables, no late binding), the
3413
+ Questions service (`questions.md`), retry triggers and `retryOn`, revisions over the complete code set, mismatch rules
3414
+ during a rollout, start barrier. (**Superseded in part** by R25 (contributions may change; rejected are registries
3415
+ created at runtime and mutable indirection), R17 (the harness starts after the barrier) and R6 (strict compatibility).)
3416
+
3417
+ ## Open
3418
+
3419
+ Everything else from earlier lists is decided (see the entries above and `packages/chord/docs/design.md` §26).
3420
+
3421
+ - **Future goal: deploy Chord apps to Cloudflare Durable Objects and similar platforms.** Hot reload there is not
3422
+ needed; composition through plugins is (plugin set fixed at deploy). Research in `research/chord-on-durable-objects.md`.
3423
+ - Deferred until a concrete case: jobs as objects and where a flow lives (when login is ported); project plugins.
3424
+ - **Chord from code mode scripts** (owner, 2026-10-30: "something we want"; p3): the agent's code mode scripts get a
3425
+ Chord call scope (`host.root`) to call objects and change things: `chord.get("notes.store", {})` by name, every
3426
+ argument checked by the provider's closed schema. Needs, to design with p3: a principal for the agent (acting as who
3427
+ runs the session, so views apply their authority; never the owner's powers in a guest's session), a by-name `get`
3428
+ and a list of served contracts with their members and schemas (the agent's declarations), values crossing the
3429
+ sandbox boundary as plain data, the scope sealed when the script ends.
3430
+ - **Every object may contribute** (owner, 2026-10-29: "note it down as something to consider for the future; can be
3431
+ changed later without a need for migrations"): lift R21 so objects with params and `remote` objects contribute too;
3432
+ the entry lives while the object is live, its identity is the object's (type, params, principal key); `contribute`
3433
+ on every `self`. Not decided; R21 stands.
3434
+ - p3 domain points (decided per domain when p3 is designed): `SessionFiles {session}` (proposed), session delete,
3435
+ connection pools, config files with a single writer, file formats as versioned interfaces, the terminal host
3436
+ process as a revision artifact.