@giovannijecha/jecode 0.8.1 → 0.8.2

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.
@@ -18,10 +18,17 @@ export function estimateRequestInputTokens(envelope) {
18
18
  }
19
19
  /** Clamp the configured output ceiling so the complete request remains usable. */
20
20
  export function budgetRequest(envelope, configuredMaxOutputTokens, policy) {
21
- if (!Number.isSafeInteger(configuredMaxOutputTokens) || configuredMaxOutputTokens <= 0) {
22
- throw new Error("max output tokens must be a positive safe integer");
23
- }
21
+ requirePositiveInteger(configuredMaxOutputTokens, "max output tokens");
24
22
  const inputTokens = estimateRequestInputTokens(envelope);
23
+ return finishBudget(inputTokens, configuredMaxOutputTokens, policy);
24
+ }
25
+ /** Reuse an exact estimate already computed for the same request envelope. */
26
+ export function budgetRequestFromInputTokens(inputTokens, configuredMaxOutputTokens, policy) {
27
+ requirePositiveInteger(inputTokens, "request input tokens");
28
+ requirePositiveInteger(configuredMaxOutputTokens, "max output tokens");
29
+ return finishBudget(inputTokens, configuredMaxOutputTokens, policy);
30
+ }
31
+ function finishBudget(inputTokens, configuredMaxOutputTokens, policy) {
25
32
  const available = policy.requestLimitTokens - inputTokens;
26
33
  const minimum = Math.min(configuredMaxOutputTokens, MIN_REQUEST_OUTPUT_TOKENS);
27
34
  if (available < minimum) {
@@ -35,3 +42,8 @@ export function budgetRequest(envelope, configuredMaxOutputTokens, policy) {
35
42
  limitTokens: policy.requestLimitTokens,
36
43
  });
37
44
  }
45
+ function requirePositiveInteger(value, label) {
46
+ if (!Number.isSafeInteger(value) || value <= 0) {
47
+ throw new Error(`${label} must be a positive safe integer`);
48
+ }
49
+ }
@@ -1,17 +1,14 @@
1
1
  // One streamed provider request with a single safe context-overflow recovery.
2
- import { budgetRequest, estimateRequestInputTokens } from "./context/budget.js";
2
+ import { budgetRequestFromInputTokens, estimateRequestInputTokens, } from "./context/budget.js";
3
3
  import { isContextOverflow } from "./context/policy.js";
4
4
  export async function requestAssistant(history, current, specs, options, events, signal) {
5
5
  let policy = await options.contextPolicy();
6
6
  const prepared = await prepareContext(history, current, specs, options, events, policy, "budget");
7
- let context = prepared === undefined ? [...current] : clone(prepared);
7
+ let context = prepared.projected === undefined ? [...current] : clone(prepared.projected);
8
+ let inputTokens = prepared.inputTokens;
8
9
  let recovered = false;
9
10
  for (;;) {
10
- const budget = budgetRequest({
11
- system: options.system,
12
- messages: context,
13
- tools: specs,
14
- }, options.maxTokens, policy);
11
+ const budget = budgetRequestFromInputTokens(inputTokens, options.maxTokens, policy);
15
12
  try {
16
13
  const message = await options.provider.send({
17
14
  model: options.model,
@@ -31,10 +28,11 @@ export async function requestAssistant(history, current, specs, options, events,
31
28
  throw error;
32
29
  if (isContextOverflow(error))
33
30
  policy = await options.contextPolicy();
34
- const projected = await prepareContext(history, context, specs, options, events, policy, "overflow", error);
35
- if (projected === undefined)
31
+ const next = await prepareContext(history, context, specs, options, events, policy, "overflow", error);
32
+ if (next.projected === undefined)
36
33
  throw error;
37
- context = clone(projected);
34
+ context = clone(next.projected);
35
+ inputTokens = next.inputTokens;
38
36
  recovered = true;
39
37
  }
40
38
  }
@@ -45,12 +43,22 @@ async function prepareContext(history, context, specs, options, events, policy,
45
43
  messages: context,
46
44
  tools: specs,
47
45
  });
48
- return events.onContext?.(history, context, {
46
+ const projected = await events.onContext?.(history, context, {
49
47
  reason,
50
48
  policy,
51
49
  inputTokens,
52
50
  ...(error === undefined ? {} : { error }),
53
51
  });
52
+ return {
53
+ projected,
54
+ inputTokens: projected === undefined
55
+ ? inputTokens
56
+ : estimateRequestInputTokens({
57
+ system: options.system,
58
+ messages: projected,
59
+ tools: specs,
60
+ }),
61
+ };
54
62
  }
55
63
  function clone(messages) {
56
64
  return structuredClone([...messages]);
@@ -97,13 +97,29 @@ export async function runTurn(history, options, events, signal, modelHistory = h
97
97
  events.onToolCall(call, preview);
98
98
  prepared.push({ call, current, preview });
99
99
  }
100
- const runs = await Promise.all(prepared.map(({ call, current, preview }) => settle(call, current, calls.length, options, events, signal, preview)));
101
- for (let offset = 0; offset < runs.length; offset++) {
100
+ const settlements = await Promise.allSettled(prepared.map(({ call, current, preview }) => settle(call, current, calls.length, options, events, signal, preview)));
101
+ let batchFailure;
102
+ for (let offset = 0; offset < settlements.length; offset++) {
102
103
  const call = prepared[offset]?.call;
103
- const run = runs[offset];
104
+ const settlement = settlements[offset];
105
+ const run = settlement.status === "fulfilled"
106
+ ? settlement.value
107
+ : refuse(call, signal?.aborted === true
108
+ ? "interrupted before completion"
109
+ : "tool processing stopped before completion", signal?.aborted === true ? "interrupted" : "failed");
110
+ if (settlement.status === "rejected" && batchFailure === undefined) {
111
+ batchFailure = { error: settlement.reason };
112
+ }
104
113
  results.push(run.result);
105
- events.onToolResult(call, run.result, run.summary);
114
+ try {
115
+ events.onToolResult(call, run.result, run.summary);
116
+ }
117
+ catch (error) {
118
+ batchFailure ??= { error };
119
+ }
106
120
  }
121
+ if (batchFailure !== undefined)
122
+ throw batchFailure.error;
107
123
  }
108
124
  }
109
125
  catch (error) {
@@ -17,6 +17,8 @@ const DEVICE_POLL = `${AUTHORITY}/api/accounts/deviceauth/token`;
17
17
  const DEVICE_VERIFY = `${AUTHORITY}/codex/device`;
18
18
  const DEVICE_REDIRECT = `${AUTHORITY}/deviceauth/callback`;
19
19
  const LOGIN_LIMIT_MS = 15 * 60_000;
20
+ /** RFC 8628 increases the polling interval by five seconds after `slow_down`. */
21
+ const SLOW_DOWN_INCREMENT_MS = 5_000;
20
22
  export async function beginBrowserLogin() {
21
23
  const verifier = randomBytes(64).toString("base64url");
22
24
  const challenge = createHash("sha256").update(verifier).digest("base64url");
@@ -121,23 +123,65 @@ async function exchange(code, signal) {
121
123
  return openAITokenReply(response.value);
122
124
  }
123
125
  async function pollDevice(deviceAuthId, userCode, interval, signal) {
124
- const started = Date.now();
125
- while (Date.now() - started < LOGIN_LIMIT_MS) {
126
- const response = await oauthRequest(DEVICE_POLL, {
127
- contentType: "application/json",
128
- value: { device_auth_id: deviceAuthId, user_code: userCode },
129
- }, signal, [200, 403, 404]);
130
- if (response.status === 200) {
131
- const value = record(response.value) ? response.value : {};
132
- return {
133
- authorizationCode: required(value["authorization_code"], "authorization code"),
134
- verifier: required(value["code_verifier"], "code verifier"),
135
- redirectUri: DEVICE_REDIRECT,
136
- };
126
+ const deadline = new AbortController();
127
+ const timer = setTimeout(() => {
128
+ deadline.abort(new Error("ChatGPT device sign-in timed out after 15 minutes"));
129
+ }, LOGIN_LIMIT_MS);
130
+ const combined = signal === undefined
131
+ ? deadline.signal
132
+ : AbortSignal.any([signal, deadline.signal]);
133
+ let intervalMs = interval * 1_000;
134
+ try {
135
+ while (true) {
136
+ const response = await oauthRequest(DEVICE_POLL, {
137
+ contentType: "application/json",
138
+ value: { device_auth_id: deviceAuthId, user_code: userCode },
139
+ }, combined, [200, 400, 403, 404, 429]);
140
+ if (combined.aborted)
141
+ throw abortReason(combined);
142
+ if (response.status === 200) {
143
+ const value = record(response.value) ? response.value : {};
144
+ return {
145
+ authorizationCode: required(value["authorization_code"], "authorization code"),
146
+ verifier: required(value["code_verifier"], "code verifier"),
147
+ redirectUri: DEVICE_REDIRECT,
148
+ };
149
+ }
150
+ const errorCode = deviceErrorCode(response.value);
151
+ if (errorCode === "access_denied") {
152
+ throw new Error("ChatGPT device sign-in was denied");
153
+ }
154
+ if (errorCode === "expired_token") {
155
+ throw new Error("ChatGPT device sign-in code expired");
156
+ }
157
+ const pending = errorCode === "authorization_pending" ||
158
+ errorCode === "deviceauth_authorization_pending" ||
159
+ ((response.status === 403 || response.status === 404) && errorCode === undefined);
160
+ const slowDown = errorCode === "slow_down" ||
161
+ (response.status === 429 && errorCode === undefined);
162
+ if (!pending && !slowDown) {
163
+ throw new Error(`ChatGPT device sign-in failed (${response.status})`);
164
+ }
165
+ if (slowDown)
166
+ intervalMs += SLOW_DOWN_INCREMENT_MS;
167
+ await sleep(intervalMs, combined);
137
168
  }
138
- await sleep(interval * 1_000, signal);
139
169
  }
140
- throw new Error("ChatGPT device sign-in timed out after 15 minutes");
170
+ finally {
171
+ clearTimeout(timer);
172
+ }
173
+ }
174
+ function deviceErrorCode(value) {
175
+ if (!record(value))
176
+ return undefined;
177
+ const error = value["error"];
178
+ const nested = typeof error === "string"
179
+ ? error
180
+ : record(error)
181
+ ? optional(error["code"])
182
+ : undefined;
183
+ const code = nested ?? optional(value["code"]);
184
+ return code?.trim().toLowerCase();
141
185
  }
142
186
  function intervalSeconds(value) {
143
187
  const parsed = typeof value === "string" ? Number(value.trim()) : value;
@@ -17,7 +17,8 @@ const KEY = "OLLAMA_API_KEY";
17
17
  const OLLAMA_EFFORTS = ["low", "medium", "high"];
18
18
  let configuredHost;
19
19
  const CONTEXT_CACHE_MS = 30_000;
20
- const contextByEndpoint = new Map();
20
+ const runtimeContextByEndpoint = new Map();
21
+ const modelContextByEndpoint = new Map();
21
22
  /** Set the endpoint selected for this process. Undefined restores key-aware inference. */
22
23
  export function configureOllama(host) {
23
24
  configuredHost = host === undefined ? undefined : parseOllamaEndpoint(host).baseUrl;
@@ -56,15 +57,16 @@ export const ollama = {
56
57
  async contextWindow(model, signal, onStatus) {
57
58
  const at = endpoint();
58
59
  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);
60
+ const runtime = cachedContext(runtimeContextByEndpoint, cacheKey);
61
+ if (runtime !== undefined)
62
+ return runtime;
63
+ const modelCapacity = cachedContext(modelContextByEndpoint, cacheKey);
64
+ const observed = await nativeContextWindow(at, model, modelCapacity, signal, onStatus);
63
65
  if (observed?.runtime === true) {
64
- contextByEndpoint.set(cacheKey, {
65
- value: observed.value,
66
- expiresAt: Date.now() + CONTEXT_CACHE_MS,
67
- });
66
+ rememberContext(runtimeContextByEndpoint, cacheKey, observed.value);
67
+ }
68
+ else if (observed !== undefined && modelCapacity === undefined) {
69
+ rememberContext(modelContextByEndpoint, cacheKey, observed.value);
68
70
  }
69
71
  return observed?.value;
70
72
  },
@@ -96,7 +98,7 @@ export const ollama = {
96
98
  return fromWireReply(reply);
97
99
  },
98
100
  };
99
- async function nativeContextWindow(at, model, signal, onStatus) {
101
+ async function nativeContextWindow(at, model, fallback, signal, onStatus) {
100
102
  try {
101
103
  const running = await getJson(`${at.baseUrl}/api/ps`, headers(at), signal, onStatus);
102
104
  const allocated = runningContext(running, model);
@@ -106,6 +108,8 @@ async function nativeContextWindow(at, model, signal, onStatus) {
106
108
  catch (error) {
107
109
  throwIfAborted(signal, error);
108
110
  }
111
+ if (fallback !== undefined)
112
+ return { value: fallback, runtime: false };
109
113
  try {
110
114
  const details = await postJson(`${at.baseUrl}/api/show`, headers(at), { model }, signal, onStatus);
111
115
  const capacity = modelCapacity(details);
@@ -118,6 +122,18 @@ async function nativeContextWindow(at, model, signal, onStatus) {
118
122
  return undefined;
119
123
  }
120
124
  }
125
+ function cachedContext(cache, key) {
126
+ const cached = cache.get(key);
127
+ if (cached === undefined)
128
+ return undefined;
129
+ if (cached.expiresAt > Date.now())
130
+ return cached.value;
131
+ cache.delete(key);
132
+ return undefined;
133
+ }
134
+ function rememberContext(cache, key, value) {
135
+ cache.set(key, { value, expiresAt: Date.now() + CONTEXT_CACHE_MS });
136
+ }
121
137
  function runningContext(value, model) {
122
138
  if (!record(value) || !Array.isArray(value["models"]))
123
139
  return undefined;
@@ -16,6 +16,7 @@ const DIRECTORY_MODE = 0o700;
16
16
  const FILE_MODE = 0o600;
17
17
  const MAX_CATALOG_ENTRIES = 4_096;
18
18
  const CATALOG_READ_CONCURRENCY = 8;
19
+ const NODE_READ_CONCURRENCY = 4;
19
20
  const SESSION_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
20
21
  const NODE_NAME = /^(\d{6})\.json$/;
21
22
  const ATOMIC_NODE_TEMP = /^\.\d{6}\.json\.\d+\.[a-f0-9-]+\.tmp$/;
@@ -296,17 +297,26 @@ async function readNodes(directory) {
296
297
  }
297
298
  const stored = [];
298
299
  const sequences = new Set();
299
- for (let index = 0; index < names.length; index++) {
300
- const name = names[index];
301
- const id = Number(NODE_NAME.exec(name)?.[1]);
302
- if (id !== index + 1)
303
- throw new Error("session conversation nodes are not contiguous");
304
- const decoded = decodeNode(await readJson(path.join(directory, name), SESSION_FILE_LIMITS.nodeBytes));
305
- if (decoded.node.id !== id || sequences.has(decoded.sequence)) {
306
- throw new Error("session conversation node identity is invalid");
300
+ for (let start = 0; start < names.length; start += NODE_READ_CONCURRENCY) {
301
+ const decoded = await Promise.all(names.slice(start, start + NODE_READ_CONCURRENCY)
302
+ .map(async (name, offset) => {
303
+ const id = Number(NODE_NAME.exec(name)?.[1]);
304
+ if (id !== start + offset + 1) {
305
+ throw new Error("session conversation nodes are not contiguous");
306
+ }
307
+ const entry = decodeNode(await readJson(path.join(directory, name), SESSION_FILE_LIMITS.nodeBytes));
308
+ if (entry.node.id !== id) {
309
+ throw new Error("session conversation node identity is invalid");
310
+ }
311
+ return entry;
312
+ }));
313
+ for (const entry of decoded) {
314
+ if (sequences.has(entry.sequence)) {
315
+ throw new Error("session conversation node identity is invalid");
316
+ }
317
+ sequences.add(entry.sequence);
318
+ stored.push(entry);
307
319
  }
308
- sequences.add(decoded.sequence);
309
- stored.push(decoded);
310
320
  }
311
321
  return stored;
312
322
  }
@@ -11,12 +11,10 @@ export function render(block, width, pal, context = {}) {
11
11
  case "reasoning":
12
12
  return renderReasoning(block, width, pal, {
13
13
  continues: context.previous?.kind === "reasoning",
14
- followsTool: context.previous?.kind === "tool",
15
14
  });
16
15
  case "tool":
17
16
  return renderTool(block, width, pal, {
18
- continues: context.previous?.kind === "tool" || context.previous?.kind === "reasoning",
19
- followsReasoning: context.previous?.kind === "reasoning",
17
+ continues: context.previous?.kind === "tool",
20
18
  now: context.now,
21
19
  motion: context.motion,
22
20
  reducedMotion: context.reducedMotion,
@@ -1,7 +1,7 @@
1
1
  import { trailingText } from "../../text-boundary.js";
2
- import { row } from "../../ui/render.js";
2
+ import { blank, row } from "../../ui/render.js";
3
3
  import { markdown } from "../../ui/markdown.js";
4
- import { transcriptLead, transcriptMark, transcriptWidth } from "../transcript-grammar.js";
4
+ import { transcriptLead, transcriptWidth } from "../transcript-grammar.js";
5
5
  export const REASONING_PREVIEW_ROWS = 3;
6
6
  const MIN_REASONING_PREVIEW_CHARS = 4_096;
7
7
  const REASONING_PREVIEW_OVERSCAN = 12;
@@ -10,12 +10,14 @@ export function renderUser(block, width, pal) {
10
10
  const content = markdown(block.text, inner, pal, inner);
11
11
  return [
12
12
  "",
13
+ blank(width, pal.surface.subtle),
13
14
  ...content.map((line, index) => row(width, [
14
15
  ...transcriptLead(width, index === 0
15
16
  ? { text: "❯", fg: pal.accent, bold: true }
16
17
  : undefined),
17
18
  ...line.segs,
18
- ])),
19
+ ], [], pal.surface.subtle)),
20
+ blank(width, pal.surface.subtle),
19
21
  ];
20
22
  }
21
23
  export function renderAnswer(block, width, pal) {
@@ -25,23 +27,17 @@ export function renderAnswer(block, width, pal) {
25
27
  ];
26
28
  }
27
29
  export function renderReasoning(block, width, pal, context = {}) {
28
- const inner = transcriptWidth(width);
29
30
  // Expanding a live stream is deferred until it is sealed. Re-parsing an
30
31
  // ever-growing full thought on every token makes the whole TUI stall.
31
32
  const expanded = block.expanded === true && block.live !== true;
32
33
  const source = !expanded
33
- ? reasoningPreviewSource(block.text, inner)
34
+ ? reasoningPreviewSource(block.text, width)
34
35
  : { text: block.text, truncated: false };
35
- const content = markdown(source.text, inner, pal, inner);
36
+ const content = markdown(source.text, width, pal, width);
36
37
  const visible = expanded ? content : content.slice(-REASONING_PREVIEW_ROWS);
37
38
  return [
38
- ...(context.followsTool === true
39
- ? [row(width, transcriptMark(width, { text: "│", fg: pal.rule }))]
40
- : context.continues === true ? [] : [""]),
41
- ...visible.map((line) => row(width, [
42
- ...transcriptLead(width, { text: "│", fg: pal.rule }),
43
- ...line.segs.map((seg) => ({ ...seg, fg: pal.ink.dim, italic: true })),
44
- ])),
39
+ ...(context.continues === true ? [] : [""]),
40
+ ...visible.map((line) => row(width, line.segs.map((seg) => ({ ...seg, fg: pal.ink.dim, italic: true })))),
45
41
  ];
46
42
  }
47
43
  export function reasoningPreviewSource(text, width) {
@@ -4,7 +4,7 @@ import { fitSegs, hasColor, plainLen, row } from "../../ui/render.js";
4
4
  import { graphemeCeiling, graphemeFloor } from "../../text-boundary.js";
5
5
  import { toolDuration } from "../../duration.js";
6
6
  import { breathe, easeInOut, easeOut, interval, mix, TOOL_BIRTH_MS, TOOL_LEADER_MAX_MS, TOOL_ROW_ARRIVAL_MS, } from "../motion.js";
7
- import { transcriptLead, transcriptMark } from "../transcript-grammar.js";
7
+ import { transcriptLead } from "../transcript-grammar.js";
8
8
  const OUTPUT_ROWS = 8;
9
9
  const LIVE_OUTPUT_ROWS = 6;
10
10
  const DIFF_ROWS = 15;
@@ -25,9 +25,7 @@ export function renderTool(block, width, pal, context = {}) {
25
25
  const right = resultSegments(block, pal, context, now);
26
26
  const leader = movingLeader(width, left, right, pal, context, now);
27
27
  return [
28
- ...(context.followsReasoning === true
29
- ? [row(width, transcriptMark(width, { text: "│", fg: pal.rule }))]
30
- : context.continues === true ? [] : [""]),
28
+ ...(context.continues === true ? [] : [""]),
31
29
  row(width, leader === undefined ? left : [...left, leader], right),
32
30
  ...shown.map(({ detail, sourceIndex }) => renderDetail(detail, block.tone, width, pal, context.reducedMotion === true ? undefined : context.motion?.rowsAt[sourceIndex ?? -1], now)),
33
31
  ];
@@ -1,4 +1,4 @@
1
- // One semantic margin shared by every transcript block.
1
+ // One semantic margin shared by the user cue and executable tool evidence.
2
2
  const GUTTER = 2;
3
3
  export function transcriptGutter(_width) {
4
4
  return GUTTER;
package/dist/ui/theme.js CHANGED
@@ -1,27 +1,27 @@
1
1
  // Jecode's fixed terminal identity. Components depend on semantic tokens, not
2
2
  // literal colours, while the product exposes one deliberate dark Slate look.
3
- // Jecode's fixed dark Slate baseline. The exported name stays stable while
4
- // cooler, quieter values leave the transcript bright enough to read and let
5
- // live state carry the colour.
3
+ // Jecode's fixed dark Slate baseline. Clear text contrast and luminous
4
+ // structural colours keep long terminal transcripts readable without making
5
+ // live state decorative.
6
6
  // Components depend on these roles rather than embedding presentation values.
7
7
  export const STEEL = {
8
- accent: [124, 164, 222],
9
- technical: [126, 186, 208],
10
- focus: [124, 164, 222],
11
- rule: [44, 60, 78],
8
+ accent: [102, 155, 210],
9
+ technical: [78, 201, 232],
10
+ focus: [102, 155, 210],
11
+ rule: [53, 80, 110],
12
12
  ink: {
13
- fg: [214, 219, 226],
14
- bright: [241, 244, 248],
15
- muted: [149, 160, 174],
16
- dim: [99, 111, 125],
17
- attention: [226, 188, 112],
18
- added: [138, 190, 150],
19
- removed: [223, 120, 120],
13
+ fg: [220, 224, 229],
14
+ bright: [235, 239, 244],
15
+ muted: [156, 169, 183],
16
+ dim: [112, 124, 137],
17
+ attention: [230, 191, 95],
18
+ added: [134, 203, 146],
19
+ removed: [232, 112, 112],
20
20
  },
21
21
  surface: {
22
- subtle: [23, 29, 37],
23
- added: [21, 52, 33],
24
- removed: [58, 24, 27],
25
- attention: [58, 47, 20],
22
+ subtle: [31, 38, 47],
23
+ added: [22, 55, 34],
24
+ removed: [62, 24, 27],
25
+ attention: [62, 50, 19],
26
26
  },
27
27
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {