@app-vitals/agent-admin 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.json +291 -0
- package/openapi.platform.json +392 -0
- package/package.json +2 -2
- package/prisma/client/browser.ts +15 -0
- package/prisma/client/client.ts +15 -0
- package/prisma/client/internal/class.ts +34 -4
- package/prisma/client/internal/prismaNamespace.ts +273 -1
- package/prisma/client/internal/prismaNamespaceBrowser.ts +47 -0
- package/prisma/client/models/AgentCronJob.ts +107 -1
- package/prisma/client/models/AgentCronRunRequest.ts +1233 -0
- package/prisma/client/models/AgentCronRunTranscript.ts +1339 -0
- package/prisma/client/models/AgentTranscriptRead.ts +1177 -0
- package/prisma/client/models.ts +3 -0
- package/prisma/migrations/20261027000000_add_cron_run_transcripts/migration.sql +37 -0
- package/prisma/migrations/20261028000000_add_cron_run_now/migration.sql +24 -0
- package/prisma/schema/schema.prisma +58 -0
- package/src/admin-boot.ts +18 -0
- package/src/admin-ui.fixtures.ts +2 -0
- package/src/admin-ui.smoke.test.ts +2 -0
- package/src/agent-cron-jobs.unit.test.ts +4 -0
- package/src/agent-cron-run-stats.smoke.test.ts +10 -0
- package/src/agent-cron-runs.integration.test.ts +93 -0
- package/src/agent-cron-runs.ts +47 -15
- package/src/agents-api.smoke.test.ts +18 -0
- package/src/agents-api.ts +30 -0
- package/src/api-auth.ts +1 -0
- package/src/api.smoke.test.ts +39 -0
- package/src/api.ts +15 -0
- package/src/chat.smoke.test.ts +2 -0
- package/src/cron-run-now-config.ts +20 -0
- package/src/cron-run-now-config.unit.test.ts +26 -0
- package/src/cron-run-now.smoke.test.ts +218 -0
- package/src/cron-run-now.ts +207 -0
- package/src/cron-run-transcripts.integration.test.ts +220 -0
- package/src/cron-run-transcripts.smoke.test.ts +286 -0
- package/src/cron-run-transcripts.ts +314 -0
- package/src/errors.ts +7 -0
- package/src/openapi-schemas.ts +78 -0
- package/src/openapi-schemas.unit.test.ts +4 -0
- package/src/transcript-config.ts +19 -0
- package/src/transcript-config.unit.test.ts +32 -0
package/prisma/client/models.ts
CHANGED
|
@@ -27,6 +27,9 @@ export type * from './models/AgentCronRunSkillUsage.ts'
|
|
|
27
27
|
export type * from './models/AgentCronRunModelBreakdown.ts'
|
|
28
28
|
export type * from './models/AgentChatTokenUsageDailyByModel.ts'
|
|
29
29
|
export type * from './models/AgentAsk.ts'
|
|
30
|
+
export type * from './models/AgentCronRunRequest.ts'
|
|
31
|
+
export type * from './models/AgentCronRunTranscript.ts'
|
|
32
|
+
export type * from './models/AgentTranscriptRead.ts'
|
|
30
33
|
export type * from './models/AgentGitHubInstallationsSnapshot.ts'
|
|
31
34
|
export type * from './models/PushSubscription.ts'
|
|
32
35
|
export type * from './models/ChatThreadWatch.ts'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- COS-43.1: redacted cron-run transcripts + per-read audit rows (additive only).
|
|
2
|
+
-- CreateTable
|
|
3
|
+
CREATE TABLE "AgentCronRunTranscript" (
|
|
4
|
+
"id" TEXT NOT NULL,
|
|
5
|
+
"runId" TEXT NOT NULL,
|
|
6
|
+
"agentId" TEXT NOT NULL,
|
|
7
|
+
"cronId" TEXT NOT NULL,
|
|
8
|
+
"sessionId" TEXT,
|
|
9
|
+
"bytes" INTEGER NOT NULL,
|
|
10
|
+
"truncated" BOOLEAN NOT NULL,
|
|
11
|
+
"redactions" INTEGER NOT NULL,
|
|
12
|
+
"body" TEXT NOT NULL,
|
|
13
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
14
|
+
|
|
15
|
+
CONSTRAINT "AgentCronRunTranscript_pkey" PRIMARY KEY ("id")
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- CreateTable
|
|
19
|
+
CREATE TABLE "AgentTranscriptRead" (
|
|
20
|
+
"id" TEXT NOT NULL,
|
|
21
|
+
"runId" TEXT NOT NULL,
|
|
22
|
+
"agentId" TEXT NOT NULL,
|
|
23
|
+
"reader" TEXT NOT NULL,
|
|
24
|
+
"scopeKind" TEXT NOT NULL,
|
|
25
|
+
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
26
|
+
|
|
27
|
+
CONSTRAINT "AgentTranscriptRead_pkey" PRIMARY KEY ("id")
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
-- CreateIndex
|
|
31
|
+
CREATE UNIQUE INDEX "AgentCronRunTranscript_runId_key" ON "AgentCronRunTranscript"("runId");
|
|
32
|
+
|
|
33
|
+
-- CreateIndex
|
|
34
|
+
CREATE INDEX "AgentCronRunTranscript_agentId_createdAt_idx" ON "AgentCronRunTranscript"("agentId", "createdAt");
|
|
35
|
+
|
|
36
|
+
-- CreateIndex
|
|
37
|
+
CREATE INDEX "AgentTranscriptRead_agentId_at_idx" ON "AgentTranscriptRead"("agentId", "at");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- COS-42.1: cron run-now request columns and audit table (additive only).
|
|
2
|
+
-- AlterTable
|
|
3
|
+
ALTER TABLE "AgentCronJob" ADD COLUMN "runRequestedAt" TIMESTAMP(3),
|
|
4
|
+
ADD COLUMN "runRequestedBy" TEXT;
|
|
5
|
+
|
|
6
|
+
-- CreateTable
|
|
7
|
+
CREATE TABLE "AgentCronRunRequest" (
|
|
8
|
+
"id" TEXT NOT NULL,
|
|
9
|
+
"cronId" TEXT NOT NULL,
|
|
10
|
+
"agentId" TEXT NOT NULL,
|
|
11
|
+
"requestedBy" TEXT NOT NULL,
|
|
12
|
+
"requestedAt" TIMESTAMP(3) NOT NULL,
|
|
13
|
+
"status" TEXT NOT NULL DEFAULT 'requested',
|
|
14
|
+
"honouredRunId" TEXT,
|
|
15
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
16
|
+
|
|
17
|
+
CONSTRAINT "AgentCronRunRequest_pkey" PRIMARY KEY ("id")
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
-- CreateIndex
|
|
21
|
+
CREATE INDEX "AgentCronRunRequest_cronId_requestedAt_idx" ON "AgentCronRunRequest"("cronId", "requestedAt");
|
|
22
|
+
|
|
23
|
+
-- CreateIndex
|
|
24
|
+
CREATE INDEX "AgentCronRunRequest_agentId_requestedAt_idx" ON "AgentCronRunRequest"("agentId", "requestedAt");
|
|
@@ -134,6 +134,11 @@ model AgentCronJob {
|
|
|
134
134
|
// restoreLockdownDisabled() can re-enable exactly those crons. Null for
|
|
135
135
|
// enabled crons and for crons the user disabled manually.
|
|
136
136
|
lockdownDisabledAt DateTime?
|
|
137
|
+
// Run-now request (COS-42.1): stamped by POST .../run-now, cleared when the
|
|
138
|
+
// runtime creates the run (commandVariant "run-now"). The runtime honours a
|
|
139
|
+
// stamp at most once and only while it is fresh (15 minutes).
|
|
140
|
+
runRequestedAt DateTime?
|
|
141
|
+
runRequestedBy String?
|
|
137
142
|
createdAt DateTime @default(now())
|
|
138
143
|
updatedAt DateTime @updatedAt
|
|
139
144
|
|
|
@@ -426,6 +431,59 @@ model AgentAsk {
|
|
|
426
431
|
@@index([fromAgentId, createdAt])
|
|
427
432
|
}
|
|
428
433
|
|
|
434
|
+
// ─── Agent Cron Run Requests ────────────────────────────────────────────────
|
|
435
|
+
// Audit row per POST /agents/{id}/crons/{cronId}/run-now (COS-42.1), written
|
|
436
|
+
// before the 202 returns. status: requested | honoured | expired. Holds no
|
|
437
|
+
// prompt text. honouredRunId links the AgentCronRun the runtime created.
|
|
438
|
+
|
|
439
|
+
model AgentCronRunRequest {
|
|
440
|
+
id String @id @default(cuid())
|
|
441
|
+
cronId String
|
|
442
|
+
agentId String
|
|
443
|
+
requestedBy String
|
|
444
|
+
requestedAt DateTime
|
|
445
|
+
status String @default("requested")
|
|
446
|
+
honouredRunId String?
|
|
447
|
+
createdAt DateTime @default(now())
|
|
448
|
+
|
|
449
|
+
@@index([cronId, requestedAt])
|
|
450
|
+
@@index([agentId, requestedAt])
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ─── Cron-run transcripts (COS-43.1) ────────────────────────────────────────
|
|
454
|
+
// One redacted session transcript per cron run, uploaded by the agent runtime
|
|
455
|
+
// (already redacted + tail-truncated to 1 MiB) and served only to admin /
|
|
456
|
+
// platform callers behind SHIPWRIGHT_TRANSCRIPT_API_ENABLED. No FK on runId:
|
|
457
|
+
// the retention sweep (newest 200 per agent) is the only lifecycle. The body
|
|
458
|
+
// is never logged. AgentTranscriptRead is the audit row written before every
|
|
459
|
+
// transcript read.
|
|
460
|
+
|
|
461
|
+
model AgentCronRunTranscript {
|
|
462
|
+
id String @id @default(cuid())
|
|
463
|
+
runId String @unique
|
|
464
|
+
agentId String
|
|
465
|
+
cronId String
|
|
466
|
+
sessionId String?
|
|
467
|
+
bytes Int
|
|
468
|
+
truncated Boolean
|
|
469
|
+
redactions Int
|
|
470
|
+
body String @db.Text
|
|
471
|
+
createdAt DateTime @default(now())
|
|
472
|
+
|
|
473
|
+
@@index([agentId, createdAt])
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
model AgentTranscriptRead {
|
|
477
|
+
id String @id @default(cuid())
|
|
478
|
+
runId String
|
|
479
|
+
agentId String
|
|
480
|
+
reader String
|
|
481
|
+
scopeKind String
|
|
482
|
+
at DateTime @default(now())
|
|
483
|
+
|
|
484
|
+
@@index([agentId, at])
|
|
485
|
+
}
|
|
486
|
+
|
|
429
487
|
// ─── Agent GitHub Installations Snapshot ────────────────────────────────────
|
|
430
488
|
// One row per agent holding the agent's most recently reported GitHub App
|
|
431
489
|
// installations (owner, installation id, state, sanitized lastError). PUT
|
package/src/admin-boot.ts
CHANGED
|
@@ -72,6 +72,8 @@ import {
|
|
|
72
72
|
import { createAdminUIApp } from "./admin-ui.ts";
|
|
73
73
|
import { AgentAskService } from "./agent-ask.ts";
|
|
74
74
|
import { parseAgentAskConfig } from "./agent-ask-config.ts";
|
|
75
|
+
import { CronRunNowService } from "./cron-run-now.ts";
|
|
76
|
+
import { parseCronRunNowConfig } from "./cron-run-now-config.ts";
|
|
75
77
|
import { AgentChatTokenService } from "./agent-chat-tokens.ts";
|
|
76
78
|
import { AgentCronJobService } from "./agent-cron-jobs.ts";
|
|
77
79
|
import { AgentCronRunStatsService } from "./agent-cron-run-stats.ts";
|
|
@@ -92,6 +94,7 @@ import {
|
|
|
92
94
|
} from "./caller-scope.ts";
|
|
93
95
|
import { parseChatCredentialConfig } from "./chat-credential-config.ts";
|
|
94
96
|
import { HttpChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
|
|
97
|
+
import { CronRunTranscriptService } from "./cron-run-transcripts.ts";
|
|
95
98
|
import { isDevAuthAllowed } from "./dev-auth-guard.ts";
|
|
96
99
|
import { HttpGithubAppProvisioningClient } from "./github-app-provisioning-client.ts";
|
|
97
100
|
import { HttpGoogleAuthClient } from "./google-auth-client.ts";
|
|
@@ -106,6 +109,7 @@ import { HttpSlackProvisioningClient } from "./slack-provisioning-client.ts";
|
|
|
106
109
|
import { SpecialtyRegistry } from "./specialty-manifest-loader.ts";
|
|
107
110
|
import { runStartupPolicyReconcile } from "./startup-policy-reconcile.ts";
|
|
108
111
|
import { makeTokenCrypto } from "./token-crypto.ts";
|
|
112
|
+
import { parseTranscriptConfig } from "./transcript-config.ts";
|
|
109
113
|
import {
|
|
110
114
|
resolveTrialExpirySweepIntervalMs,
|
|
111
115
|
TrialExpirySweeper,
|
|
@@ -290,6 +294,11 @@ export async function startAdmin(
|
|
|
290
294
|
|
|
291
295
|
// Agent-to-agent ask (COS-31.1). Throws at boot on a malformed allowlist.
|
|
292
296
|
const agentAskConfig = parseAgentAskConfig(process.env);
|
|
297
|
+
// Cron run-now (COS-42.1).
|
|
298
|
+
const cronRunNowConfig = parseCronRunNowConfig(process.env);
|
|
299
|
+
|
|
300
|
+
// Cron-run transcript API (COS-43.1): flag off by default.
|
|
301
|
+
const transcriptConfig = parseTranscriptConfig(process.env);
|
|
293
302
|
|
|
294
303
|
// Web Push (CFB-4.2). Enabled ONLY when public + private + subject are all
|
|
295
304
|
// set — a partial config must not half-enable the feature. When disabled,
|
|
@@ -415,6 +424,7 @@ export async function startAdmin(
|
|
|
415
424
|
adminApiKeys,
|
|
416
425
|
agentTokenService,
|
|
417
426
|
anthropicFederationEnabled,
|
|
427
|
+
transcriptUploadEnabled: transcriptConfig.enabled,
|
|
418
428
|
});
|
|
419
429
|
|
|
420
430
|
root.route("/agents", runtimeApp);
|
|
@@ -456,7 +466,15 @@ export async function startAdmin(
|
|
|
456
466
|
config: agentAskConfig,
|
|
457
467
|
store: new AgentAskService(prisma),
|
|
458
468
|
},
|
|
469
|
+
cronRunNow: {
|
|
470
|
+
config: cronRunNowConfig,
|
|
471
|
+
store: new CronRunNowService(prisma),
|
|
472
|
+
},
|
|
459
473
|
...(chatClient ? { chatClient } : {}),
|
|
474
|
+
transcripts: {
|
|
475
|
+
config: transcriptConfig,
|
|
476
|
+
store: new CronRunTranscriptService(prisma),
|
|
477
|
+
},
|
|
460
478
|
...resolvedExtensions.apiDeps,
|
|
461
479
|
});
|
|
462
480
|
root.route("/", adminApiApp);
|
package/src/admin-ui.fixtures.ts
CHANGED
|
@@ -116,6 +116,8 @@ function makeFakePrisma(opts: {
|
|
|
116
116
|
system: true,
|
|
117
117
|
parentCronId: null,
|
|
118
118
|
lockdownDisabledAt: null,
|
|
119
|
+
runRequestedAt: null,
|
|
120
|
+
runRequestedBy: null,
|
|
119
121
|
createdAt: new Date("2024-01-01"),
|
|
120
122
|
updatedAt: new Date("2024-01-01"),
|
|
121
123
|
...seed,
|
|
@@ -156,6 +158,8 @@ function makeFakePrisma(opts: {
|
|
|
156
158
|
system: true,
|
|
157
159
|
parentCronId: null,
|
|
158
160
|
lockdownDisabledAt: null,
|
|
161
|
+
runRequestedAt: null,
|
|
162
|
+
runRequestedBy: null,
|
|
159
163
|
createdAt: new Date("2024-01-01"),
|
|
160
164
|
updatedAt: new Date("2024-01-01"),
|
|
161
165
|
...data,
|
|
@@ -276,6 +276,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
276
276
|
system: false,
|
|
277
277
|
parentCronId: null,
|
|
278
278
|
lockdownDisabledAt: null,
|
|
279
|
+
runRequestedAt: null,
|
|
280
|
+
runRequestedBy: null,
|
|
279
281
|
createdAt: new Date("2024-01-01"),
|
|
280
282
|
updatedAt: new Date("2024-01-01"),
|
|
281
283
|
}),
|
|
@@ -293,6 +295,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
293
295
|
system: false,
|
|
294
296
|
parentCronId: null,
|
|
295
297
|
lockdownDisabledAt: null,
|
|
298
|
+
runRequestedAt: null,
|
|
299
|
+
runRequestedBy: null,
|
|
296
300
|
createdAt: new Date("2024-01-01"),
|
|
297
301
|
updatedAt: new Date("2024-01-01"),
|
|
298
302
|
}),
|
|
@@ -311,6 +315,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
311
315
|
system: false,
|
|
312
316
|
parentCronId: null,
|
|
313
317
|
lockdownDisabledAt: null,
|
|
318
|
+
runRequestedAt: null,
|
|
319
|
+
runRequestedBy: null,
|
|
314
320
|
createdAt: new Date("2024-01-01"),
|
|
315
321
|
updatedAt: new Date("2024-01-01"),
|
|
316
322
|
}),
|
|
@@ -328,6 +334,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
328
334
|
system: false,
|
|
329
335
|
parentCronId: null,
|
|
330
336
|
lockdownDisabledAt: null,
|
|
337
|
+
runRequestedAt: null,
|
|
338
|
+
runRequestedBy: null,
|
|
331
339
|
createdAt: new Date("2024-01-01"),
|
|
332
340
|
updatedAt: new Date("2024-01-01"),
|
|
333
341
|
}),
|
|
@@ -345,6 +353,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
345
353
|
system: false,
|
|
346
354
|
parentCronId: null,
|
|
347
355
|
lockdownDisabledAt: null,
|
|
356
|
+
runRequestedAt: null,
|
|
357
|
+
runRequestedBy: null,
|
|
348
358
|
createdAt: new Date("2024-01-01"),
|
|
349
359
|
updatedAt: new Date("2024-01-01"),
|
|
350
360
|
}),
|
|
@@ -1489,3 +1489,96 @@ describeOrSkip("AgentCronRunService — run telemetry (integration)", () => {
|
|
|
1489
1489
|
).toBe(0);
|
|
1490
1490
|
});
|
|
1491
1491
|
});
|
|
1492
|
+
|
|
1493
|
+
describeOrSkip(
|
|
1494
|
+
"AgentCronRunService run-now clear-on-create (integration)",
|
|
1495
|
+
() => {
|
|
1496
|
+
let prisma: PrismaClient;
|
|
1497
|
+
let agentId: string;
|
|
1498
|
+
let cronId: string;
|
|
1499
|
+
let runService: AgentCronRunService;
|
|
1500
|
+
|
|
1501
|
+
async function stamp(requestedAt: Date) {
|
|
1502
|
+
await prisma.agentCronJob.update({
|
|
1503
|
+
where: { id: cronId },
|
|
1504
|
+
data: { runRequestedAt: requestedAt, runRequestedBy: "tester" },
|
|
1505
|
+
});
|
|
1506
|
+
return prisma.agentCronRunRequest.create({
|
|
1507
|
+
data: {
|
|
1508
|
+
cronId,
|
|
1509
|
+
agentId,
|
|
1510
|
+
requestedBy: "tester",
|
|
1511
|
+
requestedAt,
|
|
1512
|
+
status: "requested",
|
|
1513
|
+
},
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
beforeEach(async () => {
|
|
1518
|
+
prisma = makePrisma();
|
|
1519
|
+
await prisma.agentCronRunRequest.deleteMany();
|
|
1520
|
+
await prisma.agentCronRun.deleteMany();
|
|
1521
|
+
await prisma.agentCronJob.deleteMany();
|
|
1522
|
+
agentId = await createAgent(prisma, "Run-now Agent");
|
|
1523
|
+
cronId = await createCron(new AgentCronJobService(prisma), agentId);
|
|
1524
|
+
runService = new AgentCronRunService(prisma);
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
afterEach(async () => {
|
|
1528
|
+
await prisma.agentCronRunRequest.deleteMany();
|
|
1529
|
+
await prisma.agentCronRun.deleteMany();
|
|
1530
|
+
await prisma.agentCronJob.deleteMany();
|
|
1531
|
+
await prisma.agent.deleteMany({ where: { id: agentId } });
|
|
1532
|
+
await prisma.$disconnect();
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
it("commandVariant run-now clears the stamp and honours the newest request", async () => {
|
|
1536
|
+
await stamp(new Date("2026-10-26T11:00:00Z"));
|
|
1537
|
+
const newest = await stamp(new Date("2026-10-26T11:30:00Z"));
|
|
1538
|
+
|
|
1539
|
+
const run = await runService.create(cronId, agentId, {
|
|
1540
|
+
startedAt: new Date("2026-10-26T11:31:00Z"),
|
|
1541
|
+
commandVariant: "run-now",
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
const job = await prisma.agentCronJob.findUniqueOrThrow({
|
|
1545
|
+
where: { id: cronId },
|
|
1546
|
+
});
|
|
1547
|
+
expect(job.runRequestedAt).toBeNull();
|
|
1548
|
+
expect(job.runRequestedBy).toBeNull();
|
|
1549
|
+
const honoured = await prisma.agentCronRunRequest.findUniqueOrThrow({
|
|
1550
|
+
where: { id: newest.id },
|
|
1551
|
+
});
|
|
1552
|
+
expect(honoured.status).toBe("honoured");
|
|
1553
|
+
expect(honoured.honouredRunId).toBe(run.id);
|
|
1554
|
+
const stillRequested = await prisma.agentCronRunRequest.count({
|
|
1555
|
+
where: { cronId, status: "requested" },
|
|
1556
|
+
});
|
|
1557
|
+
expect(stillRequested).toBe(0);
|
|
1558
|
+
const superseded = await prisma.agentCronRunRequest.count({
|
|
1559
|
+
where: { cronId, status: "expired" },
|
|
1560
|
+
});
|
|
1561
|
+
expect(superseded).toBe(1);
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
it("any other variant leaves the stamp and the request alone", async () => {
|
|
1565
|
+
const req = await stamp(new Date("2026-10-26T11:30:00Z"));
|
|
1566
|
+
await runService.create(cronId, agentId, {
|
|
1567
|
+
startedAt: new Date("2026-10-26T11:31:00Z"),
|
|
1568
|
+
commandVariant: "default",
|
|
1569
|
+
});
|
|
1570
|
+
await runService.create(cronId, agentId, {
|
|
1571
|
+
startedAt: new Date("2026-10-26T11:32:00Z"),
|
|
1572
|
+
});
|
|
1573
|
+
const job = await prisma.agentCronJob.findUniqueOrThrow({
|
|
1574
|
+
where: { id: cronId },
|
|
1575
|
+
});
|
|
1576
|
+
expect(job.runRequestedAt).not.toBeNull();
|
|
1577
|
+
const row = await prisma.agentCronRunRequest.findUniqueOrThrow({
|
|
1578
|
+
where: { id: req.id },
|
|
1579
|
+
});
|
|
1580
|
+
expect(row.status).toBe("requested");
|
|
1581
|
+
expect(row.honouredRunId).toBeNull();
|
|
1582
|
+
});
|
|
1583
|
+
},
|
|
1584
|
+
);
|
package/src/agent-cron-runs.ts
CHANGED
|
@@ -15,6 +15,9 @@ import { NotFoundError } from "./errors.ts";
|
|
|
15
15
|
|
|
16
16
|
export type { AgentCronRun };
|
|
17
17
|
|
|
18
|
+
/** commandVariant a runtime reports when it honours a run-now request. */
|
|
19
|
+
const RUN_NOW_VARIANT = "run-now";
|
|
20
|
+
|
|
18
21
|
/** An AgentCronRun with its per-model token/cost breakdown rows attached. */
|
|
19
22
|
type AgentCronRunWithModelBreakdown = Prisma.AgentCronRunGetPayload<{
|
|
20
23
|
include: { modelBreakdown: true };
|
|
@@ -193,21 +196,50 @@ export class AgentCronRunService {
|
|
|
193
196
|
throw new NotFoundError(`cron job ${cronId} not found`);
|
|
194
197
|
}
|
|
195
198
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
199
|
+
const data = {
|
|
200
|
+
cronId,
|
|
201
|
+
agentId,
|
|
202
|
+
startedAt: input.startedAt,
|
|
203
|
+
completedAt: input.completedAt ?? null,
|
|
204
|
+
skipped: input.skipped ?? false,
|
|
205
|
+
skipReason: input.skipReason ?? null,
|
|
206
|
+
outcome: input.outcome ?? null,
|
|
207
|
+
error: input.error ?? null,
|
|
208
|
+
phaseId: input.phaseId ?? null,
|
|
209
|
+
itemType: input.itemType ?? null,
|
|
210
|
+
itemId: input.itemId ?? null,
|
|
211
|
+
commandVariant: input.commandVariant ?? null,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (input.commandVariant !== RUN_NOW_VARIANT) {
|
|
215
|
+
return this.prisma.agentCronRun.create({ data });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Run-now (COS-42.1): creating the run honours the pending request, so
|
|
219
|
+
// clear the stamp and link the newest requested audit row to this run.
|
|
220
|
+
return this.prisma.$transaction(async (tx) => {
|
|
221
|
+
const run = await tx.agentCronRun.create({ data });
|
|
222
|
+
await tx.agentCronJob.update({
|
|
223
|
+
where: { id: cronId },
|
|
224
|
+
data: { runRequestedAt: null, runRequestedBy: null },
|
|
225
|
+
});
|
|
226
|
+
const request = await tx.agentCronRunRequest.findFirst({
|
|
227
|
+
where: { cronId, status: "requested" },
|
|
228
|
+
orderBy: [{ requestedAt: "desc" }, { createdAt: "desc" }],
|
|
229
|
+
});
|
|
230
|
+
if (request) {
|
|
231
|
+
await tx.agentCronRunRequest.update({
|
|
232
|
+
where: { id: request.id },
|
|
233
|
+
data: { status: "honoured", honouredRunId: run.id },
|
|
234
|
+
});
|
|
235
|
+
// Older still-pending requests were superseded by this one and can
|
|
236
|
+
// never be honoured: record them as expired rather than pending.
|
|
237
|
+
await tx.agentCronRunRequest.updateMany({
|
|
238
|
+
where: { cronId, status: "requested", id: { not: request.id } },
|
|
239
|
+
data: { status: "expired" },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return run;
|
|
211
243
|
});
|
|
212
244
|
}
|
|
213
245
|
|
|
@@ -183,6 +183,8 @@ const MOCK_CRON = {
|
|
|
183
183
|
system: false,
|
|
184
184
|
parentCronId: null as string | null,
|
|
185
185
|
lockdownDisabledAt: null,
|
|
186
|
+
runRequestedAt: null,
|
|
187
|
+
runRequestedBy: null,
|
|
186
188
|
createdAt: new Date("2024-01-01"),
|
|
187
189
|
updatedAt: new Date("2024-01-01"),
|
|
188
190
|
};
|
|
@@ -380,6 +382,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
380
382
|
system: false,
|
|
381
383
|
parentCronId: null,
|
|
382
384
|
lockdownDisabledAt: null,
|
|
385
|
+
runRequestedAt: null,
|
|
386
|
+
runRequestedBy: null,
|
|
383
387
|
createdAt: new Date("2024-01-01"),
|
|
384
388
|
updatedAt: new Date("2024-01-01"),
|
|
385
389
|
}),
|
|
@@ -397,6 +401,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
397
401
|
system: false,
|
|
398
402
|
parentCronId: null,
|
|
399
403
|
lockdownDisabledAt: null,
|
|
404
|
+
runRequestedAt: null,
|
|
405
|
+
runRequestedBy: null,
|
|
400
406
|
createdAt: new Date("2024-01-01"),
|
|
401
407
|
updatedAt: new Date("2024-01-01"),
|
|
402
408
|
}),
|
|
@@ -415,6 +421,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
415
421
|
system: false,
|
|
416
422
|
parentCronId: null,
|
|
417
423
|
lockdownDisabledAt: null,
|
|
424
|
+
runRequestedAt: null,
|
|
425
|
+
runRequestedBy: null,
|
|
418
426
|
createdAt: new Date("2024-01-01"),
|
|
419
427
|
updatedAt: new Date("2024-01-01"),
|
|
420
428
|
}),
|
|
@@ -432,6 +440,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
432
440
|
system: false,
|
|
433
441
|
parentCronId: null,
|
|
434
442
|
lockdownDisabledAt: null,
|
|
443
|
+
runRequestedAt: null,
|
|
444
|
+
runRequestedBy: null,
|
|
435
445
|
createdAt: new Date("2024-01-01"),
|
|
436
446
|
updatedAt: new Date("2024-01-01"),
|
|
437
447
|
}),
|
|
@@ -449,6 +459,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
449
459
|
system: false,
|
|
450
460
|
parentCronId: null,
|
|
451
461
|
lockdownDisabledAt: null,
|
|
462
|
+
runRequestedAt: null,
|
|
463
|
+
runRequestedBy: null,
|
|
452
464
|
createdAt: new Date("2024-01-01"),
|
|
453
465
|
updatedAt: new Date("2024-01-01"),
|
|
454
466
|
}),
|
|
@@ -4631,6 +4643,8 @@ describe("admin API — cron runs", () => {
|
|
|
4631
4643
|
system: false,
|
|
4632
4644
|
parentCronId: null,
|
|
4633
4645
|
lockdownDisabledAt: null,
|
|
4646
|
+
runRequestedAt: null,
|
|
4647
|
+
runRequestedBy: null,
|
|
4634
4648
|
createdAt: new Date("2024-01-01"),
|
|
4635
4649
|
updatedAt: new Date("2024-01-01"),
|
|
4636
4650
|
lastRun: null,
|
|
@@ -4649,6 +4663,8 @@ describe("admin API — cron runs", () => {
|
|
|
4649
4663
|
name: "shipwright-dev-task",
|
|
4650
4664
|
system: true,
|
|
4651
4665
|
lockdownDisabledAt: null,
|
|
4666
|
+
runRequestedAt: null,
|
|
4667
|
+
runRequestedBy: null,
|
|
4652
4668
|
parentCronId: CRON_ID,
|
|
4653
4669
|
createdAt: new Date("2024-01-01"),
|
|
4654
4670
|
updatedAt: new Date("2024-01-01"),
|
|
@@ -4936,6 +4952,8 @@ function makeMockDepsWithRunSummary(): AdminDeps {
|
|
|
4936
4952
|
system: false,
|
|
4937
4953
|
parentCronId: null,
|
|
4938
4954
|
lockdownDisabledAt: null,
|
|
4955
|
+
runRequestedAt: null,
|
|
4956
|
+
runRequestedBy: null,
|
|
4939
4957
|
createdAt: new Date("2024-01-01"),
|
|
4940
4958
|
updatedAt: new Date("2024-01-01"),
|
|
4941
4959
|
lastRun: {
|
package/src/agents-api.ts
CHANGED
|
@@ -46,6 +46,10 @@ import {
|
|
|
46
46
|
registerAgentAskRoutes,
|
|
47
47
|
} from "./agent-ask.ts";
|
|
48
48
|
import type { AgentChatTokenService } from "./agent-chat-tokens.ts";
|
|
49
|
+
import {
|
|
50
|
+
type CronRunNowDeps,
|
|
51
|
+
registerCronRunNowRoutes,
|
|
52
|
+
} from "./cron-run-now.ts";
|
|
49
53
|
import type {
|
|
50
54
|
AgentCronJobService,
|
|
51
55
|
AgentCronJobWithRunSummary,
|
|
@@ -83,6 +87,10 @@ import {
|
|
|
83
87
|
memberOnlyCallerScopeResolver,
|
|
84
88
|
} from "./caller-scope.ts";
|
|
85
89
|
import type { ChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
|
|
90
|
+
import {
|
|
91
|
+
type CronRunTranscriptDeps,
|
|
92
|
+
registerCronRunTranscriptRoutes,
|
|
93
|
+
} from "./cron-run-transcripts.ts";
|
|
86
94
|
import {
|
|
87
95
|
ApiError,
|
|
88
96
|
BadRequestError,
|
|
@@ -285,12 +293,23 @@ export interface AdminDeps {
|
|
|
285
293
|
* 404. See agent-ask.ts.
|
|
286
294
|
*/
|
|
287
295
|
agentAsk?: AgentAskDeps;
|
|
296
|
+
/**
|
|
297
|
+
* Cron run-now (COS-42.1): flag config, the stamp+audit store and an
|
|
298
|
+
* injectable clock. Absent or flag off -> the run-now route 404s. See
|
|
299
|
+
* cron-run-now.ts.
|
|
300
|
+
*/
|
|
301
|
+
cronRunNow?: CronRunNowDeps;
|
|
288
302
|
/**
|
|
289
303
|
* Chat-service thread/message client behind POST /agents/:id/ask
|
|
290
304
|
* (admin-boot wires the HttpChatClient the chat UI uses). Absent → the ask
|
|
291
305
|
* route 503s, even with the flag on.
|
|
292
306
|
*/
|
|
293
307
|
chatClient?: AskChatClient;
|
|
308
|
+
/**
|
|
309
|
+
* Cron-run transcripts (COS-43.1): flag config + store. Absent or flag off
|
|
310
|
+
* -> the transcript routes 404. See cron-run-transcripts.ts.
|
|
311
|
+
*/
|
|
312
|
+
transcripts?: CronRunTranscriptDeps;
|
|
294
313
|
}
|
|
295
314
|
|
|
296
315
|
export type { AdminApiKey };
|
|
@@ -2335,6 +2354,14 @@ export function createAdminApp(deps: AdminDeps): OpenAPIHono<AdminAuthEnv> {
|
|
|
2335
2354
|
agentService,
|
|
2336
2355
|
});
|
|
2337
2356
|
|
|
2357
|
+
// ─── Cron run-now (COS-42.1) ──────────────────────────────────────────────
|
|
2358
|
+
|
|
2359
|
+
registerCronRunNowRoutes(app, { cronRunNow: deps.cronRunNow });
|
|
2360
|
+
|
|
2361
|
+
// ─── Cron-run transcripts (COS-43.1) ──────────────────────────────────────
|
|
2362
|
+
|
|
2363
|
+
registerCronRunTranscriptRoutes(app, { transcripts: deps.transcripts });
|
|
2364
|
+
|
|
2338
2365
|
// ─── Plugins ───────────────────────────────────────────────────────────────
|
|
2339
2366
|
|
|
2340
2367
|
// POST /agents/:id/plugins — add a plugin
|
|
@@ -2516,10 +2543,12 @@ export function createAdminApp(deps: AdminDeps): OpenAPIHono<AdminAuthEnv> {
|
|
|
2516
2543
|
function serializeCron(cron: {
|
|
2517
2544
|
createdAt: Date;
|
|
2518
2545
|
updatedAt: Date;
|
|
2546
|
+
runRequestedAt?: Date | null;
|
|
2519
2547
|
[k: string]: unknown;
|
|
2520
2548
|
}): z.infer<typeof AgentCronJobSchema> {
|
|
2521
2549
|
return {
|
|
2522
2550
|
...cron,
|
|
2551
|
+
runRequestedAt: cron.runRequestedAt?.toISOString() ?? null,
|
|
2523
2552
|
createdAt: cron.createdAt.toISOString(),
|
|
2524
2553
|
updatedAt: cron.updatedAt.toISOString(),
|
|
2525
2554
|
} as z.infer<typeof AgentCronJobSchema>;
|
|
@@ -2549,6 +2578,7 @@ function serializeCronWithSummary(job: AgentCronJobWithRunSummary): z.infer<
|
|
|
2549
2578
|
name: job.name,
|
|
2550
2579
|
system: job.system,
|
|
2551
2580
|
parentCronId: job.parentCronId,
|
|
2581
|
+
runRequestedAt: job.runRequestedAt?.toISOString() ?? null,
|
|
2552
2582
|
createdAt: job.createdAt.toISOString(),
|
|
2553
2583
|
updatedAt: job.updatedAt.toISOString(),
|
|
2554
2584
|
lastRun: job.lastRun
|
package/src/api-auth.ts
CHANGED
|
@@ -106,6 +106,7 @@ export const PLATFORM_OPS_ROUTES: readonly PlatformOpsRoute[] = [
|
|
|
106
106
|
{ method: "DELETE", path: "/agents/{id}/repos", subpaths: true },
|
|
107
107
|
{ method: "PATCH", path: "/agents/{id}/crons/{cronId}" },
|
|
108
108
|
{ method: "POST", path: "/agents/{id}/ask" },
|
|
109
|
+
{ method: "POST", path: "/agents/{id}/crons/{cronId}/run-now" },
|
|
109
110
|
{ method: "POST", path: "/agents/{id}/chat-credentials", subpaths: true },
|
|
110
111
|
{ method: "DELETE", path: "/agents/{id}/chat-credentials", subpaths: true },
|
|
111
112
|
];
|