@adecore/agents 0.27.3 → 0.27.5

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.
@@ -10,6 +10,7 @@ export interface BackendLaunch {
10
10
  runtimeMode: RuntimeMode;
11
11
  resume: string | null;
12
12
  generation: number;
13
+ now?(): number;
13
14
  instructions: string | null;
14
15
  resumeNote: string | null;
15
16
  resumeInstructions?: string | null;
@@ -197,6 +198,7 @@ export type BackendEvent = {
197
198
  lastUuid?: string;
198
199
  };
199
200
  limit?: ChatTurnLimit;
201
+ replaceProcess?: boolean;
200
202
  } | {
201
203
  type: 'failed';
202
204
  message: string;
@@ -85,6 +85,7 @@ export declare class ChatSession {
85
85
  private readonly options;
86
86
  private readonly projector;
87
87
  private backend;
88
+ private replaceProcess;
88
89
  private starting;
89
90
  private pendingStart;
90
91
  private sendAfterStop;
@@ -43,6 +43,7 @@ export class ChatSession {
43
43
  options;
44
44
  projector;
45
45
  backend = null;
46
+ replaceProcess = false;
46
47
  starting = null;
47
48
  pendingStart = null;
48
49
  sendAfterStop = false;
@@ -1564,7 +1565,8 @@ export class ChatSession {
1564
1565
  }
1565
1566
  requiresReplacement() {
1566
1567
  return (this.backend !== null &&
1567
- (JSON.stringify(this.info.selection) !== JSON.stringify(this.launchedSelection) ||
1568
+ (this.replaceProcess ||
1569
+ JSON.stringify(this.info.selection) !== JSON.stringify(this.launchedSelection) ||
1568
1570
  this.info.runtimeMode !== this.launchedRuntimeMode ||
1569
1571
  this.info.account !== this.launchedAccount ||
1570
1572
  this.info.cwd !== this.launchedCwd ||
@@ -1626,6 +1628,7 @@ export class ChatSession {
1626
1628
  runtimeMode: info.runtimeMode,
1627
1629
  resume: info.agentSessionId,
1628
1630
  generation,
1631
+ now: () => this.options.limitResume?.now() ?? (this.options.clock ?? systemClock).now(),
1629
1632
  saveGeneratedImage: this.options.saveGeneratedImage,
1630
1633
  instructions: this.options.instructions?.() ?? null,
1631
1634
  resumeNote: this.options.resumeNote?.() ?? null,
@@ -1650,6 +1653,7 @@ export class ChatSession {
1650
1653
  }
1651
1654
  const backend = made.backend;
1652
1655
  this.backend = backend;
1656
+ this.replaceProcess = false;
1653
1657
  this.emit([this.thread.patchInfo({ running: true, effectiveRuntimeMode: undefined, permissionMode: undefined })]);
1654
1658
  let started;
1655
1659
  try {
@@ -1730,6 +1734,9 @@ export class ChatSession {
1730
1734
  if (event.promptId !== undefined) {
1731
1735
  this.staleResults = 0;
1732
1736
  }
1737
+ if (event.replaceProcess === true) {
1738
+ this.replaceProcess = true;
1739
+ }
1733
1740
  const promptId = event.promptId ?? this.submission?.input.promptId;
1734
1741
  if (promptId !== undefined) {
1735
1742
  this.acceptPrompt(promptId);
@@ -32,7 +32,7 @@ export class ClaudeBackend {
32
32
  this.launch = launch;
33
33
  this.host = host;
34
34
  this.allowedTools = options.allowedTools ?? [];
35
- this.protocol = new ClaudeProtocol(launch.goal ?? null);
35
+ this.protocol = new ClaudeProtocol(launch.goal ?? null, launch.now);
36
36
  }
37
37
  get running() {
38
38
  return this.child !== null;
@@ -3,6 +3,7 @@ import type { ApprovalDecision, BackendEvent } from './backend.ts';
3
3
  export declare function claudeGoalFromCommand(text: string, current: ChatGoal | null): ChatGoal | null | undefined;
4
4
  export declare function claudeGoalCheck(text: string, current: ChatGoal | null): ChatGoal | undefined;
5
5
  export declare class ClaudeProtocol {
6
+ private readonly now;
6
7
  private promptId;
7
8
  private readonly pending;
8
9
  private streamMessageId;
@@ -18,13 +19,14 @@ export declare class ClaudeProtocol {
18
19
  private readonly subagentModels;
19
20
  private apiError;
20
21
  private readonly refusedWindows;
22
+ private readonly refusedResets;
21
23
  private readonly stopRequests;
22
24
  private readonly steerIds;
23
25
  private goal;
24
26
  private goalAnswered;
25
27
  private interrupting;
26
28
  private readonly backgroundAgents;
27
- constructor(goal?: ChatGoal | null);
29
+ constructor(goal?: ChatGoal | null, now?: () => number);
28
30
  noteInterrupt(): void;
29
31
  private setGoal;
30
32
  beginSteer(promptId: string): void;
@@ -50,6 +52,7 @@ export declare class ClaudeProtocol {
50
52
  private handleUser;
51
53
  private handleResult;
52
54
  private noteRefusal;
55
+ private expireRefusedResets;
53
56
  private limit;
54
57
  private handleControlRequest;
55
58
  private textRef;
@@ -153,6 +153,7 @@ export function claudeGoalCheck(text, current) {
153
153
  * belongs to one process, so a resumed CLI that numbers its messages from the start gets its own.
154
154
  */
155
155
  export class ClaudeProtocol {
156
+ now;
156
157
  promptId = null;
157
158
  pending = new Map();
158
159
  streamMessageId = null;
@@ -174,6 +175,8 @@ export class ClaudeProtocol {
174
175
  // Only main-chain errors classify the turn; each plan window can refuse it independently.
175
176
  apiError = null;
176
177
  refusedWindows = new Map();
178
+ // Sparse refusals can omit a reset; cached metadata alone must never classify a later 429.
179
+ refusedResets = new Map();
177
180
  // The task each `stop_task` request named, so an error answer finds it again.
178
181
  stopRequests = new Map();
179
182
  steerIds = new Set();
@@ -184,8 +187,9 @@ export class ClaudeProtocol {
184
187
  interrupting = false;
185
188
  // Background subagents still at work; Claude Code holds a goal's check until they report.
186
189
  backgroundAgents = new Set();
187
- constructor(goal = null) {
190
+ constructor(goal = null, now = () => Date.now()) {
188
191
  this.goal = goal;
192
+ this.now = now;
189
193
  }
190
194
  /* A stop is on its way; the turn it ends leaves the goal set but nobody working on it. */
191
195
  noteInterrupt() {
@@ -204,6 +208,7 @@ export class ClaudeProtocol {
204
208
  this.promptId = promptId;
205
209
  this.apiError = null;
206
210
  this.refusedWindows.clear();
211
+ this.expireRefusedResets();
207
212
  this.lastUuid = null;
208
213
  }
209
214
  handle(frame) {
@@ -713,6 +718,10 @@ export class ClaudeProtocol {
713
718
  const lastUuid = this.lastUuid;
714
719
  this.lastUuid = null;
715
720
  const limit = failed ? this.limit() : null;
721
+ const replaceProcess = failed && this.apiError === 'authentication_failed';
722
+ if (limit?.kind !== 'usage') {
723
+ this.refusedResets.clear();
724
+ }
716
725
  this.apiError = null;
717
726
  this.refusedWindows.clear();
718
727
  // Before the turn ends, so whoever settles work on that end already reads where the goal stands.
@@ -724,21 +733,41 @@ export class ClaudeProtocol {
724
733
  costUsd: num(frame.total_cost_usd),
725
734
  ...(failed ? { error: errors[0] ?? str(frame.result) ?? `The turn ended with ${str(frame.subtype) ?? 'an error'}` } : {}),
726
735
  ...(lastUuid === null ? {} : { native: { lastUuid } }),
727
- ...(limit === null ? {} : { limit })
736
+ ...(limit === null ? {} : { limit }),
737
+ ...(replaceProcess ? { replaceProcess: true } : {})
728
738
  });
729
739
  }
730
740
  noteRefusal(info) {
731
741
  if (!isRecord(info) || typeof info.status !== 'string') {
732
742
  return;
733
743
  }
734
- const window = str(info.rateLimitType) ?? 'unknown';
744
+ this.expireRefusedResets();
745
+ const namedWindow = str(info.rateLimitType);
746
+ const window = namedWindow ?? 'unknown';
735
747
  const overageAllowed = info.overageStatus === 'allowed' || info.overageStatus === 'allowed_warning' || info.isUsingOverage === true || info.overageInUse === true;
736
748
  if (info.status === 'rejected' && !overageAllowed) {
737
749
  const resetsAt = typeof info.resetsAt === 'number' ? info.resetsAt * 1000 : NaN;
738
- this.refusedWindows.set(window, Number.isFinite(resetsAt) && resetsAt > 0 && resetsAt < 8.64e15 ? resetsAt : null);
750
+ const validReset = Number.isFinite(resetsAt) && resetsAt > this.now() && resetsAt < 8.64e15 ? resetsAt : null;
751
+ const reset = validReset ?? (info.resetsAt === undefined && namedWindow ? (this.refusedResets.get(window) ?? null) : null);
752
+ this.refusedWindows.set(window, reset);
753
+ if (namedWindow && reset !== null && reset > this.now()) {
754
+ this.refusedResets.set(window, reset);
755
+ }
756
+ else {
757
+ this.refusedResets.delete(window);
758
+ }
739
759
  }
740
760
  else if (info.status === 'allowed' || info.status === 'allowed_warning' || overageAllowed) {
741
761
  this.refusedWindows.delete(window);
762
+ this.refusedResets.delete(window);
763
+ }
764
+ }
765
+ expireRefusedResets() {
766
+ const now = this.now();
767
+ for (const [window, reset] of this.refusedResets) {
768
+ if (reset <= now) {
769
+ this.refusedResets.delete(window);
770
+ }
742
771
  }
743
772
  }
744
773
  /*
@@ -750,7 +779,7 @@ export class ClaudeProtocol {
750
779
  if (this.refusedWindows.size > 0) {
751
780
  let resetsAt = 0;
752
781
  for (const reset of this.refusedWindows.values()) {
753
- if (reset === null) {
782
+ if (reset === null || reset <= this.now()) {
754
783
  return { kind: 'usage' };
755
784
  }
756
785
  resetsAt = Math.max(resetsAt, reset);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.27.3",
3
+ "version": "0.27.5",
4
4
  "description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -81,8 +81,8 @@
81
81
  "test": "bun --conditions=source test"
82
82
  },
83
83
  "dependencies": {
84
- "@adecore/agent-contracts": "0.27.3",
85
- "@adecore/intelligent-ui": "0.27.3",
84
+ "@adecore/agent-contracts": "0.27.5",
85
+ "@adecore/intelligent-ui": "0.27.5",
86
86
  "zod": "^4.6.5"
87
87
  },
88
88
  "devDependencies": {
@@ -35,6 +35,8 @@ export interface BackendLaunch {
35
35
  // How often this chat started a CLI; a protocol that numbers its own requests from zero
36
36
  // needs it to keep the ids of one process apart from those of the one before.
37
37
  generation: number;
38
+ // Shares the host clock with expiry checks when a test advances time by hand.
39
+ now?(): number;
38
40
  // What the agent is told once, at the start of a process: a system prompt, or a new thread's developer instructions.
39
41
  instructions: string | null;
40
42
  // The part of it a resumed thread has to hear again in front of its next prompt, for a CLI that ignores new
@@ -182,6 +184,8 @@ export type BackendEvent =
182
184
  error?: string;
183
185
  native?: { turnId?: string; lastUuid?: string };
184
186
  limit?: ChatTurnLimit;
187
+ // The next prompt needs fresh process credentials, once native work and requests have settled.
188
+ replaceProcess?: boolean;
185
189
  }
186
190
  // The CLI could not be reached or refused the request; the turn ends and the chat needs a new one.
187
191
  | { type: 'failed'; message: string; processAlive?: boolean }
@@ -170,6 +170,7 @@ export class ChatSession {
170
170
  private readonly options: ChatSessionOptions;
171
171
  private readonly projector: ThreadProjector;
172
172
  private backend: ChatBackend | null = null;
173
+ private replaceProcess = false;
173
174
  private starting: Promise<ChatBackend> | null = null;
174
175
  private pendingStart: { turnId: string; warning: unknown } | null = null;
175
176
  private sendAfterStop = false;
@@ -1805,7 +1806,8 @@ export class ChatSession {
1805
1806
  private requiresReplacement(): boolean {
1806
1807
  return (
1807
1808
  this.backend !== null &&
1808
- (JSON.stringify(this.info.selection) !== JSON.stringify(this.launchedSelection) ||
1809
+ (this.replaceProcess ||
1810
+ JSON.stringify(this.info.selection) !== JSON.stringify(this.launchedSelection) ||
1809
1811
  this.info.runtimeMode !== this.launchedRuntimeMode ||
1810
1812
  this.info.account !== this.launchedAccount ||
1811
1813
  this.info.cwd !== this.launchedCwd ||
@@ -1874,6 +1876,7 @@ export class ChatSession {
1874
1876
  runtimeMode: info.runtimeMode,
1875
1877
  resume: info.agentSessionId,
1876
1878
  generation,
1879
+ now: () => this.options.limitResume?.now() ?? (this.options.clock ?? systemClock).now(),
1877
1880
  saveGeneratedImage: this.options.saveGeneratedImage,
1878
1881
  instructions: this.options.instructions?.() ?? null,
1879
1882
  resumeNote: this.options.resumeNote?.() ?? null,
@@ -1897,6 +1900,7 @@ export class ChatSession {
1897
1900
  }
1898
1901
  const backend = made.backend;
1899
1902
  this.backend = backend;
1903
+ this.replaceProcess = false;
1900
1904
  this.emit([this.thread.patchInfo({ running: true, effectiveRuntimeMode: undefined, permissionMode: undefined })]);
1901
1905
  let started: Promise<void>;
1902
1906
  try {
@@ -1980,6 +1984,9 @@ export class ChatSession {
1980
1984
  if (event.promptId !== undefined) {
1981
1985
  this.staleResults = 0;
1982
1986
  }
1987
+ if (event.replaceProcess === true) {
1988
+ this.replaceProcess = true;
1989
+ }
1983
1990
  const promptId = event.promptId ?? this.submission?.input.promptId;
1984
1991
  if (promptId !== undefined) {
1985
1992
  this.acceptPrompt(promptId);
@@ -41,7 +41,7 @@ export class ClaudeBackend implements ChatBackend {
41
41
  this.launch = launch;
42
42
  this.host = host;
43
43
  this.allowedTools = options.allowedTools ?? [];
44
- this.protocol = new ClaudeProtocol(launch.goal ?? null);
44
+ this.protocol = new ClaudeProtocol(launch.goal ?? null, launch.now);
45
45
  }
46
46
 
47
47
  get running(): boolean {
@@ -187,6 +187,7 @@ export function claudeGoalCheck(text: string, current: ChatGoal | null): ChatGoa
187
187
  * belongs to one process, so a resumed CLI that numbers its messages from the start gets its own.
188
188
  */
189
189
  export class ClaudeProtocol {
190
+ private readonly now: () => number;
190
191
  private promptId: string | null = null;
191
192
  private readonly pending = new Map<string, Pending>();
192
193
  private streamMessageId: string | null = null;
@@ -208,6 +209,8 @@ export class ClaudeProtocol {
208
209
  // Only main-chain errors classify the turn; each plan window can refuse it independently.
209
210
  private apiError: string | null = null;
210
211
  private readonly refusedWindows = new Map<string, number | null>();
212
+ // Sparse refusals can omit a reset; cached metadata alone must never classify a later 429.
213
+ private readonly refusedResets = new Map<string, number>();
211
214
 
212
215
  // The task each `stop_task` request named, so an error answer finds it again.
213
216
  private readonly stopRequests = new Map<string, string>();
@@ -221,8 +224,9 @@ export class ClaudeProtocol {
221
224
  // Background subagents still at work; Claude Code holds a goal's check until they report.
222
225
  private readonly backgroundAgents = new Set<string>();
223
226
 
224
- constructor(goal: ChatGoal | null = null) {
227
+ constructor(goal: ChatGoal | null = null, now: () => number = () => Date.now()) {
225
228
  this.goal = goal;
229
+ this.now = now;
226
230
  }
227
231
 
228
232
  /* A stop is on its way; the turn it ends leaves the goal set but nobody working on it. */
@@ -245,6 +249,7 @@ export class ClaudeProtocol {
245
249
  this.promptId = promptId;
246
250
  this.apiError = null;
247
251
  this.refusedWindows.clear();
252
+ this.expireRefusedResets();
248
253
  this.lastUuid = null;
249
254
  }
250
255
 
@@ -765,6 +770,10 @@ export class ClaudeProtocol {
765
770
  const lastUuid = this.lastUuid;
766
771
  this.lastUuid = null;
767
772
  const limit = failed ? this.limit() : null;
773
+ const replaceProcess = failed && this.apiError === 'authentication_failed';
774
+ if (limit?.kind !== 'usage') {
775
+ this.refusedResets.clear();
776
+ }
768
777
  this.apiError = null;
769
778
  this.refusedWindows.clear();
770
779
  // Before the turn ends, so whoever settles work on that end already reads where the goal stands.
@@ -776,7 +785,8 @@ export class ClaudeProtocol {
776
785
  costUsd: num(frame.total_cost_usd),
777
786
  ...(failed ? { error: errors[0] ?? str(frame.result) ?? `The turn ended with ${str(frame.subtype) ?? 'an error'}` } : {}),
778
787
  ...(lastUuid === null ? {} : { native: { lastUuid } }),
779
- ...(limit === null ? {} : { limit })
788
+ ...(limit === null ? {} : { limit }),
789
+ ...(replaceProcess ? { replaceProcess: true } : {})
780
790
  });
781
791
  }
782
792
 
@@ -784,14 +794,33 @@ export class ClaudeProtocol {
784
794
  if (!isRecord(info) || typeof info.status !== 'string') {
785
795
  return;
786
796
  }
787
- const window = str(info.rateLimitType) ?? 'unknown';
797
+ this.expireRefusedResets();
798
+ const namedWindow = str(info.rateLimitType);
799
+ const window = namedWindow ?? 'unknown';
788
800
  const overageAllowed =
789
801
  info.overageStatus === 'allowed' || info.overageStatus === 'allowed_warning' || info.isUsingOverage === true || info.overageInUse === true;
790
802
  if (info.status === 'rejected' && !overageAllowed) {
791
803
  const resetsAt = typeof info.resetsAt === 'number' ? info.resetsAt * 1000 : NaN;
792
- this.refusedWindows.set(window, Number.isFinite(resetsAt) && resetsAt > 0 && resetsAt < 8.64e15 ? resetsAt : null);
804
+ const validReset = Number.isFinite(resetsAt) && resetsAt > this.now() && resetsAt < 8.64e15 ? resetsAt : null;
805
+ const reset = validReset ?? (info.resetsAt === undefined && namedWindow ? (this.refusedResets.get(window) ?? null) : null);
806
+ this.refusedWindows.set(window, reset);
807
+ if (namedWindow && reset !== null && reset > this.now()) {
808
+ this.refusedResets.set(window, reset);
809
+ } else {
810
+ this.refusedResets.delete(window);
811
+ }
793
812
  } else if (info.status === 'allowed' || info.status === 'allowed_warning' || overageAllowed) {
794
813
  this.refusedWindows.delete(window);
814
+ this.refusedResets.delete(window);
815
+ }
816
+ }
817
+
818
+ private expireRefusedResets(): void {
819
+ const now = this.now();
820
+ for (const [window, reset] of this.refusedResets) {
821
+ if (reset <= now) {
822
+ this.refusedResets.delete(window);
823
+ }
795
824
  }
796
825
  }
797
826
 
@@ -804,7 +833,7 @@ export class ClaudeProtocol {
804
833
  if (this.refusedWindows.size > 0) {
805
834
  let resetsAt = 0;
806
835
  for (const reset of this.refusedWindows.values()) {
807
- if (reset === null) {
836
+ if (reset === null || reset <= this.now()) {
808
837
  return { kind: 'usage' };
809
838
  }
810
839
  resetsAt = Math.max(resetsAt, reset);