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 @@ -24,7 +24,6 @@
"@corbits/memory": "github:corbitsdev/corbits-memory#8be608becc749fa0795a899826277823dca6db3e",
"@corbits/oauth-core": "^0.3.0",
"@corbits/webhooks": "github:corbitsdev/webhooks#3f4f83147fd826d3aec5bcac7ef4b0ae7f75c00d",
"@corbits/workflows": "workspace:*",
"@corbits/xai-provider": "0.1.2",
"@intx/authz": "0.4.0",
"@intx/crypto": "0.4.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/hub/src/mailbox-persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { DB } from "@intx/db";
import { tenant as tenantTable } from "@intx/db/schema";
import type { AuthorizeMailboxSender, MailboxPersistArgs } from "@corbits/mailbox";
import { resolveRoutableAddress } from "@intx/hub-sessions";
import { ensureRunSession, type EventCollectorPort } from "@corbits/workflows";
import { ensureRunSession, type EventCollectorPort } from "./lazy-run-collector";
import { reportError } from "@corbits/error-sink";
import { getLogger } from "@intx/log";

Expand Down
2 changes: 1 addition & 1 deletion apps/hub/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
type SidecarProvisionerChooser,
type WsHandle,
} from "@intx/hub-sessions";
import { withLazyRunCollector } from "@corbits/workflows";
import { withLazyRunCollector } from "./lazy-run-collector";
import { generateKeyPair } from "@intx/crypto";
import { timeWindowEvaluator } from "@intx/authz";
import type { ConditionRegistry } from "@intx/types/authz";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"@corbits/mcp": "^0.3.0",
"@corbits/react-ui": "0.3.0",
"@corbits/worker": "workspace:*",
"@corbits/workflows": "workspace:*",
"@intx/mime": "workspace:*",
"@intx/types": "workspace:*",
"@isomorphic-git/lightning-fs": "4.7.0",
Expand All @@ -32,6 +31,7 @@
"@tanstack/react-query": "catalog:",
"arktype": "catalog:",
"buffer": "^6.0.3",
"cronstrue": "^3.24.0",
"isomorphic-git": "catalog:",
"lucide-react": "^1.49.0",
"react": "^19.2.0",
Expand Down
5 changes: 1 addition & 4 deletions apps/web/src/agent-source-read.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// No stock file-read route exists for a workflow asset (only
// package-registry tarballs get one), so this fetches `main` over the
// asset's smart-HTTP git remote with a short-lived read-only token.
import {
parseWorkflowSourceDefinition,
WORKFLOW_SOURCE_DEFINITION_PATH,
} from "@corbits/workflows/client";
import { parseWorkflowSourceDefinition, WORKFLOW_SOURCE_DEFINITION_PATH } from "./workflow-source";
import { MCP_TOOLS_PACKAGE } from "@corbits/worker/workflow-ids";
import { type } from "arktype";

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/chat/message-attachments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// agent never calls the hub; anything else is a plain file list.

import { Button } from "@corbits/react-ui";
import { cronSentence } from "@corbits/workflows/client";
import { cronSentence } from "../cron-sentence";

import { WarningCircle } from "@/lib/icons";

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/insights-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// routines). No new analytics backend — I1 is an honest live surface on
// existing endpoints.

import { runOutcomeStatus, withListingAbandoned } from "@corbits/workflows/client";
import { runOutcomeStatus, withListingAbandoned } from "./run-outcome";

import { isAgentDeploySourceAssetName } from "./agent-deploy";
import type { Approval } from "./api";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/bench-insights.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// stock `GET /workflows/runs` listing of the bench's own tenant.

import { RichEmptyState, Skeleton } from "@corbits/react-ui";
import { cronSentence } from "@corbits/workflows/client";
import { cronSentence } from "../cron-sentence";
import { useQueries, useQuery } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/insights-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
type RunStatus,
} from "@corbits/react-ui";
import { ChartBar } from "@/lib/icons";
import { runOutcomeStatus, runStatusLabel, withListingAbandoned } from "@corbits/workflows/client";
import { runOutcomeStatus, runStatusLabel, withListingAbandoned } from "../run-outcome";
import type * as React from "react";
import { useState } from "react";

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/routines-page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Workflows: an ops table of deployed workflow definitions, including
// paused (`stopped`) ones. Run-now is the only write.
import { EmptyState, RichEmptyState, RunNowButton } from "@corbits/react-ui";
import { cronSentence } from "@corbits/workflows/client";
import { cronSentence } from "../cron-sentence";
import { useState, type ReactNode } from "react";
import { Clock } from "@/lib/icons";

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/workbench-schedules-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// only a deleted agent stops the row for good.

import { Button, Input, Select, Skeleton, formatRelativeTime } from "@corbits/react-ui";
import { cronSentence } from "@corbits/workflows/client";
import { cronSentence } from "../cron-sentence";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";

Expand Down
3 changes: 0 additions & 3 deletions apps/web/src/query-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,6 @@ export const tenantKeys = {
// Delegates to `@/chat`'s own key builder so every workbench-listing
// surface keys against one shape, not a copy that could drift apart.
workbenches: (tenantId: string) => workbenchesQueryKey(tenantId),
// The `feed=fires` route is gone; the key is kept so a future native
// fires equivalent has somewhere to rewire.
routineActivity: (tenantId: string) => ["tenant", tenantId, "routine-activity"] as const,
/** A workbench's own timeline reads: `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
82 changes: 82 additions & 0 deletions apps/web/src/run-outcome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// See docs/run-outcome-status.md.

type ListingTurn = {
readonly status: string;
readonly endedAt?: string | null;
};

type ListingRun = {
readonly createdAt: string;
readonly abandoned?: boolean;
readonly hasInFlightTurn?: boolean;
readonly turns?: readonly ListingTurn[];
readonly run?: Record<string, unknown>;
};

const FIRE_RUNNING_WINDOW_MS = 10 * 60 * 1000;

type InFlightSignal = "yes" | "no" | "unknown";

function isInFlightTurn(turn: unknown): boolean {
if (typeof turn !== "object" || turn === null) return false;
const status = "status" in turn ? turn.status : undefined;
if (status !== "running") return false;
const endedAt = "endedAt" in turn ? turn.endedAt : undefined;
return endedAt === undefined || endedAt === null || endedAt === "";
}

function listingInFlightSignal(listing: ListingRun): InFlightSignal {
const nested = Array.isArray(listing.run?.turns) ? listing.run.turns : undefined;
if (listing.hasInFlightTurn === true || listing.run?.hasInFlightTurn === true) return "yes";
if (listing.turns?.some(isInFlightTurn) === true || nested?.some(isInFlightTurn) === true) {
return "yes";
}
if (listing.turns !== undefined || nested !== undefined) return "no";
if (listing.hasInFlightTurn === false || listing.run?.hasInFlightTurn === false) return "no";
return "unknown";
}

function listingAbandoned(listing: ListingRun, now: number): boolean {
if (listingInFlightSignal(listing) !== "no") return false;
const startedAt = Date.parse(listing.createdAt);
return !Number.isNaN(startedAt) && now - startedAt > FIRE_RUNNING_WINDOW_MS;
}

export function withListingAbandoned<T extends ListingRun>(listing: T, now: number): T {
return listingAbandoned(listing, now) ? { ...listing, abandoned: true } : listing;
}

export function runOutcomeStatus(
run: {
readonly createdAt: string;
readonly status: string;
readonly endedAt?: string | null;
readonly abandoned?: boolean;
readonly hasInFlightTurn?: boolean;
readonly turns?: readonly ListingTurn[];
},
now: number,
): string | null {
const listing = withListingAbandoned(run, now);
if (listing.status !== "running") return listing.status;
if (typeof listing.endedAt === "string" && listing.endedAt !== "") return "completed";
if (listingInFlightSignal(listing) === "yes" || listing.abandoned !== true) return listing.status;
const startedAt = Date.parse(listing.createdAt);
if (Number.isNaN(startedAt)) return listing.status;
return now - startedAt > FIRE_RUNNING_WINDOW_MS ? "completed" : "running";
}

const RUN_STATUS_WORDS: Readonly<Record<string, string>> = {
running: "Running now",
updating: "Running now",
completed: "Finished",
failed: "Failed",
error: "Failed",
cancelled: "Cancelled",
queued: "Waiting to start",
pending: "Waiting to start",
};

export function runStatusLabel(status: string): string {
return RUN_STATUS_WORDS[status] ?? status;
}
17 changes: 2 additions & 15 deletions apps/web/src/shell/bench-activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { listWorkbenches, type Workbench } from "@/chat/workbench-tenants";

import { listAgentDefinitions, type AgentDefinition } from "../agents-api";
import { tenantKeys } from "../query-client";
import { listRoutineActivity } from "./routine-activity";
import type { RoutineActivityItem } from "./routine-activity";

export type BenchActivityQuery =
| { readonly kind: "loading" }
Expand All @@ -17,7 +15,6 @@ export type BenchActivityQuery =
readonly kind: "ready";
readonly workbenches: readonly Workbench[];
readonly agents: readonly AgentDefinition[];
readonly routines: readonly RoutineActivityItem[];
};

function errorMessage(cause: unknown): string {
Expand All @@ -36,11 +33,6 @@ export function useBenchActivity(tenantId: string | null): BenchActivityQuery {
enabled,
queryFn: () => listWorkbenches(key),
});
const routinesQuery = useQuery({
queryKey: tenantKeys.routineActivity(key),
enabled,
queryFn: () => listRoutineActivity(),
});
const agentsQuery = useQuery({
queryKey: tenantKeys.visibleAgents(key),
enabled,
Expand All @@ -49,21 +41,16 @@ export function useBenchActivity(tenantId: string | null): BenchActivityQuery {

if (tenantId === null) return { kind: "empty" };

for (const query of [workbenchesQuery, routinesQuery, agentsQuery]) {
for (const query of [workbenchesQuery, agentsQuery]) {
if (query.isError) return { kind: "error", message: errorMessage(query.error) };
}
if (
workbenchesQuery.data === undefined ||
routinesQuery.data === undefined ||
agentsQuery.data === undefined
) {
if (workbenchesQuery.data === undefined || agentsQuery.data === undefined) {
return { kind: "loading" };
}

return {
kind: "ready",
workbenches: workbenchesQuery.data,
agents: agentsQuery.data,
routines: routinesQuery.data,
};
}
18 changes: 0 additions & 18 deletions apps/web/src/shell/routine-activity.ts

This file was deleted.

2 changes: 1 addition & 1 deletion apps/web/src/worker-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
WorkerWorkflowInput,
} from "@corbits/worker/definition-json";
import { WORKER_WORKFLOW_ID } from "@corbits/worker/workflow-ids";
import { renderBundledWorkflowSourceTree } from "@corbits/workflows/client";
import { renderBundledWorkflowSourceTree } from "./workflow-source";
import { type } from "arktype";

import { ensureAgentHubCredential } from "./agent-hub-credential";
Expand Down
65 changes: 65 additions & 0 deletions apps/web/src/workflow-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const WORKFLOW_SOURCE_ENTRY_PATH = "workflow.js";
const WORKFLOW_SOURCE_ENTRY = `./${WORKFLOW_SOURCE_ENTRY_PATH}`;
const WORKFLOW_SOURCE_DIRECTORS_PATH = "directors.js";
const WORKFLOW_SOURCE_DIRECTORS = `./${WORKFLOW_SOURCE_DIRECTORS_PATH}`;
const WORKFLOW_SOURCE_MANIFEST_PATH = "package.json";
export const WORKFLOW_SOURCE_DEFINITION_PATH = "definition.json";

function manifestFor(packageName: string): string {
return `${JSON.stringify(
{
name: packageName,
version: "0.0.0",
private: true,
type: "module",
interchange: {
workflow: WORKFLOW_SOURCE_ENTRY,
directors: WORKFLOW_SOURCE_DIRECTORS,
},
},
null,
2,
)}\n`;
}

export function renderBundledWorkflowSourceTree(args: {
packageName: string;
bundle: string;
directorsBundle: string;
buildExport: string;
buildInput: unknown;
workflowJson: string;
}): Readonly<Record<string, string>> {
const call = `${args.buildExport}(${JSON.stringify(args.buildInput)})`;
return {
[WORKFLOW_SOURCE_MANIFEST_PATH]: manifestFor(args.packageName),
[WORKFLOW_SOURCE_ENTRY_PATH]: `${args.bundle}\nexport default ${call};\n`,
[WORKFLOW_SOURCE_DIRECTORS_PATH]: args.directorsBundle,
[WORKFLOW_SOURCE_DEFINITION_PATH]: `${args.workflowJson}\n`,
};
}

class RetiredWorkflowEnvelopeError extends Error {
readonly assetId: string;

constructor(assetId: string, options?: { cause?: unknown }) {
super(
`Asset "${assetId}" carries no ${WORKFLOW_SOURCE_DEFINITION_PATH} beside its ` +
`${WORKFLOW_SOURCE_ENTRY_PATH} source entry. Re-author and re-deploy the definition ` +
`to write its source tree; nothing can read or edit it until then.`,
options,
);
this.name = "RetiredWorkflowEnvelopeError";
this.assetId = assetId;
}
}

export function parseWorkflowSourceDefinition(definitionJson: string, assetId: string): string {
const trimmed = definitionJson.trim();
try {
JSON.parse(trimmed);
} catch (cause) {
throw new RetiredWorkflowEnvelopeError(assetId, { cause });
}
return trimmed;
}
Loading
Loading