@tonyclaw/agent-inspector 2.0.41 → 2.0.43

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 (42) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DCg6S2cl.js → CompareDrawer-CxM1gCfL.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-TtRG-0E7.js +117 -0
  4. package/.output/public/assets/{ReplayDialog-ClVgjSDE.js → ReplayDialog-UseUkucS.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-BhqvLQp9.js → RequestAnatomy-aPxgEJ2L.js} +1 -1
  6. package/.output/public/assets/{ResponseView-BZAJoK6B.js → ResponseView-DA-F4F97.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-CaORmoKX.js → StreamingChunkSequence-JSQEPeNS.js} +1 -1
  8. package/.output/public/assets/_sessionId-BEXuCWq5.js +1 -0
  9. package/.output/public/assets/index-Bhsa_2xX.js +1 -0
  10. package/.output/public/assets/index-DOWlRJ0W.css +1 -0
  11. package/.output/public/assets/{main-CI3HFEcV.js → main-BpGVJcpB.js} +2 -2
  12. package/.output/server/{_sessionId-DQ0ljHQ8.mjs → _sessionId-DGn-TENM.mjs} +2 -2
  13. package/.output/server/_ssr/{CompareDrawer-nkVa9epn.mjs → CompareDrawer-CFElCSYY.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-CXA7iH3H.mjs → ProxyViewerContainer-B7SR9mrD.mjs} +383 -76
  15. package/.output/server/_ssr/{ReplayDialog-CK71-ucq.mjs → ReplayDialog-DfKapj0k.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-DDCUJJ3X.mjs → RequestAnatomy-CUxTCg31.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-C6ZA7V3B.mjs → ResponseView-BXY0w197.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-DW5v_o6G.mjs → StreamingChunkSequence-CzMnES9H.mjs} +2 -2
  19. package/.output/server/_ssr/{index-Ckex98yq.mjs → index-CVlT-REW.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-ChJIVv7-.mjs → router-B2d1LUx6.mjs} +1313 -468
  22. package/.output/server/{_tanstack-start-manifest_v-CJ4__weU.mjs → _tanstack-start-manifest_v-Drpi28BM.mjs} +1 -1
  23. package/.output/server/index.mjs +60 -60
  24. package/README.md +36 -12
  25. package/package.json +1 -1
  26. package/src/components/ProxyViewer.tsx +156 -17
  27. package/src/components/ProxyViewerContainer.tsx +66 -4
  28. package/src/components/groups/GroupsDialog.tsx +11 -10
  29. package/src/components/providers/SettingsDialog.tsx +137 -0
  30. package/src/components/proxy-viewer/LogEntry.tsx +107 -56
  31. package/src/components/ui/dialog.tsx +1 -1
  32. package/src/lib/runContract.ts +3 -0
  33. package/src/mcp/currentContext.ts +65 -0
  34. package/src/mcp/mode.ts +56 -0
  35. package/src/mcp/server.ts +466 -15
  36. package/src/mcp/toolHandlers.ts +372 -0
  37. package/src/proxy/evidenceExporter.ts +1 -0
  38. package/src/proxy/runStore.ts +74 -22
  39. package/.output/public/assets/ProxyViewerContainer-DsU6QETm.js +0 -115
  40. package/.output/public/assets/_sessionId-BHqywvM3.js +0 -1
  41. package/.output/public/assets/index-B_LPYuM0.js +0 -1
  42. package/.output/public/assets/index-BfGJEb-2.css +0 -1
@@ -16,10 +16,18 @@
16
16
  */
17
17
 
18
18
  import { Buffer } from "node:buffer";
19
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
20
+ import { dirname, join } from "node:path";
21
+ import JSZip from "jszip";
19
22
  import { z } from "zod";
20
23
  import { getCurrentPort } from "../lib/serverPort";
21
24
  import { LoopbackTimeoutError, type CallApiOptions } from "./loopback";
22
25
  import { extractLastUserMessagePreview, extractResponsePreview } from "./previewExtractor";
26
+ import {
27
+ getCurrentContext,
28
+ setCurrentContext,
29
+ type InspectorCurrentContextPatch,
30
+ } from "./currentContext";
23
31
  import {
24
32
  type AddInspectorGroupSessionInput,
25
33
  type CreateInspectorGroupInput,
@@ -36,6 +44,7 @@ import {
36
44
  exportInspectorGroupEvidence,
37
45
  getInspectorGroup,
38
46
  listInspectorGroups,
47
+ readInspectorGroupEvidence,
39
48
  updateInspectorGroup,
40
49
  } from "../services/groups";
41
50
  import {
@@ -43,6 +52,7 @@ import {
43
52
  exportInspectorRunEvidence,
44
53
  getInspectorRun,
45
54
  listInspectorRecentFailures,
55
+ readInspectorRunEvidence,
46
56
  updateInspectorRun,
47
57
  } from "../services/runs";
48
58
 
@@ -423,6 +433,227 @@ export async function exportGroupEvidenceImpl(args: ExportGroupEvidenceArgs): Pr
423
433
  return textJson(exported.value);
424
434
  }
425
435
 
436
+ const SESSION_TIMELINE_DEFAULT_LIMIT = 20;
437
+ const SESSION_TIMELINE_LIMIT_MAX = 50;
438
+
439
+ export type SessionTimelineArgs = {
440
+ sessionId: string;
441
+ limit?: number;
442
+ offset?: number;
443
+ };
444
+
445
+ function clampTimelineLimit(input: number | undefined): number {
446
+ if (input === undefined) return SESSION_TIMELINE_DEFAULT_LIMIT;
447
+ if (!Number.isFinite(input) || input < 1) return 1;
448
+ return Math.min(Math.floor(input), SESSION_TIMELINE_LIMIT_MAX);
449
+ }
450
+
451
+ export function buildSessionTimeline(
452
+ sessionId: string,
453
+ logs: CapturedLog[],
454
+ total: number,
455
+ limit: number,
456
+ ): Record<string, unknown> {
457
+ const sortedLogs = [...logs].sort((left, right) => left.id - right.id);
458
+ const events = sortedLogs.map((log, index) => ({
459
+ sequence: index + 1,
460
+ kind: "request",
461
+ logId: log.id,
462
+ ...buildLogSummary(log),
463
+ }));
464
+ const first = sortedLogs[0];
465
+ const last = sortedLogs[sortedLogs.length - 1];
466
+ const errorCount = events.filter((event) => event.hasError).length;
467
+ const models = [...new Set(sortedLogs.map((log) => log.model).filter((model) => model !== null))];
468
+ const providers = [
469
+ ...new Set(
470
+ sortedLogs.map((log) => log.providerName ?? null).filter((provider) => provider !== null),
471
+ ),
472
+ ];
473
+
474
+ return {
475
+ sessionId,
476
+ logCoverage: "bounded",
477
+ total,
478
+ limit,
479
+ eventCount: events.length,
480
+ requestCount: events.length,
481
+ errorCount,
482
+ startedAt: first?.timestamp ?? null,
483
+ updatedAt: last?.timestamp ?? null,
484
+ models,
485
+ providers,
486
+ events,
487
+ };
488
+ }
489
+
490
+ export async function getSessionTimelineImpl(
491
+ callApi: CallApiFn,
492
+ args: SessionTimelineArgs,
493
+ ): Promise<ToolResult> {
494
+ const limit = clampTimelineLimit(args.limit);
495
+ const offset = args.offset ?? 0;
496
+ const params = new URLSearchParams({
497
+ offset: String(offset),
498
+ limit: String(limit),
499
+ compact: "1",
500
+ sessionId: args.sessionId,
501
+ });
502
+ const path = `/api/logs?${params.toString()}`;
503
+ const res = await callApi(path);
504
+ if (!res.ok) return toolError(`GET ${path} returned ${res.status}`);
505
+ const rawBody: unknown = await res.json();
506
+ const parsed = LogsListResponseSchema.safeParse(rawBody);
507
+ if (!parsed.success) return toolError("GET /api/logs returned an unparseable timeline shape");
508
+ const logs = parsed.data.logs ?? [];
509
+ return textJson(
510
+ buildSessionTimeline(args.sessionId, logs, parsed.data.total ?? logs.length, limit),
511
+ );
512
+ }
513
+
514
+ type EvidenceFileKind = "json" | "markdown" | "html" | "zip";
515
+
516
+ type EvidenceFileSummary = {
517
+ kind: EvidenceFileKind;
518
+ path: string;
519
+ exists: boolean;
520
+ bytes: number | null;
521
+ included?: boolean;
522
+ };
523
+
524
+ type EvidencePaths = {
525
+ jsonPath: string;
526
+ markdownPath: string;
527
+ htmlPath: string;
528
+ exportedAt: string;
529
+ };
530
+
531
+ function fileBytes(path: string): number | null {
532
+ if (!existsSync(path)) return null;
533
+ try {
534
+ return statSync(path).size;
535
+ } catch {
536
+ return null;
537
+ }
538
+ }
539
+
540
+ function evidenceFileSummary(kind: EvidenceFileKind, path: string): EvidenceFileSummary {
541
+ return {
542
+ kind,
543
+ path,
544
+ exists: existsSync(path),
545
+ bytes: fileBytes(path),
546
+ };
547
+ }
548
+
549
+ function evidenceFiles(evidence: EvidencePaths): EvidenceFileSummary[] {
550
+ return [
551
+ evidenceFileSummary("json", evidence.jsonPath),
552
+ evidenceFileSummary("markdown", evidence.markdownPath),
553
+ evidenceFileSummary("html", evidence.htmlPath),
554
+ ];
555
+ }
556
+
557
+ function readEvidenceFile(path: string): Buffer | null {
558
+ if (!existsSync(path)) return null;
559
+ try {
560
+ return readFileSync(path);
561
+ } catch {
562
+ return null;
563
+ }
564
+ }
565
+
566
+ async function exportEvidenceZip(
567
+ ownerKind: "run" | "group",
568
+ ownerId: string,
569
+ evidence: EvidencePaths,
570
+ ): Promise<Record<string, unknown>> {
571
+ const zip = new JSZip();
572
+ const sourceFiles = evidenceFiles(evidence);
573
+ const includedFiles: EvidenceFileSummary[] = [];
574
+
575
+ for (const file of sourceFiles) {
576
+ const content = readEvidenceFile(file.path);
577
+ const included = content !== null;
578
+ includedFiles.push({ ...file, included });
579
+ if (included) {
580
+ zip.file(`${file.kind}.${file.kind === "markdown" ? "md" : file.kind}`, content);
581
+ }
582
+ }
583
+
584
+ zip.file(
585
+ "manifest.json",
586
+ `${JSON.stringify(
587
+ {
588
+ ownerKind,
589
+ ownerId,
590
+ exportedAt: evidence.exportedAt,
591
+ bundledAt: new Date().toISOString(),
592
+ files: includedFiles,
593
+ },
594
+ null,
595
+ 2,
596
+ )}\n`,
597
+ );
598
+
599
+ const zipPath = join(
600
+ dirname(evidence.jsonPath),
601
+ ownerKind === "run" ? "evidence.zip" : "group-evidence.zip",
602
+ );
603
+ const buffer = await zip.generateAsync({ type: "nodebuffer" });
604
+ writeFileSync(zipPath, buffer);
605
+ const zipSummary = evidenceFileSummary("zip", zipPath);
606
+ return {
607
+ ownerKind,
608
+ ownerId,
609
+ bundlePath: zipPath,
610
+ bytes: zipSummary.bytes,
611
+ files: [...includedFiles, zipSummary],
612
+ };
613
+ }
614
+
615
+ export function getEvidenceFilesImpl(args: { runId: string }): ToolResult {
616
+ const evidence = readInspectorRunEvidence(args.runId);
617
+ if (!evidence.ok) return toolError(evidence.message);
618
+ return textJson({
619
+ runId: args.runId,
620
+ exportedAt: evidence.value.evidence.exportedAt,
621
+ files: evidenceFiles(evidence.value.evidence),
622
+ });
623
+ }
624
+
625
+ export async function exportEvidenceBundleImpl(args: { runId: string }): Promise<ToolResult> {
626
+ const evidence = readInspectorRunEvidence(args.runId);
627
+ if (!evidence.ok) return toolError(evidence.message);
628
+ return textJson(await exportEvidenceZip("run", args.runId, evidence.value.evidence));
629
+ }
630
+
631
+ export function getGroupEvidenceFilesImpl(args: { groupId: string }): ToolResult {
632
+ const evidence = readInspectorGroupEvidence(args.groupId);
633
+ if (!evidence.ok) return toolError(evidence.message);
634
+ return textJson({
635
+ groupId: args.groupId,
636
+ exportedAt: evidence.value.evidence.exportedAt,
637
+ files: evidenceFiles(evidence.value.evidence),
638
+ });
639
+ }
640
+
641
+ export async function exportGroupEvidenceBundleImpl(args: {
642
+ groupId: string;
643
+ }): Promise<ToolResult> {
644
+ const evidence = readInspectorGroupEvidence(args.groupId);
645
+ if (!evidence.ok) return toolError(evidence.message);
646
+ return textJson(await exportEvidenceZip("group", args.groupId, evidence.value.evidence));
647
+ }
648
+
649
+ export function getCurrentContextImpl(): ToolResult {
650
+ return textJson(getCurrentContext());
651
+ }
652
+
653
+ export function setCurrentContextImpl(args: InspectorCurrentContextPatch): ToolResult {
654
+ return textJson(setCurrentContext(args));
655
+ }
656
+
426
657
  export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
427
658
  const res = await callApi("/api/models");
428
659
  if (!res.ok) return toolError(`GET /api/models returned ${res.status}`);
@@ -540,6 +771,147 @@ export async function testProviderImpl(callApi: CallApiFn, id: string): Promise<
540
771
  return textJson(await res.json());
541
772
  }
542
773
 
774
+ type ProviderDiagnosticIssue = {
775
+ path: string;
776
+ status: number | null;
777
+ error: string | null;
778
+ durationMs: number | null;
779
+ endpoint: string | null;
780
+ };
781
+
782
+ function isRecord(value: unknown): value is Record<string, unknown> {
783
+ return typeof value === "object" && value !== null && !Array.isArray(value);
784
+ }
785
+
786
+ function stringField(record: Record<string, unknown>, key: string): string | null {
787
+ const value = record[key];
788
+ return typeof value === "string" && value.length > 0 ? value : null;
789
+ }
790
+
791
+ function numberField(record: Record<string, unknown>, key: string): number | null {
792
+ const value = record[key];
793
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
794
+ }
795
+
796
+ function firstStringField(record: Record<string, unknown>, keys: string[]): string | null {
797
+ for (const key of keys) {
798
+ const value = stringField(record, key);
799
+ if (value !== null) return value;
800
+ }
801
+ return null;
802
+ }
803
+
804
+ function firstNumberField(record: Record<string, unknown>, keys: string[]): number | null {
805
+ for (const key of keys) {
806
+ const value = numberField(record, key);
807
+ if (value !== null) return value;
808
+ }
809
+ return null;
810
+ }
811
+
812
+ function collectProviderDiagnosticIssues(
813
+ value: unknown,
814
+ path: string,
815
+ issues: ProviderDiagnosticIssue[],
816
+ ): void {
817
+ if (Array.isArray(value)) {
818
+ value.forEach((item, index) =>
819
+ collectProviderDiagnosticIssues(item, `${path}[${String(index)}]`, issues),
820
+ );
821
+ return;
822
+ }
823
+ if (!isRecord(value)) return;
824
+
825
+ const success = value["success"];
826
+ const status = firstNumberField(value, ["status", "responseStatus", "statusCode"]);
827
+ const error = firstStringField(value, ["error", "message", "errorMessage"]);
828
+ if (success === false || error !== null || (status !== null && status >= 400)) {
829
+ issues.push({
830
+ path,
831
+ status,
832
+ error,
833
+ durationMs: firstNumberField(value, ["durationMs", "elapsedMs", "latencyMs"]),
834
+ endpoint: firstStringField(value, ["endpoint", "url", "requestUrl", "baseUrl"]),
835
+ });
836
+ }
837
+
838
+ for (const [key, child] of Object.entries(value)) {
839
+ if (typeof child === "object" && child !== null) {
840
+ collectProviderDiagnosticIssues(child, `${path}.${key}`, issues);
841
+ }
842
+ }
843
+ }
844
+
845
+ function stringifyUnknown(value: unknown): string {
846
+ try {
847
+ const text = JSON.stringify(value);
848
+ return text === undefined ? "" : text.toLowerCase();
849
+ } catch {
850
+ return "";
851
+ }
852
+ }
853
+
854
+ function providerDiagnosticHints(result: unknown): string[] {
855
+ const text = stringifyUnknown(result);
856
+ const hints: string[] = [];
857
+ if (text.includes("timeout") || text.includes("aborted")) {
858
+ hints.push(
859
+ "Provider test timed out or was aborted. Increase Settings > Proxy > Provider test timeout when the provider or model is slow.",
860
+ );
861
+ }
862
+ if (text.includes("localhost") || text.includes("127.0.0.1")) {
863
+ hints.push(
864
+ "The upstream URL contains localhost. If the agent or provider runs in another container, replace localhost with a reachable host/IP.",
865
+ );
866
+ }
867
+ if (text.includes("econnrefused") || text.includes("enotfound") || text.includes("network")) {
868
+ hints.push(
869
+ "This looks like a network or DNS failure. Verify the Inspector container/host can reach the provider base URL.",
870
+ );
871
+ }
872
+ if (
873
+ text.includes("401") ||
874
+ text.includes("403") ||
875
+ text.includes("unauthorized") ||
876
+ text.includes("forbidden")
877
+ ) {
878
+ hints.push(
879
+ "This looks like an authentication failure. Check the provider API key and auth header mode.",
880
+ );
881
+ }
882
+ if (text.includes("404") || text.includes("not found")) {
883
+ hints.push(
884
+ "This looks like a base URL or path mismatch. Check whether the provider endpoint already includes /v1 or needs a vendor-specific path.",
885
+ );
886
+ }
887
+ if (text.includes("nonstream") || text.includes("non-stream")) {
888
+ hints.push(
889
+ "The failure mentions non-streaming. Compare the OpenAI/Anthropic non-streaming path and payload with the streaming request.",
890
+ );
891
+ }
892
+ return [...new Set(hints)];
893
+ }
894
+
895
+ export async function diagnoseProviderTestImpl(
896
+ callApi: CallApiFn,
897
+ id: string,
898
+ ): Promise<ToolResult> {
899
+ const res = await callApi(`/api/providers/${encodeURIComponent(id)}/test`, {
900
+ method: "POST",
901
+ });
902
+ if (!res.ok) return toolError(`POST /api/providers/${id}/test returned ${res.status}`);
903
+ const result: unknown = await res.json();
904
+ const issues: ProviderDiagnosticIssue[] = [];
905
+ collectProviderDiagnosticIssues(result, "result", issues);
906
+ return textJson({
907
+ providerId: id,
908
+ issueCount: issues.length,
909
+ issues,
910
+ hints: providerDiagnosticHints(result),
911
+ result,
912
+ });
913
+ }
914
+
543
915
  export type CreateSessionKnowledgeArgs = {
544
916
  sessionId: string;
545
917
  };
@@ -48,6 +48,7 @@ function buildMarkdown(document: EvidenceDocument): string {
48
48
  "",
49
49
  `- Run ID: ${document.run.id}`,
50
50
  `- Session ID: ${document.run.sessionId}`,
51
+ `- Group ID: ${document.run.groupId ?? "n/a"}`,
51
52
  `- Status: ${document.run.status}`,
52
53
  `- Project: ${document.run.project ?? "n/a"}`,
53
54
  `- Agent: ${document.run.agent ?? "n/a"}`,
@@ -12,6 +12,7 @@ import {
12
12
  type UpdateInspectorRunInput,
13
13
  } from "../lib/runContract";
14
14
  import { getDataDir } from "./dataDir";
15
+ import { addGroupSession, getGroup } from "./groupStore";
15
16
 
16
17
  const RunsFileSchema = z.object({
17
18
  runs: z.array(InspectorRunSchema),
@@ -48,6 +49,51 @@ function normalizeSessionId(input: string | undefined, runId: string): string {
48
49
  return input.trim();
49
50
  }
50
51
 
52
+ function normalizeGroupId(input: string | null | undefined): string | null {
53
+ if (input === undefined || input === null) return null;
54
+ const normalized = stableId(input);
55
+ return normalized.length > 0 ? normalized : null;
56
+ }
57
+
58
+ function metadataText(metadata: InspectorRun["metadata"], key: string): string | undefined {
59
+ const value = metadata[key];
60
+ if (typeof value !== "string") return undefined;
61
+ const trimmed = value.trim();
62
+ return trimmed.length > 0 ? trimmed : undefined;
63
+ }
64
+
65
+ function optionalText(value: string | null): string | undefined {
66
+ return value ?? undefined;
67
+ }
68
+
69
+ function autoGroupMemberId(runId: string): string {
70
+ return stableId(`run-${runId}`);
71
+ }
72
+
73
+ function runHasGroupMember(run: InspectorRun): boolean {
74
+ if (run.groupId === null) return false;
75
+ const group = getGroup(run.groupId);
76
+ if (group === null) return false;
77
+ return group.members.some((member) => member.id === autoGroupMemberId(run.id));
78
+ }
79
+
80
+ function syncRunGroupMember(run: InspectorRun, createIfMissing: boolean): void {
81
+ if (run.groupId === null) return;
82
+ if (!createIfMissing && !runHasGroupMember(run)) return;
83
+
84
+ addGroupSession(run.groupId, {
85
+ memberId: autoGroupMemberId(run.id),
86
+ sessionId: run.sessionId,
87
+ runId: run.id,
88
+ label: run.title,
89
+ agent: optionalText(run.agent),
90
+ provider: metadataText(run.metadata, "provider") ?? metadataText(run.metadata, "providerName"),
91
+ model: metadataText(run.metadata, "model") ?? metadataText(run.metadata, "modelName"),
92
+ status: run.status,
93
+ metadata: run.metadata,
94
+ });
95
+ }
96
+
51
97
  function readRuns(): InspectorRun[] {
52
98
  const filePath = runsFilePath();
53
99
  if (!existsSync(filePath)) return [];
@@ -83,9 +129,11 @@ export function createRun(input: CreateInspectorRunInput): InspectorRun {
83
129
 
84
130
  const timestamp = nowIso();
85
131
  const sessionId = normalizeSessionId(parsed?.sessionId, runId);
132
+ const groupId = normalizeGroupId(parsed?.groupId);
86
133
  const run: InspectorRun = {
87
134
  id: runId,
88
135
  sessionId,
136
+ groupId,
89
137
  title: parsed?.title ?? `Inspector Run ${runId}`,
90
138
  task: parsed?.task ?? null,
91
139
  project: parsed?.project ?? null,
@@ -99,6 +147,7 @@ export function createRun(input: CreateInspectorRunInput): InspectorRun {
99
147
  };
100
148
 
101
149
  writeRuns([...readRuns(), run]);
150
+ syncRunGroupMember(run, parsed?.sessionId !== undefined);
102
151
  return run;
103
152
  }
104
153
 
@@ -125,32 +174,35 @@ export function updateRunEvidence(
125
174
  export function updateRun(runId: string, input: UpdateInspectorRunInput): InspectorRun | null {
126
175
  const parsed = UpdateInspectorRunInputSchema.parse(input);
127
176
  const runs = readRuns();
128
- let updatedRun: InspectorRun | null = null;
177
+ const run = runs.find((candidate) => candidate.id === runId) ?? null;
129
178
  const updatedAt = nowIso();
130
179
 
131
- const updatedRuns = runs.map((run) => {
132
- if (run.id !== runId) return run;
133
- updatedRun = {
134
- ...run,
135
- sessionId:
136
- parsed.sessionId === undefined
137
- ? run.sessionId
138
- : normalizeSessionId(parsed.sessionId, run.id),
139
- title: parsed.title ?? run.title,
140
- task: parsed.task === undefined ? run.task : parsed.task,
141
- project: parsed.project === undefined ? run.project : parsed.project,
142
- agent: parsed.agent === undefined ? run.agent : parsed.agent,
143
- status: parsed.status ?? run.status,
144
- tags: parsed.tags ?? run.tags,
145
- metadata:
146
- parsed.metadata === undefined ? run.metadata : { ...run.metadata, ...parsed.metadata },
147
- updatedAt,
148
- };
149
- return updatedRun;
150
- });
180
+ if (run === null) return null;
181
+
182
+ const updatedRun: InspectorRun = {
183
+ ...run,
184
+ sessionId:
185
+ parsed.sessionId === undefined ? run.sessionId : normalizeSessionId(parsed.sessionId, run.id),
186
+ groupId: parsed.groupId === undefined ? run.groupId : normalizeGroupId(parsed.groupId),
187
+ title: parsed.title ?? run.title,
188
+ task: parsed.task === undefined ? run.task : parsed.task,
189
+ project: parsed.project === undefined ? run.project : parsed.project,
190
+ agent: parsed.agent === undefined ? run.agent : parsed.agent,
191
+ status: parsed.status ?? run.status,
192
+ tags: parsed.tags ?? run.tags,
193
+ metadata:
194
+ parsed.metadata === undefined ? run.metadata : { ...run.metadata, ...parsed.metadata },
195
+ updatedAt,
196
+ };
197
+ const updatedRuns = runs.map((candidate) => (candidate.id === runId ? updatedRun : candidate));
151
198
 
152
- if (updatedRun === null) return null;
153
199
  writeRuns(updatedRuns);
200
+ syncRunGroupMember(
201
+ updatedRun,
202
+ parsed.sessionId !== undefined ||
203
+ parsed.groupId !== undefined ||
204
+ updatedRun.sessionId !== updatedRun.id,
205
+ );
154
206
  return updatedRun;
155
207
  }
156
208