Server: migration prompts, structured logging, heartbeat reaping, and issue-run tracking

Replace auto-migrate-if-empty with interactive migration flow that inspects
pending migrations and prompts before applying. Add pino-pretty for structured
console + file logging. Add reapOrphanedRuns to clean up stuck heartbeat runs
on startup and periodically. Track runId through auth middleware, activity logs,
and all mutation routes. Add issue-run cross-reference queries, live-run and
active-run endpoints for issues, issue identifier lookup, reopen-via-comment
flow, and done/cancelled -> todo status transitions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-19 09:09:40 -06:00
parent 21b7bc8da0
commit a90063415e
14 changed files with 605 additions and 67 deletions

View File

@@ -13,8 +13,11 @@ export function actorMiddleware(db: Db): RequestHandler {
return async (req, _res, next) => {
req.actor = { type: "board", userId: "board" };
const runIdHeader = req.header("x-paperclip-run-id");
const authHeader = req.header("authorization");
if (!authHeader?.toLowerCase().startsWith("bearer ")) {
if (runIdHeader) req.actor.runId = runIdHeader;
next();
return;
}
@@ -60,6 +63,7 @@ export function actorMiddleware(db: Db): RequestHandler {
agentId: claims.sub,
companyId: claims.company_id,
keyId: undefined,
runId: runIdHeader || undefined,
};
next();
return;
@@ -75,6 +79,7 @@ export function actorMiddleware(db: Db): RequestHandler {
agentId: key.agentId,
companyId: key.companyId,
keyId: key.id,
runId: runIdHeader || undefined,
};
next();

View File

@@ -1,5 +1,41 @@
import path from "node:path";
import fs from "node:fs";
import pino from "pino";
import { pinoHttp } from "pino-http";
export const logger = pino();
export const httpLogger = pinoHttp({ logger });
const logDir = path.resolve(process.cwd(), ".paperclip", "logs");
fs.mkdirSync(logDir, { recursive: true });
const logFile = path.join(logDir, "server.log");
const sharedOpts = {
translateTime: "HH:MM:ss",
ignore: "pid,hostname",
};
export const logger = pino({
level: "debug",
}, pino.transport({
targets: [
{
target: "pino-pretty",
options: { ...sharedOpts, ignore: "pid,hostname,req,res", hideObject: true, colorize: true, destination: 1 },
level: "info",
},
{
target: "pino-pretty",
options: { ...sharedOpts, colorize: false, destination: logFile, mkdir: true },
level: "debug",
},
],
}));
export const httpLogger = pinoHttp({
logger,
customSuccessMessage(req, res) {
return `${req.method} ${req.url} ${res.statusCode}`;
},
customErrorMessage(req, res) {
return `${req.method} ${req.url} ${res.statusCode}`;
},
});