mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 02:20:38 +09:00
Disable timer heartbeats by default for new agents
This commit is contained in:
parent
5640d29ab0
commit
844b061267
8 changed files with 151 additions and 22 deletions
|
|
@ -213,6 +213,80 @@ describe("agent permission routes", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("normalizes direct agent creation to disable timer heartbeats by default", async () => {
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Builder",
|
||||
role: "engineer",
|
||||
adapterType: "process",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {
|
||||
heartbeat: {
|
||||
intervalSec: 3600,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(mockAgentService.create).toHaveBeenCalledWith(
|
||||
companyId,
|
||||
expect.objectContaining({
|
||||
runtimeConfig: {
|
||||
heartbeat: {
|
||||
enabled: false,
|
||||
intervalSec: 3600,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes hire requests to disable timer heartbeats by default", async () => {
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/agent-hires`)
|
||||
.send({
|
||||
name: "Builder",
|
||||
role: "engineer",
|
||||
adapterType: "process",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {
|
||||
heartbeat: {
|
||||
intervalSec: 3600,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(mockAgentService.create).toHaveBeenCalledWith(
|
||||
companyId,
|
||||
expect.objectContaining({
|
||||
runtimeConfig: {
|
||||
heartbeat: {
|
||||
enabled: false,
|
||||
intervalSec: 3600,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes explicit task assignment access on agent detail", async () => {
|
||||
mockAccessService.listPrincipalGrants.mockResolvedValue([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -449,11 +449,25 @@ export function agentRoutes(db: Db) {
|
|||
function parseSchedulerHeartbeatPolicy(runtimeConfig: unknown) {
|
||||
const heartbeat = asRecord(asRecord(runtimeConfig)?.heartbeat) ?? {};
|
||||
return {
|
||||
enabled: parseBooleanLike(heartbeat.enabled) ?? true,
|
||||
enabled: parseBooleanLike(heartbeat.enabled) ?? false,
|
||||
intervalSec: Math.max(0, parseNumberLike(heartbeat.intervalSec) ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeNewAgentRuntimeConfig(runtimeConfig: unknown): Record<string, unknown> {
|
||||
const parsedRuntimeConfig = asRecord(runtimeConfig);
|
||||
const normalizedRuntimeConfig = parsedRuntimeConfig ? { ...parsedRuntimeConfig } : {};
|
||||
const parsedHeartbeat = asRecord(normalizedRuntimeConfig.heartbeat);
|
||||
const heartbeat = parsedHeartbeat ? { ...parsedHeartbeat } : {};
|
||||
|
||||
if (parseBooleanLike(heartbeat.enabled) == null) {
|
||||
heartbeat.enabled = false;
|
||||
}
|
||||
|
||||
normalizedRuntimeConfig.heartbeat = heartbeat;
|
||||
return normalizedRuntimeConfig;
|
||||
}
|
||||
|
||||
function generateEd25519PrivateKeyPem(): string {
|
||||
const { privateKey } = generateKeyPairSync("ed25519");
|
||||
return privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
||||
|
|
@ -1308,6 +1322,7 @@ export function agentRoutes(db: Db) {
|
|||
const normalizedHireInput = {
|
||||
...hireInput,
|
||||
adapterConfig: normalizedAdapterConfig,
|
||||
runtimeConfig: normalizeNewAgentRuntimeConfig(hireInput.runtimeConfig),
|
||||
};
|
||||
|
||||
const company = await db
|
||||
|
|
@ -1474,6 +1489,7 @@ export function agentRoutes(db: Db) {
|
|||
const createdAgent = await svc.create(companyId, {
|
||||
...createInput,
|
||||
adapterConfig: normalizedAdapterConfig,
|
||||
runtimeConfig: normalizeNewAgentRuntimeConfig(createInput.runtimeConfig),
|
||||
status: "idle",
|
||||
spentMonthlyCents: 0,
|
||||
lastHeartbeatAt: null,
|
||||
|
|
|
|||
|
|
@ -2159,7 +2159,7 @@ export function heartbeatService(db: Db) {
|
|||
const heartbeat = parseObject(runtimeConfig.heartbeat);
|
||||
|
||||
return {
|
||||
enabled: asBoolean(heartbeat.enabled, true),
|
||||
enabled: asBoolean(heartbeat.enabled, false),
|
||||
intervalSec: Math.max(0, asNumber(heartbeat.intervalSec, 0)),
|
||||
wakeOnDemand: asBoolean(heartbeat.wakeOnDemand ?? heartbeat.wakeOnAssignment ?? heartbeat.wakeOnOnDemand ?? heartbeat.wakeOnAutomation, true),
|
||||
maxConcurrentRuns: normalizeMaxConcurrentRuns(heartbeat.maxConcurrentRuns),
|
||||
|
|
|
|||
|
|
@ -923,14 +923,14 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||
<ToggleWithNumber
|
||||
label="Heartbeat on interval"
|
||||
hint={help.heartbeatInterval}
|
||||
checked={eff("heartbeat", "enabled", heartbeat.enabled !== false)}
|
||||
checked={eff("heartbeat", "enabled", heartbeat.enabled === true)}
|
||||
onCheckedChange={(v) => mark("heartbeat", "enabled", v)}
|
||||
number={eff("heartbeat", "intervalSec", Number(heartbeat.intervalSec ?? 300))}
|
||||
onNumberChange={(v) => mark("heartbeat", "intervalSec", v)}
|
||||
numberLabel="sec"
|
||||
numberPrefix="Run heartbeat every"
|
||||
numberHint={help.intervalSec}
|
||||
showNumber={eff("heartbeat", "enabled", heartbeat.enabled !== false)}
|
||||
showNumber={eff("heartbeat", "enabled", heartbeat.enabled === true)}
|
||||
/>
|
||||
</div>
|
||||
<CollapsibleSection
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
buildOnboardingProjectPayload,
|
||||
selectDefaultCompanyGoalId
|
||||
} from "../lib/onboarding-launch";
|
||||
import { buildNewAgentRuntimeConfig } from "../lib/new-agent-runtime-config";
|
||||
import {
|
||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||
DEFAULT_CODEX_LOCAL_MODEL
|
||||
|
|
@ -460,15 +461,7 @@ export function OnboardingWizard() {
|
|||
role: "ceo",
|
||||
adapterType,
|
||||
adapterConfig: buildAdapterConfig(),
|
||||
runtimeConfig: {
|
||||
heartbeat: {
|
||||
enabled: true,
|
||||
intervalSec: 3600,
|
||||
wakeOnDemand: true,
|
||||
cooldownSec: 10,
|
||||
maxConcurrentRuns: 1
|
||||
}
|
||||
}
|
||||
runtimeConfig: buildNewAgentRuntimeConfig()
|
||||
});
|
||||
setCreatedAgentId(agent.id);
|
||||
queryClient.invalidateQueries({
|
||||
|
|
|
|||
34
ui/src/lib/new-agent-runtime-config.test.ts
Normal file
34
ui/src/lib/new-agent-runtime-config.test.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// @vitest-environment node
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildNewAgentRuntimeConfig } from "./new-agent-runtime-config";
|
||||
|
||||
describe("buildNewAgentRuntimeConfig", () => {
|
||||
it("defaults new agents to no timer heartbeat", () => {
|
||||
expect(buildNewAgentRuntimeConfig()).toEqual({
|
||||
heartbeat: {
|
||||
enabled: false,
|
||||
intervalSec: 300,
|
||||
wakeOnDemand: true,
|
||||
cooldownSec: 10,
|
||||
maxConcurrentRuns: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves explicit heartbeat settings", () => {
|
||||
expect(
|
||||
buildNewAgentRuntimeConfig({
|
||||
heartbeatEnabled: true,
|
||||
intervalSec: 3600,
|
||||
}),
|
||||
).toEqual({
|
||||
heartbeat: {
|
||||
enabled: true,
|
||||
intervalSec: 3600,
|
||||
wakeOnDemand: true,
|
||||
cooldownSec: 10,
|
||||
maxConcurrentRuns: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
16
ui/src/lib/new-agent-runtime-config.ts
Normal file
16
ui/src/lib/new-agent-runtime-config.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { defaultCreateValues } from "../components/agent-config-defaults";
|
||||
|
||||
export function buildNewAgentRuntimeConfig(input?: {
|
||||
heartbeatEnabled?: boolean;
|
||||
intervalSec?: number;
|
||||
}) {
|
||||
return {
|
||||
heartbeat: {
|
||||
enabled: input?.heartbeatEnabled ?? defaultCreateValues.heartbeatEnabled,
|
||||
intervalSec: input?.intervalSec ?? defaultCreateValues.intervalSec,
|
||||
wakeOnDemand: true,
|
||||
cooldownSec: 10,
|
||||
maxConcurrentRuns: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import { getUIAdapter, listUIAdapters } from "../adapters";
|
|||
import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters";
|
||||
import { isValidAdapterType } from "../adapters/metadata";
|
||||
import { ReportsToPicker } from "../components/ReportsToPicker";
|
||||
import { buildNewAgentRuntimeConfig } from "../lib/new-agent-runtime-config";
|
||||
import {
|
||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||
DEFAULT_CODEX_LOCAL_MODEL,
|
||||
|
|
@ -175,15 +176,10 @@ export function NewAgent() {
|
|||
...(selectedSkillKeys.length > 0 ? { desiredSkills: selectedSkillKeys } : {}),
|
||||
adapterType: configValues.adapterType,
|
||||
adapterConfig: buildAdapterConfig(),
|
||||
runtimeConfig: {
|
||||
heartbeat: {
|
||||
enabled: configValues.heartbeatEnabled,
|
||||
intervalSec: configValues.intervalSec,
|
||||
wakeOnDemand: true,
|
||||
cooldownSec: 10,
|
||||
maxConcurrentRuns: 1,
|
||||
},
|
||||
},
|
||||
runtimeConfig: buildNewAgentRuntimeConfig({
|
||||
heartbeatEnabled: configValues.heartbeatEnabled,
|
||||
intervalSec: configValues.intervalSec,
|
||||
}),
|
||||
budgetMonthlyCents: 0,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue