agent-nuvira 3.3.4 → 3.3.5
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/agent-sdk/src/scaffold.d.ts +1 -1
- package/dist/agent-sdk/src/scaffold.js +1 -1
- package/dist/config/process-env.d.ts +136 -0
- package/dist/config/process-env.d.ts.map +1 -0
- package/dist/config/process-env.js +217 -0
- package/dist/config/process-env.js.map +1 -0
- package/dist/learning/model-reachability.d.ts +95 -0
- package/dist/learning/model-reachability.d.ts.map +1 -0
- package/dist/learning/model-reachability.js +111 -0
- package/dist/learning/model-reachability.js.map +1 -0
- package/dist/learning/model-registry.d.ts +22 -0
- package/dist/learning/model-registry.d.ts.map +1 -1
- package/dist/learning/model-registry.js +24 -0
- package/dist/learning/model-registry.js.map +1 -1
- package/dist/learning/model-verify-job.d.ts +131 -0
- package/dist/learning/model-verify-job.d.ts.map +1 -0
- package/dist/learning/model-verify-job.js +221 -0
- package/dist/learning/model-verify-job.js.map +1 -0
- package/dist/web-dashboard/process-env-inventory.d.ts +57 -0
- package/dist/web-dashboard/process-env-inventory.d.ts.map +1 -0
- package/dist/web-dashboard/process-env-inventory.js +96 -0
- package/dist/web-dashboard/process-env-inventory.js.map +1 -0
- package/dist/web-dashboard/server.d.ts.map +1 -1
- package/dist/web-dashboard/server.js +246 -17
- package/dist/web-dashboard/server.js.map +1 -1
- package/dist/web-dashboard/src/types.d.ts +65 -0
- package/dist/web-dashboard/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/web-dashboard/public/assets/{index-XJjj2cBX.css → index-CIJ6FHHZ.css} +1 -1
- package/src/web-dashboard/public/assets/{index-YWF9FpwQ.js → index-CsySzR41.js} +82 -82
- package/src/web-dashboard/public/assets/index-CsySzR41.js.map +1 -0
- package/src/web-dashboard/public/index.html +2 -2
- package/src/web-dashboard/public/assets/index-YWF9FpwQ.js.map +0 -1
|
@@ -34,7 +34,7 @@ export interface ScaffoldOptions {
|
|
|
34
34
|
* a custom agent fail to load. A test asserts the two agree, so a release that
|
|
35
35
|
* forgets this constant fails rather than shipping.
|
|
36
36
|
*/
|
|
37
|
-
export declare const SDK_VERSION = "3.3.
|
|
37
|
+
export declare const SDK_VERSION = "3.3.5";
|
|
38
38
|
/**
|
|
39
39
|
* Scaffold a new custom agent project.
|
|
40
40
|
*
|
|
@@ -34,7 +34,7 @@ function toCamelCase(pascal) {
|
|
|
34
34
|
* a custom agent fail to load. A test asserts the two agree, so a release that
|
|
35
35
|
* forgets this constant fails rather than shipping.
|
|
36
36
|
*/
|
|
37
|
-
export const SDK_VERSION = '3.3.
|
|
37
|
+
export const SDK_VERSION = '3.3.5';
|
|
38
38
|
// ─── Templates ──────────────────────────────────────────────────────────────
|
|
39
39
|
const PACKAGE_JSON_TEMPLATE = (opts) => `{
|
|
40
40
|
"name": "${toKebabCase(opts.agentName)}-agent",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated process-environment variables — the allowlist behind the dashboard's
|
|
3
|
+
* Process Environment page.
|
|
4
|
+
*
|
|
5
|
+
* WHY AN ALLOWLIST. `~/.nuvira/.env` already has a general writer: the skill
|
|
6
|
+
* secret editor, which accepts any well-formed NAME because a skill may declare
|
|
7
|
+
* any name it likes. The process-behaviour switches are a different thing
|
|
8
|
+
* entirely — they change how a RUN behaves rather than what a skill can read,
|
|
9
|
+
* and several of them are tri-state (asked / declined / nobody said anything),
|
|
10
|
+
* which a free-text NAME=VALUE box cannot express. So the dashboard does not
|
|
11
|
+
* expose "write any variable"; it exposes a fixed, curated set of switches, and
|
|
12
|
+
* the endpoint that writes them refuses every name on this list and no other.
|
|
13
|
+
*
|
|
14
|
+
* WHY THE RULES ARE HERE AND NOT IN THE UI. Every flag below is read somewhere
|
|
15
|
+
* else in the codebase by a specific rule, and those rules are NOT the same:
|
|
16
|
+
*
|
|
17
|
+
* - `envAsks` (worktree.ts) — `1`/`true`/`yes` ask; everything else, including
|
|
18
|
+
* unset, does not.
|
|
19
|
+
* - `resolveResumeRequest` (step-checkpoint.ts) — `1`/`true`/`yes` ask for the
|
|
20
|
+
* automatic record, `''`/`0`/`false` are off, and ANY OTHER VALUE IS A
|
|
21
|
+
* RECORD NAME, which is an active resume. Note what that means for `off` and
|
|
22
|
+
* `no`: unlike the two rules above, `NUVIRA_RESUME=no` is a resume, not a
|
|
23
|
+
* refusal. The page reports what the reader will really do rather than the
|
|
24
|
+
* tidier thing it looks like it should do, because a config page that lies
|
|
25
|
+
* about behaviour is worse than one that omits the variable.
|
|
26
|
+
* - `debugLoggingEnabled` (debug-log.ts) and `otelExportEnabled` (otel.ts) —
|
|
27
|
+
* anything except `0`/`false`/`off`/`no`/blank, so `NUVIRA_OTEL=false` in an
|
|
28
|
+
* `.env` means OFF rather than "a non-empty string".
|
|
29
|
+
* - `strictModelMode` (route-resolver.ts) — the literal `1`, and nothing else.
|
|
30
|
+
* `NUVIRA_STRICT_MODEL=true` does NOT enable it, which is exactly the kind of
|
|
31
|
+
* detail a config UI gets wrong by assuming.
|
|
32
|
+
*
|
|
33
|
+
* Because the readers disagree, the page must not write "whatever the user
|
|
34
|
+
* typed" and then claim it took effect. It writes the canonical value the
|
|
35
|
+
* reader understands (`1` for on, `0` for off) and says, per variable, what
|
|
36
|
+
* leaving it unset means.
|
|
37
|
+
*/
|
|
38
|
+
/** The vocabulary a variable is declared for, which is also its UI grouping. */
|
|
39
|
+
export type ProcessEnvGroup = 'turn' | 'observability' | 'hooks';
|
|
40
|
+
/** How the reader that owns a switch decides it is ON. */
|
|
41
|
+
export type ProcessEnvFlagRule =
|
|
42
|
+
/** `1`/`true`/`yes` ask for it; anything else is off. (`envAsks`) */
|
|
43
|
+
'asks'
|
|
44
|
+
/** Any value except `0`/`false`/`off`/`no`/blank. (`debugLoggingEnabled`, `otelExportEnabled`) */
|
|
45
|
+
| 'truthy'
|
|
46
|
+
/** The literal `1` and nothing else. (`strictModelMode`) */
|
|
47
|
+
| 'strict-one'
|
|
48
|
+
/** `1`/`true`/`yes` ask; `''`/`0`/`false` are off; any other value NAMES one. (`resolveResumeRequest`) */
|
|
49
|
+
| 'asks-or-names';
|
|
50
|
+
/** One curated variable, in the shape both the endpoint and the page render. */
|
|
51
|
+
export interface ProcessEnvVarSpec {
|
|
52
|
+
/** The exact environment name. `NUVIRA_*`; the `BUFF_*` aliases are legacy. */
|
|
53
|
+
name: string;
|
|
54
|
+
/** Short human name for the row. */
|
|
55
|
+
label: string;
|
|
56
|
+
/** Which section of the page the row belongs to. */
|
|
57
|
+
group: ProcessEnvGroup;
|
|
58
|
+
/**
|
|
59
|
+
* `flag` renders on/off/unset, because the variable's meaning is a decision.
|
|
60
|
+
* `text` renders a single-line value, because the variable IS a value.
|
|
61
|
+
*/
|
|
62
|
+
kind: 'flag' | 'text';
|
|
63
|
+
/** For `flag` only: the rule its reader uses. */
|
|
64
|
+
rule?: ProcessEnvFlagRule;
|
|
65
|
+
/**
|
|
66
|
+
* For a `flag` that ALSO accepts a name — `NUVIRA_RESUME=1` asks for the
|
|
67
|
+
* automatic record for this ask in this directory, while any other non-falsey
|
|
68
|
+
* value names a specific checkpoint. Without this the page could only express
|
|
69
|
+
* the boolean half of a variable that has two.
|
|
70
|
+
*/
|
|
71
|
+
acceptsValue?: boolean;
|
|
72
|
+
/** Label for the optional value box (only when `acceptsValue`). */
|
|
73
|
+
valueLabel?: string;
|
|
74
|
+
/** Placeholder for a `text` row or the optional value box. */
|
|
75
|
+
placeholder?: string;
|
|
76
|
+
/** What the variable does, in one line. */
|
|
77
|
+
description: string;
|
|
78
|
+
/**
|
|
79
|
+
* What leaving it UNSET means. This is the state the row starts in and the
|
|
80
|
+
* state a user has to be able to reason about, so it is written down per
|
|
81
|
+
* variable rather than assumed to be "off" — for most of these it is, but
|
|
82
|
+
* "nobody said anything" and "someone said no" are different for the two
|
|
83
|
+
* tri-state switches, and the page must not blur them.
|
|
84
|
+
*/
|
|
85
|
+
unsetMeans: string;
|
|
86
|
+
/** Where the same switch is reachable without the dashboard. */
|
|
87
|
+
cliEquivalent?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The set — deliberately small, and every entry is a switch this codebase
|
|
91
|
+
* actually reads. A variable that nothing consumes would be a control that
|
|
92
|
+
* silently does nothing, which is worse than no control at all.
|
|
93
|
+
*/
|
|
94
|
+
export declare const PROCESS_ENV_VARS: readonly ProcessEnvVarSpec[];
|
|
95
|
+
/** Look up a spec by its exact name. */
|
|
96
|
+
export declare function processEnvVarSpec(name: string): ProcessEnvVarSpec | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Is this name on the allowlist?
|
|
99
|
+
*
|
|
100
|
+
* The write endpoints call this FIRST, before touching the file, so a request
|
|
101
|
+
* cannot append an arbitrary name to the credential `.env` through a page that
|
|
102
|
+
* claims to be curated.
|
|
103
|
+
*/
|
|
104
|
+
export declare function isCuratedProcessEnvVar(name: string): boolean;
|
|
105
|
+
/** Why a value was refused — a stable code the page turns into copy. */
|
|
106
|
+
export type ProcessEnvValueError = 'unknown-name' | 'empty' | 'multiline' | 'too-long' | 'not-a-flag';
|
|
107
|
+
/** Bound on a stored value. A hook command is a command, not a script. */
|
|
108
|
+
export declare const PROCESS_ENV_VALUE_MAX_CHARS = 512;
|
|
109
|
+
/**
|
|
110
|
+
* Normalize a value to the single spelling its reader understands.
|
|
111
|
+
*
|
|
112
|
+
* `flag` rows are canonicalized to `1`/`0` rather than stored as typed, because
|
|
113
|
+
* `NUVIRA_STRICT_MODEL=true` reads as OFF (`strictModelMode` compares to `'1'`).
|
|
114
|
+
* Storing the user's word would let the page show "on" while the run disagrees —
|
|
115
|
+
* the failure this whole module exists to prevent.
|
|
116
|
+
*
|
|
117
|
+
* A `flag` with `acceptsValue` keeps any other non-falsey string: that is the
|
|
118
|
+
* checkpoint-id half of `NUVIRA_RESUME`, and it is the reader's own rule
|
|
119
|
+
* (`resolveResumeRequest`) that "anything else names one".
|
|
120
|
+
*/
|
|
121
|
+
export declare function normalizeProcessEnvValue(name: string, raw: string): {
|
|
122
|
+
ok: true;
|
|
123
|
+
value: string;
|
|
124
|
+
} | {
|
|
125
|
+
ok: false;
|
|
126
|
+
reason: ProcessEnvValueError;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Is this stored value ON, by the rule its own reader uses?
|
|
130
|
+
*
|
|
131
|
+
* Deliberately not `normalize(...).value === '1'`: a value written by hand —
|
|
132
|
+
* `NUVIRA_OTEL=anything`, which IS on — must be reported as it will really
|
|
133
|
+
* behave, not as the page wishes it were spelled.
|
|
134
|
+
*/
|
|
135
|
+
export declare function processEnvFlagIsOn(rule: ProcessEnvFlagRule | undefined, value: string | null): boolean;
|
|
136
|
+
//# sourceMappingURL=process-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-env.d.ts","sourceRoot":"","sources":["../../src/config/process-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,gFAAgF;AAChF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,eAAe,GAAG,OAAO,CAAC;AAEjE,0DAA0D;AAC1D,MAAM,MAAM,kBAAkB;AAC5B,qEAAqE;AACnE,MAAM;AACR,kGAAkG;GAChG,QAAQ;AACV,4DAA4D;GAC1D,YAAY;AACd,0GAA0G;GACxG,eAAe,CAAC;AAEpB,gFAAgF;AAChF,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,KAAK,EAAE,eAAe,CAAC;IACvB;;;OAGG;IACH,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,iDAAiD;IACjD,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,iBAAiB,EAkGxD,CAAC;AAEF,wCAAwC;AACxC,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAE7E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D;AAMD,wEAAwE;AACxE,MAAM,MAAM,oBAAoB,GAAG,cAAc,GAAG,OAAO,GAAG,WAAW,GAAG,UAAU,GAAG,YAAY,CAAC;AAEtG,0EAA0E;AAC1E,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,CAoB3E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAkBtG"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated process-environment variables — the allowlist behind the dashboard's
|
|
3
|
+
* Process Environment page.
|
|
4
|
+
*
|
|
5
|
+
* WHY AN ALLOWLIST. `~/.nuvira/.env` already has a general writer: the skill
|
|
6
|
+
* secret editor, which accepts any well-formed NAME because a skill may declare
|
|
7
|
+
* any name it likes. The process-behaviour switches are a different thing
|
|
8
|
+
* entirely — they change how a RUN behaves rather than what a skill can read,
|
|
9
|
+
* and several of them are tri-state (asked / declined / nobody said anything),
|
|
10
|
+
* which a free-text NAME=VALUE box cannot express. So the dashboard does not
|
|
11
|
+
* expose "write any variable"; it exposes a fixed, curated set of switches, and
|
|
12
|
+
* the endpoint that writes them refuses every name on this list and no other.
|
|
13
|
+
*
|
|
14
|
+
* WHY THE RULES ARE HERE AND NOT IN THE UI. Every flag below is read somewhere
|
|
15
|
+
* else in the codebase by a specific rule, and those rules are NOT the same:
|
|
16
|
+
*
|
|
17
|
+
* - `envAsks` (worktree.ts) — `1`/`true`/`yes` ask; everything else, including
|
|
18
|
+
* unset, does not.
|
|
19
|
+
* - `resolveResumeRequest` (step-checkpoint.ts) — `1`/`true`/`yes` ask for the
|
|
20
|
+
* automatic record, `''`/`0`/`false` are off, and ANY OTHER VALUE IS A
|
|
21
|
+
* RECORD NAME, which is an active resume. Note what that means for `off` and
|
|
22
|
+
* `no`: unlike the two rules above, `NUVIRA_RESUME=no` is a resume, not a
|
|
23
|
+
* refusal. The page reports what the reader will really do rather than the
|
|
24
|
+
* tidier thing it looks like it should do, because a config page that lies
|
|
25
|
+
* about behaviour is worse than one that omits the variable.
|
|
26
|
+
* - `debugLoggingEnabled` (debug-log.ts) and `otelExportEnabled` (otel.ts) —
|
|
27
|
+
* anything except `0`/`false`/`off`/`no`/blank, so `NUVIRA_OTEL=false` in an
|
|
28
|
+
* `.env` means OFF rather than "a non-empty string".
|
|
29
|
+
* - `strictModelMode` (route-resolver.ts) — the literal `1`, and nothing else.
|
|
30
|
+
* `NUVIRA_STRICT_MODEL=true` does NOT enable it, which is exactly the kind of
|
|
31
|
+
* detail a config UI gets wrong by assuming.
|
|
32
|
+
*
|
|
33
|
+
* Because the readers disagree, the page must not write "whatever the user
|
|
34
|
+
* typed" and then claim it took effect. It writes the canonical value the
|
|
35
|
+
* reader understands (`1` for on, `0` for off) and says, per variable, what
|
|
36
|
+
* leaving it unset means.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* The set — deliberately small, and every entry is a switch this codebase
|
|
40
|
+
* actually reads. A variable that nothing consumes would be a control that
|
|
41
|
+
* silently does nothing, which is worse than no control at all.
|
|
42
|
+
*/
|
|
43
|
+
export const PROCESS_ENV_VARS = [
|
|
44
|
+
{
|
|
45
|
+
name: 'NUVIRA_ISOLATE',
|
|
46
|
+
label: 'Isolate every turn',
|
|
47
|
+
group: 'turn',
|
|
48
|
+
kind: 'flag',
|
|
49
|
+
rule: 'asks',
|
|
50
|
+
description: 'Run each turn in its own git worktree of the project and return the diff against the commit it started from.',
|
|
51
|
+
unsetMeans: 'A turn runs in the project directory itself.',
|
|
52
|
+
cliEquivalent: 'nuvira chat --worktree',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'NUVIRA_RESUME',
|
|
56
|
+
label: 'Replay recorded steps',
|
|
57
|
+
group: 'turn',
|
|
58
|
+
kind: 'flag',
|
|
59
|
+
rule: 'asks-or-names',
|
|
60
|
+
acceptsValue: true,
|
|
61
|
+
valueLabel: '…or a checkpoint id',
|
|
62
|
+
placeholder: 'blank = this ask, in this directory',
|
|
63
|
+
description: 'Replay the model calls of this ask whose whole input is unchanged instead of paying for them again.',
|
|
64
|
+
unsetMeans: 'Nothing is replayed; every step is paid for.',
|
|
65
|
+
cliEquivalent: 'nuvira chat --resume [id]',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'NUVIRA_STRICT_MODEL',
|
|
69
|
+
label: 'Refuse model substitution',
|
|
70
|
+
group: 'turn',
|
|
71
|
+
kind: 'flag',
|
|
72
|
+
rule: 'strict-one',
|
|
73
|
+
description: 'Fail instead of substituting a model when a pinned one is unavailable, so "it ran on something else" cannot happen silently.',
|
|
74
|
+
unsetMeans: 'A dead pin is repaired by substituting an available model, and the swap is announced.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'NUVIRA_DEBUG_LOG',
|
|
78
|
+
label: 'Write a session debug log',
|
|
79
|
+
group: 'observability',
|
|
80
|
+
kind: 'flag',
|
|
81
|
+
rule: 'truthy',
|
|
82
|
+
description: 'Write one redacted, bounded log file per turn to <config dir>/debug-logs, ready to attach to a bug report.',
|
|
83
|
+
unsetMeans: 'No log is written.',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'NUVIRA_OTEL',
|
|
87
|
+
label: 'Export OTLP spans',
|
|
88
|
+
group: 'observability',
|
|
89
|
+
kind: 'flag',
|
|
90
|
+
rule: 'truthy',
|
|
91
|
+
description: 'Export a span tree per turn (nuvira.turn with one child per tool call that really ran) over OTLP/HTTP.',
|
|
92
|
+
unsetMeans: 'Nothing is built and the OpenTelemetry SDK is never even imported.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'OTEL_EXPORTER_OTLP_ENDPOINT',
|
|
96
|
+
label: 'OTLP collector endpoint',
|
|
97
|
+
group: 'observability',
|
|
98
|
+
kind: 'text',
|
|
99
|
+
placeholder: 'http://localhost:4318',
|
|
100
|
+
description: 'Where spans are shipped. The OTLP spec appends /v1/traces to this; set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT instead to give the full path.',
|
|
101
|
+
unsetMeans: 'NUVIRA_OTEL builds spans and then drops them, because there is nowhere to send them.',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'NUVIRA_TOOL_HOOK_BEFORE',
|
|
105
|
+
label: 'Before a tool call',
|
|
106
|
+
group: 'hooks',
|
|
107
|
+
kind: 'text',
|
|
108
|
+
placeholder: 'node ~/deny-shell.mjs',
|
|
109
|
+
description: 'A command run before each tool call. The call arrives as JSON on stdin; silence means allow, and {"decision":"deny","reason":"…"} stops the call.',
|
|
110
|
+
unsetMeans: 'No before-hook. A hook declared in tools.hooks (buffconfig.json) still runs.',
|
|
111
|
+
cliEquivalent: 'tools.hooks in buffconfig.json',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'NUVIRA_TOOL_HOOK_AFTER',
|
|
115
|
+
label: 'After a tool call',
|
|
116
|
+
group: 'hooks',
|
|
117
|
+
kind: 'text',
|
|
118
|
+
placeholder: 'node ~/audit.mjs',
|
|
119
|
+
description: 'A command run after each tool call, with the call and a bounded preview of its result on stdin. It cannot veto — the call already happened.',
|
|
120
|
+
unsetMeans: 'No after-hook. A hook declared in tools.hooks (buffconfig.json) still runs.',
|
|
121
|
+
cliEquivalent: 'tools.hooks in buffconfig.json',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'NUVIRA_TOOL_HOOK_FAILED',
|
|
125
|
+
label: 'After a failed tool call',
|
|
126
|
+
group: 'hooks',
|
|
127
|
+
kind: 'text',
|
|
128
|
+
placeholder: 'node ~/on-failure.mjs',
|
|
129
|
+
description: 'A command run when a tool call fails, with the failure on stdin.',
|
|
130
|
+
unsetMeans: 'No failed-hook. A hook declared in tools.hooks (buffconfig.json) still runs.',
|
|
131
|
+
cliEquivalent: 'tools.hooks in buffconfig.json',
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
/** Look up a spec by its exact name. */
|
|
135
|
+
export function processEnvVarSpec(name) {
|
|
136
|
+
return PROCESS_ENV_VARS.find((v) => v.name === name);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Is this name on the allowlist?
|
|
140
|
+
*
|
|
141
|
+
* The write endpoints call this FIRST, before touching the file, so a request
|
|
142
|
+
* cannot append an arbitrary name to the credential `.env` through a page that
|
|
143
|
+
* claims to be curated.
|
|
144
|
+
*/
|
|
145
|
+
export function isCuratedProcessEnvVar(name) {
|
|
146
|
+
return processEnvVarSpec(name) !== undefined;
|
|
147
|
+
}
|
|
148
|
+
/** The words that mean ON / OFF, as the readers themselves accept them. */
|
|
149
|
+
const ON_WORDS = new Set(['1', 'true', 'yes', 'on']);
|
|
150
|
+
const OFF_WORDS = new Set(['0', 'false', 'off', 'no']);
|
|
151
|
+
/** Bound on a stored value. A hook command is a command, not a script. */
|
|
152
|
+
export const PROCESS_ENV_VALUE_MAX_CHARS = 512;
|
|
153
|
+
/**
|
|
154
|
+
* Normalize a value to the single spelling its reader understands.
|
|
155
|
+
*
|
|
156
|
+
* `flag` rows are canonicalized to `1`/`0` rather than stored as typed, because
|
|
157
|
+
* `NUVIRA_STRICT_MODEL=true` reads as OFF (`strictModelMode` compares to `'1'`).
|
|
158
|
+
* Storing the user's word would let the page show "on" while the run disagrees —
|
|
159
|
+
* the failure this whole module exists to prevent.
|
|
160
|
+
*
|
|
161
|
+
* A `flag` with `acceptsValue` keeps any other non-falsey string: that is the
|
|
162
|
+
* checkpoint-id half of `NUVIRA_RESUME`, and it is the reader's own rule
|
|
163
|
+
* (`resolveResumeRequest`) that "anything else names one".
|
|
164
|
+
*/
|
|
165
|
+
export function normalizeProcessEnvValue(name, raw) {
|
|
166
|
+
const spec = processEnvVarSpec(name);
|
|
167
|
+
if (!spec)
|
|
168
|
+
return { ok: false, reason: 'unknown-name' };
|
|
169
|
+
// A newline would smuggle a second variable into the file, so it is refused
|
|
170
|
+
// rather than stripped: silently dropping half a pasted value is how a config
|
|
171
|
+
// ends up containing something the user never wrote.
|
|
172
|
+
if (/[\r\n]/.test(raw))
|
|
173
|
+
return { ok: false, reason: 'multiline' };
|
|
174
|
+
const value = raw.trim();
|
|
175
|
+
if (value === '')
|
|
176
|
+
return { ok: false, reason: 'empty' };
|
|
177
|
+
if (value.length > PROCESS_ENV_VALUE_MAX_CHARS)
|
|
178
|
+
return { ok: false, reason: 'too-long' };
|
|
179
|
+
if (spec.kind === 'text')
|
|
180
|
+
return { ok: true, value };
|
|
181
|
+
const word = value.toLowerCase();
|
|
182
|
+
if (ON_WORDS.has(word))
|
|
183
|
+
return { ok: true, value: '1' };
|
|
184
|
+
if (OFF_WORDS.has(word))
|
|
185
|
+
return { ok: true, value: '0' };
|
|
186
|
+
if (spec.acceptsValue)
|
|
187
|
+
return { ok: true, value };
|
|
188
|
+
return { ok: false, reason: 'not-a-flag' };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Is this stored value ON, by the rule its own reader uses?
|
|
192
|
+
*
|
|
193
|
+
* Deliberately not `normalize(...).value === '1'`: a value written by hand —
|
|
194
|
+
* `NUVIRA_OTEL=anything`, which IS on — must be reported as it will really
|
|
195
|
+
* behave, not as the page wishes it were spelled.
|
|
196
|
+
*/
|
|
197
|
+
export function processEnvFlagIsOn(rule, value) {
|
|
198
|
+
if (value === null)
|
|
199
|
+
return false;
|
|
200
|
+
const word = value.trim().toLowerCase();
|
|
201
|
+
switch (rule) {
|
|
202
|
+
case 'truthy':
|
|
203
|
+
return word !== '' && word !== '0' && word !== 'false' && word !== 'off' && word !== 'no';
|
|
204
|
+
case 'asks':
|
|
205
|
+
return word === '1' || word === 'true' || word === 'yes';
|
|
206
|
+
case 'strict-one':
|
|
207
|
+
return value.trim() === '1';
|
|
208
|
+
case 'asks-or-names':
|
|
209
|
+
// `resolveResumeRequest` in full: only blank, `0` and `false` decline.
|
|
210
|
+
// `off` and `no` are NOT refusals here — they name a record.
|
|
211
|
+
return word !== '' && word !== '0' && word !== 'false';
|
|
212
|
+
default:
|
|
213
|
+
// A `text` row has no on/off reading; callers only ask this for flags.
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=process-env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-env.js","sourceRoot":"","sources":["../../src/config/process-env.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Can the router use this model RIGHT NOW — and if not, what exactly is wrong?
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS MODULE EXISTS. The dashboard's Discovery Timeline answered that
|
|
5
|
+
* question three different ways on one screen and they disagreed: the summary
|
|
6
|
+
* cards and the filter classified on PROBE age (`lastProbedAt`), while each
|
|
7
|
+
* row's badge checked verification `status` first. MEASURED on a real profile:
|
|
8
|
+
* 555 tracked models, 17 verified, and 531 probed within 7 days — so the "Fresh
|
|
9
|
+
* (531)" card opened a list in which 514 rows read "Unverified". Nothing was
|
|
10
|
+
* broken and nothing agreed.
|
|
11
|
+
*
|
|
12
|
+
* The deeper fault was the question. "Fresh" and "unverified" are not two
|
|
13
|
+
* answers to one question, they are two different questions:
|
|
14
|
+
*
|
|
15
|
+
* - `lastProbedAt` — is the provider still listing this id? (discovery)
|
|
16
|
+
* - `status` / `lastVerifiedAt` — has a real turn been proven to work on it?
|
|
17
|
+
* (routing eligibility)
|
|
18
|
+
*
|
|
19
|
+
* A model can be freshly probed and never verified (the common case: a provider
|
|
20
|
+
* lists hundreds of ids, none tried), or verified and long unprobed. Flattening
|
|
21
|
+
* both into one label is what produced the contradiction, so this module keeps
|
|
22
|
+
* them apart and names each state for what it actually is.
|
|
23
|
+
*
|
|
24
|
+
* THE ROUTING VERDICT IS NOT RE-DERIVED HERE. `classifyReachability` mirrors
|
|
25
|
+
* `ModelRegistry.isUsable()` clause for clause, in its order, including the
|
|
26
|
+
* subtlety that a verified model is blocked only by a MODEL-level park
|
|
27
|
+
* (`providerParked === false`) and never by a provider-level one. Two copies of
|
|
28
|
+
* this rule is exactly the divergence that made the Models page present a
|
|
29
|
+
* listing as availability (see that panel's own history), so `routable` here
|
|
30
|
+
* means "isUsable() would return true" and nothing else.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* What the router would do with this model right now.
|
|
34
|
+
*
|
|
35
|
+
* An enum rather than a boolean because "not routable" is four different
|
|
36
|
+
* situations behind one word, and they need opposite actions from a user:
|
|
37
|
+
* one is unknown and will be resolved by a spot-check, one is proven dead, one
|
|
38
|
+
* clears itself, and one is repaired by re-probing.
|
|
39
|
+
*/
|
|
40
|
+
export type ModelReachability =
|
|
41
|
+
/** Verified, un-parked, verified within the staleness window — `isUsable() === true`. */
|
|
42
|
+
'routable'
|
|
43
|
+
/** Verified, but resting on a MODEL-level quota park. Clears by itself. */
|
|
44
|
+
| 'parked'
|
|
45
|
+
/** Verified, but the proof is older than `DEFAULT_STALE_MS` (7d) — re-probe to restore. */
|
|
46
|
+
| 'proof-expired'
|
|
47
|
+
/** `unavailable`: a real call established this id does not work here. */
|
|
48
|
+
| 'proven-dead'
|
|
49
|
+
/** `unverified`: the provider lists it and nothing has ever succeeded on it. NOT a failure. */
|
|
50
|
+
| 'never-verified';
|
|
51
|
+
/** The subset of a registry entry this classifier reads. */
|
|
52
|
+
export interface ReachabilityInput {
|
|
53
|
+
status: string;
|
|
54
|
+
lastVerifiedAt?: number;
|
|
55
|
+
quotaParkedUntil?: number;
|
|
56
|
+
providerParked?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Mirror of `ModelRegistry.isUsable()`, clause for clause and in the same order.
|
|
60
|
+
*
|
|
61
|
+
* Order is load-bearing and is NOT the order you would guess: a model that is
|
|
62
|
+
* `unavailable` AND parked reads as `proven-dead`, because the status check
|
|
63
|
+
* comes first. Keeping the two functions adjacent in shape is what makes them
|
|
64
|
+
* diffable by eye.
|
|
65
|
+
*/
|
|
66
|
+
export declare function classifyReachability(entry: ReachabilityInput, now?: number): ModelReachability;
|
|
67
|
+
/** How a state is worded. One place, so the page and any CLI cannot drift. */
|
|
68
|
+
export declare const REACHABILITY_COPY: Record<ModelReachability, {
|
|
69
|
+
label: string;
|
|
70
|
+
blurb: string;
|
|
71
|
+
color: string;
|
|
72
|
+
}>;
|
|
73
|
+
/** Would the router pick this model right now? The one question routing asks. */
|
|
74
|
+
export declare function isRoutable(reachability: ModelReachability): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Is the provider still listing this id?
|
|
77
|
+
*
|
|
78
|
+
* Deliberately separate from reachability: probe age says nothing about whether
|
|
79
|
+
* a turn would work, and reachability says nothing about whether the model still
|
|
80
|
+
* exists in the catalog. The old page treated them as one axis.
|
|
81
|
+
*/
|
|
82
|
+
export type ModelFreshness = 'fresh' | 'stale' | 'likely-removed';
|
|
83
|
+
/** Probe-age thresholds. Kept here so the numbers are stated once. */
|
|
84
|
+
export declare const FRESH_DAYS = 7;
|
|
85
|
+
export declare const REMOVED_DAYS = 30;
|
|
86
|
+
/** Error rate above which a long-unprobed model reads as actually gone. */
|
|
87
|
+
export declare const REMOVED_ERROR_RATE = 0.5;
|
|
88
|
+
export declare function classifyFreshness(lastProbedAt: number | undefined, errorRate: number | undefined, now?: number): ModelFreshness;
|
|
89
|
+
/** How a freshness state is worded. */
|
|
90
|
+
export declare const FRESHNESS_COPY: Record<ModelFreshness, {
|
|
91
|
+
label: string;
|
|
92
|
+
blurb: string;
|
|
93
|
+
color: string;
|
|
94
|
+
}>;
|
|
95
|
+
//# sourceMappingURL=model-reachability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-reachability.d.ts","sourceRoot":"","sources":["../../src/learning/model-reachability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB;AAC3B,yFAAyF;AACvF,UAAU;AACZ,2EAA2E;GACzE,QAAQ;AACV,2FAA2F;GACzF,eAAe;AACjB,yEAAyE;GACvE,aAAa;AACf,+FAA+F;GAC7F,gBAAgB,CAAC;AAErB,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,iBAAiB,EACxB,GAAG,GAAE,MAAmB,GACvB,iBAAiB,CAWnB;AAED,8EAA8E;AAC9E,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA4BxG,CAAC;AAEF,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,YAAY,EAAE,iBAAiB,GAAG,OAAO,CAEnE;AAED;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,gBAAgB,CAAC;AAElE,sEAAsE;AACtE,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,GAAG,GAAE,MAAmB,GACvB,cAAc,CAKhB;AAED,uCAAuC;AACvC,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAQlG,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Can the router use this model RIGHT NOW — and if not, what exactly is wrong?
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS MODULE EXISTS. The dashboard's Discovery Timeline answered that
|
|
5
|
+
* question three different ways on one screen and they disagreed: the summary
|
|
6
|
+
* cards and the filter classified on PROBE age (`lastProbedAt`), while each
|
|
7
|
+
* row's badge checked verification `status` first. MEASURED on a real profile:
|
|
8
|
+
* 555 tracked models, 17 verified, and 531 probed within 7 days — so the "Fresh
|
|
9
|
+
* (531)" card opened a list in which 514 rows read "Unverified". Nothing was
|
|
10
|
+
* broken and nothing agreed.
|
|
11
|
+
*
|
|
12
|
+
* The deeper fault was the question. "Fresh" and "unverified" are not two
|
|
13
|
+
* answers to one question, they are two different questions:
|
|
14
|
+
*
|
|
15
|
+
* - `lastProbedAt` — is the provider still listing this id? (discovery)
|
|
16
|
+
* - `status` / `lastVerifiedAt` — has a real turn been proven to work on it?
|
|
17
|
+
* (routing eligibility)
|
|
18
|
+
*
|
|
19
|
+
* A model can be freshly probed and never verified (the common case: a provider
|
|
20
|
+
* lists hundreds of ids, none tried), or verified and long unprobed. Flattening
|
|
21
|
+
* both into one label is what produced the contradiction, so this module keeps
|
|
22
|
+
* them apart and names each state for what it actually is.
|
|
23
|
+
*
|
|
24
|
+
* THE ROUTING VERDICT IS NOT RE-DERIVED HERE. `classifyReachability` mirrors
|
|
25
|
+
* `ModelRegistry.isUsable()` clause for clause, in its order, including the
|
|
26
|
+
* subtlety that a verified model is blocked only by a MODEL-level park
|
|
27
|
+
* (`providerParked === false`) and never by a provider-level one. Two copies of
|
|
28
|
+
* this rule is exactly the divergence that made the Models page present a
|
|
29
|
+
* listing as availability (see that panel's own history), so `routable` here
|
|
30
|
+
* means "isUsable() would return true" and nothing else.
|
|
31
|
+
*/
|
|
32
|
+
import { DEFAULT_STALE_MS } from './model-registry.js';
|
|
33
|
+
/**
|
|
34
|
+
* Mirror of `ModelRegistry.isUsable()`, clause for clause and in the same order.
|
|
35
|
+
*
|
|
36
|
+
* Order is load-bearing and is NOT the order you would guess: a model that is
|
|
37
|
+
* `unavailable` AND parked reads as `proven-dead`, because the status check
|
|
38
|
+
* comes first. Keeping the two functions adjacent in shape is what makes them
|
|
39
|
+
* diffable by eye.
|
|
40
|
+
*/
|
|
41
|
+
export function classifyReachability(entry, now = Date.now()) {
|
|
42
|
+
if (entry.status !== 'verified') {
|
|
43
|
+
return entry.status === 'unavailable' ? 'proven-dead' : 'never-verified';
|
|
44
|
+
}
|
|
45
|
+
// Model-specific park only — a provider-wide park deliberately does NOT
|
|
46
|
+
// exclude a proven model (the "Gemini parking bug").
|
|
47
|
+
const parkedUntil = entry.quotaParkedUntil ?? 0;
|
|
48
|
+
if (parkedUntil > now && entry.providerParked === false)
|
|
49
|
+
return 'parked';
|
|
50
|
+
const verifiedAt = entry.lastVerifiedAt ?? 0;
|
|
51
|
+
if (now - verifiedAt > DEFAULT_STALE_MS)
|
|
52
|
+
return 'proof-expired';
|
|
53
|
+
return 'routable';
|
|
54
|
+
}
|
|
55
|
+
/** How a state is worded. One place, so the page and any CLI cannot drift. */
|
|
56
|
+
export const REACHABILITY_COPY = {
|
|
57
|
+
routable: {
|
|
58
|
+
label: 'Routable',
|
|
59
|
+
blurb: 'Verified within 7 days and not parked — the router can pick this now.',
|
|
60
|
+
color: '#3fb950',
|
|
61
|
+
},
|
|
62
|
+
parked: {
|
|
63
|
+
label: 'Parked',
|
|
64
|
+
blurb: 'Resting on a model-level quota window. It re-enters routing on its own when the window lapses.',
|
|
65
|
+
color: '#d29922',
|
|
66
|
+
},
|
|
67
|
+
'proof-expired': {
|
|
68
|
+
label: 'Proof expired',
|
|
69
|
+
blurb: 'Was verified, but not within the last 7 days, so the registry no longer offers it. A re-probe restores it.',
|
|
70
|
+
color: '#d29922',
|
|
71
|
+
},
|
|
72
|
+
'proven-dead': {
|
|
73
|
+
label: 'Proven dead',
|
|
74
|
+
blurb: 'A real call established this id does not work here — re-probing will not fix it.',
|
|
75
|
+
color: '#f85149',
|
|
76
|
+
},
|
|
77
|
+
'never-verified': {
|
|
78
|
+
label: 'Never verified',
|
|
79
|
+
blurb: 'The provider lists this id and nothing has ever been tried against it. This is an unknown, not a failure — ' +
|
|
80
|
+
'background spot-checks work through these a few at a time.',
|
|
81
|
+
color: '#58a6ff',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
/** Would the router pick this model right now? The one question routing asks. */
|
|
85
|
+
export function isRoutable(reachability) {
|
|
86
|
+
return reachability === 'routable';
|
|
87
|
+
}
|
|
88
|
+
/** Probe-age thresholds. Kept here so the numbers are stated once. */
|
|
89
|
+
export const FRESH_DAYS = 7;
|
|
90
|
+
export const REMOVED_DAYS = 30;
|
|
91
|
+
/** Error rate above which a long-unprobed model reads as actually gone. */
|
|
92
|
+
export const REMOVED_ERROR_RATE = 0.5;
|
|
93
|
+
export function classifyFreshness(lastProbedAt, errorRate, now = Date.now()) {
|
|
94
|
+
if (!lastProbedAt)
|
|
95
|
+
return 'likely-removed';
|
|
96
|
+
const days = (now - lastProbedAt) / (24 * 60 * 60 * 1000);
|
|
97
|
+
if (days > REMOVED_DAYS && (errorRate ?? 0) > REMOVED_ERROR_RATE)
|
|
98
|
+
return 'likely-removed';
|
|
99
|
+
return days > FRESH_DAYS ? 'stale' : 'fresh';
|
|
100
|
+
}
|
|
101
|
+
/** How a freshness state is worded. */
|
|
102
|
+
export const FRESHNESS_COPY = {
|
|
103
|
+
fresh: { label: 'Fresh', blurb: `Probed within ${FRESH_DAYS} days.`, color: '#3fb950' },
|
|
104
|
+
stale: { label: 'Stale', blurb: `Not probed for over ${FRESH_DAYS} days.`, color: '#d29922' },
|
|
105
|
+
'likely-removed': {
|
|
106
|
+
label: 'Likely removed',
|
|
107
|
+
blurb: `Not probed for over ${REMOVED_DAYS} days and failing — probably gone from the provider.`,
|
|
108
|
+
color: '#f85149',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=model-reachability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-reachability.js","sourceRoot":"","sources":["../../src/learning/model-reachability.ts"],"names":[],"mappings":"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"}
|
|
@@ -694,6 +694,28 @@ export declare class ModelRegistry {
|
|
|
694
694
|
pruneAbsentModels(provider: string, liveModels: string[]): number;
|
|
695
695
|
/** Load the JSON mirror synchronously (never throws). */
|
|
696
696
|
private loadMirror;
|
|
697
|
+
/**
|
|
698
|
+
* Re-read the JSON mirror into memory — adopt what OTHER processes learned.
|
|
699
|
+
*
|
|
700
|
+
* The singleton loads the mirror exactly ONCE, in the constructor, and
|
|
701
|
+
* {@link persist} writes the WHOLE entry map from `this.data`. Those two facts
|
|
702
|
+
* together mean a long-lived process holds a snapshot of every model it never
|
|
703
|
+
* touched, and flushes that snapshot back over the file the moment it persists
|
|
704
|
+
* anything — silently reverting anything another process (the gateway, a CLI
|
|
705
|
+
* run, the warmup daemon) verified in the meantime.
|
|
706
|
+
*
|
|
707
|
+
* Callers that are about to spend a probe budget and persist should reload
|
|
708
|
+
* first so that `this.data` is as current as the file allows. The window does
|
|
709
|
+
* not close — a write from another process DURING a long run is still lost on
|
|
710
|
+
* this process's next persist — but it shrinks from "since this process booted"
|
|
711
|
+
* to "since this run started".
|
|
712
|
+
*
|
|
713
|
+
* DISCARDING, deliberately: this drops any in-memory change this process has
|
|
714
|
+
* made but not yet persisted. Every mutator on this class persists immediately,
|
|
715
|
+
* so there is normally nothing pending — but that is the contract that makes
|
|
716
|
+
* this safe, and it is why this is not called from arbitrary paths.
|
|
717
|
+
*/
|
|
718
|
+
reloadFromMirror(): void;
|
|
697
719
|
/**
|
|
698
720
|
* Persist: JSON mirror synchronously (canonical, guaranteed), then mirror to
|
|
699
721
|
* the VectorStore namespace asynchronously (best-effort, auto-tiers to JSON
|