From 551293aee084cfe3f6e2015e682de39366ac8a86 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 22 Aug 2026 09:06:26 -0700 Subject: [PATCH 1/2] Add test for routine trigger mail on empty stored input (CL-6678) A section-mode routine run's onTrigger occurrence only starts in response to inbound mail; skipping mail on empty stored input leaves it permanently un-triggered. This test currently fails against the existing implementation. --- apps/hub/src/routine-launcher.test.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/hub/src/routine-launcher.test.ts b/apps/hub/src/routine-launcher.test.ts index 9998ee7c..a657afcb 100644 --- a/apps/hub/src/routine-launcher.test.ts +++ b/apps/hub/src/routine-launcher.test.ts @@ -187,15 +187,33 @@ describe("createHubRoutineLauncher", () => { ); }); - test("sends no mail when the routine's stored input is empty", async () => { + // CL-6678: the run deploys under AGENT_SECTION_MODE (an `onTrigger` + // section, CL-6329/CL-6367) — a turn only ever runs in response to an + // inbound mail. Skipping mail on empty stored input (the pre-CL-6367 + // "starts from system prompt alone" behavior) left the section with + // zero occurrences forever: deployed, never delivering, stuck + // "running". A placeholder mail — mirroring + // `triggerNativeWorkflowRoutineRun`'s own empty-content substitution — + // is what actually fires the section's first occurrence. + test("sends a placeholder trigger mail when the routine's stored input is empty", async () => { launchFoldedRunCalls = []; sendFoldedMailWithRetryCalls = []; + sendFoldedMailWithRetryResult = { + ok: true, + mail: { id: "m_1", createdAt: new Date().toISOString() }, + }; const result = await buildLauncher().launchRoutineRun(baseInput({})); expect(result.runId).toBeTruthy(); expect(launchFoldedRunCalls).toHaveLength(1); - expect(sendFoldedMailWithRetryCalls).toHaveLength(0); + expect(sendFoldedMailWithRetryCalls).toHaveLength(1); + + const [, params] = sendFoldedMailWithRetryCalls[0] as [ + unknown, + { content: string }, + ]; + expect(params.content).toBe("Run this routine now."); }); // CL-6367: a routine-driven run with no stable-id -> current-run From 6f24d484c68b87a24620dd952d36d375aeffbdcc Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 22 Aug 2026 09:06:34 -0700 Subject: [PATCH 2/2] Routine launcher: always send a trigger mail, even with no stored input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routine runs deploy under AGENT_SECTION_MODE, an onTrigger section whose one and only occurrence starts in response to an inbound mail (CL-6329/CL-6367). Skipping mail when the routine's stored input was empty — the pre-CL-6367 "starts from the system prompt alone" behavior — left the section with zero occurrences: deployed, never delivering, stuck "running" forever, exactly the CL-6678 repro. Mirrors triggerNativeWorkflowRoutineRun's own placeholder substitution for the multi-step path. --- apps/hub/src/routine-launcher.ts | 67 +++++++++++++++++++------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/apps/hub/src/routine-launcher.ts b/apps/hub/src/routine-launcher.ts index 4f95e2e9..c38cb066 100644 --- a/apps/hub/src/routine-launcher.ts +++ b/apps/hub/src/routine-launcher.ts @@ -8,13 +8,17 @@ // logic (what a folded run is, how a routine fires) lives in those // packages, and this adapter is pure composition. // -// After launch, a non-empty stored `input` (the stepper-collected -// topic/focus a routine's creator recorded) is delivered as the run's -// first inbound mail via `sendFoldedMailWithRetry` — the same seam -// `@corbits/webhook-triggers`' `launchWebhookTrigger` uses for its own -// rendered input (both hardened identically; see that file's own note). -// Both "run now" and a scheduled fire land here (see `@corbits/routines`' -// `launchAndCorrelate`), so this one call covers both; a +// After launch, the routine's stored `input` (the stepper-collected +// topic/focus a routine's creator recorded), or a placeholder when it +// stored none, is delivered as the run's first inbound mail via +// `sendFoldedMailWithRetry` — the same seam `@corbits/webhook-triggers`' +// `launchWebhookTrigger` uses for its own rendered input (both hardened +// identically; see that file's own note). The mail is never optional: the +// run deploys under `AGENT_SECTION_MODE`, an `onTrigger` section whose +// one and only occurrence starts in response to an inbound mail, so a +// routine with no stored input still needs a real message to ever run a +// turn (CL-6678). Both "run now" and a scheduled fire land here (see +// `@corbits/routines`' `launchAndCorrelate`), so this one call covers both; a // webhook-triggered routine's fire never reaches this adapter at all // (`launchWebhookTrigger` launches directly), so its own input delivery // is that package's concern, not this one's. @@ -215,27 +219,34 @@ export function createHubRoutineLauncher( } } - // Empty/absent stored input keeps prior behavior: no mail, the - // agent starts from its system prompt alone. - const content = renderRoutineInput(input.input); - if (content !== "") { - const cryptoProvider = await deps.cryptoProviderCache.get(instanceId); - const result = await sendFoldedMailWithRetry(deps, { - tenantId: input.tenantId, - sessionId: launched.sessionId, - agentAddress: triggerAddress, - from: `${input.principalId}@${tenantRow.domain}`, - domain: domainOf(triggerAddress), - content, - cryptoProvider, - }); - if (!result.ok) { - const reason = - result.error instanceof Error - ? result.error.message - : String(result.error); - log.error`routine run ${instanceId} launched but its stored input failed to deliver after ${result.attempts} attempts: ${reason}`; - } + // The run just deployed under AGENT_SECTION_MODE — an `onTrigger` + // section (CL-6329/CL-6367) whose one and only occurrence starts + // in response to an inbound mail. Unlike the pre-CL-6367 `step` + // shape, there is no "start from the system prompt alone" + // fallback: skipping mail here leaves the section permanently + // un-triggered (deployed, zero occurrences, forever "running" — + // CL-6678). Mirror `triggerNativeWorkflowRoutineRun`'s own + // empty-content substitution so a routine with no separately + // stored input still fires. + const renderedContent = renderRoutineInput(input.input); + const content = + renderedContent === "" ? "Run this routine now." : renderedContent; + const cryptoProvider = await deps.cryptoProviderCache.get(instanceId); + const result = await sendFoldedMailWithRetry(deps, { + tenantId: input.tenantId, + sessionId: launched.sessionId, + agentAddress: triggerAddress, + from: `${input.principalId}@${tenantRow.domain}`, + domain: domainOf(triggerAddress), + content, + cryptoProvider, + }); + if (!result.ok) { + const reason = + result.error instanceof Error + ? result.error.message + : String(result.error); + log.error`routine run ${instanceId} launched but its trigger mail failed to deliver after ${result.attempts} attempts: ${reason}`; } return { runId: instanceId };