@app-vitals/agent-admin 0.2.0 → 0.3.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 +291 -0
- package/package.json +2 -2
- package/prisma/client/browser.ts +10 -0
- package/prisma/client/client.ts +10 -0
- package/prisma/client/internal/class.ts +24 -4
- package/prisma/client/internal/prismaNamespace.ts +181 -1
- package/prisma/client/internal/prismaNamespaceBrowser.ts +30 -0
- package/prisma/client/models/AgentCronRunTranscript.ts +1339 -0
- package/prisma/client/models/AgentTranscriptRead.ts +1177 -0
- package/prisma/client/models.ts +2 -0
- package/prisma/migrations/20261027000000_add_cron_run_transcripts/migration.sql +37 -0
- package/prisma/schema/schema.prisma +34 -0
- package/src/admin-boot.ts +10 -0
- package/src/agents-api.ts +13 -0
- package/src/api.smoke.test.ts +27 -0
- package/src/api.ts +15 -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 +64 -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,8 @@ 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/AgentCronRunTranscript.ts'
|
|
31
|
+
export type * from './models/AgentTranscriptRead.ts'
|
|
30
32
|
export type * from './models/AgentGitHubInstallationsSnapshot.ts'
|
|
31
33
|
export type * from './models/PushSubscription.ts'
|
|
32
34
|
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");
|
|
@@ -426,6 +426,40 @@ model AgentAsk {
|
|
|
426
426
|
@@index([fromAgentId, createdAt])
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
// ─── Cron-run transcripts (COS-43.1) ────────────────────────────────────────
|
|
430
|
+
// One redacted session transcript per cron run, uploaded by the agent runtime
|
|
431
|
+
// (already redacted + tail-truncated to 1 MiB) and served only to admin /
|
|
432
|
+
// platform callers behind SHIPWRIGHT_TRANSCRIPT_API_ENABLED. No FK on runId:
|
|
433
|
+
// the retention sweep (newest 200 per agent) is the only lifecycle. The body
|
|
434
|
+
// is never logged. AgentTranscriptRead is the audit row written before every
|
|
435
|
+
// transcript read.
|
|
436
|
+
|
|
437
|
+
model AgentCronRunTranscript {
|
|
438
|
+
id String @id @default(cuid())
|
|
439
|
+
runId String @unique
|
|
440
|
+
agentId String
|
|
441
|
+
cronId String
|
|
442
|
+
sessionId String?
|
|
443
|
+
bytes Int
|
|
444
|
+
truncated Boolean
|
|
445
|
+
redactions Int
|
|
446
|
+
body String @db.Text
|
|
447
|
+
createdAt DateTime @default(now())
|
|
448
|
+
|
|
449
|
+
@@index([agentId, createdAt])
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
model AgentTranscriptRead {
|
|
453
|
+
id String @id @default(cuid())
|
|
454
|
+
runId String
|
|
455
|
+
agentId String
|
|
456
|
+
reader String
|
|
457
|
+
scopeKind String
|
|
458
|
+
at DateTime @default(now())
|
|
459
|
+
|
|
460
|
+
@@index([agentId, at])
|
|
461
|
+
}
|
|
462
|
+
|
|
429
463
|
// ─── Agent GitHub Installations Snapshot ────────────────────────────────────
|
|
430
464
|
// One row per agent holding the agent's most recently reported GitHub App
|
|
431
465
|
// installations (owner, installation id, state, sanitized lastError). PUT
|
package/src/admin-boot.ts
CHANGED
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
} from "./caller-scope.ts";
|
|
93
93
|
import { parseChatCredentialConfig } from "./chat-credential-config.ts";
|
|
94
94
|
import { HttpChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
|
|
95
|
+
import { CronRunTranscriptService } from "./cron-run-transcripts.ts";
|
|
95
96
|
import { isDevAuthAllowed } from "./dev-auth-guard.ts";
|
|
96
97
|
import { HttpGithubAppProvisioningClient } from "./github-app-provisioning-client.ts";
|
|
97
98
|
import { HttpGoogleAuthClient } from "./google-auth-client.ts";
|
|
@@ -106,6 +107,7 @@ import { HttpSlackProvisioningClient } from "./slack-provisioning-client.ts";
|
|
|
106
107
|
import { SpecialtyRegistry } from "./specialty-manifest-loader.ts";
|
|
107
108
|
import { runStartupPolicyReconcile } from "./startup-policy-reconcile.ts";
|
|
108
109
|
import { makeTokenCrypto } from "./token-crypto.ts";
|
|
110
|
+
import { parseTranscriptConfig } from "./transcript-config.ts";
|
|
109
111
|
import {
|
|
110
112
|
resolveTrialExpirySweepIntervalMs,
|
|
111
113
|
TrialExpirySweeper,
|
|
@@ -291,6 +293,9 @@ export async function startAdmin(
|
|
|
291
293
|
// Agent-to-agent ask (COS-31.1). Throws at boot on a malformed allowlist.
|
|
292
294
|
const agentAskConfig = parseAgentAskConfig(process.env);
|
|
293
295
|
|
|
296
|
+
// Cron-run transcript API (COS-43.1): flag off by default.
|
|
297
|
+
const transcriptConfig = parseTranscriptConfig(process.env);
|
|
298
|
+
|
|
294
299
|
// Web Push (CFB-4.2). Enabled ONLY when public + private + subject are all
|
|
295
300
|
// set — a partial config must not half-enable the feature. When disabled,
|
|
296
301
|
// pushService is undefined, the push routes 503, and the toggle renders "".
|
|
@@ -415,6 +420,7 @@ export async function startAdmin(
|
|
|
415
420
|
adminApiKeys,
|
|
416
421
|
agentTokenService,
|
|
417
422
|
anthropicFederationEnabled,
|
|
423
|
+
transcriptUploadEnabled: transcriptConfig.enabled,
|
|
418
424
|
});
|
|
419
425
|
|
|
420
426
|
root.route("/agents", runtimeApp);
|
|
@@ -457,6 +463,10 @@ export async function startAdmin(
|
|
|
457
463
|
store: new AgentAskService(prisma),
|
|
458
464
|
},
|
|
459
465
|
...(chatClient ? { chatClient } : {}),
|
|
466
|
+
transcripts: {
|
|
467
|
+
config: transcriptConfig,
|
|
468
|
+
store: new CronRunTranscriptService(prisma),
|
|
469
|
+
},
|
|
460
470
|
...resolvedExtensions.apiDeps,
|
|
461
471
|
});
|
|
462
472
|
root.route("/", adminApiApp);
|
package/src/agents-api.ts
CHANGED
|
@@ -83,6 +83,10 @@ import {
|
|
|
83
83
|
memberOnlyCallerScopeResolver,
|
|
84
84
|
} from "./caller-scope.ts";
|
|
85
85
|
import type { ChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
|
|
86
|
+
import {
|
|
87
|
+
type CronRunTranscriptDeps,
|
|
88
|
+
registerCronRunTranscriptRoutes,
|
|
89
|
+
} from "./cron-run-transcripts.ts";
|
|
86
90
|
import {
|
|
87
91
|
ApiError,
|
|
88
92
|
BadRequestError,
|
|
@@ -291,6 +295,11 @@ export interface AdminDeps {
|
|
|
291
295
|
* route 503s, even with the flag on.
|
|
292
296
|
*/
|
|
293
297
|
chatClient?: AskChatClient;
|
|
298
|
+
/**
|
|
299
|
+
* Cron-run transcripts (COS-43.1): flag config + store. Absent or flag off
|
|
300
|
+
* -> the transcript routes 404. See cron-run-transcripts.ts.
|
|
301
|
+
*/
|
|
302
|
+
transcripts?: CronRunTranscriptDeps;
|
|
294
303
|
}
|
|
295
304
|
|
|
296
305
|
export type { AdminApiKey };
|
|
@@ -2335,6 +2344,10 @@ export function createAdminApp(deps: AdminDeps): OpenAPIHono<AdminAuthEnv> {
|
|
|
2335
2344
|
agentService,
|
|
2336
2345
|
});
|
|
2337
2346
|
|
|
2347
|
+
// ─── Cron-run transcripts (COS-43.1) ──────────────────────────────────────
|
|
2348
|
+
|
|
2349
|
+
registerCronRunTranscriptRoutes(app, { transcripts: deps.transcripts });
|
|
2350
|
+
|
|
2338
2351
|
// ─── Plugins ───────────────────────────────────────────────────────────────
|
|
2339
2352
|
|
|
2340
2353
|
// POST /agents/:id/plugins — add a plugin
|
package/src/api.smoke.test.ts
CHANGED
|
@@ -236,6 +236,7 @@ function buildApp(opts?: {
|
|
|
236
236
|
maxFindings?: number;
|
|
237
237
|
cleanupMergedWorktrees?: boolean;
|
|
238
238
|
cleanupAfterDays?: number;
|
|
239
|
+
transcriptUploadEnabled?: boolean;
|
|
239
240
|
}) {
|
|
240
241
|
const hasAgent = opts?.hasAgent ?? true;
|
|
241
242
|
const bundle: AgentEnvBundle | null =
|
|
@@ -308,6 +309,7 @@ function buildApp(opts?: {
|
|
|
308
309
|
agentTokenService: { validate: async () => null },
|
|
309
310
|
sessionSecret: SESSION_SECRET,
|
|
310
311
|
anthropicFederationEnabled: opts?.anthropicFederationEnabled,
|
|
312
|
+
transcriptUploadEnabled: opts?.transcriptUploadEnabled,
|
|
311
313
|
});
|
|
312
314
|
}
|
|
313
315
|
|
|
@@ -326,6 +328,31 @@ describe("GET /:id/config with no phase-methodology service (platform only)", ()
|
|
|
326
328
|
});
|
|
327
329
|
});
|
|
328
330
|
|
|
331
|
+
describe("GET /:id/config features (COS-43.1)", () => {
|
|
332
|
+
const get = (app: ReturnType<typeof buildApp>) =>
|
|
333
|
+
app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
334
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("features default to all off", async () => {
|
|
338
|
+
const body = await (await get(buildApp())).json();
|
|
339
|
+
expect(body.features).toEqual({
|
|
340
|
+
transcriptUpload: false,
|
|
341
|
+
repoAccessCheck: false,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("features.transcriptUpload follows the transcript flag", async () => {
|
|
346
|
+
const body = await (
|
|
347
|
+
await get(buildApp({ transcriptUploadEnabled: true }))
|
|
348
|
+
).json();
|
|
349
|
+
expect(body.features).toEqual({
|
|
350
|
+
transcriptUpload: true,
|
|
351
|
+
repoAccessCheck: false,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
329
356
|
describe("GET /:id/config (mounted as GET /agents/:id/config from root)", () => {
|
|
330
357
|
test("200 with full bundle when agent exists with env vars, tools, plugins", async () => {
|
|
331
358
|
const app = buildApp();
|
package/src/api.ts
CHANGED
|
@@ -93,6 +93,11 @@ export interface AgentConfigResponse {
|
|
|
93
93
|
maxFindings: number;
|
|
94
94
|
cleanupMergedWorktrees: boolean;
|
|
95
95
|
cleanupAfterDays: number;
|
|
96
|
+
/**
|
|
97
|
+
* COS-43.1: feature flags the runtime acts on. Optional so fixtures built
|
|
98
|
+
* before it keep compiling; the live handler always populates it.
|
|
99
|
+
*/
|
|
100
|
+
features?: { transcriptUpload: boolean; repoAccessCheck: boolean };
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
interface AgentEnvServiceLike {
|
|
@@ -169,6 +174,11 @@ export interface AgentRuntimeDeps {
|
|
|
169
174
|
agentTokenService: Pick<AgentTokenService, "validate">;
|
|
170
175
|
/** WIF-2.1: Anthropic federation is enabled; defaults to false. */
|
|
171
176
|
anthropicFederationEnabled?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* COS-43.1: SHIPWRIGHT_TRANSCRIPT_API_ENABLED is on — surfaced to the runtime
|
|
179
|
+
* as `features.transcriptUpload` in the config bundle. Defaults to false.
|
|
180
|
+
*/
|
|
181
|
+
transcriptUploadEnabled?: boolean;
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
// ─── Route definitions ────────────────────────────────────────────────────────
|
|
@@ -342,6 +352,11 @@ export function createAgentRuntimeApp(deps: AgentRuntimeDeps): OpenAPIHono {
|
|
|
342
352
|
maxFindings: agent.maxFindings,
|
|
343
353
|
cleanupMergedWorktrees: agent.cleanupMergedWorktrees,
|
|
344
354
|
cleanupAfterDays: agent.cleanupAfterDays,
|
|
355
|
+
// COS-43.1: repoAccessCheck is wired by COS-44.1; false until then.
|
|
356
|
+
features: {
|
|
357
|
+
transcriptUpload: deps.transcriptUploadEnabled === true,
|
|
358
|
+
repoAccessCheck: false,
|
|
359
|
+
},
|
|
345
360
|
};
|
|
346
361
|
|
|
347
362
|
// The route's Zod schema requires phaseMethodology; a platform-only admin
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/cron-run-transcripts.integration.test.ts
|
|
3
|
+
* Round-trip and retention sweep against a real Postgres (COS-43.1).
|
|
4
|
+
*
|
|
5
|
+
* Requires DATABASE_URL_ADMIN_TEST to be set; skips otherwise.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
9
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
10
|
+
import { AgentService } from "./agents.ts";
|
|
11
|
+
import {
|
|
12
|
+
type AdminDeps,
|
|
13
|
+
createAdminApp,
|
|
14
|
+
parseAdminApiKeys,
|
|
15
|
+
} from "./agents-api.ts";
|
|
16
|
+
import {
|
|
17
|
+
CronRunTranscriptService,
|
|
18
|
+
TRANSCRIPT_RETENTION_PER_AGENT,
|
|
19
|
+
} from "./cron-run-transcripts.ts";
|
|
20
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
21
|
+
import { parseTranscriptConfig } from "./transcript-config.ts";
|
|
22
|
+
|
|
23
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
24
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
25
|
+
|
|
26
|
+
const ADMIN = "admin-key";
|
|
27
|
+
|
|
28
|
+
describeOrSkip("cron-run transcripts (integration)", () => {
|
|
29
|
+
let prisma: PrismaClient;
|
|
30
|
+
let agentId: string;
|
|
31
|
+
let otherAgentId: string;
|
|
32
|
+
let cronId: string;
|
|
33
|
+
let otherCronId: string;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
prisma = createAdminPrismaClient(TEST_DB as string);
|
|
37
|
+
await prisma.agentTranscriptRead.deleteMany();
|
|
38
|
+
await prisma.agentCronRunTranscript.deleteMany();
|
|
39
|
+
agentId = (await prisma.agent.create({ data: { name: "TxA" } })).id;
|
|
40
|
+
otherAgentId = (await prisma.agent.create({ data: { name: "TxB" } })).id;
|
|
41
|
+
cronId = (
|
|
42
|
+
await prisma.agentCronJob.create({
|
|
43
|
+
data: { agentId, name: "c", schedule: "* * * * *", prompt: "p" },
|
|
44
|
+
})
|
|
45
|
+
).id;
|
|
46
|
+
otherCronId = (
|
|
47
|
+
await prisma.agentCronJob.create({
|
|
48
|
+
data: {
|
|
49
|
+
agentId: otherAgentId,
|
|
50
|
+
name: "c",
|
|
51
|
+
schedule: "* * * * *",
|
|
52
|
+
prompt: "p",
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
).id;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(async () => {
|
|
59
|
+
await prisma.agentTranscriptRead.deleteMany();
|
|
60
|
+
await prisma.agentCronRunTranscript.deleteMany();
|
|
61
|
+
await prisma.agent.deleteMany({
|
|
62
|
+
where: { id: { in: [agentId, otherAgentId] } },
|
|
63
|
+
});
|
|
64
|
+
await prisma.$disconnect();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function makeApp() {
|
|
68
|
+
const deps = {
|
|
69
|
+
agentService: new AgentService(prisma),
|
|
70
|
+
agentTokenService: { validate: async () => null },
|
|
71
|
+
adminApiKeys: parseAdminApiKeys(`root:${ADMIN}:*`),
|
|
72
|
+
sessionSecret: "s".repeat(32),
|
|
73
|
+
prisma,
|
|
74
|
+
transcripts: {
|
|
75
|
+
config: parseTranscriptConfig({
|
|
76
|
+
SHIPWRIGHT_TRANSCRIPT_API_ENABLED: "enabled",
|
|
77
|
+
}),
|
|
78
|
+
store: new CronRunTranscriptService(prisma),
|
|
79
|
+
},
|
|
80
|
+
} as unknown as AdminDeps;
|
|
81
|
+
return createAdminApp(deps);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const newRun = (agent: string, cron: string, startedAt = new Date()) =>
|
|
85
|
+
prisma.agentCronRun.create({
|
|
86
|
+
data: { agentId: agent, cronId: cron, startedAt },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const put = (
|
|
90
|
+
app: ReturnType<typeof createAdminApp>,
|
|
91
|
+
runId: string,
|
|
92
|
+
body: string,
|
|
93
|
+
cron = cronId,
|
|
94
|
+
agent = agentId,
|
|
95
|
+
) =>
|
|
96
|
+
app.request(`/agents/${agent}/crons/${cron}/runs/${runId}/transcript`, {
|
|
97
|
+
method: "PUT",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
Authorization: `Bearer ${ADMIN}`,
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
sessionId: "s",
|
|
104
|
+
body,
|
|
105
|
+
truncated: false,
|
|
106
|
+
redactions: 1,
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("round-trips PUT then GET and writes an audit row", async () => {
|
|
111
|
+
const app = makeApp();
|
|
112
|
+
const run = await newRun(agentId, cronId);
|
|
113
|
+
expect((await put(app, run.id, "hello\n")).status).toBe(200);
|
|
114
|
+
const res = await app.request(
|
|
115
|
+
`/agents/${agentId}/crons/${cronId}/runs/${run.id}/transcript`,
|
|
116
|
+
{ headers: { Authorization: `Bearer ${ADMIN}` } },
|
|
117
|
+
);
|
|
118
|
+
expect(res.status).toBe(200);
|
|
119
|
+
const json = (await res.json()) as Record<string, unknown>;
|
|
120
|
+
expect(json).toMatchObject({
|
|
121
|
+
runId: run.id,
|
|
122
|
+
cronId,
|
|
123
|
+
sessionId: "s",
|
|
124
|
+
bytes: 6,
|
|
125
|
+
truncated: false,
|
|
126
|
+
redactions: 1,
|
|
127
|
+
body: "hello\n",
|
|
128
|
+
});
|
|
129
|
+
const reads = await prisma.agentTranscriptRead.findMany();
|
|
130
|
+
expect(reads).toHaveLength(1);
|
|
131
|
+
expect(reads[0]).toMatchObject({
|
|
132
|
+
runId: run.id,
|
|
133
|
+
agentId,
|
|
134
|
+
scopeKind: "admin",
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("upserts: a second PUT replaces the body, one row per run", async () => {
|
|
139
|
+
const app = makeApp();
|
|
140
|
+
const run = await newRun(agentId, cronId);
|
|
141
|
+
await put(app, run.id, "one");
|
|
142
|
+
await put(app, run.id, "two-two");
|
|
143
|
+
const rows = await prisma.agentCronRunTranscript.findMany();
|
|
144
|
+
expect(rows).toHaveLength(1);
|
|
145
|
+
expect(rows[0]?.body).toBe("two-two");
|
|
146
|
+
expect(rows[0]?.bytes).toBe(7);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("404s a run owned by a different agent or cron", async () => {
|
|
150
|
+
const app = makeApp();
|
|
151
|
+
const run = await newRun(otherAgentId, otherCronId);
|
|
152
|
+
expect((await put(app, run.id, "x")).status).toBe(404);
|
|
153
|
+
const mine = await newRun(agentId, cronId);
|
|
154
|
+
expect((await put(app, mine.id, "x", otherCronId)).status).toBe(404);
|
|
155
|
+
expect(await prisma.agentCronRunTranscript.count()).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it(`keeps only the newest ${TRANSCRIPT_RETENTION_PER_AGENT} per agent`, async () => {
|
|
159
|
+
const service = new CronRunTranscriptService(prisma);
|
|
160
|
+
const total = TRANSCRIPT_RETENTION_PER_AGENT + 5;
|
|
161
|
+
const base = Date.now() - total * 1000;
|
|
162
|
+
const runs: Array<{ id: string }> = [];
|
|
163
|
+
for (let i = 0; i < total; i++) {
|
|
164
|
+
runs.push(await newRun(agentId, cronId, new Date(base + i)));
|
|
165
|
+
}
|
|
166
|
+
for (const [i, run] of runs.entries()) {
|
|
167
|
+
await prisma.agentCronRunTranscript.create({
|
|
168
|
+
data: {
|
|
169
|
+
runId: run.id,
|
|
170
|
+
agentId,
|
|
171
|
+
cronId,
|
|
172
|
+
bytes: 1,
|
|
173
|
+
truncated: false,
|
|
174
|
+
redactions: 0,
|
|
175
|
+
body: "x",
|
|
176
|
+
createdAt: new Date(base + i * 10),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const otherRun = await newRun(otherAgentId, otherCronId);
|
|
181
|
+
await prisma.agentCronRunTranscript.create({
|
|
182
|
+
data: {
|
|
183
|
+
runId: otherRun.id,
|
|
184
|
+
agentId: otherAgentId,
|
|
185
|
+
cronId: otherCronId,
|
|
186
|
+
bytes: 1,
|
|
187
|
+
truncated: false,
|
|
188
|
+
redactions: 0,
|
|
189
|
+
body: "x",
|
|
190
|
+
createdAt: new Date(base),
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const fresh = await newRun(agentId, cronId);
|
|
195
|
+
await service.upsert({
|
|
196
|
+
runId: fresh.id,
|
|
197
|
+
agentId,
|
|
198
|
+
cronId,
|
|
199
|
+
sessionId: null,
|
|
200
|
+
bytes: 1,
|
|
201
|
+
truncated: false,
|
|
202
|
+
redactions: 0,
|
|
203
|
+
body: "new",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const mine = await prisma.agentCronRunTranscript.findMany({
|
|
207
|
+
where: { agentId },
|
|
208
|
+
orderBy: { createdAt: "desc" },
|
|
209
|
+
});
|
|
210
|
+
expect(mine).toHaveLength(TRANSCRIPT_RETENTION_PER_AGENT);
|
|
211
|
+
expect(mine[0]?.runId).toBe(fresh.id);
|
|
212
|
+
// The oldest were dropped; the other agent's row is untouched.
|
|
213
|
+
expect(mine.some((r) => r.runId === runs[0]?.id)).toBe(false);
|
|
214
|
+
expect(
|
|
215
|
+
await prisma.agentCronRunTranscript.count({
|
|
216
|
+
where: { agentId: otherAgentId },
|
|
217
|
+
}),
|
|
218
|
+
).toBe(1);
|
|
219
|
+
});
|
|
220
|
+
});
|