replicas-engine 0.1.132 → 0.1.134

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 (2) hide show
  1. package/dist/src/index.js +165 -30
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -967,6 +967,9 @@ import { homedir as homedir5 } from "os";
967
967
  import { exec } from "child_process";
968
968
  import { promisify as promisify2 } from "util";
969
969
 
970
+ // ../shared/src/event.ts
971
+ var CODEX_QUOTA_STATUS_EVENT_TYPE = "codex-quota-status";
972
+
970
973
  // ../shared/src/pricing.ts
971
974
  var PLANS = {
972
975
  hobby: {
@@ -1166,7 +1169,7 @@ function parseReplicasConfigString(content, filename) {
1166
1169
  }
1167
1170
 
1168
1171
  // ../shared/src/engine/environment.ts
1169
- var DAYTONA_SNAPSHOT_ID = "30-04-2026-islington-v1";
1172
+ var DAYTONA_SNAPSHOT_ID = "01-05-2026-royal-york-v1";
1170
1173
 
1171
1174
  // ../shared/src/engine/types.ts
1172
1175
  var DEFAULT_CHAT_TITLES = {
@@ -2828,12 +2831,40 @@ function isJsonlEvent2(value) {
2828
2831
  function sleep(ms) {
2829
2832
  return new Promise((resolve2) => setTimeout(resolve2, ms));
2830
2833
  }
2834
+ function extractRateLimitsSnapshot(parsed) {
2835
+ if (!isRecord(parsed)) return null;
2836
+ const payload = isRecord(parsed.payload) ? parsed.payload : parsed;
2837
+ const rateLimits = isRecord(payload.rate_limits) ? payload.rate_limits : null;
2838
+ if (!rateLimits) return null;
2839
+ const credits = isRecord(rateLimits.credits) ? rateLimits.credits : null;
2840
+ const hasCredits = credits && typeof credits.has_credits === "boolean" ? credits.has_credits : null;
2841
+ const unlimited = credits && typeof credits.unlimited === "boolean" ? credits.unlimited : null;
2842
+ const balance = credits && typeof credits.balance === "string" ? credits.balance : null;
2843
+ if (unlimited === true) return null;
2844
+ const rateLimitResetType = typeof rateLimits.rate_limit_reached_type === "string" && rateLimits.rate_limit_reached_type.length > 0 ? rateLimits.rate_limit_reached_type : null;
2845
+ const planType = typeof rateLimits.plan_type === "string" ? rateLimits.plan_type : null;
2846
+ let state = "ok";
2847
+ if (hasCredits === false) {
2848
+ state = "out_of_credits";
2849
+ } else if (rateLimitResetType !== null) {
2850
+ state = "rate_limited";
2851
+ }
2852
+ return { state, balance, rateLimitResetType, planType };
2853
+ }
2831
2854
  var CodexManager = class extends CodingAgentManager {
2832
2855
  codex;
2833
2856
  currentThreadId = null;
2834
2857
  currentThread = null;
2835
2858
  tempImageDir;
2836
2859
  activeAbortController = null;
2860
+ /** Most recent quota state observed from a rollout `rate_limits` payload. */
2861
+ latestQuotaState = "ok";
2862
+ /** Last state actually emitted to the UI. Drives dedup so seeded historical state still emits the first time it surfaces in a turn. */
2863
+ lastEmittedQuotaState = "ok";
2864
+ /** Last full snapshot, retained so we can re-emit when a user retries while blocked. */
2865
+ latestQuotaSnapshot = null;
2866
+ /** When true, new turns short-circuit instead of running. Set by `out_of_credits`. */
2867
+ quotaBlocked = false;
2837
2868
  constructor(options) {
2838
2869
  super(options);
2839
2870
  const codexApiKey = resolveCodexApiKey();
@@ -2850,6 +2881,73 @@ var CodexManager = class extends CodingAgentManager {
2850
2881
  console.log(`[CodexManager] Restored thread ID from persisted state: ${this.currentThreadId}`);
2851
2882
  }
2852
2883
  }
2884
+ /**
2885
+ * One-shot pass over the current session's rollout jsonl to find the most
2886
+ * recent `rate_limits` entry and emit a quota state if it has changed.
2887
+ * Used after `runStreamed` throws — the tail loop may not have pumped the
2888
+ * fatal line before the SDK exited.
2889
+ */
2890
+ async flushQuotaSnapshotFromCurrentSession() {
2891
+ if (!this.currentThreadId) return;
2892
+ try {
2893
+ const sessionFile = await this.findSessionFile(this.currentThreadId);
2894
+ if (!sessionFile) return;
2895
+ const content = await readFile7(sessionFile, "utf-8");
2896
+ const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
2897
+ let latest = null;
2898
+ for (const line of lines) {
2899
+ try {
2900
+ const parsed = JSON.parse(line);
2901
+ const snapshot = extractRateLimitsSnapshot(parsed);
2902
+ if (snapshot) {
2903
+ latest = snapshot;
2904
+ }
2905
+ } catch {
2906
+ }
2907
+ }
2908
+ if (latest) {
2909
+ this.emitQuotaStatus(latest);
2910
+ }
2911
+ } catch (error) {
2912
+ console.warn("[CodexManager] Failed to flush quota snapshot from session:", error);
2913
+ }
2914
+ }
2915
+ /**
2916
+ * Emit a synthetic codex-quota-status AgentEvent and update local state.
2917
+ * Dedupes against `lastEmittedQuotaState` (what the UI has actually seen),
2918
+ * not `latestQuotaState`, so a state primed silently by `seedSeenLines` on
2919
+ * engine restart still emits the first time it surfaces in a turn. Pass
2920
+ * `force` to re-emit on a retry so users see the banner reappear.
2921
+ */
2922
+ emitQuotaStatus(snapshot, force = false) {
2923
+ const stateChanged = snapshot.state !== this.lastEmittedQuotaState;
2924
+ if (!stateChanged && !force) {
2925
+ return;
2926
+ }
2927
+ this.latestQuotaState = snapshot.state;
2928
+ this.lastEmittedQuotaState = snapshot.state;
2929
+ this.quotaBlocked = snapshot.state === "out_of_credits";
2930
+ this.latestQuotaSnapshot = snapshot;
2931
+ const payload = {
2932
+ state: snapshot.state,
2933
+ balance: snapshot.balance,
2934
+ rateLimitResetType: snapshot.rateLimitResetType,
2935
+ planType: snapshot.planType
2936
+ };
2937
+ this.onEvent({
2938
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2939
+ type: CODEX_QUOTA_STATUS_EVENT_TYPE,
2940
+ payload
2941
+ });
2942
+ if (!stateChanged) return;
2943
+ if (snapshot.state === "out_of_credits") {
2944
+ console.warn("[CodexManager] Codex account out of credits \u2014 pausing turn processing.");
2945
+ } else if (snapshot.state === "rate_limited") {
2946
+ console.warn(`[CodexManager] Codex rate limit reached (${snapshot.rateLimitResetType}).`);
2947
+ } else {
2948
+ console.log("[CodexManager] Codex quota recovered.");
2949
+ }
2950
+ }
2853
2951
  async interruptActiveTurn() {
2854
2952
  if (this.activeAbortController) {
2855
2953
  this.activeAbortController.abort();
@@ -2908,6 +3006,18 @@ var CodexManager = class extends CodingAgentManager {
2908
3006
  * Internal method that actually processes the message
2909
3007
  */
2910
3008
  async processMessageInternal(request) {
3009
+ if (this.quotaBlocked && this.latestQuotaSnapshot) {
3010
+ await this.flushQuotaSnapshotFromCurrentSession();
3011
+ if (this.quotaBlocked && this.latestQuotaSnapshot) {
3012
+ this.emitQuotaStatus(this.latestQuotaSnapshot, true);
3013
+ try {
3014
+ await this.onTurnComplete();
3015
+ } catch (error) {
3016
+ console.error("[CodexManager] onTurnComplete failed during quota-blocked turn:", error);
3017
+ }
3018
+ return;
3019
+ }
3020
+ }
2911
3021
  const {
2912
3022
  message,
2913
3023
  model,
@@ -2944,8 +3054,8 @@ var CodexManager = class extends CodingAgentManager {
2944
3054
  this.currentThread = this.codex.resumeThread(this.currentThreadId, threadOptions);
2945
3055
  } else {
2946
3056
  this.currentThread = this.codex.startThread(threadOptions);
2947
- const { events: events2 } = await this.currentThread.runStreamed("Hello", { signal: abortController.signal });
2948
- for await (const event of events2) {
3057
+ const { events } = await this.currentThread.runStreamed("Hello", { signal: abortController.signal });
3058
+ for await (const event of events) {
2949
3059
  if (event.type === "thread.started") {
2950
3060
  this.currentThreadId = event.thread_id;
2951
3061
  await this.onSaveSessionId(this.currentThreadId);
@@ -2969,38 +3079,47 @@ var CodexManager = class extends CodingAgentManager {
2969
3079
  } else {
2970
3080
  input = message;
2971
3081
  }
2972
- const { events } = await this.currentThread.runStreamed(input, { signal: abortController.signal });
2973
- let latestThoughtEvent = null;
2974
- for await (const event of events) {
2975
- if (linearSessionId) {
2976
- const plan = extractPlanFromCodexEvent(event);
2977
- if (plan) {
2978
- monolithService.sendEvent({
2979
- type: "agent_plan_update",
2980
- payload: { linearSessionId, plan }
2981
- }).catch(() => {
2982
- });
2983
- }
2984
- const linearEvent = convertCodexEvent(event, linearSessionId);
2985
- if (linearEvent) {
2986
- if (latestThoughtEvent) {
2987
- monolithService.sendEvent({ type: "agent_update", payload: latestThoughtEvent }).catch(() => {
3082
+ try {
3083
+ const { events } = await this.currentThread.runStreamed(input, { signal: abortController.signal });
3084
+ let latestThoughtEvent = null;
3085
+ for await (const event of events) {
3086
+ if (linearSessionId) {
3087
+ const plan = extractPlanFromCodexEvent(event);
3088
+ if (plan) {
3089
+ monolithService.sendEvent({
3090
+ type: "agent_plan_update",
3091
+ payload: { linearSessionId, plan }
3092
+ }).catch(() => {
2988
3093
  });
2989
3094
  }
2990
- if (isLinearThoughtEvent2(linearEvent)) {
2991
- latestThoughtEvent = linearEvent;
2992
- } else {
2993
- latestThoughtEvent = null;
2994
- monolithService.sendEvent({ type: "agent_update", payload: linearEvent }).catch(() => {
2995
- });
3095
+ const linearEvent = convertCodexEvent(event, linearSessionId);
3096
+ if (linearEvent) {
3097
+ if (latestThoughtEvent) {
3098
+ monolithService.sendEvent({ type: "agent_update", payload: latestThoughtEvent }).catch(() => {
3099
+ });
3100
+ }
3101
+ if (isLinearThoughtEvent2(linearEvent)) {
3102
+ latestThoughtEvent = linearEvent;
3103
+ } else {
3104
+ latestThoughtEvent = null;
3105
+ monolithService.sendEvent({ type: "agent_update", payload: linearEvent }).catch(() => {
3106
+ });
3107
+ }
2996
3108
  }
2997
3109
  }
2998
3110
  }
2999
- }
3000
- if (linearSessionId && latestThoughtEvent) {
3001
- const responseEvent = linearThoughtToResponse(latestThoughtEvent);
3002
- monolithService.sendEvent({ type: "agent_update", payload: responseEvent }).catch(() => {
3003
- });
3111
+ if (linearSessionId && latestThoughtEvent) {
3112
+ const responseEvent = linearThoughtToResponse(latestThoughtEvent);
3113
+ monolithService.sendEvent({ type: "agent_update", payload: responseEvent }).catch(() => {
3114
+ });
3115
+ }
3116
+ } catch (error) {
3117
+ await this.flushQuotaSnapshotFromCurrentSession();
3118
+ if (this.quotaBlocked) {
3119
+ console.warn("[CodexManager] runStreamed failed while quota was blocked \u2014 surfacing as quota state.");
3120
+ return;
3121
+ }
3122
+ throw error;
3004
3123
  }
3005
3124
  } finally {
3006
3125
  if (stopTail) {
@@ -3100,8 +3219,20 @@ var CodexManager = class extends CodingAgentManager {
3100
3219
  try {
3101
3220
  const content = await readFile7(sessionFile, "utf-8");
3102
3221
  const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
3222
+ let latest = null;
3103
3223
  for (const line of lines) {
3104
3224
  seenLines.add(line);
3225
+ try {
3226
+ const parsed = JSON.parse(line);
3227
+ const snapshot = extractRateLimitsSnapshot(parsed);
3228
+ if (snapshot) latest = snapshot;
3229
+ } catch {
3230
+ }
3231
+ }
3232
+ if (latest) {
3233
+ this.latestQuotaState = latest.state;
3234
+ this.latestQuotaSnapshot = latest;
3235
+ this.quotaBlocked = latest.state === "out_of_credits";
3105
3236
  }
3106
3237
  } catch {
3107
3238
  }
@@ -3121,6 +3252,10 @@ var CodexManager = class extends CodingAgentManager {
3121
3252
  seenLines.add(trimmed);
3122
3253
  try {
3123
3254
  const parsed = JSON.parse(trimmed);
3255
+ const snapshot = extractRateLimitsSnapshot(parsed);
3256
+ if (snapshot) {
3257
+ this.emitQuotaStatus(snapshot);
3258
+ }
3124
3259
  if (isJsonlEvent2(parsed)) {
3125
3260
  this.onEvent(parsed);
3126
3261
  emitted += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.132",
3
+ "version": "0.1.134",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",