ccdeck 3.29.10 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +1 -1
  2. package/bin/agent-dag.js +67 -118
  3. package/bin/cli/help.js +67 -0
  4. package/bin/cli/login-item.js +182 -0
  5. package/bin/cli/one-shot.js +254 -0
  6. package/bin/cli/package.js +39 -0
  7. package/bin/cli/pulse.js +98 -0
  8. package/bin/cli/restart.js +179 -0
  9. package/bin/cli/screen.js +166 -0
  10. package/bin/cli/second-start.js +120 -0
  11. package/bin/cli/startup.js +459 -0
  12. package/bin/cli/uninstall.js +169 -0
  13. package/bin/deck.js +112 -1559
  14. package/dist/web/assets/BrowserWatchModal-DP3YMtEi.js +1 -0
  15. package/dist/web/assets/UsageHistoryModal-CcdPVXAw.js +1 -0
  16. package/dist/web/assets/index-3vS6A0vt.css +1 -0
  17. package/dist/web/assets/index-BVTWHDta.js +298 -0
  18. package/dist/web/index.html +2 -2
  19. package/hook/hook.js +15 -14
  20. package/package.json +1 -1
  21. package/release-notes.json +28 -0
  22. package/src/server/account-projects.mjs +25 -11
  23. package/src/server/account-routes.mjs +254 -0
  24. package/src/server/activity.mjs +3 -3
  25. package/src/server/append-budget.mjs +185 -0
  26. package/src/server/append-failures.mjs +147 -0
  27. package/src/server/args.mjs +37 -5
  28. package/src/server/atomic-write.mjs +300 -0
  29. package/src/server/block-notify.mjs +0 -5
  30. package/src/server/boot-lock.mjs +1 -1
  31. package/src/server/brand.mjs +7 -6
  32. package/src/server/browser-react.mjs +10 -2
  33. package/src/server/browser-watch-decks.mjs +146 -0
  34. package/src/server/browser-watch-feed.mjs +69 -0
  35. package/src/server/browser-watch-log.mjs +173 -0
  36. package/src/server/browser-watch-mtime.mjs +18 -0
  37. package/src/server/browser-watch-relay.mjs +112 -0
  38. package/src/server/browser-watch-routes.mjs +172 -0
  39. package/src/server/browser-watch-seen.mjs +127 -0
  40. package/src/server/browser-watch-store.mjs +14 -143
  41. package/src/server/browser-watch.mjs +278 -626
  42. package/src/server/canonical-path.mjs +150 -0
  43. package/src/server/ccusage-failure.mjs +64 -0
  44. package/src/server/ccusage-install.mjs +459 -0
  45. package/src/server/ccusage-runner.mjs +271 -0
  46. package/src/server/ccusage.mjs +21 -756
  47. package/src/server/claude-accounts.mjs +220 -464
  48. package/src/server/claude-collector.mjs +189 -0
  49. package/src/server/claude-dir.mjs +2 -2
  50. package/src/server/claude-identity.mjs +5 -4
  51. package/src/server/claude-reset-credits.mjs +130 -0
  52. package/src/server/claude-verdicts.mjs +174 -0
  53. package/src/server/clear-route.mjs +191 -0
  54. package/src/server/codex-auth.mjs +4 -4
  55. package/src/server/codex-dir.mjs +52 -8
  56. package/src/server/codex-enrichment.mjs +310 -0
  57. package/src/server/codex-quota.mjs +5 -3
  58. package/src/server/codex-translate.mjs +407 -0
  59. package/src/server/codex-usage.mjs +8 -5
  60. package/src/server/codex-watch.mjs +496 -0
  61. package/src/server/cswap-auto-loop.mjs +342 -0
  62. package/src/server/cswap-auto-readers.mjs +303 -0
  63. package/src/server/cswap-auto.mjs +51 -609
  64. package/src/server/cswap-install.mjs +69 -265
  65. package/src/server/cswap-layout.mjs +242 -0
  66. package/src/server/deck-home.mjs +1 -1
  67. package/src/server/deck-prefs.mjs +51 -184
  68. package/src/server/deck-scope.mjs +52 -0
  69. package/src/server/desktop-update-routes.mjs +75 -0
  70. package/src/server/detach.mjs +78 -0
  71. package/src/server/discovery.mjs +298 -0
  72. package/src/server/event-log.mjs +328 -0
  73. package/src/server/event-pipeline.mjs +235 -0
  74. package/src/server/event-ring.mjs +214 -0
  75. package/src/server/event-routes.mjs +352 -0
  76. package/src/server/event-sink.mjs +26 -0
  77. package/src/server/exec-children.mjs +128 -0
  78. package/src/server/exec-not-found.mjs +213 -0
  79. package/src/server/exec-output.mjs +23 -0
  80. package/src/server/exec-spec.mjs +341 -0
  81. package/src/server/exec.mjs +74 -661
  82. package/src/server/health-route.mjs +42 -0
  83. package/src/server/hook-challenge.mjs +37 -0
  84. package/src/server/http-io.mjs +104 -0
  85. package/src/server/index.mjs +172 -7620
  86. package/src/server/install-layout.mjs +506 -0
  87. package/src/server/installer.mjs +79 -608
  88. package/src/server/jsonl-chunks.mjs +139 -0
  89. package/src/server/lan-accounts.mjs +27 -0
  90. package/src/server/lan-beacon.mjs +457 -0
  91. package/src/server/lan-call.mjs +239 -0
  92. package/src/server/lan-copies.mjs +270 -0
  93. package/src/server/lan-deck.mjs +583 -0
  94. package/src/server/lan-dials.mjs +161 -0
  95. package/src/server/lan-engine.mjs +475 -1024
  96. package/src/server/lan-hearing.mjs +49 -0
  97. package/src/server/lan-inbound.mjs +100 -0
  98. package/src/server/lan-invite-offer.mjs +77 -0
  99. package/src/server/lan-invite.mjs +207 -0
  100. package/src/server/lan-lines.mjs +73 -0
  101. package/src/server/lan-manifest.mjs +74 -0
  102. package/src/server/lan-reach.mjs +82 -69
  103. package/src/server/lan-requests.mjs +249 -0
  104. package/src/server/lan-round-record.mjs +45 -0
  105. package/src/server/lan-round-timer.mjs +62 -0
  106. package/src/server/lan-routes.mjs +143 -0
  107. package/src/server/lan-serve.mjs +112 -0
  108. package/src/server/lan-socket.mjs +243 -925
  109. package/src/server/lan-sync.mjs +58 -1026
  110. package/src/server/lan-tailnet-poll.mjs +57 -0
  111. package/src/server/lan-turns.mjs +80 -0
  112. package/src/server/lan-wire.mjs +620 -0
  113. package/src/server/lifecycle.mjs +421 -0
  114. package/src/server/listen.mjs +110 -0
  115. package/src/server/live-decks.mjs +138 -0
  116. package/src/server/load-average.mjs +32 -0
  117. package/src/server/log-election.mjs +235 -0
  118. package/src/server/log-replay.mjs +374 -0
  119. package/src/server/log-writer.mjs +72 -557
  120. package/src/server/login-service.mjs +3 -2
  121. package/src/server/macmon.mjs +1 -1
  122. package/src/server/memory-files.mjs +163 -0
  123. package/src/server/memory-metrics.mjs +178 -0
  124. package/src/server/metrics-history.mjs +94 -0
  125. package/src/server/metrics-run.mjs +100 -0
  126. package/src/server/music-routes.mjs +134 -0
  127. package/src/server/network-metrics.mjs +5 -3
  128. package/src/server/network-sampler.mjs +256 -0
  129. package/src/server/npm-latest.mjs +385 -0
  130. package/src/server/npm-upgrade.mjs +352 -0
  131. package/src/server/npx.mjs +41 -7
  132. package/src/server/output-watch.mjs +5 -4
  133. package/src/server/pinned-build.mjs +108 -0
  134. package/src/server/prefs-path.mjs +14 -0
  135. package/src/server/prefs-refusal.mjs +77 -0
  136. package/src/server/prefs-routes.mjs +76 -0
  137. package/src/server/prefs-set-aside.mjs +86 -0
  138. package/src/server/prefs-state.mjs +51 -0
  139. package/src/server/process-command.mjs +104 -0
  140. package/src/server/process-list.mjs +547 -0
  141. package/src/server/pulse-line.mjs +132 -0
  142. package/src/server/purge-key.mjs +7 -7
  143. package/src/server/quota-cli.mjs +174 -0
  144. package/src/server/quota-oauth.mjs +190 -0
  145. package/src/server/quota-shape.mjs +229 -0
  146. package/src/server/quota-store-resets.mjs +206 -0
  147. package/src/server/quota.mjs +123 -509
  148. package/src/server/relay-guard.mjs +3 -3
  149. package/src/server/request-gates.mjs +441 -0
  150. package/src/server/restart-note.mjs +180 -0
  151. package/src/server/retire-sound-hook.mjs +3 -3
  152. package/src/server/ring-bounds.mjs +215 -0
  153. package/src/server/running-deck.mjs +1 -1
  154. package/src/server/running-version.mjs +24 -0
  155. package/src/server/self-update.mjs +39 -1371
  156. package/src/server/session-enrichment.mjs +622 -0
  157. package/src/server/session-read-gate.mjs +49 -0
  158. package/src/server/session-tracking.mjs +242 -0
  159. package/src/server/sse-clients.mjs +222 -0
  160. package/src/server/stale-discovery.mjs +30 -0
  161. package/src/server/static-serve.mjs +81 -0
  162. package/src/server/supervisor.mjs +73 -12
  163. package/src/server/system-metrics.mjs +169 -1935
  164. package/src/server/term.mjs +11 -249
  165. package/src/server/thermal-metrics.mjs +520 -0
  166. package/src/server/thermal-sampler.mjs +204 -0
  167. package/src/server/token-redact.mjs +123 -0
  168. package/src/server/transcript-gate.mjs +107 -0
  169. package/src/server/transcript-scan.mjs +563 -0
  170. package/src/server/usage-routes.mjs +93 -0
  171. package/src/server/uv-bootstrap.mjs +1 -1
  172. package/src/server/wordmark.mjs +127 -0
  173. package/dist/web/assets/BrowserWatchModal-CMn28FHb.js +0 -1
  174. package/dist/web/assets/UsageHistoryModal-C-sPhsKT.js +0 -1
  175. package/dist/web/assets/index-C1YFI9f4.js +0 -298
  176. package/dist/web/assets/index-Ck2RAla3.css +0 -1
@@ -0,0 +1,407 @@
1
+ // One Codex rollout record in, one synthetic hook payload out: the whole of the
2
+ // Codex translation, and the one place the shape of a rollout line is read.
3
+ //
4
+ // These lived in src/server/index.mjs, inside the rollout watcher, and they are
5
+ // the part of it that neither watches nor tails — no file, no timer, no ring.
6
+ // The watcher decides which lines to read and what becomes of the payload; this
7
+ // decides what a line MEANS, with each note about a record Codex writes (#397,
8
+ // #398, #399, #417) beside the branch it explains. The tests that pin the
9
+ // mapping against the real reducer import it from here, without importing the
10
+ // server they never needed.
11
+ //
12
+ // The two per-session maps came with it, because codexObjToPayload is what
13
+ // writes them. They are exported as they are rather than wrapped: the watcher
14
+ // reads the model back after each line to announce a change, and forgetSession
15
+ // clears both when a session expires, exactly as it did beside them.
16
+
17
+ export const codexSessionModel = new Map(); // sid -> last model string
18
+ // sid -> the `approval_policy` of the newest `turn_context` seen on this
19
+ // session. See codexObjToPayload for why this is read and what it is NOT used
20
+ // for; #398 for why the deck holds it at all.
21
+ export const codexSessionApproval = new Map(); // sid -> last approval_policy string
22
+
23
+ // ─── What a rollout record states ─────────────────────────────────────────
24
+ // The record shapes more than one reader takes a fact off: the translation
25
+ // below, which reads them as events; codex-enrichment.mjs, which reads a
26
+ // rollout's head and tail for a hook-delivered session's usage, model and
27
+ // window; codex-usage.mjs, which reads every token_count for the usage windows;
28
+ // and the watcher's header read in codex-watch.mjs. Each tested the shape for
29
+ // itself, four spellings of the same `obj.type` and `payload.type` checks, and
30
+ // OpenAI has renamed a record once already (#996) — so the shape is read here,
31
+ // once, and each reader asks for the fact. A shape only the translation reads
32
+ // stays inline in codexObjToPayload, beside the note that explains it.
33
+
34
+ /** The payload of a `session_meta` record — the rollout's first line, with the
35
+ * session's id, its cwd and sometimes its model — or null. */
36
+ export function sessionMeta(obj) {
37
+ return obj && obj.type === "session_meta" && obj.payload ? obj.payload : null;
38
+ }
39
+
40
+ /** The `info` of a `token_count` event: the session's cumulative spend
41
+ * (`total_token_usage`), the last request's (`last_token_usage`) and the
42
+ * window (`model_context_window`) — see codexObjToPayload for which is which —
43
+ * or null. */
44
+ export function tokenCountInfo(obj) {
45
+ const pl = obj && obj.payload;
46
+ return obj && obj.type === "event_msg" && pl && pl.type === "token_count" && pl.info ? pl.info : null;
47
+ }
48
+
49
+ /** The context window a `task_started` event states, or null. It fires once per
50
+ * turn, and is the other carrier of the window beside token_count. */
51
+ export function taskStartedWindow(obj) {
52
+ const pl = obj && obj.payload;
53
+ return obj && obj.type === "event_msg" && pl && pl.type === "task_started" && typeof pl.model_context_window === "number"
54
+ ? pl.model_context_window
55
+ : null;
56
+ }
57
+
58
+ /** The model a `response_item` names, or null. */
59
+ export function responseItemModel(obj) {
60
+ const pl = obj && obj.payload;
61
+ return obj && obj.type === "response_item" && pl && typeof pl.model === "string" ? pl.model : null;
62
+ }
63
+
64
+ /**
65
+ * The human's prompt out of a 0.147-era `item_completed` item.
66
+ *
67
+ * `item.content` is an array of parts — every UserMessage observed carries a
68
+ * single `{ type: "text", text, text_elements }` — so the parts are joined
69
+ * rather than indexed, and a part with no string `text` contributes nothing
70
+ * instead of printing "undefined" into the prompt the card shows.
71
+ */
72
+ function codexItemText(item) {
73
+ const parts = Array.isArray(item && item.content) ? item.content : [];
74
+ return parts.map(p => (p && typeof p.text === "string" ? p.text : "")).join("");
75
+ }
76
+
77
+ // Map one parsed rollout object to a synthetic hook payload (or null to skip).
78
+ // Mutates codexSessionModel and returns { payload, modelEvent } where
79
+ // modelEvent is an optional ModelObserved to emit first when the model changed.
80
+ //
81
+ // Exported for the rollout watcher in codex-watch.mjs, and for the tests: this
82
+ // is the whole of the Codex translation, and the lifecycle it produces is worth
83
+ // pinning against the real reducer without standing up a watcher, a temp home
84
+ // and a 1.5s poll to get at it.
85
+ export function codexObjToPayload(obj, sid, cwd) {
86
+ const type = obj && obj.type;
87
+ const pl = (obj && obj.payload) || {};
88
+ const model = codexSessionModel.get(sid);
89
+ // Rides on every payload this function returns, exactly as `model` does and
90
+ // for the same reason: it is a property of the SESSION rather than of any one
91
+ // event, the rollout restates it on every turn, and a field spread onto each
92
+ // payload needs no event of its own and self-heals on the next line the
93
+ // watcher reads. The reducer stamps it on the root beside `contextWindow`.
94
+ const approval_policy = codexSessionApproval.get(sid);
95
+ const base = { session_id: sid, cwd, provider: "codex", approval_policy };
96
+
97
+ // Track model from turn_context / response_item before mapping events.
98
+ if (type === "turn_context") {
99
+ if (typeof pl.model === "string") codexSessionModel.set(sid, pl.model);
100
+ // #398: the deck used to read `model` out of this record and drop
101
+ // everything else, and `approval_policy` was the field that mattered most
102
+ // among the discarded ones. It is the ONLY recorded fact anywhere in a
103
+ // rollout that says whether this session is even capable of stopping to ask
104
+ // a human: at "never" Codex denies an escalation outright rather than
105
+ // prompting, so a quiet session there is genuinely working, while at
106
+ // "on-request" / "on-failure" / "untrusted" a quiet one may be parked on a
107
+ // prompt the deck will never see.
108
+ //
109
+ // It is read to SAY that, and deliberately not to infer a block from it —
110
+ // see codex-approval.ts. Codex persists no approval record of any kind (58
111
+ // turn_contexts and ~1,100 records across the rollouts on this machine
112
+ // carry no approval-shaped type; the persist filter keeps every *_end and
113
+ // drops every request/begin — patch_apply_end with no patch_apply_begin,
114
+ // web_search_end with no begin, item_completed with no item_started), so a
115
+ // "waiting" state derived from this plus a pending call would be a guess
116
+ // wearing the clothes of a measurement.
117
+ //
118
+ // Deleted from the record when the value is missing or not a string, so this
119
+ // server stops stamping a stale policy on the payloads it builds from here
120
+ // on. The page does not un-say it, though. `approval_policy: undefined` does
121
+ // not survive JSON.stringify, so the wire has no way to say "cleared", and
122
+ // the reducer applies the field only when it is a non-empty string — a card
123
+ // keeps the last policy it was told for as long as the session is on the
124
+ // board. This comment used to promise the stale answer was cleared; it is
125
+ // cleared here and nowhere else (#996).
126
+ if (typeof pl.approval_policy === "string" && pl.approval_policy) {
127
+ codexSessionApproval.set(sid, pl.approval_policy);
128
+ } else {
129
+ codexSessionApproval.delete(sid);
130
+ }
131
+ return null;
132
+ }
133
+ const itemModel = responseItemModel(obj);
134
+ if (itemModel !== null) {
135
+ codexSessionModel.set(sid, itemModel);
136
+ }
137
+
138
+ if (type === "event_msg") {
139
+ if (pl.type === "user_message") {
140
+ const prompt = typeof pl.message === "string" ? pl.message : "";
141
+ return { ...base, hook_event_name: "UserPromptSubmit", prompt, model };
142
+ }
143
+ // Codex 0.147 stopped writing `user_message` and writes the same submission
144
+ // as an `item_completed` carrying a `UserMessage` item instead. The two are
145
+ // mutually exclusive per CLI version — across the rollouts sampled here the
146
+ // 0.144 files have `user_message` and no `item_completed` at all, and the
147
+ // 0.147 files have exactly one `UserMessage` item per turn and no
148
+ // `user_message` — so handling both names emits one prompt per turn either
149
+ // way rather than two on either version. The item is the human's typed text
150
+ // only: the AGENTS.md preamble Codex prepends is written as a bare
151
+ // `response_item` with role "user" and never gets an item of its own.
152
+ //
153
+ // This matters far beyond the prompt text. `UserPromptSubmit` is what puts
154
+ // a settled root back to `active` (session-lifecycle.ts in the client), so
155
+ // on 0.147 it is the ONLY thing that reopens a session for its second turn —
156
+ // without it the `Stop` below would trade "live forever" for "done
157
+ // forever", which is not better.
158
+ if (pl.type === "item_completed" && pl.item && pl.item.type === "UserMessage") {
159
+ return { ...base, hook_event_name: "UserPromptSubmit", prompt: codexItemText(pl.item), model };
160
+ }
161
+ // Codex states THREE numbers on every `token_count` and this branch used to
162
+ // take one of them (#399). The other two are what the context donut is drawn
163
+ // from, so the deck rendered no context readout at all for the only provider
164
+ // that reports its window exactly.
165
+ //
166
+ // Measured across every rollout under this machine's CODEX_HOME — 178
167
+ // `token_count` records, 164 on Codex 0.144.5 and 14 on 0.147.0 — all 178
168
+ // carry `info.last_token_usage`, `info.total_token_usage` and
169
+ // `info.model_context_window`. Nothing here is a new read: the line is
170
+ // already parsed and the object already destructured.
171
+ //
172
+ // total_token_usage cumulative SPEND for the session. Every request's
173
+ // prompt summed, so it counts the cached prefix again
174
+ // on every turn and passes the context window many
175
+ // times over inside one session (5,238,700 against a
176
+ // 258,400 window in the longest file here). Correct
177
+ // for cost, meaningless as an occupancy figure.
178
+ //
179
+ // last_token_usage the MOST RECENT request: `input_tokens` is the whole
180
+ // conversation Codex sent (it already contains the
181
+ // cached prefix — see billedInputTokens), plus that
182
+ // request's completion. `total_tokens` is exactly
183
+ // input + output on 177 of the 178 records.
184
+ //
185
+ // model_context_window the CLI's own ceiling, 258,400 for gpt-5.6 against
186
+ // the 1,050,000 the static table guesses.
187
+ //
188
+ // WHY `last_token_usage.total_tokens` AND NOT `input_tokens`. The prompt-only
189
+ // figure is the closer analogue of the Claude side, which sums the last usage
190
+ // block's input + cache_read + cache_creation and leaves the completion out.
191
+ // The difference is one response — 20 to 2,288 tokens in this sample, under
192
+ // 1% of the window — and `total_tokens` wins on the case where they diverge
193
+ // for real: on `thread_rolled_back` (the user rewinding the conversation)
194
+ // Codex writes a `token_count` whose per-request components are all zero and
195
+ // whose `last_token_usage.total_tokens` is the RECOMPUTED context size —
196
+ // 47,355, down from 58,516 — while `total_token_usage` does not move at all,
197
+ // because no request was made. Codex is using that field as "tokens in the
198
+ // window", and reading `input_tokens` there would collapse the donut to 0%
199
+ // at precisely the moment the number changed most.
200
+ //
201
+ // The window rides along too. `task_started` below is the only other carrier
202
+ // and it fires once per turn, so a deck that attached mid-turn — the ordinary
203
+ // case, since the watcher skips a pre-existing session's history at startup —
204
+ // had to wait for the next turn before the donut could be scaled against
205
+ // anything but the wrong static default.
206
+ const info = tokenCountInfo(obj);
207
+ if (info) {
208
+ const last = info.last_token_usage;
209
+ const contextTokens = last && typeof last.total_tokens === "number" ? last.total_tokens : undefined;
210
+ const window = typeof info.model_context_window === "number" ? info.model_context_window : undefined;
211
+ // A record that states none of the three says nothing, and emitting an
212
+ // event for it would put an empty envelope in the ring buffer and in the
213
+ // persisted log for every reader to skip forever.
214
+ if (!info.total_token_usage && contextTokens === undefined && window === undefined) return null;
215
+ return {
216
+ ...base,
217
+ hook_event_name: "UsageObserved",
218
+ usage: info.total_token_usage,
219
+ model,
220
+ model_context_window: window,
221
+ context_tokens: contextTokens,
222
+ };
223
+ }
224
+ const startWindow = taskStartedWindow(obj);
225
+ if (startWindow !== null) {
226
+ return { ...base, hook_event_name: "ModelObserved", model, model_context_window: startWindow };
227
+ }
228
+ // The end of a turn, which is the only end Codex ever announces. Both names
229
+ // are one outcome as far as the deck is concerned — the turn is over and
230
+ // nothing is running — so both settle the root the way Claude's own Stop
231
+ // hook does. They are also exhaustive: across the rollouts sampled here
232
+ // every `task_started` is answered by exactly one of the two (54 completes
233
+ // + 1 abort for 55 starts), so no turn is left open by this mapping and
234
+ // none is closed twice. `turn_aborted` is the Esc key, and it is the case
235
+ // that matters most on the deck: pressing Esc is precisely when the user is
236
+ // watching to confirm the thing stopped.
237
+ //
238
+ // Per TURN, not per session, and that is correct: Codex writes no
239
+ // session-close record at all — a rollout simply stops growing when the
240
+ // terminal goes away — so `sweepStaleSessions` remains the only thing that
241
+ // ends a Codex *session*, and it must. What changes is that it now only
242
+ // ever sees the sessions it was written for: the ones that died without
243
+ // finishing. A turn that really ended is settled here, at the moment it
244
+ // ended, with no `reaped` flag, because it was a finish and not a guess.
245
+ if (pl.type === "task_complete" || pl.type === "turn_aborted") {
246
+ return { ...base, hook_event_name: "Stop", model };
247
+ }
248
+ return null;
249
+ }
250
+ if (type === "response_item") {
251
+ if (pl.type === "function_call") {
252
+ let input = pl.arguments;
253
+ try { input = JSON.parse(pl.arguments); } catch {}
254
+ return { ...base, hook_event_name: "PreToolUse", tool_name: pl.name ?? "tool", tool_input: input, tool_use_id: pl.call_id, model };
255
+ }
256
+ if (pl.type === "custom_tool_call") {
257
+ return { ...base, hook_event_name: "PreToolUse", tool_name: pl.name ?? "tool", tool_input: codexCustomToolInput(pl.name, pl.input), tool_use_id: pl.call_id, model };
258
+ }
259
+ // #397: the outcome, not just the fact that an outcome arrived. This used
260
+ // to hardcode "PostToolUse" for both output types, and the reducer derives
261
+ // `ok` from the event NAME (`tc.ok = name === "PostToolUse"`) — so `ok` was
262
+ // structurally incapable of being false on the Codex path and a command
263
+ // that exited non-zero drew exactly like one that succeeded. Every surface
264
+ // that reads the flag inherited the lie: the burst dot, the tool row, the
265
+ // ToolModal styling, the detail-panel error count, and the session
266
+ // summary's "Errors" stat, which was therefore pinned at 0 for the life of
267
+ // a Codex session. `PostToolUseFailure` is the name the reducer already
268
+ // understands (it sets `ok = false` and writes an `errorPreview` from the
269
+ // response); nothing but this mapper was missing.
270
+ if (pl.type === "function_call_output" || pl.type === "custom_tool_call_output") {
271
+ const tool_response = pl.output != null ? parseCodexOutput(pl.output) : undefined;
272
+ const name = codexCallFailed(pl.output) ? "PostToolUseFailure" : "PostToolUse";
273
+ return { ...base, hook_event_name: name, tool_use_id: pl.call_id, tool_response, model };
274
+ }
275
+ }
276
+ return null;
277
+ }
278
+
279
+ /**
280
+ * Wrap a `custom_tool_call`'s raw input under the key that describes what it
281
+ * actually is.
282
+ *
283
+ * WHY THIS IS NOT JUST `{ patch: input }` ANY MORE (#417). Codex's
284
+ * `custom_tool_call` container carries a bare string and nothing that says what
285
+ * kind of string it is — the tool's NAME is the only discriminator. When this
286
+ * branch was written the container had exactly one inhabitant, `apply_patch`,
287
+ * so it hardcoded `patch` and was right. It is no longer the only inhabitant,
288
+ * and on 0.147 it is not even the common one:
289
+ *
290
+ * CLI custom_tool_call name count
291
+ * 0.144.5 exec 77
292
+ * 0.144.5 apply_patch 2
293
+ * 0.147.0 exec 6
294
+ *
295
+ * Eighty-three shell scripts were therefore filed on the deck as patches. The
296
+ * client's `commandStringOf` reads `cmd` / `command` / `script` and never
297
+ * `patch`, so no Codex call could show what it ran; `extractFilePath` mean-
298
+ * while reads `patch` and tried to find a `*** Update File:` header in a
299
+ * JavaScript program. Keying off the name fixes both directions at once.
300
+ *
301
+ * WHY `exec` GETS `script` AND NOT `command`. Because it is not a command. The
302
+ * `exec` tool takes a small JavaScript program that calls into a `tools.*` API
303
+ * — 83 of 83 inputs on this machine start with `const `, none parses as JSON,
304
+ * all are multi-line — and Codex's own result wrapper calls it one, prefixing
305
+ * the output with "Script completed" / "Script failed" rather than an exit code
306
+ * (the line #397 reads). Filing a program under `command` would make the deck
307
+ * draw the first token of the program as the command that ran, which is the
308
+ * word `const` on every Codex call in the session. `script` is both true and
309
+ * already understood by the client, which digs the real command out of the
310
+ * program from there.
311
+ *
312
+ * WHY THE FALLBACK IS `input` AND NOT A GUESS. A name this function has never
313
+ * heard of is a string whose meaning is unknown, and the one thing worse than
314
+ * showing it under a neutral key is showing it under a confident wrong one —
315
+ * that is the whole of this bug, repeated. `input` claims nothing; the tool's
316
+ * own name still reaches the bubble, so an unrecognised Codex tool degrades to
317
+ * "a tool I cannot read the arguments of" instead of "a patch".
318
+ */
319
+ const CODEX_CUSTOM_TOOL_INPUT_KEY = {
320
+ // A `*** Begin Patch … *** End Patch` document (2/2 observed, both 0.144.5).
321
+ // Unchanged, and it has to stay unchanged: the client's extractFilePath()
322
+ // pulls the edited file's path out of `input.patch` for the sub-bubble.
323
+ apply_patch: "patch",
324
+ // A JavaScript program. See above.
325
+ exec: "script",
326
+ };
327
+
328
+ function codexCustomToolInput(name, input) {
329
+ const key = CODEX_CUSTOM_TOOL_INPUT_KEY[name] ?? "input";
330
+ return { [key]: input };
331
+ }
332
+
333
+ /**
334
+ * The text parts of a Codex tool result, in the order Codex wrote them.
335
+ *
336
+ * Codex writes the result in two different containers and the deck sees both,
337
+ * so this is where the difference stops. Across the rollouts sampled here:
338
+ * `custom_tool_call_output.output` is an ARRAY of `{ type: "input_text", text }`
339
+ * parts (85/85, on 0.144 and 0.147 alike), and `function_call_output.output` is
340
+ * a bare string (32/32). The `{ output, metadata }` envelope `parseCodexOutput`
341
+ * unwraps was written by neither, but it is cheap to keep tolerating and the
342
+ * unwrapping already lives there, so the string case is routed through it
343
+ * rather than duplicating the guess.
344
+ */
345
+ function codexOutputParts(output) {
346
+ if (output == null) return [];
347
+ if (Array.isArray(output)) {
348
+ return output.map(p => (p && typeof p.text === "string" ? p.text : ""));
349
+ }
350
+ const unwrapped = parseCodexOutput(output);
351
+ return typeof unwrapped === "string" ? [unwrapped] : [];
352
+ }
353
+
354
+ /**
355
+ * Did this Codex tool call actually fail?
356
+ *
357
+ * Codex prepends its own wrapper line to the tool's output and that line — not
358
+ * any structured field — is where the outcome lives. Verified against every
359
+ * tool result in this machine's CODEX_HOME:
360
+ *
361
+ * 0.144.5 exec "Script completed" 75 "Script failed" 2
362
+ * 0.147.0 exec "Script completed" 6 (no failure observed)
363
+ * 0.144.5 apply_patch "Exit code: 0" 2
364
+ * 0.144.5 exec_command / run — bare string, no wrapper line at all 32
365
+ *
366
+ * The two CLI versions spell it IDENTICALLY, which is why one rule covers both
367
+ * and why this needs no version sniffing: 0.147 renamed the prompt event (see
368
+ * the `item_completed` branch above) but left the exec wrapper alone.
369
+ *
370
+ * Only the FIRST part's FIRST line is read, and that precision is load-bearing
371
+ * rather than tidiness. The wrapper line is at part index 0 in 85 of 85 results
372
+ * that have one; the later parts are the command's own stdout, and the command
373
+ * prints whatever it likes there. On this machine two exec results contain a
374
+ * line reading "Script error:" in part 1 — output from a script that ran fine
375
+ * under a wrapper that says "Script completed" — so a rule that scanned every
376
+ * part would paint two successful calls red. `\r` is stripped because the same
377
+ * wrapper is written by Codex on Windows.
378
+ *
379
+ * Silence means success, deliberately. A result with no wrapper line — every
380
+ * `function_call_output` on 0.144, and whatever container a future Codex
381
+ * invents — keeps today's behaviour of mapping to `PostToolUse`. Reporting an
382
+ * unknown outcome as a failure would trade one wrong colour for another, and
383
+ * this direction is the recoverable one: a missed failure is a call that draws
384
+ * as it always has, while a false failure puts a red dot and an "Errors" count
385
+ * on a session that did nothing wrong.
386
+ */
387
+ function codexCallFailed(output) {
388
+ const first = codexOutputParts(output)[0];
389
+ if (typeof first !== "string") return false;
390
+ const head = first.split("\n")[0].replace(/\r$/, "");
391
+ if (/^Script failed\b/.test(head)) return true;
392
+ // apply_patch reports itself with an exit code instead of a word. Anything
393
+ // non-zero is a patch that did not apply.
394
+ const exit = /^Exit code:\s*(\d+)/.exec(head);
395
+ if (exit) return Number(exit[1]) !== 0;
396
+ return false;
397
+ }
398
+
399
+ function parseCodexOutput(raw) {
400
+ if (typeof raw !== "string") return raw;
401
+ try {
402
+ const o = JSON.parse(raw);
403
+ return (o && typeof o.output === "string") ? o.output : raw;
404
+ } catch {
405
+ return raw;
406
+ }
407
+ }
@@ -13,6 +13,9 @@ import { createReadStream } from "node:fs";
13
13
  import zlib from "node:zlib";
14
14
  import { STOP, walkRolloutDays } from "./codex-dir.mjs";
15
15
  import { PRODUCT } from "./brand.mjs";
16
+ // The shape of a token_count record, read where the translation reads it — see
17
+ // codex-translate.mjs.
18
+ import { tokenCountInfo } from "./codex-translate.mjs";
16
19
 
17
20
  // Cache results for 60s (lighter than Claude quota — reads more files)
18
21
  let _cache = null;
@@ -132,8 +135,7 @@ function foldTokenLine(series, raw) {
132
135
  if (!raw.includes("total_token_usage")) return;
133
136
  let obj;
134
137
  try { obj = JSON.parse(raw); } catch { return; }
135
- if (obj.type !== "event_msg" || obj.payload?.type !== "token_count") return;
136
- const u = obj.payload.info?.total_token_usage;
138
+ const u = tokenCountInfo(obj)?.total_token_usage;
137
139
  if (!u) return;
138
140
  const ts = obj.timestamp ? Date.parse(obj.timestamp) : NaN;
139
141
  series.push({
@@ -401,9 +403,10 @@ function parseRolloutTime(filename) {
401
403
 
402
404
  // List rollout files whose start times fall within the given window.
403
405
  //
404
- // The walk over $CODEX_HOME/sessions is shared with the two entry points in
405
- // index.mjs (codex-dir.mjs) rather than repeated here, so the files this counts
406
- // usage from are exactly the files the watcher tails. They used to be two
406
+ // The walk over $CODEX_HOME/sessions is shared with the rollout lookup in
407
+ // codex-enrichment.mjs and the watcher in codex-watch.mjs (codex-dir.mjs) rather
408
+ // than repeated here, so the files this counts usage from are exactly the files
409
+ // the watcher tails. They used to be two
407
410
  // verbatim copies of the same four nested readdirs over two verbatim copies of
408
411
  // the same CODEX_SESSIONS_DIR — which is how one of them came to resolve a
409
412
  // relative CODEX_HOME differently from the other (#375).