@tickernelz/paperclip-pro-server 2026.1001.0 → 2026.1001.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 +3 -0
- package/dist/index.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 +194 -30
- 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 +3 -0
- package/dist/services/recovery/service.d.ts.map +1 -1
- package/dist/services/recovery/service.js +154 -102
- package/dist/services/recovery/service.js.map +1 -1
- package/package.json +21 -21
- package/ui-dist/assets/{Agents.production-fNeT2seP.js → Agents.production-tjOEQe3l.js} +2 -2
- package/ui-dist/assets/{CompanyActivity.production-CEF0hx9W.js → CompanyActivity.production-DeT7xKMQ.js} +1 -1
- package/ui-dist/assets/{CompanyExport-DT63ZLv6.js → CompanyExport-BLyfRuJ5.js} +1 -1
- package/ui-dist/assets/{CompanySkills.production-74w4FgIl.js → CompanySkills.production-BQbzHmtn.js} +1 -1
- package/ui-dist/assets/{Costs.production-B0j-0g6E.js → Costs.production-5RBWzUGB.js} +1 -1
- package/ui-dist/assets/{OrgChart.production-BdkDUcmQ.js → OrgChart.production-D_5tQ3tx.js} +1 -1
- package/ui-dist/assets/{RoutineDetail.production-grakkTMs.js → RoutineDetail.production-Dd163Q10.js} +1 -1
- package/ui-dist/assets/{Routines.production-qCZmmUZQ.js → Routines.production-g9pJjOvz.js} +1 -1
- package/ui-dist/assets/{definition-6ehunDEH.js → definition-BtNjkL-Z.js} +1 -1
- package/ui-dist/assets/{index-DrB9-JGQ.js → index-B_VCIODd.js} +4 -4
- package/ui-dist/index.html +1 -1
- package/ui-dist/sw.js +1 -1
|
@@ -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";
|
|
@@ -335,6 +336,11 @@ const PROCESS_LOSS_ERROR_CODES = {
|
|
|
335
336
|
function isTransientWorkspaceGitScanCode(code) {
|
|
336
337
|
return code === WORKSPACE_GIT_SCAN_ERROR_CODES.timeout || code === WORKSPACE_GIT_SCAN_ERROR_CODES.saturated;
|
|
337
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
|
+
};
|
|
338
344
|
const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_MAX_ATTEMPTS = BOUNDED_TRANSIENT_HEARTBEAT_RETRY_DELAYS_MS.length;
|
|
339
345
|
export { INTERACTION_CONTINUATION_INFRA_RETRY_REASON, INTERACTION_CONTINUATION_INFRA_WAKE_REASON, };
|
|
340
346
|
const INTERACTION_CONTINUATION_INFRA_MAX_ATTEMPTS = 2;
|
|
@@ -807,6 +813,15 @@ const nativeSessionResumeDispatchTimers = new Map();
|
|
|
807
813
|
// resolveHeartbeatSchedulingSuppression() check and every heartbeatService()
|
|
808
814
|
// instance see the same drain.
|
|
809
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
|
+
}
|
|
810
825
|
function readTaskDrain(now) {
|
|
811
826
|
if (taskDrainState &&
|
|
812
827
|
taskDrainState.expiresAt !== null &&
|
|
@@ -3926,6 +3941,7 @@ const SESSION_CONFIG_FINGERPRINT_KEY = "__paperclipConfigFingerprint";
|
|
|
3926
3941
|
const SESSION_CONFIG_FINGERPRINT_VERSION_KEY = "__paperclipConfigFingerprintVersion";
|
|
3927
3942
|
const SESSION_CONFIG_CATEGORIES_KEY = "__paperclipConfigCategories";
|
|
3928
3943
|
const SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY = "__paperclipConfigCategoryFingerprints";
|
|
3944
|
+
const SESSION_CONFIG_RESUME_FINGERPRINT_KEY = "__paperclipConfigResumeFingerprint";
|
|
3929
3945
|
const PAPERCLIP_SESSION_METADATA_KEYS = new Set([
|
|
3930
3946
|
SESSION_AI_CREDENTIAL_IDENTITY_KEY,
|
|
3931
3947
|
SESSION_CONFIGURED_MODEL_KEY,
|
|
@@ -3933,6 +3949,7 @@ const PAPERCLIP_SESSION_METADATA_KEYS = new Set([
|
|
|
3933
3949
|
SESSION_CONFIG_FINGERPRINT_VERSION_KEY,
|
|
3934
3950
|
SESSION_CONFIG_CATEGORIES_KEY,
|
|
3935
3951
|
SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY,
|
|
3952
|
+
SESSION_CONFIG_RESUME_FINGERPRINT_KEY,
|
|
3936
3953
|
]);
|
|
3937
3954
|
const WORKSPACE_CONFIG_FINGERPRINT_METADATA_KEY = "configFingerprint";
|
|
3938
3955
|
const EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES = [
|
|
@@ -3957,6 +3974,19 @@ const EFFECTIVE_RUN_WORKSPACE_CONFIG_CATEGORIES = [
|
|
|
3957
3974
|
"environment",
|
|
3958
3975
|
"realization",
|
|
3959
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 };
|
|
3960
3990
|
/**
|
|
3961
3991
|
* Projectless native runs bind their immutable envelope to the run id even
|
|
3962
3992
|
* though no project-scoped execution-workspace row exists. Treat that value
|
|
@@ -4127,6 +4157,7 @@ function readConfigFingerprintFromSessionParams(sessionParams) {
|
|
|
4127
4157
|
version,
|
|
4128
4158
|
categories: readConfigCategoriesFromSessionParams(sessionParams),
|
|
4129
4159
|
categoryFingerprints: parseStoredConfigCategoryFingerprints(sessionParams[SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY]),
|
|
4160
|
+
resumeFingerprint: readNonEmptyString(sessionParams[SESSION_CONFIG_RESUME_FINGERPRINT_KEY]),
|
|
4130
4161
|
};
|
|
4131
4162
|
}
|
|
4132
4163
|
function describeEffectiveRunConfigCategories(categories) {
|
|
@@ -4375,9 +4406,10 @@ function buildSessionConfigCategoryValues(input) {
|
|
|
4375
4406
|
export async function buildEffectiveRunSessionConfigMetadata(input) {
|
|
4376
4407
|
const secretManifest = input.secretManifest ?? [];
|
|
4377
4408
|
const instructions = await resolveInstructionsConfigFingerprintMetadata(input.effectiveAdapterConfig);
|
|
4409
|
+
const fingerprintAdapterConfig = managedAiSessionFingerprintConfig(input.effectiveAdapterConfig, input.managedAiHome);
|
|
4378
4410
|
const categoryValues = buildSessionConfigCategoryValues({
|
|
4379
4411
|
adapterType: input.adapterType,
|
|
4380
|
-
effectiveAdapterConfig:
|
|
4412
|
+
effectiveAdapterConfig: fingerprintAdapterConfig,
|
|
4381
4413
|
agentRuntimeConfig: input.agentRuntimeConfig,
|
|
4382
4414
|
instructions,
|
|
4383
4415
|
issueOverrides: input.issueOverrides,
|
|
@@ -4400,11 +4432,26 @@ export async function buildEffectiveRunSessionConfigMetadata(input) {
|
|
|
4400
4432
|
subcategories: EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES,
|
|
4401
4433
|
secretManifest,
|
|
4402
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
|
+
});
|
|
4403
4449
|
return {
|
|
4404
4450
|
version: EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION,
|
|
4405
4451
|
fingerprint: fingerprints.sessionFingerprint.fingerprint,
|
|
4406
4452
|
categories: [...EFFECTIVE_RUN_SESSION_CONFIG_CATEGORIES],
|
|
4407
4453
|
categoryFingerprints,
|
|
4454
|
+
resumeFingerprint,
|
|
4408
4455
|
fingerprints,
|
|
4409
4456
|
};
|
|
4410
4457
|
}
|
|
@@ -4618,6 +4665,8 @@ function attachPaperclipSessionMetadataToSessionParams(sessionParams, configured
|
|
|
4618
4665
|
next[SESSION_CONFIG_CATEGORIES_KEY] = configMetadata.categories;
|
|
4619
4666
|
next[SESSION_CONFIG_CATEGORY_FINGERPRINTS_KEY] =
|
|
4620
4667
|
configMetadata.categoryFingerprints;
|
|
4668
|
+
next[SESSION_CONFIG_RESUME_FINGERPRINT_KEY] =
|
|
4669
|
+
configMetadata.resumeFingerprint;
|
|
4621
4670
|
}
|
|
4622
4671
|
return next;
|
|
4623
4672
|
}
|
|
@@ -4647,6 +4696,17 @@ export function stripPaperclipSessionMetadataFromSessionParams(sessionParams) {
|
|
|
4647
4696
|
}
|
|
4648
4697
|
return next;
|
|
4649
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
|
+
}
|
|
4650
4710
|
export function resolveTaskSessionConfigFreshness(input) {
|
|
4651
4711
|
if (!input.hasTaskSession) {
|
|
4652
4712
|
return {
|
|
@@ -4684,7 +4744,14 @@ export function resolveTaskSessionConfigFreshness(input) {
|
|
|
4684
4744
|
previous: storedConfig.categoryFingerprints,
|
|
4685
4745
|
next: input.configMetadata.categoryFingerprints,
|
|
4686
4746
|
});
|
|
4687
|
-
|
|
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
|
+
}
|
|
4688
4755
|
}
|
|
4689
4756
|
}
|
|
4690
4757
|
if (input.wakeResetReason)
|
|
@@ -6266,18 +6333,20 @@ export function resolveHeartbeatSchedulingSuppression(env = process.env, overrid
|
|
|
6266
6333
|
isTruthyRuntimeEnvValue(env.PAPERCLIP_RESTORE_IN_PROGRESS)) {
|
|
6267
6334
|
return { suppressed: true, reason: "database_restore_in_progress" };
|
|
6268
6335
|
}
|
|
6336
|
+
if (serverShutdownSignal !== null) {
|
|
6337
|
+
return { suppressed: true, reason: "server_shutdown" };
|
|
6338
|
+
}
|
|
6269
6339
|
if (readTaskDrain(new Date()) !== null) {
|
|
6270
6340
|
return { suppressed: true, reason: "task_drain" };
|
|
6271
6341
|
}
|
|
6272
6342
|
return { suppressed: false, reason: null };
|
|
6273
6343
|
}
|
|
6274
6344
|
export function heartbeatService(db, options = {}) {
|
|
6275
|
-
|
|
6345
|
+
const runtimeEnv = options.runtimeEnv ?? process.env;
|
|
6276
6346
|
const instanceSettings = instanceSettingsService(db);
|
|
6277
6347
|
const getCurrentUserRedactionOptions = async () => ({
|
|
6278
6348
|
enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs,
|
|
6279
6349
|
});
|
|
6280
|
-
const runtimeEnv = options.runtimeEnv ?? process.env;
|
|
6281
6350
|
const inWorktreeRuntime = isTruthyRuntimeEnvValue(runtimeEnv.PAPERCLIP_IN_WORKTREE);
|
|
6282
6351
|
// Preview worktree instances suppress the run engine by default. Users can lift
|
|
6283
6352
|
// that per-worktree via the `enableWorktreeRunExecution` experimental setting
|
|
@@ -10078,7 +10147,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
10078
10147
|
return (readNonEmptyString(parseObject(input.adapterConfig).engine) !== "cli");
|
|
10079
10148
|
}
|
|
10080
10149
|
async function prepareHotRestartShutdown(signal, now = new Date()) {
|
|
10081
|
-
|
|
10150
|
+
beginHeartbeatServerShutdown(signal);
|
|
10082
10151
|
const idleSessions = await closeIdleWarmNativeSessionsForRestart();
|
|
10083
10152
|
if (idleSessions.failed > 0) {
|
|
10084
10153
|
logger.warn({ idleSessions }, "idle native sessions could not checkpoint before controller shutdown");
|
|
@@ -10129,6 +10198,25 @@ export function heartbeatService(db, options = {}) {
|
|
|
10129
10198
|
...intent,
|
|
10130
10199
|
previousServerVersion: intent.previousServerVersion ?? serverVersion,
|
|
10131
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
|
+
}
|
|
10132
10220
|
const serverStdioRuns = activeRuns.filter(isServerStdioBoundHotRestartRun);
|
|
10133
10221
|
if (serverStdioRuns.length > 0) {
|
|
10134
10222
|
const activeServerStdioRunIds = serverStdioRuns.map(({ run }) => run.id);
|
|
@@ -10305,18 +10393,15 @@ export function heartbeatService(db, options = {}) {
|
|
|
10305
10393
|
classify(candidate, "finalized_while_down", `run_status_${run.status}`, patch);
|
|
10306
10394
|
continue;
|
|
10307
10395
|
}
|
|
10308
|
-
const
|
|
10396
|
+
const hasSelectiveDrain = (intent.drainReason === "active_acp_run" ||
|
|
10397
|
+
intent.drainReason === "service_stop_kills_children") &&
|
|
10309
10398
|
(intent.drainRunIds?.length ?? 0) > 0;
|
|
10310
|
-
if (
|
|
10399
|
+
if (hasSelectiveDrain &&
|
|
10311
10400
|
intent.drainRunIds?.includes(candidate.runId)) {
|
|
10312
|
-
// A selective ACP drain is expected to persist a terminal row before
|
|
10313
|
-
// the new server starts. If the process was terminated but that write
|
|
10314
|
-
// failed, surface the run as lost instead of hiding it as an expected
|
|
10315
|
-
// drain skip.
|
|
10316
10401
|
classify(candidate, "lost", "selective_drain_not_finalized", patch);
|
|
10317
10402
|
continue;
|
|
10318
10403
|
}
|
|
10319
|
-
if (intent.drainRequired && !
|
|
10404
|
+
if (intent.drainRequired && !hasSelectiveDrain) {
|
|
10320
10405
|
classify(candidate, "skipped", "drain_required", patch);
|
|
10321
10406
|
continue;
|
|
10322
10407
|
}
|
|
@@ -10544,6 +10629,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
10544
10629
|
};
|
|
10545
10630
|
}
|
|
10546
10631
|
async function drainRunningRunsForShutdown(signal, now = new Date(), runIds = null) {
|
|
10632
|
+
beginHeartbeatServerShutdown(signal);
|
|
10547
10633
|
const selectedRunIds = runIds ? [...new Set(runIds)] : null;
|
|
10548
10634
|
if (selectedRunIds?.length === 0) {
|
|
10549
10635
|
return {
|
|
@@ -10566,6 +10652,27 @@ export function heartbeatService(db, options = {}) {
|
|
|
10566
10652
|
const interruptedRunIds = [];
|
|
10567
10653
|
const retryRunIds = [];
|
|
10568
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
|
+
}
|
|
10569
10676
|
for (const { run, agent } of activeRuns) {
|
|
10570
10677
|
// Shutdown owns only this boot's legacy executions. Expired foreign
|
|
10571
10678
|
// owners belong to the reaper, not another container's drain.
|
|
@@ -10639,7 +10746,12 @@ export function heartbeatService(db, options = {}) {
|
|
|
10639
10746
|
runtimeMode: run.runtimeMode,
|
|
10640
10747
|
});
|
|
10641
10748
|
}
|
|
10642
|
-
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) {
|
|
10643
10755
|
await terminateHeartbeatRunProcess({
|
|
10644
10756
|
pid: running.child.pid,
|
|
10645
10757
|
processGroupId: running.processGroupId,
|
|
@@ -10665,8 +10777,20 @@ export function heartbeatService(db, options = {}) {
|
|
|
10665
10777
|
errorMessage: message,
|
|
10666
10778
|
}),
|
|
10667
10779
|
});
|
|
10668
|
-
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
|
+
}
|
|
10669
10792
|
continue;
|
|
10793
|
+
}
|
|
10670
10794
|
let interrupted = interruptedStatus.run;
|
|
10671
10795
|
await setWakeupStatus(run.wakeupRequestId, "cancelled", {
|
|
10672
10796
|
finishedAt: now,
|
|
@@ -15625,6 +15749,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
15625
15749
|
taskSessionParams: taskSession?.sessionParamsJson ?? taskSessionDecodedParams,
|
|
15626
15750
|
configMetadata: sessionConfigMetadata,
|
|
15627
15751
|
wakeResetReason: wakeSessionResetReason,
|
|
15752
|
+
processLossContinuation: isProcessLossContinuationRun(run, context),
|
|
15628
15753
|
preserveLegacySessionWithoutConfigMetadata: acceptedPlanContinuationWake && !acceptedPlanWakeRoutingDecision,
|
|
15629
15754
|
});
|
|
15630
15755
|
const resetTaskSession = shouldResetTaskSessionForWake(context) || sessionConfigFreshness.reset;
|
|
@@ -18302,7 +18427,12 @@ export function heartbeatService(db, options = {}) {
|
|
|
18302
18427
|
failedProcessRunCancellations.get(run.id);
|
|
18303
18428
|
await processCancellation?.settled;
|
|
18304
18429
|
let outcome;
|
|
18430
|
+
let processLossErrorCode = null;
|
|
18305
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);
|
|
18306
18436
|
if (isHeartbeatRunTerminalStatus(latestRun?.status)) {
|
|
18307
18437
|
outcome = latestRun.status;
|
|
18308
18438
|
}
|
|
@@ -18318,6 +18448,11 @@ export function heartbeatService(db, options = {}) {
|
|
|
18318
18448
|
? "cancelled"
|
|
18319
18449
|
: "failed";
|
|
18320
18450
|
}
|
|
18451
|
+
else if (shutdownTerminatedRunIds.has(run.id) &&
|
|
18452
|
+
!paperclipCancellationRecorded) {
|
|
18453
|
+
outcome = "interrupted";
|
|
18454
|
+
processLossErrorCode = "server_shutdown_interrupted";
|
|
18455
|
+
}
|
|
18321
18456
|
else if (adapterResult.timedOut) {
|
|
18322
18457
|
outcome = "timed_out";
|
|
18323
18458
|
}
|
|
@@ -18329,6 +18464,20 @@ export function heartbeatService(db, options = {}) {
|
|
|
18329
18464
|
}
|
|
18330
18465
|
else {
|
|
18331
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
|
+
}
|
|
18332
18481
|
}
|
|
18333
18482
|
const nextSessionState = resolveNextSessionState({
|
|
18334
18483
|
adapterType: agent.adapterType,
|
|
@@ -18352,18 +18501,21 @@ export function heartbeatService(db, options = {}) {
|
|
|
18352
18501
|
? (latestRun?.error ?? adapterResult.errorMessage ?? "Cancelled")
|
|
18353
18502
|
: outcome === "succeeded"
|
|
18354
18503
|
? null
|
|
18355
|
-
:
|
|
18356
|
-
|
|
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);
|
|
18357
18508
|
const recordedResponsibleUserDenialCode = normalizeResponsibleUserDenialCode(latestRun?.errorCode);
|
|
18358
|
-
const runErrorCode =
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18363
|
-
|
|
18364
|
-
|
|
18365
|
-
|
|
18366
|
-
|
|
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);
|
|
18367
18519
|
let logSummary = null;
|
|
18368
18520
|
if (handle) {
|
|
18369
18521
|
logSummary = await runLogStore.finalize(handle);
|
|
@@ -18388,7 +18540,9 @@ export function heartbeatService(db, options = {}) {
|
|
|
18388
18540
|
? "cancelled"
|
|
18389
18541
|
: outcome === "timed_out"
|
|
18390
18542
|
? "timed_out"
|
|
18391
|
-
: "
|
|
18543
|
+
: outcome === "interrupted"
|
|
18544
|
+
? "interrupted"
|
|
18545
|
+
: "failed";
|
|
18392
18546
|
const cacheAdjustedCostUsd = resolveCacheAdjustedCostUsd(adapterResult);
|
|
18393
18547
|
const usageJson = normalizedUsage ||
|
|
18394
18548
|
adapterResult.costUsd != null ||
|
|
@@ -18512,7 +18666,11 @@ export function heartbeatService(db, options = {}) {
|
|
|
18512
18666
|
persistedRun =
|
|
18513
18667
|
(await classifyAndPersistRunLiveness(persistedRun, persistedResultJson)) ?? persistedRun;
|
|
18514
18668
|
}
|
|
18515
|
-
await setWakeupStatus(run.wakeupRequestId, outcome === "succeeded"
|
|
18669
|
+
await setWakeupStatus(run.wakeupRequestId, outcome === "succeeded"
|
|
18670
|
+
? "completed"
|
|
18671
|
+
: outcome === "interrupted"
|
|
18672
|
+
? "cancelled"
|
|
18673
|
+
: status, {
|
|
18516
18674
|
finishedAt: new Date(),
|
|
18517
18675
|
error: runErrorMessage,
|
|
18518
18676
|
});
|
|
@@ -18654,7 +18812,10 @@ export function heartbeatService(db, options = {}) {
|
|
|
18654
18812
|
catch (err) {
|
|
18655
18813
|
await onLog("stderr", `[paperclip] Failed to resolve run presentation: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
18656
18814
|
}
|
|
18657
|
-
if (
|
|
18815
|
+
if (processLossErrorCode) {
|
|
18816
|
+
await enqueueProcessLossRetry(livenessRun, agent, new Date());
|
|
18817
|
+
}
|
|
18818
|
+
else if (outcome === "failed" && isMaxTurnExhaustionRun(livenessRun)) {
|
|
18658
18819
|
const policy = parseMaxTurnContinuationPolicy(agent);
|
|
18659
18820
|
if (policy.enabled && policy.maxAttempts > 0) {
|
|
18660
18821
|
await scheduleBoundedRetryForRun(livenessRun, agent, {
|
|
@@ -19370,6 +19531,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
19370
19531
|
finally {
|
|
19371
19532
|
controllerLease.stop();
|
|
19372
19533
|
activeRunExecutions.delete(run.id);
|
|
19534
|
+
shutdownTerminatedRunIds.delete(run.id);
|
|
19373
19535
|
clearLocalCliRunStarting(run.id);
|
|
19374
19536
|
// A failed owned Stop remains visible until this exact executor settles,
|
|
19375
19537
|
// including a graceful exit result arriving after the cancellation error.
|
|
@@ -19393,7 +19555,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
19393
19555
|
}
|
|
19394
19556
|
if (!nativeSessionResumeScheduled &&
|
|
19395
19557
|
!nativeWorkspaceFinalizeScheduled &&
|
|
19396
|
-
|
|
19558
|
+
serverShutdownSignal === null) {
|
|
19397
19559
|
if (latestRun)
|
|
19398
19560
|
await resumeRemoteStopComments(latestRun).catch(err => {
|
|
19399
19561
|
logger.warn({ err, runId: run.id }, "failed to resume user messages after remote Stop");
|
|
@@ -19651,7 +19813,8 @@ export function heartbeatService(db, options = {}) {
|
|
|
19651
19813
|
// continuation wake landed mid-drain left its issue in `todo` with no
|
|
19652
19814
|
// run and no path until a person noticed.
|
|
19653
19815
|
if (schedulingSuppression.suppressed &&
|
|
19654
|
-
schedulingSuppression.reason !== "task_drain"
|
|
19816
|
+
schedulingSuppression.reason !== "task_drain" &&
|
|
19817
|
+
schedulingSuppression.reason !== "server_shutdown") {
|
|
19655
19818
|
await writeSkippedHeartbeatRequest("heartbeat.scheduling_suppressed", {
|
|
19656
19819
|
reason: schedulingSuppression.reason,
|
|
19657
19820
|
});
|
|
@@ -21990,6 +22153,7 @@ export function heartbeatService(db, options = {}) {
|
|
|
21990
22153
|
dispatchPendingNativeStatusWakeups,
|
|
21991
22154
|
triggerIssueMonitor,
|
|
21992
22155
|
reportRunActivity: clearDetachedRunWarning,
|
|
22156
|
+
beginServerShutdown: beginHeartbeatServerShutdown,
|
|
21993
22157
|
prepareHotRestartShutdown,
|
|
21994
22158
|
reconcileHotRestartAdoption,
|
|
21995
22159
|
recoverNativeRunsAfterRestart,
|