@tickernelz/paperclip-pro-server 2026.930.2 → 2026.930.4
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/routes/issues.d.ts +6 -0
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +266 -88
- package/dist/routes/issues.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.map +1 -1
- package/dist/services/run-identity.js +1 -1
- 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/ui-dist/assets/{Agents.production-T8untZn6.js → Agents.production-DbUeI7Fn.js} +2 -2
- package/ui-dist/assets/{CompanyActivity.production-Cr4M1ZEK.js → CompanyActivity.production-ah_9HNB7.js} +1 -1
- package/ui-dist/assets/{CompanyExport-p04ns6Xl.js → CompanyExport-DfQ2w0Io.js} +1 -1
- package/ui-dist/assets/{CompanySkills.production-PSbLuKmV.js → CompanySkills.production-D1z4_-0O.js} +1 -1
- package/ui-dist/assets/{Costs.production-1J4q1eUe.js → Costs.production-lj6CMOJY.js} +1 -1
- package/ui-dist/assets/{OrgChart.production-BNeUJho5.js → OrgChart.production-BYJcahNU.js} +1 -1
- package/ui-dist/assets/{RoutineDetail.production-B5RS5N7-.js → RoutineDetail.production-7Lqoltnn.js} +1 -1
- package/ui-dist/assets/{Routines.production-Dn91nmsp.js → Routines.production-2drUSpFP.js} +1 -1
- package/ui-dist/assets/{definition-D6egFwaP.js → definition-CN1PqJxy.js} +1 -1
- package/ui-dist/assets/{index-BqF0txu7.js → index-CFJc7t_k.js} +4 -4
- package/ui-dist/index.html +1 -1
- 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,6 +11560,128 @@ 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
|
+
queuedInteractionId(pending.payload) !== input.commentId)
|
|
11653
|
+
continue;
|
|
11654
|
+
const locked = await lockQueuedCommentState({
|
|
11655
|
+
tx,
|
|
11656
|
+
issue: input.issue,
|
|
11657
|
+
actor: input.actor,
|
|
11658
|
+
queueId: pending.id,
|
|
11659
|
+
lockActiveRun: false,
|
|
11660
|
+
}).catch((error) => {
|
|
11661
|
+
if (error instanceof HttpError)
|
|
11662
|
+
return null;
|
|
11663
|
+
throw error;
|
|
11664
|
+
});
|
|
11665
|
+
if (!locked?.queue.entries.some((entry) => entry.comment.id === input.commentId))
|
|
11666
|
+
continue;
|
|
11667
|
+
await withdrawSteeredComment({
|
|
11668
|
+
tx,
|
|
11669
|
+
issueId: input.issue.id,
|
|
11670
|
+
queue: locked.queue,
|
|
11671
|
+
wake: locked.wake,
|
|
11672
|
+
queueRun: locked.queueRun,
|
|
11673
|
+
commentId: input.commentId,
|
|
11674
|
+
});
|
|
11675
|
+
}
|
|
11676
|
+
await recordSteeringAcknowledgement({
|
|
11677
|
+
tx,
|
|
11678
|
+
runId: input.runId,
|
|
11679
|
+
commentId: input.commentId,
|
|
11680
|
+
queueId: input.queueId,
|
|
11681
|
+
turnId: input.turnId,
|
|
11682
|
+
});
|
|
11683
|
+
});
|
|
11684
|
+
}
|
|
11552
11685
|
async function steerQueuedCommentInTransaction(input) {
|
|
11553
11686
|
const { issue, actor, commentId } = input;
|
|
11554
11687
|
const responseWake = await db.select().from(agentWakeupRequests).where(and(eq(agentWakeupRequests.id, input.queueId), eq(agentWakeupRequests.companyId, issue.companyId))).then(rows => rows[0]);
|
|
@@ -11604,7 +11737,6 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11604
11737
|
.select()
|
|
11605
11738
|
.from(heartbeatRuns)
|
|
11606
11739
|
.where(and(eq(heartbeatRuns.id, input.targetRunId), eq(heartbeatRuns.companyId, issue.companyId), eq(heartbeatRuns.agentId, retryWake.agentId)))
|
|
11607
|
-
.for("update")
|
|
11608
11740
|
.limit(1)
|
|
11609
11741
|
.then((rows) => rows[0] ?? null)
|
|
11610
11742
|
: null;
|
|
@@ -11635,6 +11767,7 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11635
11767
|
actor,
|
|
11636
11768
|
queueId: input.queueId,
|
|
11637
11769
|
targetRunId: input.targetRunId,
|
|
11770
|
+
lockActiveRun: false,
|
|
11638
11771
|
});
|
|
11639
11772
|
if (!locked.activeRun) {
|
|
11640
11773
|
throw conflict("The queued message targets a stale run", {
|
|
@@ -11681,11 +11814,12 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11681
11814
|
});
|
|
11682
11815
|
}
|
|
11683
11816
|
steeringDeliveryAttempted = true;
|
|
11817
|
+
const activeRunId = locked.activeRun.id;
|
|
11684
11818
|
const acknowledgement = (await storedSteeringAcknowledgement(tx, steeringIdentity ?? {
|
|
11685
|
-
companyId: issue.companyId, runId:
|
|
11819
|
+
companyId: issue.companyId, runId: activeRunId, messageId: commentId,
|
|
11686
11820
|
})) ??
|
|
11687
11821
|
(await steerNativeSession({
|
|
11688
|
-
runId:
|
|
11822
|
+
runId: activeRunId,
|
|
11689
11823
|
message: steeringExposesLowTrustRaw
|
|
11690
11824
|
? entry.comment.body
|
|
11691
11825
|
: sanitizeQuarantinedCommentForHigherTrust({
|
|
@@ -11693,51 +11827,39 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11693
11827
|
sourceTrust: entry.comment.sourceTrust ?? null,
|
|
11694
11828
|
}).body,
|
|
11695
11829
|
correlationId: commentId,
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
11830
|
+
timeoutMs: input.timeoutMs,
|
|
11831
|
+
onAcknowledged: ({ turnId }) => settleLateSteeringAcknowledgement({
|
|
11832
|
+
issue,
|
|
11833
|
+
actor,
|
|
11834
|
+
commentId,
|
|
11835
|
+
queueId: input.queueId,
|
|
11836
|
+
runId: activeRunId,
|
|
11837
|
+
turnId,
|
|
11838
|
+
identity: steeringIdentity,
|
|
11839
|
+
}),
|
|
11840
|
+
onUnacknowledged: async () => {
|
|
11841
|
+
if (steeringIdentity)
|
|
11842
|
+
await rejectSteeredIdentity(db, steeringIdentity);
|
|
11843
|
+
},
|
|
11699
11844
|
}));
|
|
11700
11845
|
if (steeringIdentity)
|
|
11701
11846
|
await acceptSteeredIdentity(tx, steeringIdentity);
|
|
11702
11847
|
acknowledgedTurnId = acknowledgement.turnId;
|
|
11703
|
-
const
|
|
11704
|
-
|
|
11705
|
-
.
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
updatedAt: now,
|
|
11719
|
-
})
|
|
11720
|
-
.where(eq(agentWakeupRequests.id, locked.wake.id))
|
|
11721
|
-
.returning()
|
|
11722
|
-
.then((rows) => rows[0] ?? locked.wake);
|
|
11723
|
-
await tx
|
|
11724
|
-
.update(heartbeatRuns)
|
|
11725
|
-
.set({
|
|
11726
|
-
resultJson: {
|
|
11727
|
-
...runResult,
|
|
11728
|
-
queuedSteeringAcknowledgements: {
|
|
11729
|
-
...acknowledgements,
|
|
11730
|
-
[commentId]: {
|
|
11731
|
-
status: "acknowledged",
|
|
11732
|
-
queueId: input.queueId,
|
|
11733
|
-
turnId: acknowledgement.turnId,
|
|
11734
|
-
acknowledgedAt: now.toISOString(),
|
|
11735
|
-
},
|
|
11736
|
-
},
|
|
11737
|
-
},
|
|
11738
|
-
updatedAt: now,
|
|
11739
|
-
})
|
|
11740
|
-
.where(eq(heartbeatRuns.id, locked.activeRun.id));
|
|
11848
|
+
const nextWake = await withdrawSteeredComment({
|
|
11849
|
+
tx,
|
|
11850
|
+
issueId: issue.id,
|
|
11851
|
+
queue: locked.queue,
|
|
11852
|
+
wake: locked.wake,
|
|
11853
|
+
queueRun: null,
|
|
11854
|
+
commentId,
|
|
11855
|
+
});
|
|
11856
|
+
await recordSteeringAcknowledgement({
|
|
11857
|
+
tx,
|
|
11858
|
+
runId: activeRunId,
|
|
11859
|
+
commentId,
|
|
11860
|
+
queueId: input.queueId,
|
|
11861
|
+
turnId: acknowledgement.turnId,
|
|
11862
|
+
});
|
|
11741
11863
|
return buildQueuedCommentQueue({
|
|
11742
11864
|
executor: tx,
|
|
11743
11865
|
issue,
|
|
@@ -11756,20 +11878,93 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11756
11878
|
if (steeringIdentity && !uncertain)
|
|
11757
11879
|
await rejectSteeredIdentity(db, steeringIdentity);
|
|
11758
11880
|
if (error instanceof NativeSessionSteeringError) {
|
|
11759
|
-
throw conflict(error.message, {
|
|
11881
|
+
throw conflict(error.message, {
|
|
11882
|
+
code: error.code,
|
|
11883
|
+
retryable: true,
|
|
11884
|
+
...(error.detail ? { detail: error.detail } : {}),
|
|
11885
|
+
});
|
|
11760
11886
|
}
|
|
11761
11887
|
throw error;
|
|
11762
11888
|
}
|
|
11763
11889
|
return { queue, duplicate, acknowledgedTurnId };
|
|
11764
11890
|
}
|
|
11891
|
+
function steeringErrorCode(error) {
|
|
11892
|
+
if (!(error instanceof HttpError))
|
|
11893
|
+
return null;
|
|
11894
|
+
const details = readObject(error.details);
|
|
11895
|
+
return typeof details.code === "string" ? details.code : null;
|
|
11896
|
+
}
|
|
11897
|
+
async function resolveQueuedSteeringTarget(input) {
|
|
11898
|
+
try {
|
|
11899
|
+
const issue = await svc.getById(input.issue.id);
|
|
11900
|
+
const activeRun = issue ? await resolveActiveIssueRun(issue) : null;
|
|
11901
|
+
const queue = await buildQueuedCommentQueue({
|
|
11902
|
+
executor: db,
|
|
11903
|
+
issue: issue ?? input.issue,
|
|
11904
|
+
activeRun,
|
|
11905
|
+
actor: input.actor,
|
|
11906
|
+
steeringDisposition: "temporarily_unavailable",
|
|
11907
|
+
});
|
|
11908
|
+
const entry = queue.entries.find((candidate) => candidate.comment.id === input.commentId);
|
|
11909
|
+
if (entry && queue.protocol !== "paperclip_runner_v1") {
|
|
11910
|
+
return { kind: "unavailable", reason: "legacy_protocol" };
|
|
11911
|
+
}
|
|
11912
|
+
return entry && queue.queueId && queue.targetRunId
|
|
11913
|
+
? {
|
|
11914
|
+
kind: "target",
|
|
11915
|
+
target: { queueId: queue.queueId, targetRunId: queue.targetRunId, revision: queue.revision },
|
|
11916
|
+
}
|
|
11917
|
+
: { kind: "none" };
|
|
11918
|
+
}
|
|
11919
|
+
catch (err) {
|
|
11920
|
+
logger.warn({ err, issueId: input.issue.id, commentId: input.commentId }, "failed to resolve the steering target for a posted comment");
|
|
11921
|
+
return { kind: "unavailable", reason: "steering_failed" };
|
|
11922
|
+
}
|
|
11923
|
+
}
|
|
11924
|
+
async function steerQueuedCommentWithRetry(input) {
|
|
11925
|
+
const deadline = Date.now() + steeringRetry.budgetMs;
|
|
11926
|
+
let target = input.target;
|
|
11927
|
+
for (let attempt = 0;; attempt += 1) {
|
|
11928
|
+
try {
|
|
11929
|
+
return await steerQueuedCommentInTransaction({
|
|
11930
|
+
issue: input.issue,
|
|
11931
|
+
actor: input.actor,
|
|
11932
|
+
commentId: input.commentId,
|
|
11933
|
+
queueId: target.queueId,
|
|
11934
|
+
targetRunId: target.targetRunId,
|
|
11935
|
+
revision: target.revision,
|
|
11936
|
+
timeoutMs: Math.max(steeringRetry.minAttemptMs, deadline - Date.now()),
|
|
11937
|
+
});
|
|
11938
|
+
}
|
|
11939
|
+
catch (error) {
|
|
11940
|
+
const code = steeringErrorCode(error);
|
|
11941
|
+
const delayMs = steeringRetry.delaysMs[attempt];
|
|
11942
|
+
const retryable = code !== null &&
|
|
11943
|
+
(RETRYABLE_STEERING_CODES[code] === true ||
|
|
11944
|
+
(input.targetIsServerResolved && code === "queued_comment_revision_conflict"));
|
|
11945
|
+
if (!retryable ||
|
|
11946
|
+
delayMs === undefined ||
|
|
11947
|
+
deadline - Date.now() - delayMs < steeringRetry.minAttemptMs) {
|
|
11948
|
+
throw error;
|
|
11949
|
+
}
|
|
11950
|
+
logger.warn({
|
|
11951
|
+
issueId: input.issue.id,
|
|
11952
|
+
commentId: input.commentId,
|
|
11953
|
+
targetRunId: target.targetRunId,
|
|
11954
|
+
attempt: attempt + 1,
|
|
11955
|
+
code,
|
|
11956
|
+
}, "retrying a transiently refused steer");
|
|
11957
|
+
await sleep(delayMs);
|
|
11958
|
+
const next = await resolveQueuedSteeringTarget(input);
|
|
11959
|
+
if (next.kind !== "target" || next.target.targetRunId !== target.targetRunId)
|
|
11960
|
+
throw error;
|
|
11961
|
+
if (input.targetIsServerResolved)
|
|
11962
|
+
target = next.target;
|
|
11963
|
+
}
|
|
11964
|
+
}
|
|
11965
|
+
}
|
|
11765
11966
|
function steeringUnavailableFromError(error) {
|
|
11766
|
-
const code = error
|
|
11767
|
-
error.details &&
|
|
11768
|
-
typeof error.details === "object" &&
|
|
11769
|
-
!Array.isArray(error.details) &&
|
|
11770
|
-
typeof error.details.code === "string"
|
|
11771
|
-
? error.details.code
|
|
11772
|
-
: null;
|
|
11967
|
+
const code = steeringErrorCode(error);
|
|
11773
11968
|
if (code === "steering_unsupported")
|
|
11774
11969
|
return "legacy_protocol";
|
|
11775
11970
|
if (code === "steering_identity_mismatch")
|
|
@@ -11853,30 +12048,11 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11853
12048
|
catch (err) {
|
|
11854
12049
|
logger.warn({ err, issueId: input.issue.id, commentId: input.commentId }, "failed to complete the comment wake dispatch before steering");
|
|
11855
12050
|
}
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
const activeRun = issue ? await resolveActiveIssueRun(issue) : null;
|
|
11860
|
-
const queue = await buildQueuedCommentQueue({
|
|
11861
|
-
executor: db,
|
|
11862
|
-
issue: issue ?? input.issue,
|
|
11863
|
-
activeRun,
|
|
11864
|
-
actor: input.actor,
|
|
11865
|
-
steeringDisposition: "temporarily_unavailable",
|
|
11866
|
-
});
|
|
11867
|
-
const entry = queue.entries.find((candidate) => candidate.comment.id === input.commentId);
|
|
11868
|
-
if (entry && queue.protocol !== "paperclip_runner_v1") {
|
|
11869
|
-
return { deliveredAs: "queued", steeringUnavailable: "legacy_protocol" };
|
|
11870
|
-
}
|
|
11871
|
-
target =
|
|
11872
|
-
entry && queue.queueId && queue.targetRunId
|
|
11873
|
-
? { queueId: queue.queueId, targetRunId: queue.targetRunId, revision: queue.revision }
|
|
11874
|
-
: null;
|
|
11875
|
-
}
|
|
11876
|
-
catch (err) {
|
|
11877
|
-
logger.warn({ err, issueId: input.issue.id, commentId: input.commentId }, "failed to resolve the steering target for a posted comment");
|
|
11878
|
-
return { deliveredAs: "queued", steeringUnavailable: "steering_failed" };
|
|
12051
|
+
const resolved = await resolveQueuedSteeringTarget(input);
|
|
12052
|
+
if (resolved.kind === "unavailable") {
|
|
12053
|
+
return { deliveredAs: "queued", steeringUnavailable: resolved.reason };
|
|
11879
12054
|
}
|
|
12055
|
+
const target = resolved.kind === "target" ? resolved.target : null;
|
|
11880
12056
|
if (!target) {
|
|
11881
12057
|
return {
|
|
11882
12058
|
deliveredAs: "queued",
|
|
@@ -11895,13 +12071,12 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11895
12071
|
}
|
|
11896
12072
|
let steered;
|
|
11897
12073
|
try {
|
|
11898
|
-
steered = await
|
|
12074
|
+
steered = await steerQueuedCommentWithRetry({
|
|
11899
12075
|
issue: input.issue,
|
|
11900
12076
|
actor: input.actor,
|
|
11901
12077
|
commentId: input.commentId,
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
revision: target.revision,
|
|
12078
|
+
target,
|
|
12079
|
+
targetIsServerResolved: true,
|
|
11905
12080
|
});
|
|
11906
12081
|
}
|
|
11907
12082
|
catch (error) {
|
|
@@ -11962,13 +12137,16 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11962
12137
|
if (!decision.allowed)
|
|
11963
12138
|
throw forbidden(decision.explanation, authorizationDeniedDetails(decision));
|
|
11964
12139
|
const actor = getActorInfo(req);
|
|
11965
|
-
const { queue, duplicate, acknowledgedTurnId } = await
|
|
12140
|
+
const { queue, duplicate, acknowledgedTurnId } = await steerQueuedCommentWithRetry({
|
|
11966
12141
|
issue,
|
|
11967
12142
|
actor,
|
|
11968
12143
|
commentId,
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
12144
|
+
target: {
|
|
12145
|
+
queueId: req.body.queueId,
|
|
12146
|
+
targetRunId: req.body.targetRunId,
|
|
12147
|
+
revision: req.body.revision,
|
|
12148
|
+
},
|
|
12149
|
+
targetIsServerResolved: false,
|
|
11972
12150
|
});
|
|
11973
12151
|
await logQueuedCommentSteered({
|
|
11974
12152
|
issue,
|