@vincemakes/kiso-code 0.40.2 → 0.40.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/adapter-options.d.ts +6 -0
- package/dist/auth/adapter-options.js +24 -0
- package/dist/chat.d.ts +62 -2
- package/dist/chat.js +134 -21
- package/dist/config.d.ts +22 -1
- package/dist/config.js +28 -1
- package/dist/dispatch.js +22 -9
- package/dist/empty-sessions.d.ts +22 -0
- package/dist/empty-sessions.js +102 -0
- package/dist/index.js +49 -4
- package/dist/learned-windows.d.ts +23 -0
- package/dist/learned-windows.js +100 -0
- package/dist/provider-label.d.ts +8 -3
- package/dist/provider-label.js +12 -4
- package/dist/state.d.ts +4 -0
- package/dist/state.js +11 -0
- package/package.json +15 -15
|
@@ -18,5 +18,11 @@ export interface AdapterOptions {
|
|
|
18
18
|
readonly accountId: string;
|
|
19
19
|
}>;
|
|
20
20
|
readonly promptCacheKey?: string;
|
|
21
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
21
22
|
}
|
|
23
|
+
/** The profile's headers with `{session}` replaced by the session id. A
|
|
24
|
+
* header that names `{session}` when there is no session (`kiso sessions`,
|
|
25
|
+
* which streams nothing) is left out, never sent with the placeholder in
|
|
26
|
+
* it. Undefined when nothing is left. */
|
|
27
|
+
export declare function expandProfileHeaders(headers: Readonly<Record<string, string>> | undefined, sessionId: string | undefined): Readonly<Record<string, string>> | undefined;
|
|
22
28
|
export declare function adapterOptionsFor(profile: ModelProfile, auth: AdapterAuth, sessionId?: string): AdapterOptions;
|
|
@@ -33,6 +33,25 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { effectiveBaseUrl } from "./credentials.js";
|
|
35
35
|
import { oauthTokenThunk } from "./token.js";
|
|
36
|
+
/** The profile's headers with `{session}` replaced by the session id. A
|
|
37
|
+
* header that names `{session}` when there is no session (`kiso sessions`,
|
|
38
|
+
* which streams nothing) is left out, never sent with the placeholder in
|
|
39
|
+
* it. Undefined when nothing is left. */
|
|
40
|
+
export function expandProfileHeaders(headers, sessionId) {
|
|
41
|
+
if (headers === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
45
|
+
if (value.includes("{session}")) {
|
|
46
|
+
if (sessionId === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
out[name] = value.split("{session}").join(sessionId);
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
out[name] = value;
|
|
52
|
+
}
|
|
53
|
+
return Object.keys(out).length === 0 ? undefined : out;
|
|
54
|
+
}
|
|
36
55
|
export function adapterOptionsFor(profile, auth, sessionId) {
|
|
37
56
|
// OR-1: exactly one sign-in shape reaches the adapter. An OAuth profile
|
|
38
57
|
// has no key to pass — it passes the thunk the adapter re-resolves per
|
|
@@ -43,6 +62,7 @@ export function adapterOptionsFor(profile, auth, sessionId) {
|
|
|
43
62
|
// own environment variable, and a profile with no baseUrl then sent the
|
|
44
63
|
// stored vendor key wherever that variable pointed.
|
|
45
64
|
const url = effectiveBaseUrl(profile.kind, profile.baseUrl);
|
|
65
|
+
const headers = expandProfileHeaders(profile.headers, sessionId);
|
|
46
66
|
return {
|
|
47
67
|
...credential,
|
|
48
68
|
...(url !== undefined ? { baseUrl: url } : {}),
|
|
@@ -53,5 +73,9 @@ export function adapterOptionsFor(profile, auth, sessionId) {
|
|
|
53
73
|
// cache.
|
|
54
74
|
...(sessionId !== undefined ? { promptCacheKey: sessionId } : {}),
|
|
55
75
|
...(profile.promptCaching !== undefined ? { promptCaching: profile.promptCaching } : {}),
|
|
76
|
+
// The endpoint's own headers (a gateway's session header, say), with
|
|
77
|
+
// the session id where the profile wrote `{session}` — the same id
|
|
78
|
+
// as the cache key above, so one conversation is one lane end to end.
|
|
79
|
+
...(headers !== undefined ? { headers } : {}),
|
|
56
80
|
};
|
|
57
81
|
}
|
package/dist/chat.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { type RunUsage } from "@vincemakes/kiso-tui";
|
|
|
9
9
|
import { type SaferFailure, type SaferOption } from "@vincemakes/kiso-tui";
|
|
10
10
|
import type { AgentSession, Run } from "@vincemakes/kiso-runtime";
|
|
11
11
|
import { type LineInput } from "./state.js";
|
|
12
|
+
import { type ContextWindowSource } from "@vincemakes/kiso-runtime/internal";
|
|
12
13
|
/**
|
|
13
14
|
* The ergonomics batch C8 — the /compact auto-trigger, OPT-IN (default off: only an
|
|
14
15
|
* explicit KISO_AUTO_COMPACT=<ratio> enables it — the CLI never defaults
|
|
@@ -42,7 +43,9 @@ export declare function autoCompactFromEnv(): AutoCompact | undefined;
|
|
|
42
43
|
* THE WINDOW SOMEBODY STATED, or null when nobody has.
|
|
43
44
|
*
|
|
44
45
|
* Three sources, in order: the config/profile window, KISO_CONTEXT_WINDOW,
|
|
45
|
-
* and the metadata registry
|
|
46
|
+
* and the metadata registry (CW-1: this endpoint's row, the upstream's,
|
|
47
|
+
* then the model's own — see lookupContextWindow). Each is a claim
|
|
48
|
+
* someone made and dated. When
|
|
46
49
|
* none of them speaks, this returns NULL rather than a number — because
|
|
47
50
|
* the question "how much of the window is left" has no answer without a
|
|
48
51
|
* window, and every display that shows a percentage needs this one, not
|
|
@@ -57,6 +60,37 @@ export declare function knownContextWindow(of?: {
|
|
|
57
60
|
readonly model: string;
|
|
58
61
|
readonly baseUrl?: string;
|
|
59
62
|
}): number | null;
|
|
63
|
+
/** CW-1: a stated window and WHO stated it — `set` is the user (profile,
|
|
64
|
+
* config or KISO_CONTEXT_WINDOW), `faux` is ours, the rest are the
|
|
65
|
+
* registry's steps (lookupContextWindow). */
|
|
66
|
+
export interface StatedWindow {
|
|
67
|
+
readonly tokens: number;
|
|
68
|
+
readonly source: "set" | "faux" | "learned" | ContextWindowSource;
|
|
69
|
+
/** the registry row's model id, for the registry's steps */
|
|
70
|
+
readonly from?: string;
|
|
71
|
+
/** CW-1 batch 2: the day an endpoint's refusal stated it, for `learned` */
|
|
72
|
+
readonly observedAt?: string;
|
|
73
|
+
}
|
|
74
|
+
/** CW-1: `knownContextWindow` with its source — the chain the displays name.
|
|
75
|
+
* The user's figure is the live binding's unless `own` carries a
|
|
76
|
+
* profile's: the /model listing passes each row's, and a row with none
|
|
77
|
+
* must not borrow the live one (`own: { configured: undefined }` — an
|
|
78
|
+
* optional parameter defaulted to the live value would take it back,
|
|
79
|
+
* since an explicit undefined selects the default). */
|
|
80
|
+
export declare function statedContextWindow(of?: {
|
|
81
|
+
readonly model: string;
|
|
82
|
+
readonly baseUrl?: string;
|
|
83
|
+
readonly upstream?: string;
|
|
84
|
+
}, own?: {
|
|
85
|
+
readonly configured: number | undefined;
|
|
86
|
+
}): StatedWindow | null;
|
|
87
|
+
/** CW-1: a window as the displays write it — `1M`, `1.05M`, `272K`,
|
|
88
|
+
* `1,048,576` (a figure that is not round is shown whole). */
|
|
89
|
+
export declare function windowLabel(tokens: number): string;
|
|
90
|
+
/** CW-1: the source, said. The short form rides a /model row; the long
|
|
91
|
+
* form is /status's. Only the model step is marked in the short form —
|
|
92
|
+
* it is the one figure no row states for this endpoint. */
|
|
93
|
+
export declare function windowSourceNote(w: StatedWindow | null, form: "short" | "long"): string;
|
|
60
94
|
export declare function contextWindowTokens(of?: {
|
|
61
95
|
readonly model: string;
|
|
62
96
|
readonly baseUrl?: string;
|
|
@@ -101,9 +135,13 @@ export declare function statusModelLabel(session: {
|
|
|
101
135
|
};
|
|
102
136
|
}): string;
|
|
103
137
|
/** ADR-0055 Amendment 2 (decision 5): with no stated window the status row
|
|
104
|
-
* shows `ctx ?`, yet the compaction tiers still assume the
|
|
138
|
+
* shows `ctx ?`, yet the compaction tiers still assume the fallback (128K
|
|
139
|
+
* since CW-1 batch 2)
|
|
105
140
|
* — the assumption is said out loud once, at agent build. */
|
|
106
141
|
export declare function unknownWindowNotice(model: string): string;
|
|
142
|
+
/** CW-1 batch 2: said once, when an endpoint's refusal states its cap — the
|
|
143
|
+
* tiers aim below it from the next request, and later sessions start on it. */
|
|
144
|
+
export declare function windowLearnedNotice(model: string, host: string, tokens: number): string;
|
|
107
145
|
/** ADR-0055 Amendment 2: the notice for a checkpoint the shrink invariant
|
|
108
146
|
* discarded — chars/4 sizes only; the checkpoint's text is the owner's
|
|
109
147
|
* work and never reaches the screen. */
|
|
@@ -231,6 +269,28 @@ export declare function turnUsageLedger(): {
|
|
|
231
269
|
report: (u: RunUsage) => void;
|
|
232
270
|
total: () => RunUsage | null;
|
|
233
271
|
};
|
|
272
|
+
/**
|
|
273
|
+
* The per-CALL view of usage reports for the two figures that must not
|
|
274
|
+
* double: the cache-miss estimate and the session's running cost.
|
|
275
|
+
*
|
|
276
|
+
* A call may report its usage more than once — the op gateway sent two
|
|
277
|
+
* usage chunks for one request (the owner's session
|
|
278
|
+
* 2026-09-23T03-00-01-230c: `fresh 3k · miss 3k` on a first turn, both
|
|
279
|
+
* reports 3,041 in / 69 out). The ledger above already makes a call's
|
|
280
|
+
* LATEST report replace the earlier; the miss and the cost were read per
|
|
281
|
+
* EVENT, so the second report compared itself with the first and called
|
|
282
|
+
* the whole prompt a miss, and a priced route would have been charged
|
|
283
|
+
* twice. Here the carrier is the PREVIOUS call's total, and a call's cost
|
|
284
|
+
* is added as the difference from what that call already added.
|
|
285
|
+
*/
|
|
286
|
+
export declare function callUsageMeter(): {
|
|
287
|
+
/** what a report's miss is measured against: the previous call's total */
|
|
288
|
+
carrier: () => number | null;
|
|
289
|
+
/** one report of the current call; returns the cost to ADD (or null) */
|
|
290
|
+
report: (total: number | null, costUsd: number | null) => number | null;
|
|
291
|
+
/** a stop ends the call */
|
|
292
|
+
endCall: () => void;
|
|
293
|
+
};
|
|
234
294
|
/** v2b: the spinner merged into the STATUS BAR (the v2a standalone glyph
|
|
235
295
|
* is gone) — docked only, 200ms rotation between the request and the
|
|
236
296
|
* first event. */
|
package/dist/chat.js
CHANGED
|
@@ -14,14 +14,19 @@ import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
|
|
|
14
14
|
import { canonicalizeUsageForModel, requestBudget } from "@vincemakes/kiso-runtime/internal";
|
|
15
15
|
import { dispatch, abortBangCommand } from "./dispatch.js";
|
|
16
16
|
import { paintWindowTitle } from "./window-title.js";
|
|
17
|
-
import { agentBaseUrl, agentModel, body, bodyLog, configuredWindow, dock, retryOnRow, retryShown, setRetryShown, floorOn, protectedFiles } from "./state.js";
|
|
17
|
+
import { agentBaseUrl, agentModel, body, bodyLog, configuredWindow, dock, retryOnRow, retryShown, setRetryShown, floorOn, protectedFiles, upstreamOf } from "./state.js";
|
|
18
18
|
import { attachImages } from "./attachments.js";
|
|
19
|
-
import {
|
|
19
|
+
import { learnedWindowFor } from "./learned-windows.js";
|
|
20
|
+
import { lookupContextWindow, lookupModelMetadata } from "@vincemakes/kiso-runtime/internal";
|
|
20
21
|
import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
21
22
|
import { FauxExhaustionError, failOnFauxExhaustion } from "./faux-glue.js";
|
|
22
23
|
import { OFFERED_MODES, getMode, setMode } from "./mode.js";
|
|
23
|
-
/** B area: default context window for the ~ctx estimate (config overridable).
|
|
24
|
-
|
|
24
|
+
/** B area: default context window for the ~ctx estimate (config overridable).
|
|
25
|
+
* CW-1 batch 2: 128,000, down from 200,000 — the figure a model nobody
|
|
26
|
+
* states a window for is assumed to hold (the reference implementation's
|
|
27
|
+
* default too). Too small costs an early compaction; too large costs a
|
|
28
|
+
* refused request on a 128K model. Registered models never reach it. */
|
|
29
|
+
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
25
30
|
/** The in-process fake provider's id, and the window we declare for it.
|
|
26
31
|
* Ours to state: faux is not a vendor's model, so "nobody published a
|
|
27
32
|
* window" — the reason a real model's capacity is unknown — cannot apply.
|
|
@@ -61,7 +66,9 @@ export function autoCompactFromEnv() {
|
|
|
61
66
|
* THE WINDOW SOMEBODY STATED, or null when nobody has.
|
|
62
67
|
*
|
|
63
68
|
* Three sources, in order: the config/profile window, KISO_CONTEXT_WINDOW,
|
|
64
|
-
* and the metadata registry
|
|
69
|
+
* and the metadata registry (CW-1: this endpoint's row, the upstream's,
|
|
70
|
+
* then the model's own — see lookupContextWindow). Each is a claim
|
|
71
|
+
* someone made and dated. When
|
|
65
72
|
* none of them speaks, this returns NULL rather than a number — because
|
|
66
73
|
* the question "how much of the window is left" has no answer without a
|
|
67
74
|
* window, and every display that shows a percentage needs this one, not
|
|
@@ -73,11 +80,21 @@ export function autoCompactFromEnv() {
|
|
|
73
80
|
* confident `ctx left ~82%` against a figure nobody had measured.
|
|
74
81
|
*/
|
|
75
82
|
export function knownContextWindow(of) {
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
return statedContextWindow(of)?.tokens ?? null;
|
|
84
|
+
}
|
|
85
|
+
/** CW-1: `knownContextWindow` with its source — the chain the displays name.
|
|
86
|
+
* The user's figure is the live binding's unless `own` carries a
|
|
87
|
+
* profile's: the /model listing passes each row's, and a row with none
|
|
88
|
+
* must not borrow the live one (`own: { configured: undefined }` — an
|
|
89
|
+
* optional parameter defaulted to the live value would take it back,
|
|
90
|
+
* since an explicit undefined selects the default). */
|
|
91
|
+
export function statedContextWindow(of, own) {
|
|
92
|
+
const configured = own !== undefined ? own.configured : configuredWindow;
|
|
93
|
+
if (configured !== undefined)
|
|
94
|
+
return { tokens: configured, source: "set" };
|
|
78
95
|
const env = Number.parseInt(process.env.KISO_CONTEXT_WINDOW ?? "", 10);
|
|
79
96
|
if (Number.isFinite(env) && env > 0)
|
|
80
|
-
return env;
|
|
97
|
+
return { tokens: env, source: "set" };
|
|
81
98
|
const model = of?.model ?? agentModel;
|
|
82
99
|
// `faux` is OURS. The registry carries no row for it because it is not a
|
|
83
100
|
// vendor's model, but the reason the window is unknown elsewhere — nobody
|
|
@@ -86,9 +103,47 @@ export function knownContextWindow(of) {
|
|
|
86
103
|
// keeps faux mode showing a real percentage instead of the `ctx ?` that
|
|
87
104
|
// belongs to models whose capacity genuinely nobody states.
|
|
88
105
|
if (model === FAUX_MODEL)
|
|
89
|
-
return FAUX_CONTEXT_WINDOW;
|
|
90
|
-
const
|
|
91
|
-
|
|
106
|
+
return { tokens: FAUX_CONTEXT_WINDOW, source: "faux" };
|
|
107
|
+
const baseUrl = of !== undefined ? of.baseUrl : agentBaseUrl;
|
|
108
|
+
// CW-1 batch 2: what this endpoint's own refusal stated, before any
|
|
109
|
+
// statement about the model — a measurement of the route.
|
|
110
|
+
const learned = learnedWindowFor(model, baseUrl);
|
|
111
|
+
if (learned !== undefined)
|
|
112
|
+
return { tokens: learned.tokens, source: "learned", observedAt: learned.observedAt };
|
|
113
|
+
return lookupContextWindow(model, baseUrl, of?.upstream ?? upstreamOf(baseUrl));
|
|
114
|
+
}
|
|
115
|
+
/** CW-1: a window as the displays write it — `1M`, `1.05M`, `272K`,
|
|
116
|
+
* `1,048,576` (a figure that is not round is shown whole). */
|
|
117
|
+
export function windowLabel(tokens) {
|
|
118
|
+
if (tokens >= 1_000_000 && tokens % 10_000 === 0)
|
|
119
|
+
return `${tokens / 1_000_000}M`;
|
|
120
|
+
if (tokens % 1000 === 0)
|
|
121
|
+
return `${tokens / 1000}K`;
|
|
122
|
+
return tokens.toLocaleString("en-US");
|
|
123
|
+
}
|
|
124
|
+
/** CW-1: the source, said. The short form rides a /model row; the long
|
|
125
|
+
* form is /status's. Only the model step is marked in the short form —
|
|
126
|
+
* it is the one figure no row states for this endpoint. */
|
|
127
|
+
export function windowSourceNote(w, form) {
|
|
128
|
+
if (w === null)
|
|
129
|
+
return form === "short" ? "ctx ?" : `window unknown — compaction assumes ${windowLabel(DEFAULT_CONTEXT_WINDOW)}; set contextWindow on the profile to state it`;
|
|
130
|
+
const size = windowLabel(w.tokens);
|
|
131
|
+
if (form === "short")
|
|
132
|
+
return w.source === "model" ? `ctx ${size} inferred` : `ctx ${size}`;
|
|
133
|
+
switch (w.source) {
|
|
134
|
+
case "set":
|
|
135
|
+
return `window ${size}, as you set it`;
|
|
136
|
+
case "faux":
|
|
137
|
+
return `window ${size} (faux)`;
|
|
138
|
+
case "learned":
|
|
139
|
+
return `window ${size}, learned from this endpoint's refusal (${w.observedAt ?? "date unknown"})`;
|
|
140
|
+
case "route":
|
|
141
|
+
return `window ${size}, the registry's for this endpoint`;
|
|
142
|
+
case "upstream":
|
|
143
|
+
return `window ${size}, the registry's for the upstream`;
|
|
144
|
+
case "model":
|
|
145
|
+
return `window ${size}, inferred from the model (${w.from}) — not stated for this endpoint`;
|
|
146
|
+
}
|
|
92
147
|
}
|
|
93
148
|
export function contextWindowTokens(of) {
|
|
94
149
|
const windowOverride = configuredWindow;
|
|
@@ -112,9 +167,21 @@ export function contextWindowTokens(of) {
|
|
|
112
167
|
// asked again. The claim is true now because the switch path calls
|
|
113
168
|
// `microcompactThresholdFor` below, not because deriving a number
|
|
114
169
|
// from this function makes anything follow it.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
170
|
+
//
|
|
171
|
+
// CW-1: the registry's steps are lookupContextWindow's — the route's row,
|
|
172
|
+
// the upstream's, then the model's own identity — the same chain
|
|
173
|
+
// `statedContextWindow` names for the displays.
|
|
174
|
+
const baseUrl = of !== undefined ? of.baseUrl : agentBaseUrl;
|
|
175
|
+
// CW-1 batch 2: faux is declared here too — it read the old 200K
|
|
176
|
+
// fallback by accident, and the fallback moving must not move it.
|
|
177
|
+
if ((of?.model ?? agentModel) === FAUX_MODEL)
|
|
178
|
+
return FAUX_CONTEXT_WINDOW;
|
|
179
|
+
const learned = learnedWindowFor(of?.model ?? agentModel, baseUrl);
|
|
180
|
+
if (learned !== undefined)
|
|
181
|
+
return learned.tokens;
|
|
182
|
+
const known = lookupContextWindow(of?.model ?? agentModel, baseUrl, upstreamOf(baseUrl));
|
|
183
|
+
if (known !== null)
|
|
184
|
+
return known.tokens;
|
|
118
185
|
return DEFAULT_CONTEXT_WINDOW;
|
|
119
186
|
}
|
|
120
187
|
/**
|
|
@@ -167,11 +234,17 @@ export function statusModelLabel(session) {
|
|
|
167
234
|
return effort !== undefined && effort !== "default" ? `${name} · ${effort}` : name;
|
|
168
235
|
}
|
|
169
236
|
/** ADR-0055 Amendment 2 (decision 5): with no stated window the status row
|
|
170
|
-
* shows `ctx ?`, yet the compaction tiers still assume the
|
|
237
|
+
* shows `ctx ?`, yet the compaction tiers still assume the fallback (128K
|
|
238
|
+
* since CW-1 batch 2)
|
|
171
239
|
* — the assumption is said out loud once, at agent build. */
|
|
172
240
|
export function unknownWindowNotice(model) {
|
|
173
241
|
return `[kiso] context window unknown for ${model} at this endpoint — compaction assumes ${DEFAULT_CONTEXT_WINDOW / 1000}K; set contextWindow on the profile to state it`;
|
|
174
242
|
}
|
|
243
|
+
/** CW-1 batch 2: said once, when an endpoint's refusal states its cap — the
|
|
244
|
+
* tiers aim below it from the next request, and later sessions start on it. */
|
|
245
|
+
export function windowLearnedNotice(model, host, tokens) {
|
|
246
|
+
return `✦ window learned — ${host === "" ? "the endpoint" : host} refused ${model} past ${windowLabel(tokens)} tokens; compaction now aims below it`;
|
|
247
|
+
}
|
|
175
248
|
/** ADR-0055 Amendment 2: the notice for a checkpoint the shrink invariant
|
|
176
249
|
* discarded — chars/4 sizes only; the checkpoint's text is the owner's
|
|
177
250
|
* work and never reaches the screen. */
|
|
@@ -179,7 +252,7 @@ export function compactionDiscardedNotice(d) {
|
|
|
179
252
|
return `✦ compaction discarded — the checkpoint did not shrink the context (~${kUnit(d.pre)} → ~${kUnit(d.post)}; it wrote ~${kUnit(d.summary)})`;
|
|
180
253
|
}
|
|
181
254
|
export function displayCtxRatio(session) {
|
|
182
|
-
// CAPACITY is not POLICY. `contextWindowTokens` falls back to
|
|
255
|
+
// CAPACITY is not POLICY. `contextWindowTokens` falls back to 128,000 so
|
|
183
256
|
// that the compaction threshold always HAS a value — a policy needs a
|
|
184
257
|
// number. A percentage on screen is a different kind of thing: it is a
|
|
185
258
|
// claim about the model, and an unstated window makes it unanswerable.
|
|
@@ -385,6 +458,43 @@ export function turnUsageLedger() {
|
|
|
385
458
|
},
|
|
386
459
|
};
|
|
387
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* The per-CALL view of usage reports for the two figures that must not
|
|
463
|
+
* double: the cache-miss estimate and the session's running cost.
|
|
464
|
+
*
|
|
465
|
+
* A call may report its usage more than once — the op gateway sent two
|
|
466
|
+
* usage chunks for one request (the owner's session
|
|
467
|
+
* 2026-09-23T03-00-01-230c: `fresh 3k · miss 3k` on a first turn, both
|
|
468
|
+
* reports 3,041 in / 69 out). The ledger above already makes a call's
|
|
469
|
+
* LATEST report replace the earlier; the miss and the cost were read per
|
|
470
|
+
* EVENT, so the second report compared itself with the first and called
|
|
471
|
+
* the whole prompt a miss, and a priced route would have been charged
|
|
472
|
+
* twice. Here the carrier is the PREVIOUS call's total, and a call's cost
|
|
473
|
+
* is added as the difference from what that call already added.
|
|
474
|
+
*/
|
|
475
|
+
export function callUsageMeter() {
|
|
476
|
+
let prevCallTotal = null;
|
|
477
|
+
let callTotal = null;
|
|
478
|
+
let callCost = null;
|
|
479
|
+
return {
|
|
480
|
+
carrier: () => prevCallTotal,
|
|
481
|
+
report(total, costUsd) {
|
|
482
|
+
if (total !== null)
|
|
483
|
+
callTotal = total;
|
|
484
|
+
if (costUsd === null)
|
|
485
|
+
return null;
|
|
486
|
+
const add = costUsd - (callCost ?? 0);
|
|
487
|
+
callCost = costUsd;
|
|
488
|
+
return add;
|
|
489
|
+
},
|
|
490
|
+
endCall() {
|
|
491
|
+
if (callTotal !== null)
|
|
492
|
+
prevCallTotal = callTotal;
|
|
493
|
+
callTotal = null;
|
|
494
|
+
callCost = null;
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
388
498
|
/** v2b: the spinner merged into the STATUS BAR (the v2a standalone glyph
|
|
389
499
|
* is gone) — docked only, 200ms rotation between the request and the
|
|
390
500
|
* first event. */
|
|
@@ -807,7 +917,7 @@ submitTurn) {
|
|
|
807
917
|
// and read at that call's usage event. Per CALL, not per turn — a turn
|
|
808
918
|
// with three model calls reports the third, and each one times itself.
|
|
809
919
|
let callFirstEventAt = null;
|
|
810
|
-
|
|
920
|
+
const meter = callUsageMeter();
|
|
811
921
|
let missed = null;
|
|
812
922
|
// v3 §02: the recap line derives ENTIRELY from the local event stream
|
|
813
923
|
// (zero tokens). R3g: what it derives is the turn's COST — wall
|
|
@@ -844,6 +954,8 @@ submitTurn) {
|
|
|
844
954
|
// the stream watchdog and the repeated-failure breaker remain active.
|
|
845
955
|
// W22-R1: the ledger sees every event; a stop is what ends a call.
|
|
846
956
|
ledger.observe(ev);
|
|
957
|
+
if (ev.type === "stop")
|
|
958
|
+
meter.endCall();
|
|
847
959
|
// ADR-0055 Amendment 1 (A1b): a compaction inside the run says so, once.
|
|
848
960
|
if (ev.type === "summarized" || ev.type === "microcompacted") {
|
|
849
961
|
const r = displayCtxRatio(session);
|
|
@@ -960,11 +1072,12 @@ submitTurn) {
|
|
|
960
1072
|
body.notice("stream interrupted — the draft above is abandoned");
|
|
961
1073
|
break;
|
|
962
1074
|
case "usage": {
|
|
963
|
-
const delta = usageFromEvent(session.provider, ev,
|
|
1075
|
+
const delta = usageFromEvent(session.provider, ev, meter.carrier(), agentModel, session.baseUrl);
|
|
964
1076
|
// W22-R1: this call's LATEST report replaces its earlier one;
|
|
965
|
-
// the turn's figure is the settled calls plus this one.
|
|
1077
|
+
// the turn's figure is the settled calls plus this one. The
|
|
1078
|
+
// miss and the cost follow the same rule (callUsageMeter).
|
|
966
1079
|
ledger.report(delta.usage);
|
|
967
|
-
|
|
1080
|
+
const costToAdd = meter.report(delta.total, delta.costUsd);
|
|
968
1081
|
missed = delta.missed;
|
|
969
1082
|
// TUI2-R1 (E): the request's canonical cost rides the same
|
|
970
1083
|
// callback the usage does — one settled request, one addition.
|
|
@@ -973,7 +1086,7 @@ submitTurn) {
|
|
|
973
1086
|
// second call is timed from ITS own first event.
|
|
974
1087
|
const tokPerSec = callFirstEventAt === null ? null : decodeRate(ev.outputTokens, Date.now() - callFirstEventAt);
|
|
975
1088
|
callFirstEventAt = null;
|
|
976
|
-
statusCb?.(turnUsage() ?? UNKNOWN_USAGE, displayCtxRatio(session),
|
|
1089
|
+
statusCb?.(turnUsage() ?? UNKNOWN_USAGE, displayCtxRatio(session), costToAdd, tokPerSec);
|
|
977
1090
|
break;
|
|
978
1091
|
}
|
|
979
1092
|
case "uncertain_pending":
|
package/dist/config.d.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* direct write (provider: "openai-compat" |
|
|
12
12
|
* "anthropic" | "openai-responses")
|
|
13
13
|
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
14
|
-
* baseUrl?: string, model: string, apiKeyEnv: string
|
|
14
|
+
* baseUrl?: string, model: string, apiKeyEnv: string,
|
|
15
|
+
* upstream?: string } } — upstream: never where requests go
|
|
15
16
|
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"|"dontAsk"
|
|
16
17
|
* contextWindow?: number — tokens
|
|
17
18
|
* autoCompact?: { thresholdRatio: number } — 0<r<1; default off
|
|
@@ -64,6 +65,26 @@ export interface ModelProfile {
|
|
|
64
65
|
* registry, where every figure has to carry a source.
|
|
65
66
|
*/
|
|
66
67
|
readonly contextWindow?: number;
|
|
68
|
+
/**
|
|
69
|
+
* What this endpoint FORWARDS to, when `baseUrl` is a local proxy — a URL
|
|
70
|
+
* (`https://gateway.example/v1`) or a name (`my gateway`). It never
|
|
71
|
+
* moves a request — they still go to `baseUrl`. The owner, 2026-09-23: a
|
|
72
|
+
* profile behind a local forwarder showed `@127.0.0.1:47821`, which says
|
|
73
|
+
* where the bytes go and not who is billed; the row now reads
|
|
74
|
+
* `@gateway.example via 127.0.0.1:47821`. CW-1: a URL here is also where
|
|
75
|
+
* the registry looks for this model's window when `baseUrl` has no row.
|
|
76
|
+
*/
|
|
77
|
+
readonly upstream?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Headers the endpoint needs on every request — a gateway that routes a
|
|
80
|
+
* conversation by a session header, say. `{session}` in a value becomes
|
|
81
|
+
* the kiso session's id, so one conversation keeps one id across /model
|
|
82
|
+
* and /resume, and two conversations never share one. Names are
|
|
83
|
+
* lower-cased. A credential header is refused by name: a key never lives
|
|
84
|
+
* in this file (`apiKeyEnv` and `kiso login` are the doors), and the
|
|
85
|
+
* framing headers belong to the adapter.
|
|
86
|
+
*/
|
|
87
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
67
88
|
}
|
|
68
89
|
export interface AutoCompactConfig {
|
|
69
90
|
readonly thresholdRatio: number;
|
package/dist/config.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* direct write (provider: "openai-compat" |
|
|
12
12
|
* "anthropic" | "openai-responses")
|
|
13
13
|
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
14
|
-
* baseUrl?: string, model: string, apiKeyEnv: string
|
|
14
|
+
* baseUrl?: string, model: string, apiKeyEnv: string,
|
|
15
|
+
* upstream?: string } } — upstream: never where requests go
|
|
15
16
|
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"|"dontAsk"
|
|
16
17
|
* contextWindow?: number — tokens
|
|
17
18
|
* autoCompact?: { thresholdRatio: number } — 0<r<1; default off
|
|
@@ -33,6 +34,27 @@ import { join } from "node:path";
|
|
|
33
34
|
import { kisoHome } from "./state.js";
|
|
34
35
|
import { AuthError, getCredential, providerIdOf } from "./auth/credentials.js";
|
|
35
36
|
import { BUILTIN_MANIFESTS } from "@vincemakes/kiso-runtime/internal";
|
|
37
|
+
/** Header names a profile may not set: credentials (a key never lives in the
|
|
38
|
+
* config file) and the framing the adapter owns. */
|
|
39
|
+
const REFUSED_HEADERS = new Set(["authorization", "proxy-authorization", "x-api-key", "api-key", "cookie", "host", "content-length", "content-type", "transfer-encoding", "connection"]);
|
|
40
|
+
function parseHeaders(field, raw, fail) {
|
|
41
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
42
|
+
fail(field, "expected an object of header names to string values");
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
45
|
+
if (!/^[A-Za-z0-9-]+$/.test(name))
|
|
46
|
+
fail(`${field}.${name}`, "expected a header name of letters, digits and dashes");
|
|
47
|
+
const lower = name.toLowerCase();
|
|
48
|
+
if (REFUSED_HEADERS.has(lower))
|
|
49
|
+
fail(`${field}.${name}`, "a credential or framing header cannot be set here — the key comes from apiKeyEnv or kiso login");
|
|
50
|
+
if (lower in out)
|
|
51
|
+
fail(`${field}.${name}`, "named twice (header names are case-insensitive)");
|
|
52
|
+
if (typeof value !== "string" || /[\r\n\0]/.test(value))
|
|
53
|
+
fail(`${field}.${name}`, "expected a one-line string value");
|
|
54
|
+
out[lower] = value;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
36
58
|
const KINDS = ["openai-compat", "anthropic", "openai-responses"];
|
|
37
59
|
const MODES_LIST = ["manual", "default", "accept-edits", "plan", "bypass", "dontAsk"];
|
|
38
60
|
export class ConfigError extends Error {
|
|
@@ -146,6 +168,9 @@ export function parseConfig(text, source) {
|
|
|
146
168
|
// your profile") had never done anything.
|
|
147
169
|
if (p.contextWindow !== undefined && (typeof p.contextWindow !== "number" || !Number.isFinite(p.contextWindow) || p.contextWindow <= 0))
|
|
148
170
|
fail(`models.${name}.contextWindow`, "expected a positive token count");
|
|
171
|
+
if (p.upstream !== undefined && (typeof p.upstream !== "string" || p.upstream.trim() === ""))
|
|
172
|
+
fail(`models.${name}.upstream`, "expected the URL or name of what this endpoint forwards to");
|
|
173
|
+
const headers = p.headers === undefined ? undefined : parseHeaders(`models.${name}.headers`, p.headers, fail);
|
|
149
174
|
models[name] = {
|
|
150
175
|
kind: p.kind,
|
|
151
176
|
model: p.model,
|
|
@@ -154,6 +179,8 @@ export function parseConfig(text, source) {
|
|
|
154
179
|
...(typeof p.promptCaching === "boolean" ? { promptCaching: p.promptCaching } : {}),
|
|
155
180
|
...(typeof p.streamIdleMs === "number" ? { streamIdleMs: p.streamIdleMs } : {}),
|
|
156
181
|
...(typeof p.contextWindow === "number" ? { contextWindow: p.contextWindow } : {}),
|
|
182
|
+
...(typeof p.upstream === "string" ? { upstream: p.upstream.trim() } : {}),
|
|
183
|
+
...(headers !== undefined ? { headers } : {}),
|
|
157
184
|
};
|
|
158
185
|
}
|
|
159
186
|
out.models = models;
|
package/dist/dispatch.js
CHANGED
|
@@ -9,11 +9,11 @@ import { buildAdapter, lookupModelMetadata, resolveContinuationScope, resolveRea
|
|
|
9
9
|
import { MODES, MODE_NOTE, OFFERED_MODES, getMode, setMode } from "./mode.js";
|
|
10
10
|
import { clipboardWrite, lastAnswer } from "./clipboard.js";
|
|
11
11
|
import { protectedBangReason, protectedShellVerdict } from "./protected-shell.js";
|
|
12
|
-
import { agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, setLastBinding } from "./state.js";
|
|
12
|
+
import { agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, upstreamOf, setLastBinding } from "./state.js";
|
|
13
13
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
14
14
|
import { profileProviderLabel, providerLabel } from "./provider-label.js";
|
|
15
15
|
import { queuedSwitchLines } from "./state.js";
|
|
16
|
-
import { contextWindowTokens, microcompactThresholdFor, startStatusSpinner } from "./chat.js";
|
|
16
|
+
import { contextWindowTokens, microcompactThresholdFor, startStatusSpinner, statedContextWindow, windowSourceNote } from "./chat.js";
|
|
17
17
|
import { authForProfile, directWriteProfile, profileAvailable, resolveContextWindow, unavailableReason } from "./config.js";
|
|
18
18
|
import { shellTool } from "@vincemakes/kiso-tools-node";
|
|
19
19
|
import { dirname, join } from "node:path";
|
|
@@ -109,6 +109,14 @@ function signInNote(p) {
|
|
|
109
109
|
}
|
|
110
110
|
return p.apiKeyEnv ?? "no key";
|
|
111
111
|
}
|
|
112
|
+
/** CW-1 (owner, 2026-09-23): a profile's window on its /model row — the
|
|
113
|
+
* profile's own stated figure first, then the registry's chain for its
|
|
114
|
+
* model, endpoint and upstream; `inferred` marks the one no row states
|
|
115
|
+
* for that endpoint. */
|
|
116
|
+
function windowNoteOf(p) {
|
|
117
|
+
const w = statedContextWindow({ model: p.model, ...(p.baseUrl !== undefined ? { baseUrl: p.baseUrl } : {}), ...(p.upstream !== undefined ? { upstream: p.upstream } : {}) }, { configured: resolveContextWindow(mergedConfig, p) });
|
|
118
|
+
return windowSourceNote(w, "short");
|
|
119
|
+
}
|
|
112
120
|
/** The ONE dispatcher — slash commands, exit, and turns. The recovery
|
|
113
121
|
* replay routes through it too — a queued "/last" must never become a
|
|
114
122
|
* user turn (v2c: the rl lives in main, so lines arrive earlier and the
|
|
@@ -487,7 +495,10 @@ export function dispatch(line, ctx) {
|
|
|
487
495
|
const ctxPct = Number.isFinite(ctxRatio) ? `~${Math.round(ctxRatio * 100)}%` : "~?";
|
|
488
496
|
bodyLog(`session ${ctx.session.id}`);
|
|
489
497
|
bodyLog(`${ctx.session.log.all.length} events`);
|
|
490
|
-
|
|
498
|
+
// CW-1: the percentage names its denominator and who stated it — a
|
|
499
|
+
// window inferred from the model reads differently from one the
|
|
500
|
+
// registry states for this endpoint.
|
|
501
|
+
bodyLog(`ctx ${ctxPct} · ${windowSourceNote(statedContextWindow(), "long")}`);
|
|
491
502
|
// The owner, 2026-09-21: /status is where "what am I actually
|
|
492
503
|
// running on" is answered, so the identity is spelled out HERE —
|
|
493
504
|
// the model, the host the request goes to, the profile it came
|
|
@@ -502,7 +513,7 @@ export function dispatch(line, ctx) {
|
|
|
502
513
|
// restored, not switched. (The real fix is to stop overloading that
|
|
503
514
|
// variable; until then, a label that cannot lie.)
|
|
504
515
|
const profileOf = currentProfileName === null ? "" : ` · profile ${currentProfileName}`;
|
|
505
|
-
bodyLog(`model ${agentModel}${providerLabel(agentBaseUrl)}${profileOf}`);
|
|
516
|
+
bodyLog(`model ${agentModel}${providerLabel(agentBaseUrl, upstreamOf(agentBaseUrl))}${profileOf}`);
|
|
506
517
|
ctx.input.prompt();
|
|
507
518
|
});
|
|
508
519
|
return;
|
|
@@ -607,8 +618,10 @@ export function dispatch(line, ctx) {
|
|
|
607
618
|
// profile key is what `/model <name>` takes, not what a chooser
|
|
608
619
|
// reading a list needs. What remains is what the chooser CANNOT
|
|
609
620
|
// infer: availability, and which one is live.
|
|
610
|
-
|
|
611
|
-
|
|
621
|
+
// CW-1: the window rides LAST — availability and the live mark
|
|
622
|
+
// are what a narrow row must keep.
|
|
623
|
+
const marks = [...(profileAvailable(profile) ? [] : ["unavailable"]), ...(name === currentProfileName ? ["current"] : []), windowNoteOf(profile)];
|
|
624
|
+
const host = profileProviderLabel(profile.kind, profile.baseUrl, profile.upstream);
|
|
612
625
|
return { label: `${profile.kind}/${profile.model}${host === "" ? "" : ` ${host}`}`, note: marks.join(" · "), ...effortAxis(profile) };
|
|
613
626
|
}),
|
|
614
627
|
// PH-1a (finding PH-F4): the example must be a syntax
|
|
@@ -658,8 +671,8 @@ export function dispatch(line, ctx) {
|
|
|
658
671
|
else {
|
|
659
672
|
for (const name of names) {
|
|
660
673
|
const p = configModels[name];
|
|
661
|
-
const hostOf = profileProviderLabel(p.kind, p.baseUrl);
|
|
662
|
-
bodyLog(` ${name} → ${p.kind}/${p.model}${hostOf === "" ? "" : ` ${hostOf}`} · ${signInNote(p)} ${profileAvailable(p) ? "(available)" : "(unavailable)"} · ${effortNote(p)}`);
|
|
674
|
+
const hostOf = profileProviderLabel(p.kind, p.baseUrl, p.upstream);
|
|
675
|
+
bodyLog(` ${name} → ${p.kind}/${p.model}${hostOf === "" ? "" : ` ${hostOf}`} · ${signInNote(p)} ${profileAvailable(p) ? "(available)" : "(unavailable)"} · ${effortNote(p)} · ${windowNoteOf(p)}`);
|
|
663
676
|
}
|
|
664
677
|
}
|
|
665
678
|
bodyLog("switch with /model <profile-name|provider/model>");
|
|
@@ -775,7 +788,7 @@ export function dispatch(line, ctx) {
|
|
|
775
788
|
// (and the owner's) read `gpt-6-astra · CH 92%`, the new
|
|
776
789
|
// model beside the previous model's figure.
|
|
777
790
|
ctx.modelSwitched();
|
|
778
|
-
body.notice(`model → ${profName} (${profile.model}${providerLabel(profile.baseUrl) === "" ? "" : ` ${providerLabel(profile.baseUrl)}`}${effortTok !== undefined ? ` · ${effortTok}` : ""}) — takes effect on the next turn`);
|
|
791
|
+
body.notice(`model → ${profName} (${profile.model}${providerLabel(profile.baseUrl, profile.upstream) === "" ? "" : ` ${providerLabel(profile.baseUrl, profile.upstream)}`}${effortTok !== undefined ? ` · ${effortTok}` : ""}) — takes effect on the next turn`);
|
|
779
792
|
}
|
|
780
793
|
}
|
|
781
794
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface EmptySession {
|
|
2
|
+
/** The session folder (a project folder, or the legacy sessions dir). */
|
|
3
|
+
readonly dir: string;
|
|
4
|
+
readonly id: string;
|
|
5
|
+
/** The files that name it, relative to `dir`. */
|
|
6
|
+
readonly files: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export interface EmptyScan {
|
|
9
|
+
readonly empty: readonly EmptySession[];
|
|
10
|
+
/** Empty by the files, but a live process holds the lock. */
|
|
11
|
+
readonly inUse: number;
|
|
12
|
+
}
|
|
13
|
+
/** A lock whose pid answers signal 0 belongs to a running process. */
|
|
14
|
+
export declare function pidAlive(pid: number): boolean;
|
|
15
|
+
/** Every session in `dirs` with a sidecar and no log. Reads only. */
|
|
16
|
+
export declare function findEmptySessions(dirs: readonly string[], alive?: (pid: number) => boolean): EmptyScan;
|
|
17
|
+
/** The platform's Trash: ~/.Trash on macOS, the freedesktop one elsewhere. */
|
|
18
|
+
export declare function defaultTrashRoot(): string;
|
|
19
|
+
/** Move the sessions into one new folder under `trashRoot`, keeping each
|
|
20
|
+
* session folder's name; returns that folder. Never deletes: a file that
|
|
21
|
+
* cannot be renamed across devices is copied, then unlinked. */
|
|
22
|
+
export declare function moveToTrash(sessions: readonly EmptySession[], trashRoot: string, now?: Date): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DC-60 — the empty sessions an older kiso left behind.
|
|
3
|
+
*
|
|
4
|
+
* Until DC-60 every process that opened a session wrote its sidecar (and,
|
|
5
|
+
* through the start-up recovery, a trace) whether or not anything ever
|
|
6
|
+
* happened in it: a started-and-closed kiso, a /resume that went nowhere, a
|
|
7
|
+
* delegated child that failed before its first turn. Each left a sidecar
|
|
8
|
+
* with no log, and each showed up in /resume as a card with no summary.
|
|
9
|
+
* The owner's disk held 28 on 2026-09-23.
|
|
10
|
+
*
|
|
11
|
+
* `kiso sessions --prune-empty` lists them. With `--yes` it MOVES them to
|
|
12
|
+
* the Trash — never deletes — keeping each project folder's name, so a
|
|
13
|
+
* mistake is a drag back. A session whose lock names a live process is
|
|
14
|
+
* skipped: something is using it right now.
|
|
15
|
+
*/
|
|
16
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { basename, join } from "node:path";
|
|
19
|
+
const SIDECAR = ".meta.json";
|
|
20
|
+
/** A lock whose pid answers signal 0 belongs to a running process. */
|
|
21
|
+
export function pidAlive(pid) {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return err.code === "EPERM";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function lockPid(path) {
|
|
31
|
+
try {
|
|
32
|
+
const pid = JSON.parse(readFileSync(path, "utf8")).pid;
|
|
33
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Every session in `dirs` with a sidecar and no log. Reads only. */
|
|
40
|
+
export function findEmptySessions(dirs, alive = pidAlive) {
|
|
41
|
+
const empty = [];
|
|
42
|
+
let inUse = 0;
|
|
43
|
+
for (const dir of dirs) {
|
|
44
|
+
let names;
|
|
45
|
+
try {
|
|
46
|
+
names = readdirSync(dir);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
if (!name.endsWith(SIDECAR))
|
|
53
|
+
continue;
|
|
54
|
+
const id = name.slice(0, -SIDECAR.length);
|
|
55
|
+
if (names.includes(`${id}.jsonl`))
|
|
56
|
+
continue;
|
|
57
|
+
const lock = `${id}.lock`;
|
|
58
|
+
if (names.includes(lock)) {
|
|
59
|
+
const pid = lockPid(join(dir, lock));
|
|
60
|
+
if (pid !== null && alive(pid)) {
|
|
61
|
+
inUse += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const files = [name];
|
|
66
|
+
if (names.includes(lock))
|
|
67
|
+
files.push(lock);
|
|
68
|
+
if (existsSync(join(dir, "traces", `${id}.jsonl`)))
|
|
69
|
+
files.push(join("traces", `${id}.jsonl`));
|
|
70
|
+
empty.push({ dir, id, files });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { empty, inUse };
|
|
74
|
+
}
|
|
75
|
+
/** The platform's Trash: ~/.Trash on macOS, the freedesktop one elsewhere. */
|
|
76
|
+
export function defaultTrashRoot() {
|
|
77
|
+
if (process.platform === "darwin")
|
|
78
|
+
return join(homedir(), ".Trash");
|
|
79
|
+
return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "Trash", "files");
|
|
80
|
+
}
|
|
81
|
+
/** Move the sessions into one new folder under `trashRoot`, keeping each
|
|
82
|
+
* session folder's name; returns that folder. Never deletes: a file that
|
|
83
|
+
* cannot be renamed across devices is copied, then unlinked. */
|
|
84
|
+
export function moveToTrash(sessions, trashRoot, now = new Date()) {
|
|
85
|
+
const target = join(trashRoot, `kiso-empty-sessions-${now.toISOString().replace(/[:.]/g, "-")}`);
|
|
86
|
+
for (const s of sessions) {
|
|
87
|
+
for (const file of s.files) {
|
|
88
|
+
const to = join(target, basename(s.dir), file);
|
|
89
|
+
mkdirSync(join(to, ".."), { recursive: true });
|
|
90
|
+
try {
|
|
91
|
+
renameSync(join(s.dir, file), to);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err.code !== "EXDEV")
|
|
95
|
+
throw err;
|
|
96
|
+
copyFileSync(join(s.dir, file), to);
|
|
97
|
+
unlinkSync(join(s.dir, file));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return target;
|
|
102
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -29,13 +29,14 @@ import { newSessionId } from "./session-id.js";
|
|
|
29
29
|
import { createInterface } from "node:readline";
|
|
30
30
|
import { Readable } from "node:stream";
|
|
31
31
|
import { fileURLToPath } from "node:url";
|
|
32
|
-
import { join } from "node:path";
|
|
32
|
+
import { basename, join } from "node:path";
|
|
33
33
|
import { Body, Editor, PROMPT, bannerLines, currentGround, resolveGround, setGround, escapeTerminal, extensionsBannerText, idColumn, idleStatus, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListHeader, sessionListRow, sessionListUnknownLine, slashCommandNames } from "@vincemakes/kiso-tui";
|
|
34
34
|
import { createAgent, disposeExtensions, loadExtensions, loadProjectExtensions, SessionStore, } from "@vincemakes/kiso-runtime";
|
|
35
35
|
import { listSessionSidecars, migrateSummaries, readProfile, runsACheck, summaryMigrationPending } from "@vincemakes/kiso-runtime/internal";
|
|
36
36
|
import { skillMenuItems } from "./skill-invoke.js";
|
|
37
37
|
import { canonicalPath, claimProjectDir, hasSession, locateSession, projectLayoutActive, sessionFolders } from "./projects.js";
|
|
38
38
|
import { migrationNotice, pendingLegacyIds, planMigration, reverseMigration, runMigration } from "./session-migration.js";
|
|
39
|
+
import { defaultTrashRoot, findEmptySessions, moveToTrash } from "./empty-sessions.js";
|
|
39
40
|
import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
40
41
|
import { createCodingTools, isProtectedPath, protectedIdentity, PROTECTED_REFUSAL } from "@vincemakes/kiso-tools-node";
|
|
41
42
|
import { MODES, OFFERED_MODES, getMode, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
|
|
@@ -50,7 +51,9 @@ import { maxRetriesFromEnv } from "./retries.js";
|
|
|
50
51
|
import { askUi, resolveProjectTrust } from "./trust-ui.js";
|
|
51
52
|
import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
|
|
52
53
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
53
|
-
import { chat, compactionDiscardedNotice, contextWindowTokens, displayCtxRatio, knownContextWindow, microcompactThresholdFor, statusModelLabel, unknownWindowNotice } from "./chat.js";
|
|
54
|
+
import { chat, compactionDiscardedNotice, contextWindowTokens, displayCtxRatio, knownContextWindow, microcompactThresholdFor, statusModelLabel, unknownWindowNotice, windowLearnedNotice } from "./chat.js";
|
|
55
|
+
import { recordLearnedWindow, useLearnedWindows } from "./learned-windows.js";
|
|
56
|
+
import { providerHost } from "./provider-label.js";
|
|
54
57
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
55
58
|
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
|
|
56
59
|
import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
|
|
@@ -775,6 +778,9 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
775
778
|
// shell tool would then write into this project's folder.
|
|
776
779
|
process.env.KISO_DELEGATION_CONFIG_JSON = JSON.stringify({ checks: merged.checks ?? {}, profiles: Object.keys(merged.models ?? {}), sessionsDir: sessionsDir() });
|
|
777
780
|
setConfigModels(merged.models ?? {});
|
|
781
|
+
// CW-1 batch 2: the windows endpoints stated by refusing — read before the
|
|
782
|
+
// first window is asked for (the unknown-window notice below).
|
|
783
|
+
useLearnedWindows();
|
|
778
784
|
const resolved = resolveModel(modelFlag, merged);
|
|
779
785
|
// AFTER the model resolves: the window a PROFILE states is about that
|
|
780
786
|
// profile's model, so it cannot be read before we know which profile is
|
|
@@ -795,7 +801,8 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
795
801
|
}
|
|
796
802
|
setAgentModel(model, resolved?.profile.baseUrl); // v2b: the status bar shows it; OR-1: the endpoint rides along
|
|
797
803
|
// ADR-0055 Amendment 2: the row says `ctx ?` for an unstated window, but
|
|
798
|
-
// the tiers still need a number and assume
|
|
804
|
+
// the tiers still need a number and assume the fallback — say so, once,
|
|
805
|
+
// at build.
|
|
799
806
|
if (!unknownWindowNoticed && knownContextWindow() === null) {
|
|
800
807
|
unknownWindowNoticed = true;
|
|
801
808
|
console.error(unknownWindowNotice(model));
|
|
@@ -873,8 +880,16 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
873
880
|
onDiscard: (d) => body.notice(compactionDiscardedNotice(d)),
|
|
874
881
|
// ADR-0055 Amendment 2 (decision 3): only a window someone stated
|
|
875
882
|
// arms the overflow belt — read from the LIVE binding, so it moves
|
|
876
|
-
// with /model and /resume; the
|
|
883
|
+
// with /model and /resume; the fallback reads as null.
|
|
877
884
|
statedWindow: () => knownContextWindow(),
|
|
885
|
+
// CW-1 batch 2: an endpoint's refusal stated its cap. The tiers
|
|
886
|
+
// took it already; kept, it is where the next session starts, and
|
|
887
|
+
// the status row's denominator moves with it. Said once per new
|
|
888
|
+
// figure — a refusal at a cap already kept says nothing.
|
|
889
|
+
onWindowLearned: (w) => {
|
|
890
|
+
if (recordLearnedWindow(w.model, w.baseUrl, w.tokens))
|
|
891
|
+
body.notice(windowLearnedNotice(w.model, providerHost(w.baseUrl) ?? "", w.tokens));
|
|
892
|
+
},
|
|
878
893
|
},
|
|
879
894
|
},
|
|
880
895
|
// R3e (owner ruling, 2026-08-28): NO turn limit on an interactive
|
|
@@ -1662,7 +1677,18 @@ async function main() {
|
|
|
1662
1677
|
// = each form's default (TTY: current; pipe: all, today's bytes).
|
|
1663
1678
|
let listScope;
|
|
1664
1679
|
let reverseManifest;
|
|
1680
|
+
// DC-60: `kiso sessions --prune-empty [--yes]` — list the sessions that
|
|
1681
|
+
// never began; with --yes, move them to the Trash
|
|
1682
|
+
let pruneEmpty;
|
|
1665
1683
|
if (args[0] === "sessions") {
|
|
1684
|
+
const pe = args.indexOf("--prune-empty");
|
|
1685
|
+
if (pe !== -1) {
|
|
1686
|
+
args.splice(pe, 1);
|
|
1687
|
+
const yes = args.indexOf("--yes");
|
|
1688
|
+
if (yes !== -1)
|
|
1689
|
+
args.splice(yes, 1);
|
|
1690
|
+
pruneEmpty = yes !== -1 ? "move" : "list";
|
|
1691
|
+
}
|
|
1666
1692
|
// 0.40.0: the undo of the per-project move, named by its manifest
|
|
1667
1693
|
const r = args.indexOf("--reverse-migration");
|
|
1668
1694
|
if (r !== -1) {
|
|
@@ -1890,6 +1916,25 @@ async function main() {
|
|
|
1890
1916
|
// "reading 'question'" on the dock-less branch). The input
|
|
1891
1917
|
// exists so the gate's ask can be answered; the listing
|
|
1892
1918
|
// itself never touches it.
|
|
1919
|
+
if (pruneEmpty !== undefined) {
|
|
1920
|
+
// DC-60: before makeAgent — the scan only reads, and nothing
|
|
1921
|
+
// needs an agent to move a file to the Trash
|
|
1922
|
+
const { empty, inUse } = findEmptySessions(listingFolders().map((f) => f.dir));
|
|
1923
|
+
const n = (k, w) => `${k} ${w}${k === 1 ? "" : "s"}`;
|
|
1924
|
+
if (empty.length === 0)
|
|
1925
|
+
console.log("no empty sessions");
|
|
1926
|
+
else if (pruneEmpty === "list") {
|
|
1927
|
+
console.log(`${n(empty.length, "empty session")} — a sidecar and no log; none of them ever ran:`);
|
|
1928
|
+
for (const s of empty)
|
|
1929
|
+
console.log(` ${basename(s.dir)}/${s.id}`);
|
|
1930
|
+
console.log("kiso sessions --prune-empty --yes moves them to the Trash");
|
|
1931
|
+
}
|
|
1932
|
+
else
|
|
1933
|
+
console.log(`moved ${n(empty.length, "empty session")} to ${moveToTrash(empty, defaultTrashRoot())}`);
|
|
1934
|
+
if (inUse > 0)
|
|
1935
|
+
console.log(`skipped ${n(inUse, "empty session")} in use — a live process holds the lock`);
|
|
1936
|
+
break;
|
|
1937
|
+
}
|
|
1893
1938
|
if (reverseManifest !== undefined) {
|
|
1894
1939
|
// before makeAgent: its folder preparation would otherwise run
|
|
1895
1940
|
// the very move this undoes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface LearnedWindow {
|
|
2
|
+
readonly tokens: number;
|
|
3
|
+
/** the day the refusal was read, YYYY-MM-DD */
|
|
4
|
+
readonly observedAt: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function learnedWindowsPath(home?: string): string;
|
|
7
|
+
/** The key for one model at one endpoint; no baseUrl is the provider's own. */
|
|
8
|
+
export declare function learnedKey(model: string, baseUrl: string | undefined): string;
|
|
9
|
+
/** Startup: read the file and keep figures learned from here on in it.
|
|
10
|
+
* Until this is called, nothing is read and nothing is kept. */
|
|
11
|
+
export declare function useLearnedWindows(path?: string): void;
|
|
12
|
+
/** The learned window for a model at an endpoint, if a refusal stated one. */
|
|
13
|
+
export declare function learnedWindowFor(model: string, baseUrl: string | undefined): LearnedWindow | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Keep a refusal's cap. Returns true when it changed what is kept (a new
|
|
16
|
+
* key, or a smaller figure), false when a figure at or below it is kept
|
|
17
|
+
* already — or when no file was named (`useLearnedWindows`). The file is
|
|
18
|
+
* re-read before the write, so a figure another process learned meanwhile
|
|
19
|
+
* is not lost.
|
|
20
|
+
*/
|
|
21
|
+
export declare function recordLearnedWindow(model: string, baseUrl: string | undefined, tokens: number, today?: Date): boolean;
|
|
22
|
+
/** Tests: back to the state before startup — nothing read, nothing kept. */
|
|
23
|
+
export declare function resetLearnedWindows(): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CW-1 batch 2 — the windows endpoints have STATED by refusing a request,
|
|
3
|
+
* kept across sessions in `<KISO_HOME>/learned-windows.json`.
|
|
4
|
+
*
|
|
5
|
+
* A refusal like "maximum context length is 1048576 tokens" is the one
|
|
6
|
+
* window source that is right for any route, relays included (the runtime
|
|
7
|
+
* reads it: window-learner.ts). The runtime's tiers take it at once; this
|
|
8
|
+
* file keeps it, so the next session starts on the real figure instead of
|
|
9
|
+
* meeting the wall again. It sits in the window chain after what the user
|
|
10
|
+
* set and before the registry: a measurement of this endpoint beats a
|
|
11
|
+
* statement about the model.
|
|
12
|
+
*
|
|
13
|
+
* Keyed by the endpoint (the baseUrl, trailing slashes dropped — a
|
|
14
|
+
* forwarder can send two paths to two upstreams) and the model id. A
|
|
15
|
+
* figure only ever goes DOWN for a key: a later, larger refusal cannot
|
|
16
|
+
* widen what a route already refused. The file is private (0600, tmp +
|
|
17
|
+
* rename); a missing or unreadable file is an empty one — the chain falls
|
|
18
|
+
* through to the registry, as it did before anything was learned.
|
|
19
|
+
*
|
|
20
|
+
* Nothing is read until the CLI's startup names the file
|
|
21
|
+
* (`useLearnedWindows`): the window chain is library code that in-process
|
|
22
|
+
* tests call, and a chain that read `~/.kiso` on its own would make their
|
|
23
|
+
* verdict depend on the machine they run on.
|
|
24
|
+
*/
|
|
25
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import { kisoHome } from "./state.js";
|
|
28
|
+
let table = null;
|
|
29
|
+
let tablePath = null;
|
|
30
|
+
export function learnedWindowsPath(home = kisoHome()) {
|
|
31
|
+
return join(home, "learned-windows.json");
|
|
32
|
+
}
|
|
33
|
+
/** The key for one model at one endpoint; no baseUrl is the provider's own. */
|
|
34
|
+
export function learnedKey(model, baseUrl) {
|
|
35
|
+
return `${baseUrl === undefined ? "-" : baseUrl.replace(/\/+$/, "")} ${model}`;
|
|
36
|
+
}
|
|
37
|
+
function read(path) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
40
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
41
|
+
return {};
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
44
|
+
const w = v;
|
|
45
|
+
if (w !== null && typeof w === "object" && typeof w.tokens === "number" && Number.isSafeInteger(w.tokens) && w.tokens > 0 && typeof w.observedAt === "string") {
|
|
46
|
+
out[k] = { tokens: w.tokens, observedAt: w.observedAt };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Startup: read the file and keep figures learned from here on in it.
|
|
56
|
+
* Until this is called, nothing is read and nothing is kept. */
|
|
57
|
+
export function useLearnedWindows(path = learnedWindowsPath()) {
|
|
58
|
+
tablePath = path;
|
|
59
|
+
table = read(path);
|
|
60
|
+
}
|
|
61
|
+
/** The learned window for a model at an endpoint, if a refusal stated one. */
|
|
62
|
+
export function learnedWindowFor(model, baseUrl) {
|
|
63
|
+
return table === null ? undefined : table[learnedKey(model, baseUrl)];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Keep a refusal's cap. Returns true when it changed what is kept (a new
|
|
67
|
+
* key, or a smaller figure), false when a figure at or below it is kept
|
|
68
|
+
* already — or when no file was named (`useLearnedWindows`). The file is
|
|
69
|
+
* re-read before the write, so a figure another process learned meanwhile
|
|
70
|
+
* is not lost.
|
|
71
|
+
*/
|
|
72
|
+
export function recordLearnedWindow(model, baseUrl, tokens, today = new Date()) {
|
|
73
|
+
const path = tablePath;
|
|
74
|
+
if (path === null)
|
|
75
|
+
return false;
|
|
76
|
+
const key = learnedKey(model, baseUrl);
|
|
77
|
+
const current = read(path);
|
|
78
|
+
const held = current[key];
|
|
79
|
+
if (held !== undefined && held.tokens <= tokens) {
|
|
80
|
+
table = current;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const next = { ...current, [key]: { tokens, observedAt: today.toISOString().slice(0, 10) } };
|
|
84
|
+
table = next;
|
|
85
|
+
try {
|
|
86
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
87
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
88
|
+
writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
89
|
+
renameSync(tmp, path);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// unwritable home: the figure still holds for this process
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/** Tests: back to the state before startup — nothing read, nothing kept. */
|
|
97
|
+
export function resetLearnedWindows() {
|
|
98
|
+
table = null;
|
|
99
|
+
tablePath = null;
|
|
100
|
+
}
|
package/dist/provider-label.d.ts
CHANGED
|
@@ -24,9 +24,14 @@
|
|
|
24
24
|
export declare function providerHost(baseUrl?: string): string | null;
|
|
25
25
|
/** `@commandcode.ai` — the provider named beside a model. `""` when the
|
|
26
26
|
* binding names no endpoint (the provider's own default origin), so a row
|
|
27
|
-
* that has nothing to say does not print a stray `@`.
|
|
28
|
-
|
|
27
|
+
* that has nothing to say does not print a stray `@`.
|
|
28
|
+
*
|
|
29
|
+
* With an `upstream` (a profile whose baseUrl is a local forwarder), the
|
|
30
|
+
* label names who is billed FIRST and where the bytes go second:
|
|
31
|
+
* `@gateway.example via 127.0.0.1:47821`. A name that is not a URL is shown as
|
|
32
|
+
* written. */
|
|
33
|
+
export declare function providerLabel(baseUrl?: string, upstream?: string): string;
|
|
29
34
|
/** The same label for a PROFILE, which knows its kind as well as its URL: a
|
|
30
35
|
* profile with no baseUrl (a first-party provider) still names itself by
|
|
31
36
|
* providerId, because a row that shows two profiles must tell them apart. */
|
|
32
|
-
export declare function profileProviderLabel(kind: string, baseUrl?: string): string;
|
|
37
|
+
export declare function profileProviderLabel(kind: string, baseUrl?: string, upstream?: string): string;
|
package/dist/provider-label.js
CHANGED
|
@@ -40,14 +40,22 @@ export function providerHost(baseUrl) {
|
|
|
40
40
|
}
|
|
41
41
|
/** `@commandcode.ai` — the provider named beside a model. `""` when the
|
|
42
42
|
* binding names no endpoint (the provider's own default origin), so a row
|
|
43
|
-
* that has nothing to say does not print a stray `@`.
|
|
44
|
-
|
|
43
|
+
* that has nothing to say does not print a stray `@`.
|
|
44
|
+
*
|
|
45
|
+
* With an `upstream` (a profile whose baseUrl is a local forwarder), the
|
|
46
|
+
* label names who is billed FIRST and where the bytes go second:
|
|
47
|
+
* `@gateway.example via 127.0.0.1:47821`. A name that is not a URL is shown as
|
|
48
|
+
* written. */
|
|
49
|
+
export function providerLabel(baseUrl, upstream) {
|
|
45
50
|
const host = providerHost(baseUrl);
|
|
51
|
+
const up = upstream === undefined || upstream.trim() === "" ? null : (providerHost(upstream) ?? upstream.trim());
|
|
52
|
+
if (up !== null)
|
|
53
|
+
return host === null || host === up ? `@${up}` : `@${up} via ${host}`;
|
|
46
54
|
return host === null ? "" : `@${host}`;
|
|
47
55
|
}
|
|
48
56
|
/** The same label for a PROFILE, which knows its kind as well as its URL: a
|
|
49
57
|
* profile with no baseUrl (a first-party provider) still names itself by
|
|
50
58
|
* providerId, because a row that shows two profiles must tell them apart. */
|
|
51
|
-
export function profileProviderLabel(kind, baseUrl) {
|
|
52
|
-
return providerLabel(baseUrl) || `@${providerIdOf(kind, baseUrl)}`;
|
|
59
|
+
export function profileProviderLabel(kind, baseUrl, upstream) {
|
|
60
|
+
return providerLabel(baseUrl, upstream) || `@${providerIdOf(kind, baseUrl)}`;
|
|
53
61
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -253,6 +253,10 @@ export declare function setConfiguredWindow(value: number | undefined): void;
|
|
|
253
253
|
* model's NAME — /model lists and switches against them. */
|
|
254
254
|
export declare let configModels: Readonly<Record<string, import("./config.js").ModelProfile>>;
|
|
255
255
|
export declare function setConfigModels(models: Readonly<Record<string, import("./config.js").ModelProfile>>): void;
|
|
256
|
+
/** CW-1: the upstream a forwarder at `baseUrl` names — a fact about the
|
|
257
|
+
* ADDRESS, so a resumed session (no profile name in hand) finds it too:
|
|
258
|
+
* the first profile at that address that names one. */
|
|
259
|
+
export declare function upstreamOf(baseUrl: string | undefined): string | undefined;
|
|
256
260
|
/** The name of the model currently driving the session ("faux" or the
|
|
257
261
|
* profile name / provider/model write / env model). */
|
|
258
262
|
/**
|
package/dist/state.js
CHANGED
|
@@ -342,6 +342,17 @@ export let configModels = {};
|
|
|
342
342
|
export function setConfigModels(models) {
|
|
343
343
|
configModels = models;
|
|
344
344
|
}
|
|
345
|
+
/** CW-1: the upstream a forwarder at `baseUrl` names — a fact about the
|
|
346
|
+
* ADDRESS, so a resumed session (no profile name in hand) finds it too:
|
|
347
|
+
* the first profile at that address that names one. */
|
|
348
|
+
export function upstreamOf(baseUrl) {
|
|
349
|
+
if (baseUrl === undefined)
|
|
350
|
+
return undefined;
|
|
351
|
+
for (const p of Object.values(configModels))
|
|
352
|
+
if (p.baseUrl === baseUrl && p.upstream !== undefined)
|
|
353
|
+
return p.upstream;
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
345
356
|
/** The name of the model currently driving the session ("faux" or the
|
|
346
357
|
* profile name / provider/model write / env model). */
|
|
347
358
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.4",
|
|
4
4
|
"description": "kiso-code \u2014 the coding agent on kiso, the durable runtime for AI agents: kiso chat / kiso resume / kiso sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,20 +18,20 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vincemakes/kiso-ask-ext": "0.40.
|
|
22
|
-
"@vincemakes/kiso-core": "0.40.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.40.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.40.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.40.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.40.
|
|
27
|
-
"@vincemakes/kiso-provider-openai-responses": "0.40.
|
|
28
|
-
"@vincemakes/kiso-runtime": "0.40.
|
|
29
|
-
"@vincemakes/kiso-skills-ext": "0.40.
|
|
30
|
-
"@vincemakes/kiso-subagent-ext": "0.40.
|
|
31
|
-
"@vincemakes/kiso-task-ext": "0.40.
|
|
32
|
-
"@vincemakes/kiso-tools-node": "0.40.
|
|
33
|
-
"@vincemakes/kiso-tui": "0.40.
|
|
34
|
-
"@vincemakes/kiso-tui-cells": "0.40.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.40.4",
|
|
22
|
+
"@vincemakes/kiso-core": "0.40.4",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.40.4",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.40.4",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.40.4",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.40.4",
|
|
27
|
+
"@vincemakes/kiso-provider-openai-responses": "0.40.4",
|
|
28
|
+
"@vincemakes/kiso-runtime": "0.40.4",
|
|
29
|
+
"@vincemakes/kiso-skills-ext": "0.40.4",
|
|
30
|
+
"@vincemakes/kiso-subagent-ext": "0.40.4",
|
|
31
|
+
"@vincemakes/kiso-task-ext": "0.40.4",
|
|
32
|
+
"@vincemakes/kiso-tools-node": "0.40.4",
|
|
33
|
+
"@vincemakes/kiso-tui": "0.40.4",
|
|
34
|
+
"@vincemakes/kiso-tui-cells": "0.40.4"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^26.1.2",
|