Add runtime and wake audit attribution

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-17 02:34:14 +00:00
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,
+4
View File
@@ -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;
+4
View File
@@ -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;
+4
View File
@@ -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);
});
});
+20
View File
@@ -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,
+38 -1
View File
@@ -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,
+12
View File
@@ -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;
+19 -1
View File
@@ -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 {
+12
View File
@@ -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,
+48 -31
View File
@@ -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));
+12
View File
@@ -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",
+7
View File
@@ -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 -1
View File
@@ -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,
+145 -3
View File
@@ -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,