Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
18 changes: 0 additions & 18 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
61 changes: 12 additions & 49 deletions apps/web/src/agents-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down Expand Up @@ -153,44 +136,24 @@ 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.
* 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,
): Promise<readonly AgentInstance[]> {
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<readonly RunFire[]> {
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
Expand Down Expand Up @@ -467,9 +430,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,
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/agents-directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ 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
* shared name-based workbench-host filter as defense in depth.
* (workbench host, invited agent, task) server-side — the native
* `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[],
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/insights-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
59 changes: 38 additions & 21 deletions apps/web/src/insights-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WorkflowRunResponse & { routineId, routineName }>
// (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<WorkflowRunResponse>
// (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";
Expand Down Expand Up @@ -230,44 +231,60 @@ 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
* them is not "no in-flight turn".
*/
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`) —
// same limit `agents-api.ts`'s `listTopLevelRuns` uses for this route.
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
* `anchorRunId IS NULL` filter never matches, because every addressed run
* self-anchors at creation, so that feed always came back empty.
* 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. 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; the resident,
* never-triggered deployment placeholder (`status: "deployed"`) is now
* included — `computeInsightsStats` counts it as deployed rather than
* 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}/top-level-runs?limit=${TOP_LEVEL_RUNS_LIMIT}&feed=fires`;
return `/api/tenants/${tenantId}/workflows/runs?limit=${TOP_LEVEL_RUNS_LIMIT}`;
}
27 changes: 27 additions & 0 deletions apps/web/src/insights-stats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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", () => {
Expand Down
42 changes: 24 additions & 18 deletions apps/web/src/insights-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -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;
Expand All @@ -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. */
Expand All @@ -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[],
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/mission-control-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ export function MissionControlRoute({
inFlightRows.length === 0 ? (
<RichEmptyState
title="Nothing running right now"
description="Agent runs and routine fires will show up here while they're in progress."
description="Routine activity has no feed in this build (CL-8087 deleted feed=fires with no native equivalent), so this stays empty until one lands."
/>
) : null}
{inFlightRows.length > 0 ? (
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/query-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Loading
Loading