@adecore/agents 0.27.2 → 0.27.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/backend.d.ts +3 -0
- package/dist/chat/chat-core.d.ts +1 -0
- package/dist/chat/chat-core.js +1 -0
- package/dist/chat/chat-session.d.ts +2 -0
- package/dist/chat/chat-session.js +9 -1
- package/dist/chat/claude-backend.js +1 -1
- package/dist/chat/claude-protocol.d.ts +4 -1
- package/dist/chat/claude-protocol.js +34 -5
- package/dist/chat/codex-backend.d.ts +1 -0
- package/dist/chat/codex-backend.js +22 -2
- package/dist/testing/fake-codex.d.ts +1 -0
- package/dist/testing/fake-codex.js +16 -0
- package/package.json +3 -3
- package/src/chat/backend.ts +7 -0
- package/src/chat/chat-core.ts +3 -0
- package/src/chat/chat-session.ts +10 -1
- package/src/chat/claude-backend.ts +1 -1
- package/src/chat/claude-protocol.ts +34 -5
- package/src/chat/codex-backend.ts +22 -2
- package/src/testing/fake-codex.ts +18 -0
package/dist/chat/backend.d.ts
CHANGED
|
@@ -10,8 +10,10 @@ 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;
|
|
16
|
+
resumeInstructions?: string | null;
|
|
15
17
|
goal?: ChatGoal;
|
|
16
18
|
folders?: readonly string[];
|
|
17
19
|
projectConfig?: boolean;
|
|
@@ -196,6 +198,7 @@ export type BackendEvent = {
|
|
|
196
198
|
lastUuid?: string;
|
|
197
199
|
};
|
|
198
200
|
limit?: ChatTurnLimit;
|
|
201
|
+
replaceProcess?: boolean;
|
|
199
202
|
} | {
|
|
200
203
|
type: 'failed';
|
|
201
204
|
message: string;
|
package/dist/chat/chat-core.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface ChatCoreHooks {
|
|
|
29
29
|
runtimeModeFor?(chatId: string, mode: RuntimeMode): RuntimeMode;
|
|
30
30
|
instructionsFor?(chatId: string): string | null;
|
|
31
31
|
resumeNoteFor?(chatId: string): string | null;
|
|
32
|
+
resumeInstructionsFor?(chatId: string): string | null;
|
|
32
33
|
foldersFor?(chatId: string): readonly string[];
|
|
33
34
|
projectConfigFor?(chatId: string): boolean;
|
|
34
35
|
hiddenFor?(chatId: string): boolean;
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -306,6 +306,7 @@ export class ChatCore {
|
|
|
306
306
|
saveGeneratedImage: (ref, upload) => this.attachments.saveGenerated(payload.chatId, ref, upload),
|
|
307
307
|
instructions: () => (this.hooks.instructionsFor ? this.hooks.instructionsFor(payload.chatId) : this.instructions),
|
|
308
308
|
resumeNote: () => this.hooks.resumeNoteFor?.(payload.chatId) ?? null,
|
|
309
|
+
resumeInstructions: () => this.hooks.resumeInstructionsFor?.(payload.chatId) ?? null,
|
|
309
310
|
folders: () => this.hooks.foldersFor?.(payload.chatId) ?? [],
|
|
310
311
|
projectConfig: () => this.hooks.projectConfigFor?.(payload.chatId) ?? true,
|
|
311
312
|
...(promptNotes ? { promptNotes } : {}),
|
|
@@ -41,6 +41,7 @@ export interface ChatSessionOptions {
|
|
|
41
41
|
saveGeneratedImage?: BackendLaunch['saveGeneratedImage'];
|
|
42
42
|
instructions?(): string | null;
|
|
43
43
|
resumeNote?(): string | null;
|
|
44
|
+
resumeInstructions?(): string | null;
|
|
44
45
|
folders?(): readonly string[];
|
|
45
46
|
projectConfig?(): boolean;
|
|
46
47
|
promptNotes?: PromptNotes;
|
|
@@ -84,6 +85,7 @@ export declare class ChatSession {
|
|
|
84
85
|
private readonly options;
|
|
85
86
|
private readonly projector;
|
|
86
87
|
private backend;
|
|
88
|
+
private replaceProcess;
|
|
87
89
|
private starting;
|
|
88
90
|
private pendingStart;
|
|
89
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
|
-
(
|
|
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,9 +1628,11 @@ 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,
|
|
1635
|
+
resumeInstructions: this.options.resumeInstructions?.() ?? null,
|
|
1632
1636
|
...(info.goal === undefined ? {} : { goal: info.goal }),
|
|
1633
1637
|
...(folders.length > 0 ? { folders } : {}),
|
|
1634
1638
|
...(projectConfig ? {} : { projectConfig }),
|
|
@@ -1649,6 +1653,7 @@ export class ChatSession {
|
|
|
1649
1653
|
}
|
|
1650
1654
|
const backend = made.backend;
|
|
1651
1655
|
this.backend = backend;
|
|
1656
|
+
this.replaceProcess = false;
|
|
1652
1657
|
this.emit([this.thread.patchInfo({ running: true, effectiveRuntimeMode: undefined, permissionMode: undefined })]);
|
|
1653
1658
|
let started;
|
|
1654
1659
|
try {
|
|
@@ -1729,6 +1734,9 @@ export class ChatSession {
|
|
|
1729
1734
|
if (event.promptId !== undefined) {
|
|
1730
1735
|
this.staleResults = 0;
|
|
1731
1736
|
}
|
|
1737
|
+
if (event.replaceProcess === true) {
|
|
1738
|
+
this.replaceProcess = true;
|
|
1739
|
+
}
|
|
1732
1740
|
const promptId = event.promptId ?? this.submission?.input.promptId;
|
|
1733
1741
|
if (promptId !== undefined) {
|
|
1734
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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -68,7 +68,7 @@ export class CodexBackend {
|
|
|
68
68
|
imageWrites = null;
|
|
69
69
|
transport = null;
|
|
70
70
|
threadId = '';
|
|
71
|
-
// Codex
|
|
71
|
+
// Codex ignores developer instructions on `thread/resume` (measured on 0.154 and 0.163), so the first prompt of a resumed process carries them.
|
|
72
72
|
contextPending = null;
|
|
73
73
|
imageInputSupported = null;
|
|
74
74
|
/* A turn asked for that Codex has not named yet; `turn/interrupt` needs that name. */
|
|
@@ -131,7 +131,7 @@ export class CodexBackend {
|
|
|
131
131
|
if (this.launch.resume) {
|
|
132
132
|
try {
|
|
133
133
|
result = await transport.request('thread/resume', { threadId: this.launch.resume, excludeTurns: true, ...params });
|
|
134
|
-
this.contextPending = this.launch.
|
|
134
|
+
this.contextPending = await this.resumeContext(transport, this.launch.resume);
|
|
135
135
|
}
|
|
136
136
|
catch (error) {
|
|
137
137
|
// A refusal means the thread is gone from Codex's store and a fresh one keeps the chat usable; a silence or an exit says nothing about the thread.
|
|
@@ -156,6 +156,26 @@ export class CodexBackend {
|
|
|
156
156
|
// An app-server without goals, or with the feature turned off, has none to report.
|
|
157
157
|
});
|
|
158
158
|
}
|
|
159
|
+
/*
|
|
160
|
+
* A developer message outranks the thread's first instructions, where a note in front of the prompt loses to
|
|
161
|
+
* them; an app-server without `thread/inject_items` gets the instructions in front of the prompt after all.
|
|
162
|
+
*/
|
|
163
|
+
async resumeContext(transport, threadId) {
|
|
164
|
+
const instructions = this.launch.resumeInstructions ?? null;
|
|
165
|
+
if (instructions !== null) {
|
|
166
|
+
try {
|
|
167
|
+
const items = [{ type: 'message', role: 'developer', content: [{ type: 'input_text', text: instructions }] }];
|
|
168
|
+
await transport.request('thread/inject_items', { threadId, items });
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!(error instanceof Error) || error.name !== 'CodexRpcError') {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
return [instructions, this.launch.resumeNote].filter((part) => part !== null).join('\n\n');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return this.launch.resumeNote;
|
|
178
|
+
}
|
|
159
179
|
async readGoal(transport) {
|
|
160
180
|
const result = await transport.request('thread/goal/get', { threadId: this.threadId });
|
|
161
181
|
const goal = codexGoal(isRecord(result) ? result.goal : null);
|
|
@@ -4,6 +4,7 @@ export declare const FAKE_CHILD_STEPS = 250;
|
|
|
4
4
|
export declare const fakeCodexForks: Frame[];
|
|
5
5
|
export interface FakeCodexOptions {
|
|
6
6
|
resumeLead?: string;
|
|
7
|
+
injectItems?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export declare function fakeCodexWith(options: FakeCodexOptions): FakeCli;
|
|
9
10
|
export declare const fakeCodex: FakeCli;
|
|
@@ -451,6 +451,22 @@ export function fakeCodexWith(options) {
|
|
|
451
451
|
notify('thread/started', { thread });
|
|
452
452
|
return;
|
|
453
453
|
}
|
|
454
|
+
case 'thread/inject_items': {
|
|
455
|
+
if (options.injectItems === false) {
|
|
456
|
+
out({ id, error: { code: -32601, message: 'unknown method thread/inject_items' } });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
// The newest developer message is the one the model follows, so `note?` answers with it.
|
|
460
|
+
const items = Array.isArray(params.items) ? params.items : [];
|
|
461
|
+
for (const injected of items) {
|
|
462
|
+
const content = Array.isArray(injected.content) ? injected.content[0] : undefined;
|
|
463
|
+
if (injected.role === 'developer' && typeof content?.text === 'string') {
|
|
464
|
+
threadInstructions.set(String(params.threadId), content.text);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
out({ id, result: {} });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
454
470
|
case 'thread/fork': {
|
|
455
471
|
const source = String(params.threadId);
|
|
456
472
|
const turns = threadTurns.get(source) ?? [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.4",
|
|
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.
|
|
85
|
-
"@adecore/intelligent-ui": "0.27.
|
|
84
|
+
"@adecore/agent-contracts": "0.27.4",
|
|
85
|
+
"@adecore/intelligent-ui": "0.27.4",
|
|
86
86
|
"zod": "^4.6.5"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
package/src/chat/backend.ts
CHANGED
|
@@ -35,11 +35,16 @@ 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
|
|
41
43
|
// instructions on a resume; null when there is nothing to repeat.
|
|
42
44
|
resumeNote: string | null;
|
|
45
|
+
// Instructions a resumed thread takes in as a developer message, for a CLI that keeps the ones its thread started
|
|
46
|
+
// with; absent or null when the thread holds them already.
|
|
47
|
+
resumeInstructions?: string | null;
|
|
43
48
|
// The goal the chat last knew of, which a CLI that takes it up again from its own transcript on a resume still works toward.
|
|
44
49
|
goal?: ChatGoal;
|
|
45
50
|
// Folders outside `cwd` the agent may reach without asking. Claude Code takes them as `--add-dir`; Codex's
|
|
@@ -179,6 +184,8 @@ export type BackendEvent =
|
|
|
179
184
|
error?: string;
|
|
180
185
|
native?: { turnId?: string; lastUuid?: string };
|
|
181
186
|
limit?: ChatTurnLimit;
|
|
187
|
+
// The next prompt needs fresh process credentials, once native work and requests have settled.
|
|
188
|
+
replaceProcess?: boolean;
|
|
182
189
|
}
|
|
183
190
|
// The CLI could not be reached or refused the request; the turn ends and the chat needs a new one.
|
|
184
191
|
| { type: 'failed'; message: string; processAlive?: boolean }
|
package/src/chat/chat-core.ts
CHANGED
|
@@ -119,6 +119,8 @@ export interface ChatCoreHooks {
|
|
|
119
119
|
instructionsFor?(chatId: string): string | null;
|
|
120
120
|
/* What of the instructions a resumed thread hears again in front of its next prompt, for a CLI that ignores them on a resume. */
|
|
121
121
|
resumeNoteFor?(chatId: string): string | null;
|
|
122
|
+
/* Instructions a resumed thread takes in again as a developer message, for a CLI that keeps those its thread started with; null when it holds them. */
|
|
123
|
+
resumeInstructionsFor?(chatId: string): string | null;
|
|
122
124
|
/*
|
|
123
125
|
* Folders outside its working directory a chat's agent may reach without asking, absolute. Asked
|
|
124
126
|
* before every turn, so a host can hand one over for as long as it is needed: a change starts the
|
|
@@ -495,6 +497,7 @@ export class ChatCore {
|
|
|
495
497
|
saveGeneratedImage: (ref, upload) => this.attachments.saveGenerated(payload.chatId, ref, upload),
|
|
496
498
|
instructions: () => (this.hooks.instructionsFor ? this.hooks.instructionsFor(payload.chatId) : this.instructions),
|
|
497
499
|
resumeNote: () => this.hooks.resumeNoteFor?.(payload.chatId) ?? null,
|
|
500
|
+
resumeInstructions: () => this.hooks.resumeInstructionsFor?.(payload.chatId) ?? null,
|
|
498
501
|
folders: () => this.hooks.foldersFor?.(payload.chatId) ?? [],
|
|
499
502
|
projectConfig: () => this.hooks.projectConfigFor?.(payload.chatId) ?? true,
|
|
500
503
|
...(promptNotes ? { promptNotes } : {}),
|
package/src/chat/chat-session.ts
CHANGED
|
@@ -76,6 +76,7 @@ export interface ChatSessionOptions {
|
|
|
76
76
|
// What the agent is told at the start of every process, and what of it a resumed thread hears again; see `BackendLaunch`.
|
|
77
77
|
instructions?(): string | null;
|
|
78
78
|
resumeNote?(): string | null;
|
|
79
|
+
resumeInstructions?(): string | null;
|
|
79
80
|
// The folders beside the working directory, asked before every turn; a change starts the next turn in a new process.
|
|
80
81
|
folders?(): readonly string[];
|
|
81
82
|
// False runs the CLI without the configuration of the working folder; asked before every turn like the folders.
|
|
@@ -169,6 +170,7 @@ export class ChatSession {
|
|
|
169
170
|
private readonly options: ChatSessionOptions;
|
|
170
171
|
private readonly projector: ThreadProjector;
|
|
171
172
|
private backend: ChatBackend | null = null;
|
|
173
|
+
private replaceProcess = false;
|
|
172
174
|
private starting: Promise<ChatBackend> | null = null;
|
|
173
175
|
private pendingStart: { turnId: string; warning: unknown } | null = null;
|
|
174
176
|
private sendAfterStop = false;
|
|
@@ -1804,7 +1806,8 @@ export class ChatSession {
|
|
|
1804
1806
|
private requiresReplacement(): boolean {
|
|
1805
1807
|
return (
|
|
1806
1808
|
this.backend !== null &&
|
|
1807
|
-
(
|
|
1809
|
+
(this.replaceProcess ||
|
|
1810
|
+
JSON.stringify(this.info.selection) !== JSON.stringify(this.launchedSelection) ||
|
|
1808
1811
|
this.info.runtimeMode !== this.launchedRuntimeMode ||
|
|
1809
1812
|
this.info.account !== this.launchedAccount ||
|
|
1810
1813
|
this.info.cwd !== this.launchedCwd ||
|
|
@@ -1873,9 +1876,11 @@ export class ChatSession {
|
|
|
1873
1876
|
runtimeMode: info.runtimeMode,
|
|
1874
1877
|
resume: info.agentSessionId,
|
|
1875
1878
|
generation,
|
|
1879
|
+
now: () => this.options.limitResume?.now() ?? (this.options.clock ?? systemClock).now(),
|
|
1876
1880
|
saveGeneratedImage: this.options.saveGeneratedImage,
|
|
1877
1881
|
instructions: this.options.instructions?.() ?? null,
|
|
1878
1882
|
resumeNote: this.options.resumeNote?.() ?? null,
|
|
1883
|
+
resumeInstructions: this.options.resumeInstructions?.() ?? null,
|
|
1879
1884
|
...(info.goal === undefined ? {} : { goal: info.goal }),
|
|
1880
1885
|
...(folders.length > 0 ? { folders } : {}),
|
|
1881
1886
|
...(projectConfig ? {} : { projectConfig }),
|
|
@@ -1895,6 +1900,7 @@ export class ChatSession {
|
|
|
1895
1900
|
}
|
|
1896
1901
|
const backend = made.backend;
|
|
1897
1902
|
this.backend = backend;
|
|
1903
|
+
this.replaceProcess = false;
|
|
1898
1904
|
this.emit([this.thread.patchInfo({ running: true, effectiveRuntimeMode: undefined, permissionMode: undefined })]);
|
|
1899
1905
|
let started: Promise<void>;
|
|
1900
1906
|
try {
|
|
@@ -1978,6 +1984,9 @@ export class ChatSession {
|
|
|
1978
1984
|
if (event.promptId !== undefined) {
|
|
1979
1985
|
this.staleResults = 0;
|
|
1980
1986
|
}
|
|
1987
|
+
if (event.replaceProcess === true) {
|
|
1988
|
+
this.replaceProcess = true;
|
|
1989
|
+
}
|
|
1981
1990
|
const promptId = event.promptId ?? this.submission?.input.promptId;
|
|
1982
1991
|
if (promptId !== undefined) {
|
|
1983
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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -81,7 +81,7 @@ export class CodexBackend implements ChatBackend {
|
|
|
81
81
|
private imageWrites: Promise<void> | null = null;
|
|
82
82
|
private transport: CodexTransport | null = null;
|
|
83
83
|
private threadId = '';
|
|
84
|
-
// Codex
|
|
84
|
+
// Codex ignores developer instructions on `thread/resume` (measured on 0.154 and 0.163), so the first prompt of a resumed process carries them.
|
|
85
85
|
private contextPending: string | null = null;
|
|
86
86
|
private imageInputSupported: boolean | null = null;
|
|
87
87
|
/* A turn asked for that Codex has not named yet; `turn/interrupt` needs that name. */
|
|
@@ -148,7 +148,7 @@ export class CodexBackend implements ChatBackend {
|
|
|
148
148
|
if (this.launch.resume) {
|
|
149
149
|
try {
|
|
150
150
|
result = await transport.request('thread/resume', { threadId: this.launch.resume, excludeTurns: true, ...params });
|
|
151
|
-
this.contextPending = this.launch.
|
|
151
|
+
this.contextPending = await this.resumeContext(transport, this.launch.resume);
|
|
152
152
|
} catch (error) {
|
|
153
153
|
// A refusal means the thread is gone from Codex's store and a fresh one keeps the chat usable; a silence or an exit says nothing about the thread.
|
|
154
154
|
if (!(error instanceof Error) || error.name !== 'CodexRpcError') {
|
|
@@ -172,6 +172,26 @@ export class CodexBackend implements ChatBackend {
|
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/*
|
|
176
|
+
* A developer message outranks the thread's first instructions, where a note in front of the prompt loses to
|
|
177
|
+
* them; an app-server without `thread/inject_items` gets the instructions in front of the prompt after all.
|
|
178
|
+
*/
|
|
179
|
+
private async resumeContext(transport: CodexTransport, threadId: string): Promise<string | null> {
|
|
180
|
+
const instructions = this.launch.resumeInstructions ?? null;
|
|
181
|
+
if (instructions !== null) {
|
|
182
|
+
try {
|
|
183
|
+
const items = [{ type: 'message', role: 'developer', content: [{ type: 'input_text', text: instructions }] }];
|
|
184
|
+
await transport.request('thread/inject_items', { threadId, items });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (!(error instanceof Error) || error.name !== 'CodexRpcError') {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
return [instructions, this.launch.resumeNote].filter((part) => part !== null).join('\n\n');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return this.launch.resumeNote;
|
|
193
|
+
}
|
|
194
|
+
|
|
175
195
|
private async readGoal(transport: CodexTransport): Promise<ChatGoal | null> {
|
|
176
196
|
const result = await transport.request('thread/goal/get', { threadId: this.threadId });
|
|
177
197
|
const goal = codexGoal(isRecord(result) ? result.goal : null);
|
|
@@ -26,6 +26,8 @@ export const fakeCodexForks: Frame[] = [];
|
|
|
26
26
|
export interface FakeCodexOptions {
|
|
27
27
|
// How the note a host repeats in front of a resumed thread's first prompt starts; `pasted?` answers it back.
|
|
28
28
|
resumeLead?: string;
|
|
29
|
+
// False answers `thread/inject_items` the way an app-server without it does.
|
|
30
|
+
injectItems?: boolean;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
|
|
@@ -490,6 +492,22 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
|
|
|
490
492
|
notify('thread/started', { thread });
|
|
491
493
|
return;
|
|
492
494
|
}
|
|
495
|
+
case 'thread/inject_items': {
|
|
496
|
+
if (options.injectItems === false) {
|
|
497
|
+
out({ id, error: { code: -32601, message: 'unknown method thread/inject_items' } });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// The newest developer message is the one the model follows, so `note?` answers with it.
|
|
501
|
+
const items = Array.isArray(params.items) ? (params.items as Frame[]) : [];
|
|
502
|
+
for (const injected of items) {
|
|
503
|
+
const content = Array.isArray(injected.content) ? (injected.content[0] as { text?: unknown } | undefined) : undefined;
|
|
504
|
+
if (injected.role === 'developer' && typeof content?.text === 'string') {
|
|
505
|
+
threadInstructions.set(String(params.threadId), content.text);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
out({ id, result: {} });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
493
511
|
case 'thread/fork': {
|
|
494
512
|
const source = String(params.threadId);
|
|
495
513
|
const turns = threadTurns.get(source) ?? [];
|