@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.
Files changed (54) hide show
  1. package/dist/build-info.json +1 -1
  2. package/dist/modules/run-dispatch/adapters/postgres.d.ts +5 -1
  3. package/dist/modules/run-dispatch/adapters/postgres.d.ts.map +1 -1
  4. package/dist/modules/run-dispatch/adapters/postgres.js +2 -1
  5. package/dist/modules/run-dispatch/adapters/postgres.js.map +1 -1
  6. package/dist/modules/run-dispatch/index.d.ts +2 -0
  7. package/dist/modules/run-dispatch/index.d.ts.map +1 -1
  8. package/dist/modules/run-dispatch/index.js +1 -1
  9. package/dist/modules/run-dispatch/index.js.map +1 -1
  10. package/dist/routes/issues.d.ts +6 -0
  11. package/dist/routes/issues.d.ts.map +1 -1
  12. package/dist/routes/issues.js +264 -88
  13. package/dist/routes/issues.js.map +1 -1
  14. package/dist/services/heartbeat.d.ts +1 -0
  15. package/dist/services/heartbeat.d.ts.map +1 -1
  16. package/dist/services/heartbeat.js +100 -20
  17. package/dist/services/heartbeat.js.map +1 -1
  18. package/dist/services/native-runtime/native-session-executor.d.ts +7 -2
  19. package/dist/services/native-runtime/native-session-executor.d.ts.map +1 -1
  20. package/dist/services/native-runtime/native-session-executor.js +35 -14
  21. package/dist/services/native-runtime/native-session-executor.js.map +1 -1
  22. package/dist/services/native-runtime/native-session-executor.test.js +25 -2
  23. package/dist/services/native-runtime/native-session-executor.test.js.map +1 -1
  24. package/dist/services/recovery/service.d.ts +1 -0
  25. package/dist/services/recovery/service.d.ts.map +1 -1
  26. package/dist/services/recovery/service.js +32 -0
  27. package/dist/services/recovery/service.js.map +1 -1
  28. package/dist/services/run-identity.d.ts.map +1 -1
  29. package/dist/services/run-identity.js +1 -1
  30. package/dist/services/run-identity.js.map +1 -1
  31. package/dist/services/steering-acknowledgements.d.ts +1 -0
  32. package/dist/services/steering-acknowledgements.d.ts.map +1 -1
  33. package/dist/services/steering-acknowledgements.js +6 -0
  34. package/dist/services/steering-acknowledgements.js.map +1 -1
  35. package/dist/services/systemd-notify.d.ts.map +1 -1
  36. package/dist/services/systemd-notify.js +6 -2
  37. package/dist/services/systemd-notify.js.map +1 -1
  38. package/dist/services/systemd-notify.test.d.ts +2 -0
  39. package/dist/services/systemd-notify.test.d.ts.map +1 -0
  40. package/dist/services/systemd-notify.test.js +35 -0
  41. package/dist/services/systemd-notify.test.js.map +1 -0
  42. package/package.json +21 -21
  43. package/ui-dist/assets/{Agents.production-T8untZn6.js → Agents.production-VItybUXr.js} +2 -2
  44. package/ui-dist/assets/{CompanyActivity.production-Cr4M1ZEK.js → CompanyActivity.production-CwZwgQQV.js} +1 -1
  45. package/ui-dist/assets/{CompanyExport-p04ns6Xl.js → CompanyExport-DD7Rya_d.js} +1 -1
  46. package/ui-dist/assets/{CompanySkills.production-PSbLuKmV.js → CompanySkills.production-Bwg9FRuw.js} +1 -1
  47. package/ui-dist/assets/{Costs.production-1J4q1eUe.js → Costs.production-lULBU1r3.js} +1 -1
  48. package/ui-dist/assets/{OrgChart.production-BNeUJho5.js → OrgChart.production-C7exT9VZ.js} +1 -1
  49. package/ui-dist/assets/{RoutineDetail.production-B5RS5N7-.js → RoutineDetail.production-DsZSvOON.js} +1 -1
  50. package/ui-dist/assets/{Routines.production-Dn91nmsp.js → Routines.production-BsmVAcGo.js} +1 -1
  51. package/ui-dist/assets/{definition-D6egFwaP.js → definition-RMjpvqXu.js} +1 -1
  52. package/ui-dist/assets/{index-BqF0txu7.js → index-DU74KtHt.js} +4 -4
  53. package/ui-dist/index.html +1 -1
  54. package/ui-dist/sw.js +1 -1
@@ -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, reconcileSteeredIdentity, reserveSteeredIdentity, acceptSteeredIdentity, rejectSteeredIdentity, } from "../services/run-identity.js";
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 activeRun = activeRunId
4818
- ? await input.tx
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: locked.activeRun.id, messageId: commentId,
11818
+ companyId: issue.companyId, runId: activeRunId, messageId: commentId,
11686
11819
  })) ??
11687
11820
  (await steerNativeSession({
11688
- runId: locked.activeRun.id,
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
- onAcknowledged: steeringIdentity
11697
- ? () => reconcileSteeredIdentity(db, steeringIdentity)
11698
- : undefined,
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 remainingIds = locked.queue.entries
11704
- .map((candidate) => candidate.comment.id)
11705
- .filter((candidateId) => candidateId !== commentId);
11706
- const now = new Date();
11707
- const nextWake = remainingIds.length === 0
11708
- ? await tx
11709
- .update(agentWakeupRequests)
11710
- .set({ status: "cancelled", finishedAt: now, updatedAt: now })
11711
- .where(eq(agentWakeupRequests.id, locked.wake.id))
11712
- .returning()
11713
- .then(() => null)
11714
- : await tx
11715
- .update(agentWakeupRequests)
11716
- .set({
11717
- payload: withQueuedCommentIdsInWakePayload(locked.wake.payload, remainingIds),
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, { code: error.code, retryable: true });
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 instanceof HttpError &&
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
- let target = null;
11857
- try {
11858
- const issue = await svc.getById(input.issue.id);
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 steerQueuedCommentInTransaction({
12072
+ steered = await steerQueuedCommentWithRetry({
11899
12073
  issue: input.issue,
11900
12074
  actor: input.actor,
11901
12075
  commentId: input.commentId,
11902
- queueId: target.queueId,
11903
- targetRunId: target.targetRunId,
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 steerQueuedCommentInTransaction({
12138
+ const { queue, duplicate, acknowledgedTurnId } = await steerQueuedCommentWithRetry({
11966
12139
  issue,
11967
12140
  actor,
11968
12141
  commentId,
11969
- queueId: req.body.queueId,
11970
- targetRunId: req.body.targetRunId,
11971
- revision: req.body.revision,
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,