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 .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"@corbits/icons",
"@corbits/inbox",
"@corbits/inference-settings",
"@corbits/insights",
"@corbits/jimmy-agent",
"@corbits/longevity-sim",
"@corbits/memory-hub",
Expand Down
2 changes: 1 addition & 1 deletion apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@corbits/granola-call-workflow": "workspace:*",
"@corbits/hub-api-client": "workspace:*",
"@corbits/inbox": "workspace:*",
"@corbits/insights": "workspace:*",
"@corbits/inference-catalog": "workspace:*",
"@corbits/last-30-days-research-workflow": "workspace:*",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#fea6211e42fddfc87e8e1755c219c437e45a7d62",
"@corbits/mcp-tools": "workspace:*",
Expand Down
125 changes: 9 additions & 116 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,6 @@ import {
createInboxRoutes,
WORKBENCH_MAILBOX_VOCABULARY,
} from "@corbits/inbox";
import {
applyInsightsMigrations,
createDrizzleRunTraceReader,
createDrizzleTurnTextSnapshotReader,
createInsightsRoutes,
createPostgresTurnLatencyStore,
createPostgresUsageStore,
createTurnLatencyTracker,
createUsageSink,
withTurnPartPersistGuard,
} from "@corbits/insights";
import { generateId } from "@intx/hub-common";

import {
Expand Down Expand Up @@ -244,6 +233,7 @@ import {
} from "./config";
import type { SidecarProvisioner } from "@intx/hub-sessions";
import { withTurnPartWriteDefaults } from "./turn-part-content-default";
import { createDrizzleTurnTextSnapshotReader } from "./turn-text-snapshot";
import { createBootAssetWiring, REGISTRIES } from "./asset-service-factory";
import {
runNowScheduledDefinition,
Expand Down Expand Up @@ -715,34 +705,8 @@ export async function createHub(config: HubConfig) {
},
) => void;
} = {};
// Package-owned insights tables, migrated ahead of the event collector
// registry so `usageSink` is live before the first `inference.usage`
// event can arrive.
await applyInsightsMigrations(config.databaseUrl);
const insightsUsage = createPostgresUsageStore(config.databaseUrl);
const insightsLatency = createPostgresTurnLatencyStore(config.databaseUrl);
const usageSink = createUsageSink({
store: insightsUsage.store,
generateId: () => generateId("inferenceTurn"),
});
// CL-6257: per-message-run stage latency (message-received →
// reactor.start → inference.start → first-token → reply-posted). The
// vendored event collector never persists the events this reads (see
// @corbits/insights' latency-tracker.ts header) and isn't ours to edit,
// so this observes the same InferenceEvent stream from outside it by
// wrapping `eventCollectors` below — the same seam `withTurnPartPersistGuard`
// already uses on the `db` handle passed into the vendored registry.
const turnLatency = createTurnLatencyTracker({
store: insightsLatency.store,
generateId: () => generateId("inferenceTurn"),
});
const baseEventCollectors = createEventCollectorRegistry({
// `withTurnPartPersistGuard` (see @corbits/insights) wraps
// `withTurnPartWriteDefaults`: it retries a turn_part insert once on
// the collector's known turn_id/session_id FK race and makes any
// surviving loss loud (error-level cause, counted) instead of a
// swallowed WRN.
db: withTurnPartPersistGuard(withTurnPartWriteDefaults(db)),
db: withTurnPartWriteDefaults(db),
// CL-7418: deliberately no terminal-status settle here. The folded
// routine-fire settle (CL-6778, keyed off routines.routine_run) died
// with the routines cut (CL-4455) and has no native equivalent: a
Expand All @@ -757,59 +721,22 @@ export async function createHub(config: HubConfig) {
onTurnFinalized: (agentAddress, turn) => {
artifactDeliveryHandlerRef.current?.(agentAddress, turn);
},
// Per-turn usage, emitted once when the collector finalizes a turn.
onUsage: (_agentAddress, usage) => {
void usageSink
.handle({
turnId: usage.turnId,
tenantId: usage.tenantId,
sessionId: usage.sessionId,
provider: usage.provider,
model: usage.model,
tokens: usage.usage,
})
.catch((err: unknown) => {
log.warn`Failed to record usage for turn ${usage.turnId}: ${err instanceof Error ? err.message : String(err)}`;
});
},
});
// Wraps every `EventCollectorRegistry` call the vendored session
// orchestrator makes: `create`/`dispatch`/`abandon` also feed
// `turnLatency`, which is the only place tenantId/sessionId land
// against an agentAddress for the raw event stream (the registry keeps
// that mapping private). Every other method passes straight through.
// Wraps `dispatch` for CL-7480: a run's turn events can start arriving
// before anything else ever recorded its session — the first inbound
// trigger that just reconciled its principal races this same dispatch.
// No live collector for the address is exactly that case: ensure the
// session (and, inside it, the collector) before delegating, rather
// than silently dropping the event for a collector that never gets
// created. Every other method passes straight through.
const eventCollectors: EventCollectorRegistry = {
...baseEventCollectors,
create(agentAddress, tenantId, sessionId, runId) {
turnLatency.onSessionCreate(agentAddress, tenantId, sessionId);
baseEventCollectors.create(agentAddress, tenantId, sessionId, runId);
},
dispatch(agentAddress, event) {
// CL-7480: a run's turn events can start arriving before anything
// else ever recorded its session — the first inbound trigger that
// just reconciled its principal races this same dispatch. No live
// collector for the address is exactly that case: ensure the
// session (and, inside it, the collector) before delegating,
// rather than silently dropping the event for a collector that
// never gets created.
if (!baseEventCollectors.has(agentAddress)) {
void ensureCollectorThenDispatch(agentAddress, event);
return;
}
turnLatency.onEvent(agentAddress, event);
baseEventCollectors.dispatch(agentAddress, event);
// Mirrors the registry's own `isTerminal` check (event-collector-registry.ts)
// so `turnLatency`'s per-agentAddress session map is cleared on the
// same terminal events that make the registry drop its own collector
// — otherwise a session that ends without `abandon()` never frees.
const isTerminal =
event.type === "reactor.done" ||
(event.type === "reactor.error" && event.data.fatal);
if (isTerminal) turnLatency.onSessionEnd(agentAddress);
},
abandon(agentAddress) {
turnLatency.onSessionEnd(agentAddress);
baseEventCollectors.abandon(agentAddress);
},
};
eventCollectorsRef.current = eventCollectors;
Expand All @@ -835,12 +762,7 @@ export async function createHub(config: HubConfig) {
extra: { agentAddress },
});
}
turnLatency.onEvent(agentAddress, event);
baseEventCollectors.dispatch(agentAddress, event);
const isTerminal =
event.type === "reactor.done" ||
(event.type === "reactor.error" && event.data.fatal);
if (isTerminal) turnLatency.onSessionEnd(agentAddress);
}
createHubSessionOrchestrator({
events: sidecarRouter.events,
Expand Down Expand Up @@ -1626,33 +1548,6 @@ export async function createHub(config: HubConfig) {
`${TENANT_PREFIX}/inbox`,
createInboxRoutes({ db: mailboxDb, bus: mailboxBus }),
);
// Insights usage sink + read API. Package-owned tables are migrated
// at hub start (idempotent ledger); the store is Postgres-backed so
// numbers survive restarts. Absent rates / pre-sink history stay null.
// runTraceReader reads the platform's own workflow_run /
// inference_turn / turn_part tables directly
// (see @corbits/insights' createDrizzleRunTraceReader) — no new storage,
// same `db` handle every other platform-table reader in this file uses.
// The sink itself is constructed earlier, alongside `eventCollectors`
// (see the `onUsage` hook on `createEventCollectorRegistry` above),
// which reports each finalized turn's usage with its run identity.
app.route(
`${TENANT_PREFIX}/insights`,
createInsightsRoutes({
store: insightsUsage.store,
requireGrant: createRequireGrant({
grantStore: chatGrantStore,
conditionRegistry: grantConditionRegistry,
}),
runTraceReader: createDrizzleRunTraceReader(db),
latencyStore: insightsLatency.store,
// Same `db` handle every other platform-table reader in this file
// uses — lets /usage, /activity, /tools, and /scope roll up a
// workspace parent's child chats (see resolveScope in
// @corbits/insights' routes.ts).
db,
}),
);
// A workflow definition's own detail page (CL-7371): what it is,
// whether it can run right now, its steps, and its access surface.
// Mounted alongside — not inside — the vendored
Expand Down Expand Up @@ -2287,8 +2182,6 @@ export async function createHub(config: HubConfig) {
chatOrchestrator.dispose();
inboxUnsnoozeSweep.stop();
cronEmitter.stop();
await insightsUsage.close();
await insightsLatency.close();
await closeMailbox();
// The pool end waits on in-flight queries; a query whose socket
// died with the process must never stall shutdown, so bound it.
Expand Down
1 change: 0 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"@corbits/error-sink": "workspace:*",
"@corbits/inbox": "workspace:*",
"@corbits/inference-settings": "workspace:*",
"@corbits/insights": "workspace:*",
"@corbits/plugins-ui": "workspace:*",
"@corbits/presence": "workspace:*",
"@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635",
Expand Down
43 changes: 3 additions & 40 deletions apps/web/src/insights-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,12 @@
import { describe, expect, test } from "bun:test";

import {
createInsightsWindow,
INSIGHTS_WINDOW_DAYS,
} from "@corbits/insights/client";

import {
insightsActivityPath,
insightsToolsPath,
insightsTopLevelRunsPath,
insightsUsagePath,
} from "./insights-api";

// Fixed clock so range math is deterministic regardless of suite time.
const NOW = new Date("2026-01-15T18:00:00.000Z");

describe("insights path range contract", () => {
test("usage, activity, and tools share the same from/to", () => {
const range = createInsightsWindow(INSIGHTS_WINDOW_DAYS, NOW);
const usage = insightsUsagePath("tenant-1", range);
const activity = insightsActivityPath("tenant-1", range);
const tools = insightsToolsPath("tenant-1", range);

for (const path of [usage, activity, tools]) {
const url = new URL(path, "http://local");
expect(url.searchParams.get("from")).toBe(range.from);
expect(url.searchParams.get("to")).toBe(range.to);
}

expect(usage.startsWith("/api/tenants/tenant-1/insights/usage?")).toBe(
true,
);
expect(
activity.startsWith("/api/tenants/tenant-1/insights/activity?"),
).toBe(true);
expect(tools.startsWith("/api/tenants/tenant-1/insights/tools?")).toBe(
true,
);
});
});
import { insightsTopLevelRunsPath } from "./insights-api";

describe("insightsTopLevelRunsPath", () => {
// 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`.
// `/me/workflows/runs`. CL-8160 deleted `packages/insights` and its
// usage/activity/tools/latency/scope routes entirely.
test("hits the native tenant-scoped GET /workflows/runs listing", () => {
const path = insightsTopLevelRunsPath("tenant-1");
expect(path.startsWith("/api/tenants/tenant-1/workflows/runs?")).toBe(true);
Expand Down
Loading
Loading