Merge origin/master into provider routing connections

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* origin/master:
  fix(ui): let a selected saved subscription be used without a tile click (#14996)
  fix: report pending execution phase at Stop timeout (#14990)
  fix: retain resolved ACP execution timeout metadata (#14986)
  fix(agents): reduce hiring templates to role descriptions (#14985)
This commit is contained in:
DottaandPaperclip committed 2026-10-02 21:56:32 -05:00
commit cba9868517
60 files changed
+2587 -1065

No files matched your search

@@ -10,6 +10,16 @@ import {
afterEach(() => vi.useRealTimers());
async function stopTimeout(pending: Promise<void>): Promise<AdapterStopTimeoutError> {
try {
await pending;
} catch (error) {
expect(error).toBeInstanceOf(AdapterStopTimeoutError);
return error as AdapterStopTimeoutError;
}
throw new Error("Stop unexpectedly settled");
}
it("holds readiness for every exact-run no-owner Stop and releases owners idempotently", async () => {
const runId = "registration-after-two-stops";
const first = captureAdapterStopOwnership(runId);
@@ -139,6 +149,7 @@ it("records the exact unconfirmed run without finishing or removing its control"
expect(error).toBeInstanceOf(AdapterStopTimeoutError);
expect((error as AdapterStopTimeoutError).diagnostics).toEqual({
runId, adapterType: "claude_local", runtimeMode: "legacy", abortRequested: true, timeoutMs: 1000,
phase: "unknown", phaseElapsedMs: null,
});
expect(adapterExecutionControls.get(runId)).toBe(control);
expect(finished).toBe(false);
@@ -151,3 +162,45 @@ it("records the exact unconfirmed run without finishing or removing its control"
adapterExecutionControls.delete(runId);
}
});
it("samples the pending phase at each Stop timeout without settling the executor", async () => {
vi.useFakeTimers();
const runId = "11111111-1111-4111-8111-111111111111";
const control = createAdapterExecutionControl();
await registerAdapterExecutionControl(runId, control);
const read = vi.spyOn(control.phases, "snapshot");
const pending = stopTimeout(waitForAdapterStop(control.settled, 1000, { runId }, control));
expect(read).not.toHaveBeenCalled();
control.phases.enter("instruction_collection");
await vi.advanceTimersByTimeAsync(1000);
const first = await pending;
expect(first.diagnostics).toMatchObject({ phase: "instruction_collection", phaseElapsedMs: expect.any(Number) });
const secondWait = stopTimeout(waitForAdapterStop(control.settled, 1000, { runId }, control));
control.phases.enter("lease_release");
await vi.advanceTimersByTimeAsync(1000);
expect((await secondWait).diagnostics.phase).toBe("lease_release");
expect(first.diagnostics.phase).toBe("instruction_collection");
expect(Object.isFrozen(first.diagnostics)).toBe(true);
expect(adapterExecutionControls.get(runId)).toBe(control);
control.finish();
adapterExecutionControls.delete(runId);
});
it.each(["replaced", "wrong_run", "wrong_promise", "throwing_reader"])("omits pending phase for a %s control", async (scenario) => {
vi.useFakeTimers();
const runId = "11111111-1111-4111-8111-111111111111";
const owner = createAdapterExecutionControl();
const other = createAdapterExecutionControl();
await registerAdapterExecutionControl(runId, owner);
owner.phases.enter("instruction_collection");
const pending = stopTimeout(waitForAdapterStop(scenario === "wrong_promise" ? other.settled : owner.settled, 1000,
{ runId: scenario === "wrong_run" ? "22222222-2222-4222-8222-222222222222" : runId }, owner));
if (scenario === "replaced") await registerAdapterExecutionControl(runId, other);
if (scenario === "throwing_reader") vi.spyOn(owner.phases, "snapshot").mockImplementation(() => { throw new Error("private fixture payload"); });
await vi.advanceTimersByTimeAsync(1000);
expect((await pending).diagnostics).toMatchObject({ phase: "unknown", phaseElapsedMs: null });
expect(JSON.stringify((await pending).diagnostics)).not.toContain("private fixture payload");
owner.finish();
other.finish();
adapterExecutionControls.delete(runId);
});
@@ -1,13 +1,15 @@
import { AdapterStopTimeoutError, type AdapterStopContext } from "./adapter-stop-timeout.js";
import { createAdapterExecutionPhaseTracker } from "./adapter-execution-phase.js";
/** Live adapter ownership shared by routes and scheduler service instances. */
export function createAdapterExecutionControl() {
const controller = new AbortController();
const phases = createAdapterExecutionPhaseTracker();
let finish!: () => void;
const settled = new Promise<void>((resolve) => {
finish = resolve;
});
return { controller, settled, finish };
return { controller, settled, phases, finish: () => { phases.finish(); finish(); } };
}
export const adapterExecutionControls = new Map<
@@ -70,7 +72,17 @@ export async function waitForAdapterStop(
settled: Promise<void>,
timeoutMs = 60_000,
diagnostics?: AdapterStopContext,
owner?: ReturnType<typeof createAdapterExecutionControl>,
) {
const currentPhase = () => {
try {
return owner && owner.settled === settled && diagnostics?.runId
&& adapterExecutionControls.get(diagnostics.runId) === owner
? owner.phases.snapshot() : null;
} catch {
return null;
}
};
let timer: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
@@ -79,7 +91,12 @@ export async function waitForAdapterStop(
timer = setTimeout(
() =>
reject(
new AdapterStopTimeoutError(timeoutMs, diagnostics),
new AdapterStopTimeoutError(timeoutMs, {
...diagnostics,
phase: undefined,
phaseElapsedMs: undefined,
...currentPhase(),
}),
),
timeoutMs,
);
@@ -0,0 +1,60 @@
import { expect, it } from "vitest";
import { createAdapterExecutionPhaseTracker, MAX_EXECUTION_PHASE_ELAPSED_MS } from "./adapter-execution-phase.js";
it("returns the latest pending scope and resumes the outer scope's original age", () => {
let now = 10;
const phases = createAdapterExecutionPhaseTracker(() => now);
const outer = phases.enter("sync_back")!;
now = 30;
const inner = phases.enter("instruction_collection")!;
now = 80;
expect(phases.snapshot()).toEqual({ phase: "instruction_collection", phaseElapsedMs: 50 });
inner();
expect(phases.snapshot()).toEqual({ phase: "sync_back", phaseElapsedMs: 70 });
outer();
expect(phases.snapshot()).toBeNull();
});
it("late and repeated releases never clear a newer scope or revive a completed scope", () => {
const phases = createAdapterExecutionPhaseTracker(() => 20);
const first = phases.enter("end_session")!;
const second = phases.enter("close_session")!;
first();
first();
expect(phases.snapshot()?.phase).toBe("close_session");
second();
expect(phases.snapshot()).toBeNull();
});
it("isolates trackers and refuses late updates after the executor finishes", () => {
const first = createAdapterExecutionPhaseTracker();
const second = createAdapterExecutionPhaseTracker();
first.enter("workspace_restore");
second.enter("instruction_cleanup");
first.finish();
first.enter("lease_release");
expect(first.snapshot()).toBeNull();
expect(second.snapshot()?.phase).toBe("instruction_cleanup");
});
it("bounds retained scopes and fails closed on overflow or arbitrary labels", () => {
const phases = createAdapterExecutionPhaseTracker();
phases.enter("private fixture payload" as "sync_back");
expect(phases.snapshot()).toBeNull();
for (let i = 0; i < 100; i++) phases.enter("sync_back");
expect(phases.snapshot()).toBeNull();
phases.finish();
expect(phases.snapshot()).toBeNull();
});
it("uses a finite nonnegative capped elapsed time", () => {
let now = 10;
const phases = createAdapterExecutionPhaseTracker(() => now);
phases.enter("close_session");
now = Infinity;
expect(phases.snapshot()).toBeNull();
now = 9;
expect(phases.snapshot()).toBeNull();
now = MAX_EXECUTION_PHASE_ELAPSED_MS * 2;
expect(phases.snapshot()).toEqual({ phase: "close_session", phaseElapsedMs: MAX_EXECUTION_PHASE_ELAPSED_MS });
});
@@ -0,0 +1,44 @@
import { performance } from "node:perf_hooks";
import {
isAdapterExecutionPhase,
type AdapterExecutionPhase,
} from "@paperclipai/adapter-utils/execution-phase";
export const MAX_EXECUTION_PHASE_ELAPSED_MS = 86_400_000;
const MAX_ACTIVE_PHASES = 16;
/** Owned by one executor, never looked up by agent, task, or provider identity. */
export function createAdapterExecutionPhaseTracker(now = () => performance.now()) {
const scopes = new Map<symbol, { phase: AdapterExecutionPhase; startedAt: number }>();
let finished = false;
let overflow = false;
return {
enter(phase: AdapterExecutionPhase) {
if (finished || !isAdapterExecutionPhase(phase)) return;
if (scopes.size >= MAX_ACTIVE_PHASES) {
// Refuse further attribution rather than retain an unbounded history or
// describe an older scope as the current one. finish clears the tracker.
overflow = true;
return;
}
const token = Symbol();
let startedAt: number;
try { startedAt = now(); } catch { return; }
scopes.set(token, { phase, startedAt });
return () => { scopes.delete(token); };
},
snapshot() {
if (finished || overflow) return null;
const current = [...scopes.values()].at(-1);
if (!current) return null;
let elapsed: number;
try { elapsed = now() - current.startedAt; } catch { return null; }
if (!Number.isFinite(elapsed) || elapsed < 0) return null;
return { phase: current.phase, phaseElapsedMs: Math.min(Math.floor(elapsed), MAX_EXECUTION_PHASE_ELAPSED_MS) };
},
finish() {
finished = true;
scopes.clear();
},
};
}
@@ -1,10 +1,14 @@
import { AGENT_ADAPTER_TYPES } from "@paperclipai/shared";
import { isAdapterExecutionPhase } from "@paperclipai/adapter-utils/execution-phase";
import { MAX_EXECUTION_PHASE_ELAPSED_MS } from "./adapter-execution-phase.js";
export interface AdapterStopContext {
runId?: string;
adapterType?: string;
runtimeMode?: string;
abortRequested?: boolean;
phase?: string;
phaseElapsedMs?: number;
}
/** Diagnostic identity only. This error never acknowledges termination. */
@@ -22,6 +26,10 @@ export class AdapterStopTimeoutError extends Error {
adapterType: AGENT_ADAPTER_TYPES.some((type) => type === context?.adapterType) ? context!.adapterType! : "unknown",
runtimeMode: context?.runtimeMode === "native" || context?.runtimeMode === "legacy" ? context.runtimeMode : "unknown",
abortRequested: typeof context?.abortRequested === "boolean" ? context.abortRequested : null,
phase: isAdapterExecutionPhase(context?.phase) ? context.phase : "unknown",
phaseElapsedMs: isAdapterExecutionPhase(context?.phase) && Number.isSafeInteger(context?.phaseElapsedMs)
&& context!.phaseElapsedMs! >= 0 && context!.phaseElapsedMs! <= MAX_EXECUTION_PHASE_ELAPSED_MS
? context!.phaseElapsedMs! : null,
});
}
}
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
const DEFAULT_AGENT_BUNDLE_FILES = {
default: ["AGENTS.md"],
ceo: ["AGENTS.md", "HEARTBEAT.md", "SOUL.md", "TOOLS.md"],
ceo: ["AGENTS.md"],
} as const;
type DefaultAgentBundleRole = keyof typeof DEFAULT_AGENT_BUNDLE_FILES;
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { resolveAdapterExecutionTargetTimeout } from "@paperclipai/adapter-utils/execution-target";
import {
buildHeartbeatRunStopMetadata,
mergeHeartbeatRunStopMetadata,
@@ -42,6 +43,50 @@ describe("heartbeat stop metadata", () => {
});
});
it.each([
{ target: "sandbox", configured: undefined, seconds: 14400, source: "sandbox_default", explicit: false },
{ target: "sandbox", configured: 0, seconds: 14400, source: "sandbox_default", explicit: false },
{ target: "sandbox", configured: 90, seconds: 90, source: "configured", explicit: true },
{ target: "sandbox", configured: 0.5, seconds: 0.5, source: "configured", explicit: true },
{ target: "sandbox", configured: -1, seconds: 0, source: "configured", explicit: true },
{ target: "local", configured: 0, seconds: 0, source: "unlimited", explicit: false },
])("retains the resolved $target timeout for config $configured", ({ target, configured, seconds, source, explicit }) => {
const adapterExecutionTimeout = resolveAdapterExecutionTargetTimeout(
target === "sandbox" ? { kind: "remote", transport: "sandbox", remoteCwd: "/workspace" } : { kind: "local" },
configured,
);
const result = mergeHeartbeatRunStopMetadata(
{ adapterExecutionTimeout, summary: "retained", executionCancellation: { state: "unconfirmed" } },
buildHeartbeatRunStopMetadata({
adapterType: "claude_local", adapterConfig: { timeoutSec: configured }, outcome: "timed_out",
}),
);
expect(result).toMatchObject({
effectiveTimeoutSec: seconds, timeoutSource: source, timeoutConfigured: explicit,
stopReason: "timeout", timeoutFired: true, summary: "retained",
executionCancellation: { state: "unconfirmed" },
});
});
it.each([
null, [], { timeoutSec: 90 }, { timeoutSec: "90", source: "configured" },
{ timeoutSec: -1, source: "configured" }, { timeoutSec: Infinity, source: "configured" },
{ timeoutSec: 90, source: "unknown" }, { timeoutSec: 90, source: "unlimited" },
{ timeoutSec: 0, source: "sandbox_default" },
])("falls back to config for an invalid adapter resolution: %j", (adapterExecutionTimeout) => {
expect(mergeHeartbeatRunStopMetadata(
{ adapterExecutionTimeout },
buildHeartbeatRunStopMetadata({ adapterType: "claude_local", adapterConfig: { timeoutSec: 45 }, outcome: "failed" }),
)).toMatchObject({ effectiveTimeoutSec: 45, timeoutConfigured: true, timeoutSource: "config", timeoutFired: false });
});
it("keeps the HTTP millisecond policy internally consistent", () => {
expect(mergeHeartbeatRunStopMetadata(
{ adapterExecutionTimeout: { timeoutSec: 90, source: "configured" } },
buildHeartbeatRunStopMetadata({ adapterType: "http", adapterConfig: { timeoutMs: 2500 }, outcome: "failed" }),
)).toMatchObject({ effectiveTimeoutSec: 2.5, effectiveTimeoutMs: 2500, timeoutSource: "config", timeoutFired: false });
});
it("distinguishes budget cancellation from manual cancellation", () => {
expect(
buildHeartbeatRunStopMetadata({
+20 -4
View File
@@ -16,7 +16,7 @@ export interface HeartbeatRunTimeoutPolicy {
effectiveTimeoutSec: number | null;
effectiveTimeoutMs?: number | null;
timeoutConfigured: boolean;
timeoutSource: "config" | "default" | "unknown";
timeoutSource: "config" | "default" | "unknown" | "configured" | "sandbox_default" | "unlimited";
}
export interface HeartbeatRunStopMetadata extends HeartbeatRunTimeoutPolicy {
@@ -121,12 +121,28 @@ export function mergeHeartbeatRunStopMetadata(
metadata: HeartbeatRunStopMetadata,
): Record<string, unknown> {
const existingMaxTurnStopReason = normalizeMaxTurnStopReason(resultJson?.stopReason);
// Only a complete, valid adapter resolution overrides the config fallback.
// Older adapters and persisted rows retain their existing interpretation.
const resolution = resultJson?.adapterExecutionTimeout;
let timeoutPolicy: HeartbeatRunTimeoutPolicy = metadata;
if (metadata.effectiveTimeoutMs == null && resolution && typeof resolution === "object" && !Array.isArray(resolution)) {
const { timeoutSec, source } = resolution as Record<string, unknown>;
if (typeof timeoutSec === "number" && Number.isFinite(timeoutSec) && timeoutSec >= 0 &&
(source === "configured" || (source === "sandbox_default" && timeoutSec > 0) ||
(source === "unlimited" && timeoutSec === 0))) {
timeoutPolicy = {
effectiveTimeoutSec: timeoutSec,
timeoutConfigured: source === "configured",
timeoutSource: source,
};
}
}
return {
...(resultJson ?? {}),
stopReason: existingMaxTurnStopReason ?? metadata.stopReason,
effectiveTimeoutSec: metadata.effectiveTimeoutSec,
timeoutConfigured: metadata.timeoutConfigured,
timeoutSource: metadata.timeoutSource,
effectiveTimeoutSec: timeoutPolicy.effectiveTimeoutSec,
timeoutConfigured: timeoutPolicy.timeoutConfigured,
timeoutSource: timeoutPolicy.timeoutSource,
timeoutFired: metadata.timeoutFired,
...(metadata.effectiveTimeoutMs != null ? { effectiveTimeoutMs: metadata.effectiveTimeoutMs } : {}),
};
+13 -7
View File
@@ -24,6 +24,7 @@ import { githubBotConnectionIdsForRun } from "./chat-github-tools.js";
import { isBrowserUseConnection } from "./browser-use-client.js";
import { readQueuedInteractionResponse } from "./queued-interaction-response.js";
import { AGENT_CHAT_DIRECTIVE, isConversation, isConversationExecutionWake, isWaitingConversation, prepareConversationTurn, settleConversationTurn } from "./agent-conversations.js";
import { withAdapterExecutionPhase } from "@paperclipai/adapter-utils/execution-phase";
import { getConversationConfirmationContext, type ConversationConfirmationContext } from "./conversation-confirmation-context.js";
import { PROCESS_IDENTITY_RECORDED, recordNativeLocalProcessStop } from "./native-local-process-stop.js";
import { hasAcknowledgedNativeReassignmentStopIntent, hasAcknowledgedNativeStopIntent, isAcknowledgedNativeStop, acknowledgedNativeStopExecutionHasStopped } from "./acknowledged-native-stop.js";
@@ -20422,6 +20423,10 @@ export function heartbeatService(
run.controllerBootId !== legacyControllerBootId) return;
activeRunExecutions.add(run.id);
const executionControl = createAdapterExecutionControl();
// This coarse scope also covers host finalization after the adapter returns.
// Nested scopes refine the label without making any termination claim.
executionControl.phases.enter("host_execution");
const executionPhaseContext = { onExecutionPhase: executionControl.phases.enter };
const controllerLease = watchLegacyControllerLease(db, run, executionControl.controller);
let runScratch: HeartbeatRunScratch | null = null;
let githubLauncherLocation:
@@ -24909,7 +24914,7 @@ export function heartbeatService(
await dispatchResolvedInteractionContinuationWithAtomicGate(
(markDispatchStarted) => {
legacyAdapterEntered = true;
return adapter.execute({
return withAdapterExecutionPhase(executionPhaseContext, "adapter_execution", () => adapter.execute({
getFreshSessionHandoff,
runId: run.id,
agent,
@@ -24933,6 +24938,7 @@ export function heartbeatService(
onLog,
onMeta: onAdapterMeta,
onEvent: onAdapterEvent,
onExecutionPhase: executionControl.phases.enter,
startupTraceContext: getStartupTraceContext(),
onRuntimeProgress: async (progress) => {
await recordCurrentHeartbeatRunRuntimeProgress(
@@ -24984,7 +24990,7 @@ export function heartbeatService(
});
},
authToken: authToken ?? undefined,
});
}));
},
);
if (!guardedDispatch.dispatched) return;
@@ -25227,7 +25233,7 @@ export function heartbeatService(
);
}
await nativeInstructionReservation?.release();
await releaseInstructionCopy();
await withAdapterExecutionPhase(executionPhaseContext, "instruction_cleanup", releaseInstructionCopy);
}
// Reconcile the referenced-project set against the real remote staging outcome. A referenced
// project can pass authorization and clone locally at run prep, then fail to stage into the
@@ -26633,8 +26639,8 @@ export function heartbeatService(
message: "Instruction edits could not be recovered before environment release. No instruction save is claimed.",
payload: { state: "unavailable", code: uncapturedInstructions.errorCode } });
}
await releaseInstructionCopy();
await releaseEnvironmentLeasesForRun({
await withAdapterExecutionPhase(executionPhaseContext, "instruction_cleanup", releaseInstructionCopy);
await withAdapterExecutionPhase(executionPhaseContext, "lease_release", () => releaseEnvironmentLeasesForRun({
runId: run.id,
companyId: run.companyId,
agentId: run.agentId,
@@ -26642,7 +26648,7 @@ export function heartbeatService(
failureReason: latestRun?.error ?? undefined,
providerResourceDisposition: providerResourceDispositionForRun,
nativeLifecycleTelemetry: nativeLifecycleTelemetryForRun,
});
}));
await releaseRuntimeServicesForRun(run.id).catch(() => undefined);
}
if (
@@ -29614,7 +29620,7 @@ export function heartbeatService(
adapterType: agent?.adapterType,
runtimeMode: run.runtimeMode,
abortRequested: control.controller.signal.aborted,
});
}, control);
const stopped = await getRun(run.id);
if (stopped && isHeartbeatRunTerminalStatus(stopped.status)) {
if (
@@ -102,7 +102,6 @@ describe("chief-of-staff persona", () => {
organizationName: "Acme",
});
expect(persona).toContain("You are Ada, chief of staff for Acme.");
expect(persona).toContain("# Working with the user");
expect(persona).not.toContain("{{agentName}}");
expect(persona).not.toContain("{{organizationName}}");
});
@@ -113,6 +112,8 @@ describe("chief-of-staff persona", () => {
organizationName: "Acme",
});
expect(bundle.entryFile).toBe("AGENTS.md");
expect(bundle.files["AGENTS.md"]).toContain("You are Ada, chief of staff for Acme.");
expect(bundle.files).toEqual({
"AGENTS.md": await renderChiefOfStaffPersona({ agentName: "Ada", organizationName: "Acme" }),
});
});
});