mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
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:
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({
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
|
||||
@@ -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" }),
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user