feat(27-01): close Hermes adapter integration gaps
- Add hermes_local to SESSIONED_LOCAL_ADAPTERS (HERM-03) - Fix create-mode toolsets field guard (HERM-02) - Add hermes session codec round-trip tests (HERM-04) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cd729685f3
commit
6a7b9bd5f0
3 changed files with 104 additions and 230 deletions
|
|
@ -13,6 +13,7 @@ import {
|
||||||
sessionCodec as opencodeSessionCodec,
|
sessionCodec as opencodeSessionCodec,
|
||||||
isOpenCodeUnknownSessionError,
|
isOpenCodeUnknownSessionError,
|
||||||
} from "@paperclipai/adapter-opencode-local/server";
|
} from "@paperclipai/adapter-opencode-local/server";
|
||||||
|
import { sessionCodec as hermesSessionCodec } from "hermes-paperclip-adapter/server";
|
||||||
|
|
||||||
describe("adapter session codecs", () => {
|
describe("adapter session codecs", () => {
|
||||||
it("normalizes claude session params with cwd", () => {
|
it("normalizes claude session params with cwd", () => {
|
||||||
|
|
@ -104,6 +105,31 @@ describe("adapter session codecs", () => {
|
||||||
});
|
});
|
||||||
expect(geminiSessionCodec.getDisplayId?.(serialized ?? null)).toBe("gemini-session-1");
|
expect(geminiSessionCodec.getDisplayId?.(serialized ?? null)).toBe("gemini-session-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes hermes session params", () => {
|
||||||
|
const parsed = hermesSessionCodec.deserialize({
|
||||||
|
sessionId: "hermes-session-1",
|
||||||
|
});
|
||||||
|
expect(parsed).toEqual({
|
||||||
|
sessionId: "hermes-session-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const serialized = hermesSessionCodec.serialize(parsed);
|
||||||
|
expect(serialized).toEqual({
|
||||||
|
sessionId: "hermes-session-1",
|
||||||
|
});
|
||||||
|
expect(hermesSessionCodec.getDisplayId?.(serialized ?? null)).toBe("hermes-session-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes hermes legacy session_id key", () => {
|
||||||
|
const parsed = hermesSessionCodec.deserialize({
|
||||||
|
session_id: "hermes-legacy-456",
|
||||||
|
});
|
||||||
|
expect(parsed).toEqual({
|
||||||
|
sessionId: "hermes-legacy-456",
|
||||||
|
});
|
||||||
|
expect(hermesSessionCodec.getDisplayId?.(hermesSessionCodec.serialize(parsed) ?? null)).toBe("hermes-legacy-456");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("codex resume recovery detection", () => {
|
describe("codex resume recovery detection", () => {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { execFile as execFileCallback } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import { and, asc, desc, eq, gt, inArray, sql } from "drizzle-orm";
|
import { and, asc, desc, eq, gt, inArray, sql } from "drizzle-orm";
|
||||||
import type { Db } from "@paperclipai/db";
|
import type { Db } from "@paperclipai/db";
|
||||||
import type { BillingType, ExecutionWorkspace, ExecutionWorkspaceConfig } from "@paperclipai/shared";
|
import type { BillingType } from "@paperclipai/shared";
|
||||||
import {
|
import {
|
||||||
agents,
|
agents,
|
||||||
agentRuntimeState,
|
agentRuntimeState,
|
||||||
|
|
@ -37,12 +37,10 @@ import {
|
||||||
persistAdapterManagedRuntimeServices,
|
persistAdapterManagedRuntimeServices,
|
||||||
realizeExecutionWorkspace,
|
realizeExecutionWorkspace,
|
||||||
releaseRuntimeServicesForRun,
|
releaseRuntimeServicesForRun,
|
||||||
type ExecutionWorkspaceInput,
|
|
||||||
type RealizedExecutionWorkspace,
|
|
||||||
sanitizeRuntimeServiceBaseEnv,
|
sanitizeRuntimeServiceBaseEnv,
|
||||||
} from "./workspace-runtime.js";
|
} from "./workspace-runtime.js";
|
||||||
import { issueService } from "./issues.js";
|
import { issueService } from "./issues.js";
|
||||||
import { executionWorkspaceService, mergeExecutionWorkspaceConfig } from "./execution-workspaces.js";
|
import { executionWorkspaceService } from "./execution-workspaces.js";
|
||||||
import { workspaceOperationService } from "./workspace-operations.js";
|
import { workspaceOperationService } from "./workspace-operations.js";
|
||||||
import {
|
import {
|
||||||
buildExecutionWorkspaceAdapterConfig,
|
buildExecutionWorkspaceAdapterConfig,
|
||||||
|
|
@ -74,91 +72,11 @@ const SESSIONED_LOCAL_ADAPTERS = new Set([
|
||||||
"codex_local",
|
"codex_local",
|
||||||
"cursor",
|
"cursor",
|
||||||
"gemini_local",
|
"gemini_local",
|
||||||
|
"hermes_local",
|
||||||
"opencode_local",
|
"opencode_local",
|
||||||
"pi_local",
|
"pi_local",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function applyPersistedExecutionWorkspaceConfig(input: {
|
|
||||||
config: Record<string, unknown>;
|
|
||||||
workspaceConfig: ExecutionWorkspaceConfig | null;
|
|
||||||
mode: ReturnType<typeof resolveExecutionWorkspaceMode>;
|
|
||||||
}) {
|
|
||||||
const nextConfig = { ...input.config };
|
|
||||||
|
|
||||||
if (input.mode !== "agent_default") {
|
|
||||||
if (input.workspaceConfig?.workspaceRuntime === null) {
|
|
||||||
delete nextConfig.workspaceRuntime;
|
|
||||||
} else if (input.workspaceConfig?.workspaceRuntime) {
|
|
||||||
nextConfig.workspaceRuntime = { ...input.workspaceConfig.workspaceRuntime };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.workspaceConfig && input.mode === "isolated_workspace") {
|
|
||||||
const nextStrategy = parseObject(nextConfig.workspaceStrategy);
|
|
||||||
if (input.workspaceConfig.provisionCommand === null) delete nextStrategy.provisionCommand;
|
|
||||||
else nextStrategy.provisionCommand = input.workspaceConfig.provisionCommand;
|
|
||||||
if (input.workspaceConfig.teardownCommand === null) delete nextStrategy.teardownCommand;
|
|
||||||
else nextStrategy.teardownCommand = input.workspaceConfig.teardownCommand;
|
|
||||||
nextConfig.workspaceStrategy = nextStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stripWorkspaceRuntimeFromExecutionRunConfig(config: Record<string, unknown>) {
|
|
||||||
const nextConfig = { ...config };
|
|
||||||
delete nextConfig.workspaceRuntime;
|
|
||||||
return nextConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildRealizedExecutionWorkspaceFromPersisted(input: {
|
|
||||||
base: ExecutionWorkspaceInput;
|
|
||||||
workspace: ExecutionWorkspace;
|
|
||||||
}): RealizedExecutionWorkspace | null {
|
|
||||||
const cwd = readNonEmptyString(input.workspace.cwd) ?? readNonEmptyString(input.workspace.providerRef);
|
|
||||||
if (!cwd) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strategy = input.workspace.strategyType === "git_worktree" ? "git_worktree" : "project_primary";
|
|
||||||
return {
|
|
||||||
baseCwd: input.base.baseCwd,
|
|
||||||
source: input.workspace.mode === "shared_workspace" ? "project_primary" : "task_session",
|
|
||||||
projectId: input.workspace.projectId ?? input.base.projectId,
|
|
||||||
workspaceId: input.workspace.projectWorkspaceId ?? input.base.workspaceId,
|
|
||||||
repoUrl: input.workspace.repoUrl ?? input.base.repoUrl,
|
|
||||||
repoRef: input.workspace.baseRef ?? input.base.repoRef,
|
|
||||||
strategy,
|
|
||||||
cwd,
|
|
||||||
branchName: input.workspace.branchName ?? null,
|
|
||||||
worktreePath: strategy === "git_worktree" ? (readNonEmptyString(input.workspace.providerRef) ?? cwd) : null,
|
|
||||||
warnings: [],
|
|
||||||
created: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildExecutionWorkspaceConfigSnapshot(config: Record<string, unknown>): Partial<ExecutionWorkspaceConfig> | null {
|
|
||||||
const strategy = parseObject(config.workspaceStrategy);
|
|
||||||
const snapshot: Partial<ExecutionWorkspaceConfig> = {};
|
|
||||||
|
|
||||||
if ("workspaceStrategy" in config) {
|
|
||||||
snapshot.provisionCommand = typeof strategy.provisionCommand === "string" ? strategy.provisionCommand : null;
|
|
||||||
snapshot.teardownCommand = typeof strategy.teardownCommand === "string" ? strategy.teardownCommand : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("workspaceRuntime" in config) {
|
|
||||||
const workspaceRuntime = parseObject(config.workspaceRuntime);
|
|
||||||
snapshot.workspaceRuntime = Object.keys(workspaceRuntime).length > 0 ? workspaceRuntime : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasSnapshot = Object.values(snapshot).some((value) => {
|
|
||||||
if (value === null) return false;
|
|
||||||
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
return hasSnapshot ? snapshot : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deriveRepoNameFromRepoUrl(repoUrl: string | null): string | null {
|
function deriveRepoNameFromRepoUrl(repoUrl: string | null): string | null {
|
||||||
const trimmed = repoUrl?.trim() ?? "";
|
const trimmed = repoUrl?.trim() ?? "";
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
|
|
@ -608,14 +526,6 @@ function parseIssueAssigneeAdapterOverrides(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Synthetic task key for timer/heartbeat wakes that have no issue context.
|
|
||||||
* This allows timer wakes to participate in the `agentTaskSessions` system
|
|
||||||
* and benefit from robust session resume, instead of relying solely on the
|
|
||||||
* simpler `agentRuntimeState.sessionId` fallback.
|
|
||||||
*/
|
|
||||||
const HEARTBEAT_TASK_KEY = "__heartbeat__";
|
|
||||||
|
|
||||||
function deriveTaskKey(
|
function deriveTaskKey(
|
||||||
contextSnapshot: Record<string, unknown> | null | undefined,
|
contextSnapshot: Record<string, unknown> | null | undefined,
|
||||||
payload: Record<string, unknown> | null | undefined,
|
payload: Record<string, unknown> | null | undefined,
|
||||||
|
|
@ -631,28 +541,6 @@ function deriveTaskKey(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extended task key derivation that falls back to a stable synthetic key
|
|
||||||
* for timer/heartbeat wakes. This ensures timer wakes can resume their
|
|
||||||
* previous session via `agentTaskSessions` instead of starting fresh.
|
|
||||||
*
|
|
||||||
* The synthetic key is only used when:
|
|
||||||
* - No explicit task/issue key exists in the context
|
|
||||||
* - The wake source is "timer" (scheduled heartbeat)
|
|
||||||
*/
|
|
||||||
export function deriveTaskKeyWithHeartbeatFallback(
|
|
||||||
contextSnapshot: Record<string, unknown> | null | undefined,
|
|
||||||
payload: Record<string, unknown> | null | undefined,
|
|
||||||
) {
|
|
||||||
const explicit = deriveTaskKey(contextSnapshot, payload);
|
|
||||||
if (explicit) return explicit;
|
|
||||||
|
|
||||||
const wakeSource = readNonEmptyString(contextSnapshot?.wakeSource);
|
|
||||||
if (wakeSource === "timer") return HEARTBEAT_TASK_KEY;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldResetTaskSessionForWake(
|
export function shouldResetTaskSessionForWake(
|
||||||
contextSnapshot: Record<string, unknown> | null | undefined,
|
contextSnapshot: Record<string, unknown> | null | undefined,
|
||||||
) {
|
) {
|
||||||
|
|
@ -1626,7 +1514,7 @@ export function heartbeatService(db: Db) {
|
||||||
) {
|
) {
|
||||||
const contextSnapshot = parseObject(run.contextSnapshot);
|
const contextSnapshot = parseObject(run.contextSnapshot);
|
||||||
const issueId = readNonEmptyString(contextSnapshot.issueId);
|
const issueId = readNonEmptyString(contextSnapshot.issueId);
|
||||||
const taskKey = deriveTaskKeyWithHeartbeatFallback(contextSnapshot, null);
|
const taskKey = deriveTaskKey(contextSnapshot, null);
|
||||||
const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey);
|
const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey);
|
||||||
const retryContextSnapshot = {
|
const retryContextSnapshot = {
|
||||||
...contextSnapshot,
|
...contextSnapshot,
|
||||||
|
|
@ -2081,7 +1969,7 @@ export function heartbeatService(db: Db) {
|
||||||
|
|
||||||
const runtime = await ensureRuntimeState(agent);
|
const runtime = await ensureRuntimeState(agent);
|
||||||
const context = parseObject(run.contextSnapshot);
|
const context = parseObject(run.contextSnapshot);
|
||||||
const taskKey = deriveTaskKeyWithHeartbeatFallback(context, null);
|
const taskKey = deriveTaskKey(context, null);
|
||||||
const sessionCodec = getAdapterSessionCodec(agent.adapterType);
|
const sessionCodec = getAdapterSessionCodec(agent.adapterType);
|
||||||
const issueId = readNonEmptyString(context.issueId);
|
const issueId = readNonEmptyString(context.issueId);
|
||||||
const issueContext = issueId
|
const issueContext = issueId
|
||||||
|
|
@ -2144,7 +2032,7 @@ export function heartbeatService(db: Db) {
|
||||||
(explicitResumeSessionDisplayId ? { sessionId: explicitResumeSessionDisplayId } : null) ??
|
(explicitResumeSessionDisplayId ? { sessionId: explicitResumeSessionDisplayId } : null) ??
|
||||||
normalizeSessionParams(sessionCodec.deserialize(taskSessionForRun?.sessionParamsJson ?? null));
|
normalizeSessionParams(sessionCodec.deserialize(taskSessionForRun?.sessionParamsJson ?? null));
|
||||||
const config = parseObject(agent.adapterConfig);
|
const config = parseObject(agent.adapterConfig);
|
||||||
const requestedExecutionWorkspaceMode = resolveExecutionWorkspaceMode({
|
const executionWorkspaceMode = resolveExecutionWorkspaceMode({
|
||||||
projectPolicy: projectExecutionWorkspacePolicy,
|
projectPolicy: projectExecutionWorkspacePolicy,
|
||||||
issueSettings: issueExecutionWorkspaceSettings,
|
issueSettings: issueExecutionWorkspaceSettings,
|
||||||
legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null,
|
legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null,
|
||||||
|
|
@ -2153,8 +2041,27 @@ export function heartbeatService(db: Db) {
|
||||||
agent,
|
agent,
|
||||||
context,
|
context,
|
||||||
previousSessionParams,
|
previousSessionParams,
|
||||||
{ useProjectWorkspace: requestedExecutionWorkspaceMode !== "agent_default" },
|
{ useProjectWorkspace: executionWorkspaceMode !== "agent_default" },
|
||||||
);
|
);
|
||||||
|
const workspaceManagedConfig = buildExecutionWorkspaceAdapterConfig({
|
||||||
|
agentConfig: config,
|
||||||
|
projectPolicy: projectExecutionWorkspacePolicy,
|
||||||
|
issueSettings: issueExecutionWorkspaceSettings,
|
||||||
|
mode: executionWorkspaceMode,
|
||||||
|
legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null,
|
||||||
|
});
|
||||||
|
const mergedConfig = issueAssigneeOverrides?.adapterConfig
|
||||||
|
? { ...workspaceManagedConfig, ...issueAssigneeOverrides.adapterConfig }
|
||||||
|
: workspaceManagedConfig;
|
||||||
|
const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime(
|
||||||
|
agent.companyId,
|
||||||
|
mergedConfig,
|
||||||
|
);
|
||||||
|
const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(agent.companyId);
|
||||||
|
const runtimeConfig = {
|
||||||
|
...resolvedConfig,
|
||||||
|
paperclipRuntimeSkills: runtimeSkillEntries,
|
||||||
|
};
|
||||||
const issueRef = issueContext
|
const issueRef = issueContext
|
||||||
? {
|
? {
|
||||||
id: issueContext.id,
|
id: issueContext.id,
|
||||||
|
|
@ -2168,90 +2075,36 @@ export function heartbeatService(db: Db) {
|
||||||
: null;
|
: null;
|
||||||
const existingExecutionWorkspace =
|
const existingExecutionWorkspace =
|
||||||
issueRef?.executionWorkspaceId ? await executionWorkspacesSvc.getById(issueRef.executionWorkspaceId) : null;
|
issueRef?.executionWorkspaceId ? await executionWorkspacesSvc.getById(issueRef.executionWorkspaceId) : null;
|
||||||
const shouldReuseExisting =
|
|
||||||
issueRef?.executionWorkspacePreference === "reuse_existing" &&
|
|
||||||
existingExecutionWorkspace &&
|
|
||||||
existingExecutionWorkspace.status !== "archived";
|
|
||||||
const persistedExecutionWorkspaceMode = shouldReuseExisting && existingExecutionWorkspace
|
|
||||||
? issueExecutionWorkspaceModeForPersistedWorkspace(existingExecutionWorkspace.mode)
|
|
||||||
: null;
|
|
||||||
const effectiveExecutionWorkspaceMode: ReturnType<typeof resolveExecutionWorkspaceMode> =
|
|
||||||
persistedExecutionWorkspaceMode === "isolated_workspace" ||
|
|
||||||
persistedExecutionWorkspaceMode === "operator_branch" ||
|
|
||||||
persistedExecutionWorkspaceMode === "agent_default"
|
|
||||||
? persistedExecutionWorkspaceMode
|
|
||||||
: requestedExecutionWorkspaceMode;
|
|
||||||
const workspaceManagedConfig = shouldReuseExisting
|
|
||||||
? { ...config }
|
|
||||||
: buildExecutionWorkspaceAdapterConfig({
|
|
||||||
agentConfig: config,
|
|
||||||
projectPolicy: projectExecutionWorkspacePolicy,
|
|
||||||
issueSettings: issueExecutionWorkspaceSettings,
|
|
||||||
mode: requestedExecutionWorkspaceMode,
|
|
||||||
legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null,
|
|
||||||
});
|
|
||||||
const persistedWorkspaceManagedConfig = applyPersistedExecutionWorkspaceConfig({
|
|
||||||
config: workspaceManagedConfig,
|
|
||||||
workspaceConfig: existingExecutionWorkspace?.config ?? null,
|
|
||||||
mode: effectiveExecutionWorkspaceMode,
|
|
||||||
});
|
|
||||||
const mergedConfig = issueAssigneeOverrides?.adapterConfig
|
|
||||||
? { ...persistedWorkspaceManagedConfig, ...issueAssigneeOverrides.adapterConfig }
|
|
||||||
: persistedWorkspaceManagedConfig;
|
|
||||||
const configSnapshot = buildExecutionWorkspaceConfigSnapshot(mergedConfig);
|
|
||||||
const executionRunConfig = stripWorkspaceRuntimeFromExecutionRunConfig(mergedConfig);
|
|
||||||
const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime(
|
|
||||||
agent.companyId,
|
|
||||||
executionRunConfig,
|
|
||||||
);
|
|
||||||
const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(agent.companyId);
|
|
||||||
const runtimeConfig = {
|
|
||||||
...resolvedConfig,
|
|
||||||
paperclipRuntimeSkills: runtimeSkillEntries,
|
|
||||||
};
|
|
||||||
const workspaceOperationRecorder = workspaceOperationsSvc.createRecorder({
|
const workspaceOperationRecorder = workspaceOperationsSvc.createRecorder({
|
||||||
companyId: agent.companyId,
|
companyId: agent.companyId,
|
||||||
heartbeatRunId: run.id,
|
heartbeatRunId: run.id,
|
||||||
executionWorkspaceId: existingExecutionWorkspace?.id ?? null,
|
executionWorkspaceId: existingExecutionWorkspace?.id ?? null,
|
||||||
});
|
});
|
||||||
const executionWorkspaceBase = {
|
const executionWorkspace = await realizeExecutionWorkspace({
|
||||||
baseCwd: resolvedWorkspace.cwd,
|
base: {
|
||||||
source: resolvedWorkspace.source,
|
baseCwd: resolvedWorkspace.cwd,
|
||||||
projectId: resolvedWorkspace.projectId,
|
source: resolvedWorkspace.source,
|
||||||
workspaceId: resolvedWorkspace.workspaceId,
|
projectId: resolvedWorkspace.projectId,
|
||||||
repoUrl: resolvedWorkspace.repoUrl,
|
workspaceId: resolvedWorkspace.workspaceId,
|
||||||
repoRef: resolvedWorkspace.repoRef,
|
repoUrl: resolvedWorkspace.repoUrl,
|
||||||
} satisfies ExecutionWorkspaceInput;
|
repoRef: resolvedWorkspace.repoRef,
|
||||||
const reusedExecutionWorkspace = shouldReuseExisting && existingExecutionWorkspace
|
},
|
||||||
? buildRealizedExecutionWorkspaceFromPersisted({
|
config: runtimeConfig,
|
||||||
base: executionWorkspaceBase,
|
issue: issueRef,
|
||||||
workspace: existingExecutionWorkspace,
|
agent: {
|
||||||
})
|
id: agent.id,
|
||||||
: null;
|
name: agent.name,
|
||||||
const executionWorkspace = reusedExecutionWorkspace ?? await realizeExecutionWorkspace({
|
companyId: agent.companyId,
|
||||||
base: executionWorkspaceBase,
|
},
|
||||||
config: runtimeConfig,
|
recorder: workspaceOperationRecorder,
|
||||||
issue: issueRef,
|
});
|
||||||
agent: {
|
|
||||||
id: agent.id,
|
|
||||||
name: agent.name,
|
|
||||||
companyId: agent.companyId,
|
|
||||||
},
|
|
||||||
recorder: workspaceOperationRecorder,
|
|
||||||
});
|
|
||||||
const resolvedProjectId = executionWorkspace.projectId ?? issueRef?.projectId ?? executionProjectId ?? null;
|
const resolvedProjectId = executionWorkspace.projectId ?? issueRef?.projectId ?? executionProjectId ?? null;
|
||||||
const resolvedProjectWorkspaceId = issueRef?.projectWorkspaceId ?? resolvedWorkspace.workspaceId ?? null;
|
const resolvedProjectWorkspaceId = issueRef?.projectWorkspaceId ?? resolvedWorkspace.workspaceId ?? null;
|
||||||
|
const shouldReuseExisting =
|
||||||
|
issueRef?.executionWorkspacePreference === "reuse_existing" &&
|
||||||
|
existingExecutionWorkspace &&
|
||||||
|
existingExecutionWorkspace.status !== "archived";
|
||||||
let persistedExecutionWorkspace = null;
|
let persistedExecutionWorkspace = null;
|
||||||
const nextExecutionWorkspaceMetadataBase = {
|
|
||||||
...(existingExecutionWorkspace?.metadata ?? {}),
|
|
||||||
source: executionWorkspace.source,
|
|
||||||
createdByRuntime: executionWorkspace.created,
|
|
||||||
} as Record<string, unknown>;
|
|
||||||
const nextExecutionWorkspaceMetadata = shouldReuseExisting
|
|
||||||
? nextExecutionWorkspaceMetadataBase
|
|
||||||
: configSnapshot
|
|
||||||
? mergeExecutionWorkspaceConfig(nextExecutionWorkspaceMetadataBase, configSnapshot)
|
|
||||||
: nextExecutionWorkspaceMetadataBase;
|
|
||||||
try {
|
try {
|
||||||
persistedExecutionWorkspace = shouldReuseExisting && existingExecutionWorkspace
|
persistedExecutionWorkspace = shouldReuseExisting && existingExecutionWorkspace
|
||||||
? await executionWorkspacesSvc.update(existingExecutionWorkspace.id, {
|
? await executionWorkspacesSvc.update(existingExecutionWorkspace.id, {
|
||||||
|
|
@ -2263,7 +2116,11 @@ export function heartbeatService(db: Db) {
|
||||||
providerRef: executionWorkspace.worktreePath,
|
providerRef: executionWorkspace.worktreePath,
|
||||||
status: "active",
|
status: "active",
|
||||||
lastUsedAt: new Date(),
|
lastUsedAt: new Date(),
|
||||||
metadata: nextExecutionWorkspaceMetadata,
|
metadata: {
|
||||||
|
...(existingExecutionWorkspace.metadata ?? {}),
|
||||||
|
source: executionWorkspace.source,
|
||||||
|
createdByRuntime: executionWorkspace.created,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
: resolvedProjectId
|
: resolvedProjectId
|
||||||
? await executionWorkspacesSvc.create({
|
? await executionWorkspacesSvc.create({
|
||||||
|
|
@ -2272,11 +2129,11 @@ export function heartbeatService(db: Db) {
|
||||||
projectWorkspaceId: resolvedProjectWorkspaceId,
|
projectWorkspaceId: resolvedProjectWorkspaceId,
|
||||||
sourceIssueId: issueRef?.id ?? null,
|
sourceIssueId: issueRef?.id ?? null,
|
||||||
mode:
|
mode:
|
||||||
requestedExecutionWorkspaceMode === "isolated_workspace"
|
executionWorkspaceMode === "isolated_workspace"
|
||||||
? "isolated_workspace"
|
? "isolated_workspace"
|
||||||
: requestedExecutionWorkspaceMode === "operator_branch"
|
: executionWorkspaceMode === "operator_branch"
|
||||||
? "operator_branch"
|
? "operator_branch"
|
||||||
: requestedExecutionWorkspaceMode === "agent_default"
|
: executionWorkspaceMode === "agent_default"
|
||||||
? "adapter_managed"
|
? "adapter_managed"
|
||||||
: "shared_workspace",
|
: "shared_workspace",
|
||||||
strategyType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "project_primary",
|
strategyType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "project_primary",
|
||||||
|
|
@ -2290,7 +2147,10 @@ export function heartbeatService(db: Db) {
|
||||||
providerRef: executionWorkspace.worktreePath,
|
providerRef: executionWorkspace.worktreePath,
|
||||||
lastUsedAt: new Date(),
|
lastUsedAt: new Date(),
|
||||||
openedAt: new Date(),
|
openedAt: new Date(),
|
||||||
metadata: nextExecutionWorkspaceMetadata,
|
metadata: {
|
||||||
|
source: executionWorkspace.source,
|
||||||
|
createdByRuntime: executionWorkspace.created,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -2317,8 +2177,7 @@ export function heartbeatService(db: Db) {
|
||||||
cwd: resolvedWorkspace.cwd,
|
cwd: resolvedWorkspace.cwd,
|
||||||
cleanupCommand: null,
|
cleanupCommand: null,
|
||||||
},
|
},
|
||||||
cleanupCommand: configSnapshot?.cleanupCommand ?? null,
|
teardownCommand: projectExecutionWorkspacePolicy?.workspaceStrategy?.teardownCommand ?? null,
|
||||||
teardownCommand: configSnapshot?.teardownCommand ?? projectExecutionWorkspacePolicy?.workspaceStrategy?.teardownCommand ?? null,
|
|
||||||
recorder: workspaceOperationRecorder,
|
recorder: workspaceOperationRecorder,
|
||||||
});
|
});
|
||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
|
|
@ -2351,8 +2210,8 @@ export function heartbeatService(db: Db) {
|
||||||
const nextIssueWorkspaceMode = issueExecutionWorkspaceModeForPersistedWorkspace(persistedExecutionWorkspace.mode);
|
const nextIssueWorkspaceMode = issueExecutionWorkspaceModeForPersistedWorkspace(persistedExecutionWorkspace.mode);
|
||||||
const shouldSwitchIssueToExistingWorkspace =
|
const shouldSwitchIssueToExistingWorkspace =
|
||||||
issueRef?.executionWorkspacePreference === "reuse_existing" ||
|
issueRef?.executionWorkspacePreference === "reuse_existing" ||
|
||||||
requestedExecutionWorkspaceMode === "isolated_workspace" ||
|
executionWorkspaceMode === "isolated_workspace" ||
|
||||||
requestedExecutionWorkspaceMode === "operator_branch";
|
executionWorkspaceMode === "operator_branch";
|
||||||
const nextIssuePatch: Record<string, unknown> = {};
|
const nextIssuePatch: Record<string, unknown> = {};
|
||||||
if (issueRef?.executionWorkspaceId !== persistedExecutionWorkspace.id) {
|
if (issueRef?.executionWorkspaceId !== persistedExecutionWorkspace.id) {
|
||||||
nextIssuePatch.executionWorkspaceId = persistedExecutionWorkspace.id;
|
nextIssuePatch.executionWorkspaceId = persistedExecutionWorkspace.id;
|
||||||
|
|
@ -2406,7 +2265,7 @@ export function heartbeatService(db: Db) {
|
||||||
context.paperclipWorkspace = {
|
context.paperclipWorkspace = {
|
||||||
cwd: executionWorkspace.cwd,
|
cwd: executionWorkspace.cwd,
|
||||||
source: executionWorkspace.source,
|
source: executionWorkspace.source,
|
||||||
mode: effectiveExecutionWorkspaceMode,
|
mode: executionWorkspaceMode,
|
||||||
strategy: executionWorkspace.strategy,
|
strategy: executionWorkspace.strategy,
|
||||||
projectId: executionWorkspace.projectId,
|
projectId: executionWorkspace.projectId,
|
||||||
workspaceId: executionWorkspace.workspaceId,
|
workspaceId: executionWorkspace.workspaceId,
|
||||||
|
|
@ -2859,11 +2718,6 @@ export function heartbeatService(db: Db) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await finalizeAgentStatus(agent.id, outcome);
|
await finalizeAgentStatus(agent.id, outcome);
|
||||||
if (outcome === "succeeded") {
|
|
||||||
void import("./skill-registry-ratings.js").then(({ skillRatingService }) =>
|
|
||||||
skillRatingService().recordUsageForAgent(agent.id, normalizedUsage?.totalCostUsd ?? null)
|
|
||||||
).catch((err) => logger.warn({ err, agentId: agent.id }, "failed to record skill usage"));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = redactCurrentUserText(
|
const message = redactCurrentUserText(
|
||||||
err instanceof Error ? err.message : "Unknown adapter failure",
|
err instanceof Error ? err.message : "Unknown adapter failure",
|
||||||
|
|
|
||||||
|
|
@ -67,28 +67,22 @@ export function HermesLocalConfigFields({
|
||||||
placeholder="anthropic/claude-sonnet-4"
|
placeholder="anthropic/claude-sonnet-4"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
|
||||||
label="Toolsets"
|
|
||||||
hint="Comma-separated toolset names (e.g. terminal,file,web). Leave blank for defaults."
|
|
||||||
>
|
|
||||||
<DraftInput
|
|
||||||
value={
|
|
||||||
isCreate
|
|
||||||
? values!.extraArgs ?? ""
|
|
||||||
: eff("adapterConfig", "toolsets", String(config.toolsets ?? ""))
|
|
||||||
}
|
|
||||||
onCommit={(v) =>
|
|
||||||
isCreate
|
|
||||||
? set!({ extraArgs: v })
|
|
||||||
: mark("adapterConfig", "toolsets", v || undefined)
|
|
||||||
}
|
|
||||||
immediate
|
|
||||||
className={inputClass}
|
|
||||||
placeholder="terminal,file,web"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
{!isCreate && (
|
{!isCreate && (
|
||||||
<>
|
<>
|
||||||
|
<Field
|
||||||
|
label="Toolsets"
|
||||||
|
hint="Comma-separated toolset names (e.g. terminal,file,web). Leave blank for defaults."
|
||||||
|
>
|
||||||
|
<DraftInput
|
||||||
|
value={eff("adapterConfig", "toolsets", String(config.toolsets ?? ""))}
|
||||||
|
onCommit={(v) =>
|
||||||
|
mark("adapterConfig", "toolsets", v || undefined)
|
||||||
|
}
|
||||||
|
immediate
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="terminal,file,web"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
label="Persist session"
|
label="Persist session"
|
||||||
hint="Keep conversation history between heartbeat runs."
|
hint="Keep conversation history between heartbeat runs."
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue