@nexrall/code-core 1.4.22 → 1.4.23

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.
@@ -1,5 +1,41 @@
1
1
  import type { Message, AgentLoopOptions, EnvContext } from '../types';
2
2
  export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
3
+ /**
4
+ * Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
5
+ * the CLI ships as a single esbuild bundle with no node_modules, and this is a
6
+ * dozen lines.
7
+ */
8
+ export declare function createLimiter(max: number): <T>(fn: () => Promise<T>) => Promise<T>;
9
+ /**
10
+ * Reduce a sub-agent's message history to the text its parent should receive.
11
+ *
12
+ * Pure + exported so the salvage rules can be tested without running a real
13
+ * sub-agent (which needs a live model stream and, for the timeout path, ten
14
+ * minutes of wall clock).
15
+ *
16
+ * `preferLast` — the normal completion path — returns the final assistant
17
+ * message, which is the sub-agent's actual answer.
18
+ *
19
+ * `preferLast: false` is the SALVAGE path, used when the sub-agent was cut off.
20
+ * A stopped sub-agent usually has no closing summary at all (it was killed
21
+ * mid-tool-round), so the last assistant message is frequently empty or a
22
+ * fragment. Concatenating what it did produce is far more useful to the parent
23
+ * model than nothing: it can build on the work instead of redoing it.
24
+ */
25
+ export declare function extractSubTaskText(messages: Message[], preferLast?: boolean): string;
26
+ /** Apply the parent-context cap to a sub-task's text, keeping head + tail. */
27
+ export declare function capSubTaskText(text: string, max?: number): string;
28
+ /**
29
+ * Summarise what a cut-short sub-agent actually accomplished, so the parent model
30
+ * can continue from it rather than starting over.
31
+ *
32
+ * This is the whole point of the salvage path. Previously a timed-out sub-agent
33
+ * returned ONLY an error string: ten minutes of work, dozens of tool calls and
34
+ * any files it wrote were invisible to the parent, which typically responded by
35
+ * re-running the same work from scratch — while the tokens for the discarded run
36
+ * had already been billed in full.
37
+ */
38
+ export declare function summariseSubTaskProgress(messages: Message[]): string;
3
39
  /** The context window (tokens) for a model alias — the same lookup runAgentLoop uses internally. */
4
40
  export declare function contextWindowFor(model?: 'turbo' | 'pro' | 'ultra'): number;
5
41
  /** Auto-prune / auto-compact thresholds as fractions of the context window — for UI display (e.g. `/context`). */
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
@@ -35,6 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = void 0;
37
37
  exports.resolveMaxIterations = resolveMaxIterations;
38
+ exports.createLimiter = createLimiter;
39
+ exports.extractSubTaskText = extractSubTaskText;
40
+ exports.capSubTaskText = capSubTaskText;
41
+ exports.summariseSubTaskProgress = summariseSubTaskProgress;
38
42
  exports.contextWindowFor = contextWindowFor;
39
43
  exports.compactionThresholds = compactionThresholds;
40
44
  exports.estimateBodyBytes = estimateBodyBytes;
@@ -241,6 +245,58 @@ async function withFileLock(absPath, fn) {
241
245
  _fileLocks.delete(absPath);
242
246
  }
243
247
  }
248
+ // ─── Sub-agent fan-out limiter ────────────────────────────────────────────────
249
+ //
250
+ // Tool calls in one turn run via Promise.all with no ceiling. For ordinary tools
251
+ // that is right — they're cheap and mostly I/O — but a `task` call spawns a WHOLE
252
+ // nested agent loop: its own model stream, its own tool executions, its own
253
+ // sub-process spawns. A model that emits ten `task` blocks in one turn therefore
254
+ // starts ten concurrent agents, each billing tokens and competing for the same
255
+ // CPU, file handles and API rate limit. The practical symptoms are the ones users
256
+ // report as "it got slow and then stalled": every sub-agent's stream slows, some
257
+ // trip their own stall watchdog, and one shared rate limit is spread across ten
258
+ // callers.
259
+ //
260
+ // Anthropic hit the same wall and capped Claude Code's concurrent subagents at 20
261
+ // (v2.1.217, July 2026); community guidance settles far lower, around 3-5, because
262
+ // past that the synthesis overhead cancels the parallelism. We default to 4:
263
+ // enough for genuine fan-out (the case sub-agents exist for), low enough that a
264
+ // runaway `task` burst degrades into a queue instead of a thundering herd.
265
+ //
266
+ // This is a QUEUE, not a rejection: every sub-task still runs, just at most N at a
267
+ // time. Failing the excess would be worse than serialising it.
268
+ const MAX_CONCURRENT_SUBTASKS = (() => {
269
+ const raw = Number(process.env.NEXRALL_MAX_CONCURRENT_SUBTASKS);
270
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 4;
271
+ })();
272
+ /**
273
+ * Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
274
+ * the CLI ships as a single esbuild bundle with no node_modules, and this is a
275
+ * dozen lines.
276
+ */
277
+ function createLimiter(max) {
278
+ let active = 0;
279
+ const queue = [];
280
+ const release = () => {
281
+ active--;
282
+ queue.shift()?.();
283
+ };
284
+ return async (fn) => {
285
+ if (active >= max)
286
+ await new Promise((resolve) => queue.push(resolve));
287
+ active++;
288
+ try {
289
+ return await fn();
290
+ }
291
+ finally {
292
+ release();
293
+ }
294
+ };
295
+ }
296
+ // Process-wide, deliberately: the limit exists to protect shared resources (CPU,
297
+ // the API rate limit, file handles), and those are shared across every concurrent
298
+ // turn in this process, not just the tool calls of one message.
299
+ const _subTaskLimit = createLimiter(MAX_CONCURRENT_SUBTASKS);
244
300
  // ─── Human-readable tool descriptions ────────────────────────────────────────
245
301
  function humanDescription(name, input) {
246
302
  switch (name) {
@@ -326,7 +382,15 @@ function humanDescription(name, input) {
326
382
  }
327
383
  }
328
384
  // ─── Sub-task runner ──────────────────────────────────────────────────────────
329
- const MAX_TASK_DEPTH = 2; // main agent (0) can spawn sub-agents (1) sub-agents cannot spawn further
385
+ // Deepest nesting level allowed to spawn a sub-agent. The main agent runs at
386
+ // depth 0 and the sub-agents it spawns at depth 1.
387
+ //
388
+ // This is 1, not 2. With 2 the guard below (`depth >= MAX_TASK_DEPTH`) let a
389
+ // depth-1 sub-agent spawn depth-2 grandchildren — contradicting both the constant's
390
+ // own comment and the system prompt's promise that "sub-agents cannot spawn further
391
+ // sub-agents", and quietly making the worst-case fan-out quadratic. The value and
392
+ // the documented behaviour now agree.
393
+ const MAX_TASK_DEPTH = 1;
330
394
  let _subTaskCounter = 0; // unique per-process id → per-sub-agent todo scope
331
395
  // A sub-agent that stalls (hung tool, model provider stuck, infinite tool-call
332
396
  // loop bypassing the iteration budget somehow) used to have NO ceiling of its
@@ -338,13 +402,112 @@ let _subTaskCounter = 0; // unique per-process id → per-sub-agent todo scope
338
402
  const SUBTASK_TIMEOUT_MS = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) > 0
339
403
  ? Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS)
340
404
  : 10 * 60 * 1000; // 10 minutes, matching the general industry norm for a stalled-agent cutoff
405
+ /** Cap on the text a sub-task hands back, so one verbose sub-agent can't blow up
406
+ * the PARENT's context in a single tool_result. */
407
+ const SUBTASK_MAX = 48000; // chars (~12k tokens)
408
+ /**
409
+ * Slice `s` to at most `max` UTF-16 units without splitting a surrogate pair.
410
+ *
411
+ * A bare `slice()` can cut between the high and low half of a non-BMP character
412
+ * (emoji, many CJK extension glyphs), producing a lone surrogate — invalid UTF-16
413
+ * that the Anthropic API rejects outright with "no low surrogate in string". That
414
+ * exact failure has already been shipped and fixed once in this codebase; every
415
+ * new cap on model-facing text has to be surrogate-aware from the start.
416
+ */
417
+ function sliceSafeEnd(s, max) {
418
+ if (s.length <= max)
419
+ return s;
420
+ let end = max;
421
+ const code = s.charCodeAt(end - 1);
422
+ if (code >= 0xd800 && code <= 0xdbff)
423
+ end--; // trailing high surrogate — drop it
424
+ return s.slice(0, end);
425
+ }
426
+ /** Mirror of sliceSafeEnd for a tail slice: never START on a low surrogate. */
427
+ function sliceSafeStart(s, from) {
428
+ if (from <= 0)
429
+ return s;
430
+ let start = from;
431
+ const code = s.charCodeAt(start);
432
+ if (code >= 0xdc00 && code <= 0xdfff)
433
+ start++; // leading low surrogate — drop it
434
+ return s.slice(start);
435
+ }
436
+ /**
437
+ * Reduce a sub-agent's message history to the text its parent should receive.
438
+ *
439
+ * Pure + exported so the salvage rules can be tested without running a real
440
+ * sub-agent (which needs a live model stream and, for the timeout path, ten
441
+ * minutes of wall clock).
442
+ *
443
+ * `preferLast` — the normal completion path — returns the final assistant
444
+ * message, which is the sub-agent's actual answer.
445
+ *
446
+ * `preferLast: false` is the SALVAGE path, used when the sub-agent was cut off.
447
+ * A stopped sub-agent usually has no closing summary at all (it was killed
448
+ * mid-tool-round), so the last assistant message is frequently empty or a
449
+ * fragment. Concatenating what it did produce is far more useful to the parent
450
+ * model than nothing: it can build on the work instead of redoing it.
451
+ */
452
+ function extractSubTaskText(messages, preferLast = true) {
453
+ const assistants = messages.filter((m) => m.role === 'assistant');
454
+ const textOf = (m) => (m?.content ?? [])
455
+ .filter((b) => b.type === 'text' && typeof b.text === 'string')
456
+ .map((b) => b.text)
457
+ .join('')
458
+ .trim();
459
+ if (preferLast)
460
+ return textOf(assistants[assistants.length - 1]);
461
+ return assistants.map(textOf).filter(Boolean).join('\n\n').trim();
462
+ }
463
+ /** Apply the parent-context cap to a sub-task's text, keeping head + tail. */
464
+ function capSubTaskText(text, max = SUBTASK_MAX) {
465
+ if (text.length <= max)
466
+ return text;
467
+ const head = sliceSafeEnd(text, Math.floor(max * 0.6));
468
+ const tail = sliceSafeStart(text, text.length - Math.floor(max * 0.4));
469
+ return `${head}\n\n[… sub-task output truncated (${text.length} chars) — kept the beginning and end …]\n\n${tail}`;
470
+ }
471
+ /**
472
+ * Summarise what a cut-short sub-agent actually accomplished, so the parent model
473
+ * can continue from it rather than starting over.
474
+ *
475
+ * This is the whole point of the salvage path. Previously a timed-out sub-agent
476
+ * returned ONLY an error string: ten minutes of work, dozens of tool calls and
477
+ * any files it wrote were invisible to the parent, which typically responded by
478
+ * re-running the same work from scratch — while the tokens for the discarded run
479
+ * had already been billed in full.
480
+ */
481
+ function summariseSubTaskProgress(messages) {
482
+ const toolNames = [];
483
+ for (const m of messages) {
484
+ if (m.role !== 'assistant' || !Array.isArray(m.content))
485
+ continue;
486
+ for (const b of m.content) {
487
+ if (b?.type === 'tool_use' && typeof b.name === 'string')
488
+ toolNames.push(b.name);
489
+ }
490
+ }
491
+ if (toolNames.length === 0)
492
+ return '';
493
+ // Collapse to "name ×N" so a 40-call run reads as a short inventory rather
494
+ // than forty repeated lines of the same tool name.
495
+ const counts = new Map();
496
+ for (const n of toolNames)
497
+ counts.set(n, (counts.get(n) ?? 0) + 1);
498
+ const inventory = [...counts.entries()]
499
+ .sort((a, b) => b[1] - a[1])
500
+ .map(([name, n]) => (n > 1 ? `${name} ×${n}` : name))
501
+ .join(', ');
502
+ return `Tool calls completed before it was stopped (${toolNames.length} total): ${inventory}.`;
503
+ }
341
504
  async function runSubTask(input, options, agentTypes) {
342
505
  const prompt = typeof input.prompt === 'string' ? input.prompt.trim() : '';
343
506
  if (!prompt)
344
507
  return { error: 'task tool requires a non-empty prompt' };
345
508
  const depth = options._depth ?? 0;
346
509
  if (depth >= MAX_TASK_DEPTH) {
347
- return { error: `Sub-task depth limit (${MAX_TASK_DEPTH}) reached — sub-agents cannot spawn further sub-agents.` };
510
+ return { error: 'Sub-agents cannot spawn further sub-agents. Do this work directly, or report back so the main agent can delegate it.' };
348
511
  }
349
512
  // Resolve an optional custom agent type (subagent_type).
350
513
  const requestedType = typeof input.subagent_type === 'string' ? input.subagent_type : '';
@@ -423,32 +586,57 @@ async function runSubTask(input, options, agentTypes) {
423
586
  onThinkingDelta: (text) => options.onThinkingDelta?.(text),
424
587
  onThinkingProgress: (tok) => options.onThinkingProgress?.(tok),
425
588
  });
426
- // The sub-agent stopped because it hit the stall timeout (not because it
427
- // finished or the parent aborted) — report this distinctly so the model
428
- // (and the user, via the tool_result) understands WHY it got cut short,
429
- // instead of silently receiving whatever partial text happened to exist.
589
+ // ── Stall timeout: SALVAGE, don't discard ────────────────────────────────
590
+ //
591
+ // The sub-agent hit its wall-clock cap (rather than finishing, or the parent
592
+ // aborting). This used to return ONLY an error string — throwing away
593
+ // everything the sub-agent had produced in up to ten minutes of work. The
594
+ // tokens were billed in full either way, and the parent model, told merely
595
+ // that "it stalled", would routinely re-run the identical work from scratch.
596
+ //
597
+ // The timeout still has to be reported unmistakably (the parent must not
598
+ // mistake a truncated run for a complete answer), but it is reported ALONGSIDE
599
+ // whatever was actually accomplished, not instead of it. `preferLast: false`
600
+ // because a killed sub-agent rarely has a closing summary — its useful output
601
+ // is spread across the assistant turns it did manage to produce.
430
602
  if (subAbort.aborted && !options.abortSignal?.aborted) {
431
- return { error: `Sub-task stalled and was stopped after ${Math.round(SUBTASK_TIMEOUT_MS / 60000)} minutes with no completion. Consider breaking it into smaller sub-tasks or investigating what it may have been stuck on (a hung command, an unresponsive MCP/tool call, or a very large scope).` };
432
- }
433
- // Extract final assistant text as the task output
434
- const lastAssistant = [...result].reverse().find((m) => m.role === 'assistant');
435
- let text = (lastAssistant?.content ?? [])
436
- .filter((b) => b.type === 'text')
437
- .map((b) => b.text)
438
- .join('')
439
- .trim();
440
- // Cap the sub-agent's returned text so a verbose sub-task (e.g. "list every
441
- // usage") can't blow up the PARENT's context in a single tool_result. Keep
442
- // the head + tail the conclusion is usually at the end.
443
- const SUBTASK_MAX = 48000; // chars (~12k tokens)
444
- if (text.length > SUBTASK_MAX) {
445
- const head = text.slice(0, Math.floor(SUBTASK_MAX * 0.6));
446
- const tail = text.slice(text.length - Math.floor(SUBTASK_MAX * 0.4));
447
- text = `${head}\n\n[… sub-task output truncated (${text.length} chars) — kept the beginning and end …]\n\n${tail}`;
603
+ const mins = Math.round(SUBTASK_TIMEOUT_MS / 60000);
604
+ const partial = capSubTaskText(extractSubTaskText(result, false));
605
+ const progress = summariseSubTaskProgress(result);
606
+ const sections = [
607
+ `Sub-task STOPPED after ${mins} minutes without completing — treat the following as PARTIAL, unverified work, not a finished answer.`,
608
+ progress,
609
+ partial ? `Partial output before it was stopped:\n\n${partial}` : '',
610
+ 'Do NOT simply re-run the same sub-task: build on what is above, or split the remaining work into smaller, more focused sub-tasks.',
611
+ ].filter(Boolean);
612
+ // Returned as `error` (not `output`) on purpose: the loop's ledger counts an
613
+ // errored call as a non-effect, which is right nothing here is verified —
614
+ // and the STALL_LIMIT runaway guard must still see repeated timeouts as
615
+ // failures so a permanently stuck sub-task can't loop forever.
616
+ return { error: sections.join('\n\n') };
448
617
  }
618
+ // Normal completion: the final assistant message is the sub-agent's answer.
619
+ const text = capSubTaskText(extractSubTaskText(result, true));
449
620
  return { output: text || '(sub-task completed with no text output)' };
450
621
  }
451
622
  catch (err) {
623
+ // Same salvage rule as the timeout path above, for the other way a sub-agent
624
+ // dies: runAgentLoop throws AgentTurnError when its stream fails, and that
625
+ // error CARRIES the history completed up to the failure precisely so callers
626
+ // don't lose it (see types.ts). Discarding it here — as this catch used to —
627
+ // reproduced the exact waste that class was written to prevent, one level down.
628
+ const salvaged = (0, types_1.salvageHistory)(err);
629
+ if (salvaged) {
630
+ const partial = capSubTaskText(extractSubTaskText(salvaged, false));
631
+ const progress = summariseSubTaskProgress(salvaged);
632
+ const sections = [
633
+ `Sub-task FAILED before completing: ${err.message}`,
634
+ progress,
635
+ partial ? `Partial output before the failure:\n\n${partial}` : '',
636
+ 'Treat the above as PARTIAL, unverified work. Build on it rather than re-running the whole sub-task.',
637
+ ].filter(Boolean);
638
+ return { error: sections.join('\n\n') };
639
+ }
452
640
  return { error: `Sub-task failed: ${err.message}` };
453
641
  }
454
642
  finally {
@@ -1515,7 +1703,26 @@ async function runAgentLoop(initialMessages, options) {
1515
1703
  result = { error: 'Permission denied by user' };
1516
1704
  }
1517
1705
  else if (name === 'task') {
1518
- result = await runSubTask(input, options, agentTypes);
1706
+ // Gated so a burst of `task` blocks in one message becomes a QUEUE
1707
+ // rather than N concurrent agent loops (see MAX_CONCURRENT_SUBTASKS).
1708
+ //
1709
+ // The gate wraps the CALL, not the inside of runSubTask, and that
1710
+ // placement is the whole trick: runSubTask arms its own 10-minute stall
1711
+ // timeout as its first act, so acquiring the slot inside it would start
1712
+ // that clock while the sub-task was still sitting in the queue. A
1713
+ // sub-task queued behind three long-running siblings could then be
1714
+ // "stopped for stalling" having never executed a single step.
1715
+ //
1716
+ // Only TOP-LEVEL fan-out is gated (`depth === 0`) — a deliberate
1717
+ // deadlock guard, kept even though MAX_TASK_DEPTH currently makes a
1718
+ // nested spawn impossible anyway. A nested spawn would request a slot
1719
+ // while its parent still holds one; if every slot were held by a parent
1720
+ // waiting on a child that can never be scheduled, the run would wedge
1721
+ // permanently. Keeping the limiter acyclic (a holder never re-enters it)
1722
+ // means raising MAX_TASK_DEPTH later can't silently reintroduce that.
1723
+ result = depth === 0
1724
+ ? await _subTaskLimit(() => runSubTask(input, options, agentTypes))
1725
+ : await runSubTask(input, options, agentTypes);
1519
1726
  }
1520
1727
  else {
1521
1728
  const pre = runToolHooks(hooks.PreToolUse, 'PreToolUse', name, input, options.workDir);
@@ -76,6 +76,18 @@ export declare function streamChat(messages: Message[], options: StreamChatOptio
76
76
  * bounds the cost if this request never lands.
77
77
  */
78
78
  export declare function cancelTurn(turnId: string): Promise<void>;
79
+ /**
80
+ * Revoke the stored refresh token server-side.
81
+ *
82
+ * Deleting the local config alone is NOT a logout once refresh tokens exist: the
83
+ * token stays valid for its full 60-day life and can keep minting access tokens
84
+ * for anyone who recovered it (a synced dotfile, a backup, a shared machine).
85
+ * The backend already exposes /api/auth/logout for exactly this.
86
+ *
87
+ * Best-effort and never throws — a failed revoke must not stop the local
88
+ * credentials from being cleared, which is the part the user can see.
89
+ */
90
+ export declare function revokeRefreshToken(): Promise<void>;
79
91
  export declare function getBalance(): Promise<number>;
80
92
  export declare function exchangeVscodeCode(code: string): Promise<AuthConfig>;
81
93
  export declare function login(email: string, password: string): Promise<AuthConfig>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;AAeD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6IAA6I;IAC7I,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAiDD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CAu7BlB;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU9D;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;AAiGD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6IAA6I;IAC7I,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAiDD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA6jClB;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU9D;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAYxD;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CA0BlD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA+B1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA0BhF"}
@@ -7,6 +7,7 @@ exports.API_BASE = void 0;
7
7
  exports.chooseFinalContent = chooseFinalContent;
8
8
  exports.streamChat = streamChat;
9
9
  exports.cancelTurn = cancelTurn;
10
+ exports.revokeRefreshToken = revokeRefreshToken;
10
11
  exports.getBalance = getBalance;
11
12
  exports.exchangeVscodeCode = exchangeVscodeCode;
12
13
  exports.login = login;
@@ -115,6 +116,91 @@ function authHeaders() {
115
116
  Authorization: `Bearer ${token}`,
116
117
  };
117
118
  }
119
+ // ─── Silent token refresh ─────────────────────────────────────────────────────
120
+ /**
121
+ * In-flight refresh, shared by every caller that discovers an expired token at the
122
+ * same moment.
123
+ *
124
+ * This is a correctness requirement, not a micro-optimisation. Refresh tokens
125
+ * ROTATE: presenting one invalidates it and returns a replacement, and presenting
126
+ * a spent one is treated by the backend as theft (`reuse_detected`) — it revokes
127
+ * the entire token family and forces a real re-login. A long turn can easily have
128
+ * several requests in flight (the stream, a cancel, a balance poll), so without
129
+ * this they would each present the SAME refresh token and all but the first would
130
+ * look exactly like an attack. De-duplicating means one rotation, one winner,
131
+ * everyone else awaiting the same promise.
132
+ */
133
+ let _refreshInFlight = null;
134
+ /**
135
+ * Exchange the stored refresh token for a fresh access token. Resolves true when
136
+ * the tokens on disk were updated, false when refreshing isn't possible (no
137
+ * refresh token stored, or the server rejected it).
138
+ *
139
+ * Deliberately never throws: every call site treats `false` as "carry on and let
140
+ * the original 401/403 surface", which is the correct fallback in all cases.
141
+ */
142
+ async function refreshAccessToken() {
143
+ if (_refreshInFlight)
144
+ return _refreshInFlight;
145
+ _refreshInFlight = (async () => {
146
+ const refreshToken = (0, index_1.getRefreshToken)();
147
+ // No refresh token: a NEXRALL_TOKEN/CI credential, or a config written before
148
+ // this field existed. Nothing to do — the caller falls back to the auth error.
149
+ if (!refreshToken)
150
+ return false;
151
+ try {
152
+ const res = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/refresh`, {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' },
155
+ body: JSON.stringify({ refreshToken }),
156
+ });
157
+ if (!res.ok)
158
+ return false; // 401 = invalid/expired/reused → a real re-login is needed
159
+ const data = (await res.json());
160
+ if (!data.token)
161
+ return false;
162
+ (0, index_1.updateTokens)(data.token, data.refreshToken);
163
+ return true;
164
+ }
165
+ catch {
166
+ // Network failure. Not an auth problem — report failure so the caller's own
167
+ // retry/backoff machinery handles it as the transport error it is.
168
+ return false;
169
+ }
170
+ })();
171
+ try {
172
+ return await _refreshInFlight;
173
+ }
174
+ finally {
175
+ _refreshInFlight = null;
176
+ }
177
+ }
178
+ /**
179
+ * Is this response an EXPIRED-CREDENTIAL failure that a refresh could fix?
180
+ *
181
+ * The backend's auth middleware answers 401 for a missing token and 403 for one
182
+ * that is invalid or expired. 403 is ambiguous — Anthropic's upstream
183
+ * "forbidden" passthrough uses it too — so the body has to disambiguate: the
184
+ * upstream shape is `{error:{type:'forbidden'}}` (a nested object), while the auth
185
+ * middleware sends a flat `{error:'Invalid or expired token'}` string. Refreshing
186
+ * on the upstream shape would be harmless but pointless; getting it backwards and
187
+ * NOT refreshing on the auth shape is the bug being fixed, so the test suite pins
188
+ * both directions.
189
+ */
190
+ function isExpiredTokenResponse(status, body) {
191
+ if (status !== 401 && status !== 403)
192
+ return false;
193
+ try {
194
+ const parsed = JSON.parse(body);
195
+ // Nested object → Anthropic's upstream passthrough, not our auth layer.
196
+ if (parsed?.error && typeof parsed.error === 'object')
197
+ return false;
198
+ return typeof parsed?.error === 'string' && /token|auth|expired/i.test(parsed.error);
199
+ }
200
+ catch {
201
+ return false;
202
+ }
203
+ }
118
204
  // ─── Retry helpers ────────────────────────────────────────────────────────────
119
205
  const MAX_RETRIES = 5;
120
206
  const RETRY_BASE_MS = 1000;
@@ -301,6 +387,71 @@ async function streamChat(messages, options, onEvent) {
301
387
  onEvent({ type: 'retry_resolved' });
302
388
  }
303
389
  };
390
+ /**
391
+ * Abandon the current turnId and prepare to restart the turn from scratch.
392
+ *
393
+ * Used whenever the server tells us this turn can no longer be followed — the
394
+ * buffered frame window moved past our cursor (410), the turn is simply gone
395
+ * (404, e.g. the owning pod was replaced by a deploy), or a mid-stream frame
396
+ * carried `notResumable`. In all of those cases resuming again with the same
397
+ * cursor would hit the identical wall, so the only recovery is a clean restart.
398
+ *
399
+ * Two things make this more than a state reset, and both are easy to get wrong
400
+ * — which is why all three call sites funnel through here rather than each
401
+ * open-coding it:
402
+ *
403
+ * 1. The OLD turn must be cancelled and AWAITED first. A 410/404 does not mean
404
+ * the turn ended — it is very likely still generating and still holding its
405
+ * idempotency claim, so re-POSTing the same turnId would be rejected as
406
+ * `turnInFlight` and we would burn the entire retry budget waiting, while
407
+ * the orphaned turn kept generating and billing.
408
+ * 2. Anything already rendered has to be reconciled. A restart re-sends the
409
+ * answer from the beginning, so unless the caller can un-render its partial
410
+ * output, restarting would duplicate it on screen.
411
+ *
412
+ * Returns `false` when the caller cannot roll back its rendered output; the
413
+ * caller must then surface a terminal error instead of restarting.
414
+ */
415
+ const prepareRestart = async (reason) => {
416
+ await cancelTurn(turnId);
417
+ turnId = (0, crypto_1.randomUUID)(); // a restart is semantically a NEW turn
418
+ resuming = false;
419
+ serverResumable = false;
420
+ lastEventId = 0;
421
+ carryText = [];
422
+ carryToolUse = [];
423
+ // Snapshot BEFORE resetting: the rollback decision needs to know what the user
424
+ // can currently see, but the counters must not carry into the restarted turn or
425
+ // a second restart would report characters this one already discarded.
426
+ const hadRendered = emittedAnythingAcrossAttempts;
427
+ const renderedChars = emittedCharsAcrossAttempts;
428
+ emittedAnythingAcrossAttempts = false;
429
+ emittedCharsAcrossAttempts = 0;
430
+ if (hadRendered && !allowRestartAfterRender)
431
+ return false;
432
+ if (hadRendered) {
433
+ onEvent({ type: 'stream_restart', reason, discardedChars: renderedChars });
434
+ }
435
+ return true;
436
+ };
437
+ /**
438
+ * Does an error response body carry the backend's explicit `notResumable` flag?
439
+ *
440
+ * The resume endpoint sets this on every "give up on this turnId" response
441
+ * (routes/code.js: the 410 gap case, the 404 no-such-turn case, and the
442
+ * synthesised mid-stream error frames). Reading the FLAG rather than matching
443
+ * the status code alone means a new give-up condition on the server is handled
444
+ * correctly here the day it ships, instead of falling through to the generic
445
+ * terminal-error path and killing a turn that was perfectly recoverable.
446
+ */
447
+ const bodySaysNotResumable = (body) => {
448
+ try {
449
+ return JSON.parse(body)?.notResumable === true;
450
+ }
451
+ catch {
452
+ return false;
453
+ }
454
+ };
304
455
  // One network attempt: connect (with connect-time 429/5xx retry) and consume the stream to
305
456
  // completion. Returns the assembled assistant Message, or throws — tagging a transient
306
457
  // pre-content failure with { retryable: true } for the outer loop below.
@@ -318,8 +469,19 @@ async function streamChat(messages, options, onEvent) {
318
469
  // short in-loop retry budget — the final throw tags `retryable: true` so it still
319
470
  // reaches the outer 5-minute stream-retry loop instead of dying as a terminal error.
320
471
  let forbiddenExhausted = false;
472
+ // Whether this attempt has already spent its one silent token refresh. Scoped to
473
+ // the attempt, not the whole call: if a freshly-minted token is ALSO rejected the
474
+ // failure isn't expiry, and retrying would pointlessly rotate the refresh token
475
+ // against a server that keeps refusing.
476
+ let refreshedThisAttempt = false;
477
+ // Set when the failure is a dead credential that refreshing could not fix, so the
478
+ // terminal throw below can tag it `authExpired` — letting the UI show a "sign in
479
+ // again" prompt instead of a generic error, and keeping it out of the outer
480
+ // retry loop (a re-login is not something reconnecting can solve).
481
+ let sessionExpired = false;
321
482
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
322
483
  try {
484
+ errorBodyOverride = null; // a fresh response — never reuse the previous body
323
485
  response = await (0, node_fetch_1.default)(...buildFetchArgs());
324
486
  if (response.status === 429 && attempt < MAX_RETRIES && canRetry()) {
325
487
  reportRetry('Rate limited by the API — retrying');
@@ -332,6 +494,57 @@ async function streamChat(messages, options, onEvent) {
332
494
  await sleepWithinBudget(backoffMs(attempt));
333
495
  continue;
334
496
  }
497
+ // ── Expired access token → refresh once, then retry ──────────────────
498
+ //
499
+ // The backend issues access tokens with a 2-HOUR TTL and has always
500
+ // returned a long-lived rotating refresh token alongside them. The web app
501
+ // refreshes silently; the CLI and extension discarded the refresh token
502
+ // entirely and stored only the access token — so after exactly two hours,
503
+ // mid-session, every request started failing with "Invalid or expired
504
+ // token" and the run simply died with no recovery and no useful guidance.
505
+ // That is the "it just stops working after a while" report.
506
+ //
507
+ // Refreshed at most once per attempt (`refreshedThisAttempt`): if the new
508
+ // token is ALSO rejected, the problem isn't expiry and looping would just
509
+ // rotate the refresh token repeatedly against a server that keeps saying no
510
+ // — every rotation invalidating the previous one.
511
+ //
512
+ // A successful refresh retries transparently. This is before any streaming,
513
+ // so nothing has been rendered and nothing can be duplicated.
514
+ if (response.status === 401 || response.status === 403) {
515
+ const bodyText = await response.text().catch(() => '');
516
+ if (isExpiredTokenResponse(response.status, bodyText)) {
517
+ if (!refreshedThisAttempt) {
518
+ refreshedThisAttempt = true;
519
+ if (await refreshAccessToken()) {
520
+ reportRetry('Session expired — signing you back in');
521
+ attempt--; // a refresh is not a failed attempt; don't spend the budget on it
522
+ continue; // buildFetchArgs()/authHeaders() will pick up the new token
523
+ }
524
+ }
525
+ // Either refreshing was impossible (no refresh token stored — a config
526
+ // predating this feature, or a NEXRALL_TOKEN credential), it was refused
527
+ // (expired/revoked/reused), or the freshly-minted token was rejected too.
528
+ // All three genuinely need a human.
529
+ //
530
+ // Signalled by BREAKING with a rewritten body rather than throwing: a
531
+ // throw here lands in this loop's own catch, which cannot tell it from a
532
+ // network failure and would retry it — firing a burst of full-context
533
+ // POSTs at a credential that will never be accepted. Breaking routes it
534
+ // through the terminal `!response.ok` handler below, which is exactly
535
+ // what a deterministic auth failure should use.
536
+ sessionExpired = true;
537
+ errorBodyOverride = JSON.stringify({
538
+ // Replaces the backend's bare "Invalid or expired token", which reads
539
+ // like a bug rather than something the user can act on.
540
+ error: 'Your session has expired. Run `nex login` to sign in again.',
541
+ });
542
+ break;
543
+ }
544
+ // Not an expiry — hand the body we already consumed to the handlers below
545
+ // (node-fetch bodies are single-use).
546
+ errorBodyOverride = bodyText;
547
+ }
335
548
  // 403 `{"error":{"type":"forbidden","message":"Request not allowed"}}` is
336
549
  // Anthropic's own upstream response shape (reproduced verbatim through the AI
337
550
  // Gateway passthrough — see services/aiGatewayClient.js), not something Nexrall's
@@ -345,7 +558,11 @@ async function streamChat(messages, options, onEvent) {
345
558
  // burning the whole reconnect budget on something that will never succeed.
346
559
  const FORBIDDEN_RETRY_LIMIT = 2;
347
560
  if (response.status === 403) {
348
- const bodyText = await response.text().catch(() => '');
561
+ // The expiry check above already consumed the body on this path, and a
562
+ // node-fetch body is single-use — calling .text() again yields '' and the
563
+ // upstream-forbidden shape would never be recognised, turning a retryable
564
+ // blip into an instant terminal failure.
565
+ const bodyText = errorBodyOverride ?? await response.text().catch(() => '');
349
566
  let isUpstreamForbidden = false;
350
567
  try {
351
568
  const parsed = JSON.parse(bodyText);
@@ -374,53 +591,49 @@ async function streamChat(messages, options, onEvent) {
374
591
  errorBodyOverride = bodyText;
375
592
  break;
376
593
  }
377
- // 410 Gone a resume was refused because the buffered window no longer contains
378
- // our position (we were away too long, or the turn produced a huge amount in the
379
- // meantime). Continuity can't be proven, and silently skipping frames would corrupt
380
- // the message being rebuilt. So abandon resuming and fall back to restarting the
381
- // turn from scratch: correct, just not free.
382
- if (response.status === 410 && resuming) {
383
- await response.text().catch(() => '');
384
- // A 410 means the BUFFER WINDOW moved past our cursor — it does NOT mean the turn
385
- // ended. It is almost certainly still generating and still holding its idempotency
386
- // claim, so re-POSTing with the same turnId would be rejected as `turnInFlight`,
387
- // and we would wait out the entire retry budget only to fail while the orphaned
388
- // turn kept generating and billing.
389
- //
390
- // So: stop the old turn (awaited the claim must be released BEFORE we re-POST),
391
- // and mint a fresh id, because a restart is semantically a new turn.
392
- await cancelTurn(turnId);
393
- turnId = (0, crypto_1.randomUUID)();
394
- resuming = false;
395
- serverResumable = false;
396
- lastEventId = 0;
397
- carryText = [];
398
- carryToolUse = [];
399
- // Snapshot BEFORE resetting: the rollback decision below needs to know what the
400
- // user can currently see, but the counters must not carry into the restarted turn
401
- // or a second restart would report characters this one already discarded.
402
- const hadRendered = emittedAnythingAcrossAttempts;
403
- const renderedChars = emittedCharsAcrossAttempts;
404
- emittedAnythingAcrossAttempts = false;
405
- emittedCharsAcrossAttempts = 0;
406
- if (hadRendered && !allowRestartAfterRender) {
407
- // Output is already on screen and the caller can't take it back, so a restart
408
- // would duplicate it. Surface the failure instead of corrupting the transcript.
409
- throw new Error('Connection lost and this turn could no longer be resumed. Send your message again.');
410
- }
411
- if (hadRendered) {
412
- onEvent({
413
- type: 'stream_restart',
414
- reason: 'Connection lost too long to resume',
415
- discardedChars: renderedChars,
416
- });
417
- }
418
- if (attempt < MAX_RETRIES && canRetry()) {
419
- reportRetry('Could not resume — restarting this turn');
420
- await sleepWithinBudget(backoffMs(attempt));
421
- continue;
594
+ // A resume that the server refuses: this turnId can no longer be followed.
595
+ //
596
+ // 410 Gone — the buffered frame window moved past our cursor (we were
597
+ // away too long, or the turn produced a lot meanwhile).
598
+ // Continuity can't be proven and skipping frames would
599
+ // corrupt the message being rebuilt.
600
+ // 404 Not Found — the turn is gone entirely: nothing buffered AND no live
601
+ // registry record, which is what a deploy/pod replacement
602
+ // looks like from here.
603
+ //
604
+ // 404 used to fall straight through to the generic terminal-error handler
605
+ // below, so a backend rolling restart mid-turn killed the turn outright with
606
+ // "No such turn to resume." — even though the recovery path for it (mint a
607
+ // new turnId and restart) was already sitting right here for 410. That was
608
+ // the "disconnects and never reconnects" case.
609
+ //
610
+ // Matched on the backend's explicit `notResumable` flag in the body rather
611
+ // than on a hardcoded status list, so a new give-up condition on the server
612
+ // is handled the day it ships. The status check keeps 410 working even if a
613
+ // proxy strips the body.
614
+ if (resuming && response.status >= 400 && response.status < 500) {
615
+ // May already have been read by the token-expiry check above (401/403);
616
+ // node-fetch bodies are single-use, so reuse it rather than reading ''.
617
+ const body = errorBodyOverride ?? await response.text().catch(() => '');
618
+ if (response.status === 410 || response.status === 404 || bodySaysNotResumable(body)) {
619
+ const canRestart = await prepareRestart('Connection lost too long to resume');
620
+ if (!canRestart) {
621
+ // Output is already on screen and the caller can't take it back, so a restart
622
+ // would duplicate it. Surface the failure instead of corrupting the transcript.
623
+ throw new Error('Connection lost and this turn could no longer be resumed. Send your message again.');
624
+ }
625
+ if (attempt < MAX_RETRIES && canRetry()) {
626
+ reportRetry('Could not resume — restarting this turn');
627
+ await sleepWithinBudget(backoffMs(attempt));
628
+ continue;
629
+ }
630
+ errorBodyOverride = JSON.stringify({ error: 'Could not resume this turn — send your message again.' });
631
+ break;
422
632
  }
423
- errorBodyOverride = JSON.stringify({ error: 'Could not resume this turn send your message again.' });
633
+ // A 4xx during resume that is NOT a give-up signal (401/403 auth, 400 bad
634
+ // turnId). Deterministic — fall through to the terminal handler, reusing the
635
+ // body we already consumed since node-fetch bodies are single-use.
636
+ errorBodyOverride = body;
424
637
  break;
425
638
  }
426
639
  // 409 from the backend's turn-idempotency gate. Two distinct meanings, and the
@@ -518,7 +731,16 @@ async function streamChat(messages, options, onEvent) {
518
731
  // `retryable: true` on `forbiddenExhausted` hands the transient 403-upstream-forbidden
519
732
  // case (see above) to the outer stream-retry loop instead of letting it die as a
520
733
  // terminal error the moment the short in-connect-loop budget runs out.
521
- throw Object.assign(new Error(errMsg), { status: response.status, balance, ...(forbiddenExhausted ? { retryable: true } : {}) });
734
+ //
735
+ // `authExpired` is the opposite signal: the credential is dead and no amount of
736
+ // reconnecting will help, so it must stay terminal AND be distinguishable, so the
737
+ // UI can offer a sign-in instead of showing a generic failure.
738
+ throw Object.assign(new Error(errMsg), {
739
+ status: response.status,
740
+ balance,
741
+ ...(forbiddenExhausted ? { retryable: true } : {}),
742
+ ...(sessionExpired ? { authExpired: true } : {}),
743
+ });
522
744
  }
523
745
  if (!response.body) {
524
746
  throw new Error('Response body is null');
@@ -928,6 +1150,15 @@ async function streamChat(messages, options, onEvent) {
928
1150
  reject(tagTransient(new Error(message)));
929
1151
  }
930
1152
  else {
1153
+ // Terminal error. The three branches above each stop the watchdog
1154
+ // and tear the socket down; this one used to do neither, so a
1155
+ // fatal SSE error left a 5-second interval running and the
1156
+ // response body open for the lifetime of the process. The
1157
+ // rejection settles the promise either way, which is exactly why
1158
+ // the leak was invisible — nothing downstream ever noticed, it
1159
+ // just accumulated one timer per failed turn across a long session.
1160
+ clearInterval(heartbeatWatchdog);
1161
+ stream.destroy?.();
931
1162
  onEvent({ type: 'error', message });
932
1163
  reject(new Error(message));
933
1164
  }
@@ -1011,32 +1242,18 @@ async function streamChat(messages, options, onEvent) {
1011
1242
  if (e.retryable && sAttempt < MAX_RETRIES && canRetry()) {
1012
1243
  // The backend explicitly said this turnId's buffer is unrecoverable (gap in the
1013
1244
  // Redis frame stream, or the owning pod is gone — see the `notResumable` case in
1014
- // client.ts's SSE parser). Resuming with the same `lastEventId` would just hit
1015
- // the same wall again, so exactly like the inline 410-while-resuming handling
1016
- // above release the old claim, mint a fresh turnId, and restart clean instead
1017
- // of falling into the "prefer resuming" branch below.
1245
+ // the SSE parser above). Resuming with the same `lastEventId` would just hit the
1246
+ // same wall again, so release the old claim, mint a fresh turnId and restart
1247
+ // clean instead of falling into the "prefer resuming" branch below.
1248
+ //
1249
+ // Shares prepareRestart() with the HTTP-status path in runAttempt: the two used
1250
+ // to be independent copies of the same fifteen lines, and the cancel-then-remint
1251
+ // ordering they encode is exactly the kind of thing that silently drifts apart.
1018
1252
  if (e.forceRestart) {
1019
- await cancelTurn(turnId);
1020
- turnId = (0, crypto_1.randomUUID)();
1021
- resuming = false;
1022
- serverResumable = false;
1023
- lastEventId = 0;
1024
- carryText = [];
1025
- carryToolUse = [];
1026
- const hadRendered = emittedAnythingAcrossAttempts;
1027
- const renderedChars = emittedCharsAcrossAttempts;
1028
- emittedAnythingAcrossAttempts = false;
1029
- emittedCharsAcrossAttempts = 0;
1030
- if (hadRendered && !allowRestartAfterRender) {
1253
+ const canRestart = await prepareRestart(err.message || 'Could not resume — restarting this turn');
1254
+ if (!canRestart) {
1031
1255
  throw new Error('Connection lost and this turn could no longer be resumed. Send your message again.');
1032
1256
  }
1033
- if (hadRendered) {
1034
- onEvent({
1035
- type: 'stream_restart',
1036
- reason: err.message || 'Could not resume — restarting this turn',
1037
- discardedChars: renderedChars,
1038
- });
1039
- }
1040
1257
  reportRetry('Could not resume — restarting this turn');
1041
1258
  await sleepWithinBudget(backoffMs(sAttempt));
1042
1259
  continue;
@@ -1116,12 +1333,50 @@ async function cancelTurn(turnId) {
1116
1333
  // Ignored deliberately — see above.
1117
1334
  }
1118
1335
  }
1336
+ // ─── Logout ───────────────────────────────────────────────────────────────────
1337
+ /**
1338
+ * Revoke the stored refresh token server-side.
1339
+ *
1340
+ * Deleting the local config alone is NOT a logout once refresh tokens exist: the
1341
+ * token stays valid for its full 60-day life and can keep minting access tokens
1342
+ * for anyone who recovered it (a synced dotfile, a backup, a shared machine).
1343
+ * The backend already exposes /api/auth/logout for exactly this.
1344
+ *
1345
+ * Best-effort and never throws — a failed revoke must not stop the local
1346
+ * credentials from being cleared, which is the part the user can see.
1347
+ */
1348
+ async function revokeRefreshToken() {
1349
+ const refreshToken = (0, index_1.getRefreshToken)();
1350
+ if (!refreshToken)
1351
+ return;
1352
+ try {
1353
+ await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/logout`, {
1354
+ method: 'POST',
1355
+ headers: { 'Content-Type': 'application/json' },
1356
+ body: JSON.stringify({ refreshToken }),
1357
+ });
1358
+ }
1359
+ catch {
1360
+ // Offline logout: the local credentials still get cleared by the caller.
1361
+ }
1362
+ }
1119
1363
  // ─── Get Balance ──────────────────────────────────────────────────────────────
1120
1364
  async function getBalance() {
1121
- const response = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/code/balance`, {
1122
- method: 'GET',
1123
- headers: authHeaders(),
1124
- });
1365
+ const fetchOnce = () => (0, node_fetch_1.default)(`${exports.API_BASE}/api/code/balance`, { method: 'GET', headers: authHeaders() });
1366
+ let response = await fetchOnce();
1367
+ // Same silent refresh as the chat path. This endpoint is polled on session start
1368
+ // and after turns, so on an expired token it is frequently the FIRST thing the
1369
+ // user sees fail — and a startup that reports "API error 403" while the chat
1370
+ // path quietly recovers is just confusing.
1371
+ if (response.status === 401 || response.status === 403) {
1372
+ const body = await response.text().catch(() => '');
1373
+ if (isExpiredTokenResponse(response.status, body) && await refreshAccessToken()) {
1374
+ response = await fetchOnce();
1375
+ }
1376
+ else {
1377
+ throw new Error(`API error ${response.status}: ${body}`);
1378
+ }
1379
+ }
1125
1380
  if (!response.ok) {
1126
1381
  const errText = await response.text();
1127
1382
  throw new Error(`API error ${response.status}: ${errText}`);
@@ -1143,7 +1398,18 @@ async function exchangeVscodeCode(code) {
1143
1398
  const data = (await response.json());
1144
1399
  if (!data.token)
1145
1400
  throw new Error('No token in exchange response');
1146
- return { token: data.token, email: data.user?.email ?? '' };
1401
+ // `refreshToken` is forwarded when present but is NOT expected today: unlike the
1402
+ // email-login path, /vscode-exchange mints a long-lived (30-day) token directly
1403
+ // rather than going through mintSession's short-access-token + refresh pair. So
1404
+ // the extension is not exposed to the 2-hour expiry that broke CLI sessions —
1405
+ // it just expires much later, with no silent renewal. Reading the field now means
1406
+ // that if the backend is ever switched to mintSession for consistency, the
1407
+ // extension starts refreshing silently instead of breaking at the 2-hour mark.
1408
+ return {
1409
+ token: data.token,
1410
+ ...(data.refreshToken ? { refreshToken: data.refreshToken } : {}),
1411
+ email: data.user?.email ?? '',
1412
+ };
1147
1413
  }
1148
1414
  // ─── Login ────────────────────────────────────────────────────────────────────
1149
1415
  async function login(email, password) {
@@ -1160,6 +1426,14 @@ async function login(email, password) {
1160
1426
  if (!data.token) {
1161
1427
  throw new Error('Login response missing token');
1162
1428
  }
1163
- return { token: data.token, email: data.email ?? email };
1429
+ // KEEP THE REFRESH TOKEN. The backend has returned one on this path all along
1430
+ // (routes/auth.js mintSession) and pairs it with an access token that expires in
1431
+ // TWO HOURS. Discarding it here is what made a long working session die mid-run
1432
+ // with "Invalid or expired token" and no way back except a manual `nex login`.
1433
+ return {
1434
+ token: data.token,
1435
+ ...(data.refreshToken ? { refreshToken: data.refreshToken } : {}),
1436
+ email: data.email ?? email,
1437
+ };
1164
1438
  }
1165
1439
  //# sourceMappingURL=client.js.map
@@ -4,4 +4,25 @@ export declare function loadAuth(): AuthConfig | null;
4
4
  export declare function clearAuth(): void;
5
5
  export declare function isAuthenticated(): boolean;
6
6
  export declare function getToken(): string | null;
7
+ /**
8
+ * The stored refresh token, or null when there isn't one to use.
9
+ *
10
+ * Deliberately returns null for a token supplied via NEXRALL_TOKEN: that path is
11
+ * for CI/headless, where the caller owns the credential's lifecycle and there is
12
+ * no refresh token to pair with it. Silently reading a refresh token out of the
13
+ * config file while the access token came from the environment would also mix two
14
+ * different identities if they disagree.
15
+ */
16
+ export declare function getRefreshToken(): string | null;
17
+ /**
18
+ * Replace the stored token pair after a successful refresh, preserving any other
19
+ * fields already in the config (notably `email`, which the refresh response does
20
+ * not echo back and which the UI shows).
21
+ *
22
+ * Refresh tokens ROTATE: the backend invalidates the presented one and returns a
23
+ * replacement, and reusing a spent token is treated as theft (`reuse_detected`)
24
+ * and revokes the family. So this must persist BOTH halves, and must not be
25
+ * skipped just because the access token is what the caller needed.
26
+ */
27
+ export declare function updateTokens(token: string, refreshToken?: string): void;
7
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAiB3C,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAGjD;AAED,wBAAgB,QAAQ,IAAI,UAAU,GAAG,IAAI,CAwB5C;AAED,wBAAgB,SAAS,IAAI,IAAI,CAQhC;AAED,wBAAgB,eAAe,IAAI,OAAO,CAGzC;AAED,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAGxC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAiB3C,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAGjD;AAED,wBAAgB,QAAQ,IAAI,UAAU,GAAG,IAAI,CAwB5C;AAED,wBAAgB,SAAS,IAAI,IAAI,CAQhC;AAED,wBAAgB,eAAe,IAAI,OAAO,CAGzC;AAED,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAGxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAM/C;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAsBvE"}
@@ -38,6 +38,8 @@ exports.loadAuth = loadAuth;
38
38
  exports.clearAuth = clearAuth;
39
39
  exports.isAuthenticated = isAuthenticated;
40
40
  exports.getToken = getToken;
41
+ exports.getRefreshToken = getRefreshToken;
42
+ exports.updateTokens = updateTokens;
41
43
  const fs = __importStar(require("fs"));
42
44
  const path = __importStar(require("path"));
43
45
  const os = __importStar(require("os"));
@@ -97,4 +99,55 @@ function getToken() {
97
99
  const auth = loadAuth();
98
100
  return auth?.token ?? null;
99
101
  }
102
+ /**
103
+ * The stored refresh token, or null when there isn't one to use.
104
+ *
105
+ * Deliberately returns null for a token supplied via NEXRALL_TOKEN: that path is
106
+ * for CI/headless, where the caller owns the credential's lifecycle and there is
107
+ * no refresh token to pair with it. Silently reading a refresh token out of the
108
+ * config file while the access token came from the environment would also mix two
109
+ * different identities if they disagree.
110
+ */
111
+ function getRefreshToken() {
112
+ const envToken = process.env.NEXRALL_TOKEN;
113
+ if (envToken && envToken.trim().length > 0)
114
+ return null;
115
+ const auth = loadAuth();
116
+ const rt = auth?.refreshToken;
117
+ return typeof rt === 'string' && rt.length > 0 ? rt : null;
118
+ }
119
+ /**
120
+ * Replace the stored token pair after a successful refresh, preserving any other
121
+ * fields already in the config (notably `email`, which the refresh response does
122
+ * not echo back and which the UI shows).
123
+ *
124
+ * Refresh tokens ROTATE: the backend invalidates the presented one and returns a
125
+ * replacement, and reusing a spent token is treated as theft (`reuse_detected`)
126
+ * and revokes the family. So this must persist BOTH halves, and must not be
127
+ * skipped just because the access token is what the caller needed.
128
+ */
129
+ function updateTokens(token, refreshToken) {
130
+ // Read the file directly rather than via loadAuth(): under NEXRALL_TOKEN,
131
+ // loadAuth short-circuits to the env value and would let a refresh overwrite the
132
+ // on-disk config with a synthetic single-field object, destroying the user's
133
+ // real stored credentials.
134
+ let existing = {};
135
+ try {
136
+ if (fs.existsSync(CONFIG_FILE)) {
137
+ const parsed = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
138
+ if (typeof parsed === 'object' && parsed !== null)
139
+ existing = parsed;
140
+ }
141
+ }
142
+ catch { /* corrupt or unreadable — start clean rather than refuse to save */ }
143
+ ensureConfigDir();
144
+ const next = {
145
+ ...existing,
146
+ token,
147
+ // Keep the previous refresh token if the server didn't send a new one, rather
148
+ // than deleting the only means of refreshing again.
149
+ ...(refreshToken ? { refreshToken } : {}),
150
+ };
151
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
152
+ }
100
153
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AA6nEtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,UAAU,CAAC,CAoBrB"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AA4sEtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,UAAU,CAAC,CAoBrB"}
@@ -405,13 +405,12 @@ async function writeFile(input, workDir) {
405
405
  }
406
406
  catch { /* ignore */ }
407
407
  }
408
- fs.writeFileSync(resolved, content, 'utf-8');
409
- if (existingMode !== undefined) {
410
- try {
411
- fs.chmodSync(resolved, existingMode);
412
- }
413
- catch { /* non-fatal */ }
414
- }
408
+ // Atomic (temp + rename), matching edit_file/multi_edit. A plain
409
+ // writeFileSync on the target truncates it first and then streams the
410
+ // content back, so an interrupt partway through a large write left a
411
+ // TRUNCATED source file and reported nothing — the worst outcome for the
412
+ // very tool most likely to be handed a huge payload.
413
+ atomicWrite(resolved, content, existingMode);
415
414
  const bytes = Buffer.byteLength(content, 'utf-8');
416
415
  const lines = content.split('\n').length;
417
416
  if (isNew) {
@@ -1272,15 +1271,102 @@ function preflightEdit(resolved) {
1272
1271
  }
1273
1272
  return { mode: stat.mode, content: fs.readFileSync(resolved, 'utf-8') };
1274
1273
  }
1275
- /** Atomic write that preserves the original file's permission bits (mode). */
1276
- function atomicWritePreservingMode(resolved, data, mode) {
1277
- const tmp = resolved + '.nexrall_tmp';
1278
- fs.writeFileSync(tmp, data, 'utf-8');
1274
+ /**
1275
+ * Write `data` to `resolved` atomically: build it in a sibling temp file, then
1276
+ * rename over the target. rename(2) is atomic within a filesystem, so a reader —
1277
+ * or a crash — sees either the entire old file or the entire new one, never a
1278
+ * half-written truncation.
1279
+ *
1280
+ * This matters most for exactly the case it used NOT to cover: a large
1281
+ * `write_file`. `fs.writeFileSync` on the target truncates it to zero and then
1282
+ * streams the content back in, so a Ctrl+C, an OOM or a machine losing power
1283
+ * partway through leaves a TRUNCATED source file with no error reported
1284
+ * anywhere — the silent-corruption failure mode this whole module tries to avoid.
1285
+ *
1286
+ * The temp name carries the pid: `withFileLock` in agent/loop.ts serialises
1287
+ * writes to one path within a process, but nothing coordinates two `nex`
1288
+ * processes (or a CLI and the VS Code extension) pointed at the same repo, and
1289
+ * a shared fixed suffix would let them clobber each other's staging file and
1290
+ * commit a mix of both payloads.
1291
+ *
1292
+ * `mode` is optional: a brand-new file has no prior permission bits to keep, and
1293
+ * should get the umask default that writeFileSync would have produced.
1294
+ */
1295
+ function atomicWrite(resolved, data, mode) {
1296
+ // Follow a symlink to its real target before staging.
1297
+ //
1298
+ // rename(2) replaces the NAME, so renaming onto a symlink would delete the link
1299
+ // and leave a regular file in its place — silently breaking the indirection the
1300
+ // user set up (a dotfile linked into a repo, a linked config). Plain
1301
+ // writeFileSync followed the link and wrote through it, and that behaviour must
1302
+ // be preserved. Resolving to the real path also keeps the temp file on the same
1303
+ // filesystem as the actual target, which is what makes the rename atomic (and
1304
+ // not an EXDEV failure).
1305
+ let target = resolved;
1306
+ try {
1307
+ if (fs.lstatSync(resolved).isSymbolicLink())
1308
+ target = fs.realpathSync(resolved);
1309
+ }
1310
+ catch {
1311
+ // Doesn't exist yet (the common create case) or is unreadable — write to the
1312
+ // path as given, exactly as before.
1313
+ }
1314
+ const tmp = `${target}.nexrall_tmp_${process.pid}`;
1315
+ // ── Stage ────────────────────────────────────────────────────────────────
1316
+ // Creating the temp file needs WRITE permission on the containing directory,
1317
+ // which a direct write to an existing file does not. That difference is a real
1318
+ // (if uncommon) compatibility break: a writable file inside a read-only
1319
+ // directory used to be editable and would suddenly stop being. Atomicity is a
1320
+ // safety improvement, so it must not take away a capability the user had —
1321
+ // when staging is impossible for that structural reason, fall back to the
1322
+ // previous direct write rather than failing the tool call.
1323
+ try {
1324
+ fs.writeFileSync(tmp, data, 'utf-8');
1325
+ }
1326
+ catch (err) {
1327
+ const code = err.code;
1328
+ if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS') {
1329
+ fs.writeFileSync(target, data, 'utf-8');
1330
+ if (mode !== undefined) {
1331
+ try {
1332
+ fs.chmodSync(target, mode);
1333
+ }
1334
+ catch { /* non-fatal */ }
1335
+ }
1336
+ return;
1337
+ }
1338
+ // ENOSPC, EDQUOT, EISDIR, … — genuine failures. Nothing was staged, so the
1339
+ // original file is untouched; surface it.
1340
+ try {
1341
+ fs.rmSync(tmp, { force: true });
1342
+ }
1343
+ catch { /* best effort */ }
1344
+ throw err;
1345
+ }
1346
+ // ── Commit ───────────────────────────────────────────────────────────────
1279
1347
  try {
1280
- fs.chmodSync(tmp, mode);
1348
+ if (mode !== undefined) {
1349
+ try {
1350
+ fs.chmodSync(tmp, mode);
1351
+ }
1352
+ catch { /* non-fatal — keep going */ }
1353
+ }
1354
+ fs.renameSync(tmp, target);
1355
+ }
1356
+ catch (err) {
1357
+ // Never leave staging debris next to the user's source on a failed commit
1358
+ // (a cross-device target, a permissions change mid-write). The original file
1359
+ // is untouched, which is the entire point of staging.
1360
+ try {
1361
+ fs.rmSync(tmp, { force: true });
1362
+ }
1363
+ catch { /* best effort */ }
1364
+ throw err;
1281
1365
  }
1282
- catch { /* non-fatal — keep going */ }
1283
- fs.renameSync(tmp, resolved);
1366
+ }
1367
+ /** Atomic write that preserves the original file's permission bits (mode). */
1368
+ function atomicWritePreservingMode(resolved, data, mode) {
1369
+ atomicWrite(resolved, data, mode);
1284
1370
  }
1285
1371
  // Normalize CRLF → LF so Windows files match LF old_strings.
1286
1372
  function normalizeLF(s) {
package/dist/types.d.ts CHANGED
@@ -186,7 +186,23 @@ export interface ToolResult {
186
186
  exitCode?: number;
187
187
  }
188
188
  export interface AuthConfig {
189
+ /** Short-lived access token (the backend issues these with a 2-hour TTL). */
189
190
  token: string;
191
+ /**
192
+ * Long-lived rotating refresh token, used to mint a new access token without a
193
+ * manual re-login.
194
+ *
195
+ * The backend has issued one on every login path for a long time and exposes
196
+ * POST /api/auth/refresh to exchange it — the web app uses both. The CLI and
197
+ * extension simply discarded it and stored the access token alone, so after
198
+ * exactly two hours every request began failing with "Invalid or expired
199
+ * token", mid-session, with no recovery: a long working session would just die.
200
+ *
201
+ * Optional because it is genuinely absent in two legitimate cases: a token
202
+ * supplied via NEXRALL_TOKEN (CI/headless, no refresh flow), and a config file
203
+ * written by a version that predates this field.
204
+ */
205
+ refreshToken?: string;
190
206
  email?: string;
191
207
  }
192
208
  export interface PermissionRequest {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,aAAa,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAKD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9D;AAKD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,iBAAiB,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE;AAED,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,YAAY,GAAG,eAAe,GAAG,UAAU,GAAG,aAAa,CAAC;AAInG,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAID,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,kBAAkB,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,0HAA0H;AAC1H,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,QAAQ,GAChB,YAAY,GACZ,eAAe,GACf,uBAAuB,GACvB,aAAa,GACb,YAAY,GACZ,aAAa,GACb,gBAAgB,GAChB,wBAAwB,GACxB,qBAAqB,GACrB,aAAa,GACb,qBAAqB,GACrB,qBAAqB,GACrB,qBAAqB,CAAC;AAI1B,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gEAAgE;IAChE,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvF,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChH,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACvF,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9E;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/E;;;;;OAKG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE,+FAA+F;IAC/F,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACnE;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAClE;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnG,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,eAAe,EAAE,UAAU,CAAC;IAChD,qFAAqF;IACrF,iBAAiB,CAAC,EAAE,OAAO,sBAAsB,EAAE,iBAAiB,CAAC;IACrE;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC;IAClC,+FAA+F;IAC/F,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAC3C;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC7B;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,6CAA6C;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAOzF;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,cAAc,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,EAAE,GAAG,IAAI,CAG7D"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.22",
3
+ "version": "1.4.23",
4
4
  "description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
5
5
  "license": "MIT",
6
6
  "author": "Nexrall <support@nexrall.com> (https://nexrall.com)",