chainlesschain 0.166.107 → 0.166.109

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 (184) hide show
  1. package/README.md +18 -9
  2. package/package.json +1 -1
  3. package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
  4. package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
  5. package/src/assets/web-panel/assets/{AIOps-DCx6L_HW.js → AIOps-DkrNbDzS.js} +1 -1
  6. package/src/assets/web-panel/assets/{ActionButton-DYm1CN3Z.js → ActionButton-DJjFXDhv.js} +1 -1
  7. package/src/assets/web-panel/assets/{Analytics-D5ItUS_y.js → Analytics-DyUbagRn.js} +3 -3
  8. package/src/assets/web-panel/assets/{AppLayout-Cb7XeRdH.js → AppLayout-p7kJWSvn.js} +5 -5
  9. package/src/assets/web-panel/assets/{Artifacts-DDDeqCDQ.js → Artifacts-C3rVh4Ak.js} +1 -1
  10. package/src/assets/web-panel/assets/{Audit-BRt3x13w.js → Audit-Bch_L_YO.js} +1 -1
  11. package/src/assets/web-panel/assets/{BackgroundAgents-C_oNALMd.js → BackgroundAgents-BNHy8zP6.js} +1 -1
  12. package/src/assets/web-panel/assets/{Backup-PSU2p6CR.js → Backup-BUaye_Ys.js} +1 -1
  13. package/src/assets/web-panel/assets/{BaseInput-G3oL4hcX.js → BaseInput-D49mzAVz.js} +1 -1
  14. package/src/assets/web-panel/assets/{Chat-C2cLI10b.js → Chat-2MDzM_us.js} +6 -6
  15. package/src/assets/web-panel/assets/ChatBubbleRenderer-DOX3Y5EJ.js +1 -0
  16. package/src/assets/web-panel/assets/{Checkbox-CHLnEgFD.js → Checkbox-5p1T_KEq.js} +1 -1
  17. package/src/assets/web-panel/assets/{Codegen-DZm9g_Bc.js → Codegen-TKtiLaoZ.js} +1 -1
  18. package/src/assets/web-panel/assets/{Col-D5Z4zSay.js → Col-BVCMG9mS.js} +1 -1
  19. package/src/assets/web-panel/assets/{Community-DHw69NSk.js → Community-D09-QczH.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compact-B-vjYNWz.js → Compact-QuRMCnj7.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compliance-DaIYVZQY.js → Compliance-CN0PV7fk.js} +1 -1
  22. package/src/assets/web-panel/assets/{Cowork-CoA9U-gF.js → Cowork-5N2zhAty.js} +2 -2
  23. package/src/assets/web-panel/assets/{Cron-DXqmxlRC.js → Cron-BxT8UZ0S.js} +2 -2
  24. package/src/assets/web-panel/assets/{Crosschain-Cdkc3ue1.js → Crosschain-Dp-noQl4.js} +1 -1
  25. package/src/assets/web-panel/assets/{DID-oKfYyNSR.js → DID-B7MZFkjG.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dashboard-n_ycvgWu.js → Dashboard-DulJ3rKV.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dropdown-D38rZ8WY.js → Dropdown-DV4ov4zF.js} +1 -1
  28. package/src/assets/web-panel/assets/{EmailListRenderer-BjreeBQ1.js → EmailListRenderer-DB2-ooz6.js} +1 -1
  29. package/src/assets/web-panel/assets/{EvolutionSettings-CTxA_SIq.js → EvolutionSettings-B1Rb6p7q.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DmECNwV5.js → FamilyGuardDashboard-U8aHayx-.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-B2IaaDmQ.js → Federation-BkQULBHY.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-DgcgqUIk.js → FormItemContext-CtviG260.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-B1-du6Qm.js → GenericCardRenderer-DnPirB24.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-XceM21Il.js → Git-CK57Oro6.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-CV97iz3z.js → Governance-DbI-LB8-.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-CI7fj305.js → Inference-sEG4T1Jg.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-B5mekWRy.js → KnowledgeGraph-9SLJR3h9.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-BlxFA0xY.js → Logs-BIZYcISQ.js} +2 -2
  39. package/src/assets/web-panel/assets/{MarkdownRenderer-rDE-XyMY.js → MarkdownRenderer-9nfWupWP.js} +1 -1
  40. package/src/assets/web-panel/assets/{Marketplace-DL1BVCiX.js → Marketplace-CRmLLwAE.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-BPtwrO3c.js → McpTools-DkOFkSO1.js} +3 -3
  42. package/src/assets/web-panel/assets/{Memory-DuSNw4E-.js → Memory-M29Kd9TU.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-DTgY9ywI.js → MobileBridge-Tl6JvPw_.js} +1 -1
  44. package/src/assets/web-panel/assets/MobileProjects-Bq-FPDzS.js +1 -0
  45. package/src/assets/web-panel/assets/{Mtc-h4fCLjXw.js → Mtc-DkUjX1F5.js} +2 -2
  46. package/src/assets/web-panel/assets/{MtcAudit-fqMDY3-w.js → MtcAudit-BCqr1I4q.js} +2 -2
  47. package/src/assets/web-panel/assets/{Multisig-Do3FehBL.js → Multisig-DmoqYGQ5.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-CQVY0kQF.js → NLProgramming-DNRQeRke.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-4c3oNvWh.js → Notes-CQSBeiY6.js} +3 -3
  50. package/src/assets/web-panel/assets/{NotificationSettings-BKr2JD4k.js → NotificationSettings-gNY9bbH6.js} +1 -1
  51. package/src/assets/web-panel/assets/{OrderTableRenderer-Bq4jgzRt.js → OrderTableRenderer-DW0CNjh8.js} +1 -1
  52. package/src/assets/web-panel/assets/{Organization-Dy3XidyA.js → Organization-CthnbKag.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-DFF0qN5K.js → Overflow-_GQ3sbXu.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-DAgYYykd.js → P2P-BTr7ea6W.js} +2 -2
  55. package/src/assets/web-panel/assets/PdhVaultBrowser-D9McHF9I.js +7 -0
  56. package/src/assets/web-panel/assets/{Permissions-Cev6uSBc.js → Permissions-DMny2Cjs.js} +3 -3
  57. package/src/assets/web-panel/assets/{PersonalDataHub-DJzbN06K.js → PersonalDataHub-CyYlMSoI.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-CoJWvbQ5.js → Pipeline-FASu2Oc4.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-DZxV6etm.js → Privacy-9bC5FnsJ.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-BFW87JnM.js → ProjectInit-BOYZwB5G.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-LBrRT27p.js → ProjectSettings-45DViNG-.js} +2 -2
  62. package/src/assets/web-panel/assets/Projects-C2fZzMZc.js +1 -0
  63. package/src/assets/web-panel/assets/{Providers-CnRbjOYk.js → Providers-BgjvZx9F.js} +1 -1
  64. package/src/assets/web-panel/assets/{QrScannerModal-BVFnIS3r.js → QrScannerModal-Brw-3csB.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-DrcHx5f1.js → QuickAsk-DWG-A5zB.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-CbRgrSJm.js → Recommend-DuQGxjIR.js} +1 -1
  67. package/src/assets/web-panel/assets/{RemoteSession-B2Uz_ElB.js → RemoteSession-T1cdSSRL.js} +2 -2
  68. package/src/assets/web-panel/assets/{Reputation-DS0ljTxL.js → Reputation-9FT2dI4G.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-ClLobFht.js → Row-yNIbNftX.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-DxxcTD0x.js → RssFeed-BIchNoPd.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-BbHN0Fik.js → Search-msDJ8UTZ.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-BAVU_EcY.js → Security-DE46oBYT.js} +3 -3
  73. package/src/assets/web-panel/assets/{Services-CtqRSVq_.js → Services-Dori7vc3.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-IJ8ZKdSZ.js → Skeleton-D3O3sUFz.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-DXxcznnO.js → Skills-BV0ct8SO.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-fRkhnA3i.js → Sla-DflIVOrh.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-Bi8sYJ-I.js → SpeechSettings-Cpbv2Zz6.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-CjeplUtS.js → SyncSettings-Dgg6-wbe.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-D4KB2Hbx.js → Tasks-Cb6e4Klx.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-BwVugi8n.js → Templates-GfdAATGz.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-CsPuu5xK.js → Tenant-D0gecLXl.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-DuaeOPjY.js → Terminal-BFRPJt-0.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-D-PMrFxO.js → TimelineRenderer-BobtChxX.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-BXZuiQPZ.js → Tokens-CifeZtHV.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-DgpTaHL3.js → Trigger-BHBvbBCf.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-Bn7lEGRY.js → Trust-6lvqjKYp.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-zz-8JAAQ.js → UkeySign-CqzMc-G5.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-DCzfQGTc.js → VideoEditing-OIiRG582.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-BiRHM_zB.js → Wallet-4MeKq0q_.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-QmPo2JwU.js → WebAuthn-eHBRjGQG.js} +5 -5
  91. package/src/assets/web-panel/assets/{WorkflowEditor-gL7SsPnY.js → WorkflowEditor-DlzYdEqW.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-BysvK4hk.js → chat-CBcjOzkX.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-CQq20vqN.js → colors-zgwsYQSa.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-CAHlOf1X.js → compact-item-Dcd9H5Bn.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-BeF3ESj_.js → createContext-DR1tO-8r.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-DYhpRtve.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-CiqnRf-U.js → hasIn-CtfbUH_p.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Cp3IRTDK.js → index-5XaB_Tjg.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DKYbru3i.js → index-B3QUK1ks.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CCX0daFQ.js → index-BXfGHUz_.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CgkUkmEJ.js → index-BndPtS9D.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-zNkw0IY7.js → index-BoxowMYS.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-i9aJV9qk.js → index-C0kP88Pb.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BtWchYfW.js → index-C5XGvJb0.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BHpHGGmB.js → index-CPLLgPxA.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-qjaCk1Mj.js → index-CSQCHpDo.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CImCEo8S.js → index-CT7zIkqP.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Br5tGVbC.js → index-CaGNqkY0.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-xwtRrgVY.js → index-Cdhy5ckh.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CowbNBJs.js → index-CmVIM-m2.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CTp89vmg.js → index-CmzQzTY5.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D7q5VH2n.js → index-Cur8LNNn.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CeIL3-nY.js → index-CwHgaYVH.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-jJhemX1R.js → index-D1GxtY-h.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DTPUVm71.js → index-D7ZOiIKb.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-QALafVsT.js → index-D8WPohye.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-ZBuJ6O6g.js → index-DA2-Bhy8.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-B5941vyo.js → index-DHEiuvhA.js} +1 -1
  119. package/src/assets/web-panel/assets/index-DPJXFIY7.js +1 -0
  120. package/src/assets/web-panel/assets/{index-BsnLkZjz.js → index-DSvcz0RO.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DHGtFwwR.js → index-DTx4FyLU.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BOiyW31p.js → index-DVpp9YSQ.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-B2AOvjgR.js → index-DWG98orp.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BfJabF_8.js → index-DWuCGhHa.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DBmGQHEz.js → index-DYPMJ5Mb.js} +3 -3
  126. package/src/assets/web-panel/assets/{index-B2xbtl_c.js → index-DeAi4CNt.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-Bvkbj3x-.js → index-DfzOYmOj.js} +1 -1
  128. package/src/assets/web-panel/assets/index-DjBhR5M_.js +1 -0
  129. package/src/assets/web-panel/assets/{index-DXsXhR7p.js → index-DvPOpvvH.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-yFQWSRoW.js → index-J-ORW1i6.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-TdVEaJfk.js → index-JSfldpw4.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-CcccefJc.js → index-LN7Hkl1C.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-Dqkls2_6.js → index-dvF6C39V.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-BcVmt6u_.js → index-hTcre7U9.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-DXT7MKkD.js → index-oWOlF7CM.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-4a0Lmz_y.js → index-z8lrkcb-.js} +1 -1
  137. package/src/assets/web-panel/assets/{initDefaultProps-BQR2HnKb.js → initDefaultProps-CVMtDrWZ.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-gAz6l-nZ.js → motion-Dye3IU8k.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-Cy5TN856.js → move-C251Zo34.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-BeT7VrGi.js → mtc-parser-BDiOORoQ.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-BLN3mlgK.js → omit-DTDA_RHv.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-DtwZ8u2d.js → pickAttrs-CqEUTPcT.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-ViMwQSa-.js → placementArrow-BOB8QWH3.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-dEL3u4E7.js → responsiveObserve-CZDXP1d9.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-l05us-Gz.js → slide-DPaRw3E9.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-Bk-Qn2Og.js → statusUtils-Ba9d7XZ7.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-Bn186qeY.js → styleChecker-mZKU3lvW.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-WP-ugR3H.js → useFlexGapSupport-VcauLq0M.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-1SvoeSCL.js → useFs-Cal9KRcx.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-CfNU8N3z.js → usePersonalDataHub-DISoRyQB.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-Bd2Ya101.js → vnode-B58C6w6B.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-Def_MeZv.js → zoom-BPlBAMKB.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/data/changelog.json +29 -3
  155. package/src/gateways/ws/llm-chat-protocol.js +12 -2
  156. package/src/gateways/ws/llm-creds.js +50 -22
  157. package/src/gateways/ws/ws-server.js +47 -0
  158. package/src/harness/jsonl-session-store.js +128 -5
  159. package/src/harness/prompt-compressor.js +18 -10
  160. package/src/harness/structured-handoff.js +151 -13
  161. package/src/lib/agent-session-export.js +20 -0
  162. package/src/lib/background-agent-supervisor.js +54 -0
  163. package/src/lib/coding-task-guidance.js +2 -1
  164. package/src/lib/context-memory-kernel/live-compaction.js +75 -12
  165. package/src/lib/read-file-loop-guard.js +124 -8
  166. package/src/lib/session-host-snapshot.js +3 -0
  167. package/src/lib/session-message-provenance.js +24 -5
  168. package/src/lib/session-tool-round.js +39 -0
  169. package/src/lib/session-transcript-history.js +2 -0
  170. package/src/lib/session-transcript-page.js +4 -1
  171. package/src/lib/session-transcript-structure.js +4 -0
  172. package/src/runtime/__tests__/message-roles.test.js +29 -0
  173. package/src/runtime/agent-core.js +115 -43
  174. package/src/runtime/headless-runner.js +191 -34
  175. package/src/runtime/headless-stream.js +140 -42
  176. package/src/runtime/message-roles.js +43 -0
  177. package/src/workers/background-agent-worker.js +146 -47
  178. package/src/assets/web-panel/assets/ChatBubbleRenderer-KX1OITf8.js +0 -1
  179. package/src/assets/web-panel/assets/MobileProjects-CTpVHJMa.js +0 -1
  180. package/src/assets/web-panel/assets/PdhVaultBrowser-DgNHa8b1.js +0 -7
  181. package/src/assets/web-panel/assets/Projects-O3uz76R1.js +0 -1
  182. package/src/assets/web-panel/assets/devWarning-D-Iugqxv.js +0 -1
  183. package/src/assets/web-panel/assets/index-B0Oz2_rQ.js +0 -1
  184. package/src/assets/web-panel/assets/index-DwoycEQa.js +0 -1
@@ -13,6 +13,11 @@ import {
13
13
  import { JsonlSessionContextPort } from "./jsonl-session-context-port.js";
14
14
  import { createCliContextMemoryRuntime } from "./runtime.js";
15
15
  import { projectRuntimeTokenUsage } from "../runtime-usage-ledger.js";
16
+ import {
17
+ projectCanonicalResumeMessages,
18
+ getDurableSystemMessageProvenance,
19
+ isRuntimeOnlySessionMessage,
20
+ } from "../session-message-provenance.js";
16
21
 
17
22
  const rejectedCompactionUsage = new WeakMap();
18
23
 
@@ -138,7 +143,7 @@ class CallbackSessionContextPort extends InMemorySessionContextPort {
138
143
  }
139
144
  }
140
145
 
141
- function kernelBudget(compressor) {
146
+ function kernelBudget(compressor, ephemeralTokens = 0) {
142
147
  const inputBudget = Math.max(
143
148
  256,
144
149
  Math.trunc(Number(compressor?.maxTokens)) || 16_384,
@@ -150,7 +155,9 @@ function kernelBudget(compressor) {
150
155
  return {
151
156
  modelWindowTokens: inputBudget + reservedOutputTokens,
152
157
  reservedOutputTokens,
153
- safetyMarginTokens: 0,
158
+ // Live host policy/retry text still consumes the provider window even
159
+ // though it is deliberately absent from the durable authority partition.
160
+ safetyMarginTokens: ephemeralTokens,
154
161
  recoveryReserveTokens: Math.min(
155
162
  256,
156
163
  Math.max(1, Math.floor(inputBudget * 0.05)),
@@ -202,7 +209,7 @@ export async function compactLiveMessagesCanonical(
202
209
  messagesInput,
203
210
  options = {},
204
211
  ) {
205
- const messages = Array.isArray(messagesInput) ? [...messagesInput] : [];
212
+ const liveMessages = Array.isArray(messagesInput) ? [...messagesInput] : [];
206
213
  const sessionId = String(options.sessionId || `live-${randomUUID()}`);
207
214
  const operationId = String(options.operationId || `compact-${randomUUID()}`);
208
215
  const trigger = options.trigger || "auto";
@@ -224,6 +231,24 @@ export async function compactLiveMessagesCanonical(
224
231
  options.persist !== false &&
225
232
  Boolean(options.sessionId) &&
226
233
  sessionExists(sessionId);
234
+ // Host policy is live-only. Durable summary/lineage systems retain their
235
+ // private provenance and remain part of the exact authority comparison.
236
+ const messages = persistentSession
237
+ ? projectCanonicalResumeMessages(liveMessages, { strict: true })
238
+ : liveMessages;
239
+ const ephemeralMessages = [];
240
+ if (persistentSession) {
241
+ let sequence = 0;
242
+ for (const message of liveMessages) {
243
+ if (
244
+ isRuntimeOnlySessionMessage(message) ||
245
+ (message.role === "system" &&
246
+ !getDurableSystemMessageProvenance(message))
247
+ )
248
+ ephemeralMessages.push({ sequence, message });
249
+ else sequence += 1;
250
+ }
251
+ }
227
252
  if (typeof options.commit === "function") {
228
253
  const bootstrapRuntime = createCliContextMemoryRuntime({
229
254
  sessionId,
@@ -323,12 +348,29 @@ export async function compactLiveMessagesCanonical(
323
348
  const provider = options.provider || "provider.local";
324
349
  const model = options.model || "default";
325
350
  const memoryRevision = await runtime.memoryPort.getRevision();
351
+ let settledOutputItems = null;
352
+ if (ephemeralMessages.length) {
353
+ const appendCompaction = runtime.sessionPort.appendCompaction.bind(
354
+ runtime.sessionPort,
355
+ );
356
+ runtime.sessionPort.appendCompaction = async (event, expectedHead) => {
357
+ const result = await appendCompaction(event, expectedHead);
358
+ if (result?.ok === true) settledOutputItems = event.outputItems;
359
+ return result;
360
+ };
361
+ }
326
362
  let receipt;
327
363
  try {
328
364
  receipt = await runtime.kernel.compactContext({
329
365
  operationId,
330
366
  sessionId,
331
- ...kernelBudget(compressor),
367
+ ...kernelBudget(
368
+ compressor,
369
+ messagesToContextItems(
370
+ ephemeralMessages.map(({ message }) => message),
371
+ { sessionId },
372
+ ).reduce((total, item) => total + item.tokenEstimate, 0),
373
+ ),
332
374
  sink: provider,
333
375
  scopeAdmissions: [{ scope: "session", scopeId: sessionId }],
334
376
  policyVersion: "cli-context-policy-v1",
@@ -421,7 +463,7 @@ export async function compactLiveMessagesCanonical(
421
463
  }
422
464
  if (!["committed", "degraded"].includes(receipt.status)) {
423
465
  return {
424
- messages,
466
+ messages: liveMessages,
425
467
  stats: {
426
468
  ...(summaryStats || {}),
427
469
  strategy: "none",
@@ -433,20 +475,41 @@ export async function compactLiveMessagesCanonical(
433
475
  };
434
476
  }
435
477
  const snapshot = await runtime.sessionPort.readSnapshot(sessionId);
436
- const outputMessages = contextItemsToMessages(snapshot.items);
478
+ // Reattach host policy and private retry nudges at their durable sequence
479
+ // boundaries. They remain live, never enter the receipt, and do not move a
480
+ // retry instruction behind tool exchanges that happened after that retry.
481
+ const restoredMessages = [];
482
+ let ephemeralIndex = 0;
483
+ for (const item of [...(settledOutputItems || snapshot.items)].sort(
484
+ (left, right) =>
485
+ (left.sourceRef.eventSequence ?? Number.MAX_SAFE_INTEGER) -
486
+ (right.sourceRef.eventSequence ?? Number.MAX_SAFE_INTEGER) ||
487
+ left.itemId.localeCompare(right.itemId, "en"),
488
+ )) {
489
+ const sequence = item.sourceRef.eventSequence ?? Number.MAX_SAFE_INTEGER;
490
+ while (
491
+ ephemeralIndex < ephemeralMessages.length &&
492
+ ephemeralMessages[ephemeralIndex].sequence <= sequence
493
+ )
494
+ restoredMessages.push(ephemeralMessages[ephemeralIndex++].message);
495
+ restoredMessages.push(...contextItemsToMessages([item]));
496
+ }
497
+ restoredMessages.push(
498
+ ...ephemeralMessages.slice(ephemeralIndex).map(({ message }) => message),
499
+ );
437
500
  return {
438
- messages: outputMessages,
501
+ messages: restoredMessages,
439
502
  stats: {
440
503
  ...(summaryStats || {}),
441
504
  strategy:
442
505
  receipt.status === "degraded" ? "deterministic-fallback" : "canonical",
443
- originalMessages: messages.length,
444
- compressedMessages: outputMessages.length,
445
- originalTokens: estimatedTokens(messages),
446
- compressedTokens: estimatedTokens(outputMessages),
506
+ originalMessages: liveMessages.length,
507
+ compressedMessages: restoredMessages.length,
508
+ originalTokens: estimatedTokens(liveMessages),
509
+ compressedTokens: estimatedTokens(restoredMessages),
447
510
  saved: Math.max(
448
511
  0,
449
- estimatedTokens(messages) - estimatedTokens(outputMessages),
512
+ estimatedTokens(liveMessages) - estimatedTokens(restoredMessages),
450
513
  ),
451
514
  canonicalAlreadySettled: true,
452
515
  canonicalReceipt: receipt,
@@ -32,6 +32,79 @@ export class ReadFileLoopGuard {
32
32
  this.batch = null;
33
33
  this.largeOutputs = new Set();
34
34
  this.editRecoveryReads = new Map();
35
+ this.visiblePages = null;
36
+ this.visibleRequest = 0;
37
+ }
38
+
39
+ /** Observe only the request actually selected for the provider, not history. */
40
+ observeVisibleMessages(messages) {
41
+ this.visibleRequest++;
42
+ this.visiblePages = [];
43
+ const retain = (value) => {
44
+ if (
45
+ typeof value?.path !== "string" ||
46
+ typeof value.fileVersion !== "string"
47
+ )
48
+ return;
49
+ if (typeof value.content === "string" && !value.contentOmitted) {
50
+ this.visiblePages.push(value);
51
+ }
52
+ for (const excerpt of Array.isArray(value.excerpts)
53
+ ? value.excerpts
54
+ : []) {
55
+ if (typeof excerpt?.content === "string")
56
+ this.visiblePages.push({
57
+ ...excerpt,
58
+ path: value.path,
59
+ fileVersion: value.fileVersion,
60
+ });
61
+ }
62
+ };
63
+ const retainContext = (context) => {
64
+ if (
65
+ typeof context !== "string" ||
66
+ !context.startsWith("[File excerpts retained across compaction")
67
+ )
68
+ return;
69
+ try {
70
+ const entries = JSON.parse(context.slice(context.indexOf("\n") + 1));
71
+ if (Array.isArray(entries)) entries.forEach(retain);
72
+ } catch {
73
+ /* A truncated hint provides no confirmed source evidence. */
74
+ }
75
+ };
76
+ for (const message of messages || []) {
77
+ if (message.role === "assistant") retainContext(message.content);
78
+ if (message.role !== "tool") continue;
79
+ let value;
80
+ try {
81
+ value =
82
+ typeof message.content === "string"
83
+ ? JSON.parse(message.content.split("\n\n[Budget ")[0])
84
+ : message.content;
85
+ } catch {
86
+ continue;
87
+ }
88
+ retain(value);
89
+ for (const context of Array.isArray(value?.retainedReadContext)
90
+ ? value.retainedReadContext
91
+ : []) {
92
+ retainContext(context);
93
+ }
94
+ }
95
+ }
96
+
97
+ isPageVisible(page) {
98
+ return (
99
+ this.visiblePages?.some(
100
+ (visible) =>
101
+ keyFor(visible.path, {}) === keyFor(page.path, {}) &&
102
+ visible.fileVersion === page.fileVersion &&
103
+ typeof page.content === "string" &&
104
+ page.content.length > 0 &&
105
+ visible.content.includes(page.content),
106
+ ) === true
107
+ );
35
108
  }
36
109
 
37
110
  recordEditRecovery(result) {
@@ -94,6 +167,7 @@ export class ReadFileLoopGuard {
94
167
  rereads: new Set(),
95
168
  summary: null,
96
169
  excerpts: [],
170
+ contextRestoreOffered: false,
97
171
  };
98
172
  }
99
173
  let page = readPage(args);
@@ -108,11 +182,11 @@ export class ReadFileLoopGuard {
108
182
  readProgress: { newContent: false },
109
183
  };
110
184
  }
111
- const span = page.readSpan;
185
+ let span = page.readSpan;
112
186
  const covered = entry.spans.some(
113
187
  ([start, end]) => start <= span.start && end >= span.end,
114
188
  );
115
- const targeted =
189
+ let targeted =
116
190
  Number(args.offset) > 0 &&
117
191
  Number(args.limit) > 0 &&
118
192
  Number(args.limit) <= 200 &&
@@ -122,20 +196,52 @@ export class ReadFileLoopGuard {
122
196
  );
123
197
  let newContent = !covered;
124
198
  const hadOutline = !!entry.summary?.outline;
199
+ // A planner can evict a read without compacting the transcript. Give one
200
+ // bounded landing page for that version, then require explicit ranges.
201
+ // Dropping the same evidence repeatedly must not reopen full-file scans.
202
+ if (
203
+ covered &&
204
+ entry.summary?.reachedEnd === true &&
205
+ !targeted &&
206
+ this.visiblePages !== null &&
207
+ entry.lastReadRequest < this.visibleRequest &&
208
+ !this.isPageVisible(page) &&
209
+ !entry.contextRestoreOffered &&
210
+ entry.rereads.size < 3
211
+ ) {
212
+ const offset =
213
+ Number.isSafeInteger(Number(args.offset)) && Number(args.offset) > 0
214
+ ? Number(args.offset)
215
+ : 1;
216
+ const bounded = readPage({ ...args, offset, limit: 80 });
217
+ if (!bounded.error && bounded.readSpan && !this.isPageVisible(bounded)) {
218
+ page = bounded;
219
+ span = page.readSpan;
220
+ targeted = true;
221
+ entry.contextRestoreOffered = true;
222
+ }
223
+ }
125
224
  if (covered || (coveredPrefix && !targeted)) {
126
225
  // Permit a bounded, explicit reread for editing or lost context. Broad
127
226
  // rescans and subsequent repeats recover automatically, including when
128
227
  // the model varies offset/limit inside an already covered region.
129
- const fingerprint = createHash("sha256")
130
- .update(JSON.stringify(span))
131
- .digest("hex");
228
+ const restoring =
229
+ this.visiblePages !== null &&
230
+ entry.lastReadRequest < this.visibleRequest &&
231
+ !this.isPageVisible(page);
232
+ const fingerprint =
233
+ (restoring ? "restore:" : "review:") +
234
+ createHash("sha256").update(JSON.stringify(span)).digest("hex");
132
235
  if (
133
236
  targeted &&
134
237
  !entry.rereads.has(fingerprint) &&
135
238
  entry.rereads.size < 3
136
239
  ) {
137
240
  entry.rereads.add(fingerprint);
138
- page.readRecovery = { action: "targeted-review" };
241
+ page.readRecovery = {
242
+ action: restoring ? "context-restore" : "targeted-review",
243
+ };
244
+ if (restoring) page.readRecovery.fileVersion = fileVersion;
139
245
  } else {
140
246
  const unread = entry.spans[0]?.[0] === 0 ? entry.spans[0][1] : 0;
141
247
  if (unread < span.total) {
@@ -162,12 +268,20 @@ export class ReadFileLoopGuard {
162
268
  ...(entry.summary?.outline
163
269
  ? { outline: entry.summary.outline }
164
270
  : {}),
165
- hint: "The entire unchanged file has already been read during this run. Do not restart the scan. Use the retained outline/findings to perform the user's task; use search_files and a small explicit offset/limit only for missing details. Reading the file is not task completion.",
271
+ recoveryRead: {
272
+ path: filePath,
273
+ offset: 1,
274
+ limit: 80,
275
+ ...(args.hashed === true ? { hashed: true } : {}),
276
+ ...(args.raw === true ? { raw: true } : {}),
277
+ },
278
+ hint: "The unchanged file was read earlier, but historical coverage does not mean its source is visible now. Use any source excerpts actually present in this request. For missing details, use search_files or read_file with a specific offset and limit (at most 80 lines); recoveryRead is a bounded starting range, not permission to restart a full scan. Reading the file is not task completion.",
166
279
  };
167
280
  }
168
281
  }
169
282
  }
170
283
  entry.spans = mergeSpan(entry.spans, page.readSpan);
284
+ entry.lastReadRequest = this.visibleRequest;
171
285
  // Coverage alone cannot restore source code after compaction. Keep bounded
172
286
  // observed pages for every file type, not only large Markdown outlines.
173
287
  // Label truncation explicitly; a retained prefix is not the whole range.
@@ -232,6 +346,8 @@ export class ReadFileLoopGuard {
232
346
  if (tool === "read_file") {
233
347
  if (result?.readRecovery?.action === "edit-anchor-refresh")
234
348
  this.batch.advanced = true;
349
+ if (result?.readRecovery?.action === "context-restore")
350
+ this.batch.advanced = true;
235
351
  if (result?.readProgress?.newContent === true) this.batch.advanced = true;
236
352
  if (result?.readProgress?.newContent === false) this.batch.duplicates++;
237
353
  } else if (
@@ -308,7 +424,7 @@ export class ReadFileLoopGuard {
308
424
 
309
425
  get recoveryHint() {
310
426
  return this.repeatedBatches >= 1
311
- ? "Repeated file reads or identical large command outputs detected. Do not switch to run_code/run_shell to dump the same file again. Filter/count locally and print only relevant rows, totals and source line numbers, or use a focused search. The runtime continues unfinished read_file scans automatically and returns use-findings after EOF. Use the retained outline to perform the user's task. Do not restart a whole-file scan after context compaction; answer once the requested evidence is sufficient."
427
+ ? "Repeated file reads or identical large command outputs detected. Do not switch to run_code/run_shell to dump the same file again. Filter/count locally and print only relevant rows, totals and source line numbers, or use a focused search. The runtime continues unfinished read_file scans automatically and returns use-findings after EOF. Use source evidence actually visible in this request; recover missing details with a specific offset and limit of at most 80 lines. Do not restart a whole-file scan after context compaction; answer once the requested evidence is sufficient."
312
428
  : null;
313
429
  }
314
430
 
@@ -27,6 +27,7 @@ import {
27
27
  sanitizePersistedMessages,
28
28
  } from "./session-message-provenance.js";
29
29
  import { createSessionTranscriptStructureProjection } from "./session-transcript-structure.js";
30
+ import { projectToolRoundMessages } from "./session-tool-round.js";
30
31
  import { normalizeSessionBudgetRootConfig } from "./session-budget-production-root.js";
31
32
  import {
32
33
  createDeferredQuestionEventReducer,
@@ -86,6 +87,8 @@ function replayMessagesFromVerifiedEvents(events) {
86
87
  isReplayableMessage(event.data)
87
88
  ) {
88
89
  suffix.push(event.data);
90
+ } else if (event?.type === "tool_round") {
91
+ suffix.push(...projectToolRoundMessages(event).toReversed());
89
92
  } else if (event?.type === "ws_turn") {
90
93
  const turn = projectWsTurnMessages(event);
91
94
  if (turn) suffix.push(turn.assistant, turn.user);
@@ -26,6 +26,7 @@ export const DURABLE_SYSTEM_MESSAGE_KINDS = Object.freeze({
26
26
 
27
27
  const DURABLE_KINDS = new Set(Object.values(DURABLE_SYSTEM_MESSAGE_KINDS));
28
28
  const RUNTIME_PROVENANCE = new WeakMap();
29
+ const RUNTIME_ONLY_MESSAGES = new WeakSet();
29
30
  const INVALID_DATA = Symbol("invalid-session-message-data");
30
31
  const MAX_DATA_DEPTH = 100;
31
32
  const MAX_DATA_NODES = 100_000;
@@ -227,11 +228,11 @@ function validWireTag(value) {
227
228
  const snapshot = plainDataSnapshot(value);
228
229
  return Boolean(
229
230
  snapshot &&
230
- snapshot.keys.length === 2 &&
231
- snapshot.keys.includes("schema") &&
232
- snapshot.keys.includes("kind") &&
233
- dataValue(snapshot, "schema") === SESSION_MESSAGE_PROVENANCE_SCHEMA &&
234
- DURABLE_KINDS.has(dataValue(snapshot, "kind")),
231
+ snapshot.keys.length === 2 &&
232
+ snapshot.keys.includes("schema") &&
233
+ snapshot.keys.includes("kind") &&
234
+ dataValue(snapshot, "schema") === SESSION_MESSAGE_PROVENANCE_SCHEMA &&
235
+ DURABLE_KINDS.has(dataValue(snapshot, "kind")),
235
236
  );
236
237
  }
237
238
 
@@ -248,6 +249,23 @@ export function markDurableSystemMessage(message, kind) {
248
249
  return attachRuntimeTag(message, canonicalTag(kind));
249
250
  }
250
251
 
252
+ // Only host-created retry/result-delivery messages can be excluded from replay.
253
+ // There is deliberately no wire tag: a provider field or a serialized copy
254
+ // cannot suppress a real user message from authority comparison.
255
+ export function markRuntimeOnlySessionMessage(message) {
256
+ const snapshot = plainDataSnapshot(message);
257
+ if (!snapshot || !["user", "assistant"].includes(dataValue(snapshot, "role")))
258
+ throw new TypeError(
259
+ "Runtime-only nudges must be user or assistant messages",
260
+ );
261
+ RUNTIME_ONLY_MESSAGES.add(message);
262
+ return message;
263
+ }
264
+
265
+ export function isRuntimeOnlySessionMessage(message) {
266
+ return RUNTIME_ONLY_MESSAGES.has(message);
267
+ }
268
+
251
269
  export function getDurableSystemMessageProvenance(message) {
252
270
  const snapshot = plainDataSnapshot(message);
253
271
  if (!snapshot || dataValue(snapshot, "role") !== "system") return null;
@@ -422,6 +440,7 @@ export function projectCanonicalResumeMessages(messages, options = {}) {
422
440
  invalid("Canonical resume message must contain JSON-safe data");
423
441
  continue;
424
442
  }
443
+ if (isRuntimeOnlySessionMessage(raw)) continue;
425
444
  if (role !== "system") {
426
445
  projected.push(clean);
427
446
  } else if (runtimeTag) {
@@ -0,0 +1,39 @@
1
+ /** Append-only, complete provider assistant/tool exchanges. No policy or user
2
+ * messages may enter durable replay through this event. */
3
+ export const TOOL_ROUND_EVENT = "tool_round";
4
+ export const TOOL_ROUND_SCHEMA = "chainlesschain.session-tool-round/v1";
5
+
6
+ export function assertCompleteToolRounds(messages) {
7
+ if (!Array.isArray(messages) || messages.length === 0)
8
+ throw new TypeError("Tool rounds require a non-empty message array");
9
+ const pending = new Set();
10
+ for (const message of messages) {
11
+ if (message?.role === "assistant" && pending.size === 0) {
12
+ if (!Array.isArray(message.tool_calls) || message.tool_calls.length === 0)
13
+ throw new TypeError("Tool round assistant must contain tool calls");
14
+ for (const call of message.tool_calls) {
15
+ if (typeof call?.id !== "string" || !call.id || pending.has(call.id))
16
+ throw new TypeError("Malformed or duplicate tool call");
17
+ pending.add(call.id);
18
+ }
19
+ } else if (
20
+ message?.role === "tool" &&
21
+ pending.delete(message.tool_call_id)
22
+ ) {
23
+ // Every result settles exactly one call in the immediately preceding round.
24
+ } else {
25
+ throw new TypeError(
26
+ "Tool round contains an orphan result or forbidden message",
27
+ );
28
+ }
29
+ }
30
+ if (pending.size) throw new TypeError("Tool round is incomplete");
31
+ return messages;
32
+ }
33
+
34
+ export function projectToolRoundMessages(event) {
35
+ if (event?.type !== TOOL_ROUND_EVENT) return null;
36
+ if (event.data?.schema !== TOOL_ROUND_SCHEMA)
37
+ throw new TypeError("Unsupported session tool round schema");
38
+ return assertCompleteToolRounds(event.data.messages);
39
+ }
@@ -11,6 +11,7 @@ import {
11
11
  } from "./session-transcript-page.js";
12
12
  import { createSessionHistoryOrigins } from "./session-history-origins.js";
13
13
  import { getDurableSystemMessageProvenance } from "./session-message-provenance.js";
14
+ import { projectToolRoundMessages } from "./session-tool-round.js";
14
15
  import {
15
16
  BRANCH_HISTORY_SCHEMA,
16
17
  BRANCH_HISTORY_SYSTEM_ORIGINS_SCHEMA,
@@ -29,6 +30,7 @@ const MAX_HISTORY_RANGES = 16384;
29
30
  // Enumerate all possible display sources, including replacement snapshots.
30
31
  // Their physical sequence never changes when the selected history rewinds.
31
32
  function eventMessages(event, imported = null) {
33
+ if (event.type === "tool_round") return projectToolRoundMessages(event);
32
34
  if (event.type === BRANCH_HISTORY_MESSAGE)
33
35
  return imported?.kind === "archive" ? [imported.message] : [];
34
36
  if (
@@ -1,3 +1,4 @@
1
+ import { projectToolRoundMessages } from "./session-tool-round.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import {
3
4
  readVerifiedProjection,
@@ -120,7 +121,9 @@ export function createSessionTranscriptPageProjection(
120
121
  ["user_message", "assistant_message", "system"].includes(event.type)
121
122
  )
122
123
  append(event.data);
123
- else if (event.type === "ws_turn") {
124
+ else if (event.type === "tool_round") {
125
+ for (const message of projectToolRoundMessages(event)) append(message);
126
+ } else if (event.type === "ws_turn") {
124
127
  const pair = projectWsTurnMessages(event);
125
128
  if (pair) {
126
129
  append(pair.user);
@@ -1,3 +1,4 @@
1
+ import { projectToolRoundMessages } from "./session-tool-round.js";
1
2
  /**
2
3
  * Streaming structural authority for canonical session transcripts.
3
4
  *
@@ -72,6 +73,9 @@ export function createSessionTranscriptStructureProjection(
72
73
 
73
74
  return Object.freeze({
74
75
  accept(event) {
76
+ // Unknown versions and malformed/orphan results fail closed at every
77
+ // verified projection, even if that projection does not display tools.
78
+ if (event?.type === "tool_round") projectToolRoundMessages(event);
75
79
  const index = eventCount;
76
80
  eventCount += 1;
77
81
  if (event?.type === "session_start") {
@@ -2,9 +2,38 @@ import { describe, it, expect } from "vitest";
2
2
  import {
3
3
  mergeConsecutiveMessages,
4
4
  collapseConsecutiveMessagesInPlace,
5
+ createResumeRoleProjection,
5
6
  _internal,
6
7
  } from "../message-roles.js";
7
8
 
9
+ describe("createResumeRoleProjection", () => {
10
+ it("normalizes retained resume pairs across clones without folding later feedback", () => {
11
+ const initial = [
12
+ { role: "system", content: "policy" },
13
+ { role: "user", content: "unanswered" },
14
+ { role: "user", content: "resume" },
15
+ ];
16
+ const project = createResumeRoleProjection(initial);
17
+ const request = [
18
+ ...structuredClone(initial),
19
+ { role: "assistant", content: "reply" },
20
+ { role: "user", content: "feedback" },
21
+ { role: "user", content: "next input" },
22
+ ];
23
+ const before = structuredClone(request);
24
+ const projected = project(request);
25
+ expect(request).toEqual(before);
26
+ expect(initial).toHaveLength(3);
27
+ expect(
28
+ projected
29
+ .filter((message) => message.role === "user")
30
+ .map((message) => message.content),
31
+ ).toEqual(["unanswered\n\nresume", "feedback", "next input"]);
32
+ expect(projected.at(-1)).toBe(request.at(-1));
33
+ expect(project(request)).toEqual(projected);
34
+ });
35
+ });
36
+
8
37
  describe("mergeConsecutiveMessages", () => {
9
38
  it("is a no-op on a healthy alternating transcript", () => {
10
39
  const msgs = [