instar 1.3.121 → 1.3.123

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 (53) hide show
  1. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  2. package/dist/core/PostUpdateMigrator.js +16 -0
  3. package/dist/core/PostUpdateMigrator.js.map +1 -1
  4. package/dist/core/SessionManager.d.ts +10 -0
  5. package/dist/core/SessionManager.d.ts.map +1 -1
  6. package/dist/core/SessionManager.js +34 -2
  7. package/dist/core/SessionManager.js.map +1 -1
  8. package/dist/core/TopicResumeMap.d.ts.map +1 -1
  9. package/dist/core/TopicResumeMap.js +18 -2
  10. package/dist/core/TopicResumeMap.js.map +1 -1
  11. package/dist/messaging/relayContentDedup.d.ts +51 -0
  12. package/dist/messaging/relayContentDedup.d.ts.map +1 -0
  13. package/dist/messaging/relayContentDedup.js +0 -0
  14. package/dist/messaging/relayContentDedup.js.map +1 -0
  15. package/dist/monitoring/frameworkActivitySignals.d.ts.map +1 -1
  16. package/dist/monitoring/frameworkActivitySignals.js +12 -1
  17. package/dist/monitoring/frameworkActivitySignals.js.map +1 -1
  18. package/dist/providers/adapters/openai-codex/observability/codexModelSwapPolicy.d.ts +83 -0
  19. package/dist/providers/adapters/openai-codex/observability/codexModelSwapPolicy.d.ts.map +1 -0
  20. package/dist/providers/adapters/openai-codex/observability/codexModelSwapPolicy.js +93 -0
  21. package/dist/providers/adapters/openai-codex/observability/codexModelSwapPolicy.js.map +1 -0
  22. package/dist/providers/adapters/openai-codex/observability/codexRateLimitReader.d.ts +95 -0
  23. package/dist/providers/adapters/openai-codex/observability/codexRateLimitReader.d.ts.map +1 -0
  24. package/dist/providers/adapters/openai-codex/observability/codexRateLimitReader.js +170 -0
  25. package/dist/providers/adapters/openai-codex/observability/codexRateLimitReader.js.map +1 -0
  26. package/dist/providers/adapters/openai-codex/observability/sessionPaths.d.ts +10 -0
  27. package/dist/providers/adapters/openai-codex/observability/sessionPaths.d.ts.map +1 -1
  28. package/dist/providers/adapters/openai-codex/observability/sessionPaths.js +44 -1
  29. package/dist/providers/adapters/openai-codex/observability/sessionPaths.js.map +1 -1
  30. package/dist/scaffold/templates.d.ts.map +1 -1
  31. package/dist/scaffold/templates.js +5 -0
  32. package/dist/scaffold/templates.js.map +1 -1
  33. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  34. package/dist/server/CapabilityIndex.js +11 -0
  35. package/dist/server/CapabilityIndex.js.map +1 -1
  36. package/dist/server/routes.d.ts.map +1 -1
  37. package/dist/server/routes.js +58 -0
  38. package/dist/server/routes.js.map +1 -1
  39. package/dist/threadline/ThreadResumeMap.d.ts.map +1 -1
  40. package/dist/threadline/ThreadResumeMap.js +19 -7
  41. package/dist/threadline/ThreadResumeMap.js.map +1 -1
  42. package/package.json +1 -1
  43. package/scripts/instar-dev-precommit.js +10 -1
  44. package/src/data/builtin-manifest.json +63 -63
  45. package/src/scaffold/templates.ts +5 -0
  46. package/upgrades/1.3.122.md +86 -0
  47. package/upgrades/1.3.123.md +105 -0
  48. package/upgrades/side-effects/codex-exec-json-silence-signal.md +64 -0
  49. package/upgrades/side-effects/codex-model-swap.md +134 -0
  50. package/upgrades/side-effects/codex-resume-jsonl-exists.md +68 -0
  51. package/upgrades/side-effects/codex-usage-visibility.md +107 -0
  52. package/upgrades/side-effects/ship-gate-sha-mismatch-error.md +54 -0
  53. package/upgrades/side-effects/threadline-relay-content-dedup.md +93 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * relayContentDedup — content-hash dedup for the agent-to-agent `relay-agent`
3
+ * ingress path.
4
+ *
5
+ * The id-based dedup in MessageRouter.relay keys on `message.id`. That misses a
6
+ * specific failure mode observed 2026-05-30: a SENDER whose HTTP call blocks on
7
+ * the RECEIVER's session spawn (slow on a loaded box) times out, then RETRIES
8
+ * the same message with a FRESH `message.id`. The retry slips past the id-based
9
+ * dedup → the receiver spawns/resumes and replies twice (the duplicate-reply
10
+ * bug — saw 6 doubled replies in one evening).
11
+ *
12
+ * This layer recognizes the retry by the STABLE triple
13
+ * (senderAgent, threadId, normalized content) within a short window, regardless
14
+ * of the message id. It is a bounded, in-memory, fixed-window dedup:
15
+ * - fresh (sender, thread, content) → process + record
16
+ * - identical within `ttlMs` of first-seen → drop (duplicate retry)
17
+ * - identical `ttlMs`+ after first-seen → process (plausibly a real re-send)
18
+ *
19
+ * Memory is bounded by `maxEntries` (oldest-evicted) plus a lazy sweep of
20
+ * expired keys on each call. The window is intentionally short so it only ever
21
+ * collapses true rapid retries, never two genuinely-distinct sends.
22
+ */
23
+ export interface RelayContentDedupOpts {
24
+ /** Window from first-seen during which an identical inbound is a duplicate. */
25
+ ttlMs?: number;
26
+ /** Hard cap on retained keys (oldest evicted past this). */
27
+ maxEntries?: number;
28
+ /** Injectable clock for deterministic tests. */
29
+ now?: () => number;
30
+ }
31
+ export declare class RelayContentDedup {
32
+ private readonly ttlMs;
33
+ private readonly maxEntries;
34
+ private readonly now;
35
+ /** key -> firstSeen epoch ms (Map preserves insertion order for eviction). */
36
+ private readonly seen;
37
+ constructor(opts?: RelayContentDedupOpts);
38
+ /** Stable dedup key for an inbound relay message. */
39
+ static keyFor(senderAgent: string, threadId: string, content: string): string;
40
+ /**
41
+ * Returns true if this inbound should be processed (fresh, or the prior
42
+ * sighting has aged past the window); false if it is a duplicate retry seen
43
+ * within the window. Fresh keys are recorded. Safe to call on every inbound.
44
+ */
45
+ shouldProcess(senderAgent: string, threadId: string, content: string): boolean;
46
+ /** Number of retained keys (test/observability aid). */
47
+ size(): number;
48
+ private sweep;
49
+ private enforceCap;
50
+ }
51
+ //# sourceMappingURL=relayContentDedup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relayContentDedup.d.ts","sourceRoot":"","sources":["../../src/messaging/relayContentDedup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,8EAA8E;IAC9E,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6B;gBAEtC,IAAI,GAAE,qBAA0B;IAM5C,qDAAqD;IACrD,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAU7E;;;;OAIG;IACH,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO;IAgB9E,wDAAwD;IACxD,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,KAAK;IAOb,OAAO,CAAC,UAAU;CASnB"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relayContentDedup.js","sourceRoot":"","sources":["../../src/messaging/relayContentDedup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,MAAM,MAAM,aAAa,CAAC;AAWjC,MAAM,OAAO,iBAAiB;IACX,KAAK,CAAS;IACd,UAAU,CAAS;IACnB,GAAG,CAAe;IACnC,8EAA8E;IAC7D,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IAElD,YAAY,OAA8B,EAAE;QAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;QAClC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC;QAC1C,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IAClC,CAAC;IAED,qDAAqD;IACrD,MAAM,CAAC,MAAM,CAAC,WAAmB,EAAE,QAAgB,EAAE,OAAe;QAClE,oEAAoE;QACpE,8CAA8C;QAC9C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACvD,OAAO,MAAM;aACV,UAAU,CAAC,QAAQ,CAAC;aACpB,MAAM,CAAC,GAAG,WAAW,IAAI,QAAQ,IAAI,UAAU,EAAE,CAAC;aAClD,MAAM,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IAED;;;;OAIG;IACH,aAAa,CAAC,WAAmB,EAAE,QAAgB,EAAE,OAAe;QAClE,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACrB,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACd,MAAM,GAAG,GAAG,iBAAiB,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QACrE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YAClD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,4EAA4E;QAC5E,4EAA4E;QAC5E,wEAAwE;QACxE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACtB,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,wDAAwD;IACxD,IAAI;QACF,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;IAEO,KAAK,CAAC,CAAS;QACrB,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QACjC,KAAK,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAChC,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,KAAK;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAEO,UAAU;QAChB,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC;QAClD,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACjC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACpB,IAAI,EAAE,CAAC,IAAI,QAAQ;gBAAE,MAAM;QAC7B,CAAC;IACH,CAAC;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"frameworkActivitySignals.d.ts","sourceRoot":"","sources":["../../src/monitoring/frameworkActivitySignals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAEpF,MAAM,WAAW,uBAAuB;IACtC,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACvC;AA2CD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,GAAG,uBAAuB,CAU9G;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,aAAa,CAAC;IAAE,SAAS,EAAE,qBAAqB,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,CAAC,CAK1H"}
1
+ {"version":3,"file":"frameworkActivitySignals.d.ts","sourceRoot":"","sources":["../../src/monitoring/frameworkActivitySignals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAEpF,MAAM,WAAW,uBAAuB;IACtC,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACvC;AAsDD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,GAAG,uBAAuB,CAU9G;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,aAAa,CAAC;IAAE,SAAS,EAAE,qBAAqB,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,CAAC,CAK1H"}
@@ -43,7 +43,18 @@ const CODEX_CLI_SIGNAL = {
43
43
  // proxy default to "still working" forever (the 2026-05-23 stuck-session
44
44
  // incident). The placeholder prompt "Find and fix a bug in @filename" is
45
45
  // likewise an IDLE indicator, not work.
46
- toolCallOrSpinner: /Working\s*\(\d+\s*s|•\s*Ran\b|exec\(|shell\(|apply_patch\(|⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏|\bgenerating\b/i,
46
+ //
47
+ // codex EXEC --json mode (jobs, autonomous spawns) emits a JSON EVENT STREAM,
48
+ // not the TUI status line: {"type":"thread.started"}, {"type":"turn.started"},
49
+ // {"type":"item.started"|"item.completed"|"item.updated"}, etc. None of the
50
+ // TUI patterns above match that, so a working `codex exec --json` session read
51
+ // as NOT active → marked paused → skipped by ActiveWorkSilenceSentinel. That
52
+ // hid a genuinely-wedged exec-json job (frozen mid-turn ~8.5h) from the
53
+ // silence watchdog. Match the event-stream namespaces so an exec-json session
54
+ // that streamed events THEN froze is silence-eligible (the OutputActivity-
55
+ // Tracker's observed-change requirement still gates "frozen before we watched").
56
+ // These are structured JSON markers, not idle status text.
57
+ toolCallOrSpinner: /Working\s*\(\d+\s*s|•\s*Ran\b|exec\(|shell\(|apply_patch\(|⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏|\bgenerating\b|"type":\s*"(thread|turn|item)\./i,
47
58
  // Codex shows a BARE "esc to interrupt" (no "press"/"hit" prefix) inside
48
59
  // its working status line, so the Claude-style prefixed pattern never
49
60
  // matched a real Codex pane. Match the bare form.
@@ -1 +1 @@
1
- {"version":3,"file":"frameworkActivitySignals.js","sourceRoot":"","sources":["../../src/monitoring/frameworkActivitySignals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA+BH,MAAM,kBAAkB,GAA4B;IAClD,WAAW,EAAE,aAAa;IAC1B,kEAAkE;IAClE,wCAAwC;IACxC,iBAAiB,EAAE,uEAAuE;IAC1F,iBAAiB,EAAE,mBAAmB;IACtC,gBAAgB,EAAE,cAAc;IAChC,oBAAoB,EAClB,+JAA+J;CAClK,CAAC;AAEF,MAAM,gBAAgB,GAA4B;IAChD,WAAW,EAAE,WAAW;IACxB,0EAA0E;IAC1E,sEAAsE;IACtE,uEAAuE;IACvE,oDAAoD;IACpD,EAAE;IACF,+DAA+D;IAC/D,gEAAgE;IAChE,uEAAuE;IACvE,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,wCAAwC;IACxC,iBAAiB,EAAE,gGAAgG;IACnH,yEAAyE;IACzE,sEAAsE;IACtE,kDAAkD;IAClD,iBAAiB,EAAE,mBAAmB;IACtC,gBAAgB,EAAE,gEAAgE;IAClF,oBAAoB,EAClB,kRAAkR;CACrR,CAAC;AAEF,MAAM,gBAAgB,GAA2D;IAC/E,aAAa,EAAE,kBAAkB;IACjC,WAAW,EAAE,gBAAgB;CAC9B,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAmD;IACnF,IAAI,CAAC,SAAS;QAAE,OAAO,kBAAkB,CAAC;IAC1C,MAAM,MAAM,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC3C,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,kEAAkE;IAClE,gEAAgE;IAChE,kEAAkE;IAClE,kEAAkE;IAClE,6CAA6C;IAC7C,OAAO,kBAAkB,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAQ,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAA6B,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAClF,SAAS;QACT,MAAM,EAAE,gBAAgB,CAAC,SAAS,CAAC;KACpC,CAAC,CAAC,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"frameworkActivitySignals.js","sourceRoot":"","sources":["../../src/monitoring/frameworkActivitySignals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA+BH,MAAM,kBAAkB,GAA4B;IAClD,WAAW,EAAE,aAAa;IAC1B,kEAAkE;IAClE,wCAAwC;IACxC,iBAAiB,EAAE,uEAAuE;IAC1F,iBAAiB,EAAE,mBAAmB;IACtC,gBAAgB,EAAE,cAAc;IAChC,oBAAoB,EAClB,+JAA+J;CAClK,CAAC;AAEF,MAAM,gBAAgB,GAA4B;IAChD,WAAW,EAAE,WAAW;IACxB,0EAA0E;IAC1E,sEAAsE;IACtE,uEAAuE;IACvE,oDAAoD;IACpD,EAAE;IACF,+DAA+D;IAC/D,gEAAgE;IAChE,uEAAuE;IACvE,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,wCAAwC;IACxC,EAAE;IACF,8EAA8E;IAC9E,+EAA+E;IAC/E,4EAA4E;IAC5E,+EAA+E;IAC/E,6EAA6E;IAC7E,wEAAwE;IACxE,8EAA8E;IAC9E,2EAA2E;IAC3E,iFAAiF;IACjF,2DAA2D;IAC3D,iBAAiB,EAAE,gIAAgI;IACnJ,yEAAyE;IACzE,sEAAsE;IACtE,kDAAkD;IAClD,iBAAiB,EAAE,mBAAmB;IACtC,gBAAgB,EAAE,gEAAgE;IAClF,oBAAoB,EAClB,kRAAkR;CACrR,CAAC;AAEF,MAAM,gBAAgB,GAA2D;IAC/E,aAAa,EAAE,kBAAkB;IACjC,WAAW,EAAE,gBAAgB;CAC9B,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAmD;IACnF,IAAI,CAAC,SAAS;QAAE,OAAO,kBAAkB,CAAC;IAC1C,MAAM,MAAM,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC3C,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,kEAAkE;IAClE,gEAAgE;IAChE,kEAAkE;IAClE,kEAAkE;IAClE,6CAA6C;IAC7C,OAAO,kBAAkB,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAQ,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAA6B,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAClF,SAAS;QACT,MAAM,EAAE,gBAAgB,CAAC,SAAS,CAAC;KACpC,CAAC,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Codex rate-limit model-swap policy.
3
+ *
4
+ * Justin's ask (2026-05-30): "swap to GPT-5.3-Codex-Spark when the other usage
5
+ * hits the limit." Codex's primary model (e.g. gpt-5.5) and a fallback like
6
+ * Codex-Spark draw on SEPARATE account quota buckets, so when the main model's
7
+ * weekly window is exhausted, launching the next session on the fallback keeps
8
+ * the agent working instead of stalling on a depleted window.
9
+ *
10
+ * This module is the DECISION; it holds no authority of its own. The pure
11
+ * `resolveCodexLaunchModel` takes a usage snapshot (from `readLatestCodexUsage`,
12
+ * the authoritative on-disk rate-limit reader) and the operator config and
13
+ * returns which model the NEXT codex session should launch with. A running
14
+ * session can't change model mid-turn, so the swap naturally applies at launch.
15
+ *
16
+ * Safe-by-default: ships DARK. With `enabled` false (the default) or no
17
+ * `fallbackModel` set, it is a no-op and adds zero overhead to the spawn path.
18
+ * The fallback model id is OPERATOR CONFIG — never hardcoded — because the exact
19
+ * Codex-Spark `--model` string + its subscription availability is the account
20
+ * owner's to confirm (it is not in instar's probed model list).
21
+ *
22
+ * RULE 3.1 RATIONALE
23
+ * Criticality: medium (cost-routing; never blocks a launch)
24
+ * Frequency: per codex session launch, only when enabled
25
+ * Stability: stable (pure decision over a structured snapshot)
26
+ * Fallback: no swap (launch with the requested model) on any uncertainty
27
+ * Verdict: pure policy + best-effort usage read; unit-tested both sides
28
+ */
29
+ import type { CodexUsageSnapshot } from './codexRateLimitReader.js';
30
+ /** Operator config for the swap, read from `.instar/config.json` → codex.rateLimitModelSwap. */
31
+ export interface CodexModelSwapConfig {
32
+ /** Master switch. Default false — the feature ships dark. */
33
+ enabled?: boolean;
34
+ /**
35
+ * The model to launch on when the main model's window is exhausted (e.g. the
36
+ * Codex-Spark model id). REQUIRED to swap — no default, because the exact id +
37
+ * subscription availability is the account owner's to confirm.
38
+ */
39
+ fallbackModel?: string;
40
+ /**
41
+ * Swap when the weekly (secondary) window's remaining percent is at or below
42
+ * this. Default 10. The authoritative `rate_limit_reached_type` flag also
43
+ * triggers a swap regardless of this threshold.
44
+ */
45
+ weeklyRemainingThreshold?: number;
46
+ }
47
+ export interface CodexModelSwapDecision {
48
+ /** The model the session should launch with (the request, unchanged, or the fallback). */
49
+ model: string | undefined;
50
+ /** True only when the policy actually substituted the fallback. */
51
+ swapped: boolean;
52
+ /** Human-readable reason when swapped, else null. */
53
+ reason: string | null;
54
+ }
55
+ export declare const DEFAULT_WEEKLY_REMAINING_THRESHOLD = 10;
56
+ /**
57
+ * Pure decision: given the framework, the requested model, the operator config,
58
+ * and the current codex usage snapshot, return which model to launch with.
59
+ * Never throws. Returns the requested model unchanged on any reason not to swap.
60
+ */
61
+ export declare function resolveCodexLaunchModel(params: {
62
+ framework: string;
63
+ requestedModel: string | undefined;
64
+ config: CodexModelSwapConfig | undefined;
65
+ usage: CodexUsageSnapshot | null;
66
+ }): CodexModelSwapDecision;
67
+ /**
68
+ * Best-effort async wrapper used on the spawn path. Reads the current codex
69
+ * usage (only when the feature is enabled — disabled = zero disk I/O) and
70
+ * applies {@link resolveCodexLaunchModel}. NEVER throws and NEVER blocks a
71
+ * launch: any read failure resolves to "no swap" (launch with the requested
72
+ * model). `readUsage` is injectable for tests.
73
+ */
74
+ export declare function resolveCodexLaunchModelWithUsage(params: {
75
+ framework: string;
76
+ requestedModel: string | undefined;
77
+ config: CodexModelSwapConfig | undefined;
78
+ codexHome?: string;
79
+ readUsage?: (opts: {
80
+ codexHome?: string;
81
+ }) => Promise<CodexUsageSnapshot | null>;
82
+ }): Promise<CodexModelSwapDecision>;
83
+ //# sourceMappingURL=codexModelSwapPolicy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codexModelSwapPolicy.d.ts","sourceRoot":"","sources":["../../../../../src/providers/adapters/openai-codex/observability/codexModelSwapPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAGpE,gGAAgG;AAChG,MAAM,WAAW,oBAAoB;IACnC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,MAAM,WAAW,sBAAsB;IACrC,0FAA0F;IAC1F,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,mEAAmE;IACnE,OAAO,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,eAAO,MAAM,kCAAkC,KAAK,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,MAAM,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACzC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAClC,GAAG,sBAAsB,CA2BzB;AAED;;;;;;GAMG;AACH,wBAAsB,gCAAgC,CAAC,MAAM,EAAE;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,MAAM,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;CAClF,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAiBlC"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Codex rate-limit model-swap policy.
3
+ *
4
+ * Justin's ask (2026-05-30): "swap to GPT-5.3-Codex-Spark when the other usage
5
+ * hits the limit." Codex's primary model (e.g. gpt-5.5) and a fallback like
6
+ * Codex-Spark draw on SEPARATE account quota buckets, so when the main model's
7
+ * weekly window is exhausted, launching the next session on the fallback keeps
8
+ * the agent working instead of stalling on a depleted window.
9
+ *
10
+ * This module is the DECISION; it holds no authority of its own. The pure
11
+ * `resolveCodexLaunchModel` takes a usage snapshot (from `readLatestCodexUsage`,
12
+ * the authoritative on-disk rate-limit reader) and the operator config and
13
+ * returns which model the NEXT codex session should launch with. A running
14
+ * session can't change model mid-turn, so the swap naturally applies at launch.
15
+ *
16
+ * Safe-by-default: ships DARK. With `enabled` false (the default) or no
17
+ * `fallbackModel` set, it is a no-op and adds zero overhead to the spawn path.
18
+ * The fallback model id is OPERATOR CONFIG — never hardcoded — because the exact
19
+ * Codex-Spark `--model` string + its subscription availability is the account
20
+ * owner's to confirm (it is not in instar's probed model list).
21
+ *
22
+ * RULE 3.1 RATIONALE
23
+ * Criticality: medium (cost-routing; never blocks a launch)
24
+ * Frequency: per codex session launch, only when enabled
25
+ * Stability: stable (pure decision over a structured snapshot)
26
+ * Fallback: no swap (launch with the requested model) on any uncertainty
27
+ * Verdict: pure policy + best-effort usage read; unit-tested both sides
28
+ */
29
+ import { readLatestCodexUsage } from './codexRateLimitReader.js';
30
+ export const DEFAULT_WEEKLY_REMAINING_THRESHOLD = 10;
31
+ /**
32
+ * Pure decision: given the framework, the requested model, the operator config,
33
+ * and the current codex usage snapshot, return which model to launch with.
34
+ * Never throws. Returns the requested model unchanged on any reason not to swap.
35
+ */
36
+ export function resolveCodexLaunchModel(params) {
37
+ const { framework, requestedModel, config, usage } = params;
38
+ const noSwap = { model: requestedModel, swapped: false, reason: null };
39
+ if (framework !== 'codex-cli')
40
+ return noSwap;
41
+ if (!config?.enabled)
42
+ return noSwap;
43
+ const fallback = config.fallbackModel;
44
+ if (!fallback)
45
+ return noSwap; // nothing to swap to — operator hasn't set the id
46
+ // Already on the fallback → don't re-swap (no-op / avoids a confusing log).
47
+ if (requestedModel && requestedModel === fallback)
48
+ return noSwap;
49
+ if (!usage)
50
+ return noSwap; // couldn't read usage → conservative: launch as requested
51
+ const threshold = config.weeklyRemainingThreshold ?? DEFAULT_WEEKLY_REMAINING_THRESHOLD;
52
+ const reached = usage.rateLimitReachedType; // authoritative "a window is hit NOW"
53
+ const weeklyRemaining = usage.secondary?.remainingPercent;
54
+ const reachedHit = reached !== null && reached !== undefined;
55
+ const weeklyLow = typeof weeklyRemaining === 'number' && weeklyRemaining <= threshold;
56
+ if (!reachedHit && !weeklyLow)
57
+ return noSwap;
58
+ return {
59
+ model: fallback,
60
+ swapped: true,
61
+ reason: reachedHit
62
+ ? `codex rate_limit_reached_type=${reached}; swapping launch model to ${fallback}`
63
+ : `codex weekly window at ${weeklyRemaining}% remaining (<= ${threshold}%); swapping launch model to ${fallback}`,
64
+ };
65
+ }
66
+ /**
67
+ * Best-effort async wrapper used on the spawn path. Reads the current codex
68
+ * usage (only when the feature is enabled — disabled = zero disk I/O) and
69
+ * applies {@link resolveCodexLaunchModel}. NEVER throws and NEVER blocks a
70
+ * launch: any read failure resolves to "no swap" (launch with the requested
71
+ * model). `readUsage` is injectable for tests.
72
+ */
73
+ export async function resolveCodexLaunchModelWithUsage(params) {
74
+ const { framework, requestedModel, config, codexHome } = params;
75
+ const noSwap = { model: requestedModel, swapped: false, reason: null };
76
+ // Fast paths that avoid any disk read when the feature can't act.
77
+ if (framework !== 'codex-cli')
78
+ return noSwap;
79
+ if (!config?.enabled || !config.fallbackModel)
80
+ return noSwap;
81
+ if (requestedModel && requestedModel === config.fallbackModel)
82
+ return noSwap;
83
+ const read = params.readUsage ?? readLatestCodexUsage;
84
+ let usage = null;
85
+ try {
86
+ usage = await read({ codexHome });
87
+ }
88
+ catch {
89
+ return noSwap; // best-effort: never block a launch on a usage read
90
+ }
91
+ return resolveCodexLaunchModel({ framework, requestedModel, config, usage });
92
+ }
93
+ //# sourceMappingURL=codexModelSwapPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codexModelSwapPolicy.js","sourceRoot":"","sources":["../../../../../src/providers/adapters/openai-codex/observability/codexModelSwapPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AA6BjE,MAAM,CAAC,MAAM,kCAAkC,GAAG,EAAE,CAAC;AAErD;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAKvC;IACC,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IAC5D,MAAM,MAAM,GAA2B,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAE/F,IAAI,SAAS,KAAK,WAAW;QAAE,OAAO,MAAM,CAAC;IAC7C,IAAI,CAAC,MAAM,EAAE,OAAO;QAAE,OAAO,MAAM,CAAC;IACpC,MAAM,QAAQ,GAAG,MAAM,CAAC,aAAa,CAAC;IACtC,IAAI,CAAC,QAAQ;QAAE,OAAO,MAAM,CAAC,CAAC,kDAAkD;IAChF,4EAA4E;IAC5E,IAAI,cAAc,IAAI,cAAc,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IACjE,IAAI,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC,CAAC,0DAA0D;IAErF,MAAM,SAAS,GAAG,MAAM,CAAC,wBAAwB,IAAI,kCAAkC,CAAC;IACxF,MAAM,OAAO,GAAG,KAAK,CAAC,oBAAoB,CAAC,CAAC,sCAAsC;IAClF,MAAM,eAAe,GAAG,KAAK,CAAC,SAAS,EAAE,gBAAgB,CAAC;IAE1D,MAAM,UAAU,GAAG,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,CAAC;IAC7D,MAAM,SAAS,GAAG,OAAO,eAAe,KAAK,QAAQ,IAAI,eAAe,IAAI,SAAS,CAAC;IACtF,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS;QAAE,OAAO,MAAM,CAAC;IAE7C,OAAO;QACL,KAAK,EAAE,QAAQ;QACf,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,UAAU;YAChB,CAAC,CAAC,iCAAiC,OAAO,8BAA8B,QAAQ,EAAE;YAClF,CAAC,CAAC,0BAA0B,eAAe,mBAAmB,SAAS,gCAAgC,QAAQ,EAAE;KACpH,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gCAAgC,CAAC,MAMtD;IACC,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAChE,MAAM,MAAM,GAA2B,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAE/F,kEAAkE;IAClE,IAAI,SAAS,KAAK,WAAW;QAAE,OAAO,MAAM,CAAC;IAC7C,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,MAAM,CAAC,aAAa;QAAE,OAAO,MAAM,CAAC;IAC7D,IAAI,cAAc,IAAI,cAAc,KAAK,MAAM,CAAC,aAAa;QAAE,OAAO,MAAM,CAAC;IAE7E,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,IAAI,oBAAoB,CAAC;IACtD,IAAI,KAAK,GAA8B,IAAI,CAAC;IAC5C,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,CAAC,oDAAoD;IACrE,CAAC;IACD,OAAO,uBAAuB,CAAC,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Reads the codex CLI's `/status`-equivalent rate-limit usage from the
3
+ * on-disk rollout JSONL stream — WITHOUT the interactive TUI.
4
+ *
5
+ * Codex has no public usage endpoint (the UsageMeterProvider falls back to
6
+ * local token accounting and reports isAuthoritative()=false). But the codex
7
+ * CLI DOES persist the authoritative account rate-limit windows it gets back
8
+ * from OpenAI: every turn it appends a `token_count` event to the session
9
+ * rollout whose `payload.rate_limits` carries the same primary (5h) /
10
+ * secondary (weekly) windows the TUI's `/status` shows:
11
+ *
12
+ * { "type": "event_msg",
13
+ * "payload": {
14
+ * "type": "token_count",
15
+ * "rate_limits": {
16
+ * "limit_id": "codex",
17
+ * "primary": { "used_percent": 13, "window_minutes": 300, "resets_at": 1780171524 },
18
+ * "secondary": { "used_percent": 93, "window_minutes": 10080, "resets_at": 1780174809 },
19
+ * "plan_type": "prolite",
20
+ * "rate_limit_reached_type": null } } }
21
+ *
22
+ * This reader finds the newest rollout(s), reads only the tail (rate-limit
23
+ * events are appended per-turn, so the freshest is near the end), and returns
24
+ * the most recent snapshot. It is read-only and never mutates any session
25
+ * state — the same discipline as the rest of the codex observability layer.
26
+ *
27
+ * RULE 3.1 RATIONALE
28
+ * Criticality: high (cost-routing / model-swap input)
29
+ * Frequency: per-poll / on-demand (route hit)
30
+ * Stability: semi-stable (codex rollout layout changes occasionally —
31
+ * shared with sessionPaths, covered by codexSessionLayoutCanary)
32
+ * Fallback: return null when no rollout / no token_count event is found
33
+ * Verdict: deterministic tail-scan; canary covers the layout
34
+ */
35
+ /** A single rate-limit window (5h primary or weekly secondary). */
36
+ export interface CodexRateWindow {
37
+ /** Percent of the window's budget consumed (0-100). */
38
+ usedPercent: number;
39
+ /** Convenience: 100 - usedPercent, clamped to [0, 100]. */
40
+ remainingPercent: number;
41
+ /** Window length in minutes (300 = 5h, 10080 = weekly). */
42
+ windowMinutes: number;
43
+ /** Absolute reset time, unix epoch seconds (as codex reports it). */
44
+ resetsAt: number;
45
+ /** Derived ISO-8601 of `resetsAt`, or null if `resetsAt` is missing/invalid. */
46
+ resetsAtIso: string | null;
47
+ /** Seconds until reset relative to the provided clock, or null when no clock. */
48
+ resetsInSeconds: number | null;
49
+ }
50
+ /** A point-in-time snapshot of codex account rate-limit usage. */
51
+ export interface CodexUsageSnapshot {
52
+ source: 'codex-rollout';
53
+ /** The rollout file the snapshot was read from. */
54
+ rolloutPath: string;
55
+ /** Thread UUID parsed from the rollout filename, or null. */
56
+ threadId: string | null;
57
+ /** Timestamp of the token_count event the snapshot came from (ISO), or null. */
58
+ capturedAt: string | null;
59
+ /** Model in effect at capture (best-effort from the latest turn_context), or null. */
60
+ model: string | null;
61
+ /** Plan tier codex reports (e.g. "plus", "prolite"), or null. */
62
+ planType: string | null;
63
+ /**
64
+ * Which window (if any) is currently exhausted. codex sets this to e.g.
65
+ * "primary" / "secondary" when a limit is hit; null when usage is fine.
66
+ * This is the authoritative "we are rate-limited" signal.
67
+ */
68
+ rateLimitReachedType: string | null;
69
+ /** The 5h rolling window (window_minutes 300), or null if absent. */
70
+ primary: CodexRateWindow | null;
71
+ /** The weekly rolling window (window_minutes 10080), or null if absent. */
72
+ secondary: CodexRateWindow | null;
73
+ }
74
+ export interface ReadCodexUsageOptions {
75
+ /** Override $CODEX_HOME (defaults to ~/.codex). */
76
+ codexHome?: string;
77
+ /** Clock for deriving `resetsInSeconds` (defaults to Date.now()). */
78
+ nowMs?: number;
79
+ /** How many newest rollouts to scan for a token_count event (default 8). */
80
+ maxRolloutsScanned?: number;
81
+ /** Tail size to read from each rollout (default 512 KiB). */
82
+ tailBytes?: number;
83
+ }
84
+ /**
85
+ * Read the freshest codex rate-limit snapshot from disk. Returns null when
86
+ * there is no codex rollout with a rate-limit-bearing token_count event
87
+ * (e.g. a pure-Claude agent, or a session that has not completed a turn yet).
88
+ */
89
+ export declare function readLatestCodexUsage(opts?: ReadCodexUsageOptions): Promise<CodexUsageSnapshot | null>;
90
+ /**
91
+ * Parse the freshest rate-limit snapshot out of a rollout tail. Exported for
92
+ * unit tests so they can exercise the parser without touching disk.
93
+ */
94
+ export declare function parseUsageFromTail(tail: string, rolloutPath: string, nowMs: number): CodexUsageSnapshot | null;
95
+ //# sourceMappingURL=codexRateLimitReader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codexRateLimitReader.d.ts","sourceRoot":"","sources":["../../../../../src/providers/adapters/openai-codex/observability/codexRateLimitReader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iFAAiF;IACjF,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,kEAAkE;AAClE,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,eAAe,CAAC;IACxB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,gFAAgF;IAChF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,sFAAsF;IACtF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iEAAiE;IACjE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;OAIG;IACH,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,qEAAqE;IACrE,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAChC,2EAA2E;IAC3E,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,qBAAqB;IACpC,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAiBpC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACZ,kBAAkB,GAAG,IAAI,CA8C3B"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Reads the codex CLI's `/status`-equivalent rate-limit usage from the
3
+ * on-disk rollout JSONL stream — WITHOUT the interactive TUI.
4
+ *
5
+ * Codex has no public usage endpoint (the UsageMeterProvider falls back to
6
+ * local token accounting and reports isAuthoritative()=false). But the codex
7
+ * CLI DOES persist the authoritative account rate-limit windows it gets back
8
+ * from OpenAI: every turn it appends a `token_count` event to the session
9
+ * rollout whose `payload.rate_limits` carries the same primary (5h) /
10
+ * secondary (weekly) windows the TUI's `/status` shows:
11
+ *
12
+ * { "type": "event_msg",
13
+ * "payload": {
14
+ * "type": "token_count",
15
+ * "rate_limits": {
16
+ * "limit_id": "codex",
17
+ * "primary": { "used_percent": 13, "window_minutes": 300, "resets_at": 1780171524 },
18
+ * "secondary": { "used_percent": 93, "window_minutes": 10080, "resets_at": 1780174809 },
19
+ * "plan_type": "prolite",
20
+ * "rate_limit_reached_type": null } } }
21
+ *
22
+ * This reader finds the newest rollout(s), reads only the tail (rate-limit
23
+ * events are appended per-turn, so the freshest is near the end), and returns
24
+ * the most recent snapshot. It is read-only and never mutates any session
25
+ * state — the same discipline as the rest of the codex observability layer.
26
+ *
27
+ * RULE 3.1 RATIONALE
28
+ * Criticality: high (cost-routing / model-swap input)
29
+ * Frequency: per-poll / on-demand (route hit)
30
+ * Stability: semi-stable (codex rollout layout changes occasionally —
31
+ * shared with sessionPaths, covered by codexSessionLayoutCanary)
32
+ * Fallback: return null when no rollout / no token_count event is found
33
+ * Verdict: deterministic tail-scan; canary covers the layout
34
+ */
35
+ import { promises as fs } from 'node:fs';
36
+ import { listAllRollouts } from './sessionPaths.js';
37
+ const DEFAULT_TAIL_BYTES = 512 * 1024;
38
+ const DEFAULT_MAX_ROLLOUTS = 8;
39
+ const UUID_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
40
+ /**
41
+ * Read the freshest codex rate-limit snapshot from disk. Returns null when
42
+ * there is no codex rollout with a rate-limit-bearing token_count event
43
+ * (e.g. a pure-Claude agent, or a session that has not completed a turn yet).
44
+ */
45
+ export async function readLatestCodexUsage(opts = {}) {
46
+ const nowMs = opts.nowMs ?? Date.now();
47
+ const tailBytes = opts.tailBytes ?? DEFAULT_TAIL_BYTES;
48
+ const maxRollouts = opts.maxRolloutsScanned ?? DEFAULT_MAX_ROLLOUTS;
49
+ const rollouts = await listAllRollouts(opts.codexHome, maxRollouts);
50
+ for (const { path: rolloutPath } of rollouts) {
51
+ let tail;
52
+ try {
53
+ tail = await readTail(rolloutPath, tailBytes);
54
+ }
55
+ catch {
56
+ continue;
57
+ }
58
+ const snapshot = parseUsageFromTail(tail, rolloutPath, nowMs);
59
+ if (snapshot)
60
+ return snapshot;
61
+ }
62
+ return null;
63
+ }
64
+ /**
65
+ * Parse the freshest rate-limit snapshot out of a rollout tail. Exported for
66
+ * unit tests so they can exercise the parser without touching disk.
67
+ */
68
+ export function parseUsageFromTail(tail, rolloutPath, nowMs) {
69
+ const lines = tail.split('\n');
70
+ let rateLimits = null;
71
+ let capturedAt = null;
72
+ let model = null;
73
+ for (const line of lines) {
74
+ if (!line || line[0] !== '{')
75
+ continue;
76
+ // Cheap pre-filter before the JSON.parse cost.
77
+ const hasRate = line.includes('"rate_limits"');
78
+ const hasModel = line.includes('"turn_context"') && line.includes('"model"');
79
+ if (!hasRate && !hasModel)
80
+ continue;
81
+ let obj;
82
+ try {
83
+ obj = JSON.parse(line);
84
+ }
85
+ catch {
86
+ continue;
87
+ }
88
+ const payload = obj.payload;
89
+ if (!payload)
90
+ continue;
91
+ if (payload.type === 'token_count' && payload.rate_limits) {
92
+ rateLimits = payload.rate_limits;
93
+ capturedAt = typeof obj.timestamp === 'string' ? obj.timestamp : capturedAt;
94
+ }
95
+ else if (payload.type === undefined && obj.type === 'turn_context') {
96
+ // turn_context nests its fields directly under payload.
97
+ const m = payload.model;
98
+ if (typeof m === 'string' && m)
99
+ model = m;
100
+ }
101
+ }
102
+ if (!rateLimits)
103
+ return null;
104
+ return {
105
+ source: 'codex-rollout',
106
+ rolloutPath,
107
+ threadId: parseThreadId(rolloutPath),
108
+ capturedAt,
109
+ model,
110
+ planType: typeof rateLimits.plan_type === 'string' ? rateLimits.plan_type : null,
111
+ rateLimitReachedType: typeof rateLimits.rate_limit_reached_type === 'string'
112
+ ? rateLimits.rate_limit_reached_type
113
+ : null,
114
+ primary: parseWindow(rateLimits.primary, nowMs),
115
+ secondary: parseWindow(rateLimits.secondary, nowMs),
116
+ };
117
+ }
118
+ function parseWindow(raw, nowMs) {
119
+ if (!raw || typeof raw !== 'object')
120
+ return null;
121
+ const w = raw;
122
+ const usedPercent = typeof w.used_percent === 'number' ? w.used_percent : null;
123
+ const windowMinutes = typeof w.window_minutes === 'number' ? w.window_minutes : null;
124
+ const resetsAt = typeof w.resets_at === 'number' ? w.resets_at : null;
125
+ if (usedPercent === null || windowMinutes === null || resetsAt === null)
126
+ return null;
127
+ const validReset = Number.isFinite(resetsAt) && resetsAt > 0;
128
+ return {
129
+ usedPercent,
130
+ remainingPercent: clampPercent(100 - usedPercent),
131
+ windowMinutes,
132
+ resetsAt,
133
+ resetsAtIso: validReset ? new Date(resetsAt * 1000).toISOString() : null,
134
+ resetsInSeconds: validReset ? Math.round((resetsAt * 1000 - nowMs) / 1000) : null,
135
+ };
136
+ }
137
+ function clampPercent(n) {
138
+ if (n < 0)
139
+ return 0;
140
+ if (n > 100)
141
+ return 100;
142
+ return n;
143
+ }
144
+ function parseThreadId(rolloutPath) {
145
+ const m = UUID_RE.exec(rolloutPath);
146
+ return m ? m[1] : null;
147
+ }
148
+ async function readTail(filePath, maxBytes) {
149
+ const handle = await fs.open(filePath, 'r');
150
+ try {
151
+ const { size } = await handle.stat();
152
+ const start = size > maxBytes ? size - maxBytes : 0;
153
+ const length = size - start;
154
+ if (length <= 0)
155
+ return '';
156
+ const buf = Buffer.alloc(length);
157
+ await handle.read(buf, 0, length, start);
158
+ let text = buf.toString('utf8');
159
+ if (start > 0) {
160
+ // The first line is likely partial — drop up to the first newline.
161
+ const nl = text.indexOf('\n');
162
+ text = nl >= 0 ? text.slice(nl + 1) : '';
163
+ }
164
+ return text;
165
+ }
166
+ finally {
167
+ await handle.close();
168
+ }
169
+ }
170
+ //# sourceMappingURL=codexRateLimitReader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codexRateLimitReader.js","sourceRoot":"","sources":["../../../../../src/providers/adapters/openai-codex/observability/codexRateLimitReader.ts"],"names":[],"mappings":"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"}
@@ -15,6 +15,16 @@
15
15
  export declare function codexHomeFromConfig(codexHome?: string): string;
16
16
  /** Find a rollout JSONL file by Codex thread UUID. Returns null on miss. */
17
17
  export declare function findRolloutFile(threadId: string, codexHome?: string): Promise<string | null>;
18
+ /**
19
+ * Synchronous variant of {@link findRolloutFile}. Needed by callers that run on
20
+ * a sync code path and cannot await — notably the resume-map `jsonlExists`
21
+ * guards (ThreadResumeMap, TopicResumeMap), which previously checked only the
22
+ * Claude flat layout and so returned false for EVERY codex session (the
23
+ * codex-compat resume bug: every codex thread looked expired/missing). Returns
24
+ * the rollout path or null. A missing `$CODEX_HOME/sessions` (e.g. a pure
25
+ * Claude agent) returns null fast — `readdirSync` throws and is caught.
26
+ */
27
+ export declare function findRolloutFileSync(threadId: string, codexHome?: string): string | null;
18
28
  /** List all rollout files under $CODEX_HOME/sessions, newest first. */
19
29
  export declare function listAllRollouts(codexHome?: string, limit?: number): Promise<ReadonlyArray<{
20
30
  path: string;
@@ -1 +1 @@
1
- {"version":3,"file":"sessionPaths.d.ts","sourceRoot":"","sources":["../../../../../src/providers/adapters/openai-codex/observability/sessionPaths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,wBAAgB,mBAAmB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,4EAA4E;AAC5E,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAIxB;AA2BD,uEAAuE;AACvE,wBAAsB,eAAe,CACnC,SAAS,CAAC,EAAE,MAAM,EAClB,KAAK,SAAM,GACV,OAAO,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAMzD"}
1
+ {"version":3,"file":"sessionPaths.d.ts","sourceRoot":"","sources":["../../../../../src/providers/adapters/openai-codex/observability/sessionPaths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,wBAAgB,mBAAmB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,4EAA4E;AAC5E,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAIxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIvF;AAoDD,uEAAuE;AACvE,wBAAsB,eAAe,CACnC,SAAS,CAAC,EAAE,MAAM,EAClB,KAAK,SAAM,GACV,OAAO,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAMzD"}