mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Add runtime and wake audit attribution
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
5d42382df4
commit
cd0a3b3a06
27 files changed
+887
-38
No files matched your search
@@ -0,0 +1,75 @@
|
||||
ALTER TABLE "workspace_operations"
|
||||
ADD COLUMN IF NOT EXISTS "actor_agent_id" uuid,
|
||||
ADD COLUMN IF NOT EXISTS "actor_user_id" text,
|
||||
ADD COLUMN IF NOT EXISTS "actor_run_id" uuid,
|
||||
ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
|
||||
|
||||
ALTER TABLE "workspace_runtime_services"
|
||||
ADD COLUMN IF NOT EXISTS "started_by_agent_id" uuid,
|
||||
ADD COLUMN IF NOT EXISTS "started_by_user_id" text,
|
||||
ADD COLUMN IF NOT EXISTS "responsible_user_id" text,
|
||||
ADD COLUMN IF NOT EXISTS "last_controlled_by_agent_id" uuid,
|
||||
ADD COLUMN IF NOT EXISTS "last_controlled_by_user_id" text,
|
||||
ADD COLUMN IF NOT EXISTS "last_controlled_by_run_id" uuid,
|
||||
ADD COLUMN IF NOT EXISTS "last_controlled_at" timestamp with time zone;
|
||||
|
||||
ALTER TABLE "environment_leases"
|
||||
ADD COLUMN IF NOT EXISTS "actor_agent_id" uuid,
|
||||
ADD COLUMN IF NOT EXISTS "actor_user_id" text,
|
||||
ADD COLUMN IF NOT EXISTS "actor_run_id" uuid,
|
||||
ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
|
||||
|
||||
ALTER TABLE "agent_wakeup_requests"
|
||||
ADD COLUMN IF NOT EXISTS "requested_by_agent_id" uuid,
|
||||
ADD COLUMN IF NOT EXISTS "requested_by_user_id" text,
|
||||
ADD COLUMN IF NOT EXISTS "requested_by_run_id" uuid,
|
||||
ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
|
||||
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM "pg_constraint" WHERE "conname" = 'workspace_operations_actor_agent_id_agents_id_fk') THEN
|
||||
ALTER TABLE "workspace_operations" ADD CONSTRAINT "workspace_operations_actor_agent_id_agents_id_fk" FOREIGN KEY ("actor_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM "pg_constraint" WHERE "conname" = 'workspace_operations_actor_run_id_heartbeat_runs_id_fk') THEN
|
||||
ALTER TABLE "workspace_operations" ADD CONSTRAINT "workspace_operations_actor_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("actor_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM "pg_constraint" WHERE "conname" = 'workspace_runtime_services_started_by_agent_id_agents_id_fk') THEN
|
||||
ALTER TABLE "workspace_runtime_services" ADD CONSTRAINT "workspace_runtime_services_started_by_agent_id_agents_id_fk" FOREIGN KEY ("started_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM "pg_constraint" WHERE "conname" = 'workspace_runtime_services_last_controlled_by_agent_id_agents_id_fk') THEN
|
||||
ALTER TABLE "workspace_runtime_services" ADD CONSTRAINT "workspace_runtime_services_last_controlled_by_agent_id_agents_id_fk" FOREIGN KEY ("last_controlled_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM "pg_constraint" WHERE "conname" = 'workspace_runtime_services_last_controlled_by_run_id_heartbeat_runs_id_fk') THEN
|
||||
ALTER TABLE "workspace_runtime_services" ADD CONSTRAINT "workspace_runtime_services_last_controlled_by_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("last_controlled_by_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM "pg_constraint" WHERE "conname" = 'environment_leases_actor_agent_id_agents_id_fk') THEN
|
||||
ALTER TABLE "environment_leases" ADD CONSTRAINT "environment_leases_actor_agent_id_agents_id_fk" FOREIGN KEY ("actor_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM "pg_constraint" WHERE "conname" = 'environment_leases_actor_run_id_heartbeat_runs_id_fk') THEN
|
||||
ALTER TABLE "environment_leases" ADD CONSTRAINT "environment_leases_actor_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("actor_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM "pg_constraint" WHERE "conname" = 'agent_wakeup_requests_requested_by_agent_id_agents_id_fk') THEN
|
||||
ALTER TABLE "agent_wakeup_requests" ADD CONSTRAINT "agent_wakeup_requests_requested_by_agent_id_agents_id_fk" FOREIGN KEY ("requested_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: Drizzle migrations run transactionally, so CONCURRENTLY is unavailable because these indexes back audit lookups by natural runtime and wake attribution rows.
|
||||
CREATE INDEX IF NOT EXISTS "workspace_operations_company_actor_agent_started_idx" ON "workspace_operations" USING btree ("company_id", "actor_agent_id", "started_at");
|
||||
CREATE INDEX IF NOT EXISTS "workspace_operations_company_actor_run_started_idx" ON "workspace_operations" USING btree ("company_id", "actor_run_id", "started_at");
|
||||
CREATE INDEX IF NOT EXISTS "workspace_operations_company_responsible_user_started_idx" ON "workspace_operations" USING btree ("company_id", "responsible_user_id", "started_at");
|
||||
|
||||
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: Drizzle migrations run transactionally, so CONCURRENTLY is unavailable because these indexes back audit lookups by natural runtime service attribution rows.
|
||||
CREATE INDEX IF NOT EXISTS "workspace_runtime_services_company_started_by_agent_idx" ON "workspace_runtime_services" USING btree ("company_id", "started_by_agent_id", "started_at");
|
||||
CREATE INDEX IF NOT EXISTS "workspace_runtime_services_company_responsible_user_idx" ON "workspace_runtime_services" USING btree ("company_id", "responsible_user_id", "started_at");
|
||||
CREATE INDEX IF NOT EXISTS "workspace_runtime_services_last_controlled_by_run_idx" ON "workspace_runtime_services" USING btree ("last_controlled_by_run_id");
|
||||
|
||||
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: Drizzle migrations run transactionally, so CONCURRENTLY is unavailable because these indexes back audit lookups by natural environment lease attribution rows.
|
||||
CREATE INDEX IF NOT EXISTS "environment_leases_actor_run_idx" ON "environment_leases" USING btree ("actor_run_id");
|
||||
CREATE INDEX IF NOT EXISTS "environment_leases_company_actor_agent_idx" ON "environment_leases" USING btree ("company_id", "actor_agent_id");
|
||||
CREATE INDEX IF NOT EXISTS "environment_leases_company_responsible_user_idx" ON "environment_leases" USING btree ("company_id", "responsible_user_id");
|
||||
|
||||
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: Drizzle migrations run transactionally, so CONCURRENTLY is unavailable because these indexes back audit lookups by natural wake request attribution rows.
|
||||
CREATE INDEX IF NOT EXISTS "agent_wakeup_requests_requested_by_run_idx" ON "agent_wakeup_requests" USING btree ("requested_by_run_id");
|
||||
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: Drizzle migrations run transactionally, so CONCURRENTLY is unavailable because this index backs audit lookups by natural wake request attribution rows.
|
||||
CREATE INDEX IF NOT EXISTS "agent_wakeup_requests_company_requested_by_agent_idx" ON "agent_wakeup_requests" USING btree ("company_id", "requested_by_agent_id", "requested_at");
|
||||
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: Drizzle migrations run transactionally, so CONCURRENTLY is unavailable because this index backs audit lookups by natural wake request attribution rows.
|
||||
CREATE INDEX IF NOT EXISTS "agent_wakeup_requests_company_responsible_user_requested_idx" ON "agent_wakeup_requests" USING btree ("company_id", "responsible_user_id", "requested_at");
|
||||
@@ -1233,6 +1233,13 @@
|
||||
"when": 1784241826832,
|
||||
"tag": "0177_activity_log_responsible_user",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 178,
|
||||
"version": "7",
|
||||
"when": 1784254315259,
|
||||
"tag": "0178_runtime_wake_attribution",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16,6 +16,10 @@ export const agentWakeupRequests = pgTable(
|
||||
coalescedCount: integer("coalesced_count").notNull().default(0),
|
||||
requestedByActorType: text("requested_by_actor_type"),
|
||||
requestedByActorId: text("requested_by_actor_id"),
|
||||
requestedByAgentId: uuid("requested_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
requestedByUserId: text("requested_by_user_id"),
|
||||
requestedByRunId: uuid("requested_by_run_id"),
|
||||
responsibleUserId: text("responsible_user_id"),
|
||||
idempotencyKey: text("idempotency_key"),
|
||||
runId: uuid("run_id"),
|
||||
requestedAt: timestamp("requested_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
@@ -36,5 +40,16 @@ export const agentWakeupRequests = pgTable(
|
||||
table.requestedAt,
|
||||
),
|
||||
agentRequestedIdx: index("agent_wakeup_requests_agent_requested_idx").on(table.agentId, table.requestedAt),
|
||||
requestedByRunIdx: index("agent_wakeup_requests_requested_by_run_idx").on(table.requestedByRunId),
|
||||
companyRequestedByAgentIdx: index("agent_wakeup_requests_company_requested_by_agent_idx").on(
|
||||
table.companyId,
|
||||
table.requestedByAgentId,
|
||||
table.requestedAt,
|
||||
),
|
||||
companyResponsibleUserRequestedIdx: index("agent_wakeup_requests_company_responsible_user_requested_idx").on(
|
||||
table.companyId,
|
||||
table.responsibleUserId,
|
||||
table.requestedAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { index, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
import { environments } from "./environments.js";
|
||||
import { executionWorkspaces } from "./execution_workspaces.js";
|
||||
import { heartbeatRuns } from "./heartbeat_runs.js";
|
||||
@@ -14,6 +15,10 @@ export const environmentLeases = pgTable(
|
||||
executionWorkspaceId: uuid("execution_workspace_id").references(() => executionWorkspaces.id, { onDelete: "set null" }),
|
||||
issueId: uuid("issue_id").references(() => issues.id, { onDelete: "set null" }),
|
||||
heartbeatRunId: uuid("heartbeat_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
||||
actorAgentId: uuid("actor_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
actorUserId: text("actor_user_id"),
|
||||
actorRunId: uuid("actor_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
||||
responsibleUserId: text("responsible_user_id"),
|
||||
status: text("status").notNull().default("active"),
|
||||
leasePolicy: text("lease_policy").notNull().default("ephemeral"),
|
||||
provider: text("provider"),
|
||||
@@ -42,5 +47,14 @@ export const environmentLeases = pgTable(
|
||||
heartbeatRunIdx: index("environment_leases_heartbeat_run_idx").on(table.heartbeatRunId),
|
||||
companyLastUsedIdx: index("environment_leases_company_last_used_idx").on(table.companyId, table.lastUsedAt),
|
||||
providerLeaseIdx: index("environment_leases_provider_lease_idx").on(table.providerLeaseId),
|
||||
actorRunIdx: index("environment_leases_actor_run_idx").on(table.actorRunId),
|
||||
companyActorAgentIdx: index("environment_leases_company_actor_agent_idx").on(
|
||||
table.companyId,
|
||||
table.actorAgentId,
|
||||
),
|
||||
companyResponsibleUserIdx: index("environment_leases_company_responsible_user_idx").on(
|
||||
table.companyId,
|
||||
table.responsibleUserId,
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
import { executionWorkspaces } from "./execution_workspaces.js";
|
||||
import { heartbeatRuns } from "./heartbeat_runs.js";
|
||||
import { issues } from "./issues.js";
|
||||
@@ -25,6 +26,14 @@ export const workspaceOperations = pgTable(
|
||||
heartbeatRunId: uuid("heartbeat_run_id").references(() => heartbeatRuns.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
actorAgentId: uuid("actor_agent_id").references(() => agents.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
actorUserId: text("actor_user_id"),
|
||||
actorRunId: uuid("actor_run_id").references(() => heartbeatRuns.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
responsibleUserId: text("responsible_user_id"),
|
||||
issueId: uuid("issue_id").references(() => issues.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
@@ -63,5 +72,20 @@ export const workspaceOperations = pgTable(
|
||||
table.issueId,
|
||||
table.startedAt,
|
||||
),
|
||||
companyActorAgentStartedIdx: index("workspace_operations_company_actor_agent_started_idx").on(
|
||||
table.companyId,
|
||||
table.actorAgentId,
|
||||
table.startedAt,
|
||||
),
|
||||
companyActorRunStartedIdx: index("workspace_operations_company_actor_run_started_idx").on(
|
||||
table.companyId,
|
||||
table.actorRunId,
|
||||
table.startedAt,
|
||||
),
|
||||
companyResponsibleUserStartedIdx: index("workspace_operations_company_responsible_user_started_idx").on(
|
||||
table.companyId,
|
||||
table.responsibleUserId,
|
||||
table.startedAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -38,6 +38,13 @@ export const workspaceRuntimeServices = pgTable(
|
||||
providerRef: text("provider_ref"),
|
||||
ownerAgentId: uuid("owner_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
startedByRunId: uuid("started_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
||||
startedByAgentId: uuid("started_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
startedByUserId: text("started_by_user_id"),
|
||||
responsibleUserId: text("responsible_user_id"),
|
||||
lastControlledByAgentId: uuid("last_controlled_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
lastControlledByUserId: text("last_controlled_by_user_id"),
|
||||
lastControlledByRunId: uuid("last_controlled_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
||||
lastControlledAt: timestamp("last_controlled_at", { withTimezone: true }),
|
||||
lastUsedAt: timestamp("last_used_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
stoppedAt: timestamp("stopped_at", { withTimezone: true }),
|
||||
@@ -63,6 +70,19 @@ export const workspaceRuntimeServices = pgTable(
|
||||
table.status,
|
||||
),
|
||||
runIdx: index("workspace_runtime_services_run_idx").on(table.startedByRunId),
|
||||
companyStartedByAgentIdx: index("workspace_runtime_services_company_started_by_agent_idx").on(
|
||||
table.companyId,
|
||||
table.startedByAgentId,
|
||||
table.startedAt,
|
||||
),
|
||||
companyResponsibleUserIdx: index("workspace_runtime_services_company_responsible_user_idx").on(
|
||||
table.companyId,
|
||||
table.responsibleUserId,
|
||||
table.startedAt,
|
||||
),
|
||||
lastControlledByRunIdx: index("workspace_runtime_services_last_controlled_by_run_idx").on(
|
||||
table.lastControlledByRunId,
|
||||
),
|
||||
companyUpdatedIdx: index("workspace_runtime_services_company_updated_idx").on(
|
||||
table.companyId,
|
||||
table.updatedAt,
|
||||
|
||||
@@ -112,6 +112,10 @@ export interface EnvironmentLease {
|
||||
executionWorkspaceId: string | null;
|
||||
issueId: string | null;
|
||||
heartbeatRunId: string | null;
|
||||
actorAgentId: string | null;
|
||||
actorUserId: string | null;
|
||||
actorRunId: string | null;
|
||||
responsibleUserId: string | null;
|
||||
status: EnvironmentLeaseStatus;
|
||||
leasePolicy: EnvironmentLeasePolicy;
|
||||
provider: string | null;
|
||||
|
||||
@@ -234,6 +234,10 @@ export interface AgentWakeupRequest {
|
||||
coalescedCount: number;
|
||||
requestedByActorType: "user" | "agent" | "system" | null;
|
||||
requestedByActorId: string | null;
|
||||
requestedByAgentId: string | null;
|
||||
requestedByUserId: string | null;
|
||||
requestedByRunId: string | null;
|
||||
responsibleUserId: string | null;
|
||||
idempotencyKey: string | null;
|
||||
runId: string | null;
|
||||
requestedAt: Date;
|
||||
|
||||
@@ -265,6 +265,10 @@ export interface IssueWakeDiagnosticWakeRequest {
|
||||
status: string;
|
||||
coalescedCount: number;
|
||||
runId: string | null;
|
||||
requestedByAgentId: string | null;
|
||||
requestedByUserId: string | null;
|
||||
requestedByRunId: string | null;
|
||||
responsibleUserId: string | null;
|
||||
requestedAt: string;
|
||||
claimedAt: string | null;
|
||||
finishedAt: string | null;
|
||||
|
||||
@@ -13,6 +13,10 @@ export interface WorkspaceOperation {
|
||||
companyId: string;
|
||||
executionWorkspaceId: string | null;
|
||||
heartbeatRunId: string | null;
|
||||
actorAgentId: string | null;
|
||||
actorUserId: string | null;
|
||||
actorRunId: string | null;
|
||||
responsibleUserId: string | null;
|
||||
issueId: string | null;
|
||||
phase: WorkspaceOperationPhase;
|
||||
command: string | null;
|
||||
|
||||
@@ -282,6 +282,13 @@ export interface WorkspaceRuntimeService {
|
||||
providerRef: string | null;
|
||||
ownerAgentId: string | null;
|
||||
startedByRunId: string | null;
|
||||
startedByAgentId: string | null;
|
||||
startedByUserId: string | null;
|
||||
responsibleUserId: string | null;
|
||||
lastControlledByAgentId: string | null;
|
||||
lastControlledByUserId: string | null;
|
||||
lastControlledByRunId: string | null;
|
||||
lastControlledAt: Date | null;
|
||||
lastUsedAt: Date;
|
||||
startedAt: Date;
|
||||
stoppedAt: Date | null;
|
||||
|
||||
@@ -112,6 +112,13 @@ export const workspaceRuntimeServiceSchema = z.object({
|
||||
providerRef: z.string().nullable(),
|
||||
ownerAgentId: z.string().uuid().nullable(),
|
||||
startedByRunId: z.string().uuid().nullable(),
|
||||
startedByAgentId: z.string().uuid().nullable(),
|
||||
startedByUserId: z.string().nullable(),
|
||||
responsibleUserId: z.string().nullable(),
|
||||
lastControlledByAgentId: z.string().uuid().nullable(),
|
||||
lastControlledByUserId: z.string().nullable(),
|
||||
lastControlledByRunId: z.string().uuid().nullable(),
|
||||
lastControlledAt: z.coerce.date().nullable(),
|
||||
lastUsedAt: z.coerce.date(),
|
||||
startedAt: z.coerce.date(),
|
||||
stoppedAt: z.coerce.date().nullable(),
|
||||
|
||||
@@ -194,6 +194,79 @@ describeEmbeddedPostgres("heartbeat responsible-user invariant", () => {
|
||||
expect(completed?.responsibleUserId).toBe(triggeringUserId);
|
||||
});
|
||||
|
||||
it("stores requester run and responsible user on queued and skipped wakeup requests", async () => {
|
||||
const { companyId, agentId } = await seedCompany();
|
||||
const requesterRunId = randomUUID();
|
||||
const responsibleUserId = `responsible-${randomUUID()}`;
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: requesterRunId,
|
||||
companyId,
|
||||
agentId,
|
||||
invocationSource: "on_demand",
|
||||
triggerDetail: "manual",
|
||||
status: "completed",
|
||||
responsibleUserId,
|
||||
});
|
||||
|
||||
const run = await heartbeat.wakeup(agentId, {
|
||||
source: "on_demand",
|
||||
triggerDetail: "manual",
|
||||
requestedByActorType: "agent",
|
||||
requestedByActorId: agentId,
|
||||
requestedByRunId: requesterRunId,
|
||||
});
|
||||
|
||||
expect(run).not.toBeNull();
|
||||
const queuedWake = await db
|
||||
.select()
|
||||
.from(agentWakeupRequests)
|
||||
.where(eq(agentWakeupRequests.runId, run!.id))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
expect(queuedWake?.requestedByActorType).toBe("agent");
|
||||
expect(queuedWake?.requestedByActorId).toBe(agentId);
|
||||
expect(queuedWake?.requestedByAgentId).toBe(agentId);
|
||||
expect(queuedWake?.requestedByRunId).toBe(requesterRunId);
|
||||
expect(queuedWake?.responsibleUserId).toBe(responsibleUserId);
|
||||
const completed = await waitForRun(db, run!.id);
|
||||
expect(completed?.responsibleUserId).toBe(responsibleUserId);
|
||||
|
||||
const skippedAgentId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: skippedAgentId,
|
||||
companyId,
|
||||
name: "SleepingAgent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: { heartbeat: { wakeOnDemand: false } },
|
||||
permissions: {},
|
||||
});
|
||||
const skippedRequesterRunId = randomUUID();
|
||||
const skippedResponsibleUserId = `responsible-${randomUUID()}`;
|
||||
const skipped = await heartbeat.wakeup(skippedAgentId, {
|
||||
source: "on_demand",
|
||||
triggerDetail: "manual",
|
||||
requestedByActorType: "agent",
|
||||
requestedByActorId: agentId,
|
||||
requestedByRunId: skippedRequesterRunId,
|
||||
responsibleUserId: skippedResponsibleUserId,
|
||||
});
|
||||
|
||||
expect(skipped).toBeNull();
|
||||
const skippedWake = await db
|
||||
.select()
|
||||
.from(agentWakeupRequests)
|
||||
.where(and(eq(agentWakeupRequests.agentId, skippedAgentId), eq(agentWakeupRequests.status, "skipped")))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
expect(skippedWake?.reason).toBe("heartbeat.wakeOnDemand.disabled");
|
||||
expect(skippedWake?.requestedByActorType).toBe("agent");
|
||||
expect(skippedWake?.requestedByActorId).toBe(agentId);
|
||||
expect(skippedWake?.requestedByAgentId).toBe(agentId);
|
||||
expect(skippedWake?.requestedByRunId).toBe(skippedRequesterRunId);
|
||||
expect(skippedWake?.responsibleUserId).toBe(skippedResponsibleUserId);
|
||||
});
|
||||
|
||||
it("falls back to the company default for system-originated runs without an issue", async () => {
|
||||
const { agentId, ownerUserId } = await seedCompany();
|
||||
const run = await heartbeat.wakeup(agentId, {
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AdapterRuntimeServiceReport } from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
agents,
|
||||
companies,
|
||||
createDb,
|
||||
environmentLeases,
|
||||
environments,
|
||||
heartbeatRuns,
|
||||
workspaceOperations,
|
||||
workspaceRuntimeServices,
|
||||
} from "@paperclipai/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
getEmbeddedPostgresTestSupport,
|
||||
startEmbeddedPostgresTestDatabase,
|
||||
} from "./helpers/embedded-postgres.js";
|
||||
import { environmentService } from "../services/environments.ts";
|
||||
import {
|
||||
persistAdapterManagedRuntimeServices,
|
||||
stopRuntimeServicesForExecutionWorkspace,
|
||||
type RealizedExecutionWorkspace,
|
||||
} from "../services/workspace-runtime.ts";
|
||||
import { workspaceOperationService } from "../services/workspace-operations.ts";
|
||||
|
||||
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||
|
||||
describeEmbeddedPostgres("runtime audit attribution", () => {
|
||||
let db!: ReturnType<typeof createDb>;
|
||||
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-runtime-audit-attribution-");
|
||||
db = createDb(tempDb.connectionString);
|
||||
}, 20_000);
|
||||
|
||||
afterEach(async () => {
|
||||
await db.delete(workspaceOperations);
|
||||
await db.delete(workspaceRuntimeServices);
|
||||
await db.delete(environmentLeases);
|
||||
await db.delete(heartbeatRuns);
|
||||
await db.delete(agents);
|
||||
await db.delete(companies);
|
||||
await db.delete(environments);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await tempDb?.cleanup();
|
||||
}, 60_000);
|
||||
|
||||
async function seedRun() {
|
||||
const companyId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
const runId = randomUUID();
|
||||
const responsibleUserId = `responsible-${randomUUID()}`;
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Audit attribution",
|
||||
issuePrefix: `A${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
defaultResponsibleUserId: responsibleUserId,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "CloudOps",
|
||||
role: "operations",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: runId,
|
||||
companyId,
|
||||
agentId,
|
||||
invocationSource: "on_demand",
|
||||
triggerDetail: "manual",
|
||||
status: "running",
|
||||
responsibleUserId,
|
||||
});
|
||||
|
||||
return { companyId, agentId, runId, responsibleUserId };
|
||||
}
|
||||
|
||||
it("stores actor and responsible-user attribution on workspace operation rows", async () => {
|
||||
const { companyId, agentId, runId, responsibleUserId } = await seedRun();
|
||||
const recorder = workspaceOperationService(db).createRecorder({
|
||||
companyId,
|
||||
heartbeatRunId: runId,
|
||||
actorAgentId: agentId,
|
||||
actorRunId: runId,
|
||||
responsibleUserId,
|
||||
});
|
||||
|
||||
const operation = await recorder.recordOperation({
|
||||
phase: "workspace_provision",
|
||||
command: "git status --short",
|
||||
cwd: "/workspace/audit",
|
||||
run: async () => ({
|
||||
status: "succeeded",
|
||||
exitCode: 0,
|
||||
stdout: "clean",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(operation.heartbeatRunId).toBe(runId);
|
||||
expect(operation.actorAgentId).toBe(agentId);
|
||||
expect(operation.actorRunId).toBe(runId);
|
||||
expect(operation.responsibleUserId).toBe(responsibleUserId);
|
||||
|
||||
const row = await db
|
||||
.select()
|
||||
.from(workspaceOperations)
|
||||
.where(eq(workspaceOperations.id, operation.id))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
expect(row?.actorAgentId).toBe(agentId);
|
||||
expect(row?.actorRunId).toBe(runId);
|
||||
expect(row?.responsibleUserId).toBe(responsibleUserId);
|
||||
});
|
||||
|
||||
it("stores actor and responsible-user attribution on environment test leases", async () => {
|
||||
const { companyId, agentId, runId, responsibleUserId } = await seedRun();
|
||||
const environmentsSvc = environmentService(db);
|
||||
const environment = await environmentsSvc.ensureLocalEnvironment(companyId);
|
||||
|
||||
const lease = await environmentsSvc.acquireLease({
|
||||
companyId,
|
||||
environmentId: environment.id,
|
||||
heartbeatRunId: runId,
|
||||
actorAgentId: agentId,
|
||||
actorRunId: runId,
|
||||
responsibleUserId,
|
||||
provider: "local",
|
||||
});
|
||||
|
||||
expect(lease.heartbeatRunId).toBe(runId);
|
||||
expect(lease.actorAgentId).toBe(agentId);
|
||||
expect(lease.actorRunId).toBe(runId);
|
||||
expect(lease.responsibleUserId).toBe(responsibleUserId);
|
||||
|
||||
const row = await db
|
||||
.select()
|
||||
.from(environmentLeases)
|
||||
.where(eq(environmentLeases.id, lease.id))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
expect(row?.actorAgentId).toBe(agentId);
|
||||
expect(row?.actorRunId).toBe(runId);
|
||||
expect(row?.responsibleUserId).toBe(responsibleUserId);
|
||||
});
|
||||
|
||||
it("stores start and manual control attribution on workspace runtime service rows", async () => {
|
||||
const { companyId, agentId, runId, responsibleUserId } = await seedRun();
|
||||
const workspace: RealizedExecutionWorkspace = {
|
||||
baseCwd: "/workspace/audit",
|
||||
source: "task_session",
|
||||
projectId: null,
|
||||
workspaceId: null,
|
||||
repoUrl: null,
|
||||
repoRef: null,
|
||||
strategy: "git_worktree",
|
||||
cwd: "/workspace/audit",
|
||||
branchName: "audit-runtime-attribution",
|
||||
worktreePath: "/workspace/audit",
|
||||
warnings: [],
|
||||
created: false,
|
||||
};
|
||||
const runtimeServiceId = randomUUID();
|
||||
const reports: AdapterRuntimeServiceReport[] = [
|
||||
{
|
||||
id: runtimeServiceId,
|
||||
serviceName: "web",
|
||||
status: "running",
|
||||
lifecycle: "ephemeral",
|
||||
scopeType: "run",
|
||||
providerRef: "adapter-web-ref",
|
||||
},
|
||||
];
|
||||
|
||||
const refs = await persistAdapterManagedRuntimeServices({
|
||||
db,
|
||||
adapterType: "codex_local",
|
||||
runId,
|
||||
agent: { id: agentId, name: "CloudOps", companyId },
|
||||
issue: null,
|
||||
workspace,
|
||||
responsibleUserId,
|
||||
reports,
|
||||
});
|
||||
expect(refs).toHaveLength(1);
|
||||
expect(refs[0]!.startedByAgentId).toBe(agentId);
|
||||
expect(refs[0]!.startedByRunId).toBe(runId);
|
||||
expect(refs[0]!.responsibleUserId).toBe(responsibleUserId);
|
||||
expect(refs[0]!.lastControlledByAgentId).toBe(agentId);
|
||||
expect(refs[0]!.lastControlledByRunId).toBe(runId);
|
||||
|
||||
const persisted = await db
|
||||
.select()
|
||||
.from(workspaceRuntimeServices)
|
||||
.where(eq(workspaceRuntimeServices.id, refs[0]!.id))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
expect(persisted?.startedByAgentId).toBe(agentId);
|
||||
expect(persisted?.startedByRunId).toBe(runId);
|
||||
expect(persisted?.responsibleUserId).toBe(responsibleUserId);
|
||||
expect(persisted?.lastControlledByAgentId).toBe(agentId);
|
||||
expect(persisted?.lastControlledByRunId).toBe(runId);
|
||||
|
||||
const controllerRunId = randomUUID();
|
||||
const controllerResponsibleUserId = `controller-${randomUUID()}`;
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: controllerRunId,
|
||||
companyId,
|
||||
agentId,
|
||||
invocationSource: "on_demand",
|
||||
triggerDetail: "manual-runtime-stop",
|
||||
status: "running",
|
||||
responsibleUserId: controllerResponsibleUserId,
|
||||
});
|
||||
|
||||
await stopRuntimeServicesForExecutionWorkspace({
|
||||
db,
|
||||
executionWorkspaceId: randomUUID(),
|
||||
runtimeServiceId: refs[0]!.id,
|
||||
controlAttribution: {
|
||||
actorAgentId: agentId,
|
||||
actorRunId: controllerRunId,
|
||||
responsibleUserId: controllerResponsibleUserId,
|
||||
},
|
||||
});
|
||||
|
||||
const stopped = await db
|
||||
.select()
|
||||
.from(workspaceRuntimeServices)
|
||||
.where(eq(workspaceRuntimeServices.id, refs[0]!.id))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
expect(stopped?.status).toBe("stopped");
|
||||
expect(stopped?.startedByAgentId).toBe(agentId);
|
||||
expect(stopped?.startedByRunId).toBe(runId);
|
||||
expect(stopped?.lastControlledByAgentId).toBe(agentId);
|
||||
expect(stopped?.lastControlledByRunId).toBe(controllerRunId);
|
||||
expect(stopped?.responsibleUserId).toBe(controllerResponsibleUserId);
|
||||
expect(stopped?.lastControlledAt).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
@@ -187,6 +187,11 @@ export function agentRoutes(
|
||||
const environmentRuntime = environmentRuntimeService(db, {
|
||||
pluginWorkerManager: options.pluginWorkerManager,
|
||||
});
|
||||
function routeResponsibleUserId(req: Request) {
|
||||
if (req.actor.type === "agent") return req.actor.onBehalfOfUserId?.trim() || null;
|
||||
if (req.actor.type === "board") return req.actor.userId?.trim() || null;
|
||||
return null;
|
||||
}
|
||||
const heartbeat = heartbeatService(db, {
|
||||
pluginWorkerManager: options.pluginWorkerManager,
|
||||
});
|
||||
@@ -259,6 +264,10 @@ export function agentRoutes(
|
||||
companyId: string;
|
||||
adapterType: string;
|
||||
environmentId: string | null;
|
||||
actorAgentId?: string | null;
|
||||
actorUserId?: string | null;
|
||||
actorRunId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
}): Promise<{
|
||||
executionTarget: AdapterExecutionTarget | null;
|
||||
environmentName: string | null;
|
||||
@@ -386,6 +395,10 @@ export function agentRoutes(
|
||||
issueId: null,
|
||||
heartbeatRunId: null,
|
||||
persistedExecutionWorkspace: null,
|
||||
actorAgentId: input.actorAgentId ?? null,
|
||||
actorUserId: input.actorUserId ?? null,
|
||||
actorRunId: input.actorRunId ?? null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
// Apply the active custom-image template so the Test boots with the
|
||||
// operator's captured sandbox customizations and prepared image state,
|
||||
// matching what real agent runs use. Without this the test would
|
||||
@@ -1776,11 +1789,16 @@ export function agentRoutes(
|
||||
{ adapterType: type },
|
||||
);
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
const { executionTarget, environmentName, fallbackChecks, sandboxIdentityCheck, release } =
|
||||
await resolveAdapterTestExecutionContext({
|
||||
companyId,
|
||||
adapterType: type,
|
||||
environmentId: requestedEnvironmentId,
|
||||
actorAgentId: actor.agentId,
|
||||
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
||||
actorRunId: actor.runId,
|
||||
responsibleUserId: routeResponsibleUserId(req),
|
||||
});
|
||||
|
||||
let releaseStatus: "released" | "failed" = "released";
|
||||
@@ -3440,6 +3458,8 @@ export function agentRoutes(
|
||||
idempotencyKey: req.body.idempotencyKey ?? null,
|
||||
requestedByActorType: req.actor.type === "agent" ? "agent" : "user",
|
||||
requestedByActorId: req.actor.type === "agent" ? req.actor.agentId ?? null : req.actor.userId ?? null,
|
||||
requestedByRunId: req.actor.runId ?? null,
|
||||
responsibleUserId: routeResponsibleUserId(req),
|
||||
contextSnapshot: {
|
||||
triggeredBy: req.actor.type,
|
||||
actorId: req.actor.type === "agent" ? req.actor.agentId : req.actor.userId,
|
||||
|
||||
@@ -249,9 +249,23 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P
|
||||
}
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
const controlAttribution = {
|
||||
actorAgentId: actor.agentId,
|
||||
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
||||
actorRunId: actor.runId,
|
||||
responsibleUserId: req.actor.type === "agent"
|
||||
? req.actor.onBehalfOfUserId?.trim() || null
|
||||
: actor.actorType === "user"
|
||||
? actor.actorId
|
||||
: null,
|
||||
};
|
||||
const recorder = workspaceOperationsSvc.createRecorder({
|
||||
companyId: existing.companyId,
|
||||
executionWorkspaceId: existing.id,
|
||||
actorAgentId: controlAttribution.actorAgentId,
|
||||
actorUserId: controlAttribution.actorUserId,
|
||||
actorRunId: controlAttribution.actorRunId,
|
||||
responsibleUserId: controlAttribution.responsibleUserId,
|
||||
});
|
||||
let runtimeServiceCount = existing.runtimeServices?.length ?? 0;
|
||||
let stdout = "";
|
||||
@@ -366,6 +380,7 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P
|
||||
executionWorkspaceId: existing.id,
|
||||
workspaceCwd,
|
||||
runtimeServiceId: selectedRuntimeServiceId,
|
||||
controlAttribution,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -394,6 +409,7 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P
|
||||
adapterEnv: {},
|
||||
onLog,
|
||||
serviceIndex: selectedServiceIndex,
|
||||
controlAttribution,
|
||||
});
|
||||
runtimeServiceCount = startedServices.length;
|
||||
} else {
|
||||
@@ -551,6 +567,12 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P
|
||||
},
|
||||
requestedByActorType: actor.actorType,
|
||||
requestedByActorId: actor.actorId,
|
||||
requestedByRunId: actor.runId,
|
||||
responsibleUserId: req.actor.type === "agent"
|
||||
? req.actor.onBehalfOfUserId?.trim() || null
|
||||
: actor.actorType === "user"
|
||||
? actor.actorId
|
||||
: null,
|
||||
contextSnapshot: {
|
||||
issueId: result.restoredSourceIssue.id,
|
||||
taskId: result.restoredSourceIssue.id,
|
||||
@@ -608,6 +630,17 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P
|
||||
const configForCleanup = readExecutionWorkspaceConfig(
|
||||
((patch.metadata as Record<string, unknown> | null | undefined) ?? (existing.metadata as Record<string, unknown> | null)) ?? null,
|
||||
);
|
||||
const actor = getActorInfo(req);
|
||||
const controlAttribution = {
|
||||
actorAgentId: actor.agentId,
|
||||
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
||||
actorRunId: actor.runId,
|
||||
responsibleUserId: req.actor.type === "agent"
|
||||
? req.actor.onBehalfOfUserId?.trim() || null
|
||||
: actor.actorType === "user"
|
||||
? actor.actorId
|
||||
: null,
|
||||
};
|
||||
|
||||
if (req.body.status === "archived" && existing.status !== "archived") {
|
||||
const readiness = await svc.getCloseReadiness(existing.id);
|
||||
@@ -663,6 +696,7 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P
|
||||
db,
|
||||
executionWorkspaceId: existing.id,
|
||||
workspaceCwd: existing.cwd,
|
||||
controlAttribution,
|
||||
});
|
||||
const projectWorkspace = existing.projectWorkspaceId
|
||||
? await db
|
||||
@@ -696,6 +730,10 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P
|
||||
recorder: workspaceOperationsSvc.createRecorder({
|
||||
companyId: existing.companyId,
|
||||
executionWorkspaceId: existing.id,
|
||||
actorAgentId: controlAttribution.actorAgentId,
|
||||
actorUserId: controlAttribution.actorUserId,
|
||||
actorRunId: controlAttribution.actorRunId,
|
||||
responsibleUserId: controlAttribution.responsibleUserId,
|
||||
}),
|
||||
});
|
||||
cleanupWarnings = cleanupResult.warnings;
|
||||
@@ -730,7 +768,6 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P
|
||||
}
|
||||
workspace = updatedWorkspace;
|
||||
}
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId: existing.companyId,
|
||||
actorType: actor.actorType,
|
||||
|
||||
@@ -1029,6 +1029,10 @@ function projectIssueWakeRequest(row: {
|
||||
status: string;
|
||||
coalescedCount: number;
|
||||
runId: string | null;
|
||||
requestedByAgentId?: string | null;
|
||||
requestedByUserId?: string | null;
|
||||
requestedByRunId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
requestedAt: Date | string;
|
||||
claimedAt: Date | string | null;
|
||||
finishedAt: Date | string | null;
|
||||
@@ -1043,6 +1047,10 @@ function projectIssueWakeRequest(row: {
|
||||
status,
|
||||
coalescedCount: row.coalescedCount,
|
||||
runId: options.includeInternalIds ? row.runId : null,
|
||||
requestedByAgentId: options.includeInternalIds ? row.requestedByAgentId ?? null : null,
|
||||
requestedByUserId: options.includeInternalIds ? row.requestedByUserId ?? null : null,
|
||||
requestedByRunId: options.includeInternalIds ? row.requestedByRunId ?? null : null,
|
||||
responsibleUserId: options.includeInternalIds ? row.responsibleUserId ?? null : null,
|
||||
requestedAt: dateToIso(row.requestedAt)!,
|
||||
claimedAt: dateToIso(row.claimedAt),
|
||||
finishedAt: dateToIso(row.finishedAt),
|
||||
@@ -1214,6 +1222,10 @@ function buildIssueWakeDiagnosticsResponse(input: {
|
||||
status: string;
|
||||
coalescedCount: number;
|
||||
runId: string | null;
|
||||
requestedByAgentId?: string | null;
|
||||
requestedByUserId?: string | null;
|
||||
requestedByRunId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
requestedAt: Date | string;
|
||||
claimedAt: Date | string | null;
|
||||
finishedAt: Date | string | null;
|
||||
|
||||
@@ -444,7 +444,23 @@ export function projectRoutes(db: Db) {
|
||||
}
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
const recorder = workspaceOperations.createRecorder({ companyId: project.companyId });
|
||||
const controlAttribution = {
|
||||
actorAgentId: actor.agentId,
|
||||
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
||||
actorRunId: actor.runId,
|
||||
responsibleUserId: req.actor.type === "agent"
|
||||
? req.actor.onBehalfOfUserId?.trim() || null
|
||||
: actor.actorType === "user"
|
||||
? actor.actorId
|
||||
: null,
|
||||
};
|
||||
const recorder = workspaceOperations.createRecorder({
|
||||
companyId: project.companyId,
|
||||
actorAgentId: controlAttribution.actorAgentId,
|
||||
actorUserId: controlAttribution.actorUserId,
|
||||
actorRunId: controlAttribution.actorRunId,
|
||||
responsibleUserId: controlAttribution.responsibleUserId,
|
||||
});
|
||||
let runtimeServiceCount = workspace.runtimeServices?.length ?? 0;
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
@@ -518,6 +534,7 @@ export function projectRoutes(db: Db) {
|
||||
db,
|
||||
projectWorkspaceId: workspace.id,
|
||||
runtimeServiceId: selectedRuntimeServiceId,
|
||||
controlAttribution,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -548,6 +565,7 @@ export function projectRoutes(db: Db) {
|
||||
adapterEnv: {},
|
||||
onLog,
|
||||
serviceIndex: selectedServiceIndex,
|
||||
controlAttribution,
|
||||
});
|
||||
runtimeServiceCount = startedServices.length;
|
||||
} else {
|
||||
|
||||
@@ -203,9 +203,16 @@ export function environmentRunOrchestrator(
|
||||
heartbeatRunId: string;
|
||||
persistedExecutionWorkspace: Pick<ExecutionWorkspace, "id" | "mode"> | null;
|
||||
adapterType: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
}): Promise<EnvironmentRuntimeLeaseRecord> {
|
||||
try {
|
||||
return await environmentRuntime.acquireRunLease(input);
|
||||
return await environmentRuntime.acquireRunLease({
|
||||
...input,
|
||||
actorAgentId: input.agentId,
|
||||
actorUserId: null,
|
||||
actorRunId: input.heartbeatRunId,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new EnvironmentRunError(
|
||||
"lease_acquire_failed",
|
||||
@@ -262,6 +269,7 @@ export function environmentRunOrchestrator(
|
||||
heartbeatRunId: string;
|
||||
agentId: string;
|
||||
persistedExecutionWorkspace: Pick<ExecutionWorkspace, "id" | "mode"> | null;
|
||||
responsibleUserId?: string | null;
|
||||
}): Promise<EnvironmentAcquisitionResult> {
|
||||
// Step 1: Resolve environment
|
||||
const environment = await resolveEnvironment({
|
||||
@@ -279,6 +287,7 @@ export function environmentRunOrchestrator(
|
||||
heartbeatRunId: input.heartbeatRunId,
|
||||
persistedExecutionWorkspace: input.persistedExecutionWorkspace,
|
||||
adapterType: input.adapterType ?? null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
});
|
||||
|
||||
// Step 3: Log lease acquisition activity
|
||||
|
||||
@@ -135,6 +135,10 @@ export interface EnvironmentDriverAcquireInput {
|
||||
* of the base image, matching what real agent runs do.
|
||||
*/
|
||||
applyCustomImageTemplate?: boolean;
|
||||
actorAgentId?: string | null;
|
||||
actorUserId?: string | null;
|
||||
actorRunId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
}
|
||||
|
||||
export interface EnvironmentDriverReleaseInput {
|
||||
@@ -221,6 +225,10 @@ function toEnvironmentLeaseSnapshot(row: typeof environmentLeases.$inferSelect):
|
||||
executionWorkspaceId: row.executionWorkspaceId ?? null,
|
||||
issueId: row.issueId ?? null,
|
||||
heartbeatRunId: row.heartbeatRunId ?? null,
|
||||
actorAgentId: row.actorAgentId ?? null,
|
||||
actorUserId: row.actorUserId ?? null,
|
||||
actorRunId: row.actorRunId ?? null,
|
||||
responsibleUserId: row.responsibleUserId ?? null,
|
||||
status: row.status as EnvironmentLease["status"],
|
||||
leasePolicy: row.leasePolicy as EnvironmentLease["leasePolicy"],
|
||||
provider: row.provider ?? null,
|
||||
@@ -513,6 +521,10 @@ function createLocalEnvironmentDriver(db: Db): EnvironmentRuntimeDriver {
|
||||
executionWorkspaceId: input.executionWorkspaceId,
|
||||
issueId: input.issueId,
|
||||
heartbeatRunId: input.heartbeatRunId,
|
||||
actorAgentId: input.actorAgentId ?? input.agentId ?? null,
|
||||
actorUserId: input.actorUserId ?? null,
|
||||
actorRunId: input.actorRunId ?? input.heartbeatRunId ?? null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
leasePolicy: "ephemeral",
|
||||
provider: "local",
|
||||
metadata: {
|
||||
@@ -567,6 +579,10 @@ function createSshEnvironmentDriver(db: Db): EnvironmentRuntimeDriver {
|
||||
executionWorkspaceId: input.executionWorkspaceId,
|
||||
issueId: input.issueId,
|
||||
heartbeatRunId: input.heartbeatRunId,
|
||||
actorAgentId: input.actorAgentId ?? input.agentId ?? null,
|
||||
actorUserId: input.actorUserId ?? null,
|
||||
actorRunId: input.actorRunId ?? input.heartbeatRunId ?? null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
leasePolicy: "ephemeral",
|
||||
provider: "ssh",
|
||||
providerLeaseId: `ssh://${parsed.config.username}@${parsed.config.host}:${parsed.config.port}${remoteCwd}`,
|
||||
@@ -935,6 +951,10 @@ function createSandboxEnvironmentDriver(
|
||||
executionWorkspaceId: input.executionWorkspaceId,
|
||||
issueId: input.issueId,
|
||||
heartbeatRunId: input.heartbeatRunId,
|
||||
actorAgentId: input.actorAgentId ?? input.agentId ?? null,
|
||||
actorUserId: input.actorUserId ?? null,
|
||||
actorRunId: input.actorRunId ?? input.heartbeatRunId ?? null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
leasePolicy: resolvedLeasePolicy,
|
||||
provider: parsed.config.provider,
|
||||
providerLeaseId: acquiredLease.providerLeaseId,
|
||||
@@ -1082,6 +1102,10 @@ function createSandboxEnvironmentDriver(
|
||||
executionWorkspaceId: input.executionWorkspaceId,
|
||||
issueId: input.issueId,
|
||||
heartbeatRunId: input.heartbeatRunId,
|
||||
actorAgentId: input.actorAgentId ?? input.agentId ?? null,
|
||||
actorUserId: input.actorUserId ?? null,
|
||||
actorRunId: input.actorRunId ?? input.heartbeatRunId ?? null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
leasePolicy: resolvedLeasePolicy,
|
||||
provider: parsed.config.provider,
|
||||
providerLeaseId: providerLease.providerLeaseId,
|
||||
@@ -1509,6 +1533,10 @@ function createPluginEnvironmentDriver(
|
||||
executionWorkspaceId: input.executionWorkspaceId,
|
||||
issueId: input.issueId,
|
||||
heartbeatRunId: input.heartbeatRunId,
|
||||
actorAgentId: input.actorAgentId ?? input.agentId ?? null,
|
||||
actorUserId: input.actorUserId ?? null,
|
||||
actorRunId: input.actorRunId ?? input.heartbeatRunId ?? null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
leasePolicy: "ephemeral",
|
||||
provider: `plugin:${parsed.config.pluginKey}:${parsed.config.driverKey}`,
|
||||
providerLeaseId: providerLease.providerLeaseId,
|
||||
@@ -1728,6 +1756,10 @@ export function environmentRuntimeService(
|
||||
* lease uses the operator-prepared custom image.
|
||||
*/
|
||||
applyCustomImageTemplate?: boolean;
|
||||
actorAgentId?: string | null;
|
||||
actorUserId?: string | null;
|
||||
actorRunId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
}): Promise<EnvironmentRuntimeLeaseRecord> {
|
||||
if (input.environment.status !== "active") {
|
||||
throw new Error(`Environment "${input.environment.name}" is not active.`);
|
||||
@@ -1747,6 +1779,10 @@ export function environmentRuntimeService(
|
||||
executionWorkspaceMode: leaseContext.executionWorkspaceMode,
|
||||
adapterType: input.adapterType ?? null,
|
||||
applyCustomImageTemplate: input.applyCustomImageTemplate ?? false,
|
||||
actorAgentId: input.actorAgentId ?? null,
|
||||
actorUserId: input.actorUserId ?? null,
|
||||
actorRunId: input.actorRunId ?? null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -156,6 +156,10 @@ function toEnvironmentLease(row: EnvironmentLeaseRow): EnvironmentLease {
|
||||
executionWorkspaceId: row.executionWorkspaceId ?? null,
|
||||
issueId: row.issueId ?? null,
|
||||
heartbeatRunId: row.heartbeatRunId ?? null,
|
||||
actorAgentId: row.actorAgentId ?? null,
|
||||
actorUserId: row.actorUserId ?? null,
|
||||
actorRunId: row.actorRunId ?? null,
|
||||
responsibleUserId: row.responsibleUserId ?? null,
|
||||
status: readEnum(row.status, ENVIRONMENT_LEASE_STATUSES, "environment lease status") ?? "active",
|
||||
leasePolicy: readEnum(row.leasePolicy, ENVIRONMENT_LEASE_POLICIES, "environment lease policy") ?? "ephemeral",
|
||||
provider: row.provider ?? null,
|
||||
@@ -601,6 +605,10 @@ export function environmentService(db: Db) {
|
||||
executionWorkspaceId?: string | null;
|
||||
issueId?: string | null;
|
||||
heartbeatRunId?: string | null;
|
||||
actorAgentId?: string | null;
|
||||
actorUserId?: string | null;
|
||||
actorRunId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
leasePolicy?: EnvironmentLeasePolicy;
|
||||
provider?: string | null;
|
||||
providerLeaseId?: string | null;
|
||||
@@ -616,6 +624,10 @@ export function environmentService(db: Db) {
|
||||
executionWorkspaceId: input.executionWorkspaceId ?? null,
|
||||
issueId: input.issueId ?? null,
|
||||
heartbeatRunId: input.heartbeatRunId ?? null,
|
||||
actorAgentId: input.actorAgentId ?? null,
|
||||
actorUserId: input.actorUserId ?? null,
|
||||
actorRunId: input.actorRunId ?? null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
status: "active",
|
||||
leasePolicy: input.leasePolicy ?? "ephemeral",
|
||||
provider: input.provider ?? null,
|
||||
|
||||
@@ -749,6 +749,13 @@ function toRuntimeService(row: WorkspaceRuntimeServiceRow): WorkspaceRuntimeServ
|
||||
providerRef: row.providerRef ?? null,
|
||||
ownerAgentId: row.ownerAgentId ?? null,
|
||||
startedByRunId: row.startedByRunId ?? null,
|
||||
startedByAgentId: row.startedByAgentId ?? null,
|
||||
startedByUserId: row.startedByUserId ?? null,
|
||||
responsibleUserId: row.responsibleUserId ?? null,
|
||||
lastControlledByAgentId: row.lastControlledByAgentId ?? null,
|
||||
lastControlledByUserId: row.lastControlledByUserId ?? null,
|
||||
lastControlledByRunId: row.lastControlledByRunId ?? null,
|
||||
lastControlledAt: row.lastControlledAt ?? null,
|
||||
lastUsedAt: row.lastUsedAt,
|
||||
startedAt: row.startedAt,
|
||||
stoppedAt: row.stoppedAt ?? null,
|
||||
|
||||
@@ -2027,6 +2027,8 @@ interface WakeupOptions {
|
||||
idempotencyKey?: string | null;
|
||||
requestedByActorType?: "user" | "agent" | "system";
|
||||
requestedByActorId?: string | null;
|
||||
requestedByRunId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
contextSnapshot?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -6104,14 +6106,23 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
requestedByActorId?: string | null;
|
||||
source?: WakeupOptions["source"] | null;
|
||||
triggerDetail?: WakeupOptions["triggerDetail"] | null;
|
||||
requestedByRunId?: string | null;
|
||||
existingRunResponsibleUserId?: string | null;
|
||||
}) {
|
||||
const contextResponsibleUserId = readNonEmptyString(input.contextSnapshot.responsibleUserId);
|
||||
const requestedUserId = input.requestedByActorType === "user"
|
||||
? readNonEmptyString(input.requestedByActorId)
|
||||
: null;
|
||||
const requesterRunResponsibleUserId = input.requestedByRunId && isUuidLike(input.requestedByRunId)
|
||||
? await db
|
||||
.select({ responsibleUserId: heartbeatRuns.responsibleUserId })
|
||||
.from(heartbeatRuns)
|
||||
.where(and(eq(heartbeatRuns.id, input.requestedByRunId), eq(heartbeatRuns.companyId, input.companyId)))
|
||||
.then((rows) => readNonEmptyString(rows[0]?.responsibleUserId))
|
||||
: null;
|
||||
if (contextResponsibleUserId) return contextResponsibleUserId;
|
||||
if (input.existingRunResponsibleUserId) return input.existingRunResponsibleUserId;
|
||||
if (requesterRunResponsibleUserId) return requesterRunResponsibleUserId;
|
||||
if (input.routineEnvContext.responsibleUserId) return input.routineEnvContext.responsibleUserId;
|
||||
if (isManualUserRun(input) && requestedUserId) return requestedUserId;
|
||||
if (input.issueContext?.responsibleUserId) return input.issueContext.responsibleUserId;
|
||||
@@ -12497,6 +12508,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
const workspaceOperationRecorder = workspaceOperationsSvc.createRecorder({
|
||||
companyId: agent.companyId,
|
||||
heartbeatRunId: run.id,
|
||||
actorAgentId: agent.id,
|
||||
actorUserId: null,
|
||||
actorRunId: run.id,
|
||||
responsibleUserId: run.responsibleUserId ?? null,
|
||||
executionWorkspaceId: workspaceReuseProvisioningPolicy.shouldRestoreExistingWorkspace
|
||||
? workspaceReuseRequest.requestedExecutionWorkspaceId
|
||||
: null,
|
||||
@@ -12738,6 +12753,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
heartbeatRunId: run.id,
|
||||
agentId: agent.id,
|
||||
persistedExecutionWorkspace,
|
||||
responsibleUserId: run.responsibleUserId ?? null,
|
||||
});
|
||||
const selectedEnvironment = acquiredEnvironment.environment;
|
||||
// Defense-in-depth: re-check the actually-acquired environment against the
|
||||
@@ -13268,6 +13284,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
executionWorkspaceId: persistedExecutionWorkspace?.id ?? issueRef?.executionWorkspaceId ?? null,
|
||||
config: hostExecutionWorkspaceConfig,
|
||||
adapterEnv,
|
||||
responsibleUserId: run.responsibleUserId ?? null,
|
||||
onLog,
|
||||
});
|
||||
if (runtimeServices.length > 0) {
|
||||
@@ -13635,6 +13652,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
},
|
||||
issue: issueRef,
|
||||
workspace: executionWorkspace,
|
||||
responsibleUserId: run.responsibleUserId ?? null,
|
||||
reports: adapterResult.runtimeServices,
|
||||
})
|
||||
: [];
|
||||
@@ -14672,7 +14690,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
requestedByActorId: deferred.requestedByActorId,
|
||||
source: promotedSource,
|
||||
triggerDetail: promotedTriggerDetail,
|
||||
existingRunResponsibleUserId: run.responsibleUserId,
|
||||
requestedByRunId: deferred.requestedByRunId,
|
||||
existingRunResponsibleUserId: deferred.responsibleUserId ?? run.responsibleUserId,
|
||||
});
|
||||
if (!promotedResponsibleUserId) {
|
||||
throw new HttpError(422, "Unable to resolve responsible user for promoted heartbeat run", {
|
||||
@@ -15120,6 +15139,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
const agent = await getAgent(agentId);
|
||||
if (!agent) throw notFound("Agent not found");
|
||||
|
||||
const wakeupRequestAttribution = {
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
requestedByAgentId: opts.requestedByActorType === "agent" ? opts.requestedByActorId ?? null : null,
|
||||
requestedByUserId: opts.requestedByActorType === "user" ? opts.requestedByActorId ?? null : null,
|
||||
requestedByRunId: opts.requestedByRunId ?? null,
|
||||
responsibleUserId: opts.responsibleUserId
|
||||
?? (opts.requestedByActorType === "user" ? opts.requestedByActorId ?? null : null),
|
||||
};
|
||||
|
||||
const writeSkippedRequest = async (
|
||||
skipReason: string,
|
||||
patch: Partial<typeof agentWakeupRequests.$inferInsert> = {},
|
||||
@@ -15132,8 +15161,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
reason: skipReason,
|
||||
payload,
|
||||
status: "skipped",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
finishedAt: new Date(),
|
||||
...patch,
|
||||
@@ -15267,10 +15295,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
contextSnapshot: enrichedContextSnapshot,
|
||||
issueContext: queuedIssueContext,
|
||||
routineEnvContext: queuedRoutineEnvContext,
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
source,
|
||||
triggerDetail,
|
||||
existingRunResponsibleUserId: opts.responsibleUserId ?? null,
|
||||
});
|
||||
if (!queuedResponsibleUserId) {
|
||||
throw new HttpError(422, "Unable to resolve responsible user for heartbeat run dispatch", {
|
||||
@@ -15348,8 +15376,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
issueId,
|
||||
agentId,
|
||||
contextSnapshot: enrichedContextSnapshot,
|
||||
requestedByActorType: opts.requestedByActorType,
|
||||
requestedByActorId: opts.requestedByActorId,
|
||||
...wakeupRequestAttribution,
|
||||
});
|
||||
|
||||
if (!treeHoldInteractionWake) {
|
||||
@@ -15427,8 +15454,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
reason: "issue_execution_issue_not_found",
|
||||
payload,
|
||||
status: "skipped",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
finishedAt: new Date(),
|
||||
});
|
||||
@@ -15451,8 +15477,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
},
|
||||
},
|
||||
status: "skipped",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
finishedAt: new Date(),
|
||||
});
|
||||
@@ -15707,8 +15732,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
unresolvedBlockerIssueIds: dependencyReadiness.unresolvedBlockerIssueIds,
|
||||
},
|
||||
status: "skipped",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
finishedAt: new Date(),
|
||||
});
|
||||
@@ -15804,8 +15828,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
},
|
||||
},
|
||||
status: "skipped",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
finishedAt: now,
|
||||
});
|
||||
@@ -15888,8 +15911,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
payload,
|
||||
status: "coalesced",
|
||||
coalescedCount: 1,
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
runId: mergedRun.id,
|
||||
finishedAt: new Date(),
|
||||
@@ -15954,8 +15976,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
reason: "issue_execution_deferred",
|
||||
payload: deferredPayload,
|
||||
status: "deferred_issue_execution",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
});
|
||||
|
||||
@@ -16060,8 +16081,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
},
|
||||
},
|
||||
status: "skipped",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
finishedAt: throttleNow,
|
||||
});
|
||||
@@ -16088,8 +16108,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
},
|
||||
},
|
||||
status: "skipped",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
finishedAt: now,
|
||||
});
|
||||
@@ -16115,8 +16134,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
reason,
|
||||
payload,
|
||||
status: "queued",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
})
|
||||
.returning()
|
||||
@@ -16143,6 +16161,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
.update(agentWakeupRequests)
|
||||
.set({
|
||||
runId: newRun.id,
|
||||
responsibleUserId: newRun.responsibleUserId,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(agentWakeupRequests.id, wakeupRequest.id));
|
||||
@@ -16230,8 +16249,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
payload,
|
||||
status: "coalesced",
|
||||
coalescedCount: 1,
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
runId: mergedRun.id,
|
||||
finishedAt: new Date(),
|
||||
@@ -16262,8 +16280,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
},
|
||||
},
|
||||
status: "skipped",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
finishedAt: now,
|
||||
});
|
||||
@@ -16289,8 +16306,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
reason,
|
||||
payload,
|
||||
status: "queued",
|
||||
requestedByActorType: opts.requestedByActorType ?? null,
|
||||
requestedByActorId: opts.requestedByActorId ?? null,
|
||||
...wakeupRequestAttribution,
|
||||
idempotencyKey: opts.idempotencyKey ?? null,
|
||||
})
|
||||
.returning()
|
||||
@@ -16317,6 +16333,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
.update(agentWakeupRequests)
|
||||
.set({
|
||||
runId: newRun.id,
|
||||
responsibleUserId: newRun.responsibleUserId,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(agentWakeupRequests.id, wakeupRequest.id));
|
||||
|
||||
@@ -627,6 +627,10 @@ type IssueWakeDiagnosticsWakeRequestRow = {
|
||||
status: string;
|
||||
coalescedCount: number;
|
||||
runId: string | null;
|
||||
requestedByAgentId: string | null;
|
||||
requestedByUserId: string | null;
|
||||
requestedByRunId: string | null;
|
||||
responsibleUserId: string | null;
|
||||
requestedAt: Date;
|
||||
claimedAt: Date | null;
|
||||
finishedAt: Date | null;
|
||||
@@ -5245,6 +5249,10 @@ export function issueService(db: Db) {
|
||||
status: agentWakeupRequests.status,
|
||||
coalescedCount: agentWakeupRequests.coalescedCount,
|
||||
runId: agentWakeupRequests.runId,
|
||||
requestedByAgentId: agentWakeupRequests.requestedByAgentId,
|
||||
requestedByUserId: agentWakeupRequests.requestedByUserId,
|
||||
requestedByRunId: agentWakeupRequests.requestedByRunId,
|
||||
responsibleUserId: agentWakeupRequests.responsibleUserId,
|
||||
requestedAt: agentWakeupRequests.requestedAt,
|
||||
claimedAt: agentWakeupRequests.claimedAt,
|
||||
finishedAt: agentWakeupRequests.finishedAt,
|
||||
@@ -5496,6 +5504,10 @@ export function issueService(db: Db) {
|
||||
wake.status,
|
||||
wake.coalesced_count AS "coalescedCount",
|
||||
wake.run_id AS "runId",
|
||||
wake.requested_by_agent_id AS "requestedByAgentId",
|
||||
wake.requested_by_user_id AS "requestedByUserId",
|
||||
wake.requested_by_run_id AS "requestedByRunId",
|
||||
wake.responsible_user_id AS "responsibleUserId",
|
||||
wake.requested_at AS "requestedAt",
|
||||
wake.claimed_at AS "claimedAt",
|
||||
wake.finished_at AS "finishedAt",
|
||||
|
||||
@@ -139,6 +139,13 @@ function toRuntimeService(row: WorkspaceRuntimeServiceRow): WorkspaceRuntimeServ
|
||||
providerRef: row.providerRef ?? null,
|
||||
ownerAgentId: row.ownerAgentId ?? null,
|
||||
startedByRunId: row.startedByRunId ?? null,
|
||||
startedByAgentId: row.startedByAgentId ?? null,
|
||||
startedByUserId: row.startedByUserId ?? null,
|
||||
responsibleUserId: row.responsibleUserId ?? null,
|
||||
lastControlledByAgentId: row.lastControlledByAgentId ?? null,
|
||||
lastControlledByUserId: row.lastControlledByUserId ?? null,
|
||||
lastControlledByRunId: row.lastControlledByRunId ?? null,
|
||||
lastControlledAt: row.lastControlledAt ?? null,
|
||||
lastUsedAt: row.lastUsedAt,
|
||||
startedAt: row.startedAt,
|
||||
stoppedAt: row.stoppedAt ?? null,
|
||||
|
||||
@@ -16,6 +16,10 @@ function toWorkspaceOperation(row: WorkspaceOperationRow): WorkspaceOperation {
|
||||
companyId: row.companyId,
|
||||
executionWorkspaceId: row.executionWorkspaceId ?? null,
|
||||
heartbeatRunId: row.heartbeatRunId ?? null,
|
||||
actorAgentId: row.actorAgentId ?? null,
|
||||
actorUserId: row.actorUserId ?? null,
|
||||
actorRunId: row.actorRunId ?? null,
|
||||
responsibleUserId: row.responsibleUserId ?? null,
|
||||
issueId: row.issueId ?? null,
|
||||
phase: row.phase as WorkspaceOperationPhase,
|
||||
command: row.command ?? null,
|
||||
@@ -52,6 +56,13 @@ function combineMetadata(
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkspaceOperationAttribution {
|
||||
actorAgentId?: string | null;
|
||||
actorUserId?: string | null;
|
||||
actorRunId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
}
|
||||
|
||||
export interface WorkspaceOperationRecorder {
|
||||
attachExecutionWorkspaceId(executionWorkspaceId: string | null): Promise<void>;
|
||||
recordOperation(input: {
|
||||
@@ -91,7 +102,7 @@ export function workspaceOperationService(db: Db) {
|
||||
heartbeatRunId?: string | null;
|
||||
executionWorkspaceId?: string | null;
|
||||
issueId?: string | null;
|
||||
}): WorkspaceOperationRecorder {
|
||||
} & WorkspaceOperationAttribution): WorkspaceOperationRecorder {
|
||||
let executionWorkspaceId = input.executionWorkspaceId ?? null;
|
||||
const createdIds: string[] = [];
|
||||
|
||||
@@ -138,6 +149,10 @@ export function workspaceOperationService(db: Db) {
|
||||
companyId: input.companyId,
|
||||
executionWorkspaceId,
|
||||
heartbeatRunId: input.heartbeatRunId ?? null,
|
||||
actorAgentId: input.actorAgentId ?? null,
|
||||
actorUserId: input.actorUserId ?? null,
|
||||
actorRunId: input.actorRunId ?? null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
issueId: input.issueId ?? null,
|
||||
phase: recordInput.phase,
|
||||
command: recordInput.command ?? null,
|
||||
|
||||
@@ -110,6 +110,13 @@ export interface RuntimeServiceRef {
|
||||
providerRef: string | null;
|
||||
ownerAgentId: string | null;
|
||||
startedByRunId: string | null;
|
||||
startedByAgentId: string | null;
|
||||
startedByUserId: string | null;
|
||||
responsibleUserId: string | null;
|
||||
lastControlledByAgentId: string | null;
|
||||
lastControlledByUserId: string | null;
|
||||
lastControlledByRunId: string | null;
|
||||
lastControlledAt: string | null;
|
||||
lastUsedAt: string;
|
||||
startedAt: string;
|
||||
stoppedAt: string | null;
|
||||
@@ -118,6 +125,13 @@ export interface RuntimeServiceRef {
|
||||
reused: boolean;
|
||||
}
|
||||
|
||||
interface RuntimeServiceAttribution {
|
||||
actorAgentId?: string | null;
|
||||
actorUserId?: string | null;
|
||||
actorRunId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
}
|
||||
|
||||
interface RuntimeServiceRecord extends RuntimeServiceRef {
|
||||
db?: Db;
|
||||
child: ChildProcess | null;
|
||||
@@ -371,6 +385,13 @@ function toRuntimeServiceRef(record: RuntimeServiceRecord, overrides?: Partial<R
|
||||
providerRef: record.providerRef,
|
||||
ownerAgentId: record.ownerAgentId,
|
||||
startedByRunId: record.startedByRunId,
|
||||
startedByAgentId: record.startedByAgentId,
|
||||
startedByUserId: record.startedByUserId,
|
||||
responsibleUserId: record.responsibleUserId,
|
||||
lastControlledByAgentId: record.lastControlledByAgentId,
|
||||
lastControlledByUserId: record.lastControlledByUserId,
|
||||
lastControlledByRunId: record.lastControlledByRunId,
|
||||
lastControlledAt: record.lastControlledAt,
|
||||
lastUsedAt: record.lastUsedAt,
|
||||
startedAt: record.startedAt,
|
||||
stoppedAt: record.stoppedAt,
|
||||
@@ -381,6 +402,45 @@ function toRuntimeServiceRef(record: RuntimeServiceRecord, overrides?: Partial<R
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeServiceAttributionFields(
|
||||
attribution: RuntimeServiceAttribution | null | undefined,
|
||||
fallback: { actorAgentId?: string | null; actorRunId?: string | null } = {},
|
||||
) {
|
||||
return {
|
||||
actorAgentId: attribution?.actorAgentId ?? fallback.actorAgentId ?? null,
|
||||
actorUserId: attribution?.actorUserId ?? null,
|
||||
actorRunId: attribution?.actorRunId ?? fallback.actorRunId ?? null,
|
||||
responsibleUserId: attribution?.responsibleUserId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function applyRuntimeServiceControlAttribution(
|
||||
record: RuntimeServiceRecord,
|
||||
attribution: RuntimeServiceAttribution | null | undefined,
|
||||
controlledAt = new Date().toISOString(),
|
||||
) {
|
||||
if (!attribution) return;
|
||||
record.lastControlledByAgentId = attribution.actorAgentId ?? null;
|
||||
record.lastControlledByUserId = attribution.actorUserId ?? null;
|
||||
record.lastControlledByRunId = attribution.actorRunId ?? null;
|
||||
record.lastControlledAt = controlledAt;
|
||||
record.responsibleUserId = attribution.responsibleUserId ?? record.responsibleUserId;
|
||||
}
|
||||
|
||||
function runtimeServiceControlUpdate(
|
||||
attribution: RuntimeServiceAttribution | null | undefined,
|
||||
controlledAt: Date,
|
||||
): Partial<typeof workspaceRuntimeServices.$inferInsert> {
|
||||
if (!attribution) return {};
|
||||
return {
|
||||
lastControlledByAgentId: attribution.actorAgentId ?? null,
|
||||
lastControlledByUserId: attribution.actorUserId ?? null,
|
||||
lastControlledByRunId: attribution.actorRunId ?? null,
|
||||
lastControlledAt: controlledAt,
|
||||
...(attribution.responsibleUserId ? { responsibleUserId: attribution.responsibleUserId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeSlugPart(value: string | null | undefined, fallback: string): string {
|
||||
const raw = (value ?? "").trim().toLowerCase();
|
||||
const normalized = raw
|
||||
@@ -3646,6 +3706,13 @@ function toPersistedWorkspaceRuntimeService(record: RuntimeServiceRecord): typeo
|
||||
providerRef: record.providerRef,
|
||||
ownerAgentId: record.ownerAgentId,
|
||||
startedByRunId: record.startedByRunId,
|
||||
startedByAgentId: record.startedByAgentId,
|
||||
startedByUserId: record.startedByUserId,
|
||||
responsibleUserId: record.responsibleUserId,
|
||||
lastControlledByAgentId: record.lastControlledByAgentId,
|
||||
lastControlledByUserId: record.lastControlledByUserId,
|
||||
lastControlledByRunId: record.lastControlledByRunId,
|
||||
lastControlledAt: record.lastControlledAt ? new Date(record.lastControlledAt) : null,
|
||||
lastUsedAt: new Date(record.lastUsedAt),
|
||||
startedAt: new Date(record.startedAt),
|
||||
stoppedAt: record.stoppedAt ? new Date(record.stoppedAt) : null,
|
||||
@@ -3682,6 +3749,13 @@ async function persistRuntimeServiceRecord(db: Db | undefined, record: RuntimeSe
|
||||
providerRef: values.providerRef,
|
||||
ownerAgentId: values.ownerAgentId,
|
||||
startedByRunId: values.startedByRunId,
|
||||
startedByAgentId: values.startedByAgentId,
|
||||
startedByUserId: values.startedByUserId,
|
||||
responsibleUserId: values.responsibleUserId,
|
||||
lastControlledByAgentId: values.lastControlledByAgentId,
|
||||
lastControlledByUserId: values.lastControlledByUserId,
|
||||
lastControlledByRunId: values.lastControlledByRunId,
|
||||
lastControlledAt: values.lastControlledAt,
|
||||
lastUsedAt: values.lastUsedAt,
|
||||
startedAt: values.startedAt,
|
||||
stoppedAt: values.stoppedAt,
|
||||
@@ -3765,6 +3839,7 @@ export function normalizeAdapterManagedRuntimeServices(input: {
|
||||
issue: ExecutionWorkspaceIssueRef | null;
|
||||
workspace: RealizedExecutionWorkspace;
|
||||
executionWorkspaceId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
reports: AdapterRuntimeServiceReport[];
|
||||
now?: Date;
|
||||
}): RuntimeServiceRef[] {
|
||||
@@ -3817,6 +3892,13 @@ export function normalizeAdapterManagedRuntimeServices(input: {
|
||||
providerRef: report.providerRef ?? null,
|
||||
ownerAgentId: report.ownerAgentId ?? input.agent.id ?? null,
|
||||
startedByRunId: input.runId,
|
||||
startedByAgentId: input.agent.id ?? null,
|
||||
startedByUserId: null,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
lastControlledByAgentId: input.agent.id ?? null,
|
||||
lastControlledByUserId: null,
|
||||
lastControlledByRunId: input.runId,
|
||||
lastControlledAt: nowIso,
|
||||
lastUsedAt: nowIso,
|
||||
startedAt: nowIso,
|
||||
stoppedAt: status === "running" || status === "starting" ? null : nowIso,
|
||||
@@ -3832,6 +3914,7 @@ type StartLocalRuntimeServiceInput = {
|
||||
runId: string;
|
||||
leaseRunId?: string | null;
|
||||
startedByRunId?: string | null;
|
||||
startAttribution?: RuntimeServiceAttribution | null;
|
||||
agent: ExecutionWorkspaceAgentRef;
|
||||
issue: ExecutionWorkspaceIssueRef | null;
|
||||
workspace: RealizedExecutionWorkspace;
|
||||
@@ -3847,6 +3930,10 @@ type StartLocalRuntimeServiceInput = {
|
||||
async function spawnLocalRuntimeService(input: StartLocalRuntimeServiceInput): Promise<LocalRuntimeServiceStart> {
|
||||
const leaseRunId = input.leaseRunId === undefined ? input.runId : input.leaseRunId;
|
||||
const startedByRunId = input.startedByRunId === undefined ? input.runId : input.startedByRunId;
|
||||
const startAttribution = runtimeServiceAttributionFields(input.startAttribution, {
|
||||
actorAgentId: input.agent.id ?? null,
|
||||
actorRunId: startedByRunId ?? null,
|
||||
});
|
||||
const identity = resolveRuntimeServiceReuseIdentity({
|
||||
service: input.service,
|
||||
workspace: input.workspace,
|
||||
@@ -3971,6 +4058,13 @@ async function spawnLocalRuntimeService(input: StartLocalRuntimeServiceInput): P
|
||||
providerRef: String(adoptedRecord.pid),
|
||||
ownerAgentId: input.agent.id ?? null,
|
||||
startedByRunId,
|
||||
startedByAgentId: startAttribution.actorAgentId,
|
||||
startedByUserId: startAttribution.actorUserId,
|
||||
responsibleUserId: startAttribution.responsibleUserId,
|
||||
lastControlledByAgentId: startAttribution.actorAgentId,
|
||||
lastControlledByUserId: startAttribution.actorUserId,
|
||||
lastControlledByRunId: startAttribution.actorRunId,
|
||||
lastControlledAt: new Date().toISOString(),
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
startedAt: adoptedRecord.startedAt,
|
||||
stoppedAt: null,
|
||||
@@ -4060,6 +4154,13 @@ async function spawnLocalRuntimeService(input: StartLocalRuntimeServiceInput): P
|
||||
providerRef: child.pid ? String(child.pid) : null,
|
||||
ownerAgentId: input.agent.id ?? null,
|
||||
startedByRunId,
|
||||
startedByAgentId: startAttribution.actorAgentId,
|
||||
startedByUserId: startAttribution.actorUserId,
|
||||
responsibleUserId: startAttribution.responsibleUserId,
|
||||
lastControlledByAgentId: startAttribution.actorAgentId,
|
||||
lastControlledByUserId: startAttribution.actorUserId,
|
||||
lastControlledByRunId: startAttribution.actorRunId,
|
||||
lastControlledAt: nowIso,
|
||||
lastUsedAt: nowIso,
|
||||
startedAt: nowIso,
|
||||
stoppedAt: null,
|
||||
@@ -4148,7 +4249,7 @@ function scheduleIdleStop(record: RuntimeServiceRecord) {
|
||||
}, idleSeconds * 1000);
|
||||
}
|
||||
|
||||
async function stopRuntimeService(serviceId: string) {
|
||||
async function stopRuntimeService(serviceId: string, controlAttribution?: RuntimeServiceAttribution | null) {
|
||||
const record = runtimeServicesById.get(serviceId);
|
||||
if (!record) return;
|
||||
clearIdleTimer(record);
|
||||
@@ -4156,6 +4257,7 @@ async function stopRuntimeService(serviceId: string) {
|
||||
record.healthStatus = "unknown";
|
||||
record.lastUsedAt = new Date().toISOString();
|
||||
record.stoppedAt = new Date().toISOString();
|
||||
applyRuntimeServiceControlAttribution(record, controlAttribution ?? null, record.stoppedAt);
|
||||
runtimeServicesById.delete(serviceId);
|
||||
if (record.reuseKey && runtimeServicesByReuseKey.get(record.reuseKey) === record.id) {
|
||||
runtimeServicesByReuseKey.delete(record.reuseKey);
|
||||
@@ -4181,6 +4283,7 @@ async function stopRuntimeService(serviceId: string) {
|
||||
async function markPersistedRuntimeServicesStoppedForExecutionWorkspace(input: {
|
||||
db: Db;
|
||||
executionWorkspaceId: string;
|
||||
controlAttribution?: RuntimeServiceAttribution | null;
|
||||
}) {
|
||||
const now = new Date();
|
||||
await input.db
|
||||
@@ -4190,6 +4293,7 @@ async function markPersistedRuntimeServicesStoppedForExecutionWorkspace(input: {
|
||||
healthStatus: "unknown",
|
||||
stoppedAt: now,
|
||||
lastUsedAt: now,
|
||||
...runtimeServiceControlUpdate(input.controlAttribution ?? null, now),
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(
|
||||
@@ -4323,6 +4427,7 @@ export async function ensureRuntimeServicesForRun(input: {
|
||||
executionWorkspaceId?: string | null;
|
||||
config: Record<string, unknown>;
|
||||
adapterEnv: Record<string, string>;
|
||||
responsibleUserId?: string | null;
|
||||
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||
}): Promise<RuntimeServiceRef[]> {
|
||||
const rawServices = selectRuntimeServiceEntries({
|
||||
@@ -4361,6 +4466,7 @@ export async function ensureRuntimeServicesForRun(input: {
|
||||
if (existing && existing.status === "running") {
|
||||
existing.leaseRunIds.add(input.runId);
|
||||
existing.lastUsedAt = new Date().toISOString();
|
||||
existing.responsibleUserId = existing.responsibleUserId ?? input.responsibleUserId ?? null;
|
||||
existing.stoppedAt = null;
|
||||
clearIdleTimer(existing);
|
||||
void touchLocalServiceRegistryRecord(existing.serviceKey, {
|
||||
@@ -4384,6 +4490,12 @@ export async function ensureRuntimeServicesForRun(input: {
|
||||
adapterEnv: input.adapterEnv,
|
||||
service,
|
||||
onLog: input.onLog,
|
||||
startAttribution: {
|
||||
actorAgentId: input.agent.id ?? null,
|
||||
actorUserId: null,
|
||||
actorRunId: input.runId,
|
||||
responsibleUserId: input.responsibleUserId ?? null,
|
||||
},
|
||||
reuseKey,
|
||||
scopeType,
|
||||
scopeId,
|
||||
@@ -4413,6 +4525,7 @@ type StartRuntimeServicesForWorkspaceControlInput = {
|
||||
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||
serviceIndex?: number | null;
|
||||
respectDesiredStates?: boolean;
|
||||
controlAttribution?: RuntimeServiceAttribution | null;
|
||||
};
|
||||
|
||||
type WorkspaceControlStartBatch = {
|
||||
@@ -4457,6 +4570,7 @@ async function startRuntimeServicesForWorkspaceControlUnlocked(
|
||||
const existing = existingId ? runtimeServicesById.get(existingId) : null;
|
||||
if (existing && existing.status === "running") {
|
||||
existing.lastUsedAt = new Date().toISOString();
|
||||
applyRuntimeServiceControlAttribution(existing, input.controlAttribution ?? null, existing.lastUsedAt);
|
||||
existing.stoppedAt = null;
|
||||
clearIdleTimer(existing);
|
||||
void touchLocalServiceRegistryRecord(existing.serviceKey, {
|
||||
@@ -4474,6 +4588,7 @@ async function startRuntimeServicesForWorkspaceControlUnlocked(
|
||||
runId: invocationId,
|
||||
leaseRunId: null,
|
||||
startedByRunId: null,
|
||||
startAttribution: input.controlAttribution ?? null,
|
||||
agent: input.actor,
|
||||
issue: input.issue,
|
||||
workspace: input.workspace,
|
||||
@@ -4624,6 +4739,7 @@ export async function stopRuntimeServicesForExecutionWorkspace(input: {
|
||||
executionWorkspaceId: string;
|
||||
workspaceCwd?: string | null;
|
||||
runtimeServiceId?: string | null;
|
||||
controlAttribution?: RuntimeServiceAttribution | null;
|
||||
}) {
|
||||
const normalizedWorkspaceCwd = input.workspaceCwd ? path.resolve(input.workspaceCwd) : null;
|
||||
const matchingServiceIds = Array.from(runtimeServicesById.values())
|
||||
@@ -4640,7 +4756,7 @@ export async function stopRuntimeServicesForExecutionWorkspace(input: {
|
||||
.map((record) => record.id);
|
||||
|
||||
for (const serviceId of matchingServiceIds) {
|
||||
await stopRuntimeService(serviceId);
|
||||
await stopRuntimeService(serviceId, input.controlAttribution ?? null);
|
||||
}
|
||||
|
||||
if (input.db) {
|
||||
@@ -4653,6 +4769,7 @@ export async function stopRuntimeServicesForExecutionWorkspace(input: {
|
||||
healthStatus: "unknown",
|
||||
stoppedAt: now,
|
||||
lastUsedAt: now,
|
||||
...runtimeServiceControlUpdate(input.controlAttribution ?? null, now),
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(workspaceRuntimeServices.id, input.runtimeServiceId));
|
||||
@@ -4660,6 +4777,7 @@ export async function stopRuntimeServicesForExecutionWorkspace(input: {
|
||||
await markPersistedRuntimeServicesStoppedForExecutionWorkspace({
|
||||
db: input.db,
|
||||
executionWorkspaceId: input.executionWorkspaceId,
|
||||
controlAttribution: input.controlAttribution ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4669,6 +4787,7 @@ export async function stopRuntimeServicesForProjectWorkspace(input: {
|
||||
db?: Db;
|
||||
projectWorkspaceId: string;
|
||||
runtimeServiceId?: string | null;
|
||||
controlAttribution?: RuntimeServiceAttribution | null;
|
||||
}) {
|
||||
const matchingServiceIds = Array.from(runtimeServicesById.values())
|
||||
.filter((record) => {
|
||||
@@ -4678,7 +4797,7 @@ export async function stopRuntimeServicesForProjectWorkspace(input: {
|
||||
.map((record) => record.id);
|
||||
|
||||
for (const serviceId of matchingServiceIds) {
|
||||
await stopRuntimeService(serviceId);
|
||||
await stopRuntimeService(serviceId, input.controlAttribution ?? null);
|
||||
}
|
||||
|
||||
if (input.db) {
|
||||
@@ -4690,6 +4809,7 @@ export async function stopRuntimeServicesForProjectWorkspace(input: {
|
||||
healthStatus: "unknown",
|
||||
stoppedAt: now,
|
||||
lastUsedAt: now,
|
||||
...runtimeServiceControlUpdate(input.controlAttribution ?? null, now),
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(
|
||||
@@ -4817,6 +4937,13 @@ export async function reconcilePersistedRuntimeServicesOnStartup(db: Db) {
|
||||
providerRef: String(adoptedRecord.pid),
|
||||
ownerAgentId: row.ownerAgentId ?? null,
|
||||
startedByRunId: row.startedByRunId ?? null,
|
||||
startedByAgentId: row.startedByAgentId ?? null,
|
||||
startedByUserId: row.startedByUserId ?? null,
|
||||
responsibleUserId: row.responsibleUserId ?? null,
|
||||
lastControlledByAgentId: row.lastControlledByAgentId ?? null,
|
||||
lastControlledByUserId: row.lastControlledByUserId ?? null,
|
||||
lastControlledByRunId: row.lastControlledByRunId ?? null,
|
||||
lastControlledAt: row.lastControlledAt?.toISOString() ?? null,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
startedAt: row.startedAt.toISOString(),
|
||||
stoppedAt: null,
|
||||
@@ -4985,6 +5112,7 @@ export async function persistAdapterManagedRuntimeServices(input: {
|
||||
issue: ExecutionWorkspaceIssueRef | null;
|
||||
workspace: RealizedExecutionWorkspace;
|
||||
executionWorkspaceId?: string | null;
|
||||
responsibleUserId?: string | null;
|
||||
reports: AdapterRuntimeServiceReport[];
|
||||
}) {
|
||||
const refs = normalizeAdapterManagedRuntimeServices(input);
|
||||
@@ -5023,6 +5151,13 @@ export async function persistAdapterManagedRuntimeServices(input: {
|
||||
providerRef: ref.providerRef,
|
||||
ownerAgentId: ref.ownerAgentId,
|
||||
startedByRunId: ref.startedByRunId,
|
||||
startedByAgentId: ref.startedByAgentId,
|
||||
startedByUserId: ref.startedByUserId,
|
||||
responsibleUserId: ref.responsibleUserId,
|
||||
lastControlledByAgentId: ref.lastControlledByAgentId,
|
||||
lastControlledByUserId: ref.lastControlledByUserId,
|
||||
lastControlledByRunId: ref.lastControlledByRunId,
|
||||
lastControlledAt: ref.lastControlledAt ? new Date(ref.lastControlledAt) : null,
|
||||
lastUsedAt: new Date(ref.lastUsedAt),
|
||||
startedAt,
|
||||
stoppedAt: ref.stoppedAt ? new Date(ref.stoppedAt) : null,
|
||||
@@ -5052,6 +5187,13 @@ export async function persistAdapterManagedRuntimeServices(input: {
|
||||
providerRef: ref.providerRef,
|
||||
ownerAgentId: ref.ownerAgentId,
|
||||
startedByRunId: ref.startedByRunId,
|
||||
startedByAgentId: ref.startedByAgentId,
|
||||
startedByUserId: ref.startedByUserId,
|
||||
responsibleUserId: ref.responsibleUserId,
|
||||
lastControlledByAgentId: ref.lastControlledByAgentId,
|
||||
lastControlledByUserId: ref.lastControlledByUserId,
|
||||
lastControlledByRunId: ref.lastControlledByRunId,
|
||||
lastControlledAt: ref.lastControlledAt ? new Date(ref.lastControlledAt) : null,
|
||||
lastUsedAt: new Date(ref.lastUsedAt),
|
||||
startedAt,
|
||||
stoppedAt: ref.stoppedAt ? new Date(ref.stoppedAt) : null,
|
||||
|
||||
Reference in new issue
Block a user