johns-harness 2026.9.31 → 2026.9.32
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/CHANGELOG.md +8 -0
- package/dist/auth-profiles-5CHn7vq1.js +80 -0
- package/dist/compact-B247y5Qt.js +7 -1
- package/dist/config-koj5M_EO.js +79 -0
- package/dist/daemon-cli.js +79 -0
- package/dist/gateway-cli-B7fBU7gD.js +81 -1
- package/dist/gateway-cli-BCNMwzNq.js +81 -1
- package/dist/model-selection-BGlGpPgM.js +79 -0
- package/dist/model-selection-BU6wl1le.js +79 -0
- package/dist/model-selection-L7RMwsG-.js +80 -0
- package/dist/pi-embedded-C6ITuRXf.js +7 -1
- package/dist/pi-embedded-DoQsYfIY.js +7 -1
- package/dist/plugin-sdk/bluebubbles.js +1 -0
- package/dist/plugin-sdk/config-4Fgm-yEH.js +79 -0
- package/dist/plugin-sdk/config-Di-lyCdh.js +79 -0
- package/dist/plugin-sdk/discord.js +79 -0
- package/dist/plugin-sdk/dispatch-BP0viZiL.js +7 -1
- package/dist/plugin-sdk/dispatch-CQsjmw7g.js +7 -1
- package/dist/plugin-sdk/dispatch-Cerq29sy.js +7 -1
- package/dist/plugin-sdk/dispatch-Cndjtt0g.js +7 -1
- package/dist/plugin-sdk/dispatch-UogiJYul.js +7 -1
- package/dist/plugin-sdk/feishu.js +1 -0
- package/dist/plugin-sdk/googlechat.js +1 -0
- package/dist/plugin-sdk/imessage.js +1 -0
- package/dist/plugin-sdk/model-auth-CX9cPHdC.js +79 -0
- package/dist/plugin-sdk/model-auth-Ciehz0x5.js +79 -0
- package/dist/plugin-sdk/model-auth-DVyo6JSX.js +79 -0
- package/dist/plugin-sdk/model-auth-kiHYHGrs.js +79 -0
- package/dist/plugin-sdk/model-selection-Dbj4jDYM.js +79 -0
- package/dist/plugin-sdk/reply-DbZnH8-h.js +7 -1
- package/dist/plugin-sdk/signal.js +1 -0
- package/dist/plugin-sdk/slack.js +1 -0
- package/dist/plugin-sdk/telegram.js +1 -0
- package/dist/plugin-sdk/tlon.js +1 -0
- package/dist/plugin-sdk/twitch.js +1 -0
- package/dist/plugin-sdk/zalo.js +1 -0
- package/dist/plugin-sdk/zalouser.js +1 -0
- package/dist/reply-C5LKjXcC.js +7 -1
- package/docs/PATCHES.md +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.9.32
|
|
4
|
+
|
|
5
|
+
- Family 79: recurring cron jobs retry transient errors (rate limit, overloaded, network, timeout, server error) at min(next natural slot, now + `cron.retry` backoff) up to `cron.retry.maxAttempts`, then return to the normal schedule, instead of burning the slot. Isolated cron runs always carry the default fallback chain, so a throttled job model falls through even when its provider differs from the default primary.
|
|
6
|
+
- New `cron.failureLog`: a file path that receives one line per cron failure, retries included: `- timestamp | cron-fail | job | error class | attempt n | next retry <time|none>`. Failures go to the file; Telegram stays reserved for each job's own announce.
|
|
7
|
+
- The `cron` tool waits 180s for the gateway (was 60s), and a cron gateway timeout no longer adds the automatic "⚠️ ⏰ Cron failed" line to the chat reply (the call usually completed server-side).
|
|
8
|
+
- Bare model shorthands always resolve to the newest shipped model. A user alias that pins an older model of the same line (for example `opus` on `claude-opus-5` after `opus` moved to `claude-opus-5-5`) yields to the shipped default, and the old model keeps a versioned alias (`opus5`, `sonnet46`, `gpt55`). Applied on every config load and written back to the config file once at gateway start, with a log line naming each move. Custom aliases and deliberate cross-provider pins are untouched.
|
|
9
|
+
- Exact replay `scripts/patch-cron-reliability.py` (37 files). Roll back with `npm i -g johns-harness@2026.9.31`.
|
|
10
|
+
|
|
3
11
|
## 2026.9.31
|
|
4
12
|
|
|
5
13
|
- Family 78: Claude Opus 5.5 (`anthropic/claude-opus-5-5`) is a first-class model. Id, $4/$20 pricing, $0.20 cache reads, 1M native context (300K owner cap), 128K max output and always-on adaptive thinking confirmed against the Anthropic model docs on 2026-09-23. Effort maps 1:1 (xhigh and max are distinct), temperature is dropped, and `opus` now resolves to Opus 5.5 on every install; `opus5` keeps Claude Opus 5 reachable and the opus-5 catalog entry is unchanged. Exact replay via `scripts/patch-opus-5-5-model.py`; verifier checks 78.1-78.3. Roll back with `npm i -g johns-harness@2026.9.30`.
|
|
@@ -5245,6 +5245,83 @@ function applyTalkApiKey(config) {
|
|
|
5245
5245
|
function applyTalkConfigNormalization(config) {
|
|
5246
5246
|
return normalizeTalkConfig(config);
|
|
5247
5247
|
}
|
|
5248
|
+
/* JOHNNESS_PATCH_ALIAS_LATEST_79: bare shorthands (DEFAULT_MODEL_ALIASES keys without digits) always resolve
|
|
5249
|
+
* to the newest shipped model in their line. A user alias that claims one but pins an older model of the same
|
|
5250
|
+
* provider and line is moved: the shipped model takes the shorthand and the old model keeps a versioned alias
|
|
5251
|
+
* (opus5, sonnet46, gpt55). Custom aliases, newer pins and cross-provider or cross-line choices are untouched.
|
|
5252
|
+
* Replaces entries of the given models map in place (never mutates an entry); returns the moves. */
|
|
5253
|
+
function johnnessMigrateStaleShorthands79(models) {
|
|
5254
|
+
const moves = [];
|
|
5255
|
+
if (!models || typeof models !== "object" || Array.isArray(models)) return moves;
|
|
5256
|
+
const refOf = (raw) => {
|
|
5257
|
+
const text = String(raw ?? "").trim().toLowerCase();
|
|
5258
|
+
const slash = text.indexOf("/");
|
|
5259
|
+
return slash > 0 ? {
|
|
5260
|
+
provider: text.slice(0, slash),
|
|
5261
|
+
model: text.slice(slash + 1)
|
|
5262
|
+
} : null;
|
|
5263
|
+
};
|
|
5264
|
+
const versionOf = (model) => {
|
|
5265
|
+
const parts = [];
|
|
5266
|
+
for (const raw of model.match(/\d+/g) ?? []) {
|
|
5267
|
+
if (raw.length >= 4) break;
|
|
5268
|
+
parts.push(Number(raw));
|
|
5269
|
+
}
|
|
5270
|
+
return parts;
|
|
5271
|
+
};
|
|
5272
|
+
const isOlder = (alias, user, shipped) => {
|
|
5273
|
+
if (user.provider !== shipped.provider || user.model === shipped.model) return false;
|
|
5274
|
+
const tokens = new Set(user.model.split(/[^a-z0-9]+/).filter(Boolean));
|
|
5275
|
+
if (!alias.split("-").every((token) => tokens.has(token))) return false;
|
|
5276
|
+
const a = versionOf(user.model);
|
|
5277
|
+
const b = versionOf(shipped.model);
|
|
5278
|
+
if (a.length === 0 || b.length === 0) return false;
|
|
5279
|
+
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
5280
|
+
const x = a[i] ?? 0;
|
|
5281
|
+
const y = b[i] ?? 0;
|
|
5282
|
+
if (x !== y) return x < y;
|
|
5283
|
+
}
|
|
5284
|
+
return false;
|
|
5285
|
+
};
|
|
5286
|
+
const versionedAliasFor = (alias, userKey, user) => {
|
|
5287
|
+
for (const [name, target] of Object.entries(DEFAULT_MODEL_ALIASES)) if (name !== alias && /\d/.test(name) && name.startsWith(alias) && String(target).toLowerCase() === userKey) return name;
|
|
5288
|
+
const digits = versionOf(user.model).join("");
|
|
5289
|
+
return digits ? `${alias}${digits}` : void 0;
|
|
5290
|
+
};
|
|
5291
|
+
for (const key of Object.keys(models)) {
|
|
5292
|
+
const entry = models[key];
|
|
5293
|
+
const alias = String(entry?.alias ?? "").trim().toLowerCase();
|
|
5294
|
+
if (!alias || /\d/.test(alias) || !Object.hasOwn(DEFAULT_MODEL_ALIASES, alias)) continue;
|
|
5295
|
+
const user = refOf(key);
|
|
5296
|
+
const shipped = refOf(DEFAULT_MODEL_ALIASES[alias]);
|
|
5297
|
+
if (!user || !shipped || !isOlder(alias, user, shipped)) continue;
|
|
5298
|
+
const userKey = `${user.provider}/${user.model}`;
|
|
5299
|
+
const taken = new Set(Object.entries(models).filter(([other]) => other !== key).map(([, value]) => String(value?.alias ?? "").trim().toLowerCase()).filter(Boolean));
|
|
5300
|
+
let versioned = versionedAliasFor(alias, userKey, user);
|
|
5301
|
+
if (versioned && (taken.has(versioned) || Object.hasOwn(DEFAULT_MODEL_ALIASES, versioned) && String(DEFAULT_MODEL_ALIASES[versioned]).toLowerCase() !== userKey)) versioned = void 0;
|
|
5302
|
+
const { alias: _previousAlias, ...rest } = entry;
|
|
5303
|
+
models[key] = versioned ? {
|
|
5304
|
+
...rest,
|
|
5305
|
+
alias: versioned
|
|
5306
|
+
} : rest;
|
|
5307
|
+
const shippedKey = Object.keys(models).find((candidate) => {
|
|
5308
|
+
const ref = refOf(candidate);
|
|
5309
|
+
return ref !== null && ref.provider === shipped.provider && ref.model === shipped.model;
|
|
5310
|
+
});
|
|
5311
|
+
if (!shippedKey) models[DEFAULT_MODEL_ALIASES[alias]] = { alias };
|
|
5312
|
+
else if (!String(models[shippedKey]?.alias ?? "").trim()) models[shippedKey] = {
|
|
5313
|
+
...models[shippedKey],
|
|
5314
|
+
alias
|
|
5315
|
+
};
|
|
5316
|
+
moves.push({
|
|
5317
|
+
alias,
|
|
5318
|
+
from: key,
|
|
5319
|
+
to: shippedKey ?? DEFAULT_MODEL_ALIASES[alias],
|
|
5320
|
+
versionedAlias: versioned
|
|
5321
|
+
});
|
|
5322
|
+
}
|
|
5323
|
+
return moves;
|
|
5324
|
+
}
|
|
5248
5325
|
function applyModelDefaults(cfg) {
|
|
5249
5326
|
let mutated = false;
|
|
5250
5327
|
let nextCfg = cfg;
|
|
@@ -5316,6 +5393,7 @@ function applyModelDefaults(cfg) {
|
|
|
5316
5393
|
const existingModels = existingAgent.models ?? {};
|
|
5317
5394
|
if (Object.keys(existingModels).length === 0) return mutated ? nextCfg : cfg;
|
|
5318
5395
|
const nextModels = { ...existingModels };
|
|
5396
|
+
if (johnnessMigrateStaleShorthands79(nextModels).length > 0) mutated = true; // JOHNNESS_PATCH_ALIAS_LATEST_79
|
|
5319
5397
|
for (const [alias, target] of Object.entries(DEFAULT_MODEL_ALIASES)) {
|
|
5320
5398
|
const entry = nextModels[target];
|
|
5321
5399
|
if (!entry) continue;
|
|
@@ -13289,6 +13367,7 @@ const JohnnessSchema = z.object({
|
|
|
13289
13367
|
cron: z.object({
|
|
13290
13368
|
enabled: z.boolean().optional(),
|
|
13291
13369
|
store: z.string().optional(),
|
|
13370
|
+
failureLog: z.string().optional(), // JOHNNESS_PATCH_CRON_RELIABILITY_79: file sink, one line per cron failure
|
|
13292
13371
|
maxConcurrentRuns: z.number().int().positive().optional(),
|
|
13293
13372
|
retry: z.object({
|
|
13294
13373
|
maxAttempts: z.number().int().min(0).max(10).optional(),
|
|
@@ -16975,3 +17054,4 @@ var auth_profiles_exports = /* @__PURE__ */ __exportAll({
|
|
|
16975
17054
|
});
|
|
16976
17055
|
//#endregion
|
|
16977
17056
|
export { normalizeTelegramCommandName as $, upsertAuthProfile as $n, retryAsync as $r, analyzeArgvCommand as $t, formatPermissionDetail as A, CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF as Ai, isSafeExecutableValue as An, resolveHooksGmailModel as Ar, AVATAR_MAX_BYTES as At, getRuntimeConfigSnapshot as B, DEFAULT_PROVIDER as Bi, collectConfigServiceEnvVars as Bn, buildKimiCodingProvider as Br, getConfigOverrides as Bt, isNonEmptyString as C, isNonSecretApiKeyMarker as Ci, splitShellArgs as Cn, normalizeModelSelection as Cr, isLegacyIpv4Literal as Ct, toDotPath as D, buildHuggingfaceModelDefinition as Di, applyMergePatch as Dn, resolveAllowlistModelKey as Dr, normalizeIpAddress as Dt, parseEnvValue as E, HUGGINGFACE_MODEL_CATALOG as Ei, resolveSafeBinProfiles as En, resolveAllowedModelRef as Er, isRfc1918Ipv4Address as Et, formatIcaclsResetCommand as F, KILOCODE_BASE_URL as Fi, resolveSlackNativeStreaming as Fn, resolveThinkingDefault as Fr, isSupportedLocalAvatarExtension as Ft, readConfigFileSnapshot as G, sanitizeTerminalText as Gn, resolveImplicitBedrockProvider as Gr, parseConfigPath as Gt, loadConfig as H, CLAUDE_CLI_PROFILE_ID as Hi, buildTalkConfigResponse as Hn, buildXiaomiProvider as Hr, setConfigOverride as Ht, config_exports as I, KILOCODE_DEFAULT_MODEL_REF as Ii, resolveSlackStreamingMode as In, QIANFAN_BASE_URL as Ir, isWorkspaceRelativeAvatarPath as It, setRuntimeConfigSnapshot as J, repairOAuthProfileIdMismatch as Jn, VENICE_BASE_URL as Jr, getTrustedSafeBinDirs as Jt, readConfigFileSnapshotForWrite as K, loadDotEnv as Kn, resolveImplicitCopilotProvider as Kr, setConfigValueAtPath as Kt, migrateLegacyConfig as L, splitTrailingAuthProfile as Li, resolveTelegramPreviewStreamMode as Ln, QIANFAN_DEFAULT_MODEL_ID as Lr, looksLikeAvatarPath as Lt, inspectPathPermissions as M, resolveCloudflareAiGatewayBaseUrl as Mi, formatSlackStreamingBooleanMigrationMessage as Mn, resolveReasoningDefault as Mr, isAvatarHttpUrl as Mt, safeStat as N, normalizeOptionalSecretInput as Ni, mapStreamingModeToSlackLegacyDraftStreamMode as Nn, resolveSubagentConfiguredModelSelection as Nr, isAvatarImageDataUrl as Nt, writeTextFileAtomic as O, discoverHuggingfaceModels as Oi, applyLegacyMigrations as On, resolveConfiguredModelRef as Or, parseCanonicalIpAddress as Ot, createIcaclsResetCommand as P, normalizeSecretInput as Pi, resolveDiscordPreviewStreamMode as Pn, resolveSubagentSpawnModelSelection as Pr, isPathWithinRoot as Pt, TELEGRAM_COMMAND_NAME_PATTERN as Q, setAuthProfileOrder as Qn, resolveRetryConfig as Qr, validateSafeBinArgv as Qt, clearRuntimeConfigSnapshot as R, DEFAULT_CONTEXT_TOKENS as Ri, INCLUDE_KEY as Rn, XIAOMI_DEFAULT_MODEL_ID as Rr, resolveAvatarMime as Rt, describeUnknownError as S, shouldEnableShellEnvFallback as Si, resolveInlineCommandMatch as Sn, normalizeModelRef as Sr, isIpv6Address as St, parseDotPath as T, HUGGINGFACE_BASE_URL as Ti, normalizeSafeBinProfileFixtures as Tn, parseModelRef as Tr, isPrivateOrLoopbackIpAddress as Tt, parseConfigJson5 as U, CODEX_CLI_PROFILE_ID as Ui, resolveAgentMaxConcurrent as Un, normalizeGoogleModelId as Ur, unsetConfigOverride as Ut, getRuntimeConfigSourceSnapshot as V, resolveAuthProfileDisplayLabel as Vi, containsEnvVarReference as Vn, buildQianfanProvider as Vr, resetConfigOverrides as Vt, readBestEffortConfig as W, resolveSubagentMaxConcurrent as Wn, normalizeProviders as Wr, getConfigValueAtPath as Wt, validateConfigObjectWithPlugins as X, listProfilesForProvider as Xn, VENICE_MODEL_CATALOG as Xr, listWritableExplicitTrustedSafeBinDirs as Xt, writeConfigFile as Y, dedupeProfileIds as Yn, VENICE_DEFAULT_MODEL_REF as Yr, isTrustedSafeBinPath as Yt, JohnnessSchema as Z, markAuthProfileGood as Zn, buildVeniceModelDefinition as Zr, normalizeTrustedSafeBinDirs as Zt, parseOAuthCallbackInput as _, resolveEnvApiKey as _i, unwrapDispatchWrappersForResolution as _n, findNormalizedProviderValue as _r, isBlockedSpecialUseIpv6Address as _t, getSoonestCooldownExpiry as a, SYNTHETIC_MODEL_CATALOG as ai, DEFAULT_SAFE_BINS as an, replaceRuntimeAuthProfileStoreSnapshots as ar, resolveIMessageRemoteAttachmentRoots as at, resolveSecretRefValue as b, getShellPathFromLoginShell as bi, POSIX_INLINE_COMMAND_FLAGS as bn, isCliProvider as br, isIpInCidr as bt, markAuthProfileUsed as c, buildAssistantMessage$1 as ci, resolveCommandResolutionFromArgv as cn, resolveAuthStorePathForDisplay as cr, SecretProviderSchema as ct, resolveApiKeyForProfile as d, buildUsageWithNoCost as di, extractShellWrapperCommand as dn, resolveProcessScopedMap as dr, isDangerousNetworkMode as dt, TOGETHER_BASE_URL as ei, analyzeShellCommand as en, upsertAuthProfileWithLock as er, resolveTelegramCustomCommands as et, evaluateStoredCredentialEligibility as f, getApiKeyForModel as fi, extractShellWrapperInlineCommand as fn, getProcessStartTime as fr, normalizeNetworkMode as ft, generateChutesPkce as g, resolveAwsSdkEnvVarName as gi, normalizeExecutableToken as gn, buildModelAliasIndex as gr, isBlockedSpecialUseIpv4Address as gt, exchangeChutesCodeForTokens as h, resolveApiKeyForProvider as hi, isShellWrapperExecutable as hn, buildConfiguredAllowlistKeys as hr, extractEmbeddedIpv4FromIpv6 as ht, clearExpiredCooldowns as i, SYNTHETIC_DEFAULT_MODEL_REF as ii, splitCommandChain as in, loadAuthProfileStoreForSecretsRuntime as ir, resolveIMessageAttachmentRoots as it, formatPermissionRemediation as j, buildCloudflareAiGatewayModelDefinition as ji, formatSlackStreamModeMigrationMessage as jn, resolveModelRefFromString as jr, isAvatarDataUrl as jt, encodeJsonPointerToken as k, isHuggingfacePolicyLocked as ki, ensureControlUiAllowedOriginsForNonLoopbackBind as kn, resolveDefaultModelForAgent as kr, parseLooseIpAddress as kt, resolveProfileUnusableUntilForDisplay as l, buildAssistantMessageWithZeroUsage as li, resolveExecutableFromPathEnv as ln, resolveJohnnessAgentDir as lr, sensitive as lt, CHUTES_AUTHORIZE_ENDPOINT as m, requireApiKey as mi, isDispatchWrapperExecutable as mn, buildAllowedModelSet as mr, parseByteSize as mt, resolveAuthProfileEligibility as n, buildTogetherModelDefinition as ni, isWindowsPlatform as nn, ensureAuthProfileStore as nr, isInboundPathAllowed as nt, isProfileInCooldown as o, buildSyntheticModelDefinition as oi, matchAllowlist as on, updateAuthProfileStoreWithLock as or, normalizeScpRemoteHost as ot, resolveTokenExpiryState as p, getCustomProviderApiKey as pi, hasEnvManipulationBeforeShellWrapper as pn, isPidAlive as pr, parseDurationMs as pt, resolveConfigSnapshotHash as q, resolveOwnerDisplaySetting as qn, resolveImplicitProviders as qr, unsetConfigValueAtPath as qt, resolveAuthProfileOrder as r, SYNTHETIC_BASE_URL as ri, resolvePlannedSegmentArgv as rn, loadAuthProfileStore as rr, mergeInboundPathRoots as rt, markAuthProfileFailure as s, createConfiguredOllamaStreamFn as si, resolveAllowlistCandidatePath as sn, resolveAuthStorePath as sr, parseNonNegativeByteSize as st, auth_profiles_exports as t, TOGETHER_MODEL_CATALOG as ti, buildEnforcedShellCommand as tn, clearRuntimeAuthProfileStoreSnapshots as tr, DEFAULT_IMESSAGE_ATTACHMENT_ROOTS as tt, resolveProfilesUnavailableReason as u, buildStreamErrorAssistantMessage as ui, POSIX_SHELL_WRAPPERS as un, withFileLock as ur, getBlockedNetworkModeReason as ut, isProviderScopedSecretResolutionError as v, resolveModelAuthMode as vi, unwrapKnownDispatchWrapperInvocation as vn, getModelRefStatus as vr, isCanonicalDottedDecimalIPv4 as vt, isRecord as w, isSecretRefHeaderValueMarker as wi, SAFE_BIN_PROFILES as wn, normalizeProviderId as wr, isLoopbackIpAddress as wt, resolveSecretRefValues as x, resolveShellEnvFallbackTimeoutMs as xi, POWERSHELL_INLINE_COMMAND_FLAGS as xn, modelKey as xr, isIpv4Address as xt, resolveSecretRefString as y, getShellEnvAppliedKeys as yi, unwrapKnownShellMultiplexerInvocation as yn, inferUniqueProviderFromConfiguredModels as yr, isCarrierGradeNatIpv4Address as yt, createConfigIO as z, DEFAULT_MODEL as zi, applyConfigEnvVars as zn, buildKilocodeProvider as zr, validateJsonSchemaValue as zt };
|
|
17057
|
+
export { johnnessMigrateStaleShorthands79 }; // JOHNNESS_PATCH_ALIAS_LATEST_79
|
package/dist/compact-B247y5Qt.js
CHANGED
|
@@ -18084,6 +18084,12 @@ function resolveToolErrorWarningPolicy(params) {
|
|
|
18084
18084
|
showWarning: false,
|
|
18085
18085
|
includeDetails
|
|
18086
18086
|
};
|
|
18087
|
+
// JOHNNESS_PATCH_CRON_RELIABILITY_79: a cron.* gateway timeout usually completed server-side; the model still
|
|
18088
|
+
// sees the tool error, the chat reply gets no automatic warning.
|
|
18089
|
+
if (normalizedToolName === "cron" && /gateway (?:request )?timeout after \d+\s*ms/i.test(String(params.lastToolError.error ?? ""))) return {
|
|
18090
|
+
showWarning: false,
|
|
18091
|
+
includeDetails
|
|
18092
|
+
};
|
|
18087
18093
|
if (params.lastToolError.mutatingAction ?? isLikelyMutatingToolName(params.lastToolError.toolName)) return {
|
|
18088
18094
|
showWarning: true,
|
|
18089
18095
|
includeDetails
|
|
@@ -46356,7 +46362,7 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Large l
|
|
|
46356
46362
|
const action = readStringParam(params, "action", { required: true });
|
|
46357
46363
|
const gatewayOpts = {
|
|
46358
46364
|
...readGatewayCallOptions(params),
|
|
46359
|
-
timeoutMs: typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? params.timeoutMs :
|
|
46365
|
+
timeoutMs: typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? params.timeoutMs : 18e4 /* JOHNNESS_PATCH_CRON_RELIABILITY_79: cron.* calls queue behind busy lanes; 180s */
|
|
46360
46366
|
};
|
|
46361
46367
|
switch (action) {
|
|
46362
46368
|
case "status": return jsonResult(await callGateway("cron.status", gatewayOpts, {}));
|
package/dist/config-koj5M_EO.js
CHANGED
|
@@ -1940,6 +1940,83 @@ function applyTalkApiKey(config) {
|
|
|
1940
1940
|
function applyTalkConfigNormalization(config) {
|
|
1941
1941
|
return normalizeTalkConfig(config);
|
|
1942
1942
|
}
|
|
1943
|
+
/* JOHNNESS_PATCH_ALIAS_LATEST_79: bare shorthands (DEFAULT_MODEL_ALIASES keys without digits) always resolve
|
|
1944
|
+
* to the newest shipped model in their line. A user alias that claims one but pins an older model of the same
|
|
1945
|
+
* provider and line is moved: the shipped model takes the shorthand and the old model keeps a versioned alias
|
|
1946
|
+
* (opus5, sonnet46, gpt55). Custom aliases, newer pins and cross-provider or cross-line choices are untouched.
|
|
1947
|
+
* Replaces entries of the given models map in place (never mutates an entry); returns the moves. */
|
|
1948
|
+
function johnnessMigrateStaleShorthands79(models) {
|
|
1949
|
+
const moves = [];
|
|
1950
|
+
if (!models || typeof models !== "object" || Array.isArray(models)) return moves;
|
|
1951
|
+
const refOf = (raw) => {
|
|
1952
|
+
const text = String(raw ?? "").trim().toLowerCase();
|
|
1953
|
+
const slash = text.indexOf("/");
|
|
1954
|
+
return slash > 0 ? {
|
|
1955
|
+
provider: text.slice(0, slash),
|
|
1956
|
+
model: text.slice(slash + 1)
|
|
1957
|
+
} : null;
|
|
1958
|
+
};
|
|
1959
|
+
const versionOf = (model) => {
|
|
1960
|
+
const parts = [];
|
|
1961
|
+
for (const raw of model.match(/\d+/g) ?? []) {
|
|
1962
|
+
if (raw.length >= 4) break;
|
|
1963
|
+
parts.push(Number(raw));
|
|
1964
|
+
}
|
|
1965
|
+
return parts;
|
|
1966
|
+
};
|
|
1967
|
+
const isOlder = (alias, user, shipped) => {
|
|
1968
|
+
if (user.provider !== shipped.provider || user.model === shipped.model) return false;
|
|
1969
|
+
const tokens = new Set(user.model.split(/[^a-z0-9]+/).filter(Boolean));
|
|
1970
|
+
if (!alias.split("-").every((token) => tokens.has(token))) return false;
|
|
1971
|
+
const a = versionOf(user.model);
|
|
1972
|
+
const b = versionOf(shipped.model);
|
|
1973
|
+
if (a.length === 0 || b.length === 0) return false;
|
|
1974
|
+
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
1975
|
+
const x = a[i] ?? 0;
|
|
1976
|
+
const y = b[i] ?? 0;
|
|
1977
|
+
if (x !== y) return x < y;
|
|
1978
|
+
}
|
|
1979
|
+
return false;
|
|
1980
|
+
};
|
|
1981
|
+
const versionedAliasFor = (alias, userKey, user) => {
|
|
1982
|
+
for (const [name, target] of Object.entries(DEFAULT_MODEL_ALIASES)) if (name !== alias && /\d/.test(name) && name.startsWith(alias) && String(target).toLowerCase() === userKey) return name;
|
|
1983
|
+
const digits = versionOf(user.model).join("");
|
|
1984
|
+
return digits ? `${alias}${digits}` : void 0;
|
|
1985
|
+
};
|
|
1986
|
+
for (const key of Object.keys(models)) {
|
|
1987
|
+
const entry = models[key];
|
|
1988
|
+
const alias = String(entry?.alias ?? "").trim().toLowerCase();
|
|
1989
|
+
if (!alias || /\d/.test(alias) || !Object.hasOwn(DEFAULT_MODEL_ALIASES, alias)) continue;
|
|
1990
|
+
const user = refOf(key);
|
|
1991
|
+
const shipped = refOf(DEFAULT_MODEL_ALIASES[alias]);
|
|
1992
|
+
if (!user || !shipped || !isOlder(alias, user, shipped)) continue;
|
|
1993
|
+
const userKey = `${user.provider}/${user.model}`;
|
|
1994
|
+
const taken = new Set(Object.entries(models).filter(([other]) => other !== key).map(([, value]) => String(value?.alias ?? "").trim().toLowerCase()).filter(Boolean));
|
|
1995
|
+
let versioned = versionedAliasFor(alias, userKey, user);
|
|
1996
|
+
if (versioned && (taken.has(versioned) || Object.hasOwn(DEFAULT_MODEL_ALIASES, versioned) && String(DEFAULT_MODEL_ALIASES[versioned]).toLowerCase() !== userKey)) versioned = void 0;
|
|
1997
|
+
const { alias: _previousAlias, ...rest } = entry;
|
|
1998
|
+
models[key] = versioned ? {
|
|
1999
|
+
...rest,
|
|
2000
|
+
alias: versioned
|
|
2001
|
+
} : rest;
|
|
2002
|
+
const shippedKey = Object.keys(models).find((candidate) => {
|
|
2003
|
+
const ref = refOf(candidate);
|
|
2004
|
+
return ref !== null && ref.provider === shipped.provider && ref.model === shipped.model;
|
|
2005
|
+
});
|
|
2006
|
+
if (!shippedKey) models[DEFAULT_MODEL_ALIASES[alias]] = { alias };
|
|
2007
|
+
else if (!String(models[shippedKey]?.alias ?? "").trim()) models[shippedKey] = {
|
|
2008
|
+
...models[shippedKey],
|
|
2009
|
+
alias
|
|
2010
|
+
};
|
|
2011
|
+
moves.push({
|
|
2012
|
+
alias,
|
|
2013
|
+
from: key,
|
|
2014
|
+
to: shippedKey ?? DEFAULT_MODEL_ALIASES[alias],
|
|
2015
|
+
versionedAlias: versioned
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
return moves;
|
|
2019
|
+
}
|
|
1943
2020
|
function applyModelDefaults(cfg) {
|
|
1944
2021
|
let mutated = false;
|
|
1945
2022
|
let nextCfg = cfg;
|
|
@@ -2011,6 +2088,7 @@ function applyModelDefaults(cfg) {
|
|
|
2011
2088
|
const existingModels = existingAgent.models ?? {};
|
|
2012
2089
|
if (Object.keys(existingModels).length === 0) return mutated ? nextCfg : cfg;
|
|
2013
2090
|
const nextModels = { ...existingModels };
|
|
2091
|
+
if (johnnessMigrateStaleShorthands79(nextModels).length > 0) mutated = true; // JOHNNESS_PATCH_ALIAS_LATEST_79
|
|
2014
2092
|
for (const [alias, target] of Object.entries(DEFAULT_MODEL_ALIASES)) {
|
|
2015
2093
|
const entry = nextModels[target];
|
|
2016
2094
|
if (!entry) continue;
|
|
@@ -9138,6 +9216,7 @@ const JohnnessSchema = z.object({
|
|
|
9138
9216
|
cron: z.object({
|
|
9139
9217
|
enabled: z.boolean().optional(),
|
|
9140
9218
|
store: z.string().optional(),
|
|
9219
|
+
failureLog: z.string().optional(), // JOHNNESS_PATCH_CRON_RELIABILITY_79: file sink, one line per cron failure
|
|
9141
9220
|
maxConcurrentRuns: z.number().int().positive().optional(),
|
|
9142
9221
|
retry: z.object({
|
|
9143
9222
|
maxAttempts: z.number().int().min(0).max(10).optional(),
|
package/dist/daemon-cli.js
CHANGED
|
@@ -5092,6 +5092,83 @@ function applyTalkApiKey(config) {
|
|
|
5092
5092
|
function applyTalkConfigNormalization(config) {
|
|
5093
5093
|
return normalizeTalkConfig(config);
|
|
5094
5094
|
}
|
|
5095
|
+
/* JOHNNESS_PATCH_ALIAS_LATEST_79: bare shorthands (DEFAULT_MODEL_ALIASES keys without digits) always resolve
|
|
5096
|
+
* to the newest shipped model in their line. A user alias that claims one but pins an older model of the same
|
|
5097
|
+
* provider and line is moved: the shipped model takes the shorthand and the old model keeps a versioned alias
|
|
5098
|
+
* (opus5, sonnet46, gpt55). Custom aliases, newer pins and cross-provider or cross-line choices are untouched.
|
|
5099
|
+
* Replaces entries of the given models map in place (never mutates an entry); returns the moves. */
|
|
5100
|
+
function johnnessMigrateStaleShorthands79(models) {
|
|
5101
|
+
const moves = [];
|
|
5102
|
+
if (!models || typeof models !== "object" || Array.isArray(models)) return moves;
|
|
5103
|
+
const refOf = (raw) => {
|
|
5104
|
+
const text = String(raw ?? "").trim().toLowerCase();
|
|
5105
|
+
const slash = text.indexOf("/");
|
|
5106
|
+
return slash > 0 ? {
|
|
5107
|
+
provider: text.slice(0, slash),
|
|
5108
|
+
model: text.slice(slash + 1)
|
|
5109
|
+
} : null;
|
|
5110
|
+
};
|
|
5111
|
+
const versionOf = (model) => {
|
|
5112
|
+
const parts = [];
|
|
5113
|
+
for (const raw of model.match(/\d+/g) ?? []) {
|
|
5114
|
+
if (raw.length >= 4) break;
|
|
5115
|
+
parts.push(Number(raw));
|
|
5116
|
+
}
|
|
5117
|
+
return parts;
|
|
5118
|
+
};
|
|
5119
|
+
const isOlder = (alias, user, shipped) => {
|
|
5120
|
+
if (user.provider !== shipped.provider || user.model === shipped.model) return false;
|
|
5121
|
+
const tokens = new Set(user.model.split(/[^a-z0-9]+/).filter(Boolean));
|
|
5122
|
+
if (!alias.split("-").every((token) => tokens.has(token))) return false;
|
|
5123
|
+
const a = versionOf(user.model);
|
|
5124
|
+
const b = versionOf(shipped.model);
|
|
5125
|
+
if (a.length === 0 || b.length === 0) return false;
|
|
5126
|
+
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
5127
|
+
const x = a[i] ?? 0;
|
|
5128
|
+
const y = b[i] ?? 0;
|
|
5129
|
+
if (x !== y) return x < y;
|
|
5130
|
+
}
|
|
5131
|
+
return false;
|
|
5132
|
+
};
|
|
5133
|
+
const versionedAliasFor = (alias, userKey, user) => {
|
|
5134
|
+
for (const [name, target] of Object.entries(DEFAULT_MODEL_ALIASES)) if (name !== alias && /\d/.test(name) && name.startsWith(alias) && String(target).toLowerCase() === userKey) return name;
|
|
5135
|
+
const digits = versionOf(user.model).join("");
|
|
5136
|
+
return digits ? `${alias}${digits}` : void 0;
|
|
5137
|
+
};
|
|
5138
|
+
for (const key of Object.keys(models)) {
|
|
5139
|
+
const entry = models[key];
|
|
5140
|
+
const alias = String(entry?.alias ?? "").trim().toLowerCase();
|
|
5141
|
+
if (!alias || /\d/.test(alias) || !Object.hasOwn(DEFAULT_MODEL_ALIASES, alias)) continue;
|
|
5142
|
+
const user = refOf(key);
|
|
5143
|
+
const shipped = refOf(DEFAULT_MODEL_ALIASES[alias]);
|
|
5144
|
+
if (!user || !shipped || !isOlder(alias, user, shipped)) continue;
|
|
5145
|
+
const userKey = `${user.provider}/${user.model}`;
|
|
5146
|
+
const taken = new Set(Object.entries(models).filter(([other]) => other !== key).map(([, value]) => String(value?.alias ?? "").trim().toLowerCase()).filter(Boolean));
|
|
5147
|
+
let versioned = versionedAliasFor(alias, userKey, user);
|
|
5148
|
+
if (versioned && (taken.has(versioned) || Object.hasOwn(DEFAULT_MODEL_ALIASES, versioned) && String(DEFAULT_MODEL_ALIASES[versioned]).toLowerCase() !== userKey)) versioned = void 0;
|
|
5149
|
+
const { alias: _previousAlias, ...rest } = entry;
|
|
5150
|
+
models[key] = versioned ? {
|
|
5151
|
+
...rest,
|
|
5152
|
+
alias: versioned
|
|
5153
|
+
} : rest;
|
|
5154
|
+
const shippedKey = Object.keys(models).find((candidate) => {
|
|
5155
|
+
const ref = refOf(candidate);
|
|
5156
|
+
return ref !== null && ref.provider === shipped.provider && ref.model === shipped.model;
|
|
5157
|
+
});
|
|
5158
|
+
if (!shippedKey) models[DEFAULT_MODEL_ALIASES[alias]] = { alias };
|
|
5159
|
+
else if (!String(models[shippedKey]?.alias ?? "").trim()) models[shippedKey] = {
|
|
5160
|
+
...models[shippedKey],
|
|
5161
|
+
alias
|
|
5162
|
+
};
|
|
5163
|
+
moves.push({
|
|
5164
|
+
alias,
|
|
5165
|
+
from: key,
|
|
5166
|
+
to: shippedKey ?? DEFAULT_MODEL_ALIASES[alias],
|
|
5167
|
+
versionedAlias: versioned
|
|
5168
|
+
});
|
|
5169
|
+
}
|
|
5170
|
+
return moves;
|
|
5171
|
+
}
|
|
5095
5172
|
function applyModelDefaults(cfg) {
|
|
5096
5173
|
let mutated = false;
|
|
5097
5174
|
let nextCfg = cfg;
|
|
@@ -5163,6 +5240,7 @@ function applyModelDefaults(cfg) {
|
|
|
5163
5240
|
const existingModels = existingAgent.models ?? {};
|
|
5164
5241
|
if (Object.keys(existingModels).length === 0) return mutated ? nextCfg : cfg;
|
|
5165
5242
|
const nextModels = { ...existingModels };
|
|
5243
|
+
if (johnnessMigrateStaleShorthands79(nextModels).length > 0) mutated = true; // JOHNNESS_PATCH_ALIAS_LATEST_79
|
|
5166
5244
|
for (const [alias, target] of Object.entries(DEFAULT_MODEL_ALIASES)) {
|
|
5167
5245
|
const entry = nextModels[target];
|
|
5168
5246
|
if (!entry) continue;
|
|
@@ -12044,6 +12122,7 @@ const JohnnessSchema = z.object({
|
|
|
12044
12122
|
cron: z.object({
|
|
12045
12123
|
enabled: z.boolean().optional(),
|
|
12046
12124
|
store: z.string().optional(),
|
|
12125
|
+
failureLog: z.string().optional(), // JOHNNESS_PATCH_CRON_RELIABILITY_79: file sink, one line per cron failure
|
|
12047
12126
|
maxConcurrentRuns: z.number().int().positive().optional(),
|
|
12048
12127
|
retry: z.object({
|
|
12049
12128
|
maxAttempts: z.number().int().min(0).max(10).optional(),
|
|
@@ -4417,7 +4417,7 @@ async function runCronIsolatedAgentTurn(params) {
|
|
|
4417
4417
|
provider,
|
|
4418
4418
|
model,
|
|
4419
4419
|
agentDir,
|
|
4420
|
-
fallbacksOverride: payloadFallbacks ?? resolveAgentModelFallbacksOverride(params.cfg, agentId),
|
|
4420
|
+
fallbacksOverride: payloadFallbacks ?? resolveAgentModelFallbacksOverride(params.cfg, agentId) ?? resolveCronRunFallbacks79(cfgWithAgentDefaults, resolvedDefault), // JOHNNESS_PATCH_CRON_RELIABILITY_79
|
|
4421
4421
|
run: async (providerOverride, modelOverride, runOptions) => {
|
|
4422
4422
|
if (abortSignal?.aborted) throw new Error(abortReason());
|
|
4423
4423
|
const bootstrapPromptWarningSignature = bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1];
|
|
@@ -6110,6 +6110,49 @@ const TRANSIENT_PATTERNS = {
|
|
|
6110
6110
|
timeout: /(timeout|etimedout)/i,
|
|
6111
6111
|
server_error: /\b5\d{2}\b/
|
|
6112
6112
|
};
|
|
6113
|
+
/* JOHNNESS_PATCH_CRON_RELIABILITY_79: recurring transient retry, run fallbacks and the cron.failureLog sink (family 79). */
|
|
6114
|
+
function classifyCronError79(error) {
|
|
6115
|
+
const text = typeof error === "string" ? error : "";
|
|
6116
|
+
if (!text) return "error";
|
|
6117
|
+
for (const [kind, pattern] of Object.entries(TRANSIENT_PATTERNS)) if (pattern.test(text)) return kind;
|
|
6118
|
+
return /\btimed out\b/i.test(text) ? "timeout" : "error";
|
|
6119
|
+
}
|
|
6120
|
+
function isRecurringCronRetryable79(error, retryOn) {
|
|
6121
|
+
if (typeof error !== "string" || error.trim() === timeoutErrorMessage()) return false;
|
|
6122
|
+
if (isTransientCronError(error, retryOn)) return true;
|
|
6123
|
+
return (!retryOn?.length || retryOn.includes("timeout")) && /\btimed out\b/i.test(error);
|
|
6124
|
+
}
|
|
6125
|
+
function resolveCronRunFallbacks79(cfg, resolvedDefault) {
|
|
6126
|
+
const model = cfg?.agents?.defaults?.model;
|
|
6127
|
+
const configured = model && typeof model === "object" && Array.isArray(model.fallbacks) ? model.fallbacks : [];
|
|
6128
|
+
const primary = resolvedDefault?.provider && resolvedDefault?.model ? [`${resolvedDefault.provider}/${resolvedDefault.model}`] : [];
|
|
6129
|
+
return [...configured, ...primary];
|
|
6130
|
+
}
|
|
6131
|
+
function cronLogField79(value) {
|
|
6132
|
+
return String(value ?? "").replace(/[\r\n|]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 160) || "-";
|
|
6133
|
+
}
|
|
6134
|
+
function cronLogTime79(ms) {
|
|
6135
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return "-";
|
|
6136
|
+
const d = new Date(ms);
|
|
6137
|
+
const offset = -d.getTimezoneOffset();
|
|
6138
|
+
const pad = (n) => String(Math.trunc(Math.abs(n))).padStart(2, "0");
|
|
6139
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${offset >= 0 ? "+" : "-"}${pad(offset / 60)}:${pad(offset % 60)}`;
|
|
6140
|
+
}
|
|
6141
|
+
function appendCronFailureLog79(state, job, result, retry) {
|
|
6142
|
+
const target = state.deps.cronConfig?.failureLog;
|
|
6143
|
+
if (typeof target !== "string" || !target.trim()) return;
|
|
6144
|
+
const file = resolveUserPath(target.trim());
|
|
6145
|
+
const next = retry?.retryAtMs !== void 0 ? cronLogTime79(retry.retryAtMs) : job.enabled && job.state.nextRunAtMs !== void 0 ? `none (next run ${cronLogTime79(job.state.nextRunAtMs)})` : job.enabled ? "none" : "none (disabled)";
|
|
6146
|
+
const line = `- ${cronLogTime79(result.endedAt)} | cron-fail | ${cronLogField79(job.name || job.id)} | ${classifyCronError79(result.error)} | attempt ${job.state.consecutiveErrors ?? 1} | next retry ${next}\n`;
|
|
6147
|
+
const write = fs$1.mkdir(path.dirname(file), { recursive: true }).then(() => fs$1.appendFile(file, line, "utf8")).catch((err) => {
|
|
6148
|
+
state.deps.log.warn({
|
|
6149
|
+
jobId: job.id,
|
|
6150
|
+
failureLog: file,
|
|
6151
|
+
err: String(err)
|
|
6152
|
+
}, "cron: failure log append failed");
|
|
6153
|
+
});
|
|
6154
|
+
state.failureLogWrites79 = write;
|
|
6155
|
+
}
|
|
6113
6156
|
function isTransientCronError(error, retryOn) {
|
|
6114
6157
|
if (!error || typeof error !== "string") return false;
|
|
6115
6158
|
return (retryOn?.length ? retryOn : Object.keys(TRANSIENT_PATTERNS)).some((k) => TRANSIENT_PATTERNS[k]?.test(error));
|
|
@@ -6241,6 +6284,7 @@ function applyJobResult(state, job, result, opts) {
|
|
|
6241
6284
|
job.state.consecutiveErrors = 0;
|
|
6242
6285
|
job.state.lastFailureAlertAtMs = void 0;
|
|
6243
6286
|
}
|
|
6287
|
+
let retry79; // JOHNNESS_PATCH_CRON_RELIABILITY_79
|
|
6244
6288
|
const shouldDelete = job.schedule.kind === "at" && job.deleteAfterRun === true && result.status === "ok";
|
|
6245
6289
|
if (!shouldDelete) if (job.schedule.kind === "at") {
|
|
6246
6290
|
if (result.status === "ok" || result.status === "skipped") {
|
|
@@ -6285,6 +6329,24 @@ function applyJobResult(state, job, result, opts) {
|
|
|
6285
6329
|
});
|
|
6286
6330
|
}
|
|
6287
6331
|
const backoffNext = result.endedAt + backoff;
|
|
6332
|
+
/* JOHNNESS_PATCH_CRON_RELIABILITY_79: a transient error on a recurring job retries at
|
|
6333
|
+
min(next natural slot, now + cron.retry backoff) up to cron.retry.maxAttempts; after that
|
|
6334
|
+
the normal schedule resumes with the error backoff below. */
|
|
6335
|
+
const retryConfig79 = resolveRetryConfig(state.deps.cronConfig);
|
|
6336
|
+
const consecutive79 = job.state.consecutiveErrors ?? 1;
|
|
6337
|
+
if (isRecurringCronRetryable79(result.error, retryConfig79.retryOn) && consecutive79 <= retryConfig79.maxAttempts) {
|
|
6338
|
+
const retryBackoff79 = Math.max(MIN_REFIRE_GAP_MS, errorBackoffMs(consecutive79, retryConfig79.backoffMs));
|
|
6339
|
+
const retryAt79 = result.endedAt + retryBackoff79;
|
|
6340
|
+
job.state.nextRunAtMs = normalNext !== void 0 ? Math.min(normalNext, retryAt79) : retryAt79;
|
|
6341
|
+
retry79 = { retryAtMs: job.state.nextRunAtMs };
|
|
6342
|
+
state.deps.log.info({
|
|
6343
|
+
jobId: job.id,
|
|
6344
|
+
jobName: job.name,
|
|
6345
|
+
consecutiveErrors: consecutive79,
|
|
6346
|
+
backoffMs: retryBackoff79,
|
|
6347
|
+
nextRunAtMs: job.state.nextRunAtMs
|
|
6348
|
+
}, "cron: scheduling recurring retry after transient error");
|
|
6349
|
+
} else {
|
|
6288
6350
|
job.state.nextRunAtMs = normalNext !== void 0 ? Math.max(normalNext, backoffNext) : backoffNext;
|
|
6289
6351
|
state.deps.log.info({
|
|
6290
6352
|
jobId: job.id,
|
|
@@ -6292,6 +6354,7 @@ function applyJobResult(state, job, result, opts) {
|
|
|
6292
6354
|
backoffMs: backoff,
|
|
6293
6355
|
nextRunAtMs: job.state.nextRunAtMs
|
|
6294
6356
|
}, "cron: applying error backoff");
|
|
6357
|
+
}
|
|
6295
6358
|
} else if (job.enabled) {
|
|
6296
6359
|
let naturalNext;
|
|
6297
6360
|
try {
|
|
@@ -6308,6 +6371,7 @@ function applyJobResult(state, job, result, opts) {
|
|
|
6308
6371
|
job.state.nextRunAtMs = naturalNext !== void 0 ? Math.max(naturalNext, minNext) : minNext;
|
|
6309
6372
|
} else job.state.nextRunAtMs = naturalNext;
|
|
6310
6373
|
} else job.state.nextRunAtMs = void 0;
|
|
6374
|
+
if (result.status === "error") appendCronFailureLog79(state, job, result, retry79 ?? (job.schedule.kind === "at" && job.enabled && job.state.nextRunAtMs !== void 0 ? { retryAtMs: job.state.nextRunAtMs } : void 0)); // JOHNNESS_PATCH_CRON_RELIABILITY_79
|
|
6311
6375
|
return shouldDelete;
|
|
6312
6376
|
}
|
|
6313
6377
|
function applyOutcomeToStoredJob(state, result) {
|
|
@@ -23471,6 +23535,21 @@ async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
23471
23535
|
const issues = configSnapshot.issues.length > 0 ? formatConfigIssueLines(configSnapshot.issues, "", { normalizeRoot: true }).join("\n") : "Unknown validation issue.";
|
|
23472
23536
|
throw new Error(`Invalid config at ${configSnapshot.path}.\n${issues}\nRun "${formatCliCommand("johnness doctor")}" to repair, then retry.`);
|
|
23473
23537
|
}
|
|
23538
|
+
/* JOHNNESS_PATCH_ALIAS_LATEST_79: bare shorthands always point at the newest shipped model. A user
|
|
23539
|
+
alias that pins an older model of the same line moves to the shipped default; the old model keeps a
|
|
23540
|
+
versioned alias. Written back to the config file once, logged, idempotent. */
|
|
23541
|
+
try {
|
|
23542
|
+
const parsed79 = configSnapshot.parsed && typeof configSnapshot.parsed === "object" ? structuredClone(configSnapshot.parsed) : null;
|
|
23543
|
+
const models79 = parsed79?.agents?.defaults?.models;
|
|
23544
|
+
const moved79 = !isNixMode && models79 && typeof models79 === "object" && !Array.isArray(models79) ? johnnessMigrateStaleShorthands79(models79) : [];
|
|
23545
|
+
if (moved79.length > 0) {
|
|
23546
|
+
await writeConfigFile(parsed79);
|
|
23547
|
+
log.info(`gateway: model shorthands moved to the newest shipped models:\n${moved79.map((move) => `- ${move.alias}: ${move.from} -> ${move.to}${move.versionedAlias ? ` (${move.from} is now "${move.versionedAlias}")` : ""}`).join("\n")}`);
|
|
23548
|
+
configSnapshot = await readConfigFileSnapshot();
|
|
23549
|
+
}
|
|
23550
|
+
} catch (err) {
|
|
23551
|
+
log.warn(`gateway: model shorthand migration skipped: ${String(err)}`);
|
|
23552
|
+
}
|
|
23474
23553
|
const autoEnable = applyPluginAutoEnable({
|
|
23475
23554
|
config: configSnapshot.config,
|
|
23476
23555
|
env: process.env
|
|
@@ -24774,3 +24853,4 @@ function registerGatewayCli(program) {
|
|
|
24774
24853
|
}
|
|
24775
24854
|
//#endregion
|
|
24776
24855
|
export { registerGatewayCli };
|
|
24856
|
+
import { johnnessMigrateStaleShorthands79 } from "./model-selection-L7RMwsG-.js"; // JOHNNESS_PATCH_ALIAS_LATEST_79
|
|
@@ -4419,7 +4419,7 @@ async function runCronIsolatedAgentTurn(params) {
|
|
|
4419
4419
|
provider,
|
|
4420
4420
|
model,
|
|
4421
4421
|
agentDir,
|
|
4422
|
-
fallbacksOverride: payloadFallbacks ?? resolveAgentModelFallbacksOverride(params.cfg, agentId),
|
|
4422
|
+
fallbacksOverride: payloadFallbacks ?? resolveAgentModelFallbacksOverride(params.cfg, agentId) ?? resolveCronRunFallbacks79(cfgWithAgentDefaults, resolvedDefault), // JOHNNESS_PATCH_CRON_RELIABILITY_79
|
|
4423
4423
|
run: async (providerOverride, modelOverride, runOptions) => {
|
|
4424
4424
|
if (abortSignal?.aborted) throw new Error(abortReason());
|
|
4425
4425
|
const bootstrapPromptWarningSignature = bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1];
|
|
@@ -6112,6 +6112,49 @@ const TRANSIENT_PATTERNS = {
|
|
|
6112
6112
|
timeout: /(timeout|etimedout)/i,
|
|
6113
6113
|
server_error: /\b5\d{2}\b/
|
|
6114
6114
|
};
|
|
6115
|
+
/* JOHNNESS_PATCH_CRON_RELIABILITY_79: recurring transient retry, run fallbacks and the cron.failureLog sink (family 79). */
|
|
6116
|
+
function classifyCronError79(error) {
|
|
6117
|
+
const text = typeof error === "string" ? error : "";
|
|
6118
|
+
if (!text) return "error";
|
|
6119
|
+
for (const [kind, pattern] of Object.entries(TRANSIENT_PATTERNS)) if (pattern.test(text)) return kind;
|
|
6120
|
+
return /\btimed out\b/i.test(text) ? "timeout" : "error";
|
|
6121
|
+
}
|
|
6122
|
+
function isRecurringCronRetryable79(error, retryOn) {
|
|
6123
|
+
if (typeof error !== "string" || error.trim() === timeoutErrorMessage()) return false;
|
|
6124
|
+
if (isTransientCronError(error, retryOn)) return true;
|
|
6125
|
+
return (!retryOn?.length || retryOn.includes("timeout")) && /\btimed out\b/i.test(error);
|
|
6126
|
+
}
|
|
6127
|
+
function resolveCronRunFallbacks79(cfg, resolvedDefault) {
|
|
6128
|
+
const model = cfg?.agents?.defaults?.model;
|
|
6129
|
+
const configured = model && typeof model === "object" && Array.isArray(model.fallbacks) ? model.fallbacks : [];
|
|
6130
|
+
const primary = resolvedDefault?.provider && resolvedDefault?.model ? [`${resolvedDefault.provider}/${resolvedDefault.model}`] : [];
|
|
6131
|
+
return [...configured, ...primary];
|
|
6132
|
+
}
|
|
6133
|
+
function cronLogField79(value) {
|
|
6134
|
+
return String(value ?? "").replace(/[\r\n|]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 160) || "-";
|
|
6135
|
+
}
|
|
6136
|
+
function cronLogTime79(ms) {
|
|
6137
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return "-";
|
|
6138
|
+
const d = new Date(ms);
|
|
6139
|
+
const offset = -d.getTimezoneOffset();
|
|
6140
|
+
const pad = (n) => String(Math.trunc(Math.abs(n))).padStart(2, "0");
|
|
6141
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${offset >= 0 ? "+" : "-"}${pad(offset / 60)}:${pad(offset % 60)}`;
|
|
6142
|
+
}
|
|
6143
|
+
function appendCronFailureLog79(state, job, result, retry) {
|
|
6144
|
+
const target = state.deps.cronConfig?.failureLog;
|
|
6145
|
+
if (typeof target !== "string" || !target.trim()) return;
|
|
6146
|
+
const file = resolveUserPath(target.trim());
|
|
6147
|
+
const next = retry?.retryAtMs !== void 0 ? cronLogTime79(retry.retryAtMs) : job.enabled && job.state.nextRunAtMs !== void 0 ? `none (next run ${cronLogTime79(job.state.nextRunAtMs)})` : job.enabled ? "none" : "none (disabled)";
|
|
6148
|
+
const line = `- ${cronLogTime79(result.endedAt)} | cron-fail | ${cronLogField79(job.name || job.id)} | ${classifyCronError79(result.error)} | attempt ${job.state.consecutiveErrors ?? 1} | next retry ${next}\n`;
|
|
6149
|
+
const write = fs$1.mkdir(path.dirname(file), { recursive: true }).then(() => fs$1.appendFile(file, line, "utf8")).catch((err) => {
|
|
6150
|
+
state.deps.log.warn({
|
|
6151
|
+
jobId: job.id,
|
|
6152
|
+
failureLog: file,
|
|
6153
|
+
err: String(err)
|
|
6154
|
+
}, "cron: failure log append failed");
|
|
6155
|
+
});
|
|
6156
|
+
state.failureLogWrites79 = write;
|
|
6157
|
+
}
|
|
6115
6158
|
function isTransientCronError(error, retryOn) {
|
|
6116
6159
|
if (!error || typeof error !== "string") return false;
|
|
6117
6160
|
return (retryOn?.length ? retryOn : Object.keys(TRANSIENT_PATTERNS)).some((k) => TRANSIENT_PATTERNS[k]?.test(error));
|
|
@@ -6243,6 +6286,7 @@ function applyJobResult(state, job, result, opts) {
|
|
|
6243
6286
|
job.state.consecutiveErrors = 0;
|
|
6244
6287
|
job.state.lastFailureAlertAtMs = void 0;
|
|
6245
6288
|
}
|
|
6289
|
+
let retry79; // JOHNNESS_PATCH_CRON_RELIABILITY_79
|
|
6246
6290
|
const shouldDelete = job.schedule.kind === "at" && job.deleteAfterRun === true && result.status === "ok";
|
|
6247
6291
|
if (!shouldDelete) if (job.schedule.kind === "at") {
|
|
6248
6292
|
if (result.status === "ok" || result.status === "skipped") {
|
|
@@ -6287,6 +6331,24 @@ function applyJobResult(state, job, result, opts) {
|
|
|
6287
6331
|
});
|
|
6288
6332
|
}
|
|
6289
6333
|
const backoffNext = result.endedAt + backoff;
|
|
6334
|
+
/* JOHNNESS_PATCH_CRON_RELIABILITY_79: a transient error on a recurring job retries at
|
|
6335
|
+
min(next natural slot, now + cron.retry backoff) up to cron.retry.maxAttempts; after that
|
|
6336
|
+
the normal schedule resumes with the error backoff below. */
|
|
6337
|
+
const retryConfig79 = resolveRetryConfig(state.deps.cronConfig);
|
|
6338
|
+
const consecutive79 = job.state.consecutiveErrors ?? 1;
|
|
6339
|
+
if (isRecurringCronRetryable79(result.error, retryConfig79.retryOn) && consecutive79 <= retryConfig79.maxAttempts) {
|
|
6340
|
+
const retryBackoff79 = Math.max(MIN_REFIRE_GAP_MS, errorBackoffMs(consecutive79, retryConfig79.backoffMs));
|
|
6341
|
+
const retryAt79 = result.endedAt + retryBackoff79;
|
|
6342
|
+
job.state.nextRunAtMs = normalNext !== void 0 ? Math.min(normalNext, retryAt79) : retryAt79;
|
|
6343
|
+
retry79 = { retryAtMs: job.state.nextRunAtMs };
|
|
6344
|
+
state.deps.log.info({
|
|
6345
|
+
jobId: job.id,
|
|
6346
|
+
jobName: job.name,
|
|
6347
|
+
consecutiveErrors: consecutive79,
|
|
6348
|
+
backoffMs: retryBackoff79,
|
|
6349
|
+
nextRunAtMs: job.state.nextRunAtMs
|
|
6350
|
+
}, "cron: scheduling recurring retry after transient error");
|
|
6351
|
+
} else {
|
|
6290
6352
|
job.state.nextRunAtMs = normalNext !== void 0 ? Math.max(normalNext, backoffNext) : backoffNext;
|
|
6291
6353
|
state.deps.log.info({
|
|
6292
6354
|
jobId: job.id,
|
|
@@ -6294,6 +6356,7 @@ function applyJobResult(state, job, result, opts) {
|
|
|
6294
6356
|
backoffMs: backoff,
|
|
6295
6357
|
nextRunAtMs: job.state.nextRunAtMs
|
|
6296
6358
|
}, "cron: applying error backoff");
|
|
6359
|
+
}
|
|
6297
6360
|
} else if (job.enabled) {
|
|
6298
6361
|
let naturalNext;
|
|
6299
6362
|
try {
|
|
@@ -6310,6 +6373,7 @@ function applyJobResult(state, job, result, opts) {
|
|
|
6310
6373
|
job.state.nextRunAtMs = naturalNext !== void 0 ? Math.max(naturalNext, minNext) : minNext;
|
|
6311
6374
|
} else job.state.nextRunAtMs = naturalNext;
|
|
6312
6375
|
} else job.state.nextRunAtMs = void 0;
|
|
6376
|
+
if (result.status === "error") appendCronFailureLog79(state, job, result, retry79 ?? (job.schedule.kind === "at" && job.enabled && job.state.nextRunAtMs !== void 0 ? { retryAtMs: job.state.nextRunAtMs } : void 0)); // JOHNNESS_PATCH_CRON_RELIABILITY_79
|
|
6313
6377
|
return shouldDelete;
|
|
6314
6378
|
}
|
|
6315
6379
|
function applyOutcomeToStoredJob(state, result) {
|
|
@@ -23473,6 +23537,21 @@ async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
23473
23537
|
const issues = configSnapshot.issues.length > 0 ? formatConfigIssueLines(configSnapshot.issues, "", { normalizeRoot: true }).join("\n") : "Unknown validation issue.";
|
|
23474
23538
|
throw new Error(`Invalid config at ${configSnapshot.path}.\n${issues}\nRun "${formatCliCommand("johnness doctor")}" to repair, then retry.`);
|
|
23475
23539
|
}
|
|
23540
|
+
/* JOHNNESS_PATCH_ALIAS_LATEST_79: bare shorthands always point at the newest shipped model. A user
|
|
23541
|
+
alias that pins an older model of the same line moves to the shipped default; the old model keeps a
|
|
23542
|
+
versioned alias. Written back to the config file once, logged, idempotent. */
|
|
23543
|
+
try {
|
|
23544
|
+
const parsed79 = configSnapshot.parsed && typeof configSnapshot.parsed === "object" ? structuredClone(configSnapshot.parsed) : null;
|
|
23545
|
+
const models79 = parsed79?.agents?.defaults?.models;
|
|
23546
|
+
const moved79 = !isNixMode && models79 && typeof models79 === "object" && !Array.isArray(models79) ? johnnessMigrateStaleShorthands79(models79) : [];
|
|
23547
|
+
if (moved79.length > 0) {
|
|
23548
|
+
await writeConfigFile(parsed79);
|
|
23549
|
+
log.info(`gateway: model shorthands moved to the newest shipped models:\n${moved79.map((move) => `- ${move.alias}: ${move.from} -> ${move.to}${move.versionedAlias ? ` (${move.from} is now "${move.versionedAlias}")` : ""}`).join("\n")}`);
|
|
23550
|
+
configSnapshot = await readConfigFileSnapshot();
|
|
23551
|
+
}
|
|
23552
|
+
} catch (err) {
|
|
23553
|
+
log.warn(`gateway: model shorthand migration skipped: ${String(err)}`);
|
|
23554
|
+
}
|
|
23476
23555
|
const autoEnable = applyPluginAutoEnable({
|
|
23477
23556
|
config: configSnapshot.config,
|
|
23478
23557
|
env: process.env
|
|
@@ -24776,3 +24855,4 @@ function registerGatewayCli(program) {
|
|
|
24776
24855
|
}
|
|
24777
24856
|
//#endregion
|
|
24778
24857
|
export { registerGatewayCli };
|
|
24858
|
+
import { johnnessMigrateStaleShorthands79 } from "./auth-profiles-5CHn7vq1.js"; // JOHNNESS_PATCH_ALIAS_LATEST_79
|