From 7571a5a7ae932cb91f00c884f595a022636d080d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 12:23:36 -0700 Subject: [PATCH 1/3] test(web): pin the scheduled-run body's person-address wording (CL-8580) --- apps/web/src/agent-deploy.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/web/src/agent-deploy.test.ts b/apps/web/src/agent-deploy.test.ts index df152874f..1133b6116 100644 --- a/apps/web/src/agent-deploy.test.ts +++ b/apps/web/src/agent-deploy.test.ts @@ -4,6 +4,7 @@ import { agentDeploySourceAssetName, agentSlugFromSourceAssetName, buildAgentDefinitionJson, + buildScheduledRunBody, } from "./agent-deploy"; describe("buildAgentDefinitionJson", () => { @@ -34,6 +35,20 @@ describe("buildAgentDefinitionJson", () => { }); }); +describe("buildScheduledRunBody", () => { + test("asks the agent to mail the deploying person's address", () => { + const body = buildScheduledRunBody("alice@example.test"); + expect(body).toContain("alice@example.test"); + expect(body).toContain("`to` list"); + }); + + test("falls back to a bare reply instruction when no address is known", () => { + const body = buildScheduledRunBody(undefined); + expect(body).not.toContain("@"); + expect(body).toContain("Reply with the result."); + }); +}); + describe("agentSlugFromSourceAssetName", () => { test("recovers the slug agentDeploySourceAssetName wrapped", () => { expect(agentSlugFromSourceAssetName(agentDeploySourceAssetName("echo-bot"))).toBe("echo-bot"); From 38a13e205b281a2cfaec158282a14bda0bbc9494 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 12:23:36 -0700 Subject: [PATCH 2/3] fix(web): a scheduled run reports to the person who deployed it (CL-8580) --- apps/web/src/agent-deploy.ts | 39 +++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/apps/web/src/agent-deploy.ts b/apps/web/src/agent-deploy.ts index 3081eab53..1048375c4 100644 --- a/apps/web/src/agent-deploy.ts +++ b/apps/web/src/agent-deploy.ts @@ -17,6 +17,10 @@ const AssetCreatedShape = type({ id: "string" }); const AssetListShape = type({ id: "string", name: "string" }).array(); const GitTokenMintShape = type({ id: "string", secret: "string" }); const TenantDomainShape = type({ domain: "string" }); +// Same shape `session.ts`'s `fetchSession` parses; a person's refId is +// their better-auth user id, exactly what `threads-api.ts` builds a +// principal's mailbox address from. +const SessionUserShape = type({ user: { id: "string" } }); const PUSH_TOKEN_LIFETIME_MS = 10 * 60 * 1000; @@ -241,6 +245,36 @@ function cronPath(tenantId: string): string { return `/api/tenants/${encodeURIComponent(tenantId)}/cron`; } +/** The scheduled-run mail body: names the person to report to (when known) + * so the agent's reply has somewhere routable to go, since the cron + * sender itself has no mailbox. */ +export function buildScheduledRunBody(deployerAddress?: string): string { + const task = "Do the work your definition describes."; + if (deployerAddress === undefined) { + return `This is your scheduled run. ${task} Reply with the result.`; + } + return `This is your scheduled run. ${task} Mail the result to ${deployerAddress} (pass it as a single-item \`to\` list) with a short, descriptive subject.`; +} + +/** The deploying person's mailbox address — same source `session.ts`'s + * `fetchSession` reads, same shape `threads-api.ts` builds a person + * participant's address from (`@`). Best-effort: a + * session probe that fails or comes back signed-out just means the + * scheduled run's body falls back to naming nobody, never a failed + * deploy over it. */ +async function resolveDeployerAddress( + tenantDomain: string, + fetchImpl: typeof fetch, +): Promise { + const response = await fetchImpl("/api/auth/get-session", { + headers: { accept: "application/json" }, + }).catch(() => undefined); + if (response === undefined || !response.ok) return undefined; + const body: unknown = await response.json().catch(() => undefined); + const parsed = SessionUserShape(body); + return parsed instanceof type.errors ? undefined : `${parsed.user.id}@${tenantDomain}`; +} + /** Creates a `@corbits/cron` schedule row addressed at a deployed agent's * run — the only way an agent fires on a cadence, since Interchange's * `schedule` trigger is reserved but unimplemented. */ @@ -248,8 +282,10 @@ async function scheduleAgentRun( tenantId: string, expression: string, runAddress: string, + tenantDomain: string, fetchImpl: typeof fetch, ): Promise { + const deployerAddress = await resolveDeployerAddress(tenantDomain, fetchImpl); const created = await fetchImpl(cronPath(tenantId), { method: "POST", headers: { "content-type": "application/json" }, @@ -257,7 +293,7 @@ async function scheduleAgentRun( expression, toAddress: runAddress, subject: "Scheduled run", - body: "This is your scheduled run. Do the work your definition describes and reply with the result.", + body: buildScheduledRunBody(deployerAddress), }), }); if (!created.ok) { @@ -346,6 +382,7 @@ export async function deployAgentSource( // The deployment id is the top-level run id (already `run_…`), and // the run address is that id at the tenant domain. `${parsed.id}@${tenant.domain}`, + tenant.domain, fetchImpl, ); } From da2cd24ee11820b646e7f889a051419c2b8e2d6f Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 12:28:03 -0700 Subject: [PATCH 3/3] fix(web): report caught errors resolving the deployer address (CL-8580) --- apps/web/src/agent-deploy.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/web/src/agent-deploy.ts b/apps/web/src/agent-deploy.ts index 1048375c4..fb55a3cfa 100644 --- a/apps/web/src/agent-deploy.ts +++ b/apps/web/src/agent-deploy.ts @@ -7,6 +7,7 @@ import { renderBundledWorkflowSourceTree } from "@corbits/workflows/client"; import { type } from "arktype"; import { WorkflowDeploymentResponse } from "@intx/types"; +import { reportError } from "@corbits/error-sink"; import { resolveExistingOffering } from "./onboarding/provider-connect-step"; import { isValidSlug, slugify } from "@/lib/slug"; @@ -266,11 +267,23 @@ async function resolveDeployerAddress( tenantDomain: string, fetchImpl: typeof fetch, ): Promise { - const response = await fetchImpl("/api/auth/get-session", { - headers: { accept: "application/json" }, - }).catch(() => undefined); - if (response === undefined || !response.ok) return undefined; - const body: unknown = await response.json().catch(() => undefined); + let response: Response; + try { + response = await fetchImpl("/api/auth/get-session", { + headers: { accept: "application/json" }, + }); + } catch (cause) { + reportError(cause, { operation: "agent_deploy_resolve_deployer_address" }); + return undefined; + } + if (!response.ok) return undefined; + let body: unknown; + try { + body = await response.json(); + } catch (cause) { + reportError(cause, { operation: "agent_deploy_resolve_deployer_address" }); + return undefined; + } const parsed = SessionUserShape(body); return parsed instanceof type.errors ? undefined : `${parsed.user.id}@${tenantDomain}`; }