@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.
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 +266 -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-DbUeI7Fn.js} +2 -2
  44. package/ui-dist/assets/{CompanyActivity.production-Cr4M1ZEK.js → CompanyActivity.production-ah_9HNB7.js} +1 -1
  45. package/ui-dist/assets/{CompanyExport-p04ns6Xl.js → CompanyExport-DfQ2w0Io.js} +1 -1
  46. package/ui-dist/assets/{CompanySkills.production-PSbLuKmV.js → CompanySkills.production-D1z4_-0O.js} +1 -1
  47. package/ui-dist/assets/{Costs.production-1J4q1eUe.js → Costs.production-lj6CMOJY.js} +1 -1
  48. package/ui-dist/assets/{OrgChart.production-BNeUJho5.js → OrgChart.production-BYJcahNU.js} +1 -1
  49. package/ui-dist/assets/{RoutineDetail.production-B5RS5N7-.js → RoutineDetail.production-7Lqoltnn.js} +1 -1
  50. package/ui-dist/assets/{Routines.production-Dn91nmsp.js → Routines.production-2drUSpFP.js} +1 -1
  51. package/ui-dist/assets/{definition-D6egFwaP.js → definition-CN1PqJxy.js} +1 -1
  52. package/ui-dist/assets/{index-BqF0txu7.js → index-CFJc7t_k.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,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: locked.activeRun.id, messageId: commentId,
11819
+ companyId: issue.companyId, runId: activeRunId, messageId: commentId,
11686
11820
  })) ??
11687
11821
  (await steerNativeSession({
11688
- runId: locked.activeRun.id,
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
- onAcknowledged: steeringIdentity
11697
- ? () => reconcileSteeredIdentity(db, steeringIdentity)
11698
- : undefined,
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 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));
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, { code: error.code, retryable: true });
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 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;
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
- 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" };
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 steerQueuedCommentInTransaction({
12074
+ steered = await steerQueuedCommentWithRetry({
11899
12075
  issue: input.issue,
11900
12076
  actor: input.actor,
11901
12077
  commentId: input.commentId,
11902
- queueId: target.queueId,
11903
- targetRunId: target.targetRunId,
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 steerQueuedCommentInTransaction({
12140
+ const { queue, duplicate, acknowledgedTurnId } = await steerQueuedCommentWithRetry({
11966
12141
  issue,
11967
12142
  actor,
11968
12143
  commentId,
11969
- queueId: req.body.queueId,
11970
- targetRunId: req.body.targetRunId,
11971
- revision: req.body.revision,
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,