Q130: every open task says why it is not moving, when it resumes, and who must act

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 21:27:29 -04:00
1 parent 271a83a7bc
commit 074a1fc8e3
14 files changed
+255 -7

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OnboardingService } from "../../src/main/services/onboarding-service";
import { ProviderService } from "../../src/main/services/provider-service";
import { providerAdapters } from "../../src/main/services/provider-adapters";
import { TaskExecutionService } from "../../src/main/services/task-execution-service";
import { SnapshotService } from "../../src/main/services/snapshot-service";
import { currentProviderUsageWindow } from "../../src/shared/provider-runtime-domain";
import type { AgentTurnResult } from "../../src/main/services/provider-tool-turn";
const testVault = {
encryptSecret: (secret: string) => Buffer.from(secret, "utf8"),
decryptSecret: (ciphertext: Buffer) => ciphertext.toString("utf8"),
};
/** A company whose kickoff task is open with no run in flight. */
function company() {
const turn = async (): Promise<AgentTurnResult> => ({ text: "Done.", toolCalls: [], rawResponseJson: "{}" });
const connection = new DatabaseConnection(join(tmpdir(), `ac-task-holds-${randomUUID()}.db`));
const providers = new ProviderService(connection.db, providerAdapters, testVault, turn);
const onboarding = new OnboardingService({ db: connection.db, saveProviderCredential: (input) => providers.saveCredential(input) });
const execution = new TaskExecutionService(connection.db, providers);
let orgId = "";
let ceoId = "";
connection.transaction(() => {
const summary = onboarding.bootstrap({
companyName: "FlowPilot", companyDescription: "CSM analytics.", employeeName: "Yang", workspacePath: "",
executionMode: "ai_ceo", autoApproveHires: true, reviewDeliverables: false,
goalTitle: "Ship the MVP.", goalDescription: "A working product.",
providerKey: "openai", providerSecret: "sk-test-key", providerModel: "gpt-5.4",
});
orgId = summary.orgId;
ceoId = summary.ceoAgentId;
});
const run = connection.db.prepare("select id, task_id, provider_id from task_runs").get() as { id: string; task_id: string; provider_id: string };
connection.db.prepare("update task_runs set state = 'failed' where id = ?").run(run.id);
const hold = () => new SnapshotService(connection.db).load().tasks.find((task) => task.id === run.task_id)!.hold;
return { connection, providers, execution, orgId, ceoId, taskId: run.task_id, providerId: run.provider_id, hold };
}
describe("why an open task is not moving", () => {
test("an open task with nothing in its way has no hold", () => {
const h = company();
expect(h.hold()).toBeNull();
h.connection.close();
});
test("a provider cooldown says when the task resumes on its own", () => {
const h = company();
const until = Date.now() + 90_000;
h.connection.db.prepare("insert into provider_cooldowns(provider_id, label, until_ms, strikes) values (?, 'OpenAI', ?, 2)").run(h.providerId, until);
expect(h.hold()).toEqual({ reason: "OpenAI reported a passing failure; its work waits out a cooldown.", resumesAt: new Date(until).toISOString(), waitingOn: "provider" });
h.connection.close();
});
test("a spent monthly request limit waits on the operator or the next window, and a zero limit only on the operator", () => {
const h = company();
const window = currentProviderUsageWindow();
h.connection.db.prepare("update providers set monthly_request_limit = 2 where id = ?").run(h.providerId);
h.connection.db.prepare(`
insert into provider_usage_windows(id, org_id, provider_id, window_start, window_end, request_count, input_char_count, output_char_count, created_at, updated_at)
values ('puw_t', ?, ?, ?, ?, 2, 0, 0, ?, ?)
`).run(h.orgId, h.providerId, window.start, window.end, new Date().toISOString(), new Date().toISOString());
expect(h.hold()).toEqual({ reason: "OpenAI used its monthly request limit (2). Raise the limit or wait for the next window.", resumesAt: window.end, waitingOn: "operator" });
h.connection.db.prepare("update providers set monthly_request_limit = 0 where id = ?").run(h.providerId);
expect(h.hold()).toEqual({ reason: "OpenAI has a monthly request limit of 0. Raise the limit to let its work run.", resumesAt: null, waitingOn: "operator" });
h.connection.close();
});
test("a spent money budget waits on the operator or next month, and a zero budget only on the operator", () => {
const h = company();
h.providers.setPrices({ orgId: h.orgId, providerId: h.providerId, inputDollarsPerMtok: 2.5, cachedInputDollarsPerMtok: null, outputDollarsPerMtok: 10 });
h.connection.db.prepare("update agents set budget_cents = 100 where id = ?").run(h.ceoId);
h.connection.db.prepare(`
insert into provider_requests(id, org_id, provider_id, provider_key, agent_id, model, prompt_sha256, state, created_at, cost_cents)
values ('preq_t', ?, ?, 'openai', ?, 'gpt-5.4', 'x', 'succeeded', ?, 150)
`).run(h.orgId, h.providerId, h.ceoId, new Date().toISOString());
expect(h.hold()).toEqual({
reason: "CEO has spent its monthly budget ($1.50 of $1.00). Raise the budget or wait for next month.",
resumesAt: currentProviderUsageWindow().end,
waitingOn: "operator",
});
h.connection.db.prepare("update agents set budget_cents = 0 where id = ?").run(h.ceoId);
expect(h.hold()).toEqual({ reason: "CEO has spent its monthly budget ($1.50 of $0.00). Raise the budget to let it run.", resumesAt: null, waitingOn: "operator" });
h.connection.close();
});
test("a heartbeat that could not queue the task records why, until it can", () => {
const h = company();
h.connection.db.prepare("update tasks set risk = 'high' where id = ?").run(h.taskId);
expect(h.connection.transaction(() => h.execution.enqueueHeartbeatRuns())).toEqual([]);
expect(h.hold()).toEqual({ reason: "Could not start: Approved task execution approval is required.", resumesAt: null, waitingOn: "operator" });
h.connection.db.prepare("update tasks set risk = 'low' where id = ?").run(h.taskId);
expect(h.connection.transaction(() => h.execution.enqueueHeartbeatRuns())).toHaveLength(1);
expect(h.hold()).toBeNull();
h.connection.close();
});
test("a stalled task waits on the operator with no resume time", () => {
const h = company();
h.connection.db.prepare("update tasks set stalled_reason = 'Stalled: 3 sessions made no progress.' where id = ?").run(h.taskId);
expect(h.hold()).toEqual({ reason: "Stalled: 3 sessions made no progress.", resumesAt: null, waitingOn: "operator" });
h.connection.close();
});
});
+1 -1
View File
@@ -35,7 +35,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93]);
connection.close();
});