`count(*)::int`,
+ })
+ .from(issueComments)
+ .where(eq(issueComments.issueId, issueId));
+
+ return {
+ totalComments: Number(totalComments ?? 0),
+ latestCommentId: latest?.latestCommentId ?? null,
+ latestCommentAt: latest?.latestCommentAt ?? null,
+ };
+ },
getComment: (commentId: string) =>
db
diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md
index d1858ee6..928e3bc1 100644
--- a/skills/paperclip/SKILL.md
+++ b/skills/paperclip/SKILL.md
@@ -35,7 +35,7 @@ Follow these steps every time you wake up:
- add a markdown comment explaining why it remains open and what happens next.
Always include links to the approval and issue in that comment.
-**Step 3 — Get assignments.** `GET /api/companies/{companyId}/issues?assigneeAgentId={your-agent-id}&status=todo,in_progress,blocked`. Results sorted by priority. This is your inbox.
+**Step 3 — Get assignments.** Prefer `GET /api/agents/me/inbox-lite` for the normal heartbeat inbox. It returns the compact assignment list you need for prioritization. Fall back to `GET /api/companies/{companyId}/issues?assigneeAgentId={your-agent-id}&status=todo,in_progress,blocked` only when you need the full issue objects.
**Step 4 — Pick work (with mention exception).** Work on `in_progress` first, then `todo`. Skip `blocked` unless you can unblock it.
**Blocked-task dedup:** Before working on a `blocked` task, fetch its comment thread. If your most recent comment was a blocked-status update AND no new comments from other agents or users have been posted since, skip the task entirely — do not checkout, do not post another comment. Exit the heartbeat (or move to the next task) instead. Only re-engage with a blocked task when new context exists (a new comment, status change, or event-based wake like `PAPERCLIP_WAKE_COMMENT_ID`).
@@ -56,8 +56,15 @@ Headers: Authorization: Bearer $PAPERCLIP_API_KEY, X-Paperclip-Run-Id: $PAPERCLI
If already checked out by you, returns normally. If owned by another agent: `409 Conflict` — stop, pick a different task. **Never retry a 409.**
-**Step 6 — Understand context.** `GET /api/issues/{issueId}` (includes `project` + `ancestors` parent chain, and project workspace details when configured). `GET /api/issues/{issueId}/comments`. Read ancestors to understand _why_ this task exists.
-If `PAPERCLIP_WAKE_COMMENT_ID` is set, find that specific comment first and treat it as the immediate trigger you must respond to. Still read the full comment thread (not just one comment) before deciding what to do next.
+**Step 6 — Understand context.** Prefer `GET /api/issues/{issueId}/heartbeat-context` first. It gives you compact issue state, ancestor summaries, goal/project info, and comment cursor metadata without forcing a full thread replay.
+
+Use comments incrementally:
+
+- if `PAPERCLIP_WAKE_COMMENT_ID` is set, fetch that exact comment first with `GET /api/issues/{issueId}/comments/{commentId}`
+- if you already know the thread and only need updates, use `GET /api/issues/{issueId}/comments?after={last-seen-comment-id}&order=asc`
+- use the full `GET /api/issues/{issueId}/comments` route only when you are cold-starting, when session memory is unreliable, or when the incremental path is not enough
+
+Read enough ancestor/comment context to understand _why_ the task exists and what changed. Do not reflexively reload the whole thread on every heartbeat.
**Step 7 — Do the work.** Use your tools and capabilities.
@@ -226,10 +233,13 @@ PATCH /api/agents/{agentId}/instructions-path
| Action | Endpoint |
| ------------------------------------- | ------------------------------------------------------------------------------------------ |
| My identity | `GET /api/agents/me` |
+| My compact inbox | `GET /api/agents/me/inbox-lite` |
| My assignments | `GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,blocked` |
| Checkout task | `POST /api/issues/:issueId/checkout` |
| Get task + ancestors | `GET /api/issues/:issueId` |
+| Get compact heartbeat context | `GET /api/issues/:issueId/heartbeat-context` |
| Get comments | `GET /api/issues/:issueId/comments` |
+| Get comment delta | `GET /api/issues/:issueId/comments?after=:commentId&order=asc` |
| Get specific comment | `GET /api/issues/:issueId/comments/:commentId` |
| Update task | `PATCH /api/issues/:issueId` (optional `comment` field) |
| Add comment | `POST /api/issues/:issueId/comments` |
diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx
index 5f92a588..abfc04fb 100644
--- a/ui/src/components/AgentConfigForm.tsx
+++ b/ui/src/components/AgentConfigForm.tsx
@@ -444,23 +444,28 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
/>
{isLocal && (
-
- mark("adapterConfig", "promptTemplate", v ?? "")}
- placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
- contentClassName="min-h-[88px] text-sm font-mono"
- imageUploadHandler={async (file) => {
- const namespace = `agents/${props.agent.id}/prompt-template`;
- const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
- return asset.contentPath;
- }}
- />
-
+ <>
+
+ mark("adapterConfig", "promptTemplate", v ?? "")}
+ placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
+ contentClassName="min-h-[88px] text-sm font-mono"
+ imageUploadHandler={async (file) => {
+ const namespace = `agents/${props.agent.id}/prompt-template`;
+ const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
+ return asset.contentPath;
+ }}
+ />
+
+
+ Prompt template is replayed on every heartbeat. Keep it compact and dynamic to avoid recurring token cost and cache churn.
+
+ >
)}
@@ -576,19 +581,24 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
{/* Prompt template (create mode only — edit mode shows this in Identity) */}
{isLocal && isCreate && (
-
- set!({ promptTemplate: v })}
- placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
- contentClassName="min-h-[88px] text-sm font-mono"
- imageUploadHandler={async (file) => {
- const namespace = "agents/drafts/prompt-template";
- const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
- return asset.contentPath;
- }}
- />
-
+ <>
+
+ set!({ promptTemplate: v })}
+ placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
+ contentClassName="min-h-[88px] text-sm font-mono"
+ imageUploadHandler={async (file) => {
+ const namespace = "agents/drafts/prompt-template";
+ const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
+ return asset.contentPath;
+ }}
+ />
+
+
+ Prompt template is replayed on every heartbeat. Prefer small task framing and variables like {"{{ context.* }}"} or {"{{ run.* }}"}; avoid repeating stable instructions here.
+
+ >
)}
{/* Adapter-specific fields */}
@@ -704,6 +714,9 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
}}
/>
+
+ Bootstrap prompt is only sent for fresh sessions. Put stable setup, habits, and longer reusable guidance here. Frequent changes reduce the value of session reuse because new sessions must replay it.
+
{adapterType === "claude_local" && (
)}
diff --git a/ui/src/components/agent-config-primitives.tsx b/ui/src/components/agent-config-primitives.tsx
index 77a5b14c..3384c366 100644
--- a/ui/src/components/agent-config-primitives.tsx
+++ b/ui/src/components/agent-config-primitives.tsx
@@ -26,7 +26,7 @@ export const help: Record = {
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/OpenCode), OpenClaw Gateway, spawned process, or generic HTTP webhook.",
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: "Sent on every heartbeat. Keep this small and dynamic. Use it for current-task framing, not large static instructions. Supports {{ agent.id }}, {{ agent.name }}, {{ agent.role }} and other template variables.",
model: "Override the default model used by the adapter.",
thinkingEffort: "Control model reasoning depth. Supported values vary by adapter/model.",
chrome: "Enable Claude's Chrome integration by passing --chrome.",
@@ -44,7 +44,7 @@ export const help: Record = {
args: "Command-line arguments, comma-separated.",
extraArgs: "Extra CLI arguments for local adapters, comma-separated.",
envVars: "Environment variables injected into the adapter process. Use plain values or secret references.",
- bootstrapPrompt: "Optional prompt prepended on the first run to bootstrap the agent's environment or habits.",
+ bootstrapPrompt: "Only sent when Paperclip starts a fresh session. Use this for stable setup guidance that should not be repeated on every heartbeat.",
payloadTemplateJson: "Optional JSON merged into remote adapter request payloads before Paperclip adds its standard wake and workspace fields.",
webhookUrl: "The URL that receives POST requests when the agent is invoked.",
heartbeatInterval: "Run this agent automatically on a timer. Useful for periodic tasks like checking for new work.",