@cartanova/qgrid-cli 2.10.2 → 2.10.4
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/bundle/dist/application/monit/log-buffer.js +7 -11
- package/bundle/dist/application/monit/monit.frame.js +6 -11
- package/bundle/dist/application/qgrid/oauth.js +2 -2
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +64 -141
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +12 -36
- package/bundle/dist/application/qgrid/qgrid.frame.js +3 -6
- package/bundle/dist/application/qgrid/qgrid.types.js +2 -1
- package/bundle/dist/application/qgrid/token-subscriber.js +4 -7
- package/bundle/dist/application/qgrid/token-window-keepalive.js +3 -2
- package/bundle/dist/application/qgrid/tool-emulation-schema.js +4 -4
- package/bundle/dist/application/qgrid/tool-emulation.js +14 -2
- package/bundle/dist/application/request-log/request-log.model.js +7 -5
- package/bundle/dist/application/setting/setting.model.js +2 -64
- package/bundle/dist/application/setting/setting.store.js +3 -4
- package/bundle/dist/application/setting/setting.types.js +2 -12
- package/bundle/dist/i18n/en.js +1 -3
- package/bundle/dist/i18n/ko.js +1 -3
- package/bundle/dist/sonamu.config.js +6 -6
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +1 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +53 -84
- package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +55 -0
- package/bundle/dist/utils/providers/anthropic/claude-session.js +11 -13
- package/bundle/dist/utils/providers/common/effort.js +1 -1
- package/bundle/dist/utils/providers/common/smooth-weighted-round-robin.js +3 -3
- package/bundle/dist/utils/providers/common/thinking.js +12 -0
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +3 -2
- package/bundle/dist/utils/providers/openai/openai-direct-client.js +6 -16
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +89 -78
- package/bundle/src/application/monit/log-buffer.test.ts +3 -4
- package/bundle/src/application/monit/log-buffer.ts +13 -27
- package/bundle/src/application/monit/monit.frame.test.ts +7 -8
- package/bundle/src/application/monit/monit.frame.ts +9 -14
- package/bundle/src/application/qgrid/oauth.ts +1 -1
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +7 -111
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +83 -179
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +62 -1
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +25 -71
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +0 -38
- package/bundle/src/application/qgrid/qgrid.frame.ts +4 -12
- package/bundle/src/application/qgrid/qgrid.types.ts +2 -0
- package/bundle/src/application/qgrid/token-subscriber.test.ts +8 -18
- package/bundle/src/application/qgrid/token-subscriber.ts +10 -20
- package/bundle/src/application/qgrid/token-window-keepalive.test.ts +2 -2
- package/bundle/src/application/qgrid/token-window-keepalive.ts +2 -1
- package/bundle/src/application/qgrid/tool-emulation-schema.test.ts +6 -3
- package/bundle/src/application/qgrid/tool-emulation-schema.ts +3 -3
- package/bundle/src/application/qgrid/tool-emulation.test.ts +8 -1
- package/bundle/src/application/qgrid/tool-emulation.ts +29 -0
- package/bundle/src/application/request-log/request-log.model.ts +5 -5
- package/bundle/src/application/setting/setting.model.test.ts +1 -101
- package/bundle/src/application/setting/setting.model.ts +1 -74
- package/bundle/src/application/setting/setting.store.ts +2 -3
- package/bundle/src/application/setting/setting.types.ts +0 -12
- package/bundle/src/application/sonamu.generated.http +2 -7
- package/bundle/src/i18n/en.ts +0 -3
- package/bundle/src/i18n/ko.ts +0 -3
- package/bundle/src/sonamu.config.ts +5 -5
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +3 -6
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +26 -0
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +70 -148
- package/bundle/src/utils/providers/anthropic/anthropic-thinking.test.ts +36 -0
- package/bundle/src/utils/providers/anthropic/anthropic-thinking.ts +84 -0
- package/bundle/src/utils/providers/anthropic/claude-session.test.ts +74 -13
- package/bundle/src/utils/providers/anthropic/claude-session.ts +11 -20
- package/bundle/src/utils/providers/common/effort.ts +1 -1
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +4 -0
- package/bundle/src/utils/providers/common/smooth-weighted-round-robin.ts +4 -4
- package/bundle/src/utils/providers/common/thinking.ts +10 -0
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +7 -3
- package/bundle/src/utils/providers/openai/openai-direct-client.test.ts +3 -3
- package/bundle/src/utils/providers/openai/openai-direct-client.ts +20 -33
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +97 -6
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +113 -77
- package/bundle/web-dist/client/assets/index-CvG4UB9v.css +1 -0
- package/bundle/web-dist/client/assets/{index-DtRTSTes.js → index-reW8VvJF.js} +4 -4
- package/bundle/web-dist/client/assets/{logs-BhVYDEzC.js → logs-DDIt97D1.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-BxN4JWqK.js → monit-DkABv7PG.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-CBCqRp2o.js → routes-BRmQ4M3-.js} +1 -1
- package/bundle/web-dist/client/assets/services.generated-jnPHYMER.js +51 -0
- package/bundle/web-dist/client/assets/settings-BHvtycEZ.js +1 -0
- package/bundle/web-dist/client/assets/{show-BzYft5N4.js → show-qeTIzgDZ.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-PMybtI8X.js → tokens-5Bep_G-w.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-C51_d6-6.js → use-oauth-login-flow-Cx-QO25M.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-D-Y8OySK.js → use-update-token-mutation-DQCwCYfR.js} +1 -1
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-Bj1cPq6x.js → lazyRouteComponent-CUR-kWzI.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-BSk9lOHe.js → logs-CagNGTBj.js} +3 -3
- package/bundle/web-dist/server/assets/{monit-D9W4ioUi.js → monit-DSxs4Jnl.js} +3 -3
- package/bundle/web-dist/server/assets/{plus-CmvbWuDf.js → plus-Bvdj8Pwx.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-BjR1Iaz8.js → react-dom-CyuShs9K.js} +1 -1
- package/bundle/web-dist/server/assets/{refresh-cw-B8U9BL9-.js → refresh-cw-3QAiSWBd.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-C7lB2Q85.js → routes-i-sTqXMX.js} +4 -4
- package/bundle/web-dist/server/assets/{services.generated-BM4X3Ecj.js → services.generated-FPwsG9p3.js} +116 -131
- package/bundle/web-dist/server/assets/{settings-BFVuldch.js → settings-DeHNwoMv.js} +40 -251
- package/bundle/web-dist/server/assets/{show-Cswh_tqs.js → show-C4jk1Hrp.js} +2 -2
- package/bundle/web-dist/server/assets/{tokens-DiTUwUwr.js → tokens-odgS2-9q.js} +4 -4
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-DYKnG4A2.js → use-oauth-login-flow-DTQKdYBO.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-DqGMopII.js → use-update-token-mutation-xvXbPrCI.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +10 -10
- package/package.json +1 -1
- package/skills/qgrid/SKILL.md +1 -0
- package/skills/qgrid/references/ai-sdk-provider-contract.md +2 -1
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +7 -4
- package/skills/qgrid/references/cli-env-and-server-boot.md +12 -9
- package/skills/qgrid/references/decision-rationale.md +9 -0
- package/skills/qgrid/references/openai-codex-runtime.md +6 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -0
- package/skills/qgrid/references/tool-calling-and-multiturn.md +18 -1
- package/skills/qgrid/references/verification-and-debugging.md +1 -1
- package/bundle/dist/utils/process-supervisor.js +0 -22
- package/bundle/dist/utils/server-restart.js +0 -61
- package/bundle/src/utils/process-supervisor.test.ts +0 -24
- package/bundle/src/utils/process-supervisor.ts +0 -17
- package/bundle/src/utils/server-restart.test.ts +0 -76
- package/bundle/src/utils/server-restart.ts +0 -68
- package/bundle/web-dist/client/assets/index-D_sagzsR.css +0 -1
- package/bundle/web-dist/client/assets/services.generated-Cbvg2sGQ.js +0 -51
- package/bundle/web-dist/client/assets/settings-hBTEC7Ki.js +0 -1
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Monit 로그
|
|
3
|
-
*
|
|
4
|
-
* 콘솔 sink 와 병렬로 등록되는 두 번째 logtape sink 가 이 버퍼를 채우고,
|
|
5
|
-
* monit.frame 이 커서 기반으로 읽어 대시보드 Monit 탭에 제공한다.
|
|
6
|
-
* 재시작하면 리셋된다 — 영속 기록은 request_log 의 몫.
|
|
7
|
-
*
|
|
8
|
-
* timestamp/processStartedAt 은 epoch ms 숫자로 유지한다. web 의 전역 dateReviver 가
|
|
9
|
-
* ISO 문자열을 Date 로 바꿔버리므로, 숫자만이 재시작 감지의 동등성 비교를 보존한다.
|
|
2
|
+
* 재시작하면 초기화되는 Monit 로그 버퍼.
|
|
3
|
+
* timestamp/processStartedAt은 web의 dateReviver 변환을 피하고 동등성 비교를 유지하도록 epoch ms를 쓴다.
|
|
10
4
|
*/
|
|
11
5
|
import { type LogRecord, type Sink } from "@logtape/logtape";
|
|
12
6
|
|
|
@@ -48,16 +42,14 @@ function renderValue(value: unknown): string {
|
|
|
48
42
|
|
|
49
43
|
export class MonitLogBuffer {
|
|
50
44
|
readonly processStartedAt = Date.now();
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
entries: MonitLogEntry[] = [];
|
|
46
|
+
nextSeq = 1;
|
|
53
47
|
|
|
54
48
|
constructor(
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
readonly capacity = DEFAULT_CAPACITY,
|
|
50
|
+
readonly maxTextLength = DEFAULT_MAX_TEXT_LENGTH,
|
|
57
51
|
) {}
|
|
58
52
|
|
|
59
|
-
// logtape sink 본체. logtape 가 sink 오류를 격리하지만, 버퍼 버그가
|
|
60
|
-
// meta logger 소음을 만들지 않도록 방어적으로 한 번 더 감싼다.
|
|
61
53
|
push(record: LogRecord): void {
|
|
62
54
|
try {
|
|
63
55
|
let text = flattenMessage(record.message);
|
|
@@ -79,31 +71,25 @@ export class MonitLogBuffer {
|
|
|
79
71
|
}
|
|
80
72
|
}
|
|
81
73
|
|
|
82
|
-
|
|
83
|
-
return this.entries[0]?.seq ?? this.nextSeq;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
get latestSeq(): number {
|
|
87
|
-
return this.nextSeq - 1;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// cursor 이후의 엔트리를 최대 limit 개 반환한다. cursor 미지정이면 tail.
|
|
74
|
+
// cursor 미지정이면 최근 엔트리부터 반환한다.
|
|
91
75
|
after(cursor: number | undefined, limit: number): MonitLogChunk {
|
|
76
|
+
const latestSeq = this.nextSeq - 1;
|
|
77
|
+
const oldestSeq = this.entries[0]?.seq ?? this.nextSeq;
|
|
92
78
|
if (cursor === undefined) {
|
|
93
79
|
const entries = this.entries.slice(-limit);
|
|
94
80
|
return {
|
|
95
81
|
entries,
|
|
96
|
-
nextCursor: entries.at(-1)?.seq ??
|
|
82
|
+
nextCursor: entries.at(-1)?.seq ?? latestSeq,
|
|
97
83
|
dropped: 0,
|
|
98
84
|
};
|
|
99
85
|
}
|
|
100
86
|
|
|
101
87
|
// 이전 프로세스의 커서 등 미래 커서는 방어적으로 현재 tail 로 재동기화한다.
|
|
102
|
-
if (cursor >
|
|
103
|
-
return { entries: [], nextCursor:
|
|
88
|
+
if (cursor > latestSeq) {
|
|
89
|
+
return { entries: [], nextCursor: latestSeq, dropped: 0 };
|
|
104
90
|
}
|
|
105
91
|
|
|
106
|
-
const dropped = Math.max(
|
|
92
|
+
const dropped = Math.max(oldestSeq - cursor - 1, 0);
|
|
107
93
|
const startIndex = this.entries.findIndex((entry) => entry.seq > cursor);
|
|
108
94
|
const entries = startIndex === -1 ? [] : this.entries.slice(startIndex, startIndex + limit);
|
|
109
95
|
return {
|
|
@@ -36,12 +36,12 @@ describe("MonitFrame.monitLogs", () => {
|
|
|
36
36
|
const chunk = await MonitFrame.monitLogs();
|
|
37
37
|
expect(chunk.processStartedAt).toBe(monitLogBuffer.processStartedAt);
|
|
38
38
|
expect(chunk.dropped).toBe(0);
|
|
39
|
-
expect(chunk.nextCursor).toBe(monitLogBuffer.
|
|
39
|
+
expect(chunk.nextCursor).toBe(monitLogBuffer.after(undefined, 1).nextCursor);
|
|
40
40
|
expect(chunk.entries.at(-1)?.text).toBe("second line");
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it("returns only entries newer than the cursor with no boundary duplicates", async () => {
|
|
44
|
-
const baseline = monitLogBuffer.
|
|
44
|
+
const baseline = monitLogBuffer.after(undefined, 1).nextCursor;
|
|
45
45
|
push("after baseline 1");
|
|
46
46
|
push("after baseline 2");
|
|
47
47
|
|
|
@@ -50,7 +50,7 @@ describe("MonitFrame.monitLogs", () => {
|
|
|
50
50
|
"after baseline 1",
|
|
51
51
|
"after baseline 2",
|
|
52
52
|
]);
|
|
53
|
-
expect(chunk.nextCursor).toBe(monitLogBuffer.
|
|
53
|
+
expect(chunk.nextCursor).toBe(monitLogBuffer.after(undefined, 1).nextCursor);
|
|
54
54
|
|
|
55
55
|
const followUp = await MonitFrame.monitLogs(chunk.nextCursor);
|
|
56
56
|
expect(followUp.entries).toEqual([]);
|
|
@@ -90,7 +90,7 @@ describe("MonitFrame.monitLogs", () => {
|
|
|
90
90
|
properties: { accessToken: "forbidden-property-secret", refreshToken: "also-forbidden" },
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
const chunk = await MonitFrame.monitLogs(monitLogBuffer.
|
|
93
|
+
const chunk = await MonitFrame.monitLogs(monitLogBuffer.after(undefined, 1).nextCursor - 1);
|
|
94
94
|
const serialized = JSON.stringify(chunk);
|
|
95
95
|
expect(serialized).not.toContain("forbidden-property-secret");
|
|
96
96
|
expect(serialized).not.toContain("also-forbidden");
|
|
@@ -115,14 +115,13 @@ describe("MonitFrame.monitLogs", () => {
|
|
|
115
115
|
it("piggybacks live vitals on every chunk, zeroed before dispatchers boot", async () => {
|
|
116
116
|
QgridDispatcher.openaiDispatcher = {
|
|
117
117
|
inFlight: 3,
|
|
118
|
-
|
|
118
|
+
getQuotaSnapshot: () => [
|
|
119
119
|
{ name: "openai/haze", usedPercent: 41, threshold: 80, blocked: false, resetsAt: null },
|
|
120
120
|
{ name: "openai/nk", usedPercent: null, threshold: null, blocked: false, resetsAt: null },
|
|
121
121
|
],
|
|
122
122
|
} as never;
|
|
123
123
|
QgridDispatcher.anthropicDispatcher = {
|
|
124
|
-
|
|
125
|
-
tokenNames: ["anthropic/haze", "anthropic/noa"],
|
|
124
|
+
tokenPool: new Map([[1, {name: "anthropic/noa"}], [2, {name: "anthropic/haze"}]]),
|
|
126
125
|
inFlight: 2,
|
|
127
126
|
} as never;
|
|
128
127
|
try {
|
|
@@ -133,7 +132,7 @@ describe("MonitFrame.monitLogs", () => {
|
|
|
133
132
|
{ name: "openai/haze", usedPercent: 41, threshold: 80, blocked: false, resetsAt: null },
|
|
134
133
|
{ name: "openai/nk", usedPercent: null, threshold: null, blocked: false, resetsAt: null },
|
|
135
134
|
],
|
|
136
|
-
anthropicTokenCount:
|
|
135
|
+
anthropicTokenCount: 2,
|
|
137
136
|
anthropicTokenNames: ["anthropic/haze", "anthropic/noa"],
|
|
138
137
|
anthropicInFlight: 2,
|
|
139
138
|
});
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Monit — 대시보드 Monit 탭용 프로세스 로그 관측 API.
|
|
3
|
-
*
|
|
4
|
-
* 읽기 전용. ring buffer 를 얇게 직접 읽으며(범용 쿼리 기계 없이),
|
|
5
|
-
* request log 를 남기지 않는다. 접근 경계는 다른 대시보드 API 와 동일하다.
|
|
6
|
-
*/
|
|
1
|
+
// 읽기 전용 모니터링 API. 조회 자체는 request log에 남기지 않는다.
|
|
7
2
|
import { api, BaseFrameClass, DB } from "sonamu";
|
|
8
3
|
|
|
9
4
|
import { resolveOpenAITransportKind } from "../../utils/providers/openai/openai-transport-config";
|
|
@@ -49,9 +44,7 @@ class MonitFrameClass extends BaseFrameClass {
|
|
|
49
44
|
};
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
//
|
|
53
|
-
// 분리된 endpoint 로 두고 웹에서 더 느슨한 주기로 폴링한다. 읽기 전용이며 request log 를
|
|
54
|
-
// 남기지 않는 monit 경계는 동일하다.
|
|
47
|
+
// DB를 조회하므로 메모리 기반 vitals보다 느린 주기로 폴링한다.
|
|
55
48
|
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
56
49
|
async monitStats(): Promise<MonitStats> {
|
|
57
50
|
const windowMinutes = 60;
|
|
@@ -60,14 +53,16 @@ class MonitFrameClass extends BaseFrameClass {
|
|
|
60
53
|
}
|
|
61
54
|
}
|
|
62
55
|
|
|
63
|
-
// dispatcher 미초기화(부팅 직후) 시 0 으로 응답한다 — 가벼운 스냅샷이라 오류로 만들지 않는다.
|
|
64
56
|
function currentVitals(): MonitVitals {
|
|
57
|
+
const anthropic = QgridDispatcher.anthropicDispatcher;
|
|
65
58
|
return {
|
|
66
59
|
openaiInFlight: QgridDispatcher.openaiDispatcher?.inFlight ?? 0,
|
|
67
|
-
openaiQuotaByToken: QgridDispatcher.openaiDispatcher?.
|
|
68
|
-
anthropicTokenCount:
|
|
69
|
-
anthropicTokenNames:
|
|
70
|
-
|
|
60
|
+
openaiQuotaByToken: QgridDispatcher.openaiDispatcher?.getQuotaSnapshot() ?? [],
|
|
61
|
+
anthropicTokenCount: anthropic?.tokenPool.size ?? 0,
|
|
62
|
+
anthropicTokenNames: anthropic
|
|
63
|
+
? [...anthropic.tokenPool.values()].map((token) => token.name).toSorted()
|
|
64
|
+
: [],
|
|
65
|
+
anthropicInFlight: anthropic?.inFlight ?? 0,
|
|
71
66
|
};
|
|
72
67
|
}
|
|
73
68
|
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
beginServerRestart,
|
|
5
|
-
resetServerRestartForTests,
|
|
6
|
-
} from "../../utils/server-restart";
|
|
7
3
|
import {
|
|
8
4
|
afterQuery,
|
|
9
5
|
beforeQuery,
|
|
10
|
-
finishActiveNativeRunsForRestart,
|
|
11
6
|
finishRunAborted,
|
|
12
7
|
finishRunWithError,
|
|
13
|
-
resetNativeRunRegistryForTests,
|
|
14
8
|
} from "./qgrid-run-lifecycle";
|
|
15
9
|
import { type QueryOutput } from "./qgrid.types";
|
|
16
10
|
|
|
@@ -65,11 +59,6 @@ function queryOutput(overrides: Partial<QueryOutput> = {}): QueryOutput {
|
|
|
65
59
|
|
|
66
60
|
let lifecycleNow = Date.now();
|
|
67
61
|
|
|
68
|
-
afterEach(() => {
|
|
69
|
-
resetServerRestartForTests();
|
|
70
|
-
resetNativeRunRegistryForTests();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
62
|
describe("qgrid run lifecycle start", () => {
|
|
74
63
|
beforeEach(() => {
|
|
75
64
|
lifecycleNow += 60_001;
|
|
@@ -122,6 +111,13 @@ describe("qgrid run lifecycle start", () => {
|
|
|
122
111
|
});
|
|
123
112
|
});
|
|
124
113
|
|
|
114
|
+
it.each([false, true, undefined])("does not log ignored effort when thinking is %s", async (thinking) => {
|
|
115
|
+
await beforeQuery({ prompt: "hi", model: "openai/gpt-6-sol", thinking, effort: "high" });
|
|
116
|
+
expect(createRunMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
117
|
+
effort: thinking === false ? undefined : "high",
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
|
|
125
121
|
it.each([false, true])("classifies automatic image output only when produced (%s)", async (hasImage) => {
|
|
126
122
|
const args = { prompt: "chat", model: "openai/gpt-6-astra", imageGeneration: "auto" as const };
|
|
127
123
|
const run = await beforeQuery(args);
|
|
@@ -214,107 +210,7 @@ describe("qgrid run lifecycle start", () => {
|
|
|
214
210
|
expect(createRunMock).toHaveBeenCalledTimes(2);
|
|
215
211
|
});
|
|
216
212
|
|
|
217
|
-
it("rejects new native runs while a restart is pending", async () => {
|
|
218
|
-
beginServerRestart();
|
|
219
|
-
|
|
220
|
-
await expect(beforeQuery({ prompt: "hi" })).rejects.toMatchObject({ statusCode: 503 });
|
|
221
|
-
expect(createRunMock).not.toHaveBeenCalled();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("finishes a run prepared during the restart race before rejecting provider execution", async () => {
|
|
225
|
-
let resolveCreate!: (requestLogId: number) => void;
|
|
226
|
-
createRunMock.mockReturnValueOnce(
|
|
227
|
-
new Promise<number>((resolve) => {
|
|
228
|
-
resolveCreate = resolve;
|
|
229
|
-
}),
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
const starting = beforeQuery({ prompt: "hi" });
|
|
233
|
-
await vi.waitFor(() => expect(createRunMock).toHaveBeenCalledOnce());
|
|
234
|
-
beginServerRestart();
|
|
235
|
-
resolveCreate(22);
|
|
236
|
-
|
|
237
|
-
await expect(starting).rejects.toMatchObject({ statusCode: 503 });
|
|
238
|
-
expect(finishRunMock).toHaveBeenCalledWith(
|
|
239
|
-
22,
|
|
240
|
-
expect.objectContaining({ status: "error", error_message: "server restarted" }),
|
|
241
|
-
);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("marks only active native runs as restarted and prevents a late success overwrite", async () => {
|
|
245
|
-
const { requestLogId, stepIndex } = await beforeQuery({ prompt: "hi" });
|
|
246
213
|
|
|
247
|
-
await finishActiveNativeRunsForRestart();
|
|
248
|
-
expect(finishRunMock).toHaveBeenCalledWith(
|
|
249
|
-
requestLogId,
|
|
250
|
-
expect.objectContaining({ status: "error", error_message: "server restarted" }),
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
appendStepMock.mockClear();
|
|
254
|
-
finishRunMock.mockClear();
|
|
255
|
-
await expect(afterQuery(requestLogId, stepIndex, { prompt: "hi" }, queryOutput())).resolves.toEqual(
|
|
256
|
-
{},
|
|
257
|
-
);
|
|
258
|
-
expect(appendStepMock).not.toHaveBeenCalled();
|
|
259
|
-
expect(finishRunMock).not.toHaveBeenCalled();
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("waits for an in-flight finalizer and writes the restart error last", async () => {
|
|
263
|
-
const { requestLogId, stepIndex } = await beforeQuery({ prompt: "hi" });
|
|
264
|
-
let releaseAppend!: () => void;
|
|
265
|
-
appendStepMock.mockReturnValueOnce(
|
|
266
|
-
new Promise<void>((resolve) => {
|
|
267
|
-
releaseAppend = resolve;
|
|
268
|
-
}),
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
const finishing = afterQuery(requestLogId, stepIndex, { prompt: "hi" }, queryOutput());
|
|
272
|
-
await vi.waitFor(() => expect(appendStepMock).toHaveBeenCalledOnce());
|
|
273
|
-
const restarting = finishActiveNativeRunsForRestart();
|
|
274
|
-
|
|
275
|
-
await Promise.resolve();
|
|
276
|
-
expect(finishRunMock).not.toHaveBeenCalledWith(
|
|
277
|
-
requestLogId,
|
|
278
|
-
expect.objectContaining({ error_message: "server restarted" }),
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
releaseAppend();
|
|
282
|
-
await Promise.all([finishing, restarting]);
|
|
283
|
-
|
|
284
|
-
const terminalStatuses = finishRunMock.mock.calls
|
|
285
|
-
.filter(([id]) => id === requestLogId)
|
|
286
|
-
.map(([, payload]) => payload.status);
|
|
287
|
-
expect(terminalStatuses.at(-1)).toBe("error");
|
|
288
|
-
expect(finishRunMock).toHaveBeenLastCalledWith(
|
|
289
|
-
requestLogId,
|
|
290
|
-
expect.objectContaining({ status: "error", error_message: "server restarted" }),
|
|
291
|
-
);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("leaves tool-wait runs alone after their provider turn has completed", async () => {
|
|
295
|
-
const { requestLogId, stepIndex } = await beforeQuery({ prompt: "use a tool" });
|
|
296
|
-
await afterQuery(
|
|
297
|
-
requestLogId,
|
|
298
|
-
stepIndex,
|
|
299
|
-
{ prompt: "use a tool" },
|
|
300
|
-
queryOutput({
|
|
301
|
-
finishReason: "tool-calls",
|
|
302
|
-
content: [
|
|
303
|
-
{
|
|
304
|
-
type: "tool-call",
|
|
305
|
-
toolCallId: "call-1",
|
|
306
|
-
toolName: "lookup",
|
|
307
|
-
input: "{}",
|
|
308
|
-
},
|
|
309
|
-
],
|
|
310
|
-
}),
|
|
311
|
-
);
|
|
312
|
-
finishRunMock.mockClear();
|
|
313
|
-
|
|
314
|
-
await finishActiveNativeRunsForRestart();
|
|
315
|
-
|
|
316
|
-
expect(finishRunMock).not.toHaveBeenCalled();
|
|
317
|
-
});
|
|
318
214
|
});
|
|
319
215
|
|
|
320
216
|
describe("qgrid run lifecycle TTFT", () => {
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
|
2
|
-
import { ServiceUnavailableException } from "sonamu";
|
|
3
2
|
|
|
4
|
-
import { SD } from "../../i18n/sd.generated";
|
|
5
|
-
import { isRestartPending } from "../../utils/server-restart";
|
|
6
3
|
import { RequestLogModel } from "../request-log/request-log.model";
|
|
7
4
|
import {
|
|
8
5
|
estimateImageGenerationCostMicroUsd,
|
|
@@ -20,44 +17,6 @@ const STALE_RUN_THRESHOLD_MS = 30 * 60 * 1000;
|
|
|
20
17
|
const STALE_CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
21
18
|
let staleCleanupInFlight: Promise<void> | undefined;
|
|
22
19
|
let lastStaleCleanupStartedAt = 0;
|
|
23
|
-
const activeNativeRunRefs = new Map<number, number>();
|
|
24
|
-
const interruptedNativeRunIds = new Set<number>();
|
|
25
|
-
const activeNativeFinalizers = new Map<number, Set<Promise<void>>>();
|
|
26
|
-
|
|
27
|
-
function restartUnavailable(): ServiceUnavailableException {
|
|
28
|
-
return new ServiceUnavailableException(SD("qgrid.restartPending")());
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function registerNativeRun(requestLogId: number): void {
|
|
32
|
-
activeNativeRunRefs.set(requestLogId, (activeNativeRunRefs.get(requestLogId) ?? 0) + 1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function releaseNativeRun(requestLogId: number): void {
|
|
36
|
-
const refs = activeNativeRunRefs.get(requestLogId);
|
|
37
|
-
if (refs === undefined || refs <= 1) activeNativeRunRefs.delete(requestLogId);
|
|
38
|
-
else activeNativeRunRefs.set(requestLogId, refs - 1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function trackNativeFinalizer<T>(requestLogId: number, finalize: () => Promise<T>): Promise<T> {
|
|
42
|
-
const operation = finalize();
|
|
43
|
-
const settled = operation.then(
|
|
44
|
-
() => undefined,
|
|
45
|
-
() => undefined,
|
|
46
|
-
);
|
|
47
|
-
const finalizers = activeNativeFinalizers.get(requestLogId) ?? new Set<Promise<void>>();
|
|
48
|
-
finalizers.add(settled);
|
|
49
|
-
activeNativeFinalizers.set(requestLogId, finalizers);
|
|
50
|
-
void settled.then(() => {
|
|
51
|
-
finalizers.delete(settled);
|
|
52
|
-
if (finalizers.size === 0) activeNativeFinalizers.delete(requestLogId);
|
|
53
|
-
});
|
|
54
|
-
return operation;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function assertNativeRunAdmission(): void {
|
|
58
|
-
if (isRestartPending()) throw restartUnavailable();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
20
|
function withProviderPrefix(
|
|
62
21
|
routeModel: string | undefined,
|
|
63
22
|
model: string | undefined,
|
|
@@ -110,8 +69,6 @@ export async function beforeQuery(args: QueryInput): Promise<{
|
|
|
110
69
|
requestLogId: number;
|
|
111
70
|
stepIndex: number;
|
|
112
71
|
}> {
|
|
113
|
-
assertNativeRunAdmission();
|
|
114
|
-
|
|
115
72
|
let requestLogId: number;
|
|
116
73
|
let stepIndex = 0;
|
|
117
74
|
|
|
@@ -131,7 +88,7 @@ export async function beforeQuery(args: QueryInput): Promise<{
|
|
|
131
88
|
user_prompt: args.prompt,
|
|
132
89
|
system_prompt: args.system,
|
|
133
90
|
requested_model_name: args.model,
|
|
134
|
-
effort: args.effort,
|
|
91
|
+
effort: args.thinking === false ? undefined : args.effort,
|
|
135
92
|
project_name: args.projectName,
|
|
136
93
|
history: filterHistoryForStorage(args.history),
|
|
137
94
|
tools: args.tools?.length ? args.tools : undefined,
|
|
@@ -140,14 +97,6 @@ export async function beforeQuery(args: QueryInput): Promise<{
|
|
|
140
97
|
});
|
|
141
98
|
}
|
|
142
99
|
|
|
143
|
-
// DB 준비 중에 재시작이 시작됐다면 provider 실행으로 넘어가지 않는다.
|
|
144
|
-
// 이 요청은 재시작 snapshot에 없었으므로 방금 준비한 run을 직접 마감한다.
|
|
145
|
-
if (isRestartPending()) {
|
|
146
|
-
await finishTerminalRun(requestLogId, "error", "server restarted");
|
|
147
|
-
throw restartUnavailable();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
registerNativeRun(requestLogId);
|
|
151
100
|
return { requestLogId, stepIndex };
|
|
152
101
|
}
|
|
153
102
|
|
|
@@ -157,102 +106,94 @@ export async function afterQuery(
|
|
|
157
106
|
args: QueryInput,
|
|
158
107
|
result: QueryOutput,
|
|
159
108
|
): Promise<RunLifecycleResult> {
|
|
160
|
-
|
|
109
|
+
// provider가 canonical model을 돌려줘도 요청 route 자체([1m] 등)는 보존한다.
|
|
110
|
+
const requestedModelName = withProviderPrefix(
|
|
111
|
+
args.model,
|
|
112
|
+
args.model ?? result.requestedModel ?? result.model,
|
|
113
|
+
);
|
|
114
|
+
const servedModelName = withProviderPrefix(args.model, result.model);
|
|
115
|
+
|
|
116
|
+
// generate step 기록
|
|
117
|
+
await RequestLogModel.appendStep(requestLogId, {
|
|
118
|
+
step_index: stepIndex,
|
|
119
|
+
type: "generate",
|
|
120
|
+
model_name: servedModelName,
|
|
121
|
+
requested_model_name: requestedModelName,
|
|
122
|
+
fallback_count: result.modelFallbacks?.length ?? 0,
|
|
123
|
+
input_tokens: result.usage.input_tokens,
|
|
124
|
+
output_tokens: result.usage.output_tokens,
|
|
125
|
+
cache_read_tokens: result.usage.cache_read_input_tokens,
|
|
126
|
+
cache_creation_tokens: result.usage.cache_creation_input_tokens,
|
|
127
|
+
cache_creation_5m_tokens: result.usage.cache_creation_5m_input_tokens,
|
|
128
|
+
cache_creation_1h_tokens: result.usage.cache_creation_1h_input_tokens,
|
|
129
|
+
cost_usd: Math.round(result.costUsd * 1_000_000),
|
|
130
|
+
cost_source: result.costSource,
|
|
131
|
+
duration_ms: result.durationMs,
|
|
132
|
+
ttft_ms: result.ttftMs,
|
|
133
|
+
finish_reason: result.finishReason,
|
|
134
|
+
});
|
|
161
135
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
136
|
+
const imageParts = getImageParts(result);
|
|
137
|
+
const imageCostMicroUsd = estimateImageGenerationCostMicroUsd(
|
|
138
|
+
result,
|
|
139
|
+
args.imageGenerationOptions,
|
|
140
|
+
);
|
|
141
|
+
for (const step of buildImageGenerationToolSteps(args, imageParts, stepIndex)) {
|
|
142
|
+
await RequestLogModel.appendStep(requestLogId, step);
|
|
143
|
+
}
|
|
170
144
|
|
|
171
|
-
|
|
145
|
+
if (result.finishReason === "tool-calls") {
|
|
146
|
+
// tool-call step을 즉시 기록하고, 다음 follow-up이 같은 row에 결과를 채운다.
|
|
147
|
+
const toolCalls = result.content.filter((c) => c.type === "tool-call");
|
|
148
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
149
|
+
const tc = toolCalls[i]!;
|
|
150
|
+
if (tc.type !== "tool-call") continue;
|
|
172
151
|
await RequestLogModel.appendStep(requestLogId, {
|
|
173
152
|
step_index: stepIndex,
|
|
174
|
-
type: "
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
output_tokens: result.usage.output_tokens,
|
|
180
|
-
cache_read_tokens: result.usage.cache_read_input_tokens,
|
|
181
|
-
cache_creation_tokens: result.usage.cache_creation_input_tokens,
|
|
182
|
-
cache_creation_5m_tokens: result.usage.cache_creation_5m_input_tokens,
|
|
183
|
-
cache_creation_1h_tokens: result.usage.cache_creation_1h_input_tokens,
|
|
184
|
-
cost_usd: Math.round(result.costUsd * 1_000_000),
|
|
185
|
-
cost_source: result.costSource,
|
|
186
|
-
duration_ms: result.durationMs,
|
|
187
|
-
ttft_ms: result.ttftMs,
|
|
188
|
-
finish_reason: result.finishReason,
|
|
153
|
+
type: "tool_call",
|
|
154
|
+
tool_call_index: imageParts.length + i,
|
|
155
|
+
tool_call_id: tc.toolCallId,
|
|
156
|
+
tool_name: tc.toolName,
|
|
157
|
+
tool_args: tc.input,
|
|
189
158
|
});
|
|
159
|
+
}
|
|
190
160
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
result,
|
|
194
|
-
args.imageGenerationOptions,
|
|
195
|
-
);
|
|
196
|
-
for (const step of buildImageGenerationToolSteps(args, imageParts, stepIndex)) {
|
|
197
|
-
await RequestLogModel.appendStep(requestLogId, step);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (result.finishReason === "tool-calls") {
|
|
201
|
-
// tool-call step을 즉시 기록하고, 다음 follow-up이 같은 row에 결과를 채운다.
|
|
202
|
-
const toolCalls = result.content.filter((c) => c.type === "tool-call");
|
|
203
|
-
for (let i = 0; i < toolCalls.length; i++) {
|
|
204
|
-
const tc = toolCalls[i]!;
|
|
205
|
-
if (tc.type !== "tool-call") continue;
|
|
206
|
-
await RequestLogModel.appendStep(requestLogId, {
|
|
207
|
-
step_index: stepIndex,
|
|
208
|
-
type: "tool_call",
|
|
209
|
-
tool_call_index: imageParts.length + i,
|
|
210
|
-
tool_call_id: tc.toolCallId,
|
|
211
|
-
tool_name: tc.toolName,
|
|
212
|
-
tool_args: tc.input,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return { runContext: { requestLogId } };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// finishReason === "stop" → run 종료 (전체 step usage 합산)
|
|
220
|
-
const agg = await RequestLogModel.aggregateStepUsage(requestLogId);
|
|
221
|
-
const responseText = formatResponseForLog(result);
|
|
222
|
-
await RequestLogModel.finishRun(requestLogId, {
|
|
223
|
-
status: "succeeded",
|
|
224
|
-
response: responseText,
|
|
225
|
-
// structured 요청만 판정한다. deti 배치의 broken JSON 류를 목록에서 바로
|
|
226
|
-
// 구별하기 위한 기록이며, 비structured 요청은 null 로 남는다.
|
|
227
|
-
response_json_ok: args.jsonSchema ? isParseableJson(responseText) : null,
|
|
228
|
-
token_name: result.tokenName,
|
|
229
|
-
input_tokens: agg.input_tokens,
|
|
230
|
-
output_tokens: agg.output_tokens,
|
|
231
|
-
cache_read_tokens: agg.cache_read_tokens,
|
|
232
|
-
cache_creation_tokens: agg.cache_creation_tokens,
|
|
233
|
-
cache_creation_5m_tokens: agg.cache_creation_5m_tokens,
|
|
234
|
-
cache_creation_1h_tokens: agg.cache_creation_1h_tokens,
|
|
235
|
-
duration_ms: agg.duration_ms,
|
|
236
|
-
// multi-step run 부모의 모델은 최종 turn을 서빙한 모델이다.
|
|
237
|
-
requested_model_name: requestedModelName,
|
|
238
|
-
model_name: servedModelName,
|
|
239
|
-
fallback_count: agg.fallback_count,
|
|
240
|
-
cost_usd: agg.cost_usd,
|
|
241
|
-
cost_source: agg.cost_source,
|
|
242
|
-
image_cost_usd: imageCostMicroUsd,
|
|
243
|
-
...(imageParts.length > 0 ? { is_image_generation: true } : {}),
|
|
244
|
-
image_cost_method:
|
|
245
|
-
imageCostMicroUsd !== null
|
|
246
|
-
? imageGenerationCostMethod(args.imageGenerationOptions, result)
|
|
247
|
-
: null,
|
|
248
|
-
history: filterHistoryForStorage(args.history),
|
|
249
|
-
});
|
|
161
|
+
return { runContext: { requestLogId } };
|
|
162
|
+
}
|
|
250
163
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
164
|
+
// finishReason === "stop" → run 종료 (전체 step usage 합산)
|
|
165
|
+
const agg = await RequestLogModel.aggregateStepUsage(requestLogId);
|
|
166
|
+
const responseText = formatResponseForLog(result);
|
|
167
|
+
await RequestLogModel.finishRun(requestLogId, {
|
|
168
|
+
status: "succeeded",
|
|
169
|
+
response: responseText,
|
|
170
|
+
// structured 요청만 판정한다. deti 배치의 broken JSON 류를 목록에서 바로
|
|
171
|
+
// 구별하기 위한 기록이며, 비structured 요청은 null 로 남는다.
|
|
172
|
+
response_json_ok: args.jsonSchema ? isParseableJson(responseText) : null,
|
|
173
|
+
token_name: result.tokenName,
|
|
174
|
+
input_tokens: agg.input_tokens,
|
|
175
|
+
output_tokens: agg.output_tokens,
|
|
176
|
+
cache_read_tokens: agg.cache_read_tokens,
|
|
177
|
+
cache_creation_tokens: agg.cache_creation_tokens,
|
|
178
|
+
cache_creation_5m_tokens: agg.cache_creation_5m_tokens,
|
|
179
|
+
cache_creation_1h_tokens: agg.cache_creation_1h_tokens,
|
|
180
|
+
duration_ms: agg.duration_ms,
|
|
181
|
+
// multi-step run 부모의 모델은 최종 turn을 서빙한 모델이다.
|
|
182
|
+
requested_model_name: requestedModelName,
|
|
183
|
+
model_name: servedModelName,
|
|
184
|
+
fallback_count: agg.fallback_count,
|
|
185
|
+
cost_usd: agg.cost_usd,
|
|
186
|
+
cost_source: agg.cost_source,
|
|
187
|
+
image_cost_usd: imageCostMicroUsd,
|
|
188
|
+
...(imageParts.length > 0 ? { is_image_generation: true } : {}),
|
|
189
|
+
image_cost_method:
|
|
190
|
+
imageCostMicroUsd !== null
|
|
191
|
+
? imageGenerationCostMethod(args.imageGenerationOptions, result)
|
|
192
|
+
: null,
|
|
193
|
+
history: filterHistoryForStorage(args.history),
|
|
255
194
|
});
|
|
195
|
+
|
|
196
|
+
return {};
|
|
256
197
|
}
|
|
257
198
|
|
|
258
199
|
export async function finishRunWithError(
|
|
@@ -260,48 +201,11 @@ export async function finishRunWithError(
|
|
|
260
201
|
errorMessage: string,
|
|
261
202
|
args?: QueryInput,
|
|
262
203
|
): Promise<void> {
|
|
263
|
-
|
|
264
|
-
return trackNativeFinalizer(requestLogId, async () => {
|
|
265
|
-
try {
|
|
266
|
-
await finishTerminalRun(requestLogId, "error", errorMessage, args);
|
|
267
|
-
} finally {
|
|
268
|
-
releaseNativeRun(requestLogId);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
204
|
+
await finishTerminalRun(requestLogId, "error", errorMessage, args);
|
|
271
205
|
}
|
|
272
206
|
|
|
273
207
|
export async function finishRunAborted(requestLogId: number, args?: QueryInput): Promise<void> {
|
|
274
|
-
|
|
275
|
-
return trackNativeFinalizer(requestLogId, async () => {
|
|
276
|
-
try {
|
|
277
|
-
await finishTerminalRun(requestLogId, "aborted", "client disconnected", args);
|
|
278
|
-
} finally {
|
|
279
|
-
releaseNativeRun(requestLogId);
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** 현재 이 프로세스가 provider를 실행 중인 native run만 재시작 오류로 마감한다. */
|
|
285
|
-
export async function finishActiveNativeRunsForRestart(): Promise<void> {
|
|
286
|
-
const requestLogIds = [...activeNativeRunRefs.keys()];
|
|
287
|
-
for (const requestLogId of requestLogIds) {
|
|
288
|
-
interruptedNativeRunIds.add(requestLogId);
|
|
289
|
-
activeNativeRunRefs.delete(requestLogId);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
await Promise.all(
|
|
293
|
-
requestLogIds.map(async (requestLogId) => {
|
|
294
|
-
await Promise.all(activeNativeFinalizers.get(requestLogId) ?? []);
|
|
295
|
-
await finishTerminalRun(requestLogId, "error", "server restarted");
|
|
296
|
-
}),
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** 프로세스 전역 registry를 격리해야 하는 단위 테스트 전용. */
|
|
301
|
-
export function resetNativeRunRegistryForTests(): void {
|
|
302
|
-
activeNativeRunRefs.clear();
|
|
303
|
-
interruptedNativeRunIds.clear();
|
|
304
|
-
activeNativeFinalizers.clear();
|
|
208
|
+
await finishTerminalRun(requestLogId, "aborted", "client disconnected", args);
|
|
305
209
|
}
|
|
306
210
|
|
|
307
211
|
async function finishTerminalRun(
|