@cartanova/qgrid-cli 2.5.7 → 2.5.9
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/README.ko.md +2 -2
- package/README.md +2 -2
- package/bundle/dist/utils/providers/openai/codex-worker.js +30 -12
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +86 -35
- package/bundle/src/utils/providers/openai/codex-worker.test.ts +105 -0
- package/bundle/src/utils/providers/openai/codex-worker.ts +36 -15
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +442 -4
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +163 -49
- package/bundle/web-dist/client/assets/{index-Bm7RpZcx.js → index-CkBeqjBm.js} +3 -3
- package/bundle/web-dist/client/assets/{logs-BrJdk9XJ.js → logs-d9HdibiE.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-BuFCoBCr.js → monit-C8qYvOMj.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-d-Fwxdg8.js → routes-DT0AY2Lq.js} +1 -1
- package/bundle/web-dist/client/assets/{show-Z5UX7FtU.js → show-DPG1oKpW.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-OA2V76xd.js → tokens-Dz7hmKFm.js} +1 -1
- package/bundle/web-dist/client/index.html +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +1 -1
- package/package.json +1 -1
- package/skills/qgrid/references/cli-env-and-server-boot.md +2 -2
- package/skills/qgrid/references/decision-rationale.md +4 -3
- package/skills/qgrid/references/openai-codex-runtime.md +15 -0
- package/skills/qgrid/references/provider-runtime-differences.md +1 -1
- package/skills/qgrid/references/verification-and-debugging.md +3 -1
|
@@ -197,6 +197,78 @@ describe("CodexAppServerWorker active turn cleanup", () => {
|
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
+
describe("CodexAppServerWorker terminal restart failure", () => {
|
|
201
|
+
it("notifies terminal failure exactly once after exhausting the restart budget", () => {
|
|
202
|
+
const worker = new CodexAppServerWorker({
|
|
203
|
+
tokenId: 1,
|
|
204
|
+
tokenName: "token",
|
|
205
|
+
accessToken: "access",
|
|
206
|
+
accountId: "account",
|
|
207
|
+
});
|
|
208
|
+
const onTerminalFailure = vi.fn();
|
|
209
|
+
worker.onTerminalFailure = onTerminalFailure;
|
|
210
|
+
const internals = worker as unknown as {
|
|
211
|
+
restartAttempts: number;
|
|
212
|
+
scheduleRestart(): void;
|
|
213
|
+
};
|
|
214
|
+
internals.restartAttempts = 3;
|
|
215
|
+
|
|
216
|
+
internals.scheduleRestart();
|
|
217
|
+
internals.scheduleRestart();
|
|
218
|
+
|
|
219
|
+
expect(onTerminalFailure).toHaveBeenCalledTimes(1);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("does not report an intentional worker shutdown as terminal failure", () => {
|
|
223
|
+
const worker = new CodexAppServerWorker({
|
|
224
|
+
tokenId: 1,
|
|
225
|
+
tokenName: "token",
|
|
226
|
+
accessToken: "access",
|
|
227
|
+
accountId: "account",
|
|
228
|
+
});
|
|
229
|
+
const onTerminalFailure = vi.fn();
|
|
230
|
+
worker.onTerminalFailure = onTerminalFailure;
|
|
231
|
+
const internals = worker as unknown as {
|
|
232
|
+
destroyed: boolean;
|
|
233
|
+
restartAttempts: number;
|
|
234
|
+
scheduleRestart(): void;
|
|
235
|
+
};
|
|
236
|
+
internals.destroyed = true;
|
|
237
|
+
internals.restartAttempts = 3;
|
|
238
|
+
|
|
239
|
+
internals.scheduleRestart();
|
|
240
|
+
|
|
241
|
+
expect(onTerminalFailure).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("resets terminal notification state after a successful initialization", async () => {
|
|
245
|
+
const worker = new CodexAppServerWorker({
|
|
246
|
+
tokenId: 1,
|
|
247
|
+
tokenName: "token",
|
|
248
|
+
accessToken: "access",
|
|
249
|
+
accountId: "account",
|
|
250
|
+
});
|
|
251
|
+
const request = vi.fn().mockResolvedValue({});
|
|
252
|
+
const internals = worker as unknown as {
|
|
253
|
+
rpc: { request: typeof request };
|
|
254
|
+
restartAttempts: number;
|
|
255
|
+
terminalFailureNotified: boolean;
|
|
256
|
+
spawnAndInit(): Promise<void>;
|
|
257
|
+
waitForLoginCompleted(): Promise<void>;
|
|
258
|
+
};
|
|
259
|
+
internals.restartAttempts = 3;
|
|
260
|
+
internals.terminalFailureNotified = true;
|
|
261
|
+
internals.rpc = { request };
|
|
262
|
+
vi.spyOn(internals, "spawnAndInit").mockResolvedValue(undefined);
|
|
263
|
+
vi.spyOn(internals, "waitForLoginCompleted").mockResolvedValue(undefined);
|
|
264
|
+
|
|
265
|
+
await worker.initialize();
|
|
266
|
+
|
|
267
|
+
expect(internals.restartAttempts).toBe(0);
|
|
268
|
+
expect(internals.terminalFailureNotified).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
200
272
|
describe("CodexAppServerWorker TTFT", () => {
|
|
201
273
|
it("captures fast non-stream deltas registered before turn/start completes", async () => {
|
|
202
274
|
let now = 1_000;
|
|
@@ -661,6 +733,39 @@ function unsubscribedIds(request: ReturnType<typeof vi.fn>): string[] {
|
|
|
661
733
|
.map(([, params]) => (params as { threadId: string }).threadId);
|
|
662
734
|
}
|
|
663
735
|
|
|
736
|
+
describe("CodexAppServerWorker partial thread creation cleanup", () => {
|
|
737
|
+
it.each([
|
|
738
|
+
["text", false],
|
|
739
|
+
["image", true],
|
|
740
|
+
])("unsubscribes an untracked %s thread when history injection fails", async (_, imageGeneration) => {
|
|
741
|
+
const { worker, request } = createWorkerWithNotificationRpc(async (method) => {
|
|
742
|
+
if (method === "thread/start") {
|
|
743
|
+
return { thread: { id: "partially-created" }, model: "gpt-test" };
|
|
744
|
+
}
|
|
745
|
+
if (method === "thread/inject_items") throw new Error("history injection failed");
|
|
746
|
+
if (method === "thread/unsubscribe") return {};
|
|
747
|
+
throw new Error(`unexpected request: ${method}`);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
await expect(
|
|
751
|
+
worker.createThread({
|
|
752
|
+
input: [{ type: "text", text: "current", text_elements: [] }],
|
|
753
|
+
history: [{ type: "text", text: "previous", text_elements: [] }],
|
|
754
|
+
imageGeneration,
|
|
755
|
+
}),
|
|
756
|
+
).rejects.toThrow("history injection failed");
|
|
757
|
+
|
|
758
|
+
expect(unsubscribedIds(request)).toEqual(["partially-created"]);
|
|
759
|
+
expect(
|
|
760
|
+
(
|
|
761
|
+
worker as unknown as {
|
|
762
|
+
threadMeta: Map<string, { lastUsedAt: number }>;
|
|
763
|
+
}
|
|
764
|
+
).threadMeta.has("partially-created"),
|
|
765
|
+
).toBe(false);
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
664
769
|
describe("CodexAppServerWorker thread unsubscribe on eviction", () => {
|
|
665
770
|
type ThreadMetaInternals = {
|
|
666
771
|
threadMeta: Map<string, { lastUsedAt: number }>;
|
|
@@ -148,12 +148,14 @@ export class CodexAppServerWorker {
|
|
|
148
148
|
private rpc: CodexRpcClient | null = null;
|
|
149
149
|
private codexHome: string;
|
|
150
150
|
private restartAttempts = 0;
|
|
151
|
+
private terminalFailureNotified = false;
|
|
151
152
|
private ready = false;
|
|
152
153
|
private destroyed = false;
|
|
153
154
|
private busy = false;
|
|
154
155
|
private activeTurnAbort?: ActiveTurnAbort;
|
|
155
156
|
active = true;
|
|
156
157
|
onReady?: () => void;
|
|
158
|
+
onTerminalFailure?: () => void;
|
|
157
159
|
// restart 시 rpc 객체가 새로 생성되므로, 핸들러를 보관했다가 매 spawn마다 재바인딩한다.
|
|
158
160
|
serverRequestHandler?: (method: string, params: unknown) => Promise<unknown>;
|
|
159
161
|
|
|
@@ -246,6 +248,7 @@ export class CodexAppServerWorker {
|
|
|
246
248
|
await this.waitForLoginCompleted();
|
|
247
249
|
this.ready = true;
|
|
248
250
|
this.restartAttempts = 0;
|
|
251
|
+
this.terminalFailureNotified = false;
|
|
249
252
|
}
|
|
250
253
|
|
|
251
254
|
async startBrowserLogin(): Promise<string> {
|
|
@@ -261,6 +264,7 @@ export class CodexAppServerWorker {
|
|
|
261
264
|
await this.waitForLoginCompleted();
|
|
262
265
|
this.ready = true;
|
|
263
266
|
this.restartAttempts = 0;
|
|
267
|
+
this.terminalFailureNotified = false;
|
|
264
268
|
logger.info(`worker ${this.config.tokenName} browser login completed`);
|
|
265
269
|
}
|
|
266
270
|
|
|
@@ -453,20 +457,28 @@ export class CodexAppServerWorker {
|
|
|
453
457
|
);
|
|
454
458
|
|
|
455
459
|
const threadId = thread.id;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
460
|
+
try {
|
|
461
|
+
if (req.history?.length) {
|
|
462
|
+
await this.rpc.request("thread/inject_items", {
|
|
463
|
+
threadId,
|
|
464
|
+
items: req.history,
|
|
465
|
+
} satisfies ThreadInjectItemsParams);
|
|
466
|
+
}
|
|
467
|
+
// 이미지 turn 은 재사용에서 제외(R8): base64 가 thread 메모리·캐시 prefix 에
|
|
468
|
+
// 상주하는 것을 막고, 이미지 thread 를 1 회용으로 둔다. threadMeta 미등록 →
|
|
469
|
+
// 후속 reuse 조회에서 이 thread 는 후보가 되지 않는다.
|
|
470
|
+
if (!req.imageGeneration) {
|
|
471
|
+
this.threadMeta.set(threadId, { lastUsedAt: Date.now() });
|
|
472
|
+
}
|
|
468
473
|
|
|
469
|
-
|
|
474
|
+
return { threadId, model: req.model ?? threadModel };
|
|
475
|
+
} catch (error) {
|
|
476
|
+
// thread/start 는 현재 RPC 커넥션을 구독자로 등록한다. 생성 후 초기화가
|
|
477
|
+
// 끝나기 전에 실패하면 executeTurn 의 finally 와 lazy sweep 어느 쪽도 이
|
|
478
|
+
// thread 를 볼 수 없으므로 여기서 즉시 재사용 차단과 구독 해제를 함께 한다.
|
|
479
|
+
this.evictThread(threadId);
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
470
482
|
}
|
|
471
483
|
|
|
472
484
|
// 기존 thread 에 turn 만 실행 (inject 없음). conversation_id 유지 → prompt cache 적중.
|
|
@@ -865,9 +877,18 @@ export class CodexAppServerWorker {
|
|
|
865
877
|
// ── Restart ─────────────────────────────────────────────────────
|
|
866
878
|
|
|
867
879
|
private scheduleRestart(): void {
|
|
868
|
-
if (this.destroyed
|
|
869
|
-
|
|
880
|
+
if (this.destroyed) return;
|
|
881
|
+
if (this.restartAttempts >= MAX_RESTART_ATTEMPTS) {
|
|
882
|
+
if (!this.terminalFailureNotified) {
|
|
883
|
+
this.terminalFailureNotified = true;
|
|
870
884
|
logger.warn(`worker ${this.config.tokenName} max restart attempts reached`);
|
|
885
|
+
try {
|
|
886
|
+
this.onTerminalFailure?.();
|
|
887
|
+
} catch (e) {
|
|
888
|
+
logger.warn(
|
|
889
|
+
`worker ${this.config.tokenName} terminal failure callback failed: ${(e as Error).message}`,
|
|
890
|
+
);
|
|
891
|
+
}
|
|
871
892
|
}
|
|
872
893
|
return;
|
|
873
894
|
}
|