Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion VENDORED.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ never a convenience.
| Vendored path | What was copied | Upstream repo @ commit | Why not a published package | Owner | Kill date | Kill-date test |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | ---------- | ----------------- |
| `apps/sidecar` | Derived from upstream's own `apps/sidecar`: 11 shared modules, of which `signing-keypair.ts` is near-verbatim and the rest (`index.ts`, `config.ts`, `tool-materialization.ts`, `workflow-run-pack-client.ts`, …) are substantially rewritten, plus workbench-only modules. A living fork, not a frozen copy, so this row carries no tree hash. | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | An app is never npm-published, so no publish can cover the execution host; retired by consuming an upstream-published host, or by renewing this row deliberately | sawyer | 2026-09-19 | `check:killdates` |
| `vendor/intx/db` | `@intx/db` source (`src/`, `migrations/`, drizzle config, manifest, tsconfigs) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the `wire_projection` column/loader delta (CL-6324); retired when upstream absorbs the delta | sawyer | 2026-09-19 | `check:killdates` |
| `vendor/intx/db` | `@intx/db` source (`src/`, `migrations/`, drizzle config, manifest, tsconfigs) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the `wire_projection` column/loader delta (CL-6324) or the `workflow_definition.origin` column separating a definition from the per-run record of one folded run's deploy (CL-6452); retired when upstream absorbs the deltas | sawyer | 2026-09-19 | `check:killdates` |
| `vendor/intx/hub-api` | `@intx/hub-api` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the `needs-you` approval-route reservation or the exported null-principal `resolveApproval` (CL-6345); retired when upstream absorbs the deltas | sawyer | 2026-09-19 | `check:killdates` |
| `vendor/intx/hub-sessions` | `@intx/hub-sessions` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the usage forward (CL-5879), pack-acceptance fixes, adopted deploy front, wire-projection writer, event-collector serialization, or anchor ordering | sawyer | 2026-09-19 | `check:killdates` |
| `vendor/intx/workflow` | `@intx/workflow` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the `onBodyFailure` trigger policy and its projection (CL-6326, CL-6324); retired when upstream absorbs the delta | sawyer | 2026-09-19 | `check:killdates` |
Expand Down
97 changes: 64 additions & 33 deletions packages/chat/src/platform-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
import { and, desc, eq } from "drizzle-orm";
import { createAgentLifecycle } from "@corbits/agent-lifecycle";
import {
authoredDefinitionCandidates,
createCryptoProviderCache,
DefinitionProjectionMissingError,
domainOf,
launchFoldedRun,
mintFoldedRun,
readDefinitionProjection,
readFoldedBody,
resolveFoldedRunSessionId,
resolveNewestProjectedDefinition,
Expand Down Expand Up @@ -502,6 +503,48 @@ export function createHubChatPlatform(
}
}

// CL-6452: every run deploy ensures a same-named sibling definition
// over the agent's asset under its per-run wire hash — a frozen
// deploy record carrying whatever projection was current at that
// deploy. Launch bodies resolve only from the hub-authored row(s) of
// the asset, so a skill pin or instructions save (which refreezes
// the authored row in place) reaches every later launch instead of
// being shadowed by the newest clone. Raises the named
// `DefinitionProjectionMissingError` — mapped to a 4xx at the route
// boundary, never an unhandled 500 — when the asset has no authored
// definition or none of its authored rows carries a projection.
async function resolveAuthoredProjectedDefinition(
tenantId: string,
definitionAsset: { assetId: string; name: string },
) {
const assetSiblingRows = await deps.db.query.workflowDefinition.findMany({
where: and(
eq(workflowDefinition.tenantId, tenantId),
eq(workflowDefinition.assetId, definitionAsset.assetId),
eq(workflowDefinition.status, "deployed"),
),
orderBy: desc(workflowDefinition.createdAt),
});
const candidates = authoredDefinitionCandidates(assetSiblingRows);
if (candidates.length === 0) {
throw new DefinitionProjectionMissingError(definitionAsset.name);
}
const resolved = await resolveNewestProjectedDefinition(
deps.db,
candidates,
);
const row = candidates.find(
(candidate) => candidate.id === resolved.definitionId,
);
if (row === undefined) {
throw new Error(
`resolved definition "${resolved.definitionId}" is not among the ` +
`authored candidates for asset "${definitionAsset.assetId}"`,
);
}
return { row, projection: resolved.projection };
}

const platform: ChatPlatform = {
async launchInvite(input): Promise<LaunchedInvite> {
const definitionRow = await deps.db.query.workflowDefinition.findFirst({
Expand Down Expand Up @@ -534,37 +577,14 @@ export function createHubChatPlatform(
throw new Error(`No tenant "${input.tenantId}"`);
}

// CL-6357: a long-lived DB can carry a definition row whose asset
// repo has gone unresolvable (DB/blob drift — a `.data` reset
// that never touched Postgres) while a newer, healthy sibling
// under the same name already exists (a re-seed, typically).
// Resolution tries every deployed sibling under this name
// newest-first and uses the first one that actually reads — the
// specifically requested (possibly stale) row never wins over a
// healthy newer one. `resolveNewestProjectedDefinition`
// raises the named `DefinitionProjectionMissingError` — mapped
// to a 4xx at the route boundary, never an unhandled 500 — only
// once every sibling has failed to resolve.
const siblingRows = await deps.db.query.workflowDefinition.findMany({
where: and(
eq(workflowDefinition.tenantId, input.tenantId),
eq(workflowDefinition.name, definitionRow.name),
eq(workflowDefinition.status, "deployed"),
),
orderBy: desc(workflowDefinition.createdAt),
});
const candidates = siblingRows.length > 0 ? siblingRows : [definitionRow];

const resolved = await resolveNewestProjectedDefinition(
deps.db,
candidates,
);
const resolvedDefinitionRow =
candidates.find((row) => row.id === resolved.definitionId) ??
definitionRow;
const { row: resolvedDefinitionRow, projection } =
await resolveAuthoredProjectedDefinition(input.tenantId, {
assetId: definitionRow.assetId,
name: definitionRow.name,
});

const foldedBody = readFoldedBody(
resolved.projection,
projection,
resolvedDefinitionRow.grantRequirements,
);
if (foldedBody.systemPrompt === "") {
Expand Down Expand Up @@ -619,7 +639,10 @@ export function createHubChatPlatform(
),
orderBy: desc(workflowDefinition.createdAt),
});
return rows
// Only hub-authored definitions are invitable: the run-deploy
// clones sharing an agent's name are deploy records, and listing
// them would offer N stale copies of every agent that has run.
return authoredDefinitionCandidates(rows)
.filter((row) => !isWorkbenchHostDefinitionName(row.name))
.map((row) => {
const base = { id: row.id, name: row.name };
Expand Down Expand Up @@ -664,10 +687,18 @@ export function createHubChatPlatform(
return;
}

const projection = await readDefinitionProjection(deps.db, definitionRow);
// The run's own definition row is the per-run clone the deploy
// repointed it to; the refresh recomputes from the hub-authored
// sibling so the saved edit — not the clone's frozen snapshot —
// is what the next wake replays.
const { row: authoredRow, projection } =
await resolveAuthoredProjectedDefinition(tenantId, {
assetId: definitionRow.assetId,
name: definitionRow.name,
});
const foldedBody = readFoldedBody(
projection,
definitionRow.grantRequirements,
authoredRow.grantRequirements,
);

await deps.db
Expand Down
Loading
Loading