@giovannijecha/jecode 0.5.0 → 0.6.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/README.md +19 -2
- package/dist/batch.js +54 -4
- package/dist/cli-info.js +3 -0
- package/dist/config.js +12 -0
- package/dist/context/capacity.js +18 -0
- package/dist/context/compactor.js +60 -0
- package/dist/context/policy.js +125 -0
- package/dist/context/projection.js +49 -0
- package/dist/controller-request.js +34 -0
- package/dist/controller.js +25 -18
- package/dist/conversation.js +31 -3
- package/dist/providers/anthropic.js +33 -3
- package/dist/providers/catalog.js +11 -4
- package/dist/providers/ollama.js +75 -1
- package/dist/providers/openai-codex.js +46 -2
- package/dist/providers/openai.js +17 -0
- package/dist/sessions/codec.js +39 -7
- package/dist/sessions/store.js +5 -5
- package/dist/settings-command.js +42 -2
- package/dist/settings.js +9 -0
- package/dist/tui/app-workflows.js +75 -4
- package/dist/usage.js +8 -0
- package/package.json +1 -1
package/dist/conversation.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// Canonical settled conversation state.
|
|
2
2
|
//
|
|
3
3
|
// A node owns one complete user-turn delta. The selected root-to-node path is
|
|
4
|
-
// the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// the durable source for the transcript and full history; context anchors can
|
|
5
|
+
// project an older prefix before it is sent to a provider. Provider traffic
|
|
6
|
+
// and live screen blocks remain prospective until a consistent checkpoint.
|
|
7
|
+
import { projectContext, validContextAnchor } from "./context/projection.js";
|
|
7
8
|
export const CONVERSATION_LIMITS = Object.freeze({
|
|
8
9
|
nodes: 1_024,
|
|
9
10
|
messageCodeUnits: 8_388_608,
|
|
10
11
|
transcriptCodeUnits: 8_388_608,
|
|
12
|
+
contextCodeUnits: 8_388_608,
|
|
11
13
|
});
|
|
12
14
|
/** Immutable tree with one selected model/transcript path. */
|
|
13
15
|
export class ConversationTree {
|
|
@@ -34,6 +36,7 @@ export class ConversationTree {
|
|
|
34
36
|
identity: node.identity,
|
|
35
37
|
messages: node.messages,
|
|
36
38
|
blocks: node.blocks,
|
|
39
|
+
...(node.context === undefined ? {} : { context: node.context }),
|
|
37
40
|
}, node.settlement);
|
|
38
41
|
const restored = tree.activeNode;
|
|
39
42
|
if (restored === undefined || restored.id !== node.id) {
|
|
@@ -92,6 +95,9 @@ export class ConversationTree {
|
|
|
92
95
|
get history() {
|
|
93
96
|
return this.#path().flatMap((node) => clone(node.messages));
|
|
94
97
|
}
|
|
98
|
+
get contextHistory() {
|
|
99
|
+
return projectContext(this.#path());
|
|
100
|
+
}
|
|
95
101
|
get transcript() {
|
|
96
102
|
return this.#path().flatMap((node) => clone(node.blocks));
|
|
97
103
|
}
|
|
@@ -108,6 +114,7 @@ export class ConversationTree {
|
|
|
108
114
|
identity: draft.identity,
|
|
109
115
|
messages: draft.messages,
|
|
110
116
|
blocks: settledBlocks(draft.blocks),
|
|
117
|
+
...(draft.context === undefined ? {} : { context: draft.context }),
|
|
111
118
|
});
|
|
112
119
|
assertTurn(node);
|
|
113
120
|
const nodes = [...this.#nodes, node];
|
|
@@ -127,6 +134,7 @@ export class ConversationTree {
|
|
|
127
134
|
identity: draft.identity,
|
|
128
135
|
messages: draft.messages,
|
|
129
136
|
blocks: settledBlocks(draft.blocks),
|
|
137
|
+
context: draft.context ?? current.context,
|
|
130
138
|
});
|
|
131
139
|
assertTurn(node);
|
|
132
140
|
const nodes = [...this.#nodes];
|
|
@@ -154,6 +162,7 @@ function ownedNode(node) {
|
|
|
154
162
|
identity: Object.freeze({ ...node.identity }),
|
|
155
163
|
messages: Object.freeze(clone(node.messages)),
|
|
156
164
|
blocks: Object.freeze(clone(node.blocks)),
|
|
165
|
+
...(node.context === undefined ? {} : { context: Object.freeze({ ...node.context }) }),
|
|
157
166
|
});
|
|
158
167
|
}
|
|
159
168
|
function settledBlocks(blocks) {
|
|
@@ -189,9 +198,13 @@ function assertTurn(node) {
|
|
|
189
198
|
function assertBounds(nodes) {
|
|
190
199
|
let messageCodeUnits = 0;
|
|
191
200
|
let transcriptCodeUnits = 0;
|
|
201
|
+
let contextCodeUnits = 0;
|
|
192
202
|
for (const node of nodes) {
|
|
193
203
|
messageCodeUnits += JSON.stringify(node.messages).length;
|
|
194
204
|
transcriptCodeUnits += JSON.stringify(node.blocks).length;
|
|
205
|
+
contextCodeUnits += node.context?.summary.length ?? 0;
|
|
206
|
+
if (node.context !== undefined)
|
|
207
|
+
assertContextPath(nodes, node);
|
|
195
208
|
}
|
|
196
209
|
if (messageCodeUnits > CONVERSATION_LIMITS.messageCodeUnits) {
|
|
197
210
|
throw new Error("conversation model history reached its session limit — start /new");
|
|
@@ -199,6 +212,21 @@ function assertBounds(nodes) {
|
|
|
199
212
|
if (transcriptCodeUnits > CONVERSATION_LIMITS.transcriptCodeUnits) {
|
|
200
213
|
throw new Error("conversation transcript reached its session limit — start /new");
|
|
201
214
|
}
|
|
215
|
+
if (contextCodeUnits > CONVERSATION_LIMITS.contextCodeUnits) {
|
|
216
|
+
throw new Error("conversation context summaries reached their session limit — start /new");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function assertContextPath(nodes, owner) {
|
|
220
|
+
const context = owner.context;
|
|
221
|
+
const boundary = nodes[context.throughNodeId - 1];
|
|
222
|
+
if (boundary === undefined || !validContextAnchor(context, boundary.messages.length)) {
|
|
223
|
+
throw new Error("turn context checkpoint is invalid");
|
|
224
|
+
}
|
|
225
|
+
let id = owner.id;
|
|
226
|
+
while (id !== 0 && id !== boundary.id)
|
|
227
|
+
id = nodes[id - 1]?.parentId ?? 0;
|
|
228
|
+
if (id !== boundary.id)
|
|
229
|
+
throw new Error("turn context checkpoint is outside its branch");
|
|
202
230
|
}
|
|
203
231
|
function validNodeId(value) {
|
|
204
232
|
return Number.isSafeInteger(value) && value >= 0;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// purpose: the default omits the reasoning text, which on a screen reads as a
|
|
8
8
|
// long silence before anything appears.
|
|
9
9
|
import { postSse } from "./http.js";
|
|
10
|
-
import {
|
|
10
|
+
import { modelCatalog } from "./catalog.js";
|
|
11
11
|
import { keyFor } from "../credentials.js";
|
|
12
12
|
import { EFFORTS, requireSupportedEffort } from "../effort.js";
|
|
13
13
|
import { assembleAnthropic } from "./anthropic-stream.js";
|
|
@@ -19,6 +19,7 @@ const KEY = "ANTHROPIC_API_KEY";
|
|
|
19
19
|
const ADAPTIVE = /^claude-(?:fable-5|mythos-(?:5|preview)|opus-(?:5|4-[678])|sonnet-(?:5|4-6))(?:-|$)/;
|
|
20
20
|
const MAX_WITHOUT_XHIGH = ["low", "medium", "high", "max"];
|
|
21
21
|
const ANTHROPIC_45_EFFORTS = ["low", "medium", "high"];
|
|
22
|
+
let contextByModel = new Map();
|
|
22
23
|
export function supportsAdaptiveThinking(model) {
|
|
23
24
|
return ADAPTIVE.test(model);
|
|
24
25
|
}
|
|
@@ -41,12 +42,24 @@ export const anthropic = {
|
|
|
41
42
|
},
|
|
42
43
|
// Newest first is how the endpoint already answers, so the order is left
|
|
43
44
|
// exactly as it arrives rather than re-sorted into something less useful.
|
|
44
|
-
models(signal, onStatus) {
|
|
45
|
-
|
|
45
|
+
async models(signal, onStatus) {
|
|
46
|
+
const catalog = await loadModels(signal, onStatus);
|
|
47
|
+
contextByModel = catalog.contexts;
|
|
48
|
+
return catalog.ids;
|
|
46
49
|
},
|
|
47
50
|
async efforts(model) {
|
|
48
51
|
return anthropicEfforts(model);
|
|
49
52
|
},
|
|
53
|
+
async contextWindow(model, signal, onStatus) {
|
|
54
|
+
if (contextByModel.has(model))
|
|
55
|
+
return contextByModel.get(model);
|
|
56
|
+
const catalog = await loadModels(signal, onStatus);
|
|
57
|
+
contextByModel = catalog.contexts;
|
|
58
|
+
const context = contextByModel.get(model);
|
|
59
|
+
if (!contextByModel.has(model))
|
|
60
|
+
contextByModel.set(model, undefined);
|
|
61
|
+
return context;
|
|
62
|
+
},
|
|
50
63
|
location: () => "cloud",
|
|
51
64
|
async send(req) {
|
|
52
65
|
const key = requireKey();
|
|
@@ -77,6 +90,23 @@ export const anthropic = {
|
|
|
77
90
|
return fromWireResponse(data);
|
|
78
91
|
},
|
|
79
92
|
};
|
|
93
|
+
async function loadModels(signal, onStatus) {
|
|
94
|
+
const entries = await modelCatalog(MODELS, headers(requireKey()), signal, onStatus);
|
|
95
|
+
return {
|
|
96
|
+
ids: entries.map((entry) => entry.id),
|
|
97
|
+
contexts: new Map(entries.map((entry) => [
|
|
98
|
+
entry.id,
|
|
99
|
+
contextWindow(entry.metadata["max_input_tokens"]),
|
|
100
|
+
])),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function contextWindow(value) {
|
|
104
|
+
return validTokenCount(value) ? Object.freeze({ tokens: value }) : undefined;
|
|
105
|
+
}
|
|
106
|
+
function validTokenCount(value) {
|
|
107
|
+
return typeof value === "number" && Number.isSafeInteger(value) &&
|
|
108
|
+
value >= 4_096 && value <= 10_000_000;
|
|
109
|
+
}
|
|
80
110
|
// Read at the moment it is used, never captured at import: a key typed into
|
|
81
111
|
// the running window has to count, and so does one exported after startup.
|
|
82
112
|
function apiKey() {
|
|
@@ -13,6 +13,9 @@ export const MAX_MODEL_CATALOG_ENTRIES = 1_000;
|
|
|
13
13
|
export const MAX_MODEL_CATALOG_ITEMS = 4_000;
|
|
14
14
|
export const MAX_MODEL_ID_CHARS = 256;
|
|
15
15
|
export async function listModels(url, headers, signal, onStatus) {
|
|
16
|
+
return (await modelCatalog(url, headers, signal, onStatus)).map((entry) => entry.id);
|
|
17
|
+
}
|
|
18
|
+
export async function modelCatalog(url, headers, signal, onStatus) {
|
|
16
19
|
const body = await getJson(url, headers, signal, onStatus);
|
|
17
20
|
const data = body.data;
|
|
18
21
|
if (!Array.isArray(data))
|
|
@@ -22,16 +25,20 @@ export async function listModels(url, headers, signal, onStatus) {
|
|
|
22
25
|
const inspected = Math.min(data.length, MAX_MODEL_CATALOG_ITEMS);
|
|
23
26
|
for (let index = 0; index < inspected && models.length < MAX_MODEL_CATALOG_ENTRIES; index++) {
|
|
24
27
|
const entry = data[index];
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
const metadata = record(entry) ? entry : undefined;
|
|
29
|
+
if (metadata === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const id = metadata["id"];
|
|
28
32
|
if (typeof id !== "string" ||
|
|
29
33
|
id === "" ||
|
|
30
34
|
id.length > MAX_MODEL_ID_CHARS ||
|
|
31
35
|
unique.has(id))
|
|
32
36
|
continue;
|
|
33
37
|
unique.add(id);
|
|
34
|
-
models.push(id);
|
|
38
|
+
models.push({ id, metadata });
|
|
35
39
|
}
|
|
36
40
|
return models;
|
|
37
41
|
}
|
|
42
|
+
function record(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
package/dist/providers/ollama.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// pulled or the subscription grants — so the model has to be named with
|
|
7
7
|
// --model.
|
|
8
8
|
import { requireSupportedEffort } from "../effort.js";
|
|
9
|
-
import { postSse } from "./http.js";
|
|
9
|
+
import { getJson, postJson, postSse } from "./http.js";
|
|
10
10
|
import { listModels } from "./catalog.js";
|
|
11
11
|
import { keyFor } from "../credentials.js";
|
|
12
12
|
import { assembleOllama } from "./ollama-stream.js";
|
|
@@ -16,6 +16,8 @@ const KEY = "OLLAMA_API_KEY";
|
|
|
16
16
|
// Ollama also accepts `none`; Jecode's product-wide reasoning floor is `low`.
|
|
17
17
|
const OLLAMA_EFFORTS = ["low", "medium", "high"];
|
|
18
18
|
let configuredHost;
|
|
19
|
+
const CONTEXT_CACHE_MS = 30_000;
|
|
20
|
+
const contextByEndpoint = new Map();
|
|
19
21
|
/** Set the endpoint selected for this process. Undefined restores key-aware inference. */
|
|
20
22
|
export function configureOllama(host) {
|
|
21
23
|
configuredHost = host === undefined ? undefined : parseOllamaEndpoint(host).baseUrl;
|
|
@@ -51,6 +53,21 @@ export const ollama = {
|
|
|
51
53
|
async efforts() {
|
|
52
54
|
return OLLAMA_EFFORTS;
|
|
53
55
|
},
|
|
56
|
+
async contextWindow(model, signal, onStatus) {
|
|
57
|
+
const at = endpoint();
|
|
58
|
+
const cacheKey = `${at.baseUrl}\u0000${model}`;
|
|
59
|
+
const cached = contextByEndpoint.get(cacheKey);
|
|
60
|
+
if (cached !== undefined && cached.expiresAt > Date.now())
|
|
61
|
+
return cached.value;
|
|
62
|
+
const observed = await nativeContextWindow(at, model, signal, onStatus);
|
|
63
|
+
if (observed?.runtime === true) {
|
|
64
|
+
contextByEndpoint.set(cacheKey, {
|
|
65
|
+
value: observed.value,
|
|
66
|
+
expiresAt: Date.now() + CONTEXT_CACHE_MS,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return observed?.value;
|
|
70
|
+
},
|
|
54
71
|
location: () => {
|
|
55
72
|
try {
|
|
56
73
|
return endpoint().loopback ? "local" : "cloud";
|
|
@@ -79,6 +96,63 @@ export const ollama = {
|
|
|
79
96
|
return fromWireReply(reply);
|
|
80
97
|
},
|
|
81
98
|
};
|
|
99
|
+
async function nativeContextWindow(at, model, signal, onStatus) {
|
|
100
|
+
try {
|
|
101
|
+
const running = await getJson(`${at.baseUrl}/api/ps`, headers(at), signal, onStatus);
|
|
102
|
+
const allocated = runningContext(running, model);
|
|
103
|
+
if (allocated !== undefined)
|
|
104
|
+
return { value: usableContext(allocated), runtime: true };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throwIfAborted(signal, error);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const details = await postJson(`${at.baseUrl}/api/show`, headers(at), { model }, signal, onStatus);
|
|
111
|
+
const capacity = modelCapacity(details);
|
|
112
|
+
return capacity === undefined
|
|
113
|
+
? undefined
|
|
114
|
+
: { value: usableContext(capacity), runtime: false };
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
throwIfAborted(signal, error);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function runningContext(value, model) {
|
|
122
|
+
if (!record(value) || !Array.isArray(value["models"]))
|
|
123
|
+
return undefined;
|
|
124
|
+
for (const entry of value["models"]) {
|
|
125
|
+
if (!record(entry))
|
|
126
|
+
continue;
|
|
127
|
+
if (entry["name"] !== model && entry["model"] !== model)
|
|
128
|
+
continue;
|
|
129
|
+
if (validTokenCount(entry["context_length"]))
|
|
130
|
+
return entry["context_length"];
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function modelCapacity(value) {
|
|
135
|
+
if (!record(value) || !record(value["model_info"]))
|
|
136
|
+
return undefined;
|
|
137
|
+
const capacities = Object.entries(value["model_info"])
|
|
138
|
+
.filter(([name, count]) => name.endsWith(".context_length") && validTokenCount(count))
|
|
139
|
+
.map(([, count]) => count);
|
|
140
|
+
return capacities.length === 0 ? undefined : Math.max(...capacities);
|
|
141
|
+
}
|
|
142
|
+
function validTokenCount(value) {
|
|
143
|
+
return typeof value === "number" && Number.isSafeInteger(value) &&
|
|
144
|
+
value >= 4_096 && value <= 10_000_000;
|
|
145
|
+
}
|
|
146
|
+
function usableContext(tokens) {
|
|
147
|
+
return Object.freeze({ tokens: Math.floor(tokens * 95 / 100) });
|
|
148
|
+
}
|
|
149
|
+
function record(value) {
|
|
150
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
151
|
+
}
|
|
152
|
+
function throwIfAborted(signal, error) {
|
|
153
|
+
if (signal?.aborted === true)
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
82
156
|
function endpoint() {
|
|
83
157
|
return ollamaConnection();
|
|
84
158
|
}
|
|
@@ -19,6 +19,7 @@ const MAX_MODELS = 1_000;
|
|
|
19
19
|
const MAX_MODEL_CHARS = 256;
|
|
20
20
|
const XHIGH_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
21
21
|
let effortByModel = new Map();
|
|
22
|
+
let contextByModel = new Map();
|
|
22
23
|
export const openaiCodex = {
|
|
23
24
|
id: ID,
|
|
24
25
|
defaultModel: "",
|
|
@@ -29,7 +30,7 @@ export const openaiCodex = {
|
|
|
29
30
|
location: () => "cloud",
|
|
30
31
|
async models(signal, onStatus) {
|
|
31
32
|
const catalog = await loadCatalog(signal, onStatus);
|
|
32
|
-
|
|
33
|
+
rememberCatalog(catalog);
|
|
33
34
|
return catalog.ids;
|
|
34
35
|
},
|
|
35
36
|
async efforts(model, signal, onStatus) {
|
|
@@ -37,9 +38,19 @@ export const openaiCodex = {
|
|
|
37
38
|
if (cached !== undefined)
|
|
38
39
|
return cached;
|
|
39
40
|
const catalog = await loadCatalog(signal, onStatus);
|
|
40
|
-
|
|
41
|
+
rememberCatalog(catalog);
|
|
41
42
|
return effortByModel.get(model) ?? fallbackEfforts(model);
|
|
42
43
|
},
|
|
44
|
+
async contextWindow(model, signal, onStatus) {
|
|
45
|
+
if (contextByModel.has(model))
|
|
46
|
+
return contextByModel.get(model);
|
|
47
|
+
const catalog = await loadCatalog(signal, onStatus);
|
|
48
|
+
rememberCatalog(catalog);
|
|
49
|
+
const context = contextByModel.get(model);
|
|
50
|
+
if (!contextByModel.has(model))
|
|
51
|
+
contextByModel.set(model, undefined);
|
|
52
|
+
return context;
|
|
53
|
+
},
|
|
43
54
|
async send(req) {
|
|
44
55
|
const efforts = effortByModel.get(req.model) ?? fallbackEfforts(req.model);
|
|
45
56
|
const effort = requireSupportedEffort(req.model, req.effort, efforts);
|
|
@@ -75,6 +86,10 @@ async function loadCatalog(signal, onStatus) {
|
|
|
75
86
|
return modelCatalog(body);
|
|
76
87
|
}, signal, onStatus);
|
|
77
88
|
}
|
|
89
|
+
function rememberCatalog(catalog) {
|
|
90
|
+
effortByModel = catalog.efforts;
|
|
91
|
+
contextByModel = catalog.contexts;
|
|
92
|
+
}
|
|
78
93
|
async function withAuthorization(operation, signal, onStatus) {
|
|
79
94
|
let authorization = await openAIAuthorization(undefined, signal, onStatus);
|
|
80
95
|
try {
|
|
@@ -120,6 +135,7 @@ function modelCatalog(value) {
|
|
|
120
135
|
id,
|
|
121
136
|
priority: typeof entry["priority"] === "number" ? entry["priority"] : 0,
|
|
122
137
|
efforts: reasoningLevels(entry, id),
|
|
138
|
+
context: modelContextWindow(entry),
|
|
123
139
|
}];
|
|
124
140
|
})
|
|
125
141
|
.sort((left, right) => left.priority - right.priority)
|
|
@@ -127,8 +143,36 @@ function modelCatalog(value) {
|
|
|
127
143
|
return {
|
|
128
144
|
ids: models.map((entry) => entry.id),
|
|
129
145
|
efforts: new Map(models.map((entry) => [entry.id, entry.efforts])),
|
|
146
|
+
contexts: new Map(models.map((entry) => [entry.id, entry.context])),
|
|
130
147
|
};
|
|
131
148
|
}
|
|
149
|
+
function modelContextWindow(entry) {
|
|
150
|
+
const resolved = tokenCount(entry["context_window"]) ?? tokenCount(entry["max_context_window"]);
|
|
151
|
+
if (resolved === undefined)
|
|
152
|
+
return undefined;
|
|
153
|
+
const percent = percentage(entry["effective_context_window_percent"]) ?? 95;
|
|
154
|
+
const tokens = Math.floor(resolved * percent / 100);
|
|
155
|
+
if (!validTokenCount(tokens))
|
|
156
|
+
return undefined;
|
|
157
|
+
const automatic = Math.floor(resolved * 9 / 10);
|
|
158
|
+
const advertised = tokenCount(entry["auto_compact_token_limit"]);
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
tokens,
|
|
161
|
+
compactAtTokens: Math.min(advertised ?? automatic, automatic, tokens),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function tokenCount(value) {
|
|
165
|
+
return validTokenCount(value) ? value : undefined;
|
|
166
|
+
}
|
|
167
|
+
function validTokenCount(value) {
|
|
168
|
+
return typeof value === "number" && Number.isSafeInteger(value) &&
|
|
169
|
+
value >= 4_096 && value <= 10_000_000;
|
|
170
|
+
}
|
|
171
|
+
function percentage(value) {
|
|
172
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= 100
|
|
173
|
+
? value
|
|
174
|
+
: undefined;
|
|
175
|
+
}
|
|
132
176
|
function reasoningLevels(entry, model) {
|
|
133
177
|
const source = entry["supported_reasoning_levels"];
|
|
134
178
|
if (!Array.isArray(source))
|
package/dist/providers/openai.js
CHANGED
|
@@ -37,6 +37,20 @@ export function openAIEfforts(model) {
|
|
|
37
37
|
return STANDARD_EFFORTS;
|
|
38
38
|
return STANDARD_EFFORTS;
|
|
39
39
|
}
|
|
40
|
+
/** Conservative capacities for the reasoning families accepted by this transport. */
|
|
41
|
+
export function openAIContextWindow(model) {
|
|
42
|
+
if (/^gpt-5\.6(?:[.-]|$)/.test(model))
|
|
43
|
+
return usableContext(1_050_000);
|
|
44
|
+
if (/^gpt-5(?:[.-]|$)/.test(model))
|
|
45
|
+
return usableContext(400_000);
|
|
46
|
+
if (/^(?:o(?:1|3|4)|codex-mini)(?:[.-]|$)/.test(model)) {
|
|
47
|
+
return usableContext(200_000);
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
function usableContext(tokens) {
|
|
52
|
+
return Object.freeze({ tokens: Math.floor(tokens * 95 / 100) });
|
|
53
|
+
}
|
|
40
54
|
export const openai = {
|
|
41
55
|
id: "openai",
|
|
42
56
|
defaultModel: "gpt-5",
|
|
@@ -55,6 +69,9 @@ export const openai = {
|
|
|
55
69
|
async efforts(model) {
|
|
56
70
|
return openAIEfforts(model);
|
|
57
71
|
},
|
|
72
|
+
async contextWindow(model) {
|
|
73
|
+
return openAIContextWindow(model);
|
|
74
|
+
},
|
|
58
75
|
location: () => "cloud",
|
|
59
76
|
async send(req) {
|
|
60
77
|
const key = requireKey();
|
package/dist/sessions/codec.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Strict codecs for session files. Disk is an untrusted boundary even when
|
|
2
2
|
// the directory is owner-only: every value is bounded and re-owned before it
|
|
3
3
|
// can become conversation or provider input.
|
|
4
|
-
|
|
4
|
+
import { CONTEXT_LIMITS } from "../context/projection.js";
|
|
5
|
+
export const SESSION_SCHEMA = 2;
|
|
5
6
|
export const SESSION_FILE_LIMITS = Object.freeze({
|
|
6
7
|
text: 1_048_576,
|
|
7
8
|
jsonDepth: 24,
|
|
@@ -16,14 +17,14 @@ export function decodeMeta(value) {
|
|
|
16
17
|
if (!record(value) || !keys(value, "createdAt,id,version,workspaceDigest,workspaceRoot")) {
|
|
17
18
|
throw invalid();
|
|
18
19
|
}
|
|
19
|
-
if (value["version"]
|
|
20
|
+
if (!schema(value["version"]) ||
|
|
20
21
|
!identifier(value["id"]) ||
|
|
21
22
|
!bounded(value["workspaceRoot"], 32_768) ||
|
|
22
23
|
!digest(value["workspaceDigest"]) ||
|
|
23
24
|
!timestamp(value["createdAt"]))
|
|
24
25
|
throw invalid();
|
|
25
26
|
return Object.freeze({
|
|
26
|
-
version:
|
|
27
|
+
version: value["version"],
|
|
27
28
|
id: value["id"],
|
|
28
29
|
workspaceRoot: value["workspaceRoot"],
|
|
29
30
|
workspaceDigest: value["workspaceDigest"],
|
|
@@ -37,7 +38,7 @@ export function decodeHead(value) {
|
|
|
37
38
|
if (!record(value) || !keys(value, "nodeId,parentId,revision,sequence,updatedAt,version")) {
|
|
38
39
|
throw invalid();
|
|
39
40
|
}
|
|
40
|
-
if (value["version"]
|
|
41
|
+
if (!schema(value["version"]) ||
|
|
41
42
|
!integer(value["sequence"], 0) ||
|
|
42
43
|
!integer(value["nodeId"], 1) ||
|
|
43
44
|
!integer(value["parentId"], 0) || value["parentId"] >= value["nodeId"] ||
|
|
@@ -45,7 +46,7 @@ export function decodeHead(value) {
|
|
|
45
46
|
!timestamp(value["updatedAt"]))
|
|
46
47
|
throw invalid();
|
|
47
48
|
return Object.freeze({
|
|
48
|
-
version:
|
|
49
|
+
version: value["version"],
|
|
49
50
|
sequence: value["sequence"],
|
|
50
51
|
nodeId: value["nodeId"],
|
|
51
52
|
parentId: value["parentId"],
|
|
@@ -67,17 +68,22 @@ export function encodeNode(node, sequence, updatedAt) {
|
|
|
67
68
|
identity: node.identity,
|
|
68
69
|
messages: node.messages.map(messageRecord),
|
|
69
70
|
blocks: node.blocks.flatMap(blockRecord),
|
|
71
|
+
context: node.context ?? null,
|
|
70
72
|
},
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
75
|
export function decodeNode(value) {
|
|
74
76
|
if (!record(value) || !keys(value, "node,sequence,updatedAt,version"))
|
|
75
77
|
throw invalid();
|
|
76
|
-
|
|
78
|
+
const version = value["version"];
|
|
79
|
+
if (!schema(version) || !integer(value["sequence"], 1) ||
|
|
77
80
|
!timestamp(value["updatedAt"]))
|
|
78
81
|
throw invalid();
|
|
79
82
|
const raw = value["node"];
|
|
80
|
-
|
|
83
|
+
const nodeKeys = version === 1
|
|
84
|
+
? "blocks,createdAt,id,identity,messages,parentId,revision,settlement"
|
|
85
|
+
: "blocks,context,createdAt,id,identity,messages,parentId,revision,settlement";
|
|
86
|
+
if (!record(raw) || !keys(raw, nodeKeys)) {
|
|
81
87
|
throw invalid();
|
|
82
88
|
}
|
|
83
89
|
const identity = raw["identity"];
|
|
@@ -94,6 +100,9 @@ export function decodeNode(value) {
|
|
|
94
100
|
!Array.isArray(messages) || messages.length > SESSION_FILE_LIMITS.blocks ||
|
|
95
101
|
!Array.isArray(blocks) || blocks.length > SESSION_FILE_LIMITS.blocks)
|
|
96
102
|
throw invalid();
|
|
103
|
+
const context = version === 1
|
|
104
|
+
? undefined
|
|
105
|
+
: contextFromRecord(raw["context"], raw["id"], messages.length);
|
|
97
106
|
const node = Object.freeze({
|
|
98
107
|
id: raw["id"],
|
|
99
108
|
parentId: raw["parentId"],
|
|
@@ -107,9 +116,29 @@ export function decodeNode(value) {
|
|
|
107
116
|
}),
|
|
108
117
|
messages: Object.freeze(messages.map(messageFromRecord)),
|
|
109
118
|
blocks: Object.freeze(blocks.map(blockFromRecord)),
|
|
119
|
+
...(context === undefined ? {} : { context: Object.freeze(context) }),
|
|
110
120
|
});
|
|
111
121
|
return Object.freeze({ sequence: value["sequence"], updatedAt: value["updatedAt"], node });
|
|
112
122
|
}
|
|
123
|
+
function contextFromRecord(value, ownerId, ownerMessages) {
|
|
124
|
+
if (value === null)
|
|
125
|
+
return undefined;
|
|
126
|
+
if (!record(value) || !keys(value, "createdAt,messageCount,summary,throughNodeId")) {
|
|
127
|
+
throw invalid();
|
|
128
|
+
}
|
|
129
|
+
if (!integer(value["throughNodeId"], 1) || value["throughNodeId"] > ownerId ||
|
|
130
|
+
!integer(value["messageCount"], 0) ||
|
|
131
|
+
(value["throughNodeId"] === ownerId && value["messageCount"] > ownerMessages) ||
|
|
132
|
+
!timestamp(value["createdAt"]) ||
|
|
133
|
+
!bounded(value["summary"], CONTEXT_LIMITS.summaryCodeUnits))
|
|
134
|
+
throw invalid();
|
|
135
|
+
return {
|
|
136
|
+
throughNodeId: value["throughNodeId"],
|
|
137
|
+
messageCount: value["messageCount"],
|
|
138
|
+
createdAt: value["createdAt"],
|
|
139
|
+
summary: value["summary"],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
113
142
|
function messageRecord(message) {
|
|
114
143
|
return {
|
|
115
144
|
role: message.role,
|
|
@@ -326,6 +355,9 @@ function boundedText(value, limit = SESSION_FILE_LIMITS.text) {
|
|
|
326
355
|
function integer(value, minimum) {
|
|
327
356
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
|
|
328
357
|
}
|
|
358
|
+
function schema(value) {
|
|
359
|
+
return value === 1 || value === SESSION_SCHEMA;
|
|
360
|
+
}
|
|
329
361
|
function nullableInteger(value, minimum) {
|
|
330
362
|
return value === null || integer(value, minimum);
|
|
331
363
|
}
|
package/dist/sessions/store.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as path from "node:path";
|
|
|
9
9
|
import { atomicWrite } from "../atomic.js";
|
|
10
10
|
import { CONVERSATION_LIMITS, ConversationTree } from "../conversation.js";
|
|
11
11
|
import { userDataPath } from "../user-data.js";
|
|
12
|
-
import { decodeHead, decodeMeta, decodeNode, encodeHead, encodeMeta, encodeNode, } from "./codec.js";
|
|
12
|
+
import { decodeHead, decodeMeta, decodeNode, encodeHead, encodeMeta, encodeNode, SESSION_SCHEMA, } from "./codec.js";
|
|
13
13
|
import { leaseOwner, leaseToken, pidIsAlive, removeLease, sessionLease, } from "./lease.js";
|
|
14
14
|
const DIRECTORY_MODE = 0o700;
|
|
15
15
|
const FILE_MODE = 0o600;
|
|
@@ -104,7 +104,7 @@ export class DurableSessionStore {
|
|
|
104
104
|
throw new Error("session checkpoint cannot be recovered safely");
|
|
105
105
|
}
|
|
106
106
|
head = {
|
|
107
|
-
version:
|
|
107
|
+
version: SESSION_SCHEMA,
|
|
108
108
|
sequence: candidate.sequence,
|
|
109
109
|
nodeId: candidate.node.id,
|
|
110
110
|
parentId: candidate.node.parentId,
|
|
@@ -128,14 +128,14 @@ export class DurableSessionStore {
|
|
|
128
128
|
const temporary = path.join(this.#bucket, `.${id}.${randomUUID()}.tmp`);
|
|
129
129
|
const target = this.#sessionDirectory(id);
|
|
130
130
|
const meta = {
|
|
131
|
-
version:
|
|
131
|
+
version: SESSION_SCHEMA,
|
|
132
132
|
id,
|
|
133
133
|
workspaceRoot: this.workspaceRoot,
|
|
134
134
|
workspaceDigest: this.workspaceDigest,
|
|
135
135
|
createdAt: now,
|
|
136
136
|
};
|
|
137
137
|
const head = {
|
|
138
|
-
version:
|
|
138
|
+
version: SESSION_SCHEMA,
|
|
139
139
|
sequence: conversation.nodes.length,
|
|
140
140
|
nodeId: active.id,
|
|
141
141
|
parentId: active.parentId,
|
|
@@ -187,7 +187,7 @@ export class DurableSessionStore {
|
|
|
187
187
|
assertSharedNodes(previous.conversation, conversation, replacesHead ? active.id : undefined);
|
|
188
188
|
const now = new Date().toISOString();
|
|
189
189
|
const head = {
|
|
190
|
-
version:
|
|
190
|
+
version: SESSION_SCHEMA,
|
|
191
191
|
sequence: previous.head.sequence + 1,
|
|
192
192
|
nodeId: active.id,
|
|
193
193
|
parentId: active.parentId,
|
package/dist/settings-command.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The persistent settings hub. Provider access and model selection reuse the
|
|
2
2
|
// same command flows exposed directly through /providers and /models.
|
|
3
3
|
import { saveCommandSettings } from "./command-settings.js";
|
|
4
|
+
import { MAX_COMPACTION_PERCENT, MIN_COMPACTION_PERCENT } from "./context/policy.js";
|
|
4
5
|
import { modelsCommand } from "./model-command.js";
|
|
5
6
|
import { providerFailure } from "./provider-errors.js";
|
|
6
7
|
import { providerLabel } from "./provider-label.js";
|
|
@@ -43,6 +44,9 @@ export async function settingsCommand(session, host) {
|
|
|
43
44
|
case "maxSteps":
|
|
44
45
|
await numberSetting(session, host, "maxSteps", "max tool steps");
|
|
45
46
|
break;
|
|
47
|
+
case "compactionPercent":
|
|
48
|
+
await compactionSetting(session, host);
|
|
49
|
+
break;
|
|
46
50
|
case "reducedMotion":
|
|
47
51
|
await motionSetting(session, host);
|
|
48
52
|
break;
|
|
@@ -53,9 +57,11 @@ export async function settingsCommand(session, host) {
|
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
export function settingsPicker(values, index = 0) {
|
|
60
|
+
const items = settingsItems(values);
|
|
56
61
|
return {
|
|
57
62
|
title: [],
|
|
58
|
-
options:
|
|
63
|
+
options: items.map((item) => item.option),
|
|
64
|
+
visible: items.length,
|
|
59
65
|
index,
|
|
60
66
|
};
|
|
61
67
|
}
|
|
@@ -76,13 +82,17 @@ function settingsItems(values) {
|
|
|
76
82
|
option: { label: "max output tokens", value: String(values.maxTokens) },
|
|
77
83
|
}]),
|
|
78
84
|
{ action: "maxSteps", option: { label: "max tool steps", value: String(values.maxSteps) } },
|
|
85
|
+
{
|
|
86
|
+
action: "compactionPercent",
|
|
87
|
+
option: { label: "context compaction", value: `${values.compactionPercent}%` },
|
|
88
|
+
},
|
|
79
89
|
{
|
|
80
90
|
action: "reducedMotion",
|
|
81
91
|
option: { label: "reduced motion", value: values.reducedMotion ? "on" : "off" },
|
|
82
92
|
},
|
|
83
93
|
{
|
|
84
94
|
action: "providers",
|
|
85
|
-
option: { label: "providers", hint: "manage
|
|
95
|
+
option: { label: "providers", hint: "manage connections" },
|
|
86
96
|
},
|
|
87
97
|
];
|
|
88
98
|
}
|
|
@@ -93,6 +103,7 @@ function settingsValues(session) {
|
|
|
93
103
|
effort: session.config.effort,
|
|
94
104
|
...(session.provider.id === "openai-codex" ? {} : { maxTokens: session.config.maxTokens }),
|
|
95
105
|
maxSteps: session.config.maxSteps,
|
|
106
|
+
compactionPercent: session.config.compactionPercent,
|
|
96
107
|
reducedMotion: session.config.reducedMotion,
|
|
97
108
|
};
|
|
98
109
|
}
|
|
@@ -185,6 +196,35 @@ async function numberSetting(session, host, name, label) {
|
|
|
185
196
|
return;
|
|
186
197
|
session.config[name] = value;
|
|
187
198
|
}
|
|
199
|
+
async function compactionSetting(session, host) {
|
|
200
|
+
if (host.type === undefined)
|
|
201
|
+
return;
|
|
202
|
+
const label = "context compaction";
|
|
203
|
+
const field = {
|
|
204
|
+
title: heading(label, `${MIN_COMPACTION_PERCENT}-${MAX_COMPACTION_PERCENT} percent`, session.palette),
|
|
205
|
+
right: "enter save · esc back",
|
|
206
|
+
editor: of(String(session.config.compactionPercent)),
|
|
207
|
+
secret: false,
|
|
208
|
+
note: "Compacts when model context reaches this percentage.",
|
|
209
|
+
};
|
|
210
|
+
const text = await host.type(field);
|
|
211
|
+
if (text === undefined)
|
|
212
|
+
return;
|
|
213
|
+
const value = Number(text);
|
|
214
|
+
if (!Number.isSafeInteger(value) ||
|
|
215
|
+
value < MIN_COMPACTION_PERCENT ||
|
|
216
|
+
value > MAX_COMPACTION_PERCENT) {
|
|
217
|
+
host.emit({
|
|
218
|
+
kind: "notice",
|
|
219
|
+
text: `${label} must be from ${MIN_COMPACTION_PERCENT} to ${MAX_COMPACTION_PERCENT}`,
|
|
220
|
+
tone: "error",
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!(await saveCommandSettings(host, { compactionPercent: value })))
|
|
225
|
+
return;
|
|
226
|
+
session.config.compactionPercent = value;
|
|
227
|
+
}
|
|
188
228
|
function chooser(host) {
|
|
189
229
|
if (host.choose === undefined) {
|
|
190
230
|
host.emit({ kind: "notice", text: "that command needs the screen", tone: "warn" });
|