Merge public-gh/master into paperclip-company-import-export

This commit is contained in:
dotta
2026-03-20 06:25:24 -05:00
41 changed files with 11912 additions and 392 deletions

View File

@@ -12,6 +12,8 @@ export interface RunProcessResult {
timedOut: boolean;
stdout: string;
stderr: string;
pid: number | null;
startedAt: string | null;
}
interface RunningProcess {
@@ -724,6 +726,7 @@ export async function runChildProcess(
graceSec: number;
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
onLogError?: (err: unknown, runId: string, message: string) => void;
onSpawn?: (meta: { pid: number; startedAt: string }) => Promise<void>;
stdin?: string;
},
): Promise<RunProcessResult> {
@@ -756,12 +759,19 @@ export async function runChildProcess(
shell: false,
stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
}) as ChildProcessWithEvents;
const startedAt = new Date().toISOString();
if (opts.stdin != null && child.stdin) {
child.stdin.write(opts.stdin);
child.stdin.end();
}
if (typeof child.pid === "number" && child.pid > 0 && opts.onSpawn) {
void opts.onSpawn({ pid: child.pid, startedAt }).catch((err) => {
onLogError(err, runId, "failed to record child process metadata");
});
}
runningProcesses.set(runId, { child, graceSec: opts.graceSec });
let timedOut = false;
@@ -820,6 +830,8 @@ export async function runChildProcess(
timedOut,
stdout,
stderr,
pid: child.pid ?? null,
startedAt,
});
});
});

View File

@@ -120,6 +120,7 @@ export interface AdapterExecutionContext {
context: Record<string, unknown>;
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
onMeta?: (meta: AdapterInvocationMeta) => Promise<void>;
onSpawn?: (meta: { pid: number; startedAt: string }) => Promise<void>;
authToken?: string;
}

View File

@@ -296,7 +296,7 @@ export async function runClaudeLogin(input: {
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
const promptTemplate = asString(
config.promptTemplate,
@@ -362,7 +362,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const sessionId = canResumeSession ? runtimeSessionId : null;
if (runtimeSessionId && !canResumeSession) {
await onLog(
"stderr",
"stdout",
`[paperclip] Claude session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
);
}
@@ -448,6 +448,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
stdin: prompt,
timeoutSec,
graceSec,
onSpawn,
onLog,
});
@@ -565,7 +566,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
isClaudeUnknownSessionError(initial.parsed)
) {
await onLog(
"stderr",
"stdout",
`[paperclip] Claude resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
);
const retry = await runAttempt(null);

View File

@@ -212,7 +212,7 @@ export async function ensureCodexSkillsInjected(
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
const promptTemplate = asString(
config.promptTemplate,
@@ -398,7 +398,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const sessionId = canResumeSession ? runtimeSessionId : null;
if (runtimeSessionId && !canResumeSession) {
await onLog(
"stderr",
"stdout",
`[paperclip] Codex session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
);
}
@@ -421,7 +421,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
await onLog(
"stderr",
"stdout",
`[paperclip] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`,
);
}
@@ -505,6 +505,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
stdin: prompt,
timeoutSec,
graceSec,
onSpawn,
onLog: async (stream, chunk) => {
if (stream !== "stderr") {
await onLog(stream, chunk);
@@ -591,7 +592,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
isCodexUnknownSessionError(initial.proc.stdout, initial.rawStderr)
) {
await onLog(
"stderr",
"stdout",
`[paperclip] Codex resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
);
const retry = await runAttempt(null);

View File

@@ -157,7 +157,7 @@ export async function ensureCursorSkillsInjected(
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
const promptTemplate = asString(
config.promptTemplate,
@@ -290,7 +290,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const sessionId = canResumeSession ? runtimeSessionId : null;
if (runtimeSessionId && !canResumeSession) {
await onLog(
"stderr",
"stdout",
`[paperclip] Cursor session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
);
}
@@ -308,13 +308,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
`Resolve any relative file references from ${instructionsDir}.\n\n`;
instructionsChars = instructionsPrefix.length;
await onLog(
"stderr",
"stdout",
`[paperclip] Loaded agent instructions file: ${instructionsFilePath}\n`,
);
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
await onLog(
"stderr",
"stdout",
`[paperclip] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`,
);
}
@@ -428,6 +428,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
timeoutSec,
graceSec,
stdin: prompt,
onSpawn,
onLog: async (stream, chunk) => {
if (stream !== "stdout") {
await onLog(stream, chunk);
@@ -520,7 +521,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
isCursorUnknownSessionError(initial.proc.stdout, initial.proc.stderr)
) {
await onLog(
"stderr",
"stdout",
`[paperclip] Cursor resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
);
const retry = await runAttempt(null);

View File

@@ -133,7 +133,7 @@ async function ensureGeminiSkillsInjected(
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
const promptTemplate = asString(
config.promptTemplate,
@@ -238,7 +238,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const sessionId = canResumeSession ? runtimeSessionId : null;
if (runtimeSessionId && !canResumeSession) {
await onLog(
"stderr",
"stdout",
`[paperclip] Gemini session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
);
}
@@ -254,13 +254,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
`The above agent instructions were loaded from ${instructionsFilePath}. ` +
`Resolve any relative file references from ${instructionsDir}.\n\n`;
await onLog(
"stderr",
"stdout",
`[paperclip] Loaded agent instructions file: ${instructionsFilePath}\n`,
);
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
await onLog(
"stderr",
"stdout",
`[paperclip] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`,
);
}
@@ -355,6 +355,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
env,
timeoutSec,
graceSec,
onSpawn,
onLog,
});
return {
@@ -453,7 +454,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
isGeminiUnknownSessionError(initial.proc.stdout, initial.proc.stderr)
) {
await onLog(
"stderr",
"stdout",
`[paperclip] Gemini resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
);
const retry = await runAttempt(null);

View File

@@ -89,7 +89,7 @@ async function ensureOpenCodeSkillsInjected(
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
const promptTemplate = asString(
config.promptTemplate,
@@ -203,7 +203,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const sessionId = canResumeSession ? runtimeSessionId : null;
if (runtimeSessionId && !canResumeSession) {
await onLog(
"stderr",
"stdout",
`[paperclip] OpenCode session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
);
}
@@ -222,13 +222,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
`The above agent instructions were loaded from ${resolvedInstructionsFilePath}. ` +
`Resolve any relative file references from ${instructionsDir}.\n\n`;
await onLog(
"stderr",
"stdout",
`[paperclip] Loaded agent instructions file: ${resolvedInstructionsFilePath}\n`,
);
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
await onLog(
"stderr",
"stdout",
`[paperclip] Warning: could not read agent instructions file "${resolvedInstructionsFilePath}": ${reason}\n`,
);
}
@@ -308,6 +308,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
stdin: prompt,
timeoutSec,
graceSec,
onSpawn,
onLog,
});
return {
@@ -394,7 +395,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
isOpenCodeUnknownSessionError(initial.proc.stdout, initial.rawStderr)
) {
await onLog(
"stderr",
"stdout",
`[paperclip] OpenCode session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
);
const retry = await runAttempt(null);

View File

@@ -106,7 +106,7 @@ function buildSessionPath(agentId: string, timestamp: string): string {
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
const promptTemplate = asString(
config.promptTemplate,
@@ -232,7 +232,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
if (runtimeSessionId && !canResumeSession) {
await onLog(
"stderr",
"stdout",
`[paperclip] Pi session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
);
}
@@ -267,14 +267,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
`Resolve any relative file references from ${instructionsFileDir}.\n\n` +
`You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.`;
await onLog(
"stderr",
"stdout",
`[paperclip] Loaded agent instructions file: ${resolvedInstructionsFilePath}\n`,
);
} catch (err) {
instructionsReadFailed = true;
const reason = err instanceof Error ? err.message : String(err);
await onLog(
"stderr",
"stdout",
`[paperclip] Warning: could not read agent instructions file "${resolvedInstructionsFilePath}": ${reason}\n`,
);
// Fall back to base prompt template
@@ -404,6 +404,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
env: runtimeEnv,
timeoutSec,
graceSec,
onSpawn,
onLog: bufferedOnLog,
stdin: buildRpcStdin(),
});
@@ -484,7 +485,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
isPiUnknownSessionError(initial.proc.stdout, initial.rawStderr)
) {
await onLog(
"stderr",
"stdout",
`[paperclip] Pi session "${runtimeSessionId}" is unavailable; retrying with a fresh session.\n`,
);
const newSessionPath = buildSessionPath(agent.id, new Date().toISOString());

View File

@@ -0,0 +1,5 @@
ALTER TABLE "heartbeat_runs" ADD COLUMN "process_pid" integer;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "process_started_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "retry_of_run_id" uuid;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "process_loss_retry_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD CONSTRAINT "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("retry_of_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE set null ON UPDATE no action;

File diff suppressed because it is too large Load Diff

View File

@@ -267,6 +267,13 @@
"when": 1773756922363,
"tag": "0037_friendly_eddie_brock",
"breakpoints": true
},
{
"idx": 38,
"version": "7",
"when": 1773931592563,
"tag": "0038_careless_iron_monger",
"breakpoints": true
}
]
}

View File

@@ -1,4 +1,4 @@
import { pgTable, uuid, text, timestamp, jsonb, index, integer, bigint, boolean } from "drizzle-orm/pg-core";
import { type AnyPgColumn, pgTable, uuid, text, timestamp, jsonb, index, integer, bigint, boolean } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { agents } from "./agents.js";
import { agentWakeupRequests } from "./agent_wakeup_requests.js";
@@ -31,6 +31,12 @@ export const heartbeatRuns = pgTable(
stderrExcerpt: text("stderr_excerpt"),
errorCode: text("error_code"),
externalRunId: text("external_run_id"),
processPid: integer("process_pid"),
processStartedAt: timestamp("process_started_at", { withTimezone: true }),
retryOfRunId: uuid("retry_of_run_id").references((): AnyPgColumn => heartbeatRuns.id, {
onDelete: "set null",
}),
processLossRetryCount: integer("process_loss_retry_count").notNull().default(0),
contextSnapshot: jsonb("context_snapshot").$type<Record<string, unknown>>(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),

View File

@@ -148,6 +148,9 @@ export type {
InstanceExperimentalSettings,
InstanceSettings,
Agent,
AgentAccessState,
AgentChainOfCommandEntry,
AgentDetail,
AgentPermissions,
AgentInstructionsBundleMode,
AgentInstructionsFileSummary,

View File

@@ -4,6 +4,10 @@ import type {
AgentRole,
AgentStatus,
} from "../constants.js";
import type {
CompanyMembership,
PrincipalPermissionGrant,
} from "./access.js";
export interface AgentPermissions {
canCreateAgents: boolean;
@@ -41,6 +45,20 @@ export interface AgentInstructionsBundle {
files: AgentInstructionsFileSummary[];
}
export interface AgentAccessState {
canAssignTasks: boolean;
taskAssignSource: "explicit_grant" | "agent_creator" | "ceo_role" | "none";
membership: CompanyMembership | null;
grants: PrincipalPermissionGrant[];
}
export interface AgentChainOfCommandEntry {
id: string;
name: string;
role: AgentRole;
title: string | null;
}
export interface Agent {
id: string;
companyId: string;
@@ -66,6 +84,11 @@ export interface Agent {
updatedAt: Date;
}
export interface AgentDetail extends Agent {
chainOfCommand: AgentChainOfCommandEntry[];
access: AgentAccessState;
}
export interface AgentKeyCreated {
id: string;
name: string;

View File

@@ -33,6 +33,10 @@ export interface HeartbeatRun {
stderrExcerpt: string | null;
errorCode: string | null;
externalRunId: string | null;
processPid: number | null;
processStartedAt: Date | null;
retryOfRunId: string | null;
processLossRetryCount: number;
contextSnapshot: Record<string, unknown> | null;
createdAt: Date;
updatedAt: Date;

View File

@@ -31,6 +31,9 @@ export type {
} from "./adapter-skills.js";
export type {
Agent,
AgentAccessState,
AgentChainOfCommandEntry,
AgentDetail,
AgentPermissions,
AgentInstructionsBundleMode,
AgentInstructionsFileSummary,

View File

@@ -120,6 +120,7 @@ export type TestAdapterEnvironment = z.infer<typeof testAdapterEnvironmentSchema
export const updateAgentPermissionsSchema = z.object({
canCreateAgents: z.boolean(),
canAssignTasks: z.boolean(),
});
export type UpdateAgentPermissions = z.infer<typeof updateAgentPermissionsSchema>;

View File

@@ -26,12 +26,18 @@ export type UpdateCompany = z.infer<typeof updateCompanySchema>;
export const updateCompanyBrandingSchema = z
.object({
name: z.string().min(1).optional(),
description: z.string().nullable().optional(),
brandColor: brandColorSchema,
logoAssetId: logoAssetIdSchema,
})
.strict()
.refine(
(value) => value.brandColor !== undefined || value.logoAssetId !== undefined,
(value) =>
value.name !== undefined
|| value.description !== undefined
|| value.brandColor !== undefined
|| value.logoAssetId !== undefined,
"At least one branding field must be provided",
);