@tickernelz/paperclip-pro-server 2026.930.6 → 2026.1001.1
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 +3 -0
- package/dist/index.js.map +1 -1
- package/dist/services/execution-projection.d.ts.map +1 -1
- package/dist/services/execution-projection.js +2 -1
- package/dist/services/execution-projection.js.map +1 -1
- package/dist/services/execution-recovery-attempt.d.ts +3 -0
- package/dist/services/execution-recovery-attempt.d.ts.map +1 -1
- package/dist/services/execution-recovery-attempt.js +9 -0
- package/dist/services/execution-recovery-attempt.js.map +1 -1
- package/dist/services/heartbeat.d.ts +18 -2
- package/dist/services/heartbeat.d.ts.map +1 -1
- package/dist/services/heartbeat.js +245 -51
- package/dist/services/heartbeat.js.map +1 -1
- package/dist/services/hot-restart.d.ts +4 -3
- package/dist/services/hot-restart.d.ts.map +1 -1
- package/dist/services/hot-restart.js +1 -1
- package/dist/services/hot-restart.js.map +1 -1
- package/dist/services/recovery/service.d.ts +4 -8
- package/dist/services/recovery/service.d.ts.map +1 -1
- package/dist/services/recovery/service.js +155 -103
- package/dist/services/recovery/service.js.map +1 -1
- package/package.json +21 -21
- package/ui-dist/assets/{Agents.production-C92v0Lx-.js → Agents.production-tjOEQe3l.js} +2 -2
- package/ui-dist/assets/{CompanyActivity.production-zCGxSTfT.js → CompanyActivity.production-DeT7xKMQ.js} +1 -1
- package/ui-dist/assets/{CompanyExport-ZX12qv9W.js → CompanyExport-BLyfRuJ5.js} +1 -1
- package/ui-dist/assets/{CompanySkills.production-ppXT58tQ.js → CompanySkills.production-BQbzHmtn.js} +1 -1
- package/ui-dist/assets/{Costs.production-BtTAEXoR.js → Costs.production-5RBWzUGB.js} +1 -1
- package/ui-dist/assets/{OrgChart.production-rZs_mwUx.js → OrgChart.production-D_5tQ3tx.js} +1 -1
- package/ui-dist/assets/{RoutineDetail.production-B-MVcelq.js → RoutineDetail.production-Dd163Q10.js} +1 -1
- package/ui-dist/assets/{Routines.production-BjxyLaNj.js → Routines.production-g9pJjOvz.js} +1 -1
- package/ui-dist/assets/{definition-BghzHoPd.js → definition-BtNjkL-Z.js} +1 -1
- package/ui-dist/assets/{index-m5QR0hsC.js → index-B_VCIODd.js} +4 -4
- package/ui-dist/index.html +1 -1
- package/ui-dist/sw.js +1 -1
|
@@ -24,7 +24,7 @@ import { claimQueuedNativeReviewRun } from "./native-runtime/native-review-dispa
|
|
|
24
24
|
import { buildNativeReviewRequest } from "./native-runtime/native-review-prompt.js";
|
|
25
25
|
import { isGracefulShutdownInterruptedRun, legacyExecutionNeedsReconciliation, terminalizeLegacyExecution, } from "./legacy-execution-recovery.js";
|
|
26
26
|
import { adapterExecutionControls, captureAdapterStopOwnership, createAdapterExecutionControl, registerAdapterExecutionControl, waitForAdapterStop, } from "./adapter-execution-control.js";
|
|
27
|
-
import { executionFailureRetryCount } from "./execution-recovery-attempt.js";
|
|
27
|
+
import { executionFailureRetryCount, processLossContinuationCount, } from "./execution-recovery-attempt.js";
|
|
28
28
|
import { buildHeartbeatRunStatusLiveEventPayload } from "./heartbeat-run-status-payload.js";
|
|
29
29
|
export { buildHeartbeatRunStatusLiveEventPayload } from "./heartbeat-run-status-payload.js";
|
|
30
30
|
import { buildExecutionContinuation } from "./execution-continuation.js";
|
|
@@ -42,6 +42,7 @@ import path from "node:path";
|
|
|
42
42
|
import { execFile as execFileCallback } from "node:child_process";
|
|
43
43
|
import { promisify } from "node:util";
|
|
44
44
|
import { createHash, randomUUID } from "node:crypto";
|
|
45
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
45
46
|
import { and, asc, desc, eq, exists, getTableColumns, gt, gte, inArray, isNull, isNotNull, lt, lte, ne, notInArray, or, sql, } from "drizzle-orm";
|
|
46
47
|
import { AGENT_DEFAULT_MAX_CONCURRENT_RUNS, INSTANCE_DEFAULT_LOCAL_START_WAIT_BYPASS_SEC, INSTANCE_DEFAULT_MAX_CONCURRENT_LOCAL_RUNS, INSTANCE_DEFAULT_MAX_CONCURRENT_LOCAL_STARTS, INSTANCE_LOCAL_START_WAIT_BYPASS_SEC_MAX, INSTANCE_LOCAL_START_WAIT_BYPASS_SEC_MIN, INSTANCE_MAX_CONCURRENT_LOCAL_RUNS_MAX, INSTANCE_MAX_CONCURRENT_LOCAL_RUNS_MIN, INSTANCE_MAX_CONCURRENT_LOCAL_STARTS_MAX, INSTANCE_MAX_CONCURRENT_LOCAL_STARTS_MIN, LOCAL_CLI_ADAPTER_TYPES, CHAT_PROVIDERS, CONNECTION_INTENT_AGENT_GUIDANCE, CONNECTION_RUNTIME_TOOL_NAMES, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, PROVIDER_QUOTA_MONITOR_SERVICE_NAME, envBindingSchema, isToolConnectionAttentionHealth, } from "@tickernelz/paperclip-pro-shared";
|
|
47
48
|
import { agents, agentConfigRevisions, agentRuntimeState, agentSessionGoalActions, agentTaskSessions, agentWakeupRequests, activityLog, approvals, assets, chatConversations, chatDeliveries, chatEndpoints, chatMessageLinks, companyMemberships, companySkillTestRuns, companySkillVersions, companySkills as companySkillsTable, companies, completionContracts, costEvents, documentAnnotationComments, documentAnnotationThreads, documentRevisions, environmentLeases, issueDocuments, executionWorkspaces, heartbeatRunEvents, heartbeatRuns, issueApprovals, issueAttachments, issueComments, issuePlanDecompositions, issueRecoveryActions, issueRelations, issueThreadInteractions, issues, issueWorkProducts, nativeRunFinalizations, projects, projectWorkspaces, routineRevisions, routineRuns, routines, toolMcpGateways, toolMcpGatewayTokens, toolCatalogEntries, toolConnectionInstalls, toolConnections, toolProfileEntries, toolProfiles, workspaceOperations, } from "@tickernelz/paperclip-pro-db";
|
|
@@ -323,9 +324,23 @@ const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_JITTER_RATIO = 0;
|
|
|
323
324
|
const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_REASON = "transient_failure";
|
|
324
325
|
const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_WAKE_REASON = "transient_failure_retry";
|
|
325
326
|
const PROCESS_LOST_RETRY_WAKE_REASON = "process_lost_retry";
|
|
327
|
+
const PROCESS_LOSS_RETRY_REASON = "process_lost";
|
|
328
|
+
const PROCESS_LOSS_CONTINUATION_MAX_ATTEMPTS = 2;
|
|
329
|
+
const PROCESS_LOSS_CONTINUATION_DELAY_MS = 30_000;
|
|
330
|
+
const PROCESS_LOSS_ERROR_CODES = {
|
|
331
|
+
orphaned_running_run: true,
|
|
332
|
+
server_shutdown_interrupted: true,
|
|
333
|
+
process_lost: true,
|
|
334
|
+
[DETACHED_PROCESS_ERROR_CODE]: true,
|
|
335
|
+
};
|
|
326
336
|
function isTransientWorkspaceGitScanCode(code) {
|
|
327
337
|
return code === WORKSPACE_GIT_SCAN_ERROR_CODES.timeout || code === WORKSPACE_GIT_SCAN_ERROR_CODES.saturated;
|
|
328
338
|
}
|
|
339
|
+
const ADAPTER_SELF_SIGNALLED_ERROR_CODES = {
|
|
340
|
+
codex_output_inactivity_monitor: true,
|
|
341
|
+
paperclip_mcp_unavailable: true,
|
|
342
|
+
hermes_gateway_cancelled: true,
|
|
343
|
+
};
|
|
329
344
|
const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_MAX_ATTEMPTS = BOUNDED_TRANSIENT_HEARTBEAT_RETRY_DELAYS_MS.length;
|
|
330
345
|
export { INTERACTION_CONTINUATION_INFRA_RETRY_REASON, INTERACTION_CONTINUATION_INFRA_WAKE_REASON, };
|
|
331
346
|
const INTERACTION_CONTINUATION_INFRA_MAX_ATTEMPTS = 2;
|
|
@@ -798,6 +813,15 @@ const nativeSessionResumeDispatchTimers = new Map();
|
|
|
798
813
|
// resolveHeartbeatSchedulingSuppression() check and every heartbeatService()
|
|
799
814
|
// instance see the same drain.
|
|
800
815
|
let taskDrainState = null;
|
|
816
|
+
let serverShutdownSignal = null;
|
|
817
|
+
const shutdownTerminatedRunIds = new Set();
|
|
818
|
+
export function beginHeartbeatServerShutdown(signal) {
|
|
819
|
+
serverShutdownSignal ??= signal;
|
|
820
|
+
}
|
|
821
|
+
export function resetHeartbeatServerShutdownForTests() {
|
|
822
|
+
serverShutdownSignal = null;
|
|
823
|
+
shutdownTerminatedRunIds.clear();
|
|
824
|
+
}
|
|
801
825
|
function readTaskDrain(now) {
|
|
802
826
|
if (taskDrainState &&
|
|
803
827
|
taskDrainState.expiresAt !== null &&
|
|
@@ -3917,6 +3941,7 @@ const SESSION_CONFIG_FINGERPRINT_KEY = "__paperclipConfigFingerprint";
|
|
|
3917
3941
|
const SESSION_CONFIG_FINGERPRINT_VERSION_KEY = "__paperclipConfigFingerprintVersion";
|
|
3918
3942
|
const SESSION_CONFIG_CATEGORIES_KEY = "__paperclipConfigCategories";
|
|
3919
3943
|
const SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY = "__paperclipConfigCategoryFingerprints";
|
|
3944
|
+
const SESSION_CONFIG_RESUME_FINGERPRINT_KEY = "__paperclipConfigResumeFingerprint";
|
|
3920
3945
|
const PAPERCLIP_SESSION_METADATA_KEYS = new Set([
|
|
3921
3946
|
SESSION_AI_CREDENTIAL_IDENTITY_KEY,
|
|
3922
3947
|
SESSION_CONFIGURED_MODEL_KEY,
|
|
@@ -3924,6 +3949,7 @@ const PAPERCLIP_SESSION_METADATA_KEYS = new Set([
|
|
|
3924
3949
|
SESSION_CONFIG_FINGERPRINT_VERSION_KEY,
|
|
3925
3950
|
SESSION_CONFIG_CATEGORIES_KEY,
|
|
3926
3951
|
SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY,
|
|
3952
|
+
SESSION_CONFIG_RESUME_FINGERPRINT_KEY,
|
|
3927
3953
|
]);
|
|
3928
3954
|
const WORKSPACE_CONFIG_FINGERPRINT_METADATA_KEY = "configFingerprint";
|
|
3929
3955
|
const EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES = [
|
|
@@ -3948,6 +3974,19 @@ const EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES = [
|
|
|
3948
3974
|
"environment",
|
|
3949
3975
|
"realization",
|
|
3950
3976
|
];
|
|
3977
|
+
const RESUME_NEUTRAL_ADAPTER_CONFIG_KEYS = [
|
|
3978
|
+
"instructionsBundleMode",
|
|
3979
|
+
"instructionsRootPath",
|
|
3980
|
+
"instructionsEntryFile",
|
|
3981
|
+
"instructionsFilePath",
|
|
3982
|
+
"promptTemplate",
|
|
3983
|
+
"bootstrapPromptTemplate",
|
|
3984
|
+
"paperclipSkillSync",
|
|
3985
|
+
"paperclipRuntimeSkills",
|
|
3986
|
+
"paperclipConnectorSkillDigest",
|
|
3987
|
+
];
|
|
3988
|
+
const PROCESS_LOSS_RESUME_NEUTRAL_SESSION_CONFIG_CATEGORIES = { instructions: true, runtimeSkills: true };
|
|
3989
|
+
const PROCESS_LOSS_RESUME_TARGET_SESSION_CONFIG_CATEGORIES = { adapter: true, adapterConfig: true };
|
|
3951
3990
|
/**
|
|
3952
3991
|
* Projectless native runs bind their immutable envelope to the run id even
|
|
3953
3992
|
* though no project-scoped execution-workspace row exists. Treat that value
|
|
@@ -4118,6 +4157,7 @@ function readConfigFingerprintFromSessionParams(sessionParams) {
|
|
|
4118
4157
|
version,
|
|
4119
4158
|
categories: readConfigCategoriesFromSessionParams(sessionParams),
|
|
4120
4159
|
categoryFingerprints: parseStoredConfigCategoryFingerprints(sessionParams[SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY]),
|
|
4160
|
+
resumeFingerprint: readNonEmptyString(sessionParams[SESSION_CONFIG_RESUME_FINGERPRINT_KEY]),
|
|
4121
4161
|
};
|
|
4122
4162
|
}
|
|
4123
4163
|
function describeEffectiveRunConfigCategories(categories) {
|
|
@@ -4366,9 +4406,10 @@ function buildSessionConfigCategoryValues(input) {
|
|
|
4366
4406
|
export async function buildEffectiveRunSessionConfigMetadata(input) {
|
|
4367
4407
|
const secretManifest = input.secretManifest ?? [];
|
|
4368
4408
|
const instructions = await resolveInstructionsConfigFingerprintMetadata(input.effectiveAdapterConfig);
|
|
4409
|
+
const fingerprintAdapterConfig = managedAiSessionFingerprintConfig(input.effectiveAdapterConfig, input.managedAiHome);
|
|
4369
4410
|
const categoryValues = buildSessionConfigCategoryValues({
|
|
4370
4411
|
adapterType: input.adapterType,
|
|
4371
|
-
effectiveAdapterConfig:
|
|
4412
|
+
effectiveAdapterConfig: fingerprintAdapterConfig,
|
|
4372
4413
|
agentRuntimeConfig: input.agentRuntimeConfig,
|
|
4373
4414
|
instructions,
|
|
4374
4415
|
issueOverrides: input.issueOverrides,
|
|
@@ -4391,11 +4432,26 @@ export async function buildEffectiveRunSessionConfigMetadata(input) {
|
|
|
4391
4432
|
subcategories: EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES,
|
|
4392
4433
|
secretManifest,
|
|
4393
4434
|
});
|
|
4435
|
+
const resumeAdapterConfig = { ...fingerprintAdapterConfig };
|
|
4436
|
+
for (const key of RESUME_NEUTRAL_ADAPTER_CONFIG_KEYS)
|
|
4437
|
+
delete resumeAdapterConfig[key];
|
|
4438
|
+
const { resume: resumeFingerprint } = createEffectiveRunConfigSubcategoryFingerprints({
|
|
4439
|
+
category: "session",
|
|
4440
|
+
value: {
|
|
4441
|
+
resume: {
|
|
4442
|
+
adapterType: input.adapterType,
|
|
4443
|
+
adapterConfig: resumeAdapterConfig,
|
|
4444
|
+
},
|
|
4445
|
+
},
|
|
4446
|
+
subcategories: ["resume"],
|
|
4447
|
+
secretManifest,
|
|
4448
|
+
});
|
|
4394
4449
|
return {
|
|
4395
4450
|
version: EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION,
|
|
4396
4451
|
fingerprint: fingerprints.sessionFingerprint.fingerprint,
|
|
4397
4452
|
categories: [...EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES],
|
|
4398
4453
|
categoryFingerprints,
|
|
4454
|
+
resumeFingerprint,
|
|
4399
4455
|
fingerprints,
|
|
4400
4456
|
};
|
|
4401
4457
|
}
|
|
@@ -4609,6 +4665,8 @@ function attachPaperclipSessionMetadataToSessionParams(sessionParams, configured
|
|
|
4609
4665
|
next[SESSION_CONFIG_CATEGORIES_KEY] = configMetadata.categories;
|
|
4610
4666
|
next[SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY] =
|
|
4611
4667
|
configMetadata.categoryFingerprints;
|
|
4668
|
+
next[SESSION_CONFIG_RESUME_FINGERPRINT_KEY] =
|
|
4669
|
+
configMetadata.resumeFingerprint;
|
|
4612
4670
|
}
|
|
4613
4671
|
return next;
|
|
4614
4672
|
}
|
|
@@ -4638,6 +4696,17 @@ export function stripPaperclipSessionMetadataFromSessionParams(sessionParams) {
|
|
|
4638
4696
|
}
|
|
4639
4697
|
return next;
|
|
4640
4698
|
}
|
|
4699
|
+
function isProcessLossContinuationRun(run, contextSnapshot) {
|
|
4700
|
+
return (run.scheduledRetryReason === PROCESS_LOSS_RETRY_REASON ||
|
|
4701
|
+
readNonEmptyString(contextSnapshot?.wakeReason) ===
|
|
4702
|
+
PROCESS_LOST_RETRY_WAKE_REASON);
|
|
4703
|
+
}
|
|
4704
|
+
function processLossContinuationCanResumeAcross(input) {
|
|
4705
|
+
const resumeTargetUnchanged = input.storedResumeFingerprint === input.nextResumeFingerprint;
|
|
4706
|
+
return input.changedCategories.every((category) => PROCESS_LOSS_RESUME_NEUTRAL_SESSION_CONFIG_CATEGORIES[category] === true ||
|
|
4707
|
+
(resumeTargetUnchanged &&
|
|
4708
|
+
PROCESS_LOSS_RESUME_TARGET_SESSION_CONFIG_CATEGORIES[category] === true));
|
|
4709
|
+
}
|
|
4641
4710
|
export function resolveTaskSessionConfigFreshness(input) {
|
|
4642
4711
|
if (!input.hasTaskSession) {
|
|
4643
4712
|
return {
|
|
@@ -4675,7 +4744,14 @@ export function resolveTaskSessionConfigFreshness(input) {
|
|
|
4675
4744
|
previous: storedConfig.categoryFingerprints,
|
|
4676
4745
|
next: input.configMetadata.categoryFingerprints,
|
|
4677
4746
|
});
|
|
4678
|
-
|
|
4747
|
+
if (!input.processLossContinuation ||
|
|
4748
|
+
!processLossContinuationCanResumeAcross({
|
|
4749
|
+
changedCategories,
|
|
4750
|
+
storedResumeFingerprint: storedConfig.resumeFingerprint,
|
|
4751
|
+
nextResumeFingerprint: input.configMetadata.resumeFingerprint,
|
|
4752
|
+
})) {
|
|
4753
|
+
reasons.push(`effective run configuration changed: ${describeEffectiveRunConfigCategories(changedCategories)}`);
|
|
4754
|
+
}
|
|
4679
4755
|
}
|
|
4680
4756
|
}
|
|
4681
4757
|
if (input.wakeResetReason)
|
|
@@ -6257,18 +6333,20 @@ export function resolveHeartbeatSchedulingSuppression(env = process.env, overrid
|
|
|
6257
6333
|
isTruthyRuntimeEnvValue(env.PAPERCLIP_RESTORE_IN_PROGRESS)) {
|
|
6258
6334
|
return { suppressed: true, reason: "database_restore_in_progress" };
|
|
6259
6335
|
}
|
|
6336
|
+
if (serverShutdownSignal !== null) {
|
|
6337
|
+
return { suppressed: true, reason: "server_shutdown" };
|
|
6338
|
+
}
|
|
6260
6339
|
if (readTaskDrain(new Date()) !== null) {
|
|
6261
6340
|
return { suppressed: true, reason: "task_drain" };
|
|
6262
6341
|
}
|
|
6263
6342
|
return { suppressed: false, reason: null };
|
|
6264
6343
|
}
|
|
6265
6344
|
export function heartbeatService(db, options = {}) {
|
|
6266
|
-
|
|
6345
|
+
const runtimeEnv = options.runtimeEnv ?? process.env;
|
|
6267
6346
|
const instanceSettings = instanceSettingsService(db);
|
|
6268
6347
|
const getCurrentUserRedactionOptions = async () => ({
|
|
6269
6348
|
enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs,
|
|
6270
6349
|
});
|
|
6271
|
-
const runtimeEnv = options.runtimeEnv ?? process.env;
|
|
6272
6350
|
const inWorktreeRuntime = isTruthyRuntimeEnvValue(runtimeEnv.PAPERCLIP_IN_WORKTREE);
|
|
6273
6351
|
// Preview worktree instances suppress the run engine by default. Users can lift
|
|
6274
6352
|
// that per-worktree via the `enableWorktreeRunExecution` experimental setting
|
|
@@ -6357,14 +6435,14 @@ export function heartbeatService(db, options = {}) {
|
|
|
6357
6435
|
const agent = await getAgent(run.agentId);
|
|
6358
6436
|
if (!agent || agent.companyId !== run.companyId)
|
|
6359
6437
|
return null;
|
|
6438
|
+
if (PROCESS_LOSS_ERROR_CODES[run.errorCode ?? ""])
|
|
6439
|
+
return enqueueProcessLossRetry(run, agent, new Date());
|
|
6360
6440
|
const result = await scheduleBoundedRetryForRun(run, agent);
|
|
6361
6441
|
return result.outcome === "scheduled" ? result.run : null;
|
|
6362
6442
|
},
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
transientRetryBudgetSpent: (run) => executionFailureRetryCount(run) >=
|
|
6367
|
-
BOUNDED_TRANSIENT_HEARTBEAT_RETRY_MAX_ATTEMPTS,
|
|
6443
|
+
retryBudgetSpent: (run) => PROCESS_LOSS_ERROR_CODES[run.errorCode ?? ""]
|
|
6444
|
+
? processLossContinuationCount(run) >= PROCESS_LOSS_CONTINUATION_MAX_ATTEMPTS
|
|
6445
|
+
: executionFailureRetryCount(run) >= BOUNDED_TRANSIENT_HEARTBEAT_RETRY_MAX_ATTEMPTS,
|
|
6368
6446
|
continueProcessGoneRun,
|
|
6369
6447
|
});
|
|
6370
6448
|
const runDispatch = createRunDispatch(db, {
|
|
@@ -6547,10 +6625,15 @@ export function heartbeatService(db, options = {}) {
|
|
|
6547
6625
|
const [source] = await db.select().from(heartbeatRuns).where(and(eq(heartbeatRuns.companyId, effect.companyId), eq(heartbeatRuns.id, effect.runId)));
|
|
6548
6626
|
const agent = source ? await getAgent(source.agentId) : null;
|
|
6549
6627
|
if (source && agent && agent.companyId === source.companyId) {
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6628
|
+
if (PROCESS_LOSS_ERROR_CODES[source.errorCode ?? ""]) {
|
|
6629
|
+
await enqueueProcessLossRetry(source, agent, new Date());
|
|
6630
|
+
}
|
|
6631
|
+
else {
|
|
6632
|
+
await scheduleBoundedRetryForRun(source, agent, effect.reviewParticipant ? {
|
|
6633
|
+
retryReason: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_RETRY_REASON,
|
|
6634
|
+
wakeReason: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_WAKE_REASON,
|
|
6635
|
+
} : undefined);
|
|
6636
|
+
}
|
|
6554
6637
|
}
|
|
6555
6638
|
}
|
|
6556
6639
|
else if (effect.kind === "run_queued") {
|
|
@@ -9970,15 +10053,18 @@ export function heartbeatService(db, options = {}) {
|
|
|
9970
10053
|
return { outcome: "retry_exhausted", queuedRun: null };
|
|
9971
10054
|
}
|
|
9972
10055
|
async function enqueueProcessLossRetry(run, agent, now, opts) {
|
|
9973
|
-
// Native sessions have their own fenced same-run controller. Legacy
|
|
9974
|
-
// bootstrap recovery shares the durable delay and incident counter with
|
|
9975
|
-
// transient retries; process loss must not open a second retry budget.
|
|
9976
10056
|
if (run.runtimeMode === "native")
|
|
9977
10057
|
return null;
|
|
9978
10058
|
if (!isGracefulShutdownInterruptedRun(run) &&
|
|
9979
10059
|
legacyExecutionNeedsReconciliation(run))
|
|
9980
10060
|
return null;
|
|
9981
|
-
const scheduled = await scheduleBoundedRetryForRun(run, agent, {
|
|
10061
|
+
const scheduled = await scheduleBoundedRetryForRun(run, agent, {
|
|
10062
|
+
now,
|
|
10063
|
+
wakeReason: opts?.wakeReason,
|
|
10064
|
+
retryReason: PROCESS_LOSS_RETRY_REASON,
|
|
10065
|
+
maxAttempts: PROCESS_LOSS_CONTINUATION_MAX_ATTEMPTS,
|
|
10066
|
+
delayMs: PROCESS_LOSS_CONTINUATION_DELAY_MS,
|
|
10067
|
+
});
|
|
9982
10068
|
return scheduled.outcome === "scheduled" ? scheduled.run : null;
|
|
9983
10069
|
}
|
|
9984
10070
|
async function continueProcessGoneRun(run) {
|
|
@@ -10061,7 +10147,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
10061
10147
|
return (readNonEmptyString(parseObject(input.adapterConfig).engine) !== "cli");
|
|
10062
10148
|
}
|
|
10063
10149
|
async function prepareHotRestartShutdown(signal, now = new Date()) {
|
|
10064
|
-
|
|
10150
|
+
beginHeartbeatServerShutdown(signal);
|
|
10065
10151
|
const idleSessions = await closeIdleWarmNativeSessionsForRestart();
|
|
10066
10152
|
if (idleSessions.failed > 0) {
|
|
10067
10153
|
logger.warn({ idleSessions }, "idle native sessions could not checkpoint before controller shutdown");
|
|
@@ -10112,6 +10198,25 @@ export function heartbeatService(db, options = {}) {
|
|
|
10112
10198
|
...intent,
|
|
10113
10199
|
previousServerVersion: intent.previousServerVersion ?? serverVersion,
|
|
10114
10200
|
};
|
|
10201
|
+
if (process.platform === "linux" && runtimeEnv.PAPERCLIP_SERVICE_MANAGED === "1") {
|
|
10202
|
+
const drainRunIds = snapshotRuns.map((run) => run.runId);
|
|
10203
|
+
await writeHotRestartShutdownSnapshot({
|
|
10204
|
+
intent: intentWithVersion,
|
|
10205
|
+
signal,
|
|
10206
|
+
activeRuns: snapshotRuns,
|
|
10207
|
+
drainReason: "service_stop_kills_children",
|
|
10208
|
+
drainRunIds,
|
|
10209
|
+
capturedAt: now,
|
|
10210
|
+
});
|
|
10211
|
+
logger.warn({ signal, previousServerPid: intent.previousServerPid, drainRunIds }, "systemd stops every process left in the service cgroup; draining live runs into process-loss continuations instead of adopting them");
|
|
10212
|
+
return {
|
|
10213
|
+
mode: "service_drain_required",
|
|
10214
|
+
skipDrain: false,
|
|
10215
|
+
activeRunIds: drainRunIds,
|
|
10216
|
+
drainRunIds,
|
|
10217
|
+
drainReason: "service_stop_kills_children",
|
|
10218
|
+
};
|
|
10219
|
+
}
|
|
10115
10220
|
const serverStdioRuns = activeRuns.filter(isServerStdioBoundHotRestartRun);
|
|
10116
10221
|
if (serverStdioRuns.length > 0) {
|
|
10117
10222
|
const activeServerStdioRunIds = serverStdioRuns.map(({ run }) => run.id);
|
|
@@ -10288,18 +10393,15 @@ export function heartbeatService(db, options = {}) {
|
|
|
10288
10393
|
classify(candidate, "finalized_while_down", `run_status_${run.status}`, patch);
|
|
10289
10394
|
continue;
|
|
10290
10395
|
}
|
|
10291
|
-
const
|
|
10396
|
+
const hasSelectiveDrain = (intent.drainReason === "active_acp_run" ||
|
|
10397
|
+
intent.drainReason === "service_stop_kills_children") &&
|
|
10292
10398
|
(intent.drainRunIds?.length ?? 0) > 0;
|
|
10293
|
-
if (
|
|
10399
|
+
if (hasSelectiveDrain &&
|
|
10294
10400
|
intent.drainRunIds?.includes(candidate.runId)) {
|
|
10295
|
-
// A selective ACP drain is expected to persist a terminal row before
|
|
10296
|
-
// the new server starts. If the process was terminated but that write
|
|
10297
|
-
// failed, surface the run as lost instead of hiding it as an expected
|
|
10298
|
-
// drain skip.
|
|
10299
10401
|
classify(candidate, "lost", "selective_drain_not_finalized", patch);
|
|
10300
10402
|
continue;
|
|
10301
10403
|
}
|
|
10302
|
-
if (intent.drainRequired && !
|
|
10404
|
+
if (intent.drainRequired && !hasSelectiveDrain) {
|
|
10303
10405
|
classify(candidate, "skipped", "drain_required", patch);
|
|
10304
10406
|
continue;
|
|
10305
10407
|
}
|
|
@@ -10527,6 +10629,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
10527
10629
|
};
|
|
10528
10630
|
}
|
|
10529
10631
|
async function drainRunningRunsForShutdown(signal, now = new Date(), runIds = null) {
|
|
10632
|
+
beginHeartbeatServerShutdown(signal);
|
|
10530
10633
|
const selectedRunIds = runIds ? [...new Set(runIds)] : null;
|
|
10531
10634
|
if (selectedRunIds?.length === 0) {
|
|
10532
10635
|
return {
|
|
@@ -10549,6 +10652,27 @@ export function heartbeatService(db, options = {}) {
|
|
|
10549
10652
|
const interruptedRunIds = [];
|
|
10550
10653
|
const retryRunIds = [];
|
|
10551
10654
|
const restartSuspendedRunIds = [];
|
|
10655
|
+
const legacyTerminations = new Map();
|
|
10656
|
+
for (const { run } of activeRuns) {
|
|
10657
|
+
const running = runningProcesses.get(run.id);
|
|
10658
|
+
if (run.runtimeMode === "native" ||
|
|
10659
|
+
!running ||
|
|
10660
|
+
(run.controllerBootId && run.controllerBootId !== legacyControllerBootId) ||
|
|
10661
|
+
isNativeRunnerOwnershipHeld(run))
|
|
10662
|
+
continue;
|
|
10663
|
+
shutdownTerminatedRunIds.add(run.id);
|
|
10664
|
+
legacyTerminations.set(run.id, terminateHeartbeatRunProcess({
|
|
10665
|
+
pid: running.child.pid,
|
|
10666
|
+
processGroupId: running.processGroupId,
|
|
10667
|
+
graceMs: Math.max(1, running.graceSec) * 1000,
|
|
10668
|
+
}).then(() => null, (error) => error ?? new Error("Run process termination failed")));
|
|
10669
|
+
}
|
|
10670
|
+
await Promise.all(legacyTerminations.values());
|
|
10671
|
+
const executorDeadline = Date.now() + (options.shutdownExecutorSettleTimeoutMs ?? 15_000);
|
|
10672
|
+
while ([...legacyTerminations.keys()].some((runId) => activeRunExecutions.has(runId)) &&
|
|
10673
|
+
Date.now() < executorDeadline) {
|
|
10674
|
+
await delay(50);
|
|
10675
|
+
}
|
|
10552
10676
|
for (const { run, agent } of activeRuns) {
|
|
10553
10677
|
// Shutdown owns only this boot's legacy executions. Expired foreign
|
|
10554
10678
|
// owners belong to the reaper, not another container's drain.
|
|
@@ -10622,7 +10746,12 @@ export function heartbeatService(db, options = {}) {
|
|
|
10622
10746
|
runtimeMode: run.runtimeMode,
|
|
10623
10747
|
});
|
|
10624
10748
|
}
|
|
10625
|
-
if (
|
|
10749
|
+
if (legacyTerminations.has(run.id)) {
|
|
10750
|
+
const terminationError = await legacyTerminations.get(run.id);
|
|
10751
|
+
if (terminationError)
|
|
10752
|
+
throw terminationError;
|
|
10753
|
+
}
|
|
10754
|
+
else if (running) {
|
|
10626
10755
|
await terminateHeartbeatRunProcess({
|
|
10627
10756
|
pid: running.child.pid,
|
|
10628
10757
|
processGroupId: running.processGroupId,
|
|
@@ -10648,8 +10777,20 @@ export function heartbeatService(db, options = {}) {
|
|
|
10648
10777
|
errorMessage: message,
|
|
10649
10778
|
}),
|
|
10650
10779
|
});
|
|
10651
|
-
if (!interruptedStatus.updated || !interruptedStatus.run)
|
|
10780
|
+
if (!interruptedStatus.updated || !interruptedStatus.run) {
|
|
10781
|
+
if (interruptedStatus.run &&
|
|
10782
|
+
isGracefulShutdownInterruptedRun(interruptedStatus.run)) {
|
|
10783
|
+
interruptedRunIds.push(run.id);
|
|
10784
|
+
const [executorRetry] = await db
|
|
10785
|
+
.select({ id: heartbeatRuns.id })
|
|
10786
|
+
.from(heartbeatRuns)
|
|
10787
|
+
.where(eq(heartbeatRuns.retryOfRunId, run.id))
|
|
10788
|
+
.limit(1);
|
|
10789
|
+
if (executorRetry)
|
|
10790
|
+
retryRunIds.push(executorRetry.id);
|
|
10791
|
+
}
|
|
10652
10792
|
continue;
|
|
10793
|
+
}
|
|
10653
10794
|
let interrupted = interruptedStatus.run;
|
|
10654
10795
|
await setWakeupStatus(run.wakeupRequestId, "cancelled", {
|
|
10655
10796
|
finishedAt: now,
|
|
@@ -10708,11 +10849,13 @@ export function heartbeatService(db, options = {}) {
|
|
|
10708
10849
|
const retryReason = opts?.retryReason ?? BOUNDED_TRANSIENT_HEARTBEAT_RETRY_REASON;
|
|
10709
10850
|
const wakeReason = opts?.wakeReason ?? BOUNDED_TRANSIENT_HEARTBEAT_RETRY_WAKE_REASON;
|
|
10710
10851
|
const maxAttempts = Math.max(0, Math.floor(opts?.maxAttempts ?? BOUNDED_TRANSIENT_HEARTBEAT_RETRY_MAX_ATTEMPTS));
|
|
10711
|
-
const nextAttempt =
|
|
10712
|
-
|
|
10713
|
-
retryReason ===
|
|
10714
|
-
|
|
10715
|
-
|
|
10852
|
+
const nextAttempt = retryReason === PROCESS_LOSS_RETRY_REASON
|
|
10853
|
+
? processLossContinuationCount(run) + 1
|
|
10854
|
+
: (retryReason === WORKSPACE_BUSY_RETRY_REASON ||
|
|
10855
|
+
retryReason === AI_CONNECTION_BUSY_RETRY_REASON ||
|
|
10856
|
+
retryReason === MAX_TURN_CONTINUATION_RETRY_REASON
|
|
10857
|
+
? (run.scheduledRetryAttempt ?? 0)
|
|
10858
|
+
: executionFailureRetryCount(run)) + 1;
|
|
10716
10859
|
const computedBaseSchedule = opts?.delayMs != null
|
|
10717
10860
|
? nextAttempt <= maxAttempts
|
|
10718
10861
|
? {
|
|
@@ -10740,16 +10883,19 @@ export function heartbeatService(db, options = {}) {
|
|
|
10740
10883
|
const contextSnapshot = parseObject(run.contextSnapshot);
|
|
10741
10884
|
const issueId = readNonEmptyString(contextSnapshot.issueId);
|
|
10742
10885
|
if (!baseSchedule) {
|
|
10886
|
+
const spentAttempts = retryReason === PROCESS_LOSS_RETRY_REASON
|
|
10887
|
+
? nextAttempt - 1
|
|
10888
|
+
: (run.scheduledRetryAttempt ?? 0);
|
|
10743
10889
|
const exhaustion = {
|
|
10744
10890
|
retryReason,
|
|
10745
|
-
scheduledRetryAttempt:
|
|
10891
|
+
scheduledRetryAttempt: spentAttempts,
|
|
10746
10892
|
maxAttempts,
|
|
10747
10893
|
};
|
|
10748
10894
|
await appendRunEvent(run, {
|
|
10749
10895
|
eventType: "lifecycle",
|
|
10750
10896
|
stream: "system",
|
|
10751
10897
|
level: "warn",
|
|
10752
|
-
message: `Bounded retry exhausted after ${
|
|
10898
|
+
message: `Bounded retry exhausted after ${spentAttempts} scheduled attempts; no further automatic retry will be queued`,
|
|
10753
10899
|
payload: exhaustion,
|
|
10754
10900
|
retryExhaustion: exhaustion,
|
|
10755
10901
|
});
|
|
@@ -10908,6 +11054,14 @@ export function heartbeatService(db, options = {}) {
|
|
|
10908
11054
|
...(retryReason === AI_CONNECTION_BUSY_RETRY_REASON
|
|
10909
11055
|
? { failureRetriesBeforeAiConnectionWait: executionFailureRetryCount(run) }
|
|
10910
11056
|
: {}),
|
|
11057
|
+
...(retryReason === PROCESS_LOSS_RETRY_REASON
|
|
11058
|
+
? {
|
|
11059
|
+
failureRetriesBeforeProcessLoss: executionFailureRetryCount(run),
|
|
11060
|
+
processLossContinuations: nextAttempt,
|
|
11061
|
+
}
|
|
11062
|
+
: retryReason === MAX_TURN_CONTINUATION_RETRY_REASON
|
|
11063
|
+
? { processLossContinuations: undefined }
|
|
11064
|
+
: {}),
|
|
10911
11065
|
...(shouldQuarantineWorkspaceForRetry
|
|
10912
11066
|
? {
|
|
10913
11067
|
workspaceValidationRecovery: {
|
|
@@ -15595,6 +15749,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
15595
15749
|
taskSessionParams: taskSession?.sessionParamsJson ?? taskSessionDecodedParams,
|
|
15596
15750
|
configMetadata: sessionConfigMetadata,
|
|
15597
15751
|
wakeResetReason: wakeSessionResetReason,
|
|
15752
|
+
processLossContinuation: isProcessLossContinuationRun(run, context),
|
|
15598
15753
|
preserveLegacySessionWithoutConfigMetadata: acceptedPlanContinuationWake && !acceptedPlanWakeRoutingDecision,
|
|
15599
15754
|
});
|
|
15600
15755
|
const resetTaskSession = shouldResetTaskSessionForWake(context) || sessionConfigFreshness.reset;
|
|
@@ -18272,7 +18427,12 @@ export function heartbeatService(db, options = {}) {
|
|
|
18272
18427
|
failedProcessRunCancellations.get(run.id);
|
|
18273
18428
|
await processCancellation?.settled;
|
|
18274
18429
|
let outcome;
|
|
18430
|
+
let processLossErrorCode = null;
|
|
18275
18431
|
const latestRun = await getRun(run.id);
|
|
18432
|
+
const latestResult = parseObject(latestRun?.resultJson);
|
|
18433
|
+
const paperclipCancellationRecorded = Boolean(processCancellation) ||
|
|
18434
|
+
Boolean(latestResult.executionCancellation) ||
|
|
18435
|
+
Boolean(latestResult.startupCancellation);
|
|
18276
18436
|
if (isHeartbeatRunTerminalStatus(latestRun?.status)) {
|
|
18277
18437
|
outcome = latestRun.status;
|
|
18278
18438
|
}
|
|
@@ -18288,6 +18448,11 @@ export function heartbeatService(db, options = {}) {
|
|
|
18288
18448
|
? "cancelled"
|
|
18289
18449
|
: "failed";
|
|
18290
18450
|
}
|
|
18451
|
+
else if (shutdownTerminatedRunIds.has(run.id) &&
|
|
18452
|
+
!paperclipCancellationRecorded) {
|
|
18453
|
+
outcome = "interrupted";
|
|
18454
|
+
processLossErrorCode = "server_shutdown_interrupted";
|
|
18455
|
+
}
|
|
18291
18456
|
else if (adapterResult.timedOut) {
|
|
18292
18457
|
outcome = "timed_out";
|
|
18293
18458
|
}
|
|
@@ -18299,6 +18464,20 @@ export function heartbeatService(db, options = {}) {
|
|
|
18299
18464
|
}
|
|
18300
18465
|
else {
|
|
18301
18466
|
outcome = "failed";
|
|
18467
|
+
if (!paperclipCancellationRecorded &&
|
|
18468
|
+
(adapterResult.signal === "SIGTERM" ||
|
|
18469
|
+
adapterResult.signal === "SIGINT" ||
|
|
18470
|
+
adapterResult.exitCode === 143 ||
|
|
18471
|
+
adapterResult.exitCode === 130) &&
|
|
18472
|
+
!ADAPTER_SELF_SIGNALLED_ERROR_CODES[adapterResult.errorCode ?? ""] &&
|
|
18473
|
+
!hasUnmanagedBackgroundTaskEvidence(parseObject(adapterResult.resultJson))) {
|
|
18474
|
+
processLossErrorCode =
|
|
18475
|
+
serverShutdownSignal === null
|
|
18476
|
+
? "process_lost"
|
|
18477
|
+
: "server_shutdown_interrupted";
|
|
18478
|
+
if (serverShutdownSignal !== null)
|
|
18479
|
+
outcome = "interrupted";
|
|
18480
|
+
}
|
|
18302
18481
|
}
|
|
18303
18482
|
const nextSessionState = resolveNextSessionState({
|
|
18304
18483
|
adapterType: agent.adapterType,
|
|
@@ -18322,18 +18501,21 @@ export function heartbeatService(db, options = {}) {
|
|
|
18322
18501
|
? (latestRun?.error ?? adapterResult.errorMessage ?? "Cancelled")
|
|
18323
18502
|
: outcome === "succeeded"
|
|
18324
18503
|
? null
|
|
18325
|
-
:
|
|
18326
|
-
|
|
18504
|
+
: outcome === "interrupted" && serverShutdownSignal !== null
|
|
18505
|
+
? `Interrupted by graceful server shutdown (${serverShutdownSignal})`
|
|
18506
|
+
: redactCurrentUserText(adapterResult.errorMessage ??
|
|
18507
|
+
(outcome === "timed_out" ? "Timed out" : "Adapter failed"), currentUserRedactionOptions);
|
|
18327
18508
|
const recordedResponsibleUserDenialCode = normalizeResponsibleUserDenialCode(latestRun?.errorCode);
|
|
18328
|
-
const runErrorCode =
|
|
18329
|
-
|
|
18330
|
-
|
|
18331
|
-
|
|
18332
|
-
|
|
18333
|
-
|
|
18334
|
-
|
|
18335
|
-
|
|
18336
|
-
|
|
18509
|
+
const runErrorCode = processLossErrorCode ??
|
|
18510
|
+
(outcome === "timed_out"
|
|
18511
|
+
? "timeout"
|
|
18512
|
+
: outcome === "cancelled"
|
|
18513
|
+
? (latestRun?.errorCode ?? "cancelled")
|
|
18514
|
+
: outcome === "failed"
|
|
18515
|
+
? (adapterResult.errorCode ??
|
|
18516
|
+
recordedResponsibleUserDenialCode ??
|
|
18517
|
+
"adapter_failed")
|
|
18518
|
+
: null);
|
|
18337
18519
|
let logSummary = null;
|
|
18338
18520
|
if (handle) {
|
|
18339
18521
|
logSummary = await runLogStore.finalize(handle);
|
|
@@ -18358,7 +18540,9 @@ export function heartbeatService(db, options = {}) {
|
|
|
18358
18540
|
? "cancelled"
|
|
18359
18541
|
: outcome === "timed_out"
|
|
18360
18542
|
? "timed_out"
|
|
18361
|
-
: "
|
|
18543
|
+
: outcome === "interrupted"
|
|
18544
|
+
? "interrupted"
|
|
18545
|
+
: "failed";
|
|
18362
18546
|
const cacheAdjustedCostUsd = resolveCacheAdjustedCostUsd(adapterResult);
|
|
18363
18547
|
const usageJson = normalizedUsage ||
|
|
18364
18548
|
adapterResult.costUsd != null ||
|
|
@@ -18482,7 +18666,11 @@ export function heartbeatService(db, options = {}) {
|
|
|
18482
18666
|
persistedRun =
|
|
18483
18667
|
(await classifyAndPersistRunLiveness(persistedRun, persistedResultJson)) ?? persistedRun;
|
|
18484
18668
|
}
|
|
18485
|
-
await setWakeupStatus(run.wakeupRequestId, outcome === "succeeded"
|
|
18669
|
+
await setWakeupStatus(run.wakeupRequestId, outcome === "succeeded"
|
|
18670
|
+
? "completed"
|
|
18671
|
+
: outcome === "interrupted"
|
|
18672
|
+
? "cancelled"
|
|
18673
|
+
: status, {
|
|
18486
18674
|
finishedAt: new Date(),
|
|
18487
18675
|
error: runErrorMessage,
|
|
18488
18676
|
});
|
|
@@ -18624,7 +18812,10 @@ export function heartbeatService(db, options = {}) {
|
|
|
18624
18812
|
catch (err) {
|
|
18625
18813
|
await onLog("stderr", `[paperclip] Failed to resolve run presentation: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
18626
18814
|
}
|
|
18627
|
-
if (
|
|
18815
|
+
if (processLossErrorCode) {
|
|
18816
|
+
await enqueueProcessLossRetry(livenessRun, agent, new Date());
|
|
18817
|
+
}
|
|
18818
|
+
else if (outcome === "failed" && isMaxTurnExhaustionRun(livenessRun)) {
|
|
18628
18819
|
const policy = parseMaxTurnContinuationPolicy(agent);
|
|
18629
18820
|
if (policy.enabled && policy.maxAttempts > 0) {
|
|
18630
18821
|
await scheduleBoundedRetryForRun(livenessRun, agent, {
|
|
@@ -19340,6 +19531,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
19340
19531
|
finally {
|
|
19341
19532
|
controllerLease.stop();
|
|
19342
19533
|
activeRunExecutions.delete(run.id);
|
|
19534
|
+
shutdownTerminatedRunIds.delete(run.id);
|
|
19343
19535
|
clearLocalCliRunStarting(run.id);
|
|
19344
19536
|
// A failed owned Stop remains visible until this exact executor settles,
|
|
19345
19537
|
// including a graceful exit result arriving after the cancellation error.
|
|
@@ -19363,7 +19555,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
19363
19555
|
}
|
|
19364
19556
|
if (!nativeSessionResumeScheduled &&
|
|
19365
19557
|
!nativeWorkspaceFinalizeScheduled &&
|
|
19366
|
-
|
|
19558
|
+
serverShutdownSignal === null) {
|
|
19367
19559
|
if (latestRun)
|
|
19368
19560
|
await resumeRemoteStopComments(latestRun).catch(err => {
|
|
19369
19561
|
logger.warn({ err, runId: run.id }, "failed to resume user messages after remote Stop");
|
|
@@ -19621,7 +19813,8 @@ export function heartbeatService(db, options = {}) {
|
|
|
19621
19813
|
// continuation wake landed mid-drain left its issue in `todo` with no
|
|
19622
19814
|
// run and no path until a person noticed.
|
|
19623
19815
|
if (schedulingSuppression.suppressed &&
|
|
19624
|
-
schedulingSuppression.reason !== "task_drain"
|
|
19816
|
+
schedulingSuppression.reason !== "task_drain" &&
|
|
19817
|
+
schedulingSuppression.reason !== "server_shutdown") {
|
|
19625
19818
|
await writeSkippedHeartbeatRequest("heartbeat.scheduling_suppressed", {
|
|
19626
19819
|
reason: schedulingSuppression.reason,
|
|
19627
19820
|
});
|
|
@@ -21960,6 +22153,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
21960
22153
|
dispatchPendingNativeStatusWakeups,
|
|
21961
22154
|
triggerIssueMonitor,
|
|
21962
22155
|
reportRunActivity: clearDetachedRunWarning,
|
|
22156
|
+
beginServerShutdown: beginHeartbeatServerShutdown,
|
|
21963
22157
|
prepareHotRestartShutdown,
|
|
21964
22158
|
reconcileHotRestartAdoption,
|
|
21965
22159
|
recoverNativeRunsAfterRestart,
|