@tickernelz/paperclip-pro-server 2026.930.2 → 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/routes/issues.d.ts +6 -0
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +264 -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-VItybUXr.js} +2 -2
- package/ui-dist/assets/{CompanyActivity.production-Cr4M1ZEK.js → CompanyActivity.production-CwZwgQQV.js} +1 -1
- package/ui-dist/assets/{CompanyExport-p04ns6Xl.js → CompanyExport-DD7Rya_d.js} +1 -1
- package/ui-dist/assets/{CompanySkills.production-PSbLuKmV.js → CompanySkills.production-Bwg9FRuw.js} +1 -1
- package/ui-dist/assets/{Costs.production-1J4q1eUe.js → Costs.production-lULBU1r3.js} +1 -1
- package/ui-dist/assets/{OrgChart.production-BNeUJho5.js → OrgChart.production-C7exT9VZ.js} +1 -1
- package/ui-dist/assets/{RoutineDetail.production-B5RS5N7-.js → RoutineDetail.production-DsZSvOON.js} +1 -1
- package/ui-dist/assets/{Routines.production-Dn91nmsp.js → Routines.production-BsmVAcGo.js} +1 -1
- package/ui-dist/assets/{definition-D6egFwaP.js → definition-RMjpvqXu.js} +1 -1
- package/ui-dist/assets/{index-BqF0txu7.js → index-DU74KtHt.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,127 @@ 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]);
|
|
@@ -11604,7 +11736,6 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11604
11736
|
.select()
|
|
11605
11737
|
.from(heartbeatRuns)
|
|
11606
11738
|
.where(and(eq(heartbeatRuns.id, input.targetRunId), eq(heartbeatRuns.companyId, issue.companyId), eq(heartbeatRuns.agentId, retryWake.agentId)))
|
|
11607
|
-
.for("update")
|
|
11608
11739
|
.limit(1)
|
|
11609
11740
|
.then((rows) => rows[0] ?? null)
|
|
11610
11741
|
: null;
|
|
@@ -11635,6 +11766,7 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11635
11766
|
actor,
|
|
11636
11767
|
queueId: input.queueId,
|
|
11637
11768
|
targetRunId: input.targetRunId,
|
|
11769
|
+
lockActiveRun: false,
|
|
11638
11770
|
});
|
|
11639
11771
|
if (!locked.activeRun) {
|
|
11640
11772
|
throw conflict("The queued message targets a stale run", {
|
|
@@ -11681,11 +11813,12 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11681
11813
|
});
|
|
11682
11814
|
}
|
|
11683
11815
|
steeringDeliveryAttempted = true;
|
|
11816
|
+
const activeRunId = locked.activeRun.id;
|
|
11684
11817
|
const acknowledgement = (await storedSteeringAcknowledgement(tx, steeringIdentity ?? {
|
|
11685
|
-
companyId: issue.companyId, runId:
|
|
11818
|
+
companyId: issue.companyId, runId: activeRunId, messageId: commentId,
|
|
11686
11819
|
})) ??
|
|
11687
11820
|
(await steerNativeSession({
|
|
11688
|
-
runId:
|
|
11821
|
+
runId: activeRunId,
|
|
11689
11822
|
message: steeringExposesLowTrustRaw
|
|
11690
11823
|
? entry.comment.body
|
|
11691
11824
|
: sanitizeQuarantinedCommentForHigherTrust({
|
|
@@ -11693,51 +11826,39 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11693
11826
|
sourceTrust: entry.comment.sourceTrust ?? null,
|
|
11694
11827
|
}).body,
|
|
11695
11828
|
correlationId: commentId,
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
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
|
+
},
|
|
11699
11843
|
}));
|
|
11700
11844
|
if (steeringIdentity)
|
|
11701
11845
|
await acceptSteeredIdentity(tx, steeringIdentity);
|
|
11702
11846
|
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));
|
|
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
|
+
});
|
|
11741
11862
|
return buildQueuedCommentQueue({
|
|
11742
11863
|
executor: tx,
|
|
11743
11864
|
issue,
|
|
@@ -11756,20 +11877,92 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11756
11877
|
if (steeringIdentity && !uncertain)
|
|
11757
11878
|
await rejectSteeredIdentity(db, steeringIdentity);
|
|
11758
11879
|
if (error instanceof NativeSessionSteeringError) {
|
|
11759
|
-
throw conflict(error.message, {
|
|
11880
|
+
throw conflict(error.message, {
|
|
11881
|
+
code: error.code,
|
|
11882
|
+
retryable: true,
|
|
11883
|
+
...(error.detail ? { detail: error.detail } : {}),
|
|
11884
|
+
});
|
|
11760
11885
|
}
|
|
11761
11886
|
throw error;
|
|
11762
11887
|
}
|
|
11763
11888
|
return { queue, duplicate, acknowledgedTurnId };
|
|
11764
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
|
+
}
|
|
11765
11964
|
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;
|
|
11965
|
+
const code = steeringErrorCode(error);
|
|
11773
11966
|
if (code === "steering_unsupported")
|
|
11774
11967
|
return "legacy_protocol";
|
|
11775
11968
|
if (code === "steering_identity_mismatch")
|
|
@@ -11853,30 +12046,11 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11853
12046
|
catch (err) {
|
|
11854
12047
|
logger.warn({ err, issueId: input.issue.id, commentId: input.commentId }, "failed to complete the comment wake dispatch before steering");
|
|
11855
12048
|
}
|
|
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" };
|
|
12049
|
+
const resolved = await resolveQueuedSteeringTarget(input);
|
|
12050
|
+
if (resolved.kind === "unavailable") {
|
|
12051
|
+
return { deliveredAs: "queued", steeringUnavailable: resolved.reason };
|
|
11879
12052
|
}
|
|
12053
|
+
const target = resolved.kind === "target" ? resolved.target : null;
|
|
11880
12054
|
if (!target) {
|
|
11881
12055
|
return {
|
|
11882
12056
|
deliveredAs: "queued",
|
|
@@ -11895,13 +12069,12 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11895
12069
|
}
|
|
11896
12070
|
let steered;
|
|
11897
12071
|
try {
|
|
11898
|
-
steered = await
|
|
12072
|
+
steered = await steerQueuedCommentWithRetry({
|
|
11899
12073
|
issue: input.issue,
|
|
11900
12074
|
actor: input.actor,
|
|
11901
12075
|
commentId: input.commentId,
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
revision: target.revision,
|
|
12076
|
+
target,
|
|
12077
|
+
targetIsServerResolved: true,
|
|
11905
12078
|
});
|
|
11906
12079
|
}
|
|
11907
12080
|
catch (error) {
|
|
@@ -11962,13 +12135,16 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
11962
12135
|
if (!decision.allowed)
|
|
11963
12136
|
throw forbidden(decision.explanation, authorizationDeniedDetails(decision));
|
|
11964
12137
|
const actor = getActorInfo(req);
|
|
11965
|
-
const { queue, duplicate, acknowledgedTurnId } = await
|
|
12138
|
+
const { queue, duplicate, acknowledgedTurnId } = await steerQueuedCommentWithRetry({
|
|
11966
12139
|
issue,
|
|
11967
12140
|
actor,
|
|
11968
12141
|
commentId,
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
12142
|
+
target: {
|
|
12143
|
+
queueId: req.body.queueId,
|
|
12144
|
+
targetRunId: req.body.targetRunId,
|
|
12145
|
+
revision: req.body.revision,
|
|
12146
|
+
},
|
|
12147
|
+
targetIsServerResolved: false,
|
|
11972
12148
|
});
|
|
11973
12149
|
await logQueuedCommentSteered({
|
|
11974
12150
|
issue,
|