@adecore/agents 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/chat/attachment-store.js +4 -5
- package/dist/chat/backend.d.ts +6 -1
- package/dist/chat/background-work.d.ts +1 -1
- package/dist/chat/background-work.js +2 -1
- package/dist/chat/chat-core.d.ts +53 -161
- package/dist/chat/chat-core.js +146 -112
- package/dist/chat/chat-log.js +3 -3
- package/dist/chat/chat-process.d.ts +21 -0
- package/dist/chat/chat-process.js +109 -9
- package/dist/chat/chat-session.d.ts +10 -17
- package/dist/chat/chat-session.js +141 -182
- package/dist/chat/chat-store.d.ts +1 -0
- package/dist/chat/chat-store.js +105 -1
- package/dist/chat/claude-backend.d.ts +4 -0
- package/dist/chat/claude-backend.js +51 -27
- package/dist/chat/claude-protocol.d.ts +12 -0
- package/dist/chat/claude-protocol.js +137 -7
- package/dist/chat/claude-title.d.ts +1 -3
- package/dist/chat/claude-title.js +11 -39
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +36 -17
- package/dist/chat/codex-backend.d.ts +21 -1
- package/dist/chat/codex-backend.js +168 -10
- package/dist/chat/codex-protocol.d.ts +2 -0
- package/dist/chat/codex-protocol.js +37 -3
- package/dist/chat/codex-thread.js +1 -3
- package/dist/chat/codex-transport.d.ts +3 -0
- package/dist/chat/codex-transport.js +38 -35
- package/dist/chat/context-breakdown.js +2 -2
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.js +2 -2
- package/dist/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/dist/chat/index.d.ts +29 -0
- package/dist/chat/index.js +23 -0
- package/dist/chat/projector.d.ts +2 -1
- package/dist/chat/projector.js +12 -3
- package/dist/chat/request-summary.js +1 -4
- package/dist/chat/subagent-settlement.js +1 -3
- package/dist/chat/thread.d.ts +3 -0
- package/dist/chat/thread.js +20 -3
- package/dist/chat/ui-queries.d.ts +2 -0
- package/dist/chat/ui-queries.js +40 -5
- package/dist/chat/ui-replies.d.ts +33 -0
- package/dist/chat/ui-replies.js +152 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +3 -0
- package/dist/coordination/index.d.ts +15 -0
- package/dist/coordination/index.js +16 -0
- package/dist/host/handlers.js +8 -0
- package/dist/host/index.d.ts +7 -0
- package/dist/host/index.js +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/claude.js +3 -1
- package/dist/providers/codex.js +3 -1
- package/dist/providers/index.d.ts +14 -0
- package/dist/providers/index.js +12 -0
- package/dist/run-process.js +54 -14
- package/dist/skills.d.ts +1 -0
- package/dist/skills.js +4 -0
- package/dist/tasks/task-coordinator.d.ts +4 -0
- package/dist/tasks/task-coordinator.js +16 -2
- package/dist/tasks/wiring.js +7 -1
- package/dist/{providers/accounts/test-accounts.d.ts → testing/accounts.d.ts} +2 -2
- package/dist/{providers/accounts/test-accounts.js → testing/accounts.js} +3 -3
- package/dist/testing/bin/fake-claude.d.ts +1 -0
- package/dist/testing/bin/fake-claude.js +6 -0
- package/dist/testing/bin/fake-codex.d.ts +1 -0
- package/dist/testing/bin/fake-codex.js +6 -0
- package/dist/{chat → testing}/fake-claude.d.ts +1 -1
- package/dist/{chat → testing}/fake-claude.js +0 -4
- package/dist/{chat → testing}/fake-cli.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.js +70 -8
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +7 -0
- package/dist/{outbox → testing}/manual-clock.d.ts +1 -1
- package/dist/{watch-test-helpers.d.ts → testing/watch.d.ts} +1 -1
- package/dist/usage/exchange.d.ts +5 -3
- package/dist/usage/exchange.js +25 -16
- package/dist/usage/index-file.d.ts +1 -0
- package/dist/usage/index-file.js +20 -2
- package/dist/usage/index.d.ts +9 -0
- package/dist/usage/index.js +6 -0
- package/dist/usage/limits/normalize.js +1 -3
- package/dist/usage/pricing.d.ts +5 -3
- package/dist/usage/pricing.js +28 -19
- package/dist/usage/scanner.js +39 -11
- package/dist/usage/usage-service.js +8 -3
- package/package.json +72 -11
- package/src/chat/attachment-store.ts +4 -6
- package/src/chat/backend.ts +8 -0
- package/src/chat/background-work.ts +2 -2
- package/src/chat/chat-core.ts +221 -143
- package/src/chat/chat-log.ts +3 -3
- package/src/chat/chat-process.ts +128 -8
- package/src/chat/chat-session.ts +153 -204
- package/src/chat/chat-store.ts +104 -1
- package/src/chat/claude-backend.ts +58 -26
- package/src/chat/claude-protocol.ts +147 -8
- package/src/chat/claude-title.ts +11 -39
- package/src/chat/claude-transcript.ts +38 -18
- package/src/chat/codex-backend.ts +182 -20
- package/src/chat/codex-protocol.ts +40 -5
- package/src/chat/codex-thread.ts +1 -4
- package/src/chat/codex-transport.ts +45 -35
- package/src/chat/context-breakdown.ts +2 -2
- package/src/chat/errors.ts +1 -0
- package/src/chat/external-sessions.ts +2 -2
- package/src/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/src/chat/index.ts +49 -0
- package/src/chat/projector.ts +13 -4
- package/src/chat/request-summary.ts +8 -5
- package/src/chat/subagent-settlement.ts +1 -4
- package/src/chat/thread.ts +22 -3
- package/src/chat/ui-queries.ts +46 -5
- package/src/chat/ui-replies.ts +187 -0
- package/src/context/index.ts +26 -0
- package/src/coordination/index.ts +67 -0
- package/src/host/handlers.ts +8 -0
- package/src/host/index.ts +8 -0
- package/src/index.ts +20 -0
- package/src/providers/accounts/status.ts +1 -4
- package/src/providers/claude.ts +3 -1
- package/src/providers/codex.ts +3 -1
- package/src/providers/index.ts +15 -0
- package/src/run-process.ts +56 -16
- package/src/skills.ts +5 -0
- package/src/tasks/task-coordinator.ts +22 -2
- package/src/tasks/wiring.ts +7 -1
- package/src/{providers/accounts/test-accounts.ts → testing/accounts.ts} +4 -4
- package/src/testing/bin/fake-claude.ts +7 -0
- package/src/testing/bin/fake-codex.ts +7 -0
- package/src/{chat → testing}/fake-claude.ts +1 -5
- package/src/{chat → testing}/fake-cli.ts +1 -1
- package/src/{chat → testing}/fake-codex.ts +75 -6
- package/src/testing/index.ts +7 -0
- package/src/{outbox → testing}/manual-clock.ts +1 -1
- package/src/{watch-test-helpers.ts → testing/watch.ts} +1 -1
- package/src/usage/exchange.ts +31 -20
- package/src/usage/index-file.ts +23 -2
- package/src/usage/index.ts +10 -0
- package/src/usage/limits/normalize.ts +1 -4
- package/src/usage/pricing.ts +34 -23
- package/src/usage/scanner.ts +48 -11
- package/src/usage/usage-service.ts +8 -3
- /package/dist/{chat → testing}/fake-cli.js +0 -0
- /package/dist/{outbox → testing}/manual-clock.js +0 -0
- /package/dist/{watch-test-helpers.js → testing/watch.js} +0 -0
package/src/chat/chat-core.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import {
|
|
4
4
|
ChatAttachmentUploadsSchema,
|
|
5
5
|
generatedImageAttachment,
|
|
6
|
+
HOST_TASK_ORIGIN,
|
|
6
7
|
type AgentKind,
|
|
7
8
|
type ChatAttachment,
|
|
8
9
|
type ChatAttachResult,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
type ChatCreatePayload,
|
|
14
15
|
type ChatEvent,
|
|
15
16
|
type ChatHistoryResult,
|
|
17
|
+
type ChatGoalAction,
|
|
16
18
|
type ChatInfo,
|
|
17
19
|
type ChatItem,
|
|
18
20
|
type ChatQueuedMessage,
|
|
@@ -102,6 +104,57 @@ const DEFAULT_RESUME_WORDS: ResumeWords = {
|
|
|
102
104
|
preamble: null
|
|
103
105
|
};
|
|
104
106
|
|
|
107
|
+
/*
|
|
108
|
+
* What a host adds to its chats. Every hook may be left out, and does nothing without it; none of them
|
|
109
|
+
* knows the client, so a check of who asks belongs on the wire.
|
|
110
|
+
*/
|
|
111
|
+
export interface ChatCoreHooks {
|
|
112
|
+
/* Throws for a folder this chat may not start in; asked every time a chat is loaded, from disk too. */
|
|
113
|
+
admit?(chatId: string, cwd: string): Promise<void>;
|
|
114
|
+
/* The model a new chat starts on when the host already knows; the explicit one still wins. */
|
|
115
|
+
openingSelection?(chatId: string, kind: AgentKind): ModelSelection | undefined;
|
|
116
|
+
/* The mode a chat runs in, which a host may narrow whatever its record or its client says. */
|
|
117
|
+
runtimeModeFor?(chatId: string, mode: RuntimeMode): RuntimeMode;
|
|
118
|
+
/* What the agent is told once at the start of every process of this chat, in place of `instructions`; null for nothing. */
|
|
119
|
+
instructionsFor?(chatId: string): string | null;
|
|
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
|
+
resumeNoteFor?(chatId: string): string | null;
|
|
122
|
+
/*
|
|
123
|
+
* Folders outside its working directory a chat's agent may reach without asking, absolute. Asked
|
|
124
|
+
* before every turn, so a host can hand one over for as long as it is needed: a change starts the
|
|
125
|
+
* next turn in a new process that resumes the thread, and never cuts a turn in flight.
|
|
126
|
+
*/
|
|
127
|
+
foldersFor?(chatId: string): readonly string[];
|
|
128
|
+
/* Whether no list of chats shows this one, which `ChatInfo.hidden` says to every client. */
|
|
129
|
+
hiddenFor?(chatId: string): boolean;
|
|
130
|
+
/* What the host says in front of a chat's next real prompt, beside what the chat keeps itself. */
|
|
131
|
+
promptNotesFor?(chatId: string): PromptNotes | undefined;
|
|
132
|
+
/* How the chats a person attached to a message are named to the agent; without it they are dropped. */
|
|
133
|
+
referencesFor?(chatId: string): ChatReferences | undefined;
|
|
134
|
+
/* The environment of this chat's CLI before its account adds its own. */
|
|
135
|
+
envFor?(chatId: string, base: Record<string, string>): Record<string, string | undefined>;
|
|
136
|
+
/* A chat just loaded or made, its stored turns settled; whatever the host lays down in it goes here. */
|
|
137
|
+
opened?(session: ChatSession, stored: ChatRecord | null): Promise<void>;
|
|
138
|
+
/* What the host keeps in the chat's record beside the thread. */
|
|
139
|
+
recordExtras?(chatId: string): ChatRecordExtras;
|
|
140
|
+
/* A clear is about to start; it waits for this. */
|
|
141
|
+
clearing?(chatId: string): Promise<void>;
|
|
142
|
+
/* The chat was cleared; what runs before the first await is in the record written right after. */
|
|
143
|
+
cleared?(chatId: string): Promise<void>;
|
|
144
|
+
/* The chat left memory for good, before its files go. */
|
|
145
|
+
forgotten?(chatId: string): void;
|
|
146
|
+
/* The chat was removed; what the host keeps of it goes along. */
|
|
147
|
+
removed?(chatId: string, chat: { info: ChatInfo; items: ChatItem[] }): Promise<void>;
|
|
148
|
+
/* An event of a chat went out, after the observers and before the clients reading its thread. */
|
|
149
|
+
broadcasted?(chatId: string, event: ChatEvent): void;
|
|
150
|
+
/* When whoever opened this chat stopped it; a turn from before that is never taken up again. */
|
|
151
|
+
endedAt?(chatId: string): number | null;
|
|
152
|
+
/* Why a turn the host went down in stays down when `onInterruptedRun` says no, in the words of the note it ends with. */
|
|
153
|
+
unownedReason?(chatId: string): string;
|
|
154
|
+
/* How a turn the host went down in is taken up in this chat. */
|
|
155
|
+
resumeWords?(chatId: string): Promise<ResumeWords>;
|
|
156
|
+
}
|
|
157
|
+
|
|
105
158
|
export interface ChatCoreOptions {
|
|
106
159
|
intelligentUi?: ChatUiHost;
|
|
107
160
|
// The info string that marks a UI fence in a reply, for a host that keeps a name of its own.
|
|
@@ -132,7 +185,7 @@ export interface ChatCoreOptions {
|
|
|
132
185
|
// What a running turn says about the plan its CLI runs on; the usage monitor takes it from here.
|
|
133
186
|
onLimits?: (update: LimitsUpdate) => void;
|
|
134
187
|
// Where Claude Code's own name for a session is read, in the projects folder of the chat's account; the other CLIs write none down.
|
|
135
|
-
claudeTitles?: { forSession(agentSessionId: string, projectsDir: string): Promise<string | null> };
|
|
188
|
+
claudeTitles?: { forSession(agentSessionId: string, projectsDir: string, cwd: string): Promise<string | null> };
|
|
136
189
|
// A name for a Codex chat, asked of a one-shot CLI: its app-server names no thread on its own.
|
|
137
190
|
nameChat?: (provider: AgentKind, input: ChatTitleInput) => Promise<string | null>;
|
|
138
191
|
// Where a subagent's whole conversation is read and how its growth is noticed; a test hands in fakes.
|
|
@@ -153,28 +206,33 @@ export interface ChatCoreOptions {
|
|
|
153
206
|
};
|
|
154
207
|
// The accounts a chat may run under; without them every chat runs under its CLI's default account.
|
|
155
208
|
accounts?: AccountLaunches;
|
|
209
|
+
hooks?: ChatCoreHooks;
|
|
156
210
|
}
|
|
157
211
|
|
|
158
212
|
/*
|
|
159
213
|
* The chats of one host: a `ChatSession` per chat, its record and its log, and the clients that read
|
|
160
214
|
* them. Everything here holds for any host. What a host adds (notes for the agent, rows of its own,
|
|
161
|
-
* what a clear or a removal takes along) it adds
|
|
162
|
-
*
|
|
215
|
+
* what a clear or a removal takes along) it adds through `ChatCoreOptions.hooks`. A subclass may add
|
|
216
|
+
* methods of its own over the public ones; the state behind them stays here.
|
|
163
217
|
*/
|
|
164
218
|
export class ChatCore {
|
|
165
|
-
|
|
166
|
-
protected readonly store: ChatStore | null;
|
|
219
|
+
// The stores the host handed in, which a subclass reads to copy a chat's bookmarks or visuals.
|
|
167
220
|
protected readonly bookmarks: BookmarkStore | null;
|
|
168
221
|
protected readonly visuals: VisualStore | null;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
222
|
+
private readonly providers: ProviderRegistry;
|
|
223
|
+
private readonly store: ChatStore | null;
|
|
224
|
+
private readonly accounts: AccountLaunches | null;
|
|
225
|
+
private readonly attachments: AttachmentStore;
|
|
226
|
+
private readonly env: Record<string, string>;
|
|
227
|
+
private readonly hooks: ChatCoreHooks;
|
|
228
|
+
private readonly chats = new Map<string, ChatSession>();
|
|
229
|
+
private readonly creating = new Map<string, Promise<ChatInfo>>();
|
|
230
|
+
private readonly sinks = new ClientSinks<AgentEvent>((clientId) => this.composerPreferences.forget(clientId));
|
|
231
|
+
private readonly attached = new Map<string, Set<string>>();
|
|
232
|
+
// How often a chat's bookmarks or visuals changed, so an attach can tell that its read went stale before it joined.
|
|
233
|
+
private readonly listChanges = new Map<string, number>();
|
|
234
|
+
private readonly coalescers = new Map<string, DeltaCoalescer>();
|
|
235
|
+
private readonly subagents: SubagentReader;
|
|
178
236
|
/* Where Claude Code keeps the projects of its default account on this machine; empty when it has none. */
|
|
179
237
|
readonly claudeProjectsDir: string;
|
|
180
238
|
readonly composerPreferences = new ComposerPreferences();
|
|
@@ -222,6 +280,7 @@ export class ChatCore {
|
|
|
222
280
|
this.nameChat = options.nameChat ?? null;
|
|
223
281
|
this.onInterruptedRun = options.onInterruptedRun ?? null;
|
|
224
282
|
this.limitResume = options.limitResume ?? null;
|
|
283
|
+
this.hooks = options.hooks ?? {};
|
|
225
284
|
this.commands = {
|
|
226
285
|
...(options.command ? { claude: options.command } : {}),
|
|
227
286
|
...(options.codexCommand ? { codex: options.codexCommand } : {})
|
|
@@ -231,11 +290,13 @@ export class ChatCore {
|
|
|
231
290
|
this.env = definedEnv(options.env ?? process.env);
|
|
232
291
|
// Only whoever reads the thread has anything to point a bookmark at or to place a visual in, so the lists go where the thread goes.
|
|
233
292
|
this.bookmarks?.listen((chatId, bookmarks) => {
|
|
293
|
+
this.listChanged(chatId);
|
|
234
294
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
235
295
|
this.sinks.to(clientId, { event: 'chat.bookmarks', payload: { chatId, bookmarks } });
|
|
236
296
|
}
|
|
237
297
|
});
|
|
238
298
|
this.visuals?.listen((chatId, visuals) => {
|
|
299
|
+
this.listChanged(chatId);
|
|
239
300
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
240
301
|
this.sinks.to(clientId, { event: 'chat.visuals', payload: { chatId, visuals } });
|
|
241
302
|
}
|
|
@@ -340,6 +401,11 @@ export class ChatCore {
|
|
|
340
401
|
return this.sinks.subscribe(clientId, sink);
|
|
341
402
|
}
|
|
342
403
|
|
|
404
|
+
/* Sends one client an event of the host's own, such as a change to something it follows that no chat event carries. */
|
|
405
|
+
sendTo(clientId: string, event: AgentEvent): void {
|
|
406
|
+
this.sinks.to(clientId, event);
|
|
407
|
+
}
|
|
408
|
+
|
|
343
409
|
/* Registers a chat; nothing is spawned until the first message. An existing chat answers its current info. */
|
|
344
410
|
create(payload: ChatCreatePayload): Promise<ChatInfo> {
|
|
345
411
|
// A create in flight has the chat in the map before its first prompt is sent, so a second caller waits for all of it.
|
|
@@ -365,19 +431,20 @@ export class ChatCore {
|
|
|
365
431
|
const kind = stored?.info.provider ?? payload.provider ?? 'claude';
|
|
366
432
|
const provider = this.providers.get(kind);
|
|
367
433
|
const catalog = provider.catalog;
|
|
368
|
-
const selection = catalog.normalize(stored?.info.selection ?? this.openingSelection(payload.chatId, kind) ?? payload.selection);
|
|
434
|
+
const selection = catalog.normalize(stored?.info.selection ?? this.hooks.openingSelection?.(payload.chatId, kind) ?? payload.selection);
|
|
369
435
|
const cwd = stored?.info.cwd ?? payload.cwd ?? this.env.HOME ?? homedir();
|
|
370
|
-
await this.admit(payload.chatId, cwd);
|
|
436
|
+
await this.hooks.admit?.(payload.chatId, cwd);
|
|
371
437
|
// A thread on disk keeps its account too. One it lost still opens, and says why on its next turn.
|
|
372
438
|
// A new one without an account takes the person's pick for this CLI, which is refused rather than replaced when it went.
|
|
373
439
|
const account = stored ? stored.info.account : storedAccount(kind, payload.account ?? this.composerPreferences.for(kind).account);
|
|
374
440
|
if (!stored) {
|
|
375
441
|
this.requireAccount(kind, account);
|
|
376
442
|
}
|
|
443
|
+
const narrowed = (mode: RuntimeMode): RuntimeMode => (this.hooks.runtimeModeFor ? this.hooks.runtimeModeFor(payload.chatId, mode) : mode);
|
|
377
444
|
// Asked again for a chat on disk, since the host may have listed it since.
|
|
378
|
-
const hidden = this.hiddenFor(payload.chatId) ? { hidden: true } : {};
|
|
445
|
+
const hidden = this.hooks.hiddenFor?.(payload.chatId) ? { hidden: true } : {};
|
|
379
446
|
const info: ChatInfo = stored?.info
|
|
380
|
-
? { ...withoutHidden(stored.info), runtimeMode:
|
|
447
|
+
? { ...withoutHidden(stored.info), runtimeMode: narrowed(stored.info.runtimeMode), ...hidden }
|
|
381
448
|
: {
|
|
382
449
|
...hidden,
|
|
383
450
|
chatId: payload.chatId,
|
|
@@ -387,7 +454,7 @@ export class ChatCore {
|
|
|
387
454
|
agentSessionId: payload.resume ?? null,
|
|
388
455
|
model: null,
|
|
389
456
|
selection,
|
|
390
|
-
runtimeMode:
|
|
457
|
+
runtimeMode: narrowed(payload.runtimeMode ?? 'full-access'),
|
|
391
458
|
status: 'idle',
|
|
392
459
|
running: false,
|
|
393
460
|
activeTurnId: null,
|
|
@@ -398,10 +465,10 @@ export class ChatCore {
|
|
|
398
465
|
const resume: ResumeDecision = stored ? await this.owedResume(payload.chatId, stored) : { resumeTurnId: null, reason: null };
|
|
399
466
|
const claudeTitles = this.claudeTitles;
|
|
400
467
|
const nameChat = this.nameChat;
|
|
401
|
-
const base = this.envFor(payload.chatId, this.env);
|
|
468
|
+
const base = this.hooks.envFor ? this.hooks.envFor(payload.chatId, this.env) : this.env;
|
|
402
469
|
this.logs.set(payload.chatId, await this.openLog(payload.chatId, stored ?? null));
|
|
403
|
-
const promptNotes = this.promptNotesFor(payload.chatId);
|
|
404
|
-
const references = this.referencesFor(payload.chatId);
|
|
470
|
+
const promptNotes = this.hooks.promptNotesFor?.(payload.chatId);
|
|
471
|
+
const references = this.hooks.referencesFor?.(payload.chatId);
|
|
405
472
|
const session = new ChatSession({
|
|
406
473
|
info,
|
|
407
474
|
...(this.uiFenceLanguage ? { uiFenceLanguage: this.uiFenceLanguage } : {}),
|
|
@@ -418,9 +485,9 @@ export class ChatCore {
|
|
|
418
485
|
return env;
|
|
419
486
|
},
|
|
420
487
|
saveGeneratedImage: (ref, upload) => this.attachments.saveGenerated(payload.chatId, ref, upload),
|
|
421
|
-
instructions: () => this.instructionsFor(payload.chatId),
|
|
422
|
-
resumeNote: () => this.resumeNoteFor(payload.chatId),
|
|
423
|
-
folders: () => this.foldersFor(payload.chatId),
|
|
488
|
+
instructions: () => (this.hooks.instructionsFor ? this.hooks.instructionsFor(payload.chatId) : this.instructions),
|
|
489
|
+
resumeNote: () => this.hooks.resumeNoteFor?.(payload.chatId) ?? null,
|
|
490
|
+
folders: () => this.hooks.foldersFor?.(payload.chatId) ?? [],
|
|
424
491
|
...(promptNotes ? { promptNotes } : {}),
|
|
425
492
|
...(references ? { references } : {}),
|
|
426
493
|
...(this.checkpoints ? { checkpoints: this.checkpoints } : {}),
|
|
@@ -430,7 +497,7 @@ export class ChatCore {
|
|
|
430
497
|
save: () => this.persistNow(payload.chatId),
|
|
431
498
|
persistSoon: () => this.persistSoon(payload.chatId),
|
|
432
499
|
...(kind === 'claude' && claudeTitles
|
|
433
|
-
? { readTitle: (agentSessionId: string) => claudeTitles.forSession(agentSessionId, this.claudeProjectsDirOf(session.info)) }
|
|
500
|
+
? { readTitle: (agentSessionId: string) => claudeTitles.forSession(agentSessionId, this.claudeProjectsDirOf(session.info), session.info.cwd) }
|
|
434
501
|
: {}),
|
|
435
502
|
...(kind === 'codex' && nameChat ? { nameThread: (input: ChatTitleInput) => nameChat(kind, input) } : {}),
|
|
436
503
|
...(kind === 'claude' ? { subagentSettlement: (toolUseId: string) => this.subagents.claudeSettlement(payload.chatId, toolUseId) } : {}),
|
|
@@ -442,11 +509,11 @@ export class ChatCore {
|
|
|
442
509
|
session.settleOwedResume();
|
|
443
510
|
await session.settleOrphanedSubagents();
|
|
444
511
|
}
|
|
445
|
-
await this.opened(session, stored ?? null);
|
|
512
|
+
await this.hooks.opened?.(session, stored ?? null);
|
|
446
513
|
return session.info;
|
|
447
514
|
}
|
|
448
515
|
|
|
449
|
-
async storedBackground(chatId: string): Promise<{ turnId: string | null; items: BackgroundWork[]; commands: string[] }> {
|
|
516
|
+
async storedBackground(chatId: string): Promise<{ turnId: string | null; items: BackgroundWork[]; commands: string[]; goal: boolean }> {
|
|
450
517
|
const record = await this.store?.read(chatId);
|
|
451
518
|
const items = record?.items ?? [];
|
|
452
519
|
const background = items.filter(
|
|
@@ -454,7 +521,12 @@ export class ChatCore {
|
|
|
454
521
|
isBackgroundWork(item) && (runsInBackground(item) || items.some((note) => note.id === `lost-background-${item.id}`))
|
|
455
522
|
);
|
|
456
523
|
const last = items.findLast((item) => item.kind === 'turn');
|
|
457
|
-
return {
|
|
524
|
+
return {
|
|
525
|
+
turnId: background.at(-1)?.turnId ?? last?.id ?? null,
|
|
526
|
+
items: background,
|
|
527
|
+
commands: (record?.info.background ?? []).map(commandLabel),
|
|
528
|
+
goal: record?.info.goal?.status === 'active'
|
|
529
|
+
};
|
|
458
530
|
}
|
|
459
531
|
|
|
460
532
|
save(chatId: string): Promise<void> {
|
|
@@ -526,9 +598,10 @@ export class ChatCore {
|
|
|
526
598
|
}
|
|
527
599
|
|
|
528
600
|
/*
|
|
529
|
-
* `attach` with the chat's bookmarks and visuals. They are read before the client joins,
|
|
530
|
-
*
|
|
531
|
-
* between the two. A file that cannot be
|
|
601
|
+
* `attach` with the chat's bookmarks and visuals. They are read before the client joins, and
|
|
602
|
+
* read again when either changed during the read, so a change reaches the client in the answer
|
|
603
|
+
* or as `chat.bookmarks` or `chat.visuals` and never falls between the two. A file that cannot be
|
|
604
|
+
* read leaves its list out rather than the thread.
|
|
532
605
|
*/
|
|
533
606
|
async attachWithBookmarks(chatId: string, clientId: string, historyLimit?: number, since?: number): Promise<ChatAttachResult> {
|
|
534
607
|
this.require(chatId);
|
|
@@ -536,14 +609,24 @@ export class ChatCore {
|
|
|
536
609
|
console.error(`The ${what} of chat ${chatId} could not be read:`, errorText(e));
|
|
537
610
|
return undefined;
|
|
538
611
|
};
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
612
|
+
let bookmarks: ChatBookmark[] | undefined;
|
|
613
|
+
let visuals: ChatVisual[] | undefined;
|
|
614
|
+
let changes: number | undefined;
|
|
615
|
+
do {
|
|
616
|
+
changes = this.listChanges.get(chatId);
|
|
617
|
+
[bookmarks, visuals] = await Promise.all([
|
|
618
|
+
this.bookmarks?.read(chatId).catch(unreadable('bookmarks')),
|
|
619
|
+
this.visuals?.list(chatId).catch(unreadable('visuals'))
|
|
620
|
+
]);
|
|
621
|
+
} while (this.listChanges.get(chatId) !== changes);
|
|
543
622
|
const result = this.attach(chatId, clientId, historyLimit, since);
|
|
544
623
|
return { ...result, ...(bookmarks === undefined ? {} : { bookmarks }), ...(visuals === undefined ? {} : { visuals }) };
|
|
545
624
|
}
|
|
546
625
|
|
|
626
|
+
private listChanged(chatId: string): void {
|
|
627
|
+
this.listChanges.set(chatId, (this.listChanges.get(chatId) ?? 0) + 1);
|
|
628
|
+
}
|
|
629
|
+
|
|
547
630
|
/* Marks a message of the chat's own thread; a message that already has one keeps it. */
|
|
548
631
|
addBookmark(chatId: string, itemId: string, name?: string): Promise<ChatBookmark[]> {
|
|
549
632
|
const item = this.require(chatId).thread.get(itemId);
|
|
@@ -601,7 +684,8 @@ export class ChatCore {
|
|
|
601
684
|
return this.visuals;
|
|
602
685
|
}
|
|
603
686
|
|
|
604
|
-
|
|
687
|
+
/* The page of a chat's thread before `cursor`, or its newest page without one. */
|
|
688
|
+
history(chatId: string, cursor?: string, limit?: number): ChatHistoryResult {
|
|
605
689
|
const session = this.require(chatId);
|
|
606
690
|
this.coalescers.get(chatId)?.flush();
|
|
607
691
|
return session.thread.history(limit, cursor);
|
|
@@ -654,7 +738,7 @@ export class ChatCore {
|
|
|
654
738
|
if (row.status !== 'running') {
|
|
655
739
|
return;
|
|
656
740
|
}
|
|
657
|
-
if (row.origin ===
|
|
741
|
+
if (row.origin === HOST_TASK_ORIGIN) {
|
|
658
742
|
throw new ChatError('chat-unsupported', 'This row stands for work the host runs; stop it there');
|
|
659
743
|
}
|
|
660
744
|
session.stopSubagent(toolUseId);
|
|
@@ -666,6 +750,11 @@ export class ChatCore {
|
|
|
666
750
|
this.require(chatId).stopTask(taskId);
|
|
667
751
|
}
|
|
668
752
|
|
|
753
|
+
async goal(chatId: string, action: ChatGoalAction): Promise<void> {
|
|
754
|
+
await this.loaded(chatId);
|
|
755
|
+
await this.require(chatId).goal(action);
|
|
756
|
+
}
|
|
757
|
+
|
|
669
758
|
/* The whole conversation of a subagent of the CLI's own, for an agent that reads it as text. */
|
|
670
759
|
async subagentItems(chatId: string, toolUseId: string): Promise<ChatItem[]> {
|
|
671
760
|
return this.subagents.readAll(chatId, toolUseId);
|
|
@@ -676,6 +765,59 @@ export class ChatCore {
|
|
|
676
765
|
return this.chats.has(chatId) || (await this.store?.has(chatId)) === true;
|
|
677
766
|
}
|
|
678
767
|
|
|
768
|
+
/*
|
|
769
|
+
* Writes the whole record of a chat nobody has loaded, such as a fork's thread before anyone opens it.
|
|
770
|
+
* The files its messages and generated images point at are copied into its own folder, so removing
|
|
771
|
+
* the chat they came from leaves them.
|
|
772
|
+
*/
|
|
773
|
+
async writeRecord(chatId: string, info: ChatInfo, items: ChatItem[], preambles: string[] = []): Promise<void> {
|
|
774
|
+
if (!this.store) {
|
|
775
|
+
throw new ChatError('chat-unsupported', 'This host keeps no chat records');
|
|
776
|
+
}
|
|
777
|
+
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
778
|
+
throw new ChatError('chat-busy', `Chat ${chatId} is already loaded`);
|
|
779
|
+
}
|
|
780
|
+
const copies = new Map<string, Promise<ChatAttachment>>();
|
|
781
|
+
const copy = (attachment: ChatAttachment): Promise<ChatAttachment> => {
|
|
782
|
+
let pending = copies.get(attachment.id);
|
|
783
|
+
if (!pending) {
|
|
784
|
+
pending = this.attachments.copy(chatId, attachment);
|
|
785
|
+
copies.set(attachment.id, pending);
|
|
786
|
+
}
|
|
787
|
+
return pending;
|
|
788
|
+
};
|
|
789
|
+
const copied = await Promise.all(
|
|
790
|
+
items.map(async (item): Promise<ChatItem> => {
|
|
791
|
+
if (item.kind === 'user' && item.attachments?.length) {
|
|
792
|
+
return { ...item, attachments: await Promise.all(item.attachments.map(copy)) };
|
|
793
|
+
}
|
|
794
|
+
const generated = generatedImageAttachment(item);
|
|
795
|
+
if (item.kind === 'tool' && generated) {
|
|
796
|
+
return { ...item, input: { ...(item.input as Record<string, unknown>), attachment: await copy(generated) } };
|
|
797
|
+
}
|
|
798
|
+
return item;
|
|
799
|
+
})
|
|
800
|
+
);
|
|
801
|
+
await this.store.write(chatId, info, copied, { seq: 0, resetSeq: 0 }, preambles);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/*
|
|
805
|
+
* Removes what a chat nobody loaded keeps on disk: its record and log, attachments, bookmarks and
|
|
806
|
+
* visuals. False, with nothing removed, for a chat that is loaded or loading; `kill` removes that one.
|
|
807
|
+
*/
|
|
808
|
+
async deleteRecord(chatId: string): Promise<boolean> {
|
|
809
|
+
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
await Promise.all([
|
|
813
|
+
this.store?.delete(chatId),
|
|
814
|
+
this.attachments.removeAll(chatId),
|
|
815
|
+
this.bookmarks?.removeChat(chatId),
|
|
816
|
+
this.visuals?.removeChat(chatId)
|
|
817
|
+
]);
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
|
|
679
821
|
/* The turn a chat took last, read off the record without loading a chat nobody has; null before its first. */
|
|
680
822
|
async lastTurn(chatId: string): Promise<ChatTurnItem | null> {
|
|
681
823
|
const session = this.chats.get(chatId);
|
|
@@ -797,6 +939,14 @@ export class ChatCore {
|
|
|
797
939
|
return session.skills(() => this.skillIndex.list(session.info.provider, session.info.cwd));
|
|
798
940
|
}
|
|
799
941
|
|
|
942
|
+
/* Ends the chat's CLI and keeps its conversation; the next message resumes it in a process that reads its skills and plugins again. */
|
|
943
|
+
async restart(chatId: string): Promise<void> {
|
|
944
|
+
const session = this.require(chatId);
|
|
945
|
+
const ended = session.restart();
|
|
946
|
+
this.skillIndex.forget(session.info.provider, session.info.cwd);
|
|
947
|
+
await ended;
|
|
948
|
+
}
|
|
949
|
+
|
|
800
950
|
compact(chatId: string): void {
|
|
801
951
|
const session = this.require(chatId);
|
|
802
952
|
if (session.busy) {
|
|
@@ -811,10 +961,10 @@ export class ChatCore {
|
|
|
811
961
|
if (session.busy && !force) {
|
|
812
962
|
throw new ChatError('chat-busy', `Chat ${chatId} is still working on the previous message`);
|
|
813
963
|
}
|
|
814
|
-
await this.clearing(chatId);
|
|
964
|
+
await this.hooks.clearing?.(chatId);
|
|
815
965
|
const ended = session.clear(force);
|
|
816
966
|
// Before the record is written, so what it takes along from the record is gone from the write as well.
|
|
817
|
-
const cleared = this.cleared(chatId);
|
|
967
|
+
const cleared = this.hooks.cleared?.(chatId);
|
|
818
968
|
// A debounced write still waiting holds the old thread and must not land after the empty one.
|
|
819
969
|
this.cancelWaiting(chatId);
|
|
820
970
|
// Folded right away: every line before the reset describes a thread that is gone.
|
|
@@ -859,6 +1009,17 @@ export class ChatCore {
|
|
|
859
1009
|
}
|
|
860
1010
|
}
|
|
861
1011
|
|
|
1012
|
+
/* Holds a chat at `needs-you` for what the host asks a person; false for a chat that is not here. */
|
|
1013
|
+
holdForPerson(chatId: string, key: string): boolean {
|
|
1014
|
+
const session = this.chats.get(chatId);
|
|
1015
|
+
session?.holdForPerson(key);
|
|
1016
|
+
return session !== undefined;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
releaseHold(chatId: string, key: string): void {
|
|
1020
|
+
this.chats.get(chatId)?.releaseHold(key);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
862
1023
|
/* Removes a chat: its CLI, its record, its log, its attachments, its bookmarks and its visuals. */
|
|
863
1024
|
async kill(chatId: string): Promise<void> {
|
|
864
1025
|
await this.loaded(chatId);
|
|
@@ -879,9 +1040,10 @@ export class ChatCore {
|
|
|
879
1040
|
this.activity.delete(chatId);
|
|
880
1041
|
this.chats.delete(chatId);
|
|
881
1042
|
this.attached.delete(chatId);
|
|
1043
|
+
this.listChanges.delete(chatId);
|
|
882
1044
|
this.announced.delete(chatId);
|
|
883
1045
|
const snapshot = session.thread.snapshot();
|
|
884
|
-
this.forgotten(chatId);
|
|
1046
|
+
this.hooks.forgotten?.(chatId);
|
|
885
1047
|
await writing;
|
|
886
1048
|
await ended;
|
|
887
1049
|
await Promise.all([
|
|
@@ -889,7 +1051,7 @@ export class ChatCore {
|
|
|
889
1051
|
this.attachments.removeAll(chatId),
|
|
890
1052
|
this.bookmarks?.removeChat(chatId),
|
|
891
1053
|
this.visuals?.removeChat(chatId),
|
|
892
|
-
this.removed(chatId, snapshot)
|
|
1054
|
+
this.hooks.removed?.(chatId, snapshot)
|
|
893
1055
|
]);
|
|
894
1056
|
}
|
|
895
1057
|
|
|
@@ -935,7 +1097,7 @@ export class ChatCore {
|
|
|
935
1097
|
}
|
|
936
1098
|
await this.create({ chatId });
|
|
937
1099
|
}
|
|
938
|
-
await this.chats.get(chatId)?.resume(turnId, attempt, await this.resumeWords(chatId));
|
|
1100
|
+
await this.chats.get(chatId)?.resume(turnId, attempt, (await this.hooks.resumeWords?.(chatId)) ?? DEFAULT_RESUME_WORDS);
|
|
939
1101
|
}
|
|
940
1102
|
|
|
941
1103
|
/*
|
|
@@ -955,7 +1117,7 @@ export class ChatCore {
|
|
|
955
1117
|
return;
|
|
956
1118
|
}
|
|
957
1119
|
const turn = session.thread.get(turnId);
|
|
958
|
-
const ended = this.endedAt(chatId);
|
|
1120
|
+
const ended = this.hooks.endedAt?.(chatId) ?? null;
|
|
959
1121
|
if (turn !== undefined && ended !== null && ended >= turn.createdAt) {
|
|
960
1122
|
session.lapseResume();
|
|
961
1123
|
return;
|
|
@@ -988,6 +1150,10 @@ export class ChatCore {
|
|
|
988
1150
|
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
989
1151
|
continue;
|
|
990
1152
|
}
|
|
1153
|
+
// Parsing the whole thread of every settled chat was most of the cost of a start.
|
|
1154
|
+
if (await this.store.settled(chatId).catch(() => false)) {
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
991
1157
|
const record = await this.store.read(chatId).catch(() => null);
|
|
992
1158
|
const unowed = record?.info.resumeAt !== undefined && this.limitResume?.owed(chatId) === false;
|
|
993
1159
|
if (record !== null && (interruptedTurn(record) !== null || unowed)) {
|
|
@@ -1040,7 +1206,7 @@ export class ChatCore {
|
|
|
1040
1206
|
items,
|
|
1041
1207
|
{ seq: log?.seq ?? 0, resetSeq: log?.resetSeq ?? 0 },
|
|
1042
1208
|
session.preambles,
|
|
1043
|
-
this.recordExtras(chatId),
|
|
1209
|
+
this.hooks.recordExtras?.(chatId) ?? {},
|
|
1044
1210
|
session.uiAccessRecord
|
|
1045
1211
|
);
|
|
1046
1212
|
} catch (e) {
|
|
@@ -1059,11 +1225,6 @@ export class ChatCore {
|
|
|
1059
1225
|
await this.creating.get(chatId)?.catch(() => undefined);
|
|
1060
1226
|
}
|
|
1061
1227
|
|
|
1062
|
-
protected persist(chatId: string): void {
|
|
1063
|
-
this.cancelWaiting(chatId);
|
|
1064
|
-
void this.persistNow(chatId).catch((e) => console.error(`Chat record for ${chatId} failed:`, errorText(e)));
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
1228
|
protected require(chatId: string): ChatSession {
|
|
1068
1229
|
const session = this.chats.get(chatId);
|
|
1069
1230
|
if (!session) {
|
|
@@ -1072,93 +1233,9 @@ export class ChatCore {
|
|
|
1072
1233
|
return session;
|
|
1073
1234
|
}
|
|
1074
1235
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
/* What of the instructions a resumed thread hears again in front of its next prompt, for a CLI that ignores them on a resume. */
|
|
1081
|
-
protected resumeNoteFor(_chatId: string): string | null {
|
|
1082
|
-
return null;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
/*
|
|
1086
|
-
* Folders outside its working directory a chat's agent may reach without asking, absolute. Asked
|
|
1087
|
-
* before every turn, so a host can hand one over for as long as it is needed: a change starts the
|
|
1088
|
-
* next turn in a new process that resumes the thread, and never cuts a turn in flight.
|
|
1089
|
-
*/
|
|
1090
|
-
protected foldersFor(_chatId: string): readonly string[] {
|
|
1091
|
-
return [];
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
/* Whether no list of chats shows this one, which `ChatInfo.hidden` says to every client. */
|
|
1095
|
-
protected hiddenFor(_chatId: string): boolean {
|
|
1096
|
-
return false;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/* What the host says in front of a chat's next real prompt, beside what the chat keeps itself. */
|
|
1100
|
-
protected promptNotesFor(_chatId: string): PromptNotes | undefined {
|
|
1101
|
-
return undefined;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
/* How the chats a person attached to a message are named to the agent; without it they are dropped. */
|
|
1105
|
-
protected referencesFor(_chatId: string): ChatReferences | undefined {
|
|
1106
|
-
return undefined;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
/* The environment of this chat's CLI before its account adds its own. */
|
|
1110
|
-
protected envFor(_chatId: string, base: Record<string, string>): Record<string, string | undefined> {
|
|
1111
|
-
return base;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/* Throws for a folder this chat may not start in; asked every time a chat is loaded. */
|
|
1115
|
-
protected async admit(_chatId: string, _cwd: string): Promise<void> {}
|
|
1116
|
-
|
|
1117
|
-
/* The model a new chat starts on when the host already knows; the explicit one still wins. */
|
|
1118
|
-
protected openingSelection(_chatId: string, _kind: AgentKind): ModelSelection | undefined {
|
|
1119
|
-
return undefined;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
/* The mode a chat runs in, which a host may narrow whatever its record or its client says. */
|
|
1123
|
-
protected runtimeModeFor(_chatId: string, mode: RuntimeMode): RuntimeMode {
|
|
1124
|
-
return mode;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
/* A chat just loaded or made, its stored turns settled; whatever the host lays down in it goes here. */
|
|
1128
|
-
protected async opened(_session: ChatSession, _stored: ChatRecord | null): Promise<void> {}
|
|
1129
|
-
|
|
1130
|
-
/* What the host keeps in the chat's record beside the thread. */
|
|
1131
|
-
protected recordExtras(_chatId: string): ChatRecordExtras {
|
|
1132
|
-
return {};
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
/* The chat was cleared; what runs before the first await is in the record written right after. */
|
|
1136
|
-
protected async cleared(_chatId: string): Promise<void> {}
|
|
1137
|
-
|
|
1138
|
-
protected async clearing(_chatId: string): Promise<void> {}
|
|
1139
|
-
|
|
1140
|
-
/* The chat left memory for good, before its files go. */
|
|
1141
|
-
protected forgotten(_chatId: string): void {}
|
|
1142
|
-
|
|
1143
|
-
/* The chat was removed; what the host keeps of it goes along. */
|
|
1144
|
-
protected async removed(_chatId: string, _chat: { info: ChatInfo; items: ChatItem[] }): Promise<void> {}
|
|
1145
|
-
|
|
1146
|
-
/* An event of a chat went out, after the observers and before the clients reading its thread. */
|
|
1147
|
-
protected broadcasted(_chatId: string, _event: ChatEvent): void {}
|
|
1148
|
-
|
|
1149
|
-
/* When whoever opened this chat stopped it; a turn from before that is never taken up again. */
|
|
1150
|
-
protected endedAt(_chatId: string): number | null {
|
|
1151
|
-
return null;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
/* Why a turn the host went down in stays down when `onInterruptedRun` says no, in the words of the note it ends with. */
|
|
1155
|
-
protected unownedReason(_chatId: string): string {
|
|
1156
|
-
return 'nothing holds this chat any more';
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
/* How a turn the host went down in is taken up in this chat. */
|
|
1160
|
-
protected async resumeWords(_chatId: string): Promise<ResumeWords> {
|
|
1161
|
-
return DEFAULT_RESUME_WORDS;
|
|
1236
|
+
private persist(chatId: string): void {
|
|
1237
|
+
this.cancelWaiting(chatId);
|
|
1238
|
+
void this.persistNow(chatId).catch((e) => console.error(`Chat record for ${chatId} failed:`, errorText(e)));
|
|
1162
1239
|
}
|
|
1163
1240
|
|
|
1164
1241
|
/* A write that may wait: a long thread is not rewritten for every small change. */
|
|
@@ -1200,7 +1277,7 @@ export class ChatCore {
|
|
|
1200
1277
|
}
|
|
1201
1278
|
// A delta held back is already in the thread, so it gets its seq before the snapshot says where it ends.
|
|
1202
1279
|
this.coalescers.get(chatId)?.flush();
|
|
1203
|
-
const extras = this.recordExtras(chatId);
|
|
1280
|
+
const extras = this.hooks.recordExtras?.(chatId) ?? {};
|
|
1204
1281
|
const uiAccess = session.uiAccessRecord;
|
|
1205
1282
|
const unlogged = JSON.stringify([session.preambles, extras, uiAccess]);
|
|
1206
1283
|
const folds = fold || log.size > COMPACT_ABOVE_BYTES || !log.onDisk;
|
|
@@ -1250,7 +1327,7 @@ export class ChatCore {
|
|
|
1250
1327
|
for (const sink of this.observers) {
|
|
1251
1328
|
sink({ event: 'chat.event', payload });
|
|
1252
1329
|
}
|
|
1253
|
-
this.broadcasted(chatId, event);
|
|
1330
|
+
this.hooks.broadcasted?.(chatId, event);
|
|
1254
1331
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
1255
1332
|
this.sinks.to(clientId, { event: 'chat.event', payload });
|
|
1256
1333
|
}
|
|
@@ -1273,14 +1350,15 @@ export class ChatCore {
|
|
|
1273
1350
|
*/
|
|
1274
1351
|
private announceStatus(chatId: string, info: ChatInfo): void {
|
|
1275
1352
|
// A limit, the resume owed after it and subagents still at work are what a header shows of an idle chat, so they count as a change too,
|
|
1276
|
-
// and so does a request opening or settling while the chat already waits on another.
|
|
1353
|
+
// and so does a request opening or settling while the chat already waits on another, and a goal set, moving on or gone.
|
|
1277
1354
|
const said = JSON.stringify([
|
|
1278
1355
|
info.status,
|
|
1279
1356
|
info.limit ?? null,
|
|
1280
1357
|
info.resumeAt ?? null,
|
|
1281
1358
|
info.resumeAtReset ?? null,
|
|
1282
1359
|
info.delegating ?? false,
|
|
1283
|
-
info.requests?.map((request) => request.requestId) ?? []
|
|
1360
|
+
info.requests?.map((request) => request.requestId) ?? [],
|
|
1361
|
+
info.goal === undefined ? null : [info.goal.status, info.goal.objective]
|
|
1284
1362
|
]);
|
|
1285
1363
|
if (this.announced.get(chatId) === said) {
|
|
1286
1364
|
return;
|
|
@@ -1322,7 +1400,7 @@ export class ChatCore {
|
|
|
1322
1400
|
if (stored.info.agentSessionId === null) {
|
|
1323
1401
|
return skip('the agent had not started a session to resume yet');
|
|
1324
1402
|
}
|
|
1325
|
-
const ended = this.endedAt(chatId);
|
|
1403
|
+
const ended = this.hooks.endedAt?.(chatId) ?? null;
|
|
1326
1404
|
if (ended !== null && ended >= turn.createdAt) {
|
|
1327
1405
|
return skip('the agent that opened it was stopped');
|
|
1328
1406
|
}
|
|
@@ -1331,7 +1409,7 @@ export class ChatCore {
|
|
|
1331
1409
|
}
|
|
1332
1410
|
try {
|
|
1333
1411
|
const owed = await this.onInterruptedRun({ chatId, turnId: turn.id, attempt: (turn.attempt ?? 1) + (turn.resumePending ? 0 : 1) });
|
|
1334
|
-
return owed ? { resumeTurnId: turn.id, reason: null } : skip(this.unownedReason(chatId));
|
|
1412
|
+
return owed ? { resumeTurnId: turn.id, reason: null } : skip(this.hooks.unownedReason?.(chatId) ?? 'nothing holds this chat any more');
|
|
1335
1413
|
} catch (e) {
|
|
1336
1414
|
console.error(`Owing a resume for chat ${chatId} failed:`, errorText(e));
|
|
1337
1415
|
return skip(errorText(e));
|