From 9f07255ba01d02f5e9853166a5a59a4802413085 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 16 Sep 2026 22:30:16 -0700 Subject: [PATCH 1/4] feat(web): read top-level runs from native GET /workflows/runs (CL-8087) --- apps/web/src/agents-api.ts | 57 +------ apps/web/src/agents-directory.ts | 4 +- apps/web/src/insights-api.test.ts | 8 +- apps/web/src/insights-api.ts | 48 +++--- apps/web/src/insights-stats.test.ts | 27 ++++ apps/web/src/insights-stats.ts | 32 ++-- apps/web/src/pages/mission-control-page.tsx | 2 +- apps/web/src/shell/bench-activity.ts | 6 +- apps/web/src/shell/routine-activity.ts | 70 ++------- apps/web/test/agents-api.test.ts | 47 ++---- apps/web/test/bench-activity.test.tsx | 52 ++----- apps/web/test/fetch-dedupe.test.tsx | 10 +- apps/web/test/insights-route.test.tsx | 51 ++++-- apps/web/test/routine-activity.test.ts | 147 +++--------------- apps/web/test/sidebar.test.tsx | 8 - apps/web/test/workbench-list.test.tsx | 2 - packages/agent-directory/README.md | 4 +- .../test/reconnect.drizzle.test.ts | 3 +- 18 files changed, 195 insertions(+), 383 deletions(-) diff --git a/apps/web/src/agents-api.ts b/apps/web/src/agents-api.ts index a7966772df..9502de3565 100644 --- a/apps/web/src/agents-api.ts +++ b/apps/web/src/agents-api.ts @@ -31,25 +31,8 @@ export type AgentDefinition = typeof WorkflowDefinitionResponse.infer; export type AgentInstance = typeof WorkflowRunResponse.infer; export type CatalogModel = typeof ModelResponse.infer; -const RunFireResponse = WorkflowRunResponse.and( - type({ - routineId: "string | null", - routineName: "string | null", - "hasInFlightTurn?": "boolean", - "turns?": type({ - status: "string", - "endedAt?": "string | null", - }).array(), - }), -); -/** A `feed=fires` row: every `AgentInstance` field plus the routine that - * fired it (both `null` for a directly-triggered deployment with no - * routine parent). */ -export type RunFire = typeof RunFireResponse.infer; - const DefinitionsPage = paginatedSchema(WorkflowDefinitionResponse); const InstancesPage = paginatedSchema(WorkflowRunResponse); -const RunFiresPage = paginatedSchema(RunFireResponse); const ModelsPage = paginatedSchema(ModelResponse); // The REST pagination ceiling (see `vendor/intx/hub-api/src/pagination.ts`). @@ -153,44 +136,20 @@ export function listAgentInstances( /** * The tenant's genuine top-level deployment runs — every non-top-level - * run (workbench host, invited agent, routine fire, task) excluded - * server-side by the same predicate `isTopLevelRun` uses (see - * `@corbits/run-scope`'s `scope-routes.ts`), not derived client-side - * from a tenant's workbenches the way `foldedRunIdsFromWorkbenches` used - * to. Used wherever a page needs "real deployments only" — the Agent - * Directory. A routine fire is NOT top-level, so this feed structurally - * never carries one; a caller that needs routine activity wants - * `listRoutineRunFires` below instead. + * run (workbench host, invited agent, task) excluded server-side by the + * native `GET /workflows/runs` listing's own predicate (`address IS NOT + * NULL AND anchorRunId = id`), the same one `isTopLevelRun` uses. Used + * wherever a page needs "real deployments only" — the Agent Directory. */ export function listTopLevelRuns( tenantId: string, ): Promise { return getJSON( - `/api/tenants/${tenantId}/top-level-runs?limit=${PAGE_LIMIT}`, + `/api/tenants/${tenantId}/workflows/runs?limit=${PAGE_LIMIT}`, InstancesPage, ).then((page) => page.data); } -/** - * `feed=fires` (CL-6249): the tenant's genuine *executed* runs — unlike - * `listTopLevelRuns`, a routine's fire is kept even though it is not a - * top-level run, tagged with the routine that fired it (see - * `@corbits/run-scope`'s `scope-routes.ts`'s `listTopLevelRunFires`). - * The shell's "Running" activity band (CL-6595) reads this, not - * `listTopLevelRuns`, so a routine's own run is actually visible here — - * `listTopLevelRuns`'s top-level-only filter drops every routine fire by - * construction, which left Mission Control's active-run count - * permanently desynced from the Routines page's own "Running now" pill. - */ -export function listRoutineRunFires( - tenantId: string, -): Promise { - return getJSON( - `/api/tenants/${tenantId}/top-level-runs?limit=${PAGE_LIMIT}&feed=fires`, - RunFiresPage, - ).then((page) => page.data); -} - /** The tenant's visible, enabled catalog models for the create-agent form's * model picker. Uses `/catalog/models` (paginated `ModelResponse`), not the * bare-array discovery route at `/models` (`ModelInfo[]`) — those are @@ -467,9 +426,9 @@ type SkillsOutcome = * so either failing alone never blanks the page. Failures surface as * `modelsError` / `skillsError` rather than silent empty collections. * `instances` comes from `listTopLevelRuns`, which already excludes every - * non-top-level run (workbench host, invited agent) server-side — see - * `@corbits/run-scope`'s `scope-routes.ts` — so this page never has to - * derive that exclusion itself from a tenant's workbenches. + * non-top-level run (workbench host, invited agent) server-side — the + * native `GET /workflows/runs` listing's own predicate — so this page + * never has to derive that exclusion itself from a tenant's workbenches. */ export async function loadAgentDirectory( tenantId: string, diff --git a/apps/web/src/agents-directory.ts b/apps/web/src/agents-directory.ts index 7eb2715436..768f56253c 100644 --- a/apps/web/src/agents-directory.ts +++ b/apps/web/src/agents-directory.ts @@ -41,8 +41,8 @@ export function purposeAgentDefinitions( /** * `instances` is expected to already come from `listTopLevelRuns` * (see `./agents-api.ts`), which excludes every non-top-level run - * (workbench host, invited agent, task) server-side — see - * `@corbits/run-scope`'s `scope-routes.ts`. This still applies the + * (workbench host, invited agent, task) server-side — the native + * `GET /workflows/runs` listing's own predicate. This still applies the * shared name-based workbench-host filter as defense in depth. */ export function purposeAgentInstances( diff --git a/apps/web/src/insights-api.test.ts b/apps/web/src/insights-api.test.ts index b07646bede..5905d3622e 100644 --- a/apps/web/src/insights-api.test.ts +++ b/apps/web/src/insights-api.test.ts @@ -41,9 +41,13 @@ describe("insights path range contract", () => { }); describe("insightsTopLevelRunsPath", () => { - test("hits the tenant-scoped top-level-runs route, not /me/workflows/runs", () => { + // CL-8087: the native tenant-scoped top-level listing, not the deleted + // `/top-level-runs` route (or its `feed=fires` variant) and not the dead + // `/me/workflows/runs`. + test("hits the native tenant-scoped GET /workflows/runs listing", () => { const path = insightsTopLevelRunsPath("tenant-1"); - expect(path.startsWith("/api/tenants/tenant-1/top-level-runs?")).toBe(true); + expect(path.startsWith("/api/tenants/tenant-1/workflows/runs?")).toBe(true); + expect(path).not.toContain("/top-level-runs"); expect(path).not.toContain("/me/workflows/runs"); }); }); diff --git a/apps/web/src/insights-api.ts b/apps/web/src/insights-api.ts index 26ddf88d26..aab1503478 100644 --- a/apps/web/src/insights-api.ts +++ b/apps/web/src/insights-api.ts @@ -12,9 +12,10 @@ // workbenches. usage/activity/tools roll up automatically when called // with a parent's tenantId (see @corbits/insights' resolveScope) — // /scope is how a page discovers that shape to build a switcher. -// Plus one more reused as Insights' run feed: GET /top-level-runs?feed=fires -// → Paginated -// (packages/run-scope/src/scope-routes.ts's `listTopLevelRunFires`). +// Plus one more reused as Insights' run feed: the native tenant-scoped +// `GET /workflows/runs` top-level listing → Paginated +// (CL-8087 deleted `@corbits/run-scope`'s `/top-level-runs?feed=fires`, +// which used to attribute each fire to its routine server-side). import { type } from "arktype"; import { WorkflowRunResponse, paginatedSchema } from "@intx/types"; @@ -230,11 +231,14 @@ export const ListingTurnSchema = type({ }); /** - * `WorkflowRunResponse` plus the two fields only the `feed=fires` mode of - * `/top-level-runs` reports (CL-6249): the routine that fired this run, - * when it has one. Both are `null` for a run with no routine/task - * parent — a directly launched workflow — so a caller falls back to - * `definitionName` honestly instead of inventing a routine. + * `WorkflowRunResponse` plus the two routine-attribution fields the deleted + * `feed=fires` mode of `/top-level-runs` used to report (CL-6249): the + * routine that fired this run, when it has one. The native + * `GET /workflows/runs` listing has no routine attribution, so both are + * absent there — callers fall back to `definitionId`/`definitionName` + * (`groupRunsByDefinition`, `runDisplayName` in `./insights-stats.ts`) + * instead of inventing a routine. Both stay `null` (when present) for a + * run with no routine/task parent — a directly launched workflow. * `turns` / `hasInFlightTurn` are this build's listing of in-flight * inference turns for the run (not Interchange fields) so a live * tool-loop can stay running past the abandoned-fire window. Omitting @@ -242,15 +246,15 @@ export const ListingTurnSchema = type({ */ export const InsightsRunSchema = WorkflowRunResponse.and( type({ - routineId: "string | null", - routineName: "string | null", + "routineId?": "string | null", + "routineName?": "string | null", "hasInFlightTurn?": "boolean", "turns?": ListingTurnSchema.array(), }), ); export type InsightsRun = typeof InsightsRunSchema.infer; -/** GET /top-level-runs?feed=fires envelope. */ +/** Native `GET /workflows/runs` envelope (CL-8087). */ export const TopLevelRunsSchema = paginatedSchema(InsightsRunSchema); // The REST pagination ceiling (see `vendor/intx/hub-api/src/pagination.ts`) — @@ -258,16 +262,22 @@ export const TopLevelRunsSchema = paginatedSchema(InsightsRunSchema); const TOP_LEVEL_RUNS_LIMIT = 100; /** - * Insights' run feed (CL-6062, `feed=fires` added by CL-6249): the - * tenant's genuine *executed* runs — a routine's fire (not a top-level - * run though it is) included, and the resident, never-triggered deployment - * placeholder for a definition (`status: "deployed"` forever) excluded — - * both decided server-side by `@corbits/run-scope`'s - * `scope-routes.ts`'s `listTopLevelRunFires`, never by a definitionName - * slug guess here. Used in place of the dead `/me/workflows/runs` — its + * Insights' run feed (CL-6062; repointed to native in CL-8087): the + * tenant's top-level runs from `GET /workflows/runs` — the native + * listing's own predicate (`address IS NOT NULL AND anchorRunId = id`, + * see `vendor/intx/hub-api/src/routes/runs.ts`) already excludes every + * non-top-level run (workbench host, invited agent), so this page never + * derives that exclusion itself. Three differences from the deleted + * `feed=fires` feed, all recorded as accepted loss in CL-8087: a + * routine's fire (not a top-level run) is no longer included, so + * Insights no longer sees routine executions; there is no routine + * attribution, so history groups by definition; and the resident, + * never-triggered deployment placeholder (`status: "deployed"`) is now + * included — `computeInsightsStats` counts it as deployed rather than + * hiding it. Used in place of the dead `/me/workflows/runs` — its * `anchorRunId IS NULL` filter never matches, because every addressed run * self-anchors at creation, so that feed always came back empty. */ export function insightsTopLevelRunsPath(tenantId: string): string { - return `/api/tenants/${tenantId}/top-level-runs?limit=${TOP_LEVEL_RUNS_LIMIT}&feed=fires`; + return `/api/tenants/${tenantId}/workflows/runs?limit=${TOP_LEVEL_RUNS_LIMIT}`; } diff --git a/apps/web/src/insights-stats.test.ts b/apps/web/src/insights-stats.test.ts index 1d0c9a7bf0..efc0459557 100644 --- a/apps/web/src/insights-stats.test.ts +++ b/apps/web/src/insights-stats.test.ts @@ -229,6 +229,22 @@ describe("groupRunsByDefinition", () => { expect(groupRunsByDefinition([])).toEqual([]); }); + test("groups native rows with no routine attribution by definition (CL-8087)", () => { + // The native `GET /workflows/runs` listing carries no `routineId`, so + // history groups by definition — never by an invented routine. + const native = run({ + id: "n1", + status: "running", + definitionId: "wfd_a", + definitionName: "Research brief", + }); + delete native.routineId; + delete native.routineName; + const groups = groupRunsByDefinition([native]); + expect(groups.map((g) => g.groupKey)).toEqual(["wfd_a"]); + expect(groups[0]?.displayName).toBe("Research brief"); + }); + test("uses the newest run's name, not input-array-first, when a definition was renamed", () => { // Old run (chronologically oldest) appears FIRST in the input array, // simulating an unsorted/out-of-order feed. Newer run (renamed) is second. @@ -314,6 +330,17 @@ describe("runDisplayName", () => { ), ).toBe("researcher"); }); + + test("falls back to the definition name for a native row with no routine attribution (CL-8087)", () => { + const native = run({ + id: "native1", + status: "running", + definitionName: "researcher", + }); + delete native.routineId; + delete native.routineName; + expect(runDisplayName(native)).toBe("researcher"); + }); }); describe("filterRunsByCreatedAt", () => { diff --git a/apps/web/src/insights-stats.ts b/apps/web/src/insights-stats.ts index 148529065e..37d067491b 100644 --- a/apps/web/src/insights-stats.ts +++ b/apps/web/src/insights-stats.ts @@ -60,16 +60,16 @@ export const INSIGHTS_RECENT_LIMIT = 12; /** * Purpose runs only — drop workbench-host anchors the same way Home does. * `insights-page.tsx` sources `runs` from `insightsTopLevelRunsPath` (see - * `./insights-api.ts`), which already excludes every non-top-level run - * (workbench host, invited agent) with no routine parent, and the - * resident never-fired deployment placeholder, server-side via - * `@corbits/run-scope`'s `scope-routes.ts`'s `listTopLevelRunFires`. This - * filter is a client-side belt-and-suspenders pass against the - * workbench-host naming pattern alone, not a second scoping layer — a - * caller no longer needs to (and cannot) hand this a non-top-level run - * id set. CL-6062 replaced the dead `/me/workflows/runs` feed (its - * `anchorRunId IS NULL` filter never matched anything, since every - * addressed run self-anchors at creation) with this scoped one. + * `./insights-api.ts`), the native `GET /workflows/runs` top-level + * listing (CL-8087) whose own predicate already excludes every + * non-top-level run (workbench host, invited agent). This filter is a + * client-side belt-and-suspenders pass against the workbench-host naming + * pattern alone, not a second scoping layer — a caller no longer needs + * to (and cannot) hand this a non-top-level run id set. CL-6062 replaced + * the dead `/me/workflows/runs` feed (its `anchorRunId IS NULL` filter + * never matched anything, since every addressed run self-anchors at + * creation) with a scoped feed; CL-8087 repointed that scoped feed from + * the deleted `feed=fires` route to the native listing. */ export function purposeRunsForInsights( runs: readonly InsightsRun[], @@ -80,12 +80,12 @@ export function purposeRunsForInsights( } /** - * A run's human-facing name (CL-6249): its routine's name when it fired - * from one, honestly falling back to the definition name for a run with - * no routine/task parent — e.g. a directly launched workflow. Never - * mapped from a client-side lookup table; the route already resolved - * `routineName` server-side (`@corbits/run-scope`'s - * `listTopLevelRunFires`). + * A run's human-facing name (CL-6249): its routine's name when the feed + * attributed one, honestly falling back to the definition name for a run + * with no routine attribution — e.g. every row of the native + * `GET /workflows/runs` listing (CL-8087), which carries no + * `routineName`, or a directly launched workflow. Never mapped from a + * client-side lookup table. */ export function runDisplayName(run: InsightsRun): string { return run.routineName ?? run.definitionName; diff --git a/apps/web/src/pages/mission-control-page.tsx b/apps/web/src/pages/mission-control-page.tsx index 638e567168..e66599ebfb 100644 --- a/apps/web/src/pages/mission-control-page.tsx +++ b/apps/web/src/pages/mission-control-page.tsx @@ -419,7 +419,7 @@ export function MissionControlRoute({ inFlightRows.length === 0 ? ( ) : null} {inFlightRows.length > 0 ? ( diff --git a/apps/web/src/shell/bench-activity.ts b/apps/web/src/shell/bench-activity.ts index e25956a14c..56bb8f5e1b 100644 --- a/apps/web/src/shell/bench-activity.ts +++ b/apps/web/src/shell/bench-activity.ts @@ -83,7 +83,11 @@ export function useBenchActivity(tenantId: string | null): BenchActivityQuery { const routinesQuery = useQuery({ queryKey: tenantKeys.topLevelRuns(key), enabled, - queryFn: () => listRoutineActivity(key), + // CL-8087: `listRoutineActivity` resolves no items without fetching + // (the `feed=fires` route is deleted and the native listing has no + // fires equivalent), so this query never fires a request — the + // column keeps the key and renders the seam's honest empty state. + queryFn: () => listRoutineActivity(), }); const agentsQuery = useQuery({ queryKey: tenantKeys.visibleAgents(key), diff --git a/apps/web/src/shell/routine-activity.ts b/apps/web/src/shell/routine-activity.ts index fb018ea1b5..41b73017a5 100644 --- a/apps/web/src/shell/routine-activity.ts +++ b/apps/web/src/shell/routine-activity.ts @@ -1,24 +1,19 @@ -// The second column's "Running" section and Mission Control's active-run -// count (CL-6595) both depend only on `RoutineActivityItem` and -// `listRoutineActivity`, never on where the data actually comes from. -// Filled from `./agents-api.ts`'s `listRoutineRunFires` — the `feed=fires` -// listing, the one top-level-runs view that keeps a routine's fire despite -// it not being a top-level run (see that function's own comment). The -// plain `listTopLevelRuns` feed looks tempting here but is wrong: its -// top-level-only filter drops every routine fire by construction, -// so a routine genuinely running would never show up in this band or count -// toward Mission Control's "Active runs" — exactly CL-6595's desync -// between the Routines page's own "Running now" pill and Mission Control's -// "0 / nothing running". -import { - runOutcomeStatus, - withListingAbandoned, -} from "@corbits/workflows/client"; +// CL-8087 deleted `@corbits/run-scope`'s `/top-level-runs?feed=fires` +// route — the only listing that kept a routine's fire despite it not +// being a top-level run — and the native `GET /workflows/runs` listing +// has no equivalent: its top-level-only predicate drops every routine +// fire by construction, and its rows carry no routine attribution to +// compose client-side from (matching fires to routines by definition +// name here would be exactly the slug guess the Insights feed refuses +// to do). So `listRoutineActivity` resolves no items without fetching: +// the shell's "Running" band and Mission Control's active-run count +// (CL-6595) honestly report no routine activity until a native fires +// equivalent exists, instead of deriving routine activity from +// top-level deployment rows that are not routine fires. The +// `RoutineActivityItem` shape is kept so both consumers keep compiling +// against the seam in `./bench-activity.ts`. import type { ListingTurn } from "@corbits/workflows/client"; -import { listRoutineRunFires } from "../agents-api"; -import type { RunFire } from "../agents-api"; - export type RoutineActivityItem = { readonly id: string; readonly name: string; @@ -29,39 +24,6 @@ export type RoutineActivityItem = { readonly turns?: readonly ListingTurn[]; }; -function toRoutineActivityItem(run: RunFire, now: number): RoutineActivityItem { - const listing = withListingAbandoned( - { - createdAt: run.createdAt, - status: run.status, - ...(run.endedAt !== undefined ? { endedAt: run.endedAt } : {}), - ...(run.hasInFlightTurn !== undefined - ? { hasInFlightTurn: run.hasInFlightTurn } - : {}), - ...(run.turns !== undefined ? { turns: run.turns } : {}), - }, - now, - ); - return { - id: run.id, - name: run.routineName ?? run.definitionName, - status: runOutcomeStatus(listing, now) ?? run.status, - startedAt: run.createdAt, - ...(run.endedAt !== undefined ? { endedAt: run.endedAt } : {}), - ...(run.hasInFlightTurn !== undefined - ? { hasInFlightTurn: run.hasInFlightTurn } - : {}), - ...(run.turns !== undefined ? { turns: run.turns } : {}), - }; -} - -export function listRoutineActivity( - tenantId: string, - now: number = Date.now(), -): Promise { - return listRoutineRunFires(tenantId).then((runs) => - runs - .filter((run) => run.routineId !== null) - .map((run) => toRoutineActivityItem(run, now)), - ); +export function listRoutineActivity(): Promise { + return Promise.resolve([]); } diff --git a/apps/web/test/agents-api.test.ts b/apps/web/test/agents-api.test.ts index c69d5d2ee4..c6765f7886 100644 --- a/apps/web/test/agents-api.test.ts +++ b/apps/web/test/agents-api.test.ts @@ -10,7 +10,6 @@ import { draftAgentDefinition, getAgentDefinitionBySlug, listCatalogModels, - listRoutineRunFires, loadAgentDirectory, updateAgentSkills, } from "../src/agents-api"; @@ -149,7 +148,7 @@ describe("loadAgentDirectory", () => { if (path.includes("/workflows/definitions")) { return json({ data: [definitionFixture], nextCursor: null }); } - if (path.includes("/top-level-runs")) { + if (path.includes("/workflows/runs")) { return json({ data: [instanceFixture], nextCursor: null }); } if (path.includes("/catalog/models")) { @@ -170,7 +169,7 @@ describe("loadAgentDirectory", () => { if (path.includes("/workflows/definitions")) { return json({ error: { message: "nope" } }, 500); } - if (path.includes("/top-level-runs")) { + if (path.includes("/workflows/runs")) { return json({ data: [instanceFixture], nextCursor: null }); } if (path.includes("/catalog/models")) { @@ -187,7 +186,7 @@ describe("loadAgentDirectory", () => { if (path.includes("/workflows/definitions")) { return json({ data: [definitionFixture], nextCursor: null }); } - if (path.includes("/top-level-runs")) { + if (path.includes("/workflows/runs")) { return json({ data: [instanceFixture], nextCursor: null }); } if (path.includes("/catalog/models")) { @@ -206,7 +205,7 @@ describe("loadAgentDirectory", () => { if (path.includes("/workflows/definitions")) { return json({ data: [definitionFixture], nextCursor: null }); } - if (path.includes("/top-level-runs")) { + if (path.includes("/workflows/runs")) { return json({ data: [instanceFixture], nextCursor: null }); } if (path.includes("/catalog/models")) { @@ -228,7 +227,7 @@ describe("loadAgentDirectory", () => { if (path.includes("/workflows/definitions")) { return json({ data: [definitionFixture], nextCursor: null }); } - if (path.includes("/top-level-runs")) { + if (path.includes("/workflows/runs")) { return json({ data: [instanceFixture], nextCursor: null }); } if (path.includes("/catalog/models")) { @@ -247,12 +246,15 @@ describe("loadAgentDirectory", () => { expect(directory.skillsError).toMatch(/500|down/i); }); - test("reads instances from the server-scoped top-level-runs endpoint, never /workflows/runs", async () => { + // CL-8087: instances come from the native tenant-scoped + // `GET /workflows/runs` top-level listing — the deleted `/top-level-runs` + // route (and its `feed=fires` variant) no longer exists. + test("reads instances from the native top-level GET /workflows/runs listing", async () => { const calls = stubFetch((path) => { if (path.includes("/workflows/definitions")) { return json({ data: [definitionFixture], nextCursor: null }); } - if (path.includes("/top-level-runs")) { + if (path.includes("/workflows/runs")) { return json({ data: [instanceFixture], nextCursor: null }); } if (path.includes("/catalog/models")) { @@ -263,8 +265,8 @@ describe("loadAgentDirectory", () => { const directory = await loadAgentDirectory("tnt_1"); expect(directory.instances).toEqual([instanceFixture]); - expect(calls.some((c) => c.path.includes("/top-level-runs"))).toBe(true); - expect(calls.some((c) => c.path.includes("/workflows/runs"))).toBe(false); + expect(calls.some((c) => c.path.includes("/workflows/runs"))).toBe(true); + expect(calls.some((c) => c.path.includes("/top-level-runs"))).toBe(false); }); }); @@ -308,31 +310,6 @@ describe("getAgentDefinitionBySlug", () => { }); }); -describe("listRoutineRunFires", () => { - test("requests the fires feed of top-level-runs, not the plain feed", async () => { - const calls = stubFetch(() => - json({ - data: [ - { - ...instanceFixture, - routineId: "rtn_1", - routineName: "Weekly digest", - }, - ], - nextCursor: null, - }), - ); - - const fires = await listRoutineRunFires("tnt_1"); - - expect(calls[0]?.path).toContain("/api/tenants/tnt_1/top-level-runs"); - expect(calls[0]?.path).toContain("feed=fires"); - expect(fires).toEqual([ - { ...instanceFixture, routineId: "rtn_1", routineName: "Weekly digest" }, - ]); - }); -}); - describe("clearAgentModel", () => { test("DELETEs the model capability rather than posting an empty name", async () => { const calls = stubFetch((path) => { diff --git a/apps/web/test/bench-activity.test.tsx b/apps/web/test/bench-activity.test.tsx index 91c810a187..3591e3c3b0 100644 --- a/apps/web/test/bench-activity.test.tsx +++ b/apps/web/test/bench-activity.test.tsx @@ -32,15 +32,12 @@ function stubTenantFetch( data: { readonly workbenches?: readonly unknown[]; readonly chats?: readonly unknown[]; - readonly runs?: readonly unknown[]; readonly agents?: readonly unknown[]; } = {}, ): void { globalThis.fetch = ((input: RequestInfo | URL) => { const path = typeof input === "string" ? input : String(input); calls.push(path); - if (path.includes("/top-level-runs")) - return Promise.resolve(json({ data: data.runs ?? [], nextCursor: null })); if (path.includes("/agent-definitions/visible")) return Promise.resolve(json({ definitions: data.agents ?? [] })); if (path.includes("kind=chat")) @@ -91,7 +88,10 @@ describe("useBenchActivity", () => { container.remove(); }); - test("fetches workbenches, chats, and running routines for the selected bench", async () => { + // CL-8087: the routines query resolves with no items and no fetch — + // the `feed=fires` route is deleted and the native listing has no + // fires equivalent — so the band renders its honest empty state. + test("fetches workbenches, chats, and agents for the selected bench; routines resolve empty", async () => { const calls: string[] = []; stubTenantFetch(calls); const { latest, root, container } = await mountHook("tnt_1"); @@ -107,7 +107,8 @@ describe("useBenchActivity", () => { // subscribes to (see `tenantKeys.workbenches`). expect(calls.some((path) => path.includes("kind=workbench"))).toBe(true); expect(calls.some((path) => path.includes("kind=chat"))).toBe(true); - expect(calls.some((path) => path.includes("/top-level-runs"))).toBe(true); + expect(calls.some((path) => path.includes("/workflows/runs"))).toBe(false); + expect(calls.some((path) => path.includes("/top-level-runs"))).toBe(false); expect( calls.some((path) => path.includes("/agent-definitions/visible")), ).toBe(true); @@ -115,7 +116,7 @@ describe("useBenchActivity", () => { container.remove(); }); - test("splits workbenches by kind and shows only genuine routine fires (CL-6595)", async () => { + test("splits workbenches by kind; routines stay empty without fetching (CL-8087)", async () => { const calls: string[] = []; stubTenantFetch(calls, { workbenches: [ @@ -138,41 +139,6 @@ describe("useBenchActivity", () => { participants: [], }, ], - // The workbench host and the invited agent never appear here: the - // hub's `/top-level-runs?feed=fires` route excludes every - // non-top-level run that isn't a routine fire (see - // `@corbits/run-scope`'s `scope-routes.ts`). A routine's own fire - // is not a top-level run, so it must still show up here - // (`run_routine1`, tagged with its - // `routineId`) -- that is the CL-6595 fix; a directly-triggered - // deployment with no routine parent (`run_deployment1`) is not - // routine activity and must not appear. - runs: [ - { - id: "run_deployment1", - definitionId: "def_researcher", - definitionName: "researcher", - tenantId: "tnt_1", - address: "run_deployment1@tnt1.example", - status: "running", - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", - routineId: null, - routineName: null, - }, - { - id: "run_routine1", - definitionId: "def_digest", - definitionName: "Digest agent", - tenantId: "tnt_1", - address: "run_routine1@tnt1.example", - status: "running", - createdAt: "2026-01-02T00:00:00.000Z", - updatedAt: "2026-01-02T00:00:00.000Z", - routineId: "rtn_1", - routineName: "Weekly digest", - }, - ], }); const { latest, root, container } = await mountHook("tnt_1"); await settle(); @@ -180,8 +146,8 @@ describe("useBenchActivity", () => { if (state.kind !== "ready") throw new Error(`not ready: ${state.kind}`); expect(state.workbenches.map((c) => c.id)).toEqual(["run_host1"]); expect(state.chats.map((c) => c.id)).toEqual(["run_chat1"]); - expect(state.routines.map((r) => r.id)).toEqual(["run_routine1"]); - expect(state.routines.map((r) => r.name)).toEqual(["Weekly digest"]); + expect(state.routines).toEqual([]); + expect(calls.some((path) => path.includes("/top-level-runs"))).toBe(false); root.unmount(); container.remove(); }); diff --git a/apps/web/test/fetch-dedupe.test.tsx b/apps/web/test/fetch-dedupe.test.tsx index d36faf305c..d25bbf920a 100644 --- a/apps/web/test/fetch-dedupe.test.tsx +++ b/apps/web/test/fetch-dedupe.test.tsx @@ -63,8 +63,6 @@ function stubFetch(calls: string[]): void { calls.push(path); if (path.includes("/api/me/principals")) return Promise.resolve(json(membership)); - if (path.includes("/top-level-runs")) - return Promise.resolve(json({ data: [], nextCursor: null })); if (path.includes("/agent-definitions/visible")) return Promise.resolve(json({ definitions: [] })); return Promise.resolve(json({ items: [] })); @@ -112,7 +110,11 @@ describe("shell listing dedupe (CL-6045)", () => { // per (tenant, kind) no matter how many bands subscribe. expect(countsByMatch(calls, (p) => p.includes("kind=workbench"))).toBe(1); expect(countsByMatch(calls, (p) => p.includes("kind=chat"))).toBe(1); - expect(countsByMatch(calls, (p) => p.includes("/top-level-runs"))).toBe(1); + // CL-8087: the routines query resolves with no items and no fetch (the + // `feed=fires` route is deleted and the native listing has no fires + // equivalent), so both mounts share that without any request at all. + expect(countsByMatch(calls, (p) => p.includes("/top-level-runs"))).toBe(0); + expect(countsByMatch(calls, (p) => p.includes("/workflows/runs"))).toBe(0); }); test("a rename invalidates the shared listing query, triggering exactly one refetch", async () => { @@ -129,8 +131,6 @@ describe("shell listing dedupe (CL-6045)", () => { calls.push(path); if (path.includes("/api/me/principals")) return Promise.resolve(json(membership)); - if (path.includes("/top-level-runs")) - return Promise.resolve(json({ data: [], nextCursor: null })); if (path.includes("/agent-definitions/visible")) return Promise.resolve(json({ definitions: [] })); if (init?.method === "PATCH") { diff --git a/apps/web/test/insights-route.test.tsx b/apps/web/test/insights-route.test.tsx index 0462e5867d..c99f4861cd 100644 --- a/apps/web/test/insights-route.test.tsx +++ b/apps/web/test/insights-route.test.tsx @@ -1,7 +1,8 @@ -// CL-6062: Insights' run feed reads from the tenant-scoped top-level-runs -// endpoint (packages/run-scope/src/scope-routes.ts), not the dead -// `/me/workflows/runs` (every addressed run self-anchors at creation, so -// that feed's `anchorRunId IS NULL` filter never matched anything). These +// CL-6062: Insights' run feed reads from the native tenant-scoped +// `GET /workflows/runs` top-level listing (CL-8087 repointed it off the +// deleted `/top-level-runs` route), not the dead `/me/workflows/runs` +// (every addressed run self-anchors at creation, so that feed's +// `anchorRunId IS NULL` filter never matched anything). These // tests exercise the real fetch wiring `InsightsRoute` owns — the unit // tests in `../src/insights-stats.test.ts` cover the pure filtering logic. import { afterEach, describe, expect, test } from "bun:test"; @@ -78,7 +79,17 @@ function stubFetch(runsBody: { data: readonly unknown[] }): RecordedCall[] { calls.push({ path }); if (path.includes("/api/me/principals")) return Promise.resolve(json(membership)); - if (path.includes("/top-level-runs")) + if (path.includes("/me/workflows/runs")) + return Promise.resolve( + new Response(JSON.stringify({ error: { message: "dead endpoint" } }), { + status: 500, + headers: { "content-type": "application/json" }, + }), + ); + // CL-8087: the native tenant-scoped top-level listing. Checked after + // the dead `/me/workflows/runs` branch above — that path also contains + // "/workflows/runs". + if (path.includes("/workflows/runs")) return Promise.resolve(json({ data: runsBody.data, nextCursor: null })); if (path.includes("/insights/usage")) return Promise.resolve( @@ -102,13 +113,6 @@ function stubFetch(runsBody: { data: readonly unknown[] }): RecordedCall[] { return Promise.resolve(json({ tools: [] })); if (path.includes("/routines")) return Promise.resolve(json({ data: [], nextCursor: null })); - if (path.includes("/me/workflows/runs")) - return Promise.resolve( - new Response(JSON.stringify({ error: { message: "dead endpoint" } }), { - status: 500, - headers: { "content-type": "application/json" }, - }), - ); return Promise.resolve(json({ data: [], nextCursor: null })); }) as typeof fetch; return calls; @@ -142,7 +146,7 @@ describe("InsightsRoute run feed", () => { const calls = stubFetch({ data: [deployment] }); const el = await mount(); expect(el.textContent).toContain("Morning brief"); - expect(calls.some((c) => c.path.includes("/top-level-runs"))).toBe(true); + expect(calls.some((c) => c.path.includes("/workflows/runs"))).toBe(true); expect(calls.some((c) => c.path.includes("/me/workflows/runs"))).toBe( false, ); @@ -159,9 +163,28 @@ describe("InsightsRoute run feed", () => { const calls = stubFetch({ data: [deployment] }); const el = await mount("/insights/runs"); expect(el.textContent).toContain("Morning brief"); - expect(calls.some((c) => c.path.includes("/top-level-runs"))).toBe(true); + expect(calls.some((c) => c.path.includes("/workflows/runs"))).toBe(true); expect(calls.some((c) => c.path.includes("/me/workflows/runs"))).toBe( false, ); }); + + // CL-8087: the native listing carries no routine attribution at all, so + // a row without `routineId`/`routineName` must still validate and reach + // the surface via the definition-name fallback. + test("a native row without routine attribution still reaches the surface", async () => { + const nativeRow = { + id: "run_native", + definitionId: "wfd_1", + definitionName: "Morning brief", + tenantId: "tnt_1", + address: "run_native@acme.localhost", + status: "running", + createdAt: new Date(Date.now() - 60_000).toISOString(), + updatedAt: new Date(Date.now() - 60_000).toISOString(), + }; + stubFetch({ data: [nativeRow] }); + const el = await mount(); + expect(el.textContent).toContain("Morning brief"); + }); }); diff --git a/apps/web/test/routine-activity.test.ts b/apps/web/test/routine-activity.test.ts index 047c76696b..742d7694b1 100644 --- a/apps/web/test/routine-activity.test.ts +++ b/apps/web/test/routine-activity.test.ts @@ -1,17 +1,15 @@ -// CL-6595: the shell's "Running" section and Mission Control's active-run -// count both read `listRoutineActivity`, which must source the `feed=fires` -// listing — the one top-level-runs view that keeps a routine's fire (see -// `@corbits/run-scope`'s `scope-routes.ts`). The plain `listTopLevelRuns` -// feed excludes every non-top-level run, and a routine fire is not a -// top-level run, so a routine genuinely running would never appear -// here at all -- Mission -// Control would read "0 active" while the Routines page's own "Running -// now" pill (driven by the same run's `workflow_run.status`) disagreed. +// CL-8087: `listRoutineActivity` resolves no items without fetching. The +// `feed=fires` route it used to read is deleted with `@corbits/run-scope`, +// and the native `GET /workflows/runs` listing has no fires equivalent — +// its top-level-only predicate drops every routine fire by construction, +// and its rows carry no routine attribution to compose client-side from. +// So the shell's "Running" band and Mission Control's active-run count +// honestly report no routine activity until a native fires equivalent +// exists, instead of deriving routine activity from top-level deployment +// rows that are not routine fires. import { afterEach, describe, expect, test } from "bun:test"; -import { FIRE_RUNNING_WINDOW_MS } from "@corbits/workflows/client"; - import { listRoutineActivity } from "../src/shell/routine-activity"; const realFetch = globalThis.fetch; @@ -20,127 +18,20 @@ afterEach(() => { globalThis.fetch = realFetch; }); -let lastRequestedPath: string | null = null; - -function stubTopLevelRunsFetch(runs: readonly unknown[]): void { - globalThis.fetch = ((input: RequestInfo | URL) => { - lastRequestedPath = typeof input === "string" ? input : input.toString(); - return Promise.resolve( - new Response(JSON.stringify({ data: runs, nextCursor: null }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - }) as typeof fetch; -} - -const runningFire = { - id: "run_1", - definitionId: "wfd_1", - definitionName: "Weekly digest", - tenantId: "tnt_1", - address: "run_1@tnt1.example", - status: "running", - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - routineId: "rtn_1", - routineName: "Weekly digest", -}; - -const completedFire = { - ...runningFire, - id: "run_2", - status: "stopped", - routineId: "rtn_1", -}; - -const nonRoutineFire = { - id: "run_3", - definitionId: "wfd_2", - definitionName: "Directly triggered deployment", - tenantId: "tnt_1", - address: "run_3@tnt1.example", - status: "running", - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", - routineId: null, - routineName: null, -}; - describe("listRoutineActivity", () => { - test("reads the fires feed, not the plain top-level-runs feed", async () => { - stubTopLevelRunsFetch([runningFire]); - await listRoutineActivity("tnt_1"); - expect(lastRequestedPath).toContain("feed=fires"); - }); - - test("maps a running routine fire into a routine activity item", async () => { - stubTopLevelRunsFetch([runningFire]); - - const items = await listRoutineActivity("tnt_1"); - - expect(items).toEqual([ - { - id: "run_1", - name: "Weekly digest", - status: "running", - startedAt: runningFire.createdAt, - }, - ]); - }); - - // The bug this ticket reports: a run that genuinely finished must not - // keep counting toward "active" just because it once fired. Mission - // Control's active-run count filters on `status === "running"`, so this - // item leaving that status here is what makes the two surfaces agree. - test("a completed routine fire no longer reads as running", async () => { - stubTopLevelRunsFetch([completedFire]); - - const [item] = await listRoutineActivity("tnt_1"); - - expect(item?.status).not.toBe("running"); + test("resolves no routine activity", async () => { + await expect(listRoutineActivity()).resolves.toEqual([]); }); - test("endedAt drops a just-finished running fire from running immediately", async () => { - stubTopLevelRunsFetch([ - { - ...runningFire, - endedAt: new Date().toISOString(), - }, - ]); - - const [item] = await listRoutineActivity("tnt_1"); - - expect(item?.status).not.toBe("running"); - }); + test("issues no fetch", async () => { + let fetched = false; + globalThis.fetch = ((_input: RequestInfo | URL) => { + fetched = true; + return Promise.resolve(new Response("{}", { status: 200 })); + }) as typeof fetch; - // A live fire still in a tool loop can outlast the abandoned-fire - // window. Persist has not settled, so the row stays running. - test("a live running fire past the window still reads as running", async () => { - stubTopLevelRunsFetch([ - { - ...runningFire, - createdAt: new Date( - Date.now() - FIRE_RUNNING_WINDOW_MS - 1, - ).toISOString(), - }, - ]); - - const [item] = await listRoutineActivity("tnt_1"); - - expect(item?.status).toBe("running"); - }); - - // A directly-triggered deployment run is also a `feed=fires` row (it is - // a top-level run), but it has no routine parent -- it must not be - // counted as routine activity. - test("drops a fires-feed row with no routine parent", async () => { - stubTopLevelRunsFetch([nonRoutineFire]); - expect(await listRoutineActivity("tnt_1")).toEqual([]); - }); + await listRoutineActivity(); - test("an empty run list is an empty routine list", async () => { - stubTopLevelRunsFetch([]); - expect(await listRoutineActivity("tnt_1")).toEqual([]); + expect(fetched).toBe(false); }); }); diff --git a/apps/web/test/sidebar.test.tsx b/apps/web/test/sidebar.test.tsx index fcb5c837f5..edff62830b 100644 --- a/apps/web/test/sidebar.test.tsx +++ b/apps/web/test/sidebar.test.tsx @@ -93,8 +93,6 @@ function stubFetch(options?: { const path = typeof input === "string" ? input : String(input); if (path.includes("/api/me/principals")) return Promise.resolve(json(membership)); - if (path.includes("/top-level-runs")) - return Promise.resolve(json({ data: [], nextCursor: null })); if (path.includes("/agent-definitions/visible")) return Promise.resolve(json({ definitions: [] })); if (path.includes("/insights/usage")) { @@ -371,8 +369,6 @@ describe("Sidebar", () => { return Promise.resolve(json({ items: [] })); if (path.includes("/approvals")) return Promise.resolve(json({ data: [], nextCursor: null })); - if (path.includes("/top-level-runs")) - return Promise.resolve(json({ data: [], nextCursor: null })); if (path.includes("/agent-definitions/visible")) return Promise.resolve(json({ definitions: [] })); return Promise.resolve(json({ items: [] })); @@ -490,8 +486,6 @@ describe("Sidebar", () => { ); if (path.includes("/approvals")) return Promise.resolve(json({ data: [], nextCursor: null })); - if (path.includes("/top-level-runs")) - return Promise.resolve(json({ data: [], nextCursor: null })); return Promise.resolve(json({ items: [] })); }) as typeof fetch; } @@ -574,8 +568,6 @@ describe("Sidebar", () => { return Promise.resolve(json({ items: [dm] })); if (path.includes("/approvals")) return Promise.resolve(json({ data: [], nextCursor: null })); - if (path.includes("/top-level-runs")) - return Promise.resolve(json({ data: [], nextCursor: null })); if (path.includes("/agent-definitions/visible")) return Promise.resolve(json({ definitions: [] })); return Promise.resolve(json({ items: [] })); diff --git a/apps/web/test/workbench-list.test.tsx b/apps/web/test/workbench-list.test.tsx index e8d85745f8..ae91c4e90a 100644 --- a/apps/web/test/workbench-list.test.tsx +++ b/apps/web/test/workbench-list.test.tsx @@ -57,8 +57,6 @@ function stubFetch( const path = typeof input === "string" ? input : String(input); if (path.includes("/api/me/principals")) return Promise.resolve(json(membership)); - if (path.includes("/top-level-runs")) - return Promise.resolve(json({ data: [], nextCursor: null })); if (path.includes("/approvals")) return Promise.resolve( json({ data: data.pendingApprovals ?? [], nextCursor: null }), diff --git a/packages/agent-directory/README.md b/packages/agent-directory/README.md index 31f0ec27bb..aa5b383b33 100644 --- a/packages/agent-directory/README.md +++ b/packages/agent-directory/README.md @@ -80,8 +80,8 @@ from wherever it gets them (`apps/web/src/agents-api.ts`'s `loadAgentDirectory`, for this repo) — this subpath issues no request and holds no state of its own. `excludeRunIds` is available for a host whose instance listing isn't already scoped server-side; this repo's own host -gets that scoping for free from `listTopLevelRuns` (see -`@corbits/run-scope`) and so never passes it. Every function is generic +gets that scoping for free from the native `GET /workflows/runs` listing +(already top-level-only) and so never passes it. Every function is generic over the host's concrete row type (constrained to the minimal shape it reads), so a host's richer types pass through untouched. diff --git a/packages/run-key-history/test/reconnect.drizzle.test.ts b/packages/run-key-history/test/reconnect.drizzle.test.ts index 8b2f6051ac..979554e156 100644 --- a/packages/run-key-history/test/reconnect.drizzle.test.ts +++ b/packages/run-key-history/test/reconnect.drizzle.test.ts @@ -1,5 +1,4 @@ -// DB-gated: skipped when no DATABASE_URL is reachable, mirroring -// `@corbits/run-scope`'s `scope-routes.drizzle.test.ts`. Runs the +// DB-gated: skipped when no DATABASE_URL is reachable. Runs the // real platform schema (`@intx/db`'s `runMigrations`) alongside this // package's own `applyRunKeyHistoryMigrations`, so the reconnect // repair is proven against a real `workflow_run` row and a real From 146b406d3ef5eb8bfa3db505b1d51b2254ae5a36 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 16 Sep 2026 22:30:17 -0700 Subject: [PATCH 2/4] feat!: delete @corbits/run-scope, unmount /top-level-runs (CL-8087) --- apps/hub/package.json | 1 - apps/hub/src/index.ts | 18 - package.json | 1 - packages/run-scope/LICENSE | 176 ---------- packages/run-scope/package.json | 28 -- packages/run-scope/src/in-flight-turns.ts | 43 --- packages/run-scope/src/index.ts | 10 - packages/run-scope/src/scope-routes.ts | 310 ------------------ .../test/scope-routes.drizzle.test.ts | 289 ---------------- packages/run-scope/tsconfig.json | 29 -- packages/run-scope/tsconfig.src.json | 26 -- tsconfig.build.json | 3 - 12 files changed, 934 deletions(-) delete mode 100644 packages/run-scope/LICENSE delete mode 100644 packages/run-scope/package.json delete mode 100644 packages/run-scope/src/in-flight-turns.ts delete mode 100644 packages/run-scope/src/index.ts delete mode 100644 packages/run-scope/src/scope-routes.ts delete mode 100644 packages/run-scope/test/scope-routes.drizzle.test.ts delete mode 100644 packages/run-scope/tsconfig.json delete mode 100644 packages/run-scope/tsconfig.src.json diff --git a/apps/hub/package.json b/apps/hub/package.json index f6d02cc57e..37829b68be 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -52,7 +52,6 @@ "@corbits/process-provisioner": "workspace:*", "@corbits/reddit-opportunity-scanner-workflow": "workspace:*", "@corbits/run-key-history": "workspace:*", - "@corbits/run-scope": "workspace:*", "@corbits/skills": "workspace:*", "@corbits/slack-tag": "workspace:*", "@corbits/tool-registry-publish": "workspace:*", diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index facaa01409..b111b0d82c 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -95,7 +95,6 @@ import type { RelaunchNoticePort } from "@corbits/chat"; import { reportError } from "@corbits/error-sink"; import type { FinalizedTurnToolCall } from "@corbits/turn-artifacts"; import { decodedOrNull } from "@corbits/url-path"; -import { createTopLevelRunRoutes } from "@corbits/run-scope"; import { createInboxRoutes, createWorkbenchMailboxDelivery, @@ -2497,23 +2496,6 @@ export async function createHub(config: HubConfig) { bus: mailboxBus, }); - // Every genuine top-level deployment run, chat-hosted and invited-agent - // runs excluded — the scoped listing CL-6061 adds so the Agent Directory - // and the shell's "Running" bands stop deriving that exclusion - // client-side from a tenant's chats alone (see - // `@corbits/run-scope`'s `scope-routes.ts`, which a non-top-level run - // with no chat involved silently slipped past). - app.route( - `${TENANT_PREFIX}/top-level-runs`, - createTopLevelRunRoutes({ - db, - requireGrant: createRequireGrant({ - grantStore: chatGrantStore, - conditionRegistry: chatConditionRegistry, - }), - }), - ); - // Recurring auto-fire: `workflow-scheduler.ts` ticks authored, deployed // definitions whose frozen projection carries a native ScheduleTrigger. // This hub has no general job-runner today, so the loop is scoped to diff --git a/package.json b/package.json index 12257cd3b4..1af14c30f6 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "puppeteer-core": "^25.7.0", "typescript": "6.0.3", "typescript-eslint": "^8.42.0", - "@corbits/run-scope": "workspace:*", "@corbits/webhook-triggers": "workspace:*" }, "engines": { diff --git a/packages/run-scope/LICENSE b/packages/run-scope/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/packages/run-scope/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ -GNU LESSER GENERAL PUBLIC LICENSE - -Version 2.1, February 1999 - -Copyright (C) 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] - -Preamble - -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. - -This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. - -When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. - -To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. - -For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. - -We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. - -To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. - -Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. - -Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. - -When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. - -We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. - -For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. - -In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. - -Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. - -The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. - -GNU LESSER GENERAL PUBLIC LICENSE -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". - -A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. - -The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) - -"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. - -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. - -1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. - -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. - -(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - -3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. - -Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. - -This option is useful when you wish to copy part of the code of the Library into a program that is not a library. - -4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. - -If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. - -5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. - -However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. - -When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. - -If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) - -Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - -6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. - -You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: - - a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. - - e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. - -For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. - -It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. - -7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. - - b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. - -8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. - -9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. - -10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. - -11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. - -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - -12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. - -13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. - -14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - -NO WARRANTY - -15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Libraries - -If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). - -To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - one line to give the library's name and an idea of what it does. - Copyright (C) year name of author - - This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: - -Yoyodyne, Inc., hereby disclaims all copyright interest in -the library `Frob' (a library for tweaking knobs) written -by James Random Hacker. - -signature of Ty Coon, 1 April 1990 -Ty Coon, President of Vice -That's all there is to it! diff --git a/packages/run-scope/package.json b/packages/run-scope/package.json deleted file mode 100644 index 5a7aef9c77..0000000000 --- a/packages/run-scope/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@corbits/run-scope", - "private": true, - "description": "Which of a tenant's workflow runs are genuine top-level deployments: the Agent Directory's and Insights' shared workflow-run listing route", - "version": "0.0.1", - "license": "LGPL-2.1-or-later", - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "@corbits/error-sink": "workspace:*", - "@intx/db": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/types": "workspace:*", - "arktype": "catalog:", - "drizzle-orm": "catalog:", - "hono": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - } -} diff --git a/packages/run-scope/src/in-flight-turns.ts b/packages/run-scope/src/in-flight-turns.ts deleted file mode 100644 index d25c806478..0000000000 --- a/packages/run-scope/src/in-flight-turns.ts +++ /dev/null @@ -1,43 +0,0 @@ -// In-flight inference turns for a batch of workflow runs. Listing -// surfaces (Insights `feed=fires`, routine run summaries) attach these -// so a live tool-loop older than the abandoned-fire window still reads -// as running, without adding a field to Interchange's WorkflowRunResponse. -import { and, eq, inArray } from "drizzle-orm"; -import type { DB } from "@intx/db"; -import { inferenceTurn } from "@intx/db/schema"; - -export type InFlightListingTurn = { - readonly status: "running"; - readonly endedAt: null; -}; - -/** - * Run ids in `runIds` that currently have at least one `inference_turn` - * still `running`. Every requested id is present after the query — an - * empty array means queried, none in flight, not "turns omitted". Empty - * input is an empty map — no round trip. - */ -export async function listingTurnsByRunId( - db: DB["db"], - runIds: readonly string[], -): Promise> { - if (runIds.length === 0) return new Map(); - const byRun = new Map(); - for (const id of runIds) byRun.set(id, []); - const rows = await db - .select({ runId: inferenceTurn.runId }) - .from(inferenceTurn) - .where( - and( - inArray(inferenceTurn.runId, [...runIds]), - eq(inferenceTurn.status, "running"), - ), - ); - const turn: InFlightListingTurn = { status: "running", endedAt: null }; - for (const row of rows) { - const existing = byRun.get(row.runId); - if (existing === undefined) byRun.set(row.runId, [turn]); - else existing.push(turn); - } - return byRun; -} diff --git a/packages/run-scope/src/index.ts b/packages/run-scope/src/index.ts deleted file mode 100644 index 64db5d0fb4..0000000000 --- a/packages/run-scope/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { - createTopLevelRunRoutes, - listTopLevelRuns, - listTopLevelRunFires, - type CreateTopLevelRunRoutesDeps, - type ResolveRoutineFires, - type RoutineFireInfo, -} from "./scope-routes"; -export { listingTurnsByRunId } from "./in-flight-turns"; -export type { InFlightListingTurn } from "./in-flight-turns"; diff --git a/packages/run-scope/src/scope-routes.ts b/packages/run-scope/src/scope-routes.ts deleted file mode 100644 index a0e6b3dfb4..0000000000 --- a/packages/run-scope/src/scope-routes.ts +++ /dev/null @@ -1,310 +0,0 @@ -// The workbench-owned answer to "which of this tenant's workflow runs -// are genuine top-level deployments?" — the question -// `packages/chat-ui/src/folded-run-ids.ts` used to answer by deriving -// an exclusion set from a tenant's *workbenches*. This route answers -// it server-side instead, straight off `workflow_run`. -// -// The listing predicate mirrors vendor's own "top-level run" predicate -// (`isNotNull(workflowRun.address)`, `anchorRunId === id`) — see -// `vendor/intx/hub-api/src/routes/runs.ts`'s `GET /workflows/runs` and -// `vendor/intx/hub-sessions/src/hub-session-lookups.ts`'s -// `isTopLevelRun`, this route's reference implementation. -// -// A second feed lives here too (`feed=fires`, `listTopLevelRunFires`, -// CL-6249): Insights' different question, "which runs actually executed" -// rather than "which deployments exist." A deployment's anchor row that -// never got triggered is not a run at all (it stays `status: "deployed"` -// forever — see `listTopLevelRunFires`'s own comment), so that feed drops -// it. `resolveRoutineFires` annotates a fire with its parent schedule -// when the host wires the port; it is not an exception to the listing -// predicate. -import { and, desc, eq, inArray, isNotNull, ne } from "drizzle-orm"; -import { Hono } from "hono"; -import { type } from "arktype"; -import type { RequireGrant, TenantEnv } from "@intx/hub-api"; -import { getDescendantTenants, type DB } from "@intx/db"; -import { workflowDefinition, workflowRun } from "@intx/db/schema"; -import type { WorkflowRunStatus } from "@intx/types"; -import { makeErrorEnvelope } from "@corbits/error-sink"; -import { listingTurnsByRunId } from "./in-flight-turns"; - -/** A routine fire's parent, resolved by `resolveRoutineFires` below. */ -export type RoutineFireInfo = { - readonly routineId: string; - readonly routineName: string; -}; - -/** - * Host-supplied port: given a batch of run ids, returns the subset that - * are scheduled fires, each with its parent schedule's id and human - * name. Optional: a host that mounts this route without wiring it - * still lists fires; those rows simply report no schedule parent. - */ -export type ResolveRoutineFires = ( - runIds: readonly string[], -) => Promise>; - -export type CreateTopLevelRunRoutesDeps = { - db: DB["db"]; - requireGrant: RequireGrant; - resolveRoutineFires?: ResolveRoutineFires; -}; - -// A positive integer parsed straight out of the raw query string — -// arktype's own `string.integer.parse` morph (validate-then-coerce in -// one step, matching how `c.req.query()` bodies are parsed elsewhere -// in this codebase, e.g. `@corbits/insights`' `RangeQuery` in its -// routes.ts) rather than a hand-rolled `Number()`/`Number.isInteger` -// check. `> 0` cannot chain directly onto a morph in arktype's string -// syntax, hence the `.narrow` — the fluent equivalent of the same -// bound. -const PositiveInteger = type("string.integer.parse").narrow((n) => n > 0); -const LimitQuery = type({ - "limit?": PositiveInteger, -}); - -// `feed=fires` opts into `listTopLevelRunFires` (Insights' executed-runs -// question); omitted (the default) keeps `listTopLevelRuns` unchanged for -// its existing callers — the Agent Directory and the shell's activity -// bands, which need every deployment, fired or not. -const FeedQuery = type({ - "feed?": "'fires'", -}); - -// Mirrors `vendor/intx/hub-api/src/routes/run-view.ts`'s -// `mapRunStatusToViewStatus` (not published from `@intx/hub-api`, so -// this is the small, stable slice of it this route's wire shape -// needs) — a run's raw `workflow_run.status` mapped onto the same -// `WorkflowRunResponse` status vocabulary every other run-listing -// surface already speaks. -function toViewStatus(status: string): WorkflowRunStatus { - switch (status) { - case "deployed": - return "deployed"; - case "running": - return "running"; - case "completed": - case "cancelled": - return "stopped"; - case "failed": - return "error"; - default: - throw new Error(`unmapped workflow_run status "${status}"`); - } -} - -function toTimestamp(date: Date): string { - return date.toISOString(); -} - -/** - * The tenant's (or tenant subtree's) genuine top-level deployment runs, - * most recent first — the query `createTopLevelRunRoutes` serves. - * Exported separately from the route so a non-HTTP caller (a future - * scoped listing elsewhere in the hub) can reuse the same predicate - * without going through Hono. - * - * `tenantId` also accepts an array — the same shape - * `@corbits/insights`' `resolveScope` (packages/insights/src/routes.ts) - * passes to `summarizeUsage`/`activityByDay`, so a workspace parent's - * runs feed rolls up its child workbenches' runs the same way its - * usage/activity numbers already do. A lone string keeps every existing - * single-tenant caller unchanged. - */ -export async function listTopLevelRuns( - db: DB["db"], - tenantId: string | readonly string[], - limit = 100, -) { - const scope = typeof tenantId === "string" ? [tenantId] : tenantId; - if (scope.length === 0) return []; - const rows = await db - .select({ - id: workflowRun.id, - definitionId: workflowRun.definitionId, - definitionName: workflowDefinition.name, - tenantId: workflowRun.tenantId, - address: workflowRun.address, - status: workflowRun.status, - publicKey: workflowRun.publicKey, - kernelId: workflowRun.kernelId, - sidecarId: workflowRun.sidecarId, - createdAt: workflowRun.createdAt, - endedAt: workflowRun.endedAt, - }) - .from(workflowRun) - .innerJoin( - workflowDefinition, - eq(workflowRun.definitionId, workflowDefinition.id), - ) - .where( - and( - inArray(workflowRun.tenantId, scope), - isNotNull(workflowRun.address), - eq(workflowRun.anchorRunId, workflowRun.id), - ), - ) - .orderBy(desc(workflowRun.createdAt), desc(workflowRun.id)) - .limit(limit); - - return rows.map((row) => ({ - id: row.id, - definitionId: row.definitionId, - definitionName: row.definitionName, - tenantId: row.tenantId, - // Guarded by `isNotNull(workflowRun.address)` above. - address: row.address as string, - status: toViewStatus(row.status), - publicKey: row.publicKey, - kernelId: row.kernelId, - sidecarId: row.sidecarId, - createdAt: toTimestamp(row.createdAt), - updatedAt: toTimestamp(row.endedAt ?? row.createdAt), - endedAt: row.endedAt ? toTimestamp(row.endedAt) : null, - })); -} - -/** - * Insights' `fires` feed (CL-6249): the tenant's genuine *executed* - * runs, as opposed to `listTopLevelRuns`' "every deployment, fired or - * not" — the Agent Directory's question. A run's own `status` already - * says which one it is: `workflow_run.status` is born "deployed" only - * for a deployment's anchor row and never changes back to it once a - * first trigger flips it to "running" (see `vendor/intx/db`'s - * `workflow-run.ts`). So `status <> 'deployed'` is exactly "this row - * is not the resident, never-fired deployment placeholder" — no - * slug/name heuristic needed. - * - * `resolveRoutineFires` (the host's scheduled-fire bridge) annotates - * a listed row with its parent schedule's id/name when the host - * recognizes it as a fire. A row the port does not claim, or a host - * that never wires the port, still lists and reports no schedule - * parent — the caller's honest "definition name" fallback. - */ -export async function listTopLevelRunFires( - db: DB["db"], - tenantId: string | readonly string[], - resolveRoutineFires: ResolveRoutineFires | undefined, - limit = 100, -) { - const scope = typeof tenantId === "string" ? [tenantId] : tenantId; - if (scope.length === 0) return []; - const rows = await db - .select({ - id: workflowRun.id, - definitionId: workflowRun.definitionId, - definitionName: workflowDefinition.name, - tenantId: workflowRun.tenantId, - address: workflowRun.address, - status: workflowRun.status, - publicKey: workflowRun.publicKey, - kernelId: workflowRun.kernelId, - sidecarId: workflowRun.sidecarId, - createdAt: workflowRun.createdAt, - endedAt: workflowRun.endedAt, - }) - .from(workflowRun) - .innerJoin( - workflowDefinition, - eq(workflowRun.definitionId, workflowDefinition.id), - ) - .where( - and( - inArray(workflowRun.tenantId, scope), - isNotNull(workflowRun.address), - eq(workflowRun.anchorRunId, workflowRun.id), - ne(workflowRun.status, "deployed"), - ), - ) - .orderBy(desc(workflowRun.createdAt), desc(workflowRun.id)) - .limit(limit); - - const runIds = rows.map((row) => row.id); - const routineFires = - runIds.length > 0 && resolveRoutineFires !== undefined - ? await resolveRoutineFires(runIds) - : new Map(); - - const turnsByRun = await listingTurnsByRunId(db, runIds); - - return rows.map((row) => { - const fire = routineFires.get(row.id); - const turns = turnsByRun.get(row.id) ?? []; - return { - id: row.id, - definitionId: row.definitionId, - definitionName: row.definitionName, - tenantId: row.tenantId, - // Guarded by `isNotNull(workflowRun.address)` above. - address: row.address as string, - status: toViewStatus(row.status), - publicKey: row.publicKey, - kernelId: row.kernelId, - sidecarId: row.sidecarId, - createdAt: toTimestamp(row.createdAt), - updatedAt: toTimestamp(row.endedAt ?? row.createdAt), - endedAt: row.endedAt ? toTimestamp(row.endedAt) : null, - routineId: fire?.routineId ?? null, - routineName: fire?.routineName ?? null, - hasInFlightTurn: turns.length > 0, - turns, - }; - }); -} - -/** - * Mounted at `${TENANT_PREFIX}/top-level-runs` in the hub composition - * root, beside every other package-owned tenant route. `GET /` is the - * one route: a paginated-shaped (but not yet cursor-paginated — see - * the "known limit" note on every caller of this route) list of the - * tenant's genuine top-level runs — rolled up over the requested - * tenant's whole descendant subtree the same way - * `@corbits/insights`' `/usage`, `/activity`, and `/tools` already do - * (see `resolveScope` in packages/insights/src/routes.ts), so a - * workspace parent's runs feed is never mismatched against its own - * usage aggregate. - */ -export function createTopLevelRunRoutes( - deps: CreateTopLevelRunRoutesDeps, -): Hono { - const app = new Hono(); - - app.get("/", deps.requireGrant("workflow-run:*", "read"), async (c) => { - const tenant = c.get("tenant"); - const rawLimit = c.req.query("limit"); - const query = LimitQuery(rawLimit === undefined ? {} : { limit: rawLimit }); - if (query instanceof type.errors) { - return c.json( - makeErrorEnvelope({ - code: "bad_request", - userMessage: query.summary, - }), - 400, - ); - } - const rawFeed = c.req.query("feed"); - const feedQuery = FeedQuery(rawFeed === undefined ? {} : { feed: rawFeed }); - if (feedQuery instanceof type.errors) { - return c.json( - makeErrorEnvelope({ - code: "bad_request", - userMessage: feedQuery.summary, - }), - 400, - ); - } - const scope = await getDescendantTenants(deps.db, tenant.id); - const data = - feedQuery.feed === "fires" - ? await listTopLevelRunFires( - deps.db, - scope, - deps.resolveRoutineFires, - query.limit, - ) - : await listTopLevelRuns(deps.db, scope, query.limit); - return c.json({ data, nextCursor: null }); - }); - - return app; -} diff --git a/packages/run-scope/test/scope-routes.drizzle.test.ts b/packages/run-scope/test/scope-routes.drizzle.test.ts deleted file mode 100644 index eebd022f24..0000000000 --- a/packages/run-scope/test/scope-routes.drizzle.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -// DB-gated: skipped when no DATABASE_URL is reachable, mirroring -// `@corbits/granola-tools`'s `credential-wiring-e2e.drizzle.test.ts`. -// Runs the real platform schema (`@intx/db`'s `runMigrations`, into its -// own named schema on the shared e2e database) so `listTopLevelRuns` -// is proven against real Postgres rows, not a hand-rolled fake `db`. -import { afterAll, beforeAll, expect, test } from "bun:test"; -import { createDB, runMigrations, dropSchema } from "@intx/db"; -import { schema } from "@intx/db"; - -import { dbTargetFromUrl } from "../../../scripts/db-setup"; -import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; -import { listTopLevelRuns, listTopLevelRunFires } from "../src/scope-routes"; -import { dbGate } from "../../../scripts/e2e/db-gate"; - -const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = dbGate(databaseUrl, import.meta.path); - -const SCHEMA = "run_scope_scope_routes_test"; -const TENANT = "tnt_scope_routes"; - -describeIfDb("listTopLevelRuns", () => { - const target = dbTargetFromUrl( - databaseUrl ?? "postgres://localhost:5432/unused", - ); - - beforeAll(async () => { - await runMigrations(target, { schema: SCHEMA }); - }); - - afterAll(async () => { - await dropSchema(target, { schema: SCHEMA }); - }); - - test("lists a genuine top-level deployment", async () => { - const { db, close } = createDB({ ...target, schema: SCHEMA }); - try { - await db.insert(schema.tenant).values({ - id: TENANT, - name: "Scope Routes Tenant", - slug: "scope-routes-tenant", - domain: "scope-routes.workbench.test", - }); - await db.insert(schema.workflowDefinition).values({ - id: "wfd_researcher", - tenantId: TENANT, - name: "researcher", - status: "deployed", - }); - - await db.insert(schema.workflowRun).values({ - id: "run_deployment1", - definitionId: "wfd_researcher", - anchorRunId: "run_deployment1", - tenantId: TENANT, - address: "run_deployment1@scope-routes.workbench.test", - status: "running", - createdAt: new Date("2026-01-01T00:00:00.000Z"), - }); - - const rows = await listTopLevelRuns(db, TENANT); - - expect(rows.map((row) => row.id)).toEqual(["run_deployment1"]); - expect(rows[0]).toMatchObject({ - id: "run_deployment1", - definitionId: "wfd_researcher", - definitionName: "researcher", - tenantId: TENANT, - status: "running", - }); - } finally { - await close(); - } - }); - - test("a child park row (no address) never appears", async () => { - const { db, close } = createDB({ ...target, schema: SCHEMA }); - try { - await db.insert(schema.workflowRun).values({ - id: "run_child_park1", - definitionId: "wfd_researcher", - anchorRunId: "run_deployment1", - tenantId: TENANT, - address: null, - status: "running", - createdAt: new Date("2026-01-03T00:00:00.000Z"), - }); - - const rows = await listTopLevelRuns(db, TENANT); - expect(rows.map((row) => row.id)).not.toContain("run_child_park1"); - } finally { - await close(); - } - }); - - test("an array of tenant ids rolls up runs across every tenant in it, still excluding other tenants", async () => { - const OTHER_TENANT = "tnt_scope_routes_other"; - const UNRELATED_TENANT = "tnt_scope_routes_unrelated"; - const { db, close } = createDB({ ...target, schema: SCHEMA }); - try { - await db.insert(schema.tenant).values([ - { - id: OTHER_TENANT, - name: "Other Tenant", - slug: "scope-routes-other-tenant", - domain: "scope-routes-other.workbench.test", - }, - { - id: UNRELATED_TENANT, - name: "Unrelated Tenant", - slug: "scope-routes-unrelated-tenant", - domain: "scope-routes-unrelated.workbench.test", - }, - ]); - await db.insert(schema.workflowRun).values([ - { - id: "run_other_deployment1", - definitionId: "wfd_researcher", - anchorRunId: "run_other_deployment1", - tenantId: OTHER_TENANT, - address: "run_other_deployment1@scope-routes.workbench.test", - status: "running", - createdAt: new Date("2026-01-04T00:00:00.000Z"), - }, - { - id: "run_unrelated_deployment1", - definitionId: "wfd_researcher", - anchorRunId: "run_unrelated_deployment1", - tenantId: UNRELATED_TENANT, - address: "run_unrelated_deployment1@scope-routes.workbench.test", - status: "running", - createdAt: new Date("2026-01-04T00:00:00.000Z"), - }, - ]); - - const rows = await listTopLevelRuns(db, [TENANT, OTHER_TENANT]); - const ids = rows.map((row) => row.id).sort(); - expect(ids).toContain("run_deployment1"); - expect(ids).toContain("run_other_deployment1"); - expect(ids).not.toContain("run_unrelated_deployment1"); - - expect(await listTopLevelRuns(db, [])).toEqual([]); - } finally { - await close(); - } - }); -}); - -const FIRES_SCHEMA = "run_scope_scope_routes_fires_test"; -const FIRES_TENANT = "tnt_scope_routes_fires"; - -describeIfDb("listTopLevelRunFires", () => { - const target = dbTargetFromUrl( - databaseUrl ?? "postgres://localhost:5432/unused", - ); - - beforeAll(async () => { - await runMigrations(target, { schema: FIRES_SCHEMA }); - }); - - afterAll(async () => { - await dropSchema(target, { schema: FIRES_SCHEMA }); - }); - - test("excludes the resident never-fired deployment row, includes a routine fire with its routine's name, and falls back to the definition name for a directly-triggered deployment", async () => { - const { db, close } = createDB({ ...target, schema: FIRES_SCHEMA }); - try { - await db.insert(schema.tenant).values({ - id: FIRES_TENANT, - name: "Scope Routes Fires Tenant", - slug: "scope-routes-fires-tenant", - domain: "scope-routes-fires.workbench.test", - }); - await db.insert(schema.workflowDefinition).values([ - { - id: "wfd_workbench_digest", - tenantId: FIRES_TENANT, - name: "workbench-digest", - status: "deployed", - }, - { - id: "wfd_researcher", - tenantId: FIRES_TENANT, - name: "researcher", - status: "deployed", - }, - ]); - - // The resident deployment row: born "deployed", never triggered. - // Plumbing, not a run — must never appear in the fires feed. - await db.insert(schema.workflowRun).values({ - id: "run_never_fired1", - definitionId: "wfd_workbench_digest", - anchorRunId: "run_never_fired1", - tenantId: FIRES_TENANT, - address: "run_never_fired1@scope-routes-fires.workbench.test", - status: "deployed", - createdAt: new Date("2026-01-01T00:00:00.000Z"), - }); - - // A routine fire that `resolveRoutineFires` below recognizes as - // a fire of "Pulse check" — must appear, carrying that routine's - // name. - await db.insert(schema.workflowRun).values({ - id: "run_fire1", - definitionId: "wfd_workbench_digest", - anchorRunId: "run_fire1", - tenantId: FIRES_TENANT, - address: "run_fire1@scope-routes-fires.workbench.test", - status: "running", - createdAt: new Date("2026-01-02T00:00:00.000Z"), - }); - - // A plain deployment, triggered directly (no routine parent): - // must appear, with no routine parent — the honest - // definition-name fallback. - await db.insert(schema.workflowRun).values({ - id: "run_direct_deployment1", - definitionId: "wfd_researcher", - anchorRunId: "run_direct_deployment1", - tenantId: FIRES_TENANT, - address: "run_direct_deployment1@scope-routes-fires.workbench.test", - status: "running", - createdAt: new Date("2026-01-03T00:00:00.000Z"), - }); - - const resolveRoutineFires = async (runIds: readonly string[]) => { - const fires = new Map< - string, - { routineId: string; routineName: string } - >(); - if (runIds.includes("run_fire1")) { - fires.set("run_fire1", { - routineId: "rtn_pulse_check", - routineName: "Pulse check", - }); - } - return fires; - }; - - const rows = await listTopLevelRunFires( - db, - FIRES_TENANT, - resolveRoutineFires, - ); - - expect(rows.map((row) => row.id).sort()).toEqual( - ["run_direct_deployment1", "run_fire1"].sort(), - ); - - const fire = rows.find((row) => row.id === "run_fire1"); - expect(fire).toMatchObject({ - definitionName: "workbench-digest", - routineId: "rtn_pulse_check", - routineName: "Pulse check", - hasInFlightTurn: false, - turns: [], - }); - - const direct = rows.find((row) => row.id === "run_direct_deployment1"); - expect(direct).toMatchObject({ - definitionName: "researcher", - routineId: null, - routineName: null, - hasInFlightTurn: false, - turns: [], - }); - } finally { - await close(); - } - }); - - test("with no resolveRoutineFires wired, fired runs still appear with no routine parent", async () => { - const { db, close } = createDB({ ...target, schema: FIRES_SCHEMA }); - try { - const rows = await listTopLevelRunFires(db, FIRES_TENANT, undefined); - const ids = rows.map((row) => row.id); - expect(ids).toContain("run_fire1"); - expect(ids).toContain("run_direct_deployment1"); - expect(ids).not.toContain("run_never_fired1"); - const fire = rows.find((row) => row.id === "run_fire1"); - expect(fire).toMatchObject({ - routineId: null, - routineName: null, - }); - } finally { - await close(); - } - }); -}); diff --git a/packages/run-scope/tsconfig.json b/packages/run-scope/tsconfig.json deleted file mode 100644 index 07fb49801a..0000000000 --- a/packages/run-scope/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [], - "references": [ - { - "path": "../../vendor/intx/db/tsconfig.src.json" - }, - { - "path": "../../vendor/intx/hub-api/tsconfig.src.json" - }, - { - "path": "../../vendor/intx/types/tsconfig.src.json" - }, - { - "path": "../error-sink/tsconfig.src.json" - } - ] -} diff --git a/packages/run-scope/tsconfig.src.json b/packages/run-scope/tsconfig.src.json deleted file mode 100644 index 12fa965092..0000000000 --- a/packages/run-scope/tsconfig.src.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - }, - "references": [ - { - "path": "../../vendor/intx/db/tsconfig.src.json" - }, - { - "path": "../../vendor/intx/hub-api/tsconfig.src.json" - }, - { - "path": "../../vendor/intx/types/tsconfig.src.json" - }, - { - "path": "../error-sink/tsconfig.src.json" - } - ] -} diff --git a/tsconfig.build.json b/tsconfig.build.json index df84f8fbe7..97591faf58 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -142,9 +142,6 @@ { "path": "./packages/run-key-history/tsconfig.src.json" }, - { - "path": "./packages/run-scope/tsconfig.src.json" - }, { "path": "./packages/sandbox-sidecar/tsconfig.src.json" }, From 7c9738c6e6fad9e04a7fdcea291416bb75e976a1 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 16 Sep 2026 22:37:04 -0700 Subject: [PATCH 3/4] chore: sync bun.lock after @corbits/run-scope deletion (CL-8087) --- bun.lock | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/bun.lock b/bun.lock index 7c3ac61eff..a881c265aa 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,5 @@ { - "lockfileVersion": 1, + "lockfileVersion": 2, "configVersion": 1, "workspaces": { "": { @@ -14,7 +14,6 @@ "@corbits/connections": "workspace:*", "@corbits/evals": "workspace:*", "@corbits/ollama-adapter": "workspace:*", - "@corbits/run-scope": "workspace:*", "@corbits/webhook-triggers": "workspace:*", "@corbits/xai-provider": "workspace:*", "@eslint/js": "^10.0.0", @@ -81,7 +80,6 @@ "@corbits/process-provisioner": "workspace:*", "@corbits/reddit-opportunity-scanner-workflow": "workspace:*", "@corbits/run-key-history": "workspace:*", - "@corbits/run-scope": "workspace:*", "@corbits/skills": "workspace:*", "@corbits/slack-tag": "workspace:*", "@corbits/tool-registry-publish": "workspace:*", @@ -1226,23 +1224,6 @@ "typescript": "catalog:", }, }, - "packages/run-scope": { - "name": "@corbits/run-scope", - "version": "0.0.1", - "dependencies": { - "@corbits/error-sink": "workspace:*", - "@intx/db": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/types": "workspace:*", - "arktype": "catalog:", - "drizzle-orm": "catalog:", - "hono": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, "packages/sandbox-sidecar": { "name": "@corbits/sandbox-sidecar", "version": "0.0.1", @@ -2287,8 +2268,6 @@ "@corbits/run-key-history": ["@corbits/run-key-history@workspace:packages/run-key-history"], - "@corbits/run-scope": ["@corbits/run-scope@workspace:packages/run-scope"], - "@corbits/sandbox-sidecar": ["@corbits/sandbox-sidecar@workspace:packages/sandbox-sidecar"], "@corbits/scout-agent": ["@corbits/scout-agent@workspace:packages/scout-agent"], @@ -3171,7 +3150,7 @@ "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "lucide-react": ["lucide-react@1.46.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Bv+FZXgZPrxc/NCl1e7JJVQFLdiCxYgxNVhqoV7X0p6I8ADJo8DxBnK1auH0fZz4AmqOJ3jgneL4f1i8LJQRAA=="], + "lucide-react": ["lucide-react@1.47.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-o8C23aXpNQypRY73W7fW02EyvWJinEMXgKeGjFoKym0zj3Q73hD97A1IQps1g8C14HTGsOpZL0Am+xjfgFZAbg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -3303,7 +3282,7 @@ "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - "node-releases": ["node-releases@2.0.55", "", {}, "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ=="], + "node-releases": ["node-releases@2.0.56", "", {}, "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A=="], "npm-install-checks": ["npm-install-checks@7.1.2", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ=="], From d8f80f588dd66510e88e042c0499b8fe0d87e032 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 16 Sep 2026 22:42:36 -0700 Subject: [PATCH 4/4] docs(web): disclose descendant-rollup loss, honest in-flight copy (CL-8087) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: record the deleted /top-level-runs descendant-subtree rollup (getDescendantTenants) as a fourth accepted loss in the Insights feed inventory and Agent Directory docs — the native GET /workflows/runs listing filters one tenant only. Mission Control In flight empty state no longer implies agent runs appear there; routine-activity query key renamed off the deleted route name. Verification: bun run typecheck pass; bun run lint pass (0 errors); bun run --filter @workbench/web test 983 pass 0 fail; structural pass. --- apps/web/src/agents-api.ts | 4 ++++ apps/web/src/agents-directory.ts | 6 ++++-- apps/web/src/insights-api.ts | 17 ++++++++++++----- apps/web/src/insights-stats.ts | 10 ++++++++-- apps/web/src/pages/mission-control-page.tsx | 2 +- apps/web/src/query-client.ts | 9 +++++++-- apps/web/src/shell/bench-activity.ts | 6 ++++-- apps/web/test/fetch-dedupe.test.tsx | 2 +- apps/web/test/mission-control-page.test.tsx | 4 ++++ apps/web/test/query-client.test.ts | 17 +++++++++++++++++ 10 files changed, 62 insertions(+), 15 deletions(-) diff --git a/apps/web/src/agents-api.ts b/apps/web/src/agents-api.ts index 9502de3565..afe0408662 100644 --- a/apps/web/src/agents-api.ts +++ b/apps/web/src/agents-api.ts @@ -140,6 +140,10 @@ export function listAgentInstances( * native `GET /workflows/runs` listing's own predicate (`address IS NOT * NULL AND anchorRunId = id`), the same one `isTopLevelRun` uses. Used * wherever a page needs "real deployments only" — the Agent Directory. + * Single-tenant (CL-8087 accepted loss): the deleted route expanded the + * requested tenant to its whole descendant subtree via + * `getDescendantTenants`; the native listing filters one tenant, so a + * workspace parent sees only its own runs, not its child workbenches'. */ export function listTopLevelRuns( tenantId: string, diff --git a/apps/web/src/agents-directory.ts b/apps/web/src/agents-directory.ts index 768f56253c..75fe0e7608 100644 --- a/apps/web/src/agents-directory.ts +++ b/apps/web/src/agents-directory.ts @@ -42,8 +42,10 @@ export function purposeAgentDefinitions( * `instances` is expected to already come from `listTopLevelRuns` * (see `./agents-api.ts`), which excludes every non-top-level run * (workbench host, invited agent, task) server-side — the native - * `GET /workflows/runs` listing's own predicate. This still applies the - * shared name-based workbench-host filter as defense in depth. + * `GET /workflows/runs` listing's own predicate. That listing is + * single-tenant (CL-8087 accepted loss: no descendant-subtree rollup), + * so this still applies the shared name-based workbench-host filter as + * defense in depth. */ export function purposeAgentInstances( instances: readonly AgentInstance[], diff --git a/apps/web/src/insights-api.ts b/apps/web/src/insights-api.ts index aab1503478..97e3ecd8bf 100644 --- a/apps/web/src/insights-api.ts +++ b/apps/web/src/insights-api.ts @@ -267,16 +267,23 @@ const TOP_LEVEL_RUNS_LIMIT = 100; * listing's own predicate (`address IS NOT NULL AND anchorRunId = id`, * see `vendor/intx/hub-api/src/routes/runs.ts`) already excludes every * non-top-level run (workbench host, invited agent), so this page never - * derives that exclusion itself. Three differences from the deleted + * derives that exclusion itself. Four differences from the deleted * `feed=fires` feed, all recorded as accepted loss in CL-8087: a * routine's fire (not a top-level run) is no longer included, so * Insights no longer sees routine executions; there is no routine - * attribution, so history groups by definition; and the resident, + * attribution, so history groups by definition; the resident, * never-triggered deployment placeholder (`status: "deployed"`) is now * included — `computeInsightsStats` counts it as deployed rather than - * hiding it. Used in place of the dead `/me/workflows/runs` — its - * `anchorRunId IS NULL` filter never matches, because every addressed run - * self-anchors at creation, so that feed always came back empty. + * hiding it; and the feed is single-tenant — the deleted route expanded + * the requested tenant to its whole descendant subtree via + * `getDescendantTenants` (the same rollup `resolveScope` still gives + * `/usage`, `/activity`, and `/tools`), while the native listing filters + * `tenantId = requested tenant` only, so a workspace parent's runs feed + * no longer rolls up its child workbenches and now sits mismatched + * against its own usage aggregate. Used in place of the dead + * `/me/workflows/runs` — its `anchorRunId IS NULL` filter never matches, + * because every addressed run self-anchors at creation, so that feed + * always came back empty. */ export function insightsTopLevelRunsPath(tenantId: string): string { return `/api/tenants/${tenantId}/workflows/runs?limit=${TOP_LEVEL_RUNS_LIMIT}`; diff --git a/apps/web/src/insights-stats.ts b/apps/web/src/insights-stats.ts index 37d067491b..47757b1465 100644 --- a/apps/web/src/insights-stats.ts +++ b/apps/web/src/insights-stats.ts @@ -99,7 +99,10 @@ export type DefinitionRunGroup = { /** `routineId` when the newest run in the group fired from one, * else `definitionId` — two different routines sharing one * definition (e.g. two workbench-digest schedules) never merge into - * one group. */ + * one group. The native `GET /workflows/runs` feed (CL-8087) carries + * no routine attribution, so in practice this is always `definitionId` + * until a fires equivalent exists — the `routineId` branch is kept for + * that feed, not removed. */ readonly groupKey: string; readonly displayName: string; /** Newest run first. */ @@ -111,7 +114,10 @@ export type DefinitionRunGroup = { * fetched for the flat list, bucketed by routine (falling back to * definition, for a run with no routine parent) and sorted newest-run * first — a client-side grouping of already-fetched data, no new endpoint. - * Group order follows each group's own newest run, newest overall first. + * With the native feed every row lacks routine attribution, so every + * group is definition-keyed today; the routine branch rejoins once a + * fires equivalent exists. Group order follows each group's own newest + * run, newest overall first. */ export function groupRunsByDefinition( runs: readonly InsightsRun[], diff --git a/apps/web/src/pages/mission-control-page.tsx b/apps/web/src/pages/mission-control-page.tsx index e66599ebfb..d5d59e5cbe 100644 --- a/apps/web/src/pages/mission-control-page.tsx +++ b/apps/web/src/pages/mission-control-page.tsx @@ -419,7 +419,7 @@ export function MissionControlRoute({ inFlightRows.length === 0 ? ( ) : null} {inFlightRows.length > 0 ? ( diff --git a/apps/web/src/query-client.ts b/apps/web/src/query-client.ts index c6529faa0a..07787c2cc6 100644 --- a/apps/web/src/query-client.ts +++ b/apps/web/src/query-client.ts @@ -128,8 +128,13 @@ export const tenantKeys = { * of the literal that could drift apart. */ workbenches: (tenantId: string, kind: WorkbenchKind) => workbenchesQueryKey(tenantId, kind), - topLevelRuns: (tenantId: string) => - ["tenant", tenantId, "top-level-runs"] as const, + /** The sidebar routine-activity seam (`./shell/routine-activity.ts`). + * CL-8087 deleted the `feed=fires` route this read, so the query resolves + * with no items and issues no fetch — the key is kept (not the deleted + * `/top-level-runs` path) so both sidebar mounts still share one cache + * entry, and so a future native fires equivalent has a key to rewire. */ + routineActivity: (tenantId: string) => + ["tenant", tenantId, "routine-activity"] as const, /** A workbench's own timeline reads (CL-6224): `tenantId` is the owning * bench chat's workbench-tenancy addresses these routes at (see * docs/workbench-tenancy.md), `workbenchId` the workbench's own id. */ diff --git a/apps/web/src/shell/bench-activity.ts b/apps/web/src/shell/bench-activity.ts index 56bb8f5e1b..e0b16ad9dd 100644 --- a/apps/web/src/shell/bench-activity.ts +++ b/apps/web/src/shell/bench-activity.ts @@ -81,12 +81,14 @@ export function useBenchActivity(tenantId: string | null): BenchActivityQuery { queryFn: () => listWorkbenches(key, "chat"), }); const routinesQuery = useQuery({ - queryKey: tenantKeys.topLevelRuns(key), + queryKey: tenantKeys.routineActivity(key), enabled, // CL-8087: `listRoutineActivity` resolves no items without fetching // (the `feed=fires` route is deleted and the native listing has no // fires equivalent), so this query never fires a request — the - // column keeps the key and renders the seam's honest empty state. + // column keeps the shared key and renders the seam's honest empty + // state. Rewire `listRoutineActivity` (not this key) when a native + // fires equivalent exists. queryFn: () => listRoutineActivity(), }); const agentsQuery = useQuery({ diff --git a/apps/web/test/fetch-dedupe.test.tsx b/apps/web/test/fetch-dedupe.test.tsx index d25bbf920a..f733196d67 100644 --- a/apps/web/test/fetch-dedupe.test.tsx +++ b/apps/web/test/fetch-dedupe.test.tsx @@ -2,7 +2,7 @@ // because independent components each fetched independently instead of // sharing a cache. Every mount of `useBenchActivity` (the sidebar's // `WorkbenchList`, and any second subscriber) shares the same TanStack -// Query keys (`tenantKeys.workbenches`, `.topLevelRuns` — see +// Query keys (`tenantKeys.workbenches`, `.routineActivity` — see // `../src/query-client.ts`) under one `QueryClient`, so two mounts fetch // each listing exactly once. import { afterEach, describe, expect, test } from "bun:test"; diff --git a/apps/web/test/mission-control-page.test.tsx b/apps/web/test/mission-control-page.test.tsx index 558e45bbd7..525c8342cf 100644 --- a/apps/web/test/mission-control-page.test.tsx +++ b/apps/web/test/mission-control-page.test.tsx @@ -276,6 +276,10 @@ describe("MissionControlRoute", () => { expect(container.textContent).not.toContain("New bench"); expect(container.textContent).toContain("Nothing waiting on you"); expect(container.textContent).toContain("Nothing running right now"); + // CL-8087: the In flight band is routine-feed only and the feed is + // stubbed, so the empty state says so instead of implying agent runs + // appear here. + expect(container.textContent).toContain("Routine activity has no feed"); expect(container.textContent).toContain("Nothing recent yet."); }); diff --git a/apps/web/test/query-client.test.ts b/apps/web/test/query-client.test.ts index 6118ebe69e..92608cb089 100644 --- a/apps/web/test/query-client.test.ts +++ b/apps/web/test/query-client.test.ts @@ -28,3 +28,20 @@ describe("pathToQueryKey", () => { expect(pathToQueryKey("/api/mystery")).toEqual(["path", "/api/mystery"]); }); }); + +describe("tenantKeys.routineActivity", () => { + // CL-8087: the sidebar routine-activity seam keeps a shared cache key that + // names the seam — not the deleted `/top-level-runs` route — so both + // mounts subscribe to one entry and a future native fires equivalent has + // a key to rewire. + test("keys routine activity per tenant without the deleted route name", () => { + expect(tenantKeys.routineActivity("tnt_1")).toEqual([ + "tenant", + "tnt_1", + "routine-activity", + ]); + expect( + (tenantKeys.routineActivity("tnt_1") as readonly unknown[]).join("/"), + ).not.toContain("top-level-runs"); + }); +});