feat: resolve agent workspace from session/project/fallback
Heartbeat service resolves cwd from task session, project primary workspace, or agent home directory (~/.paperclip/instances/.../workspaces/). Adapters receive workspace context and forward it as env vars and session params. cwd is now optional in adapter config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -84,6 +84,14 @@ Configure storage provider/settings:
|
|||||||
pnpm paperclip configure --section storage
|
pnpm paperclip configure --section storage
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Default Agent Workspaces
|
||||||
|
|
||||||
|
When a local agent run has no resolved project/session workspace, Paperclip falls back to an agent home workspace under the instance root:
|
||||||
|
|
||||||
|
- `~/.paperclip/instances/default/workspaces/<agent-id>`
|
||||||
|
|
||||||
|
This path honors `PAPERCLIP_HOME` and `PAPERCLIP_INSTANCE_ID` in non-default setups.
|
||||||
|
|
||||||
## Quick Health Checks
|
## Quick Health Checks
|
||||||
|
|
||||||
In another terminal:
|
In another terminal:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const agentConfigurationDoc = `# claude_local agent configuration
|
|||||||
Adapter: claude_local
|
Adapter: claude_local
|
||||||
|
|
||||||
Core fields:
|
Core fields:
|
||||||
- cwd (string, required): absolute working directory for the agent process
|
- cwd (string, optional): default absolute working directory fallback for the agent process
|
||||||
- model (string, optional): Claude model id
|
- model (string, optional): Claude model id
|
||||||
- effort (string, optional): reasoning effort passed via --effort (low|medium|high)
|
- effort (string, optional): reasoning effort passed via --effort (low|medium|high)
|
||||||
- promptTemplate (string, optional): run prompt template
|
- promptTemplate (string, optional): run prompt template
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ interface ClaudeExecutionInput {
|
|||||||
interface ClaudeRuntimeConfig {
|
interface ClaudeRuntimeConfig {
|
||||||
command: string;
|
command: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
workspaceId: string | null;
|
||||||
|
workspaceRepoUrl: string | null;
|
||||||
|
workspaceRepoRef: string | null;
|
||||||
env: Record<string, string>;
|
env: Record<string, string>;
|
||||||
timeoutSec: number;
|
timeoutSec: number;
|
||||||
graceSec: number;
|
graceSec: number;
|
||||||
@@ -87,7 +90,13 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
|||||||
const { runId, agent, config, context, authToken } = input;
|
const { runId, agent, config, context, authToken } = input;
|
||||||
|
|
||||||
const command = asString(config.command, "claude");
|
const command = asString(config.command, "claude");
|
||||||
const cwd = asString(config.cwd, process.cwd());
|
const workspaceContext = parseObject(context.paperclipWorkspace);
|
||||||
|
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||||
|
const workspaceSource = asString(workspaceContext.source, "");
|
||||||
|
const workspaceId = asString(workspaceContext.workspaceId, "") || null;
|
||||||
|
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "") || null;
|
||||||
|
const workspaceRepoRef = asString(workspaceContext.repoRef, "") || null;
|
||||||
|
const cwd = workspaceCwd || asString(config.cwd, process.cwd());
|
||||||
await ensureAbsoluteDirectory(cwd);
|
await ensureAbsoluteDirectory(cwd);
|
||||||
|
|
||||||
const envConfig = parseObject(config.env);
|
const envConfig = parseObject(config.env);
|
||||||
@@ -138,6 +147,21 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
|||||||
if (linkedIssueIds.length > 0) {
|
if (linkedIssueIds.length > 0) {
|
||||||
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||||
}
|
}
|
||||||
|
if (workspaceCwd) {
|
||||||
|
env.PAPERCLIP_WORKSPACE_CWD = workspaceCwd;
|
||||||
|
}
|
||||||
|
if (workspaceSource) {
|
||||||
|
env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
|
||||||
|
}
|
||||||
|
if (workspaceId) {
|
||||||
|
env.PAPERCLIP_WORKSPACE_ID = workspaceId;
|
||||||
|
}
|
||||||
|
if (workspaceRepoUrl) {
|
||||||
|
env.PAPERCLIP_WORKSPACE_REPO_URL = workspaceRepoUrl;
|
||||||
|
}
|
||||||
|
if (workspaceRepoRef) {
|
||||||
|
env.PAPERCLIP_WORKSPACE_REPO_REF = workspaceRepoRef;
|
||||||
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(envConfig)) {
|
for (const [key, value] of Object.entries(envConfig)) {
|
||||||
if (typeof value === "string") env[key] = value;
|
if (typeof value === "string") env[key] = value;
|
||||||
@@ -161,6 +185,9 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
|||||||
return {
|
return {
|
||||||
command,
|
command,
|
||||||
cwd,
|
cwd,
|
||||||
|
workspaceId,
|
||||||
|
workspaceRepoUrl,
|
||||||
|
workspaceRepoRef,
|
||||||
env,
|
env,
|
||||||
timeoutSec,
|
timeoutSec,
|
||||||
graceSec,
|
graceSec,
|
||||||
@@ -225,7 +252,17 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
context,
|
context,
|
||||||
authToken,
|
authToken,
|
||||||
});
|
});
|
||||||
const { command, cwd, env, timeoutSec, graceSec, extraArgs } = runtimeConfig;
|
const {
|
||||||
|
command,
|
||||||
|
cwd,
|
||||||
|
workspaceId,
|
||||||
|
workspaceRepoUrl,
|
||||||
|
workspaceRepoRef,
|
||||||
|
env,
|
||||||
|
timeoutSec,
|
||||||
|
graceSec,
|
||||||
|
extraArgs,
|
||||||
|
} = runtimeConfig;
|
||||||
const skillsDir = await buildSkillsDir();
|
const skillsDir = await buildSkillsDir();
|
||||||
|
|
||||||
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
||||||
@@ -372,7 +409,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
parsedStream.sessionId ??
|
parsedStream.sessionId ??
|
||||||
(asString(parsed.session_id, opts.fallbackSessionId ?? "") || opts.fallbackSessionId);
|
(asString(parsed.session_id, opts.fallbackSessionId ?? "") || opts.fallbackSessionId);
|
||||||
const resolvedSessionParams = resolvedSessionId
|
const resolvedSessionParams = resolvedSessionId
|
||||||
? ({ sessionId: resolvedSessionId, cwd } as Record<string, unknown>)
|
? ({
|
||||||
|
sessionId: resolvedSessionId,
|
||||||
|
cwd,
|
||||||
|
...(workspaceId ? { workspaceId } : {}),
|
||||||
|
...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
|
||||||
|
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
|
||||||
|
} as Record<string, unknown>)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -17,7 +17,16 @@ export const sessionCodec: AdapterSessionCodec = {
|
|||||||
readNonEmptyString(record.cwd) ??
|
readNonEmptyString(record.cwd) ??
|
||||||
readNonEmptyString(record.workdir) ??
|
readNonEmptyString(record.workdir) ??
|
||||||
readNonEmptyString(record.folder);
|
readNonEmptyString(record.folder);
|
||||||
return cwd ? { sessionId, cwd } : { sessionId };
|
const workspaceId = readNonEmptyString(record.workspaceId) ?? readNonEmptyString(record.workspace_id);
|
||||||
|
const repoUrl = readNonEmptyString(record.repoUrl) ?? readNonEmptyString(record.repo_url);
|
||||||
|
const repoRef = readNonEmptyString(record.repoRef) ?? readNonEmptyString(record.repo_ref);
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
...(cwd ? { cwd } : {}),
|
||||||
|
...(workspaceId ? { workspaceId } : {}),
|
||||||
|
...(repoUrl ? { repoUrl } : {}),
|
||||||
|
...(repoRef ? { repoRef } : {}),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
serialize(params: Record<string, unknown> | null) {
|
serialize(params: Record<string, unknown> | null) {
|
||||||
if (!params) return null;
|
if (!params) return null;
|
||||||
@@ -27,7 +36,16 @@ export const sessionCodec: AdapterSessionCodec = {
|
|||||||
readNonEmptyString(params.cwd) ??
|
readNonEmptyString(params.cwd) ??
|
||||||
readNonEmptyString(params.workdir) ??
|
readNonEmptyString(params.workdir) ??
|
||||||
readNonEmptyString(params.folder);
|
readNonEmptyString(params.folder);
|
||||||
return cwd ? { sessionId, cwd } : { sessionId };
|
const workspaceId = readNonEmptyString(params.workspaceId) ?? readNonEmptyString(params.workspace_id);
|
||||||
|
const repoUrl = readNonEmptyString(params.repoUrl) ?? readNonEmptyString(params.repo_url);
|
||||||
|
const repoRef = readNonEmptyString(params.repoRef) ?? readNonEmptyString(params.repo_ref);
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
...(cwd ? { cwd } : {}),
|
||||||
|
...(workspaceId ? { workspaceId } : {}),
|
||||||
|
...(repoUrl ? { repoUrl } : {}),
|
||||||
|
...(repoRef ? { repoRef } : {}),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
getDisplayId(params: Record<string, unknown> | null) {
|
getDisplayId(params: Record<string, unknown> | null) {
|
||||||
if (!params) return null;
|
if (!params) return null;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const agentConfigurationDoc = `# codex_local agent configuration
|
|||||||
Adapter: codex_local
|
Adapter: codex_local
|
||||||
|
|
||||||
Core fields:
|
Core fields:
|
||||||
- cwd (string, required): absolute working directory for the agent process
|
- cwd (string, optional): default absolute working directory fallback for the agent process
|
||||||
- model (string, optional): Codex model id
|
- model (string, optional): Codex model id
|
||||||
- modelReasoningEffort (string, optional): reasoning effort override (minimal|low|medium|high) passed via -c model_reasoning_effort=...
|
- modelReasoningEffort (string, optional): reasoning effort override (minimal|low|medium|high) passed via -c model_reasoning_effort=...
|
||||||
- promptTemplate (string, optional): run prompt template
|
- promptTemplate (string, optional): run prompt template
|
||||||
|
|||||||
@@ -108,7 +108,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
asBoolean(config.dangerouslyBypassSandbox, false),
|
asBoolean(config.dangerouslyBypassSandbox, false),
|
||||||
);
|
);
|
||||||
|
|
||||||
const cwd = asString(config.cwd, process.cwd());
|
const workspaceContext = parseObject(context.paperclipWorkspace);
|
||||||
|
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||||
|
const workspaceSource = asString(workspaceContext.source, "");
|
||||||
|
const workspaceId = asString(workspaceContext.workspaceId, "");
|
||||||
|
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
|
||||||
|
const workspaceRepoRef = asString(workspaceContext.repoRef, "");
|
||||||
|
const cwd = workspaceCwd || asString(config.cwd, process.cwd());
|
||||||
await ensureAbsoluteDirectory(cwd);
|
await ensureAbsoluteDirectory(cwd);
|
||||||
await ensureCodexSkillsInjected(onLog);
|
await ensureCodexSkillsInjected(onLog);
|
||||||
const envConfig = parseObject(config.env);
|
const envConfig = parseObject(config.env);
|
||||||
@@ -157,6 +163,21 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
if (linkedIssueIds.length > 0) {
|
if (linkedIssueIds.length > 0) {
|
||||||
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||||
}
|
}
|
||||||
|
if (workspaceCwd) {
|
||||||
|
env.PAPERCLIP_WORKSPACE_CWD = workspaceCwd;
|
||||||
|
}
|
||||||
|
if (workspaceSource) {
|
||||||
|
env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
|
||||||
|
}
|
||||||
|
if (workspaceId) {
|
||||||
|
env.PAPERCLIP_WORKSPACE_ID = workspaceId;
|
||||||
|
}
|
||||||
|
if (workspaceRepoUrl) {
|
||||||
|
env.PAPERCLIP_WORKSPACE_REPO_URL = workspaceRepoUrl;
|
||||||
|
}
|
||||||
|
if (workspaceRepoRef) {
|
||||||
|
env.PAPERCLIP_WORKSPACE_REPO_REF = workspaceRepoRef;
|
||||||
|
}
|
||||||
for (const [k, v] of Object.entries(envConfig)) {
|
for (const [k, v] of Object.entries(envConfig)) {
|
||||||
if (typeof v === "string") env[k] = v;
|
if (typeof v === "string") env[k] = v;
|
||||||
}
|
}
|
||||||
@@ -270,7 +291,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
|
|
||||||
const resolvedSessionId = attempt.parsed.sessionId ?? runtimeSessionId ?? runtime.sessionId ?? null;
|
const resolvedSessionId = attempt.parsed.sessionId ?? runtimeSessionId ?? runtime.sessionId ?? null;
|
||||||
const resolvedSessionParams = resolvedSessionId
|
const resolvedSessionParams = resolvedSessionId
|
||||||
? ({ sessionId: resolvedSessionId, cwd } as Record<string, unknown>)
|
? ({
|
||||||
|
sessionId: resolvedSessionId,
|
||||||
|
cwd,
|
||||||
|
...(workspaceId ? { workspaceId } : {}),
|
||||||
|
...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
|
||||||
|
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
|
||||||
|
} as Record<string, unknown>)
|
||||||
: null;
|
: null;
|
||||||
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
|
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
|
||||||
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
||||||
|
|||||||
@@ -17,7 +17,16 @@ export const sessionCodec: AdapterSessionCodec = {
|
|||||||
readNonEmptyString(record.cwd) ??
|
readNonEmptyString(record.cwd) ??
|
||||||
readNonEmptyString(record.workdir) ??
|
readNonEmptyString(record.workdir) ??
|
||||||
readNonEmptyString(record.folder);
|
readNonEmptyString(record.folder);
|
||||||
return cwd ? { sessionId, cwd } : { sessionId };
|
const workspaceId = readNonEmptyString(record.workspaceId) ?? readNonEmptyString(record.workspace_id);
|
||||||
|
const repoUrl = readNonEmptyString(record.repoUrl) ?? readNonEmptyString(record.repo_url);
|
||||||
|
const repoRef = readNonEmptyString(record.repoRef) ?? readNonEmptyString(record.repo_ref);
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
...(cwd ? { cwd } : {}),
|
||||||
|
...(workspaceId ? { workspaceId } : {}),
|
||||||
|
...(repoUrl ? { repoUrl } : {}),
|
||||||
|
...(repoRef ? { repoRef } : {}),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
serialize(params: Record<string, unknown> | null) {
|
serialize(params: Record<string, unknown> | null) {
|
||||||
if (!params) return null;
|
if (!params) return null;
|
||||||
@@ -27,7 +36,16 @@ export const sessionCodec: AdapterSessionCodec = {
|
|||||||
readNonEmptyString(params.cwd) ??
|
readNonEmptyString(params.cwd) ??
|
||||||
readNonEmptyString(params.workdir) ??
|
readNonEmptyString(params.workdir) ??
|
||||||
readNonEmptyString(params.folder);
|
readNonEmptyString(params.folder);
|
||||||
return cwd ? { sessionId, cwd } : { sessionId };
|
const workspaceId = readNonEmptyString(params.workspaceId) ?? readNonEmptyString(params.workspace_id);
|
||||||
|
const repoUrl = readNonEmptyString(params.repoUrl) ?? readNonEmptyString(params.repo_url);
|
||||||
|
const repoRef = readNonEmptyString(params.repoRef) ?? readNonEmptyString(params.repo_ref);
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
...(cwd ? { cwd } : {}),
|
||||||
|
...(workspaceId ? { workspaceId } : {}),
|
||||||
|
...(repoUrl ? { repoUrl } : {}),
|
||||||
|
...(repoRef ? { repoRef } : {}),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
getDisplayId(params: Record<string, unknown> | null) {
|
getDisplayId(params: Record<string, unknown> | null) {
|
||||||
if (!params) return null;
|
if (!params) return null;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
|||||||
|
|
||||||
const DEFAULT_INSTANCE_ID = "default";
|
const DEFAULT_INSTANCE_ID = "default";
|
||||||
const INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
const INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
||||||
|
const PATH_SEGMENT_RE = /^[a-zA-Z0-9_-]+$/;
|
||||||
|
|
||||||
function expandHomePrefix(value: string): string {
|
function expandHomePrefix(value: string): string {
|
||||||
if (value === "~") return os.homedir();
|
if (value === "~") return os.homedir();
|
||||||
@@ -48,6 +49,14 @@ export function resolveDefaultStorageDir(): string {
|
|||||||
return path.resolve(resolvePaperclipInstanceRoot(), "data", "storage");
|
return path.resolve(resolvePaperclipInstanceRoot(), "data", "storage");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveDefaultAgentWorkspaceDir(agentId: string): string {
|
||||||
|
const trimmed = agentId.trim();
|
||||||
|
if (!PATH_SEGMENT_RE.test(trimmed)) {
|
||||||
|
throw new Error(`Invalid agent id for workspace path '${agentId}'.`);
|
||||||
|
}
|
||||||
|
return path.resolve(resolvePaperclipInstanceRoot(), "workspaces", trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveHomeAwarePath(value: string): string {
|
export function resolveHomeAwarePath(value: string): string {
|
||||||
return path.resolve(expandHomePrefix(value));
|
return path.resolve(expandHomePrefix(value));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
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 "@paperclip/db";
|
import type { Db } from "@paperclip/db";
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
heartbeatRuns,
|
heartbeatRuns,
|
||||||
costEvents,
|
costEvents,
|
||||||
issues,
|
issues,
|
||||||
|
projectWorkspaces,
|
||||||
} from "@paperclip/db";
|
} from "@paperclip/db";
|
||||||
import { conflict, notFound } from "../errors.js";
|
import { conflict, notFound } from "../errors.js";
|
||||||
import { logger } from "../middleware/logger.js";
|
import { logger } from "../middleware/logger.js";
|
||||||
@@ -19,6 +21,7 @@ import type { AdapterExecutionResult, AdapterInvocationMeta, AdapterSessionCodec
|
|||||||
import { createLocalAgentJwt } from "../agent-auth-jwt.js";
|
import { createLocalAgentJwt } from "../agent-auth-jwt.js";
|
||||||
import { parseObject, asBoolean, asNumber, appendWithCap, MAX_EXCERPT_BYTES } from "../adapters/utils.js";
|
import { parseObject, asBoolean, asNumber, appendWithCap, MAX_EXCERPT_BYTES } from "../adapters/utils.js";
|
||||||
import { secretService } from "./secrets.js";
|
import { secretService } from "./secrets.js";
|
||||||
|
import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js";
|
||||||
|
|
||||||
const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024;
|
const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024;
|
||||||
const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = 1;
|
const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = 1;
|
||||||
@@ -336,6 +339,74 @@ export function heartbeatService(db: Db) {
|
|||||||
return runtimeForRun?.sessionId ?? null;
|
return runtimeForRun?.sessionId ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveWorkspaceForRun(
|
||||||
|
agent: typeof agents.$inferSelect,
|
||||||
|
context: Record<string, unknown>,
|
||||||
|
previousSessionParams: Record<string, unknown> | null,
|
||||||
|
) {
|
||||||
|
const sessionCwd = readNonEmptyString(previousSessionParams?.cwd);
|
||||||
|
if (sessionCwd) {
|
||||||
|
const sessionCwdExists = await fs
|
||||||
|
.stat(sessionCwd)
|
||||||
|
.then((stats) => stats.isDirectory())
|
||||||
|
.catch(() => false);
|
||||||
|
if (sessionCwdExists) {
|
||||||
|
return {
|
||||||
|
cwd: sessionCwd,
|
||||||
|
source: "task_session" as const,
|
||||||
|
projectId: readNonEmptyString(context.projectId),
|
||||||
|
workspaceId: readNonEmptyString(previousSessionParams?.workspaceId),
|
||||||
|
repoUrl: readNonEmptyString(previousSessionParams?.repoUrl),
|
||||||
|
repoRef: readNonEmptyString(previousSessionParams?.repoRef),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const issueId = readNonEmptyString(context.issueId);
|
||||||
|
if (issueId) {
|
||||||
|
const issue = await db
|
||||||
|
.select({ id: issues.id, projectId: issues.projectId })
|
||||||
|
.from(issues)
|
||||||
|
.where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
|
||||||
|
.then((rows) => rows[0] ?? null);
|
||||||
|
if (issue?.projectId) {
|
||||||
|
const workspace = await db
|
||||||
|
.select()
|
||||||
|
.from(projectWorkspaces)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(projectWorkspaces.companyId, agent.companyId),
|
||||||
|
eq(projectWorkspaces.projectId, issue.projectId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id))
|
||||||
|
.limit(1)
|
||||||
|
.then((rows) => rows[0] ?? null);
|
||||||
|
if (workspace) {
|
||||||
|
return {
|
||||||
|
cwd: workspace.cwd,
|
||||||
|
source: "project_primary" as const,
|
||||||
|
projectId: issue.projectId,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
repoUrl: workspace.repoUrl,
|
||||||
|
repoRef: workspace.repoRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cwd = resolveDefaultAgentWorkspaceDir(agent.id);
|
||||||
|
await fs.mkdir(cwd, { recursive: true });
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
source: "agent_home" as const,
|
||||||
|
projectId: readNonEmptyString(context.projectId),
|
||||||
|
workspaceId: null,
|
||||||
|
repoUrl: null,
|
||||||
|
repoRef: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function upsertTaskSession(input: {
|
async function upsertTaskSession(input: {
|
||||||
companyId: string;
|
companyId: string;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
@@ -786,6 +857,18 @@ export function heartbeatService(db: Db) {
|
|||||||
const previousSessionParams = normalizeSessionParams(
|
const previousSessionParams = normalizeSessionParams(
|
||||||
sessionCodec.deserialize(taskSession?.sessionParamsJson ?? null),
|
sessionCodec.deserialize(taskSession?.sessionParamsJson ?? null),
|
||||||
);
|
);
|
||||||
|
const resolvedWorkspace = await resolveWorkspaceForRun(agent, context, previousSessionParams);
|
||||||
|
context.paperclipWorkspace = {
|
||||||
|
cwd: resolvedWorkspace.cwd,
|
||||||
|
source: resolvedWorkspace.source,
|
||||||
|
projectId: resolvedWorkspace.projectId,
|
||||||
|
workspaceId: resolvedWorkspace.workspaceId,
|
||||||
|
repoUrl: resolvedWorkspace.repoUrl,
|
||||||
|
repoRef: resolvedWorkspace.repoRef,
|
||||||
|
};
|
||||||
|
if (resolvedWorkspace.projectId && !readNonEmptyString(context.projectId)) {
|
||||||
|
context.projectId = resolvedWorkspace.projectId;
|
||||||
|
}
|
||||||
const runtimeSessionFallback = taskKey ? null : runtime.sessionId;
|
const runtimeSessionFallback = taskKey ? null : runtime.sessionId;
|
||||||
const previousSessionDisplayId = truncateDisplayId(
|
const previousSessionDisplayId = truncateDisplayId(
|
||||||
taskSession?.sessionDisplayId ??
|
taskSession?.sessionDisplayId ??
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const help: Record<string, string> = {
|
|||||||
reportsTo: "The agent this one reports to in the org hierarchy.",
|
reportsTo: "The agent this one reports to in the org hierarchy.",
|
||||||
capabilities: "Describes what this agent can do. Shown in the org chart and used for task routing.",
|
capabilities: "Describes what this agent can do. Shown in the org chart and used for task routing.",
|
||||||
adapterType: "How this agent runs: local CLI (Claude/Codex), spawned process, or HTTP webhook.",
|
adapterType: "How this agent runs: local CLI (Claude/Codex), spawned process, or HTTP webhook.",
|
||||||
cwd: "The working directory where the agent operates. Use an absolute path on the machine running Paperclip.",
|
cwd: "Default working directory fallback for local adapters. Use an absolute path on the machine running Paperclip.",
|
||||||
promptTemplate: "The prompt sent to the agent on each heartbeat. Supports {{ agent.id }}, {{ agent.name }}, {{ agent.role }} variables.",
|
promptTemplate: "The prompt sent to the agent on each heartbeat. Supports {{ agent.id }}, {{ agent.name }}, {{ agent.role }} variables.",
|
||||||
model: "Override the default model used by the adapter.",
|
model: "Override the default model used by the adapter.",
|
||||||
thinkingEffort: "Control model reasoning depth. Supported values vary by adapter/model.",
|
thinkingEffort: "Control model reasoning depth. Supported values vary by adapter/model.",
|
||||||
|
|||||||
Reference in New Issue
Block a user