@agent-native/dispatch 0.15.23 → 0.15.25

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 (46) 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-curated-workspace-templates.js +1 -1
  5. package/dist/actions/list-curated-workspace-templates.js.map +1 -1
  6. package/dist/actions/provider-api-register.d.ts +11 -11
  7. package/dist/actions/remix-workspace-template.js +5 -5
  8. package/dist/actions/remix-workspace-template.js.map +1 -1
  9. package/dist/actions/search-agent-threads.js +1 -1
  10. package/dist/actions/search-agent-threads.js.map +1 -1
  11. package/dist/actions/start-workspace-app-creation.js +1 -1
  12. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  13. package/dist/components/create-app-popover.js +2 -2
  14. package/dist/components/create-app-popover.js.map +1 -1
  15. package/dist/components/workspace-template-card.js +4 -4
  16. package/dist/components/workspace-template-card.js.map +1 -1
  17. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  18. package/dist/routes/pages/thread-debug.js +25 -11
  19. package/dist/routes/pages/thread-debug.js.map +1 -1
  20. package/dist/server/lib/app-creation-store.js +2 -2
  21. package/dist/server/lib/app-creation-store.js.map +1 -1
  22. package/dist/server/lib/curated-workspace-templates.d.ts +1 -1
  23. package/dist/server/lib/curated-workspace-templates.js +2 -2
  24. package/dist/server/lib/curated-workspace-templates.js.map +1 -1
  25. package/dist/server/lib/thread-debug-store.d.ts +7 -1
  26. package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
  27. package/dist/server/lib/thread-debug-store.js +43 -5
  28. package/dist/server/lib/thread-debug-store.js.map +1 -1
  29. package/dist/server/plugins/integrations.js +1 -1
  30. package/dist/server/plugins/integrations.js.map +1 -1
  31. package/package.json +3 -3
  32. package/src/actions/get-agent-thread-debug.ts +30 -15
  33. package/src/actions/list-curated-workspace-templates.ts +1 -1
  34. package/src/actions/remix-workspace-template.spec.ts +2 -2
  35. package/src/actions/remix-workspace-template.ts +5 -5
  36. package/src/actions/search-agent-threads.ts +1 -1
  37. package/src/actions/start-workspace-app-creation.ts +1 -1
  38. package/src/components/create-app-popover.tsx +2 -2
  39. package/src/components/workspace-template-card.spec.tsx +4 -4
  40. package/src/components/workspace-template-card.tsx +4 -4
  41. package/src/routes/pages/thread-debug.tsx +34 -14
  42. package/src/server/lib/app-creation-store.ts +2 -2
  43. package/src/server/lib/curated-workspace-templates.ts +2 -2
  44. package/src/server/lib/thread-debug-store.spec.ts +82 -0
  45. package/src/server/lib/thread-debug-store.ts +62 -6
  46. package/src/server/plugins/integrations.ts +1 -1
@@ -17,11 +17,11 @@ function buildRemixPrompt(input: {
17
17
  description?: string | null;
18
18
  }): string {
19
19
  return [
20
- "Create a private workspace remix of a curated first-party template.",
20
+ "Create a private workspace app from a curated first-party template.",
21
21
  `Source template: ${input.templateName} (${input.templateId}).`,
22
22
  "Recreate the source template's product shape and capabilities as an independent workspace app.",
23
23
  "Never copy source-app data, records, user content, secrets, credentials, tokens, API keys, or private configuration.",
24
- "Use empty or synthetic seed data only, keep the remix private to the current workspace, and do not create a public demo.",
24
+ "Use empty or synthetic seed data only, keep the new app private to the current workspace, and do not create a public demo.",
25
25
  `Setup note: ${input.setupNote}`,
26
26
  input.description?.trim()
27
27
  ? `Requested customization: ${input.description.trim()}`
@@ -31,7 +31,7 @@ function buildRemixPrompt(input: {
31
31
 
32
32
  export default defineAction({
33
33
  description:
34
- "Create a private, independent remix of one of Dispatch's curated first-party workspace templates. Valid templates are mail, calendar, analytics, slides, content, clips, brain, assets, forms, and design. The remix must not copy source-app data, secrets, credentials, or private configuration; local workspaces scaffold the template, while hosted workspaces start a Builder app-creation branch.",
34
+ "Create a private, independent app from one of Dispatch's curated first-party workspace templates. Valid templates are mail, calendar, analytics, slides, content, clips, brain, assets, forms, and design. The new app must not copy source-app data, secrets, credentials, or private configuration; local workspaces scaffold the template, while hosted workspaces start a Builder app-creation branch.",
35
35
  schema: z.object({
36
36
  templateId: z
37
37
  .string()
@@ -56,7 +56,7 @@ export default defineAction({
56
56
  .max(500)
57
57
  .optional()
58
58
  .nullable()
59
- .describe("Optional customization or purpose for the private remix."),
59
+ .describe("Optional customization or purpose for the private app."),
60
60
  }),
61
61
  run: async (input) => {
62
62
  const sourceTemplate = getCuratedWorkspaceTemplate(input.templateId);
@@ -83,7 +83,7 @@ export default defineAction({
83
83
  action: "workspace-app.remix-requested",
84
84
  targetType: "workspace-app",
85
85
  targetId: appId,
86
- summary: `Requested private remix of ${sourceTemplate.name}`,
86
+ summary: `Requested private app from ${sourceTemplate.name}`,
87
87
  metadata: {
88
88
  sourceTemplate: sourceTemplate.template,
89
89
  mode:
@@ -5,7 +5,7 @@ import { searchAgentThreads } from "../server/lib/thread-debug-store.js";
5
5
 
6
6
  export default defineAction({
7
7
  description:
8
- "Search agent chat threads by title, preview, or full persisted thread content. Non-admins are limited to their own current Dispatch DB threads.",
8
+ "Search agent chat threads by title, preview, full persisted thread content, or an exact request/run ID. Non-admins are limited to their own current Dispatch DB threads.",
9
9
  schema: z.object({
10
10
  sourceId: z
11
11
  .string()
@@ -6,7 +6,7 @@ import { startWorkspaceAppCreation } from "../server/lib/app-creation-store.js";
6
6
 
7
7
  export default defineAction({
8
8
  description:
9
- 'Start creating a new workspace app from Dispatch when the request truly needs its own app. Callers should include a concise generated description by default; Dispatch generates one from the prompt when omitted. In local dev this returns a code-agent prompt; in production it creates a Builder branch only when a Builder branch project is configured. If the result mode is "coming-soon", the work still requires a code change but no Builder Cloud Agent can run here; tell the user to edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like, and do not send them to Builder org/beta settings. The result must be a separate workspace app under apps/<app-id>, not a new route or file in apps/chat. If chat is used as the source template, the finished app must be branded as the requested app and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind. If the request needs Mail, Calendar, Analytics, Brain, Assets, or another first-party app, use the existing hosted/connected app via links or A2A; do not clone, wrap, or nest those templates inside the new app unless the user explicitly asks for a customized copy.',
9
+ 'Start creating a new workspace app from Dispatch when the request truly needs its own app. Callers should include a concise generated description by default; Dispatch generates one from the prompt when omitted. In local dev this returns a code-agent prompt; in production it creates a Builder branch only when a Builder branch project is configured. If the result mode is "coming-soon", the work still requires a code change but no Builder Cloud Agent can run here; tell the user to edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like, and do not send them to Builder org/beta settings. The result must be a separate workspace app under apps/<app-id>, not a new route or file in apps/chat. If chat is used as the source template, the finished app must be branded as the requested app and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind. If the request needs Mail, Calendar, Analytics, Brain, Assets, or another first-party app, use the existing hosted/connected app via links or A2A; do not wrap or nest those apps inside the new app unless the user explicitly asks for a customized app from that template.',
10
10
  schema: z.object({
11
11
  prompt: z.string().min(1).describe("The user's app creation request"),
12
12
  appId: z
@@ -117,8 +117,8 @@ function buildAppCreationPrompt(input: {
117
117
  `Use relative workspace links like /${input.appId}. Do not hardcode localhost, 127.0.0.1, 8080, 8100, or any dev port; the active workspace gateway/browser origin owns the port.`,
118
118
  `Use the framework/template UI stack: shadcn/ui components and @tabler/icons-react. Do not add lucide-react or another icon library for standard UI.`,
119
119
  `Existing first-party apps are neighbors, not implementation details for this app. If the user's prompt mentions Mail, Calendar, Analytics, Brain, Assets, 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, Analytics, Brain, and Assets already exist at https://mail.agent-native.com, https://calendar.agent-native.com, https://analytics.agent-native.com, https://brain.agent-native.com, and https://assets.agent-native.com.`,
120
- `Do not clone first-party templates, create wrapper apps, or scaffold child apps/routes for Mail, Calendar, Analytics, Brain, Assets, etc. inside apps/${input.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.`,
121
- `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.`,
120
+ `Do not create wrapper apps or scaffold child apps/routes for Mail, Calendar, Analytics, Brain, Assets, etc. inside apps/${input.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.`,
121
+ `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.`,
122
122
  `Do not satisfy this 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.`,
123
123
  input.vaultAccessMode === "all-apps"
124
124
  ? `Do not create per-app Dispatch vault grants unless the workspace switches vault access to manual or the user explicitly asks for manual grants.`
@@ -95,7 +95,7 @@ describe("WorkspaceTemplateCard", () => {
95
95
  ).toBe(null);
96
96
  });
97
97
 
98
- it("remixes with the default app id and allows an override", async () => {
98
+ it("creates an app with the default app id and allows an override", async () => {
99
99
  await act(async () => {
100
100
  root.render(
101
101
  <WorkspaceTemplateCard template={template} defaultAppId="pipeline" />,
@@ -103,7 +103,7 @@ describe("WorkspaceTemplateCard", () => {
103
103
  });
104
104
 
105
105
  const trigger = Array.from(container.querySelectorAll("button")).find(
106
- (button) => button.textContent?.includes("Remix into workspace"),
106
+ (button) => button.textContent?.includes("Create from template"),
107
107
  );
108
108
  expect(trigger).not.toBeUndefined();
109
109
 
@@ -140,7 +140,7 @@ describe("WorkspaceTemplateCard", () => {
140
140
  });
141
141
  });
142
142
  expect(toast.success).toHaveBeenCalledWith(
143
- "Template remixed into your workspace.",
143
+ "Template app creation started.",
144
144
  );
145
145
  });
146
146
 
@@ -167,7 +167,7 @@ describe("WorkspaceTemplateCard", () => {
167
167
  expect(container.textContent).toContain("Installed");
168
168
  expect(container.querySelector('a[href^="https://"]')).toBeNull();
169
169
  const remixButton = Array.from(container.querySelectorAll("button")).find(
170
- (button) => button.textContent?.includes("Remix into workspace"),
170
+ (button) => button.textContent?.includes("Create from template"),
171
171
  );
172
172
  expect(remixButton).not.toHaveProperty("disabled", true);
173
173
  });
@@ -78,10 +78,10 @@ const DEFAULT_LABELS: WorkspaceTemplateLabels = {
78
78
  cancel: "Cancel",
79
79
  integrationSetup: "Integration setup",
80
80
  installed: "Installed",
81
- remix: "Remix into workspace",
82
- remixing: "Remixing…",
83
- remixSuccess: "Template remixed into your workspace.",
84
- remixError: "Could not remix this template",
81
+ remix: "Create from template",
82
+ remixing: "Creating app…",
83
+ remixSuccess: "Template app creation started.",
84
+ remixError: "Could not create an app from this template",
85
85
  appIdRequired: "App ID is required.",
86
86
  source: "Source",
87
87
  viewLiveApp: "View the live app",
@@ -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>
@@ -1765,8 +1765,8 @@ function buildWorkspaceAppPrompt(input: {
1765
1765
  `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
1766
  `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
1767
  "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.",
1768
+ `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.`,
1769
+ "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
1770
  selectedKeys.length
1771
1771
  ? `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
1772
  : "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.