@wrongstack/core 0.306.3 → 0.306.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coordination/index.js +136 -40
- package/dist/coordination/mail-tools.d.ts +1 -1
- package/dist/core/conversation-state.d.ts +1 -2
- package/dist/core/fallback-model.d.ts +19 -1
- package/dist/core/fallback-profile-manager.d.ts +21 -3
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +152 -46
- package/dist/defaults/index.js +295 -91
- package/dist/execution/auto-compaction-middleware.d.ts +64 -0
- package/dist/execution/compaction-core.d.ts +4 -0
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +230 -89
- package/dist/execution/retry-policy.d.ts +27 -0
- package/dist/hq/index.js +50 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +588 -134
- package/dist/infrastructure/index.js +21 -0
- package/dist/plugin/index.js +195 -19
- package/dist/security/index.js +50 -15
- package/dist/session-catalog/index.js +50 -15
- package/dist/session-catalog/project-server.js +50 -15
- package/dist/storage/cloud-config-sync/sanitize.d.ts +18 -0
- package/dist/storage/cloud-config-sync.d.ts +1 -1
- package/dist/storage/index.js +276 -23
- package/dist/storage/provider-config-watcher.d.ts +9 -0
- package/dist/storage/session-store/strict-empty-check.d.ts +7 -0
- package/dist/storage/session-store.d.ts +1 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +83 -22
- package/dist/types/config/root.d.ts +14 -0
- package/dist/types/session.d.ts +7 -0
- package/package.json +3 -3
package/dist/execution/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/core/context.ts
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
|
|
4
5
|
// src/types/blocks.ts
|
|
@@ -552,12 +553,15 @@ var ConversationState = class {
|
|
|
552
553
|
* cap determines the starting index for the sum but does not gate it.
|
|
553
554
|
*/
|
|
554
555
|
overflowCount(arr) {
|
|
555
|
-
|
|
556
|
-
|
|
556
|
+
const contextClass = this.ctx.constructor;
|
|
557
|
+
const maxMessages = contextClass.MAX_MESSAGES;
|
|
558
|
+
const maxMessageTokens = contextClass.MAX_MESSAGE_TOKENS;
|
|
559
|
+
let drop = maxMessages > 0 ? Math.max(0, arr.length - maxMessages) : 0;
|
|
560
|
+
if (maxMessageTokens <= 0) return this.protocolSafeDropCount(arr, drop);
|
|
557
561
|
let total = 0;
|
|
558
562
|
for (let i = drop; i < arr.length; i++) total += arr[i]?._estTokens ?? 0;
|
|
559
|
-
if (total <=
|
|
560
|
-
while (drop < arr.length - 1 && total >
|
|
563
|
+
if (total <= maxMessageTokens) return this.protocolSafeDropCount(arr, drop);
|
|
564
|
+
while (drop < arr.length - 1 && total > maxMessageTokens) {
|
|
561
565
|
total -= arr[drop]?._estTokens ?? 0;
|
|
562
566
|
drop++;
|
|
563
567
|
}
|
|
@@ -1380,6 +1384,19 @@ var Context = class _Context {
|
|
|
1380
1384
|
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
1381
1385
|
throw new Error(`Working directory "${resolved}" is outside project root "${root}"`);
|
|
1382
1386
|
}
|
|
1387
|
+
let realTarget = resolved;
|
|
1388
|
+
let realRoot = root;
|
|
1389
|
+
try {
|
|
1390
|
+
realTarget = realpathSync.native(resolved);
|
|
1391
|
+
realRoot = realpathSync.native(root);
|
|
1392
|
+
} catch {
|
|
1393
|
+
}
|
|
1394
|
+
const realRel = path.relative(realRoot, realTarget);
|
|
1395
|
+
if (realRel.startsWith("..") || path.isAbsolute(realRel)) {
|
|
1396
|
+
throw new Error(
|
|
1397
|
+
`Working directory "${resolved}" resolves to "${realTarget}", outside project root "${realRoot}"`
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1383
1400
|
}
|
|
1384
1401
|
const old = this.workingDir;
|
|
1385
1402
|
this.workingDir = resolved;
|
|
@@ -2752,6 +2769,12 @@ function findExchangeStart(messages, userIndex) {
|
|
|
2752
2769
|
|
|
2753
2770
|
// src/execution/auto-compaction-middleware.ts
|
|
2754
2771
|
var LEVEL_RANK = { warn: 0, soft: 1, hard: 2 };
|
|
2772
|
+
function pressureLevelFor(load, thresholds) {
|
|
2773
|
+
if (load >= thresholds.hard) return "hard";
|
|
2774
|
+
if (load >= thresholds.soft) return "soft";
|
|
2775
|
+
if (load >= thresholds.warn) return "warn";
|
|
2776
|
+
return null;
|
|
2777
|
+
}
|
|
2755
2778
|
var MAX_DIGEST_LOG_CHARS = 4e3;
|
|
2756
2779
|
function truncateDigest(digest) {
|
|
2757
2780
|
if (digest.length <= MAX_DIGEST_LOG_CHARS) return digest;
|
|
@@ -2797,8 +2820,19 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
2797
2820
|
* 1 / 2.5 = 0.4.
|
|
2798
2821
|
*/
|
|
2799
2822
|
static GUARD_GATE_LOAD = 0.4;
|
|
2823
|
+
/**
|
|
2824
|
+
* How much the context must grow between two history-rewriting hygiene
|
|
2825
|
+
* passes, as a fraction of the available input window and as an absolute
|
|
2826
|
+
* floor. Every pass rewrites already-transmitted messages, which forces the
|
|
2827
|
+
* provider to re-cache the whole prompt; spacing the passes out is what lets
|
|
2828
|
+
* the conversation prefix stay cached for the turns in between.
|
|
2829
|
+
*/
|
|
2830
|
+
static HYGIENE_GROWTH_RATIO = 0.15;
|
|
2831
|
+
static HYGIENE_MIN_GROWTH_TOKENS = 2e4;
|
|
2800
2832
|
/** Tracks the most recent no-op attempt so we can avoid re-firing per turn. */
|
|
2801
2833
|
lastNoopAttempt = null;
|
|
2834
|
+
/** Context size at the last hygiene pass; anchors the growth interval. */
|
|
2835
|
+
lastHygieneTokens = null;
|
|
2802
2836
|
/**
|
|
2803
2837
|
* Cached token estimate from the last handler() invocation. When the
|
|
2804
2838
|
* message count and tool count haven't changed since the last estimate
|
|
@@ -2858,55 +2892,9 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
2858
2892
|
handler() {
|
|
2859
2893
|
return async (ctx, next) => {
|
|
2860
2894
|
if (!this._enabled) return next(ctx);
|
|
2861
|
-
|
|
2862
|
-
maxRetainedTokens: this.resolveToolResultRetention(ctx)
|
|
2863
|
-
});
|
|
2864
|
-
const receiptHygiene = collapseAcknowledgedToolReceipts(rawHygiene.messages, {
|
|
2865
|
-
maxPairs: this.resolveToolReceiptRetention(ctx)
|
|
2866
|
-
});
|
|
2867
|
-
if (rawHygiene.changed || receiptHygiene.changed) {
|
|
2868
|
-
ctx.state.replaceMessages(receiptHygiene.messages);
|
|
2869
|
-
ctx.clearFileTracking();
|
|
2870
|
-
this.invalidateTokenCaches(ctx);
|
|
2871
|
-
}
|
|
2872
|
-
const msgCount = ctx.messages.length;
|
|
2873
|
-
const toolCount = (ctx.tools ?? []).length;
|
|
2874
|
-
const revision = ctx.state?.revision ?? -1;
|
|
2875
|
-
let tokens;
|
|
2876
|
-
const anchorAt = typeof ctx.meta?.["realAnchorMsgCount"] === "number" ? ctx.meta["realAnchorMsgCount"] : void 0;
|
|
2877
|
-
const anchored = realAnchoredInputTokens(ctx.messages, ctx.lastRealInputTokens, anchorAt);
|
|
2878
|
-
if (anchored !== null) {
|
|
2879
|
-
tokens = anchored;
|
|
2880
|
-
} else if (this._estimator) {
|
|
2881
|
-
tokens = this._estimator(ctx);
|
|
2882
|
-
} else if (msgCount === this._cachedMsgCount && toolCount === this._cachedToolCount && revision === this._cachedRevision && ctx.systemPrompt === this._cachedSystemRef && ctx.tools === this._cachedToolsRef && this._cachedTokens >= 0) {
|
|
2883
|
-
tokens = this._cachedTokens;
|
|
2884
|
-
} else if (this.tryStashedTokens(ctx, msgCount, toolCount, revision) !== null) {
|
|
2885
|
-
const stashed = this.tryStashedTokens(ctx, msgCount, toolCount, revision);
|
|
2886
|
-
const cal = getCalibrationState(`${ctx.provider?.id ?? "unknown"}/${ctx.model}`);
|
|
2887
|
-
tokens = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
|
|
2888
|
-
this._cachedTokens = tokens;
|
|
2889
|
-
this._cachedMsgCount = msgCount;
|
|
2890
|
-
this._cachedToolCount = toolCount;
|
|
2891
|
-
this._cachedRevision = revision;
|
|
2892
|
-
this._cachedSystemRef = ctx.systemPrompt;
|
|
2893
|
-
this._cachedToolsRef = ctx.tools;
|
|
2894
|
-
} else {
|
|
2895
|
-
tokens = estimateRequestTokensCalibrated(
|
|
2896
|
-
ctx.messages,
|
|
2897
|
-
ctx.systemPrompt,
|
|
2898
|
-
ctx.tools ?? [],
|
|
2899
|
-
`${ctx.provider?.id ?? "unknown"}/${ctx.model}`
|
|
2900
|
-
).total;
|
|
2901
|
-
this._cachedTokens = tokens;
|
|
2902
|
-
this._cachedMsgCount = msgCount;
|
|
2903
|
-
this._cachedToolCount = toolCount;
|
|
2904
|
-
this._cachedRevision = revision;
|
|
2905
|
-
this._cachedSystemRef = ctx.systemPrompt;
|
|
2906
|
-
this._cachedToolsRef = ctx.tools;
|
|
2907
|
-
}
|
|
2895
|
+
let tokens = this.estimateContextTokens(ctx);
|
|
2908
2896
|
const runtimeMaxContext = effectiveMaxContext(ctx, this._maxContext);
|
|
2909
|
-
|
|
2897
|
+
let budget = computeContextWindowBudget(ctx, tokens, runtimeMaxContext);
|
|
2910
2898
|
const calibratedLoad = budget.load;
|
|
2911
2899
|
const policy = this.policyProvider?.(ctx);
|
|
2912
2900
|
const thresholds = policy?.thresholds ?? {
|
|
@@ -2920,22 +2908,27 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
2920
2908
|
});
|
|
2921
2909
|
const aggressiveOn = policy?.aggressiveOn ?? this.aggressiveOn;
|
|
2922
2910
|
const targetLoad = normalizeTargetLoad(policy?.targetLoad, adaptiveThresholds);
|
|
2923
|
-
let load = calibratedLoad;
|
|
2924
|
-
|
|
2925
|
-
const guardTotal = estimateRequestTokensUpperBound(
|
|
2926
|
-
ctx.messages,
|
|
2927
|
-
ctx.systemPrompt,
|
|
2928
|
-
ctx.tools ?? [],
|
|
2929
|
-
`${ctx.provider?.id ?? "unknown"}/${ctx.model}`
|
|
2930
|
-
).total;
|
|
2931
|
-
const guardLoad = guardTotal / budget.availableInputTokens;
|
|
2932
|
-
if (guardLoad > load) load = guardLoad;
|
|
2933
|
-
}
|
|
2934
|
-
const level = load >= adaptiveThresholds.hard ? "hard" : load >= adaptiveThresholds.soft ? "soft" : load >= adaptiveThresholds.warn ? "warn" : null;
|
|
2911
|
+
let load = this.applySendGuard(ctx, calibratedLoad, budget.availableInputTokens);
|
|
2912
|
+
let level = pressureLevelFor(load, adaptiveThresholds);
|
|
2935
2913
|
if (!level) {
|
|
2936
2914
|
this.lastNoopAttempt = null;
|
|
2937
2915
|
return next(ctx);
|
|
2938
2916
|
}
|
|
2917
|
+
if (this.shouldRunHygiene(level, tokens, budget.availableInputTokens)) {
|
|
2918
|
+
const changed = this.runHistoryHygiene(ctx);
|
|
2919
|
+
tokens = changed ? this.estimateContextTokens(ctx) : tokens;
|
|
2920
|
+
this.lastHygieneTokens = tokens;
|
|
2921
|
+
if (changed) {
|
|
2922
|
+
budget = computeContextWindowBudget(ctx, tokens, runtimeMaxContext);
|
|
2923
|
+
load = this.applySendGuard(ctx, budget.load, budget.availableInputTokens);
|
|
2924
|
+
const relevelled = pressureLevelFor(load, adaptiveThresholds);
|
|
2925
|
+
if (!relevelled) {
|
|
2926
|
+
this.lastNoopAttempt = null;
|
|
2927
|
+
return next(ctx);
|
|
2928
|
+
}
|
|
2929
|
+
level = relevelled;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2939
2932
|
if (this.shouldSkipNoopRetry(level, tokens)) {
|
|
2940
2933
|
return next(ctx);
|
|
2941
2934
|
}
|
|
@@ -2952,6 +2945,123 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
2952
2945
|
return next(ctx);
|
|
2953
2946
|
};
|
|
2954
2947
|
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Full-request token total for the current context.
|
|
2950
|
+
*
|
|
2951
|
+
* Reuses the last estimate when the context hasn't grown since the previous
|
|
2952
|
+
* check — common in autonomous idle loops. The cached value is invalidated
|
|
2953
|
+
* whenever messages or tools change.
|
|
2954
|
+
*
|
|
2955
|
+
* IMPORTANT: the cache is only valid for the deterministic
|
|
2956
|
+
* `estimateRequestTokensCalibrated` path (messages+system+tools → fixed
|
|
2957
|
+
* output). When a custom `_estimator` is provided (e.g. in tests with a
|
|
2958
|
+
* mutable closure, or a dynamic policy provider), always call it fresh — the
|
|
2959
|
+
* estimator owns its own semantics and the middleware cannot safely cache its
|
|
2960
|
+
* result across calls.
|
|
2961
|
+
*/
|
|
2962
|
+
estimateContextTokens(ctx) {
|
|
2963
|
+
const msgCount = ctx.messages.length;
|
|
2964
|
+
const toolCount = (ctx.tools ?? []).length;
|
|
2965
|
+
const revision = ctx.state?.revision ?? -1;
|
|
2966
|
+
const anchorAt = typeof ctx.meta?.["realAnchorMsgCount"] === "number" ? ctx.meta["realAnchorMsgCount"] : void 0;
|
|
2967
|
+
const anchored = realAnchoredInputTokens(ctx.messages, ctx.lastRealInputTokens, anchorAt);
|
|
2968
|
+
if (anchored !== null) return anchored;
|
|
2969
|
+
if (this._estimator) return this._estimator(ctx);
|
|
2970
|
+
if (msgCount === this._cachedMsgCount && toolCount === this._cachedToolCount && revision === this._cachedRevision && ctx.systemPrompt === this._cachedSystemRef && ctx.tools === this._cachedToolsRef && this._cachedTokens >= 0) {
|
|
2971
|
+
return this._cachedTokens;
|
|
2972
|
+
}
|
|
2973
|
+
const stashed = this.tryStashedTokens(ctx, msgCount, toolCount, revision);
|
|
2974
|
+
let tokens;
|
|
2975
|
+
if (stashed !== null) {
|
|
2976
|
+
const cal = getCalibrationState(`${ctx.provider?.id ?? "unknown"}/${ctx.model}`);
|
|
2977
|
+
tokens = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
|
|
2978
|
+
} else {
|
|
2979
|
+
tokens = estimateRequestTokensCalibrated(
|
|
2980
|
+
ctx.messages,
|
|
2981
|
+
ctx.systemPrompt,
|
|
2982
|
+
ctx.tools ?? [],
|
|
2983
|
+
`${ctx.provider?.id ?? "unknown"}/${ctx.model}`
|
|
2984
|
+
).total;
|
|
2985
|
+
}
|
|
2986
|
+
this._cachedTokens = tokens;
|
|
2987
|
+
this._cachedMsgCount = msgCount;
|
|
2988
|
+
this._cachedToolCount = toolCount;
|
|
2989
|
+
this._cachedRevision = revision;
|
|
2990
|
+
this._cachedSystemRef = ctx.systemPrompt;
|
|
2991
|
+
this._cachedToolsRef = ctx.tools;
|
|
2992
|
+
return tokens;
|
|
2993
|
+
}
|
|
2994
|
+
/**
|
|
2995
|
+
* Never-undercount send guard.
|
|
2996
|
+
*
|
|
2997
|
+
* The calibrated estimate can under-count dense content (CJK, base64,
|
|
2998
|
+
* minified) by >1.5×, which would let an over-limit request slip past the
|
|
2999
|
+
* thresholds and reach the provider. Once the calibrated load is high enough
|
|
3000
|
+
* that even the max density factor (2.5×) *could* overflow (load ≥ 1/2.5 =
|
|
3001
|
+
* 0.4), re-check with the upper-bound estimator and escalate to whichever
|
|
3002
|
+
* load is larger. Below 0.4 an overflow is arithmetically impossible, so the
|
|
3003
|
+
* extra scan is skipped.
|
|
3004
|
+
*/
|
|
3005
|
+
applySendGuard(ctx, calibratedLoad, availableInputTokens) {
|
|
3006
|
+
if (calibratedLoad < _AutoCompactionMiddleware.GUARD_GATE_LOAD) return calibratedLoad;
|
|
3007
|
+
const guardTotal = estimateRequestTokensUpperBound(
|
|
3008
|
+
ctx.messages,
|
|
3009
|
+
ctx.systemPrompt,
|
|
3010
|
+
ctx.tools ?? [],
|
|
3011
|
+
`${ctx.provider?.id ?? "unknown"}/${ctx.model}`
|
|
3012
|
+
).total;
|
|
3013
|
+
const guardLoad = guardTotal / availableInputTokens;
|
|
3014
|
+
return guardLoad > calibratedLoad ? guardLoad : calibratedLoad;
|
|
3015
|
+
}
|
|
3016
|
+
/**
|
|
3017
|
+
* Rewrite acknowledged tool protocol in place.
|
|
3018
|
+
*
|
|
3019
|
+
* Tool results are protocol inputs for the immediately following model
|
|
3020
|
+
* response, not unlimited durable prompt memory. Once a later assistant
|
|
3021
|
+
* message proves the provider consumed them, keep a mode-sized raw window and
|
|
3022
|
+
* replace the rest with semantic head/tail receipts, then fold fully-aged
|
|
3023
|
+
* pairs into the history digest.
|
|
3024
|
+
*
|
|
3025
|
+
* Every one of those edits touches a message the provider has already seen,
|
|
3026
|
+
* so each call costs a full prompt re-cache — it belongs behind
|
|
3027
|
+
* {@link shouldRunHygiene}, never on the per-turn path. Content staleness is
|
|
3028
|
+
* not a reason to bypass that gate: the edit/replace tools guard against
|
|
3029
|
+
* acting on an out-of-date read with their own mtime + sha-256 check.
|
|
3030
|
+
*
|
|
3031
|
+
* @returns whether the conversation was rewritten.
|
|
3032
|
+
*/
|
|
3033
|
+
runHistoryHygiene(ctx) {
|
|
3034
|
+
const raw = eliseAcknowledgedToolResults(ctx.messages, {
|
|
3035
|
+
maxRetainedTokens: this.resolveToolResultRetention(ctx)
|
|
3036
|
+
});
|
|
3037
|
+
const receipts = collapseAcknowledgedToolReceipts(raw.messages, {
|
|
3038
|
+
maxPairs: this.resolveToolReceiptRetention(ctx)
|
|
3039
|
+
});
|
|
3040
|
+
if (!raw.changed && !receipts.changed) return false;
|
|
3041
|
+
ctx.state.replaceMessages(receipts.messages);
|
|
3042
|
+
ctx.clearFileTracking();
|
|
3043
|
+
this.invalidateTokenCaches(ctx);
|
|
3044
|
+
return true;
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* Whether the history-rewriting hygiene pass may run this turn.
|
|
3048
|
+
*
|
|
3049
|
+
* Hard pressure always runs it — staying under the window outranks caching.
|
|
3050
|
+
* Otherwise it runs at most once per growth interval, so the conversation
|
|
3051
|
+
* stays append-only (and therefore cacheable by the provider) in between.
|
|
3052
|
+
*/
|
|
3053
|
+
shouldRunHygiene(level, tokens, availableInputTokens) {
|
|
3054
|
+
if (level === "hard") return true;
|
|
3055
|
+
const last = this.lastHygieneTokens;
|
|
3056
|
+
if (last === null) return true;
|
|
3057
|
+
const anchor = Math.min(last, tokens);
|
|
3058
|
+
this.lastHygieneTokens = anchor;
|
|
3059
|
+
const interval = Math.max(
|
|
3060
|
+
_AutoCompactionMiddleware.HYGIENE_MIN_GROWTH_TOKENS,
|
|
3061
|
+
Math.floor(availableInputTokens * _AutoCompactionMiddleware.HYGIENE_GROWTH_RATIO)
|
|
3062
|
+
);
|
|
3063
|
+
return tokens - anchor >= interval;
|
|
3064
|
+
}
|
|
2955
3065
|
/**
|
|
2956
3066
|
* H1: try to read a pre-computed token total from `ctx.lastRequestTokens`
|
|
2957
3067
|
* (set by the agent loop's pre-flight or its restash in emitContextPct).
|
|
@@ -4964,6 +5074,12 @@ function normalizeModelRef(ref, defaultProvider) {
|
|
|
4964
5074
|
function hasText(value) {
|
|
4965
5075
|
return typeof value === "string" && value.trim().length > 0;
|
|
4966
5076
|
}
|
|
5077
|
+
function asRefList(value) {
|
|
5078
|
+
return Array.isArray(value) ? value : void 0;
|
|
5079
|
+
}
|
|
5080
|
+
function asProfileName(value) {
|
|
5081
|
+
return hasText(value) ? value : void 0;
|
|
5082
|
+
}
|
|
4967
5083
|
function providerHasKey(entry) {
|
|
4968
5084
|
if (!entry) return false;
|
|
4969
5085
|
if (hasText(entry.apiKey)) return true;
|
|
@@ -4974,7 +5090,7 @@ function providerHasKey(entry) {
|
|
|
4974
5090
|
}
|
|
4975
5091
|
function visibleProviderModels(config, providerId, providerModels) {
|
|
4976
5092
|
const entry = config.providers?.[providerId];
|
|
4977
|
-
return entry?.models
|
|
5093
|
+
return Array.isArray(entry?.models) ? [...entry.models] : providerModels;
|
|
4978
5094
|
}
|
|
4979
5095
|
function buildProfiles(config) {
|
|
4980
5096
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -5011,13 +5127,34 @@ var FallbackProfileManager = class {
|
|
|
5011
5127
|
listProfiles() {
|
|
5012
5128
|
return Object.freeze([...this.profiles.keys()]);
|
|
5013
5129
|
}
|
|
5130
|
+
/**
|
|
5131
|
+
* The profile the session has selected (`config.fallbackProfile`, set by
|
|
5132
|
+
* `/fallback profile use <name>`), or undefined when none is selected or the
|
|
5133
|
+
* name no longer resolves to a defined profile.
|
|
5134
|
+
*
|
|
5135
|
+
* Consulted by every resolution entry point when the caller does not name a
|
|
5136
|
+
* profile itself. Without this the leader — which passes no profile — could
|
|
5137
|
+
* never use a named profile at all: `config.fallbackProfiles` was reachable
|
|
5138
|
+
* only by copying a chain into `fallbackModels`.
|
|
5139
|
+
*/
|
|
5140
|
+
activeProfileName() {
|
|
5141
|
+
const name = asProfileName(this.config.fallbackProfile);
|
|
5142
|
+
return name && this.profiles.has(name) ? name : void 0;
|
|
5143
|
+
}
|
|
5014
5144
|
// ── Resolution ─────────────────────────────────────────────────────────
|
|
5015
5145
|
/**
|
|
5016
5146
|
* Resolve a named fallback profile to a validated, provider-filtered chain.
|
|
5017
5147
|
*
|
|
5018
|
-
* Returns an empty chain when
|
|
5019
|
-
*
|
|
5020
|
-
*
|
|
5148
|
+
* Returns an empty chain when the profile doesn't exist, or when every entry
|
|
5149
|
+
* is excluded, quarantined, or blacked out.
|
|
5150
|
+
*
|
|
5151
|
+
* Filtering is intentionally identical to {@link resolveRefs} (the explicit
|
|
5152
|
+
* `fallbackModels` path): the self-exclusion, the runtime status tracker,
|
|
5153
|
+
* and the availability calendar — nothing else. Anything a named profile
|
|
5154
|
+
* drops, an explicit chain drops too, and vice versa. Profiles used to apply
|
|
5155
|
+
* two extra filters (provider "usability" and the `providers[].models`
|
|
5156
|
+
* snapshot) that the explicit path did not, which silently rerouted roles
|
|
5157
|
+
* pinned to a profile onto a different model than the one configured.
|
|
5021
5158
|
*
|
|
5022
5159
|
* @param name - Profile name from config.fallbackProfiles.
|
|
5023
5160
|
* @param defaultProvider - Used when an entry has no explicit provider.
|
|
@@ -5038,13 +5175,9 @@ var FallbackProfileManager = class {
|
|
|
5038
5175
|
if (seen.has(key)) continue;
|
|
5039
5176
|
seen.add(key);
|
|
5040
5177
|
if (excludeKey && key === excludeKey) continue;
|
|
5041
|
-
const health = this.checkProvider(providerId);
|
|
5042
|
-
if (!health.usable) continue;
|
|
5043
5178
|
if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
|
|
5044
5179
|
if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
|
|
5045
5180
|
continue;
|
|
5046
|
-
const allowedModels = this.config.providers?.[providerId]?.models;
|
|
5047
|
-
if (allowedModels && !allowedModels.includes(parsed.model)) continue;
|
|
5048
5181
|
resolved.push({
|
|
5049
5182
|
providerId,
|
|
5050
5183
|
model: parsed.model,
|
|
@@ -5062,12 +5195,14 @@ var FallbackProfileManager = class {
|
|
|
5062
5195
|
resolveEffective(opts = {}) {
|
|
5063
5196
|
const bridge = this.resolveBridge(opts.exclude);
|
|
5064
5197
|
let selected = FREEZER_EMPTY;
|
|
5065
|
-
|
|
5066
|
-
|
|
5198
|
+
const explicitRefs = asRefList(opts.fallbackModels);
|
|
5199
|
+
const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
|
|
5200
|
+
if (explicitRefs && explicitRefs.length > 0) {
|
|
5201
|
+
const resolved = this.resolveRefs(explicitRefs, opts.exclude);
|
|
5067
5202
|
if (resolved.length > 0) selected = resolved;
|
|
5068
5203
|
}
|
|
5069
|
-
if (selected.length === 0 &&
|
|
5070
|
-
const resolved = this.resolve(
|
|
5204
|
+
if (selected.length === 0 && profileName) {
|
|
5205
|
+
const resolved = this.resolve(profileName, { exclude: opts.exclude });
|
|
5071
5206
|
if (resolved.length > 0) selected = resolved;
|
|
5072
5207
|
}
|
|
5073
5208
|
if (selected.length === 0 && opts.fallbackAuto !== false) {
|
|
@@ -5143,13 +5278,14 @@ var FallbackProfileManager = class {
|
|
|
5143
5278
|
};
|
|
5144
5279
|
const configFallbackAuto = this.config.fallbackAuto;
|
|
5145
5280
|
const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
|
|
5146
|
-
const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
|
|
5281
|
+
const explicitRefs = asRefList(opts.fallbackModels) ?? asRefList(this.config.fallbackModels);
|
|
5282
|
+
const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
|
|
5147
5283
|
const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
|
|
5148
|
-
const profileUsable =
|
|
5284
|
+
const profileUsable = profileName !== void 0 && this.hasProfile(profileName) && this.resolve(profileName, { exclude: current }).length > 0;
|
|
5149
5285
|
const fromExplicitSource = explicitUsable || profileUsable;
|
|
5150
|
-
const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) :
|
|
5286
|
+
const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : profileName ? this.resolve(profileName, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
|
|
5151
5287
|
fallbackModels: explicitRefs,
|
|
5152
|
-
fallbackProfile:
|
|
5288
|
+
fallbackProfile: profileName,
|
|
5153
5289
|
fallbackAuto: effectiveFallbackAuto,
|
|
5154
5290
|
exclude: current
|
|
5155
5291
|
});
|
|
@@ -5166,7 +5302,7 @@ var FallbackProfileManager = class {
|
|
|
5166
5302
|
});
|
|
5167
5303
|
}
|
|
5168
5304
|
candidates.push(...selectedChain);
|
|
5169
|
-
if (!fromExplicitSource && effectiveFallbackAuto &&
|
|
5305
|
+
if (!fromExplicitSource && effectiveFallbackAuto && profileName !== "default") {
|
|
5170
5306
|
candidates.push(...this.resolve("default", { exclude: current }));
|
|
5171
5307
|
}
|
|
5172
5308
|
if (!fromExplicitSource && effectiveFallbackAuto) {
|
|
@@ -5244,7 +5380,7 @@ var FallbackProfileManager = class {
|
|
|
5244
5380
|
const leaderModel = this.config.model;
|
|
5245
5381
|
const providers = this.config.providers ?? {};
|
|
5246
5382
|
const favoriteSet = new Set(
|
|
5247
|
-
(this.config.favoriteModels ?? []).map((ref) => {
|
|
5383
|
+
(asRefList(this.config.favoriteModels) ?? []).map((ref) => {
|
|
5248
5384
|
const p = parseModelRef(ref);
|
|
5249
5385
|
return `${p.provider ?? leaderProvider}/${p.model}`;
|
|
5250
5386
|
})
|
|
@@ -5324,6 +5460,7 @@ function effectiveFallbackChain(config) {
|
|
|
5324
5460
|
const mgr = new FallbackProfileManager(config);
|
|
5325
5461
|
return mgr.resolveEffective({
|
|
5326
5462
|
fallbackModels: config.fallbackModels,
|
|
5463
|
+
fallbackProfile: config.fallbackProfile,
|
|
5327
5464
|
fallbackAuto: config.fallbackAuto
|
|
5328
5465
|
}).map((e) => `${e.providerId}/${e.model}`);
|
|
5329
5466
|
}
|
|
@@ -19164,26 +19301,29 @@ function escapeRegExp(s) {
|
|
|
19164
19301
|
// src/execution/retry-policy.ts
|
|
19165
19302
|
import { randomInt } from "node:crypto";
|
|
19166
19303
|
var MAX_RETRY_AFTER_MS = 6e4;
|
|
19304
|
+
var MODEL_RETRIES = 3;
|
|
19167
19305
|
var MAX_ATTEMPTS_BY_KIND = {
|
|
19168
|
-
rate_limit:
|
|
19306
|
+
rate_limit: MODEL_RETRIES,
|
|
19307
|
+
overloaded: MODEL_RETRIES,
|
|
19308
|
+
server: MODEL_RETRIES,
|
|
19309
|
+
timeout: MODEL_RETRIES,
|
|
19310
|
+
network: MODEL_RETRIES,
|
|
19311
|
+
stream_hang: MODEL_RETRIES,
|
|
19169
19312
|
quota_exhausted: 0,
|
|
19170
|
-
stream_hang: 2,
|
|
19171
|
-
// proxy-level timeout — retrying 5x wastes ~40s before fallback kicks in
|
|
19172
|
-
overloaded: 3,
|
|
19173
|
-
server: 3,
|
|
19174
|
-
timeout: 2,
|
|
19175
|
-
network: 2,
|
|
19176
19313
|
auth: 0,
|
|
19177
19314
|
invalid_request: 0,
|
|
19178
19315
|
context_overflow: 0,
|
|
19179
19316
|
content_filter: 0,
|
|
19180
19317
|
unknown: 0
|
|
19181
19318
|
};
|
|
19319
|
+
var FAILOVER_RETRY_AFTER_MS = 15e3;
|
|
19182
19320
|
var DefaultRetryPolicy = class {
|
|
19183
19321
|
shouldRetry(err, attempt) {
|
|
19184
19322
|
const isProviderErr = err instanceof ProviderError || ProviderError.isProviderError(err);
|
|
19185
19323
|
if (isProviderErr) {
|
|
19186
19324
|
if (!err.retryable) return false;
|
|
19325
|
+
const hint = retryAfterMsFromError(err);
|
|
19326
|
+
if (hint !== void 0 && hint >= FAILOVER_RETRY_AFTER_MS) return false;
|
|
19187
19327
|
return attempt < this.maxAttempts(err);
|
|
19188
19328
|
}
|
|
19189
19329
|
const msg = err.message ?? "";
|
|
@@ -22992,6 +23132,7 @@ export {
|
|
|
22992
23132
|
HybridCompactor,
|
|
22993
23133
|
IntelligentCompactor,
|
|
22994
23134
|
MAX_COUNCIL_CONCURRENCY,
|
|
23135
|
+
MODEL_RETRIES,
|
|
22995
23136
|
OneShotOrchestrator,
|
|
22996
23137
|
ParallelEternalEngine,
|
|
22997
23138
|
SelectiveCompactor,
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { ProviderError } from '../types/provider.js';
|
|
2
2
|
import type { RetryPolicy } from '../types/retry-policy.js';
|
|
3
|
+
/**
|
|
4
|
+
* Retries a single model gets before the turn moves on to the next entry in
|
|
5
|
+
* the fallback chain.
|
|
6
|
+
*
|
|
7
|
+
* One number for every recoverable failure kind, on purpose. The per-kind
|
|
8
|
+
* budgets used to range from 2 (timeout, network, stream_hang) to 5
|
|
9
|
+
* (rate_limit), which made "how long until we try another model?" depend on
|
|
10
|
+
* which error came back: a rate-limited model could spend five backoff waits —
|
|
11
|
+
* each honouring a `Retry-After` up to 60s — before the cross-model fallback
|
|
12
|
+
* engine, which only engages once in-place retries are exhausted, got its
|
|
13
|
+
* turn, while a timeout gave up after two. The contract is now uniform and
|
|
14
|
+
* predictable across every surface that runs the agent loop (leader, host
|
|
15
|
+
* subagents, Chimera reviewers, SDD workers — they all resolve this one
|
|
16
|
+
* policy from the container): try the model {@link MODEL_RETRIES} times, then
|
|
17
|
+
* fail over.
|
|
18
|
+
*
|
|
19
|
+
* Exhaustive by construction (`Record<ProviderErrorKind, …>`) — adding a new
|
|
20
|
+
* kind refuses to compile until it gets an attempt budget.
|
|
21
|
+
*
|
|
22
|
+
* Zero means "do not replay this request against this model":
|
|
23
|
+
* - request-shaped failures (auth / invalid_request / context_overflow /
|
|
24
|
+
* content_filter) would fail identically on every attempt;
|
|
25
|
+
* - `quota_exhausted` is a depleted account or plan, not a transient burst, so
|
|
26
|
+
* the route is done until it resets — retrying only delays the hop, and the
|
|
27
|
+
* kind IS fallback-worthy, so another provider gets tried immediately.
|
|
28
|
+
*/
|
|
29
|
+
export declare const MODEL_RETRIES = 3;
|
|
3
30
|
export declare class DefaultRetryPolicy implements RetryPolicy {
|
|
4
31
|
shouldRetry(err: Error | ProviderError, attempt: number): boolean;
|
|
5
32
|
maxAttempts(err: Error | ProviderError): number;
|
package/dist/hq/index.js
CHANGED
|
@@ -675,6 +675,24 @@ import * as v8 from "node:v8";
|
|
|
675
675
|
var SESSION_RECIPIENT_PREFIX = "@session:";
|
|
676
676
|
|
|
677
677
|
// src/security/secret-scrubber.ts
|
|
678
|
+
var JSON_CREDENTIAL_KEY_ANCHORS = [
|
|
679
|
+
'Key"',
|
|
680
|
+
'key"',
|
|
681
|
+
'KEY"',
|
|
682
|
+
'token"',
|
|
683
|
+
'Token"',
|
|
684
|
+
'TOKEN"',
|
|
685
|
+
'secret"',
|
|
686
|
+
'Secret"',
|
|
687
|
+
'SECRET"',
|
|
688
|
+
'password"',
|
|
689
|
+
'Password"',
|
|
690
|
+
'PASSWORD"',
|
|
691
|
+
'authorization"',
|
|
692
|
+
'Authorization"',
|
|
693
|
+
'bearer"',
|
|
694
|
+
'Bearer"'
|
|
695
|
+
];
|
|
678
696
|
var PATTERNS = [
|
|
679
697
|
// Anchored at the start where possible so partial matches inside larger
|
|
680
698
|
// strings don't trigger false positives.
|
|
@@ -777,6 +795,30 @@ var PATTERNS = [
|
|
|
777
795
|
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
778
796
|
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
|
|
779
797
|
},
|
|
798
|
+
{
|
|
799
|
+
type: "json_credential_key",
|
|
800
|
+
// The JSON counterpart to `high_entropy_env`, and the pattern that the
|
|
801
|
+
// now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
|
|
802
|
+
// anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
|
|
803
|
+
// "this text may hold a secret", every pattern then declined to match, and
|
|
804
|
+
// the value went out verbatim. `high_entropy_env` cannot cover these: it
|
|
805
|
+
// requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
|
|
806
|
+
// never matched.
|
|
807
|
+
//
|
|
808
|
+
// Tool results are routinely serialised as JSON, and a credential with no
|
|
809
|
+
// recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
|
|
810
|
+
// has no other pattern that can catch it — this is the only thing standing
|
|
811
|
+
// between such a value and the session JSONL, chronicle, HQ broadcast and
|
|
812
|
+
// the model's own context.
|
|
813
|
+
//
|
|
814
|
+
// The key may carry a prefix (`"anthropicApiKey"`), but the credential word
|
|
815
|
+
// must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
|
|
816
|
+
// the closing quote has to follow the word immediately.
|
|
817
|
+
// Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
|
|
818
|
+
// out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
|
|
819
|
+
regex: /("[A-Za-z0-9_]*(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s*:\s*")([^"\\]{8,512})(")/gi,
|
|
820
|
+
anchor: JSON_CREDENTIAL_KEY_ANCHORS
|
|
821
|
+
},
|
|
780
822
|
// ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
|
|
781
823
|
// The plugin runtime carried 37 patterns while this scrubber — the one that
|
|
782
824
|
// guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
|
|
@@ -859,9 +901,12 @@ var PATTERNS = [
|
|
|
859
901
|
anchor: "GOCSPX-"
|
|
860
902
|
}
|
|
861
903
|
];
|
|
862
|
-
var SIMPLE_PATTERNS = PATTERNS.filter(
|
|
904
|
+
var SIMPLE_PATTERNS = PATTERNS.filter(
|
|
905
|
+
(p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
|
|
906
|
+
);
|
|
863
907
|
var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
|
|
864
908
|
var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
|
|
909
|
+
var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
|
|
865
910
|
var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
|
|
866
911
|
var SCRUB_CHUNK_BYTES = 64 * 1024;
|
|
867
912
|
var SCRUB_OVERLAP_BYTES = 1024;
|
|
@@ -872,20 +917,7 @@ var PATTERN_ANCHORS = [
|
|
|
872
917
|
)
|
|
873
918
|
)
|
|
874
919
|
];
|
|
875
|
-
var
|
|
876
|
-
'"apiKey"',
|
|
877
|
-
'"api_key"',
|
|
878
|
-
'"token"',
|
|
879
|
-
'"secret"',
|
|
880
|
-
'"password"',
|
|
881
|
-
'"authorization"',
|
|
882
|
-
'"bearer"',
|
|
883
|
-
'"private_key"',
|
|
884
|
-
'"access_token"',
|
|
885
|
-
'"refresh_token"',
|
|
886
|
-
'"client_secret"'
|
|
887
|
-
];
|
|
888
|
-
var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
|
|
920
|
+
var ALL_ANCHORS = PATTERN_ANCHORS;
|
|
889
921
|
function hasCredentialAnchors(text) {
|
|
890
922
|
for (const anchor of ALL_ANCHORS) {
|
|
891
923
|
if (text.includes(anchor)) return true;
|
|
@@ -934,6 +966,9 @@ var DefaultSecretScrubber = class {
|
|
|
934
966
|
out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
|
|
935
967
|
return `${lead}${key}=[REDACTED:high_entropy_env]`;
|
|
936
968
|
});
|
|
969
|
+
out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
|
|
970
|
+
return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
|
|
971
|
+
});
|
|
937
972
|
return out;
|
|
938
973
|
}
|
|
939
974
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export { Context, type ContextInit, type ProviderMemoryEvidence, type RunOptions
|
|
|
48
48
|
export { type ContinuationInput, type ContinuationSource, detectContinueIntent, type ResolvedContinuation, resolveContinuation, } from './core/continue-intent.js';
|
|
49
49
|
export { type ContinueDirective, makeContinueToNextIterationTool, parseContinueDirective, } from './core/continue-to-next-iteration.js';
|
|
50
50
|
export { ConversationState, type ReadonlyConversationState, type StateChange, type StateChangeHandler, wrapAsState, } from './core/conversation-state.js';
|
|
51
|
-
export { createFallbackModelExtension, effectiveFallbackChain, type FallbackModelDeps, fallbackProfileChain, formatModelRef, normalizeModelRef, parseModelRef, smartDefaultFallbackChain, } from './core/fallback-model.js';
|
|
51
|
+
export { createFallbackModelExtension, effectiveFallbackChain, type FallbackModelDeps, fallbackProfileChain, formatModelRef, normalizeModelRef, parseModelRef, runtimeFallbackChain, smartDefaultFallbackChain, } from './core/fallback-model.js';
|
|
52
52
|
export type { FallbackChain, FallbackChainEntry, ProviderHealth, } from './core/fallback-profile-manager.js';
|
|
53
53
|
export { FallbackProfileManager } from './core/fallback-profile-manager.js';
|
|
54
54
|
export { InputBuilder, type InputBuilderEvent, type InputBuilderOptions, } from './core/input-builder.js';
|