@vincemakes/kiso-code 0.36.0 → 0.38.0
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.js +11 -6
- package/dist/breaker.d.ts +2 -2
- package/dist/breaker.js +2 -2
- package/dist/chat.d.ts +101 -14
- package/dist/chat.js +194 -53
- package/dist/config.d.ts +16 -1
- package/dist/config.js +6 -1
- package/dist/dispatch.js +27 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +44 -6
- package/package.json +15 -15
|
@@ -15,12 +15,17 @@
|
|
|
15
15
|
* The extraction found the two sites had ALREADY drifted:
|
|
16
16
|
*
|
|
17
17
|
* - `promptCacheKey` was passed at startup whenever the session had an
|
|
18
|
-
* id, but on `/model` only for an OAuth profile.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* the
|
|
23
|
-
*
|
|
18
|
+
* id, but on `/model` only for an OAuth profile. Both sites pass it
|
|
19
|
+
* identically now, which is a tidiness fix and NOT a wire change —
|
|
20
|
+
* finding IA-0360-F1 corrects the claim this comment used to make.
|
|
21
|
+
* The Responses adapter serializes it on the ChatGPT (OAuth) target
|
|
22
|
+
* ALONE; the first-party API-key target builds an empty `extraBody`
|
|
23
|
+
* (provider-openai-responses `resolveTarget`), so it never carried a
|
|
24
|
+
* cache key and still does not. Passing an internal option at two
|
|
25
|
+
* construction sites is not evidence that the adapter puts it on the
|
|
26
|
+
* wire on both authentication paths — I traced the field as far as the
|
|
27
|
+
* provider's constructor and stopped one level short of the branch
|
|
28
|
+
* that drops it.
|
|
24
29
|
* - `streamIdleMs` is set at startup and not on `/model`. It is NOT an
|
|
25
30
|
* adapter option — it is an agent-definition field the runtime reads —
|
|
26
31
|
* so it is out of this function's scope and stays where it is, noted
|
package/dist/breaker.d.ts
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* deny beat `bypass` — and the model receives it as the call's result with
|
|
19
19
|
* the reason (the 1c grammar: refuse with a reason and continue), so the
|
|
20
20
|
* next turn has to change course. The human sees the denial on the
|
|
21
|
-
* transcript with `decidedBy: breaker
|
|
22
|
-
*
|
|
21
|
+
* transcript with `decidedBy: breaker`. LT2B-F1 removes the periodic
|
|
22
|
+
* turn-count question; this evidence-based refusal remains independent.
|
|
23
23
|
*
|
|
24
24
|
* Counting: CONSECUTIVE failed results of one (tool, input) key within a
|
|
25
25
|
* run. A success, a different call, or the run's terminal resets it. The
|
package/dist/breaker.js
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* deny beat `bypass` — and the model receives it as the call's result with
|
|
19
19
|
* the reason (the 1c grammar: refuse with a reason and continue), so the
|
|
20
20
|
* next turn has to change course. The human sees the denial on the
|
|
21
|
-
* transcript with `decidedBy: breaker
|
|
22
|
-
*
|
|
21
|
+
* transcript with `decidedBy: breaker`. LT2B-F1 removes the periodic
|
|
22
|
+
* turn-count question; this evidence-based refusal remains independent.
|
|
23
23
|
*
|
|
24
24
|
* Counting: CONSECUTIVE failed results of one (tool, input) key within a
|
|
25
25
|
* run. A success, a different call, or the run's terminal resets it. The
|
package/dist/chat.d.ts
CHANGED
|
@@ -26,8 +26,58 @@ export declare function autoCompactFromEnv(): AutoCompact | undefined;
|
|
|
26
26
|
* config window (merge round B), both beat the 200k default. The microcompact
|
|
27
27
|
* threshold is derived from it (50%), and the status line's ~ctx estimate
|
|
28
28
|
* is measured against it — one source of truth for the window.
|
|
29
|
+
*
|
|
30
|
+
* CTX-1: with no argument this reads the LIVE binding, which is what every
|
|
31
|
+
* display caller wants. `/model` needs the window of the model it is
|
|
32
|
+
* switching TO, before that binding is live, so it passes the pair
|
|
33
|
+
* explicitly. Reading module state a caller is about to change is how the
|
|
34
|
+
* threshold got stuck in the first place.
|
|
35
|
+
*
|
|
36
|
+
* The argument is a PAIR, never two optional halves: a profile with no
|
|
37
|
+
* `baseUrl` is endpoint-less, and a half-given argument would have it
|
|
38
|
+
* inherit the OUTGOING model's endpoint — the same bug one field over.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* THE WINDOW SOMEBODY STATED, or null when nobody has.
|
|
42
|
+
*
|
|
43
|
+
* Three sources, in order: the config/profile window, KISO_CONTEXT_WINDOW,
|
|
44
|
+
* and the metadata registry. Each is a claim someone made and dated. When
|
|
45
|
+
* none of them speaks, this returns NULL rather than a number — because
|
|
46
|
+
* the question "how much of the window is left" has no answer without a
|
|
47
|
+
* window, and every display that shows a percentage needs this one, not
|
|
48
|
+
* the policy value below.
|
|
49
|
+
*
|
|
50
|
+
* DeepSeek is the live case: its /models endpoint returns ids only, the
|
|
51
|
+
* responses carry no window, and the registry records null on purpose. The
|
|
52
|
+
* display used to divide by a hardcoded 200,000 anyway and print a
|
|
53
|
+
* confident `ctx left ~82%` against a figure nobody had measured.
|
|
54
|
+
*/
|
|
55
|
+
export declare function knownContextWindow(of?: {
|
|
56
|
+
readonly model: string;
|
|
57
|
+
readonly baseUrl?: string;
|
|
58
|
+
}): number | null;
|
|
59
|
+
export declare function contextWindowTokens(of?: {
|
|
60
|
+
readonly model: string;
|
|
61
|
+
readonly baseUrl?: string;
|
|
62
|
+
}): number;
|
|
63
|
+
/**
|
|
64
|
+
* CTX-1: THE ONE derivation of the compaction threshold from the window.
|
|
65
|
+
*
|
|
66
|
+
* Startup (`index.ts`) and the `/model` switch (`dispatch.ts`) both need
|
|
67
|
+
* this number, and two call sites computing the same thing is how
|
|
68
|
+
* `promptCacheKey` drifted before `adapterOptionsFor` collected it. The
|
|
69
|
+
* policy — half the window — lives here and nowhere else.
|
|
70
|
+
*
|
|
71
|
+
* CAPACITY is not POLICY. The window is what the model can hold; this is
|
|
72
|
+
* when we choose to clear old tool results. They are 2:1 today because
|
|
73
|
+
* that ratio has never been measured, not because it is derived from
|
|
74
|
+
* anything. Moving the policy means changing this line, and the whole
|
|
75
|
+
* product moves with it.
|
|
29
76
|
*/
|
|
30
|
-
export declare function
|
|
77
|
+
export declare function microcompactThresholdFor(of?: {
|
|
78
|
+
readonly model: string;
|
|
79
|
+
readonly baseUrl?: string;
|
|
80
|
+
}): number;
|
|
31
81
|
/**
|
|
32
82
|
* B area: approximate context ratio — chars/4 of the projected messages vs
|
|
33
83
|
* the model window. Marked ~ everywhere it is shown; no counting API.
|
|
@@ -95,6 +145,56 @@ export interface UsageDelta {
|
|
|
95
145
|
* total convention (INPUT_CONVENTIONS), never a crash.
|
|
96
146
|
*/
|
|
97
147
|
export declare function usageFromEvent(route: string | undefined, ev: import("@vincemakes/kiso-core").Usage, prevTotal: number | null, model?: string, endpoint?: string): UsageDelta;
|
|
148
|
+
/** W22 (owner, 2026-09-14) — the TURN's usage, summed across its calls.
|
|
149
|
+
*
|
|
150
|
+
* EXISTING-BEHAVIOR CHANGE — declared, never a silent side-fix:
|
|
151
|
+
* the recap and the running row carried the LAST call's figures
|
|
152
|
+
* (`usage = delta.usage`), and read as the turn's by everyone who saw
|
|
153
|
+
* them: the owner's own dogfood, a nine-call turn that had spent 120,358
|
|
154
|
+
* prompt tokens and 4,608 output tokens, reported `in 412 out 924 ·
|
|
155
|
+
* cache 98%` — the ninth call's fresh input and the ninth call's output.
|
|
156
|
+
* The variables are the same and the definitions are unchanged; the SCOPE
|
|
157
|
+
* is the turn's.
|
|
158
|
+
*
|
|
159
|
+
* Two rules, inherited rather than invented:
|
|
160
|
+
* - `in` stays the canonical FRESH count (E2, R2a-1): the turn's sum is
|
|
161
|
+
* the input the turn actually bought at full price. `cache` is summed
|
|
162
|
+
* too, but only the recap's ratio reads it (cache/(in+cache)) — the raw
|
|
163
|
+
* total is the ledger's business, never the row's, because a sum over
|
|
164
|
+
* calls counts the same prefix once per call.
|
|
165
|
+
* - an unmeasured term makes the sum unmeasured (OR-10's rule, applied to
|
|
166
|
+
* the sum): `null` propagates, and one call with `known: false` makes
|
|
167
|
+
* the turn's figure unknown rather than a lower bound dressed as a
|
|
168
|
+
* total. The row then says nothing, as it always did for an unknown
|
|
169
|
+
* call.
|
|
170
|
+
*
|
|
171
|
+
* The miss estimate and the decode rate stay PER CALL: `missed` is the
|
|
172
|
+
* overlap of consecutive prompts (a total-side quantity), and the rate is
|
|
173
|
+
* the call that just settled timing itself. */
|
|
174
|
+
export declare function accumulateUsage(prev: RunUsage, delta: RunUsage): RunUsage;
|
|
175
|
+
/**
|
|
176
|
+
* W22-R1 (Astra): the turn's usage ledger, as ONE definition.
|
|
177
|
+
*
|
|
178
|
+
* A TURN is the sum over its CALLS; a CALL is its LATEST report. Both
|
|
179
|
+
* halves are load-bearing: the openai-compat adapter yields a usage event
|
|
180
|
+
* from either of two stream shapes and its `usageSent` flag only decides
|
|
181
|
+
* whether to append a trailing unknown — it does not stop both from
|
|
182
|
+
* firing. W22 substituted, so a second report silently replaced the first
|
|
183
|
+
* and no screen changed; summing without the call boundary turned the same
|
|
184
|
+
* stream into a WRONG NUMBER (a ledger of fresh 80 / out 10 shown as
|
|
185
|
+
* 160 / 15).
|
|
186
|
+
*
|
|
187
|
+
* It is a function rather than three variables in the loop because the rule
|
|
188
|
+
* then has one place to be right — the same reason `microcompactThresholdFor`
|
|
189
|
+
* exists, and the drift `promptCacheKey` demonstrated when it did not.
|
|
190
|
+
*/
|
|
191
|
+
export declare function turnUsageLedger(): {
|
|
192
|
+
observe: (ev: {
|
|
193
|
+
readonly type: string;
|
|
194
|
+
}) => void;
|
|
195
|
+
report: (u: RunUsage) => void;
|
|
196
|
+
total: () => RunUsage | null;
|
|
197
|
+
};
|
|
98
198
|
/** v2b: the spinner merged into the STATUS BAR (the v2a standalone glyph
|
|
99
199
|
* is gone) — docked only, 200ms rotation between the request and the
|
|
100
200
|
* first event. */
|
|
@@ -170,19 +270,6 @@ export declare function parseSaferOptions(text: string): SaferOption[] | null;
|
|
|
170
270
|
* echo is UI, the chip is the record; the momentary double-render is
|
|
171
271
|
* the design's explicit point).
|
|
172
272
|
*/
|
|
173
|
-
/** LT-2b — the turn checkpoint (kiso-doc/kiso-lt1-mini-spec-2026-09-09.md,
|
|
174
|
-
* part two). R3e stands: no hard turn limit on an interactive session. What
|
|
175
|
-
* a long run gets instead is a QUESTION every CHECKPOINT_TURNS model turns —
|
|
176
|
-
* the run is paused between two model turns (the generator is simply not
|
|
177
|
-
* pulled while the panel is up; the kernel does nothing meanwhile) and the
|
|
178
|
-
* human answers. "Keep going" resumes it, the next question one checkpoint
|
|
179
|
-
* later; "Stop here" aborts the run — `aborted by user`, resumable, never
|
|
180
|
-
* silent. A decline (esc) is "keep going": the run was already running, and
|
|
181
|
-
* esc on a panel is the panel's decline, not the run's abort. Headless
|
|
182
|
-
* entries never see it: the panel needs the dock, and a pipe, `-p` or a
|
|
183
|
-
* task file has none. 50 is named here; a finding moves it, never a stray
|
|
184
|
-
* edit. */
|
|
185
|
-
export declare const CHECKPOINT_TURNS = 50;
|
|
186
273
|
export declare function consumeRun(session: AgentSession, run: Run, input: LineInput, turnNo: number, faux: boolean, statusCb: ((usage: RunUsage, ctxRatio: number, costUsd?: number | null, tokPerSec?: number | null) => void) | null,
|
|
187
274
|
/** W21: the amend words ("Yes + feedback") ride the NEXT user turn —
|
|
188
275
|
* threaded from chat's submitTurn; absent in the recovery flow
|
package/dist/chat.js
CHANGED
|
@@ -20,6 +20,15 @@ import { FauxExhaustionError, failOnFauxExhaustion } from "./faux-glue.js";
|
|
|
20
20
|
import { MODES, getMode, setMode } from "./mode.js";
|
|
21
21
|
/** B area: default context window for the ~ctx estimate (config overridable). */
|
|
22
22
|
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
23
|
+
/** The in-process fake provider's id, and the window we declare for it.
|
|
24
|
+
* Ours to state: faux is not a vendor's model, so "nobody published a
|
|
25
|
+
* window" — the reason a real model's capacity is unknown — cannot apply.
|
|
26
|
+
* 200,000 is the figure every faux transcript has been measured against
|
|
27
|
+
* since the fallback existed, so declaring it changes no behaviour; it
|
|
28
|
+
* only stops an honest `ctx ?` from firing where the honest answer is a
|
|
29
|
+
* number we own. */
|
|
30
|
+
const FAUX_MODEL = "faux";
|
|
31
|
+
const FAUX_CONTEXT_WINDOW = 200_000;
|
|
23
32
|
/** Parse KISO_AUTO_COMPACT — an invalid value is OFF, never a crash. */
|
|
24
33
|
export function autoCompactFromEnv() {
|
|
25
34
|
const raw = process.env.KISO_AUTO_COMPACT;
|
|
@@ -35,8 +44,51 @@ export function autoCompactFromEnv() {
|
|
|
35
44
|
* config window (merge round B), both beat the 200k default. The microcompact
|
|
36
45
|
* threshold is derived from it (50%), and the status line's ~ctx estimate
|
|
37
46
|
* is measured against it — one source of truth for the window.
|
|
47
|
+
*
|
|
48
|
+
* CTX-1: with no argument this reads the LIVE binding, which is what every
|
|
49
|
+
* display caller wants. `/model` needs the window of the model it is
|
|
50
|
+
* switching TO, before that binding is live, so it passes the pair
|
|
51
|
+
* explicitly. Reading module state a caller is about to change is how the
|
|
52
|
+
* threshold got stuck in the first place.
|
|
53
|
+
*
|
|
54
|
+
* The argument is a PAIR, never two optional halves: a profile with no
|
|
55
|
+
* `baseUrl` is endpoint-less, and a half-given argument would have it
|
|
56
|
+
* inherit the OUTGOING model's endpoint — the same bug one field over.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* THE WINDOW SOMEBODY STATED, or null when nobody has.
|
|
60
|
+
*
|
|
61
|
+
* Three sources, in order: the config/profile window, KISO_CONTEXT_WINDOW,
|
|
62
|
+
* and the metadata registry. Each is a claim someone made and dated. When
|
|
63
|
+
* none of them speaks, this returns NULL rather than a number — because
|
|
64
|
+
* the question "how much of the window is left" has no answer without a
|
|
65
|
+
* window, and every display that shows a percentage needs this one, not
|
|
66
|
+
* the policy value below.
|
|
67
|
+
*
|
|
68
|
+
* DeepSeek is the live case: its /models endpoint returns ids only, the
|
|
69
|
+
* responses carry no window, and the registry records null on purpose. The
|
|
70
|
+
* display used to divide by a hardcoded 200,000 anyway and print a
|
|
71
|
+
* confident `ctx left ~82%` against a figure nobody had measured.
|
|
38
72
|
*/
|
|
39
|
-
export function
|
|
73
|
+
export function knownContextWindow(of) {
|
|
74
|
+
if (configuredWindow !== undefined)
|
|
75
|
+
return configuredWindow;
|
|
76
|
+
const env = Number.parseInt(process.env.KISO_CONTEXT_WINDOW ?? "", 10);
|
|
77
|
+
if (Number.isFinite(env) && env > 0)
|
|
78
|
+
return env;
|
|
79
|
+
const model = of?.model ?? agentModel;
|
|
80
|
+
// `faux` is OURS. The registry carries no row for it because it is not a
|
|
81
|
+
// vendor's model, but the reason the window is unknown elsewhere — nobody
|
|
82
|
+
// published one — does not apply to a model we wrote. Declaring it is a
|
|
83
|
+
// statement about our own artifact, with ourselves as the source, and it
|
|
84
|
+
// keeps faux mode showing a real percentage instead of the `ctx ?` that
|
|
85
|
+
// belongs to models whose capacity genuinely nobody states.
|
|
86
|
+
if (model === FAUX_MODEL)
|
|
87
|
+
return FAUX_CONTEXT_WINDOW;
|
|
88
|
+
const known = lookupModelMetadata(model, of !== undefined ? of.baseUrl : agentBaseUrl)?.capabilities.contextWindow;
|
|
89
|
+
return known ?? null;
|
|
90
|
+
}
|
|
91
|
+
export function contextWindowTokens(of) {
|
|
40
92
|
const windowOverride = configuredWindow;
|
|
41
93
|
if (windowOverride !== undefined)
|
|
42
94
|
return windowOverride;
|
|
@@ -45,17 +97,41 @@ export function contextWindowTokens() {
|
|
|
45
97
|
return window;
|
|
46
98
|
// PH-1c (finding PH-F15): the window follows the LIVE model when the
|
|
47
99
|
// metadata registry knows it — /model to a known model moves the
|
|
48
|
-
// window
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
100
|
+
// window without an env var. The default argument is the same live
|
|
101
|
+
// binding the status row shows; an unknown model keeps the 200k
|
|
102
|
+
// default — the registry never guesses, so neither do we. OR-1: the
|
|
103
|
+
// ENDPOINT narrows the row — gpt-5.5 is 1,050,000 at the first-party
|
|
104
|
+
// API and 272,000 at the subscription backend; the two are set
|
|
105
|
+
// together (setAgentModel).
|
|
106
|
+
//
|
|
107
|
+
// CTX-1 erratum: this comment used to add "(and the microcompact
|
|
108
|
+
// threshold derived from it)". The window moved; the threshold did
|
|
109
|
+
// not — it was computed once at startup and the switch path never
|
|
110
|
+
// asked again. The claim is true now because the switch path calls
|
|
111
|
+
// `microcompactThresholdFor` below, not because deriving a number
|
|
112
|
+
// from this function makes anything follow it.
|
|
113
|
+
const known = lookupModelMetadata(of?.model ?? agentModel, of !== undefined ? of.baseUrl : agentBaseUrl)?.capabilities.contextWindow;
|
|
55
114
|
if (known !== undefined && known !== null)
|
|
56
115
|
return known;
|
|
57
116
|
return DEFAULT_CONTEXT_WINDOW;
|
|
58
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* CTX-1: THE ONE derivation of the compaction threshold from the window.
|
|
120
|
+
*
|
|
121
|
+
* Startup (`index.ts`) and the `/model` switch (`dispatch.ts`) both need
|
|
122
|
+
* this number, and two call sites computing the same thing is how
|
|
123
|
+
* `promptCacheKey` drifted before `adapterOptionsFor` collected it. The
|
|
124
|
+
* policy — half the window — lives here and nowhere else.
|
|
125
|
+
*
|
|
126
|
+
* CAPACITY is not POLICY. The window is what the model can hold; this is
|
|
127
|
+
* when we choose to clear old tool results. They are 2:1 today because
|
|
128
|
+
* that ratio has never been measured, not because it is derived from
|
|
129
|
+
* anything. Moving the policy means changing this line, and the whole
|
|
130
|
+
* product moves with it.
|
|
131
|
+
*/
|
|
132
|
+
export function microcompactThresholdFor(of) {
|
|
133
|
+
return contextWindowTokens(of) / 2;
|
|
134
|
+
}
|
|
59
135
|
/**
|
|
60
136
|
* B area: approximate context ratio — chars/4 of the projected messages vs
|
|
61
137
|
* the model window. Marked ~ everywhere it is shown; no counting API.
|
|
@@ -79,7 +155,15 @@ export function statusModelLabel(session) {
|
|
|
79
155
|
return effort !== undefined && effort !== "default" ? `${agentModel} · ${effort}` : agentModel;
|
|
80
156
|
}
|
|
81
157
|
export function displayCtxRatio(session) {
|
|
82
|
-
|
|
158
|
+
// CAPACITY is not POLICY. `contextWindowTokens` falls back to 200,000 so
|
|
159
|
+
// that the compaction threshold always HAS a value — a policy needs a
|
|
160
|
+
// number. A percentage on screen is a different kind of thing: it is a
|
|
161
|
+
// claim about the model, and an unstated window makes it unanswerable.
|
|
162
|
+
// NaN reaches the status row as `ctx ?`.
|
|
163
|
+
const window = knownContextWindow();
|
|
164
|
+
if (window === null)
|
|
165
|
+
return Number.NaN;
|
|
166
|
+
return requestBudget(session.requestParts(), window).ratio;
|
|
83
167
|
}
|
|
84
168
|
/** A1a: the number the auto-compact decision reads — the pre-A1a estimate,
|
|
85
169
|
* unchanged this round, named so a gate can pin that it did not move. */
|
|
@@ -165,6 +249,84 @@ endpoint) {
|
|
|
165
249
|
costUsd: c.costUsd,
|
|
166
250
|
};
|
|
167
251
|
}
|
|
252
|
+
/** The accumulator's zero value — nothing measured yet. One shared
|
|
253
|
+
* literal: RunUsage's fields are readonly, and the old code repeated this
|
|
254
|
+
* object at every reset. */
|
|
255
|
+
const UNKNOWN_USAGE = { in: null, out: null, cache: null, known: false };
|
|
256
|
+
/** W22 (owner, 2026-09-14) — the TURN's usage, summed across its calls.
|
|
257
|
+
*
|
|
258
|
+
* EXISTING-BEHAVIOR CHANGE — declared, never a silent side-fix:
|
|
259
|
+
* the recap and the running row carried the LAST call's figures
|
|
260
|
+
* (`usage = delta.usage`), and read as the turn's by everyone who saw
|
|
261
|
+
* them: the owner's own dogfood, a nine-call turn that had spent 120,358
|
|
262
|
+
* prompt tokens and 4,608 output tokens, reported `in 412 out 924 ·
|
|
263
|
+
* cache 98%` — the ninth call's fresh input and the ninth call's output.
|
|
264
|
+
* The variables are the same and the definitions are unchanged; the SCOPE
|
|
265
|
+
* is the turn's.
|
|
266
|
+
*
|
|
267
|
+
* Two rules, inherited rather than invented:
|
|
268
|
+
* - `in` stays the canonical FRESH count (E2, R2a-1): the turn's sum is
|
|
269
|
+
* the input the turn actually bought at full price. `cache` is summed
|
|
270
|
+
* too, but only the recap's ratio reads it (cache/(in+cache)) — the raw
|
|
271
|
+
* total is the ledger's business, never the row's, because a sum over
|
|
272
|
+
* calls counts the same prefix once per call.
|
|
273
|
+
* - an unmeasured term makes the sum unmeasured (OR-10's rule, applied to
|
|
274
|
+
* the sum): `null` propagates, and one call with `known: false` makes
|
|
275
|
+
* the turn's figure unknown rather than a lower bound dressed as a
|
|
276
|
+
* total. The row then says nothing, as it always did for an unknown
|
|
277
|
+
* call.
|
|
278
|
+
*
|
|
279
|
+
* The miss estimate and the decode rate stay PER CALL: `missed` is the
|
|
280
|
+
* overlap of consecutive prompts (a total-side quantity), and the rate is
|
|
281
|
+
* the call that just settled timing itself. */
|
|
282
|
+
export function accumulateUsage(prev, delta) {
|
|
283
|
+
const sum = (a, b) => (a === null || b === null ? null : a + b);
|
|
284
|
+
return {
|
|
285
|
+
in: sum(prev.in, delta.in),
|
|
286
|
+
out: sum(prev.out, delta.out),
|
|
287
|
+
cache: sum(prev.cache, delta.cache),
|
|
288
|
+
known: prev.known && delta.known,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* W22-R1 (Astra): the turn's usage ledger, as ONE definition.
|
|
293
|
+
*
|
|
294
|
+
* A TURN is the sum over its CALLS; a CALL is its LATEST report. Both
|
|
295
|
+
* halves are load-bearing: the openai-compat adapter yields a usage event
|
|
296
|
+
* from either of two stream shapes and its `usageSent` flag only decides
|
|
297
|
+
* whether to append a trailing unknown — it does not stop both from
|
|
298
|
+
* firing. W22 substituted, so a second report silently replaced the first
|
|
299
|
+
* and no screen changed; summing without the call boundary turned the same
|
|
300
|
+
* stream into a WRONG NUMBER (a ledger of fresh 80 / out 10 shown as
|
|
301
|
+
* 160 / 15).
|
|
302
|
+
*
|
|
303
|
+
* It is a function rather than three variables in the loop because the rule
|
|
304
|
+
* then has one place to be right — the same reason `microcompactThresholdFor`
|
|
305
|
+
* exists, and the drift `promptCacheKey` demonstrated when it did not.
|
|
306
|
+
*/
|
|
307
|
+
export function turnUsageLedger() {
|
|
308
|
+
let settled = null;
|
|
309
|
+
let inFlight = null;
|
|
310
|
+
const fold = (a, b) => a === null ? b : b === null ? a : accumulateUsage(a, b);
|
|
311
|
+
return {
|
|
312
|
+
// every event of the run passes through here: a stop ENDS a call, so
|
|
313
|
+
// the report in flight settles and the next call starts its own.
|
|
314
|
+
observe(ev) {
|
|
315
|
+
if (ev.type !== "stop")
|
|
316
|
+
return;
|
|
317
|
+
settled = fold(settled, inFlight);
|
|
318
|
+
inFlight = null;
|
|
319
|
+
},
|
|
320
|
+
// this call's latest report REPLACES its earlier one.
|
|
321
|
+
report(u) {
|
|
322
|
+
inFlight = u;
|
|
323
|
+
},
|
|
324
|
+
// the turn so far: the settled calls plus the one in flight.
|
|
325
|
+
total() {
|
|
326
|
+
return fold(settled, inFlight);
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
168
330
|
/** v2b: the spinner merged into the STATUS BAR (the v2a standalone glyph
|
|
169
331
|
* is gone) — docked only, 200ms rotation between the request and the
|
|
170
332
|
* first event. */
|
|
@@ -553,44 +715,24 @@ function toRenderInput(ev) {
|
|
|
553
715
|
* echo is UI, the chip is the record; the momentary double-render is
|
|
554
716
|
* the design's explicit point).
|
|
555
717
|
*/
|
|
556
|
-
/** LT-2b — the turn checkpoint (kiso-doc/kiso-lt1-mini-spec-2026-09-09.md,
|
|
557
|
-
* part two). R3e stands: no hard turn limit on an interactive session. What
|
|
558
|
-
* a long run gets instead is a QUESTION every CHECKPOINT_TURNS model turns —
|
|
559
|
-
* the run is paused between two model turns (the generator is simply not
|
|
560
|
-
* pulled while the panel is up; the kernel does nothing meanwhile) and the
|
|
561
|
-
* human answers. "Keep going" resumes it, the next question one checkpoint
|
|
562
|
-
* later; "Stop here" aborts the run — `aborted by user`, resumable, never
|
|
563
|
-
* silent. A decline (esc) is "keep going": the run was already running, and
|
|
564
|
-
* esc on a panel is the panel's decline, not the run's abort. Headless
|
|
565
|
-
* entries never see it: the panel needs the dock, and a pipe, `-p` or a
|
|
566
|
-
* task file has none. 50 is named here; a finding moves it, never a stray
|
|
567
|
-
* edit. */
|
|
568
|
-
export const CHECKPOINT_TURNS = 50;
|
|
569
|
-
async function checkpointAsk(input, turns) {
|
|
570
|
-
const verdict = await askPanel(input, askView({
|
|
571
|
-
questions: [
|
|
572
|
-
{
|
|
573
|
-
question: `${turns} turns and still working — keep going?`,
|
|
574
|
-
header: "checkpoint",
|
|
575
|
-
options: [
|
|
576
|
-
{ label: "Keep going", description: `the next question at ${turns + CHECKPOINT_TURNS}` },
|
|
577
|
-
{ label: "Stop here", description: "the run ends as aborted by you; kiso resume continues it later" },
|
|
578
|
-
],
|
|
579
|
-
},
|
|
580
|
-
],
|
|
581
|
-
}));
|
|
582
|
-
if (verdict.action !== "answers" || !("answers" in verdict.result))
|
|
583
|
-
return true; // declined: nothing changes
|
|
584
|
-
const first = verdict.result.answers[0];
|
|
585
|
-
return !(first !== undefined && "choice" in first && first.choice === "Stop here");
|
|
586
|
-
}
|
|
587
718
|
export async function consumeRun(session, run, input, turnNo, faux, statusCb,
|
|
588
719
|
/** W21: the amend words ("Yes + feedback") ride the NEXT user turn —
|
|
589
720
|
* threaded from chat's submitTurn; absent in the recovery flow
|
|
590
721
|
* (resume) where a dropped amend is noticed instead. */
|
|
591
722
|
submitTurn) {
|
|
592
723
|
let last;
|
|
593
|
-
|
|
724
|
+
// W22: the TURN's usage — null until a call settles, then the sum of
|
|
725
|
+
// every call in this turn (accumulateUsage). Null rather than an empty
|
|
726
|
+
// accumulator, so the first call is the sum rather than an addition to
|
|
727
|
+
// nothing.
|
|
728
|
+
//
|
|
729
|
+
// W22-R1 (Astra): one call can report TWICE, so the turn is a sum over
|
|
730
|
+
// CALLS and a call is its LATEST report — see `turnUsageLedger`, which
|
|
731
|
+
// holds that rule. Every event passes through `observe` (a stop ends a
|
|
732
|
+
// call); the usage case reports; the status line and the recap read
|
|
733
|
+
// `total()`.
|
|
734
|
+
const ledger = turnUsageLedger();
|
|
735
|
+
const turnUsage = () => ledger.total();
|
|
594
736
|
// TPS-1: the decode clock, armed by the FIRST streamed event of a call
|
|
595
737
|
// and read at that call's usage event. Per CALL, not per turn — a turn
|
|
596
738
|
// with three model calls reports the third, and each one times itself.
|
|
@@ -618,16 +760,13 @@ submitTurn) {
|
|
|
618
760
|
tailers.delete(callId);
|
|
619
761
|
};
|
|
620
762
|
try {
|
|
621
|
-
let modelTurns = 0;
|
|
622
763
|
for await (const ev of run) {
|
|
623
764
|
last = ev;
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
run.abort();
|
|
630
|
-
}
|
|
765
|
+
// LT2B-F1: a turn count is not evidence of a loop. Keep consuming
|
|
766
|
+
// healthy runs without a periodic presence check; explicit cancellation,
|
|
767
|
+
// the stream watchdog and the repeated-failure breaker remain active.
|
|
768
|
+
// W22-R1: the ledger sees every event; a stop is what ends a call.
|
|
769
|
+
ledger.observe(ev);
|
|
631
770
|
if (ev.type !== "thinking") {
|
|
632
771
|
if (thinkingSince !== null) {
|
|
633
772
|
thoughtSeconds += (Date.now() - thinkingSince) / 1000;
|
|
@@ -726,7 +865,9 @@ submitTurn) {
|
|
|
726
865
|
break;
|
|
727
866
|
case "usage": {
|
|
728
867
|
const delta = usageFromEvent(session.provider, ev, prevTotal, agentModel, session.baseUrl);
|
|
729
|
-
|
|
868
|
+
// W22-R1: this call's LATEST report replaces its earlier one;
|
|
869
|
+
// the turn's figure is the settled calls plus this one.
|
|
870
|
+
ledger.report(delta.usage);
|
|
730
871
|
prevTotal = delta.total;
|
|
731
872
|
missed = delta.missed;
|
|
732
873
|
// TUI2-R1 (E): the request's canonical cost rides the same
|
|
@@ -736,7 +877,7 @@ submitTurn) {
|
|
|
736
877
|
// second call is timed from ITS own first event.
|
|
737
878
|
const tokPerSec = callFirstEventAt === null ? null : decodeRate(ev.outputTokens, Date.now() - callFirstEventAt);
|
|
738
879
|
callFirstEventAt = null;
|
|
739
|
-
statusCb?.(
|
|
880
|
+
statusCb?.(turnUsage() ?? UNKNOWN_USAGE, displayCtxRatio(session), delta.costUsd, tokPerSec);
|
|
740
881
|
break;
|
|
741
882
|
}
|
|
742
883
|
case "uncertain_pending":
|
|
@@ -843,7 +984,7 @@ submitTurn) {
|
|
|
843
984
|
// v3 §02: the run's recap line REPLACES the old "done" label
|
|
844
985
|
// + status line — one local line, derived from this run's
|
|
845
986
|
// events (zero tokens). The dock's status bar still paints.
|
|
846
|
-
statusCb?.(
|
|
987
|
+
statusCb?.(turnUsage() ?? UNKNOWN_USAGE, displayCtxRatio(session));
|
|
847
988
|
const ratio = displayCtxRatio(session);
|
|
848
989
|
// TV-1B: the settle verdict — the checklist stops lying. When
|
|
849
990
|
// every item is CLAIMED done, the settled block's tail says
|
|
@@ -901,7 +1042,7 @@ submitTurn) {
|
|
|
901
1042
|
// the recap cut itself down to one character. A width
|
|
902
1043
|
// that is not a positive number is not a width.
|
|
903
1044
|
width: process.stdout.columns > 0 ? process.stdout.columns : 80,
|
|
904
|
-
usage,
|
|
1045
|
+
usage: turnUsage() ?? UNKNOWN_USAGE,
|
|
905
1046
|
// R-C item 4: only an above-floor miss is surfaced —
|
|
906
1047
|
// the recap gains "· miss N" on the cache segment.
|
|
907
1048
|
...(missed !== null ? { missed } : {}),
|
package/dist/config.d.ts
CHANGED
|
@@ -49,6 +49,21 @@ export interface ModelProfile {
|
|
|
49
49
|
/** LT-1: milliseconds the model stream may go silent before the request
|
|
50
50
|
* is aborted and retried. Default 120,000; 0 disables the watchdog. */
|
|
51
51
|
readonly streamIdleMs?: number;
|
|
52
|
+
/**
|
|
53
|
+
* The model's context window, in tokens, when YOU know it and nobody
|
|
54
|
+
* else publishes it.
|
|
55
|
+
*
|
|
56
|
+
* The registry carries a window only where a vendor states one, dated
|
|
57
|
+
* and sourced. DeepSeek states none: its /models endpoint returns ids
|
|
58
|
+
* only and its responses carry no window. Without a window the status
|
|
59
|
+
* row has no denominator and shows `ctx ?` rather than a percentage of
|
|
60
|
+
* a number nobody measured.
|
|
61
|
+
*
|
|
62
|
+
* Setting this makes the percentage real FOR YOU, and it is your claim,
|
|
63
|
+
* not ours — which is why it lives in your profile rather than in the
|
|
64
|
+
* registry, where every figure has to carry a source.
|
|
65
|
+
*/
|
|
66
|
+
readonly contextWindow?: number;
|
|
52
67
|
}
|
|
53
68
|
export interface AutoCompactConfig {
|
|
54
69
|
readonly thresholdRatio: number;
|
|
@@ -145,7 +160,7 @@ export declare function resolveModel(modelFlag: string | undefined, merged: Kiso
|
|
|
145
160
|
export declare function resolveModeFromConfig(merged: KisoConfig): Mode | undefined;
|
|
146
161
|
/** Context window: env (KISO_CONTEXT_WINDOW) > config.contextWindow >
|
|
147
162
|
* default (200k — the caller's default). */
|
|
148
|
-
export declare function resolveContextWindow(merged: KisoConfig): number | undefined;
|
|
163
|
+
export declare function resolveContextWindow(merged: KisoConfig, profile?: ModelProfile): number | undefined;
|
|
149
164
|
/** Auto-compact: env (KISO_AUTO_COMPACT) > config.autoCompact > off. An
|
|
150
165
|
* invalid env value is OFF (it set the env → it wins, and it is invalid). */
|
|
151
166
|
export declare function resolveAutoCompact(merged: KisoConfig): AutoCompactConfig | undefined;
|
package/dist/config.js
CHANGED
|
@@ -373,10 +373,15 @@ export function resolveModeFromConfig(merged) {
|
|
|
373
373
|
}
|
|
374
374
|
/** Context window: env (KISO_CONTEXT_WINDOW) > config.contextWindow >
|
|
375
375
|
* default (200k — the caller's default). */
|
|
376
|
-
export function resolveContextWindow(merged) {
|
|
376
|
+
export function resolveContextWindow(merged, profile) {
|
|
377
377
|
const fromEnv = Number.parseInt(process.env.KISO_CONTEXT_WINDOW ?? "", 10);
|
|
378
378
|
if (Number.isFinite(fromEnv) && fromEnv > 0)
|
|
379
379
|
return fromEnv;
|
|
380
|
+
// The PROFILE's window beats the global one: it is stated about a
|
|
381
|
+
// specific model, and the global figure is a default for whatever is
|
|
382
|
+
// selected. Env still beats both — someone who set it meant it.
|
|
383
|
+
if (profile?.contextWindow !== undefined && profile.contextWindow > 0)
|
|
384
|
+
return profile.contextWindow;
|
|
380
385
|
return merged.contextWindow;
|
|
381
386
|
}
|
|
382
387
|
/** Auto-compact: env (KISO_AUTO_COMPACT) > config.autoCompact > off. An
|
package/dist/dispatch.js
CHANGED
|
@@ -8,9 +8,10 @@ import { newSessionId } from "./session-id.js";
|
|
|
8
8
|
import { buildAdapter, lookupModelMetadata, resolveContinuationScope, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
|
|
9
9
|
import { MODES, MODE_NOTE, getMode, setMode } from "./mode.js";
|
|
10
10
|
import { clipboardWrite, lastAnswer } from "./clipboard.js";
|
|
11
|
-
import { agentModel, body, bodyLog, codingToolOptions, kisoHome, configModels, dock, lastBinding, readContextLedger, sessionsDir, setAgentModel, setCurrentModelName, setModelChoice, setLastBinding } from "./state.js";
|
|
11
|
+
import { agentModel, body, bodyLog, codingToolOptions, kisoHome, configModels, dock, lastBinding, mergedConfig, readContextLedger, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setLastBinding } from "./state.js";
|
|
12
12
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
13
|
-
import {
|
|
13
|
+
import { microcompactThresholdFor } from "./chat.js";
|
|
14
|
+
import { authForProfile, directWriteProfile, profileAvailable, resolveContextWindow, unavailableReason } from "./config.js";
|
|
14
15
|
import { shellTool } from "@vincemakes/kiso-tools-node";
|
|
15
16
|
import { join } from "node:path";
|
|
16
17
|
/** The picker's CLI half (owner 2026-09-08): a profile's LEGAL effort levels
|
|
@@ -637,6 +638,23 @@ export function dispatch(line, ctx) {
|
|
|
637
638
|
adapter,
|
|
638
639
|
model: profile.model,
|
|
639
640
|
provider: profile.kind,
|
|
641
|
+
// CTX-1: the compaction threshold is derived from the
|
|
642
|
+
// LIVE model's window, so it moves with the binding.
|
|
643
|
+
// It used to be computed once at startup: switching
|
|
644
|
+
// from a 200k model to a 1M one showed 1,000,000 on
|
|
645
|
+
// the status row while still clearing tool results at
|
|
646
|
+
// 100,000, and switching the other way waited for
|
|
647
|
+
// 500,000 in a window that cannot hold it.
|
|
648
|
+
//
|
|
649
|
+
// PH-F8 fixed this class for the model id, the
|
|
650
|
+
// provider, the endpoint and the scope. This is the
|
|
651
|
+
// field one over that was never asked.
|
|
652
|
+
microcompact: {
|
|
653
|
+
thresholdTokens: microcompactThresholdFor({
|
|
654
|
+
model: profile.model,
|
|
655
|
+
...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
|
|
656
|
+
}),
|
|
657
|
+
},
|
|
640
658
|
// OR-1: the endpoint is the fourth passenger — the cost
|
|
641
659
|
// path and the window lookup key on (model, endpoint).
|
|
642
660
|
...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
|
|
@@ -650,6 +668,13 @@ export function dispatch(line, ctx) {
|
|
|
650
668
|
// §2.5: the ONE source a reload reads for the model — a
|
|
651
669
|
// switch made here must survive the rebuild.
|
|
652
670
|
setModelChoice(arg);
|
|
671
|
+
// The window a PROFILE states travels with the switch,
|
|
672
|
+
// like the model id and the endpoint. Leaving it behind
|
|
673
|
+
// is CTX-1 one field over: the row would show a
|
|
674
|
+
// percentage of the OUTGOING model's window. An
|
|
675
|
+
// explicit env still wins, which resolveContextWindow
|
|
676
|
+
// decides.
|
|
677
|
+
setConfiguredWindow(resolveContextWindow(mergedConfig, profile));
|
|
653
678
|
// OR-7 (owner, 2026-09-09): the status row is repainted BY the
|
|
654
679
|
// switch — it used to keep the old model until the next
|
|
655
680
|
// recap repainted it, so "takes effect on the next turn"
|
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export { applyProjectMerges } from "./trust-ui.js";
|
|
|
31
31
|
*/
|
|
32
32
|
/** The built-in prompt. Exported for scripts/request-surface.mjs — the
|
|
33
33
|
* model-side token-rent counter measures the REAL bytes, never a copy. */
|
|
34
|
-
export declare const SYSTEM_PROMPT = "You are kiso, a coding agent. You work in a workspace\ndirectory and change code with tools. Be concise: answer in a few lines\nunless the task genuinely needs more. Never claim a file was changed\nunless a tool confirmed it.\n\nWhat you can reach:\n- The workspace: read_file, list_dir, search_text, write_file, edit_file.\n- This machine and the network: shell \u2014 builds, tests, git, package\n managers, curl for HTTP, system queries. A request one command can\n answer is answered by running it.\n- The human: ask_user, for a decision that is theirs.\n\nTool discipline:\n- READ BEFORE YOU EDIT. For any file you are about to change, read it\n first \u2014 never guess its content.\n- Use edit_file for targeted changes and write_file for full rewrites.\n Prefer many small edits over one large write.\n- shell is for commands: builds, tests, git, grep. Be careful \u2014 shell has\n side effects and may take time. Run one command at a time and inspect\n the output before continuing.\n- Batch independent tool calls into one reply \u2014 they run in parallel.\n- search_text and list_dir are cheap \u2014 locate first, then read ranges\n with read_file offset/limit; never read a whole large file in one call.\n- Do not re-read a file you already read unchanged \u2014 rely on the earlier\n
|
|
34
|
+
export declare const SYSTEM_PROMPT = "You are kiso, a coding agent. You work in a workspace\ndirectory and change code with tools. Be concise: answer in a few lines\nunless the task genuinely needs more. Never claim a file was changed\nunless a tool confirmed it.\n\nWhat you can reach:\n- The workspace: read_file, list_dir, search_text, write_file, edit_file.\n- This machine and the network: shell \u2014 builds, tests, git, package\n managers, curl for HTTP, system queries. A request one command can\n answer is answered by running it.\n- The human: ask_user, for a decision that is theirs.\n\nTool discipline:\n- READ BEFORE YOU EDIT. For any file you are about to change, read it\n first \u2014 never guess its content.\n- Use edit_file for targeted changes and write_file for full rewrites.\n Prefer many small edits over one large write.\n- shell is for commands: builds, tests, git, grep. Be careful \u2014 shell has\n side effects and may take time. Run one command at a time and inspect\n the output before continuing.\n- Batch independent tool calls into one reply \u2014 they run in parallel.\n- search_text and list_dir are cheap \u2014 locate first, then read ranges\n with read_file offset/limit; never read a whole large file in one call.\n- Do not re-read a file you already read unchanged, or one you changed\n yourself through a confirmed edit \u2014 rely on the earlier result and\n on the change you just made.\n- When a tool fails, read the error and adjust; do not repeat the same\n call blindly.\n\nAn authorization covers what it NAMES: when the scope is not named \u2014\nwhich files, which branches, whether to push \u2014 ask before acting.\nDelivering, sharing or handing over changes means committing locally and\nstopping; pushing, publishing or sending happens only when the human\nnames it.\n\nWorkflow: understand the request, find the relevant code, make the\nsmallest change that works, then verify with a command (tests/build).\nReport what you did in one or two lines per change.";
|
|
35
35
|
/** Hard cap for injected instructions — truncate and say so. */
|
|
36
36
|
/**
|
|
37
37
|
* A area: read the FIRST present instruction file (AGENTS.md preferred) and
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ import { agentModel, atFiles, body, bodyLog, codingToolOptions, kisoHome, builtI
|
|
|
41
41
|
import { askUi, resolveProjectTrust } from "./trust-ui.js";
|
|
42
42
|
import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
|
|
43
43
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
44
|
-
import { chat,
|
|
44
|
+
import { chat, displayCtxRatio, microcompactThresholdFor, statusModelLabel } from "./chat.js";
|
|
45
45
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
46
46
|
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
|
|
47
47
|
import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
|
|
@@ -598,8 +598,9 @@ Tool discipline:
|
|
|
598
598
|
- Batch independent tool calls into one reply — they run in parallel.
|
|
599
599
|
- search_text and list_dir are cheap — locate first, then read ranges
|
|
600
600
|
with read_file offset/limit; never read a whole large file in one call.
|
|
601
|
-
- Do not re-read a file you already read unchanged
|
|
602
|
-
result
|
|
601
|
+
- Do not re-read a file you already read unchanged, or one you changed
|
|
602
|
+
yourself through a confirmed edit — rely on the earlier result and
|
|
603
|
+
on the change you just made.
|
|
603
604
|
- When a tool fails, read the error and adjust; do not repeat the same
|
|
604
605
|
call blindly.
|
|
605
606
|
|
|
@@ -739,8 +740,12 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
739
740
|
// the environment. A model never supplies a command; it names a check.
|
|
740
741
|
process.env.KISO_DELEGATION_CONFIG_JSON = JSON.stringify({ checks: merged.checks ?? {}, profiles: Object.keys(merged.models ?? {}) });
|
|
741
742
|
setConfigModels(merged.models ?? {});
|
|
742
|
-
setConfiguredWindow(resolveContextWindow(merged));
|
|
743
743
|
const resolved = resolveModel(modelFlag, merged);
|
|
744
|
+
// AFTER the model resolves: the window a PROFILE states is about that
|
|
745
|
+
// profile's model, so it cannot be read before we know which profile is
|
|
746
|
+
// selected. Reading it a line too early is how the compaction threshold
|
|
747
|
+
// ended up frozen at the wrong model's value (CTX-1).
|
|
748
|
+
setConfiguredWindow(resolveContextWindow(merged, resolved?.profile));
|
|
744
749
|
const model = resolved === null ? "faux" : resolved.profile.model;
|
|
745
750
|
if (resolved === null) {
|
|
746
751
|
console.log("[faux mode — set ANTHROPIC_API_KEY or OPENAI_API_KEY, or configure models in ~/.kiso/config.json]\n");
|
|
@@ -784,7 +789,9 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
784
789
|
// half the model window (KISO_CONTEXT_WINDOW override included;
|
|
785
790
|
// 200k window → 100k tokens). Long sessions compact old read/list/
|
|
786
791
|
// search/shell outputs instead of silently growing past the window.
|
|
787
|
-
|
|
792
|
+
// CTX-1: the startup value; `/model` recomputes it through the same
|
|
793
|
+
// function, so the two can never drift apart.
|
|
794
|
+
microcompact: { thresholdTokens: microcompactThresholdFor() },
|
|
788
795
|
// E6: the run-start context policy — OFF unless env-armed (beats the microcompact default when both fire).
|
|
789
796
|
...(contextPolicy !== undefined ? { contextPolicy } : {}),
|
|
790
797
|
// R3e (owner ruling, 2026-08-28): NO turn limit on an interactive
|
|
@@ -887,6 +894,33 @@ async function pickSession(agent, input) {
|
|
|
887
894
|
* formatter — never a boot-time copy, which would drift from the real
|
|
888
895
|
* row the moment either changed.
|
|
889
896
|
*/
|
|
897
|
+
/**
|
|
898
|
+
* CTX-1 (Astra F34-1): EVERY entry point that opens a session must bind the
|
|
899
|
+
* policy that follows the session's OWN model.
|
|
900
|
+
*
|
|
901
|
+
* `agent.session()` restores the model recorded in the durable profile —
|
|
902
|
+
* any model that session ever used, not necessarily the one this process
|
|
903
|
+
* started on. The display and the compaction threshold are both derived
|
|
904
|
+
* from it, so both belong here.
|
|
905
|
+
*
|
|
906
|
+
* The first fix put these two lines in chatLoop only. `kiso resume <id>`
|
|
907
|
+
* and `kiso -p <text> <id>` open through their own paths, restored the
|
|
908
|
+
* model, and kept the STARTUP threshold: a session recorded on a 1M model,
|
|
909
|
+
* resumed from a 200k start, cleared its tool results at 100,000 while the
|
|
910
|
+
* interactive entry point cleared at 500,000. Three doors, one of them
|
|
911
|
+
* fixed, is the same defect with a smaller blast radius.
|
|
912
|
+
*
|
|
913
|
+
* The pair is passed EXPLICITLY, never read back out of the display state
|
|
914
|
+
* set on the line above: depending on two lines staying in order is how the
|
|
915
|
+
* threshold got stuck in the first place.
|
|
916
|
+
*/
|
|
917
|
+
function bindRestoredSession(session) {
|
|
918
|
+
setAgentModel(session.model, session.baseUrl);
|
|
919
|
+
session.setMicrocompactThreshold(microcompactThresholdFor({
|
|
920
|
+
model: session.model,
|
|
921
|
+
...(session.baseUrl !== undefined ? { baseUrl: session.baseUrl } : {}),
|
|
922
|
+
}));
|
|
923
|
+
}
|
|
890
924
|
function paintBootStatus(session) {
|
|
891
925
|
if (!dock.active)
|
|
892
926
|
return;
|
|
@@ -1089,7 +1123,9 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1089
1123
|
// XP-1 §2.1: the switched-to session's OWN truth repaints the row —
|
|
1090
1124
|
// the global display state never outlives the session it described
|
|
1091
1125
|
// (pre-XP the row kept the previous session's /model selection).
|
|
1092
|
-
|
|
1126
|
+
// CTX-1: the restored session's own model decides the display AND
|
|
1127
|
+
// the threshold. One step, shared by all three entry points.
|
|
1128
|
+
bindRestoredSession(session);
|
|
1093
1129
|
setCurrentModelName(session.model);
|
|
1094
1130
|
paintBootStatus(session);
|
|
1095
1131
|
const nav = {
|
|
@@ -1377,6 +1413,7 @@ async function main() {
|
|
|
1377
1413
|
agent = await makeAgent(id, input, modelFlag);
|
|
1378
1414
|
applyConfigMode();
|
|
1379
1415
|
const session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
|
|
1416
|
+
bindRestoredSession(session); // CTX-1 (F34-1): the -p door, too
|
|
1380
1417
|
faux = currentFaux;
|
|
1381
1418
|
await resume(session, printPrompt, faux, input);
|
|
1382
1419
|
const last = [...session.log.all].reverse().find((e) => e.type === "terminal");
|
|
@@ -1431,6 +1468,7 @@ async function main() {
|
|
|
1431
1468
|
break;
|
|
1432
1469
|
}
|
|
1433
1470
|
const session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
|
|
1471
|
+
bindRestoredSession(session); // CTX-1 (F34-1): the explicit-id door
|
|
1434
1472
|
faux = currentFaux;
|
|
1435
1473
|
// E area: the durable script position is computed from the
|
|
1436
1474
|
// session id, and on the picker path the id did not exist when
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
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.
|
|
22
|
-
"@vincemakes/kiso-core": "0.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
27
|
-
"@vincemakes/kiso-provider-openai-responses": "0.
|
|
28
|
-
"@vincemakes/kiso-runtime": "0.
|
|
29
|
-
"@vincemakes/kiso-skills-ext": "0.
|
|
30
|
-
"@vincemakes/kiso-subagent-ext": "0.
|
|
31
|
-
"@vincemakes/kiso-task-ext": "0.
|
|
32
|
-
"@vincemakes/kiso-tools-node": "0.
|
|
33
|
-
"@vincemakes/kiso-tui": "0.
|
|
34
|
-
"@vincemakes/kiso-tui-cells": "0.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.38.0",
|
|
22
|
+
"@vincemakes/kiso-core": "0.38.0",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.38.0",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.38.0",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.38.0",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.38.0",
|
|
27
|
+
"@vincemakes/kiso-provider-openai-responses": "0.38.0",
|
|
28
|
+
"@vincemakes/kiso-runtime": "0.38.0",
|
|
29
|
+
"@vincemakes/kiso-skills-ext": "0.38.0",
|
|
30
|
+
"@vincemakes/kiso-subagent-ext": "0.38.0",
|
|
31
|
+
"@vincemakes/kiso-task-ext": "0.38.0",
|
|
32
|
+
"@vincemakes/kiso-tools-node": "0.38.0",
|
|
33
|
+
"@vincemakes/kiso-tui": "0.38.0",
|
|
34
|
+
"@vincemakes/kiso-tui-cells": "0.38.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^26.1.2",
|