Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 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
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
2 changes: 2 additions & 0 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2272,6 +2272,7 @@ export async function createHub(config: HubConfig) {
isRoutable: (address) =>
sidecarRouter.getRoutableAddresses().includes(address),
cryptoProviderCache: cryptoProviders,
eventCollectors,
persistLaunch: async (input) => {
await workbenchLaunchPersistExtra(input)(db);
},
Expand Down Expand Up @@ -3069,6 +3070,7 @@ export async function createHub(config: HubConfig) {
repoStore: agentRepoStore.repoStore,
workflowAllocationService,
sessionService,
eventCollectors,
},
runnerInput,
),
Expand Down
20 changes: 15 additions & 5 deletions packages/agent-directory/src/one-shot-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
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 +58,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 @@ -267,7 +269,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 @@ -321,10 +327,14 @@ export async function runOneShotPrompt(
// 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,
});
await recordAgentSessionForRun(
deps.db,
{
sessionId: launched.sessionId,
anchorRunId: launched.runId,
},
deps.eventCollectors,
);
} catch (err) {
reportError(err, {
operation: "agent-directory.one-shot.recordAgentSession",
Expand Down
7 changes: 3 additions & 4 deletions packages/chat/src/platform-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { and, desc, eq } from "drizzle-orm";
import {
authoredDefinitionCandidates,
DefinitionProjectionMissingError,
endAgentSessionForPrincipal,
endAgentSessionForRun,
readFoldedBody,
resolveNewestProjectedDefinition,
WORKFLOW_SOURCE_ENTRY,
Expand Down Expand Up @@ -258,6 +258,7 @@ 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 @@ -476,9 +477,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
18 changes: 13 additions & 5 deletions packages/chat/src/send-run-mail.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// 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 {
recordAgentSessionForRun,
type EventCollectorPort,
} from "@corbits/workflows";
import { reportError } from "@corbits/error-sink";
import { sessionMail } from "@intx/db/schema";
import type { DB } from "@intx/db";
Expand All @@ -11,6 +14,7 @@ export type RunMailDeps = {
db: DB["db"];
sessionService: Pick<SessionService, "sendUserMessage">;
sidecarRouter: Pick<SidecarRouter, "dispatchAgentEvent">;
eventCollectors: Pick<EventCollectorPort, "create">;
};

export type SendRunMailParams = {
Expand Down Expand Up @@ -109,10 +113,14 @@ export async function sendRunMail(
// 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,
});
await recordAgentSessionForRun(
deps.db,
{
sessionId: params.sessionId,
address: params.agentAddress,
},
deps.eventCollectors,
);
} catch (err) {
reportError(err, {
operation: "chat.sendRunMail.recordAgentSession",
Expand Down
14 changes: 10 additions & 4 deletions packages/webhook-triggers/src/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
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 @@ -58,6 +59,7 @@ export type LaunchWebhookTriggerDeps = {
*/
isRoutable: (address: string) => boolean;
cryptoProviderCache: CryptoProviderCache;
eventCollectors: Pick<EventCollectorPort, "create">;
/**
* Host-supplied cipher. Interchange decrypts bindings inside
* `prepareProvisionedDeployment`; this port still owns the field so
Expand Down Expand Up @@ -245,10 +247,14 @@ export async function launchWebhookTrigger(
// a run that never became routable stays without a session until its
// first successful send.
try {
await recordAgentSessionForRun(deps.db, {
sessionId,
anchorRunId: prepared.anchorRunId,
});
await recordAgentSessionForRun(
deps.db,
{
sessionId,
anchorRunId: prepared.anchorRunId,
},
deps.eventCollectors,
);
} catch (error) {
reportError(error, {
operation: "webhookTriggers.launch.recordAgentSession",
Expand Down
3 changes: 3 additions & 0 deletions packages/webhook-triggers/test/launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ function baseDeps() {
},
},
isRoutable: () => isRoutableForTest,
eventCollectors: {
create: () => undefined,
},
};
}

Expand Down
1 change: 1 addition & 0 deletions packages/workflows/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export {
endAgentSessionForPrincipal,
endAgentSessionForRun,
type RecordAgentSessionParams,
type EventCollectorPort,
} from "./launch/agent-session";
export {
deliverWhenRoutable,
Expand Down
45 changes: 41 additions & 4 deletions packages/workflows/src/launch/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,27 @@
import { eq } from "drizzle-orm";
import type { DB } from "@intx/db";
import { agentSession, workflowRun } from "@intx/db/schema";
import type { EventCollectorRegistry } from "@intx/hub-sessions";

export type RecordAgentSessionParams = {
/** The id `prepareProvisionedDeployment` was called with. */
readonly sessionId: string;
} & ({ readonly anchorRunId: string } | { readonly address: string });

/**
* The one port every launcher threads the same wrapped
* `EventCollectorRegistry` through (`apps/hub/src/index.ts`'s
* `eventCollectors`) — never a second registry construction. `create`
* is what actually makes `inference_turn`/`turn_part` rows exist for a
* run (CL-7479: nothing else ever called it once `folded-runs` was
* deleted); `abandon` tears the collector down when a run's session
* ends.
*/
export type EventCollectorPort = Pick<
EventCollectorRegistry,
"create" | "abandon"
>;

/**
* Inserts the `agent_session` row a run needs to be mail-routable: the
* run's `tenantId`, `definitionId` (the `agent_id` FK — the column keeps
Expand All @@ -41,6 +56,7 @@ export type RecordAgentSessionParams = {
export async function recordAgentSessionForRun(
db: DB["db"],
params: RecordAgentSessionParams,
eventCollectors: Pick<EventCollectorPort, "create">,
): Promise<boolean> {
const runRow = await db.query.workflowRun.findFirst({
where:
Expand All @@ -65,6 +81,19 @@ export async function recordAgentSessionForRun(
updatedAt: now,
})
.onConflictDoNothing({ target: agentSession.id });

const agentAddress = "address" in params ? params.address : runRow.address;
if (agentAddress !== null) {
// Idempotent across wakes: a collector already live for this
// address is replaced, matching the registry's own contract
// (`event-collector-registry.ts`'s `create`).
eventCollectors.create(
agentAddress,
runRow.tenantId,
params.sessionId,
runRow.id,
);
}
return true;
}

Expand All @@ -87,14 +116,19 @@ export async function endAgentSessionForPrincipal(

/**
* Same as `endAgentSessionForPrincipal`, keyed by the run's own id
* instead — for a caller (e.g. a one-shot prompt's teardown) that only
* ever held the run id, never minted or looked up its principal.
* No-ops for a run row already gone, or one never triggered (no
* principal, hence no session was ever recorded for it).
* instead — for a caller (e.g. a one-shot prompt's teardown, or a chat
* relaunch replacing a terminal run) that only ever held the run id,
* never minted or looked up its principal. Also abandons the outgoing
* run's event collector, through the same shared
* `EventCollectorPort` every launcher threads — one mechanism for
* ending a run's session. No-ops for a run row already gone, or one
* never triggered (no principal, hence no session was ever recorded
* for it).
*/
export async function endAgentSessionForRun(
db: DB["db"],
anchorRunId: string,
eventCollectors: Pick<EventCollectorPort, "abandon">,
): Promise<void> {
const runRow = await db.query.workflowRun.findFirst({
where: eq(workflowRun.id, anchorRunId),
Expand All @@ -103,4 +137,7 @@ export async function endAgentSessionForRun(
return;
}
await endAgentSessionForPrincipal(db, runRow.principalId);
if (runRow.address !== null) {
eventCollectors.abandon(runRow.address);
}
}
53 changes: 51 additions & 2 deletions packages/workflows/test/agent-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import { generateId } from "@intx/hub-common";
import { resolveRunSessionId } from "@intx/hub-sessions";
import { dbGate } from "../../../scripts/e2e/db-gate";

import { recordAgentSessionForRun } from "../src/launch/agent-session";
import {
endAgentSessionForRun,
recordAgentSessionForRun,
} from "../src/launch/agent-session";

function dbConfigFromUrl(databaseUrl: string) {
const url = new URL(databaseUrl);
Expand Down Expand Up @@ -98,9 +101,55 @@ describeIfDb("recordAgentSessionForRun", () => {
});

test("writes an agent_session resolveRunSessionId reads back", async () => {
await recordAgentSessionForRun(db.db, { sessionId, anchorRunId });
await recordAgentSessionForRun(
db.db,
{ sessionId, anchorRunId },
{ create: () => undefined },
);

const resolved = await resolveRunSessionId(db.db, principalId);
expect(resolved).toBe(sessionId);
});

test("creates the run's event collector with its address/tenant/session/run id", async () => {
const createCalls: unknown[] = [];
const otherSessionId = generateId("session");

await recordAgentSessionForRun(
db.db,
{ sessionId: otherSessionId, anchorRunId },
{
create: (...args: unknown[]) => {
createCalls.push(args);
},
},
);

expect(createCalls).toEqual([
[
`${anchorRunId}@agent-session-${tenantId}.localhost`,
tenantId,
otherSessionId,
anchorRunId,
],
]);

await db.db
.delete(schema.agentSession)
.where(eq(schema.agentSession.id, otherSessionId));
});

test("abandons the run's event collector when the session ends", async () => {
const abandonCalls: string[] = [];

await endAgentSessionForRun(db.db, anchorRunId, {
abandon: (address: string) => {
abandonCalls.push(address);
},
});

expect(abandonCalls).toEqual([
`${anchorRunId}@agent-session-${tenantId}.localhost`,
]);
});
});
Loading