Skip to content

Commit 6a2d65e

Browse files
fix(web): a scheduled run reports to the person who deployed it (CL-8580) (#940)
* test(web): pin the scheduled-run body's person-address wording (CL-8580) * fix(web): a scheduled run reports to the person who deployed it (CL-8580) * fix(web): report caught errors resolving the deployer address (CL-8580)
1 parent 33411c2 commit 6a2d65e

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

apps/web/src/agent-deploy.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
agentDeploySourceAssetName,
55
agentSlugFromSourceAssetName,
66
buildAgentDefinitionJson,
7+
buildScheduledRunBody,
78
} from "./agent-deploy";
89

910
describe("buildAgentDefinitionJson", () => {
@@ -34,6 +35,20 @@ describe("buildAgentDefinitionJson", () => {
3435
});
3536
});
3637

38+
describe("buildScheduledRunBody", () => {
39+
test("asks the agent to mail the deploying person's address", () => {
40+
const body = buildScheduledRunBody("alice@example.test");
41+
expect(body).toContain("alice@example.test");
42+
expect(body).toContain("`to` list");
43+
});
44+
45+
test("falls back to a bare reply instruction when no address is known", () => {
46+
const body = buildScheduledRunBody(undefined);
47+
expect(body).not.toContain("@");
48+
expect(body).toContain("Reply with the result.");
49+
});
50+
});
51+
3752
describe("agentSlugFromSourceAssetName", () => {
3853
test("recovers the slug agentDeploySourceAssetName wrapped", () => {
3954
expect(agentSlugFromSourceAssetName(agentDeploySourceAssetName("echo-bot"))).toBe("echo-bot");

apps/web/src/agent-deploy.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { renderBundledWorkflowSourceTree } from "@corbits/workflows/client";
88
import { type } from "arktype";
99
import { WorkflowDeploymentResponse } from "@intx/types";
10+
import { reportError } from "@corbits/error-sink";
1011

1112
import { resolveExistingOffering } from "./onboarding/provider-connect-step";
1213
import { isValidSlug, slugify } from "@/lib/slug";
@@ -17,6 +18,10 @@ const AssetCreatedShape = type({ id: "string" });
1718
const AssetListShape = type({ id: "string", name: "string" }).array();
1819
const GitTokenMintShape = type({ id: "string", secret: "string" });
1920
const TenantDomainShape = type({ domain: "string" });
21+
// Same shape `session.ts`'s `fetchSession` parses; a person's refId is
22+
// their better-auth user id, exactly what `threads-api.ts` builds a
23+
// principal's mailbox address from.
24+
const SessionUserShape = type({ user: { id: "string" } });
2025

2126
const PUSH_TOKEN_LIFETIME_MS = 10 * 60 * 1000;
2227

@@ -241,23 +246,67 @@ function cronPath(tenantId: string): string {
241246
return `/api/tenants/${encodeURIComponent(tenantId)}/cron`;
242247
}
243248

249+
/** The scheduled-run mail body: names the person to report to (when known)
250+
* so the agent's reply has somewhere routable to go, since the cron
251+
* sender itself has no mailbox. */
252+
export function buildScheduledRunBody(deployerAddress?: string): string {
253+
const task = "Do the work your definition describes.";
254+
if (deployerAddress === undefined) {
255+
return `This is your scheduled run. ${task} Reply with the result.`;
256+
}
257+
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.`;
258+
}
259+
260+
/** The deploying person's mailbox address — same source `session.ts`'s
261+
* `fetchSession` reads, same shape `threads-api.ts` builds a person
262+
* participant's address from (`<refId>@<tenantDomain>`). Best-effort: a
263+
* session probe that fails or comes back signed-out just means the
264+
* scheduled run's body falls back to naming nobody, never a failed
265+
* deploy over it. */
266+
async function resolveDeployerAddress(
267+
tenantDomain: string,
268+
fetchImpl: typeof fetch,
269+
): Promise<string | undefined> {
270+
let response: Response;
271+
try {
272+
response = await fetchImpl("/api/auth/get-session", {
273+
headers: { accept: "application/json" },
274+
});
275+
} catch (cause) {
276+
reportError(cause, { operation: "agent_deploy_resolve_deployer_address" });
277+
return undefined;
278+
}
279+
if (!response.ok) return undefined;
280+
let body: unknown;
281+
try {
282+
body = await response.json();
283+
} catch (cause) {
284+
reportError(cause, { operation: "agent_deploy_resolve_deployer_address" });
285+
return undefined;
286+
}
287+
const parsed = SessionUserShape(body);
288+
return parsed instanceof type.errors ? undefined : `${parsed.user.id}@${tenantDomain}`;
289+
}
290+
244291
/** Creates a `@corbits/cron` schedule row addressed at a deployed agent's
245292
* run — the only way an agent fires on a cadence, since Interchange's
246293
* `schedule` trigger is reserved but unimplemented. */
247294
async function scheduleAgentRun(
248295
tenantId: string,
249296
expression: string,
250297
runAddress: string,
298+
tenantDomain: string,
251299
fetchImpl: typeof fetch,
252300
): Promise<void> {
301+
const deployerAddress = await resolveDeployerAddress(tenantDomain, fetchImpl);
253302
const created = await fetchImpl(cronPath(tenantId), {
254303
method: "POST",
255304
headers: { "content-type": "application/json" },
256305
body: JSON.stringify({
257306
expression,
258307
toAddress: runAddress,
259308
subject: "Scheduled run",
260-
body: "This is your scheduled run. Do the work your definition describes and reply with the result.",
309+
body: buildScheduledRunBody(deployerAddress),
261310
}),
262311
});
263312
if (!created.ok) {
@@ -346,6 +395,7 @@ export async function deployAgentSource(
346395
// The deployment id is the top-level run id (already `run_…`), and
347396
// the run address is that id at the tenant domain.
348397
`${parsed.id}@${tenant.domain}`,
398+
tenant.domain,
349399
fetchImpl,
350400
);
351401
}

0 commit comments

Comments
 (0)