instar 1.3.729 → 1.3.731
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +254 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/CoherenceGate.d.ts +119 -0
- package/dist/core/CoherenceGate.d.ts.map +1 -1
- package/dist/core/CoherenceGate.js +342 -19
- package/dist/core/CoherenceGate.js.map +1 -1
- package/dist/core/CoherenceReviewer.d.ts +30 -0
- package/dist/core/CoherenceReviewer.d.ts.map +1 -1
- package/dist/core/CoherenceReviewer.js +14 -0
- package/dist/core/CoherenceReviewer.js.map +1 -1
- package/dist/core/FileClassifier.d.ts.map +1 -1
- package/dist/core/FileClassifier.js +7 -0
- package/dist/core/FileClassifier.js.map +1 -1
- package/dist/core/ModelSwapService.d.ts +13 -0
- package/dist/core/ModelSwapService.d.ts.map +1 -1
- package/dist/core/ModelSwapService.js +11 -0
- package/dist/core/ModelSwapService.js.map +1 -1
- package/dist/core/ModelTierEscalation.d.ts +10 -0
- package/dist/core/ModelTierEscalation.d.ts.map +1 -1
- package/dist/core/ModelTierEscalation.js +4 -0
- package/dist/core/ModelTierEscalation.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +43 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/ProactiveSwapMonitor.d.ts +74 -20
- package/dist/core/ProactiveSwapMonitor.d.ts.map +1 -1
- package/dist/core/ProactiveSwapMonitor.js +349 -28
- package/dist/core/ProactiveSwapMonitor.js.map +1 -1
- package/dist/core/QuotaAwareScheduler.d.ts +70 -3
- package/dist/core/QuotaAwareScheduler.d.ts.map +1 -1
- package/dist/core/QuotaAwareScheduler.js +100 -16
- package/dist/core/QuotaAwareScheduler.js.map +1 -1
- package/dist/core/ResponseReviewDecisionLog.d.ts +45 -0
- package/dist/core/ResponseReviewDecisionLog.d.ts.map +1 -0
- package/dist/core/ResponseReviewDecisionLog.js +85 -0
- package/dist/core/ResponseReviewDecisionLog.js.map +1 -0
- package/dist/core/SessionManager.d.ts +28 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +102 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SessionRefresh.d.ts +116 -1
- package/dist/core/SessionRefresh.d.ts.map +1 -1
- package/dist/core/SessionRefresh.js +280 -17
- package/dist/core/SessionRefresh.js.map +1 -1
- package/dist/core/SwapAntiThrash.d.ts +329 -0
- package/dist/core/SwapAntiThrash.d.ts.map +1 -0
- package/dist/core/SwapAntiThrash.js +1025 -0
- package/dist/core/SwapAntiThrash.js.map +1 -0
- package/dist/core/SwapLedger.d.ts +168 -0
- package/dist/core/SwapLedger.d.ts.map +1 -0
- package/dist/core/SwapLedger.js +275 -0
- package/dist/core/SwapLedger.js.map +1 -0
- package/dist/core/SwapWorkGate.d.ts +100 -0
- package/dist/core/SwapWorkGate.d.ts.map +1 -0
- package/dist/core/SwapWorkGate.js +175 -0
- package/dist/core/SwapWorkGate.js.map +1 -0
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +21 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/core/conversationContextWiring.d.ts +51 -0
- package/dist/core/conversationContextWiring.d.ts.map +1 -0
- package/dist/core/conversationContextWiring.js +78 -0
- package/dist/core/conversationContextWiring.js.map +1 -0
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +6 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/reviewers/conversational-tone.d.ts.map +1 -1
- package/dist/core/reviewers/conversational-tone.js +15 -1
- package/dist/core/reviewers/conversational-tone.js.map +1 -1
- package/dist/core/types.d.ts +102 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/untrustedConversationContext.d.ts +77 -0
- package/dist/core/untrustedConversationContext.d.ts.map +1 -0
- package/dist/core/untrustedConversationContext.js +134 -0
- package/dist/core/untrustedConversationContext.js.map +1 -0
- package/dist/monitoring/ReviewCanaryBattery.d.ts +160 -0
- package/dist/monitoring/ReviewCanaryBattery.d.ts.map +1 -0
- package/dist/monitoring/ReviewCanaryBattery.js +416 -0
- package/dist/monitoring/ReviewCanaryBattery.js.map +1 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +40 -1
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +6 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +4 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +20 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +5 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +72 -4
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/scaffold/templates/jobs/instar/review-canary-battery.md +31 -0
- package/src/scaffold/templates.ts +6 -0
- package/upgrades/1.3.730.md +112 -0
- package/upgrades/1.3.731.md +97 -0
- package/upgrades/side-effects/context-aware-outbound-review.md +62 -0
- package/upgrades/side-effects/swap-continuity-antithrash.md +118 -0
|
@@ -13,25 +13,32 @@
|
|
|
13
13
|
* This monitor moves a session OFF an account BEFORE it walls, at a lag-aware
|
|
14
14
|
* measured threshold below the real limit.
|
|
15
15
|
*
|
|
16
|
-
* ──
|
|
17
|
-
* The
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* ── Anti-thrash brakes (swap-continuity-antithrash §3) ──
|
|
17
|
+
* The 2026-07-02 thrash day (36 executed proactive swaps in 8 waves, repeated
|
|
18
|
+
* kills of six parallel build subagents) proved the bare threshold loop
|
|
19
|
+
* oscillates. When an SwapAntiThrashEngine is wired (`cfg.antiThrash`), every
|
|
20
|
+
* proactive decision runs the brake pipeline at THIS chokepoint:
|
|
21
|
+
* ledger-lost pause → thrash breaker → dwell → validity gate + all-hot →
|
|
22
|
+
* filter→score→verify target selection → reversal refusal. In dryRun the
|
|
23
|
+
* legacy decision path stays byte-identical to v1.3.722 while the engine logs
|
|
24
|
+
* would-refuse rows (the rung-2 soak); live, the engine's verdict binds and
|
|
25
|
+
* UNTAGGED sessions leave the proactive candidate set entirely (Q3 — a
|
|
26
|
+
* background optimizer must never mutate the default-slot binding).
|
|
22
27
|
*
|
|
23
|
-
* ──
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* is
|
|
28
|
+
* ── In-flight work deferral (swap-continuity-antithrash §4) ──
|
|
29
|
+
* When a SwapWorkGate is wired (`cfg.workGate`) and swapContinuity is live, a
|
|
30
|
+
* braked proactive swap whose session is BUSY (mid-turn or carrying live
|
|
31
|
+
* subagents) is DEFERRED — the intent is retried each tick through the FULL
|
|
32
|
+
* brake pipeline (I9), bounded by `deferralCeilingMs`; at the ceiling the
|
|
33
|
+
* intent is DROPPED (the wall wins; the reactive floor exists) and the
|
|
34
|
+
* session enters re-intent backoff. The monitor owns the deferral lifecycle
|
|
35
|
+
* (the gate is a stateless predicate).
|
|
29
36
|
*
|
|
30
37
|
* ── Bounded, non-storming ──
|
|
31
|
-
* Per evaluation:
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* enters
|
|
38
|
+
* Per evaluation: candidates are sorted newest-first and capped per cycle
|
|
39
|
+
* (executed swaps only — deferrals never consume the budget); at most one
|
|
40
|
+
* executed swap per target account per tick (pile-on cap); each swapped
|
|
41
|
+
* session enters dwell (ledger-backed, restart-safe). Near the wall the
|
|
35
42
|
* monitor triggers a fresh poll so a fast burn isn't missed between the
|
|
36
43
|
* low-frequency baseline polls.
|
|
37
44
|
*
|
|
@@ -40,6 +47,8 @@
|
|
|
40
47
|
* (injected deps) so it tests with zero sessions and zero network.
|
|
41
48
|
*/
|
|
42
49
|
import type { SubscriptionAccount } from './SubscriptionPool.js';
|
|
50
|
+
import type { SwapAntiThrashEngine, AntiThrashKnobs } from './SwapAntiThrash.js';
|
|
51
|
+
import type { WorkProbeResult } from './SwapWorkGate.js';
|
|
43
52
|
/** A running, swap-eligible session as the monitor sees it. */
|
|
44
53
|
export interface ProactiveSwapSession {
|
|
45
54
|
/** tmux session name (what the swap path keys on). */
|
|
@@ -54,6 +63,15 @@ export interface ProactiveSwapSession {
|
|
|
54
63
|
export interface ProactiveSwapOutcome {
|
|
55
64
|
swapped: boolean;
|
|
56
65
|
toAccountId: string | null;
|
|
66
|
+
reason?: string;
|
|
67
|
+
}
|
|
68
|
+
/** swapContinuity knobs as the monitor consumes them (resolved by the wiring). */
|
|
69
|
+
export interface SwapContinuityKnobs {
|
|
70
|
+
enabled: boolean;
|
|
71
|
+
dryRun: boolean;
|
|
72
|
+
deferralCeilingMs: number;
|
|
73
|
+
reactiveGraceMs: number;
|
|
74
|
+
recheckMs: number;
|
|
57
75
|
}
|
|
58
76
|
export interface ProactiveSwapMonitorConfig {
|
|
59
77
|
/** Current pool accounts (e.g. () => pool.list()). */
|
|
@@ -63,11 +81,16 @@ export interface ProactiveSwapMonitorConfig {
|
|
|
63
81
|
/** The pool account the DEFAULT config is logged into right now (or null).
|
|
64
82
|
* Untagged sessions run here; from InUseAccountResolver in production. */
|
|
65
83
|
resolveDefaultAccountId: () => Promise<string | null>;
|
|
66
|
-
/** Performs the actual swap (wraps QuotaAwareScheduler.onQuotaPressure).
|
|
84
|
+
/** Performs the actual swap (wraps QuotaAwareScheduler.onQuotaPressure).
|
|
85
|
+
* When the brakes are LIVE the monitor passes the authoritative
|
|
86
|
+
* `targetAccountId` through (§3.3 — the checked target IS the executed
|
|
87
|
+
* target, I1) plus `callerClass: 'proactive-swap'`. */
|
|
67
88
|
swap: (args: {
|
|
68
89
|
sessionName: string;
|
|
69
90
|
exhaustedAccountId: string;
|
|
70
91
|
nowMs: number;
|
|
92
|
+
targetAccountId?: string;
|
|
93
|
+
callerClass?: 'proactive-swap';
|
|
71
94
|
}) => Promise<ProactiveSwapOutcome>;
|
|
72
95
|
/** Optional fresh-poll trigger, awaited when an account is in the watch zone. */
|
|
73
96
|
triggerPoll?: () => Promise<unknown>;
|
|
@@ -80,10 +103,21 @@ export interface ProactiveSwapMonitorConfig {
|
|
|
80
103
|
/** Max sessions swapped per evaluation cycle (storm guard). Default 3. */
|
|
81
104
|
maxSwapsPerCycle?: number;
|
|
82
105
|
/** Per-session cooldown after a successful swap before it's eligible again.
|
|
83
|
-
* Default 600000 (10m) — must exceed the swap+restart time.
|
|
106
|
+
* Default 600000 (10m) — must exceed the swap+restart time. SUBSUMED by
|
|
107
|
+
* antiThrash dwell when the brakes are live (§9). */
|
|
84
108
|
cooldownMs?: number;
|
|
85
109
|
/** Monitor tick cadence. Default 180000 (3m). */
|
|
86
110
|
tickMs?: number;
|
|
111
|
+
/** Anti-thrash brakes (Piece 1). Knobs read LIVE per tick (§7.1). */
|
|
112
|
+
antiThrash?: {
|
|
113
|
+
engine: SwapAntiThrashEngine;
|
|
114
|
+
getKnobs: () => AntiThrashKnobs;
|
|
115
|
+
};
|
|
116
|
+
/** In-flight work deferral (Piece 2). Knobs read live per evaluation. */
|
|
117
|
+
workGate?: {
|
|
118
|
+
probe: (sessionName: string) => Promise<WorkProbeResult>;
|
|
119
|
+
getContinuity: () => SwapContinuityKnobs;
|
|
120
|
+
};
|
|
87
121
|
/** Injected for tests. */
|
|
88
122
|
now?: () => number;
|
|
89
123
|
logger?: {
|
|
@@ -108,15 +142,18 @@ export declare class ProactiveSwapMonitor {
|
|
|
108
142
|
private readonly tickMs;
|
|
109
143
|
private readonly now;
|
|
110
144
|
private readonly logger;
|
|
111
|
-
/** Last successful-swap timestamp per session (cooldown bookkeeping). */
|
|
145
|
+
/** Last successful-swap timestamp per session (legacy cooldown bookkeeping). */
|
|
112
146
|
private readonly lastSwapAt;
|
|
147
|
+
/** Pending deferred proactive intents (Piece 2 — monitor-owned lifecycle). */
|
|
148
|
+
private readonly deferrals;
|
|
113
149
|
private timer;
|
|
114
150
|
private ticking;
|
|
115
151
|
private lastResult;
|
|
116
152
|
constructor(cfg: ProactiveSwapMonitorConfig);
|
|
117
153
|
start(): void;
|
|
118
154
|
stop(): void;
|
|
119
|
-
/** Status for the read route (never throws).
|
|
155
|
+
/** Status for the read route (never throws). Additive `brakes`/`deferrals`
|
|
156
|
+
* blocks per swap-continuity-antithrash §6.3 (all fields LOCAL-SCOPE). */
|
|
120
157
|
status(): {
|
|
121
158
|
thresholdPct: number;
|
|
122
159
|
watchPct: number;
|
|
@@ -125,6 +162,15 @@ export declare class ProactiveSwapMonitor {
|
|
|
125
162
|
tickMs: number;
|
|
126
163
|
running: boolean;
|
|
127
164
|
lastResult: ProactiveSwapTickResult | null;
|
|
165
|
+
antiThrash?: {
|
|
166
|
+
enabled: boolean;
|
|
167
|
+
dryRun: boolean;
|
|
168
|
+
};
|
|
169
|
+
brakes?: Record<string, unknown>;
|
|
170
|
+
deferrals?: {
|
|
171
|
+
active: number;
|
|
172
|
+
sessions: string[];
|
|
173
|
+
};
|
|
128
174
|
};
|
|
129
175
|
/**
|
|
130
176
|
* One monitor pass: if any at-risk account is in the watch zone, refresh the
|
|
@@ -134,11 +180,19 @@ export declare class ProactiveSwapMonitor {
|
|
|
134
180
|
/**
|
|
135
181
|
* Pure-ish decision + swap on the CURRENT snapshots (no poll refresh). Exposed
|
|
136
182
|
* for tests and the on-demand check route.
|
|
183
|
+
*
|
|
184
|
+
* With the anti-thrash brakes LIVE the braked pipeline binds; in dryRun (or
|
|
185
|
+
* with no engine wired) the legacy v1.3.722 decision path is byte-identical
|
|
186
|
+
* while the engine (when present) logs would-decisions for the soak.
|
|
137
187
|
*/
|
|
138
188
|
evaluate(): Promise<{
|
|
139
189
|
swapped: string[];
|
|
140
190
|
considered: number;
|
|
141
191
|
}>;
|
|
192
|
+
private evaluateBraked;
|
|
193
|
+
private evaluateLegacy;
|
|
194
|
+
private safeKnobs;
|
|
195
|
+
private safeContinuity;
|
|
142
196
|
/**
|
|
143
197
|
* Map running sessions to candidates whose EFFECTIVE account is at/over minPct.
|
|
144
198
|
* Effective account = the session's tag, else the default-config login (so the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProactiveSwapMonitor.d.ts","sourceRoot":"","sources":["../../src/core/ProactiveSwapMonitor.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ProactiveSwapMonitor.d.ts","sourceRoot":"","sources":["../../src/core/ProactiveSwapMonitor.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,25 +13,32 @@
|
|
|
13
13
|
* This monitor moves a session OFF an account BEFORE it walls, at a lag-aware
|
|
14
14
|
* measured threshold below the real limit.
|
|
15
15
|
*
|
|
16
|
-
* ──
|
|
17
|
-
* The
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* ── Anti-thrash brakes (swap-continuity-antithrash §3) ──
|
|
17
|
+
* The 2026-07-02 thrash day (36 executed proactive swaps in 8 waves, repeated
|
|
18
|
+
* kills of six parallel build subagents) proved the bare threshold loop
|
|
19
|
+
* oscillates. When an SwapAntiThrashEngine is wired (`cfg.antiThrash`), every
|
|
20
|
+
* proactive decision runs the brake pipeline at THIS chokepoint:
|
|
21
|
+
* ledger-lost pause → thrash breaker → dwell → validity gate + all-hot →
|
|
22
|
+
* filter→score→verify target selection → reversal refusal. In dryRun the
|
|
23
|
+
* legacy decision path stays byte-identical to v1.3.722 while the engine logs
|
|
24
|
+
* would-refuse rows (the rung-2 soak); live, the engine's verdict binds and
|
|
25
|
+
* UNTAGGED sessions leave the proactive candidate set entirely (Q3 — a
|
|
26
|
+
* background optimizer must never mutate the default-slot binding).
|
|
22
27
|
*
|
|
23
|
-
* ──
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* is
|
|
28
|
+
* ── In-flight work deferral (swap-continuity-antithrash §4) ──
|
|
29
|
+
* When a SwapWorkGate is wired (`cfg.workGate`) and swapContinuity is live, a
|
|
30
|
+
* braked proactive swap whose session is BUSY (mid-turn or carrying live
|
|
31
|
+
* subagents) is DEFERRED — the intent is retried each tick through the FULL
|
|
32
|
+
* brake pipeline (I9), bounded by `deferralCeilingMs`; at the ceiling the
|
|
33
|
+
* intent is DROPPED (the wall wins; the reactive floor exists) and the
|
|
34
|
+
* session enters re-intent backoff. The monitor owns the deferral lifecycle
|
|
35
|
+
* (the gate is a stateless predicate).
|
|
29
36
|
*
|
|
30
37
|
* ── Bounded, non-storming ──
|
|
31
|
-
* Per evaluation:
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* enters
|
|
38
|
+
* Per evaluation: candidates are sorted newest-first and capped per cycle
|
|
39
|
+
* (executed swaps only — deferrals never consume the budget); at most one
|
|
40
|
+
* executed swap per target account per tick (pile-on cap); each swapped
|
|
41
|
+
* session enters dwell (ledger-backed, restart-safe). Near the wall the
|
|
35
42
|
* monitor triggers a fresh poll so a fast burn isn't missed between the
|
|
36
43
|
* low-frequency baseline polls.
|
|
37
44
|
*
|
|
@@ -49,8 +56,10 @@ export class ProactiveSwapMonitor {
|
|
|
49
56
|
tickMs;
|
|
50
57
|
now;
|
|
51
58
|
logger;
|
|
52
|
-
/** Last successful-swap timestamp per session (cooldown bookkeeping). */
|
|
59
|
+
/** Last successful-swap timestamp per session (legacy cooldown bookkeeping). */
|
|
53
60
|
lastSwapAt = new Map();
|
|
61
|
+
/** Pending deferred proactive intents (Piece 2 — monitor-owned lifecycle). */
|
|
62
|
+
deferrals = new Map();
|
|
54
63
|
timer = null;
|
|
55
64
|
ticking = false;
|
|
56
65
|
lastResult = null;
|
|
@@ -80,9 +89,10 @@ export class ProactiveSwapMonitor {
|
|
|
80
89
|
this.timer = null;
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
|
-
/** Status for the read route (never throws).
|
|
92
|
+
/** Status for the read route (never throws). Additive `brakes`/`deferrals`
|
|
93
|
+
* blocks per swap-continuity-antithrash §6.3 (all fields LOCAL-SCOPE). */
|
|
84
94
|
status() {
|
|
85
|
-
|
|
95
|
+
const base = {
|
|
86
96
|
thresholdPct: this.thresholdPct,
|
|
87
97
|
watchPct: Math.max(0, this.thresholdPct - this.watchMarginPct),
|
|
88
98
|
maxSwapsPerCycle: this.maxSwapsPerCycle,
|
|
@@ -91,6 +101,15 @@ export class ProactiveSwapMonitor {
|
|
|
91
101
|
running: this.timer !== null,
|
|
92
102
|
lastResult: this.lastResult,
|
|
93
103
|
};
|
|
104
|
+
if (!this.cfg.antiThrash)
|
|
105
|
+
return base;
|
|
106
|
+
const knobs = this.safeKnobs();
|
|
107
|
+
return {
|
|
108
|
+
...base,
|
|
109
|
+
...(knobs ? { antiThrash: { enabled: knobs.enabled, dryRun: knobs.dryRun } } : {}),
|
|
110
|
+
brakes: this.cfg.antiThrash.engine.status(this.now()),
|
|
111
|
+
deferrals: { active: this.deferrals.size, sessions: [...this.deferrals.keys()] },
|
|
112
|
+
};
|
|
94
113
|
}
|
|
95
114
|
/**
|
|
96
115
|
* One monitor pass: if any at-risk account is in the watch zone, refresh the
|
|
@@ -128,10 +147,260 @@ export class ProactiveSwapMonitor {
|
|
|
128
147
|
/**
|
|
129
148
|
* Pure-ish decision + swap on the CURRENT snapshots (no poll refresh). Exposed
|
|
130
149
|
* for tests and the on-demand check route.
|
|
150
|
+
*
|
|
151
|
+
* With the anti-thrash brakes LIVE the braked pipeline binds; in dryRun (or
|
|
152
|
+
* with no engine wired) the legacy v1.3.722 decision path is byte-identical
|
|
153
|
+
* while the engine (when present) logs would-decisions for the soak.
|
|
131
154
|
*/
|
|
132
155
|
async evaluate() {
|
|
133
156
|
const nowMs = this.now();
|
|
134
157
|
const accounts = this.cfg.listAccounts();
|
|
158
|
+
const knobs = this.safeKnobs();
|
|
159
|
+
const engine = this.cfg.antiThrash?.engine ?? null;
|
|
160
|
+
const engineActive = !!(engine && knobs?.enabled);
|
|
161
|
+
const live = !!(engineActive && knobs && !knobs.dryRun);
|
|
162
|
+
if (engine && knobs?.enabled)
|
|
163
|
+
engine.beginTick(accounts, nowMs, true);
|
|
164
|
+
try {
|
|
165
|
+
if (live && engine && knobs) {
|
|
166
|
+
return await this.evaluateBraked(engine, accounts, nowMs);
|
|
167
|
+
}
|
|
168
|
+
return await this.evaluateLegacy(accounts, nowMs, engineActive ? engine : null);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
if (engine && knobs?.enabled)
|
|
172
|
+
engine.endTick(nowMs);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// ── The LIVE braked pipeline (§3 + §4 proactive arm) ─────────────────────
|
|
176
|
+
async evaluateBraked(engine, accounts, nowMs) {
|
|
177
|
+
const byId = new Map(accounts.map((a) => [a.id, a]));
|
|
178
|
+
// Candidate set: TAGGED sessions only (Q3 — untagged sessions are outside
|
|
179
|
+
// the proactive candidate set by construction, I10), whose source account
|
|
180
|
+
// carries a VALID fresh reading at/over the threshold (§3.3 source leg).
|
|
181
|
+
const candidates = [];
|
|
182
|
+
const currentAccountBySession = new Map();
|
|
183
|
+
for (const s of this.cfg.listRunningSessions()) {
|
|
184
|
+
currentAccountBySession.set(s.sessionName, s.accountId);
|
|
185
|
+
if (!s.accountId)
|
|
186
|
+
continue;
|
|
187
|
+
const acct = byId.get(s.accountId);
|
|
188
|
+
if (!acct)
|
|
189
|
+
continue;
|
|
190
|
+
if (!engine.sourceEligible(acct, nowMs))
|
|
191
|
+
continue;
|
|
192
|
+
const startedMs = s.startedAt ? Date.parse(s.startedAt) : NaN;
|
|
193
|
+
candidates.push({
|
|
194
|
+
sessionName: s.sessionName,
|
|
195
|
+
accountId: s.accountId,
|
|
196
|
+
startedMs: Number.isFinite(startedMs) ? startedMs : 0,
|
|
197
|
+
untagged: false,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
candidates.sort((a, b) => b.startedMs - a.startedMs);
|
|
201
|
+
// Deferral invalidation sweep (I9): an intent whose session's account
|
|
202
|
+
// changed underneath it (a reactive swap moved it), or whose session left
|
|
203
|
+
// candidacy (wave subsided / session gone), is INVALIDATED — never
|
|
204
|
+
// executed as a second kill inside the dwell window.
|
|
205
|
+
for (const [session, d] of [...this.deferrals]) {
|
|
206
|
+
const stillCandidate = candidates.some((c) => c.sessionName === session);
|
|
207
|
+
const currentAcct = currentAccountBySession.get(session);
|
|
208
|
+
if (!stillCandidate || currentAcct !== d.from) {
|
|
209
|
+
engine.recordInvalidated({
|
|
210
|
+
session,
|
|
211
|
+
from: d.from,
|
|
212
|
+
to: d.to,
|
|
213
|
+
nowMs,
|
|
214
|
+
deferralAgeMs: nowMs - d.firstAtMs,
|
|
215
|
+
deferCount: d.count,
|
|
216
|
+
});
|
|
217
|
+
this.deferrals.delete(session);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const targetsUsedThisTick = new Set();
|
|
221
|
+
const swapped = [];
|
|
222
|
+
let considered = 0;
|
|
223
|
+
const continuity = this.safeContinuity();
|
|
224
|
+
for (const c of candidates) {
|
|
225
|
+
if (swapped.length >= this.maxSwapsPerCycle)
|
|
226
|
+
break;
|
|
227
|
+
considered += 1;
|
|
228
|
+
const deferral = this.deferrals.get(c.sessionName);
|
|
229
|
+
const deferralAgeMs = deferral ? nowMs - deferral.firstAtMs : undefined;
|
|
230
|
+
// Full brake pipeline — re-run on every deferred retry too (I9: the
|
|
231
|
+
// intent that finally fires is one that would have been approved fresh).
|
|
232
|
+
const verdict = engine.evaluateIntent({
|
|
233
|
+
session: c.sessionName,
|
|
234
|
+
fromAccountId: c.accountId,
|
|
235
|
+
accounts,
|
|
236
|
+
nowMs,
|
|
237
|
+
targetsUsedThisTick,
|
|
238
|
+
...(deferralAgeMs !== undefined ? { deferralAgeMs } : {}),
|
|
239
|
+
...(deferral ? { deferCount: deferral.count } : {}),
|
|
240
|
+
});
|
|
241
|
+
if (verdict.action !== 'execute')
|
|
242
|
+
continue; // refusal rows already written by the engine
|
|
243
|
+
// Piece 2: the work gate (proactive arm — defer, ceiling-drop).
|
|
244
|
+
if (this.cfg.workGate && continuity?.enabled) {
|
|
245
|
+
let probe = null;
|
|
246
|
+
try {
|
|
247
|
+
probe = await this.cfg.workGate.probe(c.sessionName);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// @silent-fallback-ok: probe machinery itself failed → indeterminate,
|
|
251
|
+
// which resolves BUSY (I7) — the swap is DEFERRED, the safe direction;
|
|
252
|
+
// the deferral is recorded on the anti-thrash engine below.
|
|
253
|
+
probe = null;
|
|
254
|
+
}
|
|
255
|
+
const busy = probe ? probe.busy : true;
|
|
256
|
+
if (busy) {
|
|
257
|
+
const reason = probe?.reason ?? 'busy-indeterminate';
|
|
258
|
+
const inFlight = { turn: probe?.turnInFlight ?? false, subagents: probe?.subagents?.length ?? 0 };
|
|
259
|
+
const subagentLeg = probe?.subagentLeg ?? 'indeterminate';
|
|
260
|
+
if (continuity.dryRun) {
|
|
261
|
+
// Would-defer (rung-2 soak): log the row, change nothing.
|
|
262
|
+
engine.recordDeferred({
|
|
263
|
+
session: c.sessionName,
|
|
264
|
+
from: c.accountId,
|
|
265
|
+
to: verdict.targetAccountId,
|
|
266
|
+
nowMs,
|
|
267
|
+
reason,
|
|
268
|
+
inFlight,
|
|
269
|
+
subagentLeg,
|
|
270
|
+
deferralAgeMs: 0,
|
|
271
|
+
deferCount: 1,
|
|
272
|
+
dryRun: true,
|
|
273
|
+
rowKind: 'first',
|
|
274
|
+
});
|
|
275
|
+
// fall through to execute (dryRun changes nothing)
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
const d = deferral ?? {
|
|
279
|
+
firstAtMs: nowMs,
|
|
280
|
+
count: 0,
|
|
281
|
+
from: c.accountId,
|
|
282
|
+
to: verdict.targetAccountId,
|
|
283
|
+
};
|
|
284
|
+
d.count += 1;
|
|
285
|
+
d.to = verdict.targetAccountId; // ceiling clock carries across target re-selection (§4.2)
|
|
286
|
+
this.deferrals.set(c.sessionName, d);
|
|
287
|
+
const age = nowMs - d.firstAtMs;
|
|
288
|
+
if (age >= continuity.deferralCeilingMs) {
|
|
289
|
+
// At the ceiling: the wall wins — DROP the intent; the session
|
|
290
|
+
// keeps working and the reactive floor absorbs a genuine wall.
|
|
291
|
+
engine.recordDropped({
|
|
292
|
+
session: c.sessionName,
|
|
293
|
+
from: d.from,
|
|
294
|
+
to: d.to,
|
|
295
|
+
nowMs,
|
|
296
|
+
deferralAgeMs: age,
|
|
297
|
+
deferCount: d.count,
|
|
298
|
+
inFlight,
|
|
299
|
+
subagentLeg,
|
|
300
|
+
});
|
|
301
|
+
this.deferrals.delete(c.sessionName);
|
|
302
|
+
}
|
|
303
|
+
else if (d.count === 1) {
|
|
304
|
+
// Dedup (§4.2): FIRST row only; the final row is the eventual
|
|
305
|
+
// swapped/dropped/invalidated row carrying deferCount.
|
|
306
|
+
engine.recordDeferred({
|
|
307
|
+
session: c.sessionName,
|
|
308
|
+
from: d.from,
|
|
309
|
+
to: d.to,
|
|
310
|
+
nowMs,
|
|
311
|
+
reason,
|
|
312
|
+
inFlight,
|
|
313
|
+
subagentLeg,
|
|
314
|
+
deferralAgeMs: age,
|
|
315
|
+
deferCount: d.count,
|
|
316
|
+
rowKind: 'first',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Execute — the checked target IS the executed target (I1); the
|
|
324
|
+
// scheduler revalidates the WHOLE decision at execute time (§3.3).
|
|
325
|
+
try {
|
|
326
|
+
const outcome = await this.cfg.swap({
|
|
327
|
+
sessionName: c.sessionName,
|
|
328
|
+
exhaustedAccountId: c.accountId,
|
|
329
|
+
nowMs,
|
|
330
|
+
targetAccountId: verdict.targetAccountId,
|
|
331
|
+
callerClass: 'proactive-swap',
|
|
332
|
+
});
|
|
333
|
+
if (outcome.swapped) {
|
|
334
|
+
engine.recordProactiveExecuted({
|
|
335
|
+
session: c.sessionName,
|
|
336
|
+
from: c.accountId,
|
|
337
|
+
to: outcome.toAccountId ?? verdict.targetAccountId,
|
|
338
|
+
nowMs,
|
|
339
|
+
fromUtilPct: verdict.fromUtilPct,
|
|
340
|
+
toUtilPct: verdict.toUtilPct,
|
|
341
|
+
...(deferral ? { deferralAgeMs: nowMs - deferral.firstAtMs, deferCount: deferral.count } : {}),
|
|
342
|
+
});
|
|
343
|
+
this.lastSwapAt.set(c.sessionName, nowMs);
|
|
344
|
+
this.deferrals.delete(c.sessionName);
|
|
345
|
+
targetsUsedThisTick.add(verdict.targetAccountId);
|
|
346
|
+
swapped.push(c.sessionName);
|
|
347
|
+
this.logger.log(`[ProactiveSwap] ${c.sessionName}: pre-emptively swapped off ${c.accountId} → ${outcome.toAccountId ?? verdict.targetAccountId} ` +
|
|
348
|
+
`(account ≥${this.thresholdPct}% measured — moved before the wall, conversation preserved)`);
|
|
349
|
+
}
|
|
350
|
+
else if (outcome.reason === 'target-revalidation-failed') {
|
|
351
|
+
engine.recordRevalidationRefusal({
|
|
352
|
+
session: c.sessionName,
|
|
353
|
+
from: c.accountId,
|
|
354
|
+
to: verdict.targetAccountId,
|
|
355
|
+
nowMs,
|
|
356
|
+
reason: 'target-revalidation-failed',
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else if (outcome.reason === 'intent-stale') {
|
|
360
|
+
engine.recordRevalidationRefusal({
|
|
361
|
+
session: c.sessionName,
|
|
362
|
+
from: c.accountId,
|
|
363
|
+
to: verdict.targetAccountId,
|
|
364
|
+
nowMs,
|
|
365
|
+
reason: 'intent-stale',
|
|
366
|
+
});
|
|
367
|
+
this.deferrals.delete(c.sessionName);
|
|
368
|
+
}
|
|
369
|
+
else if (outcome.reason === 'session-busy') {
|
|
370
|
+
// Funnel-gate race: the SessionRefresh work gate saw busy after our
|
|
371
|
+
// probe. Treat exactly like a busy probe (deferral bookkeeping).
|
|
372
|
+
const d = deferral ?? { firstAtMs: nowMs, count: 0, from: c.accountId, to: verdict.targetAccountId };
|
|
373
|
+
d.count += 1;
|
|
374
|
+
this.deferrals.set(c.sessionName, d);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
engine.recordExecFailure({
|
|
378
|
+
session: c.sessionName,
|
|
379
|
+
from: c.accountId,
|
|
380
|
+
to: verdict.targetAccountId,
|
|
381
|
+
kind: 'proactive',
|
|
382
|
+
errorClass: outcome.reason ?? 'refresh-failed',
|
|
383
|
+
nowMs,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
// §3.6: a swap execution that THROWS is a failed row + backoff — never
|
|
389
|
+
// a silent every-tick retry.
|
|
390
|
+
engine.recordExecFailure({
|
|
391
|
+
session: c.sessionName,
|
|
392
|
+
from: c.accountId,
|
|
393
|
+
to: verdict.targetAccountId,
|
|
394
|
+
kind: 'proactive',
|
|
395
|
+
errorClass: err instanceof Error ? err.constructor.name : 'Error',
|
|
396
|
+
nowMs,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { swapped, considered };
|
|
401
|
+
}
|
|
402
|
+
// ── The legacy v1.3.722 decision path (dark / dryRun — byte-identical) ───
|
|
403
|
+
async evaluateLegacy(accounts, nowMs, shadowEngine) {
|
|
135
404
|
const atPressure = await this.mapCandidates(this.thresholdPct);
|
|
136
405
|
const eligible = atPressure.filter((c) => {
|
|
137
406
|
const last = this.lastSwapAt.get(c.sessionName);
|
|
@@ -146,13 +415,28 @@ export class ProactiveSwapMonitor {
|
|
|
146
415
|
// (it just restarted on compaction/recovery) ranks ahead of idle background
|
|
147
416
|
// sessions, so under the per-cycle cap it is rescued first.
|
|
148
417
|
eligible.sort((a, b) => b.startedMs - a.startedMs);
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
418
|
+
// Rung-2 dry-run shadow (§10): the engine evaluates the LIVE rule's
|
|
419
|
+
// candidate set (tagged only) and writes would-refuse/would-defer rows —
|
|
420
|
+
// observability with ZERO decision change.
|
|
421
|
+
if (shadowEngine) {
|
|
422
|
+
const shadowTargets = new Set();
|
|
423
|
+
for (const c of atPressure) {
|
|
424
|
+
if (c.untagged)
|
|
425
|
+
continue; // Q3 — untagged is outside the live candidate set
|
|
426
|
+
try {
|
|
427
|
+
shadowEngine.evaluateIntent({
|
|
428
|
+
session: c.sessionName,
|
|
429
|
+
fromAccountId: c.accountId,
|
|
430
|
+
accounts,
|
|
431
|
+
nowMs,
|
|
432
|
+
targetsUsedThisTick: shadowTargets,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// @silent-fallback-ok: the shadow must never affect the legacy path
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
156
440
|
const toSwap = eligible.slice(0, this.maxSwapsPerCycle);
|
|
157
441
|
const swapped = [];
|
|
158
442
|
for (const c of toSwap) {
|
|
@@ -162,21 +446,57 @@ export class ProactiveSwapMonitor {
|
|
|
162
446
|
sessionName: c.sessionName,
|
|
163
447
|
exhaustedAccountId: c.accountId,
|
|
164
448
|
nowMs,
|
|
449
|
+
callerClass: 'proactive-swap',
|
|
165
450
|
});
|
|
166
451
|
}
|
|
167
|
-
catch {
|
|
168
|
-
//
|
|
452
|
+
catch (err) {
|
|
453
|
+
// Legacy behavior: retried next cycle (no cooldown set). With the
|
|
454
|
+
// engine present the failure is at least RECORDED (§3.6 observability;
|
|
455
|
+
// decision behavior unchanged in dryRun — no backoff binds here).
|
|
456
|
+
shadowEngine?.recordExecFailure({
|
|
457
|
+
session: c.sessionName,
|
|
458
|
+
from: c.accountId,
|
|
459
|
+
kind: 'proactive',
|
|
460
|
+
errorClass: err instanceof Error ? err.constructor.name : 'Error',
|
|
461
|
+
nowMs,
|
|
462
|
+
});
|
|
169
463
|
continue;
|
|
170
464
|
}
|
|
171
465
|
if (outcome.swapped) {
|
|
172
466
|
this.lastSwapAt.set(c.sessionName, nowMs);
|
|
173
467
|
swapped.push(c.sessionName);
|
|
468
|
+
shadowEngine?.recordProactiveExecuted({
|
|
469
|
+
session: c.sessionName,
|
|
470
|
+
from: c.accountId,
|
|
471
|
+
to: outcome.toAccountId ?? '',
|
|
472
|
+
nowMs,
|
|
473
|
+
...(c.untagged ? { defaultAccountChanged: true } : {}),
|
|
474
|
+
});
|
|
174
475
|
this.logger.log(`[ProactiveSwap] ${c.sessionName}: pre-emptively swapped off ${c.accountId} → ${outcome.toAccountId} ` +
|
|
175
476
|
`(account ≥${this.thresholdPct}% measured — moved before the wall, conversation preserved)`);
|
|
176
477
|
}
|
|
177
478
|
}
|
|
178
479
|
return { swapped, considered: eligible.length };
|
|
179
480
|
}
|
|
481
|
+
safeKnobs() {
|
|
482
|
+
try {
|
|
483
|
+
return this.cfg.antiThrash?.getKnobs() ?? null;
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// @silent-fallback-ok: a broken knob getter reads as feature-dark (null)
|
|
487
|
+
// — the brakes simply stay out of the path; legacy swap behavior holds.
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
safeContinuity() {
|
|
492
|
+
try {
|
|
493
|
+
return this.cfg.workGate?.getContinuity() ?? null;
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// @silent-fallback-ok: same feature-dark degrade as safeKnobs above.
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
180
500
|
/**
|
|
181
501
|
* Map running sessions to candidates whose EFFECTIVE account is at/over minPct.
|
|
182
502
|
* Effective account = the session's tag, else the default-config login (so the
|
|
@@ -207,6 +527,7 @@ export class ProactiveSwapMonitor {
|
|
|
207
527
|
sessionName: s.sessionName,
|
|
208
528
|
accountId: eff,
|
|
209
529
|
startedMs: Number.isFinite(startedMs) ? startedMs : 0,
|
|
530
|
+
untagged: s.accountId === null,
|
|
210
531
|
});
|
|
211
532
|
}
|
|
212
533
|
return out;
|