Skip to content

Commit 6f2177f

Browse files
committed
fix(lobster): preserve explicit empty managed flow state
1 parent cd9cb4d commit 6f2177f

2 files changed

Lines changed: 50 additions & 12 deletions

File tree

extensions/lobster/src/lobster-tool.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,41 @@ describe("lobster plugin tool", () => {
323323
expect(mutation.applied).toBe(true);
324324
});
325325

326+
it("preserves explicit empty flow state in managed TaskFlow run mode", async () => {
327+
const runner = {
328+
run: vi.fn().mockResolvedValue({
329+
ok: true,
330+
status: "ok",
331+
output: [],
332+
requiresApproval: null,
333+
}),
334+
};
335+
const taskFlow = createFakeTaskFlow();
336+
337+
const tool = createLobsterTool(fakeApi(), { runner, taskFlow });
338+
await tool.execute("call-managed-run-empty-state", {
339+
action: "run",
340+
pipeline: "noop",
341+
flowControllerId: "tests/lobster",
342+
flowGoal: "Run Lobster workflow",
343+
flowStateJson: "{}",
344+
});
345+
346+
expect(taskFlow.createManaged).toHaveBeenCalledWith({
347+
controllerId: "tests/lobster",
348+
goal: "Run Lobster workflow",
349+
currentStep: "run_lobster",
350+
stateJson: {},
351+
});
352+
expect(runner.run).toHaveBeenCalledWith({
353+
action: "run",
354+
pipeline: "noop",
355+
cwd: process.cwd(),
356+
timeoutMs: 20_000,
357+
maxStdoutBytes: 512_000,
358+
});
359+
});
360+
326361
it("rejects managed TaskFlow params when no bound taskFlow runtime is available", async () => {
327362
const tool = createLobsterTool(fakeApi(), {
328363
runner: { run: vi.fn() },

extensions/lobster/src/lobster-tool.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,22 @@ function parseOptionalFlowStateJson(value: unknown): JsonLike | undefined {
102102
return undefined;
103103
}
104104
try {
105-
const parsed = JSON.parse(trimmed) as JsonLike;
106-
if (
107-
parsed !== null &&
108-
typeof parsed === "object" &&
109-
!Array.isArray(parsed) &&
110-
Object.keys(parsed).length === 0
111-
) {
112-
return undefined;
113-
}
114-
return parsed;
105+
return JSON.parse(trimmed) as JsonLike;
115106
} catch {
116107
throw new Error("flowStateJson must be valid JSON");
117108
}
118109
}
119110

111+
function isEmptyJsonObject(value: JsonLike | undefined): boolean {
112+
return (
113+
value !== undefined &&
114+
value !== null &&
115+
typeof value === "object" &&
116+
!Array.isArray(value) &&
117+
Object.keys(value).length === 0
118+
);
119+
}
120+
120121
function parseRunFlowParams(params: Record<string, unknown>): ManagedFlowRunParams | null {
121122
const controllerId = readOptionalTrimmedString(params.flowControllerId, "flowControllerId");
122123
const goal = readOptionalTrimmedString(params.flowGoal, "flowGoal");
@@ -125,13 +126,14 @@ function parseRunFlowParams(params: Record<string, unknown>): ManagedFlowRunPara
125126
const stateJson = parseOptionalFlowStateJson(params.flowStateJson);
126127
const resumeFlowId = readOptionalTrimmedString(params.flowId, "flowId");
127128
const resumeRevision = readOptionalNumber(params.flowExpectedRevision, "flowExpectedRevision");
129+
const stateJsonSignalsRunMode = stateJson !== undefined && !isEmptyJsonObject(stateJson);
128130

129131
const hasRunFields =
130132
controllerId !== undefined ||
131133
goal !== undefined ||
132134
currentStep !== undefined ||
133135
waitingStep !== undefined ||
134-
stateJson !== undefined;
136+
stateJsonSignalsRunMode;
135137

136138
if (!hasRunFields) {
137139
return null;
@@ -175,7 +177,8 @@ function parseResumeFlowParams(params: Record<string, unknown>): ManagedFlowResu
175177
return null;
176178
}
177179
const stateJson = parseOptionalFlowStateJson(params.flowStateJson);
178-
if (runControllerId !== undefined || runGoal !== undefined || stateJson !== undefined) {
180+
const stateJsonDisallowed = stateJson !== undefined && !isEmptyJsonObject(stateJson);
181+
if (runControllerId !== undefined || runGoal !== undefined || stateJsonDisallowed) {
179182
throw new Error("resume action does not accept flowControllerId, flowGoal, or flowStateJson");
180183
}
181184
if (!flowId) {

0 commit comments

Comments
 (0)