mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🛡️ fix: Codex final round — paused-job TTL + pendingAction in resume contract
Two of three findings on e7d9cf21b6 (third deferred to Slice B):
- K2 paused-job TTL: a paused (requires_action) job no longer inherits the
20-minute running TTL — it uses a dedicated requires_action backstop
(default 24h, configurable) so a no-expiry approval (the buildPendingAction
default), which the API treats as live, isn't evicted by Redis mid-window.
A longer pendingAction.expiresAt still extends beyond the backstop.
- K3 resume contract: pendingAction is now carried on the typed ResumeState
(data-provider) and populated by getResumeState for a live paused job, so a
reloading / cross-replica client can rebuild the prompt from resumeState
(the contract useResumeOnLoad actually reads), not just a loose status field.
Deferred (Slice B): K1 — emit a terminal SSE event on expiry so already-
subscribed clients close. Requires the manager/eventTransport layer (the
store-level lifecycle and cleanup loops have no transport access) and has no
live subscriber until the Slice B subscribe/resume path exists; tracked there.
tsc + lint clean; policy + type-contract specs pass.
This commit is contained in:
parent
e7d9cf21b6
commit
c27a2b20d0
3 changed files with 33 additions and 7 deletions
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { InMemoryEventTransport } from './implementations/InMemoryEventTransport';
|
||||
import { InMemoryJobStore } from './implementations/InMemoryJobStore';
|
||||
import { filterPersistableAbortContent } from './abortContent';
|
||||
import { isPendingActionStale } from './interfaces/IJobStore';
|
||||
import { ApprovalLifecycle } from './ApprovalLifecycle';
|
||||
|
||||
/** Error surfaced to any client still attached when a stale/hung job is reaped. */
|
||||
|
|
@ -1478,6 +1479,12 @@ class GenerationJobManagerClass {
|
|||
replayEvents,
|
||||
collectedUsage,
|
||||
contextUsage,
|
||||
// Carry the live pending approval in the resume contract so a reloading /
|
||||
// cross-replica client can rebuild the prompt from resumeState.
|
||||
pendingAction:
|
||||
jobData.status === 'requires_action' && !isPendingActionStale(jobData)
|
||||
? jobData.pendingAction
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,14 @@ const DEFAULT_TTL = {
|
|||
runStepsAfterComplete: 0,
|
||||
/** Safety-net TTL for per-user job tracking sets (24 hours). Refreshed on each createJob. */
|
||||
userJobsSet: 86400,
|
||||
/**
|
||||
* Backstop TTL for a job paused for human review (24 hours). A paused job is
|
||||
* NOT a hung generation, so it must not inherit the 20-minute running TTL —
|
||||
* an approval with no explicit `expiresAt` is "live" per the API contract and
|
||||
* would otherwise be evicted mid-window. A pendingAction with a longer
|
||||
* `expiresAt` extends beyond this (see pauseTtlSeconds).
|
||||
*/
|
||||
requiresAction: 86400,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -117,6 +125,8 @@ export interface RedisJobStoreOptions {
|
|||
runStepsAfterCompleteTtl?: number;
|
||||
/** TTL for per-user job tracking sets in seconds (default: 86400 = 24 hours). 0 = no TTL. */
|
||||
userJobsSetTtl?: number;
|
||||
/** Backstop TTL for a paused (requires_action) job in seconds (default: 86400 = 24 hours). */
|
||||
requiresActionTtl?: number;
|
||||
}
|
||||
|
||||
export class RedisJobStore implements IJobStore {
|
||||
|
|
@ -152,6 +162,7 @@ export class RedisJobStore implements IJobStore {
|
|||
chunksAfterComplete: options?.chunksAfterCompleteTtl ?? DEFAULT_TTL.chunksAfterComplete,
|
||||
runStepsAfterComplete: options?.runStepsAfterCompleteTtl ?? DEFAULT_TTL.runStepsAfterComplete,
|
||||
userJobsSet: options?.userJobsSetTtl ?? DEFAULT_TTL.userJobsSet,
|
||||
requiresAction: options?.requiresActionTtl ?? DEFAULT_TTL.requiresAction,
|
||||
};
|
||||
// Detect cluster mode using ioredis's isCluster property
|
||||
this.isCluster = (redis as Cluster).isCluster === true;
|
||||
|
|
@ -351,19 +362,21 @@ export class RedisJobStore implements IJobStore {
|
|||
}
|
||||
|
||||
/**
|
||||
* Live-key TTL (seconds) for a paused job. Defaults to the running TTL but
|
||||
* extends to cover a pendingAction whose `expiresAt` is farther out, plus a
|
||||
* grace margin so a decision arriving right at the deadline can still resume.
|
||||
* Without this, a long approval window (e.g. 1h) on the default 20m stream
|
||||
* TTL would let Redis evict the paused job mid-window.
|
||||
* Live-key TTL (seconds) for a paused job. A paused job isn't a hung
|
||||
* generation, so it uses the longer requires_action backstop rather than the
|
||||
* running TTL — otherwise a no-expiry approval (the buildPendingAction
|
||||
* default), which the API treats as "live", would be evicted after the 20m
|
||||
* running window. A pendingAction with an `expiresAt` farther out than the
|
||||
* backstop extends to cover it, plus a grace margin so a decision arriving
|
||||
* right at the deadline can still resume.
|
||||
*/
|
||||
private pauseTtlSeconds(pendingAction?: Agents.PendingAction): number {
|
||||
const exp = pendingAction?.expiresAt;
|
||||
if (exp == null) {
|
||||
return this.ttl.running;
|
||||
return this.ttl.requiresAction;
|
||||
}
|
||||
const secondsUntilExpiry = Math.ceil((exp - Date.now()) / 1000) + 60;
|
||||
return Math.max(this.ttl.running, secondsUntilExpiry);
|
||||
return Math.max(this.ttl.requiresAction, secondsUntilExpiry);
|
||||
}
|
||||
|
||||
/** The membership set a status belongs to; terminal statuses have none. */
|
||||
|
|
|
|||
|
|
@ -246,6 +246,12 @@ export namespace Agents {
|
|||
collectedUsage?: TTokenUsageEvent[];
|
||||
/** Latest context window snapshot; restores the usage gauge on resume */
|
||||
contextUsage?: TContextUsageEvent;
|
||||
/**
|
||||
* Live pending approval when the run is paused for human review. Carried in
|
||||
* the resume contract (not just /chat/status) so a reloading or
|
||||
* cross-replica client can rebuild and render the prompt from `resumeState`.
|
||||
*/
|
||||
pendingAction?: PendingAction;
|
||||
}
|
||||
/**
|
||||
* Represents a run step delta i.e. any changed fields on a run step during
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue