@mnemom/mnemom 0.17.6-next.1 → 0.17.6-next.3
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/README.md +8 -2
- package/dist/commands/agent-doctor.d.ts +21 -4
- package/dist/commands/agent-doctor.js +51 -5
- package/dist/commands/agent-goal.d.ts +1 -1
- package/dist/commands/agent-goal.js +6 -3
- package/dist/commands/agent-setup.d.ts +16 -5
- package/dist/commands/agent-setup.js +29 -12
- package/dist/commands/agent.d.ts +28 -6
- package/dist/commands/agent.js +86 -31
- package/dist/commands/wrap.d.ts +8 -0
- package/dist/commands/wrap.js +36 -3
- package/dist/index.js +1 -1
- package/dist/lib/agent.d.ts +44 -8
- package/dist/lib/agent.js +36 -7
- package/dist/lib/api.d.ts +27 -2
- package/dist/lib/api.js +64 -2
- package/dist/lib/first-run/backtest.d.ts +16 -33
- package/dist/lib/first-run/backtest.js +19 -54
- package/dist/lib/first-run/index.js +1 -1
- package/dist/lib/first-run/screen.d.ts +9 -9
- package/dist/lib/first-run/screen.js +31 -20
- package/dist/lib/launcher-config.d.ts +2 -1
- package/dist/lib/subscription-login.d.ts +152 -20
- package/dist/lib/subscription-login.js +215 -28
- package/dist/mcp/handoff-server.mjs +35 -13
- package/dist/rc-proxy.mjs +6 -3
- package/package.json +1 -1
package/dist/commands/wrap.js
CHANGED
|
@@ -20,7 +20,9 @@ import { mergeAgentConfig } from "../lib/agent-config.js";
|
|
|
20
20
|
import { askSelect, askInput, isInteractive } from "../lib/prompt.js";
|
|
21
21
|
import { getGatewayUrl } from "../lib/config.js";
|
|
22
22
|
import { fmt } from "../lib/format.js";
|
|
23
|
+
import { NOOP_SESSION_CONFIG_B64, SESSION_CONFIG_HEADER } from "../lib/agent.js";
|
|
23
24
|
const AGENT_HEADER = "x-mnemom-agent";
|
|
25
|
+
const MNEMOM_KEY_HEADER = "x-mnemom-api-key";
|
|
24
26
|
const PROVIDERS = {
|
|
25
27
|
anthropic: {
|
|
26
28
|
label: "Anthropic",
|
|
@@ -208,6 +210,10 @@ export async function birthThroughGateway(gatewayUrl, provider, agentName, provi
|
|
|
208
210
|
[prov.keyHeader]: authValue,
|
|
209
211
|
[AGENT_HEADER]: agentName,
|
|
210
212
|
};
|
|
213
|
+
if (opts.mnemomKey) {
|
|
214
|
+
headers[MNEMOM_KEY_HEADER] = opts.mnemomKey;
|
|
215
|
+
headers[SESSION_CONFIG_HEADER] = NOOP_SESSION_CONFIG_B64;
|
|
216
|
+
}
|
|
211
217
|
if (provider === "anthropic") {
|
|
212
218
|
headers["anthropic-version"] = "2023-06-01";
|
|
213
219
|
}
|
|
@@ -235,13 +241,24 @@ export async function birthThroughGateway(gatewayUrl, provider, agentName, provi
|
|
|
235
241
|
messages: [{ role: "user", content: "hello" }],
|
|
236
242
|
});
|
|
237
243
|
}
|
|
238
|
-
const
|
|
244
|
+
const post = (h) => fetch(url, {
|
|
239
245
|
method: "POST",
|
|
240
|
-
headers,
|
|
246
|
+
headers: h,
|
|
241
247
|
body,
|
|
242
248
|
signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
|
|
243
249
|
});
|
|
244
|
-
|
|
250
|
+
let response = await post(headers);
|
|
251
|
+
let agentId = response.headers.get(AGENT_HEADER);
|
|
252
|
+
// An agent an earlier plain call already birthed sits unclaimed in the sandbox org, and the
|
|
253
|
+
// gateway refuses agent traffic for it with 402 `agent_unclaimed` (and no agent id) — yet that
|
|
254
|
+
// is exactly the agent `mnemom agent setup` must claim. Ask once more as plain traffic (no
|
|
255
|
+
// session config): that serves it exactly as before and returns the id for the claim step.
|
|
256
|
+
if (!agentId && SESSION_CONFIG_HEADER in headers && (await isAgentUnclaimedRefusal(response))) {
|
|
257
|
+
const plain = { ...headers };
|
|
258
|
+
Reflect.deleteProperty(plain, SESSION_CONFIG_HEADER);
|
|
259
|
+
response = await post(plain);
|
|
260
|
+
agentId = response.headers.get(AGENT_HEADER);
|
|
261
|
+
}
|
|
245
262
|
if (!response.ok && !agentId) {
|
|
246
263
|
const text = await response.text().catch(() => "");
|
|
247
264
|
throw new MnemomApiError(response.status, `Gateway returned ${response.status}: ${text.slice(0, 300)}`);
|
|
@@ -251,6 +268,22 @@ export async function birthThroughGateway(gatewayUrl, provider, agentName, provi
|
|
|
251
268
|
}
|
|
252
269
|
return agentId;
|
|
253
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Is `response` the gateway's agent-traffic refusal for an agent still unclaimed in the sandbox
|
|
273
|
+
* org — 402 with `error.details.reason === "agent_unclaimed"`? Reads a clone, so the caller can
|
|
274
|
+
* still read the body. Anything unparseable is `false`.
|
|
275
|
+
*/
|
|
276
|
+
async function isAgentUnclaimedRefusal(response) {
|
|
277
|
+
if (response.status !== 402)
|
|
278
|
+
return false;
|
|
279
|
+
try {
|
|
280
|
+
const body = (await response.clone().json());
|
|
281
|
+
return body?.error?.details?.reason === "agent_unclaimed";
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
254
287
|
// ── Starter cards ─────────────────────────────────────────────────────────────
|
|
255
288
|
function buildStarterAlignmentCard(agentId) {
|
|
256
289
|
return {
|
package/dist/index.js
CHANGED
|
@@ -175,7 +175,7 @@ const agentCmd = program
|
|
|
175
175
|
.addOption(new Option("--slack", "EXPERIMENTAL (`mnemom experimental enable slack`): attach the Slack channel — messages addressed to this session arrive in it, and it can reply (see `mnemom agent slack`)").hideHelp(hiddenUnless("slack")))
|
|
176
176
|
.option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
|
|
177
177
|
.option("--no-context", "Disable context folding for this launch (folding is on by default; tells the gateway to skip it for this session)")
|
|
178
|
-
.addOption(new Option("--context-profile <profile>", 'How the gateway folds this session\'s context. "balanced" (default) keeps more of the earlier work verbatim and folds later — for long sessions that revisit earlier files and decisions. "aggressive" folds earlier and keeps a tighter working set — for cost-sensitive sessions and short, focused tasks. "off" leaves compaction to the coding agent. Also: config key context_profile, env MNEMOM_AGENT_CONTEXT_PROFILE.').argParser((v) => {
|
|
178
|
+
.addOption(new Option("--context-profile <profile>", 'How the gateway folds this session\'s context. "balanced" (default) keeps more of the earlier work verbatim and folds later — for long sessions that revisit earlier files and decisions. "aggressive" folds earlier and keeps a tighter working set — for cost-sensitive sessions and short, focused tasks. "adaptive" sizes the fold to the conversation: no fold below 200k tokens, aggressive from 200k to 1M, balanced above. "off" leaves compaction to the coding agent. Also: config key context_profile, env MNEMOM_AGENT_CONTEXT_PROFILE.').argParser((v) => {
|
|
179
179
|
// One validator for all three surfaces (flag, MNEMOM_AGENT_CONTEXT_PROFILE,
|
|
180
180
|
// launcher.toml context_profile): trimmed, case-insensitive, same error wording.
|
|
181
181
|
try {
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -293,6 +293,12 @@ export declare const CONTEXT_RUNTIME_KEY = "CONTEXT_RUNTIME_ENABLED";
|
|
|
293
293
|
export declare const CONTEXT_TAIL_BUDGET_KEY = "CONTEXT_RUNTIME_TAIL_BUDGET_TOKENS";
|
|
294
294
|
export declare const CONTEXT_READ_CACHE_KEY = "CONTEXT_RUNTIME_READ_CACHE_TOKENS";
|
|
295
295
|
export declare const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT_RECLAIM";
|
|
296
|
+
/**
|
|
297
|
+
* The gateway key the `adaptive` context profile sets (allow-listed per session): turns on the
|
|
298
|
+
* size-banded fold policy. Only the literal `banded` enables it; the band thresholds and budgets
|
|
299
|
+
* stay at the gateway's own defaults.
|
|
300
|
+
*/
|
|
301
|
+
export declare const CONTEXT_SIZE_POLICY_KEY = "CONTEXT_RUNTIME_SIZE_POLICY";
|
|
296
302
|
/**
|
|
297
303
|
* The named presets for how the gateway folds a session's context (`--context-profile`,
|
|
298
304
|
* launcher.toml `context_profile`, `MNEMOM_AGENT_CONTEXT_PROFILE`):
|
|
@@ -301,8 +307,11 @@ export declare const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT
|
|
|
301
307
|
* - `balanced` → NO fold keys: the gateway's own defaults stand. The default.
|
|
302
308
|
* - `aggressive` → a smaller tail budget + read cache and head-breakpoint reclaim on: folds
|
|
303
309
|
* earlier and keeps a tighter working set.
|
|
310
|
+
* - `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY: "banded"` — the gateway picks the fold by the
|
|
311
|
+
* conversation's original size: no fold below 200k tokens, the aggressive
|
|
312
|
+
* budgets from 200k to 1M, balanced above. Opt-in; never the default.
|
|
304
313
|
*/
|
|
305
|
-
export declare const CONTEXT_PROFILES: readonly ["off", "balanced", "aggressive"];
|
|
314
|
+
export declare const CONTEXT_PROFILES: readonly ["off", "balanced", "aggressive", "adaptive"];
|
|
306
315
|
export type ContextProfile = (typeof CONTEXT_PROFILES)[number];
|
|
307
316
|
/**
|
|
308
317
|
* The gateway env key that gates the whole context-handoff capability (level lines +
|
|
@@ -462,8 +471,10 @@ export declare function launchPreferenceExplicit(opts: {
|
|
|
462
471
|
* Has an orchestrator already run the session + µ-balance preflight for this
|
|
463
472
|
* launch? `MNEMOM_AGENT_PREFLIGHTED=1` is for orchestrators (the ADW fleet) that
|
|
464
473
|
* checked sign-in and balance themselves before spawning many launches; it skips
|
|
465
|
-
* the per-launch session
|
|
466
|
-
*
|
|
474
|
+
* the per-launch session prompt and balance round trips. The launch org still
|
|
475
|
+
* resolves as on a normal launch (active org, else personal org — the launch's
|
|
476
|
+
* Mnemom key is minted for it), and the Mnemom Agent entitlement (which also
|
|
477
|
+
* gates Remote Control, fail-closed) is still fetched for it.
|
|
467
478
|
* Only the exact value `1` counts — anything else, including `true` or ` 1`, is
|
|
468
479
|
* a normal launch.
|
|
469
480
|
*/
|
|
@@ -512,7 +523,11 @@ export declare function resolveRemoteControlShape(args: {
|
|
|
512
523
|
export declare function buildCustomHeaders(opts: {
|
|
513
524
|
/** Absent on a subscription-login launch: Claude Code then sends its own claude.ai login. */
|
|
514
525
|
anthropicKey?: string;
|
|
515
|
-
/**
|
|
526
|
+
/**
|
|
527
|
+
* The Mnemom key every `mnemom agent` launch sends (lib/subscription-login.ts
|
|
528
|
+
* resolveLaunchMnemomKey) — the gateway refuses the session without it, and on a subscription
|
|
529
|
+
* login it is also the governed-identity anchor.
|
|
530
|
+
*/
|
|
516
531
|
mnemomKey?: string;
|
|
517
532
|
agent: string;
|
|
518
533
|
conversationId: string;
|
|
@@ -543,7 +558,7 @@ export declare function parseContextProfile(raw: string, source: string): Contex
|
|
|
543
558
|
/**
|
|
544
559
|
* Which context profile this launch runs under. Precedence, highest wins:
|
|
545
560
|
* 1. `--no-context` — always `off`. Combined with an explicit `--context-profile
|
|
546
|
-
* balanced|aggressive` it is a CONFLICT and the launch refuses (never a silent override);
|
|
561
|
+
* balanced|aggressive|adaptive` it is a CONFLICT and the launch refuses (never a silent override);
|
|
547
562
|
* `--context-profile off` agrees with it.
|
|
548
563
|
* 2. an explicit `--context-profile <profile>` flag (`--context` alongside it is fine — the
|
|
549
564
|
* profile wins);
|
|
@@ -561,8 +576,8 @@ export declare function resolveContextProfile(opts: {
|
|
|
561
576
|
/**
|
|
562
577
|
* The decoded per-session env overlay for this launch — the `config` object of the
|
|
563
578
|
* `x-mnemom-session-config` payload, in wire order: the profile's keys first
|
|
564
|
-
* (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`;
|
|
565
|
-
* for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
|
|
579
|
+
* (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`;
|
|
580
|
+
* `CONTEXT_RUNTIME_SIZE_POLICY` for `adaptive`; nothing for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
|
|
566
581
|
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
|
|
567
582
|
* `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
|
|
568
583
|
* cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
|
|
@@ -597,7 +612,8 @@ export declare function buildSessionConfigPayload(opts: {
|
|
|
597
612
|
* nothing to override (the gateway's own defaults stand — `balanced` with everything else off,
|
|
598
613
|
* byte-identical to a launch before profiles existed). Independent overlays:
|
|
599
614
|
* - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
|
|
600
|
-
* `aggressive` → the three fold-tuning keys, `
|
|
615
|
+
* `aggressive` → the three fold-tuning keys, `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY:
|
|
616
|
+
* "banded"`, `balanced` → nothing;
|
|
601
617
|
* - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
|
|
602
618
|
* `"on"` (the `compaction_intercept` setting);
|
|
603
619
|
* - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
|
|
@@ -616,6 +632,14 @@ export declare function buildSessionConfigB64(opts: {
|
|
|
616
632
|
compactAt?: number | null;
|
|
617
633
|
experimental?: readonly string[];
|
|
618
634
|
}): string | undefined;
|
|
635
|
+
/**
|
|
636
|
+
* A no-op `x-mnemom-session-config` value: `{ v: 1, config: {} }` overrides nothing (the gateway
|
|
637
|
+
* applies an empty set, exactly its own defaults). buildSessionConfigB64 returns `undefined` for
|
|
638
|
+
* the defaults — no header at all; this is for a call that must be Mnemom agent traffic (the
|
|
639
|
+
* gateway's agent-traffic gate keys on the header's presence) without changing any setting: the
|
|
640
|
+
* launcher's birth probe (commands/wrap.ts birthThroughGateway).
|
|
641
|
+
*/
|
|
642
|
+
export declare const NOOP_SESSION_CONFIG_B64: string;
|
|
619
643
|
/**
|
|
620
644
|
* Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
|
|
621
645
|
* this launch? OFF by default (operator directive, curation 2026-09-25): an absent flag
|
|
@@ -651,6 +675,12 @@ export declare const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
|
|
|
651
675
|
* it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
|
|
652
676
|
*/
|
|
653
677
|
export declare const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
|
|
678
|
+
/**
|
|
679
|
+
* Beside `HANDOFF_RC_KEY_ENV_NAME`: the non-`ANTHROPIC_*` var `spawnRcCli` sets to the launch's
|
|
680
|
+
* Mnemom key when context handoff is on, so the handoff call carries the `x-mnemom-api-key` the
|
|
681
|
+
* gateway requires on every `mnemom agent` request.
|
|
682
|
+
*/
|
|
683
|
+
export declare const HANDOFF_RC_MNEMOM_KEY_ENV_NAME = "MNEMOM_HANDOFF_MNEMOM_KEY";
|
|
654
684
|
/**
|
|
655
685
|
* How the `handoff` MCP server (cli/src/mcp/handoff-server.mjs) recovers the Anthropic key
|
|
656
686
|
* WITHOUT it ever being written into the `--mcp-config` file on disk: `envName` is the NAME of a
|
|
@@ -682,6 +712,12 @@ export interface HandoffMcpParams {
|
|
|
682
712
|
/** The gateway ORIGIN (e.g. https://gateway.mnemom.ai) — see `resolveHandoffGatewayHost`. */
|
|
683
713
|
gatewayHost: string;
|
|
684
714
|
anthropicKeySource: HandoffAnthropicKeySource;
|
|
715
|
+
/**
|
|
716
|
+
* Where the server reads the Mnemom key (`x-mnemom-api-key`) from — same contract as
|
|
717
|
+
* `anthropicKeySource`: `ANTHROPIC_CUSTOM_HEADERS` (its `x-mnemom-api-key:` line) on a terminal
|
|
718
|
+
* launch, `HANDOFF_RC_MNEMOM_KEY_ENV_NAME` under Remote Control. Absent ⇒ no Mnemom key header.
|
|
719
|
+
*/
|
|
720
|
+
mnemomKeySource?: HandoffAnthropicKeySource;
|
|
685
721
|
agent: string;
|
|
686
722
|
conversationId: string;
|
|
687
723
|
/** `buildSessionConfigB64` output — forwarded so the route resolves the SAME per-session flag. */
|
package/dist/lib/agent.js
CHANGED
|
@@ -451,6 +451,12 @@ export const CONTEXT_RUNTIME_KEY = "CONTEXT_RUNTIME_ENABLED";
|
|
|
451
451
|
export const CONTEXT_TAIL_BUDGET_KEY = "CONTEXT_RUNTIME_TAIL_BUDGET_TOKENS";
|
|
452
452
|
export const CONTEXT_READ_CACHE_KEY = "CONTEXT_RUNTIME_READ_CACHE_TOKENS";
|
|
453
453
|
export const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT_RECLAIM";
|
|
454
|
+
/**
|
|
455
|
+
* The gateway key the `adaptive` context profile sets (allow-listed per session): turns on the
|
|
456
|
+
* size-banded fold policy. Only the literal `banded` enables it; the band thresholds and budgets
|
|
457
|
+
* stay at the gateway's own defaults.
|
|
458
|
+
*/
|
|
459
|
+
export const CONTEXT_SIZE_POLICY_KEY = "CONTEXT_RUNTIME_SIZE_POLICY";
|
|
454
460
|
/**
|
|
455
461
|
* The named presets for how the gateway folds a session's context (`--context-profile`,
|
|
456
462
|
* launcher.toml `context_profile`, `MNEMOM_AGENT_CONTEXT_PROFILE`):
|
|
@@ -459,8 +465,11 @@ export const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT_RECLAIM
|
|
|
459
465
|
* - `balanced` → NO fold keys: the gateway's own defaults stand. The default.
|
|
460
466
|
* - `aggressive` → a smaller tail budget + read cache and head-breakpoint reclaim on: folds
|
|
461
467
|
* earlier and keeps a tighter working set.
|
|
468
|
+
* - `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY: "banded"` — the gateway picks the fold by the
|
|
469
|
+
* conversation's original size: no fold below 200k tokens, the aggressive
|
|
470
|
+
* budgets from 200k to 1M, balanced above. Opt-in; never the default.
|
|
462
471
|
*/
|
|
463
|
-
export const CONTEXT_PROFILES = ["off", "balanced", "aggressive"];
|
|
472
|
+
export const CONTEXT_PROFILES = ["off", "balanced", "aggressive", "adaptive"];
|
|
464
473
|
/** The per-session env overlay each profile expands to (key order is the wire order). */
|
|
465
474
|
const CONTEXT_PROFILE_CONFIG = {
|
|
466
475
|
off: [[CONTEXT_RUNTIME_KEY, "false"]],
|
|
@@ -470,6 +479,7 @@ const CONTEXT_PROFILE_CONFIG = {
|
|
|
470
479
|
[CONTEXT_READ_CACHE_KEY, "8000"],
|
|
471
480
|
[CONTEXT_HEAD_RECLAIM_KEY, "true"],
|
|
472
481
|
],
|
|
482
|
+
adaptive: [[CONTEXT_SIZE_POLICY_KEY, "banded"]],
|
|
473
483
|
};
|
|
474
484
|
/**
|
|
475
485
|
* The gateway env key that gates the whole context-handoff capability (level lines +
|
|
@@ -798,8 +808,10 @@ export function launchPreferenceExplicit(opts, env = process.env) {
|
|
|
798
808
|
* Has an orchestrator already run the session + µ-balance preflight for this
|
|
799
809
|
* launch? `MNEMOM_AGENT_PREFLIGHTED=1` is for orchestrators (the ADW fleet) that
|
|
800
810
|
* checked sign-in and balance themselves before spawning many launches; it skips
|
|
801
|
-
* the per-launch session
|
|
802
|
-
*
|
|
811
|
+
* the per-launch session prompt and balance round trips. The launch org still
|
|
812
|
+
* resolves as on a normal launch (active org, else personal org — the launch's
|
|
813
|
+
* Mnemom key is minted for it), and the Mnemom Agent entitlement (which also
|
|
814
|
+
* gates Remote Control, fail-closed) is still fetched for it.
|
|
803
815
|
* Only the exact value `1` counts — anything else, including `true` or ` 1`, is
|
|
804
816
|
* a normal launch.
|
|
805
817
|
*/
|
|
@@ -902,7 +914,7 @@ export function parseContextProfile(raw, source) {
|
|
|
902
914
|
/**
|
|
903
915
|
* Which context profile this launch runs under. Precedence, highest wins:
|
|
904
916
|
* 1. `--no-context` — always `off`. Combined with an explicit `--context-profile
|
|
905
|
-
* balanced|aggressive` it is a CONFLICT and the launch refuses (never a silent override);
|
|
917
|
+
* balanced|aggressive|adaptive` it is a CONFLICT and the launch refuses (never a silent override);
|
|
906
918
|
* `--context-profile off` agrees with it.
|
|
907
919
|
* 2. an explicit `--context-profile <profile>` flag (`--context` alongside it is fine — the
|
|
908
920
|
* profile wins);
|
|
@@ -931,8 +943,8 @@ export function resolveContextProfile(opts) {
|
|
|
931
943
|
/**
|
|
932
944
|
* The decoded per-session env overlay for this launch — the `config` object of the
|
|
933
945
|
* `x-mnemom-session-config` payload, in wire order: the profile's keys first
|
|
934
|
-
* (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`;
|
|
935
|
-
* for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
|
|
946
|
+
* (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`;
|
|
947
|
+
* `CONTEXT_RUNTIME_SIZE_POLICY` for `adaptive`; nothing for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
|
|
936
948
|
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
|
|
937
949
|
* `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
|
|
938
950
|
* cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
|
|
@@ -977,7 +989,8 @@ export function buildSessionConfigPayload(opts) {
|
|
|
977
989
|
* nothing to override (the gateway's own defaults stand — `balanced` with everything else off,
|
|
978
990
|
* byte-identical to a launch before profiles existed). Independent overlays:
|
|
979
991
|
* - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
|
|
980
|
-
* `aggressive` → the three fold-tuning keys, `
|
|
992
|
+
* `aggressive` → the three fold-tuning keys, `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY:
|
|
993
|
+
* "banded"`, `balanced` → nothing;
|
|
981
994
|
* - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
|
|
982
995
|
* `"on"` (the `compaction_intercept` setting);
|
|
983
996
|
* - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
|
|
@@ -995,6 +1008,14 @@ export function buildSessionConfigB64(opts) {
|
|
|
995
1008
|
return undefined;
|
|
996
1009
|
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
997
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* A no-op `x-mnemom-session-config` value: `{ v: 1, config: {} }` overrides nothing (the gateway
|
|
1013
|
+
* applies an empty set, exactly its own defaults). buildSessionConfigB64 returns `undefined` for
|
|
1014
|
+
* the defaults — no header at all; this is for a call that must be Mnemom agent traffic (the
|
|
1015
|
+
* gateway's agent-traffic gate keys on the header's presence) without changing any setting: the
|
|
1016
|
+
* launcher's birth probe (commands/wrap.ts birthThroughGateway).
|
|
1017
|
+
*/
|
|
1018
|
+
export const NOOP_SESSION_CONFIG_B64 = Buffer.from(JSON.stringify({ v: 1, config: {} }), "utf8").toString("base64url");
|
|
998
1019
|
/**
|
|
999
1020
|
* Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
|
|
1000
1021
|
* this launch? OFF by default (operator directive, curation 2026-09-25): an absent flag
|
|
@@ -1038,6 +1059,12 @@ export const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
|
|
|
1038
1059
|
* it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
|
|
1039
1060
|
*/
|
|
1040
1061
|
export const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
|
|
1062
|
+
/**
|
|
1063
|
+
* Beside `HANDOFF_RC_KEY_ENV_NAME`: the non-`ANTHROPIC_*` var `spawnRcCli` sets to the launch's
|
|
1064
|
+
* Mnemom key when context handoff is on, so the handoff call carries the `x-mnemom-api-key` the
|
|
1065
|
+
* gateway requires on every `mnemom agent` request.
|
|
1066
|
+
*/
|
|
1067
|
+
export const HANDOFF_RC_MNEMOM_KEY_ENV_NAME = "MNEMOM_HANDOFF_MNEMOM_KEY";
|
|
1041
1068
|
/** The `--mcp-config` document registering exactly the `handoff` tool. Pure — no I/O. */
|
|
1042
1069
|
export function buildHandoffMcpConfig(p) {
|
|
1043
1070
|
const env = {
|
|
@@ -1046,6 +1073,8 @@ export function buildHandoffMcpConfig(p) {
|
|
|
1046
1073
|
MNEMOM_HANDOFF_AGENT: p.agent,
|
|
1047
1074
|
MNEMOM_HANDOFF_CONVERSATION_ID: p.conversationId,
|
|
1048
1075
|
};
|
|
1076
|
+
if (p.mnemomKeySource)
|
|
1077
|
+
env.MNEMOM_HANDOFF_MNEMOM_KEY_ENV_NAME = p.mnemomKeySource.envName;
|
|
1049
1078
|
if (p.sessionConfigB64)
|
|
1050
1079
|
env.MNEMOM_HANDOFF_SESSION_CONFIG = p.sessionConfigB64;
|
|
1051
1080
|
return {
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1069,6 +1069,27 @@ export declare function listApiKeys(): Promise<ApiKeyListItem[]>;
|
|
|
1069
1069
|
* for non-staff, admin:org for users not in any org-admin role).
|
|
1070
1070
|
*/
|
|
1071
1071
|
export declare function createApiKey(name: string, scopes: ApiKeyScope[]): Promise<ApiKeyCreated>;
|
|
1072
|
+
/**
|
|
1073
|
+
* POST /v1/orgs/{org_id}/api-keys — mint an ORG-owned key (its `org_id` is that org, so the
|
|
1074
|
+
* gateway births a new agent there). Owners and admins may mint any scopes; a member may mint a
|
|
1075
|
+
* key whose scopes are exactly ["gateway"]. A refusal (viewer/auditor, a non-member, or an API
|
|
1076
|
+
* that predates member org keys) is thrown as MnemomApiError with its status (403/404).
|
|
1077
|
+
*/
|
|
1078
|
+
export declare function createOrgApiKey(orgId: string, name: string, scopes: ApiKeyScope[]): Promise<ApiKeyCreated>;
|
|
1079
|
+
/**
|
|
1080
|
+
* GET /v1/api-keys/whoami authenticated by `key` ALONE (`X-Mnemom-Api-Key`, no session): does
|
|
1081
|
+
* the API accept this key? true = 200. false = 401/403 — revoked, rotated or unknown, OR a valid
|
|
1082
|
+
* `gateway`-only key: the API refuses every key without an `api:*` scope (ADR-049), so false
|
|
1083
|
+
* alone never proves a key dead. Throws on anything else (network, timeout, 429, 5xx): the
|
|
1084
|
+
* caller reads that as "could not check".
|
|
1085
|
+
*/
|
|
1086
|
+
export declare function apiAcceptsMnemomKey(key: string, timeoutMs: number): Promise<boolean>;
|
|
1087
|
+
/**
|
|
1088
|
+
* GET /v1/api-keys with a timeout, for checking a key: the signed-in caller's ACTIVE keys (a
|
|
1089
|
+
* revoked or rotated key is absent). null when there is no usable session (no credential, or
|
|
1090
|
+
* 401/403 after the refresh retry). Throws on anything else (network, timeout, 5xx).
|
|
1091
|
+
*/
|
|
1092
|
+
export declare function listOwnApiKeysForCheck(timeoutMs: number): Promise<ApiKeyListItem[] | null>;
|
|
1072
1093
|
/**
|
|
1073
1094
|
* POST /v1/api-keys/{key_id}/rotate — atomic mint-new + revoke-old.
|
|
1074
1095
|
* Returns the full new secret only on this call. The new key inherits
|
|
@@ -1078,9 +1099,13 @@ export declare function createApiKey(name: string, scopes: ApiKeyScope[]): Promi
|
|
|
1078
1099
|
export declare function rotateApiKey(keyId: string): Promise<ApiKeyCreated>;
|
|
1079
1100
|
/**
|
|
1080
1101
|
* DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
|
|
1081
|
-
* audit; `is_active` flips to false and `revoked_at` is timestamped.
|
|
1102
|
+
* audit; `is_active` flips to false and `revoked_at` is timestamped. Only
|
|
1103
|
+
* a key the signed-in user created (an org key they minted included) is
|
|
1104
|
+
* found; anything else is a 404. `timeoutMs` bounds the call.
|
|
1082
1105
|
*/
|
|
1083
|
-
export declare function revokeApiKey(keyId: string
|
|
1106
|
+
export declare function revokeApiKey(keyId: string, opts?: {
|
|
1107
|
+
timeoutMs?: number;
|
|
1108
|
+
}): Promise<void>;
|
|
1084
1109
|
export interface LicenseValidationResult {
|
|
1085
1110
|
valid: boolean;
|
|
1086
1111
|
license_id?: string;
|
package/dist/lib/api.js
CHANGED
|
@@ -1385,6 +1385,65 @@ export async function createApiKey(name, scopes) {
|
|
|
1385
1385
|
}
|
|
1386
1386
|
return (await response.json());
|
|
1387
1387
|
}
|
|
1388
|
+
/**
|
|
1389
|
+
* POST /v1/orgs/{org_id}/api-keys — mint an ORG-owned key (its `org_id` is that org, so the
|
|
1390
|
+
* gateway births a new agent there). Owners and admins may mint any scopes; a member may mint a
|
|
1391
|
+
* key whose scopes are exactly ["gateway"]. A refusal (viewer/auditor, a non-member, or an API
|
|
1392
|
+
* that predates member org keys) is thrown as MnemomApiError with its status (403/404).
|
|
1393
|
+
*/
|
|
1394
|
+
export async function createOrgApiKey(orgId, name, scopes) {
|
|
1395
|
+
const url = validateUrl(`${API_BASE}/v1/orgs/${encodeURIComponent(sanitizeForHttp(orgId))}/api-keys`);
|
|
1396
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1397
|
+
method: "POST",
|
|
1398
|
+
headers: { ...(await authHeaders()), "Content-Type": "application/json" },
|
|
1399
|
+
body: JSON.stringify({ name, scopes }),
|
|
1400
|
+
}));
|
|
1401
|
+
if (!response.ok) {
|
|
1402
|
+
if (response.status === 401)
|
|
1403
|
+
throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
|
|
1404
|
+
throw await readApiError(response, "Failed to create org api key");
|
|
1405
|
+
}
|
|
1406
|
+
return (await response.json());
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* GET /v1/api-keys/whoami authenticated by `key` ALONE (`X-Mnemom-Api-Key`, no session): does
|
|
1410
|
+
* the API accept this key? true = 200. false = 401/403 — revoked, rotated or unknown, OR a valid
|
|
1411
|
+
* `gateway`-only key: the API refuses every key without an `api:*` scope (ADR-049), so false
|
|
1412
|
+
* alone never proves a key dead. Throws on anything else (network, timeout, 429, 5xx): the
|
|
1413
|
+
* caller reads that as "could not check".
|
|
1414
|
+
*/
|
|
1415
|
+
export async function apiAcceptsMnemomKey(key, timeoutMs) {
|
|
1416
|
+
const url = validateUrl(`${API_BASE}/v1/api-keys/whoami`);
|
|
1417
|
+
const response = await mnemomFetch(url, {
|
|
1418
|
+
headers: { "X-Mnemom-Api-Key": sanitizeForHttp(key), Accept: "application/json" },
|
|
1419
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1420
|
+
});
|
|
1421
|
+
if (response.ok)
|
|
1422
|
+
return true;
|
|
1423
|
+
if (response.status === 401 || response.status === 403)
|
|
1424
|
+
return false;
|
|
1425
|
+
throw new MnemomApiError(response.status, `api-key whoami: HTTP ${response.status}`);
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* GET /v1/api-keys with a timeout, for checking a key: the signed-in caller's ACTIVE keys (a
|
|
1429
|
+
* revoked or rotated key is absent). null when there is no usable session (no credential, or
|
|
1430
|
+
* 401/403 after the refresh retry). Throws on anything else (network, timeout, 5xx).
|
|
1431
|
+
*/
|
|
1432
|
+
export async function listOwnApiKeysForCheck(timeoutMs) {
|
|
1433
|
+
if ((await resolveAuth()).type === "none")
|
|
1434
|
+
return null;
|
|
1435
|
+
const url = validateUrl(`${API_BASE}/v1/api-keys`);
|
|
1436
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1437
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
1438
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1439
|
+
}));
|
|
1440
|
+
if (response.status === 401 || response.status === 403)
|
|
1441
|
+
return null;
|
|
1442
|
+
if (!response.ok) {
|
|
1443
|
+
throw new MnemomApiError(response.status, `list api keys: HTTP ${response.status}`);
|
|
1444
|
+
}
|
|
1445
|
+
return (await response.json()).keys ?? [];
|
|
1446
|
+
}
|
|
1388
1447
|
/**
|
|
1389
1448
|
* POST /v1/api-keys/{key_id}/rotate — atomic mint-new + revoke-old.
|
|
1390
1449
|
* Returns the full new secret only on this call. The new key inherits
|
|
@@ -1410,13 +1469,16 @@ export async function rotateApiKey(keyId) {
|
|
|
1410
1469
|
}
|
|
1411
1470
|
/**
|
|
1412
1471
|
* DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
|
|
1413
|
-
* audit; `is_active` flips to false and `revoked_at` is timestamped.
|
|
1472
|
+
* audit; `is_active` flips to false and `revoked_at` is timestamped. Only
|
|
1473
|
+
* a key the signed-in user created (an org key they minted included) is
|
|
1474
|
+
* found; anything else is a 404. `timeoutMs` bounds the call.
|
|
1414
1475
|
*/
|
|
1415
|
-
export async function revokeApiKey(keyId) {
|
|
1476
|
+
export async function revokeApiKey(keyId, opts = {}) {
|
|
1416
1477
|
const url = validateUrl(`${API_BASE}/v1/api-keys/${encodeURIComponent(keyId)}`);
|
|
1417
1478
|
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1418
1479
|
method: "DELETE",
|
|
1419
1480
|
headers: await authHeaders(),
|
|
1481
|
+
signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
|
|
1420
1482
|
}));
|
|
1421
1483
|
if (!response.ok) {
|
|
1422
1484
|
if (response.status === 401) {
|
|
@@ -6,17 +6,14 @@
|
|
|
6
6
|
* Read-only; no network. Only token counts, model ids and timestamps are read from a
|
|
7
7
|
* transcript line — never message content.
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
* mnemom-adw scratchpad/
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 700k – 1M aggressive → the 400–700k bound, EXTRAPOLATED (no measured pair)
|
|
18
|
-
* ≥ 1M balanced → 0 (live sessions only; a stock history cannot get
|
|
19
|
-
* there because Claude Code compacts first — counted)
|
|
9
|
+
* The rate is ONE flat number (operator policy 2026-10-02): the median paired total-bill
|
|
10
|
+
* saving of the S46 chain45 C2n arm (context runtime, aggressive profile, no goal) against
|
|
11
|
+
* stock, 23.8% over n = 13 counted rounds (mnemom-adw scratchpad/trustcode-mvp/runs/
|
|
12
|
+
* s46-status.json; early exits and void arms excluded). It applies to every request whose
|
|
13
|
+
* original context is at or above the aggressive profile's fold point (its 60k tail
|
|
14
|
+
* budget); smaller requests never fold, so they save 0. A flat rate over-counts mid-size
|
|
15
|
+
* contexts and under-counts large ones; in aggregate that evens out. Per-size rates come
|
|
16
|
+
* back once there are enough measured pairs.
|
|
20
17
|
*/
|
|
21
18
|
/** One billed request, deduped by message id. `t` is epoch seconds. */
|
|
22
19
|
export interface Row {
|
|
@@ -59,28 +56,14 @@ export declare function parseUsageLine(line: Buffer, subagent: boolean): {
|
|
|
59
56
|
* session copies messages) is skipped, so each request is billed once, to its origin.
|
|
60
57
|
*/
|
|
61
58
|
export declare function loadTranscript(path: string, seen?: Set<string>): Row[];
|
|
62
|
-
/**
|
|
63
|
-
export declare const
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
* aggressive: no measured pair below 100k or at/above 700k.
|
|
67
|
-
*/
|
|
68
|
-
export declare const CURVE_LOWER: Readonly<Record<"aggressive" | "balanced", Record<number, number>>>;
|
|
69
|
-
export declare const NO_FOLD_BELOW = 200000;
|
|
70
|
-
export declare const EXTRAPOLATE_FROM = 700000;
|
|
71
|
-
export declare const BALANCED_FROM = 1000000;
|
|
72
|
-
export declare const POLICY = "none <200k / aggressive 200k-1M / balanced 1M+ (1M+ applies only to live sessions)";
|
|
59
|
+
/** S46 C2n vs stock, median paired total-bill saving (n = 13). */
|
|
60
|
+
export declare const S46_MEDIAN_SAVING = 0.238;
|
|
61
|
+
/** The aggressive profile's tail budget (cli/src/lib/agent.ts): below it nothing folds. */
|
|
62
|
+
export declare const AGGRESSIVE_FOLD_FROM = 60000;
|
|
73
63
|
export declare const ESTIMATOR_METHOD: string;
|
|
74
|
-
export type Strategy = "none" | "aggressive"
|
|
75
|
-
export type RateNote = "
|
|
76
|
-
/** The
|
|
77
|
-
export declare function binOf(x: number): number;
|
|
78
|
-
/** The screen never claims a negative saving: a negative measured rate shows as 0. */
|
|
79
|
-
export declare function floorRate(raw: number): {
|
|
80
|
-
screen: number;
|
|
81
|
-
note: "floored_at_0" | null;
|
|
82
|
-
};
|
|
83
|
-
/** The per-request rate the screen uses (`screen`) and the honest measured one (`raw`). */
|
|
64
|
+
export type Strategy = "none" | "aggressive";
|
|
65
|
+
export type RateNote = "below_aggressive" | null;
|
|
66
|
+
/** The per-request rate the screen uses. `raw` equals `screen`: no floor, no curve. */
|
|
84
67
|
export declare function ruleRate(x: number): {
|
|
85
68
|
strategy: Strategy;
|
|
86
69
|
raw: number;
|
|
@@ -90,7 +73,7 @@ export declare function ruleRate(x: number): {
|
|
|
90
73
|
export interface Estimate {
|
|
91
74
|
actual: number;
|
|
92
75
|
saved: number;
|
|
93
|
-
/**
|
|
76
|
+
/** Same as `saved` now that no rate is floored (kept for the audit record). */
|
|
94
77
|
rawSaved: number;
|
|
95
78
|
peak: number;
|
|
96
79
|
requests: number;
|
|
@@ -6,17 +6,14 @@
|
|
|
6
6
|
* Read-only; no network. Only token counts, model ids and timestamps are read from a
|
|
7
7
|
* transcript line — never message content.
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
* mnemom-adw scratchpad/
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 700k – 1M aggressive → the 400–700k bound, EXTRAPOLATED (no measured pair)
|
|
18
|
-
* ≥ 1M balanced → 0 (live sessions only; a stock history cannot get
|
|
19
|
-
* there because Claude Code compacts first — counted)
|
|
9
|
+
* The rate is ONE flat number (operator policy 2026-10-02): the median paired total-bill
|
|
10
|
+
* saving of the S46 chain45 C2n arm (context runtime, aggressive profile, no goal) against
|
|
11
|
+
* stock, 23.8% over n = 13 counted rounds (mnemom-adw scratchpad/trustcode-mvp/runs/
|
|
12
|
+
* s46-status.json; early exits and void arms excluded). It applies to every request whose
|
|
13
|
+
* original context is at or above the aggressive profile's fold point (its 60k tail
|
|
14
|
+
* budget); smaller requests never fold, so they save 0. A flat rate over-counts mid-size
|
|
15
|
+
* contexts and under-counts large ones; in aggregate that evens out. Per-size rates come
|
|
16
|
+
* back once there are enough measured pairs.
|
|
20
17
|
*/
|
|
21
18
|
import { forEachLine, isoSeconds, parseJson } from "./lines.js";
|
|
22
19
|
/**
|
|
@@ -137,51 +134,19 @@ export function loadTranscript(path, seen) {
|
|
|
137
134
|
}
|
|
138
135
|
return out;
|
|
139
136
|
}
|
|
140
|
-
// ── the
|
|
141
|
-
/**
|
|
142
|
-
export const
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// bin 1 (100-200k) is measured at -0.059 but never used: < 200k runs no fold.
|
|
149
|
-
aggressive: { 2: 0.137, 3: 0.282, 4: 0.342 },
|
|
150
|
-
balanced: { 0: -0.222, 1: -0.028, 2: 0.055, 3: 0.081, 4: 0.163, 5: 0.43 },
|
|
151
|
-
};
|
|
152
|
-
export const NO_FOLD_BELOW = 200_000;
|
|
153
|
-
export const EXTRAPOLATE_FROM = 700_000;
|
|
154
|
-
export const BALANCED_FROM = 1_000_000;
|
|
155
|
-
export const POLICY = "none <200k / aggressive 200k-1M / balanced 1M+ (1M+ applies only to live sessions)";
|
|
156
|
-
export const ESTIMATOR_METHOD = "empirical curve (lower 95% bound, rounded down), strategy per request by its original context, " +
|
|
157
|
-
`policy ${POLICY}: < 200k the balanced arm's measured rate floored at 0, 200k-1M aggressive by bin ` +
|
|
158
|
-
"(>= 700k: the 400-700k rate, extrapolated); request count and output held at stock";
|
|
159
|
-
/** The curve bin of an original context (0 = below 100k; the top bin is open-ended). */
|
|
160
|
-
export function binOf(x) {
|
|
161
|
-
let i = 0;
|
|
162
|
-
while (i < CURVE_EDGES.length - 2 && x >= CURVE_EDGES[i + 1])
|
|
163
|
-
i++;
|
|
164
|
-
return i;
|
|
165
|
-
}
|
|
166
|
-
/** The screen never claims a negative saving: a negative measured rate shows as 0. */
|
|
167
|
-
export function floorRate(raw) {
|
|
168
|
-
return raw < 0 ? { screen: 0, note: "floored_at_0" } : { screen: raw, note: null };
|
|
169
|
-
}
|
|
170
|
-
/** The per-request rate the screen uses (`screen`) and the honest measured one (`raw`). */
|
|
137
|
+
// ── the saving rate ──────────────────────────────────────────────────────────
|
|
138
|
+
/** S46 C2n vs stock, median paired total-bill saving (n = 13). */
|
|
139
|
+
export const S46_MEDIAN_SAVING = 0.238;
|
|
140
|
+
/** The aggressive profile's tail budget (cli/src/lib/agent.ts): below it nothing folds. */
|
|
141
|
+
export const AGGRESSIVE_FOLD_FROM = 60_000;
|
|
142
|
+
export const ESTIMATOR_METHOD = "flat S46 median (23.8%, C2n vs stock, n = 13) on every request whose original context is " +
|
|
143
|
+
">= 60k (the aggressive profile's fold point), 0 below; request count and output held at stock";
|
|
144
|
+
/** The per-request rate the screen uses. `raw` equals `screen`: no floor, no curve. */
|
|
171
145
|
export function ruleRate(x) {
|
|
172
|
-
if (x
|
|
173
|
-
return { strategy: "
|
|
174
|
-
}
|
|
175
|
-
if (x < NO_FOLD_BELOW) {
|
|
176
|
-
const raw = CURVE_LOWER.balanced[binOf(x)];
|
|
177
|
-
return { strategy: "none", raw, ...floorRate(raw) };
|
|
178
|
-
}
|
|
179
|
-
if (x >= EXTRAPOLATE_FROM) {
|
|
180
|
-
const r = CURVE_LOWER.aggressive[binOf(400_000)];
|
|
181
|
-
return { strategy: "aggressive", raw: r, screen: r, note: "extrapolated" };
|
|
146
|
+
if (x < AGGRESSIVE_FOLD_FROM) {
|
|
147
|
+
return { strategy: "none", raw: 0, screen: 0, note: "below_aggressive" };
|
|
182
148
|
}
|
|
183
|
-
|
|
184
|
-
return { strategy: "aggressive", raw: r, screen: r, note: null };
|
|
149
|
+
return { strategy: "aggressive", raw: S46_MEDIAN_SAVING, screen: S46_MEDIAN_SAVING, note: null };
|
|
185
150
|
}
|
|
186
151
|
/** The one swap point: rows → list-price actual and the conservative saving. */
|
|
187
152
|
export function estimate(rows) {
|
|
@@ -209,7 +209,7 @@ export const LEDGER_SIGNALS = new Set(["registry_cc_id", "registry_conv_id"]);
|
|
|
209
209
|
/** The screen's hard budget: a regression in buildScreen's line dropping must fail loudly. */
|
|
210
210
|
export function assertScreenBudget(lines) {
|
|
211
211
|
if (lines.length > MAX_LINES || lines.some((l) => l.length > WIDTH)) {
|
|
212
|
-
throw new Error(
|
|
212
|
+
throw new Error(`first-run screen exceeded its ${MAX_LINES} x ${WIDTH} budget`);
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
/** A transcript's mtime (ms), 0 when it vanished since discovery (it then sorts first). */
|