Skip to content

Commit d5eebf0

Browse files
authored
fix(workboard): record resolved runtime metadata instead of hardcoded codex engine (#108887)
* fix(workboard): record resolved runtime metadata instead of hardcoded codex engine Workboard executions labeled every dispatched run engine=codex, model=default, and id suffix :codex even for Claude/other harness agents. The gateway agent admission phase now returns the resolved {harness, provider, model} for plugin subagent runs; the dispatcher records it verbatim and omits engine/model when unresolved. Engine becomes an open runtime identifier in the workboard contract (built-in launch choices stay a closed list), store/UI normalizers preserve historical labels as written instead of inventing codex, and new execution ids use an :agent-session suffix. Fixes #108362 * fix(workboard): accept undefined engine in ui engineModel helper
1 parent f538add commit d5eebf0

16 files changed

Lines changed: 331 additions & 71 deletions

File tree

extensions/workboard/src/dispatcher.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,61 @@ function createMemoryStore<T = PersistedWorkboardCard>(): WorkboardKeyedStore<T>
2424
}
2525

2626
describe("dispatchAndStartWorkboardCards", () => {
27+
it("persists the resolved subagent runtime on new executions", async () => {
28+
const store = new WorkboardStore(createMemoryStore());
29+
const card = await store.create({
30+
title: "Claude worker",
31+
status: "ready",
32+
workspaceAccess: { unrestricted: true },
33+
});
34+
const run = vi.fn().mockResolvedValue({
35+
runId: "run-claude",
36+
runtime: {
37+
harness: "claude-cli",
38+
provider: "anthropic",
39+
model: "claude-sonnet-4-6",
40+
},
41+
});
42+
43+
await dispatchAndStartWorkboardCards({
44+
store,
45+
subagent: { run },
46+
options: { now: 10, maxStarts: 1 },
47+
});
48+
49+
await expect(store.get(card.id)).resolves.toMatchObject({
50+
execution: {
51+
id: `${card.id}:agent-session`,
52+
engine: "claude-cli",
53+
model: "anthropic/claude-sonnet-4-6",
54+
runId: "run-claude",
55+
},
56+
});
57+
});
58+
59+
it("omits unresolved runtime metadata instead of labeling it codex", async () => {
60+
const store = new WorkboardStore(createMemoryStore());
61+
const card = await store.create({
62+
title: "Unknown runtime worker",
63+
status: "ready",
64+
workspaceAccess: { unrestricted: true },
65+
});
66+
67+
await dispatchAndStartWorkboardCards({
68+
store,
69+
subagent: { run: vi.fn().mockResolvedValue({ runId: "run-unknown" }) },
70+
options: { now: 10, maxStarts: 1 },
71+
});
72+
73+
const execution = (await store.get(card.id))?.execution;
74+
expect(execution).toMatchObject({
75+
id: `${card.id}:agent-session`,
76+
runId: "run-unknown",
77+
});
78+
expect(execution).not.toHaveProperty("engine");
79+
expect(execution).not.toHaveProperty("model");
80+
});
81+
2782
it("materializes managed worktrees, supplies cwd, persists them, and cleans up on run end", async () => {
2883
const store = new WorkboardStore(createMemoryStore());
2984
const card = await store.create({

extensions/workboard/src/dispatcher.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424

2525
const DEFAULT_DISPATCH_MAX_STARTS = 3;
2626
const DEFAULT_DISPATCH_OWNER = "workboard-dispatcher";
27-
const DEFAULT_DISPATCH_MODEL = "default";
2827

2928
export type WorkboardSubagentRuntime = Pick<PluginRuntime["subagent"], "run">;
3029
export type WorkboardWorktreeRuntime = PluginRuntime["worktrees"];
@@ -94,16 +93,20 @@ function buildExecution(params: {
9493
card: WorkboardCard;
9594
sessionKey: string;
9695
runId: string;
97-
model: string;
96+
runtime: Awaited<ReturnType<WorkboardSubagentRuntime["run"]>>["runtime"];
9897
now: number;
9998
}): WorkboardExecution {
10099
return {
101-
id: params.card.execution?.id ?? `${params.card.id}:codex`,
100+
id: params.card.execution?.id ?? `${params.card.id}:agent-session`,
102101
kind: "agent-session",
103-
engine: "codex",
104102
mode: "autonomous",
105103
status: "running",
106-
model: params.model,
104+
...(params.runtime
105+
? {
106+
engine: params.runtime.harness,
107+
model: `${params.runtime.provider}/${params.runtime.model}`,
108+
}
109+
: {}),
107110
sessionKey: params.sessionKey,
108111
runId: params.runId,
109112
startedAt: params.now,
@@ -272,7 +275,6 @@ export async function dispatchAndStartWorkboardCards(params: {
272275
);
273276
const started: WorkboardStartedRun[] = [];
274277
const startFailures: WorkboardStartFailure[] = [];
275-
const model = params.options?.model?.trim() || DEFAULT_DISPATCH_MODEL;
276278
const cards = await params.store.list();
277279
const candidates = await params.store.list({ boardId });
278280

@@ -430,7 +432,7 @@ export async function dispatchAndStartWorkboardCards(params: {
430432
card: claimed.card,
431433
sessionKey,
432434
runId: run.runId,
433-
model,
435+
runtime: run.runtime,
434436
now,
435437
}),
436438
...(materializedWorkspace ? { workspace: materializedWorkspace } : {}),

extensions/workboard/src/sqlite-store.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,10 +472,12 @@ function readExecution(row: Row): WorkboardExecution | undefined {
472472
return {
473473
id,
474474
kind: "agent-session",
475-
engine: requiredString(row, "execution_engine") as WorkboardExecution["engine"],
476475
mode: requiredString(row, "execution_mode") as WorkboardExecution["mode"],
477476
status: requiredString(row, "execution_status") as WorkboardExecution["status"],
478-
model: requiredString(row, "execution_model"),
477+
...(stringValue(row, "execution_engine")
478+
? { engine: stringValue(row, "execution_engine") }
479+
: {}),
480+
...(stringValue(row, "execution_model") ? { model: stringValue(row, "execution_model") } : {}),
479481
...(stringValue(row, "execution_session_key")
480482
? { sessionKey: stringValue(row, "execution_session_key") }
481483
: {}),

extensions/workboard/src/store-card-helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ export function syncExecutionAttemptMetadata(
8484
id: existingAttempt?.id ?? key,
8585
status: attemptStatus,
8686
startedAt: existingAttempt?.startedAt ?? execution.startedAt,
87-
engine: execution.engine,
8887
mode: execution.mode,
89-
model: execution.model,
88+
...(execution.engine ? { engine: execution.engine } : {}),
89+
...(execution.model ? { model: execution.model } : {}),
9090
...(execution.sessionKey ? { sessionKey: execution.sessionKey } : {}),
9191
...(execution.runId ? { runId: execution.runId } : {}),
9292
...(attemptStatus !== "running" && { endedAt: execution.updatedAt || now }),

extensions/workboard/src/store-normalizers.ts

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
WORKBOARD_DIAGNOSTIC_KINDS,
66
WORKBOARD_DIAGNOSTIC_SEVERITIES,
77
WORKBOARD_EVENT_KINDS,
8-
WORKBOARD_EXECUTION_ENGINES,
98
WORKBOARD_EXECUTION_MODES,
109
WORKBOARD_EXECUTION_STATUSES,
1110
WORKBOARD_LINK_TYPES,
@@ -28,7 +27,6 @@ import {
2827
type WorkboardEvent,
2928
type WorkboardEventKind,
3029
type WorkboardExecution,
31-
type WorkboardExecutionEngine,
3230
type WorkboardExecutionMode,
3331
type WorkboardExecutionStatus,
3432
type WorkboardLink,
@@ -486,19 +484,6 @@ export function deriveChildIdempotencyKey(
486484
return key.length <= 160 ? key : undefined;
487485
}
488486

489-
function normalizeExecutionEngine(
490-
value: unknown,
491-
fallback: WorkboardExecutionEngine,
492-
): WorkboardExecutionEngine {
493-
if (
494-
typeof value === "string" &&
495-
WORKBOARD_EXECUTION_ENGINES.includes(value as WorkboardExecutionEngine)
496-
) {
497-
return value as WorkboardExecutionEngine;
498-
}
499-
return fallback;
500-
}
501-
502487
function normalizeExecutionMode(
503488
value: unknown,
504489
fallback: WorkboardExecutionMode,
@@ -630,16 +615,14 @@ function normalizeAttempt(value: unknown): WorkboardRunAttempt | null {
630615
const sessionKey = normalizeOptionalString(record.sessionKey);
631616
const runId = normalizeOptionalString(record.runId);
632617
const error = normalizeBoundedString(record.error, undefined, 800, "attempt error");
618+
const engine = normalizeBoundedString(record.engine, undefined, 160, "attempt engine");
633619
const model = normalizeBoundedString(record.model, undefined, 160, "attempt model");
634620
return {
635621
id,
636622
status: normalizeAttemptStatus(record.status, "running"),
637623
startedAt,
638624
...(endedAt ? { endedAt } : {}),
639-
...(typeof record.engine === "string" &&
640-
WORKBOARD_EXECUTION_ENGINES.includes(record.engine as WorkboardExecutionEngine)
641-
? { engine: record.engine as WorkboardExecutionEngine }
642-
: {}),
625+
...(engine ? { engine } : {}),
643626
...(typeof record.mode === "string" &&
644627
WORKBOARD_EXECUTION_MODES.includes(record.mode as WorkboardExecutionMode)
645628
? { mode: record.mode as WorkboardExecutionMode }
@@ -1128,24 +1111,27 @@ export function normalizeExecution(value: unknown): WorkboardExecution | undefin
11281111
}
11291112
const record = value as Record<string, unknown>;
11301113
const now = Date.now();
1131-
const model = normalizeOptionalString(record.model);
1132-
const id = normalizeOptionalString(record.id) ?? randomUUID();
1133-
if (!model) {
1114+
// Preserve historical labels as written; old hardcoded "codex" rows cannot be inferred safely.
1115+
const engine = normalizeBoundedString(record.engine, undefined, 160, "execution engine");
1116+
const model = normalizeBoundedString(record.model, undefined, 160, "execution model");
1117+
const normalizedId = normalizeOptionalString(record.id);
1118+
const sessionKey = normalizeOptionalString(record.sessionKey);
1119+
const runId = normalizeOptionalString(record.runId);
1120+
if (!normalizedId && !engine && !model && !sessionKey && !runId) {
11341121
return undefined;
11351122
}
1123+
const id = normalizedId ?? randomUUID();
11361124
const startedAt = normalizeTimestamp(record.startedAt, now);
11371125
const updatedAt = normalizeTimestamp(record.updatedAt, startedAt);
1138-
const sessionKey = normalizeOptionalString(record.sessionKey);
1139-
const runId = normalizeOptionalString(record.runId);
11401126
return {
11411127
id,
11421128
kind: "agent-session",
1143-
engine: normalizeExecutionEngine(record.engine, "codex"),
11441129
mode: normalizeExecutionMode(record.mode, "autonomous"),
11451130
status: normalizeExecutionStatus(record.status, "idle"),
1146-
model,
11471131
startedAt,
11481132
updatedAt,
1133+
...(engine ? { engine } : {}),
1134+
...(model ? { model } : {}),
11491135
...(sessionKey ? { sessionKey } : {}),
11501136
...(runId ? { runId } : {}),
11511137
};
@@ -1167,6 +1153,12 @@ export function syncExecutionSessionKey(
11671153

11681154
function removeUndefinedExecutionFields(execution: WorkboardExecution): WorkboardExecution {
11691155
const next = { ...execution };
1156+
if (next.engine === undefined) {
1157+
delete next.engine;
1158+
}
1159+
if (next.model === undefined) {
1160+
delete next.model;
1161+
}
11701162
if (next.sessionKey === undefined) {
11711163
delete next.sessionKey;
11721164
}

extensions/workboard/src/store.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
WorkboardKeyedStore,
1414
} from "./persistence-types.js";
1515
import { createWorkboardSqliteStores } from "./sqlite-store.js";
16+
import { normalizeExecution } from "./store-normalizers.js";
1617
import { WorkboardStore } from "./store.js";
1718

1819
function createMemoryStore<T = PersistedWorkboardCard>(options?: {
@@ -163,6 +164,18 @@ describe("WorkboardStore", () => {
163164
updatedAt: 2,
164165
},
165166
});
167+
const unresolvedRuntimeCard = await store.create({
168+
title: "Persist unresolved runtime",
169+
boardId: board.id,
170+
execution: {
171+
id: "exec-unresolved",
172+
kind: "agent-session",
173+
mode: "autonomous",
174+
status: "running",
175+
startedAt: 3,
176+
updatedAt: 4,
177+
},
178+
});
166179
await store.addComment(card.id, { body: "round trip" });
167180
const attached = await store.addAttachment(card.id, {
168181
fileName: "proof.txt",
@@ -241,6 +254,14 @@ describe("WorkboardStore", () => {
241254
]),
242255
},
243256
});
257+
const reopenedUnresolvedRuntimeCard = await reopened.get(unresolvedRuntimeCard.id);
258+
expect(reopenedUnresolvedRuntimeCard?.execution).toMatchObject({
259+
id: "exec-unresolved",
260+
mode: "autonomous",
261+
status: "running",
262+
});
263+
expect(reopenedUnresolvedRuntimeCard?.execution).not.toHaveProperty("engine");
264+
expect(reopenedUnresolvedRuntimeCard?.execution).not.toHaveProperty("model");
244265
expect(await reopened.getAttachment(attachmentId ?? "")).toMatchObject({
245266
contentBase64: Buffer.from("ok").toString("base64"),
246267
});
@@ -368,6 +389,44 @@ describe("WorkboardStore", () => {
368389
expect(Object.hasOwn(entry?.card ?? {}, "metadata")).toBe(false);
369390
});
370391

392+
it("preserves open execution engine identifiers without rewriting historical labels", async () => {
393+
const store = new WorkboardStore(createMemoryStore());
394+
const runtimeCard = await store.create({
395+
title: "Runtime identity",
396+
execution: {
397+
id: "exec-runtime",
398+
kind: "agent-session",
399+
engine: "claude-cli",
400+
mode: "autonomous",
401+
status: "running",
402+
model: "anthropic/claude-sonnet-4-6",
403+
startedAt: 1,
404+
updatedAt: 1,
405+
},
406+
});
407+
const historicalCard = await store.create({
408+
title: "Historical identity",
409+
execution: {
410+
id: "exec-historical",
411+
kind: "agent-session",
412+
engine: "codex",
413+
mode: "autonomous",
414+
status: "running",
415+
model: "default",
416+
startedAt: 1,
417+
updatedAt: 1,
418+
},
419+
});
420+
421+
expect(runtimeCard.execution?.engine).toBe("claude-cli");
422+
expect(runtimeCard.metadata?.attempts?.[0]?.engine).toBe("claude-cli");
423+
expect(historicalCard.execution?.engine).toBe("codex");
424+
});
425+
426+
it("rejects empty execution records instead of fabricating lifecycle state", () => {
427+
expect(normalizeExecution({})).toBeUndefined();
428+
});
429+
371430
it("preserves explicit zero positions", async () => {
372431
const store = new WorkboardStore(createMemoryStore());
373432

packages/workboard-contract/src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const WORKBOARD_STATUSES = [
1212
] as const;
1313

1414
export const WORKBOARD_PRIORITIES = ["low", "normal", "high", "urgent"] as const;
15+
/** Built-in launch choices. Persisted execution engines remain an open runtime identifier. */
1516
export const WORKBOARD_EXECUTION_ENGINES = ["codex", "claude"] as const;
1617
export const WORKBOARD_EXECUTION_MODES = ["autonomous", "manual"] as const;
1718
export const WORKBOARD_EXECUTION_STATUSES = [
@@ -81,7 +82,7 @@ export function isValidWorkboardBoardId(value: unknown): value is string {
8182

8283
export type WorkboardStatus = (typeof WORKBOARD_STATUSES)[number];
8384
export type WorkboardPriority = (typeof WORKBOARD_PRIORITIES)[number];
84-
export type WorkboardExecutionEngine = (typeof WORKBOARD_EXECUTION_ENGINES)[number];
85+
export type WorkboardExecutionEngine = string;
8586
export type WorkboardExecutionMode = (typeof WORKBOARD_EXECUTION_MODES)[number];
8687
export type WorkboardExecutionStatus = (typeof WORKBOARD_EXECUTION_STATUSES)[number];
8788
export type WorkboardEventKind = (typeof WORKBOARD_EVENT_KINDS)[number];
@@ -96,10 +97,10 @@ export type WorkboardNotificationKind = (typeof WORKBOARD_NOTIFICATION_KINDS)[nu
9697
export type WorkboardExecution = {
9798
id: string;
9899
kind: "agent-session";
99-
engine: WorkboardExecutionEngine;
100+
engine?: WorkboardExecutionEngine;
100101
mode: WorkboardExecutionMode;
101102
status: WorkboardExecutionStatus;
102-
model: string;
103+
model?: string;
103104
sessionKey?: string;
104105
runId?: string;
105106
startedAt: number;

0 commit comments

Comments
 (0)