@tonyclaw/agent-inspector 3.0.28 → 3.0.30
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/.output/cli.js +791 -191
- package/.output/nitro.json +1 -1
- package/.output/public/assets/CompareDrawer-B6cXJohL.js +1 -0
- package/.output/public/assets/InspectorPet-mAzeGmEE.js +4108 -0
- package/.output/public/assets/ProxyViewerContainer-CkFWv_Nm.js +126 -0
- package/.output/public/assets/{ReplayDialog-CZMApaF4.js → ReplayDialog-DodiTL6E.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DNWccR7O.js → RequestAnatomy-B3IauToI.js} +1 -1
- package/.output/public/assets/ResponseView-Hn-5ordK.js +3 -0
- package/.output/public/assets/StreamingChunkSequence-vLDOVQM_.js +1 -0
- package/.output/public/assets/_sessionId-BFH4TOaI.js +1 -0
- package/.output/public/assets/{_sessionId-DRfvuE2k.js → _sessionId-Cj_HIFZx.js} +1 -1
- package/.output/public/assets/{index-D4HkS7Ct.js → index-Bmj2lcWE.js} +1 -1
- package/.output/public/assets/index-CgJj-HBC.css +1 -0
- package/.output/public/assets/index-Ch7uqnCT.js +1 -0
- package/.output/public/assets/index-DkDdKvLl.js +76 -0
- package/.output/public/assets/{index-B3aAuqWq.js → index-ZlhqCrSg.js} +6 -6
- package/.output/public/assets/{json-viewer-wuJSKaed.js → json-viewer-BdrnVQu-.js} +1 -1
- package/.output/public/assets/jszip.min-CS_nt_o1.js +2 -0
- package/.output/public/assets/qwen-mMn3f5ul.webp +0 -0
- package/.output/server/_libs/jszip.mjs +35 -9
- package/.output/server/_libs/lucide-react.mjs +86 -86
- package/.output/server/_libs/radix-ui__react-dialog.mjs +3 -3
- package/.output/server/_libs/tanstack__react-router.mjs +1 -1
- package/.output/server/{_sessionId-BJTNiP_O.mjs → _sessionId-C5S3pGZd.mjs} +15 -15
- package/.output/server/_sessionId-g6D69lKq.mjs +81 -0
- package/.output/server/_ssr/{CompareDrawer-BMRH3N97.mjs → CompareDrawer-CqUmGHal.mjs} +138 -103
- package/.output/server/_ssr/InspectorPet-DI0UJd01.mjs +925 -0
- package/.output/server/_ssr/{ProxyViewerContainer-AFwcHAIR.mjs → ProxyViewerContainer-CvsUmbJ3.mjs} +1216 -1659
- package/.output/server/_ssr/{ReplayDialog-CbWvRHiN.mjs → ReplayDialog-DS48dpFG.mjs} +18 -18
- package/.output/server/_ssr/{RequestAnatomy-C4kcPyqN.mjs → RequestAnatomy-DxYVQU1C.mjs} +58 -58
- package/.output/server/_ssr/{ResponseView-D8VYt-gZ.mjs → ResponseView-BLXW73eq.mjs} +60 -60
- package/.output/server/_ssr/{StreamingChunkSequence-BUUo6vU0.mjs → StreamingChunkSequence-DD0iFuy0.mjs} +16 -16
- package/.output/server/_ssr/{index-DTwMEEXO.mjs → index-B10LIaK0.mjs} +15 -15
- package/.output/server/_ssr/index-RHVJnU2p.mjs +81 -0
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-BqF1pSQD.mjs → json-viewer-LDu2XBaK.mjs} +58 -58
- package/.output/server/_ssr/{router-C8BOns7y.mjs → router-GKvrwifJ.mjs} +9005 -6488
- package/.output/server/_tanstack-start-manifest_v-Df2n4mbT.mjs +4 -0
- package/.output/server/index.mjs +105 -77
- package/.output/workers/logFinalizer.worker.js +16908 -0
- package/.output/workers/sessionWorkerEntry.js +16904 -0
- package/README.md +21 -5
- package/package.json +6 -2
- package/src/assets/logos/qwen.webp +0 -0
- package/src/cli.ts +85 -10
- package/src/components/OnboardingBanner.tsx +41 -21
- package/src/components/ProxyViewer.tsx +198 -269
- package/src/components/ProxyViewerContainer.tsx +128 -17
- package/src/components/clients/ClientLogo.tsx +9 -6
- package/src/components/ecosystem/AgentLabDialog.tsx +166 -19
- package/src/components/errors/SafeErrorBoundary.tsx +201 -0
- package/src/components/pi-agent/PiAgentPanel.tsx +4 -1
- package/src/components/providers/ImportWizardDialog.tsx +18 -42
- package/src/components/providers/ProviderLogo.tsx +1 -1
- package/src/components/providers/ProvidersPanel.tsx +8 -5
- package/src/components/providers/SettingsDialog.tsx +34 -11
- package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
- package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
- package/src/components/proxy-viewer/ConversationGroupList.tsx +49 -34
- package/src/components/proxy-viewer/ConversationHeader.tsx +22 -18
- package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
- package/src/components/proxy-viewer/LogEntry.tsx +283 -275
- package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
- package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
- package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
- package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
- package/src/components/proxy-viewer/accessibility.ts +8 -0
- package/src/components/proxy-viewer/lazy.ts +4 -0
- package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
- package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
- package/src/components/proxy-viewer/viewerState.ts +2 -6
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/tabs.tsx +3 -3
- package/src/components/ui/transient-toast.tsx +1 -1
- package/src/lib/apiClient.ts +17 -2
- package/src/lib/ecosystemContract.ts +34 -0
- package/src/lib/export-logs.ts +1 -1
- package/src/lib/providerContract.ts +70 -4
- package/src/lib/providerImportContract.ts +27 -0
- package/src/lib/providerModelMetadata.ts +16 -7
- package/src/lib/resourceLimits.ts +152 -0
- package/src/lib/safeDiagnostic.ts +38 -0
- package/src/lib/useProviders.ts +4 -4
- package/src/lib/utils.ts +5 -1
- package/src/mcp/server.ts +39 -6
- package/src/mcp/toolHandlers.ts +131 -4
- package/src/proxy/chunkStorage.ts +20 -6
- package/src/proxy/config.ts +20 -6
- package/src/proxy/dataDir.ts +3 -0
- package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
- package/src/proxy/ecosystemExecutionRoute.ts +116 -0
- package/src/proxy/ecosystemTasks.ts +48 -0
- package/src/proxy/evidenceExporter.ts +23 -9
- package/src/proxy/groupEvidenceExporter.ts +26 -5
- package/src/proxy/groupStore.ts +13 -3
- package/src/proxy/handler.ts +103 -20
- package/src/proxy/identityProxy.ts +333 -14
- package/src/proxy/logFinalizer.ts +73 -6
- package/src/proxy/logImportUpload.ts +128 -0
- package/src/proxy/logImporter.ts +321 -70
- package/src/proxy/logIndex.ts +16 -6
- package/src/proxy/logger.ts +294 -41
- package/src/proxy/privateDataPath.ts +183 -0
- package/src/proxy/providerScanStore.ts +87 -0
- package/src/proxy/providerSecretStore.ts +58 -19
- package/src/proxy/providers.ts +175 -58
- package/src/proxy/rawStreamCapture.ts +66 -5
- package/src/proxy/runStore.ts +13 -3
- package/src/proxy/runtimeAdmission.ts +52 -0
- package/src/proxy/runtimeHealth.ts +206 -0
- package/src/proxy/runtimeShutdown.ts +75 -0
- package/src/proxy/sessionArchive.ts +15 -2
- package/src/proxy/sessionProcess.ts +19 -0
- package/src/proxy/sessionRuntime.ts +7 -0
- package/src/proxy/shutdownCoordinator.ts +90 -0
- package/src/proxy/sqliteLogIndex.ts +18 -2
- package/src/proxy/store.ts +18 -2
- package/src/routes/__root.tsx +18 -1
- package/src/routes/api/alerts.summary.ts +12 -8
- package/src/routes/api/alerts.ts +27 -9
- package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
- package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
- package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
- package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
- package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
- package/src/routes/api/health.ts +9 -2
- package/src/routes/api/knowledge.project-context.ts +11 -0
- package/src/routes/api/knowledge.search.ts +33 -2
- package/src/routes/api/logs.$id.body.ts +16 -2
- package/src/routes/api/logs.import.ts +7 -18
- package/src/routes/api/logs.stream.ts +147 -69
- package/src/routes/api/logs.ts +55 -14
- package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
- package/src/routes/api/providers.$providerId.ts +37 -6
- package/src/routes/api/providers.export.ts +34 -16
- package/src/routes/api/providers.import.ts +42 -8
- package/src/routes/api/providers.scan.ts +13 -8
- package/src/routes/api/providers.ts +36 -5
- package/src/routes/api/runs.ts +12 -12
- package/src/routes/api/sessions.ts +15 -8
- package/src/routes/index.tsx +6 -0
- package/src/routes/livez.ts +13 -0
- package/src/routes/readyz.ts +18 -0
- package/src/routes/session/$sessionId.tsx +6 -0
- package/styles/globals.css +19 -3
- package/.output/public/assets/CompareDrawer-TghZr5t1.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-fShBAZjj.js +0 -4234
- package/.output/public/assets/ResponseView-d25MdFhY.js +0 -3
- package/.output/public/assets/StreamingChunkSequence-DIFZEnWe.js +0 -1
- package/.output/public/assets/index-BsxAGqMA.css +0 -1
- package/.output/public/assets/index-DzARL2Fi.js +0 -76
- package/.output/public/assets/qwen-CONDcHqt.png +0 -0
- package/.output/server/_tanstack-start-manifest_v-BBf2nRhb.mjs +0 -4
- package/src/assets/logos/mcp.png +0 -0
- package/src/assets/logos/qwen.png +0 -0
- package/src/components/ui/mcp-logo.tsx +0 -20
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
1
2
|
import { Buffer } from "node:buffer";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
+
import { open, unlink, type FileHandle } from "node:fs/promises";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { getDataDir } from "./dataDir";
|
|
7
|
+
import {
|
|
8
|
+
ensurePrivateDirectorySync,
|
|
9
|
+
PRIVATE_FILE_MODE,
|
|
10
|
+
securePrivateFileSync,
|
|
11
|
+
} from "./privateDataPath";
|
|
6
12
|
|
|
7
13
|
const DEFAULT_MEMORY_LIMIT_BYTES = 1024 * 1024;
|
|
14
|
+
const FILE_WRITE_BATCH_BYTES = 64 * 1024;
|
|
15
|
+
const activeCaptures = new Set<RawStreamCapture>();
|
|
8
16
|
|
|
9
17
|
export type RawStreamSource =
|
|
10
18
|
| {
|
|
@@ -37,10 +45,16 @@ export class RawStreamCapture {
|
|
|
37
45
|
private chunks: string[] = [];
|
|
38
46
|
private byteLength = 0;
|
|
39
47
|
private filePath: string | null = null;
|
|
48
|
+
private fileHandle: FileHandle | null = null;
|
|
49
|
+
private pendingFileChunks: string[] = [];
|
|
50
|
+
private pendingFileBytes = 0;
|
|
51
|
+
private operation = Promise.resolve();
|
|
52
|
+
private closed = false;
|
|
40
53
|
private sawChunk = false;
|
|
41
54
|
|
|
42
55
|
constructor(memoryLimitBytes: number = resolveMemoryLimitBytes()) {
|
|
43
56
|
this.memoryLimitBytes = memoryLimitBytes;
|
|
57
|
+
activeCaptures.add(this);
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
get hasChunks(): boolean {
|
|
@@ -49,6 +63,22 @@ export class RawStreamCapture {
|
|
|
49
63
|
|
|
50
64
|
async append(text: string): Promise<void> {
|
|
51
65
|
if (text === "") return;
|
|
66
|
+
const next = this.operation.then(() => this.appendInternal(text));
|
|
67
|
+
this.operation = next.catch(() => undefined);
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async finalize(): Promise<RawStreamSource> {
|
|
72
|
+
const next = this.operation.then(() => this.finalizeInternal());
|
|
73
|
+
this.operation = next.then(
|
|
74
|
+
() => undefined,
|
|
75
|
+
() => undefined,
|
|
76
|
+
);
|
|
77
|
+
return next.finally(() => activeCaptures.delete(this));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async appendInternal(text: string): Promise<void> {
|
|
81
|
+
if (this.closed) return;
|
|
52
82
|
this.sawChunk = true;
|
|
53
83
|
|
|
54
84
|
const nextBytes = Buffer.byteLength(text, "utf-8");
|
|
@@ -63,12 +93,23 @@ export class RawStreamCapture {
|
|
|
63
93
|
}
|
|
64
94
|
|
|
65
95
|
if (this.filePath !== null) {
|
|
66
|
-
|
|
96
|
+
this.pendingFileChunks.push(text);
|
|
97
|
+
this.pendingFileBytes += nextBytes;
|
|
67
98
|
this.byteLength += nextBytes;
|
|
99
|
+
if (this.pendingFileBytes >= FILE_WRITE_BATCH_BYTES) await this.flushFileBuffer();
|
|
68
100
|
}
|
|
69
101
|
}
|
|
70
102
|
|
|
71
|
-
|
|
103
|
+
private async finalizeInternal(): Promise<RawStreamSource> {
|
|
104
|
+
if (!this.closed) {
|
|
105
|
+
this.closed = true;
|
|
106
|
+
await this.flushFileBuffer();
|
|
107
|
+
if (this.fileHandle !== null) {
|
|
108
|
+
await this.fileHandle.sync();
|
|
109
|
+
await this.fileHandle.close();
|
|
110
|
+
this.fileHandle = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
72
113
|
if (this.filePath !== null) {
|
|
73
114
|
return { type: "file", path: this.filePath };
|
|
74
115
|
}
|
|
@@ -77,9 +118,29 @@ export class RawStreamCapture {
|
|
|
77
118
|
|
|
78
119
|
private async spillToFile(): Promise<void> {
|
|
79
120
|
const path = createRawStreamPath();
|
|
80
|
-
|
|
81
|
-
await
|
|
121
|
+
assert(ensurePrivateDirectorySync(getRawStreamDir()), "Could not secure raw-stream directory");
|
|
122
|
+
const handle = await open(path, "wx", PRIVATE_FILE_MODE);
|
|
123
|
+
const memoryText = this.chunks.join("");
|
|
124
|
+
if (memoryText.length > 0) await handle.write(memoryText, undefined, "utf-8");
|
|
125
|
+
if (!securePrivateFileSync(path)) {
|
|
126
|
+
await handle.close();
|
|
127
|
+
await unlink(path);
|
|
128
|
+
assert(false, "Could not secure raw-stream file");
|
|
129
|
+
}
|
|
82
130
|
this.filePath = path;
|
|
131
|
+
this.fileHandle = handle;
|
|
83
132
|
this.chunks = [];
|
|
84
133
|
}
|
|
134
|
+
|
|
135
|
+
private async flushFileBuffer(): Promise<void> {
|
|
136
|
+
if (this.fileHandle === null || this.pendingFileChunks.length === 0) return;
|
|
137
|
+
const text = this.pendingFileChunks.join("");
|
|
138
|
+
this.pendingFileChunks = [];
|
|
139
|
+
this.pendingFileBytes = 0;
|
|
140
|
+
await this.fileHandle.write(text, undefined, "utf-8");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function flushAllRawStreamCaptures(): Promise<void> {
|
|
145
|
+
await Promise.all([...activeCaptures].map((capture) => capture.finalize()));
|
|
85
146
|
}
|
package/src/proxy/runStore.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
import { z } from "zod";
|
|
@@ -14,6 +15,11 @@ import {
|
|
|
14
15
|
import { getDataDir } from "./dataDir";
|
|
15
16
|
import { addGroupSession, getGroup } from "./groupStore";
|
|
16
17
|
import { readJsonFile } from "../lib/jsonFile";
|
|
18
|
+
import {
|
|
19
|
+
ensurePrivateDirectorySync,
|
|
20
|
+
PRIVATE_FILE_MODE,
|
|
21
|
+
securePrivateFileSync,
|
|
22
|
+
} from "./privateDataPath";
|
|
17
23
|
|
|
18
24
|
const RunsFileSchema = z.object({
|
|
19
25
|
runs: z.array(InspectorRunSchema),
|
|
@@ -102,8 +108,12 @@ function readRuns(): InspectorRun[] {
|
|
|
102
108
|
|
|
103
109
|
function writeRuns(runs: readonly InspectorRun[]): void {
|
|
104
110
|
const filePath = runsFilePath();
|
|
105
|
-
|
|
106
|
-
writeFileSync(filePath, `${JSON.stringify({ runs }, null, 2)}\n`,
|
|
111
|
+
assert(ensurePrivateDirectorySync(dirname(filePath)), "Could not secure runs directory");
|
|
112
|
+
writeFileSync(filePath, `${JSON.stringify({ runs }, null, 2)}\n`, {
|
|
113
|
+
encoding: "utf8",
|
|
114
|
+
mode: PRIVATE_FILE_MODE,
|
|
115
|
+
});
|
|
116
|
+
assert(securePrivateFileSync(filePath), "Could not secure runs file");
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
export function listRuns(): InspectorRun[] {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type RuntimeRequestLease = {
|
|
2
|
+
release: () => void;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
let acceptingRequests = true;
|
|
6
|
+
let activeRequests = 0;
|
|
7
|
+
const drainWaiters = new Set<() => void>();
|
|
8
|
+
|
|
9
|
+
function notifyDrained(): void {
|
|
10
|
+
if (activeRequests !== 0) return;
|
|
11
|
+
for (const resolve of drainWaiters) resolve();
|
|
12
|
+
drainWaiters.clear();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function acquireRuntimeRequest(): RuntimeRequestLease | null {
|
|
16
|
+
if (!acceptingRequests) return null;
|
|
17
|
+
activeRequests += 1;
|
|
18
|
+
let released = false;
|
|
19
|
+
return {
|
|
20
|
+
release(): void {
|
|
21
|
+
if (released) return;
|
|
22
|
+
released = true;
|
|
23
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
24
|
+
notifyDrained();
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function stopRuntimeAdmission(): void {
|
|
30
|
+
acceptingRequests = false;
|
|
31
|
+
notifyDrained();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function waitForRuntimeTrafficDrain(): Promise<void> {
|
|
35
|
+
if (activeRequests === 0) return;
|
|
36
|
+
await new Promise<void>((resolve) => {
|
|
37
|
+
drainWaiters.add(resolve);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getRuntimeAdmissionState(): {
|
|
42
|
+
acceptingRequests: boolean;
|
|
43
|
+
activeRequests: number;
|
|
44
|
+
} {
|
|
45
|
+
return { acceptingRequests, activeRequests };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function _resetRuntimeAdmissionForTests(): void {
|
|
49
|
+
acceptingRequests = true;
|
|
50
|
+
activeRequests = 0;
|
|
51
|
+
notifyDrained();
|
|
52
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { writeFile, rm, statfs } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getDataDir } from "./dataDir";
|
|
5
|
+
import { getFinalizerRuntimeHealth } from "./logFinalizer";
|
|
6
|
+
import { ensurePrivateDirectorySync, PRIVATE_FILE_MODE } from "./privateDataPath";
|
|
7
|
+
import { isSqliteLogIndexReady } from "./sqliteLogIndex";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_CACHE_MS = 2_000;
|
|
10
|
+
const DEFAULT_MIN_DISK_HEADROOM_BYTES = 256 * 1024 * 1024;
|
|
11
|
+
const DEFAULT_MAX_FINALIZER_QUEUE = 256;
|
|
12
|
+
|
|
13
|
+
export type ReadinessLevel = "ready" | "degraded" | "unavailable";
|
|
14
|
+
export type ReadinessCheckLevel = "ok" | "degraded" | "unavailable";
|
|
15
|
+
|
|
16
|
+
export type SafeReadiness = {
|
|
17
|
+
status: ReadinessLevel;
|
|
18
|
+
checks: {
|
|
19
|
+
storage: ReadinessCheckLevel;
|
|
20
|
+
disk: ReadinessCheckLevel;
|
|
21
|
+
workers: ReadinessCheckLevel;
|
|
22
|
+
index: ReadinessCheckLevel;
|
|
23
|
+
queue: ReadinessCheckLevel;
|
|
24
|
+
shutdown: ReadinessCheckLevel;
|
|
25
|
+
};
|
|
26
|
+
checkedAt: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ReadinessDependencies = {
|
|
30
|
+
checkStorage: () => Promise<ReadinessCheckLevel>;
|
|
31
|
+
checkDisk: () => Promise<ReadinessCheckLevel>;
|
|
32
|
+
checkWorkers: () => Promise<ReadinessCheckLevel>;
|
|
33
|
+
checkIndex: () => Promise<ReadinessCheckLevel>;
|
|
34
|
+
checkQueue: () => Promise<ReadinessCheckLevel>;
|
|
35
|
+
checkShutdown: () => Promise<ReadinessCheckLevel>;
|
|
36
|
+
now: () => number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ReadinessService = {
|
|
40
|
+
get: () => Promise<SafeReadiness>;
|
|
41
|
+
invalidate: () => void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let runtimeShuttingDown = false;
|
|
45
|
+
|
|
46
|
+
function configuredPositiveInteger(name: string, fallback: number): number {
|
|
47
|
+
const raw = process.env[name];
|
|
48
|
+
if (raw === undefined || raw === "") return fallback;
|
|
49
|
+
const parsed = Number(raw);
|
|
50
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function checkStorageWritable(): Promise<ReadinessCheckLevel> {
|
|
54
|
+
const dataDir = getDataDir();
|
|
55
|
+
if (!ensurePrivateDirectorySync(dataDir)) return "unavailable";
|
|
56
|
+
const probePath = join(dataDir, `.readiness-${String(process.pid)}-${randomUUID()}`);
|
|
57
|
+
try {
|
|
58
|
+
await writeFile(probePath, "", { flag: "wx", mode: PRIVATE_FILE_MODE });
|
|
59
|
+
return "ok";
|
|
60
|
+
} catch {
|
|
61
|
+
return "unavailable";
|
|
62
|
+
} finally {
|
|
63
|
+
await rm(probePath, { force: true }).catch(() => undefined);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function checkDiskHeadroom(): Promise<ReadinessCheckLevel> {
|
|
68
|
+
try {
|
|
69
|
+
const stats = await statfs(getDataDir());
|
|
70
|
+
const availableBytes = stats.bavail * stats.bsize;
|
|
71
|
+
const minimumBytes = configuredPositiveInteger(
|
|
72
|
+
"AGENT_INSPECTOR_MIN_DISK_HEADROOM_BYTES",
|
|
73
|
+
DEFAULT_MIN_DISK_HEADROOM_BYTES,
|
|
74
|
+
);
|
|
75
|
+
return availableBytes >= minimumBytes ? "ok" : "unavailable";
|
|
76
|
+
} catch {
|
|
77
|
+
return "degraded";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function finalizerChecks(): {
|
|
82
|
+
workers: ReadinessCheckLevel;
|
|
83
|
+
queue: ReadinessCheckLevel;
|
|
84
|
+
} {
|
|
85
|
+
const health = getFinalizerRuntimeHealth();
|
|
86
|
+
const runtimeArtifactMissing = health.runtime !== "inline" && !health.artifactAvailable;
|
|
87
|
+
const initializedWorkerMissing =
|
|
88
|
+
health.runtime === "worker" &&
|
|
89
|
+
health.activeWorkers !== null &&
|
|
90
|
+
health.activeWorkers < health.configuredWorkers;
|
|
91
|
+
const workers =
|
|
92
|
+
runtimeArtifactMissing || initializedWorkerMissing
|
|
93
|
+
? "unavailable"
|
|
94
|
+
: health.runtime === "inline"
|
|
95
|
+
? "degraded"
|
|
96
|
+
: "ok";
|
|
97
|
+
|
|
98
|
+
const queueLimit = configuredPositiveInteger(
|
|
99
|
+
"AGENT_INSPECTOR_READY_MAX_FINALIZER_QUEUE",
|
|
100
|
+
DEFAULT_MAX_FINALIZER_QUEUE,
|
|
101
|
+
);
|
|
102
|
+
const warningThreshold = Math.max(1, Math.floor(queueLimit * 0.8));
|
|
103
|
+
const queue =
|
|
104
|
+
health.pendingJobs >= queueLimit
|
|
105
|
+
? "unavailable"
|
|
106
|
+
: health.pendingJobs >= warningThreshold
|
|
107
|
+
? "degraded"
|
|
108
|
+
: "ok";
|
|
109
|
+
return { workers, queue };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function checkWorkers(): Promise<ReadinessCheckLevel> {
|
|
113
|
+
return Promise.resolve(finalizerChecks().workers);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function checkQueue(): Promise<ReadinessCheckLevel> {
|
|
117
|
+
return Promise.resolve(finalizerChecks().queue);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function checkIndex(): Promise<ReadinessCheckLevel> {
|
|
121
|
+
return (await isSqliteLogIndexReady()) ? "ok" : "degraded";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function checkShutdown(): Promise<ReadinessCheckLevel> {
|
|
125
|
+
return Promise.resolve(runtimeShuttingDown ? "unavailable" : "ok");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readinessLevel(checks: SafeReadiness["checks"]): ReadinessLevel {
|
|
129
|
+
const levels = Object.values(checks);
|
|
130
|
+
if (levels.some((level) => level === "unavailable")) return "unavailable";
|
|
131
|
+
return levels.some((level) => level === "degraded") ? "degraded" : "ready";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createReadinessService(
|
|
135
|
+
dependencies: ReadinessDependencies,
|
|
136
|
+
cacheMs = DEFAULT_CACHE_MS,
|
|
137
|
+
): ReadinessService {
|
|
138
|
+
let cached: { expiresAt: number; value: SafeReadiness } | null = null;
|
|
139
|
+
let inFlight: Promise<SafeReadiness> | null = null;
|
|
140
|
+
|
|
141
|
+
async function evaluate(): Promise<SafeReadiness> {
|
|
142
|
+
// Establish the directory before checking its filesystem. On a clean install
|
|
143
|
+
// the data directory may not exist until the first write probe creates it.
|
|
144
|
+
const storage = await dependencies.checkStorage();
|
|
145
|
+
const [disk, workers, index, queue, shutdown] = await Promise.all([
|
|
146
|
+
dependencies.checkDisk(),
|
|
147
|
+
dependencies.checkWorkers(),
|
|
148
|
+
dependencies.checkIndex(),
|
|
149
|
+
dependencies.checkQueue(),
|
|
150
|
+
dependencies.checkShutdown(),
|
|
151
|
+
]);
|
|
152
|
+
const checks = { storage, disk, workers, index, queue, shutdown };
|
|
153
|
+
return {
|
|
154
|
+
status: readinessLevel(checks),
|
|
155
|
+
checks,
|
|
156
|
+
checkedAt: new Date(dependencies.now()).toISOString(),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function get(): Promise<SafeReadiness> {
|
|
161
|
+
const now = dependencies.now();
|
|
162
|
+
if (cached !== null && cached.expiresAt > now) return cached.value;
|
|
163
|
+
if (inFlight !== null) return await inFlight;
|
|
164
|
+
|
|
165
|
+
const evaluation = evaluate();
|
|
166
|
+
inFlight = evaluation;
|
|
167
|
+
try {
|
|
168
|
+
const value = await evaluation;
|
|
169
|
+
cached = { expiresAt: dependencies.now() + Math.max(0, cacheMs), value };
|
|
170
|
+
return value;
|
|
171
|
+
} finally {
|
|
172
|
+
if (inFlight === evaluation) inFlight = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function invalidate(): void {
|
|
177
|
+
cached = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { get, invalidate };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const readinessService = createReadinessService({
|
|
184
|
+
checkStorage: checkStorageWritable,
|
|
185
|
+
checkDisk: checkDiskHeadroom,
|
|
186
|
+
checkWorkers,
|
|
187
|
+
checkIndex,
|
|
188
|
+
checkQueue,
|
|
189
|
+
checkShutdown,
|
|
190
|
+
now: Date.now,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
export async function getRuntimeReadiness(): Promise<SafeReadiness> {
|
|
194
|
+
return await readinessService.get();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Shutdown coordination will call this before stopping admission. */
|
|
198
|
+
export function setRuntimeShuttingDown(shuttingDown: boolean): void {
|
|
199
|
+
runtimeShuttingDown = shuttingDown;
|
|
200
|
+
readinessService.invalidate();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function _resetRuntimeHealthForTests(): void {
|
|
204
|
+
runtimeShuttingDown = false;
|
|
205
|
+
readinessService.invalidate();
|
|
206
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { closeLogFinalizerWorkers, forceCloseLogFinalizerWorkers } from "./logFinalizer";
|
|
2
|
+
import { flushIndex } from "./logIndex";
|
|
3
|
+
import { flushApplicationLog, flushLogBuffer } from "./logger";
|
|
4
|
+
import { flushAllRawStreamCaptures } from "./rawStreamCapture";
|
|
5
|
+
import { setRuntimeShuttingDown } from "./runtimeHealth";
|
|
6
|
+
import { stopRuntimeAdmission, waitForRuntimeTrafficDrain } from "./runtimeAdmission";
|
|
7
|
+
import { createShutdownCoordinator, createShutdownSignalHandler } from "./shutdownCoordinator";
|
|
8
|
+
import { destroyAllSessionProcesses } from "./sessionProcess";
|
|
9
|
+
import { flushSessionTasks } from "./sessionRuntime";
|
|
10
|
+
import { closeSqliteLogIndex } from "./sqliteLogIndex";
|
|
11
|
+
import { flushSessionArchiveWrites } from "./store";
|
|
12
|
+
import { flushToolSchemaWarningAnalysis } from "./logFinalizer";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_SHUTDOWN_DEADLINE_MS = 15_000;
|
|
15
|
+
|
|
16
|
+
function shutdownDeadlineMs(): number {
|
|
17
|
+
const raw = process.env["AGENT_INSPECTOR_SHUTDOWN_TIMEOUT_MS"];
|
|
18
|
+
if (raw === undefined || raw === "") return DEFAULT_SHUTDOWN_DEADLINE_MS;
|
|
19
|
+
const parsed = Number(raw);
|
|
20
|
+
return Number.isSafeInteger(parsed) && parsed > 0 && parsed <= 300_000
|
|
21
|
+
? parsed
|
|
22
|
+
: DEFAULT_SHUTDOWN_DEADLINE_MS;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function drainFinalizers(): Promise<void> {
|
|
26
|
+
await flushAllRawStreamCaptures();
|
|
27
|
+
await flushSessionTasks();
|
|
28
|
+
await flushToolSchemaWarningAnalysis();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function flushPersistence(): Promise<void> {
|
|
32
|
+
await flushLogBuffer();
|
|
33
|
+
await flushSessionArchiveWrites();
|
|
34
|
+
await flushIndex();
|
|
35
|
+
await flushApplicationLog();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function closeResources(): Promise<void> {
|
|
39
|
+
await closeLogFinalizerWorkers();
|
|
40
|
+
destroyAllSessionProcesses();
|
|
41
|
+
closeSqliteLogIndex();
|
|
42
|
+
await flushApplicationLog();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function forceCloseResources(): void {
|
|
46
|
+
forceCloseLogFinalizerWorkers();
|
|
47
|
+
destroyAllSessionProcesses();
|
|
48
|
+
closeSqliteLogIndex();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const runtimeShutdownCoordinator = createShutdownCoordinator(
|
|
52
|
+
{
|
|
53
|
+
markShuttingDown: () => setRuntimeShuttingDown(true),
|
|
54
|
+
stopAdmission: stopRuntimeAdmission,
|
|
55
|
+
drainTraffic: waitForRuntimeTrafficDrain,
|
|
56
|
+
drainFinalizers,
|
|
57
|
+
flushPersistence,
|
|
58
|
+
closeResources,
|
|
59
|
+
forceClose: forceCloseResources,
|
|
60
|
+
},
|
|
61
|
+
shutdownDeadlineMs(),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
let signalHandlersInstalled = false;
|
|
65
|
+
|
|
66
|
+
/** Install once in the server runtime; the CLI supervisor owns its own public-ingress signals. */
|
|
67
|
+
export function installRuntimeShutdownSignalHandlers(): void {
|
|
68
|
+
if (signalHandlersInstalled) return;
|
|
69
|
+
signalHandlersInstalled = true;
|
|
70
|
+
const handleSignal = createShutdownSignalHandler(runtimeShutdownCoordinator, (result) => {
|
|
71
|
+
process.exit(result.forced ? 1 : 0);
|
|
72
|
+
});
|
|
73
|
+
process.on("SIGINT", () => handleSignal("SIGINT"));
|
|
74
|
+
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
|
75
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { readdir, rm } from "node:fs/promises";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { CapturedLogSchema, type CapturedLog } from "./schemas";
|
|
6
6
|
import { logger, resolveLogDir } from "./logger";
|
|
7
|
+
import { ensurePrivateDirectorySync, securePrivateFileSync } from "./privateDataPath";
|
|
7
8
|
|
|
8
9
|
type SqliteConstructorResult = {
|
|
9
10
|
constructor: ((path: string) => unknown) | null;
|
|
@@ -207,10 +208,22 @@ async function openSessionArchiveDb(filePath: string, create: boolean): Promise<
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
try {
|
|
210
|
-
if (create
|
|
211
|
+
if (!create && !existsSync(filePath)) return null;
|
|
212
|
+
if (!ensurePrivateDirectorySync(dirname(filePath))) {
|
|
213
|
+
logger.warn("[sessionArchive] Failed to secure archive directory:", dirname(filePath));
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
211
216
|
const db = sqliteConstructor.constructor(filePath);
|
|
212
217
|
execSql(db, "PRAGMA journal_mode = WAL");
|
|
213
218
|
execSql(db, "PRAGMA synchronous = NORMAL");
|
|
219
|
+
if (!securePrivateFileSync(filePath, true)) {
|
|
220
|
+
closeDatabase(db);
|
|
221
|
+
logger.warn("[sessionArchive] Failed to secure archive database:", filePath);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
for (const sidecarPath of [`${filePath}-wal`, `${filePath}-shm`]) {
|
|
225
|
+
if (existsSync(sidecarPath)) void securePrivateFileSync(sidecarPath);
|
|
226
|
+
}
|
|
214
227
|
if (!execSql(db, CREATE_SCHEMA_SQL)) {
|
|
215
228
|
closeDatabase(db);
|
|
216
229
|
logger.warn("[sessionArchive] Failed to initialize schema:", filePath);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
import { logger } from "./logger";
|
|
5
6
|
import type { FinalizeLogJob, FinalizeLogResult } from "./logFinalizer";
|
|
6
7
|
import { isPlainRecord, safeGetOwnProperty } from "../lib/objectUtils";
|
|
@@ -20,6 +21,11 @@ function errorMessage(err: unknown): string {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function resolveSessionWorkerPath(): string {
|
|
24
|
+
const workerDir = process.env["AGENT_INSPECTOR_WORKER_DIR"];
|
|
25
|
+
if (workerDir !== undefined && workerDir.trim().length > 0) {
|
|
26
|
+
const builtWorkerPath = join(workerDir, "sessionWorkerEntry.js");
|
|
27
|
+
if (existsSync(builtWorkerPath)) return builtWorkerPath;
|
|
28
|
+
}
|
|
23
29
|
return fileURLToPath(new URL("./sessionWorkerEntry.ts", import.meta.url));
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -153,6 +159,10 @@ export class SessionProcess {
|
|
|
153
159
|
this.child.kill();
|
|
154
160
|
this.child = null;
|
|
155
161
|
}
|
|
162
|
+
for (const [, pending] of this.pending) {
|
|
163
|
+
pending.reject(new Error(`Session process shut down, session=${this.sessionId}`));
|
|
164
|
+
}
|
|
165
|
+
this.pending.clear();
|
|
156
166
|
_processes.delete(this.sessionId);
|
|
157
167
|
}
|
|
158
168
|
}
|
|
@@ -167,6 +177,15 @@ export function getSessionProcess(sessionId: string): SessionProcess {
|
|
|
167
177
|
return sp;
|
|
168
178
|
}
|
|
169
179
|
|
|
180
|
+
/** Aggregate only queue depth for readiness; session identifiers remain private. */
|
|
181
|
+
export function getSessionProcessPendingCount(): number {
|
|
182
|
+
let pending = 0;
|
|
183
|
+
for (const processWorker of _processes.values()) {
|
|
184
|
+
pending += processWorker.pendingCount;
|
|
185
|
+
}
|
|
186
|
+
return pending;
|
|
187
|
+
}
|
|
188
|
+
|
|
170
189
|
/** Forcefully tear down all session processes (e.g. on server shutdown). */
|
|
171
190
|
export function destroyAllSessionProcesses(): void {
|
|
172
191
|
for (const [, sp] of _processes) {
|
|
@@ -74,3 +74,10 @@ export function enqueueFinalizeLogJob(job: FinalizeLogJob): Promise<void> {
|
|
|
74
74
|
executeFinalizeLogJob(job),
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
/** Wait until every per-session finalizer queue has settled. */
|
|
79
|
+
export async function flushSessionTasks(): Promise<void> {
|
|
80
|
+
while (queues.size > 0) {
|
|
81
|
+
await Promise.all([...queues.values()]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export type ShutdownReason = "SIGINT" | "SIGTERM" | "manual";
|
|
2
|
+
|
|
3
|
+
export type ShutdownResult = {
|
|
4
|
+
forced: boolean;
|
|
5
|
+
reason: "completed" | "deadline" | "error";
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ShutdownDependencies = {
|
|
9
|
+
markShuttingDown: () => void;
|
|
10
|
+
stopAdmission: () => void;
|
|
11
|
+
drainTraffic: () => Promise<void>;
|
|
12
|
+
drainFinalizers: () => Promise<void>;
|
|
13
|
+
flushPersistence: () => Promise<void>;
|
|
14
|
+
closeResources: () => Promise<void>;
|
|
15
|
+
forceClose: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ShutdownCoordinator = {
|
|
19
|
+
shutdown: (reason?: ShutdownReason) => Promise<ShutdownResult>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function createShutdownCoordinator(
|
|
23
|
+
dependencies: ShutdownDependencies,
|
|
24
|
+
deadlineMs: number,
|
|
25
|
+
): ShutdownCoordinator {
|
|
26
|
+
let shutdownPromise: Promise<ShutdownResult> | null = null;
|
|
27
|
+
|
|
28
|
+
async function shutdown(_reason: ShutdownReason = "manual"): Promise<ShutdownResult> {
|
|
29
|
+
if (shutdownPromise !== null) return await shutdownPromise;
|
|
30
|
+
|
|
31
|
+
dependencies.markShuttingDown();
|
|
32
|
+
dependencies.stopAdmission();
|
|
33
|
+
|
|
34
|
+
let forced = false;
|
|
35
|
+
let forceClosed = false;
|
|
36
|
+
let deadline: ReturnType<typeof setTimeout> | null = null;
|
|
37
|
+
|
|
38
|
+
function forceCloseOnce(): void {
|
|
39
|
+
if (forceClosed) return;
|
|
40
|
+
forceClosed = true;
|
|
41
|
+
dependencies.forceClose();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function gracefulShutdown(): Promise<ShutdownResult> {
|
|
45
|
+
try {
|
|
46
|
+
await dependencies.drainTraffic();
|
|
47
|
+
if (forced) return { forced: true, reason: "deadline" };
|
|
48
|
+
await dependencies.drainFinalizers();
|
|
49
|
+
if (forced) return { forced: true, reason: "deadline" };
|
|
50
|
+
await dependencies.flushPersistence();
|
|
51
|
+
if (forced) return { forced: true, reason: "deadline" };
|
|
52
|
+
await dependencies.closeResources();
|
|
53
|
+
return { forced: false, reason: "completed" };
|
|
54
|
+
} catch {
|
|
55
|
+
forceCloseOnce();
|
|
56
|
+
return { forced: true, reason: "error" };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const timedOut = new Promise<ShutdownResult>((resolve) => {
|
|
61
|
+
deadline = setTimeout(
|
|
62
|
+
() => {
|
|
63
|
+
forced = true;
|
|
64
|
+
forceCloseOnce();
|
|
65
|
+
resolve({ forced: true, reason: "deadline" });
|
|
66
|
+
},
|
|
67
|
+
Math.max(1, deadlineMs),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
shutdownPromise = Promise.race([gracefulShutdown(), timedOut]).finally(() => {
|
|
72
|
+
if (deadline !== null) clearTimeout(deadline);
|
|
73
|
+
});
|
|
74
|
+
return await shutdownPromise;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { shutdown };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createShutdownSignalHandler(
|
|
81
|
+
coordinator: ShutdownCoordinator,
|
|
82
|
+
onComplete: (result: ShutdownResult) => void,
|
|
83
|
+
): (signal: "SIGINT" | "SIGTERM") => void {
|
|
84
|
+
let handled = false;
|
|
85
|
+
return (signal): void => {
|
|
86
|
+
if (handled) return;
|
|
87
|
+
handled = true;
|
|
88
|
+
void coordinator.shutdown(signal).then(onComplete);
|
|
89
|
+
};
|
|
90
|
+
}
|