Files
PaperClipAI/server/src/services/tool-access/activity/projection.ts
T
DottaandPaperclip ffcd113761 refactor(tools): isolate runtime and activity ownership (#17159)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Operators inspect tool execution, review pending actions and control
local runtimes.
> - These paths shared a large service with token exchange and app
authorization.
> - Runtime state and execution history need explicit owners and stable
public responses.
> - This pull request separates runtime controls, health summaries,
activity reads and the review queue.
> - The benefit is focused modules that preserve the current supervisor
and approval behavior.

## Linked Issues or Issue Description

Depends on #17157. Earlier stack: #17144 → #17146 → #17147 → #17155 →
#17157 → this PR.

**What existing behavior does this improve?**

Maintenance of tool runtime administration and execution activity views.

**Current behavior**

The 16,100-line service contains runtime slot controls, health
calculations, usage history, invocation decisions and action review
queries.

**Proposed behavior**

Runtime modules own slot persistence, operator controls and health
summaries. Activity modules own history, response projections and the
review queue. The public service delegates to these owners.

**Reason and benefit**

The entry point falls to 14,877 lines, a reduction of 1,223 lines. The
coordinator no longer needs execution-history joins or runtime alert
calculations.

## What Changed

- Extract five modules totaling 1,282 lines. The largest is 426 lines.
- Pass the existing supervisor to slot controls. Pass the same live
deployment options, clock and host getter to runtime health.
- Keep activity reads scoped as before. Keep unsigned-request grace and
invalid-signature cancellation in the review-queue owner.
- Preserve public methods, response projections, query order and audit
writes.

## Verification

- The public service and unsigned review-queue tests pass before and
after the move. The combined post-move run passes 469 cases with one
database worker.
- Existing cases exercise actual local stdio stop/restart, route
permissions, stale-slot and audit-failure health, activity attribution,
run decisions and unsigned pending requests.
- Server TypeScript check, module-boundary gate and `git diff --check`
pass.
- Normalized syntax comparison finds all 256 pre-existing
function/method bodies unchanged.
- Full typecheck/build will run at the stack handoff. The earlier
interrupted local full suite remains unverified; all current-head CI
gates pass, with Greptile 5/5 and no open review threads.

## Risks

- Creating another supervisor would split process ownership. The new
module receives the same instance; it does not construct one.
- Copying deployment settings could freeze live values. The health
reader holds the existing options object and getter.
- Review-queue reads can cancel invalid requests. Those writes and the
unsigned-request grace period are preserved and covered by the existing
regression test.
- No schema, public API, authorization policy or background scheduling
change is intended.

## Model Used

OpenAI Codex, GPT-6 family, with repository tools and code execution.
The exact serving model ID and context-window size are not exposed in
this session.

## Checklist


- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-21 22:21:42 -05:00

139 lines
4.3 KiB
TypeScript

import { toolActionRequests, toolCallEvents, toolInvocations } from "@paperclipai/db";
import type { ToolActionRequest, ToolCallEvent, ToolInvocation } from "@paperclipai/shared";
export function toToolInvocation(
row: typeof toolInvocations.$inferSelect,
): ToolInvocation {
return {
id: row.id,
companyId: row.companyId,
idempotencyKey: row.idempotencyKey,
actorType: row.actorType as ToolInvocation["actorType"],
actorId: row.actorId,
agentId: row.agentId,
issueId: row.issueId,
runId: row.runId,
applicationId: row.applicationId,
connectionId: row.connectionId,
catalogEntryId: row.catalogEntryId,
toolName: row.toolName,
argumentsHash: row.argumentsHash,
argumentsSummary: row.argumentsSummary ?? null,
policyDecision: row.policyDecision,
matchedPolicyIds: row.matchedPolicyIds,
approvalState: row.approvalState,
status: row.status,
upstreamRequestId: row.upstreamRequestId,
resultHash: row.resultHash,
resultSummary: row.resultSummary ?? null,
resultSizeBytes: row.resultSizeBytes,
resultArtifactId: row.resultArtifactId,
errorCode: row.errorCode,
errorMessage: row.errorMessage,
startedAt: row.startedAt,
completedAt: row.completedAt,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
}
export function toToolActionRequest(
row: typeof toolActionRequests.$inferSelect,
): ToolActionRequest {
return {
id: row.id,
companyId: row.companyId,
invocationId: row.invocationId,
issueId: row.issueId,
interactionId: row.interactionId,
approvalId: row.approvalId,
status: row.status,
canonicalArgumentsHash: row.canonicalArgumentsHash,
canonicalArgumentsSummary: row.canonicalArgumentsSummary,
signedArguments: row.signedArguments,
previewMarkdown: row.previewMarkdown,
requestedByAgentId: row.requestedByAgentId,
requestedByUserId: row.requestedByUserId,
resolvedByAgentId: row.resolvedByAgentId,
resolvedByUserId: row.resolvedByUserId,
decidedByAgentId: row.decidedByAgentId,
decidedByUserId: row.decidedByUserId,
decidedAt: row.decidedAt,
expiresAt: row.expiresAt,
resolvedAt: row.resolvedAt,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
}
export function toToolCallEvent(
row: typeof toolCallEvents.$inferSelect,
): ToolCallEvent {
return {
id: row.id,
companyId: row.companyId,
eventType: row.eventType,
actorType: row.actorType as ToolCallEvent["actorType"],
actorId: row.actorId,
agentId: row.agentId,
runId: row.runId,
issueId: row.issueId,
applicationId: row.applicationId,
connectionId: row.connectionId,
catalogEntryId: row.catalogEntryId,
invocationId: row.invocationId,
actionRequestId: row.actionRequestId,
runtimeSlotId: row.runtimeSlotId,
toolName: row.toolName,
decision: row.decision,
matchedPolicyIds: row.matchedPolicyIds,
reasonCode: row.reasonCode,
outcome: row.outcome,
latencyMs: row.latencyMs,
argumentsSummary: row.argumentsSummary ?? null,
requestHash: row.requestHash,
requestSummary: row.requestSummary ?? null,
resultHash: row.resultHash,
resultSummary: row.resultSummary ?? null,
resultSizeBytes: row.resultSizeBytes,
redactionPlan: row.redactionPlan ?? null,
rateLimitState: row.rateLimitState ?? null,
metadata: row.metadata ?? null,
errorCode: row.errorCode,
errorMessage: row.errorMessage,
createdAt: row.createdAt,
};
}
export function userFallbackName(userId: string): string {
if (userId === "local-board") return "Board";
return userId;
}
export function denialReasonForDecision(
invocation: typeof toolInvocations.$inferSelect,
latestAuditEvent: typeof toolCallEvents.$inferSelect | null,
) {
if (
invocation.status === "denied" ||
invocation.status === "rate_limited" ||
invocation.status === "failed" ||
invocation.status === "timed_out"
) {
return (
invocation.errorMessage ??
invocation.errorCode ??
latestAuditEvent?.reasonCode ??
null
);
}
if (
latestAuditEvent?.outcome === "denied" ||
latestAuditEvent?.outcome === "failure" ||
latestAuditEvent?.outcome === "timeout"
) {
return latestAuditEvent.errorMessage ?? latestAuditEvent.reasonCode ?? null;
}
return null;
}