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,
|
||||
isOpenCodeUnknownSessionError,
|
||||
} from "@paperclipai/adapter-opencode-local/server";
|
||||
import { sessionCodec as hermesSessionCodec } from "hermes-paperclip-adapter/server";
|
||||
|
||||
describe("adapter session codecs", () => {
|
||||
it("normalizes claude session params with cwd", () => {
|
||||
|
|
@ -104,6 +105,31 @@ describe("adapter session codecs", () => {
|
|||
});
|
||||
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", () => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { execFile as execFileCallback } from "node:child_process";
|
|||
import { promisify } from "node:util";
|
||||
import { and, asc, desc, eq, gt, inArray, sql } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import type { BillingType, ExecutionWorkspace, ExecutionWorkspaceConfig } from "@paperclipai/shared";
|
||||
import type { BillingType } from "@paperclipai/shared";
|
||||
import {
|
||||
agents,
|
||||
agentRuntimeState,
|
||||
|
|
@ -37,12 +37,10 @@ import {
|
|||
persistAdapterManagedRuntimeServices,
|
||||
realizeExecutionWorkspace,
|
||||
releaseRuntimeServicesForRun,
|
||||
type ExecutionWorkspaceInput,
|
||||
type RealizedExecutionWorkspace,
|
||||
sanitizeRuntimeServiceBaseEnv,
|
||||
} from "./workspace-runtime.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 {
|
||||
buildExecutionWorkspaceAdapterConfig,
|
||||
|
|
@ -74,91 +72,11 @@ const SESSIONED_LOCAL_ADAPTERS = new Set([
|
|||
"codex_local",
|
||||
"cursor",
|
||||
"gemini_local",
|
||||
"hermes_local",
|
||||
"opencode_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 {
|
||||
const trimmed = repoUrl?.trim() ?? "";
|
||||
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(
|
||||
contextSnapshot: 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(
|
||||
contextSnapshot: Record<string, unknown> | null | undefined,
|
||||
) {
|
||||
|
|
@ -1626,7 +1514,7 @@ export function heartbeatService(db: Db) {
|
|||
) {
|
||||
const contextSnapshot = parseObject(run.contextSnapshot);
|
||||
const issueId = readNonEmptyString(contextSnapshot.issueId);
|
||||
const taskKey = deriveTaskKeyWithHeartbeatFallback(contextSnapshot, null);
|
||||
const taskKey = deriveTaskKey(contextSnapshot, null);
|
||||
const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey);
|
||||
const retryContextSnapshot = {
|
||||
...contextSnapshot,
|
||||
|
|
@ -2081,7 +1969,7 @@ export function heartbeatService(db: Db) {
|
|||
|
||||
const runtime = await ensureRuntimeState(agent);
|
||||
const context = parseObject(run.contextSnapshot);
|
||||
const taskKey = deriveTaskKeyWithHeartbeatFallback(context, null);
|
||||
const taskKey = deriveTaskKey(context, null);
|
||||
const sessionCodec = getAdapterSessionCodec(agent.adapterType);
|
||||
const issueId = readNonEmptyString(context.issueId);
|
||||
const issueContext = issueId
|
||||
|
|
@ -2144,7 +2032,7 @@ export function heartbeatService(db: Db) {
|
|||
(explicitResumeSessionDisplayId ? { sessionId: explicitResumeSessionDisplayId } : null) ??
|
||||
normalizeSessionParams(sessionCodec.deserialize(taskSessionForRun?.sessionParamsJson ?? null));
|
||||
const config = parseObject(agent.adapterConfig);
|
||||
const requestedExecutionWorkspaceMode = resolveExecutionWorkspaceMode({
|
||||
const executionWorkspaceMode = resolveExecutionWorkspaceMode({
|
||||
projectPolicy: projectExecutionWorkspacePolicy,
|
||||
issueSettings: issueExecutionWorkspaceSettings,
|
||||
legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null,
|
||||
|
|
@ -2153,8 +2041,27 @@ export function heartbeatService(db: Db) {
|
|||
agent,
|
||||
context,
|
||||
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
|
||||
? {
|
||||
id: issueContext.id,
|
||||
|
|
@ -2168,90 +2075,36 @@ export function heartbeatService(db: Db) {
|
|||
: null;
|
||||
const existingExecutionWorkspace =
|
||||
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({
|
||||
companyId: agent.companyId,
|
||||
heartbeatRunId: run.id,
|
||||
executionWorkspaceId: existingExecutionWorkspace?.id ?? null,
|
||||
});
|
||||
const executionWorkspaceBase = {
|
||||
baseCwd: resolvedWorkspace.cwd,
|
||||
source: resolvedWorkspace.source,
|
||||
projectId: resolvedWorkspace.projectId,
|
||||
workspaceId: resolvedWorkspace.workspaceId,
|
||||
repoUrl: resolvedWorkspace.repoUrl,
|
||||
repoRef: resolvedWorkspace.repoRef,
|
||||
} satisfies ExecutionWorkspaceInput;
|
||||
const reusedExecutionWorkspace = shouldReuseExisting && existingExecutionWorkspace
|
||||
? buildRealizedExecutionWorkspaceFromPersisted({
|
||||
base: executionWorkspaceBase,
|
||||
workspace: existingExecutionWorkspace,
|
||||
})
|
||||
: null;
|
||||
const executionWorkspace = reusedExecutionWorkspace ?? await realizeExecutionWorkspace({
|
||||
base: executionWorkspaceBase,
|
||||
config: runtimeConfig,
|
||||
issue: issueRef,
|
||||
agent: {
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
companyId: agent.companyId,
|
||||
},
|
||||
recorder: workspaceOperationRecorder,
|
||||
});
|
||||
const executionWorkspace = await realizeExecutionWorkspace({
|
||||
base: {
|
||||
baseCwd: resolvedWorkspace.cwd,
|
||||
source: resolvedWorkspace.source,
|
||||
projectId: resolvedWorkspace.projectId,
|
||||
workspaceId: resolvedWorkspace.workspaceId,
|
||||
repoUrl: resolvedWorkspace.repoUrl,
|
||||
repoRef: resolvedWorkspace.repoRef,
|
||||
},
|
||||
config: runtimeConfig,
|
||||
issue: issueRef,
|
||||
agent: {
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
companyId: agent.companyId,
|
||||
},
|
||||
recorder: workspaceOperationRecorder,
|
||||
});
|
||||
const resolvedProjectId = executionWorkspace.projectId ?? issueRef?.projectId ?? executionProjectId ?? null;
|
||||
const resolvedProjectWorkspaceId = issueRef?.projectWorkspaceId ?? resolvedWorkspace.workspaceId ?? null;
|
||||
const shouldReuseExisting =
|
||||
issueRef?.executionWorkspacePreference === "reuse_existing" &&
|
||||
existingExecutionWorkspace &&
|
||||
existingExecutionWorkspace.status !== "archived";
|
||||
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 {
|
||||
persistedExecutionWorkspace = shouldReuseExisting && existingExecutionWorkspace
|
||||
? await executionWorkspacesSvc.update(existingExecutionWorkspace.id, {
|
||||
|
|
@ -2263,7 +2116,11 @@ export function heartbeatService(db: Db) {
|
|||
providerRef: executionWorkspace.worktreePath,
|
||||
status: "active",
|
||||
lastUsedAt: new Date(),
|
||||
metadata: nextExecutionWorkspaceMetadata,
|
||||
metadata: {
|
||||
...(existingExecutionWorkspace.metadata ?? {}),
|
||||
source: executionWorkspace.source,
|
||||
createdByRuntime: executionWorkspace.created,
|
||||
},
|
||||
})
|
||||
: resolvedProjectId
|
||||
? await executionWorkspacesSvc.create({
|
||||
|
|
@ -2272,11 +2129,11 @@ export function heartbeatService(db: Db) {
|
|||
projectWorkspaceId: resolvedProjectWorkspaceId,
|
||||
sourceIssueId: issueRef?.id ?? null,
|
||||
mode:
|
||||
requestedExecutionWorkspaceMode === "isolated_workspace"
|
||||
executionWorkspaceMode === "isolated_workspace"
|
||||
? "isolated_workspace"
|
||||
: requestedExecutionWorkspaceMode === "operator_branch"
|
||||
: executionWorkspaceMode === "operator_branch"
|
||||
? "operator_branch"
|
||||
: requestedExecutionWorkspaceMode === "agent_default"
|
||||
: executionWorkspaceMode === "agent_default"
|
||||
? "adapter_managed"
|
||||
: "shared_workspace",
|
||||
strategyType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "project_primary",
|
||||
|
|
@ -2290,7 +2147,10 @@ export function heartbeatService(db: Db) {
|
|||
providerRef: executionWorkspace.worktreePath,
|
||||
lastUsedAt: new Date(),
|
||||
openedAt: new Date(),
|
||||
metadata: nextExecutionWorkspaceMetadata,
|
||||
metadata: {
|
||||
source: executionWorkspace.source,
|
||||
createdByRuntime: executionWorkspace.created,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
} catch (error) {
|
||||
|
|
@ -2317,8 +2177,7 @@ export function heartbeatService(db: Db) {
|
|||
cwd: resolvedWorkspace.cwd,
|
||||
cleanupCommand: null,
|
||||
},
|
||||
cleanupCommand: configSnapshot?.cleanupCommand ?? null,
|
||||
teardownCommand: configSnapshot?.teardownCommand ?? projectExecutionWorkspacePolicy?.workspaceStrategy?.teardownCommand ?? null,
|
||||
teardownCommand: projectExecutionWorkspacePolicy?.workspaceStrategy?.teardownCommand ?? null,
|
||||
recorder: workspaceOperationRecorder,
|
||||
});
|
||||
} catch (cleanupError) {
|
||||
|
|
@ -2351,8 +2210,8 @@ export function heartbeatService(db: Db) {
|
|||
const nextIssueWorkspaceMode = issueExecutionWorkspaceModeForPersistedWorkspace(persistedExecutionWorkspace.mode);
|
||||
const shouldSwitchIssueToExistingWorkspace =
|
||||
issueRef?.executionWorkspacePreference === "reuse_existing" ||
|
||||
requestedExecutionWorkspaceMode === "isolated_workspace" ||
|
||||
requestedExecutionWorkspaceMode === "operator_branch";
|
||||
executionWorkspaceMode === "isolated_workspace" ||
|
||||
executionWorkspaceMode === "operator_branch";
|
||||
const nextIssuePatch: Record<string, unknown> = {};
|
||||
if (issueRef?.executionWorkspaceId !== persistedExecutionWorkspace.id) {
|
||||
nextIssuePatch.executionWorkspaceId = persistedExecutionWorkspace.id;
|
||||
|
|
@ -2406,7 +2265,7 @@ export function heartbeatService(db: Db) {
|
|||
context.paperclipWorkspace = {
|
||||
cwd: executionWorkspace.cwd,
|
||||
source: executionWorkspace.source,
|
||||
mode: effectiveExecutionWorkspaceMode,
|
||||
mode: executionWorkspaceMode,
|
||||
strategy: executionWorkspace.strategy,
|
||||
projectId: executionWorkspace.projectId,
|
||||
workspaceId: executionWorkspace.workspaceId,
|
||||
|
|
@ -2859,11 +2718,6 @@ export function heartbeatService(db: Db) {
|
|||
}
|
||||
}
|
||||
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) {
|
||||
const message = redactCurrentUserText(
|
||||
err instanceof Error ? err.message : "Unknown adapter failure",
|
||||
|
|
|
|||
|
|
@ -67,28 +67,22 @@ export function HermesLocalConfigFields({
|
|||
placeholder="anthropic/claude-sonnet-4"
|
||||
/>
|
||||
</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 && (
|
||||
<>
|
||||
<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
|
||||
label="Persist session"
|
||||
hint="Keep conversation history between heartbeat runs."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue