Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 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
af790b2
Add tests for eager session recording and launch-spec session lookup
TheGreatAxios Sep 6, 2026
ae8d33b
Record a run's session at provision; re-key it when Interchange ancho…
TheGreatAxios Sep 6, 2026
2b5dd7a
Fix CI: prettier formatting, principal enum value, and workflowRunLau…
TheGreatAxios Sep 6, 2026
8b469ac
Merge the re-pin branch into cl-7481-eager-session
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
88 changes: 76 additions & 12 deletions 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,8 @@ import {
createScheduledWorkflowRoutes,
renderWorkflowSourceTree,
WORKFLOW_SOURCE_ENTRY,
ensureRunSession,
recordAgentSessionAtProvision,
} from "@corbits/workflows";
import {
createSidecarProvisioner as createE2BSidecarProvisioner,
Expand All @@ -219,6 +222,7 @@ import {
createWorkflowAllocationService,
createWorkflowDispatchService,
DEFAULT_ASSET_REF,
resolveRoutableAddress,
WorkflowProvisioningError,
type AgentRepoStore,
type EventCollectorRegistry,
Expand Down Expand Up @@ -713,6 +717,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 +759,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 +902,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 +929,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 @@ -2017,15 +2075,7 @@ export async function createHub(config: HubConfig) {
}

try {
// Not `recordAgentSessionForRun` here: this mints a deployment
// with no message sent against it — a `workflow_run` born this
// way carries no principal until its first trigger (a call this
// deployer never makes) reconciles one onto it, so the call
// would only ever no-op. That trigger runs entirely through
// Interchange's own native route, with no Workbench-owned hook
// to record the session afterward (CL-7477 tracks the chat,
// webhook, and one-shot-prompt launch paths, which all send a
// message of their own and record there instead).
const sessionId = generateId("session");
const prepared =
await workflowAllocationService.prepareProvisionedDeployment({
tenantId,
Expand All @@ -2038,12 +2088,26 @@ export async function createHub(config: HubConfig) {
},
entry,
definitionAssetId: assetId,
sessionId: generateId("session"),
sessionId,
sourceAuthorityPrincipalId: principalId,
sourceOfferingIds,
defaultSourceOfferingId,
deployContent: { systemPrompt: "" },
});
// The eager record every native launcher makes right after
// `prepareProvisionedDeployment` returns (CL-7481): this
// deployment mints no opening message of its own, but the
// trigger that eventually reconciles a principal onto it runs
// entirely through Interchange's own native route, with no
// Workbench-owned hook downstream to record the session
// afterward — so this is the only chance to record it at all.
await recordAgentSessionAtProvision({
db,
eventCollectors,
runId: prepared.anchorRunId,
sessionId,
sourceAuthorityPrincipalId: principalId,
});
return {
deploymentId: prepared.anchorRunId,
definitionAssetId: assetId,
Expand Down Expand Up @@ -2269,10 +2333,10 @@ export async function createHub(config: HubConfig) {
repoStore: agentRepoStore.repoStore,
workflowAllocationService,
credentialCipher,
eventCollectors,
isRoutable: (address) =>
sidecarRouter.getRoutableAddresses().includes(address),
cryptoProviderCache: cryptoProviders,
eventCollectors,
persistLaunch: async (input) => {
await workbenchLaunchPersistExtra(input)(db);
},
Expand Down
68 changes: 68 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,11 @@ import {
type ChatStore,
type RoomMessageStore,
} from "@corbits/chat";
import { ensureRunSession, type EventCollectorPort } from "@corbits/workflows";
import { reportError } from "@corbits/error-sink";
import { getLogger } from "@intx/log";

const logger = getLogger(["hub", "mailbox-persist"]);

/**
* `@corbits/mailbox` does not export `ResolveMailboxRefs` itself (only the
Expand Down Expand Up @@ -57,6 +63,68 @@ 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 extends readonly unknown[],
>(
db: DB["db"],
eventCollectorsRef: { current?: Pick<EventCollectorPort, "create" | "has"> },
upstream: (args: MailboxPersistArgs) => Promise<R>,
): (args: MailboxPersistArgs) => Promise<R> {
return async (args) => {
const sender = await resolveRoutableAddress(db, args.senderAddress);
try {
const eventCollectors = eventCollectorsRef.current;
if (sender !== undefined && eventCollectors !== undefined) {
await ensureRunSession({ db, eventCollectors, runId: sender.id });
}
} catch (err) {
reportError(err, {
operation: "hub.mailboxPersist.ensureRunSession",
extra: { senderAddress: args.senderAddress },
});
}
// The vendored `session_mail` ledger (`upstream`, `baseLookups.persistMail`)
// keys on the sender run's principal, which does not exist until the
// run's first trigger anchors one — a run's own first outbound mail can
// reach this wrapper before that happens. `@corbits/mailbox`'s own write
// (this function's caller, `createMailboxPersist`) is keyed on the run
// address instead and runs regardless, so it is already the durable
// record for this frame; skip the vendored delegate rather than let it
// throw "no session for address" past the caller.
if (sender !== undefined && sender.sessionId === null) {
logger.debug(
"skipping vendored persistMail for run {runId}: no session yet",
{ runId: sender.id, senderAddress: args.senderAddress },
);
// No `session_mail` rows were written for this frame: an empty
// result, not a thrown error, so a caller iterating this like
// `handleMailPersist` does simply emits no `mail.persisted` events.
return [] as unknown as R;
}
return upstream(args);
};
}

export function createHubMailboxAuthorizeSender(
db: DB["db"],
): AuthorizeMailboxSender {
Expand Down
37 changes: 13 additions & 24 deletions packages/agent-directory/src/one-shot-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
endAgentSessionForRun,
readDefinitionProjection,
readFoldedBody,
recordAgentSessionForRun,
recordAgentSessionAtProvision,
WORKFLOW_SOURCE_ENTRY,
type EventCollectorPort,
} from "@corbits/workflows";
Expand Down Expand Up @@ -166,10 +166,13 @@ async function provisionOnAsset(
? { toolPackagePins: input.foldedBody.toolPackagePins }
: {}),
});
// Not `recordAgentSessionForRun` 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.
await recordAgentSessionAtProvision({
db: deps.db,
eventCollectors: deps.eventCollectors,
runId: prepared.anchorRunId,
sessionId,
sourceAuthorityPrincipalId: input.principalId,
});
return {
runId: prepared.anchorRunId,
address: prepared.deploymentAddress,
Expand Down Expand Up @@ -322,25 +325,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,
},
deps.eventCollectors,
);
} 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
34 changes: 28 additions & 6 deletions packages/chat/src/platform-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
authoredDefinitionCandidates,
DefinitionProjectionMissingError,
endAgentSessionForRun,
ensureRunSession,
readFoldedBody,
recordAgentSessionAtProvision,
resolveNewestProjectedDefinition,
WORKFLOW_SOURCE_ENTRY,
} from "@corbits/workflows";
Expand Down Expand Up @@ -258,7 +260,6 @@ export function createHubChatPlatform(
db: deps.db,
sessionService: deps.sessionService,
sidecarRouter: deps.sidecarRouter,
eventCollectors: deps.eventCollectors,
};

function offeringDigest(sourceOfferingIds: readonly string[]): string {
Expand Down Expand Up @@ -344,11 +345,13 @@ export function createHubChatPlatform(
? { toolPackagePins: input.foldedBody.toolPackagePins }
: {}),
});
// Not `recordAgentSessionForRun` 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.
await recordAgentSessionAtProvision({
db: deps.db,
eventCollectors: deps.eventCollectors,
runId: prepared.anchorRunId,
sessionId,
sourceAuthorityPrincipalId: input.sourceAuthorityPrincipalId,
});
return {
runId: prepared.anchorRunId,
address: prepared.deploymentAddress,
Expand Down Expand Up @@ -1313,6 +1316,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
Loading
Loading