@agent-native/dispatch 0.15.24 → 0.15.26

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 (68) hide show
  1. package/dist/actions/get-agent-thread-debug.d.ts +7 -1
  2. package/dist/actions/get-agent-thread-debug.js +16 -3
  3. package/dist/actions/get-agent-thread-debug.js.map +1 -1
  4. package/dist/actions/list-connected-agents.js +7 -3
  5. package/dist/actions/list-connected-agents.js.map +1 -1
  6. package/dist/actions/list-curated-workspace-templates.js +1 -1
  7. package/dist/actions/list-curated-workspace-templates.js.map +1 -1
  8. package/dist/actions/provider-api-register.d.ts +14 -14
  9. package/dist/actions/remix-workspace-template.js +5 -5
  10. package/dist/actions/remix-workspace-template.js.map +1 -1
  11. package/dist/actions/search-agent-threads.js +1 -1
  12. package/dist/actions/search-agent-threads.js.map +1 -1
  13. package/dist/actions/start-workspace-app-creation.js +1 -1
  14. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  15. package/dist/components/create-app-popover.js +2 -2
  16. package/dist/components/create-app-popover.js.map +1 -1
  17. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  18. package/dist/components/dispatch-control-plane.js +13 -3
  19. package/dist/components/dispatch-control-plane.js.map +1 -1
  20. package/dist/components/workspace-app-card.d.ts.map +1 -1
  21. package/dist/components/workspace-app-card.js +35 -4
  22. package/dist/components/workspace-app-card.js.map +1 -1
  23. package/dist/components/workspace-template-card.js +4 -4
  24. package/dist/components/workspace-template-card.js.map +1 -1
  25. package/dist/lib/workspace-apps.d.ts +3 -0
  26. package/dist/lib/workspace-apps.d.ts.map +1 -1
  27. package/dist/lib/workspace-apps.js.map +1 -1
  28. package/dist/routes/pages/apps.d.ts.map +1 -1
  29. package/dist/routes/pages/apps.js +19 -4
  30. package/dist/routes/pages/apps.js.map +1 -1
  31. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  32. package/dist/routes/pages/thread-debug.js +25 -11
  33. package/dist/routes/pages/thread-debug.js.map +1 -1
  34. package/dist/server/lib/app-creation-store.d.ts +3 -0
  35. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  36. package/dist/server/lib/app-creation-store.js +54 -2
  37. package/dist/server/lib/app-creation-store.js.map +1 -1
  38. package/dist/server/lib/curated-workspace-templates.d.ts +1 -1
  39. package/dist/server/lib/curated-workspace-templates.js +2 -2
  40. package/dist/server/lib/curated-workspace-templates.js.map +1 -1
  41. package/dist/server/lib/thread-debug-store.d.ts +7 -1
  42. package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
  43. package/dist/server/lib/thread-debug-store.js +43 -5
  44. package/dist/server/lib/thread-debug-store.js.map +1 -1
  45. package/dist/server/plugins/integrations.js +1 -1
  46. package/dist/server/plugins/integrations.js.map +1 -1
  47. package/package.json +3 -3
  48. package/src/actions/get-agent-thread-debug.ts +30 -15
  49. package/src/actions/list-connected-agents.ts +7 -2
  50. package/src/actions/list-curated-workspace-templates.ts +1 -1
  51. package/src/actions/remix-workspace-template.spec.ts +2 -2
  52. package/src/actions/remix-workspace-template.ts +5 -5
  53. package/src/actions/search-agent-threads.ts +1 -1
  54. package/src/actions/start-workspace-app-creation.ts +1 -1
  55. package/src/components/create-app-popover.tsx +2 -2
  56. package/src/components/dispatch-control-plane.tsx +70 -4
  57. package/src/components/workspace-app-card.spec.tsx +43 -0
  58. package/src/components/workspace-app-card.tsx +97 -1
  59. package/src/components/workspace-template-card.spec.tsx +4 -4
  60. package/src/components/workspace-template-card.tsx +4 -4
  61. package/src/lib/workspace-apps.ts +3 -0
  62. package/src/routes/pages/apps.tsx +84 -4
  63. package/src/routes/pages/thread-debug.tsx +34 -14
  64. package/src/server/lib/app-creation-store.ts +95 -2
  65. package/src/server/lib/curated-workspace-templates.ts +2 -2
  66. package/src/server/lib/thread-debug-store.spec.ts +82 -0
  67. package/src/server/lib/thread-debug-store.ts +62 -6
  68. package/src/server/plugins/integrations.ts +1 -1
@@ -87,6 +87,7 @@ interface ThreadDebugResponse {
87
87
  };
88
88
  access: { viewerEmail: string; scope: string; canInspectAll: boolean };
89
89
  thread: ThreadSearchResult;
90
+ lookup?: { requestedId: string; threadId: string; runId: string | null };
90
91
  messages: ThreadMessage[];
91
92
  debug: any;
92
93
  debugRuns: any[];
@@ -424,7 +425,7 @@ export default function ThreadDebugRoute() {
424
425
  const [sourceId, setSourceId] = useState(initialSourceId);
425
426
  const [query, setQuery] = useState(initialQuery);
426
427
  const [ownerEmail, setOwnerEmail] = useState(initialOwnerEmail);
427
- const [threadId, setThreadId] = useState("");
428
+ const [lookupId, setLookupId] = useState("");
428
429
  const [submittedSearch, setSubmittedSearch] = useState({
429
430
  sourceId: initialSourceId,
430
431
  query: initialQuery,
@@ -432,7 +433,8 @@ export default function ThreadDebugRoute() {
432
433
  });
433
434
  const [selected, setSelected] = useState<{
434
435
  sourceId: string;
435
- threadId: string;
436
+ lookupId: string;
437
+ lookupKind: "thread" | "run";
436
438
  ownerEmail?: string;
437
439
  } | null>(null);
438
440
 
@@ -475,7 +477,9 @@ export default function ThreadDebugRoute() {
475
477
  const detailParams = useMemo(
476
478
  () => ({
477
479
  sourceId: selected?.sourceId ?? "current",
478
- threadId: selected?.threadId ?? "",
480
+ ...(selected?.lookupKind === "run"
481
+ ? { runId: selected.lookupId }
482
+ : { threadId: selected?.lookupId ?? "" }),
479
483
  ownerEmail: selected?.ownerEmail,
480
484
  maxRuns: 20,
481
485
  maxEvents: 800,
@@ -492,7 +496,7 @@ export default function ThreadDebugRoute() {
492
496
  "get-agent-thread-debug",
493
497
  detailParams,
494
498
  {
495
- enabled: Boolean(selected?.threadId),
499
+ enabled: Boolean(selected?.lookupId),
496
500
  },
497
501
  );
498
502
 
@@ -512,10 +516,21 @@ export default function ThreadDebugRoute() {
512
516
  sourceId,
513
517
  query,
514
518
  ownerEmail: ownerEmail.trim() || undefined,
515
- threadId: selected?.threadId ?? (threadId.trim() || undefined),
519
+ threadId:
520
+ selected?.lookupKind === "thread"
521
+ ? selected.lookupId
522
+ : !selected && lookupId.trim() && !lookupId.startsWith("run-")
523
+ ? lookupId.trim()
524
+ : undefined,
525
+ runId:
526
+ selected?.lookupKind === "run"
527
+ ? selected.lookupId
528
+ : !selected && lookupId.trim() && lookupId.startsWith("run-")
529
+ ? lookupId.trim()
530
+ : undefined,
516
531
  }),
517
532
  }).catch(() => {});
518
- }, [ownerEmail, query, selected?.threadId, sourceId, threadId]);
533
+ }, [ownerEmail, query, selected, sourceId, lookupId]);
519
534
 
520
535
  return (
521
536
  <DispatchShell
@@ -573,20 +588,21 @@ export default function ThreadDebugRoute() {
573
588
 
574
589
  <div className="mt-3 grid gap-3 lg:grid-cols-[1fr_auto]">
575
590
  <Input
576
- value={threadId}
577
- onChange={(event) => setThreadId(event.target.value)}
578
- placeholder="Paste thread ID"
591
+ value={lookupId}
592
+ onChange={(event) => setLookupId(event.target.value)}
593
+ placeholder="Paste thread or request/run ID"
579
594
  className="font-mono"
580
595
  />
581
596
  <Button
582
597
  type="button"
583
598
  variant="outline"
584
599
  onClick={() => {
585
- const trimmed = threadId.trim();
600
+ const trimmed = lookupId.trim();
586
601
  if (!trimmed) return;
587
602
  setSelected({
588
603
  sourceId,
589
- threadId: trimmed,
604
+ lookupId: trimmed,
605
+ lookupKind: trimmed.startsWith("run-") ? "run" : "thread",
590
606
  ownerEmail: ownerEmail.trim() || undefined,
591
607
  });
592
608
  }}
@@ -660,11 +676,15 @@ export default function ThreadDebugRoute() {
660
676
  <ResultCard
661
677
  key={result.id}
662
678
  result={result}
663
- selected={selected?.threadId === result.id}
679
+ selected={
680
+ selected?.lookupKind === "thread" &&
681
+ selected.lookupId === result.id
682
+ }
664
683
  onSelect={() =>
665
684
  setSelected({
666
685
  sourceId: submittedSearch.sourceId,
667
- threadId: result.id,
686
+ lookupId: result.id,
687
+ lookupKind: "thread",
668
688
  ownerEmail: submittedSearch.ownerEmail || undefined,
669
689
  })
670
690
  }
@@ -691,7 +711,7 @@ export default function ThreadDebugRoute() {
691
711
  ) : (
692
712
  <div className="flex min-h-[520px] flex-col items-center justify-center rounded-lg border border-dashed bg-card px-4 text-center text-sm text-muted-foreground">
693
713
  <IconFileSearch className="mb-2 h-5 w-5" />
694
- Select or inspect a thread.
714
+ Select or inspect a thread or request/run ID.
695
715
  </div>
696
716
  )}
697
717
  </section>
@@ -66,6 +66,9 @@ export interface WorkspaceAppSummary {
66
66
  builderUrl?: string | null;
67
67
  branchName?: string | null;
68
68
  createdAt?: string | null;
69
+ createdBy?: string | null;
70
+ owner?: string | null;
71
+ teams?: string[];
69
72
  agentCardUrl?: string | null;
70
73
  agentCardReachable?: boolean;
71
74
  a2aEndpointUrl?: string | null;
@@ -125,6 +128,9 @@ interface PendingWorkspaceApp {
125
128
  contextId: string | null;
126
129
  contextLabel: string | null;
127
130
  audience?: WorkspaceAppAudience;
131
+ createdBy?: string | null;
132
+ owner?: string | null;
133
+ teams?: string[];
128
134
  createdAt: string;
129
135
  updatedAt: string;
130
136
  expiresAt: string | null;
@@ -387,6 +393,12 @@ function applyWorkspaceAppMetadataOverride(
387
393
  ...app,
388
394
  ...(shouldApplyName ? { name } : {}),
389
395
  ...(shouldApplyDescription ? { description } : {}),
396
+ ...(app.status === "pending" && !app.createdBy && override.updatedBy
397
+ ? { createdBy: override.updatedBy }
398
+ : {}),
399
+ ...(app.status === "pending" && !app.owner && override.updatedBy
400
+ ? { owner: override.updatedBy }
401
+ : {}),
390
402
  };
391
403
  }
392
404
 
@@ -448,6 +460,75 @@ function normalizeWorkspaceAppPathList(value: unknown): string[] {
448
460
  return Array.from(new Set(paths));
449
461
  }
450
462
 
463
+ function normalizeWorkspaceAppTeams(value: unknown): string[] {
464
+ const rawTeams = Array.isArray(value)
465
+ ? value
466
+ : typeof value === "string"
467
+ ? value.split(",")
468
+ : [];
469
+ return Array.from(
470
+ new Set(
471
+ rawTeams
472
+ .map((team) => (typeof team === "string" ? team.trim() : ""))
473
+ .filter(Boolean),
474
+ ),
475
+ );
476
+ }
477
+
478
+ function workspaceAppMetadataFromRecord(
479
+ record: Record<string, any>,
480
+ ): Pick<WorkspaceAppSummary, "createdAt" | "createdBy" | "owner" | "teams"> {
481
+ const config = record["agent-native"] ?? record.agentNative;
482
+ const workspaceConfig =
483
+ config && typeof config === "object" && !Array.isArray(config)
484
+ ? (config.workspaceApp ?? config.workspace ?? config)
485
+ : {};
486
+ const metadata =
487
+ record.metadata &&
488
+ typeof record.metadata === "object" &&
489
+ !Array.isArray(record.metadata)
490
+ ? record.metadata
491
+ : {};
492
+ const pick = (...values: unknown[]) =>
493
+ values.map(cleanOptionalText).find((value) => value !== undefined);
494
+
495
+ const teams = normalizeWorkspaceAppTeams(
496
+ record.teams ??
497
+ record.team ??
498
+ metadata.teams ??
499
+ metadata.team ??
500
+ workspaceConfig.teams ??
501
+ workspaceConfig.team,
502
+ );
503
+ return {
504
+ createdAt: pick(
505
+ record.createdAt,
506
+ record.created_at,
507
+ metadata.createdAt,
508
+ workspaceConfig.createdAt,
509
+ ),
510
+ createdBy: pick(
511
+ record.createdBy,
512
+ record.createdByEmail,
513
+ record.created_by,
514
+ metadata.createdBy,
515
+ metadata.createdByEmail,
516
+ workspaceConfig.createdBy,
517
+ workspaceConfig.createdByEmail,
518
+ ),
519
+ owner: pick(
520
+ record.owner,
521
+ record.ownerEmail,
522
+ record.owner_email,
523
+ metadata.owner,
524
+ metadata.ownerEmail,
525
+ workspaceConfig.owner,
526
+ workspaceConfig.ownerEmail,
527
+ ),
528
+ teams,
529
+ };
530
+ }
531
+
451
532
  function workspaceAppAudienceFromPackageJson(
452
533
  pkg: unknown,
453
534
  ): WorkspaceAppAudience | undefined {
@@ -513,6 +594,7 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
513
594
  const id = typeof entry.id === "string" ? entry.id.trim() : "";
514
595
  const pathValue = typeof entry.path === "string" ? entry.path.trim() : "";
515
596
  if (!id || !pathValue.startsWith("/")) return null;
597
+ const metadata = workspaceAppMetadataFromRecord(entry);
516
598
  return {
517
599
  id,
518
600
  name:
@@ -534,6 +616,7 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
534
616
  publicPaths: normalizeWorkspaceAppPathList(entry.publicPaths),
535
617
  protectedPaths: normalizeWorkspaceAppPathList(entry.protectedPaths),
536
618
  status: "ready",
619
+ ...metadata,
537
620
  } satisfies WorkspaceAppSummary;
538
621
  })
539
622
  .filter(
@@ -645,6 +728,9 @@ function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
645
728
  : null,
646
729
  contextId: cleanOptionalString(record.contextId),
647
730
  contextLabel: cleanOptionalString(record.contextLabel),
731
+ createdBy: cleanOptionalString(record.createdBy),
732
+ owner: cleanOptionalString(record.owner ?? record.ownerEmail),
733
+ teams: normalizeWorkspaceAppTeams(record.teams ?? record.team),
648
734
  ...(record.audience === undefined
649
735
  ? {}
650
736
  : { audience: normalizeWorkspaceAppAudience(record.audience) }),
@@ -755,6 +841,9 @@ function pendingAppToSummary(app: PendingWorkspaceApp): WorkspaceAppSummary {
755
841
  builderUrl: app.builderUrl,
756
842
  branchName: app.branchName,
757
843
  createdAt: app.createdAt,
844
+ createdBy: app.createdBy,
845
+ owner: app.owner,
846
+ teams: app.teams,
758
847
  };
759
848
  }
760
849
 
@@ -918,6 +1007,8 @@ async function recordPendingWorkspaceApp(input: {
918
1007
  projectId: input.projectId,
919
1008
  contextId: context?.id ?? null,
920
1009
  contextLabel: context?.label ?? null,
1010
+ createdBy: currentOwnerEmail(),
1011
+ owner: currentOwnerEmail(),
921
1012
  createdAt: existing?.createdAt || now,
922
1013
  updatedAt: now,
923
1014
  expiresAt: pendingWorkspaceAppExpiresAt(existing?.createdAt || now),
@@ -1038,6 +1129,7 @@ function readWorkspaceAppsFromFilesystem(
1038
1129
  const pkg = readJson(path.join(appDir, "package.json"));
1039
1130
  if (!pkg) return null;
1040
1131
  const routeAccess = workspaceAppRouteAccessFromPackageJson(pkg);
1132
+ const metadata = workspaceAppMetadataFromRecord(pkg);
1041
1133
  return {
1042
1134
  id: entry.name,
1043
1135
  name: pkg.displayName || titleCase(entry.name),
@@ -1051,6 +1143,7 @@ function readWorkspaceAppsFromFilesystem(
1051
1143
  publicPaths: routeAccess.publicPaths,
1052
1144
  protectedPaths: routeAccess.protectedPaths,
1053
1145
  status: "ready",
1146
+ ...metadata,
1054
1147
  } satisfies WorkspaceAppSummary;
1055
1148
  })
1056
1149
  .filter((app): app is WorkspaceAppSummary => !!app)
@@ -1765,8 +1858,8 @@ function buildWorkspaceAppPrompt(input: {
1765
1858
  `Use the workspace app layout: create it under apps/${appId}, mount it at /${appId}, keep it on the shared workspace database/hosting model, and avoid table-name collisions by namespacing any new domain tables to the app.`,
1766
1859
  `Important routing rule: from outside the app, link to /${appId}; inside apps/${appId}, React Router routes are app-local. Use <Link to="/review"> and navigate("/review"), not "/${appId}/review"; APP_BASE_PATH supplies the mounted prefix, and hardcoding it causes doubled URLs like /${appId}/${appId}/review.`,
1767
1860
  "Existing first-party apps are neighbors, not implementation details for this app. If the user prompt mentions Mail, Calendar, Analytics, Dispatch, or other templates, treat them as existing hosted/connected apps that this app can link to or call through A2A/default connected agents. For example, Mail, Calendar, and Analytics already exist at https://mail.agent-native.com, https://calendar.agent-native.com, and https://analytics.agent-native.com.",
1768
- `Do not clone first-party templates, create wrapper apps, or scaffold child apps/routes for Mail, Calendar, Analytics, etc. inside apps/${appId} just so this app can access them. If the request is a cross-app dashboard or overview, build only the new dashboard/overview app and delegate to the existing apps for domain work.`,
1769
- "Only create another first-party app copy when the user explicitly asks for a customized fork/copy of that app; otherwise keep using the hosted/shared app so improvements to the base template keep flowing to users.",
1861
+ `Do not create wrapper apps or scaffold child apps/routes for Mail, Calendar, Analytics, etc. inside apps/${appId} just so this app can access them. If the request is a cross-app dashboard or overview, build only the new dashboard/overview app and delegate to the existing apps for domain work.`,
1862
+ "Only create another first-party app when the user explicitly asks for a customized app from that template; otherwise keep using the hosted/shared app so improvements to the base app keep flowing to users.",
1770
1863
  selectedKeys.length
1771
1864
  ? `Dispatch will create pending vault requests for the selected keys for appId "${appId}" after this app creation request is accepted. Do not grant or sync vault keys directly from the app-creation branch.`
1772
1865
  : "Do not grant or request any Dispatch vault keys unless the user asks later.",
@@ -17,7 +17,7 @@ export interface CuratedWorkspaceTemplateStatus extends CuratedWorkspaceTemplate
17
17
  }
18
18
 
19
19
  /**
20
- * Stable first-party template metadata for the initial remix catalog.
20
+ * Stable first-party template metadata for the initial app-creation catalog.
21
21
  * `liveUrl` identifies the product URL; it is not a public-demo claim.
22
22
  */
23
23
  export const CURATED_WORKSPACE_TEMPLATES: readonly CuratedWorkspaceTemplate[] =
@@ -85,7 +85,7 @@ export const CURATED_WORKSPACE_TEMPLATES: readonly CuratedWorkspaceTemplate[] =
85
85
  liveUrl: "https://content.agent-native.com",
86
86
  category: "content",
87
87
  setupNote:
88
- "Choose a content folder or create a first document for the private remix.",
88
+ "Choose a content folder or create a first document for the private app.",
89
89
  },
90
90
  {
91
91
  id: "clips",
@@ -0,0 +1,82 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ execute: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("@agent-native/core/db", () => ({
8
+ createDbExec: vi.fn(),
9
+ getDbExec: () => ({ execute: mocks.execute }),
10
+ }));
11
+
12
+ vi.mock("./dispatch-store.js", () => ({
13
+ currentOrgId: () => null,
14
+ currentOwnerEmail: () => "owner@example.com",
15
+ }));
16
+
17
+ import {
18
+ getAgentThreadDebug,
19
+ searchAgentThreads,
20
+ } from "./thread-debug-store.js";
21
+
22
+ const thread = {
23
+ id: "thread-1",
24
+ owner_email: "owner@example.com",
25
+ title: "A production run",
26
+ preview: "Investigate this run",
27
+ thread_data: JSON.stringify({ messages: [] }),
28
+ message_count: 0,
29
+ created_at: 1,
30
+ updated_at: 2,
31
+ };
32
+
33
+ const run = {
34
+ id: "run-prod-1",
35
+ thread_id: "thread-1",
36
+ status: "completed",
37
+ started_at: 1,
38
+ completed_at: 2,
39
+ heartbeat_at: 2,
40
+ };
41
+
42
+ function rowsForQuery(sql: string, args: unknown[]) {
43
+ if (sql.includes("FROM org_members")) return [];
44
+ if (sql.includes("FROM agent_runs") && sql.includes("WHERE id = ?")) {
45
+ return [run];
46
+ }
47
+ if (sql.includes("FROM agent_runs") && sql.includes("WHERE thread_id = ?")) {
48
+ return [run];
49
+ }
50
+ if (sql.includes("FROM chat_threads")) {
51
+ return args[0] === "thread-1" || args[0] === "owner@example.com"
52
+ ? [thread]
53
+ : [];
54
+ }
55
+ return [];
56
+ }
57
+
58
+ describe("thread-debug-store request/run lookup", () => {
59
+ beforeEach(() => {
60
+ mocks.execute.mockImplementation(async ({ sql, args }) => ({
61
+ rows: rowsForQuery(sql, args),
62
+ }));
63
+ });
64
+
65
+ it("finds a thread when search input is an exact run id", async () => {
66
+ const result = await searchAgentThreads({ query: run.id });
67
+
68
+ expect(result.threads).toHaveLength(1);
69
+ expect(result.threads[0]?.id).toBe(thread.id);
70
+ });
71
+
72
+ it("resolves a run id before loading its debug snapshot", async () => {
73
+ const result = await getAgentThreadDebug({ runId: run.id });
74
+
75
+ expect(result.lookup).toEqual({
76
+ requestedId: run.id,
77
+ threadId: thread.id,
78
+ runId: run.id,
79
+ });
80
+ expect(result.thread.id).toBe(thread.id);
81
+ });
82
+ });
@@ -565,12 +565,30 @@ export async function searchAgentThreads(input: {
565
565
  const scope = ownerScope(access, input.ownerEmail);
566
566
  const where = [scope.sql];
567
567
  const args: unknown[] = [...scope.args];
568
+ const runThreadIds = q
569
+ ? (
570
+ await optionalRows<{ thread_id: string }>(
571
+ exec,
572
+ "SELECT thread_id FROM agent_runs WHERE id = ? LIMIT 1",
573
+ [q],
574
+ )
575
+ )
576
+ .map((row) => String(row.thread_id ?? "").trim())
577
+ .filter(Boolean)
578
+ : [];
568
579
  if (q) {
569
- const pattern = `%${escapeLike(q.toLowerCase())}%`;
580
+ const pattern = "%" + escapeLike(q.toLowerCase()) + "%";
581
+ const runIdClause =
582
+ runThreadIds.length > 0
583
+ ? " OR id IN (" + runThreadIds.map(() => "?").join(", ") + ")"
584
+ : "";
570
585
  where.push(
571
- `(LOWER(title) LIKE ? ESCAPE '\\' OR LOWER(preview) LIKE ? ESCAPE '\\' OR LOWER(thread_data) LIKE ? ESCAPE '\\')`,
586
+ "(LOWER(title) LIKE ? ESCAPE '\\' OR LOWER(preview) LIKE ? ESCAPE '\\' OR LOWER(thread_data) LIKE ? ESCAPE '\\'" +
587
+ runIdClause +
588
+ ")",
572
589
  );
573
590
  args.push(pattern, pattern, pattern);
591
+ args.push(...runThreadIds);
574
592
  }
575
593
  args.push(limit);
576
594
 
@@ -603,7 +621,8 @@ export async function searchAgentThreads(input: {
603
621
 
604
622
  export async function getAgentThreadDebug(input: {
605
623
  sourceId?: string;
606
- threadId: string;
624
+ threadId?: string;
625
+ runId?: string;
607
626
  ownerEmail?: string;
608
627
  maxRuns?: number;
609
628
  maxEvents?: number;
@@ -614,17 +633,49 @@ export async function getAgentThreadDebug(input: {
614
633
  assertSourceAccess(source, access);
615
634
  const exec = await execForSource(source);
616
635
  const scope = ownerScope(access, input.ownerEmail);
617
- const rows = await queryRows<ChatThreadRow>(
636
+ const requestedId = input.runId?.trim() || input.threadId?.trim() || "";
637
+ if (!requestedId) {
638
+ throw new Error("A thread ID or request/run ID is required.");
639
+ }
640
+
641
+ let rows = await queryRows<ChatThreadRow>(
618
642
  exec,
619
643
  `SELECT id, owner_email, title, preview, thread_data, message_count, created_at, updated_at
620
644
  FROM chat_threads
621
645
  WHERE id = ? AND ${scope.sql}
622
646
  LIMIT 1`,
623
- [input.threadId, ...scope.args],
647
+ [input.threadId?.trim() || requestedId, ...scope.args],
624
648
  );
649
+ let resolvedRunId = input.runId?.trim() || null;
650
+
651
+ if (!rows[0]) {
652
+ const runRows = await optionalRows<{ id: string; thread_id: string }>(
653
+ exec,
654
+ `SELECT id, thread_id
655
+ FROM agent_runs
656
+ WHERE id = ?
657
+ LIMIT 1`,
658
+ [requestedId],
659
+ );
660
+ const threadId = runRows[0]?.thread_id
661
+ ? String(runRows[0].thread_id).trim()
662
+ : "";
663
+ if (threadId) {
664
+ rows = await queryRows<ChatThreadRow>(
665
+ exec,
666
+ `SELECT id, owner_email, title, preview, thread_data, message_count, created_at, updated_at
667
+ FROM chat_threads
668
+ WHERE id = ? AND ${scope.sql}
669
+ LIMIT 1`,
670
+ [threadId, ...scope.args],
671
+ );
672
+ if (rows[0]) resolvedRunId = requestedId;
673
+ }
674
+ }
675
+
625
676
  const row = rows[0];
626
677
  if (!row) {
627
- throw new Error(`Thread "${input.threadId}" was not found.`);
678
+ throw new Error(`Thread or request/run ID "${requestedId}" was not found.`);
628
679
  }
629
680
 
630
681
  const threadData = safeJsonParse<Record<string, unknown>>(
@@ -763,6 +814,11 @@ export async function getAgentThreadDebug(input: {
763
814
  createdAt: numberField(row.created_at),
764
815
  updatedAt: numberField(row.updated_at),
765
816
  },
817
+ lookup: {
818
+ requestedId,
819
+ threadId: String(row.id),
820
+ runId: resolvedRunId,
821
+ },
766
822
  messages: normalizeMessages(threadData),
767
823
  debug: (threadData as any)?._debug ?? null,
768
824
  debugRuns: Array.isArray((threadData as any)?._debugRuns)
@@ -40,7 +40,7 @@ When a user asks for something:
40
40
  - After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.
41
41
  - Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.
42
42
  - If the user asks to create, build, make, scaffold, or generate an "agent" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.
43
- - If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized fork/copy.
43
+ - If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized app from that template.
44
44
  - If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind.
45
45
  - If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode "coming-soon", say this requires a code change and they can edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like; do not send them to Builder org/beta settings. If it returns mode "builder-unavailable", the action also returns a \`reason\` code plus a user-facing \`message\` and (for some reasons) an operator-facing \`detail\`. Relay the \`message\` to the user as-is; only surface \`detail\` when the user is clearly an operator debugging the deployment (e.g. troubleshooting a broken Builder connection for the workspace), never as routine detail for an end user.
46
46
  - For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.