@gajae-code/coding-agent 0.11.11 → 0.12.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 (344) hide show
  1. package/CHANGELOG.md +131 -4
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/cli/read-cli.d.ts +1 -0
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/provider-auth-health.d.ts +14 -0
  9. package/dist/types/config/provider-ranking.d.ts +59 -0
  10. package/dist/types/config/settings-schema.d.ts +83 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/exa/mcp-client.d.ts +10 -1
  13. package/dist/types/exec/bash-executor.d.ts +45 -12
  14. package/dist/types/extensibility/extensions/types.d.ts +38 -4
  15. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  17. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  18. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  19. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  20. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  22. package/dist/types/extensibility/shared-events.d.ts +6 -0
  23. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  24. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  25. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  26. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  27. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  29. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  30. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
  31. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  32. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  33. package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
  34. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  35. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  36. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  37. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
  38. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  39. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  40. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  41. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  42. package/dist/types/main.d.ts +1 -1
  43. package/dist/types/memories/index.d.ts +2 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +10 -19
  45. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  46. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  47. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  48. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  49. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  50. package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
  51. package/dist/types/modes/components/settings-selector.d.ts +11 -0
  52. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  53. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  54. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  55. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  56. package/dist/types/modes/interactive-mode.d.ts +2 -0
  57. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  58. package/dist/types/modes/types.d.ts +2 -0
  59. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  60. package/dist/types/reminders/star-reminder.d.ts +3 -13
  61. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  62. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  64. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  65. package/dist/types/sdk/broker/transport.d.ts +2 -0
  66. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  68. package/dist/types/sdk/bus/config.d.ts +6 -2
  69. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  70. package/dist/types/sdk/bus/index.d.ts +44 -1
  71. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
  72. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  73. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  74. package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
  75. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  76. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  77. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  78. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  79. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  80. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  81. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  82. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  83. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  84. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  85. package/dist/types/sdk/host/host.d.ts +2 -0
  86. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  87. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  88. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  89. package/dist/types/sdk/prompt-status.d.ts +23 -3
  90. package/dist/types/sdk/session.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +82 -12
  92. package/dist/types/session/artifacts.d.ts +8 -0
  93. package/dist/types/session/blob-store.d.ts +57 -4
  94. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  95. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  96. package/dist/types/session/session-manager.d.ts +48 -19
  97. package/dist/types/session/session-storage.d.ts +4 -0
  98. package/dist/types/session/streaming-output.d.ts +96 -12
  99. package/dist/types/setup/credential-import.d.ts +12 -0
  100. package/dist/types/skill-state/active-state.d.ts +12 -4
  101. package/dist/types/skill-state/workflow-hud.d.ts +9 -0
  102. package/dist/types/task/executor.d.ts +3 -2
  103. package/dist/types/task/index.d.ts +3 -1
  104. package/dist/types/task/provider-retry-status.d.ts +8 -0
  105. package/dist/types/task/receipt.d.ts +7 -1
  106. package/dist/types/task/types.d.ts +23 -0
  107. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  108. package/dist/types/tools/bash-interactive.d.ts +4 -1
  109. package/dist/types/tools/bash.d.ts +3 -1
  110. package/dist/types/tools/browser/launch.d.ts +7 -0
  111. package/dist/types/tools/fetch.d.ts +17 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -0
  113. package/dist/types/tools/index.d.ts +4 -0
  114. package/dist/types/tools/output-meta.d.ts +44 -3
  115. package/dist/types/tools/path-utils.d.ts +1 -0
  116. package/dist/types/tools/read-internals.d.ts +12 -0
  117. package/dist/types/tools/read.d.ts +45 -7
  118. package/dist/types/tools/resource-gc.d.ts +4 -0
  119. package/dist/types/tools/review.d.ts +5 -7
  120. package/dist/types/tools/subagent.d.ts +2 -0
  121. package/dist/types/tools/tool-result.d.ts +7 -1
  122. package/dist/types/utils/gh.d.ts +13 -0
  123. package/dist/types/utils/image-resize.d.ts +1 -0
  124. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  125. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  126. package/dist/types/web/search/providers/xai.d.ts +2 -0
  127. package/package.json +11 -13
  128. package/scripts/acp-conformance-agent.ts +218 -0
  129. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  130. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  131. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  132. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  133. package/scripts/resident-memory-bench.ts +1341 -0
  134. package/scripts/run-acp-conformance.ts +285 -0
  135. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  136. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  137. package/src/async/job-manager.ts +78 -20
  138. package/src/capability/index.ts +8 -0
  139. package/src/cli/fast-help.ts +1 -1
  140. package/src/cli/plugin-cli.ts +176 -18
  141. package/src/cli/read-cli.ts +2 -1
  142. package/src/cli.ts +1 -0
  143. package/src/commands/deep-interview.ts +29 -9
  144. package/src/commands/harness.ts +35 -12
  145. package/src/commands/read.ts +11 -2
  146. package/src/commands/team.ts +20 -2
  147. package/src/config/file-lock.ts +11 -2
  148. package/src/config/model-registry.ts +21 -2
  149. package/src/config/provider-auth-health.ts +42 -0
  150. package/src/config/provider-ranking.ts +119 -0
  151. package/src/config/settings-schema.ts +74 -1
  152. package/src/config/settings.ts +246 -108
  153. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  154. package/src/coordinator-mcp/server.ts +5 -12
  155. package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
  156. package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
  157. package/src/defaults/gjc/skills/team/SKILL.md +3 -1
  158. package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
  159. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  160. package/src/defaults/gjc-defaults.ts +4 -4
  161. package/src/eval/py/executor.ts +7 -2
  162. package/src/exa/mcp-client.ts +12 -3
  163. package/src/exec/bash-executor.ts +144 -27
  164. package/src/extensibility/extensions/runner.ts +15 -0
  165. package/src/extensibility/extensions/types.ts +46 -5
  166. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  167. package/src/extensibility/gjc-plugins/index.ts +19 -2
  168. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  169. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  170. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  171. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  172. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  173. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  174. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  175. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  176. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  177. package/src/extensibility/gjc-plugins/types.ts +141 -0
  178. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  179. package/src/extensibility/shared-events.ts +5 -0
  180. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  181. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  182. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  183. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  184. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  185. package/src/gjc-runtime/gc-render.ts +1 -0
  186. package/src/gjc-runtime/gc-runtime.ts +11 -1
  187. package/src/gjc-runtime/launch-tmux.ts +25 -6
  188. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  189. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  190. package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
  191. package/src/gjc-runtime/state-runtime.ts +2 -1
  192. package/src/gjc-runtime/state-writer.ts +70 -222
  193. package/src/gjc-runtime/team-launch.ts +42 -5
  194. package/src/gjc-runtime/team-runtime.ts +1085 -40
  195. package/src/gjc-runtime/team-store.ts +32 -0
  196. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  197. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  198. package/src/gjc-runtime/team-workers.ts +1 -0
  199. package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
  200. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  201. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  202. package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
  203. package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
  204. package/src/gjc-runtime/workflow-manifest.ts +38 -247
  205. package/src/hooks/native-skill-hook.ts +17 -4
  206. package/src/hooks/skill-state.ts +1 -23
  207. package/src/internal-urls/docs-index.generated.ts +21 -22
  208. package/src/internal-urls/local-root-gc.ts +127 -0
  209. package/src/main.ts +9 -5
  210. package/src/memories/index.ts +10 -0
  211. package/src/modes/DESIGN.md +98 -0
  212. package/src/modes/acp/acp-agent.ts +339 -191
  213. package/src/modes/acp/acp-event-mapper.ts +14 -7
  214. package/src/modes/acp/acp-mode.ts +39 -3
  215. package/src/modes/components/assistant-message.ts +123 -22
  216. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  217. package/src/modes/components/irc-sidebar.ts +176 -62
  218. package/src/modes/components/model-selector.ts +61 -4
  219. package/src/modes/components/notifications-settings-editor.ts +12 -1
  220. package/src/modes/components/oauth-selector.ts +96 -28
  221. package/src/modes/components/read-tool-group.ts +82 -14
  222. package/src/modes/components/session-observer-overlay.ts +457 -83
  223. package/src/modes/components/settings-selector.ts +116 -23
  224. package/src/modes/components/status-line/gh.ts +16 -0
  225. package/src/modes/components/tool-execution.ts +138 -27
  226. package/src/modes/components/tool-status-header.ts +3 -14
  227. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  228. package/src/modes/controllers/command-controller.ts +4 -0
  229. package/src/modes/controllers/event-controller.ts +109 -27
  230. package/src/modes/controllers/extension-ui-controller.ts +26 -2
  231. package/src/modes/controllers/input-controller.ts +18 -3
  232. package/src/modes/controllers/selector-controller.ts +127 -36
  233. package/src/modes/interactive-mode.ts +62 -3
  234. package/src/modes/irc-observation-ledger.ts +71 -15
  235. package/src/modes/runtime-init.ts +22 -2
  236. package/src/modes/types.ts +2 -0
  237. package/src/modes/utils/ui-helpers.ts +94 -1
  238. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  239. package/src/prompts/agents/architect.md +9 -1
  240. package/src/prompts/agents/critic.md +9 -0
  241. package/src/prompts/system/eager-todo.md +8 -4
  242. package/src/prompts/system/system-prompt.md +11 -5
  243. package/src/prompts/tools/bash.md +5 -5
  244. package/src/prompts/tools/read.md +4 -2
  245. package/src/prompts/tools/search-tool-bm25.md +5 -0
  246. package/src/prompts/tools/task.md +1 -1
  247. package/src/reminders/star-reminder.ts +4 -42
  248. package/src/runtime-mcp/manager.ts +19 -4
  249. package/src/runtime-mcp/smithery-auth.ts +17 -2
  250. package/src/runtime-mcp/smithery-connect.ts +11 -1
  251. package/src/sdk/acp/adapter.ts +22 -6
  252. package/src/sdk/broker/lifecycle.ts +25 -6
  253. package/src/sdk/broker/transport.ts +35 -3
  254. package/src/sdk/bus/attachment-registry.ts +5 -1
  255. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  256. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  257. package/src/sdk/bus/config.ts +18 -2
  258. package/src/sdk/bus/control-drain-lease.ts +104 -0
  259. package/src/sdk/bus/index.ts +984 -146
  260. package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
  261. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  262. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  263. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  264. package/src/sdk/bus/reconciliation-store.ts +303 -0
  265. package/src/sdk/bus/rich-draft.ts +1 -1
  266. package/src/sdk/bus/rich-render.ts +2 -2
  267. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  268. package/src/sdk/bus/telegram-cli.ts +33 -8
  269. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  270. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  271. package/src/sdk/bus/telegram-daemon.ts +916 -23
  272. package/src/sdk/bus/telegram-reference.ts +40 -3
  273. package/src/sdk/bus/threaded-render.ts +48 -3
  274. package/src/sdk/bus/topic-registry.ts +20 -0
  275. package/src/sdk/host/control/dispatch.ts +5 -1
  276. package/src/sdk/host/control/operations.ts +1 -1
  277. package/src/sdk/host/host.ts +23 -1
  278. package/src/sdk/host/query/handlers.ts +55 -0
  279. package/src/sdk/host/query/revision-store.ts +36 -2
  280. package/src/sdk/host/reverse-leases.ts +44 -7
  281. package/src/sdk/mcp/server.ts +46 -18
  282. package/src/sdk/prompt-status.ts +16 -3
  283. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  284. package/src/sdk/protocol/operation-registry.ts +7 -2
  285. package/src/sdk/session.ts +101 -10
  286. package/src/session/agent-session.ts +1286 -220
  287. package/src/session/artifacts.ts +22 -0
  288. package/src/session/auth-broker-config.ts +10 -3
  289. package/src/session/blob-store.ts +915 -24
  290. package/src/session/internal/managed-session-scope.ts +44 -6
  291. package/src/session/internal/managed-session-storage.ts +48 -6
  292. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  293. package/src/session/session-manager.ts +2364 -438
  294. package/src/session/session-storage.ts +9 -1
  295. package/src/session/streaming-output.ts +500 -111
  296. package/src/setup/credential-import.ts +75 -13
  297. package/src/setup/provider-onboarding.ts +19 -4
  298. package/src/skill-state/active-state.ts +32 -10
  299. package/src/skill-state/workflow-hud.ts +42 -2
  300. package/src/skill-state/workflow-mutation-guard.ts +560 -8
  301. package/src/task/executor.ts +122 -39
  302. package/src/task/index.ts +23 -14
  303. package/src/task/provider-retry-status.ts +16 -0
  304. package/src/task/receipt.ts +23 -7
  305. package/src/task/render.ts +6 -2
  306. package/src/task/types.ts +108 -0
  307. package/src/tools/ask-answer-registry.ts +12 -6
  308. package/src/tools/ask.ts +169 -118
  309. package/src/tools/ast-edit.ts +1 -0
  310. package/src/tools/ast-grep.ts +1 -0
  311. package/src/tools/bash-interactive.ts +8 -3
  312. package/src/tools/bash.ts +503 -60
  313. package/src/tools/browser/launch.ts +41 -6
  314. package/src/tools/fetch.ts +114 -23
  315. package/src/tools/find.ts +1 -0
  316. package/src/tools/image-gen.ts +29 -5
  317. package/src/tools/index.ts +4 -0
  318. package/src/tools/output-meta.ts +281 -20
  319. package/src/tools/path-utils.ts +8 -3
  320. package/src/tools/read-internals.ts +44 -0
  321. package/src/tools/read.ts +1560 -310
  322. package/src/tools/resource-gc.ts +182 -18
  323. package/src/tools/review.ts +5 -7
  324. package/src/tools/search.ts +1 -0
  325. package/src/tools/subagent-render.ts +6 -2
  326. package/src/tools/subagent.ts +6 -0
  327. package/src/tools/telegram-send.ts +6 -3
  328. package/src/tools/tool-result.ts +14 -1
  329. package/src/utils/gh.ts +39 -0
  330. package/src/utils/image-resize.ts +3 -0
  331. package/src/web/search/providers/anthropic.ts +4 -4
  332. package/src/web/search/providers/codex.ts +1 -1
  333. package/src/web/search/providers/kimi.ts +15 -5
  334. package/src/web/search/providers/searxng.ts +47 -4
  335. package/src/web/search/providers/xai.ts +7 -2
  336. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  337. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  338. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  339. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  340. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  341. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  342. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  343. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
  344. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -1,10 +1,11 @@
1
+ import * as crypto from "node:crypto";
1
2
  import * as fs from "node:fs";
2
- import type { ToolResultMessage } from "@gajae-code/ai";
3
+ import type { ToolCall, ToolResultMessage } from "@gajae-code/ai";
3
4
  import { matchesKey } from "@gajae-code/tui";
4
5
  import { formatDuration, formatNumber } from "@gajae-code/utils";
5
6
  import type { KeyId } from "../../config/keybindings";
6
7
  import { isSilentAbort } from "../../session/messages";
7
- import type { SessionMessageEntry } from "../../session/session-manager";
8
+ import type { FileEntry, SessionMessageEntry } from "../../session/session-manager";
8
9
  import { parseSessionEntries } from "../../session/session-manager";
9
10
  import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
10
11
  import { theme } from "../theme/theme";
@@ -15,13 +16,86 @@ import {
15
16
  } from "./tool-transcript-format";
16
17
  import { type TranscriptViewerEntry, TranscriptViewerOverlay } from "./transcript-viewer-overlay";
17
18
 
19
+ const FATAL_UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
20
+
21
+ type FileIdentity = { dev: bigint; ino: bigint };
22
+ type StableSnapshot = {
23
+ identity: FileIdentity;
24
+ size: number;
25
+ prefix: Buffer;
26
+ tail: Buffer;
27
+ offsetReset: boolean;
28
+ };
29
+ type SnapshotReadResult =
30
+ | { status: "stable"; snapshot: StableSnapshot }
31
+ | { status: "unstable" }
32
+ | { status: "unavailable" };
33
+
34
+ class UnstableSnapshotError extends Error {}
35
+ type ObserverCache = {
36
+ path: string;
37
+ identity: FileIdentity;
38
+ completeOffset: number;
39
+ prefixDigest: string;
40
+ entries: SessionMessageEntry[];
41
+ model?: string;
42
+ projectedCount: number;
43
+ projectedOutput: TranscriptViewerEntry[];
44
+ toolCallOutputIndex: Map<string, number>;
45
+ projectionSafe: boolean;
46
+ };
47
+
48
+ type ProjectionState = Pick<
49
+ ObserverCache,
50
+ "projectedCount" | "projectedOutput" | "toolCallOutputIndex" | "projectionSafe"
51
+ >;
52
+ type ProjectionUpdate = { state: ProjectionState; preserveLayoutCache: boolean };
53
+
54
+ export const __sessionObserverProjectionCounters = {
55
+ enabled: false,
56
+ projectionFullRuns: 0,
57
+ projectionIncrementalRuns: 0,
58
+ sourceMessagesProjected: 0,
59
+ projectedEntriesAppended: 0,
60
+ resultPatchCount: 0,
61
+ snapshot() {
62
+ return {
63
+ projectionFullRuns: this.projectionFullRuns,
64
+ projectionIncrementalRuns: this.projectionIncrementalRuns,
65
+ sourceMessagesProjected: this.sourceMessagesProjected,
66
+ projectedEntriesAppended: this.projectedEntriesAppended,
67
+ resultPatchCount: this.resultPatchCount,
68
+ };
69
+ },
70
+ enable(): void {
71
+ this.enabled = true;
72
+ },
73
+ disable(): void {
74
+ this.enabled = false;
75
+ },
76
+ reset(): void {
77
+ this.projectionFullRuns = 0;
78
+ this.projectionIncrementalRuns = 0;
79
+ this.sourceMessagesProjected = 0;
80
+ this.projectedEntriesAppended = 0;
81
+ this.resultPatchCount = 0;
82
+ },
83
+ };
84
+
85
+ function recordProjectionCounter(
86
+ counter: keyof ReturnType<typeof __sessionObserverProjectionCounters.snapshot>,
87
+ amount = 1,
88
+ ): void {
89
+ if (__sessionObserverProjectionCounters.enabled) __sessionObserverProjectionCounters[counter] += amount;
90
+ }
91
+
18
92
  /** Session-observer adapter. The shared viewer owns navigation and fold state. */
19
93
  export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
20
94
  #registry: SessionObserverRegistry;
21
95
  #onDone: () => void;
22
96
  #observeKeys: readonly KeyId[];
23
97
  #selectedSessionId?: string;
24
- #cache?: { path: string; bytesRead: number; entries: SessionMessageEntry[]; model?: string };
98
+ #cache?: ObserverCache;
25
99
 
26
100
  constructor(registry: SessionObserverRegistry, onDone: () => void, observeKeys: KeyId[]) {
27
101
  // The option closures run during the base constructor's initial refresh,
@@ -48,11 +122,21 @@ export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
48
122
  this.#observeKeys = observeKeys;
49
123
  this.#selectedSessionId = this.#mostRecent()?.id;
50
124
  if (!this.#selectedSessionId) queueMicrotask(onDone);
51
- this.refresh();
125
+ this.#refreshCurrentSource();
52
126
  }
53
127
 
54
128
  refreshFromRegistry(): void {
55
- this.refresh();
129
+ this.#refreshCurrentSource();
130
+ }
131
+ #refreshCurrentSource(): void {
132
+ const session = this.#registry.getSessions().find(candidate => candidate.id === this.#selectedSessionId);
133
+ if (!session?.sessionFile) {
134
+ this.#clearSource();
135
+ this.refresh(undefined, false);
136
+ return;
137
+ }
138
+ const preserveLayoutCache = this.#load(session.sessionFile);
139
+ this.refresh(undefined, preserveLayoutCache);
56
140
  }
57
141
  override handleInput(keyData: string): void {
58
142
  if (this.#observeKeys.some(key => matchesKey(keyData, key))) {
@@ -86,12 +170,10 @@ export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
86
170
  this.#selectedSessionId = ids[(current + direction + ids.length) % ids.length];
87
171
  this.#cache = undefined;
88
172
  this.resetSourceState();
89
- this.refresh();
173
+ this.#refreshCurrentSource();
90
174
  }
91
175
  #entries(): readonly TranscriptViewerEntry[] {
92
- const session = this.#registry.getSessions().find(candidate => candidate.id === this.#selectedSessionId);
93
- if (!session?.sessionFile) return [];
94
- return entriesFromMessages(this.#load(session.sessionFile));
176
+ return this.#cache?.projectedOutput ?? [];
95
177
  }
96
178
  #headerLines(): string[] {
97
179
  const session = this.#registry.getSessions().find(candidate => candidate.id === this.#selectedSessionId);
@@ -125,33 +207,246 @@ export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
125
207
  if (progress.cost > 0) stats.push(`$${progress.cost.toFixed(2)}`);
126
208
  return stats.length ? [theme.fg("dim", stats.join(theme.sep.dot))] : [];
127
209
  }
128
- #load(filePath: string): SessionMessageEntry[] {
129
- if (this.#cache?.path !== filePath) this.#cache = undefined;
130
- const fromByte = this.#cache?.bytesRead ?? 0;
131
- const result = readFileIncremental(filePath, fromByte);
132
- if (!result) return this.#cache?.entries ?? [];
133
- if (result.newSize < fromByte) {
210
+ #load(filePath: string): boolean {
211
+ if (this.#cache && this.#cache.path !== filePath) {
134
212
  this.#cache = undefined;
135
- return this.#load(filePath);
213
+ this.resetSourceState();
214
+ }
215
+
216
+ const cache = this.#cache;
217
+ const read = readStableSnapshot(filePath, cache?.completeOffset ?? 0);
218
+ if (read.status === "unstable") return cache !== undefined;
219
+ if (read.status === "unavailable") {
220
+ if (cache) this.#clearSource();
221
+ return false;
222
+ }
223
+ const snapshot = read.snapshot;
224
+ const canAppend =
225
+ cache &&
226
+ !snapshot.offsetReset &&
227
+ sameIdentity(cache.identity, snapshot.identity) &&
228
+ snapshot.size >= cache.completeOffset &&
229
+ digest(snapshot.prefix) === cache.prefixDigest;
230
+ if (canAppend) return this.#appendStable(cache, snapshot);
231
+
232
+ const candidate = cacheEntries(filePath, asFullSnapshot(snapshot));
233
+ if (!candidate) {
234
+ this.#clearSource();
235
+ return false;
236
+ }
237
+ this.#cache = candidate;
238
+ if (cache) this.resetSourceState();
239
+ return false;
240
+ }
241
+ #appendStable(cache: ObserverCache, snapshot: StableSnapshot): boolean {
242
+ const complete = completePrefix(snapshot.tail);
243
+ if (!complete) {
244
+ if (!isValidUtf8Prefix(snapshot.tail)) {
245
+ this.#clearSource();
246
+ return false;
247
+ }
248
+ return true;
249
+ }
250
+ const remainder = snapshot.tail.subarray(complete.bytes.length);
251
+ if (!isValidUtf8Prefix(remainder)) {
252
+ this.#clearSource();
253
+ return false;
136
254
  }
137
- if (!this.#cache) this.#cache = { path: filePath, bytesRead: 0, entries: [] };
138
- if (result.text) {
139
- const lastNewline = result.text.lastIndexOf("\n");
140
- if (lastNewline >= 0) {
141
- const complete = result.text.slice(0, lastNewline + 1);
142
- for (const entry of parseSessionEntries(complete)) {
143
- if (entry.type === "message") {
144
- this.#cache.entries.push(entry);
145
- if (!this.#cache.model && entry.message.role === "assistant") this.#cache.model = entry.message.model;
146
- } else if (entry.type === "model_change") this.#cache.model = entry.model;
255
+ const parsed = parseCompleteEntries(complete.bytes);
256
+ if (!parsed) {
257
+ this.#clearSource();
258
+ return false;
259
+ }
260
+ if (parsed.hasPatchRecords) {
261
+ const candidate = cacheEntries(cache.path, asFullSnapshot(snapshot));
262
+ if (!candidate) {
263
+ this.#clearSource();
264
+ return false;
265
+ }
266
+ this.#cache = candidate;
267
+ return false;
268
+ }
269
+
270
+ const entries = [...cache.entries, ...parsed.entries];
271
+ const projection = this.#appendProjection(cache, parsed.entries);
272
+ this.#cache = {
273
+ ...cache,
274
+ completeOffset: cache.completeOffset + complete.bytes.length,
275
+ prefixDigest: digest(Buffer.concat([snapshot.prefix, complete.bytes])),
276
+ entries,
277
+ model: parsed.model ?? cache.model,
278
+ ...projection.state,
279
+ };
280
+ return projection.preserveLayoutCache;
281
+ }
282
+ #appendProjection(cache: ObserverCache, suffix: readonly SessionMessageEntry[]): ProjectionUpdate {
283
+ if (suffix.length === 0) {
284
+ return {
285
+ state: projectionState(cache),
286
+ preserveLayoutCache: true,
287
+ };
288
+ }
289
+ if (!cache.projectionSafe || cache.projectedCount !== cache.entries.length) {
290
+ return {
291
+ state: fullProjection([...cache.entries, ...suffix]),
292
+ preserveLayoutCache: false,
293
+ };
294
+ }
295
+
296
+ const projectedOutput = [...cache.projectedOutput];
297
+ const toolCallOutputIndex = new Map(cache.toolCallOutputIndex);
298
+ const patchedIds: string[] = [];
299
+ let appendedCount = 0;
300
+ let patchCount = 0;
301
+ for (const entry of suffix) {
302
+ const message = entry.message;
303
+ if (message.role === "toolResult") {
304
+ const index = toolCallOutputIndex.get(message.toolCallId);
305
+ const existing = index === undefined ? undefined : projectedOutput[index];
306
+ const source = existing?.payload.source;
307
+ const call = isToolProjectionSource(source) ? source.call : undefined;
308
+ if (index === undefined || !call) {
309
+ return {
310
+ state: fullProjection([...cache.entries, ...suffix]),
311
+ preserveLayoutCache: false,
312
+ };
313
+ }
314
+ projectedOutput[index] = toolEntry(call, message);
315
+ patchedIds.push(projectedOutput[index].id);
316
+ patchCount++;
317
+ continue;
318
+ }
319
+
320
+ const appended = entriesFromMessages([entry]);
321
+ for (const outputEntry of appended) {
322
+ if (outputEntry.id.startsWith("tool:")) {
323
+ const callId = outputEntry.id.slice("tool:".length);
324
+ if (toolCallOutputIndex.has(callId)) {
325
+ return {
326
+ state: fullProjection([...cache.entries, ...suffix]),
327
+ preserveLayoutCache: false,
328
+ };
329
+ }
330
+ toolCallOutputIndex.set(callId, projectedOutput.length);
147
331
  }
148
- this.#cache.bytesRead = fromByte + Buffer.byteLength(complete, "utf-8");
332
+ projectedOutput.push(outputEntry);
149
333
  }
334
+ appendedCount += appended.length;
150
335
  }
151
- return this.#cache.entries;
336
+ if (patchedIds.length > 0) this.invalidateLayoutEntries(patchedIds);
337
+ recordProjectionCounter("projectionIncrementalRuns");
338
+ recordProjectionCounter("sourceMessagesProjected", suffix.length);
339
+ recordProjectionCounter("projectedEntriesAppended", appendedCount);
340
+ recordProjectionCounter("resultPatchCount", patchCount);
341
+ return {
342
+ state: {
343
+ projectedCount: cache.projectedCount + suffix.length,
344
+ projectedOutput,
345
+ toolCallOutputIndex,
346
+ projectionSafe: true,
347
+ },
348
+ preserveLayoutCache: true,
349
+ };
152
350
  }
351
+ #clearSource(): void {
352
+ if (this.#cache) this.resetSourceState();
353
+ this.#cache = undefined;
354
+ }
355
+ }
356
+
357
+ type ToolProjectionSource = { call: ToolCall; result?: ToolResultMessage };
358
+
359
+ function isToolProjectionSource(source: unknown): source is ToolProjectionSource {
360
+ if (!source || typeof source !== "object" || !("call" in source)) return false;
361
+ const call = source.call;
362
+ return Boolean(call && typeof call === "object" && "type" in call && call.type === "toolCall");
153
363
  }
154
364
 
365
+ function projectionState(cache: ObserverCache): ProjectionState {
366
+ return {
367
+ projectedCount: cache.projectedCount,
368
+ projectedOutput: cache.projectedOutput,
369
+ toolCallOutputIndex: cache.toolCallOutputIndex,
370
+ projectionSafe: cache.projectionSafe,
371
+ };
372
+ }
373
+
374
+ function fullProjection(
375
+ entries: readonly SessionMessageEntry[],
376
+ ): Pick<ObserverCache, "projectedCount" | "projectedOutput" | "toolCallOutputIndex" | "projectionSafe"> {
377
+ const projectedOutput = entriesFromMessages(entries);
378
+ const toolCallOutputIndex = new Map<string, number>();
379
+ let projectionSafe = true;
380
+ const knownToolCallIds = new Set<string>();
381
+ const unresolvedToolResultIds = new Set<string>();
382
+ for (const entry of entries) {
383
+ const message = entry.message;
384
+ if (message.role === "assistant") {
385
+ for (const content of message.content) if (content.type === "toolCall") knownToolCallIds.add(content.id);
386
+ } else if (message.role === "toolResult") unresolvedToolResultIds.add(message.toolCallId);
387
+ }
388
+ for (const callId of knownToolCallIds) unresolvedToolResultIds.delete(callId);
389
+ if (unresolvedToolResultIds.size > 0) projectionSafe = false;
390
+ for (let index = 0; index < projectedOutput.length; index++) {
391
+ const id = projectedOutput[index].id;
392
+ if (!id.startsWith("tool:")) continue;
393
+ const callId = id.slice("tool:".length);
394
+ if (toolCallOutputIndex.has(callId)) projectionSafe = false;
395
+ else toolCallOutputIndex.set(callId, index);
396
+ }
397
+ recordProjectionCounter("projectionFullRuns");
398
+ recordProjectionCounter("sourceMessagesProjected", entries.length);
399
+ return { projectedCount: entries.length, projectedOutput, toolCallOutputIndex, projectionSafe };
400
+ }
401
+
402
+ function toolEntry(call: ToolCall, result?: ToolResultMessage): TranscriptViewerEntry {
403
+ const resultText =
404
+ result?.content
405
+ .filter(part => part.type === "text")
406
+ .map(part => part.text)
407
+ .join("\n")
408
+ .trim() ?? "";
409
+ const hasResult = result !== undefined;
410
+ const canonicalPayload = {
411
+ text: composeToolText({
412
+ name: call.name,
413
+ args: call.arguments,
414
+ intent: call.intent,
415
+ resultText,
416
+ isError: result?.isError ?? false,
417
+ hasResult,
418
+ }),
419
+ metadata: {
420
+ name: call.name,
421
+ arguments: call.arguments,
422
+ intent: call.intent,
423
+ resultText,
424
+ isError: result?.isError ?? false,
425
+ hasResult,
426
+ detailsData: result?.details,
427
+ },
428
+ source: { call, result },
429
+ };
430
+ return buildToolTranscriptEntry({
431
+ canonicalPayload,
432
+ renderDescriptor: createToolTranscriptRenderDescriptor({
433
+ name: call.name,
434
+ args: call.arguments,
435
+ intent: call.intent,
436
+ resultContent: resultText,
437
+ isError: result?.isError,
438
+ hasResult,
439
+ detailsData: result?.details,
440
+ }),
441
+ capabilities: { copyable: true, foldable: true, rawViewable: true },
442
+ identity: { id: `tool:${call.id}`, label: call.name, display: "full" },
443
+ });
444
+ }
445
+
446
+ /**
447
+ * This deliberately remains the eager full-history projection. PR2 owns projection
448
+ * virtualization/incrementalization; source acquisition above only controls snapshot safety.
449
+ */
155
450
  export function entriesFromMessages(entries: readonly SessionMessageEntry[]): TranscriptViewerEntry[] {
156
451
  const results = new Map<string, ToolResultMessage>();
157
452
  for (const entry of entries)
@@ -186,51 +481,7 @@ export function entriesFromMessages(entries: readonly SessionMessageEntry[]): Tr
186
481
  payload: { text: content.text, metadata: {}, source: content },
187
482
  foldable: true,
188
483
  });
189
- if (content.type === "toolCall") {
190
- const result = results.get(content.id);
191
- const resultText =
192
- result?.content
193
- .filter(part => part.type === "text")
194
- .map(part => part.text)
195
- .join("\n")
196
- .trim() ?? "";
197
- const canonicalPayload = {
198
- text: composeToolText({
199
- name: content.name,
200
- args: content.arguments,
201
- intent: content.intent,
202
- resultText,
203
- isError: result?.isError ?? false,
204
- hasResult: results.has(content.id),
205
- }),
206
- metadata: {
207
- name: content.name,
208
- arguments: content.arguments,
209
- intent: content.intent,
210
- resultText,
211
- isError: result?.isError ?? false,
212
- hasResult: results.has(content.id),
213
- detailsData: result?.details,
214
- },
215
- source: { call: content, result },
216
- };
217
- output.push(
218
- buildToolTranscriptEntry({
219
- canonicalPayload,
220
- renderDescriptor: createToolTranscriptRenderDescriptor({
221
- name: content.name,
222
- args: content.arguments,
223
- intent: content.intent,
224
- resultContent: resultText,
225
- isError: result?.isError,
226
- hasResult: results.has(content.id),
227
- detailsData: result?.details,
228
- }),
229
- capabilities: { copyable: true, foldable: true, rawViewable: true },
230
- identity: { id: `tool:${content.id}`, label: content.name, display: "full" },
231
- }),
232
- );
233
- }
484
+ if (content.type === "toolCall") output.push(toolEntry(content, results.get(content.id)));
234
485
  });
235
486
  }
236
487
  if (message.role === "user" || message.role === "developer") {
@@ -259,19 +510,142 @@ function truncateThinking(text: string, expanded: boolean): string {
259
510
  return text.length > limit ? `${text.slice(0, limit)}...` : text;
260
511
  }
261
512
 
262
- function readFileIncremental(filePath: string, fromByte: number): { text: string; newSize: number } | null {
513
+ function readStableSnapshot(filePath: string, completeOffset: number): SnapshotReadResult {
514
+ let fd: number | undefined;
263
515
  try {
264
- const stat = fs.statSync(filePath);
265
- if (stat.size <= fromByte) return { text: "", newSize: stat.size };
266
- const buffer = Buffer.alloc(stat.size - fromByte);
267
- const fd = fs.openSync(filePath, "r");
268
- try {
269
- fs.readSync(fd, buffer, 0, buffer.length, fromByte);
270
- } finally {
271
- fs.closeSync(fd);
516
+ fd = fs.openSync(filePath, "r");
517
+ const before = fs.fstatSync(fd, { bigint: true });
518
+ if (before.size > BigInt(Number.MAX_SAFE_INTEGER)) return { status: "unavailable" };
519
+ const size = Number(before.size);
520
+ const offsetReset = completeOffset > size;
521
+ const offset = offsetReset ? 0 : completeOffset;
522
+ const prefix = readExactly(fd, 0, offset);
523
+ const tail = readExactly(fd, offset, size - offset);
524
+ const after = fs.fstatSync(fd, { bigint: true });
525
+ if (
526
+ before.dev !== after.dev ||
527
+ before.ino !== after.ino ||
528
+ before.size !== after.size ||
529
+ before.mtimeNs !== after.mtimeNs ||
530
+ before.ctimeNs !== after.ctimeNs
531
+ )
532
+ return { status: "unstable" };
533
+ return {
534
+ status: "stable",
535
+ snapshot: { identity: { dev: before.dev, ino: before.ino }, size, prefix, tail, offsetReset },
536
+ };
537
+ } catch (err) {
538
+ return err instanceof UnstableSnapshotError ? { status: "unstable" } : { status: "unavailable" };
539
+ } finally {
540
+ if (fd !== undefined) fs.closeSync(fd);
541
+ }
542
+ }
543
+
544
+ function asFullSnapshot(snapshot: StableSnapshot): StableSnapshot {
545
+ if (snapshot.prefix.length === 0) return snapshot;
546
+ return {
547
+ ...snapshot,
548
+ prefix: Buffer.alloc(0),
549
+ tail: Buffer.concat([snapshot.prefix, snapshot.tail]),
550
+ offsetReset: true,
551
+ };
552
+ }
553
+
554
+ function readExactly(fd: number, position: number, length: number): Buffer {
555
+ const buffer = Buffer.alloc(length);
556
+ let offset = 0;
557
+ while (offset < length) {
558
+ const bytesRead = fs.readSync(fd, buffer, offset, length - offset, position + offset);
559
+ if (bytesRead === 0) throw new UnstableSnapshotError("Short session file read");
560
+ offset += bytesRead;
561
+ }
562
+ return buffer;
563
+ }
564
+
565
+ function sameIdentity(left: FileIdentity, right: FileIdentity): boolean {
566
+ return left.dev === right.dev && left.ino === right.ino;
567
+ }
568
+
569
+ function digest(bytes: Buffer): string {
570
+ return crypto.createHash("sha256").update(bytes).digest("hex");
571
+ }
572
+
573
+ function completePrefix(bytes: Buffer): { bytes: Buffer } | undefined {
574
+ const newline = bytes.lastIndexOf(0x0a);
575
+ return newline < 0 ? undefined : { bytes: bytes.subarray(0, newline + 1) };
576
+ }
577
+
578
+ function parseCompleteEntries(
579
+ bytes: Buffer,
580
+ ): { entries: SessionMessageEntry[]; model?: string; hasPatchRecords: boolean } | null {
581
+ try {
582
+ const text = FATAL_UTF8_DECODER.decode(bytes);
583
+ let hasPatchRecords = false;
584
+ for (const line of text.split("\n")) {
585
+ if (!line.trim()) continue;
586
+ const record: unknown = JSON.parse(line);
587
+ if (
588
+ record !== null &&
589
+ typeof record === "object" &&
590
+ "type" in record &&
591
+ (record.type === "entry_patch" || record.type === "header_patch")
592
+ )
593
+ hasPatchRecords = true;
272
594
  }
273
- return { text: buffer.toString("utf-8"), newSize: stat.size };
595
+ const parsed = parseSessionEntries(text);
596
+ return {
597
+ entries: parsed.filter((entry): entry is SessionMessageEntry => entry.type === "message"),
598
+ model: modelFromEntries(parsed),
599
+ hasPatchRecords,
600
+ };
274
601
  } catch {
275
602
  return null;
276
603
  }
277
604
  }
605
+
606
+ function modelFromEntries(entries: readonly FileEntry[]): string | undefined {
607
+ let model: string | undefined;
608
+ for (const entry of entries) {
609
+ if (entry.type === "model_change") model = entry.model;
610
+ else if (entry.type === "message" && entry.message.role === "assistant" && entry.message.model)
611
+ model = entry.message.model;
612
+ }
613
+ return model;
614
+ }
615
+
616
+ function isValidUtf8Prefix(bytes: Buffer): boolean {
617
+ try {
618
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: true });
619
+ return true;
620
+ } catch {
621
+ return false;
622
+ }
623
+ }
624
+
625
+ function cacheEntries(filePath: string, snapshot: StableSnapshot): ObserverCache | null {
626
+ const complete = completePrefix(snapshot.tail);
627
+ if (!complete) {
628
+ if (!isValidUtf8Prefix(snapshot.tail)) return null;
629
+ return {
630
+ path: filePath,
631
+ identity: snapshot.identity,
632
+ completeOffset: 0,
633
+ prefixDigest: digest(Buffer.alloc(0)),
634
+ entries: [],
635
+ ...fullProjection([]),
636
+ };
637
+ }
638
+ const remainder = snapshot.tail.subarray(complete.bytes.length);
639
+ if (!isValidUtf8Prefix(remainder)) return null;
640
+ const parsed = parseCompleteEntries(complete.bytes);
641
+ if (!parsed) return null;
642
+ return {
643
+ path: filePath,
644
+ identity: snapshot.identity,
645
+ completeOffset: complete.bytes.length,
646
+ prefixDigest: digest(complete.bytes),
647
+ entries: parsed.entries,
648
+ model: parsed.model,
649
+ ...fullProjection(parsed.entries),
650
+ };
651
+ }