diff --git a/services/platform/app/features/tasks/lib/display.test.ts b/services/platform/app/features/tasks/lib/display.test.ts index 7fe4fc0f5b..9bf9c037f6 100644 --- a/services/platform/app/features/tasks/lib/display.test.ts +++ b/services/platform/app/features/tasks/lib/display.test.ts @@ -123,7 +123,6 @@ describe('run-admission refusals', () => { const codes = recordedRefusals(); // A walk that found nothing would pass anything. expect(codes.has('task_circuit_breaker')).toBe(true); - expect(codes.has('agent_busy')).toBe(true); expect( [...codes].filter((code) => !TASK_RUN_REFUSAL_LABEL_KEY[code]), ).toEqual([]); diff --git a/services/platform/backend/core/sandbox/session_naming.test.ts b/services/platform/backend/core/sandbox/session_naming.test.ts index 6830c1fe3f..4fd683c5cf 100644 --- a/services/platform/backend/core/sandbox/session_naming.test.ts +++ b/services/platform/backend/core/sandbox/session_naming.test.ts @@ -115,7 +115,7 @@ describe('agent worker sessions', () => { } }); - it('never takes a member worker for a standing one [SBX-R20]', () => { + it('never takes a member worker for a standing one [SBX-R21]', () => { for (const worker of [1, 2, 3]) { expect( isStandingProjectAgentSession( diff --git a/services/platform/backend/domains/sandbox/idle-release.test.ts b/services/platform/backend/domains/sandbox/idle-release.test.ts index 215daba9a6..e9d6d18575 100644 --- a/services/platform/backend/domains/sandbox/idle-release.test.ts +++ b/services/platform/backend/domains/sandbox/idle-release.test.ts @@ -149,7 +149,7 @@ describe('idle release worker', () => { ); }); - it('lets only a live run that names the stopped worker keep it [SBX-R18]', async () => { + it('lets only a live run that names the stopped worker keep it [SBX-R19]', async () => { const texts: string[] = []; const query = (strings: TemplateStringsArray) => { texts.push(strings.join('?').replaceAll(/\s+/g, ' ')); diff --git a/services/platform/backend/domains/sandbox/routes.test.ts b/services/platform/backend/domains/sandbox/routes.test.ts index 01670534d9..4a92f0f525 100644 --- a/services/platform/backend/domains/sandbox/routes.test.ts +++ b/services/platform/backend/domains/sandbox/routes.test.ts @@ -239,7 +239,7 @@ describe('sandbox settings read and write authority', () => { ]); }); - it('counts every run waiting for room beside the rows [SBX-R17]', async () => { + it('counts every run waiting for room beside the rows [SBX-R18]', async () => { listViews.mockResolvedValue([]); const counts = { total: 3, diff --git a/services/platform/backend/domains/sandbox/sessions.admission.test.ts b/services/platform/backend/domains/sandbox/sessions.admission.test.ts index 5c697527ec..3594b86ab7 100644 --- a/services/platform/backend/domains/sandbox/sessions.admission.test.ts +++ b/services/platform/backend/domains/sandbox/sessions.admission.test.ts @@ -207,7 +207,7 @@ describe('reserveSessionSlot — each workload against its own limit [SBX-R8]', }); }); -describe('every agent worker holds a slot of its own [SBX-R17]', () => { +describe('every agent worker holds a slot of its own [SBX-R18]', () => { const SECOND_WORKER = { ...AGENT_SESSION, sessionId: 'pa-agent-1-w2' }; it("admits an agent's second worker beside its first while a slot is left", async () => { diff --git a/services/platform/backend/domains/sandbox/sessions.release.test.ts b/services/platform/backend/domains/sandbox/sessions.release.test.ts index 47a4e78a52..646517f367 100644 --- a/services/platform/backend/domains/sandbox/sessions.release.test.ts +++ b/services/platform/backend/domains/sandbox/sessions.release.test.ts @@ -85,7 +85,7 @@ describe('releaseProjectAgentSessionSlot', () => { expect(update?.values).toEqual(['agent-1', 'org-1']); }); - it("stops a worker once its own run ended, whatever the agent's other workers do [SBX-R18]", async () => { + it("stops a worker once its own run ended, whatever the agent's other workers do [SBX-R19]", async () => { const { sql, statements } = fakeSql([{ id: 'row-1' }]); await releaseProjectAgentSessionSlot(sql, ARGS); diff --git a/services/platform/backend/domains/sandbox/sessions.views.test.ts b/services/platform/backend/domains/sandbox/sessions.views.test.ts index 41558016d3..a792da1870 100644 --- a/services/platform/backend/domains/sandbox/sessions.views.test.ts +++ b/services/platform/backend/domains/sandbox/sessions.views.test.ts @@ -186,7 +186,7 @@ describe('listSandboxViewsForOrg', () => { describe('agent workers on the Sandboxes page', () => { const AGENT = '0b7e7a4c-1f7e-4a39-9c55-6f1d3c1f2a10'; - test('names each worker of an agent and the task it works [SBX-R17]', async () => { + test('names each worker of an agent and the task it works [SBX-R18]', async () => { const worker = (sessionId: string, createdAt: number): SessionRow => ({ ...session(sessionId, createdAt), ownerId: AGENT, @@ -242,7 +242,7 @@ describe('agent workers on the Sandboxes page', () => { ]); }); - test('counts every waiting run by its reason, a park without one as unknown [SBX-R17]', async () => { + test('counts every waiting run by its reason, a park without one as unknown [SBX-R18]', async () => { const query = (strings: TemplateStringsArray, ...values: unknown[]) => { const text = strings.join('?'); // A run waits while it is parked, as every read of one tells it. diff --git a/services/platform/backend/domains/sandbox/spec.md b/services/platform/backend/domains/sandbox/spec.md index 97452a7e19..4934496f69 100644 --- a/services/platform/backend/domains/sandbox/spec.md +++ b/services/platform/backend/domains/sandbox/spec.md @@ -128,7 +128,7 @@ limit, because what frees is an empty workspace, not room. Every run of a project agent that works at the same time as another works in a sandbox of its own, a worker. One agent working three tasks at once has three workers. -### SBX-R17 · Every agent worker is one sandbox and holds one agent-worker slot +### SBX-R18 · Every agent worker is one sandbox and holds one agent-worker slot The limit of agent workers counts workers, not agents: one agent working three tasks at once holds three slots. A run that would need one more worker than the limit allows waits for a @@ -137,7 +137,7 @@ slot; a burst of starts opens no more workers than there are slots left. - **Example**: Ada's organization allows 2 agent workers. Scribe works two tasks → 2 of 2 are in use. Lector is started on a task → its run waits until one of Scribe's workers frees. -### SBX-R18 · A worker gives its slot back as soon as its own run ends +### SBX-R19 · A worker gives its slot back as soon as its own run ends A worker stops and frees its slot once no run of its own is left: none working in it, and none that has taken it and not started yet. The agent's other workers do not keep it up. A pinned @@ -148,7 +148,7 @@ until that process has ended. another. "Release notes" finishes → its worker stops and its slot is free at once, while "Changelog" keeps working. -### SBX-R19 · A run takes a free worker before a new one is opened +### SBX-R20 · A run takes a free worker before a new one is opened A starting run takes, in order: its task's previous worker, a worker that is still up, a stopped worker (lowest number first), and only then a new one. When the only free worker is @@ -158,7 +158,7 @@ it. - **Example**: Scribe's workers 1 and 2 are stopped. Ada starts Scribe on a new task → it works in worker 1, and no worker 3 is created. -### SBX-R20 · A member's runs work in that member's own workers +### SBX-R21 · A member's runs work in that member's own workers The runs a member starts work in workers kept for that member and the agent, apart from the workers of the runs editors start, and stay limited to their own task (`SBX-R7`) in whichever diff --git a/services/platform/backend/domains/tasks/agent-runs.test.ts b/services/platform/backend/domains/tasks/agent-runs.test.ts index 5a60c4de30..7a381295eb 100644 --- a/services/platform/backend/domains/tasks/agent-runs.test.ts +++ b/services/platform/backend/domains/tasks/agent-runs.test.ts @@ -1419,7 +1419,7 @@ describe('what every read shows of a waiting run [TASK-R25]', () => { }); }); -describe('wakeAgentParkedAgentRun — a worker that stays up wakes its own family [SBX-R18]', () => { +describe('wakeAgentParkedAgentRun — a worker that stays up wakes its own family [SBX-R19]', () => { const AGENT = '0b7e7a4c-1f7e-4a39-9c55-6f1d3c1f2a10'; const CANDIDATES = 'SELECT id, session_id AS "sessionId" FROM app.project_agent_runs'; diff --git a/services/platform/backend/domains/tasks/agent-workers.test.ts b/services/platform/backend/domains/tasks/agent-workers.test.ts index 16580b53b3..40b3d99153 100644 --- a/services/platform/backend/domains/tasks/agent-workers.test.ts +++ b/services/platform/backend/domains/tasks/agent-workers.test.ts @@ -90,20 +90,20 @@ describe('which worker a run starts in', () => { ).toEqual({ sessionId: w(1), worker: 1 }); }); - it('takes a stopped worker before it opens a new one [SBX-R19]', () => { + it('takes a stopped worker before it opens a new one [SBX-R20]', () => { // Workers 1 and 2 are stopped: the run works in worker 1, no worker 3. expect( chooseWorker(facts({ workers: [worker(2), worker(1)], room: 2 })), ).toEqual({ sessionId: w(1), worker: 1 }); }); - it('takes a worker that is still up before a stopped one [SBX-R19]', () => { + it('takes a worker that is still up before a stopped one [SBX-R20]', () => { expect( chooseWorker(facts({ workers: [worker(1), worker(2, { warm: true })] })), ).toEqual({ sessionId: w(2), worker: 2 }); }); - it('opens the lowest free number, worker 1 again once it is gone [SBX-R19]', () => { + it('opens the lowest free number, worker 1 again once it is gone [SBX-R20]', () => { expect(chooseWorker(facts({ workers: [] }))).toEqual({ sessionId: w(1), worker: 1, @@ -118,13 +118,13 @@ describe('which worker a run starts in', () => { ).toEqual({ sessionId: w(2), worker: 2 }); }); - it('waits for a Destroy rather than opening a worker beside it [SBX-R19]', () => { + it('waits for a Destroy rather than opening a worker beside it [SBX-R20]', () => { expect( chooseWorker(facts({ workers: [worker(1, { destroyPending: true })] })), ).toEqual({ wait: 'destroy_pending' }); }); - it('opens and wakes no worker the organization has no slot for [SBX-R17]', () => { + it('opens and wakes no worker the organization has no slot for [SBX-R18]', () => { // Every slot is held: a stopped worker would need one, a new one too. expect( chooseWorker( @@ -170,7 +170,7 @@ describe('which worker a run starts in', () => { ).toEqual({ sessionId: w(3), worker: 3 }); }); - it("names a member's workers within the member's family [SBX-R20]", () => { + it("names a member's workers within the member's family [SBX-R21]", () => { const base = memberWorkerSessionId(AGENT, 'user-mia', 1); expect( chooseWorker( @@ -395,7 +395,7 @@ describe('claiming a worker', () => { ); }); - it("moves a run whose starter lost the editor role into the member's family [SBX-R20]", async () => { + it("moves a run whose starter lost the editor role into the member's family [SBX-R21]", async () => { const member = memberWorkerSessionId(AGENT, 'user-ada', 1); vi.mocked(sessionIdForAgentRun).mockResolvedValue(member); const { sql } = claimSql({ @@ -408,7 +408,7 @@ describe('claiming a worker', () => { }); }); - it('never moves a member run into the standing family [SBX-R20]', async () => { + it('never moves a member run into the standing family [SBX-R21]', async () => { const member = memberWorkerSessionId(AGENT, 'user-mia', 1); const { sql } = claimSql({ run: { sessionId: member }, @@ -466,7 +466,7 @@ describe('a start that has not waited and a run woken for room [TASK-R25]', () = ); }); - it('still takes a worker that is up, which needs no room [SBX-R19]', async () => { + it('still takes a worker that is up, which needs no room [SBX-R20]', async () => { const { sql } = claimSql({ family: [{ sessionId: w(1), status: 'active', pinned: false }], inFlight: 1, diff --git a/services/platform/backend/domains/tasks/agent-workers.ts b/services/platform/backend/domains/tasks/agent-workers.ts index 642e1a8dbe..ddd6671cde 100644 --- a/services/platform/backend/domains/tasks/agent-workers.ts +++ b/services/platform/backend/domains/tasks/agent-workers.ts @@ -385,7 +385,7 @@ async function readWorkerFacts( // every kick names that one, so counting the name would send a task's // next run away from its own worker, and a burst of starts to new // workers, whenever another start came a moment earlier. The name still - // guards the worker against an idle release (`SBX-R18`). A start by an + // guards the worker against an idle release (`SBX-R19`). A start by an // image that never claims shares worker 1 with a claim while both images // serve; the per-session limit on processes bounds that. const others = await tx<{ sessionId: string }[]>` diff --git a/services/platform/backend/domains/tasks/service.ops-indicators.test.ts b/services/platform/backend/domains/tasks/service.ops-indicators.test.ts index 714dd1dbfe..8037172bd6 100644 --- a/services/platform/backend/domains/tasks/service.ops-indicators.test.ts +++ b/services/platform/backend/domains/tasks/service.ops-indicators.test.ts @@ -18,7 +18,6 @@ vi.mock('../projects/service.ts', async (importOriginal) => ({ // After the service: imported first, it leaves the service bound to the // project service without the mock above. -import { parkedRunSql, parkedWaitingReasonSql } from './agent-runs.ts'; import { getTaskOpsIndicators, getTaskOpsIndicatorsForAccessibleProjects, @@ -165,7 +164,10 @@ describe('the board reads every live agent run with its worker and wait', () => "ORDER BY (status = 'running') DESC, started_at_ms, seq", ); // Whether a run waits, and why, is read through the predicate every - // read of a waiting run shares. + // read of a waiting run shares. Loaded here, not at the top: imported + // ahead of the service, it binds the service to the real project service. + const { parkedRunSql, parkedWaitingReasonSql } = + await import('./agent-runs.ts'); expect(read?.values).toEqual([ parkedRunSql(), parkedWaitingReasonSql(), diff --git a/services/platform/backend/domains/tasks/spec.md b/services/platform/backend/domains/tasks/spec.md index 487b059c1a..6a8ba609e7 100644 --- a/services/platform/backend/domains/tasks/spec.md +++ b/services/platform/backend/domains/tasks/spec.md @@ -308,7 +308,7 @@ on its day, also after it is moved. ## One agent on several tasks An agent is a configuration; each of its runs works in a worker, a sandbox of its own. The -organization's limit of agent workers (`SBX-R17`) decides how many work at once. +organization's limit of agent workers (`SBX-R18`) decides how many work at once. ### TASK-R24 · An agent works each of its running tasks in a worker of its own