@tickernelz/paperclip-pro-server 2026.930.1 → 2026.930.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +1 -1
- package/dist/modules/run-dispatch/adapters/postgres.d.ts +5 -1
- package/dist/modules/run-dispatch/adapters/postgres.d.ts.map +1 -1
- package/dist/modules/run-dispatch/adapters/postgres.js +2 -1
- package/dist/modules/run-dispatch/adapters/postgres.js.map +1 -1
- package/dist/modules/run-dispatch/index.d.ts +2 -0
- package/dist/modules/run-dispatch/index.d.ts.map +1 -1
- package/dist/modules/run-dispatch/index.js +1 -1
- package/dist/modules/run-dispatch/index.js.map +1 -1
- package/dist/modules/wake-queue/adapters/postgres.d.ts.map +1 -1
- package/dist/modules/wake-queue/adapters/postgres.js +16 -3
- package/dist/modules/wake-queue/adapters/postgres.js.map +1 -1
- package/dist/modules/wake-queue/adapters/postgres.test.js +33 -0
- package/dist/modules/wake-queue/adapters/postgres.test.js.map +1 -1
- package/dist/modules/wake-queue/application/ports.d.ts +3 -0
- package/dist/modules/wake-queue/application/ports.d.ts.map +1 -1
- package/dist/modules/wake-queue/application/ports.js.map +1 -1
- package/dist/modules/wake-queue/application/use-cases.d.ts.map +1 -1
- package/dist/modules/wake-queue/application/use-cases.js +6 -3
- package/dist/modules/wake-queue/application/use-cases.js.map +1 -1
- package/dist/modules/wake-queue/application/use-cases.test.js +52 -2
- package/dist/modules/wake-queue/application/use-cases.test.js.map +1 -1
- package/dist/routes/issues.d.ts +6 -0
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +323 -90
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/openapi.js +1 -1
- package/dist/routes/openapi.js.map +1 -1
- package/dist/services/heartbeat.d.ts +1 -0
- package/dist/services/heartbeat.d.ts.map +1 -1
- package/dist/services/heartbeat.js +100 -20
- package/dist/services/heartbeat.js.map +1 -1
- package/dist/services/native-runtime/native-session-executor.d.ts +7 -2
- package/dist/services/native-runtime/native-session-executor.d.ts.map +1 -1
- package/dist/services/native-runtime/native-session-executor.js +35 -14
- package/dist/services/native-runtime/native-session-executor.js.map +1 -1
- package/dist/services/native-runtime/native-session-executor.test.js +25 -2
- package/dist/services/native-runtime/native-session-executor.test.js.map +1 -1
- package/dist/services/recovery/service.d.ts +1 -0
- package/dist/services/recovery/service.d.ts.map +1 -1
- package/dist/services/recovery/service.js +32 -0
- package/dist/services/recovery/service.js.map +1 -1
- package/dist/services/run-identity.d.ts +1 -0
- package/dist/services/run-identity.d.ts.map +1 -1
- package/dist/services/run-identity.js +13 -3
- package/dist/services/run-identity.js.map +1 -1
- package/dist/services/steering-acknowledgements.d.ts +1 -0
- package/dist/services/steering-acknowledgements.d.ts.map +1 -1
- package/dist/services/steering-acknowledgements.js +6 -0
- package/dist/services/steering-acknowledgements.js.map +1 -1
- package/dist/services/systemd-notify.d.ts.map +1 -1
- package/dist/services/systemd-notify.js +6 -2
- package/dist/services/systemd-notify.js.map +1 -1
- package/dist/services/systemd-notify.test.d.ts +2 -0
- package/dist/services/systemd-notify.test.d.ts.map +1 -0
- package/dist/services/systemd-notify.test.js +35 -0
- package/dist/services/systemd-notify.test.js.map +1 -0
- package/package.json +21 -21
- package/skills/paperclip/SKILL.md +3 -1
- package/skills/paperclip/references/api-reference.md +2 -0
- package/ui-dist/assets/{Agents.production-BDXEK4Q0.js → Agents.production-VItybUXr.js} +2 -2
- package/ui-dist/assets/{CompanyActivity.production-AoQ1Xzp6.js → CompanyActivity.production-CwZwgQQV.js} +1 -1
- package/ui-dist/assets/{CompanyExport-Bgynl5QC.js → CompanyExport-DD7Rya_d.js} +1 -1
- package/ui-dist/assets/{CompanySkills.production-BtKXe0Dw.js → CompanySkills.production-Bwg9FRuw.js} +1 -1
- package/ui-dist/assets/{Costs.production-dqa_yTCM.js → Costs.production-lULBU1r3.js} +1 -1
- package/ui-dist/assets/{MarkdownEditor-DfrwLv1M.js → MarkdownEditor-BN7aGwIv.js} +1 -1
- package/ui-dist/assets/{OrgChart.production-Ca77vSHO.js → OrgChart.production-C7exT9VZ.js} +1 -1
- package/ui-dist/assets/{RoutineDetail.production-BBuPjKy1.js → RoutineDetail.production-DsZSvOON.js} +1 -1
- package/ui-dist/assets/{RoutineRunVariablesDialog-B1KRZJIl.js → RoutineRunVariablesDialog-DO1hY6dt.js} +1 -1
- package/ui-dist/assets/{Routines.production-C5HqDjoO.js → Routines.production-BsmVAcGo.js} +1 -1
- package/ui-dist/assets/{checkbox-757DLJRS.js → checkbox-D66RYMkN.js} +1 -1
- package/ui-dist/assets/{definition-Cfy5D9lX.js → definition-RMjpvqXu.js} +1 -1
- package/ui-dist/assets/{index-B3vYH8Jq.js → index-DU74KtHt.js} +6 -6
- package/ui-dist/assets/{label-BueB30Ch.js → label-BjzFqp93.js} +1 -1
- package/ui-dist/assets/{sheet-CQJe5sfn.js → sheet-ASM5ba9D.js} +1 -1
- package/ui-dist/index.html +6 -6
- package/ui-dist/sw.js +1 -1
package/dist/routes/issues.js
CHANGED
|
@@ -8,7 +8,7 @@ import { releaseDependencyGateRecoveryHold } from "../services/dependency-gate-r
|
|
|
8
8
|
import { documentExportFileName, extractIssueReferenceIdentifiers, requiresExecutionReconciliation } from "@tickernelz/paperclip-pro-shared";
|
|
9
9
|
import { renderDocumentPdf } from "../services/document-pdf.js";
|
|
10
10
|
import { validateExecutionReconciliation, markExecutionReconciliation, } from "../services/execution-recovery-resolution.js";
|
|
11
|
-
import { storedSteeringAcknowledgement,
|
|
11
|
+
import { storedSteeringAcknowledgement, reserveSteeredIdentity, acceptSteeredIdentity, rejectSteeredIdentity, } from "../services/run-identity.js";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { Router } from "express";
|
|
14
14
|
import multer from "multer";
|
|
@@ -74,8 +74,17 @@ import { evaluateIssueThreadInteractionResolverAudience, issueThreadInteractionA
|
|
|
74
74
|
import { resolveSelectedSuggestedTasks } from "../services/issue-thread-interactions.js";
|
|
75
75
|
import { crossIssueInfluenceLimitError, crossIssueInfluenceRunContextError, observeCrossIssueInfluence, } from "../services/cross-issue-influence-limit.js";
|
|
76
76
|
import { getNativeSessionSteeringState, hasLiveAdapterSteering, NativeSessionSteeringError, steerNativeSession, } from "../services/native-runtime/native-session-executor.js";
|
|
77
|
-
import { buildQueuedCommentQueueSnapshot, decideQueuedCommentQueueSteering, queuedCommentIdsFromWakePayload, withQueuedCommentIdsInWakePayload, } from "../services/issue-queued-comment-queue.js";
|
|
77
|
+
import { buildQueuedCommentQueueSnapshot, decideQueuedCommentQueueSteering, queuedCommentIdsFromWakePayload, withQueuedCommentIdsInRunContext, withQueuedCommentIdsInWakePayload, } from "../services/issue-queued-comment-queue.js";
|
|
78
|
+
import { resultJsonWithSteeringAcknowledgement } from "../services/steering-acknowledgements.js";
|
|
79
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
78
80
|
const MAX_ISSUE_COMMENT_LIMIT = 500;
|
|
81
|
+
const DEFAULT_STEERING_RETRY = { delaysMs: [1_000, 3_000], budgetMs: 10_000, minAttemptMs: 2_000 };
|
|
82
|
+
const RETRYABLE_STEERING_CODES = {
|
|
83
|
+
steering_rejected: true,
|
|
84
|
+
steering_stale_turn: true,
|
|
85
|
+
steering_temporarily_unavailable: true,
|
|
86
|
+
steering_identity_pending: true,
|
|
87
|
+
};
|
|
79
88
|
const updateIssueRouteSchema = updateIssueSchema.extend({
|
|
80
89
|
interrupt: z.boolean().optional(),
|
|
81
90
|
});
|
|
@@ -4814,13 +4823,15 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
4814
4823
|
const activeRunId = state === "deferred"
|
|
4815
4824
|
? (input.targetRunId ?? input.issue.executionRunId ?? null)
|
|
4816
4825
|
: null;
|
|
4817
|
-
const
|
|
4818
|
-
?
|
|
4826
|
+
const activeRunQuery = activeRunId
|
|
4827
|
+
? input.tx
|
|
4819
4828
|
.select()
|
|
4820
4829
|
.from(heartbeatRuns)
|
|
4821
4830
|
.where(and(eq(heartbeatRuns.id, activeRunId), eq(heartbeatRuns.companyId, input.issue.companyId), input.allowStoppedTarget ? undefined : eq(heartbeatRuns.status, "running")))
|
|
4822
|
-
.for("update")
|
|
4823
4831
|
.limit(1)
|
|
4832
|
+
: null;
|
|
4833
|
+
const activeRun = activeRunQuery
|
|
4834
|
+
? await (input.lockActiveRun === false ? activeRunQuery : activeRunQuery.for("update"))
|
|
4824
4835
|
.then((rows) => rows[0] ?? null)
|
|
4825
4836
|
: null;
|
|
4826
4837
|
if (input.targetRunId) {
|
|
@@ -11549,18 +11560,156 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11549
11560
|
});
|
|
11550
11561
|
res.json(await runRedactions.redactForIssue(issue.companyId, issue.id, queue));
|
|
11551
11562
|
});
|
|
11563
|
+
const steeringRetry = opts.steeringRetry ?? DEFAULT_STEERING_RETRY;
|
|
11564
|
+
async function withdrawSteeredComment(input) {
|
|
11565
|
+
const { tx, wake, queueRun, commentId } = input;
|
|
11566
|
+
const remainingIds = input.queue.entries
|
|
11567
|
+
.map((candidate) => candidate.comment.id)
|
|
11568
|
+
.filter((candidateId) => candidateId !== commentId);
|
|
11569
|
+
const now = new Date();
|
|
11570
|
+
if (remainingIds.length === 0) {
|
|
11571
|
+
await tx
|
|
11572
|
+
.update(agentWakeupRequests)
|
|
11573
|
+
.set({ status: "cancelled", finishedAt: now, updatedAt: now })
|
|
11574
|
+
.where(eq(agentWakeupRequests.id, wake.id));
|
|
11575
|
+
if (queueRun) {
|
|
11576
|
+
await tx
|
|
11577
|
+
.update(heartbeatRuns)
|
|
11578
|
+
.set({
|
|
11579
|
+
status: "cancelled",
|
|
11580
|
+
finishedAt: now,
|
|
11581
|
+
error: "Queued message was steered into the running turn",
|
|
11582
|
+
errorCode: "queued_comment_steered",
|
|
11583
|
+
updatedAt: now,
|
|
11584
|
+
})
|
|
11585
|
+
.where(and(eq(heartbeatRuns.id, queueRun.id), eq(heartbeatRuns.status, "queued")));
|
|
11586
|
+
await tx
|
|
11587
|
+
.update(issueRows)
|
|
11588
|
+
.set({ executionRunId: null, executionAgentNameKey: null, executionLockedAt: null, updatedAt: now })
|
|
11589
|
+
.where(and(eq(issueRows.id, input.issueId), eq(issueRows.executionRunId, queueRun.id)));
|
|
11590
|
+
}
|
|
11591
|
+
return null;
|
|
11592
|
+
}
|
|
11593
|
+
if (queueRun) {
|
|
11594
|
+
await tx
|
|
11595
|
+
.update(heartbeatRuns)
|
|
11596
|
+
.set({
|
|
11597
|
+
contextSnapshot: withQueuedCommentIdsInRunContext(queueRun.contextSnapshot, remainingIds),
|
|
11598
|
+
updatedAt: now,
|
|
11599
|
+
})
|
|
11600
|
+
.where(and(eq(heartbeatRuns.id, queueRun.id), eq(heartbeatRuns.status, "queued")));
|
|
11601
|
+
}
|
|
11602
|
+
return tx
|
|
11603
|
+
.update(agentWakeupRequests)
|
|
11604
|
+
.set({
|
|
11605
|
+
payload: withQueuedCommentIdsInWakePayload(wake.payload, remainingIds),
|
|
11606
|
+
updatedAt: now,
|
|
11607
|
+
})
|
|
11608
|
+
.where(eq(agentWakeupRequests.id, wake.id))
|
|
11609
|
+
.returning()
|
|
11610
|
+
.then((rows) => rows[0] ?? wake);
|
|
11611
|
+
}
|
|
11612
|
+
async function recordSteeringAcknowledgement(input) {
|
|
11613
|
+
const now = new Date();
|
|
11614
|
+
await input.tx
|
|
11615
|
+
.update(heartbeatRuns)
|
|
11616
|
+
.set({
|
|
11617
|
+
resultJson: resultJsonWithSteeringAcknowledgement(input.commentId, {
|
|
11618
|
+
status: "acknowledged",
|
|
11619
|
+
queueId: input.queueId,
|
|
11620
|
+
turnId: input.turnId,
|
|
11621
|
+
acknowledgedAt: now.toISOString(),
|
|
11622
|
+
}),
|
|
11623
|
+
updatedAt: now,
|
|
11624
|
+
})
|
|
11625
|
+
.where(eq(heartbeatRuns.id, input.runId));
|
|
11626
|
+
}
|
|
11627
|
+
async function settleLateSteeringAcknowledgement(input) {
|
|
11628
|
+
await db.transaction(async (tx) => {
|
|
11629
|
+
await tx
|
|
11630
|
+
.select({ id: issueRows.id })
|
|
11631
|
+
.from(issueRows)
|
|
11632
|
+
.where(and(eq(issueRows.id, input.issue.id), eq(issueRows.companyId, input.issue.companyId)))
|
|
11633
|
+
.for("update");
|
|
11634
|
+
const run = await tx
|
|
11635
|
+
.select({ resultJson: heartbeatRuns.resultJson })
|
|
11636
|
+
.from(heartbeatRuns)
|
|
11637
|
+
.where(and(eq(heartbeatRuns.id, input.runId), eq(heartbeatRuns.companyId, input.issue.companyId)))
|
|
11638
|
+
.then((rows) => rows[0] ?? null);
|
|
11639
|
+
if (!run)
|
|
11640
|
+
return;
|
|
11641
|
+
if (input.identity)
|
|
11642
|
+
await acceptSteeredIdentity(tx, input.identity);
|
|
11643
|
+
const recorded = readObject(readObject(readObject(run.resultJson).queuedSteeringAcknowledgements)[input.commentId]);
|
|
11644
|
+
if (recorded.status === "acknowledged")
|
|
11645
|
+
return;
|
|
11646
|
+
const pendingWakes = await tx
|
|
11647
|
+
.select({ id: agentWakeupRequests.id, payload: agentWakeupRequests.payload })
|
|
11648
|
+
.from(agentWakeupRequests)
|
|
11649
|
+
.where(and(eq(agentWakeupRequests.companyId, input.issue.companyId), inArray(agentWakeupRequests.status, ["deferred_issue_execution", "queued"]), sql `${agentWakeupRequests.payload}->>'issueId' = ${input.issue.id}`));
|
|
11650
|
+
for (const pending of pendingWakes) {
|
|
11651
|
+
if (!queuedCommentIdsFromWakePayload(pending.payload).includes(input.commentId))
|
|
11652
|
+
continue;
|
|
11653
|
+
const locked = await lockQueuedCommentState({
|
|
11654
|
+
tx,
|
|
11655
|
+
issue: input.issue,
|
|
11656
|
+
actor: input.actor,
|
|
11657
|
+
queueId: pending.id,
|
|
11658
|
+
lockActiveRun: false,
|
|
11659
|
+
}).catch((error) => {
|
|
11660
|
+
if (error instanceof HttpError)
|
|
11661
|
+
return null;
|
|
11662
|
+
throw error;
|
|
11663
|
+
});
|
|
11664
|
+
if (!locked?.queue.entries.some((entry) => entry.comment.id === input.commentId))
|
|
11665
|
+
continue;
|
|
11666
|
+
await withdrawSteeredComment({
|
|
11667
|
+
tx,
|
|
11668
|
+
issueId: input.issue.id,
|
|
11669
|
+
queue: locked.queue,
|
|
11670
|
+
wake: locked.wake,
|
|
11671
|
+
queueRun: locked.queueRun,
|
|
11672
|
+
commentId: input.commentId,
|
|
11673
|
+
});
|
|
11674
|
+
}
|
|
11675
|
+
await recordSteeringAcknowledgement({
|
|
11676
|
+
tx,
|
|
11677
|
+
runId: input.runId,
|
|
11678
|
+
commentId: input.commentId,
|
|
11679
|
+
queueId: input.queueId,
|
|
11680
|
+
turnId: input.turnId,
|
|
11681
|
+
});
|
|
11682
|
+
});
|
|
11683
|
+
}
|
|
11552
11684
|
async function steerQueuedCommentInTransaction(input) {
|
|
11553
11685
|
const { issue, actor, commentId } = input;
|
|
11554
11686
|
const responseWake = await db.select().from(agentWakeupRequests).where(and(eq(agentWakeupRequests.id, input.queueId), eq(agentWakeupRequests.companyId, issue.companyId))).then(rows => rows[0]);
|
|
11555
11687
|
const response = responseWake
|
|
11556
11688
|
? await readQueuedInteractionResponse(db, issue.companyId, issue.id, responseWake.payload)
|
|
11557
11689
|
: null;
|
|
11690
|
+
const steeringTargetRun = await db
|
|
11691
|
+
.select({ id: heartbeatRuns.id, agentId: heartbeatRuns.agentId })
|
|
11692
|
+
.from(heartbeatRuns)
|
|
11693
|
+
.where(and(eq(heartbeatRuns.id, input.targetRunId), eq(heartbeatRuns.companyId, issue.companyId)))
|
|
11694
|
+
.then((rows) => rows[0] ?? null);
|
|
11695
|
+
const steeringTargetIssue = await db
|
|
11696
|
+
.select({
|
|
11697
|
+
companyId: issueRows.companyId,
|
|
11698
|
+
projectId: issueRows.projectId,
|
|
11699
|
+
executionPolicy: issueRows.executionPolicy,
|
|
11700
|
+
})
|
|
11701
|
+
.from(issueRows)
|
|
11702
|
+
.where(and(eq(issueRows.id, issue.id), eq(issueRows.companyId, issue.companyId)))
|
|
11703
|
+
.then((rows) => rows[0] ?? null);
|
|
11704
|
+
const steeringExposesLowTrustRaw = steeringTargetRun !== null &&
|
|
11705
|
+
(await resolveAgentTrustForIssue({ agentId: steeringTargetRun.agentId, runId: steeringTargetRun.id }, issue.companyId, steeringTargetIssue))?.kind === "low_trust_review";
|
|
11558
11706
|
const steeringIdentity = await reserveSteeredIdentity(db, {
|
|
11559
11707
|
companyId: issue.companyId,
|
|
11560
11708
|
runId: input.targetRunId,
|
|
11561
11709
|
issueId: issue.id,
|
|
11562
11710
|
messageId: commentId,
|
|
11563
11711
|
source: response?.comment.id === commentId ? "interaction" : "comment",
|
|
11712
|
+
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
11564
11713
|
});
|
|
11565
11714
|
let steeringDeliveryAttempted = false;
|
|
11566
11715
|
let acknowledgedTurnId = null;
|
|
@@ -11587,7 +11736,6 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11587
11736
|
.select()
|
|
11588
11737
|
.from(heartbeatRuns)
|
|
11589
11738
|
.where(and(eq(heartbeatRuns.id, input.targetRunId), eq(heartbeatRuns.companyId, issue.companyId), eq(heartbeatRuns.agentId, retryWake.agentId)))
|
|
11590
|
-
.for("update")
|
|
11591
11739
|
.limit(1)
|
|
11592
11740
|
.then((rows) => rows[0] ?? null)
|
|
11593
11741
|
: null;
|
|
@@ -11618,6 +11766,7 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11618
11766
|
actor,
|
|
11619
11767
|
queueId: input.queueId,
|
|
11620
11768
|
targetRunId: input.targetRunId,
|
|
11769
|
+
lockActiveRun: false,
|
|
11621
11770
|
});
|
|
11622
11771
|
if (!locked.activeRun) {
|
|
11623
11772
|
throw conflict("The queued message targets a stale run", {
|
|
@@ -11664,58 +11813,52 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11664
11813
|
});
|
|
11665
11814
|
}
|
|
11666
11815
|
steeringDeliveryAttempted = true;
|
|
11816
|
+
const activeRunId = locked.activeRun.id;
|
|
11667
11817
|
const acknowledgement = (await storedSteeringAcknowledgement(tx, steeringIdentity ?? {
|
|
11668
|
-
companyId: issue.companyId, runId:
|
|
11818
|
+
companyId: issue.companyId, runId: activeRunId, messageId: commentId,
|
|
11669
11819
|
})) ??
|
|
11670
11820
|
(await steerNativeSession({
|
|
11671
|
-
runId:
|
|
11672
|
-
message:
|
|
11821
|
+
runId: activeRunId,
|
|
11822
|
+
message: steeringExposesLowTrustRaw
|
|
11823
|
+
? entry.comment.body
|
|
11824
|
+
: sanitizeQuarantinedCommentForHigherTrust({
|
|
11825
|
+
body: entry.comment.body,
|
|
11826
|
+
sourceTrust: entry.comment.sourceTrust ?? null,
|
|
11827
|
+
}).body,
|
|
11673
11828
|
correlationId: commentId,
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11829
|
+
timeoutMs: input.timeoutMs,
|
|
11830
|
+
onAcknowledged: ({ turnId }) => settleLateSteeringAcknowledgement({
|
|
11831
|
+
issue,
|
|
11832
|
+
actor,
|
|
11833
|
+
commentId,
|
|
11834
|
+
queueId: input.queueId,
|
|
11835
|
+
runId: activeRunId,
|
|
11836
|
+
turnId,
|
|
11837
|
+
identity: steeringIdentity,
|
|
11838
|
+
}),
|
|
11839
|
+
onUnacknowledged: async () => {
|
|
11840
|
+
if (steeringIdentity)
|
|
11841
|
+
await rejectSteeredIdentity(db, steeringIdentity);
|
|
11842
|
+
},
|
|
11677
11843
|
}));
|
|
11678
11844
|
if (steeringIdentity)
|
|
11679
11845
|
await acceptSteeredIdentity(tx, steeringIdentity);
|
|
11680
11846
|
acknowledgedTurnId = acknowledgement.turnId;
|
|
11681
|
-
const
|
|
11682
|
-
|
|
11683
|
-
.
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
updatedAt: now,
|
|
11697
|
-
})
|
|
11698
|
-
.where(eq(agentWakeupRequests.id, locked.wake.id))
|
|
11699
|
-
.returning()
|
|
11700
|
-
.then((rows) => rows[0] ?? locked.wake);
|
|
11701
|
-
await tx
|
|
11702
|
-
.update(heartbeatRuns)
|
|
11703
|
-
.set({
|
|
11704
|
-
resultJson: {
|
|
11705
|
-
...runResult,
|
|
11706
|
-
queuedSteeringAcknowledgements: {
|
|
11707
|
-
...acknowledgements,
|
|
11708
|
-
[commentId]: {
|
|
11709
|
-
status: "acknowledged",
|
|
11710
|
-
queueId: input.queueId,
|
|
11711
|
-
turnId: acknowledgement.turnId,
|
|
11712
|
-
acknowledgedAt: now.toISOString(),
|
|
11713
|
-
},
|
|
11714
|
-
},
|
|
11715
|
-
},
|
|
11716
|
-
updatedAt: now,
|
|
11717
|
-
})
|
|
11718
|
-
.where(eq(heartbeatRuns.id, locked.activeRun.id));
|
|
11847
|
+
const nextWake = await withdrawSteeredComment({
|
|
11848
|
+
tx,
|
|
11849
|
+
issueId: issue.id,
|
|
11850
|
+
queue: locked.queue,
|
|
11851
|
+
wake: locked.wake,
|
|
11852
|
+
queueRun: null,
|
|
11853
|
+
commentId,
|
|
11854
|
+
});
|
|
11855
|
+
await recordSteeringAcknowledgement({
|
|
11856
|
+
tx,
|
|
11857
|
+
runId: activeRunId,
|
|
11858
|
+
commentId,
|
|
11859
|
+
queueId: input.queueId,
|
|
11860
|
+
turnId: acknowledgement.turnId,
|
|
11861
|
+
});
|
|
11719
11862
|
return buildQueuedCommentQueue({
|
|
11720
11863
|
executor: tx,
|
|
11721
11864
|
issue,
|
|
@@ -11734,22 +11877,96 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11734
11877
|
if (steeringIdentity && !uncertain)
|
|
11735
11878
|
await rejectSteeredIdentity(db, steeringIdentity);
|
|
11736
11879
|
if (error instanceof NativeSessionSteeringError) {
|
|
11737
|
-
throw conflict(error.message, {
|
|
11880
|
+
throw conflict(error.message, {
|
|
11881
|
+
code: error.code,
|
|
11882
|
+
retryable: true,
|
|
11883
|
+
...(error.detail ? { detail: error.detail } : {}),
|
|
11884
|
+
});
|
|
11738
11885
|
}
|
|
11739
11886
|
throw error;
|
|
11740
11887
|
}
|
|
11741
11888
|
return { queue, duplicate, acknowledgedTurnId };
|
|
11742
11889
|
}
|
|
11890
|
+
function steeringErrorCode(error) {
|
|
11891
|
+
if (!(error instanceof HttpError))
|
|
11892
|
+
return null;
|
|
11893
|
+
const details = readObject(error.details);
|
|
11894
|
+
return typeof details.code === "string" ? details.code : null;
|
|
11895
|
+
}
|
|
11896
|
+
async function resolveQueuedSteeringTarget(input) {
|
|
11897
|
+
try {
|
|
11898
|
+
const issue = await svc.getById(input.issue.id);
|
|
11899
|
+
const activeRun = issue ? await resolveActiveIssueRun(issue) : null;
|
|
11900
|
+
const queue = await buildQueuedCommentQueue({
|
|
11901
|
+
executor: db,
|
|
11902
|
+
issue: issue ?? input.issue,
|
|
11903
|
+
activeRun,
|
|
11904
|
+
actor: input.actor,
|
|
11905
|
+
steeringDisposition: "temporarily_unavailable",
|
|
11906
|
+
});
|
|
11907
|
+
const entry = queue.entries.find((candidate) => candidate.comment.id === input.commentId);
|
|
11908
|
+
if (entry && queue.protocol !== "paperclip_runner_v1") {
|
|
11909
|
+
return { kind: "unavailable", reason: "legacy_protocol" };
|
|
11910
|
+
}
|
|
11911
|
+
return entry && queue.queueId && queue.targetRunId
|
|
11912
|
+
? {
|
|
11913
|
+
kind: "target",
|
|
11914
|
+
target: { queueId: queue.queueId, targetRunId: queue.targetRunId, revision: queue.revision },
|
|
11915
|
+
}
|
|
11916
|
+
: { kind: "none" };
|
|
11917
|
+
}
|
|
11918
|
+
catch (err) {
|
|
11919
|
+
logger.warn({ err, issueId: input.issue.id, commentId: input.commentId }, "failed to resolve the steering target for a posted comment");
|
|
11920
|
+
return { kind: "unavailable", reason: "steering_failed" };
|
|
11921
|
+
}
|
|
11922
|
+
}
|
|
11923
|
+
async function steerQueuedCommentWithRetry(input) {
|
|
11924
|
+
const deadline = Date.now() + steeringRetry.budgetMs;
|
|
11925
|
+
let target = input.target;
|
|
11926
|
+
for (let attempt = 0;; attempt += 1) {
|
|
11927
|
+
try {
|
|
11928
|
+
return await steerQueuedCommentInTransaction({
|
|
11929
|
+
issue: input.issue,
|
|
11930
|
+
actor: input.actor,
|
|
11931
|
+
commentId: input.commentId,
|
|
11932
|
+
queueId: target.queueId,
|
|
11933
|
+
targetRunId: target.targetRunId,
|
|
11934
|
+
revision: target.revision,
|
|
11935
|
+
timeoutMs: Math.max(steeringRetry.minAttemptMs, deadline - Date.now()),
|
|
11936
|
+
});
|
|
11937
|
+
}
|
|
11938
|
+
catch (error) {
|
|
11939
|
+
const code = steeringErrorCode(error);
|
|
11940
|
+
const delayMs = steeringRetry.delaysMs[attempt];
|
|
11941
|
+
const retryable = code !== null &&
|
|
11942
|
+
(RETRYABLE_STEERING_CODES[code] === true ||
|
|
11943
|
+
(input.targetIsServerResolved && code === "queued_comment_revision_conflict"));
|
|
11944
|
+
if (!retryable ||
|
|
11945
|
+
delayMs === undefined ||
|
|
11946
|
+
deadline - Date.now() - delayMs < steeringRetry.minAttemptMs) {
|
|
11947
|
+
throw error;
|
|
11948
|
+
}
|
|
11949
|
+
logger.warn({
|
|
11950
|
+
issueId: input.issue.id,
|
|
11951
|
+
commentId: input.commentId,
|
|
11952
|
+
targetRunId: target.targetRunId,
|
|
11953
|
+
attempt: attempt + 1,
|
|
11954
|
+
code,
|
|
11955
|
+
}, "retrying a transiently refused steer");
|
|
11956
|
+
await sleep(delayMs);
|
|
11957
|
+
const next = await resolveQueuedSteeringTarget(input);
|
|
11958
|
+
if (next.kind !== "target" || next.target.targetRunId !== target.targetRunId)
|
|
11959
|
+
throw error;
|
|
11960
|
+
target = next.target;
|
|
11961
|
+
}
|
|
11962
|
+
}
|
|
11963
|
+
}
|
|
11743
11964
|
function steeringUnavailableFromError(error) {
|
|
11744
|
-
const code = error
|
|
11745
|
-
error.details &&
|
|
11746
|
-
typeof error.details === "object" &&
|
|
11747
|
-
!Array.isArray(error.details) &&
|
|
11748
|
-
typeof error.details.code === "string"
|
|
11749
|
-
? error.details.code
|
|
11750
|
-
: null;
|
|
11965
|
+
const code = steeringErrorCode(error);
|
|
11751
11966
|
if (code === "steering_unsupported")
|
|
11752
11967
|
return "legacy_protocol";
|
|
11968
|
+
if (code === "steering_identity_mismatch")
|
|
11969
|
+
return "identity_mismatch";
|
|
11753
11970
|
if (code === "queued_comment_stale_target" ||
|
|
11754
11971
|
code === "queued_comment_not_pending" ||
|
|
11755
11972
|
code === "queued_comment_already_dispatching" ||
|
|
@@ -11775,6 +11992,25 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11775
11992
|
.limit(1);
|
|
11776
11993
|
return !earlier;
|
|
11777
11994
|
}
|
|
11995
|
+
async function agentSteeringBlock(input) {
|
|
11996
|
+
const [run] = await db
|
|
11997
|
+
.select({ agentId: heartbeatRuns.agentId, responsibleUserId: heartbeatRuns.responsibleUserId })
|
|
11998
|
+
.from(heartbeatRuns)
|
|
11999
|
+
.where(and(eq(heartbeatRuns.id, input.targetRunId), eq(heartbeatRuns.companyId, input.companyId)));
|
|
12000
|
+
if (!run)
|
|
12001
|
+
return "no_active_run";
|
|
12002
|
+
if (input.actorAgentId && run.agentId === input.actorAgentId)
|
|
12003
|
+
return "board_only";
|
|
12004
|
+
const [comment] = await db
|
|
12005
|
+
.select({ authorUserId: issueComments.authorUserId, onBehalfOfUserId: issueComments.onBehalfOfUserId })
|
|
12006
|
+
.from(issueComments)
|
|
12007
|
+
.where(and(eq(issueComments.id, input.commentId), eq(issueComments.companyId, input.companyId)));
|
|
12008
|
+
if (!comment)
|
|
12009
|
+
return "steering_failed";
|
|
12010
|
+
const matchesRunIdentity = comment.authorUserId !== null ||
|
|
12011
|
+
(comment.onBehalfOfUserId !== null && comment.onBehalfOfUserId === run.responsibleUserId);
|
|
12012
|
+
return matchesRunIdentity ? null : "identity_mismatch";
|
|
12013
|
+
}
|
|
11778
12014
|
/**
|
|
11779
12015
|
* Resolve how a freshly posted message was actually delivered, and steer it
|
|
11780
12016
|
* when the resolved mode is `"steer"`.
|
|
@@ -11789,7 +12025,11 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11789
12025
|
if (effective === "queue") {
|
|
11790
12026
|
return { deliveredAs: "queued", steeringUnavailable: "not_requested" };
|
|
11791
12027
|
}
|
|
11792
|
-
if (input.actor.actorType
|
|
12028
|
+
if (input.actor.actorType !== "agent" && !input.boardUserId) {
|
|
12029
|
+
return { deliveredAs: "queued", steeringUnavailable: "board_only" };
|
|
12030
|
+
}
|
|
12031
|
+
if (input.actor.actorType === "agent" &&
|
|
12032
|
+
(!input.actor.agentId || input.actor.agentId === input.issue.assigneeAgentId)) {
|
|
11793
12033
|
return { deliveredAs: "queued", steeringUnavailable: "board_only" };
|
|
11794
12034
|
}
|
|
11795
12035
|
if (input.issue.conversationAgentId) {
|
|
@@ -11806,45 +12046,35 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11806
12046
|
catch (err) {
|
|
11807
12047
|
logger.warn({ err, issueId: input.issue.id, commentId: input.commentId }, "failed to complete the comment wake dispatch before steering");
|
|
11808
12048
|
}
|
|
11809
|
-
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
const activeRun = issue ? await resolveActiveIssueRun(issue) : null;
|
|
11813
|
-
const queue = await buildQueuedCommentQueue({
|
|
11814
|
-
executor: db,
|
|
11815
|
-
issue: issue ?? input.issue,
|
|
11816
|
-
activeRun,
|
|
11817
|
-
actor: input.actor,
|
|
11818
|
-
steeringDisposition: "temporarily_unavailable",
|
|
11819
|
-
});
|
|
11820
|
-
const entry = queue.entries.find((candidate) => candidate.comment.id === input.commentId);
|
|
11821
|
-
if (entry && queue.protocol !== "paperclip_runner_v1") {
|
|
11822
|
-
return { deliveredAs: "queued", steeringUnavailable: "legacy_protocol" };
|
|
11823
|
-
}
|
|
11824
|
-
target =
|
|
11825
|
-
entry && queue.queueId && queue.targetRunId
|
|
11826
|
-
? { queueId: queue.queueId, targetRunId: queue.targetRunId, revision: queue.revision }
|
|
11827
|
-
: null;
|
|
11828
|
-
}
|
|
11829
|
-
catch (err) {
|
|
11830
|
-
logger.warn({ err, issueId: input.issue.id, commentId: input.commentId }, "failed to resolve the steering target for a posted comment");
|
|
11831
|
-
return { deliveredAs: "queued", steeringUnavailable: "steering_failed" };
|
|
12049
|
+
const resolved = await resolveQueuedSteeringTarget(input);
|
|
12050
|
+
if (resolved.kind === "unavailable") {
|
|
12051
|
+
return { deliveredAs: "queued", steeringUnavailable: resolved.reason };
|
|
11832
12052
|
}
|
|
12053
|
+
const target = resolved.kind === "target" ? resolved.target : null;
|
|
11833
12054
|
if (!target) {
|
|
11834
12055
|
return {
|
|
11835
12056
|
deliveredAs: "queued",
|
|
11836
12057
|
steeringUnavailable: input.requested === "steer" ? "no_active_run" : "not_requested",
|
|
11837
12058
|
};
|
|
11838
12059
|
}
|
|
12060
|
+
if (input.actor.actorType === "agent") {
|
|
12061
|
+
const blocked = await agentSteeringBlock({
|
|
12062
|
+
companyId: input.issue.companyId,
|
|
12063
|
+
commentId: input.commentId,
|
|
12064
|
+
actorAgentId: input.actor.agentId ?? null,
|
|
12065
|
+
targetRunId: target.targetRunId,
|
|
12066
|
+
});
|
|
12067
|
+
if (blocked)
|
|
12068
|
+
return { deliveredAs: "queued", steeringUnavailable: blocked };
|
|
12069
|
+
}
|
|
11839
12070
|
let steered;
|
|
11840
12071
|
try {
|
|
11841
|
-
steered = await
|
|
12072
|
+
steered = await steerQueuedCommentWithRetry({
|
|
11842
12073
|
issue: input.issue,
|
|
11843
12074
|
actor: input.actor,
|
|
11844
12075
|
commentId: input.commentId,
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
revision: target.revision,
|
|
12076
|
+
target,
|
|
12077
|
+
targetIsServerResolved: true,
|
|
11848
12078
|
});
|
|
11849
12079
|
}
|
|
11850
12080
|
catch (error) {
|
|
@@ -11905,13 +12135,16 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11905
12135
|
if (!decision.allowed)
|
|
11906
12136
|
throw forbidden(decision.explanation, authorizationDeniedDetails(decision));
|
|
11907
12137
|
const actor = getActorInfo(req);
|
|
11908
|
-
const { queue, duplicate, acknowledgedTurnId } = await
|
|
12138
|
+
const { queue, duplicate, acknowledgedTurnId } = await steerQueuedCommentWithRetry({
|
|
11909
12139
|
issue,
|
|
11910
12140
|
actor,
|
|
11911
12141
|
commentId,
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
12142
|
+
target: {
|
|
12143
|
+
queueId: req.body.queueId,
|
|
12144
|
+
targetRunId: req.body.targetRunId,
|
|
12145
|
+
revision: req.body.revision,
|
|
12146
|
+
},
|
|
12147
|
+
targetIsServerResolved: false,
|
|
11915
12148
|
});
|
|
11916
12149
|
await logQueuedCommentSteered({
|
|
11917
12150
|
issue,
|