@tonyclaw/agent-inspector 3.0.29 → 3.0.31

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.
Files changed (156) hide show
  1. package/.output/cli.js +791 -191
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/CompareDrawer-DB2KggLE.js +1 -0
  4. package/.output/public/assets/InspectorPet-CfwDw6kD.js +4108 -0
  5. package/.output/public/assets/ProxyViewerContainer-PcrO96_F.js +126 -0
  6. package/.output/public/assets/{ReplayDialog-CctnITbt.js → ReplayDialog-3Pl7EKjp.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-BrN0RBbL.js → RequestAnatomy-BcPjcHLX.js} +1 -1
  8. package/.output/public/assets/ResponseView-DOx51wV3.js +2 -0
  9. package/.output/public/assets/StreamingChunkSequence-LkYJWM1A.js +1 -0
  10. package/.output/public/assets/_sessionId-BSg32vC6.js +1 -0
  11. package/.output/public/assets/{_sessionId-DKM91udC.js → _sessionId-C0CA68QV.js} +1 -1
  12. package/.output/public/assets/{index-Bq-nqolG.js → index-BpBUqdAB.js} +1 -1
  13. package/.output/public/assets/index-D93dCgAQ.js +1 -0
  14. package/.output/public/assets/{index-DPiESBo2.js → index-FV5sxqx3.js} +6 -6
  15. package/.output/public/assets/index-VUsF4nhf.js +76 -0
  16. package/.output/public/assets/index-hyQHZeQI.css +1 -0
  17. package/.output/public/assets/{json-viewer-CfaDJwx8.js → json-viewer-WctIbAh_.js} +1 -1
  18. package/.output/public/assets/jszip.min-DnSIoTix.js +2 -0
  19. package/.output/public/assets/qwen-mMn3f5ul.webp +0 -0
  20. package/.output/server/_libs/jszip.mjs +35 -9
  21. package/.output/server/_libs/lucide-react.mjs +80 -80
  22. package/.output/server/_libs/radix-ui__react-dialog.mjs +3 -3
  23. package/.output/server/_libs/tanstack__react-router.mjs +1 -1
  24. package/.output/server/{_sessionId-CwGXPD4C.mjs → _sessionId-BHBK3VbF.mjs} +15 -15
  25. package/.output/server/_sessionId-CyuJ6Kfl.mjs +81 -0
  26. package/.output/server/_ssr/{CompareDrawer-R_P1-VNr.mjs → CompareDrawer-27amlv_a.mjs} +138 -103
  27. package/.output/server/_ssr/InspectorPet-CQnGXfLs.mjs +925 -0
  28. package/.output/server/_ssr/{ProxyViewerContainer-DUgxRNbj.mjs → ProxyViewerContainer-DlAl0uF-.mjs} +1569 -1646
  29. package/.output/server/_ssr/{ReplayDialog-De9wKKzS.mjs → ReplayDialog-5m2jtlLa.mjs} +18 -18
  30. package/.output/server/_ssr/{RequestAnatomy-Db4MxUYo.mjs → RequestAnatomy--1WtQNY9.mjs} +58 -58
  31. package/.output/server/_ssr/{ResponseView-8rDXOiNB.mjs → ResponseView-CgbqS8sr.mjs} +60 -71
  32. package/.output/server/_ssr/{StreamingChunkSequence-N9teZHkA.mjs → StreamingChunkSequence-BwpnopWx.mjs} +16 -16
  33. package/.output/server/_ssr/{index-D4DvByuW.mjs → index-CNVi1TzT.mjs} +15 -15
  34. package/.output/server/_ssr/index-DMCjH_4V.mjs +81 -0
  35. package/.output/server/_ssr/index.mjs +2 -2
  36. package/.output/server/_ssr/{json-viewer-BjoHRbjS.mjs → json-viewer-DNGN2uoe.mjs} +58 -58
  37. package/.output/server/_ssr/{router-BYlePDj8.mjs → router-CgsqLnk5.mjs} +16007 -12632
  38. package/.output/server/_tanstack-start-manifest_v-BH5f_kWV.mjs +4 -0
  39. package/.output/server/index.mjs +99 -71
  40. package/.output/workers/logFinalizer.worker.js +16908 -0
  41. package/.output/workers/sessionWorkerEntry.js +16904 -0
  42. package/README.md +21 -5
  43. package/package.json +6 -2
  44. package/src/assets/logos/qwen.webp +0 -0
  45. package/src/cli.ts +85 -10
  46. package/src/components/OnboardingBanner.tsx +41 -21
  47. package/src/components/ProxyViewer.tsx +198 -269
  48. package/src/components/ProxyViewerContainer.tsx +128 -17
  49. package/src/components/ecosystem/AgentLabDialog.tsx +166 -19
  50. package/src/components/errors/SafeErrorBoundary.tsx +201 -0
  51. package/src/components/pi-agent/PiAgentPanel.tsx +209 -35
  52. package/src/components/pi-agent/piAgentChatLogic.ts +263 -0
  53. package/src/components/providers/ImportWizardDialog.tsx +18 -42
  54. package/src/components/providers/ProviderLogo.tsx +1 -1
  55. package/src/components/providers/ProvidersPanel.tsx +8 -5
  56. package/src/components/providers/SettingsDialog.tsx +34 -11
  57. package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
  58. package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
  59. package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
  60. package/src/components/proxy-viewer/LogEntry.tsx +283 -275
  61. package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
  62. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
  63. package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
  64. package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
  65. package/src/components/proxy-viewer/accessibility.ts +8 -0
  66. package/src/components/proxy-viewer/lazy.ts +4 -0
  67. package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
  68. package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
  69. package/src/components/proxy-viewer/viewerState.ts +2 -6
  70. package/src/components/ui/select.tsx +1 -1
  71. package/src/components/ui/tabs.tsx +3 -3
  72. package/src/components/ui/transient-toast.tsx +7 -6
  73. package/src/lib/apiClient.ts +17 -2
  74. package/src/lib/ecosystemContract.ts +34 -0
  75. package/src/lib/export-logs.ts +1 -1
  76. package/src/lib/piAgentContract.ts +80 -0
  77. package/src/lib/providerContract.ts +70 -4
  78. package/src/lib/providerImportContract.ts +27 -0
  79. package/src/lib/providerModelMetadata.ts +16 -7
  80. package/src/lib/resourceLimits.ts +152 -0
  81. package/src/lib/safeDiagnostic.ts +38 -0
  82. package/src/lib/useProviders.ts +4 -4
  83. package/src/mcp/server.ts +39 -6
  84. package/src/mcp/toolHandlers.ts +131 -4
  85. package/src/proxy/chunkStorage.ts +20 -6
  86. package/src/proxy/config.ts +20 -6
  87. package/src/proxy/dataDir.ts +3 -0
  88. package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
  89. package/src/proxy/ecosystemExecutionRoute.ts +116 -0
  90. package/src/proxy/ecosystemTasks.ts +48 -0
  91. package/src/proxy/evidenceExporter.ts +23 -9
  92. package/src/proxy/groupEvidenceExporter.ts +26 -5
  93. package/src/proxy/groupStore.ts +13 -3
  94. package/src/proxy/handler.ts +103 -20
  95. package/src/proxy/identityProxy.ts +333 -14
  96. package/src/proxy/logFinalizer.ts +73 -6
  97. package/src/proxy/logImportUpload.ts +128 -0
  98. package/src/proxy/logImporter.ts +321 -70
  99. package/src/proxy/logIndex.ts +16 -6
  100. package/src/proxy/logger.ts +294 -41
  101. package/src/proxy/privateDataPath.ts +183 -0
  102. package/src/proxy/providerScanStore.ts +87 -0
  103. package/src/proxy/providerSecretStore.ts +58 -19
  104. package/src/proxy/providers.ts +175 -58
  105. package/src/proxy/rawStreamCapture.ts +66 -5
  106. package/src/proxy/runStore.ts +13 -3
  107. package/src/proxy/runtimeAdmission.ts +52 -0
  108. package/src/proxy/runtimeHealth.ts +206 -0
  109. package/src/proxy/runtimeShutdown.ts +75 -0
  110. package/src/proxy/sessionArchive.ts +15 -2
  111. package/src/proxy/sessionProcess.ts +19 -0
  112. package/src/proxy/sessionRuntime.ts +7 -0
  113. package/src/proxy/shutdownCoordinator.ts +90 -0
  114. package/src/proxy/sqliteLogIndex.ts +18 -2
  115. package/src/proxy/store.ts +18 -2
  116. package/src/routes/__root.tsx +18 -1
  117. package/src/routes/api/alerts.summary.ts +12 -8
  118. package/src/routes/api/alerts.ts +27 -9
  119. package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
  120. package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
  121. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
  122. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
  123. package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
  124. package/src/routes/api/health.ts +9 -2
  125. package/src/routes/api/knowledge.project-context.ts +11 -0
  126. package/src/routes/api/knowledge.search.ts +33 -2
  127. package/src/routes/api/logs.$id.body.ts +16 -2
  128. package/src/routes/api/logs.import.ts +7 -18
  129. package/src/routes/api/logs.stream.ts +147 -69
  130. package/src/routes/api/logs.ts +55 -14
  131. package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
  132. package/src/routes/api/providers.$providerId.ts +37 -6
  133. package/src/routes/api/providers.export.ts +34 -16
  134. package/src/routes/api/providers.import.ts +42 -8
  135. package/src/routes/api/providers.scan.ts +13 -8
  136. package/src/routes/api/providers.ts +36 -5
  137. package/src/routes/api/runs.ts +12 -12
  138. package/src/routes/api/sessions.ts +15 -8
  139. package/src/routes/index.tsx +6 -0
  140. package/src/routes/livez.ts +13 -0
  141. package/src/routes/readyz.ts +18 -0
  142. package/src/routes/session/$sessionId.tsx +6 -0
  143. package/src/services/piAgent.ts +131 -102
  144. package/src/services/piAgentRetrieval.ts +1180 -0
  145. package/styles/globals.css +19 -3
  146. package/.output/public/assets/CompareDrawer-Cb5ccPeX.js +0 -1
  147. package/.output/public/assets/ProxyViewerContainer-COBe5GTT.js +0 -4234
  148. package/.output/public/assets/ResponseView-Cqq87lX_.js +0 -3
  149. package/.output/public/assets/StreamingChunkSequence-CnTa9mCr.js +0 -1
  150. package/.output/public/assets/index-DTLNVcgc.js +0 -76
  151. package/.output/public/assets/index-DliqmwUw.css +0 -1
  152. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  153. package/.output/server/_tanstack-start-manifest_v-DMCA8KK3.mjs +0 -4
  154. package/src/assets/logos/mcp.png +0 -0
  155. package/src/assets/logos/qwen.png +0 -0
  156. 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 { mkdir, appendFile, writeFile } from "node:fs/promises";
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
- await appendFile(this.filePath, text, "utf-8");
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
- snapshot(): RawStreamSource {
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
- await mkdir(getRawStreamDir(), { recursive: true });
81
- await writeFile(path, this.chunks.join(""), "utf-8");
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
  }
@@ -1,4 +1,5 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
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
- mkdirSync(dirname(filePath), { recursive: true });
106
- writeFileSync(filePath, `${JSON.stringify({ runs }, null, 2)}\n`, "utf8");
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 { mkdir, readdir, rm } from "node:fs/promises";
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) await mkdir(dirname(filePath), { recursive: true });
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
+ }