@love-moon/ai-sdk 0.9.0 → 0.10.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,13 @@
|
|
|
1
1
|
# @love-moon/ai-sdk
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 43c4f87: Add the `codex-app-server-session` provider, recover Codex oversized threads by
|
|
8
|
+
rolling onto a fresh provider thread with bounded recent history (one retry per
|
|
9
|
+
turn), and trim the dsh quota raw payload.
|
|
10
|
+
|
|
3
11
|
## 0.9.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
|
@@ -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 };
|
package/dist/manager/types.d.ts
CHANGED
|
@@ -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
|
-
}):
|
|
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.
|
|
3
|
+
"version": "0.10.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.
|
|
56
|
+
"@love-moon/chat-web": "^0.10.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": "
|
|
63
|
+
"gitCommitId": "bc7b3a5"
|
|
64
64
|
}
|