Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b1e6caa
Add test for workflowDeployBody's catalog-offering shape
TheGreatAxios Sep 6, 2026
fae77c0
e2e: deploy workflows by catalog offering ids
TheGreatAxios Sep 6, 2026
b03ce16
Fix formatting
TheGreatAxios Sep 6, 2026
f981798
Add tests for deploying default workflows by catalog offering ids
TheGreatAxios Sep 6, 2026
1ce35c4
Seeding: deploy default workflows by catalog offering ids
TheGreatAxios Sep 6, 2026
2587cd2
longevity-sim: deploy workflows by catalog offering ids
TheGreatAxios Sep 6, 2026
2430449
Fix formatting: prettier
TheGreatAxios Sep 6, 2026
b65781e
Merge remote-tracking branch 'origin/cl-7461-seed-deploy-offering-ids…
TheGreatAxios Sep 6, 2026
602ad68
sandbox-sidecar: add tests for generation-0 ensure/destroy requests
TheGreatAxios Sep 6, 2026
e01feda
sandbox-sidecar: accept the allocation service's initial generation
TheGreatAxios Sep 6, 2026
c6da1fa
sandbox-sidecar: fix formatting in provisioner test
TheGreatAxios Sep 6, 2026
72d5be0
Merge remote-tracking branch 'origin/cl-7107-repin-and-re-vendor-audi…
TheGreatAxios Sep 6, 2026
824c2b6
Add tests for resolved catalog offerings and modelSourceFor's trimmed…
TheGreatAxios Sep 6, 2026
ec87551
Seed deploys against a tenant's resolved catalog offerings, not just …
TheGreatAxios Sep 6, 2026
dd1fe50
Merge remote-tracking branch 'origin/cl-7461-seed-deploy-offering-ids…
TheGreatAxios Sep 6, 2026
bcb8703
Add a regression test pinning the shared-sidecar auth gap (CL-7472)
TheGreatAxios Sep 6, 2026
ccbe497
e2e: stop pre-provisioning a shared sidecar identity (CL-7472)
TheGreatAxios Sep 6, 2026
308ab25
ci: upload hub/sidecar e2e logs as a build artifact (CL-7472)
TheGreatAxios Sep 6, 2026
27b8f84
Merge remote-tracking branch 'origin/cl-7107-repin-and-re-vendor-audi…
TheGreatAxios Sep 6, 2026
b72964e
Add tests for tenant-scoped workbench_launch address resolution (CL-7…
TheGreatAxios Sep 6, 2026
262a381
Chat: workbench_launch address/stableId lookups are tenant-scoped
TheGreatAxios Sep 6, 2026
b75eebc
Chat: run prettier on the tenant-scoping change
TheGreatAxios Sep 6, 2026
040f085
Merge remote-tracking branch 'origin/cl-7107-repin-and-re-vendor-audi…
TheGreatAxios Sep 6, 2026
75300cb
Merge the re-pin branch into cl-7474-cross-tenant-launch
TheGreatAxios Sep 6, 2026
0eef0e0
Chat: tenant-scoped binding reads require the tenant; the one unscope…
TheGreatAxios Sep 6, 2026
b677451
Add tests for the shared retry-until-routable delivery helper
TheGreatAxios Sep 6, 2026
5cb3164
Deliver a webhook trigger's opening mail once its sidecar becomes rou…
TheGreatAxios Sep 6, 2026
a0c8a27
Log a skip, not an error, when root tenant seed has no operator key c…
TheGreatAxios Sep 6, 2026
ff22eef
Merge remote-tracking branch 'origin/cl-7474-cross-tenant-launch' int…
TheGreatAxios Sep 6, 2026
1fd89dd
Run prettier
TheGreatAxios Sep 6, 2026
3f8fdc9
Chat: run prettier on the tenant-scoping test file
TheGreatAxios Sep 6, 2026
3cf5507
Merge remote-tracking branches 'origin/cl-7474-cross-tenant-launch' a…
TheGreatAxios Sep 6, 2026
46a4a93
Add a DB-gated test for recording a run's agent_session (CL-7477)
TheGreatAxios Sep 6, 2026
39ea012
Native launch records the run's agent_session so mail and spans persi…
TheGreatAxios Sep 6, 2026
5f92950
Merge remote-tracking branch 'origin/cl-7107-repin-and-re-vendor-audi…
TheGreatAxios Sep 6, 2026
f31e0e1
Add tests for the run's event collector create/abandon wiring
TheGreatAxios Sep 6, 2026
9260c3a
Native launch creates the run's event collector so turns are recorded…
TheGreatAxios Sep 6, 2026
3706a53
Merge remote-tracking branch 'origin/cl-7107-repin-and-re-vendor-audi…
TheGreatAxios Sep 6, 2026
f6369d9
Add tests for lazy, idempotent run-session ensure (CL-7480)
TheGreatAxios Sep 6, 2026
d6eda39
Hub ensures a run's session and collector on first use (CL-7480)
TheGreatAxios Sep 6, 2026
63b2d2e
Fix prettier formatting in agent-session.ts (CL-7480)
TheGreatAxios Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ import {
import {
createHubMailboxAuthorizeSender,
createHubMailboxResolveRefs,
createHubPersistMailWithSessionEnsure,
} from "./mailbox-persist";
import {
createCommandRegistry,
Expand Down Expand Up @@ -196,6 +197,7 @@ import {
createScheduledWorkflowRoutes,
renderWorkflowSourceTree,
WORKFLOW_SOURCE_ENTRY,
ensureRunSession,
} from "@corbits/workflows";
import {
createSidecarProvisioner as createE2BSidecarProvisioner,
Expand All @@ -219,6 +221,7 @@ import {
createWorkflowAllocationService,
createWorkflowDispatchService,
DEFAULT_ASSET_REF,
resolveRoutableAddress,
WorkflowProvisioningError,
type AgentRepoStore,
type EventCollectorRegistry,
Expand Down Expand Up @@ -713,6 +716,15 @@ export async function createHub(config: HubConfig) {
args: Parameters<typeof baseLookups.registerSignalCorrelation>[0],
) => Promise<void>;
} = {};
// Forward reference: `eventCollectors` (the wrapped
// `EventCollectorRegistry`) isn't constructed until later in this
// composition, but `createHubPersistMailWithSessionEnsure` below needs
// it. Set once `eventCollectors` exists; every persistMail call before
// that point (there are none — the server isn't serving requests yet)
// would just no-op.
const eventCollectorsRef: {
current?: Pick<EventCollectorRegistry, "create" | "has">;
} = {};
// CL-6499 (native multi-step routines): materializes a mail-triggered
// run's authorization grants from its deploy-approved snapshot, so
// ANY plain mail delivered to a workflow deployment's address — not
Expand Down Expand Up @@ -746,7 +758,11 @@ export async function createHub(config: HubConfig) {
// workbench ref is present before the post-commit bus event fires --
// no out-of-band UPDATE, no polling read.
persistMail: createMailboxPersist(mailboxDb, {
upstream: baseLookups.persistMail,
upstream: createHubPersistMailWithSessionEnsure(
db,
eventCollectorsRef,
baseLookups.persistMail,
),
authorizeSender: createHubMailboxAuthorizeSender(db),
bus: mailboxBus,
resolveRefs: createHubMailboxResolveRefs(chatStore, roomMessages),
Expand Down Expand Up @@ -885,6 +901,17 @@ export async function createHub(config: HubConfig) {
baseEventCollectors.create(agentAddress, tenantId, sessionId, runId);
},
dispatch(agentAddress, event) {
// CL-7480: a run's turn events can start arriving before anything
// else ever recorded its session — the first inbound trigger that
// just reconciled its principal races this same dispatch. No live
// collector for the address is exactly that case: ensure the
// session (and, inside it, the collector) before delegating,
// rather than silently dropping the event for a collector that
// never gets created.
if (!baseEventCollectors.has(agentAddress)) {
void ensureCollectorThenDispatch(agentAddress, event);
return;
}
turnLatency.onEvent(agentAddress, event);
baseEventCollectors.dispatch(agentAddress, event);
// Mirrors the registry's own `isTerminal` check (event-collector-registry.ts)
Expand All @@ -901,6 +928,36 @@ export async function createHub(config: HubConfig) {
baseEventCollectors.abandon(agentAddress);
},
};
eventCollectorsRef.current = eventCollectors;
// Named so `dispatch`'s no-collector branch above reads clearly; not
// inlined there because it needs to be `async` and `dispatch` itself
// must stay synchronous (the `EventCollectorRegistry` contract).
async function ensureCollectorThenDispatch(
agentAddress: string,
event: Parameters<EventCollectorRegistry["dispatch"]>[1],
): Promise<void> {
try {
const run = await resolveRoutableAddress(db, agentAddress);
if (run !== undefined) {
await ensureRunSession({
db,
eventCollectors: baseEventCollectors,
runId: run.id,
});
}
} catch (err) {
reportError(err, {
operation: "hub.eventCollectors.ensureRunSession",
extra: { agentAddress },
});
}
turnLatency.onEvent(agentAddress, event);
baseEventCollectors.dispatch(agentAddress, event);
const isTerminal =
event.type === "reactor.done" ||
(event.type === "reactor.error" && event.data.fatal);
if (isTerminal) turnLatency.onSessionEnd(agentAddress);
}
createHubSessionOrchestrator({
events: sidecarRouter.events,
router: sidecarRouter,
Expand Down Expand Up @@ -3069,6 +3126,7 @@ export async function createHub(config: HubConfig) {
repoStore: agentRepoStore.repoStore,
workflowAllocationService,
sessionService,
eventCollectors,
},
runnerInput,
),
Expand Down
45 changes: 45 additions & 0 deletions apps/hub/src/mailbox-persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { tenant as tenantTable } from "@intx/db/schema";
import type {
AuthorizeMailboxSender,
CreateMailboxPersistOpts,
MailboxPersistArgs,
MailboxRef,
} from "@corbits/mailbox";
import { resolveRoutableAddress } from "@intx/hub-sessions";
Expand All @@ -24,6 +25,8 @@ import {
type ChatStore,
type RoomMessageStore,
} from "@corbits/chat";
import { ensureRunSession, type EventCollectorPort } from "@corbits/workflows";
import { reportError } from "@corbits/error-sink";

/**
* `@corbits/mailbox` does not export `ResolveMailboxRefs` itself (only the
Expand Down Expand Up @@ -57,6 +60,48 @@ type ResolveMailboxRefs = NonNullable<
* fact from two directions: one extra indexed lookup by address per frame,
* not per recipient.
*/
/**
* Wraps the hub's own `persistMail` (vendor's `baseLookups.persistMail`)
* so a run's `agent_session` and event collector exist before that write
* runs — CL-7480: the first inbound mail on a freshly triggered run is
* often the earliest point the run is mail-routable at all, since
* `workflow_run.principal_id` only reconciles onto the trigger that just
* fired. Resolves the frame's own sender address to its run
* (`resolveRoutableAddress`, the same resolver `persistMail` itself
* uses) rather than assuming the caller already knows it; a sender that
* is not a live run resolves to `undefined` and this is a no-op past
* that point. Best-effort: `ensureRunSession` failing must not swallow
* the mail upstream is about to persist regardless, so this only
* degrades to "no session yet" rather than ever throwing past itself.
*
* `eventCollectorsRef` is a forward reference (the same pattern this
* file's other composition-order refs use, e.g. `apps/hub/src/index.ts`'s
* `grantAllowanceGateRef`): the wrapped `eventCollectors` registry isn't
* constructed until after `lookups` is, so this only reads `.current`
* inside the returned closure, never at wiring time.
*/
export function createHubPersistMailWithSessionEnsure<R>(
db: DB["db"],
eventCollectorsRef: { current?: Pick<EventCollectorPort, "create" | "has"> },
upstream: (args: MailboxPersistArgs) => Promise<R>,
): (args: MailboxPersistArgs) => Promise<R> {
return async (args) => {
try {
const eventCollectors = eventCollectorsRef.current;
const sender = await resolveRoutableAddress(db, args.senderAddress);
if (sender !== undefined && eventCollectors !== undefined) {
await ensureRunSession({ db, eventCollectors, runId: sender.id });
}
} catch (err) {
reportError(err, {
operation: "hub.mailboxPersist.ensureRunSession",
extra: { senderAddress: args.senderAddress },
});
}
return upstream(args);
};
}

export function createHubMailboxAuthorizeSender(
db: DB["db"],
): AuthorizeMailboxSender {
Expand Down
35 changes: 15 additions & 20 deletions packages/agent-directory/src/one-shot-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
endAgentSessionForRun,
readDefinitionProjection,
readFoldedBody,
recordAgentSessionForRun,
WORKFLOW_SOURCE_ENTRY,
type EventCollectorPort,
} from "@corbits/workflows";
import { listVisibleOfferings, type DB } from "@intx/db";
import { tenant as tenantTable, workflowDefinition } from "@intx/db/schema";
Expand Down Expand Up @@ -57,6 +57,7 @@ export type OneShotRunnerDeps = {
"prepareProvisionedDeployment"
>;
readonly sessionService: Pick<SessionService, "sendUserMessage">;
readonly eventCollectors: EventCollectorPort;
/**
* Test seam only. Production never sets these; they default to
* Interchange `prepareProvisionedDeployment` and `sendUserMessage`.
Expand Down Expand Up @@ -164,10 +165,10 @@ async function provisionOnAsset(
? { toolPackagePins: input.foldedBody.toolPackagePins }
: {}),
});
// Not `recordAgentSessionForRun` here: a freshly provisioned run's
// Not `ensureRunSession` here: a freshly provisioned run's
// `workflow_run.principal_id` is still null until its first trigger
// reconciles one onto it — `runOneShotPrompt` records the session
// right after the opening prompt actually sends.
// reconciles one onto it, and the hub's own mail/dispatch seams
// ensure the session lazily once that happens (CL-7480).
return {
runId: prepared.anchorRunId,
address: prepared.deploymentAddress,
Expand Down Expand Up @@ -267,7 +268,11 @@ export async function runOneShotPrompt(
clearTimeout(timer);
unsubscribe();
try {
await endAgentSessionForRun(deps.db, launched.runId);
await endAgentSessionForRun(
deps.db,
launched.runId,
deps.eventCollectors,
);
} catch (err) {
reportError(err, {
operation: "agent-directory.one-shot.end-session",
Expand Down Expand Up @@ -316,21 +321,11 @@ export async function runOneShotPrompt(
cryptoProvider,
});
}
// The send above just drove the run's trigger path, the only
// thing that ever reconciles a principal onto a freshly
// provisioned `workflow_run` (CL-7477) — recording only now is
// what makes this ever find one.
try {
await recordAgentSessionForRun(deps.db, {
sessionId: launched.sessionId,
anchorRunId: launched.runId,
});
} catch (err) {
reportError(err, {
operation: "agent-directory.one-shot.recordAgentSession",
extra: { address: launched.address },
});
}
// A run's session and event collector are ensured lazily now, at
// the hub seams that actually see the run become mail-routable
// (`apps/hub/src/mailbox-persist.ts`, the wrapped
// `eventCollectors` dispatch in `apps/hub/src/index.ts`) —
// CL-7480. Nothing here needs to record it.
} catch (cause) {
reportError(cause, {
operation: "agent-directory.one-shot.send",
Expand Down
33 changes: 26 additions & 7 deletions packages/chat/src/platform-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { and, desc, eq } from "drizzle-orm";
import {
authoredDefinitionCandidates,
DefinitionProjectionMissingError,
endAgentSessionForPrincipal,
endAgentSessionForRun,
ensureRunSession,
readFoldedBody,
resolveNewestProjectedDefinition,
WORKFLOW_SOURCE_ENTRY,
Expand Down Expand Up @@ -343,11 +344,12 @@ export function createHubChatPlatform(
? { toolPackagePins: input.foldedBody.toolPackagePins }
: {}),
});
// Not `recordAgentSessionForRun` here: a freshly provisioned run's
// Not `ensureRunSession` here: a freshly provisioned run's
// `workflow_run.principal_id` is still null (an invite sits
// un-triggered until someone actually writes into it), so this
// would only ever no-op. `sendRunMail` records it instead, once the
// run's first turn has actually reconciled a principal onto it.
// would only ever no-op. The hub's own mail/dispatch seams ensure
// the session lazily instead, once the run's first turn has
// actually reconciled a principal onto it (CL-7480).
return {
runId: prepared.anchorRunId,
address: prepared.deploymentAddress,
Expand Down Expand Up @@ -476,9 +478,7 @@ export function createHubChatPlatform(
foldedBody: binding.foldedBody,
});

if (run.principalId !== null) {
await endAgentSessionForPrincipal(deps.db, run.principalId);
}
await endAgentSessionForRun(deps.db, run.id, deps.eventCollectors);

await repointBinding(
deps.db,
Expand Down Expand Up @@ -1314,6 +1314,25 @@ export function createHubChatPlatform(
// idle sweep the moment they see traffic.
lifecycle?.track(deliveryAddress);

// CL-7480: a run just woken/reconciled above can be routable
// before anything ever recorded its session — waking it is not
// itself a mail or dispatch seam, so nothing else along this path
// would ensure one. Best-effort: an ensure failure here still
// falls through to `resolveRunSessionIdOrThrow`'s own error.
try {
await ensureRunSession({
db: deps.db,
eventCollectors: deps.eventCollectors,
runId: delivery.run.id,
});
} catch (err) {
reportError(err, {
operation: "chat.sendMail.ensureRunSession",
tenantId: input.tenantId,
agentId: deliveryAddress,
extra: { runId: delivery.run.id },
});
}
const sessionId = await resolveRunSessionIdOrThrow(deps.db, delivery.run);
const domain = domainOf(deliveryAddress);
// `fromWorkbenchId` names the room a dispatch speaks for. A room
Expand Down
26 changes: 5 additions & 21 deletions packages/chat/src/send-run-mail.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// Signs one inbound message via Interchange `sendUserMessage` and
// records it on session_mail so the run's own live subscribers see it.
import { recordAgentSessionForRun } from "@corbits/workflows";
import { reportError } from "@corbits/error-sink";
import { sessionMail } from "@intx/db/schema";
import type { DB } from "@intx/db";
import type { SessionService, SidecarRouter } from "@intx/hub-sessions";
Expand Down Expand Up @@ -102,24 +100,10 @@ export async function sendRunMail(
const mailId = crypto.randomUUID();
const now = new Date();
const rawMIME = await deliverRunMailMIME(deps, params, mailId, now);
// `sendUserMessage` just drove this run's trigger path, which is the
// only thing that ever reconciles a principal onto a freshly
// provisioned `workflow_run` (CL-7477) — so the run is mail-routable
// now if it never was before. Best-effort and idempotent past the
// first successful call; a failure here must not swallow the mail
// that already sent.
try {
await recordAgentSessionForRun(deps.db, {
sessionId: params.sessionId,
address: params.agentAddress,
});
} catch (err) {
reportError(err, {
operation: "chat.sendRunMail.recordAgentSession",
tenantId: params.tenantId,
agentId: params.agentAddress,
extra: { sessionId: params.sessionId },
});
}
// A run's session and event collector are ensured lazily now, at the
// hub seams that actually see the run become mail-routable
// (`apps/hub/src/mailbox-persist.ts`, the wrapped `eventCollectors`
// dispatch in `apps/hub/src/index.ts`) — CL-7480. Nothing here needs
// to record it.
return recordRunMail(deps, params, mailId, now, rawMIME);
}
25 changes: 5 additions & 20 deletions packages/webhook-triggers/src/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
deliverWhenRoutable,
readDefinitionProjection,
readFoldedBody,
recordAgentSessionForRun,
WORKFLOW_SOURCE_ENTRY,
} from "@corbits/workflows";
import { listVisibleOfferings, type DB } from "@intx/db";
Expand Down Expand Up @@ -238,25 +237,11 @@ export async function launchWebhookTrigger(
});
}

// The delivery attempt above is what drove the run's trigger path,
// the only thing that ever reconciles a principal onto a freshly
// provisioned `workflow_run` (CL-7477) — recording only now, after
// that attempt, is what makes this call ever find one. Best-effort:
// a run that never became routable stays without a session until its
// first successful send.
try {
await recordAgentSessionForRun(deps.db, {
sessionId,
anchorRunId: prepared.anchorRunId,
});
} catch (error) {
reportError(error, {
operation: "webhookTriggers.launch.recordAgentSession",
tenantId: trigger.tenantId,
agentId: prepared.deploymentAddress,
extra: { instanceId: prepared.anchorRunId },
});
}
// A run's session and event collector are ensured lazily now, at the
// hub seams that actually see the run become mail-routable
// (`apps/hub/src/mailbox-persist.ts`, the wrapped `eventCollectors`
// dispatch in `apps/hub/src/index.ts`) — CL-7480. Nothing here needs
// to record it.

return {
instanceId: prepared.anchorRunId,
Expand Down
Loading
Loading