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
11 changes: 11 additions & 0 deletions VENDORED.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,17 @@ can re-invert is now persisted as-is; anything else collapses to a stable
bad tool-call name fails that turn cleanly instead of wedging the room.
`@intx/inference` is added to `vendor/intx/hub-sessions`'s own
`package.json` dependencies for this.
`vendor/intx/hub-sessions` (CL-6595) fixes `workflow-run-kind.ts`'s
newly-terminal detection, which skipped a run's `events.jsonl` subtree
entirely (`enumerateEventBlobs` only walks per-event `<seq>.json` files),
so a run sealed from birth — its whole event log arriving pre-combined in
one push, with no per-event blobs ever landing — never fired `markTerminal`
and stayed "running" in `workflow_run.status` forever despite the run
having genuinely finished; `validatePush` now also scans a newly-sealed
run's combined log for its terminal event, and `hub-session-lookups.ts`
gained a same-push defense-in-depth backfill via the new
`readCommittedWorkflowRunTerminalStatus` export, in case a future pack still
slips past the primary detection.
Each package's `VENDORED-FROM` file restates its own delta.

`apps/sidecar` records `b5580a02` (v0.3.0): the fork tracks the
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/pages/mission-control-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -508,16 +508,16 @@ export function MissionControlRoute({
) : null}
{insightsActivity.kind === "ready" ? (
<p className="mission-control-week-summary">
{priorDays
{days
.reduce((sum, day) => sum + day.turns, 0)
.toLocaleString()}{" "}
runs ·{" "}
{formatUsd(
priorDays.some((day) =>
days.some((day) =>
day.byModel.some((model) => model.costUsd === null),
)
? null
: priorDays.reduce(
: days.reduce(
(sum, day) =>
sum +
day.byModel.reduce(
Expand Down
77 changes: 77 additions & 0 deletions apps/web/test/mission-control-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,49 @@ function stubEmptyBenchFetch(): void {
}) as typeof fetch;
}

function stubBenchFetchWithActivity(
days: {
day: string;
turns: number;
tokens: number;
byModel: { model: string; tokens: number; costUsd: number | null }[];
}[],
): void {
globalThis.fetch = ((input: RequestInfo | URL) => {
const url = typeof input === "string" ? input : input.toString();
if (url.includes("/approvals/needs-you")) {
return Promise.resolve(
new Response(JSON.stringify({ items: [] }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
}
if (url.includes("/insights/activity")) {
return Promise.resolve(
new Response(JSON.stringify({ days }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
}
if (url.includes("/agent-definitions/visible")) {
return Promise.resolve(
new Response(JSON.stringify({ definitions: [] }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
}
return Promise.resolve(
new Response(JSON.stringify({ items: [], data: [], nextCursor: null }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
}) as typeof fetch;
}

describe("MissionControlRoute", () => {
let container: HTMLDivElement | null = null;
let root: Root | null = null;
Expand Down Expand Up @@ -192,4 +235,38 @@ describe("MissionControlRoute", () => {
expect(container.textContent).toContain("Nothing running right now");
expect(container.textContent).toContain("Nothing recent yet.");
});

test("This week's run count includes today, so it is never less than runs today (CL-6667)", async () => {
const todayKey = new Date().toISOString().slice(0, 10);
const yesterdayKey = new Date(Date.now() - 24 * 60 * 60 * 1000)
.toISOString()
.slice(0, 10);
stubBenchFetchWithActivity([
{ day: yesterdayKey, turns: 4, tokens: 100, byModel: [] },
{ day: todayKey, turns: 10, tokens: 200, byModel: [] },
]);
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root?.render(
<TestQueryProvider>
<NavigationProvider navigate={() => undefined}>
<BenchContext.Provider value={benchState}>
<MissionControlRoute navigate={() => undefined} />
</BenchContext.Provider>
</NavigationProvider>
</TestQueryProvider>,
);
});
for (let count = 0; count < 5; count += 1) {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
}
// Today (10) + yesterday (4) = 14. A "This week" that silently excludes
// today would show 4, which is less than "Runs today" (10) -- the
// logical impossibility CL-6667 reported.
expect(container.textContent).toContain("14 runs");
});
});
2 changes: 1 addition & 1 deletion scripts/checks/kill-dates.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
apps/sidecar | sawyer | 2026-09-19
vendor/intx/db | sawyer | 2026-09-19 | 642b29735a7e36a1d3decdf8af26c33fa91e0989720323addb4c2f786ae88a18
vendor/intx/hub-api | sawyer | 2026-09-19 | 8449bf150e0253ca5d5eb26450c3ce2e5ff81116a02cdf2ba049b26453cfa201
vendor/intx/hub-sessions | sawyer | 2026-09-19 | ac63efef43d612a610af470a632298a3358fff6f9400cf4ce4b4fa431ef10015
vendor/intx/hub-sessions | sawyer | 2026-09-19 | df5f1d275e197668851c53f58c51182d8bc455f3585e2ed65784d3687a856001
vendor/intx/workflow | sawyer | 2026-09-19 | 34628e7bbd0587f131a07e3a206141983881106963a20ab607e68aeed1135593
vendor/intx/workflow-deploy | sawyer | 2026-09-19 | 95711adf282180852b0daec1cac39d00a4dc24aff15f9a515e07eb3d2ca749f9
vendor/intx/workflow-host | sawyer | 2026-09-19 | 6e6717e784cc55035a595320b2b8e6ea01b49ac42d4c77b444a0dc59e354b8d0
Expand Down
1 change: 1 addition & 0 deletions vendor/intx/hub-sessions/VENDORED-FROM
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Local modifications: exports map repointed from the upstream intx-src condition
CL-6388: `deployCodeSourcedWorkflow` now INSERTs its anchor `workflow_run` row BEFORE emitting the source-ref deploy frame (publicKey null until the ack stamps it; a failed emit deletes the row). Upstream's frame-then-insert ordering let the spawned child's first refs/heads/events pack push race the deploy ack, and receiveWorkflowRunPack fails closed (path_violation) on the missing anchor row, so every fresh deployment's first events pack was rejected and the durable event log never bootstrapped.
CL-6395: CL-6388's "a failed emit deletes the row" was too broad — any rejection from `emitSourceRefDeployFrame`, including an ack-timeout or socket-drop that fires strictly AFTER the `agent.deploy` frame already reached the sidecar, deleted the anchor row and permanently orphaned an already-spawned child on the missing-anchor `path_violation` path. `ws/sidecar-handler.ts` now exports `DeployFrameNotSentError`, thrown only by a guard clause that runs before `conn.send()` or by `conn.send()` itself throwing synchronously — the sole cases that provably never reached the wire; every other deploy rejection (timeout, disconnect, reconnect takeover, ack-processing failure) is raised through the pending-deploy's `reject()`, which by construction only fires after the send. `deployCodeSourcedWorkflow` deletes the pre-inserted row only on `DeployFrameNotSentError`; any other failure keeps the row and logs one reconciliation line. Also corrects an overclaiming comment in hub-session-lookups.ts: `markTerminal`'s null return means no row in a LIVE status (`deployed` or `running`) matched, not specifically "running".
CL-6478: `event-collector.ts`'s `tool_call` handling in `handleInferenceDone` now runs `block.name` through a new `sanitize-tool-name.ts` module before persisting it. `@intx/inference`'s `decodeToolName` is deliberately total — a hallucinated or provider-mangled function name is returned verbatim rather than throwing — but `encodeToolName` throws when that same name is later put back on the wire to build the next turn's outbound request, so persisting a decoded name unchecked wedged the room forever once the bad name was durable. `sanitizeToolNameForPersistence` round-trips the name through `encodeToolName` before it is written; a name that cannot be re-encoded collapses to a stable `malformed_tool_call` placeholder instead. `@intx/inference` is added to this package's own `package.json` dependencies for the check.
CL-6595: `workflow-run-kind.ts`'s newly-terminal detection in `validatePush` only ever scanned a run's per-event `runs/<runId>/events/<seq>.json` blobs; `enumerateEventBlobs` explicitly skips a run whose events already live in a combined `events.jsonl` (`hasCombined` -> `continue`), so a run sealed from birth — its entire event log, including the terminal event, arriving pre-combined in a single push with no per-event blobs ever landing — was never surfaced as newly terminal and `markTerminal` never fired, leaving `workflow_run.status` stuck live forever despite the run having genuinely finished. `validatePush` now also walks `validateCombinedEventRuns`' `combinedRunIds` and reports a newly-sealed run (absent from the prior tree's combined form) as terminal by reading its combined log's last (terminal, by `checkCombinedStructure`'s own invariant) event. A new `readCommittedWorkflowRunTerminalStatus` export mirrors `readCommittedWorkflowRunLifecycle` but returns the mapped `workflow_run.status` value instead of just live/terminal/absent; `hub-session-lookups.ts`'s pack-receive path now calls it as a same-push defense-in-depth backfill (calling `markTerminal` directly) whenever the committed log proves a run terminal, independent of whether the primary per-push detection caught it.
40 changes: 39 additions & 1 deletion vendor/intx/hub-sessions/src/hub-session-lookups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import {
listAcceptedWorkflowDispatches,
listConsumedWorkflowDispatches,
} from "./workflow-dispatch-settlement";
import { readCommittedWorkflowRunLifecycle } from "./workflow-run-kind";
import {
readCommittedWorkflowRunLifecycle,
readCommittedWorkflowRunTerminalStatus,
} from "./workflow-run-kind";

const logger = getLogger(["hub", "lookups"]);

Expand Down Expand Up @@ -730,6 +733,41 @@ export function createHubSessionLookups(
} catch (error) {
logger.error`Failed to close unsettled workflow dispatches for terminal run ${anchorAddress}: ${error instanceof Error ? error.message : String(error)}`;
}

// Defense in depth for CL-6595: the committed Git log just proved
// this run terminal, independent of whether the newly-terminal
// detection above caught it on this pack (or any earlier one). If
// `workflow_run.status` is still live, self-heal it here rather than
// leaving every future reader to hit the same stale column.
try {
const reads = await agentRepoStore.repoStore.openCommittedReads(
{ kind: "hub" },
repoId,
ref,
);
const status = await readCommittedWorkflowRunTerminalStatus(
reads,
anchor.id,
);
if (status !== null) {
const won = await db.transaction((tx) =>
workflowRunStore.markTerminal(anchor.id, status, now, tx),
);
if (won !== null && won.principalId !== null) {
await db
.update(principal)
.set({ status: "deactivated", updatedAt: now })
.where(
and(
eq(principal.id, won.principalId),
eq(principal.refId, anchor.id),
),
);
}
}
} catch (error) {
logger.error`Terminal-status backfill failed for run ${anchor.id}; workflow_run.status may still read live: ${error instanceof Error ? error.message : String(error)}`;
}
}

return { accepted: true };
Expand Down
1 change: 1 addition & 0 deletions vendor/intx/hub-sessions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export {
markConsumed,
readOwnedMessageIds,
readCommittedWorkflowRunLifecycle,
readCommittedWorkflowRunTerminalStatus,
readWorkflowRunLifecycle,
replayProcessingToInbox,
WORKFLOW_RUN_GITIGNORE_PATH,
Expand Down
Loading
Loading