ccdeck 3.29.11 → 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.
- package/README.md +1 -1
- package/bin/agent-dag.js +67 -118
- package/bin/cli/help.js +67 -0
- package/bin/cli/login-item.js +182 -0
- package/bin/cli/one-shot.js +254 -0
- package/bin/cli/package.js +39 -0
- package/bin/cli/pulse.js +98 -0
- package/bin/cli/restart.js +179 -0
- package/bin/cli/screen.js +166 -0
- package/bin/cli/second-start.js +120 -0
- package/bin/cli/startup.js +459 -0
- package/bin/cli/uninstall.js +169 -0
- package/bin/deck.js +112 -1559
- package/dist/web/assets/BrowserWatchModal-DP3YMtEi.js +1 -0
- package/dist/web/assets/UsageHistoryModal-CcdPVXAw.js +1 -0
- package/dist/web/assets/index-3vS6A0vt.css +1 -0
- package/dist/web/assets/index-BVTWHDta.js +298 -0
- package/dist/web/index.html +2 -2
- package/hook/hook.js +15 -14
- package/package.json +1 -1
- package/release-notes.json +22 -0
- package/src/server/account-projects.mjs +25 -11
- package/src/server/account-routes.mjs +254 -0
- package/src/server/activity.mjs +3 -3
- package/src/server/append-budget.mjs +185 -0
- package/src/server/append-failures.mjs +147 -0
- package/src/server/args.mjs +37 -5
- package/src/server/atomic-write.mjs +300 -0
- package/src/server/block-notify.mjs +0 -5
- package/src/server/boot-lock.mjs +1 -1
- package/src/server/brand.mjs +7 -6
- package/src/server/browser-react.mjs +10 -2
- package/src/server/browser-watch-decks.mjs +146 -0
- package/src/server/browser-watch-feed.mjs +69 -0
- package/src/server/browser-watch-log.mjs +173 -0
- package/src/server/browser-watch-mtime.mjs +18 -0
- package/src/server/browser-watch-relay.mjs +112 -0
- package/src/server/browser-watch-routes.mjs +172 -0
- package/src/server/browser-watch-seen.mjs +127 -0
- package/src/server/browser-watch-store.mjs +14 -143
- package/src/server/browser-watch.mjs +278 -626
- package/src/server/canonical-path.mjs +150 -0
- package/src/server/ccusage-failure.mjs +64 -0
- package/src/server/ccusage-install.mjs +459 -0
- package/src/server/ccusage-runner.mjs +271 -0
- package/src/server/ccusage.mjs +21 -756
- package/src/server/claude-accounts.mjs +220 -464
- package/src/server/claude-collector.mjs +189 -0
- package/src/server/claude-dir.mjs +2 -2
- package/src/server/claude-identity.mjs +5 -4
- package/src/server/claude-reset-credits.mjs +130 -0
- package/src/server/claude-verdicts.mjs +174 -0
- package/src/server/clear-route.mjs +191 -0
- package/src/server/codex-auth.mjs +4 -4
- package/src/server/codex-dir.mjs +52 -8
- package/src/server/codex-enrichment.mjs +310 -0
- package/src/server/codex-quota.mjs +5 -3
- package/src/server/codex-translate.mjs +407 -0
- package/src/server/codex-usage.mjs +8 -5
- package/src/server/codex-watch.mjs +496 -0
- package/src/server/cswap-auto-loop.mjs +342 -0
- package/src/server/cswap-auto-readers.mjs +303 -0
- package/src/server/cswap-auto.mjs +51 -609
- package/src/server/cswap-install.mjs +69 -265
- package/src/server/cswap-layout.mjs +242 -0
- package/src/server/deck-home.mjs +1 -1
- package/src/server/deck-prefs.mjs +51 -184
- package/src/server/deck-scope.mjs +52 -0
- package/src/server/desktop-update-routes.mjs +75 -0
- package/src/server/detach.mjs +78 -0
- package/src/server/discovery.mjs +298 -0
- package/src/server/event-log.mjs +328 -0
- package/src/server/event-pipeline.mjs +235 -0
- package/src/server/event-ring.mjs +214 -0
- package/src/server/event-routes.mjs +352 -0
- package/src/server/event-sink.mjs +26 -0
- package/src/server/exec-children.mjs +128 -0
- package/src/server/exec-not-found.mjs +213 -0
- package/src/server/exec-output.mjs +23 -0
- package/src/server/exec-spec.mjs +341 -0
- package/src/server/exec.mjs +74 -661
- package/src/server/health-route.mjs +42 -0
- package/src/server/hook-challenge.mjs +37 -0
- package/src/server/http-io.mjs +104 -0
- package/src/server/index.mjs +172 -7620
- package/src/server/install-layout.mjs +506 -0
- package/src/server/installer.mjs +79 -608
- package/src/server/jsonl-chunks.mjs +139 -0
- package/src/server/lan-accounts.mjs +27 -0
- package/src/server/lan-beacon.mjs +457 -0
- package/src/server/lan-call.mjs +239 -0
- package/src/server/lan-copies.mjs +270 -0
- package/src/server/lan-deck.mjs +583 -0
- package/src/server/lan-dials.mjs +161 -0
- package/src/server/lan-engine.mjs +475 -1024
- package/src/server/lan-hearing.mjs +49 -0
- package/src/server/lan-inbound.mjs +100 -0
- package/src/server/lan-invite-offer.mjs +77 -0
- package/src/server/lan-invite.mjs +207 -0
- package/src/server/lan-lines.mjs +73 -0
- package/src/server/lan-manifest.mjs +74 -0
- package/src/server/lan-reach.mjs +82 -69
- package/src/server/lan-requests.mjs +249 -0
- package/src/server/lan-round-record.mjs +45 -0
- package/src/server/lan-round-timer.mjs +62 -0
- package/src/server/lan-routes.mjs +143 -0
- package/src/server/lan-serve.mjs +112 -0
- package/src/server/lan-socket.mjs +243 -925
- package/src/server/lan-sync.mjs +58 -1026
- package/src/server/lan-tailnet-poll.mjs +57 -0
- package/src/server/lan-turns.mjs +80 -0
- package/src/server/lan-wire.mjs +620 -0
- package/src/server/lifecycle.mjs +421 -0
- package/src/server/listen.mjs +110 -0
- package/src/server/live-decks.mjs +138 -0
- package/src/server/load-average.mjs +32 -0
- package/src/server/log-election.mjs +235 -0
- package/src/server/log-replay.mjs +374 -0
- package/src/server/log-writer.mjs +72 -557
- package/src/server/login-service.mjs +3 -2
- package/src/server/macmon.mjs +1 -1
- package/src/server/memory-files.mjs +163 -0
- package/src/server/memory-metrics.mjs +178 -0
- package/src/server/metrics-history.mjs +94 -0
- package/src/server/metrics-run.mjs +100 -0
- package/src/server/music-routes.mjs +134 -0
- package/src/server/network-metrics.mjs +5 -3
- package/src/server/network-sampler.mjs +256 -0
- package/src/server/npm-latest.mjs +385 -0
- package/src/server/npm-upgrade.mjs +352 -0
- package/src/server/npx.mjs +41 -7
- package/src/server/output-watch.mjs +5 -4
- package/src/server/pinned-build.mjs +108 -0
- package/src/server/prefs-path.mjs +14 -0
- package/src/server/prefs-refusal.mjs +77 -0
- package/src/server/prefs-routes.mjs +76 -0
- package/src/server/prefs-set-aside.mjs +86 -0
- package/src/server/prefs-state.mjs +51 -0
- package/src/server/process-command.mjs +104 -0
- package/src/server/process-list.mjs +547 -0
- package/src/server/pulse-line.mjs +132 -0
- package/src/server/purge-key.mjs +7 -7
- package/src/server/quota-cli.mjs +174 -0
- package/src/server/quota-oauth.mjs +190 -0
- package/src/server/quota-shape.mjs +229 -0
- package/src/server/quota-store-resets.mjs +206 -0
- package/src/server/quota.mjs +123 -509
- package/src/server/relay-guard.mjs +3 -3
- package/src/server/request-gates.mjs +441 -0
- package/src/server/restart-note.mjs +180 -0
- package/src/server/retire-sound-hook.mjs +3 -3
- package/src/server/ring-bounds.mjs +215 -0
- package/src/server/running-deck.mjs +1 -1
- package/src/server/running-version.mjs +24 -0
- package/src/server/self-update.mjs +39 -1371
- package/src/server/session-enrichment.mjs +622 -0
- package/src/server/session-read-gate.mjs +49 -0
- package/src/server/session-tracking.mjs +242 -0
- package/src/server/sse-clients.mjs +222 -0
- package/src/server/stale-discovery.mjs +30 -0
- package/src/server/static-serve.mjs +81 -0
- package/src/server/supervisor.mjs +73 -12
- package/src/server/system-metrics.mjs +169 -1935
- package/src/server/term.mjs +11 -249
- package/src/server/thermal-metrics.mjs +520 -0
- package/src/server/thermal-sampler.mjs +204 -0
- package/src/server/token-redact.mjs +123 -0
- package/src/server/transcript-gate.mjs +107 -0
- package/src/server/transcript-scan.mjs +563 -0
- package/src/server/usage-routes.mjs +93 -0
- package/src/server/uv-bootstrap.mjs +1 -1
- package/src/server/wordmark.mjs +127 -0
- package/dist/web/assets/BrowserWatchModal-CyAFTvAY.js +0 -1
- package/dist/web/assets/UsageHistoryModal-KJfli0oa.js +0 -1
- package/dist/web/assets/index-Ck2RAla3.css +0 -1
- package/dist/web/assets/index-jmrcEsCj.js +0 -298
|
@@ -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
|
-
|
|
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
|
|
405
|
-
//
|
|
406
|
-
//
|
|
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).
|