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.
Files changed (40) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/auth-profiles-5CHn7vq1.js +80 -0
  3. package/dist/compact-B247y5Qt.js +7 -1
  4. package/dist/config-koj5M_EO.js +79 -0
  5. package/dist/daemon-cli.js +79 -0
  6. package/dist/gateway-cli-B7fBU7gD.js +81 -1
  7. package/dist/gateway-cli-BCNMwzNq.js +81 -1
  8. package/dist/model-selection-BGlGpPgM.js +79 -0
  9. package/dist/model-selection-BU6wl1le.js +79 -0
  10. package/dist/model-selection-L7RMwsG-.js +80 -0
  11. package/dist/pi-embedded-C6ITuRXf.js +7 -1
  12. package/dist/pi-embedded-DoQsYfIY.js +7 -1
  13. package/dist/plugin-sdk/bluebubbles.js +1 -0
  14. package/dist/plugin-sdk/config-4Fgm-yEH.js +79 -0
  15. package/dist/plugin-sdk/config-Di-lyCdh.js +79 -0
  16. package/dist/plugin-sdk/discord.js +79 -0
  17. package/dist/plugin-sdk/dispatch-BP0viZiL.js +7 -1
  18. package/dist/plugin-sdk/dispatch-CQsjmw7g.js +7 -1
  19. package/dist/plugin-sdk/dispatch-Cerq29sy.js +7 -1
  20. package/dist/plugin-sdk/dispatch-Cndjtt0g.js +7 -1
  21. package/dist/plugin-sdk/dispatch-UogiJYul.js +7 -1
  22. package/dist/plugin-sdk/feishu.js +1 -0
  23. package/dist/plugin-sdk/googlechat.js +1 -0
  24. package/dist/plugin-sdk/imessage.js +1 -0
  25. package/dist/plugin-sdk/model-auth-CX9cPHdC.js +79 -0
  26. package/dist/plugin-sdk/model-auth-Ciehz0x5.js +79 -0
  27. package/dist/plugin-sdk/model-auth-DVyo6JSX.js +79 -0
  28. package/dist/plugin-sdk/model-auth-kiHYHGrs.js +79 -0
  29. package/dist/plugin-sdk/model-selection-Dbj4jDYM.js +79 -0
  30. package/dist/plugin-sdk/reply-DbZnH8-h.js +7 -1
  31. package/dist/plugin-sdk/signal.js +1 -0
  32. package/dist/plugin-sdk/slack.js +1 -0
  33. package/dist/plugin-sdk/telegram.js +1 -0
  34. package/dist/plugin-sdk/tlon.js +1 -0
  35. package/dist/plugin-sdk/twitch.js +1 -0
  36. package/dist/plugin-sdk/zalo.js +1 -0
  37. package/dist/plugin-sdk/zalouser.js +1 -0
  38. package/dist/reply-C5LKjXcC.js +7 -1
  39. package/docs/PATCHES.md +9 -0
  40. 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
@@ -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 : 6e4
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, {}));
@@ -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(),
@@ -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