@love-moon/ai-sdk 0.9.0 → 0.11.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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 30afccc: Make the claude backend usable when conductor runs as root.
8
+
9
+ Claude Code refuses `bypassPermissions` / `--dangerously-skip-permissions` under
10
+ root unless `IS_SANDBOX=1` or `CLAUDE_CODE_BUBBLEWRAP` is set, so every claude
11
+ turn died immediately on root installs (docker, CI, bare VPS). Both routes a
12
+ claude command takes are now root-aware, sharing one check
13
+ (`isClaudeRootPermissionRestricted`, which mirrors claude's own gate):
14
+
15
+ - The agent-SDK session downgrades the permission mode to `acceptEdits` (auto
16
+ mode) and stops passing `allowDangerouslySkipPermissions`.
17
+ - The daemon's tool-preset PTY path rewrites the configured `allow_cli_list`
18
+ command via `resolveClaudeCommandForRoot`, using the same env the spawned
19
+ child receives, so a per-task `IS_SANDBOX=1` opt-out is honored.
20
+ - `conductor config` no longer writes `--dangerously-skip-permissions` into a
21
+ config generated on a root machine.
22
+
23
+ The mode is also configurable now: `claude --permission-mode acceptEdits` in
24
+ `allow_cli_list` is lifted out of the command string. An unrecognized mode still
25
+ falls back to the default instead of failing, but logs a warning at session boot.
26
+
27
+ ai-sdk gains three exports: `isClaudeRootPermissionRestricted`,
28
+ `resolveClaudeCommandForRoot`, `resolveClaudePermissionPolicy`.
29
+
30
+ ## 0.10.0
31
+
32
+ ### Minor Changes
33
+
34
+ - 43c4f87: Add the `codex-app-server-session` provider, recover Codex oversized threads by
35
+ rolling onto a fresh provider thread with bounded recent history (one retry per
36
+ turn), and trim the dsh quota raw payload.
37
+
3
38
  ## 0.9.0
4
39
 
5
40
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
2
2
  export { PROVIDER_MEDIA_CAPABILITIES } from "./media-adapters.js";
3
3
  export { createAiSession, RemoteAiSession } from "./client.js";
4
+ export { isClaudeRootPermissionRestricted, resolveClaudeCommandForRoot, resolveClaudePermissionPolicy } from "./providers/claude-agent-sdk-session.js";
4
5
  export { appendContextFilesToPrompt, normalizeContextFiles } from "./context-files.js";
5
6
  export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normalizeMediaInputs, resolveTurnMedia } from "./media-input.js";
6
7
  export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus } from "./shared.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createAiSession, RemoteAiSession } from "./client.js";
2
2
  export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
3
3
  export { PROVIDER_MEDIA_CAPABILITIES } from "./media-adapters.js";
4
+ export { isClaudeRootPermissionRestricted, resolveClaudeCommandForRoot, resolveClaudePermissionPolicy, } from "./providers/claude-agent-sdk-session.js";
4
5
  export { appendContextFilesToPrompt, normalizeContextFiles } from "./context-files.js";
5
6
  export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normalizeMediaInputs, resolveTurnMedia, } from "./media-input.js";
6
7
  export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus, } from "./shared.js";
@@ -110,6 +110,10 @@ export async function getDshQuota(options = {}) {
110
110
  }
111
111
  const payload = (await res.json());
112
112
  const { isAvailable, balances } = parseDshBalance(payload);
113
+ // No `raw` field: unlike the window-based providers whose parsing is
114
+ // lossy, `balances`/`isAvailable` already capture the whole balance
115
+ // response, so keeping the raw payload would only bloat the cache and the
116
+ // worker/WebSocket hop to the web client.
113
117
  const quota = {
114
118
  tool: "dsh",
115
119
  isAvailable,
@@ -117,7 +121,6 @@ export async function getDshQuota(options = {}) {
117
121
  balances,
118
122
  fetchedAt: Math.floor(Date.now() / 1000),
119
123
  source: "fresh",
120
- raw: payload,
121
124
  };
122
125
  const entry = await writeCache(file, quota);
123
126
  return { ...quota, fetchedAt: entry.fetchedAt };
@@ -128,7 +128,6 @@ export interface DshQuota {
128
128
  balances: DshBalanceInfo[];
129
129
  fetchedAt: number;
130
130
  source: QuotaSource;
131
- raw?: Record<string, unknown>;
132
131
  error?: string;
133
132
  }
134
133
  export interface ExternalQuota {
@@ -1,3 +1,11 @@
1
+ export function isClaudeRootPermissionRestricted(env?: NodeJS.ProcessEnv): boolean;
2
+ export function resolveClaudePermissionPolicy(options?: {}, env?: NodeJS.ProcessEnv): {
3
+ permissionMode: string;
4
+ allowDangerouslySkipPermissions: boolean;
5
+ downgradedFrom: string;
6
+ invalidMode: any;
7
+ };
8
+ export function resolveClaudeCommandForRoot(commandLine: any, env?: NodeJS.ProcessEnv): string;
1
9
  export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
2
10
  static capabilities: Readonly<{
3
11
  goal: true;
@@ -49,6 +57,12 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
49
57
  turnDeadlineMs: any;
50
58
  sdkModulePromise: Promise<any> | Promise<typeof import("@anthropic-ai/claude-agent-sdk")> | null;
51
59
  env: any;
60
+ permissionPolicy: {
61
+ permissionMode: string;
62
+ allowDangerouslySkipPermissions: boolean;
63
+ downgradedFrom: string;
64
+ invalidMode: any;
65
+ };
52
66
  writeLog(message: any): void;
53
67
  trace(message: any): void;
54
68
  get threadId(): any;
@@ -131,7 +145,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
131
145
  abortController: any;
132
146
  cwd: any;
133
147
  env: any;
134
- permissionMode: "default" | "plan" | "acceptEdits" | "bypassPermissions" | "dontAsk";
148
+ permissionMode: string;
135
149
  settingSources: string[];
136
150
  persistSession: boolean;
137
151
  };
@@ -8,6 +8,10 @@ const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
8
8
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
9
9
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
10
10
  const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
11
+ const PERMISSION_MODES = new Set(["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk"]);
12
+ const DEFAULT_PERMISSION_MODE = "bypassPermissions";
13
+ // Auto ("accept edits") mode: the strongest mode claude allows as root.
14
+ const ROOT_PERMISSION_MODE = "acceptEdits";
11
15
  function waitForever() {
12
16
  return new Promise(() => { });
13
17
  }
@@ -50,16 +54,83 @@ function normalizeSettingSources(value) {
50
54
  }
51
55
  function normalizePermissionMode(value) {
52
56
  const normalized = typeof value === "string" ? value.trim() : "";
53
- switch (normalized) {
54
- case "default":
55
- case "acceptEdits":
56
- case "bypassPermissions":
57
- case "plan":
58
- case "dontAsk":
59
- return normalized;
60
- default:
61
- return "bypassPermissions";
57
+ return PERMISSION_MODES.has(normalized) ? normalized : DEFAULT_PERMISSION_MODE;
58
+ }
59
+ // Mirrors claude's own root gate byte for byte:
60
+ //
61
+ // permissionMode === "bypassPermissions" || dangerouslySkipPermissions
62
+ // ? getuid() === 0 && process.env.IS_SANDBOX !== "1" && !CLAUDE_CODE_BUBBLEWRAP
63
+ // -> "--dangerously-skip-permissions cannot be used with root/sudo
64
+ // privileges for security reasons" + exit(1)
65
+ //
66
+ // Two different env idioms in that one line, and both are load-bearing:
67
+ // IS_SANDBOX is a *strict* `=== "1"` compare, while CLAUDE_CODE_BUBBLEWRAP goes
68
+ // through claude's loose truthy parser (`1|true|yes|on`, trimmed+lowercased).
69
+ // Do not "helpfully" accept IS_SANDBOX=true here — being more permissive than
70
+ // the gate keeps bypassPermissions and just moves the failure into claude,
71
+ // which is the exact bug we are fixing.
72
+ export function isClaudeRootPermissionRestricted(env = process.env) {
73
+ if (typeof process.getuid !== "function" || process.getuid() !== 0) {
74
+ return false;
75
+ }
76
+ if (env?.IS_SANDBOX === "1") {
77
+ return false;
78
+ }
79
+ const bubblewrap = String(env?.CLAUDE_CODE_BUBBLEWRAP ?? "").trim().toLowerCase();
80
+ return !["1", "true", "yes", "on"].includes(bubblewrap);
81
+ }
82
+ // Root installs (docker, CI, bare VPS) would otherwise fail every turn, so fall
83
+ // back to the closest usable mode instead of dying.
84
+ export function resolveClaudePermissionPolicy(options = {}, env = process.env) {
85
+ const requested = normalizePermissionMode(options.permissionMode);
86
+ // A configured value we don't recognize still falls back to the default (a
87
+ // typo must never take the session down), but the caller reports it so the
88
+ // user finds out their config line is doing nothing.
89
+ const rawMode = typeof options.permissionMode === "string" ? options.permissionMode.trim() : "";
90
+ const invalidMode = rawMode && !PERMISSION_MODES.has(rawMode) ? rawMode : "";
91
+ if (!isClaudeRootPermissionRestricted(env)) {
92
+ return {
93
+ permissionMode: requested,
94
+ allowDangerouslySkipPermissions: requested === DEFAULT_PERMISSION_MODE || options.allowDangerouslySkipPermissions === true,
95
+ downgradedFrom: "",
96
+ invalidMode,
97
+ };
98
+ }
99
+ const permissionMode = requested === DEFAULT_PERMISSION_MODE ? ROOT_PERMISSION_MODE : requested;
100
+ return {
101
+ permissionMode,
102
+ allowDangerouslySkipPermissions: false,
103
+ downgradedFrom: permissionMode === requested ? "" : requested,
104
+ invalidMode,
105
+ };
106
+ }
107
+ // The PTY / tool-preset path in the daemon hands a configured allow_cli_list
108
+ // command straight to a shell, so it never reaches the session class above and
109
+ // never sees resolveClaudePermissionPolicy. Rewrite the command string there
110
+ // too, or a root box keeps failing in terminal tasks after the SDK path is fixed.
111
+ export function resolveClaudeCommandForRoot(commandLine, env = process.env) {
112
+ const command = typeof commandLine === "string" ? commandLine : "";
113
+ if (!command.trim() || !isClaudeRootPermissionRestricted(env)) {
114
+ return command;
115
+ }
116
+ // Also drop a valueless `--permission-mode` (end of string, or immediately
117
+ // followed by another flag). Without this the append branch below would emit
118
+ // `--permission-mode --permission-mode acceptEdits`, and claude reads the
119
+ // flag name as the mode and `acceptEdits` as a positional prompt.
120
+ const stripped = command
121
+ .replace(/\s*--dangerously-skip-permissions(?=\s|$)/g, "")
122
+ .replace(/\s*--permission-mode=(?=\s|$)/g, "")
123
+ .replace(/\s*--permission-mode(?=\s+-|\s*$)/g, "")
124
+ .trim();
125
+ const configuredMode = extractLongFlagFromCommandLine(stripped, "permission-mode");
126
+ if (!configuredMode) {
127
+ return `${stripped} --permission-mode ${ROOT_PERMISSION_MODE}`;
128
+ }
129
+ if (configuredMode === DEFAULT_PERMISSION_MODE) {
130
+ return stripped.replace(/(--permission-mode[=\s]+)bypassPermissions(?=\s|$)/, `$1${ROOT_PERMISSION_MODE}`);
62
131
  }
132
+ // Any other configured mode is already root-safe; leave it verbatim.
133
+ return stripped;
63
134
  }
64
135
  function normalizeText(value) {
65
136
  return typeof value === "string" ? value : "";
@@ -163,6 +234,14 @@ export class ClaudeAgentSdkSession extends EventEmitter {
163
234
  options = { ...options, effort: effortFromCommandLine };
164
235
  }
165
236
  }
237
+ // Same for `--permission-mode`, so `claude --permission-mode acceptEdits`
238
+ // in allow_cli_list picks the mode without new plumbing.
239
+ if (options.permissionMode === undefined && typeof options.commandLine === "string") {
240
+ const modeFromCommandLine = extractLongFlagFromCommandLine(options.commandLine, "permission-mode");
241
+ if (modeFromCommandLine) {
242
+ options = { ...options, permissionMode: modeFromCommandLine };
243
+ }
244
+ }
166
245
  this.options = options;
167
246
  this.logger = normalizeLogger(options.logger);
168
247
  this.cwd =
@@ -205,6 +284,18 @@ export class ClaudeAgentSdkSession extends EventEmitter {
205
284
  if (!this.env.CLAUDE_AGENT_SDK_CLIENT_APP) {
206
285
  this.env.CLAUDE_AGENT_SDK_CLIENT_APP = "conductor-ai-sdk/0.0.0";
207
286
  }
287
+ // Resolved once at boot (options and env are both fixed by now) so the
288
+ // config warning lands at session start instead of once per turn.
289
+ this.permissionPolicy = resolveClaudePermissionPolicy(this.options, { ...process.env, ...this.env });
290
+ if (this.permissionPolicy.invalidMode) {
291
+ this.trace(`WARN unknown permission mode ${JSON.stringify(this.permissionPolicy.invalidMode)} in config; `
292
+ + `using ${this.permissionPolicy.permissionMode} instead `
293
+ + `(valid: ${[...PERMISSION_MODES].join(", ")})`);
294
+ }
295
+ if (this.permissionPolicy.downgradedFrom) {
296
+ this.trace(`permission mode ${this.permissionPolicy.downgradedFrom} -> ${this.permissionPolicy.permissionMode} `
297
+ + `(claude rejects ${this.permissionPolicy.downgradedFrom} as root; set IS_SANDBOX=1 to keep it)`);
298
+ }
208
299
  }
209
300
  writeLog(message) {
210
301
  emitLog(this.logger, message);
@@ -537,7 +628,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
537
628
  });
538
629
  }
539
630
  buildSdkOptions(abortController) {
540
- const permissionMode = normalizePermissionMode(this.options.permissionMode);
631
+ const { permissionMode } = this.permissionPolicy;
541
632
  const options = {
542
633
  abortController,
543
634
  cwd: this.cwd,
@@ -549,7 +640,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
549
640
  settingSources: normalizeSettingSources(this.options.settingSources),
550
641
  persistSession: this.options.persistSession !== false,
551
642
  };
552
- if (permissionMode === "bypassPermissions" || this.options.allowDangerouslySkipPermissions === true) {
643
+ if (this.permissionPolicy.allowDangerouslySkipPermissions) {
553
644
  options.allowDangerouslySkipPermissions = true;
554
645
  }
555
646
  if (this.sessionId) {
@@ -53,6 +53,7 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
53
53
  } | null;
54
54
  bootPromise: Promise<void> | null;
55
55
  booted: boolean;
56
+ _contextRecoveryInFlight: boolean;
56
57
  goalMode: boolean;
57
58
  currentGoal: (import("../shared.js").GoalState & {
58
59
  raw?: unknown;
@@ -127,6 +128,22 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
127
128
  }): void;
128
129
  applySessionConfigured(params: any): void;
129
130
  resolveContextUsagePercent(): number | undefined;
131
+ /**
132
+ * Whether a turn failure was caused by the provider thread outgrowing the
133
+ * model context window. Such failures are recoverable by rolling onto a fresh
134
+ * thread; ordinary failures (timeouts, closes, tool errors) are not.
135
+ *
136
+ * @param {any} error
137
+ * @returns {boolean}
138
+ */
139
+ isRecoverableContextOverflow(error: any): boolean;
140
+ /**
141
+ * Reset thread-bound state so the next {@link boot} starts a brand-new Codex
142
+ * thread, seeded with the most-recent {@link RECOVERY_HISTORY_LIMIT} history
143
+ * entries as a prompt preface. The oversized thread is abandoned (never
144
+ * resumed) so its context can no longer overflow.
145
+ */
146
+ rollOntoFreshThread(): void;
130
147
  normalizeWorkingStatusPayload(payload: any): {
131
148
  source: string;
132
149
  reply_in_progress: boolean;
@@ -226,26 +243,7 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
226
243
  runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, jsonSchema }?: {
227
244
  useInitialImages?: boolean | undefined;
228
245
  jsonSchema?: null | undefined;
229
- }): Promise<{
230
- text: string;
231
- usage: null;
232
- items: never[];
233
- events: never[];
234
- provider?: undefined;
235
- metadata?: undefined;
236
- } | {
237
- text: string;
238
- usage: any;
239
- items: any;
240
- events: never[];
241
- provider: string;
242
- metadata: {
243
- source: string;
244
- threadId: any;
245
- threadPath: string | undefined;
246
- nativeSessionId: string | undefined;
247
- };
248
- }>;
246
+ }): any;
249
247
  /**
250
248
  * Normalize a goal payload from a codex `thread/goal/*` notification into
251
249
  * the cross-layer {@link GoalState} shape.
@@ -12,6 +12,15 @@ function isTerminalGoalStatusLocal(status) {
12
12
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
13
13
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
14
14
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
15
+ // Oversized-thread recovery: when a Codex turn fails because the provider
16
+ // thread outgrew the model context window (and remote compaction could not
17
+ // recover it), roll the same logical chat onto a fresh thread seeded with this
18
+ // many most-recent history entries and retry the prompt once.
19
+ const RECOVERY_HISTORY_LIMIT = 12;
20
+ // Matches the various ways Codex / the underlying model report a context-window
21
+ // overflow. Kept broad but anchored on "context"/"token" wording so ordinary
22
+ // failures are not misclassified as recoverable.
23
+ const CONTEXT_OVERFLOW_PATTERN = /context[ _-]?(?:length|window|limit)|context[ _-]?length[ _-]?exceeded|maximum[ _-]?context|exceed(?:s|ed)?[^.]{0,40}context|too many tokens|token[ _-]?limit|input[^.]{0,20}too long|prompt[^.]{0,20}too long/i;
15
24
  function waitForever() {
16
25
  return new Promise(() => { });
17
26
  }
@@ -217,6 +226,7 @@ export class CodexAppServerSession extends EventEmitter {
217
226
  this.currentTurn = null;
218
227
  this.bootPromise = null;
219
228
  this.booted = false;
229
+ this._contextRecoveryInFlight = false;
220
230
  this.goalMode = options.goalMode === true;
221
231
  this.currentGoal = null;
222
232
  // Goal-mode lifecycle is decoupled from `currentTurn` so that goal events
@@ -465,6 +475,61 @@ export class CodexAppServerSession extends EventEmitter {
465
475
  }
466
476
  return Math.max(0, Math.min(100, Math.round((totalTokens / contextWindow) * 100)));
467
477
  }
478
+ /**
479
+ * Whether a turn failure was caused by the provider thread outgrowing the
480
+ * model context window. Such failures are recoverable by rolling onto a fresh
481
+ * thread; ordinary failures (timeouts, closes, tool errors) are not.
482
+ *
483
+ * @param {any} error
484
+ * @returns {boolean}
485
+ */
486
+ isRecoverableContextOverflow(error) {
487
+ if (!error || error.reason === "session_closed" || error.reason === "turn_timeout") {
488
+ return false;
489
+ }
490
+ const parts = [];
491
+ if (typeof error.message === "string")
492
+ parts.push(error.message);
493
+ if (typeof error.turnStatus === "string")
494
+ parts.push(error.turnStatus);
495
+ const turnError = error.turnError;
496
+ if (typeof turnError === "string") {
497
+ parts.push(turnError);
498
+ }
499
+ else if (turnError && typeof turnError === "object") {
500
+ for (const key of ["message", "code", "type", "reason"]) {
501
+ if (typeof turnError[key] === "string")
502
+ parts.push(turnError[key]);
503
+ }
504
+ try {
505
+ parts.push(JSON.stringify(turnError));
506
+ }
507
+ catch {
508
+ // Ignore non-serializable provider payloads.
509
+ }
510
+ }
511
+ return CONTEXT_OVERFLOW_PATTERN.test(parts.join(" | "));
512
+ }
513
+ /**
514
+ * Reset thread-bound state so the next {@link boot} starts a brand-new Codex
515
+ * thread, seeded with the most-recent {@link RECOVERY_HISTORY_LIMIT} history
516
+ * entries as a prompt preface. The oversized thread is abandoned (never
517
+ * resumed) so its context can no longer overflow.
518
+ */
519
+ rollOntoFreshThread() {
520
+ const recent = this.history.slice(-RECOVERY_HISTORY_LIMIT);
521
+ this.history = recent;
522
+ this.pendingHistorySeed = recent.length > 0;
523
+ this.sessionId = "";
524
+ this.threadPath = "";
525
+ this.nativeSessionId = "";
526
+ this.resumeSessionId = "";
527
+ this.manualResumeReady = false;
528
+ this.sessionInfo = null;
529
+ this.tokenUsage = null;
530
+ this.booted = false;
531
+ this.bootPromise = null;
532
+ }
468
533
  normalizeWorkingStatusPayload(payload) {
469
534
  return {
470
535
  source: CODEX_APP_SERVER_VARIANT,
@@ -1125,6 +1190,51 @@ export class CodexAppServerSession extends EventEmitter {
1125
1190
  if (error?.reason === "turn_timeout") {
1126
1191
  await this.interruptCurrentTurn();
1127
1192
  }
1193
+ // Oversized-thread recovery: the provider thread outgrew the context
1194
+ // window and remote compaction could not save it. Roll the same logical
1195
+ // chat onto a fresh thread seeded with bounded recent history and retry
1196
+ // the prompt exactly once, rather than surfacing a dead turn. The
1197
+ // in-flight guard ensures a single retry: if the fresh thread also
1198
+ // overflows, the recursive call skips recovery and fails normally.
1199
+ if (!this._contextRecoveryInFlight &&
1200
+ !this.closeRequested &&
1201
+ !currentTurn.goalMode &&
1202
+ this.isRecoverableContextOverflow(error)) {
1203
+ // Drop the user message pushed for the failed attempt; the retry
1204
+ // re-adds it (and folds prior history in as a preface via buildPrompt).
1205
+ if (this.history.length > 0 &&
1206
+ String(this.history[this.history.length - 1]?.role || "").toLowerCase() === "user") {
1207
+ this.history.pop();
1208
+ }
1209
+ if (this.currentTurn === currentTurn) {
1210
+ this.currentTurn = null;
1211
+ }
1212
+ this._contextRecoveryInFlight = true;
1213
+ // Release the outer turn's guards before recursing so their timers do
1214
+ // not linger for the duration of the retry. Both cleanups are
1215
+ // idempotent; the outer `finally` calls them again harmlessly.
1216
+ closeGuard.cleanup();
1217
+ turnTimeoutGuard.cleanup();
1218
+ try {
1219
+ this.rollOntoFreshThread();
1220
+ this.trace("context overflow; rolling onto a fresh thread and retrying the turn");
1221
+ await this.emitWorkingStatus({
1222
+ phase: "working",
1223
+ reply_in_progress: true,
1224
+ status_line: "codex context full; retrying on a fresh thread",
1225
+ });
1226
+ this.emit("thread_recovered", { reason: "context_overflow", backend: this.backend });
1227
+ return await this.runTurn(promptText, {
1228
+ useInitialImages,
1229
+ media: mediaInput,
1230
+ contextFiles,
1231
+ jsonSchema,
1232
+ });
1233
+ }
1234
+ finally {
1235
+ this._contextRecoveryInFlight = false;
1236
+ }
1237
+ }
1128
1238
  if (!this.closeRequested && error?.reason !== "session_closed") {
1129
1239
  await this.emitWorkingStatus({
1130
1240
  phase: "turn_failed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -53,12 +53,12 @@
53
53
  "zod": "^4.1.5"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@love-moon/chat-web": "^0.9.0"
56
+ "@love-moon/chat-web": "^0.11.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^22.10.2",
60
60
  "tsx": "^4.20.6",
61
61
  "typescript": "^5.6.3"
62
62
  },
63
- "gitCommitId": "606e5a6"
63
+ "gitCommitId": "7d57852"
64
64
  }