codiedev 0.9.55 → 0.9.57

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/dist/hook.js CHANGED
@@ -44,6 +44,7 @@ const http = __importStar(require("http"));
44
44
  const child_process_1 = require("child_process");
45
45
  const utils_1 = require("./utils");
46
46
  const limitEventReporter_1 = require("./limitEventReporter");
47
+ const codexLimitScan_1 = require("./codexLimitScan");
47
48
  const version_1 = require("./version");
48
49
  const captureLog_1 = require("./captureLog");
49
50
  const captureExclusions_1 = require("./captureExclusions");
@@ -143,10 +144,96 @@ async function runStopFailure(hookInput) {
143
144
  await readAndPostLimitEvent(hookInput, "claude-code", true);
144
145
  process.exit(0);
145
146
  }
147
+ // Codex path. Two things differ from Claude Code:
148
+ //
149
+ // 1. There is no event that fires when a credit/rate-limit block kills a turn.
150
+ // Codex skips its Stop hook for exactly that turn (measured: every normal
151
+ // task_complete pairs with a Stop-hook run, the blocked one never does),
152
+ // and its HookEventName enum has no StopFailure equivalent. So the block is
153
+ // reported from the developer's NEXT prompt instead — see runCodexLimitSweep.
154
+ // 2. Rollouts run to tens of megabytes and keep growing after the block, so a
155
+ // fixed tail read loses the marker (in one real 44 MB rollout it sat 1.13 MB
156
+ // from the end). Scan backwards for it instead.
157
+ async function reportCodexLimitFromFile(config, transcriptPath, sessionId) {
158
+ const hit = (0, codexLimitScan_1.scanCodexRolloutForLimit)(transcriptPath);
159
+ if (!hit)
160
+ return false;
161
+ await (0, limitEventReporter_1.postLimitEvent)(config, {
162
+ source: "codex",
163
+ ...hit.event,
164
+ sessionId: sessionId ?? (0, codexLimitScan_1.sessionIdFromRolloutPath)(transcriptPath),
165
+ transcriptTail: (0, codexLimitScan_1.readTailEndingAt)(transcriptPath, hit.lineEnd),
166
+ });
167
+ // Tell the prompt sweep this one is done, so it neither re-scans the
168
+ // rollout nor re-posts an event the server would only deduplicate.
169
+ (0, codexLimitScan_1.recordCodexLimitReported)(transcriptPath, hit.event.at);
170
+ return true;
171
+ }
146
172
  async function runCodexLimit(hookInput) {
147
- await readAndPostLimitEvent(hookInput, "codex", false);
173
+ const config = (0, utils_1.readConfig)();
174
+ const transcriptPath = hookInput.transcript_path;
175
+ if (config && transcriptPath) {
176
+ try {
177
+ const sent = await reportCodexLimitFromFile(config, transcriptPath, hookInput.session_id);
178
+ if (sent) {
179
+ (0, captureLog_1.appendCaptureLog)({
180
+ stage: "limit",
181
+ sessionId: hookInput.session_id,
182
+ outcome: "ok",
183
+ detail: "source=codex trigger=stop",
184
+ });
185
+ }
186
+ }
187
+ catch (err) {
188
+ (0, captureLog_1.appendCaptureLog)({
189
+ stage: "limit",
190
+ sessionId: hookInput.session_id,
191
+ outcome: "ERROR",
192
+ detail: `source=codex trigger=stop ${err.message}`,
193
+ });
194
+ }
195
+ }
148
196
  process.exit(0);
149
197
  }
198
+ const PROMPT_LIMIT_POST_TIMEOUT_MS = 2_500;
199
+ // UserPromptSubmit: the first Codiedev hook that runs after a block, because
200
+ // Codex runs no hook at the block itself. Scans the recent rollouts for any
201
+ // block not yet reported. Cheap in the normal case — a per-file size cache
202
+ // means an unchanged, unblocked rollout costs one statSync.
203
+ async function runCodexLimitSweep(sessionId) {
204
+ const config = (0, utils_1.readConfig)();
205
+ if (!config)
206
+ return;
207
+ try {
208
+ const posted = await (0, codexLimitScan_1.sweepCodexLimits)({
209
+ sessionsDir: (0, codexLimitScan_1.codexSessionsDir)(),
210
+ // A shorter timeout than the default: this is on the user's prompt
211
+ // clock, and the next prompt retries anything that does not land.
212
+ post: (payload) => (0, limitEventReporter_1.postLimitEvent)(config, payload, PROMPT_LIMIT_POST_TIMEOUT_MS),
213
+ // Unchanged rollouts cost one statSync and do not spend the read
214
+ // budget, so in steady state this is nearly free. The module default
215
+ // covers a cold start; see DEFAULT_MAX_SCANS for why it is not small.
216
+ findOptions: { withinMs: 24 * 3600_000 },
217
+ scanOptions: { maxScanBytes: 4 * 1024 * 1024 },
218
+ });
219
+ if (posted > 0) {
220
+ (0, captureLog_1.appendCaptureLog)({
221
+ stage: "limit",
222
+ sessionId,
223
+ outcome: "ok",
224
+ detail: `source=codex trigger=prompt events=${posted}`,
225
+ });
226
+ }
227
+ }
228
+ catch (err) {
229
+ (0, captureLog_1.appendCaptureLog)({
230
+ stage: "limit",
231
+ sessionId,
232
+ outcome: "ERROR",
233
+ detail: `source=codex trigger=prompt ${err.message}`,
234
+ });
235
+ }
236
+ }
150
237
  // Skip-reason ping: this session is ending WITHOUT a capture, and here's why.
151
238
  // Turns silent coverage loss into per-reason buckets on the admin Capture
152
239
  // Health panel (patches the session's aiSessionStarts row server-side). Tiny
@@ -454,6 +541,11 @@ async function main() {
454
541
  if (process.argv[2] === "discover" ||
455
542
  process.argv[2] === "discover-codex" ||
456
543
  process.argv[2] === "discover-codie") {
544
+ // A Codex block leaves no hook of its own, so this prompt is the first
545
+ // chance to report it. Runs before runDiscover, which exits the process.
546
+ if (process.argv[2] === "discover-codex") {
547
+ await runCodexLimitSweep(hookInput.session_id);
548
+ }
457
549
  await runDiscover(hookInput.prompt);
458
550
  return;
459
551
  }
@@ -51,11 +51,36 @@ function parseLimitEventFromTranscript(transcript) {
51
51
  }
52
52
  return found;
53
53
  }
54
+ /**
55
+ * Codex's `rate_limit_reached_type` is exactly five values (read off the 0.154
56
+ * binary): `rate_limit_reached`, `workspace_{owner,member}_credits_depleted`
57
+ * and `workspace_{owner,member}_usage_limit_reached`.
58
+ *
59
+ * The split that matters to a reader of the dashboard is "someone must act on
60
+ * money" vs "this dev is throttled and it will reset" — so map on what the
61
+ * snapshot actually asserts, not on the word "credits":
62
+ *
63
+ * - A member out of their OWN allowance is a rate limit. Real snapshots carry
64
+ * `spend_control_reached: null` and `individual_limit: null` — nobody
65
+ * configured a budget, the window is simply used up. Calling that personal
66
+ * spend tells a manager the dev overspent, which is not what happened.
67
+ *
68
+ * Deliberately NOT gated on `individual_limit`: it has only ever been
69
+ * observed null, so its populated shape and meaning are unknown, and a
70
+ * wrong guess reinstates the mislabel this exists to remove. The reason
71
+ * string is the causal signal; `individual_limit` is at best correlative.
72
+ * - A member whose ADMIN capped them (`usage_limit_reached`) is personal spend:
73
+ * there really is a cap, and someone can raise it.
74
+ * - Anything owner-scoped is org spend — the org's allowance or credits are
75
+ * gone and somebody has to act.
76
+ */
54
77
  function codexLimitKind(reason, spendControlReached) {
55
78
  if (spendControlReached || reason.startsWith("workspace_owner_"))
56
79
  return "org_spend";
57
- if (reason.startsWith("workspace_member_"))
80
+ if (reason === "workspace_member_usage_limit_reached")
58
81
  return "user_spend";
82
+ if (reason === "workspace_member_credits_depleted")
83
+ return "session";
59
84
  return reason === "rate_limit_reached" ? "session" : "other";
60
85
  }
61
86
  function codexResetText(rateLimits) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.55",
3
+ "version": "0.9.57",
4
4
  "description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",