@tonyclaw/agent-inspector 2.1.17 → 2.1.19

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 (44) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-B9sLBHw5.js → CompareDrawer-CHXN5HYU.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-TZJV7HhE.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-D9I9W9n3.js → ReplayDialog-DQ7nS5Rr.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-DGXK_Rii.js → RequestAnatomy-DXWweH_2.js} +1 -1
  6. package/.output/public/assets/{ResponseView-df_JzwsS.js → ResponseView-3PimPfiD.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-i3DM5IKL.js → StreamingChunkSequence-4v52v-7a.js} +1 -1
  8. package/.output/public/assets/_sessionId-DfkBPvCC.js +1 -0
  9. package/.output/public/assets/{index-Dqc2r1ea.js → index-BuhFRImh.js} +1 -1
  10. package/.output/public/assets/index-ChgjUix0.css +1 -0
  11. package/.output/public/assets/index-Uf87L52J.js +1 -0
  12. package/.output/public/assets/{json-viewer-D3QWQlSB.js → json-viewer-d2gWBDLO.js} +1 -1
  13. package/.output/public/assets/{main-ZhxhOCmF.js → main-Do-E-46G.js} +2 -2
  14. package/.output/server/{_sessionId-YFrP0lZP.mjs → _sessionId-B82aMW2A.mjs} +2 -2
  15. package/.output/server/_ssr/{CompareDrawer-qC3adEeq.mjs → CompareDrawer-Bud_yIrM.mjs} +2 -2
  16. package/.output/server/_ssr/{ProxyViewerContainer-DCZIJC2b.mjs → ProxyViewerContainer-DqgVb0So.mjs} +1976 -1988
  17. package/.output/server/_ssr/{ReplayDialog-CvX4XYCi.mjs → ReplayDialog-DKbV5fnW.mjs} +3 -3
  18. package/.output/server/_ssr/{RequestAnatomy-HQfrxjO1.mjs → RequestAnatomy-MStxp1Wp.mjs} +2 -2
  19. package/.output/server/_ssr/{ResponseView-QSlSD11e.mjs → ResponseView-HLTCpJcF.mjs} +2 -2
  20. package/.output/server/_ssr/{StreamingChunkSequence-DTAiwlnI.mjs → StreamingChunkSequence-DEiyiUs_.mjs} +2 -2
  21. package/.output/server/_ssr/{index-D_EzfXaN.mjs → index-DamOPFDI.mjs} +2 -2
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{json-viewer-CrIX0e0Q.mjs → json-viewer-MmTWKxqh.mjs} +2 -2
  24. package/.output/server/_ssr/{router-__o2wrfO.mjs → router-50BIB6ED.mjs} +545 -22
  25. package/.output/server/{_tanstack-start-manifest_v-DbqyLpsI.mjs → _tanstack-start-manifest_v-TSwb1vFc.mjs} +1 -1
  26. package/.output/server/index.mjs +66 -66
  27. package/package.json +1 -1
  28. package/src/components/proxy-viewer/ConversationGroupList.tsx +11 -11
  29. package/src/components/proxy-viewer/ConversationHeader.tsx +7 -7
  30. package/src/components/proxy-viewer/LogEntry.tsx +15 -26
  31. package/src/components/proxy-viewer/LogEntryHeader.tsx +11 -14
  32. package/src/components/proxy-viewer/RequestToolsPanel.tsx +15 -15
  33. package/src/components/proxy-viewer/ThreadConnector.tsx +75 -91
  34. package/src/components/proxy-viewer/ToolTraceEvents.tsx +1 -1
  35. package/src/components/proxy-viewer/TurnGroup.tsx +11 -14
  36. package/src/mcp/server.ts +275 -0
  37. package/src/mcp/toolHandlers.ts +380 -3
  38. package/src/proxy/logSearch.ts +3 -0
  39. package/src/proxy/store.ts +40 -15
  40. package/src/routes/api/logs.ts +8 -2
  41. package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +0 -106
  42. package/.output/public/assets/_sessionId-CTpCHdFh.js +0 -1
  43. package/.output/public/assets/index-Blqvndcn.js +0 -1
  44. package/.output/public/assets/index-Cs79WRQj.css +0 -1
@@ -85,6 +85,8 @@ const SearchLogsResponseSchema = z.object({
85
85
  const PAGINATION_DEFAULTS = { offset: 0, limit: 3 };
86
86
  export const LIMIT_HARD_CAP = 5;
87
87
  const LOG_DETAIL_TEXT_LIMIT = 200_000;
88
+ const PROCESS_LOG_LIMIT_DEFAULT = 50;
89
+ const PROCESS_LOG_LIMIT_MAX = 200;
88
90
 
89
91
  export function clampLimit(input: number | undefined): number {
90
92
  if (input === undefined) return PAGINATION_DEFAULTS.limit;
@@ -92,6 +94,12 @@ export function clampLimit(input: number | undefined): number {
92
94
  return Math.min(Math.floor(input), LIMIT_HARD_CAP);
93
95
  }
94
96
 
97
+ function clampProcessLogLimit(input: number | undefined): number {
98
+ if (input === undefined) return PROCESS_LOG_LIMIT_DEFAULT;
99
+ if (!Number.isFinite(input) || input < 1) return 1;
100
+ return Math.min(Math.floor(input), PROCESS_LOG_LIMIT_MAX);
101
+ }
102
+
95
103
  function textJson(data: unknown): ToolResult {
96
104
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
97
105
  }
@@ -147,10 +155,15 @@ function buildBoundedLogDetail(log: CapturedLog): Record<string, unknown> {
147
155
  };
148
156
  }
149
157
 
150
- export function buildLogSummary(log: CapturedLog) {
151
- const hasError =
158
+ function logHasError(log: CapturedLog): boolean {
159
+ return (
152
160
  (log.error !== null && log.error !== undefined && log.error.length > 0) ||
153
- (log.responseStatus !== null && log.responseStatus !== undefined && log.responseStatus >= 400);
161
+ (log.responseStatus !== null && log.responseStatus !== undefined && log.responseStatus >= 400)
162
+ );
163
+ }
164
+
165
+ export function buildLogSummary(log: CapturedLog) {
166
+ const hasError = logHasError(log);
154
167
  const rawRequestBodyBytes = log.rawRequestBodyBytes ?? textByteLength(log.rawRequestBody);
155
168
  const responseTextBytes = log.responseTextBytes ?? textByteLength(log.responseText);
156
169
  return {
@@ -190,6 +203,92 @@ export function buildLogSummary(log: CapturedLog) {
190
203
  };
191
204
  }
192
205
 
206
+ function sortedUniqueStrings(values: Array<string | null | undefined>): string[] {
207
+ return [
208
+ ...new Set(
209
+ values.filter(
210
+ (value): value is string => value !== null && value !== undefined && value !== "",
211
+ ),
212
+ ),
213
+ ].sort((left, right) => left.localeCompare(right));
214
+ }
215
+
216
+ function sortedUniqueNumbers(values: Array<number | null | undefined>): number[] {
217
+ return [
218
+ ...new Set(
219
+ values.filter(
220
+ (value): value is number => value !== null && value !== undefined && Number.isFinite(value),
221
+ ),
222
+ ),
223
+ ].sort((left, right) => left - right);
224
+ }
225
+
226
+ function inferAgentFromUserAgent(userAgent: string | null | undefined): string | null {
227
+ if (userAgent === null || userAgent === undefined) return null;
228
+ const lower = userAgent.toLowerCase();
229
+ if (lower.includes("codex")) return "Codex";
230
+ if (lower.includes("opencode")) return "OpenCode";
231
+ if (lower.includes("claude")) return "Claude Code";
232
+ if (lower.includes("mimocode")) return "MiMoCode";
233
+ if (lower.includes("ai-sdk")) return "AI SDK";
234
+ return null;
235
+ }
236
+
237
+ function processSummary(
238
+ clientPid: number,
239
+ logs: readonly CapturedLog[],
240
+ total: number,
241
+ limit: number,
242
+ ): Record<string, unknown> {
243
+ const sortedLogs = [...logs].sort((left, right) => left.id - right.id);
244
+ const first = sortedLogs[0];
245
+ const last = sortedLogs[sortedLogs.length - 1];
246
+ const userAgents = sortedUniqueStrings(sortedLogs.map((log) => log.userAgent));
247
+ const agents = sortedUniqueStrings(userAgents.map(inferAgentFromUserAgent));
248
+ const sessions = sortedUniqueStrings(sortedLogs.map((log) => log.sessionId));
249
+ return {
250
+ clientPid,
251
+ total,
252
+ limit,
253
+ logCount: sortedLogs.length,
254
+ sessionCount: sessions.length,
255
+ sessions,
256
+ agents,
257
+ userAgents,
258
+ clientPorts: sortedUniqueNumbers(sortedLogs.map((log) => log.clientPort)),
259
+ clientCwds: sortedUniqueStrings(sortedLogs.map((log) => log.clientCwd)),
260
+ clientProjectFolders: sortedUniqueStrings(sortedLogs.map((log) => log.clientProjectFolder)),
261
+ models: sortedUniqueStrings(sortedLogs.map((log) => log.model)),
262
+ providers: sortedUniqueStrings(sortedLogs.map((log) => log.providerName)),
263
+ errorCount: sortedLogs.filter(logHasError).length,
264
+ startedAt: first?.timestamp ?? null,
265
+ updatedAt: last?.timestamp ?? null,
266
+ latestLogs: sortedLogs.slice(-Math.min(10, sortedLogs.length)).reverse().map(buildLogSummary),
267
+ };
268
+ }
269
+
270
+ async function readCompactLogsPage(
271
+ callApi: CallApiFn,
272
+ params: URLSearchParams,
273
+ ): Promise<{ logs: CapturedLog[]; total: number } | ToolResult> {
274
+ const path = `/api/logs?${params.toString()}`;
275
+ const res = await callApi(path);
276
+ if (!res.ok) return toolError(`GET ${path} returned ${res.status}`);
277
+ const rawBody: unknown = await res.json();
278
+ const parsed = LogsListResponseSchema.safeParse(rawBody);
279
+ if (!parsed.success) return toolError("GET /api/logs returned an unparseable logs shape");
280
+ const logs = parsed.data.logs ?? [];
281
+ return { logs, total: parsed.data.total ?? logs.length };
282
+ }
283
+
284
+ function isToolResult(value: unknown): value is ToolResult {
285
+ return (
286
+ typeof value === "object" &&
287
+ value !== null &&
288
+ Object.getOwnPropertyDescriptor(value, "content") !== undefined
289
+ );
290
+ }
291
+
193
292
  // ===========================================================================
194
293
  // Per-tool impls
195
294
  // ===========================================================================
@@ -199,6 +298,7 @@ export type ListLogsArgs = {
199
298
  limit?: number;
200
299
  sessionId?: string;
201
300
  model?: string;
301
+ clientPid?: number;
202
302
  };
203
303
 
204
304
  export async function listLogsImpl(callApi: CallApiFn, args: ListLogsArgs): Promise<ToolResult> {
@@ -215,6 +315,9 @@ export async function listLogsImpl(callApi: CallApiFn, args: ListLogsArgs): Prom
215
315
  if (args.model !== undefined && args.model.length > 0) {
216
316
  params.set("model", args.model);
217
317
  }
318
+ if (args.clientPid !== undefined) {
319
+ params.set("clientPid", String(args.clientPid));
320
+ }
218
321
  const res = await callApi(`/api/logs?${params.toString()}`);
219
322
  if (!res.ok) return toolError(`GET /api/logs returned ${res.status}`);
220
323
  const rawBody: unknown = await res.json();
@@ -231,6 +334,7 @@ export type SearchLogsArgs = {
231
334
  scanLimit?: number;
232
335
  sessionId?: string;
233
336
  model?: string;
337
+ clientPid?: number;
234
338
  };
235
339
 
236
340
  export async function searchLogsImpl(
@@ -253,6 +357,9 @@ export async function searchLogsImpl(
253
357
  if (args.model !== undefined && args.model.length > 0) {
254
358
  params.set("model", args.model);
255
359
  }
360
+ if (args.clientPid !== undefined) {
361
+ params.set("clientPid", String(args.clientPid));
362
+ }
256
363
 
257
364
  const res = await callApi(`/api/logs?${params.toString()}`);
258
365
  if (!res.ok) return toolError(`GET /api/logs search returned ${res.status}`);
@@ -514,6 +621,276 @@ export async function getSessionTimelineImpl(
514
621
  );
515
622
  }
516
623
 
624
+ export type ListProcessesArgs = {
625
+ limit?: number;
626
+ scanLimit?: number;
627
+ };
628
+
629
+ export async function listProcessesImpl(
630
+ callApi: CallApiFn,
631
+ args: ListProcessesArgs = {},
632
+ ): Promise<ToolResult> {
633
+ const scanLimit = clampProcessLogLimit(args.scanLimit ?? args.limit);
634
+ const params = new URLSearchParams({
635
+ q: "",
636
+ offset: "0",
637
+ limit: String(Math.min(scanLimit, 50)),
638
+ scanLimit: String(scanLimit),
639
+ });
640
+ const res = await callApi(`/api/logs?${params.toString()}`);
641
+ if (!res.ok) return toolError(`GET /api/logs process scan returned ${res.status}`);
642
+ const rawBody: unknown = await res.json();
643
+ const parsed = SearchLogsResponseSchema.safeParse(rawBody);
644
+ if (!parsed.success) return toolError("GET /api/logs returned an unparseable process scan shape");
645
+
646
+ const grouped = new Map<number, CapturedLog[]>();
647
+ for (const log of parsed.data.logs) {
648
+ if (log.clientPid === null || log.clientPid === undefined) continue;
649
+ const existing = grouped.get(log.clientPid) ?? [];
650
+ existing.push(log);
651
+ grouped.set(log.clientPid, existing);
652
+ }
653
+
654
+ const processes = [...grouped.entries()]
655
+ .map(([clientPid, logs]) => processSummary(clientPid, logs, logs.length, logs.length))
656
+ .sort((left, right) => {
657
+ const leftUpdated = typeof left["updatedAt"] === "string" ? left["updatedAt"] : "";
658
+ const rightUpdated = typeof right["updatedAt"] === "string" ? right["updatedAt"] : "";
659
+ return rightUpdated.localeCompare(leftUpdated);
660
+ });
661
+
662
+ return textJson({
663
+ scanned: parsed.data.scanned ?? parsed.data.logs.length,
664
+ processCount: processes.length,
665
+ processes,
666
+ });
667
+ }
668
+
669
+ export type ProcessArgs = {
670
+ clientPid: number;
671
+ limit?: number;
672
+ offset?: number;
673
+ };
674
+
675
+ export async function getProcessImpl(callApi: CallApiFn, args: ProcessArgs): Promise<ToolResult> {
676
+ const limit = clampProcessLogLimit(args.limit);
677
+ const offset = args.offset ?? 0;
678
+ const params = new URLSearchParams({
679
+ clientPid: String(args.clientPid),
680
+ offset: String(offset),
681
+ limit: String(limit),
682
+ compact: "1",
683
+ });
684
+ const result = await readCompactLogsPage(callApi, params);
685
+ if (isToolResult(result)) return result;
686
+ return textJson(processSummary(args.clientPid, result.logs, result.total, limit));
687
+ }
688
+
689
+ export async function getProcessTimelineImpl(
690
+ callApi: CallApiFn,
691
+ args: ProcessArgs,
692
+ ): Promise<ToolResult> {
693
+ const limit = clampTimelineLimit(args.limit);
694
+ const offset = args.offset ?? 0;
695
+ const params = new URLSearchParams({
696
+ clientPid: String(args.clientPid),
697
+ offset: String(offset),
698
+ limit: String(limit),
699
+ compact: "1",
700
+ });
701
+ const result = await readCompactLogsPage(callApi, params);
702
+ if (isToolResult(result)) return result;
703
+ const sortedLogs = [...result.logs].sort((left, right) => left.id - right.id);
704
+ return textJson({
705
+ clientPid: args.clientPid,
706
+ logCoverage: "bounded",
707
+ total: result.total,
708
+ limit,
709
+ eventCount: sortedLogs.length,
710
+ requestCount: sortedLogs.length,
711
+ errorCount: sortedLogs.filter(logHasError).length,
712
+ startedAt: sortedLogs[0]?.timestamp ?? null,
713
+ updatedAt: sortedLogs.at(-1)?.timestamp ?? null,
714
+ sessions: sortedUniqueStrings(sortedLogs.map((log) => log.sessionId)),
715
+ models: sortedUniqueStrings(sortedLogs.map((log) => log.model)),
716
+ providers: sortedUniqueStrings(sortedLogs.map((log) => log.providerName)),
717
+ events: sortedLogs.map((log, index) => ({
718
+ sequence: index + 1,
719
+ kind: "request",
720
+ logId: log.id,
721
+ ...buildLogSummary(log),
722
+ })),
723
+ });
724
+ }
725
+
726
+ export async function getLogHeadersImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
727
+ const res = await callApi(`/api/logs/${id}`);
728
+ if (!res.ok) return toolError(`GET /api/logs/${id} returned ${res.status}`);
729
+ const rawBody: unknown = await res.json();
730
+ const parsed = CapturedLogSchema.safeParse(rawBody);
731
+ if (!parsed.success) {
732
+ return toolError(`GET /api/logs/${id} returned an unparseable CapturedLog`);
733
+ }
734
+ const log = parsed.data;
735
+ return textJson({
736
+ id: log.id,
737
+ sessionId: log.sessionId,
738
+ userAgent: log.userAgent,
739
+ origin: log.origin,
740
+ clientPid: log.clientPid ?? null,
741
+ clientPort: log.clientPort ?? null,
742
+ clientCwd: log.clientCwd ?? null,
743
+ clientProjectFolder: log.clientProjectFolder ?? null,
744
+ apiFormat: log.apiFormat,
745
+ method: log.method,
746
+ path: log.path,
747
+ model: log.model,
748
+ rawHeaders: log.rawHeaders ?? null,
749
+ headers: log.headers ?? null,
750
+ hasRawHeaders: log.rawHeaders !== undefined && Object.keys(log.rawHeaders).length > 0,
751
+ hasHeaders: log.headers !== undefined && Object.keys(log.headers).length > 0,
752
+ bodyContentMode: log.bodyContentMode ?? null,
753
+ });
754
+ }
755
+
756
+ export async function getRuntimeConfigImpl(callApi: CallApiFn): Promise<ToolResult> {
757
+ const configRes = await callApi("/api/config");
758
+ if (!configRes.ok) return toolError(`GET /api/config returned ${configRes.status}`);
759
+ const statsRes = await callApi("/api/logs?stats=1");
760
+ if (!statsRes.ok) return toolError(`GET /api/logs?stats=1 returned ${statsRes.status}`);
761
+ const config: unknown = await configRes.json();
762
+ const storage: unknown = await statsRes.json();
763
+ return textJson({
764
+ config,
765
+ storage,
766
+ });
767
+ }
768
+
769
+ export type DiagnoseCaptureArgs = {
770
+ id?: number;
771
+ };
772
+
773
+ export async function diagnoseCaptureImpl(
774
+ callApi: CallApiFn,
775
+ args: DiagnoseCaptureArgs = {},
776
+ ): Promise<ToolResult> {
777
+ const runtime = await getRuntimeConfigImpl(callApi);
778
+ if (runtime.isError === true) return runtime;
779
+ const runtimeText = runtime.content[0]?.text ?? "{}";
780
+ const runtimePayload: unknown = JSON.parse(runtimeText);
781
+ const runtimeRecord = isRecord(runtimePayload) ? runtimePayload : {};
782
+ const configRecord = isRecord(runtimeRecord["config"]) ? runtimeRecord["config"] : {};
783
+ const captureMode = stringField(configRecord, "captureMode");
784
+
785
+ let log: CapturedLog | null = null;
786
+ if (args.id !== undefined) {
787
+ const logRes = await callApi(`/api/logs/${args.id}`);
788
+ if (!logRes.ok) return toolError(`GET /api/logs/${args.id} returned ${logRes.status}`);
789
+ const rawLog: unknown = await logRes.json();
790
+ const parsed = CapturedLogSchema.safeParse(rawLog);
791
+ if (!parsed.success) return toolError(`GET /api/logs/${args.id} returned an unparseable log`);
792
+ log = parsed.data;
793
+ }
794
+
795
+ const findings: string[] = [];
796
+ const recommendations: string[] = [];
797
+ if (captureMode !== "full") {
798
+ findings.push(
799
+ "Runtime captureMode is not full, so request/response bodies and headers may be compacted.",
800
+ );
801
+ recommendations.push(
802
+ "Restart or launch Agent Inspector with full capture mode when header/body evidence is required.",
803
+ );
804
+ }
805
+ if (log !== null) {
806
+ if (log.rawHeaders === undefined || Object.keys(log.rawHeaders).length === 0) {
807
+ findings.push(`Log ${String(log.id)} has no rawHeaders stored.`);
808
+ }
809
+ if (log.headers === undefined || Object.keys(log.headers).length === 0) {
810
+ findings.push(`Log ${String(log.id)} has no upstream headers stored.`);
811
+ }
812
+ if (log.rawRequestBody === null) {
813
+ findings.push(`Log ${String(log.id)} does not include rawRequestBody in this response.`);
814
+ }
815
+ if (log.bodyContentMode === "compact") {
816
+ findings.push(`Log ${String(log.id)} is in compact body mode.`);
817
+ recommendations.push(
818
+ "Use inspector_get_log_headers for header-only checks and enable full capture for future body inspection.",
819
+ );
820
+ }
821
+ }
822
+
823
+ return textJson({
824
+ captureMode,
825
+ logId: log?.id ?? null,
826
+ hasLog: log !== null,
827
+ findings,
828
+ recommendations,
829
+ runtime: runtimeRecord,
830
+ });
831
+ }
832
+
833
+ export type AddGroupProcessArgs = {
834
+ groupId: string;
835
+ clientPid: number;
836
+ labelPrefix?: string;
837
+ };
838
+
839
+ export async function addGroupProcessImpl(
840
+ callApi: CallApiFn,
841
+ args: AddGroupProcessArgs,
842
+ ): Promise<ToolResult> {
843
+ const params = new URLSearchParams({
844
+ clientPid: String(args.clientPid),
845
+ offset: "0",
846
+ limit: String(PROCESS_LOG_LIMIT_MAX),
847
+ compact: "1",
848
+ });
849
+ const result = await readCompactLogsPage(callApi, params);
850
+ if (isToolResult(result)) return result;
851
+
852
+ const bySession = new Map<string, CapturedLog[]>();
853
+ for (const log of result.logs) {
854
+ if (log.sessionId === null || log.sessionId === "") continue;
855
+ const existing = bySession.get(log.sessionId) ?? [];
856
+ existing.push(log);
857
+ bySession.set(log.sessionId, existing);
858
+ }
859
+ if (bySession.size === 0) {
860
+ return toolError(`No sessions found for clientPid ${String(args.clientPid)}`);
861
+ }
862
+
863
+ let groupResult: ReturnType<typeof addInspectorGroupSession> | null = null;
864
+ const attached: Array<Record<string, unknown>> = [];
865
+ for (const [sessionId, logs] of bySession.entries()) {
866
+ const first = logs[0];
867
+ const last = logs.at(-1);
868
+ const userAgent = last?.userAgent ?? first?.userAgent ?? null;
869
+ const agent = inferAgentFromUserAgent(userAgent);
870
+ groupResult = addInspectorGroupSession(args.groupId, {
871
+ sessionId,
872
+ label: `${args.labelPrefix ?? `PID ${String(args.clientPid)}`} - ${sessionId}`,
873
+ agent,
874
+ provider: last?.providerName ?? first?.providerName ?? null,
875
+ model: last?.model ?? first?.model ?? null,
876
+ metadata: {
877
+ clientPid: args.clientPid,
878
+ processLogCount: logs.length,
879
+ userAgent: userAgent ?? "",
880
+ },
881
+ });
882
+ if (!groupResult.ok) return toolError(groupResult.message);
883
+ attached.push({ sessionId, logCount: logs.length, agent, userAgent });
884
+ }
885
+
886
+ if (groupResult === null || !groupResult.ok) return toolError("Group process attach failed");
887
+ return textJson({
888
+ group: groupResult.value,
889
+ clientPid: args.clientPid,
890
+ attached,
891
+ });
892
+ }
893
+
517
894
  type EvidenceFileKind = "json" | "markdown" | "html" | "zip";
518
895
 
519
896
  type EvidenceFileSummary = {
@@ -10,6 +10,7 @@ export type LogSearchOptions = {
10
10
  query: string;
11
11
  sessionId?: string;
12
12
  model?: string;
13
+ clientPid?: number;
13
14
  offset: number;
14
15
  limit: number;
15
16
  scanLimit: number;
@@ -93,6 +94,7 @@ export async function searchLogsPage(options: LogSearchOptions): Promise<LogSear
93
94
  const firstPage = await listLogsPage({
94
95
  sessionId: options.sessionId,
95
96
  model: options.model,
97
+ clientPid: options.clientPid,
96
98
  offset: 0,
97
99
  limit: 1,
98
100
  includeBodies: false,
@@ -101,6 +103,7 @@ export async function searchLogsPage(options: LogSearchOptions): Promise<LogSear
101
103
  const scanPage = await listLogsPage({
102
104
  sessionId: options.sessionId,
103
105
  model: options.model,
106
+ clientPid: options.clientPid,
104
107
  offset: scanOffset,
105
108
  limit: scanLimit,
106
109
  includeBodies: false,
@@ -99,6 +99,7 @@ export type ClearPersistedLogsByIdsResult = {
99
99
  export type ListLogsPageOptions = {
100
100
  sessionId?: string;
101
101
  model?: string;
102
+ clientPid?: number;
102
103
  offset: number;
103
104
  limit: number;
104
105
  includeBodies?: boolean;
@@ -114,6 +115,7 @@ export type ListLogsPageResult = {
114
115
  export type ListLogsCursorPageOptions = {
115
116
  sessionId?: string;
116
117
  model?: string;
118
+ clientPid?: number;
117
119
  limit: number;
118
120
  includeBodies?: boolean;
119
121
  beforeLogId?: number;
@@ -145,6 +147,7 @@ type LogCursorPageIndex = {
145
147
  key: string;
146
148
  sessionId?: string;
147
149
  model?: string;
150
+ clientPid?: number;
148
151
  ids: number[];
149
152
  logsById: Map<number, CapturedLog>;
150
153
  entriesById: Map<number, LogIndexEntry>;
@@ -155,17 +158,19 @@ let logCursorPageIndexBuildCount = 0;
155
158
  let sessionArchiveQueue: Promise<void> = Promise.resolve();
156
159
 
157
160
  function cursorPageIndexKey(
158
- options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
161
+ options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
159
162
  ): string {
160
163
  return JSON.stringify({
161
164
  sessionId: options.sessionId ?? null,
162
165
  model: options.model ?? null,
166
+ clientPid: options.clientPid ?? null,
163
167
  });
164
168
  }
165
169
 
166
170
  function matchesCursorIndexFilters(log: CapturedLog, index: LogCursorPageIndex): boolean {
167
171
  if (index.sessionId !== undefined && getLogSessionId(log) !== index.sessionId) return false;
168
172
  if (index.model !== undefined && log.model !== index.model) return false;
173
+ if (index.clientPid !== undefined && log.clientPid !== index.clientPid) return false;
169
174
  return true;
170
175
  }
171
176
 
@@ -561,17 +566,27 @@ async function scanLogFileForId(filePath: string, id: number): Promise<CapturedL
561
566
  return lastMatch;
562
567
  }
563
568
 
564
- export function getFilteredLogs(sessionId?: string, model?: string): CapturedLog[] {
569
+ export function getFilteredLogs(
570
+ sessionId?: string,
571
+ model?: string,
572
+ clientPid?: number,
573
+ ): CapturedLog[] {
565
574
  // Cache maintains insertion order (sorted by ID since logs are added in ID order)
566
575
  // Use spread instead of Array.from for slightly better performance
567
576
  return [...memoryCache.values()].filter((l) => {
568
- return matchesLogFilters(l, sessionId, model);
577
+ return matchesLogFilters(l, sessionId, model, clientPid);
569
578
  });
570
579
  }
571
580
 
572
- function matchesLogFilters(log: CapturedLog, sessionId?: string, model?: string): boolean {
581
+ function matchesLogFilters(
582
+ log: CapturedLog,
583
+ sessionId?: string,
584
+ model?: string,
585
+ clientPid?: number,
586
+ ): boolean {
573
587
  if (sessionId !== undefined && getLogSessionId(log) !== sessionId) return false;
574
588
  if (model !== undefined && log.model !== model) return false;
589
+ if (clientPid !== undefined && log.clientPid !== clientPid) return false;
575
590
  return true;
576
591
  }
577
592
 
@@ -637,7 +652,11 @@ async function visitArchivedSessionLogs(
637
652
  }
638
653
 
639
654
  function canUseIndexedListPage(options: ListLogsPageOptions): boolean {
640
- return options.sessionId === undefined && options.model === undefined;
655
+ return (
656
+ options.sessionId === undefined &&
657
+ options.model === undefined &&
658
+ options.clientPid === undefined
659
+ );
641
660
  }
642
661
 
643
662
  function hasUsableIndexOffset(entry: LogIndexEntry): boolean {
@@ -695,7 +714,7 @@ export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLo
695
714
  let total = 0;
696
715
 
697
716
  const visitLog = (log: CapturedLog): void => {
698
- if (!matchesLogFilters(log, options.sessionId, options.model)) return;
717
+ if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) return;
699
718
 
700
719
  if (!seenIds.has(log.id)) {
701
720
  seenIds.add(log.id);
@@ -781,16 +800,20 @@ function matchesLogIndexEntryFilters(
781
800
  entry: LogIndexEntry,
782
801
  sessionId: string | undefined,
783
802
  model: string | undefined,
803
+ clientPid: number | undefined,
784
804
  ): boolean {
785
805
  if (sessionId !== undefined && entry.sessionId !== sessionId) return false;
786
806
  if (model !== undefined && entry.model !== model) return false;
807
+ if (clientPid !== undefined && entry.summary?.clientPid !== clientPid) return false;
787
808
  return true;
788
809
  }
789
810
 
790
811
  async function buildLogCursorPageIndexFromLogIndex(
791
- options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
812
+ options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
792
813
  allowRebuild: boolean,
793
814
  ): Promise<LogCursorPageIndex | null> {
815
+ if (options.clientPid !== undefined) return null;
816
+
794
817
  let entries = await listFilteredIndexEntries(options);
795
818
  if (entries.length === 0 && allowRebuild) {
796
819
  await rebuildIndex();
@@ -804,7 +827,7 @@ async function buildLogCursorPageIndexFromLogIndex(
804
827
  }
805
828
 
806
829
  const matchingEntries = entries.filter((entry) =>
807
- matchesLogIndexEntryFilters(entry, options.sessionId, options.model),
830
+ matchesLogIndexEntryFilters(entry, options.sessionId, options.model, options.clientPid),
808
831
  );
809
832
 
810
833
  if (matchingEntries.some((entry) => !hasLogIndexSummary(entry))) {
@@ -828,7 +851,7 @@ async function buildLogCursorPageIndexFromLogIndex(
828
851
 
829
852
  const logsById = new Map<number, CapturedLog>();
830
853
  for (const log of memoryCache.values()) {
831
- if (!matchesLogFilters(log, options.sessionId, options.model)) continue;
854
+ if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) continue;
832
855
  idsById.add(log.id);
833
856
  logsById.set(log.id, log);
834
857
  }
@@ -838,7 +861,7 @@ async function buildLogCursorPageIndexFromLogIndex(
838
861
  const archivedLogs = await listArchivedSessionLogs(options.sessionId);
839
862
  for (const archivedLog of archivedLogs) {
840
863
  const log = normalizeLog(archivedLog);
841
- if (!matchesLogFilters(log, options.sessionId, options.model)) continue;
864
+ if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) continue;
842
865
  observeSessionLog(log);
843
866
  idsById.add(log.id);
844
867
  logsById.set(log.id, log);
@@ -849,6 +872,7 @@ async function buildLogCursorPageIndexFromLogIndex(
849
872
  key: cursorPageIndexKey(options),
850
873
  sessionId: options.sessionId,
851
874
  model: options.model,
875
+ clientPid: options.clientPid,
852
876
  ids: [...idsById].sort((left, right) => left - right),
853
877
  logsById,
854
878
  entriesById,
@@ -856,7 +880,7 @@ async function buildLogCursorPageIndexFromLogIndex(
856
880
  }
857
881
 
858
882
  async function buildLogCursorPageIndex(
859
- options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
883
+ options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
860
884
  ): Promise<LogCursorPageIndex> {
861
885
  logCursorPageIndexBuildCount += 1;
862
886
  const indexed = await buildLogCursorPageIndexFromLogIndex(options, true);
@@ -866,7 +890,7 @@ async function buildLogCursorPageIndex(
866
890
  const logsById = new Map<number, CapturedLog>();
867
891
 
868
892
  const visitLog = (log: CapturedLog): void => {
869
- if (!matchesLogFilters(log, options.sessionId, options.model)) return;
893
+ if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) return;
870
894
  logsById.set(log.id, log);
871
895
  };
872
896
 
@@ -885,6 +909,7 @@ async function buildLogCursorPageIndex(
885
909
  key,
886
910
  sessionId: options.sessionId,
887
911
  model: options.model,
912
+ clientPid: options.clientPid,
888
913
  ids: [...logsById.keys()].sort((left, right) => left - right),
889
914
  logsById,
890
915
  entriesById: new Map<number, LogIndexEntry>(),
@@ -892,7 +917,7 @@ async function buildLogCursorPageIndex(
892
917
  }
893
918
 
894
919
  async function getLogCursorPageIndex(
895
- options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
920
+ options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
896
921
  ): Promise<LogCursorPageIndex> {
897
922
  const key = cursorPageIndexKey(options);
898
923
  const cached = logCursorPageIndexes.get(key);
@@ -1027,7 +1052,7 @@ async function collectIndexedSessionLogSummaries(sessionId: string): Promise<Ses
1027
1052
 
1028
1053
  const needsMetadataRebuild = entries.some((entry) => !hasLogIndexFilterMetadata(entry));
1029
1054
  const needsSummaryRebuild = entries.some((entry) => {
1030
- if (!matchesLogIndexEntryFilters(entry, sessionId, undefined)) return false;
1055
+ if (!matchesLogIndexEntryFilters(entry, sessionId, undefined, undefined)) return false;
1031
1056
  return !hasLogIndexSummary(entry);
1032
1057
  });
1033
1058
 
@@ -1038,7 +1063,7 @@ async function collectIndexedSessionLogSummaries(sessionId: string): Promise<Ses
1038
1063
 
1039
1064
  const summaries: SessionLogSummary[] = [];
1040
1065
  for (const entry of entries) {
1041
- if (!matchesLogIndexEntryFilters(entry, sessionId, undefined)) continue;
1066
+ if (!matchesLogIndexEntryFilters(entry, sessionId, undefined, undefined)) continue;
1042
1067
  if (entry.summary === undefined) continue;
1043
1068
  summaries.push(buildSessionLogSummary(compactLogFromIndexSummary(entry.summary)));
1044
1069
  }