@tickernelz/paperclip-pro-server 2026.929.1 → 2026.929.2

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 (57) hide show
  1. package/dist/build-info.json +1 -1
  2. package/dist/index.js +2 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/middleware/http-log-redaction.d.ts +1 -1
  5. package/dist/middleware/http-log-redaction.d.ts.map +1 -1
  6. package/dist/middleware/http-log-redaction.js +5 -0
  7. package/dist/middleware/http-log-redaction.js.map +1 -1
  8. package/dist/routes/access.d.ts.map +1 -1
  9. package/dist/routes/access.js +6 -0
  10. package/dist/routes/access.js.map +1 -1
  11. package/dist/routes/openapi.d.ts.map +1 -1
  12. package/dist/routes/openapi.js +7 -4
  13. package/dist/routes/openapi.js.map +1 -1
  14. package/dist/services/execution-workspace-policy.d.ts.map +1 -1
  15. package/dist/services/execution-workspace-policy.js +18 -10
  16. package/dist/services/execution-workspace-policy.js.map +1 -1
  17. package/dist/services/execution-workspaces.d.ts.map +1 -1
  18. package/dist/services/execution-workspaces.js +3 -0
  19. package/dist/services/execution-workspaces.js.map +1 -1
  20. package/dist/services/heartbeat.d.ts +1 -0
  21. package/dist/services/heartbeat.d.ts.map +1 -1
  22. package/dist/services/heartbeat.js +79 -7
  23. package/dist/services/heartbeat.js.map +1 -1
  24. package/dist/services/paperclip-cloud-connector.d.ts +1 -1
  25. package/dist/services/paperclip-cloud-connector.d.ts.map +1 -1
  26. package/dist/services/paperclip-cloud-connector.js +55 -2
  27. package/dist/services/paperclip-cloud-connector.js.map +1 -1
  28. package/dist/services/paperclip-cloud-connector.test.js +88 -0
  29. package/dist/services/paperclip-cloud-connector.test.js.map +1 -1
  30. package/dist/services/recovery/service.d.ts +9 -0
  31. package/dist/services/recovery/service.d.ts.map +1 -1
  32. package/dist/services/recovery/service.js +132 -2
  33. package/dist/services/recovery/service.js.map +1 -1
  34. package/dist/services/run-identity.d.ts.map +1 -1
  35. package/dist/services/run-identity.js +12 -6
  36. package/dist/services/run-identity.js.map +1 -1
  37. package/dist/services/work-products.d.ts.map +1 -1
  38. package/dist/services/work-products.js +18 -1
  39. package/dist/services/work-products.js.map +1 -1
  40. package/package.json +21 -21
  41. package/ui-dist/assets/{Agents.production-zgRr_fIZ.js → Agents.production-DXVQbH40.js} +2 -2
  42. package/ui-dist/assets/{CompanyActivity.production-DRpXVqnb.js → CompanyActivity.production-CinmmGVv.js} +1 -1
  43. package/ui-dist/assets/{CompanyExport-BH9rrwu1.js → CompanyExport-DaS-YmqD.js} +1 -1
  44. package/ui-dist/assets/{CompanySkills.production-Bz-JcjC5.js → CompanySkills.production-DayJEg3Z.js} +1 -1
  45. package/ui-dist/assets/{Costs.production-CQTfI_sh.js → Costs.production-qBNyNNRY.js} +1 -1
  46. package/ui-dist/assets/{MarkdownBody-DJQ35hak.js → MarkdownBody-NJwkQWmo.js} +2 -2
  47. package/ui-dist/assets/{OrgChart.production-Cg16xkld.js → OrgChart.production-7-WQ0htv.js} +1 -1
  48. package/ui-dist/assets/{RoutineDetail.production-BHPOgs21.js → RoutineDetail.production-CEF8wtwL.js} +1 -1
  49. package/ui-dist/assets/{RoutineRunVariablesDialog-Bt4KAWCh.js → RoutineRunVariablesDialog-B1KRZJIl.js} +2 -2
  50. package/ui-dist/assets/{Routines.production-Blu3EE_K.js → Routines.production-C15V_czC.js} +1 -1
  51. package/ui-dist/assets/StatusIcon-DgMlA1TU.js +1 -0
  52. package/ui-dist/assets/{definition-rtnhHFhU.js → definition-C_4HgU9U.js} +1 -1
  53. package/ui-dist/assets/{index-CR1_ZQyb.js → index-DMN58MRe.js} +163 -160
  54. package/ui-dist/assets/{index-J-4PQOv1.css → index-Dkjt7SQK.css} +1 -1
  55. package/ui-dist/index.html +5 -5
  56. package/ui-dist/sw.js +1 -1
  57. package/ui-dist/assets/StatusIcon-BuUYMqqU.js +0 -1
@@ -1087,8 +1087,20 @@ export function recoveryService(db, deps) {
1087
1087
  });
1088
1088
  return null;
1089
1089
  }
1090
- if (deps.scheduleRecoveryRetry)
1091
- return deps.scheduleRecoveryRetry(predecessor.id);
1090
+ if (deps.scheduleRecoveryRetry) {
1091
+ const retry = await deps.scheduleRecoveryRetry(predecessor.id);
1092
+ if (retry)
1093
+ return retry;
1094
+ // A spent budget is the one "no retry" the sweeper must not wait
1095
+ // out: nothing else will ever queue a successor for this run, so
1096
+ // it reports the exhaustion instead of leaving the issue with no
1097
+ // live path (2026-09-25: three deploy restarts in a row spent the
1098
+ // budget and the issue sat in_progress with no run, unescalated).
1099
+ if (input.outcome && deps.transientRetryBudgetSpent?.(predecessor)) {
1100
+ input.outcome.retryExhausted = true;
1101
+ }
1102
+ return null;
1103
+ }
1092
1104
  return null;
1093
1105
  }
1094
1106
  }
@@ -2816,6 +2828,7 @@ export function recoveryService(db, deps) {
2816
2828
  successfulContinuationObserved: 0,
2817
2829
  orphanBlockersAssigned: 0,
2818
2830
  successfulRunHandoffEscalated: 0,
2831
+ successfulRunHandoffRetried: 0,
2819
2832
  reviewParticipantRequeued: 0,
2820
2833
  escalated: 0,
2821
2834
  waitingOnReviewResolved: 0,
@@ -3358,6 +3371,7 @@ export function recoveryService(db, deps) {
3358
3371
  result.skipped += 1;
3359
3372
  continue;
3360
3373
  }
3374
+ const reviewOutcome = {};
3361
3375
  const queued = await enqueueStrandedIssueRecovery({
3362
3376
  issueId: issue.id,
3363
3377
  agentId: participantAgentId,
@@ -3370,11 +3384,36 @@ export function recoveryService(db, deps) {
3370
3384
  currentStageType: pendingExecutionState.currentStageType ?? null,
3371
3385
  reviewRecoveryInstruction: "The previous reviewer run ended while this execution-review stage was still pending. Submit the review decision now, or mark the issue blocked with the exact unblock action.",
3372
3386
  },
3387
+ outcome: reviewOutcome,
3373
3388
  });
3374
3389
  if (queued) {
3375
3390
  result.reviewParticipantRequeued += 1;
3376
3391
  result.issueIds.push(issue.id);
3377
3392
  }
3393
+ else if (reviewOutcome.retryExhausted &&
3394
+ !(await hasActiveExecutionPath(issue.companyId, issue.id, null))) {
3395
+ // Same exhaustion as the other lanes: the reviewer run's bounded
3396
+ // retries are spent, so escalate as the review-recovery failure it
3397
+ // is instead of skipping on every sweep with no live path. The
3398
+ // live-path re-read guards the write against a run or wake that
3399
+ // started after the loop's check — for ANY agent, not only the
3400
+ // participant: `blocked` is issue-wide, so another agent still
3401
+ // working the issue must never be blocked under.
3402
+ const updated = await escalateStrandedAssignedIssue({
3403
+ issue,
3404
+ previousStatus: "in_review",
3405
+ latestRun: participantLatestRun,
3406
+ notice: buildExecutionReviewParticipantRecoveryNoticeSeed(),
3407
+ recoveryCause: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_REASON,
3408
+ });
3409
+ if (updated) {
3410
+ result.escalated += 1;
3411
+ result.issueIds.push(issue.id);
3412
+ }
3413
+ else {
3414
+ result.skipped += 1;
3415
+ }
3416
+ }
3378
3417
  else {
3379
3418
  result.skipped += 1;
3380
3419
  }
@@ -3441,6 +3480,7 @@ export function recoveryService(db, deps) {
3441
3480
  result.skipped += 1;
3442
3481
  continue;
3443
3482
  }
3483
+ const dispatchOutcome = {};
3444
3484
  const queued = await enqueueStrandedIssueRecovery({
3445
3485
  issueId: issue.id,
3446
3486
  agentId,
@@ -3448,11 +3488,39 @@ export function recoveryService(db, deps) {
3448
3488
  retryReason: "assignment_recovery",
3449
3489
  source: "issue.assignment_recovery",
3450
3490
  retryOfRunId: latestRun.id,
3491
+ outcome: dispatchOutcome,
3451
3492
  });
3452
3493
  if (queued) {
3453
3494
  result.dispatchRequeued += 1;
3454
3495
  result.issueIds.push(issue.id);
3455
3496
  }
3497
+ else if (dispatchOutcome.retryExhausted &&
3498
+ !(await hasActiveExecutionPath(issue.companyId, issue.id, null))) {
3499
+ // Same exhaustion as the in_progress lane: the lost dispatch's
3500
+ // bounded retries are spent, so escalate instead of skipping on
3501
+ // every sweep with no live path. The live-path re-read guards the
3502
+ // `blocked` write against a run or wake that started after the
3503
+ // loop's check.
3504
+ const updated = await escalateStrandedAssignedIssue({
3505
+ issue,
3506
+ previousStatus: "todo",
3507
+ latestRun,
3508
+ notice: {
3509
+ body: "Paperclip automatically retried dispatch for this assigned `todo` issue after a lost wake/run, " +
3510
+ "but the bounded retry budget is spent and it still has no live execution path. " +
3511
+ "Moving it to `blocked` so it is visible for intervention.",
3512
+ title: "No live execution path",
3513
+ tone: "danger",
3514
+ },
3515
+ });
3516
+ if (updated) {
3517
+ result.escalated += 1;
3518
+ result.issueIds.push(issue.id);
3519
+ }
3520
+ else {
3521
+ result.skipped += 1;
3522
+ }
3523
+ }
3456
3524
  else {
3457
3525
  result.skipped += 1;
3458
3526
  }
@@ -3488,6 +3556,38 @@ export function recoveryService(db, deps) {
3488
3556
  result.skipped += 1;
3489
3557
  continue;
3490
3558
  }
3559
+ // An interrupted corrective run is not evidence that the agent could
3560
+ // not choose a disposition: a graceful server shutdown (a deploy
3561
+ // restart, a lost process) ended the attempt before the agent
3562
+ // finished. The attempt cap counts attempts the agent got to finish,
3563
+ // so give the interrupted run the same bounded transient retry any
3564
+ // interrupted run gets — the retry keeps the handoff context, so it
3565
+ // is still the corrective run — and escalate only once that retry
3566
+ // budget is spent or a finished attempt still leaves no disposition.
3567
+ // Native-runtime corrective runs have no process-loss retry lane
3568
+ // (a graceful shutdown suspends their controller for reattach
3569
+ // instead of interrupting them; other native interruptions are
3570
+ // reconciled by their own finalizer), so the recovery enqueue
3571
+ // returns null for them and they escalate exactly as before.
3572
+ if (latestRun?.status === "interrupted") {
3573
+ if (await isInvocationBudgetBlocked(issue, agentId)) {
3574
+ result.skipped += 1;
3575
+ continue;
3576
+ }
3577
+ const retried = await enqueueStrandedIssueRecovery({
3578
+ issueId: issue.id,
3579
+ agentId,
3580
+ reason: "issue_continuation_needed",
3581
+ retryReason: "issue_continuation_needed",
3582
+ source: "issue.successful_run_handoff_interrupted_retry",
3583
+ retryOfRunId: latestRun.id,
3584
+ });
3585
+ if (retried) {
3586
+ result.successfulRunHandoffRetried += 1;
3587
+ result.issueIds.push(issue.id);
3588
+ continue;
3589
+ }
3590
+ }
3491
3591
  const updated = await escalateStrandedAssignedIssue({
3492
3592
  issue,
3493
3593
  previousStatus: "in_progress",
@@ -3653,6 +3753,7 @@ export function recoveryService(db, deps) {
3653
3753
  result.skipped += 1;
3654
3754
  continue;
3655
3755
  }
3756
+ const recoveryOutcome = {};
3656
3757
  const queued = await enqueueStrandedIssueRecovery({
3657
3758
  issueId: issue.id,
3658
3759
  agentId,
@@ -3660,11 +3761,40 @@ export function recoveryService(db, deps) {
3660
3761
  retryReason: "issue_continuation_needed",
3661
3762
  source: "issue.continuation_recovery",
3662
3763
  retryOfRunId: latestRun?.id ?? issue.checkoutRunId ?? null,
3764
+ outcome: recoveryOutcome,
3663
3765
  });
3664
3766
  if (queued) {
3665
3767
  result.continuationRequeued += 1;
3666
3768
  result.issueIds.push(issue.id);
3667
3769
  }
3770
+ else if (recoveryOutcome.retryExhausted &&
3771
+ !(await hasActiveExecutionPath(issue.companyId, issue.id, null))) {
3772
+ // The failed run's bounded transient retries are all spent, so no
3773
+ // successor will ever be queued for it. Escalate rather than skip
3774
+ // on every sweep forever: the issue would otherwise stay
3775
+ // `in_progress` with no run and no path until a person noticed.
3776
+ // The live-path re-read guards the `blocked` write against a run or
3777
+ // wake that started after the loop's check.
3778
+ const updated = await escalateStrandedAssignedIssue({
3779
+ issue,
3780
+ previousStatus: "in_progress",
3781
+ latestRun,
3782
+ notice: {
3783
+ body: "Paperclip retried this issue's run after it ended without finishing, but the bounded retry budget " +
3784
+ "is spent and it still has no live execution path. " +
3785
+ "Moving it to `blocked` so it is visible for intervention.",
3786
+ title: "No live execution path",
3787
+ tone: "danger",
3788
+ },
3789
+ });
3790
+ if (updated) {
3791
+ result.escalated += 1;
3792
+ result.issueIds.push(issue.id);
3793
+ }
3794
+ else {
3795
+ result.skipped += 1;
3796
+ }
3797
+ }
3668
3798
  else {
3669
3799
  result.skipped += 1;
3670
3800
  }