@tickernelz/paperclip-pro-server 2026.929.0 → 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.
- package/dist/build-info.json +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/http-log-redaction.d.ts +1 -1
- package/dist/middleware/http-log-redaction.d.ts.map +1 -1
- package/dist/middleware/http-log-redaction.js +5 -0
- package/dist/middleware/http-log-redaction.js.map +1 -1
- package/dist/routes/access.d.ts.map +1 -1
- package/dist/routes/access.js +6 -0
- package/dist/routes/access.js.map +1 -1
- package/dist/routes/openapi.d.ts.map +1 -1
- package/dist/routes/openapi.js +7 -4
- package/dist/routes/openapi.js.map +1 -1
- package/dist/services/execution-workspace-policy.d.ts.map +1 -1
- package/dist/services/execution-workspace-policy.js +18 -10
- package/dist/services/execution-workspace-policy.js.map +1 -1
- package/dist/services/execution-workspaces.d.ts.map +1 -1
- package/dist/services/execution-workspaces.js +3 -0
- package/dist/services/execution-workspaces.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 +79 -7
- package/dist/services/heartbeat.js.map +1 -1
- package/dist/services/paperclip-cloud-connector.d.ts +1 -1
- package/dist/services/paperclip-cloud-connector.d.ts.map +1 -1
- package/dist/services/paperclip-cloud-connector.js +55 -2
- package/dist/services/paperclip-cloud-connector.js.map +1 -1
- package/dist/services/paperclip-cloud-connector.test.js +88 -0
- package/dist/services/paperclip-cloud-connector.test.js.map +1 -1
- package/dist/services/recovery/service.d.ts +9 -0
- package/dist/services/recovery/service.d.ts.map +1 -1
- package/dist/services/recovery/service.js +132 -2
- 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 +12 -6
- package/dist/services/run-identity.js.map +1 -1
- package/dist/services/work-products.d.ts.map +1 -1
- package/dist/services/work-products.js +18 -1
- package/dist/services/work-products.js.map +1 -1
- package/package.json +21 -21
- package/ui-dist/assets/{Agents.production-BeDi727Q.js → Agents.production-DXVQbH40.js} +2 -2
- package/ui-dist/assets/{CompanyActivity.production-Qt-mLMpq.js → CompanyActivity.production-CinmmGVv.js} +1 -1
- package/ui-dist/assets/{CompanyExport-Dq2gmciJ.js → CompanyExport-DaS-YmqD.js} +1 -1
- package/ui-dist/assets/{CompanySkills.production-C4smxeMX.js → CompanySkills.production-DayJEg3Z.js} +1 -1
- package/ui-dist/assets/{Costs.production-CDsWvATt.js → Costs.production-qBNyNNRY.js} +1 -1
- package/ui-dist/assets/{MarkdownBody-DJQ35hak.js → MarkdownBody-NJwkQWmo.js} +2 -2
- package/ui-dist/assets/{OrgChart.production-uQij-DWC.js → OrgChart.production-7-WQ0htv.js} +1 -1
- package/ui-dist/assets/{RoutineDetail.production-DmubEXM7.js → RoutineDetail.production-CEF8wtwL.js} +1 -1
- package/ui-dist/assets/{RoutineRunVariablesDialog-Bt4KAWCh.js → RoutineRunVariablesDialog-B1KRZJIl.js} +2 -2
- package/ui-dist/assets/{Routines.production-_siggo5J.js → Routines.production-C15V_czC.js} +1 -1
- package/ui-dist/assets/StatusIcon-DgMlA1TU.js +1 -0
- package/ui-dist/assets/{definition-DroCAncf.js → definition-C_4HgU9U.js} +1 -1
- package/ui-dist/assets/{index-BCClSQew.js → index-DMN58MRe.js} +163 -160
- package/ui-dist/assets/{index-J-4PQOv1.css → index-Dkjt7SQK.css} +1 -1
- package/ui-dist/index.html +5 -5
- package/ui-dist/sw.js +1 -1
- 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
|
-
|
|
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
|
}
|