@songsid/agend 2.2.0-alpha.1 → 2.2.0-alpha.3

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 (317) hide show
  1. package/README.md +6 -4
  2. package/README.zh-TW.md +6 -4
  3. package/dist/agent-cli.js +6 -1
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +4 -1
  6. package/dist/agent-endpoint.js +82 -14
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-token-header.d.ts +13 -0
  9. package/dist/agent-token-header.js +16 -0
  10. package/dist/agent-token-header.js.map +1 -0
  11. package/dist/atomic-write.d.ts +21 -0
  12. package/dist/atomic-write.js +65 -0
  13. package/dist/atomic-write.js.map +1 -0
  14. package/dist/auth-api.js +2 -0
  15. package/dist/auth-api.js.map +1 -1
  16. package/dist/backend/antigravity.js +4 -3
  17. package/dist/backend/antigravity.js.map +1 -1
  18. package/dist/backend/codex.d.ts +7 -0
  19. package/dist/backend/codex.js +7 -0
  20. package/dist/backend/codex.js.map +1 -1
  21. package/dist/backend/grok.js +1 -1
  22. package/dist/backend/grok.js.map +1 -1
  23. package/dist/backend/kiro.d.ts +7 -0
  24. package/dist/backend/kiro.js +82 -33
  25. package/dist/backend/kiro.js.map +1 -1
  26. package/dist/backend/muse.d.ts +7 -0
  27. package/dist/backend/muse.js +8 -1
  28. package/dist/backend/muse.js.map +1 -1
  29. package/dist/backend/types.d.ts +12 -0
  30. package/dist/backend/types.js.map +1 -1
  31. package/dist/binary-probe.d.ts +80 -0
  32. package/dist/binary-probe.js +182 -0
  33. package/dist/binary-probe.js.map +1 -0
  34. package/dist/cache-analysis.d.ts +92 -0
  35. package/dist/cache-analysis.js +147 -0
  36. package/dist/cache-analysis.js.map +1 -0
  37. package/dist/cache-ledger.d.ts +108 -0
  38. package/dist/cache-ledger.js +230 -0
  39. package/dist/cache-ledger.js.map +1 -0
  40. package/dist/cache-prices.d.ts +32 -0
  41. package/dist/cache-prices.js +73 -0
  42. package/dist/cache-prices.js.map +1 -0
  43. package/dist/cache-scan.d.ts +39 -0
  44. package/dist/cache-scan.js +115 -0
  45. package/dist/cache-scan.js.map +1 -0
  46. package/dist/cache-service.d.ts +131 -0
  47. package/dist/cache-service.js +452 -0
  48. package/dist/cache-service.js.map +1 -0
  49. package/dist/channel/adapters/discord.d.ts +5 -1
  50. package/dist/channel/adapters/discord.js +56 -11
  51. package/dist/channel/adapters/discord.js.map +1 -1
  52. package/dist/channel/adapters/telegram.d.ts +38 -1
  53. package/dist/channel/adapters/telegram.js +82 -0
  54. package/dist/channel/adapters/telegram.js.map +1 -1
  55. package/dist/channel/mcp-tools.js +1 -1
  56. package/dist/channel/mcp-tools.js.map +1 -1
  57. package/dist/channel/tool-router.d.ts +8 -1
  58. package/dist/channel/tool-router.js +4 -2
  59. package/dist/channel/tool-router.js.map +1 -1
  60. package/dist/channel/types.d.ts +21 -0
  61. package/dist/cli-entry.d.ts +19 -0
  62. package/dist/cli-entry.js +36 -0
  63. package/dist/cli-entry.js.map +1 -0
  64. package/dist/cli.js +376 -54
  65. package/dist/cli.js.map +1 -1
  66. package/dist/connection-secrets.d.ts +5 -0
  67. package/dist/connection-secrets.js.map +1 -1
  68. package/dist/cpu-profile.js +50 -0
  69. package/dist/cpu-profile.js.map +1 -1
  70. package/dist/daemon.d.ts +18 -0
  71. package/dist/daemon.js +256 -63
  72. package/dist/daemon.js.map +1 -1
  73. package/dist/delivery-outbox.d.ts +1 -0
  74. package/dist/delivery-outbox.js +12 -0
  75. package/dist/delivery-outbox.js.map +1 -1
  76. package/dist/discord-permissions.d.ts +33 -0
  77. package/dist/discord-permissions.js +42 -0
  78. package/dist/discord-permissions.js.map +1 -0
  79. package/dist/env-file.d.ts +11 -0
  80. package/dist/env-file.js +27 -0
  81. package/dist/env-file.js.map +1 -0
  82. package/dist/event-log.d.ts +4 -1
  83. package/dist/event-log.js +6 -3
  84. package/dist/event-log.js.map +1 -1
  85. package/dist/fleet-context.d.ts +5 -0
  86. package/dist/fleet-context.js.map +1 -1
  87. package/dist/fleet-control-audit.d.ts +5 -0
  88. package/dist/fleet-control-audit.js +7 -0
  89. package/dist/fleet-control-audit.js.map +1 -1
  90. package/dist/fleet-level-config.d.ts +8 -1
  91. package/dist/fleet-level-config.js +53 -0
  92. package/dist/fleet-level-config.js.map +1 -1
  93. package/dist/fleet-manager.d.ts +122 -7
  94. package/dist/fleet-manager.js +871 -152
  95. package/dist/fleet-manager.js.map +1 -1
  96. package/dist/fleet-stop-budget.d.ts +22 -0
  97. package/dist/fleet-stop-budget.js +95 -0
  98. package/dist/fleet-stop-budget.js.map +1 -0
  99. package/dist/full-restart.js +2 -3
  100. package/dist/full-restart.js.map +1 -1
  101. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
  102. package/dist/install-lock.d.ts +49 -0
  103. package/dist/install-lock.js +200 -0
  104. package/dist/install-lock.js.map +1 -0
  105. package/dist/instance-config-impact.js +3 -2
  106. package/dist/instance-config-impact.js.map +1 -1
  107. package/dist/kiro-transcript-lane.d.ts +8 -4
  108. package/dist/kiro-transcript-lane.js +66 -44
  109. package/dist/kiro-transcript-lane.js.map +1 -1
  110. package/dist/locale.js +96 -0
  111. package/dist/locale.js.map +1 -1
  112. package/dist/next-steps.d.ts +5 -0
  113. package/dist/next-steps.js +26 -0
  114. package/dist/next-steps.js.map +1 -0
  115. package/dist/outbound-handlers.js +8 -0
  116. package/dist/outbound-handlers.js.map +1 -1
  117. package/dist/outbound-schemas.d.ts +4 -0
  118. package/dist/outbound-schemas.js +7 -2
  119. package/dist/outbound-schemas.js.map +1 -1
  120. package/dist/outbox-open-error.d.ts +7 -0
  121. package/dist/outbox-open-error.js +15 -0
  122. package/dist/outbox-open-error.js.map +1 -0
  123. package/dist/package-preimage.d.ts +26 -0
  124. package/dist/package-preimage.js +96 -0
  125. package/dist/package-preimage.js.map +1 -0
  126. package/dist/pane-context-cache.d.ts +25 -0
  127. package/dist/pane-context-cache.js +73 -0
  128. package/dist/pane-context-cache.js.map +1 -0
  129. package/dist/paths.d.ts +0 -7
  130. package/dist/paths.js +8 -2
  131. package/dist/paths.js.map +1 -1
  132. package/dist/provider-probe.d.ts +15 -0
  133. package/dist/provider-probe.js +20 -0
  134. package/dist/provider-probe.js.map +1 -1
  135. package/dist/public-link-progress.d.ts +73 -0
  136. package/dist/public-link-progress.js +190 -0
  137. package/dist/public-link-progress.js.map +1 -0
  138. package/dist/public-web-gateway.js +2 -2
  139. package/dist/public-web-gateway.js.map +1 -1
  140. package/dist/public-web-link.d.ts +6 -1
  141. package/dist/public-web-link.js +87 -15
  142. package/dist/public-web-link.js.map +1 -1
  143. package/dist/quickstart-api.d.ts +93 -20
  144. package/dist/quickstart-api.js +194 -62
  145. package/dist/quickstart-api.js.map +1 -1
  146. package/dist/quickstart.d.ts +15 -0
  147. package/dist/quickstart.js +110 -33
  148. package/dist/quickstart.js.map +1 -1
  149. package/dist/reply-buttons-controller.d.ts +98 -0
  150. package/dist/reply-buttons-controller.js +174 -0
  151. package/dist/reply-buttons-controller.js.map +1 -0
  152. package/dist/reply-buttons.d.ts +113 -0
  153. package/dist/reply-buttons.js +208 -0
  154. package/dist/reply-buttons.js.map +1 -0
  155. package/dist/reply-dedup.js +3 -1
  156. package/dist/reply-dedup.js.map +1 -1
  157. package/dist/restart-guard.d.ts +53 -0
  158. package/dist/restart-guard.js +114 -0
  159. package/dist/restart-guard.js.map +1 -0
  160. package/dist/scheduler/db.js +6 -2
  161. package/dist/scheduler/db.js.map +1 -1
  162. package/dist/service-activation.d.ts +62 -7
  163. package/dist/service-activation.js +237 -55
  164. package/dist/service-activation.js.map +1 -1
  165. package/dist/service-installer.d.ts +56 -1
  166. package/dist/service-installer.js +152 -12
  167. package/dist/service-installer.js.map +1 -1
  168. package/dist/service-plan.d.ts +65 -0
  169. package/dist/service-plan.js +108 -0
  170. package/dist/service-plan.js.map +1 -0
  171. package/dist/settings-api.js +92 -10
  172. package/dist/settings-api.js.map +1 -1
  173. package/dist/settings-authority.d.ts +11 -0
  174. package/dist/settings-authority.js +128 -0
  175. package/dist/settings-authority.js.map +1 -0
  176. package/dist/settings-change.d.ts +5 -0
  177. package/dist/settings-change.js +37 -8
  178. package/dist/settings-change.js.map +1 -1
  179. package/dist/settings-confirmation.d.ts +3 -0
  180. package/dist/settings-confirmation.js +7 -1
  181. package/dist/settings-confirmation.js.map +1 -1
  182. package/dist/settings-effect.js +19 -7
  183. package/dist/settings-effect.js.map +1 -1
  184. package/dist/settings-http-confirmation.js +1 -1
  185. package/dist/settings-http-confirmation.js.map +1 -1
  186. package/dist/settings-redaction.d.ts +18 -0
  187. package/dist/settings-redaction.js +58 -0
  188. package/dist/settings-redaction.js.map +1 -0
  189. package/dist/setup-form.d.ts +9 -1
  190. package/dist/setup-form.js +25 -1
  191. package/dist/setup-form.js.map +1 -1
  192. package/dist/setup-host.js +15 -6
  193. package/dist/setup-host.js.map +1 -1
  194. package/dist/setup-wizard.js +27 -19
  195. package/dist/setup-wizard.js.map +1 -1
  196. package/dist/sqlite-open-errors.d.ts +12 -0
  197. package/dist/sqlite-open-errors.js +13 -0
  198. package/dist/sqlite-open-errors.js.map +1 -0
  199. package/dist/systemd-runtime.d.ts +15 -0
  200. package/dist/systemd-runtime.js +41 -0
  201. package/dist/systemd-runtime.js.map +1 -0
  202. package/dist/tmux-control.d.ts +44 -0
  203. package/dist/tmux-control.js +170 -21
  204. package/dist/tmux-control.js.map +1 -1
  205. package/dist/token-env-name.d.ts +37 -0
  206. package/dist/token-env-name.js +93 -0
  207. package/dist/token-env-name.js.map +1 -0
  208. package/dist/tool-permissions.js +5 -3
  209. package/dist/tool-permissions.js.map +1 -1
  210. package/dist/topic-commands.d.ts +7 -16
  211. package/dist/topic-commands.js +56 -85
  212. package/dist/topic-commands.js.map +1 -1
  213. package/dist/transcript-monitor.js +3 -0
  214. package/dist/transcript-monitor.js.map +1 -1
  215. package/dist/transcript-sources.d.ts +68 -0
  216. package/dist/transcript-sources.js +336 -29
  217. package/dist/transcript-sources.js.map +1 -1
  218. package/dist/transcript-turns.d.ts +39 -0
  219. package/dist/transcript-turns.js +58 -0
  220. package/dist/transcript-turns.js.map +1 -0
  221. package/dist/tunnel/cloudflared-install.d.ts +18 -0
  222. package/dist/tunnel/cloudflared-install.js +15 -1
  223. package/dist/tunnel/cloudflared-install.js.map +1 -1
  224. package/dist/turn-reply-guard.d.ts +3 -0
  225. package/dist/turn-reply-guard.js.map +1 -1
  226. package/dist/ui/chat-commands.js +43 -0
  227. package/dist/ui/chat-render.js +34 -8
  228. package/dist/ui/chat-store.js +51 -2
  229. package/dist/ui/chat-strings.js +30 -2
  230. package/dist/ui/chat-thread.js +141 -47
  231. package/dist/ui/first-run.js +64 -0
  232. package/dist/ui/fleet-cache.js +168 -0
  233. package/dist/ui/html-attachment.js +26 -0
  234. package/dist/ui/panel-chat.js +117 -23
  235. package/dist/ui/panel-fleet.js +31 -17
  236. package/dist/ui/panel-settings.js +29 -15
  237. package/dist/ui/preview-panel.js +237 -0
  238. package/dist/ui/preview.js +7 -3
  239. package/dist/ui/settings-apply.js +3 -1
  240. package/dist/ui/settings-dialogs.js +95 -25
  241. package/dist/ui/settings-discord.js +130 -0
  242. package/dist/ui/settings-model.js +21 -0
  243. package/dist/ui/settings-strings.js +50 -8
  244. package/dist/ui/settings-token.js +54 -0
  245. package/dist/ui/settings-wizard.js +63 -29
  246. package/dist/ui/shared/app-route.js +1 -1
  247. package/dist/ui/shared/app-shell.js +25 -37
  248. package/dist/ui/shared/app-store.js +5 -3
  249. package/dist/ui/shared/app-stream.js +2 -1
  250. package/dist/ui/shared/app.css +181 -5
  251. package/dist/ui/shared/app.js +2 -1
  252. package/dist/ui/shared/instance-nav.js +310 -0
  253. package/dist/ui/shared/panel-view.js +12 -190
  254. package/dist/ui/shared/signin.js +13 -1
  255. package/dist/ui/shared/ui-icons.js +2 -0
  256. package/dist/ui/shared/view-roster-store.js +5 -0
  257. package/dist/ui/shared/view-strings.js +12 -0
  258. package/dist/ui/signin.html +2 -1
  259. package/dist/update-dispatch.d.ts +28 -0
  260. package/dist/update-dispatch.js +73 -0
  261. package/dist/update-dispatch.js.map +1 -0
  262. package/dist/update-install.d.ts +78 -8
  263. package/dist/update-install.js +158 -30
  264. package/dist/update-install.js.map +1 -1
  265. package/dist/update-launch.d.ts +22 -0
  266. package/dist/update-launch.js +106 -0
  267. package/dist/update-launch.js.map +1 -0
  268. package/dist/update-marker.js +3 -8
  269. package/dist/update-marker.js.map +1 -1
  270. package/dist/usage/format-rich.js +7 -1
  271. package/dist/usage/format-rich.js.map +1 -1
  272. package/dist/usage/i18n-keys.d.ts +1 -1
  273. package/dist/usage/i18n-keys.js +1 -1
  274. package/dist/usage/i18n-keys.js.map +1 -1
  275. package/dist/usage/providers.d.ts +4 -0
  276. package/dist/usage/providers.js +2 -1
  277. package/dist/usage/providers.js.map +1 -1
  278. package/dist/usage/usage-api.js +48 -2
  279. package/dist/usage/usage-api.js.map +1 -1
  280. package/dist/view-api.d.ts +40 -0
  281. package/dist/view-api.js +28 -10
  282. package/dist/view-api.js.map +1 -1
  283. package/dist/web-api.d.ts +22 -0
  284. package/dist/web-api.js +115 -16
  285. package/dist/web-api.js.map +1 -1
  286. package/dist/web-auth.d.ts +8 -0
  287. package/dist/web-auth.js +34 -8
  288. package/dist/web-auth.js.map +1 -1
  289. package/dist/web-chat-history.d.ts +21 -0
  290. package/dist/web-chat-history.js +44 -0
  291. package/dist/web-chat-history.js.map +1 -1
  292. package/dist/web-commands.d.ts +77 -0
  293. package/dist/web-commands.js +81 -0
  294. package/dist/web-commands.js.map +1 -0
  295. package/dist/web-host-guard.d.ts +18 -2
  296. package/dist/web-host-guard.js +30 -5
  297. package/dist/web-host-guard.js.map +1 -1
  298. package/dist/web-session.d.ts +7 -1
  299. package/dist/web-session.js +23 -2
  300. package/dist/web-session.js.map +1 -1
  301. package/dist/web-shell-routes.d.ts +1 -1
  302. package/dist/web-shell-routes.js +1 -1
  303. package/dist/web-shell-routes.js.map +1 -1
  304. package/launcher/agend +110 -0
  305. package/launcher/agend-agent +110 -0
  306. package/launcher/agend-agent.cjs +4 -0
  307. package/launcher/agend.cjs +4 -0
  308. package/launcher/install-admission.cjs +105 -0
  309. package/launcher/launch.cjs +64 -0
  310. package/launcher/postinstall.cjs +118 -0
  311. package/launcher/run-in-process.cjs +13 -0
  312. package/launcher/runtime-platform.cjs +86 -0
  313. package/launcher/runtime-select.cjs +217 -0
  314. package/package.json +12 -4
  315. package/scripts/preinstall-guard.cjs +40 -21
  316. package/templates/launchd.plist.ejs +2 -2
  317. package/templates/systemd.service.ejs +4 -4
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Whether a backend's executable is on PATH, without blocking the fleet (#1490).
3
+ *
4
+ * `/ui/backends` used to run `execFileSync("which", …)` seven times per request, each with a 2 s timeout, on the fleet's
5
+ * event loop: up to ~14 s of blocking per request on a slow or broken PATH. The same `which` now runs as an
6
+ * asynchronous child process. Not an in-process fs walk: on a dead network mount `access`/`stat` can stay pending
7
+ * forever, each one holding a thread of the libuv pool the whole process shares (fs, DNS, crypto), and nothing can
8
+ * cancel it (#1498 review). A child is outside that pool and can be killed.
9
+ *
10
+ * Every probe answers within PROBE_DEADLINE_MS: the path, null (not on PATH), or unknown (no answer in time, `which`
11
+ * could not run, or no probe could be admitted). A child counts as running until it has actually exited, not until
12
+ * its deadline: at most one per binary and PROBE_MAX_RUNNING in all, so a PATH that hangs every lookup costs a fixed
13
+ * number of stuck processes, never one per request.
14
+ */
15
+ export type ProbeResult = {
16
+ known: true;
17
+ path: string | null;
18
+ } | {
19
+ known: false;
20
+ };
21
+ /** A running probe: its answer (path or null), a kill switch, and proof that the process is gone. */
22
+ export interface ProbeChild {
23
+ answer: Promise<string | null>;
24
+ kill(): void;
25
+ exited: Promise<void>;
26
+ }
27
+ /** How long a known answer is reused: long enough that a polled route rarely probes, short enough to see installs. */
28
+ export declare const BINARY_PROBE_TTL_MS = 30000;
29
+ /** The old synchronous lookup's timeout, now a deadline on the answer instead of a block on the event loop. */
30
+ export declare const PROBE_DEADLINE_MS = 2000;
31
+ /** Probe processes that may exist at once, stuck ones included (the backend catalog has seven binaries). */
32
+ export declare const PROBE_MAX_RUNNING = 8;
33
+ /** `which <binary>` as a child process. */
34
+ export declare function spawnWhich(binary: string): ProbeChild;
35
+ /**
36
+ * One probe process: exit 0 with output → the first line, any other exit → null, failing to start → rejection.
37
+ * `exited` settles on `close` (or a spawn that never produced a process). Exported so tests can run one that hangs.
38
+ */
39
+ export declare function spawnProbeCommand(command: string, args: string[]): ProbeChild;
40
+ /**
41
+ * Probe answers per binary, reused for BINARY_PROBE_TTL_MS (monotonic clock); concurrent probes of one binary share
42
+ * one answer. `fresh: true` wants a lookup that starts after it asked (an install just ran): it skips the cached and
43
+ * the shared answer and accepts only a child spawned after its request, waiting (within its own deadline) for an older
44
+ * child of that binary to exit first. Callers that waited together share the next child. An answer is accepted only
45
+ * while the caller's deadline holds; the newest caller of a binary stores it, `invalidate()` discards what running
46
+ * probes learn, and an unknown answer is never cached.
47
+ *
48
+ * Bounded state (#1498 review): each child is subscribed to once, at spawn (its answer, its exit, and one kill timer at
49
+ * the deadline); a waiting caller is a removable entry with its own timer, gone when it is woken or its deadline
50
+ * passes. A child that never exits keeps its reservation (one per binary, PROBE_MAX_RUNNING in all), not the callers.
51
+ */
52
+ export declare class BinaryProbe {
53
+ private readonly spawnProbe;
54
+ private readonly now;
55
+ private readonly ttlMs;
56
+ private readonly deadlineMs;
57
+ private readonly maxRunning;
58
+ private cache;
59
+ private shared;
60
+ private newest;
61
+ private running;
62
+ private spawnSeq;
63
+ constructor(spawnProbe?: (binary: string) => ProbeChild, now?: () => number, ttlMs?: number, deadlineMs?: number, maxRunning?: number);
64
+ /** Never rejects; settles within the deadline. */
65
+ probe(binary: string, opts?: {
66
+ fresh?: boolean;
67
+ }): Promise<ProbeResult>;
68
+ /** Probe children not yet exited, stuck ones included: the physical cost the limits bound. */
69
+ get runningCount(): number;
70
+ /** Forget every answer (or one binary's); probes already running will not store theirs. */
71
+ invalidate(binary?: string): void;
72
+ private attempt;
73
+ /** The answer, taken (and stored by the newest caller); the loop has re-checked the caller's deadline just before. */
74
+ private accept;
75
+ private spawnSlot;
76
+ /** Until this child answers or exits (true), or the deadline passes (false); the caller is then forgotten. */
77
+ private waitOn;
78
+ }
79
+ /** The fleet process's shared probe. */
80
+ export declare const binaryProbe: BinaryProbe;
@@ -0,0 +1,182 @@
1
+ import { spawn } from "node:child_process";
2
+ /** How long a known answer is reused: long enough that a polled route rarely probes, short enough to see installs. */
3
+ export const BINARY_PROBE_TTL_MS = 30_000;
4
+ /** The old synchronous lookup's timeout, now a deadline on the answer instead of a block on the event loop. */
5
+ export const PROBE_DEADLINE_MS = 2_000;
6
+ /** Probe processes that may exist at once, stuck ones included (the backend catalog has seven binaries). */
7
+ export const PROBE_MAX_RUNNING = 8;
8
+ const UNKNOWN = { known: false };
9
+ /** `which <binary>` as a child process. */
10
+ export function spawnWhich(binary) {
11
+ return spawnProbeCommand("which", [binary]);
12
+ }
13
+ /**
14
+ * One probe process: exit 0 with output → the first line, any other exit → null, failing to start → rejection.
15
+ * `exited` settles on `close` (or a spawn that never produced a process). Exported so tests can run one that hangs.
16
+ */
17
+ export function spawnProbeCommand(command, args) {
18
+ let markExited;
19
+ const exited = new Promise((resolve) => { markExited = resolve; });
20
+ let child;
21
+ const answer = new Promise((resolve, reject) => {
22
+ const proc = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] });
23
+ child = proc;
24
+ let out = "";
25
+ proc.stdout?.setEncoding("utf8");
26
+ proc.stdout?.on("data", (chunk) => { if (out.length < 4096)
27
+ out += chunk; });
28
+ proc.once("error", (error) => {
29
+ reject(error);
30
+ if (proc.pid === undefined)
31
+ markExited();
32
+ });
33
+ proc.once("close", (code) => {
34
+ const first = out.trim().split("\n")[0];
35
+ resolve(code === 0 && first ? first : null); // the answer first, then the exit
36
+ markExited();
37
+ });
38
+ });
39
+ answer.catch(() => { });
40
+ return { answer, exited, kill: () => { try {
41
+ child?.kill("SIGKILL");
42
+ }
43
+ catch { /* already gone */ } } };
44
+ }
45
+ /**
46
+ * Probe answers per binary, reused for BINARY_PROBE_TTL_MS (monotonic clock); concurrent probes of one binary share
47
+ * one answer. `fresh: true` wants a lookup that starts after it asked (an install just ran): it skips the cached and
48
+ * the shared answer and accepts only a child spawned after its request, waiting (within its own deadline) for an older
49
+ * child of that binary to exit first. Callers that waited together share the next child. An answer is accepted only
50
+ * while the caller's deadline holds; the newest caller of a binary stores it, `invalidate()` discards what running
51
+ * probes learn, and an unknown answer is never cached.
52
+ *
53
+ * Bounded state (#1498 review): each child is subscribed to once, at spawn (its answer, its exit, and one kill timer at
54
+ * the deadline); a waiting caller is a removable entry with its own timer, gone when it is woken or its deadline
55
+ * passes. A child that never exits keeps its reservation (one per binary, PROBE_MAX_RUNNING in all), not the callers.
56
+ */
57
+ export class BinaryProbe {
58
+ spawnProbe;
59
+ now;
60
+ ttlMs;
61
+ deadlineMs;
62
+ maxRunning;
63
+ cache = new Map();
64
+ shared = new Map();
65
+ newest = new Map();
66
+ running = new Map();
67
+ spawnSeq = 0;
68
+ constructor(spawnProbe = spawnWhich, now = () => performance.now(), ttlMs = BINARY_PROBE_TTL_MS, deadlineMs = PROBE_DEADLINE_MS, maxRunning = PROBE_MAX_RUNNING) {
69
+ this.spawnProbe = spawnProbe;
70
+ this.now = now;
71
+ this.ttlMs = ttlMs;
72
+ this.deadlineMs = deadlineMs;
73
+ this.maxRunning = maxRunning;
74
+ }
75
+ /** Never rejects; settles within the deadline. */
76
+ probe(binary, opts = {}) {
77
+ if (!opts.fresh) {
78
+ const joined = this.shared.get(binary);
79
+ if (joined)
80
+ return joined;
81
+ const cached = this.cache.get(binary);
82
+ if (cached && this.now() - cached.at < this.ttlMs)
83
+ return Promise.resolve({ known: true, path: cached.path });
84
+ }
85
+ const token = {};
86
+ this.newest.set(binary, token);
87
+ const minSeq = opts.fresh ? this.spawnSeq + 1 : 0;
88
+ const result = this.attempt(binary, token, minSeq, this.now() + this.deadlineMs).catch(() => UNKNOWN);
89
+ this.shared.set(binary, result);
90
+ void result.then(() => { if (this.shared.get(binary) === result)
91
+ this.shared.delete(binary); });
92
+ return result;
93
+ }
94
+ /** Probe children not yet exited, stuck ones included: the physical cost the limits bound. */
95
+ get runningCount() { return this.running.size; }
96
+ /** Forget every answer (or one binary's); probes already running will not store theirs. */
97
+ invalidate(binary) {
98
+ for (const map of [this.cache, this.shared, this.newest]) {
99
+ if (binary === undefined)
100
+ map.clear();
101
+ else
102
+ map.delete(binary);
103
+ }
104
+ }
105
+ async attempt(binary, token, minSeq, deadline) {
106
+ let target;
107
+ for (;;) {
108
+ // Re-checked on every step, so also when an answer is taken: one that arrives after the caller's deadline is not
109
+ // an answer, whichever of its event and the deadline timer ran first (#1498 review).
110
+ if (this.now() >= deadline)
111
+ return UNKNOWN;
112
+ if (target) {
113
+ if (target.answer)
114
+ return this.accept(binary, token, target.answer);
115
+ if (!(await this.waitOn(target, deadline)))
116
+ return UNKNOWN;
117
+ continue;
118
+ }
119
+ const slot = this.running.get(binary);
120
+ if (slot && slot.seq >= minSeq) {
121
+ target = slot;
122
+ continue;
123
+ } // started after we asked: share it
124
+ if (slot) {
125
+ if (!(await this.waitOn(slot, deadline)))
126
+ return UNKNOWN;
127
+ continue;
128
+ } // an older child: let it go
129
+ if (this.running.size >= this.maxRunning)
130
+ return UNKNOWN;
131
+ target = this.spawnSlot(binary);
132
+ }
133
+ }
134
+ /** The answer, taken (and stored by the newest caller); the loop has re-checked the caller's deadline just before. */
135
+ accept(binary, token, answer) {
136
+ if (answer.known && this.newest.get(binary) === token)
137
+ this.cache.set(binary, { at: this.now(), path: answer.path });
138
+ return answer;
139
+ }
140
+ spawnSlot(binary) {
141
+ const slot = { seq: ++this.spawnSeq, child: this.spawnProbe(binary), answer: undefined, exited: false, waiters: new Set() };
142
+ this.running.set(binary, slot);
143
+ const wake = () => { for (const waiter of [...slot.waiters])
144
+ waiter(); };
145
+ // No answer by the deadline: kill it. It stays reserved until it has actually exited.
146
+ const kill = setTimeout(() => { if (!slot.answer)
147
+ slot.child.kill(); }, this.deadlineMs);
148
+ kill.unref?.();
149
+ const answered = slot.child.answer.then((path) => ({ known: true, path }), () => UNKNOWN);
150
+ void answered.then((answer) => {
151
+ clearTimeout(kill);
152
+ slot.answer ??= answer;
153
+ wake();
154
+ });
155
+ // An answer that settled before the exit is the answer; a child that exits without one answered unknown.
156
+ void slot.child.exited.then(() => Promise.race([answered, Promise.resolve(UNKNOWN)])).then((answer) => {
157
+ clearTimeout(kill);
158
+ slot.exited = true;
159
+ slot.answer ??= answer;
160
+ if (this.running.get(binary) === slot)
161
+ this.running.delete(binary);
162
+ wake();
163
+ });
164
+ return slot;
165
+ }
166
+ /** Until this child answers or exits (true), or the deadline passes (false); the caller is then forgotten. */
167
+ waitOn(slot, deadline) {
168
+ const remaining = deadline - this.now();
169
+ if (remaining <= 0)
170
+ return Promise.resolve(false);
171
+ return new Promise((resolve) => {
172
+ const done = (woken) => { clearTimeout(timer); slot.waiters.delete(wake); resolve(woken); };
173
+ const wake = () => done(true);
174
+ const timer = setTimeout(() => done(false), remaining);
175
+ timer.unref?.();
176
+ slot.waiters.add(wake);
177
+ });
178
+ }
179
+ }
180
+ /** The fleet process's shared probe. */
181
+ export const binaryProbe = new BinaryProbe();
182
+ //# sourceMappingURL=binary-probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary-probe.js","sourceRoot":"","sources":["../src/binary-probe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * #1468: the prompt-cache expiry analysis — pure, over a ledger (cache-ledger.ts). For a window [from, to]:
3
+ *
4
+ * - Each gap carries the TTL of the cache it sat on, as that session last wrote it: Claude Code's own write split
5
+ * (1 hour, or 5 minutes), Codex's 30 minutes (GPT-5.6 and later). Sessions of one instance can differ.
6
+ * - An expired request: one that came after a gap longer than its TTL and had to write at least half of the context
7
+ * the previous request left cached (the cache can outlive its TTL; then nothing was lost). What it rewrote, up to
8
+ * that context, is the expiry's tokens; its cost is those tokens at the write rate minus what reading them would
9
+ * have cost, at the returning request's own price tier.
10
+ * - Keep-warm, simulated: a ping at every I = TTL − TTL/10 after a request (t + k·I, k ≥ 1, up to and including
11
+ * the moment the gap ends — floor(gap / I) of them), each reading the cached context at that context's own tier.
12
+ * Only pings whose instant falls inside the window are counted. A gap that ended in an expired request inside the
13
+ * window saves its expiry cost; any other gap only pays for its pings. After a session's last request the pings
14
+ * are pure loss, up to the moment the next session of the instance starts, or the window ends.
15
+ * - Recommendation: on when the simulated pings cost less than the expiries they prevent (by a margin), else off.
16
+ *
17
+ * Money is list price (cache-prices.ts). If any model the window touches — requests, pinged contexts, idle tails —
18
+ * is missing from the table, the whole instance is weighed in input-price units instead (write 1.25x, 1-hour write
19
+ * 2x, read 0.1x, output 5x) and shows no dollars.
20
+ */
21
+ import { type Rates } from "./cache-prices.js";
22
+ import { type GapEvent, type Ledger } from "./cache-ledger.js";
23
+ export type Backend = "claude" | "codex";
24
+ export interface SimOptions {
25
+ rates: (model: string, prompt: number) => Rates;
26
+ from: number;
27
+ to: number;
28
+ /** Codex: use each event's measured write (the instance records real writes) rather than the estimate. */
29
+ measured: boolean;
30
+ }
31
+ export interface SimTail {
32
+ t: number;
33
+ ctx: number;
34
+ model: string;
35
+ ttl: number;
36
+ until: number;
37
+ }
38
+ export interface SimResult {
39
+ pastTtl: number;
40
+ expired: number;
41
+ rewriteTokens: number;
42
+ /** Expiry cost — and, below, every amount — in USD, or in input-price units when the instance is not priced. */
43
+ expiryCost: number;
44
+ pings: number;
45
+ pingCost: number;
46
+ tailPings: number;
47
+ tailCost: number;
48
+ /** What keep-warm would have saved, net of every ping. */
49
+ net: number;
50
+ /** A gap judged in this window used an estimated write (Codex's uncached input), not a recorded one. */
51
+ estimated: boolean;
52
+ }
53
+ export declare const UNITS: Rates;
54
+ export declare const pingIntervalMs: (ttlSec: number) => number;
55
+ /** Pings at t + k·I (k ≥ 1) no later than `end`, counted only where they fall inside [from, to]. */
56
+ export declare function pingsBetween(t: number, end: number, intervalMs: number, from: number, to: number): number;
57
+ /** The keep-warm simulation over a window's long gaps and its sessions' ends. */
58
+ export declare function simulate(gaps: readonly GapEvent[], tails: readonly SimTail[], o: SimOptions): SimResult;
59
+ /**
60
+ * Each session's end: its last request, until the next session of the instance starts (the earliest other session
61
+ * that starts at or after it — one that starts at the same instant leaves no idle time), or `to`.
62
+ */
63
+ export declare function sessionTails(ledger: Ledger, to: number): SimTail[];
64
+ /** Codex GPT-5.6 and later bill cache writes and keep a prefix 30 minutes. */
65
+ export declare const isOpenAiTtlModel: (model: string) => boolean;
66
+ export interface InstanceAnalysis {
67
+ requests: number;
68
+ /** The TTL the instance's latest session uses (each gap is judged by its own session's TTL). */
69
+ ttlSec: number;
70
+ /** Writes (and so expiries) are estimated: Codex records none. Also true for a Codex model whose TTL is assumed. */
71
+ estimate: boolean;
72
+ priced: boolean;
73
+ models: string[];
74
+ gapBuckets: number[];
75
+ sim: SimResult;
76
+ totalCost: number;
77
+ share: number | null;
78
+ recommendation: {
79
+ on: boolean;
80
+ reason: "quiet" | "never_expired" | "saves" | "costs_more";
81
+ pingsPerExpiry?: number;
82
+ };
83
+ }
84
+ export interface AnalyzeOptions {
85
+ backend: Backend;
86
+ from: number;
87
+ to: number;
88
+ rates?: (model: string, prompt: number) => Rates | null;
89
+ }
90
+ /** Requests in a window below this are too few to say anything. */
91
+ export declare const MIN_REQUESTS = 3;
92
+ export declare function analyzeLedger(ledger: Ledger, o: AnalyzeOptions): InstanceAnalysis;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * #1468: the prompt-cache expiry analysis — pure, over a ledger (cache-ledger.ts). For a window [from, to]:
3
+ *
4
+ * - Each gap carries the TTL of the cache it sat on, as that session last wrote it: Claude Code's own write split
5
+ * (1 hour, or 5 minutes), Codex's 30 minutes (GPT-5.6 and later). Sessions of one instance can differ.
6
+ * - An expired request: one that came after a gap longer than its TTL and had to write at least half of the context
7
+ * the previous request left cached (the cache can outlive its TTL; then nothing was lost). What it rewrote, up to
8
+ * that context, is the expiry's tokens; its cost is those tokens at the write rate minus what reading them would
9
+ * have cost, at the returning request's own price tier.
10
+ * - Keep-warm, simulated: a ping at every I = TTL − TTL/10 after a request (t + k·I, k ≥ 1, up to and including
11
+ * the moment the gap ends — floor(gap / I) of them), each reading the cached context at that context's own tier.
12
+ * Only pings whose instant falls inside the window are counted. A gap that ended in an expired request inside the
13
+ * window saves its expiry cost; any other gap only pays for its pings. After a session's last request the pings
14
+ * are pure loss, up to the moment the next session of the instance starts, or the window ends.
15
+ * - Recommendation: on when the simulated pings cost less than the expiries they prevent (by a margin), else off.
16
+ *
17
+ * Money is list price (cache-prices.ts). If any model the window touches — requests, pinged contexts, idle tails —
18
+ * is missing from the table, the whole instance is weighed in input-price units instead (write 1.25x, 1-hour write
19
+ * 2x, read 0.1x, output 5x) and shows no dollars.
20
+ */
21
+ import { ratesFor } from "./cache-prices.js";
22
+ import { GAP_BUCKETS } from "./cache-ledger.js";
23
+ export const UNITS = { input: 1, read: 0.1, write5m: 1.25, write1h: 2, write: 1.25, output: 5 };
24
+ const M = 1_000_000;
25
+ export const pingIntervalMs = (ttlSec) => Math.round(ttlSec * 900);
26
+ const writeRate = (r, ttlSec) => (ttlSec >= 3600 ? r.write1h : ttlSec <= 300 ? r.write5m : r.write);
27
+ /** Pings at t + k·I (k ≥ 1) no later than `end`, counted only where they fall inside [from, to]. */
28
+ export function pingsBetween(t, end, intervalMs, from, to) {
29
+ if (!(intervalMs > 0))
30
+ return 0;
31
+ const kMin = Math.max(1, Math.ceil((from - t) / intervalMs));
32
+ const kMax = Math.floor((Math.min(end, to) - t) / intervalMs);
33
+ return Math.max(0, kMax - kMin + 1);
34
+ }
35
+ /** The keep-warm simulation over a window's long gaps and its sessions' ends. */
36
+ export function simulate(gaps, tails, o) {
37
+ const res = { pastTtl: 0, expired: 0, rewriteTokens: 0, expiryCost: 0, pings: 0, pingCost: 0, tailPings: 0, tailCost: 0, net: 0, estimated: false };
38
+ let saved = 0;
39
+ for (const [tEnd, gap, ctx, ctxModel, ttl, model, prompt, measured, estimated] of gaps) {
40
+ if (tEnd < o.from)
41
+ continue;
42
+ const start = tEnd - Math.round(gap * 1000);
43
+ const pings = pingsBetween(start, tEnd, pingIntervalMs(ttl), o.from, o.to);
44
+ if (pings) {
45
+ res.pings += pings;
46
+ res.pingCost += pings * ctx * o.rates(ctxModel, ctx).read / M;
47
+ }
48
+ if (tEnd > o.to || !(gap > ttl))
49
+ continue;
50
+ res.pastTtl++;
51
+ const useMeasured = o.measured && measured !== null;
52
+ if (!useMeasured)
53
+ res.estimated = true;
54
+ const rewrite = useMeasured ? measured : estimated;
55
+ const lost = Math.min(rewrite, ctx);
56
+ if (ctx > 0 && lost >= ctx / 2) {
57
+ const r = o.rates(model, prompt);
58
+ const cost = lost * (writeRate(r, ttl) - r.read) / M;
59
+ res.expired++;
60
+ res.rewriteTokens += lost;
61
+ res.expiryCost += cost;
62
+ saved += cost;
63
+ }
64
+ }
65
+ for (const tail of tails) {
66
+ const n = pingsBetween(tail.t, tail.until, pingIntervalMs(tail.ttl), o.from, o.to);
67
+ if (!n)
68
+ continue;
69
+ res.tailPings += n;
70
+ res.tailCost += n * tail.ctx * o.rates(tail.model, tail.ctx).read / M;
71
+ }
72
+ res.net = saved - res.pingCost - res.tailCost;
73
+ return res;
74
+ }
75
+ /**
76
+ * Each session's end: its last request, until the next session of the instance starts (the earliest other session
77
+ * that starts at or after it — one that starts at the same instant leaves no idle time), or `to`.
78
+ */
79
+ export function sessionTails(ledger, to) {
80
+ const sessions = Object.values(ledger.files).filter((f) => f.last && f.first !== undefined).map((f) => ({ first: f.first, last: f.last }));
81
+ return sessions.map((s) => {
82
+ let until = to;
83
+ for (const o of sessions)
84
+ if (o !== s && o.first >= s.last.t && o.first < until)
85
+ until = o.first;
86
+ return { t: s.last.t, ctx: s.last.ctx, model: s.last.model, ttl: s.last.ttl, until };
87
+ });
88
+ }
89
+ /** Codex GPT-5.6 and later bill cache writes and keep a prefix 30 minutes. */
90
+ export const isOpenAiTtlModel = (model) => /^gpt-(5\.(6|[7-9])|[6-9])/.test(model);
91
+ /** Requests in a window below this are too few to say anything. */
92
+ export const MIN_REQUESTS = 3;
93
+ export function analyzeLedger(ledger, o) {
94
+ const listed = o.rates ?? ratesFor;
95
+ const inWindow = (h) => h + 3_600_000 > o.from && h <= o.to;
96
+ const tails = sessionTails(ledger, o.to);
97
+ const estimatedTotals = o.backend === "codex" && !ledger.writesSeen;
98
+ // One unit for the whole instance: dollars when every model that contributes to this window is priced — the
99
+ // window's requests, and whatever the simulation itself prices (a pinged context, an expired request, an idle tail).
100
+ // A probe run records exactly the models the simulation asks a rate for.
101
+ const requestModels = new Set();
102
+ for (const [hk, hour] of Object.entries(ledger.hours)) {
103
+ if (!inWindow(Number(hk)))
104
+ continue;
105
+ for (const mk of Object.keys(hour.m))
106
+ requestModels.add(mk.slice(0, mk.lastIndexOf("|")));
107
+ }
108
+ const touched = new Set(requestModels);
109
+ simulate(ledger.gaps, tails, { rates: (model) => { touched.add(model); return UNITS; }, from: o.from, to: o.to, measured: !estimatedTotals });
110
+ const priced = [...touched].every((m) => listed(m, 0) !== null);
111
+ const rates = (model, prompt) => (priced ? listed(model, prompt) : null) ?? UNITS;
112
+ const gapBuckets = new Array(GAP_BUCKETS).fill(0);
113
+ let requests = 0, totalCost = 0;
114
+ for (const [hk, hour] of Object.entries(ledger.hours)) {
115
+ if (!inWindow(Number(hk)))
116
+ continue;
117
+ hour.g.forEach((n, i) => { gapBuckets[i] += n; });
118
+ for (const [mk, [n, uncached, read, write5m, write1h, write, output]] of Object.entries(hour.m)) {
119
+ const bar = mk.lastIndexOf("|");
120
+ const model = mk.slice(0, bar), long = mk.slice(bar + 1) === "1";
121
+ requests += n;
122
+ const r = rates(model, long ? Number.MAX_SAFE_INTEGER : 0);
123
+ // Codex on GPT-5.6+ with no recorded writes: its uncached input is what got written (the estimate).
124
+ const uncachedRate = estimatedTotals && isOpenAiTtlModel(model) ? r.write : r.input;
125
+ totalCost += (uncached * uncachedRate + read * r.read + write5m * r.write5m + write1h * r.write1h + write * r.write + output * r.output) / M;
126
+ }
127
+ }
128
+ const sim = simulate(ledger.gaps, tails, { rates, from: o.from, to: o.to, measured: !estimatedTotals });
129
+ const latest = tails.reduce((a, t) => (!a || t.t > a.t ? t : a), null);
130
+ const ttlSec = latest ? latest.ttl : o.backend === "codex" ? 1800 : 3600;
131
+ const assumedTtl = o.backend === "codex" && [...requestModels].some((m) => !isOpenAiTtlModel(m));
132
+ let recommendation;
133
+ if (requests < MIN_REQUESTS)
134
+ recommendation = { on: false, reason: "quiet" };
135
+ else if (!sim.expired)
136
+ recommendation = { on: false, reason: "never_expired" };
137
+ else if (sim.net > 0 && sim.net >= sim.expiryCost * 0.1)
138
+ recommendation = { on: true, reason: "saves" };
139
+ else
140
+ recommendation = { on: false, reason: "costs_more", pingsPerExpiry: Math.round((sim.pings + sim.tailPings) / sim.expired) };
141
+ return {
142
+ // An estimate when anything shown used one: the totals (Codex with no recorded write), a gap's rewrite, a TTL.
143
+ requests, ttlSec, estimate: (estimatedTotals && requests > 0) || sim.estimated || assumedTtl, priced, models: [...requestModels].sort(), gapBuckets, sim,
144
+ totalCost, share: totalCost > 0 ? sim.expiryCost / totalCost : null, recommendation,
145
+ };
146
+ }
147
+ //# sourceMappingURL=cache-analysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-analysis.js","sourceRoot":"","sources":["../src/cache-analysis.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * #1468: what the prompt-cache analysis keeps per instance — never the transcripts themselves. Each model request a
3
+ * CLI made (a Claude assistant message, a Codex `token_count`) becomes a "turn"; the ledger folds turns into:
4
+ * - hourly totals per model and price tier (requests and tokens: uncached input, cache reads, cache writes, output)
5
+ * with the hour's histogram of gaps between consecutive requests of the same session;
6
+ * - one event per long gap (≥ GAP_EVENT_SEC): when it ended, how long it was, the context it left cached, what the
7
+ * request after it wrote to the cache — all the keep-warm simulation needs;
8
+ * - per transcript file: where reading stopped (a line start), and the session's first and last request.
9
+ * That keeps a month of a busy fleet in kilobytes per instance, and a file is read once, incrementally.
10
+ *
11
+ * Usage fields (real shapes, checked 2026-10-09):
12
+ * - Claude Code: `message.usage` { input_tokens, cache_read_input_tokens, cache_creation_input_tokens,
13
+ * cache_creation: { ephemeral_5m_input_tokens, ephemeral_1h_input_tokens }, output_tokens }. A message spans several
14
+ * lines (one per content block) carrying the same id and usage: counted once.
15
+ * - Codex (0.160): `event_msg` `token_count` with `info.last_token_usage` { input_tokens (cached included),
16
+ * cached_input_tokens, cache_write_input_tokens, output_tokens }. The write field exists but was 0 in every one of
17
+ * 38,551 real events on GPT-5.6+ models — so a write is estimated as the request's uncached input unless a real
18
+ * write count shows up. The model comes from the preceding `turn_context`.
19
+ */
20
+ import { type ScanLine } from "./cache-scan.js";
21
+ export type TranscriptKind = "claude" | "codex";
22
+ /** One model request, as the transcript records it. */
23
+ export interface Turn {
24
+ t: number;
25
+ model: string;
26
+ /** Everything the request sent: uncached input + cache reads + cache writes. */
27
+ prompt: number;
28
+ uncached: number;
29
+ read: number;
30
+ write5m: number;
31
+ write1h: number;
32
+ /** An OpenAI cache write as recorded (null: not recorded, 0 included when the field is absent). */
33
+ write: number | null;
34
+ output: number;
35
+ }
36
+ /** Gap histogram edges, seconds: ≤5 min, ≤30 min, ≤1 h, ≤2 h, ≤6 h, ≤24 h, longer. */
37
+ export declare const GAP_EDGES: readonly [300, 1800, 3600, 7200, 21600, 86400];
38
+ export declare const GAP_BUCKETS: number;
39
+ /** Gaps at least this long are kept one by one (the shortest keep-warm interval is a 5-minute TTL's 270 s). */
40
+ export declare const GAP_EVENT_SEC = 240;
41
+ export declare const KEEP_DAYS = 31;
42
+ /** [requests, uncached, read, write5m, write1h, write, output] */
43
+ export type HourTotals = [number, number, number, number, number, number, number];
44
+ export interface HourAgg {
45
+ m: Record<string, HourTotals>;
46
+ g: number[];
47
+ }
48
+ /**
49
+ * One long gap: [tEnd, gapSec, contextCached, contextModel, ttlSec, model, promptAfter, measuredWrite, estimatedWrite].
50
+ * - `contextCached`/`contextModel`: what the previous request left cached, and its model — what a ping would read.
51
+ * - `ttlSec`: the TTL of that cache as the session last wrote it (Claude's own write split; Codex's 30 minutes).
52
+ * - `measuredWrite`: what the request after the gap recorded writing (Claude always; Codex its write field, 0
53
+ * included, or null when the field is absent). `estimatedWrite`: Codex's uncached input — used only while the
54
+ * instance has never recorded a real write (the field exists but stays 0 on the CLIs seen so far).
55
+ */
56
+ export type GapEvent = [number, number, number, string, number, string, number, number | null, number];
57
+ export interface SessionPoint {
58
+ t: number;
59
+ ctx: number;
60
+ model: string;
61
+ ttl: number;
62
+ }
63
+ export interface FileCursor {
64
+ kind: TranscriptKind;
65
+ ino: number;
66
+ offset: number;
67
+ first?: number;
68
+ last?: SessionPoint;
69
+ /** Claude: the TTL the session's latest cache write used (1 h or 5 min). */
70
+ ttl?: number;
71
+ /** The file size at which only an unfinished line was left: not read again until the file grows. */
72
+ stalled?: number;
73
+ /** Codex: the model of the latest turn_context; the last cumulative total (token_count repeats itself). */
74
+ model?: string;
75
+ total?: number;
76
+ /** Claude: ids of the last messages counted (one message spans several lines). */
77
+ ids?: string[];
78
+ }
79
+ export interface Ledger {
80
+ v: 2;
81
+ files: Record<string, FileCursor>;
82
+ hours: Record<string, HourAgg>;
83
+ gaps: GapEvent[];
84
+ /** A real OpenAI cache-write count was seen (Codex): writes are then measured, not estimated. */
85
+ writesSeen?: boolean;
86
+ }
87
+ export declare function emptyLedger(): Ledger;
88
+ /** A ledger read back from disk; anything that is not one is a fresh ledger. */
89
+ export declare function reviveLedger(raw: unknown): Ledger;
90
+ export declare const hourOf: (t: number) => number;
91
+ export declare const gapBucket: (sec: number) => number;
92
+ /** The key hourly totals are kept under: the model, and whether the request paid its long-prompt row. */
93
+ export declare const modelKey: (model: string, prompt: number) => string;
94
+ /** Codex keeps a prefix 30 minutes (GPT-5.6 and later; assumed for any other Codex model, which is then an estimate). */
95
+ export declare const CODEX_TTL_SEC = 1800;
96
+ /** The TTL a Claude request's own cache writes used: its write split decides; no write leaves the session's as it was. */
97
+ export declare function claudeTtlAfter(turn: Turn, current: number | undefined): number;
98
+ /**
99
+ * Fold one turn into the ledger, in file order. Turns older than `cutoff` only move the session's last point (the
100
+ * next gap needs it); everything else is counted. Pure apart from mutating `ledger`.
101
+ */
102
+ export declare function addTurn(ledger: Ledger, file: FileCursor, turn: Turn, cutoff: number): void;
103
+ /** Drop what is older than `cutoff` (hours and gap events). */
104
+ export declare function pruneLedger(ledger: Ledger, cutoff: number): void;
105
+ /** The turn a Claude Code transcript line records, or null; a message already counted (`file.ids`) is null too. */
106
+ export declare function claudeLine(line: ScanLine, file: FileCursor): Turn | null;
107
+ /** The turn a Codex rollout line records, or null. A `turn_context` line updates the file's model. */
108
+ export declare function codexLine(line: ScanLine, file: FileCursor): Turn | null;