Implement agent runtime services and WebSocket realtime

Expand heartbeat service with full run executor, wakeup coordinator,
and adapter lifecycle. Add run-log-store for pluggable log persistence.
Add live-events service and WebSocket handler for realtime updates.
Expand agent and issue routes with runtime operations. Add ws dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-17 12:24:43 -06:00
parent 2583bf4c43
commit c9c75bbc0a
11 changed files with 1746 additions and 156 deletions

View File

@@ -7,12 +7,14 @@ import {
updateIssueSchema,
} from "@paperclip/shared";
import { validate } from "../middleware/validate.js";
import { issueService, logActivity } from "../services/index.js";
import { heartbeatService, issueService, logActivity } from "../services/index.js";
import { logger } from "../middleware/logger.js";
import { assertCompanyAccess, getActorInfo } from "./authz.js";
export function issueRoutes(db: Db) {
const router = Router();
const svc = issueService(db);
const heartbeat = heartbeatService(db);
router.get("/companies/:companyId/issues", async (req, res) => {
const companyId = req.params.companyId as string;
@@ -58,6 +60,20 @@ export function issueRoutes(db: Db) {
details: { title: issue.title },
});
if (issue.assigneeAgentId) {
void heartbeat
.wakeup(issue.assigneeAgentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId: issue.id, mutation: "create" },
requestedByActorType: actor.actorType,
requestedByActorId: actor.actorId,
contextSnapshot: { issueId: issue.id, source: "issue.create" },
})
.catch((err) => logger.warn({ err, issueId: issue.id }, "failed to wake assignee on issue create"));
}
res.status(201).json(issue);
});
@@ -88,6 +104,22 @@ export function issueRoutes(db: Db) {
details: req.body,
});
const assigneeChanged =
req.body.assigneeAgentId !== undefined && req.body.assigneeAgentId !== existing.assigneeAgentId;
if (assigneeChanged && issue.assigneeAgentId) {
void heartbeat
.wakeup(issue.assigneeAgentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId: issue.id, mutation: "update" },
requestedByActorType: actor.actorType,
requestedByActorId: actor.actorId,
contextSnapshot: { issueId: issue.id, source: "issue.update" },
})
.catch((err) => logger.warn({ err, issueId: issue.id }, "failed to wake assignee on issue update"));
}
res.json(issue);
});
@@ -148,6 +180,18 @@ export function issueRoutes(db: Db) {
details: { agentId: req.body.agentId },
});
void heartbeat
.wakeup(req.body.agentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_checked_out",
payload: { issueId: issue.id, mutation: "checkout" },
requestedByActorType: actor.actorType,
requestedByActorId: actor.actorId,
contextSnapshot: { issueId: issue.id, source: "issue.checkout" },
})
.catch((err) => logger.warn({ err, issueId: issue.id }, "failed to wake assignee on issue checkout"));
res.json(updated);
});